- Add Like button: promotes wave track to permanent library and refills buffer - Add Dislike button: blacklists track in SQLite disliked_tracks, removes audio, refills buffer - Add Delete button in history table for library file removal - Add AI Radio module: wttr.in weather, Qwen 2.5 host monologues, speech synthesis, ID3 tags - Generate interleaved 'AI Радио.m3u8' alongside clean 'Моя Волна.m3u8' - Update Web UI with Radio DJ card, voice selection, and track actions
132 lines
4.7 KiB
Python
132 lines
4.7 KiB
Python
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()
|