Using Nyora from an AI agent or programmatically¶
This page is written for LLM-driven agents and automation that need to drive
Nyora directly. Every example is copy-pasteable and matches the real API. If you
are an agent reading this: prefer the library (nyora.Nyora) for
direct calls from your own process, and the nyora-cli --json path when you
can only shell out. Both are covered below.
Minimal import surface¶
Almost everything you need is one import:
from nyora import Nyora
Nyora is a self-contained, typed client. A bare Nyora() launches a bundled
local engine — no cloud, no Node process (a JRE is auto-downloaded if needed).
Point it at a server with Nyora(base_url=...), nyora config set-url, or the
NYORA_BASE_URL environment variable.
Other useful re-exports from the top-level nyora package:
Import |
What it is |
|---|---|
|
The default client (bundled local engine). |
|
The async read client. |
|
Cloud account + library sync (OAuth2/JWT). |
|
Base SDK exception. |
|
Typed result dataclasses (all have |
The client is a context manager — always close it (use with) so the HTTP
connection is released.
End-to-end: search → details → pages → download bytes¶
A complete, typed flow that finds a source, searches it, opens the first result, resolves the first chapter’s pages, and downloads the page images with the correct per-page headers. This is the canonical agent recipe.
from __future__ import annotations
from pathlib import Path
from urllib.parse import urlparse
import httpx
from nyora import Nyora
from nyora.models import MangaPage
BROWSER_UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)
def page_headers(page: MangaPage) -> dict[str, str]:
"""Build request headers for a page image.
Sources may require a ``Referer`` and other headers; they are carried on
``page.headers``. We start from a browser UA, merge the source's headers,
and synthesize a ``Referer`` from the image origin when none is given.
"""
headers: dict[str, str] = {"User-Agent": BROWSER_UA}
headers.update({str(k): str(v) for k, v in page.headers.items()})
if "Referer" not in headers:
parsed = urlparse(page.url)
if parsed.scheme and parsed.netloc:
headers["Referer"] = f"{parsed.scheme}://{parsed.netloc}/"
return headers
with Nyora() as client:
# 1. Resolve a source (fuzzy: matches id or name, case-insensitive).
source = client.sources.find("mangadex") # -> Source
# 2. Search (page is 1-based). Use .popular()/.latest() for browsing.
results = client.manga.search(source.id, "berserk", page=1) # -> SearchPage
first = results.entries[0] # -> Manga
# 3. Full metadata + chapter list.
details = client.manga.details(source.id, first.url)
chapter = details.chapters[0] # -> MangaChapter
# 4. Resolve the chapter's image pages (no download yet).
pages = client.manga.pages(source.id, chapter.url, branch=chapter.branch)
# 5. Download the image bytes with the right headers.
out = Path("download")
out.mkdir(exist_ok=True)
width = len(str(len(pages)))
with httpx.Client(follow_redirects=True, timeout=60.0) as http:
for i, page in enumerate(pages, start=1):
resp = http.get(page.url, headers=page_headers(page))
resp.raise_for_status()
(out / f"{i:0{width}d}.jpg").write_bytes(resp.content)
print(f"Saved {len(pages)} pages to {out}")
Key facts an agent should rely on:
pagearguments are 1-based.SearchPagehas.entries: list[Manga]and.has_next_page: bool.MangaDetailshas.manga: Mangaand.chapters: list[MangaChapter].Listing/details URLs may be source-relative (e.g.
/title/...); pass them straight back intodetails()/pages()— the cloud resolves them.details()takes(source_id, manga_url, *, manga_id=None); the URL alone is usually enough.Each
MangaPagecarries.urland.headers; use those headers when downloading, or you may get 403s.chapter.branchselects a scanlation/translation; pass it through topages()(it may beNone).
Fastest path: one-shot grab¶
Every nyora-cli call spawns a process and attaches the engine, so an agent
should minimise round-trips. grab collapses search → details → download into
a single call and emits a machine-readable manifest — prefer it whenever the
goal is “get me chapter(s) of X”:
manifest = cli_json("grab", "-s", "mangadex", "berserk", "-c", "1", "-o", "./out")
# {
# "source": "mangadex",
# "manga": {"title": "Berserk", "url": "/title/..."},
# "out_dir": "./out",
# "downloaded": [{"chapter": "Chapter 1", "file": "out/Chapter_1.cbz",
# "pages": 42, "total": 42}]
# }
Chapter selection: -c N (Nth chapter in reading order, default 1),
--range A-B (e.g. 1-10, 5-, -3), or --all. The first search match is
used. Downloads are concurrent; pages are written in reading order.
Driving nyora-cli --json and parsing it¶
When you need finer control, every read command supports --json (global flag,
before the subcommand). Output goes to stdout. With --json, errors are
structured too — a failure prints {"error": "<message>"} to stdout and exits
non-zero, so you can parse success and failure the same way.
import json
import subprocess
def cli_json(*args: str):
"""Run `nyora-cli --json <args>` and return the parsed JSON.
On failure the CLI prints `{"error": ...}` and exits non-zero, so we surface
it as an exception the agent can catch.
"""
proc = subprocess.run(["nyora-cli", "--json", *args], capture_output=True, text=True)
data = json.loads(proc.stdout) if proc.stdout.strip() else {}
if isinstance(data, dict) and "error" in data:
raise RuntimeError(data["error"])
return data
sources = cli_json("sources") # list[dict]
popular = cli_json("popular", "-s", "mangadex") # {"entries": [...], "has_next_page": ...}
first_url = popular["entries"][0]["url"]
details = cli_json("details", "-s", "mangadex", first_url) # {"manga": {...}, "chapters": [...]}
chapter_url = details["chapters"][0]["url"]
pages = cli_json("pages", "-s", "mangadex", chapter_url) # [{"url": ..., "headers": {...}}, ...]
# Download the chapter to a .cbz and read back where it went.
saved = cli_json("download", "-s", "mangadex", chapter_url) # {"file": ..., "pages": int, "total": int}
print(saved["file"], f"({saved['pages']}/{saved['total']} pages)")
JSON shapes by command:
Command |
JSON shape |
|---|---|
|
|
|
array of |
|
|
|
|
|
array of |
|
|
|
|
any failure |
|
Tip
For shell scripting, pipe into jq. Note --json must precede the subcommand:
nyora-cli --json sources | jq -r '.[].id'.
Syncing a user’s library¶
To read or write a signed-in user’s library, use nyora.sync.NyoraSync
(OAuth2 password grant + JWT against https://sync.nyora.xyz). Tokens
persist to ~/.config/nyora/sync.json, so a process stays signed in across runs.
from nyora.sync import NyoraSync
sync = NyoraSync()
if not sync.is_signed_in:
sync.sign_in("[email protected]", "hunter2")
# Push last-write-wins rows and pull them back.
sync.upsert("nyora_favourite", [{"manga_id": "abc", "sort_key": 0}])
favourites = sync.select("nyora_favourite") # list[dict]
history = sync.select("nyora_history", since="2026-01-01T00:00:00Z")
The tables are nyora_manga, nyora_favourite, nyora_history, and
nyora_bookmark. See the sync guide for the full API and row shapes.
Error handling¶
All SDK failures derive from nyora.errors.NyoraError. The most
common cases:
from nyora import Nyora, NyoraError, NyoraHTTPError
with Nyora() as client:
try:
source = client.sources.find("does-not-exist")
except LookupError as exc:
# .sources.find raises LookupError when nothing matches the query.
print("no such source:", exc)
try:
page = client.manga.popular("mangadex")
except NyoraHTTPError as exc:
# The cloud returned a 4xx/5xx.
print("HTTP", exc.status_code, exc.body)
except NyoraError as exc:
print("Nyora error:", exc)
Guidance for agents:
client.sources.find(query)raisesLookupErrorif no source matches — catch it and fall back (e.g. callclient.sources.list()and pick).Cloud failures raise
NyoraHTTPError(aNyoraError) with.status_codeand.body.nyora.sync.NotSignedInErroris raised if you callupsert/selectbefore signing in.The CLI maps
NyoraError/LookupErrorto exit code1, argparse usage errors to2, andCtrl+Cto130. With--json, failures print{"error": "<message>"}to stdout (plain text to stderr otherwise).
Cheat sheet — intent → SDK call → CLI command¶
Assume client = Nyora() (use it as a context manager). All page args are
1-based.
Intent |
SDK call |
CLI command |
|---|---|---|
Search + download in one call |
(search → details → download) |
|
List all sources |
|
|
Find a source (fuzzy) |
|
|
Popular manga |
|
|
Latest manga |
|
|
Search |
|
|
Manga details + chapters |
|
|
Chapter page URLs |
|
|
Download a chapter as a |
(loop over |
|
Download every chapter as |
(loop over |
|
Sign in to sync |
|
— |
Push/pull library rows |
|
— |
Package version |
|
|
Where sid is a resolved source id (e.g. client.sources.find("dex").id) and
SRC is any fuzzy id/name the CLI resolves the same way.