From c707bcf0f6c236c6ab6ef8da089e237a42b9517e Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 15 Aug 2023 18:03:43 -0500 Subject: [PATCH] Add jsdoc types for models --- server/Database.js | 20 +- server/models/Book.js | 379 ++++++++++--------- server/models/BookAuthor.js | 82 +++-- server/models/BookSeries.js | 87 +++-- server/models/Collection.js | 564 +++++++++++++++-------------- server/models/CollectionBook.js | 91 +++-- server/models/Device.js | 235 ++++++------ server/models/Feed.js | 620 +++++++++++++++++--------------- server/models/FeedEpisode.js | 189 ++++++---- server/models/Library.js | 424 ++++++++++++---------- server/models/LibraryFolder.js | 79 ++-- 11 files changed, 1537 insertions(+), 1233 deletions(-) diff --git a/server/Database.js b/server/Database.js index 7dc4f5aa..4d97574b 100644 --- a/server/Database.js +++ b/server/Database.js @@ -93,25 +93,25 @@ class Database { buildModels(force = false) { require('./models/User')(this.sequelize) - require('./models/Library')(this.sequelize) - require('./models/LibraryFolder')(this.sequelize) - require('./models/Book')(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/BookSeries')(this.sequelize) + require('./models/BookSeries').init(this.sequelize) require('./models/Author').init(this.sequelize) - require('./models/BookAuthor')(this.sequelize) - require('./models/Collection')(this.sequelize) - require('./models/CollectionBook')(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/Device')(this.sequelize) + require('./models/Device').init(this.sequelize) require('./models/PlaybackSession')(this.sequelize) - require('./models/Feed')(this.sequelize) - require('./models/FeedEpisode')(this.sequelize) + require('./models/Feed').init(this.sequelize) + require('./models/FeedEpisode').init(this.sequelize) require('./models/Setting')(this.sequelize) return this.sequelize.sync({ force, alter: false }) diff --git a/server/models/Book.js b/server/models/Book.js index b17afc6d..415064de 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -1,178 +1,231 @@ const { DataTypes, Model } = require('sequelize') const Logger = require('../Logger') -module.exports = (sequelize) => { - class Book extends Model { - static getOldBook(libraryItemExpanded) { - const bookExpanded = libraryItemExpanded.media - let authors = [] - if (bookExpanded.authors?.length) { - authors = bookExpanded.authors.map(au => { - return { - id: au.id, - name: au.name - } - }) - } else if (bookExpanded.bookAuthors?.length) { - authors = bookExpanded.bookAuthors.map(ba => { - if (ba.author) { - return { - id: ba.author.id, - name: ba.author.name - } - } else { - Logger.error(`[Book] Invalid bookExpanded bookAuthors: no author`, ba) - return null - } - }).filter(a => a) - } +class Book extends Model { + constructor(values, options) { + super(values, options) - let series = [] - if (bookExpanded.series?.length) { - series = bookExpanded.series.map(se => { - return { - id: se.id, - name: se.name, - sequence: se.bookSeries.sequence - } - }) - } else if (bookExpanded.bookSeries?.length) { - series = bookExpanded.bookSeries.map(bs => { - if (bs.series) { - return { - id: bs.series.id, - name: bs.series.name, - sequence: bs.sequence - } - } else { - Logger.error(`[Book] Invalid bookExpanded bookSeries: no series`, bs) - return null - } - }).filter(s => s) - } + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.title + /** @type {string} */ + this.titleIgnorePrefix + /** @type {string} */ + this.publishedYear + /** @type {string} */ + this.publishedDate + /** @type {string} */ + this.publisher + /** @type {string} */ + this.description + /** @type {string} */ + this.isbn + /** @type {string} */ + this.asin + /** @type {string} */ + this.language + /** @type {boolean} */ + this.explicit + /** @type {boolean} */ + this.abridged + /** @type {string} */ + this.coverPath + /** @type {number} */ + this.duration + /** @type {Object} */ + this.narrators + /** @type {Object} */ + this.audioFiles + /** @type {Object} */ + this.ebookFile + /** @type {Object} */ + this.chapters + /** @type {Object} */ + this.tags + /** @type {Object} */ + this.genres + /** @type {Date} */ + this.updatedAt + /** @type {Date} */ + this.createdAt + } - return { - id: bookExpanded.id, - libraryItemId: libraryItemExpanded.id, - coverPath: bookExpanded.coverPath, - tags: bookExpanded.tags, - audioFiles: bookExpanded.audioFiles, - chapters: bookExpanded.chapters, - ebookFile: bookExpanded.ebookFile, - metadata: { - title: bookExpanded.title, - subtitle: bookExpanded.subtitle, - authors: authors, - narrators: bookExpanded.narrators, - series: series, - genres: bookExpanded.genres, - publishedYear: bookExpanded.publishedYear, - publishedDate: bookExpanded.publishedDate, - publisher: bookExpanded.publisher, - description: bookExpanded.description, - isbn: bookExpanded.isbn, - asin: bookExpanded.asin, - language: bookExpanded.language, - explicit: bookExpanded.explicit, - abridged: bookExpanded.abridged + static getOldBook(libraryItemExpanded) { + const bookExpanded = libraryItemExpanded.media + let authors = [] + if (bookExpanded.authors?.length) { + authors = bookExpanded.authors.map(au => { + return { + id: au.id, + name: au.name } - } - } - - /** - * @param {object} oldBook - * @returns {boolean} true if updated - */ - static saveFromOld(oldBook) { - const book = this.getFromOld(oldBook) - return this.update(book, { - where: { - id: book.id - } - }).then(result => result[0] > 0).catch((error) => { - Logger.error(`[Book] Failed to save book ${book.id}`, error) - return false }) + } else if (bookExpanded.bookAuthors?.length) { + authors = bookExpanded.bookAuthors.map(ba => { + if (ba.author) { + return { + id: ba.author.id, + name: ba.author.name + } + } else { + Logger.error(`[Book] Invalid bookExpanded bookAuthors: no author`, ba) + return null + } + }).filter(a => a) } - static getFromOld(oldBook) { - return { - id: oldBook.id, - title: oldBook.metadata.title, - titleIgnorePrefix: oldBook.metadata.titleIgnorePrefix, - subtitle: oldBook.metadata.subtitle, - publishedYear: oldBook.metadata.publishedYear, - publishedDate: oldBook.metadata.publishedDate, - publisher: oldBook.metadata.publisher, - description: oldBook.metadata.description, - isbn: oldBook.metadata.isbn, - asin: oldBook.metadata.asin, - language: oldBook.metadata.language, - explicit: !!oldBook.metadata.explicit, - abridged: !!oldBook.metadata.abridged, - narrators: oldBook.metadata.narrators, - ebookFile: oldBook.ebookFile?.toJSON() || null, - coverPath: oldBook.coverPath, - duration: oldBook.duration, - audioFiles: oldBook.audioFiles?.map(af => af.toJSON()) || [], - chapters: oldBook.chapters, - tags: oldBook.tags, - genres: oldBook.metadata.genres + let series = [] + if (bookExpanded.series?.length) { + series = bookExpanded.series.map(se => { + return { + id: se.id, + name: se.name, + sequence: se.bookSeries.sequence + } + }) + } else if (bookExpanded.bookSeries?.length) { + series = bookExpanded.bookSeries.map(bs => { + if (bs.series) { + return { + id: bs.series.id, + name: bs.series.name, + sequence: bs.sequence + } + } else { + Logger.error(`[Book] Invalid bookExpanded bookSeries: no series`, bs) + return null + } + }).filter(s => s) + } + + return { + id: bookExpanded.id, + libraryItemId: libraryItemExpanded.id, + coverPath: bookExpanded.coverPath, + tags: bookExpanded.tags, + audioFiles: bookExpanded.audioFiles, + chapters: bookExpanded.chapters, + ebookFile: bookExpanded.ebookFile, + metadata: { + title: bookExpanded.title, + subtitle: bookExpanded.subtitle, + authors: authors, + narrators: bookExpanded.narrators, + series: series, + genres: bookExpanded.genres, + publishedYear: bookExpanded.publishedYear, + publishedDate: bookExpanded.publishedDate, + publisher: bookExpanded.publisher, + description: bookExpanded.description, + isbn: bookExpanded.isbn, + asin: bookExpanded.asin, + language: bookExpanded.language, + explicit: bookExpanded.explicit, + abridged: bookExpanded.abridged } } } - Book.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - title: DataTypes.STRING, - titleIgnorePrefix: DataTypes.STRING, - subtitle: DataTypes.STRING, - publishedYear: DataTypes.STRING, - publishedDate: DataTypes.STRING, - publisher: DataTypes.STRING, - description: DataTypes.TEXT, - isbn: DataTypes.STRING, - asin: DataTypes.STRING, - language: DataTypes.STRING, - explicit: DataTypes.BOOLEAN, - abridged: DataTypes.BOOLEAN, - coverPath: DataTypes.STRING, - duration: DataTypes.FLOAT, - - narrators: DataTypes.JSON, - audioFiles: DataTypes.JSON, - ebookFile: DataTypes.JSON, - chapters: DataTypes.JSON, - tags: DataTypes.JSON, - genres: DataTypes.JSON - }, { - sequelize, - modelName: 'book', - indexes: [ - { - fields: [{ - name: 'title', - collate: 'NOCASE' - }] - }, - { - fields: [{ - name: 'titleIgnorePrefix', - collate: 'NOCASE' - }] - }, - { - fields: ['publishedYear'] - }, - { - fields: ['duration'] + /** + * @param {object} oldBook + * @returns {boolean} true if updated + */ + static saveFromOld(oldBook) { + const book = this.getFromOld(oldBook) + return this.update(book, { + where: { + id: book.id } - ] - }) + }).then(result => result[0] > 0).catch((error) => { + Logger.error(`[Book] Failed to save book ${book.id}`, error) + return false + }) + } - return Book -} \ No newline at end of file + static getFromOld(oldBook) { + return { + id: oldBook.id, + title: oldBook.metadata.title, + titleIgnorePrefix: oldBook.metadata.titleIgnorePrefix, + subtitle: oldBook.metadata.subtitle, + publishedYear: oldBook.metadata.publishedYear, + publishedDate: oldBook.metadata.publishedDate, + publisher: oldBook.metadata.publisher, + description: oldBook.metadata.description, + isbn: oldBook.metadata.isbn, + asin: oldBook.metadata.asin, + language: oldBook.metadata.language, + explicit: !!oldBook.metadata.explicit, + abridged: !!oldBook.metadata.abridged, + narrators: oldBook.metadata.narrators, + ebookFile: oldBook.ebookFile?.toJSON() || null, + coverPath: oldBook.coverPath, + duration: oldBook.duration, + audioFiles: oldBook.audioFiles?.map(af => af.toJSON()) || [], + chapters: oldBook.chapters, + tags: oldBook.tags, + genres: oldBook.metadata.genres + } + } + + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + title: DataTypes.STRING, + titleIgnorePrefix: DataTypes.STRING, + subtitle: DataTypes.STRING, + publishedYear: DataTypes.STRING, + publishedDate: DataTypes.STRING, + publisher: DataTypes.STRING, + description: DataTypes.TEXT, + isbn: DataTypes.STRING, + asin: DataTypes.STRING, + language: DataTypes.STRING, + explicit: DataTypes.BOOLEAN, + abridged: DataTypes.BOOLEAN, + coverPath: DataTypes.STRING, + duration: DataTypes.FLOAT, + + narrators: DataTypes.JSON, + audioFiles: DataTypes.JSON, + ebookFile: DataTypes.JSON, + chapters: DataTypes.JSON, + tags: DataTypes.JSON, + genres: DataTypes.JSON + }, { + sequelize, + modelName: 'book', + indexes: [ + { + fields: [{ + name: 'title', + collate: 'NOCASE' + }] + }, + { + fields: [{ + name: 'titleIgnorePrefix', + collate: 'NOCASE' + }] + }, + { + fields: ['publishedYear'] + }, + { + fields: ['duration'] + } + ] + }) + } +} + +module.exports = Book \ No newline at end of file diff --git a/server/models/BookAuthor.js b/server/models/BookAuthor.js index c425d2fd..9f8860ee 100644 --- a/server/models/BookAuthor.js +++ b/server/models/BookAuthor.js @@ -1,41 +1,57 @@ const { DataTypes, Model } = require('sequelize') -module.exports = (sequelize) => { - class BookAuthor extends Model { - static removeByIds(authorId = null, bookId = null) { - const where = {} - if (authorId) where.authorId = authorId - if (bookId) where.bookId = bookId - return this.destroy({ - where - }) - } +class BookAuthor extends Model { + constructor(values, options) { + super(values, options) + + /** @type {UUIDV4} */ + this.id + /** @type {UUIDV4} */ + this.bookId + /** @type {UUIDV4} */ + this.authorId + /** @type {Date} */ + this.createdAt } - BookAuthor.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - } - }, { - sequelize, - modelName: 'bookAuthor', - timestamps: true, - updatedAt: false - }) + static removeByIds(authorId = null, bookId = null) { + const where = {} + if (authorId) where.authorId = authorId + if (bookId) where.bookId = bookId + return this.destroy({ + where + }) + } - // Super Many-to-Many - // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship - const { book, author } = sequelize.models - book.belongsToMany(author, { through: BookAuthor }) - author.belongsToMany(book, { through: BookAuthor }) + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + } + }, { + sequelize, + modelName: 'bookAuthor', + timestamps: true, + updatedAt: false + }) - book.hasMany(BookAuthor) - BookAuthor.belongsTo(book) + // Super Many-to-Many + // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship + const { book, author } = sequelize.models + book.belongsToMany(author, { through: BookAuthor }) + author.belongsToMany(book, { through: BookAuthor }) - author.hasMany(BookAuthor) - BookAuthor.belongsTo(author) + book.hasMany(BookAuthor) + BookAuthor.belongsTo(book) - return BookAuthor -} \ No newline at end of file + author.hasMany(BookAuthor) + BookAuthor.belongsTo(author) + } +} +module.exports = BookAuthor \ No newline at end of file diff --git a/server/models/BookSeries.js b/server/models/BookSeries.js index ba6581f2..680ad0c1 100644 --- a/server/models/BookSeries.js +++ b/server/models/BookSeries.js @@ -1,42 +1,61 @@ const { DataTypes, Model } = require('sequelize') -module.exports = (sequelize) => { - class BookSeries extends Model { - static removeByIds(seriesId = null, bookId = null) { - const where = {} - if (seriesId) where.seriesId = seriesId - if (bookId) where.bookId = bookId - return this.destroy({ - where - }) - } +class BookSeries extends Model { + constructor(values, options) { + super(values, options) + + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.sequence + /** @type {UUIDV4} */ + this.bookId + /** @type {UUIDV4} */ + this.seriesId + /** @type {Date} */ + this.createdAt } - BookSeries.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - sequence: DataTypes.STRING - }, { - sequelize, - modelName: 'bookSeries', - timestamps: true, - updatedAt: false - }) + static removeByIds(seriesId = null, bookId = null) { + const where = {} + if (seriesId) where.seriesId = seriesId + if (bookId) where.bookId = bookId + return this.destroy({ + where + }) + } - // Super Many-to-Many - // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship - const { book, series } = sequelize.models - book.belongsToMany(series, { through: BookSeries }) - series.belongsToMany(book, { through: BookSeries }) + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + sequence: DataTypes.STRING + }, { + sequelize, + modelName: 'bookSeries', + timestamps: true, + updatedAt: false + }) - book.hasMany(BookSeries) - BookSeries.belongsTo(book) + // Super Many-to-Many + // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship + const { book, series } = sequelize.models + book.belongsToMany(series, { through: BookSeries }) + series.belongsToMany(book, { through: BookSeries }) - series.hasMany(BookSeries) - BookSeries.belongsTo(series) + book.hasMany(BookSeries) + BookSeries.belongsTo(book) - return BookSeries -} \ No newline at end of file + series.hasMany(BookSeries) + BookSeries.belongsTo(series) + } +} + +module.exports = BookSeries \ No newline at end of file diff --git a/server/models/Collection.js b/server/models/Collection.js index f4aa9b46..9d3a8e0a 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -1,151 +1,97 @@ const { DataTypes, Model, Sequelize } = require('sequelize') const oldCollection = require('../objects/Collection') -const { areEquivalent } = require('../utils/index') -module.exports = (sequelize) => { - class Collection extends Model { - /** - * Get all old collections - * @returns {Promise} - */ - static async getOldCollections() { - const collections = await this.findAll({ - include: { - model: sequelize.models.book, - include: sequelize.models.libraryItem + +class Collection extends Model { + constructor(values, options) { + super(values, options) + + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.name + /** @type {string} */ + this.description + /** @type {UUIDV4} */ + this.libraryId + /** @type {Date} */ + this.updatedAt + /** @type {Date} */ + this.createdAt + } + /** + * Get all old collections + * @returns {Promise} + */ + static async getOldCollections() { + const collections = await this.findAll({ + include: { + model: this.sequelize.models.book, + include: this.sequelize.models.libraryItem + }, + order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']] + }) + return collections.map(c => this.getOldCollection(c)) + } + + /** + * Get all old collections toJSONExpanded, items filtered for user permissions + * @param {[oldUser]} user + * @param {[string]} libraryId + * @param {[string[]]} include + * @returns {Promise} oldCollection.toJSONExpanded + */ + static async getOldCollectionsJsonExpanded(user, libraryId, include) { + let collectionWhere = null + if (libraryId) { + collectionWhere = { + libraryId + } + } + + // Optionally include rssfeed for collection + const collectionIncludes = [] + if (include.includes('rssfeed')) { + collectionIncludes.push({ + model: this.sequelize.models.feed + }) + } + + const collections = await this.findAll({ + where: collectionWhere, + include: [ + { + model: this.sequelize.models.book, + include: [ + { + model: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + }, + + ] }, - order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] - }) - return collections.map(c => this.getOldCollection(c)) - } - - /** - * Get all old collections toJSONExpanded, items filtered for user permissions - * @param {[oldUser]} user - * @param {[string]} libraryId - * @param {[string[]]} include - * @returns {Promise} oldCollection.toJSONExpanded - */ - static async getOldCollectionsJsonExpanded(user, libraryId, include) { - let collectionWhere = null - if (libraryId) { - collectionWhere = { - libraryId - } - } - - // Optionally include rssfeed for collection - const collectionIncludes = [] - if (include.includes('rssfeed')) { - collectionIncludes.push({ - model: sequelize.models.feed - }) - } - - const collections = await this.findAll({ - where: collectionWhere, - include: [ - { - model: sequelize.models.book, - include: [ - { - model: sequelize.models.libraryItem - }, - { - model: sequelize.models.author, - through: { - attributes: [] - } - }, - { - model: sequelize.models.series, - through: { - attributes: ['sequence'] - } - }, - - ] - }, - ...collectionIncludes - ], - order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] - }) - // TODO: Handle user permission restrictions on initial query - return collections.map(c => { - const oldCollection = this.getOldCollection(c) - - // Filter books using user permissions - const books = c.books?.filter(b => { - if (user) { - if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) { - return false - } - if (b.explicit === true && !user.canAccessExplicitContent) { - return false - } - } - return true - }) || [] - - // Map to library items - const libraryItems = books.map(b => { - const libraryItem = b.libraryItem - delete b.libraryItem - libraryItem.media = b - return sequelize.models.libraryItem.getOldLibraryItem(libraryItem) - }) - - // Users with restricted permissions will not see this collection - if (!books.length && oldCollection.books.length) { - return null - } - - const collectionExpanded = oldCollection.toJSONExpanded(libraryItems) - - // Map feed if found - if (c.feeds?.length) { - collectionExpanded.rssFeed = sequelize.models.feed.getOldFeed(c.feeds[0]) - } - - return collectionExpanded - }).filter(c => c) - } - - /** - * Get old collection toJSONExpanded, items filtered for user permissions - * @param {[oldUser]} user - * @param {[string[]]} include - * @returns {Promise} oldCollection.toJSONExpanded - */ - async getOldJsonExpanded(user, include) { - this.books = await this.getBooks({ - include: [ - { - model: sequelize.models.libraryItem - }, - { - model: sequelize.models.author, - through: { - attributes: [] - } - }, - { - model: sequelize.models.series, - through: { - attributes: ['sequence'] - } - }, - - ], - order: [Sequelize.literal('`collectionBook.order` ASC')] - }) || [] - - const oldCollection = sequelize.models.collection.getOldCollection(this) + ...collectionIncludes + ], + order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']] + }) + // TODO: Handle user permission restrictions on initial query + return collections.map(c => { + const oldCollection = this.getOldCollection(c) // Filter books using user permissions - // TODO: Handle user permission restrictions on initial query - const books = this.books?.filter(b => { + const books = c.books?.filter(b => { if (user) { if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) { return false @@ -162,7 +108,7 @@ module.exports = (sequelize) => { const libraryItem = b.libraryItem delete b.libraryItem libraryItem.media = b - return sequelize.models.libraryItem.getOldLibraryItem(libraryItem) + return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem) }) // Users with restricted permissions will not see this collection @@ -172,151 +118,225 @@ module.exports = (sequelize) => { const collectionExpanded = oldCollection.toJSONExpanded(libraryItems) - if (include?.includes('rssfeed')) { - const feeds = await this.getFeeds() - if (feeds?.length) { - collectionExpanded.rssFeed = sequelize.models.feed.getOldFeed(feeds[0]) - } + // Map feed if found + if (c.feeds?.length) { + collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(c.feeds[0]) } return collectionExpanded + }).filter(c => c) + } + + /** + * Get old collection toJSONExpanded, items filtered for user permissions + * @param {[oldUser]} user + * @param {[string[]]} include + * @returns {Promise} oldCollection.toJSONExpanded + */ + async getOldJsonExpanded(user, include) { + this.books = await this.getBooks({ + include: [ + { + model: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + }, + + ], + order: [Sequelize.literal('`collectionBook.order` ASC')] + }) || [] + + const oldCollection = this.sequelize.models.collection.getOldCollection(this) + + // Filter books using user permissions + // TODO: Handle user permission restrictions on initial query + const books = this.books?.filter(b => { + if (user) { + if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) { + return false + } + if (b.explicit === true && !user.canAccessExplicitContent) { + return false + } + } + return true + }) || [] + + // Map to library items + const libraryItems = books.map(b => { + const libraryItem = b.libraryItem + delete b.libraryItem + libraryItem.media = b + return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem) + }) + + // Users with restricted permissions will not see this collection + if (!books.length && oldCollection.books.length) { + return null } - /** - * Get old collection from Collection - * @param {Collection} collectionExpanded - * @returns {oldCollection} - */ - static getOldCollection(collectionExpanded) { - const libraryItemIds = collectionExpanded.books?.map(b => b.libraryItem?.id || null).filter(lid => lid) || [] - return new oldCollection({ - id: collectionExpanded.id, - libraryId: collectionExpanded.libraryId, - name: collectionExpanded.name, - description: collectionExpanded.description, - books: libraryItemIds, - lastUpdate: collectionExpanded.updatedAt.valueOf(), - createdAt: collectionExpanded.createdAt.valueOf() - }) - } + const collectionExpanded = oldCollection.toJSONExpanded(libraryItems) - static createFromOld(oldCollection) { - const collection = this.getFromOld(oldCollection) - return this.create(collection) - } - - static getFromOld(oldCollection) { - return { - id: oldCollection.id, - name: oldCollection.name, - description: oldCollection.description, - libraryId: oldCollection.libraryId + if (include?.includes('rssfeed')) { + const feeds = await this.getFeeds() + if (feeds?.length) { + collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0]) } } - static removeById(collectionId) { - return this.destroy({ - where: { - id: collectionId - } - }) - } + return collectionExpanded + } - /** - * Get old collection by id - * @param {string} collectionId - * @returns {Promise} returns null if not found - */ - static async getOldById(collectionId) { - if (!collectionId) return null - const collection = await this.findByPk(collectionId, { - include: { - model: sequelize.models.book, - include: sequelize.models.libraryItem - }, - order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] - }) - if (!collection) return null - return this.getOldCollection(collection) - } + /** + * Get old collection from Collection + * @param {Collection} collectionExpanded + * @returns {oldCollection} + */ + static getOldCollection(collectionExpanded) { + const libraryItemIds = collectionExpanded.books?.map(b => b.libraryItem?.id || null).filter(lid => lid) || [] + return new oldCollection({ + id: collectionExpanded.id, + libraryId: collectionExpanded.libraryId, + name: collectionExpanded.name, + description: collectionExpanded.description, + books: libraryItemIds, + lastUpdate: collectionExpanded.updatedAt.valueOf(), + createdAt: collectionExpanded.createdAt.valueOf() + }) + } - /** - * Get old collection from current - * @returns {Promise} - */ - async getOld() { - this.books = await this.getBooks({ - include: [ - { - model: sequelize.models.libraryItem - }, - { - model: sequelize.models.author, - through: { - attributes: [] - } - }, - { - model: sequelize.models.series, - through: { - attributes: ['sequence'] - } - }, + static createFromOld(oldCollection) { + const collection = this.getFromOld(oldCollection) + return this.create(collection) + } - ], - order: [Sequelize.literal('`collectionBook.order` ASC')] - }) || [] - - return sequelize.models.collection.getOldCollection(this) - } - - /** - * Remove all collections belonging to library - * @param {string} libraryId - * @returns {Promise} number of collections destroyed - */ - static async removeAllForLibrary(libraryId) { - if (!libraryId) return 0 - return this.destroy({ - where: { - libraryId - } - }) - } - - static async getAllForBook(bookId) { - const collections = await this.findAll({ - include: { - model: sequelize.models.book, - where: { - id: bookId - }, - required: true, - include: sequelize.models.libraryItem - }, - order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] - }) - return collections.map(c => this.getOldCollection(c)) + static getFromOld(oldCollection) { + return { + id: oldCollection.id, + name: oldCollection.name, + description: oldCollection.description, + libraryId: oldCollection.libraryId } } - Collection.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - name: DataTypes.STRING, - description: DataTypes.TEXT - }, { - sequelize, - modelName: 'collection' - }) + static removeById(collectionId) { + return this.destroy({ + where: { + id: collectionId + } + }) + } - const { library } = sequelize.models + /** + * Get old collection by id + * @param {string} collectionId + * @returns {Promise} returns null if not found + */ + static async getOldById(collectionId) { + if (!collectionId) return null + const collection = await this.findByPk(collectionId, { + include: { + model: this.sequelize.models.book, + include: this.sequelize.models.libraryItem + }, + order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']] + }) + if (!collection) return null + return this.getOldCollection(collection) + } - library.hasMany(Collection) - Collection.belongsTo(library) + /** + * Get old collection from current + * @returns {Promise} + */ + async getOld() { + this.books = await this.getBooks({ + include: [ + { + model: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + }, - return Collection -} \ No newline at end of file + ], + order: [Sequelize.literal('`collectionBook.order` ASC')] + }) || [] + + return this.sequelize.models.collection.getOldCollection(this) + } + + /** + * Remove all collections belonging to library + * @param {string} libraryId + * @returns {Promise} number of collections destroyed + */ + static async removeAllForLibrary(libraryId) { + if (!libraryId) return 0 + return this.destroy({ + where: { + libraryId + } + }) + } + + static async getAllForBook(bookId) { + const collections = await this.findAll({ + include: { + model: this.sequelize.models.book, + where: { + id: bookId + }, + required: true, + include: this.sequelize.models.libraryItem + }, + order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']] + }) + return collections.map(c => this.getOldCollection(c)) + } + + /** + * 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: 'collection' + }) + + const { library } = sequelize.models + + library.hasMany(Collection) + Collection.belongsTo(library) + } +} + +module.exports = Collection \ No newline at end of file diff --git a/server/models/CollectionBook.js b/server/models/CollectionBook.js index 16ab70c0..aab3a1d3 100644 --- a/server/models/CollectionBook.js +++ b/server/models/CollectionBook.js @@ -1,46 +1,61 @@ const { DataTypes, Model } = require('sequelize') -module.exports = (sequelize) => { - class CollectionBook extends Model { - static removeByIds(collectionId, bookId) { - return this.destroy({ - where: { - bookId, - collectionId - } - }) - } +class CollectionBook extends Model { + constructor(values, options) { + super(values, options) + + /** @type {UUIDV4} */ + this.id + /** @type {number} */ + this.order + /** @type {UUIDV4} */ + this.bookId + /** @type {UUIDV4} */ + this.collectionId + /** @type {Date} */ + this.createdAt } - CollectionBook.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - order: DataTypes.INTEGER - }, { - sequelize, - timestamps: true, - updatedAt: false, - modelName: 'collectionBook' - }) + static removeByIds(collectionId, bookId) { + return this.destroy({ + where: { + bookId, + collectionId + } + }) + } - // Super Many-to-Many - // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship - const { book, collection } = sequelize.models - book.belongsToMany(collection, { through: CollectionBook }) - collection.belongsToMany(book, { through: CollectionBook }) + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + order: DataTypes.INTEGER + }, { + sequelize, + timestamps: true, + updatedAt: false, + modelName: 'collectionBook' + }) - book.hasMany(CollectionBook, { - onDelete: 'CASCADE' - }) - CollectionBook.belongsTo(book) + // Super Many-to-Many + // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship + const { book, collection } = sequelize.models + book.belongsToMany(collection, { through: CollectionBook }) + collection.belongsToMany(book, { through: CollectionBook }) - collection.hasMany(CollectionBook, { - onDelete: 'CASCADE' - }) - CollectionBook.belongsTo(collection) + book.hasMany(CollectionBook, { + onDelete: 'CASCADE' + }) + CollectionBook.belongsTo(book) - return CollectionBook -} \ No newline at end of file + collection.hasMany(CollectionBook, { + onDelete: 'CASCADE' + }) + CollectionBook.belongsTo(collection) + } +} + +module.exports = CollectionBook \ No newline at end of file diff --git a/server/models/Device.js b/server/models/Device.js index a8917c19..24cd2276 100644 --- a/server/models/Device.js +++ b/server/models/Device.js @@ -1,116 +1,147 @@ const { DataTypes, Model } = require('sequelize') const oldDevice = require('../objects/DeviceInfo') -module.exports = (sequelize) => { - class Device extends Model { - getOldDevice() { - let browserVersion = null - let sdkVersion = null - if (this.clientName === 'Abs Android') { - sdkVersion = this.deviceVersion || null - } else { - browserVersion = this.deviceVersion || null - } +class Device extends Model { + constructor(values, options) { + super(values, options) - return new oldDevice({ - id: this.id, - deviceId: this.deviceId, - userId: this.userId, - ipAddress: this.ipAddress, - browserName: this.extraData.browserName || null, - browserVersion, - osName: this.extraData.osName || null, - osVersion: this.extraData.osVersion || null, - clientVersion: this.clientVersion || null, - manufacturer: this.extraData.manufacturer || null, - model: this.extraData.model || null, - sdkVersion, - deviceName: this.deviceName, - clientName: this.clientName - }) + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.deviceId + /** @type {string} */ + this.clientName + /** @type {string} */ + this.clientVersion + /** @type {string} */ + this.ipAddress + /** @type {string} */ + this.deviceName + /** @type {string} */ + this.deviceVersion + /** @type {object} */ + this.extraData + /** @type {UUIDV4} */ + this.userId + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + } + + getOldDevice() { + let browserVersion = null + let sdkVersion = null + if (this.clientName === 'Abs Android') { + sdkVersion = this.deviceVersion || null + } else { + browserVersion = this.deviceVersion || null } - static async getOldDeviceByDeviceId(deviceId) { - const device = await this.findOne({ - where: { - deviceId - } - }) - if (!device) return null - return device.getOldDevice() + return new oldDevice({ + id: this.id, + deviceId: this.deviceId, + userId: this.userId, + ipAddress: this.ipAddress, + browserName: this.extraData.browserName || null, + browserVersion, + osName: this.extraData.osName || null, + osVersion: this.extraData.osVersion || null, + clientVersion: this.clientVersion || null, + manufacturer: this.extraData.manufacturer || null, + model: this.extraData.model || null, + sdkVersion, + deviceName: this.deviceName, + clientName: this.clientName + }) + } + + static async getOldDeviceByDeviceId(deviceId) { + const device = await this.findOne({ + where: { + deviceId + } + }) + if (!device) return null + return device.getOldDevice() + } + + static createFromOld(oldDevice) { + const device = this.getFromOld(oldDevice) + return this.create(device) + } + + static updateFromOld(oldDevice) { + const device = this.getFromOld(oldDevice) + return this.update(device, { + where: { + id: device.id + } + }) + } + + static getFromOld(oldDeviceInfo) { + let extraData = {} + + if (oldDeviceInfo.manufacturer) { + extraData.manufacturer = oldDeviceInfo.manufacturer + } + if (oldDeviceInfo.model) { + extraData.model = oldDeviceInfo.model + } + if (oldDeviceInfo.osName) { + extraData.osName = oldDeviceInfo.osName + } + if (oldDeviceInfo.osVersion) { + extraData.osVersion = oldDeviceInfo.osVersion + } + if (oldDeviceInfo.browserName) { + extraData.browserName = oldDeviceInfo.browserName } - static createFromOld(oldDevice) { - const device = this.getFromOld(oldDevice) - return this.create(device) - } - - static updateFromOld(oldDevice) { - const device = this.getFromOld(oldDevice) - return this.update(device, { - where: { - id: device.id - } - }) - } - - static getFromOld(oldDeviceInfo) { - let extraData = {} - - if (oldDeviceInfo.manufacturer) { - extraData.manufacturer = oldDeviceInfo.manufacturer - } - if (oldDeviceInfo.model) { - extraData.model = oldDeviceInfo.model - } - if (oldDeviceInfo.osName) { - extraData.osName = oldDeviceInfo.osName - } - if (oldDeviceInfo.osVersion) { - extraData.osVersion = oldDeviceInfo.osVersion - } - if (oldDeviceInfo.browserName) { - extraData.browserName = oldDeviceInfo.browserName - } - - return { - id: oldDeviceInfo.id, - deviceId: oldDeviceInfo.deviceId, - clientName: oldDeviceInfo.clientName || null, - clientVersion: oldDeviceInfo.clientVersion || null, - ipAddress: oldDeviceInfo.ipAddress, - deviceName: oldDeviceInfo.deviceName || null, - deviceVersion: oldDeviceInfo.sdkVersion || oldDeviceInfo.browserVersion || null, - userId: oldDeviceInfo.userId, - extraData - } + return { + id: oldDeviceInfo.id, + deviceId: oldDeviceInfo.deviceId, + clientName: oldDeviceInfo.clientName || null, + clientVersion: oldDeviceInfo.clientVersion || null, + ipAddress: oldDeviceInfo.ipAddress, + deviceName: oldDeviceInfo.deviceName || null, + deviceVersion: oldDeviceInfo.sdkVersion || oldDeviceInfo.browserVersion || null, + userId: oldDeviceInfo.userId, + extraData } } - Device.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - deviceId: DataTypes.STRING, - clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android - clientVersion: DataTypes.STRING, // e.g. Server version or mobile version - ipAddress: DataTypes.STRING, - deviceName: DataTypes.STRING, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3 - deviceVersion: DataTypes.STRING, // e.g. Browser version or Android SDK - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'device' - }) + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + deviceId: DataTypes.STRING, + clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android + clientVersion: DataTypes.STRING, // e.g. Server version or mobile version + ipAddress: DataTypes.STRING, + deviceName: DataTypes.STRING, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3 + deviceVersion: DataTypes.STRING, // e.g. Browser version or Android SDK + extraData: DataTypes.JSON + }, { + sequelize, + modelName: 'device' + }) - const { user } = sequelize.models + const { user } = sequelize.models - user.hasMany(Device, { - onDelete: 'CASCADE' - }) - Device.belongsTo(user) + user.hasMany(Device, { + onDelete: 'CASCADE' + }) + Device.belongsTo(user) + } +} - return Device -} \ No newline at end of file +module.exports = Device \ No newline at end of file diff --git a/server/models/Feed.js b/server/models/Feed.js index 25248b3c..5cf68f7c 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -1,307 +1,361 @@ const { DataTypes, Model } = require('sequelize') const oldFeed = require('../objects/Feed') const areEquivalent = require('../utils/areEquivalent') -/* - * Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/ - * Feeds can be created from LibraryItem, Collection, Playlist or Series - */ -module.exports = (sequelize) => { - class Feed extends Model { - static async getOldFeeds() { - const feeds = await this.findAll({ - include: { - model: sequelize.models.feedEpisode - } - }) - return feeds.map(f => this.getOldFeed(f)) - } - /** - * Get old feed from Feed and optionally Feed with FeedEpisodes - * @param {Feed} feedExpanded - * @returns {oldFeed} - */ - static getOldFeed(feedExpanded) { - const episodes = feedExpanded.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode()) - return new oldFeed({ - id: feedExpanded.id, - slug: feedExpanded.slug, - userId: feedExpanded.userId, - entityType: feedExpanded.entityType, - entityId: feedExpanded.entityId, - entityUpdatedAt: feedExpanded.entityUpdatedAt?.valueOf() || null, - coverPath: feedExpanded.coverPath || null, - meta: { - title: feedExpanded.title, - description: feedExpanded.description, - author: feedExpanded.author, - imageUrl: feedExpanded.imageURL, - feedUrl: feedExpanded.feedURL, - link: feedExpanded.siteURL, - explicit: feedExpanded.explicit, - type: feedExpanded.podcastType, - language: feedExpanded.language, - preventIndexing: feedExpanded.preventIndexing, - ownerName: feedExpanded.ownerName, - ownerEmail: feedExpanded.ownerEmail - }, - serverAddress: feedExpanded.serverAddress, +class Feed extends Model { + constructor(values, options) { + super(values, options) + + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.slug + /** @type {string} */ + this.entityType + /** @type {UUIDV4} */ + this.entityId + /** @type {Date} */ + this.entityUpdatedAt + /** @type {string} */ + this.serverAddress + /** @type {string} */ + this.feedURL + /** @type {string} */ + this.imageURL + /** @type {string} */ + this.siteURL + /** @type {string} */ + this.title + /** @type {string} */ + this.description + /** @type {string} */ + this.author + /** @type {string} */ + this.podcastType + /** @type {string} */ + this.language + /** @type {string} */ + this.ownerName + /** @type {string} */ + this.ownerEmail + /** @type {boolean} */ + this.explicit + /** @type {boolean} */ + this.preventIndexing + /** @type {string} */ + this.coverPath + /** @type {UUIDV4} */ + this.userId + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + } + + static async getOldFeeds() { + const feeds = await this.findAll({ + include: { + model: this.sequelize.models.feedEpisode + } + }) + return feeds.map(f => this.getOldFeed(f)) + } + + /** + * Get old feed from Feed and optionally Feed with FeedEpisodes + * @param {Feed} feedExpanded + * @returns {oldFeed} + */ + static getOldFeed(feedExpanded) { + const episodes = feedExpanded.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode()) + return new oldFeed({ + id: feedExpanded.id, + slug: feedExpanded.slug, + userId: feedExpanded.userId, + entityType: feedExpanded.entityType, + entityId: feedExpanded.entityId, + entityUpdatedAt: feedExpanded.entityUpdatedAt?.valueOf() || null, + coverPath: feedExpanded.coverPath || null, + meta: { + title: feedExpanded.title, + description: feedExpanded.description, + author: feedExpanded.author, + imageUrl: feedExpanded.imageURL, feedUrl: feedExpanded.feedURL, - episodes: episodes || [], - createdAt: feedExpanded.createdAt.valueOf(), - updatedAt: feedExpanded.updatedAt.valueOf() - }) - } + link: feedExpanded.siteURL, + explicit: feedExpanded.explicit, + type: feedExpanded.podcastType, + language: feedExpanded.language, + preventIndexing: feedExpanded.preventIndexing, + ownerName: feedExpanded.ownerName, + ownerEmail: feedExpanded.ownerEmail + }, + serverAddress: feedExpanded.serverAddress, + feedUrl: feedExpanded.feedURL, + episodes: episodes || [], + createdAt: feedExpanded.createdAt.valueOf(), + updatedAt: feedExpanded.updatedAt.valueOf() + }) + } - static removeById(feedId) { - return this.destroy({ - where: { - id: feedId - } - }) - } - - /** - * Find all library item ids that have an open feed (used in library filter) - * @returns {Promise>} array of library item ids - */ - static async findAllLibraryItemIds() { - const feeds = await this.findAll({ - attributes: ['entityId'], - where: { - entityType: 'libraryItem' - } - }) - return feeds.map(f => f.entityId).filter(f => f) || [] - } - - /** - * Find feed where and return oldFeed - * @param {object} where sequelize where object - * @returns {Promise} oldFeed - */ - static async findOneOld(where) { - if (!where) return null - const feedExpanded = await this.findOne({ - where, - include: { - model: sequelize.models.feedEpisode - } - }) - if (!feedExpanded) return null - return this.getOldFeed(feedExpanded) - } - - /** - * Find feed and return oldFeed - * @param {string} id - * @returns {Promise} oldFeed - */ - static async findByPkOld(id) { - if (!id) return null - const feedExpanded = await this.findByPk(id, { - include: { - model: sequelize.models.feedEpisode - } - }) - if (!feedExpanded) return null - return this.getOldFeed(feedExpanded) - } - - static async fullCreateFromOld(oldFeed) { - const feedObj = this.getFromOld(oldFeed) - const newFeed = await this.create(feedObj) - - if (oldFeed.episodes?.length) { - for (const oldFeedEpisode of oldFeed.episodes) { - const feedEpisode = sequelize.models.feedEpisode.getFromOld(oldFeedEpisode) - feedEpisode.feedId = newFeed.id - await sequelize.models.feedEpisode.create(feedEpisode) - } + static removeById(feedId) { + return this.destroy({ + where: { + id: feedId } - } + }) + } - static async fullUpdateFromOld(oldFeed) { - const oldFeedEpisodes = oldFeed.episodes || [] - const feedObj = this.getFromOld(oldFeed) - - const existingFeed = await this.findByPk(feedObj.id, { - include: sequelize.models.feedEpisode - }) - if (!existingFeed) return false - - let hasUpdates = false - for (const feedEpisode of existingFeed.feedEpisodes) { - const oldFeedEpisode = oldFeedEpisodes.find(ep => ep.id === feedEpisode.id) - // Episode removed - if (!oldFeedEpisode) { - feedEpisode.destroy() - } else { - let episodeHasUpdates = false - const oldFeedEpisodeCleaned = sequelize.models.feedEpisode.getFromOld(oldFeedEpisode) - for (const key in oldFeedEpisodeCleaned) { - if (!areEquivalent(oldFeedEpisodeCleaned[key], feedEpisode[key])) { - episodeHasUpdates = true - } - } - if (episodeHasUpdates) { - await feedEpisode.update(oldFeedEpisodeCleaned) - hasUpdates = true - } - } + /** + * Find all library item ids that have an open feed (used in library filter) + * @returns {Promise>} array of library item ids + */ + static async findAllLibraryItemIds() { + const feeds = await this.findAll({ + attributes: ['entityId'], + where: { + entityType: 'libraryItem' } + }) + return feeds.map(f => f.entityId).filter(f => f) || [] + } - let feedHasUpdates = false - for (const key in feedObj) { - let existingValue = existingFeed[key] - if (existingValue instanceof Date) existingValue = existingValue.valueOf() - - if (!areEquivalent(existingValue, feedObj[key])) { - feedHasUpdates = true - } + /** + * Find feed where and return oldFeed + * @param {object} where sequelize where object + * @returns {Promise} oldFeed + */ + static async findOneOld(where) { + if (!where) return null + const feedExpanded = await this.findOne({ + where, + include: { + model: this.sequelize.models.feedEpisode } + }) + if (!feedExpanded) return null + return this.getOldFeed(feedExpanded) + } - if (feedHasUpdates) { - await existingFeed.update(feedObj) - hasUpdates = true + /** + * Find feed and return oldFeed + * @param {string} id + * @returns {Promise} oldFeed + */ + static async findByPkOld(id) { + if (!id) return null + const feedExpanded = await this.findByPk(id, { + include: { + model: this.sequelize.models.feedEpisode } + }) + if (!feedExpanded) return null + return this.getOldFeed(feedExpanded) + } - return hasUpdates - } + static async fullCreateFromOld(oldFeed) { + const feedObj = this.getFromOld(oldFeed) + const newFeed = await this.create(feedObj) - static getFromOld(oldFeed) { - const oldFeedMeta = oldFeed.meta || {} - return { - id: oldFeed.id, - slug: oldFeed.slug, - entityType: oldFeed.entityType, - entityId: oldFeed.entityId, - entityUpdatedAt: oldFeed.entityUpdatedAt, - serverAddress: oldFeed.serverAddress, - feedURL: oldFeed.feedUrl, - coverPath: oldFeed.coverPath || null, - imageURL: oldFeedMeta.imageUrl, - siteURL: oldFeedMeta.link, - title: oldFeedMeta.title, - description: oldFeedMeta.description, - author: oldFeedMeta.author, - podcastType: oldFeedMeta.type || null, - language: oldFeedMeta.language || null, - ownerName: oldFeedMeta.ownerName || null, - ownerEmail: oldFeedMeta.ownerEmail || null, - explicit: !!oldFeedMeta.explicit, - preventIndexing: !!oldFeedMeta.preventIndexing, - userId: oldFeed.userId + if (oldFeed.episodes?.length) { + for (const oldFeedEpisode of oldFeed.episodes) { + const feedEpisode = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode) + feedEpisode.feedId = newFeed.id + await this.sequelize.models.feedEpisode.create(feedEpisode) } } - - getEntity(options) { - if (!this.entityType) return Promise.resolve(null) - const mixinMethodName = `get${sequelize.uppercaseFirst(this.entityType)}` - return this[mixinMethodName](options) - } } - Feed.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - slug: DataTypes.STRING, - entityType: DataTypes.STRING, - entityId: DataTypes.UUIDV4, - entityUpdatedAt: DataTypes.DATE, - serverAddress: DataTypes.STRING, - feedURL: DataTypes.STRING, - imageURL: DataTypes.STRING, - siteURL: DataTypes.STRING, - title: DataTypes.STRING, - description: DataTypes.TEXT, - author: DataTypes.STRING, - podcastType: DataTypes.STRING, - language: DataTypes.STRING, - ownerName: DataTypes.STRING, - ownerEmail: DataTypes.STRING, - explicit: DataTypes.BOOLEAN, - preventIndexing: DataTypes.BOOLEAN, - coverPath: DataTypes.STRING - }, { - sequelize, - modelName: 'feed' - }) + static async fullUpdateFromOld(oldFeed) { + const oldFeedEpisodes = oldFeed.episodes || [] + const feedObj = this.getFromOld(oldFeed) - const { user, libraryItem, collection, series, playlist } = sequelize.models + const existingFeed = await this.findByPk(feedObj.id, { + include: this.sequelize.models.feedEpisode + }) + if (!existingFeed) return false - user.hasMany(Feed) - Feed.belongsTo(user) - - libraryItem.hasMany(Feed, { - foreignKey: 'entityId', - constraints: false, - scope: { - entityType: 'libraryItem' - } - }) - Feed.belongsTo(libraryItem, { foreignKey: 'entityId', constraints: false }) - - collection.hasMany(Feed, { - foreignKey: 'entityId', - constraints: false, - scope: { - entityType: 'collection' - } - }) - Feed.belongsTo(collection, { foreignKey: 'entityId', constraints: false }) - - series.hasMany(Feed, { - foreignKey: 'entityId', - constraints: false, - scope: { - entityType: 'series' - } - }) - Feed.belongsTo(series, { foreignKey: 'entityId', constraints: false }) - - playlist.hasMany(Feed, { - foreignKey: 'entityId', - constraints: false, - scope: { - entityType: 'playlist' - } - }) - Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false }) - - Feed.addHook('afterFind', findResult => { - if (!findResult) return - - if (!Array.isArray(findResult)) findResult = [findResult] - for (const instance of findResult) { - if (instance.entityType === 'libraryItem' && instance.libraryItem !== undefined) { - instance.entity = instance.libraryItem - instance.dataValues.entity = instance.dataValues.libraryItem - } else if (instance.entityType === 'collection' && instance.collection !== undefined) { - instance.entity = instance.collection - instance.dataValues.entity = instance.dataValues.collection - } else if (instance.entityType === 'series' && instance.series !== undefined) { - instance.entity = instance.series - instance.dataValues.entity = instance.dataValues.series - } else if (instance.entityType === 'playlist' && instance.playlist !== undefined) { - instance.entity = instance.playlist - instance.dataValues.entity = instance.dataValues.playlist + let hasUpdates = false + for (const feedEpisode of existingFeed.feedEpisodes) { + const oldFeedEpisode = oldFeedEpisodes.find(ep => ep.id === feedEpisode.id) + // Episode removed + if (!oldFeedEpisode) { + feedEpisode.destroy() + } else { + let episodeHasUpdates = false + const oldFeedEpisodeCleaned = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode) + for (const key in oldFeedEpisodeCleaned) { + if (!areEquivalent(oldFeedEpisodeCleaned[key], feedEpisode[key])) { + episodeHasUpdates = true + } + } + if (episodeHasUpdates) { + await feedEpisode.update(oldFeedEpisodeCleaned) + hasUpdates = true + } } - - // To prevent mistakes: - delete instance.libraryItem - delete instance.dataValues.libraryItem - delete instance.collection - delete instance.dataValues.collection - delete instance.series - delete instance.dataValues.series - delete instance.playlist - delete instance.dataValues.playlist } - }) - return Feed -} \ No newline at end of file + let feedHasUpdates = false + for (const key in feedObj) { + let existingValue = existingFeed[key] + if (existingValue instanceof Date) existingValue = existingValue.valueOf() + + if (!areEquivalent(existingValue, feedObj[key])) { + feedHasUpdates = true + } + } + + if (feedHasUpdates) { + await existingFeed.update(feedObj) + hasUpdates = true + } + + return hasUpdates + } + + static getFromOld(oldFeed) { + const oldFeedMeta = oldFeed.meta || {} + return { + id: oldFeed.id, + slug: oldFeed.slug, + entityType: oldFeed.entityType, + entityId: oldFeed.entityId, + entityUpdatedAt: oldFeed.entityUpdatedAt, + serverAddress: oldFeed.serverAddress, + feedURL: oldFeed.feedUrl, + coverPath: oldFeed.coverPath || null, + imageURL: oldFeedMeta.imageUrl, + siteURL: oldFeedMeta.link, + title: oldFeedMeta.title, + description: oldFeedMeta.description, + author: oldFeedMeta.author, + podcastType: oldFeedMeta.type || null, + language: oldFeedMeta.language || null, + ownerName: oldFeedMeta.ownerName || null, + ownerEmail: oldFeedMeta.ownerEmail || null, + explicit: !!oldFeedMeta.explicit, + preventIndexing: !!oldFeedMeta.preventIndexing, + userId: oldFeed.userId + } + } + + getEntity(options) { + if (!this.entityType) return Promise.resolve(null) + const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.entityType)}` + return this[mixinMethodName](options) + } + + /** + * Initialize model + * + * Polymorphic association: Feeds can be created from LibraryItem, Collection, Playlist or Series + * @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 + }, + slug: DataTypes.STRING, + entityType: DataTypes.STRING, + entityId: DataTypes.UUIDV4, + entityUpdatedAt: DataTypes.DATE, + serverAddress: DataTypes.STRING, + feedURL: DataTypes.STRING, + imageURL: DataTypes.STRING, + siteURL: DataTypes.STRING, + title: DataTypes.STRING, + description: DataTypes.TEXT, + author: DataTypes.STRING, + podcastType: DataTypes.STRING, + language: DataTypes.STRING, + ownerName: DataTypes.STRING, + ownerEmail: DataTypes.STRING, + explicit: DataTypes.BOOLEAN, + preventIndexing: DataTypes.BOOLEAN, + coverPath: DataTypes.STRING + }, { + sequelize, + modelName: 'feed' + }) + + const { user, libraryItem, collection, series, playlist } = sequelize.models + + user.hasMany(Feed) + Feed.belongsTo(user) + + libraryItem.hasMany(Feed, { + foreignKey: 'entityId', + constraints: false, + scope: { + entityType: 'libraryItem' + } + }) + Feed.belongsTo(libraryItem, { foreignKey: 'entityId', constraints: false }) + + collection.hasMany(Feed, { + foreignKey: 'entityId', + constraints: false, + scope: { + entityType: 'collection' + } + }) + Feed.belongsTo(collection, { foreignKey: 'entityId', constraints: false }) + + series.hasMany(Feed, { + foreignKey: 'entityId', + constraints: false, + scope: { + entityType: 'series' + } + }) + Feed.belongsTo(series, { foreignKey: 'entityId', constraints: false }) + + playlist.hasMany(Feed, { + foreignKey: 'entityId', + constraints: false, + scope: { + entityType: 'playlist' + } + }) + Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false }) + + Feed.addHook('afterFind', findResult => { + if (!findResult) return + + if (!Array.isArray(findResult)) findResult = [findResult] + for (const instance of findResult) { + if (instance.entityType === 'libraryItem' && instance.libraryItem !== undefined) { + instance.entity = instance.libraryItem + instance.dataValues.entity = instance.dataValues.libraryItem + } else if (instance.entityType === 'collection' && instance.collection !== undefined) { + instance.entity = instance.collection + instance.dataValues.entity = instance.dataValues.collection + } else if (instance.entityType === 'series' && instance.series !== undefined) { + instance.entity = instance.series + instance.dataValues.entity = instance.dataValues.series + } else if (instance.entityType === 'playlist' && instance.playlist !== undefined) { + instance.entity = instance.playlist + instance.dataValues.entity = instance.dataValues.playlist + } + + // To prevent mistakes: + delete instance.libraryItem + delete instance.dataValues.libraryItem + delete instance.collection + delete instance.dataValues.collection + delete instance.series + delete instance.dataValues.series + delete instance.playlist + delete instance.dataValues.playlist + } + }) + } +} + +module.exports = Feed \ No newline at end of file diff --git a/server/models/FeedEpisode.js b/server/models/FeedEpisode.js index 2525b664..b126f7a6 100644 --- a/server/models/FeedEpisode.js +++ b/server/models/FeedEpisode.js @@ -1,82 +1,125 @@ const { DataTypes, Model } = require('sequelize') -module.exports = (sequelize) => { - class FeedEpisode extends Model { - getOldEpisode() { - const enclosure = { - url: this.enclosureURL, - size: this.enclosureSize, - type: this.enclosureType - } - return { - id: this.id, - title: this.title, - description: this.description, - enclosure, - pubDate: this.pubDate, - link: this.siteURL, - author: this.author, - explicit: this.explicit, - duration: this.duration, - season: this.season, - episode: this.episode, - episodeType: this.episodeType, - fullPath: this.filePath - } - } +class FeedEpisode extends Model { + constructor(values, options) { + super(values, options) - static getFromOld(oldFeedEpisode) { - return { - id: oldFeedEpisode.id, - title: oldFeedEpisode.title, - author: oldFeedEpisode.author, - description: oldFeedEpisode.description, - siteURL: oldFeedEpisode.link, - enclosureURL: oldFeedEpisode.enclosure?.url || null, - enclosureType: oldFeedEpisode.enclosure?.type || null, - enclosureSize: oldFeedEpisode.enclosure?.size || null, - pubDate: oldFeedEpisode.pubDate, - season: oldFeedEpisode.season || null, - episode: oldFeedEpisode.episode || null, - episodeType: oldFeedEpisode.episodeType || null, - duration: oldFeedEpisode.duration, - filePath: oldFeedEpisode.fullPath, - explicit: !!oldFeedEpisode.explicit - } + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.title + /** @type {string} */ + this.description + /** @type {string} */ + this.siteURL + /** @type {string} */ + this.enclosureURL + /** @type {string} */ + this.enclosureType + /** @type {BigInt} */ + this.enclosureSize + /** @type {string} */ + this.pubDate + /** @type {string} */ + this.season + /** @type {string} */ + this.episode + /** @type {string} */ + this.episodeType + /** @type {number} */ + this.duration + /** @type {string} */ + this.filePath + /** @type {boolean} */ + this.explicit + /** @type {UUIDV4} */ + this.feedId + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + } + + getOldEpisode() { + const enclosure = { + url: this.enclosureURL, + size: this.enclosureSize, + type: this.enclosureType + } + return { + id: this.id, + title: this.title, + description: this.description, + enclosure, + pubDate: this.pubDate, + link: this.siteURL, + author: this.author, + explicit: this.explicit, + duration: this.duration, + season: this.season, + episode: this.episode, + episodeType: this.episodeType, + fullPath: this.filePath } } - FeedEpisode.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - title: DataTypes.STRING, - author: DataTypes.STRING, - description: DataTypes.TEXT, - siteURL: DataTypes.STRING, - enclosureURL: DataTypes.STRING, - enclosureType: DataTypes.STRING, - enclosureSize: DataTypes.BIGINT, - pubDate: DataTypes.STRING, - season: DataTypes.STRING, - episode: DataTypes.STRING, - episodeType: DataTypes.STRING, - duration: DataTypes.FLOAT, - filePath: DataTypes.STRING, - explicit: DataTypes.BOOLEAN - }, { - sequelize, - modelName: 'feedEpisode' - }) + static getFromOld(oldFeedEpisode) { + return { + id: oldFeedEpisode.id, + title: oldFeedEpisode.title, + author: oldFeedEpisode.author, + description: oldFeedEpisode.description, + siteURL: oldFeedEpisode.link, + enclosureURL: oldFeedEpisode.enclosure?.url || null, + enclosureType: oldFeedEpisode.enclosure?.type || null, + enclosureSize: oldFeedEpisode.enclosure?.size || null, + pubDate: oldFeedEpisode.pubDate, + season: oldFeedEpisode.season || null, + episode: oldFeedEpisode.episode || null, + episodeType: oldFeedEpisode.episodeType || null, + duration: oldFeedEpisode.duration, + filePath: oldFeedEpisode.fullPath, + explicit: !!oldFeedEpisode.explicit + } + } - const { feed } = sequelize.models + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + title: DataTypes.STRING, + author: DataTypes.STRING, + description: DataTypes.TEXT, + siteURL: DataTypes.STRING, + enclosureURL: DataTypes.STRING, + enclosureType: DataTypes.STRING, + enclosureSize: DataTypes.BIGINT, + pubDate: DataTypes.STRING, + season: DataTypes.STRING, + episode: DataTypes.STRING, + episodeType: DataTypes.STRING, + duration: DataTypes.FLOAT, + filePath: DataTypes.STRING, + explicit: DataTypes.BOOLEAN + }, { + sequelize, + modelName: 'feedEpisode' + }) - feed.hasMany(FeedEpisode, { - onDelete: 'CASCADE' - }) - FeedEpisode.belongsTo(feed) + const { feed } = sequelize.models - return FeedEpisode -} \ No newline at end of file + feed.hasMany(FeedEpisode, { + onDelete: 'CASCADE' + }) + FeedEpisode.belongsTo(feed) + } +} + +module.exports = FeedEpisode \ No newline at end of file diff --git a/server/models/Library.js b/server/models/Library.js index 1fb528cd..9b1d1ace 100644 --- a/server/models/Library.js +++ b/server/models/Library.js @@ -2,217 +2,251 @@ const { DataTypes, Model } = require('sequelize') const Logger = require('../Logger') const oldLibrary = require('../objects/Library') -module.exports = (sequelize) => { - class Library extends Model { - /** - * Get all old libraries - * @returns {Promise} - */ - static async getAllOldLibraries() { - const libraries = await this.findAll({ - include: sequelize.models.libraryFolder, - order: [['displayOrder', 'ASC']] - }) - return libraries.map(lib => this.getOldLibrary(lib)) - } - /** - * Convert expanded Library to oldLibrary - * @param {Library} libraryExpanded - * @returns {Promise} - */ - static getOldLibrary(libraryExpanded) { - const folders = libraryExpanded.libraryFolders.map(folder => { - return { - id: folder.id, - fullPath: folder.path, - libraryId: folder.libraryId, - addedAt: folder.createdAt.valueOf() - } - }) - return new oldLibrary({ - id: libraryExpanded.id, - oldLibraryId: libraryExpanded.extraData?.oldLibraryId || null, - name: libraryExpanded.name, - folders, - displayOrder: libraryExpanded.displayOrder, - icon: libraryExpanded.icon, - mediaType: libraryExpanded.mediaType, - provider: libraryExpanded.provider, - settings: libraryExpanded.settings, - createdAt: libraryExpanded.createdAt.valueOf(), - lastUpdate: libraryExpanded.updatedAt.valueOf() - }) - } +class Library extends Model { + constructor(values, options) { + super(values, options) - /** - * @param {object} oldLibrary - * @returns {Library|null} - */ - static async createFromOld(oldLibrary) { - const library = this.getFromOld(oldLibrary) + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.name + /** @type {number} */ + this.displayOrder + /** @type {string} */ + this.icon + /** @type {string} */ + this.mediaType + /** @type {string} */ + this.provider + /** @type {Date} */ + this.lastScan + /** @type {string} */ + this.lastScanVersion + /** @type {Object} */ + this.settings + /** @type {Object} */ + this.extraData + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + } - library.libraryFolders = oldLibrary.folders.map(folder => { - return { - id: folder.id, - path: folder.fullPath - } - }) + /** + * Get all old libraries + * @returns {Promise} + */ + static async getAllOldLibraries() { + const libraries = await this.findAll({ + include: this.sequelize.models.libraryFolder, + order: [['displayOrder', 'ASC']] + }) + return libraries.map(lib => this.getOldLibrary(lib)) + } - return this.create(library, { - include: sequelize.models.libraryFolder - }).catch((error) => { - Logger.error(`[Library] Failed to create library ${library.id}`, error) - return null - }) - } - - /** - * Update library and library folders - * @param {object} oldLibrary - * @returns - */ - static async updateFromOld(oldLibrary) { - const existingLibrary = await this.findByPk(oldLibrary.id, { - include: sequelize.models.libraryFolder - }) - if (!existingLibrary) { - Logger.error(`[Library] Failed to update library ${oldLibrary.id} - not found`) - return null - } - - const library = this.getFromOld(oldLibrary) - - const libraryFolders = oldLibrary.folders.map(folder => { - return { - id: folder.id, - path: folder.fullPath, - libraryId: library.id - } - }) - for (const libraryFolder of libraryFolders) { - const existingLibraryFolder = existingLibrary.libraryFolders.find(lf => lf.id === libraryFolder.id) - if (!existingLibraryFolder) { - await sequelize.models.libraryFolder.create(libraryFolder) - } else if (existingLibraryFolder.path !== libraryFolder.path) { - await existingLibraryFolder.update({ path: libraryFolder.path }) - } - } - - const libraryFoldersRemoved = existingLibrary.libraryFolders.filter(lf => !libraryFolders.some(_lf => _lf.id === lf.id)) - for (const existingLibraryFolder of libraryFoldersRemoved) { - await existingLibraryFolder.destroy() - } - - return existingLibrary.update(library) - } - - static getFromOld(oldLibrary) { - const extraData = {} - if (oldLibrary.oldLibraryId) { - extraData.oldLibraryId = oldLibrary.oldLibraryId - } + /** + * Convert expanded Library to oldLibrary + * @param {Library} libraryExpanded + * @returns {Promise} + */ + static getOldLibrary(libraryExpanded) { + const folders = libraryExpanded.libraryFolders.map(folder => { return { - id: oldLibrary.id, - name: oldLibrary.name, - displayOrder: oldLibrary.displayOrder, - icon: oldLibrary.icon || null, - mediaType: oldLibrary.mediaType || null, - provider: oldLibrary.provider, - settings: oldLibrary.settings?.toJSON() || {}, - createdAt: oldLibrary.createdAt, - updatedAt: oldLibrary.lastUpdate, - extraData + id: folder.id, + fullPath: folder.path, + libraryId: folder.libraryId, + addedAt: folder.createdAt.valueOf() + } + }) + return new oldLibrary({ + id: libraryExpanded.id, + oldLibraryId: libraryExpanded.extraData?.oldLibraryId || null, + name: libraryExpanded.name, + folders, + displayOrder: libraryExpanded.displayOrder, + icon: libraryExpanded.icon, + mediaType: libraryExpanded.mediaType, + provider: libraryExpanded.provider, + settings: libraryExpanded.settings, + createdAt: libraryExpanded.createdAt.valueOf(), + lastUpdate: libraryExpanded.updatedAt.valueOf() + }) + } + + /** + * @param {object} oldLibrary + * @returns {Library|null} + */ + static async createFromOld(oldLibrary) { + const library = this.getFromOld(oldLibrary) + + library.libraryFolders = oldLibrary.folders.map(folder => { + return { + id: folder.id, + path: folder.fullPath + } + }) + + return this.create(library, { + include: this.sequelize.models.libraryFolder + }).catch((error) => { + Logger.error(`[Library] Failed to create library ${library.id}`, error) + return null + }) + } + + /** + * Update library and library folders + * @param {object} oldLibrary + * @returns + */ + static async updateFromOld(oldLibrary) { + const existingLibrary = await this.findByPk(oldLibrary.id, { + include: this.sequelize.models.libraryFolder + }) + if (!existingLibrary) { + Logger.error(`[Library] Failed to update library ${oldLibrary.id} - not found`) + return null + } + + const library = this.getFromOld(oldLibrary) + + const libraryFolders = oldLibrary.folders.map(folder => { + return { + id: folder.id, + path: folder.fullPath, + libraryId: library.id + } + }) + for (const libraryFolder of libraryFolders) { + const existingLibraryFolder = existingLibrary.libraryFolders.find(lf => lf.id === libraryFolder.id) + if (!existingLibraryFolder) { + await this.sequelize.models.libraryFolder.create(libraryFolder) + } else if (existingLibraryFolder.path !== libraryFolder.path) { + await existingLibraryFolder.update({ path: libraryFolder.path }) } } - /** - * Destroy library by id - * @param {string} libraryId - * @returns - */ - static removeById(libraryId) { - return this.destroy({ - where: { - id: libraryId - } - }) + const libraryFoldersRemoved = existingLibrary.libraryFolders.filter(lf => !libraryFolders.some(_lf => _lf.id === lf.id)) + for (const existingLibraryFolder of libraryFoldersRemoved) { + await existingLibraryFolder.destroy() } - /** - * Get all library ids - * @returns {Promise} array of library ids - */ - static async getAllLibraryIds() { - const libraries = await this.findAll({ - attributes: ['id', 'displayOrder'], - order: [['displayOrder', 'ASC']] - }) - return libraries.map(l => l.id) - } + return existingLibrary.update(library) + } - /** - * Find Library by primary key & return oldLibrary - * @param {string} libraryId - * @returns {Promise} Returns null if not found - */ - static async getOldById(libraryId) { - if (!libraryId) return null - const library = await this.findByPk(libraryId, { - include: sequelize.models.libraryFolder - }) - if (!library) return null - return this.getOldLibrary(library) + static getFromOld(oldLibrary) { + const extraData = {} + if (oldLibrary.oldLibraryId) { + extraData.oldLibraryId = oldLibrary.oldLibraryId } - - /** - * Get the largest value in the displayOrder column - * Used for setting a new libraries display order - * @returns {Promise} - */ - static getMaxDisplayOrder() { - return this.max('displayOrder') || 0 + return { + id: oldLibrary.id, + name: oldLibrary.name, + displayOrder: oldLibrary.displayOrder, + icon: oldLibrary.icon || null, + mediaType: oldLibrary.mediaType || null, + provider: oldLibrary.provider, + settings: oldLibrary.settings?.toJSON() || {}, + createdAt: oldLibrary.createdAt, + updatedAt: oldLibrary.lastUpdate, + extraData } + } - /** - * Updates displayOrder to be sequential - * Used after removing a library - */ - static async resetDisplayOrder() { - const libraries = await this.findAll({ - order: [['displayOrder', 'ASC']] - }) - for (let i = 0; i < libraries.length; i++) { - const library = libraries[i] - if (library.displayOrder !== i + 1) { - Logger.dev(`[Library] Updating display order of library from ${library.displayOrder} to ${i + 1}`) - await library.update({ displayOrder: i + 1 }).catch((error) => { - Logger.error(`[Library] Failed to update library display order to ${i + 1}`, error) - }) - } + /** + * Destroy library by id + * @param {string} libraryId + * @returns + */ + static removeById(libraryId) { + return this.destroy({ + where: { + id: libraryId + } + }) + } + + /** + * Get all library ids + * @returns {Promise} array of library ids + */ + static async getAllLibraryIds() { + const libraries = await this.findAll({ + attributes: ['id', 'displayOrder'], + order: [['displayOrder', 'ASC']] + }) + return libraries.map(l => l.id) + } + + /** + * Find Library by primary key & return oldLibrary + * @param {string} libraryId + * @returns {Promise} Returns null if not found + */ + static async getOldById(libraryId) { + if (!libraryId) return null + const library = await this.findByPk(libraryId, { + include: sequelize.models.libraryFolder + }) + if (!library) return null + return this.getOldLibrary(library) + } + + /** + * Get the largest value in the displayOrder column + * Used for setting a new libraries display order + * @returns {Promise} + */ + static getMaxDisplayOrder() { + return this.max('displayOrder') || 0 + } + + /** + * Updates displayOrder to be sequential + * Used after removing a library + */ + static async resetDisplayOrder() { + const libraries = await this.findAll({ + order: [['displayOrder', 'ASC']] + }) + for (let i = 0; i < libraries.length; i++) { + const library = libraries[i] + if (library.displayOrder !== i + 1) { + Logger.dev(`[Library] Updating display order of library from ${library.displayOrder} to ${i + 1}`) + await library.update({ displayOrder: i + 1 }).catch((error) => { + Logger.error(`[Library] Failed to update library display order to ${i + 1}`, error) + }) } } } - Library.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - name: DataTypes.STRING, - displayOrder: DataTypes.INTEGER, - icon: DataTypes.STRING, - mediaType: DataTypes.STRING, - provider: DataTypes.STRING, - lastScan: DataTypes.DATE, - lastScanVersion: DataTypes.STRING, - settings: DataTypes.JSON, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: '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, + displayOrder: DataTypes.INTEGER, + icon: DataTypes.STRING, + mediaType: DataTypes.STRING, + provider: DataTypes.STRING, + lastScan: DataTypes.DATE, + lastScanVersion: DataTypes.STRING, + settings: DataTypes.JSON, + extraData: DataTypes.JSON + }, { + sequelize, + modelName: 'library' + }) + } +} - return Library -} \ No newline at end of file +module.exports = Library \ No newline at end of file diff --git a/server/models/LibraryFolder.js b/server/models/LibraryFolder.js index 1ba240e7..6ae7a8ac 100644 --- a/server/models/LibraryFolder.js +++ b/server/models/LibraryFolder.js @@ -1,36 +1,55 @@ const { DataTypes, Model } = require('sequelize') -module.exports = (sequelize) => { - class LibraryFolder extends Model { - /** - * Gets all library folder path strings - * @returns {Promise} array of library folder paths - */ - static async getAllLibraryFolderPaths() { - const libraryFolders = await this.findAll({ - attributes: ['path'] - }) - return libraryFolders.map(l => l.path) - } +class LibraryFolder extends Model { + constructor(values, options) { + super(values, options) + + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.path + /** @type {UUIDV4} */ + this.libraryId + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt } - LibraryFolder.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - path: DataTypes.STRING - }, { - sequelize, - modelName: 'libraryFolder' - }) + /** + * Gets all library folder path strings + * @returns {Promise} array of library folder paths + */ + static async getAllLibraryFolderPaths() { + const libraryFolders = await this.findAll({ + attributes: ['path'] + }) + return libraryFolders.map(l => l.path) + } - const { library } = sequelize.models - library.hasMany(LibraryFolder, { - onDelete: 'CASCADE' - }) - LibraryFolder.belongsTo(library) + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + path: DataTypes.STRING + }, { + sequelize, + modelName: 'libraryFolder' + }) - return LibraryFolder -} \ No newline at end of file + const { library } = sequelize.models + library.hasMany(LibraryFolder, { + onDelete: 'CASCADE' + }) + LibraryFolder.belongsTo(library) + } +} + +module.exports = LibraryFolder \ No newline at end of file