From: Marcel van der Veldt Date: Fri, 17 Jan 2025 15:57:31 +0000 (+0100) Subject: Fix resume position handling X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=2c738ae6ab27a430185a5e364ce6f21ab8efbcae;p=music-assistant-server.git Fix resume position handling --- diff --git a/music_assistant/controllers/music.py b/music_assistant/controllers/music.py index 1d28ee6d..164f5711 100644 --- a/music_assistant/controllers/music.py +++ b/music_assistant/controllers/music.py @@ -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, ) ) diff --git a/music_assistant/controllers/player_queues.py b/music_assistant/controllers/player_queues.py index 8aa2198f..dc52381f 100644 --- a/music_assistant/controllers/player_queues.py +++ b/music_assistant/controllers/player_queues.py @@ -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, ) diff --git a/music_assistant/controllers/players.py b/music_assistant/controllers/players.py index 9de73151..b2498c90 100644 --- a/music_assistant/controllers/players.py +++ b/music_assistant/controllers/players.py @@ -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) diff --git a/music_assistant/models/music_provider.py b/music_assistant/models/music_provider.py index 8ed2131b..49fa8862 100644 --- a/music_assistant/models/music_provider.py +++ b/music_assistant/models/music_provider.py @@ -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: