mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(server): add transcode presets (#2084)
* feat: add transcode presets * Add migration * chore: generate api * refactor: use enum type instead of string for transcode option * chore: generate api * refactor: enhance readability of runVideoEncode method * refactor: reuse SettingSelect for transcoding presets * refactor: simplify return statement * chore: regenerate api * fix: correct label attribute * Update import * fix test --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		
							
								
								
									
										2
									
								
								mobile/openapi/doc/SystemConfigFFmpegDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/SystemConfigFFmpegDto.md
									
									
									
										generated
									
									
									
								
							| @@ -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) | ||||
| 
 | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|   | ||||
| @@ -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 | ||||
|     }); | ||||
| 
 | ||||
|   | ||||
| @@ -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> { | ||||
|   | ||||
| @@ -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": { | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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'`); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -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 | ||||
|   | ||||
| @@ -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> | ||||
|  | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user