1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2025-01-14 14:43:17 +08:00

implementing video converting task

note: braking changes in database and config file
This commit is contained in:
Patrik J. Braun 2019-12-09 14:05:06 +01:00
parent 51801026cb
commit f8a361cb9e
54 changed files with 519 additions and 197 deletions

View File

@ -5,7 +5,7 @@ import {LogLevel} from '../common/config/private/IPrivateConfig';
export const winstonSettings = {
transports: [
new winston.transports.Console(<any>{
level: LogLevel[Config.Server.log.level],
level: LogLevel[Config.Server.Log.level],
handleExceptions: true,
json: false,
colorize: true,

View File

@ -1,12 +1,18 @@
import * as path from 'path';
import * as fs from 'fs';
import {Config} from '../common/config/private/Config';
class ProjectPathClass {
public Root: string;
public ImageFolder: string;
public ThumbnailFolder: string;
public TranscendedFolder: string;
public FrontendFolder: string;
constructor() {
this.reset();
}
isAbsolutePath(pathStr: string) {
return path.resolve(pathStr) === path.normalize(pathStr);
}
@ -19,15 +25,22 @@ class ProjectPathClass {
return this.isAbsolutePath(pathStr) ? pathStr : path.join(this.Root, pathStr);
}
constructor() {
this.reset();
getRelativePathToImages(pathStr: string): string {
return path.relative(this.ImageFolder, pathStr);
}
reset() {
this.Root = path.join(__dirname, '/../');
this.ImageFolder = this.getAbsolutePath(Config.Server.imagesFolder);
this.ThumbnailFolder = this.getAbsolutePath(Config.Server.thumbnail.folder);
this.ThumbnailFolder = this.getAbsolutePath(Config.Server.Thumbnail.folder);
this.TranscendedFolder = path.join(this.ThumbnailFolder, 'tc');
this.FrontendFolder = path.join(this.Root, 'dist');
// create thumbnail folder if not exist
if (!fs.existsSync(this.ThumbnailFolder)) {
fs.mkdirSync(this.ThumbnailFolder);
}
}
}

View File

@ -19,7 +19,7 @@ export class AdminMWs {
public static async loadStatistic(req: Request, res: Response, next: NextFunction) {
if (Config.Server.database.type === DatabaseType.memory) {
if (Config.Server.Database.type === DatabaseType.memory) {
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'Statistic is only available for indexed content'));
}
@ -43,7 +43,7 @@ export class AdminMWs {
public static async getDuplicates(req: Request, res: Response, next: NextFunction) {
if (Config.Server.database.type === DatabaseType.memory) {
if (Config.Server.Database.type === DatabaseType.memory) {
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'Statistic is only available for indexed content'));
}
@ -72,10 +72,10 @@ export class AdminMWs {
if (databaseSettings.type !== DatabaseType.memory) {
await SQLConnection.tryConnection(databaseSettings);
}
Config.Server.database = databaseSettings;
Config.Server.Database = databaseSettings;
// only updating explicitly set config (not saving config set by the diagnostics)
const original = Config.original();
original.Server.database = databaseSettings;
original.Server.Database = databaseSettings;
if (databaseSettings.type === DatabaseType.memory) {
original.Client.Sharing.enabled = false;
original.Client.Search.enabled = false;
@ -86,7 +86,7 @@ export class AdminMWs {
Logger.info(LOG_TAG, JSON.stringify(Config, null, '\t'));
await ObjectManagers.reset();
if (Config.Server.database.type !== DatabaseType.memory) {
if (Config.Server.Database.type !== DatabaseType.memory) {
await ObjectManagers.InitSQLManagers();
} else {
await ObjectManagers.InitMemoryManagers();
@ -318,11 +318,11 @@ export class AdminMWs {
await ConfigDiagnostics.testServerThumbnailConfig(settings.server);
await ConfigDiagnostics.testClientThumbnailConfig(settings.client);
Config.Server.thumbnail = settings.server;
Config.Server.Thumbnail = settings.server;
Config.Client.Thumbnail = settings.client;
// only updating explicitly set config (not saving config set by the diagnostics)
const original = Config.original();
original.Server.thumbnail = settings.server;
original.Server.Thumbnail = settings.server;
original.Client.Thumbnail = settings.client;
original.save();
ProjectPath.reset();
@ -398,8 +398,8 @@ export class AdminMWs {
original.Client.Other.enableOnScrollThumbnailPrioritising = settings.Client.enableOnScrollThumbnailPrioritising;
original.Client.Other.defaultPhotoSortingMethod = settings.Client.defaultPhotoSortingMethod;
original.Client.Other.NavBar.showItemCount = settings.Client.NavBar.showItemCount;
original.Server.threading.enable = settings.Server.enable;
original.Server.threading.thumbnailThreads = settings.Server.thumbnailThreads;
original.Server.Threading.enable = settings.Server.enable;
original.Server.Threading.thumbnailThreads = settings.Server.thumbnailThreads;
original.save();
await ConfigDiagnostics.runDiagnostics();
Logger.info(LOG_TAG, 'new config:');
@ -420,11 +420,11 @@ export class AdminMWs {
try {
const settings: IndexingConfig = req.body.settings;
Config.Server.indexing = settings;
Config.Server.Indexing = settings;
// only updating explicitly set config (not saving config set by the diagnostics)
const original = Config.original();
original.Server.indexing = settings;
original.Server.Indexing = settings;
original.save();
await ConfigDiagnostics.runDiagnostics();
Logger.info(LOG_TAG, 'new config:');
@ -451,8 +451,8 @@ export class AdminMWs {
const original = Config.original();
await ConfigDiagnostics.testTasksConfig(settings, original);
Config.Server.tasks = settings;
original.Server.tasks = settings;
Config.Server.Tasks = settings;
original.Server.Tasks = settings;
original.save();
await ConfigDiagnostics.runDiagnostics();

View File

@ -15,6 +15,7 @@ import {MediaDTO} from '../../common/entities/MediaDTO';
import {VideoDTO} from '../../common/entities/VideoDTO';
import {Utils} from '../../common/Utils';
import {QueryParams} from '../../common/QueryParams';
import {VideoConverterMWs} from './VideoConverterMWs';
const LOG_TAG = '[GalleryMWs]';
@ -165,7 +166,7 @@ export class GalleryMWs {
}
const fullMediaPath = path.join(ProjectPath.ImageFolder, req.params.mediaPath);
// check if thumbnail already exist
// check if file exist
if (fs.existsSync(fullMediaPath) === false) {
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'no such file:' + req.params.mediaPath, 'can\'t find file: ' + fullMediaPath));
}
@ -178,6 +179,32 @@ export class GalleryMWs {
return next();
}
public static loadBestFitVideo(req: Request, res: Response, next: NextFunction) {
if (!(req.params.mediaPath)) {
return next();
}
const fullMediaPath = path.join(ProjectPath.ImageFolder, req.params.mediaPath);
if (fs.statSync(fullMediaPath).isDirectory()) {
return next();
}
// check if video exist
if (fs.existsSync(fullMediaPath) === false) {
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'no such file:' + req.params.mediaPath, 'can\'t find file: ' + fullMediaPath));
}
req.resultPipe = fullMediaPath;
const convertedVideo = VideoConverterMWs.generateConvertedFileName(fullMediaPath);
// check if transcoded video exist
if (fs.existsSync(convertedVideo) === true) {
req.resultPipe = convertedVideo;
}
return next();
}
public static async search(req: Request, res: Response, next: NextFunction) {
if (Config.Client.Search.enabled === false) {

View File

@ -0,0 +1,68 @@
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import * as util from 'util';
import {ITaskExecuter, TaskExecuter} from '../model/threading/TaskExecuter';
import {VideoConverterInput, VideoConverterWorker} from '../model/threading/VideoConverterWorker';
import {MetadataLoader} from '../model/threading/MetadataLoader';
import {Config} from '../../common/config/private/Config';
import {ProjectPath} from '../ProjectPath';
const existPr = util.promisify(fs.exists);
export class VideoConverterMWs {
private static taskQue: ITaskExecuter<VideoConverterInput, void> =
new TaskExecuter(Math.max(1, os.cpus().length - 1), (input => VideoConverterWorker.convert(input)));
public static generateConvertedFileName(videoPath: string): string {
const extension = path.extname(videoPath);
const file = path.basename(videoPath, extension);
const postfix = Math.round(Config.Server.Video.transcoding.bitRate / 1024) + 'k' +
Config.Server.Video.transcoding.codec.toString().toLowerCase() +
Config.Server.Video.transcoding.resolution;
return path.join(ProjectPath.TranscendedFolder,
ProjectPath.getRelativePathToImages(path.dirname(videoPath)), file +
'_' + postfix + '.' + Config.Server.Video.transcoding.format);
}
public static async convertVideo(videoPath: string): Promise<void> {
const outPath = this.generateConvertedFileName(videoPath);
if (await existPr(outPath)) {
return;
}
const metaData = await MetadataLoader.loadVideoMetadata(videoPath);
const renderInput: VideoConverterInput = {
videoPath: videoPath,
output: {
path: outPath,
codec: Config.Server.Video.transcoding.codec,
format: Config.Server.Video.transcoding.format
}
};
if (metaData.bitRate > Config.Server.Video.transcoding.bitRate) {
renderInput.output.bitRate = Config.Server.Video.transcoding.bitRate;
}
if (metaData.fps > Config.Server.Video.transcoding.fps) {
renderInput.output.fps = Config.Server.Video.transcoding.fps;
}
if (Config.Server.Video.transcoding.resolution < metaData.size.height) {
renderInput.output.resolution = Config.Server.Video.transcoding.resolution;
}
const outDir = path.dirname(renderInput.output.path);
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, {recursive: true});
}
await VideoConverterMWs.taskQue.execute(renderInput);
}
}

View File

@ -27,9 +27,9 @@ export class ThumbnailGeneratorMWs {
}
if (Config.Server.threading.enable === true) {
if (Config.Server.threading.thumbnailThreads > 0) {
Config.Client.Thumbnail.concurrentThumbnailGenerations = Config.Server.threading.thumbnailThreads;
if (Config.Server.Threading.enable === true) {
if (Config.Server.Threading.thumbnailThreads > 0) {
Config.Client.Thumbnail.concurrentThumbnailGenerations = Config.Server.Threading.thumbnailThreads;
} else {
Config.Client.Thumbnail.concurrentThumbnailGenerations = Math.max(1, os.cpus().length - 1);
}
@ -37,12 +37,12 @@ export class ThumbnailGeneratorMWs {
Config.Client.Thumbnail.concurrentThumbnailGenerations = 1;
}
if (Config.Server.threading.enable === true &&
Config.Server.thumbnail.processingLibrary === ThumbnailProcessingLib.Jimp) {
if (Config.Server.Threading.enable === true &&
Config.Server.Thumbnail.processingLibrary === ThumbnailProcessingLib.Jimp) {
this.taskQue = new ThumbnailTH(Config.Client.Thumbnail.concurrentThumbnailGenerations);
} else {
this.taskQue = new TaskExecuter(Config.Client.Thumbnail.concurrentThumbnailGenerations,
(input => ThumbnailWorker.render(input, Config.Server.thumbnail.processingLibrary)));
(input => ThumbnailWorker.render(input, Config.Server.Thumbnail.processingLibrary)));
}
this.initDone = true;
@ -131,14 +131,10 @@ export class ThumbnailGeneratorMWs {
return next();
}
// create thumbnail folder if not exist
if (!fs.existsSync(ProjectPath.ThumbnailFolder)) {
fs.mkdirSync(ProjectPath.ThumbnailFolder);
}
const margin = {
x: Math.round(photo.metadata.faces[0].box.width * (Config.Server.thumbnail.personFaceMargin)),
y: Math.round(photo.metadata.faces[0].box.height * (Config.Server.thumbnail.personFaceMargin))
x: Math.round(photo.metadata.faces[0].box.width * (Config.Server.Thumbnail.personFaceMargin)),
y: Math.round(photo.metadata.faces[0].box.height * (Config.Server.Thumbnail.personFaceMargin))
};
@ -155,7 +151,7 @@ export class ThumbnailGeneratorMWs {
width: photo.metadata.faces[0].box.width + margin.x,
height: photo.metadata.faces[0].box.height + margin.y
},
qualityPriority: Config.Server.thumbnail.qualityPriority
qualityPriority: Config.Server.Thumbnail.qualityPriority
};
input.cut.width = Math.min(input.cut.width, photo.metadata.size.width - input.cut.left);
input.cut.height = Math.min(input.cut.height, photo.metadata.size.height - input.cut.top);
@ -260,10 +256,6 @@ export class ThumbnailGeneratorMWs {
return next();
}
// create thumbnail folder if not exist
if (!fs.existsSync(ProjectPath.ThumbnailFolder)) {
fs.mkdirSync(ProjectPath.ThumbnailFolder);
}
// run on other thread
const input = <RendererInput>{
@ -272,7 +264,7 @@ export class ThumbnailGeneratorMWs {
size: size,
thPath: thPath,
makeSquare: makeSquare,
qualityPriority: Config.Server.thumbnail.qualityPriority
qualityPriority: Config.Server.Thumbnail.qualityPriority
};
try {
await this.taskQue.execute(input);

View File

@ -11,21 +11,22 @@ export class DiskManager {
static threadPool: DiskManagerTH = null;
public static init() {
if (Config.Server.threading.enable === true) {
if (Config.Server.Threading.enable === true) {
DiskManager.threadPool = new DiskManagerTH(1);
}
}
public static async scanDirectory(relativeDirectoryName: string): Promise<DirectoryDTO> {
public static async scanDirectory(relativeDirectoryName: string,
settings: DiskMangerWorker.DirectoryScanSettings = {}): Promise<DirectoryDTO> {
Logger.silly(LOG_TAG, 'scanning directory:', relativeDirectoryName);
let directory: DirectoryDTO;
let directory: DirectoryDTO = null;
if (Config.Server.threading.enable === true) {
directory = await DiskManager.threadPool.execute(relativeDirectoryName);
if (Config.Server.Threading.enable === true) {
directory = await DiskManager.threadPool.execute(relativeDirectoryName, settings);
} else {
directory = await DiskMangerWorker.scanDirectory(relativeDirectoryName);
directory = await DiskMangerWorker.scanDirectory(relativeDirectoryName, settings);
}
const addDirs = (dir: DirectoryDTO) => {
dir.media.forEach((ph) => {

View File

@ -134,7 +134,7 @@ export class ConfigDiagnostics {
static async testFacesConfig(faces: ClientConfig.FacesConfig, config: IPrivateConfig) {
if (faces.enabled === true) {
if (config.Server.database.type === DatabaseType.memory) {
if (config.Server.Database.type === DatabaseType.memory) {
throw new Error('Memory Database do not support faces');
}
if (config.Client.Search.enabled === false) {
@ -145,7 +145,7 @@ export class ConfigDiagnostics {
static async testSearchConfig(search: ClientConfig.SearchConfig, config: IPrivateConfig) {
if (search.enabled === true &&
config.Server.database.type === DatabaseType.memory) {
config.Server.Database.type === DatabaseType.memory) {
throw new Error('Memory Database do not support searching');
}
}
@ -153,7 +153,7 @@ export class ConfigDiagnostics {
static async testSharingConfig(sharing: ClientConfig.SharingConfig, config: IPrivateConfig) {
if (sharing.enabled === true &&
config.Server.database.type === DatabaseType.memory) {
config.Server.Database.type === DatabaseType.memory) {
throw new Error('Memory Database do not support sharing');
}
if (sharing.enabled === true &&
@ -164,7 +164,7 @@ export class ConfigDiagnostics {
static async testRandomPhotoConfig(sharing: ClientConfig.RandomPhotoConfig, config: IPrivateConfig) {
if (sharing.enabled === true &&
config.Server.database.type === DatabaseType.memory) {
config.Server.Database.type === DatabaseType.memory) {
throw new Error('Memory Database do not support sharing');
}
}
@ -194,9 +194,9 @@ export class ConfigDiagnostics {
static async runDiagnostics() {
if (Config.Server.database.type !== DatabaseType.memory) {
if (Config.Server.Database.type !== DatabaseType.memory) {
try {
await ConfigDiagnostics.testDatabase(Config.Server.database);
await ConfigDiagnostics.testDatabase(Config.Server.Database);
} catch (ex) {
const err: Error = ex;
Logger.warn(LOG_TAG, '[SQL error]', err.toString());
@ -206,24 +206,24 @@ export class ConfigDiagnostics {
}
}
if (Config.Server.thumbnail.processingLibrary !== ThumbnailProcessingLib.Jimp) {
if (Config.Server.Thumbnail.processingLibrary !== ThumbnailProcessingLib.Jimp) {
try {
await ConfigDiagnostics.testThumbnailLib(Config.Server.thumbnail.processingLibrary);
await ConfigDiagnostics.testThumbnailLib(Config.Server.Thumbnail.processingLibrary);
} catch (ex) {
const err: Error = ex;
NotificationManager.warning('Thumbnail hardware acceleration is not possible.' +
' \'' + ThumbnailProcessingLib[Config.Server.thumbnail.processingLibrary] + '\' node module is not found.' +
' \'' + ThumbnailProcessingLib[Config.Server.Thumbnail.processingLibrary] + '\' node module is not found.' +
' Falling back temporally to JS based thumbnail generation', err.toString());
Logger.warn(LOG_TAG, '[Thumbnail hardware acceleration] module error: ', err.toString());
Logger.warn(LOG_TAG, 'Thumbnail hardware acceleration is not possible.' +
' \'' + ThumbnailProcessingLib[Config.Server.thumbnail.processingLibrary] + '\' node module is not found.' +
' \'' + ThumbnailProcessingLib[Config.Server.Thumbnail.processingLibrary] + '\' node module is not found.' +
' Falling back temporally to JS based thumbnail generation');
Config.Server.thumbnail.processingLibrary = ThumbnailProcessingLib.Jimp;
Config.Server.Thumbnail.processingLibrary = ThumbnailProcessingLib.Jimp;
}
}
try {
await ConfigDiagnostics.testThumbnailFolder(Config.Server.thumbnail.folder);
await ConfigDiagnostics.testThumbnailFolder(Config.Server.Thumbnail.folder);
} catch (ex) {
const err: Error = ex;
NotificationManager.error('Thumbnail folder error', err.toString());
@ -288,7 +288,7 @@ export class ConfigDiagnostics {
try {
await ConfigDiagnostics.testTasksConfig(Config.Server.tasks, Config);
await ConfigDiagnostics.testTasksConfig(Config.Server.Tasks, Config);
} catch (ex) {
const err: Error = ex;
NotificationManager.warning('Some Tasks are not supported with these settings. Disabling temporally. ' +

View File

@ -16,9 +16,9 @@ export class GalleryManager implements IGalleryManager {
if (knownLastModified && knownLastScanned) {
const stat = fs.statSync(path.join(ProjectPath.ImageFolder, relativeDirectoryName));
const lastModified = DiskMangerWorker.calcLastModified(stat);
if (Date.now() - knownLastScanned <= Config.Server.indexing.cachedFolderTimeout &&
if (Date.now() - knownLastScanned <= Config.Server.Indexing.cachedFolderTimeout &&
lastModified === knownLastModified &&
Config.Server.indexing.reIndexingSensitivity < ReIndexingSensitivity.high) {
Config.Server.Indexing.reIndexingSensitivity < ReIndexingSensitivity.high) {
return Promise.resolve(null);
}
}

View File

@ -43,11 +43,11 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
if (knownLastModified && knownLastScanned
&& lastModified === knownLastModified &&
dir.lastScanned === knownLastScanned) {
if (Config.Server.indexing.reIndexingSensitivity === ReIndexingSensitivity.low) {
if (Config.Server.Indexing.reIndexingSensitivity === ReIndexingSensitivity.low) {
return null;
}
if (Date.now() - dir.lastScanned <= Config.Server.indexing.cachedFolderTimeout &&
Config.Server.indexing.reIndexingSensitivity === ReIndexingSensitivity.medium) {
if (Date.now() - dir.lastScanned <= Config.Server.Indexing.cachedFolderTimeout &&
Config.Server.Indexing.reIndexingSensitivity === ReIndexingSensitivity.medium) {
return null;
}
}
@ -61,13 +61,13 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
// not indexed since a while, index it in a lazy manner
if ((Date.now() - dir.lastScanned > Config.Server.indexing.cachedFolderTimeout &&
Config.Server.indexing.reIndexingSensitivity >= ReIndexingSensitivity.medium) ||
Config.Server.indexing.reIndexingSensitivity >= ReIndexingSensitivity.high) {
if ((Date.now() - dir.lastScanned > Config.Server.Indexing.cachedFolderTimeout &&
Config.Server.Indexing.reIndexingSensitivity >= ReIndexingSensitivity.medium) ||
Config.Server.Indexing.reIndexingSensitivity >= ReIndexingSensitivity.high) {
// on the fly reindexing
Logger.silly(LOG_TAG, 'lazy reindexing reason: cache timeout: lastScanned: '
+ (Date.now() - dir.lastScanned) + ' ms ago, cachedFolderTimeout:' + Config.Server.indexing.cachedFolderTimeout);
+ (Date.now() - dir.lastScanned) + ' ms ago, cachedFolderTimeout:' + Config.Server.Indexing.cachedFolderTimeout);
ObjectManagers.getInstance().IndexingManager.indexDirectory(relativeDirectoryName).catch((err) => {
console.error(err);
});
@ -133,7 +133,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
query.andWhere('photo.metadata.size.width <= photo.metadata.size.height');
}
if (Config.Server.database.type === DatabaseType.mysql) {
if (Config.Server.Database.type === DatabaseType.mysql) {
return await query.groupBy('RAND(), photo.id').limit(1).getOne();
}
return await query.groupBy('RANDOM()').limit(1).getOne();
@ -183,7 +183,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
'media.name=innerMedia.name AND media.metadata.fileSize = innerMedia.fileSize')
.innerJoinAndSelect('media.directory', 'directory')
.orderBy('media.name, media.metadata.fileSize')
.limit(Config.Server.duplicates.listingLimit).getMany();
.limit(Config.Server.Duplicates.listingLimit).getMany();
const duplicateParis: DuplicatesDTO[] = [];
@ -237,7 +237,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
'media.metadata.creationDate=innerMedia.creationDate AND media.metadata.fileSize = innerMedia.fileSize')
.innerJoinAndSelect('media.directory', 'directory')
.orderBy('media.metadata.creationDate, media.metadata.fileSize')
.limit(Config.Server.duplicates.listingLimit).getMany();
.limit(Config.Server.Duplicates.listingLimit).getMany();
processDuplicates(duplicates,
(a, b) => a.metadata.creationDate === b.metadata.creationDate &&
@ -297,7 +297,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
dir: dir.directories[i].id
})
.orderBy('media.metadata.creationDate', 'ASC')
.limit(Config.Server.indexing.folderPreviewSize)
.limit(Config.Server.Indexing.folderPreviewSize)
.getMany();
dir.directories[i].isPartial = true;

View File

@ -30,7 +30,7 @@ export class SQLConnection {
public static async getConnection(): Promise<Connection> {
if (this.connection == null) {
const options: any = this.getDriver(Config.Server.database);
const options: any = this.getDriver(Config.Server.Database);
// options.name = 'main';
options.entities = [
UserEntity,
@ -45,8 +45,8 @@ export class SQLConnection {
VersionEntity
];
options.synchronize = false;
if (Config.Server.log.sqlLevel !== SQLLogLevel.none) {
options.logging = SQLLogLevel[Config.Server.log.sqlLevel];
if (Config.Server.Log.sqlLevel !== SQLLogLevel.none) {
options.logging = SQLLogLevel[Config.Server.Log.sqlLevel];
}
this.connection = await this.createConnection(options);
@ -75,8 +75,8 @@ export class SQLConnection {
VersionEntity
];
options.synchronize = false;
if (Config.Server.log.sqlLevel !== SQLLogLevel.none) {
options.logging = SQLLogLevel[Config.Server.log.sqlLevel];
if (Config.Server.Log.sqlLevel !== SQLLogLevel.none) {
options.logging = SQLLogLevel[Config.Server.Log.sqlLevel];
}
const conn = await this.createConnection(options);
await SQLConnection.schemeSync(conn);

View File

@ -42,7 +42,7 @@ export class SharingManager implements ISharingManager {
path: inSharing.path
});
if (sharing.timeStamp < Date.now() - Config.Server.sharing.updateTimeout) {
if (sharing.timeStamp < Date.now() - Config.Server.Sharing.updateTimeout) {
throw new Error('Sharing is locked, can\'t update anymore');
}
if (inSharing.password == null) {

View File

@ -5,11 +5,11 @@ import {ColumnOptions} from 'typeorm/decorator/options/ColumnOptions';
export class ColumnCharsetCS implements ColumnOptions {
public get charset(): string {
return Config.Server.database.type === DatabaseType.mysql ? 'utf8' : null;
return Config.Server.Database.type === DatabaseType.mysql ? 'utf8' : null;
}
public get collation(): string {
return Config.Server.database.type === DatabaseType.mysql ? 'utf8_bin' : null;
return Config.Server.Database.type === DatabaseType.mysql ? 'utf8_bin' : null;
}
}

View File

@ -16,6 +16,9 @@ export class VideoMetadataEntity extends MediaMetadataEntity implements VideoMet
})
duration: number;
@Column('int')
fps: number;
}

View File

@ -1,6 +1,6 @@
import {ITaskManager} from '../interfaces/ITaskManager';
import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO';
import {ITask} from './ITask';
import {ITask} from './tasks/ITask';
import {TaskRepository} from './TaskRepository';
import {Config} from '../../../common/config/private/Config';
import {TaskScheduleDTO, TaskTriggerType} from '../../../common/entities/task/TaskScheduleDTO';
@ -20,7 +20,10 @@ export class TaskManager implements ITaskManager {
const m: { [id: string]: TaskProgressDTO } = {};
TaskRepository.Instance.getAvailableTasks()
.filter(t => t.Progress)
.forEach(t => m[t.Name] = t.Progress);
.forEach(t => {
t.Progress.time.current = Date.now();
m[t.Name] = t.Progress;
});
return m;
}
@ -57,7 +60,7 @@ export class TaskManager implements ITaskManager {
public runSchedules(): void {
this.stopSchedules();
Logger.info(LOG_TAG, 'Running task schedules');
Config.Server.tasks.scheduled.forEach(s => this.runSchedule(s));
Config.Server.Tasks.scheduled.forEach(s => this.runSchedule(s));
}
protected getNextDayOfTheWeek(refDate: Date, dayOfWeek: number) {

View File

@ -1,6 +1,7 @@
import {ITask} from './ITask';
import {IndexingTask} from './IndexingTask';
import {DBRestTask} from './DBResetTask';
import {ITask} from './tasks/ITask';
import {IndexingTask} from './tasks/IndexingTask';
import {DBRestTask} from './tasks/DBResetTask';
import {VideoConvertingTask} from './tasks/VideoConvertingTask';
export class TaskRepository {
@ -26,3 +27,4 @@ export class TaskRepository {
TaskRepository.Instance.register(new IndexingTask());
TaskRepository.Instance.register(new DBRestTask());
TaskRepository.Instance.register(new VideoConvertingTask());

View File

@ -1,8 +1,8 @@
import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO';
import {ObjectManagers} from '../ObjectManagers';
import {Config} from '../../../common/config/private/Config';
import {DatabaseType} from '../../../common/config/private/IPrivateConfig';
import {ConfigTemplateEntry, DefaultsTasks} from '../../../common/entities/task/TaskDTO';
import {TaskProgressDTO} from '../../../../common/entities/settings/TaskProgressDTO';
import {ObjectManagers} from '../../ObjectManagers';
import {Config} from '../../../../common/config/private/Config';
import {DatabaseType} from '../../../../common/config/private/IPrivateConfig';
import {ConfigTemplateEntry, DefaultsTasks} from '../../../../common/entities/task/TaskDTO';
import {Task} from './Task';
const LOG_TAG = '[DBRestTask]';
@ -12,7 +12,7 @@ export class DBRestTask extends Task {
public readonly ConfigTemplate: ConfigTemplateEntry[] = null;
public get Supported(): boolean {
return Config.Server.database.type !== DatabaseType.memory;
return Config.Server.Database.type !== DatabaseType.memory;
}
protected async init() {

View File

@ -1,5 +1,5 @@
import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO';
import {TaskDTO} from '../../../common/entities/task/TaskDTO';
import {TaskProgressDTO} from '../../../../common/entities/settings/TaskProgressDTO';
import {TaskDTO} from '../../../../common/entities/task/TaskDTO';
export interface ITask<T> extends TaskDTO {
Name: string;

View File

@ -1,16 +1,16 @@
import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO';
import {ObjectManagers} from '../ObjectManagers';
import {TaskProgressDTO} from '../../../../common/entities/settings/TaskProgressDTO';
import {ObjectManagers} from '../../ObjectManagers';
import * as path from 'path';
import * as fs from 'fs';
import {Logger} from '../../Logger';
import {RendererInput, ThumbnailSourceType, ThumbnailWorker} from '../threading/ThumbnailWorker';
import {Config} from '../../../common/config/private/Config';
import {MediaDTO} from '../../../common/entities/MediaDTO';
import {ProjectPath} from '../../ProjectPath';
import {ThumbnailGeneratorMWs} from '../../middlewares/thumbnail/ThumbnailGeneratorMWs';
import {Logger} from '../../../Logger';
import {RendererInput, ThumbnailSourceType, ThumbnailWorker} from '../../threading/ThumbnailWorker';
import {Config} from '../../../../common/config/private/Config';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
import {ProjectPath} from '../../../ProjectPath';
import {ThumbnailGeneratorMWs} from '../../../middlewares/thumbnail/ThumbnailGeneratorMWs';
import {Task} from './Task';
import {DatabaseType} from '../../../common/config/private/IPrivateConfig';
import {ConfigTemplateEntry, DefaultsTasks} from '../../../common/entities/task/TaskDTO';
import {DatabaseType} from '../../../../common/config/private/IPrivateConfig';
import {ConfigTemplateEntry, DefaultsTasks} from '../../../../common/entities/task/TaskDTO';
declare const global: any;
const LOG_TAG = '[IndexingTask]';
@ -26,7 +26,7 @@ export class IndexingTask extends Task<{ createThumbnails: boolean }> {
}];
public get Supported(): boolean {
return Config.Server.database.type !== DatabaseType.memory;
return Config.Server.Database.type !== DatabaseType.memory;
}
protected async init() {
@ -68,8 +68,8 @@ export class IndexingTask extends Task<{ createThumbnails: boolean }> {
size: Config.Client.Thumbnail.thumbnailSizes[0],
thPath: thPath,
makeSquare: false,
qualityPriority: Config.Server.thumbnail.qualityPriority
}, Config.Server.thumbnail.processingLibrary);
qualityPriority: Config.Server.Thumbnail.qualityPriority
}, Config.Server.Thumbnail.processingLibrary);
} catch (e) {
console.error(e);
Logger.error(LOG_TAG, 'Error during indexing job: ' + e.toString());

View File

@ -1,7 +1,7 @@
import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO';
import {Logger} from '../../Logger';
import {TaskProgressDTO} from '../../../../common/entities/settings/TaskProgressDTO';
import {Logger} from '../../../Logger';
import {ITask} from './ITask';
import {ConfigTemplateEntry} from '../../../common/entities/task/TaskDTO';
import {ConfigTemplateEntry} from '../../../../common/entities/task/TaskDTO';
declare const process: any;

View File

@ -0,0 +1,64 @@
import {TaskProgressDTO} from '../../../../common/entities/settings/TaskProgressDTO';
import {Config} from '../../../../common/config/private/Config';
import {ConfigTemplateEntry, DefaultsTasks} from '../../../../common/entities/task/TaskDTO';
import {Task} from './Task';
import {ProjectPath} from '../../../ProjectPath';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
import {Logger} from '../../../Logger';
import * as path from 'path';
import {DiskManager} from '../../DiskManger';
import {VideoConverterMWs} from '../../../middlewares/VideoConverterMWs';
import {VideoDTO} from '../../../../common/entities/VideoDTO';
const LOG_TAG = '[VideoConvertingTask]';
export class VideoConvertingTask extends Task {
public readonly Name = DefaultsTasks[DefaultsTasks['Video Converting']];
public readonly ConfigTemplate: ConfigTemplateEntry[] = null;
queue: (string | VideoDTO)[] = [];
public get Supported(): boolean {
return Config.Client.Video.enabled === true;
}
protected async init() {
this.queue.push('/');
}
protected async step(): Promise<TaskProgressDTO> {
if (this.queue.length === 0) {
if (global.gc) {
global.gc();
}
return null;
}
if (this.running === false) {
return null;
}
const entry = this.queue.shift();
this.progress.left = this.queue.length;
this.progress.progress++;
this.progress.time.current = Date.now();
if (typeof entry === 'string') {
const directory = entry;
this.progress.comment = 'scanning directory: ' + entry;
const scanned = await DiskManager.scanDirectory(directory, {noPhoto: true, noMetaFile: true});
for (let i = 0; i < scanned.directories.length; i++) {
this.queue.push(path.join(scanned.directories[i].path, scanned.directories[i].name));
}
this.queue = this.queue.concat(<VideoDTO[]>scanned.media.filter(m => MediaDTO.isVideo(m)));
} else {
const video: VideoDTO = entry;
const videoPath = path.join(ProjectPath.ImageFolder, video.directory.path, video.directory.name, video.name);
this.progress.comment = 'transcoding: ' + videoPath;
try {
await VideoConverterMWs.convertVideo(videoPath);
} catch (e) {
console.error(e);
Logger.error(LOG_TAG, 'Error during transcoding a video: ' + e.toString());
}
}
return this.progress;
}
}

View File

@ -12,6 +12,7 @@ import {Logger} from '../../Logger';
const LOG_TAG = '[DiskManagerTask]';
export class DiskMangerWorker {
private static readonly SupportedEXT = {
@ -61,8 +62,8 @@ export class DiskMangerWorker {
const absoluteName = path.normalize(path.join(absoluteDirectoryName, name));
const relativeName = path.normalize(path.join(relativeDirectoryName, name));
for (let j = 0; j < Config.Server.indexing.excludeFolderList.length; j++) {
const exclude = Config.Server.indexing.excludeFolderList[j];
for (let j = 0; j < Config.Server.Indexing.excludeFolderList.length; j++) {
const exclude = Config.Server.Indexing.excludeFolderList[j];
if (exclude.startsWith('/')) {
if (exclude === absoluteName) {
@ -79,8 +80,8 @@ export class DiskMangerWorker {
}
}
// exclude dirs that have the given files (like .ignore)
for (let j = 0; j < Config.Server.indexing.excludeFileList.length; j++) {
const exclude = Config.Server.indexing.excludeFileList[j];
for (let j = 0; j < Config.Server.Indexing.excludeFileList.length; j++) {
const exclude = Config.Server.Indexing.excludeFileList[j];
if (fs.existsSync(path.join(absoluteName, exclude))) {
return true;
@ -90,7 +91,7 @@ export class DiskMangerWorker {
return false;
}
public static scanDirectory(relativeDirectoryName: string, maxPhotos: number = null, photosOnly: boolean = false): Promise<DirectoryDTO> {
public static scanDirectory(relativeDirectoryName: string, settings: DiskMangerWorker.DirectoryScanSettings = {}): Promise<DirectoryDTO> {
return new Promise<DirectoryDTO>((resolve, reject) => {
relativeDirectoryName = this.normalizeDirPath(relativeDirectoryName);
const directoryName = DiskMangerWorker.dirName(relativeDirectoryName);
@ -120,29 +121,36 @@ export class DiskMangerWorker {
const file = list[i];
const fullFilePath = path.normalize(path.join(absoluteDirectoryName, file));
if (fs.statSync(fullFilePath).isDirectory()) {
if (photosOnly === true) {
if (settings.noDirectory === true) {
continue;
}
if (DiskMangerWorker.excludeDir(file, relativeDirectoryName, absoluteDirectoryName)) {
continue;
}
// create preview directory
const d = await DiskMangerWorker.scanDirectory(path.join(relativeDirectoryName, file),
Config.Server.indexing.folderPreviewSize, true
{
maxPhotos: Config.Server.Indexing.folderPreviewSize,
noMetaFile: true,
noVideo: true,
noDirectory: false
}
);
d.lastScanned = 0; // it was not a fully scan
d.isPartial = true;
directory.directories.push(d);
} else if (DiskMangerWorker.isImage(fullFilePath)) {
} else if (!settings.noPhoto && DiskMangerWorker.isImage(fullFilePath)) {
directory.media.push(<PhotoDTO>{
name: file,
directory: null,
metadata: await MetadataLoader.loadPhotoMetadata(fullFilePath)
});
if (maxPhotos != null && directory.media.length > maxPhotos) {
if (settings.maxPhotos && directory.media.length > settings.maxPhotos) {
break;
}
} else if (photosOnly === false && Config.Client.Video.enabled === true &&
} else if (!settings.noVideo && Config.Client.Video.enabled === true &&
DiskMangerWorker.isVideo(fullFilePath)) {
try {
directory.media.push(<VideoDTO>{
@ -154,7 +162,7 @@ export class DiskMangerWorker {
Logger.warn('Media loading error, skipping: ' + file + ', reason: ' + e.toString());
}
} else if (photosOnly === false && Config.Client.MetaFile.enabled === true &&
} else if (!settings.noMetaFile && Config.Client.MetaFile.enabled === true &&
DiskMangerWorker.isMetaFile(fullFilePath)) {
directory.metaFile.push(<FileDTO>{
name: file,
@ -192,3 +200,13 @@ export class DiskMangerWorker {
}
}
export namespace DiskMangerWorker {
export interface DirectoryScanSettings {
maxPhotos?: number;
noMetaFile?: boolean;
noVideo?: boolean;
noPhoto?: boolean;
noDirectory?: boolean;
}
}

View File

@ -28,7 +28,8 @@ export class MetadataLoader {
bitRate: 0,
duration: 0,
creationDate: 0,
fileSize: 0
fileSize: 0,
fps: 0
};
try {
const stat = fs.statSync(fullPath);
@ -56,6 +57,9 @@ export class MetadataLoader {
if (Utils.isInt32(parseInt(data.streams[i].bit_rate, 10))) {
metadata.bitRate = parseInt(data.streams[i].bit_rate, 10) || null;
}
if (Utils.isInt32(parseInt(data.streams[i].avg_frame_rate, 10))) {
metadata.fps = parseInt(data.streams[i].avg_frame_rate, 10) || null;
}
metadata.creationDate = Date.parse(data.streams[i].tags.creation_time) || metadata.creationDate;
break;
}

View File

@ -6,6 +6,7 @@ import {RendererInput} from './ThumbnailWorker';
import {Config} from '../../../common/config/private/Config';
import {TaskQue, TaskQueEntry} from './TaskQue';
import {ITaskExecuter} from './TaskExecuter';
import {DiskMangerWorker} from './DiskMangerWorker';
interface WorkerWrapper<O> {
@ -26,6 +27,12 @@ export class ThreadPool<O> {
}
}
protected executeTask(task: WorkerTask): Promise<O> {
const promise = this.taskQue.add(task).promise.obj;
this.run();
return promise;
}
private run = () => {
if (this.taskQue.isEmpty()) {
return;
@ -40,12 +47,6 @@ export class ThreadPool<O> {
worker.worker.send(poolTask.data);
};
protected executeTask(task: WorkerTask): Promise<O> {
const promise = this.taskQue.add(task).promise.obj;
this.run();
return promise;
}
private getFreeWorker() {
for (let i = 0; i < this.workers.length; i++) {
if (this.workers[i].poolTask == null) {
@ -88,10 +89,11 @@ export class ThreadPool<O> {
}
export class DiskManagerTH extends ThreadPool<DirectoryDTO> implements ITaskExecuter<string, DirectoryDTO> {
execute(relativeDirectoryName: string): Promise<DirectoryDTO> {
execute(relativeDirectoryName: string, settings: DiskMangerWorker.DirectoryScanSettings = {}): Promise<DirectoryDTO> {
return super.executeTask(<DiskManagerTask>{
type: WorkerTaskTypes.diskManager,
relativeDirectoryName: relativeDirectoryName
relativeDirectoryName: relativeDirectoryName,
settings: settings
});
}
}
@ -101,7 +103,7 @@ export class ThumbnailTH extends ThreadPool<void> implements ITaskExecuter<Rende
return super.executeTask(<ThumbnailTask>{
type: WorkerTaskTypes.thumbnail,
input: input,
renderer: Config.Server.thumbnail.processingLibrary
renderer: Config.Server.Thumbnail.processingLibrary
});
}
}

View File

@ -0,0 +1,69 @@
import {Logger} from '../../Logger';
import {FfmpegCommand} from 'fluent-ffmpeg';
import {FFmpegFactory} from '../FFmpegFactory';
export interface VideoConverterInput {
videoPath: string;
output: {
path: string,
bitRate?: number,
resolution?: 240 | 360 | 480 | 720 | 1080 | 1440 | 2160 | 4320,
fps?: number,
codec: string,
format: string
};
}
export class VideoConverterWorker {
private static ffmpeg = FFmpegFactory.get();
public static convert(input: VideoConverterInput): Promise<void> {
if (this.ffmpeg == null) {
this.ffmpeg = FFmpegFactory.get();
}
return new Promise((resolve, reject) => {
Logger.silly('[FFmpeg] transcoding video: ' + input.videoPath);
const command: FfmpegCommand = this.ffmpeg(input.videoPath);
let executedCmd = '';
command
.on('start', (cmd: string) => {
Logger.silly('[FFmpeg] running:' + cmd);
executedCmd = cmd;
})
.on('end', () => {
resolve();
})
.on('error', (e: any) => {
reject('[FFmpeg] ' + e.toString() + ' executed: ' + executedCmd);
});
// set video bitrate
if (input.output.bitRate) {
command.videoBitrate((input.output.bitRate / 1024) + 'k');
}
// set target codec
command.videoCodec(input.output.codec);
if (input.output.resolution) {
command.size('?x' + input.output.resolution);
}
// set fps
if (input.output.fps) {
command.fps(input.output.fps);
}
// set output format to force
command.format(input.output.format)
// save to file
.save(input.output.path);
});
}
}

View File

@ -15,7 +15,7 @@ export class Worker {
let result = null;
switch (task.type) {
case WorkerTaskTypes.diskManager:
result = await DiskMangerWorker.scanDirectory((<DiskManagerTask>task).relativeDirectoryName);
result = await DiskMangerWorker.scanDirectory((<DiskManagerTask>task).relativeDirectoryName, (<DiskManagerTask>task).settings);
if (global.gc) {
global.gc();
}
@ -48,6 +48,7 @@ export interface WorkerTask {
export interface DiskManagerTask extends WorkerTask {
relativeDirectoryName: string;
settings: DiskMangerWorker.DirectoryScanSettings;
}
export interface ThumbnailTask extends WorkerTask {

View File

@ -16,6 +16,7 @@ export class GalleryRouter {
this.addGetVideoThumbnail(app);
this.addGetImage(app);
this.addGetVideo(app);
this.addGetBestFitVideo(app);
this.addGetMetaFile(app);
this.addRandom(app);
this.addDirectoryList(app);
@ -58,6 +59,15 @@ export class GalleryRouter {
RenderingMWs.renderFile
);
}
private static addGetBestFitVideo(app: Express) {
app.get(['/api/gallery/content/:mediaPath(*\.(mp4|ogg|ogv|webm))/bestFit'],
AuthenticationMWs.authenticate,
AuthenticationMWs.normalizePathParam('mediaPath'),
AuthenticationMWs.authorisePath('mediaPath', false),
GalleryMWs.loadBestFitVideo,
RenderingMWs.renderFile
);
}
private static addGetMetaFile(app: Express) {
app.get(['/api/gallery/content/:mediaPath(*\.(gpx))'],

View File

@ -82,7 +82,7 @@ export class Server {
Localizations.init();
this.app.use(locale(Config.Client.languages, 'en'));
if (Config.Server.database.type !== DatabaseType.memory) {
if (Config.Server.Database.type !== DatabaseType.memory) {
await ObjectManagers.InitSQLManagers();
} else {
await ObjectManagers.InitMemoryManagers();

View File

@ -45,7 +45,7 @@ export class Benchmarks {
async bmListDirectory(): Promise<BenchmarkResult> {
const gm = new GalleryManager();
await this.setupDB();
Config.Server.indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
Config.Server.Indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
return await this.benchmark(() => gm.listDirectory('./'));
}
@ -122,8 +122,8 @@ export class Benchmarks {
if (fs.existsSync(this.dbPath)) {
fs.unlinkSync(this.dbPath);
}
Config.Server.database.type = DatabaseType.sqlite;
Config.Server.database.sqlite.storage = this.dbPath;
Config.Server.Database.type = DatabaseType.sqlite;
Config.Server.Database.sqlite.storage = this.dbPath;
await ObjectManagers.InitSQLManagers();
};

View File

@ -1 +1 @@
export const DataStructureVersion = 13;
export const DataStructureVersion = 14;

View File

@ -6,7 +6,7 @@ export enum DatabaseType {
}
export enum LogLevel {
error = 1, warn = 2, info = 3, verbose = 4, debug = 5, silly = 6
error = 1, warn = 2, info = 3, verbose = 4, debug = 5, silly = 6
}
export enum SQLLogLevel {
@ -55,8 +55,8 @@ export interface IndexingConfig {
folderPreviewSize: number;
cachedFolderTimeout: number; // Do not rescans the folder if seems ok
reIndexingSensitivity: ReIndexingSensitivity;
excludeFolderList: string[]
excludeFileList: string[]
excludeFolderList: string[];
excludeFileList: string[];
}
export interface ThreadingConfig {
@ -77,20 +77,31 @@ export interface TaskConfig {
scheduled: TaskScheduleDTO[];
}
export interface VideoConfig {
transcoding: {
bitRate: number,
resolution: 240 | 360 | 480 | 720 | 1080 | 1440 | 2160 | 4320,
fps: number,
codec: 'libvpx-vp9' | 'libx264' | 'libvpx',
format: 'mp4' | 'webm'
};
}
export interface ServerConfig {
port: number;
host: string;
imagesFolder: string;
thumbnail: ThumbnailConfig;
threading: ThreadingConfig;
database: DataBaseConfig;
sharing: SharingConfig;
Thumbnail: ThumbnailConfig;
Threading: ThreadingConfig;
Database: DataBaseConfig;
Sharing: SharingConfig;
sessionTimeout: number;
indexing: IndexingConfig;
Indexing: IndexingConfig;
photoMetadataSize: number;
duplicates: DuplicatesConfig;
log: LogConfig;
tasks: TaskConfig;
Duplicates: DuplicatesConfig;
Log: LogConfig;
Tasks: TaskConfig;
Video: VideoConfig;
}
export interface IPrivateConfig {

View File

@ -12,8 +12,6 @@ import * as path from 'path';
import {ConfigLoader} from 'typeconfig';
import {Utils} from '../../Utils';
import {UserRoles} from '../../entities/UserDTO';
import {TaskScheduleDTO} from '../../entities/task/TaskScheduleDTO';
import {Config} from './Config';
/**
* This configuration will be only at backend
@ -24,19 +22,19 @@ export class PrivateConfigClass extends PublicConfigClass implements IPrivateCon
port: 80,
host: '0.0.0.0',
imagesFolder: 'demo/images',
thumbnail: {
Thumbnail: {
folder: 'demo/TEMP',
processingLibrary: ThumbnailProcessingLib.sharp,
qualityPriority: true,
personFaceMargin: 0.6
},
log: {
Log: {
level: LogLevel.info,
sqlLevel: SQLLogLevel.error
},
sessionTimeout: 1000 * 60 * 60 * 24 * 7,
photoMetadataSize: 512 * 1024,
database: {
Database: {
type: DatabaseType.sqlite,
mysql: {
host: '',
@ -49,31 +47,40 @@ export class PrivateConfigClass extends PublicConfigClass implements IPrivateCon
storage: 'sqlite.db'
}
},
sharing: {
Sharing: {
updateTimeout: 1000 * 60 * 5
},
threading: {
Threading: {
enable: true,
thumbnailThreads: 0
},
indexing: {
Indexing: {
folderPreviewSize: 2,
cachedFolderTimeout: 1000 * 60 * 60,
reIndexingSensitivity: ReIndexingSensitivity.low,
excludeFolderList: [],
excludeFileList: []
},
duplicates: {
Duplicates: {
listingLimit: 1000
},
tasks: {
Tasks: {
scheduled: []
},
Video: {
transcoding: {
bitRate: 5 * 1024 * 1024,
codec: 'libx264',
format: 'mp4',
fps: 25,
resolution: 720
}
}
};
private ConfigLoader: any;
public setDatabaseType(type: DatabaseType) {
this.Server.database.type = type;
this.Server.Database.type = type;
if (type === DatabaseType.memory) {
this.Client.Search.enabled = false;
this.Client.Sharing.enabled = false;
@ -94,11 +101,11 @@ export class PrivateConfigClass extends PublicConfigClass implements IPrivateCon
if (Utils.enumToArray(UserRoles).map(r => r.key).indexOf(this.Client.unAuthenticatedUserRole) === -1) {
throw new Error('Unknown user role for Client.unAuthenticatedUserRole, found: ' + this.Client.unAuthenticatedUserRole);
}
if (Utils.enumToArray(LogLevel).map(r => r.key).indexOf(this.Server.log.level) === -1) {
throw new Error('Unknown Server.log.level, found: ' + this.Server.log.level);
if (Utils.enumToArray(LogLevel).map(r => r.key).indexOf(this.Server.Log.level) === -1) {
throw new Error('Unknown Server.log.level, found: ' + this.Server.Log.level);
}
if (Utils.enumToArray(SQLLogLevel).map(r => r.key).indexOf(this.Server.log.sqlLevel) === -1) {
throw new Error('Unknown Server.log.level, found: ' + this.Server.log.sqlLevel);
if (Utils.enumToArray(SQLLogLevel).map(r => r.key).indexOf(this.Server.Log.sqlLevel) === -1) {
throw new Error('Unknown Server.log.level, found: ' + this.Server.Log.sqlLevel);
}
}

View File

@ -15,4 +15,5 @@ export interface VideoMetadata extends MediaMetadata {
bitRate: number;
duration: number; // in milliseconds
fileSize: number;
fps: number;
}

View File

@ -2,7 +2,7 @@ export type fieldType = 'string' | 'number' | 'boolean';
export enum DefaultsTasks {
Indexing = 1, 'Database Reset' = 2
Indexing = 1, 'Database Reset' = 2, 'Video Converting' = 3
}
export interface ConfigTemplateEntry {

View File

@ -33,12 +33,19 @@ export class MediaIcon {
this.media.directory.path, this.media.directory.name, this.media.name, 'icon');
}
getPhotoPath() {
getMediaPath() {
return Utils.concatUrls(Config.Client.urlBase,
'/api/gallery/content/',
this.media.directory.path, this.media.directory.name, this.media.name);
}
getBestFitMediaPath() {
return Utils.concatUrls(Config.Client.urlBase,
'/api/gallery/content/',
this.media.directory.path, this.media.directory.name, this.media.name,
'/bestFit');
}
equals(other: MediaDTO | MediaIcon): boolean {
// is gridphoto

View File

@ -92,6 +92,12 @@
font-size: 1.5rem;
}
.controls-zoom {
bottom: 0;
z-index: 3;
position: absolute;
}
.controls-playback {
padding-right: 15px;
bottom: 0;
@ -103,6 +109,7 @@
padding-right: 15px;
bottom: 0;
position: absolute;
z-index: 3;
}
.controls-video .oi,

View File

@ -5,7 +5,7 @@
<div class="controls controls-top">
<a class="highlight control-button"
*ngIf="activePhoto"
[href]="activePhoto.gridMedia.getPhotoPath()"
[href]="activePhoto.gridMedia.getMediaPath()"
[download]="activePhoto.gridMedia.media.name">
<span class="oi oi-data-transfer-download"
title="download" i18n-title></span>
@ -55,7 +55,7 @@
[style.left.px]="photoFrameDim.width/2"
[style.width.px]="faceContainerDim.width"
[style.height.px]="faceContainerDim.height"
*ngIf="facesEnabled && activePhoto && zoom == 1">
*ngIf="facesEnabled && activePhoto && zoom == 1 && activePhoto.gridMedia.Photo.metadata.faces && activePhoto.gridMedia.Photo.metadata.faces.length > 0">
<a
class="face"
[routerLink]="['/search', face.name, {type: SearchTypes[SearchTypes.person]}]"

View File

@ -50,6 +50,9 @@
<div class="col-6" *ngIf="VideoData.duration">
<ng-container i18n>duration</ng-container>
: {{VideoData.duration | duration}}</div>
<div class="col-6" *ngIf="VideoData.fps">
fps: {{VideoData.fps}}/s
</div>
<div class="col-6" *ngIf="VideoData.bitRate">
<ng-container i18n>bit rate</ng-container>
: {{VideoData.bitRate | fileSize}}/s

View File

@ -22,7 +22,7 @@
(error)="onImageError()"
(timeupdate)="onVideoProgress()"
#video>
<source [src]="gridMedia.getPhotoPath()" type="{{getVideoType()}}">
<source [src]="gridMedia.getBestFitMediaPath()" type="{{getVideoType()}}">
</video>
</div>

View File

@ -55,7 +55,7 @@ export class GalleryLightboxMediaComponent implements OnChanges {
}
if (this.photoSrc == null && this.gridMedia && this.loadMedia) {
FixOrientationPipe.transform(this.gridMedia.getPhotoPath(), this.gridMedia.Orientation)
FixOrientationPipe.transform(this.gridMedia.getMediaPath(), this.gridMedia.Orientation)
.then((src) => this.photoSrc = src);
}
}
@ -144,7 +144,7 @@ export class GalleryLightboxMediaComponent implements OnChanges {
onImageError() {
// TODO:handle error
this.imageLoadFinished = true;
console.error('Error: cannot load media for lightbox url: ' + this.gridMedia.getPhotoPath());
console.error('Error: cannot load media for lightbox url: ' + this.gridMedia.getMediaPath());
}
@ -165,7 +165,7 @@ export class GalleryLightboxMediaComponent implements OnChanges {
}
public get PhotoSrc(): string {
return this.gridMedia.getPhotoPath();
return this.gridMedia.getMediaPath();
}
public showThumbnail(): boolean {

View File

@ -25,7 +25,7 @@ export class DatabaseSettingsComponent extends SettingsComponent<DataBaseConfig>
_settingsService: DatabaseSettingsService,
notification: NotificationService,
i18n: I18n) {
super(i18n('Database'), _authService, _navigation, _settingsService, notification, i18n, s => s.Server.database);
super(i18n('Database'), _authService, _navigation, _settingsService, notification, i18n, s => s.Server.Database);
}
ngOnInit() {

View File

@ -13,7 +13,7 @@ export class FacesSettingsService extends AbstractSettingsService<ClientConfig.F
}
public isSupported(): boolean {
return this._settingsService.settings.value.Server.database.type !== DatabaseType.memory &&
return this._settingsService.settings.value.Server.Database.type !== DatabaseType.memory &&
this._settingsService.settings.value.Client.Search.enabled === true;
}

View File

@ -37,7 +37,7 @@ export class IndexingSettingsComponent extends SettingsComponent<IndexingConfig,
<any>_settingsService,
notification,
i18n,
s => s.Server.indexing);
s => s.Server.Indexing);
}

View File

@ -41,7 +41,7 @@ export class IndexingSettingsService extends AbstractSettingsService<IndexingCon
public isSupported(): boolean {
return this._settingsService.settings.value.Server.database.type !== DatabaseType.memory;
return this._settingsService.settings.value.Server.Database.type !== DatabaseType.memory;
}

View File

@ -28,7 +28,7 @@ export class OtherSettingsComponent extends SettingsComponent<OtherConfigDTO> im
notification: NotificationService,
i18n: I18n) {
super(i18n('Other'), _authService, _navigation, _settingsService, notification, i18n, s => ({
Server: s.Server.threading,
Server: s.Server.Threading,
Client: s.Client.Other
}));
this.types = Utils.enumToArray(SortingMethods);

View File

@ -19,7 +19,7 @@ export class RandomPhotoSettingsService extends AbstractSettingsService<ClientCo
}
public isSupported(): boolean {
return this._settingsService.settings.value.Server.database.type !== DatabaseType.memory;
return this._settingsService.settings.value.Server.Database.type !== DatabaseType.memory;
}
public updateSettings(settings: ClientConfig.SharingConfig): Promise<void> {

View File

@ -13,7 +13,7 @@ export class SearchSettingsService extends AbstractSettingsService<ClientConfig.
}
public isSupported(): boolean {
return this._settingsService.settings.value.Server.database.type !== DatabaseType.memory;
return this._settingsService.settings.value.Server.Database.type !== DatabaseType.memory;
}
public updateSettings(settings: ClientConfig.SearchConfig): Promise<void> {

View File

@ -82,31 +82,31 @@ export class SettingsService {
languages: []
},
Server: {
database: {
Database: {
type: DatabaseType.memory
},
log: {
Log: {
level: LogLevel.info,
sqlLevel: SQLLogLevel.error
},
sharing: {
Sharing: {
updateTimeout: 2000
},
imagesFolder: '',
port: 80,
host: '0.0.0.0',
thumbnail: {
Thumbnail: {
personFaceMargin: 0.1,
folder: '',
qualityPriority: true,
processingLibrary: ThumbnailProcessingLib.sharp
},
threading: {
Threading: {
enable: true,
thumbnailThreads: 0
},
sessionTimeout: 0,
indexing: {
Indexing: {
cachedFolderTimeout: 0,
folderPreviewSize: 0,
reIndexingSensitivity: ReIndexingSensitivity.medium,
@ -114,11 +114,20 @@ export class SettingsService {
excludeFileList: []
},
photoMetadataSize: 512 * 1024,
duplicates: {
Duplicates: {
listingLimit: 1000
},
tasks: {
Tasks: {
scheduled: []
},
Video: {
transcoding: {
bitRate: 5 * 1024 * 1024,
codec: 'libx264',
format: 'mp4',
fps: 25,
resolution: 720
}
}
}
});

View File

@ -15,7 +15,7 @@ export class ShareSettingsService extends AbstractSettingsService<ClientConfig.S
public isSupported(): boolean {
return this._settingsService.settings.value.Server.database.type !== DatabaseType.memory &&
return this._settingsService.settings.value.Server.Database.type !== DatabaseType.memory &&
this._settingsService.settings.value.Client.authenticationRequired === true;
}

View File

@ -46,7 +46,7 @@ export class TasksSettingsComponent extends SettingsComponent<TaskConfig, TasksS
<any>_settingsService,
notification,
i18n,
s => s.Server.tasks);
s => s.Server.Tasks);
this.hasAvailableSettings = !this.simplifiedMode;
this.taskTriggerType = Utils.enumToArray(TaskTriggerType);

View File

@ -29,7 +29,7 @@ export class ThumbnailSettingsComponent
i18n: I18n) {
super(i18n('Thumbnail'), _authService, _navigation, _settingsService, notification, i18n, s => ({
client: s.Client.Thumbnail,
server: s.Server.thumbnail
server: s.Server.Thumbnail
}));
}

View File

@ -62,13 +62,13 @@ export class SQLTestHelper {
private async initSQLite() {
await this.resetSQLite();
Config.Server.database.type = DatabaseType.sqlite;
Config.Server.database.sqlite.storage = this.dbPath;
Config.Server.Database.type = DatabaseType.sqlite;
Config.Server.Database.sqlite.storage = this.dbPath;
}
private async initMySQL() {
Config.Server.database.type = DatabaseType.mysql;
Config.Server.database.mysql.database = 'pigallery2_test';
Config.Server.Database.type = DatabaseType.mysql;
Config.Server.Database.mysql.database = 'pigallery2_test';
await this.resetMySQL();
}
@ -85,8 +85,8 @@ export class SQLTestHelper {
}
private async resetMySQL() {
Config.Server.database.type = DatabaseType.mysql;
Config.Server.database.mysql.database = 'pigallery2_test';
Config.Server.Database.type = DatabaseType.mysql;
Config.Server.Database.mysql.database = 'pigallery2_test';
const conn = await SQLConnection.getConnection();
await conn.query('DROP DATABASE IF EXISTS ' + conn.options.database);
await conn.query('CREATE DATABASE IF NOT EXISTS ' + conn.options.database);
@ -94,8 +94,8 @@ export class SQLTestHelper {
}
private async clearUpMysql() {
Config.Server.database.type = DatabaseType.mysql;
Config.Server.database.mysql.database = 'pigallery2_test';
Config.Server.Database.type = DatabaseType.mysql;
Config.Server.Database.mysql.database = 'pigallery2_test';
const conn = await SQLConnection.getConnection();
await conn.query('DROP DATABASE IF EXISTS ' + conn.options.database);
await SQLConnection.close();

View File

@ -31,8 +31,8 @@ describe('Typeorm integration', () => {
fs.mkdirSync(tempDir);
}
Config.Server.database.type = DatabaseType.sqlite;
Config.Server.database.sqlite.storage = dbPath;
Config.Server.Database.type = DatabaseType.sqlite;
Config.Server.Database.sqlite.storage = dbPath;
};

View File

@ -457,7 +457,7 @@ describe('IndexingManager', (sqlHelper: SQLTestHelper) => {
});
it('with re indexing severity low', async () => {
Config.Server.indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
Config.Server.Indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
// @ts-ignore
fs.statSync = () => ({ctime: new Date(dirTime), mtime: new Date(dirTime)});