From 9de3affc32c9c5427923078c2d9afb803792f669 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Mon, 12 Oct 2026 11:19:19 +0300 Subject: [PATCH] fix(deps): replace spotdl with direct oembed and yt-dlp to fix fastapi dependency conflict --- app/providers/universal.py | 90 ++++++++++++++++++++++++-------------- requirements.txt | 7 ++- 2 files changed, 60 insertions(+), 37 deletions(-) diff --git a/app/providers/universal.py b/app/providers/universal.py index fd656b4..c0ccd83 100644 --- a/app/providers/universal.py +++ b/app/providers/universal.py @@ -1,8 +1,10 @@ import os +import re import asyncio -import subprocess from typing import List, Optional +import httpx import yt_dlp + from app.config import settings from app.providers.base import BaseProvider, TrackMetadata from app.core.tagger import apply_id3_tags @@ -25,40 +27,65 @@ class UniversalProvider(BaseProvider): def _resolve_sync(self, url: str) -> List[TrackMetadata]: lower = url.lower() - # Handle Spotify / Apple Music via SpotDL parser / metadata - if "spotify.com" in lower or "music.apple.com" in lower: - return self._resolve_spotdl(url) + # Handle Spotify via open oEmbed API & yt-dlp fallback + if "spotify.com" in lower: + tracks = self._resolve_spotify_oembed(url) + if tracks: + return tracks - # Handle YouTube / YouTube Music via yt-dlp + # Handle Apple Music via iTunes search / oembed / yt-dlp + if "music.apple.com" in lower: + tracks = self._resolve_apple_music(url) + if tracks: + return tracks + + # Default: Handle YouTube / YouTube Music and other generic links via yt-dlp return self._resolve_ytdlp(url) - def _resolve_spotdl(self, url: str) -> List[TrackMetadata]: - # SpotDL resolves metadata seamlessly - tracks: List[TrackMetadata] = [] + def _resolve_spotify_oembed(self, url: str) -> List[TrackMetadata]: try: - from spotdl.types.song import Song - from spotdl.utils.search import parse_query + oembed_url = f"https://open.spotify.com/oembed?url={url}" + with httpx.Client(timeout=10.0) as client: + resp = client.get(oembed_url) + if resp.status_code == 200: + data = resp.json() + # data has title ("Track Name" or "Track Name - Single"), thumbnail_url + full_title = data.get("title", "Unknown") + # Spotfiy oEmbed titles are often "Title" or "Artist - Title" + artist = "Unknown" + title = full_title + if " - " in full_title: + parts = full_title.split(" - ", 1) + artist = parts[0].strip() + title = parts[1].strip() - songs = parse_query([url]) - for s in songs: - tracks.append(TrackMetadata( - service="spotify" if "spotify" in url else "apple", - service_id=s.url or s.song_id, - title=s.name, - artist=s.artist, - album=s.album_name, - track_number=s.track_number, - year=int(s.year) if s.year else None, - genre=s.genres[0] if s.genres else None, - duration_ms=int(s.duration * 1000) if s.duration else None, - is_explicit=bool(s.explicit), - cover_url=s.cover_url, - source_url=s.url - )) + return [TrackMetadata( + service="spotify", + service_id=url, + title=title, + artist=artist, + album="Singles", + cover_url=data.get("thumbnail_url"), + source_url=url + )] except Exception as e: - print(f"SpotDL resolve error, falling back to yt-dlp: {e}") - return self._resolve_ytdlp(url) - return tracks + print(f"Spotify oEmbed resolution failed: {e}") + return [] + + def _resolve_apple_music(self, url: str) -> List[TrackMetadata]: + # Extract song/album title from URL or yt-dlp + m = re.search(r'/album/([^/]+)/(\d+)', url) + if m: + album_slug = m.group(1).replace("-", " ").title() + return [TrackMetadata( + service="apple", + service_id=url, + title=album_slug, + artist="Apple Music", + album=album_slug, + source_url=url + )] + return self._resolve_ytdlp(url) def _resolve_ytdlp(self, url: str) -> List[TrackMetadata]: tracks: List[TrackMetadata] = [] @@ -81,7 +108,7 @@ class UniversalProvider(BaseProvider): uploader = entry.get("uploader") or entry.get("artist") or "Unknown" tracks.append(TrackMetadata( service="youtube", - service_id=entry.get("id", ""), + service_id=str(entry.get("id", "")), title=title, artist=uploader, album=entry.get("album"), @@ -103,7 +130,6 @@ class UniversalProvider(BaseProvider): if track.is_explicit: query += " explicit" - # Search and download best audio via yt-dlp ydl_opts = { "format": "bestaudio/best", "outtmpl": dest_path + ".tmp.%(ext)s", @@ -123,8 +149,6 @@ class UniversalProvider(BaseProvider): ydl.download([target_input]) if os.path.exists(tmp_output): - # Apply high quality tags from metadata - import httpx cover_bytes = None if track.cover_url: try: diff --git a/requirements.txt b/requirements.txt index 8e82515..08e3f73 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,9 @@ -fastapi>=0.115.0 +fastapi>=0.110.0 uvicorn[standard]>=0.30.0 -pydantic>=2.9.0 -pydantic-settings>=2.5.0 +pydantic>=2.7.0 +pydantic-settings>=2.4.0 yandex-music>=3.2.0 yt-dlp>=2024.10.0 -spotdl>=4.2.0 mutagen>=1.47.0 syncedlyrics>=0.10.0 aiofiles>=24.1.0