mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
fix: sort nested program queries by index
Allows specification of sorting in the search API now
This commit is contained in:
1
docs/generated/tunarr-v1.0.18-openapi.json
Normal file
1
docs/generated/tunarr-v1.0.18-openapi.json
Normal file
File diff suppressed because one or more lines are too long
@@ -85,6 +85,7 @@ export default defineConfig(
|
|||||||
'warn',
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
|
'react-hooks/preserve-manual-memoization': ['warn'],
|
||||||
// Don't error on promise-returning functions in JSX attributes
|
// Don't error on promise-returning functions in JSX attributes
|
||||||
'@typescript-eslint/no-misused-promises': [
|
'@typescript-eslint/no-misused-promises': [
|
||||||
2,
|
2,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export class SearchProgramsCommand {
|
|||||||
restrictSearchTo: req.query
|
restrictSearchTo: req.query
|
||||||
.restrictSearchTo as Path<ProgramSearchDocument>[],
|
.restrictSearchTo as Path<ProgramSearchDocument>[],
|
||||||
facets: ['type'],
|
facets: ['type'],
|
||||||
|
sort: req.query.sort ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [programIds, groupingIds] = result.results.reduce(
|
const [programIds, groupingIds] = result.results.reduce(
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ const ProgramsIndex: TunarrSearchIndex<ProgramSearchDocument> = {
|
|||||||
],
|
],
|
||||||
sortable: [
|
sortable: [
|
||||||
'title',
|
'title',
|
||||||
|
'sortTitle',
|
||||||
'duration',
|
'duration',
|
||||||
'originalReleaseDate',
|
'originalReleaseDate',
|
||||||
'originalReleaseYear',
|
'originalReleaseYear',
|
||||||
@@ -257,6 +258,7 @@ type BaseProgramSearchDocument = {
|
|||||||
libraryId: SingleCaseString;
|
libraryId: SingleCaseString;
|
||||||
title: string;
|
title: string;
|
||||||
titleReverse: string;
|
titleReverse: string;
|
||||||
|
sortTitle?: string;
|
||||||
rating: Nullable<string>;
|
rating: Nullable<string>;
|
||||||
summary: Nullable<string>;
|
summary: Nullable<string>;
|
||||||
plot: Nullable<string>;
|
plot: Nullable<string>;
|
||||||
@@ -333,6 +335,7 @@ type SearchRequest<
|
|||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
};
|
};
|
||||||
|
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FreeSearchResponse<DocumentType extends Record<string, unknown>> = {
|
export type FreeSearchResponse<DocumentType extends Record<string, unknown>> = {
|
||||||
@@ -777,6 +780,7 @@ export class MeilisearchService implements ISearchService {
|
|||||||
tagline: show.tagline,
|
tagline: show.tagline,
|
||||||
title: show.title,
|
title: show.title,
|
||||||
titleReverse: show.title.split('').reverse().join(''),
|
titleReverse: show.title.split('').reverse().join(''),
|
||||||
|
sortTitle: show.sortTitle,
|
||||||
rating: show.rating,
|
rating: show.rating,
|
||||||
genres: show.genres,
|
genres: show.genres,
|
||||||
actors: show.actors,
|
actors: show.actors,
|
||||||
@@ -824,6 +828,7 @@ export class MeilisearchService implements ISearchService {
|
|||||||
tagline: season.tagline,
|
tagline: season.tagline,
|
||||||
title: season.title,
|
title: season.title,
|
||||||
titleReverse: season.title.split('').reverse().join(''),
|
titleReverse: season.title.split('').reverse().join(''),
|
||||||
|
sortTitle: season.sortTitle,
|
||||||
director: [],
|
director: [],
|
||||||
rating: null,
|
rating: null,
|
||||||
actors: [],
|
actors: [],
|
||||||
@@ -948,6 +953,7 @@ export class MeilisearchService implements ISearchService {
|
|||||||
tagline: artist.tagline,
|
tagline: artist.tagline,
|
||||||
title: artist.title,
|
title: artist.title,
|
||||||
titleReverse: artist.title.split('').reverse().join(''),
|
titleReverse: artist.title.split('').reverse().join(''),
|
||||||
|
sortTitle: artist.sortTitle,
|
||||||
rating: null,
|
rating: null,
|
||||||
genres: artist.genres ?? [],
|
genres: artist.genres ?? [],
|
||||||
actors: [],
|
actors: [],
|
||||||
@@ -993,6 +999,7 @@ export class MeilisearchService implements ISearchService {
|
|||||||
tagline: album.tagline,
|
tagline: album.tagline,
|
||||||
title: album.title,
|
title: album.title,
|
||||||
titleReverse: album.title.split('').reverse().join(''),
|
titleReverse: album.title.split('').reverse().join(''),
|
||||||
|
sortTitle: album.sortTitle,
|
||||||
director: [],
|
director: [],
|
||||||
rating: null,
|
rating: null,
|
||||||
actors: [],
|
actors: [],
|
||||||
@@ -1138,19 +1145,24 @@ export class MeilisearchService implements ISearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isNonEmptyString(request.query)) {
|
if (isNonEmptyString(request.query)) {
|
||||||
|
const sort = request.sort?.map(
|
||||||
|
({ field, direction }) => `${field}:${direction}`,
|
||||||
|
) ?? ['title:asc'];
|
||||||
const req = {
|
const req = {
|
||||||
filter,
|
filter,
|
||||||
page: request.paging?.page,
|
page: request.paging?.page,
|
||||||
limit: request.paging?.limit,
|
limit: request.paging?.limit,
|
||||||
attributesToSearchOn: request.restrictSearchTo ?? undefined,
|
attributesToSearchOn: request.restrictSearchTo ?? undefined,
|
||||||
facets: request.facets ?? undefined,
|
facets: request.facets ?? undefined,
|
||||||
|
sort,
|
||||||
} satisfies SearchParams;
|
} satisfies SearchParams;
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Issuing search: query = %s, filter: %O (parsed: %O)',
|
'Issuing search: query = %s, filter: %O (parsed: %O), sort %O',
|
||||||
request.query,
|
request.query,
|
||||||
request.filter ?? {},
|
request.filter ?? {},
|
||||||
req,
|
req,
|
||||||
|
sort,
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchResults = await this.client()
|
const searchResults = await this.client()
|
||||||
@@ -1165,9 +1177,14 @@ export class MeilisearchService implements ISearchService {
|
|||||||
const offset = request.paging
|
const offset = request.paging
|
||||||
? request.paging.page * request.paging.limit
|
? request.paging.page * request.paging.limit
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const sort = request.sort?.map(
|
||||||
|
({ field, direction }) => `${field}:${direction}`,
|
||||||
|
) ?? ['title:asc'];
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Issuing get documents request: filter: "%s". offset: %d limit %d',
|
'Issuing get documents request: filter: "%s", sort: %O. offset: %d limit %d',
|
||||||
filter ?? '',
|
filter ?? '',
|
||||||
|
sort,
|
||||||
offset ?? 0,
|
offset ?? 0,
|
||||||
request.paging?.limit ?? -1,
|
request.paging?.limit ?? -1,
|
||||||
);
|
);
|
||||||
@@ -1180,7 +1197,8 @@ export class MeilisearchService implements ISearchService {
|
|||||||
offset,
|
offset,
|
||||||
// This does not exist on the type yet. Explicit cast because
|
// This does not exist on the type yet. Explicit cast because
|
||||||
// the API supports it. Need https://github.com/meilisearch/meilisearch-js/pull/2038
|
// the API supports it. Need https://github.com/meilisearch/meilisearch-js/pull/2038
|
||||||
sort: ['title:asc' /*, 'originalReleaseDate:asc'*/],
|
// sort: ['title:asc' /*, 'originalReleaseDate:asc'*/],
|
||||||
|
sort,
|
||||||
} as DocumentsQuery<IndexDocumentTypeT>);
|
} as DocumentsQuery<IndexDocumentTypeT>);
|
||||||
return {
|
return {
|
||||||
type: 'filter',
|
type: 'filter',
|
||||||
@@ -1502,6 +1520,7 @@ export class MeilisearchService implements ISearchService {
|
|||||||
tagline: program.type === 'movie' ? program.tagline : null,
|
tagline: program.type === 'movie' ? program.tagline : null,
|
||||||
title: program.title,
|
title: program.title,
|
||||||
titleReverse: program.title.split('').reverse().join(''),
|
titleReverse: program.title.split('').reverse().join(''),
|
||||||
|
sortTitle: program.sortTitle,
|
||||||
type: program.type,
|
type: program.type,
|
||||||
index:
|
index:
|
||||||
program.type === 'episode'
|
program.type === 'episode'
|
||||||
|
|||||||
@@ -107,8 +107,19 @@ export const SearchFilterQuerySchema: z.ZodDiscriminatedUnion<
|
|||||||
|
|
||||||
export type SearchFilter = z.infer<typeof SearchFilterQuerySchema>;
|
export type SearchFilter = z.infer<typeof SearchFilterQuerySchema>;
|
||||||
|
|
||||||
|
export const SearchSortFields = [
|
||||||
|
'title',
|
||||||
|
'sortTitle',
|
||||||
|
'duration',
|
||||||
|
'originalReleaseDate',
|
||||||
|
'originalReleaseYear',
|
||||||
|
'index',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type SearchSortField = TupleToUnion<typeof SearchSortFields>;
|
||||||
|
|
||||||
export const SearchSortSchema = z.object({
|
export const SearchSortSchema = z.object({
|
||||||
field: z.string(),
|
field: z.enum(SearchSortFields),
|
||||||
direction: z.enum(['asc', 'desc']),
|
direction: z.enum(['asc', 'desc']),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,7 +129,7 @@ export const SearchRequestSchema = z.object({
|
|||||||
query: z.string().nullish(),
|
query: z.string().nullish(),
|
||||||
restrictSearchTo: z.string().array().optional(),
|
restrictSearchTo: z.string().array().optional(),
|
||||||
filter: SearchFilterQuerySchema.nullish(),
|
filter: SearchFilterQuerySchema.nullish(),
|
||||||
sort: SearchSortSchema.nullish(),
|
sort: SearchSortSchema.array().nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SearchRequest = z.infer<typeof SearchRequestSchema>;
|
export type SearchRequest = z.infer<typeof SearchRequestSchema>;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
ProgramOrFolder,
|
ProgramOrFolder,
|
||||||
} from '@tunarr/types';
|
} from '@tunarr/types';
|
||||||
import { type ProgramLike } from '@tunarr/types';
|
import { type ProgramLike } from '@tunarr/types';
|
||||||
import type { SearchFilter } from '@tunarr/types/api';
|
import type { SearchFilter, SearchSort } from '@tunarr/types/api';
|
||||||
import {
|
import {
|
||||||
type ProgramSearchResponse,
|
type ProgramSearchResponse,
|
||||||
type SearchRequest,
|
type SearchRequest,
|
||||||
@@ -87,8 +87,17 @@ export const LibraryProgramGrid = ({
|
|||||||
|
|
||||||
const query = useMemo<SearchRequest>(() => {
|
const query = useMemo<SearchRequest>(() => {
|
||||||
if (currentParentContext) {
|
if (currentParentContext) {
|
||||||
|
// sort override
|
||||||
|
const sort = match(currentParentContext)
|
||||||
|
.returnType<Maybe<SearchSort[]>>()
|
||||||
|
.with({ type: P.union('show', 'season', 'album') }, () => [
|
||||||
|
{ field: 'index', direction: 'asc' },
|
||||||
|
])
|
||||||
|
.otherwise(() => undefined);
|
||||||
|
const filter = getChildSearchFilter(currentParentContext);
|
||||||
return {
|
return {
|
||||||
filter: getChildSearchFilter(currentParentContext),
|
filter,
|
||||||
|
sort,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1811,77 +1811,6 @@ export type PostApiTasksByIdRunResponses = {
|
|||||||
202: unknown;
|
202: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetApiJobsData = {
|
|
||||||
body?: never;
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/api/jobs';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetApiJobsResponses = {
|
|
||||||
/**
|
|
||||||
* Default Response
|
|
||||||
*/
|
|
||||||
200: Array<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
scheduledTasks: Array<{
|
|
||||||
running: boolean;
|
|
||||||
lastExecution?: string;
|
|
||||||
lastExecutionEpoch?: number;
|
|
||||||
nextExecution?: string;
|
|
||||||
nextExecutionEpoch?: number;
|
|
||||||
args: {
|
|
||||||
[key: string]: never;
|
|
||||||
} | string;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetApiJobsResponse = GetApiJobsResponses[keyof GetApiJobsResponses];
|
|
||||||
|
|
||||||
export type PostApiJobsByIdRunData = {
|
|
||||||
body?: never;
|
|
||||||
path: {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
query?: {
|
|
||||||
background?: string;
|
|
||||||
};
|
|
||||||
url: '/api/jobs/{id}/run';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PostApiJobsByIdRunErrors = {
|
|
||||||
/**
|
|
||||||
* Default Response
|
|
||||||
*/
|
|
||||||
400: {
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Default Response
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
/**
|
|
||||||
* Default Response
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PostApiJobsByIdRunError = PostApiJobsByIdRunErrors[keyof PostApiJobsByIdRunErrors];
|
|
||||||
|
|
||||||
export type PostApiJobsByIdRunResponses = {
|
|
||||||
/**
|
|
||||||
* Default Response
|
|
||||||
*/
|
|
||||||
200: unknown;
|
|
||||||
/**
|
|
||||||
* Default Response
|
|
||||||
*/
|
|
||||||
202: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetChannelsData = {
|
export type GetChannelsData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
@@ -11821,10 +11750,10 @@ export type PostApiProgramsSearchData = {
|
|||||||
query?: string | null;
|
query?: string | null;
|
||||||
restrictSearchTo?: Array<string>;
|
restrictSearchTo?: Array<string>;
|
||||||
filter?: SearchFilterInput | null;
|
filter?: SearchFilterInput | null;
|
||||||
sort?: {
|
sort?: Array<{
|
||||||
field: string;
|
field: 'title' | 'sortTitle' | 'duration' | 'originalReleaseDate' | 'originalReleaseYear' | 'index';
|
||||||
direction: 'asc' | 'desc';
|
direction: 'asc' | 'desc';
|
||||||
} | null;
|
}> | null;
|
||||||
};
|
};
|
||||||
restrictSeachTo?: Array<string>;
|
restrictSeachTo?: Array<string>;
|
||||||
mediaSourceId?: string;
|
mediaSourceId?: string;
|
||||||
@@ -17694,7 +17623,7 @@ export type GetApiPlexByMediaSourceIdFiltersResponses = {
|
|||||||
Meta: {
|
Meta: {
|
||||||
Type: Array<{
|
Type: Array<{
|
||||||
key: string;
|
key: string;
|
||||||
type: 'movie' | 'show' | 'artist' | 'photo' | 'track' | 'season' | 'album' | 'folder';
|
type: 'movie' | 'show' | 'artist' | 'photo' | 'track' | 'episode' | 'season' | 'album' | 'folder';
|
||||||
title: string;
|
title: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
Filter?: Array<{
|
Filter?: Array<{
|
||||||
|
|||||||
Reference in New Issue
Block a user