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):
"""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