shelfinventory 0.1.0__tar.gz
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.
- shelfinventory-0.1.0/LICENSE +21 -0
- shelfinventory-0.1.0/PKG-INFO +130 -0
- shelfinventory-0.1.0/README.md +110 -0
- shelfinventory-0.1.0/pyproject.toml +41 -0
- shelfinventory-0.1.0/setup.cfg +4 -0
- shelfinventory-0.1.0/src/shelfinventory/__init__.py +8 -0
- shelfinventory-0.1.0/src/shelfinventory/api.py +130 -0
- shelfinventory-0.1.0/src/shelfinventory/cli.py +257 -0
- shelfinventory-0.1.0/src/shelfinventory/rows.py +203 -0
- shelfinventory-0.1.0/src/shelfinventory.egg-info/PKG-INFO +130 -0
- shelfinventory-0.1.0/src/shelfinventory.egg-info/SOURCES.txt +13 -0
- shelfinventory-0.1.0/src/shelfinventory.egg-info/dependency_links.txt +1 -0
- shelfinventory-0.1.0/src/shelfinventory.egg-info/entry_points.txt +2 -0
- shelfinventory-0.1.0/src/shelfinventory.egg-info/top_level.txt +1 -0
- shelfinventory-0.1.0/tests/test_shelfinventory.py +475 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Younes Z.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shelfinventory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Export an Audiobookshelf library to CSV, one row per book, with the columns people asked for. Read only, no dependencies.
|
|
5
|
+
Author: Younes Z.
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Rezarys/shelfinventory
|
|
8
|
+
Project-URL: Issues, https://github.com/Rezarys/shelfinventory/issues
|
|
9
|
+
Keywords: audiobookshelf,audiobook,library,inventory,csv,export,catalog,selfhosted
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Utilities
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# shelfinventory
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
pip install shelfinventory
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Export an Audiobookshelf library to CSV, one row per book, with the columns people actually asked for on the upstream request thread. It reads over the HTTP API and writes nothing back to your server.
|
|
28
|
+
|
|
29
|
+
Why it exists: the request thread [advplyr/audiobookshelf#2085](https://github.com/advplyr/audiobookshelf/issues/2085) asks for a CSV inventory of what is really in the library. The workarounds described there are exporting from another tool, which reflects a different catalog, or opening the SQLite database with a generic client. This does the same job in one command, through the API, without touching the database file.
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
Set a token, then run it.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
export AUDIOBOOKSHELF_TOKEN=your-api-token
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
shelfinventory --url https://abs.example
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That prints the CSV on standard output. To write a file instead:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
shelfinventory --url https://abs.example --out books.csv
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The token is the API token from your Audiobookshelf account settings. It is read from the environment only, never from an argument, so it does not land in your shell history. The server address can also come from `AUDIOBOOKSHELF_URL`.
|
|
50
|
+
|
|
51
|
+
Titles contain commas, so the delimiter is yours to choose:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
shelfinventory --url https://abs.example --delimiter tab
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`--delimiter` takes one character, or one of `comma`, `semicolon`, `tab`, `pipe`, `colon`. Fields are quoted properly whatever you pick, so a comma inside a title never splits a row.
|
|
58
|
+
|
|
59
|
+
## Columns
|
|
60
|
+
|
|
61
|
+
Eighteen columns, in this order:
|
|
62
|
+
|
|
63
|
+
- `title`
|
|
64
|
+
- `subtitle`
|
|
65
|
+
- `authors`
|
|
66
|
+
- `narrators`
|
|
67
|
+
- `series_name`
|
|
68
|
+
- `series_sequence`
|
|
69
|
+
- `genres`
|
|
70
|
+
- `tags`
|
|
71
|
+
- `published_date`
|
|
72
|
+
- `published_year`
|
|
73
|
+
- `publisher`
|
|
74
|
+
- `asin`
|
|
75
|
+
- `isbn`
|
|
76
|
+
- `duration`
|
|
77
|
+
- `date_added`
|
|
78
|
+
- `library_name`
|
|
79
|
+
- `relative_path`
|
|
80
|
+
- `explicit`
|
|
81
|
+
|
|
82
|
+
That list is not mine. It is the list written on the request thread, in the same order, with one change: the single "published date or year" entry became two columns, because the server stores those as two separate fields and merging them would throw one away.
|
|
83
|
+
|
|
84
|
+
Multiple genres, tags or series are joined with `; ` inside one cell. Multiple authors and narrators get that same `; ` join only when you pass `--exact`; without it, they keep whatever separator the server itself used to join them. `duration` is `H:MM:SS` by default, or a number of seconds with `--duration seconds`. `date_added` is an ISO 8601 instant in UTC.
|
|
85
|
+
|
|
86
|
+
Every run, unless you pass `--quiet`, prints a short report on standard error saying how many books were exported and which columns your server left empty for every one of them. That report is the honest answer to whether your library really carries this metadata, and it never mixes into the CSV.
|
|
87
|
+
|
|
88
|
+
With no `--library`, every book library goes into the same CSV, and the `library_name` column says which one each row came from.
|
|
89
|
+
|
|
90
|
+
## Series sequences, and one honest caveat
|
|
91
|
+
|
|
92
|
+
The paginated library endpoint does not expose a series sequence as its own field. It exposes one joined string per book, built by the server as `"<name> #<sequence>"` entries separated by a comma. This tool splits that back apart, which is a guess and not a reading: a series whose own name contains a comma cannot be recovered that way.
|
|
93
|
+
|
|
94
|
+
If exact series and author fields matter to you, use:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
shelfinventory --url https://abs.example --exact
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
That reads each book a second time, through the endpoint that returns real lists, so nothing has to be split. It costs one extra request per book, which is why it is not the default.
|
|
101
|
+
|
|
102
|
+
## Other options
|
|
103
|
+
|
|
104
|
+
- `--library NAME` picks one library by name or id, and repeats.
|
|
105
|
+
- `--list-libraries` prints what the token can read, then stops.
|
|
106
|
+
- `--page-size N` sets how many books are fetched per request.
|
|
107
|
+
- `--timeout SECONDS` sets the request timeout.
|
|
108
|
+
- `--quiet` drops the column report.
|
|
109
|
+
|
|
110
|
+
Podcast libraries are never exported. The columns above are book columns.
|
|
111
|
+
|
|
112
|
+
## What is not verified
|
|
113
|
+
|
|
114
|
+
Being plain about this matters more than looking finished:
|
|
115
|
+
|
|
116
|
+
- No real Audiobookshelf instance has been read with this tool. The response shapes it expects come from the upstream public source code, read on 2026-09-30: `server/models/Book.js`, `server/models/LibraryItem.js` and `server/controllers/LibraryController.js`.
|
|
117
|
+
- The tests run on sample payloads written for the tests, not on captured traffic from a live server.
|
|
118
|
+
- Whether your server fills all eighteen columns is not something anyone has measured. The report printed on standard error is there so you can see it for yourself on the first run.
|
|
119
|
+
|
|
120
|
+
If you run it on a real library, please say so on the issue tracker, whether it worked or not. The paragraph above gets corrected the day someone reports a first-hand result.
|
|
121
|
+
|
|
122
|
+
## Requirements
|
|
123
|
+
|
|
124
|
+
Python 3.9 or newer. The standard library only, no dependencies.
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT. Built with AI assistance, reviewed and tested by me.
|
|
129
|
+
|
|
130
|
+
Younes Z.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# shelfinventory
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
pip install shelfinventory
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Export an Audiobookshelf library to CSV, one row per book, with the columns people actually asked for on the upstream request thread. It reads over the HTTP API and writes nothing back to your server.
|
|
8
|
+
|
|
9
|
+
Why it exists: the request thread [advplyr/audiobookshelf#2085](https://github.com/advplyr/audiobookshelf/issues/2085) asks for a CSV inventory of what is really in the library. The workarounds described there are exporting from another tool, which reflects a different catalog, or opening the SQLite database with a generic client. This does the same job in one command, through the API, without touching the database file.
|
|
10
|
+
|
|
11
|
+
## Use
|
|
12
|
+
|
|
13
|
+
Set a token, then run it.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
export AUDIOBOOKSHELF_TOKEN=your-api-token
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
shelfinventory --url https://abs.example
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That prints the CSV on standard output. To write a file instead:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
shelfinventory --url https://abs.example --out books.csv
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The token is the API token from your Audiobookshelf account settings. It is read from the environment only, never from an argument, so it does not land in your shell history. The server address can also come from `AUDIOBOOKSHELF_URL`.
|
|
30
|
+
|
|
31
|
+
Titles contain commas, so the delimiter is yours to choose:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
shelfinventory --url https://abs.example --delimiter tab
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`--delimiter` takes one character, or one of `comma`, `semicolon`, `tab`, `pipe`, `colon`. Fields are quoted properly whatever you pick, so a comma inside a title never splits a row.
|
|
38
|
+
|
|
39
|
+
## Columns
|
|
40
|
+
|
|
41
|
+
Eighteen columns, in this order:
|
|
42
|
+
|
|
43
|
+
- `title`
|
|
44
|
+
- `subtitle`
|
|
45
|
+
- `authors`
|
|
46
|
+
- `narrators`
|
|
47
|
+
- `series_name`
|
|
48
|
+
- `series_sequence`
|
|
49
|
+
- `genres`
|
|
50
|
+
- `tags`
|
|
51
|
+
- `published_date`
|
|
52
|
+
- `published_year`
|
|
53
|
+
- `publisher`
|
|
54
|
+
- `asin`
|
|
55
|
+
- `isbn`
|
|
56
|
+
- `duration`
|
|
57
|
+
- `date_added`
|
|
58
|
+
- `library_name`
|
|
59
|
+
- `relative_path`
|
|
60
|
+
- `explicit`
|
|
61
|
+
|
|
62
|
+
That list is not mine. It is the list written on the request thread, in the same order, with one change: the single "published date or year" entry became two columns, because the server stores those as two separate fields and merging them would throw one away.
|
|
63
|
+
|
|
64
|
+
Multiple genres, tags or series are joined with `; ` inside one cell. Multiple authors and narrators get that same `; ` join only when you pass `--exact`; without it, they keep whatever separator the server itself used to join them. `duration` is `H:MM:SS` by default, or a number of seconds with `--duration seconds`. `date_added` is an ISO 8601 instant in UTC.
|
|
65
|
+
|
|
66
|
+
Every run, unless you pass `--quiet`, prints a short report on standard error saying how many books were exported and which columns your server left empty for every one of them. That report is the honest answer to whether your library really carries this metadata, and it never mixes into the CSV.
|
|
67
|
+
|
|
68
|
+
With no `--library`, every book library goes into the same CSV, and the `library_name` column says which one each row came from.
|
|
69
|
+
|
|
70
|
+
## Series sequences, and one honest caveat
|
|
71
|
+
|
|
72
|
+
The paginated library endpoint does not expose a series sequence as its own field. It exposes one joined string per book, built by the server as `"<name> #<sequence>"` entries separated by a comma. This tool splits that back apart, which is a guess and not a reading: a series whose own name contains a comma cannot be recovered that way.
|
|
73
|
+
|
|
74
|
+
If exact series and author fields matter to you, use:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
shelfinventory --url https://abs.example --exact
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
That reads each book a second time, through the endpoint that returns real lists, so nothing has to be split. It costs one extra request per book, which is why it is not the default.
|
|
81
|
+
|
|
82
|
+
## Other options
|
|
83
|
+
|
|
84
|
+
- `--library NAME` picks one library by name or id, and repeats.
|
|
85
|
+
- `--list-libraries` prints what the token can read, then stops.
|
|
86
|
+
- `--page-size N` sets how many books are fetched per request.
|
|
87
|
+
- `--timeout SECONDS` sets the request timeout.
|
|
88
|
+
- `--quiet` drops the column report.
|
|
89
|
+
|
|
90
|
+
Podcast libraries are never exported. The columns above are book columns.
|
|
91
|
+
|
|
92
|
+
## What is not verified
|
|
93
|
+
|
|
94
|
+
Being plain about this matters more than looking finished:
|
|
95
|
+
|
|
96
|
+
- No real Audiobookshelf instance has been read with this tool. The response shapes it expects come from the upstream public source code, read on 2026-09-30: `server/models/Book.js`, `server/models/LibraryItem.js` and `server/controllers/LibraryController.js`.
|
|
97
|
+
- The tests run on sample payloads written for the tests, not on captured traffic from a live server.
|
|
98
|
+
- Whether your server fills all eighteen columns is not something anyone has measured. The report printed on standard error is there so you can see it for yourself on the first run.
|
|
99
|
+
|
|
100
|
+
If you run it on a real library, please say so on the issue tracker, whether it worked or not. The paragraph above gets corrected the day someone reports a first-hand result.
|
|
101
|
+
|
|
102
|
+
## Requirements
|
|
103
|
+
|
|
104
|
+
Python 3.9 or newer. The standard library only, no dependencies.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT. Built with AI assistance, reviewed and tested by me.
|
|
109
|
+
|
|
110
|
+
Younes Z.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shelfinventory"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Export an Audiobookshelf library to CSV, one row per book, with the columns people asked for. Read only, no dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Younes Z." }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"audiobookshelf",
|
|
15
|
+
"audiobook",
|
|
16
|
+
"library",
|
|
17
|
+
"inventory",
|
|
18
|
+
"csv",
|
|
19
|
+
"export",
|
|
20
|
+
"catalog",
|
|
21
|
+
"selfhosted",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Environment :: Console",
|
|
26
|
+
"Intended Audience :: End Users/Desktop",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Topic :: Utilities",
|
|
30
|
+
]
|
|
31
|
+
dependencies = []
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/Rezarys/shelfinventory"
|
|
35
|
+
Issues = "https://github.com/Rezarys/shelfinventory/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
shelfinventory = "shelfinventory.cli:main"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Minimal read-only client for the Audiobookshelf HTTP API.
|
|
2
|
+
|
|
3
|
+
Only GET requests exist here. There is no method that writes, patches or deletes
|
|
4
|
+
anything on the server, by design.
|
|
5
|
+
|
|
6
|
+
Endpoints used, with the upstream source that documents their shape:
|
|
7
|
+
|
|
8
|
+
* ``GET /api/libraries`` returns ``{"libraries": [...]}``
|
|
9
|
+
(``server/controllers/LibraryController.js``).
|
|
10
|
+
* ``GET /api/libraries/{id}/items?limit=&page=`` returns
|
|
11
|
+
``{"results": [...], "total": N, "limit": N, "page": N}`` and each entry is the
|
|
12
|
+
minified library item JSON (``server/models/LibraryItem.js``,
|
|
13
|
+
``toOldJSONMinified``).
|
|
14
|
+
* ``GET /api/items/{id}`` returns the expanded library item JSON, which is the
|
|
15
|
+
only place where series sequences and author lists are separate fields
|
|
16
|
+
(``server/models/Book.js``, ``oldMetadataToJSON``).
|
|
17
|
+
|
|
18
|
+
Authentication is a bearer token in the ``Authorization`` header
|
|
19
|
+
(``server/Auth.js``, ``fromAuthHeaderAsBearerToken``).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.parse
|
|
27
|
+
import urllib.request
|
|
28
|
+
|
|
29
|
+
DEFAULT_PAGE_SIZE = 500
|
|
30
|
+
DEFAULT_TIMEOUT = 30.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ApiError(Exception):
|
|
34
|
+
"""Any failure while talking to the server, with a readable message."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _default_opener(request, timeout):
|
|
38
|
+
return urllib.request.urlopen(request, timeout=timeout)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Client:
|
|
42
|
+
"""A read-only Audiobookshelf client.
|
|
43
|
+
|
|
44
|
+
``opener`` exists so the tests can answer requests from sample payloads
|
|
45
|
+
instead of reaching a server. It takes ``(request, timeout)`` and returns a
|
|
46
|
+
file-like object, exactly like :func:`urllib.request.urlopen`.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, base_url, token, timeout=DEFAULT_TIMEOUT, opener=None):
|
|
50
|
+
if not base_url:
|
|
51
|
+
raise ApiError("no server address was given")
|
|
52
|
+
if not token:
|
|
53
|
+
raise ApiError("no API token was given")
|
|
54
|
+
self.base_url = base_url.rstrip("/")
|
|
55
|
+
self.token = token
|
|
56
|
+
self.timeout = timeout
|
|
57
|
+
self._opener = opener or _default_opener
|
|
58
|
+
|
|
59
|
+
def get_json(self, path, params=None):
|
|
60
|
+
url = self.base_url + path
|
|
61
|
+
if params:
|
|
62
|
+
url += "?" + urllib.parse.urlencode(params)
|
|
63
|
+
request = urllib.request.Request(
|
|
64
|
+
url,
|
|
65
|
+
method="GET",
|
|
66
|
+
headers={
|
|
67
|
+
"Authorization": "Bearer " + self.token,
|
|
68
|
+
"Accept": "application/json",
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
try:
|
|
72
|
+
response = self._opener(request, self.timeout)
|
|
73
|
+
except urllib.error.HTTPError as error:
|
|
74
|
+
raise ApiError(_http_message(error, url)) from error
|
|
75
|
+
except urllib.error.URLError as error:
|
|
76
|
+
raise ApiError("could not reach %s: %s" % (url, error.reason)) from error
|
|
77
|
+
with response:
|
|
78
|
+
raw = response.read()
|
|
79
|
+
try:
|
|
80
|
+
return json.loads(raw.decode("utf-8"))
|
|
81
|
+
except (UnicodeDecodeError, ValueError) as error:
|
|
82
|
+
raise ApiError("%s did not answer with JSON" % url) from error
|
|
83
|
+
|
|
84
|
+
def libraries(self):
|
|
85
|
+
"""Every library the token can see, as a list of dicts."""
|
|
86
|
+
payload = self.get_json("/api/libraries")
|
|
87
|
+
libraries = payload.get("libraries")
|
|
88
|
+
if not isinstance(libraries, list):
|
|
89
|
+
raise ApiError("the server did not return a list of libraries")
|
|
90
|
+
return libraries
|
|
91
|
+
|
|
92
|
+
def library_items(self, library_id, page_size=DEFAULT_PAGE_SIZE):
|
|
93
|
+
"""Yield every library item of one library, walking the pages."""
|
|
94
|
+
if page_size < 1:
|
|
95
|
+
raise ApiError("the page size must be at least 1")
|
|
96
|
+
page = 0
|
|
97
|
+
seen = 0
|
|
98
|
+
while True:
|
|
99
|
+
payload = self.get_json(
|
|
100
|
+
"/api/libraries/%s/items" % urllib.parse.quote(str(library_id)),
|
|
101
|
+
{"limit": page_size, "page": page},
|
|
102
|
+
)
|
|
103
|
+
results = payload.get("results")
|
|
104
|
+
if not isinstance(results, list):
|
|
105
|
+
raise ApiError("the server did not return a list of items")
|
|
106
|
+
if not results:
|
|
107
|
+
return
|
|
108
|
+
for entry in results:
|
|
109
|
+
yield entry
|
|
110
|
+
seen += len(results)
|
|
111
|
+
total = payload.get("total")
|
|
112
|
+
if isinstance(total, int) and seen >= total:
|
|
113
|
+
return
|
|
114
|
+
if len(results) < page_size:
|
|
115
|
+
return
|
|
116
|
+
page += 1
|
|
117
|
+
|
|
118
|
+
def item(self, item_id):
|
|
119
|
+
"""One library item in its expanded form."""
|
|
120
|
+
return self.get_json("/api/items/%s" % urllib.parse.quote(str(item_id)))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _http_message(error, url):
|
|
124
|
+
if error.code == 401:
|
|
125
|
+
return "the server rejected the token (401) for %s" % url
|
|
126
|
+
if error.code == 403:
|
|
127
|
+
return "the token is not allowed to read %s (403)" % url
|
|
128
|
+
if error.code == 404:
|
|
129
|
+
return "%s does not exist on this server (404)" % url
|
|
130
|
+
return "%s answered %s %s" % (url, error.code, error.reason)
|