mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +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
 | ||||||
|  | |||||||
| @ -151,3 +151,22 @@ module.exports.downloadFile = async (url, filepath) => { | |||||||
|     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 | ||||||
|  |   if (libraryMediaType === 'podcast') { | ||||||
|  |     return getPodcastDataFromDir(folderPath, relPath, parseSubtitle) | ||||||
|  |   } else { | ||||||
|     return getBookDataFromDir(folderPath, relPath, parseSubtitle) |     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