Implement category-based folder organization for downloads with single category selection for multiple videos

This commit is contained in:
Jarian Cottingham 2026-02-20 09:07:40 -06:00
parent 1d0364cbfc
commit 56e77c19b1

View File

@ -102,11 +102,29 @@ class YouTubeCLI:
def load_config(self, config_path=None): def load_config(self, config_path=None):
"""Load configuration from file or use defaults.""" """Load configuration from file or use defaults."""
default_config = { default_config = {
"download_dir": str(Path.home() / "Downloads" / "youtube"), "download_dir": "/Volumes/MediaServer/Youtube/",
"default_locations": [ "default_locations": [
str(Path.home() / "Downloads" / "youtube"), "/Volumes/MediaServer/Youtube/",
str(Path.home() / "Movies" / "youtube"), "/Volumes/MediaServer/Youtube/Tech",
"/tmp/youtube", "/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, "max_videos_per_page": 15,
"yt_dlp_args": { "yt_dlp_args": {
@ -527,6 +545,13 @@ class YouTubeCLI:
return return
if valid_videos: 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) # Ask user for network folder name (optional)
network_folder = None network_folder = None
console.print("[blue]Choose download destination:[/blue]") console.print("[blue]Choose download destination:[/blue]")
@ -544,7 +569,7 @@ class YouTubeCLI:
network_folder = None network_folder = None
console.print( 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): for i, selected_video in enumerate(valid_videos):
console.print( console.print(
@ -553,13 +578,14 @@ class YouTubeCLI:
# Check if this is a playlist and download accordingly # Check if this is a playlist and download accordingly
if selected_video.get("is_playlist", False): if selected_video.get("is_playlist", False):
self.download_playlist( self.download_playlist(
selected_video["url"], config, network_folder selected_video["url"], config, network_folder, selected_category
) )
else: else:
self.download_video( self.download_video(
selected_video["url"], selected_video["url"],
config, config,
network_folder=network_folder, network_folder=network_folder,
category=selected_category,
) )
# Give a small pause between downloads to avoid rate limiting # 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]" "[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.""" """Download a video using yt-dlp with progress bar."""
# Validate URL before proceeding # Validate URL before proceeding
@ -601,8 +662,28 @@ class YouTubeCLI:
) )
return return
# Set download directory # Set download directory - always use category folder
download_dir = Path(config["download_dir"]) 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) download_dir.mkdir(parents=True, exist_ok=True)
# Prepare yt-dlp command with better handling for JS challenges # Prepare yt-dlp command with better handling for JS challenges
@ -709,7 +790,7 @@ class YouTubeCLI:
except Exception as e: except Exception as e:
console.print(f"[red]Error during download: {str(e)}[/red]") 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.""" """Download a YouTube playlist into a dedicated folder."""
console.print(f"[blue]Preparing to download playlist:[/blue] {url}") console.print(f"[blue]Preparing to download playlist:[/blue] {url}")
@ -748,8 +829,31 @@ class YouTubeCLI:
except Exception: except Exception:
playlist_title = "Unknown Playlist" playlist_title = "Unknown Playlist"
# Set download directory # Set download directory with category support
download_dir = Path(config["download_dir"]) 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 = download_dir / playlist_title
playlist_dir.mkdir(parents=True, exist_ok=True) 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]" "[red]Error: You must provide a video URL for downloading[/red]"
) )
return 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 return
# Default behavior - show search results # Default behavior - show search results