)
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."""
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
__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
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