bep40 commited on
Commit
6646eb5
·
verified ·
1 Parent(s): 5190f38

fix: regex for multi-part probe - use greedy (.+) to match last 2 digits before .m3u8

Browse files
Files changed (1) hide show
  1. shorts_carousel.py +39 -99
shorts_carousel.py CHANGED
@@ -61,51 +61,57 @@ 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
- Tries multiple methods to find ALL video parts."""
66
  try:
67
  r = requests.get(article_url, headers=HEADERS, timeout=10)
68
  r.encoding = "utf-8"
69
  html = r.text
 
 
 
 
 
70
 
71
- # Method 1: Find all m3u8 URLs directly in HTML
72
- all_m3u8 = re.findall(r'(https?://cdn\.24h\.com\.vn/[^\s"\'\\<>]+\.m3u8)', html)
73
- full_quality = list(dict.fromkeys(u for u in all_m3u8 if '_720p' not in u))
74
-
75
  if not full_quality:
76
  return None
77
-
78
  # Get poster
79
  soup = BeautifulSoup(html, "lxml")
80
  og = soup.find("meta", property="og:image")
81
  poster = og.get("content", "") if og else ""
82
-
83
- # Method 2: Check how many playlist items the page has
84
- playlist_count = len(set(re.findall(r'track_slow_playlist_next_(\d+)', html)))
85
-
86
- # Method 3: Try to find playlist JSON in page
87
- # Look for patterns like: sources: [{file:'...'}] or playlist:[{sources:[{file:'...'}]}]
88
- playlist_urls = []
89
-
90
- # Search for all CDN video URLs (including escaped ones)
91
- escaped_m3u8 = re.findall(r'(https?:\\/\\/cdn\.24h\.com\.vn\\/[^\s"\'\\<>]+\.m3u8)', html)
92
- escaped_m3u8 = [u.replace('\\/','/' ) for u in escaped_m3u8]
93
- all_found = list(dict.fromkeys(u for u in (full_quality + escaped_m3u8) if '_720p' not in u))
94
-
95
- if len(all_found) > 1:
96
- # Multiple m3u8 URLs found directly - these ARE the parts
97
- return {"src": all_found[0], "poster": poster, "vtype": "hls", "all_parts": all_found}
98
-
99
- # Method 4: Probe numbered variants
100
  base_url = full_quality[0]
101
  parts_found = [base_url]
 
 
 
 
 
 
102
 
103
- # Try pattern: {name}01.m3u8 → {name}02.m3u8
104
- base_match = re.match(r'(.+?)(\d{1,2})(\.m3u8)$', base_url)
105
- if base_match:
106
- base, num, ext = base_match.group(1), int(base_match.group(2)), base_match.group(3)
107
- for i in range(num + 1, num + 5):
108
- probe_url = f"{base}{i:02d}{ext}" if num >= 10 else f"{base}{i}{ext}"
 
 
 
 
 
 
109
  try:
110
  pr = requests.head(probe_url, headers=HEADERS, timeout=3, allow_redirects=True)
111
  if pr.status_code == 200:
@@ -114,77 +120,11 @@ def _extract_24h_video_url(article_url):
114
  break
115
  except:
116
  break
117
-
118
- # Method 5: Try common naming patterns for hiep/part
119
- if len(parts_found) == 1 and playlist_count > 0:
120
- # The page DOES have a playlist but we can't find URLs
121
- # Try variations of the filename
122
- filename = base_url.rsplit('/', 1)[-1] # e.g. "cp_special_bundesliga-1777224470-dortmund_freiburg01.m3u8"
123
- dir_url = base_url.rsplit('/', 1)[0] + '/'
124
-
125
- # Try: remove trailing digits and probe 1,2,3...
126
- name_no_num = re.sub(r'\d+\.m3u8$', '', filename)
127
- if name_no_num != filename.replace('.m3u8',''):
128
- for i in range(1, 5):
129
- probe = dir_url + name_no_num + str(i) + '.m3u8'
130
- if probe not in parts_found:
131
- try:
132
- pr = requests.head(probe, headers=HEADERS, timeout=3, allow_redirects=True)
133
- if pr.status_code == 200:
134
- parts_found.append(probe)
135
- except:
136
- pass
137
-
138
- # Try: append _part2, _hiep2 etc
139
- base_name = base_url.replace('.m3u8','')
140
- for suffix in ['_2.m3u8', '_part2.m3u8', '_hiep2.m3u8', 'b.m3u8']:
141
- probe = base_name + suffix
142
- try:
143
- pr = requests.head(probe, headers=HEADERS, timeout=3, allow_redirects=True)
144
- if pr.status_code == 200:
145
- parts_found.append(probe)
146
- except:
147
- pass
148
-
149
- # Method 6: Try AJAX endpoint for playlist
150
- article_id_match = re.search(r'a(\d+)\.html', article_url)
151
- if article_id_match and len(parts_found) == 1 and playlist_count > 0:
152
- article_id = article_id_match.group(1)
153
- ajax_urls = [
154
- f"https://24h.24hstatic.com/ajax/video/get_playlist?id={article_id}",
155
- f"{BASE_24H}/ajax/video/playlist/{article_id}",
156
- ]
157
- for ajax_url in ajax_urls:
158
- try:
159
- ar = requests.get(ajax_url, headers={**HEADERS, "X-Requested-With": "XMLHttpRequest"}, timeout=5)
160
- if ar.status_code == 200 and ar.text.strip():
161
- # Try parse as JSON
162
- try:
163
- data = ar.json()
164
- if isinstance(data, list):
165
- for item in data:
166
- if isinstance(item, dict):
167
- src = item.get("file") or item.get("src") or item.get("source") or ""
168
- if src and 'm3u8' in src and src not in parts_found:
169
- parts_found.append(src)
170
- elif isinstance(item, str) and 'm3u8' in item:
171
- if item not in parts_found:
172
- parts_found.append(item)
173
- except:
174
- # Try extract m3u8 from text
175
- ajax_m3u8 = re.findall(r'(https?://[^\s"\'\\<>]+\.m3u8)', ar.text)
176
- for u in ajax_m3u8:
177
- if '_720p' not in u and u not in parts_found:
178
- parts_found.append(u)
179
- if len(parts_found) > 1:
180
- break
181
- except:
182
- pass
183
 
184
  if len(parts_found) > 1:
185
  return {"src": parts_found[0], "poster": poster, "vtype": "hls", "all_parts": parts_found}
186
- else:
187
- return {"src": parts_found[0], "poster": poster, "vtype": "hls"}
188
  except:
189
  return None
190
 
 
61
 
62
 
63
  def _extract_24h_video_url(article_url):
64
+ """Extract m3u8 video URL(s) from a 24h article.
65
+ Returns dict with 'src', 'poster', 'vtype', and optionally 'all_parts' list."""
66
  try:
67
  r = requests.get(article_url, headers=HEADERS, timeout=10)
68
  r.encoding = "utf-8"
69
  html = r.text
70
+
71
+ # Find all m3u8 URLs in HTML (including escaped ones like \/)
72
+ raw_m3u8 = re.findall(r'(https?://cdn\.24h\.com\.vn/[^\s"\'\\<>]+\.m3u8)', html)
73
+ escaped_m3u8 = re.findall(r'(https?:\\/\\/cdn\.24h\.com\.vn\\/[^\s"\'<>]+\.m3u8)', html)
74
+ escaped_m3u8 = [u.replace('\\/', '/') for u in escaped_m3u8]
75
 
76
+ all_m3u8 = list(dict.fromkeys(raw_m3u8 + escaped_m3u8))
77
+ full_quality = [u for u in all_m3u8 if '_720p' not in u]
78
+
 
79
  if not full_quality:
80
  return None
81
+
82
  # Get poster
83
  soup = BeautifulSoup(html, "lxml")
84
  og = soup.find("meta", property="og:image")
85
  poster = og.get("content", "") if og else ""
86
+
87
+ # If multiple full-quality URLs found directly those are the parts
88
+ if len(full_quality) > 1:
89
+ return {"src": full_quality[0], "poster": poster, "vtype": "hls", "all_parts": full_quality}
90
+
91
+ # Probe for numbered parts using GREEDY regex
92
+ # Example: ...psg_bayern01.m3u8 → base="...psg_bayern", num="01"
93
+ # Use GREEDY (.+) so it matches the LONGEST prefix, leaving only last digits
 
 
 
 
 
 
 
 
 
 
94
  base_url = full_quality[0]
95
  parts_found = [base_url]
96
+
97
+ # Pattern: ends with 2 digits before .m3u8 (like "01.m3u8", "02.m3u8")
98
+ match_2digit = re.match(r'^(.+?)(\d{2})(\.m3u8)$', base_url)
99
+ if not match_2digit:
100
+ # Try 1 digit at end
101
+ match_2digit = re.match(r'^(.+?)(\d)(\.m3u8)$', base_url)
102
 
103
+ if match_2digit:
104
+ base = match_2digit.group(1)
105
+ start_num = int(match_2digit.group(2))
106
+ ext = match_2digit.group(3)
107
+ num_width = len(match_2digit.group(2)) # 1 or 2 digits
108
+
109
+ # Probe next numbers
110
+ for i in range(start_num + 1, start_num + 6):
111
+ if num_width == 2:
112
+ probe_url = f"{base}{i:02d}{ext}"
113
+ else:
114
+ probe_url = f"{base}{i}{ext}"
115
  try:
116
  pr = requests.head(probe_url, headers=HEADERS, timeout=3, allow_redirects=True)
117
  if pr.status_code == 200:
 
120
  break
121
  except:
122
  break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  if len(parts_found) > 1:
125
  return {"src": parts_found[0], "poster": poster, "vtype": "hls", "all_parts": parts_found}
126
+
127
+ return {"src": full_quality[0], "poster": poster, "vtype": "hls"}
128
  except:
129
  return None
130