/
/
/
1"""Helpers for Audiobookshelf provider."""
2
3import asyncio
4import time
5from dataclasses import dataclass, field
6
7from aioaudiobookshelf.schema.media_progress import MediaProgress
8from mashumaro.mixins.dict import DataClassDictMixin
9
10
11@dataclass(kw_only=True)
12class LibraryHelper(DataClassDictMixin):
13 """Lib name + media items' uuids."""
14
15 name: str
16 item_ids: set[str] = field(default_factory=set)
17
18
19@dataclass(kw_only=True)
20class LibrariesHelper(DataClassDictMixin):
21 """Helper class to store ABSLibrary name, id and the uuids of its media items.
22
23 Dictionary is lib_id:AbsLibraryWithItemIDs.
24 """
25
26 audiobooks: dict[str, LibraryHelper] = field(default_factory=dict)
27 podcasts: dict[str, LibraryHelper] = field(default_factory=dict)
28
29
30@dataclass(kw_only=True)
31class SessionHelper:
32 """Helper class to store some session information."""
33
34 abs_session_id: str
35 last_sync_time: float
36 hls_stream_open: asyncio.Event # only used for hls_streams, otherwise ignored
37
38
39@dataclass(kw_only=True)
40class _ProgressHelper:
41 id_: str # audiobook or podcast id
42 episode_id: str | None = None
43 last_update_ms: int # last update in ms epoch (same as last_update in abs)
44
45
46class ProgressGuard:
47 """Class used to avoid ping pong between abs and mass.
48
49 We continuously update the progress from mass to abs with the provider's on_played function.
50 We also register callbacks for progress reports from abs to mass. This is not only triggered
51 on external updates, but also on our own update. To avoid messages going back and forth, this
52 class is used.
53 """
54
55 def __init__(self) -> None:
56 """Init."""
57 self._progresses: list[_ProgressHelper] = []
58 self._max_progresses = 100
59 # 12s have to have passed before we accept an external progress update
60 # abs updates every 15 s
61 self._min_time_between_updates_ms = 12000
62
63 def _get_progress(self, item_id: str, episode_id: str | None = None) -> _ProgressHelper | None:
64 """Get a helper progress."""
65 for x in self._progresses:
66 if x.id_ == item_id and x.episode_id == episode_id:
67 return x
68 return None
69
70 def _remove_oldest(self) -> None:
71 """Remove oldest helper progress."""
72 progresses = sorted(self._progresses, key=lambda x: x.last_update_ms)
73 if len(progresses) > 0:
74 self._progresses.remove(progresses[0])
75
76 def remove_progress(self, item_id: str, episode_id: str | None = None) -> None:
77 """Remove a helper progress."""
78 progress = self._get_progress(item_id=item_id, episode_id=episode_id)
79 if progress is not None:
80 self._progresses.remove(progress)
81
82 def add_progress(self, item_id: str, episode_id: str | None = None) -> None:
83 """Store a timestamp for the last update of an audiobook or podcast episode, mass ids."""
84 if len(self._progresses) > self._max_progresses:
85 self._remove_oldest()
86 self.remove_progress(item_id=item_id, episode_id=episode_id)
87 progress = _ProgressHelper(
88 id_=item_id, episode_id=episode_id, last_update_ms=int(time.time() * 1000)
89 )
90 self._progresses.append(progress)
91
92 def guard_ok_abs(self, abs_progress: MediaProgress) -> bool:
93 """Check, if we may update against an abs media progress.
94
95 The abs media progress has a property last_update_ms, which also reflects non
96 mass external updates. Here, we compare this property against a potential
97 stored one.
98 """
99 item_id = abs_progress.library_item_id
100 episode_id = abs_progress.episode_id
101 stored_progress = self._get_progress(item_id=item_id, episode_id=episode_id)
102 if stored_progress is None:
103 return True
104 return bool(
105 abs_progress.last_update - stored_progress.last_update_ms
106 >= self._min_time_between_updates_ms
107 )
108
109 def guard_ok_mass(self, item_id: str, episode_id: str | None = None) -> bool:
110 """Check, if we may update against a mass internal item.
111
112 Here, we use the current time and compare it against the stored time.
113 """
114 stored_progress = self._get_progress(item_id=item_id, episode_id=episode_id)
115 if stored_progress is None:
116 return True
117 return (
118 int(time.time() * 1000) - stored_progress.last_update_ms
119 >= self._min_time_between_updates_ms
120 )
121