Skip to content

Fixed: Movie file quality missing from the paged movie/scene index - #326

Merged
plz12345 merged 2 commits into
eros-developfrom
fix/paged-index-movie-file-quality
Jul 17, 2026
Merged

Fixed: Movie file quality missing from the paged movie/scene index#326
plz12345 merged 2 commits into
eros-developfrom
fix/paged-index-movie-file-quality

Conversation

@plz12345

Copy link
Copy Markdown
Contributor

Fixes Whisparr/Whisparr#1104

The index status bar showed a generic "Downloaded" instead of the file's quality. Radarr shows the quality, and this used to as well.

What was actually wrong

The issue suspected an expensive movie.moviefileid.quality.id relationship walk needing an in-memory movie-file cache. It turned out to be neither expensive nor a frontend gap:

  • The frontend was already complete. Table, poster and overview — for both scenes and movies — already render movieFile.quality.quality.name with ?? translate('Downloaded') as the fallback. That fallback is the reported bug. No frontend changes here.
  • The JOIN already existed. PagedBuilder already LEFT JOINs MovieFiles. But QueryJoined builds its SELECT from its type parameters, so PagedQuery's <Movie, MovieMetadata> emitted only Movies.* and MovieMetadata.* — the file was joined and then discarded. Movie.MovieFile is a plain property rather than LazyLoaded, so nothing back-filled it and ToResource's null-conditional dropped it.

The change

GetPaged now maps MovieFile through its own query. Two constraints shaped it, and both are worth knowing before simplifying this:

  1. PagedQuery is deliberately left alone. It is shared with MoviesWithoutFiles and MoviesWhereCutoffUnmet, which GROUP BY Movies.Id. Selecting a non-aggregated file column under that GROUP BY errors on PostgreSQL (column "MovieFiles.Id" must appear in the GROUP BY clause) — SQLite tolerates it, so this would not surface in local testing. Neither Wanted page needs the file: their rows already fetch it via useSingleMovieFile.
  2. QualityProfile must be hydrated. MovieFileResource.ToResource dereferences movie.QualityProfile unconditionally to compute QualityCutoffNotMet, and UpgradableSpecification.QualityCutoffNotMet dereferences profile.UpgradeAllowed on its first line. Hydrating MovieFile without a profile throws NullReferenceException (verified) — latent today only because MovieFile is always null. The profile comes from the repository with a fallback, the way All() already handles movies pointing at a profile that no longer exists.

MediaInfo is excluded from the query. Measured against a real library it stores the full ffprobe dump at ~7KB a row, ~90% of it rawStreamData, which MediaInfoResource discards anyway and no index view renders. Including it would deserialize ~7MB per request on a page of 1000. Consequence: /movie/paged returns movieFile.mediaInfo = null while /movie still populates it.

The column list is derived from the mapper rather than hardcoded, so a new MovieFiles column can't silently go missing, and Id is forced to lead the segment because Dapper splits on the first Id column.

Second commit

POST /movie/paged took request.PageSize verbatim, so a client could request any number of records. The tag-filter branch of this same endpoint, StudioController and PerformerController all already guard theirs. Kept as a separate commit — it's a pre-existing gap, not part of the quality fix.

Uses <= 1000 rather than the < 1000 those three use: the index page size options allow 1000 inclusive, so the exclusive form would quietly serve 10 records to anyone picking the documented maximum. Behavior change for any client passing a page size above 1000 (now falls back to 10, matching the existing guards).

Testing

  • 7 repository tests covering quality hydration, profile hydration, the missing-profile fallback, MediaInfo exclusion, and null-file. 4 of them fail without the fix.
  • A MoviesWithoutFiles test pinning constraint 1 — that path had no coverage at all.
  • 4 MovieResource mapping tests covering constraint 2, which the repository tests can't reach.
  • Verified end-to-end against a real library: /movie/paged returns the file's quality with qualityCutoffNotMet computed and mediaInfo: null; Wanted/Missing and Cutoff Unmet still return records; 1000 records served in ~48ms. Quality confirmed rendering on the index in the UI.

Not verified: the Postgres GROUP BY hazard is reasoned from the shared-PagedQuery structure and avoided by construction rather than reproduced — SQLite won't surface it.

plz12345 added 2 commits July 15, 2026 22:52
The index status bar showed a generic "Downloaded" instead of the file's
quality. Every index view (table, poster, overview, for both scenes and
movies) already renders movieFile.quality.quality.name and falls back to
"Downloaded" when it is absent -- the paged API simply never sent it.

PagedBuilder already LEFT JOINs MovieFiles, but QueryJoined builds its
SELECT from its type parameters, so PagedQuery's <Movie, MovieMetadata>
emitted only Movies.* and MovieMetadata.*. Movie.MovieFile is a plain
property rather than LazyLoaded, so nothing back-filled it and
ToResource's null-conditional dropped the file.

GetPaged now maps MovieFile via its own query. Two constraints shaped it:

- PagedQuery is shared with MoviesWithoutFiles and MoviesWhereCutoffUnmet,
  which GROUP BY Movies.Id. Selecting a non-aggregated file column under
  that GROUP BY errors on Postgres, so those keep using PagedQuery and only
  GetPaged changes. Neither needs the file: their rows fetch it separately.
- MovieFileResource.ToResource dereferences Movie.QualityProfile to compute
  QualityCutoffNotMet, so hydrating MovieFile without a profile would throw.
  The profile is hydrated from the repository, falling back the way All()
  does when a movie points at a profile that no longer exists.

MediaInfo is left out of the query: it stores the full ffprobe dump
(~7KB a row, measured) that no index view renders, and it would otherwise
be deserialized for every row on a page of up to 1000.

Fixes Whisparr/Whisparr#1104
POST /movie/paged took request.PageSize verbatim, so a client could ask
for any number of records while the index page size options cap at 1000.
The tag-filter branch of this same endpoint, StudioController and
PerformerController all already guard their page size.

Uses <= 1000 rather than the < 1000 those three use: the page size options
allow 1000 inclusive, so the exclusive form would quietly serve 10 records
to anyone who picked the documented maximum.

Out-of-range values fall back to 10, matching the existing guards. This is
a behavior change for any client that passes a page size above 1000.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@plz12345
plz12345 merged commit d027b4f into eros-develop Jul 17, 2026
38 checks passed
@plz12345
plz12345 deleted the fix/paged-index-movie-file-quality branch July 17, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[V3] Quality of video file doesn't show in status column in table view

3 participants