Skip to content

fix(poster_renamerr): report only genuine Plex uploads and stop steady-state border churn#356

Merged
chodeus merged 2 commits into
mainfrom
fix/poster-renamerr-plex-upload-hardening
Jul 20, 2026
Merged

fix(poster_renamerr): report only genuine Plex uploads and stop steady-state border churn#356
chodeus merged 2 commits into
mainfrom
fix/poster-renamerr-plex-upload-hardening

Conversation

@chodeus

@chodeus chodeus commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to a full audit of the poster_renamerr → upload-to-Plex path (apply_method == "plex"). The audit confirmed the two-layer skip-unchanged pipeline is sound — a steady-state scheduled run uploads nothing — but surfaced four fixes, all bounded/config-gated:

1. Notifications report genuine uploads only (upload_posters.py, poster_renamerr.py, notification_formatting.py)

The scheduled run notified from the staged set and discarded the uploader's outcome (PosterUploader(...).run() return was unused), so skipped/failed posters — including per-run re-flow retries — appeared in Discord/Notifiarr as uploads on every schedule.

  • The uploader payload now carries uploaded: per-poster records for action == "updated" only (title/year/season/library/instance).
  • The plex path notifies from that (Uploaded to Movies, Movies 4K, Season 02 uploaded to TV); when nothing genuinely uploaded, a one-line heartbeat replaces the poster list: No posters were uploaded (nothing changed). or No posters were uploaded (N failed — check the logs).
  • Kometa path unchanged (there the rename output is the outcome). Mirrors the webhook path's existing outcome-gated pattern (job_processor._handle_post_rename_actions).
  • The formatter now renders artist/album too, so music-only runs no longer misreport as "No files were renamed."

2. Plex border pass restricted to the manifest (border_replacerr.py, poster_renamerr.py)

The plex path ran the border pass with process_all=True, sweeping all matched rows. Layer-A-skipped rows point at prior runs' deleted staging dirs, so every poster was re-decoded/re-encoded into a recreated stale /tmp/chub_poster_plex_* dir that nothing cleans (RAM-backed /tmp on Unraid) — pure waste; nothing uploads from there. New manifest_only flag hard-restricts the plex border pass to this run's manifest, overriding every full-library trigger (process_all, holiday reset_all, missing manifest). Kometa keeps its full-library re-border.

3. Year-guard on the lone live-search candidate (plex.py)

The live title-only fallback accepted a lone candidate with no year check (only the multi-candidate branch was year-tolerant), so a poster could land on a same-title sibling of a clearly different year (a 2018 poster on the 1978 original) when the cache missed. The single-candidate branch now applies the same ±tolerance; a candidate with NO year is kept (a metadata gap must not reject the only candidate — mirrors PlexMediaIndex._disambiguate_by_year).

4. Border-excluded assets get staged unbordered (border_replacerr.py)

With borders enabled the border op is the sole file writer, so an exclusion_list/ignore_folders title never got a staged file — the plex uploader failed "Could not read poster file" on every run and the kometa destination stayed empty. Excluded assets now get a crash-safe unbordered copy staged (exclusion means no border, not no poster). The manifest branch's duplicated inline exclusion checks were consolidated into _asset_excluded.

Also

  • Structural dry_run guard on the hash-equal persist branch in _sync_single_asset (unreachable today, guarded anyway).
  • Collection analogue of the media skip-column persistence test — source_file_hash/uploaded_libraries/file_hash/file_mtime/original_file must survive a collections re-sync upsert or every collection would re-upload each run (previously untested; guards the ON CONFLICT DO UPDATE SET column list).

Behavior changes to note

  • An excluded title now uploads/stages an unbordered poster instead of nothing; on kometa, an old bordered copy at the destination is refreshed to the unbordered source on the next pass.
  • On the plex path, a border-settings/holiday change no longer triggers the (previously useless) full re-encode; posters already in Plex keep their old border until their source changes — pre-existing limitation, now explicit instead of hidden behind wasted CPU.

Test plan

  • ruff check . clean; full pytest green.
  • New tests: notify-builder unit tests (genuine-uploads-only, empty/None shapes), formatter empty_text override + artist/album rendering, lone-hit year-guard (wrong-year rejected / year-less kept), manifest_only overrides process_all e2e, excluded-manifest-asset staged with exact source bytes e2e, collection skip-column persistence across re-sync.

Summary by CodeRabbit

  • New Features

    • Plex poster processing now targets manifest-tracked assets and notification reporting lists only posters that were genuinely uploaded.
    • Excluded posters are still staged/copied (without border work) so uploads can continue smoothly.
    • Discord/Plex notifications now include Artist and Album details and use clearer “nothing changed” messaging when applicable.
  • Bug Fixes

    • Improved Plex matching rejects single title-only hits when the release year conflicts or is malformed.
    • Dry runs no longer perform unnecessary database writes when poster bytes are unchanged.
    • Upload results now report year and season number.
  • Tests

    • Added/expanded coverage for manifest-only behavior, staging of excluded posters, notification payload correctness, and year-matching edge cases.

…y-state border churn

Audit of the plex apply path (apply_method == "plex") confirmed the
skip-unchanged pipeline is sound, but surfaced four fixes:

- Notifications: the scheduled run notified from the STAGED set and
  discarded the uploader's outcome, so skipped/failed posters (and
  per-run re-flow retries) appeared in Discord/Notifiarr as uploads
  every schedule. The uploader now returns a per-poster "uploaded"
  record (action == "updated" only) and the plex path notifies from
  that; when nothing genuinely uploaded a one-line heartbeat replaces
  the poster list ("No posters were uploaded (nothing changed)." /
  "(N failed — check the logs)."). Kometa path unchanged. The
  formatter also renders artist/album now, so music-only runs no
  longer misreport as "No files were renamed."

- Border churn + /tmp leak: the plex path ran the border pass with
  process_all=True, sweeping ALL matched rows. Layer-A-skipped rows
  point at prior runs' deleted staging dirs, so every poster was
  re-encoded into a recreated stale /tmp/chub_poster_plex_* dir that
  nothing cleans (RAM-backed /tmp on Unraid) — pure waste, nothing
  uploads from there. New manifest_only flag hard-restricts the plex
  border pass to this run's manifest, overriding every full-library
  trigger (process_all, holiday reset_all, missing manifest). Kometa
  keeps its full-library re-border.

- Wrong-year live upload: the live title-only fallback accepted a
  LONE candidate with no year check, so a poster could land on a
  same-title sibling of a clearly different year (e.g. a 2018 poster
  on the 1978 original) when the cache missed. The single-candidate
  branch is now year-guarded like the multi-candidate one; a
  candidate with NO year is kept (metadata gap must not reject the
  only candidate, mirroring PlexMediaIndex._disambiguate_by_year).

- Border-excluded assets: with borders enabled the border op is the
  sole file writer, so an excluded title never got a staged file —
  the plex uploader failed "Could not read poster file" on every run
  and the kometa destination stayed empty. Excluded assets now get a
  crash-safe unbordered copy staged (exclusion means no border, not
  no poster).

Also: structural dry-run guard on the hash-equal persist branch, and
a collection analogue of the media skip-column persistence test
(source_file_hash / uploaded_libraries / file_hash / file_mtime /
original_file must survive re-sync or collections would re-upload
every run — previously untested).
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9319dbee-7c54-4292-a4f2-61f0d7c66fcd

📥 Commits

Reviewing files that changed from the base of the PR and between bee0cc1 and 4fc63da.

📒 Files selected for processing (3)
  • backend/util/plex.py
  • tests/test_border_replacerr.py
  • tests/test_plex_connect.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/test_plex_connect.py
  • backend/util/plex.py
  • tests/test_border_replacerr.py

📝 Walkthrough

Walkthrough

Changes

The PR adds manifest-only border processing with atomic staging for excluded posters, reports only genuine uploads for Plex notifications, expands notification formatting, tightens lone Plex title matching by year, and tests collection cache field preservation.

Poster processing and upload reporting

Layer / File(s) Summary
Manifest border staging
backend/modules/border_replacerr.py, tests/test_border_replacerr.py
Adds atomic staging for excluded assets and restricts manifest-only border processing, with forwarding and regression coverage.
Upload result reporting
backend/util/upload_posters.py, backend/modules/poster_renamerr.py, tests/test_poster_renamerr.py
Adds poster metadata and genuine-upload records, then uses those records for Plex notifications.
Notification field formatting
backend/util/notification_formatting.py, tests/test_notification.py
Renders Artist and Album fields and supports custom empty-result text.

Plex matching and cache preservation

Layer / File(s) Summary
Plex title-only year matching
backend/util/plex.py, tests/test_plex_connect.py
Accepts yearless lone candidates and rejects incompatible or malformed candidate years.
Collection cache resync
tests/test_media_cache_upsert.py
Verifies upload-related collection fields survive resynchronization while ARR fields refresh.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PosterRenamerr
  participant BorderReplacerr
  participant UploadPosters
  participant NotificationFormatter
  PosterRenamerr->>BorderReplacerr: Process manifest-only posters
  BorderReplacerr-->>PosterRenamerr: Return staged files
  PosterRenamerr->>UploadPosters: Upload poster files
  UploadPosters-->>PosterRenamerr: Return genuine uploaded records
  PosterRenamerr->>NotificationFormatter: Format upload notification
  NotificationFormatter-->>PosterRenamerr: Return formatted output
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes around Plex upload reporting and reducing border-processing churn.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/poster-renamerr-plex-upload-hardening

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

Comment thread tests/test_border_replacerr.py Fixed
def _subset_harness(tmp_path, monkeypatch, exclusion_list=None):
"""Shared setup for manifest-mode runs: two real source posters (ids 1/2),
a fake DB, notifications stubbed. Returns (br, dest_dir, rows_by_id)."""
import backend.modules.border_replacerr as border_mod

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Declining: the local import backend.modules.border_replacerr as border_mod (monkeypatch target) alongside the top-level from ... import BorderReplacerr is the established pattern in this file — four pre-existing tests do the same. Changing only the new helper would diverge from its siblings; restructuring the file's imports is out of scope here.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
backend/util/plex.py (1)

567-571: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the year-guard comment concise.

This five-line explanation includes rationale that is better represented by the code and tests. Reduce it to one or two navigational lines, such as: # Reject explicit year mismatches; keep yearless candidates.

As per path instructions, comments must be navigational/instructional only (1-2 lines) and avoid why/history essays.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/util/plex.py` around lines 567 - 571, Shorten the year-guard comment
near the lone-candidate logic to one or two navigational lines describing the
behavior: reject explicit year mismatches while retaining yearless candidates.
Remove the examples, rationale, and references to implementation history or
other methods.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/util/plex.py`:
- Around line 572-579: Update the year compatibility logic around lone_year and
year_compatible so only genuinely missing year values bypass validation.
Distinguish empty or absent years from non-empty malformed values, and reject or
skip candidates whose explicit candidate year cannot be converted; preserve the
tolerance comparison for valid numeric years.

---

Nitpick comments:
In `@backend/util/plex.py`:
- Around line 567-571: Shorten the year-guard comment near the lone-candidate
logic to one or two navigational lines describing the behavior: reject explicit
year mismatches while retaining yearless candidates. Remove the examples,
rationale, and references to implementation history or other methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e6ab13a0-6b15-406c-93f8-4d208f6c40b5

📥 Commits

Reviewing files that changed from the base of the PR and between e8820f2 and bee0cc1.

📒 Files selected for processing (10)
  • backend/modules/border_replacerr.py
  • backend/modules/poster_renamerr.py
  • backend/util/notification_formatting.py
  • backend/util/plex.py
  • backend/util/upload_posters.py
  • tests/test_border_replacerr.py
  • tests/test_media_cache_upsert.py
  • tests/test_notification.py
  • tests/test_plex_connect.py
  • tests/test_poster_renamerr.py

Comment thread backend/util/plex.py Outdated
Review follow-ups: a non-empty malformed year on the lone title-only
candidate now rejects it (only genuinely missing years are treated as
a metadata gap), with a test locking it; shortened the year-guard
comment; closed the file handle in the excluded-asset staging test.
@chodeus
chodeus merged commit 687a1b8 into main Jul 20, 2026
11 checks passed
@chodeus
chodeus deleted the fix/poster-renamerr-plex-upload-hardening branch July 20, 2026 17:54
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