Set of tools to download manga from comizy.io website. Optimized tools for use with Koharu translator.
  • JavaScript 50.6%
  • Python 49.4%
Find a file
2026-07-27 01:28:45 +03:00
.gitignore Prep repo for public release 2026-07-23 21:11:59 +03:00
CLAUDE.md Added grouping 2026-07-27 01:28:45 +03:00
download.js Added grouping 2026-07-27 01:28:45 +03:00
download.py Added grouping 2026-07-27 01:28:45 +03:00
images_to_pdf.js Added grouping 2026-07-27 01:28:45 +03:00
images_to_pdf.py Added grouping 2026-07-27 01:28:45 +03:00
LICENSE Prep repo for public release 2026-07-23 21:11:59 +03:00
package-lock.json Added PDF merger 2026-07-27 00:18:20 +03:00
package.json Added PDF merger 2026-07-27 00:18:20 +03:00
README.md Added grouping 2026-07-27 01:28:45 +03:00
requirements.txt Added PDF merger 2026-07-27 00:18:20 +03:00

comizy.io downloader

Downloads chapters of a comic from comizy.io as images, organized into per-chapter folders.

Two equivalent implementations are included — pick whichever you have installed. Both use the same API flow and produce the same output.

Note: this is an unofficial tool, not affiliated with or endorsed by comizy.io. See Legal / responsible use before running it.

Usage

Node:

node download.js <comic-url> [output-dir] [options]

Python:

python download.py <comic-url> [output-dir] [options]

Both accept the same options — run either with -h / --help for the full list:

Option Meaning
--chapters <selector> Download a subset of chapters (default: all)
--group <n> Combine every n chapters into a single folder
-h, --help Show usage and exit

--chapters addresses chapters by position in reading order (1 = first chapter, regardless of its slug/number):

  • --chapters 5 — a single chapter
  • --chapters 1-10 — an inclusive range
  • --chapters 1,3,5-7 — a comma-separated mix of singles and ranges
  • --chapters latest:5 — the most recent 5 chapters

Example:

node download.js https://comizy.io/<comic-name> output --chapters 1-10
python download.py https://comizy.io/<comic-name> output --chapters latest:5

Output structure:

output/
  001_Chapter 1/
    01.webp
    02.webp
    ...
  002_Chapter 1.1/
    01.webp
    ...

Chapter folders are numbered in reading order (by chapter number, not by slug), and images within each chapter are numbered in the order the site returns them. Folder numbers reflect the chapter's position across the full chapter list even when --chapters is used to download a subset — e.g. --chapters latest:2 on a 103-chapter comic produces folders 102_... and 103_..., not 01_... and 02_....

Grouping chapters into one folder

--group <n> puts every n chapters into a single folder instead of one folder per chapter, which is handy for producing one PDF per batch of chapters rather than per chapter:

node download.js https://comizy.io/<comic-name> output --group 5
python download.py https://comizy.io/<comic-name> output --group 5
output/
  001-005/
    001_01.webp     <- chapter 1, page 1
    001_02.webp
    ...
    005_03.webp     <- chapter 5, page 3
  006-010/
    006_01.webp
    ...

Inside a grouped folder each filename is prefixed with the chapter's position, so pages stay in reading order across the whole folder and can't collide between chapters. Group boundaries are computed over the full chapter list, so they don't shift when --chapters selects a subset — a given chapter always lands in the same group folder.

Requirements

  • Node.js 18+ (uses the built-in fetch), or
  • Python 3.8+ (stdlib only — urllib + concurrent.futures, no pip install needed)

Both scripts use a 20-second timeout per HTTP request (with a few retries) so a stalled connection doesn't hang the whole run, and to avoid hammering the site with indefinitely-retried requests.

If a download times out mid-body, the request is automatically retried in smaller Range-limited pieces, which works around network paths that stall on large responses. This is transparent — you may just notice the first image of a run taking a few seconds longer.

Converting a chapter to PDF

images_to_pdf takes a folder of images (e.g. one of the chapter folders produced above) and combines them into a single PDF, in natural sort order (2.jpg before 10.jpg).

Node (requires npm install — uses sharp + pdf-lib):

npm install
node images_to_pdf.js <input-dir> [output.pdf] [options]

Python (requires pip install -r requirements.txt — uses Pillow):

pip install -r requirements.txt
python images_to_pdf.py <input-dir> [output.pdf] [options]

Both accept the same options:

Option Meaning
-o, --output-dir <dir> Where to write the PDF when output.pdf is omitted (default: output)
-q, --quality <1-100> JPEG quality (default: 75)
-w, --max-width <px> Downscale pages wider than this (default: 0, no downscaling)
-g, --grayscale Encode pages as grayscale — smaller, but only sane for B/W comics
-r, --recursive Write one PDF per immediate subdirectory containing images
-h, --help Show usage and exit

If output.pdf is omitted the PDF is named after the input folder and written into output/ (override with -o). --recursive turns a whole download into one PDF per chapter (or per group) in a single pass:

node images_to_pdf.js output -r
python images_to_pdf.py output -r

About PDF size

Pages are embedded as JPEG, which is PDF's native photographic format (DCTDecode). PDF has no WebP support at all — there is no filter for it in the format, so the WebP files comizy.io serves cannot be stored as-is and must be re-encoded no matter what. Since WebP is the more efficient format of the two, a straight conversion at default quality lands near or slightly above the total size of the source images; that's a property of the formats, not a bug in the conversion.

If you need a smaller file, --quality and --max-width are the levers — dropping to -q 60 or capping width with -w 800 both make a substantial difference. --grayscale helps too, but only use it on comics that are actually black and white.

The Node script produces noticeably smaller files than the Python one at identical settings, because sharp bundles mozjpeg while Pillow uses standard libjpeg. Both are valid PDFs at the requested quality; if file size matters most to you, prefer the Node script.

How it works

comizy.io is a Next.js app backed by a separate API host (api.comizy.io). No login or cookies are needed to read public chapters — just a browser-like User-Agent and Referer header. The flow, reverse-engineered from a browser HAR capture:

  1. Fetch the comic pageGET https://comizy.io/<slug> The HTML contains a <script id="__NEXT_DATA__"> blob with:

    • buildId — the current Next.js build id, needed to construct data-fetch URLs
    • props.pageProps.initialManga.id — the comic's internal id (a short opaque string)
  2. List chaptersGET https://api.comizy.io/titles/<mangaId>/chapters Returns data.chapters, an array of { id, slug, name, number, ... }. number is the correct reading order; the human-readable name/slug (e.g. "Chapter 1.1") doesn't sort correctly as a string, so chapters are sorted by number before downloading.

  3. Get chapter imagesGET https://comizy.io/_next/data/<buildId>/<slug>/<chapterSlug>.json (with header x-nextjs-data: 1) This is Next.js's own data endpoint for the chapter reader page. The response's pageProps.initialChapter.images is an ordered array of full CDN image URLs (hosted on *.cmzcdn.org), already in reading order.

  4. Download each image directly from its CDN URL with a Referer: https://comizy.io/ header.

No API key, auth token, or Cloudflare challenge-solving was needed for any of the above — confirmed by hitting each endpoint directly outside the browser.

Caveats / things that could break this

  • buildId is tied to the current Next.js deployment. If comizy.io redeploys, old build ids stop working and the script needs to re-fetch a fresh comic page each run (which it does) — this isn't cached across runs, so it self-heals automatically.
  • This was reverse-engineered from a single comic. Other comics are expected to follow the same API shape since it's the same site-wide Next.js app, but haven't all been verified.
  • If comizy.io adds Cloudflare bot-challenge pages or requires auth for some content in the future, this script does not handle solving challenges or logging in.
  • The Python version sends Accept-Encoding: gzip, deflate and manually decompresses the response. Cloudflare stalls (hangs, never times out on its own) requests from urllib's default client when that header is absent — fetch in Node sends it automatically, but urllib doesn't, so it's set explicitly and the body is gunzipped by hand.
  • The Range retry fallback depends on the CDN honouring range requests, which it currently does (Accept-Ranges: bytes). If that changed, networks that stall on large responses would have no workaround — the scripts report the failure rather than hanging, but can't get around it.

Security

  • No credentials required or stored. All endpoints used here are public; the scripts never send or read cookies, tokens, or login state.
  • No dynamic code execution. Nothing is eval'd and no shell commands are spawned — all network responses (HTML/JSON) are parsed with a regex extraction + standard JSON parsing, never executed.
  • TLS verification is on in both fetch (Node) and urlopen (Python) and is never disabled.
  • Filenames are constrained. Every file written to disk is named from a zero-padded numeric index plus a sanitized chapter title (path separators and other filesystem-unsafe characters are stripped) — chapter/image data from the API cannot be used to write outside the output directory.
  • Trust boundary: you are pointing this tool at a URL and letting it fetch whatever images that site's API returns for it. That's inherent to what a downloader does, not a bug — but as with any tool that fetches remote content, only point it at sites you trust, and treat the output/ directory contents as content from that site, not vetted by this tool.
  • If you find an actual security issue in this code (not "the site could serve something malicious" — that's the trust boundary above, but a genuine bug in the scripts themselves), please open an issue.

This project is an unofficial client for comizy.io's public web API — it is not affiliated with, endorsed by, or sponsored by comizy.io. It automates the same requests a browser makes when reading a comic on the site; it does not bypass paywalls, DRM, or access controls.

You are responsible for how you use this tool. Respect comizy.io's Terms of Service and applicable copyright law in your jurisdiction — this includes not redistributing downloaded content you don't have the rights to, and downloading only for personal, offline use where that is legally permitted. The authors of this repository assume no liability for misuse.

Contributing

Issues and PRs are welcome. If comizy.io changes its site (new build system, different API shape, Cloudflare challenges, etc.) and one of the caveats above starts happening in practice, a bug report with the failing URL and error output is the most useful thing you can attach.