-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapply_fixes.py
More file actions
396 lines (355 loc) · 16.9 KB
/
Copy pathapply_fixes.py
File metadata and controls
396 lines (355 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
#!/usr/bin/env python3
"""
EXStreamTV Fix Applicator
Applies the Plex tuning + EPG title fixes directly to source files.
Run from the EXStreamTV root directory:
python3 apply_fixes.py
"""
import re
import sys
from pathlib import Path
def fix_plex_resolver(path: Path) -> bool:
"""Fix: prefer config/cache tokens over expired URL-embedded tokens."""
text = path.read_text()
# Check if already fixed
if "url_token" in text and "Last resort" in text:
print(f" [skip] {path.name} — already patched")
return False
# Replace the _extract_plex_info method's URL token extraction + fallback chain.
# Match from "Try to extract from URL" through "return info" at the end of the method.
old = re.compile(
r"( # Try to extract from URL\n"
r" url = self\._get_url\(media_item\)\n"
r" if url:\n"
r" import re\n"
r".*?" # everything in between
r")( return info)",
re.DOTALL,
)
replacement = (
" # Extract rating_key and server_url from the stored URL.\n"
" # The token extracted here is from import time and likely EXPIRED,\n"
" # so we store it separately and only use it as a last resort.\n"
" url = self._get_url(media_item)\n"
" url_token = None\n"
" if url:\n"
" import re\n"
" match = re.search(r\"/library/metadata/(\\d+)\", url)\n"
" if match:\n"
" info[\"rating_key\"] = match.group(1)\n"
" \n"
" match = re.search(r\"(https?://[^/]+)\", url)\n"
" if match and not info.get(\"server_url\"):\n"
" info[\"server_url\"] = match.group(1)\n"
" \n"
" # Save URL-embedded token separately — it's stale from import time\n"
" match = re.search(r\"X-Plex-Token=([^&]+)\", url)\n"
" if match:\n"
" url_token = match.group(1)\n"
" \n"
" # Now look for FRESH tokens from authoritative sources (library cache,\n"
" # global config). These are maintained by the user and take priority\n"
" # over expired tokens embedded in imported URLs.\n"
" \n"
" # Source 1: Library cache (per-library server_url + token)\n"
" _load_plex_library_cache()\n"
" \n"
" library_id = getattr(media_item, \"library_id\", None)\n"
" if library_id and library_id in _plex_library_cache:\n"
" lib_info = _plex_library_cache[library_id]\n"
" if not info.get(\"server_url\"):\n"
" info[\"server_url\"] = lib_info[\"server_url\"]\n"
" if not info.get(\"token\"):\n"
" info[\"token\"] = lib_info[\"token\"]\n"
" logger.debug(f\"Using Plex credentials from cached library {library_id}: {lib_info['name']}\")\n"
" \n"
" # Source 2: First available Plex library from cache\n"
" if not info.get(\"token\"):\n"
" if _plex_first_library_cache:\n"
" lib_info = _plex_first_library_cache\n"
" if not info.get(\"server_url\"):\n"
" info[\"server_url\"] = lib_info[\"server_url\"]\n"
" info[\"token\"] = lib_info[\"token\"]\n"
" logger.debug(f\"Using Plex credentials from first cached library: {lib_info['name']}\")\n"
" \n"
" # Source 3: Global config\n"
" if not info.get(\"token\"):\n"
" try:\n"
" from exstreamtv.config import get_config\n"
" config = get_config()\n"
" \n"
" plex_config = getattr(config, 'plex', None)\n"
" if plex_config:\n"
" if not info.get(\"server_url\"):\n"
" info[\"server_url\"] = getattr(plex_config, 'url', '') or getattr(plex_config, 'base_url', '')\n"
" if not info.get(\"token\"):\n"
" info[\"token\"] = getattr(plex_config, 'token', '')\n"
" \n"
" libraries_plex = getattr(getattr(config, 'libraries', None), 'plex', None)\n"
" if libraries_plex:\n"
" if not info.get(\"server_url\"):\n"
" info[\"server_url\"] = getattr(libraries_plex, 'url', '')\n"
" if not info.get(\"token\"):\n"
" info[\"token\"] = getattr(libraries_plex, 'token', '')\n"
" \n"
" if info.get(\"token\"):\n"
" logger.debug(\"Using Plex credentials from global config\")\n"
" except Exception as e:\n"
" logger.warning(f\"Failed to load Plex config: {e}\")\n"
" \n"
" # Last resort: use the token embedded in the stored URL (likely expired)\n"
" if not info.get(\"token\") and url_token:\n"
" info[\"token\"] = url_token\n"
" logger.debug(\"Using Plex token from stored URL (may be expired)\")\n"
" \n"
" return info"
)
new_text, count = old.subn(replacement.replace("\\", "\\\\"), text)
if count == 0:
print(f" [WARN] {path.name} — pattern not found, applying manual fix")
# Simpler approach: just replace the token extraction line
if 'info["token"] = match.group(1)' in text and "url_token" not in text:
# Replace the direct token assignment with url_token capture
text = text.replace(
' # Extract token\n'
' match = re.search(r"X-Plex-Token=([^&]+)", url)\n'
' if match:\n'
' info["token"] = match.group(1)',
' # Save URL-embedded token separately — stale from import time\n'
' match = re.search(r"X-Plex-Token=([^&]+)", url)\n'
' if match:\n'
' url_token = match.group(1)'
)
# Add url_token initialization before the URL block
text = text.replace(
' # Try to extract from URL\n'
' url = self._get_url(media_item)',
' # Try to extract from URL\n'
' url_token = None\n'
' url = self._get_url(media_item)'
)
# Change fallback conditions to always check for fresh token
text = text.replace(
' # FALLBACK 1: Try library_id',
' # PRIORITY 1: Try library_id (fresh token)'
)
text = text.replace(
' # FALLBACK 2: Try first available',
' # PRIORITY 2: Try first available'
)
text = text.replace(
' # FALLBACK 3: If still missing',
' # PRIORITY 3: Global config'
)
# Remove the condition that skips fallbacks when URL token exists
for old_cond in [
'if not info.get("server_url") or not info.get("token"):',
]:
# Only change the ones guarding the token fallback, keep server_url checks
pass
# Add last-resort url_token usage before return
text = text.replace(
' return info',
' # Last resort: use URL-embedded token (likely expired)\n'
' if not info.get("token") and url_token:\n'
' info["token"] = url_token\n'
' logger.debug("Using Plex token from stored URL (may be expired)")\n'
' \n'
' return info',
1 # Only first occurrence
)
path.write_text(text)
print(f" [OK] {path.name} — applied manual token priority fix")
return True
else:
print(f" [FAIL] {path.name} — could not apply fix automatically")
return False
path.write_text(new_text)
print(f" [OK] {path.name} — token priority fixed")
return True
def fix_epg_titles(path: Path) -> bool:
"""Fix: filter 'Item NNNNNN' placeholder titles from all sources."""
text = path.read_text()
if "^Item \\d+$" in text:
print(f" [skip] {path.name} — already has Item filter")
return False
# Ensure 'import re' is at the top
if "\nimport re\n" not in text:
text = text.replace(
"import logging\n",
"import logging\nimport re\n",
1,
)
changes = 0
# Fix the XMLTV EPG path: custom_title priority with placeholder filter
old_xmltv = (
' title = schedule_item.get("custom_title")\n'
' if not title:\n'
' # Get media item title, handling None safely\n'
' title = media_item.title if (media_item and media_item.title) else None'
)
new_xmltv = (
' custom_title = schedule_item.get("custom_title")\n'
' title = custom_title\n'
' if not title:\n'
' title = media_item.title if (media_item and media_item.title) else None\n'
'\n'
' # Skip placeholder titles from importers (e.g. "Item 332245")\n'
' if title and re.match(r"^Item \\d+$", title):\n'
' real_title = None\n'
' if media_item:\n'
' real_title = (\n'
' getattr(media_item, "original_title", None)\n'
' or getattr(media_item, "sort_title", None)\n'
' or getattr(media_item, "episode_title", None)\n'
' )\n'
' if not real_title:\n'
' series = getattr(media_item, "series_title", None) or getattr(media_item, "show_title", None)\n'
' if series:\n'
' ep = getattr(media_item, "episode_title", None)\n'
' real_title = f"{series} - {ep}" if ep else series\n'
' title = real_title if real_title else None'
)
if old_xmltv in text:
text = text.replace(old_xmltv, new_xmltv, 1)
changes += 1
# Also fix M3U path if present
old_m3u = 'title = schedule_item.get("custom_title") or media_item.title'
if old_m3u in text:
new_m3u = (
'custom_title = schedule_item.get("custom_title")\n'
' title = custom_title or media_item.title\n'
' # Skip placeholder titles from importers\n'
' if title and re.match(r"^Item \\d+$", title):\n'
' title = (\n'
' getattr(media_item, "original_title", None)\n'
' or getattr(media_item, "sort_title", None)\n'
' or getattr(media_item, "episode_title", None)\n'
' or media_item.title\n'
' )'
)
text = text.replace(old_m3u, new_m3u, 1)
changes += 1
if changes:
path.write_text(text)
print(f" [OK] {path.name} — {changes} EPG title filter(s) applied")
return True
else:
print(f" [WARN] {path.name} — title patterns not found (file may differ)")
return False
def fix_channel_manager(path: Path) -> bool:
"""Fix: advance item index on failure + keep-alive for empty playout."""
text = path.read_text()
changes = 0
# Fix 1: advance item index on URL resolution failure
old_error = (
' except Exception as e:\n'
' logger.error(f"Error getting next playout item: {e}")\n'
' return None'
)
new_error = (
' except Exception as e:\n'
' logger.error(\n'
' f"Error getting next playout item at index {self._current_item_index} "\n'
' f"for channel {self.channel_number}: {e}"\n'
' )\n'
' self._current_item_index += 1\n'
' return None'
)
if "self._current_item_index += 1" not in text.split("Error getting next playout item")[0] + text.split("Error getting next playout item")[-1]:
if old_error in text:
text = text.replace(old_error, new_error, 1)
changes += 1
# Fix 2: send keep-alive null packets when no content
old_wait = (
' # No content available - show offline slate or wait\n'
' logger.debug(\n'
' f"No content for channel {self.channel_number}, waiting..."\n'
' )\n'
' await asyncio.sleep(5.0)\n'
' continue'
)
new_wait = (
' logger.warning(\n'
' f"Channel {self.channel_number}: no playable content. "\n'
' f"Check media source connectivity (Plex token, server URL, etc.)"\n'
' )\n'
' null_packet = bytes([0x47, 0x1F, 0xFF, 0x10] + [0xFF] * 184)\n'
' for _ in range(7):\n'
' await self._broadcast_chunk(null_packet)\n'
' await asyncio.sleep(5.0)\n'
' continue'
)
if old_wait in text:
text = text.replace(old_wait, new_wait, 1)
changes += 1
if changes:
path.write_text(text)
print(f" [OK] {path.name} — {changes} fix(es) applied")
return True
else:
print(f" [skip] {path.name} — fixes already applied or patterns differ")
return False
def cleanup_debug_logs(root: Path) -> int:
"""Remove #region agent log blocks from all source files."""
count = 0
for pyfile in root.rglob("*.py"):
if ".build" in str(pyfile) or "venv" in str(pyfile):
continue
text = pyfile.read_text()
if "# #region agent log" not in text:
continue
# Remove blocks: # #region agent log ... # #endregion (with surrounding blank lines)
new_text = re.sub(
r'\n?\s*# #region agent log\n.*?# #endregion\n?',
'\n',
text,
flags=re.DOTALL,
)
# Also remove standalone blocks without #endregion (try/except pattern)
new_text = re.sub(
r'\n\s*# #region agent log\n\s*try:\n\s*import json[^\n]*\n.*?except:\s*pass\n',
'\n',
new_text,
flags=re.DOTALL,
)
if new_text != text:
pyfile.write_text(new_text)
count += 1
print(f" [OK] {pyfile.relative_to(root)} — debug logs removed")
return count
def main():
root = Path(".")
# Verify we're in the right directory
if not (root / "exstreamtv").is_dir():
print("ERROR: Run this from the EXStreamTV root directory")
sys.exit(1)
print("EXStreamTV Fix Applicator")
print("=" * 50)
print("\n1. Fixing Plex resolver (expired token priority)...")
plex_path = root / "exstreamtv" / "streaming" / "resolvers" / "plex.py"
if plex_path.exists():
fix_plex_resolver(plex_path)
else:
print(f" [WARN] {plex_path} not found")
print("\n2. Fixing EPG titles ('Item NNNNNN' placeholders)...")
iptv_path = root / "exstreamtv" / "api" / "iptv.py"
if iptv_path.exists():
fix_epg_titles(iptv_path)
else:
print(f" [WARN] {iptv_path} not found")
print("\n3. Fixing channel manager (stuck items + keep-alive)...")
cm_path = root / "exstreamtv" / "streaming" / "channel_manager.py"
if cm_path.exists():
fix_channel_manager(cm_path)
else:
print(f" [WARN] {cm_path} not found")
print("\n4. Cleaning up debug log instrumentation...")
cleaned = cleanup_debug_logs(root / "exstreamtv")
if cleaned == 0:
print(" [skip] No debug log blocks found")
print("\n" + "=" * 50)
print("Done. Restart EXStreamTV to apply changes:")
print(" ./stop.sh && ./start.sh")
if __name__ == "__main__":
main()