feat: progressive music streaming, library-first search, player fixes
Rebuild & Deploy / rebuild (push) Successful in 59s
Rebuild & Deploy / rebuild (push) Successful in 59s
- Stream downloads as they grow: raw best-audio (no mp3 transcode) served chunked from the stream endpoint mid-download, then with native Range once complete. Songs play within seconds of hitting Play. - Fix searched songs never starting: reconcileMedia now wires the stream URL when a track that was preparing gains one; audio_url is stable for the whole lifecycle so the player never reloads mid-playback. - Progress bars now parse yt-dlp stderr (stdout never carried [download]). - Prefer downloaded songs: /media/api/search merges ready library matches ahead of YouTube results; search_music/play_song tooling and the agent prompt tell Hope to always pick in_library=true. - Mute button turns solid red while the mic is muted.
This commit is contained in:
1 parent
d89918fd2a
commit
4f8eb00c9f
7 files changed
+440
-107
No files matched your search
+7
-4
@@ -213,10 +213,13 @@ SYSTEM_PROMPT = textwrap.dedent("""\
|
||||
- Ambiguous results → offer_song_choices with the top 2-3 options, name
|
||||
them briefly, and let the user pick by voice or in the player panel.
|
||||
Never start playback on a guess.
|
||||
The library is permanent: list_library shows what is already downloaded,
|
||||
and those tracks play instantly without searching. If the user names a
|
||||
library track exactly, just play it. Control playback with pause_music,
|
||||
resume_music, skip_song and seek_music; check what's on with now_playing.
|
||||
Prefer what is already saved: search_music puts downloaded library
|
||||
matches first (in_library=true) — always pick one of those over a fresh
|
||||
YouTube result, and list_library shows the whole saved collection which
|
||||
plays instantly. If the user names a library track exactly, just play it.
|
||||
A brand-new song starts playing within seconds and finishes downloading
|
||||
in the background. Control playback with pause_music, resume_music,
|
||||
skip_song and seek_music; check what's on with now_playing.
|
||||
add_stream registers a live radio or direct audio URL as a library track.
|
||||
While music is playing you cannot hear the user — they will come back to
|
||||
you when they want to talk again, and the music pauses automatically.
|
||||
|
||||
+16
-13
@@ -69,27 +69,30 @@ def _fmt_duration(seconds) -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def search_music(query: str, limit: int = 5) -> str:
|
||||
"""Search YouTube Music for songs. Call this before playing anything the
|
||||
user describes by name, artist or lyrics. Returns JSON results with
|
||||
video_id, title, channel, duration and in_library flags. Pick the result
|
||||
that clearly matches what the user asked for; if two or more could be
|
||||
right, use offer_song_choices instead of guessing."""
|
||||
"""Search for songs: already-downloaded library matches come first, then
|
||||
YouTube Music results. Call this before playing anything the user
|
||||
describes by name, artist or lyrics. Results carry video_id, title,
|
||||
channel, duration and in_library flags. ALWAYS prefer a result with
|
||||
in_library=true — it is already saved and plays instantly. Pick the
|
||||
result that clearly matches what the user asked for; if two or more could
|
||||
be right, use offer_song_choices instead of guessing."""
|
||||
data = _media_post("/media/api/search", {"query": query, "limit": max(1, min(limit, 10))})
|
||||
if "error" in data:
|
||||
return f"Search failed: {data['error']}"
|
||||
results = data.get("results", [])
|
||||
if not results:
|
||||
return f"No YouTube Music results for '{query}'."
|
||||
return f"No results for '{query}'."
|
||||
return json.dumps(results, ensure_ascii=False)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def play_song(video_id: str, title: str = "", channel: str = "") -> str:
|
||||
"""Play a song in the user's web player. Pass the video_id from
|
||||
search_music (or an in_library track_id). Downloads and transcodes first
|
||||
if needed; playback starts automatically once ready. After calling this,
|
||||
tell the user what is playing in one short sentence and stop talking —
|
||||
the player takes over and the conversation pauses until they return."""
|
||||
search_music — for an in_library result this plays the saved copy with no
|
||||
download; a new song downloads and starts playing automatically while it
|
||||
arrives. After calling this, tell the user what is playing in one short
|
||||
sentence and stop talking — the player takes over and the conversation
|
||||
pauses until they return."""
|
||||
data = _media_post("/media/api/play", {
|
||||
"room": MEDIA_ROOM, "video_id": video_id, "title": title, "channel": channel,
|
||||
})
|
||||
@@ -100,9 +103,9 @@ def play_song(video_id: str, title: str = "", channel: str = "") -> str:
|
||||
name = track.get("title") or title or video_id
|
||||
if status == "downloading":
|
||||
return (
|
||||
f"'{name}' is downloading and will start playing automatically when "
|
||||
"ready (usually under a minute). Say it's on the way, then hand over "
|
||||
"to the player."
|
||||
f"'{name}' is downloading and starts playing within seconds — "
|
||||
"audio streams in while the download finishes. Say it's on the "
|
||||
"way, then hand over to the player."
|
||||
)
|
||||
return f"Playing '{name}' now. Hand over to the player with a short sign-off."
|
||||
|
||||
|
||||
Reference in new issue
Block a user