#!/usr/bin/env python3 """ Create YouTube Music playlist "Core Fest 2025" 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 # List of all festival bands ordered by start time BANDS = [ # 12:00 - DETARTRATED (CLUB STAGE) "DETARTRATED", # 13:30 - BAD ASSUMPTION (CLUB STAGE) "BAD ASSUMPTION", # 14:00 - WATCH ME RISE (MAIN STAGE) "WATCH ME RISE", # 14:30 - SEVEN BLOOD (CLUB STAGE) "SEVEN BLOOD", # 15:00 - ATENA (MAIN STAGE) "ATENA", # 15:30 - VICIOUS RAIN (CLUB STAGE) "VICIOUS RAIN", # 16:00 - DIAMOND CONSTRUCT (MAIN STAGE) "DIAMOND CONSTRUCT", # 16:30 - DAGGER THREAT (CLUB STAGE) "DAGGER THREAT", # 17:00 - STAIN THE CANVAS (MAIN STAGE) "STAIN THE CANVAS", # 17:30 - KANINE (CLUB STAGE) "KANINE", # 18:10 - AS EVERYTHING UNFOLDS (MAIN STAGE) "AS EVERYTHING UNFOLDS", # 18:30 - GLOOM IN THE CORNER (CLUB STAGE) "GLOOM IN THE CORNER", # 19:20 - AVIANA (MAIN STAGE) "AVIANA", # 19:40 - MENTAL CRUELTY (CLUB STAGE) "MENTAL CRUELTY", # 20:40 - FUTURE PALACE (MAIN STAGE) "FUTURE PALACE", # 21:00 - WITHIN DESTRUCTION (CLUB STAGE) "WITHIN DESTRUCTION", # 22:00 - DEAD BY APRIL (MAIN STAGE) "DEAD BY APRIL", ] PLAYLIST_NAME = "Core Fest 2025" PLAYLIST_DESCRIPTION = "Festival playlist featuring top songs from Core Fest 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) # Format: "Header-Name: value\nHeader-Name2: value2" 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: # Add a dummy authorization header with SAPISIDHASH to trigger browser detection # ytmusicapi will regenerate this properly when needed parsed_headers["authorization"] = "SAPISIDHASH dummy_for_detection" # Re-check auth type detected_type = determine_auth_type(parsed_headers) # Reconstruct auth_string with the authorization header auth_dict = dict(parsed_headers) auth_string = json.dumps(auth_dict) # Initialize with the properly formatted auth string # Pass brand account ID if specified (works with browser auth too) 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: error_msg = str(e) print(f"Error initializing YTMusic: {error_msg}") # If it's the OAuth detection error, provide specific guidance if "oauth" in error_msg.lower() and "oauth_credentials" in error_msg.lower(): print("\n⚠ OAuth detection error detected.") print("This may be a ytmusicapi version issue or file format issue.") print("\nTry one of these solutions:") print("1. Regenerate browser.json using: ytmusicapi browser") print("2. Update ytmusicapi: pip install --upgrade ytmusicapi") print("3. Ensure browser.json has exactly these keys:") print(" - User-Agent") print(" - Cookie") print(" - X-Goog-AuthUser") print(" - x-origin") print(" - Accept (optional)") print(" - Accept-Language (optional)") print(" - Content-Type (optional)") else: 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: # Search for songs by this band 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 songs_to_add = [] 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) 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 result in search_results[:len(songs_to_add)]: title = result.get("title", "Unknown") artist = result.get("artists", [{}])[0].get("name", "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()