diff --git a/server/controllers/ToolsController.js b/server/controllers/ToolsController.js index 8aa9f832..94122b46 100644 --- a/server/controllers/ToolsController.js +++ b/server/controllers/ToolsController.js @@ -7,6 +7,11 @@ const Database = require('../Database') * @property {import('../models/User')} user * * @typedef {Request & RequestUserObject} RequestWithUser + * + * @typedef RequestEntityObject + * @property {import('../models/LibraryItem')} libraryItem + * + * @typedef {RequestWithUser & RequestEntityObject} RequestWithLibraryItem */ class ToolsController { @@ -18,7 +23,7 @@ class ToolsController { * * @this import('../routers/ApiRouter') * - * @param {RequestWithUser} req + * @param {RequestWithLibraryItem} req * @param {Response} res */ async encodeM4b(req, res) { @@ -27,12 +32,12 @@ class ToolsController { return res.status(404).send('Audiobook not found') } - if (req.libraryItem.mediaType !== 'book') { + if (!req.libraryItem.isBook) { Logger.error(`[MiscController] encodeM4b: Invalid library item ${req.params.id}: not a book`) return res.status(400).send('Invalid library item: not a book') } - if (req.libraryItem.media.tracks.length <= 0) { + if (!req.libraryItem.hasAudioTracks) { Logger.error(`[MiscController] encodeM4b: Invalid audiobook ${req.params.id}: no audio tracks`) return res.status(400).send('Invalid audiobook: no audio tracks') } @@ -72,11 +77,11 @@ class ToolsController { * * @this import('../routers/ApiRouter') * - * @param {RequestWithUser} req + * @param {RequestWithLibraryItem} req * @param {Response} res */ async embedAudioFileMetadata(req, res) { - if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) { + if (req.libraryItem.isMissing || !req.libraryItem.hasAudioTracks || !req.libraryItem.isBook) { Logger.error(`[ToolsController] Invalid library item`) return res.sendStatus(400) } @@ -111,7 +116,7 @@ class ToolsController { const libraryItems = [] for (const libraryItemId of libraryItemIds) { - const libraryItem = await Database.libraryItemModel.getOldById(libraryItemId) + const libraryItem = await Database.libraryItemModel.getExpandedById(libraryItemId) if (!libraryItem) { Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not found`) return res.sendStatus(404) @@ -123,7 +128,7 @@ class ToolsController { return res.sendStatus(403) } - if (libraryItem.isMissing || !libraryItem.hasAudioFiles || !libraryItem.isBook) { + if (libraryItem.isMissing || !libraryItem.hasAudioTracks || !libraryItem.isBook) { Logger.error(`[ToolsController] Batch embed invalid library item (${libraryItemId})`) return res.sendStatus(400) } @@ -157,7 +162,7 @@ class ToolsController { } if (req.params.id) { - const item = await Database.libraryItemModel.getOldById(req.params.id) + const item = await Database.libraryItemModel.getExpandedById(req.params.id) if (!item?.media) return res.sendStatus(404) // Check user can access this library item diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js index ea70d73c..f6a56160 100644 --- a/server/managers/AbMergeManager.js +++ b/server/managers/AbMergeManager.js @@ -51,7 +51,7 @@ class AbMergeManager { /** * * @param {string} userId - * @param {import('../objects/LibraryItem')} libraryItem + * @param {import('../models/LibraryItem')} libraryItem * @param {AbMergeEncodeOptions} [options={}] */ async startAudiobookMerge(userId, libraryItem, options = {}) { @@ -67,7 +67,7 @@ class AbMergeManager { libraryItemId: libraryItem.id, libraryItemDir, userId, - originalTrackPaths: libraryItem.media.tracks.map((t) => t.metadata.path), + originalTrackPaths: libraryItem.media.includedAudioFiles.map((t) => t.metadata.path), inos: libraryItem.media.includedAudioFiles.map((f) => f.ino), tempFilepath, targetFilename, @@ -86,9 +86,9 @@ class AbMergeManager { key: 'MessageTaskEncodingM4b' } const taskDescriptionString = { - text: `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`, + text: `Encoding audiobook "${libraryItem.media.title}" into a single m4b file.`, key: 'MessageTaskEncodingM4bDescription', - subs: [libraryItem.media.metadata.title] + subs: [libraryItem.media.title] } task.setData('encode-m4b', taskTitleString, taskDescriptionString, false, taskData) TaskManager.addTask(task) @@ -103,7 +103,7 @@ class AbMergeManager { /** * - * @param {import('../objects/LibraryItem')} libraryItem + * @param {import('../models/LibraryItem')} libraryItem * @param {Task} task * @param {AbMergeEncodeOptions} encodingOptions */ @@ -141,7 +141,7 @@ class AbMergeManager { const embedFraction = 1 - encodeFraction try { const trackProgressMonitor = new TrackProgressMonitor( - libraryItem.media.tracks.map((t) => t.duration), + libraryItem.media.includedAudioFiles.map((t) => t.duration), (trackIndex) => SocketAuthority.adminEmitter('track_started', { libraryItemId: libraryItem.id, ino: task.data.inos[trackIndex] }), (trackIndex, progressInTrack, taskProgress) => { SocketAuthority.adminEmitter('track_progress', { libraryItemId: libraryItem.id, ino: task.data.inos[trackIndex], progress: progressInTrack }) @@ -150,7 +150,7 @@ class AbMergeManager { (trackIndex) => SocketAuthority.adminEmitter('track_finished', { libraryItemId: libraryItem.id, ino: task.data.inos[trackIndex] }) ) task.data.ffmpeg = new Ffmpeg() - await ffmpegHelpers.mergeAudioFiles(libraryItem.media.tracks, task.data.duration, task.data.itemCachePath, task.data.tempFilepath, encodingOptions, (progress) => trackProgressMonitor.update(progress), task.data.ffmpeg) + await ffmpegHelpers.mergeAudioFiles(libraryItem.media.includedAudioFiles, task.data.duration, task.data.itemCachePath, task.data.tempFilepath, encodingOptions, (progress) => trackProgressMonitor.update(progress), task.data.ffmpeg) delete task.data.ffmpeg trackProgressMonitor.finish() } catch (error) { diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 36aecb97..7471a1ca 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -40,14 +40,14 @@ class AudioMetadataMangaer { * @returns */ getMetadataObjectForApi(libraryItem) { - return ffmpegHelpers.getFFMetadataObject(libraryItem.toOldJSONExpanded(), libraryItem.media.includedAudioFiles.length) + return ffmpegHelpers.getFFMetadataObject(libraryItem, libraryItem.media.includedAudioFiles.length) } /** * * @param {string} userId - * @param {*} libraryItems - * @param {*} options + * @param {import('../models/LibraryItem')[]} libraryItems + * @param {UpdateMetadataOptions} options */ handleBatchEmbed(userId, libraryItems, options = {}) { libraryItems.forEach((li) => { @@ -58,7 +58,7 @@ class AudioMetadataMangaer { /** * * @param {string} userId - * @param {import('../objects/LibraryItem')} libraryItem + * @param {import('../models/LibraryItem')} libraryItem * @param {UpdateMetadataOptions} [options={}] */ async updateMetadataForItem(userId, libraryItem, options = {}) { @@ -108,14 +108,14 @@ class AudioMetadataMangaer { key: 'MessageTaskEmbeddingMetadata' } const taskDescriptionString = { - text: `Embedding metadata in audiobook "${libraryItem.media.metadata.title}".`, + text: `Embedding metadata in audiobook "${libraryItem.media.title}".`, key: 'MessageTaskEmbeddingMetadataDescription', - subs: [libraryItem.media.metadata.title] + subs: [libraryItem.media.title] } task.setData('embed-metadata', taskTitleString, taskDescriptionString, false, taskData) if (this.tasksRunning.length >= this.MAX_CONCURRENT_TASKS) { - Logger.info(`[AudioMetadataManager] Queueing embed metadata for audiobook "${libraryItem.media.metadata.title}"`) + Logger.info(`[AudioMetadataManager] Queueing embed metadata for audiobook "${libraryItem.media.title}"`) SocketAuthority.adminEmitter('metadata_embed_queue_update', { libraryItemId: libraryItem.id, queued: true diff --git a/server/models/Book.js b/server/models/Book.js index dff79da2..5a4eee54 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -130,130 +130,6 @@ class Book extends Model { this.series } - static getOldBook(libraryItemExpanded) { - const bookExpanded = libraryItemExpanded.media - let authors = [] - if (bookExpanded.authors?.length) { - authors = bookExpanded.authors.map((au) => { - return { - id: au.id, - name: au.name - } - }) - } else if (bookExpanded.bookAuthors?.length) { - authors = bookExpanded.bookAuthors - .map((ba) => { - if (ba.author) { - return { - id: ba.author.id, - name: ba.author.name - } - } else { - Logger.error(`[Book] Invalid bookExpanded bookAuthors: no author`, ba) - return null - } - }) - .filter((a) => a) - } - - let series = [] - if (bookExpanded.series?.length) { - series = bookExpanded.series.map((se) => { - return { - id: se.id, - name: se.name, - sequence: se.bookSeries.sequence - } - }) - } else if (bookExpanded.bookSeries?.length) { - series = bookExpanded.bookSeries - .map((bs) => { - if (bs.series) { - return { - id: bs.series.id, - name: bs.series.name, - sequence: bs.sequence - } - } else { - Logger.error(`[Book] Invalid bookExpanded bookSeries: no series`, bs) - return null - } - }) - .filter((s) => s) - } - - return { - id: bookExpanded.id, - libraryItemId: libraryItemExpanded.id, - coverPath: bookExpanded.coverPath, - tags: bookExpanded.tags, - audioFiles: bookExpanded.audioFiles, - chapters: bookExpanded.chapters, - ebookFile: bookExpanded.ebookFile, - metadata: { - title: bookExpanded.title, - subtitle: bookExpanded.subtitle, - authors: authors, - narrators: bookExpanded.narrators, - series: series, - genres: bookExpanded.genres, - publishedYear: bookExpanded.publishedYear, - publishedDate: bookExpanded.publishedDate, - publisher: bookExpanded.publisher, - description: bookExpanded.description, - isbn: bookExpanded.isbn, - asin: bookExpanded.asin, - language: bookExpanded.language, - explicit: bookExpanded.explicit, - abridged: bookExpanded.abridged - } - } - } - - /** - * @param {object} oldBook - * @returns {boolean} true if updated - */ - static saveFromOld(oldBook) { - const book = this.getFromOld(oldBook) - return this.update(book, { - where: { - id: book.id - } - }) - .then((result) => result[0] > 0) - .catch((error) => { - Logger.error(`[Book] Failed to save book ${book.id}`, error) - return false - }) - } - - static getFromOld(oldBook) { - return { - id: oldBook.id, - title: oldBook.metadata.title, - titleIgnorePrefix: oldBook.metadata.titleIgnorePrefix, - subtitle: oldBook.metadata.subtitle, - publishedYear: oldBook.metadata.publishedYear, - publishedDate: oldBook.metadata.publishedDate, - publisher: oldBook.metadata.publisher, - description: oldBook.metadata.description, - isbn: oldBook.metadata.isbn, - asin: oldBook.metadata.asin, - language: oldBook.metadata.language, - explicit: !!oldBook.metadata.explicit, - abridged: !!oldBook.metadata.abridged, - narrators: oldBook.metadata.narrators, - ebookFile: oldBook.ebookFile?.toJSON() || null, - coverPath: oldBook.coverPath, - duration: oldBook.duration, - audioFiles: oldBook.audioFiles?.map((af) => af.toJSON()) || [], - chapters: oldBook.chapters, - tags: oldBook.tags, - genres: oldBook.metadata.genres - } - } - /** * Initialize model * @param {import('../Database').sequelize} sequelize diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index d19816a3..4035630d 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -1,11 +1,8 @@ -const util = require('util') const Path = require('path') const { DataTypes, Model } = require('sequelize') const fsExtra = require('../libs/fsExtra') const Logger = require('../Logger') -const oldLibraryItem = require('../objects/LibraryItem') const libraryFilters = require('../utils/queries/libraryFilters') -const { areEquivalent } = require('../utils/index') const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') const LibraryFile = require('../objects/files/LibraryFile') const Book = require('./Book') @@ -122,44 +119,6 @@ class LibraryItem extends Model { }) } - /** - * Convert an expanded LibraryItem into an old library item - * - * @param {Model} libraryItemExpanded - * @returns {oldLibraryItem} - */ - static getOldLibraryItem(libraryItemExpanded) { - let media = null - if (libraryItemExpanded.mediaType === 'book') { - media = this.sequelize.models.book.getOldBook(libraryItemExpanded) - } else if (libraryItemExpanded.mediaType === 'podcast') { - media = this.sequelize.models.podcast.getOldPodcast(libraryItemExpanded) - } - - return new oldLibraryItem({ - id: libraryItemExpanded.id, - ino: libraryItemExpanded.ino, - oldLibraryItemId: libraryItemExpanded.extraData?.oldLibraryItemId || null, - libraryId: libraryItemExpanded.libraryId, - folderId: libraryItemExpanded.libraryFolderId, - path: libraryItemExpanded.path, - relPath: libraryItemExpanded.relPath, - isFile: libraryItemExpanded.isFile, - mtimeMs: libraryItemExpanded.mtime?.valueOf(), - ctimeMs: libraryItemExpanded.ctime?.valueOf(), - birthtimeMs: libraryItemExpanded.birthtime?.valueOf(), - addedAt: libraryItemExpanded.createdAt.valueOf(), - updatedAt: libraryItemExpanded.updatedAt.valueOf(), - lastScan: libraryItemExpanded.lastScan?.valueOf(), - scanVersion: libraryItemExpanded.lastScanVersion, - isMissing: !!libraryItemExpanded.isMissing, - isInvalid: !!libraryItemExpanded.isInvalid, - mediaType: libraryItemExpanded.mediaType, - media, - libraryFiles: libraryItemExpanded.libraryFiles - }) - } - /** * Remove library item by id * @@ -318,61 +277,12 @@ class LibraryItem extends Model { return libraryItem } - /** - * Get old library item by id - * @param {string} libraryItemId - * @returns {oldLibraryItem} - */ - static async getOldById(libraryItemId) { - if (!libraryItemId) return null - - const libraryItem = await this.findByPk(libraryItemId) - if (!libraryItem) { - Logger.error(`[LibraryItem] Library item not found with id "${libraryItemId}"`) - return null - } - - if (libraryItem.mediaType === 'podcast') { - libraryItem.media = await libraryItem.getMedia({ - include: [ - { - model: this.sequelize.models.podcastEpisode - } - ] - }) - } else { - libraryItem.media = await libraryItem.getMedia({ - include: [ - { - model: this.sequelize.models.author, - through: { - attributes: [] - } - }, - { - model: this.sequelize.models.series, - through: { - attributes: ['sequence'] - } - } - ], - order: [ - [this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'], - [this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'] - ] - }) - } - - if (!libraryItem.media) return null - return this.getOldLibraryItem(libraryItem) - } - /** * Get library items using filter and sort * @param {import('./Library')} library * @param {import('./User')} user * @param {object} options - * @returns {{ libraryItems:oldLibraryItem[], count:number }} + * @returns {{ libraryItems:Object[], count:number }} */ static async getByFilterAndSort(library, user, options) { let start = Date.now() @@ -426,17 +336,19 @@ class LibraryItem extends Model { // "Continue Listening" shelf const itemsInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, user, include, limit, false) if (itemsInProgressPayload.items.length) { - const ebookOnlyItemsInProgress = itemsInProgressPayload.items.filter((li) => li.media.isEBookOnly) - const audioOnlyItemsInProgress = itemsInProgressPayload.items.filter((li) => !li.media.isEBookOnly) + const ebookOnlyItemsInProgress = itemsInProgressPayload.items.filter((li) => li.media.ebookFormat && !li.media.numTracks) + const audioItemsInProgress = itemsInProgressPayload.items.filter((li) => li.media.numTracks) - shelves.push({ - id: 'continue-listening', - label: 'Continue Listening', - labelStringKey: 'LabelContinueListening', - type: library.isPodcast ? 'episode' : 'book', - entities: audioOnlyItemsInProgress, - total: itemsInProgressPayload.count - }) + if (audioItemsInProgress.length) { + shelves.push({ + id: 'continue-listening', + label: 'Continue Listening', + labelStringKey: 'LabelContinueListening', + type: library.isPodcast ? 'episode' : 'book', + entities: audioItemsInProgress, + total: itemsInProgressPayload.count + }) + } if (ebookOnlyItemsInProgress.length) { // "Continue Reading" shelf @@ -535,17 +447,19 @@ class LibraryItem extends Model { // "Listen Again" shelf const mediaFinishedPayload = await libraryFilters.getMediaFinished(library, user, include, limit) if (mediaFinishedPayload.items.length) { - const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.isEBookOnly) - const audioOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => !li.media.isEBookOnly) + const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.ebookFormat && !li.media.numTracks) + const audioItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.numTracks) - shelves.push({ - id: 'listen-again', - label: 'Listen Again', - labelStringKey: 'LabelListenAgain', - type: library.isPodcast ? 'episode' : 'book', - entities: audioOnlyItemsInProgress, - total: mediaFinishedPayload.count - }) + if (audioItemsInProgress.length) { + shelves.push({ + id: 'listen-again', + label: 'Listen Again', + labelStringKey: 'LabelListenAgain', + type: library.isPodcast ? 'episode' : 'book', + entities: audioItemsInProgress, + total: mediaFinishedPayload.count + }) + } // "Read Again" shelf if (ebookOnlyItemsInProgress.length) { diff --git a/server/models/MediaProgress.js b/server/models/MediaProgress.js index 80204ef5..bb827682 100644 --- a/server/models/MediaProgress.js +++ b/server/models/MediaProgress.js @@ -36,33 +36,6 @@ class MediaProgress extends Model { this.createdAt } - static upsertFromOld(oldMediaProgress) { - const mediaProgress = this.getFromOld(oldMediaProgress) - return this.upsert(mediaProgress) - } - - static getFromOld(oldMediaProgress) { - return { - id: oldMediaProgress.id, - userId: oldMediaProgress.userId, - mediaItemId: oldMediaProgress.mediaItemId, - mediaItemType: oldMediaProgress.mediaItemType, - duration: oldMediaProgress.duration, - currentTime: oldMediaProgress.currentTime, - ebookLocation: oldMediaProgress.ebookLocation || null, - ebookProgress: oldMediaProgress.ebookProgress || null, - isFinished: !!oldMediaProgress.isFinished, - hideFromContinueListening: !!oldMediaProgress.hideFromContinueListening, - finishedAt: oldMediaProgress.finishedAt, - createdAt: oldMediaProgress.startedAt || oldMediaProgress.lastUpdate, - updatedAt: oldMediaProgress.lastUpdate, - extraData: { - libraryItemId: oldMediaProgress.libraryItemId, - progress: oldMediaProgress.progress - } - } - } - static removeById(mediaProgressId) { return this.destroy({ where: { @@ -71,12 +44,6 @@ class MediaProgress extends Model { }) } - getMediaItem(options) { - if (!this.mediaItemType) return Promise.resolve(null) - const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}` - return this[mixinMethodName](options) - } - /** * Initialize model * @@ -162,6 +129,12 @@ class MediaProgress extends Model { MediaProgress.belongsTo(user) } + getMediaItem(options) { + if (!this.mediaItemType) return Promise.resolve(null) + const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}` + return this[mixinMethodName](options) + } + getOldMediaProgress() { const isPodcastEpisode = this.mediaItemType === 'podcastEpisode' diff --git a/server/models/Podcast.js b/server/models/Podcast.js index aa7afbac..084911bf 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -66,66 +66,6 @@ class Podcast extends Model { this.podcastEpisodes } - static getOldPodcast(libraryItemExpanded) { - const podcastExpanded = libraryItemExpanded.media - const podcastEpisodes = podcastExpanded.podcastEpisodes?.map((ep) => ep.getOldPodcastEpisode(libraryItemExpanded.id).toJSON()).sort((a, b) => a.index - b.index) - return { - id: podcastExpanded.id, - libraryItemId: libraryItemExpanded.id, - metadata: { - title: podcastExpanded.title, - author: podcastExpanded.author, - description: podcastExpanded.description, - releaseDate: podcastExpanded.releaseDate, - genres: podcastExpanded.genres, - feedUrl: podcastExpanded.feedURL, - imageUrl: podcastExpanded.imageURL, - itunesPageUrl: podcastExpanded.itunesPageURL, - itunesId: podcastExpanded.itunesId, - itunesArtistId: podcastExpanded.itunesArtistId, - explicit: podcastExpanded.explicit, - language: podcastExpanded.language, - type: podcastExpanded.podcastType - }, - coverPath: podcastExpanded.coverPath, - tags: podcastExpanded.tags, - episodes: podcastEpisodes || [], - autoDownloadEpisodes: podcastExpanded.autoDownloadEpisodes, - autoDownloadSchedule: podcastExpanded.autoDownloadSchedule, - lastEpisodeCheck: podcastExpanded.lastEpisodeCheck?.valueOf() || null, - maxEpisodesToKeep: podcastExpanded.maxEpisodesToKeep, - maxNewEpisodesToDownload: podcastExpanded.maxNewEpisodesToDownload - } - } - - static getFromOld(oldPodcast) { - const oldPodcastMetadata = oldPodcast.metadata - return { - id: oldPodcast.id, - title: oldPodcastMetadata.title, - titleIgnorePrefix: oldPodcastMetadata.titleIgnorePrefix, - author: oldPodcastMetadata.author, - releaseDate: oldPodcastMetadata.releaseDate, - feedURL: oldPodcastMetadata.feedUrl, - imageURL: oldPodcastMetadata.imageUrl, - description: oldPodcastMetadata.description, - itunesPageURL: oldPodcastMetadata.itunesPageUrl, - itunesId: oldPodcastMetadata.itunesId, - itunesArtistId: oldPodcastMetadata.itunesArtistId, - language: oldPodcastMetadata.language, - podcastType: oldPodcastMetadata.type, - explicit: !!oldPodcastMetadata.explicit, - autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes, - autoDownloadSchedule: oldPodcast.autoDownloadSchedule, - lastEpisodeCheck: oldPodcast.lastEpisodeCheck, - maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep, - maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload, - coverPath: oldPodcast.coverPath, - tags: oldPodcast.tags, - genres: oldPodcastMetadata.genres - } - } - /** * Payload from the /api/podcasts POST endpoint * diff --git a/server/models/PodcastEpisode.js b/server/models/PodcastEpisode.js index c1e66fdf..c6a1b9fa 100644 --- a/server/models/PodcastEpisode.js +++ b/server/models/PodcastEpisode.js @@ -1,5 +1,4 @@ const { DataTypes, Model } = require('sequelize') -const oldPodcastEpisode = require('../objects/entities/PodcastEpisode') /** * @typedef ChapterObject @@ -53,40 +52,6 @@ class PodcastEpisode extends Model { this.updatedAt } - static createFromOld(oldEpisode) { - const podcastEpisode = this.getFromOld(oldEpisode) - return this.create(podcastEpisode) - } - - static getFromOld(oldEpisode) { - const extraData = {} - if (oldEpisode.oldEpisodeId) { - extraData.oldEpisodeId = oldEpisode.oldEpisodeId - } - if (oldEpisode.guid) { - extraData.guid = oldEpisode.guid - } - return { - id: oldEpisode.id, - index: oldEpisode.index, - season: oldEpisode.season, - episode: oldEpisode.episode, - episodeType: oldEpisode.episodeType, - title: oldEpisode.title, - subtitle: oldEpisode.subtitle, - description: oldEpisode.description, - pubDate: oldEpisode.pubDate, - enclosureURL: oldEpisode.enclosure?.url || null, - enclosureSize: oldEpisode.enclosure?.length || null, - enclosureType: oldEpisode.enclosure?.type || null, - publishedAt: oldEpisode.publishedAt, - podcastId: oldEpisode.podcastId, - audioFile: oldEpisode.audioFile?.toJSON() || null, - chapters: oldEpisode.chapters, - extraData - } - } - /** * * @param {import('../utils/podcastUtils').RssPodcastEpisode} rssPodcastEpisode @@ -208,42 +173,6 @@ class PodcastEpisode extends Model { return track } - /** - * @param {string} libraryItemId - * @returns {oldPodcastEpisode} - */ - getOldPodcastEpisode(libraryItemId = null) { - let enclosure = null - if (this.enclosureURL) { - enclosure = { - url: this.enclosureURL, - type: this.enclosureType, - length: this.enclosureSize !== null ? String(this.enclosureSize) : null - } - } - return new oldPodcastEpisode({ - libraryItemId: libraryItemId || null, - podcastId: this.podcastId, - id: this.id, - oldEpisodeId: this.extraData?.oldEpisodeId || null, - index: this.index, - season: this.season, - episode: this.episode, - episodeType: this.episodeType, - title: this.title, - subtitle: this.subtitle, - description: this.description, - enclosure, - guid: this.extraData?.guid || null, - pubDate: this.pubDate, - chapters: this.chapters, - audioFile: this.audioFile, - publishedAt: this.publishedAt?.valueOf() || null, - addedAt: this.createdAt.valueOf(), - updatedAt: this.updatedAt.valueOf() - }) - } - toOldJSON(libraryItemId) { if (!libraryItemId) { throw new Error(`[PodcastEpisode] Cannot convert to old JSON because libraryItemId is not provided`) diff --git a/server/models/User.js b/server/models/User.js index b2a4fd2b..56d6ba0e 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -563,9 +563,8 @@ class User extends Model { /** * Check user can access library item - * TODO: Currently supports both old and new library item models * - * @param {import('../objects/LibraryItem')|import('./LibraryItem')} libraryItem + * @param {import('./LibraryItem')} libraryItem * @returns {boolean} */ checkCanAccessLibraryItem(libraryItem) { diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js deleted file mode 100644 index 3cf89b10..00000000 --- a/server/objects/LibraryItem.js +++ /dev/null @@ -1,153 +0,0 @@ -const fs = require('../libs/fsExtra') -const Path = require('path') -const Logger = require('../Logger') -const LibraryFile = require('./files/LibraryFile') -const Book = require('./mediaTypes/Book') -const Podcast = require('./mediaTypes/Podcast') -const { areEquivalent, copyValue } = require('../utils/index') -const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') - -class LibraryItem { - constructor(libraryItem = null) { - this.id = null - this.ino = null // Inode - this.oldLibraryItemId = null - - this.libraryId = null - this.folderId = null - - this.path = null - this.relPath = null - this.isFile = false - this.mtimeMs = null - this.ctimeMs = null - this.birthtimeMs = null - this.addedAt = null - this.updatedAt = null - this.lastScan = null - this.scanVersion = null - - // Was scanned and no longer exists - this.isMissing = false - // Was scanned and no longer has media files - this.isInvalid = false - - this.mediaType = null - this.media = null - - /** @type {LibraryFile[]} */ - this.libraryFiles = [] - - if (libraryItem) { - this.construct(libraryItem) - } - - // Temporary attributes - this.isSavingMetadata = false - } - - construct(libraryItem) { - this.id = libraryItem.id - this.ino = libraryItem.ino || null - this.oldLibraryItemId = libraryItem.oldLibraryItemId - this.libraryId = libraryItem.libraryId - this.folderId = libraryItem.folderId - this.path = libraryItem.path - this.relPath = libraryItem.relPath - this.isFile = !!libraryItem.isFile - this.mtimeMs = libraryItem.mtimeMs || 0 - this.ctimeMs = libraryItem.ctimeMs || 0 - this.birthtimeMs = libraryItem.birthtimeMs || 0 - this.addedAt = libraryItem.addedAt - this.updatedAt = libraryItem.updatedAt || this.addedAt - this.lastScan = libraryItem.lastScan || null - this.scanVersion = libraryItem.scanVersion || null - - this.isMissing = !!libraryItem.isMissing - this.isInvalid = !!libraryItem.isInvalid - - this.mediaType = libraryItem.mediaType - if (this.mediaType === 'book') { - this.media = new Book(libraryItem.media) - } else if (this.mediaType === 'podcast') { - this.media = new Podcast(libraryItem.media) - } - this.media.libraryItemId = this.id - - this.libraryFiles = libraryItem.libraryFiles.map((f) => new LibraryFile(f)) - - // Migration for v2.2.23 to set ebook library files as supplementary - if (this.isBook && this.media.ebookFile) { - for (const libraryFile of this.libraryFiles) { - if (libraryFile.isEBookFile && libraryFile.isSupplementary === null) { - libraryFile.isSupplementary = this.media.ebookFile.ino !== libraryFile.ino - } - } - } - } - - toJSON() { - return { - id: this.id, - ino: this.ino, - oldLibraryItemId: this.oldLibraryItemId, - libraryId: this.libraryId, - folderId: this.folderId, - path: this.path, - relPath: this.relPath, - isFile: this.isFile, - mtimeMs: this.mtimeMs, - ctimeMs: this.ctimeMs, - birthtimeMs: this.birthtimeMs, - addedAt: this.addedAt, - updatedAt: this.updatedAt, - lastScan: this.lastScan, - scanVersion: this.scanVersion, - isMissing: !!this.isMissing, - isInvalid: !!this.isInvalid, - mediaType: this.mediaType, - media: this.media.toJSON(), - libraryFiles: this.libraryFiles.map((f) => f.toJSON()) - } - } - - toJSONMinified() { - return { - id: this.id, - ino: this.ino, - oldLibraryItemId: this.oldLibraryItemId, - libraryId: this.libraryId, - folderId: this.folderId, - path: this.path, - relPath: this.relPath, - isFile: this.isFile, - mtimeMs: this.mtimeMs, - ctimeMs: this.ctimeMs, - birthtimeMs: this.birthtimeMs, - addedAt: this.addedAt, - updatedAt: this.updatedAt, - isMissing: !!this.isMissing, - isInvalid: !!this.isInvalid, - mediaType: this.mediaType, - media: this.media.toJSONMinified(), - numFiles: this.libraryFiles.length, - size: this.size - } - } - - get isPodcast() { - return this.mediaType === 'podcast' - } - get isBook() { - return this.mediaType === 'book' - } - get size() { - let total = 0 - this.libraryFiles.forEach((lf) => (total += lf.metadata.size)) - return total - } - get hasAudioFiles() { - return this.libraryFiles.some((lf) => lf.fileType === 'audio') - } -} -module.exports = LibraryItem diff --git a/server/objects/entities/PodcastEpisode.js b/server/objects/entities/PodcastEpisode.js deleted file mode 100644 index 6a3f4cf6..00000000 --- a/server/objects/entities/PodcastEpisode.js +++ /dev/null @@ -1,149 +0,0 @@ -const { areEquivalent, copyValue } = require('../../utils/index') -const AudioFile = require('../files/AudioFile') -const AudioTrack = require('../files/AudioTrack') - -class PodcastEpisode { - constructor(episode) { - this.libraryItemId = null - this.podcastId = null - this.id = null - this.oldEpisodeId = null - this.index = null - - this.season = null - this.episode = null - this.episodeType = null - this.title = null - this.subtitle = null - this.description = null - this.enclosure = null - this.guid = null - this.pubDate = null - this.chapters = [] - - this.audioFile = null - this.publishedAt = null - this.addedAt = null - this.updatedAt = null - - if (episode) { - this.construct(episode) - } - } - - construct(episode) { - this.libraryItemId = episode.libraryItemId - this.podcastId = episode.podcastId - this.id = episode.id - this.oldEpisodeId = episode.oldEpisodeId - this.index = episode.index - this.season = episode.season - this.episode = episode.episode - this.episodeType = episode.episodeType - this.title = episode.title - this.subtitle = episode.subtitle - this.description = episode.description - this.enclosure = episode.enclosure ? { ...episode.enclosure } : null - this.guid = episode.guid || null - this.pubDate = episode.pubDate - this.chapters = episode.chapters?.map((ch) => ({ ...ch })) || [] - this.audioFile = episode.audioFile ? new AudioFile(episode.audioFile) : null - this.publishedAt = episode.publishedAt - this.addedAt = episode.addedAt - this.updatedAt = episode.updatedAt - - if (this.audioFile) { - this.audioFile.index = 1 // Only 1 audio file per episode - } - } - - toJSON() { - return { - libraryItemId: this.libraryItemId, - podcastId: this.podcastId, - id: this.id, - oldEpisodeId: this.oldEpisodeId, - index: this.index, - season: this.season, - episode: this.episode, - episodeType: this.episodeType, - title: this.title, - subtitle: this.subtitle, - description: this.description, - enclosure: this.enclosure ? { ...this.enclosure } : null, - guid: this.guid, - pubDate: this.pubDate, - chapters: this.chapters.map((ch) => ({ ...ch })), - audioFile: this.audioFile?.toJSON() || null, - publishedAt: this.publishedAt, - addedAt: this.addedAt, - updatedAt: this.updatedAt - } - } - - toJSONExpanded() { - return { - libraryItemId: this.libraryItemId, - podcastId: this.podcastId, - id: this.id, - oldEpisodeId: this.oldEpisodeId, - index: this.index, - season: this.season, - episode: this.episode, - episodeType: this.episodeType, - title: this.title, - subtitle: this.subtitle, - description: this.description, - enclosure: this.enclosure ? { ...this.enclosure } : null, - guid: this.guid, - pubDate: this.pubDate, - chapters: this.chapters.map((ch) => ({ ...ch })), - audioFile: this.audioFile?.toJSON() || null, - audioTrack: this.audioTrack?.toJSON() || null, - publishedAt: this.publishedAt, - addedAt: this.addedAt, - updatedAt: this.updatedAt, - duration: this.duration, - size: this.size - } - } - - get audioTrack() { - if (!this.audioFile) return null - const audioTrack = new AudioTrack() - audioTrack.setData(this.libraryItemId, this.audioFile, 0) - return audioTrack - } - get tracks() { - return [this.audioTrack] - } - get duration() { - return this.audioFile?.duration || 0 - } - get size() { - return this.audioFile?.metadata.size || 0 - } - get enclosureUrl() { - return this.enclosure?.url || null - } - - update(payload) { - let hasUpdates = false - for (const key in this.toJSON()) { - let newValue = payload[key] - if (newValue === '') newValue = null - let existingValue = this[key] - if (existingValue === '') existingValue = null - - if (newValue != undefined && !areEquivalent(newValue, existingValue)) { - this[key] = copyValue(newValue) - hasUpdates = true - } - } - if (hasUpdates) { - this.updatedAt = Date.now() - } - return hasUpdates - } -} -module.exports = PodcastEpisode diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js deleted file mode 100644 index b270e0e7..00000000 --- a/server/objects/mediaTypes/Book.js +++ /dev/null @@ -1,138 +0,0 @@ -const Logger = require('../../Logger') -const BookMetadata = require('../metadata/BookMetadata') -const { areEquivalent, copyValue } = require('../../utils/index') -const { filePathToPOSIX } = require('../../utils/fileUtils') -const AudioFile = require('../files/AudioFile') -const AudioTrack = require('../files/AudioTrack') -const EBookFile = require('../files/EBookFile') - -class Book { - constructor(book) { - this.id = null - this.libraryItemId = null - this.metadata = null - - this.coverPath = null - this.tags = [] - - this.audioFiles = [] - this.chapters = [] - this.ebookFile = null - - this.lastCoverSearch = null - this.lastCoverSearchQuery = null - - if (book) { - this.construct(book) - } - } - - construct(book) { - this.id = book.id - this.libraryItemId = book.libraryItemId - 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.ebookFile = book.ebookFile ? new EBookFile(book.ebookFile) : null - this.lastCoverSearch = book.lastCoverSearch || null - this.lastCoverSearchQuery = book.lastCoverSearchQuery || null - } - - toJSON() { - return { - id: this.id, - libraryItemId: this.libraryItemId, - metadata: this.metadata.toJSON(), - coverPath: this.coverPath, - tags: [...this.tags], - audioFiles: this.audioFiles.map((f) => f.toJSON()), - chapters: this.chapters.map((c) => ({ ...c })), - ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null - } - } - - toJSONMinified() { - return { - id: this.id, - metadata: this.metadata.toJSONMinified(), - coverPath: this.coverPath, - tags: [...this.tags], - numTracks: this.tracks.length, - numAudioFiles: this.audioFiles.length, - numChapters: this.chapters.length, - duration: this.duration, - size: this.size, - ebookFormat: this.ebookFile?.ebookFormat - } - } - - toJSONForMetadataFile() { - return { - tags: [...this.tags], - chapters: this.chapters.map((c) => ({ ...c })), - ...this.metadata.toJSONForMetadataFile() - } - } - - get size() { - var total = 0 - this.audioFiles.forEach((af) => (total += af.metadata.size)) - if (this.ebookFile) { - total += this.ebookFile.metadata.size - } - return total - } - get includedAudioFiles() { - return this.audioFiles.filter((af) => !af.exclude) - } - get tracks() { - let startOffset = 0 - return this.includedAudioFiles.map((af) => { - const audioTrack = new AudioTrack() - audioTrack.setData(this.libraryItemId, af, startOffset) - startOffset += audioTrack.duration - return audioTrack - }) - } - get duration() { - let total = 0 - this.tracks.forEach((track) => (total += track.duration)) - return total - } - get numTracks() { - return this.tracks.length - } - get isEBookOnly() { - return this.ebookFile && !this.numTracks - } - - update(payload) { - const json = this.toJSON() - - let hasUpdates = false - for (const key in json) { - if (payload[key] !== undefined) { - if (key === 'metadata') { - if (this.metadata.update(payload.metadata)) { - hasUpdates = true - } - } else if (!areEquivalent(payload[key], json[key])) { - this[key] = copyValue(payload[key]) - Logger.debug('[Book] Key updated', key, this[key]) - hasUpdates = true - } - } - } - return hasUpdates - } - - updateCover(coverPath) { - coverPath = filePathToPOSIX(coverPath) - if (this.coverPath === coverPath) return false - this.coverPath = coverPath - return true - } -} -module.exports = Book diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js deleted file mode 100644 index 2ec4a873..00000000 --- a/server/objects/mediaTypes/Podcast.js +++ /dev/null @@ -1,161 +0,0 @@ -const Logger = require('../../Logger') -const PodcastEpisode = require('../entities/PodcastEpisode') -const PodcastMetadata = require('../metadata/PodcastMetadata') -const { areEquivalent, copyValue } = require('../../utils/index') -const { filePathToPOSIX } = require('../../utils/fileUtils') - -class Podcast { - constructor(podcast) { - this.id = null - this.libraryItemId = null - this.metadata = null - this.coverPath = null - this.tags = [] - this.episodes = [] - - this.autoDownloadEpisodes = false - this.autoDownloadSchedule = null - this.lastEpisodeCheck = 0 - this.maxEpisodesToKeep = 0 - this.maxNewEpisodesToDownload = 3 - - this.lastCoverSearch = null - this.lastCoverSearchQuery = null - - if (podcast) { - this.construct(podcast) - } - } - - construct(podcast) { - this.id = podcast.id - this.libraryItemId = podcast.libraryItemId - this.metadata = new PodcastMetadata(podcast.metadata) - this.coverPath = podcast.coverPath - this.tags = [...podcast.tags] - this.episodes = podcast.episodes.map((e) => { - var podcastEpisode = new PodcastEpisode(e) - podcastEpisode.libraryItemId = this.libraryItemId - return podcastEpisode - }) - this.autoDownloadEpisodes = !!podcast.autoDownloadEpisodes - this.autoDownloadSchedule = podcast.autoDownloadSchedule || '0 * * * *' // Added in 2.1.3 so default to hourly - this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0 - this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0 - - // Default is 3 but 0 is allowed - if (typeof podcast.maxNewEpisodesToDownload !== 'number') { - this.maxNewEpisodesToDownload = 3 - } else { - this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload - } - } - - toJSON() { - return { - id: this.id, - libraryItemId: this.libraryItemId, - metadata: this.metadata.toJSON(), - coverPath: this.coverPath, - tags: [...this.tags], - episodes: this.episodes.map((e) => e.toJSON()), - autoDownloadEpisodes: this.autoDownloadEpisodes, - autoDownloadSchedule: this.autoDownloadSchedule, - lastEpisodeCheck: this.lastEpisodeCheck, - maxEpisodesToKeep: this.maxEpisodesToKeep, - maxNewEpisodesToDownload: this.maxNewEpisodesToDownload - } - } - - toJSONMinified() { - return { - id: this.id, - metadata: this.metadata.toJSONMinified(), - coverPath: this.coverPath, - tags: [...this.tags], - numEpisodes: this.episodes.length, - autoDownloadEpisodes: this.autoDownloadEpisodes, - autoDownloadSchedule: this.autoDownloadSchedule, - lastEpisodeCheck: this.lastEpisodeCheck, - maxEpisodesToKeep: this.maxEpisodesToKeep, - maxNewEpisodesToDownload: this.maxNewEpisodesToDownload, - size: this.size - } - } - - toJSONForMetadataFile() { - return { - tags: [...this.tags], - title: this.metadata.title, - author: this.metadata.author, - description: this.metadata.description, - releaseDate: this.metadata.releaseDate, - genres: [...this.metadata.genres], - feedURL: this.metadata.feedUrl, - imageURL: this.metadata.imageUrl, - itunesPageURL: this.metadata.itunesPageUrl, - itunesId: this.metadata.itunesId, - itunesArtistId: this.metadata.itunesArtistId, - explicit: this.metadata.explicit, - language: this.metadata.language, - podcastType: this.metadata.type - } - } - - get size() { - var total = 0 - this.episodes.forEach((ep) => (total += ep.size)) - return total - } - get duration() { - let total = 0 - this.episodes.forEach((ep) => (total += ep.duration)) - return total - } - get numTracks() { - return this.episodes.length - } - - update(payload) { - var json = this.toJSON() - delete json.episodes // do not update media entities here - var hasUpdates = false - for (const key in json) { - if (payload[key] !== undefined) { - if (key === 'metadata') { - if (this.metadata.update(payload.metadata)) { - hasUpdates = true - } - } else if (!areEquivalent(payload[key], json[key])) { - this[key] = copyValue(payload[key]) - Logger.debug('[Podcast] Key updated', key, this[key]) - hasUpdates = true - } - } - } - return hasUpdates - } - - updateEpisode(id, payload) { - var episode = this.episodes.find((ep) => ep.id == id) - if (!episode) return false - return episode.update(payload) - } - - updateCover(coverPath) { - coverPath = filePathToPOSIX(coverPath) - if (this.coverPath === coverPath) return false - this.coverPath = coverPath - return true - } - - getEpisode(episodeId) { - if (!episodeId) return null - - // Support old episode ids for mobile downloads - if (episodeId.startsWith('ep_')) return this.episodes.find((ep) => ep.oldEpisodeId == episodeId) - - return this.episodes.find((ep) => ep.id == episodeId) - } -} -module.exports = Podcast diff --git a/server/objects/metadata/BookMetadata.js b/server/objects/metadata/BookMetadata.js deleted file mode 100644 index 5116f2f4..00000000 --- a/server/objects/metadata/BookMetadata.js +++ /dev/null @@ -1,154 +0,0 @@ -const Logger = require('../../Logger') -const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index') -const parseNameString = require('../../utils/parsers/parseNameString') -class BookMetadata { - constructor(metadata) { - this.title = null - this.subtitle = null - this.authors = [] - this.narrators = [] // Array of strings - this.series = [] - this.genres = [] // Array of strings - this.publishedYear = null - this.publishedDate = null - this.publisher = null - this.description = null - this.isbn = null - this.asin = null - this.language = null - this.explicit = false - this.abridged = false - - if (metadata) { - this.construct(metadata) - } - } - - construct(metadata) { - this.title = metadata.title - this.subtitle = metadata.subtitle - this.authors = metadata.authors?.map ? metadata.authors.map((a) => ({ ...a })) : [] - this.narrators = metadata.narrators ? [...metadata.narrators].filter((n) => n) : [] - this.series = metadata.series?.map - ? metadata.series.map((s) => ({ - ...s, - name: s.name || 'No Title' - })) - : [] - this.genres = metadata.genres ? [...metadata.genres] : [] - this.publishedYear = metadata.publishedYear || null - this.publishedDate = metadata.publishedDate || null - this.publisher = metadata.publisher - this.description = metadata.description - this.isbn = metadata.isbn - this.asin = metadata.asin - this.language = metadata.language - this.explicit = !!metadata.explicit - this.abridged = !!metadata.abridged - } - - toJSON() { - return { - title: this.title, - subtitle: this.subtitle, - authors: this.authors.map((a) => ({ ...a })), // Author JSONMinimal with name and id - narrators: [...this.narrators], - series: this.series.map((s) => ({ ...s })), // Series JSONMinimal with name, id and sequence - genres: [...this.genres], - publishedYear: this.publishedYear, - publishedDate: this.publishedDate, - publisher: this.publisher, - description: this.description, - isbn: this.isbn, - asin: this.asin, - language: this.language, - explicit: this.explicit, - abridged: this.abridged - } - } - - toJSONMinified() { - return { - title: this.title, - titleIgnorePrefix: this.titlePrefixAtEnd, - subtitle: this.subtitle, - authorName: this.authorName, - authorNameLF: this.authorNameLF, - narratorName: this.narratorName, - seriesName: this.seriesName, - genres: [...this.genres], - publishedYear: this.publishedYear, - publishedDate: this.publishedDate, - publisher: this.publisher, - description: this.description, - isbn: this.isbn, - asin: this.asin, - language: this.language, - explicit: this.explicit, - abridged: this.abridged - } - } - - toJSONForMetadataFile() { - const json = this.toJSON() - json.authors = json.authors.map((au) => au.name) - json.series = json.series.map((se) => { - if (!se.sequence) return se.name - return `${se.name} #${se.sequence}` - }) - return json - } - - clone() { - return new BookMetadata(this.toJSON()) - } - - get titleIgnorePrefix() { - return getTitleIgnorePrefix(this.title) - } - get titlePrefixAtEnd() { - return getTitlePrefixAtEnd(this.title) - } - get authorName() { - if (!this.authors.length) return '' - return this.authors.map((au) => au.name).join(', ') - } - get authorNameLF() { - // Last, First - if (!this.authors.length) return '' - return this.authors.map((au) => parseNameString.nameToLastFirst(au.name)).join(', ') - } - get seriesName() { - if (!this.series.length) return '' - return this.series - .map((se) => { - if (!se.sequence) return se.name - return `${se.name} #${se.sequence}` - }) - .join(', ') - } - get narratorName() { - return this.narrators.join(', ') - } - - getSeries(seriesId) { - return this.series.find((se) => se.id == seriesId) - } - - update(payload) { - const json = this.toJSON() - let hasUpdates = false - - for (const key in json) { - if (payload[key] !== undefined) { - if (!areEquivalent(payload[key], json[key])) { - this[key] = copyValue(payload[key]) - Logger.debug('[BookMetadata] Key updated', key, this[key]) - hasUpdates = true - } - } - } - return hasUpdates - } -} -module.exports = BookMetadata diff --git a/server/objects/metadata/PodcastMetadata.js b/server/objects/metadata/PodcastMetadata.js deleted file mode 100644 index ccc94ce0..00000000 --- a/server/objects/metadata/PodcastMetadata.js +++ /dev/null @@ -1,105 +0,0 @@ -const Logger = require('../../Logger') -const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index') - -class PodcastMetadata { - constructor(metadata) { - this.title = null - this.author = null - this.description = null - this.releaseDate = null - this.genres = [] - this.feedUrl = null - this.imageUrl = null - this.itunesPageUrl = null - this.itunesId = null - this.itunesArtistId = null - this.explicit = false - this.language = null - this.type = null - - if (metadata) { - this.construct(metadata) - } - } - - construct(metadata) { - this.title = metadata.title - this.author = metadata.author - this.description = metadata.description - this.releaseDate = metadata.releaseDate - this.genres = [...metadata.genres] - this.feedUrl = metadata.feedUrl - this.imageUrl = metadata.imageUrl - this.itunesPageUrl = metadata.itunesPageUrl - this.itunesId = metadata.itunesId - this.itunesArtistId = metadata.itunesArtistId - this.explicit = metadata.explicit - this.language = metadata.language || null - this.type = metadata.type || 'episodic' - } - - toJSON() { - return { - title: this.title, - author: this.author, - description: this.description, - releaseDate: this.releaseDate, - genres: [...this.genres], - feedUrl: this.feedUrl, - imageUrl: this.imageUrl, - itunesPageUrl: this.itunesPageUrl, - itunesId: this.itunesId, - itunesArtistId: this.itunesArtistId, - explicit: this.explicit, - language: this.language, - type: this.type - } - } - - toJSONMinified() { - return { - title: this.title, - titleIgnorePrefix: this.titlePrefixAtEnd, - author: this.author, - description: this.description, - releaseDate: this.releaseDate, - genres: [...this.genres], - feedUrl: this.feedUrl, - imageUrl: this.imageUrl, - itunesPageUrl: this.itunesPageUrl, - itunesId: this.itunesId, - itunesArtistId: this.itunesArtistId, - explicit: this.explicit, - language: this.language, - type: this.type - } - } - - clone() { - return new PodcastMetadata(this.toJSON()) - } - - get titleIgnorePrefix() { - return getTitleIgnorePrefix(this.title) - } - - get titlePrefixAtEnd() { - return getTitlePrefixAtEnd(this.title) - } - - update(payload) { - const json = this.toJSON() - let hasUpdates = false - for (const key in json) { - if (payload[key] !== undefined) { - if (!areEquivalent(payload[key], json[key])) { - this[key] = copyValue(payload[key]) - Logger.debug('[PodcastMetadata] Key updated', key, this[key]) - hasUpdates = true - } - } - } - return hasUpdates - } -} -module.exports = PodcastMetadata diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 5d4e1cc5..1a2a7aaf 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -32,7 +32,7 @@ class Scanner { * @param {import('../routers/ApiRouter')} apiRouterCtx * @param {import('../models/LibraryItem')} libraryItem * @param {QuickMatchOptions} options - * @returns {Promise<{updated: boolean, libraryItem: import('../objects/LibraryItem')}>} + * @returns {Promise<{updated: boolean, libraryItem: Object}>} */ async quickMatchLibraryItem(apiRouterCtx, libraryItem, options = {}) { const provider = options.provider || 'google' diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index 8771ae7a..f81f889c 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -5,7 +5,6 @@ const fs = require('../libs/fsExtra') const Path = require('path') const Logger = require('../Logger') const { filePathToPOSIX, copyToExisting } = require('./fileUtils') -const LibraryItem = require('../objects/LibraryItem') function escapeSingleQuotes(path) { // A ' within a quoted string is escaped with '\'' in ffmpeg (see https://www.ffmpeg.org/ffmpeg-utils.html#Quoting-and-escaping) @@ -365,28 +364,26 @@ function escapeFFMetadataValue(value) { /** * Retrieves the FFmpeg metadata object for a given library item. * - * @param {LibraryItem} libraryItem - The library item containing the media metadata. + * @param {import('../models/LibraryItem')} libraryItem - The library item containing the media metadata. * @param {number} audioFilesLength - The length of the audio files. * @returns {Object} - The FFmpeg metadata object. */ function getFFMetadataObject(libraryItem, audioFilesLength) { - const metadata = libraryItem.media.metadata - const ffmetadata = { - title: metadata.title, - artist: metadata.authorName, - album_artist: metadata.authorName, - album: (metadata.title || '') + (metadata.subtitle ? `: ${metadata.subtitle}` : ''), - TIT3: metadata.subtitle, // mp3 only - genre: metadata.genres?.join('; '), - date: metadata.publishedYear, - comment: metadata.description, - description: metadata.description, - composer: metadata.narratorName, - copyright: metadata.publisher, - publisher: metadata.publisher, // mp3 only + title: libraryItem.media.title, + artist: libraryItem.media.authorName, + album_artist: libraryItem.media.authorName, + album: (libraryItem.media.title || '') + (libraryItem.media.subtitle ? `: ${libraryItem.media.subtitle}` : ''), + TIT3: libraryItem.media.subtitle, // mp3 only + genre: libraryItem.media.genres?.join('; '), + date: libraryItem.media.publishedYear, + comment: libraryItem.media.description, + description: libraryItem.media.description, + composer: (libraryItem.media.narrators || []).join(', '), + copyright: libraryItem.media.publisher, + publisher: libraryItem.media.publisher, // mp3 only TRACKTOTAL: `${audioFilesLength}`, // mp3 only - grouping: metadata.series?.map((s) => s.name + (s.sequence ? ` #${s.sequence}` : '')).join('; ') + grouping: libraryItem.media.series?.map((s) => s.name + (s.bookSeries.sequence ? ` #${s.bookSeries.sequence}` : '')).join('; ') } Object.keys(ffmetadata).forEach((key) => { if (!ffmetadata[key]) { @@ -402,7 +399,7 @@ module.exports.getFFMetadataObject = getFFMetadataObject /** * Merges audio files into a single output file using FFmpeg. * - * @param {Array} audioTracks - The audio tracks to merge. + * @param {import('../models/Book').AudioFileObject} audioTracks - The audio tracks to merge. * @param {number} duration - The total duration of the audio tracks. * @param {string} itemCachePath - The path to the item cache. * @param {string} outputFilePath - The path to the output file. diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js index 664bd6e3..5702071e 100644 --- a/server/utils/libraryHelpers.js +++ b/server/utils/libraryHelpers.js @@ -6,35 +6,41 @@ const naturalSort = createNewSortInstance({ }) module.exports = { - getSeriesFromBooks(books, filterSeries, hideSingleBookSeries) { + /** + * + * @param {import('../models/LibraryItem')[]} libraryItems + * @param {*} filterSeries + * @param {*} hideSingleBookSeries + * @returns + */ + getSeriesFromBooks(libraryItems, filterSeries, hideSingleBookSeries) { const _series = {} const seriesToFilterOut = {} - books.forEach((libraryItem) => { + libraryItems.forEach((libraryItem) => { // get all book series for item that is not already filtered out - const bookSeries = (libraryItem.media.metadata.series || []).filter((se) => !seriesToFilterOut[se.id]) - if (!bookSeries.length) return + const allBookSeries = (libraryItem.media.series || []).filter((se) => !seriesToFilterOut[se.id]) + if (!allBookSeries.length) return - bookSeries.forEach((bookSeriesObj) => { - // const series = allSeries.find(se => se.id === bookSeriesObj.id) - - const abJson = libraryItem.toJSONMinified() - abJson.sequence = bookSeriesObj.sequence + allBookSeries.forEach((bookSeries) => { + const abJson = libraryItem.toOldJSONMinified() + abJson.sequence = bookSeries.bookSeries.sequence if (filterSeries) { - abJson.filterSeriesSequence = libraryItem.media.metadata.getSeries(filterSeries).sequence + const series = libraryItem.media.series.find((se) => se.id === filterSeries) + abJson.filterSeriesSequence = series.bookSeries.sequence } - if (!_series[bookSeriesObj.id]) { - _series[bookSeriesObj.id] = { - id: bookSeriesObj.id, - name: bookSeriesObj.name, - nameIgnorePrefix: getTitlePrefixAtEnd(bookSeriesObj.name), - nameIgnorePrefixSort: getTitleIgnorePrefix(bookSeriesObj.name), + if (!_series[bookSeries.id]) { + _series[bookSeries.id] = { + id: bookSeries.id, + name: bookSeries.name, + nameIgnorePrefix: getTitlePrefixAtEnd(bookSeries.name), + nameIgnorePrefixSort: getTitleIgnorePrefix(bookSeries.name), type: 'series', books: [abJson], totalDuration: isNullOrNaN(abJson.media.duration) ? 0 : Number(abJson.media.duration) } } else { - _series[bookSeriesObj.id].books.push(abJson) - _series[bookSeriesObj.id].totalDuration += isNullOrNaN(abJson.media.duration) ? 0 : Number(abJson.media.duration) + _series[bookSeries.id].books.push(abJson) + _series[bookSeries.id].totalDuration += isNullOrNaN(abJson.media.duration) ? 0 : Number(abJson.media.duration) } }) }) @@ -52,6 +58,13 @@ module.exports = { }) }, + /** + * + * @param {import('../models/LibraryItem')[]} libraryItems + * @param {string} filterSeries - series id + * @param {boolean} hideSingleBookSeries + * @returns + */ collapseBookSeries(libraryItems, filterSeries, hideSingleBookSeries) { // Get series from the library items. If this list is being collapsed after filtering for a series, // don't collapse that series, only books that are in other series. @@ -123,8 +136,9 @@ module.exports = { let libraryItems = books .map((book) => { const libraryItem = book.libraryItem + delete book.libraryItem libraryItem.media = book - return Database.libraryItemModel.getOldLibraryItem(libraryItem) + return libraryItem }) .filter((li) => { return user.checkCanAccessLibraryItem(li) @@ -143,15 +157,18 @@ module.exports = { if (!payload.sortBy || payload.sortBy === 'sequence') { sortArray = [ { - [direction]: (li) => li.media.metadata.getSeries(seriesId).sequence + [direction]: (li) => { + const series = li.media.series.find((se) => se.id === seriesId) + return series.bookSeries.sequence + } }, { // If no series sequence then fallback to sorting by title (or collapsed series name for sub-series) [direction]: (li) => { if (sortingIgnorePrefix) { - return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix + return li.collapsedSeries?.nameIgnorePrefix || li.media.titleIgnorePrefix } else { - return li.collapsedSeries?.name || li.media.metadata.title + return li.collapsedSeries?.name || li.media.title } } } @@ -174,9 +191,9 @@ module.exports = { [direction]: (li) => { if (payload.sortBy === 'media.metadata.title') { if (sortingIgnorePrefix) { - return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix + return li.collapsedSeries?.nameIgnorePrefix || li.media.titleIgnorePrefix } else { - return li.collapsedSeries?.name || li.media.metadata.title + return li.collapsedSeries?.name || li.media.title } } else { return payload.sortBy.split('.').reduce((a, b) => a[b], li) @@ -194,12 +211,12 @@ module.exports = { return Promise.all( libraryItems.map(async (li) => { - const filteredSeries = li.media.metadata.getSeries(seriesId) - const json = li.toJSONMinified() + const filteredSeries = li.media.series.find((se) => se.id === seriesId) + const json = li.toOldJSONMinified() json.media.metadata.series = { id: filteredSeries.id, name: filteredSeries.name, - sequence: filteredSeries.sequence + sequence: filteredSeries.bookSeries.sequence } if (li.collapsedSeries) { diff --git a/server/utils/migrations/dbMigration.js b/server/utils/migrations/dbMigration.js index eb42c81c..1d4c4798 100644 --- a/server/utils/migrations/dbMigration.js +++ b/server/utils/migrations/dbMigration.js @@ -1200,7 +1200,7 @@ async function migrationPatchNewColumns(queryInterface) { */ async function handleOldLibraryItems(ctx) { const oldLibraryItems = await oldDbFiles.loadOldData('libraryItems') - const libraryItems = (await ctx.models.libraryItem.findAllExpandedWhere()).map((li) => ctx.models.libraryItem.getOldLibraryItem(li)) + const libraryItems = await ctx.models.libraryItem.findAllExpandedWhere() const bulkUpdateItems = [] const bulkUpdateEpisodes = [] @@ -1218,8 +1218,8 @@ async function handleOldLibraryItems(ctx) { } }) - if (libraryItem.media.episodes?.length && matchingOldLibraryItem.media.episodes?.length) { - for (const podcastEpisode of libraryItem.media.episodes) { + if (libraryItem.media.podcastEpisodes?.length && matchingOldLibraryItem.media.episodes?.length) { + for (const podcastEpisode of libraryItem.media.podcastEpisodes) { // Find matching old episode by audio file ino const matchingOldPodcastEpisode = matchingOldLibraryItem.media.episodes.find((oep) => oep.audioFile?.ino && oep.audioFile.ino === podcastEpisode.audioFile?.ino) if (matchingOldPodcastEpisode) { diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index 60c07805..5d5f0c83 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -415,7 +415,7 @@ module.exports = { * @param {import('../../models/User')} user * @param {number} limit * @param {number} offset - * @returns {Promise<{ libraryItems:import('../../objects/LibraryItem')[], count:number }>} + * @returns {Promise<{ libraryItems:import('../../models/LibraryItem')[], count:number }>} */ async getLibraryItemsForAuthor(author, user, limit, offset) { const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(author.libraryId, user, 'authors', author.id, 'addedAt', true, false, [], limit, offset) diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 0aaf6f4b..36241f33 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -297,7 +297,7 @@ module.exports = { delete podcast.libraryItem libraryItem.media = podcast - libraryItem.recentEpisode = ep.getOldPodcastEpisode(libraryItem.id).toJSON() + libraryItem.recentEpisode = ep.toOldJSON(libraryItem.id) return libraryItem }) @@ -460,13 +460,14 @@ module.exports = { }) const episodeResults = episodes.map((ep) => { - const libraryItem = ep.podcast.libraryItem - libraryItem.media = ep.podcast - const oldPodcast = Database.podcastModel.getOldPodcast(libraryItem) - const oldPodcastEpisode = ep.getOldPodcastEpisode(libraryItem.id).toJSONExpanded() - oldPodcastEpisode.podcast = oldPodcast - oldPodcastEpisode.libraryId = libraryItem.libraryId - return oldPodcastEpisode + ep.podcast.podcastEpisodes = [] // Not needed + const oldPodcastJson = ep.podcast.toOldJSON(ep.podcast.libraryItem.id) + + const oldPodcastEpisodeJson = ep.toOldJSONExpanded(ep.podcast.libraryItem.id) + + oldPodcastEpisodeJson.podcast = oldPodcastJson + oldPodcastEpisodeJson.libraryId = ep.podcast.libraryItem.libraryId + return oldPodcastEpisodeJson }) return episodeResults