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
This commit is contained in:
1 parent
3db9bdb38b
commit
e5afeb06cc
15 files changed
+945
-8
No files matched your search
@@ -0,0 +1,131 @@
|
||||
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()
|
||||
Reference in new issue
Block a user