From 608689732546c980c03dd6505fc09874b8df2428 Mon Sep 17 00:00:00 2001 From: Frank Schwenk Date: Tue, 18 Nov 2025 15:35:52 +0100 Subject: [PATCH] Initial commit: YouTube Music playlist creator for Core Fest 2025 - Add playlist creation script with browser authentication - Support for 17 festival bands with top 3 songs each - Includes setup documentation and requirements - Fixes OAuth detection issue by adding dummy authorization header --- .gitignore | 24 ++++++ README.md | 29 +++++++ SETUP.md | 102 +++++++++++++++++++++++ create_playlist.py | 199 +++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 5 files changed, 356 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 SETUP.md create mode 100644 create_playlist.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bd4995 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Browser authentication file (contains sensitive cookies) +browser.json + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + diff --git a/README.md b/README.md new file mode 100644 index 0000000..bcedbed --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Core Fest 2025 YouTube Music Playlist Creator + +Automatically creates a YouTube Music playlist "Core Fest 2025" with the top 3 songs from each festival band. + +## Quick Start + +1. **Set up browser authentication** (see [SETUP.md](SETUP.md)) +2. **Install dependencies:** + ```bash + pip install -r requirements.txt + ``` +3. **Run the script:** + ```bash + python create_playlist.py + ``` + +## What It Does + +- Creates a playlist named "Core Fest 2025" +- Searches for each of the 17 festival bands +- Adds the top 3 songs from each band to the playlist +- Handles errors gracefully and logs progress + +## Bands Included + +**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 + diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..32c9516 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,102 @@ +# Browser Authentication Setup + +This guide will help you extract the necessary authentication cookies from your browser to use with `ytmusicapi`. + +## Step 1: Open YouTube Music + +1. Open your browser (Chrome or Firefox) +2. Navigate to [https://music.youtube.com](https://music.youtube.com) +3. **Make sure you are logged in** to your Google account + +## Step 2: Open Developer Tools + +- **Chrome/Edge:** Press `F12` or `Ctrl+Shift+I` (Windows/Linux) / `Cmd+Option+I` (Mac) +- **Firefox:** Press `F12` or `Ctrl+Shift+I` (Windows/Linux) / `Cmd+Option+I` (Mac) + +## Step 3: Go to Network Tab + +1. Click on the **"Network"** tab in Developer Tools +2. If you don't see it, look for it in the tabs at the top of the developer tools panel + +## Step 4: Capture Network Requests + +1. **Refresh the page** (press `F5` or `Ctrl+R` / `Cmd+R`) +2. Wait for the page to load completely +3. Look for requests to `music.youtube.com` in the network list + +## Step 5: Find the Right Request + +1. Click on any request that goes to `music.youtube.com` +2. Look for requests like: + - `browse` + - `getBrowse` + - `search` + - Or any request that shows `music.youtube.com` in the URL + +## Step 6: Copy Headers + +1. With a request selected, click on the **"Headers"** tab +2. Scroll down to **"Request Headers"** section +3. You need to copy the following headers: + +### Required Headers: + +- **Cookie** - A long string starting with something like `VISITOR_INFO1_LIVE=...` +- **User-Agent** - Something like `Mozilla/5.0 (Windows NT 10.0; Win64; x64)...` + +### Optional but Recommended: + +- **X-Goog-AuthUser** - Usually `0` or `1` +- **Accept** - Usually `*/*` or `application/json` +- **Accept-Language** - Your language preference +- **Content-Type** - Usually `application/json` +- **x-origin** - Usually `https://music.youtube.com` + +## Step 7: Create browser.json + +Create a file named `browser.json` in this directory with the following structure: + +```json +{ + "User-Agent": "PASTE_YOUR_USER_AGENT_HERE", + "Accept": "*/*", + "Accept-Language": "en-US,en;q=0.9", + "Content-Type": "application/json", + "X-Goog-AuthUser": "0", + "x-origin": "https://music.youtube.com", + "Cookie": "PASTE_YOUR_COOKIE_HERE" +} +``` + +Replace: +- `PASTE_YOUR_USER_AGENT_HERE` with the User-Agent header value you copied +- `PASTE_YOUR_COOKIE_HERE` with the Cookie header value you copied + +**Important:** The Cookie value is very long and contains sensitive information. Keep this file private and never commit it to version control. + +## Step 8: Verify + +Your `browser.json` should look something like this (with your actual values): + +```json +{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Accept": "*/*", + "Accept-Language": "en-US,en;q=0.9", + "Content-Type": "application/json", + "X-Goog-AuthUser": "0", + "x-origin": "https://music.youtube.com", + "Cookie": "VISITOR_INFO1_LIVE=abc123...; YSC=xyz789...; [many more cookie values]" +} +``` + +## Troubleshooting + +- **Can't find the Cookie header?** Make sure you're looking at a request to `music.youtube.com`, not `youtube.com` +- **Authentication fails?** Your cookies may have expired. Repeat the process to get fresh cookies +- **Still having issues?** Try using a different request from the network tab, or refresh the page and try again + +## Security Note + +The `browser.json` file contains your authentication cookies. Anyone with access to this file can access your YouTube Music account. Keep it secure and never share it publicly. + diff --git a/create_playlist.py b/create_playlist.py new file mode 100644 index 0000000..ed9b386 --- /dev/null +++ b/create_playlist.py @@ -0,0 +1,199 @@ +#!/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 +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" + + +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 + print(f"Initializing YouTube Music API with {BROWSER_AUTH_FILE}...") + 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 + yt = YTMusic(auth=auth_string) + 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 + 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) + except Exception as e: + print(f" ✗ Error adding songs: {e}") + failed_bands.append(band) + + 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/requirements.txt b/requirements.txt new file mode 100644 index 0000000..01296e6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +ytmusicapi>=1.0.0 +