Fixed: Movie file quality missing from the paged movie/scene index - #326
Merged
Conversation
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.
|
sampulsar
approved these changes
Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


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.idrelationship walk needing an in-memory movie-file cache. It turned out to be neither expensive nor a frontend gap:movieFile.quality.quality.namewith?? translate('Downloaded')as the fallback. That fallback is the reported bug. No frontend changes here.PagedBuilderalready LEFT JOINsMovieFiles. ButQueryJoinedbuilds its SELECT from its type parameters, soPagedQuery's<Movie, MovieMetadata>emitted onlyMovies.*andMovieMetadata.*— the file was joined and then discarded.Movie.MovieFileis a plain property rather thanLazyLoaded, so nothing back-filled it andToResource's null-conditional dropped it.The change
GetPagednow mapsMovieFilethrough its own query. Two constraints shaped it, and both are worth knowing before simplifying this:PagedQueryis deliberately left alone. It is shared withMoviesWithoutFilesandMoviesWhereCutoffUnmet, whichGROUP 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 viauseSingleMovieFile.QualityProfilemust be hydrated.MovieFileResource.ToResourcedereferencesmovie.QualityProfileunconditionally to computeQualityCutoffNotMet, andUpgradableSpecification.QualityCutoffNotMetdereferencesprofile.UpgradeAllowedon its first line. HydratingMovieFilewithout a profile throwsNullReferenceException(verified) — latent today only becauseMovieFileis always null. The profile comes from the repository with a fallback, the wayAll()already handles movies pointing at a profile that no longer exists.MediaInfois excluded from the query. Measured against a real library it stores the full ffprobe dump at ~7KB a row, ~90% of itrawStreamData, whichMediaInfoResourcediscards anyway and no index view renders. Including it would deserialize ~7MB per request on a page of 1000. Consequence:/movie/pagedreturnsmovieFile.mediaInfo = nullwhile/moviestill populates it.The column list is derived from the mapper rather than hardcoded, so a new
MovieFilescolumn can't silently go missing, andIdis forced to lead the segment because Dapper splits on the firstIdcolumn.Second commit
POST /movie/pagedtookrequest.PageSizeverbatim, so a client could request any number of records. The tag-filter branch of this same endpoint,StudioControllerandPerformerControllerall already guard theirs. Kept as a separate commit — it's a pre-existing gap, not part of the quality fix.Uses
<= 1000rather than the< 1000those 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
MediaInfoexclusion, and null-file. 4 of them fail without the fix.MoviesWithoutFilestest pinning constraint 1 — that path had no coverage at all.MovieResourcemapping tests covering constraint 2, which the repository tests can't reach./movie/pagedreturns the file's quality withqualityCutoffNotMetcomputed andmediaInfo: 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-
PagedQuerystructure and avoided by construction rather than reproduced — SQLite won't surface it.