shvchenko commited on
Commit
cde23f3
Β·
verified Β·
1 Parent(s): d8577e2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -107
app.py CHANGED
@@ -9,146 +9,238 @@ CHANNELS_JSON = "channels_fixed.json"
9
 
10
  # Abbreviations mapping for Events channels
11
  ABBREVIATIONS = {
12
- "BTN": "BIG TEN Network (BTN USA)"
13
- # Add more abbreviations here if needed
14
  }
15
 
16
- # Special cases mapping
17
  SPECIAL_CASES = {
18
  "MLB LEAGUE PASS": "MLB.Baseball.Dummy.us",
19
- "NHL GAMECENTER": "NHL.Hockey.Dummy.us"
20
  }
21
 
22
- def normalize(name):
23
- name = name.lower()
24
- name = re.sub(r'[^a-z0-9]', '', name)
25
- name = re.sub(r'0+', '', name)
26
- return name
27
-
28
- def fuzzy_match(name, json_keys, cutoff=0.8):
29
- matches = get_close_matches(normalize(name), [normalize(k) for k in json_keys], n=1, cutoff=cutoff)
 
 
 
 
 
 
 
 
 
30
  if matches:
31
- for k in json_keys:
32
- if normalize(k) == matches[0]:
33
- return k
34
  return None
35
 
36
  def generate_filename(upper=False):
37
- chars = ''.join(random.choices(string.ascii_uppercase if upper else string.ascii_lowercase, k=3))
38
- return f"{chars}.m3u"
39
-
40
- def process_playlist(m3u_content):
41
- m3u_lines = m3u_content.splitlines()
42
 
 
 
43
  try:
44
- with open(CHANNELS_JSON, 'r', encoding='utf-8') as f:
45
  channels_ref = json.load(f)
46
  except Exception as e:
47
- return f"Error reading JSON: {e}", None, None
48
-
49
- m3u_24_7 = []
50
- m3u_events = []
51
- current_section = None
52
-
53
- for line in m3u_lines:
54
- if line.startswith('#EXTINF'):
55
- if 'group-title="24/7 CHANNELS' in line:
56
- current_section = '24_7'
57
- elif 'EVENTS|' in line:
58
- current_section = 'events'
 
 
 
 
 
59
  else:
60
- current_section = None
61
-
62
- if current_section == '24_7':
63
- m3u_24_7.append(line)
64
- elif current_section == 'events':
65
- line = re.sub(r'group-title="24/7 CHANNELS[^"]*"', '', line)
66
- m3u_events.append(line)
 
 
 
67
  else:
68
- m3u_24_7.append(line)
69
- m3u_events.append(line)
70
-
71
- log_lines = []
72
-
73
- def update_tvg_id(lines, is_event=False):
74
- updated = []
75
- for line in lines:
76
- if line.startswith('#EXTINF'):
77
- parts = line.split(',')
78
- ch_name = parts[-1].strip()
79
-
80
- new_tvg_id = None
81
-
82
- if is_event:
83
- match_brackets = re.search(r'\[(.*?)\]', ch_name)
84
- ch_name_to_match = match_brackets.group(1) if match_brackets else ch_name
85
-
86
- if ch_name_to_match in SPECIAL_CASES:
87
- new_tvg_id = SPECIAL_CASES[ch_name_to_match]
88
- elif ch_name_to_match in ABBREVIATIONS:
89
- match_key = ABBREVIATIONS[ch_name_to_match]
90
- new_tvg_id = channels_ref.get(match_key, {}).get('tvg_id', 'Live.Event.us')
91
- else:
92
- if re.search(r'ATP|WTA', ch_name_to_match, re.IGNORECASE):
93
- new_tvg_id = 'Tennis.Channel.us'
94
- else:
95
- ch_name_norm = normalize(ch_name_to_match)
96
- json_keys_norm = {k: normalize(k) for k in channels_ref.keys()}
97
- match_key = None
98
- matches = get_close_matches(ch_name_norm, list(json_keys_norm.values()), n=1, cutoff=0.7)
99
- if matches:
100
- for k, v in json_keys_norm.items():
101
- if v == matches[0]:
102
- match_key = k
103
- break
104
- new_tvg_id = channels_ref[match_key]['tvg_id'] if match_key else 'Live.Event.us'
105
-
106
- # Ensure tvg-id="test" is replaced with tvg-id="Live.Event.us"
107
- line = re.sub(r'tvg-id="test"', 'tvg-id="Live.Event.us"', line)
 
108
  else:
109
- match_key = fuzzy_match(ch_name, channels_ref.keys())
110
- new_tvg_id = channels_ref[match_key]['tvg_id'] if match_key else 'Info.Guide.Dummy.us'
111
- line = re.sub(r'tvg-id="test"', 'tvg-id="Info.Guide.Dummy.us"', line)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- if is_event:
114
- line = re.sub(r'group-title="[^"]*"', '', line)
 
115
 
116
- if 'tvg-id=' in line:
117
- line = re.sub(r'tvg-id=".*?"', f'tvg-id="{new_tvg_id}"', line)
 
 
118
  else:
119
- line = line.replace('#EXTINF:', f'#EXTINF:-1 tvg-id="{new_tvg_id}",')
 
120
 
121
- log_lines.append(f"{'βœ…' if (match_key or not is_event) else '⚠️'} {ch_name} β†’ {new_tvg_id}")
 
 
122
 
123
- updated.append(line)
124
- return updated
 
 
 
 
125
 
126
- updated_24_7 = update_tvg_id(m3u_24_7, is_event=False)
127
- updated_events = update_tvg_id(m3u_events, is_event=True)
128
 
129
- filename_24_7 = generate_filename(upper=True)
130
- filename_events = generate_filename(upper=False)
 
 
 
 
 
 
131
 
132
- with open(filename_24_7, 'w', encoding='utf-8') as f:
133
- f.write('\n'.join(updated_24_7))
 
134
 
135
- with open(filename_events, 'w', encoding='utf-8') as f:
136
- f.write('\n'.join(updated_events))
 
 
 
 
 
137
 
138
- log_text = '\n'.join(log_lines)
139
- return log_text, filename_24_7, filename_events
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
  # --- Gradio UI ---
 
 
 
 
142
  demo = gr.Interface(
143
- fn=process_playlist,
144
- inputs=[gr.Textbox(label="Paste M3U8 Playlist Here", lines=25)],
145
  outputs=[
146
  gr.Textbox(label="Update Log", lines=25),
147
  gr.File(label="Download 24/7 Channels M3U"),
148
- gr.File(label="Download Events M3U")
149
  ],
150
- title="Project 1 Playlist Updater - Smart Events",
151
- description="Paste the M3U8 playlist content. Updates 24/7 and Events channels tvg-id using channels_fixed.json with smart Events matching. Handles special cases like MLB, NHL, ATP/WTA. Replaces tvg-id=\"test\" properly."
152
  )
153
 
154
- demo.launch()
 
 
9
 
10
  # Abbreviations mapping for Events channels
11
  ABBREVIATIONS = {
12
+ "BTN": "BIG TEN Network (BTN USA)",
13
+ # add other known abbreviations here
14
  }
15
 
16
+ # Special-case tvg-ids
17
  SPECIAL_CASES = {
18
  "MLB LEAGUE PASS": "MLB.Baseball.Dummy.us",
19
+ "NHL GAMECENTER": "NHL.Hockey.Dummy.us",
20
  }
21
 
22
+ def normalize(s: str) -> str:
23
+ s = (s or "").lower()
24
+ s = re.sub(r'[^a-z0-9]', '', s)
25
+ s = re.sub(r'^0+', '', s) # remove leading zeros
26
+ return s
27
+
28
+ def find_best_match(candidate: str, keys):
29
+ """Normalize + fuzzy match candidate against keys; returns matching key or None."""
30
+ if not candidate:
31
+ return None
32
+ candidate_norm = normalize(candidate)
33
+ norm_map = {normalize(k): k for k in keys}
34
+ # direct normalized match
35
+ if candidate_norm in norm_map:
36
+ return norm_map[candidate_norm]
37
+ # fuzzy on normalized keys
38
+ matches = get_close_matches(candidate_norm, list(norm_map.keys()), n=1, cutoff=0.75)
39
  if matches:
40
+ return norm_map[matches[0]]
 
 
41
  return None
42
 
43
  def generate_filename(upper=False):
44
+ letters = ''.join(random.choices(string.ascii_uppercase if upper else string.ascii_lowercase, k=3))
45
+ return f"{letters}.m3u"
 
 
 
46
 
47
+ def process_playlist_text(m3u_text):
48
+ # load JSON reference
49
  try:
50
+ with open(CHANNELS_JSON, "r", encoding="utf-8") as f:
51
  channels_ref = json.load(f)
52
  except Exception as e:
53
+ return f"Error loading {CHANNELS_JSON}: {e}", None, None
54
+
55
+ lines = m3u_text.splitlines()
56
+ headers = []
57
+ blocks = [] # list of dicts: {ext, url, section, orig_index}
58
+
59
+ i = 0
60
+ idx = 0
61
+ while i < len(lines):
62
+ line = lines[i].rstrip("\n")
63
+ if line.startswith("#EXTINF"):
64
+ ext = line
65
+ url = ""
66
+ # take the next line as the URL if present
67
+ if i + 1 < len(lines):
68
+ url = lines[i+1].rstrip("\n")
69
+ i += 2
70
  else:
71
+ i += 1
72
+ # determine section
73
+ sec = "other"
74
+ # robust check for 24/7 and EVENTS
75
+ if 'group-title="' in ext and '24/7' in ext:
76
+ sec = "24_7"
77
+ elif 'EVENTS|' in ext:
78
+ sec = "events"
79
+ blocks.append({"ext": ext, "url": url, "section": sec, "idx": idx})
80
+ idx += 1
81
  else:
82
+ # header / comment lines (keep for both outputs)
83
+ headers.append(line)
84
+ i += 1
85
+
86
+ # now process each block to decide tvg-id updates; we will NOT remove group-title here
87
+ out_24_7_blocks = []
88
+ out_events_blocks = []
89
+ log = []
90
+
91
+ json_keys = list(channels_ref.keys())
92
+
93
+ for b in blocks:
94
+ ext = b["ext"]
95
+ url = b["url"]
96
+ sec = b["section"]
97
+
98
+ # default: no change
99
+ new_ext = ext
100
+
101
+ if sec == "events":
102
+ # extract bracketed channel name if present, otherwise fallback to display name (after last comma)
103
+ m = re.search(r'\[([^\]]+)\]', ext)
104
+ candidate = m.group(1).strip() if m else ext.split(",")[-1].strip()
105
+
106
+ # check abbreviations first (exact match)
107
+ match_key = None
108
+ if candidate in ABBREVIATIONS:
109
+ mapkey = ABBREVIATIONS[candidate]
110
+ if mapkey in channels_ref:
111
+ match_key = mapkey
112
+
113
+ # special cases by substring/inclusion in candidate or ext line
114
+ if not match_key:
115
+ up = candidate.upper()
116
+ ext_up = ext.upper()
117
+ if "MLB LEAGUE PASS" in up or "MLB LEAGUE PASS" in ext_up:
118
+ chosen_tvg = SPECIAL_CASES["MLB LEAGUE PASS"]
119
+ match_key = None
120
+ elif "NHL GAMECENTER" in up or "NHL GAMECENTER" in ext_up:
121
+ chosen_tvg = SPECIAL_CASES["NHL GAMECENTER"]
122
+ match_key = None
123
  else:
124
+ chosen_tvg = None
125
+
126
+ # ATP/WTA fallback
127
+ if not match_key and not ('chosen_tvg' in locals() and chosen_tvg):
128
+ if re.search(r'\b(ATP|WTA)\b', candidate, flags=re.IGNORECASE):
129
+ chosen_tvg = "Tennis.Channel.us"
130
+
131
+ # if no chosen_tvg yet, attempt fuzzy lookup
132
+ if not ('chosen_tvg' in locals() and chosen_tvg):
133
+ if match_key is None:
134
+ best = find_best_match(candidate, json_keys)
135
+ if best:
136
+ match_key = best
137
+
138
+ # derive tvg-id
139
+ if 'chosen_tvg' in locals() and chosen_tvg:
140
+ tvg_id = chosen_tvg
141
+ elif match_key:
142
+ tvg_id = channels_ref.get(match_key, {}).get("tvg_id", None)
143
+ else:
144
+ tvg_id = "Live.Event.us"
145
 
146
+ # if ext had tvg-id="test", override with Live.Event.us (explicit)
147
+ if 'tvg-id="test"' in ext:
148
+ tvg_id = "Live.Event.us"
149
 
150
+ # insert or replace tvg-id attribute (do NOT strip group-title here)
151
+ if tvg_id:
152
+ if 'tvg-id="' in new_ext:
153
+ new_ext = re.sub(r'tvg-id="[^"]*"', f'tvg-id="{tvg_id}"', new_ext, count=1)
154
  else:
155
+ # insert after the #EXTINF:-1 token (keep other attributes)
156
+ new_ext = re.sub(r'(#EXTINF:-?\d+)(\s*)', r'\1 tvg-id="' + tvg_id + r'"\2', new_ext, count=1)
157
 
158
+ # clean up chosen_tvg variable for next loop
159
+ if 'chosen_tvg' in locals():
160
+ del chosen_tvg
161
 
162
+ # logging
163
+ if match_key:
164
+ log.append(f"βœ… EVENTS | {candidate} β†’ {tvg_id} (matched {match_key})")
165
+ else:
166
+ # if we used an abbreviations mapped key, we already set match_key; otherwise unmatched
167
+ log.append(f"⚠️ EVENTS | {candidate} β†’ {tvg_id} (no exact json match)")
168
 
169
+ out_events_blocks.append((new_ext, url))
 
170
 
171
+ elif sec == "24_7":
172
+ # use display name after last comma
173
+ display_name = ext.split(",")[-1].strip()
174
+ best = find_best_match(display_name, json_keys)
175
+ if best:
176
+ tvg_id = channels_ref.get(best, {}).get("tvg_id", None)
177
+ else:
178
+ tvg_id = "Info.Guide.Dummy.us"
179
 
180
+ # replace test if present
181
+ if 'tvg-id="test"' in ext:
182
+ tvg_id = "Info.Guide.Dummy.us"
183
 
184
+ # insert or replace
185
+ if 'tvg-id="' in new_ext:
186
+ new_ext = re.sub(r'tvg-id="[^"]*"', f'tvg-id="{tvg_id}"', new_ext, count=1)
187
+ else:
188
+ new_ext = re.sub(r'(#EXTINF:-?\d+)(\s*)', r'\1 tvg-id="' + tvg_id + r'"\2', new_ext, count=1)
189
+
190
+ log.append(f"βœ… 24/7 | {display_name} β†’ {tvg_id}" if best else f"⚠️ 24/7 | {display_name} β†’ {tvg_id} (no json match)")
191
 
192
+ out_24_7_blocks.append((new_ext, url))
193
+
194
+ else:
195
+ # other: keep as-is (but still write as block if has URL)
196
+ out_24_7_blocks.append((ext, url))
197
+ out_events_blocks.append((ext, url))
198
+
199
+ # Build output text for both playlists (headers first, then blocks)
200
+ def build_text(header_lines, block_list):
201
+ out = []
202
+ # write header lines, but skip the url-tvg epg line if present (optional)
203
+ for h in header_lines:
204
+ # optional: skip url-tvg line that pointed to xml epg if present
205
+ if h.strip().startswith('#EXTM3U url-tvg='):
206
+ continue
207
+ out.append(h)
208
+ # then blocks
209
+ for ext, url in block_list:
210
+ out.append(ext)
211
+ # write URL on next line (even if empty)
212
+ if url is not None:
213
+ out.append(url)
214
+ return "\n".join(out) + "\n"
215
+
216
+ text_24_7 = build_text(headers, out_24_7_blocks)
217
+ text_events = build_text(headers, out_events_blocks)
218
+
219
+ # write to files with random names
220
+ f24 = generate_filename(upper=True)
221
+ fev = generate_filename(upper=False)
222
+ with open(f24, "w", encoding="utf-8") as f:
223
+ f.write(text_24_7)
224
+ with open(fev, "w", encoding="utf-8") as f:
225
+ f.write(text_events)
226
+
227
+ return ("\n".join(log), f24, fev)
228
 
229
  # --- Gradio UI ---
230
+ def run_gradio(m3u_text):
231
+ status, f1, f2 = process_playlist_text(m3u_text)
232
+ return status, f1, f2
233
+
234
  demo = gr.Interface(
235
+ fn=process_playlist_text,
236
+ inputs=[gr.Textbox(label="Paste M3U8 Playlist Here", lines=30)],
237
  outputs=[
238
  gr.Textbox(label="Update Log", lines=25),
239
  gr.File(label="Download 24/7 Channels M3U"),
240
+ gr.File(label="Download Events M3U"),
241
  ],
242
+ title="Project 1 β€” M3U Updater (stable output format)"
 
243
  )
244
 
245
+ if __name__ == "__main__":
246
+ demo.launch()