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

+9 -1
View File
@@ -124,8 +124,16 @@ class WaveCollector:
return profile
def get_existing_track_keys(self) -> Set[str]:
"""Returns set of normalized 'artist|title' of all songs already in Navidrome library."""
"""Returns set of normalized 'artist|title' of all songs already in Navidrome library and disliked tracks."""
keys: Set[str] = set()
# Exclude disliked / blacklisted tracks
try:
from app.db import get_disliked_track_keys
keys.update(get_disliked_track_keys())
except Exception as e:
print(f"[WaveCollector] Warning reading disliked keys: {e}")
conn = self._get_connection()
if not conn:
return keys
+94
View File
@@ -227,6 +227,100 @@ class WaveRotator:
"purged_tracks": purged
}
async def dislike_track(self, filename: str) -> Dict[str, Any]:
"""User dislikes a track: delete it from Wave, add to blacklisted tracks, refill buffer."""
fpath = os.path.join(self.wave_dir, os.path.basename(filename))
if not os.path.exists(fpath):
raise FileNotFoundError(f"Track not found in wave: {filename}")
name_no_ext = os.path.splitext(os.path.basename(filename))[0]
artist = "Unknown"
title = name_no_ext
if " - " in name_no_ext:
parts = name_no_ext.split(" - ", 1)
artist = parts[0].strip()
title = parts[1].strip()
from app.db import add_disliked_track
add_disliked_track(artist, title)
print(f"[WaveRotator] 👎 Disliked track: {artist} - {title}")
try:
os.remove(fpath)
lrc = os.path.splitext(fpath)[0] + ".lrc"
if os.path.exists(lrc):
os.remove(lrc)
except Exception as e:
print(f"[WaveRotator] Error removing disliked track {filename}: {e}")
# Refill buffer by 1 track
await self.fill_buffer(refresh_count=0)
ensure_smart_playlist()
return {
"status": "disliked",
"artist": artist,
"title": title,
"buffer_status": self.get_status()
}
async def like_track(self, filename: str) -> Dict[str, Any]:
"""User likes a track: promote it immediately to permanent library, refill buffer."""
fpath = os.path.join(self.wave_dir, os.path.basename(filename))
if not os.path.exists(fpath):
raise FileNotFoundError(f"Track not found in wave: {filename}")
name_no_ext = os.path.splitext(os.path.basename(filename))[0]
artist = "Unknown"
title = name_no_ext
album = "Singles"
if " - " in name_no_ext:
parts = name_no_ext.split(" - ", 1)
artist = parts[0].strip()
title = parts[1].strip()
dest_path = get_track_dest_path(
settings.MUSIC_DIR,
artist=artist,
album=album,
track_num=None,
title=title,
ext="mp3"
)
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
shutil.move(fpath, dest_path)
# Move .lrc if exists
src_lrc = os.path.splitext(fpath)[0] + ".lrc"
if os.path.exists(src_lrc):
dest_lrc = os.path.splitext(dest_path)[0] + ".lrc"
shutil.move(src_lrc, dest_lrc)
# Record in downloaded_tracks
from app.db import record_downloaded_track
record_downloaded_track(
service="wave_liked",
service_id=filename,
artist=artist,
title=title,
album=album,
file_path=dest_path
)
print(f"[WaveRotator] ⭐ Liked and promoted to library: {artist} - {title} -> {dest_path}")
# Refill buffer by 1 track
await self.fill_buffer(refresh_count=0)
ensure_smart_playlist()
return {
"status": "liked",
"artist": artist,
"title": title,
"dest_path": dest_path,
"buffer_status": self.get_status()
}
def get_status(self) -> Dict[str, Any]:
"""Get summary status of Smart Wave."""
buffer_tracks = self.get_current_buffer()