mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
parent
e803dcd325
commit
061695f922
@ -41,6 +41,40 @@ class RSSFeedController {
|
||||
})
|
||||
}
|
||||
|
||||
// POST: api/feeds/collection/:collectionId/open
|
||||
async openRSSFeedForCollection(req, res) {
|
||||
const options = req.body || {}
|
||||
|
||||
const collection = this.db.collections.find(li => li.id === req.params.collectionId)
|
||||
if (!collection) return res.sendStatus(404)
|
||||
|
||||
// Check request body options exist
|
||||
if (!options.serverAddress || !options.slug) {
|
||||
Logger.error(`[RSSFeedController] Invalid request body to open RSS feed`)
|
||||
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)
|
||||
if (this.rssFeedManager.feeds[options.slug]) {
|
||||
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
||||
return res.status(400).send('Slug already in use')
|
||||
}
|
||||
|
||||
const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length)
|
||||
|
||||
// Check collection has audio tracks
|
||||
if (!collectionItemsWithTracks.length) {
|
||||
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')
|
||||
}
|
||||
|
||||
const feed = await this.rssFeedManager.openFeedForCollection(req.user, collectionExpanded, req.body)
|
||||
res.json({
|
||||
feed: feed.toJSONMinified()
|
||||
})
|
||||
}
|
||||
|
||||
// POST: api/feeds/:id/close
|
||||
async closeRSSFeed(req, res) {
|
||||
await this.rssFeedManager.closeRssFeed(req.params.id)
|
||||
|
@ -51,6 +51,18 @@ class RssFeedManager {
|
||||
feed.updateFromItem(libraryItem)
|
||||
await this.db.updateEntity('feed', feed)
|
||||
}
|
||||
} else if (feed.entityType === 'collection') {
|
||||
// TODO: Also trigger an update if any item in the collection was updated
|
||||
const collection = this.db.collections.find(c => c.id === feed.entityId)
|
||||
if (collection) {
|
||||
const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
if (!feed.entityUpdatedAt || collection.lastUpdate > feed.entityUpdatedAt) {
|
||||
Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`)
|
||||
|
||||
feed.updateFromCollection(collectionExpanded)
|
||||
await this.db.updateEntity('feed', feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const xml = feed.buildXml()
|
||||
@ -107,10 +119,18 @@ class RssFeedManager {
|
||||
return feed
|
||||
}
|
||||
|
||||
closeFeedForItem(libraryItemId) {
|
||||
const feed = this.findFeedForItem(libraryItemId)
|
||||
if (!feed) return
|
||||
return this.closeRssFeed(feed.id)
|
||||
async openFeedForCollection(user, collectionExpanded, options) {
|
||||
const serverAddress = options.serverAddress
|
||||
const slug = options.slug
|
||||
|
||||
const feed = new Feed()
|
||||
feed.setFromCollection(user.id, slug, collectionExpanded, serverAddress)
|
||||
this.feeds[feed.id] = feed
|
||||
|
||||
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
||||
await this.db.insertEntity('feed', feed)
|
||||
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
||||
return feed
|
||||
}
|
||||
|
||||
async closeRssFeed(id) {
|
||||
|
@ -1,4 +1,3 @@
|
||||
const Logger = require('../Logger')
|
||||
const { getId } = require('../utils/index')
|
||||
|
||||
class Collection {
|
||||
@ -46,6 +45,18 @@ class Collection {
|
||||
return json
|
||||
}
|
||||
|
||||
// Expanded and filtered out items not accessible to user
|
||||
toJSONExpandedForUser(user, libraryItems) {
|
||||
const json = this.toJSON()
|
||||
json.books = json.books.map(libraryItemId => {
|
||||
const libraryItem = libraryItems.find(li => li.id === libraryItemId)
|
||||
return libraryItem ? libraryItem.toJSONExpanded() : null
|
||||
}).filter(li => {
|
||||
return li && user.checkCanAccessLibraryItem(li)
|
||||
})
|
||||
return json
|
||||
}
|
||||
|
||||
construct(collection) {
|
||||
this.id = collection.id
|
||||
this.libraryId = collection.libraryId
|
||||
|
@ -129,6 +129,7 @@ class Feed {
|
||||
const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
|
||||
|
||||
this.entityUpdatedAt = libraryItem.updatedAt
|
||||
this.coverPath = media.coverPath || null
|
||||
|
||||
this.meta.title = mediaMetadata.title
|
||||
this.meta.description = mediaMetadata.description
|
||||
@ -155,6 +156,72 @@ class Feed {
|
||||
this.xml = null
|
||||
}
|
||||
|
||||
setFromCollection(userId, slug, collectionExpanded, serverAddress) {
|
||||
const feedUrl = `${serverAddress}/feed/${slug}`
|
||||
|
||||
const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length)
|
||||
const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath)
|
||||
|
||||
this.id = slug
|
||||
this.slug = slug
|
||||
this.userId = userId
|
||||
this.entityType = 'collection'
|
||||
this.entityId = collectionExpanded.id
|
||||
this.entityUpdatedAt = collectionExpanded.lastUpdate
|
||||
this.coverPath = firstItemWithCover?.coverPath || null
|
||||
this.serverAddress = serverAddress
|
||||
this.feedUrl = feedUrl
|
||||
|
||||
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 ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`
|
||||
this.meta.feedUrl = feedUrl
|
||||
this.meta.link = `${serverAddress}/collection/${collectionExpanded.id}`
|
||||
this.meta.explicit = !!itemsWithTracks.some(li => li.media.metadata.explicit) // explicit if any item is explicit
|
||||
|
||||
this.episodes = []
|
||||
|
||||
itemsWithTracks.forEach((item, index) => {
|
||||
item.media.tracks.forEach((audioTrack) => {
|
||||
const feedEpisode = new FeedEpisode()
|
||||
feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, index)
|
||||
this.episodes.push(feedEpisode)
|
||||
})
|
||||
})
|
||||
|
||||
this.createdAt = Date.now()
|
||||
this.updatedAt = Date.now()
|
||||
}
|
||||
|
||||
updateFromCollection(collectionExpanded) {
|
||||
const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length)
|
||||
const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath)
|
||||
|
||||
this.entityUpdatedAt = collectionExpanded.lastUpdate
|
||||
this.coverPath = firstItemWithCover?.coverPath || null
|
||||
|
||||
this.meta.title = collectionExpanded.name
|
||||
this.meta.description = collectionExpanded.description || ''
|
||||
this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
|
||||
this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover` : `${this.serverAddress}/Logo.png`
|
||||
this.meta.explicit = !!itemsWithTracks.some(li => li.media.metadata.explicit) // explicit if any item is explicit
|
||||
|
||||
this.episodes = []
|
||||
|
||||
itemsWithTracks.forEach((item, index) => {
|
||||
item.media.tracks.forEach((audioTrack) => {
|
||||
const feedEpisode = new FeedEpisode()
|
||||
feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, index)
|
||||
this.episodes.push(feedEpisode)
|
||||
})
|
||||
})
|
||||
|
||||
this.updatedAt = Date.now()
|
||||
this.xml = null
|
||||
}
|
||||
|
||||
buildXml() {
|
||||
if (this.xml) return this.xml
|
||||
|
||||
@ -165,5 +232,16 @@ class Feed {
|
||||
this.xml = rssfeed.xml()
|
||||
return this.xml
|
||||
}
|
||||
|
||||
getAuthorsStringFromLibraryItems(libraryItems) {
|
||||
let itemAuthors = []
|
||||
libraryItems.forEach((item) => itemAuthors.push(...item.media.metadata.authors.map(au => au.name)))
|
||||
itemAuthors = [...new Set(itemAuthors)] // Filter out dupes
|
||||
let author = itemAuthors.slice(0, 3).join(', ')
|
||||
if (itemAuthors.length > 3) {
|
||||
author += ' & more'
|
||||
}
|
||||
return author
|
||||
}
|
||||
}
|
||||
module.exports = Feed
|
@ -83,9 +83,15 @@ class FeedEpisode {
|
||||
this.fullPath = episode.audioFile.metadata.path
|
||||
}
|
||||
|
||||
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta) {
|
||||
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, additionalOffset = 0) {
|
||||
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
||||
const 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
|
||||
|
||||
// Additional offset can be used for collections/series
|
||||
if (additionalOffset && !isNaN(additionalOffset)) {
|
||||
timeOffset += Number(additionalOffset) * 1000
|
||||
}
|
||||
|
||||
// e.g. Track 1 will have a pub date before Track 2
|
||||
const audiobookPubDate = date.format(new Date(libraryItem.addedAt + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
||||
|
||||
|
@ -267,6 +267,7 @@ class ApiRouter {
|
||||
// RSS Feed Routes (Admin and up)
|
||||
//
|
||||
this.router.post('/feeds/item/:itemId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForItem.bind(this))
|
||||
this.router.post('/feeds/collection/:collectionId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForCollection.bind(this))
|
||||
this.router.post('/feeds/:id/close', RSSFeedController.middleware.bind(this), RSSFeedController.closeRSSFeed.bind(this))
|
||||
|
||||
//
|
||||
|
Loading…
Reference in New Issue
Block a user