diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index baecbde4..cd2bd1cf 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -120,17 +120,22 @@ export default { streamLibraryItem() { return this.$store.state.streamLibraryItem }, + streamEpisode() { + if (!this.$store.state.streamEpisodeId) return null + const episodes = this.streamLibraryItem.media.episodes || [] + return episodes.find((ep) => ep.id === this.$store.state.streamEpisodeId) + }, libraryItemId() { - return this.streamLibraryItem ? this.streamLibraryItem.id : null + return this.streamLibraryItem?.id || null }, media() { - return this.streamLibraryItem ? this.streamLibraryItem.media || {} : {} + return this.streamLibraryItem?.media || {} }, isPodcast() { - return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'podcast' : false + return this.streamLibraryItem?.mediaType === 'podcast' }, isMusic() { - return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'music' : false + return this.streamLibraryItem?.mediaType === 'music' }, isExplicit() { return this.mediaMetadata.explicit || false @@ -139,6 +144,7 @@ export default { return this.media.metadata || {} }, chapters() { + if (this.streamEpisode) return this.streamEpisode.chapters || [] return this.media.chapters || [] }, title() { diff --git a/client/components/tables/podcast/EpisodeTableRow.vue b/client/components/tables/podcast/EpisodeTableRow.vue index 3012faf4..a5b85e6e 100644 --- a/client/components/tables/podcast/EpisodeTableRow.vue +++ b/client/components/tables/podcast/EpisodeTableRow.vue @@ -12,6 +12,7 @@

Season #{{ episode.season }}

Episode #{{ episode.episode }}

+

{{ episode.chapters.length }} Chapters

Published {{ $formatDate(publishedAt, dateFormat) }}

diff --git a/client/players/PlayerHandler.js b/client/players/PlayerHandler.js index 1e591069..2fb188e8 100644 --- a/client/players/PlayerHandler.js +++ b/client/players/PlayerHandler.js @@ -123,7 +123,7 @@ export default class PlayerHandler { playerError() { // Switch to HLS stream on error - if (!this.isCasting && !this.currentStreamId && (this.player instanceof LocalAudioPlayer)) { + if (!this.isCasting && (this.player instanceof LocalAudioPlayer)) { console.log(`[PlayerHandler] Audio player error switching to HLS stream`) this.prepare(true) } @@ -183,6 +183,8 @@ export default class PlayerHandler { } async prepare(forceTranscode = false) { + this.currentSessionId = null // Reset session + const payload = { deviceInfo: { deviceId: this.getDeviceId() @@ -260,6 +262,7 @@ export default class PlayerHandler { this.player = null this.playerState = 'IDLE' this.libraryItem = null + this.currentSessionId = null this.startTime = 0 this.stopPlayInterval() } diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index e60238cc..d50bc7bb 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -53,15 +53,15 @@ class PodcastManager { } async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) { - var index = libraryItem.media.episodes.length + 1 - episodesToDownload.forEach((ep) => { - var newPe = new PodcastEpisode() + let index = libraryItem.media.episodes.length + 1 + for (const ep of episodesToDownload) { + const newPe = new PodcastEpisode() newPe.setData(ep, index++) newPe.libraryItemId = libraryItem.id - var newPeDl = new PodcastEpisodeDownload() + const newPeDl = new PodcastEpisodeDownload() newPeDl.setData(newPe, libraryItem, isAutoDownload, libraryItem.libraryId) this.startPodcastEpisodeDownload(newPeDl) - }) + } } async startPodcastEpisodeDownload(podcastEpisodeDownload) { @@ -94,7 +94,6 @@ class PodcastManager { await filePerms.setDefault(this.currentDownload.libraryItem.path) } - let success = false if (this.currentDownload.urlFileExtension === 'mp3') { // Download episode and tag it @@ -156,6 +155,11 @@ class PodcastManager { const podcastEpisode = this.currentDownload.podcastEpisode podcastEpisode.audioFile = audioFile + + if (audioFile.chapters?.length) { + podcastEpisode.chapters = audioFile.chapters.map(ch => ({ ...ch })) + } + libraryItem.media.addPodcastEpisode(podcastEpisode) if (libraryItem.isInvalid) { // First episode added to an empty podcast @@ -214,13 +218,13 @@ class PodcastManager { } async probeAudioFile(libraryFile) { - var path = libraryFile.metadata.path - var mediaProbeData = await prober.probe(path) + const path = libraryFile.metadata.path + const mediaProbeData = await prober.probe(path) if (mediaProbeData.error) { Logger.error(`[PodcastManager] Podcast Episode downloaded but failed to probe "${path}"`, mediaProbeData.error) return false } - var newAudioFile = new AudioFile() + const newAudioFile = new AudioFile() newAudioFile.setDataFromProbe(libraryFile, mediaProbeData) return newAudioFile } diff --git a/server/objects/entities/PodcastEpisode.js b/server/objects/entities/PodcastEpisode.js index 80e66611..394a0bea 100644 --- a/server/objects/entities/PodcastEpisode.js +++ b/server/objects/entities/PodcastEpisode.js @@ -1,6 +1,6 @@ const Path = require('path') const Logger = require('../../Logger') -const { getId, cleanStringForSearch } = require('../../utils/index') +const { getId, cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index') const AudioFile = require('../files/AudioFile') const AudioTrack = require('../files/AudioTrack') @@ -18,6 +18,7 @@ class PodcastEpisode { this.description = null this.enclosure = null this.pubDate = null + this.chapters = [] this.audioFile = null this.publishedAt = null @@ -41,6 +42,7 @@ class PodcastEpisode { this.description = episode.description this.enclosure = episode.enclosure ? { ...episode.enclosure } : null this.pubDate = episode.pubDate + this.chapters = episode.chapters?.map(ch => ({ ...ch })) || [] this.audioFile = new AudioFile(episode.audioFile) this.publishedAt = episode.publishedAt this.addedAt = episode.addedAt @@ -62,6 +64,7 @@ class PodcastEpisode { description: this.description, enclosure: this.enclosure ? { ...this.enclosure } : null, pubDate: this.pubDate, + chapters: this.chapters.map(ch => ({ ...ch })), audioFile: this.audioFile.toJSON(), publishedAt: this.publishedAt, addedAt: this.addedAt, @@ -82,6 +85,7 @@ class PodcastEpisode { description: this.description, enclosure: this.enclosure ? { ...this.enclosure } : null, pubDate: this.pubDate, + chapters: this.chapters.map(ch => ({ ...ch })), audioFile: this.audioFile.toJSON(), audioTrack: this.audioTrack.toJSON(), publishedAt: this.publishedAt, @@ -136,6 +140,7 @@ class PodcastEpisode { this.setDataFromAudioMetaTags(audioFile.metaTags, true) + this.chapters = audioFile.chapters?.map((c) => ({ ...c })) this.addedAt = Date.now() this.updatedAt = Date.now() } @@ -143,8 +148,8 @@ class PodcastEpisode { update(payload) { let hasUpdates = false for (const key in this.toJSON()) { - if (payload[key] != undefined && payload[key] != this[key]) { - this[key] = payload[key] + if (payload[key] != undefined && !areEquivalent(payload[key], this[key])) { + this[key] = copyValue(payload[key]) hasUpdates = true } } diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index d59a3ec4..553ad7d1 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -74,12 +74,12 @@ function extractPodcastMetadata(channel) { function extractEpisodeData(item) { // Episode must have url - if (!item.enclosure || !item.enclosure.length || !item.enclosure[0]['$'] || !item.enclosure[0]['$'].url) { + if (!item.enclosure?.[0]?.['$']?.url) { Logger.error(`[podcastUtils] Invalid podcast episode data`) return null } - var episode = { + const episode = { enclosure: { ...item.enclosure[0]['$'] } @@ -91,6 +91,12 @@ function extractEpisodeData(item) { episode.description = htmlSanitizer.sanitize(rawDescription) } + // Extract chapters + if (item['podcast:chapters']?.[0]?.['$']?.url) { + episode.chaptersUrl = item['podcast:chapters'][0]['$'].url + episode.chaptersType = item['podcast:chapters'][0]['$'].type || 'application/json' + } + // Supposed to be the plaintext description but not always followed if (item['description']) { const rawDescription = extractFirstArrayItem(item, 'description') || '' @@ -133,14 +139,16 @@ function cleanEpisodeData(data) { duration: data.duration || '', explicit: data.explicit || '', publishedAt, - enclosure: data.enclosure + enclosure: data.enclosure, + chaptersUrl: data.chaptersUrl || null, + chaptersType: data.chaptersType || null } } function extractPodcastEpisodes(items) { - var episodes = [] + const episodes = [] items.forEach((item) => { - var extracted = extractEpisodeData(item) + const extracted = extractEpisodeData(item) if (extracted) { episodes.push(cleanEpisodeData(extracted)) }