bep40 commited on
Commit
e4080d4
·
verified ·
1 Parent(s): 2ff7eb0

fix shorts_carousel.py ONLY: relax URL filter + 3:4 thumbnail ratio

Browse files
Files changed (1) hide show
  1. shorts_carousel.py +52 -78
shorts_carousel.py CHANGED
@@ -1,4 +1,4 @@
1
- """Self-contained 24h Shorts Carousel + multi-part video extraction."""
2
  import re
3
  import requests
4
  from bs4 import BeautifulSoup
@@ -12,48 +12,46 @@ BASE_24H = "https://www.24h.com.vn"
12
 
13
 
14
  def scrape_24h_news_shorts():
15
- """Scrape https://www.24h.com.vn/video/video-tin-tuc-cvd769.html"""
 
16
  try:
17
  url = f"{BASE_24H}/video/video-tin-tuc-cvd769.html"
18
- r = requests.get(url, headers=HEADERS, timeout=15)
19
  r.encoding = "utf-8"
20
  soup = BeautifulSoup(r.text, "lxml")
21
  articles, seen = [], set()
22
  for a in soup.find_all("a", href=True):
23
  href = a.get("href", "")
24
- if not re.search(r'-c\d+a\d+\.html$', href):
 
 
 
 
 
25
  continue
26
  if not href.startswith("http"):
27
  href = BASE_24H + href
28
  if href in seen:
29
  continue
30
- seen.add(href)
31
- title = a.get("title", "") or ""
32
  img = a.find("img")
33
- if img and not title:
34
- title = img.get("alt", "")
35
- if not title:
36
- title = a.get_text(strip=True)
37
- if not title or len(title) < 10:
 
 
 
38
  continue
39
- img_src = None
40
- if img:
41
- img_src = img.get("data-original") or img.get("data-src") or img.get("src") or ""
42
- if "base64" in img_src or len(img_src) < 20:
43
- img_src = None
44
- if not img_src:
45
- parent = a.parent
46
- if parent:
47
- parent_img = parent.find("img")
48
- if parent_img:
49
- img_src = parent_img.get("data-original") or parent_img.get("data-src") or parent_img.get("src") or ""
50
- if "base64" in img_src or len(img_src) < 20:
51
- img_src = None
52
- if not img_src:
53
  continue
 
54
  articles.append({
55
  "title": title, "link": href, "img": img_src,
56
- "source": "24h", "is_video": True
57
  })
58
  return articles[:20]
59
  except:
@@ -61,80 +59,56 @@ def scrape_24h_news_shorts():
61
 
62
 
63
  def _extract_24h_video_url(article_url):
64
- """Extract m3u8 video URL(s) from a 24h article.
65
- Probes for multi-part videos using CDN naming patterns."""
66
  try:
67
  r = requests.get(article_url, headers=HEADERS, timeout=10)
68
  r.encoding = "utf-8"
69
  html = r.text
70
-
71
  raw_m3u8 = re.findall(r'(https?://cdn\.24h\.com\.vn/[^\s"\'\\<>]+\.m3u8)', html)
72
  escaped_m3u8 = re.findall(r'(https?:\\/\\/cdn\.24h\.com\.vn\\/[^\s"\'<>]+\.m3u8)', html)
73
  escaped_m3u8 = [u.replace('\\/', '/') for u in escaped_m3u8]
74
-
75
  all_m3u8 = list(dict.fromkeys(raw_m3u8 + escaped_m3u8))
76
  full_quality = [u for u in all_m3u8 if '_720p' not in u]
77
  has_720p = [u for u in all_m3u8 if '_720p' in u]
78
-
79
  if not full_quality and not has_720p:
80
  return None
81
-
82
  primary_list = full_quality if full_quality else has_720p
83
  base_url = primary_list[0]
84
-
85
  soup = BeautifulSoup(html, "lxml")
86
  og = soup.find("meta", property="og:image")
87
  poster = og.get("content", "") if og else ""
88
-
89
  if len(primary_list) > 1:
90
  return {"src": primary_list[0], "poster": poster, "vtype": "hls", "all_parts": primary_list}
91
-
92
  parts_found = [base_url]
93
-
94
  is_720p = '_720p.m3u8' in base_url
95
- clean_url = base_url.replace('_720p.m3u8', '.m3u8') if is_720p else base_url
96
- name_part = clean_url.replace('.m3u8', '')
97
-
98
  m = re.search(r'(\D)(\d{1,2})$', name_part)
99
  if m:
100
- current_num = int(m.group(2))
101
- num_width = len(m.group(2))
102
  base_name = name_part[:m.start(2)]
103
- for i in range(current_num + 1, current_num + 5):
104
- next_name = f"{base_name}{i:02d}" if num_width == 2 else f"{base_name}{i}"
105
- for suffix in ['_720p.m3u8' if is_720p else '.m3u8', '_720p.m3u8', '.m3u8']:
106
- probe_url = next_name + suffix
107
- if probe_url in parts_found:
108
- continue
109
  try:
110
- pr = requests.head(probe_url, headers=HEADERS, timeout=3, allow_redirects=True)
111
- if pr.status_code == 200:
112
- parts_found.append(probe_url)
113
- break
114
- except:
115
- continue
116
- else:
117
- break
118
-
119
- if len(parts_found) == 1:
120
  m2 = re.match(r'^(.+-)(\d{7,})(-.+?)(\d{1,2})((?:_720p)?\.m3u8)$', base_url)
121
  if m2:
122
- prefix = m2.group(1)
123
- cdn_id = int(m2.group(2))
124
- mid_part = m2.group(3)
125
- part_num = int(m2.group(4))
126
- suffix = m2.group(5)
127
- for i in range(1, 5):
128
- probe_url = f"{prefix}{cdn_id + i}{mid_part}{part_num + i}{suffix}"
129
  try:
130
- pr = requests.head(probe_url, headers=HEADERS, timeout=3, allow_redirects=True)
131
- if pr.status_code == 200:
132
- parts_found.append(probe_url)
133
- else:
134
- break
135
- except:
136
- break
137
-
138
  if len(parts_found) > 1:
139
  return {"src": parts_found[0], "poster": poster, "vtype": "hls", "all_parts": parts_found}
140
  return {"src": base_url, "poster": poster, "vtype": "hls"}
@@ -143,18 +117,18 @@ def _extract_24h_video_url(article_url):
143
 
144
 
145
  SHORTS_CSS = """
146
- /* Shorts 9:16 Slider */
147
- .vslide-shorts-item{flex:0 0 110px;scroll-snap-align:start;cursor:pointer;transition:transform .15s}
148
  .vslide-shorts-item:hover{transform:scale(1.04)}
149
- @media(min-width:768px){.vslide-shorts-item{flex:0 0 130px}}
150
- .vslide-shorts-thumb{position:relative;width:100%;aspect-ratio:9/16;border-radius:12px;overflow:hidden;background:#222}
151
  .vslide-shorts-thumb img{width:100%;height:100%;object-fit:cover}
152
  .vslide-shorts-title{color:#ccc;font-size:10.5px;margin:5px 0 0;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
153
  """
154
 
155
 
156
  def render_shorts_carousel(shorts, esc_fn, safe_url_fn, make_id_fn, slug_fn):
157
- """Render 9:16 vertical shorts carousel HTML."""
158
  vids = [v for v in shorts if v.get("img")]
159
  if not vids:
160
  return ""
@@ -173,7 +147,7 @@ def render_shorts_carousel(shorts, esc_fn, safe_url_fn, make_id_fn, slug_fn):
173
  + '<p class="vslide-shorts-title">' + title + '</p></div>'
174
  )
175
  h = '<div class="vslide-wrap"><div class="vslide-header">'
176
- h += '<span class="vslide-label">\U0001f4f1 Shorts Tin T\u1ee9c 24h</span>'
177
  h += "<div class=\"vslide-nav\"><button class=\"vslide-btn\" onclick=\"window.bdpSlideScroll(-1,'vslide-shorts')\">\u25c0</button>"
178
  h += "<button class=\"vslide-btn\" onclick=\"window.bdpSlideScroll(1,'vslide-shorts')\">\u25b6</button></div></div>"
179
  h += '<div class="vslide-track" id="vslide-shorts">' + ''.join(items) + '</div></div>'
 
1
+ """24h Shorts Carousel + video extraction."""
2
  import re
3
  import requests
4
  from bs4 import BeautifulSoup
 
12
 
13
 
14
  def scrape_24h_news_shorts():
15
+ """Scrape https://www.24h.com.vn/video/video-tin-tuc-cvd769.html
16
+ Lấy tất cả link bài viết có ảnh thumbnail."""
17
  try:
18
  url = f"{BASE_24H}/video/video-tin-tuc-cvd769.html"
19
+ r = requests.get(url, headers=HEADERS, timeout=12)
20
  r.encoding = "utf-8"
21
  soup = BeautifulSoup(r.text, "lxml")
22
  articles, seen = [], set()
23
  for a in soup.find_all("a", href=True):
24
  href = a.get("href", "")
25
+ # Bỏ qua link javascript, anchor, category
26
+ if not href.endswith(".html"):
27
+ continue
28
+ if "javascript:" in href or href == "#":
29
+ continue
30
+ if "-cvd" in href or "-c953" in href:
31
  continue
32
  if not href.startswith("http"):
33
  href = BASE_24H + href
34
  if href in seen:
35
  continue
36
+ # Phải có ảnh
 
37
  img = a.find("img")
38
+ if not img:
39
+ p = a.parent
40
+ if p:
41
+ img = p.find("img")
42
+ if not img:
43
+ continue
44
+ img_src = img.get("data-original") or img.get("data-src") or img.get("src") or ""
45
+ if not img_src or "base64" in img_src or len(img_src) < 20:
46
  continue
47
+ # Lấy title
48
+ title = img.get("alt","") or a.get("title","") or a.get_text(strip=True)
49
+ if not title or len(title) < 8:
 
 
 
 
 
 
 
 
 
 
 
50
  continue
51
+ seen.add(href)
52
  articles.append({
53
  "title": title, "link": href, "img": img_src,
54
+ "source": "24h-shorts", "is_video": True
55
  })
56
  return articles[:20]
57
  except:
 
59
 
60
 
61
  def _extract_24h_video_url(article_url):
62
+ """Extract m3u8 video URL(s) from a 24h article."""
 
63
  try:
64
  r = requests.get(article_url, headers=HEADERS, timeout=10)
65
  r.encoding = "utf-8"
66
  html = r.text
 
67
  raw_m3u8 = re.findall(r'(https?://cdn\.24h\.com\.vn/[^\s"\'\\<>]+\.m3u8)', html)
68
  escaped_m3u8 = re.findall(r'(https?:\\/\\/cdn\.24h\.com\.vn\\/[^\s"\'<>]+\.m3u8)', html)
69
  escaped_m3u8 = [u.replace('\\/', '/') for u in escaped_m3u8]
 
70
  all_m3u8 = list(dict.fromkeys(raw_m3u8 + escaped_m3u8))
71
  full_quality = [u for u in all_m3u8 if '_720p' not in u]
72
  has_720p = [u for u in all_m3u8 if '_720p' in u]
 
73
  if not full_quality and not has_720p:
74
  return None
 
75
  primary_list = full_quality if full_quality else has_720p
76
  base_url = primary_list[0]
 
77
  soup = BeautifulSoup(html, "lxml")
78
  og = soup.find("meta", property="og:image")
79
  poster = og.get("content", "") if og else ""
 
80
  if len(primary_list) > 1:
81
  return {"src": primary_list[0], "poster": poster, "vtype": "hls", "all_parts": primary_list}
82
+ # Probe multi-part
83
  parts_found = [base_url]
 
84
  is_720p = '_720p.m3u8' in base_url
85
+ name_part = base_url.replace('_720p.m3u8', '').replace('.m3u8', '')
 
 
86
  m = re.search(r'(\D)(\d{1,2})$', name_part)
87
  if m:
88
+ cur = int(m.group(2))
89
+ w = len(m.group(2))
90
  base_name = name_part[:m.start(2)]
91
+ for i in range(cur+1, cur+5):
92
+ nn = f"{base_name}{i:02d}" if w==2 else f"{base_name}{i}"
93
+ for sfx in [('_720p.m3u8' if is_720p else '.m3u8'), '_720p.m3u8', '.m3u8']:
94
+ pu = nn+sfx
95
+ if pu in parts_found: continue
 
96
  try:
97
+ if requests.head(pu, headers=HEADERS, timeout=3, allow_redirects=True).status_code==200:
98
+ parts_found.append(pu); break
99
+ except: continue
100
+ else: break
101
+ if len(parts_found)==1:
 
 
 
 
 
102
  m2 = re.match(r'^(.+-)(\d{7,})(-.+?)(\d{1,2})((?:_720p)?\.m3u8)$', base_url)
103
  if m2:
104
+ pfx,cid,mid,pn,sfx = m2.group(1),int(m2.group(2)),m2.group(3),int(m2.group(4)),m2.group(5)
105
+ for i in range(1,5):
106
+ pu = f"{pfx}{cid+i}{mid}{pn+i}{sfx}"
 
 
 
 
107
  try:
108
+ if requests.head(pu, headers=HEADERS, timeout=3, allow_redirects=True).status_code==200:
109
+ parts_found.append(pu)
110
+ else: break
111
+ except: break
 
 
 
 
112
  if len(parts_found) > 1:
113
  return {"src": parts_found[0], "poster": poster, "vtype": "hls", "all_parts": parts_found}
114
  return {"src": base_url, "poster": poster, "vtype": "hls"}
 
117
 
118
 
119
  SHORTS_CSS = """
120
+ /* Shorts 3:4 Slider */
121
+ .vslide-shorts-item{flex:0 0 120px;scroll-snap-align:start;cursor:pointer;transition:transform .15s}
122
  .vslide-shorts-item:hover{transform:scale(1.04)}
123
+ @media(min-width:768px){.vslide-shorts-item{flex:0 0 140px}}
124
+ .vslide-shorts-thumb{position:relative;width:100%;aspect-ratio:3/4;border-radius:10px;overflow:hidden;background:#222}
125
  .vslide-shorts-thumb img{width:100%;height:100%;object-fit:cover}
126
  .vslide-shorts-title{color:#ccc;font-size:10.5px;margin:5px 0 0;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
127
  """
128
 
129
 
130
  def render_shorts_carousel(shorts, esc_fn, safe_url_fn, make_id_fn, slug_fn):
131
+ """Render 3:4 shorts carousel."""
132
  vids = [v for v in shorts if v.get("img")]
133
  if not vids:
134
  return ""
 
147
  + '<p class="vslide-shorts-title">' + title + '</p></div>'
148
  )
149
  h = '<div class="vslide-wrap"><div class="vslide-header">'
150
+ h += '<span class="vslide-label">\U0001f4f1 Shorts 24h</span>'
151
  h += "<div class=\"vslide-nav\"><button class=\"vslide-btn\" onclick=\"window.bdpSlideScroll(-1,'vslide-shorts')\">\u25c0</button>"
152
  h += "<button class=\"vslide-btn\" onclick=\"window.bdpSlideScroll(1,'vslide-shorts')\">\u25b6</button></div></div>"
153
  h += '<div class="vslide-track" id="vslide-shorts">' + ''.join(items) + '</div></div>'