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:
parent
0395fa87ff
commit
13e828d210
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -14,7 +14,7 @@ import {PersonEntry} from '../../model/database/enitites/PersonEntry';
|
|||||||
|
|
||||||
export class ThumbnailGeneratorMWs {
|
export class ThumbnailGeneratorMWs {
|
||||||
private static ThumbnailMapEntries =
|
private static ThumbnailMapEntries =
|
||||||
Config.Media.Thumbnail.generateThumbnailMapEntries();
|
Config.Media.Photo.generateThumbnailMapEntries();
|
||||||
|
|
||||||
@ServerTime('2.th', 'Thumbnail decoration')
|
@ServerTime('2.th', 'Thumbnail decoration')
|
||||||
public static async addThumbnailInformation(
|
public static async addThumbnailInformation(
|
||||||
@ -34,7 +34,7 @@ export class ThumbnailGeneratorMWs {
|
|||||||
|
|
||||||
// regenerate in case the list change since startup
|
// regenerate in case the list change since startup
|
||||||
ThumbnailGeneratorMWs.ThumbnailMapEntries =
|
ThumbnailGeneratorMWs.ThumbnailMapEntries =
|
||||||
Config.Media.Thumbnail.generateThumbnailMapEntries();
|
Config.Media.Photo.generateThumbnailMapEntries();
|
||||||
if (cw.directory) {
|
if (cw.directory) {
|
||||||
ThumbnailGeneratorMWs.addThInfoTODir(cw.directory);
|
ThumbnailGeneratorMWs.addThInfoTODir(cw.directory);
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ export class ThumbnailGeneratorMWs {
|
|||||||
|
|
||||||
let erroredItem: PersonEntry = null;
|
let erroredItem: PersonEntry = null;
|
||||||
try {
|
try {
|
||||||
const size: number = Config.Media.Thumbnail.personThumbnailSize;
|
const size: number = Config.Media.Photo.personThumbnailSize;
|
||||||
|
|
||||||
const persons: PersonEntry[] = req.resultPipe as PersonEntry[];
|
const persons: PersonEntry[] = req.resultPipe as PersonEntry[];
|
||||||
|
|
||||||
@ -147,11 +147,11 @@ export class ThumbnailGeneratorMWs {
|
|||||||
const mediaPath = req.resultPipe as string;
|
const mediaPath = req.resultPipe as string;
|
||||||
let size: number =
|
let size: number =
|
||||||
parseInt(req.params.size, 10) ||
|
parseInt(req.params.size, 10) ||
|
||||||
Config.Media.Thumbnail.thumbnailSizes[0];
|
Config.Media.Photo.thumbnailSizes[0];
|
||||||
|
|
||||||
// validate size
|
// validate size
|
||||||
if (Config.Media.Thumbnail.thumbnailSizes.indexOf(size) === -1) {
|
if (Config.Media.Photo.thumbnailSizes.indexOf(size) === -1) {
|
||||||
size = Config.Media.Thumbnail.thumbnailSizes[0];
|
size = Config.Media.Photo.thumbnailSizes[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -188,7 +188,7 @@ export class ThumbnailGeneratorMWs {
|
|||||||
|
|
||||||
// load parameters
|
// load parameters
|
||||||
const mediaPath = req.resultPipe as string;
|
const mediaPath = req.resultPipe as string;
|
||||||
const size: number = Config.Media.Thumbnail.iconSize;
|
const size: number = Config.Media.Photo.iconSize;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
req.resultPipe = await PhotoProcessing.generateThumbnail(
|
req.resultPipe = await PhotoProcessing.generateThumbnail(
|
||||||
|
@ -20,21 +20,24 @@ import {
|
|||||||
ServerAlbumCoverConfig,
|
ServerAlbumCoverConfig,
|
||||||
ServerDataBaseConfig,
|
ServerDataBaseConfig,
|
||||||
ServerJobConfig,
|
ServerJobConfig,
|
||||||
ServerThumbnailConfig,
|
ServerPhotoConfig,
|
||||||
ServerVideoConfig,
|
ServerVideoConfig,
|
||||||
} from '../../../common/config/private/PrivateConfig';
|
} from '../../../common/config/private/PrivateConfig';
|
||||||
import {SearchQueryParser} from '../../../common/SearchQueryParser';
|
import {SearchQueryParser} from '../../../common/SearchQueryParser';
|
||||||
import {SearchQueryTypes, TextSearch,} from '../../../common/entities/SearchQueryDTO';
|
import {SearchQueryTypes, TextSearch,} from '../../../common/entities/SearchQueryDTO';
|
||||||
import {Utils} from '../../../common/Utils';
|
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]';
|
const LOG_TAG = '[ConfigDiagnostics]';
|
||||||
|
|
||||||
export class ConfigDiagnostics {
|
export class ConfigDiagnostics {
|
||||||
static testAlbumsConfig(
|
static testAlbumsConfig(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
albumConfig: ClientAlbumConfig,
|
albumConfig: ClientAlbumConfig,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
original: PrivateConfigClass
|
original: PrivateConfigClass
|
||||||
): void {
|
): void {
|
||||||
Logger.debug(LOG_TAG, 'Testing album config');
|
Logger.debug(LOG_TAG, 'Testing album config');
|
||||||
// nothing to check
|
// nothing to check
|
||||||
@ -53,27 +56,39 @@ export class ConfigDiagnostics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async testDatabase(
|
static async testDatabase(
|
||||||
databaseConfig: ServerDataBaseConfig
|
databaseConfig: ServerDataBaseConfig
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Logger.debug(LOG_TAG, 'Testing database config');
|
Logger.debug(LOG_TAG, 'Testing database config');
|
||||||
await SQLConnection.tryConnection(databaseConfig);
|
await SQLConnection.tryConnection(databaseConfig);
|
||||||
if (databaseConfig.type === DatabaseType.sqlite) {
|
if (databaseConfig.type === DatabaseType.sqlite) {
|
||||||
try {
|
try {
|
||||||
await this.checkReadWritePermission(
|
await this.checkReadWritePermission(
|
||||||
SQLConnection.getSQLiteDB(databaseConfig)
|
SQLConnection.getSQLiteDB(databaseConfig)
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Cannot read or write sqlite storage file: ' +
|
'Cannot read or write sqlite storage file: ' +
|
||||||
SQLConnection.getSQLiteDB(databaseConfig)
|
SQLConnection.getSQLiteDB(databaseConfig)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
static async testMetaFileConfig(
|
||||||
metaFileConfig: ClientMetaFileConfig,
|
metaFileConfig: ClientMetaFileConfig,
|
||||||
config: PrivateConfigClass
|
config: PrivateConfigClass
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Logger.debug(LOG_TAG, 'Testing meta file config');
|
Logger.debug(LOG_TAG, 'Testing meta file config');
|
||||||
if (metaFileConfig.gpx === true && config.Map.enabled === false) {
|
if (metaFileConfig.gpx === true && config.Map.enabled === false) {
|
||||||
@ -98,19 +113,19 @@ export class ConfigDiagnostics {
|
|||||||
ffmpeg().getAvailableCodecs((err: Error) => {
|
ffmpeg().getAvailableCodecs((err: Error) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return reject(
|
return reject(
|
||||||
new Error(
|
new Error(
|
||||||
'Error accessing ffmpeg, cant find executable: ' +
|
'Error accessing ffmpeg, cant find executable: ' +
|
||||||
err.toString()
|
err.toString()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ffmpeg(__dirname + '/blank.jpg').ffprobe((err2: Error) => {
|
ffmpeg(__dirname + '/blank.jpg').ffprobe((err2: Error) => {
|
||||||
if (err2) {
|
if (err2) {
|
||||||
return reject(
|
return reject(
|
||||||
new Error(
|
new Error(
|
||||||
'Error accessing ffmpeg-probe, cant find executable: ' +
|
'Error accessing ffmpeg-probe, cant find executable: ' +
|
||||||
err2.toString()
|
err2.toString()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return resolve();
|
return resolve();
|
||||||
@ -157,26 +172,26 @@ export class ConfigDiagnostics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static async testThumbnailConfig(
|
static async testPhotoConfig(
|
||||||
thumbnailConfig: ServerThumbnailConfig
|
photoConfig: ServerPhotoConfig
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Logger.debug(LOG_TAG, 'Testing thumbnail config');
|
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');
|
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(
|
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');
|
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) {
|
if (isNaN(item) || item <= 0) {
|
||||||
throw new Error('Thumbnail size has to be >= 0 integer, got: ' + item);
|
throw new Error('Thumbnail size has to be >= 0 integer, got: ' + item);
|
||||||
}
|
}
|
||||||
@ -184,18 +199,18 @@ export class ConfigDiagnostics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async testTasksConfig(
|
static async testTasksConfig(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
task: ServerJobConfig,
|
task: ServerJobConfig,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
config: PrivateConfigClass
|
config: PrivateConfigClass
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Logger.debug(LOG_TAG, 'Testing tasks config');
|
Logger.debug(LOG_TAG, 'Testing tasks config');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async testFacesConfig(
|
static async testFacesConfig(
|
||||||
faces: ClientFacesConfig,
|
faces: ClientFacesConfig,
|
||||||
config: PrivateConfigClass
|
config: PrivateConfigClass
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Logger.debug(LOG_TAG, 'Testing faces config');
|
Logger.debug(LOG_TAG, 'Testing faces config');
|
||||||
if (faces.enabled === true) {
|
if (faces.enabled === true) {
|
||||||
@ -206,33 +221,33 @@ export class ConfigDiagnostics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async testSearchConfig(
|
static async testSearchConfig(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
search: ClientSearchConfig,
|
search: ClientSearchConfig,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
config: PrivateConfigClass
|
config: PrivateConfigClass
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Logger.debug(LOG_TAG, 'Testing search config');
|
Logger.debug(LOG_TAG, 'Testing search config');
|
||||||
//nothing to check
|
//nothing to check
|
||||||
}
|
}
|
||||||
|
|
||||||
static async testSharingConfig(
|
static async testSharingConfig(
|
||||||
sharing: ClientSharingConfig,
|
sharing: ClientSharingConfig,
|
||||||
config: PrivateConfigClass
|
config: PrivateConfigClass
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Logger.debug(LOG_TAG, 'Testing sharing config');
|
Logger.debug(LOG_TAG, 'Testing sharing config');
|
||||||
if (
|
if (
|
||||||
sharing.enabled === true &&
|
sharing.enabled === true &&
|
||||||
config.Users.authenticationRequired === false
|
config.Users.authenticationRequired === false
|
||||||
) {
|
) {
|
||||||
throw new Error('In case of no authentication, sharing is not supported');
|
throw new Error('In case of no authentication, sharing is not supported');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async testRandomPhotoConfig(
|
static async testRandomPhotoConfig(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
sharing: ClientRandomPhotoConfig,
|
sharing: ClientRandomPhotoConfig,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
config: PrivateConfigClass
|
config: PrivateConfigClass
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Logger.debug(LOG_TAG, 'Testing random photo config');
|
Logger.debug(LOG_TAG, 'Testing random photo config');
|
||||||
//nothing to check
|
//nothing to check
|
||||||
@ -244,14 +259,14 @@ export class ConfigDiagnostics {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
map.mapProvider === MapProviders.Mapbox &&
|
map.mapProvider === MapProviders.Mapbox &&
|
||||||
(!map.mapboxAccessToken || map.mapboxAccessToken.length === 0)
|
(!map.mapboxAccessToken || map.mapboxAccessToken.length === 0)
|
||||||
) {
|
) {
|
||||||
throw new Error('Mapbox needs a valid api key.');
|
throw new Error('Mapbox needs a valid api key.');
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
map.mapProvider === MapProviders.Custom &&
|
map.mapProvider === MapProviders.Custom &&
|
||||||
(!map.customLayers || map.customLayers.length === 0)
|
(!map.customLayers || map.customLayers.length === 0)
|
||||||
) {
|
) {
|
||||||
throw new Error('Custom maps need at least one valid layer');
|
throw new Error('Custom maps need at least one valid layer');
|
||||||
}
|
}
|
||||||
@ -268,10 +283,10 @@ export class ConfigDiagnostics {
|
|||||||
Logger.debug(LOG_TAG, 'Testing cover config');
|
Logger.debug(LOG_TAG, 'Testing cover config');
|
||||||
const sp = new SearchQueryParser();
|
const sp = new SearchQueryParser();
|
||||||
if (
|
if (
|
||||||
!Utils.equalsFilter(
|
!Utils.equalsFilter(
|
||||||
sp.parse(sp.stringify(settings.SearchQuery)),
|
sp.parse(sp.stringify(settings.SearchQuery)),
|
||||||
settings.SearchQuery
|
settings.SearchQuery
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw new Error('SearchQuery is not valid. Got: ' + JSON.stringify(sp.parse(sp.stringify(settings.SearchQuery))));
|
throw new Error('SearchQuery is not valid. Got: ' + JSON.stringify(sp.parse(sp.stringify(settings.SearchQuery))));
|
||||||
}
|
}
|
||||||
@ -286,7 +301,7 @@ export class ConfigDiagnostics {
|
|||||||
await ConfigDiagnostics.testMetaFileConfig(config.MetaFile, config);
|
await ConfigDiagnostics.testMetaFileConfig(config.MetaFile, config);
|
||||||
await ConfigDiagnostics.testAlbumsConfig(config.Album, config);
|
await ConfigDiagnostics.testAlbumsConfig(config.Album, config);
|
||||||
await ConfigDiagnostics.testImageFolder(config.Media.folder);
|
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.testSearchConfig(config.Search, config);
|
||||||
await ConfigDiagnostics.testAlbumCoverConfig(config.AlbumCover);
|
await ConfigDiagnostics.testAlbumCoverConfig(config.AlbumCover);
|
||||||
await ConfigDiagnostics.testFacesConfig(config.Faces, config);
|
await ConfigDiagnostics.testFacesConfig(config.Faces, config);
|
||||||
@ -294,6 +309,7 @@ export class ConfigDiagnostics {
|
|||||||
await ConfigDiagnostics.testSharingConfig(config.Sharing, config);
|
await ConfigDiagnostics.testSharingConfig(config.Sharing, config);
|
||||||
await ConfigDiagnostics.testRandomPhotoConfig(config.Sharing, config);
|
await ConfigDiagnostics.testRandomPhotoConfig(config.Sharing, config);
|
||||||
await ConfigDiagnostics.testMapConfig(config.Map);
|
await ConfigDiagnostics.testMapConfig(config.Map);
|
||||||
|
await ConfigDiagnostics.testJobsConfig(config.Jobs);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,8 +327,8 @@ export class ConfigDiagnostics {
|
|||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
Logger.warn(LOG_TAG, '[SQL error]', err.toString());
|
Logger.warn(LOG_TAG, '[SQL error]', err.toString());
|
||||||
Logger.error(
|
Logger.error(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Error during initializing SQL DB, check DB connection and settings'
|
'Error during initializing SQL DB, check DB connection and settings'
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@ -323,15 +339,15 @@ export class ConfigDiagnostics {
|
|||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
|
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'[Thumbnail hardware acceleration] module error: ',
|
'[Thumbnail hardware acceleration] module error: ',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Thumbnail hardware acceleration is not possible.' +
|
'Thumbnail hardware acceleration is not possible.' +
|
||||||
' \'sharp\' node module is not found.' +
|
' \'sharp\' node module is not found.' +
|
||||||
' Falling back temporally to JS based thumbnail generation'
|
' Falling back temporally to JS based thumbnail generation'
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@ -349,32 +365,32 @@ export class ConfigDiagnostics {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
NotificationManager.warning(
|
NotificationManager.warning(
|
||||||
'Video support error, switching off..',
|
'Video support error, switching off..',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Video support error, switching off..',
|
'Video support error, switching off..',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Config.Media.Video.enabled = false;
|
Config.Media.Video.enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ConfigDiagnostics.testMetaFileConfig(
|
await ConfigDiagnostics.testMetaFileConfig(
|
||||||
Config.MetaFile,
|
Config.MetaFile,
|
||||||
Config
|
Config
|
||||||
);
|
);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
NotificationManager.warning(
|
NotificationManager.warning(
|
||||||
'Meta file support error, switching off gpx..',
|
'Meta file support error, switching off gpx..',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Meta file support error, switching off..',
|
'Meta file support error, switching off..',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Config.MetaFile.gpx = false;
|
Config.MetaFile.gpx = false;
|
||||||
}
|
}
|
||||||
@ -384,13 +400,13 @@ export class ConfigDiagnostics {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
NotificationManager.warning(
|
NotificationManager.warning(
|
||||||
'Albums support error, switching off..',
|
'Albums support error, switching off..',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Meta file support error, switching off..',
|
'Meta file support error, switching off..',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Config.Album.enabled = false;
|
Config.Album.enabled = false;
|
||||||
}
|
}
|
||||||
@ -403,8 +419,8 @@ export class ConfigDiagnostics {
|
|||||||
Logger.error(LOG_TAG, 'Images folder error', err.toString());
|
Logger.error(LOG_TAG, 'Images folder error', err.toString());
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ConfigDiagnostics.testThumbnailConfig(
|
await ConfigDiagnostics.testPhotoConfig(
|
||||||
Config.Media.Thumbnail
|
Config.Media.Photo
|
||||||
);
|
);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
@ -417,14 +433,14 @@ export class ConfigDiagnostics {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
NotificationManager.warning(
|
NotificationManager.warning(
|
||||||
'Search is not supported with these settings. Disabling temporally. ' +
|
'Search is not supported with these settings. Disabling temporally. ' +
|
||||||
'Please adjust the config properly.',
|
'Please adjust the config properly.',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Search is not supported with these settings, switching off..',
|
'Search is not supported with these settings, switching off..',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Config.Search.enabled = false;
|
Config.Search.enabled = false;
|
||||||
}
|
}
|
||||||
@ -434,13 +450,13 @@ export class ConfigDiagnostics {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
NotificationManager.warning(
|
NotificationManager.warning(
|
||||||
'Cover settings are not valid, resetting search query',
|
'Cover settings are not valid, resetting search query',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Cover settings are not valid, resetting search query',
|
'Cover settings are not valid, resetting search query',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Config.AlbumCover.SearchQuery = {
|
Config.AlbumCover.SearchQuery = {
|
||||||
type: SearchQueryTypes.any_text,
|
type: SearchQueryTypes.any_text,
|
||||||
@ -453,14 +469,14 @@ export class ConfigDiagnostics {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
NotificationManager.warning(
|
NotificationManager.warning(
|
||||||
'Faces are not supported with these settings. Disabling temporally. ' +
|
'Faces are not supported with these settings. Disabling temporally. ' +
|
||||||
'Please adjust the config properly.',
|
'Please adjust the config properly.',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Faces are not supported with these settings, switching off..',
|
'Faces are not supported with these settings, switching off..',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Config.Faces.enabled = false;
|
Config.Faces.enabled = false;
|
||||||
}
|
}
|
||||||
@ -470,14 +486,14 @@ export class ConfigDiagnostics {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
NotificationManager.warning(
|
NotificationManager.warning(
|
||||||
'Some Tasks are not supported with these settings. Disabling temporally. ' +
|
'Some Tasks are not supported with these settings. Disabling temporally. ' +
|
||||||
'Please adjust the config properly.',
|
'Please adjust the config properly.',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Some Tasks not supported with these settings, switching off..',
|
'Some Tasks not supported with these settings, switching off..',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Config.Faces.enabled = false;
|
Config.Faces.enabled = false;
|
||||||
}
|
}
|
||||||
@ -487,34 +503,34 @@ export class ConfigDiagnostics {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
NotificationManager.warning(
|
NotificationManager.warning(
|
||||||
'Sharing is not supported with these settings. Disabling temporally. ' +
|
'Sharing is not supported with these settings. Disabling temporally. ' +
|
||||||
'Please adjust the config properly.',
|
'Please adjust the config properly.',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Sharing is not supported with these settings, switching off..',
|
'Sharing is not supported with these settings, switching off..',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Config.Sharing.enabled = false;
|
Config.Sharing.enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ConfigDiagnostics.testRandomPhotoConfig(
|
await ConfigDiagnostics.testRandomPhotoConfig(
|
||||||
Config.Sharing,
|
Config.Sharing,
|
||||||
Config
|
Config
|
||||||
);
|
);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
NotificationManager.warning(
|
NotificationManager.warning(
|
||||||
'Random Media is not supported with these settings. Disabling temporally. ' +
|
'Random Media is not supported with these settings. Disabling temporally. ' +
|
||||||
'Please adjust the config properly.',
|
'Please adjust the config properly.',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Random Media is not supported with these settings, switching off..',
|
'Random Media is not supported with these settings, switching off..',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Config.Sharing.enabled = false;
|
Config.Sharing.enabled = false;
|
||||||
}
|
}
|
||||||
@ -524,20 +540,41 @@ export class ConfigDiagnostics {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const err: Error = ex;
|
const err: Error = ex;
|
||||||
NotificationManager.warning(
|
NotificationManager.warning(
|
||||||
'Maps is not supported with these settings. Using open street maps temporally. ' +
|
'Maps is not supported with these settings. Using open street maps temporally. ' +
|
||||||
'Please adjust the config properly.',
|
'Please adjust the config properly.',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
LOG_TAG,
|
LOG_TAG,
|
||||||
'Maps is not supported with these settings. Using open street maps temporally ' +
|
'Maps is not supported with these settings. Using open street maps temporally ' +
|
||||||
'Please adjust the config properly.',
|
'Please adjust the config properly.',
|
||||||
err.toString()
|
err.toString()
|
||||||
);
|
);
|
||||||
Config.Map.mapProvider = MapProviders.OpenStreetMap;
|
Config.Map.mapProvider = MapProviders.OpenStreetMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,13 +21,13 @@ export class PhotoProcessing {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.Media.Thumbnail.concurrentThumbnailGenerations = Math.max(
|
Config.Media.Photo.concurrentThumbnailGenerations = Math.max(
|
||||||
1,
|
1,
|
||||||
os.cpus().length - 1
|
os.cpus().length - 1
|
||||||
);
|
);
|
||||||
|
|
||||||
this.taskQue = new TaskExecuter(
|
this.taskQue = new TaskExecuter(
|
||||||
Config.Media.Thumbnail.concurrentThumbnailGenerations,
|
Config.Media.Photo.concurrentThumbnailGenerations,
|
||||||
(input): Promise<void> => PhotoWorker.render(input)
|
(input): Promise<void> => PhotoWorker.render(input)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ export class PhotoProcessing {
|
|||||||
photo.directory.name,
|
photo.directory.name,
|
||||||
photo.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);
|
const faceRegion = person.sampleRegion.media.metadata.faces.find(f => f.name === person.name);
|
||||||
// generate thumbnail path
|
// generate thumbnail path
|
||||||
const thPath = PhotoProcessing.generatePersonThumbnailPath(
|
const thPath = PhotoProcessing.generatePersonThumbnailPath(
|
||||||
@ -65,11 +65,11 @@ export class PhotoProcessing {
|
|||||||
const margin = {
|
const margin = {
|
||||||
x: Math.round(
|
x: Math.round(
|
||||||
faceRegion.box.width *
|
faceRegion.box.width *
|
||||||
Config.Media.Thumbnail.personFaceMargin
|
Config.Media.Photo.personFaceMargin
|
||||||
),
|
),
|
||||||
y: Math.round(
|
y: Math.round(
|
||||||
faceRegion.box.height *
|
faceRegion.box.height *
|
||||||
Config.Media.Thumbnail.personFaceMargin
|
Config.Media.Photo.personFaceMargin
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -90,9 +90,9 @@ export class PhotoProcessing {
|
|||||||
width: faceRegion.box.width + margin.x,
|
width: faceRegion.box.width + margin.x,
|
||||||
height: faceRegion.box.height + margin.y,
|
height: faceRegion.box.height + margin.y,
|
||||||
},
|
},
|
||||||
useLanczos3: Config.Media.Thumbnail.useLanczos3,
|
useLanczos3: Config.Media.Photo.useLanczos3,
|
||||||
quality: Config.Media.Thumbnail.quality,
|
quality: Config.Media.Photo.quality,
|
||||||
smartSubsample: Config.Media.Thumbnail.smartSubsample,
|
smartSubsample: Config.Media.Photo.smartSubsample,
|
||||||
} as MediaRendererInput;
|
} as MediaRendererInput;
|
||||||
input.cut.width = Math.min(
|
input.cut.width = Math.min(
|
||||||
input.cut.width,
|
input.cut.width,
|
||||||
@ -110,13 +110,13 @@ export class PhotoProcessing {
|
|||||||
|
|
||||||
public static generateConvertedPath(mediaPath: string, size: number): string {
|
public static generateConvertedPath(mediaPath: string, size: number): string {
|
||||||
const file = path.basename(mediaPath);
|
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(
|
return path.join(
|
||||||
ProjectPath.TranscodedFolder,
|
ProjectPath.TranscodedFolder,
|
||||||
ProjectPath.getRelativePathToImages(path.dirname(mediaPath)),
|
ProjectPath.getRelativePathToImages(path.dirname(mediaPath)),
|
||||||
file + '_' + size + 'q' + Config.Media.Thumbnail.quality +
|
file + '_' + size + 'q' + Config.Media.Photo.quality +
|
||||||
(animated ? 'anim' : '') +
|
(animated ? 'anim' : '') +
|
||||||
(Config.Media.Thumbnail.smartSubsample ? 'cs' : '') +
|
(Config.Media.Photo.smartSubsample ? 'cs' : '') +
|
||||||
PhotoProcessing.CONVERTED_EXTENSION
|
PhotoProcessing.CONVERTED_EXTENSION
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -142,7 +142,7 @@ export class PhotoProcessing {
|
|||||||
.digest('hex') +
|
.digest('hex') +
|
||||||
'_' +
|
'_' +
|
||||||
size +
|
size +
|
||||||
'_' + Config.Media.Thumbnail.personFaceMargin +
|
'_' + Config.Media.Photo.personFaceMargin +
|
||||||
PhotoProcessing.CONVERTED_EXTENSION
|
PhotoProcessing.CONVERTED_EXTENSION
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -177,8 +177,7 @@ export class PhotoProcessing {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
(size + '').length !== sizeStr.length ||
|
(size + '').length !== sizeStr.length ||
|
||||||
(Config.Media.Thumbnail.thumbnailSizes.indexOf(size) === -1 &&
|
(Config.Media.Photo.thumbnailSizes.indexOf(size) === -1)
|
||||||
Config.Media.Photo.Converting.resolution !== size)
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -189,7 +188,7 @@ export class PhotoProcessing {
|
|||||||
const quality = parseInt(qualityStr, 10);
|
const quality = parseInt(qualityStr, 10);
|
||||||
|
|
||||||
if ((quality + '').length !== qualityStr.length ||
|
if ((quality + '').length !== qualityStr.length ||
|
||||||
quality !== Config.Media.Thumbnail.quality) {
|
quality !== Config.Media.Photo.quality) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +197,7 @@ export class PhotoProcessing {
|
|||||||
|
|
||||||
|
|
||||||
const lowerExt = path.extname(origFilePath).toLowerCase();
|
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 (shouldBeAnimated) {
|
||||||
if (convertedPath.substring(
|
if (convertedPath.substring(
|
||||||
nextIndex,
|
nextIndex,
|
||||||
@ -210,7 +209,7 @@ export class PhotoProcessing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (Config.Media.Thumbnail.smartSubsample) {
|
if (Config.Media.Photo.smartSubsample) {
|
||||||
if (convertedPath.substring(
|
if (convertedPath.substring(
|
||||||
nextIndex,
|
nextIndex,
|
||||||
nextIndex + 2
|
nextIndex + 2
|
||||||
@ -236,14 +235,6 @@ export class PhotoProcessing {
|
|||||||
return true;
|
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(
|
static async convertedPhotoExist(
|
||||||
mediaPath: string,
|
mediaPath: string,
|
||||||
@ -287,9 +278,9 @@ export class PhotoProcessing {
|
|||||||
size,
|
size,
|
||||||
outPath,
|
outPath,
|
||||||
makeSquare,
|
makeSquare,
|
||||||
useLanczos3: Config.Media.Thumbnail.useLanczos3,
|
useLanczos3: Config.Media.Photo.useLanczos3,
|
||||||
quality: Config.Media.Thumbnail.quality,
|
quality: Config.Media.Photo.quality,
|
||||||
smartSubsample: Config.Media.Thumbnail.smartSubsample,
|
smartSubsample: Config.Media.Photo.smartSubsample,
|
||||||
} as MediaRendererInput;
|
} as MediaRendererInput;
|
||||||
|
|
||||||
const outDir = path.dirname(input.outPath);
|
const outDir = path.dirname(input.outPath);
|
||||||
@ -328,9 +319,9 @@ viewBox="${svgIcon.viewBox || '0 0 512 512'}">d="${svgIcon.items}</svg>`,
|
|||||||
outPath,
|
outPath,
|
||||||
makeSquare: false,
|
makeSquare: false,
|
||||||
animate: false,
|
animate: false,
|
||||||
useLanczos3: Config.Media.Thumbnail.useLanczos3,
|
useLanczos3: Config.Media.Photo.useLanczos3,
|
||||||
quality: Config.Media.Thumbnail.quality,
|
quality: Config.Media.Photo.quality,
|
||||||
smartSubsample: Config.Media.Thumbnail.smartSubsample,
|
smartSubsample: Config.Media.Photo.smartSubsample,
|
||||||
} as SvgRendererInput;
|
} as SvgRendererInput;
|
||||||
|
|
||||||
const outDir = path.dirname(input.outPath);
|
const outDir = path.dirname(input.outPath);
|
||||||
|
@ -3,7 +3,6 @@ import {IndexingJob} from './jobs/IndexingJob';
|
|||||||
import {GalleryRestJob} from './jobs/GalleryResetJob';
|
import {GalleryRestJob} from './jobs/GalleryResetJob';
|
||||||
import {VideoConvertingJob} from './jobs/VideoConvertingJob';
|
import {VideoConvertingJob} from './jobs/VideoConvertingJob';
|
||||||
import {PhotoConvertingJob} from './jobs/PhotoConvertingJob';
|
import {PhotoConvertingJob} from './jobs/PhotoConvertingJob';
|
||||||
import {ThumbnailGenerationJob} from './jobs/ThumbnailGenerationJob';
|
|
||||||
import {TempFolderCleaningJob} from './jobs/TempFolderCleaningJob';
|
import {TempFolderCleaningJob} from './jobs/TempFolderCleaningJob';
|
||||||
import {AlbumCoverFillingJob} from './jobs/AlbumCoverFillingJob';
|
import {AlbumCoverFillingJob} from './jobs/AlbumCoverFillingJob';
|
||||||
import {GPXCompressionJob} from './jobs/GPXCompressionJob';
|
import {GPXCompressionJob} from './jobs/GPXCompressionJob';
|
||||||
@ -33,6 +32,10 @@ export class JobRepository {
|
|||||||
}
|
}
|
||||||
this.availableJobs[job.Name] = job;
|
this.availableJobs[job.Name] = job;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exists(name: string) {
|
||||||
|
return !!this.availableJobs[name];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
JobRepository.Instance.register(new IndexingJob());
|
JobRepository.Instance.register(new IndexingJob());
|
||||||
@ -41,7 +44,6 @@ JobRepository.Instance.register(new AlbumCoverFillingJob());
|
|||||||
JobRepository.Instance.register(new AlbumCoverRestJob());
|
JobRepository.Instance.register(new AlbumCoverRestJob());
|
||||||
JobRepository.Instance.register(new VideoConvertingJob());
|
JobRepository.Instance.register(new VideoConvertingJob());
|
||||||
JobRepository.Instance.register(new PhotoConvertingJob());
|
JobRepository.Instance.register(new PhotoConvertingJob());
|
||||||
JobRepository.Instance.register(new ThumbnailGenerationJob());
|
|
||||||
JobRepository.Instance.register(new GPXCompressionJob());
|
JobRepository.Instance.register(new GPXCompressionJob());
|
||||||
JobRepository.Instance.register(new TempFolderCleaningJob());
|
JobRepository.Instance.register(new TempFolderCleaningJob());
|
||||||
JobRepository.Instance.register(new AlbumRestJob());
|
JobRepository.Instance.register(new AlbumRestJob());
|
||||||
|
@ -2,26 +2,94 @@ import {Config} from '../../../../common/config/private/Config';
|
|||||||
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
||||||
import {FileJob} from './FileJob';
|
import {FileJob} from './FileJob';
|
||||||
import {PhotoProcessing} from '../../fileaccess/fileprocessing/PhotoProcessing';
|
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']];
|
public readonly Name = DefaultsJobs[DefaultsJobs['Photo Converting']];
|
||||||
|
|
||||||
constructor() {
|
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 {
|
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> {
|
protected async shouldProcess(mPath: string): Promise<boolean> {
|
||||||
return !(await PhotoProcessing.convertedPhotoExist(
|
for (const item of this.config.sizes) {
|
||||||
mPath,
|
if (!(await PhotoProcessing.convertedPhotoExist(mPath, item))) {
|
||||||
Config.Media.Photo.Converting.resolution
|
return true;
|
||||||
));
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async processFile(mPath: string): Promise<void> {
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -22,7 +22,7 @@ export abstract class Messenger<C extends Record<string, unknown> = Record<strin
|
|||||||
private async getThumbnail(m: MediaDTO) {
|
private async getThumbnail(m: MediaDTO) {
|
||||||
return await PhotoProcessing.generateThumbnail(
|
return await PhotoProcessing.generateThumbnail(
|
||||||
path.join(ProjectPath.ImageFolder, m.directory.path, m.directory.name, m.name),
|
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,
|
MediaDTOUtils.isPhoto(m) ? ThumbnailSourceType.Photo : ThumbnailSourceType.Video,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,6 @@ import {UserRoles} from '../../common/entities/UserDTO';
|
|||||||
import {ThumbnailSourceType} from '../model/fileaccess/PhotoWorker';
|
import {ThumbnailSourceType} from '../model/fileaccess/PhotoWorker';
|
||||||
import {VersionMWs} from '../middlewares/VersionMWs';
|
import {VersionMWs} from '../middlewares/VersionMWs';
|
||||||
import {SupportedFormats} from '../../common/SupportedFormats';
|
import {SupportedFormats} from '../../common/SupportedFormats';
|
||||||
import {PhotoConverterMWs} from '../middlewares/thumbnail/PhotoConverterMWs';
|
|
||||||
import {ServerTimingMWs} from '../middlewares/ServerTimingMWs';
|
import {ServerTimingMWs} from '../middlewares/ServerTimingMWs';
|
||||||
import {MetaFileMWs} from '../middlewares/MetaFileMWs';
|
import {MetaFileMWs} from '../middlewares/MetaFileMWs';
|
||||||
import {Config} from '../../common/config/private/Config';
|
import {Config} from '../../common/config/private/Config';
|
||||||
@ -16,9 +15,8 @@ export class GalleryRouter {
|
|||||||
public static route(app: Express): void {
|
public static route(app: Express): void {
|
||||||
this.addGetImageIcon(app);
|
this.addGetImageIcon(app);
|
||||||
this.addGetVideoIcon(app);
|
this.addGetVideoIcon(app);
|
||||||
this.addGetPhotoThumbnail(app);
|
this.addGetResizedPhoto(app);
|
||||||
this.addGetVideoThumbnail(app);
|
this.addGetVideoThumbnail(app);
|
||||||
this.addGetBestFitImage(app);
|
|
||||||
this.addGetImage(app);
|
this.addGetImage(app);
|
||||||
this.addGetBestFitVideo(app);
|
this.addGetBestFitVideo(app);
|
||||||
this.addGetVideo(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 {
|
protected static addGetVideo(app: Express): void {
|
||||||
app.get(
|
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(
|
app.get(
|
||||||
Config.Server.apiPath + '/gallery/content/:mediaPath(*.(' +
|
Config.Server.apiPath + '/gallery/content/:mediaPath(*.(' +
|
||||||
SupportedFormats.Photos.join('|') +
|
SupportedFormats.Photos.join('|') +
|
||||||
'))/thumbnail/:size?',
|
'))/:size',
|
||||||
// common part
|
// common part
|
||||||
AuthenticationMWs.authenticate,
|
AuthenticationMWs.authenticate,
|
||||||
AuthenticationMWs.normalizePathParam('mediaPath'),
|
AuthenticationMWs.normalizePathParam('mediaPath'),
|
||||||
@ -219,7 +202,7 @@ export class GalleryRouter {
|
|||||||
app.get(
|
app.get(
|
||||||
Config.Server.apiPath + '/gallery/content/:mediaPath(*.(' +
|
Config.Server.apiPath + '/gallery/content/:mediaPath(*.(' +
|
||||||
SupportedFormats.Videos.join('|') +
|
SupportedFormats.Videos.join('|') +
|
||||||
'))/thumbnail/:size?',
|
'))/:size?',
|
||||||
// common part
|
// common part
|
||||||
AuthenticationMWs.authenticate,
|
AuthenticationMWs.authenticate,
|
||||||
AuthenticationMWs.normalizePathParam('mediaPath'),
|
AuthenticationMWs.normalizePathParam('mediaPath'),
|
||||||
|
@ -3,11 +3,12 @@ export type backendText = number;
|
|||||||
export const backendTexts = {
|
export const backendTexts = {
|
||||||
indexedFilesOnly: {name: 10, description: 12},
|
indexedFilesOnly: {name: 10, description: 12},
|
||||||
sizeToGenerate: {name: 20, description: 22},
|
sizeToGenerate: {name: 20, description: 22},
|
||||||
|
maxVideoSize: {name: 23, description: 24},
|
||||||
indexChangesOnly: {name: 30, description: 32},
|
indexChangesOnly: {name: 30, description: 32},
|
||||||
mediaPick: {name: 40, description: 42},
|
mediaPick: {name: 40, description: 42},
|
||||||
emailTo: {name: 70, description: 72},
|
emailTo: {name: 70, description: 72},
|
||||||
emailSubject: {name: 90, description: 92},
|
emailSubject: {name: 90, description: 92},
|
||||||
emailText: {name: 100, description: 102},
|
emailText: {name: 100, description: 102},
|
||||||
messenger: {name: 110,description: 112}
|
messenger: {name: 110, description: 112}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
export class Utils {
|
export class Utils {
|
||||||
static GUID(): string {
|
static GUID(): string {
|
||||||
const s4 = (): string =>
|
const s4 = (): string =>
|
||||||
Math.floor((1 + Math.random()) * 0x10000)
|
Math.floor((1 + Math.random()) * 0x10000)
|
||||||
.toString(16)
|
.toString(16)
|
||||||
.substring(1);
|
.substring(1);
|
||||||
|
|
||||||
return s4() + s4() + '-' + s4() + s4();
|
return s4() + s4() + '-' + s4() + s4();
|
||||||
}
|
}
|
||||||
@ -135,8 +135,8 @@ export class Utils {
|
|||||||
|
|
||||||
public static canonizePath(path: string): string {
|
public static canonizePath(path: string): string {
|
||||||
return path
|
return path
|
||||||
.replace(new RegExp('\\\\', 'g'), '/')
|
.replace(new RegExp('\\\\', 'g'), '/')
|
||||||
.replace(new RegExp('/+', 'g'), '/');
|
.replace(new RegExp('/+', 'g'), '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
static concatUrls(...args: string[]): string {
|
static concatUrls(...args: string[]): string {
|
||||||
@ -262,9 +262,9 @@ export class Utils {
|
|||||||
const E = Math.pow(10, 38);
|
const E = Math.pow(10, 38);
|
||||||
const nE = Math.pow(10, -38);
|
const nE = Math.pow(10, -38);
|
||||||
return (
|
return (
|
||||||
!isNaN(value) &&
|
!isNaN(value) &&
|
||||||
((value >= -3.402823466 * E && value <= -1.175494351 * nE) ||
|
((value >= -3.402823466 * E && value <= -1.175494351 * nE) ||
|
||||||
(value <= 3.402823466 * E && value >= 1.175494351 * nE))
|
(value <= 3.402823466 * E && value >= 1.175494351 * nE))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,11 +16,9 @@ import {
|
|||||||
ClientMediaConfig,
|
ClientMediaConfig,
|
||||||
ClientMetaFileConfig,
|
ClientMetaFileConfig,
|
||||||
ClientPhotoConfig,
|
ClientPhotoConfig,
|
||||||
ClientPhotoConvertingConfig,
|
|
||||||
ClientServiceConfig,
|
ClientServiceConfig,
|
||||||
ClientSharingConfig,
|
ClientSharingConfig,
|
||||||
ClientSortingConfig,
|
ClientSortingConfig,
|
||||||
ClientThumbnailConfig,
|
|
||||||
ClientUserConfig,
|
ClientUserConfig,
|
||||||
ClientVideoConfig,
|
ClientVideoConfig,
|
||||||
ConfigPriority,
|
ConfigPriority,
|
||||||
@ -299,7 +297,7 @@ export class ServerUserConfig extends ClientUserConfig {
|
|||||||
|
|
||||||
|
|
||||||
@SubConfigClass({softReadonly: true})
|
@SubConfigClass({softReadonly: true})
|
||||||
export class ServerThumbnailConfig extends ClientThumbnailConfig {
|
export class ServerPhotoConfig extends ClientPhotoConfig {
|
||||||
@ConfigProperty({
|
@ConfigProperty({
|
||||||
tags:
|
tags:
|
||||||
{
|
{
|
||||||
@ -664,16 +662,10 @@ export class ServerJobConfig {
|
|||||||
{}
|
{}
|
||||||
),
|
),
|
||||||
new JobScheduleConfig(
|
new JobScheduleConfig(
|
||||||
DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
|
DefaultsJobs[DefaultsJobs['Photo Converting']],
|
||||||
DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
|
DefaultsJobs[DefaultsJobs['Photo Converting']],
|
||||||
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Album Cover Filling']]),
|
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Album Cover Filling']]),
|
||||||
{sizes: [240], indexedOnly: true}
|
{sizes: [320], maxVideoSize: 800, indexedOnly: true}
|
||||||
),
|
|
||||||
new JobScheduleConfig(
|
|
||||||
DefaultsJobs[DefaultsJobs['Photo Converting']],
|
|
||||||
DefaultsJobs[DefaultsJobs['Photo Converting']],
|
|
||||||
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]),
|
|
||||||
{indexedOnly: true}
|
|
||||||
),
|
),
|
||||||
new JobScheduleConfig(
|
new JobScheduleConfig(
|
||||||
DefaultsJobs[DefaultsJobs['Video Converting']],
|
DefaultsJobs[DefaultsJobs['Video Converting']],
|
||||||
@ -823,44 +815,6 @@ export class ServerVideoConfig extends ClientVideoConfig {
|
|||||||
transcoding: VideoTranscodingConfig = new VideoTranscodingConfig();
|
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})
|
@SubConfigClass({softReadonly: true})
|
||||||
export class ServerAlbumCoverConfig {
|
export class ServerAlbumCoverConfig {
|
||||||
@ -951,23 +905,10 @@ export class ServerMediaConfig extends ClientMediaConfig {
|
|||||||
name: $localize`Photo`,
|
name: $localize`Photo`,
|
||||||
uiIcon: 'ionCameraOutline',
|
uiIcon: 'ionCameraOutline',
|
||||||
priority: ConfigPriority.advanced,
|
priority: ConfigPriority.advanced,
|
||||||
uiJob: [
|
uiJob: [{job: DefaultsJobs[DefaultsJobs['Photo Converting']]}]
|
||||||
{
|
|
||||||
job: DefaultsJobs[DefaultsJobs['Photo Converting']],
|
|
||||||
relevant: (c) => c.Media.Photo.Converting.enabled
|
|
||||||
}]
|
|
||||||
} as TAGS
|
} as TAGS
|
||||||
})
|
})
|
||||||
Photo: ServerPhotoConfig = new ServerPhotoConfig();
|
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})
|
@SubConfigClass({softReadonly: true})
|
||||||
|
@ -620,60 +620,6 @@ export class ClientMapConfig {
|
|||||||
bendLongPathsTrigger: number = 0.5;
|
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 {
|
export enum NavigationLinkTypes {
|
||||||
gallery = 1, faces, albums, search, url
|
gallery = 1, faces, albums, search, url
|
||||||
}
|
}
|
||||||
@ -1004,6 +950,16 @@ export class ClientLightboxConfig {
|
|||||||
loopVideos: boolean = false;
|
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({
|
@ConfigProperty({
|
||||||
tags: {
|
tags: {
|
||||||
name: $localize`Titles`,
|
name: $localize`Titles`,
|
||||||
@ -1156,8 +1112,10 @@ export class ClientGalleryConfig {
|
|||||||
@ConfigProperty({
|
@ConfigProperty({
|
||||||
tags: {
|
tags: {
|
||||||
name: $localize`Lightbox`,
|
name: $localize`Lightbox`,
|
||||||
|
uiIcon: 'ionImageOutline',
|
||||||
priority: ConfigPriority.advanced,
|
priority: ConfigPriority.advanced,
|
||||||
},
|
} as TAGS,
|
||||||
|
description: $localize`Photo and video preview window.`
|
||||||
})
|
})
|
||||||
Lightbox: ClientLightboxConfig = new ClientLightboxConfig();
|
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})
|
@SubConfigClass({tags: {client: true}, softReadonly: true})
|
||||||
export class ClientPhotoConfig {
|
export class ClientPhotoConfig {
|
||||||
|
|
||||||
|
|
||||||
@ConfigProperty({
|
@ConfigProperty({
|
||||||
|
type: 'unsignedInt', max: 100,
|
||||||
tags: {
|
tags: {
|
||||||
name: $localize`Photo converting`,
|
name: $localize`Map Icon size`,
|
||||||
priority: ConfigPriority.advanced
|
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({
|
@ConfigProperty({
|
||||||
arrayType: 'string',
|
arrayType: 'string',
|
||||||
@ -1288,13 +1274,6 @@ export class ClientGPXCompressingConfig {
|
|||||||
|
|
||||||
@SubConfigClass({tags: {client: true}, softReadonly: true})
|
@SubConfigClass({tags: {client: true}, softReadonly: true})
|
||||||
export class ClientMediaConfig {
|
export class ClientMediaConfig {
|
||||||
@ConfigProperty({
|
|
||||||
tags: {
|
|
||||||
name: $localize`Thumbnail`,
|
|
||||||
priority: ConfigPriority.advanced
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Thumbnail: ClientThumbnailConfig = new ClientThumbnailConfig();
|
|
||||||
@ConfigProperty({
|
@ConfigProperty({
|
||||||
tags: {
|
tags: {
|
||||||
name: $localize`Video`,
|
name: $localize`Video`,
|
||||||
|
@ -4,8 +4,7 @@ export enum DefaultsJobs {
|
|||||||
Indexing = 1,
|
Indexing = 1,
|
||||||
'Gallery Reset' = 2,
|
'Gallery Reset' = 2,
|
||||||
'Video Converting' = 3,
|
'Video Converting' = 3,
|
||||||
'Photo Converting' = 4,
|
'Photo Converting' = 5,
|
||||||
'Thumbnail Generation' = 5,
|
|
||||||
'Temp Folder Cleaning' = 6,
|
'Temp Folder Cleaning' = 6,
|
||||||
'Album Cover Filling' = 7,
|
'Album Cover Filling' = 7,
|
||||||
'Album Cover Reset' = 8,
|
'Album Cover Reset' = 8,
|
||||||
|
@ -14,6 +14,10 @@ export class BackendtextService {
|
|||||||
return $localize`Size to generate`;
|
return $localize`Size to generate`;
|
||||||
case backendTexts.sizeToGenerate.description:
|
case backendTexts.sizeToGenerate.description:
|
||||||
return $localize`These thumbnails will be generated. The list should be a subset of the enabled thumbnail sizes`;
|
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:
|
case backendTexts.indexedFilesOnly.name:
|
||||||
return $localize`Indexed only`;
|
return $localize`Indexed only`;
|
||||||
case backendTexts.indexedFilesOnly.description:
|
case backendTexts.indexedFilesOnly.description:
|
||||||
@ -58,8 +62,6 @@ export class BackendtextService {
|
|||||||
return $localize`Gallery reset`;
|
return $localize`Gallery reset`;
|
||||||
case DefaultsJobs['Album Reset']:
|
case DefaultsJobs['Album Reset']:
|
||||||
return $localize`Album reset`;
|
return $localize`Album reset`;
|
||||||
case DefaultsJobs['Thumbnail Generation']:
|
|
||||||
return $localize`Thumbnail generation`;
|
|
||||||
case DefaultsJobs['Photo Converting']:
|
case DefaultsJobs['Photo Converting']:
|
||||||
return $localize`Photo converting`;
|
return $localize`Photo converting`;
|
||||||
case DefaultsJobs['Video Converting']:
|
case DefaultsJobs['Video Converting']:
|
||||||
@ -92,10 +94,8 @@ export class BackendtextService {
|
|||||||
return $localize`Deletes all directories, photos and videos from the DB.`;
|
return $localize`Deletes all directories, photos and videos from the DB.`;
|
||||||
case DefaultsJobs['Album Reset']:
|
case DefaultsJobs['Album Reset']:
|
||||||
return $localize`Removes all albums from the DB`;
|
return $localize`Removes all albums from the DB`;
|
||||||
case DefaultsJobs['Thumbnail Generation']:
|
case DefaultsJobs['Photo Converting']:
|
||||||
return $localize`Generates thumbnails 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['Photo Converting']:
|
|
||||||
return $localize`Generates high res photos from all media files and stores them in the tmp folder.`;
|
|
||||||
case DefaultsJobs['Video Converting']:
|
case DefaultsJobs['Video Converting']:
|
||||||
return $localize`Transcodes all videos and stores them in the tmp folder.`;
|
return $localize`Transcodes all videos and stores them in the tmp folder.`;
|
||||||
case DefaultsJobs['Temp Folder Cleaning']:
|
case DefaultsJobs['Temp Folder Cleaning']:
|
||||||
|
@ -4,8 +4,6 @@ import {Config} from '../../../../common/config/public/Config';
|
|||||||
import {MediaDTO, MediaDTOUtils} from '../../../../common/entities/MediaDTO';
|
import {MediaDTO, MediaDTOUtils} from '../../../../common/entities/MediaDTO';
|
||||||
|
|
||||||
export class Media extends MediaIcon {
|
export class Media extends MediaIcon {
|
||||||
static readonly sortedThumbnailSizes =
|
|
||||||
Config.Media.Thumbnail.thumbnailSizes.sort((a, b): number => a - b);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
media: MediaDTO,
|
media: MediaDTO,
|
||||||
@ -27,8 +25,7 @@ export class Media extends MediaIcon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getThumbnailSize(): number {
|
getThumbnailSize(): number {
|
||||||
const longerEdge = Math.max(this.renderWidth, this.renderHeight);
|
return this.getMediaSize(this.renderWidth,this.renderHeight);
|
||||||
return Utils.findClosestinSorted(longerEdge, Media.sortedThumbnailSizes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getReplacementThumbnailSize(): number {
|
getReplacementThumbnailSize(): number {
|
||||||
@ -37,7 +34,7 @@ export class Media extends MediaIcon {
|
|||||||
|
|
||||||
const size = this.getThumbnailSize();
|
const size = this.getThumbnailSize();
|
||||||
if (this.media.missingThumbnails) {
|
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
|
// eslint-disable-next-line no-bitwise
|
||||||
if (
|
if (
|
||||||
(this.media.missingThumbnails & MediaIcon.ThumbnailMap[thSize]) ===
|
(this.media.missingThumbnails & MediaIcon.ThumbnailMap[thSize]) ===
|
||||||
@ -73,7 +70,6 @@ export class Media extends MediaIcon {
|
|||||||
Config.Server.apiPath,
|
Config.Server.apiPath,
|
||||||
'/gallery/content/',
|
'/gallery/content/',
|
||||||
this.getRelativePath(),
|
this.getRelativePath(),
|
||||||
'thumbnail',
|
|
||||||
size.toString()
|
size.toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -83,14 +79,6 @@ export class Media extends MediaIcon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getThumbnailPath(): string {
|
getThumbnailPath(): string {
|
||||||
const size = this.getThumbnailSize();
|
return this.getBestSizedMediaPath(this.renderWidth,this.renderHeight);
|
||||||
return Utils.concatUrls(
|
|
||||||
Config.Server.urlBase,
|
|
||||||
Config.Server.apiPath,
|
|
||||||
'/gallery/content/',
|
|
||||||
this.getRelativePath(),
|
|
||||||
'thumbnail',
|
|
||||||
size.toString()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,10 @@ import {MediaDTO} from '../../../../common/entities/MediaDTO';
|
|||||||
|
|
||||||
export class MediaIcon {
|
export class MediaIcon {
|
||||||
protected static readonly ThumbnailMap =
|
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;
|
protected replacementSizeCache: number | boolean = false;
|
||||||
|
|
||||||
@ -17,79 +20,112 @@ export class MediaIcon {
|
|||||||
|
|
||||||
iconLoaded(): void {
|
iconLoaded(): void {
|
||||||
this.media.missingThumbnails -=
|
this.media.missingThumbnails -=
|
||||||
MediaIcon.ThumbnailMap[Config.Media.Thumbnail.iconSize];
|
MediaIcon.ThumbnailMap[Config.Media.Photo.iconSize];
|
||||||
}
|
}
|
||||||
|
|
||||||
isIconAvailable(): boolean {
|
isIconAvailable(): boolean {
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
return (
|
return (
|
||||||
(this.media.missingThumbnails &
|
(this.media.missingThumbnails &
|
||||||
MediaIcon.ThumbnailMap[Config.Media.Thumbnail.iconSize]) ===
|
MediaIcon.ThumbnailMap[Config.Media.Photo.iconSize]) ===
|
||||||
0
|
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 {
|
getReadableRelativePath(): string {
|
||||||
return Utils.concatUrls(
|
return Utils.concatUrls(
|
||||||
this.media.directory.path,
|
this.media.directory.path,
|
||||||
this.media.directory.name,
|
this.media.directory.name,
|
||||||
this.media.name
|
this.media.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRelativePath(): string {
|
getRelativePath(): string {
|
||||||
return (
|
return (
|
||||||
encodeURI(
|
encodeURI(
|
||||||
this.getReadableRelativePath()
|
this.getReadableRelativePath()
|
||||||
)
|
)
|
||||||
// do not escape all urls with encodeURIComponent because that make the URL ugly and not needed
|
// do not escape all urls with encodeURIComponent because that make the URL ugly and not needed
|
||||||
// do not escape before concatUrls as that would make prevent optimizations
|
// do not escape before concatUrls as that would make prevent optimizations
|
||||||
// .replace(new RegExp('%', 'g'), '%25') // order important
|
// .replace(new RegExp('%', 'g'), '%25') // order important
|
||||||
.replace(new RegExp('#', 'g'), '%23')
|
.replace(new RegExp('#', 'g'), '%23')
|
||||||
.replace(new RegExp('\\$', 'g'), '%24')
|
.replace(new RegExp('\\$', 'g'), '%24')
|
||||||
.replace(new RegExp('\\?', 'g'), '%3F')
|
.replace(new RegExp('\\?', 'g'), '%3F')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getIconPath(): string {
|
getIconPath(): string {
|
||||||
return Utils.concatUrls(
|
return Utils.concatUrls(
|
||||||
Config.Server.urlBase,
|
Config.Server.urlBase,
|
||||||
Config.Server.apiPath,
|
Config.Server.apiPath,
|
||||||
'/gallery/content/',
|
'/gallery/content/',
|
||||||
this.getRelativePath(),
|
this.getRelativePath(),
|
||||||
'icon'
|
'icon'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMediaPath(): string {
|
getOriginalMediaPath(): string {
|
||||||
return Utils.concatUrls(
|
return Utils.concatUrls(
|
||||||
Config.Server.urlBase,
|
Config.Server.urlBase,
|
||||||
Config.Server.apiPath,
|
Config.Server.apiPath,
|
||||||
'/gallery/content/',
|
'/gallery/content/',
|
||||||
this.getRelativePath()
|
this.getRelativePath()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBestFitMediaPath(): string {
|
getMediaSize(renderWidth: number, renderHeight: number): number {
|
||||||
return Utils.concatUrls(this.getMediaPath(), '/bestFit');
|
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 {
|
equals(other: MediaDTO | MediaIcon): boolean {
|
||||||
// is gridphoto
|
// is gridphoto
|
||||||
if (other instanceof MediaIcon) {
|
if (other instanceof MediaIcon) {
|
||||||
return (
|
return (
|
||||||
this.media.directory.path === other.media.directory.path &&
|
this.media.directory.path === other.media.directory.path &&
|
||||||
this.media.directory.name === other.media.directory.name &&
|
this.media.directory.name === other.media.directory.name &&
|
||||||
this.media.name === other.media.name
|
this.media.name === other.media.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// is media
|
// is media
|
||||||
if (other.directory) {
|
if (other.directory) {
|
||||||
return (
|
return (
|
||||||
this.media.directory.path === other.directory.path &&
|
this.media.directory.path === other.directory.path &&
|
||||||
this.media.directory.name === other.directory.name &&
|
this.media.directory.name === other.directory.name &&
|
||||||
this.media.name === other.name
|
this.media.name === other.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
role="menu" aria-labelledby="button-basic">
|
role="menu" aria-labelledby="button-basic">
|
||||||
<li role="menuitem">
|
<li role="menuitem">
|
||||||
<a *ngIf="activePhoto"
|
<a *ngIf="activePhoto"
|
||||||
[href]="activePhoto.gridMedia.getMediaPath()"
|
[href]="activePhoto.gridMedia.getOriginalMediaPath()"
|
||||||
[download]="activePhoto.gridMedia.media.name"
|
[download]="activePhoto.gridMedia.media.name"
|
||||||
class="dropdown-item">
|
class="dropdown-item">
|
||||||
<ng-icon class="me-2"
|
<ng-icon class="me-2"
|
||||||
|
@ -251,7 +251,7 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges {
|
|||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.setAttribute('type', 'hidden');
|
link.setAttribute('type', 'hidden');
|
||||||
link.href = this.activePhoto.gridMedia.getMediaPath();
|
link.href = this.activePhoto.gridMedia.getOriginalMediaPath();
|
||||||
link.download = this.activePhoto.gridMedia.media.name;
|
link.download = this.activePhoto.gridMedia.media.name;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
(error)="onImageError()"
|
(error)="onImageError()"
|
||||||
(timeupdate)="onVideoProgress()"
|
(timeupdate)="onVideoProgress()"
|
||||||
#video>
|
#video>
|
||||||
<source [src]="gridMedia.getBestFitMediaPath()" (error)="onSourceError()">
|
<source [src]="gridMedia.getBestFitVideoPath()" (error)="onSourceError()">
|
||||||
Something went wrong.
|
Something went wrong.
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
|
@ -48,17 +48,17 @@ export class GalleryLightboxMediaComponent implements OnChanges {
|
|||||||
|
|
||||||
get ImageTransform(): SafeStyle {
|
get ImageTransform(): SafeStyle {
|
||||||
return this.sanitizer.bypassSecurityTrustStyle(
|
return this.sanitizer.bypassSecurityTrustStyle(
|
||||||
'scale(' +
|
'scale(' +
|
||||||
this.zoom +
|
this.zoom +
|
||||||
') translate(calc(' +
|
') translate(calc(' +
|
||||||
-50 / this.zoom +
|
-50 / this.zoom +
|
||||||
'% + ' +
|
'% + ' +
|
||||||
this.drag.x / this.zoom +
|
this.drag.x / this.zoom +
|
||||||
'px), calc(' +
|
'px), calc(' +
|
||||||
-50 / this.zoom +
|
-50 / this.zoom +
|
||||||
'% + ' +
|
'% + ' +
|
||||||
this.drag.y / this.zoom +
|
this.drag.y / this.zoom +
|
||||||
'px))'
|
'px))'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ export class GalleryLightboxMediaComponent implements OnChanges {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.video.nativeElement.currentTime =
|
this.video.nativeElement.currentTime =
|
||||||
this.video.nativeElement.duration * (value / 100);
|
this.video.nativeElement.duration * (value / 100);
|
||||||
if (this.video.nativeElement.paused) {
|
if (this.video.nativeElement.paused) {
|
||||||
this.video.nativeElement.play().catch(console.error);
|
this.video.nativeElement.play().catch(console.error);
|
||||||
}
|
}
|
||||||
@ -134,9 +134,9 @@ export class GalleryLightboxMediaComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
this.setImageSize();
|
this.setImageSize();
|
||||||
if (
|
if (
|
||||||
this.thumbnailSrc == null &&
|
this.thumbnailSrc == null &&
|
||||||
this.gridMedia &&
|
this.gridMedia &&
|
||||||
this.ThumbnailUrl !== null
|
this.ThumbnailUrl !== null
|
||||||
) {
|
) {
|
||||||
this.thumbnailSrc = this.ThumbnailUrl;
|
this.thumbnailSrc = this.ThumbnailUrl;
|
||||||
}
|
}
|
||||||
@ -167,8 +167,8 @@ export class GalleryLightboxMediaComponent implements OnChanges {
|
|||||||
// TODO:handle error
|
// TODO:handle error
|
||||||
this.imageLoadFinished.this = true;
|
this.imageLoadFinished.this = true;
|
||||||
console.error(
|
console.error(
|
||||||
'Error: cannot load media for lightbox url: ' +
|
'Error: cannot load media for lightbox url: ' +
|
||||||
this.gridMedia.getBestFitMediaPath()
|
this.gridMedia.getBestSizedMediaPath(window.innerWidth, window.innerHeight)
|
||||||
);
|
);
|
||||||
this.loadNextPhoto();
|
this.loadNextPhoto();
|
||||||
}
|
}
|
||||||
@ -181,11 +181,12 @@ export class GalleryLightboxMediaComponent implements OnChanges {
|
|||||||
|
|
||||||
public showThumbnail(): boolean {
|
public showThumbnail(): boolean {
|
||||||
return (
|
return (
|
||||||
this.gridMedia &&
|
this.gridMedia &&
|
||||||
!this.mediaLoaded &&
|
!this.mediaLoaded &&
|
||||||
this.thumbnailSrc !== null &&
|
this.thumbnailSrc !== null &&
|
||||||
(this.gridMedia.isThumbnailAvailable() ||
|
!this.gridMedia.isPhotoAvailable(window.innerWidth, window.innerHeight) &&
|
||||||
this.gridMedia.isReplacementThumbnailAvailable())
|
(this.gridMedia.isThumbnailAvailable() ||
|
||||||
|
this.gridMedia.isReplacementThumbnailAvailable())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,11 +209,7 @@ export class GalleryLightboxMediaComponent implements OnChanges {
|
|||||||
this.imageLoadFinished.next = true;
|
this.imageLoadFinished.next = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Config.Media.Photo.Converting.enabled === true) {
|
this.nextImage.src = this.nextGridMedia.getBestSizedMediaPath(window.innerWidth, window.innerHeight);
|
||||||
this.nextImage.src = this.nextGridMedia.getBestFitMediaPath();
|
|
||||||
} else {
|
|
||||||
this.nextImage.src = this.nextGridMedia.getMediaPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.nextImage.onload = () => this.imageLoadFinished.next = true;
|
this.nextImage.onload = () => this.imageLoadFinished.next = true;
|
||||||
this.nextImage.onerror = () => {
|
this.nextImage.onerror = () => {
|
||||||
@ -231,29 +228,24 @@ export class GalleryLightboxMediaComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.zoom === 1 ||
|
this.zoom === 1 ||
|
||||||
Config.Media.Photo.Converting.loadFullImageOnZoom === false
|
Config.Gallery.Lightbox.loadFullImageOnZoom === false
|
||||||
) {
|
) {
|
||||||
if (this.photo.src == null) {
|
if (this.photo.src == null) {
|
||||||
if (Config.Media.Photo.Converting.enabled === true) {
|
this.photo.src = this.gridMedia.getBestSizedMediaPath(window.innerWidth, window.innerHeight);
|
||||||
this.photo.src = this.gridMedia.getBestFitMediaPath();
|
this.photo.isBestFit = true;
|
||||||
this.photo.isBestFit = true;
|
|
||||||
} else {
|
|
||||||
this.photo.src = this.gridMedia.getMediaPath();
|
|
||||||
this.photo.isBestFit = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// on zoom load high res photo
|
// on zoom load high res photo
|
||||||
} else if (this.photo.isBestFit === true || this.photo.src == null) {
|
} 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;
|
this.photo.isBestFit = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onVideoProgress(): void {
|
public onVideoProgress(): void {
|
||||||
this.videoProgress =
|
this.videoProgress =
|
||||||
(100 / this.video.nativeElement.duration) *
|
(100 / this.video.nativeElement.duration) *
|
||||||
this.video.nativeElement.currentTime;
|
this.video.nativeElement.currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setImageSize(): void {
|
private setImageSize(): void {
|
||||||
|
@ -72,12 +72,12 @@ export class GalleryMapLightboxComponent implements OnChanges, OnDestroy {
|
|||||||
defLayer: TileLayer;
|
defLayer: TileLayer;
|
||||||
darkLayer: TileLayer;
|
darkLayer: TileLayer;
|
||||||
private smallIconSize = new Point(
|
private smallIconSize = new Point(
|
||||||
Config.Media.Thumbnail.iconSize * 0.75,
|
Config.Media.Photo.iconSize * 0.75,
|
||||||
Config.Media.Thumbnail.iconSize * 0.75
|
Config.Media.Photo.iconSize * 0.75
|
||||||
);
|
);
|
||||||
private iconSize = new Point(
|
private iconSize = new Point(
|
||||||
Config.Media.Thumbnail.iconSize,
|
Config.Media.Photo.iconSize,
|
||||||
Config.Media.Thumbnail.iconSize
|
Config.Media.Photo.iconSize
|
||||||
);
|
);
|
||||||
private usedIconSize = this.iconSize;
|
private usedIconSize = this.iconSize;
|
||||||
private mapLayersControlOption: LeafletControlLayersConfig & {
|
private mapLayersControlOption: LeafletControlLayersConfig & {
|
||||||
|
@ -25,7 +25,7 @@ export class ThumbnailLoaderService {
|
|||||||
if (
|
if (
|
||||||
this.que.length === 0 ||
|
this.que.length === 0 ||
|
||||||
this.runningRequests >=
|
this.runningRequests >=
|
||||||
Config.Media.Thumbnail.concurrentThumbnailGenerations
|
Config.Media.Photo.concurrentThumbnailGenerations
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,9 @@ export class ScheduledJobsService {
|
|||||||
this.availableMessengers = new BehaviorSubject([]);
|
this.availableMessengers = new BehaviorSubject([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValidJob(name: string): boolean {
|
||||||
|
return !!this.availableJobs.value.find(j => j.Name === name);
|
||||||
|
}
|
||||||
|
|
||||||
public async getAvailableJobs(): Promise<void> {
|
public async getAvailableJobs(): Promise<void> {
|
||||||
this.availableJobs.next(
|
this.availableJobs.next(
|
||||||
|
@ -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 *ngFor="let schedule of sortedSchedules() as sortedSchedules; let i= index">
|
||||||
<div
|
<div
|
||||||
class="card bg-body-tertiary mt-2 mb-2 no-changed-settings {{shouldIdent(schedule,sortedSchedules[i-1])? 'ms-4' : ''}}">
|
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="card-header">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div (click)="showDetails[schedule.name]=!showDetails[schedule.name]">
|
<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"
|
<ng-icon style="font-size: 1.3em; margin-left: -4px"
|
||||||
[name]="showDetails[schedule.name] ? 'ionChevronDownOutline' : 'ionChevronForwardOutline'"></ng-icon>
|
[name]="showDetails[schedule.name] ? 'ionChevronDownOutline' : 'ionChevronForwardOutline'"></ng-icon>
|
||||||
{{schedule.name}}
|
{{ schedule.name }}
|
||||||
<ng-container [ngSwitch]="schedule.trigger.type">
|
<ng-container [ngSwitch]="schedule.trigger.type">
|
||||||
<ng-container *ngSwitchCase="JobTriggerType.periodic">
|
<ng-container *ngSwitchCase="JobTriggerType.periodic">
|
||||||
<span class="badge bg-primary" i18n>every</span>
|
<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>
|
||||||
<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>
|
<span class="badge bg-secondary" *ngSwitchCase="JobTriggerType.never" i18n>never</span>
|
||||||
<ng-container *ngSwitchCase="JobTriggerType.after">
|
<ng-container *ngSwitchCase="JobTriggerType.after">
|
||||||
<span class="badge bg-primary" i18n>after</span>
|
<span class="badge bg-primary" i18n>after</span>
|
||||||
{{$any(schedule.trigger).afterScheduleName}}
|
{{ $any(schedule.trigger).afterScheduleName }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
@ -27,6 +37,7 @@
|
|||||||
<ng-icon name="ionTrashOutline" title="Delete" i18n-title></ng-icon>
|
<ng-icon name="ionTrashOutline" title="Delete" i18n-title></ng-icon>
|
||||||
</button>
|
</button>
|
||||||
<app-settings-job-button class="ms-md-2 mt-2 mt-md-0"
|
<app-settings-job-button class="ms-md-2 mt-2 mt-md-0"
|
||||||
|
*ngIf="jobsService.isValidJob(schedule.jobName)"
|
||||||
(jobError)="error=$event"
|
(jobError)="error=$event"
|
||||||
[allowParallelRun]="schedule.allowParallelRun"
|
[allowParallelRun]="schedule.allowParallelRun"
|
||||||
[jobName]="schedule.jobName" [config]="schedule.config"
|
[jobName]="schedule.jobName" [config]="schedule.config"
|
||||||
@ -42,12 +53,13 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="alert alert-secondary" role="alert"
|
<div class="alert alert-secondary" role="alert"
|
||||||
*ngIf="settingsService.configStyle == ConfigStyle.full">
|
*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>
|
||||||
<div class="mb-1 row">
|
<div class="mb-1 row">
|
||||||
<label class="col-md-2 control-label" i18n>Job:</label>
|
<label class="col-md-2 control-label" i18n>Job:</label>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
{{backendTextService.getJobName(schedule.jobName)}}
|
{{ backendTextService.getJobName(schedule.jobName) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<app-settings-job-button class="float-end"
|
<app-settings-job-button class="float-end"
|
||||||
@ -68,7 +80,7 @@
|
|||||||
[id]="'repeatType'+i"
|
[id]="'repeatType'+i"
|
||||||
required>
|
required>
|
||||||
<option *ngFor="let jobTrigger of JobTriggerTypeMap"
|
<option *ngFor="let jobTrigger of JobTriggerTypeMap"
|
||||||
[ngValue]="jobTrigger.key">{{jobTrigger.value}}
|
[ngValue]="jobTrigger.key">{{ jobTrigger.value }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<small class="form-text text-muted" *ngIf="settingsService.configStyle == ConfigStyle.full"
|
<small class="form-text text-muted" *ngIf="settingsService.configStyle == ConfigStyle.full"
|
||||||
@ -89,7 +101,7 @@
|
|||||||
[id]="'triggerAfter'+i" required>
|
[id]="'triggerAfter'+i" required>
|
||||||
<ng-container *ngFor="let sch of sortedSchedules">
|
<ng-container *ngFor="let sch of sortedSchedules">
|
||||||
<option *ngIf="sch.name !== schedule.name"
|
<option *ngIf="sch.name !== schedule.name"
|
||||||
[ngValue]="sch.name">{{sch.name}}
|
[ngValue]="sch.name">{{ sch.name }}
|
||||||
</option>
|
</option>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</select>
|
</select>
|
||||||
@ -128,7 +140,7 @@
|
|||||||
<option *ngFor="let period of periods; let i = index"
|
<option *ngFor="let period of periods; let i = index"
|
||||||
[ngValue]="i">
|
[ngValue]="i">
|
||||||
<ng-container i18n>every</ng-container>
|
<ng-container i18n>every</ng-container>
|
||||||
{{period}}
|
{{ period }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<app-timestamp-timepicker
|
<app-timestamp-timepicker
|
||||||
@ -173,7 +185,7 @@
|
|||||||
*ngIf="!configEntry.validIf || schedule.config[configEntry.validIf.configFiled] == configEntry.validIf.equalsValue"
|
*ngIf="!configEntry.validIf || schedule.config[configEntry.validIf.configFiled] == configEntry.validIf.equalsValue"
|
||||||
[class.mb-3]="settingsService.configStyle == ConfigStyle.full">
|
[class.mb-3]="settingsService.configStyle == ConfigStyle.full">
|
||||||
<label class="col-md-2 control-label"
|
<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="col-md-10">
|
||||||
<div [class.input-group]="'MediaPickDTO-array'!=configEntry.type">
|
<div [class.input-group]="'MediaPickDTO-array'!=configEntry.type">
|
||||||
<ng-container [ngSwitch]="configEntry.type">
|
<ng-container [ngSwitch]="configEntry.type">
|
||||||
@ -235,7 +247,7 @@
|
|||||||
(ngModelChange)="onChange($event)"
|
(ngModelChange)="onChange($event)"
|
||||||
[(ngModel)]="schedule.config[configEntry.id]"
|
[(ngModel)]="schedule.config[configEntry.id]"
|
||||||
class="form-select">
|
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>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
@ -246,7 +258,9 @@
|
|||||||
<div class="mb-1 row"
|
<div class="mb-1 row"
|
||||||
[class.mb-3]="settingsService.configStyle == ConfigStyle.full">
|
[class.mb-3]="settingsService.configStyle == ConfigStyle.full">
|
||||||
<label class="col-md-2 control-label"
|
<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="col-md-10">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<app-gallery-search-field
|
<app-gallery-search-field
|
||||||
@ -363,7 +377,7 @@
|
|||||||
<small class="form-text text-muted" *ngIf="settingsService.configStyle == ConfigStyle.full">
|
<small class="form-text text-muted" *ngIf="settingsService.configStyle == ConfigStyle.full">
|
||||||
<ng-container *ngIf="configEntry.type == 'number-array'" i18n>';' separated integers.
|
<ng-container *ngIf="configEntry.type == 'number-array'" i18n>';' separated integers.
|
||||||
</ng-container>
|
</ng-container>
|
||||||
{{backendTextService.get(configEntry.description)}}
|
{{ backendTextService.get(configEntry.description) }}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -405,7 +419,7 @@
|
|||||||
[(ngModel)]="newSchedule.jobName"
|
[(ngModel)]="newSchedule.jobName"
|
||||||
name="newJobName" required>
|
name="newJobName" required>
|
||||||
<option *ngFor="let availableJob of jobsService.availableJobs | async"
|
<option *ngFor="let availableJob of jobsService.availableJobs | async"
|
||||||
[ngValue]="availableJob.Name">{{backendTextService.getJobName(availableJob.Name)}}
|
[ngValue]="availableJob.Name">{{ backendTextService.getJobName(availableJob.Name) }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<small class="form-text text-muted" *ngIf="settingsService.configStyle == ConfigStyle.full"
|
<small class="form-text text-muted" *ngIf="settingsService.configStyle == ConfigStyle.full"
|
||||||
|
@ -7,51 +7,26 @@ import {PhotoProcessing} from '../../../../../src/backend/model/fileaccess/filep
|
|||||||
|
|
||||||
describe('PhotoProcessing', () => {
|
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 () => {
|
it('should generate converted gif file path', async () => {
|
||||||
|
|
||||||
await Config.load();
|
await Config.load();
|
||||||
Config.Media.Thumbnail.thumbnailSizes = [];
|
Config.Media.Photo.thumbnailSizes = [];
|
||||||
ProjectPath.ImageFolder = path.join(__dirname, './../../../assets');
|
ProjectPath.ImageFolder = path.join(__dirname, './../../../assets');
|
||||||
const gifPath = path.join(ProjectPath.ImageFolder, 'earth.gif');
|
const gifPath = path.join(ProjectPath.ImageFolder, 'earth.gif');
|
||||||
|
|
||||||
Config.Media.Thumbnail.animateGif = true;
|
|
||||||
expect(await PhotoProcessing
|
|
||||||
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(gifPath,
|
|
||||||
Config.Media.Photo.Converting.resolution)))
|
|
||||||
.to.be.true;
|
|
||||||
|
|
||||||
Config.Media.Thumbnail.animateGif = false;
|
for (const thSize of Config.Media.Photo.thumbnailSizes) {
|
||||||
expect(await PhotoProcessing
|
Config.Media.Photo.animateGif = true;
|
||||||
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(gifPath,
|
|
||||||
Config.Media.Photo.Converting.resolution)))
|
expect(await PhotoProcessing
|
||||||
.to.be.true;
|
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(gifPath, thSize)))
|
||||||
|
.to.be.true;
|
||||||
|
|
||||||
|
Config.Media.Photo.animateGif = false;
|
||||||
|
expect(await PhotoProcessing
|
||||||
|
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(gifPath, thSize)))
|
||||||
|
.to.be.true;
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -60,12 +35,11 @@ describe('PhotoProcessing', () => {
|
|||||||
it('should generate converted thumbnail path', async () => {
|
it('should generate converted thumbnail path', async () => {
|
||||||
|
|
||||||
await Config.load();
|
await Config.load();
|
||||||
Config.Media.Photo.Converting.resolution = (null as any);
|
Config.Media.Photo.thumbnailSizes = [10, 20];
|
||||||
Config.Media.Thumbnail.thumbnailSizes = [10, 20];
|
|
||||||
ProjectPath.ImageFolder = path.join(__dirname, './../../../assets');
|
ProjectPath.ImageFolder = path.join(__dirname, './../../../assets');
|
||||||
const photoPath = path.join(ProjectPath.ImageFolder, 'test_png.png');
|
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
|
expect(await PhotoProcessing
|
||||||
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(photoPath, thSize)))
|
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(photoPath, thSize)))
|
||||||
.to.be.true;
|
.to.be.true;
|
||||||
|
Loading…
Reference in New Issue
Block a user