fix(library): scope sort options to audiobook libraries
getLibrarySortRelevanceScope (in both LibraryBrowse and libraryPageSearchParams) now recognizes the audiobook libraryType — the DB stores it as the plural "audiobooks", the sort-scope constant is singular "audiobook", so accept both spellings. Video-only sort fields (Content Rating, IMDb/TMDB/RT ratings, Resolution) now declare applicableMediaScopes = ALL_VIDEO_SCOPES (no audiobook), so audiobook libraries no longer offer them in the sort dropdown. Mixed libraries and episode browse mode keep the same sort set as before: the "all" relevance check still uses ALL_VIDEO_SCOPES with the historical "every-scope" semantic, so series-only sorts like Latest Episode Air Date continue to normalize away in episode mode. Adds two tests covering the audiobook scope: video-only sorts get normalized to title; audiobook-applicable sorts (runtime) survive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
eb8f67d2a2
commit
1c291935c5
@@ -47,7 +47,14 @@ interface QuerySortLike {
|
||||
order?: string | null;
|
||||
}
|
||||
|
||||
const ALL_MEDIA_SCOPES: ApplicableMediaScope[] = ["movie", "series", "episode", "audiobook"];
|
||||
// ALL_VIDEO_SCOPES is the universe of video-side scopes — used as the
|
||||
// baseline for the "all" relevance scope (mixed libraries / episode
|
||||
// browse mode) so that sorts limited to series+episode are still
|
||||
// filtered out as not universally applicable. Audiobook is explicit
|
||||
// opt-in: a sort field must list "audiobook" in its applicableMediaScopes
|
||||
// to be eligible for an audiobook-only library.
|
||||
const ALL_VIDEO_SCOPES: ApplicableMediaScope[] = ["movie", "series", "episode"];
|
||||
const ALL_MEDIA_SCOPES: ApplicableMediaScope[] = [...ALL_VIDEO_SCOPES, "audiobook"];
|
||||
|
||||
export const QUERY_SORT_OPTIONS: QuerySortOption[] = [
|
||||
{
|
||||
@@ -91,7 +98,7 @@ export const QUERY_SORT_OPTIONS: QuerySortOption[] = [
|
||||
label: "Content Rating",
|
||||
defaultOrder: "asc",
|
||||
personalized: false,
|
||||
applicableMediaScopes: ALL_MEDIA_SCOPES,
|
||||
applicableMediaScopes: ALL_VIDEO_SCOPES,
|
||||
},
|
||||
{
|
||||
value: "runtime",
|
||||
@@ -105,35 +112,35 @@ export const QUERY_SORT_OPTIONS: QuerySortOption[] = [
|
||||
label: "IMDb Rating",
|
||||
defaultOrder: "desc",
|
||||
personalized: false,
|
||||
applicableMediaScopes: ALL_MEDIA_SCOPES,
|
||||
applicableMediaScopes: ALL_VIDEO_SCOPES,
|
||||
},
|
||||
{
|
||||
value: "rating_tmdb",
|
||||
label: "TMDB Rating",
|
||||
defaultOrder: "desc",
|
||||
personalized: false,
|
||||
applicableMediaScopes: ALL_MEDIA_SCOPES,
|
||||
applicableMediaScopes: ALL_VIDEO_SCOPES,
|
||||
},
|
||||
{
|
||||
value: "rating_rt_critic",
|
||||
label: "RT Critic Rating",
|
||||
defaultOrder: "desc",
|
||||
personalized: false,
|
||||
applicableMediaScopes: ALL_MEDIA_SCOPES,
|
||||
applicableMediaScopes: ALL_VIDEO_SCOPES,
|
||||
},
|
||||
{
|
||||
value: "rating_rt_audience",
|
||||
label: "RT Audience Rating",
|
||||
defaultOrder: "desc",
|
||||
personalized: false,
|
||||
applicableMediaScopes: ALL_MEDIA_SCOPES,
|
||||
applicableMediaScopes: ALL_VIDEO_SCOPES,
|
||||
},
|
||||
{
|
||||
value: "resolution",
|
||||
label: "Resolution",
|
||||
defaultOrder: "desc",
|
||||
personalized: false,
|
||||
applicableMediaScopes: ALL_MEDIA_SCOPES,
|
||||
applicableMediaScopes: ALL_VIDEO_SCOPES,
|
||||
},
|
||||
{
|
||||
value: "bitrate",
|
||||
@@ -183,8 +190,12 @@ function optionMatchesRelevanceScope(
|
||||
if (!relevanceScope) {
|
||||
return true;
|
||||
}
|
||||
// "all" — applies to mixed libraries and the episode browse mode.
|
||||
// Match the historical baseline (universal across all video scopes)
|
||||
// so series-only sorts still get normalized away. Audiobook scope is
|
||||
// intentionally not part of this baseline; see ALL_VIDEO_SCOPES.
|
||||
if (relevanceScope === "all") {
|
||||
return ALL_MEDIA_SCOPES.every((scope) => option.applicableMediaScopes.includes(scope));
|
||||
return ALL_VIDEO_SCOPES.every((scope) => option.applicableMediaScopes.includes(scope));
|
||||
}
|
||||
return option.applicableMediaScopes.includes(relevanceScope);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,17 @@ function getLibrarySortRelevanceScope(
|
||||
if (libraryType === "movie" || libraryType === "series") {
|
||||
return libraryType;
|
||||
}
|
||||
if (mediaScope === "movie" || mediaScope === "series" || mediaScope === "episode") {
|
||||
// The DB stores audiobook library type as the plural "audiobooks";
|
||||
// the sort scope is the singular "audiobook" (matches QueryDefinition.media_scope).
|
||||
if (libraryType === "audiobook" || libraryType === "audiobooks") {
|
||||
return "audiobook";
|
||||
}
|
||||
if (
|
||||
mediaScope === "movie" ||
|
||||
mediaScope === "series" ||
|
||||
mediaScope === "episode" ||
|
||||
mediaScope === "audiobook"
|
||||
) {
|
||||
return mediaScope;
|
||||
}
|
||||
return "all";
|
||||
@@ -148,6 +158,7 @@ export default function LibraryBrowse({
|
||||
sortRelevanceScope={sortRelevanceScope}
|
||||
resultCountLabel={itemCountLabel}
|
||||
resultCountLoading={isLoading}
|
||||
libraryType={libraryType}
|
||||
/>
|
||||
<ItemGrid
|
||||
totalItems={totalItems}
|
||||
|
||||
@@ -169,6 +169,22 @@ describe("parseLibraryPageState", () => {
|
||||
expect(state.queryDefinition.sort).toEqual({ field: "title", order: "asc" });
|
||||
});
|
||||
|
||||
it("normalizes video-only sorts away on audiobook libraries", () => {
|
||||
const state = parseLibraryPageState(
|
||||
params("tab=library&sort=rating_imdb&order=desc"),
|
||||
"audiobooks",
|
||||
);
|
||||
expect(state.queryDefinition.sort).toEqual({ field: "title", order: "asc" });
|
||||
});
|
||||
|
||||
it("keeps audiobook-applicable sorts on audiobook libraries", () => {
|
||||
const state = parseLibraryPageState(
|
||||
params("tab=library&sort=runtime&order=desc"),
|
||||
"audiobooks",
|
||||
);
|
||||
expect(state.queryDefinition.sort).toEqual({ field: "runtime", order: "desc" });
|
||||
});
|
||||
|
||||
it("normalizes legacy sort aliases to canonical values", () => {
|
||||
expect(
|
||||
parseLibraryPageState(params("tab=library&sort=sort_title"), "mixed").queryDefinition.sort
|
||||
|
||||
@@ -66,7 +66,17 @@ function getLibrarySortRelevanceScope(
|
||||
if (libraryType === "movie" || libraryType === "series") {
|
||||
return libraryType;
|
||||
}
|
||||
if (mediaScope === "movie" || mediaScope === "series" || mediaScope === "episode") {
|
||||
// The DB stores audiobook library type as the plural "audiobooks";
|
||||
// the sort scope is the singular "audiobook" (matches QueryDefinition.media_scope).
|
||||
if (libraryType === "audiobook" || libraryType === "audiobooks") {
|
||||
return "audiobook";
|
||||
}
|
||||
if (
|
||||
mediaScope === "movie" ||
|
||||
mediaScope === "series" ||
|
||||
mediaScope === "episode" ||
|
||||
mediaScope === "audiobook"
|
||||
) {
|
||||
return mediaScope;
|
||||
}
|
||||
return "all";
|
||||
|
||||
Reference in New Issue
Block a user