From 4787e7fdb5f4bc209f978159c6f693d051faabd0 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 2 Jan 2025 15:42:52 -0600 Subject: [PATCH] Updates to LibraryItemController to use new model --- server/controllers/LibraryItemController.js | 212 +++++++++++++++----- server/managers/AudioMetadataManager.js | 7 +- server/managers/CoverManager.js | 23 ++- server/models/LibraryItem.js | 44 +++- server/objects/LibraryItem.js | 15 -- server/objects/mediaTypes/Book.js | 117 ++--------- server/objects/mediaTypes/Podcast.js | 14 -- server/scanner/AudioFileScanner.js | 8 +- 8 files changed, 245 insertions(+), 195 deletions(-) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 8a5ab860..f1d11c15 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -32,7 +32,7 @@ const ShareManager = require('../managers/ShareManager') * @typedef {RequestWithUser & RequestEntityObject} LibraryItemControllerRequest * * @typedef RequestLibraryFileObject - * @property {import('../models/LibraryItem').LibraryFileObject} libraryFile + * @property {import('../objects/files/LibraryFile')} libraryFile * * @typedef {RequestWithUser & RequestEntityObject & RequestLibraryFileObject} LibraryItemControllerRequestWithFile */ @@ -83,6 +83,10 @@ class LibraryItemController { } /** + * PATCH: /api/items/:id + * + * @deprecated + * Use the updateMedia /api/items/:id/media endpoint instead or updateCover /api/items/:id/cover * * @param {LibraryItemControllerRequest} req * @param {Response} res @@ -288,10 +292,10 @@ class LibraryItemController { let result = null if (req.body?.url) { Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`) - result = await CoverManager.downloadCoverFromUrl(req.oldLibraryItem, req.body.url) + result = await CoverManager.downloadCoverFromUrlNew(req.body.url, req.libraryItem.id, req.libraryItem.isFile ? null : req.libraryItem.path) } else if (req.files?.cover) { Logger.debug(`[LibraryItemController] Handling uploaded cover`) - result = await CoverManager.uploadCover(req.oldLibraryItem, req.files.cover) + result = await CoverManager.uploadCover(req.libraryItem, req.files.cover) } else { return res.status(400).send('Invalid request no file or url') } @@ -303,8 +307,15 @@ class LibraryItemController { } if (updateAndReturnJson) { - await Database.updateLibraryItem(req.oldLibraryItem) - SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) + req.libraryItem.media.coverPath = result.cover + req.libraryItem.media.changed('coverPath', true) + await req.libraryItem.media.save() + + // client uses updatedAt timestamp in URL to force refresh cover + req.libraryItem.changed('updatedAt', true) + await req.libraryItem.save() + + SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded()) res.json({ success: true, cover: result.cover @@ -323,13 +334,20 @@ class LibraryItemController { return res.status(400).send('Invalid request no cover path') } - const validationResult = await CoverManager.validateCoverPath(req.body.cover, req.oldLibraryItem) + const validationResult = await CoverManager.validateCoverPath(req.body.cover, req.libraryItem) if (validationResult.error) { return res.status(500).send(validationResult.error) } if (validationResult.updated) { - await Database.updateLibraryItem(req.oldLibraryItem) - SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) + req.libraryItem.media.coverPath = validationResult.cover + req.libraryItem.media.changed('coverPath', true) + await req.libraryItem.media.save() + + // client uses updatedAt timestamp in URL to force refresh cover + req.libraryItem.changed('updatedAt', true) + await req.libraryItem.save() + + SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded()) } res.json({ success: true, @@ -345,10 +363,17 @@ class LibraryItemController { */ async removeCover(req, res) { if (req.libraryItem.media.coverPath) { - req.oldLibraryItem.updateMediaCover('') + req.libraryItem.media.coverPath = null + req.libraryItem.media.changed('coverPath', true) + await req.libraryItem.media.save() + + // client uses updatedAt timestamp in URL to force refresh cover + req.libraryItem.changed('updatedAt', true) + await req.libraryItem.save() + await CacheManager.purgeCoverCache(req.libraryItem.id) - await Database.updateLibraryItem(req.oldLibraryItem) - SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) + + SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded()) } res.sendStatus(200) @@ -451,11 +476,32 @@ class LibraryItemController { Logger.error(`[LibraryItemController] updateTracks invalid orderedFileData ${req.libraryItem.id}`) return res.sendStatus(400) } + // Ensure that each orderedFileData has a valid ino and is in the book audioFiles + if (orderedFileData.some((fileData) => !fileData?.ino || !req.libraryItem.media.audioFiles.some((af) => af.ino === fileData.ino))) { + Logger.error(`[LibraryItemController] updateTracks invalid orderedFileData ${req.libraryItem.id}`) + return res.sendStatus(400) + } - req.oldLibraryItem.media.updateAudioTracks(orderedFileData) - await Database.updateLibraryItem(req.oldLibraryItem) - SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) - res.json(req.oldLibraryItem.toJSON()) + let index = 1 + const updatedAudioFiles = orderedFileData.map((fileData) => { + const audioFile = req.libraryItem.media.audioFiles.find((af) => af.ino === fileData.ino) + audioFile.manuallyVerified = true + audioFile.exclude = !!fileData.exclude + if (audioFile.exclude) { + audioFile.index = -1 + } else { + audioFile.index = index++ + } + return audioFile + }) + updatedAudioFiles.sort((a, b) => a.index - b.index) + + req.libraryItem.media.audioFiles = updatedAudioFiles + req.libraryItem.media.changed('audioFiles', true) + await req.libraryItem.media.save() + + SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded()) + res.json(req.libraryItem.toOldJSON()) } /** @@ -787,7 +833,7 @@ class LibraryItemController { return res.sendStatus(500) } - res.json(this.audioMetadataManager.getMetadataObjectForApi(req.oldLibraryItem)) + res.json(this.audioMetadataManager.getMetadataObjectForApi(req.libraryItem)) } /** @@ -802,26 +848,51 @@ class LibraryItemController { return res.sendStatus(403) } - if (req.libraryItem.isMissing || !req.libraryItem.isBook || !req.libraryItem.media.includedAudioFiles.length) { + if (req.libraryItem.isMissing || !req.libraryItem.isBook || !req.libraryItem.media.hasAudioTracks) { Logger.error(`[LibraryItemController] Invalid library item`) return res.sendStatus(500) } - if (!req.body.chapters) { + if (!Array.isArray(req.body.chapters) || req.body.chapters.some((c) => !c.title || typeof c.title !== 'string' || c.start === undefined || typeof c.start !== 'number' || c.end === undefined || typeof c.end !== 'number')) { Logger.error(`[LibraryItemController] Invalid payload`) return res.sendStatus(400) } const chapters = req.body.chapters || [] - const wasUpdated = req.oldLibraryItem.media.updateChapters(chapters) - if (wasUpdated) { - await Database.updateLibraryItem(req.oldLibraryItem) - SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) + + let hasUpdates = false + if (chapters.length !== req.libraryItem.media.chapters.length) { + req.libraryItem.media.chapters = chapters.map((c, index) => { + return { + id: index, + title: c.title, + start: c.start, + end: c.end + } + }) + hasUpdates = true + } else { + for (const [index, chapter] of chapters.entries()) { + const currentChapter = req.libraryItem.media.chapters[index] + if (currentChapter.title !== chapter.title || currentChapter.start !== chapter.start || currentChapter.end !== chapter.end) { + currentChapter.title = chapter.title + currentChapter.start = chapter.start + currentChapter.end = chapter.end + hasUpdates = true + } + } + } + + if (hasUpdates) { + req.libraryItem.media.changed('chapters', true) + await req.libraryItem.media.save() + + SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded()) } res.json({ success: true, - updated: wasUpdated + updated: hasUpdates }) } @@ -829,7 +900,7 @@ class LibraryItemController { * GET: /api/items/:id/ffprobe/:fileid * FFProbe JSON result from audio file * - * @param {LibraryItemControllerRequestWithFile} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async getFFprobeData(req, res) { @@ -837,18 +908,14 @@ class LibraryItemController { Logger.error(`[LibraryItemController] Non-admin user "${req.user.username}" attempted to get ffprobe data`) return res.sendStatus(403) } - if (req.libraryFile.fileType !== 'audio') { - Logger.error(`[LibraryItemController] Invalid filetype "${req.libraryFile.fileType}" for fileid "${req.params.fileid}". Expected audio file`) - return res.sendStatus(400) - } - const audioFile = req.oldLibraryItem.media.findFileWithInode(req.params.fileid) + const audioFile = req.libraryItem.getAudioFileWithIno(req.params.fileid) if (!audioFile) { Logger.error(`[LibraryItemController] Audio file not found with inode value ${req.params.fileid}`) return res.sendStatus(404) } - const ffprobeData = await AudioFileScanner.probeAudioFile(audioFile) + const ffprobeData = await AudioFileScanner.probeAudioFile(audioFile.metadata.path) res.json(ffprobeData) } @@ -889,17 +956,35 @@ class LibraryItemController { await fs.remove(libraryFile.metadata.path).catch((error) => { Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error) }) - req.oldLibraryItem.removeLibraryFile(req.params.fileid) - if (req.oldLibraryItem.media.removeFileWithInode(req.params.fileid)) { - // If book has no more media files then mark it as missing - if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaFiles) { - req.oldLibraryItem.setMissing() + req.libraryItem.libraryFiles = req.libraryItem.libraryFiles.filter((lf) => lf.ino !== req.params.fileid) + req.libraryItem.changed('libraryFiles', true) + + if (req.libraryItem.isBook) { + if (req.libraryItem.media.audioFiles.some((af) => af.ino === req.params.fileid)) { + req.libraryItem.media.audioFiles = req.libraryItem.media.audioFiles.filter((af) => af.ino !== req.params.fileid) + req.libraryItem.media.changed('audioFiles', true) + } else if (req.libraryItem.media.ebookFile?.ino === req.params.fileid) { + req.libraryItem.media.ebookFile = null + req.libraryItem.media.changed('ebookFile', true) } + if (!req.libraryItem.media.hasMediaFiles) { + req.libraryItem.isMissing = true + } + } else if (req.libraryItem.media.podcastEpisodes.some((ep) => ep.audioFile.ino === req.params.fileid)) { + const episodeToRemove = req.libraryItem.media.podcastEpisodes.find((ep) => ep.audioFile.ino === req.params.fileid) + await episodeToRemove.destroy() + + req.libraryItem.media.podcastEpisodes = req.libraryItem.media.podcastEpisodes.filter((ep) => ep.audioFile.ino !== req.params.fileid) } - req.oldLibraryItem.updatedAt = Date.now() - await Database.updateLibraryItem(req.oldLibraryItem) - SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) + + if (req.libraryItem.media.changed()) { + await req.libraryItem.media.save() + } + + await req.libraryItem.save() + + SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded()) res.sendStatus(200) } @@ -961,13 +1046,13 @@ class LibraryItemController { async getEBookFile(req, res) { let ebookFile = null if (req.params.fileid) { - ebookFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid) + ebookFile = req.libraryItem.getLibraryFileWithIno(req.params.fileid) if (!ebookFile?.isEBookFile) { Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`) return res.status(400).send('Invalid ebook file id') } } else { - ebookFile = req.oldLibraryItem.media.ebookFile + ebookFile = req.libraryItem.media.ebookFile } if (!ebookFile) { @@ -999,28 +1084,55 @@ class LibraryItemController { * if an ebook file is the primary ebook, then it will be changed to supplementary * if an ebook file is supplementary, then it will be changed to primary * - * @param {LibraryItemControllerRequest} req + * @param {LibraryItemControllerRequestWithFile} req * @param {Response} res */ async updateEbookFileStatus(req, res) { - const ebookLibraryFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid) - if (!ebookLibraryFile?.isEBookFile) { + if (!req.libraryItem.isBook) { + Logger.error(`[LibraryItemController] Invalid media type for ebook file status update`) + return res.sendStatus(400) + } + if (!req.libraryFile?.isEBookFile) { Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`) return res.status(400).send('Invalid ebook file id') } + const ebookLibraryFile = req.libraryFile + let primaryEbookFile = null + + const ebookLibraryFileInos = req.libraryItem + .getLibraryFiles() + .filter((lf) => lf.isEBookFile) + .map((lf) => lf.ino) + if (ebookLibraryFile.isSupplementary) { Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to primary`) - req.oldLibraryItem.setPrimaryEbook(ebookLibraryFile) + + primaryEbookFile = ebookLibraryFile.toJSON() + delete primaryEbookFile.isSupplementary + delete primaryEbookFile.fileType + primaryEbookFile.ebookFormat = ebookLibraryFile.metadata.format } else { Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to supplementary`) - ebookLibraryFile.isSupplementary = true - req.oldLibraryItem.setPrimaryEbook(null) } - req.oldLibraryItem.updatedAt = Date.now() - await Database.updateLibraryItem(req.oldLibraryItem) - SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) + req.libraryItem.media.ebookFile = primaryEbookFile + req.libraryItem.media.changed('ebookFile', true) + await req.libraryItem.media.save() + + req.libraryItem.libraryFiles = req.libraryItem.libraryFiles.map((lf) => { + if (ebookLibraryFileInos.includes(lf.ino)) { + lf.isSupplementary = lf.ino !== primaryEbookFile?.ino + } + return lf + }) + req.libraryItem.changed('libraryFiles', true) + + req.libraryItem.isMissing = !req.libraryItem.media.hasMediaFiles + + await req.libraryItem.save() + + SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded()) res.sendStatus(200) } @@ -1042,7 +1154,7 @@ class LibraryItemController { // For library file routes, get the library file if (req.params.fileid) { - req.libraryFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid) + req.libraryFile = req.libraryItem.getLibraryFileWithIno(req.params.fileid) if (!req.libraryFile) { Logger.error(`[LibraryItemController] Library file "${req.params.fileid}" does not exist for library item`) return res.sendStatus(404) diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 7911178e..36aecb97 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -34,8 +34,13 @@ class AudioMetadataMangaer { return this.tasksQueued.some((t) => t.data.libraryItemId === libraryItemId) || this.tasksRunning.some((t) => t.data.libraryItemId === libraryItemId) } + /** + * + * @param {import('../models/LibraryItem')} libraryItem + * @returns + */ getMetadataObjectForApi(libraryItem) { - return ffmpegHelpers.getFFMetadataObject(libraryItem, libraryItem.media.includedAudioFiles.length) + return ffmpegHelpers.getFFMetadataObject(libraryItem.toOldJSONExpanded(), libraryItem.media.includedAudioFiles.length) } /** diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js index 2b3a697d..c995a446 100644 --- a/server/managers/CoverManager.js +++ b/server/managers/CoverManager.js @@ -79,6 +79,12 @@ class CoverManager { return imgType } + /** + * + * @param {import('../models/LibraryItem')} libraryItem + * @param {*} coverFile - file object from req.files + * @returns {Promise<{error:string}|{cover:string}>} + */ async uploadCover(libraryItem, coverFile) { const extname = Path.extname(coverFile.name.toLowerCase()) if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) { @@ -110,14 +116,20 @@ class CoverManager { await this.removeOldCovers(coverDirPath, extname) await CacheManager.purgeCoverCache(libraryItem.id) - Logger.info(`[CoverManager] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.metadata.title}"`) + Logger.info(`[CoverManager] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.title}"`) - libraryItem.updateMediaCover(coverFullPath) return { cover: coverFullPath } } + /** + * + * @param {Object} libraryItem - old library item + * @param {string} url + * @param {boolean} [forceLibraryItemFolder=false] + * @returns {Promise<{error:string}|{cover:string}>} + */ async downloadCoverFromUrl(libraryItem, url, forceLibraryItemFolder = false) { try { // Force save cover with library item is used for adding new podcasts @@ -166,6 +178,12 @@ class CoverManager { } } + /** + * + * @param {string} coverPath + * @param {import('../models/LibraryItem')} libraryItem + * @returns {Promise<{error:string}|{cover:string,updated:boolean}>} + */ async validateCoverPath(coverPath, libraryItem) { // Invalid cover path if (!coverPath || coverPath.startsWith('http:') || coverPath.startsWith('https:')) { @@ -235,7 +253,6 @@ class CoverManager { await CacheManager.purgeCoverCache(libraryItem.id) - libraryItem.updateMediaCover(coverPath) return { cover: coverPath, updated: true diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 412860d2..03e67a9e 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -1152,13 +1152,49 @@ class LibraryItem extends Model { Logger.error(`[LibraryItem] hasAudioTracks: Library item "${this.id}" does not have media`) return false } - if (this.mediaType === 'book') { + if (this.isBook) { return this.media.audioFiles?.length > 0 } else { return this.media.podcastEpisodes?.length > 0 } } + /** + * + * @param {string} ino + * @returns {import('./Book').AudioFileObject} + */ + getAudioFileWithIno(ino) { + if (!this.media) { + Logger.error(`[LibraryItem] getAudioFileWithIno: Library item "${this.id}" does not have media`) + return null + } + if (this.isBook) { + return this.media.audioFiles.find((af) => af.ino === ino) + } else { + return this.media.podcastEpisodes.find((pe) => pe.audioFile?.ino === ino)?.audioFile + } + } + + /** + * + * @param {string} ino + * @returns {LibraryFile} + */ + getLibraryFileWithIno(ino) { + const libraryFile = this.libraryFiles.find((lf) => lf.ino === ino) + if (!libraryFile) return null + return new LibraryFile(libraryFile) + } + + getLibraryFiles() { + return this.libraryFiles.map((lf) => new LibraryFile(lf)) + } + + getLibraryFilesJson() { + return this.libraryFiles.map((lf) => new LibraryFile(lf).toJSON()) + } + toOldJSON() { if (!this.media) { throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`) @@ -1184,7 +1220,8 @@ class LibraryItem extends Model { isInvalid: !!this.isInvalid, mediaType: this.mediaType, media: this.media.toOldJSON(this.id), - libraryFiles: structuredClone(this.libraryFiles) + // LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database + libraryFiles: this.getLibraryFilesJson() } } @@ -1237,7 +1274,8 @@ class LibraryItem extends Model { isInvalid: !!this.isInvalid, mediaType: this.mediaType, media: this.media.toOldJSONExpanded(this.id), - libraryFiles: structuredClone(this.libraryFiles), + // LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database + libraryFiles: this.getLibraryFilesJson(), size: this.size } } diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js index 84a37897..4656a028 100644 --- a/server/objects/LibraryItem.js +++ b/server/objects/LibraryItem.js @@ -327,20 +327,5 @@ class LibraryItem { } return false } - - /** - * Set the EBookFile from a LibraryFile - * If null then ebookFile will be removed from the book - * all ebook library files that are not primary are marked as supplementary - * - * @param {LibraryFile} [libraryFile] - */ - setPrimaryEbook(ebookLibraryFile = null) { - const ebookLibraryFiles = this.libraryFiles.filter((lf) => lf.isEBookFile) - for (const libraryFile of ebookLibraryFiles) { - libraryFile.isSupplementary = ebookLibraryFile?.ino !== libraryFile.ino - } - this.media.setEbookFile(ebookLibraryFile) - } } module.exports = LibraryItem diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js index 8fdff988..4701e422 100644 --- a/server/objects/mediaTypes/Book.js +++ b/server/objects/mediaTypes/Book.js @@ -33,8 +33,8 @@ class Book { this.metadata = new BookMetadata(book.metadata) this.coverPath = book.coverPath this.tags = [...book.tags] - this.audioFiles = book.audioFiles.map(f => new AudioFile(f)) - this.chapters = book.chapters.map(c => ({ ...c })) + this.audioFiles = book.audioFiles.map((f) => new AudioFile(f)) + this.chapters = book.chapters.map((c) => ({ ...c })) this.ebookFile = book.ebookFile ? new EBookFile(book.ebookFile) : null this.lastCoverSearch = book.lastCoverSearch || null this.lastCoverSearchQuery = book.lastCoverSearchQuery || null @@ -47,8 +47,8 @@ class Book { metadata: this.metadata.toJSON(), coverPath: this.coverPath, tags: [...this.tags], - audioFiles: this.audioFiles.map(f => f.toJSON()), - chapters: this.chapters.map(c => ({ ...c })), + audioFiles: this.audioFiles.map((f) => f.toJSON()), + chapters: this.chapters.map((c) => ({ ...c })), ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null } } @@ -75,11 +75,11 @@ class Book { metadata: this.metadata.toJSONExpanded(), coverPath: this.coverPath, tags: [...this.tags], - audioFiles: this.audioFiles.map(f => f.toJSON()), - chapters: this.chapters.map(c => ({ ...c })), + audioFiles: this.audioFiles.map((f) => f.toJSON()), + chapters: this.chapters.map((c) => ({ ...c })), duration: this.duration, size: this.size, - tracks: this.tracks.map(t => t.toJSON()), + tracks: this.tracks.map((t) => t.toJSON()), ebookFile: this.ebookFile?.toJSON() || null } } @@ -87,14 +87,14 @@ class Book { toJSONForMetadataFile() { return { tags: [...this.tags], - chapters: this.chapters.map(c => ({ ...c })), + chapters: this.chapters.map((c) => ({ ...c })), ...this.metadata.toJSONForMetadataFile() } } get size() { var total = 0 - this.audioFiles.forEach((af) => total += af.metadata.size) + this.audioFiles.forEach((af) => (total += af.metadata.size)) if (this.ebookFile) { total += this.ebookFile.metadata.size } @@ -104,7 +104,7 @@ class Book { return !!this.tracks.length || this.ebookFile } get includedAudioFiles() { - return this.audioFiles.filter(af => !af.exclude) + return this.audioFiles.filter((af) => !af.exclude) } get tracks() { let startOffset = 0 @@ -117,7 +117,7 @@ class Book { } get duration() { let total = 0 - this.tracks.forEach((track) => total += track.duration) + this.tracks.forEach((track) => (total += track.duration)) return total } get numTracks() { @@ -149,30 +149,6 @@ class Book { return hasUpdates } - updateChapters(chapters) { - var hasUpdates = this.chapters.length !== chapters.length - if (hasUpdates) { - this.chapters = chapters.map(ch => ({ - id: ch.id, - start: ch.start, - end: ch.end, - title: ch.title - })) - } else { - for (let i = 0; i < this.chapters.length; i++) { - const currChapter = this.chapters[i] - const newChapter = chapters[i] - if (!hasUpdates && (currChapter.title !== newChapter.title || currChapter.start !== newChapter.start || currChapter.end !== newChapter.end)) { - hasUpdates = true - } - this.chapters[i].title = newChapter.title - this.chapters[i].start = newChapter.start - this.chapters[i].end = newChapter.end - } - } - return hasUpdates - } - updateCover(coverPath) { coverPath = filePathToPOSIX(coverPath) if (this.coverPath === coverPath) return false @@ -180,75 +156,6 @@ class Book { return true } - removeFileWithInode(inode) { - if (this.audioFiles.some(af => af.ino === inode)) { - this.audioFiles = this.audioFiles.filter(af => af.ino !== inode) - return true - } - if (this.ebookFile && this.ebookFile.ino === inode) { - this.ebookFile = null - return true - } - return false - } - - /** - * Get audio file or ebook file from inode - * @param {string} inode - * @returns {(AudioFile|EBookFile|null)} - */ - findFileWithInode(inode) { - const audioFile = this.audioFiles.find(af => af.ino === inode) - if (audioFile) return audioFile - if (this.ebookFile && this.ebookFile.ino === inode) return this.ebookFile - return null - } - - /** - * Set the EBookFile from a LibraryFile - * If null then ebookFile will be removed from the book - * - * @param {LibraryFile} [libraryFile] - */ - setEbookFile(libraryFile = null) { - if (!libraryFile) { - this.ebookFile = null - } else { - const ebookFile = new EBookFile() - ebookFile.setData(libraryFile) - this.ebookFile = ebookFile - } - } - - addAudioFile(audioFile) { - this.audioFiles.push(audioFile) - } - - updateAudioTracks(orderedFileData) { - let index = 1 - this.audioFiles = orderedFileData.map((fileData) => { - const audioFile = this.audioFiles.find(af => af.ino === fileData.ino) - audioFile.manuallyVerified = true - audioFile.error = null - if (fileData.exclude !== undefined) { - audioFile.exclude = !!fileData.exclude - } - if (audioFile.exclude) { - audioFile.index = -1 - } else { - audioFile.index = index++ - } - return audioFile - }) - - this.rebuildTracks() - } - - rebuildTracks() { - Logger.debug(`[Book] Tracks being rebuilt...!`) - this.audioFiles.sort((a, b) => a.index - b.index) - } - // Only checks container format checkCanDirectPlay(payload) { var supportedMimeTypes = payload.supportedMimeTypes || [] @@ -268,7 +175,7 @@ class Book { } getChapters() { - return this.chapters?.map(ch => ({ ...ch })) || [] + return this.chapters?.map((ch) => ({ ...ch })) || [] } } module.exports = Book diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js index c7d91d0d..510575ed 100644 --- a/server/objects/mediaTypes/Podcast.js +++ b/server/objects/mediaTypes/Podcast.js @@ -181,20 +181,6 @@ class Podcast { return true } - removeFileWithInode(inode) { - const hasEpisode = this.episodes.some((ep) => ep.audioFile.ino === inode) - if (hasEpisode) { - this.episodes = this.episodes.filter((ep) => ep.audioFile.ino !== inode) - } - return hasEpisode - } - - findFileWithInode(inode) { - var episode = this.episodes.find((ep) => ep.audioFile.ino === inode) - if (episode) return episode.audioFile - return null - } - setData(mediaData) { this.metadata = new PodcastMetadata() if (mediaData.metadata) { diff --git a/server/scanner/AudioFileScanner.js b/server/scanner/AudioFileScanner.js index 6c808aaa..00bd44d3 100644 --- a/server/scanner/AudioFileScanner.js +++ b/server/scanner/AudioFileScanner.js @@ -202,12 +202,12 @@ class AudioFileScanner { /** * - * @param {AudioFile} audioFile + * @param {string} audioFilePath * @returns {object} */ - probeAudioFile(audioFile) { - Logger.debug(`[AudioFileScanner] Running ffprobe for audio file at "${audioFile.metadata.path}"`) - return prober.rawProbe(audioFile.metadata.path) + probeAudioFile(audioFilePath) { + Logger.debug(`[AudioFileScanner] Running ffprobe for audio file at "${audioFilePath}"`) + return prober.rawProbe(audioFilePath) } /**