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 46a36d839b..d2a1a0c83b 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.41.1 +- API version: 1.42.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements diff --git a/mobile/openapi/doc/SystemConfigFFmpegDto.md b/mobile/openapi/doc/SystemConfigFFmpegDto.md index b208d7b9ff..cfe86a5afb 100644 --- a/mobile/openapi/doc/SystemConfigFFmpegDto.md +++ b/mobile/openapi/doc/SystemConfigFFmpegDto.md @@ -13,6 +13,7 @@ Name | Type | Description | Notes **targetVideoCodec** | **String** | | **targetAudioCodec** | **String** | | **targetScaling** | **String** | | +**transcodeAll** | **bool** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 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 064bdd36c2..9c929b652d 100644 --- a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -18,6 +18,7 @@ class SystemConfigFFmpegDto { required this.targetVideoCodec, required this.targetAudioCodec, required this.targetScaling, + required this.transcodeAll, }); String crf; @@ -30,13 +31,16 @@ class SystemConfigFFmpegDto { String targetScaling; + bool transcodeAll; + @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegDto && other.crf == crf && other.preset == preset && other.targetVideoCodec == targetVideoCodec && other.targetAudioCodec == targetAudioCodec && - other.targetScaling == targetScaling; + other.targetScaling == targetScaling && + other.transcodeAll == transcodeAll; @override int get hashCode => @@ -45,10 +49,11 @@ class SystemConfigFFmpegDto { (preset.hashCode) + (targetVideoCodec.hashCode) + (targetAudioCodec.hashCode) + - (targetScaling.hashCode); + (targetScaling.hashCode) + + (transcodeAll.hashCode); @override - String toString() => 'SystemConfigFFmpegDto[crf=$crf, preset=$preset, targetVideoCodec=$targetVideoCodec, targetAudioCodec=$targetAudioCodec, targetScaling=$targetScaling]'; + String toString() => 'SystemConfigFFmpegDto[crf=$crf, preset=$preset, targetVideoCodec=$targetVideoCodec, targetAudioCodec=$targetAudioCodec, targetScaling=$targetScaling, transcodeAll=$transcodeAll]'; Map toJson() { final json = {}; @@ -57,6 +62,7 @@ class SystemConfigFFmpegDto { json[r'targetVideoCodec'] = this.targetVideoCodec; json[r'targetAudioCodec'] = this.targetAudioCodec; json[r'targetScaling'] = this.targetScaling; + json[r'transcodeAll'] = this.transcodeAll; return json; } @@ -84,6 +90,7 @@ class SystemConfigFFmpegDto { targetVideoCodec: mapValueOfType(json, r'targetVideoCodec')!, targetAudioCodec: mapValueOfType(json, r'targetAudioCodec')!, targetScaling: mapValueOfType(json, r'targetScaling')!, + transcodeAll: mapValueOfType(json, r'transcodeAll')!, ); } return null; @@ -138,6 +145,7 @@ class SystemConfigFFmpegDto { 'targetVideoCodec', 'targetAudioCodec', 'targetScaling', + 'transcodeAll', }; } diff --git a/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart b/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart index a088dc6110..e0b862f1a2 100644 --- a/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart +++ b/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart @@ -41,6 +41,11 @@ void main() { // TODO }); + // bool transcodeAll + test('to test the property `transcodeAll`', () async { + // TODO + }); + }); diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts index 37893b4a4d..741cd3854e 100644 --- a/server/apps/immich/src/api-v1/asset/asset-repository.ts +++ b/server/apps/immich/src/api-v1/asset/asset-repository.ts @@ -39,6 +39,7 @@ export interface IAssetRepository { getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise; 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}