Get from a clean machine to reading manga page URLs in a couple of minutes.
This page is a single copy-pasteable walkthrough: install → first search →
fetch a chapter → get pages, using the real exported API of nyora-sdk. It
covers both the library and the nyora-cli tool — the same
npm install ships both — and ends with a troubleshooting section.
Nyora is a self-contained manga sources SDK. The default client embeds the
JavaScript parser bundle inside a jsdom
window, so it needs no JVM helper: HTTP is handled by Node's native fetch
and HTML parsing by jsdom. The parser bundle and source catalog are kept current
through over-the-air (OTA) updates.
nyora-sdk (0.1.1)"type": "module")>=18 (developed and tested on Node 18–26)fetch, AbortController,
crypto.createHash, and modern jsdom — all of which need Node 18+.Check your Node version first:
node --version # must be >= 18
npm install nyora-sdk
The package is ESM-only. Import it from an .mjs file, or from a project
with "type": "module" in its package.json:
import { Nyora } from "nyora-sdk";
TypeScript declarations ship in the package (./dist/index.d.ts), so types work
out of the box.
npm install -g nyora-sdk
This installs two equivalent bin aliases, both pointing at dist/cli.js:
nyora-clinyoraVerify it works:
nyora-cli version
nyora 0.1.1
OTA parsers: bundled
OTA parsers: bundled means no OTA update has been applied yet, so the SDK is
using the parser bundle and catalog shipped inside the package (235 bundled
sources). After an update this shows the installed OTA version number instead.
The examples below use a real bundled source,
BANANASCAN_COM. List every source available to you withnyora-cli sources(orclient.sources.list()) and substitute any id you see there.
The default export and the named export Nyora are the same class. Create a
client, look up a source, run a search, and always close() when done so the
embedded jsdom window is released.
// quickstart.mjs (or quickstart.ts)
import { Nyora } from "nyora-sdk";
const client = new Nyora();
try {
// `find` does a case-insensitive substring match on a source's id OR name.
// It returns the FIRST match and throws if nothing matches.
const source = client.sources.find("bananascan");
console.log("Using source:", source.id, "—", source.name, `(${source.lang})`);
// search(sourceId, query, page = 1) → SearchPage { entries: Manga[], hasNextPage: boolean }
const results = await client.manga.search(source.id, "one", 1);
console.log(`Found ${results.entries.length} entries (more: ${results.hasNextPage})`);
for (const manga of results.entries.slice(0, 5)) {
console.log("-", manga.title, "→", manga.url);
}
} finally {
client.close(); // release the jsdom runtime
}
Run it:
node quickstart.mjs
If you don't have a query yet, list popular or latest manga. Both take
(sourceId, page = 1) and return the same SearchPage shape:
const popular = await client.manga.popular(source.id, 1);
const latest = await client.manga.latest(source.id, 1);
find worksclient.sources.find(query) lowercases query and returns the first source
whose id or name contains it. If nothing matches it throws:
No bundled source matched '<query>'
To see every available source, use client.sources.list(), which returns an
array of Source objects (id, name, lang, baseUrl, engine,
contentType, isNsfw, …).
Source ids are upper-snake-case, e.g.
BANANASCAN_COM. You can pass either the exact id or a fuzzy fragment of the id/name tofind.
A Manga entry from a listing only carries summary fields. To get the
chapter list, call details with the manga's url. Passing the known
title helps some parsers resolve the entry.
import { Nyora } from "nyora-sdk";
const client = new Nyora();
try {
const source = client.sources.find("bananascan");
const results = await client.manga.search(source.id, "one", 1);
const first = results.entries[0];
if (!first) throw new Error("no results — try another query or source");
// details(sourceId, mangaUrl, options?) → MangaDetails { manga, chapters }
const details = await client.manga.details(source.id, first.url, {
title: first.title, // optional, passed through to the parser
});
console.log("Title: ", details.manga.title);
console.log("Authors: ", details.manga.authors.join(", "));
console.log("State: ", details.manga.state ?? "(unknown)");
console.log("Chapters: ", details.chapters.length);
// Each entry is a MangaChapter:
// { id, title, number, volume, url, scanlator, uploadDate, branch, pages, index }
const chapter = details.chapters[0];
console.log("\nFirst chapter:", chapter.number, chapter.title, "→", chapter.url);
} finally {
client.close();
}
Key MangaChapter fields you'll use next:
url — the chapter URL you pass to pages.branch — scanlation branch/translation name (may be null). Useful when a
source has multiple translations of the same chapter.uploadDate — epoch milliseconds (a number).number — chapter number (may be fractional).Pass a chapter url to pages. It returns an ordered array of
MangaPage objects, each with an image url and the request headers
required to fetch it (some sources need a Referer).
import { Nyora } from "nyora-sdk";
const client = new Nyora();
try {
const source = client.sources.find("bananascan");
const results = await client.manga.search(source.id, "one", 1);
const details = await client.manga.details(source.id, results.entries[0].url);
const chapter = details.chapters[0];
// pages(sourceId, chapterUrl, options?) → MangaPage[]
// options.branch selects a scanlation branch (default: null).
const pages = await client.manga.pages(source.id, chapter.url, {
branch: chapter.branch, // pass the chapter's own branch, or null
});
console.log(`Chapter has ${pages.length} pages:`);
pages.forEach((page, i) => {
console.log(`${String(i + 1).padStart(3)} ${page.url}`);
});
// Each MangaPage is { url: string, headers: Record<string,string> }.
// The headers (e.g. Referer) are what you must send when downloading the image.
} finally {
client.close();
}
MangaPage.headers carries everything a source requires to serve the image.
Merge them into your fetch and reuse the runtime's User-Agent, exported as
BROWSER_UA:
import { BROWSER_UA } from "nyora-sdk";
const page = pages[0];
const res = await fetch(page.url, {
headers: {
"User-Agent": BROWSER_UA,
...page.headers, // source-specific headers (Referer, etc.)
},
});
const bytes = Buffer.from(await res.arrayBuffer());
// write `bytes` to disk as 001.jpg, etc.
For a ready-made archive, the CLI's download command packs a whole chapter
into a .cbz for you (see below).
Every step above maps to a nyora-cli subcommand. All of these accept
-s/--source (a source id or a fuzzy name) and return exit code 0 on
success, 1 on a handled error, and 2 for an unknown command.
# 1. List / filter sources
nyora-cli sources # all 235 sources: id<TAB>name<TAB>lang, then "(N sources)"
nyora-cli sources --search banana # filter by id/name substring
# 2. Search a source (-p/--page defaults to 1)
nyora-cli search -s bananascan -p 1 one
# 3. Fetch details + chapter list for a manga URL
nyora-cli details -s bananascan "<manga-url>"
# 4. Get the page image URLs for a chapter URL
nyora-cli pages -s bananascan "<chapter-url>"
nyora-cli pages -s bananascan --branch "English" "<chapter-url>"
# 5. (Bonus) Download a whole chapter as a .cbz archive
nyora-cli download -s bananascan "<chapter-url>" # → <slug>.cbz in the cwd
nyora-cli download -s bananascan -o ~/Downloads "<chapter-url>" # -o dir → <slug>.cbz inside it
nyora-cli download -s bananascan -o ./ch1.cbz "<chapter-url>" # -o ending in .cbz → exact file
Notes on the CLI:
The <url> arguments are positional — put them after the flags. Use --
to stop flag parsing if a URL begins with -.
You can also browse with nyora-cli popular -s <src> and
nyora-cli latest -s <src> (both take -p/--page).
download writes a CBZ (a plain ZIP of the page images, stored uncompressed).
It prints Saved N/total pages to <file> and exits 0 if any pages were
saved, 1 if none.
Add the global --json flag before the subcommand to emit raw JSON instead
of formatted text:
nyora-cli --json search -s bananascan one | jq '.entries[0]'
Run nyora-cli --help (or -h) for the full usage summary.
See the CLI guide for the complete command manual.
Running nyora-cli (or nyora) with no subcommand launches an interactive
terminal reader that walks you through filter → browse/search → pick manga →
pick chapter → page URLs, using arrow keys and Enter.
nyora-cli
It requires an interactive terminal (a TTY). When stdout is piped/redirected
(e.g. under CI), it prints a notice and exits 0 instead of launching — so use
the subcommands above for scripting. See the TUI guide.
The parser bundle and source catalog update over the air. The client owns an
OtaManager at client.ota, plus two convenience methods that also
reload the runtime so new parsers take effect immediately.
From the CLI:
nyora-cli update # fetch the latest parser bundle (sha256-verified) + catalog
nyora-cli update --force # re-download even if already current
nyora-cli version # show package + installed OTA version
update prints either Updated to OTA version X or
Already up to date (OTA version X), then the cached bundle: and sources:
paths.
From code:
import { Nyora } from "nyora-sdk";
const client = new Nyora();
try {
// Safe opportunistic check (network errors → { available: false }).
const status = await client.checkUpdate();
// { available: boolean, installed: number | null, latest: number | null }
console.log(status);
if (status.available) {
// Downloads, SHA-256-verifies, caches, AND reloads the runtime.
const result = await client.update(); // or client.update({ force: true })
// { updated, version, bundlePath, sourcesPath }
console.log("OTA now at version", result.version);
}
// Lower-level access if you need it:
console.log("OTA cache dir:", client.ota.cacheDir);
console.log("Installed OTA version:", client.ota.installedVersion()); // number | null
} finally {
client.close();
}
The OTA feed lives at https://Hasan72341.github.io/nyora-ota-parsers (exported
as OTA_BASE). When the cache is empty, reads fall back to the assets bundled
in the package, so the SDK works fully offline on first run. See the
OTA guide for cache layout and verification details.
import fails / require is not defined / "Cannot use import statement"nyora-sdk is ESM-only ("type": "module"). You cannot require() it from
CommonJS. Fix one of:
.mjs, or"type": "module" to your package.json, orconst { Nyora } = await import("nyora-sdk");fetch is not defined / Node version errorsThe package requires Node.js >= 18. It relies on a global fetch,
AbortController, crypto.createHash, and modern jsdom — all Node 18+. If you
see fetch is not defined, you're almost certainly on Node < 18:
node --version # must be >= 18 — upgrade if lower
jsdom (^24.1.0) is a direct dependency and is pure JavaScript — there is no
native compilation step, so a normal npm install should just work. If
installation looks broken, do a clean reinstall:
rm -rf node_modules package-lock.json
npm install
new Nyora() (and new ParserRuntime()) builds a jsdom window eagerly in its
constructor, so a missing jsdom surfaces as Cannot find module 'jsdom' the
moment you construct a client. Reinstalling dependencies resolves it.
ParserRuntimeError: Parser bundle did not expose NyoraParsersThe cached or bundled parsers.bundle.js couldn't be evaluated into the
expected global — usually a corrupted OTA cache. Clear it (the SDK then falls
back to the package-bundled assets) and re-update:
# macOS
rm -rf ~/Library/Caches/nyora/ota
# Linux
rm -rf ~/.cache/nyora/ota
nyora-cli update --force # re-download a fresh, SHA-256-verified bundle
Parser not found: <ID> or No bundled source matched '<query>'No bundled source matched comes from sources.find(...): your fuzzy query
matched no source id/name. List them with nyora-cli sources (or
client.sources.list()).Parser not found (a ParserRuntimeError) means the resolved source id
has no parser in the current bundle. Try nyora-cli update to refresh
parsers, or pick a different source.pages array (no error thrown)The runtime is deliberately tolerant: HTTP transport failures return an
empty body instead of throwing, and the response body is returned for any
status code. So a network hiccup, an anti-bot wall, or a parser that needs a
different branch can yield entries: [] or pages: [] with no exception. Try:
pages, pass the chapter's own branch (options.branch) — some sources
key pages by translation branch.details, pass the known title so the parser can resolve the entry.You ran bare nyora-cli (the TUI) in a non-interactive context (piped,
redirected, or CI). That's expected — it prints a notice and exits 0. Use the
subcommands (sources, search, details, pages, …) for scripting instead.
Each Nyora / ParserRuntime / NyoraServer instance owns one jsdom
window, and calls are serialized through a promise chain. For true parallelism,
create multiple clients (each with its own runtime) and close() every one.
import {
Nyora, // default client (also the default export)
SourcesService, // client.sources
MangaService, // client.manga
OtaManager, // client.ota
ParserRuntime, // low-level jsdom parser executor
NyoraServer, // helper-compatible REST server
OTA_BASE, // "https://Hasan72341.github.io/nyora-ota-parsers"
BROWSER_UA, // Chrome User-Agent string
} from "nyora-sdk";
const client = new Nyora();
client.sources.list(); // Source[]
client.sources.find("bananascan"); // Source (throws if none)
await client.manga.popular(id, page); // SearchPage
await client.manga.latest(id, page); // SearchPage
await client.manga.search(id, query, page); // SearchPage
await client.manga.details(id, url, { title }); // MangaDetails
await client.manga.pages(id, url, { branch }); // MangaPage[]
await client.checkUpdate(); // OtaUpdateAvailability { available, installed, latest }
await client.update({ force }); // OtaUpdateResult { updated, version, bundlePath, sourcesPath }
client.ota.cacheDir; // string
client.ota.installedVersion(); // number | null
client.close(); // release the jsdom runtime