From: Eric Munson Date: Mon, 23 Jun 2025 10:25:43 +0000 (-0400) Subject: Subsonic: Display the newest podcast episodes as front page recommendation (#2242) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=5a694eb7805455a41c732a5dd73f1a3962b65352;p=music-assistant-server.git Subsonic: Display the newest podcast episodes as front page recommendation (#2242) * Add cover art to podcast episodes when present We really should display cover art for each episode when we can. This will be espeically important when we display the newest episodes from any podcast on the front page. Signed-off-by: Eric B Munson * Add newest podcast episodes RecommdationFolder This is a simple way to easily see the latest episodes from your subscribed feeds on the front page. Signed-off-by: Eric B Munson --------- Signed-off-by: Eric B Munson --- diff --git a/music_assistant/providers/opensubsonic/parsers.py b/music_assistant/providers/opensubsonic/parsers.py index 95a1f8b0..406812b5 100644 --- a/music_assistant/providers/opensubsonic/parsers.py +++ b/music_assistant/providers/opensubsonic/parsers.py @@ -479,4 +479,23 @@ def parse_epsiode( if sonic_episode.description: episode.metadata.description = sonic_episode.description + if sonic_episode.cover_art: + episode.metadata.add_image( + MediaItemImage( + type=ImageType.THUMB, + path=sonic_episode.cover_art, + provider=instance_id, + remotely_accessible=False, + ) + ) + elif sonic_channel.cover_art: + episode.metadata.add_image( + MediaItemImage( + type=ImageType.THUMB, + path=sonic_channel.cover_art, + provider=instance_id, + remotely_accessible=False, + ) + ) + return episode diff --git a/music_assistant/providers/opensubsonic/sonic_provider.py b/music_assistant/providers/opensubsonic/sonic_provider.py index 1ffb9a2d..c6b6cf3d 100644 --- a/music_assistant/providers/opensubsonic/sonic_provider.py +++ b/music_assistant/providers/opensubsonic/sonic_provider.py @@ -66,6 +66,7 @@ if TYPE_CHECKING: from libopensonic.media import Child as SonicSong from libopensonic.media import OpenSubsonicExtension from libopensonic.media import Playlist as SonicPlaylist + from libopensonic.media import PodcastChannel as SonicChannel from libopensonic.media import PodcastEpisode as SonicEpisode @@ -778,10 +779,33 @@ class OpenSonicProvider(MusicProvider): async def recommendations(self) -> list[RecommendationFolder]: """Provide recommendations. - These can provide favorited items, recently added albums, and most played albums. - What is included is configured with the provider. + These can provide favorited items, recently added albums, newest podcast episodes, + and most played albums. What is included is configured with the provider. """ recos: list[RecommendationFolder] = [] + + if self._enable_podcasts: + podcasts: RecommendationFolder = RecommendationFolder( + item_id="subsonic_newest_podcasts", + provider=self.domain, + name="Newest Podcast Episodes", + ) + sonic_episodes = await self._run_async( + self.conn.get_newest_podcasts, count=self._reco_limit + ) + sonic_channel: SonicChannel | None = None + for ep in sonic_episodes: + if sonic_channel is None or sonic_channel.id != ep.channel_id: + channels = await self._run_async( + self.conn.get_podcasts, inc_episodes=True, pid=ep.channel_id + ) + if not channels: + self.logger.warning("Can't find podcast channel for id %s", ep.channel_id) + continue + sonic_channel = channels[0] + podcasts.items.append(parse_epsiode(self.instance_id, ep, sonic_channel)) + recos.append(podcasts) + if self._show_faves: faves: RecommendationFolder = RecommendationFolder( item_id="subsonic_starred_albums", provider=self.domain, name="Starred Items" diff --git a/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr b/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr index 4bb485d0..df501ae2 100644 --- a/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr +++ b/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr @@ -969,7 +969,145 @@ 'description': 'The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.', 'explicit': None, 'genres': None, - 'images': None, + 'images': list([ + dict({ + 'path': 'pd-5', + 'provider': 'xx-instance-id-xx', + 'remotely_accessible': False, + 'type': 'thumb', + }), + ]), + 'label': None, + 'languages': None, + 'last_refresh': None, + 'links': None, + 'lrc_lyrics': None, + 'lyrics': None, + 'mood': None, + 'performers': None, + 'popularity': None, + 'preview': None, + 'release_date': '2012-05-06T18:18:38+00:00', + 'review': None, + 'style': None, + }), + 'name': '179- The End', + 'podcast': dict({ + 'external_ids': list([ + ]), + 'favorite': False, + 'is_playable': True, + 'item_id': 'pd-5', + 'media_type': 'podcast', + 'metadata': dict({ + 'chapters': None, + 'copyright': None, + 'description': 'A weekly podcast tracing the rise, decline and fall of the Roman Empire. Now complete!', + 'explicit': None, + 'genres': None, + 'images': list([ + dict({ + 'path': 'pd-5', + 'provider': 'xx-instance-id-xx', + 'remotely_accessible': False, + 'type': 'thumb', + }), + ]), + 'label': None, + 'languages': None, + 'last_refresh': None, + 'links': None, + 'lrc_lyrics': None, + 'lyrics': None, + 'mood': None, + 'performers': None, + 'popularity': None, + 'preview': None, + 'release_date': None, + 'review': None, + 'style': None, + }), + 'name': 'The History of Rome', + 'position': None, + 'provider': 'opensubsonic', + 'provider_mappings': list([ + dict({ + 'audio_format': dict({ + 'bit_depth': 16, + 'bit_rate': 0, + 'channels': 2, + 'codec_type': '?', + 'content_type': '?', + 'output_format_str': '?', + 'sample_rate': 44100, + }), + 'available': True, + 'details': None, + 'item_id': 'pd-5', + 'provider_domain': 'opensubsonic', + 'provider_instance': 'xx-instance-id-xx', + 'url': None, + }), + ]), + 'publisher': None, + 'sort_name': 'history of rome, the', + 'total_episodes': 5, + 'translation_key': None, + 'uri': 'http://feeds.feedburner.com/TheHistoryOfRome', + 'version': '', + }), + 'position': 5, + 'provider': 'opensubsonic', + 'provider_mappings': list([ + dict({ + 'audio_format': dict({ + 'bit_depth': 16, + 'bit_rate': 0, + 'channels': 2, + 'codec_type': '?', + 'content_type': '?', + 'output_format_str': '?', + 'sample_rate': 44100, + }), + 'available': True, + 'details': None, + 'item_id': 'pd-5$!$pe-1860', + 'provider_domain': 'opensubsonic', + 'provider_instance': 'xx-instance-id-xx', + 'url': None, + }), + ]), + 'resume_position_ms': None, + 'sort_name': '179- the end', + 'translation_key': None, + 'uri': 'opensubsonic://podcast_episode/pd-5$!$pe-1860', + 'version': '', + }) +# --- +# name: test_parse_episode[no-cover.episode] + dict({ + 'duration': 0, + 'external_ids': list([ + ]), + 'favorite': False, + 'fully_played': None, + 'is_playable': True, + 'item_id': 'pd-5$!$pe-1860', + 'media_type': 'podcast_episode', + 'metadata': dict({ + 'chapters': None, + 'copyright': None, + 'description': 'The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.', + 'explicit': None, + 'genres': None, + 'images': list([ + dict({ + 'path': 'pd-5', + 'provider': 'xx-instance-id-xx', + 'remotely_accessible': False, + 'type': 'thumb', + }), + ]), 'label': None, 'languages': None, 'last_refresh': None, @@ -1093,7 +1231,14 @@ 'description': 'The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.', 'explicit': None, 'genres': None, - 'images': None, + 'images': list([ + dict({ + 'path': 'pd-5', + 'provider': 'xx-instance-id-xx', + 'remotely_accessible': False, + 'type': 'thumb', + }), + ]), 'label': None, 'languages': None, 'last_refresh': None, diff --git a/tests/providers/opensubsonic/fixtures/episodes/no-cover.episode.json b/tests/providers/opensubsonic/fixtures/episodes/no-cover.episode.json new file mode 100644 index 00000000..93f611b1 --- /dev/null +++ b/tests/providers/opensubsonic/fixtures/episodes/no-cover.episode.json @@ -0,0 +1,17 @@ +{ + "id": "pe-1860", + "isDir": false, + "title": "179- The End", + "parent": "", + "year": 2012, + "genre": "Podcast", + "size": 15032655, + "contentType": "audio/mpeg", + "suffix": "mp3", + "path": "", + "channelId": "pd-5", + "status": "completed", + "streamId": "pe-1860", + "description": "The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.", + "publishDate": "2012-05-06T18:18:38Z" +} diff --git a/tests/providers/opensubsonic/fixtures/episodes/no-cover.podcast.json b/tests/providers/opensubsonic/fixtures/episodes/no-cover.podcast.json new file mode 100644 index 00000000..2cac9bc8 --- /dev/null +++ b/tests/providers/opensubsonic/fixtures/episodes/no-cover.podcast.json @@ -0,0 +1,101 @@ +{ + "id": "pd-5", + "url": "http://feeds.feedburner.com/TheHistoryOfRome", + "status": "skipped", + "title": "The History of Rome", + "description": "A weekly podcast tracing the rise, decline and fall of the Roman Empire. Now complete!", + "coverArt": "pd-5", + "originalImageUrl": "https://static.libsyn.com/p/assets/1/2/f/c/12fc067020662e91/THoR_Logo_1500x1500.jpg", + "episode": [ + { + "id": "pe-4805", + "isDir": false, + "title": "Ad-Free History of Rome Patreon", + "parent": "", + "year": 2024, + "genre": "Podcast", + "coverArt": "pd-5", + "size": 1717520, + "contentType": "audio/mpeg", + "suffix": "mp3", + "path": "", + "channelId": "pd-5", + "status": "completed", + "streamId": "pe-4805", + "description": "Become a patron and get the entire History of Rome backcatalog ad-fre, plus bonus content, behind the scenes peeks at the new book, plus a chat community where you can talk to me directly. Join today! Patreon: patreon.com/thehistoryofrome Merch Store: cottonbureau.com/mikeduncan", + "publishDate": "2024-11-05T02:35:00Z" + }, + { + "id": "pe-1857", + "isDir": false, + "title": "The Storm Before The Storm: Chapter 1- The Beasts of Italy", + "parent": "", + "year": 2017, + "genre": "Podcast", + "coverArt": "pd-5", + "size": 80207374, + "contentType": "audio/mpeg", + "suffix": "mp3", + "path": "", + "channelId": "pd-5", + "status": "completed", + "streamId": "pe-1857", + "description": "Audio excerpt from The Storm Before the Storm: The Beginning of the End of the Roman Republic by Mike Duncan. Forthcoming Oct. 24, 2017. Pre-order a copy today! Amazon Powells Barnes & Noble Indibound Books-a-Million Or visit us at: revolutionspodcast.com thehistoryofrome.com", + "publishDate": "2017-07-27T11:30:00Z" + }, + { + "id": "pe-1858", + "isDir": false, + "title": "Revolutions Launch", + "parent": "", + "year": 2013, + "genre": "Podcast", + "coverArt": "pd-5", + "size": 253200, + "contentType": "audio/mpeg", + "suffix": "mp3", + "path": "", + "channelId": "pd-5", + "status": "completed", + "streamId": "pe-1858", + "description": "Available at revolutionspodcast.com, iTunes, or anywhere else fine podcasts can be found.", + "publishDate": "2013-09-16T15:39:57Z" + }, + { + "id": "pe-1859", + "isDir": false, + "title": "Update- One Year Later", + "parent": "", + "year": 2013, + "genre": "Podcast", + "coverArt": "pd-5", + "size": 1588998, + "contentType": "audio/mpeg", + "suffix": "mp3", + "path": "", + "channelId": "pd-5", + "status": "completed", + "streamId": "pe-1859", + "description": "Next show coming soon!", + "publishDate": "2013-05-30T15:18:57Z" + }, + { + "id": "pe-1860", + "isDir": false, + "title": "179- The End", + "parent": "", + "year": 2012, + "genre": "Podcast", + "coverArt": "pd-5", + "size": 15032655, + "contentType": "audio/mpeg", + "suffix": "mp3", + "path": "", + "channelId": "pd-5", + "status": "completed", + "streamId": "pe-1860", + "description": "The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.", + "publishDate": "2012-05-06T18:18:38Z" + } + ] +}