From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:07:39 +0000 (+0100) Subject: fix: abs - discarded progress (#2598) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=bc39fb4a570e1203d4ac173f42ad0e8c824a5ab6;p=music-assistant-server.git fix: abs - discarded progress (#2598) --- diff --git a/music_assistant/controllers/music.py b/music_assistant/controllers/music.py index 29493bc0..8084501b 100644 --- a/music_assistant/controllers/music.py +++ b/music_assistant/controllers/music.py @@ -517,6 +517,40 @@ class MusicController(CoreController): ) return result + async def get_playlog_provider_item_ids( + self, provider_instance_id: str, limit: int = 0 + ) -> list[tuple[MediaType, str]]: + """Return a list of MediaType and provider_item_id of items in playlog of provider.""" + query = ( + f"SELECT * FROM {DB_TABLE_PLAYLOG} " + "WHERE media_type in ('audiobook', 'podcast_episode') " + f"AND provider in ('library','{provider_instance_id}')" + ) + assert self.mass.music.database is not None # for type checking + db_rows = await self.mass.music.database.get_rows_from_query(query, limit=limit) + + result: list[tuple[MediaType, str]] = [] + for db_row in db_rows: + if db_row["provider"] == "library": + # If the provider is library, we need to make sure that the item + # is part of the passed provider_instance_id. + # A podcast_episode cannot be in the provider_mappings + # so these entries must be audiobooks. + subquery = ( + f"SELECT * FROM {DB_TABLE_PROVIDER_MAPPINGS} " + f"WHERE media_type = 'audiobook' AND item_id = {db_row['item_id']} " + f"AND provider_instance = '{provider_instance_id}'" + ) + subrow = await self.mass.music.database.get_rows_from_query(subquery) + if len(subrow) != 1: + continue + result.append((MediaType.AUDIOBOOK, subrow[0]["provider_item_id"])) + continue + # non library - item id is provider_item_id + result.append((MediaType(db_row["media_type"]), db_row["item_id"])) + + return result + @api_command("music/item_by_uri") async def get_item_by_uri(self, uri: str) -> MediaItemType | BrowseFolder: """Fetch MediaItem by uri.""" diff --git a/music_assistant/providers/audiobookshelf/__init__.py b/music_assistant/providers/audiobookshelf/__init__.py index 35f16ef4..8fe35228 100644 --- a/music_assistant/providers/audiobookshelf/__init__.py +++ b/music_assistant/providers/audiobookshelf/__init__.py @@ -6,6 +6,7 @@ import functools import itertools import time from collections.abc import AsyncGenerator, Callable, Coroutine, Sequence +from contextlib import suppress from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast import aioaudiobookshelf as aioabs @@ -1430,8 +1431,17 @@ for more details. __updated_items = 0 known_ids = self._get_all_known_item_ids() + abs_ids_with_progress = set() for progress in progresses: + # save progress ids for later + ma_item_id = ( + progress.library_item_id + if progress.episode_id is None + else f"{progress.library_item_id} {progress.episode_id}" + ) + abs_ids_with_progress.add(ma_item_id) + # Guard. Also makes sure, that we don't write to db again if no state change happened. # This is achieved by adding a Helper Progress in the update playlog functions, which # then has the most recent timestamp. If a subsequent progress sent by abs has an older @@ -1450,6 +1460,32 @@ for more details. await self._update_playlog_episode(progress) self.logger.debug(f"Updated {__updated_items} from full playlog.") + # Get MA's known progresses of ABS. + # In ABS the user may discard a progress, which removes the progress completely. + # There is no socket notification for this event. + ma_playlog_state = await self.mass.music.get_playlog_provider_item_ids( + provider_instance_id=self.instance_id + ) + ma_ids_with_progress = {x for _, x in ma_playlog_state} + discarded_progress_ids = ma_ids_with_progress.difference(abs_ids_with_progress) + for discarded_progress_id in discarded_progress_ids: + if len(discarded_progress_id.split(" ")) == 1: + if discarded_item := await self.mass.music.get_library_item_by_prov_id( + media_type=MediaType.AUDIOBOOK, + item_id=discarded_progress_id, + provider_instance_id_or_domain=self.lookup_key, + ): + self.progress_guard.add_progress(discarded_progress_id) + await self.mass.music.mark_item_unplayed(discarded_item) + else: + with suppress(MediaNotFoundError): + discarded_item = await self.get_podcast_episode( + prov_episode_id=discarded_progress_id, add_progress=False + ) + self.progress_guard.add_progress(*discarded_progress_id.split(" ")) + await self.mass.music.mark_item_unplayed(discarded_item) + self.logger.debug("Discarded item %s ", discarded_progress_id) + async def _update_playlog_book(self, progress: MediaProgress) -> None: # helper progress also ensures no useless progress updates, # see comment above