24 KiB
Scraper guide: supplying the MangaCircus library
Audience: an automated agent or program that downloads manga, comics or webtoons from upstream sources and writes them into a MangaCircus library folder.
Normative words: MUST, MUST NOT, SHOULD, SHOULD NOT and MAY as in RFC 2119. Rules have ids (R-…) so you can cite them in logs and reviews.
Background reading (optional; this guide is self-contained):
- library-format.md: the format.
- import.md: how MangaCircus reads it.
0. The contract in short
- You write plain folders and image files. There is no upload API.
- MangaCircus scans the library folder every
SCAN_INTERVALminutes (default 10), and on demand (§9). - The layout is
<library>/<Manga>/<Book>/<Chapter>/<pages>, always three levels. - Names carry meaning:
- the manga folder name is the manga's id,
- the book/chapter numbers in folder names are ids and order,
- the chapter folder name is the version id,
- page file names carry order and page roles.
- MangaCircus notifies followers as soon as a new chapter number becomes visible. So a chapter MUST become visible complete and in one step: stage it in a hidden folder, then rename it (§10).
- MangaCircus never writes to the library. You own it; MangaCircus only reads it.
1. Inputs you need
| Input | Required | Notes |
|---|---|---|
| Library root path | yes | The folder MangaCircus mounts as LIBRARY (/library in Docker) |
| Upstream manga metadata | yes | Title, reading direction/type, chapter list with numbers |
| Per chapter: ordered page images | yes | Plus, where available: which pages are covers, spreads, credits/notes |
| MangaCircus URL + an admin account | no | Only to trigger an immediate rescan (§9) |
2. Target layout (template)
<library>/
<Manga folder>/ R-M*
manga.json R-J1
cover.jpg R-M5
v01[ <Book title>]/ R-B*
book.json optional
c0001[ <Chapter title>] [<Source>]/ R-C*, R-V*
chapter.json R-J2
0001_cover.jpg R-P*
0002.jpg
…
3. Manga folders
- R-M1 The manga folder name MUST NOT start with
.and MUST be a valid name on Windows, macOS and Linux:- no
< > : " / \ | ? *and no control characters, - no trailing space or dot,
- not a reserved Windows name (
CON,PRN,AUX,NUL,COM1–COM9,LPT1–LPT9), - Unicode normalised to NFC,
- SHOULD be ≤ 120 characters.
- no
- R-M2 Derive the name from the title once (e.g. the English or romanised title, sanitised per R-M1). After that you MUST NOT rename it: the folder name is the manga's id, and renaming loses every user's progress, follows and votes. Put title changes into
manga.json→title. - R-M3 Before creating a manga, you MUST look for an existing folder for the same work, so one work never ends up in two folders. Match on
manga.json→sources[].url(or your own id field, R-J1). Only if nothing matches, create a new folder. - R-M4 A new manga MUST be published as a whole: stage it as
<library>/.staging-<random>/with at least one complete chapter, then rename it to its final name (§10). - R-M5 Put the series cover in the manga folder as
cover.<ext>(jpg,jpeg,png,webp,gif,avif,bmp). It SHOULD be at most about 1000 px tall, because it is shown in the library grid at original size. If you have no cover, omit it: the first chapter's cover page is used instead. - R-M6 You MUST NOT put page images directly into the manga folder. Only
manga.json,cover.*and book folders belong there.
4. Books (middle level)
- R-B1 Book folder name pattern:
v<NN>[ <title>]for volumes, ors<NN>[ <title>]for seasons,x<NN>[ <title>]for extra books (specials, art books, one-shot collections) that sit outside the numbering.- Example:
v01 Spring,s02 Season 2,x01 Specials.
- R-B2 Pad numbers to 2 digits, or to 3 digits for series that can exceed 99 volumes, and keep the width the same within a manga. Re-padding a book folder later (
v01→v001) is safe: the number, not the text, is the id. - R-B3 Book numbers MUST be unique within a manga. MangaCircus ignores a second folder with the same number.
- R-B4 A chapter's id includes its book number (
<book>/<chapter>). Moving a chapter to another book later creates a "new" chapter: progress and votes stay behind, and followers get notified again. Therefore you MUST choose a book strategy per manga that never needs to move chapters:- Flat (default): every chapter goes into
v01(for examplev01 Chapters). Use it for ongoing series and for sources without reliable volume data. - Volumes: chapters go into their volume, only if the source gives a final volume number at the time you first write the chapter, and for every chapter.
- Seasons (webtoons): only if the source states the season.
Record the strategy for yourself (e.g. in your own field in
manga.json, R-J1) so later runs follow it.
- Flat (default): every chapter goes into
- R-B5 The book folder MUST contain only
book.json,cover.*and chapter folders.
5. Chapters
- R-C1 Chapter folder name:
c<NNNN>[.<d>][ <title>][ [<Source>]] regular chapter x<NN>[ <title>][ [<Source>]] extra chapter outside the numbering special-mangadex-<chapter-uuid>[ <title>][ [<Source>]] unnumbered source release- A space MUST follow the number when anything follows.
c0012_Titlesorts wrongly in file browsers;c0012aandc0012Titleare not recognised at all. - Recommended check:
^(c\d{4}(\.\d*[1-9])?|x\d{2})( [^\[\]]+?)?( \[[^\[\]]+\])?$ - For the additive
special-mangadex-form, validate the complete UUID usingchapter-identity.js. Do not apply the numeric-only check above. The key is the lower-case source UUID prefix, not an invented numeric chapter. Books remain numeric.
- A space MUST follow the number when anything follows.
- R-C2 Pad the chapter number's integer part to 4 digits. Never change the padding of an existing manga: chapter folder names are version ids.
- R-C3 Map upstream numbering like this:
| Upstream | Folder key | Notes |
|---|---|---|
12 |
c0012 |
|
12.5 |
c0012.5 |
In-between chapter, stays in reading order |
12.50 |
c0012.5 |
MUST drop trailing decimal zeros. 12.10 would collide with 12.1 |
12a, 12b |
c0012.1, c0012.2 |
Keep "12a" in chapter.json → title if it matters. Check for collisions with real 12.1 |
12 part 1/2 |
c0012.1, c0012.2 |
As above |
0, Prologue |
c0000 |
|
Omake, Extra, Side story placed after chapter 12 |
c0012.5 + "extra": true in chapter.json |
In order, but skippable |
Special, Bonus, Character profiles, not tied to a position |
x01, x02, … in the book they belong to |
Listed after the book's regular chapters, skippable |
| Volume extras (afterword, sketches) | x<NN> in that volume's book |
|
| One-shot | its own manga (v01/c0001), or x<NN> inside an x01 Oneshots book of the related series |
|
| Negative or non-numeric only | x<NN> |
Never invent a number that collides with a real one |
| Unnumbered MangaDex release | special-mangadex-<chapter-uuid> |
Stable upstream release identity; label Pilot, One-shot or Special; do not assign chapter 0/1 |
Unnumbered source-ID releases sort after numeric chapters. Their chapter.json
stores special:true, label, sourceId and the original sourceNumber (usually
null). They are not automatically skippable extras: an entire pilot may be story
content. Keep different release UUIDs separate rather than guessing that they are
the same logical chapter. Reimporting the same key is duplicate-safe. Existing
numeric and x folder conventions and progress IDs MUST NOT be migrated or renamed.
- R-C4 The title part of the folder name is for humans only.
- The authoritative title is
chapter.json→title, without the "Chapter 12:" prefix. - Remove
[and]from titles in folder names (a trailing[…]is read as the source).
- The authoritative title is
- R-C5 Chapter keys MUST be unique within a book, except for deliberate versions (§6).
- R-C6 Use the upstream's canonical chapter numbering (preferably the official one). If two sources number the same content differently, map both to one numbering; otherwise they won't be recognised as versions of each other.
6. Versions
- R-V1 Each source's copy of a chapter is its own version folder in the same book, with the same chapter number and a source suffix:
The suffix SHOULD always be present when a manga has, or may get, more than one source.
c0012 The Storm [MangaDex]/ c0012 The Storm [Official]/ - R-V2 The version folder name is the version's id, and user votes attach to it. You MUST NOT rename an existing version folder. To update its content, replace it under the same name (R-W4).
- R-V3 Set
chapter.json→source={ "name", "url" }for every version. - R-V4 You MAY set
chapter.json→priority(number, default0) to decide which version wins while users haven't voted, e.g.1for official releases. Votes always beat priority. - R-V5 You MUST NOT delete another source's version to "replace" it. Add your version next to it.
7. Pages
- R-P1 Page file name:
<NNNN>[_<tag>]…<.ext>- Numbers are 4 digits, contiguous, starting at
0001, in reading order. - Use 5 digits for the whole chapter if it has more than 9999 files.
- Recommended check:
^\d{4,5}(_[a-z0-9]+)*\.(jpg|jpeg|png|webp|gif|avif|bmp)$
- Numbers are 4 digits, contiguous, starting at
- R-P2 Allowed formats: JPEG, PNG, WebP, GIF, AVIF, BMP. The extension MUST match the actual format (it decides the
Content-Type).- SHOULD keep the upstream bytes as they are; don't re-encode.
- Convert only formats that are not allowed.
- R-P3 Tags: add only what you know. When unsure, add no tag.
| Tag | Add when | Don't add when |
|---|---|---|
_cover |
The page is a chapter cover or title splash (usually the first page, often in colour, with the chapter title) | A normal first story page |
_double |
One image shows two facing pages (a spread). Usually the width is > 1.2 × the height of normal pages | The source split the spread into two images: keep those as two normal pages, untagged, in order |
_extra |
Credits, recruitment or "support us" pages, translator notes, scanlator ads, previews of other works | Story content |
_rot90 / _rot180 / _rot270 |
The source says, or you can detect with certainty, that the stored image is turned. The value is the clockwise angle needed to show it upright | You only guess |
- R-P4 Webtoons (
direction: "ttb"):- Keep the slices as delivered, in order, preferably all the same width. Don't tag spreads.
- SHOULD NOT stitch slices into one huge image; very tall images fail on some devices.
- SHOULD NOT cut slices smaller.
- R-P5 The chapter folder MUST contain only page images and
chapter.json. Other files are ignored, and sub-folders are not read. - R-P6 You SHOULD keep extra pages (tagged
_extra) instead of deleting them; readers can skip them. You MAY drop pure advertising.
8. Metadata files
All are UTF-8 JSON without BOM, pretty-printing optional.
-
R-J0 Every JSON file MUST be valid JSON. A broken file is ignored with a warning; the folder then falls back to name-based defaults.
-
R-J1
manga.json{ "title": "Moon Garden", "altTitles": ["Tsuki no Niwa"], "authors": ["Aki Tanaka"], "status": "ongoing", "year": 2021, "tags": ["Fantasy", "Slice of Life"], "description": "Synopsis, line breaks allowed.", "direction": "rtl", "sources": [ { "name": "MangaDex", "url": "https://mangadex.org/title/…" } ], "x-scraper": { "strategy": "flat", "ids": { "mangadex": "…" } } }-
directionwhen an operator or reliable source explicitly provides the reading order:Upstream type Value Japanese manga rtlWestern comic, printed manhwa or manhua ltrWebtoon / vertical scroll (also most digital manhwa and manhua) ttb -
sources: merge. Add your entry if itsurlis missing, update your own entry, and never remove others. -
Unknown fields are kept and ignored by the app. Keep your own state in one namespaced field such as
"x-scraper". -
The whole file is shown to logged-in users. It MUST NOT contain secrets (tokens, cookies, credentials).
-
Read-modify-write: keep fields you don't own, because people may have edited them. Write atomically (R-W5).
-
R-J4 The current adapters do not infer printed reading order from host, language, country, or catalog labels. They emit
rtlwith"x-reading-direction":{"version":1,"value":"rtl","source":"fallback"}. The publisher checks already downloaded image dimensions: at least three pages with height/width ≥ 4, at least 75% tall among known pages, consistent strip widths within 25%, and dimensions known for at least 75% of pages yielddirection: "ttb"withsource: "detected"and numeric evidence. A credits card can coexist with the strips. This detects layout, not content or language. Plain legacy directions and explicitdirectionOverrideat any level are preserved across later imports. Do not bulk-convert oldrtlvalues: a past scraper default and a human choice are indistinguishable. Use the reviewed migration tool on a named series after checking representative chapters.node tools/review-reading-direction.js <absolute-library-manga-folder> <weebcentral-series-id> --chapter=c0056is a read-only review. To apply a supported single-series correction, first pause and stop every writer and create a stopped-writer snapshot, then add--apply --writers-stopped. The tool verifies source identity, unchanged legacy RTL metadata, several chapter samples, and the requested chapter before an atomic metadata write.
-
-
R-J2
chapter.json, one per version folder:{ "title": "The Storm", "source": { "name": "MangaDex", "url": "https://mangadex.org/chapter/…" }, "added": "2024-03-15T10:00:00Z", "priority": 0, "extra": false }Field Meaning addedUpstream release date, else the download date (ISO 8601). It drives "recently updated" extraOnly for in-order omake (see R-C3) directionOnly if this chapter differs from the manga (e.g. a vertical colour special) -
R-J3
book.json:{ "title": "Spring" }. Optional; only write it if the source has volume titles.
9. Triggering the import (optional)
New content shows up by itself at the next scheduled scan. For immediate pickup, trigger one rescan after a batch, not per chapter:
curl -s -c jar.txt -H 'Content-Type: application/json' -d '{"name":"<admin>","password":"<password>"}' "$MANGACIRCUS/api/login"
curl -s -b jar.txt -H 'Content-Type: application/json' -d '{}' "$MANGACIRCUS/api/admin/rescan"
- The response contains
scan.error(null on success) and counts. - Use a dedicated admin account for the scraper, and keep its password out of the library.
10. Writing safely
MangaCircus can scan at any moment. Everything it can see must always be complete.
-
R-W1 All staging paths MUST start with
.and MUST be on the same file system as their final location; only then isrenameatomic. Put staging folders next to the target:Target Staging location new chapter or version <book>/.staging-<random>/new book the book folder itself may be created early (an empty book is ignored), then stage chapters inside it new manga <library>/.staging-<random>/ -
R-W2 Publish = one
renameof the finished staging folder to its final name. Write every image andchapter.jsonbefore that rename. -
R-W3 You MUST NOT overwrite image files in place. MangaCircus caches image sizes per folder mtime, and an in-place overwrite goes unnoticed. Replace whole version folders instead (R-W4).
-
R-W4 Replacing an existing version (same folder name):
- stage the new copy,
rename(final → .old-<random>),rename(staging → final),- delete
.old-<random>. The chapter is briefly absent, which is harmless: it won't be re-announced, and progress is kept.
-
R-W5 Updating a JSON file: write
.<name>.tmpin the same folder, thenrenameit over the target. -
R-W6 Preserve incomplete staging folders: validated images are recovery assets, not disposable garbage. The managed publisher resumes a hidden folder only when its complete chapter/source/page-manifest fingerprint matches, and revalidates stored images. Legacy random stages lack this identity and require manual review; do not infer ownership or delete them merely because they are old. Completed replacement
.old-*backups may be cleaned after successful publication. -
R-W7 On Windows,
renamecan fail withEPERM/EBUSYwhile another process reads the folder. Retry a few times with a short delay. -
R-W8 Files MUST be readable by the MangaCircus process. In Docker that is uid 1000, so use mode
0644for files and0755for folders. -
R-W9 You MUST NOT write anything into MangaCircus's
DATAfolder (db.json,scan-cache.json).
10.1 Reference implementation (Node.js, no dependencies)
const fs = require('fs'), path = require('path'), crypto = require('crypto');
const rnd = () => crypto.randomBytes(4).toString('hex');
function renameRetry(from, to, tries = 5) {
for (let i = 0; ; i++) {
try { return fs.renameSync(from, to); }
catch (e) {
if (i >= tries || !['EPERM', 'EBUSY', 'EACCES'].includes(e.code)) throw e;
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (i + 1)); // sleep
}
}
}
// pages: [{ name: '0001_cover.jpg', data: Buffer }, …] in reading order
function publishChapter(bookDir, folderName, pages, chapterJson) {
fs.mkdirSync(bookDir, { recursive: true });
const stage = path.join(bookDir, '.staging-' + rnd());
fs.mkdirSync(stage);
for (const p of pages) fs.writeFileSync(path.join(stage, p.name), p.data);
fs.writeFileSync(path.join(stage, 'chapter.json'), JSON.stringify(chapterJson, null, 2) + '\n');
const target = path.join(bookDir, folderName);
let old = null;
if (fs.existsSync(target)) { old = path.join(bookDir, '.old-' + rnd()); renameRetry(target, old); }
renameRetry(stage, target);
if (old) fs.rmSync(old, { recursive: true, force: true });
}
function writeJsonAtomic(file, obj) {
const tmp = path.join(path.dirname(file), '.' + path.basename(file) + '.tmp');
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n');
renameRetry(tmp, file);
}
11. Procedure per run
for each manga to scrape:
folder = find existing manga folder whose manga.json.sources[].url (or x-scraper id) matches # R-M3
if none: folder = sanitize(title) # R-M1/2
make sure it does not exist yet; if it does and belongs to another work, add a suffix
strategy = folder's x-scraper.strategy or choose one (flat unless final volumes are known) # R-B4
upstream_chapters = fetch chapter list
for each upstream chapter:
book = strategy == flat ? "v01" : "v%02d" % volume # R-B1/2
key = map_number(upstream number) # R-C3
name = key + (" " + sanitize_title(title) if title) + " [" + source_name + "]" # R-C1/C4/V1
if <book>/<name> exists and is complete and unchanged upstream: skip # idempotent
pages = download in reading order; tag cover/double/extra/rot only when certain # R-P*
validate(pages, name) # §12
publishChapter(<manga>/<book>, name, pages, chapter.json) # §10
merge + write manga.json atomically, update cover.* if missing # R-J1, R-M5
(new manga: do all of the above inside <library>/.staging-<rnd>/, then rename once) # R-M4
after the batch: optionally trigger one rescan # §9
12. Validation checklist (before every publish)
- Manga folder name passes R-M1 and is the existing folder for this work (R-M3).
- Book folder matches
^(v|s|x)\d{2,3}( [^\[\]]+)?$, and its number is unique in the manga. - Chapter folder matches R-C1, has no trailing decimal zeros, and its number follows R-C3.
- The same chapter number exists in this book only as deliberate versions with distinct
[Source]suffixes. - ≥ 1 page. Names
0001…Nare contiguous and match R-P1. Extensions match the real formats. - Tags come only from
cover,double,extra,rot90,rot180,rot270(plus your own on purpose). - At most one or two
_coverpages, and they are at the start. chapter.jsonparses, andtitlehas no "Chapter N" prefix.sourceis{name, url}, andaddedis ISO 8601.- No other files in the chapter folder.
- Staging folder starts with
.and lives next to the target. - Publishing is a single rename.
- No secrets in
manga.json.
13. What MangaCircus does with your output
| You do | MangaCircus does |
|---|---|
| Publish a manga folder for the first time | Adds it to the library. No notifications (first sight) |
| Publish a new chapter number in a known manga | Lists it, and notifies followers in the app and by webhook within one scan |
| Publish another version of an existing chapter | Adds it to the version picker. No notification. The best-voted (then highest priority) version is the default |
| Make a chapter visible before it is complete | Indexes it as it is, notifies followers, and readers see missing pages |
| Rename a manga folder | Treats it as a new manga. User data stays with the old name |
| Move or renumber a chapter | Treats it as a new chapter and notifies again. Old progress and votes are orphaned |
| Rename a version folder | Votes for it are lost |
| Remove a chapter | It disappears from the list. It is not re-announced if it comes back |
| Write invalid names or JSON | Skips the folder or ignores the JSON, and logs a [scan] warning in MangaCircus's output |
14. Worked example
Upstream (simplified):
{ "title": "Moon Garden", "type": "manga", "url": "https://src.example/t/42",
"chapters": [
{ "num": "1", "title": "Chapter 1: The Seed", "vol": 1, "date": "2024-01-10", "pages": ["a.jpg", "b.jpg", "c.jpg"] },
{ "num": "2", "title": "Roots", "vol": 1, "date": "2024-01-24", "pages": ["…"] },
{ "num": "2.50", "title": "Omake: Tea Time", "vol": 1, "date": "2024-02-01", "pages": ["…"] },
{ "num": "Special", "title": "Character Sheets", "vol": 1, "date": "2024-02-02", "pages": ["…"] }
] }
The volume numbers are final, so the volumes strategy applies. Result:
Moon Garden/
manga.json {"title":"Moon Garden","direction":"rtl","sources":[{"name":"SrcExample","url":"https://src.example/t/42"}],"x-scraper":{"strategy":"volumes"}}
cover.jpg
v01/
c0001 The Seed [SrcExample]/ chapter.json {"title":"The Seed", …} 0001_cover.jpg 0002.jpg 0003.jpg
c0002 Roots [SrcExample]/ chapter.json {"title":"Roots", …} 0001.jpg …
c0002.5 Tea Time [SrcExample]/ chapter.json {"title":"Omake: Tea Time","extra":true, …}
x01 Character Sheets [SrcExample]/ chapter.json {"title":"Character Sheets", …} 0001_extra.jpg …
a.jpgbecame0001_cover.jpgonly because the upstream marks it as a cover, or it was verified to be one.2.50becamec0002.5(R-C3).- "Special" without a position became
x01in its volume (R-C3).