#########################################################################################
EMAIL = ""
PASSWORD = ""
COMPETITION_UID = "3e8ced55-b7a9-48c2-859f-ed2e1003f6b8"
SEASON_UID = "8c0b72e9-a9f3-4fad-823f-d958ae066a26"
#########################################################################################
import json, os, time, re, unicodedata, requests
from datetime import datetime, timezone
SIGNIN_URL = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword"
REFRESH_URL = "https://securetoken.googleapis.com/v1/token"
API_KEY = "AIzaSyBL6EHNfDVU5kvvoUvsl8u8dmI2_BuYCQM"
TIMEOUT = 20
STREAM_BASE = "https://leagues-live.fra1.cdn.digitaloceanspaces.com"
OUTPATH = "leagues.m3u"
# How many days ahead to include (0 = only today)
EVENTS_LOOKAHEAD_DAYS = 2
SHOW_PAST_TODAY_EVENTS = True
def signin():
url = f"{SIGNIN_URL}?key={API_KEY}"
payload = {
"returnSecureToken": True,
"email": EMAIL,
"password": PASSWORD,
"clientType": "CLIENT_TYPE_WEB"
}
r = requests.post(url, json=payload, timeout=TIMEOUT)
r.raise_for_status()
return r.json()
def refresh(refresh_token):
url = f"{REFRESH_URL}?key={API_KEY}"
form = f"grant_type=refresh_token&refresh_token={requests.utils.quote(refresh_token, safe='')}"
headers = {"content-type": "application/x-www-form-urlencoded"}
r = requests.post(url, headers=headers, data=form, timeout=TIMEOUT)
r.raise_for_status()
return r.json()
def leagues_get(url):
r = requests.get(url, timeout=TIMEOUT)
r.raise_for_status()
return r.json()
def get_next_possible_month(cid):
return leagues_get(f"https://www.leagues.football/json/2/fixture/calendar/{cid}/nextPossibleMonth.json")
def get_calendar_for(cid, y, m):
return leagues_get(f"https://www.leagues.football/json/2/fixture/calendar/{cid}/{y}/{m}.json")
def get_fixture_single(fid):
return leagues_get(f"https://www.leagues.football/json/2/fixture/single/{fid}.json")
def _normalize_for_slug(s):
s = s.lower().replace("♀"," w ").replace("ä","ae").replace("ö","oe").replace("ü","ue").replace("ß","ss")
s = unicodedata.normalize("NFKD", s)
s = "".join(c for c in s if not unicodedata.combining(c))
return re.sub(r"[^a-z0-9]","",s)
def make_cdn_slug_from_title(title):
if not title:
return None
parts = re.split(r"\s+vs\s+", title, flags=re.IGNORECASE, maxsplit=1)
base = _normalize_for_slug(parts[0].strip())
if not base.endswith("w"):
base += "w"
return base
def make_event_id(title: str) -> str:
if not title: return ""
return re.sub(r"[^a-z0-9]","",title.lower())
def parse_iso_to_epoch(s):
if not s:
return None
try:
dt = datetime.fromisoformat(s)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return int(dt.timestamp())
except Exception:
return None
def build_league_channels(month_meta, fixtures_detail):
fixtures=[]
for guid,det in fixtures_detail.items():
title = det.get("name") or det.get("title")
slug = make_cdn_slug_from_title(title)
stream_url = f"{STREAM_BASE}/{slug}/hd/stream.m3u8" if slug else None
fixtures.append({
"title": title,
"start": det.get("startDate"),
"start_ts": parse_iso_to_epoch(det.get("startDate")),
"stream_url": stream_url
})
fixtures.sort(key=lambda x:(x.get("start_ts") is None,x.get("start_ts") or 0))
return fixtures
def filter_channels_for_export(channels_json: list) -> list:
now = int(time.time())
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
today_start = int(today.timestamp())
today_end = today_start + 24*60*60 - 1
end_limit = now + (EVENTS_LOOKAHEAD_DAYS * 86400) + 86399
out = []
for ch in channels_json:
start_ts = ch.get("start_ts") or 0
if not start_ts:
continue
if not SHOW_PAST_TODAY_EVENTS and start_ts < today_start:
continue
if EVENTS_LOOKAHEAD_DAYS == 0:
if not (today_start <= start_ts <= today_end):
continue
else:
if start_ts > end_limit:
continue
out.append(ch)
out.sort(key=lambda x: (x.get("start_ts") is None, x.get("start_ts") or 0))
return out
def _escape_attr(s: str) -> str:
return (s or "").replace('"', r'\"')
def export_m3u(channels: list, outpath: str = OUTPATH) -> str:
lines = ["#EXTM3U"]
for ch in channels:
title = ch.get("title") or "Match"
tvg_id = make_event_id(title)
start_ts = ch.get("start_ts")
display_dt = ""
if start_ts:
try:
dt = datetime.fromtimestamp(start_ts)
display_dt = f" ({dt.strftime('%Y-%m-%d %H:%M')})"
except Exception:
pass
display_name = f"{title}{display_dt}"
tvg_name = display_name
group = "Leagues"
logo = ""
url = ch.get("stream_url")
if not url:
continue
extinf = (
f'#EXTINF:-1 tvg-id="{_escape_attr(tvg_id)}" '
f'tvg-name="{_escape_attr(tvg_name)}" '
f'tvg-logo="{_escape_attr(logo)}" '
f'group-title="{_escape_attr(group)}",{display_name}'
)
lines.append(extinf)
lines.append(url)
content = "\n".join(lines) + "\n"
with open(outpath, "w", encoding="utf-8") as f:
f.write(content)
return outpath
def main():
s = signin()
refresh(s["refreshToken"])
next_month = get_next_possible_month(COMPETITION_UID)
cal = get_calendar_for(COMPETITION_UID, next_month["year"], str(next_month["month"]).zfill(2))
fixture_details = {}
for _, entries in cal.items():
for entry in entries:
fid = entry.get("fixture_guid")
if fid:
fixture_details[fid] = get_fixture_single(fid)
channels_doc = build_league_channels(next_month, fixture_details)
filtered = filter_channels_for_export(channels_doc)
outpath = export_m3u(filtered, OUTPATH)
print(f"[OK] Wrote {len(filtered)} entries to {outpath}")
if __name__ == "__main__":
main()