From 5ed89f3fc52ac4eed261fe2ca6684588d3bd1851 Mon Sep 17 00:00:00 2001 From: joelilas Date: Mon, 26 Jan 2026 19:25:33 +0100 Subject: [PATCH] feat(scrobble): submit final scrobble on playback finish with listen threshold --- player.py | 32 ++++++++++++++++++++++++++++++++ subsonic.py | 4 +++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/player.py b/player.py index 29c12d0..574d3ac 100644 --- a/player.py +++ b/player.py @@ -1,6 +1,7 @@ ''' A player object that handles playback and data for its respective guild ''' import asyncio +import time import discord import data @@ -16,6 +17,7 @@ _default_data: dict[str, any] = { "current-song": None, "current-position": 0, "queue": [], + "current-start-ms": None, } class Player(): @@ -62,6 +64,16 @@ class Player(): self._player_loop = loop + @property + def current_start_ms(self) -> int | None: + ''' The epoch time in ms when current song playback started ''' + return self._data["current-start-ms"] + + @current_start_ms.setter + def current_start_ms(self, value: int | None) -> None: + self._data["current-start-ms"] = value + + @@ -127,6 +139,24 @@ class Player(): return logger.debug("Playback finished.") + # Attempt a final scrobble submission if sufficient playback occurred + try: + if self.current_start_ms is not None and self.current_song is not None: + now_ms = int(time.time() * 1000) + duration_ms = int(self.current_song.duration) * 1000 + threshold_ms = min(240_000, duration_ms // 2) + played_ms = now_ms - int(self.current_start_ms) + if played_ms >= threshold_ms: + logger.info( + f"Submitting scrobble for {self.current_song.title} (played {played_ms}ms, threshold {threshold_ms}ms)" + ) + asyncio.create_task(scrobble(self.current_song.song_id, submission=True, timestamp=int(self.current_start_ms // 1000))) + else: + logger.info( + f"Skipping scrobble for {self.current_song.title} (played {played_ms}ms < threshold {threshold_ms}ms)" + ) + except Exception as e: + logger.error(f"Failed to submit final scrobble: {e}") try: # Only proceed if voice client is still connected if voice_client and voice_client.is_connected(): @@ -150,6 +180,8 @@ class Player(): await ui.ErrMsg.msg(interaction, "Voice connection was lost. Please try again.") return + # Mark playback start time (ms) and start playing + self.current_start_ms = int(time.time() * 1000) voice_client.play(audio_src, after=lambda e: loop.create_task(playback_finished(e))) logger.info(f"Started playing: {song.title} by {song.artist}") # Fire-and-forget scrobble (now-playing ping with submission=False) diff --git a/subsonic.py b/subsonic.py index 290498e..2ecd887 100644 --- a/subsonic.py +++ b/subsonic.py @@ -586,7 +586,9 @@ async def scrobble(song_id: str, *, submission: bool=True, timestamp: int=None) "submission": "true" if submission else "false", } if timestamp is not None: - scrobble_params["time"] = str(timestamp) + # Subsonic expects epoch time in milliseconds + time_ms = int(timestamp) * 1000 + scrobble_params["time"] = str(time_ms) params = SUBSONIC_REQUEST_PARAMS | scrobble_params