From 448be52f4a029a1228e9926802a31d709b157415 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Sun, 22 Feb 2026 11:48:33 -0600 Subject: [PATCH] Fix playlist playback: use MA enqueue and search resolution The play_playlist_on_speaker function was sending text search queries to raw Cast entities which can't resolve them. Now uses enqueue: "replace" for the first track and "add" for subsequent tracks. Added 1s delay between requests so MA can process each Apple Music search. Increased HTTP timeout to 30s for search latency. The caller must pass a Music Assistant entity (_2 suffix) for text-based search to work. Co-Authored-By: Claude Opus 4.6 --- src/haunt_fm/services/music_assistant.py | 47 +++++++++++------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/haunt_fm/services/music_assistant.py b/src/haunt_fm/services/music_assistant.py index e72b094..d396768 100644 --- a/src/haunt_fm/services/music_assistant.py +++ b/src/haunt_fm/services/music_assistant.py @@ -1,3 +1,4 @@ +import asyncio import logging import httpx @@ -13,7 +14,7 @@ async def _ha_request(method: str, path: str, **kwargs) -> dict: "Authorization": f"Bearer {settings.ha_token}", "Content-Type": "application/json", } - async with httpx.AsyncClient(timeout=10) as client: + async with httpx.AsyncClient(timeout=30) as client: resp = await client.request( method, f"{settings.ha_url}{path}", headers=headers, **kwargs ) @@ -81,37 +82,31 @@ async def play_playlist_on_speaker( tracks: list[dict], speaker_entity: str, ) -> None: - """Play a list of tracks on a speaker. Each track dict has 'artist' and 'title'. + """Play a list of tracks on a speaker via Music Assistant. - Enqueues tracks via Music Assistant. + Each track dict has 'artist' and 'title'. The speaker_entity MUST be a + Music Assistant entity (the _2 suffix ones, e.g. media_player.living_room_speaker_2) + so that text search queries are resolved via Apple Music. """ if not tracks: return for i, track in enumerate(tracks): + search_query = f"{track['artist']} {track['title']}" try: - if i == 0: - # Play first track - await _ha_request( - "POST", - "/api/services/media_player/play_media", - json={ - "entity_id": speaker_entity, - "media_content_id": f"{track['artist']} - {track['title']}", - "media_content_type": "music", - }, - ) - else: - # Enqueue subsequent tracks - await _ha_request( - "POST", - "/api/services/media_player/play_media", - json={ - "entity_id": speaker_entity, - "media_content_id": f"{track['artist']} - {track['title']}", - "media_content_type": "music", - "enqueue": "add", - }, - ) + await _ha_request( + "POST", + "/api/services/media_player/play_media", + json={ + "entity_id": speaker_entity, + "media_content_id": search_query, + "media_content_type": "music", + "enqueue": "replace" if i == 0 else "add", + }, + ) + logger.info("Enqueued [%d/%d]: %s", i + 1, len(tracks), search_query) + # Brief pause between requests so MA can process each search + if i < len(tracks) - 1: + await asyncio.sleep(1) except Exception: logger.exception("Failed to enqueue %s - %s", track["artist"], track["title"])