Skip to content

mangalib API reference

mangalib-python-sdk (import mangalib): the SDK for mangalib.me, on the same libsocial-core as ranobelib. Chapters are page images rather than text. What the API returns for MangaLib is in API notes.

Entry point

mangalib.MangaLib

MangaLib(
    url: str,
    *,
    cache_dir: str | Path | None = None,
    cache_ttl: float | None = None,
    verbosity: Verbosity = False,
    headers: Mapping[str, str] | None = None,
)

Bases: TitleClient[Title, Chapter, Volume]

Entry point for interacting with a single mangalib.me title.

Closed chapters (paid, or in early access) can't be read without an account, which the SDK doesn't support: get_chapter() raises AuthRequiredError for one, while download_title() and estimate_title_size() leave out every translation the chapter list marks as closed (ChapterBranch.is_open) — a chapter with only closed ones is skipped, and translation_index counts the open translations only.

Example
async with MangaLib("https://mangalib.me/ru/manga/7965--chainsaw-man") as lib:
    chapter = await lib.get_chapter(volume=1, number="1", branch_id=4667)
    first_page = await lib.download_page(chapter.pages[0])
    await lib.export([chapter], fmt="cbz", path="chainsaw-man-1.cbz")

Initialize the SDK for a title.

Parameters:

Name Type Description Default
url str

A mangalib.me title URL, or a bare {id}--{slug} identifier.

required
cache_dir str | Path | None

Where to cache raw API responses (title metadata, chapter list, chapter pages, image servers) on disk. Page images themselves aren't cached. Defaults to .mangalib_cache in the current working directory.

None
cache_ttl float | None

Seconds after which a cached response is treated as stale and re-fetched. None (the default) means cached responses never expire on their own — see refresh=True on individual methods to force one anyway.

None
verbosity Verbosity

Console output level. False (default): silent. "progress_only": progress bars during download_title(), export() and estimate_title_size(). "full": the same, plus a line logged for every title/chapter-list/chapter fetch, noting whether it was served from the disk cache or the API, and for every closed chapter a whole-title download skips.

False
headers Mapping[str, str] | None

Extra HTTP headers merged over the SDK's defaults (browser-like Origin/Referer/User-Agent, see docs/api-notes.md, section "WAF 403") — a key given here replaces the default one, matched case-insensitively. Applied to every API request and to page image downloads. Meant for when the site's edge protection changes which requests it lets through (AccessBlockedError). None (the default) sends the defaults as-is.

None
Source code in packages/mangalib/src/mangalib/sdk.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def __init__(
    self,
    url: str,
    *,
    cache_dir: str | Path | None = None,
    cache_ttl: float | None = None,
    verbosity: Verbosity = False,
    headers: Mapping[str, str] | None = None,
) -> None:
    """Initialize the SDK for a title.

    Args:
        url: A mangalib.me title URL, or a bare ``{id}--{slug}`` identifier.
        cache_dir: Where to cache raw API responses (title metadata, chapter list,
            chapter pages, image servers) on disk. Page images themselves aren't
            cached. Defaults to ``.mangalib_cache`` in the current working directory.
        cache_ttl: Seconds after which a cached response is treated as stale and
            re-fetched. ``None`` (the default) means cached responses never expire on
            their own — see ``refresh=True`` on individual methods to force one anyway.
        verbosity: Console output level. ``False`` (default): silent. ``"progress_only"``:
            progress bars during ``download_title()``, ``export()`` and
            ``estimate_title_size()``. ``"full"``: the same, plus a line logged for every
            title/chapter-list/chapter fetch, noting whether it was served from the disk
            cache or the API, and for every closed chapter a whole-title download skips.
        headers: Extra HTTP headers merged over the SDK's defaults (browser-like
            ``Origin``/``Referer``/``User-Agent``, see docs/api-notes.md, section "WAF
            403") — a key given here replaces the default one, matched
            case-insensitively. Applied to every API request and to page image
            downloads. Meant for when the site's edge protection changes which requests
            it lets through (``AccessBlockedError``). ``None`` (the default) sends the
            defaults as-is.
    """
    super().__init__(
        url,
        cache_dir=cache_dir if cache_dir is not None else DEFAULT_CACHE_DIR,
        cache_ttl=cache_ttl,
        verbosity=verbosity,
        headers=headers,
    )
    self._image_client: httpx.AsyncClient | None = None
    self._image_fetcher: images.ImageFetcher | None = None
    self._image_servers: list[ImageServer] | None = None

slug_url property

slug_url: str

The title's {id}--{slug} identifier.

aclose async

aclose() -> None

Close the underlying HTTP clients.

Source code in packages/mangalib/src/mangalib/sdk.py
121
122
123
124
125
async def aclose(self) -> None:
    """Close the underlying HTTP clients."""
    await super().aclose()
    if self._image_client is not None:
        await self._image_client.aclose()

chapter_size async

chapter_size(
    chapter: Chapter,
    *,
    server: str | None = None,
    concurrency: int = DEFAULT_IMAGE_CONCURRENCY,
) -> int

The exact size in bytes of a fetched chapter's page images, without downloading them.

One HEAD request per page: the image servers answer it with the file's exact Content-Length (see docs/api-notes.md, "MangaLib"), so this costs requests, not image traffic. Servers are tried, paced and retried as in download_page().

Parameters:

Name Type Description Default
chapter Chapter

A chapter fetched with get_chapter() (or another fetching method).

required
server str | None

The ImageServer.id to try first — see download_page().

None
concurrency int

How many pages to ask about at once.

DEFAULT_IMAGE_CONCURRENCY

Raises:

Type Description
ValueError

If chapter has no pages (not fetched), server isn't one of list_image_servers(), or concurrency is less than 1.

PageDownloadError

If no server gave a page's size.

Source code in packages/mangalib/src/mangalib/sdk.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
async def chapter_size(
    self,
    chapter: Chapter,
    *,
    server: str | None = None,
    concurrency: int = DEFAULT_IMAGE_CONCURRENCY,
) -> int:
    """The exact size in bytes of a fetched chapter's page images, without downloading them.

    One ``HEAD`` request per page: the image servers answer it with the file's exact
    ``Content-Length`` (see docs/api-notes.md, "MangaLib"), so this costs requests, not
    image traffic. Servers are tried, paced and retried as in ``download_page()``.

    Args:
        chapter: A chapter fetched with ``get_chapter()`` (or another fetching method).
        server: The ``ImageServer.id`` to try first — see ``download_page()``.
        concurrency: How many pages to ask about at once.

    Raises:
        ValueError: If ``chapter`` has no pages (not fetched), ``server`` isn't one of
            ``list_image_servers()``, or ``concurrency`` is less than 1.
        PageDownloadError: If no server gave a page's size.
    """
    if not chapter.pages:
        raise ValueError(
            f"Chapter {chapter.volume}/{chapter.number} has no pages to size — fetch it "
            "with get_chapter() first; get_table_of_contents() doesn't include pages."
        )
    order = await self._server_order(server, concurrency)
    fetcher = self._fetcher()
    sizes = await _gather_bounded(
        [_bind(fetcher.size, page, order) for page in chapter.pages], concurrency
    )
    return sum(sizes)

download_page async

download_page(
    page: Page, *, server: str | None = None
) -> bytes

Download one page image.

Tries server first, if given, then images.DEFAULT_SERVER_ORDER (compress, download, main, secondary). compress comes first because it's the one that served full-size pages when checked; main/secondary sit behind a DDoS-Guard check scripts don't pass (see docs/api-notes.md, "MangaLib"). Requests to an image host are paced across the process (images.DEFAULT_IMAGE_REQUEST_DELAY), and a 429, a 5xx or a network error is retried with backoff on the same server before moving on to the next one; a refusal (another 4xx, a response that isn't an image) moves on at once.

Parameters:

Name Type Description Default
page Page

The page, from a fetched chapter's pages.

required
server str | None

The ImageServer.id to try first (see list_image_servers()).

None

Returns:

Type Description
bytes

The image file's bytes, as the server sent them.

Raises:

Type Description
ValueError

If server isn't one of list_image_servers().

PageDownloadError

If no server served the image.

Source code in packages/mangalib/src/mangalib/sdk.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
async def download_page(self, page: Page, *, server: str | None = None) -> bytes:
    """Download one page image.

    Tries ``server`` first, if given, then ``images.DEFAULT_SERVER_ORDER`` (``compress``,
    ``download``, ``main``, ``secondary``). ``compress`` comes first because it's the one
    that served full-size pages when checked; ``main``/``secondary`` sit behind a
    DDoS-Guard check scripts don't pass (see docs/api-notes.md, "MangaLib"). Requests to
    an image host are paced across the process (``images.DEFAULT_IMAGE_REQUEST_DELAY``),
    and a 429, a 5xx or a network error is retried with backoff on the same server before
    moving on to the next one; a refusal (another 4xx, a response that isn't an image)
    moves on at once.

    Args:
        page: The page, from a fetched chapter's ``pages``.
        server: The ``ImageServer.id`` to try first (see ``list_image_servers()``).

    Returns:
        The image file's bytes, as the server sent them.

    Raises:
        ValueError: If ``server`` isn't one of ``list_image_servers()``.
        PageDownloadError: If no server served the image.
    """
    order = images.server_order(await self.list_image_servers(), server)
    return await self._fetcher().download(page, order)

download_pages async

download_pages(
    chapter: Chapter,
    *,
    server: str | None = None,
    concurrency: int = DEFAULT_IMAGE_CONCURRENCY,
) -> list[bytes]

Download every page image of a fetched chapter, in reading order.

Each page as download_page() does it, up to concurrency at a time.

Parameters:

Name Type Description Default
chapter Chapter

A chapter fetched with get_chapter() (or get_chapters(), get_volume(), ...): one from get_table_of_contents() has no pages.

required
server str | None

The ImageServer.id to try first — see download_page().

None
concurrency int

How many pages to download at once. The pacing per image host applies on top, so more than a few mostly adds waiting.

DEFAULT_IMAGE_CONCURRENCY

Raises:

Type Description
ValueError

If server isn't one of list_image_servers(), or concurrency is less than 1.

PageDownloadError

If a page couldn't be downloaded from any server; the other downloads still in flight are cancelled.

Source code in packages/mangalib/src/mangalib/sdk.py
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
async def download_pages(
    self,
    chapter: Chapter,
    *,
    server: str | None = None,
    concurrency: int = DEFAULT_IMAGE_CONCURRENCY,
) -> list[bytes]:
    """Download every page image of a fetched chapter, in reading order.

    Each page as ``download_page()`` does it, up to ``concurrency`` at a time.

    Args:
        chapter: A chapter fetched with ``get_chapter()`` (or ``get_chapters()``,
            ``get_volume()``, ...): one from ``get_table_of_contents()`` has no pages.
        server: The ``ImageServer.id`` to try first — see ``download_page()``.
        concurrency: How many pages to download at once. The pacing per image host
            applies on top, so more than a few mostly adds waiting.

    Raises:
        ValueError: If ``server`` isn't one of ``list_image_servers()``, or
            ``concurrency`` is less than 1.
        PageDownloadError: If a page couldn't be downloaded from any server; the other
            downloads still in flight are cancelled.
    """
    order = await self._server_order(server, concurrency)
    return await self._download_all(chapter.pages, order, concurrency)

download_title async

download_title(
    *,
    branch_id: int | None = None,
    translation_index: int | None = None,
    chapter_delay: float = 0.0,
    on_chapter: Callable[[int, int], None] | None = None,
    max_rate_limit_retries: int = DEFAULT_RATE_LIMIT_RETRIES,
) -> list[VolumeT]

Download every chapter of the title, across all its volumes.

Fetches the chapter list once, then every chapter's content, sequentially, in the API's own order — same pacing/retry/backoff as any other chapter fetch (see ApiClient), plus chapter_delay on top if given.

Resolving translations is checked for the whole title up front, not chapter by chapter: if any chapters have more than one translation, this fetches nothing and raises MultipleTitleTranslationsError listing all of them at once (unless branch_id or translation_index resolves every one of them), rather than failing partway through a long download on the first ambiguous chapter.

A long title's sequential download can outlast ApiClient's own retry budget for 429s (tuned for a single one-off request, not hundreds/thousands of sequential fetches — see docs/api-notes.md). On top of that, each chapter fetch that still comes back rate-limited is retried again here, up to max_rate_limit_retries times, with its own (coarser) backoff. If a chapter fetch still fails after that — from rate limiting or anything else — the chapters already downloaded are not discarded: they're attached to a raised DownloadTitleInterruptedError instead of being lost.

Parameters:

Name Type Description Default
branch_id int | None

Translation to use for every chapter that has more than one — same meaning as get_chapter(..., branch_id=...), applied title-wide. Chapters with only one translation are unaffected. Mutually exclusive with translation_index.

None
translation_index int | None

Alternative to branch_id: pick the translation at this position in each ambiguous chapter's branches list (0 for the first, and so on) — useful when ambiguous chapters don't share one branch_id across the title. Mutually exclusive with branch_id.

None
chapter_delay float

Extra delay, in seconds, after fetching each chapter, on top of ApiClient's own per-request pacing — to be gentler on the API during a large bulk download. Defaults to no extra delay.

0.0
on_chapter Callable[[int, int], None] | None

Called with (completed, total) after each chapter is fetched, if given — a programmatic progress hook for callers that aren't printing to a console (e.g. a web app polling/streaming download progress to a browser), who would otherwise have no way to observe anything before this whole await returns. Independent of verbosity: both can be set at once, each drives its own output.

None
max_rate_limit_retries int

How many extra times to retry a single chapter fetch that comes back rate-limited, on top of ApiClient's own retries, before giving up on the whole download — see above. Each retry honors the API's Retry-After if it sent one, otherwise waits with its own capped exponential backoff. Set to 0 to disable this extra layer and let a persistent RateLimitError end the download immediately (still wrapped in DownloadTitleInterruptedError with whatever was already fetched).

DEFAULT_RATE_LIMIT_RETRIES

Raises:

Type Description
ValueError

If both branch_id and translation_index are given.

MultipleTitleTranslationsError

If the title has one or more chapters with more than one translation that branch_id/translation_index didn't resolve.

DownloadTitleInterruptedError

If fetching a chapter fails unrecoverably partway through the download — carries every chapter already fetched.

Source code in packages/core/src/libsocial/title.py
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
async def download_title(
    self,
    *,
    branch_id: int | None = None,
    translation_index: int | None = None,
    chapter_delay: float = 0.0,
    on_chapter: Callable[[int, int], None] | None = None,
    max_rate_limit_retries: int = DEFAULT_RATE_LIMIT_RETRIES,
) -> list[VolumeT]:
    """Download every chapter of the title, across all its volumes.

    Fetches the chapter list once, then every chapter's content, sequentially, in the
    API's own order — same pacing/retry/backoff as any other chapter fetch (see
    ``ApiClient``), plus ``chapter_delay`` on top if given.

    Resolving translations is checked for the whole title up front, not chapter by
    chapter: if any chapters have more than one translation, this fetches nothing and
    raises ``MultipleTitleTranslationsError`` listing *all* of them at once (unless
    ``branch_id`` or ``translation_index`` resolves every one of them), rather than
    failing partway through a long download on the first ambiguous chapter.

    A long title's sequential download can outlast ``ApiClient``'s own retry budget for
    429s (tuned for a single one-off request, not hundreds/thousands of sequential
    fetches — see docs/api-notes.md). On top of that, each chapter fetch that still comes
    back rate-limited is retried again here, up to ``max_rate_limit_retries`` times, with
    its own (coarser) backoff. If a chapter fetch still fails after that — from rate
    limiting or anything else — the chapters already downloaded are not discarded: they're
    attached to a raised ``DownloadTitleInterruptedError`` instead of being lost.

    Args:
        branch_id: Translation to use for every chapter that has more than one — same
            meaning as ``get_chapter(..., branch_id=...)``, applied title-wide. Chapters
            with only one translation are unaffected. Mutually exclusive with
            ``translation_index``.
        translation_index: Alternative to ``branch_id``: pick the translation at this
            position in each ambiguous chapter's ``branches`` list (``0`` for the first,
            and so on) — useful when ambiguous chapters don't share one ``branch_id``
            across the title. Mutually exclusive with ``branch_id``.
        chapter_delay: Extra delay, in seconds, after fetching each chapter, on top of
            ``ApiClient``'s own per-request pacing — to be gentler on the API during a
            large bulk download. Defaults to no extra delay.
        on_chapter: Called with ``(completed, total)`` after each chapter is fetched, if
            given — a programmatic progress hook for callers that aren't printing to a
            console (e.g. a web app polling/streaming download progress to a browser),
            who would otherwise have no way to observe anything before this whole
            ``await`` returns. Independent of ``verbosity``: both can be set at once,
            each drives its own output.
        max_rate_limit_retries: How many extra times to retry a single chapter fetch that
            comes back rate-limited, on top of ``ApiClient``'s own retries, before giving
            up on the whole download — see above. Each retry honors the API's
            ``Retry-After`` if it sent one, otherwise waits with its own capped
            exponential backoff. Set to ``0`` to disable this extra layer and let a
            persistent ``RateLimitError`` end the download immediately (still wrapped in
            ``DownloadTitleInterruptedError`` with whatever was already fetched).

    Raises:
        ValueError: If both ``branch_id`` and ``translation_index`` are given.
        MultipleTitleTranslationsError: If the title has one or more chapters with more
            than one translation that ``branch_id``/``translation_index`` didn't resolve.
        DownloadTitleInterruptedError: If fetching a chapter fails unrecoverably partway
            through the download — carries every chapter already fetched.
    """
    self._check_translation_selectors(branch_id, translation_index)
    raw_chapters = await self._get_chapters()
    planned, unresolved = self._plan_chapters(
        raw_chapters, branch_id=branch_id, translation_index=translation_index
    )
    if unresolved:
        raise MultipleTitleTranslationsError(
            self._slug_url,
            chapters=[
                AmbiguousChapter(
                    volume=item["volume"],
                    number=item["number"],
                    branches=[
                        ChapterBranch.model_validate(branch) for branch in item["branches"]
                    ],
                )
                for item in unresolved
            ],
        )

    chapters: list[ChapterT] = []
    total = len(planned)
    try:
        with self._reporter.progress(f"Downloading {self._slug_url}", total) as advance:
            for index, chapter in enumerate(planned):
                chapters.append(
                    await self._fetch_chapter_riding_out_rate_limits(
                        chapter.volume,
                        chapter.number,
                        chapter.branch_id,
                        max_rate_limit_retries=max_rate_limit_retries,
                    )
                )
                advance()
                if on_chapter is not None:
                    on_chapter(index + 1, total)
                if chapter_delay and index < total - 1:
                    await asyncio.sleep(chapter_delay)
    except LibSocialError as exc:
        raise DownloadTitleInterruptedError(
            self._slug_url,
            volumes=self._group_into_volumes(chapters),
            completed=len(chapters),
            total=total,
        ) from exc
    return self._group_into_volumes(chapters)

estimate_title_size async

estimate_title_size(
    *,
    sample_size: int = DEFAULT_SAMPLE_SIZE,
    branch_id: int | None = None,
    translation_index: int | None = None,
    server: str | None = None,
    concurrency: int = DEFAULT_IMAGE_CONCURRENCY,
) -> int

Estimate the whole title's page images in bytes, from a sample of its chapters.

Fetches the chapter list, then up to sample_size chapters spread evenly across the title (their page lists, cached like any chapter fetch), sizes their pages exactly with chapter_size() — HEAD requests, no image traffic — and extrapolates the sample's average across every chapter download_title() would download. The chapter list has no page counts, so the sample is what both the page count and the page weight are estimated from.

Parameters:

Name Type Description Default
sample_size int

How many chapters to sample — clamped to how many there are.

DEFAULT_SAMPLE_SIZE
branch_id int | None

Same as download_title(branch_id=...). Mutually exclusive with translation_index.

None
translation_index int | None

Same as download_title(translation_index=...).

None
server str | None

The ImageServer.id to try first — see download_page().

None
concurrency int

How many pages to ask about at once.

DEFAULT_IMAGE_CONCURRENCY

Returns:

Type Description
int

The estimated size in bytes, or 0 if the title has no chapter with an open

int

translation that branch_id/translation_index resolves.

Raises:

Type Description
ValueError

If both branch_id and translation_index are given, or if sample_size or concurrency is less than 1.

Note

Chapters with more than one translation that branch_id/translation_index doesn't resolve are left out of the sample rather than raising, like closed chapters: the estimate needs a chapter of this title to measure, not a particular translation.

Source code in packages/mangalib/src/mangalib/sdk.py
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
async def estimate_title_size(
    self,
    *,
    sample_size: int = DEFAULT_SAMPLE_SIZE,
    branch_id: int | None = None,
    translation_index: int | None = None,
    server: str | None = None,
    concurrency: int = DEFAULT_IMAGE_CONCURRENCY,
) -> int:
    """Estimate the whole title's page images in bytes, from a sample of its chapters.

    Fetches the chapter list, then up to ``sample_size`` chapters spread evenly across
    the title (their page lists, cached like any chapter fetch), sizes their pages
    exactly with ``chapter_size()`` — ``HEAD`` requests, no image traffic — and
    extrapolates the sample's average across every chapter ``download_title()`` would
    download. The chapter list has no page counts, so the sample is what both the page
    count and the page weight are estimated from.

    Args:
        sample_size: How many chapters to sample — clamped to how many there are.
        branch_id: Same as ``download_title(branch_id=...)``. Mutually exclusive with
            ``translation_index``.
        translation_index: Same as ``download_title(translation_index=...)``.
        server: The ``ImageServer.id`` to try first — see ``download_page()``.
        concurrency: How many pages to ask about at once.

    Returns:
        The estimated size in bytes, or ``0`` if the title has no chapter with an open
        translation that ``branch_id``/``translation_index`` resolves.

    Raises:
        ValueError: If both ``branch_id`` and ``translation_index`` are given, or if
            ``sample_size`` or ``concurrency`` is less than 1.

    Note:
        Chapters with more than one translation that ``branch_id``/``translation_index``
        doesn't resolve are left out of the sample rather than raising, like closed
        chapters: the estimate needs *a* chapter of this title to measure, not a
        particular translation.
    """
    self._check_translation_selectors(branch_id, translation_index)
    if sample_size < 1:
        raise ValueError(f"sample_size must be at least 1, got {sample_size}.")
    _check_concurrency(concurrency)

    planned, _ = self._plan_chapters(
        await self._get_chapters(), branch_id=branch_id, translation_index=translation_index
    )
    if not planned:
        return 0
    size = min(sample_size, len(planned))
    step = len(planned) / size
    sample = [planned[int(index * step)] for index in range(size)]

    total = 0
    with self._reporter.progress(f"Sampling {self.slug_url}", len(sample)) as advance:
        for volume_str, number, selected_branch_id in sample:
            chapter = await self._fetch_chapter(volume_str, number, selected_branch_id)
            if chapter.pages:
                total += await self.chapter_size(
                    chapter, server=server, concurrency=concurrency
                )
            advance()
    return round(total / len(sample) * len(planned))

export async

export(
    chapters: Chapter | Volume | Sequence[Chapter | Volume],
    *,
    fmt: str,
    path: str | Path,
    server: str | None = None,
    concurrency: int = DEFAULT_IMAGE_CONCURRENCY,
    on_chapter: Callable[[int, int], None] | None = None,
) -> Path

Export fetched chapters to one file, downloading their page images as it goes.

One chapter, several, a volume, several, or a whole title — download_title()'s result — all go in: volumes are expanded into their chapters, in order. Pages are downloaded chapter by chapter (as download_pages() does it) and written into the file, which only appears at path once complete.

Parameters:

Name Type Description Default
chapters Chapter | Volume | Sequence[Chapter | Volume]

Fetched chapters and/or volumes, in the order they should appear.

required
fmt str

Export format — a key of mangalib.exporters.EXPORTERS: "cbz", and "pdf" when the pdf extra (img2pdf) is installed.

required
path str | Path

Where to write the exported file.

required
server str | None

The ImageServer.id to try first — see download_page().

None
concurrency int

How many pages to download at once.

DEFAULT_IMAGE_CONCURRENCY
on_chapter Callable[[int, int], None] | None

Called with (completed, total) after each chapter is written — the same progress hook as download_title(on_chapter=...), independent of verbosity.

None

Raises:

Type Description
ValueError

If fmt isn't a registered format, there are no chapters, one of them has no pages (not fetched), server isn't one of list_image_servers(), or concurrency is less than 1 — all checked before anything is downloaded.

PageDownloadError

If a page couldn't be downloaded from any server. Nothing is written to path then.

Source code in packages/mangalib/src/mangalib/sdk.py
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
async def export(
    self,
    chapters: Chapter | Volume | Sequence[Chapter | Volume],
    *,
    fmt: str,
    path: str | Path,
    server: str | None = None,
    concurrency: int = DEFAULT_IMAGE_CONCURRENCY,
    on_chapter: Callable[[int, int], None] | None = None,
) -> Path:
    """Export fetched chapters to one file, downloading their page images as it goes.

    One chapter, several, a volume, several, or a whole title — ``download_title()``'s
    result — all go in: volumes are expanded into their chapters, in order. Pages are
    downloaded chapter by chapter (as ``download_pages()`` does it) and written into the
    file, which only appears at ``path`` once complete.

    Args:
        chapters: Fetched chapters and/or volumes, in the order they should appear.
        fmt: Export format — a key of ``mangalib.exporters.EXPORTERS``: ``"cbz"``, and
            ``"pdf"`` when the ``pdf`` extra (img2pdf) is installed.
        path: Where to write the exported file.
        server: The ``ImageServer.id`` to try first — see ``download_page()``.
        concurrency: How many pages to download at once.
        on_chapter: Called with ``(completed, total)`` after each chapter is written —
            the same progress hook as ``download_title(on_chapter=...)``, independent of
            ``verbosity``.

    Raises:
        ValueError: If ``fmt`` isn't a registered format, there are no chapters, one of
            them has no pages (not fetched), ``server`` isn't one of
            ``list_image_servers()``, or ``concurrency`` is less than 1 — all checked
            before anything is downloaded.
        PageDownloadError: If a page couldn't be downloaded from any server. Nothing is
            written to ``path`` then.
    """
    exporter_cls = EXPORTERS.get(fmt)
    if exporter_cls is None:
        available = ", ".join(sorted(EXPORTERS)) or "(none registered)"
        raise ValueError(f"Unknown export format {fmt!r}. Available: {available}")
    flat = _flatten(chapters)
    if not flat:
        raise ValueError("Nothing to export: no chapters given.")
    for chapter in flat:
        if not chapter.pages:
            raise ValueError(
                f"Chapter {chapter.volume}/{chapter.number} has no pages — fetch it with "
                "get_chapter() (or download_title(), ...) first; get_table_of_contents() "
                "doesn't include pages."
            )
    order = await self._server_order(server, concurrency)
    title = await self.get_info()

    async def fetch_pages(chapter: Chapter) -> list[bytes]:
        return await self._download_all(chapter.pages, order, concurrency)

    total = len(flat)
    completed = 0
    with self._reporter.progress(f"Exporting to {fmt}", total) as advance:

        def chapter_done() -> None:
            nonlocal completed
            completed += 1
            advance()
            if on_chapter is not None:
                on_chapter(completed, total)

        return await exporter_cls().export(
            title, flat, Path(path), fetch_pages=fetch_pages, on_chapter=chapter_done
        )

get_chapter async

get_chapter(
    volume: int,
    number: str,
    *,
    branch_id: int | None = None,
    refresh: bool = False,
) -> ChapterT

Fetch a single chapter, including its content.

When a chapter has more than one team's translation and branch_id isn't given, this raises MultipleTranslationsError rather than guessing — see get_translations() and docs/api-notes.md for why the API's own default (when branch_id is omitted) isn't relied on. Doing this check costs an extra request to fetch the chapter list, only when branch_id isn't already given.

Parameters:

Name Type Description Default
volume int

The chapter's volume number.

required
number str

The chapter number, as returned by the API — may contain a decimal (e.g. "51.6").

required
branch_id int | None

Which translation to fetch, as returned by get_translations(). Only required when the chapter has more than one.

None
refresh bool

Bypass the disk cache and re-fetch from the API even if a cached response is on hand.

False

Raises:

Type Description
MultipleTranslationsError

If the chapter has more than one translation and branch_id wasn't given.

Source code in packages/core/src/libsocial/title.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
async def get_chapter(
    self,
    volume: int,
    number: str,
    *,
    branch_id: int | None = None,
    refresh: bool = False,
) -> ChapterT:
    """Fetch a single chapter, including its content.

    When a chapter has more than one team's translation and ``branch_id`` isn't given,
    this raises ``MultipleTranslationsError`` rather than guessing — see
    ``get_translations()`` and docs/api-notes.md for why the API's own default (when
    ``branch_id`` is omitted) isn't relied on. Doing this check costs an extra request
    to fetch the chapter list, only when ``branch_id`` isn't already given.

    Args:
        volume: The chapter's volume number.
        number: The chapter number, as returned by the API — may contain a decimal
            (e.g. ``"51.6"``).
        branch_id: Which translation to fetch, as returned by ``get_translations()``.
            Only required when the chapter has more than one.
        refresh: Bypass the disk cache and re-fetch from the API even if a cached
            response is on hand.

    Raises:
        MultipleTranslationsError: If the chapter has more than one translation and
            ``branch_id`` wasn't given.
    """
    volume_str = str(volume)
    if branch_id is None:
        raw_chapters = await self._get_chapters(refresh=refresh)
        branch_id = self._resolve_branch_id(volume_str, number, raw_chapters)
    return await self._fetch_chapter(volume_str, number, branch_id, refresh=refresh)

get_chapters async

get_chapters(
    chapters: list[tuple[int, str]],
) -> list[ChapterT]

Fetch several chapters, including their content.

Fetches the chapter list once, shared across all requested chapters (also used to detect chapters with more than one translation — see get_chapter), then each chapter individually and sequentially, in the order given.

Parameters:

Name Type Description Default
chapters list[tuple[int, str]]

A list of (volume, number) pairs identifying each chapter.

required

Raises:

Type Description
ChapterNotFoundError

If any requested chapter doesn't exist.

MultipleTranslationsError

If any requested chapter has more than one translation.

Source code in packages/core/src/libsocial/title.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
async def get_chapters(self, chapters: list[tuple[int, str]]) -> list[ChapterT]:
    """Fetch several chapters, including their content.

    Fetches the chapter list once, shared across all requested chapters (also used to
    detect chapters with more than one translation — see ``get_chapter``), then each
    chapter individually and sequentially, in the order given.

    Args:
        chapters: A list of ``(volume, number)`` pairs identifying each chapter.

    Raises:
        ChapterNotFoundError: If any requested chapter doesn't exist.
        MultipleTranslationsError: If any requested chapter has more than one
            translation.
    """
    raw_chapters = await self._get_chapters()
    result = []
    for volume, number in chapters:
        volume_str = str(volume)
        branch_id = self._resolve_branch_id(volume_str, number, raw_chapters)
        result.append(await self._fetch_chapter(volume_str, number, branch_id))
    return result

get_info async

get_info(*, refresh: bool = False) -> TitleT

Fetch title metadata: names, cover, summary, genres, tags, authors, teams.

Parameters:

Name Type Description Default
refresh bool

Bypass the disk cache and re-fetch from the API even if a cached response is on hand.

False
Source code in packages/core/src/libsocial/title.py
177
178
179
180
181
182
183
184
185
async def get_info(self, *, refresh: bool = False) -> TitleT:
    """Fetch title metadata: names, cover, summary, genres, tags, authors, teams.

    Args:
        refresh: Bypass the disk cache and re-fetch from the API even if a cached
            response is on hand.
    """
    data = await self._get_title(refresh=refresh)
    return self.title_model.model_validate(data)

get_table_of_contents async

get_table_of_contents(
    *, refresh: bool = False
) -> list[VolumeT]

Fetch the title's volumes and chapter names/numbers, without chapter content.

Parameters:

Name Type Description Default
refresh bool

Bypass the disk cache and re-fetch from the API even if a cached response is on hand — needed to pick up newly published chapters, since otherwise the cached chapter list would keep being reused.

False
Source code in packages/core/src/libsocial/title.py
187
188
189
190
191
192
193
194
195
196
197
async def get_table_of_contents(self, *, refresh: bool = False) -> list[VolumeT]:
    """Fetch the title's volumes and chapter names/numbers, without chapter content.

    Args:
        refresh: Bypass the disk cache and re-fetch from the API even if a cached
            response is on hand — needed to pick up newly published chapters, since
            otherwise the cached chapter list would keep being reused.
    """
    raw_chapters = await self._get_chapters(refresh=refresh)
    chapters = [self.chapter_model.model_validate(item) for item in raw_chapters]
    return self._group_into_volumes(chapters)

get_translations async

get_translations(
    volume: int, number: str
) -> list[ChapterBranch]

Fetch the available translations (branches) for a chapter.

Each one says whether it's open to read without an account (ChapterBranch.is_open).

Parameters:

Name Type Description Default
volume int

The chapter's volume number.

required
number str

The chapter number.

required

Raises:

Type Description
ChapterNotFoundError

If no chapter exists for this volume/number.

Source code in packages/mangalib/src/mangalib/sdk.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
async def get_translations(  # type: ignore[override]
    self, volume: int, number: str
) -> list[ChapterBranch]:
    """Fetch the available translations (branches) for a chapter.

    Each one says whether it's open to read without an account (``ChapterBranch.is_open``).

    Args:
        volume: The chapter's volume number.
        number: The chapter number.

    Raises:
        ChapterNotFoundError: If no chapter exists for this volume/number.
    """
    volume_str = str(volume)
    for item in await self._get_chapters():
        if item.get("volume") == volume_str and item.get("number") == number:
            return [ChapterBranch.model_validate(branch) for branch in item["branches"] or []]
    raise ChapterNotFoundError(self.slug_url, volume=volume_str, number=number)

get_volume async

get_volume(volume: int) -> VolumeT

Fetch a whole volume: all its chapters, each including content.

The API has no bulk "volume content" endpoint (see docs/api-notes.md), so this fetches the chapter list once to find which numbers belong to the volume, then fetches each of those chapters individually and sequentially.

Parameters:

Name Type Description Default
volume int

The volume number.

required

Raises:

Type Description
VolumeNotFoundError

If the title has no chapters for this volume.

MultipleTranslationsError

If any of the volume's chapters has more than one translation.

Source code in packages/core/src/libsocial/title.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
async def get_volume(self, volume: int) -> VolumeT:
    """Fetch a whole volume: all its chapters, each including content.

    The API has no bulk "volume content" endpoint (see docs/api-notes.md), so this
    fetches the chapter list once to find which numbers belong to the volume, then
    fetches each of those chapters individually and sequentially.

    Args:
        volume: The volume number.

    Raises:
        VolumeNotFoundError: If the title has no chapters for this volume.
        MultipleTranslationsError: If any of the volume's chapters has more than one
            translation.
    """
    raw_chapters = await self._get_chapters()
    return await self._build_volume(volume, raw_chapters)

get_volumes async

get_volumes(volumes: list[int]) -> list[VolumeT]

Fetch several whole volumes, each including chapter content.

Fetches the chapter list once, shared across all requested volumes, then each chapter individually and sequentially — same approach as get_volume, applied to more than one volume without re-fetching the chapter list per volume.

Parameters:

Name Type Description Default
volumes list[int]

The volume numbers to fetch.

required

Raises:

Type Description
VolumeNotFoundError

If the title has no chapters for one of the volumes.

MultipleTranslationsError

If any of the volumes' chapters has more than one translation.

Source code in packages/core/src/libsocial/title.py
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
async def get_volumes(self, volumes: list[int]) -> list[VolumeT]:
    """Fetch several whole volumes, each including chapter content.

    Fetches the chapter list once, shared across all requested volumes, then each
    chapter individually and sequentially — same approach as ``get_volume``, applied
    to more than one volume without re-fetching the chapter list per volume.

    Args:
        volumes: The volume numbers to fetch.

    Raises:
        VolumeNotFoundError: If the title has no chapters for one of the volumes.
        MultipleTranslationsError: If any of the volumes' chapters has more than one
            translation.
    """
    raw_chapters = await self._get_chapters()
    return [await self._build_volume(volume, raw_chapters) for volume in volumes]

list_image_servers async

list_image_servers(
    *, refresh: bool = False
) -> list[ImageServer]

List the servers mangalib.me serves page images from.

Not every one serves pages to a script: see download_page() for which are tried.

Parameters:

Name Type Description Default
refresh bool

Bypass the disk cache and re-fetch from the API.

False
Source code in packages/mangalib/src/mangalib/sdk.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
async def list_image_servers(self, *, refresh: bool = False) -> list[ImageServer]:
    """List the servers mangalib.me serves page images from.

    Not every one serves pages to a script: see ``download_page()`` for which are tried.

    Args:
        refresh: Bypass the disk cache and re-fetch from the API.
    """
    if self._image_servers is not None and not refresh:
        return self._image_servers
    data = None if refresh else self._cache.get(_IMAGE_SERVERS_CACHE_KEY)
    if data is None:
        data = await self._client.list_image_servers()
        self._cache.set(_IMAGE_SERVERS_CACHE_KEY, data)
    self._image_servers = [
        ImageServer.model_validate(item)
        for item in data
        if self.site.id in item.get("site_ids", []) and item.get("url")
    ]
    return self._image_servers

volume_size async

volume_size(
    volume: Volume,
    *,
    server: str | None = None,
    concurrency: int = DEFAULT_IMAGE_CONCURRENCY,
) -> int

The exact size in bytes of a fetched volume's page images — chapter_size() summed.

Parameters:

Name Type Description Default
volume Volume

A volume fetched with get_volume()/get_volumes()/ download_title().

required
server str | None

The ImageServer.id to try first — see download_page().

None
concurrency int

How many pages to ask about at once.

DEFAULT_IMAGE_CONCURRENCY

Raises:

Type Description
ValueError

See chapter_size().

PageDownloadError

If no server gave a page's size.

Source code in packages/mangalib/src/mangalib/sdk.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
async def volume_size(
    self,
    volume: Volume,
    *,
    server: str | None = None,
    concurrency: int = DEFAULT_IMAGE_CONCURRENCY,
) -> int:
    """The exact size in bytes of a fetched volume's page images — ``chapter_size()`` summed.

    Args:
        volume: A volume fetched with ``get_volume()``/``get_volumes()``/
            ``download_title()``.
        server: The ``ImageServer.id`` to try first — see ``download_page()``.
        concurrency: How many pages to ask about at once.

    Raises:
        ValueError: See ``chapter_size()``.
        PageDownloadError: If no server gave a page's size.
    """
    total = 0
    for chapter in volume.chapters:
        total += await self.chapter_size(chapter, server=server, concurrency=concurrency)
    return total

mangalib.Catalog

Catalog(
    *,
    cache_dir: str | Path | None = None,
    cache_ttl: float | None = None,
    headers: Mapping[str, str] | None = None,
)

Bases: Catalog[Title]

Entry point for listing/searching the mangalib.me catalog.

Lists titles (list_titles()) and the filter options for them (list_genres(), list_countries() — on mangalib.me these are the title types: manga, manhwa, manhua, ... — and list_statuses()), each filtered to mangalib.me.

Example
async with Catalog() as catalog:
    page = await catalog.list_titles(query="chainsaw", statuses=[1])
    for title in page.items:
        print(title.name)

Initialize the catalog client.

Parameters:

Name Type Description Default
cache_dir str | Path | None

Where to cache raw API responses on disk, one entry per distinct set of listing parameters. Defaults to .mangalib_cache in the current working directory, the same as MangaLib's.

None
cache_ttl float | None

Seconds after which a cached page is treated as stale and re-fetched. None (the default) means cached pages never expire on their own — see refresh=True on list_titles() to force one anyway.

None
headers Mapping[str, str] | None

Extra HTTP headers merged over the SDK's defaults, same as MangaLib(headers=...) — see there.

None
Source code in packages/mangalib/src/mangalib/catalog.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def __init__(
    self,
    *,
    cache_dir: str | Path | None = None,
    cache_ttl: float | None = None,
    headers: Mapping[str, str] | None = None,
) -> None:
    """Initialize the catalog client.

    Args:
        cache_dir: Where to cache raw API responses on disk, one entry per distinct set
            of listing parameters. Defaults to ``.mangalib_cache`` in the current
            working directory, the same as ``MangaLib``'s.
        cache_ttl: Seconds after which a cached page is treated as stale and re-fetched.
            ``None`` (the default) means cached pages never expire on their own — see
            ``refresh=True`` on ``list_titles()`` to force one anyway.
        headers: Extra HTTP headers merged over the SDK's defaults, same as
            ``MangaLib(headers=...)`` — see there.
    """
    super().__init__(
        cache_dir=cache_dir if cache_dir is not None else DEFAULT_CACHE_DIR,
        cache_ttl=cache_ttl,
        headers=headers,
    )

aclose async

aclose() -> None

Close the underlying HTTP client.

Source code in packages/core/src/libsocial/catalog.py
103
104
105
async def aclose(self) -> None:
    """Close the underlying HTTP client."""
    await self._client.aclose()

list_countries async

list_countries(*, refresh: bool = False) -> list[Country]

Fetch the full list of catalog countries (id → name), for use as filter options with list_titles(countries=...).

Mirrors list_genres() exactly, down to the network-wide, not-site-scoped shape of the underlying endpoint (GET /api/constants?fields[]=types — the API's own name for what this SDK calls "country" is "type", see docs/api-notes.md and Country's docstring). This filters that down to entries tagged for site before returning, same as list_genres() does for genres.

Parameters:

Name Type Description Default
refresh bool

Bypass the disk cache and re-fetch from the API even if the country list was already cached.

False

Returns:

Type Description
list[Country]

Every Country (id, name) that applies to site.

Source code in packages/core/src/libsocial/catalog.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
async def list_countries(self, *, refresh: bool = False) -> list[Country]:
    """Fetch the full list of catalog countries (id → name), for use as filter options
    with ``list_titles(countries=...)``.

    Mirrors ``list_genres()`` exactly, down to the network-wide, not-site-scoped shape
    of the underlying endpoint (``GET /api/constants?fields[]=types`` — the API's own
    name for what this SDK calls "country" is "type", see docs/api-notes.md and
    ``Country``'s docstring). This filters that down to entries tagged for ``site`` before
    returning, same as ``list_genres()`` does for genres.

    Args:
        refresh: Bypass the disk cache and re-fetch from the API even if the country
            list was already cached.

    Returns:
        Every ``Country`` (id, name) that applies to ``site``.
    """
    key = "catalog:countries"
    if not refresh:
        cached = self._cache.get(key)
        if cached is not None:
            return _for_site(Country, cached, self.site)

    data = await self._client.list_countries()
    self._cache.set(key, data)
    return _for_site(Country, data, self.site)

list_genres async

list_genres(*, refresh: bool = False) -> list[Genre]

Fetch the full list of catalog genres (id → name), for use as filter options with list_titles(genres=[...]).

The underlying endpoint (GET /api/constants?fields[]=genres) has no site-scoping parameter — it returns the full genre list shared across the whole lib.social network in one shot, each tagged with the site ids it applies to (see docs/api-notes.md). This filters that down to genres tagged for site before returning, so callers don't get filter options that could never match any of the site's titles (e.g. "Детское", tagged only for site 5, on ranobelib.me).

Parameters:

Name Type Description Default
refresh bool

Bypass the disk cache and re-fetch from the API even if the genre list was already cached.

False

Returns:

Type Description
list[Genre]

Every Genre (id, name, adult flag) that applies to site.

Source code in packages/core/src/libsocial/catalog.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
async def list_genres(self, *, refresh: bool = False) -> list[Genre]:
    """Fetch the full list of catalog genres (id → name), for use as filter options
    with ``list_titles(genres=[...])``.

    The underlying endpoint (``GET /api/constants?fields[]=genres``) has no site-scoping
    parameter — it returns the full genre list shared across the whole lib.social
    network in one shot, each tagged with the site ids it applies to (see
    docs/api-notes.md). This filters that down to genres tagged for ``site`` before
    returning, so callers don't get filter options that could never match any of the
    site's titles (e.g. "Детское", tagged only for site 5, on ranobelib.me).

    Args:
        refresh: Bypass the disk cache and re-fetch from the API even if the genre list
            was already cached.

    Returns:
        Every ``Genre`` (id, name, adult flag) that applies to ``site``.
    """
    # No site in the key, unlike list_titles(): the response is the whole network's
    # list, and _for_site() filters it on every read, cached or not.
    key = "catalog:genres"
    if not refresh:
        cached = self._cache.get(key)
        if cached is not None:
            return _for_site(Genre, cached, self.site)

    data = await self._client.list_genres()
    self._cache.set(key, data)
    return _for_site(Genre, data, self.site)

list_statuses async

list_statuses(*, refresh: bool = False) -> list[Label]

Fetch the full list of title statuses (id → label), for use as filter options with list_titles(statuses=...).

Mirrors list_genres()/list_countries(): the underlying endpoint (GET /api/constants?fields[]=status) isn't site-scoped, so entries not tagged for site are dropped before returning (see docs/api-notes.md). Returns the same Label model as Title.status, so a title's status can be compared against these directly.

Parameters:

Name Type Description Default
refresh bool

Bypass the disk cache and re-fetch from the API even if the status list was already cached.

False

Returns:

Type Description
list[Label]

Every status (Label: id, label) that applies to site.

Source code in packages/core/src/libsocial/catalog.py
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
async def list_statuses(self, *, refresh: bool = False) -> list[Label]:
    """Fetch the full list of title statuses (id → label), for use as filter options
    with ``list_titles(statuses=...)``.

    Mirrors ``list_genres()``/``list_countries()``: the underlying endpoint
    (``GET /api/constants?fields[]=status``) isn't site-scoped, so entries not tagged for
    ``site`` are dropped before returning (see docs/api-notes.md). Returns the same
    ``Label`` model as ``Title.status``, so a title's status can be compared against
    these directly.

    Args:
        refresh: Bypass the disk cache and re-fetch from the API even if the status list
            was already cached.

    Returns:
        Every status (``Label``: id, label) that applies to ``site``.
    """
    key = "catalog:statuses"
    if not refresh:
        cached = self._cache.get(key)
        if cached is not None:
            return _for_site(Label, cached, self.site)

    data = await self._client.list_statuses()
    self._cache.set(key, data)
    return _for_site(Label, data, self.site)

list_titles async

list_titles(
    *,
    page: int = 1,
    per_page: int = 30,
    query: str | None = None,
    genres: list[int] | None = None,
    tags: list[int] | None = None,
    statuses: list[int] | None = None,
    countries: list[int] | None = None,
    min_chapters: int | None = None,
    max_chapters: int | None = None,
    sort: str = DEFAULT_SORT,
    refresh: bool = False,
) -> CatalogPage[TitleT]

Fetch one page of catalog listing/search results.

Parameters:

Name Type Description Default
page int

1-based page number.

1
per_page int

How many titles per page. Must be between MIN_PER_PAGE (10) and MAX_PER_PAGE (60) — the API's own limits (see docs/api-notes.md); anything outside that range raises here instead of a confusing 422 from the API.

30
query str | None

Free-text search term, matched against name/rus_name/eng_name. None (the default) or an empty string lists without searching.

None
genres list[int] | None

Genre ids to filter by. A title must have all of them, not just one (AND, not OR — see docs/api-notes.md). Not validated against list_genres() before sending — an unrecognized id just matches nothing rather than erroring.

None
tags list[int] | None

Tag.ids (from Title.tags) to filter by. Same AND semantics as genres — a title must have all of them, not just any one (confirmed against the live API, see docs/api-notes.md; not the OR some might expect from tags being more numerous/specific than genres). Also not validated before sending, same as genres.

None
statuses list[int] | None

Title.status.ids to filter by (e.g. ongoing, completed). A title matches if its own status is any of these (OR, same as countries — a title only has one status, see docs/api-notes.md). None or an empty list applies no status filter. Ids come from list_statuses().

None
countries list[int] | None

Country.ids to filter by. A title matches if its own country is any of these (OR, not AND like genres/tags — a title only has one country of origin, so requiring all of them could never match past the first, see docs/api-notes.md). None or an empty list applies no country filter. Ids come from list_countries(). Sent on the wire as the API's types[] parameter, not a country/countries[] one — see docs/api-notes.md for why (Country mirrors the API's own "type" concept, which isn't strictly limited to literal countries).

None
min_chapters int | None

Only titles with at least this many chapters (inclusive). Counts the same number as Title.chapter_count (uploaded chapters). None (the default) applies no lower bound.

None
max_chapters int | None

Only titles with at most this many chapters (inclusive). None (the default) applies no upper bound.

None
sort str

Sort order. Despite the name, this is sent as the API's sort_by parameter — an actual sort parameter exists but is silently ignored by the API (see docs/api-notes.md). Known accepted values: "name", "created_at", "views", "chap_count", "last_chapter_at" (the default), "rate_avg", "random" — this list isn't guaranteed exhaustive.

DEFAULT_SORT
refresh bool

Bypass the disk cache and re-fetch from the API even if this exact combination of parameters was already cached.

False

Returns:

Type Description
CatalogPage[TitleT]

A page_model page: the matching titles plus whether a next page exists.

Raises:

Type Description
ValueError

If page is less than 1, per_page is outside MIN_PER_PAGE..MAX_PER_PAGE, min_chapters/max_chapters is negative, or min_chapters is greater than max_chapters.

Source code in packages/core/src/libsocial/catalog.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
async def list_titles(
    self,
    *,
    page: int = 1,
    per_page: int = 30,
    query: str | None = None,
    genres: list[int] | None = None,
    tags: list[int] | None = None,
    statuses: list[int] | None = None,
    countries: list[int] | None = None,
    min_chapters: int | None = None,
    max_chapters: int | None = None,
    sort: str = DEFAULT_SORT,
    refresh: bool = False,
) -> CatalogPage[TitleT]:
    """Fetch one page of catalog listing/search results.

    Args:
        page: 1-based page number.
        per_page: How many titles per page. Must be between ``MIN_PER_PAGE`` (10) and
            ``MAX_PER_PAGE`` (60) — the API's own limits (see docs/api-notes.md);
            anything outside that range raises here instead of a confusing 422 from
            the API.
        query: Free-text search term, matched against name/rus_name/eng_name. ``None``
            (the default) or an empty string lists without searching.
        genres: Genre ids to filter by. A title must have *all* of them, not just one
            (AND, not OR — see docs/api-notes.md). Not validated against ``list_genres()``
            before sending — an unrecognized id just matches nothing rather than erroring.
        tags: ``Tag.id``s (from ``Title.tags``) to filter by. Same AND semantics as
            ``genres`` — a title must have *all* of them, not just any one (confirmed
            against the live API, see docs/api-notes.md; not the OR some might expect
            from tags being more numerous/specific than genres). Also not validated
            before sending, same as ``genres``.
        statuses: ``Title.status.id``s to filter by (e.g. ongoing, completed). A title
            matches if its own status is *any* of these (OR, same as ``countries`` — a
            title only has one status, see docs/api-notes.md). ``None`` or an empty list
            applies no status filter. Ids come from ``list_statuses()``.
        countries: ``Country.id``s to filter by. A title matches if its own country is
            *any* of these (OR, not AND like ``genres``/``tags`` — a title only has one
            country of origin, so requiring all of them could never match past the first,
            see docs/api-notes.md). ``None`` or an empty list applies no country filter.
            Ids come from ``list_countries()``. Sent on the wire as the API's ``types[]``
            parameter, not a ``country``/``countries[]`` one — see docs/api-notes.md for
            why (``Country`` mirrors the API's own "type" concept, which isn't strictly
            limited to literal countries).
        min_chapters: Only titles with at least this many chapters (inclusive). Counts
            the same number as ``Title.chapter_count`` (uploaded chapters). ``None``
            (the default) applies no lower bound.
        max_chapters: Only titles with at most this many chapters (inclusive). ``None``
            (the default) applies no upper bound.
        sort: Sort order. Despite the name, this is sent as the API's ``sort_by``
            parameter — an actual ``sort`` parameter exists but is silently ignored by
            the API (see docs/api-notes.md). Known accepted values: ``"name"``,
            ``"created_at"``, ``"views"``, ``"chap_count"``, ``"last_chapter_at"``
            (the default), ``"rate_avg"``, ``"random"`` — this list isn't guaranteed
            exhaustive.
        refresh: Bypass the disk cache and re-fetch from the API even if this exact
            combination of parameters was already cached.

    Returns:
        A ``page_model`` page: the matching titles plus whether a next page exists.

    Raises:
        ValueError: If ``page`` is less than 1, ``per_page`` is outside
            ``MIN_PER_PAGE..MAX_PER_PAGE``, ``min_chapters``/``max_chapters`` is
            negative, or ``min_chapters`` is greater than ``max_chapters``.
    """
    if page < 1:
        raise ValueError(f"page must be at least 1, got {page}.")
    if not MIN_PER_PAGE <= per_page <= MAX_PER_PAGE:
        raise ValueError(
            f"per_page must be between {MIN_PER_PAGE} and {MAX_PER_PAGE}, got {per_page}."
        )
    for name, value in (("min_chapters", min_chapters), ("max_chapters", max_chapters)):
        if value is not None and value < 0:
            raise ValueError(f"{name} must be at least 0, got {value}.")
    if min_chapters is not None and max_chapters is not None and min_chapters > max_chapters:
        # The API itself silently swaps the bounds instead (see docs/api-notes.md) —
        # raising here rather than relying on that undocumented behavior.
        raise ValueError(
            f"min_chapters ({min_chapters}) must not be greater than "
            f"max_chapters ({max_chapters})."
        )

    key = _cache_key(
        site_id=self.site.id,
        page=page,
        per_page=per_page,
        query=query,
        genres=genres,
        tags=tags,
        statuses=statuses,
        countries=countries,
        min_chapters=min_chapters,
        max_chapters=max_chapters,
        sort=sort,
    )
    if not refresh:
        cached = self._cache.get(key)
        if cached is not None:
            return self._build_page(cached)

    data = await self._client.list_titles(
        page=page,
        per_page=per_page,
        query=query,
        genres=genres,
        tags=tags,
        statuses=statuses,
        countries=countries,
        min_chapters=min_chapters,
        max_chapters=max_chapters,
        sort=sort,
    )
    self._cache.set(key, data)
    return self._build_page(data)

Models

mangalib.CatalogPage

Bases: CatalogPage[Title]

One page of catalog listing/search results, as returned by Catalog.list_titles().

items reuses Title as-is: the fields a listing doesn't send (genres, summary, chapter_count, last_chapter_at, ...) come back at their defaults.

mangalib.Title

Bases: Title

Metadata for a single title (manga, manhwa, manhua, comic, ...).

base_names property

base_names: list[str]

The title's names without a trailing format marker.

rus_name, eng_name, name and other_names, in that order, each with a bare format marker at the end removed — "Return of the Runebound Professor (webtoon)" → "Return of the Runebound Professor", likewise (Manhwa), (Манга), [Вебтун], (Комикс), ... Unlike ranobelib.me's (Новелла), mangalib.me doesn't append one itself: only a few dozen names have one, typed by hand. Other bracketed tails — (Doujinshi), (Colored), (перезапуск), a year, (манга-версия) — tell titles apart and are kept. Whitespace is trimmed; empty names and duplicates (keeping the first) are dropped. Nothing else is normalized (case, punctuation): that's up to whatever compares the names.

The original fields are left as they are. On Catalog.list_titles() items other_names is empty (the listing doesn't return it), so it contributes nothing.

chapter_count class-attribute instance-attribute

chapter_count: int | None = None

Number of uploaded chapters. Only TitleClient.get_info() fills it in: the catalog listing endpoint doesn't return it (see docs/api-notes.md), so it's None on Catalog.list_titles() items.

last_chapter_at class-attribute instance-attribute

last_chapter_at: datetime | None = Field(
    default=None, alias="last_item_at"
)

When the most recent chapter was published — the value the catalog's sort="last_chapter_at" orders by. Only TitleClient.get_info() fills it in: the catalog listing endpoint doesn't return it (see docs/api-notes.md), so it's None on Catalog.list_titles() items, and on a title with no chapters yet.

mangalib.Chapter

Bases: Chapter

A chapter: volume, number, name, available translations, and optionally its pages.

index/item_number/branches_count/branches come from the chapter-list endpoint (see MangaLib.get_table_of_contents()) and are None/empty when a Chapter instead comes from fetching a single chapter, which has a different response shape. pages is the reverse: only filled in by fetching the chapter (MangaLib.get_chapter() and friends), in reading order.

restricted_view class-attribute instance-attribute

restricted_view: RestrictedView | None = None

Set on a fetched chapter whose translation is (or was) paid or in early access.

teams class-attribute instance-attribute

teams: list[Team] = Field(default_factory=list)

The teams credited for the fetched translation (empty on the chapter list's entries, where each of branches has its own).

mangalib.Page

Bases: BaseModel

One page image of a chapter.

url is relative to an image server (see ImageServer and mangalib.page_url()): the API doesn't say which server a page is on, every server serves every page.

image instance-attribute

image: str

The image's file name. Its extension isn't reliable as a format: the compress server sends a JPEG for a .png name (see docs/api-notes.md).

number class-attribute instance-attribute

number: int = Field(alias='slug')

1-based position in the chapter — the API's slug.

url instance-attribute

url: str

Path of the image on an image server, e.g. "//manga/{slug}/chapters/1-1/00.jpg".

mangalib.ChapterBranch

A single team's (or solo uploader's) translation of a chapter.

A chapter has more than one branch when several teams have translated it independently; see Chapter.branches_count.

is_open property

is_open: bool

Whether this translation can be read without an account.

False means MangaLib.get_chapter() raises AuthRequiredError for it.

restricted_view class-attribute instance-attribute

restricted_view: RestrictedView | None = None

Set when this translation is paid or in early access — see is_open.

mangalib.RestrictedView

Bases: BaseModel

Access to a paid or early-access chapter translation.

Present only on translations that are (or were) restricted. Logging in isn't supported by the SDK, so a closed one can't be read through it — see ChapterBranch.is_open.

expired_at class-attribute instance-attribute

expired_at: datetime | None = None

When early access ends and the chapter opens for everyone; None for a chapter that stays paid.

is_open instance-attribute

is_open: bool

Whether anyone can read it now, without an account.

price class-attribute instance-attribute

price: int | None = None

The price the site asks, in its own currency. The chapter list leaves it None; fetching the chapter fills it in.

mangalib.ImageServer

Bases: BaseModel

A server page images are downloaded from (GET /constants?fields[]=imageServers).

A page's full URL is the server's url followed by Page.url.

id instance-attribute

id: str

The API's name for it: "main", "secondary", "compress", "download", "crop" (see docs/api-notes.md for which of them actually serve pages).

mangalib.Volume

Bases: Volume[Chapter]

A volume: its number and the chapters it contains, in title order.

Page images

mangalib.page_url

page_url(page: Page, server: ImageServer) -> str

The full URL of page's image on server.

Page.url starts with //; the slashes are collapsed to one (both forms serve the same image, checked on every server).

Source code in packages/mangalib/src/mangalib/images.py
57
58
59
60
61
62
63
def page_url(page: Page, server: ImageServer) -> str:
    """The full URL of ``page``'s image on ``server``.

    ``Page.url`` starts with ``//``; the slashes are collapsed to one (both forms serve the
    same image, checked on every server).
    """
    return f"{server.url.rstrip('/')}/{page.url.lstrip('/')}"

mangalib.images.DEFAULT_SERVER_ORDER module-attribute

DEFAULT_SERVER_ORDER = (
    "compress",
    "download",
    "main",
    "secondary",
)

Image servers tried, in this order, when no server is chosen. crop isn't one of them: it serves 310×450 previews, not the page itself — choose it explicitly if that's what you want.

mangalib.images.DEFAULT_IMAGE_CONCURRENCY module-attribute

DEFAULT_IMAGE_CONCURRENCY = 5

Page requests in flight at once by default (download_pages(), export(), size methods).

mangalib.images.DEFAULT_IMAGE_REQUEST_DELAY module-attribute

DEFAULT_IMAGE_REQUEST_DELAY = 0.2

Minimum time, in seconds, between the starts of two requests to the same image host, across every client in the process. With five at once, 300 full-size pages went through without a single 429 when checked; much denser request rates got DDoS-Guard's 429.

Exporters

Not re-exported from the top-level mangalib package — import from mangalib.exporters. A new format is a new module in mangalib/exporters/ implementing Exporter, decorated with @register. "pdf" is registered only when the pdf extra (img2pdf) is installed.

mangalib.exporters.Exporter

Bases: Protocol

Renders a title's chapters, as page images, to a single file in some format.

format class-attribute

format: str

The registry key this exporter is selected by, e.g. "cbz".

export async

export(
    title: Title,
    chapters: list[Chapter],
    output_path: Path,
    *,
    fetch_pages: FetchPages,
    on_chapter: Callable[[], None] | None = None,
) -> Path

Write chapters (in the given order) to output_path.

Parameters:

Name Type Description Default
title Title

The chapters' parent title, for metadata (name, authors, ...).

required
chapters list[Chapter]

The chapters to include, in the order they should appear — fetched ones, with pages.

required
output_path Path

Where to write the exported file.

required
fetch_pages FetchPages

Downloads one chapter's page images. The exporter calls it once per chapter, in order, so it never holds more than it has to in memory.

required
on_chapter Callable[[], None] | None

Called once per chapter written, if given — drives MangaLib.export()'s progress reporting.

None

Returns:

Type Description
Path

output_path, once the file has been written.

Source code in packages/mangalib/src/mangalib/exporters/__init__.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
async def export(
    self,
    title: Title,
    chapters: list[Chapter],
    output_path: Path,
    *,
    fetch_pages: FetchPages,
    on_chapter: Callable[[], None] | None = None,
) -> Path:
    """Write ``chapters`` (in the given order) to ``output_path``.

    Args:
        title: The chapters' parent title, for metadata (name, authors, ...).
        chapters: The chapters to include, in the order they should appear — fetched
            ones, with ``pages``.
        output_path: Where to write the exported file.
        fetch_pages: Downloads one chapter's page images. The exporter calls it once per
            chapter, in order, so it never holds more than it has to in memory.
        on_chapter: Called once per chapter written, if given — drives
            ``MangaLib.export()``'s progress reporting.

    Returns:
        ``output_path``, once the file has been written.
    """
    ...

mangalib.exporters.register

register(exporter: ExporterT) -> ExporterT

Class decorator: register exporter under its format key.

Source code in packages/mangalib/src/mangalib/exporters/__init__.py
64
65
66
67
def register(exporter: ExporterT) -> ExporterT:
    """Class decorator: register ``exporter`` under its ``format`` key."""
    EXPORTERS[exporter.format] = exporter
    return exporter

Registered exporters, keyed by Exporter.format.

mangalib.exporters.cbz.CbzExporter

Writes chapters as one CBZ: every page image in reading order, plus ComicInfo.xml.

Pages are stored without recompression (they're JPEGs already), named by their position in the book with zero padding (0001.jpg, 0002.jpg, ...) so every reader sorts them right, with the extension taken from the image's bytes. Where each chapter starts is recorded as a Bookmark on its first page in ComicInfo.xml (schema 2.1, as read by Komga, Kavita, ComicRack and others), which also carries the title's metadata.

export async

export(
    title: Title,
    chapters: list[Chapter],
    output_path: Path,
    *,
    fetch_pages: FetchPages,
    on_chapter: Callable[[], None] | None = None,
) -> Path

Write chapters to output_path as a CBZ — see Exporter.export().

Source code in packages/mangalib/src/mangalib/exporters/cbz.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
async def export(
    self,
    title: Title,
    chapters: list[Chapter],
    output_path: Path,
    *,
    fetch_pages: FetchPages,
    on_chapter: Callable[[], None] | None = None,
) -> Path:
    """Write ``chapters`` to ``output_path`` as a CBZ — see ``Exporter.export()``."""
    total_pages = sum(len(chapter.pages) for chapter in chapters)
    width = max(4, len(str(total_pages)))
    entries: list[_PageEntry] = []
    with (
        atomic_output(output_path) as temporary,
        zipfile.ZipFile(temporary, "w", zipfile.ZIP_STORED) as archive,
    ):
        for chapter in chapters:
            images = await fetch_pages(chapter)
            for position, (page, data) in enumerate(zip(chapter.pages, images, strict=True)):
                index = len(entries)
                archive.writestr(f"{index + 1:0{width}}.{image_extension(data)}", data)
                entries.append(
                    _PageEntry(
                        size=len(data),
                        width=page.width,
                        height=page.height,
                        bookmark=chapter_label(chapter) if position == 0 else None,
                    )
                )
            if on_chapter is not None:
                on_chapter()
        archive.writestr("ComicInfo.xml", comic_info(title, chapters, entries))
    return output_path

mangalib.exporters.pdf.PdfExporter

Writes chapters as one PDF: a page per page image, an outline entry per chapter.

JPEGs are embedded as they are, without recompression (img2pdf), so the file is about the size of the images. Each PDF page is the image's own size; a tall webtoon strip beyond the PDF page-size limit is scaled with /UserUnit rather than cut. The PDF is assembled in memory, so for a whole long title CBZ is the lighter choice.

export async

export(
    title: Title,
    chapters: list[Chapter],
    output_path: Path,
    *,
    fetch_pages: FetchPages,
    on_chapter: Callable[[], None] | None = None,
) -> Path

Write chapters to output_path as a PDF — see Exporter.export().

Source code in packages/mangalib/src/mangalib/exporters/pdf.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
async def export(
    self,
    title: Title,
    chapters: list[Chapter],
    output_path: Path,
    *,
    fetch_pages: FetchPages,
    on_chapter: Callable[[], None] | None = None,
) -> Path:
    """Write ``chapters`` to ``output_path`` as a PDF — see ``Exporter.export()``."""
    with tempfile.TemporaryDirectory(prefix="mangalib-pdf-") as directory:
        files: list[Path] = []
        outline: list[tuple[str, int]] = []
        for chapter in chapters:
            images = await fetch_pages(chapter)
            if images:
                outline.append((chapter_label(chapter), len(files)))
            for data in images:
                path = Path(directory) / f"{len(files):06}.{image_extension(data)}"
                path.write_bytes(data)
                files.append(path)
            if on_chapter is not None:
                on_chapter()
        with atomic_output(output_path) as temporary:
            await asyncio.to_thread(_write_pdf, title, files, outline, temporary)
    return output_path

Exceptions

MangaLibError is the base class of every error the SDK raises, the same class as ranobelib.RanobeLibError; the others are shared with ranobelib too, except PageDownloadError.

mangalib.PageDownloadError

PageDownloadError(failures: list[tuple[str, str]])

Bases: LibSocialError

Raised when a page image couldn't be downloaded from any image server.

Each server is tried in turn (see MangaLib.download_page()); this is raised only after the last one failed too.

Attributes:

Name Type Description
failures

(url, reason) for every attempt, in the order they were made.

Source code in packages/mangalib/src/mangalib/exceptions.py
39
40
41
42
def __init__(self, failures: list[tuple[str, str]]) -> None:
    self.failures = failures
    attempts = "; ".join(f"{url}: {reason}" for url, reason in failures)
    super().__init__(f"Couldn't download the page from any image server ({attempts})")

mangalib.AuthRequiredError

AuthRequiredError(url: str)

Bases: LibSocialError

Raised when the requested content requires authorization (paid or early access).

Attributes:

Name Type Description
url

The request URL that returned 403.

Source code in packages/core/src/libsocial/exceptions.py
152
153
154
def __init__(self, url: str) -> None:
    self.url = url
    super().__init__(f"Authorization required to access: {url}")

mangalib.ChapterNotFoundError

ChapterNotFoundError(
    slug_url: str, *, volume: str, number: str
)

Bases: LibSocialError

Raised when a chapter cannot be found for a given volume/number.

Attributes:

Name Type Description
slug_url

The title's {id}--{slug} identifier.

volume

The volume number that was requested.

number

The chapter number that was requested.

Source code in packages/core/src/libsocial/exceptions.py
37
38
39
40
41
def __init__(self, slug_url: str, *, volume: str, number: str) -> None:
    self.slug_url = slug_url
    self.volume = volume
    self.number = number
    super().__init__(f"Chapter not found: {slug_url!r} volume={volume!r} number={number!r}")

mangalib.VolumeNotFoundError

VolumeNotFoundError(slug_url: str, *, volume: str)

Bases: LibSocialError

Raised when a title has no chapters for a given volume number.

Attributes:

Name Type Description
slug_url

The title's {id}--{slug} identifier.

volume

The volume number that was requested.

Source code in packages/core/src/libsocial/exceptions.py
52
53
54
55
def __init__(self, slug_url: str, *, volume: str) -> None:
    self.slug_url = slug_url
    self.volume = volume
    super().__init__(f"Volume not found: {slug_url!r} volume={volume!r}")

mangalib.MultipleTranslationsError

MultipleTranslationsError(
    slug_url: str,
    *,
    volume: str,
    number: str,
    branches: list[ChapterBranch],
)

Bases: LibSocialError

Raised when a chapter has more than one team's translation and none was selected.

The SDK does not guess a default: the API's own default when branch_id is omitted is not simply the first branch listed, and the actual rule it uses isn't documented (see docs/api-notes.md) — returning it silently would be unpredictable. Call TitleClient.get_translations() to list the available branches, then pass one's branch_id explicitly.

Attributes:

Name Type Description
slug_url

The title's {id}--{slug} identifier.

volume

The chapter's volume number.

number

The chapter number.

branches

The chapter's available translations, as returned by TitleClient.get_translations().

Source code in packages/core/src/libsocial/exceptions.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def __init__(
    self, slug_url: str, *, volume: str, number: str, branches: list[ChapterBranch]
) -> None:
    self.slug_url = slug_url
    self.volume = volume
    self.number = number
    self.branches = branches
    options = ", ".join(
        f"branch_id={branch.branch_id} ({_describe_branch(branch)})" for branch in branches
    )
    super().__init__(
        f"Multiple translations for {slug_url!r} volume={volume!r} number={number!r}: "
        f"{options}. Pass branch_id to select one (see get_translations())."
    )

mangalib.MultipleTitleTranslationsError

MultipleTitleTranslationsError(
    slug_url: str, *, chapters: list[AmbiguousChapter]
)

Bases: LibSocialError

Raised by download_title() when one or more chapters have an unresolved translation.

Unlike MultipleTranslationsError (one chapter), this can list several: a title-wide bulk download checks every chapter up front rather than stopping at the first ambiguous one, so a caller doesn't have to fix one chapter, rerun, hit the next, and rerun again. Pass branch_id or translation_index to download_title() to resolve them, or fetch the listed chapters individually with get_chapter(..., branch_id=...).

Attributes:

Name Type Description
slug_url

The title's {id}--{slug} identifier.

chapters

The ambiguous chapters and their available translations.

Source code in packages/core/src/libsocial/exceptions.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def __init__(self, slug_url: str, *, chapters: list[AmbiguousChapter]) -> None:
    self.slug_url = slug_url
    self.chapters = chapters
    lines = [
        f"  volume={chapter.volume!r} number={chapter.number!r}: "
        + ", ".join(
            f"branch_id={branch.branch_id} ({_describe_branch(branch)})"
            for branch in chapter.branches
        )
        for chapter in chapters
    ]
    super().__init__(
        f"{len(chapters)} chapter(s) in {slug_url!r} have more than one translation "
        "unresolved by the given branch_id/translation_index. Pass branch_id or "
        "translation_index to download_title() to select one for every chapter listed, "
        "or fetch them individually with get_chapter(..., branch_id=...):\n" + "\n".join(lines)
    )

mangalib.DownloadTitleInterruptedError

DownloadTitleInterruptedError(
    slug_url: str,
    *,
    volumes: Sequence[Volume[Any]],
    completed: int,
    total: int,
)

Bases: LibSocialError

Raised by download_title() when a chapter fetch fails unrecoverably partway through.

download_title() fetches hundreds-to-thousands of chapters sequentially for a long title, which can outlast even the extra, bulk-specific rate-limit retry budget described in download_title()'s max_rate_limit_retries — or hit some other unrecoverable error (e.g. AuthRequiredError on a chapter that turns out to need a paywall/auth token). Rather than letting that error propagate on its own and silently discard every chapter already fetched, it's wrapped in this exception instead, carrying what was already downloaded — so a caller can keep that partial result, or simply call download_title() again: the SDK's disk cache means already-fetched chapters aren't re-requested, so a retry resumes close to where this one stopped rather than restarting the whole title. See docs/api-notes.md's "Rate limiting и retry" section and issue #41.

Attributes:

Name Type Description
slug_url

The title's {id}--{slug} identifier.

volumes list[Volume[Any]]

Chapters fetched before the failure, grouped into volumes the same way a successful download_title() call would return them.

completed

How many chapters were fetched before the failure.

total

How many chapters download_title() had planned to fetch in total.

Source code in packages/core/src/libsocial/exceptions.py
230
231
232
233
234
235
236
237
238
239
240
def __init__(
    self, slug_url: str, *, volumes: Sequence[Volume[Any]], completed: int, total: int
) -> None:
    self.slug_url = slug_url
    self.volumes: list[Volume[Any]] = list(volumes)
    self.completed = completed
    self.total = total
    super().__init__(
        f"download_title() for {slug_url!r} was interrupted after {completed}/{total} "
        "chapter(s) fetched — see __cause__ for why, .volumes for what was already fetched."
    )

Cache maintenance

mangalib.cache_stats

cache_stats(
    cache_dir: str | Path = DEFAULT_CACHE_DIR,
) -> CacheStats

Count the entries in an SDK disk cache and their total size on disk.

Parameters:

Name Type Description Default
cache_dir str | Path

The cache directory — the same cache_dir passed to MangaLib / Catalog (default: theirs, .mangalib_cache in the current directory).

DEFAULT_CACHE_DIR

Returns:

Type Description
CacheStats

The number of entries and their total size in bytes. A missing directory is an empty

CacheStats

cache, CacheStats(entries=0, bytes=0), not an error.

Source code in packages/mangalib/src/mangalib/cache.py
22
23
24
25
26
27
28
29
30
31
32
33
def cache_stats(cache_dir: str | Path = DEFAULT_CACHE_DIR) -> CacheStats:
    """Count the entries in an SDK disk cache and their total size on disk.

    Args:
        cache_dir: The cache directory — the same ``cache_dir`` passed to ``MangaLib`` /
            ``Catalog`` (default: theirs, ``.mangalib_cache`` in the current directory).

    Returns:
        The number of entries and their total size in bytes. A missing directory is an empty
        cache, ``CacheStats(entries=0, bytes=0)``, not an error.
    """
    return _core.cache_stats(cache_dir)

mangalib.clear_cache

clear_cache(
    cache_dir: str | Path = DEFAULT_CACHE_DIR,
    *,
    older_than: float | None = None,
) -> CacheStats

Remove entries from an SDK disk cache.

Only files the cache itself writes are removed — anything else in the directory, and the directory itself, is left alone.

Parameters:

Name Type Description Default
cache_dir str | Path

The cache directory — see cache_stats().

DEFAULT_CACHE_DIR
older_than float | None

If given, only remove entries written more than this many seconds ago, plus any corrupted entry. None (the default) removes every entry.

None

Returns:

Type Description
CacheStats

The number of entries removed and the bytes they took.

Raises:

Type Description
ValueError

If older_than is negative.

Source code in packages/mangalib/src/mangalib/cache.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def clear_cache(
    cache_dir: str | Path = DEFAULT_CACHE_DIR, *, older_than: float | None = None
) -> CacheStats:
    """Remove entries from an SDK disk cache.

    Only files the cache itself writes are removed — anything else in the directory, and the
    directory itself, is left alone.

    Args:
        cache_dir: The cache directory — see ``cache_stats()``.
        older_than: If given, only remove entries written more than this many seconds ago,
            plus any corrupted entry. ``None`` (the default) removes every entry.

    Returns:
        The number of entries removed and the bytes they took.

    Raises:
        ValueError: If ``older_than`` is negative.
    """
    return _core.clear_cache(cache_dir, older_than=older_than)

mangalib.CacheStats dataclass

CacheStats(entries: int, bytes: int)

How many cache entries there are (or were removed), and their total size on disk.