mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Fix library check path and set provider, update podcast model and UI
This commit is contained in:
		
							parent
							
								
									deadc63dbb
								
							
						
					
					
						commit
						43bbfbfee3
					
				| @ -4,7 +4,6 @@ | ||||
|       <span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="backArrowPress">arrow_back</span> | ||||
|       <p class="px-4 text-xl">{{ title }}</p> | ||||
|     </div> | ||||
| 
 | ||||
|     <div v-if="!showDirectoryPicker" class="w-full h-full py-4"> | ||||
|       <div class="flex flex-wrap md:flex-nowrap -mx-1"> | ||||
|         <div class="w-2/5 md:w-72 px-1 py-1 md:py-0"> | ||||
| @ -202,7 +201,6 @@ export default { | ||||
|         mediaType: this.mediaType, | ||||
|         disableWatcher: this.disableWatcher | ||||
|       } | ||||
| 
 | ||||
|       this.$emit('update:processing', true) | ||||
|       this.$axios | ||||
|         .$post('/api/libraries', newLibraryPayload) | ||||
|  | ||||
| @ -10,26 +10,29 @@ | ||||
|         <div class="w-full md:w-1/2 p-4"> | ||||
|           <p class="text-lg font-semibold mb-2">Details</p> | ||||
|           <div class="flex flex-wrap"> | ||||
|             <div class="p-2 w-full"> | ||||
|             <div v-if="podcast.imageUrl" class="p-1 w-full"> | ||||
|               <img :src="podcast.imageUrl" class="h-16 w-16 object-contain" /> | ||||
|             </div> | ||||
|             <div class="p-1 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.title" label="Title" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|             <div class="p-1 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.author" label="Author" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.feedUrl" label="Feed URL" /> | ||||
|             <div class="p-1 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.feedUrl" label="Feed URL" readonly /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|             <div class="p-1 w-full"> | ||||
|               <ui-multi-select v-model="podcast.genres" :items="podcast.genres" label="Genres" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.releaseDate" label="Release Date" /> | ||||
|             <div class="p-1 w-full"> | ||||
|               <ui-textarea-with-label v-model="podcast.description" label="Description" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.itunesPageUrl" label="Page URL" /> | ||||
|             <div class="p-1 w-full"> | ||||
|               <ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" label="Folder" @input="folderUpdated" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.feedImageUrl" label="Feed Image URL" /> | ||||
|             <div class="p-1 w-full"> | ||||
|               <ui-text-input-with-label v-model="fullPath" label="Podcast Path" readonly /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-checkbox v-model="podcast.autoDownloadEpisodes" label="Auto Download Episodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> | ||||
| @ -39,14 +42,15 @@ | ||||
|         <div class="w-full md:w-1/2 p-4"> | ||||
|           <p class="text-lg font-semibold mb-2">Episodes</p> | ||||
|           <div ref="episodeContainer" id="episodes-scroll" class="w-full overflow-x-hidden overflow-y-auto"> | ||||
|             <div v-for="(episode, index) in episodes" :key="index" class="relative cursor-pointer" :class="index % 2 == 0 ? 'bg-primary bg-opacity-25 hover:bg-opacity-40' : 'bg-primary bg-opacity-5 hover:bg-opacity-25'" @click="toggleSelectEpisode(index)"> | ||||
|             <div v-for="(episode, index) in episodes" :key="index" class="relative cursor-pointer" :class="selectedEpisodes[String(index)] ? 'bg-success bg-opacity-10' : index % 2 == 0 ? 'bg-primary bg-opacity-25 hover:bg-opacity-40' : 'bg-primary bg-opacity-5 hover:bg-opacity-25'" @click="toggleSelectEpisode(index)"> | ||||
|               <div class="absolute top-0 left-0 h-full flex items-center p-2"> | ||||
|                 <ui-checkbox v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" /> | ||||
|               </div> | ||||
|               <div class="px-8 py-2"> | ||||
|                 <p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p> | ||||
|                 <p class="break-words">{{ episode.title }}</p> | ||||
|                 <p class="text-xs text-gray-300">Published {{ episode.pubDate || 'Unknown' }}</p> | ||||
|                 <p class="break-words mb-1">{{ episode.title }}</p> | ||||
|                 <p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p> | ||||
|                 <p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p> | ||||
|                 <!-- <span class="material-icons cursor-pointer text-lg hover:text-success" @click="saveEpisode(episode)">save</span> --> | ||||
|               </div> | ||||
|             </div> | ||||
| @ -62,6 +66,8 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import Path from 'path' | ||||
| 
 | ||||
| export default { | ||||
|   props: { | ||||
|     value: Boolean, | ||||
| @ -77,6 +83,8 @@ export default { | ||||
|   data() { | ||||
|     return { | ||||
|       processing: false, | ||||
|       selectedFolderId: null, | ||||
|       fullPath: null, | ||||
|       podcast: { | ||||
|         title: '', | ||||
|         author: '', | ||||
| @ -115,11 +123,27 @@ export default { | ||||
|     title() { | ||||
|       return this._podcastData.title | ||||
|     }, | ||||
|     currentLibrary() { | ||||
|       return this.$store.getters['libraries/getCurrentLibrary'] | ||||
|     }, | ||||
|     folders() { | ||||
|       if (!this.currentLibrary) return [] | ||||
|       return this.currentLibrary.folders || [] | ||||
|     }, | ||||
|     folderItems() { | ||||
|       return this.folders.map((fold) => { | ||||
|         return { | ||||
|           value: fold.id, | ||||
|           text: fold.fullPath | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     _podcastData() { | ||||
|       return this.podcastData || {} | ||||
|     }, | ||||
|     feedMetadata() { | ||||
|       return this._podcastData.metadata || {} | ||||
|       if (!this.podcastFeedData) return {} | ||||
|       return this.podcastFeedData.metadata || {} | ||||
|     }, | ||||
|     episodes() { | ||||
|       if (!this.podcastFeedData) return [] | ||||
| @ -135,9 +159,23 @@ export default { | ||||
|       if (!this.episodesSelected.length) return 'Add Podcast' | ||||
|       if (this.episodesSelected.length == 1) return 'Add Podcast & Download 1 Episode' | ||||
|       return `Add Podcast & Download ${this.episodesSelected.length} Episodes` | ||||
|     }, | ||||
|     selectedFolder() { | ||||
|       return this.folders.find((f) => f.id === this.selectedFolderId) | ||||
|     }, | ||||
|     selectedFolderPath() { | ||||
|       if (!this.selectedFolder) return '' | ||||
|       return this.selectedFolder.fullPath | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     folderUpdated() { | ||||
|       if (!this.selectedFolderPath || !this.podcast.title) { | ||||
|         this.fullPath = '' | ||||
|         return | ||||
|       } | ||||
|       this.fullPath = Path.join(this.selectedFolderPath, this.podcast.title) | ||||
|     }, | ||||
|     toggleSelectEpisode(index) { | ||||
|       this.selectedEpisodes[String(index)] = !this.selectedEpisodes[String(index)] | ||||
|     }, | ||||
| @ -152,15 +190,21 @@ export default { | ||||
|       this.podcast.releaseDate = this._podcastData.releaseDate || '' | ||||
|       this.podcast.genres = this._podcastData.genres || [] | ||||
|       this.podcast.feedUrl = this._podcastData.feedUrl | ||||
|       this.podcast.feedImageUrl = this._podcastData.cover || '' | ||||
|       this.podcast.imageUrl = this._podcastData.cover || '' | ||||
|       this.podcast.itunesPageUrl = this._podcastData.pageUrl || '' | ||||
|       this.podcast.itunesId = this._podcastData.id || '' | ||||
|       this.podcast.itunesArtistId = this._podcastData.artistId || '' | ||||
|       this.podcast.language = this._podcastData.language || '' | ||||
|       this.podcast.autoDownloadEpisodes = false | ||||
| 
 | ||||
|       for (let i = 0; i < this.episodes.length; i++) { | ||||
|         this.$set(this.selectedEpisodes, String(i), false) | ||||
|       } | ||||
| 
 | ||||
|       if (this.folderItems[0]) { | ||||
|         this.selectedFolderId = this.folderItems[0].value | ||||
|         this.folderUpdated() | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -174,4 +218,14 @@ export default { | ||||
| #episodes-scroll { | ||||
|   max-height: calc(80vh - 200px); | ||||
| } | ||||
| .episode-subtitle { | ||||
|   word-break: break-word; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   display: -webkit-box; | ||||
|   line-height: 16px; /* fallback */ | ||||
|   max-height: 32px; /* fallback */ | ||||
|   -webkit-line-clamp: 2; /* number of lines to show */ | ||||
|   -webkit-box-orient: vertical; | ||||
| } | ||||
| </style> | ||||
| @ -3,7 +3,7 @@ | ||||
|     <p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''"> | ||||
|       {{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em> | ||||
|     </p> | ||||
|     <ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :type="type" class="w-full" /> | ||||
|     <ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -17,6 +17,7 @@ export default { | ||||
|       type: String, | ||||
|       default: 'text' | ||||
|     }, | ||||
|     readonly: Boolean, | ||||
|     disabled: Boolean | ||||
|   }, | ||||
|   data() { | ||||
|  | ||||
| @ -19,6 +19,24 @@ class LibraryController { | ||||
|       return res.status(500).send('Invalid request') | ||||
|     } | ||||
| 
 | ||||
|     // Validate folder paths exist or can be created & resolve rel paths
 | ||||
|     //   returns 400 if a folder fails to access
 | ||||
|     newLibraryPayload.folders = newLibraryPayload.folders.map(f => { | ||||
|       f.fullPath = Path.resolve(f.fullPath) | ||||
|       return f | ||||
|     }) | ||||
|     for (var folder of newLibraryPayload.folders) { | ||||
|       var success = await fs.ensureDir(folder.fullPath).then(() => true).catch((error) => { | ||||
|         Logger.error(`[LibraryController] Failed to ensure folder dir "${folder.fullPath}"`, error) | ||||
|         return false | ||||
|       }) | ||||
|       if (!success) { | ||||
|         return res.status(400).send(`Invalid folder directory "${folder.fullPath}"`) | ||||
|       } else { | ||||
|         await filePerms.setDefault(folder.fullPath) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     var library = new Library() | ||||
|     newLibraryPayload.displayOrder = this.db.libraries.length + 1 | ||||
|     library.setData(newLibraryPayload) | ||||
|  | ||||
| @ -88,6 +88,7 @@ class Library { | ||||
|     this.displayOrder = data.displayOrder || 1 | ||||
|     this.icon = data.icon || 'database' | ||||
|     this.mediaType = data.mediaType || 'book' | ||||
|     this.provider = data.provider || 'google' | ||||
|     this.disableWatcher = !!data.disableWatcher | ||||
|     this.createdAt = Date.now() | ||||
|     this.lastUpdate = Date.now() | ||||
|  | ||||
| @ -8,6 +8,11 @@ class PodcastEpisode { | ||||
|     this.podcastId = null | ||||
|     this.episodeNumber = null | ||||
| 
 | ||||
|     this.title = null | ||||
|     this.description = null | ||||
|     this.enclosure = null | ||||
|     this.pubDate = null | ||||
| 
 | ||||
|     this.audioFile = null | ||||
|     this.addedAt = null | ||||
|     this.updatedAt = null | ||||
| @ -22,6 +27,10 @@ class PodcastEpisode { | ||||
|     this.index = episode.index | ||||
|     this.podcastId = episode.podcastId | ||||
|     this.episodeNumber = episode.episodeNumber | ||||
|     this.title = episode.title | ||||
|     this.description = episode.description | ||||
|     this.enclosure = episode.enclosure ? { ...episode.enclosure } : null | ||||
|     this.pubDate = episode.pubDate | ||||
|     this.audioFile = new AudioFile(episode.audioFile) | ||||
|     this.addedAt = episode.addedAt | ||||
|     this.updatedAt = episode.updatedAt | ||||
| @ -33,6 +42,10 @@ class PodcastEpisode { | ||||
|       index: this.index, | ||||
|       podcastId: this.podcastId, | ||||
|       episodeNumber: this.episodeNumber, | ||||
|       title: this.title, | ||||
|       description: this.description, | ||||
|       enclosure: this.enclosure ? { ...this.enclosure } : null, | ||||
|       pubDate: this.pubDate, | ||||
|       audioFile: this.audioFile.toJSON(), | ||||
|       addedAt: this.addedAt, | ||||
|       updatedAt: this.updatedAt | ||||
|  | ||||
| @ -6,11 +6,12 @@ class PodcastMetadata { | ||||
|     this.releaseDate = null | ||||
|     this.genres = [] | ||||
|     this.feedUrl = null | ||||
|     this.feedImageUrl = null | ||||
|     this.imageUrl = null | ||||
|     this.itunesPageUrl = null | ||||
|     this.itunesId = null | ||||
|     this.itunesArtistId = null | ||||
|     this.explicit = false | ||||
|     this.language = null | ||||
| 
 | ||||
|     if (metadata) { | ||||
|       this.construct(metadata) | ||||
| @ -24,11 +25,12 @@ class PodcastMetadata { | ||||
|     this.releaseDate = metadata.releaseDate | ||||
|     this.genres = [...metadata.genres] | ||||
|     this.feedUrl = metadata.feedUrl | ||||
|     this.feedImageUrl = metadata.feedImageUrl | ||||
|     this.imageUrl = metadata.imageUrl | ||||
|     this.itunesPageUrl = metadata.itunesPageUrl | ||||
|     this.itunesId = metadata.itunesId | ||||
|     this.itunesArtistId = metadata.itunesArtistId | ||||
|     this.explicit = metadata.explicit | ||||
|     this.language = metadata.language || null | ||||
|   } | ||||
| 
 | ||||
|   toJSON() { | ||||
| @ -39,11 +41,12 @@ class PodcastMetadata { | ||||
|       releaseDate: this.releaseDate, | ||||
|       genres: [...this.genres], | ||||
|       feedUrl: this.feedUrl, | ||||
|       feedImageUrl: this.feedImageUrl, | ||||
|       imageUrl: this.imageUrl, | ||||
|       itunesPageUrl: this.itunesPageUrl, | ||||
|       itunesId: this.itunesId, | ||||
|       itunesArtistId: this.itunesArtistId, | ||||
|       explicit: this.explicit | ||||
|       explicit: this.explicit, | ||||
|       language: this.language | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -57,7 +57,7 @@ function extractEpisodeData(item) { | ||||
|     Logger.error(`[podcastUtils] Invalid podcast episode data`) | ||||
|     return null | ||||
|   } | ||||
|   var arrayFields = ['title', 'pubDate', 'description', 'itunes:episodeType', 'itunes:episode', 'itunes:author', 'itunes:duration', 'itunes:explicit'] | ||||
|   var arrayFields = ['title', 'pubDate', 'description', 'itunes:episodeType', 'itunes:episode', 'itunes:author', 'itunes:duration', 'itunes:explicit', 'itunes:subtitle'] | ||||
|   var episode = { | ||||
|     enclosure: { | ||||
|       ...item.enclosure[0]['$'] | ||||
| @ -70,12 +70,27 @@ function extractEpisodeData(item) { | ||||
|   return episode | ||||
| } | ||||
| 
 | ||||
| function cleanEpisodeData(data) { | ||||
|   return { | ||||
|     title: data.title, | ||||
|     subtitle: data.subtitle || '', | ||||
|     description: data.description || '', | ||||
|     pubDate: data.pubDate || '', | ||||
|     episodeType: data.episodeType || '', | ||||
|     episode: data.episode || '', | ||||
|     author: data.author || '', | ||||
|     duration: data.duration || '', | ||||
|     explicit: data.explicit || '', | ||||
|     publishedAt: (new Date(data.pubDate)).valueOf() | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function extractPodcastEpisodes(items) { | ||||
|   var episodes = [] | ||||
|   items.forEach((item) => { | ||||
|     var cleaned = extractEpisodeData(item) | ||||
|     if (cleaned) { | ||||
|       episodes.push(cleaned) | ||||
|     var extracted = extractEpisodeData(item) | ||||
|     if (extracted) { | ||||
|       episodes.push(cleanEpisodeData(extracted)) | ||||
|     } | ||||
|   }) | ||||
|   return episodes | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user