Files
simvision/validate_playlist_urls.py
Michael Simard 872354b834 Initial commit: SimVision tvOS streaming app
Features:
- VOD library with movie grouping and version detection
- TV show library with season/episode organization
- TMDB integration for trending shows and recently aired episodes
- Recent releases section with TMDB release date sorting
- Watch history tracking with continue watching
- Playlist caching (12-hour TTL) for offline support
- M3U playlist parsing with XStream API support
- Authentication with credential storage

Technical:
- SwiftUI for tvOS
- Actor-based services for thread safety
- Persistent caching for playlists, TMDB data, and watch history
- KSPlayer integration for video playback

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 22:12:08 -06:00

180 lines
5.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
M3U Playlist URL Validator
Tests video URLs with retry logic and timeout handling similar to VLC player
"""
import sys
import urllib.request
import urllib.error
import socket
from typing import List, Tuple
def test_url(url: str, timeout: int = 10, max_retries: int = 3) -> bool:
"""
Test if a video URL is accessible by attempting to read the first 1KB.
Uses retry logic and follows redirects similar to VLC player.
Args:
url: The video URL to test
timeout: Connection timeout in seconds
max_retries: Maximum number of retry attempts
Returns:
True if URL is accessible, False otherwise
"""
for attempt in range(max_retries):
try:
# Create request with headers similar to VLC
req = urllib.request.Request(url)
req.add_header('User-Agent', 'VLCPlayer/3.0')
req.add_header('Range', 'bytes=0-1023') # Request first 1KB only
# Attempt to open URL with timeout
with urllib.request.urlopen(req, timeout=timeout) as response:
# Try to read first 1KB
data = response.read(1024)
# If we got data, URL is valid
if len(data) > 0:
return True
except urllib.error.HTTPError as e:
# HTTP 520 or other server errors - retry
if attempt < max_retries - 1:
continue
return False
except urllib.error.URLError as e:
# Network error - retry
if attempt < max_retries - 1:
continue
return False
except socket.timeout:
# Timeout - retry
if attempt < max_retries - 1:
continue
return False
except Exception as e:
# Other errors - retry
if attempt < max_retries - 1:
continue
return False
return False
def parse_m3u(filepath: str) -> List[Tuple[str, str]]:
"""
Parse M3U playlist file and extract entries with their URLs.
Args:
filepath: Path to M3U file
Returns:
List of tuples containing (entry_info, url)
"""
entries = []
current_info = None
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line.startswith('#EXTINF:'):
current_info = line
elif line and not line.startswith('#'):
# This is a URL line
if current_info:
entries.append((current_info, line))
current_info = None
return entries
def validate_playlist(input_file: str, output_file: str = None, verbose: bool = True):
"""
Validate all URLs in an M3U playlist and optionally create filtered version.
Args:
input_file: Path to input M3U file
output_file: Path to output filtered M3U file (optional)
verbose: Print validation progress
"""
print(f"📋 Reading playlist: {input_file}")
entries = parse_m3u(input_file)
print(f"📊 Found {len(entries)} entries to validate\n")
valid_entries = []
invalid_entries = []
for i, (info, url) in enumerate(entries, 1):
# Extract name from info line
name = "Unknown"
if 'tvg-name="' in info:
start = info.index('tvg-name="') + 10
end = info.index('"', start)
name = info[start:end]
if verbose:
print(f"[{i}/{len(entries)}] Testing: {name}")
print(f" URL: {url}")
# Test URL
is_valid = test_url(url)
if is_valid:
if verbose:
print(f" ✅ VALID\n")
valid_entries.append((info, url))
else:
if verbose:
print(f" ❌ INVALID\n")
invalid_entries.append((info, url))
# Print summary
print("=" * 70)
print(f"📊 Validation Summary:")
print(f" Total entries: {len(entries)}")
print(f" Valid entries: {len(valid_entries)} ({len(valid_entries)/len(entries)*100:.1f}%)")
print(f" Invalid entries: {len(invalid_entries)} ({len(invalid_entries)/len(entries)*100:.1f}%)")
print("=" * 70)
# Write filtered playlist if output file specified
if output_file:
print(f"\n💾 Writing filtered playlist to: {output_file}")
with open(output_file, 'w', encoding='utf-8') as f:
f.write("#EXTM3U\n\n")
for info, url in valid_entries:
f.write(f"{info}\n")
f.write(f"{url}\n\n")
print(f"✅ Filtered playlist created successfully")
# List invalid entries
if invalid_entries and verbose:
print(f"\n❌ Invalid entries:")
for info, url in invalid_entries:
name = "Unknown"
if 'tvg-name="' in info:
start = info.index('tvg-name="') + 10
end = info.index('"', start)
name = info[start:end]
print(f" - {name}")
print(f" {url}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 validate_playlist_urls.py <input.m3u> [output.m3u]")
print("\nExamples:")
print(" python3 validate_playlist_urls.py sample_playlist.m3u")
print(" python3 validate_playlist_urls.py sample_playlist.m3u filtered_playlist.m3u")
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else None
validate_playlist(input_file, output_file, verbose=True)