| import os |
| import requests |
| from urllib.parse import urljoin, quote, unquote |
| from flask import Flask, Response, send_from_directory, request |
|
|
| app = Flask(__name__) |
|
|
| |
| |
| |
| DUCKDNS_DOMAIN = "chrtv" |
| DUCKDNS_TOKEN = "0a7471b5-56fc-4b60-a5d9-5381f66930d9" |
|
|
| def update_duckdns(): |
| try: |
| url = f"https://www.duckdns.org/update?domains={DUCKDNS_DOMAIN}&token={DUCKDNS_TOKEN}&ip=" |
| res = requests.get(url, timeout=10) |
| if "OK" in res.text: |
| print(f"[DuckDNS] Cập nhật Domain {DUCKDNS_DOMAIN}.duckdns.org THÀNH CÔNG!") |
| else: |
| print(f"[DuckDNS] Cập nhật thất bại: {res.text}") |
| except Exception as e: |
| print(f"[DuckDNS] Lỗi kết nối DuckDNS: {e}") |
|
|
| |
| update_duckdns() |
|
|
| |
| |
| |
| TARGET_SERVER = "http://23.237.104.106:8080" |
| AMAGI_STUDIO_C = "https://amg12058-c15studio-amg12058c1-lg-us-5787.playouts.now.amagi.tv/playlist.m3u8" |
| WARNER_TV = "http://190.61.90.17:40000/play/a0ho/index.m3u8" |
| PARAMOUNT = "http://206.212.244.63/137/index.m3u8" |
|
|
| KENSCOM_180 = "http://103.152.216.26:8443/live/DUC3mlZ3O2Z0i4May6u1cA/1785547208/180.m3u8" |
| KENSCOM_191 = "http://103.152.216.26:8443/live/DUC3mlZ3O2Z0i4May6u1cA/1785547208/191.m3u8" |
|
|
| HEADERS = { |
| "User-Agent": "VLC/3.0.20 LibVLC/3.0.20", |
| "Accept": "*/*", |
| "Accept-Language": "vi-VN,vi;q=0.9,en-US;q=0.8,en;q=0.7", |
| "Connection": "keep-alive", |
| "Host": "103.152.216.26:8443" |
| } |
|
|
| PUBLIC_BASE_URL = os.environ.get("PUBLIC_BASE_URL", "https://ken1402-antv-live.hf.space") |
|
|
| def add_cors_headers(response): |
| response.headers['Access-Control-Allow-Origin'] = '*' |
| response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS, HEAD' |
| response.headers['Access-Control-Allow-Headers'] = '*' |
| return response |
|
|
| @app.route('/') |
| def home(): |
| return f"IPTV Server & ErsatzTV Engine is Running! DuckDNS Active: {DUCKDNS_DOMAIN}.duckdns.org", 200 |
|
|
| |
| |
| |
| @app.route('/playlist.m3u', methods=['GET', 'OPTIONS']) |
| def get_playlist(): |
| if request.method == 'OPTIONS': |
| return add_cors_headers(Response(status=204)) |
|
|
| m3u_content = f"""#EXTM3U |
| #EXTINF:-1 tvg-id="HBO" group-title="US Live" tvg-logo="https://i.imgur.com/vHqQZ4u.png",HBO Movies HD |
| {PUBLIC_BASE_URL}/stream/hbo/master.m3u8 |
| #EXTINF:-1 tvg-id="WarnerTV" group-title="US Live" tvg-logo="https://i.imgur.com/2Xy349p.png",Warner TV HD |
| {PUBLIC_BASE_URL}/stream/warnertv |
| #EXTINF:-1 tvg-id="Paramount" group-title="US Live" tvg-logo="https://i.imgur.com/Q23kLMl.png",Paramount Network HD |
| {PUBLIC_BASE_URL}/stream/paramount |
| #EXTINF:-1 tvg-id="StudioC" group-title="US Live",Amagi Studio C HD |
| {PUBLIC_BASE_URL}/stream/studioc |
| #EXTINF:-1 tvg-id="Live180" group-title="VN Live",KensCom Channel 180 HD |
| {PUBLIC_BASE_URL}/stream/180.m3u8 |
| #EXTINF:-1 tvg-id="Live191" group-title="VN Live",KensCom Channel 191 HD |
| {PUBLIC_BASE_URL}/stream/191.m3u8""" |
| res = Response(m3u_content.strip(), mimetype='application/x-mpegurl') |
| return add_cors_headers(res) |
|
|
| |
| |
| |
| @app.route('/stream/hbo/<path:filename>', methods=['GET', 'OPTIONS']) |
| def serve_hbo(filename): |
| if request.method == 'OPTIONS': |
| return add_cors_headers(Response(status=204)) |
|
|
| folder_path = '/app/hls/hbo' |
| if filename in ["index.m3u8", "master.m3u8"]: |
| filename = "master.m3u8" |
| |
| file_path = os.path.join(folder_path, filename) |
| if not os.path.exists(file_path): |
| return "Stream đang khởi tạo...", 503 |
|
|
| res = send_from_directory(folder_path, filename) |
| add_cors_headers(res) |
| if filename.endswith('.m3u8'): |
| res.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate, max-age=0' |
| else: |
| res.headers['Cache-Control'] = 'max-age=3600' |
| return res |
|
|
| |
| |
| |
| def handle_hls_proxy(default_target_url, endpoint_path): |
| if request.method == 'OPTIONS': |
| return add_cors_headers(Response(status=204)) |
|
|
| target_url = request.args.get('url') or default_target_url |
| target_url = unquote(target_url) |
| |
| req_headers = HEADERS.copy() |
| if "amagi.tv" in target_url: |
| req_headers.pop("Host", None) |
|
|
| try: |
| resp = requests.get(target_url, headers=req_headers, timeout=10, stream=True) |
| if resp.status_code != 200: |
| return add_cors_headers(Response(f"Upstream Error: HTTP {resp.status_code}", status=resp.status_code)) |
|
|
| raw_bytes = resp.content |
| raw_text = raw_bytes.decode('utf-8-sig', errors='ignore') |
|
|
| if raw_text.strip().startswith('#EXTM3U'): |
| lines = raw_text.splitlines() |
| rewritten_lines = [] |
| for line in lines: |
| line_str = line.strip() |
| if not line_str or line_str.startswith('#'): |
| rewritten_lines.append(line_str) |
| else: |
| full_url = urljoin(target_url, line_str) |
| rewritten_lines.append(f"{endpoint_path}?url={quote(full_url)}") |
| |
| res = Response("\n".join(rewritten_lines).strip(), status=200, mimetype='application/x-mpegurl') |
| res.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' |
| return add_cors_headers(res) |
| else: |
| res = Response(raw_bytes, status=200, mimetype='video/mp2t') |
| return add_cors_headers(res) |
| except Exception as e: |
| return add_cors_headers(Response(f"Proxy Exception: {str(e)}", status=502)) |
|
|
| @app.route('/stream/studioc', methods=['GET', 'OPTIONS']) |
| def proxy_studioc(): return handle_hls_proxy(AMAGI_STUDIO_C, "/stream/studioc") |
|
|
| @app.route('/stream/warnertv', methods=['GET', 'OPTIONS']) |
| def proxy_warnertv(): return handle_hls_proxy(WARNER_TV, "/stream/warnertv") |
|
|
| @app.route('/stream/paramount', methods=['GET', 'OPTIONS']) |
| def proxy_paramount(): return handle_hls_proxy(PARAMOUNT, "/stream/paramount") |
|
|
| @app.route('/stream/180.m3u8', methods=['GET', 'OPTIONS']) |
| def proxy_180(): return handle_hls_proxy(KENSCOM_180, "/stream/180.m3u8") |
|
|
| @app.route('/stream/191.m3u8', methods=['GET', 'OPTIONS']) |
| def proxy_191(): return handle_hls_proxy(KENSCOM_191, "/stream/191.m3u8") |
|
|
| if __name__ == '__main__': |
| app.run(host='0.0.0.0', port=7860, threaded=True) |