From 4e0fe27de3677635c4374fff8f8fd462ff793fdc Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Sun, 22 Jan 2023 02:09:02 +0000 Subject: [PATCH] feat(server): transcoding improvements (#1370) * feat: support isEdited flag for SettingSwitch * feat: add transcodeAll ffmpeg settings for extra transcoding control * refactor: tidy up and rename current video transcoding code + transcode everything * feat: better video transcoding with ffprobe analyses video files to see if they are already in the desired format allows admin to choose to transcode all videos regardless of the current format * fix: always serve encoded video if it exists * feat: change video codec option to a select box, limit options removed previous video codec config option as it's incompatible with new options removed mapping for encoder to codec as we now store the codec in the config * feat: add video conversion job for transcoding previously missed videos * chore: fix spelling of job messages to pluralise assets * chore: fix prettier/eslint warnings * feat: force switch targetAudioCodec default to aac to avoid iOS incompatibility * chore: lint issues after rebase --- .gitignore | 3 +- mobile/openapi/README.md | Bin 13908 -> 13908 bytes mobile/openapi/doc/SystemConfigFFmpegDto.md | Bin 561 -> 595 bytes .../lib/model/system_config_f_fmpeg_dto.dart | Bin 4640 -> 4932 bytes .../test/system_config_f_fmpeg_dto_test.dart | Bin 1019 -> 1126 bytes .../src/api-v1/asset/asset-repository.ts | 10 +++ .../src/api-v1/asset/asset.service.spec.ts | 1 + .../immich/src/api-v1/asset/asset.service.ts | 8 +-- .../apps/immich/src/api-v1/job/job.service.ts | 21 +++++- .../schedule-tasks/schedule-tasks.service.ts | 2 +- .../processors/asset-uploaded.processor.ts | 2 +- .../processors/video-transcode.processor.ts | 68 +++++++++++++----- server/immich-openapi-specs.json | 6 +- .../interfaces/video-transcode.interface.ts | 4 +- server/libs/domain/src/job/job.constants.ts | 2 +- server/libs/domain/src/job/job.repository.ts | 4 +- .../dto/system-config-ffmpeg.dto.ts | 5 +- .../src/system-config/system-config.core.ts | 5 +- .../system-config.service.spec.ts | 5 +- server/libs/domain/test/fixtures.ts | 5 +- .../src/db/entities/system-config.entity.ts | 2 + ...4263302005-RemoveVideoCodecConfigOption.ts | 12 ++++ web/src/api/open-api/api.ts | 27 +++++-- web/src/api/open-api/base.ts | 2 +- web/src/api/open-api/common.ts | 2 +- web/src/api/open-api/configuration.ts | 2 +- web/src/api/open-api/index.ts | 2 +- .../admin-page/jobs/jobs-panel.svelte | 39 +++++++++- .../settings/ffmpeg/ffmpeg-settings.svelte | 17 +++-- .../admin-page/settings/setting-select.svelte | 39 ++++++++++ .../admin-page/settings/setting-switch.svelte | 20 +++++- 31 files changed, 256 insertions(+), 59 deletions(-) create mode 100644 server/libs/infra/src/db/migrations/1674263302005-RemoveVideoCodecConfigOption.ts create mode 100644 web/src/lib/components/admin-page/settings/setting-select.svelte diff --git a/.gitignore b/.gitignore index 58561174d7..984d98d430 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ .idea docker/upload -coverage +uploads +coverage \ No newline at end of file diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 46a36d839b1a40aabc39072d49e67611fea848d5..d2a1a0c83b278890523455cb1da2b9ff06dbee5b 100644 GIT binary patch delta 14 WcmcbTb0ufOEM_A;gN<{yn*sndcm{X? delta 14 WcmcbTb0ufOEM`MJ!;N#dn*sndb_RF= diff --git a/mobile/openapi/doc/SystemConfigFFmpegDto.md b/mobile/openapi/doc/SystemConfigFFmpegDto.md index b208d7b9ffface6615f4a851fec658db2196b36d..cfe86a5afb03b23ece04cf408103aa850eba61cf 100644 GIT binary patch delta 37 scmdnUa+zgA52K`(R!LD}UU71MN~&W{j+T}}je?d|Qht8U=l}o! delta 11 Scmcc2vXNy&598!@j7tF=w*;{O diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart index 064bdd36c26aae0018eacc666d5df663bcdef7cc..9c929b652d4a6f8752b3b572c14ebb41cadfa2c7 100644 GIT binary patch delta 304 zcmZ3Wazt%|Hj|8kLQ!gAX=YJsib6?7X0cvLQDR; getAssetByChecksum(userId: string, checksum: Buffer): Promise; getAssetWithNoThumbnail(): Promise; + getAssetWithNoEncodedVideo(): Promise; getAssetWithNoEXIF(): Promise; getAssetWithNoSmartInfo(): Promise; getExistingAssets( @@ -80,6 +81,15 @@ export class AssetRepository implements IAssetRepository { }); } + async getAssetWithNoEncodedVideo(): Promise { + return await this.assetRepository.find({ + where: [ + { type: AssetType.VIDEO, encodedVideoPath: IsNull() }, + { type: AssetType.VIDEO, encodedVideoPath: '' }, + ], + }); + } + async getAssetWithNoEXIF(): Promise { return await this.assetRepository .createQueryBuilder('asset') diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts index 02b4511c22..a07873d815 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts @@ -128,6 +128,7 @@ describe('AssetService', () => { getAssetWithNoEXIF: jest.fn(), getAssetWithNoThumbnail: jest.fn(), getAssetWithNoSmartInfo: jest.fn(), + getAssetWithNoEncodedVideo: jest.fn(), getExistingAssets: jest.fn(), countByIdAndUser: jest.fn(), }; diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index ad582a5d25..c4977cd5bf 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -37,13 +37,13 @@ import { import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto'; import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto'; import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; -import { assetUtils, timeUtils } from '@app/common/utils'; +import { timeUtils } from '@app/common/utils'; import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto'; import { UpdateAssetDto } from './dto/update-asset.dto'; import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; -import { IAssetUploadedJob, IVideoTranscodeJob, QueueName, JobName } from '@app/domain'; +import { IAssetUploadedJob, IVideoTranscodeJob, JobName, QueueName } from '@app/domain'; import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; import { DownloadService } from '../../modules/download/download.service'; @@ -122,7 +122,7 @@ export class AssetService { await this.storageService.moveAsset(livePhotoAssetEntity, originalAssetData.originalname); - await this.videoConversionQueue.add(JobName.MP4_CONVERSION, { asset: livePhotoAssetEntity }); + await this.videoConversionQueue.add(JobName.VIDEO_CONVERSION, { asset: livePhotoAssetEntity }); } const assetEntity = await this.createUserAsset( @@ -456,7 +456,7 @@ export class AssetService { await fs.access(videoPath, constants.R_OK | constants.W_OK); - if (query.isWeb && !assetUtils.isWebPlayable(asset.mimeType)) { + if (asset.encodedVideoPath) { videoPath = asset.encodedVideoPath == '' ? String(asset.originalPath) : String(asset.encodedVideoPath); mimeType = asset.encodedVideoPath == '' ? asset.mimeType : 'video/mp4'; } diff --git a/server/apps/immich/src/api-v1/job/job.service.ts b/server/apps/immich/src/api-v1/job/job.service.ts index 519c527fd3..4bba764bc4 100644 --- a/server/apps/immich/src/api-v1/job/job.service.ts +++ b/server/apps/immich/src/api-v1/job/job.service.ts @@ -3,8 +3,8 @@ import { IMetadataExtractionJob, IThumbnailGenerationJob, IVideoTranscodeJob, - QueueName, JobName, + QueueName, } from '@app/domain'; import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; @@ -53,7 +53,7 @@ export class JobService { case JobId.METADATA_EXTRACTION: return this.runMetadataExtractionJob(); case JobId.VIDEO_CONVERSION: - return 0; + return this.runVideoConversionJob(); case JobId.MACHINE_LEARNING: return this.runMachineLearningPipeline(); case JobId.STORAGE_TEMPLATE_MIGRATION: @@ -79,7 +79,6 @@ export class JobService { response.videoConversionQueueCount = videoConversionJobCount; response.isMachineLearningActive = Boolean(machineLearningJobCount.waiting); response.machineLearningQueueCount = machineLearningJobCount; - response.isStorageMigrationActive = Boolean(storageMigrationJobCount.active); response.storageMigrationQueueCount = storageMigrationJobCount; @@ -188,6 +187,22 @@ export class JobService { return assetWithNoSmartInfo.length; } + private async runVideoConversionJob(): Promise { + const jobCount = await this.videoConversionQueue.getJobCounts(); + + if (jobCount.waiting > 0) { + throw new BadRequestException('Video conversion job is already running'); + } + + const assetsWithNoConvertedVideo = await this._assetRepository.getAssetWithNoEncodedVideo(); + + for (const asset of assetsWithNoConvertedVideo) { + await this.videoConversionQueue.add(JobName.VIDEO_CONVERSION, { asset }); + } + + return assetsWithNoConvertedVideo.length; + } + async runStorageMigration() { const jobCount = await this.configQueue.getJobCounts(); diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts index 3cd565cc9d..e1e92dcd2b 100644 --- a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts +++ b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts @@ -69,7 +69,7 @@ export class ScheduleTasksService { }); for (const asset of assets) { - await this.videoConversionQueue.add(JobName.MP4_CONVERSION, { asset }); + await this.videoConversionQueue.add(JobName.VIDEO_CONVERSION, { asset }); } } diff --git a/server/apps/microservices/src/processors/asset-uploaded.processor.ts b/server/apps/microservices/src/processors/asset-uploaded.processor.ts index c7602f46c3..ea0005ee3c 100644 --- a/server/apps/microservices/src/processors/asset-uploaded.processor.ts +++ b/server/apps/microservices/src/processors/asset-uploaded.processor.ts @@ -40,7 +40,7 @@ export class AssetUploadedProcessor { // Video Conversion if (asset.type == AssetType.VIDEO) { - await this.videoConversionQueue.add(JobName.MP4_CONVERSION, { asset }); + await this.videoConversionQueue.add(JobName.VIDEO_CONVERSION, { asset }); await this.metadataExtractionQueue.add(JobName.EXTRACT_VIDEO_METADATA, { asset, fileName }); } else { // Extract Metadata/Exif for Images - Currently the EXIF library on the web cannot extract EXIF for video yet diff --git a/server/apps/microservices/src/processors/video-transcode.processor.ts b/server/apps/microservices/src/processors/video-transcode.processor.ts index 170cb70de2..52557e62fe 100644 --- a/server/apps/microservices/src/processors/video-transcode.processor.ts +++ b/server/apps/microservices/src/processors/video-transcode.processor.ts @@ -1,14 +1,12 @@ import { APP_UPLOAD_LOCATION } from '@app/common/constants'; import { AssetEntity } from '@app/infra'; -import { QueueName, JobName } from '@app/domain'; -import { IMp4ConversionProcessor } from '@app/domain'; +import { IVideoConversionProcessor, JobName, QueueName, SystemConfigService } from '@app/domain'; import { Process, Processor } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Job } from 'bull'; -import ffmpeg from 'fluent-ffmpeg'; +import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; import { existsSync, mkdirSync } from 'fs'; -import { SystemConfigService } from '@app/domain'; import { Repository } from 'typeorm'; @Processor(QueueName.VIDEO_CONVERSION) @@ -19,24 +17,60 @@ export class VideoTranscodeProcessor { private systemConfigService: SystemConfigService, ) {} - @Process({ name: JobName.MP4_CONVERSION, concurrency: 2 }) - async mp4Conversion(job: Job) { + @Process({ name: JobName.VIDEO_CONVERSION, concurrency: 2 }) + async videoConversion(job: Job) { const { asset } = job.data; - if (asset.mimeType != 'video/mp4') { - const basePath = APP_UPLOAD_LOCATION; - const encodedVideoPath = `${basePath}/${asset.userId}/encoded-video`; + const basePath = APP_UPLOAD_LOCATION; + const encodedVideoPath = `${basePath}/${asset.userId}/encoded-video`; - if (!existsSync(encodedVideoPath)) { - mkdirSync(encodedVideoPath, { recursive: true }); - } + if (!existsSync(encodedVideoPath)) { + mkdirSync(encodedVideoPath, { recursive: true }); + } - const savedEncodedPath = encodedVideoPath + '/' + asset.id + '.mp4'; + const savedEncodedPath = `${encodedVideoPath}/${asset.id}.mp4`; - if (asset.encodedVideoPath == '' || !asset.encodedVideoPath) { - // Put the processing into its own async function to prevent the job exist right away - await this.runFFMPEGPipeLine(asset, savedEncodedPath); - } + if (!asset.encodedVideoPath) { + // Put the processing into its own async function to prevent the job exist right away + await this.runVideoEncode(asset, savedEncodedPath); + } + } + + async runFFProbePipeline(asset: AssetEntity): Promise { + return new Promise((resolve, reject) => { + ffmpeg.ffprobe(asset.originalPath, (err, data) => { + if (err || !data) { + Logger.error(`Cannot probe video ${err}`, 'mp4Conversion'); + reject(err); + } + + resolve(data); + }); + }); + } + + async runVideoEncode(asset: AssetEntity, savedEncodedPath: string): Promise { + const config = await this.systemConfigService.getConfig(); + + if (config.ffmpeg.transcodeAll) { + return this.runFFMPEGPipeLine(asset, savedEncodedPath); + } + + const videoInfo = await this.runFFProbePipeline(asset); + + const videoStreams = videoInfo.streams.filter((stream) => { + return stream.codec_type === 'video'; + }); + + const longestVideoStream = videoStreams.sort((stream1, stream2) => { + const stream1Frames = Number.parseInt(stream1.nb_frames ?? '0'); + const stream2Frames = Number.parseInt(stream2.nb_frames ?? '0'); + 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); } } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index c37159c9c1..d9f5f23ece 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -2899,6 +2899,9 @@ }, "targetScaling": { "type": "string" + }, + "transcodeAll": { + "type": "boolean" } }, "required": [ @@ -2906,7 +2909,8 @@ "preset", "targetVideoCodec", "targetAudioCodec", - "targetScaling" + "targetScaling", + "transcodeAll" ] }, "SystemConfigOAuthDto": { diff --git a/server/libs/domain/src/job/interfaces/video-transcode.interface.ts b/server/libs/domain/src/job/interfaces/video-transcode.interface.ts index 6cc8870cfa..325a491e9f 100644 --- a/server/libs/domain/src/job/interfaces/video-transcode.interface.ts +++ b/server/libs/domain/src/job/interfaces/video-transcode.interface.ts @@ -1,10 +1,10 @@ import { AssetEntity } from '@app/infra/db/entities'; -export interface IMp4ConversionProcessor { +export interface IVideoConversionProcessor { /** * The Asset entity that was saved in the database */ asset: AssetEntity; } -export type IVideoTranscodeJob = IMp4ConversionProcessor; +export type IVideoTranscodeJob = IVideoConversionProcessor; diff --git a/server/libs/domain/src/job/job.constants.ts b/server/libs/domain/src/job/job.constants.ts index e66c81e3c8..dd5d060c44 100644 --- a/server/libs/domain/src/job/job.constants.ts +++ b/server/libs/domain/src/job/job.constants.ts @@ -12,7 +12,7 @@ export enum QueueName { export enum JobName { ASSET_UPLOADED = 'asset-uploaded', - MP4_CONVERSION = 'mp4-conversion', + VIDEO_CONVERSION = 'mp4-conversion', GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail', GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail', EXIF_EXTRACTION = 'exif-extraction', diff --git a/server/libs/domain/src/job/job.repository.ts b/server/libs/domain/src/job/job.repository.ts index 66177a49a5..fe129b7748 100644 --- a/server/libs/domain/src/job/job.repository.ts +++ b/server/libs/domain/src/job/job.repository.ts @@ -3,7 +3,7 @@ import { IDeleteFileOnDiskJob, IExifExtractionProcessor, IMachineLearningJob, - IMp4ConversionProcessor, + IVideoConversionProcessor, IReverseGeocodingProcessor, IUserDeletionJob, JpegGeneratorProcessor, @@ -13,7 +13,7 @@ import { JobName } from './job.constants'; export type JobItem = | { name: JobName.ASSET_UPLOADED; data: IAssetUploadedJob } - | { name: JobName.MP4_CONVERSION; data: IMp4ConversionProcessor } + | { name: JobName.VIDEO_CONVERSION; data: IVideoConversionProcessor } | { name: JobName.GENERATE_JPEG_THUMBNAIL; data: JpegGeneratorProcessor } | { name: JobName.GENERATE_WEBP_THUMBNAIL; data: WebpGeneratorProcessor } | { name: JobName.EXIF_EXTRACTION; data: IExifExtractionProcessor } diff --git a/server/libs/domain/src/system-config/dto/system-config-ffmpeg.dto.ts b/server/libs/domain/src/system-config/dto/system-config-ffmpeg.dto.ts index 2b96addb2d..5dc75c62d3 100644 --- a/server/libs/domain/src/system-config/dto/system-config-ffmpeg.dto.ts +++ b/server/libs/domain/src/system-config/dto/system-config-ffmpeg.dto.ts @@ -1,4 +1,4 @@ -import { IsString } from 'class-validator'; +import { IsBoolean, IsString } from 'class-validator'; export class SystemConfigFFmpegDto { @IsString() @@ -15,4 +15,7 @@ export class SystemConfigFFmpegDto { @IsString() targetScaling!: string; + + @IsBoolean() + transcodeAll!: boolean; } diff --git a/server/libs/domain/src/system-config/system-config.core.ts b/server/libs/domain/src/system-config/system-config.core.ts index 6d1dd53b02..36e1b548f3 100644 --- a/server/libs/domain/src/system-config/system-config.core.ts +++ b/server/libs/domain/src/system-config/system-config.core.ts @@ -11,9 +11,10 @@ const defaults: SystemConfig = Object.freeze({ ffmpeg: { crf: '23', preset: 'ultrafast', - targetVideoCodec: 'libx264', - targetAudioCodec: 'mp3', + targetVideoCodec: 'h264', + targetAudioCodec: 'aac', targetScaling: '1280:-2', + transcodeAll: false, }, oauth: { enabled: false, diff --git a/server/libs/domain/src/system-config/system-config.service.spec.ts b/server/libs/domain/src/system-config/system-config.service.spec.ts index 715ea183d7..6a2b5ce953 100644 --- a/server/libs/domain/src/system-config/system-config.service.spec.ts +++ b/server/libs/domain/src/system-config/system-config.service.spec.ts @@ -15,9 +15,10 @@ const updatedConfig = Object.freeze({ ffmpeg: { crf: 'a new value', preset: 'ultrafast', - targetAudioCodec: 'mp3', + targetAudioCodec: 'aac', targetScaling: '1280:-2', - targetVideoCodec: 'libx264', + targetVideoCodec: 'h264', + transcodeAll: false, }, oauth: { autoLaunch: true, diff --git a/server/libs/domain/test/fixtures.ts b/server/libs/domain/test/fixtures.ts index f7d9222a1c..a94b24a2c8 100644 --- a/server/libs/domain/test/fixtures.ts +++ b/server/libs/domain/test/fixtures.ts @@ -48,9 +48,10 @@ export const systemConfigStub = { ffmpeg: { crf: '23', preset: 'ultrafast', - targetAudioCodec: 'mp3', + targetAudioCodec: 'aac', targetScaling: '1280:-2', - targetVideoCodec: 'libx264', + targetVideoCodec: 'h264', + transcodeAll: false, }, oauth: { autoLaunch: false, diff --git a/server/libs/infra/src/db/entities/system-config.entity.ts b/server/libs/infra/src/db/entities/system-config.entity.ts index de9280e4e5..0c47534cbd 100644 --- a/server/libs/infra/src/db/entities/system-config.entity.ts +++ b/server/libs/infra/src/db/entities/system-config.entity.ts @@ -18,6 +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', OAUTH_ENABLED = 'oauth.enabled', OAUTH_ISSUER_URL = 'oauth.issuerUrl', OAUTH_CLIENT_ID = 'oauth.clientId', @@ -39,6 +40,7 @@ export interface SystemConfig { targetVideoCodec: string; targetAudioCodec: string; targetScaling: string; + transcodeAll: boolean; }; oauth: { enabled: boolean; diff --git a/server/libs/infra/src/db/migrations/1674263302005-RemoveVideoCodecConfigOption.ts b/server/libs/infra/src/db/migrations/1674263302005-RemoveVideoCodecConfigOption.ts new file mode 100644 index 0000000000..5f64b11559 --- /dev/null +++ b/server/libs/infra/src/db/migrations/1674263302005-RemoveVideoCodecConfigOption.ts @@ -0,0 +1,12 @@ +import {MigrationInterface, QueryRunner} from 'typeorm'; + +export class RemoveVideoCodecConfigOption1674263302006 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DELETE FROM "system_config" WHERE key = 'ffmpeg.targetVideoCodec'`); + await queryRunner.query(`DELETE FROM "system_config" WHERE key = 'ffmpeg.targetAudioCodec'`); + } + + public async down(): Promise { + // noop + } +} diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 5af6cb396f..ba8b0e9f9c 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.41.1 + * The version of the OpenAPI document: 1.42.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -13,13 +13,24 @@ */ -import { Configuration } from './configuration'; -import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import {Configuration} from './configuration'; +import globalAxios, {AxiosInstance, AxiosPromise, AxiosRequestConfig} from 'axios'; // Some imports not used depending on template conditions // @ts-ignore -import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; +import { + assertParamExists, + createRequestFunction, + DUMMY_BASE_URL, + serializeDataIfNeeded, + setApiKeyToObject, + setBasicAuthToObject, + setBearerAuthToObject, + setOAuthToObject, + setSearchParams, + toPathString +} from './common'; // @ts-ignore -import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from './base'; +import {BASE_PATH, BaseAPI, COLLECTION_FORMATS, RequestArgs, RequiredError} from './base'; /** * @@ -1799,6 +1810,12 @@ export interface SystemConfigFFmpegDto { * @memberof SystemConfigFFmpegDto */ 'targetScaling': string; + /** + * + * @type {boolean} + * @memberof SystemConfigFFmpegDto + */ + 'transcodeAll': boolean; } /** * diff --git a/web/src/api/open-api/base.ts b/web/src/api/open-api/base.ts index 2d8f7a0a43..3902b9e4f8 100644 --- a/web/src/api/open-api/base.ts +++ b/web/src/api/open-api/base.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.41.1 + * The version of the OpenAPI document: 1.42.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/common.ts b/web/src/api/open-api/common.ts index acd18acb28..a2e7728e2e 100644 --- a/web/src/api/open-api/common.ts +++ b/web/src/api/open-api/common.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.41.1 + * The version of the OpenAPI document: 1.42.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/configuration.ts b/web/src/api/open-api/configuration.ts index 65556da1f2..ccb51fb3f9 100644 --- a/web/src/api/open-api/configuration.ts +++ b/web/src/api/open-api/configuration.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.41.1 + * The version of the OpenAPI document: 1.42.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/index.ts b/web/src/api/open-api/index.ts index ea27b5ef1e..bcbb0bd7a0 100644 --- a/web/src/api/open-api/index.ts +++ b/web/src/api/open-api/index.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.41.1 + * The version of the OpenAPI document: 1.42.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index f374340dd0..5c3dcdc93f 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -33,7 +33,7 @@ if (data) { notificationController.show({ - message: `Thumbnail generation job started for ${data} asset`, + message: `Thumbnail generation job started for ${data} assets`, type: NotificationType.Info }); } else { @@ -60,7 +60,7 @@ if (data) { notificationController.show({ - message: `Extract EXIF job started for ${data} asset`, + message: `Extract EXIF job started for ${data} assets`, type: NotificationType.Info }); } else { @@ -87,7 +87,7 @@ if (data) { notificationController.show({ - message: `Object detection job started for ${data} asset`, + message: `Object detection job started for ${data} assets`, type: NotificationType.Info }); } else { @@ -101,6 +101,28 @@ } }; + const runVideoConversion = async () => { + try { + const { data } = await api.jobApi.sendJobCommand(JobId.VideoConversion, { + command: JobCommand.Start + }); + + if (data) { + notificationController.show({ + message: `Video conversion job started for ${data} assets`, + type: NotificationType.Info + }); + } else { + notificationController.show({ + message: `No videos without an encoded version found`, + type: NotificationType.Info + }); + } + } catch (error) { + handleError(error, `Error running video conversion job, check console for more detail`); + } + }; + const runTemplateMigration = async () => { try { const { data } = await api.jobApi.sendJobCommand(JobId.StorageTemplateMigration, { @@ -159,6 +181,17 @@ Note that some assets may not have any objects detected, this is normal. + + Note that some videos won't require transcoding, this is normal. + + - @@ -114,6 +116,13 @@ required={true} isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)} /> + +
diff --git a/web/src/lib/components/admin-page/settings/setting-select.svelte b/web/src/lib/components/admin-page/settings/setting-select.svelte new file mode 100644 index 0000000000..9f6ff7636e --- /dev/null +++ b/web/src/lib/components/admin-page/settings/setting-select.svelte @@ -0,0 +1,39 @@ + + +
+
+ + + {#if isEdited} +
+ Unsaved change +
+ {/if} +
+ +
diff --git a/web/src/lib/components/admin-page/settings/setting-switch.svelte b/web/src/lib/components/admin-page/settings/setting-switch.svelte index e7c591b934..c5816f4ed2 100644 --- a/web/src/lib/components/admin-page/settings/setting-switch.svelte +++ b/web/src/lib/components/admin-page/settings/setting-switch.svelte @@ -1,15 +1,29 @@
-

- {title} -

+
+ + {#if isEdited} +
+ Unsaved change +
+ {/if} +

{subtitle}