Skip to content

build: v2.8.0#138

Merged
jasoneri merged 14 commits intoGUIfrom
2.7-dev
Jan 3, 2026
Merged

build: v2.8.0#138
jasoneri merged 14 commits intoGUIfrom
2.7-dev

Conversation

@jasoneri
Copy link
Owner

@jasoneri jasoneri commented Jan 3, 2026

Description

refactor: sv directory structure
upd: Complete metainfo
fix(try): reboot lock

Related Issues

Checklist:

  • Have you checked to ensure there aren't other open Pull Requests for the same update/change?
  • Have you linted your code locally prior to submission?
  • Have you successfully ran app with your changes locally?

Summary by Sourcery

Introduce rV-aware storage and metadata tracking with a new scanning pipeline, update mechanisms, and documentation for the v2.8.0 release.

New Features:

  • Add an rV metadata database (rV.db) with per-episode tracking and a filesystem scan pipeline that understands normal and R18 storage layouts and different episode formats.
  • Introduce configurable post-processing rules via .cgsRule.json to bind a storage directory to a chosen downloaded_handle and validate configuration changes.
  • Enhance ComicInfo.xml generation and CBZ post-processing to better support external readers such as Komga and ComicRack-style tools.
  • Add RVManager and background scan threading to keep rV records in sync and expose a GUI workflow for showing records and scanning local content.
  • Extend site parsers (JM, Wnacg, Ehentai, Hitomi) and Info models to capture richer metadata such as artist, tags, language, type, and publish date for storage in rV.db.
  • Switch the internal updater to tag-based release detection and fetch release notes directly from the repository for use in the GUI.

Bug Fixes:

  • Fix reboot/update lockups by simplifying the in-app update flow to open the changelog/update instructions instead of running an embedded updater script.
  • Resolve edge cases where sections or episode names used sentinel values like 'meaningless' that interfered with path building, de-duplication, or display logic.

Enhancements:

  • Refactor redViewer integration into an rV handler class with strategy-based episode scanning and SQL-backed show_max/delete operations.
  • Rename and clarify SQL helper classes, separating download de-duplication (SqlRecorder) from rV metadata storage (SqlrV).
  • Speed up search result parsing by parallelizing item parsing for multiple supported sites using ThreadPoolExecutor and minor UI responsiveness tweaks.
  • Improve GUI flows around rV tools, record display, and task lifecycle cleanup, including better InfoBar messaging and automatic scans when storage path changes.
  • Adjust R18 storage folder naming and path conventions to align with the new rule and scanning system.

Documentation:

  • Document the new v2.8.0 release with a dedicated changelog entry and surface the changelog prominently in the docs home and updater UI.
  • Update configuration documentation for the new post-processing model, rV.db metadata storage, and storage-directory rules for different downloaded_handle modes.
  • Refresh README and docs to highlight recommended reader integrations (rV, Komga, PicView) and de-emphasize GitHub releases in favor of in-app update info.
  • Add a developer doc describing recommended AI API proxy services and related links.

Chores:

  • Bump the project version and docs version string to 2.8.0 to reflect the new release.
  • Tweak localization strings for the updater, rV tools, and ERO book folder naming to match the new behavior and messaging.

@sourcery-ai
Copy link

sourcery-ai bot commented Jan 3, 2026

Reviewer's Guide

Refactors storage and rV integration around a new per-library rule file and rV.db, adds richer metadata capture and ComicInfo generation, reworks the updater to use tags and docs-hosted notes, and updates GUI/CLI tooling and website parsers accordingly for v2.8.0.

Sequence diagram for RV scan and episode sync

sequenceDiagram
    actor User
    participant rvTool as rvTool
    participant RVManager as RVManager
    participant RvThread as RvThread
    participant Handler as Handler
    participant SqlrV as SqlrV
    participant FS as FileSystem
    participant CgsRuleMgr as CgsRuleMgr
    participant SpiderGUI as SpiderGUI

    User->>rvTool: click Scan Local
    rvTool->>RVManager: start_scan(show_progress=True parent_widget=self pos=infobar_pos)

    RVManager->>RVManager: check existing scan_thread
    RVManager->>RvThread: create(gui, show_progress=True)
    RVManager->>RvThread: connect signals
    RVManager->>RvThread: start()

    RvThread->>RVManager: scan_progress("backend scaning local...")
    RVManager->>User: InfoBar.info("backend scaning local...")

    RvThread->>Handler: scan(conf, init=False)
    activate Handler
    Handler->>CgsRuleMgr: get_download_handle(conf)
    CgsRuleMgr-->>Handler: downloaded_handle
    Handler->>Handler: _scan_filesystem(conf.sv_path)
    Handler->>FS: iterate sv_path
    FS-->>Handler: book and episode paths
    Handler->>Handler: strategy.should_scan_as_episode(item)
    Handler->>Handler: strategy.get_episode_name(item)
    Handler-->>RvThread: fs_episodes set

    RvThread->>SqlrV: __enter__()
    activate SqlrV
    SqlrV->>SqlrV: connect(), create tables if needed

    RvThread->>SqlrV: reset_exist() when init True (optional)
    RvThread->>SqlrV: sync_episodes(fs_episodes)
    SqlrV->>SqlrV: get_episodes()
    SqlrV->>SqlrV: diff FS vs DB
    SqlrV->>SqlrV: batch_write_episodes(updates)
    SqlrV-->>RvThread: done

    RvThread->>SqlrV: __exit__()
    SqlrV->>SqlrV: close()
    deactivate SqlrV

    Handler-->>RvThread: total=len(fs_episodes)
    deactivate Handler

    RvThread->>RVManager: scan_completed(total)
    RVManager->>SpiderGUI: reset gui.bsm=None
    RVManager->>User: InfoBar.success or InfoBar.warning "scaned: total books/epsiodes"

    RvThread-->>RVManager: thread finished
Loading

Class diagram for new rV scanning and storage rule system

classDiagram
    class Handler {
        +ero: int|None
        +strategy: ScanStrategy
        +__init__(ero=None)
        +sql() SqlrV
        +scan(_conf, init: bool) int
        +_scan_filesystem(sv_path: pathlib_Path) set
        +_scan_normal_comics(sv_path: pathlib_Path) set
        +_scan_ero_books(sv_path: pathlib_Path) set
        +show_max() Dict~str,BookShow~
        +delete_record(bn: str)
    }

    class ScanStrategy {
        <<abstract>>
        +should_scan_as_episode(item: pathlib_Path) bool
        +get_episode_name(item: pathlib_Path) str
    }

    class FolderStrategy {
        +should_scan_as_episode(item: pathlib_Path) bool
        +get_episode_name(item: pathlib_Path) str
    }

    class CbzStrategy {
        +should_scan_as_episode(item: pathlib_Path) bool
        +get_episode_name(item: pathlib_Path) str
    }

    class ScanStrategyFactory {
        +_strategies: dict
        +create(downloaded_handle: str) ScanStrategy
    }

    class BookShow {
        +name: str
        +show_max: str
        +dl_max: str
        +show() str
    }

    class SqlRecorder {
        +init_flag: bool
        +__init__()
        +check_dupe(taskid: str) bool
        +batch_check_dupe(taskids: list) list
        +add(taskid: str)
        +close()
    }

    class SqlrV {
        +init_flag: bool
        +db: pathlib_Path
        +written_meta: set
        +meta_tb: str
        +eps_tb: str
        +ero: int|None
        +__init__(ero: int|None)
        +__enter__() SqlrV
        +__exit__(exc_type, exc_val, exc_tb) bool
        +connect() SqlrV
        +table_exists() bool
        +create()
        +write_meta(**sql_kw)
        +write_episode(book: str, ep: str, exist: int)
        +batch_write_episodes(episodes: list)
        +get_episodes(book: str) list
        +delete_episodes(book: str)
        +reset_exist()
        +sync_episodes(fs_episodes: set)
        +close()
    }

    class CgsRuleMgr {
        +RULE_FILENAME: str
        +_exist_flags: dict
        +_cached_rules: dict
        +_get_rule_file(sv_path: Path) Path
        +exists(sv_path: Path) bool
        +create(sv_path: Path, conf_dh: str) bool
        +get_download_handle(_conf) str
        +validate(sv_path: Path, new_handle: str) Tuple~bool,str~
    }

    class RVManager {
        +pos: InfoBarPosition
        +gui
        +scan_thread: RvThread
        +__init__(gui)
        +start_scan(show_progress: bool)
        +_show_scan_progress(message: str)
        +_on_scan_completed(total: int)
        +stop_scan()
    }

    class RvThread {
        +scan_completed: pyqtSignal
        +scan_progress: pyqtSignal
        +gui
        +show_progress: bool
        +__init__(gui, show_progress: bool)
        +run()
    }

    class SpiderGUI {
        +rv_tools: Handler
        +rv_mgr: RVManager
        +say_show_max()
    }

    class rvTool {
        +gui
        +infobar_pos: InfoBarPosition
        +show_max()
        +rv_scan()
        +run()
    }

    Handler --> ScanStrategy : uses
    Handler --> SqlrV : uses
    Handler --> BookShow : builds
    ScanStrategy <|-- FolderStrategy
    ScanStrategy <|-- CbzStrategy
    ScanStrategyFactory --> ScanStrategy : creates
    ScanStrategyFactory --> FolderStrategy
    ScanStrategyFactory --> CbzStrategy

    SpiderGUI --> Handler : rv_tools
    SpiderGUI --> RVManager : rv_mgr
    rvTool --> Handler : gui.rv_tools
    rvTool --> RVManager : gui.rv_mgr

    RVManager --> RvThread : creates
    RvThread --> Handler : gui.rv_tools.scan
    RvThread --> SqlrV : via Handler.sql

    SqlrV --> CgsRuleMgr : uses get_download_handle via Handler
    CgsRuleMgr ..> Handler : configures scan rules

    SqlRecorder <.. SpiderGUI : used for dedupe
    SqlRecorder <.. BaseComicSpider : record downloads
Loading

Class diagram for updated metadata and ComicInfo generation

classDiagram
    class MetaMixin {
    }

    class BookInfo {
        +name: str
        +artist: str|None
        +source: str
        +preview_url: str
        +public_date: str|None
        +tags: list
        +pages: int|str|None
        +btype: str|None
        +to_sql() dict
        +clip_info()
        +to_tasks_obj() TasksObj
        +id_and_md5() tuple
    }

    class Episode {
        +from_book: BookInfo
        +id: str
        +idx: int
        +url: str
        +name: str|None
        +pages: int|str|None
        +id_and_md5() tuple
    }

    class TasksObj {
        +taskid: str
        +title: str
        +tasks_count: int
        +title_url: str|None
        +episode_name: str|None
        +display_title: str
    }

    class ComicInfo {
        +file: str
        +is_ep: bool
        +title: str
        +series: str
        +number: str|None
        +pages: int|None
        +preview_url: str
        +artist: str|None
        +tags: list
        +btype: list|None
        +source: str
        +public_date: str|None
        +year: str|None
        +month: str|None
        +day: str|None
        +language_iso: str
        +_extra_fields: dict
        +__init__(info: BookInfo|Episode)
        +_extract_series_name(name: str) str
        +_extract_number_from_episode_name(episode_name: str) str|None
        +_parse_date()
        +_parse_language() str
        +add(tag: str, value: str|None) ComicInfo
        +content: str
        +fin_callback(_p: pathlib_Path)
    }

    class Blank {
    }

    class ComicspiderItem {
        +get_group_infos(resp_meta: dict) dict
    }

    class SpiderImagesPipeline {
        +file_path(request, response, info, item) str
        +file_folder(basepath, section, spider, title, item) str
        +page_naming(taskid, page, info) str
    }

    class create_cbz_func {
        +create_cbz(src: pathlib_Path, is_ep: bool)
    }

    BookInfo <|-- Episode : from_book
    MetaMixin <|-- ComicInfo
    MetaMixin <|-- Blank

    ComicInfo --> BookInfo : init with
    ComicInfo --> Episode : init with

    BookInfo --> TasksObj : to_tasks_obj
    TasksObj --> ComicInfo : meta_info in TasksObj

    create_cbz_func ..> ComicInfo : used in fin_callback

    SpiderImagesPipeline --> ComicspiderItem : uses group infos
    SpiderImagesPipeline --> create_cbz_func : calls
    SpiderImagesPipeline --> ComicInfo : sv_meta_in/fin_callback

    ComicspiderItem --> BookInfo : title
    ComicspiderItem --> Episode : section may be None
Loading

File-Level Changes

Change Details Files
Introduce structured rV integration with a dedicated SQLite schema, filesystem scanner, and GUI management for reading/downloading records.
  • Add SqlrV with metainfos and episodes tables, CRUD helpers, and filesystem sync logic based on exist flags and ero type.
  • Replace legacy text-record-based redViewer tools with a Handler class that scans normal and ero comics using pluggable scan strategies and reads/deletes episode records from SqlrV.
  • Wire rV tools into the GUI via SpiderGUI.rv_tools, RVManager, and RvThread to support background scans, InfoBar feedback, and per-website ero switching.
utils/sql/__init__.py
utils/redViewer_tools.py
GUI/gui.py
GUI/tools/rv_tool.py
GUI/manager/rv.py
GUI/thread/__init__.py
GUI/uic/qfluent/components/cust.py
Refine download storage layout and enforce per-library post-processing rules via a .cgsRule.json manager.
  • Rename ero book folder constants to begin with an underscore and adjust all affected paths for SPECIAL sites and tests.
  • Introduce CgsRuleMgr to create, cache, and validate .cgsRule.json per sv_path, and use it in the pipeline and config dialog to initialize rules and block incompatible downloaded_handle changes.
  • Trigger automatic rV scans on storage-path or handle changes and surface validation errors via InfoBars.
assets/res/locale/en_US.yml
assets/res/locale/zh_CN.yml
ComicSpider/pipelines.py
utils/config/rule.py
GUI/conf_dialog.py
ComicSpider/spiders/basecomicspider.py
utils/script/extra.py
Capture and persist richer metadata from source sites and generate better ComicInfo.xml and CBZ output for both books and episodes.
  • Enhance website parsers (jm, wnacg, ehentai, hitomi) to extract artist, tags, btype, public_date, language, and pages more accurately and in some cases with parallel parsing for performance.
  • Extend BookInfo/Episode structures with to_sql(), nullable episode names, and cleaner display_title/clip_info semantics tied into SqlrV.write_meta and write_episode.
  • Refactor ComicInfo to distinguish book vs episode, emit additional fields (btype/Genre, Tags, preview_url), and update CBZ creation to optionally keep a folder around non-episode books.
utils/meta/__init__.py
utils/website/ins.py
utils/website/info.py
ComicSpider/spiders/ehentai.py
ComicSpider/spiders/jm.py
ComicSpider/spiders/wnacg.py
ComicSpider/spiders/hitomi.py
ComicSpider/items.py
utils/core.py
ComicSpider/spiders/basecomicspider.py
Rework update-checking to rely on Git tags and docs-hosted release notes, and simplify the in-app update UX.
  • Extend GitHandler with tag listing and raw release_notes.md fetching by commit SHA, and update Proj.check to compare the local version with the latest tag instead of separate stable/dev releases.
  • Adjust Updater to open the hosted changelog URL for updates, tweak InfoBar messages, and re-enable ProjUpdateThread-based checking from the config dialog.
  • Update localized strings to reflect the new update flow and rename the update confirmation text to describe viewing update instructions.
deploy/update.py
GUI/manager/__init__.py
GUI/conf_dialog.py
assets/res/locale/en_US.yml
assets/res/locale/zh_CN.yml
Update documentation, versioning, and README to describe v2.8.0 behavior and new integrations.
  • Add a v2.8.0 entry and collapsible historical sections to the changelog, plus a new link in the docs index navbar.
  • Document the new post-processing and rV.db behavior, and add a dev/link page with external resources.
  • Bump project/version constants to 2.8.0 and adjust README’s update and reader integration sections to highlight rV, Komga, and PicView.
docs/changelog/history.md
docs/index.md
docs/config/index.md
docs/_github/release_notes.md
docs/.vitepress/config.ts
docs/dev/link.md
README.md
variables/__init__.py
pyproject.toml
Miscellaneous UX and robustness tweaks including reboot behavior, search parsing speedups, and CLI fixes.
  • Use ThreadPoolExecutor to parallelize parsing of search results for several spiders and slightly reduce sleeps in GUI fetch threads.
  • Ensure preview_url is consistently set on parsed BookInfo instances, and adjust WorkThread flags and GUI cleanup paths for better shutdown behavior.
  • Fix crawl_only imports to use the shared select helper and tidy various logging and InfoBar usages.
GUI/thread/__init__.py
ComicSpider/spiders/basecomicspider.py
ComicSpider/spiders/ehentai.py
ComicSpider/spiders/jm.py
ComicSpider/spiders/wnacg.py
ComicSpider/spiders/hitomi.py
GUI/gui.py
crawl_only.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@jasoneri jasoneri requested a review from jsonmaki January 3, 2026 06:20
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 8 security issues, 7 other issues, and left some high level feedback:

Security issues:

  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)
  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)
  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)
  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)
  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)
  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)
  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)
  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)

General comments:

  • The CgsRuleMgr.create implementation looks fragile: it monkey‑patches create on first call and caches _exist_flags using str(sv_path) while exists() caches by Path, which means later exists() calls for the same path won’t see the True flag; consider removing the self‑reassignment and normalizing the key type (e.g., always Path.resolve() or always str) consistently across exists/create/validate/get_download_handle.
  • In CgsRuleMgr.validate, when the .cgsRule.json file cannot be read you silently return (True, ""), which hides corruption or partial writes; it would be safer to either surface a warning/InfoBar or force re‑creation of the rule file so users understand why validation is effectively bypassed.
  • The SqlrV.get_meta method currently contains only a triple‑quoted string with example code and returns None at runtime; if it’s meant to be a stub, consider either implementing it or clearly marking it pass with a docstring/comment so it doesn’t look like a partially commented‑out implementation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `CgsRuleMgr.create` implementation looks fragile: it monkey‑patches `create` on first call and caches `_exist_flags` using `str(sv_path)` while `exists()` caches by `Path`, which means later `exists()` calls for the same path won’t see the `True` flag; consider removing the self‑reassignment and normalizing the key type (e.g., always `Path.resolve()` or always `str`) consistently across `exists/create/validate/get_download_handle`.
- In `CgsRuleMgr.validate`, when the `.cgsRule.json` file cannot be read you silently return `(True, "")`, which hides corruption or partial writes; it would be safer to either surface a warning/InfoBar or force re‑creation of the rule file so users understand why validation is effectively bypassed.
- The `SqlrV.get_meta` method currently contains only a triple‑quoted string with example code and returns `None` at runtime; if it’s meant to be a stub, consider either implementing it or clearly marking it `pass` with a docstring/comment so it doesn’t look like a partially commented‑out implementation.

## Individual Comments

### Comment 1
<location> `utils/config/rule.py:18-23` </location>
<code_context>
+        return sv_path / cls.RULE_FILENAME
+
+    @classmethod
+    def exists(cls, sv_path: Path) -> bool:
+        if sv_path in cls._exist_flags:
+            return cls._exist_flags[sv_path]
+        result = cls._get_rule_file(sv_path).exists()
+        cls._exist_flags[sv_path] = result
+        return result
+
+    @classmethod
</code_context>

<issue_to_address>
**issue (bug_risk):** Inconsistent cache keys for `sv_path` can cause `exists()` and rule caching to misbehave

`exists()` uses the `Path` object as the cache key for `_exist_flags`, while `create()` and `get_download_handle()`/`validate()` use `str(sv_path)` for `_cached_rules`. This desynchronizes the caches, so `exists()` can return `False` even after a rule is created and cached, or vice versa. Please standardize on a single key type for both caches (either `Path` or `str(sv_path)`).
</issue_to_address>

### Comment 2
<location> `utils/config/rule.py:26-35` </location>
<code_context>
+        return result
+
+    @classmethod
+    def create(cls, sv_path: Path, conf_dh) -> bool:
+        if cls.exists(sv_path):
+            cls.create = lambda _, __: None
+            return False
+        try:
+            rule_data = {"downloaded_handle": conf_dh}
+            with open(cls._get_rule_file(sv_path), 'w', encoding='utf-8') as f:
+                json.dump(rule_data, f, ensure_ascii=False, indent=2)
+            key = str(sv_path)
+            cls._exist_flags[key] = True
+            cls._cached_rules[key] = rule_data
+            return True
+        except Exception:
+            return False
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Overwriting `CgsRuleMgr.create` at runtime is fragile and can break future calls

Rebinding `cls.create` to a no-op inside `create()` mutates the class globally, so any later `CgsRuleMgr.create()` call (even for a different `sv_path`) will silently do nothing. Instead, just return `False` when the rule file exists and rely on `exists()` and its cache for the fast path, without changing the method itself.
</issue_to_address>

### Comment 3
<location> `GUI/manager/rv.py:39-40` </location>
<code_context>
+        self.gui.log.info(f"scaned: {total} episodes scanned")
+        self.gui.bsm = None
+        parent = show_kws.get("parent_widget", self.gui.textBrowser)
+        info_what = InfoBar.success if total else InfoBar.warning
+        info_what(title='', content=f'scaned: {total} books/epsiodes',
+            position=show_kws['pos'], duration=3000, parent=parent)
+
</code_context>

<issue_to_address>
**nitpick (typo):** User-facing scan completion message has typos and might confuse users

`scaned` and `epsiodes` are misspelled in this user-visible message. Please correct the wording (for example: `f'Scanned: {total} books/episodes'`) and ensure it’s localized if needed.

Suggested implementation:

```python
    def _on_scan_completed(self, total: int, **show_kws):
        # Log a clear, correctly spelled message for developers
        self.gui.log.info(f"Scanned: {total} episodes")
        self.gui.bsm = None
        parent = show_kws.get("parent_widget", self.gui.textBrowser)

```

In the same file (`GUI/manager/rv.py`), update the user-facing `InfoBar` completion message that currently uses the typoed string:
- Replace any `content=f'scaned: {total} books/epsiodes'` with a correctly spelled and clearer variant, for example:
  - `content=self.tr(f'Scanned: {total} books/episodes')` if you already use Qt localization (`self.tr`) elsewhere, or
  - `content=f'Scanned: {total} books/episodes'` if no localization mechanism is in place.
Also check for any other occurrences of `scaned` or `epsiodes` in this file (or related GUI scan code) and correct them similarly.
</issue_to_address>

### Comment 4
<location> `docs/config/index.md:75` </location>
<code_context>
-::: tip 了解其他热门元数据标准的可参与开发,或 [提需求(详细描述)](https://github.com/jasoneri/ComicGUISpider/issues/new?template=feature-request.yml)  
+1. 为区分 🔞 存储路径,要想无感使用 Komga 等框架只能分开存储路径下载  
+2. 同一存储目录混放不同后处理时,会导致 rV/CGS 扫描异常  
+3. 注意不同获取方式的元数据字段可能不同,搜索接口的一般没有书页接口(例如读剪贴板/车号)的全
 :::

</code_context>

<issue_to_address>
**issue (typo):** 句尾“的全”语义不完整,建议补全表述。

结尾“的全”读起来像是少了个词,建议改成类似“没有书页接口(例如读剪贴板/车号)的那么全”,语义会更完整自然。

Suggested implementation:

```
1. 为区分 🔞 存储路径,要想无感使用 Komga 等框架只能分开存储路径下载  
2. 同一存储目录混放不同后处理时,会导致 rV/CGS 扫描异常  
3. 注意不同获取方式的元数据字段可能不同,搜索接口的一般没有书页接口(例如读剪贴板/车号)的那么全  

```

如果该段落在文档中还有英文版本或重复描述的位置,请同步将对应位置的句尾从“的全”改为“的那么全”,保持多语言文档的一致性。
</issue_to_address>

### Comment 5
<location> `README.md:99` </location>
<code_context>
+  </tr>
+  <tr>
+    <td>rV, 自用<br>全面无感适配 CGS<br><s>CGS 为它服务</s></td>
+    <td>komga/ComicRack系<br>需后处理设<code>.czb</code></td>
+    <td>PicView<br>图片管理器, 但用来操作子目录图片<br>或是<code>.cbz</code>都是不错选择</td>
+  </tr>
</code_context>

<issue_to_address>
**issue (typo):** 疑似将“.cbz”误写为“.czb”。

上文关于后处理和阅读器始终用的是 `.cbz`,这里写成 `.czb` 应该是笔误。建议改为 `.cbz`,保持扩展名一致,避免用户配置出错。

```suggestion
    <td>komga/ComicRack系<br>需后处理设<code>.cbz</code></td>
```
</issue_to_address>

### Comment 6
<location> `docs/index.md:97-99` </location>
<code_context>
+  </tr>
+  <tr>
+    <td>rV, 自用<br>全面无感适配 CGS<br><s>CGS 为它服务</s></td>
+    <td>komga/ComicRack系<br>需后处理设<code>.czb</code></td>
+    <td>PicView<br>图片管理器, 但用来操作子目录图片<br>或是<code>.cbz</code>都是不错选择</td>
+  </tr>
</code_context>

<issue_to_address>
**issue (typo):** 这里的“.czb”扩展名与其他文档不一致,可能是“.cbz”的笔误。

与 README 中相同位置的说明相比,这里应当也是指 `.cbz` 格式。如无特殊含义,请改为 `.cbz` 以保持一致。

```suggestion
    <td>rV, 自用<br>全面无感适配 CGS<br><s>CGS 为它服务</s></td>
    <td>komga/ComicRack系<br>需后处理设<code>.cbz</code></td>
    <td>PicView<br>图片管理器, 但用来操作子目录图片<br>或是<code>.cbz</code>都是不错选择</td>
```
</issue_to_address>

### Comment 7
<location> `docs/dev/link.md:20` </location>
<code_context>
+## 其他
+
+- [🎅LDC 投喂](https://credit.linux.do/paying/online?token=26af47ee90b1192086095d107dc9bc1ca4137bd12496fefaf22efadcc349a98a)  
+- [学英语浏览器插件](Ries.ai?c=Jzva)
</code_context>

<issue_to_address>
**issue (bug_risk):** Ries 链接缺少协议,可能会被当作相对路径处理。

如果是外部站点,请改成完整地址,例如 `https://Ries.ai?c=Jzva`,以避免在不同渲染环境中被错误解析为相对链接。
</issue_to_address>

### Comment 8
<location> `utils/sql/__init__.py:127` </location>
<code_context>
        self.cursor.execute(meta_tb_sql)
</code_context>

<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

### Comment 9
<location> `utils/sql/__init__.py:128` </location>
<code_context>
        self.cursor.execute(eps_tb_sql)
</code_context>

<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

### Comment 10
<location> `utils/sql/__init__.py:145-148` </location>
<code_context>
        self.cursor.execute(sql, (
            book_md5, book_name,
            sql_kw.get('artist'), sql_kw.get('source'), sql_kw.get('preview_url'), sql_kw.get('public_date'),
            tags_str, sql_kw.get('pages'), sql_kw.get('btype'), self.ero))
</code_context>

<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

### Comment 11
<location> `utils/sql/__init__.py:159` </location>
<code_context>
        self.cursor.execute(sql, (set_author_ahead(book), ep or '', exist, None, self.ero))
</code_context>

<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

### Comment 12
<location> `utils/sql/__init__.py:204` </location>
<code_context>
        self.cursor.execute(sql, params)
</code_context>

<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

### Comment 13
<location> `utils/sql/__init__.py:209` </location>
<code_context>
        self.cursor.execute(sql, (book,))
</code_context>

<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

### Comment 14
<location> `utils/sql/__init__.py:215` </location>
<code_context>
            self.cursor.execute(sql, (self.ero,))
</code_context>

<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

### Comment 15
<location> `utils/sql/__init__.py:218` </location>
<code_context>
            self.cursor.execute(sql)
</code_context>

<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +39 to +40
info_what = InfoBar.success if total else InfoBar.warning
info_what(title='', content=f'scaned: {total} books/epsiodes',
Copy link

Choose a reason for hiding this comment

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

nitpick (typo): User-facing scan completion message has typos and might confuse users

scaned and epsiodes are misspelled in this user-visible message. Please correct the wording (for example: f'Scanned: {total} books/episodes') and ensure it’s localized if needed.

Suggested implementation:

    def _on_scan_completed(self, total: int, **show_kws):
        # Log a clear, correctly spelled message for developers
        self.gui.log.info(f"Scanned: {total} episodes")
        self.gui.bsm = None
        parent = show_kws.get("parent_widget", self.gui.textBrowser)

In the same file (GUI/manager/rv.py), update the user-facing InfoBar completion message that currently uses the typoed string:

  • Replace any content=f'scaned: {total} books/epsiodes' with a correctly spelled and clearer variant, for example:
    • content=self.tr(f'Scanned: {total} books/episodes') if you already use Qt localization (self.tr) elsewhere, or
    • content=f'Scanned: {total} books/episodes' if no localization mechanism is in place.
      Also check for any other occurrences of scaned or epsiodes in this file (or related GUI scan code) and correct them similarly.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jan 3, 2026

Deploying comicguispider-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6fb5f15
Status: ✅  Deploy successful!
Preview URL: https://58d4e258.comicguispider-docs.pages.dev
Branch Preview URL: https://2-7-dev.comicguispider-docs.pages.dev

View logs

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

New security issues found

UNIQUE(`book`, `ep`)
);'''

self.cursor.execute(meta_tb_sql)
Copy link

Choose a reason for hiding this comment

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

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

);'''

self.cursor.execute(meta_tb_sql)
self.cursor.execute(eps_tb_sql)
Copy link

Choose a reason for hiding this comment

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

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

Comment on lines +145 to +148
self.cursor.execute(sql, (
book_md5, book_name,
sql_kw.get('artist'), sql_kw.get('source'), sql_kw.get('preview_url'), sql_kw.get('public_date'),
tags_str, sql_kw.get('pages'), sql_kw.get('btype'), self.ero))
Copy link

Choose a reason for hiding this comment

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

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

exist = excluded.exist,
rv_handle = excluded.rv_handle,
ero = excluded.ero;'''
self.cursor.execute(sql, (set_author_ahead(book), ep or '', exist, None, self.ero))
Copy link

Choose a reason for hiding this comment

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

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

FROM {self.eps_tb} {where_clause}
ORDER BY book, ep;'''

self.cursor.execute(sql, params)
Copy link

Choose a reason for hiding this comment

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

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep


def delete_episodes(self, book: str):
sql = f'''DELETE FROM {self.eps_tb} WHERE book = ?;'''
self.cursor.execute(sql, (book,))
Copy link

Choose a reason for hiding this comment

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

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

def reset_exist(self):
if self.ero is not None:
sql = f'''UPDATE {self.eps_tb} SET exist = 0 WHERE ero = ?;'''
self.cursor.execute(sql, (self.ero,))
Copy link

Choose a reason for hiding this comment

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

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

self.cursor.execute(sql, (self.ero,))
else:
sql = f'''UPDATE {self.eps_tb} SET exist = 0;'''
self.cursor.execute(sql)
Copy link

Choose a reason for hiding this comment

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

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

@jasoneri jasoneri merged commit 0514f1d into GUI Jan 3, 2026
2 of 3 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