diff --git a/backend/middlewares/AdminMWs.ts b/backend/middlewares/AdminMWs.ts index d955dd9..940f688 100644 --- a/backend/middlewares/AdminMWs.ts +++ b/backend/middlewares/AdminMWs.ts @@ -5,7 +5,7 @@ import {Logger} from '../Logger'; import {SQLConnection} from '../model/sql/SQLConnection'; import {DataBaseConfig, DatabaseType, IndexingConfig, IPrivateConfig, ThumbnailConfig} from '../../common/config/private/IPrivateConfig'; import {Config} from '../../common/config/private/Config'; -import {ConfigDiagnostics} from '../model/ConfigDiagnostics'; +import {ConfigDiagnostics} from '../model/diagnostics/ConfigDiagnostics'; import {ClientConfig} from '../../common/config/public/ConfigClass'; import {BasicConfigDTO} from '../../common/entities/settings/BasicConfigDTO'; import {OtherConfigDTO} from '../../common/entities/settings/OtherConfigDTO'; @@ -80,6 +80,27 @@ export class AdminMWs { return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + JSON.stringify(err, null, ' '), err)); } } + public static async updateVideoSettings(req: Request, res: Response, next: NextFunction) { + if ((typeof req.body === 'undefined') || (typeof req.body.settings === 'undefined')) { + return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'settings is needed')); + } + + try { + await ConfigDiagnostics.testVideoConfig(req.body.settings); + + Config.Client.Video = req.body.settings; + // only updating explicitly set config (not saving config set by the diagnostics) + const original = Config.original(); + original.Client.Video = req.body.settings; + original.save(); + await ConfigDiagnostics.runDiagnostics(); + Logger.info(LOG_TAG, 'new config:'); + Logger.info(LOG_TAG, JSON.stringify(Config, null, '\t')); + return next(); + } catch (err) { + return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + err.toString(), err)); + } + } public static async updateShareSettings(req: Request, res: Response, next: NextFunction) { if ((typeof req.body === 'undefined') || (typeof req.body.settings === 'undefined')) { diff --git a/backend/middlewares/GalleryMWs.ts b/backend/middlewares/GalleryMWs.ts index b55ff8d..c39515c 100644 --- a/backend/middlewares/GalleryMWs.ts +++ b/backend/middlewares/GalleryMWs.ts @@ -52,7 +52,7 @@ export class GalleryMWs { } - public static removeCyclicDirectoryReferences(req: Request, res: Response, next: NextFunction) { + public static cleanUpGalleryResults(req: Request, res: Response, next: NextFunction) { if (!req.resultPipe) { return next(); } @@ -61,17 +61,6 @@ export class GalleryMWs { if (cw.notModified === true) { return next(); } - const removeDirs = (dir: DirectoryDTO) => { - dir.media.forEach((photo: PhotoDTO) => { - photo.directory = null; - }); - - dir.directories.forEach((directory: DirectoryDTO) => { - removeDirs(directory); - directory.parent = null; - }); - - }; const cleanUpMedia = (media: MediaDTO[]) => { media.forEach(m => { @@ -89,7 +78,7 @@ export class GalleryMWs { }; if (cw.directory) { - removeDirs(cw.directory); + DirectoryDTO.removeReferences(cw.directory); // TODO: remove when typeorm inheritance is fixed cleanUpMedia(cw.directory.media); } @@ -98,6 +87,19 @@ export class GalleryMWs { } + if (Config.Client.Video.enabled === false) { + if (cw.directory) { + const removeVideos = (dir: DirectoryDTO) => { + dir.media = dir.media.filter(m => !MediaDTO.isVideo(m)); + dir.directories.forEach(d => removeVideos(d)); + }; + removeVideos(cw.directory); + } + if (cw.searchResult) { + cw.searchResult.media = cw.searchResult.media.filter(m => !MediaDTO.isVideo(m)); + } + } + return next(); } diff --git a/backend/model/FFmpegFactory.ts b/backend/model/FFmpegFactory.ts new file mode 100644 index 0000000..24ae026 --- /dev/null +++ b/backend/model/FFmpegFactory.ts @@ -0,0 +1,13 @@ +export class FFmpegFactory { + public static get() { + const ffmpeg = require('fluent-ffmpeg'); + try { + const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path; + ffmpeg.setFfmpegPath(ffmpegPath); + const ffprobePath = require('@ffprobe-installer/ffprobe').path; + ffmpeg.setFfprobePath(ffprobePath); + } catch (e) { + } + return ffmpeg; + } +} diff --git a/backend/model/ConfigDiagnostics.ts b/backend/model/diagnostics/ConfigDiagnostics.ts similarity index 81% rename from backend/model/ConfigDiagnostics.ts rename to backend/model/diagnostics/ConfigDiagnostics.ts index c610be4..563cc2b 100644 --- a/backend/model/ConfigDiagnostics.ts +++ b/backend/model/diagnostics/ConfigDiagnostics.ts @@ -1,17 +1,19 @@ -import {Config} from '../../common/config/private/Config'; +import {Config} from '../../../common/config/private/Config'; import { DataBaseConfig, DatabaseType, IPrivateConfig, ThumbnailConfig, ThumbnailProcessingLib -} from '../../common/config/private/IPrivateConfig'; -import {Logger} from '../Logger'; -import {NotificationManager} from './NotifocationManager'; -import {ProjectPath} from '../ProjectPath'; -import {SQLConnection} from './sql/SQLConnection'; +} from '../../../common/config/private/IPrivateConfig'; +import {Logger} from '../../Logger'; +import {NotificationManager} from '../NotifocationManager'; +import {ProjectPath} from '../../ProjectPath'; +import {SQLConnection} from '../sql/SQLConnection'; import * as fs from 'fs'; -import {ClientConfig} from '../../common/config/public/ConfigClass'; +import {ClientConfig} from '../../../common/config/public/ConfigClass'; +import VideoConfig = ClientConfig.VideoConfig; +import {FFmpegFactory} from '../FFmpegFactory'; const LOG_TAG = '[ConfigDiagnostics]'; @@ -24,6 +26,31 @@ export class ConfigDiagnostics { } + static testVideoConfig(videoConfig: VideoConfig) { + return new Promise((resolve, reject) => { + try { + if (videoConfig.enabled === true) { + const ffmpeg = FFmpegFactory.get(); + ffmpeg().getAvailableCodecs((err) => { + if (err) { + return reject(new Error('Error accessing ffmpeg, cant find executable: ' + err.toString())); + } + ffmpeg(__dirname + '/blank.jpg').ffprobe((err2) => { + if (err2) { + return reject(new Error('Error accessing ffmpeg-probe, cant find executable: ' + err2.toString())); + } + return resolve(); + }); + }); + } else { + return resolve(); + } + } catch (e) { + return reject(new Error('unkown video error: ' + e.toString())); + } + }); + } + static async testThumbnailLib(processingLibrary: ThumbnailProcessingLib) { switch (processingLibrary) { case ThumbnailProcessingLib.sharp: @@ -44,8 +71,8 @@ export class ConfigDiagnostics { } } - static async testThumbnailFolder(folder: string) { - await new Promise((resolve, reject) => { + static testThumbnailFolder(folder: string) { + return new Promise((resolve, reject) => { fs.access(folder, fs.constants.W_OK, (err) => { if (err) { reject({message: 'Error during getting write access to temp folder', error: err.toString()}); @@ -55,8 +82,8 @@ export class ConfigDiagnostics { }); } - static async testImageFolder(folder: string) { - await new Promise((resolve, reject) => { + static testImageFolder(folder: string) { + return new Promise((resolve, reject) => { if (!fs.existsSync(folder)) { reject('Images folder not exists: \'' + folder + '\''); } @@ -164,6 +191,16 @@ export class ConfigDiagnostics { } + try { + await ConfigDiagnostics.testVideoConfig(Config.Client.Video); + } catch (ex) { + const err: Error = ex; + NotificationManager.warning('Video support error, switching off..', err.toString()); + Logger.warn(LOG_TAG, 'Video support error, switching off..', err.toString()); + Config.Client.Video.enabled = false; + } + + try { await ConfigDiagnostics.testImageFolder(Config.Server.imagesFolder); } catch (ex) { diff --git a/backend/model/diagnostics/blank.jpg b/backend/model/diagnostics/blank.jpg new file mode 100644 index 0000000..1cda9a5 Binary files /dev/null and b/backend/model/diagnostics/blank.jpg differ diff --git a/backend/model/sql/SQLConnection.ts b/backend/model/sql/SQLConnection.ts index 252fe23..ee660a1 100644 --- a/backend/model/sql/SQLConnection.ts +++ b/backend/model/sql/SQLConnection.ts @@ -17,7 +17,7 @@ import {VideoEntity} from './enitites/VideoEntity'; export class SQLConnection { - private static VERSION = 1; + private static VERSION = 2; constructor() { } diff --git a/backend/model/threading/DiskMangerWorker.ts b/backend/model/threading/DiskMangerWorker.ts index 3cb39b3..b806c17 100644 --- a/backend/model/threading/DiskMangerWorker.ts +++ b/backend/model/threading/DiskMangerWorker.ts @@ -5,15 +5,17 @@ import {CameraMetadata, GPSMetadata, PhotoDTO, PhotoMetadata} from '../../../com import {Logger} from '../../Logger'; import {IptcParser} from 'ts-node-iptc'; import {ExifParserFactory, OrientationTypes} from 'ts-exif-parser'; -import * as ffmpeg from 'fluent-ffmpeg'; import {FfprobeData} from 'fluent-ffmpeg'; import {ProjectPath} from '../../ProjectPath'; import {Config} from '../../../common/config/private/Config'; import {VideoDTO, VideoMetadata} from '../../../common/entities/VideoDTO'; -import {MediaDimension, MediaMetadata} from '../../../common/entities/MediaDTO'; +import {MediaDimension} from '../../../common/entities/MediaDTO'; +import {FFmpegFactory} from '../FFmpegFactory'; const LOG_TAG = '[DiskManagerTask]'; +const ffmpeg = FFmpegFactory.get(); + export class DiskMangerWorker { private static isImage(fullPath: string) { const extensions = [ @@ -33,8 +35,7 @@ export class DiskMangerWorker { private static isVideo(fullPath: string) { const extensions = [ - '.mp4', - '.webm' + '.mp4' ]; const extension = path.extname(fullPath).toLowerCase(); @@ -83,7 +84,8 @@ export class DiskMangerWorker { if (maxPhotos != null && directory.media.length > maxPhotos) { break; } - } else if (DiskMangerWorker.isVideo(fullFilePath)) { + } else if (Config.Client.Video.enabled === true && + DiskMangerWorker.isVideo(fullFilePath)) { directory.media.push({ name: file, directory: null, @@ -98,7 +100,7 @@ export class DiskMangerWorker { return resolve(directory); } catch (err) { - return reject({error: err}); + return reject({error: err.toString()}); } }); diff --git a/backend/model/threading/ThumbnailWorker.ts b/backend/model/threading/ThumbnailWorker.ts index b6a3f61..cdccfbc 100644 --- a/backend/model/threading/ThumbnailWorker.ts +++ b/backend/model/threading/ThumbnailWorker.ts @@ -3,6 +3,7 @@ import {Dimensions, State} from 'gm'; import {Logger} from '../../Logger'; import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg'; import {ThumbnailProcessingLib} from '../../../common/config/private/IPrivateConfig'; +import {FFmpegFactory} from '../FFmpegFactory'; export class ThumbnailWorker { @@ -50,7 +51,7 @@ export interface RendererInput { export class VideoRendererFactory { public static build(): (input: RendererInput) => Promise { - const ffmpeg = require('fluent-ffmpeg'); + const ffmpeg = FFmpegFactory.get(); return (input: RendererInput): Promise => { return new Promise((resolve, reject) => { diff --git a/backend/routes/AdminRouter.ts b/backend/routes/AdminRouter.ts index 3107cb6..44cccf2 100644 --- a/backend/routes/AdminRouter.ts +++ b/backend/routes/AdminRouter.ts @@ -59,6 +59,12 @@ export class AdminRouter { AdminMWs.updateMapSettings, RenderingMWs.renderOK ); + app.put('/api/settings/video', + AuthenticationMWs.authenticate, + AuthenticationMWs.authorise(UserRoles.Admin), + AdminMWs.updateVideoSettings, + RenderingMWs.renderOK + ); app.put('/api/settings/authentication', AuthenticationMWs.authenticate, diff --git a/backend/routes/GalleryRouter.ts b/backend/routes/GalleryRouter.ts index a377655..4ccc6b6 100644 --- a/backend/routes/GalleryRouter.ts +++ b/backend/routes/GalleryRouter.ts @@ -27,7 +27,7 @@ export class GalleryRouter { AuthenticationMWs.authoriseDirectory, GalleryMWs.listDirectory, ThumbnailGeneratorMWs.addThumbnailInformation, - GalleryMWs.removeCyclicDirectoryReferences, + GalleryMWs.cleanUpGalleryResults, RenderingMWs.renderResult ); } @@ -98,7 +98,7 @@ export class GalleryRouter { AuthenticationMWs.authorise(UserRoles.Guest), GalleryMWs.search, ThumbnailGeneratorMWs.addThumbnailInformation, - GalleryMWs.removeCyclicDirectoryReferences, + GalleryMWs.cleanUpGalleryResults, RenderingMWs.renderResult ); } @@ -109,7 +109,7 @@ export class GalleryRouter { AuthenticationMWs.authorise(UserRoles.Guest), GalleryMWs.instantSearch, ThumbnailGeneratorMWs.addThumbnailInformation, - GalleryMWs.removeCyclicDirectoryReferences, + GalleryMWs.cleanUpGalleryResults, RenderingMWs.renderResult ); } diff --git a/backend/server.ts b/backend/server.ts index 76aa4a2..6f73399 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -17,7 +17,7 @@ import {LoggerRouter} from './routes/LoggerRouter'; import {ThumbnailGeneratorMWs} from './middlewares/thumbnail/ThumbnailGeneratorMWs'; import {DiskManager} from './model/DiskManger'; import {NotificationRouter} from './routes/NotificationRouter'; -import {ConfigDiagnostics} from './model/ConfigDiagnostics'; +import {ConfigDiagnostics} from './model/diagnostics/ConfigDiagnostics'; import {Localizations} from './model/Localizations'; import {CookieNames} from '../common/CookieNames'; diff --git a/common/config/public/ConfigClass.ts b/common/config/public/ConfigClass.ts index 495ad86..b9fe229 100644 --- a/common/config/public/ConfigClass.ts +++ b/common/config/public/ConfigClass.ts @@ -43,6 +43,11 @@ export module ClientConfig { NavBar: NavBarConfig; } + export interface VideoConfig { + enabled: boolean; + } + + export interface Config { applicationTitle: string; publicUrl: string; @@ -55,6 +60,7 @@ export module ClientConfig { Other: OtherConfig; authenticationRequired: boolean; languages: string[]; + Video: VideoConfig; } } @@ -91,6 +97,9 @@ export class PublicConfigClass { RandomPhoto: { enabled: true }, + Video: { + enabled: true + }, Other: { enableCache: true, enableOnScrollRendering: true, diff --git a/common/entities/DirectoryDTO.ts b/common/entities/DirectoryDTO.ts index c6b8774..2c87de4 100644 --- a/common/entities/DirectoryDTO.ts +++ b/common/entities/DirectoryDTO.ts @@ -1,4 +1,5 @@ import {MediaDTO} from './MediaDTO'; +import {PhotoDTO} from './PhotoDTO'; export interface DirectoryDTO { id: number; @@ -23,4 +24,16 @@ export module DirectoryDTO { directory.parent = dir; }); }; + + export const removeReferences = (dir: DirectoryDTO) => { + dir.media.forEach((photo: PhotoDTO) => { + photo.directory = null; + }); + + dir.directories.forEach((directory: DirectoryDTO) => { + removeReferences(directory); + directory.parent = null; + }); + + }; } diff --git a/frontend/app/admin/admin.component.html b/frontend/app/admin/admin.component.html index 1a096d1..b5c98c0 100644 --- a/frontend/app/admin/admin.component.html +++ b/frontend/app/admin/admin.component.html @@ -43,13 +43,15 @@ + + [simplifiedMode]="simplifiedMode"> try { await this._settingsService.updateSettings(this.settings); await this.getSettings(); - this.notification.success(this.name + this.i18n(' settings saved'), this.i18n('Success')); + this.notification.success(this.name + ' ' + this.i18n('settings saved'), this.i18n('Success')); this.inProgress = false; return true; } catch (err) { diff --git a/frontend/app/settings/database/database.settings.component.html b/frontend/app/settings/database/database.settings.component.html index b8d3b6d..574d6a6 100644 --- a/frontend/app/settings/database/database.settings.component.html +++ b/frontend/app/settings/database/database.settings.component.html @@ -16,9 +16,9 @@

MySQL settings:

- - diff --git a/frontend/app/settings/settings.service.ts b/frontend/app/settings/settings.service.ts index 7b7ea41..d807f7d 100644 --- a/frontend/app/settings/settings.service.ts +++ b/frontend/app/settings/settings.service.ts @@ -36,6 +36,9 @@ export class SettingsService { RandomPhoto: { enabled: true }, + Video: { + enabled: true + }, Other: { enableCache: true, enableOnScrollRendering: true, diff --git a/frontend/app/settings/usermanager/usermanager.settings.component.html b/frontend/app/settings/usermanager/usermanager.settings.component.html index 0e31566..29dbbff 100644 --- a/frontend/app/settings/usermanager/usermanager.settings.component.html +++ b/frontend/app/settings/usermanager/usermanager.settings.component.html @@ -74,7 +74,7 @@