Spaces:
Running
Running
fix: OG image URL encoding, HLS.js for VnExpress video, GIF onerror no longer hides parent
Browse files
app.py
CHANGED
|
@@ -46,6 +46,10 @@ def strip_links(html):
|
|
| 46 |
return re.sub(r'</a>', '', re.sub(r'<a\s[^>]*>', '', html))
|
| 47 |
def esc(text):
|
| 48 |
return text.replace("\\", "\\\\").replace("'", "\\'").replace('"', '\\"').replace("\n", " ")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
def make_id(url):
|
| 50 |
return hashlib.md5(url.encode()).hexdigest()[:12]
|
| 51 |
def slug(text):
|
|
@@ -119,7 +123,8 @@ def _fetch_bdp_video(embed_src):
|
|
| 119 |
video = soup.select_one("video")
|
| 120 |
if source:
|
| 121 |
return {"type": "video", "src": source.get("src", ""),
|
| 122 |
-
"poster": video.get("poster", "") if video else ""
|
|
|
|
| 123 |
except Exception:
|
| 124 |
pass
|
| 125 |
return None
|
|
@@ -167,9 +172,12 @@ def scrape_vne_article(url):
|
|
| 167 |
# Check for video inside <p>
|
| 168 |
vid = ch.find("video")
|
| 169 |
if vid:
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
| 173 |
continue
|
| 174 |
im = ch.find("img")
|
| 175 |
if im:
|
|
@@ -181,10 +189,13 @@ def scrape_vne_article(url):
|
|
| 181 |
# Check for video in figure
|
| 182 |
vid = ch.find("video")
|
| 183 |
if vid:
|
| 184 |
-
|
|
|
|
| 185 |
cap = ch.find("figcaption")
|
| 186 |
-
if
|
| 187 |
-
|
|
|
|
|
|
|
| 188 |
"caption": cap.get_text(strip=True) if cap else ""})
|
| 189 |
continue
|
| 190 |
im = ch.find("img"); cap = ch.find("figcaption")
|
|
@@ -297,7 +308,7 @@ def _list_card(art, big):
|
|
| 297 |
img_html = ""
|
| 298 |
if art.get("img"):
|
| 299 |
c = "bdp-card-img bdp-card-img-big" if big else "bdp-card-img"
|
| 300 |
-
img_html = f'<div class="{c}"><img src="{art["img"]}" alt="{esc(art["title"])}" onerror="this.
|
| 301 |
time_html = f'<span class="bdp-card-time">🕐 {art["time"]}</span>' if art.get("time") else ""
|
| 302 |
summ_html = f'<p class="bdp-card-summ">{art["summary"][:120]}...</p>' if art.get("summary") and len(art["summary"]) > 10 else ""
|
| 303 |
link = art.get("link", "#"); aid = make_id(link)
|
|
@@ -305,9 +316,9 @@ def _list_card(art, big):
|
|
| 305 |
grp = art.get("group", ""); badge = ""
|
| 306 |
if art.get("source") == "vne": badge = f'<span class="bdp-badge bdp-badge-vne">{grp or "VnExpress"}</span>'
|
| 307 |
elif art.get("source") == "bdp": badge = f'<span class="bdp-badge bdp-badge-bdp">{grp or "BongDaPlus"}</span>'
|
| 308 |
-
title_slug = slug(art["title"]); img_url =
|
| 309 |
-
share_js = f"event.stopPropagation();window.bdpShare('{esc(art['title'])}','{aid}','{title_slug}','{img_url}')"
|
| 310 |
-
click_js = f"window.bdpOpen('{esc(link)}','{aid}','{title_slug}','{img_url}')"
|
| 311 |
return f"""<div class="bdp-card" onclick="{click_js}">
|
| 312 |
{img_html}<div class="bdp-card-body">{badge}<h3 class="{tc}">{art['title']}</h3>
|
| 313 |
{summ_html}<div class="bdp-card-footer">{time_html}
|
|
@@ -315,7 +326,7 @@ def _list_card(art, big):
|
|
| 315 |
|
| 316 |
def render_article_html(article):
|
| 317 |
aid = make_id(article["source_url"]); title_slug = slug(article["title"])
|
| 318 |
-
og_img = article.get("og_image", "")
|
| 319 |
share_js = f"window.bdpShare('{esc(article['title'])}','{aid}','{title_slug}','{esc(og_img)}')"
|
| 320 |
src_label = "VnExpress" if article.get("source") == "vne" else "BongDaPlus"
|
| 321 |
seo = f'<div style="display:none" itemscope itemtype="https://schema.org/NewsArticle"><meta itemprop="headline" content="{esc(article["title"])}"><meta itemprop="datePublished" content="{article.get("time","")}"><meta itemprop="publisher" content="{src_label}"><meta itemprop="image" content="{og_img}"><meta itemprop="description" content="{esc(article.get("summary","")[:160])}"></div>'
|
|
@@ -326,16 +337,33 @@ def render_article_html(article):
|
|
| 326 |
if article.get("summary"): parts.append(f'<div class="bdp-article-summary">{article["summary"]}</div>')
|
| 327 |
for item in article.get("body", []):
|
| 328 |
if item["type"] == "video":
|
| 329 |
-
poster = item.get("poster", "")
|
| 330 |
poster_attr = f' poster="{poster}"' if poster else ""
|
| 331 |
caption = item.get("caption", "")
|
| 332 |
cap_html = f'<p class="bdp-figcap">{caption}</p>' if caption else ""
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
<video controls playsinline preload="metadata"{poster_attr} class="bdp-video">
|
| 335 |
-
<source src="{
|
| 336 |
elif item["type"] == "img":
|
| 337 |
alt = item.get("alt", ""); cap = f'<figcaption class="bdp-figcap">{alt}</figcaption>' if alt else ""
|
| 338 |
-
parts.append(f'<figure class="bdp-figure"><img src="{item["src"]}" alt="{alt}" onerror="this.
|
| 339 |
elif item["type"] == "p": parts.append(f'<p class="bdp-article-p">{item["html"]}</p>')
|
| 340 |
elif item["type"] == "heading": parts.append(f'<h2 class="bdp-article-h2">{item["text"]}</h2>')
|
| 341 |
elif item["type"] == "quote": parts.append(f'<blockquote class="bdp-quote">{item["text"]}</blockquote>')
|
|
@@ -452,6 +480,7 @@ JS_HEAD = """
|
|
| 452 |
<meta name="twitter:card" content="summary_large_image">
|
| 453 |
<meta name="twitter:image" content="" id="tw-image">
|
| 454 |
<link rel="canonical" href="https://bep40-bongdaplus-news.hf.space">
|
|
|
|
| 455 |
<script>
|
| 456 |
(function(){
|
| 457 |
window.bdpOpen=function(url,aid,sl,img){
|
|
|
|
| 46 |
return re.sub(r'</a>', '', re.sub(r'<a\s[^>]*>', '', html))
|
| 47 |
def esc(text):
|
| 48 |
return text.replace("\\", "\\\\").replace("'", "\\'").replace('"', '\\"').replace("\n", " ")
|
| 49 |
+
def esc_url(url):
|
| 50 |
+
"""URL-encode spaces and special chars for safe use in JS/HTML attributes."""
|
| 51 |
+
if not url: return ""
|
| 52 |
+
return url.replace(" ", "%20").replace("'", "%27").replace('"', "%22")
|
| 53 |
def make_id(url):
|
| 54 |
return hashlib.md5(url.encode()).hexdigest()[:12]
|
| 55 |
def slug(text):
|
|
|
|
| 123 |
video = soup.select_one("video")
|
| 124 |
if source:
|
| 125 |
return {"type": "video", "src": source.get("src", ""),
|
| 126 |
+
"poster": video.get("poster", "") if video else "",
|
| 127 |
+
"vtype": "mp4"}
|
| 128 |
except Exception:
|
| 129 |
pass
|
| 130 |
return None
|
|
|
|
| 172 |
# Check for video inside <p>
|
| 173 |
vid = ch.find("video")
|
| 174 |
if vid:
|
| 175 |
+
vsrc = vid.get("src", "")
|
| 176 |
+
vid_id = vid.get("id", "")
|
| 177 |
+
if vsrc:
|
| 178 |
+
vtype = "hls" if "m3u8" in vsrc else "mp4"
|
| 179 |
+
body.append({"type": "video", "src": vsrc, "poster": vid.get("poster", ""),
|
| 180 |
+
"vtype": vtype, "vid_id": vid_id})
|
| 181 |
continue
|
| 182 |
im = ch.find("img")
|
| 183 |
if im:
|
|
|
|
| 189 |
# Check for video in figure
|
| 190 |
vid = ch.find("video")
|
| 191 |
if vid:
|
| 192 |
+
vsrc = vid.get("src", "")
|
| 193 |
+
vid_id = vid.get("id", "")
|
| 194 |
cap = ch.find("figcaption")
|
| 195 |
+
if vsrc:
|
| 196 |
+
vtype = "hls" if "m3u8" in vsrc else "mp4"
|
| 197 |
+
body.append({"type": "video", "src": vsrc, "poster": vid.get("poster", ""),
|
| 198 |
+
"vtype": vtype, "vid_id": vid_id,
|
| 199 |
"caption": cap.get_text(strip=True) if cap else ""})
|
| 200 |
continue
|
| 201 |
im = ch.find("img"); cap = ch.find("figcaption")
|
|
|
|
| 308 |
img_html = ""
|
| 309 |
if art.get("img"):
|
| 310 |
c = "bdp-card-img bdp-card-img-big" if big else "bdp-card-img"
|
| 311 |
+
img_html = f'<div class="{c}"><img src="{esc_url(art["img"])}" alt="{esc(art["title"])}" onerror="this.style.display=\'none\'"></div>'
|
| 312 |
time_html = f'<span class="bdp-card-time">🕐 {art["time"]}</span>' if art.get("time") else ""
|
| 313 |
summ_html = f'<p class="bdp-card-summ">{art["summary"][:120]}...</p>' if art.get("summary") and len(art["summary"]) > 10 else ""
|
| 314 |
link = art.get("link", "#"); aid = make_id(link)
|
|
|
|
| 316 |
grp = art.get("group", ""); badge = ""
|
| 317 |
if art.get("source") == "vne": badge = f'<span class="bdp-badge bdp-badge-vne">{grp or "VnExpress"}</span>'
|
| 318 |
elif art.get("source") == "bdp": badge = f'<span class="bdp-badge bdp-badge-bdp">{grp or "BongDaPlus"}</span>'
|
| 319 |
+
title_slug = slug(art["title"]); img_url = esc_url(art.get("img","") or "")
|
| 320 |
+
share_js = f"event.stopPropagation();window.bdpShare('{esc(art['title'])}','{aid}','{title_slug}','{esc(img_url)}')"
|
| 321 |
+
click_js = f"window.bdpOpen('{esc(link)}','{aid}','{title_slug}','{esc(img_url)}')"
|
| 322 |
return f"""<div class="bdp-card" onclick="{click_js}">
|
| 323 |
{img_html}<div class="bdp-card-body">{badge}<h3 class="{tc}">{art['title']}</h3>
|
| 324 |
{summ_html}<div class="bdp-card-footer">{time_html}
|
|
|
|
| 326 |
|
| 327 |
def render_article_html(article):
|
| 328 |
aid = make_id(article["source_url"]); title_slug = slug(article["title"])
|
| 329 |
+
og_img = esc_url(article.get("og_image", ""))
|
| 330 |
share_js = f"window.bdpShare('{esc(article['title'])}','{aid}','{title_slug}','{esc(og_img)}')"
|
| 331 |
src_label = "VnExpress" if article.get("source") == "vne" else "BongDaPlus"
|
| 332 |
seo = f'<div style="display:none" itemscope itemtype="https://schema.org/NewsArticle"><meta itemprop="headline" content="{esc(article["title"])}"><meta itemprop="datePublished" content="{article.get("time","")}"><meta itemprop="publisher" content="{src_label}"><meta itemprop="image" content="{og_img}"><meta itemprop="description" content="{esc(article.get("summary","")[:160])}"></div>'
|
|
|
|
| 337 |
if article.get("summary"): parts.append(f'<div class="bdp-article-summary">{article["summary"]}</div>')
|
| 338 |
for item in article.get("body", []):
|
| 339 |
if item["type"] == "video":
|
| 340 |
+
poster = esc_url(item.get("poster", ""))
|
| 341 |
poster_attr = f' poster="{poster}"' if poster else ""
|
| 342 |
caption = item.get("caption", "")
|
| 343 |
cap_html = f'<p class="bdp-figcap">{caption}</p>' if caption else ""
|
| 344 |
+
vtype = item.get("vtype", "mp4")
|
| 345 |
+
vid_id = item.get("vid_id", f"vid-{aid}")
|
| 346 |
+
vsrc = item["src"]
|
| 347 |
+
if vtype == "hls":
|
| 348 |
+
# HLS stream: use HLS.js for Chrome/Firefox, native for Safari
|
| 349 |
+
parts.append(f"""<div class="bdp-video-wrap">
|
| 350 |
+
<video id="{vid_id}" controls playsinline preload="metadata"{poster_attr} class="bdp-video"></video>{cap_html}
|
| 351 |
+
<script>
|
| 352 |
+
(function(){{var v=document.getElementById('{vid_id}');if(!v)return;
|
| 353 |
+
var src='{vsrc}';
|
| 354 |
+
if(v.canPlayType('application/vnd.apple.mpegURL')){{v.src=src;}}
|
| 355 |
+
else if(window.Hls&&Hls.isSupported()){{var h=new Hls();h.loadSource(src);h.attachMedia(v);}}
|
| 356 |
+
else{{v.innerHTML='<source src="'+src+'" type="application/x-mpegURL">';}}
|
| 357 |
+
}})();
|
| 358 |
+
</script></div>""")
|
| 359 |
+
else:
|
| 360 |
+
# Direct MP4
|
| 361 |
+
parts.append(f"""<div class="bdp-video-wrap">
|
| 362 |
<video controls playsinline preload="metadata"{poster_attr} class="bdp-video">
|
| 363 |
+
<source src="{vsrc}" type="video/mp4">Video không hỗ trợ.</video>{cap_html}</div>""")
|
| 364 |
elif item["type"] == "img":
|
| 365 |
alt = item.get("alt", ""); cap = f'<figcaption class="bdp-figcap">{alt}</figcaption>' if alt else ""
|
| 366 |
+
parts.append(f'<figure class="bdp-figure"><img src="{esc_url(item["src"])}" alt="{alt}" onerror="this.style.display=\'none\'">{cap}</figure>')
|
| 367 |
elif item["type"] == "p": parts.append(f'<p class="bdp-article-p">{item["html"]}</p>')
|
| 368 |
elif item["type"] == "heading": parts.append(f'<h2 class="bdp-article-h2">{item["text"]}</h2>')
|
| 369 |
elif item["type"] == "quote": parts.append(f'<blockquote class="bdp-quote">{item["text"]}</blockquote>')
|
|
|
|
| 480 |
<meta name="twitter:card" content="summary_large_image">
|
| 481 |
<meta name="twitter:image" content="" id="tw-image">
|
| 482 |
<link rel="canonical" href="https://bep40-bongdaplus-news.hf.space">
|
| 483 |
+
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"></script>
|
| 484 |
<script>
|
| 485 |
(function(){
|
| 486 |
window.bdpOpen=function(url,aid,sl,img){
|