mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 00:53: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',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'react-hooks/preserve-manual-memoization': ['warn'],
|
||||
// Don't error on promise-returning functions in JSX attributes
|
||||
'@typescript-eslint/no-misused-promises': [
|
||||
2,
|
||||
|
||||
@@ -45,6 +45,7 @@ export class SearchProgramsCommand {
|
||||
restrictSearchTo: req.query
|
||||
.restrictSearchTo as Path<ProgramSearchDocument>[],
|
||||
facets: ['type'],
|
||||
sort: req.query.sort ?? undefined,
|
||||
});
|
||||
|
||||
const [programIds, groupingIds] = result.results.reduce(
|
||||
|
||||
@@ -164,6 +164,7 @@ const ProgramsIndex: TunarrSearchIndex<ProgramSearchDocument> = {
|
||||
],
|
||||
sortable: [
|
||||
'title',
|
||||
'sortTitle',
|
||||
'duration',
|
||||
'originalReleaseDate',
|
||||
'originalReleaseYear',
|
||||
@@ -257,6 +258,7 @@ type BaseProgramSearchDocument = {
|
||||
libraryId: SingleCaseString;
|
||||
title: string;
|
||||
titleReverse: string;
|
||||
sortTitle?: string;
|
||||
rating: Nullable<string>;
|
||||
summary: Nullable<string>;
|
||||
plot: Nullable<string>;
|
||||
@@ -333,6 +335,7 @@ type SearchRequest<
|
||||
page: number;
|
||||
limit: number;
|
||||
};
|
||||
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
||||
};
|
||||
|
||||
export type FreeSearchResponse<DocumentType extends Record<string, unknown>> = {
|
||||
@@ -777,6 +780,7 @@ export class MeilisearchService implements ISearchService {
|
||||
tagline: show.tagline,
|
||||
title: show.title,
|
||||
titleReverse: show.title.split('').reverse().join(''),
|
||||
sortTitle: show.sortTitle,
|
||||
rating: show.rating,
|
||||
genres: show.genres,
|
||||
actors: show.actors,
|
||||
@@ -824,6 +828,7 @@ export class MeilisearchService implements ISearchService {
|
||||
tagline: season.tagline,
|
||||
title: season.title,
|
||||
titleReverse: season.title.split('').reverse().join(''),
|
||||
sortTitle: season.sortTitle,
|
||||
director: [],
|
||||
rating: null,
|
||||
actors: [],
|
||||
@@ -948,6 +953,7 @@ export class MeilisearchService implements ISearchService {
|
||||
tagline: artist.tagline,
|
||||
title: artist.title,
|
||||
titleReverse: artist.title.split('').reverse().join(''),
|
||||
sortTitle: artist.sortTitle,
|
||||
rating: null,
|
||||
genres: artist.genres ?? [],
|
||||
actors: [],
|
||||
@@ -993,6 +999,7 @@ export class MeilisearchService implements ISearchService {
|
||||
tagline: album.tagline,
|
||||
title: album.title,
|
||||
titleReverse: album.title.split('').reverse().join(''),
|
||||
sortTitle: album.sortTitle,
|
||||
director: [],
|
||||
rating: null,
|
||||
actors: [],
|
||||
@@ -1138,19 +1145,24 @@ export class MeilisearchService implements ISearchService {
|
||||
}
|
||||
|
||||
if (isNonEmptyString(request.query)) {
|
||||
const sort = request.sort?.map(
|
||||
({ field, direction }) => `${field}:${direction}`,
|
||||
) ?? ['title:asc'];
|
||||
const req = {
|
||||
filter,
|
||||
page: request.paging?.page,
|
||||
limit: request.paging?.limit,
|
||||
attributesToSearchOn: request.restrictSearchTo ?? undefined,
|
||||
facets: request.facets ?? undefined,
|
||||
sort,
|
||||
} satisfies SearchParams;
|
||||
|
||||
this.logger.debug(
|
||||
'Issuing search: query = %s, filter: %O (parsed: %O)',
|
||||
'Issuing search: query = %s, filter: %O (parsed: %O), sort %O',
|
||||
request.query,
|
||||
request.filter ?? {},
|
||||
req,
|
||||
sort,
|
||||
);
|
||||
|
||||
const searchResults = await this.client()
|
||||
@@ -1165,9 +1177,14 @@ export class MeilisearchService implements ISearchService {
|
||||
const offset = request.paging
|
||||
? request.paging.page * request.paging.limit
|
||||
: undefined;
|
||||
const sort = request.sort?.map(
|
||||
({ field, direction }) => `${field}:${direction}`,
|
||||
) ?? ['title:asc'];
|
||||
|
||||
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 ?? '',
|
||||
sort,
|
||||
offset ?? 0,
|
||||
request.paging?.limit ?? -1,
|
||||
);
|
||||
@@ -1180,7 +1197,8 @@ export class MeilisearchService implements ISearchService {
|
||||
offset,
|
||||
// This does not exist on the type yet. Explicit cast because
|
||||
// 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>);
|
||||
return {
|
||||
type: 'filter',
|
||||
@@ -1502,6 +1520,7 @@ export class MeilisearchService implements ISearchService {
|
||||
tagline: program.type === 'movie' ? program.tagline : null,
|
||||
title: program.title,
|
||||
titleReverse: program.title.split('').reverse().join(''),
|
||||
sortTitle: program.sortTitle,
|
||||
type: program.type,
|
||||
index:
|
||||
program.type === 'episode'
|
||||
|
||||
@@ -107,8 +107,19 @@ export const SearchFilterQuerySchema: z.ZodDiscriminatedUnion<
|
||||
|
||||
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({
|
||||
field: z.string(),
|
||||
field: z.enum(SearchSortFields),
|
||||
direction: z.enum(['asc', 'desc']),
|
||||
});
|
||||
|
||||
@@ -118,7 +129,7 @@ export const SearchRequestSchema = z.object({
|
||||
query: z.string().nullish(),
|
||||
restrictSearchTo: z.string().array().optional(),
|
||||
filter: SearchFilterQuerySchema.nullish(),
|
||||
sort: SearchSortSchema.nullish(),
|
||||
sort: SearchSortSchema.array().nullish(),
|
||||
});
|
||||
|
||||
export type SearchRequest = z.infer<typeof SearchRequestSchema>;
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
ProgramOrFolder,
|
||||
} 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 {
|
||||
type ProgramSearchResponse,
|
||||
type SearchRequest,
|
||||
@@ -87,8 +87,17 @@ export const LibraryProgramGrid = ({
|
||||
|
||||
const query = useMemo<SearchRequest>(() => {
|
||||
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 {
|
||||
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;
|
||||
};
|
||||
|
||||
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 = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
@@ -11821,10 +11750,10 @@ export type PostApiProgramsSearchData = {
|
||||
query?: string | null;
|
||||
restrictSearchTo?: Array<string>;
|
||||
filter?: SearchFilterInput | null;
|
||||
sort?: {
|
||||
field: string;
|
||||
sort?: Array<{
|
||||
field: 'title' | 'sortTitle' | 'duration' | 'originalReleaseDate' | 'originalReleaseYear' | 'index';
|
||||
direction: 'asc' | 'desc';
|
||||
} | null;
|
||||
}> | null;
|
||||
};
|
||||
restrictSeachTo?: Array<string>;
|
||||
mediaSourceId?: string;
|
||||
@@ -17694,7 +17623,7 @@ export type GetApiPlexByMediaSourceIdFiltersResponses = {
|
||||
Meta: {
|
||||
Type: Array<{
|
||||
key: string;
|
||||
type: 'movie' | 'show' | 'artist' | 'photo' | 'track' | 'season' | 'album' | 'folder';
|
||||
type: 'movie' | 'show' | 'artist' | 'photo' | 'track' | 'episode' | 'season' | 'album' | 'folder';
|
||||
title: string;
|
||||
active: boolean;
|
||||
Filter?: Array<{
|
||||
|
||||
Reference in New Issue
Block a user