Skip to content

upgrades and wants: clear upgrades once they land, one want per item - #4

Merged
Harrsn merged 9 commits into
mainfrom
fix/upgrades-and-wants
Sep 17, 2026
Merged

Harrsn merged 9 commits into
mainfrom
fix/upgrades-and-wants

Conversation

@Harrsn

@Harrsn Harrsn commented Sep 16, 2026

Copy link
Copy Markdown
Owner

What was wrong

F14: upgrades never cleared, so they re-downloaded every 48h.

  • The sorter files an upgrade as Title (Year).mkv / Show - S01E01.mkv, which carries no resolution tag. The scanner ranked that file 0 and kept the old tagged copy (Zootopia 2 2025 1080p TS, Rags.2012.720p…) as "best".
  • The upgrade want therefore never cleared, and every GRAB_RETRY_HOURS the hunter grabbed another copy. After sort: never lose or overwrite data when filing releases #3 those copies get quarantined instead of overwriting, so the loop now fills _failed/ rather than the library, but the downloads never stop.
  • The existing test test_movie_720p_upgrades_and_clears_when_satisfied passes only because it lands the upgrade under a name the sorter never produces.

F14, second half: the hunter never checked that an upgrade was an upgrade.

  • A ["1080p","720p"] profile happily ranks a 720p release for a 720p-owned movie.

F14, episodes: library_episodes kept whichever of two copies the scan walked last, so upgrades flip-flopped.

F19: prod still has the old title-keyed constraint.

  • Prod's wanted table still has UNIQUE(kind, series_id, season, episode, title) (confirmed earlier).
  • Wants were written select-then-insert, so a TMDb retitle or a concurrent reconcile duplicates a want and the item is hunted twice.

F20: movie wants were never unique. Their season/episode are NULL, and SQLite treats NULLs in a UNIQUE constraint as distinct.

What changed

  • Scanner (library.py)

    • Takes quality from the sorter's library_files record (sort: never lose or overwrite data when filing releases #3), falling back to the filename. A record whose size no longer matches is ignored.
    • A row saved with NULL quality is re-read once its record appears.
    • Both movies and episodes now keep the best file deterministically; ties keep the existing row.
    • library_files rows for vanished files are pruned inside the existing mass-disappearance guard.
  • Superseded copies are retired to LIBRARY_ROOT/_superseded/<same relative path>, along with their sidecar subtitles. It's a rename on the same share, reversible, and outside the movies/tvshows sections, with .plexignore/.ignore markers in case a media server points at the root. A file is only moved when:

    • the winner is sorter-recorded with a matching size,
    • the winner is strictly better,
    • both files are in the same folder (collection folders and Fix Location are never touched),
    • the scan didn't trip the prune guard.

    SUPERSEDED_ACTION=keep disables this.

  • Hunter (scheduler.py): upgrade wants only consider releases where quality.is_upgrade() holds against the owned file. A cam of unknown resolution is beaten by any real source with a known resolution. When nothing qualifies, the detail line says so.

  • Wants

    • The new wants.upsert() is the only write path for both reconcilers. It runs under BEGIN IMMEDIATE with INSERT OR IGNORE, so select-then-insert can't race.
    • db._migrate_wanted_key() collapses duplicates (keeping an in-flight grabbed row, then the most recently searched, then the oldest). It then adds ux_wanted_key on (kind, IFNULL(series_id,-1), IFNULL(season,-1), IFNULL(episode,-1)).
    • No table rebuild: the old title-keyed constraint is a superset key, so it never rejects anything the new index allows.
    • Migration failure is logged at ERROR instead of silently swallowed.

Testing

  • tests/test_wants_upgrades.py adds 24 tests. Highlights:
    • F14 end-to-end through the real sorter, for movies, cams and episodes, in both walk orders.
    • The hunter filter for movies, cams and episodes.
    • Prod's exact old wanted schema with duplicates, migrated.
    • 16 threads racing upsert() for an episode and for a movie.
    • Every superseded guard rail.
  • Against unmodified main, 15 of the first 17 tests fail. The two that pass pin behavior that's deliberately unchanged: unknown quality never triggers an upgrade.
  • Full suite: 168 passed (144 + 24) locally on 3.12.

Deploying

  • Back up the DB first. The migration deletes duplicate wants.
  • On first start the dedupe and index run once. Prod had no duplicates at last check, so it should be a no-op apart from the index.
  • Upgrades that land after this deploy will clear and retire the old copy. Movies that already had a sorter-filed upgrade before sort: never lose or overwrite data when filing releases #3 have no quality record, so they stay as they are (no re-download, no move) until another upgrade lands.
  • _superseded/ is created on first use under /library. Purge it by hand whenever you like.

Not in this PR

  • Run lock (F5/F12).
  • Hunter backoff and fetch fixes (F13/F15/F29/F30/F33). F29 also covers the README's "302 to magnet just works" line, which is still false.

Nine commits from API pushes; squash-merge.

@Harrsn
Harrsn merged commit f299ef9 into main Sep 17, 2026
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.

1 participant