UNPKG

wranglebot

Version:

open source media asset management

1,156 lines (969 loc) 35.4 kB
import { WrangleBot } from "../WrangleBot.js"; import MetaLibraryData from "./MetaLibraryData.js"; import { SearchLite } from "searchlite"; import { MetaFile } from "./MetaFile.js"; import Task from "../media/Task.js"; import ExportBot from "../export/index.js"; import DB from "../database/DB.js"; import LogBot from "logbotjs"; import { MetaCopy } from "./MetaCopy.js"; import { finder } from "../system/index.js"; import utility from "../system/utility.js"; import config from "../system/Config.js"; import { TranscodeTask } from "../transcode/TranscodeTask.js"; import { indexer } from "../media/Indexer.js"; import Job from "../media/Job.js"; import Status from "../media/Status.js"; import createTaskOptions from "./createTaskOptions.js"; import Folders from "./Folders.js"; import MetaLibraryOptions from "./MetaLibraryOptions.js"; import MetaLibraryUpdateOptions from "./MetaLibraryUpdateOptions.js"; import CancelToken from "./CancelToken.js"; interface ReportOptions { format: "html" | "json" | "text" | "pdf" | "csv"; template: object; pathToExport?: string; reportName?: string; logoPath?: string; uniqueNames: boolean; credits?: object; } export default class MetaLibrary { wb: WrangleBot; name: string = "UNNAMED"; folders: Folders[] = []; /** * The metadata of the library, this is info that can be saved and used in the handlebars * @type {MetaLibraryData} */ drops = new MetaLibraryData(); pathToLibrary; metaFiles: MetaFile[] = []; tasks: Task[] = []; transcodes: TranscodeTask[] = []; readOnly = false; /** * The creation date of the library * @type {Date} */ creationDate = new Date(); query; constructor(wb, options: MetaLibraryOptions | null) { if (!wb) throw new Error("Failed to create library! Reason: Missing WrangleBot Instance"); this.wb = wb; if (options) { if (!options.name) throw new Error("new MetaLibrary must have a option .name"); if (!options.pathToLibrary) throw new Error("new MetaLibrary must have a option .pathToLibrary"); this.name = options.name; this.pathToLibrary = options.pathToLibrary; this.drops = new MetaLibraryData(options.drops || null); this.folders = options.folders || []; } return this; } /** * Updates and saves the library * @param options {{pathToLibrary?:string, drops?:Map<name,value>, folders?:Folders}} * @param save * @returns {boolean} */ update(options: MetaLibraryUpdateOptions, save = true) { if (options.pathToLibrary) { if (!finder.isReachable(options.pathToLibrary) && save && !this.readOnly) { throw new Error(options.pathToLibrary + " is not reachable and can not be updated."); } this.pathToLibrary = options.pathToLibrary; //check if the folder already exists if (!finder.existsSync(this.pathToLibrary)) { //if it does not create the base folder finder.mkdirSync(this.pathToLibrary, { recursive: true }); } } if (options.folders) this.folders = options.folders; if (options.drops) this.drops = new MetaLibraryData(options.drops); if (finder.existsSync(this.pathToLibrary)) { this.readOnly = false; this.createFoldersOnDiskFromTemplate(); } if (save) return this.save(options); this.wb.emit("metalibrary-edit", this); return true; } async updateFolder(folderPath, overwriteOptions) { let folder = this.getFolderByPath(folderPath); try { if (!folder) { throw new Error(`Folder ${folderPath} not found`); } if (Object.keys(overwriteOptions).length === 0) { throw new Error(`No options to update folder ${folderPath}`); } if (overwriteOptions.name && overwriteOptions.name !== folder.name) { //check if folder is empty if (finder.readdirSync(finder.join(this.pathToLibrary, folderPath)).length > 0) { throw new Error(`Folder ${folderPath} is not empty, can not rename`); } finder.rename(finder.join(this.pathToLibrary, folderPath), overwriteOptions.name); folder.name = overwriteOptions.name; } if (overwriteOptions.watch !== undefined) { folder.watch = overwriteOptions.watch; } if (overwriteOptions.folders) { folder.folders = overwriteOptions.folders; this.createFoldersOnDiskFromTemplate(); } this.save({ folders: this.folders, }); this.wb.emit("metalibrary-edit", this); return true; } catch (e) { throw e; } } getFolderByPath(folderPath): Folders | null { if (!this.folders) return null; //remove leading slash and trailing slash folderPath = folderPath.replace(/^\/|\/$/g, ""); let folderPathArray = folderPath.split("/"); let folder: Folders[] = this.folders; for (let i = 0; i < folderPathArray.length; i++) { let folderName = folderPathArray[i]; let found = false; for (let j = 0; j < folder.length; j++) { if (folder[j].name === folderName) { found = true; if (i === folderPathArray.length - 1) { return folder[j]; } folder = folder[j].folders; break; } } if (!found) return null; } return null; } save(options = {}) { return DB().updateOne("libraries", { name: this.name }, options); } /** * REBUILD * Takes a library database Structure and assembles all attached elements * * @param {Object} metaLibraryProto * @param readOnly * @return {Promise<boolean>} */ async rebuild(metaLibraryProto, readOnly = false) { if (!metaLibraryProto) throw new Error("Failed to Rebuild library! Reason: Missing Proto"); if (!metaLibraryProto.name) throw new Error("Failed to Rebuild library! Reason: Missing Name"); if (!metaLibraryProto.creationDate) throw new Error("Failed to Rebuild library! Reason: Missing Creation Date"); if (!metaLibraryProto.folders) throw new Error("Failed to Rebuild library! Reason: Missing Folders"); if (!metaLibraryProto.pathToLibrary) throw new Error("Failed to Rebuild library! Reason: Missing Path To Library"); this.readOnly = readOnly; try { /* GENERAL */ this.name = metaLibraryProto.name; this.folders = metaLibraryProto.folders; this.pathToLibrary = metaLibraryProto.pathToLibrary; this.drops = new MetaLibraryData(metaLibraryProto.drops); this.creationDate = new Date(metaLibraryProto.creationDate); /* METAFILES */ const metaFilesRaw = DB().getMany("metafiles", { library: this.name }); const allMetaCopiesRaw = DB().getMany("metacopies", { library: this.name }); for (let metaFileRaw of metaFilesRaw) { const thumbnailsRaw = DB().getMany("thumbnails", { metaFile: metaFileRaw.id }); const newMetaFile = new MetaFile({ ...metaFileRaw, thumbnails: thumbnailsRaw }); if (metaFileRaw.copies) { const metaCopiesRaw = metaFileRaw.copies.map((copy) => { const copyRaw = allMetaCopiesRaw.find((c) => c.id === copy); if (!copyRaw) throw new Error(`Failed to find copy ${copy.id} for ${newMetaFile.name}`); return copyRaw; }); if (metaCopiesRaw.length > 0) { for (let metaCopyRaw of metaCopiesRaw) { metaCopyRaw.metaFile = newMetaFile; const metaCopy = new MetaCopy(metaCopyRaw); this.wb.addToRuntime("metaCopies", metaCopy); newMetaFile.addCopy(metaCopy); } } } this.metaFiles.push(newMetaFile); this.wb.addToRuntime("metaFiles", newMetaFile); } /* REBUILD TASKS */ const tasks = DB().getMany("tasks", { library: this.name }); for (let task of tasks) { for (let job of task.jobs) { if (this.wb.index.metaCopies[job.metaCopy]) { job.metaCopy = this.wb.index.metaCopies[job.metaCopy]; } else { task.status = -1; } } const newTask = new Task(task); this.tasks.push(newTask); this.wb.addToRuntime("copyTasks", newTask); } /* REBUILD TRANSCODES */ const transcodes = DB().getMany("transcodes", { library: this.name }); for (let transcode of transcodes) { try { for (let job of transcode.jobs) { if (job.metaFile) { job.metaFile = this.wb.index.metaFiles[job.metaFile]; if (job.metaCopy) { job.metaCopy = this.wb.index.metaCopies[job.metaCopy]; } } else { transcode.status = -1; } } const t = new TranscodeTask(null, transcode); this.transcodes.push(t); this.#addToRuntime("transcodes", t); } catch (e: any) { LogBot.log(500, "Failed to rebuild transcode. Reason: " + e.message); } } if (!this.readOnly) { this.createFoldersOnDiskFromTemplate(this.folders); } return true; } catch (e) { console.error(e); return false; } } /** * Iterates over the given folders and creates them on the disk relative to pathToLibrary */ createFoldersOnDiskFromTemplate(folders: Folders[] = this.folders, basePath: string = this.pathToLibrary, jobs: any[] = []): void { if (!finder.existsSync(this.pathToLibrary)) { finder.mkdirSync(this.pathToLibrary, { recursive: true }); } for (let folder of folders) { let folderPath = finder.join(basePath, folder.name); if (folder.folders) { this.createFoldersOnDiskFromTemplate(folder.folders, folderPath, jobs); } if (!finder.existsSync(folderPath)) { finder.mkdirSync(folderPath, { recursive: true }); } } } async createCopyTaskForNewFiles() { const jobs = await this.scanLibraryForNewFiles(); if (jobs.length > 0) { LogBot.log(100, `Found ${jobs.length} new files to add to the library`); const r = await this.addOneTask({ label: "Delta Detection " + new Date().toLocaleString(), jobs, }); if (r) { return r; } } return false; } async scanLibraryForNewFiles(folders: Folders[] = this.folders, basePath: string = this.pathToLibrary, jobs: any[] = []): Promise<Array<Job>> { for (let folder of folders.filter((f) => f.watch)) { let folderPath = finder.join(basePath, folder.name); const r = await indexer.index(folderPath); for (let indexItem of r.items) { let metaCopy = this.getMetaCopyByPath(indexItem.pathToFile); let f = jobs.find((j: Job) => j.source === indexItem.pathToFile); let t = Object.values(this.wb.index.copyTasks).find((t) => t.jobs.find((j) => { if (j.destinations === null) return false; const inDestinations = j.destinations.find((d) => d === indexItem.pathToFile); return j.source === indexItem.pathToFile || inDestinations; }) ); if (!metaCopy && !f && !t) { jobs.push({ source: indexItem.pathToFile, }); } } if (folder.folders && folder.folders.length > 0) { await this.scanLibraryForNewFiles(folder.folders, folderPath, jobs); } } return jobs; } getMetaCopyByPath(path) { for (let file of this.metaFiles) { for (let copy of file.copies) { if (copy.pathToBucket.file.toLowerCase() === path.toLowerCase() || copy.pathToSource.toLowerCase() === path.toLowerCase()) { return copy; } } } return false; } log(message, type) { LogBot.log(`${this.name}:${type}`, message); } /** * * @param {string} list * @param {string} value * @param {"_id"|"id"|"label"|string} property * @return {MetaFile|MetaCopy} */ get(list, value = "", property = "id") { if (this[list]) { return SearchLite.find(this[list], property, value).result; } else { return undefined; } } /** * 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.wb.index[list]) { const foundItem = this.wb.index[list][item.id]; if (foundItem) { delete this.wb.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.wb.index[list]) { const alreadyExists = this.wb.index[list][item.id]; if (!alreadyExists) { this.wb.index[list][item.id] = item; return 0; } else { this.wb.index[list][item.id] = item; return 1; } } return -1; } /* META DATA (LIBRARY) */ async updateMetaData(col, value) { if (this.drops.updateCol(col, value)) { const result = await DB().updateOne( "libraries", { name: this.name }, { drops: this.drops, } ); return true; } return new Error("Could not update metaData for <" + col + "> to <" + value + "> in library <" + this.name + ">"); } async removeMetaData(col) { if (this.drops.removeCol(col)) { const result = await DB().updateOne( "libraries", { name: this.name }, { drops: this.drops, } ); return true; } return Error("Could not remove key: <" + col + "> from metaData of library <" + this.name + ">"); } /* METAFILES */ /** * Adds a MetaFile to the database(), as well as the runtime * * @param metaFile {MetaFile | Object | string} the MetaFile to add, it can be a MetaFile, a JSON Object or a string to a file * @return {Promise<void>} */ async addOneMetaFile(metaFile): Promise<MetaFile> { try { if (this.readOnly) throw new Error("Library is read only"); //the metaFile is a path to a file if (typeof metaFile === "string") { metaFile = await MetaFile.fromFile(metaFile); //check if the file is already in the library if (this.findMetaFileByHash(metaFile.hash)) throw new Error("File already exists in library"); } //the metaFile is not a MetaFile instance and is used to initialize one if (!(metaFile instanceof MetaFile)) { metaFile = new MetaFile(metaFile); } //add copies for (let metacopy of metaFile.copies) { await DB().updateOne("metacopies", { id: metacopy.id, library: this.name, metaFile: metaFile.id }, metacopy.toJSON({ db: true })); this.wb.addToRuntime("metaCopies", metacopy); } //the metaFile is now def a MetaFile instance await DB().updateOne("metafiles", { id: metaFile.id, library: this.name }, metaFile.toJSON({ db: true })); this.metaFiles.push(metaFile); //add to local array this.wb.addToRuntime("metaFiles", metaFile); //add global index store this.wb.emit("metafile-new", metaFile); //emit event return metaFile; } catch (e: any) { throw new Error("Failed to add MetaFile to " + this.name + ": " + e.message); } } /** * Retrieves a MetaFile from its library by hash, can lead to collisions * * @param hash * @return {MetaFile} */ findMetaFileByHash(hash) { const search = SearchLite.find(this.metaFiles, "_hash", hash); if (search.wasSuccess()) { return search.result; } return null; } /** * Retrieves a MetaFile from its library from its id * * @param {string} metaFileId * @return {MetaFile} */ getOneMetaFile(metaFileId) { return this.metaFiles.find((e) => e.id === metaFileId); } getManyMetaFiles(filters: { $ids?: string[] } = {}): MetaFile[] { const files = this.metaFiles; if (filters.$ids) { const filteredFiles: MetaFile[] = []; filters.$ids.forEach((id) => { const f = files.find((e) => e.id === id); if (f) filteredFiles.push(f); }); return filteredFiles; } if (Object.entries(filters).length > 0) { return this.metaFiles.filter((mf) => { for (let key in filters) { if (mf[key] === filters[key]) return true; } }); } else { return this.metaFiles; } } removeOneMetaFile(metaFile, save = true) { if (this.readOnly && save) throw new Error("Library is read only"); return this.removeManyMetaFiles([metaFile], save); } removeManyMetaFiles(metaFiles, save = true) { if (this.readOnly && save) throw new Error("Library is read only"); let listOfIdsToRemove: string[] = []; try { for (let file of metaFiles) { for (let copy of file.copies) { this.wb.removeFromRuntime("metaCopies", copy); } for (let thumbnail of file.thumbnails) { finder.rmSync(finder.join(config.getPathToUserData(), "thumbnails", thumbnail.id + ".jpg")); } listOfIdsToRemove.push(file.id); this.wb.removeFromRuntime("metaFiles", file); } //remove the files from the library for (let f of this.metaFiles) { if (listOfIdsToRemove.includes(f.id)) { if (save) { DB().removeOne("metafiles", { id: f.id, library: this.name }); DB().removeMany("metacopies", { metafile: f.id, library: this.name }); DB().removeMany("thumbnails", { metafile: f.id, library: this.name }); } this.wb.emit("metafile-removed", f.id); this.metaFiles.splice(this.metaFiles.indexOf(f), 1); } } return true; } catch (e) { console.error(e); return false; } } /* METACOPIES */ async addOneMetaCopy(metaCopy: MetaCopy | Object, metaFile): Promise<any> { try { if (this.readOnly) throw new Error("Library is read only"); if (!(metaCopy instanceof MetaCopy)) { metaCopy = new MetaCopy(metaCopy); } metaFile.addCopy(metaCopy); this.wb.addToRuntime("metaCopies", metaCopy); DB().updateOne( "metafiles", { id: metaFile.id, library: this.name }, { copies: metaFile.copies.map((c) => c.id), } ); await utility.twiddleThumbs(5); //wait 5 seconds to make sure the timestamp is incremented DB().updateOne( "metacopies", { id: (<MetaCopy>metaCopy).id, library: this.name, metaFile: metaFile.id }, (<MetaCopy>metaCopy).toJSON({ db: true }) ); this.wb.emit("metacopy-new", metaCopy); return metaCopy; } catch (e: any) { throw new Error("Failed to add MetaCopy to " + this.name + ": " + e.message); } } getOneMetaCopy(metaFileId, metaCopyId) { const metaFile = this.getOneMetaFile(metaFileId); if (metaFile) { return metaFile.getMetaCopy(metaCopyId); } return null; } getManyMetaCopies(metaFileID) { const metaFile = this.getOneMetaFile(metaFileID); if (metaFile) { return metaFile.copies; } return []; } removeOneMetaCopy(metaCopy, options = { deleteFile: false }, save = true) { if (this.readOnly && save) throw new Error("Library is read only"); if (save) { const result = DB().removeOne("metacopies", { id: metaCopy.id, library: this.name }); } if (options.deleteFile) { try { finder.rmSync(metaCopy.pathToBucket.file); } catch (e) { LogBot.log(404, "Could not delete file <" + metaCopy.pathToBucket.file + ">"); } } metaCopy.metaFile.dropCopy(metaCopy); if (save) { const updateResult = DB().updateOne( "metafiles", { id: metaCopy.metaFile.id, library: this.name }, { copies: metaCopy.metaFile.copies.map((c) => c.id), } ); this.wb.emit("metacopy-remove", metaCopy.id); } this.#removeFromRunTime("metaCopies", metaCopy); return true; } updateMetaDataOfFile(metafile, key, value) { if (metafile) { metafile.metaData.updateEntry(key, value); const set = { metaData: {} }; set.metaData[key] = value; DB().updateOne("metafiles", { id: metafile.id, library: this.name }, set); this.wb.emit("metafile-metadata-edit", { id: metafile.id, key: key, value: value, }); return true; } throw new Error("File not found"); } /* THUMBNAILS */ async downloadOneThumbnail(thumb) { //if the thumbnail doesn't exist, try to get it from the database and save it to the thumbnail folder const thumbnailInDB = await DB().getOne("thumbnails", { id: thumb.id }); if (thumbnailInDB) { finder.mkdirSync(finder.join(config.getPathToUserData(), "thumbnails")); const newPath = finder.join(config.getPathToUserData(), "thumbnails", thumb.id + ".jpg"); let buff = Buffer.from(thumbnailInDB.data, "base64"); finder.writeFileSync(newPath, buff); } } /* TASKS */ async generateOneTask(options: createTaskOptions) { if (this.readOnly) throw new Error("Library is read only"); if (!options.settings) throw new Error("No options provided"); //remove trailing slashes from source and destinations options.source = options.source.replace(/\/+$/, ""); options.destinations = options.destinations.map((d) => d.replace(/\/+$/, "")); const index = await indexer.index(options.source, options.types, options.matchExpression ? new RegExp(options.matchExpression) : null); const jobs: { source: string; destinations: string[] | null }[] = []; if (index) { for (let item of index.items) { let destinations: string[] = []; //if this index items path is a duplicate of metacopy, skip it if the user doesn't want duplicates if (options.settings.ignoreDuplicates) { let metacopy = this.getMetaCopyByPath(item.pathToFile); if (metacopy) { continue; } } if (options.destinations.length > 0) { for (let folder of options.destinations) { if (options.settings) { if (!options.settings.preserveFolderStructure && index.duplicates) { throw new Error("Must preserve folder structure when there are duplicates"); } if (options.settings.preserveFolderStructure) { let prefixToStrip = options.source; let prefix = item.pathToFile.replace(prefixToStrip, ""); destinations.push(folder + (options.settings.createSubFolder ? "/" + options.label : "") + prefix); } else { destinations.push(folder + (options.settings.createSubFolder ? "/" + options.label : "") + "/" + item.basename); } } else { destinations.push(folder + "/" + item.basename); } } } jobs.push({ source: item.pathToFile, destinations: destinations.length > 0 ? destinations : null, }); } if (jobs.length > 0) { const task = await this.addOneTask({ label: options.label, jobs: jobs, }); this.wb.emit("copytask-new", task); return task; } else { throw new Error("No files that matched the criteria were found"); } } throw new Error("Indexing failed"); } /** * Creates CopyTask and adds it to the library * * @param {{label: string; jobs: {source: string; destinations?: string[]}[]}} options * @return {Promise<Task>} */ async addOneTask(options) { if (this.readOnly) throw new Error("Library is read only"); if (!options.label) { throw new Error("No label provided"); } if (!options.jobs) { throw new Error("No jobs provided"); } //check if task with label already exists const search = SearchLite.find(Object.values(this.tasks), "label", options.label); if (search.wasFailure()) { for (let job of options.jobs) { if (!job.source) { throw new Error("No source provided"); } if (job.destinations !== null && job.destinations instanceof Array && job.destinations.length === 0) { throw new Error("No destinations provided. Arrays must not be empty, use null instead."); } if (!finder.existsSync(job.source)) { throw new Error("Source file does not exist: " + job.source); } let stats = finder.statSync(job.source); if (!stats.isFile()) { throw new Error("Source is not a file: " + job.source); } job.stats = { size: stats.size, }; } const task = new Task(options); this.tasks.push(task); await DB().updateOne("tasks", { id: task.id, library: this.name }, task.toJSON({ db: true })); this.wb.addToRuntime("copyTasks", task); this.wb.emit("copytask-new", task); return task; } else { throw new Error("Task with this label already exists"); } } /** * * @param {string} id * @return {Task} */ getOneTask(id) { const search = this.tasks.find((task) => task.id === id); if (search) return search; throw Error("No task found with that key"); } /** * Runs all jobs of a task and syncs metafiles and copies as needed * * @param {string} id the id of the task * @param {Function} cb the callback to get progress and speed * @param {{cancel:boolean}} cancelToken cancel the operation */ async runOneTask(id, cb, cancelToken: CancelToken) { if (this.readOnly) throw new Error("Library is read only"); const task = this.getOneTask(id); if (!task) { throw new Error("Task not found"); } const addMetaCopy = async (executedJob, task, metaFile) => { if (executedJob.destinations === null) { //add metacopy that is in place const newMetaCopy = new MetaCopy({ hash: executedJob.result.hash, pathToSource: executedJob.source, pathToBucket: executedJob.source, label: task.label, metaFile: metaFile, }); //push changes to the database await this.addOneMetaCopy(newMetaCopy, metaFile); await utility.twiddleThumbs(5); //wait 5 milliseconds to make sure the timestamp is incremented return; } for (let destination of executedJob.destinations) { //add metacopy to the metafile const newMetaCopy = new MetaCopy({ hash: executedJob.result.hash, pathToSource: executedJob.source, pathToBucket: destination, label: task.label, metaFile: metaFile, }); //push changes to the database await this.addOneMetaCopy(newMetaCopy, metaFile); await utility.twiddleThumbs(5); //wait 5 milliseconds to make sure the timestamp is incremented } }; try { //iterate over all jobs for (let job of task.jobs) { if (job.status === Status.DONE) { continue; } let executedJob; if (cancelToken.cancel) break; //listen on cancel and exit task before the job is executed executedJob = await task.runOneJob(job, cb, cancelToken); //run the job if (cancelToken.cancel) break; //listen on cancel and skip after the job is executed //search for hash in library const foundMetaFile = this.findMetaFileByHash(executedJob.result.hash); if (foundMetaFile) { //if found, add copy to metafile await addMetaCopy(executedJob, task, foundMetaFile); } else { //create new metafile const basename = finder.basename(executedJob.source).toString(); const newMetaFile = new MetaFile({ hash: executedJob.result.hash, metaData: executedJob.result.metaData, basename: basename, name: basename.substring(0, basename.lastIndexOf(".")), size: executedJob.result.size, fileType: finder.getFileType(basename), extension: finder.extname(basename), }); await this.addOneMetaFile(newMetaFile); await utility.twiddleThumbs(5); //wait a few ms to make sure the timestamp is different await addMetaCopy(executedJob, task, newMetaFile); } } DB().updateOne("tasks", { id: task.id, library: this.name }, task.toJSON({ db: true })); this.wb.emit("copytask-edit", task); return task; } catch (e) { DB().updateOne("tasks", { id: task.id, library: this.name }, task.toJSON({ db: true })); LogBot.log(500, "Task failed or cancelled"); this.wb.emit("copytask-edit", task); throw e; } } /** * Returns all tasks of the library * @returns {Task[]} */ getManyTasks() { return this.tasks; } /** * Updates or Upserts a Task * * @param options {{label:string, jobs: {source:string, destination?:string}}} options * @returns {Promise<Error|boolean>} */ async updateOneTask(options: Task) { if (this.readOnly) throw new Error("Library is read only"); const copyTask = this.getOneTask(options.id); if (copyTask) { copyTask.label = options.label; const result = await DB().updateOne("tasks", { id: copyTask.id, library: this.name }, copyTask.toJSON({ db: true })); this.wb.emit("copytask-edit", copyTask); return true; } return new Error("No task found with that id."); } /** * Remove a Task from the library, it will attempt t remove it from the database() first. If it succeeds it will splice it from the runtime array * * @param {string} key * @param {'id'|'_id'|'label'} by * @param save * @return {Promise<{deletedCount:number}>} */ removeOneTask(key, by = "id", save = true) { if (this.readOnly && save) throw new Error("Library is read only"); const task = this.getManyTasks().find((t) => t[by] === key); if (task) { if (save) { DB().removeOne("tasks", { id: task.id, library: this.name }); this.wb.emit("copytask-remove", task); } this.tasks = this.tasks.filter((t) => t.id !== task.id); this.wb.removeFromRuntime("copyTasks", task); return true; } } /** * Removes all tasks that match the filter * * @param filters {{any?:any?}} * @returns {Promise<Task[]>} the remaining tasks */ removeManyTasks(filters) { if (this.readOnly) throw new Error("Library is read only"); return new Promise(async (resolve) => { const tasks = this.getManyTasks(); for (let task of tasks) { for (let key in filters) { if (task[key] !== filters[key]) continue; this.removeOneTask(filters[key], key); await utility.twiddleThumbs(5); //wait a few ms to make sure the timestamp is different } } resolve(tasks); }).catch((e) => { LogBot.log(500, e); }); } getOneTranscodeTask(id) { const search = this.transcodes.find((job) => job.id === id); if (search) return search; throw Error("No job found with that key"); } getManyTranscodeTasks(filters = {}) { return this.transcodes; } async addOneTranscodeTask(files: MetaFile[], options: { pathToExport: string }) { if (!options.pathToExport) throw new Error("No path to export set"); if (!finder.isReachable(options.pathToExport)) throw new Error("Path to export is not reachable"); const newTask = new TranscodeTask(files, options); this.transcodes.push(newTask); await DB().updateOne("transcodes", { library: this.name, id: newTask.id }, newTask.toJSON({ db: true })); this.wb.addToRuntime("transcodes", newTask); this.wb.emit("transcode-new", newTask); return newTask; } removeOneTranscodeTask(id, save = true) { if (this.readOnly && save) throw new Error("Library is read only"); const task = this.getOneTranscodeTask(id); if (task && task.status !== 2) { this.transcodes = this.transcodes.filter((j) => j.id !== id); if (save) { DB().removeOne("transcodes", { id: task.id, library: this.name }); this.wb.emit("transcode-remove", task); } this.wb.removeFromRuntime("transcodes", task); return true; } throw Error("Job does not exist or is still running."); } async runOneTranscodeTask(id, cb, cancelToken) { const task = this.getOneTranscodeTask(id); if (task) { try { await task.run(this, cb, cancelToken, (job) => { //DB().updateOne("transcodes", { id: task.id, library: this.name }, task.toJSON({ db: true })); }); DB().updateOne("transcodes", { id: task.id, library: this.name }, task.toJSON({ db: true })); this.wb.emit("transcode-edit", task); return true; } catch (e) { DB().updateOne("transcodes", { id: task.id, library: this.name }, task.toJSON({ db: true })); throw e; } } } async generateOneReport(metaFiles: MetaFile[], options: ReportOptions): Promise<Boolean> { if (metaFiles.length === 0) throw new Error("No files to generate report for."); if (options.format === "html") { // return await ExportBot.generateHTML(metaFiles, options); throw new Error("HTML reports are not supported yet."); } if (options.format === "pdf") { try { return await ExportBot.exportPDF(metaFiles, { paths: [options.pathToExport || this.pathToLibrary + "/_reports"], fileName: options.reportName, logo: options.logoPath, uniqueNames: options.uniqueNames, credits: options.credits, template: options.template, }); } catch (e) { return false; } } return false; } /* SAVING */ /** * Returns the flattened version of the library with statistics * * @return {{metaData: Object, copyTasks: number, buckets: number, name, files: number, creationDate: string}} */ toJSON(options: { db: boolean } = { db: false }) { let stats = { count: { total: this.metaFiles.length, video: 0, "video-raw": 0, audio: 0, photo: 0, sidecar: 0, lessThanTwo: 0, }, size: 0, }; for (let f of this.metaFiles) { stats.size += f.size; stats.count[f.fileType]++; if (f.copies.length < 2) stats.count.lessThanTwo++; } return { creationDate: this.creationDate.toString(), name: this.name, pathToLibrary: this.pathToLibrary, drops: this.drops.getCols(), stats: !options.db ? stats : undefined, files: this.metaFiles.map((f) => f.id), tasks: this.tasks.map((t) => t.id), folders: this.folders, readOnly: this.readOnly, }; } }