Subsonic: Display the newest podcast episodes as front page recommendation (#2242)
authorEric Munson <eric@munsonfam.org>
Mon, 23 Jun 2025 10:25:43 +0000 (06:25 -0400)
committerGitHub <noreply@github.com>
Mon, 23 Jun 2025 10:25:43 +0000 (12:25 +0200)
* 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 <eric@munsonfam.org>
* 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 <eric@munsonfam.org>
---------

Signed-off-by: Eric B Munson <eric@munsonfam.org>
music_assistant/providers/opensubsonic/parsers.py
music_assistant/providers/opensubsonic/sonic_provider.py
tests/providers/opensubsonic/__snapshots__/test_parsers.ambr
tests/providers/opensubsonic/fixtures/episodes/no-cover.episode.json [new file with mode: 0644]
tests/providers/opensubsonic/fixtures/episodes/no-cover.podcast.json [new file with mode: 0644]

index 95a1f8b0cd4e5836161678a8785204644755c0d4..406812b53f6e9c6af8e2308a99502537f8dbf48e 100644 (file)
@@ -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
index 1ffb9a2d4917e2c869e77b72b41050f4796cc1e5..c6b6cf3d06794392ff7cfb3d872555b5a16b4a41 100644 (file)
@@ -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"
index 4bb485d0c12e7e08fd35bbd99673d949f6807e2a..df501ae2f74397174e492e076513a64367666d50 100644 (file)
       '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,
       '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 (file)
index 0000000..93f611b
--- /dev/null
@@ -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 (file)
index 0000000..2cac9bc
--- /dev/null
@@ -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"
+        }
+    ]
+}