Add jsdoc types for models

This commit is contained in:
advplyr 2023-08-15 18:03:43 -05:00
parent 7afda1295b
commit c707bcf0f6
11 changed files with 1537 additions and 1233 deletions

View File

@ -93,25 +93,25 @@ class Database {
buildModels(force = false) { buildModels(force = false) {
require('./models/User')(this.sequelize) require('./models/User')(this.sequelize)
require('./models/Library')(this.sequelize) require('./models/Library').init(this.sequelize)
require('./models/LibraryFolder')(this.sequelize) require('./models/LibraryFolder').init(this.sequelize)
require('./models/Book')(this.sequelize) require('./models/Book').init(this.sequelize)
require('./models/Podcast')(this.sequelize) require('./models/Podcast')(this.sequelize)
require('./models/PodcastEpisode')(this.sequelize) require('./models/PodcastEpisode')(this.sequelize)
require('./models/LibraryItem')(this.sequelize) require('./models/LibraryItem')(this.sequelize)
require('./models/MediaProgress')(this.sequelize) require('./models/MediaProgress')(this.sequelize)
require('./models/Series')(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/Author').init(this.sequelize)
require('./models/BookAuthor')(this.sequelize) require('./models/BookAuthor').init(this.sequelize)
require('./models/Collection')(this.sequelize) require('./models/Collection').init(this.sequelize)
require('./models/CollectionBook')(this.sequelize) require('./models/CollectionBook').init(this.sequelize)
require('./models/Playlist')(this.sequelize) require('./models/Playlist')(this.sequelize)
require('./models/PlaylistMediaItem')(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/PlaybackSession')(this.sequelize)
require('./models/Feed')(this.sequelize) require('./models/Feed').init(this.sequelize)
require('./models/FeedEpisode')(this.sequelize) require('./models/FeedEpisode').init(this.sequelize)
require('./models/Setting')(this.sequelize) require('./models/Setting')(this.sequelize)
return this.sequelize.sync({ force, alter: false }) return this.sequelize.sync({ force, alter: false })

View File

@ -1,178 +1,231 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
const Logger = require('../Logger') const Logger = require('../Logger')
module.exports = (sequelize) => { class Book extends Model {
class Book extends Model { constructor(values, options) {
static getOldBook(libraryItemExpanded) { super(values, options)
const bookExpanded = libraryItemExpanded.media
let authors = []
if (bookExpanded.authors?.length) {
authors = bookExpanded.authors.map(au => {
return {
id: au.id,
name: au.name
}
})
} else if (bookExpanded.bookAuthors?.length) {
authors = bookExpanded.bookAuthors.map(ba => {
if (ba.author) {
return {
id: ba.author.id,
name: ba.author.name
}
} else {
Logger.error(`[Book] Invalid bookExpanded bookAuthors: no author`, ba)
return null
}
}).filter(a => a)
}
let series = [] /** @type {UUIDV4} */
if (bookExpanded.series?.length) { this.id
series = bookExpanded.series.map(se => { /** @type {string} */
return { this.title
id: se.id, /** @type {string} */
name: se.name, this.titleIgnorePrefix
sequence: se.bookSeries.sequence /** @type {string} */
} this.publishedYear
}) /** @type {string} */
} else if (bookExpanded.bookSeries?.length) { this.publishedDate
series = bookExpanded.bookSeries.map(bs => { /** @type {string} */
if (bs.series) { this.publisher
return { /** @type {string} */
id: bs.series.id, this.description
name: bs.series.name, /** @type {string} */
sequence: bs.sequence this.isbn
} /** @type {string} */
} else { this.asin
Logger.error(`[Book] Invalid bookExpanded bookSeries: no series`, bs) /** @type {string} */
return null this.language
} /** @type {boolean} */
}).filter(s => s) 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 { static getOldBook(libraryItemExpanded) {
id: bookExpanded.id, const bookExpanded = libraryItemExpanded.media
libraryItemId: libraryItemExpanded.id, let authors = []
coverPath: bookExpanded.coverPath, if (bookExpanded.authors?.length) {
tags: bookExpanded.tags, authors = bookExpanded.authors.map(au => {
audioFiles: bookExpanded.audioFiles, return {
chapters: bookExpanded.chapters, id: au.id,
ebookFile: bookExpanded.ebookFile, name: au.name
metadata: {
title: bookExpanded.title,
subtitle: bookExpanded.subtitle,
authors: authors,
narrators: bookExpanded.narrators,
series: series,
genres: bookExpanded.genres,
publishedYear: bookExpanded.publishedYear,
publishedDate: bookExpanded.publishedDate,
publisher: bookExpanded.publisher,
description: bookExpanded.description,
isbn: bookExpanded.isbn,
asin: bookExpanded.asin,
language: bookExpanded.language,
explicit: bookExpanded.explicit,
abridged: bookExpanded.abridged
} }
}
}
/**
* @param {object} oldBook
* @returns {boolean} true if updated
*/
static saveFromOld(oldBook) {
const book = this.getFromOld(oldBook)
return this.update(book, {
where: {
id: book.id
}
}).then(result => result[0] > 0).catch((error) => {
Logger.error(`[Book] Failed to save book ${book.id}`, error)
return false
}) })
} 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) { let series = []
return { if (bookExpanded.series?.length) {
id: oldBook.id, series = bookExpanded.series.map(se => {
title: oldBook.metadata.title, return {
titleIgnorePrefix: oldBook.metadata.titleIgnorePrefix, id: se.id,
subtitle: oldBook.metadata.subtitle, name: se.name,
publishedYear: oldBook.metadata.publishedYear, sequence: se.bookSeries.sequence
publishedDate: oldBook.metadata.publishedDate, }
publisher: oldBook.metadata.publisher, })
description: oldBook.metadata.description, } else if (bookExpanded.bookSeries?.length) {
isbn: oldBook.metadata.isbn, series = bookExpanded.bookSeries.map(bs => {
asin: oldBook.metadata.asin, if (bs.series) {
language: oldBook.metadata.language, return {
explicit: !!oldBook.metadata.explicit, id: bs.series.id,
abridged: !!oldBook.metadata.abridged, name: bs.series.name,
narrators: oldBook.metadata.narrators, sequence: bs.sequence
ebookFile: oldBook.ebookFile?.toJSON() || null, }
coverPath: oldBook.coverPath, } else {
duration: oldBook.duration, Logger.error(`[Book] Invalid bookExpanded bookSeries: no series`, bs)
audioFiles: oldBook.audioFiles?.map(af => af.toJSON()) || [], return null
chapters: oldBook.chapters, }
tags: oldBook.tags, }).filter(s => s)
genres: oldBook.metadata.genres }
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: { * @param {object} oldBook
type: DataTypes.UUID, * @returns {boolean} true if updated
defaultValue: DataTypes.UUIDV4, */
primaryKey: true static saveFromOld(oldBook) {
}, const book = this.getFromOld(oldBook)
title: DataTypes.STRING, return this.update(book, {
titleIgnorePrefix: DataTypes.STRING, where: {
subtitle: DataTypes.STRING, id: book.id
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']
} }
] }).then(result => result[0] > 0).catch((error) => {
}) Logger.error(`[Book] Failed to save book ${book.id}`, error)
return false
})
}
return Book 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

View File

@ -1,41 +1,57 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class BookAuthor extends Model {
class BookAuthor extends Model { constructor(values, options) {
static removeByIds(authorId = null, bookId = null) { super(values, options)
const where = {}
if (authorId) where.authorId = authorId /** @type {UUIDV4} */
if (bookId) where.bookId = bookId this.id
return this.destroy({ /** @type {UUIDV4} */
where this.bookId
}) /** @type {UUIDV4} */
} this.authorId
/** @type {Date} */
this.createdAt
} }
BookAuthor.init({ static removeByIds(authorId = null, bookId = null) {
id: { const where = {}
type: DataTypes.UUID, if (authorId) where.authorId = authorId
defaultValue: DataTypes.UUIDV4, if (bookId) where.bookId = bookId
primaryKey: true return this.destroy({
} where
}, { })
sequelize, }
modelName: 'bookAuthor',
timestamps: true,
updatedAt: false
})
// 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 * Initialize model
const { book, author } = sequelize.models * @param {import('../Database').sequelize} sequelize
book.belongsToMany(author, { through: BookAuthor }) */
author.belongsToMany(book, { through: BookAuthor }) static init(sequelize) {
super.init({
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
}
}, {
sequelize,
modelName: 'bookAuthor',
timestamps: true,
updatedAt: false
})
book.hasMany(BookAuthor) // Super Many-to-Many
BookAuthor.belongsTo(book) // 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) book.hasMany(BookAuthor)
BookAuthor.belongsTo(author) BookAuthor.belongsTo(book)
return BookAuthor author.hasMany(BookAuthor)
} BookAuthor.belongsTo(author)
}
}
module.exports = BookAuthor

View File

@ -1,42 +1,61 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class BookSeries extends Model {
class BookSeries extends Model { constructor(values, options) {
static removeByIds(seriesId = null, bookId = null) { super(values, options)
const where = {}
if (seriesId) where.seriesId = seriesId /** @type {UUIDV4} */
if (bookId) where.bookId = bookId this.id
return this.destroy({ /** @type {string} */
where this.sequence
}) /** @type {UUIDV4} */
} this.bookId
/** @type {UUIDV4} */
this.seriesId
/** @type {Date} */
this.createdAt
} }
BookSeries.init({ static removeByIds(seriesId = null, bookId = null) {
id: { const where = {}
type: DataTypes.UUID, if (seriesId) where.seriesId = seriesId
defaultValue: DataTypes.UUIDV4, if (bookId) where.bookId = bookId
primaryKey: true return this.destroy({
}, where
sequence: DataTypes.STRING })
}, { }
sequelize,
modelName: 'bookSeries',
timestamps: true,
updatedAt: false
})
// 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 * Initialize model
const { book, series } = sequelize.models * @param {import('../Database').sequelize} sequelize
book.belongsToMany(series, { through: BookSeries }) */
series.belongsToMany(book, { through: BookSeries }) 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) // Super Many-to-Many
BookSeries.belongsTo(book) // 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) book.hasMany(BookSeries)
BookSeries.belongsTo(series) BookSeries.belongsTo(book)
return BookSeries series.hasMany(BookSeries)
} BookSeries.belongsTo(series)
}
}
module.exports = BookSeries

View File

@ -1,151 +1,97 @@
const { DataTypes, Model, Sequelize } = require('sequelize') const { DataTypes, Model, Sequelize } = require('sequelize')
const oldCollection = require('../objects/Collection') const oldCollection = require('../objects/Collection')
const { areEquivalent } = require('../utils/index')
module.exports = (sequelize) => {
class Collection extends Model { class Collection extends Model {
/** constructor(values, options) {
* Get all old collections super(values, options)
* @returns {Promise<oldCollection[]>}
*/ /** @type {UUIDV4} */
static async getOldCollections() { this.id
const collections = await this.findAll({ /** @type {string} */
include: { this.name
model: sequelize.models.book, /** @type {string} */
include: sequelize.models.libraryItem this.description
/** @type {UUIDV4} */
this.libraryId
/** @type {Date} */
this.updatedAt
/** @type {Date} */
this.createdAt
}
/**
* Get all old collections
* @returns {Promise<oldCollection[]>}
*/
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<object[]>} 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']] ...collectionIncludes
}) ],
return collections.map(c => this.getOldCollection(c)) order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']]
} })
// TODO: Handle user permission restrictions on initial query
/** return collections.map(c => {
* Get all old collections toJSONExpanded, items filtered for user permissions const oldCollection = this.getOldCollection(c)
* @param {[oldUser]} user
* @param {[string]} libraryId
* @param {[string[]]} include
* @returns {Promise<object[]>} 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<object>} 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)
// Filter books using user permissions // Filter books using user permissions
// TODO: Handle user permission restrictions on initial query const books = c.books?.filter(b => {
const books = this.books?.filter(b => {
if (user) { if (user) {
if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) { if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) {
return false return false
@ -162,7 +108,7 @@ module.exports = (sequelize) => {
const libraryItem = b.libraryItem const libraryItem = b.libraryItem
delete b.libraryItem delete b.libraryItem
libraryItem.media = b 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 // Users with restricted permissions will not see this collection
@ -172,151 +118,225 @@ module.exports = (sequelize) => {
const collectionExpanded = oldCollection.toJSONExpanded(libraryItems) const collectionExpanded = oldCollection.toJSONExpanded(libraryItems)
if (include?.includes('rssfeed')) { // Map feed if found
const feeds = await this.getFeeds() if (c.feeds?.length) {
if (feeds?.length) { collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(c.feeds[0])
collectionExpanded.rssFeed = sequelize.models.feed.getOldFeed(feeds[0])
}
} }
return collectionExpanded return collectionExpanded
}).filter(c => c)
}
/**
* Get old collection toJSONExpanded, items filtered for user permissions
* @param {[oldUser]} user
* @param {[string[]]} include
* @returns {Promise<object>} 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
} }
/** const collectionExpanded = oldCollection.toJSONExpanded(libraryItems)
* 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()
})
}
static createFromOld(oldCollection) { if (include?.includes('rssfeed')) {
const collection = this.getFromOld(oldCollection) const feeds = await this.getFeeds()
return this.create(collection) if (feeds?.length) {
} collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0])
static getFromOld(oldCollection) {
return {
id: oldCollection.id,
name: oldCollection.name,
description: oldCollection.description,
libraryId: oldCollection.libraryId
} }
} }
static removeById(collectionId) { return collectionExpanded
return this.destroy({ }
where: {
id: collectionId
}
})
}
/** /**
* Get old collection by id * Get old collection from Collection
* @param {string} collectionId * @param {Collection} collectionExpanded
* @returns {Promise<oldCollection|null>} returns null if not found * @returns {oldCollection}
*/ */
static async getOldById(collectionId) { static getOldCollection(collectionExpanded) {
if (!collectionId) return null const libraryItemIds = collectionExpanded.books?.map(b => b.libraryItem?.id || null).filter(lid => lid) || []
const collection = await this.findByPk(collectionId, { return new oldCollection({
include: { id: collectionExpanded.id,
model: sequelize.models.book, libraryId: collectionExpanded.libraryId,
include: sequelize.models.libraryItem name: collectionExpanded.name,
}, description: collectionExpanded.description,
order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] books: libraryItemIds,
}) lastUpdate: collectionExpanded.updatedAt.valueOf(),
if (!collection) return null createdAt: collectionExpanded.createdAt.valueOf()
return this.getOldCollection(collection) })
} }
/** static createFromOld(oldCollection) {
* Get old collection from current const collection = this.getFromOld(oldCollection)
* @returns {Promise<oldCollection>} return this.create(collection)
*/ }
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 getFromOld(oldCollection) {
order: [Sequelize.literal('`collectionBook.order` ASC')] return {
}) || [] id: oldCollection.id,
name: oldCollection.name,
return sequelize.models.collection.getOldCollection(this) description: oldCollection.description,
} libraryId: oldCollection.libraryId
/**
* Remove all collections belonging to library
* @param {string} libraryId
* @returns {Promise<number>} 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))
} }
} }
Collection.init({ static removeById(collectionId) {
id: { return this.destroy({
type: DataTypes.UUID, where: {
defaultValue: DataTypes.UUIDV4, id: collectionId
primaryKey: true }
}, })
name: DataTypes.STRING, }
description: DataTypes.TEXT
}, {
sequelize,
modelName: 'collection'
})
const { library } = sequelize.models /**
* Get old collection by id
* @param {string} collectionId
* @returns {Promise<oldCollection|null>} 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<oldCollection>}
*/
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 ],
} 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>} 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

View File

@ -1,46 +1,61 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class CollectionBook extends Model {
class CollectionBook extends Model { constructor(values, options) {
static removeByIds(collectionId, bookId) { super(values, options)
return this.destroy({
where: { /** @type {UUIDV4} */
bookId, this.id
collectionId /** @type {number} */
} this.order
}) /** @type {UUIDV4} */
} this.bookId
/** @type {UUIDV4} */
this.collectionId
/** @type {Date} */
this.createdAt
} }
CollectionBook.init({ static removeByIds(collectionId, bookId) {
id: { return this.destroy({
type: DataTypes.UUID, where: {
defaultValue: DataTypes.UUIDV4, bookId,
primaryKey: true collectionId
}, }
order: DataTypes.INTEGER })
}, { }
sequelize,
timestamps: true,
updatedAt: false,
modelName: 'collectionBook'
})
// Super Many-to-Many static init(sequelize) {
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship super.init({
const { book, collection } = sequelize.models id: {
book.belongsToMany(collection, { through: CollectionBook }) type: DataTypes.UUID,
collection.belongsToMany(book, { through: CollectionBook }) defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
order: DataTypes.INTEGER
}, {
sequelize,
timestamps: true,
updatedAt: false,
modelName: 'collectionBook'
})
book.hasMany(CollectionBook, { // Super Many-to-Many
onDelete: 'CASCADE' // 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
CollectionBook.belongsTo(book) book.belongsToMany(collection, { through: CollectionBook })
collection.belongsToMany(book, { through: CollectionBook })
collection.hasMany(CollectionBook, { book.hasMany(CollectionBook, {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
CollectionBook.belongsTo(collection) CollectionBook.belongsTo(book)
return CollectionBook collection.hasMany(CollectionBook, {
} onDelete: 'CASCADE'
})
CollectionBook.belongsTo(collection)
}
}
module.exports = CollectionBook

View File

@ -1,116 +1,147 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
const oldDevice = require('../objects/DeviceInfo') const oldDevice = require('../objects/DeviceInfo')
module.exports = (sequelize) => { class Device extends Model {
class Device extends Model { constructor(values, options) {
getOldDevice() { super(values, options)
let browserVersion = null
let sdkVersion = null
if (this.clientName === 'Abs Android') {
sdkVersion = this.deviceVersion || null
} else {
browserVersion = this.deviceVersion || null
}
return new oldDevice({ /** @type {UUIDV4} */
id: this.id, this.id
deviceId: this.deviceId, /** @type {string} */
userId: this.userId, this.deviceId
ipAddress: this.ipAddress, /** @type {string} */
browserName: this.extraData.browserName || null, this.clientName
browserVersion, /** @type {string} */
osName: this.extraData.osName || null, this.clientVersion
osVersion: this.extraData.osVersion || null, /** @type {string} */
clientVersion: this.clientVersion || null, this.ipAddress
manufacturer: this.extraData.manufacturer || null, /** @type {string} */
model: this.extraData.model || null, this.deviceName
sdkVersion, /** @type {string} */
deviceName: this.deviceName, this.deviceVersion
clientName: this.clientName /** @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) { return new oldDevice({
const device = await this.findOne({ id: this.id,
where: { deviceId: this.deviceId,
deviceId userId: this.userId,
} ipAddress: this.ipAddress,
}) browserName: this.extraData.browserName || null,
if (!device) return null browserVersion,
return device.getOldDevice() 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) { return {
const device = this.getFromOld(oldDevice) id: oldDeviceInfo.id,
return this.create(device) deviceId: oldDeviceInfo.deviceId,
} clientName: oldDeviceInfo.clientName || null,
clientVersion: oldDeviceInfo.clientVersion || null,
static updateFromOld(oldDevice) { ipAddress: oldDeviceInfo.ipAddress,
const device = this.getFromOld(oldDevice) deviceName: oldDeviceInfo.deviceName || null,
return this.update(device, { deviceVersion: oldDeviceInfo.sdkVersion || oldDeviceInfo.browserVersion || null,
where: { userId: oldDeviceInfo.userId,
id: device.id extraData
}
})
}
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
}
} }
} }
Device.init({ /**
id: { * Initialize model
type: DataTypes.UUID, * @param {import('../Database').sequelize} sequelize
defaultValue: DataTypes.UUIDV4, */
primaryKey: true static init(sequelize) {
}, super.init({
deviceId: DataTypes.STRING, id: {
clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android type: DataTypes.UUID,
clientVersion: DataTypes.STRING, // e.g. Server version or mobile version defaultValue: DataTypes.UUIDV4,
ipAddress: DataTypes.STRING, primaryKey: true
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 deviceId: DataTypes.STRING,
extraData: DataTypes.JSON clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android
}, { clientVersion: DataTypes.STRING, // e.g. Server version or mobile version
sequelize, ipAddress: DataTypes.STRING,
modelName: 'device' 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, { user.hasMany(Device, {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
Device.belongsTo(user) Device.belongsTo(user)
}
}
return Device module.exports = Device
}

View File

@ -1,307 +1,361 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
const oldFeed = require('../objects/Feed') const oldFeed = require('../objects/Feed')
const areEquivalent = require('../utils/areEquivalent') 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))
}
/** class Feed extends Model {
* Get old feed from Feed and optionally Feed with FeedEpisodes constructor(values, options) {
* @param {Feed} feedExpanded super(values, options)
* @returns {oldFeed}
*/ /** @type {UUIDV4} */
static getOldFeed(feedExpanded) { this.id
const episodes = feedExpanded.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode()) /** @type {string} */
return new oldFeed({ this.slug
id: feedExpanded.id, /** @type {string} */
slug: feedExpanded.slug, this.entityType
userId: feedExpanded.userId, /** @type {UUIDV4} */
entityType: feedExpanded.entityType, this.entityId
entityId: feedExpanded.entityId, /** @type {Date} */
entityUpdatedAt: feedExpanded.entityUpdatedAt?.valueOf() || null, this.entityUpdatedAt
coverPath: feedExpanded.coverPath || null, /** @type {string} */
meta: { this.serverAddress
title: feedExpanded.title, /** @type {string} */
description: feedExpanded.description, this.feedURL
author: feedExpanded.author, /** @type {string} */
imageUrl: feedExpanded.imageURL, this.imageURL
feedUrl: feedExpanded.feedURL, /** @type {string} */
link: feedExpanded.siteURL, this.siteURL
explicit: feedExpanded.explicit, /** @type {string} */
type: feedExpanded.podcastType, this.title
language: feedExpanded.language, /** @type {string} */
preventIndexing: feedExpanded.preventIndexing, this.description
ownerName: feedExpanded.ownerName, /** @type {string} */
ownerEmail: feedExpanded.ownerEmail this.author
}, /** @type {string} */
serverAddress: feedExpanded.serverAddress, 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, feedUrl: feedExpanded.feedURL,
episodes: episodes || [], link: feedExpanded.siteURL,
createdAt: feedExpanded.createdAt.valueOf(), explicit: feedExpanded.explicit,
updatedAt: feedExpanded.updatedAt.valueOf() 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) { static removeById(feedId) {
return this.destroy({ return this.destroy({
where: { where: {
id: feedId id: feedId
}
})
}
/**
* Find all library item ids that have an open feed (used in library filter)
* @returns {Promise<Array<String>>} 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<objects.Feed>} 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<objects.Feed>} 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 async fullUpdateFromOld(oldFeed) { /**
const oldFeedEpisodes = oldFeed.episodes || [] * Find all library item ids that have an open feed (used in library filter)
const feedObj = this.getFromOld(oldFeed) * @returns {Promise<Array<String>>} array of library item ids
*/
const existingFeed = await this.findByPk(feedObj.id, { static async findAllLibraryItemIds() {
include: sequelize.models.feedEpisode const feeds = await this.findAll({
}) attributes: ['entityId'],
if (!existingFeed) return false where: {
entityType: 'libraryItem'
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
}
}
} }
})
return feeds.map(f => f.entityId).filter(f => f) || []
}
let feedHasUpdates = false /**
for (const key in feedObj) { * Find feed where and return oldFeed
let existingValue = existingFeed[key] * @param {object} where sequelize where object
if (existingValue instanceof Date) existingValue = existingValue.valueOf() * @returns {Promise<objects.Feed>} oldFeed
*/
if (!areEquivalent(existingValue, feedObj[key])) { static async findOneOld(where) {
feedHasUpdates = true 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) * Find feed and return oldFeed
hasUpdates = true * @param {string} id
* @returns {Promise<objects.Feed>} 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) { if (oldFeed.episodes?.length) {
const oldFeedMeta = oldFeed.meta || {} for (const oldFeedEpisode of oldFeed.episodes) {
return { const feedEpisode = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode)
id: oldFeed.id, feedEpisode.feedId = newFeed.id
slug: oldFeed.slug, await this.sequelize.models.feedEpisode.create(feedEpisode)
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${sequelize.uppercaseFirst(this.entityType)}`
return this[mixinMethodName](options)
}
} }
Feed.init({ static async fullUpdateFromOld(oldFeed) {
id: { const oldFeedEpisodes = oldFeed.episodes || []
type: DataTypes.UUID, const feedObj = this.getFromOld(oldFeed)
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 const existingFeed = await this.findByPk(feedObj.id, {
include: this.sequelize.models.feedEpisode
})
if (!existingFeed) return false
user.hasMany(Feed) let hasUpdates = false
Feed.belongsTo(user) for (const feedEpisode of existingFeed.feedEpisodes) {
const oldFeedEpisode = oldFeedEpisodes.find(ep => ep.id === feedEpisode.id)
libraryItem.hasMany(Feed, { // Episode removed
foreignKey: 'entityId', if (!oldFeedEpisode) {
constraints: false, feedEpisode.destroy()
scope: { } else {
entityType: 'libraryItem' let episodeHasUpdates = false
} const oldFeedEpisodeCleaned = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode)
}) for (const key in oldFeedEpisodeCleaned) {
Feed.belongsTo(libraryItem, { foreignKey: 'entityId', constraints: false }) if (!areEquivalent(oldFeedEpisodeCleaned[key], feedEpisode[key])) {
episodeHasUpdates = true
collection.hasMany(Feed, { }
foreignKey: 'entityId', }
constraints: false, if (episodeHasUpdates) {
scope: { await feedEpisode.update(oldFeedEpisodeCleaned)
entityType: 'collection' hasUpdates = true
} }
})
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
} }
})
return Feed 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

View File

@ -1,82 +1,125 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class FeedEpisode extends Model {
class FeedEpisode extends Model { constructor(values, options) {
getOldEpisode() { super(values, options)
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
}
}
static getFromOld(oldFeedEpisode) { /** @type {UUIDV4} */
return { this.id
id: oldFeedEpisode.id, /** @type {string} */
title: oldFeedEpisode.title, this.title
author: oldFeedEpisode.author, /** @type {string} */
description: oldFeedEpisode.description, this.description
siteURL: oldFeedEpisode.link, /** @type {string} */
enclosureURL: oldFeedEpisode.enclosure?.url || null, this.siteURL
enclosureType: oldFeedEpisode.enclosure?.type || null, /** @type {string} */
enclosureSize: oldFeedEpisode.enclosure?.size || null, this.enclosureURL
pubDate: oldFeedEpisode.pubDate, /** @type {string} */
season: oldFeedEpisode.season || null, this.enclosureType
episode: oldFeedEpisode.episode || null, /** @type {BigInt} */
episodeType: oldFeedEpisode.episodeType || null, this.enclosureSize
duration: oldFeedEpisode.duration, /** @type {string} */
filePath: oldFeedEpisode.fullPath, this.pubDate
explicit: !!oldFeedEpisode.explicit /** @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({ static getFromOld(oldFeedEpisode) {
id: { return {
type: DataTypes.UUID, id: oldFeedEpisode.id,
defaultValue: DataTypes.UUIDV4, title: oldFeedEpisode.title,
primaryKey: true author: oldFeedEpisode.author,
}, description: oldFeedEpisode.description,
title: DataTypes.STRING, siteURL: oldFeedEpisode.link,
author: DataTypes.STRING, enclosureURL: oldFeedEpisode.enclosure?.url || null,
description: DataTypes.TEXT, enclosureType: oldFeedEpisode.enclosure?.type || null,
siteURL: DataTypes.STRING, enclosureSize: oldFeedEpisode.enclosure?.size || null,
enclosureURL: DataTypes.STRING, pubDate: oldFeedEpisode.pubDate,
enclosureType: DataTypes.STRING, season: oldFeedEpisode.season || null,
enclosureSize: DataTypes.BIGINT, episode: oldFeedEpisode.episode || null,
pubDate: DataTypes.STRING, episodeType: oldFeedEpisode.episodeType || null,
season: DataTypes.STRING, duration: oldFeedEpisode.duration,
episode: DataTypes.STRING, filePath: oldFeedEpisode.fullPath,
episodeType: DataTypes.STRING, explicit: !!oldFeedEpisode.explicit
duration: DataTypes.FLOAT, }
filePath: DataTypes.STRING, }
explicit: DataTypes.BOOLEAN
}, {
sequelize,
modelName: 'feedEpisode'
})
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, { const { feed } = sequelize.models
onDelete: 'CASCADE'
})
FeedEpisode.belongsTo(feed)
return FeedEpisode feed.hasMany(FeedEpisode, {
} onDelete: 'CASCADE'
})
FeedEpisode.belongsTo(feed)
}
}
module.exports = FeedEpisode

View File

@ -2,217 +2,251 @@ const { DataTypes, Model } = require('sequelize')
const Logger = require('../Logger') const Logger = require('../Logger')
const oldLibrary = require('../objects/Library') const oldLibrary = require('../objects/Library')
module.exports = (sequelize) => {
class Library extends Model {
/**
* Get all old libraries
* @returns {Promise<oldLibrary[]>}
*/
static async getAllOldLibraries() {
const libraries = await this.findAll({
include: sequelize.models.libraryFolder,
order: [['displayOrder', 'ASC']]
})
return libraries.map(lib => this.getOldLibrary(lib))
}
/** class Library extends Model {
* Convert expanded Library to oldLibrary constructor(values, options) {
* @param {Library} libraryExpanded super(values, options)
* @returns {Promise<oldLibrary>}
*/
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()
})
}
/** /** @type {UUIDV4} */
* @param {object} oldLibrary this.id
* @returns {Library|null} /** @type {string} */
*/ this.name
static async createFromOld(oldLibrary) { /** @type {number} */
const library = this.getFromOld(oldLibrary) 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 { * Get all old libraries
id: folder.id, * @returns {Promise<oldLibrary[]>}
path: folder.fullPath */
} 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 * Convert expanded Library to oldLibrary
}).catch((error) => { * @param {Library} libraryExpanded
Logger.error(`[Library] Failed to create library ${library.id}`, error) * @returns {Promise<oldLibrary>}
return null */
}) static getOldLibrary(libraryExpanded) {
} const folders = libraryExpanded.libraryFolders.map(folder => {
/**
* 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
}
return { return {
id: oldLibrary.id, id: folder.id,
name: oldLibrary.name, fullPath: folder.path,
displayOrder: oldLibrary.displayOrder, libraryId: folder.libraryId,
icon: oldLibrary.icon || null, addedAt: folder.createdAt.valueOf()
mediaType: oldLibrary.mediaType || null, }
provider: oldLibrary.provider, })
settings: oldLibrary.settings?.toJSON() || {}, return new oldLibrary({
createdAt: oldLibrary.createdAt, id: libraryExpanded.id,
updatedAt: oldLibrary.lastUpdate, oldLibraryId: libraryExpanded.extraData?.oldLibraryId || null,
extraData 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 })
} }
} }
/** const libraryFoldersRemoved = existingLibrary.libraryFolders.filter(lf => !libraryFolders.some(_lf => _lf.id === lf.id))
* Destroy library by id for (const existingLibraryFolder of libraryFoldersRemoved) {
* @param {string} libraryId await existingLibraryFolder.destroy()
* @returns
*/
static removeById(libraryId) {
return this.destroy({
where: {
id: libraryId
}
})
} }
/** return existingLibrary.update(library)
* Get all library ids }
* @returns {Promise<string[]>} array of library ids
*/
static async getAllLibraryIds() {
const libraries = await this.findAll({
attributes: ['id', 'displayOrder'],
order: [['displayOrder', 'ASC']]
})
return libraries.map(l => l.id)
}
/** static getFromOld(oldLibrary) {
* Find Library by primary key & return oldLibrary const extraData = {}
* @param {string} libraryId if (oldLibrary.oldLibraryId) {
* @returns {Promise<oldLibrary|null>} Returns null if not found extraData.oldLibraryId = oldLibrary.oldLibraryId
*/
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)
} }
return {
/** id: oldLibrary.id,
* Get the largest value in the displayOrder column name: oldLibrary.name,
* Used for setting a new libraries display order displayOrder: oldLibrary.displayOrder,
* @returns {Promise<number>} icon: oldLibrary.icon || null,
*/ mediaType: oldLibrary.mediaType || null,
static getMaxDisplayOrder() { provider: oldLibrary.provider,
return this.max('displayOrder') || 0 settings: oldLibrary.settings?.toJSON() || {},
createdAt: oldLibrary.createdAt,
updatedAt: oldLibrary.lastUpdate,
extraData
} }
}
/** /**
* Updates displayOrder to be sequential * Destroy library by id
* Used after removing a library * @param {string} libraryId
*/ * @returns
static async resetDisplayOrder() { */
const libraries = await this.findAll({ static removeById(libraryId) {
order: [['displayOrder', 'ASC']] return this.destroy({
}) where: {
for (let i = 0; i < libraries.length; i++) { id: libraryId
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) /**
}) * Get all library ids
} * @returns {Promise<string[]>} 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<oldLibrary|null>} 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<number>}
*/
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: { * Initialize model
type: DataTypes.UUID, * @param {import('../Database').sequelize} sequelize
defaultValue: DataTypes.UUIDV4, */
primaryKey: true static init(sequelize) {
}, super.init({
name: DataTypes.STRING, id: {
displayOrder: DataTypes.INTEGER, type: DataTypes.UUID,
icon: DataTypes.STRING, defaultValue: DataTypes.UUIDV4,
mediaType: DataTypes.STRING, primaryKey: true
provider: DataTypes.STRING, },
lastScan: DataTypes.DATE, name: DataTypes.STRING,
lastScanVersion: DataTypes.STRING, displayOrder: DataTypes.INTEGER,
settings: DataTypes.JSON, icon: DataTypes.STRING,
extraData: DataTypes.JSON mediaType: DataTypes.STRING,
}, { provider: DataTypes.STRING,
sequelize, lastScan: DataTypes.DATE,
modelName: 'library' lastScanVersion: DataTypes.STRING,
}) settings: DataTypes.JSON,
extraData: DataTypes.JSON
}, {
sequelize,
modelName: 'library'
})
}
}
return Library module.exports = Library
}

View File

@ -1,36 +1,55 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class LibraryFolder extends Model {
class LibraryFolder extends Model { constructor(values, options) {
/** super(values, options)
* Gets all library folder path strings
* @returns {Promise<string[]>} array of library folder paths /** @type {UUIDV4} */
*/ this.id
static async getAllLibraryFolderPaths() { /** @type {string} */
const libraryFolders = await this.findAll({ this.path
attributes: ['path'] /** @type {UUIDV4} */
}) this.libraryId
return libraryFolders.map(l => l.path) /** @type {Date} */
} this.createdAt
/** @type {Date} */
this.updatedAt
} }
LibraryFolder.init({ /**
id: { * Gets all library folder path strings
type: DataTypes.UUID, * @returns {Promise<string[]>} array of library folder paths
defaultValue: DataTypes.UUIDV4, */
primaryKey: true static async getAllLibraryFolderPaths() {
}, const libraryFolders = await this.findAll({
path: DataTypes.STRING attributes: ['path']
}, { })
sequelize, return libraryFolders.map(l => l.path)
modelName: 'libraryFolder' }
})
const { library } = sequelize.models /**
library.hasMany(LibraryFolder, { * Initialize model
onDelete: 'CASCADE' * @param {import('../Database').sequelize} sequelize
}) */
LibraryFolder.belongsTo(library) static init(sequelize) {
super.init({
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
path: DataTypes.STRING
}, {
sequelize,
modelName: 'libraryFolder'
})
return LibraryFolder const { library } = sequelize.models
} library.hasMany(LibraryFolder, {
onDelete: 'CASCADE'
})
LibraryFolder.belongsTo(library)
}
}
module.exports = LibraryFolder