Files
silo-server/web/src/lib/querySortOptions.ts
T

272 lines
6.9 KiB
TypeScript

export type QuerySortOrder = "asc" | "desc";
export type QuerySortRelevanceScope = "movie" | "series" | "episode" | "all";
export type QuerySortField =
| "title"
| "added_at"
| "release_date"
| "last_air_date"
| "year"
| "content_rating"
| "runtime"
| "rating_imdb"
| "rating_tmdb"
| "rating_rt_critic"
| "rating_rt_audience"
| "resolution"
| "bitrate"
| "progress"
| "date_viewed"
| "plays";
type ApplicableMediaScope = Exclude<QuerySortRelevanceScope, "all">;
export interface QuerySortOption {
value: QuerySortField;
label: string;
defaultOrder: QuerySortOrder;
personalized: boolean;
applicableMediaScopes: ApplicableMediaScope[];
/**
* When set, the wizard's scope selector switches to this scope
* if the user picks this sort in a different scope. Used for sorts
* whose semantics imply a particular media type (e.g. Latest Episode
* Air Date is most meaningful when results are episodes).
*/
preferredMediaScope?: ApplicableMediaScope;
}
export interface QuerySortOptionsConfig {
includePersonalized?: boolean;
relevanceScope?: QuerySortRelevanceScope;
}
type QuerySortOptionsInput = boolean | QuerySortOptionsConfig;
interface QuerySortLike {
field?: string | null;
order?: string | null;
}
const ALL_MEDIA_SCOPES: ApplicableMediaScope[] = ["movie", "series", "episode"];
export const QUERY_SORT_OPTIONS: QuerySortOption[] = [
{
value: "title",
label: "Title",
defaultOrder: "asc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "added_at",
label: "Date Added",
defaultOrder: "desc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "release_date",
label: "Release Date",
defaultOrder: "desc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "last_air_date",
label: "Latest Episode Air Date",
defaultOrder: "desc",
personalized: false,
applicableMediaScopes: ["series", "episode"],
preferredMediaScope: "episode",
},
{
value: "year",
label: "Year",
defaultOrder: "desc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "content_rating",
label: "Content Rating",
defaultOrder: "asc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "runtime",
label: "Duration",
defaultOrder: "desc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "rating_imdb",
label: "IMDb Rating",
defaultOrder: "desc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "rating_tmdb",
label: "TMDB Rating",
defaultOrder: "desc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "rating_rt_critic",
label: "RT Critic Rating",
defaultOrder: "desc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "rating_rt_audience",
label: "RT Audience Rating",
defaultOrder: "desc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "resolution",
label: "Resolution",
defaultOrder: "desc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "bitrate",
label: "Bitrate",
defaultOrder: "desc",
personalized: false,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "progress",
label: "Progress",
defaultOrder: "desc",
personalized: true,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "date_viewed",
label: "Date Viewed",
defaultOrder: "desc",
personalized: true,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
{
value: "plays",
label: "Plays",
defaultOrder: "desc",
personalized: true,
applicableMediaScopes: ALL_MEDIA_SCOPES,
},
];
const QUERY_SORT_OPTION_MAP = new Map(QUERY_SORT_OPTIONS.map((option) => [option.value, option]));
function normalizeQuerySortOptionsConfig(
input: QuerySortOptionsInput = false,
): QuerySortOptionsConfig {
if (typeof input === "boolean") {
return { includePersonalized: input };
}
return input;
}
function optionMatchesRelevanceScope(
option: QuerySortOption,
relevanceScope?: QuerySortRelevanceScope,
): boolean {
if (!relevanceScope) {
return true;
}
if (relevanceScope === "all") {
return ALL_MEDIA_SCOPES.every((scope) => option.applicableMediaScopes.includes(scope));
}
return option.applicableMediaScopes.includes(relevanceScope);
}
export function getQuerySortOptions(input: QuerySortOptionsInput = false): QuerySortOption[] {
const { includePersonalized = false, relevanceScope } = normalizeQuerySortOptionsConfig(input);
return QUERY_SORT_OPTIONS.filter(
(option) =>
(includePersonalized || !option.personalized) &&
optionMatchesRelevanceScope(option, relevanceScope),
);
}
export function normalizeQuerySortForScope(
sort?: QuerySortLike | null,
input: QuerySortOptionsInput = false,
): { field: QuerySortField; order: QuerySortOrder } {
const sortOptions = getQuerySortOptions(input);
const normalizedField = normalizeQuerySortField(sort?.field);
if (normalizedField && normalizedField !== "relevance") {
const matchingOption = sortOptions.find((option) => option.value === normalizedField);
if (matchingOption) {
return {
field: matchingOption.value,
order:
sort?.order === "asc" || sort?.order === "desc"
? sort.order
: matchingOption.defaultOrder,
};
}
}
const fallbackOption = sortOptions[0] ?? QUERY_SORT_OPTION_MAP.get("added_at");
if (!fallbackOption) {
return { field: "added_at", order: "desc" };
}
return {
field: fallbackOption.value,
order: fallbackOption.defaultOrder,
};
}
export function normalizeQuerySortField(
field?: string | null,
): QuerySortField | "relevance" | undefined {
const normalized = field?.trim().toLowerCase();
switch (normalized) {
case undefined:
return undefined;
case "":
return undefined;
case "sort_title":
return "title";
case "recently_added":
return "added_at";
case "rating":
return "rating_imdb";
case "relevance":
return "relevance";
default:
return QUERY_SORT_OPTION_MAP.has(normalized as QuerySortField)
? (normalized as QuerySortField)
: undefined;
}
}
export function getDefaultQuerySortOrder(field?: string | null): QuerySortOrder {
const normalized = normalizeQuerySortField(field);
if (!normalized || normalized === "relevance") {
return "desc";
}
return QUERY_SORT_OPTION_MAP.get(normalized)?.defaultOrder ?? "desc";
}
export function isPersonalizedQuerySortField(field?: string | null): boolean {
const normalized = normalizeQuerySortField(field);
return normalized != null && normalized !== "relevance"
? (QUERY_SORT_OPTION_MAP.get(normalized)?.personalized ?? false)
: false;
}