mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge pull request #3487 from mikiher/lazy-bookshelf-authors
Move authors to LazyBookshelf
This commit is contained in:
		
						commit
						a7ac82b023
					
				@ -24,7 +24,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="shelf.type === 'authors'" class="flex items-center">
 | 
			
		||||
          <template v-for="entity in shelf.entities">
 | 
			
		||||
            <cards-author-card :key="entity.id" :author="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" />
 | 
			
		||||
            <cards-author-card :key="entity.id" :authorMount="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" />
 | 
			
		||||
          </template>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="shelf.type === 'narrators'" class="flex items-center">
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,7 @@
 | 
			
		||||
        <p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
 | 
			
		||||
        <span v-else class="material-symbols text-lg"></span>
 | 
			
		||||
      </nuxt-link>
 | 
			
		||||
      <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'">
 | 
			
		||||
      <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/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>
 | 
			
		||||
        <svg v-else class="w-5 h-5" viewBox="0 0 24 24">
 | 
			
		||||
          <path
 | 
			
		||||
@ -62,7 +62,7 @@
 | 
			
		||||
        <ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
 | 
			
		||||
      </template>
 | 
			
		||||
      <!-- library & collections page -->
 | 
			
		||||
      <template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
 | 
			
		||||
      <template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome && !isAuthorsPage">
 | 
			
		||||
        <p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
 | 
			
		||||
 | 
			
		||||
        <div class="flex-grow hidden sm:inline-block" />
 | 
			
		||||
@ -92,12 +92,14 @@
 | 
			
		||||
        <ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
 | 
			
		||||
      </template>
 | 
			
		||||
      <!-- authors page -->
 | 
			
		||||
      <template v-else-if="page === 'authors'">
 | 
			
		||||
        <div class="flex-grow" />
 | 
			
		||||
        <ui-btn v-if="userCanUpdate && authors?.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
 | 
			
		||||
      <template v-else-if="isAuthorsPage">
 | 
			
		||||
        <p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
 | 
			
		||||
 | 
			
		||||
        <div class="flex-grow hidden sm:inline-block" />
 | 
			
		||||
        <ui-btn v-if="userCanUpdate && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
 | 
			
		||||
 | 
			
		||||
        <!-- author sort select -->
 | 
			
		||||
        <controls-sort-select v-if="authors?.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
 | 
			
		||||
        <controls-sort-select v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
 | 
			
		||||
      </template>
 | 
			
		||||
      <!-- home page -->
 | 
			
		||||
      <template v-else-if="isHome">
 | 
			
		||||
@ -117,11 +119,7 @@ export default {
 | 
			
		||||
      type: Object,
 | 
			
		||||
      default: () => null
 | 
			
		||||
    },
 | 
			
		||||
    searchQuery: String,
 | 
			
		||||
    authors: {
 | 
			
		||||
      type: Array,
 | 
			
		||||
      default: () => []
 | 
			
		||||
    }
 | 
			
		||||
    searchQuery: String
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
@ -268,7 +266,7 @@ export default {
 | 
			
		||||
      return this.$route.name === 'library-library-podcast-latest'
 | 
			
		||||
    },
 | 
			
		||||
    isAuthorsPage() {
 | 
			
		||||
      return this.$route.name === 'library-library-authors'
 | 
			
		||||
      return this.page === 'authors'
 | 
			
		||||
    },
 | 
			
		||||
    isAlbumsPage() {
 | 
			
		||||
      return this.page === 'albums'
 | 
			
		||||
@ -284,6 +282,7 @@ export default {
 | 
			
		||||
      if (this.isSeriesPage) return this.$strings.LabelSeries
 | 
			
		||||
      if (this.isCollectionsPage) return this.$strings.LabelCollections
 | 
			
		||||
      if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
 | 
			
		||||
      if (this.isAuthorsPage) return this.$strings.LabelAuthors
 | 
			
		||||
      return ''
 | 
			
		||||
    },
 | 
			
		||||
    seriesId() {
 | 
			
		||||
@ -479,36 +478,48 @@ export default {
 | 
			
		||||
          this.processingSeries = false
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
    async fetchAllAuthors() {
 | 
			
		||||
      // fetch all authors from the server, in the order that they are currently displayed
 | 
			
		||||
      const response = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/authors?sort=${this.settings.authorSortBy}&desc=${this.settings.authorSortDesc}`)
 | 
			
		||||
      return response.authors
 | 
			
		||||
    },
 | 
			
		||||
    async matchAllAuthors() {
 | 
			
		||||
      this.processingAuthors = true
 | 
			
		||||
 | 
			
		||||
      for (const author of this.authors) {
 | 
			
		||||
        const payload = {}
 | 
			
		||||
        if (author.asin) payload.asin = author.asin
 | 
			
		||||
        else payload.q = author.name
 | 
			
		||||
      try {
 | 
			
		||||
        const authors = await this.fetchAllAuthors()
 | 
			
		||||
 | 
			
		||||
        payload.region = 'us'
 | 
			
		||||
        if (this.libraryProvider.startsWith('audible.')) {
 | 
			
		||||
          payload.region = this.libraryProvider.split('.').pop() || 'us'
 | 
			
		||||
        for (const author of authors) {
 | 
			
		||||
          const payload = {}
 | 
			
		||||
          if (author.asin) payload.asin = author.asin
 | 
			
		||||
          else payload.q = author.name
 | 
			
		||||
 | 
			
		||||
          payload.region = 'us'
 | 
			
		||||
          if (this.libraryProvider.startsWith('audible.')) {
 | 
			
		||||
            payload.region = this.libraryProvider.split('.').pop() || 'us'
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          this.$eventBus.$emit(`searching-author-${author.id}`, true)
 | 
			
		||||
 | 
			
		||||
          var response = await this.$axios.$post(`/api/authors/${author.id}/match`, payload).catch((error) => {
 | 
			
		||||
            console.error('Failed', error)
 | 
			
		||||
            return null
 | 
			
		||||
          })
 | 
			
		||||
          if (!response) {
 | 
			
		||||
            console.error(`Author ${author.name} not found`)
 | 
			
		||||
            this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name]))
 | 
			
		||||
          } else if (response.updated) {
 | 
			
		||||
            if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
 | 
			
		||||
            else console.log(`Author ${response.author.name} was updated (no image found)`)
 | 
			
		||||
          } else {
 | 
			
		||||
            console.log(`No updates were made for Author ${response.author.name}`)
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          this.$eventBus.$emit(`searching-author-${author.id}`, false)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$eventBus.$emit(`searching-author-${author.id}`, true)
 | 
			
		||||
 | 
			
		||||
        var response = await this.$axios.$post(`/api/authors/${author.id}/match`, payload).catch((error) => {
 | 
			
		||||
          console.error('Failed', error)
 | 
			
		||||
          return null
 | 
			
		||||
        })
 | 
			
		||||
        if (!response) {
 | 
			
		||||
          console.error(`Author ${author.name} not found`)
 | 
			
		||||
          this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name]))
 | 
			
		||||
        } else if (response.updated) {
 | 
			
		||||
          if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
 | 
			
		||||
          else console.log(`Author ${response.author.name} was updated (no image found)`)
 | 
			
		||||
        } else {
 | 
			
		||||
          console.log(`No updates were made for Author ${response.author.name}`)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$eventBus.$emit(`searching-author-${author.id}`, false)
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('Failed to match all authors', error)
 | 
			
		||||
        this.$toast.error(this.$strings.ToastMatchAllAuthorsFailed)
 | 
			
		||||
      }
 | 
			
		||||
      this.processingAuthors = false
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -91,6 +91,7 @@ export default {
 | 
			
		||||
      if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
 | 
			
		||||
      if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
 | 
			
		||||
      if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
 | 
			
		||||
      if (this.page === 'authors') return this.$strings.MessageNoAuthors
 | 
			
		||||
      if (this.hasFilter) {
 | 
			
		||||
        if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
 | 
			
		||||
        else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
 | 
			
		||||
@ -111,6 +112,12 @@ export default {
 | 
			
		||||
    seriesFilterBy() {
 | 
			
		||||
      return this.$store.getters['user/getUserSetting']('seriesFilterBy')
 | 
			
		||||
    },
 | 
			
		||||
    authorSortBy() {
 | 
			
		||||
      return this.$store.getters['user/getUserSetting']('authorSortBy')
 | 
			
		||||
    },
 | 
			
		||||
    authorSortDesc() {
 | 
			
		||||
      return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
 | 
			
		||||
    },
 | 
			
		||||
    orderBy() {
 | 
			
		||||
      return this.$store.getters['user/getUserSetting']('orderBy')
 | 
			
		||||
    },
 | 
			
		||||
@ -217,6 +224,8 @@ export default {
 | 
			
		||||
        this.$store.commit('globals/setEditCollection', entity)
 | 
			
		||||
      } else if (this.entityName === 'playlists') {
 | 
			
		||||
        this.$store.commit('globals/setEditPlaylist', entity)
 | 
			
		||||
      } else if (this.entityName === 'authors') {
 | 
			
		||||
        this.$store.commit('globals/showEditAuthorModal', entity)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    clearSelectedEntities() {
 | 
			
		||||
@ -457,6 +466,9 @@ export default {
 | 
			
		||||
        if (this.collapseBookSeries) {
 | 
			
		||||
          searchParams.set('collapseseries', 1)
 | 
			
		||||
        }
 | 
			
		||||
      } else if (this.page === 'authors') {
 | 
			
		||||
        searchParams.set('sort', this.authorSortBy)
 | 
			
		||||
        searchParams.set('desc', this.authorSortDesc ? 1 : 0)
 | 
			
		||||
      } else {
 | 
			
		||||
        if (this.filterBy && this.filterBy !== 'all') {
 | 
			
		||||
          searchParams.set('filter', this.filterBy)
 | 
			
		||||
@ -601,6 +613,34 @@ export default {
 | 
			
		||||
        this.executeRebuild()
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    authorAdded(author) {
 | 
			
		||||
      if (this.entityName !== 'authors') return
 | 
			
		||||
      console.log(`[LazyBookshelf] authorAdded ${author.id}`, author)
 | 
			
		||||
      this.resetEntities()
 | 
			
		||||
    },
 | 
			
		||||
    authorUpdated(author) {
 | 
			
		||||
      if (this.entityName !== 'authors') return
 | 
			
		||||
      console.log(`[LazyBookshelf] authorUpdated ${author.id}`, author)
 | 
			
		||||
      const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id)
 | 
			
		||||
      if (indexOf >= 0) {
 | 
			
		||||
        this.entities[indexOf] = author
 | 
			
		||||
        if (this.entityComponentRefs[indexOf]) {
 | 
			
		||||
          this.entityComponentRefs[indexOf].setEntity(author)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    authorRemoved(author) {
 | 
			
		||||
      if (this.entityName !== 'authors') return
 | 
			
		||||
      console.log(`[LazyBookshelf] authorRemoved ${author.id}`, author)
 | 
			
		||||
      const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id)
 | 
			
		||||
      if (indexOf >= 0) {
 | 
			
		||||
        this.entities = this.entities.filter((ent) => ent.id !== author.id)
 | 
			
		||||
        this.totalEntities--
 | 
			
		||||
        this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
 | 
			
		||||
        this.executeRebuild()
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    shareOpen(mediaItemShare) {
 | 
			
		||||
      if (this.entityName === 'items' || this.entityName === 'series-books') {
 | 
			
		||||
        var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
 | 
			
		||||
@ -727,6 +767,9 @@ export default {
 | 
			
		||||
        this.$root.socket.on('playlist_added', this.playlistAdded)
 | 
			
		||||
        this.$root.socket.on('playlist_updated', this.playlistUpdated)
 | 
			
		||||
        this.$root.socket.on('playlist_removed', this.playlistRemoved)
 | 
			
		||||
        this.$root.socket.on('author_added', this.authorAdded)
 | 
			
		||||
        this.$root.socket.on('author_updated', this.authorUpdated)
 | 
			
		||||
        this.$root.socket.on('author_removed', this.authorRemoved)
 | 
			
		||||
        this.$root.socket.on('share_open', this.shareOpen)
 | 
			
		||||
        this.$root.socket.on('share_closed', this.shareClosed)
 | 
			
		||||
      } else {
 | 
			
		||||
@ -756,6 +799,9 @@ export default {
 | 
			
		||||
        this.$root.socket.off('playlist_added', this.playlistAdded)
 | 
			
		||||
        this.$root.socket.off('playlist_updated', this.playlistUpdated)
 | 
			
		||||
        this.$root.socket.off('playlist_removed', this.playlistRemoved)
 | 
			
		||||
        this.$root.socket.off('author_added', this.authorAdded)
 | 
			
		||||
        this.$root.socket.off('author_updated', this.authorUpdated)
 | 
			
		||||
        this.$root.socket.off('author_removed', this.authorRemoved)
 | 
			
		||||
        this.$root.socket.off('share_open', this.shareOpen)
 | 
			
		||||
        this.$root.socket.off('share_closed', this.shareClosed)
 | 
			
		||||
      } else {
 | 
			
		||||
 | 
			
		||||
@ -58,7 +58,7 @@
 | 
			
		||||
        <div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
 | 
			
		||||
      </nuxt-link>
 | 
			
		||||
 | 
			
		||||
      <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'">
 | 
			
		||||
      <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/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">
 | 
			
		||||
          <path
 | 
			
		||||
            fill="currentColor"
 | 
			
		||||
@ -180,7 +180,7 @@ export default {
 | 
			
		||||
      return this.$route.name === 'library-library-series-id' || this.paramId === 'series'
 | 
			
		||||
    },
 | 
			
		||||
    isAuthorsPage() {
 | 
			
		||||
      return this.$route.name === 'library-library-authors'
 | 
			
		||||
      return this.libraryBookshelfPage && this.paramId === 'authors'
 | 
			
		||||
    },
 | 
			
		||||
    isNarratorsPage() {
 | 
			
		||||
      return this.$route.name === 'library-library-narrators'
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
 | 
			
		||||
    <nuxt-link :to="`/author/${author.id}`">
 | 
			
		||||
  <div class="pb-3e" :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
 | 
			
		||||
    <nuxt-link :to="`/author/${author?.id}`">
 | 
			
		||||
      <div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
 | 
			
		||||
        <div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
 | 
			
		||||
          <!-- Image or placeholder -->
 | 
			
		||||
@ -40,7 +40,7 @@
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    author: {
 | 
			
		||||
    authorMount: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
      default: () => {}
 | 
			
		||||
    },
 | 
			
		||||
@ -57,7 +57,8 @@ export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      searching: false,
 | 
			
		||||
      isHovering: false
 | 
			
		||||
      isHovering: false,
 | 
			
		||||
      author: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
@ -68,34 +69,37 @@ export default {
 | 
			
		||||
      return this.height * this.sizeMultiplier
 | 
			
		||||
    },
 | 
			
		||||
    userToken() {
 | 
			
		||||
      return this.$store.getters['user/getToken']
 | 
			
		||||
      return this.store.getters['user/getToken']
 | 
			
		||||
    },
 | 
			
		||||
    _author() {
 | 
			
		||||
      return this.author || {}
 | 
			
		||||
    },
 | 
			
		||||
    authorId() {
 | 
			
		||||
      return this._author.id
 | 
			
		||||
      return this._author?.id || ''
 | 
			
		||||
    },
 | 
			
		||||
    name() {
 | 
			
		||||
      return this._author.name || ''
 | 
			
		||||
      return this._author?.name || ''
 | 
			
		||||
    },
 | 
			
		||||
    asin() {
 | 
			
		||||
      return this._author.asin || ''
 | 
			
		||||
      return this._author?.asin || ''
 | 
			
		||||
    },
 | 
			
		||||
    numBooks() {
 | 
			
		||||
      return this._author.numBooks || 0
 | 
			
		||||
      return this._author?.numBooks || 0
 | 
			
		||||
    },
 | 
			
		||||
    store() {
 | 
			
		||||
      return this.$store || this.$nuxt.$store
 | 
			
		||||
    },
 | 
			
		||||
    userCanUpdate() {
 | 
			
		||||
      return this.$store.getters['user/getUserCanUpdate']
 | 
			
		||||
      return this.store.getters['user/getUserCanUpdate']
 | 
			
		||||
    },
 | 
			
		||||
    currentLibraryId() {
 | 
			
		||||
      return this.$store.state.libraries.currentLibraryId
 | 
			
		||||
      return this.store.state.libraries.currentLibraryId
 | 
			
		||||
    },
 | 
			
		||||
    libraryProvider() {
 | 
			
		||||
      return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
 | 
			
		||||
      return this.store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
 | 
			
		||||
    },
 | 
			
		||||
    sizeMultiplier() {
 | 
			
		||||
      return this.$store.getters['user/getSizeMultiplier']
 | 
			
		||||
      return this.store.getters['user/getSizeMultiplier']
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
@ -132,13 +136,40 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    setSearching(isSearching) {
 | 
			
		||||
      this.searching = isSearching
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
    setEntity(author) {
 | 
			
		||||
      this.removeListeners()
 | 
			
		||||
      this.author = author
 | 
			
		||||
      this.addListeners()
 | 
			
		||||
    },
 | 
			
		||||
    addListeners() {
 | 
			
		||||
      if (this.author) {
 | 
			
		||||
        this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    removeListeners() {
 | 
			
		||||
      if (this.author) {
 | 
			
		||||
        this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    destroy() {
 | 
			
		||||
      // destroy the vue listeners, etc
 | 
			
		||||
      this.$destroy()
 | 
			
		||||
 | 
			
		||||
      // remove the element from the DOM
 | 
			
		||||
      if (this.$el && this.$el.parentNode) {
 | 
			
		||||
        this.$el.parentNode.removeChild(this.$el)
 | 
			
		||||
      } else if (this.$el && this.$el.remove) {
 | 
			
		||||
        this.$el.remove()
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    setSelectionMode(val) {}
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching)
 | 
			
		||||
    if (this.authorMount) this.setEntity(this.authorMount)
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
 | 
			
		||||
    this.removeListeners()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -65,7 +65,7 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
        authors: {
 | 
			
		||||
          component: 'cards-author-card',
 | 
			
		||||
          itemPropName: 'author',
 | 
			
		||||
          itemPropName: 'author-mount',
 | 
			
		||||
          itemIdFunc: (item) => item.id
 | 
			
		||||
        },
 | 
			
		||||
        narrators: {
 | 
			
		||||
 | 
			
		||||
@ -5,14 +5,14 @@ import Tooltip from '@/components/ui/Tooltip.vue'
 | 
			
		||||
import LoadingSpinner from '@/components/widgets/LoadingSpinner.vue'
 | 
			
		||||
 | 
			
		||||
describe('AuthorCard', () => {
 | 
			
		||||
  const author = {
 | 
			
		||||
  const authorMount = {
 | 
			
		||||
    id: 1,
 | 
			
		||||
    name: 'John Doe',
 | 
			
		||||
    numBooks: 5
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const propsData = {
 | 
			
		||||
    author,
 | 
			
		||||
    authorMount,
 | 
			
		||||
    nameBelow: false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ import LazySeriesCard from '@/components/cards/LazySeriesCard'
 | 
			
		||||
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
 | 
			
		||||
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
 | 
			
		||||
import LazyAlbumCard from '@/components/cards/LazyAlbumCard'
 | 
			
		||||
import AuthorCard from '@/components/cards/AuthorCard'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
@ -20,6 +21,7 @@ export default {
 | 
			
		||||
      if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
 | 
			
		||||
      if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
 | 
			
		||||
      if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard)
 | 
			
		||||
      if (this.entityName === 'authors') return Vue.extend(AuthorCard)
 | 
			
		||||
      return Vue.extend(LazyBookCard)
 | 
			
		||||
    },
 | 
			
		||||
    getComponentName() {
 | 
			
		||||
@ -27,6 +29,7 @@ export default {
 | 
			
		||||
      if (this.entityName === 'collections') return 'cards-lazy-collection-card'
 | 
			
		||||
      if (this.entityName === 'playlists') return 'cards-lazy-playlist-card'
 | 
			
		||||
      if (this.entityName === 'albums') return 'cards-lazy-album-card'
 | 
			
		||||
      if (this.entityName === 'authors') return 'cards-author-card'
 | 
			
		||||
      return 'cards-lazy-book-card'
 | 
			
		||||
    },
 | 
			
		||||
    async setCardSize() {
 | 
			
		||||
@ -46,13 +49,14 @@ export default {
 | 
			
		||||
        props.orderBy = this.seriesSortBy
 | 
			
		||||
      }
 | 
			
		||||
      const instance = new ComponentClass({
 | 
			
		||||
        propsData: props
 | 
			
		||||
        propsData: props,
 | 
			
		||||
        parent: this
 | 
			
		||||
      })
 | 
			
		||||
      instance.$mount()
 | 
			
		||||
      this.resizeObserver = new ResizeObserver((entries) => {
 | 
			
		||||
        for (let entry of entries) {
 | 
			
		||||
          this.cardWidth = entry.contentRect.width
 | 
			
		||||
          this.cardHeight = entry.contentRect.height
 | 
			
		||||
          this.cardWidth = entry.borderBoxSize[0].inlineSize
 | 
			
		||||
          this.cardHeight = entry.borderBoxSize[0].blockSize
 | 
			
		||||
          this.resizeObserver.disconnect()
 | 
			
		||||
          this.$refs.bookshelf.removeChild(instance.$el)
 | 
			
		||||
        }
 | 
			
		||||
@ -72,7 +76,7 @@ export default {
 | 
			
		||||
      })
 | 
			
		||||
      const timeAfter = performance.now()
 | 
			
		||||
    },
 | 
			
		||||
    async mountEntityCard(index) {
 | 
			
		||||
    mountEntityCard(index) {
 | 
			
		||||
      var shelf = Math.floor(index / this.entitiesPerShelf)
 | 
			
		||||
      var shelfEl = document.getElementById(`shelf-${shelf}`)
 | 
			
		||||
      if (!shelfEl) {
 | 
			
		||||
@ -114,6 +118,7 @@ export default {
 | 
			
		||||
      const _this = this
 | 
			
		||||
      const instance = new ComponentClass({
 | 
			
		||||
        propsData: props,
 | 
			
		||||
        parent: this,
 | 
			
		||||
        created() {
 | 
			
		||||
          this.$on('edit', (entity) => {
 | 
			
		||||
            if (_this.editEntity) _this.editEntity(entity)
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ export default {
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (!author) {
 | 
			
		||||
      return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`)
 | 
			
		||||
      return redirect(`/library/${store.state.libraries.currentLibraryId}/bookshelf/authors`)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (store.state.libraries.currentLibraryId !== author.libraryId || !store.state.libraries.filterData) {
 | 
			
		||||
@ -109,7 +109,7 @@ export default {
 | 
			
		||||
    authorRemoved(author) {
 | 
			
		||||
      if (author.id === this.author.id) {
 | 
			
		||||
        console.warn('Author was removed')
 | 
			
		||||
        this.$router.replace(`/library/${this.currentLibraryId}/authors`)
 | 
			
		||||
        this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/authors`)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -1,115 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="page" :class="streamLibraryItem ? 'streaming' : ''">
 | 
			
		||||
    <app-book-shelf-toolbar page="authors" is-home :authors="authors" />
 | 
			
		||||
    <div id="bookshelf" class="w-full h-full p-8e overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
 | 
			
		||||
      <!-- Cover size widget -->
 | 
			
		||||
      <widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
 | 
			
		||||
      <div class="flex flex-wrap justify-center">
 | 
			
		||||
        <template v-for="author in authorsSorted">
 | 
			
		||||
          <cards-author-card :key="author.id" :author="author" class="p-3e" @edit="editAuthor" />
 | 
			
		||||
        </template>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  async asyncData({ store, params, redirect, query, app }) {
 | 
			
		||||
    var libraryId = params.library
 | 
			
		||||
    var libraryData = await store.dispatch('libraries/fetch', libraryId)
 | 
			
		||||
    if (!libraryData) {
 | 
			
		||||
      return redirect('/oops?message=Library not found')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const library = libraryData.library
 | 
			
		||||
    if (library.mediaType === 'podcast') {
 | 
			
		||||
      return redirect(`/library/${libraryId}`)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      libraryId
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      loading: true,
 | 
			
		||||
      authors: []
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    sizeMultiplier() {
 | 
			
		||||
      return this.$store.getters['user/getSizeMultiplier']
 | 
			
		||||
    },
 | 
			
		||||
    streamLibraryItem() {
 | 
			
		||||
      return this.$store.state.streamLibraryItem
 | 
			
		||||
    },
 | 
			
		||||
    currentLibraryId() {
 | 
			
		||||
      return this.$store.state.libraries.currentLibraryId
 | 
			
		||||
    },
 | 
			
		||||
    selectedAuthor() {
 | 
			
		||||
      return this.$store.state.globals.selectedAuthor
 | 
			
		||||
    },
 | 
			
		||||
    authorSortBy() {
 | 
			
		||||
      return this.$store.getters['user/getUserSetting']('authorSortBy') || 'name'
 | 
			
		||||
    },
 | 
			
		||||
    authorSortDesc() {
 | 
			
		||||
      return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
 | 
			
		||||
    },
 | 
			
		||||
    authorsSorted() {
 | 
			
		||||
      const sortProp = this.authorSortBy
 | 
			
		||||
      const bDesc = this.authorSortDesc ? -1 : 1
 | 
			
		||||
      return this.authors.sort((a, b) => {
 | 
			
		||||
        if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') {
 | 
			
		||||
          // Fallback to name sort if equal
 | 
			
		||||
          if (a[sortProp] === b[sortProp]) return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) * bDesc
 | 
			
		||||
          return a[sortProp] > b[sortProp] ? bDesc : -bDesc
 | 
			
		||||
        }
 | 
			
		||||
        return a[sortProp]?.localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    async init() {
 | 
			
		||||
      this.authors = await this.$axios
 | 
			
		||||
        .$get(`/api/libraries/${this.currentLibraryId}/authors`)
 | 
			
		||||
        .then((response) => response.authors)
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Failed to load authors', error)
 | 
			
		||||
          return []
 | 
			
		||||
        })
 | 
			
		||||
      this.loading = false
 | 
			
		||||
    },
 | 
			
		||||
    authorAdded(author) {
 | 
			
		||||
      if (!this.authors.some((au) => au.id === author.id)) {
 | 
			
		||||
        this.authors.push(author)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    authorUpdated(author) {
 | 
			
		||||
      this.authors = this.authors.map((au) => {
 | 
			
		||||
        if (au.id === author.id) {
 | 
			
		||||
          return author
 | 
			
		||||
        }
 | 
			
		||||
        return au
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    authorRemoved(author) {
 | 
			
		||||
      this.authors = this.authors.filter((au) => au.id !== author.id)
 | 
			
		||||
    },
 | 
			
		||||
    editAuthor(author) {
 | 
			
		||||
      this.$store.commit('globals/showEditAuthorModal', author)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.init()
 | 
			
		||||
    this.$root.socket.on('author_added', this.authorAdded)
 | 
			
		||||
    this.$root.socket.on('author_updated', this.authorUpdated)
 | 
			
		||||
    this.$root.socket.on('author_removed', this.authorRemoved)
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    this.$root.socket.off('author_added', this.authorAdded)
 | 
			
		||||
    this.$root.socket.off('author_updated', this.authorUpdated)
 | 
			
		||||
    this.$root.socket.off('author_removed', this.authorRemoved)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@ -27,7 +27,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
    // Redirect podcast libraries
 | 
			
		||||
    const library = libraryData.library
 | 
			
		||||
    if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series')) {
 | 
			
		||||
    if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series' || params.id === 'authors')) {
 | 
			
		||||
      return redirect(`/library/${libraryId}`)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -920,6 +920,7 @@
 | 
			
		||||
  "ToastLibraryScanFailedToStart": "Failed to start scan",
 | 
			
		||||
  "ToastLibraryScanStarted": "Library scan started",
 | 
			
		||||
  "ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
 | 
			
		||||
  "ToastMatchAllAuthorsFailed": "Failed to match all authors",
 | 
			
		||||
  "ToastNameEmailRequired": "Name and email are required",
 | 
			
		||||
  "ToastNameRequired": "Name is required",
 | 
			
		||||
  "ToastNewUserCreatedFailed": "Failed to create account: \"{0}\"",
 | 
			
		||||
 | 
			
		||||
@ -493,8 +493,8 @@ class LibraryController {
 | 
			
		||||
    const payload = {
 | 
			
		||||
      results: [],
 | 
			
		||||
      total: undefined,
 | 
			
		||||
      limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
 | 
			
		||||
      page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
 | 
			
		||||
      limit: req.query.limit,
 | 
			
		||||
      page: req.query.page,
 | 
			
		||||
      sortBy: req.query.sort,
 | 
			
		||||
      sortDesc: req.query.desc === '1',
 | 
			
		||||
      filterBy: req.query.filter,
 | 
			
		||||
@ -504,13 +504,6 @@ class LibraryController {
 | 
			
		||||
      include: include.join(',')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!Number.isInteger(payload.limit) || payload.limit < 0) {
 | 
			
		||||
      return res.status(400).send('Invalid request. Limit must be a positive integer')
 | 
			
		||||
    }
 | 
			
		||||
    if (!Number.isInteger(payload.page) || payload.page < 0) {
 | 
			
		||||
      return res.status(400).send('Invalid request. Page must be a positive integer')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    payload.offset = payload.page * payload.limit
 | 
			
		||||
 | 
			
		||||
    // TODO: Temporary way of handling collapse sub-series. Either remove feature or handle through sql queries
 | 
			
		||||
@ -602,8 +595,8 @@ class LibraryController {
 | 
			
		||||
    const payload = {
 | 
			
		||||
      results: [],
 | 
			
		||||
      total: 0,
 | 
			
		||||
      limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
 | 
			
		||||
      page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
 | 
			
		||||
      limit: req.query.limit,
 | 
			
		||||
      page: req.query.page,
 | 
			
		||||
      sortBy: req.query.sort,
 | 
			
		||||
      sortDesc: req.query.desc === '1',
 | 
			
		||||
      filterBy: req.query.filter,
 | 
			
		||||
@ -674,8 +667,8 @@ class LibraryController {
 | 
			
		||||
    const payload = {
 | 
			
		||||
      results: [],
 | 
			
		||||
      total: 0,
 | 
			
		||||
      limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
 | 
			
		||||
      page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
 | 
			
		||||
      limit: req.query.limit,
 | 
			
		||||
      page: req.query.page,
 | 
			
		||||
      sortBy: req.query.sort,
 | 
			
		||||
      sortDesc: req.query.desc === '1',
 | 
			
		||||
      filterBy: req.query.filter,
 | 
			
		||||
@ -710,8 +703,8 @@ class LibraryController {
 | 
			
		||||
    const payload = {
 | 
			
		||||
      results: [],
 | 
			
		||||
      total: playlistsForUser.length,
 | 
			
		||||
      limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
 | 
			
		||||
      page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
 | 
			
		||||
      limit: req.query.limit,
 | 
			
		||||
      page: req.query.page
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (payload.limit) {
 | 
			
		||||
@ -742,7 +735,7 @@ class LibraryController {
 | 
			
		||||
   * @param {Response} res
 | 
			
		||||
   */
 | 
			
		||||
  async getUserPersonalizedShelves(req, res) {
 | 
			
		||||
    const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
 | 
			
		||||
    const limitPerShelf = req.query.limit || 10
 | 
			
		||||
    const include = (req.query.include || '')
 | 
			
		||||
      .split(',')
 | 
			
		||||
      .map((v) => v.trim().toLowerCase())
 | 
			
		||||
@ -815,7 +808,7 @@ class LibraryController {
 | 
			
		||||
      return res.status(400).send('Invalid request. Query param "q" must be a string')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
 | 
			
		||||
    const limit = req.query.limit || 12
 | 
			
		||||
    const query = asciiOnlyToLowerCase(req.query.q.trim())
 | 
			
		||||
 | 
			
		||||
    const matches = await libraryItemFilters.search(req.user, req.library, query, limit)
 | 
			
		||||
@ -873,8 +866,40 @@ class LibraryController {
 | 
			
		||||
   * @param {Response} res
 | 
			
		||||
   */
 | 
			
		||||
  async getAuthors(req, res) {
 | 
			
		||||
    const isPaginated = req.query.limit && !isNaN(req.query.limit) && !isNaN(req.query.page)
 | 
			
		||||
 | 
			
		||||
    const payload = {
 | 
			
		||||
      results: [],
 | 
			
		||||
      total: 0,
 | 
			
		||||
      limit: isPaginated ? Number(req.query.limit) : 0,
 | 
			
		||||
      page: isPaginated ? Number(req.query.page) : 0,
 | 
			
		||||
      sortBy: req.query.sort,
 | 
			
		||||
      sortDesc: req.query.desc === '1',
 | 
			
		||||
      filterBy: req.query.filter,
 | 
			
		||||
      minified: req.query.minified === '1',
 | 
			
		||||
      include: req.query.include
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // create order, limit and offset for pagination
 | 
			
		||||
    let offset = isPaginated ? payload.page * payload.limit : undefined
 | 
			
		||||
    let limit = isPaginated ? payload.limit : undefined
 | 
			
		||||
    let order = undefined
 | 
			
		||||
    const direction = payload.sortDesc ? 'DESC' : 'ASC'
 | 
			
		||||
    if (payload.sortBy === 'name') {
 | 
			
		||||
      order = [[Sequelize.literal('name COLLATE NOCASE'), direction]]
 | 
			
		||||
    } else if (payload.sortBy === 'lastFirst') {
 | 
			
		||||
      order = [[Sequelize.literal('lastFirst COLLATE NOCASE'), direction]]
 | 
			
		||||
    } else if (payload.sortBy === 'addedAt') {
 | 
			
		||||
      order = [['createdAt', direction]]
 | 
			
		||||
    } else if (payload.sortBy === 'updatedAt') {
 | 
			
		||||
      order = [['updatedAt', direction]]
 | 
			
		||||
    } else if (payload.sortBy === 'numBooks') {
 | 
			
		||||
      offset = undefined
 | 
			
		||||
      limit = undefined
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.user)
 | 
			
		||||
    const authors = await Database.authorModel.findAll({
 | 
			
		||||
    const { rows: authors, count } = await Database.authorModel.findAndCountAll({
 | 
			
		||||
      where: {
 | 
			
		||||
        libraryId: req.library.id
 | 
			
		||||
      },
 | 
			
		||||
@ -888,10 +913,13 @@ class LibraryController {
 | 
			
		||||
          attributes: []
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      order: [[Sequelize.literal('name COLLATE NOCASE'), 'ASC']]
 | 
			
		||||
      order: order,
 | 
			
		||||
      limit: limit,
 | 
			
		||||
      offset: offset,
 | 
			
		||||
      distinct: true
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const oldAuthors = []
 | 
			
		||||
    let oldAuthors = []
 | 
			
		||||
 | 
			
		||||
    for (const author of authors) {
 | 
			
		||||
      const oldAuthor = author.toOldJSONExpanded(author.books.length)
 | 
			
		||||
@ -899,9 +927,25 @@ class LibraryController {
 | 
			
		||||
      oldAuthors.push(oldAuthor)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.json({
 | 
			
		||||
      authors: oldAuthors
 | 
			
		||||
    })
 | 
			
		||||
    // numBooks sort is handled post-query
 | 
			
		||||
    if (payload.sortBy === 'numBooks') {
 | 
			
		||||
      oldAuthors.sort((a, b) => (payload.sortDesc ? b.numBooks - a.numBooks : a.numBooks - b.numBooks))
 | 
			
		||||
      if (isPaginated) {
 | 
			
		||||
        const startIndex = payload.page * payload.limit
 | 
			
		||||
        const endIndex = startIndex + payload.limit
 | 
			
		||||
        oldAuthors = oldAuthors.slice(startIndex, endIndex)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    payload.results = oldAuthors
 | 
			
		||||
    if (isPaginated) {
 | 
			
		||||
      payload.total = count
 | 
			
		||||
      res.json(payload)
 | 
			
		||||
    } else {
 | 
			
		||||
      res.json({
 | 
			
		||||
        authors: payload.results
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -1096,8 +1140,8 @@ class LibraryController {
 | 
			
		||||
 | 
			
		||||
    const payload = {
 | 
			
		||||
      episodes: [],
 | 
			
		||||
      limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
 | 
			
		||||
      page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
 | 
			
		||||
      limit: req.query.limit,
 | 
			
		||||
      page: req.query.page
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const offset = payload.page * payload.limit
 | 
			
		||||
@ -1200,6 +1244,17 @@ class LibraryController {
 | 
			
		||||
      return res.status(404).send('Library not found')
 | 
			
		||||
    }
 | 
			
		||||
    req.library = library
 | 
			
		||||
 | 
			
		||||
    // Ensure pagination query params are positive integers
 | 
			
		||||
    for (const queryKey of ['limit', 'page']) {
 | 
			
		||||
      if (req.query[queryKey] !== undefined) {
 | 
			
		||||
        req.query[queryKey] = !isNaN(req.query[queryKey]) ? Number(req.query[queryKey]) : 0
 | 
			
		||||
        if (!Number.isInteger(req.query[queryKey]) || req.query[queryKey] < 0) {
 | 
			
		||||
          return res.status(400).send(`Invalid request. ${queryKey} must be a positive integer`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    next()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user