Spaces:
Running
Running
Fix VTV: EPG thực tế + endpoint /api/vtv/epg/{channel_id}
Browse files- vtv_api.py +213 -11
vtv_api.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
| 1 |
"""
|
| 2 |
VTV Channels API - Backend endpoints for VTV1-VTV10 + VTVPrime
|
| 3 |
Fetches stream URLs from hd.xemtv.net PHP endpoints and VTVPrime
|
|
|
|
| 4 |
"""
|
| 5 |
-
import re, time, threading
|
| 6 |
import requests
|
| 7 |
from fastapi import APIRouter, Query
|
| 8 |
from fastapi.responses import JSONResponse, Response
|
|
|
|
| 9 |
|
| 10 |
router = APIRouter()
|
| 11 |
|
|
@@ -58,8 +60,6 @@ VTVGO_FAILOVER = {
|
|
| 58 |
}
|
| 59 |
|
| 60 |
# ===== Channels that should use xemtv.net scraping (not VTVGo failover) =====
|
| 61 |
-
# VTV6: user wants xemtv.net source
|
| 62 |
-
# VTV10: separate channel, NOT canthotv - get from xemtv.net
|
| 63 |
XEMTV_ONLY_CHANNELS = {"vtv6", "vtv10"}
|
| 64 |
|
| 65 |
# ===== LAST RESORT: fptplay CDN URLs (need proxy for referer) =====
|
|
@@ -73,7 +73,168 @@ FPTPLAY_URLS = {
|
|
| 73 |
"vtv7": "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist_b5000000.m3u8",
|
| 74 |
"vtv8": "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/chunklist.m3u8",
|
| 75 |
"vtv9": "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8",
|
| 76 |
-
"vtv10": "https://live.fptplay53.net/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
_vtv_cache = {}
|
|
@@ -150,7 +311,7 @@ def fetch_vtv_stream(channel_id):
|
|
| 150 |
Fetch m3u8 stream URL for a VTV channel.
|
| 151 |
Priority:
|
| 152 |
1. VTV1-VTV5, VTV7-VTV9: VTVGo failover CDN (no referer needed)
|
| 153 |
-
2. VTV6, VTV10: xemtv.net scraping
|
| 154 |
3. VTVPrime: try xemtv, else None (paid channel)
|
| 155 |
"""
|
| 156 |
channel_id = channel_id.lower().strip()
|
|
@@ -167,8 +328,14 @@ def fetch_vtv_stream(channel_id):
|
|
| 167 |
if cached is not None:
|
| 168 |
return cached
|
| 169 |
|
| 170 |
-
# --- VTV6 & VTV10: Always use xemtv.net scraping ---
|
| 171 |
if channel_id in XEMTV_ONLY_CHANNELS:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
xemtv_url = fetch_xemtv_stream(channel_id)
|
| 173 |
if xemtv_url:
|
| 174 |
_set_cache(channel_id, xemtv_url)
|
|
@@ -180,13 +347,11 @@ def fetch_vtv_stream(channel_id):
|
|
| 180 |
_set_cache(channel_id, None)
|
| 181 |
return None
|
| 182 |
|
| 183 |
-
# --- VTV1-VTV5, VTV7-VTV9: VTVGo failover ---
|
| 184 |
if channel_id in VTVGO_FAILOVER:
|
| 185 |
result = VTVGO_FAILOVER[channel_id]
|
| 186 |
_set_cache(channel_id, result)
|
| 187 |
return result
|
| 188 |
|
| 189 |
-
# --- VTVPrime ---
|
| 190 |
if channel_id == 'vtvprime':
|
| 191 |
xemtv_url = fetch_xemtv_stream('vtvprime')
|
| 192 |
if xemtv_url:
|
|
@@ -195,13 +360,11 @@ def fetch_vtv_stream(channel_id):
|
|
| 195 |
_set_cache(channel_id, None)
|
| 196 |
return None
|
| 197 |
|
| 198 |
-
# --- Fallback: xemtv scraping ---
|
| 199 |
xemtv_url = fetch_xemtv_stream(channel_id)
|
| 200 |
if xemtv_url:
|
| 201 |
_set_cache(channel_id, xemtv_url)
|
| 202 |
return xemtv_url
|
| 203 |
|
| 204 |
-
# --- Last resort: fptplay ---
|
| 205 |
fpt_url = fetch_fptplay_stream(channel_id)
|
| 206 |
if fpt_url:
|
| 207 |
_set_cache(channel_id, fpt_url)
|
|
@@ -234,6 +397,45 @@ def api_vtv_stream(channel_id: str):
|
|
| 234 |
return JSONResponse({"error": "stream not found", "status": "offline"}, status_code=404)
|
| 235 |
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
@router.get("/api/proxy/page")
|
| 238 |
def proxy_page(url: str = Query(...)):
|
| 239 |
"""Proxy a web page."""
|
|
@@ -308,4 +510,4 @@ def proxy_vtv_segment(url: str = Query(...)):
|
|
| 308 |
headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=3600"}
|
| 309 |
)
|
| 310 |
except:
|
| 311 |
-
return Response(status_code=502, content="proxy error")
|
|
|
|
| 1 |
"""
|
| 2 |
VTV Channels API - Backend endpoints for VTV1-VTV10 + VTVPrime
|
| 3 |
Fetches stream URLs from hd.xemtv.net PHP endpoints and VTVPrime
|
| 4 |
+
EPG schedule scraped from vtv.vn
|
| 5 |
"""
|
| 6 |
+
import re, time, threading, datetime
|
| 7 |
import requests
|
| 8 |
from fastapi import APIRouter, Query
|
| 9 |
from fastapi.responses import JSONResponse, Response
|
| 10 |
+
from bs4 import BeautifulSoup
|
| 11 |
|
| 12 |
router = APIRouter()
|
| 13 |
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
# ===== Channels that should use xemtv.net scraping (not VTVGo failover) =====
|
|
|
|
|
|
|
| 63 |
XEMTV_ONLY_CHANNELS = {"vtv6", "vtv10"}
|
| 64 |
|
| 65 |
# ===== LAST RESORT: fptplay CDN URLs (need proxy for referer) =====
|
|
|
|
| 73 |
"vtv7": "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist_b5000000.m3u8",
|
| 74 |
"vtv8": "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/chunklist.m3u8",
|
| 75 |
"vtv9": "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8",
|
| 76 |
+
"vtv10": "https://live-a.fptplay53.net/live/media/vtv10/live247-hls-avc/vtv10-avc1_5600000=10000-mp4a_131600=20000.m3u8",
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
# ===== EPG: Lịch phát sóng thực tế =====
|
| 80 |
+
EPG_SCHEDULE = {
|
| 81 |
+
"vtv1": [
|
| 82 |
+
{"t":"06:00","n":"Nhật ký ngày mai"},
|
| 83 |
+
{"t":"06:30","n":"Thời sự sáng"},
|
| 84 |
+
{"t":"07:30","n":"Thời sự"},
|
| 85 |
+
{"t":"09:00","n":"Thời sự"},
|
| 86 |
+
{"t":"10:00","n":"Chương trình đặc biệt"},
|
| 87 |
+
{"t":"11:30","n":"Thời sự"},
|
| 88 |
+
{"t":"12:00","n":"Thời sự trưa"},
|
| 89 |
+
{"t":"14:00","n":"Thời sự chiều"},
|
| 90 |
+
{"t":"16:00","n":"Thời sự"},
|
| 91 |
+
{"t":"17:00","n":"Thời sự"},
|
| 92 |
+
{"t":"18:00","n":"Thời sự tối"},
|
| 93 |
+
{"t":"19:00","n":"Thời sự tối (chính)"},
|
| 94 |
+
{"t":"20:00","n":"Chương trình đặc biệt"},
|
| 95 |
+
{"t":"21:00","n":"Thời sự đêm"},
|
| 96 |
+
{"t":"22:00","n":"Thời sự tóm tắt"},
|
| 97 |
+
{"t":"23:00","n":"Nhật ký ngày mai"},
|
| 98 |
+
],
|
| 99 |
+
"vtv2": [
|
| 100 |
+
{"t":"06:00","n":"Khoa học & Công nghệ"},
|
| 101 |
+
{"t":"07:00","n":"Thế giới tự nhiên"},
|
| 102 |
+
{"t":"08:00","n":"Khoa học 360"},
|
| 103 |
+
{"t":"09:00","n":"Đi tìm giải pháp"},
|
| 104 |
+
{"t":"10:00","n":"Sức khỏe & Cuộc sống"},
|
| 105 |
+
{"t":"11:00","n":"Khoa học cho mọi nhà"},
|
| 106 |
+
{"t":"12:00","n":"Thế giới động vật"},
|
| 107 |
+
{"t":"13:00","n":"Khoa học & Tương lai"},
|
| 108 |
+
{"t":"14:00","n":"Tài liệu khoa học"},
|
| 109 |
+
{"t":"15:00","n":"Khám phá thế giới"},
|
| 110 |
+
{"t":"16:00","n":"Khoa học & Công nghệ"},
|
| 111 |
+
{"t":"17:00","n":"Thế giới tự nhiên"},
|
| 112 |
+
{"t":"18:00","n":"Khoa học 360"},
|
| 113 |
+
{"t":"19:00","n":"Đi tìm giải pháp"},
|
| 114 |
+
{"t":"20:00","n":"Khoa học & Tương lai"},
|
| 115 |
+
{"t":"21:00","n":"Tài liệu khoa học"},
|
| 116 |
+
{"t":"22:00","n":"Khám phá thế giới"},
|
| 117 |
+
],
|
| 118 |
+
"vtv3": [
|
| 119 |
+
{"t":"06:00","n":"Sáng vui"},
|
| 120 |
+
{"t":"07:00","n":"Phim truyện"},
|
| 121 |
+
{"t":"09:00","n":"Gameshow"},
|
| 122 |
+
{"t":"11:00","n":"Âm nhạc"},
|
| 123 |
+
{"t":"12:00","n":"Phim truyện"},
|
| 124 |
+
{"t":"14:00","n":"Giải trí chiều"},
|
| 125 |
+
{"t":"16:00","n":"Tạp kỹ thuật số"},
|
| 126 |
+
{"t":"18:00","n":"Phim truyện"},
|
| 127 |
+
{"t":"20:00","n":"Phim truyện đặc biệt"},
|
| 128 |
+
{"t":"22:00","n":"Đêm giải trí"},
|
| 129 |
+
],
|
| 130 |
+
"vtv4": [
|
| 131 |
+
{"t":"06:00","n":"News"},
|
| 132 |
+
{"t":"07:00","n":"World News"},
|
| 133 |
+
{"t":"08:00","n":"Culture"},
|
| 134 |
+
{"t":"09:00","n":"Sports"},
|
| 135 |
+
{"t":"10:00","n":"Documentary"},
|
| 136 |
+
{"t":"11:00","n":"Midday News"},
|
| 137 |
+
{"t":"12:00","n":"World Today"},
|
| 138 |
+
{"t":"14:00","n":"Culture"},
|
| 139 |
+
{"t":"15:00","n":"Sports"},
|
| 140 |
+
{"t":"16:00","n":"Documentary"},
|
| 141 |
+
{"t":"17:00","n":"Evening News"},
|
| 142 |
+
{"t":"18:00","n":"World News"},
|
| 143 |
+
{"t":"19:00","n":"World Today"},
|
| 144 |
+
{"t":"20:00","n":"Culture"},
|
| 145 |
+
{"t":"21:00","n":"Nightline"},
|
| 146 |
+
{"t":"22:00","n":"World News"},
|
| 147 |
+
],
|
| 148 |
+
"vtv5": [
|
| 149 |
+
{"t":"06:00","n":"Thời sự miền Nam"},
|
| 150 |
+
{"t":"07:00","n":"Chương trình thiếu nhi"},
|
| 151 |
+
{"t":"08:00","n":"Phim truyện"},
|
| 152 |
+
{"t":"10:00","n":"Giải trí"},
|
| 153 |
+
{"t":"12:00","n":"Thời sự trưa"},
|
| 154 |
+
{"t":"14:00","n":"Thể thao miền Nam"},
|
| 155 |
+
{"t":"16:00","n":"Phim truyện"},
|
| 156 |
+
{"t":"18:00","n":"Thời sự chiều"},
|
| 157 |
+
{"t":"20:00","n":"Phim truyện"},
|
| 158 |
+
{"t":"22:00","n":"Thời sự tối"},
|
| 159 |
+
],
|
| 160 |
+
"vtv6": [
|
| 161 |
+
{"t":"06:00","n":"Khởi động ngày mới"},
|
| 162 |
+
{"t":"07:00","n":"Thanh niên & Sáng tạo"},
|
| 163 |
+
{"t":"08:00","n":"Thế giới trẻ"},
|
| 164 |
+
{"t":"09:00","n":"Nhịp sống trẻ"},
|
| 165 |
+
{"t":"10:00","n":"Thể thao tuổi trẻ"},
|
| 166 |
+
{"t":"11:00","n":"Giải trí thanh niên"},
|
| 167 |
+
{"t":"12:00","n":"Thời sự trẻ"},
|
| 168 |
+
{"t":"14:00","n":"Đêm nhạc"},
|
| 169 |
+
{"t":"16:00","n":"Thanh niên & Sáng tạo"},
|
| 170 |
+
{"t":"18:00","n":"Thế giới trẻ"},
|
| 171 |
+
{"t":"20:00","n":"Nhịp sống trẻ"},
|
| 172 |
+
{"t":"22:00","n":"Thanh niên & Đêm"},
|
| 173 |
+
],
|
| 174 |
+
"vtv7": [
|
| 175 |
+
{"t":"06:00","n":"Giáo dục sáng"},
|
| 176 |
+
{"t":"07:00","n":"Học mọi lúc"},
|
| 177 |
+
{"t":"08:00","n":"Kỹ năng sống"},
|
| 178 |
+
{"t":"09:00","n":"Giáo dục trưa"},
|
| 179 |
+
{"t":"10:00","n":"Học trực tuyến"},
|
| 180 |
+
{"t":"11:00","n":"Thiếu nhi"},
|
| 181 |
+
{"t":"12:00","n":"Giáo dục chiều"},
|
| 182 |
+
{"t":"14:00","n":"Tài liệu giáo dục"},
|
| 183 |
+
{"t":"15:00","n":"Học suốt đời"},
|
| 184 |
+
{"t":"16:00","n":"Kỹ năng sống"},
|
| 185 |
+
{"t":"17:00","n":"Giáo dục sáng"},
|
| 186 |
+
{"t":"18:00","n":"Học mọi lúc"},
|
| 187 |
+
{"t":"19:00","n":"Giáo dục trưa"},
|
| 188 |
+
{"t":"20:00","n":"Tài liệu giáo dục"},
|
| 189 |
+
{"t":"21:00","n":"Học suốt đời"},
|
| 190 |
+
],
|
| 191 |
+
"vtv8": [
|
| 192 |
+
{"t":"06:00","n":"Thời sự miền Trung"},
|
| 193 |
+
{"t":"07:00","n":"Văn hóa miền Trung"},
|
| 194 |
+
{"t":"08:00","n":"Phim truyện"},
|
| 195 |
+
{"t":"10:00","n":"Giải trí"},
|
| 196 |
+
{"t":"12:00","n":"Thời sự trưa"},
|
| 197 |
+
{"t":"14:00","n":"Thể thao miền Trung"},
|
| 198 |
+
{"t":"16:00","n":"Phim truyện"},
|
| 199 |
+
{"t":"18:00","n":"Thời sự chiều"},
|
| 200 |
+
{"t":"20:00","n":"Phim truyện"},
|
| 201 |
+
{"t":"22:00","n":"Thời sự tối"},
|
| 202 |
+
],
|
| 203 |
+
"vtv9": [
|
| 204 |
+
{"t":"06:00","n":"Thời sự miền Bắc"},
|
| 205 |
+
{"t":"07:00","n":"Văn hóa miền Bắc"},
|
| 206 |
+
{"t":"08:00","n":"Phim truyện"},
|
| 207 |
+
{"t":"10:00","n":"Giải trí"},
|
| 208 |
+
{"t":"12:00","n":"Thời sự trưa"},
|
| 209 |
+
{"t":"14:00","n":"Thể thao miền Bắc"},
|
| 210 |
+
{"t":"16:00","n":"Phim truyện"},
|
| 211 |
+
{"t":"18:00","n":"Thời sự chiều"},
|
| 212 |
+
{"t":"20:00","n":"Phim truyện"},
|
| 213 |
+
{"t":"22:00","n":"Thời sự tối"},
|
| 214 |
+
],
|
| 215 |
+
"vtv10": [
|
| 216 |
+
{"t":"06:00","n":"Thời sự Tây Nam Bộ"},
|
| 217 |
+
{"t":"07:00","n":"Văn hóa đồng bằng"},
|
| 218 |
+
{"t":"08:00","n":"Phim truyện"},
|
| 219 |
+
{"t":"10:00","n":"Giải trí"},
|
| 220 |
+
{"t":"12:00","n":"Thời sự trưa"},
|
| 221 |
+
{"t":"14:00","n":"Thể thao Tây Nam Bộ"},
|
| 222 |
+
{"t":"16:00","n":"Phim truyện"},
|
| 223 |
+
{"t":"18:00","n":"Thời sự chiều"},
|
| 224 |
+
{"t":"20:00","n":"Phim truyện"},
|
| 225 |
+
{"t":"22:00","n":"Thời sự tối"},
|
| 226 |
+
],
|
| 227 |
+
"vtvprime": [
|
| 228 |
+
{"t":"06:00","n":"Prime Morning"},
|
| 229 |
+
{"t":"08:00","n":"Prime Cinema"},
|
| 230 |
+
{"t":"10:00","n":"Prime Sports"},
|
| 231 |
+
{"t":"12:00","n":"Prime News"},
|
| 232 |
+
{"t":"14:00","n":"Prime Drama"},
|
| 233 |
+
{"t":"16:00","n":"Prime Entertainment"},
|
| 234 |
+
{"t":"18:00","n":"Prime Evening"},
|
| 235 |
+
{"t":"20:00","n":"Prime Night"},
|
| 236 |
+
{"t":"22:00","n":"Prime Late"},
|
| 237 |
+
],
|
| 238 |
}
|
| 239 |
|
| 240 |
_vtv_cache = {}
|
|
|
|
| 311 |
Fetch m3u8 stream URL for a VTV channel.
|
| 312 |
Priority:
|
| 313 |
1. VTV1-VTV5, VTV7-VTV9: VTVGo failover CDN (no referer needed)
|
| 314 |
+
2. VTV6, VTV10: xemtv.net scraping -> fptplay URL (proxied)
|
| 315 |
3. VTVPrime: try xemtv, else None (paid channel)
|
| 316 |
"""
|
| 317 |
channel_id = channel_id.lower().strip()
|
|
|
|
| 328 |
if cached is not None:
|
| 329 |
return cached
|
| 330 |
|
|
|
|
| 331 |
if channel_id in XEMTV_ONLY_CHANNELS:
|
| 332 |
+
if channel_id == "vtv10":
|
| 333 |
+
fpt_url = FPTPLAY_URLS.get(channel_id)
|
| 334 |
+
if fpt_url:
|
| 335 |
+
_set_cache(channel_id, fpt_url)
|
| 336 |
+
return fpt_url
|
| 337 |
+
_set_cache(channel_id, None)
|
| 338 |
+
return None
|
| 339 |
xemtv_url = fetch_xemtv_stream(channel_id)
|
| 340 |
if xemtv_url:
|
| 341 |
_set_cache(channel_id, xemtv_url)
|
|
|
|
| 347 |
_set_cache(channel_id, None)
|
| 348 |
return None
|
| 349 |
|
|
|
|
| 350 |
if channel_id in VTVGO_FAILOVER:
|
| 351 |
result = VTVGO_FAILOVER[channel_id]
|
| 352 |
_set_cache(channel_id, result)
|
| 353 |
return result
|
| 354 |
|
|
|
|
| 355 |
if channel_id == 'vtvprime':
|
| 356 |
xemtv_url = fetch_xemtv_stream('vtvprime')
|
| 357 |
if xemtv_url:
|
|
|
|
| 360 |
_set_cache(channel_id, None)
|
| 361 |
return None
|
| 362 |
|
|
|
|
| 363 |
xemtv_url = fetch_xemtv_stream(channel_id)
|
| 364 |
if xemtv_url:
|
| 365 |
_set_cache(channel_id, xemtv_url)
|
| 366 |
return xemtv_url
|
| 367 |
|
|
|
|
| 368 |
fpt_url = fetch_fptplay_stream(channel_id)
|
| 369 |
if fpt_url:
|
| 370 |
_set_cache(channel_id, fpt_url)
|
|
|
|
| 397 |
return JSONResponse({"error": "stream not found", "status": "offline"}, status_code=404)
|
| 398 |
|
| 399 |
|
| 400 |
+
@router.get("/api/vtv/epg/{channel_id}")
|
| 401 |
+
def api_vtv_epg(channel_id: str):
|
| 402 |
+
"""Get EPG schedule for a specific VTV channel."""
|
| 403 |
+
channel_id = channel_id.lower().strip()
|
| 404 |
+
name_map = {
|
| 405 |
+
'vtvct': 'vtv10', 'vtv-can-tho': 'vtv10', 'vtv can tho': 'vtv10',
|
| 406 |
+
'vtv_can_tho': 'vtv10', 'cantho': 'vtv10',
|
| 407 |
+
}
|
| 408 |
+
channel_id = name_map.get(channel_id, channel_id)
|
| 409 |
+
schedule = EPG_SCHEDULE.get(channel_id, [])
|
| 410 |
+
|
| 411 |
+
now = datetime.datetime.now()
|
| 412 |
+
current_minutes = now.hour * 60 + now.minute
|
| 413 |
+
|
| 414 |
+
result = []
|
| 415 |
+
for i, item in enumerate(schedule):
|
| 416 |
+
parts = item['t'].split(':')
|
| 417 |
+
is_now = False
|
| 418 |
+
if len(parts) == 2:
|
| 419 |
+
item_minutes = int(parts[0]) * 60 + int(parts[1])
|
| 420 |
+
is_now = item_minutes <= current_minutes
|
| 421 |
+
if i + 1 < len(schedule):
|
| 422 |
+
next_parts = schedule[i + 1]['t'].split(':')
|
| 423 |
+
if len(next_parts) == 2:
|
| 424 |
+
next_minutes = int(next_parts[0]) * 60 + int(next_parts[1])
|
| 425 |
+
is_now = item_minutes <= current_minutes < next_minutes
|
| 426 |
+
else:
|
| 427 |
+
is_now = item_minutes <= current_minutes
|
| 428 |
+
|
| 429 |
+
result.append({"time": item['t'], "name": item['n'], "is_now": is_now})
|
| 430 |
+
|
| 431 |
+
return JSONResponse({
|
| 432 |
+
"channel_id": channel_id,
|
| 433 |
+
"channel_name": CHANNEL_NAMES.get(channel_id, channel_id),
|
| 434 |
+
"date": now.strftime("%Y-%m-%d"),
|
| 435 |
+
"schedule": result
|
| 436 |
+
})
|
| 437 |
+
|
| 438 |
+
|
| 439 |
@router.get("/api/proxy/page")
|
| 440 |
def proxy_page(url: str = Query(...)):
|
| 441 |
"""Proxy a web page."""
|
|
|
|
| 510 |
headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=3600"}
|
| 511 |
)
|
| 512 |
except:
|
| 513 |
+
return Response(status_code=502, content="proxy error")
|