###############################################################
PORT = 4323
ADDON_SIG_TTL = 600 # 10 Minuten
###############################################################
import json
import gzip
import requests
import os
from io import BytesIO
import uuid
from flask import Flask, request, Response, abort
import time
import socket
import threading
# ---------------- LÄNDER ----------------
REGIONS = [
{"language": "nl", "region": "BE"},
]
GEOIP_URL = "https://www.vavoo.tv/geoip"
PING_URL = "https://www.vavoo.tv/api/app/ping"
CATALOG_URL = "https://vavoo.to/mediahubmx-catalog.json"
RESOLVE_URL = "https://vavoo.to/mediahubmx-resolve.json"
LANGUAGE = "de"
REGION = "DE"
HEADERS = {
"accept": "*/*",
"user-agent": "electron-fetch/1.0 electron (+https://github.com/arantes555/electron-fetch)",
"Accept-Language": LANGUAGE,
"Accept-Encoding": "gzip, deflate",
"Connection": "close",
}
def decode_response(resp):
if resp.content[:2] == b'\x1f\x8b':
return json.loads(gzip.decompress(resp.content))
return resp.json()
session = requests.Session()
session.headers.update(HEADERS)
# ---------------- GEO + INITIAL PING ----------------
r_geo = session.get(GEOIP_URL)
r_geo.raise_for_status()
geo_data = decode_response(r_geo)
unique_id = str(uuid.uuid4())
current_timestamp = int(time.time() * 1000)
initial_payload = {
"reason": "app-focus",
"locale": "de",
"theme": "dark",
"metadata": {
"device": {"type": "desktop", "uniqueId": unique_id},
"os": {"name": "win32", "version": "Windows 10 Pro", "abis": ["x64"], "host": "Lenovo"},
"app": {"platform": "electron"},
"version": {"package": "tv.vavoo.app", "binary": "3.1.8", "js": "3.1.8"},
},
"appFocusTime": 0,
"playerActive": False,
"playDuration": 0,
"devMode": False,
"hasAddon": True,
"castConnected": False,
"package": "tv.vavoo.app",
"version": "3.1.8",
"process": "app",
"firstAppStart": current_timestamp,
"lastAppStart": current_timestamp,
"ipLocation": None,
"adblockEnabled": True,
"proxy": {"supported": ["ss"], "engine": "Mu", "enabled": False, "autoServer": True},
"iap": {"supported": False},
}
r1 = session.post(PING_URL, json=initial_payload)
r1.raise_for_status()
data1 = decode_response(r1)
# ---------------- ADDON SIG MANAGEMENT ----------------
addon_sig_lock = threading.Lock()
addon_sig_data = {
"sig": data1.get("addonSig"),
"ts": time.time()
}
def refresh_addon_sig_if_needed(force=False):
with addon_sig_lock:
now = time.time()
if not force and now - addon_sig_data["ts"] < ADDON_SIG_TTL:
return addon_sig_data["sig"]
payload = initial_payload.copy()
payload["lastAppStart"] = int(time.time() * 1000)
r = session.post(PING_URL, json=payload)
r.raise_for_status()
data = decode_response(r)
sig = data.get("addonSig")
if not sig:
raise RuntimeError("No addonSig received")
addon_sig_data["sig"] = sig
addon_sig_data["ts"] = now
print("[✓] addonSig refreshed")
return sig
# ---------------- CATALOG LOAD (MULTI-REGION) ----------------
items_by_region = {}
for entry in REGIONS:
LANGUAGE = entry["language"]
REGION = entry["region"]
region_key = f"{LANGUAGE}-{REGION}"
print(f"[+] Lade Katalog für {region_key}")
catalog_headers = {
"content-type": "application/json; charset=utf-8",
"mediahubmx-signature": addon_sig_data["sig"],
"user-agent": "MediaHubMX/2",
"accept": "*/*",
"Accept-Language": LANGUAGE,
"Accept-Encoding": "gzip, deflate",
"Connection": "close",
}
cursor = None
while True:
catalog_payload = {
"language": LANGUAGE,
"region": REGION,
"catalogId": "iptv",
"id": "iptv",
"adult": False,
"search": "",
"sort": "",
"filter": {},
"cursor": cursor,
"clientVersion": "3.0.2"
}
r_catalog = session.post(CATALOG_URL, json=catalog_payload, headers=catalog_headers)
r_catalog.raise_for_status()
catalog_data = decode_response(r_catalog)
for item in catalog_data.get("items", []):
if item.get("type") == "iptv":
items_by_region.setdefault(region_key, []).append({
"id": item["ids"]["id"],
"url": item["url"],
"name": item["name"],
"group": item["group"],
"logo": item["logo"],
"language": LANGUAGE,
"region": REGION
})
cursor = catalog_data.get("nextCursor")
if not cursor:
break
print(f"[✓] Gesamtanzahl IPTV-Sender: {sum(len(v) for v in items_by_region.values())}")
# ---------------- M3U SAVE (MULTI-FILE) ----------------
LOCAL_IP = "127.0.0.1"
def get_local_ip():
return LOCAL_IP
# Unterordner erstellen
PLAYLIST_DIR = "playlists"
os.makedirs(PLAYLIST_DIR, exist_ok=True)
def save_m3u_files():
local_ip = get_local_ip()
for region_key, items in items_by_region.items():
filename = os.path.join(PLAYLIST_DIR, f"vavoo_{region_key}.m3u")
m3u = "#EXTM3U\n"
for item in items:
m3u += (
f'#EXTINF:-1 tvg-id="{item["id"]}" '
f'tvg-name="{item["name"]}" '
f'tvg-logo="{item["logo"]}" '
f'group-title="{item["group"]} ({item["region"]})",{item["name"]}\n'
)
m3u += f"http://{local_ip}:{PORT}/vavoo?channel={item['id']}\n"
with open(filename, "w", encoding="utf-8") as f:
f.write(m3u)
print(f"[✓] {filename} geschrieben")
def save_master_m3u():
master = "#EXTM3U\n"
for region_key in items_by_region.keys():
filename = f"playlists/vavoo_{region_key}.m3u"
master += f'#EXTINF:-1 group-title="Master",{region_key}\n'
master += f"{filename}\n"
with open("vavoo_master.m3u", "w", encoding="utf-8") as f:
f.write(master)
print("[✓] vavoo_master.m3u geschrieben")
save_m3u_files()
save_master_m3u()
# ---------------- FLASK APP ----------------
app = Flask(__name__)
@app.route("/vavoo")
def stream_proxy():
channel_id = request.args.get("channel")
if not channel_id:
abort(400)
channel = None
for region_items in items_by_region.values():
for i in region_items:
if i["id"] == channel_id:
channel = i
break
if not channel:
abort(404)
try:
sig = refresh_addon_sig_if_needed()
except Exception as e:
abort(502, str(e))
resolve_headers = {
"content-type": "application/json; charset=utf-8",
"mediahubmx-signature": sig,
"user-agent": "MediaHubMX/2",
"accept": "*/*",
"Accept-Language": channel["language"],
"Accept-Encoding": "gzip, deflate",
"Connection": "close",
}
resolve_payload = {
"language": channel["language"],
"region": channel["region"],
"url": channel["url"],
"clientVersion": "3.0.2"
}
r_resolve = session.post(RESOLVE_URL, json=resolve_payload, headers=resolve_headers)
if r_resolve.status_code == 403:
sig = refresh_addon_sig_if_needed(force=True)
resolve_headers["mediahubmx-signature"] = sig
r_resolve = session.post(RESOLVE_URL, json=resolve_payload, headers=resolve_headers)
r_resolve.raise_for_status()
result = decode_response(r_resolve)
if result:
return Response(status=302, headers={"Location": result[0]["url"]})
abort(404)
# ---------------- ALL-IN-ONE PLAYLIST ----------------
@app.route("/playlist.m3u")
def playlist():
local_ip = get_local_ip()
m3u = "#EXTM3U\n"
for region_items in items_by_region.values():
for item in region_items:
m3u += (
f'#EXTINF:-1 tvg-id="{item["id"]}" '
f'tvg-name="{item["name"]}" '
f'tvg-logo="{item["logo"]}" '
f'group-title="{item["group"]} ({item["region"]})",{item["name"]}\n'
)
m3u += f"http://{local_ip}:{PORT}/vavoo?channel={item['id']}\n"
return Response(m3u, mimetype="application/x-mpegURL")
# ---------------- MASTER PLAYLIST ----------------
@app.route("/master.m3u")
def master_playlist():
try:
with open("vavoo_master.m3u", "r", encoding="utf-8") as f:
content = f.read()
return Response(content, mimetype="application/x-mpegURL")
except FileNotFoundError:
return Response("#EXTM3U\n# Master playlist not found\n", mimetype="application/x-mpegURL")
# ---------------- START SERVER ----------------
if __name__ == "__main__":
app.run(host="0.0.0.0", port=PORT)