This guide covers Mandown's main public Python API. It focuses on the usual workflow: find a series, load it as a comic, download it, and optionally process or convert the downloaded files.
import mandownIf you already have a supported series URL:
comic = mandown.query(
"https://www.webtoons.com/en/action/omniscient-reader/list?title_no=2154"
)
mandown.download(comic, "./downloads")
mandown.convert(
"./downloads/omniscient-reader",
mandown.ConvertFormats.CBZ,
"./books",
)If you only know the series name:
import asyncio
async def find_source():
async with mandown.AniListClient() as client:
results = await client.search_manga(
"solo leveling",
include_external_links=True,
)
for manga in results.items:
if isinstance(manga, mandown.AniListManga):
sources = client.extract_supported_sources(manga.external_links)
if sources:
return sources[0].url
source_url = asyncio.run(find_source())
if source_url:
mandown.download(source_url, "./downloads")AniList is Mandown's only active title-search and metadata source. The primary API is async and keeps one HTTP session for the lifetime of the client:
import asyncio
from mandown import AniListClient
async def main():
async with AniListClient() as client:
results = await client.search_manga("Solo Leveling", per_page=10)
selected = results.items[0]
details = await client.get_manga(selected.id)
sources = client.extract_supported_sources(details.external_links)
print(selected.title.english, sources)
asyncio.run(main())AniListClient(
*,
endpoint="https://graphql.anilist.co",
timeout=(5, 20),
session=None,
)endpoint(str): Configurable GraphQL endpoint.timeout(float | tuple[float, float]):requeststimeout passed to every GraphQL POST.session(requests.Session | None): Optional externally managed session. When omitted, the client creates and owns one reusable session.
The client is an async context manager. aclose() closes only a session created
by the client; an injected session remains owned by the caller.
Public configuration constants:
ANILIST_ENDPOINT: Default GraphQL endpoint.ANILIST_MAX_PER_PAGE = 50: AniList rootPage.perPageAPI maximum.DEFAULT_PER_PAGE = 10: Python API and CLI default.MANDOWN_PER_PAGE_LIMIT = 25: Mandown CLI product/performance limit.
search_manga() uses Page.media with type: MANGA, preserves AniList's
result order, and returns AniListSearchResponse(items, page_info).
page_info exposes current_page, last_page, has_next_page,
per_page, and total. AniList notes that total and lastPage are not
reliable for pagination logic; use has_next_page.
Arguments:
query(str): General AniList search phrase. Empty values raiseValueError.page(int): One-based page number. Default:1.per_page(int): Results per page. Default:10; AniList API range:1..50.fields(AniListFieldSet | None): Exact trusted field preset/selection. Search defaults toCARD; details default toDETAIL.include_details: Upgrade a search to detailed fields in one request.include_external_links,include_description,include_cover: Explicitly add or remove those fields.Nonepreserves the preset.
Available presets are LIGHT, CARD, DETAIL, and FULL. Query text is
built only from supported field fragments. User input and pagination values are
always sent as GraphQL variables.
Field presets:
| Preset | Requested media fields |
|---|---|
LIGHT |
id, titles, siteUrl |
CARD |
LIGHT plus MAL ID, synonyms, format, status, country, popularity, score, and cover |
DETAIL |
CARD plus description, chapters, volumes, genres, and external links |
FULL |
Every AniList field currently supported by Mandown |
id is always requested even for a custom AniListFieldSet. Custom sets accept
only AniListField values; arbitrary GraphQL text is rejected.
A one-request rich search is also supported:
async with AniListClient() as client:
results = await client.search_manga(
"Solo Leveling",
include_details=True,
include_external_links=True,
include_description=True,
)details = await client.get_manga(
105398,
fields=mandown.AniListFieldSet.DETAIL,
include_external_links=True,
include_description=True,
include_cover=True,
)get_manga() requires a positive AniList ID and returns AniListManga.
DETAIL is the default field set. The three include options accept True,
False, or None; None preserves the selected preset.
Returned models include AniListTitle, AniListCoverImage,
AniListExternalLink, AniListMangaSummary, AniListManga,
AniListPageInfo, and AniListSearchResponse. Nullable English titles,
descriptions, chapter/volume counts, and empty external-link lists remain
nullable/empty instead of being replaced with invented values. Unknown AniList
enum strings are retained without failing parsing.
AniListMangaSummary:id,id_mal,title,synonyms,format,status,country_of_origin,popularity,average_score,cover_image, andsite_url.AniListManga: all summary fields plusdescription,chapters,volumes,genres, andexternal_links.AniListTitle: nullableromaji,english, andnative.AniListCoverImage: nullableextra_large,large,medium, andcolor.AniListExternalLink:site,url,type,language,is_disabled, andnotes.AniListSearchResponse: ordereditemstuple andpage_info.AniListPageInfo:current_page,last_page,has_next_page,per_page, andtotal. Usehas_next_pagefor pagination decisions.
extract_supported_sources() accepts AniList external links and returns only
resolver-ready MangaDex, WEBTOON, Naver Webtoon, and Naver Series URLs. It
validates both an explicit hostname allowlist and Mandown's URL resolver; a
plausible site label cannot make an unsupported domain valid. Disabled links
are excluded by default, duplicate provider/language/URL entries are removed,
and STREAMING links sort before INFO links.
Each AniListSupportedSource contains:
provider:mangadex,webtoons,naver, ornaver_series.url: URL accepted by the matching Mandown resolver.language,site, andtype: AniList external-link metadata.is_disabled: Whether AniList marked the link disabled.
Pass include_disabled=True only when disabled links are intentionally needed:
sources = mandown.extract_supported_sources(
details.external_links,
include_disabled=True,
)Unsupported links remain available in AniListManga.external_links; they are
not passed to mandown.query().
The client maps timeout, network, HTTP, invalid JSON/shape, GraphQL, and HTTP 429
responses to distinct AniListError subclasses. AniListRateLimitError
retains retry_after and reset_at when AniList supplies those headers.
Use async with or call await client.aclose() when finished.
| Exception | Meaning |
|---|---|
AniListTimeoutError |
Connect/read timeout |
AniListNetworkError |
Connection or other request failure |
AniListHTTPError |
Non-success HTTP status |
AniListRateLimitError |
HTTP 429; includes retry_after and reset_at |
AniListResponseError |
Invalid JSON or unexpected response shape |
AniListGraphQLResponseError |
GraphQL errors; typed entries are available in graphql_errors |
All inherit from AniListError, which inherits from MandownError.
AniList exposes a general search index. It does not offer a separate
English-title substring search. A short fragment such as "Omni" is therefore
not guaranteed to return a title whose English name contains that fragment.
Mandown does not build a local title index.
mandown.search(title, source=None) remains as a synchronous migration adapter.
It returns the historical SearchResults mapping with naver, webtoons,
mangadex, and anilist keys, but only anilist can contain matches.
The adapter requests external links so SearchItem.comic can lazily resolve a
supported downloader URL. Retired source filters raise a migration error.
mandown.search_all() remains a deprecated async generator and yields one
("anilist", matches) batch. Multi-provider fallback, retry, and deduplication
options are retired and reject non-default use.
The CLI equivalent is:
mandown search "Solo Leveling" --page 1 --limit 10
mandown search "Solo Leveling" --details --external-linksThe AniList API accepts up to 50 root Page results. Mandown's CLI deliberately
limits --limit to 1..25 as a product/performance choice; this is not an
AniList API limitation.
CLI options:
--page: One-based AniList page. Default:1.--limit: Results per page. Default:10; Mandown CLI range:1..25.--details: Show description, chapter/volume counts, genres, and links.--external-links: Show all AniList links and resolver-ready sources.
Loads one supported series URL.
Arguments:
url(str): A series or chapter URL recognized by one of Mandown's source adapters.
Returns:
BaseComic: The full comic metadata and chapter list.
Raises:
ValueError: The URL does not match a supported source.- Network or source-specific errors if the remote site cannot be queried.
comic = mandown.query("https://mangadex.org/title/SERIES_ID")
print(comic.metadata.title)
print(comic.metadata.authors)
print(comic.chapters[0].title)A queried or locally loaded comic.
Important members:
metadata(BaseMetadata): Title, authors, source URL, genres, description, cover URL, and generatedtitle_slug.chapters(list[BaseChapter]): Chapters in download order.source: The adapter used for chapter image requests.asdict(): Returns metadata and chapters as plain dictionaries.get_chapter_image_urls(chapter): Fetches the image URLs for one chapter.set_chapter_range(...): Keeps only a selected chapter range.update(chapters=True, metadata=True): Refreshes remote chapter and metadata information.
Fields:
title(str)authors(list[str])url(str)genres(list[str])description(str)cover_art(str)title_slug(str, generated from the title)
metadata.asdict() returns the serializable metadata fields.
Fields:
title(str)url(str)slug(str)chapter_number(str)
chapter.numeric_chapter_number is a Decimal when chapter_number is
numeric, otherwise None.
Downloads a comic and waits until the download thread finishes.
Arguments:
comic(BaseComic | str): A queried comic or supported URL.path(Path | str): Parent destination directory. Mandown creates a child folder usingcomic.metadata.title_slug.start(int | None): First selected chapter.end(int | None): End of the selected range.threads(int): Parallel image download workers. Default:4.only_download_missing(bool): Skip existing numbered images. Default:True.raise_on_failed_download(bool): RaiseImageDownloadErrorwhen the downloaded image count is incomplete. Default:True.panel_size(tuple[int, int] | None):(height, width)used to resize and split long images into fixed panels. Thedownload()default is(1280, 800). PassNoneto preserve normal source pages.image_format(str | None): Convert images to"jpg"or"png".progress_callback(Callable[[Progress, Progress], None] | None): Called with overall and current-chapter progress objects.
Chapter range semantics:
- When a source exposes real chapter numbers,
startandendrefer to those numbers and both are inclusive. - Otherwise they are list indexes:
startis inclusive andendis exclusive.
Returns:
(main_progress, chapter_progress, thread): TwoProgressobjects and the completed download thread.
def show_progress(overall, chapter):
print(f"series: {overall.progress}% chapter: {chapter.progress}%")
overall, chapter, thread = mandown.download(
comic,
"./downloads",
threads=8,
panel_size=None,
image_format="jpg",
progress_callback=show_progress,
)
assert not thread.is_alive()The iterator form of the downloader.
It accepts the same main download options, plus optional main_progress,
chapter_progress, and a no-argument progress_callback for integrations that
manage their own Progress instances.
Returns:
Iterator[str]: Yields each chapter title as that chapter begins.
for chapter_title in mandown.download_progress(comic, "./downloads"):
print("Downloading:", chapter_title)Writes the comic to <path>/md-metadata.json.
Arguments:
comic(BaseComic): Comic data to save.path(Path | str): Existing destination comic folder.
Returns:
None
Loads a Mandown comic folder containing md-metadata.json.
Arguments:
path(Path | str): Mandown comic folder.
Returns:
BaseComic
Raises:
FileNotFoundError:md-metadata.jsondoes not exist.OSError: The path cannot be read.
mandown.save_metadata(comic, "./downloads/my-series")
local_comic = mandown.load("./downloads/my-series")Loads an existing Mandown folder or initializes metadata for a folder of comic images.
Arguments:
path(Path | str): Comic folder.donor_comic(BaseComic | None): Optional source comic used to fill metadata.download_cover(bool): Download the donor's cover when initializing. Requiresdonor_comic.
Returns:
BaseComic: Existing or newly parsed local comic.
Raises:
AttributeError:download_cover=Truewas used withoutdonor_comic.
Converts a Mandown image folder or an existing comic archive.
Arguments:
comic_path(Path | str): Mandown folder or supported input archive.to(ConvertFormats):CBZ,EPUB,PDF,MOBI, orNONE.dest_folder(Path | str | None): Output directory. Defaults to the current directory.remove_after(bool): Remove the input after successful conversion. Default:False.
Returns:
None
mandown.convert(
"./downloads/my-series",
mandown.ConvertFormats.EPUB,
"./books",
)Iterator form of conversion. It also accepts split_by_chapters=True to create
one output per chapter for a Mandown folder.
Returns:
Iterator[str | int]: Conversion progress events produced by the conversion backend.
Processes images found below a comic directory.
Arguments:
comic_path(Path | str): Folder containing chapter image folders.ops(list[ProcessOps]): Operations applied in order.config(ProcessConfig | None): Resize configuration when required.
Returns:
None
Available operations include:
ProcessOps.ROTATE_DOUBLE_PAGESProcessOps.SPLIT_DOUBLE_PAGESProcessOps.TRIM_BORDERSProcessOps.RESIZEProcessOps.WEBP_TO_PNGProcessOps.NO_POSTPROCESSING
config = mandown.ProcessConfig(target_size=(1200, 800))
mandown.process(
"./downloads/my-series",
[mandown.ProcessOps.TRIM_BORDERS, mandown.ProcessOps.RESIZE],
config,
)Iterator form of image processing.
Returns:
Iterator[str]: Yields"Processing"after each processed image.
AniList search failures use the typed AniListError hierarchy described above.
Direct URL resolver/downloader implementations can still raise
SourceResponseError, requests.RequestException, or source-specific runtime
errors when a remote site changes or becomes unavailable.
async def safe_search(user_text):
try:
async with mandown.AniListClient() as client:
return await client.search_manga(user_text)
except ValueError as error:
print("Invalid search:", error)
except mandown.AniListRateLimitError as error:
print("Rate limited; retry after:", error.retry_after)
except mandown.AniListError as error:
print("AniList search failed:", error)