瀏覽代碼

Merge branch 'main' of github.com:immich-app/immich

Alex Tran 2 年之前
父節點
當前提交
0c61521521

+ 3 - 2
.github/workflows/prepare-release.yml

@@ -41,8 +41,9 @@ jobs:
         id: push-tag
         uses: EndBug/add-and-commit@v9
         with:
-          author_name: Immich Release Bot
-          author_email: bot@immich.app
+          author_name: Alex The Bot
+          author_email: alex.tran1502@gmail.com
+          default_author: user_info 
           message: "Version ${{ env.IMMICH_VERSION }}"
           tag: ${{ env.IMMICH_VERSION }}
           push: true

+ 1 - 1
mobile/openapi/doc/SystemConfigFFmpegDto.md

@@ -13,7 +13,7 @@ Name | Type | Description | Notes
 **targetVideoCodec** | **String** |  | 
 **targetAudioCodec** | **String** |  | 
 **targetScaling** | **String** |  | 
-**transcodeAll** | **bool** |  | 
+**transcode** | **String** |  | 
 
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 

+ 85 - 8
mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart

@@ -18,7 +18,7 @@ class SystemConfigFFmpegDto {
     required this.targetVideoCodec,
     required this.targetAudioCodec,
     required this.targetScaling,
-    required this.transcodeAll,
+    required this.transcode,
   });
 
   String crf;
@@ -31,7 +31,7 @@ class SystemConfigFFmpegDto {
 
   String targetScaling;
 
-  bool transcodeAll;
+  SystemConfigFFmpegDtoTranscodeEnum transcode;
 
   @override
   bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegDto &&
@@ -40,7 +40,7 @@ class SystemConfigFFmpegDto {
      other.targetVideoCodec == targetVideoCodec &&
      other.targetAudioCodec == targetAudioCodec &&
      other.targetScaling == targetScaling &&
-     other.transcodeAll == transcodeAll;
+     other.transcode == transcode;
 
   @override
   int get hashCode =>
@@ -50,10 +50,10 @@ class SystemConfigFFmpegDto {
     (targetVideoCodec.hashCode) +
     (targetAudioCodec.hashCode) +
     (targetScaling.hashCode) +
-    (transcodeAll.hashCode);
+    (transcode.hashCode);
 
   @override
-  String toString() => 'SystemConfigFFmpegDto[crf=$crf, preset=$preset, targetVideoCodec=$targetVideoCodec, targetAudioCodec=$targetAudioCodec, targetScaling=$targetScaling, transcodeAll=$transcodeAll]';
+  String toString() => 'SystemConfigFFmpegDto[crf=$crf, preset=$preset, targetVideoCodec=$targetVideoCodec, targetAudioCodec=$targetAudioCodec, targetScaling=$targetScaling, transcode=$transcode]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -62,7 +62,7 @@ class SystemConfigFFmpegDto {
       json[r'targetVideoCodec'] = this.targetVideoCodec;
       json[r'targetAudioCodec'] = this.targetAudioCodec;
       json[r'targetScaling'] = this.targetScaling;
-      json[r'transcodeAll'] = this.transcodeAll;
+      json[r'transcode'] = this.transcode;
     return json;
   }
 
@@ -90,7 +90,7 @@ class SystemConfigFFmpegDto {
         targetVideoCodec: mapValueOfType<String>(json, r'targetVideoCodec')!,
         targetAudioCodec: mapValueOfType<String>(json, r'targetAudioCodec')!,
         targetScaling: mapValueOfType<String>(json, r'targetScaling')!,
-        transcodeAll: mapValueOfType<bool>(json, r'transcodeAll')!,
+        transcode: SystemConfigFFmpegDtoTranscodeEnum.fromJson(json[r'transcode'])!,
       );
     }
     return null;
@@ -145,7 +145,84 @@ class SystemConfigFFmpegDto {
     'targetVideoCodec',
     'targetAudioCodec',
     'targetScaling',
-    'transcodeAll',
+    'transcode',
   };
 }
 
+
+class SystemConfigFFmpegDtoTranscodeEnum {
+  /// Instantiate a new enum with the provided [value].
+  const SystemConfigFFmpegDtoTranscodeEnum._(this.value);
+
+  /// The underlying value of this enum member.
+  final String value;
+
+  @override
+  String toString() => value;
+
+  String toJson() => value;
+
+  static const all = SystemConfigFFmpegDtoTranscodeEnum._(r'all');
+  static const optimal = SystemConfigFFmpegDtoTranscodeEnum._(r'optimal');
+  static const required_ = SystemConfigFFmpegDtoTranscodeEnum._(r'required');
+
+  /// List of all possible values in this [enum][SystemConfigFFmpegDtoTranscodeEnum].
+  static const values = <SystemConfigFFmpegDtoTranscodeEnum>[
+    all,
+    optimal,
+    required_,
+  ];
+
+  static SystemConfigFFmpegDtoTranscodeEnum? fromJson(dynamic value) => SystemConfigFFmpegDtoTranscodeEnumTypeTransformer().decode(value);
+
+  static List<SystemConfigFFmpegDtoTranscodeEnum>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <SystemConfigFFmpegDtoTranscodeEnum>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = SystemConfigFFmpegDtoTranscodeEnum.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+}
+
+/// Transformation class that can [encode] an instance of [SystemConfigFFmpegDtoTranscodeEnum] to String,
+/// and [decode] dynamic data back to [SystemConfigFFmpegDtoTranscodeEnum].
+class SystemConfigFFmpegDtoTranscodeEnumTypeTransformer {
+  factory SystemConfigFFmpegDtoTranscodeEnumTypeTransformer() => _instance ??= const SystemConfigFFmpegDtoTranscodeEnumTypeTransformer._();
+
+  const SystemConfigFFmpegDtoTranscodeEnumTypeTransformer._();
+
+  String encode(SystemConfigFFmpegDtoTranscodeEnum data) => data.value;
+
+  /// Decodes a [dynamic value][data] to a SystemConfigFFmpegDtoTranscodeEnum.
+  ///
+  /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
+  /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
+  /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
+  ///
+  /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
+  /// and users are still using an old app with the old code.
+  SystemConfigFFmpegDtoTranscodeEnum? decode(dynamic data, {bool allowNull = true}) {
+    if (data != null) {
+      switch (data.toString()) {
+        case r'all': return SystemConfigFFmpegDtoTranscodeEnum.all;
+        case r'optimal': return SystemConfigFFmpegDtoTranscodeEnum.optimal;
+        case r'required': return SystemConfigFFmpegDtoTranscodeEnum.required_;
+        default:
+          if (!allowNull) {
+            throw ArgumentError('Unknown enum value to decode: $data');
+          }
+      }
+    }
+    return null;
+  }
+
+  /// Singleton [SystemConfigFFmpegDtoTranscodeEnumTypeTransformer] instance.
+  static SystemConfigFFmpegDtoTranscodeEnumTypeTransformer? _instance;
+}
+
+

+ 2 - 2
mobile/openapi/test/system_config_f_fmpeg_dto_test.dart

@@ -41,8 +41,8 @@ void main() {
       // TODO
     });
 
-    // bool transcodeAll
-    test('to test the property `transcodeAll`', () async {
+    // String transcode
+    test('to test the property `transcode`', () async {
       // TODO
     });
 

+ 35 - 6
server/apps/microservices/src/processors/video-transcode.processor.ts

@@ -8,10 +8,11 @@ import {
   QueueName,
   StorageCore,
   StorageFolder,
+  SystemConfigFFmpegDto,
   SystemConfigService,
   WithoutProperty,
 } from '@app/domain';
-import { AssetEntity, AssetType } from '@app/infra/db/entities';
+import { AssetEntity, AssetType, TranscodePreset } from '@app/infra/db/entities';
 import { Process, Processor } from '@nestjs/bull';
 import { Inject, Logger } from '@nestjs/common';
 import { Job } from 'bull';
@@ -74,10 +75,41 @@ export class VideoTranscodeProcessor {
   async runVideoEncode(asset: AssetEntity, savedEncodedPath: string): Promise<void> {
     const config = await this.systemConfigService.getConfig();
 
-    if (config.ffmpeg.transcodeAll) {
+    const transcode = await this.needsTranscoding(asset, config.ffmpeg);
+    if (transcode) {
+      //TODO: If video or audio are already the correct format, don't re-encode, copy the stream
       return this.runFFMPEGPipeLine(asset, savedEncodedPath);
     }
+  }
+
+  async needsTranscoding(asset: AssetEntity, ffmpegConfig: SystemConfigFFmpegDto): Promise<boolean> {
+    switch (ffmpegConfig.transcode) {
+      case TranscodePreset.ALL:
+        return true;
+
+      case TranscodePreset.REQUIRED:
+        {
+          const videoStream = await this.getVideoStream(asset);
+          if (videoStream.codec_name !== ffmpegConfig.targetVideoCodec) {
+            return true;
+          }
+        }
+        break;
 
+      case TranscodePreset.OPTIMAL: {
+        const videoStream = await this.getVideoStream(asset);
+        if (videoStream.codec_name !== ffmpegConfig.targetVideoCodec) {
+          return true;
+        }
+
+        const videoHeightThreshold = 1080;
+        return !videoStream.height || videoStream.height > videoHeightThreshold;
+      }
+    }
+    return false;
+  }
+
+  async getVideoStream(asset: AssetEntity): Promise<ffmpeg.FfprobeStream> {
     const videoInfo = await this.runFFProbePipeline(asset);
 
     const videoStreams = videoInfo.streams.filter((stream) => {
@@ -90,10 +122,7 @@ export class VideoTranscodeProcessor {
       return stream2Frames - stream1Frames;
     })[0];
 
-    //TODO: If video or audio are already the correct format, don't re-encode, copy the stream
-    if (longestVideoStream.codec_name !== config.ffmpeg.targetVideoCodec) {
-      return this.runFFMPEGPipeLine(asset, savedEncodedPath);
-    }
+    return longestVideoStream;
   }
 
   async runFFMPEGPipeLine(asset: AssetEntity, savedEncodedPath: string): Promise<void> {

+ 8 - 3
server/immich-openapi-specs.json

@@ -4601,8 +4601,13 @@
           "targetScaling": {
             "type": "string"
           },
-          "transcodeAll": {
-            "type": "boolean"
+          "transcode": {
+            "type": "string",
+            "enum": [
+              "all",
+              "optimal",
+              "required"
+            ]
           }
         },
         "required": [
@@ -4611,7 +4616,7 @@
           "targetVideoCodec",
           "targetAudioCodec",
           "targetScaling",
-          "transcodeAll"
+          "transcode"
         ]
       },
       "SystemConfigOAuthDto": {

+ 2 - 7
server/libs/domain/src/storage-template/storage-template.core.ts

@@ -133,11 +133,10 @@ export class StorageTemplateCore {
     const substitutions: Record<string, string> = {
       filename,
       ext,
+      filetype: asset.type == AssetType.IMAGE ? 'IMG' : 'VID',
+      filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO',
     };
 
-    const fileType = asset.type == AssetType.IMAGE ? 'IMG' : 'VID';
-    const fileTypeFull = asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO';
-
     const dt = luxon.DateTime.fromISO(new Date(asset.fileCreatedAt).toISOString());
 
     const dateTokens = [
@@ -153,10 +152,6 @@ export class StorageTemplateCore {
       substitutions[token] = dt.toFormat(token);
     }
 
-    // Support file type token
-    substitutions.filetype = fileType;
-    substitutions.filetypefull = fileTypeFull;
-
     return template(substitutions);
   }
 }

+ 4 - 3
server/libs/domain/src/system-config/dto/system-config-ffmpeg.dto.ts

@@ -1,4 +1,5 @@
-import { IsBoolean, IsString } from 'class-validator';
+import { IsEnum, IsString } from 'class-validator';
+import { TranscodePreset } from '@app/infra/db/entities';
 
 export class SystemConfigFFmpegDto {
   @IsString()
@@ -16,6 +17,6 @@ export class SystemConfigFFmpegDto {
   @IsString()
   targetScaling!: string;
 
-  @IsBoolean()
-  transcodeAll!: boolean;
+  @IsEnum(TranscodePreset)
+  transcode!: TranscodePreset;
 }

+ 2 - 2
server/libs/domain/src/system-config/system-config.core.ts

@@ -1,4 +1,4 @@
-import { SystemConfig, SystemConfigEntity, SystemConfigKey } from '@app/infra/db/entities';
+import { SystemConfig, SystemConfigEntity, SystemConfigKey, TranscodePreset } from '@app/infra/db/entities';
 import { BadRequestException, Injectable, Logger } from '@nestjs/common';
 import * as _ from 'lodash';
 import { Subject } from 'rxjs';
@@ -14,7 +14,7 @@ const defaults: SystemConfig = Object.freeze({
     targetVideoCodec: 'h264',
     targetAudioCodec: 'aac',
     targetScaling: '1280:-2',
-    transcodeAll: false,
+    transcode: TranscodePreset.REQUIRED,
   },
   oauth: {
     enabled: false,

+ 2 - 2
server/libs/domain/src/system-config/system-config.service.spec.ts

@@ -1,4 +1,4 @@
-import { SystemConfigEntity, SystemConfigKey } from '@app/infra/db/entities';
+import { SystemConfigEntity, SystemConfigKey, TranscodePreset } from '@app/infra/db/entities';
 import { BadRequestException } from '@nestjs/common';
 import { newJobRepositoryMock, newSystemConfigRepositoryMock, systemConfigStub } from '../../test';
 import { IJobRepository, JobName } from '../job';
@@ -18,7 +18,7 @@ const updatedConfig = Object.freeze({
     targetAudioCodec: 'aac',
     targetScaling: '1280:-2',
     targetVideoCodec: 'h264',
-    transcodeAll: false,
+    transcode: TranscodePreset.REQUIRED,
   },
   oauth: {
     autoLaunch: true,

+ 2 - 1
server/libs/domain/test/fixtures.ts

@@ -6,6 +6,7 @@ import {
   SharedLinkEntity,
   SharedLinkType,
   SystemConfig,
+  TranscodePreset,
   UserEntity,
   UserTokenEntity,
 } from '@app/infra/db/entities';
@@ -401,7 +402,7 @@ export const systemConfigStub = {
       targetAudioCodec: 'aac',
       targetScaling: '1280:-2',
       targetVideoCodec: 'h264',
-      transcodeAll: false,
+      transcode: TranscodePreset.REQUIRED,
     },
     oauth: {
       autoLaunch: false,

+ 8 - 2
server/libs/infra/src/db/entities/system-config.entity.ts

@@ -18,7 +18,7 @@ export enum SystemConfigKey {
   FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg.targetVideoCodec',
   FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg.targetAudioCodec',
   FFMPEG_TARGET_SCALING = 'ffmpeg.targetScaling',
-  FFMPEG_TRANSCODE_ALL = 'ffmpeg.transcodeAll',
+  FFMPEG_TRANSCODE = 'ffmpeg.transcode',
   OAUTH_ENABLED = 'oauth.enabled',
   OAUTH_ISSUER_URL = 'oauth.issuerUrl',
   OAUTH_CLIENT_ID = 'oauth.clientId',
@@ -33,6 +33,12 @@ export enum SystemConfigKey {
   STORAGE_TEMPLATE = 'storageTemplate.template',
 }
 
+export enum TranscodePreset {
+  ALL = 'all',
+  OPTIMAL = 'optimal',
+  REQUIRED = 'required',
+}
+
 export interface SystemConfig {
   ffmpeg: {
     crf: string;
@@ -40,7 +46,7 @@ export interface SystemConfig {
     targetVideoCodec: string;
     targetAudioCodec: string;
     targetScaling: string;
-    transcodeAll: boolean;
+    transcode: TranscodePreset;
   };
   oauth: {
     enabled: boolean;

+ 27 - 0
server/libs/infra/src/db/migrations/1679751316282-UpdateTranscodeOption.ts

@@ -0,0 +1,27 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class UpdateTranscodeOption1679751316282 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`
+          UPDATE system_config
+          SET 
+            key = 'ffmpeg.transcode', 
+            value = '"all"'
+          WHERE 
+            key = 'ffmpeg.transcodeAll' AND value = 'true'
+        `);
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`
+          UPDATE system_config
+          SET 
+            key = 'ffmpeg.transcodeAll',
+            value = 'true'
+          WHERE 
+            key = 'ffmpeg.transcode' AND value = '"all"'
+        `);
+
+    await queryRunner.query(`DELETE FROM "system_config" WHERE key = 'ffmpeg.transcode'`);
+  }
+}

+ 11 - 2
web/src/api/open-api/api.ts

@@ -1987,11 +1987,20 @@ export interface SystemConfigFFmpegDto {
     'targetScaling': string;
     /**
      * 
-     * @type {boolean}
+     * @type {string}
      * @memberof SystemConfigFFmpegDto
      */
-    'transcodeAll': boolean;
+    'transcode': SystemConfigFFmpegDtoTranscodeEnum;
 }
+
+export const SystemConfigFFmpegDtoTranscodeEnum = {
+    All: 'all',
+    Optimal: 'optimal',
+    Required: 'required'
+} as const;
+
+export type SystemConfigFFmpegDtoTranscodeEnum = typeof SystemConfigFFmpegDtoTranscodeEnum[keyof typeof SystemConfigFFmpegDtoTranscodeEnum];
+
 /**
  * 
  * @export

+ 23 - 8
web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte

@@ -3,11 +3,10 @@
 		notificationController,
 		NotificationType
 	} from '$lib/components/shared-components/notification/notification';
-	import { api, SystemConfigFFmpegDto } from '@api';
+	import { api, SystemConfigFFmpegDto, SystemConfigFFmpegDtoTranscodeEnum } from '@api';
 	import SettingButtonsRow from '../setting-buttons-row.svelte';
 	import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
 	import SettingSelect from '../setting-select.svelte';
-	import SettingSwitch from '../setting-switch.svelte';
 	import { isEqual } from 'lodash-es';
 	import { fade } from 'svelte/transition';
 
@@ -105,7 +104,12 @@
 					<SettingSelect
 						label="VIDEO CODEC (-vcodec)"
 						bind:value={ffmpegConfig.targetVideoCodec}
-						options={['h264', 'hevc', 'vp9']}
+						options={[
+							{ value: 'h264', text: 'h264' },
+							{ value: 'hevc', text: 'hevc' },
+							{ value: 'vp9', text: 'vp9' }
+						]}
+						name="vcodec"
 						isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
 					/>
 
@@ -117,11 +121,22 @@
 						isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)}
 					/>
 
-					<SettingSwitch
-						title="TRANSCODE ALL"
-						subtitle="Transcode all files, even if they already match the specified format?"
-						bind:checked={ffmpegConfig.transcodeAll}
-						isEdited={!(ffmpegConfig.transcodeAll == savedConfig.transcodeAll)}
+					<SettingSelect
+						label="TRANSCODE"
+						bind:value={ffmpegConfig.transcode}
+						name="transcode"
+						options={[
+							{ value: SystemConfigFFmpegDtoTranscodeEnum.All, text: 'All videos' },
+							{
+								value: SystemConfigFFmpegDtoTranscodeEnum.Optimal,
+								text: 'Videos higher than 1080p or not in the desired format'
+							},
+							{
+								value: SystemConfigFFmpegDtoTranscodeEnum.Required,
+								text: 'Only videos not in the desired format'
+							}
+						]}
+						isEdited={!(ffmpegConfig.transcode == savedConfig.transcode)}
 					/>
 				</div>
 

+ 6 - 5
web/src/lib/components/admin-page/settings/setting-select.svelte

@@ -3,8 +3,9 @@
 	import { fly } from 'svelte/transition';
 
 	export let value: string;
-	export let options: string[];
+	export let options: { value: string; text: string }[];
 	export let label = '';
+	export let name = '';
 	export let isEdited = false;
 
 	const handleChange = (e: Event) => {
@@ -14,7 +15,7 @@
 
 <div class="w-full">
 	<div class={`flex place-items-center gap-1 h-[26px]`}>
-		<label class={`immich-form-label text-sm`} for={label}>{label}</label>
+		<label class={`immich-form-label text-sm`} for="{name}-select">{label}</label>
 
 		{#if isEdited}
 			<div
@@ -27,13 +28,13 @@
 	</div>
 	<select
 		class="immich-form-input w-full"
-		name="presets"
-		id="preset-select"
+		{name}
+		id="{name}-select"
 		bind:value
 		on:change={handleChange}
 	>
 		{#each options as option}
-			<option value={option}>{option}</option>
+			<option value={option.value}>{option.text}</option>
 		{/each}
 	</select>
 </div>