PORT = 24689
LANGUAGE = "de"
REGION = "DE"

import json
import gzip
import requests
import uuid
import time
import socket
import re
from collections import defaultdict
from flask import Flask, request, Response, abort
from urllib.parse import urljoin
import os

REF_DATA = {
  "mode": "magenta",
  "groups": [
    {"group-title":"OeR","channels":[{"channel":"Das Erste"},{"channel":"ZDF"},{"channel":"phoenix"},{"channel":"ARD-alpha"},{"channel":"ZDFneo"},{"channel":"ONE"},{"channel":"ZDFinfo"},{"channel":"ARTE"},{"channel":"3sat"},{"channel":"WDR Köln"},{"channel":"NDR Niedersachsen"},{"channel":"SWR Rheinland-Pfalz"},{"channel":"SR"},{"channel":"RBB Berlin"},{"channel":"MDR Sachsen"},{"channel":"hr-fernsehen"},{"channel":"BR Nord"},{"channel":"ORF 1"},{"channel":"ORF 2"},{"channel":"tagesschau24"},{"channel":"ntv"},{"channel":"WELT"},{"channel":"euronews"},{"channel":"CNN Europe"},{"channel":"Al Jazeera English"}]},
    {"group-title":"Private","channels":[{"channel":"RTL"},{"channel":"SAT.1"},{"channel":"ProSieben"},{"channel":"Kabel Eins"},{"channel":"RTLZWEI"},{"channel":"Super RTL"},{"channel":"VOX"},{"channel":"DMAX"},{"channel":"RTLup"},{"channel":"RTL Crime"},{"channel":"RTL Passion"},{"channel":"NITRO"},{"channel":"ProSieben MAXX"},{"channel":"ProSieben FUN"},{"channel":"SAT.1 emotions"},{"channel":"SAT.1 GOLD"},{"channel":"VOXup"},{"channel":"sixx"},{"channel":"Romance TV"},{"channel":"TLC"},{"channel":"TELE 5"},{"channel":"DF1"},{"channel":"Comedy Central"}]},
    {"group-title":"Sky","channels":[{"channel":"Sky One"},{"channel":"Sky Replay"},{"channel":"Sky Showcase"},{"channel":"Sky Atlantic"},{"channel":"Sky Action"},{"channel":"Sky Cinema Highlights"},{"channel":"Sky Cinema Classics"},{"channel":"Sky Cinema Family"},{"channel":"Sky Cinema Premieren"},{"channel":"Sky Krimi"},{"channel":"SyFy"},{"channel":"WarnerTV Film"},{"channel":"WarnerTV Serie"},{"channel":"WarnerTV Comedy"},{"channel":"Universal TV"},{"channel":"13th Street"},{"channel":"Kabel Eins CLASSICS"},{"channel":"KinoweltTV"},{"channel":"AXN White"},{"channel":"AXN Black"},{"channel":"Heimatkanal"}]},
    {"group-title":"Doku","channels":[{"channel":"Sky Crime"},{"channel":"Sky Documentaries"},{"channel":"Sky Nature"},{"channel":"Animal Planet"},{"channel":"GEO Television"},{"channel":"Discovery Channel"},{"channel":"National Geographic WILD"},{"channel":"National Geographic"},{"channel":"History"},{"channel":"Crime + Investigation"},{"channel":"Curiosity Channel"},{"channel":"SPIEGEL Geschichte"},{"channel":"Welt der Wunder"},{"channel":"N24 Doku"},{"channel":"Kabel Eins Doku"},{"channel":"RTL Living"},{"channel":"travelxp"},{"channel":"BonGusto"},{"channel":"Marco Polo TV"}]},
    {"group-title":"Bundesliga","channels":[{"channel":"DAZN 1"},{"channel":"DAZN 2"},{"channel":"Sky Sport Bundesliga"},{"channel":"Sky Sport Bundesliga 1"},{"channel":"Sky Sport Bundesliga 2"},{"channel":"Sky Sport Bundesliga 3"},{"channel":"Sky Sport Bundesliga 4"},{"channel":"Sky Sport Bundesliga 5"},{"channel":"Sky Sport Bundesliga 6"},{"channel":"Sky Sport Bundesliga 7"},{"channel":"Sky Sport Bundesliga 8"},{"channel":"Sky Sport Bundesliga 9"},{"channel":"Sky Sport Bundesliga 10"}]},
    {"group-title":"Dazn","channels":[{"channel":"DAZN 1"},{"channel":"DAZN 2"},{"channel":"DAZN RISE"},{"channel":"DAZN FAST+"},{"channel":"Eurosport 1"},{"channel":"Eurosport 2"},{"channel":"SPORTDIGITAL FUSSBALL"},{"channel":"Billiard TV"},{"channel":"Red Bull TV"},{"channel":"PowerSports World"},{"channel":"ACL Cornhole TV"},{"channel":"PLL Network"},{"channel":"Unbeaten"},{"channel":"MLB Network"},{"channel":"NFL Network"}]},
    {"group-title":"Sport","channels":[{"channel":"Sky Sport F1"},{"channel":"Sky Sport Top Event"},{"channel":"Sky Sport Premier League"},{"channel":"Sky Sport Mix"},{"channel":"Sky Sport Tennis"},{"channel":"Sky Sport Golf"},{"channel":"Sky Sport News"},{"channel":"Sky Sport 1"},{"channel":"Sky Sport 2"},{"channel":"Sky Sport 3"},{"channel":"Sky Sport 4"},{"channel":"Sky Sport 5"},{"channel":"Sky Sport 6"},{"channel":"Sky Sport 7"},{"channel":"Sky Sport 8"},{"channel":"Sky Sport 9"},{"channel":"Sky Sport 10"},{"channel":"Sky Sport Austria 1"},{"channel":"Sky Sport Austria 2"},{"channel":"Sky Sport Austria 3"},{"channel":"Sky Sport Austria 4"},{"channel":"Sky Sport Austria 5"},{"channel":"Sky Sport Austria 6"},{"channel":"Sky Sport Austria 7"},{"channel":"SPORT1"},{"channel":"SPORTDIGITAL1+"},{"channel":"ORF Sport +"},{"channel":"auto motor sport channel"},{"channel":"Eurosport 1"},{"channel":"Eurosport 2"},{"channel":"DAZN RISE"},{"channel":"DAZN FAST"},{"channel":"MOTORVISION+"},{"channel":"eSportsONE"},{"channel":"More Than Sports TV"},{"channel":"Sportdigital EDGE"},{"channel":"Red Bull TV Extreme"}]},
    {"group-title":"Magenta Sport","channels":[{"channel":"Sport 1 - myTeamTV"},{"channel":"Sport 2 - myTeamTV"},{"channel":"Sport 3 - myTeamTV"},{"channel":"Sport 4 - myTeamTV"},{"channel":"Sport 5 - myTeamTV"},{"channel":"Sport 6 - myTeamTV"},{"channel":"Sport 7 - myTeamTV"},{"channel":"Sport 8 - myTeamTV"},{"channel":"Sport 9 - myTeamTV"},{"channel":"Sport 10 - myTeamTV"},{"channel":"Sport 11 - myTeamTV"},{"channel":"Sport 12 - myTeamTV"},{"channel":"Sport 13 - myTeamTV"},{"channel":"Sport 14 - myTeamTV"},{"channel":"Sport 15 - myTeamTV"},{"channel":"Sport 16 - myTeamTV"},{"channel":"Sport 17 - myTeamTV"},{"channel":"Sport 18 - myTeamTV"}]}
  ]
}

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"

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(r):
    if not r or not getattr(r, "content", None):
        return {}
    if r.content[:2] == b"\x1f\x8b":
        try:
            return json.loads(gzip.decompress(r.content))
        except:
            return {}
    try:
        return r.json()
    except:
        return {}

def ip():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        a = s.getsockname()[0]
        s.close()
#        return a
        return "skiptv.org"
    except:
        return "127.0.0.1"

def norm_key(name):
    if not isinstance(name, str):
        return ""
    n = name.upper()
    n = re.sub(r"\bKABEL\s*1\b", "KABEL EINS", n)
    n = re.sub(r"\bPRO\s*7\b", "PROSIEBEN", n)
    n = re.sub(r"\bS\.\s*", "SKY ", n)
    n = re.sub(r"\bSKY\s+BUNDESLIGA\b", "SKY SPORT BUNDESLIGA", n)
    n = re.sub(r"\s*\.[SB C]$", "", n)
    n = re.sub(r"(?:4K|UHD|FHD|HD\+?|SD|HEVC|H\.?265|H\.?264|AVC|ʜᴅ)", "", n)
    n = re.sub(r"\([^)]*\)", "", n)
    n = re.sub(r"[^A-Z0-9]", "", n)
    return n.lower()

def clean_display_name(n):
    if not isinstance(n, str):
        return ""
    s = n
    s = re.sub(r"\[[^\]]*\]", "", s)
    s = re.sub(r"\([^)]*\)", "", s)
    s = re.sub(r"\b(?:4K|UHD|FHD|HD\+?|SD|HEVC|H\.?265|H\.?264|AVC|ʜᴅ)\b", "", s, flags=re.IGNORECASE)
    s = re.sub(r"\s*\.[sb c]$", "", s, flags=re.IGNORECASE)
    s = re.sub(r"\s+", " ", s)
    return s.strip()

def base_name(name):
    if not isinstance(name, str):
        return ""
    n = name.upper()
    n = re.sub(r"\bKABEL\s*1\b", "KABEL EINS", n)
    n = re.sub(r"\bPRO\s*7\b", "PROSIEBEN", n)
    n = re.sub(r"\bS\.\s*SPORT\b", "SKY SPORT", n)
    n = re.sub(r"\bS\.\s*", "SKY ", n)
    n = re.sub(r"\bSKY\s+BUNDESLIGA\b", "SKY SPORT BUNDESLIGA", n)
    n = re.sub(r"\s*\.[SB C]$", "", n)
    n = re.sub(r"(?:4K|UHD|FHD|HD\+?|SD|HEVC|H\.?265|H\.?264|AVC|ʜᴅ)", "", n)
    n = re.sub(r"\[[^\]]*\]", "", n)
    n = re.sub(r"\([^)]*\)", "", n)
    n = re.sub(r"[^A-Z0-9 ]", " ", n)
    n = re.sub(r"\s+\d+\s*$", "", n)
    n = re.sub(r"\s+", " ", n).strip()
    return n

session = requests.Session()
session.headers.update(HEADERS)
session.get(GEOIP_URL, timeout=8).raise_for_status()

uid = str(uuid.uuid4())
ts = int(time.time() * 1000)

init = {
    "reason": "app-focus",
    "locale": LANGUAGE,
    "theme": "dark",
    "metadata": {
        "device": {"type": "desktop", "uniqueId": uid},
        "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": ts,
    "lastAppStart": ts,
    "ipLocation": None,
    "adblockEnabled": True,
    "proxy": {"supported": ["ss"], "engine": "Mu", "enabled": False, "autoServer": True},
    "iap": {"supported": False},
}

addon_sig = decode(session.post(PING_URL, json=init, timeout=8)).get("addonSig")
if not addon_sig:
    raise RuntimeError("no addonSig")

COMMON_HEADERS = {
    "content-type": "application/json; charset=utf-8",
    "mediahubmx-signature": addon_sig,
    "user-agent": "MediaHubMX/2",
    "accept": "*/*",
    "Accept-Language": LANGUAGE,
    "Accept-Encoding": "gzip, deflate",
    "Connection": "close",
}

items = []
cursor = None
while True:
    p = {
        "language": LANGUAGE,
        "region": REGION,
        "catalogId": "iptv",
        "id": "iptv",
        "adult": False,
        "search": "",
        "sort": "",
        "filter": {},
        "cursor": cursor,
        "clientVersion": "3.1.4",
    }
    d = decode(session.post(CATALOG_URL, json=p, headers=COMMON_HEADERS, timeout=8))
    for i in d.get("items", []):
        if i.get("type") == "iptv":
            nm = i.get("name", "")
            items.append({
                "id": i["ids"]["id"],
                "url": i["url"],
                "name": nm,
                "_key": norm_key(nm),
                "_base": base_name(nm),
                "group": "",
            })
    cursor = d.get("nextCursor")
    if cursor is None:
        break

ref_map = {}
ref_order = []
for g in REF_DATA["groups"]:
    for ch in g["channels"]:
        k = norm_key(ch["channel"])
        ref_map[k] = g["group-title"]
        ref_order.append(k)

base_count = defaultdict(int)
for i in items:
    if i["_base"]:
        base_count[i["_base"]] += 1

def group_for_item(i):
    k = i["_key"]
    if k in ref_map:
        return ref_map[k]
    sg = special_group(i["name"])
    if sg:
        return sg
    b = i["_base"]
    if b and base_count.get(b, 0) >= 5:
        return b
    return "Germany"

def special_group(name):
    if not isinstance(name, str):
        return None
    up = name.upper()
    up = re.sub(r"\bKABEL\s*1\b", "KABEL EINS", up)
    up = re.sub(r"\bS\.\s*SPORT\b", "SKY SPORT", up)
    up = re.sub(r"\bS\.\s*", "SKY ", up)
    if re.match(r"^(WDR|MDR|NDR|RBB|RBW)\b", up):
        return "OeR"
    if re.search(r"\bRTL\s*NITRO\b", up):
        return "Private"
    if re.match(r"\b(BR|BR\s+TV)\b", up):
        return "OeR"
    if up.startswith("EURONEWS"):
        return "Sport"
    if re.match(r"^SWR\b", up):
       return "OeR"
    if up.startswith("WELT"):
        return "OeR"
    if up.startswith("SKY PREMIEREN"):
        return "Sky"
    if "EUROSPORT" in up:
        return "Sport"
    if re.search(r"\bSERVUS\s*TV\b", up):
        return "Private"
    if up.startswith("BLUETV"):
        return "BLUETV"
    if re.search(r"\bSILVERLINE\b", up):
        return "Sky"
    if re.match(r"\bSKY\s*\d+\b", up):
        return "Sky"
    if re.search(r"\bSKY\s*(BEST\s*OF|BEST)\b", up):
        return "Sky"
    if re.search(r"\bSKY\s*BUNDESLIGA\d+\b", up):
        return "Bundesliga"
    if re.search(r"\bSKY\s*(CLASSICS|COMEDY|FAMILY|FUN|HITS|SPECIAL|THRILLER)\b", up):
        return "Sky"
    if re.search(r"\bSKY\s*SERIEN\s*&?\s*SHOWS\b", up):
        return "Sky"
    if up.startswith("SKY BOX"):
        return "Sky"
    if up.startswith("SKY CINEMA"):
        return "Sky"
    if up.startswith("TNT"):
        return "Sky"
    if up.startswith("WARNER"):
        return "Sky"
    if "SKY SELECT" in up:
        return "Sky Select"
    if "SKY SPORT BUNDESLIGA" in up or re.search(r"\bSKY\s+BUNDESLIGA\b", up):
        return "Bundesliga"
    if re.search(r"\bDB\s+LIGA\b", up):
        return "DB LIGA"
    if up.startswith("DAZN") or "DAZN SPORT" in up:
        return "Dazn"
    if up.startswith("SKY SPORT"):
        return "Sport"
    if "SPORT DIGITAL" in up or "SPORTDIGITAL" in up:
        return "Sport"
    if "MOTOR" in up or "AUTO" in up:
        return "Sport"
    if "NAT GEO" in up:
        return "Doku"
    if "SPIEGEL" in up:
        return "Doku"
    return None

ordered = []
used = set()

for k in ref_order:
    for i in items:
        if i["_key"] == k and i["id"] not in used:
            i["group"] = ref_map[k]
            ordered.append(i)
            used.add(i["id"])

for i in items:
    if i["id"] in used:
        continue
    sg = special_group(i["name"])
    if sg:
        i["group"] = sg
    elif base_count.get(i["_base"], 0) >= 5:
        i["group"] = i["_base"]
    else:
        i["group"] = "Germany"
    ordered.append(i)

items = ordered

def resolve_iptv(url):
    p = {"language": LANGUAGE, "region": REGION, "url": url, "clientVersion": "3.1.4"}
    r = decode(session.post(RESOLVE_URL, json=p, headers=COMMON_HEADERS, timeout=6))
    return r[0]["url"] if r else None

def save_tv_playlist():
    a = ip()
    o = "#EXTM3U\n"
    for i in items:
        name = clean_display_name(i["name"])
        o += f'#EXTINF:-1 tvg-id="{i["id"]}" tvg-name="{name}" tvg-logo="" group-title="{i["group"]}",{name}\n'
        o += f"http://{a}:{PORT}/vavoo?channel={i['id']}\n"
    open("tv.m3u", "w", encoding="utf-8").write(o)

save_tv_playlist()

app = Flask(__name__)

@app.route("/vavoo")
def vavoo():
    cid = request.args.get("channel")
    item = next((x for x in items if x["id"] == cid), None)
    if not item:
        abort(404)
    u = resolve_iptv(item["url"])
    if not u:
        abort(404)
    r = requests.get(u, timeout=8, headers={
        "User-Agent": HEADERS["user-agent"],
        "Accept-Language": LANGUAGE,
        "Accept": "*/*",
    })
    base = u.rsplit("/", 1)[0] + "/"
    lines = r.text.splitlines()
    patched = "\n".join(l if l.startswith("#") else urljoin(base, l) for l in lines)
    return Response(patched, mimetype="application/vnd.apple.mpegurl")

@app.route("/playlist.m3u")
def playlist():
    save_tv_playlist()
    return Response(open("tv.m3u", "r", encoding="utf-8").read(), mimetype="application/x-mpegURL")

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=PORT)
