Created
October 29, 2025 18:47
-
-
Save Codycody31/fa360c62b80e86b101bc067d4cd18c76 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import argparse | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| def download_playlist(url: str, output_dir: Path, args) -> bool: | |
| """Download YouTube playlist with yt-dlp.""" | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| # yt-dlp command configuration | |
| cmd = [ | |
| "yt-dlp", | |
| "--extract-audio", | |
| "--audio-format", "mp3", | |
| "--audio-quality", "0", # best quality | |
| "--write-info-json", | |
| "--write-thumbnail", | |
| "--convert-thumbnails", "webp", | |
| "--output", str(output_dir / "%(title)s.%(ext)s"), | |
| "--embed-metadata", | |
| "--no-playlist" if args.no_playlist else "--yes-playlist", | |
| ] | |
| # Optional: limit number of items | |
| if args.max_downloads: | |
| cmd.extend(["--max-downloads", str(args.max_downloads)]) | |
| # Optional: date filters | |
| if args.date_after: | |
| cmd.extend(["--dateafter", args.date_after]) | |
| if args.date_before: | |
| cmd.extend(["--datebefore", args.date_before]) | |
| # Optional: playlist range | |
| if args.playlist_start: | |
| cmd.extend(["--playlist-start", str(args.playlist_start)]) | |
| if args.playlist_end: | |
| cmd.extend(["--playlist-end", str(args.playlist_end)]) | |
| # Add any extra arguments | |
| if args.yt_dlp_args: | |
| cmd.extend(args.yt_dlp_args.split()) | |
| # Add the URL last | |
| cmd.append(url) | |
| print(f"[info] Downloading from: {url}") | |
| print(f"[info] Output directory: {output_dir}") | |
| print(f"[cmd] {' '.join(cmd)}\n") | |
| try: | |
| result = subprocess.run(cmd, check=True) | |
| return result.returncode == 0 | |
| except subprocess.CalledProcessError as e: | |
| print(f"[error] yt-dlp failed with exit code {e.returncode}", file=sys.stderr) | |
| return False | |
| except FileNotFoundError: | |
| print("[error] yt-dlp not found. Install it with: pip install yt-dlp", file=sys.stderr) | |
| return False | |
| def embed_metadata(output_dir: Path, args): | |
| """Run the embed script on downloaded files.""" | |
| embed_script = Path(__file__).parent / "embed_webp_to_mp3.py" | |
| if not embed_script.exists(): | |
| print(f"[warn] Embed script not found at {embed_script}, skipping metadata embedding.") | |
| return False | |
| print(f"\n[info] Embedding metadata into MP3 files...") | |
| cmd = [sys.executable, str(embed_script), str(output_dir)] | |
| # Pass through relevant arguments | |
| if args.default_album: | |
| cmd.extend(["--default-album", args.default_album]) | |
| if args.prefer_channel_as_artist: | |
| cmd.append("--prefer-channel-as-artist") | |
| if args.no_jpeg_fallback: | |
| cmd.append("--no-jpeg-fallback") | |
| if args.no_webp: | |
| cmd.append("--no-webp") | |
| if args.require_cover: | |
| cmd.append("--require-cover") | |
| try: | |
| result = subprocess.run(cmd, check=True) | |
| return result.returncode == 0 | |
| except subprocess.CalledProcessError as e: | |
| print(f"[error] Embed script failed with exit code {e.returncode}", file=sys.stderr) | |
| return False | |
| def main(): | |
| ap = argparse.ArgumentParser( | |
| description="Download YouTube playlist and embed metadata into MP3 files.", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| %(prog)s "https://www.youtube.com/playlist?list=..." -o ~/Music/MyPlaylist | |
| %(prog)s "https://www.youtube.com/watch?v=..." --no-playlist -o ~/Music/Singles | |
| %(prog)s "PLxxx" -o ~/Music/Favorites --max-downloads 10 | |
| """ | |
| ) | |
| # Required arguments | |
| ap.add_argument("url", help="YouTube playlist URL or ID") | |
| # Output options | |
| ap.add_argument("-o", "--output", required=True, | |
| help="Output directory for downloaded files") | |
| # yt-dlp options | |
| ap.add_argument("--no-playlist", action="store_true", | |
| help="Download only the video (not the playlist)") | |
| ap.add_argument("--max-downloads", type=int, | |
| help="Maximum number of items to download") | |
| ap.add_argument("--playlist-start", type=int, | |
| help="Playlist video to start at (default: 1)") | |
| ap.add_argument("--playlist-end", type=int, | |
| help="Playlist video to end at (default: last)") | |
| ap.add_argument("--date-after", metavar="DATE", | |
| help="Download only videos uploaded on or after this date (YYYYMMDD)") | |
| ap.add_argument("--date-before", metavar="DATE", | |
| help="Download only videos uploaded on or before this date (YYYYMMDD)") | |
| ap.add_argument("--yt-dlp-args", | |
| help="Additional yt-dlp arguments (as a single quoted string)") | |
| # Embed script options | |
| ap.add_argument("--default-album", default="YouTube Music", | |
| help="Album name to use if JSON has none (default: %(default)s)") | |
| ap.add_argument("--prefer-channel-as-artist", action="store_true", | |
| help="Prefer channel/uploader as Artist/Album Artist") | |
| ap.add_argument("--no-jpeg-fallback", action="store_true", | |
| help="Do not embed JPEG fallback") | |
| ap.add_argument("--no-webp", action="store_true", | |
| help="Do not embed WEBP (embed only JPEG fallback)") | |
| ap.add_argument("--require-cover", action="store_true", | |
| help="Skip files without a matching .webp") | |
| # Workflow options | |
| ap.add_argument("--download-only", action="store_true", | |
| help="Download files but don't embed metadata") | |
| ap.add_argument("--embed-only", action="store_true", | |
| help="Skip download, only embed metadata") | |
| args = ap.parse_args() | |
| output_dir = Path(args.output).expanduser().resolve() | |
| # Download phase | |
| if not args.embed_only: | |
| success = download_playlist(args.url, output_dir, args) | |
| if not success: | |
| print("\n[error] Download failed, stopping.", file=sys.stderr) | |
| sys.exit(1) | |
| # Embed phase | |
| if not args.download_only: | |
| success = embed_metadata(output_dir, args) | |
| if not success: | |
| print("\n[warn] Metadata embedding encountered errors.") | |
| print("\n[done] All operations completed.") | |
| if __name__ == "__main__": | |
| main() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment