mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Podcast library item card, edit details, batch edit
This commit is contained in:
		
							parent
							
								
									5446aea910
								
							
						
					
					
						commit
						e32d05ea27
					
				@ -47,10 +47,10 @@
 | 
				
			|||||||
      <div v-show="numLibraryItemsSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
 | 
					      <div v-show="numLibraryItemsSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
 | 
				
			||||||
        <h1 class="text-2xl px-4">{{ numLibraryItemsSelected }} Selected</h1>
 | 
					        <h1 class="text-2xl px-4">{{ numLibraryItemsSelected }} Selected</h1>
 | 
				
			||||||
        <div class="flex-grow" />
 | 
					        <div class="flex-grow" />
 | 
				
			||||||
        <ui-tooltip :text="`Mark as ${selectedIsFinished ? 'Not Finished' : 'Finished'}`" direction="bottom">
 | 
					        <ui-tooltip v-if="!isPodcastLibrary" :text="`Mark as ${selectedIsFinished ? 'Not Finished' : 'Finished'}`" direction="bottom">
 | 
				
			||||||
          <ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
 | 
					          <ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
 | 
				
			||||||
        </ui-tooltip>
 | 
					        </ui-tooltip>
 | 
				
			||||||
        <ui-tooltip v-if="userCanUpdate" text="Add to Collection" direction="bottom">
 | 
					        <ui-tooltip v-if="userCanUpdate && !isPodcastLibrary" text="Add to Collection" direction="bottom">
 | 
				
			||||||
          <ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
 | 
					          <ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
 | 
				
			||||||
        </ui-tooltip>
 | 
					        </ui-tooltip>
 | 
				
			||||||
        <template v-if="userCanUpdate && numLibraryItemsSelected < 50">
 | 
					        <template v-if="userCanUpdate && numLibraryItemsSelected < 50">
 | 
				
			||||||
@ -79,6 +79,12 @@ export default {
 | 
				
			|||||||
    libraryName() {
 | 
					    libraryName() {
 | 
				
			||||||
      return this.currentLibrary ? this.currentLibrary.name : 'unknown'
 | 
					      return this.currentLibrary ? this.currentLibrary.name : 'unknown'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    libraryMediaType() {
 | 
				
			||||||
 | 
					      return this.currentLibrary ? this.currentLibrary.mediaType : null
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isPodcastLibrary() {
 | 
				
			||||||
 | 
					      return this.libraryMediaType === 'podcast'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    isHome() {
 | 
					    isHome() {
 | 
				
			||||||
      return this.$route.name === 'library-library'
 | 
					      return this.$route.name === 'library-library'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@
 | 
				
			|||||||
      <p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
 | 
					      <p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
 | 
				
			||||||
        {{ displayTitle }}
 | 
					        {{ displayTitle }}
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
      <p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
 | 
					      <p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor || ' ' }}</p>
 | 
				
			||||||
      <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
 | 
					      <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -146,6 +146,12 @@ export default {
 | 
				
			|||||||
    mediaMetadata() {
 | 
					    mediaMetadata() {
 | 
				
			||||||
      return this.media.metadata || {}
 | 
					      return this.media.metadata || {}
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    mediaType() {
 | 
				
			||||||
 | 
					      return this._libraryItem.mediaType
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isPodcast() {
 | 
				
			||||||
 | 
					      return this.mediaType === 'podcast'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    placeholderUrl() {
 | 
					    placeholderUrl() {
 | 
				
			||||||
      return '/book_placeholder.jpg'
 | 
					      return '/book_placeholder.jpg'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -195,6 +201,7 @@ export default {
 | 
				
			|||||||
      return this.mediaMetadata.authors || []
 | 
					      return this.mediaMetadata.authors || []
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    author() {
 | 
					    author() {
 | 
				
			||||||
 | 
					      if (this.isPodcast) return this.mediaMetadata.author
 | 
				
			||||||
      return this.authors.map((au) => au.name).join(', ')
 | 
					      return this.authors.map((au) => au.name).join(', ')
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    authorLF() {
 | 
					    authorLF() {
 | 
				
			||||||
@ -216,6 +223,7 @@ export default {
 | 
				
			|||||||
      return this.title
 | 
					      return this.title
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    displayAuthor() {
 | 
					    displayAuthor() {
 | 
				
			||||||
 | 
					      if (this.isPodcast) return this.author
 | 
				
			||||||
      if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
 | 
					      if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
 | 
				
			||||||
      return this.author
 | 
					      return this.author
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -301,7 +309,9 @@ export default {
 | 
				
			|||||||
      return this.store.getters['user/getIsRoot']
 | 
					      return this.store.getters['user/getIsRoot']
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    moreMenuItems() {
 | 
					    moreMenuItems() {
 | 
				
			||||||
      var items = [
 | 
					      var items = []
 | 
				
			||||||
 | 
					      if (!this.isPodcast) {
 | 
				
			||||||
 | 
					        items = [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            func: 'toggleFinished',
 | 
					            func: 'toggleFinished',
 | 
				
			||||||
            text: `Mark as ${this.itemIsFinished ? 'Not Finished' : 'Finished'}`
 | 
					            text: `Mark as ${this.itemIsFinished ? 'Not Finished' : 'Finished'}`
 | 
				
			||||||
@ -311,19 +321,18 @@ export default {
 | 
				
			|||||||
            text: 'Add to Collection'
 | 
					            text: 'Add to Collection'
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      if (this.userCanUpdate) {
 | 
					      if (this.userCanUpdate) {
 | 
				
			||||||
        if (this.numTracks) {
 | 
					 | 
				
			||||||
        items.push({
 | 
					        items.push({
 | 
				
			||||||
          func: 'showEditModalTracks',
 | 
					          func: 'showEditModalTracks',
 | 
				
			||||||
            text: 'Tracks'
 | 
					          text: 'Files'
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        items.push({
 | 
					        items.push({
 | 
				
			||||||
          func: 'showEditModalMatch',
 | 
					          func: 'showEditModalMatch',
 | 
				
			||||||
          text: 'Match'
 | 
					          text: 'Match'
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (this.userCanDownload) {
 | 
					      if (this.userCanDownload && !this.isPodcast) {
 | 
				
			||||||
        items.push({
 | 
					        items.push({
 | 
				
			||||||
          func: 'showEditModalDownload',
 | 
					          func: 'showEditModalDownload',
 | 
				
			||||||
          text: 'Download'
 | 
					          text: 'Download'
 | 
				
			||||||
 | 
				
			|||||||
@ -47,6 +47,11 @@ export default {
 | 
				
			|||||||
          title: 'Chapters',
 | 
					          title: 'Chapters',
 | 
				
			||||||
          component: 'modals-item-tabs-chapters'
 | 
					          component: 'modals-item-tabs-chapters'
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          id: 'episodes',
 | 
				
			||||||
 | 
					          title: 'Episodes',
 | 
				
			||||||
 | 
					          component: 'modals-item-tabs-episodes'
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          id: 'files',
 | 
					          id: 'files',
 | 
				
			||||||
          title: 'Files',
 | 
					          title: 'Files',
 | 
				
			||||||
@ -118,8 +123,10 @@ export default {
 | 
				
			|||||||
      if (!this.userCanUpdate && !this.userCanDownload) return []
 | 
					      if (!this.userCanUpdate && !this.userCanDownload) return []
 | 
				
			||||||
      return this.tabs.filter((tab) => {
 | 
					      return this.tabs.filter((tab) => {
 | 
				
			||||||
        if (tab.id === 'download' && this.isMissing) return false
 | 
					        if (tab.id === 'download' && this.isMissing) return false
 | 
				
			||||||
        if ((tab.id === 'download' || tab.id === 'files' || tab.id === 'authors') && this.userCanDownload) return true
 | 
					        if (tab.id === 'chapters' && this.mediaType !== 'book') return false
 | 
				
			||||||
        if (tab.id !== 'download' && tab.id !== 'files' && tab.id !== 'authors' && this.userCanUpdate) return true
 | 
					        if (tab.id === 'episodes' && this.mediaType !== 'podcast') return false
 | 
				
			||||||
 | 
					        if ((tab.id === 'download' || tab.id === 'files') && this.userCanDownload) return true
 | 
				
			||||||
 | 
					        if (tab.id !== 'download' && tab.id !== 'files' && this.userCanUpdate) return true
 | 
				
			||||||
        if (tab.id === 'match' && this.userCanUpdate && this.showExperimentalFeatures) return true
 | 
					        if (tab.id === 'match' && this.userCanUpdate && this.showExperimentalFeatures) return true
 | 
				
			||||||
        return false
 | 
					        return false
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
@ -147,6 +154,9 @@ export default {
 | 
				
			|||||||
    mediaMetadata() {
 | 
					    mediaMetadata() {
 | 
				
			||||||
      return this.media.metadata || {}
 | 
					      return this.media.metadata || {}
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    mediaType() {
 | 
				
			||||||
 | 
					      return this.libraryItem ? this.libraryItem.mediaType : null
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    title() {
 | 
					    title() {
 | 
				
			||||||
      return this.mediaMetadata.title || 'No Title'
 | 
					      return this.mediaMetadata.title || 'No Title'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -1,201 +0,0 @@
 | 
				
			|||||||
<template>
 | 
					 | 
				
			||||||
  <div class="w-full h-full overflow-hidden px-4 py-6 relative">
 | 
					 | 
				
			||||||
    <template v-for="(authorName, index) in searchAuthors">
 | 
					 | 
				
			||||||
      <cards-search-author-card :key="index" :author-name="authorName" @match="setSelectedMatch" />
 | 
					 | 
				
			||||||
    </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div v-show="processing" class="flex h-full items-center justify-center">
 | 
					 | 
				
			||||||
      <p>Loading...</p>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <div v-if="selectedMatch" class="absolute top-0 left-0 w-full bg-bg h-full p-8 max-h-full overflow-y-auto overflow-x-hidden">
 | 
					 | 
				
			||||||
      <div class="flex mb-2">
 | 
					 | 
				
			||||||
        <div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="selectedMatch = null">
 | 
					 | 
				
			||||||
          <span class="material-icons text-3xl">arrow_back</span>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <p class="text-xl pl-3">Update Author Details</p>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <form @submit.prevent="submitMatchUpdate">
 | 
					 | 
				
			||||||
        <div v-if="selectedMatch.image" class="flex items-center py-2">
 | 
					 | 
				
			||||||
          <ui-checkbox v-model="selectedMatchUsage.image" />
 | 
					 | 
				
			||||||
          <img :src="selectedMatch.image" class="w-24 object-contain ml-4" />
 | 
					 | 
				
			||||||
          <ui-text-input-with-label v-model="selectedMatch.image" :disabled="!selectedMatchUsage.image" label="Image" class="flex-grow ml-4" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div v-if="selectedMatch.name" class="flex items-center py-2">
 | 
					 | 
				
			||||||
          <ui-checkbox v-model="selectedMatchUsage.name" />
 | 
					 | 
				
			||||||
          <ui-text-input-with-label v-model="selectedMatch.name" :disabled="!selectedMatchUsage.name" label="Name" class="flex-grow ml-4" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div v-if="selectedMatch.description" class="flex items-center py-2">
 | 
					 | 
				
			||||||
          <ui-checkbox v-model="selectedMatchUsage.description" />
 | 
					 | 
				
			||||||
          <ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" label="Description" class="flex-grow ml-4" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div class="flex items-center justify-end py-2">
 | 
					 | 
				
			||||||
          <ui-btn color="success" type="submit">Update</ui-btn>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </form>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    processing: Boolean,
 | 
					 | 
				
			||||||
    audiobook: {
 | 
					 | 
				
			||||||
      type: Object,
 | 
					 | 
				
			||||||
      default: () => {}
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      searchAuthors: [],
 | 
					 | 
				
			||||||
      audiobookId: null,
 | 
					 | 
				
			||||||
      searchAuthor: null,
 | 
					 | 
				
			||||||
      lastSearch: null,
 | 
					 | 
				
			||||||
      hasSearched: false,
 | 
					 | 
				
			||||||
      selectedMatch: null,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      selectedMatchUsage: {
 | 
					 | 
				
			||||||
        image: true,
 | 
					 | 
				
			||||||
        name: true,
 | 
					 | 
				
			||||||
        description: true
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  watch: {
 | 
					 | 
				
			||||||
    audiobook: {
 | 
					 | 
				
			||||||
      immediate: true,
 | 
					 | 
				
			||||||
      handler(newVal) {
 | 
					 | 
				
			||||||
        if (newVal) this.init()
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  computed: {
 | 
					 | 
				
			||||||
    isProcessing: {
 | 
					 | 
				
			||||||
      get() {
 | 
					 | 
				
			||||||
        return this.processing
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      set(val) {
 | 
					 | 
				
			||||||
        this.$emit('update:processing', val)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					 | 
				
			||||||
    // getSearchQuery() {
 | 
					 | 
				
			||||||
    //   return `q=${this.searchAuthor}`
 | 
					 | 
				
			||||||
    // },
 | 
					 | 
				
			||||||
    // submitSearch() {
 | 
					 | 
				
			||||||
    //   if (!this.searchTitle) {
 | 
					 | 
				
			||||||
    //     this.$toast.warning('Search title is required')
 | 
					 | 
				
			||||||
    //     return
 | 
					 | 
				
			||||||
    //   }
 | 
					 | 
				
			||||||
    //   this.runSearch()
 | 
					 | 
				
			||||||
    // },
 | 
					 | 
				
			||||||
    // async runSearch() {
 | 
					 | 
				
			||||||
    //   var searchQuery = this.getSearchQuery()
 | 
					 | 
				
			||||||
    //   if (this.lastSearch === searchQuery) return
 | 
					 | 
				
			||||||
    //   this.selectedMatch = null
 | 
					 | 
				
			||||||
    //   this.isProcessing = true
 | 
					 | 
				
			||||||
    //   this.lastSearch = searchQuery
 | 
					 | 
				
			||||||
    //   var result = await this.$axios.$get(`/api/authors/search?${searchQuery}`).catch((error) => {
 | 
					 | 
				
			||||||
    //     console.error('Failed', error)
 | 
					 | 
				
			||||||
    //     return []
 | 
					 | 
				
			||||||
    //   })
 | 
					 | 
				
			||||||
    //   if (result) {
 | 
					 | 
				
			||||||
    //     this.selectedMatch = result
 | 
					 | 
				
			||||||
    //   }
 | 
					 | 
				
			||||||
    //   this.isProcessing = false
 | 
					 | 
				
			||||||
    //   this.hasSearched = true
 | 
					 | 
				
			||||||
    // },
 | 
					 | 
				
			||||||
    init() {
 | 
					 | 
				
			||||||
      this.selectedMatch = null
 | 
					 | 
				
			||||||
      // this.selectedMatchUsage = {
 | 
					 | 
				
			||||||
      //   title: true,
 | 
					 | 
				
			||||||
      //   subtitle: true,
 | 
					 | 
				
			||||||
      //   cover: true,
 | 
					 | 
				
			||||||
      //   author: true,
 | 
					 | 
				
			||||||
      //   description: true,
 | 
					 | 
				
			||||||
      //   isbn: true,
 | 
					 | 
				
			||||||
      //   publisher: true,
 | 
					 | 
				
			||||||
      //   publishYear: true
 | 
					 | 
				
			||||||
      // }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.audiobook.id !== this.audiobookId) {
 | 
					 | 
				
			||||||
        this.selectedMatch = null
 | 
					 | 
				
			||||||
        this.hasSearched = false
 | 
					 | 
				
			||||||
        this.audiobookId = this.audiobook.id
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (!this.audiobook.book || !this.audiobook.book.authorFL) {
 | 
					 | 
				
			||||||
        this.searchAuthors = []
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.searchAuthors = (this.audiobook.book.authorFL || '').split(', ')
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    selectMatch(match) {
 | 
					 | 
				
			||||||
      this.selectedMatch = match
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    buildMatchUpdatePayload() {
 | 
					 | 
				
			||||||
      var updatePayload = {}
 | 
					 | 
				
			||||||
      for (const key in this.selectedMatchUsage) {
 | 
					 | 
				
			||||||
        if (this.selectedMatchUsage[key] && this.selectedMatch[key]) {
 | 
					 | 
				
			||||||
          updatePayload[key] = this.selectedMatch[key]
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return updatePayload
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    async submitMatchUpdate() {
 | 
					 | 
				
			||||||
      var updatePayload = this.buildMatchUpdatePayload()
 | 
					 | 
				
			||||||
      if (!Object.keys(updatePayload).length) {
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.isProcessing = true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (updatePayload.cover) {
 | 
					 | 
				
			||||||
        var coverPayload = {
 | 
					 | 
				
			||||||
          url: updatePayload.cover
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        var success = await this.$axios.$post(`/api/items/${this.audiobook.id}/cover`, coverPayload).catch((error) => {
 | 
					 | 
				
			||||||
          console.error('Failed to update', error)
 | 
					 | 
				
			||||||
          return false
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        if (success) {
 | 
					 | 
				
			||||||
          this.$toast.success('Book Cover Updated')
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          this.$toast.error('Book Cover Failed to Update')
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        console.log('Updated cover')
 | 
					 | 
				
			||||||
        delete updatePayload.cover
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (Object.keys(updatePayload).length) {
 | 
					 | 
				
			||||||
        var bookUpdatePayload = {
 | 
					 | 
				
			||||||
          book: updatePayload
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        var success = await this.$axios.$patch(`/api/items/${this.audiobook.id}`, bookUpdatePayload).catch((error) => {
 | 
					 | 
				
			||||||
          console.error('Failed to update', error)
 | 
					 | 
				
			||||||
          return false
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        if (success) {
 | 
					 | 
				
			||||||
          this.$toast.success('Book Details Updated')
 | 
					 | 
				
			||||||
          this.selectedMatch = null
 | 
					 | 
				
			||||||
          this.$emit('selectTab', 'details')
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          this.$toast.error('Book Details Failed to Update')
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        this.selectedMatch = null
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.isProcessing = false
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    setSelectedMatch(authorMatchObj) {
 | 
					 | 
				
			||||||
      this.selectedMatch = authorMatchObj
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
.matchListWrapper {
 | 
					 | 
				
			||||||
  height: calc(100% - 80px);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="w-full h-full relative">
 | 
					  <div class="w-full h-full relative">
 | 
				
			||||||
    <widgets-item-details-edit ref="itemDetailsEdit" :library-item="libraryItem" @submit="submitForm" />
 | 
					    <widgets-book-details-edit v-if="mediaType == 'book'" ref="itemDetailsEdit" :library-item="libraryItem" @submit="submitForm" />
 | 
				
			||||||
 | 
					    <widgets-podcast-details-edit v-else ref="itemDetailsEdit" :library-item="libraryItem" @submit="submitForm" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'box-shadow-sm-up border-t border-primary border-opacity-50'">
 | 
					    <div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'box-shadow-sm-up border-t border-primary border-opacity-50'">
 | 
				
			||||||
      <div class="flex items-center px-4">
 | 
					      <div class="flex items-center px-4">
 | 
				
			||||||
@ -8,11 +9,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <div class="flex-grow" />
 | 
					        <div class="flex-grow" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ui-tooltip v-if="!isMissing" text="(Root User Only) Save a NFO metadata file in your audiobooks directory" direction="bottom" class="mr-4 hidden sm:block">
 | 
					        <ui-tooltip v-if="!isMissing && mediaType == 'book'" text="(Root User Only) Save a NFO metadata file in your audiobooks directory" direction="bottom" class="mr-4 hidden sm:block">
 | 
				
			||||||
          <ui-btn v-if="isRootUser" :loading="savingMetadata" color="bg" type="button" class="h-full" small @click.stop.prevent="saveMetadata">Save Metadata</ui-btn>
 | 
					          <ui-btn v-if="isRootUser" :loading="savingMetadata" color="bg" type="button" class="h-full" small @click.stop.prevent="saveMetadata">Save Metadata</ui-btn>
 | 
				
			||||||
        </ui-tooltip>
 | 
					        </ui-tooltip>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ui-tooltip :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-4">
 | 
					        <ui-tooltip v-if="mediaType == 'book'" :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-4">
 | 
				
			||||||
          <ui-btn v-if="isRootUser" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
 | 
					          <ui-btn v-if="isRootUser" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
 | 
				
			||||||
        </ui-tooltip>
 | 
					        </ui-tooltip>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -65,6 +66,9 @@ export default {
 | 
				
			|||||||
    media() {
 | 
					    media() {
 | 
				
			||||||
      return this.libraryItem ? this.libraryItem.media || {} : {}
 | 
					      return this.libraryItem ? this.libraryItem.media || {} : {}
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    mediaType() {
 | 
				
			||||||
 | 
					      return this.libraryItem ? this.libraryItem.mediaType : null
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    mediaMetadata() {
 | 
					    mediaMetadata() {
 | 
				
			||||||
      return this.media.metadata || {}
 | 
					      return this.media.metadata || {}
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										55
									
								
								client/components/modals/item/tabs/Episodes.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								client/components/modals/item/tabs/Episodes.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
 | 
				
			||||||
 | 
					    <div class="w-full mb-4">
 | 
				
			||||||
 | 
					      <div class="w-full p-4 bg-primary">
 | 
				
			||||||
 | 
					        <p>Podcast Episodes</p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">No Episodes</div>
 | 
				
			||||||
 | 
					      <table v-else class="text-sm tracksTable">
 | 
				
			||||||
 | 
					        <tr class="font-book">
 | 
				
			||||||
 | 
					          <th class="text-left w-16"><span class="px-4">#</span></th>
 | 
				
			||||||
 | 
					          <th class="text-left">Title</th>
 | 
				
			||||||
 | 
					          <th class="text-center w-28">Duration</th>
 | 
				
			||||||
 | 
					          <th class="text-center w-28">Size</th>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					        <tr v-for="episode in episodes" :key="episode.id">
 | 
				
			||||||
 | 
					          <td class="text-left">
 | 
				
			||||||
 | 
					            <p class="px-4">{{ episode.index }}</p>
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
 | 
					          <td class="font-book">
 | 
				
			||||||
 | 
					            {{ episode.title }}
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
 | 
					          <td class="font-mono text-center">
 | 
				
			||||||
 | 
					            {{ $secondsToTimestamp(episode.duration) }}
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
 | 
					          <td class="font-mono text-center">
 | 
				
			||||||
 | 
					            {{ $bytesPretty(episode.size) }}
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					      </table>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    libraryItem: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      default: () => {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {}
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    media() {
 | 
				
			||||||
 | 
					      return this.libraryItem ? this.libraryItem.media || {} : {}
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    episodes() {
 | 
				
			||||||
 | 
					      return this.media.episodes || []
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										224
									
								
								client/components/widgets/PodcastDetailsEdit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								client/components/widgets/PodcastDetailsEdit.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,224 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="w-full h-full relative">
 | 
				
			||||||
 | 
					    <form class="w-full h-full" @submit.prevent="submitForm">
 | 
				
			||||||
 | 
					      <div id="formWrapper" class="px-4 py-6 details-form-wrapper w-full overflow-hidden overflow-y-auto">
 | 
				
			||||||
 | 
					        <div class="flex -mx-1">
 | 
				
			||||||
 | 
					          <div class="w-1/2 px-1">
 | 
				
			||||||
 | 
					            <ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="flex-grow px-1">
 | 
				
			||||||
 | 
					            <ui-text-input-with-label ref="authorInput" v-model="details.author" label="Author" />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" label="RSS Feed URL" class="mt-2" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="flex mt-2 -mx-1">
 | 
				
			||||||
 | 
					          <div class="w-1/2 px-1">
 | 
				
			||||||
 | 
					            <ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="flex-grow px-1">
 | 
				
			||||||
 | 
					            <ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="flex mt-2 -mx-1">
 | 
				
			||||||
 | 
					          <div class="w-1/4 px-1">
 | 
				
			||||||
 | 
					            <ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" label="Release Date" />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="w-1/4 px-1">
 | 
				
			||||||
 | 
					            <ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="w-1/4 px-1">
 | 
				
			||||||
 | 
					            <ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="flex-grow px-1 pt-6">
 | 
				
			||||||
 | 
					            <div class="flex justify-center">
 | 
				
			||||||
 | 
					              <ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    libraryItem: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      default: () => {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      details: {
 | 
				
			||||||
 | 
					        title: null,
 | 
				
			||||||
 | 
					        author: null,
 | 
				
			||||||
 | 
					        description: null,
 | 
				
			||||||
 | 
					        releaseDate: null,
 | 
				
			||||||
 | 
					        genres: [],
 | 
				
			||||||
 | 
					        feedUrl: null,
 | 
				
			||||||
 | 
					        imageUrl: null,
 | 
				
			||||||
 | 
					        itunesPageUrl: null,
 | 
				
			||||||
 | 
					        itunesId: null,
 | 
				
			||||||
 | 
					        itunesArtistId: null,
 | 
				
			||||||
 | 
					        explicit: false,
 | 
				
			||||||
 | 
					        language: null
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      autoDownloadEpisodes: false,
 | 
				
			||||||
 | 
					      newTags: []
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  watch: {
 | 
				
			||||||
 | 
					    libraryItem: {
 | 
				
			||||||
 | 
					      immediate: true,
 | 
				
			||||||
 | 
					      handler(newVal) {
 | 
				
			||||||
 | 
					        if (newVal) this.init()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    media() {
 | 
				
			||||||
 | 
					      return this.libraryItem ? this.libraryItem.media || {} : {}
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    mediaMetadata() {
 | 
				
			||||||
 | 
					      return this.media.metadata || {}
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    genres() {
 | 
				
			||||||
 | 
					      return this.filterData.genres || []
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    tags() {
 | 
				
			||||||
 | 
					      return this.filterData.tags || []
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    filterData() {
 | 
				
			||||||
 | 
					      return this.$store.state.libraries.filterData || {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    getDetails() {
 | 
				
			||||||
 | 
					      this.forceBlur()
 | 
				
			||||||
 | 
					      return this.checkForChanges()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    getTitleAndAuthorName() {
 | 
				
			||||||
 | 
					      this.forceBlur()
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        title: this.details.title,
 | 
				
			||||||
 | 
					        author: this.details.author
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    mapBatchDetails(batchDetails) {
 | 
				
			||||||
 | 
					      for (const key in batchDetails) {
 | 
				
			||||||
 | 
					        if (key === 'tags') {
 | 
				
			||||||
 | 
					          this.newTags = [...batchDetails.tags]
 | 
				
			||||||
 | 
					        } else if (key === 'genres') {
 | 
				
			||||||
 | 
					          this.details[key] = [...batchDetails[key]]
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.details[key] = batchDetails[key]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    forceBlur() {
 | 
				
			||||||
 | 
					      if (this.$refs.titleInput) this.$refs.titleInput.blur()
 | 
				
			||||||
 | 
					      if (this.$refs.authorInput) this.$refs.authorInput.blur()
 | 
				
			||||||
 | 
					      if (this.$refs.releaseDateInput) this.$refs.releaseDateInput.blur()
 | 
				
			||||||
 | 
					      if (this.$refs.descriptionInput) this.$refs.descriptionInput.blur()
 | 
				
			||||||
 | 
					      if (this.$refs.feedUrlInput) this.$refs.feedUrlInput.blur()
 | 
				
			||||||
 | 
					      if (this.$refs.itunesIdInput) this.$refs.itunesIdInput.blur()
 | 
				
			||||||
 | 
					      if (this.$refs.languageInput) this.$refs.languageInput.blur()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.$refs.genresSelect && this.$refs.genresSelect.isFocused) {
 | 
				
			||||||
 | 
					        this.$refs.genresSelect.forceBlur()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this.$refs.tagsSelect && this.$refs.tagsSelect.isFocused) {
 | 
				
			||||||
 | 
					        this.$refs.tagsSelect.forceBlur()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    stringArrayEqual(array1, array2) {
 | 
				
			||||||
 | 
					      // return false if different
 | 
				
			||||||
 | 
					      if (array1.length !== array2.length) return false
 | 
				
			||||||
 | 
					      for (var item of array1) {
 | 
				
			||||||
 | 
					        if (!array2.includes(item)) return false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    objectArrayEqual(array1, array2) {
 | 
				
			||||||
 | 
					      const isIterable = (value) => {
 | 
				
			||||||
 | 
					        return Symbol.iterator in Object(value)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!isIterable(array1) || !isIterable(array2)) {
 | 
				
			||||||
 | 
					        console.error(array1, array2)
 | 
				
			||||||
 | 
					        throw new Error('Invalid arrays passed in')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // array of objects with id key
 | 
				
			||||||
 | 
					      if (array1.length !== array2.length) return false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (var item of array1) {
 | 
				
			||||||
 | 
					        var matchingItem = array2.find((a) => a.id === item.id)
 | 
				
			||||||
 | 
					        if (!matchingItem) return false
 | 
				
			||||||
 | 
					        for (var key in item) {
 | 
				
			||||||
 | 
					          if (item[key] !== matchingItem[key]) {
 | 
				
			||||||
 | 
					            // console.log('Object array item keys changed', key, item[key], matchingItem[key])
 | 
				
			||||||
 | 
					            return false
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    checkForChanges() {
 | 
				
			||||||
 | 
					      var metadata = {}
 | 
				
			||||||
 | 
					      for (const key in this.details) {
 | 
				
			||||||
 | 
					        var newValue = this.details[key]
 | 
				
			||||||
 | 
					        var oldValue = this.mediaMetadata[key]
 | 
				
			||||||
 | 
					        // Key cleared out or key first populated
 | 
				
			||||||
 | 
					        if ((!newValue && oldValue) || (newValue && !oldValue)) {
 | 
				
			||||||
 | 
					          metadata[key] = newValue
 | 
				
			||||||
 | 
					        } else if (key === 'genres') {
 | 
				
			||||||
 | 
					          // Check array of strings
 | 
				
			||||||
 | 
					          if (!this.stringArrayEqual(newValue, oldValue)) {
 | 
				
			||||||
 | 
					            metadata[key] = [...newValue]
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else if (newValue && newValue != oldValue) {
 | 
				
			||||||
 | 
					          // Intentional !=
 | 
				
			||||||
 | 
					          metadata[key] = newValue
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      var updatePayload = {}
 | 
				
			||||||
 | 
					      if (!!Object.keys(metadata).length) updatePayload.metadata = metadata
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!this.stringArrayEqual(this.newTags, this.media.tags || [])) {
 | 
				
			||||||
 | 
					        updatePayload.tags = [...this.newTags]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        updatePayload,
 | 
				
			||||||
 | 
					        hasChanges: !!Object.keys(updatePayload).length
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    init() {
 | 
				
			||||||
 | 
					      this.details.title = this.mediaMetadata.title
 | 
				
			||||||
 | 
					      this.details.author = this.mediaMetadata.author || ''
 | 
				
			||||||
 | 
					      this.details.description = this.mediaMetadata.description || ''
 | 
				
			||||||
 | 
					      this.details.releaseDate = this.mediaMetadata.releaseDate || ''
 | 
				
			||||||
 | 
					      this.details.genres = [...(this.mediaMetadata.genres || [])]
 | 
				
			||||||
 | 
					      this.details.feedUrl = this.mediaMetadata.feedUrl || ''
 | 
				
			||||||
 | 
					      this.details.imageUrl = this.mediaMetadata.imageUrl || ''
 | 
				
			||||||
 | 
					      this.details.itunesPageUrl = this.mediaMetadata.itunesPageUrl || ''
 | 
				
			||||||
 | 
					      this.details.itunesId = this.mediaMetadata.itunesId || ''
 | 
				
			||||||
 | 
					      this.details.itunesArtistId = this.mediaMetadata.itunesArtistId || ''
 | 
				
			||||||
 | 
					      this.details.language = this.mediaMetadata.language || ''
 | 
				
			||||||
 | 
					      this.details.explicit = !!this.mediaMetadata.explicit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.autoDownloadEpisodes = !!this.media.autoDownloadEpisodes
 | 
				
			||||||
 | 
					      this.newTags = [...(this.media.tags || [])]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    submitForm() {
 | 
				
			||||||
 | 
					      this.$emit('submit')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mounted() {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@ -9,20 +9,20 @@
 | 
				
			|||||||
      <div class="overflow-hidden">
 | 
					      <div class="overflow-hidden">
 | 
				
			||||||
        <transition name="slide">
 | 
					        <transition name="slide">
 | 
				
			||||||
          <div v-if="openMapOptions" class="flex flex-wrap">
 | 
					          <div v-if="openMapOptions" class="flex flex-wrap">
 | 
				
			||||||
            <div class="flex items-center px-4 w-1/2">
 | 
					            <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
 | 
				
			||||||
              <ui-checkbox v-model="selectedBatchUsage.subtitle" />
 | 
					              <ui-checkbox v-model="selectedBatchUsage.subtitle" />
 | 
				
			||||||
              <ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" label="Subtitle" class="mb-4 ml-4" />
 | 
					              <ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" label="Subtitle" class="mb-4 ml-4" />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="flex items-center px-4 w-1/2">
 | 
					            <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
 | 
				
			||||||
              <ui-checkbox v-model="selectedBatchUsage.authors" />
 | 
					              <ui-checkbox v-model="selectedBatchUsage.authors" />
 | 
				
			||||||
              <!-- Authors filter only contains authors in this library, use query input to query all authors -->
 | 
					              <!-- Authors filter only contains authors in this library, use query input to query all authors -->
 | 
				
			||||||
              <ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" label="Authors" endpoint="authors/search" class="mb-4 ml-4" />
 | 
					              <ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" label="Authors" endpoint="authors/search" class="mb-4 ml-4" />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="flex items-center px-4 w-1/2">
 | 
					            <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
 | 
				
			||||||
              <ui-checkbox v-model="selectedBatchUsage.publishedYear" />
 | 
					              <ui-checkbox v-model="selectedBatchUsage.publishedYear" />
 | 
				
			||||||
              <ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" label="Publish Year" class="mb-4 ml-4" />
 | 
					              <ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" label="Publish Year" class="mb-4 ml-4" />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="flex items-center px-4 w-1/2">
 | 
					            <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
 | 
				
			||||||
              <ui-checkbox v-model="selectedBatchUsage.series" />
 | 
					              <ui-checkbox v-model="selectedBatchUsage.series" />
 | 
				
			||||||
              <ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" label="Series" :items="seriesItems" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
 | 
					              <ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" label="Series" :items="seriesItems" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
@ -34,11 +34,11 @@
 | 
				
			|||||||
              <ui-checkbox v-model="selectedBatchUsage.tags" />
 | 
					              <ui-checkbox v-model="selectedBatchUsage.tags" />
 | 
				
			||||||
              <ui-multi-select ref="tagsSelect" v-model="batchDetails.tags" label="Tags" :disabled="!selectedBatchUsage.tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" class="mb-4 ml-4" />
 | 
					              <ui-multi-select ref="tagsSelect" v-model="batchDetails.tags" label="Tags" :disabled="!selectedBatchUsage.tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" class="mb-4 ml-4" />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="flex items-center px-4 w-1/2">
 | 
					            <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
 | 
				
			||||||
              <ui-checkbox v-model="selectedBatchUsage.narrators" />
 | 
					              <ui-checkbox v-model="selectedBatchUsage.narrators" />
 | 
				
			||||||
              <ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" label="Narrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
 | 
					              <ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" label="Narrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="flex items-center px-4 w-1/2">
 | 
					            <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
 | 
				
			||||||
              <ui-checkbox v-model="selectedBatchUsage.publisher" />
 | 
					              <ui-checkbox v-model="selectedBatchUsage.publisher" />
 | 
				
			||||||
              <ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" label="Publisher" class="mb-4 ml-4" />
 | 
					              <ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" label="Publisher" class="mb-4 ml-4" />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
@ -72,7 +72,8 @@
 | 
				
			|||||||
    <div class="flex justify-center flex-wrap">
 | 
					    <div class="flex justify-center flex-wrap">
 | 
				
			||||||
      <template v-for="libraryItem in libraryItemCopies">
 | 
					      <template v-for="libraryItem in libraryItemCopies">
 | 
				
			||||||
        <div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
 | 
					        <div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
 | 
				
			||||||
          <widgets-item-details-edit :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
 | 
					          <widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
 | 
				
			||||||
 | 
					          <widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </template>
 | 
					      </template>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@ -99,6 +100,7 @@ export default {
 | 
				
			|||||||
      return []
 | 
					      return []
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
					      mediaType: libraryItems[0].mediaType,
 | 
				
			||||||
      libraryItems
 | 
					      libraryItems
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@ -139,6 +141,9 @@ export default {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
 | 
					    isPodcastLibrary() {
 | 
				
			||||||
 | 
					      return this.mediaType === 'podcast'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    coverAspectRatio() {
 | 
					    coverAspectRatio() {
 | 
				
			||||||
      return this.$store.getters['getServerSetting']('coverAspectRatio')
 | 
					      return this.$store.getters['getServerSetting']('coverAspectRatio')
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,7 @@
 | 
				
			|||||||
                <p v-if="bookSubtitle" class="sm:ml-4 text-gray-400 text-xl md:text-2xl">{{ bookSubtitle }}</p>
 | 
					                <p v-if="bookSubtitle" class="sm:ml-4 text-gray-400 text-xl md:text-2xl">{{ bookSubtitle }}</p>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor }}</p>
 | 
					              <p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
 | 
				
			||||||
              <p v-else-if="authorsList.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">
 | 
					              <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>
 | 
				
			||||||
@ -126,15 +126,15 @@
 | 
				
			|||||||
              <ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
 | 
					              <ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
 | 
				
			||||||
            </ui-tooltip>
 | 
					            </ui-tooltip>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <ui-tooltip v-if="userCanDownload" :disabled="isMissing" text="Download" direction="top">
 | 
					            <ui-tooltip v-if="userCanDownload && !isPodcast" :disabled="isMissing" text="Download" direction="top">
 | 
				
			||||||
              <ui-icon-btn icon="download" :disabled="isMissing" class="mx-0.5" @click="downloadClick" />
 | 
					              <ui-icon-btn icon="download" :disabled="isMissing" class="mx-0.5" @click="downloadClick" />
 | 
				
			||||||
            </ui-tooltip>
 | 
					            </ui-tooltip>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
 | 
					            <ui-tooltip v-if="!isPodcast" :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
 | 
				
			||||||
              <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
 | 
					              <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
 | 
				
			||||||
            </ui-tooltip>
 | 
					            </ui-tooltip>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <ui-tooltip text="Collections" direction="top">
 | 
					            <ui-tooltip v-if="!isPodcast" text="Collections" direction="top">
 | 
				
			||||||
              <ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
 | 
					              <ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
 | 
				
			||||||
            </ui-tooltip>
 | 
					            </ui-tooltip>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -59,6 +59,26 @@ class PodcastEpisode {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toJSONExpanded() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      index: this.index,
 | 
				
			||||||
 | 
					      episode: this.episode,
 | 
				
			||||||
 | 
					      episodeType: this.episodeType,
 | 
				
			||||||
 | 
					      title: this.title,
 | 
				
			||||||
 | 
					      subtitle: this.subtitle,
 | 
				
			||||||
 | 
					      description: this.description,
 | 
				
			||||||
 | 
					      enclosure: this.enclosure ? { ...this.enclosure } : null,
 | 
				
			||||||
 | 
					      pubDate: this.pubDate,
 | 
				
			||||||
 | 
					      audioFile: this.audioFile.toJSON(),
 | 
				
			||||||
 | 
					      publishedAt: this.publishedAt,
 | 
				
			||||||
 | 
					      addedAt: this.addedAt,
 | 
				
			||||||
 | 
					      updatedAt: this.updatedAt,
 | 
				
			||||||
 | 
					      duration: this.duration,
 | 
				
			||||||
 | 
					      size: this.size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get tracks() {
 | 
					  get tracks() {
 | 
				
			||||||
    return [this.audioFile]
 | 
					    return [this.audioFile]
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -47,7 +47,8 @@ class Podcast {
 | 
				
			|||||||
      coverPath: this.coverPath,
 | 
					      coverPath: this.coverPath,
 | 
				
			||||||
      tags: [...this.tags],
 | 
					      tags: [...this.tags],
 | 
				
			||||||
      episodes: this.episodes.map(e => e.toJSON()),
 | 
					      episodes: this.episodes.map(e => e.toJSON()),
 | 
				
			||||||
      autoDownloadEpisodes: this.autoDownloadEpisodes
 | 
					      autoDownloadEpisodes: this.autoDownloadEpisodes,
 | 
				
			||||||
 | 
					      size: this.size
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -56,8 +57,9 @@ class Podcast {
 | 
				
			|||||||
      metadata: this.metadata.toJSONExpanded(),
 | 
					      metadata: this.metadata.toJSONExpanded(),
 | 
				
			||||||
      coverPath: this.coverPath,
 | 
					      coverPath: this.coverPath,
 | 
				
			||||||
      tags: [...this.tags],
 | 
					      tags: [...this.tags],
 | 
				
			||||||
      episodes: this.episodes.map(e => e.toJSON()),
 | 
					      episodes: this.episodes.map(e => e.toJSONExpanded()),
 | 
				
			||||||
      autoDownloadEpisodes: this.autoDownloadEpisodes
 | 
					      autoDownloadEpisodes: this.autoDownloadEpisodes,
 | 
				
			||||||
 | 
					      size: this.size
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,6 @@
 | 
				
			|||||||
 | 
					const Logger = require('../../Logger')
 | 
				
			||||||
 | 
					const { areEquivalent, copyValue } = require('../../utils/index')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PodcastMetadata {
 | 
					class PodcastMetadata {
 | 
				
			||||||
  constructor(metadata) {
 | 
					  constructor(metadata) {
 | 
				
			||||||
    this.title = null
 | 
					    this.title = null
 | 
				
			||||||
@ -87,5 +90,20 @@ class PodcastMetadata {
 | 
				
			|||||||
      this.genres = [...mediaMetadata.genres]
 | 
					      this.genres = [...mediaMetadata.genres]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  update(payload) {
 | 
				
			||||||
 | 
					    var json = this.toJSON()
 | 
				
			||||||
 | 
					    var hasUpdates = false
 | 
				
			||||||
 | 
					    for (const key in json) {
 | 
				
			||||||
 | 
					      if (payload[key] !== undefined) {
 | 
				
			||||||
 | 
					        if (!areEquivalent(payload[key], json[key])) {
 | 
				
			||||||
 | 
					          this[key] = copyValue(payload[key])
 | 
				
			||||||
 | 
					          Logger.debug('[PodcastMetadata] Key updated', key, this[key])
 | 
				
			||||||
 | 
					          hasUpdates = true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return hasUpdates
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
module.exports = PodcastMetadata
 | 
					module.exports = PodcastMetadata
 | 
				
			||||||
@ -120,12 +120,11 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
 | 
				
			|||||||
  return libraryItemGroup
 | 
					  return libraryItemGroup
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function cleanFileObjects(libraryItemPath, folderPath, files) {
 | 
					function cleanFileObjects(libraryItemPath, files) {
 | 
				
			||||||
  return Promise.all(files.map(async (file) => {
 | 
					  return Promise.all(files.map(async (file) => {
 | 
				
			||||||
    var filePath = Path.posix.join(libraryItemPath, file)
 | 
					    var filePath = Path.posix.join(libraryItemPath, file)
 | 
				
			||||||
    var relFilePath = filePath.replace(folderPath, '')
 | 
					 | 
				
			||||||
    var newLibraryFile = new LibraryFile()
 | 
					    var newLibraryFile = new LibraryFile()
 | 
				
			||||||
    await newLibraryFile.setDataFromPath(filePath, relFilePath)
 | 
					    await newLibraryFile.setDataFromPath(filePath, file)
 | 
				
			||||||
    return newLibraryFile
 | 
					    return newLibraryFile
 | 
				
			||||||
  }))
 | 
					  }))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -153,7 +152,7 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
 | 
				
			|||||||
  for (const libraryItemPath in libraryItemGrouping) {
 | 
					  for (const libraryItemPath in libraryItemGrouping) {
 | 
				
			||||||
    var libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings)
 | 
					    var libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var fileObjs = await cleanFileObjects(libraryItemData.path, folderPath, libraryItemGrouping[libraryItemPath])
 | 
					    var fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath])
 | 
				
			||||||
    var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
 | 
					    var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
 | 
				
			||||||
    items.push({
 | 
					    items.push({
 | 
				
			||||||
      folderId: folder.id,
 | 
					      folderId: folder.id,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user