From 56e77c19b1429f25f6bc447f537a4f3572f36f08 Mon Sep 17 00:00:00 2001 From: Jarian Cottingham Date: Fri, 20 Feb 2026 09:07:40 -0600 Subject: [PATCH] Implement category-based folder organization for downloads with single category selection for multiple videos --- youtube_cli/main.py | 131 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 118 insertions(+), 13 deletions(-) diff --git a/youtube_cli/main.py b/youtube_cli/main.py index 2876cff..a2d575b 100644 --- a/youtube_cli/main.py +++ b/youtube_cli/main.py @@ -102,11 +102,29 @@ class YouTubeCLI: def load_config(self, config_path=None): """Load configuration from file or use defaults.""" default_config = { - "download_dir": str(Path.home() / "Downloads" / "youtube"), + "download_dir": "/Volumes/MediaServer/Youtube/", "default_locations": [ - str(Path.home() / "Downloads" / "youtube"), - str(Path.home() / "Movies" / "youtube"), - "/tmp/youtube", + "/Volumes/MediaServer/Youtube/", + "/Volumes/MediaServer/Youtube/Tech", + "/Volumes/MediaServer/Youtube/AI", + "/Volumes/MediaServer/Youtube/Art", + "/Volumes/MediaServer/Youtube/Homes", + "/Volumes/MediaServer/Youtube/Cooking", + "/Volumes/MediaServer/Youtube/Fitness", + "/Volumes/MediaServer/Youtube/Music", + "/Volumes/MediaServer/Youtube/Gaming", + "/Volumes/MediaServer/Youtube/Education", + "/Volumes/MediaServer/Youtube/Travel", + "/Volumes/MediaServer/Youtube/Business", + "/Volumes/MediaServer/Youtube/Science", + "/Volumes/MediaServer/Youtube/History", + "/Volumes/MediaServer/Youtube/Comedy", + "/Volumes/MediaServer/Youtube/News", + "/Volumes/MediaServer/Youtube/Sports", + "/Volumes/MediaServer/Youtube/Nature", + "/Volumes/MediaServer/Youtube/Photography", + "/Volumes/MediaServer/Youtube/Language", + "/Volumes/MediaServer/Youtube/Automotive", ], "max_videos_per_page": 15, "yt_dlp_args": { @@ -527,6 +545,13 @@ class YouTubeCLI: return if valid_videos: + # Ask user for category selection first + console.print("[blue]Select category for all downloads:[/blue]") + selected_category = self.select_category(config) + if not selected_category: + console.print("[yellow]Download cancelled.[/yellow]") + return + # Ask user for network folder name (optional) network_folder = None console.print("[blue]Choose download destination:[/blue]") @@ -544,7 +569,7 @@ class YouTubeCLI: network_folder = None console.print( - f"[blue]Downloading {len(valid_videos)} videos in sequence...[/blue]" + f"[blue]Downloading {len(valid_videos)} videos in sequence to category '{Path(selected_category).name}'...[/blue]" ) for i, selected_video in enumerate(valid_videos): console.print( @@ -553,13 +578,14 @@ class YouTubeCLI: # Check if this is a playlist and download accordingly if selected_video.get("is_playlist", False): self.download_playlist( - selected_video["url"], config, network_folder + selected_video["url"], config, network_folder, selected_category ) else: self.download_video( selected_video["url"], config, network_folder=network_folder, + category=selected_category, ) # Give a small pause between downloads to avoid rate limiting @@ -582,7 +608,42 @@ class YouTubeCLI: "[red]Invalid input. Please enter a number or range of numbers, 'n', 's', or 'q'.[/red]" ) - def download_video(self, url, config, network_folder=None): + def get_categories(self, config): + """Get list of available categories from config.""" + return config.get("default_locations", []) + + def select_category(self, config): + """Allow user to select a category for download.""" + categories = self.get_categories(config) + + if not categories: + console.print("[red]No categories found in configuration[/red]") + return None + + console.print("\n[blue]Available Categories:[/blue]") + for i, category in enumerate(categories, 1): + # Extract just the folder name for display + folder_name = Path(category).name if Path(category).name else "Root" + console.print(f" [green]{i}[/green] - {folder_name}") + + while True: + try: + console.print("\n[blue]Select a category (enter number):[/blue]") + choice = input().strip() + choice_num = int(choice) + if 1 <= choice_num <= len(categories): + selected_category = categories[choice_num - 1] + console.print(f"[green]Selected category: {Path(selected_category).name}[/green]") + return selected_category + else: + console.print("[red]Invalid selection. Please try again.[/red]") + except ValueError: + console.print("[red]Please enter a valid number.[/red]") + except KeyboardInterrupt: + console.print("\n[yellow]Operation cancelled.[/yellow]") + return None + + def download_video(self, url, config, network_folder=None, category=None): """Download a video using yt-dlp with progress bar.""" # Validate URL before proceeding @@ -601,8 +662,28 @@ class YouTubeCLI: ) return - # Set download directory - download_dir = Path(config["download_dir"]) + # Set download directory - always use category folder + if category: + # Ensure we're not downloading directly to base path + base_dir = Path(config["download_dir"]) + category_path = Path(category) + + # If category is just the base path, we need to select a different category + if str(category_path) == str(base_dir): + console.print("[yellow]Cannot download directly to base path. Please select a category.[/yellow]") + selected_category = self.select_category(config) + if not selected_category: + return + download_dir = base_dir / selected_category + else: + download_dir = base_dir / category + else: + # If no category specified, ask user to select one + selected_category = self.select_category(config) + if not selected_category: + return + download_dir = Path(config["download_dir"]) / selected_category + download_dir.mkdir(parents=True, exist_ok=True) # Prepare yt-dlp command with better handling for JS challenges @@ -709,7 +790,7 @@ class YouTubeCLI: except Exception as e: console.print(f"[red]Error during download: {str(e)}[/red]") - def download_playlist(self, url, config, network_folder=None): + def download_playlist(self, url, config, network_folder=None, category=None): """Download a YouTube playlist into a dedicated folder.""" console.print(f"[blue]Preparing to download playlist:[/blue] {url}") @@ -748,8 +829,31 @@ class YouTubeCLI: except Exception: playlist_title = "Unknown Playlist" - # Set download directory - download_dir = Path(config["download_dir"]) + # Set download directory with category support + if category: + # Ensure we're not downloading directly to base path + base_dir = Path(config["download_dir"]) + category_path = Path(category) + + # If category is just the base path, we need to select a different category + if str(category_path) == str(base_dir): + console.print("[yellow]Cannot download directly to base path. Please select a category.[/yellow]") + selected_category = self.select_category(config) + if not selected_category: + return + download_dir = base_dir / selected_category + else: + download_dir = base_dir / category + else: + # If no category specified, ask user to select one + selected_category = self.select_category(config) + if not selected_category: + return + download_dir = Path(config["download_dir"]) / selected_category + + download_dir.mkdir(parents=True, exist_ok=True) + + # Create playlist directory within the category folder playlist_dir = download_dir / playlist_title playlist_dir.mkdir(parents=True, exist_ok=True) @@ -896,7 +1000,8 @@ def main(): "[red]Error: You must provide a video URL for downloading[/red]" ) return - cli.download_video(args.query, cli.config) + # For direct download, we'll ask for category selection + cli.download_video(args.query, cli.config, category=None) return # Default behavior - show search results