From 62a490eca2e066318644964f9bead196b792480d Mon Sep 17 00:00:00 2001
From: Gus Price <42309183+GusPrice@users.noreply.github.com>
Date: Thu, 26 Sep 2024 10:34:01 -0700
Subject: [PATCH 01/21] docs: add clarity to non root user section (#12956)
* clarity
* prettier
---
docs/docs/FAQ.mdx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx
index b1a24e1788a2f..3144b1b9a8456 100644
--- a/docs/docs/FAQ.mdx
+++ b/docs/docs/FAQ.mdx
@@ -333,7 +333,11 @@ You may need to add mount points or docker volumes for the following internal co
- `immich-machine-learning:/.cache`
- `redis:/data`
-The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`.
+The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION` and `/cache` for machine-learning.
+
+:::note Docker Compose Volumes
+The Docker Compose top level volume element does not support non-root access, all of the above volumes must be local volume mounts.
+:::
For a further hardened system, you can add the following block to every container except for `immich_postgres`.
From b6f871786c969c4bc49cac9f0141f371dc2a0238 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen
Date: Thu, 26 Sep 2024 14:32:10 -0400
Subject: [PATCH 02/21] fix(server): handle numeric hierarchical subject values
(#12949)
---
server/src/interfaces/metadata.interface.ts | 21 +++++++++++++++++---
server/src/services/metadata.service.spec.ts | 15 ++++++++++++--
server/src/services/metadata.service.ts | 15 +++++++-------
3 files changed, 39 insertions(+), 12 deletions(-)
diff --git a/server/src/interfaces/metadata.interface.ts b/server/src/interfaces/metadata.interface.ts
index 1805969beb046..574420e27a1c8 100644
--- a/server/src/interfaces/metadata.interface.ts
+++ b/server/src/interfaces/metadata.interface.ts
@@ -7,7 +7,18 @@ export interface ExifDuration {
Scale?: number;
}
-type TagsWithWrongTypes = 'FocalLength' | 'Duration' | 'Description' | 'ImageDescription' | 'RegionInfo';
+type StringOrNumber = string | number;
+
+type TagsWithWrongTypes =
+ | 'FocalLength'
+ | 'Duration'
+ | 'Description'
+ | 'ImageDescription'
+ | 'RegionInfo'
+ | 'TagsList'
+ | 'Keywords'
+ | 'HierarchicalSubject'
+ | 'ISO';
export interface ImmichTags extends Omit {
ContentIdentifier?: string;
MotionPhoto?: number;
@@ -20,10 +31,14 @@ export interface ImmichTags extends Omit {
EmbeddedVideoType?: string;
EmbeddedVideoFile?: BinaryField;
MotionPhotoVideo?: BinaryField;
+ TagsList?: StringOrNumber[];
+ HierarchicalSubject?: StringOrNumber[];
+ Keywords?: StringOrNumber | StringOrNumber[];
+ ISO?: number | number[];
// Type is wrong, can also be number.
- Description?: string | number;
- ImageDescription?: string | number;
+ Description?: StringOrNumber;
+ ImageDescription?: StringOrNumber;
// Extended properties for image regions, such as faces
RegionInfo?: {
diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts
index ad01aa5784afe..c74883c283bb2 100644
--- a/server/src/services/metadata.service.spec.ts
+++ b/server/src/services/metadata.service.spec.ts
@@ -316,7 +316,7 @@ describe(MetadataService.name, () => {
it('should handle lists of numbers', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]);
- metadataMock.readTags.mockResolvedValue({ ISO: [160] as any });
+ metadataMock.readTags.mockResolvedValue({ ISO: [160] });
await sut.handleMetadataExtraction({ id: assetStub.image.id });
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
@@ -411,7 +411,7 @@ describe(MetadataService.name, () => {
it('should extract tags from Keywords as a list with a number', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]);
- metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent', 2024] as any[] });
+ metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent', 2024] });
tagMock.upsertValue.mockResolvedValue(tagStub.parent);
await sut.handleMetadataExtraction({ id: assetStub.image.id });
@@ -467,6 +467,17 @@ describe(MetadataService.name, () => {
expect(tagMock.upsertValue).toHaveBeenNthCalledWith(3, { userId: 'user-id', value: 'TagA', parent: undefined });
});
+ it('should extract tags from HierarchicalSubject as a list with a number', async () => {
+ assetMock.getByIds.mockResolvedValue([assetStub.image]);
+ metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent', 2024] });
+ tagMock.upsertValue.mockResolvedValue(tagStub.parent);
+
+ await sut.handleMetadataExtraction({ id: assetStub.image.id });
+
+ expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined });
+ expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined });
+ });
+
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]);
metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Mom/Dad'] });
diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts
index bf76be07311b2..224ef03b3b019 100644
--- a/server/src/services/metadata.service.ts
+++ b/server/src/services/metadata.service.ts
@@ -11,6 +11,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEmit } from 'src/decorators';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity';
+import { ExifEntity } from 'src/entities/exif.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { AssetType, SourceType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
@@ -236,7 +237,7 @@ export class MetadataService {
const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags);
const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding);
- const exifData = {
+ const exifData: Partial = {
assetId: asset.id,
// dates
@@ -264,7 +265,7 @@ export class MetadataService {
make: exifTags.Make ?? null,
model: exifTags.Model ?? null,
fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)),
- iso: validate(exifTags.ISO),
+ iso: validate(exifTags.ISO) as number,
exposureTime: exifTags.ExposureTime ?? null,
lensModel: exifTags.LensModel ?? null,
fNumber: validate(exifTags.FNumber),
@@ -395,13 +396,13 @@ export class MetadataService {
}
private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) {
- const tags: Array = [];
+ const tags: string[] = [];
if (exifTags.TagsList) {
- tags.push(...exifTags.TagsList);
+ tags.push(...exifTags.TagsList.map(String));
} else if (exifTags.HierarchicalSubject) {
tags.push(
...exifTags.HierarchicalSubject.map((tag) =>
- tag
+ String(tag)
// convert | to /
.replaceAll('/', '')
.replaceAll('|', '/')
@@ -413,10 +414,10 @@ export class MetadataService {
if (!Array.isArray(keywords)) {
keywords = [keywords];
}
- tags.push(...keywords);
+ tags.push(...keywords.map(String));
}
- const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags: tags.map(String) });
+ const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags });
await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds: results.map((tag) => tag.id) });
}
From a6e703ed6b52a7e26c37aa323ea1546e7e03c207 Mon Sep 17 00:00:00 2001
From: Alex
Date: Fri, 27 Sep 2024 08:11:22 +0700
Subject: [PATCH 03/21] chore(mobile): post release task (#12955)
---
mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++---
mobile/ios/Runner/Info.plist | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj
index 2d7cdc153cab8..241cb8ecd99df 100644
--- a/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ b/mobile/ios/Runner.xcodeproj/project.pbxproj
@@ -401,7 +401,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 175;
+ CURRENT_PROJECT_VERSION = 176;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 175;
+ CURRENT_PROJECT_VERSION = 176;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -571,7 +571,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 175;
+ CURRENT_PROJECT_VERSION = 176;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index 1831798a4288e..14fc27b56dd61 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -58,11 +58,11 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.115.0
+ 1.116.0
CFBundleSignature
????
CFBundleVersion
- 175
+ 176
FLTEnableImpeller
ITSAppUsesNonExemptEncryption
From 42ad3e6bb0bc897ec341092db911939fe9fba89b Mon Sep 17 00:00:00 2001
From: Lauritz Tieste <84938977+Lauritz-Tieste@users.noreply.github.com>
Date: Fri, 27 Sep 2024 03:40:07 +0200
Subject: [PATCH 04/21] fix(mobile): navigation panel overlaps with right
rotate (#12950)
fix: navigation panel overlaps with right rotate
---
mobile/lib/pages/editing/crop.page.dart | 184 ++++++++++++------------
1 file changed, 93 insertions(+), 91 deletions(-)
diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart
index 729b59ded5911..8bfb8c8bb9bf9 100644
--- a/mobile/lib/pages/editing/crop.page.dart
+++ b/mobile/lib/pages/editing/crop.page.dart
@@ -51,107 +51,109 @@ class CropImagePage extends HookWidget {
],
),
backgroundColor: context.scaffoldBackgroundColor,
- body: LayoutBuilder(
- builder: (BuildContext context, BoxConstraints constraints) {
- return Column(
- children: [
- Container(
- padding: const EdgeInsets.only(top: 20),
- width: constraints.maxWidth * 0.9,
- height: constraints.maxHeight * 0.6,
- child: CropImage(
- controller: cropController,
- image: image,
- gridColor: Colors.white,
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints constraints) {
+ return Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.only(top: 20),
+ width: constraints.maxWidth * 0.9,
+ height: constraints.maxHeight * 0.6,
+ child: CropImage(
+ controller: cropController,
+ image: image,
+ gridColor: Colors.white,
+ ),
),
- ),
- Expanded(
- child: Container(
- width: double.infinity,
- decoration: BoxDecoration(
- color: context.scaffoldBackgroundColor,
- borderRadius: const BorderRadius.only(
- topLeft: Radius.circular(20),
- topRight: Radius.circular(20),
+ Expanded(
+ child: Container(
+ width: double.infinity,
+ decoration: BoxDecoration(
+ color: context.scaffoldBackgroundColor,
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(20),
+ topRight: Radius.circular(20),
+ ),
),
- ),
- child: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Padding(
- padding: const EdgeInsets.only(
- left: 20,
- right: 20,
- bottom: 10,
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- IconButton(
- icon: Icon(
- Icons.rotate_left,
- color: Theme.of(context).iconTheme.color,
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ left: 20,
+ right: 20,
+ bottom: 10,
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ IconButton(
+ icon: Icon(
+ Icons.rotate_left,
+ color: Theme.of(context).iconTheme.color,
+ ),
+ onPressed: () {
+ cropController.rotateLeft();
+ },
),
- onPressed: () {
- cropController.rotateLeft();
- },
- ),
- IconButton(
- icon: Icon(
- Icons.rotate_right,
- color: Theme.of(context).iconTheme.color,
+ IconButton(
+ icon: Icon(
+ Icons.rotate_right,
+ color: Theme.of(context).iconTheme.color,
+ ),
+ onPressed: () {
+ cropController.rotateRight();
+ },
),
- onPressed: () {
- cropController.rotateRight();
- },
+ ],
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ _AspectRatioButton(
+ cropController: cropController,
+ aspectRatio: aspectRatio,
+ ratio: null,
+ label: 'Free',
+ ),
+ _AspectRatioButton(
+ cropController: cropController,
+ aspectRatio: aspectRatio,
+ ratio: 1.0,
+ label: '1:1',
+ ),
+ _AspectRatioButton(
+ cropController: cropController,
+ aspectRatio: aspectRatio,
+ ratio: 16.0 / 9.0,
+ label: '16:9',
+ ),
+ _AspectRatioButton(
+ cropController: cropController,
+ aspectRatio: aspectRatio,
+ ratio: 3.0 / 2.0,
+ label: '3:2',
+ ),
+ _AspectRatioButton(
+ cropController: cropController,
+ aspectRatio: aspectRatio,
+ ratio: 7.0 / 5.0,
+ label: '7:5',
),
],
),
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- _AspectRatioButton(
- cropController: cropController,
- aspectRatio: aspectRatio,
- ratio: null,
- label: 'Free',
- ),
- _AspectRatioButton(
- cropController: cropController,
- aspectRatio: aspectRatio,
- ratio: 1.0,
- label: '1:1',
- ),
- _AspectRatioButton(
- cropController: cropController,
- aspectRatio: aspectRatio,
- ratio: 16.0 / 9.0,
- label: '16:9',
- ),
- _AspectRatioButton(
- cropController: cropController,
- aspectRatio: aspectRatio,
- ratio: 3.0 / 2.0,
- label: '3:2',
- ),
- _AspectRatioButton(
- cropController: cropController,
- aspectRatio: aspectRatio,
- ratio: 7.0 / 5.0,
- label: '7:5',
- ),
- ],
- ),
- ],
+ ],
+ ),
),
),
),
- ),
- ],
- );
- },
+ ],
+ );
+ },
+ ),
),
);
}
From c86fa81e477cb03a7ddc4beb22578b78f6a507db Mon Sep 17 00:00:00 2001
From: Spencer Fasulo <92827970+spfncer@users.noreply.github.com>
Date: Thu, 26 Sep 2024 21:41:22 -0400
Subject: [PATCH 05/21] docs(web): JSDoc comments for svelte actions (#12963)
* Web: JSDoc comments for Actions
* Remove comment
---
web/src/lib/actions/click-outside.ts | 6 ++++++
web/src/lib/actions/focus-outside.ts | 5 +++++
web/src/lib/actions/focus.ts | 1 +
web/src/lib/actions/intersection-observer.ts | 8 ++++++++
web/src/lib/actions/list-navigation.ts | 5 +++++
web/src/lib/actions/shortcut.ts | 7 +++++++
web/src/lib/actions/thumbhash.ts | 5 +++++
7 files changed, 37 insertions(+)
diff --git a/web/src/lib/actions/click-outside.ts b/web/src/lib/actions/click-outside.ts
index bbcb0c405b718..1a421f1f5625e 100644
--- a/web/src/lib/actions/click-outside.ts
+++ b/web/src/lib/actions/click-outside.ts
@@ -6,6 +6,12 @@ interface Options {
onEscape?: () => void;
}
+/**
+ * Calls a function when a click occurs outside of the element, or when the escape key is pressed.
+ * @param node
+ * @param options Object containing onOutclick and onEscape functions
+ * @returns
+ */
export function clickOutside(node: HTMLElement, options: Options = {}): ActionReturn {
const { onOutclick, onEscape } = options;
diff --git a/web/src/lib/actions/focus-outside.ts b/web/src/lib/actions/focus-outside.ts
index 2266ea8f0ff83..c302e33d4ca2c 100644
--- a/web/src/lib/actions/focus-outside.ts
+++ b/web/src/lib/actions/focus-outside.ts
@@ -2,6 +2,11 @@ interface Options {
onFocusOut?: (event: FocusEvent) => void;
}
+/**
+ * Calls a function when focus leaves the element.
+ * @param node
+ * @param options Object containing onFocusOut function
+ */
export function focusOutside(node: HTMLElement, options: Options = {}) {
const { onFocusOut } = options;
diff --git a/web/src/lib/actions/focus.ts b/web/src/lib/actions/focus.ts
index 81185625f7332..3b6049f24732f 100644
--- a/web/src/lib/actions/focus.ts
+++ b/web/src/lib/actions/focus.ts
@@ -1,3 +1,4 @@
+/** Focus the given element when it is mounted. */
export const initInput = (element: HTMLInputElement) => {
element.focus();
};
diff --git a/web/src/lib/actions/intersection-observer.ts b/web/src/lib/actions/intersection-observer.ts
index 700ae0c3733b4..edbc07e5c1f09 100644
--- a/web/src/lib/actions/intersection-observer.ts
+++ b/web/src/lib/actions/intersection-observer.ts
@@ -13,7 +13,9 @@ type OnIntersectCallback = (entryOrElement: IntersectionObserverEntry | HTMLElem
type OnSeparateCallback = (element: HTMLElement) => unknown;
type IntersectionObserverActionProperties = {
key?: string;
+ /** Function to execute when the element leaves the viewport */
onSeparate?: OnSeparateCallback;
+ /** Function to execute when the element enters the viewport */
onIntersect?: OnIntersectCallback;
root?: Element | Document | null;
@@ -112,6 +114,12 @@ function _intersectionObserver(
};
}
+/**
+ * Monitors an element's visibility in the viewport and calls functions when it enters or leaves (based on a threshold).
+ * @param element
+ * @param properties One or multiple configurations for the IntersectionObserver(s)
+ * @returns
+ */
export function intersectionObserver(
element: HTMLElement,
properties: IntersectionObserverActionProperties | IntersectionObserverActionProperties[],
diff --git a/web/src/lib/actions/list-navigation.ts b/web/src/lib/actions/list-navigation.ts
index b981f675214fe..8f8ed62ed009e 100644
--- a/web/src/lib/actions/list-navigation.ts
+++ b/web/src/lib/actions/list-navigation.ts
@@ -1,6 +1,11 @@
import { shortcuts } from '$lib/actions/shortcut';
import type { Action } from 'svelte/action';
+/**
+ * Enables keyboard navigation (up and down arrows) for a list of elements.
+ * @param node Element which listens for keyboard events
+ * @param container Element containing the list of elements
+ */
export const listNavigation: Action = (node, container: HTMLElement) => {
const moveFocus = (direction: 'up' | 'down') => {
const children = Array.from(container?.children);
diff --git a/web/src/lib/actions/shortcut.ts b/web/src/lib/actions/shortcut.ts
index df155ea821ad0..6348257c40496 100644
--- a/web/src/lib/actions/shortcut.ts
+++ b/web/src/lib/actions/shortcut.ts
@@ -10,11 +10,16 @@ export type Shortcut = {
export type ShortcutOptions = {
shortcut: Shortcut;
+ /** If true, the event handler will not execute if the event comes from an input field */
ignoreInputFields?: boolean;
onShortcut: (event: KeyboardEvent & { currentTarget: T }) => unknown;
preventDefault?: boolean;
};
+/** Determines whether an event should be ignored. The event will be ignored if:
+ * - The element dispatching the event is not the same as the element which the event listener is attached to
+ * - The element dispatching the event is an input field
+ */
export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => {
if (event.target === event.currentTarget) {
return false;
@@ -33,6 +38,7 @@ export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => {
);
};
+/** Bind a single keyboard shortcut to node. */
export const shortcut = (
node: T,
option: ShortcutOptions,
@@ -47,6 +53,7 @@ export const shortcut = (
};
};
+/** Binds multiple keyboard shortcuts to node */
export const shortcuts = (
node: T,
options: ShortcutOptions[],
diff --git a/web/src/lib/actions/thumbhash.ts b/web/src/lib/actions/thumbhash.ts
index ab9d28ffc9b4d..e49f04dbee546 100644
--- a/web/src/lib/actions/thumbhash.ts
+++ b/web/src/lib/actions/thumbhash.ts
@@ -1,6 +1,11 @@
import { decodeBase64 } from '$lib/utils';
import { thumbHashToRGBA } from 'thumbhash';
+/**
+ * Renders a thumbnail onto a canvas from a base64 encoded hash.
+ * @param canvas
+ * @param param1 object containing the base64 encoded hash (base64Thumbhash: yourString)
+ */
export function thumbhash(canvas: HTMLCanvasElement, { base64ThumbHash }: { base64ThumbHash: string }) {
const ctx = canvas.getContext('2d');
if (ctx) {
From 26f33652e179e5f3336f8a326dbab1da05c822de Mon Sep 17 00:00:00 2001
From: KD-MM2 <57068549+KD-MM2@users.noreply.github.com>
Date: Fri, 27 Sep 2024 10:57:26 +0900
Subject: [PATCH 06/21] feat(docs): add Vietnamese translation (#12967)
* feat(readme): add Vietnamese translation
* feat(readme): add Vietnamese translation
* refactor(readme): update Vietnamese translation section
---------
Co-authored-by: tdcaot
---
README.md | 1 +
readme_i18n/README_vi_VN.md | 132 ++++++++++++++++++++++++++++++++++++
2 files changed, 133 insertions(+)
create mode 100644 readme_i18n/README_vi_VN.md
diff --git a/README.md b/README.md
index 44c38e6d14813..5c4b9c39edd1d 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@
Português Brasileiro
Svenska
العربية
+Tiếng Việt
diff --git a/readme_i18n/README_vi_VN.md b/readme_i18n/README_vi_VN.md
new file mode 100644
index 0000000000000..17345ef75daf0
--- /dev/null
+++ b/readme_i18n/README_vi_VN.md
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+Giải pháp quản lý ảnh và video tự lưu trữ hiệu suất cao
+
+
+
+
+
+
+
+Català
+Español
+Français
+Italiano
+日本語
+한국어
+Deutsch
+Nederlands
+Türkçe
+中文
+Русский
+Português Brasileiro
+Svenska
+العربية
+Tiếng Việt
+
+
+
+## Tuyên bố miễn trừ trách nhiệm
+
+- ⚠️ Dự án đang được phát triển **rất tích cực**.
+- ⚠️ Dự kiến sẽ có lỗi và thay đổi đột ngột.
+- ⚠️ **Không sử dụng ứng dụng như là cách duy nhất để lưu trữ ảnh và video của bạn.**
+- ⚠️ Luôn tuân thủ kế hoạch sao lưu [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) cho những bức ảnh và video quý giá của bạn!
+
+> [!NOTE]
+> Bạn có thể tìm thấy tài liệu chính, bao gồm hướng dẫn cài đặt, tại https://immich.app/.
+
+## Liên kết
+
+- [Tài liệu](https://immich.app/docs)
+- [Giới thiệu](https://immich.app/docs/overview/introduction)
+- [Cài đặt](https://immich.app/docs/install/requirements)
+- [Lộ trình](https://immich.app/roadmap)
+- [Demo](#demo)
+- [Tính năng](#features)
+- [Dịch thuật](https://immich.app/docs/developer/translations)
+- [Đóng góp](https://immich.app/docs/overview/support-the-project)
+
+## Demo
+
+Truy cập bản demo [tại đây](https://demo.immich.app). Bản demo đang chạy trên máy ảo Oracle Free-tier ở Amsterdam với CPU ARM64 lõi tứ 2,4 GHz và RAM 24 GB.
+
+Đối với ứng dụng di động, bạn có thể sử dụng `https://demo.immich.app/api` cho `Server Endpoint URL`
+
+### Thông tin đăng nhập
+
+| Email | Mật khẩu |
+| --------------- | -------- |
+| demo@immich.app | demo |
+
+## Tính năng
+
+| Tính năng | Mobile | Web |
+| :------------------------------------------- | ------ | --- |
+| Tải lên và xem video, ảnh | Có | Có |
+| Tự động sao lưu khi ứng dụng được mở | Có | N/A |
+| Ngăn chặn sự trùng lặp nội dung | Có | Có |
+| Album được chọn để sao lưu | Có | N/A |
+| Tải ảnh và video xuống thiết bị cục bộ | Có | Có |
+| Hỗ trợ nhiều người dùng | Có | Có |
+| Album và Album được chia sẻ | Có | Có |
+| Thanh cuộn có thể chà / kéo | Có | Có |
+| Hỗ trợ định dạng raw | Có | Có |
+| Xem metadata (EXIF, bản đồ) | Có | Có |
+| Tìm kiếm theo metadata, đối tượng, khuôn mặt và CLIP | Có | Có |
+| Chức năng quản trị (quản lý người dùng) | Không | Có |
+| Sao lưu trong nền | Có | N/A |
+| Cuộn ảo | Có | Có |
+| Hỗ trợ OAuth | Có | Có |
+| API Keys | N/A | Có |
+| Sao lưu và phát lại Live Photo/Motion Photo | Có | Có |
+| Hỗ trợ hiển thị hình ảnh 360 độ | Không | Có |
+| Cấu trúc lưu trữ do người dùng xác định | Có | Có |
+| Chia sẻ công khai | Có | Có |
+| Lưu trữ và Yêu thích | Có | Có |
+| Bản đồ toàn cầu | Có | Có |
+| Chia sẻ đối tác | Có | Có |
+| Nhận dạng khuôn mặt và phân cụm | Có | Có |
+| Kỷ niệm (x năm trước) | Có | Có |
+| Hỗ trợ ngoại tuyến | Có | Không |
+| Thư viện chỉ đọc | Có | Có |
+| Ảnh xếp chồng | Có | Có |
+
+## Dịch thuật
+
+Đọc thêm về dịch thuật [tại đây](https://immich.app/docs/developer/translations).
+
+
+
+
+
+## Hoạt động của repository
+
+![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image")
+
+## Lịch sử Đánh dấu sao
+
+
+
+
+
+
+
+
+
+## Người đóng góp
+
+
+
+
From d5ee823fbceab9179b97bddb07920045e2c65477 Mon Sep 17 00:00:00 2001
From: KD-MM2 <57068549+KD-MM2@users.noreply.github.com>
Date: Fri, 27 Sep 2024 11:40:00 +0900
Subject: [PATCH 07/21] refactor(docs): fix heading tag, update Vietnamese
translation for image alt, formatting features table (#12971)
* feat(readme): add Vietnamese translation
* feat(readme): add Vietnamese translation
* refactor(readme): update Vietnamese translation section
* Update README_vi_VN.md
* refactor(docs): fix heading tag, update Vietnamese translation for image alt, formatting features table
---------
Co-authored-by: tdcaot
---
readme_i18n/README_vi_VN.md | 105 ++++++++++++++++++------------------
1 file changed, 53 insertions(+), 52 deletions(-)
diff --git a/readme_i18n/README_vi_VN.md b/readme_i18n/README_vi_VN.md
index 17345ef75daf0..7ec4b9c948a2a 100644
--- a/readme_i18n/README_vi_VN.md
+++ b/readme_i18n/README_vi_VN.md
@@ -1,6 +1,6 @@
-
+
@@ -9,31 +9,32 @@
-
+
Giải pháp quản lý ảnh và video tự lưu trữ hiệu suất cao
-
+
-Català
-Español
-Français
-Italiano
-日本語
-한국어
-Deutsch
-Nederlands
-Türkçe
-中文
-Русский
-Português Brasileiro
-Svenska
-العربية
-Tiếng Việt
+English
+Català
+Español
+Français
+Italiano
+日本語
+한국어
+Deutsch
+Nederlands
+Türkçe
+中文
+Русский
+Português Brasileiro
+Svenska
+العربية
+Tiếng Việt
@@ -54,7 +55,7 @@
- [Cài đặt](https://immich.app/docs/install/requirements)
- [Lộ trình](https://immich.app/roadmap)
- [Demo](#demo)
-- [Tính năng](#features)
+- [Tính năng](#Tính-năng)
- [Dịch thuật](https://immich.app/docs/developer/translations)
- [Đóng góp](https://immich.app/docs/overview/support-the-project)
@@ -72,48 +73,48 @@ Truy cập bản demo [tại đây](https://demo.immich.app). Bản demo đang c
## Tính năng
-| Tính năng | Mobile | Web |
-| :------------------------------------------- | ------ | --- |
-| Tải lên và xem video, ảnh | Có | Có |
-| Tự động sao lưu khi ứng dụng được mở | Có | N/A |
-| Ngăn chặn sự trùng lặp nội dung | Có | Có |
-| Album được chọn để sao lưu | Có | N/A |
-| Tải ảnh và video xuống thiết bị cục bộ | Có | Có |
-| Hỗ trợ nhiều người dùng | Có | Có |
-| Album và Album được chia sẻ | Có | Có |
-| Thanh cuộn có thể chà / kéo | Có | Có |
-| Hỗ trợ định dạng raw | Có | Có |
-| Xem metadata (EXIF, bản đồ) | Có | Có |
-| Tìm kiếm theo metadata, đối tượng, khuôn mặt và CLIP | Có | Có |
-| Chức năng quản trị (quản lý người dùng) | Không | Có |
-| Sao lưu trong nền | Có | N/A |
-| Cuộn ảo | Có | Có |
-| Hỗ trợ OAuth | Có | Có |
-| API Keys | N/A | Có |
-| Sao lưu và phát lại Live Photo/Motion Photo | Có | Có |
-| Hỗ trợ hiển thị hình ảnh 360 độ | Không | Có |
-| Cấu trúc lưu trữ do người dùng xác định | Có | Có |
-| Chia sẻ công khai | Có | Có |
-| Lưu trữ và Yêu thích | Có | Có |
-| Bản đồ toàn cầu | Có | Có |
-| Chia sẻ đối tác | Có | Có |
-| Nhận dạng khuôn mặt và phân cụm | Có | Có |
-| Kỷ niệm (x năm trước) | Có | Có |
-| Hỗ trợ ngoại tuyến | Có | Không |
-| Thư viện chỉ đọc | Có | Có |
-| Ảnh xếp chồng | Có | Có |
+| Tính năng | Mobile | Web |
+| :--------------------------------------------------- | ------ | ----- |
+| Tải lên và xem video, ảnh | Có | Có |
+| Tự động sao lưu khi ứng dụng được mở | Có | N/A |
+| Ngăn chặn sự trùng lặp nội dung | Có | Có |
+| Album được chọn để sao lưu | Có | N/A |
+| Tải ảnh và video xuống thiết bị cục bộ | Có | Có |
+| Hỗ trợ nhiều người dùng | Có | Có |
+| Album và Album được chia sẻ | Có | Có |
+| Thanh cuộn có thể chà / kéo | Có | Có |
+| Hỗ trợ định dạng raw | Có | Có |
+| Xem metadata (EXIF, bản đồ) | Có | Có |
+| Tìm kiếm theo metadata, đối tượng, khuôn mặt và CLIP | Có | Có |
+| Chức năng quản trị (quản lý người dùng) | Không | Có |
+| Sao lưu trong nền | Có | N/A |
+| Cuộn ảo | Có | Có |
+| Hỗ trợ OAuth | Có | Có |
+| API Keys | N/A | Có |
+| Sao lưu và phát lại Live Photo/Motion Photo | Có | Có |
+| Hỗ trợ hiển thị hình ảnh 360 độ | Không | Có |
+| Cấu trúc lưu trữ do người dùng xác định | Có | Có |
+| Chia sẻ công khai | Có | Có |
+| Lưu trữ và Yêu thích | Có | Có |
+| Bản đồ toàn cầu | Có | Có |
+| Chia sẻ đối tác | Có | Có |
+| Nhận dạng khuôn mặt và phân cụm | Có | Có |
+| Kỷ niệm (x năm trước) | Có | Có |
+| Hỗ trợ ngoại tuyến | Có | Không |
+| Thư viện chỉ đọc | Có | Có |
+| Ảnh xếp chồng | Có | Có |
## Dịch thuật
Đọc thêm về dịch thuật [tại đây](https://immich.app/docs/developer/translations).
-
+
## Hoạt động của repository
-![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image")
+![Hoạt động](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Hình ảnh phân tích Repobeats")
## Lịch sử Đánh dấu sao
@@ -121,7 +122,7 @@ Truy cập bản demo [tại đây](https://demo.immich.app). Bản demo đang c
-
+
From 971ba63447834f5d6e51437099aa22523c4991a6 Mon Sep 17 00:00:00 2001
From: Alex
Date: Fri, 27 Sep 2024 09:40:55 +0700
Subject: [PATCH 08/21] fix(mobile): uninitialize provider causes unable to
logging in (#12970)
fix(mobile): use uninitialize provider
---
mobile/lib/providers/app_life_cycle.provider.dart | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart
index 938961efb62ac..5561d3fefd683 100644
--- a/mobile/lib/providers/app_life_cycle.provider.dart
+++ b/mobile/lib/providers/app_life_cycle.provider.dart
@@ -86,12 +86,16 @@ class AppLifeCycleNotifier extends StateNotifier {
void handleAppPause() {
state = AppLifeCycleEnum.paused;
_wasPaused = true;
- // Do not cancel backup if manual upload is in progress
- if (_ref.read(backupProvider.notifier).backupProgress !=
- BackUpProgressEnum.manualInProgress) {
- _ref.read(backupProvider.notifier).cancelBackup();
+
+ if (_ref.read(authenticationProvider).isAuthenticated) {
+ // Do not cancel backup if manual upload is in progress
+ if (_ref.read(backupProvider.notifier).backupProgress !=
+ BackUpProgressEnum.manualInProgress) {
+ _ref.read(backupProvider.notifier).cancelBackup();
+ }
+ _ref.read(websocketProvider.notifier).disconnect();
}
- _ref.read(websocketProvider.notifier).disconnect();
+
ImmichLogger().flush();
}
From 5b282733fecf3ffa44c2c485c044bbb7d2ee7888 Mon Sep 17 00:00:00 2001
From: Ryan Ribeiro <47592751+ryanrpj@users.noreply.github.com>
Date: Fri, 27 Sep 2024 09:15:25 -0300
Subject: [PATCH 09/21] chore(Brazilian README): fix broken image links and
update translation (#12980)
---
readme_i18n/README_pt_BR.md | 135 ++++++++++++++++++++++--------------
1 file changed, 82 insertions(+), 53 deletions(-)
diff --git a/readme_i18n/README_pt_BR.md b/readme_i18n/README_pt_BR.md
index d872b8435b058..51ea8238dabb0 100644
--- a/readme_i18n/README_pt_BR.md
+++ b/readme_i18n/README_pt_BR.md
@@ -1,84 +1,90 @@
-
-
+
+
-
-
+
+
-
+
-Immich - Solução self-hosted de alta performance para backup de fotos e vídeos
+Solução self-hosted de alta performance para backup de fotos e vídeos
-
+
- English
- Català
- Español
- Français
- Italiano
- 日本語
- 한국어
- Deutsch
- Nederlands
- Türkçe
- 中文
- Русский
- Svenska
- العربية
+
+English
+Català
+Español
+Français
+Italiano
+日本語
+한국어
+Deutsch
+Nederlands
+Türkçe
+中文
+Русский
+Svenska
+العربية
+Tiếng Việt
+
## Avisos
- ⚠️ Este projeto está sob **desenvolvimento constante**.
-- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a compatibilidade com versões anteriores).
-- ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e vídeos.**
-- ⚠️ Sempre siga o plano [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) de backup para as suas mídias preciosas!
+- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a
+ compatibilidade com versões anteriores).
+- ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e
+ vídeos.**
+- ⚠️ Sempre siga o plano
+ [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) de backup
+ para as suas mídias preciosas!
-## Conteúdo
+> [!NOTE]
+> Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/.
-- [Documentação Oficial](https://immich.app/docs)
-- [Roadmap](https://github.com/orgs/immich-app/projects/1)
-- [Demonstração](#demo)
-- [Recursos](#features)
-- [Introdução](https://immich.app/docs/overview/introduction)
+## Links
+
+- [Documentação](https://immich.app/docs)
+- [Sobre](https://immich.app/docs/overview/introduction)
- [Instalação](https://immich.app/docs/install/requirements)
+- [Roadmap](https://github.com/orgs/immich-app/projects/1)
+- [Demonstração](#demonstração)
+- [Funcionalidades](#funcionalidades)
+- [Traduções](https://immich.app/docs/developer/translations)
- [Diretrizes de Contribuição](https://immich.app/docs/overview/support-the-project)
-## Documentação
-
-Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/.
-
## Demonstração
-Você pode acessar a demonstração web em https://demo.immich.app
+Acesse a demonstração [aqui](https://demo.immich.app). A demonstração está
+hospedada no Nível Gratuito da Oracle VM em Amsterdam com um processador 2.4Ghz
+quad-core ARM64 e 24GB de RAM.
-No aplicativo para dispositivos móveis, você pode usar `https://demo.immich.app/api` no campo `Server Endpoint URL`
+No aplicativo para dispositivos móveis, você pode usar
+`https://demo.immich.app/api` no campo `Server Endpoint URL`
-```bash title="Credenciais de Demonstração"
-Credenciais de Demonstração
-email: demo@immich.app
-senha: demo
-```
+### Credenciais de login
-```
-Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
-```
+| Email | Senha |
+| --------------- | ----- |
+| demo@immich.app | demo |
## Atividades
-![Atividades](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imagem de Analytics do Repobeats")
-## Recursos
+![Atividades](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imagem de Analytics do Repobeats")
+## Funcionalidades
-| Recursos | Aplicativo Móvel | Web |
-|:----------------------------------------------------|------------------|-----|
+| Funcionalidades | Aplicativo Móvel | Web |
+| :-------------------------------------------------- | ---------------- | --- |
| Fazer upload e visualizar fotos e vídeos | Sim | Sim |
| Backup automático ao abrir o aplicativo | Sim | N/A |
| Prevenir a duplicação de arquivos | Sim | Sim |
@@ -88,17 +94,17 @@ Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core AR
| Criação de álbuns e álbuns compartilhados | Sim | Sim |
| Barra de rolagem arrastável | Sim | Sim |
| Suporta formatos RAW | Sim | Sim |
-| Visualização de metadados (EXIF, map) | Sim | Sim |
-| Pesquisar por metadados, objetos, rostos, and CLIP | Sim | Sim |
+| Visualização de metadados (EXIF, mapa) | Sim | Sim |
+| Pesquisar por metadados, objetos, rostos, e CLIP | Sim | Sim |
| Funções administrativas (gerenciamento de usuários) | Não | Sim |
| Backup em segundo plano | Sim | N/A |
-| Virtual scroll | Sim | Sim |
+| Rolagem virtual | Sim | Sim |
| Suporte OAuth | Sim | Sim |
| Chaves de API | N/A | Sim |
-| Backup e visualização de LivePhoto/MotionPhoto | Sim | Sim |
+| Backup e reprodução de LivePhoto/MotionPhoto | Sim | Sim |
| Visualização de imagens 360º | Não | Sim |
| Estrutura de armazenamento definida pelo usuário | Sim | Sim |
-| Compartilhar com o público | Não | Sim |
+| Compartilhar com o público | Sim | Sim |
| Arquivo e Favoritos | Sim | Sim |
| Mapa Global | Sim | Sim |
| Compartilhamento com parceiro | Sim | Sim |
@@ -108,6 +114,29 @@ Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core AR
| Galeria em modo apenas leitura | Sim | Sim |
| Empilhamento de fotos | Sim | Sim |
+## Traduções
+
+Leia mais sobre as traduções
+[aqui](https://immich.app/docs/developer/translations).
+
+
+
+
+
+## Atividade do repositório
+
+![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imagem de análise de atividade Repobeats")
+
+## Histórico de estrelas
+
+
+
+
+
+
+
+
+
## Contribuidores
From 12da25002877fda49908e26e3925ce502c73d7f3 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen
Date: Fri, 27 Sep 2024 10:28:42 -0400
Subject: [PATCH 10/21] refactor: enums (#12988)
---
server/src/config.ts | 87 ++----------
server/src/constants.ts | 5 -
.../src/controllers/asset-media.controller.ts | 5 +-
server/src/controllers/asset.controller.ts | 4 +-
server/src/controllers/auth.controller.ts | 2 +-
server/src/controllers/oauth.controller.ts | 2 +-
server/src/controllers/user.controller.ts | 5 +-
server/src/cores/storage.core.ts | 12 +-
server/src/decorators.ts | 4 +-
server/src/dtos/audit.dto.ts | 3 +-
server/src/dtos/system-config.dto.ts | 6 +-
server/src/entities/move.entity.ts | 19 +--
server/src/enum.ts | 130 ++++++++++++++++++
server/src/interfaces/logger.interface.ts | 2 +-
server/src/interfaces/media.interface.ts | 2 +-
server/src/interfaces/move.interface.ts | 3 +-
server/src/main.ts | 2 +-
server/src/middleware/auth.guard.ts | 16 +--
.../src/middleware/file-upload.interceptor.ts | 14 +-
server/src/repositories/asset.repository.ts | 4 +-
server/src/repositories/logger.repository.ts | 2 +-
server/src/repositories/media.repository.ts | 2 +-
server/src/repositories/move.repository.ts | 3 +-
server/src/repositories/person.repository.ts | 4 +-
server/src/repositories/search.repository.ts | 4 +-
.../src/services/asset-media.service.spec.ts | 4 +-
server/src/services/asset-media.service.ts | 6 +-
server/src/services/audit.service.ts | 13 +-
server/src/services/auth.service.spec.ts | 2 +-
server/src/services/auth.service.ts | 4 +-
server/src/services/media.service.spec.ts | 7 +-
server/src/services/media.service.ts | 16 ++-
server/src/services/person.service.spec.ts | 5 +-
server/src/services/person.service.ts | 14 +-
server/src/services/server.service.ts | 4 +-
.../services/storage-template.service.spec.ts | 2 +-
.../src/services/storage-template.service.ts | 5 +-
server/src/services/storage.service.ts | 4 +-
.../services/system-config.service.spec.ts | 9 +-
server/src/services/system-config.service.ts | 3 +-
server/src/services/user.service.spec.ts | 4 +-
server/src/services/user.service.ts | 6 +-
server/src/utils/events.ts | 4 +-
server/src/utils/file.ts | 7 +-
server/src/utils/media.ts | 2 +-
server/src/utils/misc.ts | 4 +-
server/src/utils/pagination.ts | 6 +-
47 files changed, 252 insertions(+), 221 deletions(-)
diff --git a/server/src/config.ts b/server/src/config.ts
index 03ea3f111b9ac..1522371487e3b 100644
--- a/server/src/config.ts
+++ b/server/src/config.ts
@@ -7,83 +7,20 @@ import { RedisOptions } from 'ioredis';
import Joi, { Root } from 'joi';
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
import { ImmichHeader } from 'src/dtos/auth.dto';
+import {
+ AudioCodec,
+ Colorspace,
+ CQMode,
+ ImageFormat,
+ LogLevel,
+ ToneMapping,
+ TranscodeHWAccel,
+ TranscodePolicy,
+ VideoCodec,
+ VideoContainer,
+} from 'src/enum';
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
-export enum TranscodePolicy {
- ALL = 'all',
- OPTIMAL = 'optimal',
- BITRATE = 'bitrate',
- REQUIRED = 'required',
- DISABLED = 'disabled',
-}
-
-export enum TranscodeTarget {
- NONE,
- AUDIO,
- VIDEO,
- ALL,
-}
-
-export enum VideoCodec {
- H264 = 'h264',
- HEVC = 'hevc',
- VP9 = 'vp9',
- AV1 = 'av1',
-}
-
-export enum AudioCodec {
- MP3 = 'mp3',
- AAC = 'aac',
- LIBOPUS = 'libopus',
-}
-
-export enum VideoContainer {
- MOV = 'mov',
- MP4 = 'mp4',
- OGG = 'ogg',
- WEBM = 'webm',
-}
-
-export enum TranscodeHWAccel {
- NVENC = 'nvenc',
- QSV = 'qsv',
- VAAPI = 'vaapi',
- RKMPP = 'rkmpp',
- DISABLED = 'disabled',
-}
-
-export enum ToneMapping {
- HABLE = 'hable',
- MOBIUS = 'mobius',
- REINHARD = 'reinhard',
- DISABLED = 'disabled',
-}
-
-export enum CQMode {
- AUTO = 'auto',
- CQP = 'cqp',
- ICQ = 'icq',
-}
-
-export enum Colorspace {
- SRGB = 'srgb',
- P3 = 'p3',
-}
-
-export enum ImageFormat {
- JPEG = 'jpeg',
- WEBP = 'webp',
-}
-
-export enum LogLevel {
- VERBOSE = 'verbose',
- DEBUG = 'debug',
- LOG = 'log',
- WARN = 'warn',
- ERROR = 'error',
- FATAL = 'fatal',
-}
-
export interface SystemConfig {
ffmpeg: {
crf: number;
diff --git a/server/src/constants.ts b/server/src/constants.ts
index 6cfcc41d89ba6..e0a4fe8cef306 100644
--- a/server/src/constants.ts
+++ b/server/src/constants.ts
@@ -54,11 +54,6 @@ export const resourcePaths = {
export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
export const LOGIN_URL = '/auth/login?autoLaunch=0';
-export enum AuthType {
- PASSWORD = 'password',
- OAUTH = 'oauth',
-}
-
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
export const FACE_THUMBNAIL_SIZE = 250;
diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts
index fb5ec58f2544c..b2d3933be4cbc 100644
--- a/server/src/controllers/asset-media.controller.ts
+++ b/server/src/controllers/asset-media.controller.ts
@@ -33,16 +33,17 @@ import {
UploadFieldName,
} from 'src/dtos/asset-media.dto';
import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto';
+import { RouteKey } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
-import { FileUploadInterceptor, Route, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
+import { FileUploadInterceptor, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
import { AssetMediaService } from 'src/services/asset-media.service';
import { sendFile } from 'src/utils/file';
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
@ApiTags('Assets')
-@Controller(Route.ASSET)
+@Controller(RouteKey.ASSET)
export class AssetMediaController {
constructor(
@Inject(ILoggerRepository) private logger: ILoggerRepository,
diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts
index 9d3d23065724c..8a5b5fb0b63a8 100644
--- a/server/src/controllers/asset.controller.ts
+++ b/server/src/controllers/asset.controller.ts
@@ -14,13 +14,13 @@ import {
} from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryLaneDto } from 'src/dtos/search.dto';
+import { RouteKey } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
-import { Route } from 'src/middleware/file-upload.interceptor';
import { AssetService } from 'src/services/asset.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Assets')
-@Controller(Route.ASSET)
+@Controller(RouteKey.ASSET)
export class AssetController {
constructor(private service: AssetService) {}
diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts
index 7dcef9df5f391..04250f530044f 100644
--- a/server/src/controllers/auth.controller.ts
+++ b/server/src/controllers/auth.controller.ts
@@ -1,7 +1,6 @@
import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
-import { AuthType } from 'src/constants';
import {
AuthDto,
ChangePasswordDto,
@@ -13,6 +12,7 @@ import {
ValidateAccessTokenResponseDto,
} from 'src/dtos/auth.dto';
import { UserAdminResponseDto } from 'src/dtos/user.dto';
+import { AuthType } from 'src/enum';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
import { AuthService, LoginDetails } from 'src/services/auth.service';
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts
index b733dc612b227..4e626b10f01b4 100644
--- a/server/src/controllers/oauth.controller.ts
+++ b/server/src/controllers/oauth.controller.ts
@@ -1,7 +1,6 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
-import { AuthType } from 'src/constants';
import {
AuthDto,
ImmichCookie,
@@ -11,6 +10,7 @@ import {
OAuthConfigDto,
} from 'src/dtos/auth.dto';
import { UserAdminResponseDto } from 'src/dtos/user.dto';
+import { AuthType } from 'src/enum';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
import { AuthService, LoginDetails } from 'src/services/auth.service';
import { respondWithCookie } from 'src/utils/response';
diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts
index 01b225839080d..10076098d6516 100644
--- a/server/src/controllers/user.controller.ts
+++ b/server/src/controllers/user.controller.ts
@@ -21,15 +21,16 @@ import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
+import { RouteKey } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
-import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
+import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
import { UserService } from 'src/services/user.service';
import { sendFile } from 'src/utils/file';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Users')
-@Controller(Route.USER)
+@Controller(RouteKey.USER)
export class UserController {
constructor(
private service: UserService,
diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts
index e20a0c658db7f..8ce8f6b67a228 100644
--- a/server/src/cores/storage.core.ts
+++ b/server/src/cores/storage.core.ts
@@ -1,12 +1,10 @@
import { randomUUID } from 'node:crypto';
import { dirname, join, resolve } from 'node:path';
-import { ImageFormat } from 'src/config';
import { APP_MEDIA_LOCATION } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetEntity } from 'src/entities/asset.entity';
-import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity';
import { PersonEntity } from 'src/entities/person.entity';
-import { AssetFileType } from 'src/enum';
+import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@@ -16,14 +14,6 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getAssetFiles } from 'src/utils/asset.util';
-export enum StorageFolder {
- ENCODED_VIDEO = 'encoded-video',
- LIBRARY = 'library',
- UPLOAD = 'upload',
- PROFILE = 'profile',
- THUMBNAILS = 'thumbs',
-}
-
export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS));
export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO));
diff --git a/server/src/decorators.ts b/server/src/decorators.ts
index 2316e114e885e..9b6910391af9b 100644
--- a/server/src/decorators.ts
+++ b/server/src/decorators.ts
@@ -4,8 +4,8 @@ import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces';
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
import _ from 'lodash';
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
+import { MetadataKey } from 'src/enum';
import { EmitEvent, ServerEvent } from 'src/interfaces/event.interface';
-import { Metadata } from 'src/middleware/auth.guard';
import { setUnion } from 'src/utils/set';
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
@@ -141,7 +141,7 @@ export type EmitConfig = {
/** lower value has higher priority, defaults to 0 */
priority?: number;
};
-export const OnEmit = (config: EmitConfig) => SetMetadata(Metadata.ON_EMIT_CONFIG, config);
+export const OnEmit = (config: EmitConfig) => SetMetadata(MetadataKey.ON_EMIT_CONFIG, config);
type LifecycleRelease = 'NEXT_RELEASE' | string;
type LifecycleMetadata = {
diff --git a/server/src/dtos/audit.dto.ts b/server/src/dtos/audit.dto.ts
index dcace5a551213..434da46eba976 100644
--- a/server/src/dtos/audit.dto.ts
+++ b/server/src/dtos/audit.dto.ts
@@ -1,8 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
-import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
-import { EntityType } from 'src/enum';
+import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from 'src/enum';
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts
index 336f50f39bc8c..4a3ca37691604 100644
--- a/server/src/dtos/system-config.dto.ts
+++ b/server/src/dtos/system-config.dto.ts
@@ -18,20 +18,20 @@ import {
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
+import { SystemConfig } from 'src/config';
+import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
import {
AudioCodec,
CQMode,
Colorspace,
ImageFormat,
LogLevel,
- SystemConfig,
ToneMapping,
TranscodeHWAccel,
TranscodePolicy,
VideoCodec,
VideoContainer,
-} from 'src/config';
-import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
+} from 'src/enum';
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
import { ValidateBoolean, validateCronExpression } from 'src/validation';
diff --git a/server/src/entities/move.entity.ts b/server/src/entities/move.entity.ts
index f3dad6b280306..5cdef5d22ef76 100644
--- a/server/src/entities/move.entity.ts
+++ b/server/src/entities/move.entity.ts
@@ -1,3 +1,4 @@
+import { PathType } from 'src/enum';
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
@Entity('move_history')
@@ -21,21 +22,3 @@ export class MoveEntity {
@Column({ type: 'varchar' })
newPath!: string;
}
-
-export enum AssetPathType {
- ORIGINAL = 'original',
- PREVIEW = 'preview',
- THUMBNAIL = 'thumbnail',
- ENCODED_VIDEO = 'encoded_video',
- SIDECAR = 'sidecar',
-}
-
-export enum PersonPathType {
- FACE = 'face',
-}
-
-export enum UserPathType {
- PROFILE = 'profile',
-}
-
-export type PathType = AssetPathType | PersonPathType | UserPathType;
diff --git a/server/src/enum.ts b/server/src/enum.ts
index 027b3160a7c32..e0c1e27859de9 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -1,3 +1,8 @@
+export enum AuthType {
+ PASSWORD = 'password',
+ OAUTH = 'oauth',
+}
+
export enum AssetType {
IMAGE = 'IMAGE',
VIDEO = 'VIDEO',
@@ -148,6 +153,14 @@ export enum SharedLinkType {
INDIVIDUAL = 'INDIVIDUAL',
}
+export enum StorageFolder {
+ ENCODED_VIDEO = 'encoded-video',
+ LIBRARY = 'library',
+ UPLOAD = 'upload',
+ PROFILE = 'profile',
+ THUMBNAILS = 'thumbs',
+}
+
export enum SystemMetadataKey {
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
FACIAL_RECOGNITION_STATE = 'facial-recognition-state',
@@ -198,3 +211,120 @@ export enum ManualJobName {
TAG_CLEANUP = 'tag-cleanup',
USER_CLEANUP = 'user-cleanup',
}
+
+export enum AssetPathType {
+ ORIGINAL = 'original',
+ PREVIEW = 'preview',
+ THUMBNAIL = 'thumbnail',
+ ENCODED_VIDEO = 'encoded_video',
+ SIDECAR = 'sidecar',
+}
+
+export enum PersonPathType {
+ FACE = 'face',
+}
+
+export enum UserPathType {
+ PROFILE = 'profile',
+}
+
+export type PathType = AssetPathType | PersonPathType | UserPathType;
+
+export enum TranscodePolicy {
+ ALL = 'all',
+ OPTIMAL = 'optimal',
+ BITRATE = 'bitrate',
+ REQUIRED = 'required',
+ DISABLED = 'disabled',
+}
+
+export enum TranscodeTarget {
+ NONE,
+ AUDIO,
+ VIDEO,
+ ALL,
+}
+
+export enum VideoCodec {
+ H264 = 'h264',
+ HEVC = 'hevc',
+ VP9 = 'vp9',
+ AV1 = 'av1',
+}
+
+export enum AudioCodec {
+ MP3 = 'mp3',
+ AAC = 'aac',
+ LIBOPUS = 'libopus',
+}
+
+export enum VideoContainer {
+ MOV = 'mov',
+ MP4 = 'mp4',
+ OGG = 'ogg',
+ WEBM = 'webm',
+}
+
+export enum TranscodeHWAccel {
+ NVENC = 'nvenc',
+ QSV = 'qsv',
+ VAAPI = 'vaapi',
+ RKMPP = 'rkmpp',
+ DISABLED = 'disabled',
+}
+
+export enum ToneMapping {
+ HABLE = 'hable',
+ MOBIUS = 'mobius',
+ REINHARD = 'reinhard',
+ DISABLED = 'disabled',
+}
+
+export enum CQMode {
+ AUTO = 'auto',
+ CQP = 'cqp',
+ ICQ = 'icq',
+}
+
+export enum Colorspace {
+ SRGB = 'srgb',
+ P3 = 'p3',
+}
+
+export enum ImageFormat {
+ JPEG = 'jpeg',
+ WEBP = 'webp',
+}
+
+export enum LogLevel {
+ VERBOSE = 'verbose',
+ DEBUG = 'debug',
+ LOG = 'log',
+ WARN = 'warn',
+ ERROR = 'error',
+ FATAL = 'fatal',
+}
+
+export enum MetadataKey {
+ AUTH_ROUTE = 'auth_route',
+ ADMIN_ROUTE = 'admin_route',
+ SHARED_ROUTE = 'shared_route',
+ API_KEY_SECURITY = 'api_key',
+ ON_EMIT_CONFIG = 'on_emit_config',
+}
+
+export enum RouteKey {
+ ASSET = 'assets',
+ USER = 'users',
+}
+
+export enum CacheControl {
+ PRIVATE_WITH_CACHE = 'private_with_cache',
+ PRIVATE_WITHOUT_CACHE = 'private_without_cache',
+ NONE = 'none',
+}
+
+export enum PaginationMode {
+ LIMIT_OFFSET = 'limit-offset',
+ SKIP_TAKE = 'skip-take',
+}
diff --git a/server/src/interfaces/logger.interface.ts b/server/src/interfaces/logger.interface.ts
index f0afdce2a521c..ce9a8e64fe27f 100644
--- a/server/src/interfaces/logger.interface.ts
+++ b/server/src/interfaces/logger.interface.ts
@@ -1,4 +1,4 @@
-import { LogLevel } from 'src/config';
+import { LogLevel } from 'src/enum';
export const ILoggerRepository = 'ILoggerRepository';
diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts
index f7389d3d068cd..459e33fc3669b 100644
--- a/server/src/interfaces/media.interface.ts
+++ b/server/src/interfaces/media.interface.ts
@@ -1,5 +1,5 @@
import { Writable } from 'node:stream';
-import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/config';
+import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/enum';
export const IMediaRepository = 'IMediaRepository';
diff --git a/server/src/interfaces/move.interface.ts b/server/src/interfaces/move.interface.ts
index c9d39e78cf497..0e79cfcadc5a8 100644
--- a/server/src/interfaces/move.interface.ts
+++ b/server/src/interfaces/move.interface.ts
@@ -1,4 +1,5 @@
-import { MoveEntity, PathType } from 'src/entities/move.entity';
+import { MoveEntity } from 'src/entities/move.entity';
+import { PathType } from 'src/enum';
export const IMoveRepository = 'IMoveRepository';
diff --git a/server/src/main.ts b/server/src/main.ts
index e32c3e43ac107..48ce179e88753 100644
--- a/server/src/main.ts
+++ b/server/src/main.ts
@@ -2,7 +2,7 @@ import { CommandFactory } from 'nest-commander';
import { fork } from 'node:child_process';
import { Worker } from 'node:worker_threads';
import { ImmichAdminModule } from 'src/app.module';
-import { LogLevel } from 'src/config';
+import { LogLevel } from 'src/enum';
import { getWorkers } from 'src/utils/workers';
const immichApp = process.argv[2] || process.env.IMMICH_APP;
diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts
index d6138f2d3ae24..7bc4f41b21c8f 100644
--- a/server/src/middleware/auth.guard.ts
+++ b/server/src/middleware/auth.guard.ts
@@ -11,19 +11,11 @@ import { Reflector } from '@nestjs/core';
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
import { Request } from 'express';
import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto';
-import { Permission } from 'src/enum';
+import { MetadataKey, Permission } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AuthService, LoginDetails } from 'src/services/auth.service';
import { UAParser } from 'ua-parser-js';
-export enum Metadata {
- AUTH_ROUTE = 'auth_route',
- ADMIN_ROUTE = 'admin_route',
- SHARED_ROUTE = 'shared_route',
- API_KEY_SECURITY = 'api_key',
- ON_EMIT_CONFIG = 'on_emit_config',
-}
-
type AdminRoute = { admin?: true };
type SharedLinkRoute = { sharedLink?: true };
type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute);
@@ -32,8 +24,8 @@ export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator =
const decorators: MethodDecorator[] = [
ApiBearerAuth(),
ApiCookieAuth(),
- ApiSecurity(Metadata.API_KEY_SECURITY),
- SetMetadata(Metadata.AUTH_ROUTE, options || {}),
+ ApiSecurity(MetadataKey.API_KEY_SECURITY),
+ SetMetadata(MetadataKey.AUTH_ROUTE, options || {}),
];
if ((options as SharedLinkRoute)?.sharedLink) {
@@ -85,7 +77,7 @@ export class AuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise {
const targets = [context.getHandler()];
- const options = this.reflector.getAllAndOverride(Metadata.AUTH_ROUTE, targets);
+ const options = this.reflector.getAllAndOverride(MetadataKey.AUTH_ROUTE, targets);
if (!options) {
return true;
}
diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts
index 6ec8b401efb7f..075a7f504636a 100644
--- a/server/src/middleware/file-upload.interceptor.ts
+++ b/server/src/middleware/file-upload.interceptor.ts
@@ -7,6 +7,7 @@ import multer, { StorageEngine, diskStorage } from 'multer';
import { createHash, randomUUID } from 'node:crypto';
import { Observable } from 'rxjs';
import { UploadFieldName } from 'src/dtos/asset-media.dto';
+import { RouteKey } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AuthRequest } from 'src/middleware/auth.guard';
import { AssetMediaService, UploadFile } from 'src/services/asset-media.service';
@@ -28,11 +29,6 @@ export function getFiles(files: UploadFiles) {
};
}
-export enum Route {
- ASSET = 'assets',
- USER = 'users',
-}
-
export interface ImmichFile extends Express.Multer.File {
/** sha1 hash of file */
uuid: string;
@@ -115,7 +111,7 @@ export class FileUploadInterceptor implements NestInterceptor {
const context_ = context.switchToHttp();
const route = this.reflect.get(PATH_METADATA, context.getClass());
- const handler: RequestHandler | null = this.getHandler(route as Route);
+ const handler: RequestHandler | null = this.getHandler(route as RouteKey);
if (handler) {
await new Promise((resolve, reject) => {
const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve());
@@ -176,13 +172,13 @@ export class FileUploadInterceptor implements NestInterceptor {
return false;
}
- private getHandler(route: Route) {
+ private getHandler(route: RouteKey) {
switch (route) {
- case Route.ASSET: {
+ case RouteKey.ASSET: {
return this.handlers.assetUpload;
}
- case Route.USER: {
+ case RouteKey.USER: {
return this.handlers.userProfile;
}
diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts
index 43e765d00b678..0ec347ed77ab7 100644
--- a/server/src/repositories/asset.repository.ts
+++ b/server/src/repositories/asset.repository.ts
@@ -6,7 +6,7 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
-import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
+import { AssetFileType, AssetOrder, AssetStatus, AssetType, PaginationMode } from 'src/enum';
import {
AssetBuilderOptions,
AssetCreate,
@@ -30,7 +30,7 @@ import {
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
import { searchAssetBuilder } from 'src/utils/database';
import { Instrumentation } from 'src/utils/instrumentation';
-import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
+import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
import {
Brackets,
FindOptionsOrder,
diff --git a/server/src/repositories/logger.repository.ts b/server/src/repositories/logger.repository.ts
index 1e0c7b74d973e..1d7e734e735e6 100644
--- a/server/src/repositories/logger.repository.ts
+++ b/server/src/repositories/logger.repository.ts
@@ -1,7 +1,7 @@
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
import { ClsService } from 'nestjs-cls';
-import { LogLevel } from 'src/config';
+import { LogLevel } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { LogColor } from 'src/utils/logger';
diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts
index a84ef6f596f4e..5d1aced5eba65 100644
--- a/server/src/repositories/media.repository.ts
+++ b/server/src/repositories/media.repository.ts
@@ -5,7 +5,7 @@ import fs from 'node:fs/promises';
import { Writable } from 'node:stream';
import { promisify } from 'node:util';
import sharp from 'sharp';
-import { Colorspace } from 'src/config';
+import { Colorspace } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import {
IMediaRepository,
diff --git a/server/src/repositories/move.repository.ts b/server/src/repositories/move.repository.ts
index a8416ff0ac4c6..45fd4465265d7 100644
--- a/server/src/repositories/move.repository.ts
+++ b/server/src/repositories/move.repository.ts
@@ -1,7 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DummyValue, GenerateSql } from 'src/decorators';
-import { MoveEntity, PathType } from 'src/entities/move.entity';
+import { MoveEntity } from 'src/entities/move.entity';
+import { PathType } from 'src/enum';
import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface';
import { Instrumentation } from 'src/utils/instrumentation';
import { Repository } from 'typeorm';
diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts
index 2247195cc3ce7..2607d2a9ec7c4 100644
--- a/server/src/repositories/person.repository.ts
+++ b/server/src/repositories/person.repository.ts
@@ -6,7 +6,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { PersonEntity } from 'src/entities/person.entity';
-import { SourceType } from 'src/enum';
+import { PaginationMode, SourceType } from 'src/enum';
import {
AssetFaceId,
DeleteAllFacesOptions,
@@ -19,7 +19,7 @@ import {
UpdateFacesData,
} from 'src/interfaces/person.interface';
import { Instrumentation } from 'src/utils/instrumentation';
-import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
+import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts
index 8115c72cf6ac1..60694b6bfe800 100644
--- a/server/src/repositories/search.repository.ts
+++ b/server/src/repositories/search.repository.ts
@@ -8,7 +8,7 @@ import { ExifEntity } from 'src/entities/exif.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
-import { AssetType } from 'src/enum';
+import { AssetType, PaginationMode } from 'src/enum';
import { DatabaseExtension } from 'src/interfaces/database.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import {
@@ -23,7 +23,7 @@ import {
} from 'src/interfaces/search.interface';
import { asVector, searchAssetBuilder } from 'src/utils/database';
import { Instrumentation } from 'src/utils/instrumentation';
-import { Paginated, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
+import { Paginated, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
import { isValidInteger } from 'src/validation';
import { Repository, SelectQueryBuilder } from 'typeorm';
diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts
index d7f0c6da0fa37..c03c974b2c8e2 100644
--- a/server/src/services/asset-media.service.spec.ts
+++ b/server/src/services/asset-media.service.spec.ts
@@ -4,7 +4,7 @@ import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos
import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
-import { AssetStatus, AssetType } from 'src/enum';
+import { AssetStatus, AssetType, CacheControl } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
@@ -12,7 +12,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { AssetMediaService } from 'src/services/asset-media.service';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichFileResponse } from 'src/utils/file';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub';
diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts
index d3dce323f0bb7..e1b30e891f936 100644
--- a/server/src/services/asset-media.service.ts
+++ b/server/src/services/asset-media.service.ts
@@ -7,7 +7,7 @@ import {
} from '@nestjs/common';
import { extname } from 'node:path';
import sanitize from 'sanitize-filename';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
+import { StorageCore } from 'src/cores/storage.core';
import {
AssetBulkUploadCheckResponseDto,
AssetMediaResponseDto,
@@ -27,7 +27,7 @@ import {
} from 'src/dtos/asset-media.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
-import { AssetStatus, AssetType, Permission } from 'src/enum';
+import { AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
@@ -37,7 +37,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { requireAccess, requireUploadAccess } from 'src/utils/access';
import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types';
import { fromChecksum } from 'src/utils/request';
import { QueryFailedError } from 'typeorm';
diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts
index 72db2b6eb56ce..ced0f49c63716 100644
--- a/server/src/services/audit.service.ts
+++ b/server/src/services/audit.service.ts
@@ -2,7 +2,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
import { resolve } from 'node:path';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
+import { StorageCore } from 'src/cores/storage.core';
import {
AuditDeletesDto,
AuditDeletesResponseDto,
@@ -12,8 +12,15 @@ import {
PathEntityType,
} from 'src/dtos/audit.dto';
import { AuthDto } from 'src/dtos/auth.dto';
-import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
-import { AssetFileType, DatabaseAction, Permission } from 'src/enum';
+import {
+ AssetFileType,
+ AssetPathType,
+ DatabaseAction,
+ Permission,
+ PersonPathType,
+ StorageFolder,
+ UserPathType,
+} from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IAuditRepository } from 'src/interfaces/audit.interface';
diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts
index acc2d3459ccd1..d22a3b3634ebc 100644
--- a/server/src/services/auth.service.spec.ts
+++ b/server/src/services/auth.service.spec.ts
@@ -1,9 +1,9 @@
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import { Issuer, generators } from 'openid-client';
-import { AuthType } from 'src/constants';
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
+import { AuthType } from 'src/enum';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts
index 6eaf755d0eb49..6b1e4c512f816 100644
--- a/server/src/services/auth.service.ts
+++ b/server/src/services/auth.service.ts
@@ -12,7 +12,7 @@ import { DateTime } from 'luxon';
import { IncomingHttpHeaders } from 'node:http';
import { Issuer, UserinfoResponse, custom, generators } from 'openid-client';
import { SystemConfig } from 'src/config';
-import { AuthType, LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
+import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { UserCore } from 'src/cores/user.core';
import {
@@ -31,7 +31,7 @@ import {
} from 'src/dtos/auth.dto';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity';
-import { Permission } from 'src/enum';
+import { AuthType, Permission } from 'src/enum';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts
index bf493de0f39d1..ce6168408f9a3 100644
--- a/server/src/services/media.service.spec.ts
+++ b/server/src/services/media.service.spec.ts
@@ -1,5 +1,8 @@
import { Stats } from 'node:fs';
+import { ExifEntity } from 'src/entities/exif.entity';
import {
+ AssetFileType,
+ AssetType,
AudioCodec,
Colorspace,
ImageFormat,
@@ -7,9 +10,7 @@ import {
TranscodeHWAccel,
TranscodePolicy,
VideoCodec,
-} from 'src/config';
-import { ExifEntity } from 'src/entities/exif.entity';
-import { AssetFileType, AssetType } from 'src/enum';
+} from 'src/enum';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts
index e74335bdc391c..55a4ee015757b 100644
--- a/server/src/services/media.service.ts
+++ b/server/src/services/media.service.ts
@@ -1,21 +1,23 @@
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
import { dirname } from 'node:path';
+import { GeneratedImageType, StorageCore } from 'src/cores/storage.core';
+import { SystemConfigCore } from 'src/cores/system-config.core';
+import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
+import { AssetEntity } from 'src/entities/asset.entity';
import {
+ AssetFileType,
+ AssetPathType,
+ AssetType,
AudioCodec,
Colorspace,
ImageFormat,
+ StorageFolder,
TranscodeHWAccel,
TranscodePolicy,
TranscodeTarget,
VideoCodec,
VideoContainer,
-} from 'src/config';
-import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
-import { AssetEntity } from 'src/entities/asset.entity';
-import { AssetPathType } from 'src/entities/move.entity';
-import { AssetFileType, AssetType } from 'src/enum';
+} from 'src/enum';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import {
diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts
index 2b111706f1ea6..03da110ac6049 100644
--- a/server/src/services/person.service.spec.ts
+++ b/server/src/services/person.service.spec.ts
@@ -1,9 +1,8 @@
import { BadRequestException, NotFoundException } from '@nestjs/common';
-import { Colorspace } from 'src/config';
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
-import { SourceType, SystemMetadataKey } from 'src/enum';
+import { CacheControl, Colorspace, SourceType, SystemMetadataKey } from 'src/enum';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
@@ -16,7 +15,7 @@ import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.inter
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { PersonService } from 'src/services/person.service';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichFileResponse } from 'src/utils/file';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { faceStub } from 'test/fixtures/face.stub';
diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts
index dd4a4cecf2b56..7cb76d1a71535 100644
--- a/server/src/services/person.service.ts
+++ b/server/src/services/person.service.ts
@@ -1,5 +1,4 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
-import { ImageFormat } from 'src/config';
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
@@ -23,9 +22,16 @@ import {
} from 'src/dtos/person.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity';
-import { PersonPathType } from 'src/entities/move.entity';
import { PersonEntity } from 'src/entities/person.entity';
-import { AssetType, Permission, SourceType, SystemMetadataKey } from 'src/enum';
+import {
+ AssetType,
+ CacheControl,
+ ImageFormat,
+ Permission,
+ PersonPathType,
+ SourceType,
+ SystemMetadataKey,
+} from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
@@ -51,7 +57,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { checkAccess, requireAccess } from 'src/utils/access';
import { getAssetFiles } from 'src/utils/asset.util';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types';
import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';
diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts
index 9db90e41b3c58..a192c2f308ba0 100644
--- a/server/src/services/server.service.ts
+++ b/server/src/services/server.service.ts
@@ -1,7 +1,7 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
import { serverVersion } from 'src/constants';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
+import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEmit } from 'src/decorators';
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
@@ -15,7 +15,7 @@ import {
ServerStorageResponseDto,
UsageByUserDto,
} from 'src/dtos/server.dto';
-import { SystemMetadataKey } from 'src/enum';
+import { StorageFolder, SystemMetadataKey } from 'src/enum';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts
index 093cc5b2ff1d6..e8e222c7b2491 100644
--- a/server/src/services/storage-template.service.spec.ts
+++ b/server/src/services/storage-template.service.spec.ts
@@ -2,7 +2,7 @@ import { Stats } from 'node:fs';
import { SystemConfig, defaults } from 'src/config';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetEntity } from 'src/entities/asset.entity';
-import { AssetPathType } from 'src/entities/move.entity';
+import { AssetPathType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts
index 9836ad40ace47..30d0eb575f1de 100644
--- a/server/src/services/storage-template.service.ts
+++ b/server/src/services/storage-template.service.ts
@@ -13,12 +13,11 @@ import {
supportedWeekTokens,
supportedYearTokens,
} from 'src/constants';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
+import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEmit } from 'src/decorators';
import { AssetEntity } from 'src/entities/asset.entity';
-import { AssetPathType } from 'src/entities/move.entity';
-import { AssetType } from 'src/enum';
+import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts
index 1591149dc20d8..6d15f097d3956 100644
--- a/server/src/services/storage.service.ts
+++ b/server/src/services/storage.service.ts
@@ -1,8 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { join } from 'node:path';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
+import { StorageCore } from 'src/cores/storage.core';
import { OnEmit } from 'src/decorators';
-import { SystemMetadataKey } from 'src/enum';
+import { StorageFolder, SystemMetadataKey } from 'src/enum';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts
index 52ad6d276b94e..8b4fb0bc2fd3c 100644
--- a/server/src/services/system-config.service.spec.ts
+++ b/server/src/services/system-config.service.spec.ts
@@ -1,19 +1,18 @@
import { BadRequestException } from '@nestjs/common';
+import { defaults, SystemConfig } from 'src/config';
import {
AudioCodec,
- CQMode,
Colorspace,
+ CQMode,
ImageFormat,
LogLevel,
- SystemConfig,
+ SystemMetadataKey,
ToneMapping,
TranscodeHWAccel,
TranscodePolicy,
VideoCodec,
VideoContainer,
- defaults,
-} from 'src/config';
-import { SystemMetadataKey } from 'src/enum';
+} from 'src/enum';
import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
import { QueueName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts
index 5ec9ab7a5db05..8a7f9123e00c2 100644
--- a/server/src/services/system-config.service.ts
+++ b/server/src/services/system-config.service.ts
@@ -1,7 +1,7 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { instanceToPlain } from 'class-transformer';
import _ from 'lodash';
-import { LogLevel, SystemConfig, defaults } from 'src/config';
+import { SystemConfig, defaults } from 'src/config';
import {
supportedDayTokens,
supportedHourTokens,
@@ -15,6 +15,7 @@ import {
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEmit, OnServerEvent } from 'src/decorators';
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
+import { LogLevel } from 'src/enum';
import { ArgOf, ClientEvent, IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
diff --git a/server/src/services/user.service.spec.ts b/server/src/services/user.service.spec.ts
index 0ac0ea6dbc7cf..f5b564e86f18b 100644
--- a/server/src/services/user.service.spec.ts
+++ b/server/src/services/user.service.spec.ts
@@ -1,6 +1,6 @@
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { UserEntity } from 'src/entities/user.entity';
-import { UserMetadataKey } from 'src/enum';
+import { CacheControl, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
@@ -9,7 +9,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { UserService } from 'src/services/user.service';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichFileResponse } from 'src/utils/file';
import { authStub } from 'test/fixtures/auth.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userStub } from 'test/fixtures/user.stub';
diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts
index cf918198ab869..dca893aa826b4 100644
--- a/server/src/services/user.service.ts
+++ b/server/src/services/user.service.ts
@@ -2,7 +2,7 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nes
import { DateTime } from 'luxon';
import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
import { SALT_ROUNDS } from 'src/constants';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
+import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AuthDto } from 'src/dtos/auth.dto';
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
@@ -11,7 +11,7 @@ import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
-import { UserMetadataKey } from 'src/enum';
+import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
@@ -19,7 +19,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
-import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichFileResponse } from 'src/utils/file';
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
@Injectable()
diff --git a/server/src/utils/events.ts b/server/src/utils/events.ts
index 064c9f75071ef..f5b079dea4ef3 100644
--- a/server/src/utils/events.ts
+++ b/server/src/utils/events.ts
@@ -1,8 +1,8 @@
import { ModuleRef, Reflector } from '@nestjs/core';
import _ from 'lodash';
import { EmitConfig } from 'src/decorators';
+import { MetadataKey } from 'src/enum';
import { EmitEvent, EmitHandler, IEventRepository } from 'src/interfaces/event.interface';
-import { Metadata } from 'src/middleware/auth.guard';
import { services } from 'src/services';
type Item = {
@@ -35,7 +35,7 @@ export const setupEventHandlers = (moduleRef: ModuleRef) => {
continue;
}
- const options = reflector.get(Metadata.ON_EMIT_CONFIG, handler);
+ const options = reflector.get(MetadataKey.ON_EMIT_CONFIG, handler);
if (!options) {
continue;
}
diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts
index 53a4d571dcfa3..3b26c3e1ba1e8 100644
--- a/server/src/utils/file.ts
+++ b/server/src/utils/file.ts
@@ -3,6 +3,7 @@ import { NextFunction, Response } from 'express';
import { access, constants } from 'node:fs/promises';
import { basename, extname, isAbsolute } from 'node:path';
import { promisify } from 'node:util';
+import { CacheControl } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ImmichReadStream } from 'src/interfaces/storage.interface';
import { isConnectionAborted } from 'src/utils/misc';
@@ -19,12 +20,6 @@ export function getLivePhotoMotionFilename(stillName: string, motionName: string
return getFileNameWithoutExtension(stillName) + extname(motionName);
}
-export enum CacheControl {
- PRIVATE_WITH_CACHE = 'private_with_cache',
- PRIVATE_WITHOUT_CACHE = 'private_without_cache',
- NONE = 'none',
-}
-
export class ImmichFileResponse {
public readonly path!: string;
public readonly contentType!: string;
diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts
index 8068f4a5e6587..d80651eece3a8 100644
--- a/server/src/utils/media.ts
+++ b/server/src/utils/media.ts
@@ -1,5 +1,5 @@
-import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/config';
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
+import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/enum';
import {
AudioStreamInfo,
BitrateDistribution,
diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts
index 47f3f552c47e7..2cef33d4f57c9 100644
--- a/server/src/utils/misc.ts
+++ b/server/src/utils/misc.ts
@@ -13,8 +13,8 @@ import path from 'node:path';
import { SystemConfig } from 'src/config';
import { CLIP_MODEL_INFO, isDev, serverVersion } from 'src/constants';
import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto';
+import { MetadataKey } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { Metadata } from 'src/middleware/auth.guard';
/**
* @returns a list of strings representing the keys of the object in dot notation
@@ -210,7 +210,7 @@ export const useSwagger = (app: INestApplication, force = false) => {
in: 'header',
name: ImmichHeader.API_KEY,
},
- Metadata.API_KEY_SECURITY,
+ MetadataKey.API_KEY_SECURITY,
)
.addServer('/api')
.build();
diff --git a/server/src/utils/pagination.ts b/server/src/utils/pagination.ts
index dec1a9de0c313..4009f219c1c96 100644
--- a/server/src/utils/pagination.ts
+++ b/server/src/utils/pagination.ts
@@ -1,4 +1,5 @@
import _ from 'lodash';
+import { PaginationMode } from 'src/enum';
import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
export interface PaginationOptions {
@@ -6,11 +7,6 @@ export interface PaginationOptions {
skip?: number;
}
-export enum PaginationMode {
- LIMIT_OFFSET = 'limit-offset',
- SKIP_TAKE = 'skip-take',
-}
-
export interface PaginatedBuilderOptions {
take: number;
skip?: number;
From 36ee72cd878af4bd06f63b8e68942912401343d3 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen
Date: Fri, 27 Sep 2024 10:28:56 -0400
Subject: [PATCH 11/21] refactor(server): access env via repository (#12987)
---
server/src/interfaces/config.interface.ts | 14 ++++++
server/src/repositories/config.repository.ts | 15 +++++++
server/src/repositories/index.ts | 3 ++
server/src/services/database.service.spec.ts | 45 ++++++++++++-------
server/src/services/database.service.ts | 9 ++--
.../repositories/config.repository.mock.ts | 14 ++++++
6 files changed, 82 insertions(+), 18 deletions(-)
create mode 100644 server/src/interfaces/config.interface.ts
create mode 100644 server/src/repositories/config.repository.ts
create mode 100644 server/test/repositories/config.repository.mock.ts
diff --git a/server/src/interfaces/config.interface.ts b/server/src/interfaces/config.interface.ts
new file mode 100644
index 0000000000000..11bccbe348b1a
--- /dev/null
+++ b/server/src/interfaces/config.interface.ts
@@ -0,0 +1,14 @@
+import { VectorExtension } from 'src/interfaces/database.interface';
+
+export const IConfigRepository = 'IConfigRepository';
+
+export interface EnvData {
+ database: {
+ skipMigrations: boolean;
+ vectorExtension: VectorExtension;
+ };
+}
+
+export interface IConfigRepository {
+ getEnv(): EnvData;
+}
diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts
new file mode 100644
index 0000000000000..f16fa3bbd4ef9
--- /dev/null
+++ b/server/src/repositories/config.repository.ts
@@ -0,0 +1,15 @@
+import { Injectable } from '@nestjs/common';
+import { getVectorExtension } from 'src/database.config';
+import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
+
+@Injectable()
+export class ConfigRepository implements IConfigRepository {
+ getEnv(): EnvData {
+ return {
+ database: {
+ skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
+ vectorExtension: getVectorExtension(),
+ },
+ };
+ }
+}
diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts
index 7082fc031faa2..fac250d6670d7 100644
--- a/server/src/repositories/index.ts
+++ b/server/src/repositories/index.ts
@@ -5,6 +5,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IAuditRepository } from 'src/interfaces/audit.interface';
+import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
@@ -39,6 +40,7 @@ import { AlbumRepository } from 'src/repositories/album.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { AuditRepository } from 'src/repositories/audit.repository';
+import { ConfigRepository } from 'src/repositories/config.repository';
import { CryptoRepository } from 'src/repositories/crypto.repository';
import { DatabaseRepository } from 'src/repositories/database.repository';
import { EventRepository } from 'src/repositories/event.repository';
@@ -74,6 +76,7 @@ export const repositories = [
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
{ provide: IAssetRepository, useClass: AssetRepository },
{ provide: IAuditRepository, useClass: AuditRepository },
+ { provide: IConfigRepository, useClass: ConfigRepository },
{ provide: ICryptoRepository, useClass: CryptoRepository },
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
{ provide: IEventRepository, useClass: EventRepository },
diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts
index c63428560e03c..fc8130cadc273 100644
--- a/server/src/services/database.service.spec.ts
+++ b/server/src/services/database.service.spec.ts
@@ -1,12 +1,20 @@
-import { DatabaseExtension, EXTENSION_NAMES, IDatabaseRepository } from 'src/interfaces/database.interface';
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import {
+ DatabaseExtension,
+ EXTENSION_NAMES,
+ IDatabaseRepository,
+ VectorExtension,
+} from 'src/interfaces/database.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { DatabaseService } from 'src/services/database.service';
+import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { Mocked } from 'vitest';
describe(DatabaseService.name, () => {
let sut: DatabaseService;
+ let configMock: Mocked;
let databaseMock: Mocked;
let loggerMock: Mocked;
let extensionRange: string;
@@ -16,9 +24,11 @@ describe(DatabaseService.name, () => {
let versionAboveRange: string;
beforeEach(() => {
+ configMock = newConfigRepositoryMock();
databaseMock = newDatabaseRepositoryMock();
loggerMock = newLoggerRepositoryMock();
- sut = new DatabaseService(databaseMock, loggerMock);
+
+ sut = new DatabaseService(configMock, databaseMock, loggerMock);
extensionRange = '0.2.x';
databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange);
@@ -33,11 +43,6 @@ describe(DatabaseService.name, () => {
});
});
- afterEach(() => {
- delete process.env.DB_SKIP_MIGRATIONS;
- delete process.env.DB_VECTOR_EXTENSION;
- });
-
it('should work', () => {
expect(sut).toBeDefined();
});
@@ -50,12 +55,12 @@ describe(DatabaseService.name, () => {
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
});
- describe.each([
+ describe.each(>[
{ extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] },
{ extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
])('should work with $extensionName', ({ extension, extensionName }) => {
beforeEach(() => {
- process.env.DB_VECTOR_EXTENSION = extensionName;
+ configMock.getEnv.mockReturnValue({ database: { skipMigrations: false, vectorExtension: extension } });
});
it(`should start up successfully with ${extension}`, async () => {
@@ -236,18 +241,28 @@ describe(DatabaseService.name, () => {
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
expect(loggerMock.fatal).not.toHaveBeenCalled();
});
+ });
- it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
- process.env.DB_SKIP_MIGRATIONS = 'true';
+ it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
+ configMock.getEnv.mockReturnValue({
+ database: {
+ skipMigrations: true,
+ vectorExtension: DatabaseExtension.VECTORS,
+ },
+ });
- await expect(sut.onBootstrap()).resolves.toBeUndefined();
+ await expect(sut.onBootstrap()).resolves.toBeUndefined();
- expect(databaseMock.runMigrations).not.toHaveBeenCalled();
- });
+ expect(databaseMock.runMigrations).not.toHaveBeenCalled();
});
it(`should throw error if pgvector extension could not be created`, async () => {
- process.env.DB_VECTOR_EXTENSION = 'pgvector';
+ configMock.getEnv.mockReturnValue({
+ database: {
+ skipMigrations: true,
+ vectorExtension: DatabaseExtension.VECTOR,
+ },
+ });
databaseMock.getExtensionVersion.mockResolvedValue({
installedVersion: null,
availableVersion: minVersionInRange,
diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts
index a5280ff28be23..ee6176115b311 100644
--- a/server/src/services/database.service.ts
+++ b/server/src/services/database.service.ts
@@ -1,8 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { Duration } from 'luxon';
import semver from 'semver';
-import { getVectorExtension } from 'src/database.config';
import { OnEmit } from 'src/decorators';
+import { IConfigRepository } from 'src/interfaces/config.interface';
import {
DatabaseExtension,
DatabaseLock,
@@ -67,6 +67,7 @@ export class DatabaseService {
private reconnection?: NodeJS.Timeout;
constructor(
+ @Inject(IConfigRepository) private configRepository: IConfigRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
) {
@@ -85,7 +86,8 @@ export class DatabaseService {
}
await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => {
- const extension = getVectorExtension();
+ const envData = this.configRepository.getEnv();
+ const extension = envData.database.vectorExtension;
const name = EXTENSION_NAMES[extension];
const extensionRange = this.databaseRepository.getExtensionVersionRange(extension);
@@ -116,7 +118,8 @@ export class DatabaseService {
await this.checkReindexing();
- if (process.env.DB_SKIP_MIGRATIONS !== 'true') {
+ const { database } = this.configRepository.getEnv();
+ if (!database.skipMigrations) {
await this.databaseRepository.runMigrations();
}
});
diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts
new file mode 100644
index 0000000000000..40110186f43f3
--- /dev/null
+++ b/server/test/repositories/config.repository.mock.ts
@@ -0,0 +1,14 @@
+import { IConfigRepository } from 'src/interfaces/config.interface';
+import { DatabaseExtension } from 'src/interfaces/database.interface';
+import { Mocked, vitest } from 'vitest';
+
+export const newConfigRepositoryMock = (): Mocked => {
+ return {
+ getEnv: vitest.fn().mockReturnValue({
+ database: {
+ skipMigration: false,
+ vectorExtension: DatabaseExtension.VECTORS,
+ },
+ }),
+ };
+};
From 3a37fc8bfde00472e068b1fb1db48f973526d15a Mon Sep 17 00:00:00 2001
From: martin <74269598+martabal@users.noreply.github.com>
Date: Fri, 27 Sep 2024 17:05:07 +0200
Subject: [PATCH 12/21] feat: no slideshow transition (#12989)
---
.../lib/components/asset-viewer/asset-viewer.svelte | 13 +++++++------
web/src/lib/components/slideshow-settings.svelte | 3 ++-
web/src/lib/i18n/en.json | 1 +
web/src/lib/stores/slideshow.store.ts | 2 ++
4 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte
index 850a7c159f43c..451915d13aca7 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte
@@ -67,6 +67,7 @@
stopProgress: stopSlideshowProgress,
slideshowNavigation,
slideshowState,
+ slideshowTransition,
} = slideshowStore;
let appearsInAlbums: AlbumResponseDto[] = [];
@@ -82,13 +83,14 @@
let numberOfComments: number;
let fullscreenElement: Element;
let unsubscribes: (() => void)[] = [];
+ let selectedEditType: string = '';
+ let stack: StackResponseDto | null = null;
+
let zoomToggle = () => void 0;
let copyImage: () => Promise;
$: isFullScreen = fullscreenElement !== null;
- let stack: StackResponseDto | null = null;
-
const refreshStack = async () => {
if (isSharedLink()) {
return;
@@ -390,11 +392,9 @@
onAction?.(action);
};
- let selectedEditType: string = '';
-
- function handleUpdateSelectedEditType(type: string) {
+ const handleUpdateSelectedEditType = (type: string) => {
selectedEditType = type;
- }
+ };
@@ -508,6 +508,7 @@
onNextAsset={() => navigateAsset('next')}
on:close={closeViewer}
{sharedLink}
+ haveFadeTransition={$slideshowState === SlideshowState.None || $slideshowTransition}
/>
{/if}
{:else}
diff --git a/web/src/lib/components/slideshow-settings.svelte b/web/src/lib/components/slideshow-settings.svelte
index e2bf6a4b2c22d..6f0397be98f19 100644
--- a/web/src/lib/components/slideshow-settings.svelte
+++ b/web/src/lib/components/slideshow-settings.svelte
@@ -18,7 +18,7 @@
import SettingDropdown from './shared-components/settings/setting-dropdown.svelte';
import { t } from 'svelte-i18n';
- const { slideshowDelay, showProgressBar, slideshowNavigation, slideshowLook } = slideshowStore;
+ const { slideshowDelay, showProgressBar, slideshowNavigation, slideshowLook, slideshowTransition } = slideshowStore;
export let onClose = () => {};
@@ -65,6 +65,7 @@
}}
/>
+
('slideshow-show-progressbar', true);
const slideshowDelay = persisted('slideshow-delay', 5, {});
+ const slideshowTransition = persisted('slideshow-transition', true);
return {
restartProgress: {
@@ -67,6 +68,7 @@ function createSlideshowStore() {
slideshowState,
slideshowDelay,
showProgressBar,
+ slideshowTransition,
};
}
From 03aa34602040ff075cb144b97c19473442c3f0cb Mon Sep 17 00:00:00 2001
From: Alex
Date: Fri, 27 Sep 2024 22:28:31 +0700
Subject: [PATCH 13/21] fix(mobile): incorrect filename is retrieved during
upload (#12990)
* fix(mobile): incorrect filename is retrieve during upload
* use the same convention to get local id
* revert previous change
* pr feedback
---
mobile/lib/interfaces/asset_media.interface.dart | 3 +++
mobile/lib/repositories/asset_media.repository.dart | 13 +++++++++++++
mobile/lib/services/background.service.dart | 3 +++
mobile/lib/services/backup.service.dart | 9 ++++++++-
4 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/mobile/lib/interfaces/asset_media.interface.dart b/mobile/lib/interfaces/asset_media.interface.dart
index f89a238dd47ee..2606d5c23c518 100644
--- a/mobile/lib/interfaces/asset_media.interface.dart
+++ b/mobile/lib/interfaces/asset_media.interface.dart
@@ -4,4 +4,7 @@ abstract interface class IAssetMediaRepository {
Future> deleteAll(List ids);
Future get(String id);
+
+ /// Obtaining the correct original filename of the asset
+ Future getOriginalFilename(String id);
}
diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart
index 20cf680339e53..68fffa08a6fcb 100644
--- a/mobile/lib/repositories/asset_media.repository.dart
+++ b/mobile/lib/repositories/asset_media.repository.dart
@@ -43,4 +43,17 @@ class AssetMediaRepository implements IAssetMediaRepository {
asset.local = local;
return asset;
}
+
+ @override
+ Future getOriginalFilename(String id) async {
+ final entity = await AssetEntity.fromId(id);
+
+ if (entity == null) {
+ return null;
+ }
+
+ // titleAsync gets the correct original filename for some assets on iOS
+ // otherwise using the `entity.title` would return a random GUID
+ return await entity.titleAsync;
+ }
}
diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart
index d06bc86d4871b..86dfd0c5998c5 100644
--- a/mobile/lib/services/background.service.dart
+++ b/mobile/lib/services/background.service.dart
@@ -15,6 +15,7 @@ import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
@@ -368,6 +369,7 @@ class BackgroundService {
BackupRepository backupAlbumRepository = BackupRepository(db);
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
FileMediaRepository fileMediaRepository = FileMediaRepository();
+ AssetMediaRepository assetMediaRepository = AssetMediaRepository();
UserRepository userRepository = UserRepository(db);
UserApiRepository userApiRepository =
UserApiRepository(apiService.usersApi);
@@ -409,6 +411,7 @@ class BackgroundService {
albumService,
albumMediaRepository,
fileMediaRepository,
+ assetMediaRepository,
);
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart
index 19d731d773d75..683339f271ed2 100644
--- a/mobile/lib/services/backup.service.dart
+++ b/mobile/lib/services/backup.service.dart
@@ -12,6 +12,7 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/interfaces/asset_media.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@@ -21,6 +22,7 @@ import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart';
@@ -40,6 +42,7 @@ final backupServiceProvider = Provider(
ref.watch(albumServiceProvider),
ref.watch(albumMediaRepositoryProvider),
ref.watch(fileMediaRepositoryProvider),
+ ref.watch(assetMediaRepositoryProvider),
),
);
@@ -52,6 +55,7 @@ class BackupService {
final AlbumService _albumService;
final IAlbumMediaRepository _albumMediaRepository;
final IFileMediaRepository _fileMediaRepository;
+ final IAssetMediaRepository _assetMediaRepository;
BackupService(
this._apiService,
@@ -60,6 +64,7 @@ class BackupService {
this._albumService,
this._albumMediaRepository,
this._fileMediaRepository,
+ this._assetMediaRepository,
);
Future?> getDeviceBackupAsset() async {
@@ -329,7 +334,9 @@ class BackupService {
}
if (file != null) {
- String originalFileName = asset.fileName;
+ String? originalFileName =
+ await _assetMediaRepository.getOriginalFilename(asset.localId!);
+ originalFileName ??= asset.fileName;
if (asset.local!.isLivePhoto) {
if (livePhotoFile == null) {
From 7c15e11efccc1f6f4d7c1da12e932ae5fc058838 Mon Sep 17 00:00:00 2001
From: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 27 Sep 2024 15:32:16 +0000
Subject: [PATCH 14/21] chore: version v1.116.1
---
cli/package-lock.json | 6 +++---
cli/package.json | 2 +-
docs/static/archived-versions.json | 4 ++++
e2e/package-lock.json | 8 ++++----
e2e/package.json | 2 +-
machine-learning/pyproject.toml | 2 +-
mobile/android/fastlane/Fastfile | 4 ++--
mobile/ios/fastlane/Fastfile | 2 +-
mobile/openapi/README.md | 2 +-
mobile/pubspec.yaml | 2 +-
open-api/immich-openapi-specs.json | 2 +-
open-api/typescript-sdk/package-lock.json | 4 ++--
open-api/typescript-sdk/package.json | 2 +-
open-api/typescript-sdk/src/fetch-client.ts | 2 +-
server/package-lock.json | 4 ++--
server/package.json | 2 +-
web/package-lock.json | 6 +++---
web/package.json | 2 +-
18 files changed, 31 insertions(+), 27 deletions(-)
diff --git a/cli/package-lock.json b/cli/package-lock.json
index c66d663576e6c..73f0e405ba2b2 100644
--- a/cli/package-lock.json
+++ b/cli/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
- "version": "2.2.20",
+ "version": "2.2.21",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
- "version": "2.2.20",
+ "version": "2.2.21",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
@@ -52,7 +52,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
- "version": "1.116.0",
+ "version": "1.116.1",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
diff --git a/cli/package.json b/cli/package.json
index ba2f8468226d4..f28bbe130f39c 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
- "version": "2.2.20",
+ "version": "2.2.21",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json
index 992aaa6d4b5d2..9fc474c729490 100644
--- a/docs/static/archived-versions.json
+++ b/docs/static/archived-versions.json
@@ -1,4 +1,8 @@
[
+ {
+ "label": "v1.116.1",
+ "url": "https://v1.116.1.archive.immich.app"
+ },
{
"label": "v1.116.0",
"url": "https://v1.116.0.archive.immich.app"
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
index 63ad7be469cf7..b451e5dacf434 100644
--- a/e2e/package-lock.json
+++ b/e2e/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
- "version": "1.116.0",
+ "version": "1.116.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
- "version": "1.116.0",
+ "version": "1.116.1",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -45,7 +45,7 @@
},
"../cli": {
"name": "@immich/cli",
- "version": "2.2.20",
+ "version": "2.2.21",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -92,7 +92,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
- "version": "1.116.0",
+ "version": "1.116.1",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
diff --git a/e2e/package.json b/e2e/package.json
index 80bf261a03512..38d671d9d5674 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
- "version": "1.116.0",
+ "version": "1.116.1",
"description": "",
"main": "index.js",
"type": "module",
diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml
index 8d1539a79b07d..1f953b882745f 100644
--- a/machine-learning/pyproject.toml
+++ b/machine-learning/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
-version = "1.116.0"
+version = "1.116.1"
description = ""
authors = ["Hau Tran "]
readme = "README.md"
diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile
index 6a6454bfe986b..43d643d2f6abd 100644
--- a/mobile/android/fastlane/Fastfile
+++ b/mobile/android/fastlane/Fastfile
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
- "android.injected.version.code" => 160,
- "android.injected.version.name" => "1.116.0",
+ "android.injected.version.code" => 161,
+ "android.injected.version.name" => "1.116.1",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile
index 1cc5524c40093..a9382cb9690bc 100644
--- a/mobile/ios/fastlane/Fastfile
+++ b/mobile/ios/fastlane/Fastfile
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release"
lane :release do
increment_version_number(
- version_number: "1.116.0"
+ version_number: "1.116.1"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 9f2261e03d8e6..e5280e31394e1 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
-- API version: 1.116.0
+- API version: 1.116.1
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index a219b6ddb1575..ac8294a0a6580 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
-version: 1.116.0+160
+version: 1.116.1+161
environment:
sdk: '>=3.3.0 <4.0.0'
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index bb0aa83009863..b2682dd95a0eb 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -7409,7 +7409,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
- "version": "1.116.0",
+ "version": "1.116.1",
"contact": {}
},
"tags": [],
diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json
index 3ab9ac0583a35..95bbddc50709f 100644
--- a/open-api/typescript-sdk/package-lock.json
+++ b/open-api/typescript-sdk/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
- "version": "1.116.0",
+ "version": "1.116.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
- "version": "1.116.0",
+ "version": "1.116.1",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json
index 45a1fada32eb1..3226f63b19303 100644
--- a/open-api/typescript-sdk/package.json
+++ b/open-api/typescript-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
- "version": "1.116.0",
+ "version": "1.116.1",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 63597d49bc6c1..bf2721f848dbc 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -1,6 +1,6 @@
/**
* Immich
- * 1.116.0
+ * 1.116.1
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
diff --git a/server/package-lock.json b/server/package-lock.json
index 57c8dd7146732..53c34aeb32d06 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "immich",
- "version": "1.116.0",
+ "version": "1.116.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
- "version": "1.116.0",
+ "version": "1.116.1",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@nestjs/bullmq": "^10.0.1",
diff --git a/server/package.json b/server/package.json
index 8ba20f6b3bc23..3817bd5d01168 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "immich",
- "version": "1.116.0",
+ "version": "1.116.1",
"description": "",
"author": "",
"private": true,
diff --git a/web/package-lock.json b/web/package-lock.json
index 172c315570d43..6a6baca4c277b 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "immich-web",
- "version": "1.116.0",
+ "version": "1.116.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
- "version": "1.116.0",
+ "version": "1.116.1",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.7.8",
@@ -74,7 +74,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
- "version": "1.116.0",
+ "version": "1.116.1",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
diff --git a/web/package.json b/web/package.json
index 938b4dc9cf008..9b8d356840c5c 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
{
"name": "immich-web",
- "version": "1.116.0",
+ "version": "1.116.1",
"license": "GNU Affero General Public License version 3",
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",
From dbe542803f6e05b3cc878797677c18bc9739cae6 Mon Sep 17 00:00:00 2001
From: bo0tzz
Date: Fri, 27 Sep 2024 19:07:00 +0200
Subject: [PATCH 15/21] docs: update FAQ CLIP search explanation (#12986)
---
docs/docs/FAQ.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx
index 3144b1b9a8456..b328d3a047099 100644
--- a/docs/docs/FAQ.mdx
+++ b/docs/docs/FAQ.mdx
@@ -187,7 +187,7 @@ However, when the trash is emptied, the files will re-appear in the main timelin
### How does smart search work?
-Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
+Immich uses CLIP models. An ML model converts each image to an "embedding", which is essentially a string of numbers that semantically encodes what is in the image. The same is done for the text that you enter when you do a search, and that text embedding is then compared with those of the images to find similar ones. As such, there are no "tags", "labels", or "descriptions" generated that you can look at. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
### How does facial recognition work?
From 789937d4a2409601c35120dff9e454fd735c6a76 Mon Sep 17 00:00:00 2001
From: Zack Pollard
Date: Fri, 27 Sep 2024 18:15:44 +0100
Subject: [PATCH 16/21] fix: library pagination to 10k to avoid too many
postgres query params (#12993)
---
server/src/interfaces/job.interface.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts
index 8b6e2c289bd28..af2726b858aee 100644
--- a/server/src/interfaces/job.interface.ts
+++ b/server/src/interfaces/job.interface.ts
@@ -116,7 +116,7 @@ export enum JobName {
}
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
-export const JOBS_LIBRARY_PAGINATION_SIZE = 100_000;
+export const JOBS_LIBRARY_PAGINATION_SIZE = 10_000;
export interface IBaseJob {
force?: boolean;
From 4ed1517e6032839b0bbc062a93b19fbb34e4758e Mon Sep 17 00:00:00 2001
From: Alex
Date: Sat, 28 Sep 2024 01:13:24 +0700
Subject: [PATCH 17/21] chore(mobile): post release task (#12991)
---
mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++---
mobile/ios/Runner/Info.plist | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj
index 241cb8ecd99df..70bddbf10b997 100644
--- a/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ b/mobile/ios/Runner.xcodeproj/project.pbxproj
@@ -401,7 +401,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 176;
+ CURRENT_PROJECT_VERSION = 177;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 176;
+ CURRENT_PROJECT_VERSION = 177;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -571,7 +571,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 176;
+ CURRENT_PROJECT_VERSION = 177;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index 14fc27b56dd61..b684804037010 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -58,11 +58,11 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.116.0
+ 1.116.1
CFBundleSignature
????
CFBundleVersion
- 176
+ 177
FLTEnableImpeller
ITSAppUsesNonExemptEncryption
From 8bbcd5c31e4a227f92864ae2977c4033bc0c50b7 Mon Sep 17 00:00:00 2001
From: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 27 Sep 2024 18:17:49 +0000
Subject: [PATCH 18/21] chore: version v1.116.2
---
cli/package-lock.json | 6 +++---
cli/package.json | 2 +-
docs/static/archived-versions.json | 4 ++++
e2e/package-lock.json | 8 ++++----
e2e/package.json | 2 +-
machine-learning/pyproject.toml | 2 +-
mobile/android/fastlane/Fastfile | 2 +-
mobile/ios/fastlane/Fastfile | 2 +-
mobile/openapi/README.md | 2 +-
mobile/pubspec.yaml | 2 +-
open-api/immich-openapi-specs.json | 2 +-
open-api/typescript-sdk/package-lock.json | 4 ++--
open-api/typescript-sdk/package.json | 2 +-
open-api/typescript-sdk/src/fetch-client.ts | 2 +-
server/package-lock.json | 4 ++--
server/package.json | 2 +-
web/package-lock.json | 6 +++---
web/package.json | 2 +-
18 files changed, 30 insertions(+), 26 deletions(-)
diff --git a/cli/package-lock.json b/cli/package-lock.json
index 73f0e405ba2b2..e508fe843f60d 100644
--- a/cli/package-lock.json
+++ b/cli/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
- "version": "2.2.21",
+ "version": "2.2.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
- "version": "2.2.21",
+ "version": "2.2.22",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
@@ -52,7 +52,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
- "version": "1.116.1",
+ "version": "1.116.2",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
diff --git a/cli/package.json b/cli/package.json
index f28bbe130f39c..522a8e593e9e7 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
- "version": "2.2.21",
+ "version": "2.2.22",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json
index 9fc474c729490..36a8fed81df1e 100644
--- a/docs/static/archived-versions.json
+++ b/docs/static/archived-versions.json
@@ -1,4 +1,8 @@
[
+ {
+ "label": "v1.116.2",
+ "url": "https://v1.116.2.archive.immich.app"
+ },
{
"label": "v1.116.1",
"url": "https://v1.116.1.archive.immich.app"
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
index b451e5dacf434..e7b463b0b2696 100644
--- a/e2e/package-lock.json
+++ b/e2e/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
- "version": "1.116.1",
+ "version": "1.116.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
- "version": "1.116.1",
+ "version": "1.116.2",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -45,7 +45,7 @@
},
"../cli": {
"name": "@immich/cli",
- "version": "2.2.21",
+ "version": "2.2.22",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -92,7 +92,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
- "version": "1.116.1",
+ "version": "1.116.2",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
diff --git a/e2e/package.json b/e2e/package.json
index 38d671d9d5674..7c0025902dd3a 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
- "version": "1.116.1",
+ "version": "1.116.2",
"description": "",
"main": "index.js",
"type": "module",
diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml
index 1f953b882745f..840aa93c06453 100644
--- a/machine-learning/pyproject.toml
+++ b/machine-learning/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
-version = "1.116.1"
+version = "1.116.2"
description = ""
authors = ["Hau Tran "]
readme = "README.md"
diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile
index 43d643d2f6abd..d1f09a011f4fe 100644
--- a/mobile/android/fastlane/Fastfile
+++ b/mobile/android/fastlane/Fastfile
@@ -36,7 +36,7 @@ platform :android do
build_type: 'Release',
properties: {
"android.injected.version.code" => 161,
- "android.injected.version.name" => "1.116.1",
+ "android.injected.version.name" => "1.116.2",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile
index a9382cb9690bc..8dc3676fb787a 100644
--- a/mobile/ios/fastlane/Fastfile
+++ b/mobile/ios/fastlane/Fastfile
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release"
lane :release do
increment_version_number(
- version_number: "1.116.1"
+ version_number: "1.116.2"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index e5280e31394e1..fecbbf482be54 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
-- API version: 1.116.1
+- API version: 1.116.2
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index ac8294a0a6580..dc1eb11ca7f24 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
-version: 1.116.1+161
+version: 1.116.2+161
environment:
sdk: '>=3.3.0 <4.0.0'
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index b2682dd95a0eb..6afd0d792ff34 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -7409,7 +7409,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
- "version": "1.116.1",
+ "version": "1.116.2",
"contact": {}
},
"tags": [],
diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json
index 95bbddc50709f..72d7a3ec546d8 100644
--- a/open-api/typescript-sdk/package-lock.json
+++ b/open-api/typescript-sdk/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
- "version": "1.116.1",
+ "version": "1.116.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
- "version": "1.116.1",
+ "version": "1.116.2",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json
index 3226f63b19303..41bc3a3b16017 100644
--- a/open-api/typescript-sdk/package.json
+++ b/open-api/typescript-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
- "version": "1.116.1",
+ "version": "1.116.2",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index bf2721f848dbc..b1ae5d28764f2 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -1,6 +1,6 @@
/**
* Immich
- * 1.116.1
+ * 1.116.2
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
diff --git a/server/package-lock.json b/server/package-lock.json
index 53c34aeb32d06..646a26b1ee9d0 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "immich",
- "version": "1.116.1",
+ "version": "1.116.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
- "version": "1.116.1",
+ "version": "1.116.2",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@nestjs/bullmq": "^10.0.1",
diff --git a/server/package.json b/server/package.json
index 3817bd5d01168..d4816109069e9 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "immich",
- "version": "1.116.1",
+ "version": "1.116.2",
"description": "",
"author": "",
"private": true,
diff --git a/web/package-lock.json b/web/package-lock.json
index 6a6baca4c277b..a32e96e67f78b 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "immich-web",
- "version": "1.116.1",
+ "version": "1.116.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
- "version": "1.116.1",
+ "version": "1.116.2",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.7.8",
@@ -74,7 +74,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
- "version": "1.116.1",
+ "version": "1.116.2",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
diff --git a/web/package.json b/web/package.json
index 9b8d356840c5c..20553759fad43 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
{
"name": "immich-web",
- "version": "1.116.1",
+ "version": "1.116.2",
"license": "GNU Affero General Public License version 3",
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",
From 7579bc43591dd72bb84b8426786f7834e76e2844 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 27 Sep 2024 22:07:59 +0000
Subject: [PATCH 19/21] fix(deps): update machine-learning (#12883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
machine-learning/Dockerfile | 4 +-
machine-learning/export/Dockerfile | 2 +-
machine-learning/poetry.lock | 67 ++++++++++++++----------------
3 files changed, 35 insertions(+), 38 deletions(-)
diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile
index e394091ae13f8..d982962fbcdc6 100644
--- a/machine-learning/Dockerfile
+++ b/machine-learning/Dockerfile
@@ -1,6 +1,6 @@
ARG DEVICE=cpu
-FROM python:3.11-bookworm@sha256:157a371e60389919fe4a72dff71ce86eaa5234f59114c23b0b346d0d02c74d39 AS builder-cpu
+FROM python:3.11-bookworm@sha256:e456ff58048f52f121025159e68bf16248c4122c8b96fadffd89331df50c9994 AS builder-cpu
FROM builder-cpu AS builder-openvino
@@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
-FROM python:3.11-slim-bookworm@sha256:669bbd08353610485a94d5d0c976b4b6498c55280fe42c00f7581f85ee9f3121 AS prod-cpu
+FROM python:3.11-slim-bookworm@sha256:585cf0799407efc267fe1cce318322ec26e015ac1b3d77f2517d50bc3acfc232 AS prod-cpu
FROM prod-cpu AS prod-openvino
diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile
index 0754f882f3ae0..195e64ab35ad6 100644
--- a/machine-learning/export/Dockerfile
+++ b/machine-learning/export/Dockerfile
@@ -1,4 +1,4 @@
-FROM mambaorg/micromamba:bookworm-slim@sha256:5f32c5742e2248f2ca07ccae6861371321aba37372bf8e1a80d6f728f1ab4418 AS builder
+FROM mambaorg/micromamba:bookworm-slim@sha256:e3797091302382ea841498bc93a7b0a50f7c1448333d5e946d2d1608d0c5f43d AS builder
ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \
diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock
index 84c9ae5d31151..5bb1726378050 100644
--- a/machine-learning/poetry.lock
+++ b/machine-learning/poetry.lock
@@ -680,13 +680,13 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi-slim"
-version = "0.114.2"
+version = "0.115.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
- {file = "fastapi_slim-0.114.2-py3-none-any.whl", hash = "sha256:52ae76c53a30ad0fa96beb84c1bf4bef9c72e88c2f7c0473e836f01d7ac3ca6b"},
- {file = "fastapi_slim-0.114.2.tar.gz", hash = "sha256:76d0a450826fb0fa740268be55ef04c44807da87a94fbbf5f16338b5a4a2d321"},
+ {file = "fastapi_slim-0.115.0-py3-none-any.whl", hash = "sha256:27ab44da95b622e68be7a19f06df1960a320b9d94e689b0adfc055bb26ee9be7"},
+ {file = "fastapi_slim-0.115.0.tar.gz", hash = "sha256:b4b962ca2aa0a31010dafdad3d4da99d368a5591223304c6fb385712fad7feb6"},
]
[package.dependencies]
@@ -2037,22 +2037,22 @@ reference = "cuda12"
[[package]]
name = "onnxruntime-openvino"
-version = "1.18.0"
+version = "1.19.0"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false
python-versions = "*"
files = [
- {file = "onnxruntime_openvino-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:565b874d21bcd48126da7d62f57db019f5ec0e1f82ae9b0740afa2ad91f8d331"},
- {file = "onnxruntime_openvino-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:7f1931060f710a6c8e32121bb73044c4772ef5925802fc8776d3fe1e87ab3f75"},
- {file = "onnxruntime_openvino-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb1723d386f70a8e26398d983ebe35d2c25ba56e9cdb382670ebbf1f5139f8ba"},
- {file = "onnxruntime_openvino-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:874a1e263dd86674593e5a879257650b06a8609c4d5768c3d8ed8dc4ae874b9c"},
- {file = "onnxruntime_openvino-1.18.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:597eb18f3de7ead69b08a242d74c4573b28bbfba40ca2a1a40f75bf7a834808e"},
+ {file = "onnxruntime_openvino-1.19.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8c5658da819b26d9f35f95204e1bdfb74a100a7533e74edab3af6316c1e316e8"},
+ {file = "onnxruntime_openvino-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb8de2a60cf78db6e201b0a489479995d166938e9c53b01ff342dc7f5f8251ff"},
+ {file = "onnxruntime_openvino-1.19.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f3a0b954026286421b3a769c746c403e8f141f3887d1dd601beb7c4dbf77488a"},
+ {file = "onnxruntime_openvino-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:12330922ecdb694ea28dbdcf08c172e47a5a84fee603040691341336ee3e42bc"},
+ {file = "onnxruntime_openvino-1.19.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:be00502b1a46ba1891cbe49049033745f71c0b99df6d24b979f5b4084b9567d0"},
]
[package.dependencies]
coloredlogs = "*"
flatbuffers = "*"
-numpy = ">=1.26.4"
+numpy = ">=1.21.6"
packaging = "*"
protobuf = "*"
sympy = "*"
@@ -2576,18 +2576,15 @@ cli = ["click (>=5.0)"]
[[package]]
name = "python-multipart"
-version = "0.0.9"
+version = "0.0.10"
description = "A streaming multipart parser for Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"},
- {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"},
+ {file = "python_multipart-0.0.10-py3-none-any.whl", hash = "sha256:2b06ad9e8d50c7a8db80e3b56dab590137b323410605af2be20d62a5f1ba1dc8"},
+ {file = "python_multipart-0.0.10.tar.gz", hash = "sha256:46eb3c6ce6fdda5fb1a03c7e11d490e407c6930a2703fe7aef4da71c374688fa"},
]
-[package.extras]
-dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"]
-
[[package]]
name = "pywin32"
version = "306"
@@ -2834,29 +2831,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
-version = "0.6.6"
+version = "0.6.8"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
- {file = "ruff-0.6.6-py3-none-linux_armv6l.whl", hash = "sha256:f5bc5398457484fc0374425b43b030e4668ed4d2da8ee7fdda0e926c9f11ccfb"},
- {file = "ruff-0.6.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:515a698254c9c47bb84335281a170213b3ee5eb47feebe903e1be10087a167ce"},
- {file = "ruff-0.6.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6bb1b4995775f1837ab70f26698dd73852bbb82e8f70b175d2713c0354fe9182"},
- {file = "ruff-0.6.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c546f412dfae8bb9cc4f27f0e45cdd554e42fecbb34f03312b93368e1cd0a6"},
- {file = "ruff-0.6.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59627e97364329e4eae7d86fa7980c10e2b129e2293d25c478ebcb861b3e3fd6"},
- {file = "ruff-0.6.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94c3f78c3d32190aafbb6bc5410c96cfed0a88aadb49c3f852bbc2aa9783a7d8"},
- {file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:704da526c1e137f38c8a067a4a975fe6834b9f8ba7dbc5fd7503d58148851b8f"},
- {file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efeede5815a24104579a0f6320660536c5ffc1c91ae94f8c65659af915fb9de9"},
- {file = "ruff-0.6.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e368aef0cc02ca3593eae2fb8186b81c9c2b3f39acaaa1108eb6b4d04617e61f"},
- {file = "ruff-0.6.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2653fc3b2a9315bd809725c88dd2446550099728d077a04191febb5ea79a4f79"},
- {file = "ruff-0.6.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bb858cd9ce2d062503337c5b9784d7b583bcf9d1a43c4df6ccb5eab774fbafcb"},
- {file = "ruff-0.6.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:488f8e15c01ea9afb8c0ba35d55bd951f484d0c1b7c5fd746ce3c47ccdedce68"},
- {file = "ruff-0.6.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:aefb0bd15f1cfa4c9c227b6120573bb3d6c4ee3b29fb54a5ad58f03859bc43c6"},
- {file = "ruff-0.6.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a4c0698cc780bcb2c61496cbd56b6a3ac0ad858c966652f7dbf4ceb029252fbe"},
- {file = "ruff-0.6.6-py3-none-win32.whl", hash = "sha256:aadf81ddc8ab5b62da7aae78a91ec933cbae9f8f1663ec0325dae2c364e4ad84"},
- {file = "ruff-0.6.6-py3-none-win_amd64.whl", hash = "sha256:0adb801771bc1f1b8cf4e0a6fdc30776e7c1894810ff3b344e50da82ef50eeb1"},
- {file = "ruff-0.6.6-py3-none-win_arm64.whl", hash = "sha256:4b4d32c137bc781c298964dd4e52f07d6f7d57c03eae97a72d97856844aa510a"},
- {file = "ruff-0.6.6.tar.gz", hash = "sha256:0fc030b6fd14814d69ac0196396f6761921bd20831725c7361e1b8100b818034"},
+ {file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"},
+ {file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"},
+ {file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"},
+ {file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"},
+ {file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"},
+ {file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"},
+ {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"},
+ {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"},
+ {file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"},
+ {file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"},
+ {file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"},
+ {file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"},
+ {file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"},
+ {file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"},
+ {file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"},
+ {file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"},
+ {file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"},
+ {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"},
]
[[package]]
From 4248594ac55c2adfcb84918c69ae29d351ca19b3 Mon Sep 17 00:00:00 2001
From: Mert <101130780+mertalev@users.noreply.github.com>
Date: Fri, 27 Sep 2024 18:10:39 -0400
Subject: [PATCH 20/21] feat(server): better transcoding logs (#13000)
* better transcoding logs
* pr feedback
---
server/src/interfaces/logger.interface.ts | 1 +
server/src/interfaces/media.interface.ts | 10 +-
server/src/repositories/media.repository.ts | 62 ++-
server/src/services/media.service.spec.ts | 405 ++++++++++--------
server/src/services/media.service.ts | 37 +-
server/src/utils/media.ts | 1 +
.../repositories/logger.repository.mock.ts | 2 +-
7 files changed, 308 insertions(+), 210 deletions(-)
diff --git a/server/src/interfaces/logger.interface.ts b/server/src/interfaces/logger.interface.ts
index ce9a8e64fe27f..42523afa6b513 100644
--- a/server/src/interfaces/logger.interface.ts
+++ b/server/src/interfaces/logger.interface.ts
@@ -6,6 +6,7 @@ export interface ILoggerRepository {
setAppName(name: string): void;
setContext(message: string): void;
setLogLevel(level: LogLevel): void;
+ isLevelEnabled(level: LogLevel): boolean;
verbose(message: any, ...args: any): void;
debug(message: any, ...args: any): void;
diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts
index 459e33fc3669b..7193684e7acc1 100644
--- a/server/src/interfaces/media.interface.ts
+++ b/server/src/interfaces/media.interface.ts
@@ -62,6 +62,10 @@ export interface TranscodeCommand {
inputOptions: string[];
outputOptions: string[];
twoPass: boolean;
+ progress: {
+ frameCount: number;
+ percentInterval: number;
+ };
}
export interface BitrateDistribution {
@@ -79,6 +83,10 @@ export interface VideoCodecHWConfig extends VideoCodecSWConfig {
getSupportedCodecs(): Array;
}
+export interface ProbeOptions {
+ countFrames: boolean;
+}
+
export interface IMediaRepository {
// image
extract(input: string, output: string): Promise;
@@ -87,6 +95,6 @@ export interface IMediaRepository {
getImageDimensions(input: string): Promise;
// video
- probe(input: string): Promise;
+ probe(input: string, options?: ProbeOptions): Promise;
transcode(input: string, output: string | Writable, command: TranscodeCommand): Promise;
}
diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts
index 5d1aced5eba65..d001aa3158b0f 100644
--- a/server/src/repositories/media.repository.ts
+++ b/server/src/repositories/media.repository.ts
@@ -1,15 +1,16 @@
import { Inject, Injectable } from '@nestjs/common';
import { exiftool } from 'exiftool-vendored';
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
+import { Duration } from 'luxon';
import fs from 'node:fs/promises';
import { Writable } from 'node:stream';
-import { promisify } from 'node:util';
import sharp from 'sharp';
-import { Colorspace } from 'src/enum';
+import { Colorspace, LogLevel } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import {
IMediaRepository,
ImageDimensions,
+ ProbeOptions,
ThumbnailOptions,
TranscodeCommand,
VideoInfo,
@@ -17,10 +18,22 @@ import {
import { Instrumentation } from 'src/utils/instrumentation';
import { handlePromiseError } from 'src/utils/misc';
-const probe = promisify(ffmpeg.ffprobe);
+const probe = (input: string, options: string[]): Promise =>
+ new Promise((resolve, reject) =>
+ ffmpeg.ffprobe(input, options, (error, data) => (error ? reject(error) : resolve(data))),
+ );
sharp.concurrency(0);
sharp.cache({ files: 0 });
+type ProgressEvent = {
+ frames: number;
+ currentFps: number;
+ currentKbps: number;
+ targetSize: number;
+ timemark: string;
+ percent?: number;
+};
+
@Instrumentation()
@Injectable()
export class MediaRepository implements IMediaRepository {
@@ -65,8 +78,8 @@ export class MediaRepository implements IMediaRepository {
.toFile(output);
}
- async probe(input: string): Promise {
- const results = await probe(input);
+ async probe(input: string, options?: ProbeOptions): Promise {
+ const results = await probe(input, options?.countFrames ? ['-count_packets'] : []); // gets frame count quickly: https://stackoverflow.com/a/28376817
return {
format: {
formatName: results.format.format_name,
@@ -83,10 +96,10 @@ export class MediaRepository implements IMediaRepository {
width: stream.width || 0,
codecName: stream.codec_name === 'h265' ? 'hevc' : stream.codec_name,
codecType: stream.codec_type,
- frameCount: Number.parseInt(stream.nb_frames ?? '0'),
- rotation: Number.parseInt(`${stream.rotation ?? 0}`),
+ frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames),
+ rotation: this.parseInt(stream.rotation),
isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67',
- bitrate: Number.parseInt(stream.bit_rate ?? '0'),
+ bitrate: this.parseInt(stream.bit_rate),
})),
audioStreams: results.streams
.filter((stream) => stream.codec_type === 'audio')
@@ -94,7 +107,7 @@ export class MediaRepository implements IMediaRepository {
index: stream.index,
codecType: stream.codec_type,
codecName: stream.codec_name,
- frameCount: Number.parseInt(stream.nb_frames ?? '0'),
+ frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames),
})),
};
}
@@ -156,10 +169,37 @@ export class MediaRepository implements IMediaRepository {
}
private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeCommand) {
- return ffmpeg(input, { niceness: 10 })
+ const ffmpegCall = ffmpeg(input, { niceness: 10 })
.inputOptions(options.inputOptions)
.outputOptions(options.outputOptions)
.output(output)
- .on('error', (error, stdout, stderr) => this.logger.error(stderr || error));
+ .on('start', (command: string) => this.logger.debug(command))
+ .on('error', (error, _, stderr) => this.logger.error(stderr || error));
+
+ const { frameCount, percentInterval } = options.progress;
+ const frameInterval = Math.ceil(frameCount / (100 / percentInterval));
+ if (this.logger.isLevelEnabled(LogLevel.DEBUG) && frameCount && frameInterval) {
+ let lastProgressFrame: number = 0;
+ ffmpegCall.on('progress', (progress: ProgressEvent) => {
+ if (progress.frames - lastProgressFrame < frameInterval) {
+ return;
+ }
+
+ lastProgressFrame = progress.frames;
+ const percent = ((progress.frames / frameCount) * 100).toFixed(2);
+ const ms = Math.floor((frameCount - progress.frames) / progress.currentFps) * 1000;
+ const duration = ms ? Duration.fromMillis(ms).rescale().toHuman({ unitDisplay: 'narrow' }) : '';
+ const outputText = output instanceof Writable ? 'stream' : output.split('/').pop();
+ this.logger.debug(
+ `Transcoding ${percent}% done${duration ? `, estimated ${duration} remaining` : ''} for output ${outputText}`,
+ );
+ });
+ }
+
+ return ffmpegCall;
+ }
+
+ private parseInt(value: string | number | undefined): number {
+ return Number.parseInt(value as string) || 0;
}
}
diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts
index ce6168408f9a3..ddda8f64fc74f 100644
--- a/server/src/services/media.service.spec.ts
+++ b/server/src/services/media.service.spec.ts
@@ -349,7 +349,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
- {
+ expect.objectContaining({
inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'],
outputOptions: [
'-fps_mode vfr',
@@ -359,7 +359,7 @@ describe(MediaService.name, () => {
String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,scale=-2:1440:flags=lanczos+accurate_rnd+full_chroma_int:out_color_matrix=601:out_range=pc,format=yuv420p`,
],
twoPass: false,
- },
+ }),
);
expect(assetMock.upsertFile).toHaveBeenCalledWith({
assetId: 'asset-id',
@@ -377,7 +377,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
- {
+ expect.objectContaining({
inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'],
outputOptions: [
'-fps_mode vfr',
@@ -387,7 +387,7 @@ describe(MediaService.name, () => {
String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p`,
],
twoPass: false,
- },
+ }),
);
expect(assetMock.upsertFile).toHaveBeenCalledWith({
assetId: 'asset-id',
@@ -407,7 +407,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
- {
+ expect.objectContaining({
inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'],
outputOptions: [
'-fps_mode vfr',
@@ -417,7 +417,7 @@ describe(MediaService.name, () => {
String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p`,
],
twoPass: false,
- },
+ }),
);
});
@@ -430,11 +430,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining([expect.stringContaining('scale=-2:1440')]),
twoPass: false,
- },
+ }),
);
});
@@ -731,21 +731,22 @@ describe(MediaService.name, () => {
it('should transcode the longest stream', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.video]);
+ loggerMock.isLevelEnabled.mockReturnValue(false);
mediaMock.probe.mockResolvedValue(probeStub.multipleVideoStreams);
await sut.handleVideoConversion({ id: assetStub.video.id });
- expect(mediaMock.probe).toHaveBeenCalledWith('/original/path.ext');
+ expect(mediaMock.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false });
expect(systemMock.get).toHaveBeenCalled();
expect(storageMock.mkdirSync).toHaveBeenCalled();
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-map 0:0', '-map 0:1']),
twoPass: false,
- },
+ }),
);
});
@@ -771,11 +772,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.any(Array),
twoPass: false,
- },
+ }),
);
});
@@ -786,11 +787,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.any(Array),
twoPass: false,
- },
+ }),
);
});
@@ -801,11 +802,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.any(Array),
twoPass: false,
- },
+ }),
);
});
@@ -816,11 +817,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.not.arrayContaining([expect.stringContaining('scale')]),
twoPass: false,
- },
+ }),
);
});
@@ -832,11 +833,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=-2:720/)]),
twoPass: false,
- },
+ }),
);
});
@@ -848,11 +849,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=720:-2/)]),
twoPass: false,
- },
+ }),
);
});
@@ -864,11 +865,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=-2:354/)]),
twoPass: false,
- },
+ }),
);
});
@@ -880,11 +881,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=354:-2/)]),
twoPass: false,
- },
+ }),
);
});
@@ -898,11 +899,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-c:v copy', '-c:a aac']),
twoPass: false,
- },
+ }),
);
});
@@ -920,11 +921,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.not.arrayContaining(['-tag:v hvc1']),
twoPass: false,
- },
+ }),
);
});
@@ -942,11 +943,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-c:v copy', '-tag:v hvc1']),
twoPass: false,
- },
+ }),
);
});
@@ -958,11 +959,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy']),
twoPass: false,
- },
+ }),
);
});
@@ -973,11 +974,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-c:v copy', '-c:a copy']),
twoPass: false,
- },
+ }),
);
});
@@ -1036,11 +1037,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-c:v h264', '-maxrate 4500k', '-bufsize 9000k']),
twoPass: false,
- },
+ }),
);
});
@@ -1052,11 +1053,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-c:v h264', '-b:v 3104k', '-minrate 1552k', '-maxrate 4500k']),
twoPass: true,
- },
+ }),
);
});
@@ -1068,11 +1069,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy']),
twoPass: false,
- },
+ }),
);
});
@@ -1090,11 +1091,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-b:v 3104k', '-minrate 1552k', '-maxrate 4500k']),
twoPass: true,
- },
+ }),
);
});
@@ -1112,11 +1113,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-maxrate')]),
twoPass: true,
- },
+ }),
);
});
@@ -1128,11 +1129,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-cpu-used 2']),
twoPass: false,
- },
+ }),
);
});
@@ -1144,11 +1145,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-cpu-used')]),
twoPass: false,
- },
+ }),
);
});
@@ -1160,11 +1161,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-threads 2']),
twoPass: false,
- },
+ }),
);
});
@@ -1176,11 +1177,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-threads 1', '-x264-params frame-threads=1:pools=none']),
twoPass: false,
- },
+ }),
);
});
@@ -1192,11 +1193,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-threads')]),
twoPass: false,
- },
+ }),
);
});
@@ -1208,11 +1209,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-c:v hevc', '-threads 1', '-x265-params frame-threads=1:pools=none']),
twoPass: false,
- },
+ }),
);
});
@@ -1224,11 +1225,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-threads')]),
twoPass: false,
- },
+ }),
);
});
@@ -1240,7 +1241,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining([
'-c:v av1',
@@ -1255,7 +1256,7 @@ describe(MediaService.name, () => {
'-crf 23',
]),
twoPass: false,
- },
+ }),
);
});
@@ -1267,11 +1268,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-preset 4']),
twoPass: false,
- },
+ }),
);
});
@@ -1283,11 +1284,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-svtav1-params mbr=2M']),
twoPass: false,
- },
+ }),
);
});
@@ -1299,11 +1300,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-svtav1-params lp=4']),
twoPass: false,
- },
+ }),
);
});
@@ -1315,11 +1316,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-svtav1-params lp=4:mbr=2M']),
twoPass: false,
- },
+ }),
);
});
@@ -1361,7 +1362,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
outputOptions: expect.arrayContaining([
'-tune hq',
@@ -1382,7 +1383,7 @@ describe(MediaService.name, () => {
'-cq:v 23',
]),
twoPass: false,
- },
+ }),
);
});
@@ -1400,11 +1401,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
outputOptions: expect.arrayContaining([expect.stringContaining('-multipass')]),
twoPass: false,
- },
+ }),
);
});
@@ -1416,11 +1417,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
outputOptions: expect.arrayContaining(['-cq:v 23', '-maxrate 10000k', '-bufsize 6897k']),
twoPass: false,
- },
+ }),
);
});
@@ -1432,11 +1433,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
outputOptions: expect.not.stringContaining('-maxrate'),
twoPass: false,
- },
+ }),
);
});
@@ -1448,11 +1449,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]),
twoPass: false,
- },
+ }),
);
});
@@ -1464,11 +1465,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-multipass')]),
twoPass: false,
- },
+ }),
);
});
@@ -1482,7 +1483,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-hwaccel cuda',
'-hwaccel_output_format cuda',
@@ -1491,7 +1492,7 @@ describe(MediaService.name, () => {
]),
outputOptions: expect.arrayContaining([expect.stringContaining('scale_cuda=-2:720:format=nv12')]),
twoPass: false,
- },
+ }),
);
});
@@ -1505,7 +1506,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']),
outputOptions: expect.arrayContaining([
expect.stringContaining(
@@ -1513,7 +1514,7 @@ describe(MediaService.name, () => {
),
]),
twoPass: false,
- },
+ }),
);
});
@@ -1526,7 +1527,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']),
outputOptions: expect.arrayContaining([
`-c:v h264_qsv`,
@@ -1547,7 +1548,7 @@ describe(MediaService.name, () => {
'-bufsize 20000k',
]),
twoPass: false,
- },
+ }),
);
});
@@ -1566,14 +1567,14 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device qsv=hw,child_device=/dev/dri/renderD128',
'-filter_hw_device hw',
]),
outputOptions: expect.any(Array),
twoPass: false,
- },
+ }),
);
});
@@ -1586,11 +1587,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]),
twoPass: false,
- },
+ }),
);
});
@@ -1603,11 +1604,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']),
outputOptions: expect.arrayContaining(['-low_power 1']),
twoPass: false,
- },
+ }),
);
});
@@ -1633,7 +1634,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-hwaccel qsv',
'-hwaccel_output_format qsv',
@@ -1645,7 +1646,7 @@ describe(MediaService.name, () => {
expect.stringContaining('scale_qsv=-1:720:async_depth=4:mode=hq:format=nv12'),
]),
twoPass: false,
- },
+ }),
);
});
@@ -1662,7 +1663,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-hwaccel qsv',
'-hwaccel_output_format qsv',
@@ -1675,7 +1676,7 @@ describe(MediaService.name, () => {
),
]),
twoPass: false,
- },
+ }),
);
});
@@ -1691,11 +1692,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-hwaccel qsv', '-qsv_device /dev/dri/renderD129']),
outputOptions: expect.any(Array),
twoPass: false,
- },
+ }),
);
});
@@ -1708,7 +1709,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device vaapi=accel:/dev/dri/renderD128',
'-filter_hw_device accel',
@@ -1728,7 +1729,7 @@ describe(MediaService.name, () => {
'-rc_mode 1',
]),
twoPass: false,
- },
+ }),
);
});
@@ -1741,7 +1742,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device vaapi=accel:/dev/dri/renderD128',
'-filter_hw_device accel',
@@ -1754,7 +1755,7 @@ describe(MediaService.name, () => {
'-rc_mode 3',
]),
twoPass: false,
- },
+ }),
);
});
@@ -1767,7 +1768,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device vaapi=accel:/dev/dri/renderD128',
'-filter_hw_device accel',
@@ -1780,7 +1781,7 @@ describe(MediaService.name, () => {
'-rc_mode 1',
]),
twoPass: false,
- },
+ }),
);
});
@@ -1793,14 +1794,14 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device vaapi=accel:/dev/dri/renderD128',
'-filter_hw_device accel',
]),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-compression_level')]),
twoPass: false,
- },
+ }),
);
});
@@ -1813,14 +1814,14 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device vaapi=accel:/dev/dri/card1',
'-filter_hw_device accel',
]),
outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]),
twoPass: false,
- },
+ }),
);
});
@@ -1833,14 +1834,14 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device vaapi=accel:/dev/dri/renderD130',
'-filter_hw_device accel',
]),
outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]),
twoPass: false,
- },
+ }),
);
});
@@ -1855,14 +1856,14 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device vaapi=accel:/dev/dri/renderD128',
'-filter_hw_device accel',
]),
outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]),
twoPass: false,
- },
+ }),
);
});
@@ -1877,11 +1878,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenLastCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-c:v h264']),
twoPass: false,
- },
+ }),
);
});
@@ -1904,7 +1905,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining([
'-hwaccel rkmpp',
'-hwaccel_output_format drm_prime',
@@ -1927,7 +1928,7 @@ describe(MediaService.name, () => {
'-qp_init 23',
]),
twoPass: false,
- },
+ }),
);
});
@@ -1948,11 +1949,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
outputOptions: expect.arrayContaining([`-c:v hevc_rkmpp`, '-level 153', '-rc_mode AVBR', '-b:v 10000k']),
twoPass: false,
- },
+ }),
);
});
@@ -1968,11 +1969,11 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
outputOptions: expect.arrayContaining([`-c:v h264_rkmpp`, '-level 51', '-rc_mode CQP', '-qp_init 30']),
twoPass: false,
- },
+ }),
);
});
@@ -1988,7 +1989,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
outputOptions: expect.arrayContaining([
expect.stringContaining(
@@ -1996,7 +1997,7 @@ describe(MediaService.name, () => {
),
]),
twoPass: false,
- },
+ }),
);
});
@@ -2012,7 +2013,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: [],
outputOptions: expect.arrayContaining([
expect.stringContaining(
@@ -2020,7 +2021,7 @@ describe(MediaService.name, () => {
),
]),
twoPass: false,
- },
+ }),
);
});
@@ -2036,7 +2037,7 @@ describe(MediaService.name, () => {
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
+ expect.objectContaining({
inputOptions: [],
outputOptions: expect.arrayContaining([
expect.stringContaining(
@@ -2044,69 +2045,101 @@ describe(MediaService.name, () => {
),
]),
twoPass: false,
- },
+ }),
);
});
- });
- it('should tonemap when policy is required and video is hdr', async () => {
- mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
- systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
- assetMock.getByIds.mockResolvedValue([assetStub.video]);
- await sut.handleVideoConversion({ id: assetStub.video.id });
- expect(mediaMock.transcode).toHaveBeenCalledWith(
- '/original/path.ext',
- 'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
- inputOptions: expect.any(Array),
- outputOptions: expect.arrayContaining([
- '-c:v h264',
- '-c:a copy',
- '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
- ]),
- twoPass: false,
- },
- );
- });
+ it('should tonemap when policy is required and video is hdr', async () => {
+ mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
+ systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
+ assetMock.getByIds.mockResolvedValue([assetStub.video]);
+ await sut.handleVideoConversion({ id: assetStub.video.id });
+ expect(mediaMock.transcode).toHaveBeenCalledWith(
+ '/original/path.ext',
+ 'upload/encoded-video/user-id/as/se/asset-id.mp4',
+ expect.objectContaining({
+ inputOptions: expect.any(Array),
+ outputOptions: expect.arrayContaining([
+ '-c:v h264',
+ '-c:a copy',
+ '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
+ ]),
+ twoPass: false,
+ }),
+ );
+ });
- it('should tonemap when policy is optimal and video is hdr', async () => {
- mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
- systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
- assetMock.getByIds.mockResolvedValue([assetStub.video]);
- await sut.handleVideoConversion({ id: assetStub.video.id });
- expect(mediaMock.transcode).toHaveBeenCalledWith(
- '/original/path.ext',
- 'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
- inputOptions: expect.any(Array),
- outputOptions: expect.arrayContaining([
- '-c:v h264',
- '-c:a copy',
- '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
- ]),
- twoPass: false,
- },
- );
- });
+ it('should tonemap when policy is optimal and video is hdr', async () => {
+ mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
+ systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
+ assetMock.getByIds.mockResolvedValue([assetStub.video]);
+ await sut.handleVideoConversion({ id: assetStub.video.id });
+ expect(mediaMock.transcode).toHaveBeenCalledWith(
+ '/original/path.ext',
+ 'upload/encoded-video/user-id/as/se/asset-id.mp4',
+ expect.objectContaining({
+ inputOptions: expect.any(Array),
+ outputOptions: expect.arrayContaining([
+ '-c:v h264',
+ '-c:a copy',
+ '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
+ ]),
+ twoPass: false,
+ }),
+ );
+ });
- it('should set npl to 250 for reinhard and mobius tone-mapping algorithms', async () => {
- mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
- systemMock.get.mockResolvedValue({ ffmpeg: { tonemap: ToneMapping.MOBIUS } });
- assetMock.getByIds.mockResolvedValue([assetStub.video]);
- await sut.handleVideoConversion({ id: assetStub.video.id });
- expect(mediaMock.transcode).toHaveBeenCalledWith(
- '/original/path.ext',
- 'upload/encoded-video/user-id/as/se/asset-id.mp4',
- {
- inputOptions: expect.any(Array),
- outputOptions: expect.arrayContaining([
- '-c:v h264',
- '-c:a copy',
- '-vf zscale=t=linear:npl=250,tonemap=mobius:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
- ]),
- twoPass: false,
- },
- );
+ it('should set npl to 250 for reinhard and mobius tone-mapping algorithms', async () => {
+ mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
+ systemMock.get.mockResolvedValue({ ffmpeg: { tonemap: ToneMapping.MOBIUS } });
+ assetMock.getByIds.mockResolvedValue([assetStub.video]);
+ await sut.handleVideoConversion({ id: assetStub.video.id });
+ expect(mediaMock.transcode).toHaveBeenCalledWith(
+ '/original/path.ext',
+ 'upload/encoded-video/user-id/as/se/asset-id.mp4',
+ expect.objectContaining({
+ inputOptions: expect.any(Array),
+ outputOptions: expect.arrayContaining([
+ '-c:v h264',
+ '-c:a copy',
+ '-vf zscale=t=linear:npl=250,tonemap=mobius:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
+ ]),
+ twoPass: false,
+ }),
+ );
+ });
+
+ it('should count frames for progress when log level is debug', async () => {
+ mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
+ loggerMock.isLevelEnabled.mockReturnValue(true);
+ assetMock.getByIds.mockResolvedValue([assetStub.video]);
+
+ await sut.handleVideoConversion({ id: assetStub.video.id });
+
+ expect(mediaMock.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: true });
+ expect(mediaMock.transcode).toHaveBeenCalledWith(
+ assetStub.video.originalPath,
+ 'upload/encoded-video/user-id/as/se/asset-id.mp4',
+ {
+ inputOptions: expect.any(Array),
+ outputOptions: expect.any(Array),
+ twoPass: false,
+ progress: {
+ frameCount: probeStub.videoStream2160p.videoStreams[0].frameCount,
+ percentInterval: expect.any(Number),
+ },
+ },
+ );
+ });
+
+ it('should not count frames for progress when log level is not debug', async () => {
+ mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
+ loggerMock.isLevelEnabled.mockReturnValue(false);
+ assetMock.getByIds.mockResolvedValue([assetStub.video]);
+ await sut.handleVideoConversion({ id: assetStub.video.id });
+
+ expect(mediaMock.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: false });
+ });
});
describe('isSRGB', () => {
diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts
index 55a4ee015757b..720bef6c7661b 100644
--- a/server/src/services/media.service.ts
+++ b/server/src/services/media.service.ts
@@ -11,6 +11,7 @@ import {
AudioCodec,
Colorspace,
ImageFormat,
+ LogLevel,
StorageFolder,
TranscodeHWAccel,
TranscodePolicy,
@@ -31,7 +32,13 @@ import {
QueueName,
} from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
-import { AudioStreamInfo, IMediaRepository, VideoFormat, VideoStreamInfo } from 'src/interfaces/media.interface';
+import {
+ AudioStreamInfo,
+ IMediaRepository,
+ TranscodeCommand,
+ VideoFormat,
+ VideoStreamInfo,
+} from 'src/interfaces/media.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
@@ -346,7 +353,9 @@ export class MediaService {
const output = StorageCore.getEncodedVideoPath(asset);
this.storageCore.ensureFolders(output);
- const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input);
+ const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input, {
+ countFrames: this.logger.isLevelEnabled(LogLevel.DEBUG), // makes frame count more reliable for progress logs
+ });
const mainVideoStream = this.getMainStream(videoStreams);
const mainAudioStream = this.getMainStream(audioStreams);
if (!mainVideoStream || !format.formatName) {
@@ -365,12 +374,14 @@ export class MediaService {
this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [asset.encodedVideoPath] } });
await this.assetRepository.update({ id: asset.id, encodedVideoPath: null });
+ } else {
+ this.logger.verbose(`Asset ${asset.id} does not require transcoding based on current policy, skipping`);
}
return JobStatus.SKIPPED;
}
- let command;
+ let command: TranscodeCommand;
try {
const config = BaseConfig.create(ffmpeg, await this.getDevices(), await this.hasMaliOpenCL());
command = config.getCommand(target, mainVideoStream, mainAudioStream);
@@ -379,16 +390,20 @@ export class MediaService {
return JobStatus.FAILED;
}
- this.logger.log(`Started encoding video ${asset.id} ${JSON.stringify(command)}`);
+ if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
+ this.logger.log(`Encoding video ${asset.id} without hardware acceleration`);
+ } else {
+ this.logger.log(`Encoding video ${asset.id} with ${ffmpeg.accel.toUpperCase()} acceleration`);
+ }
+
try {
await this.mediaRepository.transcode(input, output, command);
- } catch (error) {
- this.logger.error(error);
- if (ffmpeg.accel !== TranscodeHWAccel.DISABLED) {
- this.logger.error(
- `Error occurred during transcoding. Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled.`,
- );
+ } catch (error: any) {
+ this.logger.error(`Error occurred during transcoding: ${error.message}`);
+ if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
+ return JobStatus.FAILED;
}
+ this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`);
const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED });
command = config.getCommand(target, mainVideoStream, mainAudioStream);
await this.mediaRepository.transcode(input, output, command);
@@ -555,7 +570,7 @@ export class MediaService {
const maliDeviceStat = await this.storageRepository.stat('/dev/mali0');
this.maliOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
} catch {
- this.logger.debug('OpenCL not available for transcoding, using CPU decoding instead.');
+ this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU decoding');
this.maliOpenCL = false;
}
}
diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts
index d80651eece3a8..6f0ab4ef81d90 100644
--- a/server/src/utils/media.ts
+++ b/server/src/utils/media.ts
@@ -80,6 +80,7 @@ export class BaseConfig implements VideoCodecSWConfig {
inputOptions: this.getBaseInputOptions(videoStream),
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
twoPass: this.eligibleForTwoPass(),
+ progress: { frameCount: videoStream.frameCount, percentInterval: 5 },
} as TranscodeCommand;
if ([TranscodeTarget.ALL, TranscodeTarget.VIDEO].includes(target)) {
const filters = this.getFilterOptions(videoStream);
diff --git a/server/test/repositories/logger.repository.mock.ts b/server/test/repositories/logger.repository.mock.ts
index 5f7262c7e5d92..6342e9e73cc85 100644
--- a/server/test/repositories/logger.repository.mock.ts
+++ b/server/test/repositories/logger.repository.mock.ts
@@ -6,7 +6,7 @@ export const newLoggerRepositoryMock = (): Mocked => {
setLogLevel: vitest.fn(),
setContext: vitest.fn(),
setAppName: vitest.fn(),
-
+ isLevelEnabled: vitest.fn(),
verbose: vitest.fn(),
debug: vitest.fn(),
log: vitest.fn(),
From 995f0fda475d40e969190925af455c20abb7a02b Mon Sep 17 00:00:00 2001
From: Mert <101130780+mertalev@users.noreply.github.com>
Date: Sat, 28 Sep 2024 02:01:04 -0400
Subject: [PATCH 21/21] feat(server): separate quality for thumbnail and
preview images (#13006)
* allow different thumbnail and preview quality, better config structure
* update web and api
* wording
* remove empty line?
---
mobile/openapi/README.md | 1 +
mobile/openapi/lib/api.dart | 1 +
mobile/openapi/lib/api_client.dart | 2 +
.../system_config_generated_image_dto.dart | 118 ++++++++++++++
.../lib/model/system_config_image_dto.dart | 58 ++-----
open-api/immich-openapi-specs.json | 48 +++---
open-api/typescript-sdk/src/fetch-client.ts | 12 +-
server/src/config.ts | 23 +--
server/src/dtos/system-config.dto.ts | 30 ++--
server/src/interfaces/media.interface.ts | 9 +-
...7-SeparateQualityForThumbnailAndPreview.ts | 37 +++++
server/src/services/media.service.spec.ts | 8 +-
server/src/services/media.service.ts | 27 ++--
server/src/services/person.service.ts | 2 +-
.../services/system-config.service.spec.ts | 15 +-
.../settings/image/image-settings.svelte | 150 ++++++++++--------
web/src/lib/i18n/en.json | 16 +-
17 files changed, 364 insertions(+), 193 deletions(-)
create mode 100644 mobile/openapi/lib/model/system_config_generated_image_dto.dart
create mode 100644 server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index fecbbf482be54..81827a9079e5a 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -416,6 +416,7 @@ Class | Method | HTTP request | Description
- [SystemConfigDto](doc//SystemConfigDto.md)
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
- [SystemConfigFacesDto](doc//SystemConfigFacesDto.md)
+ - [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md)
- [SystemConfigImageDto](doc//SystemConfigImageDto.md)
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 22b48df2fbcb1..8be44029805d5 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -229,6 +229,7 @@ part 'model/stack_update_dto.dart';
part 'model/system_config_dto.dart';
part 'model/system_config_f_fmpeg_dto.dart';
part 'model/system_config_faces_dto.dart';
+part 'model/system_config_generated_image_dto.dart';
part 'model/system_config_image_dto.dart';
part 'model/system_config_job_dto.dart';
part 'model/system_config_library_dto.dart';
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 3db3297acb091..9e38eaf30a8a9 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -512,6 +512,8 @@ class ApiClient {
return SystemConfigFFmpegDto.fromJson(value);
case 'SystemConfigFacesDto':
return SystemConfigFacesDto.fromJson(value);
+ case 'SystemConfigGeneratedImageDto':
+ return SystemConfigGeneratedImageDto.fromJson(value);
case 'SystemConfigImageDto':
return SystemConfigImageDto.fromJson(value);
case 'SystemConfigJobDto':
diff --git a/mobile/openapi/lib/model/system_config_generated_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_image_dto.dart
new file mode 100644
index 0000000000000..2192a7cb0cbd5
--- /dev/null
+++ b/mobile/openapi/lib/model/system_config_generated_image_dto.dart
@@ -0,0 +1,118 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.18
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class SystemConfigGeneratedImageDto {
+ /// Returns a new [SystemConfigGeneratedImageDto] instance.
+ SystemConfigGeneratedImageDto({
+ required this.format,
+ required this.quality,
+ required this.size,
+ });
+
+ ImageFormat format;
+
+ /// Minimum value: 1
+ /// Maximum value: 100
+ int quality;
+
+ /// Minimum value: 1
+ int size;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedImageDto &&
+ other.format == format &&
+ other.quality == quality &&
+ other.size == size;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (format.hashCode) +
+ (quality.hashCode) +
+ (size.hashCode);
+
+ @override
+ String toString() => 'SystemConfigGeneratedImageDto[format=$format, quality=$quality, size=$size]';
+
+ Map toJson() {
+ final json = {};
+ json[r'format'] = this.format;
+ json[r'quality'] = this.quality;
+ json[r'size'] = this.size;
+ return json;
+ }
+
+ /// Returns a new [SystemConfigGeneratedImageDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static SystemConfigGeneratedImageDto? fromJson(dynamic value) {
+ upgradeDto(value, "SystemConfigGeneratedImageDto");
+ if (value is Map) {
+ final json = value.cast();
+
+ return SystemConfigGeneratedImageDto(
+ format: ImageFormat.fromJson(json[r'format'])!,
+ quality: mapValueOfType(json, r'quality')!,
+ size: mapValueOfType(json, r'size')!,
+ );
+ }
+ return null;
+ }
+
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = SystemConfigGeneratedImageDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = SystemConfigGeneratedImageDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of SystemConfigGeneratedImageDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = SystemConfigGeneratedImageDto.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'format',
+ 'quality',
+ 'size',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart
index 681a8c00c3bc0..5309f7745c44d 100644
--- a/mobile/openapi/lib/model/system_config_image_dto.dart
+++ b/mobile/openapi/lib/model/system_config_image_dto.dart
@@ -15,64 +15,42 @@ class SystemConfigImageDto {
SystemConfigImageDto({
required this.colorspace,
required this.extractEmbedded,
- required this.previewFormat,
- required this.previewSize,
- required this.quality,
- required this.thumbnailFormat,
- required this.thumbnailSize,
+ required this.preview,
+ required this.thumbnail,
});
Colorspace colorspace;
bool extractEmbedded;
- ImageFormat previewFormat;
+ SystemConfigGeneratedImageDto preview;
- /// Minimum value: 1
- int previewSize;
-
- /// Minimum value: 1
- /// Maximum value: 100
- int quality;
-
- ImageFormat thumbnailFormat;
-
- /// Minimum value: 1
- int thumbnailSize;
+ SystemConfigGeneratedImageDto thumbnail;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto &&
other.colorspace == colorspace &&
other.extractEmbedded == extractEmbedded &&
- other.previewFormat == previewFormat &&
- other.previewSize == previewSize &&
- other.quality == quality &&
- other.thumbnailFormat == thumbnailFormat &&
- other.thumbnailSize == thumbnailSize;
+ other.preview == preview &&
+ other.thumbnail == thumbnail;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(colorspace.hashCode) +
(extractEmbedded.hashCode) +
- (previewFormat.hashCode) +
- (previewSize.hashCode) +
- (quality.hashCode) +
- (thumbnailFormat.hashCode) +
- (thumbnailSize.hashCode);
+ (preview.hashCode) +
+ (thumbnail.hashCode);
@override
- String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]';
+ String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, preview=$preview, thumbnail=$thumbnail]';
Map toJson() {
final json = {};
json[r'colorspace'] = this.colorspace;
json[r'extractEmbedded'] = this.extractEmbedded;
- json[r'previewFormat'] = this.previewFormat;
- json[r'previewSize'] = this.previewSize;
- json[r'quality'] = this.quality;
- json[r'thumbnailFormat'] = this.thumbnailFormat;
- json[r'thumbnailSize'] = this.thumbnailSize;
+ json[r'preview'] = this.preview;
+ json[r'thumbnail'] = this.thumbnail;
return json;
}
@@ -87,11 +65,8 @@ class SystemConfigImageDto {
return SystemConfigImageDto(
colorspace: Colorspace.fromJson(json[r'colorspace'])!,
extractEmbedded: mapValueOfType(json, r'extractEmbedded')!,
- previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!,
- previewSize: mapValueOfType(json, r'previewSize')!,
- quality: mapValueOfType(json, r'quality')!,
- thumbnailFormat: ImageFormat.fromJson(json[r'thumbnailFormat'])!,
- thumbnailSize: mapValueOfType(json, r'thumbnailSize')!,
+ preview: SystemConfigGeneratedImageDto.fromJson(json[r'preview'])!,
+ thumbnail: SystemConfigGeneratedImageDto.fromJson(json[r'thumbnail'])!,
);
}
return null;
@@ -141,11 +116,8 @@ class SystemConfigImageDto {
static const requiredKeys = {
'colorspace',
'extractEmbedded',
- 'previewFormat',
- 'previewSize',
- 'quality',
- 'thumbnailFormat',
- 'thumbnailSize',
+ 'preview',
+ 'thumbnail',
};
}
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index 6afd0d792ff34..1077762ac3a56 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -11654,42 +11654,48 @@
],
"type": "object"
},
- "SystemConfigImageDto": {
+ "SystemConfigGeneratedImageDto": {
"properties": {
- "colorspace": {
- "$ref": "#/components/schemas/Colorspace"
- },
- "extractEmbedded": {
- "type": "boolean"
- },
- "previewFormat": {
+ "format": {
"$ref": "#/components/schemas/ImageFormat"
},
- "previewSize": {
- "minimum": 1,
- "type": "integer"
- },
"quality": {
"maximum": 100,
"minimum": 1,
"type": "integer"
},
- "thumbnailFormat": {
- "$ref": "#/components/schemas/ImageFormat"
- },
- "thumbnailSize": {
+ "size": {
"minimum": 1,
"type": "integer"
}
},
+ "required": [
+ "format",
+ "quality",
+ "size"
+ ],
+ "type": "object"
+ },
+ "SystemConfigImageDto": {
+ "properties": {
+ "colorspace": {
+ "$ref": "#/components/schemas/Colorspace"
+ },
+ "extractEmbedded": {
+ "type": "boolean"
+ },
+ "preview": {
+ "$ref": "#/components/schemas/SystemConfigGeneratedImageDto"
+ },
+ "thumbnail": {
+ "$ref": "#/components/schemas/SystemConfigGeneratedImageDto"
+ }
+ },
"required": [
"colorspace",
"extractEmbedded",
- "previewFormat",
- "previewSize",
- "quality",
- "thumbnailFormat",
- "thumbnailSize"
+ "preview",
+ "thumbnail"
],
"type": "object"
},
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index b1ae5d28764f2..e88f431e8c787 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -1100,14 +1100,16 @@ export type SystemConfigFFmpegDto = {
transcode: TranscodePolicy;
twoPass: boolean;
};
+export type SystemConfigGeneratedImageDto = {
+ format: ImageFormat;
+ quality: number;
+ size: number;
+};
export type SystemConfigImageDto = {
colorspace: Colorspace;
extractEmbedded: boolean;
- previewFormat: ImageFormat;
- previewSize: number;
- quality: number;
- thumbnailFormat: ImageFormat;
- thumbnailSize: number;
+ preview: SystemConfigGeneratedImageDto;
+ thumbnail: SystemConfigGeneratedImageDto;
};
export type JobSettingsDto = {
concurrency: number;
diff --git a/server/src/config.ts b/server/src/config.ts
index 1522371487e3b..3317351f9ff3a 100644
--- a/server/src/config.ts
+++ b/server/src/config.ts
@@ -20,6 +20,7 @@ import {
VideoContainer,
} from 'src/enum';
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
+import { ImageOutputConfig } from 'src/interfaces/media.interface';
export interface SystemConfig {
ffmpeg: {
@@ -109,11 +110,8 @@ export interface SystemConfig {
template: string;
};
image: {
- thumbnailFormat: ImageFormat;
- thumbnailSize: number;
- previewFormat: ImageFormat;
- previewSize: number;
- quality: number;
+ thumbnail: ImageOutputConfig;
+ preview: ImageOutputConfig;
colorspace: Colorspace;
extractEmbedded: boolean;
};
@@ -259,11 +257,16 @@ export const defaults = Object.freeze({
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
},
image: {
- thumbnailFormat: ImageFormat.WEBP,
- thumbnailSize: 250,
- previewFormat: ImageFormat.JPEG,
- previewSize: 1440,
- quality: 80,
+ thumbnail: {
+ format: ImageFormat.WEBP,
+ size: 250,
+ quality: 80,
+ },
+ preview: {
+ format: ImageFormat.JPEG,
+ size: 1440,
+ quality: 80,
+ },
colorspace: Colorspace.P3,
extractEmbedded: false,
},
diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts
index 4a3ca37691604..c12a54cd613e6 100644
--- a/server/src/dtos/system-config.dto.ts
+++ b/server/src/dtos/system-config.dto.ts
@@ -473,33 +473,35 @@ export class SystemConfigThemeDto {
customCss!: string;
}
-class SystemConfigImageDto {
+class SystemConfigGeneratedImageDto {
@IsEnum(ImageFormat)
@ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
- thumbnailFormat!: ImageFormat;
+ format!: ImageFormat;
@IsInt()
@Min(1)
+ @Max(100)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
- thumbnailSize!: number;
-
- @IsEnum(ImageFormat)
- @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
- previewFormat!: ImageFormat;
+ quality!: number;
@IsInt()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
- previewSize!: number;
+ size!: number;
+}
- @IsInt()
- @Min(1)
- @Max(100)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- quality!: number;
+class SystemConfigImageDto {
+ @Type(() => SystemConfigGeneratedImageDto)
+ @ValidateNested()
+ @IsObject()
+ thumbnail!: SystemConfigGeneratedImageDto;
+
+ @Type(() => SystemConfigGeneratedImageDto)
+ @ValidateNested()
+ @IsObject()
+ preview!: SystemConfigGeneratedImageDto;
@IsEnum(Colorspace)
@ApiProperty({ enumName: 'Colorspace', enum: Colorspace })
diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts
index 7193684e7acc1..64ba6236e80f0 100644
--- a/server/src/interfaces/media.interface.ts
+++ b/server/src/interfaces/media.interface.ts
@@ -10,11 +10,14 @@ export interface CropOptions {
height: number;
}
-export interface ThumbnailOptions {
- size: number;
+export interface ImageOutputConfig {
format: ImageFormat;
- colorspace: string;
quality: number;
+ size: number;
+}
+
+export interface ThumbnailOptions extends ImageOutputConfig {
+ colorspace: string;
crop?: CropOptions;
processInvalidImages: boolean;
}
diff --git a/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts b/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts
new file mode 100644
index 0000000000000..e02203997f723
--- /dev/null
+++ b/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts
@@ -0,0 +1,37 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class SeparateQualityForThumbnailAndPreview1727471863507 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`
+ update system_metadata
+ set value = jsonb_set(value, '{image}', jsonb_strip_nulls(
+ jsonb_build_object(
+ 'preview', jsonb_build_object(
+ 'format', value->'image'->'previewFormat',
+ 'quality', value->'image'->'quality',
+ 'size', value->'image'->'previewSize'),
+ 'thumbnail', jsonb_build_object(
+ 'format', value->'image'->'thumbnailFormat',
+ 'quality', value->'image'->'quality',
+ 'size', value->'image'->'thumbnailSize'),
+ 'extractEmbedded', value->'extractEmbedded',
+ 'colorspace', value->'colorspace'
+ )))
+ where key = 'system-config'`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`
+ update system_metadata
+ set value = jsonb_set(value, '{image}', jsonb_strip_nulls(jsonb_build_object(
+ 'previewFormat', value->'image'->'preview'->'format',
+ 'previewSize', value->'image'->'preview'->'size',
+ 'thumbnailFormat', value->'image'->'thumbnail'->'format',
+ 'thumbnailSize', value->'image'->'thumbnail'->'size',
+ 'extractEmbedded', value->'extractEmbedded',
+ 'colorspace', value->'colorspace',
+ 'quality', value->'image'->'preview'->'quality'
+ )))
+ where key = 'system-config'`);
+ }
+}
diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts
index ddda8f64fc74f..c0903fa101412 100644
--- a/server/src/services/media.service.spec.ts
+++ b/server/src/services/media.service.spec.ts
@@ -285,7 +285,7 @@ describe(MediaService.name, () => {
});
it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => {
- systemMock.get.mockResolvedValue({ image: { previewFormat: format } });
+ systemMock.get.mockResolvedValue({ image: { preview: { format } } });
assetMock.getByIds.mockResolvedValue([assetStub.image]);
const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.${format}`;
@@ -307,7 +307,7 @@ describe(MediaService.name, () => {
});
it('should delete previous preview if different path', async () => {
- systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
+ systemMock.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.WEBP } } });
assetMock.getByIds.mockResolvedValue([assetStub.image]);
await sut.handleGeneratePreview({ id: assetStub.image.id });
@@ -464,7 +464,7 @@ describe(MediaService.name, () => {
it.each(Object.values(ImageFormat))(
'should generate a %s thumbnail for an image when specified',
async (format) => {
- systemMock.get.mockResolvedValue({ image: { thumbnailFormat: format } });
+ systemMock.get.mockResolvedValue({ image: { thumbnail: { format } } });
assetMock.getByIds.mockResolvedValue([assetStub.image]);
const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`;
@@ -487,7 +487,7 @@ describe(MediaService.name, () => {
);
it('should delete previous thumbnail if different path', async () => {
- systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
+ systemMock.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.WEBP } } });
assetMock.getByIds.mockResolvedValue([assetStub.image]);
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts
index 720bef6c7661b..1b69c5acd5504 100644
--- a/server/src/services/media.service.ts
+++ b/server/src/services/media.service.ts
@@ -10,7 +10,6 @@ import {
AssetType,
AudioCodec,
Colorspace,
- ImageFormat,
LogLevel,
StorageFolder,
TranscodeHWAccel,
@@ -175,18 +174,15 @@ export class MediaService {
return JobStatus.FAILED;
}
- await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.previewFormat);
- await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
+ await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.preview.format);
+ await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
await this.storageCore.moveAssetVideo(asset);
return JobStatus.SUCCESS;
}
async handleGeneratePreview({ id }: IEntityJob): Promise {
- const [{ image }, [asset]] = await Promise.all([
- this.configCore.getConfig({ withCache: true }),
- this.assetRepository.getByIds([id], { exifInfo: true, files: true }),
- ]);
+ const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true, files: true });
if (!asset) {
return JobStatus.FAILED;
}
@@ -195,7 +191,7 @@ export class MediaService {
return JobStatus.SKIPPED;
}
- const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
+ const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW);
if (!previewPath) {
return JobStatus.SKIPPED;
}
@@ -213,9 +209,9 @@ export class MediaService {
return JobStatus.SUCCESS;
}
- private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) {
+ private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType) {
const { image, ffmpeg } = await this.configCore.getConfig({ withCache: true });
- const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize;
+ const { size, format, quality } = image[type];
const path = StorageCore.getImagePath(asset, type, format);
this.storageCore.ensureFolders(path);
@@ -226,13 +222,13 @@ export class MediaService {
const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath));
try {
- const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize));
+ const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.preview.size));
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
const imageOptions = {
format,
size,
colorspace,
- quality: image.quality,
+ quality,
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
};
@@ -274,10 +270,7 @@ export class MediaService {
}
async handleGenerateThumbnail({ id }: IEntityJob): Promise {
- const [{ image }, [asset]] = await Promise.all([
- this.configCore.getConfig({ withCache: true }),
- this.assetRepository.getByIds([id], { exifInfo: true, files: true }),
- ]);
+ const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true, files: true });
if (!asset) {
return JobStatus.FAILED;
}
@@ -286,7 +279,7 @@ export class MediaService {
return JobStatus.SKIPPED;
}
- const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
+ const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL);
if (!thumbnailPath) {
return JobStatus.SKIPPED;
}
diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts
index 7cb76d1a71535..651c8eebee54e 100644
--- a/server/src/services/person.service.ts
+++ b/server/src/services/person.service.ts
@@ -574,7 +574,7 @@ export class PersonService {
format: ImageFormat.JPEG,
size: FACE_THUMBNAIL_SIZE,
colorspace: image.colorspace,
- quality: image.quality,
+ quality: image.thumbnail.quality,
crop: this.getCrop({ old: { width: oldWidth, height: oldHeight }, new: { width, height } }, { x1, y1, x2, y2 }),
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
} as const;
diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts
index 8b4fb0bc2fd3c..514d8aa0f8d58 100644
--- a/server/src/services/system-config.service.spec.ts
+++ b/server/src/services/system-config.service.spec.ts
@@ -135,11 +135,16 @@ const updatedConfig = Object.freeze({
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
},
image: {
- thumbnailFormat: ImageFormat.WEBP,
- thumbnailSize: 250,
- previewFormat: ImageFormat.JPEG,
- previewSize: 1440,
- quality: 80,
+ thumbnail: {
+ size: 250,
+ format: ImageFormat.WEBP,
+ quality: 80,
+ },
+ preview: {
+ size: 1440,
+ format: ImageFormat.JPEG,
+ quality: 80,
+ },
colorspace: Colorspace.P3,
extractEmbedded: false,
},
diff --git a/web/src/lib/components/admin-page/settings/image/image-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte
index d6fc814b98e4c..b5e381d5f87a0 100644
--- a/web/src/lib/components/admin-page/settings/image/image-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte
@@ -11,6 +11,7 @@
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import { t } from 'svelte-i18n';
+ import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -24,73 +25,96 @@