import asyncio import uuid from typing import Dict, List, Optional from datetime import datetime from pydantic import BaseModel from app.config import settings from app.db import is_track_downloaded, record_downloaded_track from app.core.tagger import get_track_dest_path from app.core.lyrics import fetch_and_save_lyrics from app.core.navidrome import trigger_navidrome_scan from app.providers.base import BaseProvider, TrackMetadata from app.providers.yandex import YandexMusicProvider from app.providers.universal import UniversalProvider class TaskInfo(BaseModel): id: str target: str status: str # pending, resolving, downloading, completed, failed progress: int = 0 total: int = 0 current_item: str = "" error: Optional[str] = None created_at: str updated_at: str class SyncManager: def __init__(self): self.tasks: Dict[str, TaskInfo] = {} self.yandex_provider = YandexMusicProvider() self.universal_provider = UniversalProvider() self._lock = asyncio.Lock() def get_provider(self, url: str) -> BaseProvider: if self.yandex_provider.can_handle(url): return self.yandex_provider return self.universal_provider async def start_sync_task(self, target: str) -> TaskInfo: task_id = str(uuid.uuid4())[:8] now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") info = TaskInfo( id=task_id, target=target, status="pending", created_at=now, updated_at=now ) self.tasks[task_id] = info asyncio.create_task(self._process_task(task_id, target)) return info async def _process_task(self, task_id: str, target: str): task = self.tasks[task_id] provider = self.get_provider(target) try: task.status = "resolving" task.current_item = "Fetching metadata..." task.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") tracks: List[TrackMetadata] = await provider.resolve(target) task.total = len(tracks) if not tracks: task.status = "completed" task.current_item = "No tracks found or URL empty." return task.status = "downloading" downloaded_any = False for idx, track in enumerate(tracks, start=1): task.progress = idx task.current_item = f"{track.artist} - {track.title}" task.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") # Check if already downloaded if is_track_downloaded(track.service, track.service_id): continue # Destination path dest_path = get_track_dest_path( settings.MUSIC_DIR, artist=track.artist, album=track.album or "Singles", track_num=track.track_number, title=track.title, ext="mp3" ) # Attempt download success = await provider.download_track(track, dest_path) # If Yandex failed (e.g. censored or region blocked), fallback to universal search if not success and track.service == "yandex": print(f"Fallback to universal search for: {track.artist} - {track.title}") success = await self.universal_provider.download_track(track, dest_path) if success: downloaded_any = True # Fetch synchronized lyrics if enabled if settings.DOWNLOAD_LYRICS: query = f"{track.artist} {track.title}" fetch_and_save_lyrics(dest_path, query) # Save to DB record_downloaded_track( service=track.service, service_id=track.service_id, artist=track.artist, title=track.title, album=track.album or "Singles", file_path=dest_path ) task.status = "completed" task.current_item = f"Finished processing {task.total} tracks." task.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") # Trigger Navidrome scan if downloaded_any: await trigger_navidrome_scan() except Exception as e: task.status = "failed" task.error = str(e) task.current_item = f"Error: {e}" task.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") sync_manager = SyncManager()