aioaudiobookshelf 0.1.7__py3-none-any.whl → 0.1.8__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.

@@ -2,29 +2,13 @@
2
2
 
3
3
  from aiohttp.client_exceptions import ClientResponseError, InvalidUrlClientError
4
4
 
5
- from aioaudiobookshelf.client import AdminClient, SessionConfiguration, SocketClient, UserClient
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.schema.calls_login import AuthorizeResponse, LoginParameters, LoginResponse
8
+ from aioaudiobookshelf.helpers import get_login_response
9
+ from aioaudiobookshelf.schema.calls_login import AuthorizeResponse
8
10
 
9
- __version__ = "0.1.7"
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.8"
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 _get_login_response(
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 _get_login_response(
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 _get_login_response(
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.exceptions import BadUserError, TokenIsMissingError
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__(self, session_config: SessionConfiguration) -> None:
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
- await self.client.emit(event="auth", data=self.session_config.token)
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
- if not self.session_config.token:
24
- self.session_config.token = login_response.user.token
25
- self._token = self.session_config.token
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__)
@@ -40,7 +49,11 @@ class BaseClient:
40
49
 
41
50
  @property
42
51
  def token(self) -> str:
43
- return self._token
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
44
57
 
45
58
  @abstractmethod
46
59
  def _verify_user(self) -> None:
@@ -52,26 +65,56 @@ class BaseClient:
52
65
  data: dict[str, Any] | None = None,
53
66
  ) -> bytes:
54
67
  """POST request to abs api."""
55
- try:
56
- response = await self.session_config.session.post(
68
+
69
+ async def _request() -> ClientResponse:
70
+ return await self.session_config.session.post(
57
71
  f"{self.session_config.url}/{endpoint}",
58
72
  json=data,
59
73
  ssl=self.session_config.verify_ssl,
60
74
  headers=self.session_config.headers,
61
75
  raise_for_status=True,
62
76
  )
77
+
78
+ try:
79
+ response = await _request()
80
+ except ClientResponseError as exc:
81
+ if exc.code == 401:
82
+ if self.session_config.auto_refresh:
83
+ self.logger.debug("Auto refreshing tokens.")
84
+ await self.refresh()
85
+ else:
86
+ raise AccessTokenExpiredError from exc
87
+ else:
88
+ raise ApiError(f"API POST call to {endpoint} failed.") from exc
89
+
90
+ # TODO: remove redundant clause
91
+ try:
92
+ response = await _request()
63
93
  except ClientResponseError as exc:
64
94
  raise ApiError(f"API POST call to {endpoint} failed.") from exc
95
+
65
96
  return await response.read()
66
97
 
67
98
  async def _get(self, endpoint: str, params: dict[str, str | int] | None = None) -> bytes:
68
99
  """GET request to abs api."""
69
- response = await self.session_config.session.get(
70
- f"{self.session_config.url}/{endpoint}",
71
- params=params,
72
- ssl=self.session_config.verify_ssl,
73
- headers=self.session_config.headers,
74
- )
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
+ )
108
+
109
+ response = await _request()
110
+ if response.status == 401:
111
+ if self.session_config.auto_refresh:
112
+ self.logger.debug("Auto refreshing tokens.")
113
+ await self.refresh()
114
+ response = await _request()
115
+ else:
116
+ raise AccessTokenExpiredError
117
+
75
118
  status = response.status
76
119
  if response.content_type == "application/json" and status == 200:
77
120
  return await response.read()
@@ -81,7 +124,8 @@ class BaseClient:
81
124
 
82
125
  async def _patch(self, endpoint: str, data: dict[str, Any] | None = None) -> None:
83
126
  """PATCH request to abs api."""
84
- try:
127
+
128
+ async def _request() -> None:
85
129
  await self.session_config.session.patch(
86
130
  f"{self.session_config.url}/{endpoint}",
87
131
  json=data,
@@ -89,21 +133,65 @@ class BaseClient:
89
133
  headers=self.session_config.headers,
90
134
  raise_for_status=True,
91
135
  )
136
+
137
+ try:
138
+ await _request()
139
+ except ClientResponseError as exc:
140
+ if exc.code == 401:
141
+ if self.session_config.auto_refresh:
142
+ self.logger.debug("Auto refreshing tokens.")
143
+ await self.refresh()
144
+ else:
145
+ raise AccessTokenExpiredError from exc
146
+ else:
147
+ raise ApiError(f"API PATCH call to {endpoint} failed.") from exc
148
+
149
+ try:
150
+ await _request()
92
151
  except ClientResponseError as exc:
93
152
  raise ApiError(f"API PATCH call to {endpoint} failed.") from exc
94
153
 
95
154
  async def _delete(self, endpoint: str) -> None:
96
155
  """DELETE request to abs api."""
97
- try:
156
+
157
+ async def _request() -> None:
98
158
  await self.session_config.session.delete(
99
159
  f"{self.session_config.url}/{endpoint}",
100
160
  ssl=self.session_config.verify_ssl,
101
161
  headers=self.session_config.headers,
102
162
  raise_for_status=True,
103
163
  )
164
+
165
+ try:
166
+ await _request()
167
+ except ClientResponseError as exc:
168
+ if exc.code == 401:
169
+ if self.session_config.auto_refresh:
170
+ self.logger.debug("Auto refreshing tokens.")
171
+ await self.refresh()
172
+ else:
173
+ raise AccessTokenExpiredError from exc
174
+ else:
175
+ raise ApiError(f"API DELETE call to {endpoint} failed.") from exc
176
+
177
+ try:
178
+ await _request()
104
179
  except ClientResponseError as exc:
105
180
  raise ApiError(f"API DELETE call to {endpoint} failed.") from exc
106
181
 
182
+ async def refresh(self) -> None:
183
+ """Refresh tokens."""
184
+ await self.session_config.refresh()
185
+
107
186
  async def logout(self) -> None:
108
187
  """Logout client."""
109
- await self._post("logout")
188
+ if self.session_config.refresh_token is not None:
189
+ # v2.26 and above
190
+ await self.session_config.session.post(
191
+ f"{self.session_config.url}/logout",
192
+ ssl=self.session_config.verify_ssl,
193
+ headers=self.session_config.headers_refresh_logout,
194
+ raise_for_status=True,
195
+ )
196
+ else:
197
+ await self._post("logout")
@@ -0,0 +1,105 @@
1
+ """Session Configuration."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from dataclasses import dataclass
6
+
7
+ from aiohttp.client import ClientSession
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
+ logger: logging.Logger | None = None
36
+
37
+ @property
38
+ def headers(self) -> dict[str, str]:
39
+ """Session headers.
40
+
41
+ These are normal request headers.
42
+ """
43
+ if self.token is not None:
44
+ return {"Authorization": f"Bearer {self.token}"}
45
+ if self.access_token is not None:
46
+ return {"Authorization": f"Bearer {self.access_token}"}
47
+ raise TokenIsMissingError("Token not set.")
48
+
49
+ @property
50
+ def headers_refresh_logout(self) -> dict[str, str]:
51
+ """Session headers for /auth/refresh and /logout.
52
+
53
+ Only v2.26 and above.
54
+ """
55
+ if self.refresh_token is None:
56
+ raise TokenIsMissingError("Refresh token not set.")
57
+ return {"x-refresh-token": self.refresh_token}
58
+
59
+ def __post_init__(self) -> None:
60
+ """Post init."""
61
+ self.url = self.url.rstrip("/")
62
+ self.__refresh_lock = asyncio.Lock()
63
+
64
+ async def refresh(self) -> None:
65
+ """Refresh access_token with refresh token.
66
+
67
+ v2.26 and above
68
+ """
69
+ if self.__refresh_lock.locked():
70
+ return
71
+ async with self.__refresh_lock:
72
+ try:
73
+ endpoint = "auth/refresh"
74
+ response = await self.session.post(
75
+ f"{self.url}/{endpoint}",
76
+ ssl=self.verify_ssl,
77
+ headers=self.headers_refresh_logout,
78
+ raise_for_status=True,
79
+ )
80
+ except ClientResponseError as err:
81
+ if err.code == 503:
82
+ raise ServiceUnavailableError from err
83
+ raise RefreshTokenExpiredError from err
84
+ data = await response.read()
85
+ refresh_response = RefreshResponse.from_json(data)
86
+ assert refresh_response.user.access_token is not None
87
+ assert refresh_response.user.refresh_token is not None
88
+ self.access_token = refresh_response.user.access_token
89
+ self.refresh_token = refresh_response.user.refresh_token
90
+
91
+ async def authenticate(self, *, username: str, password: str) -> None:
92
+ """Relogin and update tokens if refresh token expired."""
93
+ async with self.__refresh_lock:
94
+ login_response = await get_login_response(
95
+ session_config=self, username=username, password=password
96
+ )
97
+ if login_response.user.access_token is None:
98
+ # pre v2.26
99
+ assert login_response.user.token is not None
100
+ self.token = login_response.user.token
101
+ return
102
+ assert login_response.user.access_token is not None
103
+ assert login_response.user.refresh_token is not None
104
+ self.access_token = login_response.user.access_token
105
+ self.refresh_token = login_response.user.refresh_token
@@ -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."""
@@ -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())
@@ -30,3 +30,6 @@ class LoginResponse(DataClassJSONMixin):
30
30
 
31
31
  # api/authorize, if token is used for authorization
32
32
  AuthorizeResponse = LoginResponse
33
+
34
+ # auth/refresh, new in v2.26
35
+ RefreshResponse = LoginResponse
@@ -52,7 +52,13 @@ class _UserBase(_BaseModel):
52
52
  class User(_UserBase):
53
53
  """User."""
54
54
 
55
- token: str
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
1
  Metadata-Version: 2.4
2
2
  Name: aioaudiobookshelf
3
- Version: 0.1.7
3
+ Version: 0.1.8
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
@@ -1,8 +1,8 @@
1
- aioaudiobookshelf/__init__.py,sha256=92IqTPNA_kicw_jm0NS78m11LoMTLkiSNiZHP0cyXzU,4138
2
- aioaudiobookshelf/exceptions.py,sha256=JQNdqBaR5AhuCUBg5DH5-orkfJrx_CJpe53ignWD6vE,377
3
- aioaudiobookshelf/helpers.py,sha256=ImZntca2l39P285TVVrFah82MjrP4wXLnVd7LMoD5Ck,1581
4
- aioaudiobookshelf/client/__init__.py,sha256=YnrNm2ZmQeVqGQ8EfAKIPRI4p2cLFqCzv4uKgL6HJPM,6931
5
- aioaudiobookshelf/client/_base.py,sha256=cxzivvqAitDkpyjqr0o_GjtaoWn6-kQ4GXCrkhZ3ufI,3779
1
+ aioaudiobookshelf/__init__.py,sha256=qJeq9-dgajENDo6mWUr_LJ8NRSQFFFooL1cjgHMd6wI,3507
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=eQjybEvl79GmvLUCJKTJmuivrF9fTo6SEt_W0BHYbSw,7183
6
6
  aioaudiobookshelf/client/authors.py,sha256=8bXN0NsPvH1rvF166xUu7rVC-0P0Sp4N7oxZrO666nc,1129
7
7
  aioaudiobookshelf/client/collections_.py,sha256=Z3r7dxHhzMm_cGCMRJkVzN5-Eesftib3d0bs4iXmmzY,879
8
8
  aioaudiobookshelf/client/items.py,sha256=4yWy7Shd6sbKj4MSGYJXklXs-yZH2q95qGIm4L-PS5o,3624
@@ -12,6 +12,7 @@ aioaudiobookshelf/client/playlists.py,sha256=sTsmuBqjSlKyTo31mc9Ha-WtzX7bh0hH7n1
12
12
  aioaudiobookshelf/client/podcasts.py,sha256=f1ECD43cn4EIwWpRvwM_RosfzDr8popFDFbRYgCZQxs,684
13
13
  aioaudiobookshelf/client/series.py,sha256=bS4RO8k1Im5wpeKt0oNcBSeeZCRIGCi9otTCEOy7xMo,820
14
14
  aioaudiobookshelf/client/session.py,sha256=YWv4GkrW5fMbq1AdnMOWQHM_YzAH1U6varzsyXlP0Jc,1249
15
+ aioaudiobookshelf/client/session_configuration.py,sha256=AbDYT2OixncuStLz0ovMF83RNuyHTKKHdzaJKqSL7X0,3881
15
16
  aioaudiobookshelf/schema/__init__.py,sha256=vEEwPVweT0j2VqlBj2V34Xk5J9OBZ7zkd6eLOUx-RdU,358
16
17
  aioaudiobookshelf/schema/audio.py,sha256=PFKkGsZgJSDYJS-7VwoTvYTcVfR8ip8qOuE8hQa9TtE,2105
17
18
  aioaudiobookshelf/schema/author.py,sha256=OZGcCUHOfXvN8N2SRUiIRnDS2Lz3UrQFWOaOZ1sKV0U,976
@@ -20,7 +21,7 @@ aioaudiobookshelf/schema/calls_authors.py,sha256=eIvdyEcrmXuOxFJuCX39cGU9HSZprl4
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=squLCKxXHRxzlm4-1BOCDQ5-Zics1717JuZ89STkN5g,791
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
@@ -38,9 +39,9 @@ aioaudiobookshelf/schema/series_books.py,sha256=rs8a4JmSHqJR6WPA2XKXmC6XDq8D9wmz
38
39
  aioaudiobookshelf/schema/server.py,sha256=yWBRxtwtX1USs43yGFuDIM3jfWbmduW3GRahisOUCqw,2726
39
40
  aioaudiobookshelf/schema/session.py,sha256=jqCHNUthuzE6jhgG3UwFgagl1HA_rfDwn6Te38jaqC8,2684
40
41
  aioaudiobookshelf/schema/shelf.py,sha256=npPr5iacm6b5FfJYF9_lFZaitJNJ3SVvBOScbxlF5MQ,5064
41
- aioaudiobookshelf/schema/user.py,sha256=Zcnl6gqfc97dmHrNOHVsX_OOqwcJq-eFMhc9zVMLIs4,2057
42
- aioaudiobookshelf-0.1.7.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
43
- aioaudiobookshelf-0.1.7.dist-info/METADATA,sha256=wqjbGkX5bPV9GEyvy7RKp_QAjQaJxvyGeRbjCE03lHM,4405
44
- aioaudiobookshelf-0.1.7.dist-info/WHEEL,sha256=ooBFpIzZCPdw3uqIQsOo4qqbA4ZRPxHnOH7peeONza0,91
45
- aioaudiobookshelf-0.1.7.dist-info/top_level.txt,sha256=2_I2_xz98xmVIT84pcF3tlq3NdZNKskfs7BqUmYZylk,18
46
- aioaudiobookshelf-0.1.7.dist-info/RECORD,,
42
+ aioaudiobookshelf/schema/user.py,sha256=cm2Oev9i062mjM3Ku-yaAjm_o4-sJOkJTczdq43_qrY,2485
43
+ aioaudiobookshelf-0.1.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
44
+ aioaudiobookshelf-0.1.8.dist-info/METADATA,sha256=lnsXI8B8rQsaaoQ2NCDEpd4hD8s9lSPX25r6COqV69o,4405
45
+ aioaudiobookshelf-0.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
+ aioaudiobookshelf-0.1.8.dist-info/top_level.txt,sha256=2_I2_xz98xmVIT84pcF3tlq3NdZNKskfs7BqUmYZylk,18
47
+ aioaudiobookshelf-0.1.8.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.0.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5