diff --git a/.gitignore b/.gitignore index fb53be3..25f57ae 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ browser.json client_secret_*.json oauth.json +# Playlist exports +playlists/ + # Python __pycache__/ *.py[cod] diff --git a/create_playlist_doom_in_bloom.py b/create_playlist_doom_in_bloom.py new file mode 100644 index 0000000..6ad1542 --- /dev/null +++ b/create_playlist_doom_in_bloom.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +""" +Create YouTube Music playlist "Doom in Bloom - Chapel Göppingen 2026" +with top 3 songs from each festival band. +""" + +import os +import sys +import json +import time +from ytmusicapi import YTMusic +from ytmusicapi.auth.browser import setup_browser + +# Lineup extracted from event poster attachment +BANDS = [ + "AHAB", + "WELL OF SOULS", + "PETRIFIED", + "APTERA", + "ASTRAL RISING", + "DAWN OF WINTER", + "TAEVUS", + "MIRROR OF DECEPTION", +] + +PLAYLIST_NAME = "Doom in Bloom - Chapel Göppingen 2026" +PLAYLIST_DESCRIPTION = ( + "Festival playlist featuring top songs from Doom in Bloom (Göppingen, 10-11 April 2026)" +) +SONGS_PER_BAND = 3 +BROWSER_AUTH_FILE = "browser.json" +BRAND_ACCOUNT_ID = "107494778873257953135" # cuidas brand account + + +def main(): + if not os.path.exists(BROWSER_AUTH_FILE): + print(f"Error: {BROWSER_AUTH_FILE} not found!") + print(f"Please follow the instructions in SETUP.md to create {BROWSER_AUTH_FILE}") + sys.exit(1) + + account_info = f" (brand account: {BRAND_ACCOUNT_ID})" if BRAND_ACCOUNT_ID else "" + print(f"Initializing YouTube Music API with {BROWSER_AUTH_FILE}{account_info}...") + try: + with open(BROWSER_AUTH_FILE, "r", encoding="utf-8") as f: + browser_data = json.load(f) + + required_keys = ["Cookie", "User-Agent"] + missing_keys = [k for k in required_keys if k not in browser_data] + if missing_keys: + print(f"Error: Missing required keys in {BROWSER_AUTH_FILE}: {missing_keys}") + sys.exit(1) + + headers_string = "\n".join([f"{k}: {v}" for k, v in browser_data.items()]) + auth_string = setup_browser(headers_raw=headers_string, filepath=None) + + from ytmusicapi.auth.auth_parse import determine_auth_type, parse_auth_str + from ytmusicapi.auth.types import AuthType + + parsed_headers, _ = parse_auth_str(auth_string) + detected_type = determine_auth_type(parsed_headers) + + # Workaround for auth type detection with browser cookie headers. + if detected_type == AuthType.OAUTH_CUSTOM_CLIENT: + parsed_headers["authorization"] = "SAPISIDHASH dummy_for_detection" + auth_dict = dict(parsed_headers) + auth_string = json.dumps(auth_dict) + + yt = YTMusic(auth=auth_string, user=BRAND_ACCOUNT_ID if BRAND_ACCOUNT_ID else None) + except json.JSONDecodeError as e: + print(f"Error parsing {BROWSER_AUTH_FILE}: {e}") + print("Please check that your browser.json file is valid JSON.") + sys.exit(1) + except Exception as e: + print(f"Error initializing YTMusic: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + print(f"\nCreating playlist: {PLAYLIST_NAME}...") + try: + playlist_id = yt.create_playlist(PLAYLIST_NAME, PLAYLIST_DESCRIPTION) + print(f"✓ Playlist created successfully! ID: {playlist_id}") + except Exception as e: + print(f"Error creating playlist: {e}") + sys.exit(1) + + added_video_ids = set() + total_added = 0 + failed_bands = [] + + print(f"\nSearching for songs from {len(BANDS)} bands...") + print("-" * 60) + + for i, band in enumerate(BANDS, 1): + print(f"\n[{i}/{len(BANDS)}] Searching for: {band}") + + try: + artist_results = yt.search(band, filter="artists", limit=1) + songs_to_add = [] + song_info_list = [] + + if not artist_results: + print(f" ⚠ No artist found for {band}, trying song search...") + search_results = yt.search(band, filter="songs", limit=SONGS_PER_BAND * 2) + if not search_results: + print(f" ⚠ No results found for {band}") + failed_bands.append(band) + continue + for result in search_results: + video_id = result.get("videoId") + if video_id and video_id not in added_video_ids: + songs_to_add.append(video_id) + song_info_list.append(result) + added_video_ids.add(video_id) + if len(songs_to_add) >= SONGS_PER_BAND: + break + else: + artist_browse_id = artist_results[0].get("browseId") + if not artist_browse_id: + print(f" ⚠ No browseId found for artist {band}") + failed_bands.append(band) + continue + + try: + artist_info = yt.get_artist(artist_browse_id) + artist_songs = artist_info.get("songs", {}).get("results", []) + + if not artist_songs: + print(f" ⚠ No songs found for artist {band}") + failed_bands.append(band) + continue + + for song in artist_songs: + video_id = song.get("videoId") + if video_id and video_id not in added_video_ids: + songs_to_add.append(video_id) + song_info_list.append(song) + added_video_ids.add(video_id) + if len(songs_to_add) >= SONGS_PER_BAND: + break + except Exception as e: + print(f" ⚠ Error getting artist info: {e}, falling back to song search...") + search_results = yt.search(band, filter="songs", limit=SONGS_PER_BAND * 2) + if not search_results: + print(f" ⚠ No results found for {band}") + failed_bands.append(band) + continue + + for result in search_results: + video_id = result.get("videoId") + if video_id and video_id not in added_video_ids: + songs_to_add.append(video_id) + song_info_list.append(result) + added_video_ids.add(video_id) + if len(songs_to_add) >= SONGS_PER_BAND: + break + + if not songs_to_add: + print(f" ⚠ No new songs to add for {band} (may be duplicates)") + continue + + max_retries = 3 + retry_delay = 5 + for attempt in range(1, max_retries + 1): + try: + yt.add_playlist_items(playlist_id, songs_to_add) + print(f" ✓ Added {len(songs_to_add)} song(s):") + for song_info in song_info_list[: len(songs_to_add)]: + title = song_info.get("title", "Unknown") + artists = song_info.get("artists", []) + if isinstance(artists, list) and len(artists) > 0: + artist = ( + artists[0].get("name", "Unknown") + if isinstance(artists[0], dict) + else str(artists[0]) + ) + else: + artist = song_info.get("artist", "Unknown") + print(f" - {title} by {artist}") + total_added += len(songs_to_add) + break + except Exception as e: + error_msg = str(e) + if "409" in error_msg or "Conflict" in error_msg: + if attempt < max_retries: + print( + f" ⚠ HTTP 409 Conflict on attempt {attempt}/{max_retries}. " + f"Retrying in {retry_delay} seconds..." + ) + time.sleep(retry_delay) + else: + print(f" ✗ Error adding songs after {max_retries} attempts: {e}") + failed_bands.append(band) + else: + print(f" ✗ Error adding songs: {e}") + failed_bands.append(band) + break + + except Exception as e: + print(f" ✗ Error searching for {band}: {e}") + failed_bands.append(band) + + print("\n" + "=" * 60) + print("SUMMARY") + print("=" * 60) + print(f"Playlist: {PLAYLIST_NAME}") + print(f"Total songs added: {total_added}") + print(f"Bands processed: {len(BANDS) - len(failed_bands)}/{len(BANDS)}") + + if failed_bands: + print(f"\nBands that failed or had no results ({len(failed_bands)}):") + for band in failed_bands: + print(f" - {band}") + + print(f"\n✓ Done! Check your YouTube Music library for the playlist.") + + +if __name__ == "__main__": + main() diff --git a/create_playlist_rock_in_weiler.py b/create_playlist_rock_in_weiler.py new file mode 100644 index 0000000..6c5e8a9 --- /dev/null +++ b/create_playlist_rock_in_weiler.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +""" +Create YouTube Music playlist "Rock in Weiler 2025" with top 3 songs from each festival band using OAuth authentication. +""" + +import os +import sys +import json +import time +from ytmusicapi import YTMusic +from ytmusicapi.auth.browser import setup_browser + +# List of all festival bands +BANDS = [ + "Unjust All", + "And Phobos Falls", + "Let Me Fall", + "Out Of Vision", + "Callejon", +] + +PLAYLIST_NAME = "Rock in Weiler 2025" +PLAYLIST_DESCRIPTION = "Festival playlist featuring top songs from Rock in Weiler 2025 bands" +SONGS_PER_BAND = 3 +BROWSER_AUTH_FILE = "browser.json" +# Brand account ID (set to None to use default account) +BRAND_ACCOUNT_ID = "107494778873257953135" # cuidas brand account + + +def main(): + # Check if browser.json exists + if not os.path.exists(BROWSER_AUTH_FILE): + print(f"Error: {BROWSER_AUTH_FILE} not found!") + print(f"Please follow the instructions in SETUP.md to create {BROWSER_AUTH_FILE}") + sys.exit(1) + + # Initialize YTMusic with browser authentication + account_info = f" (brand account: {BRAND_ACCOUNT_ID})" if BRAND_ACCOUNT_ID else "" + print(f"Initializing YouTube Music API with {BROWSER_AUTH_FILE}{account_info}...") + try: + # Load browser.json as dict + with open(BROWSER_AUTH_FILE, 'r') as f: + browser_data = json.load(f) + + # Ensure we have required keys for browser auth + required_keys = ['Cookie', 'User-Agent'] + missing_keys = [k for k in required_keys if k not in browser_data] + if missing_keys: + print(f"Error: Missing required keys in {BROWSER_AUTH_FILE}: {missing_keys}") + sys.exit(1) + + # Convert dict to headers string format (as ytmusicapi expects) + headers_string = "\n".join([f"{k}: {v}" for k, v in browser_data.items()]) + + # Use setup_browser to properly format and validate browser headers + auth_string = setup_browser(headers_raw=headers_string, filepath=None) + + # Parse the result to check auth type detection + from ytmusicapi.auth.auth_parse import determine_auth_type, parse_auth_str + from ytmusicapi.auth.types import AuthType + + parsed_headers, _ = parse_auth_str(auth_string) + detected_type = determine_auth_type(parsed_headers) + + # Workaround: determine_auth_type() only detects browser auth if there's an + # "authorization" header with "SAPISIDHASH". Since browser.json doesn't have it, + # we need to add a dummy one to pass detection. ytmusicapi will regenerate it properly. + if detected_type == AuthType.OAUTH_CUSTOM_CLIENT: + parsed_headers["authorization"] = "SAPISIDHASH dummy_for_detection" + detected_type = determine_auth_type(parsed_headers) + auth_dict = dict(parsed_headers) + auth_string = json.dumps(auth_dict) + + # Initialize with the properly formatted auth string + yt = YTMusic(auth=auth_string, user=BRAND_ACCOUNT_ID if BRAND_ACCOUNT_ID else None) + except json.JSONDecodeError as e: + print(f"Error parsing {BROWSER_AUTH_FILE}: {e}") + print("Please check that your browser.json file is valid JSON.") + sys.exit(1) + except Exception as e: + print(f"Error initializing YTMusic: {e}") + print("Please check that your browser.json file is correctly formatted.") + import traceback + traceback.print_exc() + sys.exit(1) + + # Create playlist + print(f"\nCreating playlist: {PLAYLIST_NAME}...") + try: + playlist_id = yt.create_playlist(PLAYLIST_NAME, PLAYLIST_DESCRIPTION) + print(f"✓ Playlist created successfully! ID: {playlist_id}") + except Exception as e: + print(f"Error creating playlist: {e}") + sys.exit(1) + + # Track added songs to avoid duplicates + added_video_ids = set() + total_added = 0 + failed_bands = [] + + # Process each band + print(f"\nSearching for songs from {len(BANDS)} bands...") + print("-" * 60) + + for i, band in enumerate(BANDS, 1): + print(f"\n[{i}/{len(BANDS)}] Searching for: {band}") + + try: + # First, search for the artist to find the correct band + artist_results = yt.search(band, filter="artists", limit=1) + songs_to_add = [] + song_info_list = [] # Store song info for display + + if not artist_results: + # Fallback: try searching for songs if artist search fails + print(f" ⚠ No artist found for {band}, trying song search...") + search_results = yt.search(band, filter="songs", limit=SONGS_PER_BAND * 2) + + if not search_results: + print(f" ⚠ No results found for {band}") + failed_bands.append(band) + continue + + # Filter to get top songs and avoid duplicates + for result in search_results: + video_id = result.get("videoId") + if video_id and video_id not in added_video_ids: + songs_to_add.append(video_id) + song_info_list.append(result) # Store for display + added_video_ids.add(video_id) + if len(songs_to_add) >= SONGS_PER_BAND: + break + else: + # Found artist, get their songs + artist_browse_id = artist_results[0].get("browseId") + if not artist_browse_id: + print(f" ⚠ No browseId found for artist {band}") + failed_bands.append(band) + continue + + # Get artist information which includes songs + try: + artist_info = yt.get_artist(artist_browse_id) + # Get songs from the artist + artist_songs = artist_info.get("songs", {}).get("results", []) + + if not artist_songs: + print(f" ⚠ No songs found for artist {band}") + failed_bands.append(band) + continue + + # Filter to get top songs and avoid duplicates + for song in artist_songs: + video_id = song.get("videoId") + if video_id and video_id not in added_video_ids: + songs_to_add.append(video_id) + song_info_list.append(song) # Store for display + added_video_ids.add(video_id) + if len(songs_to_add) >= SONGS_PER_BAND: + break + except Exception as e: + print(f" ⚠ Error getting artist info: {e}, falling back to song search...") + # Fallback to song search + search_results = yt.search(band, filter="songs", limit=SONGS_PER_BAND * 2) + if not search_results: + print(f" ⚠ No results found for {band}") + failed_bands.append(band) + continue + + for result in search_results: + video_id = result.get("videoId") + if video_id and video_id not in added_video_ids: + songs_to_add.append(video_id) + song_info_list.append(result) # Store for display + added_video_ids.add(video_id) + if len(songs_to_add) >= SONGS_PER_BAND: + break + + if not songs_to_add: + print(f" ⚠ No new songs to add for {band} (may be duplicates)") + continue + + # Add songs to playlist with retry logic for HTTP 409 conflicts + max_retries = 3 + retry_delay = 5 # seconds + added_successfully = False + + for attempt in range(1, max_retries + 1): + try: + yt.add_playlist_items(playlist_id, songs_to_add) + print(f" ✓ Added {len(songs_to_add)} song(s):") + for song_info in song_info_list[:len(songs_to_add)]: + title = song_info.get("title", "Unknown") + # Handle both artist list format and single artist format + artists = song_info.get("artists", []) + if isinstance(artists, list) and len(artists) > 0: + artist = artists[0].get("name", "Unknown") if isinstance(artists[0], dict) else str(artists[0]) + else: + artist = song_info.get("artist", "Unknown") + print(f" - {title} by {artist}") + total_added += len(songs_to_add) + added_successfully = True + break + except Exception as e: + error_msg = str(e) + # Check if it's an HTTP 409 Conflict error + if "409" in error_msg or "Conflict" in error_msg: + if attempt < max_retries: + print(f" ⚠ HTTP 409 Conflict on attempt {attempt}/{max_retries}. Retrying in {retry_delay} seconds...") + time.sleep(retry_delay) + else: + print(f" ✗ Error adding songs after {max_retries} attempts: {e}") + failed_bands.append(band) + else: + # For other errors, don't retry + print(f" ✗ Error adding songs: {e}") + failed_bands.append(band) + break + + except Exception as e: + print(f" ✗ Error searching for {band}: {e}") + failed_bands.append(band) + + # Summary + print("\n" + "=" * 60) + print("SUMMARY") + print("=" * 60) + print(f"Playlist: {PLAYLIST_NAME}") + print(f"Total songs added: {total_added}") + print(f"Bands processed: {len(BANDS) - len(failed_bands)}/{len(BANDS)}") + + if failed_bands: + print(f"\nBands that failed or had no results ({len(failed_bands)}):") + for band in failed_bands: + print(f" - {band}") + + print(f"\n✓ Done! Check your YouTube Music library for the playlist.") + + +if __name__ == "__main__": + main() + diff --git a/create_playlist_summer_breeze.py b/create_playlist_summer_breeze.py new file mode 100644 index 0000000..7b5fd32 --- /dev/null +++ b/create_playlist_summer_breeze.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +""" +Create YouTube Music playlist "Summer Breeze 2026" with top 3 songs from each festival band. +Bands from https://www.summer-breeze.de/de/bands/ +""" + +import os +import sys +import json +import time +from ytmusicapi import YTMusic +from ytmusicapi.auth.browser import setup_browser + +# List of all festival bands from https://www.summer-breeze.de/de/bands/ +BANDS = [ + "Helloween", + "In Flames", + "Arch Enemy", + "Eisbrecher", + "Saxon", + "Lamb Of God", + "Airbourne", + "Alestorm", + "Versengold", + "The Ghost Inside", + "Thy Art Is Murder", + "Testament", + "Amorphis", + "Imminence", + "Paleface Swiss", + "Alcest", + "The Butcher Sisters", + "Orbit Culture", + "Hatebreed", + "Skindred", + "Kim Dracula", + "Soulfly", + "Paradise Lost", + "Kadavar", + "Deicide", + "Brothers Of Metal", + "Fit For An Autopsy", + "Northlane", + "dARTAGNAN", + "Betontod", + "Terror", + "Deafheaven", + "Decapitated", + "Future Palace", + "BLACKBRAID", + "Der Weg Einer Freiheit", + "Miracle Of Sound", + "Soen", + "Alien Ant Farm", + "Mushroomhead", + "Municipal Waste", + "EIVØR", + "Das Lumpenpack", + "Wolves In The Throne Room", + "Trollfest", + "Thundermother", + "Saor", + "Heavysaurus", + "Nanowar of Steel", + "From Fall To Spring", + "Unprocessed", + "THE SONS OF HUENS", + "SANGUISUGABOGG", + "SPEED", + "Deserted Fear", + "Misery Index", + "Bloodred Hourglass", + "Cryptopsy", + "Brainstorm", + "Parasite Inc.", + "Excrementory Grindfuckers", + "Grand Magus", + "MASSIVE WAGONS", + "Our Promise", + "Green Lung", + "The Narrator", + "FULCI", + "Illdisposed", + "Setyøursails", + "Ten56.", + "Møl", + "200 Stab Wounds", + "Groza", + "Mittel Alta", + "Slomosa", + "Nytt Land", + "Soulbound", + "Urne", + "Manntra", + "Haggefugg", + "NECKBREAKKER", + "KING NUGGET GANG", + "Skeleton Pit", + "CASTLE RAT", + "Wucan", + "Blood Command", + "Cabal", + "Filth", + "Erdling", + "Brymir", + "Rectal Smegma", + "Stam1na", + "Zerre", + "CÂN BARDD", + "BROKEN BY THE SCREAM", + "BIZARREKULT", + "INNER SPACE", + "FIREBORN", + "Luna Kills", + "PRIDIAN", + "INHUMAN NATURE", + "802", + "PERSECUTOR", + "Blasmusik Illenschwang", +] + +PLAYLIST_NAME = "Summer Breeze 2026" +PLAYLIST_DESCRIPTION = "Festival playlist featuring top songs from Summer Breeze 2026 bands" +SONGS_PER_BAND = 3 +BROWSER_AUTH_FILE = "browser.json" +BRAND_ACCOUNT_ID = "107494778873257953135" # cuidas brand account + + +def main(): + if not os.path.exists(BROWSER_AUTH_FILE): + print(f"Error: {BROWSER_AUTH_FILE} not found!") + print(f"Please follow the instructions in SETUP.md to create {BROWSER_AUTH_FILE}") + sys.exit(1) + + account_info = f" (brand account: {BRAND_ACCOUNT_ID})" if BRAND_ACCOUNT_ID else "" + print(f"Initializing YouTube Music API with {BROWSER_AUTH_FILE}{account_info}...") + try: + with open(BROWSER_AUTH_FILE, 'r') as f: + browser_data = json.load(f) + + required_keys = ['Cookie', 'User-Agent'] + missing_keys = [k for k in required_keys if k not in browser_data] + if missing_keys: + print(f"Error: Missing required keys in {BROWSER_AUTH_FILE}: {missing_keys}") + sys.exit(1) + + headers_string = "\n".join([f"{k}: {v}" for k, v in browser_data.items()]) + auth_string = setup_browser(headers_raw=headers_string, filepath=None) + + from ytmusicapi.auth.auth_parse import determine_auth_type, parse_auth_str + from ytmusicapi.auth.types import AuthType + + parsed_headers, _ = parse_auth_str(auth_string) + detected_type = determine_auth_type(parsed_headers) + + if detected_type == AuthType.OAUTH_CUSTOM_CLIENT: + parsed_headers["authorization"] = "SAPISIDHASH dummy_for_detection" + detected_type = determine_auth_type(parsed_headers) + auth_dict = dict(parsed_headers) + auth_string = json.dumps(auth_dict) + + yt = YTMusic(auth=auth_string, user=BRAND_ACCOUNT_ID if BRAND_ACCOUNT_ID else None) + except json.JSONDecodeError as e: + print(f"Error parsing {BROWSER_AUTH_FILE}: {e}") + sys.exit(1) + except Exception as e: + print(f"Error initializing YTMusic: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + print(f"\nCreating playlist: {PLAYLIST_NAME}...") + try: + playlist_id = yt.create_playlist(PLAYLIST_NAME, PLAYLIST_DESCRIPTION) + print(f"✓ Playlist created successfully! ID: {playlist_id}") + except Exception as e: + print(f"Error creating playlist: {e}") + sys.exit(1) + + added_video_ids = set() + total_added = 0 + failed_bands = [] + + print(f"\nSearching for songs from {len(BANDS)} bands...") + print("-" * 60) + + for i, band in enumerate(BANDS, 1): + print(f"\n[{i}/{len(BANDS)}] Searching for: {band}") + + try: + artist_results = yt.search(band, filter="artists", limit=1) + songs_to_add = [] + song_info_list = [] + + if not artist_results: + print(f" ⚠ No artist found for {band}, trying song search...") + search_results = yt.search(band, filter="songs", limit=SONGS_PER_BAND * 2) + + if not search_results: + print(f" ⚠ No results found for {band}") + failed_bands.append(band) + continue + + for result in search_results: + video_id = result.get("videoId") + if video_id and video_id not in added_video_ids: + songs_to_add.append(video_id) + song_info_list.append(result) + added_video_ids.add(video_id) + if len(songs_to_add) >= SONGS_PER_BAND: + break + else: + artist_browse_id = artist_results[0].get("browseId") + if not artist_browse_id: + print(f" ⚠ No browseId found for artist {band}") + failed_bands.append(band) + continue + + try: + artist_info = yt.get_artist(artist_browse_id) + artist_songs = artist_info.get("songs", {}).get("results", []) + + if not artist_songs: + print(f" ⚠ No songs found for artist {band}") + failed_bands.append(band) + continue + + for song in artist_songs: + video_id = song.get("videoId") + if video_id and video_id not in added_video_ids: + songs_to_add.append(video_id) + song_info_list.append(song) + added_video_ids.add(video_id) + if len(songs_to_add) >= SONGS_PER_BAND: + break + except Exception as e: + print(f" ⚠ Error getting artist info: {e}, falling back to song search...") + search_results = yt.search(band, filter="songs", limit=SONGS_PER_BAND * 2) + if not search_results: + print(f" ⚠ No results found for {band}") + failed_bands.append(band) + continue + + for result in search_results: + video_id = result.get("videoId") + if video_id and video_id not in added_video_ids: + songs_to_add.append(video_id) + song_info_list.append(result) + added_video_ids.add(video_id) + if len(songs_to_add) >= SONGS_PER_BAND: + break + + if not songs_to_add: + print(f" ⚠ No new songs to add for {band} (may be duplicates)") + continue + + max_retries = 3 + retry_delay = 5 + + for attempt in range(1, max_retries + 1): + try: + yt.add_playlist_items(playlist_id, songs_to_add) + print(f" ✓ Added {len(songs_to_add)} song(s):") + for song_info in song_info_list[:len(songs_to_add)]: + title = song_info.get("title", "Unknown") + artists = song_info.get("artists", []) + if isinstance(artists, list) and len(artists) > 0: + artist = artists[0].get("name", "Unknown") if isinstance(artists[0], dict) else str(artists[0]) + else: + artist = song_info.get("artist", "Unknown") + print(f" - {title} by {artist}") + total_added += len(songs_to_add) + break + except Exception as e: + error_msg = str(e) + if "409" in error_msg or "Conflict" in error_msg: + if attempt < max_retries: + print(f" ⚠ HTTP 409 Conflict on attempt {attempt}/{max_retries}. Retrying in {retry_delay} seconds...") + time.sleep(retry_delay) + else: + print(f" ✗ Error adding songs after {max_retries} attempts: {e}") + failed_bands.append(band) + else: + print(f" ✗ Error adding songs: {e}") + failed_bands.append(band) + break + + except Exception as e: + print(f" ✗ Error searching for {band}: {e}") + failed_bands.append(band) + + print("\n" + "=" * 60) + print("SUMMARY") + print("=" * 60) + print(f"Playlist: {PLAYLIST_NAME}") + print(f"Total songs added: {total_added}") + print(f"Bands processed: {len(BANDS) - len(failed_bands)}/{len(BANDS)}") + + if failed_bands: + print(f"\nBands that failed or had no results ({len(failed_bands)}):") + for band in failed_bands: + print(f" - {band}") + + print(f"\n✓ Done! Check your YouTube Music library for the playlist.") + + +if __name__ == "__main__": + main() diff --git a/fetch_all_playlists.py b/fetch_all_playlists.py new file mode 100644 index 0000000..5dad65b --- /dev/null +++ b/fetch_all_playlists.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +Fetch all playlists from YouTube Music brand account and save each playlist's +information to a separate JSON file. +""" + +import os +import sys +import json +import time +from ytmusicapi import YTMusic +from ytmusicapi.auth.browser import setup_browser + +BROWSER_AUTH_FILE = "browser.json" +BRAND_ACCOUNT_ID = "107494778873257953135" # cuidas brand account +OUTPUT_DIR = "playlists" + + +def main(): + # Check if browser.json exists + if not os.path.exists(BROWSER_AUTH_FILE): + print(f"Error: {BROWSER_AUTH_FILE} not found!") + print(f"Please follow the instructions in SETUP.md to create {BROWSER_AUTH_FILE}") + sys.exit(1) + + # Initialize YTMusic with browser authentication + account_info = f" (brand account: {BRAND_ACCOUNT_ID})" + print(f"Initializing YouTube Music API with {BROWSER_AUTH_FILE}{account_info}...") + try: + # Load browser.json as dict + with open(BROWSER_AUTH_FILE, 'r') as f: + browser_data = json.load(f) + + # Ensure we have required keys for browser auth + required_keys = ['Cookie', 'User-Agent'] + missing_keys = [k for k in required_keys if k not in browser_data] + if missing_keys: + print(f"Error: Missing required keys in {BROWSER_AUTH_FILE}: {missing_keys}") + sys.exit(1) + + # Convert dict to headers string format + headers_string = "\n".join([f"{k}: {v}" for k, v in browser_data.items()]) + + # Use setup_browser to properly format and validate browser headers + auth_string = setup_browser(headers_raw=headers_string, filepath=None) + + # Parse the result to check auth type detection + from ytmusicapi.auth.auth_parse import determine_auth_type, parse_auth_str + from ytmusicapi.auth.types import AuthType + + parsed_headers, _ = parse_auth_str(auth_string) + detected_type = determine_auth_type(parsed_headers) + + # Workaround: determine_auth_type() only detects browser auth if there's an + # "authorization" header with "SAPISIDHASH". Since browser.json doesn't have it, + # we need to add a dummy one to pass detection. ytmusicapi will regenerate it properly. + if detected_type == AuthType.OAUTH_CUSTOM_CLIENT: + parsed_headers["authorization"] = "SAPISIDHASH dummy_for_detection" + detected_type = determine_auth_type(parsed_headers) + auth_dict = dict(parsed_headers) + auth_string = json.dumps(auth_dict) + + # Initialize with the properly formatted auth string + yt = YTMusic(auth=auth_string, user=BRAND_ACCOUNT_ID if BRAND_ACCOUNT_ID else None) + except json.JSONDecodeError as e: + print(f"Error parsing {BROWSER_AUTH_FILE}: {e}") + print("Please check that your browser.json file is valid JSON.") + sys.exit(1) + except Exception as e: + print(f"Error initializing YTMusic: {e}") + print("Please check that your browser.json file is correctly formatted.") + import traceback + traceback.print_exc() + sys.exit(1) + + # Create output directory + if not os.path.exists(OUTPUT_DIR): + os.makedirs(OUTPUT_DIR) + print(f"Created output directory: {OUTPUT_DIR}") + + # Fetch all playlists + print(f"\nFetching all playlists from brand account...") + print("-" * 60) + + try: + # Get all playlists (limit=None should get all) + playlists = yt.get_library_playlists(limit=None) + print(f"Found {len(playlists)} playlist(s)") + + if not playlists: + print("No playlists found!") + return + + # Process each playlist + for i, playlist in enumerate(playlists, 1): + playlist_id = playlist.get('playlistId') + playlist_title = playlist.get('title', 'Unknown') + + print(f"\n[{i}/{len(playlists)}] Processing: {playlist_title} (ID: {playlist_id})") + + try: + # Get full playlist details + playlist_details = yt.get_playlist(playlist_id, limit=None) + + if not playlist_details: + print(f" ⚠ Warning: get_playlist returned None for {playlist_id}") + # Save what we have from library_info + playlist_info = { + "library_info": playlist, + "full_details": None, + "error": "get_playlist returned None" + } + else: + # Combine library playlist info with full details + playlist_info = { + "library_info": playlist, # Info from get_library_playlists + "full_details": playlist_details # Full details from get_playlist + } + + # Create safe filename from playlist title + safe_filename = "".join(c for c in playlist_title if c.isalnum() or c in (' ', '-', '_')).rstrip() + safe_filename = safe_filename.replace(' ', '_') + if not safe_filename: + safe_filename = f"playlist_{playlist_id}" + + # Add playlist ID to filename to ensure uniqueness + filename = f"{safe_filename}_{playlist_id}.json" + filepath = os.path.join(OUTPUT_DIR, filename) + + # Save to file + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(playlist_info, f, indent=2, ensure_ascii=False) + + # Print summary + if playlist_details: + tracks_count = playlist_details.get('trackCount', 0) + print(f" ✓ Saved to: {filepath}") + print(f" Tracks: {tracks_count}") + print(f" Title: {playlist_details.get('title', 'Unknown')}") + description = playlist_details.get('description', 'N/A') + if description: + print(f" Description: {description[:50]}...") + else: + print(f" Description: N/A") + else: + print(f" ⚠ Saved (library info only) to: {filepath}") + print(f" Title: {playlist_title}") + + except Exception as e: + print(f" ✗ Error processing playlist {playlist_title}: {e}") + import traceback + traceback.print_exc() + continue + + # Summary + print("\n" + "=" * 60) + print("SUMMARY") + print("=" * 60) + print(f"Total playlists processed: {len(playlists)}") + print(f"Output directory: {OUTPUT_DIR}") + print(f"✓ Done! All playlist information saved to individual files.") + + except Exception as e: + print(f"Error fetching playlists: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() +