mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
feat(web,server): run jobs for specific assets (#3712)
* feat(web,server): manually queue asset job * chore: open api * chore: tests
This commit is contained in:
parent
66490d5db4
commit
5e901e4d21
26 changed files with 506 additions and 18 deletions
124
cli/src/api/open-api/api.ts
generated
124
cli/src/api/open-api/api.ts
generated
|
|
@ -525,6 +525,42 @@ export const AssetIdsResponseDtoErrorEnum = {
|
||||||
|
|
||||||
export type AssetIdsResponseDtoErrorEnum = typeof AssetIdsResponseDtoErrorEnum[keyof typeof AssetIdsResponseDtoErrorEnum];
|
export type AssetIdsResponseDtoErrorEnum = typeof AssetIdsResponseDtoErrorEnum[keyof typeof AssetIdsResponseDtoErrorEnum];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const AssetJobName = {
|
||||||
|
RegenerateThumbnail: 'regenerate-thumbnail',
|
||||||
|
RefreshMetadata: 'refresh-metadata',
|
||||||
|
TranscodeVideo: 'transcode-video'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AssetJobName = typeof AssetJobName[keyof typeof AssetJobName];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface AssetJobsDto
|
||||||
|
*/
|
||||||
|
export interface AssetJobsDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof AssetJobsDto
|
||||||
|
*/
|
||||||
|
'assetIds': Array<string>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {AssetJobName}
|
||||||
|
* @memberof AssetJobsDto
|
||||||
|
*/
|
||||||
|
'name': AssetJobName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
|
@ -5784,6 +5820,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AssetJobsDto} assetJobsDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
runAssetJobs: async (assetJobsDto: AssetJobsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'assetJobsDto' is not null or undefined
|
||||||
|
assertParamExists('runAssetJobs', 'assetJobsDto', assetJobsDto)
|
||||||
|
const localVarPath = `/asset/jobs`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication cookie required
|
||||||
|
|
||||||
|
// authentication api_key required
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
localVarRequestOptions.data = serializeDataIfNeeded(assetJobsDto, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {SearchAssetDto} searchAssetDto
|
* @param {SearchAssetDto} searchAssetDto
|
||||||
|
|
@ -6331,6 +6411,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.importFile(importAssetDto, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.importFile(importAssetDto, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AssetJobsDto} assetJobsDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async runAssetJobs(assetJobsDto: AssetJobsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.runAssetJobs(assetJobsDto, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {SearchAssetDto} searchAssetDto
|
* @param {SearchAssetDto} searchAssetDto
|
||||||
|
|
@ -6584,6 +6674,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||||
importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig): AxiosPromise<AssetFileUploadResponseDto> {
|
importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig): AxiosPromise<AssetFileUploadResponseDto> {
|
||||||
return localVarFp.importFile(requestParameters.importAssetDto, options).then((request) => request(axios, basePath));
|
return localVarFp.importFile(requestParameters.importAssetDto, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AssetApiRunAssetJobsRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
runAssetJobs(requestParameters: AssetApiRunAssetJobsRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
|
||||||
|
return localVarFp.runAssetJobs(requestParameters.assetJobsDto, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {AssetApiSearchAssetRequest} requestParameters Request parameters.
|
* @param {AssetApiSearchAssetRequest} requestParameters Request parameters.
|
||||||
|
|
@ -7066,6 +7165,20 @@ export interface AssetApiImportFileRequest {
|
||||||
readonly importAssetDto: ImportAssetDto
|
readonly importAssetDto: ImportAssetDto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for runAssetJobs operation in AssetApi.
|
||||||
|
* @export
|
||||||
|
* @interface AssetApiRunAssetJobsRequest
|
||||||
|
*/
|
||||||
|
export interface AssetApiRunAssetJobsRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {AssetJobsDto}
|
||||||
|
* @memberof AssetApiRunAssetJobs
|
||||||
|
*/
|
||||||
|
readonly assetJobsDto: AssetJobsDto
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request parameters for searchAsset operation in AssetApi.
|
* Request parameters for searchAsset operation in AssetApi.
|
||||||
* @export
|
* @export
|
||||||
|
|
@ -7472,6 +7585,17 @@ export class AssetApi extends BaseAPI {
|
||||||
return AssetApiFp(this.configuration).importFile(requestParameters.importAssetDto, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).importFile(requestParameters.importAssetDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AssetApiRunAssetJobsRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof AssetApi
|
||||||
|
*/
|
||||||
|
public runAssetJobs(requestParameters: AssetApiRunAssetJobsRequest, options?: AxiosRequestConfig) {
|
||||||
|
return AssetApiFp(this.configuration).runAssetJobs(requestParameters.assetJobsDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {AssetApiSearchAssetRequest} requestParameters Request parameters.
|
* @param {AssetApiSearchAssetRequest} requestParameters Request parameters.
|
||||||
|
|
|
||||||
6
mobile/openapi/.openapi-generator/FILES
generated
6
mobile/openapi/.openapi-generator/FILES
generated
|
|
@ -23,6 +23,8 @@ doc/AssetBulkUploadCheckResult.md
|
||||||
doc/AssetFileUploadResponseDto.md
|
doc/AssetFileUploadResponseDto.md
|
||||||
doc/AssetIdsDto.md
|
doc/AssetIdsDto.md
|
||||||
doc/AssetIdsResponseDto.md
|
doc/AssetIdsResponseDto.md
|
||||||
|
doc/AssetJobName.md
|
||||||
|
doc/AssetJobsDto.md
|
||||||
doc/AssetResponseDto.md
|
doc/AssetResponseDto.md
|
||||||
doc/AssetStatsResponseDto.md
|
doc/AssetStatsResponseDto.md
|
||||||
doc/AssetTypeEnum.md
|
doc/AssetTypeEnum.md
|
||||||
|
|
@ -168,6 +170,8 @@ lib/model/asset_bulk_upload_check_result.dart
|
||||||
lib/model/asset_file_upload_response_dto.dart
|
lib/model/asset_file_upload_response_dto.dart
|
||||||
lib/model/asset_ids_dto.dart
|
lib/model/asset_ids_dto.dart
|
||||||
lib/model/asset_ids_response_dto.dart
|
lib/model/asset_ids_response_dto.dart
|
||||||
|
lib/model/asset_job_name.dart
|
||||||
|
lib/model/asset_jobs_dto.dart
|
||||||
lib/model/asset_response_dto.dart
|
lib/model/asset_response_dto.dart
|
||||||
lib/model/asset_stats_response_dto.dart
|
lib/model/asset_stats_response_dto.dart
|
||||||
lib/model/asset_type_enum.dart
|
lib/model/asset_type_enum.dart
|
||||||
|
|
@ -282,6 +286,8 @@ test/asset_bulk_upload_check_result_test.dart
|
||||||
test/asset_file_upload_response_dto_test.dart
|
test/asset_file_upload_response_dto_test.dart
|
||||||
test/asset_ids_dto_test.dart
|
test/asset_ids_dto_test.dart
|
||||||
test/asset_ids_response_dto_test.dart
|
test/asset_ids_response_dto_test.dart
|
||||||
|
test/asset_job_name_test.dart
|
||||||
|
test/asset_jobs_dto_test.dart
|
||||||
test/asset_response_dto_test.dart
|
test/asset_response_dto_test.dart
|
||||||
test/asset_stats_response_dto_test.dart
|
test/asset_stats_response_dto_test.dart
|
||||||
test/asset_type_enum_test.dart
|
test/asset_type_enum_test.dart
|
||||||
|
|
|
||||||
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetJobName.md
generated
Normal file
BIN
mobile/openapi/doc/AssetJobName.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/AssetJobsDto.md
generated
Normal file
BIN
mobile/openapi/doc/AssetJobsDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_helper.dart
generated
BIN
mobile/openapi/lib/api_helper.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/asset_job_name.dart
generated
Normal file
BIN
mobile/openapi/lib/model/asset_job_name.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/asset_jobs_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/asset_jobs_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/asset_job_name_test.dart
generated
Normal file
BIN
mobile/openapi/test/asset_job_name_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/asset_jobs_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/asset_jobs_dto_test.dart
generated
Normal file
Binary file not shown.
|
|
@ -1367,6 +1367,41 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/asset/jobs": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "runAssetJobs",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AssetJobsDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Asset"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/asset/map-marker": {
|
"/asset/map-marker": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getMapMarkers",
|
"operationId": "getMapMarkers",
|
||||||
|
|
@ -5042,6 +5077,33 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"AssetJobName": {
|
||||||
|
"enum": [
|
||||||
|
"regenerate-thumbnail",
|
||||||
|
"refresh-metadata",
|
||||||
|
"transcode-video"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"AssetJobsDto": {
|
||||||
|
"properties": {
|
||||||
|
"assetIds": {
|
||||||
|
"items": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"$ref": "#/components/schemas/AssetJobName"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"assetIds",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"AssetResponseDto": {
|
"AssetResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"checksum": {
|
"checksum": {
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,17 @@ import {
|
||||||
newAccessRepositoryMock,
|
newAccessRepositoryMock,
|
||||||
newAssetRepositoryMock,
|
newAssetRepositoryMock,
|
||||||
newCryptoRepositoryMock,
|
newCryptoRepositoryMock,
|
||||||
|
newJobRepositoryMock,
|
||||||
newStorageRepositoryMock,
|
newStorageRepositoryMock,
|
||||||
} from '@test';
|
} from '@test';
|
||||||
import { when } from 'jest-when';
|
import { when } from 'jest-when';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { ICryptoRepository } from '../crypto';
|
import { ICryptoRepository } from '../crypto';
|
||||||
|
import { IJobRepository, JobName } from '../index';
|
||||||
import { IStorageRepository } from '../storage';
|
import { IStorageRepository } from '../storage';
|
||||||
import { AssetStats, IAssetRepository } from './asset.repository';
|
import { AssetStats, IAssetRepository } from './asset.repository';
|
||||||
import { AssetService, UploadFieldName } from './asset.service';
|
import { AssetService, UploadFieldName } from './asset.service';
|
||||||
import { AssetStatsResponseDto, DownloadResponseDto } from './dto';
|
import { AssetJobName, AssetStatsResponseDto, DownloadResponseDto } from './dto';
|
||||||
import { mapAsset } from './response-dto';
|
import { mapAsset } from './response-dto';
|
||||||
|
|
||||||
const downloadResponse: DownloadResponseDto = {
|
const downloadResponse: DownloadResponseDto = {
|
||||||
|
|
@ -145,6 +147,7 @@ describe(AssetService.name, () => {
|
||||||
let accessMock: IAccessRepositoryMock;
|
let accessMock: IAccessRepositoryMock;
|
||||||
let assetMock: jest.Mocked<IAssetRepository>;
|
let assetMock: jest.Mocked<IAssetRepository>;
|
||||||
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
||||||
|
let jobMock: jest.Mocked<IJobRepository>;
|
||||||
let storageMock: jest.Mocked<IStorageRepository>;
|
let storageMock: jest.Mocked<IStorageRepository>;
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
|
|
@ -155,8 +158,9 @@ describe(AssetService.name, () => {
|
||||||
accessMock = newAccessRepositoryMock();
|
accessMock = newAccessRepositoryMock();
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
|
jobMock = newJobRepositoryMock();
|
||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
sut = new AssetService(accessMock, assetMock, cryptoMock, storageMock);
|
sut = new AssetService(accessMock, assetMock, cryptoMock, jobMock, storageMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canUpload', () => {
|
describe('canUpload', () => {
|
||||||
|
|
@ -532,4 +536,24 @@ describe(AssetService.name, () => {
|
||||||
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('run', () => {
|
||||||
|
it('should run the refresh metadata job', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA }),
|
||||||
|
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run the refresh thumbnails job', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }),
|
||||||
|
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run the transcode video', async () => {
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.TRANSCODE_VIDEO }),
|
||||||
|
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,14 @@ import { AuthUserDto } from '../auth';
|
||||||
import { ICryptoRepository } from '../crypto';
|
import { ICryptoRepository } from '../crypto';
|
||||||
import { mimeTypes } from '../domain.constant';
|
import { mimeTypes } from '../domain.constant';
|
||||||
import { HumanReadableSize, usePagination } from '../domain.util';
|
import { HumanReadableSize, usePagination } from '../domain.util';
|
||||||
|
import { IJobRepository, JobName } from '../job';
|
||||||
import { ImmichReadStream, IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
import { ImmichReadStream, IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
||||||
import { IAssetRepository } from './asset.repository';
|
import { IAssetRepository } from './asset.repository';
|
||||||
import {
|
import {
|
||||||
AssetBulkUpdateDto,
|
AssetBulkUpdateDto,
|
||||||
AssetIdsDto,
|
AssetIdsDto,
|
||||||
|
AssetJobName,
|
||||||
|
AssetJobsDto,
|
||||||
DownloadArchiveInfo,
|
DownloadArchiveInfo,
|
||||||
DownloadInfoDto,
|
DownloadInfoDto,
|
||||||
DownloadResponseDto,
|
DownloadResponseDto,
|
||||||
|
|
@ -54,6 +57,7 @@ export class AssetService {
|
||||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
) {
|
) {
|
||||||
this.access = new AccessCore(accessRepository);
|
this.access = new AccessCore(accessRepository);
|
||||||
|
|
@ -275,4 +279,24 @@ export class AssetService {
|
||||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
|
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
|
||||||
await this.assetRepository.updateAll(ids, options);
|
await this.assetRepository.updateAll(ids, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async run(authUser: AuthUserDto, dto: AssetJobsDto) {
|
||||||
|
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, dto.assetIds);
|
||||||
|
|
||||||
|
for (const id of dto.assetIds) {
|
||||||
|
switch (dto.name) {
|
||||||
|
case AssetJobName.REFRESH_METADATA:
|
||||||
|
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id } });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AssetJobName.REGENERATE_THUMBNAIL:
|
||||||
|
await this.jobRepository.queue({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id } });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AssetJobName.TRANSCODE_VIDEO:
|
||||||
|
await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,20 @@
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEnum } from 'class-validator';
|
||||||
import { ValidateUUID } from '../../domain.util';
|
import { ValidateUUID } from '../../domain.util';
|
||||||
|
|
||||||
export class AssetIdsDto {
|
export class AssetIdsDto {
|
||||||
@ValidateUUID({ each: true })
|
@ValidateUUID({ each: true })
|
||||||
assetIds!: string[];
|
assetIds!: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AssetJobName {
|
||||||
|
REGENERATE_THUMBNAIL = 'regenerate-thumbnail',
|
||||||
|
REFRESH_METADATA = 'refresh-metadata',
|
||||||
|
TRANSCODE_VIDEO = 'transcode-video',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AssetJobsDto extends AssetIdsDto {
|
||||||
|
@ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName })
|
||||||
|
@IsEnum(AssetJobName)
|
||||||
|
name!: AssetJobName;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
AssetBulkUpdateDto,
|
AssetBulkUpdateDto,
|
||||||
AssetIdsDto,
|
AssetIdsDto,
|
||||||
|
AssetJobsDto,
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
AssetService,
|
AssetService,
|
||||||
AssetStatsDto,
|
AssetStatsDto,
|
||||||
|
|
@ -78,6 +79,12 @@ export class AssetController {
|
||||||
return this.service.getByTimeBucket(authUser, dto);
|
return this.service.getByTimeBucket(authUser, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('jobs')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
runAssetJobs(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetJobsDto): Promise<void> {
|
||||||
|
return this.service.run(authUser, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Put()
|
@Put()
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
updateAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
|
updateAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
APIKeyApi,
|
APIKeyApi,
|
||||||
AssetApi,
|
AssetApi,
|
||||||
AssetApiFp,
|
AssetApiFp,
|
||||||
|
AssetJobName,
|
||||||
AuthenticationApi,
|
AuthenticationApi,
|
||||||
Configuration,
|
Configuration,
|
||||||
ConfigurationParameters,
|
ConfigurationParameters,
|
||||||
|
|
@ -120,6 +121,26 @@ export class ImmichApi {
|
||||||
|
|
||||||
return names[jobName];
|
return names[jobName];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAssetJobName(job: AssetJobName) {
|
||||||
|
const names: Record<AssetJobName, string> = {
|
||||||
|
[AssetJobName.RefreshMetadata]: 'Refresh metadata',
|
||||||
|
[AssetJobName.RegenerateThumbnail]: 'Refresh thumbnails',
|
||||||
|
[AssetJobName.TranscodeVideo]: 'Refresh encoded videos',
|
||||||
|
};
|
||||||
|
|
||||||
|
return names[job];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAssetJobMessage(job: AssetJobName) {
|
||||||
|
const messages: Record<AssetJobName, string> = {
|
||||||
|
[AssetJobName.RefreshMetadata]: 'Refreshing metadata',
|
||||||
|
[AssetJobName.RegenerateThumbnail]: `Regenerating thumbnails`,
|
||||||
|
[AssetJobName.TranscodeVideo]: `Refreshing encoded video`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return messages[job];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = new ImmichApi({ basePath: '/api' });
|
export const api = new ImmichApi({ basePath: '/api' });
|
||||||
|
|
|
||||||
124
web/src/api/open-api/api.ts
generated
124
web/src/api/open-api/api.ts
generated
|
|
@ -525,6 +525,42 @@ export const AssetIdsResponseDtoErrorEnum = {
|
||||||
|
|
||||||
export type AssetIdsResponseDtoErrorEnum = typeof AssetIdsResponseDtoErrorEnum[keyof typeof AssetIdsResponseDtoErrorEnum];
|
export type AssetIdsResponseDtoErrorEnum = typeof AssetIdsResponseDtoErrorEnum[keyof typeof AssetIdsResponseDtoErrorEnum];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const AssetJobName = {
|
||||||
|
RegenerateThumbnail: 'regenerate-thumbnail',
|
||||||
|
RefreshMetadata: 'refresh-metadata',
|
||||||
|
TranscodeVideo: 'transcode-video'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AssetJobName = typeof AssetJobName[keyof typeof AssetJobName];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface AssetJobsDto
|
||||||
|
*/
|
||||||
|
export interface AssetJobsDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof AssetJobsDto
|
||||||
|
*/
|
||||||
|
'assetIds': Array<string>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {AssetJobName}
|
||||||
|
* @memberof AssetJobsDto
|
||||||
|
*/
|
||||||
|
'name': AssetJobName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
|
@ -5784,6 +5820,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AssetJobsDto} assetJobsDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
runAssetJobs: async (assetJobsDto: AssetJobsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'assetJobsDto' is not null or undefined
|
||||||
|
assertParamExists('runAssetJobs', 'assetJobsDto', assetJobsDto)
|
||||||
|
const localVarPath = `/asset/jobs`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication cookie required
|
||||||
|
|
||||||
|
// authentication api_key required
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
localVarRequestOptions.data = serializeDataIfNeeded(assetJobsDto, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {SearchAssetDto} searchAssetDto
|
* @param {SearchAssetDto} searchAssetDto
|
||||||
|
|
@ -6331,6 +6411,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.importFile(importAssetDto, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.importFile(importAssetDto, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AssetJobsDto} assetJobsDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async runAssetJobs(assetJobsDto: AssetJobsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.runAssetJobs(assetJobsDto, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {SearchAssetDto} searchAssetDto
|
* @param {SearchAssetDto} searchAssetDto
|
||||||
|
|
@ -6584,6 +6674,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||||
importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig): AxiosPromise<AssetFileUploadResponseDto> {
|
importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig): AxiosPromise<AssetFileUploadResponseDto> {
|
||||||
return localVarFp.importFile(requestParameters.importAssetDto, options).then((request) => request(axios, basePath));
|
return localVarFp.importFile(requestParameters.importAssetDto, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AssetApiRunAssetJobsRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
runAssetJobs(requestParameters: AssetApiRunAssetJobsRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
|
||||||
|
return localVarFp.runAssetJobs(requestParameters.assetJobsDto, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {AssetApiSearchAssetRequest} requestParameters Request parameters.
|
* @param {AssetApiSearchAssetRequest} requestParameters Request parameters.
|
||||||
|
|
@ -7066,6 +7165,20 @@ export interface AssetApiImportFileRequest {
|
||||||
readonly importAssetDto: ImportAssetDto
|
readonly importAssetDto: ImportAssetDto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for runAssetJobs operation in AssetApi.
|
||||||
|
* @export
|
||||||
|
* @interface AssetApiRunAssetJobsRequest
|
||||||
|
*/
|
||||||
|
export interface AssetApiRunAssetJobsRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {AssetJobsDto}
|
||||||
|
* @memberof AssetApiRunAssetJobs
|
||||||
|
*/
|
||||||
|
readonly assetJobsDto: AssetJobsDto
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request parameters for searchAsset operation in AssetApi.
|
* Request parameters for searchAsset operation in AssetApi.
|
||||||
* @export
|
* @export
|
||||||
|
|
@ -7472,6 +7585,17 @@ export class AssetApi extends BaseAPI {
|
||||||
return AssetApiFp(this.configuration).importFile(requestParameters.importAssetDto, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).importFile(requestParameters.importAssetDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AssetApiRunAssetJobsRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof AssetApi
|
||||||
|
*/
|
||||||
|
public runAssetJobs(requestParameters: AssetApiRunAssetJobsRequest, options?: AxiosRequestConfig) {
|
||||||
|
return AssetApiFp(this.configuration).runAssetJobs(requestParameters.assetJobsDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {AssetApiSearchAssetRequest} requestParameters Request parameters.
|
* @param {AssetApiSearchAssetRequest} requestParameters Request parameters.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { clickOutside } from '$lib/utils/click-outside';
|
import { clickOutside } from '$lib/utils/click-outside';
|
||||||
import type { AssetResponseDto } from '@api';
|
import { AssetJobName, AssetResponseDto, AssetTypeEnum, api } from '@api';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||||
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
||||||
|
|
@ -29,7 +29,22 @@
|
||||||
|
|
||||||
const isOwner = asset.ownerId === $page.data.user?.id;
|
const isOwner = asset.ownerId === $page.data.user?.id;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
type MenuItemEvent = 'addToAlbum' | 'addToSharedAlbum' | 'asProfileImage' | 'runJob';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
goBack: void;
|
||||||
|
stopMotionPhoto: void;
|
||||||
|
playMotionPhoto: void;
|
||||||
|
download: void;
|
||||||
|
showDetail: void;
|
||||||
|
favorite: void;
|
||||||
|
delete: void;
|
||||||
|
toggleArchive: void;
|
||||||
|
addToAlbum: void;
|
||||||
|
addToSharedAlbum: void;
|
||||||
|
asProfileImage: void;
|
||||||
|
runJob: AssetJobName;
|
||||||
|
}>();
|
||||||
|
|
||||||
let contextMenuPosition = { x: 0, y: 0 };
|
let contextMenuPosition = { x: 0, y: 0 };
|
||||||
let isShowAssetOptions = false;
|
let isShowAssetOptions = false;
|
||||||
|
|
@ -39,7 +54,12 @@
|
||||||
isShowAssetOptions = !isShowAssetOptions;
|
isShowAssetOptions = !isShowAssetOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMenuClick = (eventName: string) => {
|
const onJobClick = (name: AssetJobName) => {
|
||||||
|
isShowAssetOptions = false;
|
||||||
|
dispatch('runJob', name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMenuClick = (eventName: MenuItemEvent) => {
|
||||||
isShowAssetOptions = false;
|
isShowAssetOptions = false;
|
||||||
dispatch(eventName);
|
dispatch(eventName);
|
||||||
};
|
};
|
||||||
|
|
@ -114,22 +134,35 @@
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<CircleIconButton isOpacity={true} logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
|
<CircleIconButton isOpacity={true} logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
|
||||||
<div use:clickOutside on:outclick={() => (isShowAssetOptions = false)}>
|
<div use:clickOutside on:outclick={() => (isShowAssetOptions = false)}>
|
||||||
<CircleIconButton isOpacity={true} logo={DotsVertical} on:click={showOptionsMenu} title="More">
|
<CircleIconButton isOpacity={true} logo={DotsVertical} on:click={showOptionsMenu} title="More" />
|
||||||
{#if isShowAssetOptions}
|
{#if isShowAssetOptions}
|
||||||
<ContextMenu {...contextMenuPosition} direction="left">
|
<ContextMenu {...contextMenuPosition} direction="left">
|
||||||
<MenuOption on:click={() => onMenuClick('addToAlbum')} text="Add to Album" />
|
<MenuOption on:click={() => onMenuClick('addToAlbum')} text="Add to Album" />
|
||||||
<MenuOption on:click={() => onMenuClick('addToSharedAlbum')} text="Add to Shared Album" />
|
<MenuOption on:click={() => onMenuClick('addToSharedAlbum')} text="Add to Shared Album" />
|
||||||
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
|
<MenuOption
|
||||||
|
on:click={() => dispatch('toggleArchive')}
|
||||||
|
text={asset.isArchived ? 'Unarchive' : 'Archive'}
|
||||||
|
/>
|
||||||
|
<MenuOption on:click={() => onMenuClick('asProfileImage')} text="As profile picture" />
|
||||||
|
<MenuOption
|
||||||
|
on:click={() => onJobClick(AssetJobName.RefreshMetadata)}
|
||||||
|
text={api.getAssetJobName(AssetJobName.RefreshMetadata)}
|
||||||
|
/>
|
||||||
|
<MenuOption
|
||||||
|
on:click={() => onJobClick(AssetJobName.RegenerateThumbnail)}
|
||||||
|
text={api.getAssetJobName(AssetJobName.RegenerateThumbnail)}
|
||||||
|
/>
|
||||||
|
{#if asset.type === AssetTypeEnum.Video}
|
||||||
<MenuOption
|
<MenuOption
|
||||||
on:click={() => dispatch('toggleArchive')}
|
on:click={() => onJobClick(AssetJobName.TranscodeVideo)}
|
||||||
text={asset.isArchived ? 'Unarchive' : 'Archive'}
|
text={api.getAssetJobName(AssetJobName.TranscodeVideo)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<MenuOption on:click={() => onMenuClick('asProfileImage')} text="As profile picture" />
|
{/if}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
{/if}
|
{/if}
|
||||||
</CircleIconButton>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { AlbumResponseDto, api, AssetResponseDto, AssetTypeEnum, SharedLinkResponseDto } from '@api';
|
import { AlbumResponseDto, api, AssetJobName, AssetResponseDto, AssetTypeEnum, SharedLinkResponseDto } from '@api';
|
||||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||||
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
||||||
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
||||||
|
|
@ -245,6 +245,15 @@
|
||||||
return 'Asset';
|
return 'Asset';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRunJob = async (name: AssetJobName) => {
|
||||||
|
try {
|
||||||
|
await api.assetApi.runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } });
|
||||||
|
notificationController.show({ type: NotificationType.Info, message: api.getAssetJobMessage(name) });
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, `Unable to submit job`);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
|
|
@ -270,6 +279,7 @@
|
||||||
on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)}
|
on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)}
|
||||||
on:toggleArchive={toggleArchive}
|
on:toggleArchive={toggleArchive}
|
||||||
on:asProfileImage={() => (isShowProfileImageCrop = true)}
|
on:asProfileImage={() => (isShowProfileImageCrop = true)}
|
||||||
|
on:runJob={({ detail: job }) => handleRunJob(job)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
|
import {
|
||||||
|
NotificationType,
|
||||||
|
notificationController,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { AssetJobName, AssetTypeEnum, api } from '@api';
|
||||||
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
|
||||||
|
export let jobs: AssetJobName[] = [
|
||||||
|
AssetJobName.RegenerateThumbnail,
|
||||||
|
AssetJobName.RefreshMetadata,
|
||||||
|
AssetJobName.TranscodeVideo,
|
||||||
|
];
|
||||||
|
|
||||||
|
const { getAssets, clearSelect } = getAssetControlContext();
|
||||||
|
|
||||||
|
$: isAllVideos = Array.from(getAssets()).every((asset) => asset.type === AssetTypeEnum.Video);
|
||||||
|
|
||||||
|
const handleRunJob = async (name: AssetJobName) => {
|
||||||
|
try {
|
||||||
|
const ids = Array.from(getAssets()).map(({ id }) => id);
|
||||||
|
await api.assetApi.runAssetJobs({ assetJobsDto: { assetIds: ids, name } });
|
||||||
|
notificationController.show({ message: api.getAssetJobMessage(name), type: NotificationType.Info });
|
||||||
|
clearSelect();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Unable to submit job');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each jobs as job}
|
||||||
|
{#if isAllVideos || job !== AssetJobName.TranscodeVideo}
|
||||||
|
<MenuOption text={api.getAssetJobName(job)} on:click={() => handleRunJob(job)} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||||
|
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
||||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||||
|
|
@ -52,6 +53,7 @@
|
||||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} />
|
<ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} />
|
||||||
|
<AssetJobActions />
|
||||||
</AssetSelectContextMenu>
|
</AssetSelectContextMenu>
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue