Add:Chapters to podcast episodes #1646

This commit is contained in:
advplyr 2023-04-09 14:32:51 -05:00
parent 5e5b674c17
commit 3dc9416da6
6 changed files with 49 additions and 22 deletions

View File

@ -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() {

View File

@ -12,6 +12,7 @@
<div class="flex justify-between pt-2 max-w-xl">
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
<p v-if="episode.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p>
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
</div>

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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))
}