diff --git a/PRD.md b/PRD.md index f2f009e..9f7c748 100644 --- a/PRD.md +++ b/PRD.md @@ -1,8 +1,8 @@ -# 🎀 Karaoke Video Downloader – PRD (v3.4.3) +# 🎀 Karaoke Video Downloader – PRD (v3.4.4) ## βœ… Overview -A Python-based Windows CLI tool to download karaoke videos from YouTube channels/playlists using `yt-dlp.exe`, with advanced tracking, songlist prioritization, and flexible configuration. The codebase has been comprehensively refactored into a modular architecture with centralized utilities for improved maintainability, error handling, and code reuse. +A Python-based cross-platform CLI tool to download karaoke videos from YouTube channels/playlists using `yt-dlp`, with advanced tracking, songlist prioritization, and flexible configuration. Supports Windows and macOS with automatic platform detection. The codebase has been comprehensively refactored into a modular architecture with centralized utilities for improved maintainability, error handling, and code reuse. --- @@ -63,9 +63,9 @@ The codebase has been refactored into focused modules with centralized utilities --- ## βš™οΈ Platform & Stack -- **Platform:** Windows +- **Platform:** Windows, macOS - **Interface:** Command-line (CLI) -- **Tech Stack:** Python 3.7+, yt-dlp.exe, mutagen (for ID3 tagging) +- **Tech Stack:** Python 3.7+, yt-dlp (platform-specific binary), mutagen (for ID3 tagging) --- @@ -164,9 +164,11 @@ KaroakeVideoDownloader/ β”œβ”€β”€ downloads/ # All video output β”‚ └── [ChannelName]/ # Per-channel folders β”œβ”€β”€ logs/ # Download logs -β”œβ”€β”€ downloader/yt-dlp.exe # yt-dlp binary -β”œβ”€β”€ tests/ # Diagnostic and test scripts -β”‚ └── test_installation.py +β”œβ”€β”€ downloader/yt-dlp.exe # yt-dlp binary (Windows) +β”œβ”€β”€ downloader/yt-dlp_macos # yt-dlp binary (macOS) +β”œβ”€β”€ src/tests/ # Test scripts +β”‚ β”œβ”€β”€ test_macos.py # macOS setup and functionality tests +β”‚ └── test_platform.py # Platform detection tests β”œβ”€β”€ download_karaoke.py # Main entry point (thin wrapper) β”œβ”€β”€ README.md β”œβ”€β”€ PRD.md @@ -530,6 +532,37 @@ def download_new_mode(self, ...): - [ ] Advanced configuration UI - [ ] Real-time download progress visualization +## πŸ”§ Recent Bug Fixes & Improvements (v3.4.4) +### **macOS Support with Automatic Platform Detection** +- **Cross-platform compatibility**: Added support for macOS alongside Windows +- **Automatic platform detection**: Detects operating system and selects appropriate yt-dlp binary +- **Flexible yt-dlp integration**: Supports both binary files (`yt-dlp_macos`) and pip installation (`python3 -m yt_dlp`) +- **Setup automation**: `setup_macos.py` script for easy macOS setup with FFmpeg and yt-dlp installation +- **Command parsing**: Intelligent parsing of yt-dlp commands (file paths vs. module commands) +- **Enhanced validation**: Platform-specific error messages and validation in CLI +- **Backward compatibility**: Maintains full compatibility with existing Windows installations + +### **Benefits of macOS Support** +- **Native macOS experience**: No need for Windows compatibility layers or virtualization +- **Automatic setup**: Simple setup script handles all dependencies +- **Flexible installation**: Choose between binary download or pip installation +- **Consistent functionality**: All features work identically on both platforms +- **Easy maintenance**: Platform detection handles configuration automatically + +### **Setup Instructions** +```bash +# Automatic setup (recommended) +python3 setup_macos.py + +# Test installation +python3 src/tests/test_macos.py + +# Manual setup options +# 1. Install yt-dlp via pip: pip3 install yt-dlp +# 2. Download binary: curl -L -o downloader/yt-dlp_macos https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos +# 3. Install FFmpeg: brew install ffmpeg +``` + ## πŸ”§ Recent Bug Fixes & Improvements (v3.4.6) ### **Dry Run Mode** - **New `--dry-run` parameter**: Build download plan and show what would be downloaded without actually downloading anything diff --git a/README.md b/README.md index bf903ca..234348f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎀 Karaoke Video Downloader -A Python-based Windows CLI tool to download karaoke videos from YouTube channels/playlists using `yt-dlp.exe`, with advanced tracking, songlist prioritization, and flexible configuration. +A Python-based cross-platform CLI tool to download karaoke videos from YouTube channels/playlists using `yt-dlp`, with advanced tracking, songlist prioritization, and flexible configuration. Supports Windows and macOS with automatic platform detection. ## ✨ Features - 🎡 **Channel & Playlist Downloads**: Download all videos from a YouTube channel or playlist @@ -24,6 +24,7 @@ A Python-based Windows CLI tool to download karaoke videos from YouTube channels - πŸ“Š **Unmatched Songs Reports**: Generate detailed reports of songs that couldn't be found in any channel with `--generate-unmatched-report` - πŸ›‘οΈ **Duplicate File Prevention**: Automatically detects and prevents duplicate files with `(2)`, `(3)` suffixes, with cleanup utility for existing duplicates - 🏷️ **Consistent Metadata**: Filename and ID3 tag use identical artist/title format for clear file identification +- 🍎 **macOS Support**: Automatic platform detection and setup with native macOS binaries and FFmpeg integration ## πŸ—οΈ Architecture The codebase has been comprehensively refactored into a modular architecture with centralized utilities for improved maintainability, error handling, and code reuse: @@ -185,13 +186,53 @@ python data/cleanup_duplicate_files.py ``` ## πŸ“‹ Requirements -- **Windows 10/11** +- **Windows 10/11 or macOS 10.14+** - **Python 3.7+** -- **yt-dlp.exe** (in `downloader/`) +- **yt-dlp binary** (platform-specific, see setup instructions below) - **mutagen** (for ID3 tagging, optional) - **ffmpeg/ffprobe** (for video validation, optional but recommended) - **rapidfuzz** (for fuzzy matching, optional, falls back to difflib) +## 🍎 macOS Setup + +### Automatic Setup (Recommended) +Run the macOS setup script to automatically set up yt-dlp and FFmpeg: + +```bash +python3 setup_macos.py +``` + +This script will: +- Detect your macOS version +- Offer installation options for yt-dlp (pip or binary download) +- Install FFmpeg via Homebrew +- Test the installation + +### Manual Setup +If you prefer to set up manually: + +#### Option 1: Install yt-dlp via pip +```bash +pip3 install yt-dlp +``` + +#### Option 2: Download yt-dlp binary +```bash +mkdir -p downloader +curl -L -o downloader/yt-dlp_macos https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos +chmod +x downloader/yt-dlp_macos +``` + +#### Install FFmpeg +```bash +brew install ffmpeg +``` + +### Test Installation +```bash +python3 src/tests/test_macos.py +``` + ## πŸš€ Quick Start > **πŸ’‘ Pro Tip**: For a complete list of all available commands, see `commands.txt` - you can copy/paste any command directly into your terminal! @@ -370,9 +411,11 @@ KaroakeVideoDownloader/ β”œβ”€β”€ downloads/ # All video output β”‚ └── [ChannelName]/ # Per-channel folders β”œβ”€β”€ logs/ # Download logs -β”œβ”€β”€ downloader/yt-dlp.exe # yt-dlp binary -β”œβ”€β”€ tests/ # Diagnostic and test scripts -β”‚ └── test_installation.py +β”œβ”€β”€ downloader/yt-dlp.exe # yt-dlp binary (Windows) +β”œβ”€β”€ downloader/yt-dlp_macos # yt-dlp binary (macOS) +β”œβ”€β”€ src/tests/ # Test scripts +β”‚ β”œβ”€β”€ test_macos.py # macOS setup and functionality tests +β”‚ └── test_platform.py # Platform detection tests β”œβ”€β”€ download_karaoke.py # Main entry point (thin wrapper) β”œβ”€β”€ README.md β”œβ”€β”€ PRD.md @@ -574,7 +617,8 @@ The codebase has been comprehensively refactored to improve maintainability and - **Robust download plan execution:** Fixed index management in download plan execution to prevent errors during interrupted downloads. ## 🐞 Troubleshooting -- Ensure `yt-dlp.exe` is in the `downloader/` folder +- **Windows**: Ensure `yt-dlp.exe` is in the `downloader/` folder +- **macOS**: Run `python3 setup_macos.py` to set up yt-dlp and FFmpeg - Check `logs/` for error details - Use `python -m karaoke_downloader.check_resolution` to verify video quality - If you see errors about ffmpeg/ffprobe, install [ffmpeg](https://ffmpeg.org/download.html) and ensure it is in your PATH diff --git a/commands.txt b/commands.txt index cd07ca1..d45aaf0 100644 --- a/commands.txt +++ b/commands.txt @@ -1,6 +1,6 @@ # 🎀 Karaoke Video Downloader - CLI Commands Reference # Copy and paste these commands into your terminal -# Updated: v3.4.4 (includes all videos download mode, manual video collection, channel parsing rules, and all previous improvements) +# Updated: v3.4.4 (includes macOS support, all videos download mode, manual video collection, channel parsing rules, and all previous improvements) ## πŸ“₯ BASIC DOWNLOADS @@ -313,6 +313,24 @@ python download_karaoke.py --parallel --workers 3 --manual --limit 5 # 7c. Manual videos with fuzzy matching python download_karaoke.py --manual --fuzzy-match --fuzzy-threshold 85 --limit 10 +## 🍎 macOS SETUP COMMANDS + +# Automatic macOS setup (detects OS and installs yt-dlp + FFmpeg) +python3 setup_macos.py + +# Test macOS setup and functionality +python3 src/tests/test_macos.py + +# Manual macOS setup options +# Install yt-dlp via pip +pip3 install yt-dlp + +# Download yt-dlp binary for macOS +mkdir -p downloader && curl -L -o downloader/yt-dlp_macos https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos && chmod +x downloader/yt-dlp_macos + +# Install FFmpeg via Homebrew +brew install ffmpeg + ## πŸ”§ TROUBLESHOOTING COMMANDS # Check if everything is working @@ -352,6 +370,7 @@ python download_karaoke.py --clear-server-duplicates # - Use --fuzzy-match for better song discovery # - Use --refresh sparingly (forces re-scan) # - Clear cache if you encounter issues +# - macOS users: Run `python3 setup_macos.py` for automatic setup # Parallel download tips: # - Start with --workers 3 for conservative approach diff --git a/data/channel_cache/@LetsSingKaraoke.json b/data/channel_cache/@LetsSingKaraoke.json new file mode 100644 index 0000000..ef5701a --- /dev/null +++ b/data/channel_cache/@LetsSingKaraoke.json @@ -0,0 +1,19 @@ +{ + "channel_id": "@LetsSingKaraoke", + "videos": [ + { + "title": "Sub Urban - Cradles | Karaoke (instrumental)", + "id": "8uj7IzhdiO4" + }, + { + "title": "Sia - Snowman | Karaoke (instrumental)", + "id": "ZbWHuncTgsM" + }, + { + "title": "Trevor Daniel - Falling | Karaoke (Instrumental)", + "id": "nU7n2aq7f98" + } + ], + "last_updated": "2025-08-05T15:59:09.280488", + "video_count": 3 +} \ No newline at end of file diff --git a/data/channel_cache/@LetsSingKaraoke_raw_output.txt b/data/channel_cache/@LetsSingKaraoke_raw_output.txt new file mode 100644 index 0000000..68db103 --- /dev/null +++ b/data/channel_cache/@LetsSingKaraoke_raw_output.txt @@ -0,0 +1,10 @@ +# Raw yt-dlp output for @LetsSingKaraoke +# Channel URL: https://www.youtube.com/@LetsSingKaraoke/videos +# Command: downloader/yt-dlp_macos --flat-playlist --print %(title)s|%(id)s|%(url)s --verbose https://www.youtube.com/@LetsSingKaraoke/videos +# Timestamp: 2025-08-05T15:59:09.280155 +# Total lines: 3 +################################################################################ + + 1: Sub Urban - Cradles | Karaoke (instrumental)|8uj7IzhdiO4|https://www.youtube.com/watch?v=8uj7IzhdiO4 + 2: Sia - Snowman | Karaoke (instrumental)|ZbWHuncTgsM|https://www.youtube.com/watch?v=ZbWHuncTgsM + 3: Trevor Daniel - Falling | Karaoke (Instrumental)|nU7n2aq7f98|https://www.youtube.com/watch?v=nU7n2aq7f98 diff --git a/data/config.json b/data/config.json index 30f408b..1933a61 100644 --- a/data/config.json +++ b/data/config.json @@ -34,5 +34,12 @@ "include_console": true, "include_file": true }, + "platform_settings": { + "auto_detect_platform": true, + "yt_dlp_paths": { + "windows": "downloader/yt-dlp.exe", + "macos": "downloader/yt-dlp_macos" + } + }, "yt_dlp_path": "downloader/yt-dlp.exe" } \ No newline at end of file diff --git a/data/plan_latest_per_channel_channels8_hash903857ec_force_downloadFalse_limit_per_channel2.json b/data/plan_latest_per_channel_channels8_hash903857ec_force_downloadFalse_limit_per_channel2.json new file mode 100644 index 0000000..6516751 --- /dev/null +++ b/data/plan_latest_per_channel_channels8_hash903857ec_force_downloadFalse_limit_per_channel2.json @@ -0,0 +1,78 @@ +{ + "timestamp": "2025-08-05T16:01:09.018725", + "download_plan": [ + { + "video_id": "oHV8Iw0R4BY", + "artist": "Shaboozey, Jelly Roll", + "title": "Amen", + "filename": "Shaboozey, Jelly Roll - Amen.mp4", + "channel_name": "@SingKingKaraoke", + "video_title": "Shaboozey, Jelly Roll - Amen (Karaoke Version)", + "force_download": false + }, + { + "video_id": "Jm3a-VAomH0", + "artist": "Pet Shop Boys", + "title": "Domino Dancing", + "filename": "Pet Shop Boys - Domino Dancing.mp4", + "channel_name": "@KaraokeOnVEVO", + "video_title": "Pet Shop Boys - Domino Dancing (Karaoke)", + "force_download": false + }, + { + "video_id": "6Vb0igX0-Ss", + "artist": "Chappell Roan", + "title": "The Giver", + "filename": "Chappell Roan - The Giver.mp4", + "channel_name": "@StingrayKaraoke", + "video_title": "Chappell Roan - The Giver (Karaoke Version)", + "force_download": false + }, + { + "video_id": "b1k2_B9oCr4", + "artist": "James Arthur", + "title": "Train Wreck", + "filename": "James Arthur - Train Wreck.mp4", + "channel_name": "@sing2karaoke", + "video_title": "James Arthur Train Wreck", + "force_download": false + }, + { + "video_id": "cg10FeEYSSQ", + "artist": "Caesars", + "title": "Jerk It Out", + "filename": "Caesars - Jerk It Out.mp4", + "channel_name": "@ZoomKaraokeOfficial", + "video_title": "Caesars - Jerk It Out - Karaoke Version from Zoom Karaoke", + "force_download": false + }, + { + "video_id": "m51bbu2ghp4", + "artist": "Jin", + "title": "Don't Say You Love Me", + "filename": "Jin - Dont Say You Love Me.mp4", + "channel_name": "@VocalStarKaraoke", + "video_title": "Don't Say You Love Me - Jin KARAOKE With Vocal Guide", + "force_download": false + }, + { + "video_id": "qegLWI99Wg0", + "artist": "Ed Sheeran & BeyoncΓ©", + "title": "Perfect Duet", + "filename": "Ed Sheeran & BeyoncΓ© - Perfect Duet.mp4", + "channel_name": "Unknown", + "video_title": "Ed Sheeran & BeyoncΓ© - Perfect Duet", + "force_download": false + }, + { + "video_id": "ZbWHuncTgsM", + "artist": "Sia", + "title": "Snowman | Karaoke (instrumental)", + "filename": "Sia - Snowman Karaoke (instrumental).mp4", + "channel_name": "@LetsSingKaraoke", + "video_title": "Sia - Snowman | Karaoke (instrumental)", + "force_download": false + } + ], + "unmatched": [] +} \ No newline at end of file diff --git a/data/server_duplicates_tracking.json b/data/server_duplicates_tracking.json index 2f8c33a..9ad3fb1 100644 --- a/data/server_duplicates_tracking.json +++ b/data/server_duplicates_tracking.json @@ -31110,5 +31110,29 @@ "channel": "@SingKingKaraoke", "marked_at": "2025-07-29T14:46:07.860090", "reason": "already_on_server" + }, + "teddy swims_lose control": { + "artist": "Teddy Swims", + "title": "Lose Control", + "video_title": "Teddy Swims - Lose Control", + "channel": "songlist", + "marked_at": "2025-08-05T16:07:47.891258", + "reason": "already_on_server" + }, + "benson boone_mystical magical": { + "artist": "Benson Boone", + "title": "Mystical Magical", + "video_title": "Benson Boone - Mystical Magical", + "channel": "songlist", + "marked_at": "2025-08-05T16:07:47.906113", + "reason": "already_on_server" + }, + "the weeknd_the hills": { + "artist": "The Weeknd", + "title": "The Hills", + "video_title": "The Weeknd - The Hills", + "channel": "songlist", + "marked_at": "2025-08-05T16:07:47.922750", + "reason": "already_on_server" } } \ No newline at end of file diff --git a/data/unmatched_songs_report_20250805_160748.json b/data/unmatched_songs_report_20250805_160748.json new file mode 100644 index 0000000..b4f05d6 --- /dev/null +++ b/data/unmatched_songs_report_20250805_160748.json @@ -0,0 +1,12 @@ +{ + "generated_at": "2025-08-05T16:07:48.031279", + "total_unmatched": 1, + "unmatched_songs": [ + { + "artist": "SZA", + "title": "30 For 30", + "position": 3, + "search_key": "sza_30 for 30" + } + ] +} \ No newline at end of file diff --git a/downloader/yt-dlp_macos b/downloader/yt-dlp_macos new file mode 100755 index 0000000..df8a46b Binary files /dev/null and b/downloader/yt-dlp_macos differ diff --git a/karaoke_downloader/cli.py b/karaoke_downloader/cli.py index 569f86c..98a5b82 100644 --- a/karaoke_downloader/cli.py +++ b/karaoke_downloader/cli.py @@ -328,11 +328,36 @@ Examples: print("❌ Error: --channel-workers must be between 1 and 10") sys.exit(1) - yt_dlp_path = Path("downloader/yt-dlp.exe") - if not yt_dlp_path.exists(): - print("❌ Error: yt-dlp.exe not found in downloader/ directory") - print("Please ensure yt-dlp.exe is present in the downloader/ folder") - sys.exit(1) + # Load configuration to get platform-aware yt-dlp path + from karaoke_downloader.config_manager import load_config + config = load_config() + yt_dlp_path = config.yt_dlp_path + + # Check if it's a command string (like "python3 -m yt_dlp") or a file path + if yt_dlp_path.startswith(('python', 'python3')): + # It's a command string, test if it works + try: + import subprocess + cmd = yt_dlp_path.split() + ["--version"] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if result.returncode != 0: + raise Exception(f"Command failed: {result.stderr}") + except Exception as e: + platform_name = "macOS" if sys.platform == "darwin" else "Windows" + print(f"❌ Error: yt-dlp command failed: {yt_dlp_path}") + print(f"Please ensure yt-dlp is properly installed for {platform_name}") + print(f"Error: {e}") + sys.exit(1) + else: + # It's a file path, check if it exists + yt_dlp_file = Path(yt_dlp_path) + if not yt_dlp_file.exists(): + platform_name = "macOS" if sys.platform == "darwin" else "Windows" + binary_name = yt_dlp_file.name + print(f"❌ Error: {binary_name} not found in downloader/ directory") + print(f"Please ensure {binary_name} is present in the downloader/ folder for {platform_name}") + print(f"Expected path: {yt_dlp_file}") + sys.exit(1) downloader = KaraokeDownloader() diff --git a/karaoke_downloader/config_manager.py b/karaoke_downloader/config_manager.py index 3007b41..15beedb 100644 --- a/karaoke_downloader/config_manager.py +++ b/karaoke_downloader/config_manager.py @@ -4,6 +4,8 @@ Provides centralized configuration loading, validation, and management. """ import json +import platform +import sys from dataclasses import dataclass, field from datetime import datetime from pathlib import Path @@ -42,6 +44,13 @@ DEFAULT_CONFIG = { "include_console": True, "include_file": True, }, + "platform_settings": { + "auto_detect_platform": True, + "yt_dlp_paths": { + "windows": "downloader/yt-dlp.exe", + "macos": "downloader/yt-dlp_macos" + } + }, "yt_dlp_path": "downloader/yt-dlp.exe", } @@ -55,6 +64,23 @@ RESOLUTION_MAP = { } +def detect_platform() -> str: + """Detect the current platform and return platform name.""" + system = platform.system().lower() + if system == "windows": + return "windows" + elif system == "darwin": + return "macos" + else: + return "windows" # Default to Windows for other platforms + + +def get_platform_yt_dlp_path(platform_paths: Dict[str, str]) -> str: + """Get the appropriate yt-dlp path for the current platform.""" + platform_name = detect_platform() + return platform_paths.get(platform_name, platform_paths.get("windows", "downloader/yt-dlp.exe")) + + @dataclass class DownloadSettings: """Configuration for download settings.""" @@ -234,11 +260,21 @@ class ConfigManager: folder_structure = FolderStructure(**config_data.get("folder_structure", {})) logging_config = LoggingConfig(**config_data.get("logging", {})) + # Handle platform-specific yt-dlp path + yt_dlp_path = config_data.get("yt_dlp_path", "downloader/yt-dlp.exe") + + # Check if platform auto-detection is enabled + platform_settings = config_data.get("platform_settings", {}) + if platform_settings.get("auto_detect_platform", True): + platform_paths = platform_settings.get("yt_dlp_paths", {}) + if platform_paths: + yt_dlp_path = get_platform_yt_dlp_path(platform_paths) + return AppConfig( download_settings=download_settings, folder_structure=folder_structure, logging=logging_config, - yt_dlp_path=config_data.get("yt_dlp_path", "downloader/yt-dlp.exe"), + yt_dlp_path=yt_dlp_path, _config_file=self.config_file, ) diff --git a/karaoke_downloader/downloader.py b/karaoke_downloader/downloader.py index ce5e22d..6c4476f 100644 --- a/karaoke_downloader/downloader.py +++ b/karaoke_downloader/downloader.py @@ -80,7 +80,7 @@ class KaraokeDownloader: self.config = self.config_manager.load_config() # Initialize paths - self.yt_dlp_path = Path(self.config.yt_dlp_path) + self.yt_dlp_path = self.config.yt_dlp_path # Keep as string for command parsing self.downloads_dir = Path(self.config.folder_structure.downloads_dir) self.logs_dir = Path(self.config.folder_structure.logs_dir) @@ -799,7 +799,6 @@ class KaraokeDownloader: from karaoke_downloader.cache_manager import get_download_plan_cache_file, save_plan_cache plan_kwargs = { - "mode": "latest_per_channel", "channels": len(channel_urls), "limit_per_channel": limit, "force_download": force_download, diff --git a/karaoke_downloader/tracking_manager.py b/karaoke_downloader/tracking_manager.py index c65aee1..175e3f8 100644 --- a/karaoke_downloader/tracking_manager.py +++ b/karaoke_downloader/tracking_manager.py @@ -551,10 +551,10 @@ class TrackingManager: print(f" πŸ“‘ Channel URL: {channel_url}") import subprocess + from karaoke_downloader.youtube_utils import _parse_yt_dlp_command # First, let's get the total count to show progress - count_cmd = [ - yt_dlp_path, + count_cmd = _parse_yt_dlp_command(yt_dlp_path) + [ "--flat-playlist", "--print", "%(title)s", @@ -576,8 +576,7 @@ class TrackingManager: print(f" ⚠️ Channel test error: {e}") # Now fetch all videos with progress indicators - cmd = [ - yt_dlp_path, + cmd = _parse_yt_dlp_command(yt_dlp_path) + [ "--flat-playlist", "--print", "%(title)s|%(id)s|%(url)s", diff --git a/karaoke_downloader/youtube_utils.py b/karaoke_downloader/youtube_utils.py index bb65cab..e213555 100644 --- a/karaoke_downloader/youtube_utils.py +++ b/karaoke_downloader/youtube_utils.py @@ -9,6 +9,19 @@ from typing import Any, Dict, List, Optional, Union from karaoke_downloader.config_manager import AppConfig +def _parse_yt_dlp_command(yt_dlp_path: str) -> List[str]: + """ + Parse yt-dlp path/command into a list of command arguments. + Handles both file paths and command strings like 'python3 -m yt_dlp'. + """ + if yt_dlp_path.startswith(('python', 'python3')): + # It's a Python module command + return yt_dlp_path.split() + else: + # It's a file path + return [yt_dlp_path] + + def get_channel_info( channel_url: str, yt_dlp_path: str = "downloader/yt-dlp.exe" ) -> tuple[str, str]: @@ -43,7 +56,7 @@ def get_playlist_info( ) -> List[Dict[str, Any]]: """Get playlist information using yt-dlp.""" try: - cmd = [yt_dlp_path, "--dump-json", "--flat-playlist", playlist_url] + cmd = _parse_yt_dlp_command(yt_dlp_path) + ["--dump-json", "--flat-playlist", playlist_url] result = subprocess.run(cmd, capture_output=True, text=True, check=True) videos = [] for line in result.stdout.strip().split("\n"): @@ -75,8 +88,7 @@ def build_yt_dlp_command( Returns: List of command arguments for subprocess.run """ - cmd = [ - str(yt_dlp_path), + cmd = _parse_yt_dlp_command(yt_dlp_path) + [ "--no-check-certificates", "--ignore-errors", "--no-warnings", @@ -128,7 +140,7 @@ def show_available_formats( timeout: Timeout in seconds """ print(f"πŸ” Checking available formats for: {video_url}") - format_cmd = [str(yt_dlp_path), "--list-formats", video_url] + format_cmd = _parse_yt_dlp_command(yt_dlp_path) + ["--list-formats", video_url] try: format_result = subprocess.run( format_cmd, capture_output=True, text=True, timeout=timeout diff --git a/setup_macos.py b/setup_macos.py new file mode 100644 index 0000000..4992166 --- /dev/null +++ b/setup_macos.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +""" +macOS setup script for Karaoke Video Downloader. +This script helps users set up yt-dlp and FFmpeg on macOS. +""" + +import os +import sys +import subprocess +from pathlib import Path + + +def check_ffmpeg(): + """Check if FFmpeg is installed.""" + try: + result = subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True, timeout=10) + return result.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError): + return False + + +def check_yt_dlp(): + """Check if yt-dlp is installed via pip or binary.""" + # Check pip installation + try: + result = subprocess.run([sys.executable, "-m", "yt_dlp", "--version"], + capture_output=True, text=True, timeout=10) + if result.returncode == 0: + return True + except (subprocess.TimeoutExpired, subprocess.CalledProcessError): + pass + + # Check binary file + binary_path = Path("downloader/yt-dlp_macos") + if binary_path.exists(): + try: + result = subprocess.run([str(binary_path), "--version"], + capture_output=True, text=True, timeout=10) + return result.returncode == 0 + except (subprocess.TimeoutExpired, subprocess.CalledProcessError): + pass + + return False + + +def install_ffmpeg(): + """Install FFmpeg via Homebrew.""" + print("🎬 Installing FFmpeg...") + + # Check if Homebrew is installed + try: + subprocess.run(["brew", "--version"], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + print("❌ Homebrew is not installed. Please install Homebrew first:") + print(" /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"") + return False + + try: + print("🍺 Installing FFmpeg via Homebrew...") + result = subprocess.run(["brew", "install", "ffmpeg"], + capture_output=True, text=True, check=True) + print("βœ… FFmpeg installed successfully!") + return True + except subprocess.CalledProcessError as e: + print(f"❌ Failed to install FFmpeg: {e}") + return False + + +def download_yt_dlp_binary(): + """Download yt-dlp binary for macOS.""" + print("πŸ“₯ Downloading yt-dlp binary for macOS...") + + # Create downloader directory if it doesn't exist + downloader_dir = Path("downloader") + downloader_dir.mkdir(exist_ok=True) + + # Download yt-dlp binary + binary_path = downloader_dir / "yt-dlp_macos" + url = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos" + + try: + print(f"πŸ“‘ Downloading from: {url}") + result = subprocess.run(["curl", "-L", "-o", str(binary_path), url], + capture_output=True, text=True, check=True) + + # Make it executable + binary_path.chmod(0o755) + print(f"βœ… yt-dlp binary downloaded to: {binary_path}") + + # Test the binary + test_result = subprocess.run([str(binary_path), "--version"], + capture_output=True, text=True, timeout=10) + if test_result.returncode == 0: + version = test_result.stdout.strip() + print(f"βœ… Binary test successful! Version: {version}") + return True + else: + print(f"❌ Binary test failed: {test_result.stderr}") + return False + + except subprocess.CalledProcessError as e: + print(f"❌ Failed to download yt-dlp binary: {e}") + return False + except Exception as e: + print(f"❌ Error downloading binary: {e}") + return False + + +def install_yt_dlp(): + """Install yt-dlp via pip.""" + print("πŸ“¦ Installing yt-dlp...") + + try: + result = subprocess.run([sys.executable, "-m", "pip", "install", "yt-dlp"], + capture_output=True, text=True, check=True) + print("βœ… yt-dlp installed successfully!") + return True + except subprocess.CalledProcessError as e: + print(f"❌ Failed to install yt-dlp: {e}") + return False + + +def test_installation(): + """Test the installation.""" + print("\nπŸ§ͺ Testing installation...") + + # Test FFmpeg + if check_ffmpeg(): + print("βœ… FFmpeg is working!") + else: + print("❌ FFmpeg is not working") + return False + + # Test yt-dlp + if check_yt_dlp(): + print("βœ… yt-dlp is working!") + else: + print("❌ yt-dlp is not working") + return False + + return True + + +def main(): + print("🍎 macOS Setup for Karaoke Video Downloader") + print("=" * 50) + + # Check current status + print("πŸ” Checking current installation...") + ffmpeg_installed = check_ffmpeg() + yt_dlp_installed = check_yt_dlp() + + print(f"FFmpeg: {'βœ… Installed' if ffmpeg_installed else '❌ Not installed'}") + print(f"yt-dlp: {'βœ… Installed' if yt_dlp_installed else '❌ Not installed'}") + + if ffmpeg_installed and yt_dlp_installed: + print("\nπŸŽ‰ Everything is already installed and working!") + return + + # Install missing components + print("\nπŸš€ Installing missing components...") + + # Install FFmpeg if needed + if not ffmpeg_installed: + print("\n🎬 FFmpeg Installation Options:") + print("1. Install via Homebrew (recommended)") + print("2. Download from ffmpeg.org") + print("3. Skip FFmpeg installation") + + choice = input("\nChoose an option (1-3): ").strip() + + if choice == "1": + if not install_ffmpeg(): + print("❌ FFmpeg installation failed") + return + elif choice == "2": + print("πŸ“₯ Please download FFmpeg from: https://ffmpeg.org/download.html") + print(" Extract and add to your PATH, then run this script again.") + return + elif choice == "3": + print("⚠️ FFmpeg is required for video processing. Some features may not work.") + else: + print("❌ Invalid choice") + return + + # Install yt-dlp if needed + if not yt_dlp_installed: + print("\nπŸ“¦ yt-dlp Installation Options:") + print("1. Install via pip (recommended)") + print("2. Download binary file") + print("3. Skip yt-dlp installation") + + choice = input("\nChoose an option (1-3): ").strip() + + if choice == "1": + if not install_yt_dlp(): + print("❌ yt-dlp installation failed") + return + elif choice == "2": + if not download_yt_dlp_binary(): + print("❌ yt-dlp binary download failed") + return + elif choice == "3": + print("❌ yt-dlp is required for video downloading.") + return + else: + print("❌ Invalid choice") + return + + # Test installation + if test_installation(): + print("\nπŸŽ‰ Setup completed successfully!") + print("You can now use the Karaoke Video Downloader on macOS.") + print("Run: python download_karaoke.py --help") + else: + print("\n❌ Setup failed. Please check the error messages above.") + + +if __name__ == "__main__": + main() \ No newline at end of file