Back to skills
SkillHub ClubResearch & OpsFull Stack

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.

Stars
20
Hot score
87
Updated
March 20, 2026
Overall rating
C1.8
Composite score
1.8
Best-practice grade
B75.9

Install command

npx @skill-hub/cli install hect0x7-jmcomic-ai-jmcomic

Repository

hect0x7/jmcomic-ai

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 repository

Best 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

Claude CodeCodex CLIGemini CLIOpenCode

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)

```

jmcomic | SkillHub