diff --git a/backend/Logger.ts b/backend/Logger.ts index 38d80cf4..69b38a62 100644 --- a/backend/Logger.ts +++ b/backend/Logger.ts @@ -5,7 +5,7 @@ import {LogLevel} from '../common/config/private/IPrivateConfig'; export const winstonSettings = { transports: [ new winston.transports.Console({ - level: LogLevel[Config.Server.log.level], + level: LogLevel[Config.Server.Log.level], handleExceptions: true, json: false, colorize: true, diff --git a/backend/ProjectPath.ts b/backend/ProjectPath.ts index bfb9d365..f74ace3e 100644 --- a/backend/ProjectPath.ts +++ b/backend/ProjectPath.ts @@ -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); + } + } } diff --git a/backend/middlewares/AdminMWs.ts b/backend/middlewares/AdminMWs.ts index e96ed188..19b39f4f 100644 --- a/backend/middlewares/AdminMWs.ts +++ b/backend/middlewares/AdminMWs.ts @@ -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(); diff --git a/backend/middlewares/GalleryMWs.ts b/backend/middlewares/GalleryMWs.ts index 5db7c565..c9b86d41 100644 --- a/backend/middlewares/GalleryMWs.ts +++ b/backend/middlewares/GalleryMWs.ts @@ -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) { diff --git a/backend/middlewares/VideoConverterMWs.ts b/backend/middlewares/VideoConverterMWs.ts new file mode 100644 index 00000000..db1f2f97 --- /dev/null +++ b/backend/middlewares/VideoConverterMWs.ts @@ -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 = + 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 { + + + 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); + + } +} + diff --git a/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts b/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts index 56897b80..4aec7691 100644 --- a/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts +++ b/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts @@ -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 = { @@ -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); diff --git a/backend/model/DiskManger.ts b/backend/model/DiskManger.ts index 3a111c8a..dbc0fc49 100644 --- a/backend/model/DiskManger.ts +++ b/backend/model/DiskManger.ts @@ -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 { + public static async scanDirectory(relativeDirectoryName: string, + settings: DiskMangerWorker.DirectoryScanSettings = {}): Promise { + 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) => { diff --git a/backend/model/diagnostics/ConfigDiagnostics.ts b/backend/model/diagnostics/ConfigDiagnostics.ts index 5a97b72c..164bd3e0 100644 --- a/backend/model/diagnostics/ConfigDiagnostics.ts +++ b/backend/model/diagnostics/ConfigDiagnostics.ts @@ -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. ' + diff --git a/backend/model/memory/GalleryManager.ts b/backend/model/memory/GalleryManager.ts index 3759addd..469f682b 100644 --- a/backend/model/memory/GalleryManager.ts +++ b/backend/model/memory/GalleryManager.ts @@ -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); } } diff --git a/backend/model/sql/GalleryManager.ts b/backend/model/sql/GalleryManager.ts index d21f511e..73854924 100644 --- a/backend/model/sql/GalleryManager.ts +++ b/backend/model/sql/GalleryManager.ts @@ -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; diff --git a/backend/model/sql/SQLConnection.ts b/backend/model/sql/SQLConnection.ts index 6b771dfb..5ed58d38 100644 --- a/backend/model/sql/SQLConnection.ts +++ b/backend/model/sql/SQLConnection.ts @@ -30,7 +30,7 @@ export class SQLConnection { public static async getConnection(): Promise { 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); diff --git a/backend/model/sql/SharingManager.ts b/backend/model/sql/SharingManager.ts index f33f2c3e..d750601c 100644 --- a/backend/model/sql/SharingManager.ts +++ b/backend/model/sql/SharingManager.ts @@ -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) { diff --git a/backend/model/sql/enitites/EntityUtils.ts b/backend/model/sql/enitites/EntityUtils.ts index d2b02a81..d8043ef5 100644 --- a/backend/model/sql/enitites/EntityUtils.ts +++ b/backend/model/sql/enitites/EntityUtils.ts @@ -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; } } diff --git a/backend/model/sql/enitites/VideoEntity.ts b/backend/model/sql/enitites/VideoEntity.ts index f7a01568..205ae951 100644 --- a/backend/model/sql/enitites/VideoEntity.ts +++ b/backend/model/sql/enitites/VideoEntity.ts @@ -16,6 +16,9 @@ export class VideoMetadataEntity extends MediaMetadataEntity implements VideoMet }) duration: number; + @Column('int') + fps: number; + } diff --git a/backend/model/tasks/TaskManager.ts b/backend/model/tasks/TaskManager.ts index 3a564c60..5f90c37d 100644 --- a/backend/model/tasks/TaskManager.ts +++ b/backend/model/tasks/TaskManager.ts @@ -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) { diff --git a/backend/model/tasks/TaskRepository.ts b/backend/model/tasks/TaskRepository.ts index 4e67eef0..93e9a625 100644 --- a/backend/model/tasks/TaskRepository.ts +++ b/backend/model/tasks/TaskRepository.ts @@ -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()); diff --git a/backend/model/tasks/DBResetTask.ts b/backend/model/tasks/tasks/DBResetTask.ts similarity index 50% rename from backend/model/tasks/DBResetTask.ts rename to backend/model/tasks/tasks/DBResetTask.ts index b2d2a08a..512ea20d 100644 --- a/backend/model/tasks/DBResetTask.ts +++ b/backend/model/tasks/tasks/DBResetTask.ts @@ -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() { diff --git a/backend/model/tasks/ITask.ts b/backend/model/tasks/tasks/ITask.ts similarity index 52% rename from backend/model/tasks/ITask.ts rename to backend/model/tasks/tasks/ITask.ts index 226d9011..06406008 100644 --- a/backend/model/tasks/ITask.ts +++ b/backend/model/tasks/tasks/ITask.ts @@ -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 extends TaskDTO { Name: string; diff --git a/backend/model/tasks/IndexingTask.ts b/backend/model/tasks/tasks/IndexingTask.ts similarity index 74% rename from backend/model/tasks/IndexingTask.ts rename to backend/model/tasks/tasks/IndexingTask.ts index 85ddd5f8..f3def143 100644 --- a/backend/model/tasks/IndexingTask.ts +++ b/backend/model/tasks/tasks/IndexingTask.ts @@ -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()); diff --git a/backend/model/tasks/Task.ts b/backend/model/tasks/tasks/Task.ts similarity index 90% rename from backend/model/tasks/Task.ts rename to backend/model/tasks/tasks/Task.ts index bfc03f2b..94c4a01e 100644 --- a/backend/model/tasks/Task.ts +++ b/backend/model/tasks/tasks/Task.ts @@ -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; diff --git a/backend/model/tasks/tasks/VideoConvertingTask.ts b/backend/model/tasks/tasks/VideoConvertingTask.ts new file mode 100644 index 00000000..2afd041e --- /dev/null +++ b/backend/model/tasks/tasks/VideoConvertingTask.ts @@ -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 { + 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(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; + } + +} diff --git a/backend/model/threading/DiskMangerWorker.ts b/backend/model/threading/DiskMangerWorker.ts index 52c7fe1c..3c253ec8 100644 --- a/backend/model/threading/DiskMangerWorker.ts +++ b/backend/model/threading/DiskMangerWorker.ts @@ -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 { + public static scanDirectory(relativeDirectoryName: string, settings: DiskMangerWorker.DirectoryScanSettings = {}): Promise { return new Promise((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({ 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({ @@ -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({ 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; + } +} diff --git a/backend/model/threading/MetadataLoader.ts b/backend/model/threading/MetadataLoader.ts index 69ade248..dd089a81 100644 --- a/backend/model/threading/MetadataLoader.ts +++ b/backend/model/threading/MetadataLoader.ts @@ -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; } diff --git a/backend/model/threading/ThreadPool.ts b/backend/model/threading/ThreadPool.ts index 2a118e40..d718ebc2 100644 --- a/backend/model/threading/ThreadPool.ts +++ b/backend/model/threading/ThreadPool.ts @@ -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 { @@ -26,6 +27,12 @@ export class ThreadPool { } } + protected executeTask(task: WorkerTask): Promise { + 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 { worker.worker.send(poolTask.data); }; - protected executeTask(task: WorkerTask): Promise { - 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 { } export class DiskManagerTH extends ThreadPool implements ITaskExecuter { - execute(relativeDirectoryName: string): Promise { + execute(relativeDirectoryName: string, settings: DiskMangerWorker.DirectoryScanSettings = {}): Promise { return super.executeTask({ type: WorkerTaskTypes.diskManager, - relativeDirectoryName: relativeDirectoryName + relativeDirectoryName: relativeDirectoryName, + settings: settings }); } } @@ -101,7 +103,7 @@ export class ThumbnailTH extends ThreadPool implements ITaskExecuter{ type: WorkerTaskTypes.thumbnail, input: input, - renderer: Config.Server.thumbnail.processingLibrary + renderer: Config.Server.Thumbnail.processingLibrary }); } } diff --git a/backend/model/threading/VideoConverterWorker.ts b/backend/model/threading/VideoConverterWorker.ts new file mode 100644 index 00000000..56cdb213 --- /dev/null +++ b/backend/model/threading/VideoConverterWorker.ts @@ -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 { + + 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); + + }); + } + +} + diff --git a/backend/model/threading/Worker.ts b/backend/model/threading/Worker.ts index 7397aa9a..ed2c80f7 100644 --- a/backend/model/threading/Worker.ts +++ b/backend/model/threading/Worker.ts @@ -15,7 +15,7 @@ export class Worker { let result = null; switch (task.type) { case WorkerTaskTypes.diskManager: - result = await DiskMangerWorker.scanDirectory((task).relativeDirectoryName); + result = await DiskMangerWorker.scanDirectory((task).relativeDirectoryName, (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 { diff --git a/backend/routes/GalleryRouter.ts b/backend/routes/GalleryRouter.ts index 8b020b49..bb316750 100644 --- a/backend/routes/GalleryRouter.ts +++ b/backend/routes/GalleryRouter.ts @@ -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))'], diff --git a/backend/server.ts b/backend/server.ts index ff9bfc05..6717d2a6 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -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(); diff --git a/benchmark/Benchmarks.ts b/benchmark/Benchmarks.ts index 6bed180a..814defea 100644 --- a/benchmark/Benchmarks.ts +++ b/benchmark/Benchmarks.ts @@ -45,7 +45,7 @@ export class Benchmarks { async bmListDirectory(): Promise { 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(); }; diff --git a/common/DataStructureVersion.ts b/common/DataStructureVersion.ts index 3fe2b26f..3767b7ce 100644 --- a/common/DataStructureVersion.ts +++ b/common/DataStructureVersion.ts @@ -1 +1 @@ -export const DataStructureVersion = 13; +export const DataStructureVersion = 14; diff --git a/common/config/private/IPrivateConfig.ts b/common/config/private/IPrivateConfig.ts index 6cab0f43..e8a6b40c 100644 --- a/common/config/private/IPrivateConfig.ts +++ b/common/config/private/IPrivateConfig.ts @@ -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 { diff --git a/common/config/private/PrivateConfigClass.ts b/common/config/private/PrivateConfigClass.ts index f39e2f11..953cb045 100644 --- a/common/config/private/PrivateConfigClass.ts +++ b/common/config/private/PrivateConfigClass.ts @@ -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); } } diff --git a/common/entities/VideoDTO.ts b/common/entities/VideoDTO.ts index e28658c5..fa754694 100644 --- a/common/entities/VideoDTO.ts +++ b/common/entities/VideoDTO.ts @@ -15,4 +15,5 @@ export interface VideoMetadata extends MediaMetadata { bitRate: number; duration: number; // in milliseconds fileSize: number; + fps: number; } diff --git a/common/entities/task/TaskDTO.ts b/common/entities/task/TaskDTO.ts index f9fbe138..6b75e750 100644 --- a/common/entities/task/TaskDTO.ts +++ b/common/entities/task/TaskDTO.ts @@ -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 { diff --git a/frontend/app/ui/gallery/MediaIcon.ts b/frontend/app/ui/gallery/MediaIcon.ts index c0e6a219..309ee8e0 100644 --- a/frontend/app/ui/gallery/MediaIcon.ts +++ b/frontend/app/ui/gallery/MediaIcon.ts @@ -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 diff --git a/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.css b/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.css index cf271f2a..c5f797f9 100644 --- a/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.css +++ b/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.css @@ -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, diff --git a/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.html b/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.html index 74728d3d..4a38570d 100644 --- a/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.html +++ b/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.html @@ -5,7 +5,7 @@ +
+ fps: {{VideoData.fps}}/s +
bit rate : {{VideoData.bitRate | fileSize}}/s diff --git a/frontend/app/ui/gallery/lightbox/media/media.lightbox.gallery.component.html b/frontend/app/ui/gallery/lightbox/media/media.lightbox.gallery.component.html index 0cee0986..cd897eeb 100644 --- a/frontend/app/ui/gallery/lightbox/media/media.lightbox.gallery.component.html +++ b/frontend/app/ui/gallery/lightbox/media/media.lightbox.gallery.component.html @@ -22,7 +22,7 @@ (error)="onImageError()" (timeupdate)="onVideoProgress()" #video> - +
diff --git a/frontend/app/ui/gallery/lightbox/media/media.lightbox.gallery.component.ts b/frontend/app/ui/gallery/lightbox/media/media.lightbox.gallery.component.ts index bfdcc36e..b4fc6930 100644 --- a/frontend/app/ui/gallery/lightbox/media/media.lightbox.gallery.component.ts +++ b/frontend/app/ui/gallery/lightbox/media/media.lightbox.gallery.component.ts @@ -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 { diff --git a/frontend/app/ui/settings/database/database.settings.component.ts b/frontend/app/ui/settings/database/database.settings.component.ts index 4a280ee8..c9ba3851 100644 --- a/frontend/app/ui/settings/database/database.settings.component.ts +++ b/frontend/app/ui/settings/database/database.settings.component.ts @@ -25,7 +25,7 @@ export class DatabaseSettingsComponent extends SettingsComponent _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() { diff --git a/frontend/app/ui/settings/faces/faces.settings.service.ts b/frontend/app/ui/settings/faces/faces.settings.service.ts index 3c10500e..319f4251 100644 --- a/frontend/app/ui/settings/faces/faces.settings.service.ts +++ b/frontend/app/ui/settings/faces/faces.settings.service.ts @@ -13,7 +13,7 @@ export class FacesSettingsService extends AbstractSettingsService_settingsService, notification, i18n, - s => s.Server.indexing); + s => s.Server.Indexing); } diff --git a/frontend/app/ui/settings/indexing/indexing.settings.service.ts b/frontend/app/ui/settings/indexing/indexing.settings.service.ts index 26c5076c..1edeecb7 100644 --- a/frontend/app/ui/settings/indexing/indexing.settings.service.ts +++ b/frontend/app/ui/settings/indexing/indexing.settings.service.ts @@ -41,7 +41,7 @@ export class IndexingSettingsService extends AbstractSettingsService 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); diff --git a/frontend/app/ui/settings/random-photo/random-photo.settings.service.ts b/frontend/app/ui/settings/random-photo/random-photo.settings.service.ts index 325cad04..844aa00f 100644 --- a/frontend/app/ui/settings/random-photo/random-photo.settings.service.ts +++ b/frontend/app/ui/settings/random-photo/random-photo.settings.service.ts @@ -19,7 +19,7 @@ export class RandomPhotoSettingsService extends AbstractSettingsService { diff --git a/frontend/app/ui/settings/search/search.settings.service.ts b/frontend/app/ui/settings/search/search.settings.service.ts index 9d81017f..240d62b9 100644 --- a/frontend/app/ui/settings/search/search.settings.service.ts +++ b/frontend/app/ui/settings/search/search.settings.service.ts @@ -13,7 +13,7 @@ export class SearchSettingsService extends AbstractSettingsService { diff --git a/frontend/app/ui/settings/settings.service.ts b/frontend/app/ui/settings/settings.service.ts index feb718d5..2b15895c 100644 --- a/frontend/app/ui/settings/settings.service.ts +++ b/frontend/app/ui/settings/settings.service.ts @@ -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 + } } } }); diff --git a/frontend/app/ui/settings/share/share.settings.service.ts b/frontend/app/ui/settings/share/share.settings.service.ts index 5713c96d..d7e50d8e 100644 --- a/frontend/app/ui/settings/share/share.settings.service.ts +++ b/frontend/app/ui/settings/share/share.settings.service.ts @@ -15,7 +15,7 @@ export class ShareSettingsService extends AbstractSettingsService_settingsService, notification, i18n, - s => s.Server.tasks); + s => s.Server.Tasks); this.hasAvailableSettings = !this.simplifiedMode; this.taskTriggerType = Utils.enumToArray(TaskTriggerType); diff --git a/frontend/app/ui/settings/thumbnail/thumbanil.settings.component.ts b/frontend/app/ui/settings/thumbnail/thumbanil.settings.component.ts index 7cb05595..366dafe1 100644 --- a/frontend/app/ui/settings/thumbnail/thumbanil.settings.component.ts +++ b/frontend/app/ui/settings/thumbnail/thumbanil.settings.component.ts @@ -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 })); } diff --git a/test/backend/SQLTestHelper.ts b/test/backend/SQLTestHelper.ts index 54b75709..b22a49b7 100644 --- a/test/backend/SQLTestHelper.ts +++ b/test/backend/SQLTestHelper.ts @@ -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(); diff --git a/test/backend/integration/model/sql/typeorm.ts b/test/backend/integration/model/sql/typeorm.ts index c159efcb..a58afeec 100644 --- a/test/backend/integration/model/sql/typeorm.ts +++ b/test/backend/integration/model/sql/typeorm.ts @@ -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; }; diff --git a/test/backend/unit/model/sql/IndexingManager.ts b/test/backend/unit/model/sql/IndexingManager.ts index cc54c3ad..45ff58bd 100644 --- a/test/backend/unit/model/sql/IndexingManager.ts +++ b/test/backend/unit/model/sql/IndexingManager.ts @@ -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)});