From a98942a361c9ac249560c480314c6303a2133eae Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 16 Aug 2023 16:38:48 -0500 Subject: [PATCH] Add jsdoc types to remaining models --- server/Database.js | 20 +- server/models/LibraryItem.js | 1540 ++++++++++++++-------------- server/models/MediaProgress.js | 302 +++--- server/models/PlaybackSession.js | 402 ++++---- server/models/Playlist.js | 615 +++++------ server/models/PlaylistMediaItem.js | 173 ++-- server/models/Podcast.js | 239 +++-- server/models/PodcastEpisode.js | 231 +++-- server/models/Series.js | 157 +-- server/models/Setting.js | 81 +- server/models/User.js | 481 +++++---- 11 files changed, 2302 insertions(+), 1939 deletions(-) diff --git a/server/Database.js b/server/Database.js index 4d97574b..ebe6d4c7 100644 --- a/server/Database.js +++ b/server/Database.js @@ -92,27 +92,27 @@ class Database { } buildModels(force = false) { - require('./models/User')(this.sequelize) + require('./models/User').init(this.sequelize) require('./models/Library').init(this.sequelize) require('./models/LibraryFolder').init(this.sequelize) require('./models/Book').init(this.sequelize) - require('./models/Podcast')(this.sequelize) - require('./models/PodcastEpisode')(this.sequelize) - require('./models/LibraryItem')(this.sequelize) - require('./models/MediaProgress')(this.sequelize) - require('./models/Series')(this.sequelize) + require('./models/Podcast').init(this.sequelize) + require('./models/PodcastEpisode').init(this.sequelize) + require('./models/LibraryItem').init(this.sequelize) + require('./models/MediaProgress').init(this.sequelize) + require('./models/Series').init(this.sequelize) require('./models/BookSeries').init(this.sequelize) require('./models/Author').init(this.sequelize) require('./models/BookAuthor').init(this.sequelize) require('./models/Collection').init(this.sequelize) require('./models/CollectionBook').init(this.sequelize) - require('./models/Playlist')(this.sequelize) - require('./models/PlaylistMediaItem')(this.sequelize) + require('./models/Playlist').init(this.sequelize) + require('./models/PlaylistMediaItem').init(this.sequelize) require('./models/Device').init(this.sequelize) - require('./models/PlaybackSession')(this.sequelize) + require('./models/PlaybackSession').init(this.sequelize) require('./models/Feed').init(this.sequelize) require('./models/FeedEpisode').init(this.sequelize) - require('./models/Setting')(this.sequelize) + require('./models/Setting').init(this.sequelize) return this.sequelize.sync({ force, alter: false }) } diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index ce56590b..f91d7dd3 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -4,793 +4,845 @@ const oldLibraryItem = require('../objects/LibraryItem') const libraryFilters = require('../utils/queries/libraryFilters') const { areEquivalent } = require('../utils/index') -module.exports = (sequelize) => { - class LibraryItem extends Model { - /** - * Loads all podcast episodes, all library items in chunks of 500, then maps them to old library items - * @todo this is a temporary solution until we can use the sqlite without loading all the library items on init - * - * @returns {Promise} old library items - */ - static async loadAllLibraryItems() { - let start = Date.now() - Logger.info(`[LibraryItem] Loading podcast episodes...`) - const podcastEpisodes = await sequelize.models.podcastEpisode.findAll() - Logger.info(`[LibraryItem] Finished loading ${podcastEpisodes.length} podcast episodes in ${((Date.now() - start) / 1000).toFixed(2)}s`) - start = Date.now() - Logger.info(`[LibraryItem] Loading library items...`) - let libraryItems = await this.getAllOldLibraryItemsIncremental() - Logger.info(`[LibraryItem] Finished loading ${libraryItems.length} library items in ${((Date.now() - start) / 1000).toFixed(2)}s`) +class LibraryItem extends Model { + constructor(values, options) { + super(values, options) - // Map LibraryItem to old library item - libraryItems = libraryItems.map(li => { - if (li.mediaType === 'podcast') { - li.media.podcastEpisodes = podcastEpisodes.filter(pe => pe.podcastId === li.media.id) - } - return this.getOldLibraryItem(li) - }) + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.ino + /** @type {string} */ + this.path + /** @type {string} */ + this.relPath + /** @type {UUIDV4} */ + this.mediaId + /** @type {string} */ + this.mediaType + /** @type {boolean} */ + this.isFile + /** @type {boolean} */ + this.isMissing + /** @type {boolean} */ + this.isInvalid + /** @type {Date} */ + this.mtime + /** @type {Date} */ + this.ctime + /** @type {Date} */ + this.birthtime + /** @type {BigInt} */ + this.size + /** @type {Date} */ + this.lastScan + /** @type {string} */ + this.lastScanVersion + /** @type {Object} */ + this.libraryFiles + /** @type {Object} */ + this.extraData + /** @type {UUIDV4} */ + this.libraryId + /** @type {UUIDV4} */ + this.libraryFolderId + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + } + /** + * Loads all podcast episodes, all library items in chunks of 500, then maps them to old library items + * @todo this is a temporary solution until we can use the sqlite without loading all the library items on init + * + * @returns {Promise} old library items + */ + static async loadAllLibraryItems() { + let start = Date.now() + Logger.info(`[LibraryItem] Loading podcast episodes...`) + const podcastEpisodes = await this.sequelize.models.podcastEpisode.findAll() + Logger.info(`[LibraryItem] Finished loading ${podcastEpisodes.length} podcast episodes in ${((Date.now() - start) / 1000).toFixed(2)}s`) + + start = Date.now() + Logger.info(`[LibraryItem] Loading library items...`) + let libraryItems = await this.getAllOldLibraryItemsIncremental() + Logger.info(`[LibraryItem] Finished loading ${libraryItems.length} library items in ${((Date.now() - start) / 1000).toFixed(2)}s`) + + // Map LibraryItem to old library item + libraryItems = libraryItems.map(li => { + if (li.mediaType === 'podcast') { + li.media.podcastEpisodes = podcastEpisodes.filter(pe => pe.podcastId === li.media.id) + } + return this.getOldLibraryItem(li) + }) + + return libraryItems + } + + /** + * Loads all LibraryItem in batches of 500 + * @todo temporary solution + * + * @param {Model[]} libraryItems + * @param {number} offset + * @returns {Promise[]>} + */ + static async getAllOldLibraryItemsIncremental(libraryItems = [], offset = 0) { + const limit = 500 + const rows = await this.getLibraryItemsIncrement(offset, limit) + libraryItems.push(...rows) + if (!rows.length || rows.length < limit) { return libraryItems } + Logger.info(`[LibraryItem] Loaded ${rows.length} library items. ${libraryItems.length} loaded so far.`) + return this.getAllOldLibraryItemsIncremental(libraryItems, offset + rows.length) + } - /** - * Loads all LibraryItem in batches of 500 - * @todo temporary solution - * - * @param {Model[]} libraryItems - * @param {number} offset - * @returns {Promise[]>} - */ - static async getAllOldLibraryItemsIncremental(libraryItems = [], offset = 0) { - const limit = 500 - const rows = await this.getLibraryItemsIncrement(offset, limit) - libraryItems.push(...rows) - if (!rows.length || rows.length < limit) { - return libraryItems - } - Logger.info(`[LibraryItem] Loaded ${rows.length} library items. ${libraryItems.length} loaded so far.`) - return this.getAllOldLibraryItemsIncremental(libraryItems, offset + rows.length) - } - - /** - * Gets library items partially expanded, not including podcast episodes - * @todo temporary solution - * - * @param {number} offset - * @param {number} limit - * @returns {Promise[]>} LibraryItem - */ - static getLibraryItemsIncrement(offset, limit) { - return this.findAll({ - benchmark: true, - logging: (sql, timeMs) => { - console.log(`[Query] Elapsed ${timeMs}ms.`) + /** + * Gets library items partially expanded, not including podcast episodes + * @todo temporary solution + * + * @param {number} offset + * @param {number} limit + * @returns {Promise[]>} LibraryItem + */ + static getLibraryItemsIncrement(offset, limit) { + return this.findAll({ + benchmark: true, + logging: (sql, timeMs) => { + console.log(`[Query] Elapsed ${timeMs}ms.`) + }, + include: [ + { + model: this.sequelize.models.book, + include: [ + { + model: this.sequelize.models.author, + through: { + attributes: ['createdAt'] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence', 'createdAt'] + } + } + ] }, - include: [ - { - model: sequelize.models.book, - include: [ - { - model: sequelize.models.author, - through: { - attributes: ['createdAt'] - } - }, - { - model: sequelize.models.series, - through: { - attributes: ['sequence', 'createdAt'] - } + { + model: this.sequelize.models.podcast + } + ], + order: [ + ['createdAt', 'ASC'], + // Ensure author & series stay in the same order + [this.sequelize.models.book, this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'], + [this.sequelize.models.book, this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'] + ], + offset, + limit + }) + } + + /** + * Currently unused because this is too slow and uses too much mem + * @param {[WhereOptions]} where + * @returns {Array} old library items + */ + static async getAllOldLibraryItems(where = null) { + let libraryItems = await this.findAll({ + where, + include: [ + { + model: this.sequelize.models.book, + include: [ + { + model: this.sequelize.models.author, + through: { + attributes: [] } - ] - }, - { - model: sequelize.models.podcast - } - ], - order: [ - ['createdAt', 'ASC'], - // Ensure author & series stay in the same order - [sequelize.models.book, sequelize.models.author, sequelize.models.bookAuthor, 'createdAt', 'ASC'], - [sequelize.models.book, sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'] - ], - offset, - limit - }) + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + } + ] + }, + { + model: this.sequelize.models.podcast, + include: [ + { + model: this.sequelize.models.podcastEpisode + } + ] + } + ] + }) + return libraryItems.map(ti => this.getOldLibraryItem(ti)) + } + + /** + * 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) } - /** - * Currently unused because this is too slow and uses too much mem - * @param {[WhereOptions]} where - * @returns {Array} old library items - */ - static async getAllOldLibraryItems(where = null) { - let libraryItems = await this.findAll({ - where, - include: [ - { - model: sequelize.models.book, - include: [ - { - model: sequelize.models.author, - through: { - attributes: [] - } - }, - { - model: sequelize.models.series, - through: { - attributes: ['sequence'] - } - } - ] - }, - { - model: sequelize.models.podcast, - include: [ - { - model: sequelize.models.podcastEpisode - } - ] - } - ] - }) - return libraryItems.map(ti => this.getOldLibraryItem(ti)) - } + 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 + }) + } - /** - * 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 = sequelize.models.book.getOldBook(libraryItemExpanded) - } else if (libraryItemExpanded.mediaType === 'podcast') { - media = sequelize.models.podcast.getOldPodcast(libraryItemExpanded) + static async fullCreateFromOld(oldLibraryItem) { + const newLibraryItem = await this.create(this.getFromOld(oldLibraryItem)) + + if (oldLibraryItem.mediaType === 'book') { + const bookObj = this.sequelize.models.book.getFromOld(oldLibraryItem.media) + bookObj.libraryItemId = newLibraryItem.id + const newBook = await this.sequelize.models.book.create(bookObj) + + const oldBookAuthors = oldLibraryItem.media.metadata.authors || [] + const oldBookSeriesAll = oldLibraryItem.media.metadata.series || [] + + for (const oldBookAuthor of oldBookAuthors) { + await this.sequelize.models.bookAuthor.create({ authorId: oldBookAuthor.id, bookId: newBook.id }) } + for (const oldSeries of oldBookSeriesAll) { + await this.sequelize.models.bookSeries.create({ seriesId: oldSeries.id, bookId: newBook.id, sequence: oldSeries.sequence }) + } + } else if (oldLibraryItem.mediaType === 'podcast') { + const podcastObj = this.sequelize.models.podcast.getFromOld(oldLibraryItem.media) + podcastObj.libraryItemId = newLibraryItem.id + const newPodcast = await this.sequelize.models.podcast.create(podcastObj) - 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 - }) + const oldEpisodes = oldLibraryItem.media.episodes || [] + for (const oldEpisode of oldEpisodes) { + const episodeObj = this.sequelize.models.podcastEpisode.getFromOld(oldEpisode) + episodeObj.libraryItemId = newLibraryItem.id + episodeObj.podcastId = newPodcast.id + await this.sequelize.models.podcastEpisode.create(episodeObj) + } } - static async fullCreateFromOld(oldLibraryItem) { - const newLibraryItem = await this.create(this.getFromOld(oldLibraryItem)) + return newLibraryItem + } - if (oldLibraryItem.mediaType === 'book') { - const bookObj = sequelize.models.book.getFromOld(oldLibraryItem.media) - bookObj.libraryItemId = newLibraryItem.id - const newBook = await sequelize.models.book.create(bookObj) - - const oldBookAuthors = oldLibraryItem.media.metadata.authors || [] - const oldBookSeriesAll = oldLibraryItem.media.metadata.series || [] - - for (const oldBookAuthor of oldBookAuthors) { - await sequelize.models.bookAuthor.create({ authorId: oldBookAuthor.id, bookId: newBook.id }) + static async fullUpdateFromOld(oldLibraryItem) { + const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, { + include: [ + { + model: this.sequelize.models.book, + include: [ + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['id', 'sequence'] + } + } + ] + }, + { + model: this.sequelize.models.podcast, + include: [ + { + model: this.sequelize.models.podcastEpisode + } + ] } - for (const oldSeries of oldBookSeriesAll) { - await sequelize.models.bookSeries.create({ seriesId: oldSeries.id, bookId: newBook.id, sequence: oldSeries.sequence }) - } - } else if (oldLibraryItem.mediaType === 'podcast') { - const podcastObj = sequelize.models.podcast.getFromOld(oldLibraryItem.media) - podcastObj.libraryItemId = newLibraryItem.id - const newPodcast = await sequelize.models.podcast.create(podcastObj) + ] + }) + if (!libraryItemExpanded) return false - const oldEpisodes = oldLibraryItem.media.episodes || [] - for (const oldEpisode of oldEpisodes) { - const episodeObj = sequelize.models.podcastEpisode.getFromOld(oldEpisode) - episodeObj.libraryItemId = newLibraryItem.id - episodeObj.podcastId = newPodcast.id - await sequelize.models.podcastEpisode.create(episodeObj) + let hasUpdates = false + + // Check update Book/Podcast + if (libraryItemExpanded.media) { + let updatedMedia = null + if (libraryItemExpanded.mediaType === 'podcast') { + updatedMedia = this.sequelize.models.podcast.getFromOld(oldLibraryItem.media) + + const existingPodcastEpisodes = libraryItemExpanded.media.podcastEpisodes || [] + const updatedPodcastEpisodes = oldLibraryItem.media.episodes || [] + + for (const existingPodcastEpisode of existingPodcastEpisodes) { + // Episode was removed + if (!updatedPodcastEpisodes.some(ep => ep.id === existingPodcastEpisode.id)) { + Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingPodcastEpisode.title}" was removed`) + await existingPodcastEpisode.destroy() + hasUpdates = true + } + } + for (const updatedPodcastEpisode of updatedPodcastEpisodes) { + const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id) + if (!existingEpisodeMatch) { + Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`) + await this.sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode) + hasUpdates = true + } else { + const updatedEpisodeCleaned = this.sequelize.models.podcastEpisode.getFromOld(updatedPodcastEpisode) + let episodeHasUpdates = false + for (const key in updatedEpisodeCleaned) { + let existingValue = existingEpisodeMatch[key] + if (existingValue instanceof Date) existingValue = existingValue.valueOf() + + if (!areEquivalent(updatedEpisodeCleaned[key], existingValue, true)) { + Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingEpisodeMatch.title}" ${key} was updated from "${existingValue}" to "${updatedEpisodeCleaned[key]}"`) + episodeHasUpdates = true + } + } + if (episodeHasUpdates) { + await existingEpisodeMatch.update(updatedEpisodeCleaned) + hasUpdates = true + } + } + } + } else if (libraryItemExpanded.mediaType === 'book') { + updatedMedia = this.sequelize.models.book.getFromOld(oldLibraryItem.media) + + const existingAuthors = libraryItemExpanded.media.authors || [] + const existingSeriesAll = libraryItemExpanded.media.series || [] + const updatedAuthors = oldLibraryItem.media.metadata.authors || [] + const updatedSeriesAll = oldLibraryItem.media.metadata.series || [] + + for (const existingAuthor of existingAuthors) { + // Author was removed from Book + if (!updatedAuthors.some(au => au.id === existingAuthor.id)) { + Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`) + await this.sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id) + hasUpdates = true + } + } + for (const updatedAuthor of updatedAuthors) { + // Author was added + if (!existingAuthors.some(au => au.id === updatedAuthor.id)) { + Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`) + await this.sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id }) + hasUpdates = true + } + } + for (const existingSeries of existingSeriesAll) { + // Series was removed + if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) { + Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`) + await this.sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id) + hasUpdates = true + } + } + for (const updatedSeries of updatedSeriesAll) { + // Series was added/updated + const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id) + if (!existingSeriesMatch) { + Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`) + await this.sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence }) + hasUpdates = true + } else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) { + Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`) + await existingSeriesMatch.bookSeries.update({ id: updatedSeries.id, sequence: updatedSeries.sequence }) + hasUpdates = true + } } } - return newLibraryItem - } - - static async fullUpdateFromOld(oldLibraryItem) { - const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, { - include: [ - { - model: sequelize.models.book, - include: [ - { - model: sequelize.models.author, - through: { - attributes: [] - } - }, - { - model: sequelize.models.series, - through: { - attributes: ['id', 'sequence'] - } - } - ] - }, - { - model: sequelize.models.podcast, - include: [ - { - model: sequelize.models.podcastEpisode - } - ] - } - ] - }) - if (!libraryItemExpanded) return false - - let hasUpdates = false - - // Check update Book/Podcast - if (libraryItemExpanded.media) { - let updatedMedia = null - if (libraryItemExpanded.mediaType === 'podcast') { - updatedMedia = sequelize.models.podcast.getFromOld(oldLibraryItem.media) - - const existingPodcastEpisodes = libraryItemExpanded.media.podcastEpisodes || [] - const updatedPodcastEpisodes = oldLibraryItem.media.episodes || [] - - for (const existingPodcastEpisode of existingPodcastEpisodes) { - // Episode was removed - if (!updatedPodcastEpisodes.some(ep => ep.id === existingPodcastEpisode.id)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingPodcastEpisode.title}" was removed`) - await existingPodcastEpisode.destroy() - hasUpdates = true - } - } - for (const updatedPodcastEpisode of updatedPodcastEpisodes) { - const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id) - if (!existingEpisodeMatch) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`) - await sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode) - hasUpdates = true - } else { - const updatedEpisodeCleaned = sequelize.models.podcastEpisode.getFromOld(updatedPodcastEpisode) - let episodeHasUpdates = false - for (const key in updatedEpisodeCleaned) { - let existingValue = existingEpisodeMatch[key] - if (existingValue instanceof Date) existingValue = existingValue.valueOf() - - if (!areEquivalent(updatedEpisodeCleaned[key], existingValue, true)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingEpisodeMatch.title}" ${key} was updated from "${existingValue}" to "${updatedEpisodeCleaned[key]}"`) - episodeHasUpdates = true - } - } - if (episodeHasUpdates) { - await existingEpisodeMatch.update(updatedEpisodeCleaned) - hasUpdates = true - } - } - } - } else if (libraryItemExpanded.mediaType === 'book') { - updatedMedia = sequelize.models.book.getFromOld(oldLibraryItem.media) - - const existingAuthors = libraryItemExpanded.media.authors || [] - const existingSeriesAll = libraryItemExpanded.media.series || [] - const updatedAuthors = oldLibraryItem.media.metadata.authors || [] - const updatedSeriesAll = oldLibraryItem.media.metadata.series || [] - - for (const existingAuthor of existingAuthors) { - // Author was removed from Book - if (!updatedAuthors.some(au => au.id === existingAuthor.id)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`) - await sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id) - hasUpdates = true - } - } - for (const updatedAuthor of updatedAuthors) { - // Author was added - if (!existingAuthors.some(au => au.id === updatedAuthor.id)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`) - await sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id }) - hasUpdates = true - } - } - for (const existingSeries of existingSeriesAll) { - // Series was removed - if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`) - await sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id) - hasUpdates = true - } - } - for (const updatedSeries of updatedSeriesAll) { - // Series was added/updated - const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id) - if (!existingSeriesMatch) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`) - await sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence }) - hasUpdates = true - } else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`) - await existingSeriesMatch.bookSeries.update({ id: updatedSeries.id, sequence: updatedSeries.sequence }) - hasUpdates = true - } - } - } - - let hasMediaUpdates = false - for (const key in updatedMedia) { - let existingValue = libraryItemExpanded.media[key] - if (existingValue instanceof Date) existingValue = existingValue.valueOf() - - if (!areEquivalent(updatedMedia[key], existingValue, true)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from ${existingValue} to ${updatedMedia[key]}`) - hasMediaUpdates = true - } - } - if (hasMediaUpdates && updatedMedia) { - await libraryItemExpanded.media.update(updatedMedia) - hasUpdates = true - } - } - - const updatedLibraryItem = this.getFromOld(oldLibraryItem) - let hasLibraryItemUpdates = false - for (const key in updatedLibraryItem) { - let existingValue = libraryItemExpanded[key] + let hasMediaUpdates = false + for (const key in updatedMedia) { + let existingValue = libraryItemExpanded.media[key] if (existingValue instanceof Date) existingValue = existingValue.valueOf() - if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`) - hasLibraryItemUpdates = true + if (!areEquivalent(updatedMedia[key], existingValue, true)) { + Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from ${existingValue} to ${updatedMedia[key]}`) + hasMediaUpdates = true } } - if (hasLibraryItemUpdates) { - await libraryItemExpanded.update(updatedLibraryItem) - Logger.info(`[LibraryItem] Library item "${libraryItemExpanded.id}" updated`) + if (hasMediaUpdates && updatedMedia) { + await libraryItemExpanded.media.update(updatedMedia) hasUpdates = true } - return hasUpdates } - static getFromOld(oldLibraryItem) { - const extraData = {} - if (oldLibraryItem.oldLibraryItemId) { - extraData.oldLibraryItemId = oldLibraryItem.oldLibraryItemId - } - return { - id: oldLibraryItem.id, - ino: oldLibraryItem.ino, - path: oldLibraryItem.path, - relPath: oldLibraryItem.relPath, - mediaId: oldLibraryItem.media.id, - mediaType: oldLibraryItem.mediaType, - isFile: !!oldLibraryItem.isFile, - isMissing: !!oldLibraryItem.isMissing, - isInvalid: !!oldLibraryItem.isInvalid, - mtime: oldLibraryItem.mtimeMs, - ctime: oldLibraryItem.ctimeMs, - birthtime: oldLibraryItem.birthtimeMs, - size: oldLibraryItem.size, - lastScan: oldLibraryItem.lastScan, - lastScanVersion: oldLibraryItem.scanVersion, - libraryId: oldLibraryItem.libraryId, - libraryFolderId: oldLibraryItem.folderId, - libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || [], - extraData + const updatedLibraryItem = this.getFromOld(oldLibraryItem) + let hasLibraryItemUpdates = false + for (const key in updatedLibraryItem) { + let existingValue = libraryItemExpanded[key] + if (existingValue instanceof Date) existingValue = existingValue.valueOf() + + if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) { + Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`) + hasLibraryItemUpdates = true } } - - static removeById(libraryItemId) { - return this.destroy({ - where: { - id: libraryItemId - }, - individualHooks: true - }) + if (hasLibraryItemUpdates) { + await libraryItemExpanded.update(updatedLibraryItem) + Logger.info(`[LibraryItem] Library item "${libraryItemExpanded.id}" updated`) + hasUpdates = true } + return hasUpdates + } - /** - * 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, { - include: [ - { - model: sequelize.models.book, - include: [ - { - model: sequelize.models.author, - through: { - attributes: [] - } - }, - { - model: sequelize.models.series, - through: { - attributes: ['sequence'] - } - } - ] - }, - { - model: sequelize.models.podcast, - include: [ - { - model: sequelize.models.podcastEpisode - } - ] - } - ] - }) - if (!libraryItem) return null - return this.getOldLibraryItem(libraryItem) + static getFromOld(oldLibraryItem) { + const extraData = {} + if (oldLibraryItem.oldLibraryItemId) { + extraData.oldLibraryItemId = oldLibraryItem.oldLibraryItemId } - - /** - * Get library items using filter and sort - * @param {oldLibrary} library - * @param {oldUser} user - * @param {object} options - * @returns {object} { libraryItems:oldLibraryItem[], count:number } - */ - static async getByFilterAndSort(library, user, options) { - let start = Date.now() - const { libraryItems, count } = await libraryFilters.getFilteredLibraryItems(library, user, options) - Logger.debug(`Loaded ${libraryItems.length} of ${count} items for libary page in ${((Date.now() - start) / 1000).toFixed(2)}s`) - - return { - libraryItems: libraryItems.map(li => { - const oldLibraryItem = this.getOldLibraryItem(li).toJSONMinified() - if (li.collapsedSeries) { - oldLibraryItem.collapsedSeries = li.collapsedSeries - } - if (li.series) { - oldLibraryItem.media.metadata.series = li.series - } - if (li.rssFeed) { - oldLibraryItem.rssFeed = sequelize.models.feed.getOldFeed(li.rssFeed).toJSONMinified() - } - if (li.media.numEpisodes) { - oldLibraryItem.media.numEpisodes = li.media.numEpisodes - } - if (li.size && !oldLibraryItem.media.size) { - oldLibraryItem.media.size = li.size - } - if (li.numEpisodesIncomplete) { - oldLibraryItem.numEpisodesIncomplete = li.numEpisodesIncomplete - } - - return oldLibraryItem - }), - count - } - } - - /** - * Get home page data personalized shelves - * @param {oldLibrary} library - * @param {oldUser} user - * @param {string[]} include - * @param {number} limit - * @returns {object[]} array of shelf objects - */ - static async getPersonalizedShelves(library, user, include, limit) { - const fullStart = Date.now() // Used for testing load times - - const shelves = [] - - // "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) - - shelves.push({ - id: 'continue-listening', - label: 'Continue Listening', - labelStringKey: 'LabelContinueListening', - type: library.isPodcast ? 'episode' : 'book', - entities: audioOnlyItemsInProgress, - total: itemsInProgressPayload.count - }) - - if (ebookOnlyItemsInProgress.length) { - // "Continue Reading" shelf - shelves.push({ - id: 'continue-reading', - label: 'Continue Reading', - labelStringKey: 'LabelContinueReading', - type: 'book', - entities: ebookOnlyItemsInProgress, - total: itemsInProgressPayload.count - }) - } - } - Logger.debug(`Loaded ${itemsInProgressPayload.items.length} of ${itemsInProgressPayload.count} items for "Continue Listening/Reading" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) - - let start = Date.now() - if (library.isBook) { - start = Date.now() - // "Continue Series" shelf - const continueSeriesPayload = await libraryFilters.getLibraryItemsContinueSeries(library, user, include, limit) - if (continueSeriesPayload.libraryItems.length) { - shelves.push({ - id: 'continue-series', - label: 'Continue Series', - labelStringKey: 'LabelContinueSeries', - type: 'book', - entities: continueSeriesPayload.libraryItems, - total: continueSeriesPayload.count - }) - } - Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - } else if (library.isPodcast) { - // "Newest Episodes" shelf - const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, user, limit) - if (newestEpisodesPayload.libraryItems.length) { - shelves.push({ - id: 'newest-episodes', - label: 'Newest Episodes', - labelStringKey: 'LabelNewestEpisodes', - type: 'episode', - entities: newestEpisodesPayload.libraryItems, - total: newestEpisodesPayload.count - }) - } - Logger.debug(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - } - - start = Date.now() - // "Recently Added" shelf - const mostRecentPayload = await libraryFilters.getLibraryItemsMostRecentlyAdded(library, user, include, limit) - if (mostRecentPayload.libraryItems.length) { - shelves.push({ - id: 'recently-added', - label: 'Recently Added', - labelStringKey: 'LabelRecentlyAdded', - type: library.mediaType, - entities: mostRecentPayload.libraryItems, - total: mostRecentPayload.count - }) - } - Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - - if (library.isBook) { - start = Date.now() - // "Recent Series" shelf - const seriesMostRecentPayload = await libraryFilters.getSeriesMostRecentlyAdded(library, user, include, 5) - if (seriesMostRecentPayload.series.length) { - shelves.push({ - id: 'recent-series', - label: 'Recent Series', - labelStringKey: 'LabelRecentSeries', - type: 'series', - entities: seriesMostRecentPayload.series, - total: seriesMostRecentPayload.count - }) - } - Logger.debug(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - - start = Date.now() - // "Discover" shelf - const discoverLibraryItemsPayload = await libraryFilters.getLibraryItemsToDiscover(library, user, include, limit) - if (discoverLibraryItemsPayload.libraryItems.length) { - shelves.push({ - id: 'discover', - label: 'Discover', - labelStringKey: 'LabelDiscover', - type: library.mediaType, - entities: discoverLibraryItemsPayload.libraryItems, - total: discoverLibraryItemsPayload.count - }) - } - Logger.debug(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - } - - start = Date.now() - // "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) - - shelves.push({ - id: 'listen-again', - label: 'Listen Again', - labelStringKey: 'LabelListenAgain', - type: library.isPodcast ? 'episode' : 'book', - entities: audioOnlyItemsInProgress, - total: mediaFinishedPayload.count - }) - - // "Read Again" shelf - if (ebookOnlyItemsInProgress.length) { - shelves.push({ - id: 'read-again', - label: 'Read Again', - labelStringKey: 'LabelReadAgain', - type: 'book', - entities: ebookOnlyItemsInProgress, - total: mediaFinishedPayload.count - }) - } - } - Logger.debug(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - - if (library.isBook) { - start = Date.now() - // "Newest Authors" shelf - const newestAuthorsPayload = await libraryFilters.getNewestAuthors(library, user, limit) - if (newestAuthorsPayload.authors.length) { - shelves.push({ - id: 'newest-authors', - label: 'Newest Authors', - labelStringKey: 'LabelNewestAuthors', - type: 'authors', - entities: newestAuthorsPayload.authors, - total: newestAuthorsPayload.count - }) - } - Logger.debug(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - } - - Logger.debug(`Loaded ${shelves.length} personalized shelves in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) - - return shelves - } - - /** - * Get book library items for author, optional use user permissions - * @param {oldAuthor} author - * @param {[oldUser]} user - * @returns {Promise} - */ - static async getForAuthor(author, user = null) { - const { libraryItems } = await libraryFilters.getLibraryItemsForAuthor(author, user, undefined, undefined) - return libraryItems.map(li => this.getOldLibraryItem(li)) - } - - /** - * Get book library items in a collection - * @param {oldCollection} collection - * @returns {Promise} - */ - static async getForCollection(collection) { - const libraryItems = await libraryFilters.getLibraryItemsForCollection(collection) - return libraryItems.map(li => this.getOldLibraryItem(li)) - } - - /** - * Check if library item exists - * @param {string} libraryItemId - * @returns {Promise} - */ - static async checkExistsById(libraryItemId) { - return (await this.count({ where: { id: libraryItemId } })) > 0 - } - - getMedia(options) { - if (!this.mediaType) return Promise.resolve(null) - const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaType)}` - return this[mixinMethodName](options) + return { + id: oldLibraryItem.id, + ino: oldLibraryItem.ino, + path: oldLibraryItem.path, + relPath: oldLibraryItem.relPath, + mediaId: oldLibraryItem.media.id, + mediaType: oldLibraryItem.mediaType, + isFile: !!oldLibraryItem.isFile, + isMissing: !!oldLibraryItem.isMissing, + isInvalid: !!oldLibraryItem.isInvalid, + mtime: oldLibraryItem.mtimeMs, + ctime: oldLibraryItem.ctimeMs, + birthtime: oldLibraryItem.birthtimeMs, + size: oldLibraryItem.size, + lastScan: oldLibraryItem.lastScan, + lastScanVersion: oldLibraryItem.scanVersion, + libraryId: oldLibraryItem.libraryId, + libraryFolderId: oldLibraryItem.folderId, + libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || [], + extraData } } - LibraryItem.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - ino: DataTypes.STRING, - path: DataTypes.STRING, - relPath: DataTypes.STRING, - mediaId: DataTypes.UUIDV4, - mediaType: DataTypes.STRING, - isFile: DataTypes.BOOLEAN, - isMissing: DataTypes.BOOLEAN, - isInvalid: DataTypes.BOOLEAN, - mtime: DataTypes.DATE(6), - ctime: DataTypes.DATE(6), - birthtime: DataTypes.DATE(6), - size: DataTypes.BIGINT, - lastScan: DataTypes.DATE, - lastScanVersion: DataTypes.STRING, - libraryFiles: DataTypes.JSON, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'libraryItem', - indexes: [ - { - fields: ['createdAt'] + static removeById(libraryItemId) { + return this.destroy({ + where: { + id: libraryItemId }, - { - fields: ['mediaId'] - }, - { - fields: ['libraryId', 'mediaType'] - }, - { - fields: ['birthtime'] - }, - { - fields: ['mtime'] + individualHooks: true + }) + } + + /** + * 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, { + include: [ + { + model: this.sequelize.models.book, + include: [ + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + } + ] + }, + { + model: this.sequelize.models.podcast, + include: [ + { + model: this.sequelize.models.podcastEpisode + } + ] + } + ] + }) + if (!libraryItem) return null + return this.getOldLibraryItem(libraryItem) + } + + /** + * Get library items using filter and sort + * @param {oldLibrary} library + * @param {oldUser} user + * @param {object} options + * @returns {object} { libraryItems:oldLibraryItem[], count:number } + */ + static async getByFilterAndSort(library, user, options) { + let start = Date.now() + const { libraryItems, count } = await libraryFilters.getFilteredLibraryItems(library, user, options) + Logger.debug(`Loaded ${libraryItems.length} of ${count} items for libary page in ${((Date.now() - start) / 1000).toFixed(2)}s`) + + return { + libraryItems: libraryItems.map(li => { + const oldLibraryItem = this.getOldLibraryItem(li).toJSONMinified() + if (li.collapsedSeries) { + oldLibraryItem.collapsedSeries = li.collapsedSeries + } + if (li.series) { + oldLibraryItem.media.metadata.series = li.series + } + if (li.rssFeed) { + oldLibraryItem.rssFeed = this.sequelize.models.feed.getOldFeed(li.rssFeed).toJSONMinified() + } + if (li.media.numEpisodes) { + oldLibraryItem.media.numEpisodes = li.media.numEpisodes + } + if (li.size && !oldLibraryItem.media.size) { + oldLibraryItem.media.size = li.size + } + if (li.numEpisodesIncomplete) { + oldLibraryItem.numEpisodesIncomplete = li.numEpisodesIncomplete + } + + return oldLibraryItem + }), + count + } + } + + /** + * Get home page data personalized shelves + * @param {oldLibrary} library + * @param {oldUser} user + * @param {string[]} include + * @param {number} limit + * @returns {object[]} array of shelf objects + */ + static async getPersonalizedShelves(library, user, include, limit) { + const fullStart = Date.now() // Used for testing load times + + const shelves = [] + + // "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) + + shelves.push({ + id: 'continue-listening', + label: 'Continue Listening', + labelStringKey: 'LabelContinueListening', + type: library.isPodcast ? 'episode' : 'book', + entities: audioOnlyItemsInProgress, + total: itemsInProgressPayload.count + }) + + if (ebookOnlyItemsInProgress.length) { + // "Continue Reading" shelf + shelves.push({ + id: 'continue-reading', + label: 'Continue Reading', + labelStringKey: 'LabelContinueReading', + type: 'book', + entities: ebookOnlyItemsInProgress, + total: itemsInProgressPayload.count + }) } - ] - }) - - const { library, libraryFolder, book, podcast } = sequelize.models - library.hasMany(LibraryItem) - LibraryItem.belongsTo(library) - - libraryFolder.hasMany(LibraryItem) - LibraryItem.belongsTo(libraryFolder) - - book.hasOne(LibraryItem, { - foreignKey: 'mediaId', - constraints: false, - scope: { - mediaType: 'book' } - }) - LibraryItem.belongsTo(book, { foreignKey: 'mediaId', constraints: false }) + Logger.debug(`Loaded ${itemsInProgressPayload.items.length} of ${itemsInProgressPayload.count} items for "Continue Listening/Reading" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) - podcast.hasOne(LibraryItem, { - foreignKey: 'mediaId', - constraints: false, - scope: { - mediaType: 'podcast' - } - }) - LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false }) - - LibraryItem.addHook('afterFind', findResult => { - if (!findResult) return - - if (!Array.isArray(findResult)) findResult = [findResult] - for (const instance of findResult) { - if (instance.mediaType === 'book' && instance.book !== undefined) { - instance.media = instance.book - instance.dataValues.media = instance.dataValues.book - } else if (instance.mediaType === 'podcast' && instance.podcast !== undefined) { - instance.media = instance.podcast - instance.dataValues.media = instance.dataValues.podcast + let start = Date.now() + if (library.isBook) { + start = Date.now() + // "Continue Series" shelf + const continueSeriesPayload = await libraryFilters.getLibraryItemsContinueSeries(library, user, include, limit) + if (continueSeriesPayload.libraryItems.length) { + shelves.push({ + id: 'continue-series', + label: 'Continue Series', + labelStringKey: 'LabelContinueSeries', + type: 'book', + entities: continueSeriesPayload.libraryItems, + total: continueSeriesPayload.count + }) } - // To prevent mistakes: - delete instance.book - delete instance.dataValues.book - delete instance.podcast - delete instance.dataValues.podcast + Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + } else if (library.isPodcast) { + // "Newest Episodes" shelf + const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, user, limit) + if (newestEpisodesPayload.libraryItems.length) { + shelves.push({ + id: 'newest-episodes', + label: 'Newest Episodes', + labelStringKey: 'LabelNewestEpisodes', + type: 'episode', + entities: newestEpisodesPayload.libraryItems, + total: newestEpisodesPayload.count + }) + } + Logger.debug(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } - }) - LibraryItem.addHook('afterDestroy', async instance => { - if (!instance) return - const media = await instance.getMedia() - if (media) { - media.destroy() + start = Date.now() + // "Recently Added" shelf + const mostRecentPayload = await libraryFilters.getLibraryItemsMostRecentlyAdded(library, user, include, limit) + if (mostRecentPayload.libraryItems.length) { + shelves.push({ + id: 'recently-added', + label: 'Recently Added', + labelStringKey: 'LabelRecentlyAdded', + type: library.mediaType, + entities: mostRecentPayload.libraryItems, + total: mostRecentPayload.count + }) } - }) + Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - return LibraryItem -} \ No newline at end of file + if (library.isBook) { + start = Date.now() + // "Recent Series" shelf + const seriesMostRecentPayload = await libraryFilters.getSeriesMostRecentlyAdded(library, user, include, 5) + if (seriesMostRecentPayload.series.length) { + shelves.push({ + id: 'recent-series', + label: 'Recent Series', + labelStringKey: 'LabelRecentSeries', + type: 'series', + entities: seriesMostRecentPayload.series, + total: seriesMostRecentPayload.count + }) + } + Logger.debug(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + + start = Date.now() + // "Discover" shelf + const discoverLibraryItemsPayload = await libraryFilters.getLibraryItemsToDiscover(library, user, include, limit) + if (discoverLibraryItemsPayload.libraryItems.length) { + shelves.push({ + id: 'discover', + label: 'Discover', + labelStringKey: 'LabelDiscover', + type: library.mediaType, + entities: discoverLibraryItemsPayload.libraryItems, + total: discoverLibraryItemsPayload.count + }) + } + Logger.debug(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + } + + start = Date.now() + // "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) + + shelves.push({ + id: 'listen-again', + label: 'Listen Again', + labelStringKey: 'LabelListenAgain', + type: library.isPodcast ? 'episode' : 'book', + entities: audioOnlyItemsInProgress, + total: mediaFinishedPayload.count + }) + + // "Read Again" shelf + if (ebookOnlyItemsInProgress.length) { + shelves.push({ + id: 'read-again', + label: 'Read Again', + labelStringKey: 'LabelReadAgain', + type: 'book', + entities: ebookOnlyItemsInProgress, + total: mediaFinishedPayload.count + }) + } + } + Logger.debug(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + + if (library.isBook) { + start = Date.now() + // "Newest Authors" shelf + const newestAuthorsPayload = await libraryFilters.getNewestAuthors(library, user, limit) + if (newestAuthorsPayload.authors.length) { + shelves.push({ + id: 'newest-authors', + label: 'Newest Authors', + labelStringKey: 'LabelNewestAuthors', + type: 'authors', + entities: newestAuthorsPayload.authors, + total: newestAuthorsPayload.count + }) + } + Logger.debug(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + } + + Logger.debug(`Loaded ${shelves.length} personalized shelves in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) + + return shelves + } + + /** + * Get book library items for author, optional use user permissions + * @param {oldAuthor} author + * @param {[oldUser]} user + * @returns {Promise} + */ + static async getForAuthor(author, user = null) { + const { libraryItems } = await libraryFilters.getLibraryItemsForAuthor(author, user, undefined, undefined) + return libraryItems.map(li => this.getOldLibraryItem(li)) + } + + /** + * Get book library items in a collection + * @param {oldCollection} collection + * @returns {Promise} + */ + static async getForCollection(collection) { + const libraryItems = await libraryFilters.getLibraryItemsForCollection(collection) + return libraryItems.map(li => this.getOldLibraryItem(li)) + } + + /** + * Check if library item exists + * @param {string} libraryItemId + * @returns {Promise} + */ + static async checkExistsById(libraryItemId) { + return (await this.count({ where: { id: libraryItemId } })) > 0 + } + + getMedia(options) { + if (!this.mediaType) return Promise.resolve(null) + const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaType)}` + return this[mixinMethodName](options) + } + + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + ino: DataTypes.STRING, + path: DataTypes.STRING, + relPath: DataTypes.STRING, + mediaId: DataTypes.UUIDV4, + mediaType: DataTypes.STRING, + isFile: DataTypes.BOOLEAN, + isMissing: DataTypes.BOOLEAN, + isInvalid: DataTypes.BOOLEAN, + mtime: DataTypes.DATE(6), + ctime: DataTypes.DATE(6), + birthtime: DataTypes.DATE(6), + size: DataTypes.BIGINT, + lastScan: DataTypes.DATE, + lastScanVersion: DataTypes.STRING, + libraryFiles: DataTypes.JSON, + extraData: DataTypes.JSON + }, { + sequelize, + modelName: 'libraryItem', + indexes: [ + { + fields: ['createdAt'] + }, + { + fields: ['mediaId'] + }, + { + fields: ['libraryId', 'mediaType'] + }, + { + fields: ['birthtime'] + }, + { + fields: ['mtime'] + } + ] + }) + + const { library, libraryFolder, book, podcast } = sequelize.models + library.hasMany(LibraryItem) + LibraryItem.belongsTo(library) + + libraryFolder.hasMany(LibraryItem) + LibraryItem.belongsTo(libraryFolder) + + book.hasOne(LibraryItem, { + foreignKey: 'mediaId', + constraints: false, + scope: { + mediaType: 'book' + } + }) + LibraryItem.belongsTo(book, { foreignKey: 'mediaId', constraints: false }) + + podcast.hasOne(LibraryItem, { + foreignKey: 'mediaId', + constraints: false, + scope: { + mediaType: 'podcast' + } + }) + LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false }) + + LibraryItem.addHook('afterFind', findResult => { + if (!findResult) return + + if (!Array.isArray(findResult)) findResult = [findResult] + for (const instance of findResult) { + if (instance.mediaType === 'book' && instance.book !== undefined) { + instance.media = instance.book + instance.dataValues.media = instance.dataValues.book + } else if (instance.mediaType === 'podcast' && instance.podcast !== undefined) { + instance.media = instance.podcast + instance.dataValues.media = instance.dataValues.podcast + } + // To prevent mistakes: + delete instance.book + delete instance.dataValues.book + delete instance.podcast + delete instance.dataValues.podcast + } + }) + + LibraryItem.addHook('afterDestroy', async instance => { + if (!instance) return + const media = await instance.getMedia() + if (media) { + media.destroy() + } + }) + } +} + +module.exports = LibraryItem diff --git a/server/models/MediaProgress.js b/server/models/MediaProgress.js index 1986605d..6214d649 100644 --- a/server/models/MediaProgress.js +++ b/server/models/MediaProgress.js @@ -1,148 +1,184 @@ const { DataTypes, Model } = require('sequelize') -/* - * Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/ - * Book has many MediaProgress. PodcastEpisode has many MediaProgress. - */ -module.exports = (sequelize) => { - class MediaProgress extends Model { - getOldMediaProgress() { - const isPodcastEpisode = this.mediaItemType === 'podcastEpisode' +class MediaProgress extends Model { + constructor(values, options) { + super(values, options) - return { - id: this.id, - userId: this.userId, - libraryItemId: this.extraData?.libraryItemId || null, - episodeId: isPodcastEpisode ? this.mediaItemId : null, - mediaItemId: this.mediaItemId, - mediaItemType: this.mediaItemType, - duration: this.duration, - progress: this.extraData?.progress || 0, - currentTime: this.currentTime, - isFinished: !!this.isFinished, - hideFromContinueListening: !!this.hideFromContinueListening, - ebookLocation: this.ebookLocation, - ebookProgress: this.ebookProgress, - lastUpdate: this.updatedAt.valueOf(), - startedAt: this.createdAt.valueOf(), - finishedAt: this.finishedAt?.valueOf() || null - } - } + /** @type {UUIDV4} */ + this.id + /** @type {UUIDV4} */ + this.mediaItemId + /** @type {string} */ + this.mediaItemType + /** @type {number} */ + this.duration + /** @type {number} */ + this.currentTime + /** @type {boolean} */ + this.isFinished + /** @type {boolean} */ + this.hideFromContinueListening + /** @type {string} */ + this.ebookLocation + /** @type {number} */ + this.ebookProgress + /** @type {Date} */ + this.finishedAt + /** @type {Object} */ + this.extraData + /** @type {UUIDV4} */ + this.userId + /** @type {Date} */ + this.updatedAt + /** @type {Date} */ + this.createdAt + } - static upsertFromOld(oldMediaProgress) { - const mediaProgress = this.getFromOld(oldMediaProgress) - return this.upsert(mediaProgress) - } + getOldMediaProgress() { + const isPodcastEpisode = this.mediaItemType === 'podcastEpisode' - 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: { - id: mediaProgressId - } - }) - } - - getMediaItem(options) { - if (!this.mediaItemType) return Promise.resolve(null) - const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` - return this[mixinMethodName](options) + return { + id: this.id, + userId: this.userId, + libraryItemId: this.extraData?.libraryItemId || null, + episodeId: isPodcastEpisode ? this.mediaItemId : null, + mediaItemId: this.mediaItemId, + mediaItemType: this.mediaItemType, + duration: this.duration, + progress: this.extraData?.progress || 0, + currentTime: this.currentTime, + isFinished: !!this.isFinished, + hideFromContinueListening: !!this.hideFromContinueListening, + ebookLocation: this.ebookLocation, + ebookProgress: this.ebookProgress, + lastUpdate: this.updatedAt.valueOf(), + startedAt: this.createdAt.valueOf(), + finishedAt: this.finishedAt?.valueOf() || null } } + static upsertFromOld(oldMediaProgress) { + const mediaProgress = this.getFromOld(oldMediaProgress) + return this.upsert(mediaProgress) + } - MediaProgress.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - mediaItemId: DataTypes.UUIDV4, - mediaItemType: DataTypes.STRING, - duration: DataTypes.FLOAT, - currentTime: DataTypes.FLOAT, - isFinished: DataTypes.BOOLEAN, - hideFromContinueListening: DataTypes.BOOLEAN, - ebookLocation: DataTypes.STRING, - ebookProgress: DataTypes.FLOAT, - finishedAt: DataTypes.DATE, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'mediaProgress', - indexes: [ - { - fields: ['updatedAt'] + 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 } - ] - }) - - const { book, podcastEpisode, user } = sequelize.models - - book.hasMany(MediaProgress, { - foreignKey: 'mediaItemId', - constraints: false, - scope: { - mediaItemType: 'book' } - }) - MediaProgress.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) + } - podcastEpisode.hasMany(MediaProgress, { - foreignKey: 'mediaItemId', - constraints: false, - scope: { - mediaItemType: 'podcastEpisode' - } - }) - MediaProgress.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) - - MediaProgress.addHook('afterFind', findResult => { - if (!findResult) return - - if (!Array.isArray(findResult)) findResult = [findResult] - - for (const instance of findResult) { - if (instance.mediaItemType === 'book' && instance.book !== undefined) { - instance.mediaItem = instance.book - instance.dataValues.mediaItem = instance.dataValues.book - } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { - instance.mediaItem = instance.podcastEpisode - instance.dataValues.mediaItem = instance.dataValues.podcastEpisode + static removeById(mediaProgressId) { + return this.destroy({ + where: { + id: mediaProgressId } - // To prevent mistakes: - delete instance.book - delete instance.dataValues.book - delete instance.podcastEpisode - delete instance.dataValues.podcastEpisode - } - }) + }) + } - user.hasMany(MediaProgress, { - onDelete: 'CASCADE' - }) - MediaProgress.belongsTo(user) + getMediaItem(options) { + if (!this.mediaItemType) return Promise.resolve(null) + const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}` + return this[mixinMethodName](options) + } - return MediaProgress -} \ No newline at end of file + /** + * Initialize model + * + * Polymorphic association: Book has many MediaProgress. PodcastEpisode has many MediaProgress. + * @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/ + * + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + mediaItemId: DataTypes.UUIDV4, + mediaItemType: DataTypes.STRING, + duration: DataTypes.FLOAT, + currentTime: DataTypes.FLOAT, + isFinished: DataTypes.BOOLEAN, + hideFromContinueListening: DataTypes.BOOLEAN, + ebookLocation: DataTypes.STRING, + ebookProgress: DataTypes.FLOAT, + finishedAt: DataTypes.DATE, + extraData: DataTypes.JSON + }, { + sequelize, + modelName: 'mediaProgress', + indexes: [ + { + fields: ['updatedAt'] + } + ] + }) + + const { book, podcastEpisode, user } = sequelize.models + + book.hasMany(MediaProgress, { + foreignKey: 'mediaItemId', + constraints: false, + scope: { + mediaItemType: 'book' + } + }) + MediaProgress.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) + + podcastEpisode.hasMany(MediaProgress, { + foreignKey: 'mediaItemId', + constraints: false, + scope: { + mediaItemType: 'podcastEpisode' + } + }) + MediaProgress.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) + + MediaProgress.addHook('afterFind', findResult => { + if (!findResult) return + + if (!Array.isArray(findResult)) findResult = [findResult] + + for (const instance of findResult) { + if (instance.mediaItemType === 'book' && instance.book !== undefined) { + instance.mediaItem = instance.book + instance.dataValues.mediaItem = instance.dataValues.book + } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { + instance.mediaItem = instance.podcastEpisode + instance.dataValues.mediaItem = instance.dataValues.podcastEpisode + } + // To prevent mistakes: + delete instance.book + delete instance.dataValues.book + delete instance.podcastEpisode + delete instance.dataValues.podcastEpisode + } + }) + + user.hasMany(MediaProgress, { + onDelete: 'CASCADE' + }) + MediaProgress.belongsTo(user) + } +} + +module.exports = MediaProgress \ No newline at end of file diff --git a/server/models/PlaybackSession.js b/server/models/PlaybackSession.js index 0e0e04b5..3bb8653b 100644 --- a/server/models/PlaybackSession.js +++ b/server/models/PlaybackSession.js @@ -2,197 +2,251 @@ const { DataTypes, Model } = require('sequelize') const oldPlaybackSession = require('../objects/PlaybackSession') -module.exports = (sequelize) => { - class PlaybackSession extends Model { - static async getOldPlaybackSessions(where = null) { - const playbackSessions = await this.findAll({ - where, - include: [ - { - model: sequelize.models.device - } - ] - }) - return playbackSessions.map(session => this.getOldPlaybackSession(session)) - } - static async getById(sessionId) { - const playbackSession = await this.findByPk(sessionId, { - include: [ - { - model: sequelize.models.device - } - ] - }) - if (!playbackSession) return null - return this.getOldPlaybackSession(playbackSession) - } +class PlaybackSession extends Model { + constructor(values, options) { + super(values, options) - static getOldPlaybackSession(playbackSessionExpanded) { - const isPodcastEpisode = playbackSessionExpanded.mediaItemType === 'podcastEpisode' + /** @type {UUIDV4} */ + this.id + /** @type {UUIDV4} */ + this.mediaItemId + /** @type {string} */ + this.mediaItemType + /** @type {string} */ + this.displayTitle + /** @type {string} */ + this.displayAuthor + /** @type {number} */ + this.duration + /** @type {number} */ + this.playMethod + /** @type {string} */ + this.mediaPlayer + /** @type {number} */ + this.startTime + /** @type {number} */ + this.currentTime + /** @type {string} */ + this.serverVersion + /** @type {string} */ + this.coverPath + /** @type {number} */ + this.timeListening + /** @type {Object} */ + this.mediaMetadata + /** @type {string} */ + this.date + /** @type {string} */ + this.dayOfWeek + /** @type {Object} */ + this.extraData + /** @type {UUIDV4} */ + this.userId + /** @type {UUIDV4} */ + this.deviceId + /** @type {UUIDV4} */ + this.libraryId + /** @type {Date} */ + this.updatedAt + /** @type {Date} */ + this.createdAt + } - return new oldPlaybackSession({ - id: playbackSessionExpanded.id, - userId: playbackSessionExpanded.userId, - libraryId: playbackSessionExpanded.libraryId, - libraryItemId: playbackSessionExpanded.extraData?.libraryItemId || null, - bookId: isPodcastEpisode ? null : playbackSessionExpanded.mediaItemId, - episodeId: isPodcastEpisode ? playbackSessionExpanded.mediaItemId : null, - mediaType: isPodcastEpisode ? 'podcast' : 'book', - mediaMetadata: playbackSessionExpanded.mediaMetadata, - chapters: null, - displayTitle: playbackSessionExpanded.displayTitle, - displayAuthor: playbackSessionExpanded.displayAuthor, - coverPath: playbackSessionExpanded.coverPath, - duration: playbackSessionExpanded.duration, - playMethod: playbackSessionExpanded.playMethod, - mediaPlayer: playbackSessionExpanded.mediaPlayer, - deviceInfo: playbackSessionExpanded.device?.getOldDevice() || null, - serverVersion: playbackSessionExpanded.serverVersion, - date: playbackSessionExpanded.date, - dayOfWeek: playbackSessionExpanded.dayOfWeek, - timeListening: playbackSessionExpanded.timeListening, - startTime: playbackSessionExpanded.startTime, - currentTime: playbackSessionExpanded.currentTime, - startedAt: playbackSessionExpanded.createdAt.valueOf(), - updatedAt: playbackSessionExpanded.updatedAt.valueOf() - }) - } - - static removeById(sessionId) { - return this.destroy({ - where: { - id: sessionId + static async getOldPlaybackSessions(where = null) { + const playbackSessions = await this.findAll({ + where, + include: [ + { + model: this.sequelize.models.device } - }) - } + ] + }) + return playbackSessions.map(session => this.getOldPlaybackSession(session)) + } - static createFromOld(oldPlaybackSession) { - const playbackSession = this.getFromOld(oldPlaybackSession) - return this.create(playbackSession) - } - - static updateFromOld(oldPlaybackSession) { - const playbackSession = this.getFromOld(oldPlaybackSession) - return this.update(playbackSession, { - where: { - id: playbackSession.id + static async getById(sessionId) { + const playbackSession = await this.findByPk(sessionId, { + include: [ + { + model: this.sequelize.models.device } - }) - } + ] + }) + if (!playbackSession) return null + return this.getOldPlaybackSession(playbackSession) + } - static getFromOld(oldPlaybackSession) { - return { - id: oldPlaybackSession.id, - mediaItemId: oldPlaybackSession.episodeId || oldPlaybackSession.bookId, - mediaItemType: oldPlaybackSession.episodeId ? 'podcastEpisode' : 'book', - libraryId: oldPlaybackSession.libraryId, - displayTitle: oldPlaybackSession.displayTitle, - displayAuthor: oldPlaybackSession.displayAuthor, - duration: oldPlaybackSession.duration, - playMethod: oldPlaybackSession.playMethod, - mediaPlayer: oldPlaybackSession.mediaPlayer, - startTime: oldPlaybackSession.startTime, - currentTime: oldPlaybackSession.currentTime, - serverVersion: oldPlaybackSession.serverVersion || null, - createdAt: oldPlaybackSession.startedAt, - updatedAt: oldPlaybackSession.updatedAt, - userId: oldPlaybackSession.userId, - deviceId: oldPlaybackSession.deviceInfo?.id || null, - timeListening: oldPlaybackSession.timeListening, - coverPath: oldPlaybackSession.coverPath, - mediaMetadata: oldPlaybackSession.mediaMetadata, - date: oldPlaybackSession.date, - dayOfWeek: oldPlaybackSession.dayOfWeek, - extraData: { - libraryItemId: oldPlaybackSession.libraryItemId - } + static getOldPlaybackSession(playbackSessionExpanded) { + const isPodcastEpisode = playbackSessionExpanded.mediaItemType === 'podcastEpisode' + + return new oldPlaybackSession({ + id: playbackSessionExpanded.id, + userId: playbackSessionExpanded.userId, + libraryId: playbackSessionExpanded.libraryId, + libraryItemId: playbackSessionExpanded.extraData?.libraryItemId || null, + bookId: isPodcastEpisode ? null : playbackSessionExpanded.mediaItemId, + episodeId: isPodcastEpisode ? playbackSessionExpanded.mediaItemId : null, + mediaType: isPodcastEpisode ? 'podcast' : 'book', + mediaMetadata: playbackSessionExpanded.mediaMetadata, + chapters: null, + displayTitle: playbackSessionExpanded.displayTitle, + displayAuthor: playbackSessionExpanded.displayAuthor, + coverPath: playbackSessionExpanded.coverPath, + duration: playbackSessionExpanded.duration, + playMethod: playbackSessionExpanded.playMethod, + mediaPlayer: playbackSessionExpanded.mediaPlayer, + deviceInfo: playbackSessionExpanded.device?.getOldDevice() || null, + serverVersion: playbackSessionExpanded.serverVersion, + date: playbackSessionExpanded.date, + dayOfWeek: playbackSessionExpanded.dayOfWeek, + timeListening: playbackSessionExpanded.timeListening, + startTime: playbackSessionExpanded.startTime, + currentTime: playbackSessionExpanded.currentTime, + startedAt: playbackSessionExpanded.createdAt.valueOf(), + updatedAt: playbackSessionExpanded.updatedAt.valueOf() + }) + } + + static removeById(sessionId) { + return this.destroy({ + where: { + id: sessionId } - } + }) + } - getMediaItem(options) { - if (!this.mediaItemType) return Promise.resolve(null) - const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` - return this[mixinMethodName](options) + static createFromOld(oldPlaybackSession) { + const playbackSession = this.getFromOld(oldPlaybackSession) + return this.create(playbackSession) + } + + static updateFromOld(oldPlaybackSession) { + const playbackSession = this.getFromOld(oldPlaybackSession) + return this.update(playbackSession, { + where: { + id: playbackSession.id + } + }) + } + + static getFromOld(oldPlaybackSession) { + return { + id: oldPlaybackSession.id, + mediaItemId: oldPlaybackSession.episodeId || oldPlaybackSession.bookId, + mediaItemType: oldPlaybackSession.episodeId ? 'podcastEpisode' : 'book', + libraryId: oldPlaybackSession.libraryId, + displayTitle: oldPlaybackSession.displayTitle, + displayAuthor: oldPlaybackSession.displayAuthor, + duration: oldPlaybackSession.duration, + playMethod: oldPlaybackSession.playMethod, + mediaPlayer: oldPlaybackSession.mediaPlayer, + startTime: oldPlaybackSession.startTime, + currentTime: oldPlaybackSession.currentTime, + serverVersion: oldPlaybackSession.serverVersion || null, + createdAt: oldPlaybackSession.startedAt, + updatedAt: oldPlaybackSession.updatedAt, + userId: oldPlaybackSession.userId, + deviceId: oldPlaybackSession.deviceInfo?.id || null, + timeListening: oldPlaybackSession.timeListening, + coverPath: oldPlaybackSession.coverPath, + mediaMetadata: oldPlaybackSession.mediaMetadata, + date: oldPlaybackSession.date, + dayOfWeek: oldPlaybackSession.dayOfWeek, + extraData: { + libraryItemId: oldPlaybackSession.libraryItemId + } } } - PlaybackSession.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - mediaItemId: DataTypes.UUIDV4, - mediaItemType: DataTypes.STRING, - displayTitle: DataTypes.STRING, - displayAuthor: DataTypes.STRING, - duration: DataTypes.FLOAT, - playMethod: DataTypes.INTEGER, - mediaPlayer: DataTypes.STRING, - startTime: DataTypes.FLOAT, - currentTime: DataTypes.FLOAT, - serverVersion: DataTypes.STRING, - coverPath: DataTypes.STRING, - timeListening: DataTypes.INTEGER, - mediaMetadata: DataTypes.JSON, - date: DataTypes.STRING, - dayOfWeek: DataTypes.STRING, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'playbackSession' - }) + getMediaItem(options) { + if (!this.mediaItemType) return Promise.resolve(null) + const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}` + return this[mixinMethodName](options) + } - const { book, podcastEpisode, user, device, library } = sequelize.models + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + mediaItemId: DataTypes.UUIDV4, + mediaItemType: DataTypes.STRING, + displayTitle: DataTypes.STRING, + displayAuthor: DataTypes.STRING, + duration: DataTypes.FLOAT, + playMethod: DataTypes.INTEGER, + mediaPlayer: DataTypes.STRING, + startTime: DataTypes.FLOAT, + currentTime: DataTypes.FLOAT, + serverVersion: DataTypes.STRING, + coverPath: DataTypes.STRING, + timeListening: DataTypes.INTEGER, + mediaMetadata: DataTypes.JSON, + date: DataTypes.STRING, + dayOfWeek: DataTypes.STRING, + extraData: DataTypes.JSON + }, { + sequelize, + modelName: 'playbackSession' + }) - user.hasMany(PlaybackSession) - PlaybackSession.belongsTo(user) + const { book, podcastEpisode, user, device, library } = sequelize.models - device.hasMany(PlaybackSession) - PlaybackSession.belongsTo(device) + user.hasMany(PlaybackSession) + PlaybackSession.belongsTo(user) - library.hasMany(PlaybackSession) - PlaybackSession.belongsTo(library) + device.hasMany(PlaybackSession) + PlaybackSession.belongsTo(device) - book.hasMany(PlaybackSession, { - foreignKey: 'mediaItemId', - constraints: false, - scope: { - mediaItemType: 'book' - } - }) - PlaybackSession.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) + library.hasMany(PlaybackSession) + PlaybackSession.belongsTo(library) - podcastEpisode.hasOne(PlaybackSession, { - foreignKey: 'mediaItemId', - constraints: false, - scope: { - mediaItemType: 'podcastEpisode' - } - }) - PlaybackSession.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) - - PlaybackSession.addHook('afterFind', findResult => { - if (!findResult) return - - if (!Array.isArray(findResult)) findResult = [findResult] - - for (const instance of findResult) { - if (instance.mediaItemType === 'book' && instance.book !== undefined) { - instance.mediaItem = instance.book - instance.dataValues.mediaItem = instance.dataValues.book - } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { - instance.mediaItem = instance.podcastEpisode - instance.dataValues.mediaItem = instance.dataValues.podcastEpisode + book.hasMany(PlaybackSession, { + foreignKey: 'mediaItemId', + constraints: false, + scope: { + mediaItemType: 'book' } - // To prevent mistakes: - delete instance.book - delete instance.dataValues.book - delete instance.podcastEpisode - delete instance.dataValues.podcastEpisode - } - }) + }) + PlaybackSession.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) - return PlaybackSession -} \ No newline at end of file + podcastEpisode.hasOne(PlaybackSession, { + foreignKey: 'mediaItemId', + constraints: false, + scope: { + mediaItemType: 'podcastEpisode' + } + }) + PlaybackSession.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) + + PlaybackSession.addHook('afterFind', findResult => { + if (!findResult) return + + if (!Array.isArray(findResult)) findResult = [findResult] + + for (const instance of findResult) { + if (instance.mediaItemType === 'book' && instance.book !== undefined) { + instance.mediaItem = instance.book + instance.dataValues.mediaItem = instance.dataValues.book + } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { + instance.mediaItem = instance.podcastEpisode + instance.dataValues.mediaItem = instance.dataValues.podcastEpisode + } + // To prevent mistakes: + delete instance.book + delete instance.dataValues.book + delete instance.podcastEpisode + delete instance.dataValues.podcastEpisode + } + }) + } +} + +module.exports = PlaybackSession diff --git a/server/models/Playlist.js b/server/models/Playlist.js index 7e466289..d6a86d68 100644 --- a/server/models/Playlist.js +++ b/server/models/Playlist.js @@ -3,318 +3,341 @@ const Logger = require('../Logger') const oldPlaylist = require('../objects/Playlist') -module.exports = (sequelize) => { - class Playlist extends Model { - static async getOldPlaylists() { - const playlists = await this.findAll({ - include: { - model: sequelize.models.playlistMediaItem, - include: [ - { - model: sequelize.models.book, - include: sequelize.models.libraryItem - }, - { - model: sequelize.models.podcastEpisode, - include: { - model: sequelize.models.podcast, - include: sequelize.models.libraryItem - } - } - ] - }, - order: [['playlistMediaItems', 'order', 'ASC']] - }) - return playlists.map(p => this.getOldPlaylist(p)) - } +class Playlist extends Model { + constructor(values, options) { + super(values, options) - static getOldPlaylist(playlistExpanded) { - const items = playlistExpanded.playlistMediaItems.map(pmi => { - const libraryItemId = pmi.mediaItem?.podcast?.libraryItem?.id || pmi.mediaItem?.libraryItem?.id || null - if (!libraryItemId) { - Logger.error(`[Playlist] Invalid playlist media item - No library item id found`, JSON.stringify(pmi, null, 2)) - return null - } - return { - episodeId: pmi.mediaItemType === 'podcastEpisode' ? pmi.mediaItemId : '', - libraryItemId - } - }).filter(pmi => pmi) + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.name + /** @type {string} */ + this.description + /** @type {UUIDV4} */ + this.libraryId + /** @type {UUIDV4} */ + this.userId + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + } - return new oldPlaylist({ - id: playlistExpanded.id, - libraryId: playlistExpanded.libraryId, - userId: playlistExpanded.userId, - name: playlistExpanded.name, - description: playlistExpanded.description, - items, - lastUpdate: playlistExpanded.updatedAt.valueOf(), - createdAt: playlistExpanded.createdAt.valueOf() - }) - } - - /** - * Get old playlist toJSONExpanded - * @param {[string[]]} include - * @returns {Promise} oldPlaylist.toJSONExpanded - */ - async getOldJsonExpanded(include) { - this.playlistMediaItems = await this.getPlaylistMediaItems({ + static async getOldPlaylists() { + const playlists = await this.findAll({ + include: { + model: this.sequelize.models.playlistMediaItem, include: [ { - model: sequelize.models.book, - include: sequelize.models.libraryItem + model: this.sequelize.models.book, + include: this.sequelize.models.libraryItem }, { - model: sequelize.models.podcastEpisode, + model: this.sequelize.models.podcastEpisode, include: { - model: sequelize.models.podcast, - include: sequelize.models.libraryItem + model: this.sequelize.models.podcast, + include: this.sequelize.models.libraryItem } } - ], - order: [['order', 'ASC']] - }) || [] - - const oldPlaylist = sequelize.models.playlist.getOldPlaylist(this) - const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId) - - let libraryItems = await sequelize.models.libraryItem.getAllOldLibraryItems({ - id: libraryItemIds - }) - - const playlistExpanded = oldPlaylist.toJSONExpanded(libraryItems) - - if (include?.includes('rssfeed')) { - const feeds = await this.getFeeds() - if (feeds?.length) { - playlistExpanded.rssFeed = sequelize.models.feed.getOldFeed(feeds[0]) - } - } - - return playlistExpanded - } - - static createFromOld(oldPlaylist) { - const playlist = this.getFromOld(oldPlaylist) - return this.create(playlist) - } - - static getFromOld(oldPlaylist) { - return { - id: oldPlaylist.id, - name: oldPlaylist.name, - description: oldPlaylist.description, - userId: oldPlaylist.userId, - libraryId: oldPlaylist.libraryId - } - } - - static removeById(playlistId) { - return this.destroy({ - where: { - id: playlistId - } - }) - } - - /** - * Get playlist by id - * @param {string} playlistId - * @returns {Promise} returns null if not found - */ - static async getById(playlistId) { - if (!playlistId) return null - const playlist = await this.findByPk(playlistId, { - include: { - model: sequelize.models.playlistMediaItem, - include: [ - { - model: sequelize.models.book, - include: sequelize.models.libraryItem - }, - { - model: sequelize.models.podcastEpisode, - include: { - model: sequelize.models.podcast, - include: sequelize.models.libraryItem - } - } - ] - }, - order: [['playlistMediaItems', 'order', 'ASC']] - }) - if (!playlist) return null - return this.getOldPlaylist(playlist) - } - - /** - * Get playlists for user and optionally for library - * @param {string} userId - * @param {[string]} libraryId optional - * @returns {Promise} - */ - static async getPlaylistsForUserAndLibrary(userId, libraryId = null) { - if (!userId && !libraryId) return [] - const whereQuery = {} - if (userId) { - whereQuery.userId = userId - } - if (libraryId) { - whereQuery.libraryId = libraryId - } - const playlists = await this.findAll({ - where: whereQuery, - include: { - model: sequelize.models.playlistMediaItem, - include: [ - { - model: sequelize.models.book, - include: sequelize.models.libraryItem - }, - { - model: sequelize.models.podcastEpisode, - include: { - model: sequelize.models.podcast, - include: sequelize.models.libraryItem - } - } - ] - }, - order: [ - [literal('name COLLATE NOCASE'), 'ASC'], - ['playlistMediaItems', 'order', 'ASC'] ] - }) - return playlists - } + }, + order: [['playlistMediaItems', 'order', 'ASC']] + }) + return playlists.map(p => this.getOldPlaylist(p)) + } - /** - * Get number of playlists for a user and library - * @param {string} userId - * @param {string} libraryId - * @returns - */ - static async getNumPlaylistsForUserAndLibrary(userId, libraryId) { - return this.count({ - where: { - userId, - libraryId - } - }) - } - - /** - * Get all playlists for mediaItemIds - * @param {string[]} mediaItemIds - * @returns {Promise} - */ - static async getPlaylistsForMediaItemIds(mediaItemIds) { - if (!mediaItemIds?.length) return [] - - const playlistMediaItemsExpanded = await sequelize.models.playlistMediaItem.findAll({ - where: { - mediaItemId: { - [Op.in]: mediaItemIds - } - }, - include: [ - { - model: sequelize.models.playlist, - include: { - model: sequelize.models.playlistMediaItem, - include: [ - { - model: sequelize.models.book, - include: sequelize.models.libraryItem - }, - { - model: sequelize.models.podcastEpisode, - include: { - model: sequelize.models.podcast, - include: sequelize.models.libraryItem - } - } - ] - } - } - ], - order: [['playlist', 'playlistMediaItems', 'order', 'ASC']] - }) - - const playlists = [] - for (const playlistMediaItem of playlistMediaItemsExpanded) { - const playlist = playlistMediaItem.playlist - if (playlists.some(p => p.id === playlist.id)) continue - - playlist.playlistMediaItems = playlist.playlistMediaItems.map(pmi => { - if (pmi.mediaItemType === 'book' && pmi.book !== undefined) { - pmi.mediaItem = pmi.book - pmi.dataValues.mediaItem = pmi.dataValues.book - } else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) { - pmi.mediaItem = pmi.podcastEpisode - pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode - } - delete pmi.book - delete pmi.dataValues.book - delete pmi.podcastEpisode - delete pmi.dataValues.podcastEpisode - return pmi - }) - playlists.push(playlist) + static getOldPlaylist(playlistExpanded) { + const items = playlistExpanded.playlistMediaItems.map(pmi => { + const libraryItemId = pmi.mediaItem?.podcast?.libraryItem?.id || pmi.mediaItem?.libraryItem?.id || null + if (!libraryItemId) { + Logger.error(`[Playlist] Invalid playlist media item - No library item id found`, JSON.stringify(pmi, null, 2)) + return null } - return playlists + return { + episodeId: pmi.mediaItemType === 'podcastEpisode' ? pmi.mediaItemId : '', + libraryItemId + } + }).filter(pmi => pmi) + + return new oldPlaylist({ + id: playlistExpanded.id, + libraryId: playlistExpanded.libraryId, + userId: playlistExpanded.userId, + name: playlistExpanded.name, + description: playlistExpanded.description, + items, + lastUpdate: playlistExpanded.updatedAt.valueOf(), + createdAt: playlistExpanded.createdAt.valueOf() + }) + } + + /** + * Get old playlist toJSONExpanded + * @param {[string[]]} include + * @returns {Promise} oldPlaylist.toJSONExpanded + */ + async getOldJsonExpanded(include) { + this.playlistMediaItems = await this.getPlaylistMediaItems({ + include: [ + { + model: this.sequelize.models.book, + include: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.podcastEpisode, + include: { + model: this.sequelize.models.podcast, + include: this.sequelize.models.libraryItem + } + } + ], + order: [['order', 'ASC']] + }) || [] + + const oldPlaylist = this.sequelize.models.playlist.getOldPlaylist(this) + const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId) + + let libraryItems = await this.sequelize.models.libraryItem.getAllOldLibraryItems({ + id: libraryItemIds + }) + + const playlistExpanded = oldPlaylist.toJSONExpanded(libraryItems) + + if (include?.includes('rssfeed')) { + const feeds = await this.getFeeds() + if (feeds?.length) { + playlistExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0]) + } + } + + return playlistExpanded + } + + static createFromOld(oldPlaylist) { + const playlist = this.getFromOld(oldPlaylist) + return this.create(playlist) + } + + static getFromOld(oldPlaylist) { + return { + id: oldPlaylist.id, + name: oldPlaylist.name, + description: oldPlaylist.description, + userId: oldPlaylist.userId, + libraryId: oldPlaylist.libraryId } } - Playlist.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - name: DataTypes.STRING, - description: DataTypes.TEXT - }, { - sequelize, - modelName: 'playlist' - }) - - const { library, user } = sequelize.models - library.hasMany(Playlist) - Playlist.belongsTo(library) - - user.hasMany(Playlist, { - onDelete: 'CASCADE' - }) - Playlist.belongsTo(user) - - Playlist.addHook('afterFind', findResult => { - if (!findResult) return - - if (!Array.isArray(findResult)) findResult = [findResult] - - for (const instance of findResult) { - if (instance.playlistMediaItems?.length) { - instance.playlistMediaItems = instance.playlistMediaItems.map(pmi => { - if (pmi.mediaItemType === 'book' && pmi.book !== undefined) { - pmi.mediaItem = pmi.book - pmi.dataValues.mediaItem = pmi.dataValues.book - } else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) { - pmi.mediaItem = pmi.podcastEpisode - pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode - } - // To prevent mistakes: - delete pmi.book - delete pmi.dataValues.book - delete pmi.podcastEpisode - delete pmi.dataValues.podcastEpisode - return pmi - }) + static removeById(playlistId) { + return this.destroy({ + where: { + id: playlistId } + }) + } + /** + * Get playlist by id + * @param {string} playlistId + * @returns {Promise} returns null if not found + */ + static async getById(playlistId) { + if (!playlistId) return null + const playlist = await this.findByPk(playlistId, { + include: { + model: this.sequelize.models.playlistMediaItem, + include: [ + { + model: this.sequelize.models.book, + include: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.podcastEpisode, + include: { + model: this.sequelize.models.podcast, + include: this.sequelize.models.libraryItem + } + } + ] + }, + order: [['playlistMediaItems', 'order', 'ASC']] + }) + if (!playlist) return null + return this.getOldPlaylist(playlist) + } + + /** + * Get playlists for user and optionally for library + * @param {string} userId + * @param {[string]} libraryId optional + * @returns {Promise} + */ + static async getPlaylistsForUserAndLibrary(userId, libraryId = null) { + if (!userId && !libraryId) return [] + const whereQuery = {} + if (userId) { + whereQuery.userId = userId } - }) + if (libraryId) { + whereQuery.libraryId = libraryId + } + const playlists = await this.findAll({ + where: whereQuery, + include: { + model: this.sequelize.models.playlistMediaItem, + include: [ + { + model: this.sequelize.models.book, + include: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.podcastEpisode, + include: { + model: this.sequelize.models.podcast, + include: this.sequelize.models.libraryItem + } + } + ] + }, + order: [ + [literal('name COLLATE NOCASE'), 'ASC'], + ['playlistMediaItems', 'order', 'ASC'] + ] + }) + return playlists + } - return Playlist -} \ No newline at end of file + /** + * Get number of playlists for a user and library + * @param {string} userId + * @param {string} libraryId + * @returns + */ + static async getNumPlaylistsForUserAndLibrary(userId, libraryId) { + return this.count({ + where: { + userId, + libraryId + } + }) + } + + /** + * Get all playlists for mediaItemIds + * @param {string[]} mediaItemIds + * @returns {Promise} + */ + static async getPlaylistsForMediaItemIds(mediaItemIds) { + if (!mediaItemIds?.length) return [] + + const playlistMediaItemsExpanded = await this.sequelize.models.playlistMediaItem.findAll({ + where: { + mediaItemId: { + [Op.in]: mediaItemIds + } + }, + include: [ + { + model: this.sequelize.models.playlist, + include: { + model: this.sequelize.models.playlistMediaItem, + include: [ + { + model: this.sequelize.models.book, + include: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.podcastEpisode, + include: { + model: this.sequelize.models.podcast, + include: this.sequelize.models.libraryItem + } + } + ] + } + } + ], + order: [['playlist', 'playlistMediaItems', 'order', 'ASC']] + }) + + const playlists = [] + for (const playlistMediaItem of playlistMediaItemsExpanded) { + const playlist = playlistMediaItem.playlist + if (playlists.some(p => p.id === playlist.id)) continue + + playlist.playlistMediaItems = playlist.playlistMediaItems.map(pmi => { + if (pmi.mediaItemType === 'book' && pmi.book !== undefined) { + pmi.mediaItem = pmi.book + pmi.dataValues.mediaItem = pmi.dataValues.book + } else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) { + pmi.mediaItem = pmi.podcastEpisode + pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode + } + delete pmi.book + delete pmi.dataValues.book + delete pmi.podcastEpisode + delete pmi.dataValues.podcastEpisode + return pmi + }) + playlists.push(playlist) + } + return playlists + } + + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + name: DataTypes.STRING, + description: DataTypes.TEXT + }, { + sequelize, + modelName: 'playlist' + }) + + const { library, user } = sequelize.models + library.hasMany(Playlist) + Playlist.belongsTo(library) + + user.hasMany(Playlist, { + onDelete: 'CASCADE' + }) + Playlist.belongsTo(user) + + Playlist.addHook('afterFind', findResult => { + if (!findResult) return + + if (!Array.isArray(findResult)) findResult = [findResult] + + for (const instance of findResult) { + if (instance.playlistMediaItems?.length) { + instance.playlistMediaItems = instance.playlistMediaItems.map(pmi => { + if (pmi.mediaItemType === 'book' && pmi.book !== undefined) { + pmi.mediaItem = pmi.book + pmi.dataValues.mediaItem = pmi.dataValues.book + } else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) { + pmi.mediaItem = pmi.podcastEpisode + pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode + } + // To prevent mistakes: + delete pmi.book + delete pmi.dataValues.book + delete pmi.podcastEpisode + delete pmi.dataValues.podcastEpisode + return pmi + }) + } + + } + }) + } +} + +module.exports = Playlist \ No newline at end of file diff --git a/server/models/PlaylistMediaItem.js b/server/models/PlaylistMediaItem.js index 915739e6..8decc7ed 100644 --- a/server/models/PlaylistMediaItem.js +++ b/server/models/PlaylistMediaItem.js @@ -1,84 +1,105 @@ const { DataTypes, Model } = require('sequelize') -module.exports = (sequelize) => { - class PlaylistMediaItem extends Model { - static removeByIds(playlistId, mediaItemId) { - return this.destroy({ - where: { - playlistId, - mediaItemId - } - }) - } +class PlaylistMediaItem extends Model { + constructor(values, options) { + super(values, options) - getMediaItem(options) { - if (!this.mediaItemType) return Promise.resolve(null) - const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` - return this[mixinMethodName](options) - } + /** @type {UUIDV4} */ + this.id + /** @type {UUIDV4} */ + this.mediaItemId + /** @type {string} */ + this.mediaItemType + /** @type {number} */ + this.order + /** @type {UUIDV4} */ + this.playlistId + /** @type {Date} */ + this.createdAt } - PlaylistMediaItem.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - mediaItemId: DataTypes.UUIDV4, - mediaItemType: DataTypes.STRING, - order: DataTypes.INTEGER - }, { - sequelize, - timestamps: true, - updatedAt: false, - modelName: 'playlistMediaItem' - }) - - const { book, podcastEpisode, playlist } = sequelize.models - - book.hasMany(PlaylistMediaItem, { - foreignKey: 'mediaItemId', - constraints: false, - scope: { - mediaItemType: 'book' - } - }) - PlaylistMediaItem.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) - - podcastEpisode.hasOne(PlaylistMediaItem, { - foreignKey: 'mediaItemId', - constraints: false, - scope: { - mediaItemType: 'podcastEpisode' - } - }) - PlaylistMediaItem.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) - - PlaylistMediaItem.addHook('afterFind', findResult => { - if (!findResult) return - - if (!Array.isArray(findResult)) findResult = [findResult] - - for (const instance of findResult) { - if (instance.mediaItemType === 'book' && instance.book !== undefined) { - instance.mediaItem = instance.book - instance.dataValues.mediaItem = instance.dataValues.book - } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { - instance.mediaItem = instance.podcastEpisode - instance.dataValues.mediaItem = instance.dataValues.podcastEpisode + static removeByIds(playlistId, mediaItemId) { + return this.destroy({ + where: { + playlistId, + mediaItemId } - // To prevent mistakes: - delete instance.book - delete instance.dataValues.book - delete instance.podcastEpisode - delete instance.dataValues.podcastEpisode - } - }) + }) + } - playlist.hasMany(PlaylistMediaItem, { - onDelete: 'CASCADE' - }) - PlaylistMediaItem.belongsTo(playlist) + getMediaItem(options) { + if (!this.mediaItemType) return Promise.resolve(null) + const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}` + return this[mixinMethodName](options) + } - return PlaylistMediaItem -} \ No newline at end of file + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + mediaItemId: DataTypes.UUIDV4, + mediaItemType: DataTypes.STRING, + order: DataTypes.INTEGER + }, { + sequelize, + timestamps: true, + updatedAt: false, + modelName: 'playlistMediaItem' + }) + + const { book, podcastEpisode, playlist } = sequelize.models + + book.hasMany(PlaylistMediaItem, { + foreignKey: 'mediaItemId', + constraints: false, + scope: { + mediaItemType: 'book' + } + }) + PlaylistMediaItem.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) + + podcastEpisode.hasOne(PlaylistMediaItem, { + foreignKey: 'mediaItemId', + constraints: false, + scope: { + mediaItemType: 'podcastEpisode' + } + }) + PlaylistMediaItem.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) + + PlaylistMediaItem.addHook('afterFind', findResult => { + if (!findResult) return + + if (!Array.isArray(findResult)) findResult = [findResult] + + for (const instance of findResult) { + if (instance.mediaItemType === 'book' && instance.book !== undefined) { + instance.mediaItem = instance.book + instance.dataValues.mediaItem = instance.dataValues.book + } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { + instance.mediaItem = instance.podcastEpisode + instance.dataValues.mediaItem = instance.dataValues.podcastEpisode + } + // To prevent mistakes: + delete instance.book + delete instance.dataValues.book + delete instance.podcastEpisode + delete instance.dataValues.podcastEpisode + } + }) + + playlist.hasMany(PlaylistMediaItem, { + onDelete: 'CASCADE' + }) + PlaylistMediaItem.belongsTo(playlist) + } +} + +module.exports = PlaylistMediaItem diff --git a/server/models/Podcast.js b/server/models/Podcast.js index f9d055c0..5f75ee3c 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -1,100 +1,155 @@ const { DataTypes, Model } = require('sequelize') -module.exports = (sequelize) => { - class Podcast extends Model { - static getOldPodcast(libraryItemExpanded) { - const podcastExpanded = libraryItemExpanded.media - const podcastEpisodes = podcastExpanded.podcastEpisodes?.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id)).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 - } - } +class Podcast extends Model { + constructor(values, options) { + super(values, options) - 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 - } + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.title + /** @type {string} */ + this.titleIgnorePrefix + /** @type {string} */ + this.author + /** @type {string} */ + this.releaseDate + /** @type {string} */ + this.feedURL + /** @type {string} */ + this.imageURL + /** @type {string} */ + this.description + /** @type {string} */ + this.itunesPageURL + /** @type {string} */ + this.itunesId + /** @type {string} */ + this.itunesArtistId + /** @type {string} */ + this.language + /** @type {string} */ + this.podcastType + /** @type {boolean} */ + this.explicit + /** @type {boolean} */ + this.autoDownloadEpisodes + /** @type {string} */ + this.autoDownloadSchedule + /** @type {Date} */ + this.lastEpisodeCheck + /** @type {number} */ + this.maxEpisodesToKeep + /** @type {string} */ + this.coverPath + /** @type {Object} */ + this.tags + /** @type {Object} */ + this.genres + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + } + + static getOldPodcast(libraryItemExpanded) { + const podcastExpanded = libraryItemExpanded.media + const podcastEpisodes = podcastExpanded.podcastEpisodes?.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id)).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 } } - Podcast.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - title: DataTypes.STRING, - titleIgnorePrefix: DataTypes.STRING, - author: DataTypes.STRING, - releaseDate: DataTypes.STRING, - feedURL: DataTypes.STRING, - imageURL: DataTypes.STRING, - description: DataTypes.TEXT, - itunesPageURL: DataTypes.STRING, - itunesId: DataTypes.STRING, - itunesArtistId: DataTypes.STRING, - language: DataTypes.STRING, - podcastType: DataTypes.STRING, - explicit: DataTypes.BOOLEAN, + 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 + } + } - autoDownloadEpisodes: DataTypes.BOOLEAN, - autoDownloadSchedule: DataTypes.STRING, - lastEpisodeCheck: DataTypes.DATE, - maxEpisodesToKeep: DataTypes.INTEGER, - maxNewEpisodesToDownload: DataTypes.INTEGER, - coverPath: DataTypes.STRING, - tags: DataTypes.JSON, - genres: DataTypes.JSON - }, { - sequelize, - modelName: 'podcast' - }) + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + title: DataTypes.STRING, + titleIgnorePrefix: DataTypes.STRING, + author: DataTypes.STRING, + releaseDate: DataTypes.STRING, + feedURL: DataTypes.STRING, + imageURL: DataTypes.STRING, + description: DataTypes.TEXT, + itunesPageURL: DataTypes.STRING, + itunesId: DataTypes.STRING, + itunesArtistId: DataTypes.STRING, + language: DataTypes.STRING, + podcastType: DataTypes.STRING, + explicit: DataTypes.BOOLEAN, - return Podcast -} \ No newline at end of file + autoDownloadEpisodes: DataTypes.BOOLEAN, + autoDownloadSchedule: DataTypes.STRING, + lastEpisodeCheck: DataTypes.DATE, + maxEpisodesToKeep: DataTypes.INTEGER, + maxNewEpisodesToDownload: DataTypes.INTEGER, + coverPath: DataTypes.STRING, + tags: DataTypes.JSON, + genres: DataTypes.JSON + }, { + sequelize, + modelName: 'podcast' + }) + } +} + +module.exports = Podcast \ No newline at end of file diff --git a/server/models/PodcastEpisode.js b/server/models/PodcastEpisode.js index 8f2d6b79..6aff7866 100644 --- a/server/models/PodcastEpisode.js +++ b/server/models/PodcastEpisode.js @@ -1,102 +1,149 @@ const { DataTypes, Model } = require('sequelize') -module.exports = (sequelize) => { - class PodcastEpisode extends Model { - 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 { - 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, - pubDate: this.pubDate, - chapters: this.chapters, - audioFile: this.audioFile, - publishedAt: this.publishedAt?.valueOf() || null, - addedAt: this.createdAt.valueOf(), - updatedAt: this.updatedAt.valueOf() +class PodcastEpisode extends Model { + constructor(values, options) { + super(values, options) + + /** @type {UUIDV4} */ + this.id + /** @type {number} */ + this.index + /** @type {string} */ + this.season + /** @type {string} */ + this.episode + /** @type {string} */ + this.episodeType + /** @type {string} */ + this.title + /** @type {string} */ + this.subtitle + /** @type {string} */ + this.description + /** @type {string} */ + this.pubDate + /** @type {string} */ + this.enclosureURL + /** @type {BigInt} */ + this.enclosureSize + /** @type {string} */ + this.enclosureType + /** @type {Date} */ + this.publishedAt + /** @type {Object} */ + this.audioFile + /** @type {Object} */ + this.chapters + /** @type {Object} */ + this.extraData + /** @type {UUIDV4} */ + this.podcastId + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + } + + getOldPodcastEpisode(libraryItemId = null) { + let enclosure = null + if (this.enclosureURL) { + enclosure = { + url: this.enclosureURL, + type: this.enclosureType, + length: this.enclosureSize !== null ? String(this.enclosureSize) : null } } - - static createFromOld(oldEpisode) { - const podcastEpisode = this.getFromOld(oldEpisode) - return this.create(podcastEpisode) - } - - static getFromOld(oldEpisode) { - const extraData = {} - if (oldEpisode.oldEpisodeId) { - extraData.oldEpisodeId = oldEpisode.oldEpisodeId - } - 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 - } + return { + 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, + pubDate: this.pubDate, + chapters: this.chapters, + audioFile: this.audioFile, + publishedAt: this.publishedAt?.valueOf() || null, + addedAt: this.createdAt.valueOf(), + updatedAt: this.updatedAt.valueOf() } } - PodcastEpisode.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - index: DataTypes.INTEGER, - season: DataTypes.STRING, - episode: DataTypes.STRING, - episodeType: DataTypes.STRING, - title: DataTypes.STRING, - subtitle: DataTypes.STRING(1000), - description: DataTypes.TEXT, - pubDate: DataTypes.STRING, - enclosureURL: DataTypes.STRING, - enclosureSize: DataTypes.BIGINT, - enclosureType: DataTypes.STRING, - publishedAt: DataTypes.DATE, + static createFromOld(oldEpisode) { + const podcastEpisode = this.getFromOld(oldEpisode) + return this.create(podcastEpisode) + } - audioFile: DataTypes.JSON, - chapters: DataTypes.JSON, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'podcastEpisode' - }) + static getFromOld(oldEpisode) { + const extraData = {} + if (oldEpisode.oldEpisodeId) { + extraData.oldEpisodeId = oldEpisode.oldEpisodeId + } + 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 + } + } - const { podcast } = sequelize.models - podcast.hasMany(PodcastEpisode, { - onDelete: 'CASCADE' - }) - PodcastEpisode.belongsTo(podcast) + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + index: DataTypes.INTEGER, + season: DataTypes.STRING, + episode: DataTypes.STRING, + episodeType: DataTypes.STRING, + title: DataTypes.STRING, + subtitle: DataTypes.STRING(1000), + description: DataTypes.TEXT, + pubDate: DataTypes.STRING, + enclosureURL: DataTypes.STRING, + enclosureSize: DataTypes.BIGINT, + enclosureType: DataTypes.STRING, + publishedAt: DataTypes.DATE, - return PodcastEpisode -} \ No newline at end of file + audioFile: DataTypes.JSON, + chapters: DataTypes.JSON, + extraData: DataTypes.JSON + }, { + sequelize, + modelName: 'podcastEpisode' + }) + + const { podcast } = sequelize.models + podcast.hasMany(PodcastEpisode, { + onDelete: 'CASCADE' + }) + PodcastEpisode.belongsTo(podcast) + } +} + +module.exports = PodcastEpisode \ No newline at end of file diff --git a/server/models/Series.js b/server/models/Series.js index e61d5e0e..f4cdbffe 100644 --- a/server/models/Series.js +++ b/server/models/Series.js @@ -2,81 +2,104 @@ const { DataTypes, Model } = require('sequelize') const oldSeries = require('../objects/entities/Series') -module.exports = (sequelize) => { - class Series extends Model { - static async getAllOldSeries() { - const series = await this.findAll() - return series.map(se => se.getOldSeries()) - } +class Series extends Model { + constructor(values, options) { + super(values, options) - getOldSeries() { - return new oldSeries({ - id: this.id, - name: this.name, - description: this.description, - libraryId: this.libraryId, - addedAt: this.createdAt.valueOf(), - updatedAt: this.updatedAt.valueOf() - }) - } + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.name + /** @type {string} */ + this.nameIgnorePrefix + /** @type {string} */ + this.description + /** @type {UUIDV4} */ + this.libraryId + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + } - static updateFromOld(oldSeries) { - const series = this.getFromOld(oldSeries) - return this.update(series, { - where: { - id: series.id - } - }) - } + static async getAllOldSeries() { + const series = await this.findAll() + return series.map(se => se.getOldSeries()) + } - static createFromOld(oldSeries) { - const series = this.getFromOld(oldSeries) - return this.create(series) - } + getOldSeries() { + return new oldSeries({ + id: this.id, + name: this.name, + description: this.description, + libraryId: this.libraryId, + addedAt: this.createdAt.valueOf(), + updatedAt: this.updatedAt.valueOf() + }) + } - static createBulkFromOld(oldSeriesObjs) { - const series = oldSeriesObjs.map(this.getFromOld) - return this.bulkCreate(series) - } - - static getFromOld(oldSeries) { - return { - id: oldSeries.id, - name: oldSeries.name, - nameIgnorePrefix: oldSeries.nameIgnorePrefix, - description: oldSeries.description, - libraryId: oldSeries.libraryId + static updateFromOld(oldSeries) { + const series = this.getFromOld(oldSeries) + return this.update(series, { + where: { + id: series.id } - } + }) + } - static removeById(seriesId) { - return this.destroy({ - where: { - id: seriesId - } - }) + static createFromOld(oldSeries) { + const series = this.getFromOld(oldSeries) + return this.create(series) + } + + static createBulkFromOld(oldSeriesObjs) { + const series = oldSeriesObjs.map(this.getFromOld) + return this.bulkCreate(series) + } + + static getFromOld(oldSeries) { + return { + id: oldSeries.id, + name: oldSeries.name, + nameIgnorePrefix: oldSeries.nameIgnorePrefix, + description: oldSeries.description, + libraryId: oldSeries.libraryId } } - Series.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - name: DataTypes.STRING, - nameIgnorePrefix: DataTypes.STRING, - description: DataTypes.TEXT - }, { - sequelize, - modelName: 'series' - }) + static removeById(seriesId) { + return this.destroy({ + where: { + id: seriesId + } + }) + } - const { library } = sequelize.models - library.hasMany(Series, { - onDelete: 'CASCADE' - }) - Series.belongsTo(library) + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + name: DataTypes.STRING, + nameIgnorePrefix: DataTypes.STRING, + description: DataTypes.TEXT + }, { + sequelize, + modelName: 'series' + }) - return Series -} \ No newline at end of file + const { library } = sequelize.models + library.hasMany(Series, { + onDelete: 'CASCADE' + }) + Series.belongsTo(library) + } +} + +module.exports = Series \ No newline at end of file diff --git a/server/models/Setting.js b/server/models/Setting.js index 9b47c227..c3348e24 100644 --- a/server/models/Setting.js +++ b/server/models/Setting.js @@ -4,42 +4,59 @@ const oldEmailSettings = require('../objects/settings/EmailSettings') const oldServerSettings = require('../objects/settings/ServerSettings') const oldNotificationSettings = require('../objects/settings/NotificationSettings') -module.exports = (sequelize) => { - class Setting extends Model { - static async getOldSettings() { - const settings = (await this.findAll()).map(se => se.value) +class Setting extends Model { + constructor(values, options) { + super(values, options) + + /** @type {string} */ + this.key + /** @type {Object} */ + this.value + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + } + + static async getOldSettings() { + const settings = (await this.findAll()).map(se => se.value) - const emailSettingsJson = settings.find(se => se.id === 'email-settings') - const serverSettingsJson = settings.find(se => se.id === 'server-settings') - const notificationSettingsJson = settings.find(se => se.id === 'notification-settings') + const emailSettingsJson = settings.find(se => se.id === 'email-settings') + const serverSettingsJson = settings.find(se => se.id === 'server-settings') + const notificationSettingsJson = settings.find(se => se.id === 'notification-settings') - return { - settings, - emailSettings: new oldEmailSettings(emailSettingsJson), - serverSettings: new oldServerSettings(serverSettingsJson), - notificationSettings: new oldNotificationSettings(notificationSettingsJson) - } - } - - static updateSettingObj(setting) { - return this.upsert({ - key: setting.id, - value: setting - }) + return { + settings, + emailSettings: new oldEmailSettings(emailSettingsJson), + serverSettings: new oldServerSettings(serverSettingsJson), + notificationSettings: new oldNotificationSettings(notificationSettingsJson) } } - Setting.init({ - key: { - type: DataTypes.STRING, - primaryKey: true - }, - value: DataTypes.JSON - }, { - sequelize, - modelName: 'setting' - }) + static updateSettingObj(setting) { + return this.upsert({ + key: setting.id, + value: setting + }) + } - return Setting -} \ No newline at end of file + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + key: { + type: DataTypes.STRING, + primaryKey: true + }, + value: DataTypes.JSON + }, { + sequelize, + modelName: 'setting' + }) + } +} + +module.exports = Setting \ No newline at end of file diff --git a/server/models/User.js b/server/models/User.js index 6d461110..6f457aa5 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -3,238 +3,273 @@ const { DataTypes, Model, Op } = require('sequelize') const Logger = require('../Logger') const oldUser = require('../objects/user/User') -module.exports = (sequelize) => { - class User extends Model { - /** - * Get all oldUsers - * @returns {Promise} - */ - static async getOldUsers() { - const users = await this.findAll({ - include: sequelize.models.mediaProgress - }) - return users.map(u => this.getOldUser(u)) - } +class User extends Model { + constructor(values, options) { + super(values, options) - static getOldUser(userExpanded) { - const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress()) + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.username + /** @type {string} */ + this.email + /** @type {string} */ + this.pash + /** @type {string} */ + this.type + /** @type {boolean} */ + this.isActive + /** @type {boolean} */ + this.isLocked + /** @type {Date} */ + this.lastSeen + /** @type {Object} */ + this.permissions + /** @type {Object} */ + this.bookmarks + /** @type {Object} */ + this.extraData + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + } - const librariesAccessible = userExpanded.permissions?.librariesAccessible || [] - const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || [] - const permissions = userExpanded.permissions || {} - delete permissions.librariesAccessible - delete permissions.itemTagsSelected + /** + * Get all oldUsers + * @returns {Promise} + */ + static async getOldUsers() { + const users = await this.findAll({ + include: this.sequelize.models.mediaProgress + }) + return users.map(u => this.getOldUser(u)) + } - return new oldUser({ - id: userExpanded.id, - oldUserId: userExpanded.extraData?.oldUserId || null, - username: userExpanded.username, - pash: userExpanded.pash, - type: userExpanded.type, - token: userExpanded.token, - mediaProgress, - seriesHideFromContinueListening: userExpanded.extraData?.seriesHideFromContinueListening || [], - bookmarks: userExpanded.bookmarks, - isActive: userExpanded.isActive, - isLocked: userExpanded.isLocked, - lastSeen: userExpanded.lastSeen?.valueOf() || null, - createdAt: userExpanded.createdAt.valueOf(), - permissions, - librariesAccessible, - itemTagsSelected - }) - } + static getOldUser(userExpanded) { + const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress()) - static createFromOld(oldUser) { - const user = this.getFromOld(oldUser) - return this.create(user) - } + const librariesAccessible = userExpanded.permissions?.librariesAccessible || [] + const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || [] + const permissions = userExpanded.permissions || {} + delete permissions.librariesAccessible + delete permissions.itemTagsSelected - static updateFromOld(oldUser) { - const user = this.getFromOld(oldUser) - return this.update(user, { - where: { - id: user.id - } - }).then((result) => result[0] > 0).catch((error) => { - Logger.error(`[User] Failed to save user ${oldUser.id}`, error) - return false - }) - } + return new oldUser({ + id: userExpanded.id, + oldUserId: userExpanded.extraData?.oldUserId || null, + username: userExpanded.username, + pash: userExpanded.pash, + type: userExpanded.type, + token: userExpanded.token, + mediaProgress, + seriesHideFromContinueListening: userExpanded.extraData?.seriesHideFromContinueListening || [], + bookmarks: userExpanded.bookmarks, + isActive: userExpanded.isActive, + isLocked: userExpanded.isLocked, + lastSeen: userExpanded.lastSeen?.valueOf() || null, + createdAt: userExpanded.createdAt.valueOf(), + permissions, + librariesAccessible, + itemTagsSelected + }) + } - static getFromOld(oldUser) { - return { - id: oldUser.id, - username: oldUser.username, - pash: oldUser.pash || null, - type: oldUser.type || null, - token: oldUser.token || null, - isActive: !!oldUser.isActive, - lastSeen: oldUser.lastSeen || null, - extraData: { - seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], - oldUserId: oldUser.oldUserId - }, - createdAt: oldUser.createdAt || Date.now(), - permissions: { - ...oldUser.permissions, - librariesAccessible: oldUser.librariesAccessible || [], - itemTagsSelected: oldUser.itemTagsSelected || [] - }, - bookmarks: oldUser.bookmarks + static createFromOld(oldUser) { + const user = this.getFromOld(oldUser) + return this.create(user) + } + + static updateFromOld(oldUser) { + const user = this.getFromOld(oldUser) + return this.update(user, { + where: { + id: user.id } - } + }).then((result) => result[0] > 0).catch((error) => { + Logger.error(`[User] Failed to save user ${oldUser.id}`, error) + return false + }) + } - static removeById(userId) { - return this.destroy({ - where: { - id: userId - } - }) - } - - /** - * Create root user - * @param {string} username - * @param {string} pash - * @param {Auth} auth - * @returns {oldUser} - */ - static async createRootUser(username, pash, auth) { - const userId = uuidv4() - - const token = await auth.generateAccessToken({ userId, username }) - - const newRoot = new oldUser({ - id: userId, - type: 'root', - username, - pash, - token, - isActive: true, - createdAt: Date.now() - }) - await this.createFromOld(newRoot) - return newRoot - } - - /** - * Get a user by id or by the old database id - * @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id - * @param {string} userId - * @returns {Promise} null if not found - */ - static async getUserByIdOrOldId(userId) { - if (!userId) return null - const user = await this.findOne({ - where: { - [Op.or]: [ - { - id: userId - }, - { - extraData: { - [Op.substring]: userId - } - } - ] - }, - include: sequelize.models.mediaProgress - }) - if (!user) return null - return this.getOldUser(user) - } - - /** - * Get user by username case insensitive - * @param {string} username - * @returns {Promise} returns null if not found - */ - static async getUserByUsername(username) { - if (!username) return null - const user = await this.findOne({ - where: { - username: { - [Op.like]: username - } - }, - include: sequelize.models.mediaProgress - }) - if (!user) return null - return this.getOldUser(user) - } - - /** - * Get user by id - * @param {string} userId - * @returns {Promise} returns null if not found - */ - static async getUserById(userId) { - if (!userId) return null - const user = await this.findByPk(userId, { - include: sequelize.models.mediaProgress - }) - if (!user) return null - return this.getOldUser(user) - } - - /** - * Get array of user id and username - * @returns {object[]} { id, username } - */ - static async getMinifiedUserObjects() { - const users = await this.findAll({ - attributes: ['id', 'username'] - }) - return users.map(u => { - return { - id: u.id, - username: u.username - } - }) - } - - /** - * Return true if root user exists - * @returns {boolean} - */ - static async getHasRootUser() { - const count = await this.count({ - where: { - type: 'root' - } - }) - return count > 0 + static getFromOld(oldUser) { + return { + id: oldUser.id, + username: oldUser.username, + pash: oldUser.pash || null, + type: oldUser.type || null, + token: oldUser.token || null, + isActive: !!oldUser.isActive, + lastSeen: oldUser.lastSeen || null, + extraData: { + seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], + oldUserId: oldUser.oldUserId + }, + createdAt: oldUser.createdAt || Date.now(), + permissions: { + ...oldUser.permissions, + librariesAccessible: oldUser.librariesAccessible || [], + itemTagsSelected: oldUser.itemTagsSelected || [] + }, + bookmarks: oldUser.bookmarks } } - User.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - username: DataTypes.STRING, - email: DataTypes.STRING, - pash: DataTypes.STRING, - type: DataTypes.STRING, - token: DataTypes.STRING, - isActive: { - type: DataTypes.BOOLEAN, - defaultValue: false - }, - isLocked: { - type: DataTypes.BOOLEAN, - defaultValue: false - }, - lastSeen: DataTypes.DATE, - permissions: DataTypes.JSON, - bookmarks: DataTypes.JSON, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'user' - }) + static removeById(userId) { + return this.destroy({ + where: { + id: userId + } + }) + } - return User -} \ No newline at end of file + /** + * Create root user + * @param {string} username + * @param {string} pash + * @param {Auth} auth + * @returns {oldUser} + */ + static async createRootUser(username, pash, auth) { + const userId = uuidv4() + + const token = await auth.generateAccessToken({ userId, username }) + + const newRoot = new oldUser({ + id: userId, + type: 'root', + username, + pash, + token, + isActive: true, + createdAt: Date.now() + }) + await this.createFromOld(newRoot) + return newRoot + } + + /** + * Get a user by id or by the old database id + * @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id + * @param {string} userId + * @returns {Promise} null if not found + */ + static async getUserByIdOrOldId(userId) { + if (!userId) return null + const user = await this.findOne({ + where: { + [Op.or]: [ + { + id: userId + }, + { + extraData: { + [Op.substring]: userId + } + } + ] + }, + include: this.sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + + /** + * Get user by username case insensitive + * @param {string} username + * @returns {Promise} returns null if not found + */ + static async getUserByUsername(username) { + if (!username) return null + const user = await this.findOne({ + where: { + username: { + [Op.like]: username + } + }, + include: this.sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + + /** + * Get user by id + * @param {string} userId + * @returns {Promise} returns null if not found + */ + static async getUserById(userId) { + if (!userId) return null + const user = await this.findByPk(userId, { + include: this.sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + + /** + * Get array of user id and username + * @returns {object[]} { id, username } + */ + static async getMinifiedUserObjects() { + const users = await this.findAll({ + attributes: ['id', 'username'] + }) + return users.map(u => { + return { + id: u.id, + username: u.username + } + }) + } + + /** + * Return true if root user exists + * @returns {boolean} + */ + static async getHasRootUser() { + const count = await this.count({ + where: { + type: 'root' + } + }) + return count > 0 + } + + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + username: DataTypes.STRING, + email: DataTypes.STRING, + pash: DataTypes.STRING, + type: DataTypes.STRING, + token: DataTypes.STRING, + isActive: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + isLocked: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + lastSeen: DataTypes.DATE, + permissions: DataTypes.JSON, + bookmarks: DataTypes.JSON, + extraData: DataTypes.JSON + }, { + sequelize, + modelName: 'user' + }) + } +} + +module.exports = User \ No newline at end of file