mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
feat: add more searchable fields
This commit is contained in:
@@ -268,6 +268,8 @@ export const AllProgramFields = [
|
||||
'program.plexFilePath',
|
||||
'program.plexRatingKey',
|
||||
'program.rating',
|
||||
'program.audienceRating',
|
||||
'program.criticRating',
|
||||
'program.seasonIcon',
|
||||
'program.seasonNumber',
|
||||
'program.seasonUuid',
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
check,
|
||||
index,
|
||||
integer,
|
||||
real,
|
||||
sqliteTable,
|
||||
text,
|
||||
uniqueIndex,
|
||||
@@ -79,6 +80,8 @@ export const Program = sqliteTable(
|
||||
plexFilePath: text(),
|
||||
plexRatingKey: text(),
|
||||
rating: text(),
|
||||
audienceRating: real(),
|
||||
criticRating: real(),
|
||||
seasonIcon: text(),
|
||||
seasonNumber: integer(),
|
||||
seasonUuid: text().references(() => ProgramGrouping.uuid),
|
||||
|
||||
@@ -116,6 +116,9 @@ const RequiredLibraryFields = [
|
||||
'OfficialRating',
|
||||
'MediaStreams',
|
||||
'MediaSources',
|
||||
'ProductionLocations',
|
||||
'CommunityRating',
|
||||
'CriticRating',
|
||||
];
|
||||
|
||||
function getJellyfinAuthorization(
|
||||
@@ -1026,6 +1029,10 @@ export class JellyfinApiClient extends MediaSourceApiClient<JellyfinItemTypes> {
|
||||
}),
|
||||
plot: movie.Overview ?? null,
|
||||
rating: movie.OfficialRating ?? null,
|
||||
countries: movie.ProductionLocations?.map((loc) => ({ name: loc })) ?? [],
|
||||
collections: [],
|
||||
audienceRating: movie.CommunityRating ?? null,
|
||||
criticRating: movie.CriticRating ?? null,
|
||||
sourceType: 'jellyfin',
|
||||
tagline: find(movie.Taglines, isNonEmptyString) ?? null,
|
||||
tags: movie.Tags?.filter(isNonEmptyString) ?? [],
|
||||
@@ -1242,6 +1249,11 @@ export class JellyfinApiClient extends MediaSourceApiClient<JellyfinItemTypes> {
|
||||
}),
|
||||
plot: series.Overview ?? null,
|
||||
rating: series.OfficialRating ?? null,
|
||||
countries:
|
||||
series.ProductionLocations?.map((loc) => ({ name: loc })) ?? [],
|
||||
collections: [],
|
||||
audienceRating: series.CommunityRating ?? null,
|
||||
criticRating: series.CriticRating ?? null,
|
||||
sourceType: 'jellyfin',
|
||||
tagline: find(series.Taglines, isNonEmptyString) ?? null,
|
||||
tags: series.Tags?.filter(isNonEmptyString) ?? [],
|
||||
|
||||
8
server/src/external/plex/PlexApiClient.ts
vendored
8
server/src/external/plex/PlexApiClient.ts
vendored
@@ -1186,11 +1186,15 @@ export class PlexApiClient extends MediaSourceApiClient<PlexTypes> {
|
||||
releaseDateString: releaseDate?.format() ?? null,
|
||||
actors: plexActorInject(plexShow.Role),
|
||||
genres: plexJoinItemInject(plexShow.Genre),
|
||||
countries: plexJoinItemInject(plexShow.Country),
|
||||
collections: plexJoinItemInject(plexShow.Collection),
|
||||
plot: plexShow.summary ?? null,
|
||||
studios: isNonEmptyString(plexShow.studio)
|
||||
? [{ name: plexShow.studio }]
|
||||
: [],
|
||||
rating: plexShow.contentRating ?? null,
|
||||
audienceRating: plexShow.audienceRating ?? null,
|
||||
criticRating: null,
|
||||
summary: null,
|
||||
tagline: plexShow.tagline ?? null,
|
||||
identifiers: [
|
||||
@@ -1538,10 +1542,14 @@ export class PlexApiClient extends MediaSourceApiClient<PlexTypes> {
|
||||
writers: plexWriterInject(plexMovie.Writer),
|
||||
studios,
|
||||
genres: plexMovie.Genre?.map(({ tag }) => ({ name: tag })) ?? [],
|
||||
countries: plexJoinItemInject(plexMovie.Country),
|
||||
collections: plexJoinItemInject(plexMovie.Collection),
|
||||
summary: plexMovie.summary ?? null,
|
||||
plot: null,
|
||||
tagline: plexMovie.tagline ?? null,
|
||||
rating: plexMovie.contentRating ?? null,
|
||||
audienceRating: plexMovie.audienceRating ?? null,
|
||||
criticRating: plexMovie.rating ?? null,
|
||||
tags: plexMovie.Label?.map((label) => label.tag) ?? [],
|
||||
externalId: plexMovie.ratingKey,
|
||||
identifiers: [
|
||||
|
||||
@@ -207,6 +207,9 @@ export class DirectMigrationProvider implements MigrationProvider {
|
||||
migration1775060606: makeKyselyMigrationFromSqlFile(
|
||||
'./sql/0043_common_zzzax.sql',
|
||||
),
|
||||
migration1776016472: makeKyselyMigrationFromSqlFile(
|
||||
'./sql/0044_add_audience_critic_rating.sql',
|
||||
),
|
||||
},
|
||||
wrapWithTransaction,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `program` ADD `audience_rating` real;--> statement-breakpoint
|
||||
ALTER TABLE `program` ADD `critic_rating` real;
|
||||
@@ -2,7 +2,7 @@
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "11c0c7ae-4449-4d42-8243-1d68565688fc",
|
||||
"prevId": "1231ad02-0d3a-4c84-be1e-64b9067f084b",
|
||||
"prevId": "63234c90-1be6-4af9-985c-db1eb9bb694c",
|
||||
"tables": {
|
||||
"artwork": {
|
||||
"name": "artwork",
|
||||
@@ -3707,6 +3707,14 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"source": {
|
||||
"name": "source",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'media'"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
@@ -3728,7 +3736,8 @@
|
||||
"name": "tag_program_id_unique_idx",
|
||||
"columns": [
|
||||
"tag_id",
|
||||
"program_id"
|
||||
"program_id",
|
||||
"source"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
@@ -3736,7 +3745,8 @@
|
||||
"name": "tag_grouping_id_unique_idx",
|
||||
"columns": [
|
||||
"tag_id",
|
||||
"grouping_id"
|
||||
"grouping_id",
|
||||
"source"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
|
||||
4074
server/src/migration/db/sql/meta/0044_snapshot.json
Normal file
4074
server/src/migration/db/sql/meta/0044_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -309,6 +309,13 @@
|
||||
"when": 1775060587475,
|
||||
"tag": "0043_common_zzzax",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 44,
|
||||
"version": "6",
|
||||
"when": 1776016472604,
|
||||
"tag": "0044_add_audience_critic_rating",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -165,6 +165,11 @@ const ProgramsIndex: TunarrSearchIndex<ProgramSearchDocument> = {
|
||||
'grandparent.studio',
|
||||
'audioLanguages',
|
||||
'subtitleLanguages',
|
||||
'summary',
|
||||
'countries.name',
|
||||
'collections.name',
|
||||
'audienceRating',
|
||||
'criticRating',
|
||||
],
|
||||
sortable: [
|
||||
'title',
|
||||
@@ -173,6 +178,8 @@ const ProgramsIndex: TunarrSearchIndex<ProgramSearchDocument> = {
|
||||
'originalReleaseDate',
|
||||
'originalReleaseYear',
|
||||
'index',
|
||||
'audienceRating',
|
||||
'criticRating',
|
||||
],
|
||||
caseSensitiveFilters: [
|
||||
'grandparent.id',
|
||||
@@ -277,6 +284,10 @@ type BaseProgramSearchDocument = {
|
||||
studio?: Studio[];
|
||||
tags: string[];
|
||||
state: ProgramState;
|
||||
countries: StringName[];
|
||||
collections: StringName[];
|
||||
audienceRating: Nullable<number>;
|
||||
criticRating: Nullable<number>;
|
||||
};
|
||||
|
||||
export type TerminalProgramSearchDocument<
|
||||
@@ -829,6 +840,10 @@ export class MeilisearchService implements ISearchService {
|
||||
),
|
||||
tags: show.tags,
|
||||
studio: show.studios,
|
||||
countries: show.countries ?? [],
|
||||
collections: show.collections ?? [],
|
||||
audienceRating: show.audienceRating ?? null,
|
||||
criticRating: show.criticRating ?? null,
|
||||
state: 'ok',
|
||||
};
|
||||
|
||||
@@ -880,6 +895,10 @@ export class MeilisearchService implements ISearchService {
|
||||
`${eid.type}|${eid.sourceId ?? ''}|${eid.id}` satisfies MergedExternalId,
|
||||
),
|
||||
tags: season.tags,
|
||||
countries: [],
|
||||
collections: [],
|
||||
audienceRating: null,
|
||||
criticRating: null,
|
||||
state: 'ok',
|
||||
parent: {
|
||||
id: encodeCaseSensitiveId(season.show.uuid),
|
||||
@@ -1004,6 +1023,10 @@ export class MeilisearchService implements ISearchService {
|
||||
`${eid.type}|${eid.sourceId ?? ''}|${eid.id}` satisfies MergedExternalId,
|
||||
),
|
||||
tags: artist.tags,
|
||||
countries: [],
|
||||
collections: [],
|
||||
audienceRating: null,
|
||||
criticRating: null,
|
||||
state: 'ok',
|
||||
};
|
||||
|
||||
@@ -1053,6 +1076,10 @@ export class MeilisearchService implements ISearchService {
|
||||
`${eid.type}|${eid.sourceId ?? ''}|${eid.id}` satisfies MergedExternalId,
|
||||
),
|
||||
tags: album.tags,
|
||||
countries: [],
|
||||
collections: [],
|
||||
audienceRating: null,
|
||||
criticRating: null,
|
||||
state: 'ok',
|
||||
parent: {
|
||||
id: encodeCaseSensitiveId(album.artist.uuid),
|
||||
@@ -1566,6 +1593,33 @@ export class MeilisearchService implements ISearchService {
|
||||
break;
|
||||
}
|
||||
|
||||
let audienceRating: number | null;
|
||||
let criticRating: number | null;
|
||||
let countries: StringName[];
|
||||
let collections: StringName[];
|
||||
switch (program.type) {
|
||||
case 'movie':
|
||||
audienceRating = program.audienceRating ?? null;
|
||||
criticRating = program.criticRating ?? null;
|
||||
countries = program.countries ?? [];
|
||||
collections = program.collections ?? [];
|
||||
break;
|
||||
case 'episode':
|
||||
audienceRating = program.season?.show?.audienceRating ?? null;
|
||||
criticRating = program.season?.show?.criticRating ?? null;
|
||||
countries = program.season?.show?.countries ?? [];
|
||||
collections = [];
|
||||
break;
|
||||
case 'track':
|
||||
case 'other_video':
|
||||
case 'music_video':
|
||||
audienceRating = null;
|
||||
criticRating = null;
|
||||
countries = [];
|
||||
collections = [];
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
id: program.uuid,
|
||||
duration: program.duration ?? null,
|
||||
@@ -1595,6 +1649,10 @@ export class MeilisearchService implements ISearchService {
|
||||
writer: program.writers ?? [],
|
||||
studio: program.studios ?? [],
|
||||
tags: program.tags,
|
||||
countries,
|
||||
collections,
|
||||
audienceRating,
|
||||
criticRating,
|
||||
mediaSourceId: encodeCaseSensitiveId(program.mediaSourceId),
|
||||
libraryId: encodeCaseSensitiveId(program.libraryId),
|
||||
videoWidth: width,
|
||||
|
||||
@@ -62,6 +62,8 @@ const FactedStringFields = [
|
||||
'video_dynamic_range',
|
||||
'media_source_name',
|
||||
'library_name',
|
||||
'country',
|
||||
'collection',
|
||||
] as const;
|
||||
|
||||
const StringFields = [
|
||||
@@ -69,6 +71,7 @@ const StringFields = [
|
||||
'library_id',
|
||||
'title',
|
||||
'show_title',
|
||||
'summary',
|
||||
] as const;
|
||||
|
||||
const StringField = createToken({
|
||||
@@ -95,6 +98,8 @@ const NumericFields = [
|
||||
'audio_channels',
|
||||
'release_year',
|
||||
'year',
|
||||
'audience_rating',
|
||||
'critic_rating',
|
||||
] as const;
|
||||
|
||||
const NumericField = createToken({
|
||||
@@ -365,6 +370,11 @@ export const virtualFieldToIndexField: Record<string, string> = {
|
||||
audio_channels: 'audioChannels',
|
||||
// library_name: 'libraryName',
|
||||
// media_source_name: 'mediaSourceName'
|
||||
summary: 'summary',
|
||||
country: 'countries.name',
|
||||
collection: 'collections.name',
|
||||
audience_rating: 'audienceRating',
|
||||
critic_rating: 'criticRating',
|
||||
};
|
||||
|
||||
export const indexFieldToVirtualField = invert(virtualFieldToIndexField, true);
|
||||
|
||||
@@ -424,6 +424,7 @@ export const PlexMovieSchema = BasePlexMediaSchema.extend({
|
||||
Media: z.array(PlexMediaDescriptionSchema).optional(),
|
||||
Genre: z.array(PlexJoinItemSchema).optional(),
|
||||
Country: z.array(PlexJoinItemSchema).optional(),
|
||||
Collection: z.array(PlexJoinItemSchema).optional(),
|
||||
Director: z.array(PlexJoinItemSchema).optional(),
|
||||
Writer: z.array(PlexJoinItemSchema).optional(),
|
||||
Role: z.array(PlexActorSchema).optional(),
|
||||
|
||||
@@ -269,6 +269,10 @@ export const MovieMetadata = z.object({
|
||||
...WithSummaryMetadata.shape,
|
||||
type: z.literal('movie'),
|
||||
rating: z.string().nullable(),
|
||||
countries: z.array(NamedEntity).optional(),
|
||||
collections: z.array(NamedEntity).optional(),
|
||||
audienceRating: z.number().nullable().optional(),
|
||||
criticRating: z.number().nullable().optional(),
|
||||
});
|
||||
|
||||
export const Movie = z.object({
|
||||
@@ -313,6 +317,10 @@ export const ShowMetadata = z.object({
|
||||
releaseDate: z.number().nullable(),
|
||||
releaseDateString: z.string().nullable(),
|
||||
year: z.number().positive().nullable(),
|
||||
countries: z.array(NamedEntity).optional(),
|
||||
collections: z.array(NamedEntity).optional(),
|
||||
audienceRating: z.number().nullable().optional(),
|
||||
criticRating: z.number().nullable().optional(),
|
||||
get seasons(): z.ZodOptional<z.ZodArray<typeof _SeasonWithTunarrMetadata>> {
|
||||
return z.array(_SeasonWithTunarrMetadata).optional();
|
||||
},
|
||||
|
||||
@@ -244,6 +244,55 @@ export const SearchFieldSpecs: NonEmptyArray<
|
||||
uiVisible: true,
|
||||
visibleForLibraryTypes: 'all',
|
||||
},
|
||||
{
|
||||
key: 'summary',
|
||||
type: 'string' as const,
|
||||
displayName: 'Summary',
|
||||
uiVisible: true,
|
||||
visibleForLibraryTypes: ['movies', 'shows'] as NoInfer<
|
||||
ReadonlyArray<MediaSourceLibrary['mediaType']>
|
||||
>,
|
||||
} satisfies SearchFieldSpec<'string'>,
|
||||
{
|
||||
key: 'countries.name',
|
||||
name: 'country',
|
||||
type: 'faceted_string' as const,
|
||||
displayName: 'Country',
|
||||
uiVisible: true,
|
||||
visibleForLibraryTypes: ['movies', 'shows'] as NoInfer<
|
||||
ReadonlyArray<MediaSourceLibrary['mediaType']>
|
||||
>,
|
||||
} satisfies SearchFieldSpec<'faceted_string'>,
|
||||
{
|
||||
key: 'collections.name',
|
||||
name: 'collection',
|
||||
type: 'faceted_string' as const,
|
||||
displayName: 'Collection',
|
||||
uiVisible: true,
|
||||
visibleForLibraryTypes: ['movies', 'shows'] as NoInfer<
|
||||
ReadonlyArray<MediaSourceLibrary['mediaType']>
|
||||
>,
|
||||
} satisfies SearchFieldSpec<'faceted_string'>,
|
||||
{
|
||||
key: 'audienceRating',
|
||||
name: 'audience_rating',
|
||||
type: 'numeric' as const,
|
||||
displayName: 'Audience Rating',
|
||||
uiVisible: true,
|
||||
visibleForLibraryTypes: ['movies', 'shows'] as NoInfer<
|
||||
ReadonlyArray<MediaSourceLibrary['mediaType']>
|
||||
>,
|
||||
} satisfies SearchFieldSpec<'numeric'>,
|
||||
{
|
||||
key: 'criticRating',
|
||||
name: 'critic_rating',
|
||||
type: 'numeric' as const,
|
||||
displayName: 'Critic Rating',
|
||||
uiVisible: true,
|
||||
visibleForLibraryTypes: ['movies', 'shows'] as NoInfer<
|
||||
ReadonlyArray<MediaSourceLibrary['mediaType']>
|
||||
>,
|
||||
} satisfies SearchFieldSpec<'numeric'>,
|
||||
];
|
||||
|
||||
interface Bij<In, Out = In> {
|
||||
|
||||
Reference in New Issue
Block a user