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>
This commit is contained in:
179
validate_playlist_urls.py
Executable file
179
validate_playlist_urls.py
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user