mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add jsdoc types to remaining models
This commit is contained in:
		
							parent
							
								
									0bc89cd40f
								
							
						
					
					
						commit
						a98942a361
					
				| @ -92,27 +92,27 @@ class Database { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   buildModels(force = false) { |   buildModels(force = false) { | ||||||
|     require('./models/User')(this.sequelize) |     require('./models/User').init(this.sequelize) | ||||||
|     require('./models/Library').init(this.sequelize) |     require('./models/Library').init(this.sequelize) | ||||||
|     require('./models/LibraryFolder').init(this.sequelize) |     require('./models/LibraryFolder').init(this.sequelize) | ||||||
|     require('./models/Book').init(this.sequelize) |     require('./models/Book').init(this.sequelize) | ||||||
|     require('./models/Podcast')(this.sequelize) |     require('./models/Podcast').init(this.sequelize) | ||||||
|     require('./models/PodcastEpisode')(this.sequelize) |     require('./models/PodcastEpisode').init(this.sequelize) | ||||||
|     require('./models/LibraryItem')(this.sequelize) |     require('./models/LibraryItem').init(this.sequelize) | ||||||
|     require('./models/MediaProgress')(this.sequelize) |     require('./models/MediaProgress').init(this.sequelize) | ||||||
|     require('./models/Series')(this.sequelize) |     require('./models/Series').init(this.sequelize) | ||||||
|     require('./models/BookSeries').init(this.sequelize) |     require('./models/BookSeries').init(this.sequelize) | ||||||
|     require('./models/Author').init(this.sequelize) |     require('./models/Author').init(this.sequelize) | ||||||
|     require('./models/BookAuthor').init(this.sequelize) |     require('./models/BookAuthor').init(this.sequelize) | ||||||
|     require('./models/Collection').init(this.sequelize) |     require('./models/Collection').init(this.sequelize) | ||||||
|     require('./models/CollectionBook').init(this.sequelize) |     require('./models/CollectionBook').init(this.sequelize) | ||||||
|     require('./models/Playlist')(this.sequelize) |     require('./models/Playlist').init(this.sequelize) | ||||||
|     require('./models/PlaylistMediaItem')(this.sequelize) |     require('./models/PlaylistMediaItem').init(this.sequelize) | ||||||
|     require('./models/Device').init(this.sequelize) |     require('./models/Device').init(this.sequelize) | ||||||
|     require('./models/PlaybackSession')(this.sequelize) |     require('./models/PlaybackSession').init(this.sequelize) | ||||||
|     require('./models/Feed').init(this.sequelize) |     require('./models/Feed').init(this.sequelize) | ||||||
|     require('./models/FeedEpisode').init(this.sequelize) |     require('./models/FeedEpisode').init(this.sequelize) | ||||||
|     require('./models/Setting')(this.sequelize) |     require('./models/Setting').init(this.sequelize) | ||||||
| 
 | 
 | ||||||
|     return this.sequelize.sync({ force, alter: false }) |     return this.sequelize.sync({ force, alter: false }) | ||||||
|   } |   } | ||||||
|  | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,148 +1,184 @@ | |||||||
| const { DataTypes, Model } = require('sequelize') | const { DataTypes, Model } = require('sequelize') | ||||||
| 
 | 
 | ||||||
| /* | class MediaProgress extends Model { | ||||||
|  * Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
 |   constructor(values, options) { | ||||||
|  * Book has many MediaProgress. PodcastEpisode has many MediaProgress. |     super(values, options) | ||||||
|  */ |  | ||||||
| module.exports = (sequelize) => { |  | ||||||
|   class MediaProgress extends Model { |  | ||||||
|     getOldMediaProgress() { |  | ||||||
|       const isPodcastEpisode = this.mediaItemType === 'podcastEpisode' |  | ||||||
| 
 | 
 | ||||||
|       return { |     /** @type {UUIDV4} */ | ||||||
|         id: this.id, |     this.id | ||||||
|         userId: this.userId, |     /** @type {UUIDV4} */ | ||||||
|         libraryItemId: this.extraData?.libraryItemId || null, |     this.mediaItemId | ||||||
|         episodeId: isPodcastEpisode ? this.mediaItemId : null, |     /** @type {string} */ | ||||||
|         mediaItemId: this.mediaItemId, |     this.mediaItemType | ||||||
|         mediaItemType: this.mediaItemType, |     /** @type {number} */ | ||||||
|         duration: this.duration, |     this.duration | ||||||
|         progress: this.extraData?.progress || 0, |     /** @type {number} */ | ||||||
|         currentTime: this.currentTime, |     this.currentTime | ||||||
|         isFinished: !!this.isFinished, |     /** @type {boolean} */ | ||||||
|         hideFromContinueListening: !!this.hideFromContinueListening, |     this.isFinished | ||||||
|         ebookLocation: this.ebookLocation, |     /** @type {boolean} */ | ||||||
|         ebookProgress: this.ebookProgress, |     this.hideFromContinueListening | ||||||
|         lastUpdate: this.updatedAt.valueOf(), |     /** @type {string} */ | ||||||
|         startedAt: this.createdAt.valueOf(), |     this.ebookLocation | ||||||
|         finishedAt: this.finishedAt?.valueOf() || null |     /** @type {number} */ | ||||||
|       } |     this.ebookProgress | ||||||
|     } |     /** @type {Date} */ | ||||||
|  |     this.finishedAt | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.extraData | ||||||
|  |     /** @type {UUIDV4} */ | ||||||
|  |     this.userId | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.updatedAt | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.createdAt | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     static upsertFromOld(oldMediaProgress) { |   getOldMediaProgress() { | ||||||
|       const mediaProgress = this.getFromOld(oldMediaProgress) |     const isPodcastEpisode = this.mediaItemType === 'podcastEpisode' | ||||||
|       return this.upsert(mediaProgress) |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     static getFromOld(oldMediaProgress) { |     return { | ||||||
|       return { |       id: this.id, | ||||||
|         id: oldMediaProgress.id, |       userId: this.userId, | ||||||
|         userId: oldMediaProgress.userId, |       libraryItemId: this.extraData?.libraryItemId || null, | ||||||
|         mediaItemId: oldMediaProgress.mediaItemId, |       episodeId: isPodcastEpisode ? this.mediaItemId : null, | ||||||
|         mediaItemType: oldMediaProgress.mediaItemType, |       mediaItemId: this.mediaItemId, | ||||||
|         duration: oldMediaProgress.duration, |       mediaItemType: this.mediaItemType, | ||||||
|         currentTime: oldMediaProgress.currentTime, |       duration: this.duration, | ||||||
|         ebookLocation: oldMediaProgress.ebookLocation || null, |       progress: this.extraData?.progress || 0, | ||||||
|         ebookProgress: oldMediaProgress.ebookProgress || null, |       currentTime: this.currentTime, | ||||||
|         isFinished: !!oldMediaProgress.isFinished, |       isFinished: !!this.isFinished, | ||||||
|         hideFromContinueListening: !!oldMediaProgress.hideFromContinueListening, |       hideFromContinueListening: !!this.hideFromContinueListening, | ||||||
|         finishedAt: oldMediaProgress.finishedAt, |       ebookLocation: this.ebookLocation, | ||||||
|         createdAt: oldMediaProgress.startedAt || oldMediaProgress.lastUpdate, |       ebookProgress: this.ebookProgress, | ||||||
|         updatedAt: oldMediaProgress.lastUpdate, |       lastUpdate: this.updatedAt.valueOf(), | ||||||
|         extraData: { |       startedAt: this.createdAt.valueOf(), | ||||||
|           libraryItemId: oldMediaProgress.libraryItemId, |       finishedAt: this.finishedAt?.valueOf() || null | ||||||
|           progress: oldMediaProgress.progress |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static removeById(mediaProgressId) { |  | ||||||
|       return this.destroy({ |  | ||||||
|         where: { |  | ||||||
|           id: mediaProgressId |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     getMediaItem(options) { |  | ||||||
|       if (!this.mediaItemType) return Promise.resolve(null) |  | ||||||
|       const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` |  | ||||||
|       return this[mixinMethodName](options) |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   static upsertFromOld(oldMediaProgress) { | ||||||
|  |     const mediaProgress = this.getFromOld(oldMediaProgress) | ||||||
|  |     return this.upsert(mediaProgress) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   MediaProgress.init({ |   static getFromOld(oldMediaProgress) { | ||||||
|     id: { |     return { | ||||||
|       type: DataTypes.UUID, |       id: oldMediaProgress.id, | ||||||
|       defaultValue: DataTypes.UUIDV4, |       userId: oldMediaProgress.userId, | ||||||
|       primaryKey: true |       mediaItemId: oldMediaProgress.mediaItemId, | ||||||
|     }, |       mediaItemType: oldMediaProgress.mediaItemType, | ||||||
|     mediaItemId: DataTypes.UUIDV4, |       duration: oldMediaProgress.duration, | ||||||
|     mediaItemType: DataTypes.STRING, |       currentTime: oldMediaProgress.currentTime, | ||||||
|     duration: DataTypes.FLOAT, |       ebookLocation: oldMediaProgress.ebookLocation || null, | ||||||
|     currentTime: DataTypes.FLOAT, |       ebookProgress: oldMediaProgress.ebookProgress || null, | ||||||
|     isFinished: DataTypes.BOOLEAN, |       isFinished: !!oldMediaProgress.isFinished, | ||||||
|     hideFromContinueListening: DataTypes.BOOLEAN, |       hideFromContinueListening: !!oldMediaProgress.hideFromContinueListening, | ||||||
|     ebookLocation: DataTypes.STRING, |       finishedAt: oldMediaProgress.finishedAt, | ||||||
|     ebookProgress: DataTypes.FLOAT, |       createdAt: oldMediaProgress.startedAt || oldMediaProgress.lastUpdate, | ||||||
|     finishedAt: DataTypes.DATE, |       updatedAt: oldMediaProgress.lastUpdate, | ||||||
|     extraData: DataTypes.JSON |       extraData: { | ||||||
|   }, { |         libraryItemId: oldMediaProgress.libraryItemId, | ||||||
|     sequelize, |         progress: oldMediaProgress.progress | ||||||
|     modelName: 'mediaProgress', |  | ||||||
|     indexes: [ |  | ||||||
|       { |  | ||||||
|         fields: ['updatedAt'] |  | ||||||
|       } |       } | ||||||
|     ] |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   const { book, podcastEpisode, user } = sequelize.models |  | ||||||
| 
 |  | ||||||
|   book.hasMany(MediaProgress, { |  | ||||||
|     foreignKey: 'mediaItemId', |  | ||||||
|     constraints: false, |  | ||||||
|     scope: { |  | ||||||
|       mediaItemType: 'book' |  | ||||||
|     } |     } | ||||||
|   }) |   } | ||||||
|   MediaProgress.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) |  | ||||||
| 
 | 
 | ||||||
|   podcastEpisode.hasMany(MediaProgress, { |   static removeById(mediaProgressId) { | ||||||
|     foreignKey: 'mediaItemId', |     return this.destroy({ | ||||||
|     constraints: false, |       where: { | ||||||
|     scope: { |         id: mediaProgressId | ||||||
|       mediaItemType: 'podcastEpisode' |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|   MediaProgress.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) |  | ||||||
| 
 |  | ||||||
|   MediaProgress.addHook('afterFind', findResult => { |  | ||||||
|     if (!findResult) return |  | ||||||
| 
 |  | ||||||
|     if (!Array.isArray(findResult)) findResult = [findResult] |  | ||||||
| 
 |  | ||||||
|     for (const instance of findResult) { |  | ||||||
|       if (instance.mediaItemType === 'book' && instance.book !== undefined) { |  | ||||||
|         instance.mediaItem = instance.book |  | ||||||
|         instance.dataValues.mediaItem = instance.dataValues.book |  | ||||||
|       } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { |  | ||||||
|         instance.mediaItem = instance.podcastEpisode |  | ||||||
|         instance.dataValues.mediaItem = instance.dataValues.podcastEpisode |  | ||||||
|       } |       } | ||||||
|       // To prevent mistakes:
 |     }) | ||||||
|       delete instance.book |   } | ||||||
|       delete instance.dataValues.book |  | ||||||
|       delete instance.podcastEpisode |  | ||||||
|       delete instance.dataValues.podcastEpisode |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
| 
 | 
 | ||||||
|   user.hasMany(MediaProgress, { |   getMediaItem(options) { | ||||||
|     onDelete: 'CASCADE' |     if (!this.mediaItemType) return Promise.resolve(null) | ||||||
|   }) |     const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}` | ||||||
|   MediaProgress.belongsTo(user) |     return this[mixinMethodName](options) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   return MediaProgress |   /** | ||||||
| } |    * Initialize model | ||||||
|  |    *  | ||||||
|  |    * Polymorphic association: Book has many MediaProgress. PodcastEpisode has many MediaProgress. | ||||||
|  |    * @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
 | ||||||
|  |    *  | ||||||
|  |    * @param {import('../Database').sequelize} sequelize  | ||||||
|  |    */ | ||||||
|  |   static init(sequelize) { | ||||||
|  |     super.init({ | ||||||
|  |       id: { | ||||||
|  |         type: DataTypes.UUID, | ||||||
|  |         defaultValue: DataTypes.UUIDV4, | ||||||
|  |         primaryKey: true | ||||||
|  |       }, | ||||||
|  |       mediaItemId: DataTypes.UUIDV4, | ||||||
|  |       mediaItemType: DataTypes.STRING, | ||||||
|  |       duration: DataTypes.FLOAT, | ||||||
|  |       currentTime: DataTypes.FLOAT, | ||||||
|  |       isFinished: DataTypes.BOOLEAN, | ||||||
|  |       hideFromContinueListening: DataTypes.BOOLEAN, | ||||||
|  |       ebookLocation: DataTypes.STRING, | ||||||
|  |       ebookProgress: DataTypes.FLOAT, | ||||||
|  |       finishedAt: DataTypes.DATE, | ||||||
|  |       extraData: DataTypes.JSON | ||||||
|  |     }, { | ||||||
|  |       sequelize, | ||||||
|  |       modelName: 'mediaProgress', | ||||||
|  |       indexes: [ | ||||||
|  |         { | ||||||
|  |           fields: ['updatedAt'] | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     const { book, podcastEpisode, user } = sequelize.models | ||||||
|  | 
 | ||||||
|  |     book.hasMany(MediaProgress, { | ||||||
|  |       foreignKey: 'mediaItemId', | ||||||
|  |       constraints: false, | ||||||
|  |       scope: { | ||||||
|  |         mediaItemType: 'book' | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     MediaProgress.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) | ||||||
|  | 
 | ||||||
|  |     podcastEpisode.hasMany(MediaProgress, { | ||||||
|  |       foreignKey: 'mediaItemId', | ||||||
|  |       constraints: false, | ||||||
|  |       scope: { | ||||||
|  |         mediaItemType: 'podcastEpisode' | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     MediaProgress.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) | ||||||
|  | 
 | ||||||
|  |     MediaProgress.addHook('afterFind', findResult => { | ||||||
|  |       if (!findResult) return | ||||||
|  | 
 | ||||||
|  |       if (!Array.isArray(findResult)) findResult = [findResult] | ||||||
|  | 
 | ||||||
|  |       for (const instance of findResult) { | ||||||
|  |         if (instance.mediaItemType === 'book' && instance.book !== undefined) { | ||||||
|  |           instance.mediaItem = instance.book | ||||||
|  |           instance.dataValues.mediaItem = instance.dataValues.book | ||||||
|  |         } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { | ||||||
|  |           instance.mediaItem = instance.podcastEpisode | ||||||
|  |           instance.dataValues.mediaItem = instance.dataValues.podcastEpisode | ||||||
|  |         } | ||||||
|  |         // To prevent mistakes:
 | ||||||
|  |         delete instance.book | ||||||
|  |         delete instance.dataValues.book | ||||||
|  |         delete instance.podcastEpisode | ||||||
|  |         delete instance.dataValues.podcastEpisode | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     user.hasMany(MediaProgress, { | ||||||
|  |       onDelete: 'CASCADE' | ||||||
|  |     }) | ||||||
|  |     MediaProgress.belongsTo(user) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = MediaProgress | ||||||
| @ -2,197 +2,251 @@ const { DataTypes, Model } = require('sequelize') | |||||||
| 
 | 
 | ||||||
| const oldPlaybackSession = require('../objects/PlaybackSession') | const oldPlaybackSession = require('../objects/PlaybackSession') | ||||||
| 
 | 
 | ||||||
| module.exports = (sequelize) => { |  | ||||||
|   class PlaybackSession extends Model { |  | ||||||
|     static async getOldPlaybackSessions(where = null) { |  | ||||||
|       const playbackSessions = await this.findAll({ |  | ||||||
|         where, |  | ||||||
|         include: [ |  | ||||||
|           { |  | ||||||
|             model: sequelize.models.device |  | ||||||
|           } |  | ||||||
|         ] |  | ||||||
|       }) |  | ||||||
|       return playbackSessions.map(session => this.getOldPlaybackSession(session)) |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     static async getById(sessionId) { | class PlaybackSession extends Model { | ||||||
|       const playbackSession = await this.findByPk(sessionId, { |   constructor(values, options) { | ||||||
|         include: [ |     super(values, options) | ||||||
|           { |  | ||||||
|             model: sequelize.models.device |  | ||||||
|           } |  | ||||||
|         ] |  | ||||||
|       }) |  | ||||||
|       if (!playbackSession) return null |  | ||||||
|       return this.getOldPlaybackSession(playbackSession) |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     static getOldPlaybackSession(playbackSessionExpanded) { |     /** @type {UUIDV4} */ | ||||||
|       const isPodcastEpisode = playbackSessionExpanded.mediaItemType === 'podcastEpisode' |     this.id | ||||||
|  |     /** @type {UUIDV4} */ | ||||||
|  |     this.mediaItemId | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.mediaItemType | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.displayTitle | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.displayAuthor | ||||||
|  |     /** @type {number} */ | ||||||
|  |     this.duration | ||||||
|  |     /** @type {number} */ | ||||||
|  |     this.playMethod | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.mediaPlayer | ||||||
|  |     /** @type {number} */ | ||||||
|  |     this.startTime | ||||||
|  |     /** @type {number} */ | ||||||
|  |     this.currentTime | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.serverVersion | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.coverPath | ||||||
|  |     /** @type {number} */ | ||||||
|  |     this.timeListening | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.mediaMetadata | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.date | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.dayOfWeek | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.extraData | ||||||
|  |     /** @type {UUIDV4} */ | ||||||
|  |     this.userId | ||||||
|  |     /** @type {UUIDV4} */ | ||||||
|  |     this.deviceId | ||||||
|  |     /** @type {UUIDV4} */ | ||||||
|  |     this.libraryId | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.updatedAt | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.createdAt | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|       return new oldPlaybackSession({ |   static async getOldPlaybackSessions(where = null) { | ||||||
|         id: playbackSessionExpanded.id, |     const playbackSessions = await this.findAll({ | ||||||
|         userId: playbackSessionExpanded.userId, |       where, | ||||||
|         libraryId: playbackSessionExpanded.libraryId, |       include: [ | ||||||
|         libraryItemId: playbackSessionExpanded.extraData?.libraryItemId || null, |         { | ||||||
|         bookId: isPodcastEpisode ? null : playbackSessionExpanded.mediaItemId, |           model: this.sequelize.models.device | ||||||
|         episodeId: isPodcastEpisode ? playbackSessionExpanded.mediaItemId : null, |  | ||||||
|         mediaType: isPodcastEpisode ? 'podcast' : 'book', |  | ||||||
|         mediaMetadata: playbackSessionExpanded.mediaMetadata, |  | ||||||
|         chapters: null, |  | ||||||
|         displayTitle: playbackSessionExpanded.displayTitle, |  | ||||||
|         displayAuthor: playbackSessionExpanded.displayAuthor, |  | ||||||
|         coverPath: playbackSessionExpanded.coverPath, |  | ||||||
|         duration: playbackSessionExpanded.duration, |  | ||||||
|         playMethod: playbackSessionExpanded.playMethod, |  | ||||||
|         mediaPlayer: playbackSessionExpanded.mediaPlayer, |  | ||||||
|         deviceInfo: playbackSessionExpanded.device?.getOldDevice() || null, |  | ||||||
|         serverVersion: playbackSessionExpanded.serverVersion, |  | ||||||
|         date: playbackSessionExpanded.date, |  | ||||||
|         dayOfWeek: playbackSessionExpanded.dayOfWeek, |  | ||||||
|         timeListening: playbackSessionExpanded.timeListening, |  | ||||||
|         startTime: playbackSessionExpanded.startTime, |  | ||||||
|         currentTime: playbackSessionExpanded.currentTime, |  | ||||||
|         startedAt: playbackSessionExpanded.createdAt.valueOf(), |  | ||||||
|         updatedAt: playbackSessionExpanded.updatedAt.valueOf() |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static removeById(sessionId) { |  | ||||||
|       return this.destroy({ |  | ||||||
|         where: { |  | ||||||
|           id: sessionId |  | ||||||
|         } |         } | ||||||
|       }) |       ] | ||||||
|     } |     }) | ||||||
|  |     return playbackSessions.map(session => this.getOldPlaybackSession(session)) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     static createFromOld(oldPlaybackSession) { |   static async getById(sessionId) { | ||||||
|       const playbackSession = this.getFromOld(oldPlaybackSession) |     const playbackSession = await this.findByPk(sessionId, { | ||||||
|       return this.create(playbackSession) |       include: [ | ||||||
|     } |         { | ||||||
| 
 |           model: this.sequelize.models.device | ||||||
|     static updateFromOld(oldPlaybackSession) { |  | ||||||
|       const playbackSession = this.getFromOld(oldPlaybackSession) |  | ||||||
|       return this.update(playbackSession, { |  | ||||||
|         where: { |  | ||||||
|           id: playbackSession.id |  | ||||||
|         } |         } | ||||||
|       }) |       ] | ||||||
|     } |     }) | ||||||
|  |     if (!playbackSession) return null | ||||||
|  |     return this.getOldPlaybackSession(playbackSession) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     static getFromOld(oldPlaybackSession) { |   static getOldPlaybackSession(playbackSessionExpanded) { | ||||||
|       return { |     const isPodcastEpisode = playbackSessionExpanded.mediaItemType === 'podcastEpisode' | ||||||
|         id: oldPlaybackSession.id, | 
 | ||||||
|         mediaItemId: oldPlaybackSession.episodeId || oldPlaybackSession.bookId, |     return new oldPlaybackSession({ | ||||||
|         mediaItemType: oldPlaybackSession.episodeId ? 'podcastEpisode' : 'book', |       id: playbackSessionExpanded.id, | ||||||
|         libraryId: oldPlaybackSession.libraryId, |       userId: playbackSessionExpanded.userId, | ||||||
|         displayTitle: oldPlaybackSession.displayTitle, |       libraryId: playbackSessionExpanded.libraryId, | ||||||
|         displayAuthor: oldPlaybackSession.displayAuthor, |       libraryItemId: playbackSessionExpanded.extraData?.libraryItemId || null, | ||||||
|         duration: oldPlaybackSession.duration, |       bookId: isPodcastEpisode ? null : playbackSessionExpanded.mediaItemId, | ||||||
|         playMethod: oldPlaybackSession.playMethod, |       episodeId: isPodcastEpisode ? playbackSessionExpanded.mediaItemId : null, | ||||||
|         mediaPlayer: oldPlaybackSession.mediaPlayer, |       mediaType: isPodcastEpisode ? 'podcast' : 'book', | ||||||
|         startTime: oldPlaybackSession.startTime, |       mediaMetadata: playbackSessionExpanded.mediaMetadata, | ||||||
|         currentTime: oldPlaybackSession.currentTime, |       chapters: null, | ||||||
|         serverVersion: oldPlaybackSession.serverVersion || null, |       displayTitle: playbackSessionExpanded.displayTitle, | ||||||
|         createdAt: oldPlaybackSession.startedAt, |       displayAuthor: playbackSessionExpanded.displayAuthor, | ||||||
|         updatedAt: oldPlaybackSession.updatedAt, |       coverPath: playbackSessionExpanded.coverPath, | ||||||
|         userId: oldPlaybackSession.userId, |       duration: playbackSessionExpanded.duration, | ||||||
|         deviceId: oldPlaybackSession.deviceInfo?.id || null, |       playMethod: playbackSessionExpanded.playMethod, | ||||||
|         timeListening: oldPlaybackSession.timeListening, |       mediaPlayer: playbackSessionExpanded.mediaPlayer, | ||||||
|         coverPath: oldPlaybackSession.coverPath, |       deviceInfo: playbackSessionExpanded.device?.getOldDevice() || null, | ||||||
|         mediaMetadata: oldPlaybackSession.mediaMetadata, |       serverVersion: playbackSessionExpanded.serverVersion, | ||||||
|         date: oldPlaybackSession.date, |       date: playbackSessionExpanded.date, | ||||||
|         dayOfWeek: oldPlaybackSession.dayOfWeek, |       dayOfWeek: playbackSessionExpanded.dayOfWeek, | ||||||
|         extraData: { |       timeListening: playbackSessionExpanded.timeListening, | ||||||
|           libraryItemId: oldPlaybackSession.libraryItemId |       startTime: playbackSessionExpanded.startTime, | ||||||
|         } |       currentTime: playbackSessionExpanded.currentTime, | ||||||
|  |       startedAt: playbackSessionExpanded.createdAt.valueOf(), | ||||||
|  |       updatedAt: playbackSessionExpanded.updatedAt.valueOf() | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static removeById(sessionId) { | ||||||
|  |     return this.destroy({ | ||||||
|  |       where: { | ||||||
|  |         id: sessionId | ||||||
|       } |       } | ||||||
|     } |     }) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     getMediaItem(options) { |   static createFromOld(oldPlaybackSession) { | ||||||
|       if (!this.mediaItemType) return Promise.resolve(null) |     const playbackSession = this.getFromOld(oldPlaybackSession) | ||||||
|       const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` |     return this.create(playbackSession) | ||||||
|       return this[mixinMethodName](options) |   } | ||||||
|  | 
 | ||||||
|  |   static updateFromOld(oldPlaybackSession) { | ||||||
|  |     const playbackSession = this.getFromOld(oldPlaybackSession) | ||||||
|  |     return this.update(playbackSession, { | ||||||
|  |       where: { | ||||||
|  |         id: playbackSession.id | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static getFromOld(oldPlaybackSession) { | ||||||
|  |     return { | ||||||
|  |       id: oldPlaybackSession.id, | ||||||
|  |       mediaItemId: oldPlaybackSession.episodeId || oldPlaybackSession.bookId, | ||||||
|  |       mediaItemType: oldPlaybackSession.episodeId ? 'podcastEpisode' : 'book', | ||||||
|  |       libraryId: oldPlaybackSession.libraryId, | ||||||
|  |       displayTitle: oldPlaybackSession.displayTitle, | ||||||
|  |       displayAuthor: oldPlaybackSession.displayAuthor, | ||||||
|  |       duration: oldPlaybackSession.duration, | ||||||
|  |       playMethod: oldPlaybackSession.playMethod, | ||||||
|  |       mediaPlayer: oldPlaybackSession.mediaPlayer, | ||||||
|  |       startTime: oldPlaybackSession.startTime, | ||||||
|  |       currentTime: oldPlaybackSession.currentTime, | ||||||
|  |       serverVersion: oldPlaybackSession.serverVersion || null, | ||||||
|  |       createdAt: oldPlaybackSession.startedAt, | ||||||
|  |       updatedAt: oldPlaybackSession.updatedAt, | ||||||
|  |       userId: oldPlaybackSession.userId, | ||||||
|  |       deviceId: oldPlaybackSession.deviceInfo?.id || null, | ||||||
|  |       timeListening: oldPlaybackSession.timeListening, | ||||||
|  |       coverPath: oldPlaybackSession.coverPath, | ||||||
|  |       mediaMetadata: oldPlaybackSession.mediaMetadata, | ||||||
|  |       date: oldPlaybackSession.date, | ||||||
|  |       dayOfWeek: oldPlaybackSession.dayOfWeek, | ||||||
|  |       extraData: { | ||||||
|  |         libraryItemId: oldPlaybackSession.libraryItemId | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   PlaybackSession.init({ |   getMediaItem(options) { | ||||||
|     id: { |     if (!this.mediaItemType) return Promise.resolve(null) | ||||||
|       type: DataTypes.UUID, |     const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}` | ||||||
|       defaultValue: DataTypes.UUIDV4, |     return this[mixinMethodName](options) | ||||||
|       primaryKey: true |   } | ||||||
|     }, |  | ||||||
|     mediaItemId: DataTypes.UUIDV4, |  | ||||||
|     mediaItemType: DataTypes.STRING, |  | ||||||
|     displayTitle: DataTypes.STRING, |  | ||||||
|     displayAuthor: DataTypes.STRING, |  | ||||||
|     duration: DataTypes.FLOAT, |  | ||||||
|     playMethod: DataTypes.INTEGER, |  | ||||||
|     mediaPlayer: DataTypes.STRING, |  | ||||||
|     startTime: DataTypes.FLOAT, |  | ||||||
|     currentTime: DataTypes.FLOAT, |  | ||||||
|     serverVersion: DataTypes.STRING, |  | ||||||
|     coverPath: DataTypes.STRING, |  | ||||||
|     timeListening: DataTypes.INTEGER, |  | ||||||
|     mediaMetadata: DataTypes.JSON, |  | ||||||
|     date: DataTypes.STRING, |  | ||||||
|     dayOfWeek: DataTypes.STRING, |  | ||||||
|     extraData: DataTypes.JSON |  | ||||||
|   }, { |  | ||||||
|     sequelize, |  | ||||||
|     modelName: 'playbackSession' |  | ||||||
|   }) |  | ||||||
| 
 | 
 | ||||||
|   const { book, podcastEpisode, user, device, library } = sequelize.models |   /** | ||||||
|  |    * Initialize model | ||||||
|  |    * @param {import('../Database').sequelize} sequelize  | ||||||
|  |    */ | ||||||
|  |   static init(sequelize) { | ||||||
|  |     super.init({ | ||||||
|  |       id: { | ||||||
|  |         type: DataTypes.UUID, | ||||||
|  |         defaultValue: DataTypes.UUIDV4, | ||||||
|  |         primaryKey: true | ||||||
|  |       }, | ||||||
|  |       mediaItemId: DataTypes.UUIDV4, | ||||||
|  |       mediaItemType: DataTypes.STRING, | ||||||
|  |       displayTitle: DataTypes.STRING, | ||||||
|  |       displayAuthor: DataTypes.STRING, | ||||||
|  |       duration: DataTypes.FLOAT, | ||||||
|  |       playMethod: DataTypes.INTEGER, | ||||||
|  |       mediaPlayer: DataTypes.STRING, | ||||||
|  |       startTime: DataTypes.FLOAT, | ||||||
|  |       currentTime: DataTypes.FLOAT, | ||||||
|  |       serverVersion: DataTypes.STRING, | ||||||
|  |       coverPath: DataTypes.STRING, | ||||||
|  |       timeListening: DataTypes.INTEGER, | ||||||
|  |       mediaMetadata: DataTypes.JSON, | ||||||
|  |       date: DataTypes.STRING, | ||||||
|  |       dayOfWeek: DataTypes.STRING, | ||||||
|  |       extraData: DataTypes.JSON | ||||||
|  |     }, { | ||||||
|  |       sequelize, | ||||||
|  |       modelName: 'playbackSession' | ||||||
|  |     }) | ||||||
| 
 | 
 | ||||||
|   user.hasMany(PlaybackSession) |     const { book, podcastEpisode, user, device, library } = sequelize.models | ||||||
|   PlaybackSession.belongsTo(user) |  | ||||||
| 
 | 
 | ||||||
|   device.hasMany(PlaybackSession) |     user.hasMany(PlaybackSession) | ||||||
|   PlaybackSession.belongsTo(device) |     PlaybackSession.belongsTo(user) | ||||||
| 
 | 
 | ||||||
|   library.hasMany(PlaybackSession) |     device.hasMany(PlaybackSession) | ||||||
|   PlaybackSession.belongsTo(library) |     PlaybackSession.belongsTo(device) | ||||||
| 
 | 
 | ||||||
|   book.hasMany(PlaybackSession, { |     library.hasMany(PlaybackSession) | ||||||
|     foreignKey: 'mediaItemId', |     PlaybackSession.belongsTo(library) | ||||||
|     constraints: false, |  | ||||||
|     scope: { |  | ||||||
|       mediaItemType: 'book' |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|   PlaybackSession.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) |  | ||||||
| 
 | 
 | ||||||
|   podcastEpisode.hasOne(PlaybackSession, { |     book.hasMany(PlaybackSession, { | ||||||
|     foreignKey: 'mediaItemId', |       foreignKey: 'mediaItemId', | ||||||
|     constraints: false, |       constraints: false, | ||||||
|     scope: { |       scope: { | ||||||
|       mediaItemType: 'podcastEpisode' |         mediaItemType: 'book' | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|   PlaybackSession.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) |  | ||||||
| 
 |  | ||||||
|   PlaybackSession.addHook('afterFind', findResult => { |  | ||||||
|     if (!findResult) return |  | ||||||
| 
 |  | ||||||
|     if (!Array.isArray(findResult)) findResult = [findResult] |  | ||||||
| 
 |  | ||||||
|     for (const instance of findResult) { |  | ||||||
|       if (instance.mediaItemType === 'book' && instance.book !== undefined) { |  | ||||||
|         instance.mediaItem = instance.book |  | ||||||
|         instance.dataValues.mediaItem = instance.dataValues.book |  | ||||||
|       } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { |  | ||||||
|         instance.mediaItem = instance.podcastEpisode |  | ||||||
|         instance.dataValues.mediaItem = instance.dataValues.podcastEpisode |  | ||||||
|       } |       } | ||||||
|       // To prevent mistakes:
 |     }) | ||||||
|       delete instance.book |     PlaybackSession.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) | ||||||
|       delete instance.dataValues.book |  | ||||||
|       delete instance.podcastEpisode |  | ||||||
|       delete instance.dataValues.podcastEpisode |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
| 
 | 
 | ||||||
|   return PlaybackSession |     podcastEpisode.hasOne(PlaybackSession, { | ||||||
| } |       foreignKey: 'mediaItemId', | ||||||
|  |       constraints: false, | ||||||
|  |       scope: { | ||||||
|  |         mediaItemType: 'podcastEpisode' | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     PlaybackSession.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) | ||||||
|  | 
 | ||||||
|  |     PlaybackSession.addHook('afterFind', findResult => { | ||||||
|  |       if (!findResult) return | ||||||
|  | 
 | ||||||
|  |       if (!Array.isArray(findResult)) findResult = [findResult] | ||||||
|  | 
 | ||||||
|  |       for (const instance of findResult) { | ||||||
|  |         if (instance.mediaItemType === 'book' && instance.book !== undefined) { | ||||||
|  |           instance.mediaItem = instance.book | ||||||
|  |           instance.dataValues.mediaItem = instance.dataValues.book | ||||||
|  |         } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { | ||||||
|  |           instance.mediaItem = instance.podcastEpisode | ||||||
|  |           instance.dataValues.mediaItem = instance.dataValues.podcastEpisode | ||||||
|  |         } | ||||||
|  |         // To prevent mistakes:
 | ||||||
|  |         delete instance.book | ||||||
|  |         delete instance.dataValues.book | ||||||
|  |         delete instance.podcastEpisode | ||||||
|  |         delete instance.dataValues.podcastEpisode | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = PlaybackSession | ||||||
|  | |||||||
| @ -3,318 +3,341 @@ const Logger = require('../Logger') | |||||||
| 
 | 
 | ||||||
| const oldPlaylist = require('../objects/Playlist') | const oldPlaylist = require('../objects/Playlist') | ||||||
| 
 | 
 | ||||||
| module.exports = (sequelize) => { | class Playlist extends Model { | ||||||
|   class Playlist extends Model { |   constructor(values, options) { | ||||||
|     static async getOldPlaylists() { |     super(values, options) | ||||||
|       const playlists = await this.findAll({ |  | ||||||
|         include: { |  | ||||||
|           model: sequelize.models.playlistMediaItem, |  | ||||||
|           include: [ |  | ||||||
|             { |  | ||||||
|               model: sequelize.models.book, |  | ||||||
|               include: sequelize.models.libraryItem |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               model: sequelize.models.podcastEpisode, |  | ||||||
|               include: { |  | ||||||
|                 model: sequelize.models.podcast, |  | ||||||
|                 include: sequelize.models.libraryItem |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|         }, |  | ||||||
|         order: [['playlistMediaItems', 'order', 'ASC']] |  | ||||||
|       }) |  | ||||||
|       return playlists.map(p => this.getOldPlaylist(p)) |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     static getOldPlaylist(playlistExpanded) { |     /** @type {UUIDV4} */ | ||||||
|       const items = playlistExpanded.playlistMediaItems.map(pmi => { |     this.id | ||||||
|         const libraryItemId = pmi.mediaItem?.podcast?.libraryItem?.id || pmi.mediaItem?.libraryItem?.id || null |     /** @type {string} */ | ||||||
|         if (!libraryItemId) { |     this.name | ||||||
|           Logger.error(`[Playlist] Invalid playlist media item - No library item id found`, JSON.stringify(pmi, null, 2)) |     /** @type {string} */ | ||||||
|           return null |     this.description | ||||||
|         } |     /** @type {UUIDV4} */ | ||||||
|         return { |     this.libraryId | ||||||
|           episodeId: pmi.mediaItemType === 'podcastEpisode' ? pmi.mediaItemId : '', |     /** @type {UUIDV4} */ | ||||||
|           libraryItemId |     this.userId | ||||||
|         } |     /** @type {Date} */ | ||||||
|       }).filter(pmi => pmi) |     this.createdAt | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.updatedAt | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|       return new oldPlaylist({ |   static async getOldPlaylists() { | ||||||
|         id: playlistExpanded.id, |     const playlists = await this.findAll({ | ||||||
|         libraryId: playlistExpanded.libraryId, |       include: { | ||||||
|         userId: playlistExpanded.userId, |         model: this.sequelize.models.playlistMediaItem, | ||||||
|         name: playlistExpanded.name, |  | ||||||
|         description: playlistExpanded.description, |  | ||||||
|         items, |  | ||||||
|         lastUpdate: playlistExpanded.updatedAt.valueOf(), |  | ||||||
|         createdAt: playlistExpanded.createdAt.valueOf() |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get old playlist toJSONExpanded |  | ||||||
|      * @param {[string[]]} include |  | ||||||
|      * @returns {Promise<object>} oldPlaylist.toJSONExpanded |  | ||||||
|      */ |  | ||||||
|     async getOldJsonExpanded(include) { |  | ||||||
|       this.playlistMediaItems = await this.getPlaylistMediaItems({ |  | ||||||
|         include: [ |         include: [ | ||||||
|           { |           { | ||||||
|             model: sequelize.models.book, |             model: this.sequelize.models.book, | ||||||
|             include: sequelize.models.libraryItem |             include: this.sequelize.models.libraryItem | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|             model: sequelize.models.podcastEpisode, |             model: this.sequelize.models.podcastEpisode, | ||||||
|             include: { |             include: { | ||||||
|               model: sequelize.models.podcast, |               model: this.sequelize.models.podcast, | ||||||
|               include: sequelize.models.libraryItem |               include: this.sequelize.models.libraryItem | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         ], |  | ||||||
|         order: [['order', 'ASC']] |  | ||||||
|       }) || [] |  | ||||||
| 
 |  | ||||||
|       const oldPlaylist = sequelize.models.playlist.getOldPlaylist(this) |  | ||||||
|       const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId) |  | ||||||
| 
 |  | ||||||
|       let libraryItems = await sequelize.models.libraryItem.getAllOldLibraryItems({ |  | ||||||
|         id: libraryItemIds |  | ||||||
|       }) |  | ||||||
| 
 |  | ||||||
|       const playlistExpanded = oldPlaylist.toJSONExpanded(libraryItems) |  | ||||||
| 
 |  | ||||||
|       if (include?.includes('rssfeed')) { |  | ||||||
|         const feeds = await this.getFeeds() |  | ||||||
|         if (feeds?.length) { |  | ||||||
|           playlistExpanded.rssFeed = sequelize.models.feed.getOldFeed(feeds[0]) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return playlistExpanded |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static createFromOld(oldPlaylist) { |  | ||||||
|       const playlist = this.getFromOld(oldPlaylist) |  | ||||||
|       return this.create(playlist) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static getFromOld(oldPlaylist) { |  | ||||||
|       return { |  | ||||||
|         id: oldPlaylist.id, |  | ||||||
|         name: oldPlaylist.name, |  | ||||||
|         description: oldPlaylist.description, |  | ||||||
|         userId: oldPlaylist.userId, |  | ||||||
|         libraryId: oldPlaylist.libraryId |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static removeById(playlistId) { |  | ||||||
|       return this.destroy({ |  | ||||||
|         where: { |  | ||||||
|           id: playlistId |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get playlist by id |  | ||||||
|      * @param {string} playlistId  |  | ||||||
|      * @returns {Promise<oldPlaylist|null>} returns null if not found |  | ||||||
|      */ |  | ||||||
|     static async getById(playlistId) { |  | ||||||
|       if (!playlistId) return null |  | ||||||
|       const playlist = await this.findByPk(playlistId, { |  | ||||||
|         include: { |  | ||||||
|           model: sequelize.models.playlistMediaItem, |  | ||||||
|           include: [ |  | ||||||
|             { |  | ||||||
|               model: sequelize.models.book, |  | ||||||
|               include: sequelize.models.libraryItem |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               model: sequelize.models.podcastEpisode, |  | ||||||
|               include: { |  | ||||||
|                 model: sequelize.models.podcast, |  | ||||||
|                 include: sequelize.models.libraryItem |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|         }, |  | ||||||
|         order: [['playlistMediaItems', 'order', 'ASC']] |  | ||||||
|       }) |  | ||||||
|       if (!playlist) return null |  | ||||||
|       return this.getOldPlaylist(playlist) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get playlists for user and optionally for library |  | ||||||
|      * @param {string} userId  |  | ||||||
|      * @param {[string]} libraryId optional |  | ||||||
|      * @returns {Promise<Playlist[]>} |  | ||||||
|      */ |  | ||||||
|     static async getPlaylistsForUserAndLibrary(userId, libraryId = null) { |  | ||||||
|       if (!userId && !libraryId) return [] |  | ||||||
|       const whereQuery = {} |  | ||||||
|       if (userId) { |  | ||||||
|         whereQuery.userId = userId |  | ||||||
|       } |  | ||||||
|       if (libraryId) { |  | ||||||
|         whereQuery.libraryId = libraryId |  | ||||||
|       } |  | ||||||
|       const playlists = await this.findAll({ |  | ||||||
|         where: whereQuery, |  | ||||||
|         include: { |  | ||||||
|           model: sequelize.models.playlistMediaItem, |  | ||||||
|           include: [ |  | ||||||
|             { |  | ||||||
|               model: sequelize.models.book, |  | ||||||
|               include: sequelize.models.libraryItem |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               model: sequelize.models.podcastEpisode, |  | ||||||
|               include: { |  | ||||||
|                 model: sequelize.models.podcast, |  | ||||||
|                 include: sequelize.models.libraryItem |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|         }, |  | ||||||
|         order: [ |  | ||||||
|           [literal('name COLLATE NOCASE'), 'ASC'], |  | ||||||
|           ['playlistMediaItems', 'order', 'ASC'] |  | ||||||
|         ] |         ] | ||||||
|       }) |       }, | ||||||
|       return playlists |       order: [['playlistMediaItems', 'order', 'ASC']] | ||||||
|     } |     }) | ||||||
|  |     return playlists.map(p => this.getOldPlaylist(p)) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     /** |   static getOldPlaylist(playlistExpanded) { | ||||||
|      * Get number of playlists for a user and library |     const items = playlistExpanded.playlistMediaItems.map(pmi => { | ||||||
|      * @param {string} userId  |       const libraryItemId = pmi.mediaItem?.podcast?.libraryItem?.id || pmi.mediaItem?.libraryItem?.id || null | ||||||
|      * @param {string} libraryId  |       if (!libraryItemId) { | ||||||
|      * @returns  |         Logger.error(`[Playlist] Invalid playlist media item - No library item id found`, JSON.stringify(pmi, null, 2)) | ||||||
|      */ |         return null | ||||||
|     static async getNumPlaylistsForUserAndLibrary(userId, libraryId) { |  | ||||||
|       return this.count({ |  | ||||||
|         where: { |  | ||||||
|           userId, |  | ||||||
|           libraryId |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get all playlists for mediaItemIds |  | ||||||
|      * @param {string[]} mediaItemIds  |  | ||||||
|      * @returns {Promise<Playlist[]>} |  | ||||||
|      */ |  | ||||||
|     static async getPlaylistsForMediaItemIds(mediaItemIds) { |  | ||||||
|       if (!mediaItemIds?.length) return [] |  | ||||||
| 
 |  | ||||||
|       const playlistMediaItemsExpanded = await sequelize.models.playlistMediaItem.findAll({ |  | ||||||
|         where: { |  | ||||||
|           mediaItemId: { |  | ||||||
|             [Op.in]: mediaItemIds |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         include: [ |  | ||||||
|           { |  | ||||||
|             model: sequelize.models.playlist, |  | ||||||
|             include: { |  | ||||||
|               model: sequelize.models.playlistMediaItem, |  | ||||||
|               include: [ |  | ||||||
|                 { |  | ||||||
|                   model: sequelize.models.book, |  | ||||||
|                   include: sequelize.models.libraryItem |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                   model: sequelize.models.podcastEpisode, |  | ||||||
|                   include: { |  | ||||||
|                     model: sequelize.models.podcast, |  | ||||||
|                     include: sequelize.models.libraryItem |  | ||||||
|                   } |  | ||||||
|                 } |  | ||||||
|               ] |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         order: [['playlist', 'playlistMediaItems', 'order', 'ASC']] |  | ||||||
|       }) |  | ||||||
| 
 |  | ||||||
|       const playlists = [] |  | ||||||
|       for (const playlistMediaItem of playlistMediaItemsExpanded) { |  | ||||||
|         const playlist = playlistMediaItem.playlist |  | ||||||
|         if (playlists.some(p => p.id === playlist.id)) continue |  | ||||||
| 
 |  | ||||||
|         playlist.playlistMediaItems = playlist.playlistMediaItems.map(pmi => { |  | ||||||
|           if (pmi.mediaItemType === 'book' && pmi.book !== undefined) { |  | ||||||
|             pmi.mediaItem = pmi.book |  | ||||||
|             pmi.dataValues.mediaItem = pmi.dataValues.book |  | ||||||
|           } else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) { |  | ||||||
|             pmi.mediaItem = pmi.podcastEpisode |  | ||||||
|             pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode |  | ||||||
|           } |  | ||||||
|           delete pmi.book |  | ||||||
|           delete pmi.dataValues.book |  | ||||||
|           delete pmi.podcastEpisode |  | ||||||
|           delete pmi.dataValues.podcastEpisode |  | ||||||
|           return pmi |  | ||||||
|         }) |  | ||||||
|         playlists.push(playlist) |  | ||||||
|       } |       } | ||||||
|       return playlists |       return { | ||||||
|  |         episodeId: pmi.mediaItemType === 'podcastEpisode' ? pmi.mediaItemId : '', | ||||||
|  |         libraryItemId | ||||||
|  |       } | ||||||
|  |     }).filter(pmi => pmi) | ||||||
|  | 
 | ||||||
|  |     return new oldPlaylist({ | ||||||
|  |       id: playlistExpanded.id, | ||||||
|  |       libraryId: playlistExpanded.libraryId, | ||||||
|  |       userId: playlistExpanded.userId, | ||||||
|  |       name: playlistExpanded.name, | ||||||
|  |       description: playlistExpanded.description, | ||||||
|  |       items, | ||||||
|  |       lastUpdate: playlistExpanded.updatedAt.valueOf(), | ||||||
|  |       createdAt: playlistExpanded.createdAt.valueOf() | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get old playlist toJSONExpanded | ||||||
|  |    * @param {[string[]]} include | ||||||
|  |    * @returns {Promise<object>} oldPlaylist.toJSONExpanded | ||||||
|  |    */ | ||||||
|  |   async getOldJsonExpanded(include) { | ||||||
|  |     this.playlistMediaItems = await this.getPlaylistMediaItems({ | ||||||
|  |       include: [ | ||||||
|  |         { | ||||||
|  |           model: this.sequelize.models.book, | ||||||
|  |           include: this.sequelize.models.libraryItem | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           model: this.sequelize.models.podcastEpisode, | ||||||
|  |           include: { | ||||||
|  |             model: this.sequelize.models.podcast, | ||||||
|  |             include: this.sequelize.models.libraryItem | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       order: [['order', 'ASC']] | ||||||
|  |     }) || [] | ||||||
|  | 
 | ||||||
|  |     const oldPlaylist = this.sequelize.models.playlist.getOldPlaylist(this) | ||||||
|  |     const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId) | ||||||
|  | 
 | ||||||
|  |     let libraryItems = await this.sequelize.models.libraryItem.getAllOldLibraryItems({ | ||||||
|  |       id: libraryItemIds | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     const playlistExpanded = oldPlaylist.toJSONExpanded(libraryItems) | ||||||
|  | 
 | ||||||
|  |     if (include?.includes('rssfeed')) { | ||||||
|  |       const feeds = await this.getFeeds() | ||||||
|  |       if (feeds?.length) { | ||||||
|  |         playlistExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0]) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return playlistExpanded | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static createFromOld(oldPlaylist) { | ||||||
|  |     const playlist = this.getFromOld(oldPlaylist) | ||||||
|  |     return this.create(playlist) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static getFromOld(oldPlaylist) { | ||||||
|  |     return { | ||||||
|  |       id: oldPlaylist.id, | ||||||
|  |       name: oldPlaylist.name, | ||||||
|  |       description: oldPlaylist.description, | ||||||
|  |       userId: oldPlaylist.userId, | ||||||
|  |       libraryId: oldPlaylist.libraryId | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Playlist.init({ |   static removeById(playlistId) { | ||||||
|     id: { |     return this.destroy({ | ||||||
|       type: DataTypes.UUID, |       where: { | ||||||
|       defaultValue: DataTypes.UUIDV4, |         id: playlistId | ||||||
|       primaryKey: true |  | ||||||
|     }, |  | ||||||
|     name: DataTypes.STRING, |  | ||||||
|     description: DataTypes.TEXT |  | ||||||
|   }, { |  | ||||||
|     sequelize, |  | ||||||
|     modelName: 'playlist' |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   const { library, user } = sequelize.models |  | ||||||
|   library.hasMany(Playlist) |  | ||||||
|   Playlist.belongsTo(library) |  | ||||||
| 
 |  | ||||||
|   user.hasMany(Playlist, { |  | ||||||
|     onDelete: 'CASCADE' |  | ||||||
|   }) |  | ||||||
|   Playlist.belongsTo(user) |  | ||||||
| 
 |  | ||||||
|   Playlist.addHook('afterFind', findResult => { |  | ||||||
|     if (!findResult) return |  | ||||||
| 
 |  | ||||||
|     if (!Array.isArray(findResult)) findResult = [findResult] |  | ||||||
| 
 |  | ||||||
|     for (const instance of findResult) { |  | ||||||
|       if (instance.playlistMediaItems?.length) { |  | ||||||
|         instance.playlistMediaItems = instance.playlistMediaItems.map(pmi => { |  | ||||||
|           if (pmi.mediaItemType === 'book' && pmi.book !== undefined) { |  | ||||||
|             pmi.mediaItem = pmi.book |  | ||||||
|             pmi.dataValues.mediaItem = pmi.dataValues.book |  | ||||||
|           } else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) { |  | ||||||
|             pmi.mediaItem = pmi.podcastEpisode |  | ||||||
|             pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode |  | ||||||
|           } |  | ||||||
|           // To prevent mistakes:
 |  | ||||||
|           delete pmi.book |  | ||||||
|           delete pmi.dataValues.book |  | ||||||
|           delete pmi.podcastEpisode |  | ||||||
|           delete pmi.dataValues.podcastEpisode |  | ||||||
|           return pmi |  | ||||||
|         }) |  | ||||||
|       } |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Get playlist by id | ||||||
|  |    * @param {string} playlistId  | ||||||
|  |    * @returns {Promise<oldPlaylist|null>} returns null if not found | ||||||
|  |    */ | ||||||
|  |   static async getById(playlistId) { | ||||||
|  |     if (!playlistId) return null | ||||||
|  |     const playlist = await this.findByPk(playlistId, { | ||||||
|  |       include: { | ||||||
|  |         model: this.sequelize.models.playlistMediaItem, | ||||||
|  |         include: [ | ||||||
|  |           { | ||||||
|  |             model: this.sequelize.models.book, | ||||||
|  |             include: this.sequelize.models.libraryItem | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             model: this.sequelize.models.podcastEpisode, | ||||||
|  |             include: { | ||||||
|  |               model: this.sequelize.models.podcast, | ||||||
|  |               include: this.sequelize.models.libraryItem | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       order: [['playlistMediaItems', 'order', 'ASC']] | ||||||
|  |     }) | ||||||
|  |     if (!playlist) return null | ||||||
|  |     return this.getOldPlaylist(playlist) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get playlists for user and optionally for library | ||||||
|  |    * @param {string} userId  | ||||||
|  |    * @param {[string]} libraryId optional | ||||||
|  |    * @returns {Promise<Playlist[]>} | ||||||
|  |    */ | ||||||
|  |   static async getPlaylistsForUserAndLibrary(userId, libraryId = null) { | ||||||
|  |     if (!userId && !libraryId) return [] | ||||||
|  |     const whereQuery = {} | ||||||
|  |     if (userId) { | ||||||
|  |       whereQuery.userId = userId | ||||||
|     } |     } | ||||||
|   }) |     if (libraryId) { | ||||||
|  |       whereQuery.libraryId = libraryId | ||||||
|  |     } | ||||||
|  |     const playlists = await this.findAll({ | ||||||
|  |       where: whereQuery, | ||||||
|  |       include: { | ||||||
|  |         model: this.sequelize.models.playlistMediaItem, | ||||||
|  |         include: [ | ||||||
|  |           { | ||||||
|  |             model: this.sequelize.models.book, | ||||||
|  |             include: this.sequelize.models.libraryItem | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             model: this.sequelize.models.podcastEpisode, | ||||||
|  |             include: { | ||||||
|  |               model: this.sequelize.models.podcast, | ||||||
|  |               include: this.sequelize.models.libraryItem | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       order: [ | ||||||
|  |         [literal('name COLLATE NOCASE'), 'ASC'], | ||||||
|  |         ['playlistMediaItems', 'order', 'ASC'] | ||||||
|  |       ] | ||||||
|  |     }) | ||||||
|  |     return playlists | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   return Playlist |   /** | ||||||
| } |    * Get number of playlists for a user and library | ||||||
|  |    * @param {string} userId  | ||||||
|  |    * @param {string} libraryId  | ||||||
|  |    * @returns  | ||||||
|  |    */ | ||||||
|  |   static async getNumPlaylistsForUserAndLibrary(userId, libraryId) { | ||||||
|  |     return this.count({ | ||||||
|  |       where: { | ||||||
|  |         userId, | ||||||
|  |         libraryId | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get all playlists for mediaItemIds | ||||||
|  |    * @param {string[]} mediaItemIds  | ||||||
|  |    * @returns {Promise<Playlist[]>} | ||||||
|  |    */ | ||||||
|  |   static async getPlaylistsForMediaItemIds(mediaItemIds) { | ||||||
|  |     if (!mediaItemIds?.length) return [] | ||||||
|  | 
 | ||||||
|  |     const playlistMediaItemsExpanded = await this.sequelize.models.playlistMediaItem.findAll({ | ||||||
|  |       where: { | ||||||
|  |         mediaItemId: { | ||||||
|  |           [Op.in]: mediaItemIds | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       include: [ | ||||||
|  |         { | ||||||
|  |           model: this.sequelize.models.playlist, | ||||||
|  |           include: { | ||||||
|  |             model: this.sequelize.models.playlistMediaItem, | ||||||
|  |             include: [ | ||||||
|  |               { | ||||||
|  |                 model: this.sequelize.models.book, | ||||||
|  |                 include: this.sequelize.models.libraryItem | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 model: this.sequelize.models.podcastEpisode, | ||||||
|  |                 include: { | ||||||
|  |                   model: this.sequelize.models.podcast, | ||||||
|  |                   include: this.sequelize.models.libraryItem | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             ] | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       order: [['playlist', 'playlistMediaItems', 'order', 'ASC']] | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     const playlists = [] | ||||||
|  |     for (const playlistMediaItem of playlistMediaItemsExpanded) { | ||||||
|  |       const playlist = playlistMediaItem.playlist | ||||||
|  |       if (playlists.some(p => p.id === playlist.id)) continue | ||||||
|  | 
 | ||||||
|  |       playlist.playlistMediaItems = playlist.playlistMediaItems.map(pmi => { | ||||||
|  |         if (pmi.mediaItemType === 'book' && pmi.book !== undefined) { | ||||||
|  |           pmi.mediaItem = pmi.book | ||||||
|  |           pmi.dataValues.mediaItem = pmi.dataValues.book | ||||||
|  |         } else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) { | ||||||
|  |           pmi.mediaItem = pmi.podcastEpisode | ||||||
|  |           pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode | ||||||
|  |         } | ||||||
|  |         delete pmi.book | ||||||
|  |         delete pmi.dataValues.book | ||||||
|  |         delete pmi.podcastEpisode | ||||||
|  |         delete pmi.dataValues.podcastEpisode | ||||||
|  |         return pmi | ||||||
|  |       }) | ||||||
|  |       playlists.push(playlist) | ||||||
|  |     } | ||||||
|  |     return playlists | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Initialize model | ||||||
|  |    * @param {import('../Database').sequelize} sequelize  | ||||||
|  |    */ | ||||||
|  |   static init(sequelize) { | ||||||
|  |     super.init({ | ||||||
|  |       id: { | ||||||
|  |         type: DataTypes.UUID, | ||||||
|  |         defaultValue: DataTypes.UUIDV4, | ||||||
|  |         primaryKey: true | ||||||
|  |       }, | ||||||
|  |       name: DataTypes.STRING, | ||||||
|  |       description: DataTypes.TEXT | ||||||
|  |     }, { | ||||||
|  |       sequelize, | ||||||
|  |       modelName: 'playlist' | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     const { library, user } = sequelize.models | ||||||
|  |     library.hasMany(Playlist) | ||||||
|  |     Playlist.belongsTo(library) | ||||||
|  | 
 | ||||||
|  |     user.hasMany(Playlist, { | ||||||
|  |       onDelete: 'CASCADE' | ||||||
|  |     }) | ||||||
|  |     Playlist.belongsTo(user) | ||||||
|  | 
 | ||||||
|  |     Playlist.addHook('afterFind', findResult => { | ||||||
|  |       if (!findResult) return | ||||||
|  | 
 | ||||||
|  |       if (!Array.isArray(findResult)) findResult = [findResult] | ||||||
|  | 
 | ||||||
|  |       for (const instance of findResult) { | ||||||
|  |         if (instance.playlistMediaItems?.length) { | ||||||
|  |           instance.playlistMediaItems = instance.playlistMediaItems.map(pmi => { | ||||||
|  |             if (pmi.mediaItemType === 'book' && pmi.book !== undefined) { | ||||||
|  |               pmi.mediaItem = pmi.book | ||||||
|  |               pmi.dataValues.mediaItem = pmi.dataValues.book | ||||||
|  |             } else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) { | ||||||
|  |               pmi.mediaItem = pmi.podcastEpisode | ||||||
|  |               pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode | ||||||
|  |             } | ||||||
|  |             // To prevent mistakes:
 | ||||||
|  |             delete pmi.book | ||||||
|  |             delete pmi.dataValues.book | ||||||
|  |             delete pmi.podcastEpisode | ||||||
|  |             delete pmi.dataValues.podcastEpisode | ||||||
|  |             return pmi | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = Playlist | ||||||
| @ -1,84 +1,105 @@ | |||||||
| const { DataTypes, Model } = require('sequelize') | const { DataTypes, Model } = require('sequelize') | ||||||
| 
 | 
 | ||||||
| module.exports = (sequelize) => { | class PlaylistMediaItem extends Model { | ||||||
|   class PlaylistMediaItem extends Model { |   constructor(values, options) { | ||||||
|     static removeByIds(playlistId, mediaItemId) { |     super(values, options) | ||||||
|       return this.destroy({ |  | ||||||
|         where: { |  | ||||||
|           playlistId, |  | ||||||
|           mediaItemId |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     getMediaItem(options) { |     /** @type {UUIDV4} */ | ||||||
|       if (!this.mediaItemType) return Promise.resolve(null) |     this.id | ||||||
|       const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` |     /** @type {UUIDV4} */ | ||||||
|       return this[mixinMethodName](options) |     this.mediaItemId | ||||||
|     } |     /** @type {string} */ | ||||||
|  |     this.mediaItemType | ||||||
|  |     /** @type {number} */ | ||||||
|  |     this.order | ||||||
|  |     /** @type {UUIDV4} */ | ||||||
|  |     this.playlistId | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.createdAt | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   PlaylistMediaItem.init({ |   static removeByIds(playlistId, mediaItemId) { | ||||||
|     id: { |     return this.destroy({ | ||||||
|       type: DataTypes.UUID, |       where: { | ||||||
|       defaultValue: DataTypes.UUIDV4, |         playlistId, | ||||||
|       primaryKey: true |         mediaItemId | ||||||
|     }, |  | ||||||
|     mediaItemId: DataTypes.UUIDV4, |  | ||||||
|     mediaItemType: DataTypes.STRING, |  | ||||||
|     order: DataTypes.INTEGER |  | ||||||
|   }, { |  | ||||||
|     sequelize, |  | ||||||
|     timestamps: true, |  | ||||||
|     updatedAt: false, |  | ||||||
|     modelName: 'playlistMediaItem' |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   const { book, podcastEpisode, playlist } = sequelize.models |  | ||||||
| 
 |  | ||||||
|   book.hasMany(PlaylistMediaItem, { |  | ||||||
|     foreignKey: 'mediaItemId', |  | ||||||
|     constraints: false, |  | ||||||
|     scope: { |  | ||||||
|       mediaItemType: 'book' |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|   PlaylistMediaItem.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) |  | ||||||
| 
 |  | ||||||
|   podcastEpisode.hasOne(PlaylistMediaItem, { |  | ||||||
|     foreignKey: 'mediaItemId', |  | ||||||
|     constraints: false, |  | ||||||
|     scope: { |  | ||||||
|       mediaItemType: 'podcastEpisode' |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|   PlaylistMediaItem.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) |  | ||||||
| 
 |  | ||||||
|   PlaylistMediaItem.addHook('afterFind', findResult => { |  | ||||||
|     if (!findResult) return |  | ||||||
| 
 |  | ||||||
|     if (!Array.isArray(findResult)) findResult = [findResult] |  | ||||||
| 
 |  | ||||||
|     for (const instance of findResult) { |  | ||||||
|       if (instance.mediaItemType === 'book' && instance.book !== undefined) { |  | ||||||
|         instance.mediaItem = instance.book |  | ||||||
|         instance.dataValues.mediaItem = instance.dataValues.book |  | ||||||
|       } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { |  | ||||||
|         instance.mediaItem = instance.podcastEpisode |  | ||||||
|         instance.dataValues.mediaItem = instance.dataValues.podcastEpisode |  | ||||||
|       } |       } | ||||||
|       // To prevent mistakes:
 |     }) | ||||||
|       delete instance.book |   } | ||||||
|       delete instance.dataValues.book |  | ||||||
|       delete instance.podcastEpisode |  | ||||||
|       delete instance.dataValues.podcastEpisode |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
| 
 | 
 | ||||||
|   playlist.hasMany(PlaylistMediaItem, { |   getMediaItem(options) { | ||||||
|     onDelete: 'CASCADE' |     if (!this.mediaItemType) return Promise.resolve(null) | ||||||
|   }) |     const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}` | ||||||
|   PlaylistMediaItem.belongsTo(playlist) |     return this[mixinMethodName](options) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   return PlaylistMediaItem |   /** | ||||||
| } |    * Initialize model | ||||||
|  |    * @param {import('../Database').sequelize} sequelize  | ||||||
|  |    */ | ||||||
|  |   static init(sequelize) { | ||||||
|  |     super.init({ | ||||||
|  |       id: { | ||||||
|  |         type: DataTypes.UUID, | ||||||
|  |         defaultValue: DataTypes.UUIDV4, | ||||||
|  |         primaryKey: true | ||||||
|  |       }, | ||||||
|  |       mediaItemId: DataTypes.UUIDV4, | ||||||
|  |       mediaItemType: DataTypes.STRING, | ||||||
|  |       order: DataTypes.INTEGER | ||||||
|  |     }, { | ||||||
|  |       sequelize, | ||||||
|  |       timestamps: true, | ||||||
|  |       updatedAt: false, | ||||||
|  |       modelName: 'playlistMediaItem' | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     const { book, podcastEpisode, playlist } = sequelize.models | ||||||
|  | 
 | ||||||
|  |     book.hasMany(PlaylistMediaItem, { | ||||||
|  |       foreignKey: 'mediaItemId', | ||||||
|  |       constraints: false, | ||||||
|  |       scope: { | ||||||
|  |         mediaItemType: 'book' | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     PlaylistMediaItem.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) | ||||||
|  | 
 | ||||||
|  |     podcastEpisode.hasOne(PlaylistMediaItem, { | ||||||
|  |       foreignKey: 'mediaItemId', | ||||||
|  |       constraints: false, | ||||||
|  |       scope: { | ||||||
|  |         mediaItemType: 'podcastEpisode' | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     PlaylistMediaItem.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) | ||||||
|  | 
 | ||||||
|  |     PlaylistMediaItem.addHook('afterFind', findResult => { | ||||||
|  |       if (!findResult) return | ||||||
|  | 
 | ||||||
|  |       if (!Array.isArray(findResult)) findResult = [findResult] | ||||||
|  | 
 | ||||||
|  |       for (const instance of findResult) { | ||||||
|  |         if (instance.mediaItemType === 'book' && instance.book !== undefined) { | ||||||
|  |           instance.mediaItem = instance.book | ||||||
|  |           instance.dataValues.mediaItem = instance.dataValues.book | ||||||
|  |         } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { | ||||||
|  |           instance.mediaItem = instance.podcastEpisode | ||||||
|  |           instance.dataValues.mediaItem = instance.dataValues.podcastEpisode | ||||||
|  |         } | ||||||
|  |         // To prevent mistakes:
 | ||||||
|  |         delete instance.book | ||||||
|  |         delete instance.dataValues.book | ||||||
|  |         delete instance.podcastEpisode | ||||||
|  |         delete instance.dataValues.podcastEpisode | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     playlist.hasMany(PlaylistMediaItem, { | ||||||
|  |       onDelete: 'CASCADE' | ||||||
|  |     }) | ||||||
|  |     PlaylistMediaItem.belongsTo(playlist) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = PlaylistMediaItem | ||||||
|  | |||||||
| @ -1,100 +1,155 @@ | |||||||
| const { DataTypes, Model } = require('sequelize') | const { DataTypes, Model } = require('sequelize') | ||||||
| 
 | 
 | ||||||
| module.exports = (sequelize) => { | class Podcast extends Model { | ||||||
|   class Podcast extends Model { |   constructor(values, options) { | ||||||
|     static getOldPodcast(libraryItemExpanded) { |     super(values, options) | ||||||
|       const podcastExpanded = libraryItemExpanded.media |  | ||||||
|       const podcastEpisodes = podcastExpanded.podcastEpisodes?.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id)).sort((a, b) => a.index - b.index) |  | ||||||
|       return { |  | ||||||
|         id: podcastExpanded.id, |  | ||||||
|         libraryItemId: libraryItemExpanded.id, |  | ||||||
|         metadata: { |  | ||||||
|           title: podcastExpanded.title, |  | ||||||
|           author: podcastExpanded.author, |  | ||||||
|           description: podcastExpanded.description, |  | ||||||
|           releaseDate: podcastExpanded.releaseDate, |  | ||||||
|           genres: podcastExpanded.genres, |  | ||||||
|           feedUrl: podcastExpanded.feedURL, |  | ||||||
|           imageUrl: podcastExpanded.imageURL, |  | ||||||
|           itunesPageUrl: podcastExpanded.itunesPageURL, |  | ||||||
|           itunesId: podcastExpanded.itunesId, |  | ||||||
|           itunesArtistId: podcastExpanded.itunesArtistId, |  | ||||||
|           explicit: podcastExpanded.explicit, |  | ||||||
|           language: podcastExpanded.language, |  | ||||||
|           type: podcastExpanded.podcastType |  | ||||||
|         }, |  | ||||||
|         coverPath: podcastExpanded.coverPath, |  | ||||||
|         tags: podcastExpanded.tags, |  | ||||||
|         episodes: podcastEpisodes || [], |  | ||||||
|         autoDownloadEpisodes: podcastExpanded.autoDownloadEpisodes, |  | ||||||
|         autoDownloadSchedule: podcastExpanded.autoDownloadSchedule, |  | ||||||
|         lastEpisodeCheck: podcastExpanded.lastEpisodeCheck?.valueOf() || null, |  | ||||||
|         maxEpisodesToKeep: podcastExpanded.maxEpisodesToKeep, |  | ||||||
|         maxNewEpisodesToDownload: podcastExpanded.maxNewEpisodesToDownload |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     static getFromOld(oldPodcast) { |     /** @type {UUIDV4} */ | ||||||
|       const oldPodcastMetadata = oldPodcast.metadata |     this.id | ||||||
|       return { |     /** @type {string} */ | ||||||
|         id: oldPodcast.id, |     this.title | ||||||
|         title: oldPodcastMetadata.title, |     /** @type {string} */ | ||||||
|         titleIgnorePrefix: oldPodcastMetadata.titleIgnorePrefix, |     this.titleIgnorePrefix | ||||||
|         author: oldPodcastMetadata.author, |     /** @type {string} */ | ||||||
|         releaseDate: oldPodcastMetadata.releaseDate, |     this.author | ||||||
|         feedURL: oldPodcastMetadata.feedUrl, |     /** @type {string} */ | ||||||
|         imageURL: oldPodcastMetadata.imageUrl, |     this.releaseDate | ||||||
|         description: oldPodcastMetadata.description, |     /** @type {string} */ | ||||||
|         itunesPageURL: oldPodcastMetadata.itunesPageUrl, |     this.feedURL | ||||||
|         itunesId: oldPodcastMetadata.itunesId, |     /** @type {string} */ | ||||||
|         itunesArtistId: oldPodcastMetadata.itunesArtistId, |     this.imageURL | ||||||
|         language: oldPodcastMetadata.language, |     /** @type {string} */ | ||||||
|         podcastType: oldPodcastMetadata.type, |     this.description | ||||||
|         explicit: !!oldPodcastMetadata.explicit, |     /** @type {string} */ | ||||||
|         autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes, |     this.itunesPageURL | ||||||
|         autoDownloadSchedule: oldPodcast.autoDownloadSchedule, |     /** @type {string} */ | ||||||
|         lastEpisodeCheck: oldPodcast.lastEpisodeCheck, |     this.itunesId | ||||||
|         maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep, |     /** @type {string} */ | ||||||
|         maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload, |     this.itunesArtistId | ||||||
|         coverPath: oldPodcast.coverPath, |     /** @type {string} */ | ||||||
|         tags: oldPodcast.tags, |     this.language | ||||||
|         genres: oldPodcastMetadata.genres |     /** @type {string} */ | ||||||
|       } |     this.podcastType | ||||||
|  |     /** @type {boolean} */ | ||||||
|  |     this.explicit | ||||||
|  |     /** @type {boolean} */ | ||||||
|  |     this.autoDownloadEpisodes | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.autoDownloadSchedule | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.lastEpisodeCheck | ||||||
|  |     /** @type {number} */ | ||||||
|  |     this.maxEpisodesToKeep | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.coverPath | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.tags | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.genres | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.createdAt | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.updatedAt | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static getOldPodcast(libraryItemExpanded) { | ||||||
|  |     const podcastExpanded = libraryItemExpanded.media | ||||||
|  |     const podcastEpisodes = podcastExpanded.podcastEpisodes?.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id)).sort((a, b) => a.index - b.index) | ||||||
|  |     return { | ||||||
|  |       id: podcastExpanded.id, | ||||||
|  |       libraryItemId: libraryItemExpanded.id, | ||||||
|  |       metadata: { | ||||||
|  |         title: podcastExpanded.title, | ||||||
|  |         author: podcastExpanded.author, | ||||||
|  |         description: podcastExpanded.description, | ||||||
|  |         releaseDate: podcastExpanded.releaseDate, | ||||||
|  |         genres: podcastExpanded.genres, | ||||||
|  |         feedUrl: podcastExpanded.feedURL, | ||||||
|  |         imageUrl: podcastExpanded.imageURL, | ||||||
|  |         itunesPageUrl: podcastExpanded.itunesPageURL, | ||||||
|  |         itunesId: podcastExpanded.itunesId, | ||||||
|  |         itunesArtistId: podcastExpanded.itunesArtistId, | ||||||
|  |         explicit: podcastExpanded.explicit, | ||||||
|  |         language: podcastExpanded.language, | ||||||
|  |         type: podcastExpanded.podcastType | ||||||
|  |       }, | ||||||
|  |       coverPath: podcastExpanded.coverPath, | ||||||
|  |       tags: podcastExpanded.tags, | ||||||
|  |       episodes: podcastEpisodes || [], | ||||||
|  |       autoDownloadEpisodes: podcastExpanded.autoDownloadEpisodes, | ||||||
|  |       autoDownloadSchedule: podcastExpanded.autoDownloadSchedule, | ||||||
|  |       lastEpisodeCheck: podcastExpanded.lastEpisodeCheck?.valueOf() || null, | ||||||
|  |       maxEpisodesToKeep: podcastExpanded.maxEpisodesToKeep, | ||||||
|  |       maxNewEpisodesToDownload: podcastExpanded.maxNewEpisodesToDownload | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Podcast.init({ |   static getFromOld(oldPodcast) { | ||||||
|     id: { |     const oldPodcastMetadata = oldPodcast.metadata | ||||||
|       type: DataTypes.UUID, |     return { | ||||||
|       defaultValue: DataTypes.UUIDV4, |       id: oldPodcast.id, | ||||||
|       primaryKey: true |       title: oldPodcastMetadata.title, | ||||||
|     }, |       titleIgnorePrefix: oldPodcastMetadata.titleIgnorePrefix, | ||||||
|     title: DataTypes.STRING, |       author: oldPodcastMetadata.author, | ||||||
|     titleIgnorePrefix: DataTypes.STRING, |       releaseDate: oldPodcastMetadata.releaseDate, | ||||||
|     author: DataTypes.STRING, |       feedURL: oldPodcastMetadata.feedUrl, | ||||||
|     releaseDate: DataTypes.STRING, |       imageURL: oldPodcastMetadata.imageUrl, | ||||||
|     feedURL: DataTypes.STRING, |       description: oldPodcastMetadata.description, | ||||||
|     imageURL: DataTypes.STRING, |       itunesPageURL: oldPodcastMetadata.itunesPageUrl, | ||||||
|     description: DataTypes.TEXT, |       itunesId: oldPodcastMetadata.itunesId, | ||||||
|     itunesPageURL: DataTypes.STRING, |       itunesArtistId: oldPodcastMetadata.itunesArtistId, | ||||||
|     itunesId: DataTypes.STRING, |       language: oldPodcastMetadata.language, | ||||||
|     itunesArtistId: DataTypes.STRING, |       podcastType: oldPodcastMetadata.type, | ||||||
|     language: DataTypes.STRING, |       explicit: !!oldPodcastMetadata.explicit, | ||||||
|     podcastType: DataTypes.STRING, |       autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes, | ||||||
|     explicit: DataTypes.BOOLEAN, |       autoDownloadSchedule: oldPodcast.autoDownloadSchedule, | ||||||
|  |       lastEpisodeCheck: oldPodcast.lastEpisodeCheck, | ||||||
|  |       maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep, | ||||||
|  |       maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload, | ||||||
|  |       coverPath: oldPodcast.coverPath, | ||||||
|  |       tags: oldPodcast.tags, | ||||||
|  |       genres: oldPodcastMetadata.genres | ||||||
|  |     } | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     autoDownloadEpisodes: DataTypes.BOOLEAN, |   /** | ||||||
|     autoDownloadSchedule: DataTypes.STRING, |    * Initialize model | ||||||
|     lastEpisodeCheck: DataTypes.DATE, |    * @param {import('../Database').sequelize} sequelize  | ||||||
|     maxEpisodesToKeep: DataTypes.INTEGER, |    */ | ||||||
|     maxNewEpisodesToDownload: DataTypes.INTEGER, |   static init(sequelize) { | ||||||
|     coverPath: DataTypes.STRING, |     super.init({ | ||||||
|     tags: DataTypes.JSON, |       id: { | ||||||
|     genres: DataTypes.JSON |         type: DataTypes.UUID, | ||||||
|   }, { |         defaultValue: DataTypes.UUIDV4, | ||||||
|     sequelize, |         primaryKey: true | ||||||
|     modelName: 'podcast' |       }, | ||||||
|   }) |       title: DataTypes.STRING, | ||||||
|  |       titleIgnorePrefix: DataTypes.STRING, | ||||||
|  |       author: DataTypes.STRING, | ||||||
|  |       releaseDate: DataTypes.STRING, | ||||||
|  |       feedURL: DataTypes.STRING, | ||||||
|  |       imageURL: DataTypes.STRING, | ||||||
|  |       description: DataTypes.TEXT, | ||||||
|  |       itunesPageURL: DataTypes.STRING, | ||||||
|  |       itunesId: DataTypes.STRING, | ||||||
|  |       itunesArtistId: DataTypes.STRING, | ||||||
|  |       language: DataTypes.STRING, | ||||||
|  |       podcastType: DataTypes.STRING, | ||||||
|  |       explicit: DataTypes.BOOLEAN, | ||||||
| 
 | 
 | ||||||
|   return Podcast |       autoDownloadEpisodes: DataTypes.BOOLEAN, | ||||||
| } |       autoDownloadSchedule: DataTypes.STRING, | ||||||
|  |       lastEpisodeCheck: DataTypes.DATE, | ||||||
|  |       maxEpisodesToKeep: DataTypes.INTEGER, | ||||||
|  |       maxNewEpisodesToDownload: DataTypes.INTEGER, | ||||||
|  |       coverPath: DataTypes.STRING, | ||||||
|  |       tags: DataTypes.JSON, | ||||||
|  |       genres: DataTypes.JSON | ||||||
|  |     }, { | ||||||
|  |       sequelize, | ||||||
|  |       modelName: 'podcast' | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = Podcast | ||||||
| @ -1,102 +1,149 @@ | |||||||
| const { DataTypes, Model } = require('sequelize') | const { DataTypes, Model } = require('sequelize') | ||||||
| 
 | 
 | ||||||
| module.exports = (sequelize) => { | class PodcastEpisode extends Model { | ||||||
|   class PodcastEpisode extends Model { |   constructor(values, options) { | ||||||
|     getOldPodcastEpisode(libraryItemId = null) { |     super(values, options) | ||||||
|       let enclosure = null | 
 | ||||||
|       if (this.enclosureURL) { |     /** @type {UUIDV4} */ | ||||||
|         enclosure = { |     this.id | ||||||
|           url: this.enclosureURL, |     /** @type {number} */ | ||||||
|           type: this.enclosureType, |     this.index | ||||||
|           length: this.enclosureSize !== null ? String(this.enclosureSize) : null |     /** @type {string} */ | ||||||
|         } |     this.season | ||||||
|       } |     /** @type {string} */ | ||||||
|       return { |     this.episode | ||||||
|         libraryItemId: libraryItemId || null, |     /** @type {string} */ | ||||||
|         podcastId: this.podcastId, |     this.episodeType | ||||||
|         id: this.id, |     /** @type {string} */ | ||||||
|         oldEpisodeId: this.extraData?.oldEpisodeId || null, |     this.title | ||||||
|         index: this.index, |     /** @type {string} */ | ||||||
|         season: this.season, |     this.subtitle | ||||||
|         episode: this.episode, |     /** @type {string} */ | ||||||
|         episodeType: this.episodeType, |     this.description | ||||||
|         title: this.title, |     /** @type {string} */ | ||||||
|         subtitle: this.subtitle, |     this.pubDate | ||||||
|         description: this.description, |     /** @type {string} */ | ||||||
|         enclosure, |     this.enclosureURL | ||||||
|         pubDate: this.pubDate, |     /** @type {BigInt} */ | ||||||
|         chapters: this.chapters, |     this.enclosureSize | ||||||
|         audioFile: this.audioFile, |     /** @type {string} */ | ||||||
|         publishedAt: this.publishedAt?.valueOf() || null, |     this.enclosureType | ||||||
|         addedAt: this.createdAt.valueOf(), |     /** @type {Date} */ | ||||||
|         updatedAt: this.updatedAt.valueOf() |     this.publishedAt | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.audioFile | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.chapters | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.extraData | ||||||
|  |     /** @type {UUIDV4} */ | ||||||
|  |     this.podcastId | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.createdAt | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.updatedAt | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getOldPodcastEpisode(libraryItemId = null) { | ||||||
|  |     let enclosure = null | ||||||
|  |     if (this.enclosureURL) { | ||||||
|  |       enclosure = { | ||||||
|  |         url: this.enclosureURL, | ||||||
|  |         type: this.enclosureType, | ||||||
|  |         length: this.enclosureSize !== null ? String(this.enclosureSize) : null | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 |     return { | ||||||
|     static createFromOld(oldEpisode) { |       libraryItemId: libraryItemId || null, | ||||||
|       const podcastEpisode = this.getFromOld(oldEpisode) |       podcastId: this.podcastId, | ||||||
|       return this.create(podcastEpisode) |       id: this.id, | ||||||
|     } |       oldEpisodeId: this.extraData?.oldEpisodeId || null, | ||||||
| 
 |       index: this.index, | ||||||
|     static getFromOld(oldEpisode) { |       season: this.season, | ||||||
|       const extraData = {} |       episode: this.episode, | ||||||
|       if (oldEpisode.oldEpisodeId) { |       episodeType: this.episodeType, | ||||||
|         extraData.oldEpisodeId = oldEpisode.oldEpisodeId |       title: this.title, | ||||||
|       } |       subtitle: this.subtitle, | ||||||
|       return { |       description: this.description, | ||||||
|         id: oldEpisode.id, |       enclosure, | ||||||
|         index: oldEpisode.index, |       pubDate: this.pubDate, | ||||||
|         season: oldEpisode.season, |       chapters: this.chapters, | ||||||
|         episode: oldEpisode.episode, |       audioFile: this.audioFile, | ||||||
|         episodeType: oldEpisode.episodeType, |       publishedAt: this.publishedAt?.valueOf() || null, | ||||||
|         title: oldEpisode.title, |       addedAt: this.createdAt.valueOf(), | ||||||
|         subtitle: oldEpisode.subtitle, |       updatedAt: this.updatedAt.valueOf() | ||||||
|         description: oldEpisode.description, |  | ||||||
|         pubDate: oldEpisode.pubDate, |  | ||||||
|         enclosureURL: oldEpisode.enclosure?.url || null, |  | ||||||
|         enclosureSize: oldEpisode.enclosure?.length || null, |  | ||||||
|         enclosureType: oldEpisode.enclosure?.type || null, |  | ||||||
|         publishedAt: oldEpisode.publishedAt, |  | ||||||
|         podcastId: oldEpisode.podcastId, |  | ||||||
|         audioFile: oldEpisode.audioFile?.toJSON() || null, |  | ||||||
|         chapters: oldEpisode.chapters, |  | ||||||
|         extraData |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   PodcastEpisode.init({ |   static createFromOld(oldEpisode) { | ||||||
|     id: { |     const podcastEpisode = this.getFromOld(oldEpisode) | ||||||
|       type: DataTypes.UUID, |     return this.create(podcastEpisode) | ||||||
|       defaultValue: DataTypes.UUIDV4, |   } | ||||||
|       primaryKey: true |  | ||||||
|     }, |  | ||||||
|     index: DataTypes.INTEGER, |  | ||||||
|     season: DataTypes.STRING, |  | ||||||
|     episode: DataTypes.STRING, |  | ||||||
|     episodeType: DataTypes.STRING, |  | ||||||
|     title: DataTypes.STRING, |  | ||||||
|     subtitle: DataTypes.STRING(1000), |  | ||||||
|     description: DataTypes.TEXT, |  | ||||||
|     pubDate: DataTypes.STRING, |  | ||||||
|     enclosureURL: DataTypes.STRING, |  | ||||||
|     enclosureSize: DataTypes.BIGINT, |  | ||||||
|     enclosureType: DataTypes.STRING, |  | ||||||
|     publishedAt: DataTypes.DATE, |  | ||||||
| 
 | 
 | ||||||
|     audioFile: DataTypes.JSON, |   static getFromOld(oldEpisode) { | ||||||
|     chapters: DataTypes.JSON, |     const extraData = {} | ||||||
|     extraData: DataTypes.JSON |     if (oldEpisode.oldEpisodeId) { | ||||||
|   }, { |       extraData.oldEpisodeId = oldEpisode.oldEpisodeId | ||||||
|     sequelize, |     } | ||||||
|     modelName: 'podcastEpisode' |     return { | ||||||
|   }) |       id: oldEpisode.id, | ||||||
|  |       index: oldEpisode.index, | ||||||
|  |       season: oldEpisode.season, | ||||||
|  |       episode: oldEpisode.episode, | ||||||
|  |       episodeType: oldEpisode.episodeType, | ||||||
|  |       title: oldEpisode.title, | ||||||
|  |       subtitle: oldEpisode.subtitle, | ||||||
|  |       description: oldEpisode.description, | ||||||
|  |       pubDate: oldEpisode.pubDate, | ||||||
|  |       enclosureURL: oldEpisode.enclosure?.url || null, | ||||||
|  |       enclosureSize: oldEpisode.enclosure?.length || null, | ||||||
|  |       enclosureType: oldEpisode.enclosure?.type || null, | ||||||
|  |       publishedAt: oldEpisode.publishedAt, | ||||||
|  |       podcastId: oldEpisode.podcastId, | ||||||
|  |       audioFile: oldEpisode.audioFile?.toJSON() || null, | ||||||
|  |       chapters: oldEpisode.chapters, | ||||||
|  |       extraData | ||||||
|  |     } | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   const { podcast } = sequelize.models |   /** | ||||||
|   podcast.hasMany(PodcastEpisode, { |    * Initialize model | ||||||
|     onDelete: 'CASCADE' |    * @param {import('../Database').sequelize} sequelize  | ||||||
|   }) |    */ | ||||||
|   PodcastEpisode.belongsTo(podcast) |   static init(sequelize) { | ||||||
|  |     super.init({ | ||||||
|  |       id: { | ||||||
|  |         type: DataTypes.UUID, | ||||||
|  |         defaultValue: DataTypes.UUIDV4, | ||||||
|  |         primaryKey: true | ||||||
|  |       }, | ||||||
|  |       index: DataTypes.INTEGER, | ||||||
|  |       season: DataTypes.STRING, | ||||||
|  |       episode: DataTypes.STRING, | ||||||
|  |       episodeType: DataTypes.STRING, | ||||||
|  |       title: DataTypes.STRING, | ||||||
|  |       subtitle: DataTypes.STRING(1000), | ||||||
|  |       description: DataTypes.TEXT, | ||||||
|  |       pubDate: DataTypes.STRING, | ||||||
|  |       enclosureURL: DataTypes.STRING, | ||||||
|  |       enclosureSize: DataTypes.BIGINT, | ||||||
|  |       enclosureType: DataTypes.STRING, | ||||||
|  |       publishedAt: DataTypes.DATE, | ||||||
| 
 | 
 | ||||||
|   return PodcastEpisode |       audioFile: DataTypes.JSON, | ||||||
| } |       chapters: DataTypes.JSON, | ||||||
|  |       extraData: DataTypes.JSON | ||||||
|  |     }, { | ||||||
|  |       sequelize, | ||||||
|  |       modelName: 'podcastEpisode' | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     const { podcast } = sequelize.models | ||||||
|  |     podcast.hasMany(PodcastEpisode, { | ||||||
|  |       onDelete: 'CASCADE' | ||||||
|  |     }) | ||||||
|  |     PodcastEpisode.belongsTo(podcast) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = PodcastEpisode | ||||||
| @ -2,81 +2,104 @@ const { DataTypes, Model } = require('sequelize') | |||||||
| 
 | 
 | ||||||
| const oldSeries = require('../objects/entities/Series') | const oldSeries = require('../objects/entities/Series') | ||||||
| 
 | 
 | ||||||
| module.exports = (sequelize) => { | class Series extends Model { | ||||||
|   class Series extends Model { |   constructor(values, options) { | ||||||
|     static async getAllOldSeries() { |     super(values, options) | ||||||
|       const series = await this.findAll() |  | ||||||
|       return series.map(se => se.getOldSeries()) |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     getOldSeries() { |     /** @type {UUIDV4} */ | ||||||
|       return new oldSeries({ |     this.id | ||||||
|         id: this.id, |     /** @type {string} */ | ||||||
|         name: this.name, |     this.name | ||||||
|         description: this.description, |     /** @type {string} */ | ||||||
|         libraryId: this.libraryId, |     this.nameIgnorePrefix | ||||||
|         addedAt: this.createdAt.valueOf(), |     /** @type {string} */ | ||||||
|         updatedAt: this.updatedAt.valueOf() |     this.description | ||||||
|       }) |     /** @type {UUIDV4} */ | ||||||
|     } |     this.libraryId | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.createdAt | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.updatedAt | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     static updateFromOld(oldSeries) { |   static async getAllOldSeries() { | ||||||
|       const series = this.getFromOld(oldSeries) |     const series = await this.findAll() | ||||||
|       return this.update(series, { |     return series.map(se => se.getOldSeries()) | ||||||
|         where: { |   } | ||||||
|           id: series.id |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     static createFromOld(oldSeries) { |   getOldSeries() { | ||||||
|       const series = this.getFromOld(oldSeries) |     return new oldSeries({ | ||||||
|       return this.create(series) |       id: this.id, | ||||||
|     } |       name: this.name, | ||||||
|  |       description: this.description, | ||||||
|  |       libraryId: this.libraryId, | ||||||
|  |       addedAt: this.createdAt.valueOf(), | ||||||
|  |       updatedAt: this.updatedAt.valueOf() | ||||||
|  |     }) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     static createBulkFromOld(oldSeriesObjs) { |   static updateFromOld(oldSeries) { | ||||||
|       const series = oldSeriesObjs.map(this.getFromOld) |     const series = this.getFromOld(oldSeries) | ||||||
|       return this.bulkCreate(series) |     return this.update(series, { | ||||||
|     } |       where: { | ||||||
| 
 |         id: series.id | ||||||
|     static getFromOld(oldSeries) { |  | ||||||
|       return { |  | ||||||
|         id: oldSeries.id, |  | ||||||
|         name: oldSeries.name, |  | ||||||
|         nameIgnorePrefix: oldSeries.nameIgnorePrefix, |  | ||||||
|         description: oldSeries.description, |  | ||||||
|         libraryId: oldSeries.libraryId |  | ||||||
|       } |       } | ||||||
|     } |     }) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     static removeById(seriesId) { |   static createFromOld(oldSeries) { | ||||||
|       return this.destroy({ |     const series = this.getFromOld(oldSeries) | ||||||
|         where: { |     return this.create(series) | ||||||
|           id: seriesId |   } | ||||||
|         } | 
 | ||||||
|       }) |   static createBulkFromOld(oldSeriesObjs) { | ||||||
|  |     const series = oldSeriesObjs.map(this.getFromOld) | ||||||
|  |     return this.bulkCreate(series) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static getFromOld(oldSeries) { | ||||||
|  |     return { | ||||||
|  |       id: oldSeries.id, | ||||||
|  |       name: oldSeries.name, | ||||||
|  |       nameIgnorePrefix: oldSeries.nameIgnorePrefix, | ||||||
|  |       description: oldSeries.description, | ||||||
|  |       libraryId: oldSeries.libraryId | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Series.init({ |   static removeById(seriesId) { | ||||||
|     id: { |     return this.destroy({ | ||||||
|       type: DataTypes.UUID, |       where: { | ||||||
|       defaultValue: DataTypes.UUIDV4, |         id: seriesId | ||||||
|       primaryKey: true |       } | ||||||
|     }, |     }) | ||||||
|     name: DataTypes.STRING, |   } | ||||||
|     nameIgnorePrefix: DataTypes.STRING, |  | ||||||
|     description: DataTypes.TEXT |  | ||||||
|   }, { |  | ||||||
|     sequelize, |  | ||||||
|     modelName: 'series' |  | ||||||
|   }) |  | ||||||
| 
 | 
 | ||||||
|   const { library } = sequelize.models |   /** | ||||||
|   library.hasMany(Series, { |    * Initialize model | ||||||
|     onDelete: 'CASCADE' |    * @param {import('../Database').sequelize} sequelize  | ||||||
|   }) |    */ | ||||||
|   Series.belongsTo(library) |   static init(sequelize) { | ||||||
|  |     super.init({ | ||||||
|  |       id: { | ||||||
|  |         type: DataTypes.UUID, | ||||||
|  |         defaultValue: DataTypes.UUIDV4, | ||||||
|  |         primaryKey: true | ||||||
|  |       }, | ||||||
|  |       name: DataTypes.STRING, | ||||||
|  |       nameIgnorePrefix: DataTypes.STRING, | ||||||
|  |       description: DataTypes.TEXT | ||||||
|  |     }, { | ||||||
|  |       sequelize, | ||||||
|  |       modelName: 'series' | ||||||
|  |     }) | ||||||
| 
 | 
 | ||||||
|   return Series |     const { library } = sequelize.models | ||||||
| } |     library.hasMany(Series, { | ||||||
|  |       onDelete: 'CASCADE' | ||||||
|  |     }) | ||||||
|  |     Series.belongsTo(library) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = Series | ||||||
| @ -4,42 +4,59 @@ const oldEmailSettings = require('../objects/settings/EmailSettings') | |||||||
| const oldServerSettings = require('../objects/settings/ServerSettings') | const oldServerSettings = require('../objects/settings/ServerSettings') | ||||||
| const oldNotificationSettings = require('../objects/settings/NotificationSettings') | const oldNotificationSettings = require('../objects/settings/NotificationSettings') | ||||||
| 
 | 
 | ||||||
| module.exports = (sequelize) => { | class Setting extends Model { | ||||||
|   class Setting extends Model { |   constructor(values, options) { | ||||||
|     static async getOldSettings() { |     super(values, options) | ||||||
|       const settings = (await this.findAll()).map(se => se.value) | 
 | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.key | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.value | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.createdAt | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.updatedAt | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static async getOldSettings() { | ||||||
|  |     const settings = (await this.findAll()).map(se => se.value) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|       const emailSettingsJson = settings.find(se => se.id === 'email-settings') |     const emailSettingsJson = settings.find(se => se.id === 'email-settings') | ||||||
|       const serverSettingsJson = settings.find(se => se.id === 'server-settings') |     const serverSettingsJson = settings.find(se => se.id === 'server-settings') | ||||||
|       const notificationSettingsJson = settings.find(se => se.id === 'notification-settings') |     const notificationSettingsJson = settings.find(se => se.id === 'notification-settings') | ||||||
| 
 | 
 | ||||||
|       return { |     return { | ||||||
|         settings, |       settings, | ||||||
|         emailSettings: new oldEmailSettings(emailSettingsJson), |       emailSettings: new oldEmailSettings(emailSettingsJson), | ||||||
|         serverSettings: new oldServerSettings(serverSettingsJson), |       serverSettings: new oldServerSettings(serverSettingsJson), | ||||||
|         notificationSettings: new oldNotificationSettings(notificationSettingsJson) |       notificationSettings: new oldNotificationSettings(notificationSettingsJson) | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static updateSettingObj(setting) { |  | ||||||
|       return this.upsert({ |  | ||||||
|         key: setting.id, |  | ||||||
|         value: setting |  | ||||||
|       }) |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Setting.init({ |   static updateSettingObj(setting) { | ||||||
|     key: { |     return this.upsert({ | ||||||
|       type: DataTypes.STRING, |       key: setting.id, | ||||||
|       primaryKey: true |       value: setting | ||||||
|     }, |     }) | ||||||
|     value: DataTypes.JSON |   } | ||||||
|   }, { |  | ||||||
|     sequelize, |  | ||||||
|     modelName: 'setting' |  | ||||||
|   }) |  | ||||||
| 
 | 
 | ||||||
|   return Setting |   /** | ||||||
| } |    * Initialize model | ||||||
|  |    * @param {import('../Database').sequelize} sequelize  | ||||||
|  |    */ | ||||||
|  |   static init(sequelize) { | ||||||
|  |     super.init({ | ||||||
|  |       key: { | ||||||
|  |         type: DataTypes.STRING, | ||||||
|  |         primaryKey: true | ||||||
|  |       }, | ||||||
|  |       value: DataTypes.JSON | ||||||
|  |     }, { | ||||||
|  |       sequelize, | ||||||
|  |       modelName: 'setting' | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = Setting | ||||||
| @ -3,238 +3,273 @@ const { DataTypes, Model, Op } = require('sequelize') | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const oldUser = require('../objects/user/User') | const oldUser = require('../objects/user/User') | ||||||
| 
 | 
 | ||||||
| module.exports = (sequelize) => { | class User extends Model { | ||||||
|   class User extends Model { |   constructor(values, options) { | ||||||
|     /** |     super(values, options) | ||||||
|      * Get all oldUsers |  | ||||||
|      * @returns {Promise<oldUser>} |  | ||||||
|      */ |  | ||||||
|     static async getOldUsers() { |  | ||||||
|       const users = await this.findAll({ |  | ||||||
|         include: sequelize.models.mediaProgress |  | ||||||
|       }) |  | ||||||
|       return users.map(u => this.getOldUser(u)) |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     static getOldUser(userExpanded) { |     /** @type {UUIDV4} */ | ||||||
|       const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress()) |     this.id | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.username | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.email | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.pash | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.type | ||||||
|  |     /** @type {boolean} */ | ||||||
|  |     this.isActive | ||||||
|  |     /** @type {boolean} */ | ||||||
|  |     this.isLocked | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.lastSeen | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.permissions | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.bookmarks | ||||||
|  |     /** @type {Object} */ | ||||||
|  |     this.extraData | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.createdAt | ||||||
|  |     /** @type {Date} */ | ||||||
|  |     this.updatedAt | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|       const librariesAccessible = userExpanded.permissions?.librariesAccessible || [] |   /** | ||||||
|       const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || [] |    * Get all oldUsers | ||||||
|       const permissions = userExpanded.permissions || {} |    * @returns {Promise<oldUser>} | ||||||
|       delete permissions.librariesAccessible |    */ | ||||||
|       delete permissions.itemTagsSelected |   static async getOldUsers() { | ||||||
|  |     const users = await this.findAll({ | ||||||
|  |       include: this.sequelize.models.mediaProgress | ||||||
|  |     }) | ||||||
|  |     return users.map(u => this.getOldUser(u)) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|       return new oldUser({ |   static getOldUser(userExpanded) { | ||||||
|         id: userExpanded.id, |     const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress()) | ||||||
|         oldUserId: userExpanded.extraData?.oldUserId || null, |  | ||||||
|         username: userExpanded.username, |  | ||||||
|         pash: userExpanded.pash, |  | ||||||
|         type: userExpanded.type, |  | ||||||
|         token: userExpanded.token, |  | ||||||
|         mediaProgress, |  | ||||||
|         seriesHideFromContinueListening: userExpanded.extraData?.seriesHideFromContinueListening || [], |  | ||||||
|         bookmarks: userExpanded.bookmarks, |  | ||||||
|         isActive: userExpanded.isActive, |  | ||||||
|         isLocked: userExpanded.isLocked, |  | ||||||
|         lastSeen: userExpanded.lastSeen?.valueOf() || null, |  | ||||||
|         createdAt: userExpanded.createdAt.valueOf(), |  | ||||||
|         permissions, |  | ||||||
|         librariesAccessible, |  | ||||||
|         itemTagsSelected |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     static createFromOld(oldUser) { |     const librariesAccessible = userExpanded.permissions?.librariesAccessible || [] | ||||||
|       const user = this.getFromOld(oldUser) |     const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || [] | ||||||
|       return this.create(user) |     const permissions = userExpanded.permissions || {} | ||||||
|     } |     delete permissions.librariesAccessible | ||||||
|  |     delete permissions.itemTagsSelected | ||||||
| 
 | 
 | ||||||
|     static updateFromOld(oldUser) { |     return new oldUser({ | ||||||
|       const user = this.getFromOld(oldUser) |       id: userExpanded.id, | ||||||
|       return this.update(user, { |       oldUserId: userExpanded.extraData?.oldUserId || null, | ||||||
|         where: { |       username: userExpanded.username, | ||||||
|           id: user.id |       pash: userExpanded.pash, | ||||||
|         } |       type: userExpanded.type, | ||||||
|       }).then((result) => result[0] > 0).catch((error) => { |       token: userExpanded.token, | ||||||
|         Logger.error(`[User] Failed to save user ${oldUser.id}`, error) |       mediaProgress, | ||||||
|         return false |       seriesHideFromContinueListening: userExpanded.extraData?.seriesHideFromContinueListening || [], | ||||||
|       }) |       bookmarks: userExpanded.bookmarks, | ||||||
|     } |       isActive: userExpanded.isActive, | ||||||
|  |       isLocked: userExpanded.isLocked, | ||||||
|  |       lastSeen: userExpanded.lastSeen?.valueOf() || null, | ||||||
|  |       createdAt: userExpanded.createdAt.valueOf(), | ||||||
|  |       permissions, | ||||||
|  |       librariesAccessible, | ||||||
|  |       itemTagsSelected | ||||||
|  |     }) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     static getFromOld(oldUser) { |   static createFromOld(oldUser) { | ||||||
|       return { |     const user = this.getFromOld(oldUser) | ||||||
|         id: oldUser.id, |     return this.create(user) | ||||||
|         username: oldUser.username, |   } | ||||||
|         pash: oldUser.pash || null, | 
 | ||||||
|         type: oldUser.type || null, |   static updateFromOld(oldUser) { | ||||||
|         token: oldUser.token || null, |     const user = this.getFromOld(oldUser) | ||||||
|         isActive: !!oldUser.isActive, |     return this.update(user, { | ||||||
|         lastSeen: oldUser.lastSeen || null, |       where: { | ||||||
|         extraData: { |         id: user.id | ||||||
|           seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], |  | ||||||
|           oldUserId: oldUser.oldUserId |  | ||||||
|         }, |  | ||||||
|         createdAt: oldUser.createdAt || Date.now(), |  | ||||||
|         permissions: { |  | ||||||
|           ...oldUser.permissions, |  | ||||||
|           librariesAccessible: oldUser.librariesAccessible || [], |  | ||||||
|           itemTagsSelected: oldUser.itemTagsSelected || [] |  | ||||||
|         }, |  | ||||||
|         bookmarks: oldUser.bookmarks |  | ||||||
|       } |       } | ||||||
|     } |     }).then((result) => result[0] > 0).catch((error) => { | ||||||
|  |       Logger.error(`[User] Failed to save user ${oldUser.id}`, error) | ||||||
|  |       return false | ||||||
|  |     }) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     static removeById(userId) { |   static getFromOld(oldUser) { | ||||||
|       return this.destroy({ |     return { | ||||||
|         where: { |       id: oldUser.id, | ||||||
|           id: userId |       username: oldUser.username, | ||||||
|         } |       pash: oldUser.pash || null, | ||||||
|       }) |       type: oldUser.type || null, | ||||||
|     } |       token: oldUser.token || null, | ||||||
| 
 |       isActive: !!oldUser.isActive, | ||||||
|     /** |       lastSeen: oldUser.lastSeen || null, | ||||||
|      * Create root user |       extraData: { | ||||||
|      * @param {string} username  |         seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], | ||||||
|      * @param {string} pash  |         oldUserId: oldUser.oldUserId | ||||||
|      * @param {Auth} auth  |       }, | ||||||
|      * @returns {oldUser} |       createdAt: oldUser.createdAt || Date.now(), | ||||||
|      */ |       permissions: { | ||||||
|     static async createRootUser(username, pash, auth) { |         ...oldUser.permissions, | ||||||
|       const userId = uuidv4() |         librariesAccessible: oldUser.librariesAccessible || [], | ||||||
| 
 |         itemTagsSelected: oldUser.itemTagsSelected || [] | ||||||
|       const token = await auth.generateAccessToken({ userId, username }) |       }, | ||||||
| 
 |       bookmarks: oldUser.bookmarks | ||||||
|       const newRoot = new oldUser({ |  | ||||||
|         id: userId, |  | ||||||
|         type: 'root', |  | ||||||
|         username, |  | ||||||
|         pash, |  | ||||||
|         token, |  | ||||||
|         isActive: true, |  | ||||||
|         createdAt: Date.now() |  | ||||||
|       }) |  | ||||||
|       await this.createFromOld(newRoot) |  | ||||||
|       return newRoot |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get a user by id or by the old database id |  | ||||||
|      * @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id |  | ||||||
|      * @param {string} userId  |  | ||||||
|      * @returns {Promise<oldUser|null>} null if not found |  | ||||||
|      */ |  | ||||||
|     static async getUserByIdOrOldId(userId) { |  | ||||||
|       if (!userId) return null |  | ||||||
|       const user = await this.findOne({ |  | ||||||
|         where: { |  | ||||||
|           [Op.or]: [ |  | ||||||
|             { |  | ||||||
|               id: userId |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               extraData: { |  | ||||||
|                 [Op.substring]: userId |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|         }, |  | ||||||
|         include: sequelize.models.mediaProgress |  | ||||||
|       }) |  | ||||||
|       if (!user) return null |  | ||||||
|       return this.getOldUser(user) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get user by username case insensitive |  | ||||||
|      * @param {string} username  |  | ||||||
|      * @returns {Promise<oldUser|null>} returns null if not found |  | ||||||
|      */ |  | ||||||
|     static async getUserByUsername(username) { |  | ||||||
|       if (!username) return null |  | ||||||
|       const user = await this.findOne({ |  | ||||||
|         where: { |  | ||||||
|           username: { |  | ||||||
|             [Op.like]: username |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         include: sequelize.models.mediaProgress |  | ||||||
|       }) |  | ||||||
|       if (!user) return null |  | ||||||
|       return this.getOldUser(user) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get user by id |  | ||||||
|      * @param {string} userId  |  | ||||||
|      * @returns {Promise<oldUser|null>} returns null if not found |  | ||||||
|      */ |  | ||||||
|     static async getUserById(userId) { |  | ||||||
|       if (!userId) return null |  | ||||||
|       const user = await this.findByPk(userId, { |  | ||||||
|         include: sequelize.models.mediaProgress |  | ||||||
|       }) |  | ||||||
|       if (!user) return null |  | ||||||
|       return this.getOldUser(user) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get array of user id and username |  | ||||||
|      * @returns {object[]} { id, username } |  | ||||||
|      */ |  | ||||||
|     static async getMinifiedUserObjects() { |  | ||||||
|       const users = await this.findAll({ |  | ||||||
|         attributes: ['id', 'username'] |  | ||||||
|       }) |  | ||||||
|       return users.map(u => { |  | ||||||
|         return { |  | ||||||
|           id: u.id, |  | ||||||
|           username: u.username |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Return true if root user exists |  | ||||||
|      * @returns {boolean} |  | ||||||
|      */ |  | ||||||
|     static async getHasRootUser() { |  | ||||||
|       const count = await this.count({ |  | ||||||
|         where: { |  | ||||||
|           type: 'root' |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|       return count > 0 |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   User.init({ |   static removeById(userId) { | ||||||
|     id: { |     return this.destroy({ | ||||||
|       type: DataTypes.UUID, |       where: { | ||||||
|       defaultValue: DataTypes.UUIDV4, |         id: userId | ||||||
|       primaryKey: true |       } | ||||||
|     }, |     }) | ||||||
|     username: DataTypes.STRING, |   } | ||||||
|     email: DataTypes.STRING, |  | ||||||
|     pash: DataTypes.STRING, |  | ||||||
|     type: DataTypes.STRING, |  | ||||||
|     token: DataTypes.STRING, |  | ||||||
|     isActive: { |  | ||||||
|       type: DataTypes.BOOLEAN, |  | ||||||
|       defaultValue: false |  | ||||||
|     }, |  | ||||||
|     isLocked: { |  | ||||||
|       type: DataTypes.BOOLEAN, |  | ||||||
|       defaultValue: false |  | ||||||
|     }, |  | ||||||
|     lastSeen: DataTypes.DATE, |  | ||||||
|     permissions: DataTypes.JSON, |  | ||||||
|     bookmarks: DataTypes.JSON, |  | ||||||
|     extraData: DataTypes.JSON |  | ||||||
|   }, { |  | ||||||
|     sequelize, |  | ||||||
|     modelName: 'user' |  | ||||||
|   }) |  | ||||||
| 
 | 
 | ||||||
|   return User |   /** | ||||||
| } |    * Create root user | ||||||
|  |    * @param {string} username  | ||||||
|  |    * @param {string} pash  | ||||||
|  |    * @param {Auth} auth  | ||||||
|  |    * @returns {oldUser} | ||||||
|  |    */ | ||||||
|  |   static async createRootUser(username, pash, auth) { | ||||||
|  |     const userId = uuidv4() | ||||||
|  | 
 | ||||||
|  |     const token = await auth.generateAccessToken({ userId, username }) | ||||||
|  | 
 | ||||||
|  |     const newRoot = new oldUser({ | ||||||
|  |       id: userId, | ||||||
|  |       type: 'root', | ||||||
|  |       username, | ||||||
|  |       pash, | ||||||
|  |       token, | ||||||
|  |       isActive: true, | ||||||
|  |       createdAt: Date.now() | ||||||
|  |     }) | ||||||
|  |     await this.createFromOld(newRoot) | ||||||
|  |     return newRoot | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get a user by id or by the old database id | ||||||
|  |    * @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id | ||||||
|  |    * @param {string} userId  | ||||||
|  |    * @returns {Promise<oldUser|null>} null if not found | ||||||
|  |    */ | ||||||
|  |   static async getUserByIdOrOldId(userId) { | ||||||
|  |     if (!userId) return null | ||||||
|  |     const user = await this.findOne({ | ||||||
|  |       where: { | ||||||
|  |         [Op.or]: [ | ||||||
|  |           { | ||||||
|  |             id: userId | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             extraData: { | ||||||
|  |               [Op.substring]: userId | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       include: this.sequelize.models.mediaProgress | ||||||
|  |     }) | ||||||
|  |     if (!user) return null | ||||||
|  |     return this.getOldUser(user) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get user by username case insensitive | ||||||
|  |    * @param {string} username  | ||||||
|  |    * @returns {Promise<oldUser|null>} returns null if not found | ||||||
|  |    */ | ||||||
|  |   static async getUserByUsername(username) { | ||||||
|  |     if (!username) return null | ||||||
|  |     const user = await this.findOne({ | ||||||
|  |       where: { | ||||||
|  |         username: { | ||||||
|  |           [Op.like]: username | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       include: this.sequelize.models.mediaProgress | ||||||
|  |     }) | ||||||
|  |     if (!user) return null | ||||||
|  |     return this.getOldUser(user) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get user by id | ||||||
|  |    * @param {string} userId  | ||||||
|  |    * @returns {Promise<oldUser|null>} returns null if not found | ||||||
|  |    */ | ||||||
|  |   static async getUserById(userId) { | ||||||
|  |     if (!userId) return null | ||||||
|  |     const user = await this.findByPk(userId, { | ||||||
|  |       include: this.sequelize.models.mediaProgress | ||||||
|  |     }) | ||||||
|  |     if (!user) return null | ||||||
|  |     return this.getOldUser(user) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get array of user id and username | ||||||
|  |    * @returns {object[]} { id, username } | ||||||
|  |    */ | ||||||
|  |   static async getMinifiedUserObjects() { | ||||||
|  |     const users = await this.findAll({ | ||||||
|  |       attributes: ['id', 'username'] | ||||||
|  |     }) | ||||||
|  |     return users.map(u => { | ||||||
|  |       return { | ||||||
|  |         id: u.id, | ||||||
|  |         username: u.username | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Return true if root user exists | ||||||
|  |    * @returns {boolean} | ||||||
|  |    */ | ||||||
|  |   static async getHasRootUser() { | ||||||
|  |     const count = await this.count({ | ||||||
|  |       where: { | ||||||
|  |         type: 'root' | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     return count > 0 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Initialize model | ||||||
|  |    * @param {import('../Database').sequelize} sequelize  | ||||||
|  |    */ | ||||||
|  |   static init(sequelize) { | ||||||
|  |     super.init({ | ||||||
|  |       id: { | ||||||
|  |         type: DataTypes.UUID, | ||||||
|  |         defaultValue: DataTypes.UUIDV4, | ||||||
|  |         primaryKey: true | ||||||
|  |       }, | ||||||
|  |       username: DataTypes.STRING, | ||||||
|  |       email: DataTypes.STRING, | ||||||
|  |       pash: DataTypes.STRING, | ||||||
|  |       type: DataTypes.STRING, | ||||||
|  |       token: DataTypes.STRING, | ||||||
|  |       isActive: { | ||||||
|  |         type: DataTypes.BOOLEAN, | ||||||
|  |         defaultValue: false | ||||||
|  |       }, | ||||||
|  |       isLocked: { | ||||||
|  |         type: DataTypes.BOOLEAN, | ||||||
|  |         defaultValue: false | ||||||
|  |       }, | ||||||
|  |       lastSeen: DataTypes.DATE, | ||||||
|  |       permissions: DataTypes.JSON, | ||||||
|  |       bookmarks: DataTypes.JSON, | ||||||
|  |       extraData: DataTypes.JSON | ||||||
|  |     }, { | ||||||
|  |       sequelize, | ||||||
|  |       modelName: 'user' | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = User | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user