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
+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