aioaudiobookshelf 0.1.4__py3-none-any.whl → 0.1.11__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.
- aioaudiobookshelf/__init__.py +8 -26
- aioaudiobookshelf/client/__init__.py +50 -28
- aioaudiobookshelf/client/_base.py +110 -22
- aioaudiobookshelf/client/authors.py +24 -1
- aioaudiobookshelf/client/libraries.py +20 -1
- aioaudiobookshelf/client/session.py +9 -2
- aioaudiobookshelf/client/session_configuration.py +106 -0
- aioaudiobookshelf/exceptions.py +12 -0
- aioaudiobookshelf/helpers.py +29 -0
- aioaudiobookshelf/schema/audio.py +1 -1
- aioaudiobookshelf/schema/book.py +1 -1
- aioaudiobookshelf/schema/calls_login.py +3 -0
- aioaudiobookshelf/schema/calls_session.py +3 -0
- aioaudiobookshelf/schema/file.py +2 -1
- aioaudiobookshelf/schema/shelf.py +188 -0
- aioaudiobookshelf/schema/user.py +7 -1
- {aioaudiobookshelf-0.1.4.dist-info → aioaudiobookshelf-0.1.11.dist-info}/METADATA +4 -3
- {aioaudiobookshelf-0.1.4.dist-info → aioaudiobookshelf-0.1.11.dist-info}/RECORD +21 -19
- {aioaudiobookshelf-0.1.4.dist-info → aioaudiobookshelf-0.1.11.dist-info}/WHEEL +1 -1
- {aioaudiobookshelf-0.1.4.dist-info → aioaudiobookshelf-0.1.11.dist-info/licenses}/LICENSE +0 -0
- {aioaudiobookshelf-0.1.4.dist-info → aioaudiobookshelf-0.1.11.dist-info}/top_level.txt +0 -0
aioaudiobookshelf/__init__.py
CHANGED
|
@@ -2,29 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from aiohttp.client_exceptions import ClientResponseError, InvalidUrlClientError
|
|
4
4
|
|
|
5
|
-
from aioaudiobookshelf.client import AdminClient,
|
|
5
|
+
from aioaudiobookshelf.client import AdminClient, SocketClient, UserClient
|
|
6
|
+
from aioaudiobookshelf.client.session_configuration import SessionConfiguration
|
|
6
7
|
from aioaudiobookshelf.exceptions import LoginError, TokenIsMissingError
|
|
7
|
-
from aioaudiobookshelf.
|
|
8
|
+
from aioaudiobookshelf.helpers import get_login_response
|
|
9
|
+
from aioaudiobookshelf.schema.calls_login import AuthorizeResponse
|
|
8
10
|
|
|
9
|
-
__version__ = "0.1.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
async def _get_login_response(
|
|
13
|
-
*, session_config: SessionConfiguration, username: str, password: str
|
|
14
|
-
) -> LoginResponse:
|
|
15
|
-
"""Login via username and password."""
|
|
16
|
-
login_request = LoginParameters(username=username, password=password).to_dict()
|
|
17
|
-
|
|
18
|
-
try:
|
|
19
|
-
resp = await session_config.session.post(
|
|
20
|
-
f"{session_config.url}/login",
|
|
21
|
-
json=login_request,
|
|
22
|
-
ssl=session_config.verify_ssl,
|
|
23
|
-
raise_for_status=True,
|
|
24
|
-
)
|
|
25
|
-
except (ClientResponseError, InvalidUrlClientError) as exc:
|
|
26
|
-
raise LoginError from exc
|
|
27
|
-
return LoginResponse.from_json(await resp.read())
|
|
11
|
+
__version__ = "0.1.11"
|
|
28
12
|
|
|
29
13
|
|
|
30
14
|
async def _get_authorize_response(*, session_config: SessionConfiguration) -> AuthorizeResponse:
|
|
@@ -70,7 +54,7 @@ async def get_user_client(
|
|
|
70
54
|
password: str,
|
|
71
55
|
) -> UserClient:
|
|
72
56
|
"""Get a user client."""
|
|
73
|
-
login_response = await
|
|
57
|
+
login_response = await get_login_response(
|
|
74
58
|
session_config=session_config, username=username, password=password
|
|
75
59
|
)
|
|
76
60
|
|
|
@@ -84,13 +68,11 @@ async def get_user_and_socket_client(
|
|
|
84
68
|
password: str,
|
|
85
69
|
) -> tuple[UserClient, SocketClient]:
|
|
86
70
|
"""Get user and socket client."""
|
|
87
|
-
login_response = await
|
|
71
|
+
login_response = await get_login_response(
|
|
88
72
|
session_config=session_config, username=username, password=password
|
|
89
73
|
)
|
|
90
74
|
|
|
91
75
|
user_client = UserClient(session_config=session_config, login_response=login_response)
|
|
92
|
-
if not session_config.token:
|
|
93
|
-
session_config.token = user_client.token
|
|
94
76
|
socket_client = SocketClient(session_config=session_config)
|
|
95
77
|
return user_client, socket_client
|
|
96
78
|
|
|
@@ -102,7 +84,7 @@ async def get_admin_client(
|
|
|
102
84
|
password: str,
|
|
103
85
|
) -> UserClient:
|
|
104
86
|
"""Get a admin client."""
|
|
105
|
-
login_response = await
|
|
87
|
+
login_response = await get_login_response(
|
|
106
88
|
session_config=session_config, username=username, password=password
|
|
107
89
|
)
|
|
108
90
|
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from collections.abc import Callable
|
|
5
|
-
from dataclasses import dataclass
|
|
6
5
|
from typing import Any
|
|
7
6
|
|
|
8
7
|
import socketio
|
|
9
8
|
import socketio.exceptions
|
|
10
|
-
from aiohttp import ClientSession
|
|
11
9
|
|
|
12
|
-
from aioaudiobookshelf.
|
|
10
|
+
from aioaudiobookshelf.client.session_configuration import SessionConfiguration
|
|
11
|
+
from aioaudiobookshelf.exceptions import (
|
|
12
|
+
BadUserError,
|
|
13
|
+
RefreshTokenExpiredError,
|
|
14
|
+
ServiceUnavailableError,
|
|
15
|
+
TokenIsMissingError,
|
|
16
|
+
)
|
|
13
17
|
from aioaudiobookshelf.schema.events_socket import (
|
|
14
18
|
LibraryItemRemoved,
|
|
15
19
|
PodcastEpisodeDownload,
|
|
@@ -30,29 +34,6 @@ from .series import SeriesClient
|
|
|
30
34
|
from .session import SessionClient
|
|
31
35
|
|
|
32
36
|
|
|
33
|
-
@dataclass(kw_only=True)
|
|
34
|
-
class SessionConfiguration:
|
|
35
|
-
"""Session configuration for abs client."""
|
|
36
|
-
|
|
37
|
-
session: ClientSession
|
|
38
|
-
url: str
|
|
39
|
-
verify_ssl: bool = True
|
|
40
|
-
token: str | None = None
|
|
41
|
-
pagination_items_per_page: int = 10
|
|
42
|
-
logger: logging.Logger | None = None
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def headers(self) -> dict[str, str]:
|
|
46
|
-
"""Session headers."""
|
|
47
|
-
if self.token is None:
|
|
48
|
-
raise TokenIsMissingError("Token not set.")
|
|
49
|
-
return {"Authorization": f"Bearer {self.token}"}
|
|
50
|
-
|
|
51
|
-
def __post_init__(self) -> None:
|
|
52
|
-
"""Post init."""
|
|
53
|
-
self.url = self.url.rstrip("/")
|
|
54
|
-
|
|
55
|
-
|
|
56
37
|
class UserClient(
|
|
57
38
|
LibrariesClient,
|
|
58
39
|
ItemsClient,
|
|
@@ -82,7 +63,10 @@ class AdminClient(UserClient):
|
|
|
82
63
|
class SocketClient:
|
|
83
64
|
"""Client for connecting to abs' socket."""
|
|
84
65
|
|
|
85
|
-
def __init__(
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
session_config: SessionConfiguration,
|
|
69
|
+
) -> None:
|
|
86
70
|
"""Init SocketClient."""
|
|
87
71
|
self.session_config = session_config
|
|
88
72
|
|
|
@@ -93,9 +77,17 @@ class SocketClient:
|
|
|
93
77
|
ssl_verify=self.session_config.verify_ssl,
|
|
94
78
|
)
|
|
95
79
|
|
|
80
|
+
if self.session_config.logger is None:
|
|
81
|
+
self.logger = logging.getLogger(__name__)
|
|
82
|
+
logging.basicConfig()
|
|
83
|
+
self.logger.setLevel(logging.DEBUG)
|
|
84
|
+
else:
|
|
85
|
+
self.logger = self.session_config.logger
|
|
86
|
+
|
|
96
87
|
self.set_item_callbacks()
|
|
97
88
|
self.set_user_callbacks()
|
|
98
89
|
self.set_podcast_episode_download_callbacks()
|
|
90
|
+
self.set_refresh_token_expired_callback()
|
|
99
91
|
|
|
100
92
|
def set_item_callbacks(
|
|
101
93
|
self,
|
|
@@ -129,9 +121,16 @@ class SocketClient:
|
|
|
129
121
|
"""Set podcast episode download callbacks."""
|
|
130
122
|
self.on_episode_download_finished = on_episode_download_finished
|
|
131
123
|
|
|
124
|
+
def set_refresh_token_expired_callback(
|
|
125
|
+
self, *, on_refresh_token_expired: Callable[[], Any] | None = None
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Set refresh token expired callback."""
|
|
128
|
+
self.on_refresh_token_expired = on_refresh_token_expired
|
|
129
|
+
|
|
132
130
|
async def init_client(self) -> None:
|
|
133
131
|
"""Initialize the client."""
|
|
134
132
|
self.client.on("connect", handler=self._on_connect)
|
|
133
|
+
self.client.on("connect_error", handler=self._on_connect_error)
|
|
135
134
|
|
|
136
135
|
self.client.on("user_updated", handler=self._on_user_updated)
|
|
137
136
|
self.client.on("user_item_progress_updated", handler=self._on_user_item_progress_updated)
|
|
@@ -153,7 +152,30 @@ class SocketClient:
|
|
|
153
152
|
logout = shutdown
|
|
154
153
|
|
|
155
154
|
async def _on_connect(self) -> None:
|
|
156
|
-
|
|
155
|
+
"""V2.26 and above: access token or api token."""
|
|
156
|
+
if self.session_config.access_token is not None:
|
|
157
|
+
token = self.session_config.access_token
|
|
158
|
+
else:
|
|
159
|
+
if self.session_config.token is None:
|
|
160
|
+
raise TokenIsMissingError
|
|
161
|
+
token = self.session_config.token
|
|
162
|
+
await self.client.emit(event="auth", data=token)
|
|
163
|
+
self.logger.debug("Socket connected.")
|
|
164
|
+
|
|
165
|
+
async def _on_connect_error(self, *_: Any) -> None:
|
|
166
|
+
if not self.session_config.auto_refresh or self.session_config.access_token is None:
|
|
167
|
+
return
|
|
168
|
+
# try to refresh token
|
|
169
|
+
self.logger.debug("Auto refreshing token")
|
|
170
|
+
try:
|
|
171
|
+
await self.session_config.refresh()
|
|
172
|
+
except RefreshTokenExpiredError:
|
|
173
|
+
if self.on_refresh_token_expired is not None:
|
|
174
|
+
await self.on_refresh_token_expired()
|
|
175
|
+
return
|
|
176
|
+
except ServiceUnavailableError:
|
|
177
|
+
# socketio will continue trying to reconnect.
|
|
178
|
+
return
|
|
157
179
|
|
|
158
180
|
async def _on_user_updated(self, data: dict[str, Any]) -> None:
|
|
159
181
|
if self.on_user_updated is not None:
|
|
@@ -4,11 +4,12 @@ import logging
|
|
|
4
4
|
from abc import abstractmethod
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
|
+
from aiohttp.client import ClientResponse
|
|
7
8
|
from aiohttp.client_exceptions import ClientResponseError
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
10
|
-
from aioaudiobookshelf.client import SessionConfiguration
|
|
11
|
-
from aioaudiobookshelf.exceptions import ApiError
|
|
11
|
+
from aioaudiobookshelf.client.session_configuration import SessionConfiguration
|
|
12
|
+
from aioaudiobookshelf.exceptions import AccessTokenExpiredError, ApiError, TokenIsMissingError
|
|
12
13
|
from aioaudiobookshelf.schema.calls_login import LoginResponse
|
|
13
14
|
|
|
14
15
|
|
|
@@ -20,9 +21,17 @@ class BaseClient:
|
|
|
20
21
|
) -> None:
|
|
21
22
|
self.session_config = session_config
|
|
22
23
|
self.user = login_response.user
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
self.
|
|
24
|
+
self.server_settings = login_response.server_settings
|
|
25
|
+
|
|
26
|
+
if not self.session_config.token and not self.session_config.refresh_token:
|
|
27
|
+
if login_response.user.refresh_token is not None:
|
|
28
|
+
assert login_response.user.access_token is not None
|
|
29
|
+
assert login_response.user.refresh_token is not None
|
|
30
|
+
self.session_config.refresh_token = login_response.user.refresh_token
|
|
31
|
+
self.session_config.access_token = login_response.user.access_token
|
|
32
|
+
elif login_response.user.token is not None:
|
|
33
|
+
assert login_response.user.token is not None
|
|
34
|
+
self.session_config.token = login_response.user.token
|
|
26
35
|
|
|
27
36
|
if self.session_config.logger is None:
|
|
28
37
|
self.logger = logging.getLogger(__name__)
|
|
@@ -32,16 +41,19 @@ class BaseClient:
|
|
|
32
41
|
self.logger = self.session_config.logger
|
|
33
42
|
|
|
34
43
|
self.logger.debug(
|
|
35
|
-
"Initialized client %s
|
|
44
|
+
"Initialized client %s",
|
|
36
45
|
self.__class__.__name__,
|
|
37
|
-
self._token,
|
|
38
46
|
)
|
|
39
47
|
|
|
40
48
|
self._verify_user()
|
|
41
49
|
|
|
42
50
|
@property
|
|
43
51
|
def token(self) -> str:
|
|
44
|
-
|
|
52
|
+
if self.session_config.access_token is not None:
|
|
53
|
+
return self.session_config.access_token
|
|
54
|
+
if self.session_config.token is None:
|
|
55
|
+
raise TokenIsMissingError
|
|
56
|
+
return self.session_config.token
|
|
45
57
|
|
|
46
58
|
@abstractmethod
|
|
47
59
|
def _verify_user(self) -> None:
|
|
@@ -53,26 +65,57 @@ class BaseClient:
|
|
|
53
65
|
data: dict[str, Any] | None = None,
|
|
54
66
|
) -> bytes:
|
|
55
67
|
"""POST request to abs api."""
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
|
|
69
|
+
async def _request() -> ClientResponse:
|
|
70
|
+
return await self.session_config.session.post(
|
|
58
71
|
f"{self.session_config.url}/{endpoint}",
|
|
59
72
|
json=data,
|
|
60
73
|
ssl=self.session_config.verify_ssl,
|
|
61
74
|
headers=self.session_config.headers,
|
|
62
75
|
raise_for_status=True,
|
|
76
|
+
timeout=self.session_config.timeout,
|
|
63
77
|
)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
response = await _request()
|
|
64
81
|
except ClientResponseError as exc:
|
|
65
|
-
|
|
82
|
+
if exc.code == 401:
|
|
83
|
+
if self.session_config.auto_refresh:
|
|
84
|
+
self.logger.debug("Auto refreshing tokens.")
|
|
85
|
+
await self.refresh()
|
|
86
|
+
# TODO: remove redundant clause
|
|
87
|
+
try:
|
|
88
|
+
response = await _request()
|
|
89
|
+
except ClientResponseError as inner_exc:
|
|
90
|
+
raise ApiError(f"API POST call to {endpoint} failed.") from inner_exc
|
|
91
|
+
else:
|
|
92
|
+
raise AccessTokenExpiredError from exc
|
|
93
|
+
else:
|
|
94
|
+
raise ApiError(f"API POST call to {endpoint} failed.") from exc
|
|
95
|
+
|
|
66
96
|
return await response.read()
|
|
67
97
|
|
|
68
98
|
async def _get(self, endpoint: str, params: dict[str, str | int] | None = None) -> bytes:
|
|
69
99
|
"""GET request to abs api."""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
100
|
+
|
|
101
|
+
async def _request() -> ClientResponse:
|
|
102
|
+
return await self.session_config.session.get(
|
|
103
|
+
f"{self.session_config.url}/{endpoint}",
|
|
104
|
+
params=params,
|
|
105
|
+
ssl=self.session_config.verify_ssl,
|
|
106
|
+
headers=self.session_config.headers,
|
|
107
|
+
timeout=self.session_config.timeout,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
response = await _request()
|
|
111
|
+
if response.status == 401:
|
|
112
|
+
if self.session_config.auto_refresh:
|
|
113
|
+
self.logger.debug("Auto refreshing tokens.")
|
|
114
|
+
await self.refresh()
|
|
115
|
+
response = await _request()
|
|
116
|
+
else:
|
|
117
|
+
raise AccessTokenExpiredError
|
|
118
|
+
|
|
76
119
|
status = response.status
|
|
77
120
|
if response.content_type == "application/json" and status == 200:
|
|
78
121
|
return await response.read()
|
|
@@ -82,29 +125,74 @@ class BaseClient:
|
|
|
82
125
|
|
|
83
126
|
async def _patch(self, endpoint: str, data: dict[str, Any] | None = None) -> None:
|
|
84
127
|
"""PATCH request to abs api."""
|
|
85
|
-
|
|
128
|
+
|
|
129
|
+
async def _request() -> None:
|
|
86
130
|
await self.session_config.session.patch(
|
|
87
131
|
f"{self.session_config.url}/{endpoint}",
|
|
88
132
|
json=data,
|
|
89
133
|
ssl=self.session_config.verify_ssl,
|
|
90
134
|
headers=self.session_config.headers,
|
|
91
135
|
raise_for_status=True,
|
|
136
|
+
timeout=self.session_config.timeout,
|
|
92
137
|
)
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
await _request()
|
|
93
141
|
except ClientResponseError as exc:
|
|
94
|
-
|
|
142
|
+
if exc.code == 401:
|
|
143
|
+
if self.session_config.auto_refresh:
|
|
144
|
+
self.logger.debug("Auto refreshing tokens.")
|
|
145
|
+
await self.refresh()
|
|
146
|
+
try:
|
|
147
|
+
await _request()
|
|
148
|
+
except ClientResponseError as inner_exc:
|
|
149
|
+
raise ApiError(f"API PATCH call to {endpoint} failed.") from inner_exc
|
|
150
|
+
else:
|
|
151
|
+
raise AccessTokenExpiredError from exc
|
|
152
|
+
else:
|
|
153
|
+
raise ApiError(f"API PATCH call to {endpoint} failed.") from exc
|
|
95
154
|
|
|
96
155
|
async def _delete(self, endpoint: str) -> None:
|
|
97
156
|
"""DELETE request to abs api."""
|
|
98
|
-
|
|
157
|
+
|
|
158
|
+
async def _request() -> None:
|
|
99
159
|
await self.session_config.session.delete(
|
|
100
160
|
f"{self.session_config.url}/{endpoint}",
|
|
101
161
|
ssl=self.session_config.verify_ssl,
|
|
102
162
|
headers=self.session_config.headers,
|
|
103
163
|
raise_for_status=True,
|
|
164
|
+
timeout=self.session_config.timeout,
|
|
104
165
|
)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
await _request()
|
|
105
169
|
except ClientResponseError as exc:
|
|
106
|
-
|
|
170
|
+
if exc.code == 401:
|
|
171
|
+
if self.session_config.auto_refresh:
|
|
172
|
+
self.logger.debug("Auto refreshing tokens.")
|
|
173
|
+
await self.refresh()
|
|
174
|
+
try:
|
|
175
|
+
await _request()
|
|
176
|
+
except ClientResponseError as inner_exc:
|
|
177
|
+
raise ApiError(f"API DELETE call to {endpoint} failed.") from inner_exc
|
|
178
|
+
else:
|
|
179
|
+
raise AccessTokenExpiredError from exc
|
|
180
|
+
else:
|
|
181
|
+
raise ApiError(f"API DELETE call to {endpoint} failed.") from exc
|
|
182
|
+
|
|
183
|
+
async def refresh(self) -> None:
|
|
184
|
+
"""Refresh tokens."""
|
|
185
|
+
await self.session_config.refresh()
|
|
107
186
|
|
|
108
187
|
async def logout(self) -> None:
|
|
109
188
|
"""Logout client."""
|
|
110
|
-
|
|
189
|
+
if self.session_config.refresh_token is not None:
|
|
190
|
+
# v2.26 and above
|
|
191
|
+
await self.session_config.session.post(
|
|
192
|
+
f"{self.session_config.url}/logout",
|
|
193
|
+
ssl=self.session_config.verify_ssl,
|
|
194
|
+
headers=self.session_config.headers_refresh_logout,
|
|
195
|
+
raise_for_status=True,
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
await self._post("logout")
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
"""Calls to /api/authors."""
|
|
2
2
|
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
3
5
|
from aioaudiobookshelf.client._base import BaseClient
|
|
4
6
|
from aioaudiobookshelf.schema.author import Author
|
|
5
7
|
from aioaudiobookshelf.schema.calls_authors import AuthorWithItems, AuthorWithItemsAndSeries
|
|
6
8
|
|
|
7
9
|
|
|
10
|
+
class AuthorImageFormat(StrEnum):
|
|
11
|
+
"""AuthorImageFormat."""
|
|
12
|
+
|
|
13
|
+
JPEG = "jpeg"
|
|
14
|
+
WEBP = "webp"
|
|
15
|
+
|
|
16
|
+
|
|
8
17
|
class AuthorsClient(BaseClient):
|
|
9
18
|
"""AuthorsClient."""
|
|
10
19
|
|
|
@@ -31,4 +40,18 @@ class AuthorsClient(BaseClient):
|
|
|
31
40
|
|
|
32
41
|
# update author
|
|
33
42
|
# match author
|
|
34
|
-
|
|
43
|
+
async def get_author_image(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
author_id: str,
|
|
47
|
+
width: int = 400,
|
|
48
|
+
height: int | None = None,
|
|
49
|
+
format_: AuthorImageFormat = AuthorImageFormat.JPEG,
|
|
50
|
+
raw: bool = False,
|
|
51
|
+
) -> bytes:
|
|
52
|
+
"""Get image of author. If height is None, image is scaled proportionally."""
|
|
53
|
+
endpoint = f"/api/authors/{author_id}/image"
|
|
54
|
+
params = {"width": width, "format": format_.value, "raw": int(raw)}
|
|
55
|
+
if height:
|
|
56
|
+
params["height"] = height
|
|
57
|
+
return await self._get(endpoint, params=params)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from collections.abc import AsyncGenerator
|
|
4
4
|
from typing import TypeVar
|
|
5
5
|
|
|
6
|
+
from mashumaro.codecs.json import json_decode
|
|
6
7
|
from mashumaro.mixins.json import DataClassJSONMixin
|
|
7
8
|
|
|
8
9
|
from aioaudiobookshelf.client._base import BaseClient
|
|
@@ -18,6 +19,14 @@ from aioaudiobookshelf.schema.calls_library import (
|
|
|
18
19
|
LibraryWithFilterDataResponse,
|
|
19
20
|
)
|
|
20
21
|
from aioaudiobookshelf.schema.library import Library, LibraryFilterData
|
|
22
|
+
from aioaudiobookshelf.schema.shelf import (
|
|
23
|
+
Shelf,
|
|
24
|
+
ShelfAuthors,
|
|
25
|
+
ShelfBook,
|
|
26
|
+
ShelfEpisode,
|
|
27
|
+
ShelfPodcast,
|
|
28
|
+
ShelfSeries,
|
|
29
|
+
)
|
|
21
30
|
|
|
22
31
|
ResponseMinified = TypeVar("ResponseMinified", bound=DataClassJSONMixin)
|
|
23
32
|
ResponseNormal = TypeVar("ResponseNormal", bound=DataClassJSONMixin)
|
|
@@ -147,7 +156,17 @@ class LibrariesClient(BaseClient):
|
|
|
147
156
|
):
|
|
148
157
|
yield result
|
|
149
158
|
|
|
150
|
-
|
|
159
|
+
async def get_library_personalized_view(
|
|
160
|
+
self, *, library_id: str, limit: int = 10
|
|
161
|
+
) -> list[ShelfBook | ShelfPodcast | ShelfAuthors | ShelfEpisode | ShelfSeries]:
|
|
162
|
+
"""Get personalized view of library.
|
|
163
|
+
|
|
164
|
+
TODO: Add rssfeed
|
|
165
|
+
"""
|
|
166
|
+
response = await self._get(
|
|
167
|
+
endpoint=f"/api/libraries/{library_id}/personalized", params={"limit": limit}
|
|
168
|
+
)
|
|
169
|
+
return json_decode(response, list[Shelf])
|
|
151
170
|
|
|
152
171
|
async def get_library_filterdata(self, *, library_id: str) -> LibraryFilterData:
|
|
153
172
|
"""Get filterdata of library."""
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"""Calls to /api/session."""
|
|
2
2
|
|
|
3
3
|
from aioaudiobookshelf.client._base import BaseClient
|
|
4
|
-
from aioaudiobookshelf.schema.calls_session import
|
|
4
|
+
from aioaudiobookshelf.schema.calls_session import (
|
|
5
|
+
CloseOpenSessionsParameters,
|
|
6
|
+
SyncOpenSessionParameters,
|
|
7
|
+
)
|
|
5
8
|
from aioaudiobookshelf.schema.session import PlaybackSessionExpanded
|
|
6
9
|
|
|
7
10
|
|
|
@@ -24,7 +27,11 @@ class SessionClient(BaseClient):
|
|
|
24
27
|
)
|
|
25
28
|
return psession
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
async def sync_open_session(
|
|
31
|
+
self, *, session_id: str, parameters: SyncOpenSessionParameters
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Sync an open session."""
|
|
34
|
+
await self._post(f"/api/session/{session_id}/sync", data=parameters.to_dict())
|
|
28
35
|
|
|
29
36
|
async def close_open_session(
|
|
30
37
|
self, *, session_id: str, parameters: CloseOpenSessionsParameters | None = None
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Session Configuration."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from aiohttp.client import DEFAULT_TIMEOUT, ClientSession, ClientTimeout
|
|
8
|
+
from aiohttp.client_exceptions import ClientResponseError
|
|
9
|
+
|
|
10
|
+
from aioaudiobookshelf.exceptions import (
|
|
11
|
+
RefreshTokenExpiredError,
|
|
12
|
+
ServiceUnavailableError,
|
|
13
|
+
TokenIsMissingError,
|
|
14
|
+
)
|
|
15
|
+
from aioaudiobookshelf.helpers import get_login_response
|
|
16
|
+
from aioaudiobookshelf.schema.calls_login import RefreshResponse
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(kw_only=True)
|
|
20
|
+
class SessionConfiguration:
|
|
21
|
+
"""Session configuration for abs client.
|
|
22
|
+
|
|
23
|
+
Relevant token information for v2.26 and above:
|
|
24
|
+
https://github.com/advplyr/audiobookshelf/discussions/4460
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
session: ClientSession
|
|
28
|
+
url: str
|
|
29
|
+
verify_ssl: bool = True
|
|
30
|
+
token: str | None = None # pre v2.26 token or api token if > v2.26
|
|
31
|
+
access_token: str | None = None # > v2.26
|
|
32
|
+
refresh_token: str | None = None # > v2.26
|
|
33
|
+
auto_refresh: bool = True # automatically refresh access token, should it be expired.
|
|
34
|
+
pagination_items_per_page: int = 10
|
|
35
|
+
timeout: ClientTimeout = DEFAULT_TIMEOUT
|
|
36
|
+
logger: logging.Logger | None = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def headers(self) -> dict[str, str]:
|
|
40
|
+
"""Session headers.
|
|
41
|
+
|
|
42
|
+
These are normal request headers.
|
|
43
|
+
"""
|
|
44
|
+
if self.token is not None:
|
|
45
|
+
return {"Authorization": f"Bearer {self.token}"}
|
|
46
|
+
if self.access_token is not None:
|
|
47
|
+
return {"Authorization": f"Bearer {self.access_token}"}
|
|
48
|
+
raise TokenIsMissingError("Token not set.")
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def headers_refresh_logout(self) -> dict[str, str]:
|
|
52
|
+
"""Session headers for /auth/refresh and /logout.
|
|
53
|
+
|
|
54
|
+
Only v2.26 and above.
|
|
55
|
+
"""
|
|
56
|
+
if self.refresh_token is None:
|
|
57
|
+
raise TokenIsMissingError("Refresh token not set.")
|
|
58
|
+
return {"x-refresh-token": self.refresh_token}
|
|
59
|
+
|
|
60
|
+
def __post_init__(self) -> None:
|
|
61
|
+
"""Post init."""
|
|
62
|
+
self.url = self.url.rstrip("/")
|
|
63
|
+
self.__refresh_lock = asyncio.Lock()
|
|
64
|
+
|
|
65
|
+
async def refresh(self) -> None:
|
|
66
|
+
"""Refresh access_token with refresh token.
|
|
67
|
+
|
|
68
|
+
v2.26 and above
|
|
69
|
+
"""
|
|
70
|
+
if self.__refresh_lock.locked():
|
|
71
|
+
return
|
|
72
|
+
async with self.__refresh_lock:
|
|
73
|
+
try:
|
|
74
|
+
endpoint = "auth/refresh"
|
|
75
|
+
response = await self.session.post(
|
|
76
|
+
f"{self.url}/{endpoint}",
|
|
77
|
+
ssl=self.verify_ssl,
|
|
78
|
+
headers=self.headers_refresh_logout,
|
|
79
|
+
raise_for_status=True,
|
|
80
|
+
)
|
|
81
|
+
except ClientResponseError as err:
|
|
82
|
+
if err.code == 503:
|
|
83
|
+
raise ServiceUnavailableError from err
|
|
84
|
+
raise RefreshTokenExpiredError from err
|
|
85
|
+
data = await response.read()
|
|
86
|
+
refresh_response = RefreshResponse.from_json(data)
|
|
87
|
+
assert refresh_response.user.access_token is not None
|
|
88
|
+
assert refresh_response.user.refresh_token is not None
|
|
89
|
+
self.access_token = refresh_response.user.access_token
|
|
90
|
+
self.refresh_token = refresh_response.user.refresh_token
|
|
91
|
+
|
|
92
|
+
async def authenticate(self, *, username: str, password: str) -> None:
|
|
93
|
+
"""Relogin and update tokens if refresh token expired."""
|
|
94
|
+
async with self.__refresh_lock:
|
|
95
|
+
login_response = await get_login_response(
|
|
96
|
+
session_config=self, username=username, password=password
|
|
97
|
+
)
|
|
98
|
+
if login_response.user.access_token is None:
|
|
99
|
+
# pre v2.26
|
|
100
|
+
assert login_response.user.token is not None
|
|
101
|
+
self.token = login_response.user.token
|
|
102
|
+
return
|
|
103
|
+
assert login_response.user.access_token is not None
|
|
104
|
+
assert login_response.user.refresh_token is not None
|
|
105
|
+
self.access_token = login_response.user.access_token
|
|
106
|
+
self.refresh_token = login_response.user.refresh_token
|
aioaudiobookshelf/exceptions.py
CHANGED
|
@@ -15,3 +15,15 @@ class ApiError(Exception):
|
|
|
15
15
|
|
|
16
16
|
class TokenIsMissingError(Exception):
|
|
17
17
|
"""Exception raised if token is missing."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AccessTokenExpiredError(Exception):
|
|
21
|
+
"""Exception raised if access token expired."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RefreshTokenExpiredError(Exception):
|
|
25
|
+
"""Exception raised if refresh token expired."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ServiceUnavailableError(Exception):
|
|
29
|
+
"""Raised if service is not available."""
|
aioaudiobookshelf/helpers.py
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
import base64
|
|
4
4
|
import urllib.parse
|
|
5
5
|
from enum import StrEnum
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from aiohttp.client_exceptions import ClientResponseError, InvalidUrlClientError
|
|
9
|
+
|
|
10
|
+
from aioaudiobookshelf.exceptions import LoginError
|
|
11
|
+
from aioaudiobookshelf.schema.calls_login import LoginParameters, LoginResponse
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from aioaudiobookshelf.client.session_configuration import SessionConfiguration
|
|
6
15
|
|
|
7
16
|
|
|
8
17
|
class FilterGroup(StrEnum):
|
|
@@ -54,3 +63,23 @@ def get_library_filter_string(
|
|
|
54
63
|
return f"{filter_group.value}.{_encoded}"
|
|
55
64
|
|
|
56
65
|
raise NotImplementedError(f"The {filter_group=} is not yet implemented.")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def get_login_response(
|
|
69
|
+
*, session_config: "SessionConfiguration", username: str, password: str
|
|
70
|
+
) -> LoginResponse:
|
|
71
|
+
"""Login via username and password."""
|
|
72
|
+
login_request = LoginParameters(username=username, password=password).to_dict()
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
resp = await session_config.session.post(
|
|
76
|
+
f"{session_config.url}/login",
|
|
77
|
+
json=login_request,
|
|
78
|
+
ssl=session_config.verify_ssl,
|
|
79
|
+
raise_for_status=True,
|
|
80
|
+
# adapt > v2.26.0 https://github.com/advplyr/audiobookshelf/discussions/4460
|
|
81
|
+
headers={"x-return-tokens": "true"},
|
|
82
|
+
)
|
|
83
|
+
except (ClientResponseError, InvalidUrlClientError) as exc:
|
|
84
|
+
raise LoginError from exc
|
|
85
|
+
return LoginResponse.from_json(await resp.read())
|
aioaudiobookshelf/schema/book.py
CHANGED
|
@@ -18,7 +18,7 @@ class EBookFile(_BaseModel):
|
|
|
18
18
|
|
|
19
19
|
ino: str
|
|
20
20
|
metadata: FileMetadata
|
|
21
|
-
ebook_format: Annotated[str, Alias("ebookFormat")]
|
|
21
|
+
ebook_format: Annotated[str | None, Alias("ebookFormat")] = None
|
|
22
22
|
added_at: Annotated[int, Alias("addedAt")] # time in ms since unix epoch
|
|
23
23
|
updated_at: Annotated[int, Alias("updatedAt")] # time in ms since unix epoch
|
|
24
24
|
|
aioaudiobookshelf/schema/file.py
CHANGED
|
@@ -17,6 +17,7 @@ class FileMetadata(_BaseModel):
|
|
|
17
17
|
path: str
|
|
18
18
|
relative_path: Annotated[str, Alias("relPath")]
|
|
19
19
|
size: int # in bytes
|
|
20
|
-
|
|
20
|
+
# might not be present, see https://github.com/music-assistant/support/issues/3914
|
|
21
|
+
modified_time_ms: Annotated[int | None, Alias("mtimeMs")] = None
|
|
21
22
|
changed_time_ms: Annotated[int, Alias("ctimeMs")]
|
|
22
23
|
created_time_ms: Annotated[int, Alias("birthtimeMs")] = 0 # 0 if unknown
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Schema for shelf, the response object to library's personalized view."""
|
|
2
|
+
# Discriminators don't work together with aliases.
|
|
3
|
+
# https://github.com/Fatal1ty/mashumaro/issues/254
|
|
4
|
+
# ruff: noqa: N815
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
from mashumaro.types import Alias, Discriminator
|
|
11
|
+
|
|
12
|
+
from aioaudiobookshelf.schema.author import AuthorExpanded
|
|
13
|
+
from aioaudiobookshelf.schema.book import BookMinified
|
|
14
|
+
|
|
15
|
+
from . import _BaseModel
|
|
16
|
+
from .library import LibraryMediaType, _LibraryItemBase
|
|
17
|
+
from .podcast import PodcastEpisode, PodcastMinified
|
|
18
|
+
from .series import Series
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ShelfId(StrEnum):
|
|
22
|
+
"""ShelfId."""
|
|
23
|
+
|
|
24
|
+
LISTEN_AGAIN = "listen-again"
|
|
25
|
+
CONTINUE_LISTENING = "continue-listening"
|
|
26
|
+
CONTINUE_SERIES = "continue-series"
|
|
27
|
+
RECOMMENDED = "recommended"
|
|
28
|
+
RECENTLY_ADDED = "recently-added"
|
|
29
|
+
EPISODES_RECENTLY_ADDED = "episodes-recently-added"
|
|
30
|
+
RECENT_SERIES = "recent-series"
|
|
31
|
+
NEWEST_AUTHORS = "newest-authors"
|
|
32
|
+
NEWEST_EPISODES = "newest-episodes"
|
|
33
|
+
DISCOVER = "discover"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(kw_only=True)
|
|
37
|
+
class ShelfLibraryItemMinified(_LibraryItemBase):
|
|
38
|
+
"""ShelfLibraryItemMinified.
|
|
39
|
+
|
|
40
|
+
Beside using type, there is another distinction on which attributes
|
|
41
|
+
are available based on the id. We allow ourselves the easy route
|
|
42
|
+
here, and just make all of them optional.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
# episode (type) and
|
|
46
|
+
# id: continue-listening, listen-again, episodes-recently-added
|
|
47
|
+
recent_episode: Annotated[PodcastEpisode | None, Alias("recentEpisode")] = None
|
|
48
|
+
|
|
49
|
+
# id: continue-listening
|
|
50
|
+
progress_last_update_ms: Annotated[int | None, Alias("progressLastUpdate")] = None
|
|
51
|
+
|
|
52
|
+
# id: continue-series
|
|
53
|
+
previous_book_in_progress_last_update_ms: Annotated[
|
|
54
|
+
int | None, Alias("prevBookInProgressLastUpdate")
|
|
55
|
+
] = None
|
|
56
|
+
# TODO: minified item has seriesSequence, which we currently ignore
|
|
57
|
+
|
|
58
|
+
# id: recommended
|
|
59
|
+
weight: float | None = None
|
|
60
|
+
|
|
61
|
+
# id: listen-again
|
|
62
|
+
finished_at_ms: Annotated[int | None, Alias("finishedAt")] = None
|
|
63
|
+
|
|
64
|
+
# This and the two subclasses are copied over from libraries, as we otherwise
|
|
65
|
+
# face issues with the discriminator
|
|
66
|
+
class Config(_LibraryItemBase.Config):
|
|
67
|
+
"""Config."""
|
|
68
|
+
|
|
69
|
+
discriminator = Discriminator(
|
|
70
|
+
field="mediaType",
|
|
71
|
+
include_subtypes=True,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
num_files: Annotated[int, Alias("numFiles")]
|
|
75
|
+
size: int
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(kw_only=True)
|
|
79
|
+
class LibraryItemMinifiedBook(ShelfLibraryItemMinified):
|
|
80
|
+
"""LibraryItemMinifiedBook."""
|
|
81
|
+
|
|
82
|
+
media: BookMinified
|
|
83
|
+
mediaType: LibraryMediaType = LibraryMediaType.BOOK
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(kw_only=True)
|
|
87
|
+
class LibraryItemMinifiedPodcast(ShelfLibraryItemMinified):
|
|
88
|
+
"""LibraryItemMinifiedPodcast."""
|
|
89
|
+
|
|
90
|
+
media: PodcastMinified
|
|
91
|
+
mediaType: LibraryMediaType = LibraryMediaType.PODCAST
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(kw_only=True)
|
|
95
|
+
class LibraryItemMinifiedBookSeriesShelf(ShelfLibraryItemMinified):
|
|
96
|
+
"""LibraryItemMinifiedBookSeriesShelf."""
|
|
97
|
+
|
|
98
|
+
# this appears not to be around?
|
|
99
|
+
series_sequence: Annotated[str | int | None, Alias("seriesSequence")] = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(kw_only=True)
|
|
103
|
+
class SeriesShelf(Series):
|
|
104
|
+
"""SeriesShelf."""
|
|
105
|
+
|
|
106
|
+
books: list[LibraryItemMinifiedBookSeriesShelf]
|
|
107
|
+
in_progress: Annotated[bool | None, Alias("inProgress")] = None
|
|
108
|
+
has_active_book: Annotated[bool | None, Alias("hasActiveBook")] = None
|
|
109
|
+
hide_from_continue_listening: Annotated[bool | None, Alias("hideFromContinueListening")] = None
|
|
110
|
+
book_in_progress_last_update_ms: Annotated[int | None, Alias("bookInProgressLastUpdate")] = None
|
|
111
|
+
first_book_unread: Annotated[
|
|
112
|
+
LibraryItemMinifiedBookSeriesShelf | None, Alias("firstBookUnread")
|
|
113
|
+
] = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ShelfType(StrEnum):
|
|
117
|
+
"""ShelfType."""
|
|
118
|
+
|
|
119
|
+
BOOK = "book"
|
|
120
|
+
SERIES = "series"
|
|
121
|
+
AUTHORS = "authors"
|
|
122
|
+
EPISODE = "episode"
|
|
123
|
+
PODCAST = "podcast"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(kw_only=True)
|
|
127
|
+
class _ShelfBase(_BaseModel):
|
|
128
|
+
"""Shelf."""
|
|
129
|
+
|
|
130
|
+
id_: Annotated[ShelfId | str, Alias("id")]
|
|
131
|
+
label: str
|
|
132
|
+
label_string_key: Annotated[str, Alias("labelStringKey")]
|
|
133
|
+
type_: Annotated[ShelfType, Alias("type")]
|
|
134
|
+
category: str | None = None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(kw_only=True)
|
|
138
|
+
class Shelf(_ShelfBase):
|
|
139
|
+
"""Shelf."""
|
|
140
|
+
|
|
141
|
+
class Config(_ShelfBase.Config):
|
|
142
|
+
"""Config."""
|
|
143
|
+
|
|
144
|
+
discriminator = Discriminator(
|
|
145
|
+
field="type",
|
|
146
|
+
include_subtypes=True,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(kw_only=True)
|
|
151
|
+
class ShelfBook(Shelf):
|
|
152
|
+
"""ShelfBook."""
|
|
153
|
+
|
|
154
|
+
type = ShelfType.BOOK
|
|
155
|
+
entities: list[ShelfLibraryItemMinified]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(kw_only=True)
|
|
159
|
+
class ShelfPodcast(Shelf):
|
|
160
|
+
"""ShelfBook."""
|
|
161
|
+
|
|
162
|
+
type = ShelfType.PODCAST
|
|
163
|
+
entities: list[ShelfLibraryItemMinified]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(kw_only=True)
|
|
167
|
+
class ShelfEpisode(Shelf):
|
|
168
|
+
"""ShelfBook."""
|
|
169
|
+
|
|
170
|
+
type = ShelfType.EPISODE
|
|
171
|
+
entities: list[ShelfLibraryItemMinified]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(kw_only=True)
|
|
175
|
+
class ShelfAuthors(Shelf):
|
|
176
|
+
"""ShelfAuthor."""
|
|
177
|
+
|
|
178
|
+
type = ShelfType.AUTHORS
|
|
179
|
+
|
|
180
|
+
entities: list[AuthorExpanded]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(kw_only=True)
|
|
184
|
+
class ShelfSeries(Shelf):
|
|
185
|
+
"""ShelfSeries."""
|
|
186
|
+
|
|
187
|
+
type = ShelfType.SERIES
|
|
188
|
+
entities: list[SeriesShelf]
|
aioaudiobookshelf/schema/user.py
CHANGED
|
@@ -52,7 +52,13 @@ class _UserBase(_BaseModel):
|
|
|
52
52
|
class User(_UserBase):
|
|
53
53
|
"""User."""
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
# see https://github.com/advplyr/audiobookshelf/discussions/4460
|
|
56
|
+
# new in v2.26.0 old token system will be removed in the future
|
|
57
|
+
# we make them optional to have some backwards compatibility
|
|
58
|
+
token: str | None = None
|
|
59
|
+
# will only be returned if x-return-tokens is set to true in header
|
|
60
|
+
refresh_token: Annotated[str | None, Alias("refreshToken")] = None
|
|
61
|
+
access_token: Annotated[str | None, Alias("accessToken")] = None
|
|
56
62
|
|
|
57
63
|
media_progress: Annotated[list[MediaProgress], Alias("mediaProgress")]
|
|
58
64
|
series_hide_from_continue_listening: Annotated[
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: aioaudiobookshelf
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: Async library for Audiobookshelf
|
|
5
5
|
Author-email: Fabian Munkes <105975993+fmunkes@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -27,6 +27,7 @@ Requires-Dist: syrupy==4.8.1; extra == "test"
|
|
|
27
27
|
Requires-Dist: tomli==2.2.1; extra == "test"
|
|
28
28
|
Requires-Dist: ruff==0.9.2; extra == "test"
|
|
29
29
|
Requires-Dist: bumpver; extra == "test"
|
|
30
|
+
Dynamic: license-file
|
|
30
31
|
|
|
31
32
|
# aioaudiobookshelf
|
|
32
33
|
Async python library to interact with
|
|
@@ -109,7 +110,7 @@ async def abs_basics():
|
|
|
109
110
|
|
|
110
111
|
# get a single podcast
|
|
111
112
|
podcast_id = "dda96167-eaad-4012-83e1-149c6700d3e8"
|
|
112
|
-
podcast_expanded = client.get_library_item_podcast(podcast_id=podcast_id, expanded=True)
|
|
113
|
+
podcast_expanded = await client.get_library_item_podcast(podcast_id=podcast_id, expanded=True)
|
|
113
114
|
|
|
114
115
|
|
|
115
116
|
asyncio.run(abs_basics())
|
|
@@ -1,33 +1,34 @@
|
|
|
1
|
-
aioaudiobookshelf/__init__.py,sha256=
|
|
2
|
-
aioaudiobookshelf/exceptions.py,sha256=
|
|
3
|
-
aioaudiobookshelf/helpers.py,sha256=
|
|
4
|
-
aioaudiobookshelf/client/__init__.py,sha256=
|
|
5
|
-
aioaudiobookshelf/client/_base.py,sha256=
|
|
6
|
-
aioaudiobookshelf/client/authors.py,sha256=
|
|
1
|
+
aioaudiobookshelf/__init__.py,sha256=xebDvWxDTQNfKKQi9I9V7yKxke3WZoBks6e2nYDPle8,3508
|
|
2
|
+
aioaudiobookshelf/exceptions.py,sha256=G3z99KrxHoSoj7kpSRVpa7Rp_RkL3jmW6266ak8h_p0,661
|
|
3
|
+
aioaudiobookshelf/helpers.py,sha256=gvoBWsu6PSiGYyXe5BH9TP6WydzAMp_6Ye9RUivvk9Y,2700
|
|
4
|
+
aioaudiobookshelf/client/__init__.py,sha256=6xwDrY0xWWC8cXHn5GK3Mc1jPpmmtfJ9HGO9aryKL8U,7993
|
|
5
|
+
aioaudiobookshelf/client/_base.py,sha256=J_RjItJFaq17Po66aEzwrpq6PiCns3xYXWAtewy48K0,7584
|
|
6
|
+
aioaudiobookshelf/client/authors.py,sha256=9F8_tiTqh-5Srn_5I0wdEa1qs_495UctzSF6OlxFOJw,1808
|
|
7
7
|
aioaudiobookshelf/client/collections_.py,sha256=Z3r7dxHhzMm_cGCMRJkVzN5-Eesftib3d0bs4iXmmzY,879
|
|
8
8
|
aioaudiobookshelf/client/items.py,sha256=4yWy7Shd6sbKj4MSGYJXklXs-yZH2q95qGIm4L-PS5o,3624
|
|
9
|
-
aioaudiobookshelf/client/libraries.py,sha256=
|
|
9
|
+
aioaudiobookshelf/client/libraries.py,sha256=VK1VqX9V6_8PAhPVx-eIPOgsS8t_U1a6iFML2nOyi14,6852
|
|
10
10
|
aioaudiobookshelf/client/me.py,sha256=cYmXNoFJHZgEI6Ds19Pf6gNQMBVOqrtcKmiyXHFFIL0,3586
|
|
11
11
|
aioaudiobookshelf/client/playlists.py,sha256=sTsmuBqjSlKyTo31mc9Ha-WtzX7bh0hH7n1BcnlO7YY,875
|
|
12
12
|
aioaudiobookshelf/client/podcasts.py,sha256=f1ECD43cn4EIwWpRvwM_RosfzDr8popFDFbRYgCZQxs,684
|
|
13
13
|
aioaudiobookshelf/client/series.py,sha256=bS4RO8k1Im5wpeKt0oNcBSeeZCRIGCi9otTCEOy7xMo,820
|
|
14
|
-
aioaudiobookshelf/client/session.py,sha256=
|
|
14
|
+
aioaudiobookshelf/client/session.py,sha256=1eMI_caj6dSTtyC3aqKgLCDqWLEOodFtBkEGoqTOmnw,1508
|
|
15
|
+
aioaudiobookshelf/client/session_configuration.py,sha256=_Y-uDA9iW87siiN9xe-pCOxMS0V41Fl6MGWY4EuZ1tA,3958
|
|
15
16
|
aioaudiobookshelf/schema/__init__.py,sha256=vEEwPVweT0j2VqlBj2V34Xk5J9OBZ7zkd6eLOUx-RdU,358
|
|
16
|
-
aioaudiobookshelf/schema/audio.py,sha256=
|
|
17
|
+
aioaudiobookshelf/schema/audio.py,sha256=PFKkGsZgJSDYJS-7VwoTvYTcVfR8ip8qOuE8hQa9TtE,2105
|
|
17
18
|
aioaudiobookshelf/schema/author.py,sha256=OZGcCUHOfXvN8N2SRUiIRnDS2Lz3UrQFWOaOZ1sKV0U,976
|
|
18
|
-
aioaudiobookshelf/schema/book.py,sha256=
|
|
19
|
+
aioaudiobookshelf/schema/book.py,sha256=mf_PZqClvbfoxUQtm88Cqz14xT-594NZPi6nkQe1_Hk,3386
|
|
19
20
|
aioaudiobookshelf/schema/calls_authors.py,sha256=eIvdyEcrmXuOxFJuCX39cGU9HSZprl40yKn8s2VjfAM,703
|
|
20
21
|
aioaudiobookshelf/schema/calls_collections.py,sha256=nXurha41Y1atAuOWG9aXJbYo66qgcFUaW02QPEVdo4w,319
|
|
21
22
|
aioaudiobookshelf/schema/calls_items.py,sha256=KF8s6WNbsVUIxAuN9TZBBGsysnxfw-FQWM9XRTljOpo,1547
|
|
22
23
|
aioaudiobookshelf/schema/calls_library.py,sha256=IX4vf55fUW5wLADo1QnCIg3tEhtQ2UuyKsZWpx5CmtU,2543
|
|
23
|
-
aioaudiobookshelf/schema/calls_login.py,sha256=
|
|
24
|
+
aioaudiobookshelf/schema/calls_login.py,sha256=DpIBtAzg5uQtS0CLDfEXHkOh_RGfDI5epFVnziFsEEs,853
|
|
24
25
|
aioaudiobookshelf/schema/calls_me.py,sha256=jdpvExRytoqX8FHkzq4N9RtKn50cmTc_B693e9lLCHQ,693
|
|
25
26
|
aioaudiobookshelf/schema/calls_playlists.py,sha256=Ll1scstd1NNc6PXgkhmWUfqSzIR07LKzqBCEvrVHACE,305
|
|
26
27
|
aioaudiobookshelf/schema/calls_series.py,sha256=aUdlTR01UE2APN0pGS1h7x0ydsREqZf53mKaHq2zeh4,597
|
|
27
|
-
aioaudiobookshelf/schema/calls_session.py,sha256=
|
|
28
|
+
aioaudiobookshelf/schema/calls_session.py,sha256=E90uPxkZrV1FUTSYS9owbzTm8HaRjH4CRmb88XrGqOA,474
|
|
28
29
|
aioaudiobookshelf/schema/collection.py,sha256=fB1VFQXE0hzM_NyWEHj9A11VztD_8rYo4t9OWkDj-Lc,883
|
|
29
30
|
aioaudiobookshelf/schema/events_socket.py,sha256=r24ssixH0dWfyOhv0K5P6GgzGrbYC9Yh8Y2w76-bkeM,1420
|
|
30
|
-
aioaudiobookshelf/schema/file.py,sha256=
|
|
31
|
+
aioaudiobookshelf/schema/file.py,sha256=frymCwsiqOh6fnZPBnkUz1pfyUuKkH4ssRd2WHpsajk,639
|
|
31
32
|
aioaudiobookshelf/schema/folder.py,sha256=3S6aA306JHhOGtCnawUwMJv13_AXsHmhmNX5RzbjnqY,414
|
|
32
33
|
aioaudiobookshelf/schema/library.py,sha256=6Gf9DVjKPJhbmKBaXXSbeUddN-VeUx7mIHZrx7HrfMQ,6174
|
|
33
34
|
aioaudiobookshelf/schema/media_progress.py,sha256=nIISrNvkFV0zmxkGTePuGaw9xW21lTo7ceDdLfG2ffQ,1390
|
|
@@ -37,9 +38,10 @@ aioaudiobookshelf/schema/series.py,sha256=XmwDvMchb-W2y-Tm-eKiE-3rPjs4kXC3Ib2dZO
|
|
|
37
38
|
aioaudiobookshelf/schema/series_books.py,sha256=rs8a4JmSHqJR6WPA2XKXmC6XDq8D9wmzxftLlPhvub8,868
|
|
38
39
|
aioaudiobookshelf/schema/server.py,sha256=yWBRxtwtX1USs43yGFuDIM3jfWbmduW3GRahisOUCqw,2726
|
|
39
40
|
aioaudiobookshelf/schema/session.py,sha256=jqCHNUthuzE6jhgG3UwFgagl1HA_rfDwn6Te38jaqC8,2684
|
|
40
|
-
aioaudiobookshelf/schema/
|
|
41
|
-
aioaudiobookshelf
|
|
42
|
-
aioaudiobookshelf-0.1.
|
|
43
|
-
aioaudiobookshelf-0.1.
|
|
44
|
-
aioaudiobookshelf-0.1.
|
|
45
|
-
aioaudiobookshelf-0.1.
|
|
41
|
+
aioaudiobookshelf/schema/shelf.py,sha256=npPr5iacm6b5FfJYF9_lFZaitJNJ3SVvBOScbxlF5MQ,5064
|
|
42
|
+
aioaudiobookshelf/schema/user.py,sha256=cm2Oev9i062mjM3Ku-yaAjm_o4-sJOkJTczdq43_qrY,2485
|
|
43
|
+
aioaudiobookshelf-0.1.11.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
44
|
+
aioaudiobookshelf-0.1.11.dist-info/METADATA,sha256=uMPo-whmeNF-pi58S6OOA-7vejepwzgNS4lcTfodzvM,4406
|
|
45
|
+
aioaudiobookshelf-0.1.11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
46
|
+
aioaudiobookshelf-0.1.11.dist-info/top_level.txt,sha256=2_I2_xz98xmVIT84pcF3tlq3NdZNKskfs7BqUmYZylk,18
|
|
47
|
+
aioaudiobookshelf-0.1.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|