jmcomic
Search, browse, and download manga from JMComic (18comic). Use for manga discovery, ranking, downloads, and configuration management.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install hect0x7-jmcomic-ai-jmcomic
Repository
Skill path: src/jmcomic_ai/skills/jmcomic
Search, browse, and download manga from JMComic (18comic). Use for manga discovery, ranking, downloads, and configuration management.
Open repositoryBest for
Primary workflow: Research & Ops.
Technical facets: Full Stack.
Target audience: everyone.
License: MIT.
Original source
Catalog source: SkillHub Club.
Repository owner: hect0x7.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install jmcomic into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/hect0x7/jmcomic-ai before adding jmcomic to shared team environments
- Use jmcomic for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: jmcomic
description: Search, browse, and download manga from JMComic (18comic). Use for manga discovery, ranking, downloads, and configuration management.
license: MIT
metadata:
version: "0.0.9"
dependencies: python>=3.10
---
# JMComic Skill
This skill enables you to interact with JMComic (18comic), a popular manga platform, to search, browse, and download manga content.
## When to Use This Skill
Activate this skill when the user wants to:
- Search for manga by keyword or category
- Browse popular manga rankings (daily, weekly, monthly)
- Download entire albums or specific chapters (**Returns structured dict with status, paths, and metadata**)
- Get detailed information about a manga album
- Configure download settings (paths, concurrency, proxies)
- **NEW**: Post-process downloaded content (Zip, PDF, LongImage) with **native parameters or `dir_rule`**
### ๐ฅ Download Tools Return Structured Data
Both `download_album` and `download_photo` now return structured dictionaries:
**`download_album(album_id: str, ctx: Context = None)`** returns:
```python
{
"status": "success" | "failed",
"album_id": str,
"title": str,
"download_path": str, # Absolute path to download directory
"error": str | None
}
```
**`download_photo(photo_id: str, ctx: Context = None)`** returns:
```python
{
"status": "success" | "failed",
"photo_id": str,
"image_count": int,
"download_path": str, # Absolute path to download directory
"error": str | None
}
```
**Real-time Progress Tracking**: Both methods accept an optional `ctx: Context` parameter (automatically injected by FastMCP). When provided, progress updates are sent via MCP notifications in real-time, allowing AI agents to monitor download progress.
## Core Capabilities
### ๐ ๏ธ Post-Processing (New in 0.0.6)
This skill supports advanced post-processing of downloaded manga. It returns structured data including the **output path** of the generated file.
- **๐ฆ Zip Compression**: Pack an entire album or individual chapters into a ZIP file.
- **๐ PDF Conversion**: Merge all images of an album into a single PDF document.
- **๐ผ๏ธ Long Image Merging**: Combine all pages of a chapter into one continuous long image.
**`post_process(album_id: str, process_type: str, params: dict = None)`** returns:
```python
{
"status": "success" | "error",
"process_type": str, # Process type used
"album_id": str, # Album ID processed
"output_path": str, # Absolute path to generated file/directory (empty string on error)
"is_directory": bool, # True if output is a directory (e.g., photo-level zip), False on error
"message": str # Success/error message
}
```
**All fields are always present**. On error, `output_path` will be an empty string and `is_directory` will be `False`.
**Output Control**: Use `dir_rule` for custom output paths. If omitted, files are saved in the configured default directory.
### ๐งฉ Post-Process `dir_rule` Examples
The `dir_rule` parameter takes a dictionary: `{"rule": "DSL_STRING", "base_dir": "BASE_PATH"}`.
- **`Bd`**: Refers to `base_dir`.
- **`Axxx`**: Album attributes (e.g., `Aid`, `Atitle`, `Aauthor`).
- **`Pxxx`**: Photo/Chapter attributes (e.g., `Pid`, `Ptitle`, `Pindex`).
- **`{attr}`**: Python string format support for any metadata attribute.
#### 1. ZIP Compression (`process_type="zip"`)
* **Album Level (Single ZIP for entire manga)**:
`{"level": "album", "dir_rule": {"rule": "Bd/{Atitle}.zip", "base_dir": "D:/Comics/Archives"}}`
* **Photo Level (Individual ZIP for each chapter)**:
`{"level": "photo", "dir_rule": {"rule": "Bd/{Atitle}/{Pindex}.zip", "base_dir": "D:/Comics/Exports"}}`
#### 2. PDF Conversion (`process_type="img2pdf"`)
* **Album Level (One PDF for all chapters combined)**:
`{"level": "album", "dir_rule": {"rule": "Bd/{Aauthor}-{Atitle}.pdf", "base_dir": "D:/Comics/PDFs"}}`
* **Photo Level (One PDF per chapter)**:
`{"level": "photo", "dir_rule": {"rule": "Bd/{Atitle}/{Pindex}.pdf", "base_dir": "D:/Comics/Chapters"}}`
#### 3. Long Image Merging (`process_type="long_img"`)
* **Album Level (All pages combined into one huge image)**:
`{"level": "album", "dir_rule": {"rule": "Bd/{Atitle}_Full.png", "base_dir": "D:/Comics/Long"}}`
* **Photo Level (One long image per chapter)**:
`{"level": "photo", "dir_rule": {"rule": "Bd/{Atitle}/{Pindex}.png", "base_dir": "D:/Comics/Long"}}`
> โ ๏ธ **Best Practice - Avoiding Overwrites**:
> When processing multiple different albums (e.g., in a loop) into the same `base_dir`, ALWAYS include unique identifiers like `{Aid}` or `{Atitle}` in your `rule`. Using a static rule like `"Bd/output.pdf"` will cause subsequent albums to overwrite previous ones.
**Workflow Suggestion**: Use `download_album` first to ensure source images exist, then call `post_process`. The tool returns the **actual predicted path** of the result.
This skill provides command-line utilities for JMComic operations. All tools are Python scripts located in the `scripts/` directory and should be executed using Python.
### Data Structure Notes
Most search and browsing tools (e.g., `search_album`, `browse_albums`) return a consistent structure that supports pagination:
```json
{
"albums": [ ... ],
"total_count": 1234
}
```
**`total_count`** provides the total number of items available across all pages, allowing you to calculate the number of remaining pages and decide if further searching is needed.
#### Important: Browse Albums Data Limitations
**`browse_albums`** is the unified tool for browsing albums by category, time range, and sorting criteria. It combines the functionality of ranking and category browsing into a single, flexible interface.
The response **does NOT include detailed statistical data** (likes, views, author, etc.). Each album in the list contains only:
- `id`: Album ID
- `title`: Album title
- `tags`: Tag list
- `cover_url`: Cover image URL
To get detailed information including likes/views/author for albums, you must call **`get_album_detail(album_id)`** for each album individually.
**Supported Sorting Options (`order_by`):**
- `latest`: Latest updates (default)
- `likes`: Most liked
- `views`: Most viewed
- `pictures`: Most pictures
- `score`: Highest rated
- `comments`: Most comments
**Common Use Cases:**
1. **Rankings (day/week/month)**: Set `time_range` + `order_by`
```python
# Monthly top liked albums
browse_albums(time_range="month", order_by="likes")
# Weekly most viewed albums
browse_albums(time_range="week", order_by="views")
# Today's trending albums
browse_albums(time_range="today", order_by="views")
```
2. **Category Browsing**: Set `category` + `order_by`
```python
# Browse doujin manga (latest)
browse_albums(category="doujin", order_by="latest")
# Browse Korean comics (latest)
browse_albums(category="hanman", order_by="latest")
```
3. **Combined Queries**: Set `category` + `time_range` + `order_by`
```python
# This month's hottest doujin manga
browse_albums(category="doujin", time_range="month", order_by="views")
# This week's most liked Korean comics
browse_albums(category="hanman", time_range="week", order_by="likes")
```
**Example workflow for getting top 10 albums with details:**
```python
# 1. Get ranking list (sorted by likes, but no likes data in response)
ranking = browse_albums(time_range="month", order_by="likes", page=1)
# 2. Get detailed info for top 10
for album in ranking["albums"][:10]:
detail = get_album_detail(album["id"])
# Now you have: detail["likes"], detail["views"], detail["author"], etc.
```
## Configuration Reference
For detailed configuration options, refer to:
- **`references/reference.md`**: Human-readable configuration guide
- **`assets/option_schema.json`**: JSON Schema for validation
Common configuration examples:
```yaml
# Change download directory
dir_rule:
base_dir: "/path/to/downloads"
rule: "Bd / Ptitle"
# Adjust concurrency
download:
threading:
image: 30 # Max concurrent image downloads
photo: 5 # Max concurrent chapter downloads
# Set proxy
client:
postman:
meta_data:
proxies:
http: "http://proxy.example.com:8080"
https: "https://proxy.example.com:8080"
# Or use system proxy
client:
postman:
meta_data:
proxies: system
# Configure login cookies
client:
postman:
meta_data:
cookies:
AVS: "your_avs_cookie_value"
# Use plugins
plugins:
after_album:
- plugin: zip
kwargs:
level: photo
suffix: zip
delete_original_file: true
```
## Available Command-Line Tools
The `scripts/` directory provides utility tools for common tasks. All tools support the `--help` flag for detailed usage information.
### ๐ฅ `doctor.py` - Environment Diagnostics
Comprehensive diagnostic tool that checks your entire setup:
```bash
python scripts/doctor.py
```
**What it checks**:
- โ
Python version compatibility
- โ
Required dependencies (jmcomic, jmcomic_ai)
- โ
Configuration file status
- โ
Network connectivity (discovers and tests available JMComic domains)
**Use this when**:
- Setting up the skill for the first time
- Troubleshooting any issues
- Verifying your environment is ready
### ๐ฆ `batch_download.py` - Batch Album Downloads
Download multiple albums from a list of IDs:
```bash
# From command line
python scripts/batch_download.py --ids 123456,789012,345678
# From file (one ID per line)
python scripts/batch_download.py --file album_ids.txt
# With custom config
python scripts/batch_download.py --ids 123456,789012 --option /path/to/option.yml
```
**Features**:
- โ
Download multiple albums in sequence
- โ
Progress tracking with success/failure counts
- โ
Error handling and summary report
### ๐ท `download_photo.py` - Batch Chapter Downloads
Download specific chapters/photos from albums:
```bash
# Download specific chapters
python scripts/download_photo.py --ids 123456,789012,345678
# Download chapters from file
python scripts/download_photo.py --file photo_ids.txt
# With custom config
python scripts/download_photo.py --ids 123456,789012 --option /path/to/option.yml
```
**Features**:
- โ
Download specific chapters without downloading entire albums
- โ
Useful for selective chapter downloads
- โ
Progress tracking and error handling
### โ
`validate_config.py` - Configuration Validation
Validate and convert configuration files:
```bash
# Validate configuration
python scripts/validate_config.py ~/.jmcomic/option.yml
# Convert YAML to JSON
python scripts/validate_config.py option.yml --convert-to-json
# Specify output path
python scripts/validate_config.py option.yml --convert-to-json --output config.json
```
**Features**:
- โ
Validate option.yml syntax and structure
- โ
Display configuration summary (client, download, directory, proxy settings)
- โ
Convert between YAML and JSON formats
### ๐ `search_export.py` - Search and Export
Search albums and export results to CSV or JSON:
```bash
# Search by keyword
python scripts/search_export.py --keyword "ๆ็ดข่ฏ" --output results.csv
# Get daily ranking
python scripts/search_export.py --ranking day --output ranking.json
# Browse category
python scripts/search_export.py --category doujin --output doujin.csv --max-pages 3
```
**Features**:
- โ
Search by keyword, ranking, or category
- โ
Multi-page support with `--max-pages`
- โ
Export to CSV or JSON format
- โ
Useful for building album catalogs and collections
### ๐ `album_info.py` - Album Information Query
Fetch detailed information for one or multiple albums:
```bash
# Single album (print to console)
python scripts/album_info.py --id 123456
# Multiple albums (export to JSON)
python scripts/album_info.py --ids 123456,789012,345678 --output details.json
# From file
python scripts/album_info.py --file album_ids.txt --output album_details.json --verbose
```
**Features**:
- โ
Query single or multiple albums
- โ
Display detailed metadata (title, author, likes, views, chapters, tags, description)
- โ
Export to JSON or print formatted summary to console
- โ
Error tracking for failed queries
### ๐ผ๏ธ `download_covers.py` - Batch Cover Downloads
Download cover images for multiple albums:
```bash
# Download covers for specific albums
python scripts/download_covers.py --ids 123456,789012,345678
# Download covers from file
python scripts/download_covers.py --file album_ids.txt --output ./my_covers
```
**Features**:
- โ
Batch download album covers
- โ
Custom output directory
- โ
Fast preview without downloading full albums
- โ
Useful for creating cover galleries
### ๐ `ranking_tracker.py` - Ranking Tracker
Track and export ranking changes over time:
```bash
# Get current daily ranking
python scripts/ranking_tracker.py --period day --output daily_ranking.json
# Get multiple pages of weekly ranking
python scripts/ranking_tracker.py --period week --max-pages 3 --output weekly_top.csv
# Track all periods (day, week, month)
python scripts/ranking_tracker.py --all --output rankings/
# Add timestamp to filename
python scripts/ranking_tracker.py --period day --output ranking.json --add-timestamp
```
**Features**:
- โ
Track daily, weekly, or monthly rankings
- โ
Multi-page support
- โ
Export to CSV or JSON with timestamps
- โ
Track all periods at once with `--all`
- โ
Useful for trend analysis and discovering popular content
### ๐ ๏ธ `post_process.py` - Post-Processing (Zip, PDF, LongImg)
Transform downloaded images into ZIP, PDF, or Long Images:
```bash
# Convert album to PDF
python scripts/post_process.py --id 123456 --type img2pdf
# Pack album into encrypted ZIP and delete original images
python scripts/post_process.py --id 123456 --type zip --password "my_secret" --delete
# Merge images into a long scroll image
python scripts/post_process.py --id 123456 --type long_img --outdir ./long_images
# Use native dir_rule DSL (recommended for precise output paths)
python scripts/post_process.py --id 123456 --type zip --dir-rule "Bd/{Atitle}/{Pindex}.zip" --base-dir "D:/Comics/Exports"
```
**Features**:
- โ
Supports ZIP, PDF, and Long Image formats
- โ
Option to encrypt output (Zip/PDF)
- โ
Automatic cleanup of original files
- โ
Custom output directories
## Script Parameters โ MCP Tools Mapping
The following table clarifies how script CLI parameters map to MCP tools.
| Script | Target Tool | Mapping Level | Notes |
| :--- | :--- | :--- | :--- |
| `search_export.py` | `search_album` / `browse_albums` | Partial | `--keyword` maps to `search_album`; `--ranking` / `--category` maps to `browse_albums`. Ranking is a convenience mode based on `time_range` + configurable sort, not a separate backend API. |
| `post_process.py` | `post_process` | High | `--id`โ`album_id`, `--type`โ`process_type`, optional flags to `params`. `--dir-rule` + `--base-dir` map to `params.dir_rule`. |
| `album_info.py` | `get_album_detail` | Partial | Batch wrapper over repeated single-album calls; output format is script-defined. |
| `download_covers.py` | `download_cover` | Partial | Batch wrapper over repeated cover calls. |
| `ranking_tracker.py` | `browse_albums` | Partial | Uses time-range/category browse semantics and exports snapshots. |
| `batch_download.py` | (non-MCP direct download flow) | None | Designed for installable skill runtime; does not guarantee MCP tool return shape/progress contract. |
| `download_photo.py` | `download_photo` (conceptual) | Partial | Script behavior focuses on batch orchestration and CLI outputs. |
| `validate_config.py` | `update_option` (adjacent) | None | Validation/format conversion utility; not a direct MCP tool wrapper. |
### Mapping Policy
- **MCP tools are the source of truth** for agent-facing contracts (name, args, return structure).
- **Scripts are operational helpers** for local/installed skill workflows and may expose different output formatting.
- If strict schema guarantees are required, prefer calling MCP tools directly instead of scripts.
## Important Notes
- **Legal Compliance**: Ensure you have the right to download content
- **Rate Limiting**: The platform may rate-limit requests; adjust threading if needed
- **Storage**: Downloads can be large; ensure sufficient disk space
- **Configuration**: Default config is at `~/.jmcomic/option.yml`
## Troubleshooting
- **Connection errors**: Try updating the domain list in client config
- **Slow downloads**: Reduce threading concurrency
- **Scrambled images**: Ensure `download.image.decode` is set to `true`
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### scripts/doctor.py
```python
#!/usr/bin/env python3
"""
Diagnostic tool for the JMComic Skill.
Checks environment, dependencies, and network connectivity.
Usage:
python scripts/doctor.py
"""
import sys
import socket
from pathlib import Path
def check_python_version():
print(f"๐ Python version: {sys.version.split()[0]}")
if sys.version_info < (3, 10):
print("โ ๏ธ Warning: Python 3.10+ is recommended.")
def check_dependencies():
print("๐ฆ Checking dependencies...")
try:
import jmcomic
print(f"โ
jmcomic version: {jmcomic.__version__}")
except ImportError:
print("โ Error: jmcomic library not found.")
try:
from jmcomic_ai.core import JmcomicService
print("โ
jmcomic_ai core is accessible.")
except ImportError:
print("โ Error: jmcomic_ai core not found.")
def check_network():
"""
ๆฃๆฅ็ฝ็ป่ฟๆฅๆง๏ผๆต่ฏๅฝๅIPๅฏไปฅ่ฎฟ้ฎๅชไบ็ฆๆผซๅๅ
ๅฎๅ
จๆ็
ง reference/assets/docs/sources/tutorial/8_pick_domain.md ๅฎ็ฐ
"""
print("๐ Checking network connectivity (Dynamic Domain Discovery)...")
try:
from jmcomic import JmOption, JmcomicText, multi_thread_launcher, disable_jm_log
except ImportError:
print("โ Error: Missing jmcomic dependencies.")
return
# ็ฆ็จ jmcomic ็ๅไฝๆฅๅฟ่พๅบ
disable_jm_log()
option = JmOption.default()
# meta_data ๅฏ็จไบ้
็ฝฎไปฃ็็ญ
meta_data = {
# 'proxies': ProxyBuilder.clash_proxy()
}
def get_all_domain():
"""่ทๅๆๆๅฏ็จๅๅ"""
template = 'https://jmcmomic.github.io/go/{}.html'
url_ls = [template.format(i) for i in range(300, 309)]
domain_set = set()
def fetch_domain(url):
try:
# ไผๅ
ไฝฟ็จ curl_cffi.requests๏ผๅฆๆไธๅฏ็จๅๅ้ๅฐ้ป่ฎคๅฎ็ฐ
try:
from curl_cffi import requests as postman
except ImportError:
from jmcomic import JmModuleConfig
postman = JmModuleConfig.get_postman_clz()()
# allow_redirects=False ๅฏนไบ่ฟไบ่ทณ่ฝฌ้กต้ข่ณๅ
ณ้่ฆ
resp = postman.get(url, allow_redirects=False, **meta_data)
text = resp.text
for domain in JmcomicText.analyse_jm_pub_html(text):
if domain.startswith('jm365.work'):
continue
domain_set.add(domain)
except Exception:
pass
multi_thread_launcher(
iter_objs=url_ls,
apply_each_obj_func=fetch_domain,
)
return domain_set
# 1. ่ทๅๆๆๅๅ
print("๐ก Fetching latest domain list from jmcmomic.github.io...")
domain_set = get_all_domain()
if not domain_set:
print("โ Failed to discover any domains. You might need a proxy to access jmcmomic.github.io.")
return
print(f"๐ Discovered {len(domain_set)} domains. Testing business connectivity...")
# 2. ๆต่ฏๆฏไธชๅๅ
domain_status_dict = {}
def test_domain(domain: str):
"""ๆต่ฏๅไธชๅๅ็ๅฏ็จๆง"""
client = option.new_jm_client(impl='html', domain_list=[domain], **meta_data)
status = 'ok'
try:
# ๆต่ฏไธไธชๅทฒ็ฅ็้็จ็ธๅID
client.get_album_detail('123456')
except Exception as e:
status = str(e.args)
domain_status_dict[domain] = status
multi_thread_launcher(
iter_objs=domain_set,
apply_each_obj_func=test_domain,
)
# 3. ่พๅบๆต่ฏ็ปๆ
print("\n" + "="*50)
print("Domain Test Results:")
print("="*50)
ok_domains = []
for domain, status in domain_status_dict.items():
if status == 'ok':
print(f"โ
{domain}: {status}")
ok_domains.append(domain)
else:
# ๆชๆญ่ฟ้ฟ็้่ฏฏไฟกๆฏ
error_msg = status[:60] + "..." if len(status) > 60 else status
print(f"โ {domain}: {error_msg}")
# 4. ่พๅบๆป็ป
print("="*50)
if ok_domains:
print(f"โจ Network summary: {len(ok_domains)}/{len(domain_set)} domains are working.")
print(f"๐ก Recommended domain for config: {ok_domains[0]}")
else:
print("โ All discovered domains failed. You likely need to configure a proxy.")
def check_config():
print("โ๏ธ Checking configuration...")
config_path = Path.home() / ".jmcomic" / "option.yml"
if config_path.exists():
print(f"โ
Config found at: {config_path}")
else:
print(f"โน๏ธ Config not found at default location (~/.jmcomic/option.yml). Using built-in defaults.")
def main():
print("๐ฅ JMComic Skill Doctor - Diagnostic Report\n" + "="*45)
check_python_version()
print("-" * 20)
check_dependencies()
print("-" * 20)
check_config()
print("-" * 20)
check_network()
print("="*45 + "\nโจ Diagnostic complete.")
if __name__ == "__main__":
main()
```
### scripts/batch_download.py
```python
#!/usr/bin/env python3
"""
Batch download tool for JMComic albums.
Downloads multiple albums from a list of IDs.
Usage:
python scripts/batch_download.py --ids 123456,789012,345678
python scripts/batch_download.py --file album_ids.txt
"""
import argparse
import sys
from pathlib import Path
try:
from jmcomic_ai.core import JmcomicService
except ImportError:
print("โ Error: jmcomic_ai not found. Please ensure the package is installed.")
sys.exit(1)
def parse_args():
parser = argparse.ArgumentParser(description="Batch download JMComic albums")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--ids",
type=str,
help="Comma-separated album IDs (e.g., 123456,789012,345678)"
)
group.add_argument(
"--file",
type=str,
help="Path to file containing album IDs (one per line)"
)
parser.add_argument(
"--option",
type=str,
help="Path to option.yml file (default: ~/.jmcomic/option.yml)"
)
return parser.parse_args()
def load_album_ids(args) -> list[str]:
"""Load album IDs from command line or file"""
if args.ids:
return [aid.strip() for aid in args.ids.split(",") if aid.strip()]
if args.file:
file_path = Path(args.file)
if not file_path.exists():
print(f"โ Error: File not found: {file_path}")
sys.exit(1)
with open(file_path, "r", encoding="utf-8") as f:
return [line.strip() for line in f if line.strip() and not line.startswith("#")]
return []
def main():
args = parse_args()
album_ids = load_album_ids(args)
if not album_ids:
print("โ Error: No album IDs provided")
sys.exit(1)
print(f"๐ฆ Batch Download Tool")
print(f"{'='*50}")
print(f"Total albums to download: {len(album_ids)}")
print(f"{'='*50}\n")
# Initialize service
service = JmcomicService(option_path=args.option)
# Download each album
success_count = 0
failed_ids = []
for i, album_id in enumerate(album_ids, 1):
print(f"[{i}/{len(album_ids)}] Downloading album {album_id}...")
try:
service.option.download_album(album_id)
print(f"โ
Successfully downloaded album {album_id}")
success_count += 1
except Exception as e:
print(f"โ Failed to download album {album_id}: {e}")
failed_ids.append(album_id)
# Summary
print(f"\n{'='*50}")
print(f"๐ Download Summary:")
print(f"โ
Successful: {success_count}/{len(album_ids)}")
print(f"โ Failed: {len(failed_ids)}/{len(album_ids)}")
if failed_ids:
print(f"\nFailed album IDs:")
for aid in failed_ids:
print(f" - {aid}")
print(f"{'='*50}")
if __name__ == "__main__":
main()
```
### scripts/download_photo.py
```python
#!/usr/bin/env python3
"""
Batch photo/chapter download tool.
Download specific chapters from albums.
Usage:
# Download specific chapters
python scripts/download_photo.py --ids 123456,789012,345678
# Download chapters from file
python scripts/download_photo.py --file photo_ids.txt
"""
import argparse
import sys
from pathlib import Path
try:
from jmcomic_ai.core import JmcomicService
except ImportError:
print("โ Error: jmcomic_ai not found. Please ensure the package is installed.")
sys.exit(1)
def parse_args():
parser = argparse.ArgumentParser(description="Batch download JMComic chapters/photos")
# Input mode
input_group = parser.add_mutually_exclusive_group(required=True)
input_group.add_argument("--ids", type=str, help="Comma-separated photo/chapter IDs")
input_group.add_argument("--file", type=str, help="File containing photo IDs (one per line)")
# Options
parser.add_argument("--option", type=str, help="Path to option.yml file")
return parser.parse_args()
def load_photo_ids(args) -> list[str]:
"""Load photo IDs from arguments"""
if args.ids:
return [pid.strip() for pid in args.ids.split(",") if pid.strip()]
if args.file:
file_path = Path(args.file)
if not file_path.exists():
print(f"โ Error: File not found: {file_path}")
sys.exit(1)
with open(file_path, "r", encoding="utf-8") as f:
return [line.strip() for line in f if line.strip() and not line.startswith("#")]
return []
def main():
args = parse_args()
photo_ids = load_photo_ids(args)
if not photo_ids:
print("โ Error: No photo IDs provided")
sys.exit(1)
print(f"๐ท Batch Photo Download Tool")
print(f"{'='*50}")
print(f"Total chapters to download: {len(photo_ids)}")
print(f"{'='*50}\n")
# Initialize service
service = JmcomicService(option_path=args.option)
# Download each photo
success_count = 0
failed_ids = []
for i, photo_id in enumerate(photo_ids, 1):
print(f"[{i}/{len(photo_ids)}] Downloading chapter {photo_id}...")
try:
service.download_photo(photo_id)
print(f"โ
Successfully downloaded chapter {photo_id}")
success_count += 1
except Exception as e:
print(f"โ Failed to download chapter {photo_id}: {e}")
failed_ids.append(photo_id)
# Summary
print(f"\n{'='*50}")
print(f"๐ Download Summary:")
print(f"โ
Successful: {success_count}/{len(photo_ids)}")
print(f"โ Failed: {len(failed_ids)}/{len(photo_ids)}")
if failed_ids:
print(f"\nFailed chapter IDs:")
for pid in failed_ids:
print(f" - {pid}")
print(f"{'='*50}")
if __name__ == "__main__":
main()
```
### scripts/validate_config.py
```python
#!/usr/bin/env python3
"""
Configuration validation and conversion tool.
Validates option.yml files and converts between formats.
Usage:
python scripts/validate_config.py ~/.jmcomic/option.yml
python scripts/validate_config.py --convert-to-json option.yml
"""
import argparse
import json
import sys
from pathlib import Path
try:
from jmcomic import JmOption, create_option_by_file
except ImportError:
print("โ Error: jmcomic not found. Please install: pip install jmcomic")
sys.exit(1)
def parse_args():
parser = argparse.ArgumentParser(description="Validate and convert JMComic configuration")
parser.add_argument(
"config_file",
type=str,
help="Path to option.yml file to validate"
)
parser.add_argument(
"--convert-to-json",
action="store_true",
help="Convert YAML config to JSON format"
)
parser.add_argument(
"--output",
type=str,
help="Output file path for conversion (default: same name with .json extension)"
)
return parser.parse_args()
def validate_config(config_path: Path) -> tuple[bool, JmOption | None, str]:
"""
Validate configuration file
Returns:
(is_valid, option_object, error_message)
"""
if not config_path.exists():
return False, None, f"File not found: {config_path}"
try:
option = create_option_by_file(str(config_path))
return True, option, ""
except Exception as e:
return False, None, str(e)
def convert_to_json(option: JmOption, output_path: Path):
"""Convert JmOption to JSON format"""
try:
option_dict = option.deconstruct()
with open(output_path, "w", encoding="utf-8") as f:
json.dump(option_dict, f, indent=2, ensure_ascii=False)
return True, ""
except Exception as e:
return False, str(e)
def print_config_summary(option: JmOption):
"""Print a summary of the configuration"""
print("\n๐ Configuration Summary:")
print(f"{'='*50}")
# Client settings
print(f"๐ Client:")
print(f" - Implementation: {option.client.get('impl', 'html')}")
if 'domain' in option.client:
print(f" - Domain: {option.client['domain']}")
# Download settings
print(f"\n๐ฅ Download:")
threading = option.download.get('threading', {})
print(f" - Image threads: {threading.get('image', 30)}")
print(f" - Photo threads: {threading.get('photo', 5)}")
# Directory settings
print(f"\n๐ Directory:")
print(f" - Base dir: {option.dir_rule.base_dir}")
print(f" - Rule: {option.dir_rule.get('rule', 'Bd / Ptitle')}")
# Proxy settings
postman_meta = option.client.get('postman', {}).get('meta_data', {})
if 'proxies' in postman_meta:
print(f"\n๐ Proxy:")
proxies = postman_meta['proxies']
if isinstance(proxies, dict):
for key, value in proxies.items():
print(f" - {key}: {value}")
else:
print(f" - {proxies}")
print(f"{'='*50}")
def main():
args = parse_args()
config_path = Path(args.config_file).resolve()
print(f"๐ Validating configuration file: {config_path}")
# Validate
is_valid, option, error_msg = validate_config(config_path)
if not is_valid:
print(f"\nโ Validation Failed:")
print(f" {error_msg}")
sys.exit(1)
print(f"โ
Configuration is valid!")
# Print summary
print_config_summary(option)
# Convert to JSON if requested
if args.convert_to_json:
output_path = Path(args.output) if args.output else config_path.with_suffix('.json')
print(f"\n๐ Converting to JSON: {output_path}")
success, error = convert_to_json(option, output_path)
if success:
print(f"โ
Successfully converted to {output_path}")
else:
print(f"โ Conversion failed: {error}")
sys.exit(1)
if __name__ == "__main__":
main()
```
### scripts/search_export.py
```python
#!/usr/bin/env python3
"""
Search and export tool for JMComic.
Search albums and export results to CSV or JSON format.
Usage:
# Search by keyword
python scripts/search_export.py --keyword "ๆ็ดข่ฏ" --output results.csv
# Get daily ranking
python scripts/search_export.py --ranking day --output ranking.json
# Browse category
python scripts/search_export.py --category doujin --output doujin.csv
"""
import argparse
import csv
import json
import sys
from pathlib import Path
try:
from jmcomic_ai.core import JmcomicService
except ImportError:
print("โ Error: jmcomic_ai not found. Please ensure the package is installed.")
sys.exit(1)
def parse_args():
parser = argparse.ArgumentParser(description="Search and export JMComic albums")
# Search mode selection (mutually exclusive)
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--keyword", type=str, help="Search by keyword")
mode.add_argument("--ranking", type=str, choices=["day", "week", "month"], help="Get ranking")
mode.add_argument("--category", type=str, help="Browse by category (e.g., doujin, hanman, single)")
# Common options
parser.add_argument("--page", type=int, default=1, help="Page number (default: 1)")
parser.add_argument("--max-pages", type=int, default=1, help="Maximum pages to fetch (default: 1)")
parser.add_argument("--output", type=str, required=True, help="Output file path (.csv or .json)")
parser.add_argument("--option", type=str, help="Path to option.yml file")
# Search-specific options
parser.add_argument("--order-by", type=str, default="latest", help="Sort order for search (default: latest)")
parser.add_argument("--sort-by", type=str, default="latest", help="Sort order for category (latest, likes, views, pictures, score, comments)")
return parser.parse_args()
def fetch_results(service: JmcomicService, args) -> list[dict]:
"""Fetch search results based on mode"""
all_results = []
for page in range(args.page, args.page + args.max_pages):
print(f"๐ Fetching page {page}...")
response = {}
if args.keyword:
response = service.search_album(args.keyword, page=page, order_by=args.order_by)
elif args.ranking:
# Ranking mode: e.g. day -> time_range="day", order_by="likes" (assumed ranking impl)
# Or use explicit ranking mapping if available.
# browse_albums supports time_range & order_by
response = service.browse_albums(time_range=args.ranking, order_by="likes", page=page)
elif args.category:
response = service.browse_albums(category=args.category, page=page, order_by=args.sort_by)
else:
response = {"albums": []}
results = response.get("albums", [])
if not results:
print(f"โ ๏ธ No results on page {page}, stopping.")
break
all_results.extend(results)
print(f"โ
Found {len(results)} albums on page {page}")
return all_results
def export_to_csv(results: list[dict], output_path: Path):
"""Export results to CSV format"""
if not results:
print("โ ๏ธ No results to export")
return
with open(output_path, "w", encoding="utf-8-sig", newline="") as f:
# Use first result to determine fields
fieldnames = ["id", "title", "tags", "cover_url"]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for result in results:
# Convert tags list to string
row = result.copy()
row["tags"] = ", ".join(result.get("tags", []))
writer.writerow(row)
print(f"โ
Exported {len(results)} albums to {output_path}")
def export_to_json(results: list[dict], output_path: Path):
"""Export results to JSON format"""
with open(output_path, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"โ
Exported {len(results)} albums to {output_path}")
def main():
args = parse_args()
output_path = Path(args.output)
# Determine export format
if output_path.suffix.lower() not in [".csv", ".json"]:
print("โ Error: Output file must be .csv or .json")
sys.exit(1)
print(f"๐ JMComic Search Export Tool")
print(f"{'='*50}")
# Initialize service
service = JmcomicService(option_path=args.option)
# Fetch results
results = fetch_results(service, args)
if not results:
print("โ No results found")
sys.exit(1)
print(f"\n๐ Total albums found: {len(results)}")
# Export
if output_path.suffix.lower() == ".csv":
export_to_csv(results, output_path)
else:
export_to_json(results, output_path)
print(f"{'='*50}")
if __name__ == "__main__":
main()
```
### scripts/album_info.py
```python
#!/usr/bin/env python3
"""
Album information query tool.
Fetch detailed information for one or multiple albums.
Usage:
# Single album
python scripts/album_info.py --id 123456
# Multiple albums
python scripts/album_info.py --ids 123456,789012,345678
# From file
python scripts/album_info.py --file album_ids.txt --output album_details.json
"""
import argparse
import json
import sys
from pathlib import Path
try:
from jmcomic_ai.core import JmcomicService
except ImportError:
print("โ Error: jmcomic_ai not found. Please ensure the package is installed.")
sys.exit(1)
def parse_args():
parser = argparse.ArgumentParser(description="Query JMComic album details")
# Input mode
input_group = parser.add_mutually_exclusive_group(required=True)
input_group.add_argument("--id", type=str, help="Single album ID")
input_group.add_argument("--ids", type=str, help="Comma-separated album IDs")
input_group.add_argument("--file", type=str, help="File containing album IDs (one per line)")
# Options
parser.add_argument("--output", type=str, help="Output JSON file (default: print to console)")
parser.add_argument("--option", type=str, help="Path to option.yml file")
parser.add_argument("--verbose", action="store_true", help="Show detailed progress")
return parser.parse_args()
def load_album_ids(args) -> list[str]:
"""Load album IDs from arguments"""
if args.id:
return [args.id]
if args.ids:
return [aid.strip() for aid in args.ids.split(",") if aid.strip()]
if args.file:
file_path = Path(args.file)
if not file_path.exists():
print(f"โ Error: File not found: {file_path}")
sys.exit(1)
with open(file_path, "r", encoding="utf-8") as f:
return [line.strip() for line in f if line.strip() and not line.startswith("#")]
return []
def fetch_album_details(service: JmcomicService, album_ids: list[str], verbose: bool = False) -> tuple[list[dict], list[dict]]:
"""Fetch details for multiple albums"""
results = []
failed = []
for i, album_id in enumerate(album_ids, 1):
if verbose:
print(f"[{i}/{len(album_ids)}] Fetching album {album_id}...")
try:
detail = service.get_album_detail(album_id)
results.append(detail)
if verbose:
print(f"โ
{detail['title']}")
except Exception as e:
if verbose:
print(f"โ Failed: {e}")
failed.append({"id": album_id, "error": str(e)})
return results, failed
def print_album_summary(album: dict):
"""Print a single album summary"""
print(f"\n{'='*60}")
print(f"๐ {album['title']}")
print(f"{'='*60}")
print(f"ID: {album['id']}")
print(f"Author: {album['author']}")
print(f"Likes: {album['likes']:,} | Views: {album['views']:,}")
print(f"Chapters: {album['chapter_count']}")
print(f"Updated: {album['update_time']}")
print(f"Tags: {', '.join(album['tags'][:5])}")
if album['description']:
desc = album['description'][:100] + "..." if len(album['description']) > 100 else album['description']
print(f"Description: {desc}")
def main():
args = parse_args()
album_ids = load_album_ids(args)
if not album_ids:
print("โ Error: No album IDs provided")
sys.exit(1)
print(f"๐ Album Information Query Tool")
print(f"{'='*60}")
print(f"Total albums to query: {len(album_ids)}")
# Initialize service
service = JmcomicService(option_path=args.option)
# Fetch details
results, failed = fetch_album_details(service, album_ids, verbose=args.verbose)
# Output results
if args.output:
output_path = Path(args.output)
output_data = {
"success_count": len(results),
"failed_count": len(failed),
"albums": results,
"failed": failed
}
with open(output_path, "w", encoding="utf-8") as f:
json.dump(output_data, f, indent=2, ensure_ascii=False)
print(f"\nโ
Exported {len(results)} album details to {output_path}")
else:
# Print to console
for album in results:
print_album_summary(album)
# Summary
print(f"\n{'='*60}")
print(f"๐ Summary:")
print(f"โ
Success: {len(results)}/{len(album_ids)}")
print(f"โ Failed: {len(failed)}/{len(album_ids)}")
if failed and not args.output:
print(f"\nFailed IDs:")
for item in failed:
print(f" - {item['id']}: {item['error']}")
print(f"{'='*60}")
if __name__ == "__main__":
main()
```
### scripts/download_covers.py
```python
#!/usr/bin/env python3
"""
Batch cover download tool.
Download cover images for multiple albums.
Usage:
# Download covers for specific albums
python scripts/download_covers.py --ids 123456,789012,345678
# Download covers from file
python scripts/download_covers.py --file album_ids.txt
# Specify output directory
python scripts/download_covers.py --ids 123456,789012 --output ./covers
"""
import argparse
import sys
from pathlib import Path
try:
from jmcomic_ai.core import JmcomicService
except ImportError:
print("โ Error: jmcomic_ai not found. Please ensure the package is installed.")
sys.exit(1)
def parse_args():
parser = argparse.ArgumentParser(description="Batch download JMComic album covers")
# Input mode
input_group = parser.add_mutually_exclusive_group(required=True)
input_group.add_argument("--ids", type=str, help="Comma-separated album IDs")
input_group.add_argument("--file", type=str, help="File containing album IDs (one per line)")
# Options
parser.add_argument("--output", type=str, help="Output directory (default: ./covers)")
parser.add_argument("--option", type=str, help="Path to option.yml file")
return parser.parse_args()
def load_album_ids(args) -> list[str]:
"""Load album IDs from arguments"""
if args.ids:
return [aid.strip() for aid in args.ids.split(",") if aid.strip()]
if args.file:
file_path = Path(args.file)
if not file_path.exists():
print(f"โ Error: File not found: {file_path}")
sys.exit(1)
with open(file_path, "r", encoding="utf-8") as f:
return [line.strip() for line in f if line.strip() and not line.startswith("#")]
return []
def download_covers(service: JmcomicService, album_ids: list[str], output_dir: Path) -> tuple[int, list[str]]:
"""Download covers for multiple albums"""
# Temporarily override the cover directory
original_base_dir = service.option.dir_rule.base_dir
service.option.dir_rule.base_dir = str(output_dir.parent)
success_count = 0
failed_ids = []
for i, album_id in enumerate(album_ids, 1):
print(f"[{i}/{len(album_ids)}] Downloading cover for album {album_id}...")
try:
# download_cover saves to base_dir/covers/
service.download_cover(album_id)
print(f"โ
Success")
success_count += 1
except Exception as e:
print(f"โ Failed: {e}")
failed_ids.append(album_id)
# Restore original base_dir
service.option.dir_rule.base_dir = original_base_dir
return success_count, failed_ids
def main():
args = parse_args()
album_ids = load_album_ids(args)
if not album_ids:
print("โ Error: No album IDs provided")
sys.exit(1)
# Determine output directory
if args.output:
output_dir = Path(args.output).resolve()
else:
output_dir = Path.cwd() / "covers"
output_dir.mkdir(parents=True, exist_ok=True)
print(f"๐ผ๏ธ Batch Cover Download Tool")
print(f"{'='*50}")
print(f"Total covers to download: {len(album_ids)}")
print(f"Output directory: {output_dir}")
print(f"{'='*50}\n")
# Initialize service
service = JmcomicService(option_path=args.option)
# Download covers
success_count, failed_ids = download_covers(service, album_ids, output_dir)
# Summary
print(f"\n{'='*50}")
print(f"๐ Download Summary:")
print(f"โ
Successful: {success_count}/{len(album_ids)}")
print(f"โ Failed: {len(failed_ids)}/{len(album_ids)}")
if failed_ids:
print(f"\nFailed album IDs:")
for aid in failed_ids:
print(f" - {aid}")
print(f"\n๐ Covers saved to: {output_dir}")
print(f"{'='*50}")
if __name__ == "__main__":
main()
```
### scripts/ranking_tracker.py
```python
#!/usr/bin/env python3
"""
Ranking tracker tool.
Track and export ranking changes over time.
Usage:
# Get current daily ranking
python scripts/ranking_tracker.py --period day --output daily_ranking.json
# Get multiple pages
python scripts/ranking_tracker.py --period week --max-pages 3 --output weekly_top.csv
# Track all periods
python scripts/ranking_tracker.py --all --output rankings/
"""
import argparse
import csv
import json
import sys
from datetime import datetime
from pathlib import Path
try:
from jmcomic_ai.core import JmcomicService
except ImportError:
print("โ Error: jmcomic_ai not found. Please ensure the package is installed.")
sys.exit(1)
def parse_args():
parser = argparse.ArgumentParser(description="Track JMComic rankings")
# Period selection
period_group = parser.add_mutually_exclusive_group(required=True)
period_group.add_argument("--period", type=str, choices=["day", "week", "month"], help="Ranking period")
period_group.add_argument("--all", action="store_true", help="Track all periods (day, week, month)")
# Options
parser.add_argument("--max-pages", type=int, default=1, help="Maximum pages to fetch (default: 1)")
parser.add_argument("--output", type=str, required=True, help="Output file/directory (.csv, .json, or directory for --all)")
parser.add_argument("--option", type=str, help="Path to option.yml file")
parser.add_argument("--add-timestamp", action="store_true", help="Add timestamp to output filename")
return parser.parse_args()
def fetch_ranking(service: JmcomicService, period: str, max_pages: int) -> list[dict]:
"""Fetch ranking for a specific period"""
all_results = []
for page in range(1, max_pages + 1):
print(f" ๐ Fetching {period} ranking page {page}...")
try:
# Use unified browse_albums API
# For ranking, we usually use order_by="likes" (or "views") combined with time_range
response = service.browse_albums(time_range=period, order_by="likes", page=page)
results = response.get("albums", [])
if not results:
print(f" โ ๏ธ No results on page {page}, stopping.")
break
# Add ranking position
for i, result in enumerate(results):
result["rank"] = (page - 1) * len(results) + i + 1
result["period"] = period
all_results.extend(results)
print(f" โ
Found {len(results)} albums")
except Exception as e:
print(f" โ Error on page {page}: {e}")
break
return all_results
def export_to_csv(results: list[dict], output_path: Path):
"""Export results to CSV format"""
if not results:
print(f"โ ๏ธ No results to export to {output_path}")
return
with open(output_path, "w", encoding="utf-8-sig", newline="") as f:
fieldnames = ["rank", "id", "title", "tags", "cover_url", "period"]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for result in results:
row = result.copy()
row["tags"] = ", ".join(result.get("tags", []))
writer.writerow(row)
print(f"โ
Exported {len(results)} albums to {output_path}")
def export_to_json(results: list[dict], output_path: Path):
"""Export results to JSON format"""
output_data = {
"timestamp": datetime.now().isoformat(),
"total_count": len(results),
"rankings": results
}
with open(output_path, "w", encoding="utf-8") as f:
json.dump(output_data, f, indent=2, ensure_ascii=False)
print(f"โ
Exported {len(results)} albums to {output_path}")
def get_output_path(base_path: str, period: str, add_timestamp: bool) -> Path:
"""Generate output path with optional timestamp"""
path = Path(base_path)
if add_timestamp:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
stem = f"{path.stem}_{timestamp}"
path = path.parent / f"{stem}{path.suffix}"
return path
def main():
args = parse_args()
print(f"๐ Ranking Tracker Tool")
print(f"{'='*50}")
# Initialize service
service = JmcomicService(option_path=args.option)
# Determine periods to track
if args.all:
periods = ["day", "week", "month"]
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
print(f"Tracking all periods, output to: {output_dir}")
else:
periods = [args.period]
# Fetch and export rankings
for period in periods:
print(f"\n๐ Fetching {period} ranking (max {args.max_pages} pages)...")
results = fetch_ranking(service, period, args.max_pages)
if not results:
print(f"โ No results for {period} ranking")
continue
# Determine output path
if args.all:
# For --all mode, create separate files for each period
output_path = output_dir / f"{period}_ranking.json"
else:
output_path = get_output_path(args.output, period, args.add_timestamp)
# Export based on file extension
if output_path.suffix.lower() == ".csv":
export_to_csv(results, output_path)
else:
export_to_json(results, output_path)
print(f"\n{'='*50}")
print(f"โจ Tracking complete!")
print(f"{'='*50}")
if __name__ == "__main__":
main()
```
### scripts/post_process.py
```python
import argparse
from jmcomic_ai.core import JmcomicService
def main():
parser = argparse.ArgumentParser(description="Post-process downloaded JMComic albums (Zip, PDF, LongImg)")
parser.add_argument("--id", required=True, help="Album ID to process")
parser.add_argument("--type", required=True, choices=["zip", "img2pdf", "long_img"], help="Processing type")
parser.add_argument("--option", help="Path to option.yml")
parser.add_argument("--delete", action="store_true", help="Delete original files after processing")
parser.add_argument("--password", help="Password for encryption (Zip/PDF)")
parser.add_argument("--outdir", help="Output directory")
parser.add_argument("--dir-rule", help="Output DSL rule, e.g. 'Bd/{Atitle}/{Pindex}.zip'")
parser.add_argument("--base-dir", help="Base directory used with --dir-rule")
parser.add_argument("--level", choices=["album", "photo"], default="photo", help="Processing level (default: photo)")
args = parser.parse_args()
if args.outdir and (args.dir_rule or args.base_dir):
parser.error("--outdir cannot be used with --dir-rule/--base-dir")
if args.dir_rule and not args.base_dir:
parser.error("--base-dir is required when using --dir-rule")
if args.base_dir and not args.dir_rule:
parser.error("--dir-rule is required when using --base-dir")
service = JmcomicService(args.option)
params = {"level": args.level}
if args.delete:
params["delete_original_file"] = True
if args.password:
if args.type == "long_img":
parser.error("--password is only supported for zip or img2pdf")
params["encrypt"] = {"password": args.password}
if args.dir_rule and args.base_dir:
params["dir_rule"] = {"rule": args.dir_rule, "base_dir": args.base_dir}
elif args.outdir:
params["dir_rule"] = {"rule": "Bd", "base_dir": args.outdir}
result = service.post_process(args.id, args.type, params)
print(result)
if __name__ == "__main__":
main()
```
### references/reference.md
```markdown
# JmOption Reference
Complete reference for configuring `jmcomic` behavior.
## Top Level Fields
- **`version`**: Configuration version string (e.g., "2.0")
- **`log`**: Boolean, whether to enable console logging (default: true)
- **`dir_rule`**: Directory and file naming rules
- **`download`**: Download behavior configuration
- **`client`**: Network client configuration (domains, retry, proxies, cookies)
- **`plugins`**: Plugin configurations
---
## dir_rule
Controls where and how files are saved.
### Fields
- **`base_dir`**: Root directory for downloads
- Supports environment variables: `${JM_DIR}/downloads/`
- Example: `D:/downloads/jmcomic/`
- **`rule`**: Directory structure DSL
- Start with `Bd` (base directory)
- Use `/` or `_` to separate levels
- Use `Pxxx` for photo properties, `Axxx` for album properties
- Supports f-string syntax: `Bd / Aauthor / (JM{Aid}-{Pindex})-{Pname}`
- Default: `Bd / Ptitle`
- Examples:
- `Bd / Ptitle` - Root / Chapter Title
- `Bd / Aid / Pindex` - Root / Album ID / Chapter Index
- `Bd / Aauthor / (JM{Aid}-{Pindex})-{Pname}` - Root / Author / (JM123-1)-ChapterName
- **`normalize_zh`**: Chinese character normalization (optional)
- `null` (default): No conversion
- `zh-cn`: Convert to simplified Chinese
- `zh-tw`: Convert to traditional Chinese
- Requires `zhconv` library
### Example
```yaml
dir_rule:
base_dir: D:/downloads/jmcomic/
rule: Bd / Aauthor / Ptitle
normalize_zh: zh-cn
```
---
## download
Download behavior settings.
### Fields
- **`cache`**: Boolean, skip downloading existing files (default: true)
- **`image`**: Image download settings
- **`decode`**: Boolean, decode scrambled images (default: true)
- **`suffix`**: String or null, force image format (e.g., `.jpg`, `.png`)
- **`threading`**: Concurrency settings
- **`image`**: Integer, max concurrent image downloads (default: 30, max: 50)
- **`photo`**: Integer, max concurrent chapter downloads (default: CPU thread count)
### Example
```yaml
download:
cache: true
image:
decode: true
suffix: .jpg
threading:
image: 30
photo: 16
```
---
## client
Network client configuration.
### Fields
- **`impl`**: Client implementation type
- `html`: Web client (IP restricted but efficient)
- `api`: APP client (no IP restriction, better compatibility)
- **`domain`**: Domain configuration for different implementations
- **`html`**: Array of domains for HTML client
- Example: `["18comic.vip", "18comic.org"]`
- **`api`**: Array of domains for API client
- Example: `["www.jmapiproxyxxx.vip"]`
- **`retry_times`**: Integer, number of retry attempts on failure (default: 5)
- **`postman`**: Request configuration
- **`meta_data`**: Metadata for requests
- **`proxies`**: Proxy configuration
- `null`: No proxy
- `system`: Use system proxy (default)
- `clash`: Use Clash proxy
- `v2ray`: Use V2Ray proxy
- `127.0.0.1:7890`: Custom proxy address
- Object with `http` and `https` keys for detailed config
- **`cookies`**: Login cookies (optional)
- **`AVS`**: AVS cookie value from browser
- **Important**: Cookies must match the domain being accessed
### Example
```yaml
client:
impl: html
domain:
html:
- 18comic.vip
- 18comic.org
api:
- www.jmapiproxyxxx.vip
retry_times: 5
postman:
meta_data:
# Proxy options:
proxies: system
# Or custom proxy:
# proxies:
# http: 127.0.0.1:7890
# https: 127.0.0.1:7890
# Login cookies (optional):
cookies:
AVS: your_avs_cookie_value
```
---
## plugins
Plugin configurations. Plugins execute at different lifecycle stages.
### Lifecycle Stages
- **`after_init`**: After initialization
- **`before_album`**: Before downloading an album
- **`after_album`**: After downloading an album
- **`before_photo`**: Before downloading a chapter
- **`after_photo`**: After downloading a chapter
- **`main`**: Main execution
### Common Plugins
#### `usage_log` (after_init)
Monitor hardware usage.
```yaml
- plugin: usage_log
kwargs:
interval: 0.5
enable_warning: true
```
#### `login` (after_init)
Login with username and password.
```yaml
- plugin: login
kwargs:
username: your_username
password: your_password
```
#### `download_cover` (before_album)
Download album covers.
```yaml
- plugin: download_cover
kwargs:
size: '_3x4' # Optional, for search page size
dir_rule:
base_dir: D:/covers/
rule: '{Atitle}/{Aid}_cover.jpg'
```
#### `zip` (after_album)
Compress downloaded files.
```yaml
- plugin: zip
kwargs:
level: photo # or 'album'
suffix: zip # or '7z'
delete_original_file: true
encrypt:
type: random # or specify password
```
#### `img2pdf` (after_photo or after_album)
Merge images into PDF.
```yaml
- plugin: img2pdf
kwargs:
pdf_dir: D:/pdf/
filename_rule: Pid # Use Axxx for after_album
encrypt:
password: "123456"
```
#### `send_qq_email` (after_album)
Send email notification.
```yaml
- plugin: send_qq_email
kwargs:
msg_from: [email protected]
msg_to: [email protected]
password: authorization_code
title: Download Complete
content: Your download has finished!
```
### Example
```yaml
plugins:
after_init:
- plugin: usage_log
kwargs:
interval: 0.5
enable_warning: true
before_album:
- plugin: download_cover
kwargs:
dir_rule:
base_dir: D:/covers/
rule: '{Atitle}/{Aid}_cover.jpg'
after_album:
- plugin: zip
kwargs:
level: photo
suffix: zip
delete_original_file: true
- plugin: send_qq_email
kwargs:
msg_from: ${EMAIL}
msg_to: [email protected]
password: ${EMAIL_PASSWORD}
title: Download Complete
content: Album downloaded successfully!
```
---
## Environment Variables
All `kwargs` parameters support environment variable references using `${VAR_NAME}` syntax.
Example:
```yaml
dir_rule:
base_dir: ${JM_DOWNLOAD_DIR}/
client:
postman:
meta_data:
cookies:
AVS: ${JM_COOKIE_AVS}
plugins:
after_album:
- plugin: send_qq_email
kwargs:
msg_from: ${EMAIL}
password: ${EMAIL_PASSWORD}
```
---
## Complete Example
```yaml
version: "2.0"
log: true
dir_rule:
base_dir: D:/downloads/jmcomic/
rule: Bd / Aauthor / Ptitle
normalize_zh: zh-cn
download:
cache: true
image:
decode: true
suffix: .jpg
threading:
image: 30
photo: 16
client:
impl: html
domain:
html:
- 18comic.vip
- 18comic.org
retry_times: 5
postman:
meta_data:
proxies: system
cookies:
AVS: your_avs_cookie_value
plugins:
after_init:
- plugin: usage_log
kwargs:
interval: 0.5
enable_warning: true
after_album:
- plugin: zip
kwargs:
level: photo
suffix: zip
delete_original_file: true
encrypt:
type: random
```
---
For the complete list of plugins and their parameters, see `assets/option_schema.json`.
```
### assets/option_schema.json
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "JmOption Configuration",
"description": "Complete configuration schema for JMComic-Crawler-Python",
"type": "object",
"properties": {
"version": {
"type": "string",
"description": "Configuration version string",
"examples": [
"2.0"
]
},
"log": {
"type": "boolean",
"default": true,
"description": "Enable or disable jmcomic logging output"
},
"client": {
"type": "object",
"description": "Client configuration",
"properties": {
"impl": {
"type": "string",
"enum": [
"html",
"api"
],
"description": "Client implementation type. 'html' for web client (IP restricted but efficient), 'api' for APP client (no IP restriction, better compatibility)"
},
"domain": {
"oneOf": [
{
"type": "object",
"properties": {
"html": {
"type": "array",
"items": {
"type": "string"
}
},
"api": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
],
"default": []
},
"retry_times": {
"type": "integer",
"default": 5,
"minimum": 0,
"description": "Number of retry attempts on request failure"
},
"postman": {
"type": "object",
"description": "Request configuration",
"properties": {
"meta_data": {
"type": "object",
"properties": {
"proxies": {
"oneOf": [
{
"type": "null",
"description": "No proxy"
},
{
"type": "string",
"enum": [
"system",
"clash",
"v2ray"
],
"description": "Predefined proxy type or proxy address (e.g., '127.0.0.1:7890')"
},
{
"type": "object",
"properties": {
"http": {
"type": "string",
"description": "HTTP proxy address"
},
"https": {
"type": "string",
"description": "HTTPS proxy address"
}
}
}
],
"default": "system",
"description": "Proxy configuration. 'system' uses system proxy, null disables proxy"
},
"cookies": {
"type": [
"object",
"null"
],
"description": "Account cookies for login. Only AVS cookie is needed. Must match the domain being accessed",
"properties": {
"AVS": {
"type": "string",
"description": "AVS cookie value from browser"
}
}
}
}
}
}
}
},
"additionalProperties": false
},
"download": {
"type": "object",
"description": "Download configuration",
"properties": {
"cache": {
"type": "boolean",
"default": true,
"description": "Skip downloading files that already exist on disk"
},
"image": {
"type": "object",
"description": "Image download settings",
"properties": {
"decode": {
"type": "boolean",
"default": true,
"description": "Decode scrambled images from JMComic"
},
"suffix": {
"type": [
"string",
"null"
],
"default": null,
"description": "Force convert all images to specified format (e.g., '.jpg')",
"examples": [
".jpg",
".png"
]
}
}
},
"threading": {
"type": "object",
"description": "Concurrency settings",
"properties": {
"image": {
"type": "integer",
"default": 30,
"minimum": 1,
"maximum": 50,
"description": "Max concurrent image downloads (JMComic web allows max 50)"
},
"photo": {
"type": "integer",
"minimum": 1,
"description": "Max concurrent chapter downloads (defaults to CPU thread count)"
}
}
}
},
"additionalProperties": false
},
"dir_rule": {
"type": "object",
"description": "Directory and file naming rules",
"properties": {
"base_dir": {
"type": "string",
"description": "Root directory for downloads. Supports environment variables (e.g., '${JM_DIR}/downloads/')",
"examples": [
"D:/downloads/jmcomic/",
"${JM_DIR}/downloads/"
]
},
"rule": {
"type": "string",
"description": "Directory structure DSL. Start with 'Bd' (base dir), use '/' or '_' to separate levels. Use Pxxx for photo properties, Axxx for album properties. Supports f-string syntax (e.g., 'Bd / Aauthor / (JM{Aid}-{Pindex})-{Pname}')",
"default": "Bd / Ptitle",
"examples": [
"Bd / Ptitle",
"Bd / Aid / Pindex",
"Bd / Aauthor / (JM{Aid}-{Pindex})-{Pname}"
]
},
"normalize_zh": {
"type": [
"string",
"null"
],
"enum": [
"zh-cn",
"zh-tw",
null
],
"default": null,
"description": "Chinese character normalization. 'zh-cn' for simplified, 'zh-tw' for traditional, null for no conversion. Requires 'zhconv' library"
}
},
"additionalProperties": false
},
"plugins": {
"type": "object",
"description": "Plugin configurations. Plugins are executed at different lifecycle stages",
"properties": {
"after_init": {
"type": "array",
"description": "Plugins executed after initialization",
"items": {
"$ref": "#/definitions/plugin"
}
},
"before_album": {
"type": "array",
"description": "Plugins executed before downloading an album",
"items": {
"$ref": "#/definitions/plugin"
}
},
"after_album": {
"type": "array",
"description": "Plugins executed after downloading an album",
"items": {
"$ref": "#/definitions/plugin"
}
},
"before_photo": {
"type": "array",
"description": "Plugins executed before downloading a chapter",
"items": {
"$ref": "#/definitions/plugin"
}
},
"after_photo": {
"type": "array",
"description": "Plugins executed after downloading a chapter",
"items": {
"$ref": "#/definitions/plugin"
}
},
"main": {
"type": "array",
"description": "Main plugins",
"items": {
"$ref": "#/definitions/plugin"
}
}
}
}
},
"definitions": {
"plugin": {
"type": "object",
"required": [
"plugin"
],
"properties": {
"plugin": {
"type": "string",
"description": "Plugin name",
"enum": [
"usage_log",
"login",
"find_update",
"image_suffix_filter",
"replace_path_string",
"client_proxy",
"auto_set_browser_cookies",
"jm_server",
"subscribe_album_update",
"download_cover",
"zip",
"delete_duplicated_files",
"send_qq_email",
"favorite_folder_export",
"skip_photo_with_few_images",
"img2pdf",
"long_img"
]
},
"log": {
"type": "boolean",
"description": "Enable logging for this plugin"
},
"kwargs": {
"type": "object",
"description": "Plugin-specific arguments. Supports environment variables (e.g., '${ENV_VAR}')"
}
}
},
"usage_log_plugin": {
"allOf": [
{
"$ref": "#/definitions/plugin"
},
{
"properties": {
"plugin": {
"const": "usage_log"
},
"kwargs": {
"type": "object",
"properties": {
"interval": {
"type": "number",
"description": "Logging interval in seconds"
},
"enable_warning": {
"type": "boolean",
"description": "Enable warnings for high resource usage"
}
}
}
}
}
]
},
"login_plugin": {
"allOf": [
{
"$ref": "#/definitions/plugin"
},
{
"properties": {
"plugin": {
"const": "login"
},
"kwargs": {
"type": "object",
"required": [
"username",
"password"
],
"properties": {
"username": {
"type": "string",
"description": "Username"
},
"password": {
"type": "string",
"description": "Password"
}
}
}
}
}
]
},
"find_update_plugin": {
"allOf": [
{
"$ref": "#/definitions/plugin"
},
{
"properties": {
"plugin": {
"const": "find_update"
},
"kwargs": {
"type": "object",
"description": "Map of album_id to last_downloaded_photo_id",
"patternProperties": {
"^[0-9]+$": {
"type": "integer"
}
}
}
}
}
]
},
"download_cover_plugin": {
"allOf": [
{
"$ref": "#/definitions/plugin"
},
{
"properties": {
"plugin": {
"const": "download_cover"
},
"kwargs": {
"type": "object",
"properties": {
"size": {
"type": "string",
"description": "Cover size (e.g., '_3x4' for search page size)"
},
"dir_rule": {
"type": "object",
"properties": {
"base_dir": {
"type": "string"
},
"rule": {
"type": "string"
}
}
}
}
}
}
}
]
},
"zip_plugin": {
"allOf": [
{
"$ref": "#/definitions/plugin"
},
{
"properties": {
"plugin": {
"const": "zip"
},
"kwargs": {
"type": "object",
"properties": {
"level": {
"type": "string",
"enum": [
"photo",
"album"
],
"description": "Compression level: 'photo' for per-chapter, 'album' for entire album"
},
"filename_rule": {
"type": "string",
"description": "Naming rule for zip files. Use Pxxx for photo level, Axxx for album level"
},
"zip_dir": {
"type": "string",
"description": "Directory to store zip files (deprecated, use dir_rule instead)"
},
"suffix": {
"type": "string",
"enum": [
"zip",
"7z"
],
"default": "zip",
"description": "Compression format"
},
"dir_rule": {
"type": "object",
"description": "New configuration replacing zip_dir and filename_rule",
"properties": {
"base_dir": {
"type": "string"
},
"rule": {
"type": "string"
}
}
},
"delete_original_file": {
"type": "boolean",
"description": "Delete original files after successful compression"
},
"encrypt": {
"type": "object",
"description": "Encryption settings",
"properties": {
"impl": {
"type": "string",
"enum": [
"zip",
"7z"
],
"description": "Encryption implementation (7z encrypts file headers)"
},
"type": {
"type": "string",
"enum": [
"random"
],
"description": "Password type: 'random' for auto-generated password"
},
"password": {
"type": "string",
"description": "Fixed password (mutually exclusive with 'type')"
}
}
}
}
}
}
}
]
},
"send_qq_email_plugin": {
"allOf": [
{
"$ref": "#/definitions/plugin"
},
{
"properties": {
"plugin": {
"const": "send_qq_email"
},
"kwargs": {
"type": "object",
"required": [
"msg_from",
"msg_to",
"password"
],
"properties": {
"msg_from": {
"type": "string",
"description": "Sender email address"
},
"msg_to": {
"type": "string",
"description": "Recipient email address"
},
"password": {
"type": "string",
"description": "Sender's authorization code (not login password)"
},
"title": {
"type": "string",
"description": "Email subject"
},
"content": {
"type": "string",
"description": "Email content"
}
}
}
}
}
]
},
"img2pdf_plugin": {
"allOf": [
{
"$ref": "#/definitions/plugin"
},
{
"properties": {
"plugin": {
"const": "img2pdf"
},
"kwargs": {
"type": "object",
"required": [
"pdf_dir",
"filename_rule"
],
"properties": {
"pdf_dir": {
"type": "string",
"description": "Directory to store PDF files"
},
"filename_rule": {
"type": "string",
"description": "PDF naming rule. Use Pxxx for after_photo, Axxx for after_album"
},
"encrypt": {
"type": "object",
"properties": {
"password": {
"type": "string",
"description": "PDF password"
}
}
}
}
}
}
}
]
},
"skip_photo_with_few_images_plugin": {
"allOf": [
{
"$ref": "#/definitions/plugin"
},
{
"properties": {
"plugin": {
"const": "skip_photo_with_few_images"
},
"kwargs": {
"type": "object",
"required": [
"at_least_image_count"
],
"properties": {
"at_least_image_count": {
"type": "integer",
"minimum": 1,
"description": "Minimum number of images required to download a chapter"
}
}
}
}
}
]
}
}
}
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### references/examples.md
```markdown
# JMComic Option Configuration Examples
This file contains various configuration examples for JMComic-Crawler-Python.
## Minimal Configuration
```yaml
# Minimal config - uses all defaults
log: true
```
## Basic Download Configuration
```yaml
version: "2.0"
log: true
# Set download directory
dir_rule:
base_dir: D:/downloads/jmcomic/
rule: Bd / Ptitle
# Basic download settings
download:
cache: true
image:
decode: true
threading:
image: 30
photo: 5
```
## Configuration with Proxy
```yaml
version: "2.0"
log: true
dir_rule:
base_dir: D:/downloads/jmcomic/
# Use system proxy
client:
postman:
meta_data:
proxies: system
# Or use custom proxy
# client:
# postman:
# meta_data:
# proxies:
# http: 127.0.0.1:7890
# https: 127.0.0.1:7890
```
## Configuration with Login
```yaml
version: "2.0"
log: true
dir_rule:
base_dir: D:/downloads/jmcomic/
# Login with cookies
client:
postman:
meta_data:
proxies: system
cookies:
AVS: your_avs_cookie_value_from_browser
# Or use login plugin
plugins:
after_init:
- plugin: login
kwargs:
username: your_username
password: your_password
```
## Advanced Directory Rules
```yaml
version: "2.0"
dir_rule:
base_dir: ${JM_DOWNLOAD_DIR}/
# Save as: BaseDir / Author / (JM123-1)-ChapterName
rule: Bd / Aauthor / (JM{Aid}-{Pindex})-{Pname}
normalize_zh: zh-cn # Convert to simplified Chinese
download:
threading:
image: 30
photo: 16
```
## Configuration with Plugins
```yaml
version: "2.0"
log: true
dir_rule:
base_dir: D:/downloads/jmcomic/
download:
cache: true
threading:
image: 30
photo: 5
plugins:
# Monitor hardware usage
after_init:
- plugin: usage_log
kwargs:
interval: 0.5
enable_warning: true
# Download album covers
before_album:
- plugin: download_cover
kwargs:
dir_rule:
base_dir: D:/covers/
rule: '{Atitle}/{Aid}_cover.jpg'
# Compress and notify after download
after_album:
- plugin: zip
kwargs:
level: photo
suffix: zip
delete_original_file: true
encrypt:
type: random
- plugin: send_qq_email
kwargs:
msg_from: ${EMAIL}
msg_to: [email protected]
password: ${EMAIL_PASSWORD}
title: Download Complete
content: Album downloaded successfully!
```
## PDF Generation Configuration
```yaml
version: "2.0"
dir_rule:
base_dir: D:/downloads/jmcomic/
plugins:
# Merge each chapter into a PDF
after_photo:
- plugin: img2pdf
kwargs:
pdf_dir: D:/pdf/
filename_rule: Pid
encrypt:
password: "123456"
# Or merge entire album into one PDF
# after_album:
# - plugin: img2pdf
# kwargs:
# pdf_dir: D:/pdf/
# filename_rule: Aname
# encrypt:
# password: "123456"
```
## Multi-Domain Configuration
```yaml
version: "2.0"
client:
impl: html
# Try multiple domains in order
domain:
html:
- 18comic.vip
- 18comic.org
- 18comic.biz
api:
- www.jmapiproxyxxx.vip
retry_times: 5
postman:
meta_data:
proxies: system
```
## Skip Low-Quality Chapters
```yaml
version: "2.0"
dir_rule:
base_dir: D:/downloads/jmcomic/
plugins:
# Skip chapters with less than 3 images (usually announcements)
before_photo:
- plugin: skip_photo_with_few_images
kwargs:
at_least_image_count: 3
```
## Auto-Update Subscription
```yaml
version: "2.0"
dir_rule:
base_dir: D:/downloads/jmcomic/
plugins:
after_init:
# Only download new chapters
- plugin: find_update
kwargs:
145504: 290266 # Album 145504, last downloaded chapter 290266
234567: 345678 # Album 234567, last downloaded chapter 345678
# Subscribe to updates and send email
- plugin: subscribe_album_update
kwargs:
download_if_has_update: true
email_notify:
msg_from: ${EMAIL}
msg_to: ${EMAIL}
password: ${EMAIL_PASSWORD}
title: New Chapter Available!
content: Your subscribed album has new chapters!
album_photo_dict:
324930: 424507
```
## Complete Production Configuration
```yaml
version: "2.0"
log: true
# Directory configuration
dir_rule:
base_dir: ${JM_DOWNLOAD_DIR}/
rule: Bd / Aauthor / (JM{Aid}-{Pindex})-{Pname}
normalize_zh: zh-cn
# Download settings
download:
cache: true
image:
decode: true
suffix: .jpg
threading:
image: 30
photo: 16
# Client configuration
client:
impl: html
domain:
html:
- 18comic.vip
- 18comic.org
retry_times: 5
postman:
meta_data:
proxies: system
cookies:
AVS: ${JM_COOKIE_AVS}
# Plugins
plugins:
after_init:
# Monitor system resources
- plugin: usage_log
kwargs:
interval: 0.5
enable_warning: true
# Only download new chapters
- plugin: find_update
kwargs:
145504: 290266
before_album:
# Download covers
- plugin: download_cover
kwargs:
dir_rule:
base_dir: ${JM_DOWNLOAD_DIR}/covers/
rule: '{Atitle}/{Aid}_cover.jpg'
before_photo:
# Skip announcement chapters
- plugin: skip_photo_with_few_images
kwargs:
at_least_image_count: 3
after_photo:
# Merge into PDF
- plugin: img2pdf
kwargs:
pdf_dir: ${JM_DOWNLOAD_DIR}/pdf/
filename_rule: Pid
encrypt:
password: ${PDF_PASSWORD}
after_album:
# Compress with encryption
- plugin: zip
kwargs:
level: photo
suffix: 7z
dir_rule:
base_dir: ${JM_DOWNLOAD_DIR}/archives/
rule: 'Bd / {Atitle} / [{Pid}]-{Ptitle}.7z'
delete_original_file: true
encrypt:
impl: 7z
type: random
# Send completion email
- plugin: send_qq_email
kwargs:
msg_from: ${EMAIL}
msg_to: ${EMAIL}
password: ${EMAIL_PASSWORD}
title: JMComic Download Complete
content: Album downloaded, compressed, and archived successfully!
```
## Notes
1. **Environment Variables**: Use `${VAR_NAME}` syntax to reference environment variables
2. **Directory Rules**:
- `Bd` = Base Directory
- `Axxx` = Album properties (e.g., `Aid`, `Atitle`, `Aauthor`)
- `Pxxx` = Photo/Chapter properties (e.g., `Pid`, `Ptitle`, `Pindex`)
3. **Proxy Settings**: Must be under `client.postman.meta_data.proxies`
4. **Cookies**: Must be under `client.postman.meta_data.cookies`
5. **Plugin Lifecycle**: Choose the right stage for each plugin
6. **Compression**: Use `7z` with `impl: 7z` for maximum privacy (encrypts file headers)
```