173 lines
7.0 KiB
Python
173 lines
7.0 KiB
Python
#!/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()
|
|
|