UNPKG

wranglebot

Version:

open source media asset management

1,283 lines (1,138 loc) 43.8 kB
import TranscodeBot from "./transcode/index.js"; import LogBot from "logbotjs"; import User from "./accounts/User.js"; import { Volume } from "./drives/Volume.js"; import { Thumbnail } from "./library/Thumbnail.js"; import MetaLibrary from "./library/MetaLibrary.js"; import { MetaFile } from "./library/MetaFile.js"; import { indexer } from "./media/Indexer.js"; import Task from "./media/Task.js"; import { MetaCopy } from "./library/MetaCopy.js"; import { TranscodeTask } from "./transcode/TranscodeTask.js"; import utility from "./system/utility.js"; import api from "../api/index.js"; import AccountManager from "./accounts/AccountManager.js"; import createTaskOptions from "./library/createTaskOptions.js"; import { MLInterface } from "./analyse/MLInterface.js"; import analyseMetaFileOptions from "./library/analyseMetaFileOptions.js"; import extensions from "../extensions/index.js"; import Extension from "./Extension.js"; import MetaLibraryOptions from "./library/MetaLibraryOptions.js"; import MetaLibraryUpdateOptions from "./library/MetaLibraryUpdateOptions.js"; import FolderOptions from "./library/FolderOptions.js"; import Transaction from "./database/Transaction.js"; import CancelToken from "./library/CancelToken.js"; import WrangleBotOptions from "./WrangleBotOptions.js"; import EventEmitter from "events"; import { config, finder } from "./system/index.js"; import { SearchLite } from "searchlite"; //load here, otherwise the config will be preloaded and the config will be overwritten import { driveBot, DriveBot } from "./drives/DriveBot.js"; import { v4 as uuidv4 } from "uuid"; import DB from "./database/DB.js"; import { DB as Database } from "./database/DB.js"; interface ReturnObject { status: 200 | 400 | 500 | 404; message?: string; result?: any; } /** * WrangleBot Interface * @class WrangleBot */ class WrangleBot extends EventEmitter { static OPEN = "open"; static CLOSED = "closed"; pingInterval; ping; // libraries: Array<MetaLibrary> = []; /** * @type {DriveBot} */ driveBot: DriveBot = driveBot; accountManager = AccountManager; finder = finder; ML; config = config; status = WrangleBot.CLOSED; /** * index */ index: { libraries: MetaLibrary[]; metaFiles: { [key: string]: MetaFile }; metaCopies: { [key: string]: MetaCopy }; copyTasks: { [key: string]: Task }; transcodes: { [key: string]: TranscodeTask }; } = { libraries: [], metaFiles: {}, metaCopies: {}, copyTasks: {}, transcodes: {}, }; private thirdPartyExtensions: Extension[] = []; private servers: any; db: Database | any; constructor() { super(); } async open(options: WrangleBotOptions) { config.build(options.app_data_location); LogBot.log(100, "Opening WrangleBot instance ... "); this.$emit("notification", { title: "Opening WrangleBot", message: "WrangleBot is starting up", }); if (!config) throw new Error("Config failed to load. Aborting. Delete the config file and restart the bot."); // get or set the port if (options.port) config.set("port", options.port); else throw new Error("No port provided"); this.pingInterval = this.config.get("pingInterval") || 5000; try { await this.loadExtensions(); let db; if (options.vault.sync_url && options.vault.token) { //CLOUD SYNC DB LogBot.log(100, "User supplied cloud database credentials. Attempting to connect to cloud database."); if (!options.vault.sync_url) throw new Error("No databaseURL provided"); if (!options.vault.token) throw new Error("No token provided"); //init db interface this.db = DB({ url: options.vault.sync_url, token: options.vault.token, }); //rebuild local model await DB().rebuildLocalModel(); //connect to db websocket await this.db.connect(options.vault.token); if (options.vault.ai_url) { //init machine learning interface this.ML = MLInterface({ url: options.vault.ai_url, token: options.vault.token, }); } } else if (options.vault.token) { //LOCAL DB LogBot.log(100, "User supplied local database credentials. Attempting to connect to local database."); //init db interface for local use this.db = DB({ token: options.vault.token, }); //rebuild local model await DB().rebuildLocalModel(); } if (this.db) { this.db.on("transaction", (transaction) => { this.applyTransaction(transaction); }); this.db.on("notification", (notification) => { this.$emit("notification", notification); }); //start Account Manager await AccountManager.init(); //start Socket and REST API await this.startServer({ port: options.port, secret: options.secret || this.config.get("jwt-secret"), mailConfig: options.mail || this.config.get("mail"), }); await this.driveBot.updateDrives(); this.driveBot.watch(); //start drive watching const libraries = this.getAvailableLibraries().map((l) => l.name); let i = 1; let total = libraries.length; for (let libraryName of libraries) { try { const str = " (" + i + "/" + total + ") Attempting to load MetaLibrary " + libraryName; this.$emit("notification", { title: str, message: `Loading library ${libraryName}`, }); LogBot.log(100, str); const r = await this.loadOneLibrary(libraryName); if (r.status !== 200) { this.error(new Error("Could not load library: " + r.message)); this.$emit("notification", { title: "Library failed to load", message: "Library " + libraryName + " was not loaded.", }); } else { const str = " (" + i + "/" + total + ") Successfully loaded MetaLibrary " + libraryName; this.$emit("notification", { title: str, message: "Library " + libraryName + " loaded", }); LogBot.log(200, str); } } catch (e: any) { this.error(new Error("Could not load library: " + e.message)); } i++; } this.driveBot.on("removed", this.handleVolumeUnmount.bind(this)); this.driveBot.on("added", this.handleVolumeMount.bind(this)); this.status = WrangleBot.OPEN; LogBot.log(200, "WrangleBot instance opened successfully: http://localhost:" + options.port); this.$emit("notification", { title: "Howdy!", message: "WrangleBot is ready to wrangle", }); this.$emit("ready", this); return this; } else { this.status = WrangleBot.CLOSED; this.$emit("notification", { title: "Could not connect to database", message: "WrangleBot could not connect to the database.", }); this.$emit("error", new Error("Could not connect to database")); return null; } } catch (e: any) { LogBot.log(500, e.message); this.status = WrangleBot.CLOSED; this.$emit("error", e); this.$emit("notification", { title: "Could not connect to database", message: "WrangleBot could not connect to the database.", }); return null; } } async close() { this.status = WrangleBot.CLOSED; clearInterval(this.ping); this.driveBot.stopWatching(); this.servers.httpServer.close(); this.servers.socketServer.close(); return WrangleBot.CLOSED; } private async startServer(options: { port: number; mailConfig: Object; secret: string }) { this.servers = await api.init(this, options); } /** * UTILITY FUNCTIONS */ $emit(event: string, ...args: any[]): Promise<boolean> { return new Promise((resolve, reject) => { this.runCustomScript(event, ...args) .then(() => { super.emit(event, ...args); resolve(true); }) .catch((err) => { LogBot.log(500, err); resolve(false); }); }); } private async runCustomScript(event: string, ...args: any[]) { for (let extension of extensions) { if (extension.events.includes(event)) { await extension.handler(event, args, this); } } for (let extension of this.thirdPartyExtensions) { if (extension.events.includes(event)) { await extension.handler(event, args, this); } } } private async loadExtensions() { try { LogBot.log(100, "Loading extensions ... "); //scan the plugins folder in the wranglebot directory //and load the routes from the plugins const pathToPlugins = finder.getPathToUserData("custom/"); const thirdPartyPluginsRAW = finder.getContentOfFolder(pathToPlugins); LogBot.log(100, "Found " + thirdPartyPluginsRAW.length + " third party plugins."); if (thirdPartyPluginsRAW.length > 0) { for (let folderName of thirdPartyPluginsRAW) { LogBot.log(100, "Loading plugin " + folderName + " ... "); const pathToPlugin = finder.getPathToUserData("custom/" + folderName); const folderContents = finder.getContentOfFolder(pathToPlugin); for (let pluginFolder of folderContents) { if (pluginFolder === "hooks") { const pathToPluginHooks = finder.getPathToUserData("custom/" + folderName + "/" + pluginFolder); const hookFolderContent = finder.getContentOfFolder(pathToPluginHooks); for (let scriptFileName of hookFolderContent) { LogBot.log(100, "Loading hook " + scriptFileName + " ... "); const script = (await import(pathToPluginHooks + "/" + scriptFileName)).default; if (!script.name || script.name === "") { LogBot.log(404, "Plugin " + folderName + " does not have a valid name. Skipping ... "); continue; } if (!script.description || script.description === "") { LogBot.log(404, "Plugin " + folderName + " does not have a valid description. Skipping ... "); continue; } if (!script.version || script.version.match(/^[0-9]+\.[0-9]+\.[0-9]+$/) === null) { LogBot.log(404, "Plugin " + folderName + " does not have a valid version (/^[0-9]+\\.[0-9]+\\.[0-9]+$/). Skipping ... "); continue; } if (!script.handler || !(script.handler instanceof Function)) { LogBot.log(404, "Plugin " + folderName + " does not have a valid handler. Skipping ... "); continue; } if (!script.events || script.events.length === 0) { LogBot.log(404, "Plugin " + folderName + " does not have any events. Skipping ... "); continue; } this.thirdPartyExtensions.push(script); } } } } } } catch (e: any) { LogBot.log(500, e.message); } } getAvailableLibraries() { return DB().getMany("libraries", {}); } private async addOneLibrary(options: MetaLibraryOptions) { if (!options.name) throw new Error("No name provided"); if (options.pathToLibrary) { if (!finder.isReachable(options.pathToLibrary)) { throw new Error(options.pathToLibrary + " is not a valid path."); } } const allowedPaths = [ //macos "/volumes/", "/users/", //linux "/media/", "/home/", ]; const path = options.pathToLibrary.toLowerCase(); //check if the folder already exists if (!finder.existsSync(path)) { //if it does not create the base folder finder.mkdirSync(path, { recursive: true }); } const allowed = allowedPaths.some((p) => path.startsWith(p)); if (!allowed) throw new Error("Path is not allowed"); //check if lib exists in database if (this.index.libraries.find((l) => l.name.toLowerCase() === options.name.toLowerCase())) { throw new Error("Library with that name already exists"); } if (this.index.libraries.find((l) => path.startsWith(l.pathToLibrary.toLowerCase()))) { throw new Error("Library in path already exists"); } const metaLibrary = new MetaLibrary(this, options); //add library to runtime this.index.libraries.unshift(metaLibrary); //add metaLibrary in database await DB().updateOne("libraries", { name: metaLibrary.name }, metaLibrary.toJSON({ db: true })); metaLibrary.createFoldersOnDiskFromTemplate(); this.$emit("metalibrary-new", metaLibrary); return metaLibrary; } private removeOneLibrary(name, save = true) { if (!this.index.libraries.find((l) => l.name === name)) { return { status: 404, error: "database with that name does not exist or has not been loaded", }; } this.unloadOneLibrary(name); if (save) { return DB().removeOne("libraries", { name }); } this.$emit("metalibrary-remove", name); return true; } private getOneLibrary(name) { const lib = this.index.libraries.find((l) => l.name === name); if (lib) return lib; return DB().getOne("libraries", { name }); } private loadOneLibrary(name: string): Promise<ReturnObject> { return new Promise<ReturnObject>((resolve, reject) => { if (this.index.libraries.find((l) => l.name === name)) { resolve({ status: 500, message: "I can not load a library, that has been loaded already.", }); } else { const lib = this.getOneLibrary(name); if (lib) { const newMetaLibrary = new MetaLibrary(this, null); let readOnly = false; if (!finder.isReachable(lib.pathToLibrary)) { readOnly = true; } newMetaLibrary .rebuild(lib, readOnly) .then(() => { this.index.libraries.unshift(newMetaLibrary); resolve({ status: 200, result: newMetaLibrary, }); }) .catch((e) => { resolve({ status: 404, result: null, message: e.message, }); }); } else { const libraries = this.getAvailableLibraries(); reject(new Error("No library named '" + name + "' found. Available libraries: ['" + libraries.map((lib) => lib.name).join(", '") + "']")); } } }).catch((e) => { this.error(e.message); return { status: 404, message: e.message }; }); } private unloadOneLibrary(name) { const search = SearchLite.find(this.index.libraries, "name", name); if (search.wasSuccess()) { this.index.libraries.splice(search.count, 1); this.config.set( "libraries", this.index.libraries.map((lib) => lib.name) ); return { status: 200, message: "Library unloaded", }; } else { return { status: 404, message: "No library with that name found", }; } } handleVolumeMount(volume) { for (let lib of this.index.libraries) { if (lib.pathToLibrary.startsWith(volume.mountpoint)) { lib.readOnly = false; } } } handleVolumeUnmount(volume) { for (let lib of this.index.libraries) { if (lib.pathToLibrary.startsWith(volume.mountpoint)) { lib.readOnly = true; } } } /* THUMBNAILS */ /** * Generates Thumbnails from a list of MetaFiles * * @param library * @param {MetaFile[]} metaFiles * @param {Function|false} callback * @param finishCallback? * @returns {Promise<boolean>} resolve to false if there is no need to generate thumbnails or if there are no copies reachable */ async generateThumbnails(library, metaFiles, callback = (progress) => {}, finishCallback = (success) => {}) { const callbackWrapper = function (p) { callback({ ...p, metaFile: currentFile }); }; let currentFile = metaFiles[0]; if (metaFiles.length > 0) { for (let file of metaFiles) { currentFile = file; try { await this.generateThumbnail(library, file, null, callbackWrapper); finishCallback(file.id); } catch (e: any) { LogBot.log(500, "Error while generating thumbnail for file " + file.id + ": " + e.message); throw e; } } return true; } else { return false; } } /** * Generates a Thumbnail from a MetaFile if it is a video or photo * * @param {string} library - the library name * @param {MetaFile} metaFile - the metaFile to generate a thumbnail for * @param {MetaCopy} metaCopy - if not provided or unreachable, the first reachable copy will be used * @param {Function} callback - callback function to update the progress * @returns {Promise<boolean>} rejects if there is no way to generate thumbnails or if there are no copies reachable */ private async generateThumbnail(library, metaFile, metaCopy, callback: Function) { if (metaFile.fileType === "photo" || metaFile.fileType === "video") { //find the first copy that is has a reachable path let reachableMetaCopy; if (metaCopy && finder.existsSync(metaCopy.pathToBucket.file)) { reachableMetaCopy = metaCopy; } else { reachableMetaCopy = metaFile.copies.find((copy) => { return finder.existsSync(copy.pathToBucket.file); }); } if (reachableMetaCopy) { const thumbnails: any = await TranscodeBot.generateThumbnails(reachableMetaCopy.pathToBucket.file, { callback, metaFile, }); if (thumbnails) { LogBot.log(200, "Generated Thumbnails for <" + metaFile.name + ">"); if (metaFile.thumbnails.length > 0) { LogBot.log(200, "Deleting old Thumbnails <" + metaFile.thumbnails.length + "> for <" + metaFile.name + ">"); let thumbs: Thumbnail[] = Object.values(metaFile.thumbnails); for (let thumb of thumbs) { metaFile.removeOneThumbnail(thumb.id); } await DB().removeMany("thumbnails", { metafile: metaFile.id, library }); await utility.twiddleThumbs(5); //wait 5 seconds to make sure the timestamp is incremented LogBot.log(200, "Deleted old Thumbnails now <" + metaFile.thumbnails.length + "> for <" + metaFile.name + ">"); } LogBot.log(200, "Saving Thumbnails <" + thumbnails.length + "> for <" + metaFile.name + ">"); for (let thumbnail of thumbnails) { metaFile.addThumbnail(thumbnail); } const thumbData: any[] = []; for (let thumb of metaFile.getThumbnails()) { thumbData.push(thumb.toJSON()); } await DB().insertMany("thumbnails", { metaFile: metaFile.id, library }, thumbData); await utility.twiddleThumbs(5); //wait 5 seconds to make sure the timestamp is incremented await DB().updateOne( "metafiles", { id: metaFile.id, library }, { thumbnails: metaFile.getThumbnails().map((t) => t.id), } ); this.$emit("thumbnail-new", metaFile.getThumbnails()); this.$emit("metafile-edit", metaFile); LogBot.log(200, "Saved Thumbnails <" + thumbnails.length + "> for <" + metaFile.name + "> in DB"); return true; } } else { throw new Error("No reachable copy found. make sure a copy is reachable before generating thumbnails"); } } throw new Error("Can't generate thumbnails for this file type"); } private getManyTransactions(filter) { return DB().getTransactions(filter); } /** * Removes an Object from the runtime * if it already exists it will be overwritten * * @param {string} list i.e. copyTasks * @param {Object} item the object to remove * @return {0|1|-1} 0 if the item was not found, 1 if it was removed, -1 if the list does not exist */ removeFromRuntime(list, item) { try { if (this.index[list]) { const foundItem = this.index[list][item.id]; if (foundItem) { delete this.index[list][item.id]; return 1; } } return -1; } catch (e) { console.error(e); } } /** * Adds an Object to the runtime * if it already exists it will be overwritten * * @param {string} list i.e. copyTasks * @param {{id:string}} item the object to add * @return {0|1|-1} 0 if the item was overwritten, 1 if it was added, -1 if the list does not exist */ addToRuntime(list, item) { if (this.index[list]) { const alreadyExists = this.index[list][item.id]; if (!alreadyExists) { this.index[list][item.id] = item; return 0; } else { this.index[list][item.id] = item; return 1; } } return -1; } /* LOGGING & DEBUGGING */ error(message) { return LogBot.log(500, message, true); } notify(title, message) { this.$emit("notification", { title, message }); } //********************************** //* API v2 * //********************************** get query() { return { library: { many: (filters = {}) => { const libs = this.index.libraries.filter((lib) => { for (let key in filters) { if (lib[key] !== filters[key]) return false; } return true; }); return { fetch: async () => { return libs; }, }; }, one: (libraryId: string) => { const lib = this.index.libraries.find((l) => l.name === libraryId); if (!lib) throw new Error("Library is not loaded or does not exist."); return { fetch(): MetaLibrary { lib.query = this; return lib; }, put: (options: MetaLibraryUpdateOptions): Boolean => { return lib.update(options); }, delete: (): Boolean => { return this.removeOneLibrary(libraryId); }, scan: async (): Promise<Task | false> => { return await lib.createCopyTaskForNewFiles(); }, transactions: { one: (id: string) => { return { fetch: (): Transaction => { const t = this.getManyTransactions({ id: id, }); if (t.length > 0) { return t[0]; } throw new Error("Transaction not found."); }, }; }, many: (filter = {}) => { return { fetch: (): Transaction[] => { return this.getManyTransactions({ ...filter, library: lib.name, }); }, }; }, }, metafiles: { one: (metaFileId: string) => { const metafile = lib.getOneMetaFile(metaFileId); if (!metafile) throw new Error("Metafile not found."); return { fetch(): MetaFile { metafile.query = this; return metafile; }, delete: (): Boolean => { return lib.removeOneMetaFile(metafile); }, thumbnails: { one: (id: string) => { return { fetch: (): Thumbnail => { return metafile.getThumbnail(id); }, }; }, many: (filters) => { const thumbnails = metafile.getThumbnails(filters); return { fetch: (): Thumbnail[] => { return thumbnails; }, analyse: async (options) => { return await metafile.analyse({ ...options, frames: thumbnails.map((t) => t.id), }); }, }; }, first: { fetch: (): Thumbnail => { return metafile.getThumbnails()[0]; }, }, center: { fetch: (): Thumbnail => { const thumbs = metafile.getThumbnails(); return thumbs[Math.floor(thumbs.length / 2)]; }, }, last: { fetch: (): Thumbnail => { const thumbs = metafile.getThumbnails(); return thumbs[thumbs.length - 1]; }, }, generate: async (): Promise<Boolean> => { return await this.generateThumbnails(lib, metafile); }, }, metacopies: { one: (metaCopyId) => { const metacopy = lib.getOneMetaCopy(metaFileId, metaCopyId); if (!metacopy) throw new Error("Metacopy not found."); return { fetch(): MetaCopy { metacopy.query = this; return metacopy; }, delete: (options = { deleteFile: false }) => { return lib.removeOneMetaCopy(metacopy, options); }, }; }, many: (filters = {}) => { return { fetch: () => { return lib.getManyMetaCopies(metaFileId); }, }; }, post: async (options): Promise<MetaCopy> => { return await lib.addOneMetaCopy(options, metafile); }, }, metadata: { put: (options): Boolean => { return lib.updateMetaDataOfFile(metafile, options.key, options.value); }, }, analyse: async (options: analyseMetaFileOptions): Promise<{ response: Object }> => { return await metafile.analyse(options); }, }; }, many: (filters) => { const files = lib.getManyMetaFiles(filters); return { fetch: (): MetaFile[] => { return files; }, export: { report: async (options): Promise<Boolean> => { return await lib.generateOneReport(files, { pathToExport: options.pathToExport ? options.pathToExport : lib.pathToLibrary + "/_Reports", reportName: options.reportName || "Report", logoPath: options.logoPath, uniqueNames: options.uniqueNames, format: options.format, template: options.template, credits: options.credits, }); }, }, }; }, post: async (metafile: MetaFile | Object | string): Promise<MetaFile> => { return await lib.addOneMetaFile(metafile); }, }, tasks: { one: (id) => { let task = lib.getOneTask(id); return { fetch(): Task { task.query = this; return task; }, run: async (callback: Function, cancelToken: CancelToken): Promise<Task> => { return await lib.runOneTask(id, callback, cancelToken); }, put: async (options) => { return await lib.updateOneTask({ id, ...options }); }, delete: async () => { return lib.removeOneTask(id); }, }; }, many: (filters = {}) => { return { fetch() { return lib.getManyTasks(); }, delete: async () => { return await lib.removeManyTasks(filters); }, }; }, post: async (options: { label: string; jobs: { source: string; destinations?: string[] | null }[] }) => { return await lib.addOneTask(options); }, generate: async (options: createTaskOptions) => { return await lib.generateOneTask(options); }, }, transcodes: { one: (id) => { let transcode = lib.getOneTranscodeTask(id); return { fetch() { transcode.query = this; return transcode; }, run: async (callback: Function, cancelToken: CancelToken): Promise<void> => { await lib.runOneTranscodeTask(id, callback, cancelToken); }, delete: (): Boolean => { return lib.removeOneTranscodeTask(id); }, }; }, many: () => { return { fetch(): TranscodeTask[] { return lib.getManyTranscodeTasks(); }, // delete: async () => { // return lib.removeManyTranscodeTask({$ids : filters.$ids}); // }, }; }, post: async (files: MetaFile[], options): Promise<TranscodeTask> => { return await lib.addOneTranscodeTask(files, options); }, }, folders: { put: async (options: FolderOptions): Promise<Boolean> => { return await lib.updateFolder(options.path, options.options); }, }, }; }, post: async (options: MetaLibraryOptions): Promise<MetaLibrary> => { return await this.addOneLibrary(options); }, load: async (name: string) => { return await this.loadOneLibrary(name); }, unload: (name: string) => { return this.unloadOneLibrary(name); }, }, users: { one: (options: { id: string }) => { if (!options.id) throw new Error("No id provided"); const user = AccountManager.getOneUser(options.id); if (!user) throw new Error("No user found with that " + options.id); return { fetch(): User { user.query = this; return user; }, put: (options) => { return AccountManager.updateUser(user, options); }, allow: (libraryName: string) => { return AccountManager.allowAccess(user, libraryName); }, revoke: (libraryName: string) => { return AccountManager.revokeAccess(user, libraryName); }, reset: () => { return AccountManager.resetPassword(user); }, }; }, many: (filters = {}): { fetch: Function } => { return { fetch(): User[] { return AccountManager.getAllUsers(filters); }, }; }, post: async (options) => { return AccountManager.addOneUser({ ...options, create: true, }); }, }, volumes: { one: (id) => { const vol = this.driveBot.drives.find((d) => d.volumeId === id); if (!vol) throw new Error("Volume not found."); return { fetch(): Volume { vol.query = this; return vol; }, eject: async () => { return await this.driveBot.eject(id); }, }; }, many: (): { fetch(): Promise<Volume[]> } => { let driveWatcher = this.driveBot; return { async fetch(): Promise<Volume[]> { return await driveWatcher.getDrives(); }, }; }, }, transactions: { one: (id) => {}, many: (filter) => { return { fetch: async () => { return this.getManyTransactions(filter); }, }; }, }, }; } get utility() { return { index: async (pathToFolder, types) => { return await indexer.index(pathToFolder, types); }, list: (pathToFolder, options: { showHidden: boolean; filters: "both" | "files" | "folders"; recursive: boolean; depth: Number }) => { if (!pathToFolder) throw new Error("No path provided."); if (pathToFolder === "/") throw new Error("Cannot list root directory."); if (!options) { options = { showHidden: false, filters: "folders", recursive: false, depth: 0, }; } return finder.getContentOfFolder(pathToFolder, options); }, uuid() { return uuidv4(); }, luts() { const pathToLuts = config.getPathToUserData() + "/LUTs"; if (finder.existsSync(pathToLuts)) { const files = finder.getContentOfFolder(pathToLuts).filter((f) => !f.startsWith(".")); return files; } else { return []; } }, }; } private async applyTransaction(transaction) { try { if (transaction.$method === "updateOne") await this.applyTransactionUpdateOne(transaction); if (transaction.$method === "insertMany") await this.applyTransactionInsertMany(transaction); if (transaction.$method === "removeOne") await this.applyTransactionRemoveOne(transaction); } catch (e) { console.error(e); LogBot.log(500, "Error applying transaction", e); } } private async applyTransactionUpdateOne(transaction) { LogBot.log(100, "applying transaction: " + transaction.id); //LIBRARY ADDED/UPDATED if (transaction.$collection === "libraries") { let lib = this.index.libraries.find((l) => l.name === transaction.$query.name); if (lib) { await lib.update(transaction.$set, false); LogBot.log(200, `Library ${lib.name} updated.`); } else { const newMetaLibrary = new MetaLibrary(this, { ...transaction.$set, ...transaction.$query }); this.addToRuntime("libraries", newMetaLibrary); LogBot.log(200, `Library ${newMetaLibrary.name} added.`); } this.servers.socketServer.inform("database", "libraries", "change"); } //METAFILE ADDED/UPDATED if (transaction.$collection === "metafiles") { if (!transaction.$query.library) throw new Error("No library provided to update metaFile."); let lib = this.index.libraries.find((l) => l.name === transaction.$query.library); if (lib) { let file = lib.metaFiles.find((f) => f.id === transaction.$query.id); if (file) { file .update(transaction.$set, false) .then(() => { LogBot.log(200, "MetaFile updated"); this.servers.socketServer.inform("database", "metafiles", "change"); }) .catch((e) => { console.error(e); LogBot.log(500, "Error updating metaFile", e); }); } else { const newMetaFile = new MetaFile({ ...transaction.$set, ...transaction.$query }); lib.metaFiles.push(newMetaFile); this.addToRuntime("metaFiles", newMetaFile); this.servers.socketServer.inform("database", "metafiles", "change"); LogBot.log(200, "MetaFile created"); } } else { throw new Error("Library not found."); } } //METACOPY ADDED/UPDATED if (transaction.$collection === "metacopies") { if (!transaction.$query.library) throw new Error("No library provided to update MetaCopy."); let lib = this.index.libraries.find((l) => l.name === transaction.$query.library); if (lib) { let copy = this.index.metaCopies[transaction.$query.id]; if (copy) { copy.update(transaction.$set, false); LogBot.log(200, "MetaCopy updated", copy); this.servers.socketServer.inform("database", "metafiles", "change"); } else { //find the file that this copy belongs to let file = lib.metaFiles.find((f) => f.id === transaction.$query.metaFile); if (!file) { LogBot.log(404, "MetaFile for MetaCopy not found"); return; } const newMetaCopy = new MetaCopy({ ...transaction.$set, ...transaction.$query }); file.addCopy(newMetaCopy); this.addToRuntime("metaCopies", newMetaCopy); this.servers.socketServer.inform("database", "metafiles", "change"); LogBot.log(200, "MetaCopy created", newMetaCopy); } } else { throw new Error("Library not found."); } } //TASKS ADDED/UPDATED if (transaction.$collection === "tasks") { if (!transaction.$query.library) throw new Error("No library provided to update Task."); let lib = this.index.libraries.find((l) => l.name === transaction.$query.library); if (lib) { let task = this.index.copyTasks[transaction.$query.id]; if (task) { task.update(transaction.$set, false); this.servers.socketServer.inform("database", "tasks", "change"); LogBot.log(200, "Task updated", task); } else { const newTask = new Task({ ...transaction.$set, ...transaction.$query }); lib.tasks.push(newTask); this.addToRuntime("copyTasks", newTask); this.servers.socketServer.inform("database", "tasks", "change"); LogBot.log(200, "Task created", newTask); } } else { throw new Error("Library not found."); } } //TRANSCODES ADDED/UPDATED if (transaction.$collection === "transcodes") { if (!transaction.$query.library) throw new Error("No library provided to update Transcodes."); let lib = this.index.libraries.find((l) => l.name === transaction.$query.library); if (lib) { let transcode = this.index.transcodes[transaction.$query.id]; if (transcode) { transcode.update(transaction.$set); this.servers.socketServer.inform("database", "transcodes", "change"); LogBot.log(200, "Transcode updated", transcode); } else { const newTranscode = new TranscodeTask(null, { ...transaction.$set, ...transaction.$query }); lib.transcodes.push(newTranscode); this.addToRuntime("transcodes", newTranscode); this.servers.socketServer.inform("database", "transcodes", "change"); LogBot.log(200, "Transcode created", newTranscode); } } else { throw new Error("Library not found."); } } } private async applyTransactionInsertMany(transaction) { if (transaction.$collection === "thumbnails") { const metaFile = this.index.metaFiles[transaction.$query.metaFile]; if (!metaFile) throw new Error("MetaFile not found."); for (let thumb of transaction.$set) { metaFile.addThumbnail(thumb); } } } private async applyTransactionRemoveOne(transaction) { if (transaction.$collection === "libraries") { let lib = this.index.libraries.find((l) => l.name === transaction.$query.name); if (lib) { this.removeOneLibrary(lib, false); this.servers.socketServer.inform("database", "libraries", "change"); LogBot.log(200, `Library ${lib.name} removed.`); } } else if (transaction.$collection === "metafiles") { if (!transaction.$query.library) throw new Error("No library provided to remove metaFile."); let lib = this.index.libraries.find((l) => l.name === transaction.$query.library); if (lib) { let file = lib.metaFiles.find((f) => f.id === transaction.$query.id); if (file) { lib.removeOneMetaFile(file, false); this.servers.socketServer.inform("database", "metafiles", "change"); LogBot.log(200, "MetaFile removed", file); } } else { throw new Error("Library not found."); } } else if (transaction.$collection === "metacopies") { if (!transaction.$query.library) throw new Error("No library provided to remove MetaCopy."); let lib = this.index.libraries.find((l) => l.name === transaction.$query.library); if (lib) { let copy = this.index.metaCopies[transaction.$query.id]; if (copy) { lib.removeOneMetaCopy(copy, { deleteFile: false }, false); this.servers.socketServer.inform("database", "metafiles", "change"); LogBot.log(200, "MetaCopy removed", copy); } } else { throw new Error("Library not found."); } } else if (transaction.$collection === "tasks") { if (!transaction.$query.library) throw new Error("No library provided to remove Task."); let lib = this.index.libraries.find((l) => l.name === transaction.$query.library); if (lib) { let task = this.index.copyTasks[transaction.$query.id]; if (task) { lib.removeOneTask(task.id, "id", false); this.servers.socketServer.inform("database", "tasks", "change"); LogBot.log(200, "Task removed", task); } } else { throw new Error("Library not found."); } } else if (transaction.$collection === "transcodes") { if (!transaction.$query.library) throw new Error("No library provided to remove Transcode."); let lib = this.index.libraries.find((l) => l.name === transaction.$query.library); if (lib) { let transcode = this.index.transcodes[transaction.$query.id]; if (transcode) { lib.removeOneTranscodeTask(transcode.id, false); this.servers.socketServer.inform("database", "transcodes", "change"); LogBot.log(200, "Transcode removed", transcode); } } else { throw new Error("Library not found."); } } } } const wb = new WrangleBot(); export default wb; export { WrangleBot, config };