Coverage for src/lanraragi_api/api/tankoubon.py: 92%
62 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-22 23:20 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-22 23:20 +0000
1from typing import Any, cast
3from requests import Response
5from lanraragi_api.api.base import (
6 BaseAPICall,
7)
8from lanraragi_api.entity.base import OperationResponse
9from lanraragi_api.entity.tankoubon import (
10 TankoubonDetailResponse,
11 TankoubonListResponse,
12 TankoubonMetadata,
13)
14from lanraragi_api.error import APIResponseDecodeError
17class TankoubonAPI(BaseAPICall):
18 """Endpoints related to Tankoubons.
20 Shared request and error behavior is documented on ``BaseAPICall``.
21 """
23 def get_tankoubon_list(self, page: int | None = None) -> TankoubonListResponse:
24 """Get list of Tankoubons paginated.
26 The amount of tanks per page depends on the server
27 ``archives_per_page`` setting.
29 Args:
30 page: Page of the list of Tankoubons. Defaults to None.
32 Returns:
33 TankoubonListResponse: Tankoubons of the requested page.
35 Raises:
36 APIHttpError: Any non-2xx status code returned by the server.
37 APIResponseDecodeError: If the response has no ``result`` list, or
38 if an item does not match ``TankoubonMetadata``.
39 """
40 path = "/api/tankoubons"
41 payload = self.request_json("GET", path, params={"page": page})
42 result = payload.get("result")
43 if not isinstance(result, list):
44 raise APIResponseDecodeError(self._to_url(path), "missing result list")
45 return TankoubonListResponse(
46 result=[
47 self.parse_model(TankoubonMetadata, t, path)
48 for t in cast(list[Any], result)
49 ],
50 total=payload.get("total"),
51 filtered=payload.get("filtered"),
52 )
54 def get_all_tankoubons(self, page: int | None = None) -> list[TankoubonMetadata]:
55 """Return only the tankoubon list of the paginated list endpoint.
57 This is a backward-compatible wrapper around ``get_tankoubon_list``.
59 Args:
60 page: Page of the list of Tankoubons. Defaults to None.
62 Returns:
63 list[TankoubonMetadata]: Tankoubons of the requested page.
64 """
65 return self.get_tankoubon_list(page=page).result
67 def get_tankoubon_detail(
68 self,
69 id: str,
70 ) -> TankoubonMetadata:
71 """Get the details of the specified tankoubon ID.
73 Args:
74 id: ID of the Tankoubon desired.
76 Returns:
77 TankoubonMetadata: Metadata of the tankoubon.
79 Raises:
80 APIHttpError: 400 if the server rejected the request, or any other
81 non-2xx status code.
82 APIResponseDecodeError: If the response does not match
83 ``TankoubonMetadata``.
84 """
85 path = f"/api/tankoubons/{id}"
86 payload = self.request_json("GET", path)
87 return self.parse_model(TankoubonMetadata, payload, path)
89 def get_tankoubon_full(
90 self,
91 id: str,
92 page: int = -1,
93 ) -> TankoubonDetailResponse:
94 """Get the details of a tankoubon with paginated archive metadata.
96 The amount of archives per page depends on the server
97 ``archives_per_page`` setting.
99 Args:
100 id: ID of the Tankoubon desired.
101 page: Page of the Archives list. Defaults to -1, which returns all
102 archives.
104 Returns:
105 TankoubonDetailResponse: Tankoubon metadata with ``full_data``
106 filled in.
108 Raises:
109 APIHttpError: 400 if the server rejected the request, or any other
110 non-2xx status code.
111 APIResponseDecodeError: If the response has no ``result`` payload,
112 or if it does not match ``TankoubonMetadata``.
113 """
114 path = f"/api/tankoubons/{id}/full"
115 payload = self.request_json(
116 "GET",
117 path,
118 params={"page": page},
119 )
120 result = payload.get("result")
121 if result is None:
122 raise APIResponseDecodeError(self._to_url(path), "missing result payload")
123 return TankoubonDetailResponse(
124 result=self.parse_model(TankoubonMetadata, result, path),
125 total=payload.get("total"),
126 filtered=payload.get("filtered"),
127 )
129 def get_tankoubon(self, id: str) -> TankoubonMetadata:
130 """Get the details of the specified tankoubon ID.
132 This is a backward-compatible wrapper around ``get_tankoubon_detail``.
134 Args:
135 id: ID of the Tankoubon desired.
137 Returns:
138 TankoubonMetadata: Metadata of the tankoubon.
139 """
140 return self.get_tankoubon_detail(id=id)
142 def get_tankoubon_thumbnail(
143 self,
144 id: str,
145 no_fallback: bool | None = None,
146 ) -> Response:
147 """Get the cover thumbnail for a given Tankoubon.
149 By default, the thumbnail is sourced from the first page of the first
150 archive. This endpoint returns a placeholder image if the thumbnail
151 does not exist yet. If you want to queue generation of the thumbnail in
152 the background, you can use the ``no_fallback`` query parameter.
154 Args:
155 id: ID of the Tankoubon desired.
156 no_fallback: Disables the placeholder image, queues the thumbnail
157 for extraction and returns a JSON with code 202. This parameter
158 does nothing if the image already exists. Defaults to None.
160 Returns:
161 Response: Response of the server, either the thumbnail bytes with
162 code 200 or the job JSON with code 202.
164 Raises:
165 APIHttpError: 400 if the server rejected the request, or any other
166 non-2xx status code.
168 Note:
169 When the thumbnail already exists, the image is returned with code
170 200 no matter what. Otherwise, ``no_fallback`` queues the
171 extraction and the 202 body carries a Minion job; use
172 ``/api/minion/:jobid`` to track when the thumbnail is ready.
173 """
174 no_fallback_value = None
175 if no_fallback is not None:
176 no_fallback_value = "true" if no_fallback else "false"
178 return self.request(
179 "GET",
180 f"/api/tankoubons/{id}/thumbnail",
181 params={"no_fallback": no_fallback_value},
182 )
184 def update_tankoubon_thumbnail(self, id: str, page: int) -> OperationResponse:
185 """Set the cover thumbnail of a Tankoubon from a global page number.
187 The global page falls within all archives of the tank, in order, and is
188 translated to the correct archive and local page automatically.
190 Args:
191 id: ID of the Tankoubon desired.
192 page: Global 1-indexed page number across all archives in the
193 tankoubon. Page 1 is the first page of the first archive, and so
194 on.
196 Returns:
197 OperationResponse: Result of the operation.
199 Raises:
200 APIResponseDecodeError: If the response body is not valid JSON, or
201 does not match ``OperationResponse``.
202 APIOperationError: If the operation failed and raising is enabled.
204 Note:
205 On success the server also returns a ``new_thumbnail`` field with
206 the path of the new thumbnail file.
207 """
208 return self.request_operation(
209 "PUT",
210 f"/api/tankoubons/{id}/thumbnail",
211 params={"page": page},
212 )
214 def update_tank_progress(self, id: str, page: int) -> OperationResponse:
215 """Tell the server which page of this Tankoubon you're reading.
217 The server updates its internal reading progression accordingly. The
218 page number is global across all Archives in the tank (if a tank has
219 two Archives with 20 and 25 pages, page 26 will be page 6 in Archive
220 #2).
222 Args:
223 id: ID of the Tankoubon to update.
224 page: Global 1-indexed page number to update the reading progress
225 to. Must be a positive integer.
227 Returns:
228 OperationResponse: Result of the operation.
230 Raises:
231 APIResponseDecodeError: If the response body is not valid JSON, or
232 does not match ``OperationResponse``.
233 APIOperationError: If the operation failed and raising is enabled.
235 Note:
236 If the server is configured to use clientside progress tracking,
237 this API call returns an error. Make sure to check through
238 ``/api/info`` whether the server tracks reading progression or not
239 before calling this endpoint.
240 """
241 return self.request_operation("PUT", f"/api/tankoubons/{id}/progress/{page}")
243 def create_tankoubon(
244 self, name: str, tankid: str | None = None
245 ) -> OperationResponse:
246 """Create a new Tankoubon or update the name of an existing one.
248 Args:
249 name: Name of the Tankoubon.
250 tankid: ID of an existing Tankoubon, if you want to change its
251 name. Defaults to None, which creates a new Tankoubon.
253 Returns:
254 OperationResponse: Result of the operation; ``tankoubon_id`` holds
255 the ID of the created or modified Tankoubon.
257 Raises:
258 APIResponseDecodeError: If the response body is not valid JSON, or
259 does not match ``OperationResponse``.
260 APIOperationError: If the operation failed and raising is enabled.
261 """
262 return self.request_operation(
263 "PUT", "/api/tankoubons", data={"name": name, "tankid": tankid}
264 )
266 def update_tankoubon(
267 self,
268 id: str,
269 archives: list[str] | None = None,
270 name: str | None = None,
271 summary: str | None = None,
272 tags: str | None = None,
273 append: bool | None = None,
274 metadata: dict[str, Any] | None = None,
275 ) -> OperationResponse:
276 """Modify the full metadata (name, summary, additional tags) or the
277 contents of a Tankoubon.
279 If you only need to change the name of a Tank, consider just using
280 ``PUT /api/tankoubons`` instead.
282 Args:
283 id: ID of the Tankoubon to update.
284 archives: Ordered list of archive IDs. Defaults to None.
285 name: Name of the Tankoubon. Defaults to None.
286 summary: Summary of the Tankoubon. Defaults to None.
287 tags: Additional tags for the Tankoubon, in LRR comma-separated
288 format. This replaces whatever additional tags the Tank already
289 has, unless ``append`` is True. Defaults to None.
290 append: If True, tags are appended to the Tank's existing own tags
291 instead of replacing them. Defaults to None, which leaves the
292 server default of False.
293 metadata: Metadata payload, merged with the explicit arguments.
294 Defaults to None.
296 Returns:
297 OperationResponse: Result of the operation.
299 Raises:
300 APIResponseDecodeError: If the response body is not valid JSON, or
301 does not match ``OperationResponse``.
302 APIOperationError: If the operation failed and raising is enabled.
304 Note:
305 If there is no need to update something in one of the metadata
306 keys, do not send the key, as this can otherwise result in unwanted
307 results.
308 """
309 payload: dict[str, Any] = {}
310 if archives is not None:
311 payload["archives"] = archives
313 metadata_payload = {} if metadata is None else dict(metadata)
314 if name is not None:
315 metadata_payload["name"] = name
316 if summary is not None:
317 metadata_payload["summary"] = summary
318 if tags is not None:
319 metadata_payload["tags"] = tags
320 if append is not None:
321 metadata_payload["append"] = append
323 if metadata_payload:
324 payload["metadata"] = metadata_payload
326 return self.request_operation("PUT", f"/api/tankoubons/{id}", json=payload)
328 def add_archive_to_tankoubon(
329 self, tankoubon_id: str, archive_id: str
330 ) -> OperationResponse:
331 """Append an archive at the final position of a Tankoubon.
333 Args:
334 tankoubon_id: ID of the Tankoubon to update.
335 archive_id: ID of the Archive to append.
337 Returns:
338 OperationResponse: Result of the operation.
340 Raises:
341 APIResponseDecodeError: If the response body is not valid JSON, or
342 does not match ``OperationResponse``.
343 APIOperationError: If the operation failed and raising is enabled.
345 Note:
346 Failure responses such as 400 (error response) or 423 (Tankoubon
347 locked for modification) are returned in the operation result, with
348 ``success`` set to 0, instead of raising.
349 """
350 return self.request_operation(
351 "PUT", f"/api/tankoubons/{tankoubon_id}/{archive_id}"
352 )
354 def remove_archive_from_tankoubon(
355 self, tankoubon_id: str, archive_id: str
356 ) -> OperationResponse:
357 """Remove an archive from a Tankoubon.
359 Args:
360 tankoubon_id: ID of the Tankoubon to update.
361 archive_id: ID of the archive to remove.
363 Returns:
364 OperationResponse: Result of the operation.
366 Raises:
367 APIResponseDecodeError: If the response body is not valid JSON, or
368 does not match ``OperationResponse``.
369 APIOperationError: If the operation failed and raising is enabled.
371 Note:
372 Failure responses such as 400 (error response) or 423 (Tankoubon
373 locked for modification) are returned in the operation result, with
374 ``success`` set to 0, instead of raising.
375 """
376 return self.request_operation(
377 "DELETE", f"/api/tankoubons/{tankoubon_id}/{archive_id}"
378 )
380 def delete_tankoubon(self, id: str) -> OperationResponse:
381 """Remove a Tankoubon from the server.
383 This doesn't delete the underlying Archives.
385 Args:
386 id: ID of the Tankoubon to delete.
388 Returns:
389 OperationResponse: Result of the operation.
391 Raises:
392 APIResponseDecodeError: If the response body is not valid JSON, or
393 does not match ``OperationResponse``.
394 APIOperationError: If the operation failed and raising is enabled.
396 Note:
397 Failure responses such as 400 (error response) or 423 (Tankoubon
398 locked for modification) are returned in the operation result, with
399 ``success`` set to 0, instead of raising.
400 """
401 return self.request_operation("DELETE", f"/api/tankoubons/{id}")