|
|
@@ -108,23 +108,110 @@ def get_popular_videos(youtube, region_code):
|
|
|
return popular_videos
|
|
|
|
|
|
def download_video(video_id, output_path):
|
|
|
- """Download a YouTube video prioritizing 4K non-AV1 formats, with fallback options."""
|
|
|
+ """Download a YouTube video with quality priority - downloads 4K VP9/AV1 if needed for conversion."""
|
|
|
try:
|
|
|
+ # Quality-priority format selection:
|
|
|
+ # 1. First try: Apple TV compatible codecs at any resolution
|
|
|
+ # 2. Fallback: Download highest quality available (including 4K VP9/AV1 for conversion)
|
|
|
+ # 3. Last resort: Best available format
|
|
|
command = [
|
|
|
"yt-dlp",
|
|
|
- "-f", "bv[height=2160][vcodec!*=av1]+ba / bv[height<=1080]+ba / best",
|
|
|
+ "-f", "(bv[vcodec~='^(avc|h264|hevc|h265)'] / bv[height>=2160] / bv[height>=1080] / best) + (ba[acodec~='^(aac|mp4a)'] / ba)",
|
|
|
"--merge-output-format", "mp4",
|
|
|
"--no-mtime",
|
|
|
f"https://www.youtube.com/watch?v={video_id}",
|
|
|
"-o", os.path.join(output_path, "%(title)s.%(ext)s")
|
|
|
]
|
|
|
subprocess.run(command, check=True)
|
|
|
- print(f"Successfully downloaded video {video_id}")
|
|
|
+ print(f"Successfully downloaded video {video_id} (Quality priority - will convert VP9/AV1 if needed)")
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
print(f"Error downloading video {video_id}: {e}")
|
|
|
return False
|
|
|
return True
|
|
|
|
|
|
+def convert_to_apple_tv_compatible(input_file):
|
|
|
+ """Convert a video to Apple TV 4K compatible format, optimized by resolution."""
|
|
|
+ base_name = os.path.splitext(input_file)[0]
|
|
|
+ mp4_file = base_name + ".mp4"
|
|
|
+
|
|
|
+ print(f"Converting {input_file} to Apple TV compatible format...")
|
|
|
+
|
|
|
+ # Detect video properties to optimize conversion
|
|
|
+ try:
|
|
|
+ import json
|
|
|
+ probe_cmd = [
|
|
|
+ "ffprobe", "-v", "quiet", "-print_format", "json",
|
|
|
+ "-show_format", "-show_streams", input_file
|
|
|
+ ]
|
|
|
+ probe_result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
|
|
|
+ video_info = json.loads(probe_result.stdout)
|
|
|
+
|
|
|
+ # Find video stream
|
|
|
+ video_stream = next((s for s in video_info['streams'] if s['codec_type'] == 'video'), None)
|
|
|
+ is_4k = video_stream and int(video_stream.get('height', 0)) >= 2160
|
|
|
+ is_high_res = video_stream and int(video_stream.get('height', 0)) >= 1080
|
|
|
+
|
|
|
+ print(f"Video resolution: {video_stream.get('height', 'unknown')}p")
|
|
|
+
|
|
|
+ except (subprocess.CalledProcessError, json.JSONDecodeError, Exception) as e:
|
|
|
+ print(f"Could not probe video properties: {e}, using default settings")
|
|
|
+ is_4k = False
|
|
|
+ is_high_res = True
|
|
|
+
|
|
|
+ # Apple TV 4K optimized encoding based on source quality:
|
|
|
+ if is_4k:
|
|
|
+ # 4K content: Use H.265 for better compression at high resolution
|
|
|
+ codec_args = [
|
|
|
+ "-c:v", "libx265", # H.265 for 4K (better compression)
|
|
|
+ "-preset", "medium", # Better compression for 4K
|
|
|
+ "-crf", "20", # Higher quality for 4K content
|
|
|
+ "-level", "5.1", # Level 5.1 supports 4K
|
|
|
+ "-pix_fmt", "yuv420p", # Ensure compatibility
|
|
|
+ ]
|
|
|
+ print("Using H.265 encoding for 4K content")
|
|
|
+ elif is_high_res:
|
|
|
+ # 1080p content: Use H.264 for maximum compatibility
|
|
|
+ codec_args = [
|
|
|
+ "-c:v", "libx264", # H.264 for 1080p (max compatibility)
|
|
|
+ "-profile:v", "high", # High profile for better quality
|
|
|
+ "-preset", "fast", # Faster encoding for 1080p
|
|
|
+ "-crf", "21", # Good quality for 1080p
|
|
|
+ "-level", "4.1", # Level 4.1 supports 1080p at high bitrates
|
|
|
+ ]
|
|
|
+ print("Using H.264 encoding for 1080p content")
|
|
|
+ else:
|
|
|
+ # Lower resolution: Use H.264 with faster settings
|
|
|
+ codec_args = [
|
|
|
+ "-c:v", "libx264", # H.264 for lower res
|
|
|
+ "-profile:v", "main", # Main profile for lower res
|
|
|
+ "-preset", "fast", # Fast encoding
|
|
|
+ "-crf", "23", # Standard quality
|
|
|
+ "-level", "3.1", # Level 3.1 for lower res
|
|
|
+ ]
|
|
|
+ print("Using H.264 encoding for lower resolution content")
|
|
|
+
|
|
|
+ command = [
|
|
|
+ "ffmpeg", "-i", input_file,
|
|
|
+ *codec_args,
|
|
|
+ "-c:a", "aac", # AAC audio codec
|
|
|
+ "-b:a", "192k", # Higher audio bitrate for better quality
|
|
|
+ "-movflags", "+faststart", # Optimize for streaming/quick playback
|
|
|
+ "-y", # Overwrite output file if exists
|
|
|
+ mp4_file
|
|
|
+ ]
|
|
|
+
|
|
|
+ try:
|
|
|
+ print("Starting conversion... This may take a while for 4K content.")
|
|
|
+ subprocess.run(command, check=True)
|
|
|
+ print(f"Conversion completed: {mp4_file}")
|
|
|
+ if os.path.exists(mp4_file):
|
|
|
+ os.remove(input_file)
|
|
|
+ print(f"Original file deleted: {input_file}")
|
|
|
+ return True
|
|
|
+ except subprocess.CalledProcessError as e:
|
|
|
+ print(f"Conversion failed for {input_file}: {e}")
|
|
|
+ return False
|
|
|
+
|
|
|
# Temporarily commented out video conversion functionality
|
|
|
# def convert_to_mp4(input_file):
|
|
|
# """Convert a video to mp4 and delete the original file."""
|
|
|
@@ -226,14 +313,34 @@ def main():
|
|
|
print(f"Skipping video {video['video_id']} due to download error.")
|
|
|
continue # Skip this video if download failed
|
|
|
|
|
|
- # Temporarily comment out conversion, but keep it in the code for future use
|
|
|
- # video_file = next(
|
|
|
- # (f for f in os.listdir(video_dir) if not f.endswith(".mp4")),
|
|
|
- # None
|
|
|
- # )
|
|
|
- # if video_file:
|
|
|
- # input_path = os.path.join(video_dir, video_file)
|
|
|
- # threading.Thread(target=convert_to_mp4, args=(input_path,)).start()
|
|
|
+ # Check if conversion is needed (VP9/AV1 codecs or non-MP4 files)
|
|
|
+ needs_conversion = False
|
|
|
+ conversion_file = None
|
|
|
+
|
|
|
+ for file in os.listdir(video_dir):
|
|
|
+ if file.endswith(('.mp4', '.mkv', '.webm', '.m4v')):
|
|
|
+ file_path = os.path.join(video_dir, file)
|
|
|
+ try:
|
|
|
+ # Quick check for problematic codecs
|
|
|
+ probe_cmd = ["ffprobe", "-v", "quiet", "-select_streams", "v:0",
|
|
|
+ "-show_entries", "stream=codec_name", "-of", "csv=p=0", file_path]
|
|
|
+ result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
|
|
|
+ codec = result.stdout.strip().lower()
|
|
|
+
|
|
|
+ if codec in ['vp9', 'av01', 'av1'] or not file.endswith('.mp4'):
|
|
|
+ needs_conversion = True
|
|
|
+ conversion_file = file_path
|
|
|
+ print(f"Found {codec} codec - will convert for Apple TV compatibility")
|
|
|
+ break
|
|
|
+ except subprocess.CalledProcessError:
|
|
|
+ # If we can't probe, assume it might need conversion if not MP4
|
|
|
+ if not file.endswith('.mp4'):
|
|
|
+ needs_conversion = True
|
|
|
+ conversion_file = file_path
|
|
|
+ break
|
|
|
+
|
|
|
+ if needs_conversion and conversion_file:
|
|
|
+ threading.Thread(target=convert_to_apple_tv_compatible, args=(conversion_file,)).start()
|
|
|
|
|
|
# Create the NFO file
|
|
|
nfo_file_path = os.path.join(video_dir, "movie.nfo")
|
|
|
@@ -263,14 +370,34 @@ def main():
|
|
|
print(f"Skipping popular video {video['video_id']} due to download error.")
|
|
|
continue # Skip this video if download failed
|
|
|
|
|
|
- # Temporarily comment out conversion, but keep it in the code for future use
|
|
|
- # video_file = next(
|
|
|
- # (f for f in os.listdir(video_dir) if not f.endswith(".mp4")),
|
|
|
- # None
|
|
|
- # )
|
|
|
- # if video_file:
|
|
|
- # input_path = os.path.join(video_dir, video_file)
|
|
|
- # threading.Thread(target=convert_to_mp4, args=(input_path,)).start()
|
|
|
+ # Check if conversion is needed (VP9/AV1 codecs or non-MP4 files)
|
|
|
+ needs_conversion = False
|
|
|
+ conversion_file = None
|
|
|
+
|
|
|
+ for file in os.listdir(video_dir):
|
|
|
+ if file.endswith(('.mp4', '.mkv', '.webm', '.m4v')):
|
|
|
+ file_path = os.path.join(video_dir, file)
|
|
|
+ try:
|
|
|
+ # Quick check for problematic codecs
|
|
|
+ probe_cmd = ["ffprobe", "-v", "quiet", "-select_streams", "v:0",
|
|
|
+ "-show_entries", "stream=codec_name", "-of", "csv=p=0", file_path]
|
|
|
+ result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
|
|
|
+ codec = result.stdout.strip().lower()
|
|
|
+
|
|
|
+ if codec in ['vp9', 'av01', 'av1'] or not file.endswith('.mp4'):
|
|
|
+ needs_conversion = True
|
|
|
+ conversion_file = file_path
|
|
|
+ print(f"Found {codec} codec - will convert for Apple TV compatibility")
|
|
|
+ break
|
|
|
+ except subprocess.CalledProcessError:
|
|
|
+ # If we can't probe, assume it might need conversion if not MP4
|
|
|
+ if not file.endswith('.mp4'):
|
|
|
+ needs_conversion = True
|
|
|
+ conversion_file = file_path
|
|
|
+ break
|
|
|
+
|
|
|
+ if needs_conversion and conversion_file:
|
|
|
+ threading.Thread(target=convert_to_apple_tv_compatible, args=(conversion_file,)).start()
|
|
|
|
|
|
# Create the NFO file
|
|
|
nfo_file_path = os.path.join(video_dir, "movie.nfo")
|