VNEWS / vtv_api.py
bep40's picture
Update VTV6 HARDCODED_URL to working freem3u CDN + add m3u8_proxy_url response field
c78ede8 verified
Raw
History Blame Contribute Delete
11.3 kB
# VTV Stream — SV2/sv.xemtivitop iframe URLs + M3U fallback + EPG
import re, time, threading, json, requests
from fastapi import APIRouter, Query
from fastapi.responses import JSONResponse, Response, StreamingResponse
from bs4 import BeautifulSoup
from datetime import datetime, timedelta, timezone
from urllib.parse import quote
VN_TZ = timezone(timedelta(hours=7))
router = APIRouter()
UA = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"Accept-Language": "vi-VN,vi;q=0.9",
}
HARDCODED_URLS = {
"vtv6": "https://freem3u.xyz/api/live/play.m3u8?vid=10043",
"vtv10": "https://live.fptplay53.net/live/media/vtv10/live247-hls-avc/index.m3u8",
}
CHANNEL_URLS = {k: (HARDCODED_URLS[k] if k in HARDCODED_URLS else None) for k in ["vtv1","vtv2","vtv3","vtv4","vtv5","vtv6","vtv7","vtv8","vtv9","vtv10"]}
CHANNEL_NAMES = {"vtv1":"VTV1","vtv2":"VTV2","vtv3":"VTV3","vtv4":"VTV4","vtv5":"VTV5","vtv6":"VTV6","vtv7":"VTV7","vtv8":"VTV8","vtv9":"VTV9","vtv10":"VTV10","vtvprime":"VTVPrime"}
# Iframe URLs cho từng kênh — lấy từ xemtivitop.com
# Với VTV7: LINK 1 = blogspot (lỗi), LINK 2 = sv.xemtivitop.com (JWPlayer, hoạt động)
IFRAME_URLS = {
"vtv1": "https://sv2.xemtivitop.com/live/hot/vtv1.php",
"vtv2": "https://sv2.xemtivitop.com/live/hot/vtv2.php",
"vtv3": "https://sv2.xemtivitop.com/live/hot/vtv3.php",
"vtv4": "https://sv2.xemtivitop.com/live/hot/vtv4.php",
"vtv5": "https://sv2.xemtivitop.com/live/hot/vtv5.php",
"vtv6": "https://sv2.xemtivitop.com/live/hot/vtv6.php",
# VTV7: LINK 2 trên xemtivitop.com — sv.xemtivitop.com (ko phải sv2), JWPlayer, cho phép iframe
"vtv7": "https://sv.xemtivitop.com/live/vtv/vtv7.php",
"vtv8": "https://sv2.xemtivitop.com/live/hot/vtv8.php",
"vtv9": "https://sv2.xemtivitop.com/live/hot/vtv9.php",
"vtv10": "https://sv2.xemtivitop.com/live/hot/vtv10.php",
}
M3U_URLS = [
"https://raw.githubusercontent.com/Love4vn/Test/refs/heads/main/IPTV.m3u",
"https://raw.githubusercontent.com/Love4vn/love4vn/main/Out_Iptv_CXT.m3u",
"https://raw.githubusercontent.com/konanda-sg/Test_Love4vn/main/IPTV.m3u",
"https://raw.githubusercontent.com/iptv-org/iptv/master/streams/vn.m3u",
]
_channel_urls_lock = threading.Lock()
_last_m3u_fetch = 0
_M3U_TTL = 900
def _fetch_m3u():
global _last_m3u_fetch
now = time.time()
if now - _last_m3u_fetch < _M3U_TTL: return
_last_m3u_fetch = now
all_urls = {}
for m3u_url in M3U_URLS:
try:
r = requests.get(m3u_url, headers=UA, timeout=10)
if r.status_code != 200: continue
text = r.text
lines = text.strip().split("\n")
current_channel = None
for line in lines:
s = line.strip()
if s.startswith("#EXTINF"):
m = re.search(r'tvg-id="?(\w+)"?', s)
if m: current_channel = m.group(1).lower()
else:
m2 = re.search(r'(VTV\d+|vtv\d+)', s, re.IGNORECASE)
if m2: current_channel = m2.group(1).lower()
else: current_channel = None
elif s.startswith("http") and current_channel:
ch_id = current_channel
ch_map = {"vtv1":"vtv1","vtv2":"vtv2","vtv3":"vtv3","vtv4":"vtv4","vtv5":"vtv5","vtv6":"vtv6","vtv7":"vtv7","vtv8":"vtv8","vtv9":"vtv9","vtv10":"vtv10","vtvcầnthơ":"vtv10","vtvcantho":"vtv10","vtvcan-tho":"vtv10"}
if ch_id in ch_map: ch_id = ch_map[ch_id]
if ch_id in CHANNEL_URLS and ch_id not in all_urls:
if CHANNEL_URLS[ch_id] and HARDCODED_URLS.get(ch_id): current_channel = None; continue
if ".flv" in s: all_urls[ch_id] = s
elif ch_id not in all_urls or ".flv" not in all_urls.get(ch_id,""):
if ch_id not in all_urls: all_urls[ch_id] = s
current_channel = None
if len(all_urls) >= 10: break
except: pass
with _channel_urls_lock:
found = 0
for ch_id in CHANNEL_URLS:
if CHANNEL_URLS[ch_id] and HARDCODED_URLS.get(ch_id): continue
if ch_id in all_urls: CHANNEL_URLS[ch_id] = all_urls[ch_id]; found += 1
print(f"[M3U] Found {found}/10 channels (hardcoded: vtv6, vtv10)")
def get_channel_url(channel_id):
_fetch_m3u()
with _channel_urls_lock: return CHANNEL_URLS.get(channel_id)
# ===================== API ENDPOINTS =====================
@router.get("/api/vtv/streams")
def api_vtv_streams():
urls = dict(CHANNEL_URLS)
result = {}
for ch_id in CHANNEL_NAMES:
stream_url = urls.get(ch_id)
iframe_url = IFRAME_URLS.get(ch_id)
is_flv = stream_url and ".flv" in stream_url
result[ch_id] = {
"name": CHANNEL_NAMES[ch_id],
"stream_url": stream_url,
"proxy_url": f"/api/proxy/flv?url={quote(stream_url, safe='')}" if stream_url and is_flv else "",
"proxy_url_hls": f"/api/proxy/stream?url={quote(stream_url, safe='')}" if stream_url and not is_flv else "",
"is_flv": is_flv,
"iframe_url": iframe_url,
"status": "ok"
}
return JSONResponse(result)
@router.get("/api/vtv/stream/{channel_id}")
def api_vtv_stream(channel_id: str):
channel_id = channel_id.lower().strip()
if channel_id not in CHANNEL_NAMES: return JSONResponse({"error":"not found"}, status_code=404)
stream_url = get_channel_url(channel_id)
iframe_url = IFRAME_URLS.get(channel_id)
is_flv = stream_url and ".flv" in stream_url
m3u8_proxy = ""
if stream_url and not is_flv and stream_url.startswith("http"):
m3u8_proxy = f"/api/proxy/m3u8?url={quote(stream_url, safe='')}"
return JSONResponse({
"name": CHANNEL_NAMES[channel_id],
"stream_url": stream_url,
"m3u8_proxy_url": m3u8_proxy,
"proxy_url": f"/api/proxy/flv?url={quote(stream_url, safe='')}" if stream_url and is_flv else "",
"proxy_url_hls": f"/api/proxy/stream?url={quote(stream_url, safe='')}" if stream_url and not is_flv else "",
"is_flv": is_flv,
"iframe_url": iframe_url,
"status": "ok"
})
@router.get("/api/vtv/m3u/refresh")
def api_vtv_m3u_refresh():
global _last_m3u_fetch
_last_m3u_fetch = 0
_fetch_m3u()
urls = dict(CHANNEL_URLS)
found = sum(1 for v in urls.values() if v)
return JSONResponse({"status":"refreshed","channels_found":found,"channels":{k:v for k,v in urls.items() if v}})
# ===================== EPG =====================
_epg_cache = {}; _epg_cache_time = 0; _EPG_CACHE_TTL = 600
VTV_CHANNEL_MAP = {"vtv1":"vtv1","vtv2":"vtv2","vtv3":"vtv3","vtv4":"vtv4","vtv5":"vtv5","vtv5-tay-nam-bo":"vtv5","vtv5-tay-nguyen":"vtv5","vtv6":"vtv6","vtv7":"vtv7","vtv8":"vtv8","vtv9":"vtv9","vtv-can-tho":"vtv10"}
def _parse_time(time_str, reference_date=None):
if not time_str: return None
time_str = time_str.strip().replace("h",":").replace("H",":")
m = re.search(r'(\d{1,2}):(\d{2})', time_str)
if m:
try:
hour, minute = int(m.group(1)), int(m.group(2))
base = reference_date or datetime.now(VN_TZ)
if hour < 5: base = base - timedelta(days=1) if base.hour >= 5 else base
dt = base.replace(hour=hour, minute=minute, second=0, microsecond=0)
if dt.tzinfo is None: dt = dt.replace(tzinfo=VN_TZ)
return dt
except: pass
return None
def _fetch_epg_from_vtv():
global _epg_cache, _epg_cache_time
now_ts = time.time()
if _epg_cache and now_ts - _epg_cache_time < _EPG_CACHE_TTL: return _epg_cache
epg_data = {}
now_vn = datetime.now(VN_TZ)
try:
h = {"User-Agent": UA["User-Agent"], "Accept-Language": "vi-VN,vi;q=0.9", "Referer": "https://vtv.vn/"}
r = requests.get("https://vtv.vn/lich-phat-song.htm", headers=h, timeout=20)
if r.status_code != 200: return epg_data
r.encoding = "utf-8"
soup = BeautifulSoup(r.text, "lxml")
channel_order = []
for link in soup.find_all('a', href=re.compile(r'truyen-hinh-truc-tuyen/([^.]+)\.htm')):
ch_id = re.search(r'truyen-hinh-truc-tuyen/([^.]+)\.htm', link.get('href',''))
if ch_id and ch_id.group(1) not in channel_order: channel_order.append(ch_id.group(1))
containers = soup.find_all('ul', class_=re.compile(r'\bprograms\b'))
for i, container in enumerate(containers):
if i >= len(channel_order): break
vtv_id = channel_order[i]
our_id = VTV_CHANNEL_MAP.get(vtv_id, vtv_id)
if our_id not in epg_data: epg_data[our_id] = []
for li in container.find_all('li', class_=re.compile(r'\bprogram\b')):
t = li.find('span', class_=re.compile(r'\btime\b'))
title_el = li.find('span', class_=re.compile(r'\btitle\b'))
genre = li.find('a', class_=re.compile(r'\bgenre\b'))
time_str = t.get_text(strip=True) if t else ""
title = genre.get_text(strip=True) if genre else ""
if not title and title_el: title = title_el.get_text(strip=True)
if not time_str or not title: continue
start_dt = _parse_time(time_str, reference_date=now_vn)
if not start_dt: continue
epg_data[our_id].append({"time":time_str[:5],"title":title[:80],"start_dt":start_dt,"date":start_dt.strftime("%d/%m/%Y")})
for ch_id in epg_data: epg_data[ch_id].sort(key=lambda x: x.get("start_dt") or datetime.min)
except Exception as e: print(f"EPG error: {e}")
_epg_cache = epg_data; _epg_cache_time = now_ts
return epg_data
@router.get("/api/vtv/epg/{channel_id}")
def api_vtv_epg(channel_id: str):
channel_id = channel_id.lower().strip()
if channel_id not in CHANNEL_NAMES: return JSONResponse({"error":"channel not found"}, status_code=404)
epg_data = _fetch_epg_from_vtv()
programmes = epg_data.get(channel_id, [])
now = datetime.now(VN_TZ)
today = now.date()
result = []
today_progs = [p for p in programmes if p.get("start_dt") and p["start_dt"].date() == today] or programmes
for i, p in enumerate(today_progs):
start_dt = p.get("start_dt")
stop_dt = today_progs[i+1].get("start_dt") if i+1 < len(today_progs) else None
is_now = bool(start_dt and ((stop_dt and start_dt <= now < stop_dt) or start_dt <= now))
end_time = stop_dt.strftime("%H:%M") if stop_dt else ""
result.append({"time":p["time"],"title":p["title"],"end_time":end_time,"now":is_now,"date":p.get("date","")})
return JSONResponse({"channel":channel_id,"channel_name":CHANNEL_NAMES.get(channel_id,channel_id),"date":now.strftime("%Y-%m-%d"),"programs":result})
@router.get("/api/vtv/epg")
def api_vtv_epg_refresh():
global _epg_cache, _epg_cache_time
_epg_cache = {}; _epg_cache_time = 0
epg_data = _fetch_epg_from_vtv()
return JSONResponse({"status":"refreshed","channels":len(epg_data),"total":sum(len(v) for v in epg_data.values())})