mirror of
https://github.com/bpatrik/pigallery2.git
synced 2024-12-23 01:27:14 +02:00
Adds main events to extension #753
This commit is contained in:
parent
538593e780
commit
f7dba927b8
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,3 +28,4 @@ locale.source.xlf
|
||||
test.*
|
||||
/db/
|
||||
/test/cypress/screenshots/
|
||||
/extensions/
|
||||
|
@ -11,6 +11,38 @@ if (forcedDebug === true) {
|
||||
);
|
||||
}
|
||||
|
||||
export type LoggerFunction = (...args: (string | number)[]) => void;
|
||||
|
||||
export interface ILogger {
|
||||
silly: LoggerFunction;
|
||||
debug: LoggerFunction;
|
||||
verbose: LoggerFunction;
|
||||
info: LoggerFunction;
|
||||
warn: LoggerFunction;
|
||||
error: LoggerFunction;
|
||||
}
|
||||
|
||||
export const createLoggerWrapper = (TAG: string): ILogger => ({
|
||||
silly: (...args: (string | number)[]) => {
|
||||
Logger.silly(TAG, ...args);
|
||||
},
|
||||
debug: (...args: (string | number)[]) => {
|
||||
Logger.debug(TAG, ...args);
|
||||
},
|
||||
verbose: (...args: (string | number)[]) => {
|
||||
Logger.verbose(TAG, ...args);
|
||||
},
|
||||
info: (...args: (string | number)[]) => {
|
||||
Logger.info(TAG, ...args);
|
||||
},
|
||||
warn: (...args: (string | number)[]) => {
|
||||
Logger.warn(TAG, ...args);
|
||||
},
|
||||
error: (...args: (string | number)[]) => {
|
||||
Logger.error(TAG, ...args);
|
||||
}
|
||||
});
|
||||
|
||||
export class Logger {
|
||||
public static silly(...args: (string | number)[]): void {
|
||||
if (!forcedDebug && Config.Server.Log.level < LogLevel.silly) {
|
||||
|
@ -16,15 +16,15 @@ export class ProjectPathClass {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
normalizeRelative(pathStr: string): string {
|
||||
public normalizeRelative(pathStr: string): string {
|
||||
return path.join(pathStr, path.sep);
|
||||
}
|
||||
|
||||
getAbsolutePath(pathStr: string): string {
|
||||
public getAbsolutePath(pathStr: string): string {
|
||||
return path.isAbsolute(pathStr) ? pathStr : path.join(this.Root, pathStr);
|
||||
}
|
||||
|
||||
getRelativePathToImages(pathStr: string): string {
|
||||
public getRelativePathToImages(pathStr: string): string {
|
||||
return path.relative(this.ImageFolder, pathStr);
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ export class ProjectPathClass {
|
||||
this.TranscodedFolder = path.join(this.TempFolder, 'tc');
|
||||
this.FacesFolder = path.join(this.TempFolder, 'f');
|
||||
this.DBFolder = this.getAbsolutePath(Config.Database.dbFolder);
|
||||
this.ExtensionFolder = path.join(this.Root, 'extension');
|
||||
this.ExtensionFolder = path.join(this.Root, 'extensions');
|
||||
|
||||
// create thumbnail folder if not exist
|
||||
if (!fs.existsSync(this.TempFolder)) {
|
||||
|
@ -19,7 +19,7 @@ export class AlbumManager implements IObjectManager {
|
||||
private static async updateAlbum(album: SavedSearchEntity): Promise<void> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const cover =
|
||||
await ObjectManagers.getInstance().CoverManager.getAlbumCover(album);
|
||||
await ObjectManagers.getInstance().CoverManager.getCoverForAlbum(album);
|
||||
const count = await
|
||||
ObjectManagers.getInstance().SearchManager.getCount((album as SavedSearchDTO).searchQuery);
|
||||
|
||||
|
@ -14,6 +14,7 @@ import {CoverPhotoDTO} from '../../../common/entities/PhotoDTO';
|
||||
import {IObjectManager} from './IObjectManager';
|
||||
import {Logger} from '../../Logger';
|
||||
import {SearchManager} from './SearchManager';
|
||||
import {ExtensionDecorator} from '../extension/ExtensionDecorator';
|
||||
|
||||
const LOG_TAG = '[CoverManager]';
|
||||
|
||||
@ -35,10 +36,11 @@ export class CoverManager implements IObjectManager {
|
||||
.execute();
|
||||
}
|
||||
|
||||
public async onNewDataVersion(changedDir: ParentDirectoryDTO): Promise<void> {
|
||||
@ExtensionDecorator(e => e.gallery.CoverManager.invalidateDirectoryCovers)
|
||||
protected async invalidateDirectoryCovers(dir: ParentDirectoryDTO) {
|
||||
// Invalidating Album cover
|
||||
let fullPath = DiskManager.normalizeDirPath(
|
||||
path.join(changedDir.path, changedDir.name)
|
||||
path.join(dir.path, dir.name)
|
||||
);
|
||||
const query = (await SQLConnection.getConnection())
|
||||
.createQueryBuilder()
|
||||
@ -77,7 +79,12 @@ export class CoverManager implements IObjectManager {
|
||||
await query.execute();
|
||||
}
|
||||
|
||||
public async getAlbumCover(album: {
|
||||
public async onNewDataVersion(changedDir: ParentDirectoryDTO): Promise<void> {
|
||||
await this.invalidateDirectoryCovers(changedDir);
|
||||
}
|
||||
|
||||
@ExtensionDecorator(e => e.gallery.CoverManager.getCoverForAlbum)
|
||||
public async getCoverForAlbum(album: {
|
||||
searchQuery: SearchQueryDTO;
|
||||
}): Promise<CoverPhotoDTOWithID> {
|
||||
const albumQuery: Brackets = await
|
||||
@ -138,11 +145,12 @@ export class CoverManager implements IObjectManager {
|
||||
.getRawMany();
|
||||
}
|
||||
|
||||
public async setAndGetCoverForDirectory(dir: {
|
||||
@ExtensionDecorator(e => e.gallery.CoverManager.getCoverForDirectory)
|
||||
protected async getCoverForDirectory(dir: {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
}): Promise<CoverPhotoDTOWithID> {
|
||||
}) {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const coverQuery = (): SelectQueryBuilder<MediaEntity> => {
|
||||
const query = connection
|
||||
@ -198,6 +206,16 @@ export class CoverManager implements IObjectManager {
|
||||
if (!coverMedia) {
|
||||
coverMedia = await coverQuery().limit(1).getOne();
|
||||
}
|
||||
return coverMedia;
|
||||
}
|
||||
|
||||
public async setAndGetCoverForDirectory(dir: {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
}): Promise<CoverPhotoDTOWithID> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const coverMedia = await this.getCoverForDirectory(dir);
|
||||
|
||||
// set validCover bit to true even if there is no cover (to prevent future updates)
|
||||
await connection
|
||||
|
@ -3,7 +3,7 @@ import {Config} from '../../../common/config/private/Config';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {IObjectManager} from '../database/IObjectManager';
|
||||
import {Logger} from '../../Logger';
|
||||
import {createLoggerWrapper, Logger} from '../../Logger';
|
||||
import {IExtensionEvents, IExtensionObject, IServerExtension} from './IExtension';
|
||||
import {ObjectManagers} from '../ObjectManagers';
|
||||
import {Server} from '../../server';
|
||||
@ -16,7 +16,19 @@ export class ExtensionManager implements IObjectManager {
|
||||
events: IExtensionEvents = {
|
||||
gallery: {
|
||||
MetadataLoader: {
|
||||
loadPhotoMetadata: new ExtensionEvent()
|
||||
loadPhotoMetadata: new ExtensionEvent(),
|
||||
loadVideoMetadata: new ExtensionEvent()
|
||||
},
|
||||
CoverManager: {
|
||||
getCoverForDirectory: new ExtensionEvent(),
|
||||
getCoverForAlbum: new ExtensionEvent(),
|
||||
invalidateDirectoryCovers: new ExtensionEvent(),
|
||||
},
|
||||
DiskManager: {
|
||||
scanDirectory: new ExtensionEvent()
|
||||
},
|
||||
ImageRenderer: {
|
||||
render: new ExtensionEvent()
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -27,15 +39,18 @@ export class ExtensionManager implements IObjectManager {
|
||||
}
|
||||
|
||||
public loadExtensionsList() {
|
||||
Logger.debug(LOG_TAG, 'Loading extension list from ' + ProjectPath.ExtensionFolder);
|
||||
if (!fs.existsSync(ProjectPath.ExtensionFolder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Config.Extensions.list = fs
|
||||
.readdirSync(ProjectPath.ExtensionFolder)
|
||||
.filter((f): boolean =>
|
||||
fs.statSync(path.join(ProjectPath.ExtensionFolder, f)).isDirectory()
|
||||
);
|
||||
Config.Extensions.list.sort();
|
||||
Logger.debug(LOG_TAG, 'Extensions found ', JSON.stringify(Config.Extensions.list));
|
||||
}
|
||||
|
||||
private async callServerFN(fn: (ext: IServerExtension, extName: string) => Promise<void>) {
|
||||
@ -44,6 +59,7 @@ export class ExtensionManager implements IObjectManager {
|
||||
const extPath = path.join(ProjectPath.ExtensionFolder, extName);
|
||||
const serverExt = path.join(extPath, 'server.js');
|
||||
if (!fs.existsSync(serverExt)) {
|
||||
Logger.silly(LOG_TAG, `Skipping ${extName} server initiation. server.js does not exists`);
|
||||
continue;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
@ -52,23 +68,24 @@ export class ExtensionManager implements IObjectManager {
|
||||
}
|
||||
}
|
||||
|
||||
private createExtensionObject(): IExtensionObject {
|
||||
private createExtensionObject(name: string): IExtensionObject {
|
||||
return {
|
||||
app: {
|
||||
_app: {
|
||||
objectManagers: ObjectManagers.getInstance(),
|
||||
config: Config,
|
||||
paths: ProjectPath,
|
||||
expressApp: Server.getInstance().app
|
||||
expressApp: Server.getInstance().app,
|
||||
config: Config
|
||||
},
|
||||
events: null
|
||||
paths: ProjectPath,
|
||||
Logger: createLoggerWrapper(`[Extension: ${name}]`),
|
||||
events: this.events
|
||||
};
|
||||
}
|
||||
|
||||
private async initExtensions() {
|
||||
await this.callServerFN(async (ext, extName) => {
|
||||
if (typeof ext?.init === 'function') {
|
||||
Logger.debug(LOG_TAG, 'Running Init on extension:' + extName);
|
||||
await ext?.init(this.createExtensionObject());
|
||||
Logger.debug(LOG_TAG, 'Running init on extension: ' + extName);
|
||||
await ext?.init(this.createExtensionObject(extName));
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -77,7 +94,7 @@ export class ExtensionManager implements IObjectManager {
|
||||
await this.callServerFN(async (ext, extName) => {
|
||||
if (typeof ext?.cleanUp === 'function') {
|
||||
Logger.debug(LOG_TAG, 'Running Init on extension:' + extName);
|
||||
await ext?.cleanUp();
|
||||
await ext?.cleanUp(this.createExtensionObject(extName));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import * as express from 'express';
|
||||
import {PrivateConfigClass} from '../../../common/config/private/Config';
|
||||
import {ObjectManagers} from '../ObjectManagers';
|
||||
import {ProjectPathClass} from '../../ProjectPath';
|
||||
import {ExtensionEvent} from './ExtensionEvent';
|
||||
import {ILogger} from '../../Logger';
|
||||
|
||||
|
||||
export type IExtensionBeforeEventHandler<I, O> = (input: { inputs: I }, event: { stopPropagation: boolean }) => Promise<{ inputs: I } | O>;
|
||||
@ -11,36 +11,66 @@ export type IExtensionAfterEventHandler<O> = (output: O) => Promise<O>;
|
||||
|
||||
export interface IExtensionEvent<I, O> {
|
||||
before: (handler: IExtensionBeforeEventHandler<I, O>) => void;
|
||||
after: (handler: IExtensionAfterEventHandler<O>) => void
|
||||
after: (handler: IExtensionAfterEventHandler<O>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* All main event callbacks in the app
|
||||
*/
|
||||
export interface IExtensionEvents {
|
||||
gallery: {
|
||||
// indexing: IExtensionEvent<any, any>;
|
||||
// scanningDirectory: IExtensionEvent<any, any>;
|
||||
/**
|
||||
* Events for Directory and Album covers
|
||||
*/
|
||||
CoverManager: {
|
||||
getCoverForAlbum: IExtensionEvent<any, any>;
|
||||
getCoverForDirectory: IExtensionEvent<any, any>
|
||||
/**
|
||||
* Invalidates directory covers for a given directory and every parent
|
||||
*/
|
||||
invalidateDirectoryCovers: IExtensionEvent<any, any>;
|
||||
},
|
||||
ImageRenderer: {
|
||||
/**
|
||||
* Renders a thumbnail or photo
|
||||
*/
|
||||
render: IExtensionEvent<any, any>
|
||||
},
|
||||
/**
|
||||
* Reads exif, iptc, etc.. metadata for photos/videos
|
||||
*/
|
||||
MetadataLoader: {
|
||||
loadVideoMetadata: IExtensionEvent<any, any>,
|
||||
loadPhotoMetadata: IExtensionEvent<any, any>
|
||||
},
|
||||
/**
|
||||
* Scans the storage for a given directory and returns the list of child directories,
|
||||
* photos, videos and metafiles
|
||||
*/
|
||||
DiskManager: {
|
||||
scanDirectory: IExtensionEvent<any, any>
|
||||
}
|
||||
|
||||
//listingDirectory: IExtensionEvent<any, any>;
|
||||
//searching: IExtensionEvent<any, any>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IExtensionApp {
|
||||
expressApp: express.Express;
|
||||
config: PrivateConfigClass;
|
||||
objectManagers: ObjectManagers;
|
||||
paths: ProjectPathClass;
|
||||
config: PrivateConfigClass;
|
||||
}
|
||||
|
||||
export interface IExtensionObject {
|
||||
app: IExtensionApp;
|
||||
_app: IExtensionApp;
|
||||
paths: ProjectPathClass;
|
||||
Logger: ILogger;
|
||||
events: IExtensionEvents;
|
||||
}
|
||||
|
||||
export interface IServerExtension {
|
||||
init(app: IExtensionObject): Promise<void>;
|
||||
|
||||
cleanUp?: () => Promise<void>;
|
||||
/**
|
||||
* Extension interface. All extension is expected to implement and export these methods
|
||||
*/
|
||||
export interface IServerExtension {
|
||||
init(extension: IExtensionObject): Promise<void>;
|
||||
cleanUp?: (extension: IExtensionObject) => Promise<void>;
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import {GPXProcessing} from './fileprocessing/GPXProcessing';
|
||||
import {MDFileDTO} from '../../../common/entities/MDFileDTO';
|
||||
import {MetadataLoader} from './MetadataLoader';
|
||||
import {NotificationManager} from '../NotifocationManager';
|
||||
import {ExtensionDecorator} from '../extension/ExtensionDecorator';
|
||||
|
||||
|
||||
const LOG_TAG = '[DiskManager]';
|
||||
@ -101,6 +102,7 @@ export class DiskManager {
|
||||
)) as ParentDirectoryDTO<FileDTO>;
|
||||
}
|
||||
|
||||
@ExtensionDecorator(e => e.gallery.DiskManager.scanDirectory)
|
||||
public static async scanDirectory(
|
||||
relativeDirectoryName: string,
|
||||
settings: DirectoryScanSettings = {}
|
||||
|
@ -18,6 +18,8 @@ const LOG_TAG = '[MetadataLoader]';
|
||||
const ffmpeg = FFmpegFactory.get();
|
||||
|
||||
export class MetadataLoader {
|
||||
|
||||
@ExtensionDecorator(e=>e.gallery.MetadataLoader.loadVideoMetadata)
|
||||
public static loadVideoMetadata(fullPath: string): Promise<VideoMetadata> {
|
||||
return new Promise<VideoMetadata>((resolve) => {
|
||||
const metadata: VideoMetadata = {
|
||||
|
@ -5,6 +5,7 @@ import {Logger} from '../../Logger';
|
||||
import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg';
|
||||
import {FFmpegFactory} from '../FFmpegFactory';
|
||||
import * as path from 'path';
|
||||
import {ExtensionDecorator} from '../extension/ExtensionDecorator';
|
||||
|
||||
|
||||
sharp.cache(false);
|
||||
@ -129,6 +130,7 @@ export class VideoRendererFactory {
|
||||
|
||||
export class ImageRendererFactory {
|
||||
|
||||
@ExtensionDecorator(e=>e.gallery.ImageRenderer.render)
|
||||
public static async render(input: MediaRendererInput | SvgRendererInput): Promise<void> {
|
||||
|
||||
let image: Sharp;
|
||||
|
@ -35,7 +35,7 @@ export class Server {
|
||||
public app: express.Express;
|
||||
private server: HttpServer;
|
||||
|
||||
static instance: Server;
|
||||
static instance: Server = null;
|
||||
|
||||
public static getInstance(): Server {
|
||||
if (this.instance === null) {
|
||||
@ -70,7 +70,13 @@ export class Server {
|
||||
).configPath +
|
||||
':'
|
||||
);
|
||||
Logger.verbose(LOG_TAG, JSON.stringify(Config.toJSON({attachDescription: false}), null, '\t'));
|
||||
Logger.verbose(LOG_TAG, JSON.stringify(Config.toJSON({attachDescription: false}), (k, v) => {
|
||||
const MAX_LENGTH = 80;
|
||||
if (typeof v === 'string' && v.length > MAX_LENGTH) {
|
||||
v = v.slice(0, MAX_LENGTH - 3) + '...';
|
||||
}
|
||||
return v;
|
||||
}, 2));
|
||||
|
||||
this.app = express();
|
||||
|
||||
|
@ -212,21 +212,21 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
|
||||
it('should get cover for saved search', async () => {
|
||||
const pm = new CoverManager();
|
||||
Config.AlbumCover.SearchQuery = null;
|
||||
expect(Utils.clone(await pm.getAlbumCover({
|
||||
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'sw'
|
||||
} as TextSearch
|
||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p4));
|
||||
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Boba'} as TextSearch;
|
||||
expect(Utils.clone(await pm.getAlbumCover({
|
||||
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'sw'
|
||||
} as TextSearch
|
||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p));
|
||||
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Derem'} as TextSearch;
|
||||
expect(Utils.clone(await pm.getAlbumCover({
|
||||
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'sw'
|
||||
@ -234,7 +234,7 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
|
||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
||||
// Having a preview search query that does not return valid result
|
||||
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'wont find it'} as TextSearch;
|
||||
expect(Utils.clone(await pm.getAlbumCover({
|
||||
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'Derem'
|
||||
@ -242,7 +242,7 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
|
||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
||||
// having a saved search that does not have any image
|
||||
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Derem'} as TextSearch;
|
||||
expect(Utils.clone(await pm.getAlbumCover({
|
||||
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'wont find it'
|
||||
|
Loading…
Reference in New Issue
Block a user