Subsonic: Better scrobbling and track enumeration (#1035)
authorEric Munson <eric@munsonfam.org>
Sat, 27 Jan 2024 17:41:51 +0000 (12:41 -0500)
committerGitHub <noreply@github.com>
Sat, 27 Jan 2024 17:41:51 +0000 (18:41 +0100)
* Subsonic: Fix stream scrobbling

get_audio_stream can be called multiple times when a track is played to
allow for MA to analyze the audio. Scrobbling at the end of the stream
was causing multiple reports when placed here. The correct way to do is
is using a callback on the StreamDetails object we create. Now we
scrobble playback when MA tells us that more than half the track has
been played.

We also now make a "Now Playing" scrobble when the StreamDetails object
is built.

Signed-off-by: Eric B Munson <eric@munsonfam.org>
* Subsonic: Better song enumeration

Rely on pagination instead of one massive request.

Signed-off-by: Eric B Munson <eric@munsonfam.org>
---------

Signed-off-by: Eric B Munson <eric@munsonfam.org>
music_assistant/server/providers/opensubsonic/sonic_provider.py

index 1d6f72ef5dd886549cde3c12e4e24036319cf8d5..00a7bc8a09c326ce045fae8b0cd7930f76bd4d23 100644 (file)
@@ -453,11 +453,28 @@ class OpenSonicProvider(MusicProvider):
 
         Note the lack of item count on this method.
         """
+        offset = 0
+        count = 500
         results = await self._run_async(
-            self._conn.search3, query="", artistCount=0, albumCount=0, songCount=999999
+            self._conn.search3,
+            query="",
+            artistCount=0,
+            albumCount=0,
+            songOffset=offset,
+            songCount=count,
         )
-        for entry in results["songs"]:
-            yield self._parse_track(entry)
+        while results["songs"]:
+            for entry in results["songs"]:
+                yield self._parse_track(entry)
+            offset += count
+            results = await self._run_async(
+                self._conn.search3,
+                query="",
+                artistCount=0,
+                albumCount=0,
+                songOffset=offset,
+                songCount=count,
+            )
 
     async def get_album(self, prov_album_id: str) -> Album:
         """Return the requested Album."""
@@ -572,16 +589,28 @@ class OpenSonicProvider(MusicProvider):
             sonic_song: SonicSong = await self._run_async(self._conn.getSong, item_id)
         except (ParameterError, DataNotFoundError) as e:
             raise MediaNotFoundError(f"Item {item_id} not found") from e
+
+        self.mass.create_task(self._report_playback_started(item_id))
+
         mime_type = sonic_song.content_type
         if mime_type.endswith("mpeg"):
             mime_type = sonic_song.suffix
+
         return StreamDetails(
             item_id=sonic_song.id,
             provider=self.instance_id,
             audio_format=AudioFormat(content_type=ContentType.try_parse(mime_type)),
             duration=sonic_song.duration if sonic_song.duration is not None else 0,
+            callback=self._report_playback_stopped,
         )
 
+    async def _report_playback_started(self, item_id: str) -> None:
+        await self._run_async(self._conn.scrobble, sid=item_id, submission=False)
+
+    async def _report_playback_stopped(self, streamdetails: StreamDetails) -> None:
+        if streamdetails.seconds_streamed >= streamdetails.duration / 2:
+            await self._run_async(self._conn.scrobble, sid=streamdetails.item_id, submission=True)
+
     async def get_audio_stream(
         self, streamdetails: StreamDetails, seek_position: int = 0
     ) -> AsyncGenerator[bytes, None]:
@@ -606,7 +635,6 @@ class OpenSonicProvider(MusicProvider):
                 # keep reading from the audio buffer until there is no more data
                 chunk = await audio_buffer.get()
                 if chunk == b"":
-                    await self._run_async(self._conn.scrobble, streamdetails.item_id)
                     break
                 yield chunk
         finally: