Fix Large PodcastEpisode listings not working (#1991)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 28 Feb 2025 16:05:14 +0000 (17:05 +0100)
committerGitHub <noreply@github.com>
Fri, 28 Feb 2025 16:05:14 +0000 (17:05 +0100)
Convert Fix Large PodcastEpisode listings not working

It turns out that some podcasts can contain many episodes, which could potentially result in a too large json message (especially with HA ingress). Adjust podcast episode listings to use asyncgenerator so it will be chunked sent over the api.

music_assistant/controllers/media/podcasts.py
music_assistant/controllers/player_queues.py
music_assistant/models/music_provider.py
music_assistant/providers/audiobookshelf/__init__.py
music_assistant/providers/filesystem_local/__init__.py
music_assistant/providers/opensubsonic/sonic_provider.py
music_assistant/providers/podcastfeed/__init__.py
music_assistant/providers/test/__init__.py
music_assistant/providers/ytmusic/__init__.py

index 6a6fd5291c1fd22ae8e91137730dfff5bd5ecd85..c7708536f17c1f7c467c50cb8c2da579891d1752 100644 (file)
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-import asyncio
+from collections.abc import AsyncGenerator
 from typing import TYPE_CHECKING, Any
 
 from music_assistant_models.enums import MediaType, ProviderFeature
@@ -100,7 +100,7 @@ class PodcastsController(MediaControllerBase[Podcast]):
         self,
         item_id: str,
         provider_instance_id_or_domain: str,
-    ) -> UniqueList[PodcastEpisode]:
+    ) -> AsyncGenerator[PodcastEpisode, None]:
         """Return podcast episodes for the given provider podcast id."""
         # always check if we have a library item for this podcast
         if provider_instance_id_or_domain == "library":
@@ -113,10 +113,10 @@ class PodcastsController(MediaControllerBase[Podcast]):
                 break
         # podcast episodes are not stored in the db/library
         # so we always need to fetch them from the provider
-        episodes = await self._get_provider_podcast_episodes(
+        async for episode in self._get_provider_podcast_episodes(
             item_id, provider_instance_id_or_domain
-        )
-        return sorted(episodes, key=lambda x: x.position)
+        ):
+            yield episode
 
     async def episode(
         self,
@@ -216,15 +216,11 @@ class PodcastsController(MediaControllerBase[Podcast]):
 
     async def _get_provider_podcast_episodes(
         self, item_id: str, provider_instance_id_or_domain: str
-    ) -> list[PodcastEpisode]:
+    ) -> AsyncGenerator[PodcastEpisode, None]:
         """Return podcast episodes for the given provider podcast id."""
         prov: MusicProvider = self.mass.get_provider(provider_instance_id_or_domain)
         if prov is None:
-            return []
-        # grab the episodes from the provider
-        # note that we do not cache any of this because its
-        # always a rather small list and we want fresh resume info
-        items = await prov.get_podcast_episodes(item_id)
+            return
 
         async def set_resume_position(episode: PodcastEpisode) -> None:
             if episode.fully_played is not None or episode.resume_position_ms:
@@ -247,8 +243,12 @@ class PodcastsController(MediaControllerBase[Podcast]):
             if resume_info_db_row["fully_played"] is not None:
                 episode.fully_played = resume_info_db_row["fully_played"]
 
-        await asyncio.gather(*[set_resume_position(x) for x in items])
-        return items
+        # grab the episodes from the provider
+        # note that we do not cache any of this because its
+        # always a rather small list and we want fresh resume info
+        async for item in prov.get_podcast_episodes(item_id):
+            await set_resume_position(item)
+            yield item
 
     async def radio_mode_base_tracks(
         self,
index facac8604a576a789005e0493899bc35e37d367a..923317d54c75bb26d6671c577dc24027526d3366 100644 (file)
@@ -1338,7 +1338,10 @@ class PlayerQueuesController(CoreController):
             "Fetching episode(s) and resume point to play for Podcast %s",
             podcast.name,
         )
-        all_episodes = await self.mass.music.podcasts.episodes(podcast.item_id, podcast.provider)
+        all_episodes = [
+            x async for x in self.mass.music.podcasts.episodes(podcast.item_id, podcast.provider)
+        ]
+        all_episodes.sort(key=lambda x: x.position)
         # if a episode was provided, a user explicitly selected a episode to play
         # so we need to find the index of the episode in the list
         if isinstance(episode, PodcastEpisode):
index a668b38774cca1809815b79f11b748a54af987d2..3c23c4aa7443a6911bdc90a8b4a731f203c88211 100644 (file)
@@ -192,8 +192,9 @@ class MusicProvider(Provider):
     async def get_podcast_episodes(
         self,
         prov_podcast_id: str,
-    ) -> list[PodcastEpisode]:
+    ) -> AsyncGenerator[PodcastEpisode, None]:
         """Get all PodcastEpisodes for given podcast id."""
+        yield
         if ProviderFeature.LIBRARY_PODCASTS in self.supported_features:
             raise NotImplementedError
 
index 7058c00c38509cecde9b140753ed3db3182dfd26..5b9cba5b3ce816bcbbb9ea147e0589a4545f67a0 100644 (file)
@@ -290,13 +290,14 @@ class Audiobookshelf(MusicProvider):
             base_url=str(self.config.get_value(CONF_URL)).rstrip("/"),
         )
 
-    async def get_podcast_episodes(self, prov_podcast_id: str) -> list[PodcastEpisode]:
+    async def get_podcast_episodes(
+        self, prov_podcast_id: str
+    ) -> AsyncGenerator[PodcastEpisode, None]:
         """Get all podcast episodes of podcast.
 
         Adds progress information.
         """
         abs_podcast = await self._get_abs_expanded_podcast(prov_podcast_id=prov_podcast_id)
-        episode_list = []
         episode_cnt = 1
         # the user has the progress of all media items
         # so we use a single api call here to obtain possibly many
@@ -320,9 +321,8 @@ class Audiobookshelf(MusicProvider):
                 base_url=str(self.config.get_value(CONF_URL)).rstrip("/"),
                 media_progress=progress,
             )
-            episode_list.append(mass_episode)
+            yield mass_episode
             episode_cnt += 1
-        return episode_list
 
     async def get_podcast_episode(
         self, prov_episode_id: str, add_progress: bool = True
index f46cc146cc774f528d68e1a967eaee7ac73783f7..1b8b4533c2fc8f215e6019ab577839f4ad4c9c60 100644 (file)
@@ -637,7 +637,7 @@ class LocalFileSystemProvider(MusicProvider):
 
     async def get_podcast(self, prov_podcast_id: str) -> Podcast:
         """Get full podcast details by id."""
-        for episode in await self.get_podcast_episodes(prov_podcast_id):
+        async for episode in self.get_podcast_episodes(prov_podcast_id):
             assert isinstance(episode.podcast, Podcast)
             return episode.podcast
         msg = f"Podcast not found: {prov_podcast_id}"
@@ -701,7 +701,9 @@ class LocalFileSystemProvider(MusicProvider):
             )
         return result
 
-    async def get_podcast_episodes(self, prov_podcast_id: str) -> list[PodcastEpisode]:
+    async def get_podcast_episodes(
+        self, prov_podcast_id: str
+    ) -> AsyncGenerator[PodcastEpisode, None]:
         """Get podcast episodes for given podcast id."""
         episodes: list[PodcastEpisode] = []
 
@@ -726,7 +728,8 @@ class LocalFileSystemProvider(MusicProvider):
                     continue
                 tm.create_task(_process_podcast_episode(item))
 
-        return episodes
+        for episode in episodes:
+            yield episode
 
     async def _parse_playlist_line(self, line: str, playlist_path: str) -> Track | None:
         """Try to parse a track from a playlist line."""
index ddfe8e7c02d21e89628b469d8f2a79f76f14a029..620ac827d6ba7423ad87b10022daf0b203ed1448 100644 (file)
@@ -607,7 +607,7 @@ class OpenSonicProvider(MusicProvider):
     async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode:
         """Get (full) podcast episode details by id."""
         podcast_id, _ = prov_episode_id.split(EP_CHAN_SEP)
-        for episode in await self.get_podcast_episodes(podcast_id):
+        async for episode in self.get_podcast_episodes(podcast_id):
             if episode.item_id == prov_episode_id:
                 return episode
         msg = f"Episode {prov_episode_id} not found"
@@ -616,20 +616,16 @@ class OpenSonicProvider(MusicProvider):
     async def get_podcast_episodes(
         self,
         prov_podcast_id: str,
-    ) -> list[PodcastEpisode]:
+    ) -> AsyncGenerator[PodcastEpisode, None]:
         """Get all Episodes for given podcast id."""
         if not self._enable_podcasts:
-            return []
-
+            return
         channels = await self._run_async(
             self._conn.getPodcasts, incEpisodes=True, pid=prov_podcast_id
         )
-
         channel = channels[0]
-        episodes = []
         for episode in channel.episodes:
-            episodes.append(self._parse_epsiode(episode, channel))
-        return episodes
+            yield self._parse_epsiode(episode, channel)
 
     async def get_podcast(self, prov_podcast_id: str) -> Podcast:
         """Get full Podcast details by id."""
index 836317058ed08ae9b18d8c7b159f4cf54b78fd06..c20f908d84134bd4edffa5cdd7728d17a9c4f093 100644 (file)
@@ -157,14 +157,12 @@ class PodcastMusicprovider(MusicProvider):
     async def get_podcast_episodes(
         self,
         prov_podcast_id: str,
-    ) -> list[PodcastEpisode]:
+    ) -> AsyncGenerator[PodcastEpisode, None]:
         """List all episodes for the podcast."""
-        episodes = []
         if prov_podcast_id != self.podcast_id:
             raise Exception(f"Podcast id not in provider: {prov_podcast_id}")
         for idx, episode in enumerate(self.parsed["episodes"]):
-            episodes.append(await self._parse_episode(episode, idx))
-        return episodes
+            yield await self._parse_episode(episode, idx)
 
     async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails:
         """Get streamdetails for a track/radio."""
index d645ad526476db4c893042566ecd77f70ab49a8e..a5687fb13256735f8daf3b3fa9c11df69dd3755b 100644 (file)
@@ -301,13 +301,11 @@ class TestProvider(MusicProvider):
     async def get_podcast_episodes(
         self,
         prov_podcast_id: str,
-    ) -> list[PodcastEpisode]:
+    ) -> AsyncGenerator[PodcastEpisode, None]:
         """Get all PodcastEpisodes for given podcast id."""
         num_episodes = 25
-        return [
-            await self.get_podcast_episode(f"{prov_podcast_id}_{episode_idx}")
-            for episode_idx in range(num_episodes)
-        ]
+        for episode_idx in range(num_episodes):
+            yield await self.get_podcast_episode(f"{prov_podcast_id}_{episode_idx}")
 
     async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode:
         """Get (full) podcast episode details by id."""
index 4e39a0cf352002f297456e3519cf3243dc97aea1..adf286ee748ce2205d0b7dbd106e5d03389d2b2d 100644 (file)
@@ -408,18 +408,18 @@ class YoutubeMusicProvider(MusicProvider):
         podcast_obj = await get_podcast(prov_podcast_id, headers=self._headers)
         return self._parse_podcast(podcast_obj)
 
-    async def get_podcast_episodes(self, prov_podcast_id: str) -> list[PodcastEpisode]:
+    async def get_podcast_episodes(
+        self, prov_podcast_id: str
+    ) -> AsyncGenerator[PodcastEpisode, None]:
         """Get all episodes from a podcast."""
         podcast_obj = await get_podcast(prov_podcast_id, headers=self._headers)
         podcast_obj["podcastId"] = prov_podcast_id
         podcast = self._parse_podcast(podcast_obj)
-        episodes = []
         for index, episode_obj in enumerate(podcast_obj.get("episodes", []), start=1):
             episode = self._parse_podcast_episode(episode_obj, podcast)
             ep_index = episode_obj.get("index") or index
             episode.position = ep_index
-            episodes.append(episode)
-        return episodes
+            yield episode
 
     async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode:
         """Get a single Podcast Episode."""