mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
716 lines
23 KiB
JavaScript
716 lines
23 KiB
JavaScript
const Path = require('path')
|
|
const { DataTypes, Model } = require('sequelize')
|
|
const oldFeed = require('../objects/Feed')
|
|
const Logger = require('../Logger')
|
|
|
|
const RSS = require('../libs/rss')
|
|
|
|
/**
|
|
* @typedef FeedOptions
|
|
* @property {boolean} preventIndexing
|
|
* @property {string} ownerName
|
|
* @property {string} ownerEmail
|
|
*/
|
|
|
|
/**
|
|
* @typedef FeedExpandedProperties
|
|
* @property {import('./FeedEpisode')} feedEpisodes
|
|
*
|
|
* @typedef {Feed & FeedExpandedProperties} FeedExpanded
|
|
*/
|
|
|
|
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
|
|
|
|
// Expanded properties
|
|
|
|
/** @type {import('./FeedEpisode')[]} - only set if expanded */
|
|
this.feedEpisodes
|
|
}
|
|
|
|
static async getOldFeeds() {
|
|
const feeds = await this.findAll({
|
|
include: {
|
|
model: this.sequelize.models.feedEpisode
|
|
}
|
|
})
|
|
return feeds.map((f) => this.getOldFeed(f))
|
|
}
|
|
|
|
/**
|
|
* Get old feed from Feed and optionally Feed with FeedEpisodes
|
|
* @param {Feed} feedExpanded
|
|
* @returns {oldFeed}
|
|
*/
|
|
static getOldFeed(feedExpanded) {
|
|
const episodes = feedExpanded.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode()) || []
|
|
|
|
// 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))
|
|
}
|
|
|
|
return new oldFeed({
|
|
id: feedExpanded.id,
|
|
slug: feedExpanded.slug,
|
|
userId: feedExpanded.userId,
|
|
entityType: feedExpanded.entityType,
|
|
entityId: feedExpanded.entityId,
|
|
entityUpdatedAt: feedExpanded.entityUpdatedAt?.valueOf() || null,
|
|
coverPath: feedExpanded.coverPath || null,
|
|
meta: {
|
|
title: feedExpanded.title,
|
|
description: feedExpanded.description,
|
|
author: feedExpanded.author,
|
|
imageUrl: feedExpanded.imageURL,
|
|
feedUrl: feedExpanded.feedURL,
|
|
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,
|
|
createdAt: feedExpanded.createdAt.valueOf(),
|
|
updatedAt: feedExpanded.updatedAt.valueOf()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @param {string} feedId
|
|
* @returns {Promise<boolean>} - true if feed was removed
|
|
*/
|
|
static async removeById(feedId) {
|
|
return (
|
|
(await this.destroy({
|
|
where: {
|
|
id: feedId
|
|
}
|
|
})) > 0
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Find feed where and return oldFeed
|
|
* @param {Object} where sequelize where object
|
|
* @returns {Promise<oldFeed>} oldFeed
|
|
*/
|
|
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)
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} userId
|
|
* @param {import('./LibraryItem').LibraryItemExpanded} libraryItem
|
|
* @param {string} slug
|
|
* @param {string} serverAddress
|
|
* @param {FeedOptions} [feedOptions=null]
|
|
*
|
|
* @returns {Feed}
|
|
*/
|
|
static getFeedObjForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions = null) {
|
|
const media = libraryItem.media
|
|
|
|
let entityUpdatedAt = libraryItem.updatedAt
|
|
|
|
// Podcast feeds should use the most recent episode updatedAt if more recent
|
|
if (libraryItem.mediaType === 'podcast') {
|
|
entityUpdatedAt = libraryItem.media.podcastEpisodes.reduce((mostRecent, episode) => {
|
|
return episode.updatedAt > mostRecent ? episode.updatedAt : mostRecent
|
|
}, entityUpdatedAt)
|
|
}
|
|
|
|
const feedObj = {
|
|
slug,
|
|
entityType: 'libraryItem',
|
|
entityId: libraryItem.id,
|
|
entityUpdatedAt,
|
|
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,
|
|
explicit: media.explicit,
|
|
coverPath: media.coverPath,
|
|
userId
|
|
}
|
|
|
|
if (feedOptions) {
|
|
feedObj.preventIndexing = feedOptions.preventIndexing
|
|
feedObj.ownerName = feedOptions.ownerName
|
|
feedObj.ownerEmail = feedOptions.ownerEmail
|
|
}
|
|
|
|
return feedObj
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} userId
|
|
* @param {import('./LibraryItem').LibraryItemExpanded} libraryItem
|
|
* @param {string} slug
|
|
* @param {string} serverAddress
|
|
* @param {FeedOptions} feedOptions
|
|
*
|
|
* @returns {Promise<FeedExpanded>}
|
|
*/
|
|
static async createFeedForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions) {
|
|
const feedObj = this.getFeedObjForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions)
|
|
|
|
/** @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
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} userId
|
|
* @param {import('./Collection')} collectionExpanded
|
|
* @param {string} slug
|
|
* @param {string} serverAddress
|
|
* @param {FeedOptions} [feedOptions=null]
|
|
*
|
|
* @returns {{ feedObj: Feed, booksWithTracks: import('./Book').BookExpandedWithLibraryItem[] }}
|
|
*/
|
|
static getFeedObjForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions = null) {
|
|
const booksWithTracks = collectionExpanded.books.filter((book) => book.includedAudioFiles.length)
|
|
|
|
const entityUpdatedAt = booksWithTracks.reduce((mostRecent, book) => {
|
|
return book.libraryItem.updatedAt > mostRecent ? book.libraryItem.updatedAt : mostRecent
|
|
}, collectionExpanded.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,
|
|
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',
|
|
explicit: booksWithTracks.some((book) => book.explicit), // If any book is explicit, the feed is explicit
|
|
coverPath: firstBookWithCover?.coverPath || null,
|
|
userId
|
|
}
|
|
|
|
if (feedOptions) {
|
|
feedObj.preventIndexing = feedOptions.preventIndexing
|
|
feedObj.ownerName = feedOptions.ownerName
|
|
feedObj.ownerEmail = feedOptions.ownerEmail
|
|
}
|
|
|
|
return {
|
|
feedObj,
|
|
booksWithTracks
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @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 { feedObj, booksWithTracks } = this.getFeedObjForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions)
|
|
|
|
/** @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)
|
|
|
|
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=null]
|
|
*
|
|
* @returns {{ feedObj: Feed, booksWithTracks: import('./Book').BookExpandedWithLibraryItem[] }}
|
|
*/
|
|
static getFeedObjForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions = null) {
|
|
const booksWithTracks = seriesExpanded.books.filter((book) => book.includedAudioFiles.length)
|
|
const entityUpdatedAt = booksWithTracks.reduce((mostRecent, book) => {
|
|
return book.libraryItem.updatedAt > mostRecent ? book.libraryItem.updatedAt : mostRecent
|
|
}, seriesExpanded.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,
|
|
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',
|
|
explicit: booksWithTracks.some((book) => book.explicit), // If any book is explicit, the feed is explicit
|
|
coverPath: firstBookWithCover?.coverPath || null,
|
|
userId
|
|
}
|
|
|
|
if (feedOptions) {
|
|
feedObj.preventIndexing = feedOptions.preventIndexing
|
|
feedObj.ownerName = feedOptions.ownerName
|
|
feedObj.ownerEmail = feedOptions.ownerEmail
|
|
}
|
|
|
|
return {
|
|
feedObj,
|
|
booksWithTracks
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @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 { feedObj, booksWithTracks } = this.getFeedObjForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions)
|
|
|
|
/** @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)
|
|
|
|
await transaction.commit()
|
|
|
|
return feed
|
|
} catch (error) {
|
|
Logger.error(`[Feed] Error creating feed for series ${seriesExpanded.id}`, error)
|
|
await transaction.rollback()
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize model
|
|
*
|
|
* Polymorphic association: Feeds can be created from LibraryItem, Collection, Playlist or Series
|
|
* @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
|
*
|
|
* @param {import('../Database').sequelize} sequelize
|
|
*/
|
|
static init(sequelize) {
|
|
super.init(
|
|
{
|
|
id: {
|
|
type: DataTypes.UUID,
|
|
defaultValue: DataTypes.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
slug: DataTypes.STRING,
|
|
entityType: DataTypes.STRING,
|
|
entityId: DataTypes.UUID,
|
|
entityUpdatedAt: DataTypes.DATE,
|
|
serverAddress: DataTypes.STRING,
|
|
feedURL: DataTypes.STRING,
|
|
imageURL: DataTypes.STRING,
|
|
siteURL: DataTypes.STRING,
|
|
title: DataTypes.STRING,
|
|
description: DataTypes.TEXT,
|
|
author: DataTypes.STRING,
|
|
podcastType: DataTypes.STRING,
|
|
language: DataTypes.STRING,
|
|
ownerName: DataTypes.STRING,
|
|
ownerEmail: DataTypes.STRING,
|
|
explicit: DataTypes.BOOLEAN,
|
|
preventIndexing: DataTypes.BOOLEAN,
|
|
coverPath: DataTypes.STRING
|
|
},
|
|
{
|
|
sequelize,
|
|
modelName: 'feed'
|
|
}
|
|
)
|
|
|
|
const { user, libraryItem, collection, series, playlist } = sequelize.models
|
|
|
|
user.hasMany(Feed)
|
|
Feed.belongsTo(user)
|
|
|
|
libraryItem.hasMany(Feed, {
|
|
foreignKey: 'entityId',
|
|
constraints: false,
|
|
scope: {
|
|
entityType: 'libraryItem'
|
|
}
|
|
})
|
|
Feed.belongsTo(libraryItem, { foreignKey: 'entityId', constraints: false })
|
|
|
|
collection.hasMany(Feed, {
|
|
foreignKey: 'entityId',
|
|
constraints: false,
|
|
scope: {
|
|
entityType: 'collection'
|
|
}
|
|
})
|
|
Feed.belongsTo(collection, { foreignKey: 'entityId', constraints: false })
|
|
|
|
series.hasMany(Feed, {
|
|
foreignKey: 'entityId',
|
|
constraints: false,
|
|
scope: {
|
|
entityType: 'series'
|
|
}
|
|
})
|
|
Feed.belongsTo(series, { foreignKey: 'entityId', constraints: false })
|
|
|
|
playlist.hasMany(Feed, {
|
|
foreignKey: 'entityId',
|
|
constraints: false,
|
|
scope: {
|
|
entityType: 'playlist'
|
|
}
|
|
})
|
|
Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false })
|
|
|
|
Feed.addHook('afterFind', (findResult) => {
|
|
if (!findResult) return
|
|
|
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
|
for (const instance of findResult) {
|
|
if (instance.entityType === 'libraryItem' && instance.libraryItem !== undefined) {
|
|
instance.entity = instance.libraryItem
|
|
instance.dataValues.entity = instance.dataValues.libraryItem
|
|
} else if (instance.entityType === 'collection' && instance.collection !== undefined) {
|
|
instance.entity = instance.collection
|
|
instance.dataValues.entity = instance.dataValues.collection
|
|
} else if (instance.entityType === 'series' && instance.series !== undefined) {
|
|
instance.entity = instance.series
|
|
instance.dataValues.entity = instance.dataValues.series
|
|
} else if (instance.entityType === 'playlist' && instance.playlist !== undefined) {
|
|
instance.entity = instance.playlist
|
|
instance.dataValues.entity = instance.dataValues.playlist
|
|
}
|
|
|
|
// To prevent mistakes:
|
|
delete instance.libraryItem
|
|
delete instance.dataValues.libraryItem
|
|
delete instance.collection
|
|
delete instance.dataValues.collection
|
|
delete instance.series
|
|
delete instance.dataValues.series
|
|
delete instance.playlist
|
|
delete instance.dataValues.playlist
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns {Promise<FeedExpanded>}
|
|
*/
|
|
async updateFeedForEntity() {
|
|
/** @type {typeof import('./FeedEpisode')} */
|
|
const feedEpisodeModel = this.sequelize.models.feedEpisode
|
|
|
|
let feedObj = null
|
|
let feedEpisodeCreateFunc = null
|
|
let feedEpisodeCreateFuncEntity = null
|
|
|
|
if (this.entityType === 'libraryItem') {
|
|
/** @type {typeof import('./LibraryItem')} */
|
|
const libraryItemModel = this.sequelize.models.libraryItem
|
|
|
|
const itemExpanded = await libraryItemModel.getExpandedById(this.entityId)
|
|
feedObj = Feed.getFeedObjForLibraryItem(this.userId, itemExpanded, this.slug, this.serverAddress)
|
|
|
|
feedEpisodeCreateFuncEntity = itemExpanded
|
|
if (itemExpanded.mediaType === 'podcast') {
|
|
feedEpisodeCreateFunc = feedEpisodeModel.createFromPodcastEpisodes.bind(feedEpisodeModel)
|
|
} else {
|
|
feedEpisodeCreateFunc = feedEpisodeModel.createFromAudiobookTracks.bind(feedEpisodeModel)
|
|
}
|
|
} else if (this.entityType === 'collection') {
|
|
/** @type {typeof import('./Collection')} */
|
|
const collectionModel = this.sequelize.models.collection
|
|
|
|
const collectionExpanded = await collectionModel.getExpandedById(this.entityId)
|
|
const feedObjData = Feed.getFeedObjForCollection(this.userId, collectionExpanded, this.slug, this.serverAddress)
|
|
feedObj = feedObjData.feedObj
|
|
feedEpisodeCreateFuncEntity = feedObjData.booksWithTracks
|
|
feedEpisodeCreateFunc = feedEpisodeModel.createFromBooks.bind(feedEpisodeModel)
|
|
} else if (this.entityType === 'series') {
|
|
/** @type {typeof import('./Series')} */
|
|
const seriesModel = this.sequelize.models.series
|
|
|
|
const seriesExpanded = await seriesModel.getExpandedById(this.entityId)
|
|
const feedObjData = Feed.getFeedObjForSeries(this.userId, seriesExpanded, this.slug, this.serverAddress)
|
|
feedObj = feedObjData.feedObj
|
|
feedEpisodeCreateFuncEntity = feedObjData.booksWithTracks
|
|
feedEpisodeCreateFunc = feedEpisodeModel.createFromBooks.bind(feedEpisodeModel)
|
|
} else {
|
|
Logger.error(`[Feed] Invalid entity type ${this.entityType} for feed ${this.id}`)
|
|
return null
|
|
}
|
|
|
|
const transaction = await this.sequelize.transaction()
|
|
try {
|
|
const updatedFeed = await this.update(feedObj, { transaction })
|
|
|
|
// Remove existing feed episodes
|
|
await feedEpisodeModel.destroy({
|
|
where: {
|
|
feedId: this.id
|
|
},
|
|
transaction
|
|
})
|
|
|
|
// Create new feed episodes
|
|
updatedFeed.feedEpisodes = await feedEpisodeCreateFunc(feedEpisodeCreateFuncEntity, updatedFeed, this.slug, transaction)
|
|
|
|
await transaction.commit()
|
|
|
|
return updatedFeed
|
|
} catch (error) {
|
|
Logger.error(`[Feed] Error updating feed ${this.entityId}`, error)
|
|
await transaction.rollback()
|
|
|
|
return null
|
|
}
|
|
}
|
|
|
|
getEntity(options) {
|
|
if (!this.entityType) return Promise.resolve(null)
|
|
const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.entityType)}`
|
|
return this[mixinMethodName](options)
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} hostPrefix
|
|
*/
|
|
buildXml(hostPrefix) {
|
|
const blockTags = [{ 'itunes:block': 'yes' }, { 'googleplay:block': 'yes' }]
|
|
const rssData = {
|
|
title: this.title,
|
|
description: this.description || '',
|
|
generator: 'Audiobookshelf',
|
|
feed_url: `${hostPrefix}${this.feedURL}`,
|
|
site_url: `${hostPrefix}${this.siteURL}`,
|
|
image_url: `${hostPrefix}${this.imageURL}`,
|
|
custom_namespaces: {
|
|
itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd',
|
|
psc: 'http://podlove.org/simple-chapters',
|
|
podcast: 'https://podcastindex.org/namespace/1.0',
|
|
googleplay: 'http://www.google.com/schemas/play-podcasts/1.0'
|
|
},
|
|
custom_elements: [
|
|
{ language: this.language || 'en' },
|
|
{ author: this.author || 'advplyr' },
|
|
{ 'itunes:author': this.author || 'advplyr' },
|
|
{ 'itunes:summary': this.description || '' },
|
|
{ 'itunes:type': this.podcastType },
|
|
{
|
|
'itunes:image': {
|
|
_attr: {
|
|
href: `${hostPrefix}${this.imageURL}`
|
|
}
|
|
}
|
|
},
|
|
{
|
|
'itunes:owner': [{ 'itunes:name': this.ownerName || this.author || '' }, { 'itunes:email': this.ownerEmail || '' }]
|
|
},
|
|
{ 'itunes:explicit': !!this.explicit },
|
|
...(this.preventIndexing ? blockTags : [])
|
|
]
|
|
}
|
|
|
|
const rssfeed = new RSS(rssData)
|
|
this.feedEpisodes.forEach((ep) => {
|
|
rssfeed.item(ep.getRSSData(hostPrefix))
|
|
})
|
|
return rssfeed.xml()
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = Feed
|