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
+66
-2
@@ -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
|
||||
|
||||
Reference in new issue
Block a user