Fix resume position handling
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 17 Jan 2025 15:57:31 +0000 (16:57 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 17 Jan 2025 15:57:31 +0000 (16:57 +0100)
music_assistant/controllers/music.py
music_assistant/controllers/player_queues.py
music_assistant/controllers/players.py
music_assistant/models/music_provider.py

index 1d28ee6db29b7c99cc653c36f209bd19f70d2195..164f57110df5ec2953752cb551c056b0bf60f62a 100644 (file)
@@ -798,13 +798,15 @@ class MusicController(CoreController):
 
         # forward to provider(s) to sync resume state (e.g. for audiobooks)
         for prov_mapping in media_item.provider_mappings:
+            if fully_played is None:
+                fully_played = True
             if music_prov := self.mass.get_provider(prov_mapping.provider_instance):
                 self.mass.create_task(
                     music_prov.on_played(
                         media_type=media_item.media_type,
                         item_id=prov_mapping.item_id,
-                        fully_played=False,
-                        position=0,
+                        fully_played=fully_played,
+                        position=seconds_played,
                     )
                 )
 
index 8aa2198f86382f94dcda0c975cd2d7e16159fea8..dc52381f54127283062b799f4c39dae9e6ba98b3 100644 (file)
@@ -934,6 +934,8 @@ class PlayerQueuesController(CoreController):
                 queue.elapsed_time = int(player.corrected_elapsed_time or 0)
                 if item_id := self._parse_player_current_item_id(queue_id, player):
                     queue.current_index = self.index_by_id(queue_id, item_id)
+                else:
+                    queue.current_index = None
             # generic attributes we update when player is playing
             queue.state = PlayerState.PLAYING
             queue.elapsed_time_last_updated = time.time()
@@ -1012,74 +1014,82 @@ class PlayerQueuesController(CoreController):
 
         # detect change in current index to report that a item has been played
         prev_item_id = prev_state["current_item_id"]
+        cur_item_id = new_state["current_item_id"]
         player_stopped = (
-            prev_state["state"] == PlayerState.PLAYING and new_state["state"] == PlayerState.IDLE
+            prev_state["state"] in (PlayerState.PLAYING, PlayerState.PAUSED)
+            and new_state["state"] == PlayerState.IDLE
         )
+        track_changed = not player_stopped and prev_item_id and prev_item_id != cur_item_id
+        prev_item = self.get_item(queue_id, prev_item_id)
         end_of_queue_reached = (
-            player_stopped and queue.current_item is not None and queue.next_item is None
-        )
-        if (
-            prev_item_id is not None
-            and (prev_item_id != new_state["current_item_id"] or player_stopped)
-            and (prev_item := self.get_item(queue_id, prev_item_id))
+            player_stopped
+            and queue.next_item is None
+            and prev_item is not None
             and (stream_details := prev_item.streamdetails)
-        ):
+            and int(prev_state["elapsed_time"]) >= (stream_details.duration or 3600) - 5
+        )
+        if prev_item and (player_stopped or track_changed or end_of_queue_reached):
             position = int(prev_state["elapsed_time"])
-            seconds_played = int(prev_state["elapsed_time"]) - stream_details.seek_position
+            prev_item = self.get_item(queue_id, prev_item_id)
+            stream_details = prev_item.streamdetails if prev_item else None
+            seconds_played = (
+                int(prev_state["elapsed_time"]) - stream_details.seek_position
+                if stream_details
+                else 0
+            )
             fully_played = position >= (stream_details.duration or 3600) - 5
             self.logger.debug(
-                "PlayerQueue %s played item %s for %s seconds",
+                "PlayerQueue %s played item %s for %s seconds - fully_played: %s - progress: %s",
                 queue.display_name,
                 prev_item.uri,
                 seconds_played,
+                fully_played,
+                prev_state["elapsed_time"],
             )
-            if prev_item.media_item and (fully_played or seconds_played > 10):
-                # add entry to playlog - this also handles resume of podcasts/audiobooks
-                self.mass.create_task(
-                    self.mass.music.mark_item_played(
-                        prev_item.media_item,
-                        fully_played=fully_played,
-                        seconds_played=seconds_played,
-                    )
-                )
-                # signal 'media item played' event,
-                # which is useful for plugins that want to do scrobbling
-                self.mass.signal_event(
-                    EventType.MEDIA_ITEM_PLAYED,
-                    object_id=prev_item.media_item.uri,
-                    data={
-                        # TODO: Maybe we should create a dataclass for this as well?!
-                        "media_item": {
-                            "uri": prev_item.media_item.uri,
-                            "name": prev_item.media_item.name,
-                            "media_type": prev_item.media_item.media_type,
-                            "artist": getattr(prev_item.media_item, "artist_str", None),
-                            "album": album.name
-                            if (album := getattr(prev_item.media_item, "album", None))
-                            else None,
-                            "image_url": self.mass.metadata.get_image_url(
-                                prev_item.media_item.image, size=512
-                            )
-                            if prev_item.media_item.image
-                            else None,
-                            "duration": getattr(prev_item.media_item, "duration", 0),
-                            "mbid": getattr(prev_item.media_item, "mbid", None),
-                        },
-                        "seconds_played": seconds_played,
-                        "fully_played": fully_played,
-                    },
+            # add entry to playlog - this also handles resume of podcasts/audiobooks
+            self.mass.create_task(
+                self.mass.music.mark_item_played(
+                    prev_item.media_item,
+                    fully_played=fully_played,
+                    seconds_played=prev_state["elapsed_time"],
                 )
-
-        if end_of_queue_reached:
-            # end of queue reached, clear items
-            self.logger.debug(
-                "PlayerQueue %s reached end of queue...",
-                queue.display_name,
             )
-            self.mass.call_later(
-                5, self._check_clear_queue, queue, task_id=f"clear_queue_{queue_id}"
+            # signal 'media item played' event,
+            # which is useful for plugins that want to do scrobbling
+            self.mass.signal_event(
+                EventType.MEDIA_ITEM_PLAYED,
+                object_id=prev_item.media_item.uri,
+                data={
+                    # TODO: Maybe we should create a dataclass for this as well?!
+                    "media_item": {
+                        "uri": prev_item.media_item.uri,
+                        "name": prev_item.media_item.name,
+                        "media_type": prev_item.media_item.media_type,
+                        "artist": getattr(prev_item.media_item, "artist_str", None),
+                        "album": album.name
+                        if (album := getattr(prev_item.media_item, "album", None))
+                        else None,
+                        "image_url": self.mass.metadata.get_image_url(
+                            prev_item.media_item.image, size=512
+                        )
+                        if prev_item.media_item.image
+                        else None,
+                        "duration": getattr(prev_item.media_item, "duration", 0),
+                        "mbid": getattr(prev_item.media_item, "mbid", None),
+                    },
+                    "seconds_played": seconds_played,
+                    "fully_played": fully_played,
+                },
             )
 
+        if (
+            end_of_queue_reached
+            and queue.current_index is not None
+            and queue.current_item is not None
+        ):
+            # end of queue reached
+            self.mass.create_task(self._check_clear_queue(queue))
+
         # watch dynamic radio items refill if needed
         if "current_item_id" in changed_keys:
             # auto enable radio mode if dont stop the music is enabled
@@ -1348,7 +1358,7 @@ class PlayerQueuesController(CoreController):
             CONF_DEFAULT_ENQUEUE_SELECT_ARTIST,
             ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE,
         )
-        self.logger.debug(
+        self.logger.info(
             "Fetching tracks to play for artist %s",
             artist.name,
         )
@@ -1387,7 +1397,7 @@ class PlayerQueuesController(CoreController):
         )
         result: list[Track] = []
         start_item_found = False
-        self.logger.debug(
+        self.logger.info(
             "Fetching tracks to play for album %s",
             album.name,
         )
@@ -1409,7 +1419,7 @@ class PlayerQueuesController(CoreController):
         """Return tracks for given playlist, based on user preference."""
         result: list[Track] = []
         start_item_found = False
-        self.logger.debug(
+        self.logger.info(
             "Fetching tracks to play for playlist %s",
             playlist.name,
         )
index 9de7315154f20f49a594fd41bb891e94d886791b..b2498c90d91ed4113c21cac83bb425e8696f4908 100644 (file)
@@ -962,6 +962,11 @@ class PlayerController(CoreController):
         self.mass.player_queues.on_player_update(player, changed_values)
 
         if len(changed_values) == 0 and not force_update:
+            # nothing changed
+            return
+
+        if changed_values.keys() == {"elapsed_time"} and not force_update:
+            # ignore elapsed_time only changes
             return
 
         # handle DSP reload of the leader when on grouping and ungrouping
@@ -970,7 +975,7 @@ class PlayerController(CoreController):
         is_player_group = player.provider.startswith("player_group")
 
         # handle special case for PlayerGroups: since there are no leaders,
-        # DSP still always works with a single player in the group.
+        # DSP still always work with a single player in the group.
         multi_device_dsp_threshold = 1 if is_player_group else 0
 
         prev_is_multiple_devices = prev_child_count > multi_device_dsp_threshold
@@ -1005,17 +1010,16 @@ class PlayerController(CoreController):
                 # - the leader has DSP enabled
                 self.mass.create_task(self.mass.players.on_player_dsp_change(player_id))
 
-        if changed_values.keys() != {"elapsed_time"} or force_update:
-            # ignore elapsed_time only changes
-            self.mass.signal_event(EventType.PLAYER_UPDATED, object_id=player_id, data=player)
-
-        if skip_forward and not force_update:
-            return
+        # signal player update on the eventbus
+        self.mass.signal_event(EventType.PLAYER_UPDATED, object_id=player_id, data=player)
 
         # handle player becoming unavailable
         if "available" in changed_values and not player.available:
             self._handle_player_unavailable(player)
 
+        if skip_forward and not force_update:
+            return
+
         # update/signal group player(s) child's when group updates
         for child_player in self.iter_group_members(player, exclude_self=True):
             self.update(child_player.player_id, skip_forward=True)
index 8ed2131b5f6db1c6739ed1659edcb166417aac8a..49fa8862c05338988c7922f5efbbaed9a753ddc3 100644 (file)
@@ -621,11 +621,31 @@ class MusicProvider(Provider):
                         library_item = await controller.update_item_in_library(
                             library_item.item_id, prov_item
                         )
-                    elif library_item.available != prov_item.available:
+                    if library_item.available != prov_item.available:
                         # existing item availability changed
                         library_item = await controller.update_item_in_library(
                             library_item.item_id, prov_item
                         )
+                    if (
+                        getattr(library_item, "resume_position_ms", None)
+                        != (resume_pos_prov := getattr(prov_item, "resume_position_ms", None))
+                        and resume_pos_prov is not None
+                    ):
+                        # resume_position_ms changed (audiobook only)
+                        library_item.resume_position_ms = resume_pos_prov
+                        library_item = await controller.update_item_in_library(
+                            library_item.item_id, prov_item
+                        )
+                    if (
+                        getattr(library_item, "fully_played", None)
+                        != (fully_played_prov := getattr(prov_item, "fully_played", None))
+                        and fully_played_prov is not None
+                    ):
+                        # fully_played changed (audiobook only)
+                        library_item.fully_played = fully_played_prov
+                        library_item = await controller.update_item_in_library(
+                            library_item.item_id, prov_item
+                        )
                     cur_db_ids.add(library_item.item_id)
                     await asyncio.sleep(0)  # yield to eventloop
                 except MusicAssistantError as err: