2e84351348
- Retry up to 3 times with 5 second delay on HTTP 409 errors - Only retry on Conflict errors, other errors fail immediately - Provides clear feedback on retry attempts
224 lines
8.7 KiB
Python
224 lines
8.7 KiB
Python
#!/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
|
|
BANDS = [
|
|
# CLUB STAGE
|
|
"DETARTRATED",
|
|
"BAD ASSUMPTION",
|
|
"SEVEN BLOOD",
|
|
"VICIOUS RAIN",
|
|
"DAGGER THREAT",
|
|
"KANINE",
|
|
"GLOOM IN THE CORNER",
|
|
"MENTAL CRUELTY",
|
|
"WITHIN DESTRUCTION",
|
|
# MAIN STAGE
|
|
"WATCH ME RISE",
|
|
"ATENA",
|
|
"DIAMOND CONSTRUCT",
|
|
"STAIN THE CANVAS",
|
|
"AS EVERYTHING UNFOLDS",
|
|
"AVIANA",
|
|
"FUTURE PALACE",
|
|
"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()
|
|
|