""" Monochrome API integration for Votify Web. Orchestrates the Spotify URL → Tidal ID → download pipeline for use from app.py background threads. """ import os import time from monochrome import discover_instances from monochrome.spotify_to_ids import ( parse_spotify_url, fetch_spotify_embed, extract_collection_name, extract_tracks, search_monochrome, find_best_match, ) from monochrome.download import ( get_stream_url_tidal, get_stream_url_qobuz, download_file, fetch_cover_art, embed_metadata, sanitize_filename, convert_to_mp3, ) def download_spotify_url(spotify_url, quality, output_dir, log=None, cancel_check=None): """Download tracks from a Spotify URL via Monochrome. Args: spotify_url: Spotify track/album/playlist URL quality: One of HI_RES_LOSSLESS, LOSSLESS, HIGH, LOW, MP3_320 output_dir: Directory to save downloaded files log: Callback (str) -> None for progress messages cancel_check: Callback () -> bool, returns True if cancelled Returns: (success_count, total_tracks) """ if log is None: log = print if cancel_check is None: cancel_check = lambda: False want_mp3 = quality == "MP3_320" api_quality = "LOSSLESS" if want_mp3 else quality # Step 1: Discover instances log("[monochrome] Discovering API instances...") instances = discover_instances(log=log) # Step 2: Parse Spotify URL sp_type, sp_id = parse_spotify_url(spotify_url) if not sp_type: log(f"[monochrome] Invalid Spotify URL: {spotify_url}") return 0, 0 # Step 3: Fetch track list from Spotify log(f"[monochrome] Fetching Spotify {sp_type}: {sp_id}") embed_data = fetch_spotify_embed(sp_type, sp_id) tracks = extract_tracks(embed_data, sp_type, sp_id) if not tracks: log(f"[monochrome] Could not extract tracks from {spotify_url}") return 0, 0 total = len(tracks) log(f"[monochrome] Found {total} track(s) on Spotify") # Create subfolder for albums/playlists dl_dir = output_dir if total > 1: collection_name = extract_collection_name(embed_data, sp_type) if collection_name: folder_name = sanitize_filename(collection_name) else: folder_name = sanitize_filename(f"{sp_type}_{sp_id}") dl_dir = os.path.join(output_dir, folder_name) os.makedirs(dl_dir, exist_ok=True) log(f"[monochrome] Saving to folder: {folder_name}") success = 0 failed_tracks = [] for i, track in enumerate(tracks): if cancel_check(): log("[monochrome] Cancelled") break query = f"{track['artist']} {track['title']}".strip() log(f"[monochrome] Track {i + 1}/{total}: {query}") # Search and match results = search_monochrome(instances, query, log=log) match, score = find_best_match(results, track["title"], track["artist"]) if not match: log(f"[monochrome] No match found for: {query}") failed_tracks.append(query) if i < total - 1: time.sleep(0.5) continue track_id = match.get("id") m_title = match.get("title", "?") m_artist_obj = match.get("artist", {}) m_artist = m_artist_obj.get("name", "?") if isinstance(m_artist_obj, dict) else str(m_artist_obj) log(f"[monochrome] Matched: {m_artist} - {m_title} (score: {score:.2f})") # Get stream URL stream_url, track_data = get_stream_url_tidal(instances, track_id, api_quality, log=log) if not stream_url: log("[monochrome] Tidal failed, trying Qobuz...") stream_url = get_stream_url_qobuz(track_id, api_quality, log=log) if not stream_url: log(f"[monochrome] Failed to get stream for: {query}") failed_tracks.append(query) if i < total - 1: time.sleep(0.5) continue # Build metadata from match info info = match if track_data and isinstance(track_data, dict): # Merge: track_data may have more detail for k, v in track_data.items(): if k not in info or not info[k]: info[k] = v # Determine file extension and path if want_mp3: ext = ".flac" elif api_quality in ("HIGH", "LOW"): ext = ".m4a" else: ext = ".flac" filename = sanitize_filename(f"{m_artist} - {m_title}{ext}") file_path = os.path.join(dl_dir, filename) # Download try: download_file(stream_url, file_path, log=log) except Exception as e: log(f"[monochrome] Download failed for {query}: {e}") failed_tracks.append(query) if i < total - 1: time.sleep(0.5) continue # Cover art and metadata cover_data = fetch_cover_art(info.get("album"), log=log) embed_metadata(file_path, info, cover_data, log=log) # Convert to MP3 if requested if want_mp3: mp3_filename = sanitize_filename(f"{m_artist} - {m_title}.mp3") mp3_path = os.path.join(dl_dir, mp3_filename) if convert_to_mp3(file_path, mp3_path, log=log): embed_metadata(mp3_path, info, cover_data, log=log) success += 1 # Rate limit between tracks if i < total - 1: time.sleep(0.5) # Summary if failed_tracks: log(f"[monochrome] Failed tracks ({len(failed_tracks)}):") for ft in failed_tracks: log(f"[monochrome] - {ft}") log(f"[monochrome] Complete: {success}/{total} tracks downloaded") return success, total