feat(scrobble): submit final scrobble on playback finish with listen threshold

This commit is contained in:
joelilas
2026-01-26 19:25:33 +01:00
parent c1952b0002
commit 5ed89f3fc5
2 changed files with 35 additions and 1 deletions

View File

@@ -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)

View File

@@ -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