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:
Antigravity committed 2026-10-12 16:34:46 +03:00
1 parent 3db9bdb38b
commit e5afeb06cc
15 files changed
+945 -8

No files matched your search

+66 -2
View File
@@ -9,8 +9,8 @@ from pydantic import BaseModel
from typing import List, Dict, Any, Optional
from app.config import settings
from app.db import init_db, get_recent_tracks, get_tracks_count, get_db
from app.queue import sync_manager
from app.db import init_db, get_recent_tracks, get_tracks_count, get_db, remove_downloaded_track
from app.sync_queue import sync_manager
from app.core.search import search_all
from app.core.tagger import get_track_dest_path
from app.core.auth import (
@@ -22,6 +22,7 @@ from app.core.auth import (
)
from app.wave.rotator import wave_rotator
from app.wave.playlist import ensure_smart_playlist
from app.radio import radio_manager
app = FastAPI(title="Navidrome Music Sync", version="1.0.0")
@@ -36,6 +37,12 @@ class DownloadRequest(BaseModel):
class WaveGenerateRequest(BaseModel):
mood: Optional[str] = "auto"
class WaveActionRequest(BaseModel):
filename: str
class RadioGenerateRequest(BaseModel):
voice: Optional[str] = "ru-RU-DmitryNeural"
async def _wave_auto_rotation_loop():
"""Background loop to periodically rotate and refill Smart Wave buffer."""
while True:
@@ -270,3 +277,60 @@ def rotate_wave(user: str = Depends(require_auth)):
"purged_tracks": purged,
"current_status": wave_rotator.get_status()
}
@app.post("/api/wave/dislike")
async def dislike_wave_track(req: WaveActionRequest, user: str = Depends(require_auth)):
"""Dislike track: remove from wave, blacklist from recommendations, refill slot."""
try:
res = await wave_rotator.dislike_track(req.filename)
return res
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Трек не найден в буфере Волны")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/wave/like")
async def like_wave_track(req: WaveActionRequest, user: str = Depends(require_auth)):
"""Like track: promote to permanent library, refill slot."""
try:
res = await wave_rotator.like_track(req.filename)
return res
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Трек не найден в буфере Волны")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/api/track")
def delete_track(file_path: str = Query(...), user: str = Depends(require_auth)):
"""Delete any track from library/disk and remove from download history."""
real_path = os.path.realpath(file_path)
music_real = os.path.realpath(settings.MUSIC_DIR)
if not real_path.startswith(music_real):
raise HTTPException(status_code=400, detail="Недопустимый путь к файлу")
if not os.path.exists(real_path):
raise HTTPException(status_code=404, detail="Файл трека не найден на диске")
try:
os.remove(real_path)
lrc_path = os.path.splitext(real_path)[0] + ".lrc"
if os.path.exists(lrc_path):
os.remove(lrc_path)
remove_downloaded_track(file_path)
ensure_smart_playlist()
return {"status": "deleted", "file_path": file_path}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка удаления: {e}")
# --- AI Radio DJ Endpoints (Phase 3) ---
@app.get("/api/radio/status")
def get_radio_status(user: str = Depends(require_auth)):
"""Return status of AI Radio host and broadcast playlist."""
return radio_manager.get_radio_status()
@app.post("/api/radio/generate")
async def generate_radio_broadcast(req: Optional[RadioGenerateRequest] = None, user: str = Depends(require_auth)):
"""Generate AI DJ speech intros and assemble the interleaved 'AI Радио.m3u8' playlist."""
voice = req.voice if req else settings.RADIO_VOICE
res = await radio_manager.generate_radio_broadcast(voice=voice)
return res