mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-28 09:38:56 +01:00
Podcast episode downloader, update podcast data model
This commit is contained in:
parent
28d76d21f1
commit
920ca683b9
@ -179,7 +179,51 @@ export default {
|
|||||||
toggleSelectEpisode(index) {
|
toggleSelectEpisode(index) {
|
||||||
this.selectedEpisodes[String(index)] = !this.selectedEpisodes[String(index)]
|
this.selectedEpisodes[String(index)] = !this.selectedEpisodes[String(index)]
|
||||||
},
|
},
|
||||||
submit() {},
|
submit() {
|
||||||
|
var episodesToDownload = []
|
||||||
|
if (this.episodesSelected.length) {
|
||||||
|
episodesToDownload = this.episodesSelected.map((episodeIndex) => this.episodes[Number(episodeIndex)])
|
||||||
|
}
|
||||||
|
|
||||||
|
const podcastPayload = {
|
||||||
|
path: this.fullPath,
|
||||||
|
folderId: this.selectedFolderId,
|
||||||
|
libraryId: this.currentLibrary.id,
|
||||||
|
media: {
|
||||||
|
metadata: {
|
||||||
|
title: this.podcast.title,
|
||||||
|
author: this.podcast.author,
|
||||||
|
description: this.podcast.description,
|
||||||
|
releaseDate: this.podcast.releaseDate,
|
||||||
|
genres: [...this.podcast.genres],
|
||||||
|
feedUrl: this.podcast.feedUrl,
|
||||||
|
imageUrl: this.podcast.imageUrl,
|
||||||
|
itunesPageUrl: this.podcast.itunesPageUrl,
|
||||||
|
itunesId: this.podcast.itunesId,
|
||||||
|
itunesArtistId: this.podcast.itunesArtistId,
|
||||||
|
language: this.podcast.language
|
||||||
|
},
|
||||||
|
autoDownloadEpisodes: this.podcast.autoDownloadEpisodes
|
||||||
|
},
|
||||||
|
episodesToDownload
|
||||||
|
}
|
||||||
|
console.log('Podcast payload', podcastPayload)
|
||||||
|
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/podcasts', podcastPayload)
|
||||||
|
.then((libraryItem) => {
|
||||||
|
this.processing = false
|
||||||
|
this.$toast.success('Podcast created successfully')
|
||||||
|
this.show = false
|
||||||
|
this.$router.push(`/item/${libraryItem.id}`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to create podcast', error)
|
||||||
|
this.processing = false
|
||||||
|
this.$toast.error('Failed to create podcast')
|
||||||
|
})
|
||||||
|
},
|
||||||
saveEpisode(episode) {
|
saveEpisode(episode) {
|
||||||
console.log('Save episode', episode)
|
console.log('Save episode', episode)
|
||||||
},
|
},
|
||||||
|
@ -6,10 +6,10 @@
|
|||||||
<div class="relative" style="height: fit-content">
|
<div class="relative" style="height: fit-content">
|
||||||
<covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<!-- Book Progress Bar -->
|
<!-- Item Progress Bar -->
|
||||||
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
|
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
|
||||||
|
|
||||||
<!-- Book Cover Overlay -->
|
<!-- Item Cover Overlay -->
|
||||||
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent>
|
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent>
|
||||||
<div v-show="showPlayButton && !streaming" class="h-full flex items-center justify-center pointer-events-none">
|
<div v-show="showPlayButton && !streaming" class="h-full flex items-center justify-center pointer-events-none">
|
||||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="startStream">
|
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="startStream">
|
||||||
@ -28,10 +28,11 @@
|
|||||||
<h1 class="text-2xl md:text-3xl font-sans">
|
<h1 class="text-2xl md:text-3xl font-sans">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h1>
|
</h1>
|
||||||
<p v-if="subtitle" class="sm:ml-4 text-gray-400 text-xl md:text-2xl">{{ subtitle }}</p>
|
<p v-if="bookSubtitle" class="sm:ml-4 text-gray-400 text-xl md:text-2xl">{{ bookSubtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="authorsList.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">
|
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor }}</p>
|
||||||
|
<p v-else-if="authorsList.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">
|
||||||
by <nuxt-link v-for="(author, index) in authorsList" :key="index" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}<span v-if="index < authorsList.length - 1">, </span></nuxt-link>
|
by <nuxt-link v-for="(author, index) in authorsList" :key="index" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}<span v-if="index < authorsList.length - 1">, </span></nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||||
@ -162,7 +163,6 @@ export default {
|
|||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
console.log(item)
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
console.error('No item...', params.id)
|
console.error('No item...', params.id)
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
@ -193,6 +193,9 @@ export default {
|
|||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
|
isPodcast() {
|
||||||
|
return this.libraryItem.mediaType === 'podcast'
|
||||||
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this.libraryItem.isMissing
|
return this.libraryItem.isMissing
|
||||||
},
|
},
|
||||||
@ -200,7 +203,9 @@ export default {
|
|||||||
return this.libraryItem.isInvalid
|
return this.libraryItem.isInvalid
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isMissing && !this.isInvalid && this.audiobooks.length
|
if (this.isMissing || this.isInvalid) return false
|
||||||
|
if (this.isPodcast) return this.podcastEpisodes.length
|
||||||
|
return this.audiobooks.length
|
||||||
},
|
},
|
||||||
libraryId() {
|
libraryId() {
|
||||||
return this.libraryItem.libraryId
|
return this.libraryItem.libraryId
|
||||||
@ -217,8 +222,8 @@ export default {
|
|||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
audiobooks() {
|
podcastEpisodes() {
|
||||||
return this.media.audiobooks || []
|
return this.media.episodes || []
|
||||||
},
|
},
|
||||||
defaultAudiobook() {
|
defaultAudiobook() {
|
||||||
if (!this.audiobooks.length) return null
|
if (!this.audiobooks.length) return null
|
||||||
@ -233,12 +238,16 @@ export default {
|
|||||||
narrator() {
|
narrator() {
|
||||||
return this.mediaMetadata.narratorName
|
return this.mediaMetadata.narratorName
|
||||||
},
|
},
|
||||||
subtitle() {
|
bookSubtitle() {
|
||||||
|
if (this.isPodcast) return null
|
||||||
return this.mediaMetadata.subtitle
|
return this.mediaMetadata.subtitle
|
||||||
},
|
},
|
||||||
genres() {
|
genres() {
|
||||||
return this.mediaMetadata.genres || []
|
return this.mediaMetadata.genres || []
|
||||||
},
|
},
|
||||||
|
podcastAuthor() {
|
||||||
|
return this.mediaMetadata.author || ''
|
||||||
|
},
|
||||||
authors() {
|
authors() {
|
||||||
return this.mediaMetadata.authors || []
|
return this.mediaMetadata.authors || []
|
||||||
},
|
},
|
||||||
|
@ -42,6 +42,16 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
async asyncData({ params, query, store, app, redirect }) {
|
||||||
|
var libraryId = params.library
|
||||||
|
var library = await store.dispatch('libraries/fetch', libraryId)
|
||||||
|
if (!library) {
|
||||||
|
return redirect('/oops?message=Library not found')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
libraryId
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
@ -61,7 +61,7 @@ class Server {
|
|||||||
this.downloadManager = new DownloadManager(this.db)
|
this.downloadManager = new DownloadManager(this.db)
|
||||||
this.playbackSessionManager = new PlaybackSessionManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
this.playbackSessionManager = new PlaybackSessionManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||||
this.coverManager = new CoverManager(this.db, this.cacheManager)
|
this.coverManager = new CoverManager(this.db, this.cacheManager)
|
||||||
this.podcastManager = new PodcastManager(this.db)
|
this.podcastManager = new PodcastManager(this.db, this.watcher, this.emitter.bind(this))
|
||||||
|
|
||||||
this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
|
this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ class FolderWatcher extends EventEmitter {
|
|||||||
this.pendingDelay = 4000
|
this.pendingDelay = 4000
|
||||||
this.pendingTimeout = null
|
this.pendingTimeout = null
|
||||||
|
|
||||||
|
this.ignoreDirs = []
|
||||||
this.disabled = false
|
this.disabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,11 +116,17 @@ class FolderWatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onNewFile(libraryId, path) {
|
onNewFile(libraryId, path) {
|
||||||
|
if (this.checkShouldIgnorePath(path)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
Logger.debug('[Watcher] File Added', path)
|
Logger.debug('[Watcher] File Added', path)
|
||||||
this.addFileUpdate(libraryId, path, 'added')
|
this.addFileUpdate(libraryId, path, 'added')
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileRemoved(libraryId, path) {
|
onFileRemoved(libraryId, path) {
|
||||||
|
if (this.checkShouldIgnorePath(path)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
Logger.debug('[Watcher] File Removed', path)
|
Logger.debug('[Watcher] File Removed', path)
|
||||||
this.addFileUpdate(libraryId, path, 'deleted')
|
this.addFileUpdate(libraryId, path, 'deleted')
|
||||||
}
|
}
|
||||||
@ -129,6 +136,9 @@ class FolderWatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onRename(libraryId, pathFrom, pathTo) {
|
onRename(libraryId, pathFrom, pathTo) {
|
||||||
|
if (this.checkShouldIgnorePath(pathTo)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
Logger.debug(`[Watcher] Rename ${pathFrom} => ${pathTo}`)
|
Logger.debug(`[Watcher] Rename ${pathFrom} => ${pathTo}`)
|
||||||
this.addFileUpdate(libraryId, pathFrom, 'renamed')
|
this.addFileUpdate(libraryId, pathFrom, 'renamed')
|
||||||
this.addFileUpdate(libraryId, pathTo, 'renamed')
|
this.addFileUpdate(libraryId, pathTo, 'renamed')
|
||||||
@ -185,5 +195,31 @@ class FolderWatcher extends EventEmitter {
|
|||||||
this.pendingFileUpdates = []
|
this.pendingFileUpdates = []
|
||||||
}, this.pendingDelay)
|
}, this.pendingDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkShouldIgnorePath(path) {
|
||||||
|
return !!this.ignoreDirs.find(dirpath => {
|
||||||
|
return path.replace(/\\/g, '/').startsWith(dirpath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanDirPath(path) {
|
||||||
|
var path = path.replace(/\\/g, '/')
|
||||||
|
if (path.endsWith('/')) path = path.slice(0, -1)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
addIgnoreDir(path) {
|
||||||
|
path = this.cleanDirPath(path)
|
||||||
|
if (this.ignoreDirs.includes(path)) return
|
||||||
|
Logger.debug(`[Watcher] Ignoring directory "${path}"`)
|
||||||
|
this.ignoreDirs.push(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeIgnoreDir(path) {
|
||||||
|
path = this.cleanDirPath(path)
|
||||||
|
if (!this.ignoreDirs.includes(path)) return
|
||||||
|
Logger.debug(`[Watcher] No longer ignoring directory "${path}"`)
|
||||||
|
this.ignoreDirs = this.ignoreDirs.filter(p => p !== path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = FolderWatcher
|
module.exports = FolderWatcher
|
@ -3,6 +3,8 @@ const fs = require('fs-extra')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
||||||
const LibraryItem = require('../objects/LibraryItem')
|
const LibraryItem = require('../objects/LibraryItem')
|
||||||
|
const { getFileTimestampsWithIno } = require('../utils/fileUtils')
|
||||||
|
const filePerms = require('../utils/filePerms')
|
||||||
|
|
||||||
class PodcastController {
|
class PodcastController {
|
||||||
|
|
||||||
@ -13,28 +15,72 @@ class PodcastController {
|
|||||||
}
|
}
|
||||||
const payload = req.body
|
const payload = req.body
|
||||||
|
|
||||||
if (await fs.pathExists(payload.path)) {
|
const library = this.db.libraries.find(lib => lib.id === payload.libraryId)
|
||||||
Logger.error(`[PodcastController] Attempt to create podcast when folder path already exists "${payload.path}"`)
|
if (!library) {
|
||||||
|
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
|
||||||
|
return res.status(400).send('Library not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = library.folders.find(fold => fold.id === payload.folderId)
|
||||||
|
if (!folder) {
|
||||||
|
Logger.error(`[PodcastController] Create: Folder not found "${payload.folderId}"`)
|
||||||
|
return res.status(400).send('Folder not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
var podcastPath = payload.path.replace(/\\/g, '/')
|
||||||
|
if (await fs.pathExists(podcastPath)) {
|
||||||
|
Logger.error(`[PodcastController] Attempt to create podcast when folder path already exists "${podcastPath}"`)
|
||||||
return res.status(400).send('Path already exists')
|
return res.status(400).send('Path already exists')
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = await fs.ensureDir(payload.path).then(() => true).catch((error) => {
|
var success = await fs.ensureDir(podcastPath).then(() => true).catch((error) => {
|
||||||
Logger.error(`[PodcastController] Failed to ensure podcast dir "${payload.path}"`, error)
|
Logger.error(`[PodcastController] Failed to ensure podcast dir "${podcastPath}"`, error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if (!success) return res.status(400).send('Invalid podcast path')
|
if (!success) return res.status(400).send('Invalid podcast path')
|
||||||
|
await filePerms.setDefault(podcastPath)
|
||||||
|
|
||||||
if (payload.mediaMetadata.imageUrl) {
|
var libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
|
||||||
// TODO: Download image
|
|
||||||
|
var relPath = payload.path.replace(folder.fullPath, '')
|
||||||
|
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
||||||
|
|
||||||
|
const libraryItemPayload = {
|
||||||
|
path: podcastPath,
|
||||||
|
relPath,
|
||||||
|
folderId: payload.folderId,
|
||||||
|
libraryId: payload.libraryId,
|
||||||
|
ino: libraryItemFolderStats.ino,
|
||||||
|
mtimeMs: libraryItemFolderStats.mtimeMs || 0,
|
||||||
|
ctimeMs: libraryItemFolderStats.ctimeMs || 0,
|
||||||
|
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
|
||||||
|
media: payload.media
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryItem = new LibraryItem()
|
var libraryItem = new LibraryItem()
|
||||||
libraryItem.setData('podcast', payload)
|
libraryItem.setData('podcast', libraryItemPayload)
|
||||||
|
|
||||||
|
// Download and save cover image
|
||||||
|
if (payload.media.metadata.imageUrl) {
|
||||||
|
var coverResponse = await this.coverManager.downloadCoverFromUrl(libraryItem, payload.media.metadata.imageUrl)
|
||||||
|
if (coverResponse) {
|
||||||
|
if (coverResponse.error) {
|
||||||
|
Logger.error(`[PodcastController] Download cover error from "${payload.media.metadata.imageUrl}": ${coverResponse.error}`)
|
||||||
|
} else if (coverResponse.cover) {
|
||||||
|
libraryItem.media.coverPath = coverResponse.cover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.db.insertLibraryItem(libraryItem)
|
await this.db.insertLibraryItem(libraryItem)
|
||||||
this.emitter('item_added', libraryItem.toJSONExpanded())
|
this.emitter('item_added', libraryItem.toJSONExpanded())
|
||||||
|
|
||||||
res.json(libraryItem.toJSONExpanded())
|
res.json(libraryItem.toJSONExpanded())
|
||||||
|
|
||||||
|
if (payload.episodesToDownload && payload.episodesToDownload.length) {
|
||||||
|
Logger.info(`[PodcastController] Podcast created now starting ${payload.episodesToDownload.length} episode downloads`)
|
||||||
|
this.podcastManager.downloadPodcastEpisodes(libraryItem, payload.episodesToDownload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPodcastFeed(req, res) {
|
getPodcastFeed(req, res) {
|
||||||
|
@ -1,12 +1,101 @@
|
|||||||
|
const fs = require('fs-extra')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
const { downloadFile } = require('../utils/fileUtils')
|
||||||
|
const prober = require('../utils/prober')
|
||||||
|
const LibraryFile = require('../objects/files/LibraryFile')
|
||||||
|
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
|
||||||
|
const PodcastEpisode = require('../objects/entities/PodcastEpisode')
|
||||||
|
const AudioFile = require('../objects/files/AudioFile')
|
||||||
|
|
||||||
class PodcastManager {
|
class PodcastManager {
|
||||||
constructor(db) {
|
constructor(db, watcher, emitter) {
|
||||||
this.db = db
|
this.db = db
|
||||||
|
this.watcher = watcher
|
||||||
|
this.emitter = emitter
|
||||||
|
|
||||||
this.downloadQueue = []
|
this.downloadQueue = []
|
||||||
|
this.currentDownload = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadPodcasts(podcasts, targetDir) {
|
async downloadPodcastEpisodes(libraryItem, episodesToDownload) {
|
||||||
|
var index = 1
|
||||||
|
episodesToDownload.forEach((ep) => {
|
||||||
|
var newPe = new PodcastEpisode()
|
||||||
|
newPe.setData(ep, index++)
|
||||||
|
var newPeDl = new PodcastEpisodeDownload()
|
||||||
|
newPeDl.setData(newPe, libraryItem)
|
||||||
|
this.startPodcastEpisodeDownload(newPeDl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async startPodcastEpisodeDownload(podcastEpisodeDownload) {
|
||||||
|
if (this.currentDownload) {
|
||||||
|
this.downloadQueue.push(podcastEpisodeDownload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.currentDownload = podcastEpisodeDownload
|
||||||
|
|
||||||
|
// Ignores all added files to this dir
|
||||||
|
this.watcher.addIgnoreDir(this.currentDownload.libraryItem.path)
|
||||||
|
|
||||||
|
var success = await downloadFile(this.currentDownload.url, this.currentDownload.targetPath).then(() => true).catch((error) => {
|
||||||
|
Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (success) {
|
||||||
|
success = await this.scanAddPodcastEpisodeAudioFile()
|
||||||
|
if (!success) {
|
||||||
|
await fs.remove(this.currentDownload.targetPath)
|
||||||
|
} else {
|
||||||
|
Logger.info(`[PodcastManager] Successfully downloaded podcast episode "${this.currentDownload.podcastEpisode.title}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.watcher.removeIgnoreDir(this.currentDownload.libraryItem.path)
|
||||||
|
this.currentDownload = null
|
||||||
|
if (this.downloadQueue.length) {
|
||||||
|
this.startPodcastEpisodeDownload(this.downloadQueue.shift())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanAddPodcastEpisodeAudioFile() {
|
||||||
|
var libraryFile = await this.getLibraryFile(this.currentDownload.targetPath, this.currentDownload.targetRelPath)
|
||||||
|
var audioFile = await this.probeAudioFile(libraryFile)
|
||||||
|
if (!audioFile) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var libraryItem = this.db.libraryItems.find(li => li.id === this.currentDownload.libraryItem.id)
|
||||||
|
if (!libraryItem) {
|
||||||
|
Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var podcastEpisode = this.currentDownload.podcastEpisode
|
||||||
|
podcastEpisode.audioFile = audioFile
|
||||||
|
libraryItem.media.addPodcastEpisode(podcastEpisode)
|
||||||
|
libraryItem.updatedAt = Date.now()
|
||||||
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLibraryFile(path, relPath) {
|
||||||
|
var newLibFile = new LibraryFile()
|
||||||
|
await newLibFile.setDataFromPath(path, relPath)
|
||||||
|
return newLibFile
|
||||||
|
}
|
||||||
|
|
||||||
|
async probeAudioFile(libraryFile) {
|
||||||
|
var path = libraryFile.metadata.path
|
||||||
|
var audioProbeData = await prober.probe(path)
|
||||||
|
if (audioProbeData.error) {
|
||||||
|
Logger.error(`[PodcastManager] Podcast Episode downloaded but failed to probe "${path}"`, audioProbeData.error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var newAudioFile = new AudioFile()
|
||||||
|
newAudioFile.setDataFromProbe(libraryFile, audioProbeData)
|
||||||
|
return newAudioFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = PodcastManager
|
module.exports = PodcastManager
|
@ -166,8 +166,10 @@ class LibraryItem {
|
|||||||
} else {
|
} else {
|
||||||
this.mediaType = 'book'
|
this.mediaType = 'book'
|
||||||
this.media = new Book()
|
this.media = new Book()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for (const key in payload) {
|
for (const key in payload) {
|
||||||
if (key === 'libraryFiles') {
|
if (key === 'libraryFiles') {
|
||||||
this.libraryFiles = payload.libraryFiles.map(lf => lf.clone())
|
this.libraryFiles = payload.libraryFiles.map(lf => lf.clone())
|
||||||
@ -175,13 +177,13 @@ class LibraryItem {
|
|||||||
// Use first image library file as cover
|
// Use first image library file as cover
|
||||||
var firstImageFile = this.libraryFiles.find(lf => lf.fileType === 'image')
|
var firstImageFile = this.libraryFiles.find(lf => lf.fileType === 'image')
|
||||||
if (firstImageFile) this.media.coverPath = firstImageFile.metadata.path
|
if (firstImageFile) this.media.coverPath = firstImageFile.metadata.path
|
||||||
} else if (this[key] !== undefined) {
|
} else if (this[key] !== undefined && key !== 'media') {
|
||||||
this[key] = payload[key]
|
this[key] = payload[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.mediaMetadata) {
|
if (payload.media) {
|
||||||
this.media.setData(payload.mediaMetadata)
|
this.media.setData(payload.media)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
|
38
server/objects/PodcastEpisodeDownload.js
Normal file
38
server/objects/PodcastEpisodeDownload.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const Path = require('path')
|
||||||
|
const { getId } = require('../utils/index')
|
||||||
|
const { sanitizeFilename } = require('../utils/fileUtils')
|
||||||
|
|
||||||
|
class PodcastEpisodeDownload {
|
||||||
|
constructor() {
|
||||||
|
this.id = null
|
||||||
|
this.podcastEpisode = null
|
||||||
|
this.url = null
|
||||||
|
this.libraryItem = null
|
||||||
|
|
||||||
|
this.isDownloading = false
|
||||||
|
this.startedAt = null
|
||||||
|
this.createdAt = null
|
||||||
|
this.finishedAt = null
|
||||||
|
}
|
||||||
|
|
||||||
|
get targetFilename() {
|
||||||
|
return sanitizeFilename(`${this.podcastEpisode.bestFilename}.mp3`)
|
||||||
|
}
|
||||||
|
|
||||||
|
get targetPath() {
|
||||||
|
return Path.join(this.libraryItem.path, this.targetFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
get targetRelPath() {
|
||||||
|
return Path.join(this.libraryItem.relPath, this.targetFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(podcastEpisode, libraryItem) {
|
||||||
|
this.id = getId('epdl')
|
||||||
|
this.podcastEpisode = podcastEpisode
|
||||||
|
this.url = podcastEpisode.enclosure.url
|
||||||
|
this.libraryItem = libraryItem
|
||||||
|
this.createdAt = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = PodcastEpisodeDownload
|
@ -7,13 +7,16 @@ class PodcastEpisode {
|
|||||||
this.id = null
|
this.id = null
|
||||||
this.index = null
|
this.index = null
|
||||||
|
|
||||||
this.episodeNumber = null
|
this.episode = null
|
||||||
|
this.episodeType = null
|
||||||
this.title = null
|
this.title = null
|
||||||
|
this.subtitle = null
|
||||||
this.description = null
|
this.description = null
|
||||||
this.enclosure = null
|
this.enclosure = null
|
||||||
this.pubDate = null
|
this.pubDate = null
|
||||||
|
|
||||||
this.audioFile = null
|
this.audioFile = null
|
||||||
|
this.publishedAt = null
|
||||||
this.addedAt = null
|
this.addedAt = null
|
||||||
this.updatedAt = null
|
this.updatedAt = null
|
||||||
|
|
||||||
@ -25,12 +28,15 @@ class PodcastEpisode {
|
|||||||
construct(episode) {
|
construct(episode) {
|
||||||
this.id = episode.id
|
this.id = episode.id
|
||||||
this.index = episode.index
|
this.index = episode.index
|
||||||
this.episodeNumber = episode.episodeNumber
|
this.episode = episode.episode
|
||||||
|
this.episodeType = episode.episodeType
|
||||||
this.title = episode.title
|
this.title = episode.title
|
||||||
|
this.subtitle = episode.subtitle
|
||||||
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.audioFile = new AudioFile(episode.audioFile)
|
this.audioFile = new AudioFile(episode.audioFile)
|
||||||
|
this.publishedAt = episode.publishedAt
|
||||||
this.addedAt = episode.addedAt
|
this.addedAt = episode.addedAt
|
||||||
this.updatedAt = episode.updatedAt
|
this.updatedAt = episode.updatedAt
|
||||||
}
|
}
|
||||||
@ -39,12 +45,15 @@ class PodcastEpisode {
|
|||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
index: this.index,
|
index: this.index,
|
||||||
episodeNumber: this.episodeNumber,
|
episode: this.episode,
|
||||||
|
episodeType: this.episodeType,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
|
subtitle: this.subtitle,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
enclosure: this.enclosure ? { ...this.enclosure } : null,
|
enclosure: this.enclosure ? { ...this.enclosure } : null,
|
||||||
pubDate: this.pubDate,
|
pubDate: this.pubDate,
|
||||||
audioFile: this.audioFile.toJSON(),
|
audioFile: this.audioFile.toJSON(),
|
||||||
|
publishedAt: this.publishedAt,
|
||||||
addedAt: this.addedAt,
|
addedAt: this.addedAt,
|
||||||
updatedAt: this.updatedAt
|
updatedAt: this.updatedAt
|
||||||
}
|
}
|
||||||
@ -58,15 +67,22 @@ class PodcastEpisode {
|
|||||||
return this.audioFile.duration
|
return this.audioFile.duration
|
||||||
}
|
}
|
||||||
get size() { return this.audioFile.metadata.size }
|
get size() { return this.audioFile.metadata.size }
|
||||||
|
get bestFilename() {
|
||||||
|
if (this.episode) return `${this.episode} - ${this.title}`
|
||||||
|
return this.title
|
||||||
|
}
|
||||||
|
|
||||||
setData(data, index = 1) {
|
setData(data, index = 1) {
|
||||||
this.id = getId('ep')
|
this.id = getId('ep')
|
||||||
this.index = index
|
this.index = index
|
||||||
this.title = data.title
|
this.title = data.title
|
||||||
|
this.subtitle = data.subtitle || ''
|
||||||
this.pubDate = data.pubDate || ''
|
this.pubDate = data.pubDate || ''
|
||||||
this.description = data.description || ''
|
this.description = data.description || ''
|
||||||
this.enclosure = data.enclosure ? { ...data.enclosure } : null
|
this.enclosure = data.enclosure ? { ...data.enclosure } : null
|
||||||
this.episodeNumber = data.episodeNumber || ''
|
this.episode = data.episode || ''
|
||||||
|
this.episodeType = data.episodeType || ''
|
||||||
|
this.publishedAt = data.publishedAt || 0
|
||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
@ -176,9 +176,11 @@ class Book {
|
|||||||
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
|
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(scanMediaMetadata) {
|
setData(mediaPayload) {
|
||||||
this.metadata = new BookMetadata()
|
this.metadata = new BookMetadata()
|
||||||
this.metadata.setData(scanMediaMetadata)
|
if (mediaPayload.metadata) {
|
||||||
|
this.metadata.setData(mediaPayload.metadata)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
|
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
|
||||||
|
@ -118,10 +118,14 @@ class Podcast {
|
|||||||
return this.episodes[0]
|
return this.episodes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(metadata, coverPath = null, autoDownload = false) {
|
setData(mediaMetadata) {
|
||||||
this.metadata = new PodcastMetadata(metadata)
|
this.metadata = new PodcastMetadata()
|
||||||
this.coverPath = coverPath
|
if (mediaMetadata.metadata) {
|
||||||
this.autoDownloadEpisodes = autoDownload
|
this.metadata.setData(mediaMetadata.metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.coverPath = mediaMetadata.coverPath || null
|
||||||
|
this.autoDownloadEpisodes = !!mediaMetadata.autoDownloadEpisodes
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
||||||
@ -150,5 +154,9 @@ class Podcast {
|
|||||||
this.episodes.forEach((ep) => total += ep.duration)
|
this.episodes.forEach((ep) => total += ep.duration)
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addPodcastEpisode(podcastEpisode) {
|
||||||
|
this.episodes.push(podcastEpisode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Podcast
|
module.exports = Podcast
|
@ -70,5 +70,22 @@ class PodcastMetadata {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setData(mediaMetadata = {}) {
|
||||||
|
this.title = mediaMetadata.title || null
|
||||||
|
this.author = mediaMetadata.author || null
|
||||||
|
this.description = mediaMetadata.description || null
|
||||||
|
this.releaseDate = mediaMetadata.releaseDate || null
|
||||||
|
this.feedUrl = mediaMetadata.feedUrl || null
|
||||||
|
this.imageUrl = mediaMetadata.imageUrl || null
|
||||||
|
this.itunesPageUrl = mediaMetadata.itunesPageUrl || null
|
||||||
|
this.itunesId = mediaMetadata.itunesId || null
|
||||||
|
this.itunesArtistId = mediaMetadata.itunesArtistId || null
|
||||||
|
this.explicit = !!mediaMetadata.explicit
|
||||||
|
this.language = mediaMetadata.language || null
|
||||||
|
if (mediaMetadata.genres && mediaMetadata.genres.length) {
|
||||||
|
this.genres = [...mediaMetadata.genres]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = PodcastMetadata
|
module.exports = PodcastMetadata
|
@ -54,7 +54,7 @@ class AudioFileScanner {
|
|||||||
return Math.floor(total / results.length)
|
return Math.floor(total / results.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
async scan(audioLibraryFile, mediaMetadataFromScan, verbose = false) {
|
async scan(mediaType, audioLibraryFile, mediaMetadataFromScan, verbose = false) {
|
||||||
var probeStart = Date.now()
|
var probeStart = Date.now()
|
||||||
var probeData = await prober.probe(audioLibraryFile.metadata.path, verbose)
|
var probeData = await prober.probe(audioLibraryFile.metadata.path, verbose)
|
||||||
if (probeData.error) {
|
if (probeData.error) {
|
||||||
@ -65,11 +65,11 @@ class AudioFileScanner {
|
|||||||
var audioFile = new AudioFile()
|
var audioFile = new AudioFile()
|
||||||
audioFile.trackNumFromMeta = probeData.trackNumber
|
audioFile.trackNumFromMeta = probeData.trackNumber
|
||||||
audioFile.discNumFromMeta = probeData.discNumber
|
audioFile.discNumFromMeta = probeData.discNumber
|
||||||
|
if (mediaType === 'book') {
|
||||||
const { trackNumber, discNumber } = this.getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile)
|
const { trackNumber, discNumber } = this.getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile)
|
||||||
audioFile.trackNumFromFilename = trackNumber
|
audioFile.trackNumFromFilename = trackNumber
|
||||||
audioFile.discNumFromFilename = discNumber
|
audioFile.discNumFromFilename = discNumber
|
||||||
|
}
|
||||||
audioFile.setDataFromProbe(audioLibraryFile, probeData)
|
audioFile.setDataFromProbe(audioLibraryFile, probeData)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -79,11 +79,11 @@ class AudioFileScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns array of { AudioFile, elapsed, averageScanDuration } from audio file scan objects
|
// Returns array of { AudioFile, elapsed, averageScanDuration } from audio file scan objects
|
||||||
async executeAudioFileScans(audioLibraryFiles, scanData) {
|
async executeAudioFileScans(mediaType, audioLibraryFiles, scanData) {
|
||||||
var mediaMetadataFromScan = scanData.mediaMetadata || null
|
var mediaMetadataFromScan = scanData.mediaMetadata || null
|
||||||
var proms = []
|
var proms = []
|
||||||
for (let i = 0; i < audioLibraryFiles.length; i++) {
|
for (let i = 0; i < audioLibraryFiles.length; i++) {
|
||||||
proms.push(this.scan(audioLibraryFiles[i], mediaMetadataFromScan))
|
proms.push(this.scan(mediaType, audioLibraryFiles[i], mediaMetadataFromScan))
|
||||||
}
|
}
|
||||||
var scanStart = Date.now()
|
var scanStart = Date.now()
|
||||||
var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
|
var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
|
||||||
@ -178,7 +178,7 @@ class AudioFileScanner {
|
|||||||
async scanAudioFiles(audioLibraryFiles, scanData, libraryItem, preferAudioMetadata, libraryScan = null) {
|
async scanAudioFiles(audioLibraryFiles, scanData, libraryItem, preferAudioMetadata, libraryScan = null) {
|
||||||
var hasUpdated = false
|
var hasUpdated = false
|
||||||
|
|
||||||
var audioScanResult = await this.executeAudioFileScans(audioLibraryFiles, scanData)
|
var audioScanResult = await this.executeAudioFileScans(libraryItem.mediaType, audioLibraryFiles, scanData)
|
||||||
if (audioScanResult.audioFiles.length) {
|
if (audioScanResult.audioFiles.length) {
|
||||||
if (libraryScan) {
|
if (libraryScan) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Audio file scan took ${audioScanResult.elapsed}ms for ${audioScanResult.audioFiles.length} with average time of ${audioScanResult.averageScanDuration}ms`)
|
libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Audio file scan took ${audioScanResult.elapsed}ms for ${audioScanResult.audioFiles.length} with average time of ${audioScanResult.averageScanDuration}ms`)
|
||||||
|
@ -502,6 +502,7 @@ class Scanner {
|
|||||||
|
|
||||||
async scanFolderUpdates(library, folder, fileUpdateGroup) {
|
async scanFolderUpdates(library, folder, fileUpdateGroup) {
|
||||||
Logger.debug(`[Scanner] Scanning file update groups in folder "${folder.id}" of library "${library.name}"`)
|
Logger.debug(`[Scanner] Scanning file update groups in folder "${folder.id}" of library "${library.name}"`)
|
||||||
|
Logger.debug(`[Scanner] scanFolderUpdates fileUpdateGroup`, fileUpdateGroup)
|
||||||
|
|
||||||
// First pass - Remove files in parent dirs of items and remap the fileupdate group
|
// First pass - Remove files in parent dirs of items and remap the fileupdate group
|
||||||
// Test Case: Moving audio files from library item folder to author folder should trigger a re-scan of the item
|
// Test Case: Moving audio files from library item folder to author folder should trigger a re-scan of the item
|
||||||
|
@ -150,4 +150,23 @@ module.exports.downloadFile = async (url, filepath) => {
|
|||||||
writer.on('finish', resolve)
|
writer.on('finish', resolve)
|
||||||
writer.on('error', reject)
|
writer.on('error', reject)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.sanitizeFilename = (filename, replacement = '') => {
|
||||||
|
if (typeof filename !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var illegalRe = /[\/\?<>\\:\*\|"]/g;
|
||||||
|
var controlRe = /[\x00-\x1f\x80-\x9f]/g;
|
||||||
|
var reservedRe = /^\.+$/;
|
||||||
|
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
||||||
|
var windowsTrailingRe = /[\. ]+$/;
|
||||||
|
|
||||||
|
var sanitized = filename
|
||||||
|
.replace(illegalRe, replacement)
|
||||||
|
.replace(controlRe, replacement)
|
||||||
|
.replace(reservedRe, replacement)
|
||||||
|
.replace(windowsReservedRe, replacement)
|
||||||
|
.replace(windowsTrailingRe, replacement);
|
||||||
|
return sanitized
|
||||||
}
|
}
|
@ -59,12 +59,12 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
libraryItems.forEach((li) => {
|
libraryItems.forEach((li) => {
|
||||||
var mediaMetadata = li.media.metadata
|
var mediaMetadata = li.media.metadata
|
||||||
if (mediaMetadata.authors.length) {
|
if (mediaMetadata.authors && mediaMetadata.authors.length) {
|
||||||
mediaMetadata.authors.forEach((author) => {
|
mediaMetadata.authors.forEach((author) => {
|
||||||
if (author && !data.authors.find(au => au.id === author.id)) data.authors.push({ id: author.id, name: author.name })
|
if (author && !data.authors.find(au => au.id === author.id)) data.authors.push({ id: author.id, name: author.name })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (mediaMetadata.series.length) {
|
if (mediaMetadata.series && mediaMetadata.series.length) {
|
||||||
mediaMetadata.series.forEach((series) => {
|
mediaMetadata.series.forEach((series) => {
|
||||||
if (series && !data.series.find(se => se.id === series.id)) data.series.push({ id: series.id, name: series.name })
|
if (series && !data.series.find(se => se.id === series.id)) data.series.push({ id: series.id, name: series.name })
|
||||||
})
|
})
|
||||||
@ -79,7 +79,7 @@ module.exports = {
|
|||||||
if (tag && !data.tags.includes(tag)) data.tags.push(tag)
|
if (tag && !data.tags.includes(tag)) data.tags.push(tag)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (mediaMetadata.narrators.length) {
|
if (mediaMetadata.narrators && mediaMetadata.narrators.length) {
|
||||||
mediaMetadata.narrators.forEach((narrator) => {
|
mediaMetadata.narrators.forEach((narrator) => {
|
||||||
if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
|
if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
|
||||||
})
|
})
|
||||||
|
@ -81,7 +81,8 @@ function cleanEpisodeData(data) {
|
|||||||
author: data.author || '',
|
author: data.author || '',
|
||||||
duration: data.duration || '',
|
duration: data.duration || '',
|
||||||
explicit: data.explicit || '',
|
explicit: data.explicit || '',
|
||||||
publishedAt: (new Date(data.pubDate)).valueOf()
|
publishedAt: (new Date(data.pubDate)).valueOf(),
|
||||||
|
enclosure: data.enclosure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +161,11 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
|
|||||||
mtimeMs: libraryItemFolderStats.mtimeMs || 0,
|
mtimeMs: libraryItemFolderStats.mtimeMs || 0,
|
||||||
ctimeMs: libraryItemFolderStats.ctimeMs || 0,
|
ctimeMs: libraryItemFolderStats.ctimeMs || 0,
|
||||||
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
|
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
|
||||||
...libraryItemData,
|
path: libraryItemData.path,
|
||||||
|
relPath: libraryItemData.relPath,
|
||||||
|
media: {
|
||||||
|
metadata: libraryItemData.mediaMetadata || null
|
||||||
|
},
|
||||||
libraryFiles: fileObjs
|
libraryFiles: fileObjs
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -262,9 +266,21 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPodcastDataFromDir(folderPath, relPath) {
|
||||||
|
relPath = relPath.replace(/\\/g, '/')
|
||||||
|
return {
|
||||||
|
relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/..
|
||||||
|
path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/..
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings) {
|
function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings) {
|
||||||
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
||||||
return getBookDataFromDir(folderPath, relPath, parseSubtitle)
|
if (libraryMediaType === 'podcast') {
|
||||||
|
return getPodcastDataFromDir(folderPath, relPath, parseSubtitle)
|
||||||
|
} else {
|
||||||
|
return getBookDataFromDir(folderPath, relPath, parseSubtitle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -284,7 +300,11 @@ async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath,
|
|||||||
birthtimeMs: libraryItemDirStats.birthtimeMs || 0,
|
birthtimeMs: libraryItemDirStats.birthtimeMs || 0,
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
libraryId: folder.libraryId,
|
libraryId: folder.libraryId,
|
||||||
...libraryItemData,
|
path: libraryItemData.path,
|
||||||
|
relPath: libraryItemData.relPath,
|
||||||
|
media: {
|
||||||
|
metadata: libraryItemData.mediaMetadata || null
|
||||||
|
},
|
||||||
libraryFiles: []
|
libraryFiles: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user