2023-07-05 01:14:44 +02:00
|
|
|
const { DataTypes, Model } = require('sequelize')
|
|
|
|
const oldFeed = require('../objects/Feed')
|
2023-07-06 01:18:37 +02:00
|
|
|
const areEquivalent = require('../utils/areEquivalent')
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
class Feed extends Model {
|
|
|
|
constructor(values, options) {
|
|
|
|
super(values, options)
|
|
|
|
|
|
|
|
/** @type {UUIDV4} */
|
|
|
|
this.id
|
|
|
|
/** @type {string} */
|
|
|
|
this.slug
|
|
|
|
/** @type {string} */
|
|
|
|
this.entityType
|
|
|
|
/** @type {UUIDV4} */
|
|
|
|
this.entityId
|
|
|
|
/** @type {Date} */
|
|
|
|
this.entityUpdatedAt
|
|
|
|
/** @type {string} */
|
|
|
|
this.serverAddress
|
|
|
|
/** @type {string} */
|
|
|
|
this.feedURL
|
|
|
|
/** @type {string} */
|
|
|
|
this.imageURL
|
|
|
|
/** @type {string} */
|
|
|
|
this.siteURL
|
|
|
|
/** @type {string} */
|
|
|
|
this.title
|
|
|
|
/** @type {string} */
|
|
|
|
this.description
|
|
|
|
/** @type {string} */
|
|
|
|
this.author
|
|
|
|
/** @type {string} */
|
|
|
|
this.podcastType
|
|
|
|
/** @type {string} */
|
|
|
|
this.language
|
|
|
|
/** @type {string} */
|
|
|
|
this.ownerName
|
|
|
|
/** @type {string} */
|
|
|
|
this.ownerEmail
|
|
|
|
/** @type {boolean} */
|
|
|
|
this.explicit
|
|
|
|
/** @type {boolean} */
|
|
|
|
this.preventIndexing
|
|
|
|
/** @type {string} */
|
|
|
|
this.coverPath
|
|
|
|
/** @type {UUIDV4} */
|
|
|
|
this.userId
|
|
|
|
/** @type {Date} */
|
|
|
|
this.createdAt
|
|
|
|
/** @type {Date} */
|
|
|
|
this.updatedAt
|
|
|
|
}
|
|
|
|
|
|
|
|
static async getOldFeeds() {
|
|
|
|
const feeds = await this.findAll({
|
|
|
|
include: {
|
|
|
|
model: this.sequelize.models.feedEpisode
|
|
|
|
}
|
|
|
|
})
|
2024-05-29 00:24:02 +02:00
|
|
|
return feeds.map((f) => this.getOldFeed(f))
|
2023-08-16 01:03:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get old feed from Feed and optionally Feed with FeedEpisodes
|
|
|
|
* @param {Feed} feedExpanded
|
|
|
|
* @returns {oldFeed}
|
|
|
|
*/
|
|
|
|
static getOldFeed(feedExpanded) {
|
|
|
|
const episodes = feedExpanded.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode())
|
|
|
|
return new oldFeed({
|
|
|
|
id: feedExpanded.id,
|
|
|
|
slug: feedExpanded.slug,
|
|
|
|
userId: feedExpanded.userId,
|
|
|
|
entityType: feedExpanded.entityType,
|
|
|
|
entityId: feedExpanded.entityId,
|
|
|
|
entityUpdatedAt: feedExpanded.entityUpdatedAt?.valueOf() || null,
|
|
|
|
coverPath: feedExpanded.coverPath || null,
|
|
|
|
meta: {
|
|
|
|
title: feedExpanded.title,
|
|
|
|
description: feedExpanded.description,
|
|
|
|
author: feedExpanded.author,
|
|
|
|
imageUrl: feedExpanded.imageURL,
|
2023-07-05 01:14:44 +02:00
|
|
|
feedUrl: feedExpanded.feedURL,
|
2023-08-16 01:03:43 +02:00
|
|
|
link: feedExpanded.siteURL,
|
|
|
|
explicit: feedExpanded.explicit,
|
|
|
|
type: feedExpanded.podcastType,
|
|
|
|
language: feedExpanded.language,
|
|
|
|
preventIndexing: feedExpanded.preventIndexing,
|
|
|
|
ownerName: feedExpanded.ownerName,
|
|
|
|
ownerEmail: feedExpanded.ownerEmail
|
|
|
|
},
|
|
|
|
serverAddress: feedExpanded.serverAddress,
|
|
|
|
feedUrl: feedExpanded.feedURL,
|
|
|
|
episodes: episodes || [],
|
|
|
|
createdAt: feedExpanded.createdAt.valueOf(),
|
|
|
|
updatedAt: feedExpanded.updatedAt.valueOf()
|
|
|
|
})
|
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
static removeById(feedId) {
|
|
|
|
return this.destroy({
|
|
|
|
where: {
|
|
|
|
id: feedId
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
/**
|
|
|
|
* Find all library item ids that have an open feed (used in library filter)
|
2023-12-14 22:45:34 +01:00
|
|
|
* @returns {Promise<string[]>} array of library item ids
|
2023-08-16 01:03:43 +02:00
|
|
|
*/
|
|
|
|
static async findAllLibraryItemIds() {
|
|
|
|
const feeds = await this.findAll({
|
|
|
|
attributes: ['entityId'],
|
|
|
|
where: {
|
|
|
|
entityType: 'libraryItem'
|
|
|
|
}
|
|
|
|
})
|
2024-05-29 00:24:02 +02:00
|
|
|
return feeds.map((f) => f.entityId).filter((f) => f) || []
|
2023-08-16 01:03:43 +02:00
|
|
|
}
|
2023-07-17 23:48:46 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
/**
|
|
|
|
* Find feed where and return oldFeed
|
2023-12-14 22:45:34 +01:00
|
|
|
* @param {Object} where sequelize where object
|
|
|
|
* @returns {Promise<oldFeed>} oldFeed
|
2023-08-16 01:03:43 +02:00
|
|
|
*/
|
|
|
|
static async findOneOld(where) {
|
|
|
|
if (!where) return null
|
|
|
|
const feedExpanded = await this.findOne({
|
|
|
|
where,
|
|
|
|
include: {
|
|
|
|
model: this.sequelize.models.feedEpisode
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if (!feedExpanded) return null
|
|
|
|
return this.getOldFeed(feedExpanded)
|
|
|
|
}
|
2023-07-17 23:48:46 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
/**
|
|
|
|
* Find feed and return oldFeed
|
|
|
|
* @param {string} id
|
2023-12-14 22:45:34 +01:00
|
|
|
* @returns {Promise<oldFeed>} oldFeed
|
2023-08-16 01:03:43 +02:00
|
|
|
*/
|
|
|
|
static async findByPkOld(id) {
|
|
|
|
if (!id) return null
|
|
|
|
const feedExpanded = await this.findByPk(id, {
|
|
|
|
include: {
|
|
|
|
model: this.sequelize.models.feedEpisode
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if (!feedExpanded) return null
|
|
|
|
return this.getOldFeed(feedExpanded)
|
|
|
|
}
|
2023-07-17 23:48:46 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
static async fullCreateFromOld(oldFeed) {
|
|
|
|
const feedObj = this.getFromOld(oldFeed)
|
|
|
|
const newFeed = await this.create(feedObj)
|
2023-07-06 01:18:37 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
if (oldFeed.episodes?.length) {
|
|
|
|
for (const oldFeedEpisode of oldFeed.episodes) {
|
|
|
|
const feedEpisode = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode)
|
|
|
|
feedEpisode.feedId = newFeed.id
|
|
|
|
await this.sequelize.models.feedEpisode.create(feedEpisode)
|
2023-07-06 01:18:37 +02:00
|
|
|
}
|
|
|
|
}
|
2023-08-16 01:03:43 +02:00
|
|
|
}
|
2023-07-06 01:18:37 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
static async fullUpdateFromOld(oldFeed) {
|
|
|
|
const oldFeedEpisodes = oldFeed.episodes || []
|
|
|
|
const feedObj = this.getFromOld(oldFeed)
|
2023-07-06 01:18:37 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
const existingFeed = await this.findByPk(feedObj.id, {
|
|
|
|
include: this.sequelize.models.feedEpisode
|
|
|
|
})
|
|
|
|
if (!existingFeed) return false
|
2023-07-06 01:18:37 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
let hasUpdates = false
|
2023-09-23 21:27:13 +02:00
|
|
|
|
|
|
|
// Remove and update existing feed episodes
|
2023-08-16 01:03:43 +02:00
|
|
|
for (const feedEpisode of existingFeed.feedEpisodes) {
|
2024-05-29 00:24:02 +02:00
|
|
|
const oldFeedEpisode = oldFeedEpisodes.find((ep) => ep.id === feedEpisode.id)
|
2023-08-16 01:03:43 +02:00
|
|
|
// Episode removed
|
|
|
|
if (!oldFeedEpisode) {
|
|
|
|
feedEpisode.destroy()
|
|
|
|
} else {
|
|
|
|
let episodeHasUpdates = false
|
|
|
|
const oldFeedEpisodeCleaned = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode)
|
|
|
|
for (const key in oldFeedEpisodeCleaned) {
|
|
|
|
if (!areEquivalent(oldFeedEpisodeCleaned[key], feedEpisode[key])) {
|
|
|
|
episodeHasUpdates = true
|
2023-07-06 01:18:37 +02:00
|
|
|
}
|
|
|
|
}
|
2023-08-16 01:03:43 +02:00
|
|
|
if (episodeHasUpdates) {
|
|
|
|
await feedEpisode.update(oldFeedEpisodeCleaned)
|
|
|
|
hasUpdates = true
|
2023-07-06 01:18:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-23 21:27:13 +02:00
|
|
|
// Add new feed episodes
|
|
|
|
for (const episode of oldFeedEpisodes) {
|
2024-05-29 00:24:02 +02:00
|
|
|
if (!existingFeed.feedEpisodes.some((fe) => fe.id === episode.id)) {
|
2023-09-23 21:27:13 +02:00
|
|
|
await this.sequelize.models.feedEpisode.createFromOld(feedObj.id, episode)
|
|
|
|
hasUpdates = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
let feedHasUpdates = false
|
|
|
|
for (const key in feedObj) {
|
|
|
|
let existingValue = existingFeed[key]
|
|
|
|
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
|
|
|
|
|
|
|
if (!areEquivalent(existingValue, feedObj[key])) {
|
|
|
|
feedHasUpdates = true
|
2023-07-06 01:18:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
if (feedHasUpdates) {
|
|
|
|
await existingFeed.update(feedObj)
|
|
|
|
hasUpdates = true
|
2023-07-05 01:14:44 +02:00
|
|
|
}
|
2023-08-16 01:03:43 +02:00
|
|
|
|
|
|
|
return hasUpdates
|
2023-07-05 01:14:44 +02:00
|
|
|
}
|
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
static getFromOld(oldFeed) {
|
|
|
|
const oldFeedMeta = oldFeed.meta || {}
|
|
|
|
return {
|
|
|
|
id: oldFeed.id,
|
|
|
|
slug: oldFeed.slug,
|
|
|
|
entityType: oldFeed.entityType,
|
|
|
|
entityId: oldFeed.entityId,
|
|
|
|
entityUpdatedAt: oldFeed.entityUpdatedAt,
|
|
|
|
serverAddress: oldFeed.serverAddress,
|
|
|
|
feedURL: oldFeed.feedUrl,
|
|
|
|
coverPath: oldFeed.coverPath || null,
|
|
|
|
imageURL: oldFeedMeta.imageUrl,
|
|
|
|
siteURL: oldFeedMeta.link,
|
|
|
|
title: oldFeedMeta.title,
|
|
|
|
description: oldFeedMeta.description,
|
|
|
|
author: oldFeedMeta.author,
|
|
|
|
podcastType: oldFeedMeta.type || null,
|
|
|
|
language: oldFeedMeta.language || null,
|
|
|
|
ownerName: oldFeedMeta.ownerName || null,
|
|
|
|
ownerEmail: oldFeedMeta.ownerEmail || null,
|
|
|
|
explicit: !!oldFeedMeta.explicit,
|
|
|
|
preventIndexing: !!oldFeedMeta.preventIndexing,
|
|
|
|
userId: oldFeed.userId
|
|
|
|
}
|
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
getEntity(options) {
|
|
|
|
if (!this.entityType) return Promise.resolve(null)
|
|
|
|
const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.entityType)}`
|
|
|
|
return this[mixinMethodName](options)
|
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
/**
|
|
|
|
* Initialize model
|
2024-05-29 00:24:02 +02:00
|
|
|
*
|
2023-08-16 01:03:43 +02:00
|
|
|
* Polymorphic association: Feeds can be created from LibraryItem, Collection, Playlist or Series
|
|
|
|
* @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
2024-05-29 00:24:02 +02:00
|
|
|
*
|
|
|
|
* @param {import('../Database').sequelize} sequelize
|
2023-08-16 01:03:43 +02:00
|
|
|
*/
|
|
|
|
static init(sequelize) {
|
2024-05-29 00:24:02 +02:00
|
|
|
super.init(
|
|
|
|
{
|
|
|
|
id: {
|
|
|
|
type: DataTypes.UUID,
|
|
|
|
defaultValue: DataTypes.UUIDV4,
|
|
|
|
primaryKey: true
|
|
|
|
},
|
|
|
|
slug: DataTypes.STRING,
|
|
|
|
entityType: DataTypes.STRING,
|
|
|
|
entityId: DataTypes.UUIDV4,
|
|
|
|
entityUpdatedAt: DataTypes.DATE,
|
|
|
|
serverAddress: DataTypes.STRING,
|
|
|
|
feedURL: DataTypes.STRING,
|
|
|
|
imageURL: DataTypes.STRING,
|
|
|
|
siteURL: DataTypes.STRING,
|
|
|
|
title: DataTypes.STRING,
|
|
|
|
description: DataTypes.TEXT,
|
|
|
|
author: DataTypes.STRING,
|
|
|
|
podcastType: DataTypes.STRING,
|
|
|
|
language: DataTypes.STRING,
|
|
|
|
ownerName: DataTypes.STRING,
|
|
|
|
ownerEmail: DataTypes.STRING,
|
|
|
|
explicit: DataTypes.BOOLEAN,
|
|
|
|
preventIndexing: DataTypes.BOOLEAN,
|
|
|
|
coverPath: DataTypes.STRING
|
2023-08-16 01:03:43 +02:00
|
|
|
},
|
2024-05-29 00:24:02 +02:00
|
|
|
{
|
|
|
|
sequelize,
|
|
|
|
modelName: 'feed'
|
|
|
|
}
|
|
|
|
)
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
const { user, libraryItem, collection, series, playlist } = sequelize.models
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
user.hasMany(Feed)
|
|
|
|
Feed.belongsTo(user)
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
libraryItem.hasMany(Feed, {
|
|
|
|
foreignKey: 'entityId',
|
|
|
|
constraints: false,
|
|
|
|
scope: {
|
|
|
|
entityType: 'libraryItem'
|
|
|
|
}
|
|
|
|
})
|
|
|
|
Feed.belongsTo(libraryItem, { foreignKey: 'entityId', constraints: false })
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
collection.hasMany(Feed, {
|
|
|
|
foreignKey: 'entityId',
|
|
|
|
constraints: false,
|
|
|
|
scope: {
|
|
|
|
entityType: 'collection'
|
|
|
|
}
|
|
|
|
})
|
|
|
|
Feed.belongsTo(collection, { foreignKey: 'entityId', constraints: false })
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
series.hasMany(Feed, {
|
|
|
|
foreignKey: 'entityId',
|
|
|
|
constraints: false,
|
|
|
|
scope: {
|
|
|
|
entityType: 'series'
|
|
|
|
}
|
|
|
|
})
|
|
|
|
Feed.belongsTo(series, { foreignKey: 'entityId', constraints: false })
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
playlist.hasMany(Feed, {
|
|
|
|
foreignKey: 'entityId',
|
|
|
|
constraints: false,
|
|
|
|
scope: {
|
|
|
|
entityType: 'playlist'
|
2023-07-05 01:14:44 +02:00
|
|
|
}
|
2023-08-16 01:03:43 +02:00
|
|
|
})
|
|
|
|
Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false })
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2024-05-29 00:24:02 +02:00
|
|
|
Feed.addHook('afterFind', (findResult) => {
|
2023-08-16 01:03:43 +02:00
|
|
|
if (!findResult) return
|
|
|
|
|
|
|
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
|
|
|
for (const instance of findResult) {
|
|
|
|
if (instance.entityType === 'libraryItem' && instance.libraryItem !== undefined) {
|
|
|
|
instance.entity = instance.libraryItem
|
|
|
|
instance.dataValues.entity = instance.dataValues.libraryItem
|
|
|
|
} else if (instance.entityType === 'collection' && instance.collection !== undefined) {
|
|
|
|
instance.entity = instance.collection
|
|
|
|
instance.dataValues.entity = instance.dataValues.collection
|
|
|
|
} else if (instance.entityType === 'series' && instance.series !== undefined) {
|
|
|
|
instance.entity = instance.series
|
|
|
|
instance.dataValues.entity = instance.dataValues.series
|
|
|
|
} else if (instance.entityType === 'playlist' && instance.playlist !== undefined) {
|
|
|
|
instance.entity = instance.playlist
|
|
|
|
instance.dataValues.entity = instance.dataValues.playlist
|
|
|
|
}
|
|
|
|
|
|
|
|
// To prevent mistakes:
|
|
|
|
delete instance.libraryItem
|
|
|
|
delete instance.dataValues.libraryItem
|
|
|
|
delete instance.collection
|
|
|
|
delete instance.dataValues.collection
|
|
|
|
delete instance.series
|
|
|
|
delete instance.dataValues.series
|
|
|
|
delete instance.playlist
|
|
|
|
delete instance.dataValues.playlist
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2024-05-29 00:24:02 +02:00
|
|
|
module.exports = Feed
|