mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-05-31 20:25:34 -04:00
Add:Chapters to podcast episodes #1646
This commit is contained in:
parent
5e5b674c17
commit
3dc9416da6
@ -120,17 +120,22 @@ export default {
|
|||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.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() {
|
libraryItemId() {
|
||||||
return this.streamLibraryItem ? this.streamLibraryItem.id : null
|
return this.streamLibraryItem?.id || null
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.streamLibraryItem ? this.streamLibraryItem.media || {} : {}
|
return this.streamLibraryItem?.media || {}
|
||||||
},
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'podcast' : false
|
return this.streamLibraryItem?.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isMusic() {
|
isMusic() {
|
||||||
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'music' : false
|
return this.streamLibraryItem?.mediaType === 'music'
|
||||||
},
|
},
|
||||||
isExplicit() {
|
isExplicit() {
|
||||||
return this.mediaMetadata.explicit || false
|
return this.mediaMetadata.explicit || false
|
||||||
@ -139,6 +144,7 @@ export default {
|
|||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
chapters() {
|
chapters() {
|
||||||
|
if (this.streamEpisode) return this.streamEpisode.chapters || []
|
||||||
return this.media.chapters || []
|
return this.media.chapters || []
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
<div class="flex justify-between pt-2 max-w-xl">
|
<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.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.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>
|
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
playerError() {
|
playerError() {
|
||||||
// Switch to HLS stream on error
|
// 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`)
|
console.log(`[PlayerHandler] Audio player error switching to HLS stream`)
|
||||||
this.prepare(true)
|
this.prepare(true)
|
||||||
}
|
}
|
||||||
@ -183,6 +183,8 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async prepare(forceTranscode = false) {
|
async prepare(forceTranscode = false) {
|
||||||
|
this.currentSessionId = null // Reset session
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
deviceId: this.getDeviceId()
|
deviceId: this.getDeviceId()
|
||||||
@ -260,6 +262,7 @@ export default class PlayerHandler {
|
|||||||
this.player = null
|
this.player = null
|
||||||
this.playerState = 'IDLE'
|
this.playerState = 'IDLE'
|
||||||
this.libraryItem = null
|
this.libraryItem = null
|
||||||
|
this.currentSessionId = null
|
||||||
this.startTime = 0
|
this.startTime = 0
|
||||||
this.stopPlayInterval()
|
this.stopPlayInterval()
|
||||||
}
|
}
|
||||||
|
@ -53,15 +53,15 @@ class PodcastManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
|
async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
|
||||||
var index = libraryItem.media.episodes.length + 1
|
let index = libraryItem.media.episodes.length + 1
|
||||||
episodesToDownload.forEach((ep) => {
|
for (const ep of episodesToDownload) {
|
||||||
var newPe = new PodcastEpisode()
|
const newPe = new PodcastEpisode()
|
||||||
newPe.setData(ep, index++)
|
newPe.setData(ep, index++)
|
||||||
newPe.libraryItemId = libraryItem.id
|
newPe.libraryItemId = libraryItem.id
|
||||||
var newPeDl = new PodcastEpisodeDownload()
|
const newPeDl = new PodcastEpisodeDownload()
|
||||||
newPeDl.setData(newPe, libraryItem, isAutoDownload, libraryItem.libraryId)
|
newPeDl.setData(newPe, libraryItem, isAutoDownload, libraryItem.libraryId)
|
||||||
this.startPodcastEpisodeDownload(newPeDl)
|
this.startPodcastEpisodeDownload(newPeDl)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startPodcastEpisodeDownload(podcastEpisodeDownload) {
|
async startPodcastEpisodeDownload(podcastEpisodeDownload) {
|
||||||
@ -94,7 +94,6 @@ class PodcastManager {
|
|||||||
await filePerms.setDefault(this.currentDownload.libraryItem.path)
|
await filePerms.setDefault(this.currentDownload.libraryItem.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let success = false
|
let success = false
|
||||||
if (this.currentDownload.urlFileExtension === 'mp3') {
|
if (this.currentDownload.urlFileExtension === 'mp3') {
|
||||||
// Download episode and tag it
|
// Download episode and tag it
|
||||||
@ -156,6 +155,11 @@ class PodcastManager {
|
|||||||
|
|
||||||
const podcastEpisode = this.currentDownload.podcastEpisode
|
const podcastEpisode = this.currentDownload.podcastEpisode
|
||||||
podcastEpisode.audioFile = audioFile
|
podcastEpisode.audioFile = audioFile
|
||||||
|
|
||||||
|
if (audioFile.chapters?.length) {
|
||||||
|
podcastEpisode.chapters = audioFile.chapters.map(ch => ({ ...ch }))
|
||||||
|
}
|
||||||
|
|
||||||
libraryItem.media.addPodcastEpisode(podcastEpisode)
|
libraryItem.media.addPodcastEpisode(podcastEpisode)
|
||||||
if (libraryItem.isInvalid) {
|
if (libraryItem.isInvalid) {
|
||||||
// First episode added to an empty podcast
|
// First episode added to an empty podcast
|
||||||
@ -214,13 +218,13 @@ class PodcastManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async probeAudioFile(libraryFile) {
|
async probeAudioFile(libraryFile) {
|
||||||
var path = libraryFile.metadata.path
|
const path = libraryFile.metadata.path
|
||||||
var mediaProbeData = await prober.probe(path)
|
const mediaProbeData = await prober.probe(path)
|
||||||
if (mediaProbeData.error) {
|
if (mediaProbeData.error) {
|
||||||
Logger.error(`[PodcastManager] Podcast Episode downloaded but failed to probe "${path}"`, mediaProbeData.error)
|
Logger.error(`[PodcastManager] Podcast Episode downloaded but failed to probe "${path}"`, mediaProbeData.error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var newAudioFile = new AudioFile()
|
const newAudioFile = new AudioFile()
|
||||||
newAudioFile.setDataFromProbe(libraryFile, mediaProbeData)
|
newAudioFile.setDataFromProbe(libraryFile, mediaProbeData)
|
||||||
return newAudioFile
|
return newAudioFile
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
const { getId, cleanStringForSearch } = require('../../utils/index')
|
const { getId, cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index')
|
||||||
const AudioFile = require('../files/AudioFile')
|
const AudioFile = require('../files/AudioFile')
|
||||||
const AudioTrack = require('../files/AudioTrack')
|
const AudioTrack = require('../files/AudioTrack')
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ class PodcastEpisode {
|
|||||||
this.description = null
|
this.description = null
|
||||||
this.enclosure = null
|
this.enclosure = null
|
||||||
this.pubDate = null
|
this.pubDate = null
|
||||||
|
this.chapters = []
|
||||||
|
|
||||||
this.audioFile = null
|
this.audioFile = null
|
||||||
this.publishedAt = null
|
this.publishedAt = null
|
||||||
@ -41,6 +42,7 @@ class PodcastEpisode {
|
|||||||
this.description = episode.description
|
this.description = episode.description
|
||||||
this.enclosure = episode.enclosure ? { ...episode.enclosure } : null
|
this.enclosure = episode.enclosure ? { ...episode.enclosure } : null
|
||||||
this.pubDate = episode.pubDate
|
this.pubDate = episode.pubDate
|
||||||
|
this.chapters = episode.chapters?.map(ch => ({ ...ch })) || []
|
||||||
this.audioFile = new AudioFile(episode.audioFile)
|
this.audioFile = new AudioFile(episode.audioFile)
|
||||||
this.publishedAt = episode.publishedAt
|
this.publishedAt = episode.publishedAt
|
||||||
this.addedAt = episode.addedAt
|
this.addedAt = episode.addedAt
|
||||||
@ -62,6 +64,7 @@ class PodcastEpisode {
|
|||||||
description: this.description,
|
description: this.description,
|
||||||
enclosure: this.enclosure ? { ...this.enclosure } : null,
|
enclosure: this.enclosure ? { ...this.enclosure } : null,
|
||||||
pubDate: this.pubDate,
|
pubDate: this.pubDate,
|
||||||
|
chapters: this.chapters.map(ch => ({ ...ch })),
|
||||||
audioFile: this.audioFile.toJSON(),
|
audioFile: this.audioFile.toJSON(),
|
||||||
publishedAt: this.publishedAt,
|
publishedAt: this.publishedAt,
|
||||||
addedAt: this.addedAt,
|
addedAt: this.addedAt,
|
||||||
@ -82,6 +85,7 @@ class PodcastEpisode {
|
|||||||
description: this.description,
|
description: this.description,
|
||||||
enclosure: this.enclosure ? { ...this.enclosure } : null,
|
enclosure: this.enclosure ? { ...this.enclosure } : null,
|
||||||
pubDate: this.pubDate,
|
pubDate: this.pubDate,
|
||||||
|
chapters: this.chapters.map(ch => ({ ...ch })),
|
||||||
audioFile: this.audioFile.toJSON(),
|
audioFile: this.audioFile.toJSON(),
|
||||||
audioTrack: this.audioTrack.toJSON(),
|
audioTrack: this.audioTrack.toJSON(),
|
||||||
publishedAt: this.publishedAt,
|
publishedAt: this.publishedAt,
|
||||||
@ -136,6 +140,7 @@ class PodcastEpisode {
|
|||||||
|
|
||||||
this.setDataFromAudioMetaTags(audioFile.metaTags, true)
|
this.setDataFromAudioMetaTags(audioFile.metaTags, true)
|
||||||
|
|
||||||
|
this.chapters = audioFile.chapters?.map((c) => ({ ...c }))
|
||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
}
|
}
|
||||||
@ -143,8 +148,8 @@ class PodcastEpisode {
|
|||||||
update(payload) {
|
update(payload) {
|
||||||
let hasUpdates = false
|
let hasUpdates = false
|
||||||
for (const key in this.toJSON()) {
|
for (const key in this.toJSON()) {
|
||||||
if (payload[key] != undefined && payload[key] != this[key]) {
|
if (payload[key] != undefined && !areEquivalent(payload[key], this[key])) {
|
||||||
this[key] = payload[key]
|
this[key] = copyValue(payload[key])
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,12 +74,12 @@ function extractPodcastMetadata(channel) {
|
|||||||
|
|
||||||
function extractEpisodeData(item) {
|
function extractEpisodeData(item) {
|
||||||
// Episode must have url
|
// 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`)
|
Logger.error(`[podcastUtils] Invalid podcast episode data`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
var episode = {
|
const episode = {
|
||||||
enclosure: {
|
enclosure: {
|
||||||
...item.enclosure[0]['$']
|
...item.enclosure[0]['$']
|
||||||
}
|
}
|
||||||
@ -91,6 +91,12 @@ function extractEpisodeData(item) {
|
|||||||
episode.description = htmlSanitizer.sanitize(rawDescription)
|
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
|
// Supposed to be the plaintext description but not always followed
|
||||||
if (item['description']) {
|
if (item['description']) {
|
||||||
const rawDescription = extractFirstArrayItem(item, 'description') || ''
|
const rawDescription = extractFirstArrayItem(item, 'description') || ''
|
||||||
@ -133,14 +139,16 @@ function cleanEpisodeData(data) {
|
|||||||
duration: data.duration || '',
|
duration: data.duration || '',
|
||||||
explicit: data.explicit || '',
|
explicit: data.explicit || '',
|
||||||
publishedAt,
|
publishedAt,
|
||||||
enclosure: data.enclosure
|
enclosure: data.enclosure,
|
||||||
|
chaptersUrl: data.chaptersUrl || null,
|
||||||
|
chaptersType: data.chaptersType || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPodcastEpisodes(items) {
|
function extractPodcastEpisodes(items) {
|
||||||
var episodes = []
|
const episodes = []
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
var extracted = extractEpisodeData(item)
|
const extracted = extractEpisodeData(item)
|
||||||
if (extracted) {
|
if (extracted) {
|
||||||
episodes.push(cleanEpisodeData(extracted))
|
episodes.push(cleanEpisodeData(extracted))
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user