mirror of
https://github.com/immich-app/immich.git
synced 2025-02-03 01:22:44 +01:00
Compare commits
18 commits
f4d811a2d2
...
01f2a058a9
Author | SHA1 | Date | |
---|---|---|---|
|
01f2a058a9 | ||
|
92dff839d0 | ||
|
fe1e09e51f | ||
|
f44669447f | ||
|
92412ca2f7 | ||
|
64d926581f | ||
|
c139e05170 | ||
|
0fe62298e1 | ||
|
e5794e6cfc | ||
|
f6cbc9db06 | ||
|
5942d9b52c | ||
|
38f903d317 | ||
|
86bf4d1d5c | ||
|
90aaefa2cc | ||
|
ac50c12173 | ||
|
0e504da241 | ||
|
71430c3a12 | ||
|
1e97f71734 |
45 changed files with 222 additions and 110 deletions
cli
docs
e2e
i18n
machine-learning
mobile
open-api
server
package-lock.jsonpackage.json
src
config.ts
dtos
entities
queries
repositories
services
album.service.tsmedia.service.spec.tsmedia.service.tsmetadata.service.spec.tsmetadata.service.tssystem-config.service.spec.ts
utils
test
web
6
cli/package-lock.json
generated
6
cli/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.44",
|
||||
"version": "2.2.47",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.44",
|
||||
"version": "2.2.47",
|
||||
"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.125.3",
|
||||
"version": "1.125.6",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.44",
|
||||
"version": "2.2.47",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
|
|
|
@ -23,6 +23,7 @@ The default configuration looks like this:
|
|||
"acceptedContainers": ["mov", "ogg", "webm"],
|
||||
"targetResolution": "720",
|
||||
"maxBitrate": "0",
|
||||
"transcodeHDR": true,
|
||||
"bframes": -1,
|
||||
"refs": 0,
|
||||
"gopSize": 0,
|
||||
|
|
12
docs/static/archived-versions.json
vendored
12
docs/static/archived-versions.json
vendored
|
@ -1,4 +1,16 @@
|
|||
[
|
||||
{
|
||||
"label": "v1.125.6",
|
||||
"url": "https://v1.125.6.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.5",
|
||||
"url": "https://v1.125.5.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.4",
|
||||
"url": "https://v1.125.4.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.3",
|
||||
"url": "https://v1.125.3.archive.immich.app"
|
||||
|
|
8
e2e/package-lock.json
generated
8
e2e/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-e2e",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"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.44",
|
||||
"version": "2.2.47",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
@ -92,7 +92,7 @@
|
|||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
|
|
@ -22,82 +22,92 @@ const user1NotShared = 'user1NotShared';
|
|||
const user2SharedUser = 'user2SharedUser';
|
||||
const user2SharedLink = 'user2SharedLink';
|
||||
const user2NotShared = 'user2NotShared';
|
||||
const user4DeletedAsset = 'user4DeletedAsset';
|
||||
const user4Empty = 'user4Empty';
|
||||
|
||||
describe('/albums', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user1: LoginResponseDto;
|
||||
let user1Asset1: AssetMediaResponseDto;
|
||||
let user1Asset2: AssetMediaResponseDto;
|
||||
let user4Asset1: AssetMediaResponseDto;
|
||||
let user1Albums: AlbumResponseDto[];
|
||||
let user2: LoginResponseDto;
|
||||
let user2Albums: AlbumResponseDto[];
|
||||
let deletedAssetAlbum: AlbumResponseDto;
|
||||
let user3: LoginResponseDto; // deleted
|
||||
let user4: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
|
||||
admin = await utils.adminSetup();
|
||||
|
||||
[user1, user2, user3] = await Promise.all([
|
||||
[user1, user2, user3, user4] = await Promise.all([
|
||||
utils.userSetup(admin.accessToken, createUserDto.user1),
|
||||
utils.userSetup(admin.accessToken, createUserDto.user2),
|
||||
utils.userSetup(admin.accessToken, createUserDto.user3),
|
||||
utils.userSetup(admin.accessToken, createUserDto.user4),
|
||||
]);
|
||||
|
||||
[user1Asset1, user1Asset2] = await Promise.all([
|
||||
[user1Asset1, user1Asset2, user4Asset1] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken, { isFavorite: true }),
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
user1Albums = await Promise.all([
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedEditorUser,
|
||||
albumUsers: [
|
||||
{ userId: admin.userId, role: AlbumUserRole.Editor },
|
||||
{ userId: user2.userId, role: AlbumUserRole.Editor },
|
||||
],
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedLink,
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1NotShared,
|
||||
assetIds: [user1Asset1.id, user1Asset2.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedViewerUser,
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
||||
assetIds: [user1Asset1.id],
|
||||
[user1Albums, user2Albums, deletedAssetAlbum] = await Promise.all([
|
||||
Promise.all([
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedEditorUser,
|
||||
albumUsers: [
|
||||
{ userId: admin.userId, role: AlbumUserRole.Editor },
|
||||
{ userId: user2.userId, role: AlbumUserRole.Editor },
|
||||
],
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedLink,
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1NotShared,
|
||||
assetIds: [user1Asset1.id, user1Asset2.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedViewerUser,
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
]),
|
||||
Promise.all([
|
||||
utils.createAlbum(user2.accessToken, {
|
||||
albumName: user2SharedUser,
|
||||
albumUsers: [
|
||||
{ userId: user1.userId, role: AlbumUserRole.Editor },
|
||||
{ userId: user3.userId, role: AlbumUserRole.Editor },
|
||||
],
|
||||
}),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
||||
]),
|
||||
utils.createAlbum(user4.accessToken, { albumName: user4DeletedAsset }),
|
||||
utils.createAlbum(user4.accessToken, { albumName: user4Empty }),
|
||||
utils.createAlbum(user3.accessToken, {
|
||||
albumName: 'Deleted',
|
||||
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
|
||||
}),
|
||||
]);
|
||||
|
||||
user2Albums = await Promise.all([
|
||||
utils.createAlbum(user2.accessToken, {
|
||||
albumName: user2SharedUser,
|
||||
albumUsers: [
|
||||
{ userId: user1.userId, role: AlbumUserRole.Editor },
|
||||
{ userId: user3.userId, role: AlbumUserRole.Editor },
|
||||
],
|
||||
}),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
||||
]);
|
||||
|
||||
await utils.createAlbum(user3.accessToken, {
|
||||
albumName: 'Deleted',
|
||||
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
|
||||
});
|
||||
|
||||
await addAssetsToAlbum(
|
||||
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
|
||||
{ headers: asBearerAuth(user1.accessToken) },
|
||||
);
|
||||
|
||||
user2Albums[0] = await getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) });
|
||||
|
||||
await Promise.all([
|
||||
addAssetsToAlbum(
|
||||
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
|
||||
{ headers: asBearerAuth(user1.accessToken) },
|
||||
),
|
||||
addAssetsToAlbum(
|
||||
{ id: deletedAssetAlbum.id, bulkIdsDto: { ids: [user4Asset1.id] } },
|
||||
{ headers: asBearerAuth(user4.accessToken) },
|
||||
),
|
||||
// add shared link to user1SharedLink album
|
||||
utils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
|
@ -110,7 +120,11 @@ describe('/albums', () => {
|
|||
}),
|
||||
]);
|
||||
|
||||
await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
||||
[user2Albums[0]] = await Promise.all([
|
||||
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
|
||||
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
utils.deleteAssets(user1.accessToken, [user4Asset1.id]),
|
||||
]);
|
||||
});
|
||||
|
||||
describe('GET /albums', () => {
|
||||
|
@ -287,6 +301,25 @@ describe('/albums', () => {
|
|||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should return empty albums and albums where all assets are deleted', async () => {
|
||||
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user4.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
ownerId: user4.userId,
|
||||
albumName: user4DeletedAsset,
|
||||
shared: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user4.userId,
|
||||
albumName: user4Empty,
|
||||
shared: false,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /albums/:id', () => {
|
||||
|
|
|
@ -701,6 +701,20 @@ describe('/asset', () => {
|
|||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should set the negative rating', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/assets/${user1Assets[0].id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ rating: -1 });
|
||||
expect(body).toMatchObject({
|
||||
id: user1Assets[0].id,
|
||||
exifInfo: expect.objectContaining({
|
||||
rating: -1,
|
||||
}),
|
||||
});
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should reject invalid rating', async () => {
|
||||
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
|
||||
const { status, body } = await request(app)
|
||||
|
|
|
@ -322,8 +322,10 @@
|
|||
"transcoding_threads_description": "Höhere Werte führen zu einer schnelleren Kodierung, lassen dem Server jedoch weniger Spielraum für die Verarbeitung anderer Aufgaben im aktiven Zustand. Dieser Wert sollte nicht höher sein als die Anzahl der CPU-Kerne. Maximiert die Auslastung, wenn der Wert auf 0 gesetzt wird.",
|
||||
"transcoding_tone_mapping": "Farbton-Mapping",
|
||||
"transcoding_tone_mapping_description": "Versucht, das Aussehen von HDR-Videos bei der Konvertierung in SDR beizubehalten. Jeder Algorithmus geht unterschiedliche Kompromisse bei Farbe, Details und Helligkeit ein. Hable bewahrt Details, Mobius bewahrt die Farbe und Reinhard bewahrt die Helligkeit.",
|
||||
"transcoding_transcode_hdr": "HDR-Videos transkodieren",
|
||||
"transcoding_transcode_hdr_setting_description": "Erzwinge die Transkodierung von HDR-Videos für optimale Kompatibilität (empfohlen)",
|
||||
"transcoding_transcode_policy": "Transcodierungsrichtlinie",
|
||||
"transcoding_transcode_policy_description": "Richtlinie, wann ein Video transkodiert werden soll. HDR-Videos werden immer transkodiert (außer wenn die Transkodierung deaktiviert ist).",
|
||||
"transcoding_transcode_policy_description": "Richtlinie, wann ein Video transkodiert werden soll. HDR-Videos werden immer transkodiert, es sei denn, diese Option wurde deaktiviert oder die Transkodierung ist insgesamt ausgeschaltet.",
|
||||
"transcoding_two_pass_encoding": "Two-Pass Codierung",
|
||||
"transcoding_two_pass_encoding_setting_description": "Führt eine Transkodierung in zwei Durchgängen durch, um besser kodierte Videos zu erzeugen. Wenn die maximale Bitrate aktiviert ist (erforderlich für die Verwendung mit H.264 und HEVC), verwendet dieser Modus einen Bitratenbereich, der auf der maximalen Bitrate basiert, und ignoriert CRF. Für VP9 kann CRF verwendet werden, wenn die maximale Bitrate deaktiviert ist.",
|
||||
"transcoding_video_codec": "Video-Codec",
|
||||
|
|
|
@ -322,8 +322,10 @@
|
|||
"transcoding_threads_description": "Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0.",
|
||||
"transcoding_tone_mapping": "Tone-mapping",
|
||||
"transcoding_tone_mapping_description": "Attempts to preserve the appearance of HDR videos when converted to SDR. Each algorithm makes different tradeoffs for color, detail and brightness. Hable preserves detail, Mobius preserves color, and Reinhard preserves brightness.",
|
||||
"transcoding_transcode_hdr": "Transcode HDR Videos",
|
||||
"transcoding_transcode_hdr_setting_description": "Force transcoding of HDR videos for optimal compatibility (recommended)",
|
||||
"transcoding_transcode_policy": "Transcode policy",
|
||||
"transcoding_transcode_policy_description": "Policy for when a video should be transcoded. HDR videos will always be transcoded (except if transcoding is disabled).",
|
||||
"transcoding_transcode_policy_description": "Policy for when a video should be transcoded. HDR videos will always be transcoded unless disabled below or transcoding is disabled in general.",
|
||||
"transcoding_two_pass_encoding": "Two-pass encoding",
|
||||
"transcoding_two_pass_encoding_setting_description": "Transcode in two passes to produce better encoded videos. When max bitrate is enabled (required for it to work with H.264 and HEVC), this mode uses a bitrate range based on the max bitrate and ignores CRF. For VP9, CRF can be used if max bitrate is disabled.",
|
||||
"transcoding_video_codec": "Video codec",
|
||||
|
@ -1350,4 +1352,4 @@
|
|||
"yes": "Yes",
|
||||
"you_dont_have_any_shared_links": "You don't have any shared links",
|
||||
"zoom_image": "Zoom Image"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.125.3"
|
||||
version = "1.125.6"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
|
|
@ -35,8 +35,8 @@ platform :android do
|
|||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 179,
|
||||
"android.injected.version.name" => "1.125.3",
|
||||
"android.injected.version.code" => 182,
|
||||
"android.injected.version.name" => "1.125.6",
|
||||
}
|
||||
)
|
||||
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')
|
||||
|
|
|
@ -19,7 +19,7 @@ platform :ios do
|
|||
desc "iOS Release"
|
||||
lane :release do
|
||||
increment_version_number(
|
||||
version_number: "1.125.3"
|
||||
version_number: "1.125.6"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
|
|
@ -277,7 +277,6 @@ class SearchPage extends HookConsumerWidget {
|
|||
fieldEndHintText: 'end_date'.tr(),
|
||||
initialEntryMode: DatePickerEntryMode.calendar,
|
||||
keyboardType: TextInputType.text,
|
||||
locale: context.locale,
|
||||
);
|
||||
|
||||
if (date == null) {
|
||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
BIN
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/update_asset_dto.dart
generated
BIN
mobile/openapi/lib/model/update_asset_dto.dart
generated
Binary file not shown.
|
@ -2,7 +2,7 @@ name: immich_mobile
|
|||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 1.125.3+179
|
||||
version: 1.125.6+182
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
|
|
@ -7454,7 +7454,7 @@
|
|||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
|
@ -7951,7 +7951,7 @@
|
|||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": 0,
|
||||
"minimum": -1,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
|
@ -11863,6 +11863,9 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"transcodeHDR": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"twoPass": {
|
||||
"type": "boolean"
|
||||
}
|
||||
|
@ -11888,6 +11891,7 @@
|
|||
"threads",
|
||||
"tonemap",
|
||||
"transcode",
|
||||
"transcodeHDR",
|
||||
"twoPass"
|
||||
],
|
||||
"type": "object"
|
||||
|
@ -12780,7 +12784,7 @@
|
|||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": 0,
|
||||
"minimum": -1,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
|
|
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Immich
|
||||
* 1.125.3
|
||||
* 1.125.6
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
|
|
4
server/package-lock.json
generated
4
server/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "immich",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@nestjs/bullmq": "^11.0.0",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "immich",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
|
|
@ -33,6 +33,7 @@ export interface SystemConfig {
|
|||
acceptedContainers: VideoContainer[];
|
||||
targetResolution: string;
|
||||
maxBitrate: string;
|
||||
transcodeHDR: boolean;
|
||||
bframes: number;
|
||||
refs: number;
|
||||
gopSize: number;
|
||||
|
@ -182,6 +183,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||
acceptedContainers: [VideoContainer.MOV, VideoContainer.OGG, VideoContainer.WEBM],
|
||||
targetResolution: '720',
|
||||
maxBitrate: '0',
|
||||
transcodeHDR: true,
|
||||
bframes: -1,
|
||||
refs: 0,
|
||||
gopSize: 0,
|
||||
|
|
|
@ -52,7 +52,7 @@ export class UpdateAssetBase {
|
|||
@Optional()
|
||||
@IsInt()
|
||||
@Max(5)
|
||||
@Min(0)
|
||||
@Min(-1)
|
||||
rating?: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -106,6 +106,9 @@ export class SystemConfigFFmpegDto {
|
|||
@IsString()
|
||||
maxBitrate!: string;
|
||||
|
||||
@ValidateBoolean()
|
||||
transcodeHDR!: boolean;
|
||||
|
||||
@IsInt()
|
||||
@Min(-1)
|
||||
@Max(16)
|
||||
|
|
|
@ -193,7 +193,7 @@ export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
|||
export function withSmartSearch<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||
return qb
|
||||
.leftJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||
.select(sql<number[]>`smart_search.embedding`.as('embedding'));
|
||||
.select((eb) => eb.fn.toJson(eb.table('smart_search')).as('smartSearch'));
|
||||
}
|
||||
|
||||
export function withFaces(eb: ExpressionBuilder<DB, 'assets'>) {
|
||||
|
|
|
@ -207,8 +207,8 @@ select
|
|||
count("assets"."id")::int as "assetCount"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id"
|
||||
left join "assets" on "assets"."id" = "album_assets"."assetsId"
|
||||
inner join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id"
|
||||
inner join "assets" on "assets"."id" = "album_assets"."assetsId"
|
||||
where
|
||||
"albums"."id" in ($1)
|
||||
and "assets"."deletedAt" is null
|
||||
|
|
|
@ -124,8 +124,8 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id')
|
||||
.leftJoin('assets', 'assets.id', 'album_assets.assetsId')
|
||||
.innerJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id')
|
||||
.innerJoin('assets', 'assets.id', 'album_assets.assetsId')
|
||||
.select('albums.id as albumId')
|
||||
.select((eb) => eb.fn.min('assets.fileCreatedAt').as('startDate'))
|
||||
.select((eb) => eb.fn.max('assets.fileCreatedAt').as('endDate'))
|
||||
|
|
|
@ -495,7 +495,6 @@ export class AssetRepository implements IAssetRepository {
|
|||
.$if(property === WithoutProperty.THUMBNAIL, (qb) =>
|
||||
qb
|
||||
.innerJoin('asset_job_status as job_status', 'assetId', 'assets.id')
|
||||
.select(withFiles)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
|
|
|
@ -100,7 +100,6 @@ export class PersonRepository implements IPersonRepository {
|
|||
.$if(!!options.personId, (qb) => qb.where('asset_faces.personId', '=', options.personId!))
|
||||
.$if(!!options.sourceType, (qb) => qb.where('asset_faces.sourceType', '=', options.sourceType!))
|
||||
.$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!))
|
||||
.$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!))
|
||||
.stream() as AsyncIterableIterator<AssetFaceEntity>;
|
||||
}
|
||||
|
||||
|
@ -109,7 +108,7 @@ export class PersonRepository implements IPersonRepository {
|
|||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.$if(!!options.ownerId, (qb) => qb.where('person.ownerId', '=', options.ownerId!))
|
||||
.$if(!!options.thumbnailPath, (qb) => qb.where('person.thumbnailPath', '=', options.thumbnailPath!))
|
||||
.$if(options.thumbnailPath !== undefined, (qb) => qb.where('person.thumbnailPath', '=', options.thumbnailPath!))
|
||||
.$if(options.faceAssetId === null, (qb) => qb.where('person.faceAssetId', 'is', null))
|
||||
.$if(!!options.faceAssetId, (qb) => qb.where('person.faceAssetId', '=', options.faceAssetId!))
|
||||
.$if(options.isHidden !== undefined, (qb) => qb.where('person.isHidden', '=', options.isHidden!))
|
||||
|
|
|
@ -64,9 +64,9 @@ export class AlbumService extends BaseService {
|
|||
return {
|
||||
...mapAlbumWithoutAssets(album),
|
||||
sharedLinks: undefined,
|
||||
startDate: albumMetadata[album.id].startDate ?? undefined,
|
||||
endDate: albumMetadata[album.id].endDate ?? undefined,
|
||||
assetCount: albumMetadata[album.id].assetCount,
|
||||
startDate: albumMetadata[album.id]?.startDate ?? undefined,
|
||||
endDate: albumMetadata[album.id]?.endDate ?? undefined,
|
||||
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
|
||||
lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt,
|
||||
};
|
||||
}),
|
||||
|
@ -83,9 +83,9 @@ export class AlbumService extends BaseService {
|
|||
|
||||
return {
|
||||
...mapAlbum(album, withAssets, auth),
|
||||
startDate: albumMetadataForIds.startDate ?? undefined,
|
||||
endDate: albumMetadataForIds.endDate ?? undefined,
|
||||
assetCount: albumMetadataForIds.assetCount,
|
||||
startDate: albumMetadataForIds?.startDate ?? undefined,
|
||||
endDate: albumMetadataForIds?.endDate ?? undefined,
|
||||
assetCount: albumMetadataForIds?.assetCount ?? 0,
|
||||
lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2313,9 +2313,9 @@ describe(MediaService.name, () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should tonemap when policy is required and video is hdr', async () => {
|
||||
it('should tonemap when policy is required, video is hdr and transcodeHDR is enabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED, transcodeHDR: true } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -2333,9 +2333,9 @@ describe(MediaService.name, () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should tonemap when policy is optimal and video is hdr', async () => {
|
||||
it('should tonemap when policy is optimal, video is hdr and transcodeHDR is enabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL, transcodeHDR: true } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -2353,9 +2353,9 @@ describe(MediaService.name, () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should transcode when policy is required and video is not yuv420p', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
|
||||
it('should transcode when policy is required and pixelformat is not supported', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamYuv444p);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED, transcodeHDR: true } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
|
|
@ -194,7 +194,7 @@ export class MediaService extends BaseService {
|
|||
await Promise.all(pathsToDelete.map((path) => this.storageRepository.unlink(path)));
|
||||
}
|
||||
|
||||
if (asset.thumbhash != generated.thumbhash) {
|
||||
if (!asset.thumbhash || Buffer.compare(asset.thumbhash, generated.thumbhash) !== 0) {
|
||||
await this.assetRepository.update({ id: asset.id, thumbhash: generated.thumbhash });
|
||||
}
|
||||
|
||||
|
@ -432,9 +432,12 @@ export class MediaService extends BaseService {
|
|||
const targetRes = Number.parseInt(ffmpegConfig.targetResolution);
|
||||
const isLargerThanTargetRes = scalingEnabled && Math.min(stream.height, stream.width) > targetRes;
|
||||
const isLargerThanTargetBitrate = stream.bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate);
|
||||
const supportedPixelFormats: string[] = ['yuv420p', 'yuvj420p', 'yuva420p', 'yuv420p10le'];
|
||||
|
||||
const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(stream.codecName as VideoCodec);
|
||||
const isRequired = !isTargetVideoCodec || !stream.pixelFormat.endsWith('420p');
|
||||
const isTargetDynamicRange = !ffmpegConfig.transcodeHDR || !stream.isHDR;
|
||||
const isRequired =
|
||||
!isTargetVideoCodec || !isTargetDynamicRange || !supportedPixelFormats.includes(stream.pixelFormat);
|
||||
|
||||
switch (ffmpegConfig.transcode) {
|
||||
case TranscodePolicy.DISABLED: {
|
||||
|
|
|
@ -1162,6 +1162,17 @@ describe(MetadataService.name, () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
it('should handle valid negative rating value', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
mockReadTags({ Rating: -1 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rating: -1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueueSidecar', () => {
|
||||
|
|
|
@ -204,7 +204,7 @@ export class MetadataService extends BaseService {
|
|||
// comments
|
||||
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
|
||||
profileDescription: exifTags.ProfileDescription || null,
|
||||
rating: validateRange(exifTags.Rating, 0, 5),
|
||||
rating: validateRange(exifTags.Rating, -1, 5),
|
||||
|
||||
// grouping
|
||||
livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null,
|
||||
|
|
|
@ -60,6 +60,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||
acceptedVideoCodecs: [VideoCodec.H264],
|
||||
acceptedContainers: [VideoContainer.MOV, VideoContainer.OGG, VideoContainer.WEBM],
|
||||
maxBitrate: '0',
|
||||
transcodeHDR: true,
|
||||
bframes: -1,
|
||||
refs: 0,
|
||||
gopSize: 0,
|
||||
|
|
|
@ -159,9 +159,13 @@ export class BaseConfig implements VideoCodecSWConfig {
|
|||
options.push(`scale=${this.getScaling(videoStream)}`);
|
||||
}
|
||||
|
||||
options.push(...this.getToneMapping(videoStream));
|
||||
if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) {
|
||||
options.push(`format=yuv420p`);
|
||||
if (!this.config.transcodeHDR && videoStream.isHDR) {
|
||||
options.push(`format=yuv420p10le`);
|
||||
} else {
|
||||
options.push(...this.getToneMapping(videoStream));
|
||||
if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) {
|
||||
options.push(`format=yuv420p`);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
|
|
16
server/test/fixtures/media.stub.ts
vendored
16
server/test/fixtures/media.stub.ts
vendored
|
@ -182,6 +182,22 @@ export const probeStub = {
|
|||
},
|
||||
],
|
||||
}),
|
||||
videoStreamYuv444p: Object.freeze<VideoInfo>({
|
||||
...probeStubDefault,
|
||||
videoStreams: [
|
||||
{
|
||||
index: 0,
|
||||
height: 480,
|
||||
width: 480,
|
||||
codecName: 'h264',
|
||||
frameCount: 100,
|
||||
rotation: 0,
|
||||
isHDR: false,
|
||||
bitrate: 0,
|
||||
pixelFormat: 'yuv444p',
|
||||
},
|
||||
],
|
||||
}),
|
||||
audioStreamAac: Object.freeze<VideoInfo>({
|
||||
...probeStubDefault,
|
||||
audioStreams: [{ index: 1, codecName: 'aac', frameCount: 100 }],
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Mocked, vitest } from 'vitest';
|
|||
export const newMediaRepositoryMock = (): Mocked<IMediaRepository> => {
|
||||
return {
|
||||
generateThumbnail: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||
generateThumbhash: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||
generateThumbhash: vitest.fn().mockResolvedValue(Buffer.from('')),
|
||||
decodeImage: vitest.fn().mockResolvedValue({ data: Buffer.from(''), info: {} }),
|
||||
extract: vitest.fn().mockResolvedValue(false),
|
||||
probe: vitest.fn(),
|
||||
|
|
6
web/package-lock.json
generated
6
web/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-web",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
|
@ -75,7 +75,7 @@
|
|||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.125.3",
|
||||
"version": "1.125.6",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"scripts": {
|
||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||
|
|
|
@ -151,6 +151,14 @@
|
|||
sortBy(savedConfig.ffmpeg.acceptedContainers),
|
||||
)}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.transcoding_transcode_hdr')}
|
||||
{disabled}
|
||||
subtitle={$t('admin.transcoding_transcode_hdr_setting_description')}
|
||||
bind:checked={config.ffmpeg.transcodeHDR}
|
||||
isEdited={config.ffmpeg.transcodeHDR !== savedConfig.ffmpeg.transcodeHDR}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
|
|
|
@ -157,7 +157,6 @@ async function fileUploader(
|
|||
}
|
||||
} catch (error) {
|
||||
console.error(`Error calculating sha1 file=${assetFile.name})`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue