mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-12 17:08:33 +01:00
Add:RSS feed for series & cleanup empty series from db #1265
This commit is contained in:
parent
a364fe5031
commit
70ba2f7850
@ -51,6 +51,11 @@
|
||||
<div class="flex-grow" />
|
||||
<ui-checkbox v-if="!isBatchSelecting" v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
|
||||
|
||||
<!-- RSS feed -->
|
||||
<ui-tooltip v-if="seriesRssFeed" :text="$strings.LabelOpenRSSFeed" direction="top">
|
||||
<ui-icon-btn icon="rss_feed" class="mx-0.5" :size="7" icon-font-size="1.2rem" bg-color="success" outlined @click="showOpenSeriesRSSFeed" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
||||
</template>
|
||||
<!-- library & collections page -->
|
||||
@ -229,6 +234,9 @@ export default {
|
||||
seriesProgress() {
|
||||
return this.selectedSeries ? this.selectedSeries.progress : null
|
||||
},
|
||||
seriesRssFeed() {
|
||||
return this.selectedSeries ? this.selectedSeries.rssFeed : null
|
||||
},
|
||||
seriesLibraryItemIds() {
|
||||
if (!this.seriesProgress) return []
|
||||
return this.seriesProgress.libraryItemIds || []
|
||||
@ -253,7 +261,7 @@ export default {
|
||||
methods: {
|
||||
seriesContextMenuAction(action) {
|
||||
if (action === 'open-rss-feed') {
|
||||
// TODO: Open RSS Feed
|
||||
this.showOpenSeriesRSSFeed()
|
||||
} else if (action === 're-add-to-continue-listening') {
|
||||
if (this.processingSeries) {
|
||||
console.warn('Already processing series')
|
||||
@ -268,6 +276,14 @@ export default {
|
||||
this.markSeriesFinished()
|
||||
}
|
||||
},
|
||||
showOpenSeriesRSSFeed() {
|
||||
this.$store.commit('globals/setRSSFeedOpenCloseModal', {
|
||||
id: this.selectedSeries.id,
|
||||
name: this.selectedSeries.name,
|
||||
type: 'series',
|
||||
feed: this.selectedSeries.rssFeed
|
||||
})
|
||||
},
|
||||
reAddSeriesToContinueListening() {
|
||||
this.processingSeries = true
|
||||
this.$axios
|
||||
@ -396,16 +412,32 @@ export default {
|
||||
},
|
||||
setBookshelfTotalEntities(totalEntities) {
|
||||
this.totalEntities = totalEntities
|
||||
},
|
||||
rssFeedOpen(data) {
|
||||
if (data.entityId === this.seriesId) {
|
||||
console.log('RSS Feed Opened', data)
|
||||
this.selectedSeries.rssFeed = data
|
||||
}
|
||||
},
|
||||
rssFeedClosed(data) {
|
||||
if (data.entityId === this.seriesId) {
|
||||
console.log('RSS Feed Closed', data)
|
||||
this.selectedSeries.rssFeed = null
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||
this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||
this.$root.socket.on('rss_feed_open', this.rssFeedOpen)
|
||||
this.$root.socket.on('rss_feed_closed', this.rssFeedClosed)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||
this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||
this.$root.socket.off('rss_feed_open', this.rssFeedOpen)
|
||||
this.$root.socket.off('rss_feed_closed', this.rssFeedClosed)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -13,6 +13,8 @@
|
||||
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||
</div>
|
||||
|
||||
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
||||
|
||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
||||
@ -125,6 +127,9 @@ export default {
|
||||
isAlternativeBookshelfView() {
|
||||
const constants = this.$constants || this.$nuxt.$constants
|
||||
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||
},
|
||||
rssFeed() {
|
||||
return this.series ? this.series.rssFeed : null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -330,12 +330,6 @@ export default {
|
||||
}
|
||||
this.$store.commit('libraries/removeUserPlaylist', playlist)
|
||||
},
|
||||
rssFeedOpen(data) {
|
||||
console.log('RSS Feed Open', data)
|
||||
},
|
||||
rssFeedClosed(data) {
|
||||
console.log('RSS Feed Closed', data)
|
||||
},
|
||||
backupApplied() {
|
||||
// Force refresh
|
||||
location.reload()
|
||||
@ -425,10 +419,6 @@ export default {
|
||||
this.socket.on('task_started', this.taskStarted)
|
||||
this.socket.on('task_finished', this.taskFinished)
|
||||
|
||||
// Feed Listeners
|
||||
this.socket.on('rss_feed_open', this.rssFeedOpen)
|
||||
this.socket.on('rss_feed_closed', this.rssFeedClosed)
|
||||
|
||||
this.socket.on('backup_applied', this.backupApplied)
|
||||
|
||||
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
|
||||
|
@ -8,8 +8,8 @@
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ store, params, redirect, query, app }) {
|
||||
var libraryId = params.library
|
||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||
const libraryId = params.library
|
||||
const libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!libraryData) {
|
||||
return redirect('/oops?message=Library not found')
|
||||
}
|
||||
@ -19,7 +19,7 @@ export default {
|
||||
return redirect(`/library/${libraryId}`)
|
||||
}
|
||||
|
||||
var series = await app.$axios.$get(`/api/series/${params.id}?include=progress`).catch((error) => {
|
||||
const series = await app.$axios.$get(`/api/series/${params.id}?include=progress,rssfeed`).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return false
|
||||
})
|
||||
|
@ -124,6 +124,7 @@ class Server {
|
||||
|
||||
await this.backupManager.init()
|
||||
await this.logManager.init()
|
||||
await this.apiRouter.checkRemoveEmptySeries(this.db.series) // Remove empty series
|
||||
await this.rssFeedManager.init()
|
||||
this.cronManager.init()
|
||||
|
||||
|
@ -375,6 +375,9 @@ class LibraryController {
|
||||
// api/libraries/:id/series
|
||||
async getAllSeriesForLibrary(req, res) {
|
||||
const libraryItems = req.libraryItems
|
||||
|
||||
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||
|
||||
const payload = {
|
||||
results: [],
|
||||
total: 0,
|
||||
@ -383,7 +386,8 @@ class LibraryController {
|
||||
sortBy: req.query.sort,
|
||||
sortDesc: req.query.desc === '1',
|
||||
filterBy: req.query.filter,
|
||||
minified: req.query.minified === '1'
|
||||
minified: req.query.minified === '1',
|
||||
include: include.join(',')
|
||||
}
|
||||
|
||||
let series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, null, payload.filterBy, req.user, payload.minified)
|
||||
@ -408,10 +412,19 @@ class LibraryController {
|
||||
payload.total = series.length
|
||||
|
||||
if (payload.limit) {
|
||||
var startIndex = payload.page * payload.limit
|
||||
const startIndex = payload.page * payload.limit
|
||||
series = series.slice(startIndex, startIndex + payload.limit)
|
||||
}
|
||||
|
||||
// add rssFeed when "include=rssfeed" is in query string
|
||||
if (include.includes('rssfeed')) {
|
||||
series = series.map((se) => {
|
||||
const feedData = this.rssFeedManager.findFeedForEntityId(se.id)
|
||||
se.rssFeed = feedData?.toJSONMinified() || null
|
||||
return se
|
||||
})
|
||||
}
|
||||
|
||||
payload.results = series
|
||||
res.json(payload)
|
||||
}
|
||||
@ -442,7 +455,7 @@ class LibraryController {
|
||||
|
||||
if (include.includes('rssfeed')) {
|
||||
const feedData = this.rssFeedManager.findFeedForEntityId(c.id)
|
||||
expanded.rssFeed = feedData ? feedData.toJSONMinified() : null
|
||||
expanded.rssFeed = feedData?.toJSONMinified() || null
|
||||
}
|
||||
|
||||
return expanded
|
||||
|
@ -92,10 +92,23 @@ class LibraryItemController {
|
||||
}
|
||||
}
|
||||
|
||||
// Book specific - Get all series being removed from this item
|
||||
let seriesRemoved = []
|
||||
if (libraryItem.isBook && mediaPayload.metadata?.series) {
|
||||
const seriesIdsInUpdate = (mediaPayload.metadata?.series || []).map(se => se.id)
|
||||
seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id))
|
||||
}
|
||||
|
||||
const hasUpdates = libraryItem.media.update(mediaPayload)
|
||||
if (hasUpdates) {
|
||||
libraryItem.updatedAt = Date.now()
|
||||
|
||||
if (seriesRemoved.length) {
|
||||
// Check remove empty series
|
||||
Logger.debug(`[LibraryItemController] Series was removed from book. Check if series is now empty.`)
|
||||
await this.checkRemoveEmptySeries(seriesRemoved)
|
||||
}
|
||||
|
||||
if (isPodcastAutoDownloadUpdated) {
|
||||
this.cronManager.checkUpdatePodcastCron(libraryItem)
|
||||
}
|
||||
|
@ -75,6 +75,41 @@ class RSSFeedController {
|
||||
})
|
||||
}
|
||||
|
||||
// POST: api/feeds/series/:seriesId/open
|
||||
async openRSSFeedForSeries(req, res) {
|
||||
const options = req.body || {}
|
||||
|
||||
const series = this.db.series.find(se => se.id === req.params.seriesId)
|
||||
if (!series) 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 seriesJson = series.toJSON()
|
||||
// Get books in series that have audio tracks
|
||||
seriesJson.books = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
|
||||
// Check series has audio tracks
|
||||
if (!seriesJson.books.length) {
|
||||
Logger.error(`[RSSFeedController] Cannot open RSS feed for series "${seriesJson.name}" because it has no audio tracks`)
|
||||
return res.status(400).send('Series has no audio tracks')
|
||||
}
|
||||
|
||||
const feed = await this.rssFeedManager.openFeedForSeries(req.user, seriesJson, req.body)
|
||||
res.json({
|
||||
feed: feed.toJSONMinified()
|
||||
})
|
||||
}
|
||||
|
||||
// POST: api/feeds/:id/close
|
||||
async closeRSSFeed(req, res) {
|
||||
await this.rssFeedManager.closeRssFeed(req.params.id)
|
||||
|
@ -5,15 +5,15 @@ class SeriesController {
|
||||
constructor() { }
|
||||
|
||||
async findOne(req, res) {
|
||||
var include = (req.query.include || '').split(',')
|
||||
const include = (req.query.include || '').split(',').map(v => v.trim()).filter(v => !!v)
|
||||
|
||||
var seriesJson = req.series.toJSON()
|
||||
const seriesJson = req.series.toJSON()
|
||||
|
||||
// Add progress map with isFinished flag
|
||||
if (include.includes('progress')) {
|
||||
var libraryItemsInSeries = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(seriesJson.id))
|
||||
var libraryItemsFinished = libraryItemsInSeries.filter(li => {
|
||||
var mediaProgress = req.user.getMediaProgress(li.id)
|
||||
const libraryItemsInSeries = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(seriesJson.id))
|
||||
const libraryItemsFinished = libraryItemsInSeries.filter(li => {
|
||||
const mediaProgress = req.user.getMediaProgress(li.id)
|
||||
return mediaProgress && mediaProgress.isFinished
|
||||
})
|
||||
seriesJson.progress = {
|
||||
@ -23,6 +23,11 @@ class SeriesController {
|
||||
}
|
||||
}
|
||||
|
||||
if (include.includes('rssfeed')) {
|
||||
const feedObj = this.rssFeedManager.findFeedForEntityId(seriesJson.id)
|
||||
seriesJson.rssFeed = feedObj?.toJSONMinified() || null
|
||||
}
|
||||
|
||||
return res.json(seriesJson)
|
||||
}
|
||||
|
||||
@ -47,7 +52,7 @@ class SeriesController {
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
var series = this.db.series.find(se => se.id === req.params.id)
|
||||
const series = this.db.series.find(se => se.id === req.params.id)
|
||||
if (!series) return res.sendStatus(404)
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
|
@ -28,6 +28,13 @@ class RssFeedManager {
|
||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`)
|
||||
return false
|
||||
}
|
||||
} else if (feedObj.entityType === 'series') {
|
||||
const series = this.db.series.find(s => s.id === feedObj.entityId)
|
||||
const hasSeriesBook = this.db.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
if (!hasSeriesBook) {
|
||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found or has no audio tracks`)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Invalid entityType "${feedObj.entityType}"`)
|
||||
return false
|
||||
@ -73,6 +80,7 @@ class RssFeedManager {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if feed needs to be updated
|
||||
if (feed.entityType === 'item') {
|
||||
const libraryItem = this.db.getLibraryItem(feed.entityId)
|
||||
if (libraryItem && (!feed.entityUpdatedAt || libraryItem.updatedAt > feed.entityUpdatedAt)) {
|
||||
@ -100,6 +108,33 @@ class RssFeedManager {
|
||||
await this.db.updateEntity('feed', feed)
|
||||
}
|
||||
}
|
||||
} else if (feed.entityType === 'series') {
|
||||
const series = this.db.series.find(s => s.id === feed.entityId)
|
||||
if (series) {
|
||||
const seriesJson = series.toJSON()
|
||||
// Get books in series that have audio tracks
|
||||
seriesJson.books = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
|
||||
// Find most recently updated item in series
|
||||
let mostRecentlyUpdatedAt = seriesJson.updatedAt
|
||||
let totalTracks = 0 // Used to detect series items removed
|
||||
seriesJson.books.forEach((libraryItem) => {
|
||||
totalTracks += libraryItem.media.tracks.length
|
||||
if (libraryItem.media.tracks.length && libraryItem.updatedAt > mostRecentlyUpdatedAt) {
|
||||
mostRecentlyUpdatedAt = libraryItem.updatedAt
|
||||
}
|
||||
})
|
||||
if (totalTracks !== feed.episodes.length) {
|
||||
mostRecentlyUpdatedAt = Date.now()
|
||||
}
|
||||
|
||||
if (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt) {
|
||||
Logger.debug(`[RssFeedManager] Updating RSS feed for series "${seriesJson.name}"`)
|
||||
|
||||
feed.updateFromSeries(seriesJson)
|
||||
await this.db.updateEntity('feed', feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const xml = feed.buildXml()
|
||||
@ -170,6 +205,20 @@ class RssFeedManager {
|
||||
return feed
|
||||
}
|
||||
|
||||
async openFeedForSeries(user, seriesExpanded, options) {
|
||||
const serverAddress = options.serverAddress
|
||||
const slug = options.slug
|
||||
|
||||
const feed = new Feed()
|
||||
feed.setFromSeries(user.id, slug, seriesExpanded, 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 handleCloseFeed(feed) {
|
||||
if (!feed) return
|
||||
await this.db.removeEntity('feed', feed.id)
|
||||
|
@ -1,6 +1,10 @@
|
||||
const FeedMeta = require('./FeedMeta')
|
||||
const FeedEpisode = require('./FeedEpisode')
|
||||
const RSS = require('../libs/rss')
|
||||
const { createNewSortInstance } = require('../libs/fastSort')
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
})
|
||||
|
||||
class Feed {
|
||||
constructor(feed) {
|
||||
@ -226,6 +230,83 @@ class Feed {
|
||||
this.xml = null
|
||||
}
|
||||
|
||||
setFromSeries(userId, slug, seriesExpanded, serverAddress) {
|
||||
const feedUrl = `${serverAddress}/feed/${slug}`
|
||||
|
||||
let itemsWithTracks = seriesExpanded.books.filter(libraryItem => libraryItem.media.tracks.length)
|
||||
// Sort series items by series sequence
|
||||
itemsWithTracks = naturalSort(itemsWithTracks).asc(li => li.media.metadata.getSeriesSequence(seriesExpanded.id))
|
||||
|
||||
const libraryId = itemsWithTracks[0].libraryId
|
||||
const firstItemWithCover = itemsWithTracks.find(li => li.media.coverPath)
|
||||
|
||||
this.id = slug
|
||||
this.slug = slug
|
||||
this.userId = userId
|
||||
this.entityType = 'series'
|
||||
this.entityId = seriesExpanded.id
|
||||
this.entityUpdatedAt = seriesExpanded.updatedAt // This will be set to the most recently updated library item
|
||||
this.coverPath = firstItemWithCover?.coverPath || null
|
||||
this.serverAddress = serverAddress
|
||||
this.feedUrl = feedUrl
|
||||
|
||||
this.meta = new FeedMeta()
|
||||
this.meta.title = seriesExpanded.name
|
||||
this.meta.description = seriesExpanded.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}/library/${libraryId}/series/${seriesExpanded.id}`
|
||||
this.meta.explicit = !!itemsWithTracks.some(li => li.media.metadata.explicit) // explicit if any item is explicit
|
||||
|
||||
this.episodes = []
|
||||
|
||||
itemsWithTracks.forEach((item, index) => {
|
||||
if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
updateFromSeries(seriesExpanded) {
|
||||
let itemsWithTracks = seriesExpanded.books.filter(libraryItem => libraryItem.media.tracks.length)
|
||||
// Sort series items by series sequence
|
||||
itemsWithTracks = naturalSort(itemsWithTracks).asc(li => li.media.metadata.getSeriesSequence(seriesExpanded.id))
|
||||
|
||||
const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath)
|
||||
|
||||
this.entityUpdatedAt = seriesExpanded.updatedAt
|
||||
this.coverPath = firstItemWithCover?.coverPath || null
|
||||
|
||||
this.meta.title = seriesExpanded.name
|
||||
this.meta.description = seriesExpanded.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) => {
|
||||
if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
|
||||
|
||||
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
|
||||
|
||||
|
@ -175,7 +175,7 @@ class BookMetadata {
|
||||
return this.series.length ? this.series[0] : null
|
||||
}
|
||||
getSeriesSequence(seriesId) {
|
||||
var series = this.series.find(se => se.id == seriesId)
|
||||
const series = this.series.find(se => se.id == seriesId)
|
||||
if (!series) return null
|
||||
return series.sequence || ''
|
||||
}
|
||||
|
@ -268,6 +268,7 @@ class ApiRouter {
|
||||
//
|
||||
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/series/:seriesId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForSeries.bind(this))
|
||||
this.router.post('/feeds/:id/close', RSSFeedController.middleware.bind(this), RSSFeedController.closeRSSFeed.bind(this))
|
||||
|
||||
//
|
||||
@ -360,13 +361,18 @@ class ApiRouter {
|
||||
|
||||
// TODO: Remove open sessions for library item
|
||||
|
||||
// remove book from collections
|
||||
const collectionsWithBook = this.db.collections.filter(c => c.books.includes(libraryItem.id))
|
||||
for (let i = 0; i < collectionsWithBook.length; i++) {
|
||||
const collection = collectionsWithBook[i]
|
||||
collection.removeBook(libraryItem.id)
|
||||
await this.db.updateEntity('collection', collection)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
|
||||
if (libraryItem.isBook) {
|
||||
// remove book from collections
|
||||
const collectionsWithBook = this.db.collections.filter(c => c.books.includes(libraryItem.id))
|
||||
for (let i = 0; i < collectionsWithBook.length; i++) {
|
||||
const collection = collectionsWithBook[i]
|
||||
collection.removeBook(libraryItem.id)
|
||||
await this.db.updateEntity('collection', collection)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
|
||||
}
|
||||
|
||||
// Check remove empty series
|
||||
await this.checkRemoveEmptySeries(libraryItem.media.metadata.series, libraryItem.id)
|
||||
}
|
||||
|
||||
// remove item from playlists
|
||||
@ -398,6 +404,21 @@ class ApiRouter {
|
||||
SocketAuthority.emitter('item_removed', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
async checkRemoveEmptySeries(seriesToCheck, excludeLibraryItemId = null) {
|
||||
if (!seriesToCheck || !seriesToCheck.length) return
|
||||
|
||||
for (const series of seriesToCheck) {
|
||||
const otherLibraryItemsInSeries = this.db.libraryItems.filter(li => li.id !== excludeLibraryItemId && li.isBook && li.media.metadata.hasSeries(series.id))
|
||||
if (!otherLibraryItemsInSeries.length) {
|
||||
// Close open RSS feed for series
|
||||
await this.rssFeedManager.closeFeedForEntityId(series.id)
|
||||
Logger.debug(`[ApiRouter] Series "${series.name}" is now empty. Removing series`)
|
||||
await this.db.removeEntity('series', series.id)
|
||||
// TODO: Socket events for series?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getUserListeningSessionsHelper(userId) {
|
||||
const userSessions = await this.db.selectUserSessions(userId)
|
||||
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
|
@ -205,7 +205,7 @@ module.exports = {
|
||||
})
|
||||
})
|
||||
|
||||
var seriesItems = Object.values(_series)
|
||||
let seriesItems = Object.values(_series)
|
||||
|
||||
// check progress filter
|
||||
if (filterBy && filterBy.startsWith('progress.') && user) {
|
||||
@ -691,6 +691,11 @@ module.exports = {
|
||||
item.rssFeed = ctx.rssFeedManager.findFeedForEntityId(item.id)?.toJSONMinified() || null
|
||||
return item
|
||||
})
|
||||
} else if (shelf.type === 'series') {
|
||||
shelf.entities = shelf.entities.map((series) => {
|
||||
series.rssFeed = ctx.rssFeedManager.findFeedForEntityId(series.id)?.toJSONMinified() || null
|
||||
return series
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user