Skip to content

Conversation

@hect0x7
Copy link
Owner

@hect0x7 hect0x7 commented Sep 20, 2025

Summary by CodeRabbit

  • New Features

    • Added API and plugin to download album cover images (configurable size and save path).
    • Added configurable domain retry strategy and per-domain retry cap.
  • Bug Fixes

    • Relaxed rule parsing to treat unknown rules as f-string rules instead of erroring.
  • Documentation

    • Updated tutorial and option docs with cover-download examples and dir_rule guidance.
  • Tests

    • Added tests for album cover downloading.
  • Chores

    • Bumped package version; normalized plugin identifier naming.

@coderabbitai
Copy link

coderabbitai bot commented Sep 20, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds album-cover download support (JmcomicText.get_album_cover_url + JmImageClient.download_album_cover), a DownloadCoverPlugin, tests and docs updates, a fallback rule parser change, domain_retry_strategy injection into AbstractJmClient, an advanced_retry plugin rename, and a version bump.

Changes

Cohort / File(s) Summary
Album cover URL builder
src/jmcomic/jm_toolkit.py
Adds JmcomicText.get_album_cover_url(album_id, image_domain=None, size='') to construct cover image URLs (selects a random image domain if none provided).
Image client API
src/jmcomic/jm_client_interface.py
Adds JmImageClient.download_album_cover(self, album_id, save_path: str, size: str='') which calls download_image using the cover URL (scramble_id=None, decode_image=False).
Client implementation / retry strategy
src/jmcomic/jm_client_impl.py
AbstractJmClient.__init__ gains optional domain_retry_strategy stored on the instance; request_with_retry uses it to obtain alternate domains for retries.
Download-cover plugin & filepath API
src/jmcomic/jm_plugin.py
decide_filepath parameter types updated to accept Optional[str] for filename_rule and suffix; adds DownloadCoverPlugin (plugin_key: download_cover) invoking downloader/client to download album covers and skipping when cached; AdvancedRetryPlugin.plugin_key renamed to advanced_retry.
Option parsing fallback
src/jmcomic/jm_option.py
Unknown dir rules that don't match expected prefixes now fall back to parse_f_string_rule instead of raising an exception.
Docs / examples / options
assets/docs/sources/tutorial/0_common_usage.md, assets/docs/sources/option_file_syntax.md
Tutorial updated with a cover-download example (download_album_cover, e.g., cover.png); option_file_syntax.md and plugin examples add before_album entries for download_cover with size and dir_rule; minor formatting fixes.
Options test config
assets/option/option_test_api.yml
Plugin identifier advanced-retry renamed to advanced_retry; adds retry_domain_max_times: 5 under retry_config for advanced_retry.
Tests
tests/test_jmcomic/test_jm_client.py
Adds test_download_cover verifying download_album_cover saves cover files (including size variant _3x4).
Version bump
src/jmcomic/__init__.py
Bumps __version__ from 2.6.7 to 2.6.8.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Caller
  participant IC as JmImageClient
  participant JT as JmcomicText
  participant Net as HTTP
  participant FS as FileSystem

  Dev->>IC: download_album_cover(album_id, save_path, size?)
  IC->>JT: get_album_cover_url(album_id, image_domain?, size?)
  JT-->>IC: cover_url
  IC->>Net: GET cover_url (scramble_id=None, decode_image=False)
  alt 200 OK
    Net-->>IC: image bytes
    IC->>FS: write save_path (e.g., cover.png)
    FS-->>IC: OK
    IC-->>Dev: success
  else error
    IC-->>Dev: error
  end
Loading
sequenceDiagram
  autonumber
  participant C as AbstractJmClient
  participant S as domain_retry_strategy (optional)
  participant Net as HTTP

  C->>Net: request(url)
  alt success
    Net-->>C: response
  else failure
    opt strategy provided
      C->>S: strategy(url, attempt, error)
      S-->>C: next_domain / None
      alt next_domain provided
        C->>Net: request(url@next_domain)
        Net-->>C: response/error
      else no suggestion
        C->>Net: fallback retry logic
        Net-->>C: response/error
      end
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

A rabbit hops and finds the cover bright,
I fetch the image, day or night.
If domains wobble, I nudge and try,
Save it as cover — a carrot sigh. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The provided title "Dev" is too vague and does not describe the primary changes in this PR. The changeset introduces a new album-cover download API/method, a DownloadCoverPlugin, a get_album_cover_url utility, an optional domain_retry_strategy parameter, added tests, and a version bump, so a reviewer cannot infer the main purpose from "Dev". For these reasons the title is inconclusive for review purposes. Please replace "Dev" with a short, descriptive title that highlights the main change (for example: "Add download_album_cover API and DownloadCoverPlugin" or "Support album cover download via get_album_cover_url"); also add a one-paragraph PR description listing the key changes and any migration or breaking notes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9fc27e and 205f332.

📒 Files selected for processing (2)
  • assets/option/option_test_api.yml (1 hunks)
  • src/jmcomic/jm_plugin.py (3 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (7)
src/jmcomic/jm_toolkit.py (1)

384-387: Non-crypto randomness is fine here; hush the linter or switch API.

Domain selection isn’t security‑sensitive. Either keep random.choice and add # noqa: S311 (as in the diff above), or replace with secrets.choice if you prefer zero lint noise.

tests/test_jmcomic/test_jm_client.py (1)

336-339: Assert the download and avoid polluting CWD.

Use a temp directory and assert the file exists to make the test meaningful and hermetic.

-    def test_download_cover(self):
-        album_id = 123
-        self.client.download_album_cover(album_id, f'./{album_id}.jpg')
+    def test_download_cover(self):
+        import os, tempfile
+        album_id = 123
+        with tempfile.TemporaryDirectory() as td:
+            path = os.path.join(td, f'{album_id}.jpg')
+            self.client.download_album_cover(album_id, path)
+            self.assertTrue(os.path.exists(path), f'cover not downloaded: {path}')
src/jmcomic/jm_client_interface.py (1)

296-303: Expose size passthrough and make import explicit for linters.

  • Optional: surface size so callers can request variants like "_3x4".
  • Lint: star import obscures JmcomicText; add an explicit import or alias.

Apply this diff:

-    def download_album_cover(self, album_id, save_path: str):
+    def download_album_cover(self, album_id, save_path: str, size: str = ''):
         self.download_image(
-            img_url=JmcomicText.get_album_cover_url(album_id),
+            img_url=JmcomicText.get_album_cover_url(album_id, size=size),
             img_save_path=save_path,
             scramble_id=None,
             decode_image=False,
         )

And near the top of the file (outside this hunk), add:

from .jm_toolkit import JmcomicText  # explicit symbol to satisfy linters (F405)

If you prefer not to change the public signature, ignore the size part and only add the explicit import. Please confirm which route you want.

assets/docs/sources/tutorial/0_common_usage.md (2)

52-54: Docs: mention optional size for cover.

Add an example that shows requesting a specific cover size to match the API surface.

-# 下载本子封面图,保存为 cover.png (图片后缀可指定为jpg、webp等)
-client.download_album_cover('427413', './cover.png')
+# 下载本子封面图,保存为 cover.png (图片后缀可指定为jpg、webp等)
+client.download_album_cover('427413', './cover.png')
+# 指定封面尺寸(例如列表页用的 3x4)
+client.download_album_cover('427413', './cover_3x4.png', size='_3x4')

69-71: Prefer helper for cover URL in examples.

Use JmcomicText.get_album_cover_url instead of hardcoding the albums path.

-    random_image_domain = JmModuleConfig.DOMAIN_IMAGE_LIST[0]
-    client.download_image(f'https://{random_image_domain}/media/albums/416130.jpg', './a.jpg')
+    client.download_image(JmcomicText.get_album_cover_url('416130'), './a.jpg')
src/jmcomic/jm_client_impl.py (2)

14-31: Constructor param added; update docstring and typing.

Document domain_retry_strategy and, if possible, type it as an Optional[Callable]. This improves discoverability and plugin authoring.

     def __init__(self,
                  postman: Postman,
                  domain_list: List[str],
                  retry_times=0,
-                 domain_retry_strategy=None,
+                 domain_retry_strategy=None,
                  ):
@@
-        :param retry_times: 重试次数
+        :param retry_times: 重试次数
+        :param domain_retry_strategy: 自定义域名/重试策略回调,签名:
+            (client, request, url, is_image, *, domain_index, retry_count, **kwargs) -> Response

Optionally (outside this hunk), annotate the field:

from typing import Optional, Callable, Any
self.domain_retry_strategy: Optional[Callable[..., Any]] = domain_retry_strategy

73-80: Pass retry context into custom strategy.

Hand the domain_index and retry_count to the strategy; it’s often needed for proper backoff/switching.

-        if self.domain_retry_strategy:
-            return self.domain_retry_strategy(self,
-                                              request,
-                                              url,
-                                              is_image,
-                                              **kwargs,
-                                              )
+        if self.domain_retry_strategy:
+            return self.domain_retry_strategy(self,
+                                              request,
+                                              url,
+                                              is_image,
+                                              domain_index=domain_index,
+                                              retry_count=retry_count,
+                                              **kwargs)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7943b9b and e610e2b.

📒 Files selected for processing (5)
  • assets/docs/sources/tutorial/0_common_usage.md (2 hunks)
  • src/jmcomic/jm_client_impl.py (2 hunks)
  • src/jmcomic/jm_client_interface.py (1 hunks)
  • src/jmcomic/jm_toolkit.py (1 hunks)
  • tests/test_jmcomic/test_jm_client.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/test_jmcomic/test_jm_client.py (1)
src/jmcomic/jm_client_interface.py (1)
  • download_album_cover (296-302)
src/jmcomic/jm_client_interface.py (5)
src/jmcomic/jm_entity.py (1)
  • album_id (363-364)
src/jmcomic/jm_toolkit.py (1)
  • get_album_cover_url (372-388)
src/jmcomic/jm_option.py (1)
  • download_album (489-495)
src/jmcomic/jm_downloader.py (3)
  • download_album (85-88)
  • download_by_image_detail (121-146)
  • download_by_image_detail (328-333)
tests/test_jmcomic/test_jm_api.py (1)
  • test_download_album_by_id (13-18)
src/jmcomic/jm_toolkit.py (2)
src/jmcomic/jm_entity.py (1)
  • album_id (363-364)
src/jmcomic/jm_config.py (1)
  • JmModuleConfig (84-503)
🪛 Ruff (0.13.1)
src/jmcomic/jm_client_interface.py

298-298: JmcomicText may be undefined, or defined from star imports

(F405)

src/jmcomic/jm_toolkit.py

374-374: PEP 484 prohibits implicit Optional

Convert to Optional[T]

(RUF013)


382-382: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


386-386: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


386-386: JmModuleConfig may be undefined, or defined from star imports

(F405)


388-388: JmModuleConfig may be undefined, or defined from star imports

(F405)

hect0x7 and others added 2 commits September 20, 2025 21:02
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/jmcomic/jm_toolkit.py (2)

377-383: Docstring punctuation triggers Ruff (RUF002).

Switch fullwidth punctuation to ASCII to appease the linter.

Apply this diff:

-        根据本子id生成封面url
+        根据本子 id 生成封面 URL
@@
-        :param image_domain: 图片cdn域名(可传入裸域或含协议的域名)
-        :param size: 尺寸后缀,例如搜索列表页会使用 size="_3x4" 的封面图
+        :param image_domain: 图片 CDN 域名 (可传入裸域或含协议的域名)
+        :param size: 尺寸后缀, 例如搜索列表页会使用 size="_3x4" 的封面图

388-389: Optional: guard against trailing slashes in domains.

If callers pass image_domain with a trailing slash, format_url currently produces a double slash. Consider normalizing in format_url.

Example (outside this hunk):

@classmethod
def format_url(cls, path, domain):
    ExceptionTool.require_true(isinstance(domain, str) and len(domain) != 0, '域名为空')
    path = path if path.startswith('/') else '/' + path
    if domain.startswith(JmModuleConfig.PROT):
        return f'{domain.rstrip("/")}{path}'
    return f'{JmModuleConfig.PROT}{domain.rstrip("/")}{path}'
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e610e2b and 2089104.

📒 Files selected for processing (1)
  • src/jmcomic/jm_toolkit.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/jmcomic/jm_toolkit.py (1)
src/jmcomic/jm_config.py (1)
  • JmModuleConfig (84-503)
🪛 Ruff (0.13.1)
src/jmcomic/jm_toolkit.py

373-373: Union may be undefined, or defined from star imports

(F405)


374-374: Optional may be undefined, or defined from star imports

(F405)


381-381: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


381-381: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


382-382: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


386-386: JmModuleConfig may be undefined, or defined from star imports

(F405)

🔇 Additional comments (2)
src/jmcomic/jm_toolkit.py (2)

371-389: Album-cover URL helper: logic LGTM.

Broader typing + format_url usage resolve the scheme-duplication concern and handle int/str album IDs correctly.


373-386: Import Optional/Union and JmModuleConfig explicitly.

Silences F405 and avoids annotation runtime issues in environments without postponed annotations.

Add these imports near the top of the file:

from typing import Optional, Union
from .jm_config import JmModuleConfig
#!/bin/bash
# Verify explicit imports exist
rg -nP 'from\s+typing\s+import\s+.*\b(Optional|Union)\b' src/jmcomic/jm_toolkit.py
rg -nP 'from\s+\.jm_config\s+import\s+JmModuleConfig' src/jmcomic/jm_toolkit.py

@hect0x7
Copy link
Owner Author

hect0x7 commented Sep 21, 2025

@coderabbitai review new commit

@coderabbitai
Copy link

coderabbitai bot commented Sep 21, 2025

@hect0x7 I'll review the new commit for you. This appears to be a feature addition for downloading album cover images, along with some infrastructure improvements for domain retry strategies.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/jmcomic/jm_plugin.py (1)

1311-1328: Register plugin 'download-cover'

Add a call to JmModuleConfig.register_plugin(DownloadCoverPlugin) (e.g., immediately after the class in src/jmcomic/jm_plugin.py) — current repo search shows no register_plugin invocations except the method definition and a docs example, so 'download-cover' is not being added to JmModuleConfig.REGISTRY_PLUGIN and will fail to resolve from config.

🧹 Nitpick comments (4)
src/jmcomic/jm_option.py (1)

161-163: Make the fallback observable; remove dead code.

Silently treating unknown tokens as f-string text is fine, but log once per token to aid misconfig debugging, and drop the commented raise.

-        return cls.parse_f_string_rule
-        # ExceptionTool.raises(f'不支持的rule配置: "{rule}"')
+        jm_log('dir_rule.fallback', f'未识别的rule片段"{rule}",按f-string文本处理。')
+        return cls.parse_f_string_rule
src/jmcomic/jm_client_interface.py (1)

296-303: Return the delegate call; import JmcomicText explicitly to satisfy Ruff F405.

  • Keep semantics aligned with download_by_image_detail by returning the inner call.
  • Add an explicit import for JmcomicText to avoid F405 from star-imports.
-    def download_album_cover(self, album_id, save_path: str, size: str = ''):
-        self.download_image(
+    def download_album_cover(self, album_id, save_path: str, size: str = ''):
+        """Download album cover without decoding."""
+        return self.download_image(
             img_url=JmcomicText.get_album_cover_url(album_id, size=size),
             img_save_path=save_path,
             scramble_id=None,
             decode_image=False,
         )

Add near the top of the file (outside this hunk):

from .jm_toolkit import JmcomicText  # explicit import to avoid F405
assets/docs/sources/option_file_syntax.md (1)

197-205: Clarify overwrite behavior for download-cover.

The plugin currently always re-downloads/overwrites the cover. Consider adding a note here (or a skip_if_exists flag) so users know repeated runs won’t be skipped.

src/jmcomic/jm_plugin.py (1)

1311-1328: Harden DownloadCoverPlugin: validate context, optional cache-skip, lint nits.

  • Validate that either album or photo is provided and that dir_rule exists.
  • Honor global cache by skipping if the target file exists.
  • Rename unused kwargs to _kwargs to satisfy ARG002.
 class DownloadCoverPlugin(JmOptionPlugin):
     plugin_key = 'download-cover'
 
-    def invoke(self,
-               dir_rule: dict,
-               size='',
-               photo: JmPhotoDetail = None,
-               album: JmAlbumDetail = None,
-               downloader=None,
-               **kwargs) -> None:
-        album_id = album.id if album else photo.album_id
-        save_path = self.decide_filepath(
-            album, photo,
-            None, None, None,
-            dir_rule
-        )
-        downloader.client.download_album_cover(album_id, save_path, size)
+    def invoke(self,
+               dir_rule: dict,
+               size='',
+               photo: JmPhotoDetail = None,
+               album: JmAlbumDetail = None,
+               downloader=None,
+               **_kwargs) -> None:
+        self.require_param(album is not None or photo is not None, 'download-cover需在before_album/after_photo上下文运行')
+        self.require_param(isinstance(dir_rule, dict), '必须提供dir_rule配置(包含base_dir与rule)')
+
+        # 构造保存路径
+        save_path = self.decide_filepath(album, photo, None, None, None, dir_rule)
+
+        # 避免重复下载(遵循全局cache设置)
+        if getattr(self.option.download, 'cache', False) and os.path.exists(save_path):
+            self.log(f'封面已存在,跳过: {save_path}')
+            return
+
+        album_id = album.id if album else photo.album_id
+        downloader.client.download_album_cover(album_id, save_path, size)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc3531a and f9fc27e.

📒 Files selected for processing (5)
  • assets/docs/sources/option_file_syntax.md (2 hunks)
  • src/jmcomic/jm_client_interface.py (1 hunks)
  • src/jmcomic/jm_option.py (1 hunks)
  • src/jmcomic/jm_plugin.py (2 hunks)
  • tests/test_jmcomic/test_jm_client.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_jmcomic/test_jm_client.py
🧰 Additional context used
🧬 Code graph analysis (2)
src/jmcomic/jm_plugin.py (2)
src/jmcomic/jm_entity.py (6)
  • JmPhotoDetail (292-451)
  • JmAlbumDetail (454-568)
  • album_id (363-364)
  • id (67-68)
  • id (433-434)
  • id (514-515)
src/jmcomic/jm_client_interface.py (1)
  • download_album_cover (296-302)
src/jmcomic/jm_client_interface.py (1)
src/jmcomic/jm_toolkit.py (1)
  • get_album_cover_url (372-389)
🪛 Ruff (0.13.1)
src/jmcomic/jm_plugin.py

114-114: Optional may be undefined, or defined from star imports

(F405)


114-114: Optional may be undefined, or defined from star imports

(F405)


114-114: Optional may be undefined, or defined from star imports

(F405)


1317-1317: JmPhotoDetail may be undefined, or defined from star imports

(F405)


1318-1318: JmAlbumDetail may be undefined, or defined from star imports

(F405)


1320-1320: Unused method argument: kwargs

(ARG002)

src/jmcomic/jm_client_interface.py

298-298: JmcomicText may be undefined, or defined from star imports

(F405)

🔇 Additional comments (2)
assets/docs/sources/option_file_syntax.md (1)

113-118: LGTM: default rule and “auto Bd” note match implementation.

Doc reflects split_rule_dsl auto-inserting Bd. Keep braces quoted in YAML as shown to avoid parser surprises.

src/jmcomic/jm_plugin.py (1)

114-116: Good change: Optional filename/suffix/base_dir unlocks flexible callers. Also import typing names explicitly.

The type loosening is helpful. To quiet Ruff F405 from star-imports, explicitly import typing names at module top.

Add at the top of this file:

from typing import Optional, Dict, Any, List, Callable

@hect0x7 hect0x7 merged commit 979802c into master Sep 21, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants