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 Path = require('path')
 | 
				
			||||||
const uuidv4 = require("uuid").v4
 | 
					const uuidv4 = require('uuid').v4
 | 
				
			||||||
const FeedMeta = require('./FeedMeta')
 | 
					const FeedMeta = require('./FeedMeta')
 | 
				
			||||||
const FeedEpisode = require('./FeedEpisode')
 | 
					const FeedEpisode = require('./FeedEpisode')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const date = require('../libs/dateAndTime')
 | 
				
			||||||
const RSS = require('../libs/rss')
 | 
					const RSS = require('../libs/rss')
 | 
				
			||||||
const { createNewSortInstance } = require('../libs/fastSort')
 | 
					const { createNewSortInstance } = require('../libs/fastSort')
 | 
				
			||||||
const naturalSort = createNewSortInstance({
 | 
					const naturalSort = createNewSortInstance({
 | 
				
			||||||
@ -46,7 +48,7 @@ class Feed {
 | 
				
			|||||||
    this.serverAddress = feed.serverAddress
 | 
					    this.serverAddress = feed.serverAddress
 | 
				
			||||||
    this.feedUrl = feed.feedUrl
 | 
					    this.feedUrl = feed.feedUrl
 | 
				
			||||||
    this.meta = new FeedMeta(feed.meta)
 | 
					    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.createdAt = feed.createdAt
 | 
				
			||||||
    this.updatedAt = feed.updatedAt
 | 
					    this.updatedAt = feed.updatedAt
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -62,7 +64,7 @@ class Feed {
 | 
				
			|||||||
      serverAddress: this.serverAddress,
 | 
					      serverAddress: this.serverAddress,
 | 
				
			||||||
      feedUrl: this.feedUrl,
 | 
					      feedUrl: this.feedUrl,
 | 
				
			||||||
      meta: this.meta.toJSON(),
 | 
					      meta: this.meta.toJSON(),
 | 
				
			||||||
      episodes: this.episodes.map(ep => ep.toJSON()),
 | 
					      episodes: this.episodes.map((ep) => ep.toJSON()),
 | 
				
			||||||
      createdAt: this.createdAt,
 | 
					      createdAt: this.createdAt,
 | 
				
			||||||
      updatedAt: this.updatedAt
 | 
					      updatedAt: this.updatedAt
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -74,12 +76,12 @@ class Feed {
 | 
				
			|||||||
      entityType: this.entityType,
 | 
					      entityType: this.entityType,
 | 
				
			||||||
      entityId: this.entityId,
 | 
					      entityId: this.entityId,
 | 
				
			||||||
      feedUrl: this.feedUrl,
 | 
					      feedUrl: this.feedUrl,
 | 
				
			||||||
      meta: this.meta.toJSONMinified(),
 | 
					      meta: this.meta.toJSONMinified()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getEpisodePath(id) {
 | 
					  getEpisodePath(id) {
 | 
				
			||||||
    var episode = this.episodes.find(ep => ep.id === id)
 | 
					    var episode = this.episodes.find((ep) => ep.id === id)
 | 
				
			||||||
    if (!episode) return null
 | 
					    if (!episode) return null
 | 
				
			||||||
    return episode.fullPath
 | 
					    return episode.fullPath
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -137,7 +139,8 @@ class Feed {
 | 
				
			|||||||
    this.meta.ownerEmail = ownerEmail
 | 
					    this.meta.ownerEmail = ownerEmail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.episodes = []
 | 
					    this.episodes = []
 | 
				
			||||||
    if (isPodcast) { // PODCAST EPISODES
 | 
					    if (isPodcast) {
 | 
				
			||||||
 | 
					      // PODCAST EPISODES
 | 
				
			||||||
      media.episodes.forEach((episode) => {
 | 
					      media.episodes.forEach((episode) => {
 | 
				
			||||||
        if (episode.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = episode.updatedAt
 | 
					        if (episode.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = episode.updatedAt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -145,7 +148,8 @@ class Feed {
 | 
				
			|||||||
        feedEpisode.setFromPodcastEpisode(libraryItem, serverAddress, slug, episode, this.meta)
 | 
					        feedEpisode.setFromPodcastEpisode(libraryItem, serverAddress, slug, episode, this.meta)
 | 
				
			||||||
        this.episodes.push(feedEpisode)
 | 
					        this.episodes.push(feedEpisode)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    } else { // AUDIOBOOK EPISODES
 | 
					    } else {
 | 
				
			||||||
 | 
					      // AUDIOBOOK EPISODES
 | 
				
			||||||
      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItem)
 | 
					      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItem)
 | 
				
			||||||
      media.tracks.forEach((audioTrack) => {
 | 
					      media.tracks.forEach((audioTrack) => {
 | 
				
			||||||
        const feedEpisode = new FeedEpisode()
 | 
					        const feedEpisode = new FeedEpisode()
 | 
				
			||||||
@ -178,7 +182,8 @@ class Feed {
 | 
				
			|||||||
    this.meta.language = mediaMetadata.language
 | 
					    this.meta.language = mediaMetadata.language
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.episodes = []
 | 
					    this.episodes = []
 | 
				
			||||||
    if (isPodcast) { // PODCAST EPISODES
 | 
					    if (isPodcast) {
 | 
				
			||||||
 | 
					      // PODCAST EPISODES
 | 
				
			||||||
      media.episodes.forEach((episode) => {
 | 
					      media.episodes.forEach((episode) => {
 | 
				
			||||||
        if (episode.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = episode.updatedAt
 | 
					        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)
 | 
					        feedEpisode.setFromPodcastEpisode(libraryItem, this.serverAddress, this.slug, episode, this.meta)
 | 
				
			||||||
        this.episodes.push(feedEpisode)
 | 
					        this.episodes.push(feedEpisode)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    } else { // AUDIOBOOK EPISODES
 | 
					    } else {
 | 
				
			||||||
 | 
					      // AUDIOBOOK EPISODES
 | 
				
			||||||
      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItem)
 | 
					      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItem)
 | 
				
			||||||
      media.tracks.forEach((audioTrack) => {
 | 
					      media.tracks.forEach((audioTrack) => {
 | 
				
			||||||
        const feedEpisode = new FeedEpisode()
 | 
					        const feedEpisode = new FeedEpisode()
 | 
				
			||||||
@ -202,8 +208,8 @@ class Feed {
 | 
				
			|||||||
  setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
 | 
					  setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
 | 
				
			||||||
    const feedUrl = `${serverAddress}/feed/${slug}`
 | 
					    const feedUrl = `${serverAddress}/feed/${slug}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.id = uuidv4()
 | 
					    this.id = uuidv4()
 | 
				
			||||||
    this.slug = slug
 | 
					    this.slug = slug
 | 
				
			||||||
@ -224,20 +230,28 @@ class Feed {
 | 
				
			|||||||
    this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png`
 | 
					    this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png`
 | 
				
			||||||
    this.meta.feedUrl = feedUrl
 | 
					    this.meta.feedUrl = feedUrl
 | 
				
			||||||
    this.meta.link = `${serverAddress}/collection/${collectionExpanded.id}`
 | 
					    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.preventIndexing = preventIndexing
 | 
				
			||||||
    this.meta.ownerName = ownerName
 | 
					    this.meta.ownerName = ownerName
 | 
				
			||||||
    this.meta.ownerEmail = ownerEmail
 | 
					    this.meta.ownerEmail = ownerEmail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.episodes = []
 | 
					    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) => {
 | 
					    itemsWithTracks.forEach((item, index) => {
 | 
				
			||||||
      if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
 | 
					      if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
 | 
					      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
 | 
				
			||||||
      item.media.tracks.forEach((audioTrack) => {
 | 
					      item.media.tracks.forEach((audioTrack) => {
 | 
				
			||||||
        const feedEpisode = new FeedEpisode()
 | 
					        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)
 | 
					        this.episodes.push(feedEpisode)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
@ -247,8 +261,8 @@ class Feed {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.entityUpdatedAt = collectionExpanded.lastUpdate
 | 
					    this.entityUpdatedAt = collectionExpanded.lastUpdate
 | 
				
			||||||
    this.coverPath = firstItemWithCover?.coverPath || null
 | 
					    this.coverPath = firstItemWithCover?.coverPath || null
 | 
				
			||||||
@ -259,17 +273,25 @@ class Feed {
 | 
				
			|||||||
    this.meta.description = collectionExpanded.description || ''
 | 
					    this.meta.description = collectionExpanded.description || ''
 | 
				
			||||||
    this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
 | 
					    this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
 | 
				
			||||||
    this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png`
 | 
					    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 = []
 | 
					    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) => {
 | 
					    itemsWithTracks.forEach((item, index) => {
 | 
				
			||||||
      if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
 | 
					      if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
 | 
					      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
 | 
				
			||||||
      item.media.tracks.forEach((audioTrack) => {
 | 
					      item.media.tracks.forEach((audioTrack) => {
 | 
				
			||||||
        const feedEpisode = new FeedEpisode()
 | 
					        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)
 | 
					        this.episodes.push(feedEpisode)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
@ -281,12 +303,12 @@ class Feed {
 | 
				
			|||||||
  setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
 | 
					  setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
 | 
				
			||||||
    const feedUrl = `${serverAddress}/feed/${slug}`
 | 
					    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
 | 
					    // 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 libraryId = itemsWithTracks[0].libraryId
 | 
				
			||||||
    const firstItemWithCover = itemsWithTracks.find(li => li.media.coverPath)
 | 
					    const firstItemWithCover = itemsWithTracks.find((li) => li.media.coverPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.id = uuidv4()
 | 
					    this.id = uuidv4()
 | 
				
			||||||
    this.slug = slug
 | 
					    this.slug = slug
 | 
				
			||||||
@ -307,20 +329,28 @@ class Feed {
 | 
				
			|||||||
    this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png`
 | 
					    this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png`
 | 
				
			||||||
    this.meta.feedUrl = feedUrl
 | 
					    this.meta.feedUrl = feedUrl
 | 
				
			||||||
    this.meta.link = `${serverAddress}/library/${libraryId}/series/${seriesExpanded.id}`
 | 
					    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.preventIndexing = preventIndexing
 | 
				
			||||||
    this.meta.ownerName = ownerName
 | 
					    this.meta.ownerName = ownerName
 | 
				
			||||||
    this.meta.ownerEmail = ownerEmail
 | 
					    this.meta.ownerEmail = ownerEmail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.episodes = []
 | 
					    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) => {
 | 
					    itemsWithTracks.forEach((item, index) => {
 | 
				
			||||||
      if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
 | 
					      if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
 | 
					      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
 | 
				
			||||||
      item.media.tracks.forEach((audioTrack) => {
 | 
					      item.media.tracks.forEach((audioTrack) => {
 | 
				
			||||||
        const feedEpisode = new FeedEpisode()
 | 
					        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)
 | 
					        this.episodes.push(feedEpisode)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
@ -330,11 +360,11 @@ class Feed {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateFromSeries(seriesExpanded) {
 | 
					  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
 | 
					    // 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.entityUpdatedAt = seriesExpanded.updatedAt
 | 
				
			||||||
    this.coverPath = firstItemWithCover?.coverPath || null
 | 
					    this.coverPath = firstItemWithCover?.coverPath || null
 | 
				
			||||||
@ -345,17 +375,25 @@ class Feed {
 | 
				
			|||||||
    this.meta.description = seriesExpanded.description || ''
 | 
					    this.meta.description = seriesExpanded.description || ''
 | 
				
			||||||
    this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
 | 
					    this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
 | 
				
			||||||
    this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png`
 | 
					    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 = []
 | 
					    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) => {
 | 
					    itemsWithTracks.forEach((item, index) => {
 | 
				
			||||||
      if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
 | 
					      if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
 | 
					      const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
 | 
				
			||||||
      item.media.tracks.forEach((audioTrack) => {
 | 
					      item.media.tracks.forEach((audioTrack) => {
 | 
				
			||||||
        const feedEpisode = new FeedEpisode()
 | 
					        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)
 | 
					        this.episodes.push(feedEpisode)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
@ -377,7 +415,7 @@ class Feed {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  getAuthorsStringFromLibraryItems(libraryItems) {
 | 
					  getAuthorsStringFromLibraryItems(libraryItems) {
 | 
				
			||||||
    let itemAuthors = []
 | 
					    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
 | 
					    itemAuthors = [...new Set(itemAuthors)] // Filter out dupes
 | 
				
			||||||
    let author = itemAuthors.slice(0, 3).join(', ')
 | 
					    let author = itemAuthors.slice(0, 3).join(', ')
 | 
				
			||||||
    if (itemAuthors.length > 3) {
 | 
					    if (itemAuthors.length > 3) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
const Path = require('path')
 | 
					const Path = require('path')
 | 
				
			||||||
const uuidv4 = require("uuid").v4
 | 
					const uuidv4 = require('uuid').v4
 | 
				
			||||||
const date = require('../libs/dateAndTime')
 | 
					const date = require('../libs/dateAndTime')
 | 
				
			||||||
const { secondsToTimestamp } = require('../utils/index')
 | 
					const { secondsToTimestamp } = require('../utils/index')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -105,20 +105,15 @@ class FeedEpisode {
 | 
				
			|||||||
   * @param {import('../objects/files/AudioTrack')} audioTrack
 | 
					   * @param {import('../objects/files/AudioTrack')} audioTrack
 | 
				
			||||||
   * @param {Object} meta
 | 
					   * @param {Object} meta
 | 
				
			||||||
   * @param {boolean} useChapterTitles
 | 
					   * @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>
 | 
					    // 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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 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
 | 
					    // 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 contentFileExtension = Path.extname(audioTrack.metadata.filename)
 | 
				
			||||||
    const contentUrl = `/feed/${slug}/item/${episodeId}/media${contentFileExtension}`
 | 
					    const contentUrl = `/feed/${slug}/item/${episodeId}/media${contentFileExtension}`
 | 
				
			||||||
@ -126,12 +121,13 @@ class FeedEpisode {
 | 
				
			|||||||
    const mediaMetadata = media.metadata
 | 
					    const mediaMetadata = media.metadata
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let title = audioTrack.title
 | 
					    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
 | 
					      title = libraryItem.media.metadata.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 = 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
 | 
					        if (matchingChapter?.title) title = matchingChapter.title
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -169,11 +165,11 @@ class FeedEpisode {
 | 
				
			|||||||
        { 'itunes:duration': secondsToTimestamp(this.duration) },
 | 
					        { 'itunes:duration': secondsToTimestamp(this.duration) },
 | 
				
			||||||
        { 'itunes:summary': this.description || '' },
 | 
					        { 'itunes:summary': this.description || '' },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          "itunes:explicit": !!this.explicit
 | 
					          'itunes:explicit': !!this.explicit
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        { "itunes:episodeType": this.episodeType },
 | 
					        { 'itunes:episodeType': this.episodeType },
 | 
				
			||||||
        { "itunes:season": this.season },
 | 
					        { 'itunes:season': this.season },
 | 
				
			||||||
        { "itunes:episode": this.episode }
 | 
					        { 'itunes:episode': this.episode }
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user