2024-12-14 23:55:56 +01:00
|
|
|
const Path = require('path')
|
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
|
|
|
|
2024-12-14 23:55:56 +01:00
|
|
|
/**
|
|
|
|
* @typedef FeedOptions
|
|
|
|
* @property {boolean} preventIndexing
|
|
|
|
* @property {string} ownerName
|
|
|
|
* @property {string} ownerEmail
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef FeedExpandedProperties
|
|
|
|
* @property {import('./FeedEpisode')} feedEpisodes
|
|
|
|
*
|
|
|
|
* @typedef {Feed & FeedExpandedProperties} FeedExpanded
|
|
|
|
*/
|
|
|
|
|
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
|
2024-12-14 23:55:56 +01:00
|
|
|
|
|
|
|
/** @type {import('./FeedEpisode')[]} - only set if expanded */
|
|
|
|
this.feedEpisodes
|
2023-08-16 01:03:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2024-12-14 23:55:56 +01:00
|
|
|
const episodes = feedExpanded.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode()) || []
|
|
|
|
|
|
|
|
// Sort episodes by pubDate. Newest to oldest for episodic, oldest to newest for serial
|
|
|
|
if (feedExpanded.podcastType === 'episodic') {
|
|
|
|
episodes.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate))
|
|
|
|
} else {
|
|
|
|
episodes.sort((a, b) => new Date(a.pubDate) - new Date(b.pubDate))
|
|
|
|
}
|
|
|
|
|
2023-08-16 01:03:43 +02:00
|
|
|
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,
|
2024-12-14 23:55:56 +01:00
|
|
|
episodes,
|
2023-08-16 01:03:43 +02:00
|
|
|
createdAt: feedExpanded.createdAt.valueOf(),
|
|
|
|
updatedAt: feedExpanded.updatedAt.valueOf()
|
|
|
|
})
|
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2024-12-15 19:37:01 +01:00
|
|
|
/**
|
|
|
|
* @param {string} feedId
|
|
|
|
* @returns {Promise<boolean>} - true if feed was removed
|
|
|
|
*/
|
|
|
|
static async removeById(feedId) {
|
|
|
|
return (
|
|
|
|
(await this.destroy({
|
|
|
|
where: {
|
|
|
|
id: feedId
|
|
|
|
}
|
|
|
|
})) > 0
|
|
|
|
)
|
2023-08-16 01:03:43 +02:00
|
|
|
}
|
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
|
|
|
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
|
|
|
|
2024-12-14 23:55:56 +01:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {string} userId
|
|
|
|
* @param {import('./LibraryItem').LibraryItemExpanded} libraryItem
|
2024-12-15 17:53:31 +01:00
|
|
|
* @param {string} slug
|
2024-12-14 23:55:56 +01:00
|
|
|
* @param {string} serverAddress
|
|
|
|
* @param {FeedOptions} feedOptions
|
|
|
|
*
|
|
|
|
* @returns {Promise<FeedExpanded>}
|
|
|
|
*/
|
|
|
|
static async createFeedForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions) {
|
|
|
|
const media = libraryItem.media
|
|
|
|
|
|
|
|
const feedObj = {
|
|
|
|
slug,
|
|
|
|
entityType: 'libraryItem',
|
|
|
|
entityId: libraryItem.id,
|
|
|
|
entityUpdatedAt: libraryItem.updatedAt,
|
|
|
|
serverAddress,
|
|
|
|
feedURL: `/feed/${slug}`,
|
|
|
|
imageURL: media.coverPath ? `/feed/${slug}/cover${Path.extname(media.coverPath)}` : `/Logo.png`,
|
|
|
|
siteURL: `/item/${libraryItem.id}`,
|
|
|
|
title: media.title,
|
|
|
|
description: media.description,
|
|
|
|
author: libraryItem.mediaType === 'podcast' ? media.author : media.authorName,
|
|
|
|
podcastType: libraryItem.mediaType === 'podcast' ? media.podcastType : 'serial',
|
|
|
|
language: media.language,
|
|
|
|
preventIndexing: feedOptions.preventIndexing,
|
|
|
|
ownerName: feedOptions.ownerName,
|
|
|
|
ownerEmail: feedOptions.ownerEmail,
|
|
|
|
explicit: media.explicit,
|
|
|
|
coverPath: media.coverPath,
|
|
|
|
userId
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @type {typeof import('./FeedEpisode')} */
|
|
|
|
const feedEpisodeModel = this.sequelize.models.feedEpisode
|
|
|
|
|
|
|
|
const transaction = await this.sequelize.transaction()
|
|
|
|
try {
|
|
|
|
const feed = await this.create(feedObj, { transaction })
|
|
|
|
|
|
|
|
if (libraryItem.mediaType === 'podcast') {
|
|
|
|
feed.feedEpisodes = await feedEpisodeModel.createFromPodcastEpisodes(libraryItem, feed, slug, transaction)
|
|
|
|
} else {
|
|
|
|
feed.feedEpisodes = await feedEpisodeModel.createFromAudiobookTracks(libraryItem, feed, slug, transaction)
|
|
|
|
}
|
|
|
|
|
|
|
|
await transaction.commit()
|
|
|
|
|
|
|
|
return feed
|
|
|
|
} catch (error) {
|
|
|
|
Logger.error(`[Feed] Error creating feed for library item ${libraryItem.id}`, error)
|
|
|
|
await transaction.rollback()
|
|
|
|
return null
|
|
|
|
}
|
2023-08-16 01:03:43 +02:00
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2024-12-15 17:53:31 +01:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {string} userId
|
|
|
|
* @param {import('./Collection')} collectionExpanded
|
|
|
|
* @param {string} slug
|
|
|
|
* @param {string} serverAddress
|
|
|
|
* @param {FeedOptions} feedOptions
|
|
|
|
*
|
|
|
|
* @returns {Promise<FeedExpanded>}
|
|
|
|
*/
|
|
|
|
static async createFeedForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions) {
|
|
|
|
const booksWithTracks = collectionExpanded.books.filter((book) => book.includedAudioFiles.length)
|
|
|
|
const libraryItemMostRecentlyUpdatedAt = booksWithTracks.reduce((mostRecent, book) => {
|
|
|
|
return book.libraryItem.updatedAt > mostRecent.libraryItem.updatedAt ? book : mostRecent
|
|
|
|
}).libraryItem.updatedAt
|
|
|
|
|
|
|
|
const firstBookWithCover = booksWithTracks.find((book) => book.coverPath)
|
|
|
|
|
|
|
|
const allBookAuthorNames = booksWithTracks.reduce((authorNames, book) => {
|
|
|
|
const bookAuthorsToAdd = book.authors.filter((author) => !authorNames.includes(author.name)).map((author) => author.name)
|
|
|
|
return authorNames.concat(bookAuthorsToAdd)
|
|
|
|
}, [])
|
|
|
|
let author = allBookAuthorNames.slice(0, 3).join(', ')
|
|
|
|
if (allBookAuthorNames.length > 3) {
|
|
|
|
author += ' & more'
|
|
|
|
}
|
|
|
|
|
|
|
|
const feedObj = {
|
|
|
|
slug,
|
|
|
|
entityType: 'collection',
|
|
|
|
entityId: collectionExpanded.id,
|
|
|
|
entityUpdatedAt: libraryItemMostRecentlyUpdatedAt,
|
|
|
|
serverAddress,
|
|
|
|
feedURL: `/feed/${slug}`,
|
|
|
|
imageURL: firstBookWithCover?.coverPath ? `/feed/${slug}/cover${Path.extname(firstBookWithCover.coverPath)}` : `/Logo.png`,
|
|
|
|
siteURL: `/collection/${collectionExpanded.id}`,
|
|
|
|
title: collectionExpanded.name,
|
|
|
|
description: collectionExpanded.description || '',
|
|
|
|
author,
|
|
|
|
podcastType: 'serial',
|
|
|
|
preventIndexing: feedOptions.preventIndexing,
|
|
|
|
ownerName: feedOptions.ownerName,
|
|
|
|
ownerEmail: feedOptions.ownerEmail,
|
|
|
|
explicit: booksWithTracks.some((book) => book.explicit), // If any book is explicit, the feed is explicit
|
|
|
|
coverPath: firstBookWithCover?.coverPath || null,
|
|
|
|
userId
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @type {typeof import('./FeedEpisode')} */
|
|
|
|
const feedEpisodeModel = this.sequelize.models.feedEpisode
|
|
|
|
|
|
|
|
const transaction = await this.sequelize.transaction()
|
|
|
|
try {
|
|
|
|
const feed = await this.create(feedObj, { transaction })
|
2024-12-15 18:44:07 +01:00
|
|
|
feed.feedEpisodes = await feedEpisodeModel.createFromBooks(booksWithTracks, feed, slug, transaction)
|
|
|
|
|
|
|
|
await transaction.commit()
|
|
|
|
|
|
|
|
return feed
|
|
|
|
} catch (error) {
|
|
|
|
Logger.error(`[Feed] Error creating feed for collection ${collectionExpanded.id}`, error)
|
|
|
|
await transaction.rollback()
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {string} userId
|
|
|
|
* @param {import('./Series')} seriesExpanded
|
|
|
|
* @param {string} slug
|
|
|
|
* @param {string} serverAddress
|
|
|
|
* @param {FeedOptions} feedOptions
|
|
|
|
*
|
|
|
|
* @returns {Promise<FeedExpanded>}
|
|
|
|
*/
|
|
|
|
static async createFeedForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions) {
|
|
|
|
const booksWithTracks = seriesExpanded.books.filter((book) => book.includedAudioFiles.length)
|
|
|
|
const libraryItemMostRecentlyUpdatedAt = booksWithTracks.reduce((mostRecent, book) => {
|
|
|
|
return book.libraryItem.updatedAt > mostRecent.libraryItem.updatedAt ? book : mostRecent
|
|
|
|
}).libraryItem.updatedAt
|
|
|
|
|
|
|
|
const firstBookWithCover = booksWithTracks.find((book) => book.coverPath)
|
|
|
|
|
|
|
|
const allBookAuthorNames = booksWithTracks.reduce((authorNames, book) => {
|
|
|
|
const bookAuthorsToAdd = book.authors.filter((author) => !authorNames.includes(author.name)).map((author) => author.name)
|
|
|
|
return authorNames.concat(bookAuthorsToAdd)
|
|
|
|
}, [])
|
|
|
|
let author = allBookAuthorNames.slice(0, 3).join(', ')
|
|
|
|
if (allBookAuthorNames.length > 3) {
|
|
|
|
author += ' & more'
|
|
|
|
}
|
|
|
|
|
|
|
|
const feedObj = {
|
|
|
|
slug,
|
|
|
|
entityType: 'series',
|
|
|
|
entityId: seriesExpanded.id,
|
|
|
|
entityUpdatedAt: libraryItemMostRecentlyUpdatedAt,
|
|
|
|
serverAddress,
|
|
|
|
feedURL: `/feed/${slug}`,
|
|
|
|
imageURL: firstBookWithCover?.coverPath ? `/feed/${slug}/cover${Path.extname(firstBookWithCover.coverPath)}` : `/Logo.png`,
|
|
|
|
siteURL: `/library/${booksWithTracks[0].libraryItem.libraryId}/series/${seriesExpanded.id}`,
|
|
|
|
title: seriesExpanded.name,
|
|
|
|
description: seriesExpanded.description || '',
|
|
|
|
author,
|
|
|
|
podcastType: 'serial',
|
|
|
|
preventIndexing: feedOptions.preventIndexing,
|
|
|
|
ownerName: feedOptions.ownerName,
|
|
|
|
ownerEmail: feedOptions.ownerEmail,
|
|
|
|
explicit: booksWithTracks.some((book) => book.explicit), // If any book is explicit, the feed is explicit
|
|
|
|
coverPath: firstBookWithCover?.coverPath || null,
|
|
|
|
userId
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @type {typeof import('./FeedEpisode')} */
|
|
|
|
const feedEpisodeModel = this.sequelize.models.feedEpisode
|
|
|
|
|
|
|
|
const transaction = await this.sequelize.transaction()
|
|
|
|
try {
|
|
|
|
const feed = await this.create(feedObj, { transaction })
|
|
|
|
feed.feedEpisodes = await feedEpisodeModel.createFromBooks(booksWithTracks, feed, slug, transaction)
|
2024-12-15 17:53:31 +01:00
|
|
|
|
|
|
|
await transaction.commit()
|
|
|
|
|
|
|
|
return feed
|
|
|
|
} catch (error) {
|
|
|
|
Logger.error(`[Feed] Error creating feed for collection ${collectionExpanded.id}`, error)
|
|
|
|
await transaction.rollback()
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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,
|
2024-11-17 22:45:21 +01:00
|
|
|
entityId: DataTypes.UUID,
|
2024-05-29 00:24:02 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2024-12-14 23:55:56 +01:00
|
|
|
|
|
|
|
getEntity(options) {
|
|
|
|
if (!this.entityType) return Promise.resolve(null)
|
|
|
|
const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.entityType)}`
|
|
|
|
return this[mixinMethodName](options)
|
|
|
|
}
|
|
|
|
|
|
|
|
toOldJSON() {
|
|
|
|
const episodes = this.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode())
|
|
|
|
return {
|
|
|
|
id: this.id,
|
|
|
|
slug: this.slug,
|
|
|
|
userId: this.userId,
|
|
|
|
entityType: this.entityType,
|
|
|
|
entityId: this.entityId,
|
|
|
|
entityUpdatedAt: this.entityUpdatedAt?.valueOf() || null,
|
|
|
|
coverPath: this.coverPath || null,
|
|
|
|
meta: {
|
|
|
|
title: this.title,
|
|
|
|
description: this.description,
|
|
|
|
author: this.author,
|
|
|
|
imageUrl: this.imageURL,
|
|
|
|
feedUrl: this.feedURL,
|
|
|
|
link: this.siteURL,
|
|
|
|
explicit: this.explicit,
|
|
|
|
type: this.podcastType,
|
|
|
|
language: this.language,
|
|
|
|
preventIndexing: this.preventIndexing,
|
|
|
|
ownerName: this.ownerName,
|
|
|
|
ownerEmail: this.ownerEmail
|
|
|
|
},
|
|
|
|
serverAddress: this.serverAddress,
|
|
|
|
feedUrl: this.feedURL,
|
|
|
|
episodes: episodes || [],
|
|
|
|
createdAt: this.createdAt.valueOf(),
|
|
|
|
updatedAt: this.updatedAt.valueOf()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toOldJSONMinified() {
|
|
|
|
return {
|
|
|
|
id: this.id,
|
|
|
|
entityType: this.entityType,
|
|
|
|
entityId: this.entityId,
|
|
|
|
feedUrl: this.feedURL,
|
|
|
|
meta: {
|
|
|
|
title: this.title,
|
|
|
|
description: this.description,
|
|
|
|
preventIndexing: this.preventIndexing,
|
|
|
|
ownerName: this.ownerName,
|
|
|
|
ownerEmail: this.ownerEmail
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-08-16 01:03:43 +02:00
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
|
2024-05-29 00:24:02 +02:00
|
|
|
module.exports = Feed
|