mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
merge: remote-tracking branch 'immich/main' into feat/integrity-checks-izzy
This commit is contained in:
@@ -21,6 +21,7 @@ export const getAssetFiles = (files: AssetFile[]) => ({
|
||||
fullsizeFile: getAssetFile(files, AssetFileType.FullSize),
|
||||
previewFile: getAssetFile(files, AssetFileType.Preview),
|
||||
thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail),
|
||||
sidecarFile: getAssetFile(files, AssetFileType.Sidecar),
|
||||
});
|
||||
|
||||
export const addAssets = async (
|
||||
|
||||
@@ -306,6 +306,46 @@ export function withTagId<O>(qb: SelectQueryBuilder<DB, 'asset', O>, tagId: stri
|
||||
);
|
||||
}
|
||||
|
||||
const isCJK = (c: number): boolean =>
|
||||
(c >= 0x4e_00 && c <= 0x9f_ff) ||
|
||||
(c >= 0xac_00 && c <= 0xd7_af) ||
|
||||
(c >= 0x30_40 && c <= 0x30_9f) ||
|
||||
(c >= 0x30_a0 && c <= 0x30_ff) ||
|
||||
(c >= 0x34_00 && c <= 0x4d_bf);
|
||||
|
||||
export const tokenizeForSearch = (text: string): string[] => {
|
||||
/* eslint-disable unicorn/prefer-code-point */
|
||||
const tokens: string[] = [];
|
||||
let i = 0;
|
||||
while (i < text.length) {
|
||||
const c = text.charCodeAt(i);
|
||||
if (c <= 32) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const start = i;
|
||||
if (isCJK(c)) {
|
||||
while (i < text.length && isCJK(text.charCodeAt(i))) {
|
||||
i++;
|
||||
}
|
||||
if (i - start === 1) {
|
||||
tokens.push(text[start]);
|
||||
} else {
|
||||
for (let k = start; k < i - 1; k++) {
|
||||
tokens.push(text[k] + text[k + 1]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (i < text.length && text.charCodeAt(i) > 32 && !isCJK(text.charCodeAt(i))) {
|
||||
i++;
|
||||
}
|
||||
tokens.push(text.slice(start, i));
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
};
|
||||
|
||||
const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
|
||||
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */
|
||||
|
||||
@@ -391,7 +431,7 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
||||
.$if(!!options.ocr, (qb) =>
|
||||
qb
|
||||
.innerJoin('ocr_search', 'asset.id', 'ocr_search.assetId')
|
||||
.where(() => sql`f_unaccent(ocr_search.text) %>> f_unaccent(${options.ocr!})`),
|
||||
.where(() => sql`f_unaccent(ocr_search.text) %>> f_unaccent(${tokenizeForSearch(options.ocr!).join(' ')})`),
|
||||
)
|
||||
.$if(!!options.type, (qb) => qb.where('asset.type', '=', options.type!))
|
||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!))
|
||||
|
||||
@@ -704,8 +704,7 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
|
||||
}
|
||||
|
||||
getBitrateOptions() {
|
||||
const options = [];
|
||||
options.push(`-${this.useCQP() ? 'q:v' : 'global_quality:v'} ${this.config.crf}`);
|
||||
const options = [`-${this.useCQP() ? 'q:v' : 'global_quality:v'} ${this.config.crf}`];
|
||||
const bitrates = this.getBitrateDistribution();
|
||||
if (bitrates.max > 0) {
|
||||
options.push(`-maxrate ${bitrates.max}${bitrates.unit}`, `-bufsize ${bitrates.max * 2}${bitrates.unit}`);
|
||||
|
||||
@@ -152,6 +152,26 @@ describe('mimeTypes', () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe('animated image', () => {
|
||||
for (const img of ['a.avif', 'a.gif', 'a.webp']) {
|
||||
it('should identify animated image mime types as such', () => {
|
||||
expect(mimeTypes.isPossiblyAnimatedImage(img)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
for (const img of ['a.cr3', 'a.jpg', 'a.tiff']) {
|
||||
it('should identify static image mime types as such', () => {
|
||||
expect(mimeTypes.isPossiblyAnimatedImage(img)).toBeFalsy();
|
||||
});
|
||||
}
|
||||
|
||||
for (const extension of Object.keys(mimeTypes.video)) {
|
||||
it('should not identify video mime types as animated', () => {
|
||||
expect(mimeTypes.isPossiblyAnimatedImage(extension)).toBeFalsy();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('video', () => {
|
||||
it('should contain only lowercase mime types', () => {
|
||||
const keys = Object.keys(mimeTypes.video);
|
||||
|
||||
@@ -64,6 +64,11 @@ const image: Record<string, string[]> = {
|
||||
'.tiff': ['image/tiff'],
|
||||
};
|
||||
|
||||
const possiblyAnimatedImageExtensions = new Set(['.avif', '.gif', '.heic', '.heif', '.jxl', '.png', '.webp']);
|
||||
const possiblyAnimatedImage: Record<string, string[]> = Object.fromEntries(
|
||||
Object.entries(image).filter(([key]) => possiblyAnimatedImageExtensions.has(key)),
|
||||
);
|
||||
|
||||
const extensionOverrides: Record<string, string> = {
|
||||
'image/jpeg': '.jpg',
|
||||
};
|
||||
@@ -119,6 +124,7 @@ export const mimeTypes = {
|
||||
isAsset: (filename: string) => isType(filename, image) || isType(filename, video),
|
||||
isImage: (filename: string) => isType(filename, image),
|
||||
isWebSupportedImage: (filename: string) => isType(filename, webSupportedImage),
|
||||
isPossiblyAnimatedImage: (filename: string) => isType(filename, possiblyAnimatedImage),
|
||||
isProfile: (filename: string) => isType(filename, profile),
|
||||
isSidecar: (filename: string) => isType(filename, sidecar),
|
||||
isVideo: (filename: string) => isType(filename, video),
|
||||
|
||||
@@ -139,7 +139,7 @@ function sortKeys<T>(target: T): T {
|
||||
}
|
||||
|
||||
const result: Partial<T> = {};
|
||||
const keys = Object.keys(target).sort() as Array<keyof T>;
|
||||
const keys = Object.keys(target).toSorted() as Array<keyof T>;
|
||||
for (const key of keys) {
|
||||
result[key] = sortKeys(target[key]);
|
||||
}
|
||||
@@ -178,10 +178,7 @@ const patchOpenAPI = (document: OpenAPIObject) => {
|
||||
throw new Error(`Invalid number format: ${schemaName}.${key}=float (use double instead). `);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.required) {
|
||||
schema.required = schema.required.sort();
|
||||
}
|
||||
schema.required?.sort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user