From 7593d8bfc2ad1f5decaf2b509f5612c4a4540897 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Thu, 13 Mar 2025 10:34:26 -0700 Subject: [PATCH] [Chore] Move podcastparser parse functions from itunes search provider to helpers (#2034) --- .../parsers.py => helpers/podcast_parsers.py} | 51 ++++++++++++++----- .../providers/itunes_podcasts/__init__.py | 12 ++--- .../providers/itunes_podcasts/manifest.json | 3 -- 3 files changed, 45 insertions(+), 21 deletions(-) rename music_assistant/{providers/itunes_podcasts/parsers.py => helpers/podcast_parsers.py} (76%) diff --git a/music_assistant/providers/itunes_podcasts/parsers.py b/music_assistant/helpers/podcast_parsers.py similarity index 76% rename from music_assistant/providers/itunes_podcasts/parsers.py rename to music_assistant/helpers/podcast_parsers.py index 5201868d..c4ae1473 100644 --- a/music_assistant/providers/itunes_podcasts/parsers.py +++ b/music_assistant/helpers/podcast_parsers.py @@ -16,19 +16,29 @@ from music_assistant_models.media_items import ( def parse_podcast( - *, feed_url: str, parsed_feed: dict[str, Any], lookup_key: str, domain: str, instance_id: str + *, + feed_url: str, + parsed_feed: dict[str, Any], + lookup_key: str, + domain: str, + instance_id: str, + mass_item_id: str | None = None, ) -> Podcast: - """Podcast -> Mass Podcast.""" + """Podcast -> Mass Podcast. + + The item_id is the feed url by default, or the optional mass_item_id instead. + """ publisher = parsed_feed.get("author") or parsed_feed.get("itunes_author", "NO_AUTHOR") + item_id = feed_url if mass_item_id is None else mass_item_id mass_podcast = Podcast( - item_id=feed_url, + item_id=item_id, name=parsed_feed.get("title", "NO_TITLE"), publisher=publisher, provider=lookup_key, uri=parsed_feed.get("link"), provider_mappings={ ProviderMapping( - item_id=feed_url, + item_id=item_id, provider_domain=domain, provider_instance=instance_id, ) @@ -65,6 +75,18 @@ def parse_podcast( return mass_podcast +def get_stream_url_and_guid_from_episode( + *, episode: dict[str, Any] +) -> tuple[str | None, str | None]: + """Give episode's stream url and guid, if it exists.""" + episode_enclosures = episode.get("enclosures", []) + if len(episode_enclosures) < 1: + raise RuntimeError + stream_url = episode_enclosures[0].get("url", None) + guid = episode.get("guid") + return stream_url, guid + + def parse_podcast_episode( *, episode: dict[str, Any], @@ -74,20 +96,25 @@ def parse_podcast_episode( lookup_key: str, domain: str, instance_id: str, + mass_item_id: str | None = None, ) -> PodcastEpisode: - """Podcast Episode -> Mass Podcast Episode.""" + """Podcast Episode -> Mass Podcast Episode. + + The item_id is {prov_podcast_id} {guid_or_stream_url} by default, or the optional mass_item_id + instead. The podcast_cover is used, if the episode should not have its own cover. + """ episode_duration = episode.get("total_time", 0.0) episode_title = episode.get("title", "NO_EPISODE_TITLE") episode_cover = episode.get("episode_art_url", podcast_cover) episode_published = episode.get("published") - episode_enclosures = episode.get("enclosures", []) - if len(episode_enclosures) < 1: - raise RuntimeError - stream_url = episode_enclosures[0].get("url", None) - # not all feeds have a guid, but a guid is preferred as identification - guid_or_stream_url = episode.get("guid", stream_url) - episode_id = f"{prov_podcast_id} {guid_or_stream_url}" + stream_url, guid = get_stream_url_and_guid_from_episode(episode=episode) + guid_or_stream_url = guid if guid is not None else stream_url + if stream_url is None: + raise RuntimeError("Episode has no stream information!") + + # Default episode id. A guid is preferred as identification. + episode_id = f"{prov_podcast_id} {guid_or_stream_url}" if mass_item_id is None else mass_item_id mass_episode = PodcastEpisode( item_id=episode_id, provider=lookup_key, diff --git a/music_assistant/providers/itunes_podcasts/__init__.py b/music_assistant/providers/itunes_podcasts/__init__.py index f54bf29c..0b124f7e 100644 --- a/music_assistant/providers/itunes_podcasts/__init__.py +++ b/music_assistant/providers/itunes_podcasts/__init__.py @@ -31,9 +31,9 @@ from music_assistant_models.media_items import ( ) from music_assistant_models.streamdetails import StreamDetails +from music_assistant.helpers.podcast_parsers import parse_podcast, parse_podcast_episode from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries from music_assistant.models.music_provider import MusicProvider -from music_assistant.providers.itunes_podcasts.parsers import parse_podcast, parse_podcast_episode from music_assistant.providers.itunes_podcasts.schema import ITunesSearchResults if TYPE_CHECKING: @@ -201,7 +201,7 @@ class ITunesPodcastsProvider(MusicProvider): async def get_podcast(self, prov_podcast_id: str) -> Podcast: """Get podcast.""" - parsed = await self._get_cached_podcast(prov_podcast_id) + parsed = await self._cache_get_podcast(prov_podcast_id) return parse_podcast( feed_url=prov_podcast_id, @@ -215,7 +215,7 @@ class ITunesPodcastsProvider(MusicProvider): self, prov_podcast_id: str ) -> AsyncGenerator[PodcastEpisode, None]: """Get podcast episodes.""" - podcast = await self._get_cached_podcast(prov_podcast_id) + podcast = await self._cache_get_podcast(prov_podcast_id) podcast_cover = podcast.get("cover_url") episodes = podcast.get("episodes", []) for cnt, episode in enumerate(episodes): @@ -232,7 +232,7 @@ class ITunesPodcastsProvider(MusicProvider): async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode: """Get single podcast episode.""" prov_podcast_id, guid_or_stream_url = prov_episode_id.split(" ") - podcast = await self._get_cached_podcast(prov_podcast_id) + podcast = await self._cache_get_podcast(prov_podcast_id) podcast_cover = podcast.get("cover_url") episodes = podcast.get("episodes", []) for cnt, episode in enumerate(episodes): @@ -254,7 +254,7 @@ class ITunesPodcastsProvider(MusicProvider): raise MediaNotFoundError("Episode not found") async def _get_episode_stream_url(self, podcast_id: str, guid_or_stream_url: str) -> str | None: - podcast = await self._get_cached_podcast(podcast_id) + podcast = await self._cache_get_podcast(podcast_id) episodes = podcast.get("episodes", []) for cnt, episode in enumerate(episodes): episode_enclosures = episode.get("enclosures", []) @@ -284,7 +284,7 @@ class ITunesPodcastsProvider(MusicProvider): allow_seek=True, ) - async def _get_cached_podcast(self, prov_podcast_id: str) -> dict[str, Any]: + async def _cache_get_podcast(self, prov_podcast_id: str) -> dict[str, Any]: parsed_podcast = await self.mass.cache.get( key=prov_podcast_id, base_key=self.lookup_key, diff --git a/music_assistant/providers/itunes_podcasts/manifest.json b/music_assistant/providers/itunes_podcasts/manifest.json index 264c9f54..4210726c 100644 --- a/music_assistant/providers/itunes_podcasts/manifest.json +++ b/music_assistant/providers/itunes_podcasts/manifest.json @@ -6,9 +6,6 @@ "codeowners": [ "@fmunkes" ], - "requirements": [ - "podcastparser==0.6.10" - ], "icon": "podcast", "documentation": "https://music-assistant.io/music-providers/itunes-podcasts/", "multi_instance": false -- 2.34.1