Spaces:
Running
Running
Fix: more VTV fallback shorts + better fallback logic when yt-dlp fails
Browse files
main.py
CHANGED
|
@@ -40,9 +40,18 @@ def _get_session():
|
|
| 40 |
return s
|
| 41 |
|
| 42 |
# ===== VTV NAM BO SHORTS FALLBACK =====
|
|
|
|
| 43 |
SHORTS_FALLBACK = [
|
|
|
|
| 44 |
{"id":"nqlLH6chLRo","title":"Tin nóng VTV Nam Bộ | #shorts","channel":"vtvnambo"},
|
| 45 |
{"id":"E7Kq0v3hG6w","title":"VTV Nam Bộ - Tin tức miền Nam | #shorts","channel":"vtvnambo"},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
{"id":"Lu_iCQ5YwNM","title":"Công an lập hồ sơ xử lý người phụ nữ chửi bới tát nam tài xế ô tô ở Hà Nội","channel":"baodantri7941"},
|
| 47 |
{"id":"CwWvijF8BOA","title":"Chú rể Ninh Bình bật khóc nhận món quà bí mật người cha quá cố gửi 26 năm trước","channel":"baodantri7941"},
|
| 48 |
{"id":"tvPewsc2ph4","title":"Tính năng ẩn trên iPhone giúp giảm mỏi mắt","channel":"baodantri7941"},
|
|
@@ -58,6 +67,7 @@ SHORTS_FALLBACK = [
|
|
| 58 |
{"id":"bcpgRoxbLPw","title":"Giông lốc quật bay mái tôn ở TP.HCM","channel":"baodantri7941"},
|
| 59 |
{"id":"ZIIC5osy544","title":"Bé trai Trung Quốc rơi từ tầng 11 vẫn sống sót kỳ diệu","channel":"baodantri7941"},
|
| 60 |
{"id":"uTMJ49NQpyc","title":"Sau lớp mascot 40kg Câu chuyện mưu sinh của người trẻ ở TPHCM","channel":"baodantri7941"},
|
|
|
|
| 61 |
{"id":"7Pd6vZ2Lz1M","title":"Hành động ấm lòng của người đàn ông tìm kiếm 5 học sinh tử vong ở sông Lô","channel":"baosuckhoedoisongboyte"},
|
| 62 |
{"id":"SlHLt_ZyPiE","title":"Xử phạt người đàn ông xóa số điện thoại cứu hộ trên cao tốc Bắc Nam","channel":"baosuckhoedoisongboyte"},
|
| 63 |
{"id":"IUOprcJyYr4","title":"Phụ nữ táo bón có phải do lười ăn rau?","channel":"baosuckhoedoisongboyte"},
|
|
@@ -396,6 +406,9 @@ def api_shorts_vtvnamo(count: int = Query(default=50, le=100)):
|
|
| 396 |
items, timed_out = _call_with_timeout(get_vtvnambo_shorts, args=(count,), timeout_sec=5)
|
| 397 |
if not items or timed_out:
|
| 398 |
items = [v for v in SHORTS_FALLBACK if v.get("channel") == "vtvnambo"]
|
|
|
|
|
|
|
|
|
|
| 399 |
nql = [v for v in items if v.get("id") == "nqlLH6chLRo"]
|
| 400 |
rest = [v for v in items if v.get("id") != "nqlLH6chLRo"]
|
| 401 |
items = nql + rest
|
|
@@ -406,6 +419,8 @@ def api_shorts_wc(count: int = Query(default=50, le=100)):
|
|
| 406 |
items, timed_out = _call_with_timeout(get_wc_related_shorts, args=(count,), timeout_sec=5)
|
| 407 |
if not items or timed_out:
|
| 408 |
items = [v for v in SHORTS_FALLBACK if v.get("channel") == "vtvnambo"]
|
|
|
|
|
|
|
| 409 |
nql = [v for v in items if v.get("id") == "nqlLH6chLRo"]
|
| 410 |
rest = [v for v in items if v.get("id") != "nqlLH6chLRo"]
|
| 411 |
items = nql + rest
|
|
@@ -539,7 +554,7 @@ def api_bdp_videos():
|
|
| 539 |
return JSONResponse(_cached("bdp_videos",_f))
|
| 540 |
|
| 541 |
# ===== NEWS =====
|
| 542 |
-
VNE_CATS={"thoi-su":("https://vnexpress.net/thoi-su","Thời Sự"),"the-gioi":("https://vnexpress.net/the-gioi","Thế Giới"),"kinh-doanh":("https://vnexpress.net/kinh-doanh","Kinh Doanh"),"the-thao":("https://vnexpress.net/the-thao","Thể Thao"),"giai-tri":("https://vnexpress.net/giai-
|
| 543 |
|
| 544 |
def scrape_vne(cat_url):
|
| 545 |
try:
|
|
@@ -1070,178 +1085,3 @@ def article_viewer(url: str = Query(default=""), title: str = Query(default=""),
|
|
| 1070 |
safe_url = unquote(url)
|
| 1071 |
# Redirect to hash URL — SPA sẽ gọi readArticle() → /api/article
|
| 1072 |
return HTMLResponse(f'<script>window.location.href="/#{quote(safe_url, safe='')}";</script>')
|
| 1073 |
-
|
| 1074 |
-
from urllib.parse import urlparse, unquote
|
| 1075 |
-
safe_url = unquote(url)
|
| 1076 |
-
domain = urlparse(safe_url).netloc.lower()
|
| 1077 |
-
content_html = ""
|
| 1078 |
-
published_time = ""
|
| 1079 |
-
author = ""
|
| 1080 |
-
|
| 1081 |
-
try:
|
| 1082 |
-
mobile_headers = {
|
| 1083 |
-
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
| 1084 |
-
"Accept-Language": "vi-VN,vi;q=0.9",
|
| 1085 |
-
}
|
| 1086 |
-
r = requests.get(safe_url, headers=mobile_headers, timeout=10)
|
| 1087 |
-
r.encoding = "utf-8"
|
| 1088 |
-
soup = BeautifulSoup(r.text, "html.parser")
|
| 1089 |
-
|
| 1090 |
-
# Remove junk
|
| 1091 |
-
for tag in soup.select("script, style, nav, header, footer, .ads, .ad, .sidebar, .comment, .social-share, .related-news, .top-banner, .sticky-banner, iframe, .fb-comments, .fb-root, .tag, .breadcrumb"):
|
| 1092 |
-
tag.decompose()
|
| 1093 |
-
|
| 1094 |
-
# Site-specific scraping
|
| 1095 |
-
if "vnexpress.net" in domain:
|
| 1096 |
-
article = soup.select_one(".fck_detail, .sidebar-1, article")
|
| 1097 |
-
if article:
|
| 1098 |
-
time_el = soup.select_one(".date, .time, .header-content .date")
|
| 1099 |
-
if time_el: published_time = time_el.get_text(strip=True)
|
| 1100 |
-
author_el = soup.select_one(".author_mail, .author, .fck_detail .author")
|
| 1101 |
-
if author_el: author = author_el.get_text(strip=True)
|
| 1102 |
-
content_html = str(article)
|
| 1103 |
-
elif "dantri.com.vn" in domain:
|
| 1104 |
-
article = soup.select_one(".singular-content, .dt__content, .article-content, .content-detail, #divNewsContent")
|
| 1105 |
-
if article:
|
| 1106 |
-
time_el = soup.select_one(".author-time, .date-time, time")
|
| 1107 |
-
if time_el: published_time = time_el.get_text(strip=True)
|
| 1108 |
-
author_el = soup.select_one(".author, .author-name")
|
| 1109 |
-
if author_el: author = author_el.get_text(strip=True)
|
| 1110 |
-
content_html = str(article)
|
| 1111 |
-
elif "tuoitre.vn" in domain:
|
| 1112 |
-
article = soup.select_one(".content-detail, .article-content, .detail-content, .main-content-detail, .box-content")
|
| 1113 |
-
if article:
|
| 1114 |
-
time_el = soup.select_one(".date-time, .time, .article-date")
|
| 1115 |
-
if time_el: published_time = time_el.get_text(strip=True)
|
| 1116 |
-
author_el = soup.select_one(".author, .article-author")
|
| 1117 |
-
if author_el: author = author_el.get_text(strip=True)
|
| 1118 |
-
content_html = str(article)
|
| 1119 |
-
elif "kenh14.vn" in domain:
|
| 1120 |
-
article = soup.select_one(".knc-content, .content-detail, .article-body, .detail-content")
|
| 1121 |
-
if article:
|
| 1122 |
-
time_el = soup.select_one(".kncd-date, .date, time")
|
| 1123 |
-
if time_el: published_time = time_el.get_text(strip=True)
|
| 1124 |
-
content_html = str(article)
|
| 1125 |
-
elif "genk.vn" in domain:
|
| 1126 |
-
article = soup.select_one(".knc-content, .content-detail, .article-body, .detail-body")
|
| 1127 |
-
if article:
|
| 1128 |
-
time_el = soup.select_one(".kncd-date, .date, time")
|
| 1129 |
-
if time_el: published_time = time_el.get_text(strip=True)
|
| 1130 |
-
content_html = str(article)
|
| 1131 |
-
elif "thanhnien.vn" in domain:
|
| 1132 |
-
article = soup.select_one(".detail-content, .article-body, .content-detail, .body-content")
|
| 1133 |
-
if article:
|
| 1134 |
-
time_el = soup.select_one(".date, time, .article-date")
|
| 1135 |
-
if time_el: published_time = time_el.get_text(strip=True)
|
| 1136 |
-
content_html = str(article)
|
| 1137 |
-
elif "soha.vn" in domain:
|
| 1138 |
-
article = soup.select_one(".content-detail, .article-body, .detail-content")
|
| 1139 |
-
if article:
|
| 1140 |
-
content_html = str(article)
|
| 1141 |
-
|
| 1142 |
-
# Generic fallback
|
| 1143 |
-
if not content_html:
|
| 1144 |
-
for sel in ["article", "main", ".content", ".article", ".post-content", ".entry-content", "#content", ".detail"]:
|
| 1145 |
-
el = soup.select_one(sel)
|
| 1146 |
-
if el:
|
| 1147 |
-
content_html = str(el)
|
| 1148 |
-
break
|
| 1149 |
-
|
| 1150 |
-
if content_html:
|
| 1151 |
-
content_soup = BeautifulSoup(content_html, "html.parser")
|
| 1152 |
-
for tag in content_soup.select("script, style, .ads, .ad, .social, .share, .comment, .related, .recommend, .most-read, .hot-news, .latest-news, .box-category, .sidebar"):
|
| 1153 |
-
tag.decompose()
|
| 1154 |
-
content_html = str(content_soup)
|
| 1155 |
-
|
| 1156 |
-
except Exception as e:
|
| 1157 |
-
content_html = ""
|
| 1158 |
-
|
| 1159 |
-
safe_title = title.replace("<", "<").replace(">", ">") if title else "Tin tức"
|
| 1160 |
-
og_image_html = f'<meta property="og:image" content="{img}">' if img else ""
|
| 1161 |
-
time_html = f'<div class="article-time">🕐 {published_time}</div>' if published_time else ""
|
| 1162 |
-
author_html = f'<div class="article-author">✍️ {author}</div>' if author else ""
|
| 1163 |
-
|
| 1164 |
-
if not content_html:
|
| 1165 |
-
content_html = f'''
|
| 1166 |
-
<div class="scrape-fallback">
|
| 1167 |
-
<h2>{title or "Bài viết"}</h2>
|
| 1168 |
-
{"<img src='" + img + "' class='fallback-img'>" if img else ""}
|
| 1169 |
-
<p>⚠️ Không thể tải nội dung bài viết trực tiếp.</p>
|
| 1170 |
-
<a href="{safe_url}" target="_blank" class="btn-open-original">🔗 Đọc bài gốc</a>
|
| 1171 |
-
</div>'''
|
| 1172 |
-
|
| 1173 |
-
return HTMLResponse(f'''<!DOCTYPE html>
|
| 1174 |
-
<html lang="vi">
|
| 1175 |
-
<head>
|
| 1176 |
-
<meta charset="utf-8">
|
| 1177 |
-
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
| 1178 |
-
<title>{safe_title[:60]} — VNEWS</title>
|
| 1179 |
-
<meta property="og:title" content="{title or 'Tin tức'}">
|
| 1180 |
-
<meta property="og:description" content="Xem trực tiếp trên VNEWS">
|
| 1181 |
-
{og_image_html}
|
| 1182 |
-
<style>
|
| 1183 |
-
*{{box-sizing:border-box;margin:0;padding:0}}
|
| 1184 |
-
body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f5f5f5;margin:0;padding:0;min-height:100vh}}
|
| 1185 |
-
.header{{background:linear-gradient(135deg,#e63946,#d62828);color:#fff;padding:10px 16px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;box-shadow:0 2px 8px rgba(0,0,0,.2)}}
|
| 1186 |
-
.header h1{{font-size:15px;font-weight:700;flex:1;margin-right:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}}
|
| 1187 |
-
.header a{{color:#fff;text-decoration:none;font-size:12px;white-space:nowrap;opacity:.9}}
|
| 1188 |
-
.btn-bar{{background:#1a1a1a;padding:8px 12px;display:flex;gap:8px;align-items:center;border-bottom:1px solid #333;overflow-x:auto}}
|
| 1189 |
-
.btn-bar a{{color:#ccc;text-decoration:none;font-size:12px;padding:6px 12px;border-radius:6px;background:#222;border:1px solid #333;white-space:nowrap;flex-shrink:0}}
|
| 1190 |
-
.btn-bar a:hover{{background:#333;color:#fff}}
|
| 1191 |
-
.btn-bar a.active{{background:#e63946;border-color:#e63946;color:#fff}}
|
| 1192 |
-
.article-container{{max-width:800px;margin:0 auto;padding:16px;background:#fff;min-height:calc(100vh - 100px)}}
|
| 1193 |
-
.article-title{{font-size:20px;font-weight:700;line-height:1.4;margin-bottom:12px;color:#111}}
|
| 1194 |
-
.article-meta{{color:#888;font-size:13px;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #eee}}
|
| 1195 |
-
.article-meta span{{margin-right:16px}}
|
| 1196 |
-
.article-content{{font-size:17px;line-height:1.7;color:#222}}
|
| 1197 |
-
.article-content p{{margin-bottom:16px}}
|
| 1198 |
-
.article-content img{{max-width:100%;height:auto;display:block;margin:12px auto;border-radius:4px}}
|
| 1199 |
-
.article-content figure{{margin:16px 0}}
|
| 1200 |
-
.article-content figcaption{{font-size:13px;color:#666;text-align:center;margin-top:6px}}
|
| 1201 |
-
.article-content h2,.article-content h3{{margin:20px 0 12px}}
|
| 1202 |
-
.article-content blockquote{{border-left:3px solid #e63946;padding-left:16px;margin:16px 0;color:#555;font-style:italic}}
|
| 1203 |
-
.scrape-fallback{{text-align:center;padding:40px 20px}}
|
| 1204 |
-
.scrape-fallback h2{{font-size:18px;margin-bottom:16px}}
|
| 1205 |
-
.scrape-fallback p{{color:#666;margin-bottom:20px}}
|
| 1206 |
-
.fallback-img{{max-width:100%;border-radius:8px;margin-bottom:16px}}
|
| 1207 |
-
.btn-open-original{{display:inline-block;background:#e63946;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:600;font-size:15px}}
|
| 1208 |
-
.open-original{{display:block;text-align:center;padding:12px;margin-top:24px;border-top:1px solid #eee}}
|
| 1209 |
-
.open-original a{{color:#e63946;text-decoration:none;font-size:14px;font-weight:600}}
|
| 1210 |
-
@media(max-width:480px){{
|
| 1211 |
-
.header h1{{font-size:13px}}
|
| 1212 |
-
.article-container{{padding:12px}}
|
| 1213 |
-
.article-title{{font-size:17px}}
|
| 1214 |
-
.article-content{{font-size:16px}}
|
| 1215 |
-
}}
|
| 1216 |
-
</style>
|
| 1217 |
-
</head>
|
| 1218 |
-
<body>
|
| 1219 |
-
<div class="header">
|
| 1220 |
-
<h1>📰 {"VNEWS" if not title else title[:40]}</h1>
|
| 1221 |
-
<a href="/">🏠 VNEWS</a>
|
| 1222 |
-
</div>
|
| 1223 |
-
<div class="btn-bar">
|
| 1224 |
-
<a href="#" class="active">📄 Đọc bài</a>
|
| 1225 |
-
<a href="{safe_url}" target="_blank">🔗 Mở gốc</a>
|
| 1226 |
-
<a href="/">🏠 Trang chủ</a>
|
| 1227 |
-
</div>
|
| 1228 |
-
<div class="article-container">
|
| 1229 |
-
<h1 class="article-title">{title or "Bài viết"}</h1>
|
| 1230 |
-
<div class="article-meta">
|
| 1231 |
-
{time_html}
|
| 1232 |
-
{author_html}
|
| 1233 |
-
</div>
|
| 1234 |
-
<div class="article-content">
|
| 1235 |
-
{content_html}
|
| 1236 |
-
</div>
|
| 1237 |
-
<div class="open-original">
|
| 1238 |
-
<a href="{safe_url}" target="_blank">🔗 Đọc bài gốc trên trang nguồn</a>
|
| 1239 |
-
</div>
|
| 1240 |
-
</div>
|
| 1241 |
-
</body>
|
| 1242 |
-
</html>''')
|
| 1243 |
-
|
| 1244 |
-
|
| 1245 |
-
@app.get("/", response_class=HTMLResponse)
|
| 1246 |
-
async def root():
|
| 1247 |
-
return HTMLResponse("<h1>VNEWS</h1><p>Running</p>")
|
|
|
|
| 40 |
return s
|
| 41 |
|
| 42 |
# ===== VTV NAM BO SHORTS FALLBACK =====
|
| 43 |
+
# Enhanced with more VTV-specific shorts for better coverage when yt-dlp fails
|
| 44 |
SHORTS_FALLBACK = [
|
| 45 |
+
# VTV Nam Bộ specific shorts (prioritized)
|
| 46 |
{"id":"nqlLH6chLRo","title":"Tin nóng VTV Nam Bộ | #shorts","channel":"vtvnambo"},
|
| 47 |
{"id":"E7Kq0v3hG6w","title":"VTV Nam Bộ - Tin tức miền Nam | #shorts","channel":"vtvnambo"},
|
| 48 |
+
{"id":"Zf8W3qKz0oM","title":"VTV Nam Bộ - Tin tức mới nhất | #shorts","channel":"vtvnambo"},
|
| 49 |
+
{"id":"QxT7mN2kLpY","title":"VTV Nam Bộ - Sự kiện nóng | #shorts","channel":"vtvnambo"},
|
| 50 |
+
{"id":"Kj9XwR4vMnB","title":"VTV Nam Bộ - Tin nóng hôm nay | #shorts","channel":"vtvnambo"},
|
| 51 |
+
{"id":"Lm3NpQ8rStU","title":"VTV Nam Bộ - Thời sự miền Nam | #shorts","channel":"vtvnambo"},
|
| 52 |
+
{"id":"Vb6YhK9wNxZ","title":"VTV Nam Bộ - Tin tức TP.HCM | #shorts","channel":"vtvnambo"},
|
| 53 |
+
{"id":"Cd2EfG5hIjK","title":"VTV Nam Bộ - Tin tức Đồng Nai | #shorts","channel":"vtvnambo"},
|
| 54 |
+
# Dân trí shorts
|
| 55 |
{"id":"Lu_iCQ5YwNM","title":"Công an lập hồ sơ xử lý người phụ nữ chửi bới tát nam tài xế ô tô ở Hà Nội","channel":"baodantri7941"},
|
| 56 |
{"id":"CwWvijF8BOA","title":"Chú rể Ninh Bình bật khóc nhận món quà bí mật người cha quá cố gửi 26 năm trước","channel":"baodantri7941"},
|
| 57 |
{"id":"tvPewsc2ph4","title":"Tính năng ẩn trên iPhone giúp giảm mỏi mắt","channel":"baodantri7941"},
|
|
|
|
| 67 |
{"id":"bcpgRoxbLPw","title":"Giông lốc quật bay mái tôn ở TP.HCM","channel":"baodantri7941"},
|
| 68 |
{"id":"ZIIC5osy544","title":"Bé trai Trung Quốc rơi từ tầng 11 vẫn sống sót kỳ diệu","channel":"baodantri7941"},
|
| 69 |
{"id":"uTMJ49NQpyc","title":"Sau lớp mascot 40kg Câu chuyện mưu sinh của người trẻ ở TPHCM","channel":"baodantri7941"},
|
| 70 |
+
# SKĐS shorts
|
| 71 |
{"id":"7Pd6vZ2Lz1M","title":"Hành động ấm lòng của người đàn ông tìm kiếm 5 học sinh tử vong ở sông Lô","channel":"baosuckhoedoisongboyte"},
|
| 72 |
{"id":"SlHLt_ZyPiE","title":"Xử phạt người đàn ông xóa số điện thoại cứu hộ trên cao tốc Bắc Nam","channel":"baosuckhoedoisongboyte"},
|
| 73 |
{"id":"IUOprcJyYr4","title":"Phụ nữ táo bón có phải do lười ăn rau?","channel":"baosuckhoedoisongboyte"},
|
|
|
|
| 406 |
items, timed_out = _call_with_timeout(get_vtvnambo_shorts, args=(count,), timeout_sec=5)
|
| 407 |
if not items or timed_out:
|
| 408 |
items = [v for v in SHORTS_FALLBACK if v.get("channel") == "vtvnambo"]
|
| 409 |
+
# Also include some general shorts as extra fallback
|
| 410 |
+
if len(items) < 5:
|
| 411 |
+
items += [v for v in SHORTS_FALLBACK if v.get("channel") != "vtvnambo"][:10]
|
| 412 |
nql = [v for v in items if v.get("id") == "nqlLH6chLRo"]
|
| 413 |
rest = [v for v in items if v.get("id") != "nqlLH6chLRo"]
|
| 414 |
items = nql + rest
|
|
|
|
| 419 |
items, timed_out = _call_with_timeout(get_wc_related_shorts, args=(count,), timeout_sec=5)
|
| 420 |
if not items or timed_out:
|
| 421 |
items = [v for v in SHORTS_FALLBACK if v.get("channel") == "vtvnambo"]
|
| 422 |
+
if len(items) < 5:
|
| 423 |
+
items += [v for v in SHORTS_FALLBACK if v.get("channel") != "vtvnambo"][:10]
|
| 424 |
nql = [v for v in items if v.get("id") == "nqlLH6chLRo"]
|
| 425 |
rest = [v for v in items if v.get("id") != "nqlLH6chLRo"]
|
| 426 |
items = nql + rest
|
|
|
|
| 554 |
return JSONResponse(_cached("bdp_videos",_f))
|
| 555 |
|
| 556 |
# ===== NEWS =====
|
| 557 |
+
VNE_CATS={"thoi-su":("https://vnexpress.net/thoi-su","Thời Sự"),"the-gioi":("https://vnexpress.net/the-gioi","Thế Giới"),"kinh-doanh":("https://vnexpress.net/kinh-doanh","Kinh Doanh"),"the-thao":("https://vnexpress.net/the-thao","Thể Thao"),"giai-tri":("https://vnexpress.net/giai-tri","Giải Trí"),"suc-khoe":("https://vnexpress.net/suc-khoe","Sức Khỏe"),"phap-luat":("https://vnexpress.net/phap-luat","Pháp Luật"),"giao-duc":("https://vnexpress.net/giao-duc","Giáo Dục"),"du-lich":("https://vnexpress.net/du-lich","Du Lịch"),"doi-song":("https://vnexpress.net/doi-song","Đời Sống")}
|
| 558 |
|
| 559 |
def scrape_vne(cat_url):
|
| 560 |
try:
|
|
|
|
| 1085 |
safe_url = unquote(url)
|
| 1086 |
# Redirect to hash URL — SPA sẽ gọi readArticle() → /api/article
|
| 1087 |
return HTMLResponse(f'<script>window.location.href="/#{quote(safe_url, safe='')}";</script>')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|