jmcomic · diff

git:20260919.169cc1d to git:20260919.d66ad4d

89 added, 116 removed. Audit B to B.

---
name: jmcomic
description: Use jmcomic (JMComic-Crawler-Python) to query JMComic metadata (album/photo details, search, rankings, comments) and to download albums or chapters, optionally exporting them to PDF, ZIP, or a long image. All commands return a single JSON object from one stable CLI entry point.
whenToUse: Use when the user supplies a JMComic album/photo id ("车号", JM123, or an 18comic album URL) and wants its metadata, wants to search/browse/rank JMComic works, wants comments, or wants to download/export an album or chapter. Do not use when the user is only asking about the upstream library's source code.
metadata:
upstream: https://github.com/hect0x7/JMComic-Crawler-Python
cli: scripts/jmctl.py
---
# JMComic via `jmctl.py`
- This skill drives **JMComic-Crawler-Python** (`jmcomic`) through one wrapper script that
- converts the library into a stable JSON contract. The same engine (`scripts/jmcore.py`)
- also backs a graphical app shipped for Windows / Linux / macOS / Android, so anything you
- can do here the user can also do by clicking.
+ Drive **JMComic-Crawler-Python** (`jmcomic`) through one wrapper that turns the library
+ into a stable JSON contract. The same engine (`scripts/jmcore.py`) backs the graphical
+ builds for Windows / Linux / macOS / Android, so anything done here the user can also do
+ by clicking.
## The one rule
**Call `scripts/jmctl.py` for everything. Never import `jmcomic` directly.**
- The wrapper exists because `jmcomic`'s own CLI prints human-oriented text and the library
- **logs progress to stdout**, which corrupts machine parsing. `jmctl.py` reroutes all
- library logging to stderr and writes exactly one JSON object to stdout.
+ The library logs progress to **stdout**, which corrupts machine parsing; `jmctl.py`
+ reroutes that logging to stderr and writes exactly one JSON object to stdout.
```bash
python <skill-dir>/scripts/jmctl.py <command> [options]
```
- Resolve `<skill-dir>` to this skill's own directory. Every command prints one JSON object
- to stdout: `{"status": "ok", ...}` or `{"status": "error", "error": "...", "hint": "..."}`.
- Exit code is `0` on success and `1` on a handled failure. Library progress stays on stderr.
-
- Because stderr carries the live progress log, run long downloads as a background job
- rather than blocking, and read stdout only when the job settles.
+ Success is `{"status": "ok", ...}`, handled failure is
+ `{"status": "error", "error": "...", "hint": "..."}` with exit code `1`. Run long
+ downloads as a background job and read stdout only when it settles — stderr is the live
+ progress log.
- ## Step 0 — check the environment before anything else
+ ## Step 0 — environment
```bash
python <skill-dir>/scripts/jmctl.py doctor
```
- This reports the interpreter, the installed `jmcomic` version, which optional export
- dependencies are present, and whether the site is reachable. If `jmcomic` is missing:
+ Reports the interpreter, the `jmcomic` version, optional export dependencies, and site
+ reachability. To install, use **the interpreter `doctor` prints** (several Pythons often
+ coexist, and installing into the wrong one looks like "installed but still not
+ importable"):
```bash
- python -m pip install jmcomic
+ "<that path>" -m pip install jmcomic
```
- Install into **the interpreter that runs the script**; `doctor` prints its exact path, so
- prefer `"<that path>" -m pip install jmcomic` when several Pythons exist. Exports need
- extra packages — install only what the requested format needs:
+ Install only the export extras the task needs:
| Format | Package |
|---|---|
| PDF | `Pillow` |
| ZIP | `pyzipper` (or `py7zr` for 7z) |
| long image | `Pillow` |
- `pip install "jmcomic[plugins]"` installs the whole set. A missing dependency makes the
- download succeed but the export silently absent, so check `exportFiles` in the result.
+ A missing dependency makes the download succeed while the export is silently absent, so
+ always check `exportFiles` in the result.
If `doctor` reports `networkOk: false`, do not retry blindly: switch `--client-impl`
- between `api` (default) and `html`, or configure a proxy in an `option.yml`
- (see [Configuration](#configuration)). Never guess a replacement domain — let the
- library resolve one.
+ between `api` (default) and `html`, or set a proxy in `option.yml`. Never guess a
+ replacement domain — the library resolves one itself.
## Commands
### `info` — metadata, no download
```bash
jmctl.py info 438696
jmctl.py info JM438696 https://18comic.vip/album/438696 --pretty
jmctl.py info 438696 --kind photo --image-urls
```
- Accepts bare numbers, `JM123` strings, or JM URLs. `--kind album` (default) returns
- `title`, `authors`, `tags`, `works`, `actors`, `pageCount`, `pubDate`, `updateDate`,
- `likes`, `views`, `commentCount`, and an `episodes` list. `--kind photo` describes one
- chapter; add `--image-urls` to also list every page URL.
-
- An empty `episodes` list is normal for a single-chapter album — the album is then its own
- chapter, and its id doubles as the photo id.
+ Accepts bare numbers, `JM123`, or URLs. `--kind album` (default) returns `title`,
+ `authors`, `tags`, `works`, `actors`, `pageCount`, `pubDate`, `updateDate`, `likes`,
+ `views`, `commentCount`, and `episodes`; `--kind photo` describes one chapter, and
+ `--image-urls` adds every page URL. An empty `episodes` is normal for a single-chapter
+ album — the album is then its own chapter and shares its id.
### `search` — find albums
```bash
jmctl.py search "MANA"
jmctl.py search "無修正" --mode tag --page 1
jmctl.py search "原神" --mode work
jmctl.py search "神里绫华" --mode actor
```
- `--mode`: `site` (default, the site's own search), `tag`, `work`, `actor`.
- Filters: `--page`, `--order-by` (`mr` latest, `mv` views, `mp` pictures, `tf` likes,
- `tr` score, `md` comments), `--time` (`a` all, `t` today, `w` week, `m` month),
- `--category` (`0` all, `doujin`, `single`, `short`, `another`, `hanman`, `meiman`,
- `doujin_cosplay`, `3D`, `english_site`), `--sub-category` (html impl only, e.g.
- `chinese`, `japanese`, `CG`, `cosplay`, `youth`, `3d`).
-
- `+tag` requires a tag and `-tag` excludes it: `"+全彩 +人妻"`.
-
- Returns `total`, `pageCount`, `page`, and `works[]` of `{id, title, tags}`. Feed an
- `id` from here straight into `info` or `download`.
+ `--mode`: `site` (default), `tag`, `work`, `actor`. Filters: `--page`, `--order-by`
+ (`mr` latest, `mv` views, `mp` pictures, `tf` likes, `tr` score, `md` comments), `--time`
+ (`a` all, `t` today, `w` week, `m` month), `--category` (`0` all, `doujin`, `single`,
+ `short`, `another`, `hanman`, `meiman`, `doujin_cosplay`, `3D`, `english_site`),
+ `--sub-category` (html impl only, e.g. `chinese`, `japanese`, `CG`, `cosplay`, `youth`,
+ `3d`). `+tag` requires a tag, `-tag` excludes it: `"+全彩 +人妻"`.
- > `total` is the site's reported result count. When it disagrees with
- > `count × pageCount`, trust `works[]` — the site's counter is approximate.
+ Returns `total`, `pageCount`, `page`, and `works[]` of `{id, title, tags}`; feed an `id`
+ straight into `info` or `download`. When `total` disagrees with `count × pageCount`,
+ trust `works[]`.
### `rank` — rankings and category browsing
```bash
jmctl.py rank --period day
jmctl.py rank --period week --category doujin
jmctl.py rank --period custom --time m --order-by tf --category hanman
```
- `--period` is `day`, `week`, `month`, or `custom` (which uses `--time` and `--order-by`
- to build an arbitrary category query). Same result shape as `search`.
+ `--period` is `day`, `week`, `month`, or `custom` (arbitrary query from `--time` +
+ `--order-by`). Same result shape as `search`.
### `comments`
```bash
jmctl.py comments --id 438696
jmctl.py comments --forum --page 1
```
Returns `comments[]` with `content`, `nickname`, `likes`, `isSpoiler`, `createdAt`, and
- nested `replies[]`. `--forum` reads site-wide comments; each then carries its `albumId`.
+ nested `replies[]`. `--forum` reads site-wide comments, each carrying its `albumId`.
### `download` — fetch and optionally export
```bash
jmctl.py download 438696
jmctl.py download 438696 --export pdf
jmctl.py download 438696 123456 --export pdf zip
jmctl.py download 438696 --kind photo
```
- `--kind album` (default) downloads every chapter; `--kind photo` downloads one chapter.
- `--export` accepts `pdf`, `zip`, `png` (long image) and may combine them. Multiple ids
- are downloaded concurrently and reported individually; one failure does not abort the
- rest.
+ `--kind album` (default) downloads every chapter, `--kind photo` one chapter. `--export`
+ takes `pdf`, `zip`, `png` (long image) and may combine them. Multiple ids download
+ concurrently and are reported individually — one failure does not abort the rest.
- Result for a single id: `id`, `title`, `savePath`, `durationSec`, `imageCount`,
- `imageFiles[]`, plus `exportFiles[]` when `--export` was used. Multiple ids return
+ Single id returns `id`, `title`, `savePath`, `durationSec`, `imageCount`, `imageFiles[]`,
+ plus `exportFiles[]` with `--export`. Multiple ids return
`{total, succeeded, allSucceeded, results[], failed{}}`.
- Downloaded files are cached on disk, so re-running the same command is cheap and is the
- correct response to a partial failure — completed images are reused. Incremental
- "only new chapters" behaviour needs the upstream `find_update` plugin and is out of scope
- here.
+ Images are cached on disk, so re-running the same command is cheap and is the correct
+ response to a partial failure. Incremental "only new chapters" needs the upstream
+ `find_update` plugin and is out of scope.
- ### `config` — inspect the configuration baseline
+ ### `config` — configuration baseline
```bash
jmctl.py config # print the effective default option.yml
- jmctl.py config --write ./option.yml # write a starter file you can then edit
+ jmctl.py config --write ./option.yml # write a starter file
```
### `doctor`
```bash
jmctl.py doctor
- jmctl.py doctor --no-network # interpreter/deps only, no network probe
+ jmctl.py doctor --no-network # interpreter and deps only
```
## Configuration
- Three layers, most specific first:
-
- 1. **CLI flags** — `--client-impl`, and everything under `download`.
- 2. **`option.yml`** — passed with `--option <path>` or `$JM_OPTION_PATH`. This is where
- proxy, cookies, download directory, threading, and image conversion belong.
- 3. **jmcomic defaults.**
-
- Generate a baseline with `jmctl.py config --write ./option.yml`, then keep only the keys
- you change. The keys that matter in practice:
+ Three layers, most specific first: **CLI flags** (`--client-impl`, everything under
+ `download`) → **`option.yml`** (`--option <path>` or `$JM_OPTION_PATH`) → **jmcomic
+ defaults**. Generate a baseline with `jmctl.py config --write ./option.yml`, or read
+ `assets/option.example.yml` (ships with this skill, fully commented). The essentials:
```yaml
client:
- impl: api # api (default, mobile) or html (web, richer search filters)
- retry_times: 5
+ impl: api # api (default) | html; html is the only one with --sub-category
postman:
meta_data:
- proxies: system # or null, or "127.0.0.1:7890", or {http: ..., https: ...}
- cookies:
- AVS: <value> # only needed for login-gated albums; must match the domain used
-
+ proxies: system # or null, or "127.0.0.1:7890"
+ cookies: {AVS: <value>} # only for login-gated albums, domain-scoped
download:
- cache: true # reuse images already on disk
- image:
- decode: true # JM images are scrambled; keep true unless you want raw files
- suffix: null # e.g. .jpg to also convert format
- threading:
- image: 30 # concurrent images (site allows <= 50)
- photo: 16 # concurrent chapters
-
+ cache: true # reuse images already on disk
+ image: {decode: true, suffix: null} # decode restores JM's scrambled images
+ threading: {image: 30, photo: 16} # site allows <= 50 images
dir_rule:
- base_dir: ./downloads # where albums are written
- rule: Bd / Aid / Ptitle # path template; Bd=base_dir, A*=album field, P*=photo field
+ base_dir: ./downloads
+ rule: Bd / Aid / Ptitle # Bd=base_dir, A*=album field, P*=chapter field
```
- `dir_rule.rule` composes from album (`A`) and chapter (`P`) fields, e.g.
- `Bd / Aauthor / (JM{Aid}-{Pindex})-{Pname}`. Which fields exist is discoverable at runtime
- with `jmctl.py info <id> --pretty`.
+ `dir_rule.rule` composes album (`A`) and chapter (`P`) fields, e.g.
+ `Bd / Aauthor / (JM{Aid}-{Pindex})-{Pname}`; available fields are visible in
+ `jmctl.py info <id> --pretty`.
- Cookies are domain-scoped: a cookie from one JM domain has no effect on another. Pass
- credentials only via `option.yml` or `$JM_OPTION_PATH`; never put them on the command line,
- and never echo them back into the conversation.
+ Cookies are domain-scoped: a cookie from one JM domain does nothing on another. Pass
+ credentials only via `option.yml` or `$JM_OPTION_PATH`, never on the command line, and
+ never echo them back into the conversation.
- Login-gated features (favourites, check-in, favourite export) are deliberately **not**
- wrapped by this skill. When a user needs them, use the library's `login` plugin through a
- custom `option.yml` rather than extending this CLI ad hoc.
+ Login-gated features (favourites, check-in, favourite export) are deliberately not
+ wrapped. When needed, use the library's `login` plugin through a custom `option.yml`
+ instead of extending this CLI.
- ## Reporting results to the user
+ ## Reporting results
- - Quote the `title`, the JM id, and the absolute `savePath` so the user can open the files.
- - `likes` and `views` are strings the site formats (e.g. `"77K"`), not integers.
- - `updateDate` of `"0"` means the site reported no update date; report it as unknown.
- - To show a cover, use `https://18comic.vip/album/<id>/` rather than inventing a CDN URL.
- - Downloading is subject to the site's own rate limits. Do not raise `threading.image` above
- 50, do not fan out many albums at once, and never loop `download` over a large id list —
- pass the ids to a single `download` invocation so one option and one client are reused.
- - If the user would rather click than read command output, point them at the local web UI:
- `python webui/server.py` (it serves `http://127.0.0.1:<port>/?token=...` and opens the
- browser), or the packaged `dist/jmcomic-downloader(.exe)` / Android APK. All of them are
- front ends over this same engine, so behavior and configuration are identical.
+ - Quote `title`, the JM id, and the absolute `savePath` so the user can open the files.
+ - `likes` and `views` are site-formatted strings (e.g. `"77K"`), not integers.
+ - `updateDate: "0"` means unknown.
+ - For a cover, use `https://18comic.vip/album/<id>/`; do not invent a CDN URL.
+ - Respect rate limits: keep `threading.image` at or below 50, do not fan out many albums,
+ and never loop `download` over a large id list — pass the ids to one invocation.
+ - If the user would rather click, point them at `python webui/server.py` (serves
+ `http://127.0.0.1:<port>/?token=...` and opens the browser) or the packaged desktop /
+ Android builds; they are front ends over this same engine.
## Troubleshooting
| Symptom | Cause and fix |
|---|---|
- | `jmcomic is not importable` | Install into the interpreter that runs the script, using the path `doctor` prints. |
- | `could not parse a JM id` | The id is non-numeric. Extract the digits from the user's text first. |
- | `MissingAlbumPhotoException` | Wrong id, or a login-gated album. Confirm the id before assuming it needs cookies. |
- | `networkOk: false` | Try the other `--client-impl`, or set a proxy in `option.yml`. The library picks working domains itself. |
- | Download ok but `exportFiles` empty | The format's dependency is missing (see Step 0), or the id produced no images. |
- | `partial download failure` | Re-run the identical command; cached images are reused. Check the `failedPhotos`/`failedImages` fields. |
- | JSON parse error from stdout | Something wrote to stdout. Confirm you invoked `jmctl.py` and not `jmcomic`/`jmv`. |
+ | `jmcomic is not importable` | Install into the interpreter `doctor` prints. |
+ | `could not parse a JM id` | Non-numeric id; extract the digits from the user's text first. |
+ | `MissingAlbumPhotoException` | Wrong id, or a login-gated album. Confirm the id before adding cookies. |
+ | `networkOk: false` | Try the other `--client-impl`, or set a proxy in `option.yml`. |
+ | Download ok but `exportFiles` empty | The format's dependency is missing, or the id produced no images. |
+ | `partial download failure` | Re-run the identical command; cached images are reused. Check `failedPhotos`/`failedImages`. |
+ | JSON parse error from stdout | Something wrote to stdout; confirm `jmctl.py` was invoked, not `jmcomic`/`jmv`. |
## Security and privacy
This skill downloads adult content. Keep it to what the user asked for, do not
- auto-download from search results without confirmation, and do not write credentials or
- cookies into anything you report back.
+ auto-download from search results without confirmation, and never write credentials or
+ cookies into anything reported back.