Compare commits

..

5 Commits

Author SHA1 Message Date
renovate[bot] ca877a004f fix(deps): update dependency permission_handler to v12 2026-05-11 14:28:27 +00:00
bo0tzz b4f719653f fix: indentation and typo (#28349)
* fix: indentation and typo

* neline
2026-05-11 09:17:39 -05:00
bo0tzz f370b4bac6 chore: fold apk comment qr in <details> (#28348) 2026-05-11 09:40:19 -04:00
Mert d788169bf3 chore(server)!: remove libopus enum (#28325) 2026-05-11 08:02:57 -04:00
Mert eea820fa2f chore(server): enable hw decoding by default (#28324) 2026-05-11 08:02:26 -04:00
18 changed files with 46 additions and 103 deletions
+4 -1
View File
@@ -175,7 +175,10 @@ jobs:
Download: ${{ env.APK_URL }}
<img src="https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=${{ env.APK_URL }}" alt="QR code" />
<details>
<summary>QR code</summary>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=${{ env.APK_URL }}" alt="QR code" />
</details>
Installs as a separate app (applicationId `app.alextran.immich.pr${{ github.event.pull_request.number }}`), so it coexists with the Play Store version and any other PR builds.
+1 -1
View File
@@ -26,7 +26,7 @@ The default configuration looks like this:
},
"ffmpeg": {
"accel": "disabled",
"accelDecode": false,
"accelDecode": true,
"acceptedAudioCodecs": ["aac", "mp3", "opus"],
"acceptedContainers": ["mov", "ogg", "webm"],
"acceptedVideoCodecs": ["h264"],
-3
View File
@@ -25,7 +25,6 @@ class AudioCodec {
static const mp3 = AudioCodec._(r'mp3');
static const aac = AudioCodec._(r'aac');
static const libopus = AudioCodec._(r'libopus');
static const opus = AudioCodec._(r'opus');
static const pcmS16le = AudioCodec._(r'pcm_s16le');
@@ -33,7 +32,6 @@ class AudioCodec {
static const values = <AudioCodec>[
mp3,
aac,
libopus,
opus,
pcmS16le,
];
@@ -76,7 +74,6 @@ class AudioCodecTypeTransformer {
switch (data) {
case r'mp3': return AudioCodec.mp3;
case r'aac': return AudioCodec.aac;
case r'libopus': return AudioCodec.libopus;
case r'opus': return AudioCodec.opus;
case r'pcm_s16le': return AudioCodec.pcmS16le;
default:
+1 -1
View File
@@ -56,7 +56,7 @@ dependencies:
path: ^1.9.1
path_provider: ^2.1.5
path_provider_foundation: ^2.6.0
permission_handler: ^11.4.0
permission_handler: ^12.0.0
photo_manager: ^3.9.0
pinput: ^5.0.2
punycode: ^1.0.0
-1
View File
@@ -16929,7 +16929,6 @@
"enum": [
"mp3",
"aac",
"libopus",
"opus",
"pcm_s16le"
],
@@ -7241,7 +7241,6 @@ export enum TranscodeHWAccel {
export enum AudioCodec {
Mp3 = "mp3",
Aac = "aac",
Libopus = "libopus",
Opus = "opus",
PcmS16Le = "pcm_s16le"
}
+6 -18
View File
@@ -480,11 +480,11 @@ importers:
specifier: ^9.0.2
version: 9.0.3
kysely:
specifier: 0.29.0
version: 0.29.0
specifier: 0.28.16
version: 0.28.16
kysely-postgres-js:
specifier: ^3.0.0
version: 3.0.0(kysely@0.29.0)(postgres@3.4.9)
version: 3.0.0(kysely@0.28.16)(postgres@3.4.9)
lodash:
specifier: ^4.17.21
version: 4.18.1
@@ -505,7 +505,7 @@ importers:
version: 6.2.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)
nestjs-kysely:
specifier: 3.1.2
version: 3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.29.0)(reflect-metadata@0.2.2)
version: 3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.28.16)(reflect-metadata@0.2.2)
nestjs-otel:
specifier: ^8.0.0
version: 8.0.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)
@@ -8727,10 +8727,6 @@ packages:
resolution: {integrity: sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww==}
engines: {node: '>=20.0.0'}
kysely@0.29.0:
resolution: {integrity: sha512-LrQfPUeTW7MXbMvT62moEMnpMTuj9TO3lqjCeLKjM975PJ4Alrl/43f2tlDX7xOsNptKgH4LSNGwIbXwEkLg4g==}
engines: {node: '>=22.0.0'}
langium@3.3.1:
resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==}
engines: {node: '>=16.0.0'}
@@ -21740,16 +21736,8 @@ snapshots:
optionalDependencies:
postgres: 3.4.9
kysely-postgres-js@3.0.0(kysely@0.29.0)(postgres@3.4.9):
dependencies:
kysely: 0.29.0
optionalDependencies:
postgres: 3.4.9
kysely@0.28.16: {}
kysely@0.29.0: {}
langium@3.3.1:
dependencies:
chevrotain: 11.0.3
@@ -22821,11 +22809,11 @@ snapshots:
reflect-metadata: 0.2.2
rxjs: 7.8.2
nestjs-kysely@3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.29.0)(reflect-metadata@0.2.2):
nestjs-kysely@3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.28.16)(reflect-metadata@0.2.2):
dependencies:
'@nestjs/common': 11.1.19(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)
kysely: 0.29.0
kysely: 0.28.16
reflect-metadata: 0.2.2
tslib: 2.8.1
+1 -1
View File
@@ -84,7 +84,7 @@
"jose": "^6.0.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"kysely": "0.29.0",
"kysely": "0.28.16",
"kysely-postgres-js": "^3.0.0",
"lodash": "^4.17.21",
"luxon": "^3.4.2",
+1 -1
View File
@@ -223,7 +223,7 @@ export const defaults = Object.freeze<SystemConfig>({
transcode: TranscodePolicy.Required,
tonemap: ToneMapping.Hable,
accel: TranscodeHardwareAcceleration.Disabled,
accelDecode: false,
accelDecode: true,
},
job: {
[QueueName.BackgroundTask]: { concurrency: 5 },
-1
View File
@@ -199,7 +199,6 @@ export const endpointTags: Record<ApiTag, string> = {
export const AUDIO_ENCODER: Record<AudioCodec, string> = {
[AudioCodec.Aac]: 'aac',
[AudioCodec.Mp3]: 'mp3',
[AudioCodec.Libopus]: 'libopus',
[AudioCodec.Opus]: 'libopus',
[AudioCodec.PcmS16le]: 'pcm_s16le',
};
+1 -5
View File
@@ -7,7 +7,6 @@ import {
OcrConfigSchema,
} from 'src/dtos/model-config.dto';
import {
AudioCodec,
AudioCodecSchema,
ColorspaceSchema,
CQModeSchema,
@@ -65,10 +64,7 @@ const SystemConfigFFmpegSchema = z
targetVideoCodec: VideoCodecSchema,
acceptedVideoCodecs: z.array(VideoCodecSchema).describe('Accepted video codecs'),
targetAudioCodec: AudioCodecSchema,
acceptedAudioCodecs: z
.array(AudioCodecSchema)
.transform((value): AudioCodec[] => value.map((v) => (v === AudioCodec.Libopus ? AudioCodec.Opus : v)))
.describe('Accepted audio codecs'),
acceptedAudioCodecs: z.array(AudioCodecSchema).describe('Accepted audio codecs'),
acceptedContainers: z.array(VideoContainerSchema).describe('Accepted containers'),
targetResolution: z.string().describe('Target resolution'),
maxBitrate: z.string().describe('Max bitrate'),
-2
View File
@@ -454,8 +454,6 @@ export enum VideoSegmentCodec {
export enum AudioCodec {
Mp3 = 'mp3',
Aac = 'aac',
/** @deprecated Use `Opus` instead */
Libopus = 'libopus',
Opus = 'opus',
PcmS16le = 'pcm_s16le',
}
@@ -765,7 +765,6 @@ export class AssetRepository {
getTimeBucket(timeBucket: string, options: TimeBucketOptions, auth: AuthDto) {
const order = options.order ?? 'desc';
const query = this.db
.$pickTables<'asset' | 'asset_exif' | 'album_asset' | 'stack'>()
.with('cte', (qb) =>
qb
.selectFrom('asset')
@@ -1,8 +1,7 @@
import { schemaDiff, schemaFromCode, schemaFromDatabase } from '@immich/sql-tools';
import { Injectable } from '@nestjs/common';
import AsyncLock from 'async-lock';
import { Kysely, sql } from 'kysely';
import { FileMigrationProvider, Migrator } from 'kysely/migration';
import { FileMigrationProvider, Kysely, Migrator, sql } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { readdir } from 'node:fs/promises';
import { join } from 'node:path';
@@ -41,7 +41,7 @@ export class NotificationTable {
type!: Generated<NotificationType>;
@Column({ type: 'jsonb', nullable: true })
data!: unknown | null;
data!: any | null;
@Column()
title!: string;
+28 -61
View File
@@ -2698,9 +2698,11 @@ describe(MediaService.name, () => {
expect(mocks.media.transcode).not.toHaveBeenCalled();
});
it('should set options for nvenc', async () => {
it('should set options for nvenc sw decode', async () => {
mocks.assetJob.getForVideoConversion.mockResolvedValue({ ...asset, ...probeStub.matroskaContainer });
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc } });
mocks.systemMetadata.get.mockResolvedValue({
ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: false },
});
await sut.handleVideoConversion({ id: 'video-id' });
expect(mocks.media.transcode).toHaveBeenCalledWith(
'/original/path.ext',
@@ -2758,7 +2760,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device', 'cuda=cuda:0', '-filter_hw_device', 'cuda']),
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining([expect.stringContaining('-multipass')]),
twoPass: false,
}),
@@ -2775,7 +2777,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device', 'cuda=cuda:0', '-filter_hw_device', 'cuda']),
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-cq:v', '23', '-maxrate', '10000k', '-bufsize', '6897k']),
twoPass: false,
}),
@@ -2792,7 +2794,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device', 'cuda=cuda:0', '-filter_hw_device', 'cuda']),
inputOptions: expect.any(Array),
outputOptions: expect.not.stringContaining('-maxrate'),
twoPass: false,
}),
@@ -2809,7 +2811,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device', 'cuda=cuda:0', '-filter_hw_device', 'cuda']),
inputOptions: expect.any(Array),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]),
twoPass: false,
}),
@@ -2824,7 +2826,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device', 'cuda=cuda:0', '-filter_hw_device', 'cuda']),
inputOptions: expect.any(Array),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-multipass')]),
twoPass: false,
}),
@@ -2894,10 +2896,10 @@ describe(MediaService.name, () => {
);
});
it('should set options for qsv', async () => {
it('should set options for qsv with sw decode', async () => {
mocks.assetJob.getForVideoConversion.mockResolvedValue({ ...asset, ...probeStub.matroskaContainer });
mocks.systemMetadata.get.mockResolvedValue({
ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, maxBitrate: '10000k' },
ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, maxBitrate: '10000k', accelDecode: false },
});
await sut.handleVideoConversion({ id: 'video-id' });
expect(mocks.media.transcode).toHaveBeenCalledWith(
@@ -2947,13 +2949,14 @@ describe(MediaService.name, () => {
);
});
it('should set options for qsv with custom dri node', async () => {
it('should set options for qsv with custom dri node with sw decode', async () => {
mocks.assetJob.getForVideoConversion.mockResolvedValue({ ...asset, ...probeStub.matroskaContainer });
mocks.systemMetadata.get.mockResolvedValue({
ffmpeg: {
accel: TranscodeHardwareAcceleration.Qsv,
maxBitrate: '10000k',
preferredHwDevice: '/dev/dri/renderD128',
accelDecode: false,
},
});
await sut.handleVideoConversion({ id: 'video-id' });
@@ -2983,12 +2986,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device',
'qsv=hw,child_device=/dev/dri/renderD128',
'-filter_hw_device',
'hw',
]),
inputOptions: expect.any(Array),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]),
twoPass: false,
}),
@@ -3005,12 +3003,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device',
'qsv=hw,child_device=/dev/dri/renderD128',
'-filter_hw_device',
'hw',
]),
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-low_power', '1']),
twoPass: false,
}),
@@ -3036,12 +3029,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device',
'qsv=hw,child_device=/dev/dri/renderD129',
'-filter_hw_device',
'hw',
]),
inputOptions: expect.arrayContaining(['-qsv_device', '/dev/dri/renderD129']),
outputOptions: expect.arrayContaining(['-c:v', 'h264_qsv']),
twoPass: false,
}),
@@ -3158,9 +3146,11 @@ describe(MediaService.name, () => {
);
});
it('should set options for vaapi', async () => {
it('should set options for sw decode vaapi', async () => {
mocks.assetJob.getForVideoConversion.mockResolvedValue({ ...asset, ...probeStub.matroskaContainer });
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } });
mocks.systemMetadata.get.mockResolvedValue({
ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: false },
});
await sut.handleVideoConversion({ id: 'video-id' });
expect(mocks.media.transcode).toHaveBeenCalledWith(
'/original/path.ext',
@@ -3211,12 +3201,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device',
'vaapi=accel:/dev/dri/renderD128',
'-filter_hw_device',
'accel',
]),
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining([
'-c:v',
'h264_vaapi',
@@ -3242,12 +3227,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device',
'vaapi=accel:/dev/dri/renderD128',
'-filter_hw_device',
'accel',
]),
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining([
'-c:v',
'h264_vaapi',
@@ -3275,12 +3255,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device',
'vaapi=accel:/dev/dri/renderD128',
'-filter_hw_device',
'accel',
]),
inputOptions: expect.any(Array),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-compression_level')]),
twoPass: false,
}),
@@ -3296,12 +3271,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device',
'vaapi=accel:/dev/dri/renderD129',
'-filter_hw_device',
'accel',
]),
inputOptions: expect.arrayContaining(['-hwaccel_device', '/dev/dri/renderD129']),
outputOptions: expect.arrayContaining(['-c:v', 'h264_vaapi']),
twoPass: false,
}),
@@ -3319,12 +3289,7 @@ describe(MediaService.name, () => {
'/original/path.ext',
expect.any(String),
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device',
'vaapi=accel:/dev/dri/renderD128',
'-filter_hw_device',
'accel',
]),
inputOptions: expect.arrayContaining(['-hwaccel_device', '/dev/dri/renderD128']),
outputOptions: expect.arrayContaining(['-c:v', 'h264_vaapi']),
twoPass: false,
}),
@@ -3481,7 +3446,9 @@ describe(MediaService.name, () => {
it('should fallback to sw transcoding if hw transcoding fails and hw decoding is disabled', async () => {
mocks.assetJob.getForVideoConversion.mockResolvedValue({ ...asset, ...probeStub.matroskaContainer });
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } });
mocks.systemMetadata.get.mockResolvedValue({
ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: false },
});
mocks.media.transcode.mockRejectedValueOnce(new Error('error'));
await sut.handleVideoConversion({ id: 'video-id' });
expect(mocks.media.transcode).toHaveBeenCalledTimes(2);
@@ -70,7 +70,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
preferredHwDevice: 'auto',
transcode: TranscodePolicy.Required,
accel: TranscodeHardwareAcceleration.Disabled,
accelDecode: false,
accelDecode: true,
tonemap: ToneMapping.Hable,
},
logging: {
-1
View File
@@ -373,7 +373,6 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
const visibility = options.visibility == null ? AssetVisibility.Timeline : options.visibility;
return kysely
.$pickTables<'asset' | 'tag_asset' | 'asset_exif' | 'asset_file' | 'album_asset' | 'ocr_search' | 'smart_search'>()
.withPlugin(joinDeduplicationPlugin)
.selectFrom('asset')
.where('asset.visibility', '=', visibility)