|
|
import gradio as gr |
|
|
import random |
|
|
import string |
|
|
import re |
|
|
import json |
|
|
from difflib import get_close_matches |
|
|
|
|
|
|
|
|
with open("channels_fixed.json", "r", encoding="utf-8") as f: |
|
|
channel_map = json.load(f) |
|
|
|
|
|
|
|
|
def random_filename(uppercase=True): |
|
|
letters = string.ascii_uppercase if uppercase else string.ascii_lowercase |
|
|
return "".join(random.choice(letters) for _ in range(3)) + ".m3u" |
|
|
|
|
|
|
|
|
def insert_tvg_id(extinf_line, tvg_id): |
|
|
line = re.sub(r'tvg-id="[^"]*"', '', extinf_line) |
|
|
line = line.replace('#EXTINF:-1', f'#EXTINF:-1 tvg-id="{tvg_id}"', 1) |
|
|
line = re.sub(r'\s+', ' ', line).strip() |
|
|
return line |
|
|
|
|
|
|
|
|
def apply_event_special_cases(extinf_line, channel_name): |
|
|
line = extinf_line |
|
|
if "MLB LEAGUE PASS" in channel_name: |
|
|
return insert_tvg_id(line, "MLB.Baseball.Dummy.us") |
|
|
if "NHL GAMECENTER" in channel_name: |
|
|
return insert_tvg_id(line, "NHL.Hockey.Dummy.us") |
|
|
if "ATP" in channel_name or "WTA" in channel_name: |
|
|
return insert_tvg_id(line, "Tennis.Channel.us") |
|
|
if 'tvg-id="test"' in line: |
|
|
return insert_tvg_id(line, "Live.Event.us") |
|
|
return line |
|
|
|
|
|
|
|
|
def normalize_name(s: str) -> str: |
|
|
if not s: |
|
|
return "" |
|
|
s = s.upper() |
|
|
|
|
|
s = re.sub(r'[^A-Z0-9]', '', s) |
|
|
return s |
|
|
|
|
|
|
|
|
def process_m3u(m3u_text): |
|
|
lines = m3u_text.splitlines() |
|
|
out_247_blocks = [] |
|
|
out_events_blocks = [] |
|
|
log = [] |
|
|
|
|
|
|
|
|
hardcoded_fixes = { |
|
|
"BTN": "Big.Ten.Network.HD.us2", |
|
|
"SNY": "SNY.SportsNet.New.York.HD.us2", |
|
|
"MASN": "MASN.-.Mid.Atlantic.Sports.Network.us2", |
|
|
"YES": "Yes.Network.us2", |
|
|
"MSG": "MSG.National.us2", |
|
|
"REELZ USA": "ReelzChannel.HD.us2", |
|
|
"ABCNY USA": "ABC.(WABC).New.York,.NY.us", |
|
|
"ABC NY USA": "ABC.(WABC).New.York,.NY.us", |
|
|
"OWN USA": "Oprah.Winfrey.Network.HD.us2", |
|
|
"DISCOVERY USA": "Discovery.Channel.ca2", |
|
|
"OUTDOOR CHANNEL": "Outdoor.Channel.HD.us2", |
|
|
"OUTDOOR CHANNEL USA": "Outdoor.Channel.HD.us2", |
|
|
"LOITV": "Soccer.Dummy.us", |
|
|
"NBCNY USA" : "WNBC-DT.us_locals1", |
|
|
"NBC USA" : "WNBC-DT.us_locals1", |
|
|
} |
|
|
|
|
|
|
|
|
normalized_hardcoded = {normalize_name(k): v for k, v in hardcoded_fixes.items()} |
|
|
|
|
|
for i, line in enumerate(lines): |
|
|
if line.startswith("#EXTINF"): |
|
|
url = lines[i+1] if i+1 < len(lines) else "" |
|
|
extinf = line |
|
|
|
|
|
|
|
|
tvg_match = re.search(r'tvg-id="([^"]*)"', extinf) |
|
|
tvg_id = tvg_match.group(1) if tvg_match else None |
|
|
name_match = re.search(r",(.*)", extinf) |
|
|
channel_name = name_match.group(1).strip() if name_match else "" |
|
|
|
|
|
|
|
|
group_match = re.search(r'group-title="([^"]*)"', extinf) |
|
|
group_title = group_match.group(1).upper().strip() if group_match else "" |
|
|
|
|
|
|
|
|
if group_title and "24/7" in group_title: |
|
|
if tvg_id == "test" or not tvg_id: |
|
|
new_ext = insert_tvg_id(extinf, "Info.Guide.Dummy.us") |
|
|
log.append(f"βΉοΈ 24/7 | {channel_name} β Info.Guide.Dummy.us") |
|
|
else: |
|
|
new_ext = extinf |
|
|
out_247_blocks.append((new_ext, url)) |
|
|
|
|
|
|
|
|
elif group_title and "EVENTS" in group_title: |
|
|
|
|
|
bracket_match = re.search(r'\[([^\]]+)\]', channel_name) |
|
|
if bracket_match: |
|
|
candidate_raw = bracket_match.group(1).strip() |
|
|
else: |
|
|
|
|
|
candidate_raw = channel_name |
|
|
|
|
|
candidate_norm = normalize_name(candidate_raw) |
|
|
|
|
|
|
|
|
matched_fix = False |
|
|
if candidate_norm: |
|
|
|
|
|
if candidate_norm in normalized_hardcoded: |
|
|
fixed_id = normalized_hardcoded[candidate_norm] |
|
|
new_ext = insert_tvg_id(extinf, fixed_id) |
|
|
new_ext = apply_event_special_cases(new_ext, candidate_raw) |
|
|
out_events_blocks.append((new_ext, url)) |
|
|
log.append(f"β
EVENTS | {candidate_raw} β {fixed_id} (hardcoded exact)") |
|
|
matched_fix = True |
|
|
else: |
|
|
|
|
|
for key_norm, fixed_id in normalized_hardcoded.items(): |
|
|
|
|
|
if candidate_norm.startswith(key_norm) or key_norm in candidate_norm: |
|
|
new_ext = insert_tvg_id(extinf, fixed_id) |
|
|
new_ext = apply_event_special_cases(new_ext, candidate_raw) |
|
|
out_events_blocks.append((new_ext, url)) |
|
|
log.append(f"β
EVENTS | {candidate_raw} β {fixed_id} (hardcoded partial)") |
|
|
matched_fix = True |
|
|
break |
|
|
|
|
|
if matched_fix: |
|
|
continue |
|
|
|
|
|
|
|
|
match = None |
|
|
for key, data in channel_map.items(): |
|
|
if data.get("tvg_id") and key.lower() in candidate_raw.lower(): |
|
|
match = data["tvg_id"] |
|
|
break |
|
|
if not match: |
|
|
keys = list(channel_map.keys()) |
|
|
guesses = get_close_matches(candidate_raw, keys, n=1, cutoff=0.6) |
|
|
if guesses: |
|
|
match = channel_map[guesses[0]].get("tvg_id") |
|
|
|
|
|
if match: |
|
|
new_ext = insert_tvg_id(extinf, match) |
|
|
log.append(f"β
EVENTS | {candidate_raw} β {match}") |
|
|
else: |
|
|
new_ext = insert_tvg_id(extinf, "Live.Event.us") |
|
|
log.append(f"β οΈ EVENTS | {candidate_raw} β Live.Event.us (fallback)") |
|
|
|
|
|
new_ext = apply_event_special_cases(new_ext, candidate_raw) |
|
|
out_events_blocks.append((new_ext, url)) |
|
|
|
|
|
|
|
|
out_247 = "\n".join([f"{ext}\n{url}" for ext, url in out_247_blocks]) |
|
|
out_events = "\n".join([f"{ext}\n{url}" for ext, url in out_events_blocks]) |
|
|
|
|
|
return out_247, out_events, "\n".join(log) |
|
|
|
|
|
|
|
|
def run_app(m3u_text): |
|
|
out_247, out_events, log = process_m3u(m3u_text) |
|
|
file_247 = random_filename(uppercase=True) |
|
|
file_events = random_filename(uppercase=False) |
|
|
with open(file_247, "w", encoding="utf-8") as f: |
|
|
f.write(out_247) |
|
|
with open(file_events, "w", encoding="utf-8") as f: |
|
|
f.write(out_events) |
|
|
return file_247, file_events, log |
|
|
|
|
|
iface = gr.Interface( |
|
|
fn=run_app, |
|
|
inputs=gr.Textbox(lines=5, placeholder="Paste your M3U playlist here..."), |
|
|
outputs=[ |
|
|
gr.File(label="24/7 Playlist"), |
|
|
gr.File(label="Events Playlist"), |
|
|
gr.Textbox(label="Log", lines=30), |
|
|
], |
|
|
title="Project 1 Playlist Processor", |
|
|
description="Splits 24/7 and Events playlists, applies JSON tvg-id mappings, adds hardcoded + special rules, and outputs two clean M3Us." |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
iface.launch() |
|
|
|