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>
180 lines
5.5 KiB
Python
Executable File
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)
|