Files
navidrome-music-sync/app/sync_queue.py
T
Antigravity e5afeb06cc feat: Phase 3 AI Radio (DJ Host & interleaved playlist) and Like/Dislike/Delete actions
- 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
2026-10-12 16:34:46 +03:00

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()