abs: report correct time_listened in sessions (#3163)
authorFabian Munkes <105975993+fmunkes@users.noreply.github.com>
Mon, 16 Feb 2026 17:30:04 +0000 (18:30 +0100)
committerGitHub <noreply@github.com>
Mon, 16 Feb 2026 17:30:04 +0000 (18:30 +0100)
* abs sessions statistics

* small progress sync fix

music_assistant/constants.py
music_assistant/controllers/player_queues.py
music_assistant/providers/audiobookshelf/__init__.py

index f82c797d50062326d2799643482425998270693f..c052dcabc740c0f839a2d1561470115a7034a8a6 100644 (file)
@@ -939,6 +939,9 @@ SOUNDTRACK_INDICATORS = [
     r"\boriginal.*cast.*recording\b",
 ]
 
+# how often we report the playback progress in the player_queues controller
+PLAYBACK_REPORT_INTERVAL_SECONDS = 30
+
 # List of providers that do not use HTTP streaming
 # but consume raw audio data over other protocols
 # for provider domains in this list, we won't show the default
index 620f22a6054a004dfbc3d4d6fae056c6d13c22ea..3dd3a8270c961d2b8b3264a4956b454e98eba611 100644 (file)
@@ -64,6 +64,7 @@ from music_assistant_models.queue_item import QueueItem
 from music_assistant.constants import (
     ATTR_ANNOUNCEMENT_IN_PROGRESS,
     MASS_LOGO_ONLINE,
+    PLAYBACK_REPORT_INTERVAL_SECONDS,
     PLAYLIST_MEDIA_TYPES,
     VERBOSE_LOG_LEVEL,
     PlaylistPlayableItem,
@@ -2315,7 +2316,7 @@ class PlayerQueuesController(CoreController):
         # we do this every 30 seconds or when the state changes
         if (
             changed_keys.intersection({"state", "current_item_id"})
-            or int(queue.elapsed_time) % 30 == 0
+            or int(queue.elapsed_time) % PLAYBACK_REPORT_INTERVAL_SECONDS == 0
         ):
             self._handle_playback_progress_report(queue, prev_state, new_state)
 
index 4b6415a12557b4432005b3056850c5e2559d7fb4..01199c57f5f56c552ac9a6fc26eecd08e40e7c01 100644 (file)
@@ -71,6 +71,7 @@ from music_assistant_models.media_items import (
 from music_assistant_models.media_items.media_item import RecommendationFolder
 from music_assistant_models.streamdetails import MultiPartPath, StreamDetails
 
+from music_assistant.constants import PLAYBACK_REPORT_INTERVAL_SECONDS
 from music_assistant.models.music_provider import MusicProvider
 from music_assistant.providers.audiobookshelf.parsers import (
     parse_audiobook,
@@ -664,6 +665,8 @@ for more details.
         async with self.create_session_lock:
             # check for an available open session
             if session_helper := self.sessions.get(mass_item_id):
+                # reset here, as this is our "time listened".
+                session_helper.last_sync_time = time.time()
                 with suppress(AbsSessionNotFoundError):
                     return await self._client.get_open_session(
                         session_id=session_helper.abs_session_id
@@ -774,7 +777,7 @@ for more details.
         # this method is called _before_ get_stream_details, so the playback session
         # is created here.
         session = await self._get_playback_session(mass_item_id=item_id)
-        finished = session.current_time > session.duration - 30
+        finished = session.current_time > session.duration - PLAYBACK_REPORT_INTERVAL_SECONDS
         self.logger.debug("Resume position: obtained.")
         return finished, int(session.current_time * 1000)
 
@@ -1007,12 +1010,20 @@ for more details.
 
         async def _update_by_session(session_helper: SessionHelper, duration: int) -> bool:
             now = time.time()
+            time_listened = now - session_helper.last_sync_time
+            if time_listened > PLAYBACK_REPORT_INTERVAL_SECONDS + 3:
+                # See player_queues controller, we get an update every 30s, and immediately on pause
+                # or play.
+                # We reset above 33, as this indicates a trigger after a longer absence and should
+                # not count into abs' statistics
+                self.logger.debug("Resetting time_listened due to longer absence.")
+                time_listened = 0.0
             try:
                 await self._client.sync_open_session(
                     session_id=session_helper.abs_session_id,
                     parameters=SyncOpenSessionParameters(
                         current_time=position,
-                        time_listened=now - session_helper.last_sync_time,
+                        time_listened=time_listened,
                         duration=duration,
                     ),
                 )
@@ -1036,7 +1047,7 @@ for more details.
             if media_item is None or not isinstance(media_item, PodcastEpisode):
                 return
 
-            if fully_played and position < media_item.duration - 30:
+            if fully_played and position < media_item.duration - PLAYBACK_REPORT_INTERVAL_SECONDS:
                 # faulty position update
                 # occurs sometimes, if a player disconnects unexpectedly, or reports
                 # a false position - seen this for MC players, but not for sendspin
@@ -1077,7 +1088,7 @@ for more details.
             if media_item is None or not isinstance(media_item, Audiobook):
                 return
 
-            if fully_played and position < media_item.duration - 30:
+            if fully_played and position < media_item.duration - PLAYBACK_REPORT_INTERVAL_SECONDS:
                 # faulty position update, see above
                 return
 
@@ -1591,7 +1602,10 @@ for more details.
             if not self.progress_guard.guard_ok_abs(progress):
                 continue
             if progress.current_time is not None:
-                if int(progress.current_time) != 0 and not progress.current_time >= 30:
+                if (
+                    int(progress.current_time) != 0
+                    and not progress.current_time >= PLAYBACK_REPORT_INTERVAL_SECONDS
+                ):
                     # same as mass default, only > 30s
                     continue
             if progress.library_item_id not in known_ids:
@@ -1642,7 +1656,7 @@ for more details.
         )
         if mass_audiobook is None:
             return
-        if int(progress.current_time) == 0:
+        if int(progress.current_time) == 0 and not progress.is_finished:
             await self.mass.music.mark_item_unplayed(mass_audiobook)
         else:
             await self.mass.music.mark_item_played(
@@ -1663,7 +1677,7 @@ for more details.
             mass_episode = await self.get_podcast_episode(_episode_id, add_progress=False)
         except MediaNotFoundError:
             return
-        if int(progress.current_time) == 0:
+        if int(progress.current_time) == 0 and not progress.is_finished:
             await self.mass.music.mark_item_unplayed(mass_episode)
         else:
             await self.mass.music.mark_item_played(