diff --git a/apps/musicseerr/tasks.py b/apps/musicseerr/tasks.py index 6b10b16..3771715 100644 --- a/apps/musicseerr/tasks.py +++ b/apps/musicseerr/tasks.py @@ -227,6 +227,40 @@ def send_telegram_notification(chat_id: int, text: str): logger.error(f"Failed to send Telegram notification: {e}") +def extract_spotify_track_title(url: str, proxy_url: str) -> str: + """ + Fetches the Spotify track page and extracts the track name and artists from the title tag. + """ + proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None + headers = { + "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" + } + logger.info(f"Extracting track title from Spotify page: {url}") + try: + response = requests.get(url, headers=headers, proxies=proxies, timeout=15) + if response.status_code == 200: + soup = BeautifulSoup(response.text, "html.parser") + title_tag = soup.find("title") + if title_tag: + title_text = title_tag.text + # Format is typically "Song Name - song and lyrics by Artist1, Artist2 | Spotify" + # Or "Song Name - song by Artist | Spotify" + if " - song " in title_text: + parts = title_text.split(" - song ", 1) + song_name = parts[0].strip() + artist_part = parts[1].split(" by ", 1) + if len(artist_part) > 1: + artists = artist_part[1].split(" | Spotify", 1)[0].strip() + return f"{artists} - {song_name}" + return song_name + elif " | Spotify" in title_text: + return title_text.split(" | Spotify")[0].strip() + return title_text.strip() + except Exception as e: + logger.warning(f"Failed to extract Spotify track title: {e}") + return "" + + def symlink_existing_tracks(temp_dir: str): """ Finds all audio files recursively in /remote-music/music, extracts artist and title, @@ -311,6 +345,15 @@ def run_download_task(task_id: str, query: str, proxy_url: str, jobs_dict: dict, logger.error(f"Failed to resolve proxy hostname: {e}") try: + download_successful = False + + # If query is a Spotify track URL, resolve it to a text title first for fallback options + resolved_title = "" + if "spotify.com/track/" in query.lower(): + resolved_title = extract_spotify_track_title(query, resolved_proxy_url) + if resolved_title: + logger.info(f"[{task_id}] Pre-resolved Spotify URL to text title: '{resolved_title}'") + is_tubidy = "tubidy" in query.lower() is_spotify = "spotify.com" in query.lower() is_youtube = "youtube.com" in query.lower() or "youtu.be" in query.lower() @@ -333,6 +376,7 @@ def run_download_task(task_id: str, query: str, proxy_url: str, jobs_dict: dict, result = subprocess.run(cmd, cwd=temp_dir, env=env, capture_output=True, text=True) if result.returncode == 0: logger.info(f"[{task_id}] spotDL download succeeded.") + download_successful = True else: logger.warning(f"[{task_id}] spotDL failed: {result.stderr or result.stdout}. Falling back to yt-dlp.") # Clear state for P2 fallback @@ -348,9 +392,12 @@ def run_download_task(task_id: str, query: str, proxy_url: str, jobs_dict: dict, env["http_proxy"] = resolved_proxy_url env["https_proxy"] = resolved_proxy_url - # If not a link, treat as search query + # If not a link or if resolved from Spotify track URL, treat as search query search_query = query - if not query.startswith("http"): + if resolved_title: + search_query = f"ytsearch1:{resolved_title}" + logger.info(f"[{task_id}] Using resolved Spotify track title for yt-dlp search: '{search_query}'") + elif not query.startswith("http"): search_query = f"ytsearch1:{query}" cmd = [ @@ -368,6 +415,7 @@ def run_download_task(task_id: str, query: str, proxy_url: str, jobs_dict: dict, result = subprocess.run(cmd, cwd=temp_dir, env=env, capture_output=True, text=True) if result.returncode == 0: logger.info(f"[{task_id}] yt-dlp download succeeded.") + download_successful = True else: logger.warning(f"[{task_id}] yt-dlp failed: {result.stderr or result.stdout}. Falling back to Tubidy Search.") @@ -393,18 +441,23 @@ def run_download_task(task_id: str, query: str, proxy_url: str, jobs_dict: dict, # Download file stream download_file_stream(mp3_url, dest_path, resolved_proxy_url) logger.info(f"[{task_id}] Tubidy download completed.") + download_successful = True # Tag metadata tag_mp3(dest_path, filename) logger.info(f"[{task_id}] Tubidy ID3 tagging complete.") # Priority 4: Tubidy Search Fallback (if spotDL and yt-dlp failed and no files are present) - downloaded_files = os.listdir(temp_dir) - if not downloaded_files and not is_tubidy: + if not download_successful and not is_tubidy: jobs_dict[task_id]["priority_used"] = "Tubidy-Search" logger.info(f"[{task_id}] Attempting Fallback Priority 4: Tubidy Search Scraper for '{query}'") - search_results = search_tubidy(query, resolved_proxy_url) + search_query = query + if resolved_title: + search_query = resolved_title + logger.info(f"[{task_id}] Using pre-resolved Spotify track title for Tubidy search: '{search_query}'") + + search_results = search_tubidy(search_query, resolved_proxy_url) if search_results: first_result = search_results[0] logger.info(f"[{task_id}] Found Tubidy match: '{first_result['title']}' (ID: {first_result['id']})") @@ -430,14 +483,18 @@ def run_download_task(task_id: str, query: str, proxy_url: str, jobs_dict: dict, # Tag metadata tag_mp3(dest_path, filename) logger.info(f"[{task_id}] Tubidy fallback ID3 tagging complete.") + download_successful = True except Exception as e: logger.error(f"[{task_id}] Tubidy fallback download failed: {e}") raise Exception(f"All download options (spotDL, yt-dlp, Tubidy) failed. Last error: {e}") else: - logger.error(f"[{task_id}] No Tubidy search results found for query '{query}'") + logger.error(f"[{task_id}] No Tubidy search results found for query '{search_query}'") raise Exception("All download options (spotDL, yt-dlp, Tubidy) failed. No Tubidy search results found.") # Verify files and move to /remote-music/music + if not download_successful: + raise Exception("Ingestion finished but no download was successful.") + downloaded_files = os.listdir(temp_dir) actual_files = [f for f in downloaded_files if not os.path.islink(os.path.join(temp_dir, f))] @@ -450,7 +507,7 @@ def run_download_task(task_id: str, query: str, proxy_url: str, jobs_dict: dict, send_telegram_notification(chat_id, f"✅ Successfully ingested request: '{query}'\n\n(All tracks already existed - skipped duplicates)") return else: - raise Exception("Ingestion finished but no files were found in local temp folder.") + raise Exception("Ingestion finished successfully but no files were found in local temp folder.") os.makedirs("/remote-music/music", exist_ok=True) moved_files = []