mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-04 03:17:00 -05:00 
			
		
		
		
	Merge branch 'master' into socket-fixes
This commit is contained in:
		
						commit
						33e183b802
					
				@ -16,17 +16,17 @@
 | 
				
			|||||||
      <nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
 | 
					      <nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
 | 
				
			||||||
        <p class="text-sm">{{ $strings.ButtonLatest }}</p>
 | 
					        <p class="text-sm">{{ $strings.ButtonLatest }}</p>
 | 
				
			||||||
      </nuxt-link>
 | 
					      </nuxt-link>
 | 
				
			||||||
      <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
 | 
					      <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
 | 
				
			||||||
        <p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
 | 
					        <p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
 | 
				
			||||||
        <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
 | 
					        <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
 | 
				
			||||||
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
 | 
					          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
 | 
				
			||||||
        </svg>
 | 
					        </svg>
 | 
				
			||||||
      </nuxt-link>
 | 
					      </nuxt-link>
 | 
				
			||||||
      <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
 | 
					      <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
 | 
				
			||||||
        <p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
 | 
					        <p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
 | 
				
			||||||
        <span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
 | 
					        <span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
 | 
				
			||||||
      </nuxt-link>
 | 
					      </nuxt-link>
 | 
				
			||||||
      <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
 | 
					      <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
 | 
				
			||||||
        <p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
 | 
					        <p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
 | 
				
			||||||
        <svg v-else class="w-5 h-5" viewBox="0 0 24 24">
 | 
					        <svg v-else class="w-5 h-5" viewBox="0 0 24 24">
 | 
				
			||||||
          <path
 | 
					          <path
 | 
				
			||||||
@ -69,7 +69,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <div class="flex-grow hidden sm:inline-block" />
 | 
					        <div class="flex-grow hidden sm:inline-block" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ui-checkbox v-if="isLibraryPage && !isPodcastLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
 | 
					        <ui-checkbox v-if="isLibraryPage && isBookLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
 | 
				
			||||||
        <controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
 | 
					        <controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
 | 
				
			||||||
        <controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
 | 
					        <controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
 | 
				
			||||||
        <controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
 | 
					        <controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
 | 
				
			||||||
@ -153,9 +153,15 @@ export default {
 | 
				
			|||||||
    currentLibraryMediaType() {
 | 
					    currentLibraryMediaType() {
 | 
				
			||||||
      return this.$store.getters['libraries/getCurrentLibraryMediaType']
 | 
					      return this.$store.getters['libraries/getCurrentLibraryMediaType']
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    isBookLibrary() {
 | 
				
			||||||
 | 
					      return this.currentLibraryMediaType === 'book'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    isPodcastLibrary() {
 | 
					    isPodcastLibrary() {
 | 
				
			||||||
      return this.currentLibraryMediaType === 'podcast'
 | 
					      return this.currentLibraryMediaType === 'podcast'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    isMusicLibrary() {
 | 
				
			||||||
 | 
					      return this.currentLibraryMediaType === 'music'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    isLibraryPage() {
 | 
					    isLibraryPage() {
 | 
				
			||||||
      return this.page === ''
 | 
					      return this.page === ''
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -184,6 +190,7 @@ export default {
 | 
				
			|||||||
      return this.totalEntities
 | 
					      return this.totalEntities
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    entityName() {
 | 
					    entityName() {
 | 
				
			||||||
 | 
					      if (this.isMusicLibrary) return 'Tracks'
 | 
				
			||||||
      if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
 | 
					      if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
 | 
				
			||||||
      if (!this.page) return this.$strings.LabelBooks
 | 
					      if (!this.page) return this.$strings.LabelBooks
 | 
				
			||||||
      if (this.isSeriesPage) return this.$strings.LabelSeries
 | 
					      if (this.isSeriesPage) return this.$strings.LabelSeries
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,7 @@
 | 
				
			|||||||
      <div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
 | 
					      <div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
 | 
				
			||||||
    </nuxt-link>
 | 
					    </nuxt-link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
 | 
					    <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
 | 
				
			||||||
      <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
 | 
					      <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
 | 
				
			||||||
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
 | 
					        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
 | 
				
			||||||
      </svg>
 | 
					      </svg>
 | 
				
			||||||
@ -41,7 +41,7 @@
 | 
				
			|||||||
      <div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
 | 
					      <div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
 | 
				
			||||||
    </nuxt-link>
 | 
					    </nuxt-link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
 | 
					    <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
 | 
				
			||||||
      <span class="material-icons-outlined text-2xl">collections_bookmark</span>
 | 
					      <span class="material-icons-outlined text-2xl">collections_bookmark</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
 | 
					      <p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
 | 
				
			||||||
@ -49,7 +49,7 @@
 | 
				
			|||||||
      <div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
 | 
					      <div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
 | 
				
			||||||
    </nuxt-link>
 | 
					    </nuxt-link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
 | 
					    <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
 | 
				
			||||||
      <svg class="w-6 h-6" viewBox="0 0 24 24">
 | 
					      <svg class="w-6 h-6" viewBox="0 0 24 24">
 | 
				
			||||||
        <path
 | 
					        <path
 | 
				
			||||||
          fill="currentColor"
 | 
					          fill="currentColor"
 | 
				
			||||||
@ -132,6 +132,9 @@ export default {
 | 
				
			|||||||
    currentLibraryMediaType() {
 | 
					    currentLibraryMediaType() {
 | 
				
			||||||
      return this.$store.getters['libraries/getCurrentLibraryMediaType']
 | 
					      return this.$store.getters['libraries/getCurrentLibraryMediaType']
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    isBookLibrary() {
 | 
				
			||||||
 | 
					      return this.currentLibraryMediaType === 'book'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    isPodcastLibrary() {
 | 
					    isPodcastLibrary() {
 | 
				
			||||||
      return this.currentLibraryMediaType === 'podcast'
 | 
					      return this.currentLibraryMediaType === 'podcast'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -122,6 +122,9 @@ export default {
 | 
				
			|||||||
    isPodcast() {
 | 
					    isPodcast() {
 | 
				
			||||||
      return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'podcast' : false
 | 
					      return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'podcast' : false
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    isMusic() {
 | 
				
			||||||
 | 
					      return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'music' : false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    mediaMetadata() {
 | 
					    mediaMetadata() {
 | 
				
			||||||
      return this.media.metadata || {}
 | 
					      return this.media.metadata || {}
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -405,8 +408,8 @@ export default {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async playLibraryItem(payload) {
 | 
					    async playLibraryItem(payload) {
 | 
				
			||||||
      var libraryItemId = payload.libraryItemId
 | 
					      const libraryItemId = payload.libraryItemId
 | 
				
			||||||
      var episodeId = payload.episodeId || null
 | 
					      const episodeId = payload.episodeId || null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) {
 | 
					      if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) {
 | 
				
			||||||
        if (payload.startTime !== null && !isNaN(payload.startTime)) {
 | 
					        if (payload.startTime !== null && !isNaN(payload.startTime)) {
 | 
				
			||||||
@ -417,11 +420,12 @@ export default {
 | 
				
			|||||||
        return
 | 
					        return
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var libraryItem = await this.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
 | 
					      const libraryItem = await this.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
 | 
				
			||||||
        console.error('Failed to fetch full item', error)
 | 
					        console.error('Failed to fetch full item', error)
 | 
				
			||||||
        return null
 | 
					        return null
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      if (!libraryItem) return
 | 
					      if (!libraryItem) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.$store.commit('setMediaPlaying', {
 | 
					      this.$store.commit('setMediaPlaying', {
 | 
				
			||||||
        libraryItem,
 | 
					        libraryItem,
 | 
				
			||||||
        episodeId,
 | 
					        episodeId,
 | 
				
			||||||
 | 
				
			|||||||
@ -190,6 +190,9 @@ export default {
 | 
				
			|||||||
    isPodcast() {
 | 
					    isPodcast() {
 | 
				
			||||||
      return this.mediaType === 'podcast'
 | 
					      return this.mediaType === 'podcast'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    isMusic() {
 | 
				
			||||||
 | 
					      return this.mediaType === 'music'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    placeholderUrl() {
 | 
					    placeholderUrl() {
 | 
				
			||||||
      const config = this.$config || this.$nuxt.$config
 | 
					      const config = this.$config || this.$nuxt.$config
 | 
				
			||||||
      return `${config.routerBasePath}/book_placeholder.jpg`
 | 
					      return `${config.routerBasePath}/book_placeholder.jpg`
 | 
				
			||||||
@ -305,6 +308,7 @@ export default {
 | 
				
			|||||||
      return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
 | 
					      return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    userProgress() {
 | 
					    userProgress() {
 | 
				
			||||||
 | 
					      if (this.isMusic) return null
 | 
				
			||||||
      if (this.episodeProgress) return this.episodeProgress
 | 
					      if (this.episodeProgress) return this.episodeProgress
 | 
				
			||||||
      return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
 | 
					      return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -67,6 +67,10 @@ export default {
 | 
				
			|||||||
          value: 'podcast',
 | 
					          value: 'podcast',
 | 
				
			||||||
          text: this.$strings.LabelPodcasts
 | 
					          text: this.$strings.LabelPodcasts
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        // {
 | 
				
			||||||
 | 
					        //   value: 'music',
 | 
				
			||||||
 | 
					        //   text: 'Music'
 | 
				
			||||||
 | 
					        // }
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    folderPaths() {
 | 
					    folderPaths() {
 | 
				
			||||||
 | 
				
			|||||||
@ -132,6 +132,7 @@
 | 
				
			|||||||
              <span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
 | 
					              <span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
 | 
				
			||||||
              {{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
 | 
					              {{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
 | 
				
			||||||
            </ui-btn>
 | 
					            </ui-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
 | 
					            <ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
 | 
				
			||||||
              <span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">error</span>
 | 
					              <span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">error</span>
 | 
				
			||||||
              {{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
 | 
					              {{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
 | 
				
			||||||
@ -150,11 +151,11 @@
 | 
				
			|||||||
              <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="!isPodcast" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
 | 
					            <ui-tooltip v-if="!isPodcast && !isMusic" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" 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 v-if="!isPodcast && userCanUpdate" :text="$strings.LabelCollections" direction="top">
 | 
					            <ui-tooltip v-if="showCollectionsButton" :text="$strings.LabelCollections" 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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -263,12 +264,18 @@ export default {
 | 
				
			|||||||
    isDeveloperMode() {
 | 
					    isDeveloperMode() {
 | 
				
			||||||
      return this.$store.state.developerMode
 | 
					      return this.$store.state.developerMode
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    isBook() {
 | 
				
			||||||
 | 
					      return this.libraryItem.mediaType === 'book'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    isPodcast() {
 | 
					    isPodcast() {
 | 
				
			||||||
      return this.libraryItem.mediaType === 'podcast'
 | 
					      return this.libraryItem.mediaType === 'podcast'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    isVideo() {
 | 
					    isVideo() {
 | 
				
			||||||
      return this.libraryItem.mediaType === 'video'
 | 
					      return this.libraryItem.mediaType === 'video'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    isMusic() {
 | 
				
			||||||
 | 
					      return this.libraryItem.mediaType === 'music'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    isMissing() {
 | 
					    isMissing() {
 | 
				
			||||||
      return this.libraryItem.isMissing
 | 
					      return this.libraryItem.isMissing
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -276,11 +283,12 @@ export default {
 | 
				
			|||||||
      return this.libraryItem.isInvalid
 | 
					      return this.libraryItem.isInvalid
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    invalidAudioFiles() {
 | 
					    invalidAudioFiles() {
 | 
				
			||||||
      if (this.isPodcast || this.isVideo) return []
 | 
					      if (!this.isBook) return []
 | 
				
			||||||
      return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
 | 
					      return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showPlayButton() {
 | 
					    showPlayButton() {
 | 
				
			||||||
      if (this.isMissing || this.isInvalid) return false
 | 
					      if (this.isMissing || this.isInvalid) return false
 | 
				
			||||||
 | 
					      if (this.isMusic) return !!this.audioFile
 | 
				
			||||||
      if (this.isVideo) return !!this.videoFile
 | 
					      if (this.isVideo) return !!this.videoFile
 | 
				
			||||||
      if (this.isPodcast) return this.podcastEpisodes.length
 | 
					      if (this.isPodcast) return this.podcastEpisodes.length
 | 
				
			||||||
      return this.tracks.length
 | 
					      return this.tracks.length
 | 
				
			||||||
@ -374,6 +382,10 @@ export default {
 | 
				
			|||||||
    videoFile() {
 | 
					    videoFile() {
 | 
				
			||||||
      return this.media.videoFile
 | 
					      return this.media.videoFile
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    audioFile() {
 | 
				
			||||||
 | 
					      // Music track
 | 
				
			||||||
 | 
					      return this.media.audioFile
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    showExperimentalReadAlert() {
 | 
					    showExperimentalReadAlert() {
 | 
				
			||||||
      return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures && !this.enableEReader
 | 
					      return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures && !this.enableEReader
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -381,6 +393,7 @@ export default {
 | 
				
			|||||||
      return this.mediaMetadata.description || ''
 | 
					      return this.mediaMetadata.description || ''
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    userMediaProgress() {
 | 
					    userMediaProgress() {
 | 
				
			||||||
 | 
					      if (this.isMusic) return null
 | 
				
			||||||
      return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
 | 
					      return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    userIsFinished() {
 | 
					    userIsFinished() {
 | 
				
			||||||
@ -425,8 +438,11 @@ export default {
 | 
				
			|||||||
      return this.userIsAdminOrUp || this.rssFeedUrl
 | 
					      return this.userIsAdminOrUp || this.rssFeedUrl
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showQueueBtn() {
 | 
					    showQueueBtn() {
 | 
				
			||||||
      if (this.isPodcast || this.isVideo) return false
 | 
					      if (!this.isBook) return false
 | 
				
			||||||
      return !this.$store.getters['getIsStreamingFromDifferentLibrary'] && this.streamLibraryItem
 | 
					      return !this.$store.getters['getIsStreamingFromDifferentLibrary'] && this.streamLibraryItem
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showCollectionsButton() {
 | 
				
			||||||
 | 
					      return this.isBook && this.userCanUpdate
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
@ -531,14 +547,14 @@ export default {
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    playItem(startTime = null) {
 | 
					    playItem(startTime = null) {
 | 
				
			||||||
      var episodeId = null
 | 
					      let episodeId = null
 | 
				
			||||||
      const queueItems = []
 | 
					      const queueItems = []
 | 
				
			||||||
      if (this.isPodcast) {
 | 
					      if (this.isPodcast) {
 | 
				
			||||||
        const episodesInListeningOrder = this.podcastEpisodes.map((ep) => ({ ...ep })).sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' }))
 | 
					        const episodesInListeningOrder = this.podcastEpisodes.map((ep) => ({ ...ep })).sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Find most recent episode unplayed
 | 
					        // Find most recent episode unplayed
 | 
				
			||||||
        var episodeIndex = episodesInListeningOrder.findLastIndex((ep) => {
 | 
					        let episodeIndex = episodesInListeningOrder.findLastIndex((ep) => {
 | 
				
			||||||
          var podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
 | 
					          const podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
 | 
				
			||||||
          return !podcastProgress || !podcastProgress.isFinished
 | 
					          return !podcastProgress || !podcastProgress.isFinished
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        if (episodeIndex < 0) episodeIndex = 0
 | 
					        if (episodeIndex < 0) episodeIndex = 0
 | 
				
			||||||
 | 
				
			|||||||
@ -17,6 +17,7 @@ export default class PlayerHandler {
 | 
				
			|||||||
    this.playerState = 'IDLE'
 | 
					    this.playerState = 'IDLE'
 | 
				
			||||||
    this.isHlsTranscode = false
 | 
					    this.isHlsTranscode = false
 | 
				
			||||||
    this.isVideo = false
 | 
					    this.isVideo = false
 | 
				
			||||||
 | 
					    this.isMusic = false
 | 
				
			||||||
    this.currentSessionId = null
 | 
					    this.currentSessionId = null
 | 
				
			||||||
    this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page)
 | 
					    this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page)
 | 
				
			||||||
    this.startTime = 0
 | 
					    this.startTime = 0
 | 
				
			||||||
@ -54,10 +55,13 @@ export default class PlayerHandler {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
 | 
					  load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
 | 
				
			||||||
    this.libraryItem = libraryItem
 | 
					    this.libraryItem = libraryItem
 | 
				
			||||||
 | 
					    this.isVideo = libraryItem.mediaType === 'video'
 | 
				
			||||||
 | 
					    this.isMusic = libraryItem.mediaType === 'music'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.episodeId = episodeId
 | 
					    this.episodeId = episodeId
 | 
				
			||||||
    this.playWhenReady = playWhenReady
 | 
					    this.playWhenReady = playWhenReady
 | 
				
			||||||
    this.initialPlaybackRate = playbackRate
 | 
					    this.initialPlaybackRate = this.isMusic ? 1 : playbackRate
 | 
				
			||||||
    this.isVideo = libraryItem.mediaType === 'video'
 | 
					
 | 
				
			||||||
    this.startTimeOverride = (startTimeOverride == null || isNaN(startTimeOverride)) ? undefined : Number(startTimeOverride)
 | 
					    this.startTimeOverride = (startTimeOverride == null || isNaN(startTimeOverride)) ? undefined : Number(startTimeOverride)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!this.player) this.switchPlayer(playWhenReady)
 | 
					    if (!this.player) this.switchPlayer(playWhenReady)
 | 
				
			||||||
@ -140,12 +144,16 @@ export default class PlayerHandler {
 | 
				
			|||||||
  playerStateChange(state) {
 | 
					  playerStateChange(state) {
 | 
				
			||||||
    console.log('[PlayerHandler] Player state change', state)
 | 
					    console.log('[PlayerHandler] Player state change', state)
 | 
				
			||||||
    this.playerState = state
 | 
					    this.playerState = state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!this.isMusic) {
 | 
				
			||||||
      if (this.playerState === 'PLAYING') {
 | 
					      if (this.playerState === 'PLAYING') {
 | 
				
			||||||
        this.setPlaybackRate(this.initialPlaybackRate)
 | 
					        this.setPlaybackRate(this.initialPlaybackRate)
 | 
				
			||||||
        this.startPlayInterval()
 | 
					        this.startPlayInterval()
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        this.stopPlayInterval()
 | 
					        this.stopPlayInterval()
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.player) {
 | 
					    if (this.player) {
 | 
				
			||||||
      if (this.playerState === 'LOADED' || this.playerState === 'PLAYING') {
 | 
					      if (this.playerState === 'LOADED' || this.playerState === 'PLAYING') {
 | 
				
			||||||
        this.ctx.setDuration(this.getDuration())
 | 
					        this.ctx.setDuration(this.getDuration())
 | 
				
			||||||
 | 
				
			|||||||
@ -230,27 +230,30 @@ class CoverManager {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async saveEmbeddedCoverArt(libraryItem) {
 | 
					  async saveEmbeddedCoverArt(libraryItem) {
 | 
				
			||||||
    var audioFileWithCover = null
 | 
					    const audioFileWithCover = null
 | 
				
			||||||
    if (libraryItem.mediaType === 'book') audioFileWithCover = libraryItem.media.audioFiles.find(af => af.embeddedCoverArt)
 | 
					    if (libraryItem.mediaType === 'book') {
 | 
				
			||||||
    else {
 | 
					      audioFileWithCover = libraryItem.media.audioFiles.find(af => af.embeddedCoverArt)
 | 
				
			||||||
      var episodeWithCover = libraryItem.media.episodes.find(ep => ep.audioFile.embeddedCoverArt)
 | 
					    } else if (libraryItem.mediaType == 'podcast') {
 | 
				
			||||||
 | 
					      const episodeWithCover = libraryItem.media.episodes.find(ep => ep.audioFile.embeddedCoverArt)
 | 
				
			||||||
      if (episodeWithCover) audioFileWithCover = episodeWithCover.audioFile
 | 
					      if (episodeWithCover) audioFileWithCover = episodeWithCover.audioFile
 | 
				
			||||||
 | 
					    } else if (libraryItem.mediaType === 'music') {
 | 
				
			||||||
 | 
					      audioFileWithCover = libraryItem.media.audioFile
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (!audioFileWithCover) return false
 | 
					    if (!audioFileWithCover) return false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var coverDirPath = this.getCoverDirectory(libraryItem)
 | 
					    const coverDirPath = this.getCoverDirectory(libraryItem)
 | 
				
			||||||
    await fs.ensureDir(coverDirPath)
 | 
					    await fs.ensureDir(coverDirPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var coverFilename = audioFileWithCover.embeddedCoverArt === 'png' ? 'cover.png' : 'cover.jpg'
 | 
					    const coverFilename = audioFileWithCover.embeddedCoverArt === 'png' ? 'cover.png' : 'cover.jpg'
 | 
				
			||||||
    var coverFilePath = Path.join(coverDirPath, coverFilename)
 | 
					    const coverFilePath = Path.join(coverDirPath, coverFilename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var coverAlreadyExists = await fs.pathExists(coverFilePath)
 | 
					    const coverAlreadyExists = await fs.pathExists(coverFilePath)
 | 
				
			||||||
    if (coverAlreadyExists) {
 | 
					    if (coverAlreadyExists) {
 | 
				
			||||||
      Logger.warn(`[CoverManager] Extract embedded cover art but cover already exists for "${libraryItem.media.metadata.title}" - bail`)
 | 
					      Logger.warn(`[CoverManager] Extract embedded cover art but cover already exists for "${libraryItem.media.metadata.title}" - bail`)
 | 
				
			||||||
      return false
 | 
					      return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
 | 
					    const success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
 | 
				
			||||||
    if (success) {
 | 
					    if (success) {
 | 
				
			||||||
      await filePerms.setDefault(coverFilePath)
 | 
					      await filePerms.setDefault(coverFilePath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -127,7 +127,7 @@ class PlaybackSessionManager {
 | 
				
			|||||||
    const shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId))
 | 
					    const shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId))
 | 
				
			||||||
    const mediaPlayer = options.mediaPlayer || 'unknown'
 | 
					    const mediaPlayer = options.mediaPlayer || 'unknown'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const userProgress = user.getMediaProgress(libraryItem.id, episodeId)
 | 
					    const userProgress = libraryItem.isMusic ? null : user.getMediaProgress(libraryItem.id, episodeId)
 | 
				
			||||||
    let userStartTime = 0
 | 
					    let userStartTime = 0
 | 
				
			||||||
    if (userProgress) {
 | 
					    if (userProgress) {
 | 
				
			||||||
      if (userProgress.isFinished) {
 | 
					      if (userProgress.isFinished) {
 | 
				
			||||||
 | 
				
			|||||||
@ -57,7 +57,9 @@ class Library {
 | 
				
			|||||||
      else if (this.icon === 'comic') this.icon = 'file-picture'
 | 
					      else if (this.icon === 'comic') this.icon = 'file-picture'
 | 
				
			||||||
      else this.icon = 'database'
 | 
					      else this.icon = 'database'
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (!this.mediaType || (this.mediaType !== 'podcast' && this.mediaType !== 'book' && this.mediaType !== 'video')) {
 | 
					
 | 
				
			||||||
 | 
					    const mediaTypes = ['podcast', 'book', 'video', 'music']
 | 
				
			||||||
 | 
					    if (!this.mediaType || !mediaTypes.includes(this.mediaType)) {
 | 
				
			||||||
      this.mediaType = 'book'
 | 
					      this.mediaType = 'book'
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@ const LibraryFile = require('./files/LibraryFile')
 | 
				
			|||||||
const Book = require('./mediaTypes/Book')
 | 
					const Book = require('./mediaTypes/Book')
 | 
				
			||||||
const Podcast = require('./mediaTypes/Podcast')
 | 
					const Podcast = require('./mediaTypes/Podcast')
 | 
				
			||||||
const Video = require('./mediaTypes/Video')
 | 
					const Video = require('./mediaTypes/Video')
 | 
				
			||||||
 | 
					const Music = require('./mediaTypes/Music')
 | 
				
			||||||
const { areEquivalent, copyValue, getId, cleanStringForSearch } = require('../utils/index')
 | 
					const { areEquivalent, copyValue, getId, cleanStringForSearch } = require('../utils/index')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LibraryItem {
 | 
					class LibraryItem {
 | 
				
			||||||
@ -72,6 +73,8 @@ class LibraryItem {
 | 
				
			|||||||
      this.media = new Podcast(libraryItem.media)
 | 
					      this.media = new Podcast(libraryItem.media)
 | 
				
			||||||
    } else if (this.mediaType === 'video') {
 | 
					    } else if (this.mediaType === 'video') {
 | 
				
			||||||
      this.media = new Video(libraryItem.media)
 | 
					      this.media = new Video(libraryItem.media)
 | 
				
			||||||
 | 
					    } else if (this.mediaType === 'music') {
 | 
				
			||||||
 | 
					      this.media = new Music(libraryItem.media)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.media.libraryItemId = this.id
 | 
					    this.media.libraryItemId = this.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -153,13 +156,14 @@ class LibraryItem {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  get isPodcast() { return this.mediaType === 'podcast' }
 | 
					  get isPodcast() { return this.mediaType === 'podcast' }
 | 
				
			||||||
  get isBook() { return this.mediaType === 'book' }
 | 
					  get isBook() { return this.mediaType === 'book' }
 | 
				
			||||||
 | 
					  get isMusic() { return this.mediaType === 'music' }
 | 
				
			||||||
  get size() {
 | 
					  get size() {
 | 
				
			||||||
    var total = 0
 | 
					    let total = 0
 | 
				
			||||||
    this.libraryFiles.forEach((lf) => total += lf.metadata.size)
 | 
					    this.libraryFiles.forEach((lf) => total += lf.metadata.size)
 | 
				
			||||||
    return total
 | 
					    return total
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  get audioFileTotalSize() {
 | 
					  get audioFileTotalSize() {
 | 
				
			||||||
    var total = 0
 | 
					    let total = 0
 | 
				
			||||||
    this.libraryFiles.filter(lf => lf.fileType == 'audio').forEach((lf) => total += lf.metadata.size)
 | 
					    this.libraryFiles.filter(lf => lf.fileType == 'audio').forEach((lf) => total += lf.metadata.size)
 | 
				
			||||||
    return total
 | 
					    return total
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -182,8 +186,10 @@ class LibraryItem {
 | 
				
			|||||||
      this.media = new Video()
 | 
					      this.media = new Video()
 | 
				
			||||||
    } else if (libraryMediaType === 'podcast') {
 | 
					    } else if (libraryMediaType === 'podcast') {
 | 
				
			||||||
      this.media = new Podcast()
 | 
					      this.media = new Podcast()
 | 
				
			||||||
    } else {
 | 
					    } else if (libraryMediaType === 'book') {
 | 
				
			||||||
      this.media = new Book()
 | 
					      this.media = new Book()
 | 
				
			||||||
 | 
					    } else if (libraryMediaType === 'music') {
 | 
				
			||||||
 | 
					      this.media = new Music()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.media.libraryItemId = this.id
 | 
					    this.media.libraryItemId = this.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -348,11 +354,11 @@ class LibraryItem {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var newLibraryFiles = []
 | 
					    const newLibraryFiles = []
 | 
				
			||||||
    var existingLibraryFiles = []
 | 
					    const existingLibraryFiles = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dataFound.libraryFiles.forEach((lf) => {
 | 
					    dataFound.libraryFiles.forEach((lf) => {
 | 
				
			||||||
      var fileFoundCheck = this.checkFileFound(lf, true)
 | 
					      const fileFoundCheck = this.checkFileFound(lf, true)
 | 
				
			||||||
      if (fileFoundCheck === null) {
 | 
					      if (fileFoundCheck === null) {
 | 
				
			||||||
        newLibraryFiles.push(lf)
 | 
					        newLibraryFiles.push(lf)
 | 
				
			||||||
      } else if (fileFoundCheck && lf.metadata.format !== 'abs') { // Ignore abs file updates
 | 
					      } else if (fileFoundCheck && lf.metadata.format !== 'abs') { // Ignore abs file updates
 | 
				
			||||||
@ -397,7 +403,7 @@ class LibraryItem {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // If cover path is in item folder, make sure libraryFile exists for it
 | 
					    // If cover path is in item folder, make sure libraryFile exists for it
 | 
				
			||||||
    if (this.media.coverPath && this.media.coverPath.startsWith(this.path)) {
 | 
					    if (this.media.coverPath && this.media.coverPath.startsWith(this.path)) {
 | 
				
			||||||
      var lf = this.libraryFiles.find(lf => lf.metadata.path === this.media.coverPath)
 | 
					      const lf = this.libraryFiles.find(lf => lf.metadata.path === this.media.coverPath)
 | 
				
			||||||
      if (!lf) {
 | 
					      if (!lf) {
 | 
				
			||||||
        Logger.warn(`[LibraryItem] Invalid cover path - library file dne "${this.media.coverPath}"`)
 | 
					        Logger.warn(`[LibraryItem] Invalid cover path - library file dne "${this.media.coverPath}"`)
 | 
				
			||||||
        this.media.updateCover('')
 | 
					        this.media.updateCover('')
 | 
				
			||||||
@ -419,7 +425,7 @@ class LibraryItem {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Set metadata from files
 | 
					  // Set metadata from files
 | 
				
			||||||
  async syncFiles(preferOpfMetadata) {
 | 
					  async syncFiles(preferOpfMetadata) {
 | 
				
			||||||
    var hasUpdated = false
 | 
					    let hasUpdated = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.mediaType === 'book') {
 | 
					    if (this.mediaType === 'book') {
 | 
				
			||||||
      // Add/update ebook file (ebooks that were removed are removed in checkScanData)
 | 
					      // Add/update ebook file (ebooks that were removed are removed in checkScanData)
 | 
				
			||||||
@ -436,7 +442,7 @@ class LibraryItem {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Set cover image if not set
 | 
					    // Set cover image if not set
 | 
				
			||||||
    var imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
 | 
					    const imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
 | 
				
			||||||
    if (imageFiles.length && !this.media.coverPath) {
 | 
					    if (imageFiles.length && !this.media.coverPath) {
 | 
				
			||||||
      this.media.coverPath = imageFiles[0].metadata.path
 | 
					      this.media.coverPath = imageFiles[0].metadata.path
 | 
				
			||||||
      Logger.debug('[LibraryItem] Set media cover path', this.media.coverPath)
 | 
					      Logger.debug('[LibraryItem] Set media cover path', this.media.coverPath)
 | 
				
			||||||
@ -444,7 +450,7 @@ class LibraryItem {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Parse metadata files
 | 
					    // Parse metadata files
 | 
				
			||||||
    var textMetadataFiles = this.libraryFiles.filter(lf => lf.fileType === 'metadata' || lf.fileType === 'text')
 | 
					    const textMetadataFiles = this.libraryFiles.filter(lf => lf.fileType === 'metadata' || lf.fileType === 'text')
 | 
				
			||||||
    if (textMetadataFiles.length) {
 | 
					    if (textMetadataFiles.length) {
 | 
				
			||||||
      if (await this.media.syncMetadataFiles(textMetadataFiles, preferOpfMetadata)) {
 | 
					      if (await this.media.syncMetadataFiles(textMetadataFiles, preferOpfMetadata)) {
 | 
				
			||||||
        hasUpdated = true
 | 
					        hasUpdated = true
 | 
				
			||||||
@ -468,12 +474,12 @@ class LibraryItem {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Saves metadata.abs file
 | 
					  // Saves metadata.abs file
 | 
				
			||||||
  async saveMetadata() {
 | 
					  async saveMetadata() {
 | 
				
			||||||
    if (this.mediaType === 'video') return
 | 
					    if (this.mediaType === 'video' || this.mediaType === 'music') return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.isSavingMetadata) return
 | 
					    if (this.isSavingMetadata) return
 | 
				
			||||||
    this.isSavingMetadata = true
 | 
					    this.isSavingMetadata = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var metadataPath = Path.join(global.MetadataPath, 'items', this.id)
 | 
					    let metadataPath = Path.join(global.MetadataPath, 'items', this.id)
 | 
				
			||||||
    if (global.ServerSettings.storeMetadataWithItem && !this.isFile) {
 | 
					    if (global.ServerSettings.storeMetadataWithItem && !this.isFile) {
 | 
				
			||||||
      metadataPath = this.path
 | 
					      metadataPath = this.path
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 | 
				
			|||||||
@ -148,7 +148,7 @@ class PodcastEpisode {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Only checks container format
 | 
					  // Only checks container format
 | 
				
			||||||
  checkCanDirectPlay(payload) {
 | 
					  checkCanDirectPlay(payload) {
 | 
				
			||||||
    var supportedMimeTypes = payload.supportedMimeTypes || []
 | 
					    const supportedMimeTypes = payload.supportedMimeTypes || []
 | 
				
			||||||
    return supportedMimeTypes.includes(this.audioFile.mimeType)
 | 
					    return supportedMimeTypes.includes(this.audioFile.mimeType)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -118,9 +118,9 @@ class Book {
 | 
				
			|||||||
    return this.missingParts.length || this.invalidAudioFiles.length
 | 
					    return this.missingParts.length || this.invalidAudioFiles.length
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  get tracks() {
 | 
					  get tracks() {
 | 
				
			||||||
    var startOffset = 0
 | 
					    let startOffset = 0
 | 
				
			||||||
    return this.includedAudioFiles.map((af) => {
 | 
					    return this.includedAudioFiles.map((af) => {
 | 
				
			||||||
      var audioTrack = new AudioTrack()
 | 
					      const audioTrack = new AudioTrack()
 | 
				
			||||||
      audioTrack.setData(this.libraryItemId, af, startOffset)
 | 
					      audioTrack.setData(this.libraryItemId, af, startOffset)
 | 
				
			||||||
      startOffset += audioTrack.duration
 | 
					      startOffset += audioTrack.duration
 | 
				
			||||||
      return audioTrack
 | 
					      return audioTrack
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										159
									
								
								server/objects/mediaTypes/Music.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								server/objects/mediaTypes/Music.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,159 @@
 | 
				
			|||||||
 | 
					const Logger = require('../../Logger')
 | 
				
			||||||
 | 
					const AudioFile = require('../files/AudioFile')
 | 
				
			||||||
 | 
					const AudioTrack = require('../files/AudioTrack')
 | 
				
			||||||
 | 
					const MusicMetadata = require('../metadata/MusicMetadata')
 | 
				
			||||||
 | 
					const { areEquivalent, copyValue } = require('../../utils/index')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Music {
 | 
				
			||||||
 | 
					  constructor(music) {
 | 
				
			||||||
 | 
					    this.libraryItemId = null
 | 
				
			||||||
 | 
					    this.metadata = null
 | 
				
			||||||
 | 
					    this.coverPath = null
 | 
				
			||||||
 | 
					    this.tags = []
 | 
				
			||||||
 | 
					    this.audioFile = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (music) {
 | 
				
			||||||
 | 
					      this.construct(music)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  construct(music) {
 | 
				
			||||||
 | 
					    this.libraryItemId = music.libraryItemId
 | 
				
			||||||
 | 
					    this.metadata = new MusicMetadata(music.metadata)
 | 
				
			||||||
 | 
					    this.coverPath = music.coverPath
 | 
				
			||||||
 | 
					    this.tags = [...music.tags]
 | 
				
			||||||
 | 
					    this.audioFile = new AudioFile(music.audioFile)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toJSON() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      libraryItemId: this.libraryItemId,
 | 
				
			||||||
 | 
					      metadata: this.metadata.toJSON(),
 | 
				
			||||||
 | 
					      coverPath: this.coverPath,
 | 
				
			||||||
 | 
					      tags: [...this.tags],
 | 
				
			||||||
 | 
					      audioFile: this.audioFile.toJSON(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toJSONMinified() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      metadata: this.metadata.toJSONMinified(),
 | 
				
			||||||
 | 
					      coverPath: this.coverPath,
 | 
				
			||||||
 | 
					      tags: [...this.tags],
 | 
				
			||||||
 | 
					      audioFile: this.audioFile.toJSON(),
 | 
				
			||||||
 | 
					      size: this.size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toJSONExpanded() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      libraryItemId: this.libraryItemId,
 | 
				
			||||||
 | 
					      metadata: this.metadata.toJSONExpanded(),
 | 
				
			||||||
 | 
					      coverPath: this.coverPath,
 | 
				
			||||||
 | 
					      tags: [...this.tags],
 | 
				
			||||||
 | 
					      audioFile: this.audioFile.toJSON(),
 | 
				
			||||||
 | 
					      size: this.size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get size() {
 | 
				
			||||||
 | 
					    return this.audioFile.metadata.size
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get hasMediaEntities() {
 | 
				
			||||||
 | 
					    return !!this.audioFile
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get shouldSearchForCover() {
 | 
				
			||||||
 | 
					    return false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get hasEmbeddedCoverArt() {
 | 
				
			||||||
 | 
					    return this.audioFile.embeddedCoverArt
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get hasIssues() {
 | 
				
			||||||
 | 
					    return false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get duration() {
 | 
				
			||||||
 | 
					    return this.audioFile.duration || 0
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get audioTrack() {
 | 
				
			||||||
 | 
					    const audioTrack = new AudioTrack()
 | 
				
			||||||
 | 
					    audioTrack.setData(this.libraryItemId, this.audioFile, 0)
 | 
				
			||||||
 | 
					    return audioTrack
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get numTracks() {
 | 
				
			||||||
 | 
					    return 1
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  update(payload) {
 | 
				
			||||||
 | 
					    const json = this.toJSON()
 | 
				
			||||||
 | 
					    delete json.episodes // do not update media entities here
 | 
				
			||||||
 | 
					    let hasUpdates = false
 | 
				
			||||||
 | 
					    for (const key in json) {
 | 
				
			||||||
 | 
					      if (payload[key] !== undefined) {
 | 
				
			||||||
 | 
					        if (key === 'metadata') {
 | 
				
			||||||
 | 
					          if (this.metadata.update(payload.metadata)) {
 | 
				
			||||||
 | 
					            hasUpdates = true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else if (!areEquivalent(payload[key], json[key])) {
 | 
				
			||||||
 | 
					          this[key] = copyValue(payload[key])
 | 
				
			||||||
 | 
					          Logger.debug('[Podcast] Key updated', key, this[key])
 | 
				
			||||||
 | 
					          hasUpdates = true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return hasUpdates
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updateCover(coverPath) {
 | 
				
			||||||
 | 
					    coverPath = coverPath.replace(/\\/g, '/')
 | 
				
			||||||
 | 
					    if (this.coverPath === coverPath) return false
 | 
				
			||||||
 | 
					    this.coverPath = coverPath
 | 
				
			||||||
 | 
					    return true
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  removeFileWithInode(inode) {
 | 
				
			||||||
 | 
					    return false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  findFileWithInode(inode) {
 | 
				
			||||||
 | 
					    return this.audioFile && this.audioFile.ino === inode
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setData(mediaData) {
 | 
				
			||||||
 | 
					    this.metadata = new MusicMetadata()
 | 
				
			||||||
 | 
					    if (mediaData.metadata) {
 | 
				
			||||||
 | 
					      this.metadata.setData(mediaData.metadata)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.coverPath = mediaData.coverPath || null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setAudioFile(audioFile) {
 | 
				
			||||||
 | 
					    this.audioFile = audioFile
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
 | 
				
			||||||
 | 
					    return false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  searchQuery(query) {
 | 
				
			||||||
 | 
					    return {}
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Only checks container format
 | 
				
			||||||
 | 
					  checkCanDirectPlay(payload) {
 | 
				
			||||||
 | 
					    return true
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getDirectPlayTracklist() {
 | 
				
			||||||
 | 
					    return [this.audioTrack]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getPlaybackTitle() {
 | 
				
			||||||
 | 
					    return this.metadata.title
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getPlaybackAuthor() {
 | 
				
			||||||
 | 
					    return this.metadata.artist
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					module.exports = Music
 | 
				
			||||||
							
								
								
									
										104
									
								
								server/objects/metadata/MusicMetadata.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								server/objects/metadata/MusicMetadata.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					const Logger = require('../../Logger')
 | 
				
			||||||
 | 
					const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MusicMetadata {
 | 
				
			||||||
 | 
					  constructor(metadata) {
 | 
				
			||||||
 | 
					    this.title = null
 | 
				
			||||||
 | 
					    this.artist = null
 | 
				
			||||||
 | 
					    this.album = null
 | 
				
			||||||
 | 
					    this.genres = [] // Array of strings
 | 
				
			||||||
 | 
					    this.releaseDate = null
 | 
				
			||||||
 | 
					    this.language = null
 | 
				
			||||||
 | 
					    this.explicit = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (metadata) {
 | 
				
			||||||
 | 
					      this.construct(metadata)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  construct(metadata) {
 | 
				
			||||||
 | 
					    this.title = metadata.title
 | 
				
			||||||
 | 
					    this.artist = metadata.artist
 | 
				
			||||||
 | 
					    this.album = metadata.album
 | 
				
			||||||
 | 
					    this.genres = metadata.genres ? [...metadata.genres] : []
 | 
				
			||||||
 | 
					    this.releaseDate = metadata.releaseDate || null
 | 
				
			||||||
 | 
					    this.language = metadata.language
 | 
				
			||||||
 | 
					    this.explicit = !!metadata.explicit
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toJSON() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      title: this.title,
 | 
				
			||||||
 | 
					      artist: this.artist,
 | 
				
			||||||
 | 
					      album: this.album,
 | 
				
			||||||
 | 
					      genres: [...this.genres],
 | 
				
			||||||
 | 
					      releaseDate: this.releaseDate,
 | 
				
			||||||
 | 
					      language: this.language,
 | 
				
			||||||
 | 
					      explicit: this.explicit
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toJSONMinified() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      title: this.title,
 | 
				
			||||||
 | 
					      titleIgnorePrefix: this.titlePrefixAtEnd,
 | 
				
			||||||
 | 
					      artist: this.artist,
 | 
				
			||||||
 | 
					      album: this.album,
 | 
				
			||||||
 | 
					      genres: [...this.genres],
 | 
				
			||||||
 | 
					      releaseDate: this.releaseDate,
 | 
				
			||||||
 | 
					      language: this.language,
 | 
				
			||||||
 | 
					      explicit: this.explicit
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toJSONExpanded() {
 | 
				
			||||||
 | 
					    return this.toJSONMinified()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  clone() {
 | 
				
			||||||
 | 
					    return new MusicMetadata(this.toJSON())
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get titleIgnorePrefix() {
 | 
				
			||||||
 | 
					    return getTitleIgnorePrefix(this.title)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get titlePrefixAtEnd() {
 | 
				
			||||||
 | 
					    return getTitlePrefixAtEnd(this.title)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  searchQuery(query) { // Returns key if match is found
 | 
				
			||||||
 | 
					    const keysToCheck = ['title', 'artist', 'album']
 | 
				
			||||||
 | 
					    for (const key of keysToCheck) {
 | 
				
			||||||
 | 
					      if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          matchKey: key,
 | 
				
			||||||
 | 
					          matchText: this[key]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setData(mediaMetadata = {}) {
 | 
				
			||||||
 | 
					    this.title = mediaMetadata.title || null
 | 
				
			||||||
 | 
					    this.artist = mediaMetadata.artist || null
 | 
				
			||||||
 | 
					    this.album = mediaMetadata.album || null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  update(payload) {
 | 
				
			||||||
 | 
					    const json = this.toJSON()
 | 
				
			||||||
 | 
					    let hasUpdates = false
 | 
				
			||||||
 | 
					    for (const key in json) {
 | 
				
			||||||
 | 
					      if (payload[key] !== undefined) {
 | 
				
			||||||
 | 
					        if (!areEquivalent(payload[key], json[key])) {
 | 
				
			||||||
 | 
					          this[key] = copyValue(payload[key])
 | 
				
			||||||
 | 
					          Logger.debug('[MusicMetadata] Key updated', key, this[key])
 | 
				
			||||||
 | 
					          hasUpdates = true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return hasUpdates
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					module.exports = MusicMetadata
 | 
				
			||||||
@ -57,9 +57,9 @@ class MediaFileScanner {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async scan(mediaType, libraryFile, mediaMetadataFromScan, verbose = false) {
 | 
					  async scan(mediaType, libraryFile, mediaMetadataFromScan, verbose = false) {
 | 
				
			||||||
    var probeStart = Date.now()
 | 
					    const probeStart = Date.now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var probeData = null
 | 
					    let probeData = null
 | 
				
			||||||
    // TODO: Temp not using tone for probing until more testing can be done
 | 
					    // TODO: Temp not using tone for probing until more testing can be done
 | 
				
			||||||
    // if (global.ServerSettings.scannerUseTone) {
 | 
					    // if (global.ServerSettings.scannerUseTone) {
 | 
				
			||||||
    //   Logger.debug(`[MediaFileScanner] using tone to probe audio file "${libraryFile.metadata.path}"`)
 | 
					    //   Logger.debug(`[MediaFileScanner] using tone to probe audio file "${libraryFile.metadata.path}"`)
 | 
				
			||||||
@ -79,7 +79,7 @@ class MediaFileScanner {
 | 
				
			|||||||
        return null
 | 
					        return null
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var videoFile = new VideoFile()
 | 
					      const videoFile = new VideoFile()
 | 
				
			||||||
      videoFile.setDataFromProbe(libraryFile, probeData)
 | 
					      videoFile.setDataFromProbe(libraryFile, probeData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
@ -92,7 +92,7 @@ class MediaFileScanner {
 | 
				
			|||||||
        return null
 | 
					        return null
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var audioFile = new AudioFile()
 | 
					      const audioFile = new AudioFile()
 | 
				
			||||||
      audioFile.trackNumFromMeta = probeData.trackNumber
 | 
					      audioFile.trackNumFromMeta = probeData.trackNumber
 | 
				
			||||||
      audioFile.discNumFromMeta = probeData.discNumber
 | 
					      audioFile.discNumFromMeta = probeData.discNumber
 | 
				
			||||||
      if (mediaType === 'book') {
 | 
					      if (mediaType === 'book') {
 | 
				
			||||||
@ -113,13 +113,13 @@ class MediaFileScanner {
 | 
				
			|||||||
  async executeMediaFileScans(libraryItem, mediaLibraryFiles, scanData) {
 | 
					  async executeMediaFileScans(libraryItem, mediaLibraryFiles, scanData) {
 | 
				
			||||||
    const mediaType = libraryItem.mediaType
 | 
					    const mediaType = libraryItem.mediaType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var scanStart = Date.now()
 | 
					    const scanStart = Date.now()
 | 
				
			||||||
    var mediaMetadataFromScan = scanData.media.metadata || null
 | 
					    const mediaMetadataFromScan = scanData.media.metadata || null
 | 
				
			||||||
    var proms = []
 | 
					    const proms = []
 | 
				
			||||||
    for (let i = 0; i < mediaLibraryFiles.length; i++) {
 | 
					    for (let i = 0; i < mediaLibraryFiles.length; i++) {
 | 
				
			||||||
      proms.push(this.scan(mediaType, mediaLibraryFiles[i], mediaMetadataFromScan))
 | 
					      proms.push(this.scan(mediaType, mediaLibraryFiles[i], mediaMetadataFromScan))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
 | 
					    const results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      audioFiles: results.filter(r => r.audioFile).map(r => r.audioFile),
 | 
					      audioFiles: results.filter(r => r.audioFile).map(r => r.audioFile),
 | 
				
			||||||
      videoFiles: results.filter(r => r.videoFile).map(r => r.videoFile),
 | 
					      videoFiles: results.filter(r => r.videoFile).map(r => r.videoFile),
 | 
				
			||||||
@ -131,7 +131,7 @@ class MediaFileScanner {
 | 
				
			|||||||
  isSequential(nums) {
 | 
					  isSequential(nums) {
 | 
				
			||||||
    if (!nums || !nums.length) return false
 | 
					    if (!nums || !nums.length) return false
 | 
				
			||||||
    if (nums.length === 1) return true
 | 
					    if (nums.length === 1) return true
 | 
				
			||||||
    var prev = nums[0]
 | 
					    let prev = nums[0]
 | 
				
			||||||
    for (let i = 1; i < nums.length; i++) {
 | 
					    for (let i = 1; i < nums.length; i++) {
 | 
				
			||||||
      if (nums[i] - prev > 1) return false
 | 
					      if (nums[i] - prev > 1) return false
 | 
				
			||||||
      prev = nums[i]
 | 
					      prev = nums[i]
 | 
				
			||||||
@ -207,9 +207,9 @@ class MediaFileScanner {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async scanMediaFiles(mediaLibraryFiles, scanData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan = null) {
 | 
					  async scanMediaFiles(mediaLibraryFiles, scanData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan = null) {
 | 
				
			||||||
    var hasUpdated = false
 | 
					    let hasUpdated = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var mediaScanResult = await this.executeMediaFileScans(libraryItem, mediaLibraryFiles, scanData)
 | 
					    const mediaScanResult = await this.executeMediaFileScans(libraryItem, mediaLibraryFiles, scanData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (libraryItem.mediaType === 'video') {
 | 
					    if (libraryItem.mediaType === 'video') {
 | 
				
			||||||
      if (mediaScanResult.videoFiles.length) {
 | 
					      if (mediaScanResult.videoFiles.length) {
 | 
				
			||||||
@ -223,32 +223,32 @@ class MediaFileScanner {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      Logger.debug(`Library Item "${scanData.path}" Media file scan took ${mediaScanResult.elapsed}ms with ${mediaScanResult.audioFiles.length} audio files averaging ${mediaScanResult.averageScanDuration}ms per MB`)
 | 
					      Logger.debug(`Library Item "${scanData.path}" Media file scan took ${mediaScanResult.elapsed}ms with ${mediaScanResult.audioFiles.length} audio files averaging ${mediaScanResult.averageScanDuration}ms per MB`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var newAudioFiles = mediaScanResult.audioFiles.filter(af => {
 | 
					      const newAudioFiles = mediaScanResult.audioFiles.filter(af => {
 | 
				
			||||||
        return !libraryItem.media.findFileWithInode(af.ino)
 | 
					        return !libraryItem.media.findFileWithInode(af.ino)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Book: Adding audio files to book media
 | 
					      // Book: Adding audio files to book media
 | 
				
			||||||
      if (libraryItem.mediaType === 'book') {
 | 
					      if (libraryItem.mediaType === 'book') {
 | 
				
			||||||
        var mediaScanFileInodes = mediaScanResult.audioFiles.map(af => af.ino)
 | 
					        const mediaScanFileInodes = mediaScanResult.audioFiles.map(af => af.ino)
 | 
				
			||||||
        // Filter for existing valid track audio files not included in the audio files scanned
 | 
					        // Filter for existing valid track audio files not included in the audio files scanned
 | 
				
			||||||
        var existingAudioFiles = libraryItem.media.audioFiles.filter(af => af.isValidTrack && !mediaScanFileInodes.includes(af.ino))
 | 
					        const existingAudioFiles = libraryItem.media.audioFiles.filter(af => af.isValidTrack && !mediaScanFileInodes.includes(af.ino))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (newAudioFiles.length) {
 | 
					        if (newAudioFiles.length) {
 | 
				
			||||||
          // Single Track Audiobooks
 | 
					          // Single Track Audiobooks
 | 
				
			||||||
          if (mediaScanFileInodes.length + existingAudioFiles.length === 1) {
 | 
					          if (mediaScanFileInodes.length + existingAudioFiles.length === 1) {
 | 
				
			||||||
            var af = mediaScanResult.audioFiles[0]
 | 
					            const af = mediaScanResult.audioFiles[0]
 | 
				
			||||||
            af.index = 1
 | 
					            af.index = 1
 | 
				
			||||||
            libraryItem.media.addAudioFile(af)
 | 
					            libraryItem.media.addAudioFile(af)
 | 
				
			||||||
            hasUpdated = true
 | 
					            hasUpdated = true
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            var allAudioFiles = existingAudioFiles.concat(mediaScanResult.audioFiles)
 | 
					            const allAudioFiles = existingAudioFiles.concat(mediaScanResult.audioFiles)
 | 
				
			||||||
            this.runSmartTrackOrder(libraryItem, allAudioFiles)
 | 
					            this.runSmartTrackOrder(libraryItem, allAudioFiles)
 | 
				
			||||||
            hasUpdated = true
 | 
					            hasUpdated = true
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          // Only update metadata not index
 | 
					          // Only update metadata not index
 | 
				
			||||||
          mediaScanResult.audioFiles.forEach((af) => {
 | 
					          mediaScanResult.audioFiles.forEach((af) => {
 | 
				
			||||||
            var existingAF = libraryItem.media.findFileWithInode(af.ino)
 | 
					            const existingAF = libraryItem.media.findFileWithInode(af.ino)
 | 
				
			||||||
            if (existingAF) {
 | 
					            if (existingAF) {
 | 
				
			||||||
              af.index = existingAF.index
 | 
					              af.index = existingAF.index
 | 
				
			||||||
              if (existingAF.updateFromScan && existingAF.updateFromScan(af)) {
 | 
					              if (existingAF.updateFromScan && existingAF.updateFromScan(af)) {
 | 
				
			||||||
@ -266,11 +266,11 @@ class MediaFileScanner {
 | 
				
			|||||||
        if (hasUpdated) {
 | 
					        if (hasUpdated) {
 | 
				
			||||||
          libraryItem.media.rebuildTracks(preferOverdriveMediaMarker)
 | 
					          libraryItem.media.rebuildTracks(preferOverdriveMediaMarker)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } else { // Podcast Media Type
 | 
					      } else if (libraryItem.mediaType === 'podcast') { // Podcast Media Type
 | 
				
			||||||
        var existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino))
 | 
					        const existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (newAudioFiles.length) {
 | 
					        if (newAudioFiles.length) {
 | 
				
			||||||
          var newIndex = libraryItem.media.episodes.length + 1
 | 
					          let newIndex = libraryItem.media.episodes.length + 1
 | 
				
			||||||
          newAudioFiles.forEach((newAudioFile) => {
 | 
					          newAudioFiles.forEach((newAudioFile) => {
 | 
				
			||||||
            libraryItem.media.addNewEpisodeFromAudioFile(newAudioFile, newIndex++)
 | 
					            libraryItem.media.addNewEpisodeFromAudioFile(newAudioFile, newIndex++)
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
@ -280,11 +280,19 @@ class MediaFileScanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        // Update audio file metadata for audio files already there
 | 
					        // Update audio file metadata for audio files already there
 | 
				
			||||||
        existingAudioFiles.forEach((af) => {
 | 
					        existingAudioFiles.forEach((af) => {
 | 
				
			||||||
          var peAudioFile = libraryItem.media.findFileWithInode(af.ino)
 | 
					          const peAudioFile = libraryItem.media.findFileWithInode(af.ino)
 | 
				
			||||||
          if (peAudioFile.updateFromScan && peAudioFile.updateFromScan(af)) {
 | 
					          if (peAudioFile.updateFromScan && peAudioFile.updateFromScan(af)) {
 | 
				
			||||||
            hasUpdated = true
 | 
					            hasUpdated = true
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					      } else if (libraryItem.mediaType === 'music') { // Music
 | 
				
			||||||
 | 
					        // Only one audio file in library item
 | 
				
			||||||
 | 
					        if (newAudioFiles.length) { // New audio file
 | 
				
			||||||
 | 
					          libraryItem.media.setAudioFile(newAudioFiles[0])
 | 
				
			||||||
 | 
					          hasUpdated = true
 | 
				
			||||||
 | 
					        } else if (libraryItem.media.audioFile && libraryItem.media.audioFile.updateFromScan(mediaScanResult.audioFiles[0])) {
 | 
				
			||||||
 | 
					          hasUpdated = true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -47,7 +47,7 @@ class Scanner {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async scanLibraryItemById(libraryItemId) {
 | 
					  async scanLibraryItemById(libraryItemId) {
 | 
				
			||||||
    var libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
 | 
					    const libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
 | 
				
			||||||
    if (!libraryItem) {
 | 
					    if (!libraryItem) {
 | 
				
			||||||
      Logger.error(`[Scanner] Scan libraryItem by id not found ${libraryItemId}`)
 | 
					      Logger.error(`[Scanner] Scan libraryItem by id not found ${libraryItemId}`)
 | 
				
			||||||
      return ScanResult.NOTHING
 | 
					      return ScanResult.NOTHING
 | 
				
			||||||
@ -68,13 +68,13 @@ class Scanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  async scanLibraryItem(libraryMediaType, folder, libraryItem) {
 | 
					  async scanLibraryItem(libraryMediaType, folder, libraryItem) {
 | 
				
			||||||
    // TODO: Support for single media item
 | 
					    // TODO: Support for single media item
 | 
				
			||||||
    var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, libraryItem.path, false, this.db.serverSettings)
 | 
					    const libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, libraryItem.path, false, this.db.serverSettings)
 | 
				
			||||||
    if (!libraryItemData) {
 | 
					    if (!libraryItemData) {
 | 
				
			||||||
      return ScanResult.NOTHING
 | 
					      return ScanResult.NOTHING
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    var hasUpdated = false
 | 
					    let hasUpdated = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var checkRes = libraryItem.checkScanData(libraryItemData)
 | 
					    const checkRes = libraryItem.checkScanData(libraryItemData)
 | 
				
			||||||
    if (checkRes.updated) hasUpdated = true
 | 
					    if (checkRes.updated) hasUpdated = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Sync other files first so that local images are used as cover art
 | 
					    // Sync other files first so that local images are used as cover art
 | 
				
			||||||
@ -84,14 +84,14 @@ class Scanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Scan all audio files
 | 
					    // Scan all audio files
 | 
				
			||||||
    if (libraryItem.hasAudioFiles) {
 | 
					    if (libraryItem.hasAudioFiles) {
 | 
				
			||||||
      var libraryAudioFiles = libraryItem.libraryFiles.filter(lf => lf.fileType === 'audio')
 | 
					      const libraryAudioFiles = libraryItem.libraryFiles.filter(lf => lf.fileType === 'audio')
 | 
				
			||||||
      if (await MediaFileScanner.scanMediaFiles(libraryAudioFiles, libraryItemData, libraryItem, this.db.serverSettings.scannerPreferAudioMetadata, this.db.serverSettings.scannerPreferOverdriveMediaMarker)) {
 | 
					      if (await MediaFileScanner.scanMediaFiles(libraryAudioFiles, libraryItemData, libraryItem, this.db.serverSettings.scannerPreferAudioMetadata, this.db.serverSettings.scannerPreferOverdriveMediaMarker)) {
 | 
				
			||||||
        hasUpdated = true
 | 
					        hasUpdated = true
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Extract embedded cover art if cover is not already in directory
 | 
					      // Extract embedded cover art if cover is not already in directory
 | 
				
			||||||
      if (libraryItem.media.hasEmbeddedCoverArt && !libraryItem.media.coverPath) {
 | 
					      if (libraryItem.media.hasEmbeddedCoverArt && !libraryItem.media.coverPath) {
 | 
				
			||||||
        var coverPath = await this.coverManager.saveEmbeddedCoverArt(libraryItem)
 | 
					        const coverPath = await this.coverManager.saveEmbeddedCoverArt(libraryItem)
 | 
				
			||||||
        if (coverPath) {
 | 
					        if (coverPath) {
 | 
				
			||||||
          Logger.debug(`[Scanner] Saved embedded cover art "${coverPath}"`)
 | 
					          Logger.debug(`[Scanner] Saved embedded cover art "${coverPath}"`)
 | 
				
			||||||
          hasUpdated = true
 | 
					          hasUpdated = true
 | 
				
			||||||
@ -172,8 +172,8 @@ class Scanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Scan each library
 | 
					    // Scan each library
 | 
				
			||||||
    for (let i = 0; i < libraryScan.folders.length; i++) {
 | 
					    for (let i = 0; i < libraryScan.folders.length; i++) {
 | 
				
			||||||
      var folder = libraryScan.folders[i]
 | 
					      const folder = libraryScan.folders[i]
 | 
				
			||||||
      var itemDataFoundInFolder = await scanFolder(libraryScan.libraryMediaType, folder, this.db.serverSettings)
 | 
					      const itemDataFoundInFolder = await scanFolder(libraryScan.libraryMediaType, folder, this.db.serverSettings)
 | 
				
			||||||
      libraryScan.addLog(LogLevel.INFO, `${itemDataFoundInFolder.length} item data found in folder "${folder.fullPath}"`)
 | 
					      libraryScan.addLog(LogLevel.INFO, `${itemDataFoundInFolder.length} item data found in folder "${folder.fullPath}"`)
 | 
				
			||||||
      libraryItemDataFound = libraryItemDataFound.concat(itemDataFoundInFolder)
 | 
					      libraryItemDataFound = libraryItemDataFound.concat(itemDataFoundInFolder)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -196,16 +196,16 @@ class Scanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Check for existing & removed library items
 | 
					    // Check for existing & removed library items
 | 
				
			||||||
    for (let i = 0; i < libraryItemsInLibrary.length; i++) {
 | 
					    for (let i = 0; i < libraryItemsInLibrary.length; i++) {
 | 
				
			||||||
      var libraryItem = libraryItemsInLibrary[i]
 | 
					      const libraryItem = libraryItemsInLibrary[i]
 | 
				
			||||||
      // Find library item folder with matching inode or matching path
 | 
					      // Find library item folder with matching inode or matching path
 | 
				
			||||||
      var dataFound = libraryItemDataFound.find(lid => lid.ino === libraryItem.ino || comparePaths(lid.relPath, libraryItem.relPath))
 | 
					      const dataFound = libraryItemDataFound.find(lid => lid.ino === libraryItem.ino || comparePaths(lid.relPath, libraryItem.relPath))
 | 
				
			||||||
      if (!dataFound) {
 | 
					      if (!dataFound) {
 | 
				
			||||||
        libraryScan.addLog(LogLevel.WARN, `Library Item "${libraryItem.media.metadata.title}" is missing`)
 | 
					        libraryScan.addLog(LogLevel.WARN, `Library Item "${libraryItem.media.metadata.title}" is missing`)
 | 
				
			||||||
        libraryScan.resultsMissing++
 | 
					        libraryScan.resultsMissing++
 | 
				
			||||||
        libraryItem.setMissing()
 | 
					        libraryItem.setMissing()
 | 
				
			||||||
        itemsToUpdate.push(libraryItem)
 | 
					        itemsToUpdate.push(libraryItem)
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        var checkRes = libraryItem.checkScanData(dataFound)
 | 
					        const checkRes = libraryItem.checkScanData(dataFound)
 | 
				
			||||||
        if (checkRes.newLibraryFiles.length || libraryScan.scanOptions.forceRescan) { // Item has new files
 | 
					        if (checkRes.newLibraryFiles.length || libraryScan.scanOptions.forceRescan) { // Item has new files
 | 
				
			||||||
          checkRes.libraryItem = libraryItem
 | 
					          checkRes.libraryItem = libraryItem
 | 
				
			||||||
          checkRes.scanData = dataFound
 | 
					          checkRes.scanData = dataFound
 | 
				
			||||||
@ -244,15 +244,15 @@ class Scanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Potential NEW Library Items
 | 
					    // Potential NEW Library Items
 | 
				
			||||||
    for (let i = 0; i < libraryItemDataFound.length; i++) {
 | 
					    for (let i = 0; i < libraryItemDataFound.length; i++) {
 | 
				
			||||||
      var dataFound = libraryItemDataFound[i]
 | 
					      const dataFound = libraryItemDataFound[i]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var hasMediaFile = dataFound.libraryFiles.some(lf => lf.isMediaFile)
 | 
					      const hasMediaFile = dataFound.libraryFiles.some(lf => lf.isMediaFile)
 | 
				
			||||||
      if (!hasMediaFile) {
 | 
					      if (!hasMediaFile) {
 | 
				
			||||||
        libraryScan.addLog(LogLevel.WARN, `Item found "${libraryItemDataFound.path}" has no media files`)
 | 
					        libraryScan.addLog(LogLevel.WARN, `Item found "${libraryItemDataFound.path}" has no media files`)
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        if (global.ServerSettings.scannerUseSingleThreadedProber) {
 | 
					        if (global.ServerSettings.scannerUseSingleThreadedProber) {
 | 
				
			||||||
          // If this item will go over max size then push current chunk
 | 
					          // If this item will go over max size then push current chunk
 | 
				
			||||||
          var mediaFileSize = 0
 | 
					          let mediaFileSize = 0
 | 
				
			||||||
          dataFound.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video').forEach(lf => mediaFileSize += lf.metadata.size)
 | 
					          dataFound.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video').forEach(lf => mediaFileSize += lf.metadata.size)
 | 
				
			||||||
          if (mediaFileSize + newItemDataToScanSize > MaxSizePerChunk && newItemDataToScan.length > 0) {
 | 
					          if (mediaFileSize + newItemDataToScanSize > MaxSizePerChunk && newItemDataToScan.length > 0) {
 | 
				
			||||||
            newItemDataToScanChunks.push(newItemDataToScan)
 | 
					            newItemDataToScanChunks.push(newItemDataToScan)
 | 
				
			||||||
@ -277,8 +277,8 @@ class Scanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Library Items not requiring a scan but require a search for cover
 | 
					    // Library Items not requiring a scan but require a search for cover
 | 
				
			||||||
    for (let i = 0; i < itemsToFindCovers.length; i++) {
 | 
					    for (let i = 0; i < itemsToFindCovers.length; i++) {
 | 
				
			||||||
      var libraryItem = itemsToFindCovers[i]
 | 
					      const libraryItem = itemsToFindCovers[i]
 | 
				
			||||||
      var updatedCover = await this.searchForCover(libraryItem, libraryScan)
 | 
					      const updatedCover = await this.searchForCover(libraryItem, libraryScan)
 | 
				
			||||||
      libraryItem.media.updateLastCoverSearch(updatedCover)
 | 
					      libraryItem.media.updateLastCoverSearch(updatedCover)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -397,10 +397,10 @@ class Scanner {
 | 
				
			|||||||
    if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Scanning new library item "${libraryItemData.path}"`)
 | 
					    if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Scanning new library item "${libraryItemData.path}"`)
 | 
				
			||||||
    else Logger.debug(`[Scanner] Scanning new item "${libraryItemData.path}"`)
 | 
					    else Logger.debug(`[Scanner] Scanning new item "${libraryItemData.path}"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var libraryItem = new LibraryItem()
 | 
					    const libraryItem = new LibraryItem()
 | 
				
			||||||
    libraryItem.setData(libraryMediaType, libraryItemData)
 | 
					    libraryItem.setData(libraryMediaType, libraryItemData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
 | 
					    const mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
 | 
				
			||||||
    if (mediaFiles.length) {
 | 
					    if (mediaFiles.length) {
 | 
				
			||||||
      await MediaFileScanner.scanMediaFiles(mediaFiles, libraryItemData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan)
 | 
					      await MediaFileScanner.scanMediaFiles(mediaFiles, libraryItemData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -414,7 +414,7 @@ class Scanner {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Extract embedded cover art if cover is not already in directory
 | 
					    // Extract embedded cover art if cover is not already in directory
 | 
				
			||||||
    if (libraryItem.media.hasEmbeddedCoverArt && !libraryItem.media.coverPath) {
 | 
					    if (libraryItem.media.hasEmbeddedCoverArt && !libraryItem.media.coverPath) {
 | 
				
			||||||
      var coverPath = await this.coverManager.saveEmbeddedCoverArt(libraryItem)
 | 
					      const coverPath = await this.coverManager.saveEmbeddedCoverArt(libraryItem)
 | 
				
			||||||
      if (coverPath) {
 | 
					      if (coverPath) {
 | 
				
			||||||
        if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Saved embedded cover art "${coverPath}"`)
 | 
					        if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Saved embedded cover art "${coverPath}"`)
 | 
				
			||||||
        else Logger.debug(`[Scanner] Saved embedded cover art "${coverPath}"`)
 | 
					        else Logger.debug(`[Scanner] Saved embedded cover art "${coverPath}"`)
 | 
				
			||||||
@ -424,7 +424,7 @@ class Scanner {
 | 
				
			|||||||
    // Scan for cover if enabled and has no cover
 | 
					    // Scan for cover if enabled and has no cover
 | 
				
			||||||
    if (libraryMediaType === 'book') {
 | 
					    if (libraryMediaType === 'book') {
 | 
				
			||||||
      if (libraryItem && findCovers && !libraryItem.media.coverPath && libraryItem.media.shouldSearchForCover) {
 | 
					      if (libraryItem && findCovers && !libraryItem.media.coverPath && libraryItem.media.shouldSearchForCover) {
 | 
				
			||||||
        var updatedCover = await this.searchForCover(libraryItem, libraryScan)
 | 
					        const updatedCover = await this.searchForCover(libraryItem, libraryScan)
 | 
				
			||||||
        libraryItem.media.updateLastCoverSearch(updatedCover)
 | 
					        libraryItem.media.updateLastCoverSearch(updatedCover)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -636,19 +636,19 @@ class Scanner {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async scanPotentialNewLibraryItem(libraryMediaType, folder, fullPath, isSingleMediaItem = false) {
 | 
					  async scanPotentialNewLibraryItem(libraryMediaType, folder, fullPath, isSingleMediaItem = false) {
 | 
				
			||||||
    var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, isSingleMediaItem, this.db.serverSettings)
 | 
					    const libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, isSingleMediaItem, this.db.serverSettings)
 | 
				
			||||||
    if (!libraryItemData) return null
 | 
					    if (!libraryItemData) return null
 | 
				
			||||||
    var serverSettings = this.db.serverSettings
 | 
					    const serverSettings = this.db.serverSettings
 | 
				
			||||||
    return this.scanNewLibraryItem(libraryItemData, libraryMediaType, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers, serverSettings.scannerPreferOverdriveMediaMarker)
 | 
					    return this.scanNewLibraryItem(libraryItemData, libraryMediaType, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers, serverSettings.scannerPreferOverdriveMediaMarker)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async searchForCover(libraryItem, libraryScan = null) {
 | 
					  async searchForCover(libraryItem, libraryScan = null) {
 | 
				
			||||||
    var options = {
 | 
					    const options = {
 | 
				
			||||||
      titleDistance: 2,
 | 
					      titleDistance: 2,
 | 
				
			||||||
      authorDistance: 2
 | 
					      authorDistance: 2
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    var scannerCoverProvider = this.db.serverSettings.scannerCoverProvider
 | 
					    const scannerCoverProvider = this.db.serverSettings.scannerCoverProvider
 | 
				
			||||||
    var results = await this.bookFinder.findCovers(scannerCoverProvider, libraryItem.media.metadata.title, libraryItem.media.metadata.authorName, options)
 | 
					    const results = await this.bookFinder.findCovers(scannerCoverProvider, libraryItem.media.metadata.title, libraryItem.media.metadata.authorName, options)
 | 
				
			||||||
    if (results.length) {
 | 
					    if (results.length) {
 | 
				
			||||||
      if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${libraryItem.media.metadata.title}"`)
 | 
					      if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${libraryItem.media.metadata.title}"`)
 | 
				
			||||||
      else Logger.debug(`[Scanner] Found best cover for "${libraryItem.media.metadata.title}"`)
 | 
					      else Logger.debug(`[Scanner] Found best cover for "${libraryItem.media.metadata.title}"`)
 | 
				
			||||||
@ -657,7 +657,7 @@ class Scanner {
 | 
				
			|||||||
      for (let i = 0; i < results.length && i < 2; i++) {
 | 
					      for (let i = 0; i < results.length && i < 2; i++) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Downloads and updates the book cover
 | 
					        // Downloads and updates the book cover
 | 
				
			||||||
        var result = await this.coverManager.downloadCoverFromUrl(libraryItem, results[i])
 | 
					        const result = await this.coverManager.downloadCoverFromUrl(libraryItem, results[i])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (result.error) {
 | 
					        if (result.error) {
 | 
				
			||||||
          Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error)
 | 
					          Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error)
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@ const LibraryFile = require('../objects/files/LibraryFile')
 | 
				
			|||||||
function isMediaFile(mediaType, ext) {
 | 
					function isMediaFile(mediaType, ext) {
 | 
				
			||||||
  if (!ext) return false
 | 
					  if (!ext) return false
 | 
				
			||||||
  var extclean = ext.slice(1).toLowerCase()
 | 
					  var extclean = ext.slice(1).toLowerCase()
 | 
				
			||||||
  if (mediaType === 'podcast') return globals.SupportedAudioTypes.includes(extclean)
 | 
					  if (mediaType === 'podcast' || mediaType === 'music') return globals.SupportedAudioTypes.includes(extclean)
 | 
				
			||||||
  else if (mediaType === 'video') return globals.SupportedVideoTypes.includes(extclean)
 | 
					  else if (mediaType === 'video') return globals.SupportedVideoTypes.includes(extclean)
 | 
				
			||||||
  return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean)
 | 
					  return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -91,26 +91,39 @@ module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths
 | 
				
			|||||||
// Input: array of relative file items (see recurseFiles)
 | 
					// Input: array of relative file items (see recurseFiles)
 | 
				
			||||||
// Output: map of files grouped into potential libarary item dirs
 | 
					// Output: map of files grouped into potential libarary item dirs
 | 
				
			||||||
function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
 | 
					function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
 | 
				
			||||||
 | 
					  // Handle music where every audio file is a library item
 | 
				
			||||||
 | 
					  if (mediaType === 'music') {
 | 
				
			||||||
 | 
					    const audioFileGroup = {}
 | 
				
			||||||
 | 
					    fileItems.filter(i => isMediaFile(mediaType, i.extension)).forEach((item) => {
 | 
				
			||||||
 | 
					      if (!item.reldirpath) {
 | 
				
			||||||
 | 
					        audioFileGroup[item.name] = item.name
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        audioFileGroup[item.reldirpath] = [item.name]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    return audioFileGroup
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Step 1: Filter out non-book-media files in root dir (with depth of 0)
 | 
					  // Step 1: Filter out non-book-media files in root dir (with depth of 0)
 | 
				
			||||||
  var itemsFiltered = fileItems.filter(i => {
 | 
					  const itemsFiltered = fileItems.filter(i => {
 | 
				
			||||||
    return i.deep > 0 || ((mediaType === 'book' || mediaType === 'video') && isMediaFile(mediaType, i.extension))
 | 
					    return i.deep > 0 || ((mediaType === 'book' || mediaType === 'video' || mediaType === 'music') && isMediaFile(mediaType, i.extension))
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Step 2: Seperate media files and other files
 | 
					  // Step 2: Seperate media files and other files
 | 
				
			||||||
  //     - Directories without a media file will not be included
 | 
					  //     - Directories without a media file will not be included
 | 
				
			||||||
  var mediaFileItems = []
 | 
					  const mediaFileItems = []
 | 
				
			||||||
  var otherFileItems = []
 | 
					  const otherFileItems = []
 | 
				
			||||||
  itemsFiltered.forEach(item => {
 | 
					  itemsFiltered.forEach(item => {
 | 
				
			||||||
    if (isMediaFile(mediaType, item.extension)) mediaFileItems.push(item)
 | 
					    if (isMediaFile(mediaType, item.extension)) mediaFileItems.push(item)
 | 
				
			||||||
    else otherFileItems.push(item)
 | 
					    else otherFileItems.push(item)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Step 3: Group audio files in library items
 | 
					  // Step 3: Group audio files in library items
 | 
				
			||||||
  var libraryItemGroup = {}
 | 
					  const libraryItemGroup = {}
 | 
				
			||||||
  mediaFileItems.forEach((item) => {
 | 
					  mediaFileItems.forEach((item) => {
 | 
				
			||||||
    var dirparts = item.reldirpath.split('/').filter(p => !!p)
 | 
					    const dirparts = item.reldirpath.split('/').filter(p => !!p)
 | 
				
			||||||
    var numparts = dirparts.length
 | 
					    const numparts = dirparts.length
 | 
				
			||||||
    var _path = ''
 | 
					    let _path = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!dirparts.length) {
 | 
					    if (!dirparts.length) {
 | 
				
			||||||
      // Media file in root
 | 
					      // Media file in root
 | 
				
			||||||
@ -118,11 +131,11 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // Iterate over directories in path
 | 
					      // Iterate over directories in path
 | 
				
			||||||
      for (let i = 0; i < numparts; i++) {
 | 
					      for (let i = 0; i < numparts; i++) {
 | 
				
			||||||
        var dirpart = dirparts.shift()
 | 
					        const dirpart = dirparts.shift()
 | 
				
			||||||
        _path = Path.posix.join(_path, dirpart)
 | 
					        _path = Path.posix.join(_path, dirpart)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (libraryItemGroup[_path]) { // Directory already has files, add file
 | 
					        if (libraryItemGroup[_path]) { // Directory already has files, add file
 | 
				
			||||||
          var relpath = Path.posix.join(dirparts.join('/'), item.name)
 | 
					          const relpath = Path.posix.join(dirparts.join('/'), item.name)
 | 
				
			||||||
          libraryItemGroup[_path].push(relpath)
 | 
					          libraryItemGroup[_path].push(relpath)
 | 
				
			||||||
          return
 | 
					          return
 | 
				
			||||||
        } else if (!dirparts.length) { // This is the last directory, create group
 | 
					        } else if (!dirparts.length) { // This is the last directory, create group
 | 
				
			||||||
@ -138,16 +151,16 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Step 4: Add other files into library item groups
 | 
					  // Step 4: Add other files into library item groups
 | 
				
			||||||
  otherFileItems.forEach((item) => {
 | 
					  otherFileItems.forEach((item) => {
 | 
				
			||||||
    var dirparts = item.reldirpath.split('/')
 | 
					    const dirparts = item.reldirpath.split('/')
 | 
				
			||||||
    var numparts = dirparts.length
 | 
					    const numparts = dirparts.length
 | 
				
			||||||
    var _path = ''
 | 
					    let _path = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Iterate over directories in path
 | 
					    // Iterate over directories in path
 | 
				
			||||||
    for (let i = 0; i < numparts; i++) {
 | 
					    for (let i = 0; i < numparts; i++) {
 | 
				
			||||||
      var dirpart = dirparts.shift()
 | 
					      const dirpart = dirparts.shift()
 | 
				
			||||||
      _path = Path.posix.join(_path, dirpart)
 | 
					      _path = Path.posix.join(_path, dirpart)
 | 
				
			||||||
      if (libraryItemGroup[_path]) { // Directory is audiobook group
 | 
					      if (libraryItemGroup[_path]) { // Directory is audiobook group
 | 
				
			||||||
        var relpath = Path.posix.join(dirparts.join('/'), item.name)
 | 
					        const relpath = Path.posix.join(dirparts.join('/'), item.name)
 | 
				
			||||||
        libraryItemGroup[_path].push(relpath)
 | 
					        libraryItemGroup[_path].push(relpath)
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -158,8 +171,8 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function cleanFileObjects(libraryItemPath, 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)
 | 
					    const filePath = Path.posix.join(libraryItemPath, file)
 | 
				
			||||||
    var newLibraryFile = new LibraryFile()
 | 
					    const newLibraryFile = new LibraryFile()
 | 
				
			||||||
    await newLibraryFile.setDataFromPath(filePath, file)
 | 
					    await newLibraryFile.setDataFromPath(filePath, file)
 | 
				
			||||||
    return newLibraryFile
 | 
					    return newLibraryFile
 | 
				
			||||||
  }))
 | 
					  }))
 | 
				
			||||||
@ -167,27 +180,27 @@ function cleanFileObjects(libraryItemPath, files) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Scan folder
 | 
					// Scan folder
 | 
				
			||||||
async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
 | 
					async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
 | 
				
			||||||
  var folderPath = folder.fullPath.replace(/\\/g, '/')
 | 
					  const folderPath = folder.fullPath.replace(/\\/g, '/')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var pathExists = await fs.pathExists(folderPath)
 | 
					  const pathExists = await fs.pathExists(folderPath)
 | 
				
			||||||
  if (!pathExists) {
 | 
					  if (!pathExists) {
 | 
				
			||||||
    Logger.error(`[scandir] Invalid folder path does not exist "${folderPath}"`)
 | 
					    Logger.error(`[scandir] Invalid folder path does not exist "${folderPath}"`)
 | 
				
			||||||
    return []
 | 
					    return []
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var fileItems = await recurseFiles(folderPath)
 | 
					  const fileItems = await recurseFiles(folderPath)
 | 
				
			||||||
  var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems)
 | 
					  const libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!Object.keys(libraryItemGrouping).length) {
 | 
					  if (!Object.keys(libraryItemGrouping).length) {
 | 
				
			||||||
    Logger.error(`Root path has no media folders: ${folderPath}`)
 | 
					    Logger.error(`Root path has no media folders: ${folderPath}`)
 | 
				
			||||||
    return []
 | 
					    return []
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var items = []
 | 
					  const items = []
 | 
				
			||||||
  for (const libraryItemPath in libraryItemGrouping) {
 | 
					  for (const libraryItemPath in libraryItemGrouping) {
 | 
				
			||||||
    var isFile = false // item is not in a folder
 | 
					    let isFile = false // item is not in a folder
 | 
				
			||||||
    var libraryItemData = null
 | 
					    let libraryItemData = null
 | 
				
			||||||
    var fileObjs = []
 | 
					    let fileObjs = []
 | 
				
			||||||
    if (libraryItemPath === libraryItemGrouping[libraryItemPath]) {
 | 
					    if (libraryItemPath === libraryItemGrouping[libraryItemPath]) {
 | 
				
			||||||
      // Media file in root only get title
 | 
					      // Media file in root only get title
 | 
				
			||||||
      libraryItemData = {
 | 
					      libraryItemData = {
 | 
				
			||||||
@ -200,11 +213,11 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
 | 
				
			|||||||
      fileObjs = await cleanFileObjects(folderPath, [libraryItemPath])
 | 
					      fileObjs = await cleanFileObjects(folderPath, [libraryItemPath])
 | 
				
			||||||
      isFile = true
 | 
					      isFile = true
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings)
 | 
					      libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings, libraryItemGrouping[libraryItemPath])
 | 
				
			||||||
      fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath])
 | 
					      fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath])
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
 | 
					    const libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
 | 
				
			||||||
    items.push({
 | 
					    items.push({
 | 
				
			||||||
      folderId: folder.id,
 | 
					      folderId: folder.id,
 | 
				
			||||||
      libraryId: folder.libraryId,
 | 
					      libraryId: folder.libraryId,
 | 
				
			||||||
@ -318,21 +331,36 @@ function getSubtitle(folder) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function getPodcastDataFromDir(folderPath, relPath) {
 | 
					function getPodcastDataFromDir(folderPath, relPath) {
 | 
				
			||||||
  relPath = relPath.replace(/\\/g, '/')
 | 
					  relPath = relPath.replace(/\\/g, '/')
 | 
				
			||||||
  var splitDir = relPath.split('/')
 | 
					  const splitDir = relPath.split('/')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Audio files will always be in the directory named for the title
 | 
					  // Audio files will always be in the directory named for the title
 | 
				
			||||||
  var title = splitDir.pop()
 | 
					  const title = splitDir.pop()
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    mediaMetadata: {
 | 
					    mediaMetadata: {
 | 
				
			||||||
      title
 | 
					      title
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/..
 | 
					    relPath: relPath, // relative podcast path i.e. /Podcast Name/..
 | 
				
			||||||
    path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/..
 | 
					    path: Path.posix.join(folderPath, relPath) // i.e. /podcasts/Podcast Name/..
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings) {
 | 
					function getMusicDataFromDir(folderPath, relPath, fileNames) {
 | 
				
			||||||
  if (libraryMediaType === 'podcast') {
 | 
					  relPath = relPath.replace(/\\/g, '/')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const firstFileName = fileNames.length ? fileNames[0] : ''
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    mediaMetadata: {
 | 
				
			||||||
 | 
					      title: Path.basename(firstFileName, Path.extname(firstFileName))
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    relPath: relPath, // relative music audio file path i.e. /Some Folder/..
 | 
				
			||||||
 | 
					    path: Path.posix.join(folderPath, relPath) // i.e. /music/Some Folder/..
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings, fileNames) {
 | 
				
			||||||
 | 
					  if (libraryMediaType === 'music') {
 | 
				
			||||||
 | 
					    return getMusicDataFromDir(folderPath, relPath, fileNames)
 | 
				
			||||||
 | 
					  } else if (libraryMediaType === 'podcast') {
 | 
				
			||||||
    return getPodcastDataFromDir(folderPath, relPath)
 | 
					    return getPodcastDataFromDir(folderPath, relPath)
 | 
				
			||||||
  } else if (libraryMediaType === 'book') {
 | 
					  } else if (libraryMediaType === 'book') {
 | 
				
			||||||
    var parseSubtitle = !!serverSettings.scannerParseSubtitle
 | 
					    var parseSubtitle = !!serverSettings.scannerParseSubtitle
 | 
				
			||||||
@ -368,7 +396,8 @@ async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath,
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    fileItems = await recurseFiles(libraryItemPath)
 | 
					    fileItems = await recurseFiles(libraryItemPath)
 | 
				
			||||||
    libraryItemData = getDataFromMediaDir(libraryMediaType, folderFullPath, libraryItemDir, serverSettings)
 | 
					    const fileNames = fileItems.map(i => i.name)
 | 
				
			||||||
 | 
					    libraryItemData = getDataFromMediaDir(libraryMediaType, folderFullPath, libraryItemDir, serverSettings, fileNames)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var libraryItemDirStats = await getFileTimestampsWithIno(libraryItemData.path)
 | 
					  var libraryItemDirStats = await getFileTimestampsWithIno(libraryItemData.path)
 | 
				
			||||||
@ -389,8 +418,8 @@ async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath,
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for (let i = 0; i < fileItems.length; i++) {
 | 
					  for (let i = 0; i < fileItems.length; i++) {
 | 
				
			||||||
    var fileItem = fileItems[i]
 | 
					    const fileItem = fileItems[i]
 | 
				
			||||||
    var newLibraryFile = new LibraryFile()
 | 
					    const newLibraryFile = new LibraryFile()
 | 
				
			||||||
    // fileItem.path is the relative path
 | 
					    // fileItem.path is the relative path
 | 
				
			||||||
    await newLibraryFile.setDataFromPath(fileItem.fullpath, fileItem.path)
 | 
					    await newLibraryFile.setDataFromPath(fileItem.fullpath, fileItem.path)
 | 
				
			||||||
    libraryItem.libraryFiles.push(newLibraryFile)
 | 
					    libraryItem.libraryFiles.push(newLibraryFile)
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user