Skip to content

fix(downloader): prevent static and dynamic cover collisions - #773

Open
H-TTTTT wants to merge 1 commit into
JoeanAmier:developfrom
H-TTTTT:fix/cover-file-collision
Open

fix(downloader): prevent static and dynamic cover collisions#773
H-TTTTT wants to merge 1 commit into
JoeanAmier:developfrom
H-TTTTT:fix/cover-file-collision

Conversation

@H-TTTTT

@H-TTTTT H-TTTTT commented Jul 12, 2026

Copy link
Copy Markdown

Summary

  • preserve existing static and single-cover filenames
  • use a distinct _dynamic stem when both cover types are downloaded
  • recognize normalized .png, .jpeg, and .webp dynamic outputs on later runs
  • continue honoring legacy dynamic-cover .webp files
  • prevent JPEG Content-Type fallback from overwriting the static cover

Testing

  • deterministic concurrent JPEG fallback regression
  • second-run skip regression
  • single-cover and legacy filename compatibility coverage
  • full test suite: 12 passed
  • changed-file Ruff format and lint: passed

Fixes #420

Summary by Sourcery

调整封面下载逻辑,避免静态封面和动态封面之间的文件名冲突,同时保持现有的命名行为。

Bug Fixes(错误修复):

  • 防止动态 JPEG 回退下载时覆盖已有的静态封面文件。
  • 确保此前已下载的动态封面(包括旧版的 .webp 文件以及规范化后的 PNG/JPEG/WEBP 变体)在后续运行中能够被正确检测并跳过下载。

Enhancements(增强功能):

  • 当同时下载静态和动态封面时,引入独立的 _dynamic 文件名主干,以区分两类输出文件。
  • 增加测试,用于覆盖并发 JPEG 回退行为、第二次运行时的跳过逻辑,以及与旧版和单一封面文件名的兼容性。

Tests(测试):

  • 添加针对封面下载命名、旧版动态封面处理以及并发 JPEG 回退行为的单元测试和集成风格测试。
Original summary in English

Summary by Sourcery

Adjust cover download logic to avoid filename collisions between static and dynamic covers while preserving existing naming behavior.

Bug Fixes:

  • Prevent dynamic JPEG fallback downloads from overwriting existing static cover files.
  • Ensure previously downloaded dynamic covers, including legacy .webp files and normalized PNG/JPEG/WEBP variants, are correctly detected and skipped on subsequent runs.

Enhancements:

  • Introduce a distinct _dynamic filename stem when both static and dynamic covers are downloaded to keep outputs separate.
  • Add tests to cover concurrent JPEG fallback behavior, second-run skipping, and compatibility with legacy and single-cover filenames.

Tests:

  • Add unit and integration-style tests for cover download naming, legacy dynamic cover handling, and concurrent JPEG fallback behavior.

@sourcery-ai

sourcery-ai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

调整封面下载文件名处理,避免静态封面与动态封面之间的冲突,同时保留已有命名方式,并新增关于并发 JPEG 下载与重复运行的回归测试。

封面下载冲突预防的时序图

sequenceDiagram
    participant Downloader
    participant Filesystem

    Downloader->>Downloader: download_cover(item, name, static_suffix, dynamic_suffix)
    Downloader->>Downloader: static_url = item[static_cover]
    Downloader->>Downloader: dynamic_url = item[dynamic_cover]
    Downloader->>Downloader: both_covers = all(static_cover, static_url, dynamic_cover, dynamic_url)

    alt static_cover download
        Downloader->>Filesystem: is_exists(actual_root.with_name(name.static_suffix))
        Filesystem-->>Downloader: false
        Downloader->>Downloader: tasks.append(static_url, temp_static_path, static_path)
    end

    Downloader->>Downloader: legacy_dynamic = actual_root.with_name(name.dynamic_suffix)
    Downloader->>Downloader: typed_dynamic = actual_root.with_name(name_dynamic.dynamic_suffix)
    loop normalized dynamic outputs
        Downloader->>Filesystem: is_exists(typed_dynamic.with_suffix(suffix))
        Filesystem-->>Downloader: exists or not
    end
    Downloader->>Downloader: dynamic_exists = any(is_exists(legacy_dynamic), is_exists(typed_dynamic), normalized_dynamic_exists)
    Downloader->>Downloader: dynamic_path = typed_dynamic if both_covers else legacy_dynamic

    alt dynamic_cover download
        Downloader->>Filesystem: is_exists(legacy_dynamic)
        Filesystem-->>Downloader: false
        Downloader->>Filesystem: is_exists(typed_dynamic)
        Filesystem-->>Downloader: false
        Downloader->>Downloader: tasks.append(dynamic_url, temp_dynamic_path(dynamic_path.name), dynamic_path)
    end
Loading

File-Level Changes

Change Details Files
在保留现有单封面命名方式的同时,防止静态封面和动态封面文件名发生冲突。
  • 为静态和动态封面 URL 引入局部变量,并添加一个布尔标志,用于指示两种类型的封面是否都已启用且存在。
  • 静态封面文件名保持为 <name>.<static_suffix>,当同时下载静态与动态封面时,将 <name>_dynamic.<dynamic_suffix> 作为动态封面的规范文件名。
  • 确保单封面场景仍然产生原有文件名(静态为 work.jpeg,动态为 work.webp)。
src/downloader/download.py
src/testers/test_download.py
识别并遵从不同归一化格式和传统行为下已存在的动态封面文件。
  • 将裸 <name>.<dynamic_suffix> WebP 视为传统动态封面,或者视为归一化为 WebP 的静态封面,从而保留现有行为。
  • 为传统动态输出(<name>.<dynamic_suffix>)与带类型的动态输出(<name>_dynamic.<dynamic_suffix>)定义各自路径。
  • 通过检查任意已存在的 <name>_dynamic.* 图像文件(使用 CONTENT_TYPE_MAP)来计算 normalized_dynamic_exists,当已存在任一被识别的归一化动态封面时,跳过动态下载。
src/downloader/download.py
添加回归测试以覆盖命名、传统行为以及封面下载的并发性。
  • 添加轻量级的 create_downloader 辅助函数和 Recorder stub,用于构造适用于测试的 Downloader
  • 测试单独的静态或动态封面时,输出文件名仍保持原样。
  • 测试含糊的已存在 work.webp 被视为传统动态封面,从而只下载静态 JPEG。
  • 添加一个异步并发测试,用于模拟并发 JPEG 静态/动态下载,断言输出文件彼此不同(work.jpegwork_dynamic.jpeg),并验证第二次运行不会再执行任何下载。
src/testers/test_download.py

Tips and commands

Interacting with Sourcery

  • 触发新的审核: 在 Pull Request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审核评论。
  • 从审核评论生成 GitHub issue: 在某条审核评论下回复,要求 Sourcery 从该评论创建 issue。你也可以直接回复 @sourcery-ai issue 来从该评论创建 issue。
  • 生成 Pull Request 标题: 在 Pull Request 标题中任意位置写入 @sourcery-ai,即可随时生成标题。也可以在 Pull Request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 摘要: 在 Pull Request 正文任意位置写入 @sourcery-ai summary,即可在该位置生成 PR 摘要。也可以在 Pull Request 中评论 @sourcery-ai summary 来在任意时间(重新)生成摘要。
  • 生成审稿人指南: 在 Pull Request 中评论 @sourcery-ai guide,即可在任意时间(重新)生成审稿人指南。
  • 解决所有 Sourcery 评论: 在 Pull Request 中评论 @sourcery-ai resolve,以一次性解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 关闭所有 Sourcery 审核: 在 Pull Request 中评论 @sourcery-ai dismiss,以关闭所有现有的 Sourcery 审核。特别适合在你想从头开始一次新的审核时使用——别忘了再评论 @sourcery-ai review 来触发新的审核!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用某些审核特性,例如 Sourcery 自动生成的 Pull Request 摘要、审稿人指南等。
  • 更改审核语言。
  • 添加、删除或编辑自定义审核指令。
  • 调整其他审核设置。

Getting Help

Original review guide in English

Reviewer's Guide

Adjusts cover-download filename handling to avoid collisions between static and dynamic covers, preserves legacy naming, and adds regression tests around concurrent JPEG downloads and reruns.

Sequence diagram for cover download collision prevention

sequenceDiagram
    participant Downloader
    participant Filesystem

    Downloader->>Downloader: download_cover(item, name, static_suffix, dynamic_suffix)
    Downloader->>Downloader: static_url = item[static_cover]
    Downloader->>Downloader: dynamic_url = item[dynamic_cover]
    Downloader->>Downloader: both_covers = all(static_cover, static_url, dynamic_cover, dynamic_url)

    alt static_cover download
        Downloader->>Filesystem: is_exists(actual_root.with_name(name.static_suffix))
        Filesystem-->>Downloader: false
        Downloader->>Downloader: tasks.append(static_url, temp_static_path, static_path)
    end

    Downloader->>Downloader: legacy_dynamic = actual_root.with_name(name.dynamic_suffix)
    Downloader->>Downloader: typed_dynamic = actual_root.with_name(name_dynamic.dynamic_suffix)
    loop normalized dynamic outputs
        Downloader->>Filesystem: is_exists(typed_dynamic.with_suffix(suffix))
        Filesystem-->>Downloader: exists or not
    end
    Downloader->>Downloader: dynamic_exists = any(is_exists(legacy_dynamic), is_exists(typed_dynamic), normalized_dynamic_exists)
    Downloader->>Downloader: dynamic_path = typed_dynamic if both_covers else legacy_dynamic

    alt dynamic_cover download
        Downloader->>Filesystem: is_exists(legacy_dynamic)
        Filesystem-->>Downloader: false
        Downloader->>Filesystem: is_exists(typed_dynamic)
        Filesystem-->>Downloader: false
        Downloader->>Downloader: tasks.append(dynamic_url, temp_dynamic_path(dynamic_path.name), dynamic_path)
    end
Loading

File-Level Changes

Change Details Files
Prevent static and dynamic cover filename collisions while preserving existing single-cover naming.
  • Introduce local variables for static and dynamic cover URLs and a boolean flag indicating that both cover types are enabled and present.
  • Keep static-cover filenames as <name>.<static_suffix> and use <name>_dynamic.<dynamic_suffix> as the canonical dynamic filename when both covers are downloaded.
  • Ensure single-cover scenarios still produce the original filenames (work.jpeg for static, work.webp for dynamic).
src/downloader/download.py
src/testers/test_download.py
Detect and honor existing dynamic cover files across different normalized formats and legacy behavior.
  • Treat a bare <name>.<dynamic_suffix> WebP as either a legacy dynamic cover or a static cover normalized to WebP, preserving existing behavior.
  • Define separate paths for legacy dynamic (<name>.<dynamic_suffix>) and typed dynamic (<name>_dynamic.<dynamic_suffix>) outputs.
  • Compute normalized_dynamic_exists by checking for any existing <name>_dynamic.* image files using CONTENT_TYPE_MAP, and skip dynamic downloads when any recognized normalized dynamic cover already exists.
src/downloader/download.py
Add regression tests to cover naming, legacy behavior, and concurrency around cover downloads.
  • Add a lightweight create_downloader helper and Recorder stub to construct a Downloader suitable for tests.
  • Test that single static or dynamic covers retain their original output filenames.
  • Test that an ambiguous existing work.webp is treated as a legacy dynamic cover, causing only the static JPEG to be downloaded.
  • Add an asynchronous concurrency test that simulates concurrent JPEG static/dynamic downloads, asserts distinct output files (work.jpeg and work_dynamic.jpeg), and verifies that a second run performs no further downloads.
src/testers/test_download.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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - 我在这里给出一些高层次的反馈:

  • 动态封面存在性相关的逻辑(legacy_dynamictyped_dynamicnormalized_dynamic_existsdynamic_exists)已经有点复杂了;可以考虑把这部分提取到一个小的辅助方法中,以更清晰地区分不同的文件名/规范化场景,并降低 download_cover 中的认知负担。
  • both_covers = all((self.static_cover, static_url, self.dynamic_cover, dynamic_url)) 这个检查在隐式地混合配置开关和 URL 是否存在;可以考虑让这个条件表达得更明确一些(例如,将开关和元数据检查拆开),这样能让意图更清晰,并在条目结构发生变化时避免一些隐蔽的 bug。
给 AI 代理的提示
请根据这次代码审查中的评论进行修改:

## 总体评论
- 动态封面存在性相关的逻辑(`legacy_dynamic``typed_dynamic``normalized_dynamic_exists``dynamic_exists`)已经有点复杂了;可以考虑把这部分提取到一个小的辅助方法中,以更清晰地区分不同的文件名/规范化场景,并降低 `download_cover` 中的认知负担。
- `both_covers = all((self.static_cover, static_url, self.dynamic_cover, dynamic_url))` 这个检查在隐式地混合配置开关和 URL 是否存在;可以考虑让这个条件表达得更明确一些(例如,将开关和元数据检查拆开),这样能让意图更清晰,并在条目结构发生变化时避免一些隐蔽的 bug。

Sourcery 对开源项目是免费的——如果你觉得我们的审查有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进之后的审查。
Original comment in English

Hey - I've left some high level feedback:

  • The dynamic cover existence logic (legacy_dynamic, typed_dynamic, normalized_dynamic_exists, dynamic_exists) is getting dense; consider extracting it into a small helper method to clarify the different filename/normalization cases and reduce cognitive load in download_cover.
  • The both_covers = all((self.static_cover, static_url, self.dynamic_cover, dynamic_url)) check implicitly mixes configuration flags and presence of URLs; consider making this condition more explicit (e.g., separate flags vs. metadata checks) to make the intent clearer and avoid subtle bugs if item structure changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The dynamic cover existence logic (`legacy_dynamic`, `typed_dynamic`, `normalized_dynamic_exists`, `dynamic_exists`) is getting dense; consider extracting it into a small helper method to clarify the different filename/normalization cases and reduce cognitive load in `download_cover`.
- The `both_covers = all((self.static_cover, static_url, self.dynamic_cover, dynamic_url))` check implicitly mixes configuration flags and presence of URLs; consider making this condition more explicit (e.g., separate flags vs. metadata checks) to make the intent clearer and avoid subtle bugs if item structure changes.

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.

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.

1 participant