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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user