mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
Merge remote-tracking branch 'origin/dev'
This commit is contained in:
@@ -447,4 +447,48 @@ describe('parsedSearchToRequest', () => {
|
||||
} satisfies SearchFilter);
|
||||
});
|
||||
});
|
||||
|
||||
test('handles audio_language mapping', () => {
|
||||
const clause = {
|
||||
type: 'single_query',
|
||||
field: 'audio_language',
|
||||
op: '=',
|
||||
value: 'eng',
|
||||
} satisfies SearchClause;
|
||||
|
||||
const request = parsedSearchToRequest(clause);
|
||||
|
||||
expect(request).toMatchObject({
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
key: 'audioLanguages',
|
||||
name: '',
|
||||
op: '=',
|
||||
type: 'string',
|
||||
value: ['eng'],
|
||||
},
|
||||
} satisfies SearchFilter);
|
||||
});
|
||||
|
||||
test('handles subtitle_language mapping', () => {
|
||||
const clause = {
|
||||
type: 'single_query',
|
||||
field: 'subtitle_language',
|
||||
op: '=',
|
||||
value: 'fra',
|
||||
} satisfies SearchClause;
|
||||
|
||||
const request = parsedSearchToRequest(clause);
|
||||
|
||||
expect(request).toMatchObject({
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
key: 'subtitleLanguages',
|
||||
name: '',
|
||||
op: '=',
|
||||
type: 'string',
|
||||
value: ['fra'],
|
||||
},
|
||||
} satisfies SearchFilter);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,9 +13,14 @@ import type {
|
||||
import { createToken, EmbeddedActionsParser, Lexer } from 'chevrotain';
|
||||
import dayjs from 'dayjs';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
|
||||
import { identity, isArray, isNumber } from 'lodash-es';
|
||||
import type { NonEmptyArray, StrictExclude, StrictOmit } from 'ts-essentials';
|
||||
import { match } from 'ts-pattern';
|
||||
import { identity, invert, isArray, isNumber } from 'lodash-es';
|
||||
import type {
|
||||
Dictionary,
|
||||
NonEmptyArray,
|
||||
StrictExclude,
|
||||
StrictOmit,
|
||||
} from 'ts-essentials';
|
||||
import { match, P } from 'ts-pattern';
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
@@ -52,7 +57,10 @@ const StringFields = [
|
||||
'type',
|
||||
'show_title',
|
||||
'show_genre',
|
||||
'show_tag',
|
||||
'show_tags',
|
||||
'show_studio',
|
||||
'audio_language',
|
||||
'subtitle_language',
|
||||
] as const;
|
||||
|
||||
const StringField = createToken({
|
||||
@@ -319,16 +327,25 @@ export const virtualFieldToIndexField: Record<string, string> = {
|
||||
// TODO: Make grouping-tyhpe specific subdocs
|
||||
show_genre: 'grandparent.genres',
|
||||
show_title: 'grandparent.title',
|
||||
show_tag: 'grandparent.tag',
|
||||
show_tags: 'grandparent.tags',
|
||||
show_studio: 'grandparent.studio',
|
||||
grandparent_genre: 'grandparent.genres',
|
||||
video_bit_depth: 'videoBitDepth',
|
||||
video_codec: 'videoCodec',
|
||||
video_height: 'videoHeight',
|
||||
video_width: 'videoWidth',
|
||||
audio_language: 'audioLanguages',
|
||||
subtitle_language: 'subtitleLanguages',
|
||||
audio_codec: 'audioCodec',
|
||||
audio_channels: 'audioChannels',
|
||||
};
|
||||
|
||||
const indexFieldToVirtualField = invert(virtualFieldToIndexField);
|
||||
|
||||
const indexOperatorToSyntax: Dictionary<string> = {
|
||||
contains: '~',
|
||||
};
|
||||
|
||||
function normalizeReleaseDate(value: string) {
|
||||
for (const format of ['YYYY-MM-DD', 'YYYYMMDD']) {
|
||||
const d = dayjs(value, format, true);
|
||||
@@ -942,6 +959,98 @@ export function parsedSearchToRequest(input: SearchClause): SearchFilter {
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSearchFilter(input: SearchFilter): SearchFilter {
|
||||
return match(input)
|
||||
.returnType<SearchFilter>()
|
||||
.with({ type: 'op', children: [P.select()] }, (child) =>
|
||||
normalizeSearchFilter(child),
|
||||
)
|
||||
.with({ type: 'op' }, (op) => ({
|
||||
...op,
|
||||
children: op.children.map(normalizeSearchFilter),
|
||||
}))
|
||||
.with({ type: 'value', fieldSpec: { type: 'numeric' } }, (numeric) => {
|
||||
const key: string =
|
||||
virtualFieldToIndexField[numeric.fieldSpec.key] ??
|
||||
numeric.fieldSpec.key;
|
||||
const valueConverter: Converter<number> =
|
||||
numeric.fieldSpec.key in numericFieldNormalizersByField
|
||||
? numericFieldNormalizersByField[
|
||||
numeric.fieldSpec
|
||||
.key as keyof typeof numericFieldNormalizersByField
|
||||
]
|
||||
: identity;
|
||||
if (isArray(numeric.fieldSpec.value)) {
|
||||
return {
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
...numeric.fieldSpec,
|
||||
key,
|
||||
value: [
|
||||
valueConverter(numeric.fieldSpec.value[0]),
|
||||
valueConverter(numeric.fieldSpec.value[1]),
|
||||
],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
...numeric.fieldSpec,
|
||||
key,
|
||||
value: valueConverter(numeric.fieldSpec.value),
|
||||
},
|
||||
};
|
||||
}
|
||||
})
|
||||
.with({ type: 'value', fieldSpec: { type: 'date' } }, ({ fieldSpec }) => {
|
||||
const key: string =
|
||||
virtualFieldToIndexField[fieldSpec.key] ?? fieldSpec.key;
|
||||
const converter = identity;
|
||||
if (isArray(fieldSpec.value)) {
|
||||
return {
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
...fieldSpec,
|
||||
key,
|
||||
value: [
|
||||
converter(fieldSpec.value[0]),
|
||||
converter(fieldSpec.value[1]),
|
||||
],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
...fieldSpec,
|
||||
key,
|
||||
value: converter(fieldSpec.value),
|
||||
},
|
||||
};
|
||||
}
|
||||
})
|
||||
.with(
|
||||
{
|
||||
type: 'value',
|
||||
fieldSpec: { type: P.union('facted_string', 'string') },
|
||||
},
|
||||
({ fieldSpec }) => {
|
||||
const key: string =
|
||||
virtualFieldToIndexField[fieldSpec.key] ?? fieldSpec.key;
|
||||
return {
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
...fieldSpec,
|
||||
key,
|
||||
type: 'string' as const,
|
||||
},
|
||||
};
|
||||
},
|
||||
)
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
export function searchFilterToString(
|
||||
input: SearchFilter,
|
||||
depth: number = 0,
|
||||
@@ -954,13 +1063,24 @@ export function searchFilterToString(
|
||||
if (depth === 0) {
|
||||
return children.join(` ${input.op.toUpperCase()} `);
|
||||
}
|
||||
if (children.length === 0) {
|
||||
return '';
|
||||
}
|
||||
if (children.length === 1) {
|
||||
return children[0];
|
||||
}
|
||||
// Wrap in parents for higher depth
|
||||
return `(${children.join(` ${input.op.toUpperCase()} `)})`;
|
||||
}
|
||||
case 'value': {
|
||||
let valueString: string;
|
||||
if (isNumber(input.fieldSpec.value)) {
|
||||
valueString = input.fieldSpec.value.toString();
|
||||
valueString =
|
||||
input.fieldSpec.type === 'date'
|
||||
? dayjs(input.fieldSpec.value).format('YYYY-MM-DD')
|
||||
: input.fieldSpec.value.toString();
|
||||
} else if (input.fieldSpec.value.length === 0) {
|
||||
return '';
|
||||
} else if (input.fieldSpec.value.length === 1) {
|
||||
const value = input.fieldSpec.value[0];
|
||||
let repr: string;
|
||||
@@ -981,7 +1101,11 @@ export function searchFilterToString(
|
||||
}
|
||||
valueString = `[${components.join(', ')}]`;
|
||||
}
|
||||
return `${input.fieldSpec.key} ${input.fieldSpec.op} ${valueString}`;
|
||||
const key =
|
||||
indexFieldToVirtualField[input.fieldSpec.key] ?? input.fieldSpec.key;
|
||||
const op =
|
||||
indexOperatorToSyntax[input.fieldSpec.op] ?? input.fieldSpec.op;
|
||||
return `${key} ${op} ${valueString}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user