harharhar
This commit is contained in:
@@ -5,6 +5,9 @@ browser.json
|
|||||||
client_secret_*.json
|
client_secret_*.json
|
||||||
oauth.json
|
oauth.json
|
||||||
|
|
||||||
|
# Playlist exports
|
||||||
|
playlists/
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
|
|
||||||
Reference in New Issue
Block a user