1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2024-11-03 21:04:03 +08:00

Merge thumbnail and photo preview (generated photo) handling #806

This commit is contained in:
Patrik J. Braun 2024-01-03 11:06:19 +01:00
parent 0395fa87ff
commit 13e828d210
26 changed files with 530 additions and 651 deletions

View File

@ -1,46 +0,0 @@
import {NextFunction, Request, Response} from 'express';
import * as fs from 'fs';
import {PhotoProcessing} from '../../model/fileaccess/fileprocessing/PhotoProcessing';
import {Config} from '../../../common/config/private/Config';
import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error';
export class PhotoConverterMWs {
public static async convertPhoto(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
if (!req.resultPipe) {
return next();
}
// if conversion is not enabled redirect, so browser can cache the full
if (Config.Media.Photo.Converting.enabled === false) {
return res.redirect(req.originalUrl.slice(0, -1 * '\\bestFit'.length));
}
const fullMediaPath = req.resultPipe as string;
const convertedVideo = PhotoProcessing.generateConvertedPath(
fullMediaPath,
Config.Media.Photo.Converting.resolution
);
// check if converted photo exist
if (fs.existsSync(convertedVideo) === true) {
req.resultPipe = convertedVideo;
return next();
}
if (Config.Media.Photo.Converting.onTheFly === true) {
try {
req.resultPipe = await PhotoProcessing.convertPhoto(fullMediaPath);
} catch (err) {
return next(new ErrorDTO(ErrorCodes.PHOTO_GENERATION_ERROR, err.message));
}
return next();
}
// not converted and won't be now
return res.redirect(req.originalUrl.slice(0, -1 * '\\bestFit'.length));
}
}

View File

@ -14,7 +14,7 @@ import {PersonEntry} from '../../model/database/enitites/PersonEntry';
export class ThumbnailGeneratorMWs {
private static ThumbnailMapEntries =
Config.Media.Thumbnail.generateThumbnailMapEntries();
Config.Media.Photo.generateThumbnailMapEntries();
@ServerTime('2.th', 'Thumbnail decoration')
public static async addThumbnailInformation(
@ -34,7 +34,7 @@ export class ThumbnailGeneratorMWs {
// regenerate in case the list change since startup
ThumbnailGeneratorMWs.ThumbnailMapEntries =
Config.Media.Thumbnail.generateThumbnailMapEntries();
Config.Media.Photo.generateThumbnailMapEntries();
if (cw.directory) {
ThumbnailGeneratorMWs.addThInfoTODir(cw.directory);
}
@ -67,7 +67,7 @@ export class ThumbnailGeneratorMWs {
let erroredItem: PersonEntry = null;
try {
const size: number = Config.Media.Thumbnail.personThumbnailSize;
const size: number = Config.Media.Photo.personThumbnailSize;
const persons: PersonEntry[] = req.resultPipe as PersonEntry[];
@ -147,11 +147,11 @@ export class ThumbnailGeneratorMWs {
const mediaPath = req.resultPipe as string;
let size: number =
parseInt(req.params.size, 10) ||
Config.Media.Thumbnail.thumbnailSizes[0];
Config.Media.Photo.thumbnailSizes[0];
// validate size
if (Config.Media.Thumbnail.thumbnailSizes.indexOf(size) === -1) {
size = Config.Media.Thumbnail.thumbnailSizes[0];
if (Config.Media.Photo.thumbnailSizes.indexOf(size) === -1) {
size = Config.Media.Photo.thumbnailSizes[0];
}
try {
@ -188,7 +188,7 @@ export class ThumbnailGeneratorMWs {
// load parameters
const mediaPath = req.resultPipe as string;
const size: number = Config.Media.Thumbnail.iconSize;
const size: number = Config.Media.Photo.iconSize;
try {
req.resultPipe = await PhotoProcessing.generateThumbnail(

View File

@ -20,12 +20,15 @@ import {
ServerAlbumCoverConfig,
ServerDataBaseConfig,
ServerJobConfig,
ServerThumbnailConfig,
ServerPhotoConfig,
ServerVideoConfig,
} from '../../../common/config/private/PrivateConfig';
import {SearchQueryParser} from '../../../common/SearchQueryParser';
import {SearchQueryTypes, TextSearch,} from '../../../common/entities/SearchQueryDTO';
import {Utils} from '../../../common/Utils';
import {JobRepository} from '../jobs/JobRepository';
import {ExtensionConfig} from '../extension/ExtensionConfigWrapper';
import {ConfigClassBuilder} from '../../../../node_modules/typeconfig/node';
const LOG_TAG = '[ConfigDiagnostics]';
@ -71,6 +74,18 @@ export class ConfigDiagnostics {
}
}
static async testJobsConfig(
jobsConfig: ServerJobConfig
): Promise<void> {
Logger.debug(LOG_TAG, 'Testing jobs config');
for(let i = 0; i< jobsConfig.scheduled.length; ++i){
const j = jobsConfig.scheduled[i];
if(!JobRepository.Instance.exists(j.name)){
throw new Error('Unknown Job :' + j.name);
}
}
}
static async testMetaFileConfig(
metaFileConfig: ClientMetaFileConfig,
config: PrivateConfigClass
@ -157,26 +172,26 @@ export class ConfigDiagnostics {
}
static async testThumbnailConfig(
thumbnailConfig: ServerThumbnailConfig
static async testPhotoConfig(
photoConfig: ServerPhotoConfig
): Promise<void> {
Logger.debug(LOG_TAG, 'Testing thumbnail config');
if (thumbnailConfig.personFaceMargin < 0 || thumbnailConfig.personFaceMargin > 1) {
if (photoConfig.personFaceMargin < 0 || photoConfig.personFaceMargin > 1) {
throw new Error('personFaceMargin should be between 0 and 1');
}
if (isNaN(thumbnailConfig.iconSize) || thumbnailConfig.iconSize <= 0) {
if (isNaN(photoConfig.iconSize) || photoConfig.iconSize <= 0) {
throw new Error(
'IconSize has to be >= 0 integer, got: ' + thumbnailConfig.iconSize
'IconSize has to be >= 0 integer, got: ' + photoConfig.iconSize
);
}
if (!thumbnailConfig.thumbnailSizes.length) {
if (!photoConfig.thumbnailSizes.length) {
throw new Error('At least one thumbnail size is needed');
}
for (const item of thumbnailConfig.thumbnailSizes) {
for (const item of photoConfig.thumbnailSizes) {
if (isNaN(item) || item <= 0) {
throw new Error('Thumbnail size has to be >= 0 integer, got: ' + item);
}
@ -286,7 +301,7 @@ export class ConfigDiagnostics {
await ConfigDiagnostics.testMetaFileConfig(config.MetaFile, config);
await ConfigDiagnostics.testAlbumsConfig(config.Album, config);
await ConfigDiagnostics.testImageFolder(config.Media.folder);
await ConfigDiagnostics.testThumbnailConfig(config.Media.Thumbnail);
await ConfigDiagnostics.testPhotoConfig(config.Media.Photo);
await ConfigDiagnostics.testSearchConfig(config.Search, config);
await ConfigDiagnostics.testAlbumCoverConfig(config.AlbumCover);
await ConfigDiagnostics.testFacesConfig(config.Faces, config);
@ -294,6 +309,7 @@ export class ConfigDiagnostics {
await ConfigDiagnostics.testSharingConfig(config.Sharing, config);
await ConfigDiagnostics.testRandomPhotoConfig(config.Sharing, config);
await ConfigDiagnostics.testMapConfig(config.Map);
await ConfigDiagnostics.testJobsConfig(config.Jobs);
}
@ -403,8 +419,8 @@ export class ConfigDiagnostics {
Logger.error(LOG_TAG, 'Images folder error', err.toString());
}
try {
await ConfigDiagnostics.testThumbnailConfig(
Config.Media.Thumbnail
await ConfigDiagnostics.testPhotoConfig(
Config.Media.Photo
);
} catch (ex) {
const err: Error = ex;
@ -538,6 +554,27 @@ export class ConfigDiagnostics {
}
try {
await ConfigDiagnostics.testJobsConfig(
Config.Jobs,
);
} catch (ex) {
const err: Error = ex;
NotificationManager.warning(
'Jobs error. Resetting to default for now to let the app start up. ' +
'Please adjust the config properly.',
err.toString()
);
Logger.warn(
LOG_TAG,
'Jobs error. Resetting to default for now to let the app start up. ' +
'Please adjust the config properly.',
err.toString()
);
const pc = ConfigClassBuilder.attachPrivateInterface(new PrivateConfigClass());
Config.Jobs.scheduled = pc.Jobs.scheduled;
}
}
}

View File

@ -21,13 +21,13 @@ export class PhotoProcessing {
return;
}
Config.Media.Thumbnail.concurrentThumbnailGenerations = Math.max(
Config.Media.Photo.concurrentThumbnailGenerations = Math.max(
1,
os.cpus().length - 1
);
this.taskQue = new TaskExecuter(
Config.Media.Thumbnail.concurrentThumbnailGenerations,
Config.Media.Photo.concurrentThumbnailGenerations,
(input): Promise<void> => PhotoWorker.render(input)
);
@ -45,7 +45,7 @@ export class PhotoProcessing {
photo.directory.name,
photo.name
);
const size: number = Config.Media.Thumbnail.personThumbnailSize;
const size: number = Config.Media.Photo.personThumbnailSize;
const faceRegion = person.sampleRegion.media.metadata.faces.find(f => f.name === person.name);
// generate thumbnail path
const thPath = PhotoProcessing.generatePersonThumbnailPath(
@ -65,11 +65,11 @@ export class PhotoProcessing {
const margin = {
x: Math.round(
faceRegion.box.width *
Config.Media.Thumbnail.personFaceMargin
Config.Media.Photo.personFaceMargin
),
y: Math.round(
faceRegion.box.height *
Config.Media.Thumbnail.personFaceMargin
Config.Media.Photo.personFaceMargin
),
};
@ -90,9 +90,9 @@ export class PhotoProcessing {
width: faceRegion.box.width + margin.x,
height: faceRegion.box.height + margin.y,
},
useLanczos3: Config.Media.Thumbnail.useLanczos3,
quality: Config.Media.Thumbnail.quality,
smartSubsample: Config.Media.Thumbnail.smartSubsample,
useLanczos3: Config.Media.Photo.useLanczos3,
quality: Config.Media.Photo.quality,
smartSubsample: Config.Media.Photo.smartSubsample,
} as MediaRendererInput;
input.cut.width = Math.min(
input.cut.width,
@ -110,13 +110,13 @@ export class PhotoProcessing {
public static generateConvertedPath(mediaPath: string, size: number): string {
const file = path.basename(mediaPath);
const animated = Config.Media.Thumbnail.animateGif && path.extname(mediaPath).toLowerCase() == '.gif';
const animated = Config.Media.Photo.animateGif && path.extname(mediaPath).toLowerCase() == '.gif';
return path.join(
ProjectPath.TranscodedFolder,
ProjectPath.getRelativePathToImages(path.dirname(mediaPath)),
file + '_' + size + 'q' + Config.Media.Thumbnail.quality +
file + '_' + size + 'q' + Config.Media.Photo.quality +
(animated ? 'anim' : '') +
(Config.Media.Thumbnail.smartSubsample ? 'cs' : '') +
(Config.Media.Photo.smartSubsample ? 'cs' : '') +
PhotoProcessing.CONVERTED_EXTENSION
);
}
@ -142,7 +142,7 @@ export class PhotoProcessing {
.digest('hex') +
'_' +
size +
'_' + Config.Media.Thumbnail.personFaceMargin +
'_' + Config.Media.Photo.personFaceMargin +
PhotoProcessing.CONVERTED_EXTENSION
);
}
@ -177,8 +177,7 @@ export class PhotoProcessing {
if (
(size + '').length !== sizeStr.length ||
(Config.Media.Thumbnail.thumbnailSizes.indexOf(size) === -1 &&
Config.Media.Photo.Converting.resolution !== size)
(Config.Media.Photo.thumbnailSizes.indexOf(size) === -1)
) {
return false;
}
@ -189,7 +188,7 @@ export class PhotoProcessing {
const quality = parseInt(qualityStr, 10);
if ((quality + '').length !== qualityStr.length ||
quality !== Config.Media.Thumbnail.quality) {
quality !== Config.Media.Photo.quality) {
return false;
}
@ -198,7 +197,7 @@ export class PhotoProcessing {
const lowerExt = path.extname(origFilePath).toLowerCase();
const shouldBeAnimated = Config.Media.Thumbnail.animateGif && lowerExt == '.gif';
const shouldBeAnimated = Config.Media.Photo.animateGif && lowerExt == '.gif';
if (shouldBeAnimated) {
if (convertedPath.substring(
nextIndex,
@ -210,7 +209,7 @@ export class PhotoProcessing {
}
if (Config.Media.Thumbnail.smartSubsample) {
if (Config.Media.Photo.smartSubsample) {
if (convertedPath.substring(
nextIndex,
nextIndex + 2
@ -236,14 +235,6 @@ export class PhotoProcessing {
return true;
}
public static async convertPhoto(mediaPath: string): Promise<string> {
return this.generateThumbnail(
mediaPath,
Config.Media.Photo.Converting.resolution,
ThumbnailSourceType.Photo,
false
);
}
static async convertedPhotoExist(
mediaPath: string,
@ -287,9 +278,9 @@ export class PhotoProcessing {
size,
outPath,
makeSquare,
useLanczos3: Config.Media.Thumbnail.useLanczos3,
quality: Config.Media.Thumbnail.quality,
smartSubsample: Config.Media.Thumbnail.smartSubsample,
useLanczos3: Config.Media.Photo.useLanczos3,
quality: Config.Media.Photo.quality,
smartSubsample: Config.Media.Photo.smartSubsample,
} as MediaRendererInput;
const outDir = path.dirname(input.outPath);
@ -328,9 +319,9 @@ viewBox="${svgIcon.viewBox || '0 0 512 512'}">d="${svgIcon.items}</svg>`,
outPath,
makeSquare: false,
animate: false,
useLanczos3: Config.Media.Thumbnail.useLanczos3,
quality: Config.Media.Thumbnail.quality,
smartSubsample: Config.Media.Thumbnail.smartSubsample,
useLanczos3: Config.Media.Photo.useLanczos3,
quality: Config.Media.Photo.quality,
smartSubsample: Config.Media.Photo.smartSubsample,
} as SvgRendererInput;
const outDir = path.dirname(input.outPath);

View File

@ -3,7 +3,6 @@ import {IndexingJob} from './jobs/IndexingJob';
import {GalleryRestJob} from './jobs/GalleryResetJob';
import {VideoConvertingJob} from './jobs/VideoConvertingJob';
import {PhotoConvertingJob} from './jobs/PhotoConvertingJob';
import {ThumbnailGenerationJob} from './jobs/ThumbnailGenerationJob';
import {TempFolderCleaningJob} from './jobs/TempFolderCleaningJob';
import {AlbumCoverFillingJob} from './jobs/AlbumCoverFillingJob';
import {GPXCompressionJob} from './jobs/GPXCompressionJob';
@ -33,6 +32,10 @@ export class JobRepository {
}
this.availableJobs[job.Name] = job;
}
exists(name: string) {
return !!this.availableJobs[name];
}
}
JobRepository.Instance.register(new IndexingJob());
@ -41,7 +44,6 @@ JobRepository.Instance.register(new AlbumCoverFillingJob());
JobRepository.Instance.register(new AlbumCoverRestJob());
JobRepository.Instance.register(new VideoConvertingJob());
JobRepository.Instance.register(new PhotoConvertingJob());
JobRepository.Instance.register(new ThumbnailGenerationJob());
JobRepository.Instance.register(new GPXCompressionJob());
JobRepository.Instance.register(new TempFolderCleaningJob());
JobRepository.Instance.register(new AlbumRestJob());

View File

@ -2,26 +2,94 @@ import {Config} from '../../../../common/config/private/Config';
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
import {FileJob} from './FileJob';
import {PhotoProcessing} from '../../fileaccess/fileprocessing/PhotoProcessing';
import {ThumbnailSourceType} from '../../fileaccess/PhotoWorker';
import {MediaDTOUtils} from '../../../../common/entities/MediaDTO';
import {FileDTO} from '../../../../common/entities/FileDTO';
import {backendTexts} from '../../../../common/BackendTexts';
export class PhotoConvertingJob extends FileJob {
export class PhotoConvertingJob extends FileJob<{
sizes?: number[];
maxVideoSize?: number;
indexedOnly?: boolean;
}> {
public readonly Name = DefaultsJobs[DefaultsJobs['Photo Converting']];
constructor() {
super({noVideo: true, noMetaFile: true});
super({noMetaFile: true});
this.ConfigTemplate.push({
id: 'sizes',
type: 'number-array',
name: backendTexts.sizeToGenerate.name,
description: backendTexts.sizeToGenerate.description,
defaultValue: [Config.Media.Photo.thumbnailSizes[0]],
}, {
id: 'maxVideoSize',
type: 'number',
name: backendTexts.maxVideoSize.name,
description: backendTexts.maxVideoSize.description,
defaultValue: 800,
});
}
public get Supported(): boolean {
return Config.Media.Photo.Converting.enabled === true;
return true;
}
start(
config: { sizes?: number[]; indexedOnly?: boolean },
soloRun = false,
allowParallelRun = false
): Promise<void> {
if (!config || !config.sizes || !Array.isArray(config.sizes) || config.sizes.length === 0) {
config = config || {};
config.sizes = this.ConfigTemplate.find(ct => ct.id == 'sizes').defaultValue as number[];
}
for (const item of config.sizes) {
if (Config.Media.Photo.thumbnailSizes.indexOf(item) === -1) {
throw new Error(
'unknown thumbnails size: ' +
item +
'. Add it to the possible thumbnail sizes.'
);
}
}
return super.start(config, soloRun, allowParallelRun);
}
protected async filterMediaFiles(files: FileDTO[]): Promise<FileDTO[]> {
return files;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async filterMetaFiles(files: FileDTO[]): Promise<FileDTO[]> {
return undefined;
}
protected async shouldProcess(mPath: string): Promise<boolean> {
return !(await PhotoProcessing.convertedPhotoExist(
mPath,
Config.Media.Photo.Converting.resolution
));
for (const item of this.config.sizes) {
if (!(await PhotoProcessing.convertedPhotoExist(mPath, item))) {
return true;
}
}
return false;
}
protected async processFile(mPath: string): Promise<void> {
await PhotoProcessing.convertPhoto(mPath);
const isVideo = MediaDTOUtils.isVideoPath(mPath);
for (const item of this.config.sizes) {
// skip hig- res photo creation for video files. App does not use photo preview fore videos in the lightbox
if (this.config.maxVideoSize < item && isVideo) {
continue;
}
await PhotoProcessing.generateThumbnail(
mPath,
item,
isVideo
? ThumbnailSourceType.Video
: ThumbnailSourceType.Photo,
false
);
}
}
}

View File

@ -1,83 +0,0 @@
import {Config} from '../../../../common/config/private/Config';
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
import {FileJob} from './FileJob';
import {PhotoProcessing} from '../../fileaccess/fileprocessing/PhotoProcessing';
import {ThumbnailSourceType} from '../../fileaccess/PhotoWorker';
import {MediaDTOUtils} from '../../../../common/entities/MediaDTO';
import {FileDTO} from '../../../../common/entities/FileDTO';
import {backendTexts} from '../../../../common/BackendTexts';
export class ThumbnailGenerationJob extends FileJob<{
sizes?: number[];
indexedOnly?: boolean;
}> {
public readonly Name = DefaultsJobs[DefaultsJobs['Thumbnail Generation']];
constructor() {
super({noMetaFile: true});
this.ConfigTemplate.push({
id: 'sizes',
type: 'number-array',
name: backendTexts.sizeToGenerate.name,
description: backendTexts.sizeToGenerate.description,
defaultValue: [Config.Media.Thumbnail.thumbnailSizes[0]],
});
}
public get Supported(): boolean {
return true;
}
start(
config: { sizes?: number[]; indexedOnly?: boolean },
soloRun = false,
allowParallelRun = false
): Promise<void> {
if (!config || !config.sizes || !Array.isArray(config.sizes) || config.sizes.length === 0) {
config = config || {};
config.sizes = this.ConfigTemplate.find(ct => ct.id == 'sizes').defaultValue as number[];
}
for (const item of config.sizes) {
if (Config.Media.Thumbnail.thumbnailSizes.indexOf(item) === -1) {
throw new Error(
'unknown thumbnails size: ' +
item +
'. Add it to the possible thumbnail sizes.'
);
}
}
return super.start(config, soloRun, allowParallelRun);
}
protected async filterMediaFiles(files: FileDTO[]): Promise<FileDTO[]> {
return files;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async filterMetaFiles(files: FileDTO[]): Promise<FileDTO[]> {
return undefined;
}
protected async shouldProcess(mPath: string): Promise<boolean> {
for (const item of this.config.sizes) {
if (!(await PhotoProcessing.convertedPhotoExist(mPath, item))) {
return true;
}
}
return false;
}
protected async processFile(mPath: string): Promise<void> {
for (const item of this.config.sizes) {
await PhotoProcessing.generateThumbnail(
mPath,
item,
MediaDTOUtils.isVideoPath(mPath)
? ThumbnailSourceType.Video
: ThumbnailSourceType.Photo,
false
);
}
}
}

View File

@ -22,7 +22,7 @@ export abstract class Messenger<C extends Record<string, unknown> = Record<strin
private async getThumbnail(m: MediaDTO) {
return await PhotoProcessing.generateThumbnail(
path.join(ProjectPath.ImageFolder, m.directory.path, m.directory.name, m.name),
Config.Media.Thumbnail.thumbnailSizes[0],
Config.Media.Photo.thumbnailSizes[0],
MediaDTOUtils.isPhoto(m) ? ThumbnailSourceType.Photo : ThumbnailSourceType.Video,
false
);

View File

@ -7,7 +7,6 @@ import {UserRoles} from '../../common/entities/UserDTO';
import {ThumbnailSourceType} from '../model/fileaccess/PhotoWorker';
import {VersionMWs} from '../middlewares/VersionMWs';
import {SupportedFormats} from '../../common/SupportedFormats';
import {PhotoConverterMWs} from '../middlewares/thumbnail/PhotoConverterMWs';
import {ServerTimingMWs} from '../middlewares/ServerTimingMWs';
import {MetaFileMWs} from '../middlewares/MetaFileMWs';
import {Config} from '../../common/config/private/Config';
@ -16,9 +15,8 @@ export class GalleryRouter {
public static route(app: Express): void {
this.addGetImageIcon(app);
this.addGetVideoIcon(app);
this.addGetPhotoThumbnail(app);
this.addGetResizedPhoto(app);
this.addGetVideoThumbnail(app);
this.addGetBestFitImage(app);
this.addGetImage(app);
this.addGetBestFitVideo(app);
this.addGetVideo(app);
@ -83,26 +81,6 @@ export class GalleryRouter {
);
}
protected static addGetBestFitImage(app: Express): void {
app.get(
[
Config.Server.apiPath + '/gallery/content/:mediaPath(*.(' +
SupportedFormats.Photos.join('|') +
'))/bestFit',
],
// common part
AuthenticationMWs.authenticate,
AuthenticationMWs.normalizePathParam('mediaPath'),
AuthenticationMWs.authorisePath('mediaPath', false),
// specific part
GalleryMWs.loadFile,
PhotoConverterMWs.convertPhoto,
ServerTimingMWs.addServerTiming,
RenderingMWs.renderFile
);
}
protected static addGetVideo(app: Express): void {
app.get(
[
@ -197,11 +175,16 @@ export class GalleryRouter {
);
}
protected static addGetPhotoThumbnail(app: Express): void {
/**
* Used for serving photo thumbnails and previews
* @param app
* @protected
*/
protected static addGetResizedPhoto(app: Express): void {
app.get(
Config.Server.apiPath + '/gallery/content/:mediaPath(*.(' +
SupportedFormats.Photos.join('|') +
'))/thumbnail/:size?',
'))/:size',
// common part
AuthenticationMWs.authenticate,
AuthenticationMWs.normalizePathParam('mediaPath'),
@ -219,7 +202,7 @@ export class GalleryRouter {
app.get(
Config.Server.apiPath + '/gallery/content/:mediaPath(*.(' +
SupportedFormats.Videos.join('|') +
'))/thumbnail/:size?',
'))/:size?',
// common part
AuthenticationMWs.authenticate,
AuthenticationMWs.normalizePathParam('mediaPath'),

View File

@ -3,11 +3,12 @@ export type backendText = number;
export const backendTexts = {
indexedFilesOnly: {name: 10, description: 12},
sizeToGenerate: {name: 20, description: 22},
maxVideoSize: {name: 23, description: 24},
indexChangesOnly: {name: 30, description: 32},
mediaPick: {name: 40, description: 42},
emailTo: {name: 70, description: 72},
emailSubject: {name: 90, description: 92},
emailText: {name: 100, description: 102},
messenger: {name: 110,description: 112}
messenger: {name: 110, description: 112}
};

View File

@ -16,11 +16,9 @@ import {
ClientMediaConfig,
ClientMetaFileConfig,
ClientPhotoConfig,
ClientPhotoConvertingConfig,
ClientServiceConfig,
ClientSharingConfig,
ClientSortingConfig,
ClientThumbnailConfig,
ClientUserConfig,
ClientVideoConfig,
ConfigPriority,
@ -299,7 +297,7 @@ export class ServerUserConfig extends ClientUserConfig {
@SubConfigClass({softReadonly: true})
export class ServerThumbnailConfig extends ClientThumbnailConfig {
export class ServerPhotoConfig extends ClientPhotoConfig {
@ConfigProperty({
tags:
{
@ -664,16 +662,10 @@ export class ServerJobConfig {
{}
),
new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
DefaultsJobs[DefaultsJobs['Photo Converting']],
DefaultsJobs[DefaultsJobs['Photo Converting']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Album Cover Filling']]),
{sizes: [240], indexedOnly: true}
),
new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Photo Converting']],
DefaultsJobs[DefaultsJobs['Photo Converting']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]),
{indexedOnly: true}
{sizes: [320], maxVideoSize: 800, indexedOnly: true}
),
new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Video Converting']],
@ -823,44 +815,6 @@ export class ServerVideoConfig extends ClientVideoConfig {
transcoding: VideoTranscodingConfig = new VideoTranscodingConfig();
}
@SubConfigClass({softReadonly: true})
export class PhotoConvertingConfig extends ClientPhotoConvertingConfig {
@ConfigProperty({
tags: {
name: $localize`On the fly converting`,
priority: ConfigPriority.underTheHood,
uiDisabled: (sc: PhotoConvertingConfig) =>
!sc.enabled
},
description: $localize`Converts photos on the fly, when they are requested.`,
})
onTheFly: boolean = true;
@ConfigProperty({
type: 'unsignedInt',
tags: {
name: $localize`Resolution`,
priority: ConfigPriority.advanced,
uiOptions: [720, 1080, 1440, 2160, 4320],
unit: 'px',
uiDisabled: (sc: PhotoConvertingConfig) =>
!sc.enabled
},
description: $localize`The shorter edge of the converted photo will be scaled down to this, while keeping the aspect ratio.`,
})
resolution: videoResolutionType = 1080;
}
@SubConfigClass({softReadonly: true})
export class ServerPhotoConfig extends ClientPhotoConfig {
@ConfigProperty({
tags: {
name: $localize`Photo resizing`,
priority: ConfigPriority.advanced,
}
})
Converting: PhotoConvertingConfig = new PhotoConvertingConfig();
}
@SubConfigClass({softReadonly: true})
export class ServerAlbumCoverConfig {
@ -951,23 +905,10 @@ export class ServerMediaConfig extends ClientMediaConfig {
name: $localize`Photo`,
uiIcon: 'ionCameraOutline',
priority: ConfigPriority.advanced,
uiJob: [
{
job: DefaultsJobs[DefaultsJobs['Photo Converting']],
relevant: (c) => c.Media.Photo.Converting.enabled
}]
uiJob: [{job: DefaultsJobs[DefaultsJobs['Photo Converting']]}]
} as TAGS
})
Photo: ServerPhotoConfig = new ServerPhotoConfig();
@ConfigProperty({
tags: {
name: $localize`Thumbnail`,
uiIcon: 'ionImageOutline',
priority: ConfigPriority.advanced,
uiJob: [{job: DefaultsJobs[DefaultsJobs['Thumbnail Generation']]}]
} as TAGS
})
Thumbnail: ServerThumbnailConfig = new ServerThumbnailConfig();
}
@SubConfigClass({softReadonly: true})

View File

@ -620,60 +620,6 @@ export class ClientMapConfig {
bendLongPathsTrigger: number = 0.5;
}
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientThumbnailConfig {
@ConfigProperty({
type: 'unsignedInt', max: 100,
tags: {
name: $localize`Map Icon size`,
unit: 'px',
priority: ConfigPriority.underTheHood
},
description: $localize`Icon size (used on maps).`,
})
iconSize: number = 45;
@ConfigProperty({
type: 'unsignedInt', tags: {
name: $localize`Person thumbnail size`,
unit: 'px',
priority: ConfigPriority.underTheHood
},
description: $localize`Person (face) thumbnail size.`,
})
personThumbnailSize: number = 200;
@ConfigProperty({
arrayType: 'unsignedInt', tags: {
name: $localize`Thumbnail sizes`,
priority: ConfigPriority.advanced
},
description: $localize`Size of the thumbnails. The best matching size will be generated. More sizes give better quality, but use more storage and CPU to render. If size is 240, that shorter side of the thumbnail will have 160 pixels.`,
})
thumbnailSizes: number[] = [240, 480];
@ConfigProperty({
volatile: true,
description: 'Updated to match he number of CPUs. This manny thumbnail will be concurrently generated.',
})
concurrentThumbnailGenerations: number = 1;
/**
* Generates a map for bitwise operation from icon and normal thumbnails
*/
generateThumbnailMap(): { [key: number]: number } {
const m: { [key: number]: number } = {};
[this.iconSize, ...this.thumbnailSizes.sort()].forEach((v, i) => {
m[v] = Math.pow(2, i + 1);
});
return m;
}
/**
* Generates a map for bitwise operation from icon and normal thumbnails
*/
generateThumbnailMapEntries(): { size: number, bit: number }[] {
return Object.entries(this.generateThumbnailMap()).map(v => ({size: parseInt(v[0]), bit: v[1]}));
}
}
export enum NavigationLinkTypes {
gallery = 1, faces, albums, search, url
}
@ -1004,6 +950,16 @@ export class ClientLightboxConfig {
loopVideos: boolean = false;
@ConfigProperty({
tags: {
name: $localize`Load full resolution image on zoom.`,
priority: ConfigPriority.advanced
},
description: $localize`Enables loading the full resolution image on zoom in the ligthbox (preview).`,
})
loadFullImageOnZoom: boolean = true;
@ConfigProperty({
tags: {
name: $localize`Titles`,
@ -1156,8 +1112,10 @@ export class ClientGalleryConfig {
@ConfigProperty({
tags: {
name: $localize`Lightbox`,
uiIcon: 'ionImageOutline',
priority: ConfigPriority.advanced,
},
} as TAGS,
description: $localize`Photo and video preview window.`
})
Lightbox: ClientLightboxConfig = new ClientLightboxConfig();
@ -1228,37 +1186,65 @@ export class ClientVideoConfig {
}
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientPhotoConvertingConfig {
@ConfigProperty({
tags: {
name: $localize`Enable`
} as TAGS,
description: $localize`Enable photo converting.`
})
enabled: boolean = true;
@ConfigProperty({
tags: {
name: $localize`Load full resolution image on zoom.`,
priority: ConfigPriority.advanced,
uiDisabled: (sc: ClientPhotoConvertingConfig) =>
!sc.enabled
},
description: $localize`Enables loading the full resolution image on zoom in the ligthbox (preview).`,
})
loadFullImageOnZoom: boolean = true;
}
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientPhotoConfig {
@ConfigProperty({
type: 'unsignedInt', max: 100,
tags: {
name: $localize`Photo converting`,
priority: ConfigPriority.advanced
}
name: $localize`Map Icon size`,
unit: 'px',
priority: ConfigPriority.underTheHood
},
description: $localize`Icon size (used on maps).`,
})
Converting: ClientPhotoConvertingConfig = new ClientPhotoConvertingConfig();
iconSize: number = 45;
@ConfigProperty({
type: 'unsignedInt', tags: {
name: $localize`Person thumbnail size`,
unit: 'px',
priority: ConfigPriority.underTheHood
},
description: $localize`Person (face) thumbnail size.`,
})
personThumbnailSize: number = 200;
@ConfigProperty({
arrayType: 'unsignedInt', tags: {
name: $localize`Thumbnail and photo preview sizes`,
priority: ConfigPriority.advanced,
githubIssue: 806
} as TAGS,
description: $localize`Size of the thumbnails and photo previews. The best matching size will be used (smaller for photo and video thumbnail, bigger for photo preview). More sizes give better quality, but use more storage and CPU to render. If size is 240, that shorter side of the thumbnail will be 240 pixels.`,
})
thumbnailSizes: number[] = [320, 540, 1080, 2160];
@ConfigProperty({
volatile: true,
description: 'Updated to match he number of CPUs. This manny thumbnail will be concurrently generated.',
})
concurrentThumbnailGenerations: number = 1;
/**
* Generates a map for bitwise operation from icon and normal thumbnails
*/
generateThumbnailMap(): { [key: number]: number } {
const m: { [key: number]: number } = {};
[this.iconSize, ...this.thumbnailSizes.sort()].forEach((v, i) => {
m[v] = Math.pow(2, i + 1);
});
return m;
}
/**
* Generates a map for bitwise operation from icon and normal thumbnails
*/
generateThumbnailMapEntries(): { size: number, bit: number }[] {
return Object.entries(this.generateThumbnailMap()).map(v => ({size: parseInt(v[0]), bit: v[1]}));
}
@ConfigProperty({
arrayType: 'string',
@ -1288,13 +1274,6 @@ export class ClientGPXCompressingConfig {
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientMediaConfig {
@ConfigProperty({
tags: {
name: $localize`Thumbnail`,
priority: ConfigPriority.advanced
}
})
Thumbnail: ClientThumbnailConfig = new ClientThumbnailConfig();
@ConfigProperty({
tags: {
name: $localize`Video`,

View File

@ -4,8 +4,7 @@ export enum DefaultsJobs {
Indexing = 1,
'Gallery Reset' = 2,
'Video Converting' = 3,
'Photo Converting' = 4,
'Thumbnail Generation' = 5,
'Photo Converting' = 5,
'Temp Folder Cleaning' = 6,
'Album Cover Filling' = 7,
'Album Cover Reset' = 8,

View File

@ -14,6 +14,10 @@ export class BackendtextService {
return $localize`Size to generate`;
case backendTexts.sizeToGenerate.description:
return $localize`These thumbnails will be generated. The list should be a subset of the enabled thumbnail sizes`;
case backendTexts.maxVideoSize.name:
return $localize`Max video size`;
case backendTexts.maxVideoSize.description:
return $localize`Sizes bigger than this value won't be generated for videos. Videos does not use photo based previews, so it is not needed to generate big previews for them.`;
case backendTexts.indexedFilesOnly.name:
return $localize`Indexed only`;
case backendTexts.indexedFilesOnly.description:
@ -58,8 +62,6 @@ export class BackendtextService {
return $localize`Gallery reset`;
case DefaultsJobs['Album Reset']:
return $localize`Album reset`;
case DefaultsJobs['Thumbnail Generation']:
return $localize`Thumbnail generation`;
case DefaultsJobs['Photo Converting']:
return $localize`Photo converting`;
case DefaultsJobs['Video Converting']:
@ -92,10 +94,8 @@ export class BackendtextService {
return $localize`Deletes all directories, photos and videos from the DB.`;
case DefaultsJobs['Album Reset']:
return $localize`Removes all albums from the DB`;
case DefaultsJobs['Thumbnail Generation']:
return $localize`Generates thumbnails from all media files and stores them in the tmp folder.`;
case DefaultsJobs['Photo Converting']:
return $localize`Generates high res photos from all media files and stores them in the tmp folder.`;
return $localize`Generates thumbnails and high-res photos from all media files and stores them in the tmp folder. Smaller sizes will be used for thumbnail (in the grid view), bigger sizes for previews (in the lightbox). Videos does not use photo previews (the app loads the video file instead).`;
case DefaultsJobs['Video Converting']:
return $localize`Transcodes all videos and stores them in the tmp folder.`;
case DefaultsJobs['Temp Folder Cleaning']:

View File

@ -4,8 +4,6 @@ import {Config} from '../../../../common/config/public/Config';
import {MediaDTO, MediaDTOUtils} from '../../../../common/entities/MediaDTO';
export class Media extends MediaIcon {
static readonly sortedThumbnailSizes =
Config.Media.Thumbnail.thumbnailSizes.sort((a, b): number => a - b);
constructor(
media: MediaDTO,
@ -27,8 +25,7 @@ export class Media extends MediaIcon {
}
getThumbnailSize(): number {
const longerEdge = Math.max(this.renderWidth, this.renderHeight);
return Utils.findClosestinSorted(longerEdge, Media.sortedThumbnailSizes);
return this.getMediaSize(this.renderWidth,this.renderHeight);
}
getReplacementThumbnailSize(): number {
@ -37,7 +34,7 @@ export class Media extends MediaIcon {
const size = this.getThumbnailSize();
if (this.media.missingThumbnails) {
for (const thSize of Config.Media.Thumbnail.thumbnailSizes) {
for (const thSize of Config.Media.Photo.thumbnailSizes) {
// eslint-disable-next-line no-bitwise
if (
(this.media.missingThumbnails & MediaIcon.ThumbnailMap[thSize]) ===
@ -73,7 +70,6 @@ export class Media extends MediaIcon {
Config.Server.apiPath,
'/gallery/content/',
this.getRelativePath(),
'thumbnail',
size.toString()
);
}
@ -83,14 +79,6 @@ export class Media extends MediaIcon {
}
getThumbnailPath(): string {
const size = this.getThumbnailSize();
return Utils.concatUrls(
Config.Server.urlBase,
Config.Server.apiPath,
'/gallery/content/',
this.getRelativePath(),
'thumbnail',
size.toString()
);
return this.getBestSizedMediaPath(this.renderWidth,this.renderHeight);
}
}

View File

@ -4,7 +4,10 @@ import {MediaDTO} from '../../../../common/entities/MediaDTO';
export class MediaIcon {
protected static readonly ThumbnailMap =
Config.Media.Thumbnail.generateThumbnailMap();
Config.Media.Photo.generateThumbnailMap();
static readonly sortedThumbnailSizes =
Config.Media.Photo.thumbnailSizes.sort((a, b): number => a - b);
protected replacementSizeCache: number | boolean = false;
@ -17,18 +20,28 @@ export class MediaIcon {
iconLoaded(): void {
this.media.missingThumbnails -=
MediaIcon.ThumbnailMap[Config.Media.Thumbnail.iconSize];
MediaIcon.ThumbnailMap[Config.Media.Photo.iconSize];
}
isIconAvailable(): boolean {
// eslint-disable-next-line no-bitwise
return (
(this.media.missingThumbnails &
MediaIcon.ThumbnailMap[Config.Media.Thumbnail.iconSize]) ===
MediaIcon.ThumbnailMap[Config.Media.Photo.iconSize]) ===
0
);
}
isPhotoAvailable(renderWidth: number, renderHeight: number): boolean {
const size = this.getMediaSize(renderWidth, renderHeight);
// eslint-disable-next-line no-bitwise
return (
(this.media.missingThumbnails &
MediaIcon.ThumbnailMap[size]) === 0
);
}
getReadableRelativePath(): string {
return Utils.concatUrls(
this.media.directory.path,
@ -61,7 +74,7 @@ export class MediaIcon {
);
}
getMediaPath(): string {
getOriginalMediaPath(): string {
return Utils.concatUrls(
Config.Server.urlBase,
Config.Server.apiPath,
@ -70,8 +83,31 @@ export class MediaIcon {
);
}
getBestFitMediaPath(): string {
return Utils.concatUrls(this.getMediaPath(), '/bestFit');
getMediaSize(renderWidth: number, renderHeight: number): number {
const longerEdge = Math.max(renderWidth, renderHeight);
return Utils.findClosestinSorted(longerEdge, MediaIcon.sortedThumbnailSizes);
}
/**
* @param renderWidth bonding box width
* @param renderHeight bounding box height
*/
getBestSizedMediaPath(renderWidth: number, renderHeight: number): string {
const size = this.getMediaSize(renderWidth, renderHeight);
return Utils.concatUrls(
Config.Server.urlBase,
Config.Server.apiPath,
'/gallery/content/',
this.getRelativePath(),
size.toString()
);
}
/**
* Uses the converted video if the original is not available
*/
getBestFitVideoPath(): string {
return Utils.concatUrls(this.getOriginalMediaPath(), '/bestFit');
}
equals(other: MediaDTO | MediaIcon): boolean {

View File

@ -43,7 +43,7 @@
role="menu" aria-labelledby="button-basic">
<li role="menuitem">
<a *ngIf="activePhoto"
[href]="activePhoto.gridMedia.getMediaPath()"
[href]="activePhoto.gridMedia.getOriginalMediaPath()"
[download]="activePhoto.gridMedia.media.name"
class="dropdown-item">
<ng-icon class="me-2"

View File

@ -251,7 +251,7 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges {
if (event.shiftKey) {
const link = document.createElement('a');
link.setAttribute('type', 'hidden');
link.href = this.activePhoto.gridMedia.getMediaPath();
link.href = this.activePhoto.gridMedia.getOriginalMediaPath();
link.download = this.activePhoto.gridMedia.media.name;
document.body.appendChild(link);
link.click();

View File

@ -23,7 +23,7 @@
(error)="onImageError()"
(timeupdate)="onVideoProgress()"
#video>
<source [src]="gridMedia.getBestFitMediaPath()" (error)="onSourceError()">
<source [src]="gridMedia.getBestFitVideoPath()" (error)="onSourceError()">
Something went wrong.
</video>

View File

@ -168,7 +168,7 @@ export class GalleryLightboxMediaComponent implements OnChanges {
this.imageLoadFinished.this = true;
console.error(
'Error: cannot load media for lightbox url: ' +
this.gridMedia.getBestFitMediaPath()
this.gridMedia.getBestSizedMediaPath(window.innerWidth, window.innerHeight)
);
this.loadNextPhoto();
}
@ -184,6 +184,7 @@ export class GalleryLightboxMediaComponent implements OnChanges {
this.gridMedia &&
!this.mediaLoaded &&
this.thumbnailSrc !== null &&
!this.gridMedia.isPhotoAvailable(window.innerWidth, window.innerHeight) &&
(this.gridMedia.isThumbnailAvailable() ||
this.gridMedia.isReplacementThumbnailAvailable())
);
@ -208,11 +209,7 @@ export class GalleryLightboxMediaComponent implements OnChanges {
this.imageLoadFinished.next = true;
return;
}
if (Config.Media.Photo.Converting.enabled === true) {
this.nextImage.src = this.nextGridMedia.getBestFitMediaPath();
} else {
this.nextImage.src = this.nextGridMedia.getMediaPath();
}
this.nextImage.src = this.nextGridMedia.getBestSizedMediaPath(window.innerWidth, window.innerHeight);
this.nextImage.onload = () => this.imageLoadFinished.next = true;
this.nextImage.onerror = () => {
@ -232,20 +229,15 @@ export class GalleryLightboxMediaComponent implements OnChanges {
if (
this.zoom === 1 ||
Config.Media.Photo.Converting.loadFullImageOnZoom === false
Config.Gallery.Lightbox.loadFullImageOnZoom === false
) {
if (this.photo.src == null) {
if (Config.Media.Photo.Converting.enabled === true) {
this.photo.src = this.gridMedia.getBestFitMediaPath();
this.photo.src = this.gridMedia.getBestSizedMediaPath(window.innerWidth, window.innerHeight);
this.photo.isBestFit = true;
} else {
this.photo.src = this.gridMedia.getMediaPath();
this.photo.isBestFit = false;
}
}
// on zoom load high res photo
} else if (this.photo.isBestFit === true || this.photo.src == null) {
this.photo.src = this.gridMedia.getMediaPath();
this.photo.src = this.gridMedia.getOriginalMediaPath();
this.photo.isBestFit = false;
}
}

View File

@ -72,12 +72,12 @@ export class GalleryMapLightboxComponent implements OnChanges, OnDestroy {
defLayer: TileLayer;
darkLayer: TileLayer;
private smallIconSize = new Point(
Config.Media.Thumbnail.iconSize * 0.75,
Config.Media.Thumbnail.iconSize * 0.75
Config.Media.Photo.iconSize * 0.75,
Config.Media.Photo.iconSize * 0.75
);
private iconSize = new Point(
Config.Media.Thumbnail.iconSize,
Config.Media.Thumbnail.iconSize
Config.Media.Photo.iconSize,
Config.Media.Photo.iconSize
);
private usedIconSize = this.iconSize;
private mapLayersControlOption: LeafletControlLayersConfig & {

View File

@ -25,7 +25,7 @@ export class ThumbnailLoaderService {
if (
this.que.length === 0 ||
this.runningRequests >=
Config.Media.Thumbnail.concurrentThumbnailGenerations
Config.Media.Photo.concurrentThumbnailGenerations
) {
return;
}

View File

@ -28,6 +28,9 @@ export class ScheduledJobsService {
this.availableMessengers = new BehaviorSubject([]);
}
public isValidJob(name: string): boolean {
return !!this.availableJobs.value.find(j => j.Name === name);
}
public async getAvailableJobs(): Promise<void> {
this.availableJobs.next(

View File

@ -1,24 +1,34 @@
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{ error }}</div>
<div *ngFor="let schedule of sortedSchedules() as sortedSchedules; let i= index">
<div
class="card bg-body-tertiary mt-2 mb-2 no-changed-settings {{shouldIdent(schedule,sortedSchedules[i-1])? 'ms-4' : ''}}">
<div class="card-header">
<div class="d-flex justify-content-between">
<div (click)="showDetails[schedule.name]=!showDetails[schedule.name]">
<span
*ngIf="!jobsService.isValidJob(schedule.jobName)"
triggers="mouseenter:mouseleave"
placement="bottom"
container="body"
[popover]="'Unknown job type: ' + schedule.jobName"
class="text-danger me-2">
<ng-icon name="ionWarningOutline"></ng-icon>
</span>
<ng-icon style="font-size: 1.3em; margin-left: -4px"
[name]="showDetails[schedule.name] ? 'ionChevronDownOutline' : 'ionChevronForwardOutline'"></ng-icon>
{{schedule.name}}
{{ schedule.name }}
<ng-container [ngSwitch]="schedule.trigger.type">
<ng-container *ngSwitchCase="JobTriggerType.periodic">
<span class="badge bg-primary" i18n>every</span>
{{periods[$any(schedule.trigger).periodicity]}} {{atTimeLocal($any(schedule.trigger).atTime) | date:"HH:mm (z)"}}
{{ periods[$any(schedule.trigger).periodicity] }} {{ atTimeLocal($any(schedule.trigger).atTime) | date:"HH:mm (z)" }}
</ng-container>
<ng-container
*ngSwitchCase="JobTriggerType.scheduled">@{{$any(schedule.trigger).time | date:"medium"}}</ng-container>
*ngSwitchCase="JobTriggerType.scheduled">@{{ $any(schedule.trigger).time | date:"medium" }}
</ng-container>
<span class="badge bg-secondary" *ngSwitchCase="JobTriggerType.never" i18n>never</span>
<ng-container *ngSwitchCase="JobTriggerType.after">
<span class="badge bg-primary" i18n>after</span>
{{$any(schedule.trigger).afterScheduleName}}
{{ $any(schedule.trigger).afterScheduleName }}
</ng-container>
</ng-container>
</div>
@ -27,6 +37,7 @@
<ng-icon name="ionTrashOutline" title="Delete" i18n-title></ng-icon>
</button>
<app-settings-job-button class="ms-md-2 mt-2 mt-md-0"
*ngIf="jobsService.isValidJob(schedule.jobName)"
(jobError)="error=$event"
[allowParallelRun]="schedule.allowParallelRun"
[jobName]="schedule.jobName" [config]="schedule.config"
@ -42,12 +53,13 @@
<div class="col-md-12">
<div class="alert alert-secondary" role="alert"
*ngIf="settingsService.configStyle == ConfigStyle.full">
<ng-icon size="1.3em" name="ionInformationCircleOutline"></ng-icon> {{getJobDescription(schedule.jobName)}}
<ng-icon size="1.3em" name="ionInformationCircleOutline"></ng-icon>
{{ getJobDescription(schedule.jobName) }}
</div>
<div class="mb-1 row">
<label class="col-md-2 control-label" i18n>Job:</label>
<div class="col-md-4">
{{backendTextService.getJobName(schedule.jobName)}}
{{ backendTextService.getJobName(schedule.jobName) }}
</div>
<div class="col-md-6">
<app-settings-job-button class="float-end"
@ -68,7 +80,7 @@
[id]="'repeatType'+i"
required>
<option *ngFor="let jobTrigger of JobTriggerTypeMap"
[ngValue]="jobTrigger.key">{{jobTrigger.value}}
[ngValue]="jobTrigger.key">{{ jobTrigger.value }}
</option>
</select>
<small class="form-text text-muted" *ngIf="settingsService.configStyle == ConfigStyle.full"
@ -89,7 +101,7 @@
[id]="'triggerAfter'+i" required>
<ng-container *ngFor="let sch of sortedSchedules">
<option *ngIf="sch.name !== schedule.name"
[ngValue]="sch.name">{{sch.name}}
[ngValue]="sch.name">{{ sch.name }}
</option>
</ng-container>
</select>
@ -128,7 +140,7 @@
<option *ngFor="let period of periods; let i = index"
[ngValue]="i">
<ng-container i18n>every</ng-container>
{{period}}
{{ period }}
</option>
</select>
<app-timestamp-timepicker
@ -173,7 +185,7 @@
*ngIf="!configEntry.validIf || schedule.config[configEntry.validIf.configFiled] == configEntry.validIf.equalsValue"
[class.mb-3]="settingsService.configStyle == ConfigStyle.full">
<label class="col-md-2 control-label"
[for]="configEntry.id+'_'+i">{{backendTextService.get(configEntry.name)}}</label>
[for]="configEntry.id+'_'+i">{{ backendTextService.get(configEntry.name) }}</label>
<div class="col-md-10">
<div [class.input-group]="'MediaPickDTO-array'!=configEntry.type">
<ng-container [ngSwitch]="configEntry.type">
@ -235,7 +247,7 @@
(ngModelChange)="onChange($event)"
[(ngModel)]="schedule.config[configEntry.id]"
class="form-select">
<option *ngFor="let msg of jobsService.availableMessengers | async" [ngValue]="msg">{{msg}}
<option *ngFor="let msg of jobsService.availableMessengers | async" [ngValue]="msg">{{ msg }}
</option>
</select>
@ -246,7 +258,9 @@
<div class="mb-1 row"
[class.mb-3]="settingsService.configStyle == ConfigStyle.full">
<label class="col-md-2 control-label"
[for]="configEntry.id+'_'+i"><ng-container>Search Query</ng-container> - {{(j + 1)}}</label>
[for]="configEntry.id+'_'+i">
<ng-container>Search Query</ng-container>
- {{ (j + 1) }}</label>
<div class="col-md-10">
<div class="input-group">
<app-gallery-search-field
@ -363,7 +377,7 @@
<small class="form-text text-muted" *ngIf="settingsService.configStyle == ConfigStyle.full">
<ng-container *ngIf="configEntry.type == 'number-array'" i18n>';' separated integers.
</ng-container>
{{backendTextService.get(configEntry.description)}}
{{ backendTextService.get(configEntry.description) }}
</small>
</div>
</div>
@ -405,7 +419,7 @@
[(ngModel)]="newSchedule.jobName"
name="newJobName" required>
<option *ngFor="let availableJob of jobsService.availableJobs | async"
[ngValue]="availableJob.Name">{{backendTextService.getJobName(availableJob.Name)}}
[ngValue]="availableJob.Name">{{ backendTextService.getJobName(availableJob.Name) }}
</option>
</select>
<small class="form-text text-muted" *ngIf="settingsService.configStyle == ConfigStyle.full"

View File

@ -7,51 +7,26 @@ import {PhotoProcessing} from '../../../../../src/backend/model/fileaccess/filep
describe('PhotoProcessing', () => {
/* eslint-disable no-unused-expressions,@typescript-eslint/no-unused-expressions */
it('should generate converted file path', async () => {
await Config.load();
Config.Media.Thumbnail.thumbnailSizes = [];
Config.Media.Thumbnail.animateGif = true;
ProjectPath.ImageFolder = path.join(__dirname, './../../../assets');
const photoPath = path.join(ProjectPath.ImageFolder, 'test_png.png');
expect(await PhotoProcessing
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(photoPath,
Config.Media.Photo.Converting.resolution)))
.to.be.true;
expect(await PhotoProcessing
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(photoPath + 'noPath',
Config.Media.Photo.Converting.resolution)))
.to.be.false;
{
const convertedPath = PhotoProcessing.generateConvertedPath(photoPath,
Config.Media.Photo.Converting.resolution);
Config.Media.Photo.Converting.resolution = (1 as any);
expect(await PhotoProcessing.isValidConvertedPath(convertedPath)).to.be.false;
}
});
it('should generate converted gif file path', async () => {
await Config.load();
Config.Media.Thumbnail.thumbnailSizes = [];
Config.Media.Photo.thumbnailSizes = [];
ProjectPath.ImageFolder = path.join(__dirname, './../../../assets');
const gifPath = path.join(ProjectPath.ImageFolder, 'earth.gif');
Config.Media.Thumbnail.animateGif = true;
for (const thSize of Config.Media.Photo.thumbnailSizes) {
Config.Media.Photo.animateGif = true;
expect(await PhotoProcessing
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(gifPath,
Config.Media.Photo.Converting.resolution)))
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(gifPath, thSize)))
.to.be.true;
Config.Media.Thumbnail.animateGif = false;
Config.Media.Photo.animateGif = false;
expect(await PhotoProcessing
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(gifPath,
Config.Media.Photo.Converting.resolution)))
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(gifPath, thSize)))
.to.be.true;
}
});
@ -60,12 +35,11 @@ describe('PhotoProcessing', () => {
it('should generate converted thumbnail path', async () => {
await Config.load();
Config.Media.Photo.Converting.resolution = (null as any);
Config.Media.Thumbnail.thumbnailSizes = [10, 20];
Config.Media.Photo.thumbnailSizes = [10, 20];
ProjectPath.ImageFolder = path.join(__dirname, './../../../assets');
const photoPath = path.join(ProjectPath.ImageFolder, 'test_png.png');
for (const thSize of Config.Media.Thumbnail.thumbnailSizes) {
for (const thSize of Config.Media.Photo.thumbnailSizes) {
expect(await PhotoProcessing
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(photoPath, thSize)))
.to.be.true;