mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Fix:Series and collection RSS feeds keeping correct order #3137
This commit is contained in:
		
							parent
							
								
									c5e60d30e1
								
							
						
					
					
						commit
						0ee3b89760
					
				| @ -1,7 +1,9 @@ | ||||
| const Path = require('path') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const uuidv4 = require('uuid').v4 | ||||
| const FeedMeta = require('./FeedMeta') | ||||
| const FeedEpisode = require('./FeedEpisode') | ||||
| 
 | ||||
| const date = require('../libs/dateAndTime') | ||||
| const RSS = require('../libs/rss') | ||||
| const { createNewSortInstance } = require('../libs/fastSort') | ||||
| const naturalSort = createNewSortInstance({ | ||||
| @ -46,7 +48,7 @@ class Feed { | ||||
|     this.serverAddress = feed.serverAddress | ||||
|     this.feedUrl = feed.feedUrl | ||||
|     this.meta = new FeedMeta(feed.meta) | ||||
|     this.episodes = feed.episodes.map(ep => new FeedEpisode(ep)) | ||||
|     this.episodes = feed.episodes.map((ep) => new FeedEpisode(ep)) | ||||
|     this.createdAt = feed.createdAt | ||||
|     this.updatedAt = feed.updatedAt | ||||
|   } | ||||
| @ -62,7 +64,7 @@ class Feed { | ||||
|       serverAddress: this.serverAddress, | ||||
|       feedUrl: this.feedUrl, | ||||
|       meta: this.meta.toJSON(), | ||||
|       episodes: this.episodes.map(ep => ep.toJSON()), | ||||
|       episodes: this.episodes.map((ep) => ep.toJSON()), | ||||
|       createdAt: this.createdAt, | ||||
|       updatedAt: this.updatedAt | ||||
|     } | ||||
| @ -74,12 +76,12 @@ class Feed { | ||||
|       entityType: this.entityType, | ||||
|       entityId: this.entityId, | ||||
|       feedUrl: this.feedUrl, | ||||
|       meta: this.meta.toJSONMinified(), | ||||
|       meta: this.meta.toJSONMinified() | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getEpisodePath(id) { | ||||
|     var episode = this.episodes.find(ep => ep.id === id) | ||||
|     var episode = this.episodes.find((ep) => ep.id === id) | ||||
|     if (!episode) return null | ||||
|     return episode.fullPath | ||||
|   } | ||||
| @ -137,7 +139,8 @@ class Feed { | ||||
|     this.meta.ownerEmail = ownerEmail | ||||
| 
 | ||||
|     this.episodes = [] | ||||
|     if (isPodcast) { // PODCAST EPISODES
 | ||||
|     if (isPodcast) { | ||||
|       // PODCAST EPISODES
 | ||||
|       media.episodes.forEach((episode) => { | ||||
|         if (episode.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = episode.updatedAt | ||||
| 
 | ||||
| @ -145,7 +148,8 @@ class Feed { | ||||
|         feedEpisode.setFromPodcastEpisode(libraryItem, serverAddress, slug, episode, this.meta) | ||||
|         this.episodes.push(feedEpisode) | ||||
|       }) | ||||
|     } else { // AUDIOBOOK EPISODES
 | ||||
|     } else { | ||||
|       // AUDIOBOOK EPISODES
 | ||||
|       const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItem) | ||||
|       media.tracks.forEach((audioTrack) => { | ||||
|         const feedEpisode = new FeedEpisode() | ||||
| @ -178,7 +182,8 @@ class Feed { | ||||
|     this.meta.language = mediaMetadata.language | ||||
| 
 | ||||
|     this.episodes = [] | ||||
|     if (isPodcast) { // PODCAST EPISODES
 | ||||
|     if (isPodcast) { | ||||
|       // PODCAST EPISODES
 | ||||
|       media.episodes.forEach((episode) => { | ||||
|         if (episode.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = episode.updatedAt | ||||
| 
 | ||||
| @ -186,7 +191,8 @@ class Feed { | ||||
|         feedEpisode.setFromPodcastEpisode(libraryItem, this.serverAddress, this.slug, episode, this.meta) | ||||
|         this.episodes.push(feedEpisode) | ||||
|       }) | ||||
|     } else { // AUDIOBOOK EPISODES
 | ||||
|     } else { | ||||
|       // AUDIOBOOK EPISODES
 | ||||
|       const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItem) | ||||
|       media.tracks.forEach((audioTrack) => { | ||||
|         const feedEpisode = new FeedEpisode() | ||||
| @ -202,8 +208,8 @@ class Feed { | ||||
|   setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) { | ||||
|     const feedUrl = `${serverAddress}/feed/${slug}` | ||||
| 
 | ||||
|     const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length) | ||||
|     const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath) | ||||
|     const itemsWithTracks = collectionExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length) | ||||
|     const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath) | ||||
| 
 | ||||
|     this.id = uuidv4() | ||||
|     this.slug = slug | ||||
| @ -224,20 +230,28 @@ class Feed { | ||||
|     this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${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.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() | ||||
|         feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, useChapterTitles, index) | ||||
| 
 | ||||
|         // 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) | ||||
|       }) | ||||
|     }) | ||||
| @ -247,8 +261,8 @@ class Feed { | ||||
|   } | ||||
| 
 | ||||
|   updateFromCollection(collectionExpanded) { | ||||
|     const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length) | ||||
|     const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath) | ||||
|     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 | ||||
| @ -259,17 +273,25 @@ class Feed { | ||||
|     this.meta.description = collectionExpanded.description || '' | ||||
|     this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) | ||||
|     this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png` | ||||
|     this.meta.explicit = !!itemsWithTracks.some(li => li.media.metadata.explicit) // explicit if any item is explicit
 | ||||
|     this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit
 | ||||
| 
 | ||||
|     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() | ||||
|         feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles, index) | ||||
| 
 | ||||
|         // 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, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles, episodePubDateOverride) | ||||
|         this.episodes.push(feedEpisode) | ||||
|       }) | ||||
|     }) | ||||
| @ -281,12 +303,12 @@ class Feed { | ||||
|   setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) { | ||||
|     const feedUrl = `${serverAddress}/feed/${slug}` | ||||
| 
 | ||||
|     let itemsWithTracks = seriesExpanded.books.filter(libraryItem => libraryItem.media.tracks.length) | ||||
|     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)) | ||||
|     itemsWithTracks = naturalSort(itemsWithTracks).asc((li) => li.media.metadata.getSeriesSequence(seriesExpanded.id)) | ||||
| 
 | ||||
|     const libraryId = itemsWithTracks[0].libraryId | ||||
|     const firstItemWithCover = itemsWithTracks.find(li => li.media.coverPath) | ||||
|     const firstItemWithCover = itemsWithTracks.find((li) => li.media.coverPath) | ||||
| 
 | ||||
|     this.id = uuidv4() | ||||
|     this.slug = slug | ||||
| @ -307,20 +329,28 @@ class Feed { | ||||
|     this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${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.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() | ||||
|         feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, useChapterTitles, index) | ||||
| 
 | ||||
|         // 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) | ||||
|       }) | ||||
|     }) | ||||
| @ -330,11 +360,11 @@ class Feed { | ||||
|   } | ||||
| 
 | ||||
|   updateFromSeries(seriesExpanded) { | ||||
|     let itemsWithTracks = seriesExpanded.books.filter(libraryItem => libraryItem.media.tracks.length) | ||||
|     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)) | ||||
|     itemsWithTracks = naturalSort(itemsWithTracks).asc((li) => li.media.metadata.getSeriesSequence(seriesExpanded.id)) | ||||
| 
 | ||||
|     const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath) | ||||
|     const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath) | ||||
| 
 | ||||
|     this.entityUpdatedAt = seriesExpanded.updatedAt | ||||
|     this.coverPath = firstItemWithCover?.coverPath || null | ||||
| @ -345,17 +375,25 @@ class Feed { | ||||
|     this.meta.description = seriesExpanded.description || '' | ||||
|     this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) | ||||
|     this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png` | ||||
|     this.meta.explicit = !!itemsWithTracks.some(li => li.media.metadata.explicit) // explicit if any item is explicit
 | ||||
|     this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit
 | ||||
| 
 | ||||
|     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() | ||||
|         feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles, index) | ||||
| 
 | ||||
|         // 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, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles, episodePubDateOverride) | ||||
|         this.episodes.push(feedEpisode) | ||||
|       }) | ||||
|     }) | ||||
| @ -377,7 +415,7 @@ class Feed { | ||||
| 
 | ||||
|   getAuthorsStringFromLibraryItems(libraryItems) { | ||||
|     let itemAuthors = [] | ||||
|     libraryItems.forEach((item) => itemAuthors.push(...item.media.metadata.authors.map(au => au.name))) | ||||
|     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) { | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| const Path = require('path') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const uuidv4 = require('uuid').v4 | ||||
| const date = require('../libs/dateAndTime') | ||||
| const { secondsToTimestamp } = require('../utils/index') | ||||
| 
 | ||||
| @ -105,20 +105,15 @@ class FeedEpisode { | ||||
|    * @param {import('../objects/files/AudioTrack')} audioTrack | ||||
|    * @param {Object} meta | ||||
|    * @param {boolean} useChapterTitles | ||||
|    * @param {number} [additionalOffset]  | ||||
|    * @param {string} [pubDateOverride] Used for series & collections to ensure correct episode order | ||||
|    */ | ||||
|   setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, useChapterTitles, additionalOffset = null) { | ||||
|   setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, useChapterTitles, pubDateOverride = null) { | ||||
|     // 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() | ||||
| 
 | ||||
|     // Additional offset can be used for collections/series
 | ||||
|     if (additionalOffset !== null && !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]') | ||||
|     const audiobookPubDate = pubDateOverride || date.format(new Date(libraryItem.addedAt + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]') | ||||
| 
 | ||||
|     const contentFileExtension = Path.extname(audioTrack.metadata.filename) | ||||
|     const contentUrl = `/feed/${slug}/item/${episodeId}/media${contentFileExtension}` | ||||
| @ -126,12 +121,13 @@ class FeedEpisode { | ||||
|     const mediaMetadata = media.metadata | ||||
| 
 | ||||
|     let title = audioTrack.title | ||||
|     if (libraryItem.media.tracks.length == 1) { // If audiobook is a single file, use book title instead of chapter/file title
 | ||||
|     if (libraryItem.media.tracks.length == 1) { | ||||
|       // If audiobook is a single file, use book title instead of chapter/file title
 | ||||
|       title = libraryItem.media.metadata.title | ||||
|     } else { | ||||
|       if (useChapterTitles) { | ||||
|         // If audio track start and chapter start are within 1 seconds of eachother then use the chapter title
 | ||||
|         const matchingChapter = libraryItem.media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1) | ||||
|         const matchingChapter = libraryItem.media.chapters.find((ch) => Math.abs(ch.start - audioTrack.startOffset) < 1) | ||||
|         if (matchingChapter?.title) title = matchingChapter.title | ||||
|       } | ||||
|     } | ||||
| @ -169,11 +165,11 @@ class FeedEpisode { | ||||
|         { 'itunes:duration': secondsToTimestamp(this.duration) }, | ||||
|         { 'itunes:summary': this.description || '' }, | ||||
|         { | ||||
|           "itunes:explicit": !!this.explicit | ||||
|           'itunes:explicit': !!this.explicit | ||||
|         }, | ||||
|         { "itunes:episodeType": this.episodeType }, | ||||
|         { "itunes:season": this.season }, | ||||
|         { "itunes:episode": this.episode } | ||||
|         { 'itunes:episodeType': this.episodeType }, | ||||
|         { 'itunes:season': this.season }, | ||||
|         { 'itunes:episode': this.episode } | ||||
|       ] | ||||
|     } | ||||
|   } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user