mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Refactor Feed model to create new feed for collection
This commit is contained in:
parent
ca2327aba3
commit
d576625cb7
@ -87,35 +87,39 @@ class RSSFeedController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async openRSSFeedForCollection(req, res) {
|
async openRSSFeedForCollection(req, res) {
|
||||||
const options = req.body || {}
|
const reqBody = req.body || {}
|
||||||
|
|
||||||
const collection = await Database.collectionModel.findByPk(req.params.collectionId)
|
const collection = await Database.collectionModel.findByPk(req.params.collectionId)
|
||||||
if (!collection) return res.sendStatus(404)
|
if (!collection) return res.sendStatus(404)
|
||||||
|
|
||||||
// Check request body options exist
|
// Check request body options exist
|
||||||
if (!options.serverAddress || !options.slug) {
|
if (!reqBody.serverAddress || !reqBody.slug || typeof reqBody.serverAddress !== 'string' || typeof reqBody.slug !== 'string') {
|
||||||
Logger.error(`[RSSFeedController] Invalid request body to open RSS feed`)
|
Logger.error(`[RSSFeedController] Invalid request body to open RSS feed`)
|
||||||
return res.status(400).send('Invalid request body')
|
return res.status(400).send('Invalid request body')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
||||||
if (await this.rssFeedManager.findFeedBySlug(options.slug)) {
|
if (await this.rssFeedManager.findFeedBySlug(reqBody.slug)) {
|
||||||
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${reqBody.slug}" is already in use`)
|
||||||
return res.status(400).send('Slug already in use')
|
return res.status(400).send('Slug already in use')
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionExpanded = await collection.getOldJsonExpanded()
|
collection.books = await collection.getBooksExpandedWithLibraryItem()
|
||||||
const collectionItemsWithTracks = collectionExpanded.books.filter((li) => li.media.tracks.length)
|
|
||||||
|
|
||||||
// Check collection has audio tracks
|
// Check collection has audio tracks
|
||||||
if (!collectionItemsWithTracks.length) {
|
if (!collection.books.some((book) => book.includedAudioFiles.length)) {
|
||||||
Logger.error(`[RSSFeedController] Cannot open RSS feed for collection "${collection.name}" because it has no audio tracks`)
|
Logger.error(`[RSSFeedController] Cannot open RSS feed for collection "${collection.name}" because it has no audio tracks`)
|
||||||
return res.status(400).send('Collection has no audio tracks')
|
return res.status(400).send('Collection has no audio tracks')
|
||||||
}
|
}
|
||||||
|
|
||||||
const feed = await this.rssFeedManager.openFeedForCollection(req.user.id, collectionExpanded, req.body)
|
const feed = await this.rssFeedManager.openFeedForCollection(req.user.id, collection, reqBody)
|
||||||
|
if (!feed) {
|
||||||
|
Logger.error(`[RSSFeedController] Failed to open RSS feed for collection "${collection.name}"`)
|
||||||
|
return res.status(500).send('Failed to open RSS feed')
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
feed: feed.toJSONMinified()
|
feed: feed.toOldJSONMinified()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,24 +264,22 @@ class RssFeedManager {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} userId
|
* @param {string} userId
|
||||||
* @param {*} collectionExpanded
|
* @param {import('../models/Collection')} collectionExpanded
|
||||||
* @param {*} options
|
* @param {*} options
|
||||||
* @returns
|
* @returns {Promise<import('../models/Feed').FeedExpanded>}
|
||||||
*/
|
*/
|
||||||
async openFeedForCollection(userId, collectionExpanded, options) {
|
async openFeedForCollection(userId, collectionExpanded, options) {
|
||||||
const serverAddress = options.serverAddress
|
const serverAddress = options.serverAddress
|
||||||
const slug = options.slug
|
const slug = options.slug
|
||||||
const preventIndexing = options.metadataDetails?.preventIndexing ?? true
|
const feedOptions = this.getFeedOptionsFromReqOptions(options)
|
||||||
const ownerName = options.metadataDetails?.ownerName
|
|
||||||
const ownerEmail = options.metadataDetails?.ownerEmail
|
|
||||||
|
|
||||||
const feed = new Feed()
|
Logger.info(`[RssFeedManager] Creating RSS feed for collection "${collectionExpanded.name}"`)
|
||||||
feed.setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing, ownerName, ownerEmail)
|
const feedExpanded = await Database.feedModel.createFeedForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions)
|
||||||
|
if (feedExpanded) {
|
||||||
Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
Logger.info(`[RssFeedManager] Opened RSS feed "${feedExpanded.feedURL}"`)
|
||||||
await Database.createFeed(feed)
|
SocketAuthority.emitter('rss_feed_open', feedExpanded.toOldJSONMinified())
|
||||||
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
}
|
||||||
return feed
|
return feedExpanded
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,6 +29,12 @@ const Logger = require('../Logger')
|
|||||||
* @property {SeriesExpanded[]} series
|
* @property {SeriesExpanded[]} series
|
||||||
*
|
*
|
||||||
* @typedef {Book & BookExpandedProperties} BookExpanded
|
* @typedef {Book & BookExpandedProperties} BookExpanded
|
||||||
|
*
|
||||||
|
* Collections use BookExpandedWithLibraryItem
|
||||||
|
* @typedef BookExpandedWithLibraryItemProperties
|
||||||
|
* @property {import('./LibraryItem')} libraryItem
|
||||||
|
*
|
||||||
|
* @typedef {BookExpanded & BookExpandedWithLibraryItemProperties} BookExpandedWithLibraryItem
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const { DataTypes, Model, Sequelize } = require('sequelize')
|
const { DataTypes, Model, Sequelize } = require('sequelize')
|
||||||
|
|
||||||
const oldCollection = require('../objects/Collection')
|
const oldCollection = require('../objects/Collection')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class Collection extends Model {
|
class Collection extends Model {
|
||||||
constructor(values, options) {
|
constructor(values, options) {
|
||||||
@ -18,6 +19,11 @@ class Collection extends Model {
|
|||||||
this.updatedAt
|
this.updatedAt
|
||||||
/** @type {Date} */
|
/** @type {Date} */
|
||||||
this.createdAt
|
this.createdAt
|
||||||
|
|
||||||
|
// Expanded properties
|
||||||
|
|
||||||
|
/** @type {import('./Book').BookExpandedWithLibraryItem[]} - only set when expanded */
|
||||||
|
this.books
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -219,6 +225,34 @@ class Collection extends Model {
|
|||||||
Collection.belongsTo(library)
|
Collection.belongsTo(library)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all books in collection expanded with library item
|
||||||
|
*
|
||||||
|
* @returns {Promise<import('./Book').BookExpandedWithLibraryItem[]>}
|
||||||
|
*/
|
||||||
|
getBooksExpandedWithLibraryItem() {
|
||||||
|
return this.getBooks({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: this.sequelize.models.libraryItem
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: this.sequelize.models.author,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: this.sequelize.models.series,
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: [Sequelize.literal('`collectionBook.order` ASC')]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get old collection toJSONExpanded, items filtered for user permissions
|
* Get old collection toJSONExpanded, items filtered for user permissions
|
||||||
*
|
*
|
||||||
|
@ -279,8 +279,8 @@ class Feed extends Model {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} userId
|
* @param {string} userId
|
||||||
* @param {string} slug
|
|
||||||
* @param {import('./LibraryItem').LibraryItemExpanded} libraryItem
|
* @param {import('./LibraryItem').LibraryItemExpanded} libraryItem
|
||||||
|
* @param {string} slug
|
||||||
* @param {string} serverAddress
|
* @param {string} serverAddress
|
||||||
* @param {FeedOptions} feedOptions
|
* @param {FeedOptions} feedOptions
|
||||||
*
|
*
|
||||||
@ -334,6 +334,72 @@ class Feed extends Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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 })
|
||||||
|
feed.feedEpisodes = await feedEpisodeModel.createFromCollectionBooks(collectionExpanded, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize model
|
* Initialize model
|
||||||
*
|
*
|
||||||
|
@ -132,12 +132,12 @@ class FeedEpisode extends Model {
|
|||||||
/**
|
/**
|
||||||
* If chapters for an audiobook match the audio tracks then use chapter titles instead of audio file names
|
* If chapters for an audiobook match the audio tracks then use chapter titles instead of audio file names
|
||||||
*
|
*
|
||||||
* @param {import('./LibraryItem').LibraryItemExpanded} libraryItemExpanded
|
* @param {import('./Book')} book
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
static checkUseChapterTitlesForEpisodes(libraryItemExpanded) {
|
static checkUseChapterTitlesForEpisodes(book) {
|
||||||
const tracks = libraryItemExpanded.media.trackList || []
|
const tracks = book.trackList || []
|
||||||
const chapters = libraryItemExpanded.media.chapters || []
|
const chapters = book.chapters || []
|
||||||
if (tracks.length !== chapters.length) return false
|
if (tracks.length !== chapters.length) return false
|
||||||
for (let i = 0; i < tracks.length; i++) {
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
if (Math.abs(chapters[i].start - tracks[i].startOffset) >= 1) {
|
if (Math.abs(chapters[i].start - tracks[i].startOffset) >= 1) {
|
||||||
@ -149,32 +149,31 @@ class FeedEpisode extends Model {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {import('./LibraryItem').LibraryItemExpanded} libraryItemExpanded
|
* @param {import('./Book')} book
|
||||||
|
* @param {Date} pubDateStart
|
||||||
* @param {import('./Feed')} feed
|
* @param {import('./Feed')} feed
|
||||||
* @param {string} slug
|
* @param {string} slug
|
||||||
* @param {import('./Book').AudioFileObject} audioTrack
|
* @param {import('./Book').AudioFileObject} audioTrack
|
||||||
* @param {boolean} useChapterTitles
|
* @param {boolean} useChapterTitles
|
||||||
* @param {string} [pubDateOverride]
|
|
||||||
*/
|
*/
|
||||||
static getFeedEpisodeObjFromAudiobookTrack(libraryItemExpanded, feed, slug, audioTrack, useChapterTitles, pubDateOverride = null) {
|
static getFeedEpisodeObjFromAudiobookTrack(book, pubDateStart, feed, slug, audioTrack, useChapterTitles) {
|
||||||
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
||||||
let timeOffset = isNaN(audioTrack.index) ? 0 : Number(audioTrack.index) * 1000 // Offset pubdate to ensure correct order
|
let timeOffset = isNaN(audioTrack.index) ? 0 : Number(audioTrack.index) * 1000 // Offset pubdate to ensure correct order
|
||||||
let episodeId = uuidv4()
|
let episodeId = uuidv4()
|
||||||
|
|
||||||
// e.g. Track 1 will have a pub date before Track 2
|
// e.g. Track 1 will have a pub date before Track 2
|
||||||
const audiobookPubDate = pubDateOverride || date.format(new Date(libraryItemExpanded.createdAt.valueOf() + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
const audiobookPubDate = date.format(new Date(pubDateStart.valueOf() + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
||||||
|
|
||||||
const contentUrl = `/feed/${slug}/item/${episodeId}/media${Path.extname(audioTrack.metadata.filename)}`
|
const contentUrl = `/feed/${slug}/item/${episodeId}/media${Path.extname(audioTrack.metadata.filename)}`
|
||||||
const media = libraryItemExpanded.media
|
|
||||||
|
|
||||||
let title = audioTrack.title
|
let title = audioTrack.title
|
||||||
if (media.trackList.length == 1) {
|
if (book.trackList.length == 1) {
|
||||||
// If audiobook is a single file, use book title instead of chapter/file title
|
// If audiobook is a single file, use book title instead of chapter/file title
|
||||||
title = media.title
|
title = book.title
|
||||||
} else {
|
} else {
|
||||||
if (useChapterTitles) {
|
if (useChapterTitles) {
|
||||||
// If audio track start and chapter start are within 1 seconds of eachother then use the chapter title
|
// If audio track start and chapter start are within 1 seconds of eachother then use the chapter title
|
||||||
const matchingChapter = media.chapters.find((ch) => Math.abs(ch.start - audioTrack.startOffset) < 1)
|
const matchingChapter = book.chapters.find((ch) => Math.abs(ch.start - audioTrack.startOffset) < 1)
|
||||||
if (matchingChapter?.title) title = matchingChapter.title
|
if (matchingChapter?.title) title = matchingChapter.title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,7 +182,7 @@ class FeedEpisode extends Model {
|
|||||||
id: episodeId,
|
id: episodeId,
|
||||||
title,
|
title,
|
||||||
author: feed.author,
|
author: feed.author,
|
||||||
description: media.description || '',
|
description: book.description || '',
|
||||||
siteURL: feed.siteURL,
|
siteURL: feed.siteURL,
|
||||||
enclosureURL: contentUrl,
|
enclosureURL: contentUrl,
|
||||||
enclosureType: audioTrack.mimeType,
|
enclosureType: audioTrack.mimeType,
|
||||||
@ -191,7 +190,7 @@ class FeedEpisode extends Model {
|
|||||||
pubDate: audiobookPubDate,
|
pubDate: audiobookPubDate,
|
||||||
duration: audioTrack.duration,
|
duration: audioTrack.duration,
|
||||||
filePath: audioTrack.metadata.path,
|
filePath: audioTrack.metadata.path,
|
||||||
explicit: media.explicit,
|
explicit: book.explicit,
|
||||||
feedId: feed.id
|
feedId: feed.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,11 +204,37 @@ class FeedEpisode extends Model {
|
|||||||
* @returns {Promise<FeedEpisode[]>}
|
* @returns {Promise<FeedEpisode[]>}
|
||||||
*/
|
*/
|
||||||
static async createFromAudiobookTracks(libraryItemExpanded, feed, slug, transaction) {
|
static async createFromAudiobookTracks(libraryItemExpanded, feed, slug, transaction) {
|
||||||
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItemExpanded)
|
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItemExpanded.media)
|
||||||
|
|
||||||
const feedEpisodeObjs = []
|
const feedEpisodeObjs = []
|
||||||
for (const track of libraryItemExpanded.media.trackList) {
|
for (const track of libraryItemExpanded.media.trackList) {
|
||||||
feedEpisodeObjs.push(this.getFeedEpisodeObjFromAudiobookTrack(libraryItemExpanded, feed, slug, track, useChapterTitles))
|
feedEpisodeObjs.push(this.getFeedEpisodeObjFromAudiobookTrack(libraryItemExpanded.media, libraryItemExpanded.createdAt, feed, slug, track, useChapterTitles))
|
||||||
|
}
|
||||||
|
Logger.info(`[FeedEpisode] Creating ${feedEpisodeObjs.length} episodes for feed ${feed.id}`)
|
||||||
|
return this.bulkCreate(feedEpisodeObjs, { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('./Collection')} collectionExpanded
|
||||||
|
* @param {import('./Feed')} feed
|
||||||
|
* @param {string} slug
|
||||||
|
* @param {import('sequelize').Transaction} transaction
|
||||||
|
* @returns {Promise<FeedEpisode[]>}
|
||||||
|
*/
|
||||||
|
static async createFromCollectionBooks(collectionExpanded, feed, slug, transaction) {
|
||||||
|
const booksWithTracks = collectionExpanded.books.filter((book) => book.includedAudioFiles.length)
|
||||||
|
|
||||||
|
const earliestLibraryItemCreatedAt = collectionExpanded.books.reduce((earliest, book) => {
|
||||||
|
return book.libraryItem.createdAt < earliest.libraryItem.createdAt ? book : earliest
|
||||||
|
}).libraryItem.createdAt
|
||||||
|
|
||||||
|
const feedEpisodeObjs = []
|
||||||
|
for (const book of booksWithTracks) {
|
||||||
|
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(book)
|
||||||
|
for (const track of book.trackList) {
|
||||||
|
feedEpisodeObjs.push(this.getFeedEpisodeObjFromAudiobookTrack(book, earliestLibraryItemCreatedAt, feed, slug, track, useChapterTitles))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Logger.info(`[FeedEpisode] Creating ${feedEpisodeObjs.length} episodes for feed ${feed.id}`)
|
Logger.info(`[FeedEpisode] Creating ${feedEpisodeObjs.length} episodes for feed ${feed.id}`)
|
||||||
return this.bulkCreate(feedEpisodeObjs, { transaction })
|
return this.bulkCreate(feedEpisodeObjs, { transaction })
|
||||||
|
@ -143,61 +143,6 @@ class Feed {
|
|||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
|
|
||||||
const feedUrl = `/feed/${slug}`
|
|
||||||
|
|
||||||
const itemsWithTracks = collectionExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length)
|
|
||||||
const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath)
|
|
||||||
|
|
||||||
this.id = uuidv4()
|
|
||||||
this.slug = slug
|
|
||||||
this.userId = userId
|
|
||||||
this.entityType = 'collection'
|
|
||||||
this.entityId = collectionExpanded.id
|
|
||||||
this.entityUpdatedAt = collectionExpanded.lastUpdate // This will be set to the most recently updated library item
|
|
||||||
this.coverPath = firstItemWithCover?.media.coverPath || null
|
|
||||||
this.serverAddress = serverAddress
|
|
||||||
this.feedUrl = feedUrl
|
|
||||||
|
|
||||||
const coverFileExtension = this.coverPath ? Path.extname(this.coverPath) : null
|
|
||||||
|
|
||||||
this.meta = new FeedMeta()
|
|
||||||
this.meta.title = collectionExpanded.name
|
|
||||||
this.meta.description = collectionExpanded.description || ''
|
|
||||||
this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
|
|
||||||
this.meta.imageUrl = this.coverPath ? `/feed/${slug}/cover${coverFileExtension}` : `/Logo.png`
|
|
||||||
this.meta.feedUrl = feedUrl
|
|
||||||
this.meta.link = `/collection/${collectionExpanded.id}`
|
|
||||||
this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit
|
|
||||||
this.meta.preventIndexing = preventIndexing
|
|
||||||
this.meta.ownerName = ownerName
|
|
||||||
this.meta.ownerEmail = ownerEmail
|
|
||||||
|
|
||||||
this.episodes = []
|
|
||||||
|
|
||||||
// Used for calculating pubdate
|
|
||||||
const earliestItemAddedAt = itemsWithTracks.reduce((earliest, item) => (item.addedAt < earliest ? item.addedAt : earliest), itemsWithTracks[0].addedAt)
|
|
||||||
|
|
||||||
itemsWithTracks.forEach((item, index) => {
|
|
||||||
if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
|
|
||||||
|
|
||||||
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
|
|
||||||
item.media.tracks.forEach((audioTrack) => {
|
|
||||||
const feedEpisode = new FeedEpisode()
|
|
||||||
|
|
||||||
// Offset pubdate to ensure correct order
|
|
||||||
let trackTimeOffset = isNaN(audioTrack.index) ? 0 : Number(audioTrack.index) * 1000 // Offset track
|
|
||||||
trackTimeOffset += index * 1000 // Offset item
|
|
||||||
const episodePubDateOverride = date.format(new Date(earliestItemAddedAt + trackTimeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
|
||||||
feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, useChapterTitles, episodePubDateOverride)
|
|
||||||
this.episodes.push(feedEpisode)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.createdAt = Date.now()
|
|
||||||
this.updatedAt = Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFromCollection(collectionExpanded) {
|
updateFromCollection(collectionExpanded) {
|
||||||
const itemsWithTracks = collectionExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length)
|
const itemsWithTracks = collectionExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length)
|
||||||
const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath)
|
const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath)
|
||||||
|
Loading…
Reference in New Issue
Block a user