2025-01-01 00:01:42 +01:00
|
|
|
const { DataTypes, Model, Op } = require('sequelize')
|
2023-07-14 21:50:37 +02:00
|
|
|
const Logger = require('../Logger')
|
2025-01-03 19:06:20 +01:00
|
|
|
const SocketAuthority = require('../SocketAuthority')
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
class Playlist extends Model {
|
|
|
|
constructor(values, options) {
|
|
|
|
super(values, options)
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
/** @type {UUIDV4} */
|
|
|
|
this.id
|
|
|
|
/** @type {string} */
|
|
|
|
this.name
|
|
|
|
/** @type {string} */
|
|
|
|
this.description
|
|
|
|
/** @type {UUIDV4} */
|
|
|
|
this.libraryId
|
|
|
|
/** @type {UUIDV4} */
|
|
|
|
this.userId
|
|
|
|
/** @type {Date} */
|
|
|
|
this.createdAt
|
|
|
|
/** @type {Date} */
|
|
|
|
this.updatedAt
|
|
|
|
|
2025-01-01 00:01:42 +01:00
|
|
|
// Expanded properties
|
2023-07-23 16:42:57 +02:00
|
|
|
|
2025-01-01 00:01:42 +01:00
|
|
|
/** @type {import('./PlaylistMediaItem')[]} - only set when expanded */
|
|
|
|
this.playlistMediaItems
|
2023-08-16 23:38:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-01-01 00:01:42 +01:00
|
|
|
* Get old playlists for user and library
|
2024-05-27 22:37:02 +02:00
|
|
|
*
|
|
|
|
* @param {string} userId
|
2025-01-01 00:01:42 +01:00
|
|
|
* @param {string} libraryId
|
|
|
|
* @async
|
2023-08-16 23:38:48 +02:00
|
|
|
*/
|
2025-01-01 00:01:42 +01:00
|
|
|
static async getOldPlaylistsForUserAndLibrary(userId, libraryId) {
|
2023-08-16 23:38:48 +02:00
|
|
|
if (!userId && !libraryId) return []
|
2025-01-01 00:01:42 +01:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
const whereQuery = {}
|
|
|
|
if (userId) {
|
|
|
|
whereQuery.userId = userId
|
2023-07-23 16:42:57 +02:00
|
|
|
}
|
2023-08-16 23:38:48 +02:00
|
|
|
if (libraryId) {
|
|
|
|
whereQuery.libraryId = libraryId
|
|
|
|
}
|
2024-05-27 22:37:02 +02:00
|
|
|
const playlistsExpanded = await this.findAll({
|
2023-08-16 23:38:48 +02:00
|
|
|
where: whereQuery,
|
|
|
|
include: {
|
|
|
|
model: this.sequelize.models.playlistMediaItem,
|
2023-07-23 16:42:57 +02:00
|
|
|
include: [
|
|
|
|
{
|
2023-08-16 23:38:48 +02:00
|
|
|
model: this.sequelize.models.book,
|
2025-01-01 00:01:42 +01:00
|
|
|
include: [
|
|
|
|
{
|
|
|
|
model: this.sequelize.models.libraryItem
|
|
|
|
},
|
|
|
|
{
|
|
|
|
model: this.sequelize.models.author,
|
|
|
|
through: {
|
|
|
|
attributes: []
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
model: this.sequelize.models.series,
|
|
|
|
through: {
|
|
|
|
attributes: ['sequence']
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
2023-08-16 23:38:48 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
model: this.sequelize.models.podcastEpisode,
|
2023-07-23 16:42:57 +02:00
|
|
|
include: {
|
2023-08-16 23:38:48 +02:00
|
|
|
model: this.sequelize.models.podcast,
|
|
|
|
include: this.sequelize.models.libraryItem
|
2023-07-23 16:42:57 +02:00
|
|
|
}
|
|
|
|
}
|
2023-08-16 23:38:48 +02:00
|
|
|
]
|
|
|
|
},
|
2025-01-01 00:01:42 +01:00
|
|
|
order: [['playlistMediaItems', 'order', 'ASC']]
|
2023-08-16 23:38:48 +02:00
|
|
|
})
|
2024-05-27 22:37:02 +02:00
|
|
|
|
2025-01-01 00:01:42 +01:00
|
|
|
// Sort by name asc
|
|
|
|
playlistsExpanded.sort((a, b) => a.name.localeCompare(b.name))
|
2024-05-27 22:37:02 +02:00
|
|
|
|
2025-01-01 00:01:42 +01:00
|
|
|
return playlistsExpanded.map((playlist) => playlist.toOldJSONExpanded())
|
2023-08-16 23:38:48 +02:00
|
|
|
}
|
2023-08-13 18:22:38 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
/**
|
|
|
|
* Get number of playlists for a user and library
|
2024-05-27 22:37:02 +02:00
|
|
|
* @param {string} userId
|
|
|
|
* @param {string} libraryId
|
|
|
|
* @returns
|
2023-08-16 23:38:48 +02:00
|
|
|
*/
|
|
|
|
static async getNumPlaylistsForUserAndLibrary(userId, libraryId) {
|
|
|
|
return this.count({
|
|
|
|
where: {
|
|
|
|
userId,
|
|
|
|
libraryId
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2023-08-13 18:22:38 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
/**
|
|
|
|
* Get all playlists for mediaItemIds
|
2024-05-27 22:37:02 +02:00
|
|
|
* @param {string[]} mediaItemIds
|
2023-08-16 23:38:48 +02:00
|
|
|
* @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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
2023-07-23 16:42:57 +02:00
|
|
|
}
|
2023-08-16 23:38:48 +02:00
|
|
|
}
|
|
|
|
],
|
|
|
|
order: [['playlist', 'playlistMediaItems', 'order', 'ASC']]
|
|
|
|
})
|
|
|
|
|
|
|
|
const playlists = []
|
|
|
|
for (const playlistMediaItem of playlistMediaItemsExpanded) {
|
|
|
|
const playlist = playlistMediaItem.playlist
|
2024-05-27 22:37:02 +02:00
|
|
|
if (playlists.some((p) => p.id === playlist.id)) continue
|
2023-08-16 23:38:48 +02:00
|
|
|
|
2024-05-27 22:37:02 +02:00
|
|
|
playlist.playlistMediaItems = playlist.playlistMediaItems.map((pmi) => {
|
2023-08-16 23:38:48 +02:00
|
|
|
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)
|
2023-07-23 16:42:57 +02:00
|
|
|
}
|
2023-08-16 23:38:48 +02:00
|
|
|
return playlists
|
2023-07-05 01:14:44 +02:00
|
|
|
}
|
|
|
|
|
2025-01-03 19:06:20 +01:00
|
|
|
/**
|
|
|
|
* Removes media items and re-orders playlists
|
|
|
|
*
|
|
|
|
* @param {string[]} mediaItemIds
|
|
|
|
*/
|
|
|
|
static async removeMediaItemsFromPlaylists(mediaItemIds) {
|
|
|
|
if (!mediaItemIds?.length) return
|
|
|
|
|
|
|
|
const playlistsWithItem = await this.getPlaylistsForMediaItemIds(mediaItemIds)
|
|
|
|
|
|
|
|
if (!playlistsWithItem.length) return
|
|
|
|
|
|
|
|
for (const playlist of playlistsWithItem) {
|
|
|
|
let numMediaItems = playlist.playlistMediaItems.length
|
|
|
|
|
|
|
|
let order = 1
|
|
|
|
// Remove items in playlist and re-order
|
|
|
|
for (const playlistMediaItem of playlist.playlistMediaItems) {
|
|
|
|
if (mediaItemIds.includes(playlistMediaItem.mediaItemId)) {
|
|
|
|
await playlistMediaItem.destroy()
|
|
|
|
numMediaItems--
|
|
|
|
} else {
|
|
|
|
if (playlistMediaItem.order !== order) {
|
|
|
|
playlistMediaItem.update({
|
|
|
|
order
|
|
|
|
})
|
|
|
|
}
|
|
|
|
order++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If playlist is now empty then remove it
|
|
|
|
const jsonExpanded = await playlist.getOldJsonExpanded()
|
|
|
|
if (!numMediaItems) {
|
|
|
|
Logger.info(`[ApiRouter] Playlist "${playlist.name}" has no more items - removing it`)
|
|
|
|
await playlist.destroy()
|
|
|
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
|
|
|
} else {
|
|
|
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
/**
|
|
|
|
* Initialize model
|
2024-05-27 22:37:02 +02:00
|
|
|
* @param {import('../Database').sequelize} sequelize
|
2023-08-16 23:38:48 +02:00
|
|
|
*/
|
|
|
|
static init(sequelize) {
|
2024-05-27 22:37:02 +02:00
|
|
|
super.init(
|
|
|
|
{
|
|
|
|
id: {
|
|
|
|
type: DataTypes.UUID,
|
|
|
|
defaultValue: DataTypes.UUIDV4,
|
|
|
|
primaryKey: true
|
|
|
|
},
|
|
|
|
name: DataTypes.STRING,
|
|
|
|
description: DataTypes.TEXT
|
2023-08-16 23:38:48 +02:00
|
|
|
},
|
2024-05-27 22:37:02 +02:00
|
|
|
{
|
|
|
|
sequelize,
|
|
|
|
modelName: 'playlist'
|
|
|
|
}
|
|
|
|
)
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
const { library, user } = sequelize.models
|
|
|
|
library.hasMany(Playlist)
|
|
|
|
Playlist.belongsTo(library)
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
user.hasMany(Playlist, {
|
|
|
|
onDelete: 'CASCADE'
|
|
|
|
})
|
|
|
|
Playlist.belongsTo(user)
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2024-05-27 22:37:02 +02:00
|
|
|
Playlist.addHook('afterFind', (findResult) => {
|
2023-08-16 23:38:48 +02:00
|
|
|
if (!findResult) return
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 23:38:48 +02:00
|
|
|
for (const instance of findResult) {
|
|
|
|
if (instance.playlistMediaItems?.length) {
|
2024-05-27 22:37:02 +02:00
|
|
|
instance.playlistMediaItems = instance.playlistMediaItems.map((pmi) => {
|
2023-08-16 23:38:48 +02:00
|
|
|
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
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2025-01-01 00:01:42 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all media items in playlist expanded with library item
|
|
|
|
*
|
|
|
|
* @returns {Promise<import('./PlaylistMediaItem')[]>}
|
|
|
|
*/
|
|
|
|
getMediaItemsExpandedWithLibraryItem() {
|
|
|
|
return this.getPlaylistMediaItems({
|
|
|
|
include: [
|
|
|
|
{
|
|
|
|
model: this.sequelize.models.book,
|
|
|
|
include: [
|
|
|
|
{
|
|
|
|
model: this.sequelize.models.libraryItem
|
|
|
|
},
|
|
|
|
{
|
|
|
|
model: this.sequelize.models.author,
|
|
|
|
through: {
|
|
|
|
attributes: []
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
model: this.sequelize.models.series,
|
|
|
|
through: {
|
|
|
|
attributes: ['sequence']
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
model: this.sequelize.models.podcastEpisode,
|
|
|
|
include: [
|
|
|
|
{
|
|
|
|
model: this.sequelize.models.podcast,
|
|
|
|
include: this.sequelize.models.libraryItem
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
],
|
|
|
|
order: [['order', 'ASC']]
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get playlists toOldJSONExpanded
|
|
|
|
*
|
|
|
|
* @async
|
|
|
|
*/
|
|
|
|
async getOldJsonExpanded() {
|
|
|
|
this.playlistMediaItems = await this.getMediaItemsExpandedWithLibraryItem()
|
|
|
|
return this.toOldJSONExpanded()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Old model used libraryItemId instead of bookId
|
|
|
|
*
|
|
|
|
* @param {string} libraryItemId
|
|
|
|
* @param {string} [episodeId]
|
|
|
|
*/
|
|
|
|
checkHasMediaItem(libraryItemId, episodeId) {
|
|
|
|
if (!this.playlistMediaItems) {
|
|
|
|
throw new Error('playlistMediaItems are required to check Playlist')
|
|
|
|
}
|
|
|
|
if (episodeId) {
|
|
|
|
return this.playlistMediaItems.some((pmi) => pmi.mediaItemId === episodeId)
|
|
|
|
}
|
|
|
|
return this.playlistMediaItems.some((pmi) => pmi.mediaItem.libraryItem.id === libraryItemId)
|
|
|
|
}
|
|
|
|
|
|
|
|
toOldJSON() {
|
|
|
|
return {
|
|
|
|
id: this.id,
|
|
|
|
name: this.name,
|
|
|
|
libraryId: this.libraryId,
|
|
|
|
userId: this.userId,
|
|
|
|
description: this.description,
|
|
|
|
lastUpdate: this.updatedAt.valueOf(),
|
|
|
|
createdAt: this.createdAt.valueOf()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toOldJSONExpanded() {
|
|
|
|
if (!this.playlistMediaItems) {
|
|
|
|
throw new Error('playlistMediaItems are required to expand Playlist')
|
|
|
|
}
|
|
|
|
|
|
|
|
const json = this.toOldJSON()
|
|
|
|
json.items = this.playlistMediaItems.map((pmi) => {
|
|
|
|
if (pmi.mediaItemType === 'book') {
|
|
|
|
const libraryItem = pmi.mediaItem.libraryItem
|
|
|
|
delete pmi.mediaItem.libraryItem
|
|
|
|
libraryItem.media = pmi.mediaItem
|
|
|
|
return {
|
|
|
|
libraryItemId: libraryItem.id,
|
2025-01-04 22:20:41 +01:00
|
|
|
libraryItem: libraryItem.toOldJSONExpanded()
|
2025-01-01 00:01:42 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const libraryItem = pmi.mediaItem.podcast.libraryItem
|
|
|
|
delete pmi.mediaItem.podcast.libraryItem
|
|
|
|
libraryItem.media = pmi.mediaItem.podcast
|
|
|
|
return {
|
|
|
|
episodeId: pmi.mediaItemId,
|
|
|
|
episode: pmi.mediaItem.toOldJSONExpanded(libraryItem.id),
|
|
|
|
libraryItemId: libraryItem.id,
|
2025-01-04 22:20:41 +01:00
|
|
|
libraryItem: libraryItem.toOldJSONMinified()
|
2025-01-01 00:01:42 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return json
|
|
|
|
}
|
2023-08-16 23:38:48 +02:00
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2024-05-27 22:37:02 +02:00
|
|
|
module.exports = Playlist
|