mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	New model updates for series, collections, authors routes
This commit is contained in:
		
							parent
							
								
									73257188f6
								
							
						
					
					
						commit
						2d19208340
					
				| @ -155,72 +155,72 @@ export default { | ||||
|     scan() { | ||||
|       this.$root.socket.emit('scan', this.$store.state.libraries.currentLibraryId) | ||||
|     }, | ||||
|     audiobookAdded(audiobook) { | ||||
|       console.log('Audiobook added', audiobook) | ||||
|       // TODO: Check if audiobook would be on this shelf | ||||
|     libraryItemAdded(libraryItem) { | ||||
|       console.log('libraryItem added', libraryItem) | ||||
|       // TODO: Check if libraryItem would be on this shelf | ||||
|       if (!this.search) { | ||||
|         this.fetchCategories() | ||||
|       } | ||||
|     }, | ||||
|     audiobookUpdated(audiobook) { | ||||
|       console.log('Audiobook updated', audiobook) | ||||
|     libraryItemUpdated(libraryItem) { | ||||
|       console.log('libraryItem updated', libraryItem) | ||||
|       this.shelves.forEach((shelf) => { | ||||
|         if (shelf.type === 'books') { | ||||
|           shelf.entities = shelf.entities.map((ent) => { | ||||
|             if (ent.id === audiobook.id) { | ||||
|               return audiobook | ||||
|             if (ent.id === libraryItem.id) { | ||||
|               return libraryItem | ||||
|             } | ||||
|             return ent | ||||
|           }) | ||||
|         } else if (shelf.type === 'series') { | ||||
|           shelf.entities.forEach((ent) => { | ||||
|             ent.books = ent.books.map((book) => { | ||||
|               if (book.id === audiobook.id) return audiobook | ||||
|               if (book.id === libraryItem.id) return libraryItem | ||||
|               return book | ||||
|             }) | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     removeBookFromShelf(audiobook) { | ||||
|     removeBookFromShelf(libraryItem) { | ||||
|       this.shelves.forEach((shelf) => { | ||||
|         if (shelf.type === 'books') { | ||||
|           shelf.entities = shelf.entities.filter((ent) => { | ||||
|             return ent.id !== audiobook.id | ||||
|             return ent.id !== libraryItem.id | ||||
|           }) | ||||
|         } else if (shelf.type === 'series') { | ||||
|           shelf.entities.forEach((ent) => { | ||||
|             ent.books = ent.books.filter((book) => { | ||||
|               return book.id !== audiobook.id | ||||
|               return book.id !== libraryItem.id | ||||
|             }) | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     audiobookRemoved(audiobook) { | ||||
|       this.removeBookFromShelf(audiobook) | ||||
|     libraryItemRemoved(libraryItem) { | ||||
|       this.removeBookFromShelf(libraryItem) | ||||
|     }, | ||||
|     audiobooksAdded(audiobooks) { | ||||
|       console.log('audiobooks added', audiobooks) | ||||
|     libraryItemsAdded(libraryItems) { | ||||
|       console.log('libraryItems added', libraryItems) | ||||
|       // TODO: Check if audiobook would be on this shelf | ||||
|       if (!this.search) { | ||||
|         this.fetchCategories() | ||||
|       } | ||||
|     }, | ||||
|     audiobooksUpdated(audiobooks) { | ||||
|       audiobooks.forEach((ab) => { | ||||
|         this.audiobookUpdated(ab) | ||||
|     libraryItemsUpdated(items) { | ||||
|       items.forEach((li) => { | ||||
|         this.libraryItemUpdated(li) | ||||
|       }) | ||||
|     }, | ||||
|     initListeners() { | ||||
|       this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated }) | ||||
| 
 | ||||
|       if (this.$root.socket) { | ||||
|         this.$root.socket.on('audiobook_updated', this.audiobookUpdated) | ||||
|         this.$root.socket.on('audiobook_added', this.audiobookAdded) | ||||
|         this.$root.socket.on('audiobook_removed', this.audiobookRemoved) | ||||
|         this.$root.socket.on('audiobooks_updated', this.audiobooksUpdated) | ||||
|         this.$root.socket.on('audiobooks_added', this.audiobooksAdded) | ||||
|         this.$root.socket.on('item_updated', this.libraryItemUpdated) | ||||
|         this.$root.socket.on('item_added', this.libraryItemAdded) | ||||
|         this.$root.socket.on('item_removed', this.libraryItemRemoved) | ||||
|         this.$root.socket.on('items_updated', this.libraryItemsUpdated) | ||||
|         this.$root.socket.on('items_added', this.libraryItemsAdded) | ||||
|       } else { | ||||
|         console.error('Error socket not initialized') | ||||
|       } | ||||
| @ -229,11 +229,11 @@ export default { | ||||
|       this.$store.commit('user/removeSettingsListener', 'bookshelf') | ||||
| 
 | ||||
|       if (this.$root.socket) { | ||||
|         this.$root.socket.off('audiobook_updated', this.audiobookUpdated) | ||||
|         this.$root.socket.off('audiobook_added', this.audiobookAdded) | ||||
|         this.$root.socket.off('audiobook_removed', this.audiobookRemoved) | ||||
|         this.$root.socket.off('audiobooks_updated', this.audiobooksUpdated) | ||||
|         this.$root.socket.off('audiobooks_added', this.audiobooksAdded) | ||||
|         this.$root.socket.off('item_updated', this.libraryItemUpdated) | ||||
|         this.$root.socket.off('item_added', this.libraryItemAdded) | ||||
|         this.$root.socket.off('item_removed', this.libraryItemRemoved) | ||||
|         this.$root.socket.off('items_updated', this.libraryItemsUpdated) | ||||
|         this.$root.socket.off('items_added', this.libraryItemsAdded) | ||||
|       } else { | ||||
|         console.error('Error socket not initialized') | ||||
|       } | ||||
|  | ||||
| @ -374,7 +374,7 @@ export default { | ||||
| 
 | ||||
|       let searchParams = new URLSearchParams() | ||||
|       if (this.page === 'series-books') { | ||||
|         searchParams.set('filter', `series.${this.seriesId}`) | ||||
|         searchParams.set('filter', `series.${this.$encode(this.seriesId)}`) | ||||
|         searchParams.set('sort', 'book.volumeNumber') | ||||
|         searchParams.set('desc', 0) | ||||
|       } else { | ||||
|  | ||||
| @ -48,6 +48,9 @@ export default { | ||||
|     _author() { | ||||
|       return this.author || {} | ||||
|     }, | ||||
|     authorId() { | ||||
|       return this._author.id | ||||
|     }, | ||||
|     name() { | ||||
|       return this._author.name || '' | ||||
|     }, | ||||
|  | ||||
| @ -51,6 +51,9 @@ export default { | ||||
|       if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2) | ||||
|       return this.width / 240 | ||||
|     }, | ||||
|     seriesId() { | ||||
|       return this.series ? this.series.id : '' | ||||
|     }, | ||||
|     title() { | ||||
|       return this.series ? this.series.name : '' | ||||
|     }, | ||||
| @ -64,13 +67,10 @@ export default { | ||||
|       return this.store.state.libraries.currentLibraryId | ||||
|     }, | ||||
|     seriesBooksRoute() { | ||||
|       return `/library/${this.currentLibraryId}/series/${this.$encode(this.title)}` | ||||
|     }, | ||||
|     seriesId() { | ||||
|       return this.series ? this.$encode(this.title) : null | ||||
|       return `/library/${this.currentLibraryId}/series/${this.seriesId}` | ||||
|     }, | ||||
|     hasValidCovers() { | ||||
|       var validCovers = this.books.map((bookItem) => bookItem.book.cover) | ||||
|       var validCovers = this.books.map((bookItem) => bookItem.media.coverPath) | ||||
|       return !!validCovers.length | ||||
|     } | ||||
|   }, | ||||
|  | ||||
| @ -13,8 +13,8 @@ | ||||
|     <div v-else-if="books.length" class="flex justify-center h-full relative bg-primary bg-opacity-95 rounded-sm"> | ||||
|       <div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" /> | ||||
| 
 | ||||
|       <covers-book-cover :audiobook="books[0]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||
|       <covers-book-cover v-if="books.length > 1" :audiobook="books[1]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||
|       <covers-book-cover :library-item="books[0]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||
|       <covers-book-cover v-if="books.length > 1" :library-item="books[1]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||
|     </div> | ||||
|     <div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm"> | ||||
|       <div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" /> | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="h-full relative" :style="{ width: coverWidth + 'px' }"> | ||||
|         <covers-book-cover :audiobook="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||
|         <covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||
|         <div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn"> | ||||
|           <div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick"> | ||||
|             <span class="material-icons">play_arrow</span> | ||||
| @ -16,8 +16,8 @@ | ||||
|       </div> | ||||
|       <div class="w-80 h-full px-2 flex items-center"> | ||||
|         <div> | ||||
|           <nuxt-link :to="`/audiobook/${book.id}`" class="truncate hover:underline">{{ bookTitle }}</nuxt-link> | ||||
|           <nuxt-link :to="`/library/${book.libraryId}/bookshelf?filter=authors.${$encode(bookAuthor)}`" class="truncate block text-gray-400 text-sm hover:underline">{{ bookAuthor }}</nuxt-link> | ||||
|           <nuxt-link :to="`/item/${book.id}`" class="truncate hover:underline">{{ bookTitle }}</nuxt-link> | ||||
|           <!-- <nuxt-link :to="`/library/${book.libraryId}/bookshelf?filter=authors.${$encode(bookAuthor)}`" class="truncate block text-gray-400 text-sm hover:underline">{{ bookAuthor }}</nuxt-link> --> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="flex-grow flex items-center"> | ||||
| @ -83,17 +83,20 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     _book() { | ||||
|       return this.book.book || {} | ||||
|     media() { | ||||
|       return this.book.media || {} | ||||
|     }, | ||||
|     mediaMetadata() { | ||||
|       return this.media.metadata || {} | ||||
|     }, | ||||
|     bookTitle() { | ||||
|       return this._book.title || '' | ||||
|       return this.mediaMetadata.title || '' | ||||
|     }, | ||||
|     bookAuthor() { | ||||
|       return this._book.authorFL || '' | ||||
|       return (this.mediaMetadata.authors || []).map((au) => au.name).join(', ') | ||||
|     }, | ||||
|     bookDuration() { | ||||
|       return this.$secondsToTimestamp(this.book.duration) | ||||
|       return this.$secondsToTimestamp(this.media.duration) | ||||
|     }, | ||||
|     isMissing() { | ||||
|       return this.book.isMissing | ||||
| @ -102,7 +105,7 @@ export default { | ||||
|       return this.book.isInvalid | ||||
|     }, | ||||
|     numTracks() { | ||||
|       return this.book.numTracks | ||||
|       return this.media.tracks.length | ||||
|     }, | ||||
|     isStreaming() { | ||||
|       return this.$store.getters['getAudiobookIdStreaming'] === this.book.id | ||||
|  | ||||
| @ -168,7 +168,6 @@ export default { | ||||
|       if (this.$refs.streamContainer) this.$refs.streamContainer.streamError(id) | ||||
|     }, | ||||
|     audiobookAdded(audiobook) { | ||||
|       // this.$store.commit('audiobooks/addUpdate', audiobook) | ||||
|       this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook) | ||||
|     }, | ||||
|     audiobooksAdded(audiobooks) { | ||||
| @ -179,7 +178,6 @@ export default { | ||||
|     audiobooksUpdated(audiobooks) { | ||||
|       audiobooks.forEach((ab) => { | ||||
|         this.audiobookUpdated(ab) | ||||
|         // this.$store.commit('audiobooks/addUpdate', ab) | ||||
|       }) | ||||
|     }, | ||||
|     libraryAdded(library) { | ||||
|  | ||||
| @ -52,9 +52,6 @@ export default { | ||||
|       return redirect('/') | ||||
|     } | ||||
|     store.commit('user/addUpdateCollection', collection) | ||||
|     collection.books.forEach((book) => { | ||||
|       store.commit('audiobooks/addUpdate', book) | ||||
|     }) | ||||
|     return { | ||||
|       collectionId: collection.id | ||||
|     } | ||||
|  | ||||
							
								
								
									
										280
									
								
								client/pages/item/_id/edit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								client/pages/item/_id/edit.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,280 @@ | ||||
| <template> | ||||
|   <div id="page-wrapper" class="bg-bg page overflow-hidden relative" :class="streamAudiobook ? 'streaming' : ''"> | ||||
|     <div v-show="saving" class="absolute z-20 w-full h-full flex items-center justify-center"> | ||||
|       <ui-loading-indicator /> | ||||
|     </div> | ||||
|     <div class="w-full h-full overflow-y-auto p-8"> | ||||
|       <div class="w-full flex justify-between items-center pb-6 pt-2"> | ||||
|         <p class="text-lg">Drag files into correct track order</p> | ||||
|         <ui-btn color="success" @click="saveTracklist">Save Tracklist</ui-btn> | ||||
|       </div> | ||||
|       <div class="w-full flex items-center text-sm py-4 bg-primary border-l border-r border-t border-gray-600"> | ||||
|         <div class="font-book text-center px-4 w-12">New</div> | ||||
|         <div class="font-book text-center px-4 w-24 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByCurrent" @mousedown.prevent> | ||||
|           <span class="text-white">Current</span> | ||||
|           <span class="material-icons ml-1" :class="currentSort === 'current' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'current' ? 'expand_more' : 'unfold_more' }}</span> | ||||
|         </div> | ||||
|         <div class="font-book text-center px-4 w-32 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByFilenameTrack" @mousedown.prevent> | ||||
|           <span class="text-white">Track From Filename</span> | ||||
|           <span class="material-icons ml-1" :class="currentSort === 'track-filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'track-filename' ? 'expand_more' : 'unfold_more' }}</span> | ||||
|         </div> | ||||
|         <div class="font-book text-center px-4 w-32 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByMetadataTrack" @mousedown.prevent> | ||||
|           <span class="text-white">Track From Metadata</span> | ||||
|           <span class="material-icons ml-1" :class="currentSort === 'metadata' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'metadata' ? 'expand_more' : 'unfold_more' }}</span> | ||||
|         </div> | ||||
|         <div class="font-mono w-20 text-center">Disc From Filename</div> | ||||
|         <div class="font-mono w-20 text-center">Disc From Metadata</div> | ||||
|         <div class="font-book text-center px-4 flex-grow flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByFilename" @mousedown.prevent> | ||||
|           <span class="text-white">Filename</span> | ||||
|           <span class="material-icons ml-1" :class="currentSort === 'filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'filename' ? 'expand_more' : 'unfold_more' }}</span> | ||||
|         </div> | ||||
|         <!-- <div class="font-book truncate px-4 flex-grow">Filename</div> --> | ||||
| 
 | ||||
|         <div class="font-mono w-20 text-center">Size</div> | ||||
|         <div class="font-mono w-20 text-center">Duration</div> | ||||
|         <div class="font-mono text-center w-20">Status</div> | ||||
|         <div class="font-mono w-56">Notes</div> | ||||
|         <div class="font-book w-40">Include in Tracklist</div> | ||||
|       </div> | ||||
|       <draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate"> | ||||
|         <transition-group type="transition" :name="!drag ? 'flip-list' : null"> | ||||
|           <li v-for="(audio, index) in files" :key="audio.path" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center"> | ||||
|             <div class="font-book text-center px-4 py-1 w-12"> | ||||
|               {{ audio.include ? index - numExcluded + 1 : -1 }} | ||||
|             </div> | ||||
|             <div class="font-book text-center px-4 w-24">{{ audio.index }}</div> | ||||
|             <div class="font-book text-center px-2 w-32"> | ||||
|               {{ audio.trackNumFromFilename }} | ||||
|             </div> | ||||
|             <div class="font-book text-center w-32"> | ||||
|               {{ audio.trackNumFromMeta }} | ||||
|             </div> | ||||
|             <div class="font-book truncate px-4 w-20"> | ||||
|               {{ audio.discNumFromFilename }} | ||||
|             </div> | ||||
|             <div class="font-book truncate px-4 w-20"> | ||||
|               {{ audio.discNumFromMeta }} | ||||
|             </div> | ||||
|             <div class="font-book truncate px-4 flex-grow"> | ||||
|               {{ audio.filename }} | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="font-mono w-20 text-center"> | ||||
|               {{ $bytesPretty(audio.size) }} | ||||
|             </div> | ||||
|             <div class="font-mono w-20"> | ||||
|               {{ $secondsToTimestamp(audio.duration) }} | ||||
|             </div> | ||||
|             <div class="font-mono text-center w-20"> | ||||
|               <span class="material-icons text-sm" :class="audio.invalid ? 'text-error' : 'text-success'">{{ getStatusIcon(audio) }}</span> | ||||
|             </div> | ||||
|             <div class="font-sans text-xs font-normal w-56"> | ||||
|               {{ audio.error }} | ||||
|             </div> | ||||
|             <div class="font-sans text-xs font-normal w-40 flex justify-center"> | ||||
|               <ui-toggle-switch v-model="audio.include" :off-color="'error'" @input="includeToggled(audio)" /> | ||||
|             </div> | ||||
|           </li> | ||||
|         </transition-group> | ||||
|       </draggable> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import draggable from 'vuedraggable' | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     draggable | ||||
|   }, | ||||
|   async asyncData({ store, params, app, redirect, route }) { | ||||
|     if (!store.state.user.user) { | ||||
|       return redirect(`/login?redirect=${route.path}`) | ||||
|     } | ||||
|     if (!store.getters['user/getUserCanUpdate']) { | ||||
|       return redirect('/?error=unauthorized') | ||||
|     } | ||||
|     var audiobook = await app.$axios.$get(`/api/books/${params.id}`).catch((error) => { | ||||
|       console.error('Failed', error) | ||||
|       return false | ||||
|     }) | ||||
|     if (!audiobook) { | ||||
|       console.error('No audiobook...', params.id) | ||||
|       return redirect('/') | ||||
|     } | ||||
|     return { | ||||
|       audiobook, | ||||
|       files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : [] | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       drag: false, | ||||
|       dragOptions: { | ||||
|         animation: 200, | ||||
|         group: 'description', | ||||
|         ghostClass: 'ghost' | ||||
|       }, | ||||
|       saving: false, | ||||
|       currentSort: 'current' | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     audioFiles() { | ||||
|       return this.audiobook.audioFiles || [] | ||||
|     }, | ||||
|     numExcluded() { | ||||
|       var count = 0 | ||||
|       this.files.forEach((file) => { | ||||
|         if (!file.include) count++ | ||||
|       }) | ||||
|       return count | ||||
|     }, | ||||
|     missingPartChunks() { | ||||
|       if (this.missingParts === 1) return this.missingParts[0] | ||||
|       var chunks = [] | ||||
| 
 | ||||
|       var currentIndex = this.missingParts[0] | ||||
|       var currentChunk = [this.missingParts[0]] | ||||
| 
 | ||||
|       for (let i = 1; i < this.missingParts.length; i++) { | ||||
|         var partIndex = this.missingParts[i] | ||||
|         if (currentIndex === partIndex - 1) { | ||||
|           currentChunk.push(partIndex) | ||||
|           currentIndex = partIndex | ||||
|         } else { | ||||
|           // console.log('Chunk ended', currentChunk.join(', '), currentIndex, partIndex) | ||||
|           if (currentChunk.length === 0) { | ||||
|             console.error('How is current chunk 0?', currentChunk.join(', ')) | ||||
|           } | ||||
|           chunks.push(currentChunk) | ||||
|           currentChunk = [partIndex] | ||||
|           currentIndex = partIndex | ||||
|         } | ||||
|       } | ||||
|       if (currentChunk.length) { | ||||
|         chunks.push(currentChunk) | ||||
|       } | ||||
|       chunks = chunks.map((chunk) => { | ||||
|         if (chunk.length === 1) return chunk[0] | ||||
|         else return `${chunk[0]}-${chunk[chunk.length - 1]}` | ||||
|       }) | ||||
|       return chunks | ||||
|     }, | ||||
|     missingParts() { | ||||
|       return this.audiobook.missingParts || [] | ||||
|     }, | ||||
|     invalidParts() { | ||||
|       return this.audiobook.invalidParts || [] | ||||
|     }, | ||||
|     audiobookId() { | ||||
|       return this.audiobook.id | ||||
|     }, | ||||
|     title() { | ||||
|       return this.book.title || 'No Title' | ||||
|     }, | ||||
|     author() { | ||||
|       return this.book.author || 'Unknown' | ||||
|     }, | ||||
|     tracks() { | ||||
|       return this.audiobook.tracks | ||||
|     }, | ||||
|     durationPretty() { | ||||
|       return this.audiobook.durationPretty | ||||
|     }, | ||||
|     sizePretty() { | ||||
|       return this.audiobook.sizePretty | ||||
|     }, | ||||
|     book() { | ||||
|       return this.audiobook.book || {} | ||||
|     }, | ||||
|     tracks() { | ||||
|       return this.audiobook.tracks || [] | ||||
|     }, | ||||
|     streamAudiobook() { | ||||
|       return this.$store.state.streamAudiobook | ||||
|     }, | ||||
|     showExperimentalFeatures() { | ||||
|       return this.$store.state.showExperimentalFeatures | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     draggableUpdate(e) { | ||||
|       this.currentSort = '' | ||||
|     }, | ||||
|     sortByCurrent() { | ||||
|       this.files.sort((a, b) => { | ||||
|         if (a.index === null) return 1 | ||||
|         return a.index - b.index | ||||
|       }) | ||||
|       this.currentSort = 'current' | ||||
|     }, | ||||
|     sortByMetadataTrack() { | ||||
|       this.files.sort((a, b) => { | ||||
|         if (a.trackNumFromMeta === null) return 1 | ||||
|         return a.trackNumFromMeta - b.trackNumFromMeta | ||||
|       }) | ||||
|       this.currentSort = 'metadata' | ||||
|     }, | ||||
|     sortByFilenameTrack() { | ||||
|       this.files.sort((a, b) => { | ||||
|         if (a.trackNumFromFilename === null) return 1 | ||||
|         return a.trackNumFromFilename - b.trackNumFromFilename | ||||
|       }) | ||||
|       this.currentSort = 'track-filename' | ||||
|     }, | ||||
|     sortByFilename() { | ||||
|       this.files.sort((a, b) => { | ||||
|         return (a.filename || '').toLowerCase().localeCompare((b.filename || '').toLowerCase()) | ||||
|       }) | ||||
|       this.currentSort = 'filename' | ||||
|     }, | ||||
|     includeToggled(audio) { | ||||
|       var new_index = 0 | ||||
|       if (audio.include) { | ||||
|         new_index = this.numExcluded | ||||
|       } | ||||
|       var old_index = this.files.findIndex((f) => f.ino === audio.ino) | ||||
|       if (new_index >= this.files.length) { | ||||
|         var k = new_index - this.files.length + 1 | ||||
|         while (k--) { | ||||
|           this.files.push(undefined) | ||||
|         } | ||||
|       } | ||||
|       this.files.splice(new_index, 0, this.files.splice(old_index, 1)[0]) | ||||
|     }, | ||||
|     saveTracklist() { | ||||
|       var orderedFileData = this.files.map((file) => { | ||||
|         return { | ||||
|           index: file.index, | ||||
|           filename: file.filename, | ||||
|           ino: file.ino, | ||||
|           exclude: !file.include | ||||
|         } | ||||
|       }) | ||||
| 
 | ||||
|       this.saving = true | ||||
|       this.$axios | ||||
|         .$patch(`/api/books/${this.audiobook.id}/tracks`, { orderedFileData }) | ||||
|         .then((data) => { | ||||
|           console.log('Finished patching files', data) | ||||
|           this.saving = false | ||||
|           this.$toast.success('Tracks Updated') | ||||
|           this.$router.push(`/audiobook/${this.audiobookId}`) | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed', error) | ||||
|           this.saving = false | ||||
|         }) | ||||
|     }, | ||||
|     getStatusIcon(audio) { | ||||
|       if (audio.invalid) { | ||||
|         return 'error_outline' | ||||
|       } else { | ||||
|         return 'check_circle' | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -7,7 +7,7 @@ | ||||
|         <div id="bookshelf" class="w-full h-full p-8 overflow-y-auto"> | ||||
|           <div class="flex flex-wrap justify-center"> | ||||
|             <template v-for="author in authors"> | ||||
|               <nuxt-link :key="author.name" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.name)}`"> | ||||
|               <nuxt-link :key="author.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`"> | ||||
|                 <cards-author-card :author="author" :width="160" :height="160" class="p-3" /> | ||||
|               </nuxt-link> | ||||
|             </template> | ||||
|  | ||||
| @ -18,9 +18,16 @@ export default { | ||||
|     if (!library) { | ||||
|       return redirect('/oops?message=Library not found') | ||||
|     } | ||||
|     var series = await app.$axios.$get(`/api/series/${params.id}`).catch((error) => { | ||||
|       console.error('Failed', error) | ||||
|       return false | ||||
|     }) | ||||
|     if (!series) { | ||||
|       return redirect('/oops?message=Series not found') | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       series: app.$decode(params.id), | ||||
|       series: series.name, | ||||
|       seriesId: params.id | ||||
|     } | ||||
|   }, | ||||
|  | ||||
| @ -73,59 +73,6 @@ export const mutations = { | ||||
|       listener.meth() | ||||
|     }) | ||||
|   }, | ||||
|   addUpdate(state, audiobook) { | ||||
|     if (state.loadedLibraryId && audiobook.libraryId !== state.loadedLibraryId) { | ||||
|       console.warn('Invalid library', audiobook, 'loaded library', state.loadedLibraryId, '"') | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     var index = state.audiobooks.findIndex(a => a.id === audiobook.id) | ||||
|     var origAudiobook = null | ||||
|     if (index >= 0) { | ||||
|       origAudiobook = { ...state.audiobooks[index] } | ||||
|       state.audiobooks.splice(index, 1, audiobook) | ||||
|     } else { | ||||
|       state.audiobooks.push(audiobook) | ||||
|     } | ||||
| 
 | ||||
|     if (audiobook.book) { | ||||
|       // GENRES
 | ||||
|       var newGenres = [] | ||||
|       audiobook.book.genres.forEach((genre) => { | ||||
|         if (!state.genres.includes(genre)) newGenres.push(genre) | ||||
|       }) | ||||
|       if (newGenres.length) { | ||||
|         state.genres = state.genres.concat(newGenres) | ||||
|         state.genres.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1) | ||||
|       } | ||||
| 
 | ||||
|       // SERIES
 | ||||
|       if (audiobook.book.series && !state.series.includes(audiobook.book.series)) { | ||||
|         state.series.push(audiobook.book.series) | ||||
|         state.series.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1) | ||||
|       } | ||||
|       if (origAudiobook && origAudiobook.book && origAudiobook.book.series) { | ||||
|         var isInAB = state.audiobooks.find(ab => ab.book && ab.book.series === origAudiobook.book.series) | ||||
|         if (!isInAB) state.series = state.series.filter(series => series !== origAudiobook.book.series) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // TAGS
 | ||||
|     var newTags = [] | ||||
|     audiobook.tags.forEach((tag) => { | ||||
|       if (!state.tags.includes(tag)) newTags.push(tag) | ||||
|     }) | ||||
|     if (newTags.length) { | ||||
|       state.tags = state.tags.concat(newTags) | ||||
|       state.tags.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1) | ||||
|     } | ||||
| 
 | ||||
|     state.listeners.forEach((listener) => { | ||||
|       if (!listener.audiobookId || listener.audiobookId === audiobook.id) { | ||||
|         listener.meth() | ||||
|       } | ||||
|     }) | ||||
|   }, | ||||
|   remove(state, audiobook) { | ||||
|     state.audiobooks = state.audiobooks.filter(a => a.id !== audiobook.id) | ||||
| 
 | ||||
|  | ||||
| @ -67,7 +67,6 @@ class ApiController { | ||||
|     this.router.post('/libraries/order', LibraryController.reorder.bind(this)) | ||||
| 
 | ||||
|     // Legacy
 | ||||
|     this.router.get('/libraries/:id/books/all', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary.bind(this)) | ||||
|     this.router.get('/libraries/:id/categories', LibraryController.middleware.bind(this), LibraryController.getLibraryCategories.bind(this)) | ||||
|     this.router.get('/libraries/:id/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this)) | ||||
| 
 | ||||
| @ -171,6 +170,7 @@ class ApiController { | ||||
|     //
 | ||||
|     // Series Routes
 | ||||
|     //
 | ||||
|     this.router.get('/series/:id', SeriesController.middleware.bind(this), SeriesController.findOne.bind(this)) | ||||
|     this.router.get('/series/search', SeriesController.search.bind(this)) | ||||
| 
 | ||||
| 
 | ||||
| @ -230,8 +230,7 @@ class ApiController { | ||||
|   } | ||||
| 
 | ||||
|   async getAuthors(req, res) { | ||||
|     var authors = this.db.authors.filter(p => p.isAuthor) | ||||
|     res.json(authors) | ||||
|     res.json(this.db.authors) | ||||
|   } | ||||
| 
 | ||||
|   searchAuthors(req, res) { | ||||
|  | ||||
| @ -11,7 +11,7 @@ class CollectionController { | ||||
|     if (!success) { | ||||
|       return res.status(500).send('Invalid collection data') | ||||
|     } | ||||
|     var jsonExpanded = newCollection.toJSONExpanded(this.db.audiobooks) | ||||
|     var jsonExpanded = newCollection.toJSONExpanded(this.db.libraryItems) | ||||
|     await this.db.insertEntity('collection', newCollection) | ||||
|     this.emitter('collection_added', jsonExpanded) | ||||
|     res.json(jsonExpanded) | ||||
| @ -19,7 +19,7 @@ class CollectionController { | ||||
| 
 | ||||
|   findAll(req, res) { | ||||
|     var collections = this.db.collections.filter(c => c.userId === req.user.id) | ||||
|     var expandedCollections = collections.map(c => c.toJSONExpanded(this.db.audiobooks)) | ||||
|     var expandedCollections = collections.map(c => c.toJSONExpanded(this.db.libraryItems)) | ||||
|     res.json(expandedCollections) | ||||
|   } | ||||
| 
 | ||||
| @ -28,7 +28,7 @@ class CollectionController { | ||||
|     if (!collection) { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
|     res.json(collection.toJSONExpanded(this.db.audiobooks)) | ||||
|     res.json(collection.toJSONExpanded(this.db.libraryItems)) | ||||
|   } | ||||
| 
 | ||||
|   async update(req, res) { | ||||
| @ -37,7 +37,7 @@ class CollectionController { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
|     var wasUpdated = collection.update(req.body) | ||||
|     var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks) | ||||
|     var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|     if (wasUpdated) { | ||||
|       await this.db.updateEntity('collection', collection) | ||||
|       this.emitter('collection_updated', jsonExpanded) | ||||
| @ -50,7 +50,7 @@ class CollectionController { | ||||
|     if (!collection) { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
|     var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks) | ||||
|     var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|     await this.db.removeEntity('collection', collection.id) | ||||
|     this.emitter('collection_removed', jsonExpanded) | ||||
|     res.sendStatus(200) | ||||
| @ -61,18 +61,18 @@ class CollectionController { | ||||
|     if (!collection) { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
|     var audiobook = this.db.audiobooks.find(ab => ab.id === req.body.id) | ||||
|     if (!audiobook) { | ||||
|     var libraryItem = this.db.libraryItems.find(li => li.id === req.body.id) | ||||
|     if (!libraryItem) { | ||||
|       return res.status(500).send('Book not found') | ||||
|     } | ||||
|     if (audiobook.libraryId !== collection.libraryId) { | ||||
|     if (libraryItem.libraryId !== collection.libraryId) { | ||||
|       return res.status(500).send('Book in different library') | ||||
|     } | ||||
|     if (collection.books.includes(req.body.id)) { | ||||
|       return res.status(500).send('Book already in collection') | ||||
|     } | ||||
|     collection.addBook(req.body.id) | ||||
|     var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks) | ||||
|     var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|     await this.db.updateEntity('collection', collection) | ||||
|     this.emitter('collection_updated', jsonExpanded) | ||||
|     res.json(jsonExpanded) | ||||
| @ -87,11 +87,11 @@ class CollectionController { | ||||
| 
 | ||||
|     if (collection.books.includes(req.params.bookId)) { | ||||
|       collection.removeBook(req.params.bookId) | ||||
|       var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks) | ||||
|       var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|       await this.db.updateEntity('collection', collection) | ||||
|       this.emitter('collection_updated', jsonExpanded) | ||||
|     } | ||||
|     res.json(collection.toJSONExpanded(this.db.audiobooks)) | ||||
|     res.json(collection.toJSONExpanded(this.db.libraryItems)) | ||||
|   } | ||||
| 
 | ||||
|   // POST: api/collections/:id/batch/add
 | ||||
| @ -113,9 +113,9 @@ class CollectionController { | ||||
|     } | ||||
|     if (hasUpdated) { | ||||
|       await this.db.updateEntity('collection', collection) | ||||
|       this.emitter('collection_updated', collection.toJSONExpanded(this.db.audiobooks)) | ||||
|       this.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems)) | ||||
|     } | ||||
|     res.json(collection.toJSONExpanded(this.db.audiobooks)) | ||||
|     res.json(collection.toJSONExpanded(this.db.libraryItems)) | ||||
|   } | ||||
| 
 | ||||
|   // POST: api/collections/:id/batch/remove
 | ||||
| @ -137,9 +137,9 @@ class CollectionController { | ||||
|     } | ||||
|     if (hasUpdated) { | ||||
|       await this.db.updateEntity('collection', collection) | ||||
|       this.emitter('collection_updated', collection.toJSONExpanded(this.db.audiobooks)) | ||||
|       this.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems)) | ||||
|     } | ||||
|     res.json(collection.toJSONExpanded(this.db.audiobooks)) | ||||
|     res.json(collection.toJSONExpanded(this.db.libraryItems)) | ||||
|   } | ||||
| } | ||||
| module.exports = new CollectionController() | ||||
| @ -93,10 +93,8 @@ class LibraryController { | ||||
|   // api/libraries/:id/items
 | ||||
|   // TODO: Optimize this method, items are iterated through several times but can be combined
 | ||||
|   getLibraryItems(req, res) { | ||||
|     var libraryId = req.library.id | ||||
|     var media = req.query.media || 'all' | ||||
|     var libraryItems = this.db.libraryItems.filter(li => { | ||||
|       if (li.libraryId !== libraryId) return false | ||||
|     var libraryItems = req.libraryItems.filter(li => { | ||||
|       if (media != 'all') return li.mediaType == media | ||||
|       return true | ||||
|     }) | ||||
| @ -151,85 +149,9 @@ class LibraryController { | ||||
|     res.json(payload) | ||||
|   } | ||||
| 
 | ||||
|   // api/libraries/:id/books/all
 | ||||
|   // TODO: Optimize this method, audiobooks are iterated through several times but can be combined
 | ||||
|   getBooksForLibrary(req, res) { | ||||
|     var libraryId = req.library.id | ||||
| 
 | ||||
|     var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId) | ||||
|     var payload = { | ||||
|       results: [], | ||||
|       total: audiobooks.length, | ||||
|       limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0, | ||||
|       page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0, | ||||
|       sortBy: req.query.sort, | ||||
|       sortDesc: req.query.desc === '1', | ||||
|       filterBy: req.query.filter, | ||||
|       minified: req.query.minified === '1', | ||||
|       collapseseries: req.query.collapseseries === '1' | ||||
|     } | ||||
| 
 | ||||
|     if (payload.filterBy) { | ||||
|       audiobooks = libraryHelpers.getFiltered(audiobooks, payload.filterBy, req.user) | ||||
|       payload.total = audiobooks.length | ||||
|     } | ||||
| 
 | ||||
|     if (payload.sortBy) { | ||||
|       var sortKey = payload.sortBy | ||||
| 
 | ||||
|       // Handle server setting sortingIgnorePrefix
 | ||||
|       if ((sortKey === 'book.series' || sortKey === 'book.title') && this.db.serverSettings.sortingIgnorePrefix) { | ||||
|         // Book.js has seriesIgnorePrefix and titleIgnorePrefix getters
 | ||||
|         sortKey += 'IgnorePrefix' | ||||
|       } | ||||
| 
 | ||||
|       var direction = payload.sortDesc ? 'desc' : 'asc' | ||||
|       audiobooks = naturalSort(audiobooks)[direction]((ab) => { | ||||
| 
 | ||||
|         // Supports dot notation strings i.e. "book.title"
 | ||||
|         return sortKey.split('.').reduce((a, b) => a[b], ab) | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     if (payload.collapseseries) { | ||||
|       var series = {} | ||||
|       // Group abs by series
 | ||||
|       for (let i = 0; i < audiobooks.length; i++) { | ||||
|         var ab = audiobooks[i] | ||||
|         if (ab.book.series) { | ||||
|           if (!series[ab.book.series]) series[ab.book.series] = [] | ||||
|           series[ab.book.series].push(ab) | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // Sort series by volume number and filter out all but the first book in series
 | ||||
|       var seriesBooksToKeep = Object.values(series).map((_series) => { | ||||
|         var sorted = naturalSort(_series).asc(_ab => _ab.book.volumeNumber) | ||||
|         return sorted[0].id | ||||
|       }) | ||||
|       // Add "booksInSeries" field to audiobook payload
 | ||||
|       audiobooks = audiobooks.filter(ab => !ab.book.series || seriesBooksToKeep.includes(ab.id)).map(ab => { | ||||
|         var abJson = payload.minified ? ab.toJSONMinified() : ab.toJSONExpanded() | ||||
|         if (ab.book.series) abJson.booksInSeries = series[ab.book.series].length | ||||
|         return abJson | ||||
|       }) | ||||
|       payload.total = audiobooks.length | ||||
|     } else { | ||||
|       audiobooks = audiobooks.map(ab => payload.minified ? ab.toJSONMinified() : ab.toJSONExpanded()) | ||||
|     } | ||||
| 
 | ||||
|     if (payload.limit) { | ||||
|       var startIndex = payload.page * payload.limit | ||||
|       audiobooks = audiobooks.slice(startIndex, startIndex + payload.limit) | ||||
|     } | ||||
|     payload.results = audiobooks | ||||
|     res.json(payload) | ||||
|   } | ||||
| 
 | ||||
|   // api/libraries/:id/series
 | ||||
|   async getAllSeriesForLibrary(req, res) { | ||||
|     var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id) | ||||
| 
 | ||||
|     var libraryItems = req.libraryItems | ||||
|     var payload = { | ||||
|       results: [], | ||||
|       total: 0, | ||||
| @ -241,7 +163,7 @@ class LibraryController { | ||||
|       minified: req.query.minified === '1' | ||||
|     } | ||||
| 
 | ||||
|     var series = libraryHelpers.getSeriesFromBooks(audiobooks, payload.minified) | ||||
|     var series = libraryHelpers.getSeriesFromBooks(libraryItems, payload.minified) | ||||
| 
 | ||||
|     var sortingIgnorePrefix = this.db.serverSettings.sortingIgnorePrefix | ||||
|     series = sort(series).asc(s => { | ||||
| @ -263,24 +185,23 @@ class LibraryController { | ||||
| 
 | ||||
|   // GET: api/libraries/:id/series/:series
 | ||||
|   async getSeriesForLibrary(req, res) { | ||||
|     var series = libraryHelpers.decode(req.params.series) | ||||
|     if (!series) { | ||||
|     if (!req.params.series) { | ||||
|       return res.status(403).send('Invalid series') | ||||
|     } | ||||
|     var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id && ab.book.series === series) | ||||
|     if (!audiobooks.length) { | ||||
|     var libraryItems = this.db.libraryItems.filter(li => li.libraryId === req.library.id && li.book.series === req.params.series) | ||||
|     if (!libraryItems.length) { | ||||
|       return res.status(404).send('Series not found') | ||||
|     } | ||||
|     var sortedBooks = libraryHelpers.sortSeriesBooks(audiobooks, false) | ||||
|     var sortedBooks = libraryHelpers.sortSeriesBooks(libraryItems, false) | ||||
|     res.json({ | ||||
|       results: sortedBooks, | ||||
|       total: audiobooks.length | ||||
|       total: libraryItems.length | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   // api/libraries/:id/series
 | ||||
|   // api/libraries/:id/collections
 | ||||
|   async getCollectionsForLibrary(req, res) { | ||||
|     var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id) | ||||
|     var libraryItems = req.libraryItems | ||||
| 
 | ||||
|     var payload = { | ||||
|       results: [], | ||||
| @ -293,7 +214,7 @@ class LibraryController { | ||||
|       minified: req.query.minified === '1' | ||||
|     } | ||||
| 
 | ||||
|     var collections = this.db.collections.filter(c => c.libraryId === req.library.id).map(c => c.toJSONExpanded(audiobooks, payload.minified)) | ||||
|     var collections = this.db.collections.filter(c => c.libraryId === req.library.id).map(c => c.toJSONExpanded(libraryItems, payload.minified)) | ||||
|     payload.total = collections.length | ||||
| 
 | ||||
|     if (payload.limit) { | ||||
| @ -521,19 +442,19 @@ class LibraryController { | ||||
|   } | ||||
| 
 | ||||
|   async getAuthors(req, res) { | ||||
|     var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id) | ||||
|     var libraryItems = req.libraryItems | ||||
|     var authors = {} | ||||
|     audiobooksInLibrary.forEach((ab) => { | ||||
|       if (ab.book._authorsList.length) { | ||||
|         ab.book._authorsList.forEach((author) => { | ||||
|           if (!author) return | ||||
|           if (!authors[author]) { | ||||
|             authors[author] = { | ||||
|               name: author, | ||||
|               numBooks: 1 | ||||
|     libraryItems.forEach((li) => { | ||||
|       if (li.media.metadata.authors && li.media.metadata.authors.length) { | ||||
|         li.media.metadata.authors.forEach((au) => { | ||||
|           if (!authors[au.id]) { | ||||
|             var _author = this.db.authors.find(_au => _au.id === au.id) | ||||
|             if (_author) { | ||||
|               authors[au.id] = _author.toJSON() | ||||
|               authors[au.id].numBooks = 1 | ||||
|             } | ||||
|           } else { | ||||
|             authors[author].numBooks++ | ||||
|             authors[au.id].numBooks++ | ||||
|           } | ||||
|         }) | ||||
|       } | ||||
|  | ||||
| @ -3,6 +3,10 @@ const Logger = require('../Logger') | ||||
| class SeriesController { | ||||
|   constructor() { } | ||||
| 
 | ||||
|   async findOne(req, res) { | ||||
|     return res.json(req.series) | ||||
|   } | ||||
| 
 | ||||
|   async search(req, res) { | ||||
|     var q = (req.query.q || '').toLowerCase() | ||||
|     if (!q) return res.json([]) | ||||
| @ -11,5 +15,21 @@ class SeriesController { | ||||
|     series = series.slice(0, limit) | ||||
|     res.json(series) | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     var series = this.db.series.find(se => se.id === req.params.id) | ||||
|     if (!series) return res.sendStatus(404) | ||||
| 
 | ||||
|     if (req.method == 'DELETE' && !req.user.canDelete) { | ||||
|       Logger.warn(`[SeriesController] User attempted to delete without permission`, req.user) | ||||
|       return res.sendStatus(403) | ||||
|     } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { | ||||
|       Logger.warn('[SeriesController] User attempted to update without permission', req.user) | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     req.series = series | ||||
|     next() | ||||
|   } | ||||
| } | ||||
| module.exports = new SeriesController() | ||||
| @ -37,10 +37,10 @@ class UserCollection { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   toJSONExpanded(audiobooks, minifiedBooks = false) { | ||||
|   toJSONExpanded(libraryItems, minifiedBooks = false) { | ||||
|     var json = this.toJSON() | ||||
|     json.books = json.books.map(bookId => { | ||||
|       var _ab = audiobooks.find(ab => ab.id === bookId) | ||||
|       var _ab = libraryItems.find(li => li.id === bookId) | ||||
|       return _ab ? minifiedBooks ? _ab.toJSONMinified() : _ab.toJSONExpanded() : null | ||||
|     }).filter(b => !!b) | ||||
|     return json | ||||
|  | ||||
| @ -99,11 +99,11 @@ class BookMetadata { | ||||
|     return this.title + '&' + this.authorName | ||||
|   } | ||||
| 
 | ||||
|   hasAuthor(authorName) { | ||||
|     return !!this.authors.find(au => au.name == authorName) | ||||
|   hasAuthor(id) { | ||||
|     return !!this.authors.find(au => au.id == id) | ||||
|   } | ||||
|   hasSeries(seriesName) { | ||||
|     return !!this.series.find(se => se.name == seriesName) | ||||
|   hasSeries(seriesId) { | ||||
|     return !!this.series.find(se => se.id == seriesId) | ||||
|   } | ||||
|   hasNarrator(narratorName) { | ||||
|     return this.narrators.includes(narratorName) | ||||
|  | ||||
| @ -20,7 +20,9 @@ module.exports = { | ||||
|       else if (group === 'tags') filtered = filtered.filter(li => li.media.tags.includes(filter)) | ||||
|       else if (group === 'series') { | ||||
|         if (filter === 'No Series') filtered = filtered.filter(li => li.media.metadata && !li.media.metadata.series.length) | ||||
|         else filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasSeries(filter)) | ||||
|         else { | ||||
|           filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasSeries(filter)) | ||||
|         } | ||||
|       } | ||||
|       else if (group === 'authors') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasAuthor(filter)) | ||||
|       else if (group === 'narrators') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasNarrator(filter)) | ||||
| @ -177,23 +179,26 @@ module.exports = { | ||||
| 
 | ||||
|   getSeriesFromBooks(books, minified = false) { | ||||
|     var _series = {} | ||||
|     books.forEach((audiobook) => { | ||||
|       if (audiobook.book.series) { | ||||
|         var abJson = minified ? audiobook.toJSONMinified() : audiobook.toJSONExpanded() | ||||
|         if (!_series[audiobook.book.series]) { | ||||
|           _series[audiobook.book.series] = { | ||||
|             id: audiobook.book.series, | ||||
|             name: audiobook.book.series, | ||||
|             type: 'series', | ||||
|             books: [abJson] | ||||
|     books.forEach((libraryItem) => { | ||||
|       if (libraryItem.media.metadata.series && libraryItem.media.metadata.series.length) { | ||||
|         libraryItem.media.metadata.series.forEach((series) => { | ||||
|           var abJson = minified ? libraryItem.toJSONMinified() : libraryItem.toJSONExpanded() | ||||
|           abJson.sequence = series.sequence | ||||
|           if (!_series[series.id]) { | ||||
|             _series[series.id] = { | ||||
|               id: series.id, | ||||
|               name: series.name, | ||||
|               type: 'series', | ||||
|               books: [abJson] | ||||
|             } | ||||
|           } else { | ||||
|             _series[series.id].books.push(abJson) | ||||
|           } | ||||
|         } else { | ||||
|           _series[audiobook.book.series].books.push(abJson) | ||||
|         } | ||||
|         }) | ||||
|       } | ||||
|     }) | ||||
|     return Object.values(_series).map((series) => { | ||||
|       series.books = naturalSort(series.books).asc(ab => ab.book.volumeNumber) | ||||
|       series.books = naturalSort(series.books).asc(li => li.sequence) | ||||
|       return series | ||||
|     }) | ||||
|   }, | ||||
| @ -221,10 +226,15 @@ module.exports = { | ||||
|     }).filter((series) => series.books.some((book) => book.userAudiobook && book.userAudiobook.isRead)) | ||||
|   }, | ||||
| 
 | ||||
|   sortSeriesBooks(books, minified = false) { | ||||
|     return naturalSort(books).asc(ab => ab.book.volumeNumber).map(ab => { | ||||
|       if (minified) return ab.toJSONMinified() | ||||
|       return ab.toJSONExpanded() | ||||
|   sortSeriesBooks(books, seriesId, minified = false) { | ||||
|     return naturalSort(books).asc(li => { | ||||
|       if (!li.media.metadata.series) return null | ||||
|       var series = li.media.metadata.series.find(se => se.id === seriesId) | ||||
|       if (!series) return null | ||||
|       return series.sequence | ||||
|     }).map(li => { | ||||
|       if (minified) return li.toJSONMinified() | ||||
|       return li.toJSONExpanded() | ||||
|     }) | ||||
|   }, | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user