aioaudiobookshelf 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aioaudiobookshelf might be problematic. Click here for more details.
- aioaudiobookshelf/__init__.py +72 -0
- aioaudiobookshelf/client/__init__.py +189 -0
- aioaudiobookshelf/client/_base.py +110 -0
- aioaudiobookshelf/client/authors.py +34 -0
- aioaudiobookshelf/client/collections_.py +28 -0
- aioaudiobookshelf/client/items.py +108 -0
- aioaudiobookshelf/client/libraries.py +173 -0
- aioaudiobookshelf/client/me.py +103 -0
- aioaudiobookshelf/client/playlists.py +29 -0
- aioaudiobookshelf/client/podcasts.py +22 -0
- aioaudiobookshelf/client/series.py +27 -0
- aioaudiobookshelf/client/session.py +35 -0
- aioaudiobookshelf/exceptions.py +13 -0
- aioaudiobookshelf/helpers.py +56 -0
- aioaudiobookshelf/schema/__init__.py +14 -0
- aioaudiobookshelf/schema/audio.py +71 -0
- aioaudiobookshelf/schema/author.py +46 -0
- aioaudiobookshelf/schema/book.py +125 -0
- aioaudiobookshelf/schema/calls_authors.py +33 -0
- aioaudiobookshelf/schema/calls_collections.py +14 -0
- aioaudiobookshelf/schema/calls_items.py +53 -0
- aioaudiobookshelf/schema/calls_library.py +103 -0
- aioaudiobookshelf/schema/calls_login.py +28 -0
- aioaudiobookshelf/schema/calls_me.py +28 -0
- aioaudiobookshelf/schema/calls_playlists.py +14 -0
- aioaudiobookshelf/schema/calls_series.py +25 -0
- aioaudiobookshelf/schema/calls_session.py +17 -0
- aioaudiobookshelf/schema/collection.py +36 -0
- aioaudiobookshelf/schema/events_socket.py +46 -0
- aioaudiobookshelf/schema/file.py +22 -0
- aioaudiobookshelf/schema/folder.py +18 -0
- aioaudiobookshelf/schema/library.py +221 -0
- aioaudiobookshelf/schema/media_progress.py +42 -0
- aioaudiobookshelf/schema/playlist.py +74 -0
- aioaudiobookshelf/schema/podcast.py +129 -0
- aioaudiobookshelf/schema/series.py +45 -0
- aioaudiobookshelf/schema/series_books.py +34 -0
- aioaudiobookshelf/schema/server.py +76 -0
- aioaudiobookshelf/schema/session.py +78 -0
- aioaudiobookshelf/schema/user.py +81 -0
- aioaudiobookshelf-0.1.0.dist-info/LICENSE +201 -0
- aioaudiobookshelf-0.1.0.dist-info/METADATA +32 -0
- aioaudiobookshelf-0.1.0.dist-info/RECORD +45 -0
- aioaudiobookshelf-0.1.0.dist-info/WHEEL +5 -0
- aioaudiobookshelf-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Calls to /api/libraries."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
from mashumaro.mixins.json import DataClassJSONMixin
|
|
7
|
+
|
|
8
|
+
from aioaudiobookshelf.client._base import BaseClient
|
|
9
|
+
from aioaudiobookshelf.schema.author import AuthorExpanded, Narrator
|
|
10
|
+
from aioaudiobookshelf.schema.calls_library import (
|
|
11
|
+
AllLibrariesResponse,
|
|
12
|
+
LibraryAuthorsResponse,
|
|
13
|
+
LibraryCollectionsMinifiedResponse,
|
|
14
|
+
LibraryItemsMinifiedResponse,
|
|
15
|
+
LibraryNarratorsResponse,
|
|
16
|
+
LibraryPlaylistsResponse,
|
|
17
|
+
LibrarySeriesMinifiedResponse,
|
|
18
|
+
LibraryWithFilterDataResponse,
|
|
19
|
+
)
|
|
20
|
+
from aioaudiobookshelf.schema.library import Library, LibraryFilterData
|
|
21
|
+
|
|
22
|
+
ResponseMinified = TypeVar("ResponseMinified", bound=DataClassJSONMixin)
|
|
23
|
+
ResponseNormal = TypeVar("ResponseNormal", bound=DataClassJSONMixin)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LibrariesClient(BaseClient):
|
|
27
|
+
"""LibrariesClient."""
|
|
28
|
+
|
|
29
|
+
# create library
|
|
30
|
+
|
|
31
|
+
async def get_all_libraries(self) -> list[Library]:
|
|
32
|
+
"""Get all user accessible libraries."""
|
|
33
|
+
response = await self._get("/api/libraries")
|
|
34
|
+
return AllLibrariesResponse.from_json(response).libraries
|
|
35
|
+
|
|
36
|
+
async def get_library(self, *, library_id: str) -> Library:
|
|
37
|
+
"""Get single library."""
|
|
38
|
+
response = await self._get(f"/api/libraries/{library_id}")
|
|
39
|
+
return Library.from_json(response)
|
|
40
|
+
|
|
41
|
+
async def get_library_with_filterdata(
|
|
42
|
+
self, *, library_id: str
|
|
43
|
+
) -> LibraryWithFilterDataResponse:
|
|
44
|
+
"""Get single library including filterdata."""
|
|
45
|
+
response = await self._get(f"/api/libraries/{library_id}?include=filterdata")
|
|
46
|
+
return LibraryWithFilterDataResponse.from_json(response)
|
|
47
|
+
|
|
48
|
+
# update library
|
|
49
|
+
# delete library
|
|
50
|
+
|
|
51
|
+
async def _get_library_with_pagination(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
endpoint: str,
|
|
55
|
+
minified: bool = False,
|
|
56
|
+
response_cls_minified: type[ResponseMinified],
|
|
57
|
+
response_cls: type[ResponseNormal],
|
|
58
|
+
filter_str: str | None = None,
|
|
59
|
+
) -> AsyncGenerator[ResponseMinified | ResponseNormal]:
|
|
60
|
+
page_cnt = 0
|
|
61
|
+
params: dict[str, int | str] = {
|
|
62
|
+
"minified": int(minified),
|
|
63
|
+
"limit": self.session_config.pagination_items_per_page,
|
|
64
|
+
}
|
|
65
|
+
if filter_str is not None:
|
|
66
|
+
params["filter"] = filter_str
|
|
67
|
+
while True:
|
|
68
|
+
params["page"] = page_cnt
|
|
69
|
+
response = await self._get(endpoint, params)
|
|
70
|
+
page_cnt += 1
|
|
71
|
+
if minified:
|
|
72
|
+
yield response_cls_minified.from_json(response)
|
|
73
|
+
else:
|
|
74
|
+
yield response_cls.from_json(response)
|
|
75
|
+
|
|
76
|
+
async def get_library_items(
|
|
77
|
+
self, *, library_id: str, filter_str: str | None = None
|
|
78
|
+
) -> AsyncGenerator[LibraryItemsMinifiedResponse]:
|
|
79
|
+
"""Get library items.
|
|
80
|
+
|
|
81
|
+
Returns only minified items at this point.
|
|
82
|
+
"""
|
|
83
|
+
# only minified response is supported at API
|
|
84
|
+
minified: bool = True
|
|
85
|
+
endpoint = f"/api/libraries/{library_id}/items"
|
|
86
|
+
async for result in self._get_library_with_pagination(
|
|
87
|
+
endpoint=endpoint,
|
|
88
|
+
minified=minified,
|
|
89
|
+
response_cls_minified=LibraryItemsMinifiedResponse,
|
|
90
|
+
response_cls=LibraryItemsMinifiedResponse,
|
|
91
|
+
filter_str=filter_str,
|
|
92
|
+
):
|
|
93
|
+
yield result
|
|
94
|
+
|
|
95
|
+
# remove item with issues
|
|
96
|
+
# get lib podcast episode downloads
|
|
97
|
+
|
|
98
|
+
async def get_library_series(
|
|
99
|
+
self, *, library_id: str
|
|
100
|
+
) -> AsyncGenerator[LibrarySeriesMinifiedResponse]:
|
|
101
|
+
"""Get series in that library.
|
|
102
|
+
|
|
103
|
+
Returns only minified items at this point.
|
|
104
|
+
"""
|
|
105
|
+
# only minified response is supported
|
|
106
|
+
minified: bool = True
|
|
107
|
+
endpoint = f"/api/libraries/{library_id}/series"
|
|
108
|
+
async for result in self._get_library_with_pagination(
|
|
109
|
+
endpoint=endpoint,
|
|
110
|
+
minified=minified,
|
|
111
|
+
response_cls=LibrarySeriesMinifiedResponse,
|
|
112
|
+
response_cls_minified=LibrarySeriesMinifiedResponse,
|
|
113
|
+
):
|
|
114
|
+
yield result
|
|
115
|
+
|
|
116
|
+
async def get_library_collections(
|
|
117
|
+
self, *, library_id: str
|
|
118
|
+
) -> AsyncGenerator[LibraryCollectionsMinifiedResponse]:
|
|
119
|
+
"""Get collections in that library.
|
|
120
|
+
|
|
121
|
+
Returns only minified items at this point.
|
|
122
|
+
"""
|
|
123
|
+
# only minified response is supported
|
|
124
|
+
minified: bool = True
|
|
125
|
+
endpoint = f"/api/libraries/{library_id}/collections"
|
|
126
|
+
async for result in self._get_library_with_pagination(
|
|
127
|
+
endpoint=endpoint,
|
|
128
|
+
minified=minified,
|
|
129
|
+
response_cls=LibraryCollectionsMinifiedResponse,
|
|
130
|
+
response_cls_minified=LibraryCollectionsMinifiedResponse,
|
|
131
|
+
):
|
|
132
|
+
yield result
|
|
133
|
+
|
|
134
|
+
async def get_library_playlists(
|
|
135
|
+
self, *, library_id: str
|
|
136
|
+
) -> AsyncGenerator[LibraryPlaylistsResponse]:
|
|
137
|
+
"""Get collections in that library.
|
|
138
|
+
|
|
139
|
+
Returns only minified items at this point.
|
|
140
|
+
"""
|
|
141
|
+
endpoint = f"/api/libraries/{library_id}/playlists"
|
|
142
|
+
async for result in self._get_library_with_pagination(
|
|
143
|
+
endpoint=endpoint,
|
|
144
|
+
minified=False, # there is no minified version
|
|
145
|
+
response_cls=LibraryPlaylistsResponse,
|
|
146
|
+
response_cls_minified=LibraryPlaylistsResponse,
|
|
147
|
+
):
|
|
148
|
+
yield result
|
|
149
|
+
|
|
150
|
+
# get library's personalized view
|
|
151
|
+
|
|
152
|
+
async def get_library_filterdata(self, *, library_id: str) -> LibraryFilterData:
|
|
153
|
+
"""Get filterdata of library."""
|
|
154
|
+
response = await self._get(endpoint=f"/api/libraries/{library_id}/filterdata")
|
|
155
|
+
return LibraryFilterData.from_json(response)
|
|
156
|
+
|
|
157
|
+
# search library
|
|
158
|
+
# get lib stats
|
|
159
|
+
|
|
160
|
+
async def get_library_authors(self, *, library_id: str) -> list[AuthorExpanded]:
|
|
161
|
+
"""Get authors of library."""
|
|
162
|
+
response = await self._get(endpoint=f"/api/libraries/{library_id}/authors")
|
|
163
|
+
return LibraryAuthorsResponse.from_json(response).authors
|
|
164
|
+
|
|
165
|
+
async def get_library_narrators(self, *, library_id: str) -> list[Narrator]:
|
|
166
|
+
"""Get narrators of a library."""
|
|
167
|
+
response = await self._get(endpoint=f"/api/libraries/{library_id}/narrators")
|
|
168
|
+
return LibraryNarratorsResponse.from_json(response).narrators
|
|
169
|
+
|
|
170
|
+
# match lib items
|
|
171
|
+
# scan lib folders
|
|
172
|
+
# library recent episodes
|
|
173
|
+
# reorder list
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Calls to /api/me."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
|
|
5
|
+
from aioaudiobookshelf.client._base import BaseClient
|
|
6
|
+
from aioaudiobookshelf.schema.calls_me import (
|
|
7
|
+
MeListeningSessionsParameters,
|
|
8
|
+
MeListeningSessionsResponse,
|
|
9
|
+
)
|
|
10
|
+
from aioaudiobookshelf.schema.media_progress import MediaProgress
|
|
11
|
+
from aioaudiobookshelf.schema.user import User
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MeClient(BaseClient):
|
|
15
|
+
"""MeClient."""
|
|
16
|
+
|
|
17
|
+
async def get_my_user(self) -> User:
|
|
18
|
+
"""Get this client's user."""
|
|
19
|
+
data = await self._get("/api/me")
|
|
20
|
+
return User.from_json(data)
|
|
21
|
+
|
|
22
|
+
async def get_my_listening_sessions(self) -> AsyncGenerator[MeListeningSessionsResponse]:
|
|
23
|
+
"""Get this user's listening sessions."""
|
|
24
|
+
raise NotImplementedError("PodcastMetadata not fully returned.")
|
|
25
|
+
page_cnt = 0
|
|
26
|
+
params = MeListeningSessionsParameters(
|
|
27
|
+
items_per_page=self.session_config.pagination_items_per_page, page=page_cnt
|
|
28
|
+
)
|
|
29
|
+
while True:
|
|
30
|
+
params.page = page_cnt
|
|
31
|
+
response = await self._get("/api/me/listening-sessions", params.to_dict())
|
|
32
|
+
page_cnt += 1
|
|
33
|
+
yield MeListeningSessionsResponse.from_json(response)
|
|
34
|
+
|
|
35
|
+
# listening stats
|
|
36
|
+
# remove item from continue listening
|
|
37
|
+
|
|
38
|
+
async def get_my_media_progress(
|
|
39
|
+
self, *, item_id: str, episode_id: str | None = None
|
|
40
|
+
) -> MediaProgress | None:
|
|
41
|
+
"""Get a MediaProgress, returns None if none found."""
|
|
42
|
+
endpoint = f"/api/me/progress/{item_id}"
|
|
43
|
+
if episode_id is not None:
|
|
44
|
+
endpoint += f"/{episode_id}"
|
|
45
|
+
response = await self._get(endpoint=endpoint)
|
|
46
|
+
if not response:
|
|
47
|
+
return None
|
|
48
|
+
return MediaProgress.from_json(response)
|
|
49
|
+
|
|
50
|
+
# batch create/ update media progress
|
|
51
|
+
|
|
52
|
+
async def update_my_media_progress(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
item_id: str,
|
|
56
|
+
episode_id: str | None = None,
|
|
57
|
+
duration_seconds: float,
|
|
58
|
+
progress_seconds: float,
|
|
59
|
+
is_finished: bool,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Update progress of media item.
|
|
62
|
+
|
|
63
|
+
0 <= progress_percent <= 1
|
|
64
|
+
|
|
65
|
+
Notes:
|
|
66
|
+
- progress in abs is percentage
|
|
67
|
+
- multiple parameters in one call don't work in all combinations
|
|
68
|
+
- currentTime is current position in s
|
|
69
|
+
- currentTime works only if duration is sent as well, but then don't
|
|
70
|
+
send progress at the same time.
|
|
71
|
+
"""
|
|
72
|
+
logger_item = "audiobook" if not episode_id else "podcast"
|
|
73
|
+
endpoint = f"/api/me/progress/{item_id}"
|
|
74
|
+
if episode_id is not None:
|
|
75
|
+
endpoint += f"/{episode_id}"
|
|
76
|
+
await self._patch(
|
|
77
|
+
endpoint,
|
|
78
|
+
data={"isFinished": is_finished},
|
|
79
|
+
)
|
|
80
|
+
if is_finished:
|
|
81
|
+
self.logger.debug("Marked %s, id %s finished.", logger_item, item_id)
|
|
82
|
+
return
|
|
83
|
+
percentage = progress_seconds / duration_seconds
|
|
84
|
+
await self._patch(
|
|
85
|
+
endpoint,
|
|
86
|
+
data={"progress": percentage},
|
|
87
|
+
)
|
|
88
|
+
await self._patch(
|
|
89
|
+
endpoint,
|
|
90
|
+
data={"duration": duration_seconds, "currentTime": progress_seconds},
|
|
91
|
+
)
|
|
92
|
+
self.logger.debug(
|
|
93
|
+
"Updated progress of %s, id %s to %.2f%%.", logger_item, item_id, percentage * 100
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def remove_my_media_progress(self, *, media_progress_id: str) -> None:
|
|
97
|
+
"""Remove a single media progress."""
|
|
98
|
+
await self._delete(f"/api/me/progress/{media_progress_id}")
|
|
99
|
+
|
|
100
|
+
# create, update, remove bookmark
|
|
101
|
+
# change password
|
|
102
|
+
# get lib items in progress
|
|
103
|
+
# remove series from continue listening
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Calls to /api/playlists."""
|
|
2
|
+
|
|
3
|
+
from aioaudiobookshelf.schema.calls_playlists import AllPlaylistsResponse
|
|
4
|
+
from aioaudiobookshelf.schema.playlist import PlaylistExpanded
|
|
5
|
+
|
|
6
|
+
from ._base import BaseClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PlaylistsClient(BaseClient):
|
|
10
|
+
"""PlaylistsClient."""
|
|
11
|
+
|
|
12
|
+
# create playlist
|
|
13
|
+
|
|
14
|
+
async def get_all_playlists(self) -> list[PlaylistExpanded]:
|
|
15
|
+
"""Get all playlists accessible to user."""
|
|
16
|
+
data = await self._get(endpoint="/api/playlists")
|
|
17
|
+
return AllPlaylistsResponse.from_json(data).playlists
|
|
18
|
+
|
|
19
|
+
async def get_playlist(self, *, playlist_id: str) -> PlaylistExpanded:
|
|
20
|
+
"""Get a playlist."""
|
|
21
|
+
data = await self._get(endpoint=f"/api/playlists/{playlist_id}")
|
|
22
|
+
return PlaylistExpanded.from_json(data)
|
|
23
|
+
|
|
24
|
+
# update
|
|
25
|
+
# delete
|
|
26
|
+
# add item
|
|
27
|
+
# remove item
|
|
28
|
+
# batch add + remove
|
|
29
|
+
# create playlist from collection
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Calls to /api/podcasts."""
|
|
2
|
+
|
|
3
|
+
from aioaudiobookshelf.client._base import BaseClient
|
|
4
|
+
from aioaudiobookshelf.schema.podcast import PodcastEpisode
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PodcastsClient(BaseClient):
|
|
8
|
+
"""PodcastClient."""
|
|
9
|
+
|
|
10
|
+
# create podcast
|
|
11
|
+
# get podcast feed
|
|
12
|
+
# feed from opml
|
|
13
|
+
# check for new episodes
|
|
14
|
+
# get podcast episode downloads
|
|
15
|
+
# search podcast feed
|
|
16
|
+
# download podcast episodes
|
|
17
|
+
# match podcast episode
|
|
18
|
+
|
|
19
|
+
async def get_podcast_episode(self, *, podcast_id: str, episode_id: str) -> PodcastEpisode:
|
|
20
|
+
"""Get podcast episode."""
|
|
21
|
+
data = await self._get(f"/api/podcasts/{podcast_id}/episode/{episode_id}")
|
|
22
|
+
return PodcastEpisode.from_json(data)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Calls to /api/series."""
|
|
2
|
+
|
|
3
|
+
from aioaudiobookshelf.client._base import BaseClient
|
|
4
|
+
from aioaudiobookshelf.schema.calls_series import SeriesWithProgress
|
|
5
|
+
from aioaudiobookshelf.schema.series import Series
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SeriesClient(BaseClient):
|
|
9
|
+
"""SeriesClient."""
|
|
10
|
+
|
|
11
|
+
async def get_series(
|
|
12
|
+
self, *, series_id: str, include_progress: bool = False
|
|
13
|
+
) -> Series | SeriesWithProgress:
|
|
14
|
+
"""Get an author.
|
|
15
|
+
|
|
16
|
+
Include series always includes items.
|
|
17
|
+
"""
|
|
18
|
+
response_cls: type[Series | SeriesWithProgress] = Series
|
|
19
|
+
endpoint = f"/api/series/{series_id}"
|
|
20
|
+
if include_progress:
|
|
21
|
+
endpoint += "?include=progress"
|
|
22
|
+
response_cls = SeriesWithProgress
|
|
23
|
+
|
|
24
|
+
response = await self._get(endpoint)
|
|
25
|
+
return response_cls.from_json(response)
|
|
26
|
+
|
|
27
|
+
# update series
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Calls to /api/session."""
|
|
2
|
+
|
|
3
|
+
from aioaudiobookshelf.client._base import BaseClient
|
|
4
|
+
from aioaudiobookshelf.schema.calls_session import CloseOpenSessionsParameters
|
|
5
|
+
from aioaudiobookshelf.schema.session import PlaybackSessionExpanded
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SessionClient(BaseClient):
|
|
9
|
+
"""SessionClient."""
|
|
10
|
+
|
|
11
|
+
# get_all_session # admin
|
|
12
|
+
# delete session
|
|
13
|
+
# sync local session(s)
|
|
14
|
+
|
|
15
|
+
async def get_open_session(self, *, session_id: str) -> PlaybackSessionExpanded:
|
|
16
|
+
"""Get open session."""
|
|
17
|
+
response = await self._get(f"/api/session/{session_id}")
|
|
18
|
+
psession = PlaybackSessionExpanded.from_json(response)
|
|
19
|
+
self.logger.debug(
|
|
20
|
+
"Got playback session %s for %s named %s.",
|
|
21
|
+
psession.id_,
|
|
22
|
+
psession.media_type,
|
|
23
|
+
psession.display_title,
|
|
24
|
+
)
|
|
25
|
+
return psession
|
|
26
|
+
|
|
27
|
+
# sync open session
|
|
28
|
+
|
|
29
|
+
async def close_open_session(
|
|
30
|
+
self, *, session_id: str, parameters: CloseOpenSessionsParameters | None = None
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Close open session."""
|
|
33
|
+
_parameters = {} if parameters is None else parameters.to_dict()
|
|
34
|
+
self.logger.debug("Closing playback session %s.", session_id)
|
|
35
|
+
await self._post(f"/api/session/{session_id}/close", data=_parameters)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Exceptions for aioaudiobookshelf."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BadUserError(Exception):
|
|
5
|
+
"""Raised if this user is not suitable for the client."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LoginError(Exception):
|
|
9
|
+
"""Exception raised if login failed."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ApiError(Exception):
|
|
13
|
+
"""Exception raised if call to api failed."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Helpers for aioaudiobookshelf."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import urllib.parse
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FilterGroup(StrEnum):
|
|
9
|
+
"""FilterGroup."""
|
|
10
|
+
|
|
11
|
+
GENRES = "genres"
|
|
12
|
+
TAGS = "tags"
|
|
13
|
+
SERIES = "series"
|
|
14
|
+
AUTHORS = "authors"
|
|
15
|
+
PROGRESS = "progress"
|
|
16
|
+
NARRATORS = "narrators"
|
|
17
|
+
MISSING = "missing"
|
|
18
|
+
LANGUAGES = "languages"
|
|
19
|
+
TRACKS = "tracks"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FilterProgressType(StrEnum):
|
|
23
|
+
"""FilterProgressType."""
|
|
24
|
+
|
|
25
|
+
FINISHED = "finished"
|
|
26
|
+
NOTSTARTED = "not-started"
|
|
27
|
+
NOTFINISHED = "not-finished"
|
|
28
|
+
INPROGRESS = "in-progress"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_library_filter_string(
|
|
32
|
+
*, filter_group: FilterGroup, filter_value: str | FilterProgressType
|
|
33
|
+
) -> str:
|
|
34
|
+
"""Obtain a string usable as filter_str.
|
|
35
|
+
|
|
36
|
+
Currently only narrators, genre, tags, languages and progress.
|
|
37
|
+
"""
|
|
38
|
+
if filter_group in [
|
|
39
|
+
FilterGroup.NARRATORS,
|
|
40
|
+
FilterGroup.GENRES,
|
|
41
|
+
FilterGroup.TAGS,
|
|
42
|
+
FilterGroup.LANGUAGES,
|
|
43
|
+
]:
|
|
44
|
+
_encoded = urllib.parse.quote(base64.b64encode(filter_value.encode()))
|
|
45
|
+
return f"{filter_group.value}.{_encoded}"
|
|
46
|
+
|
|
47
|
+
if filter_group == FilterGroup.PROGRESS:
|
|
48
|
+
if filter_value not in FilterProgressType:
|
|
49
|
+
raise RuntimeError("Filter value not acceptable for progress.")
|
|
50
|
+
filter_value = (
|
|
51
|
+
filter_value.value if isinstance(filter_value, FilterProgressType) else filter_value
|
|
52
|
+
)
|
|
53
|
+
_encoded = urllib.parse.quote(base64.b64encode(filter_value.encode()))
|
|
54
|
+
return f"{filter_group.value}.{_encoded}"
|
|
55
|
+
|
|
56
|
+
raise NotImplementedError(f"The {filter_group=} is not yet implemented.")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Schema for Audiobookshelf (abs)."""
|
|
2
|
+
|
|
3
|
+
from mashumaro.config import BaseConfig
|
|
4
|
+
from mashumaro.mixins.json import DataClassJSONMixin
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _BaseModel(DataClassJSONMixin):
|
|
8
|
+
"""Model shared between schema definitions."""
|
|
9
|
+
|
|
10
|
+
class Config(BaseConfig):
|
|
11
|
+
"""Base configuration."""
|
|
12
|
+
|
|
13
|
+
forbid_extra_keys = False
|
|
14
|
+
serialize_by_alias = True
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Schema for audio."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from mashumaro.types import Alias
|
|
7
|
+
|
|
8
|
+
from . import _BaseModel
|
|
9
|
+
from .file import FileMetadata
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(kw_only=True)
|
|
13
|
+
class AudioBookmark(_BaseModel):
|
|
14
|
+
"""AudioBookmark. No variants.
|
|
15
|
+
|
|
16
|
+
https://api.audiobookshelf.org/#audio-bookmark
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
library_item_id: Annotated[str, Alias("libraryItemId")]
|
|
20
|
+
title: str
|
|
21
|
+
time: int # seconds
|
|
22
|
+
created_at: Annotated[int, Alias("createdAt")] # unix epoch ms
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(kw_only=True)
|
|
26
|
+
class AudioTrack(_BaseModel):
|
|
27
|
+
"""ABS audioTrack. No variants.
|
|
28
|
+
|
|
29
|
+
https://api.audiobookshelf.org/#audio-track
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
index: int | None
|
|
33
|
+
start_offset: Annotated[float, Alias("startOffset")]
|
|
34
|
+
duration: float
|
|
35
|
+
title: str
|
|
36
|
+
content_url: Annotated[str, Alias("contentUrl")]
|
|
37
|
+
metadata: FileMetadata | None
|
|
38
|
+
# is missing if part of library...
|
|
39
|
+
mime_type: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(kw_only=True)
|
|
43
|
+
class AudioFile(_BaseModel):
|
|
44
|
+
"""Audiofile."""
|
|
45
|
+
|
|
46
|
+
index: int
|
|
47
|
+
ino: str
|
|
48
|
+
metadata: FileMetadata
|
|
49
|
+
added_at: Annotated[int, Alias("addedAt")]
|
|
50
|
+
updated_at: Annotated[int, Alias("updatedAt")]
|
|
51
|
+
track_num_from_meta: Annotated[int | None, Alias("trackNumFromMeta")] = None
|
|
52
|
+
disc_num_from_meta: Annotated[int | None, Alias("discNumFromMeta")] = None
|
|
53
|
+
track_num_from_filename: Annotated[int | None, Alias("trackNumFromFilename")] = None
|
|
54
|
+
disc_num_from_filename: Annotated[int | None, Alias("discNumFromFilename")] = None
|
|
55
|
+
manually_verified: Annotated[bool, Alias("manuallyVerified")]
|
|
56
|
+
exclude: bool
|
|
57
|
+
error: str | None = None
|
|
58
|
+
format: str
|
|
59
|
+
duration: float
|
|
60
|
+
bit_rate: Annotated[int, Alias("bitRate")]
|
|
61
|
+
language: str | None = None
|
|
62
|
+
codec: str
|
|
63
|
+
time_base: Annotated[str, Alias("timeBase")]
|
|
64
|
+
channels: int
|
|
65
|
+
channel_layout: Annotated[str, Alias("channelLayout")]
|
|
66
|
+
embedded_cover_art: Annotated[str | None, Alias("embeddedCoverArt")] = None
|
|
67
|
+
mime_type: Annotated[str, Alias("mimeType")]
|
|
68
|
+
# if part of a book
|
|
69
|
+
# chapters: list[BookChapter]
|
|
70
|
+
# id3 tags:
|
|
71
|
+
# meta_tags: AudioMetaTags
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Schema for Author."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from mashumaro.types import Alias
|
|
7
|
+
|
|
8
|
+
from . import _BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(kw_only=True)
|
|
12
|
+
class AuthorMinified(_BaseModel):
|
|
13
|
+
"""AuthorMinified.
|
|
14
|
+
|
|
15
|
+
https://api.audiobookshelf.org/#author
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
id_: Annotated[str, Alias("id")]
|
|
19
|
+
name: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(kw_only=True)
|
|
23
|
+
class Author(AuthorMinified):
|
|
24
|
+
"""Author."""
|
|
25
|
+
|
|
26
|
+
asin: str | None = None
|
|
27
|
+
description: str | None = None
|
|
28
|
+
image_path: Annotated[str | None, Alias("imagePath")] = None
|
|
29
|
+
added_at: Annotated[int, Alias("addedAt")] # ms epoch
|
|
30
|
+
updated_at: Annotated[int, Alias("updatedAt")] # ms epoch
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(kw_only=True)
|
|
34
|
+
class AuthorExpanded(Author):
|
|
35
|
+
"""ABSAuthorExpanded."""
|
|
36
|
+
|
|
37
|
+
num_books: Annotated[int, Alias("numBooks")]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(kw_only=True)
|
|
41
|
+
class Narrator(_BaseModel):
|
|
42
|
+
"""Narrator."""
|
|
43
|
+
|
|
44
|
+
id_: Annotated[str, Alias("id")]
|
|
45
|
+
name: str
|
|
46
|
+
num_books: Annotated[int, Alias("numBooks")]
|