-        
{{ title }}
+      
     
     
diff --git a/client/components/ui/LibrariesDropdown.vue b/client/components/ui/LibrariesDropdown.vue
index 74d4a674b..757c9fd0d 100644
--- a/client/components/ui/LibrariesDropdown.vue
+++ b/client/components/ui/LibrariesDropdown.vue
@@ -74,9 +74,18 @@ export default {
       this.showMenu = false
     },
     async updateLibrary(library) {
+      var currLibraryId = this.currentLibraryId
+
       this.disabled = true
       await this.$store.dispatch('libraries/fetch', library.id)
-      this.$router.push(`/library/${library.id}`)
+
+      if (this.$route.name.startsWith('library')) {
+        var newRoute = this.$route.path.replace(currLibraryId, library.id)
+        this.$router.push(newRoute)
+      } else {
+        this.$router.push(`/library/${library.id}`)
+      }
+
       this.disabled = false
     }
   },
diff --git a/client/mixins/bookshelfCardsHelpers.js b/client/mixins/bookshelfCardsHelpers.js
new file mode 100644
index 000000000..91a49b6da
--- /dev/null
+++ b/client/mixins/bookshelfCardsHelpers.js
@@ -0,0 +1,82 @@
+import Vue from 'vue'
+import LazyBookCard from '@/components/cards/LazyBookCard'
+import LazySeriesCard from '@/components/cards/LazySeriesCard'
+import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
+
+export default {
+  data() {
+    return {
+      cardsHelpers: {
+        mountEntityCard: this.mountEntityCard
+      }
+    }
+  },
+  methods: {
+    getComponentClass() {
+      if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
+      if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
+      return Vue.extend(LazyBookCard)
+    },
+    async mountEntityCard(index) {
+      var shelf = Math.floor(index / this.entitiesPerShelf)
+      var shelfEl = document.getElementById(`shelf-${shelf}`)
+      if (!shelfEl) {
+        console.error('invalid shelf', shelf, 'book index', index)
+        return
+      }
+      this.entityIndexesMounted.push(index)
+      if (this.entityComponentRefs[index]) {
+        var bookComponent = this.entityComponentRefs[index]
+        shelfEl.appendChild(bookComponent.$el)
+        if (this.isSelectionMode) {
+          bookComponent.setSelectionMode(true)
+          if (this.selectedAudiobooks.includes(bookComponent.audiobookId) || this.isSelectAll) {
+            bookComponent.selected = true
+          } else {
+            bookComponent.selected = false
+          }
+        } else {
+          bookComponent.setSelectionMode(false)
+        }
+        bookComponent.isHovering = false
+        return
+      }
+      var shelfOffsetY = 16
+      var row = index % this.entitiesPerShelf
+      var shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
+
+      var ComponentClass = this.getComponentClass()
+
+      var _this = this
+      var instance = new ComponentClass({
+        propsData: {
+          index: index,
+          width: this.entityWidth
+        },
+        created() {
+          this.$on('edit', (entity) => {
+            if (_this.editEntity) _this.editEntity(entity)
+          })
+          this.$on('select', (entity) => {
+            if (_this.selectEntity) _this.selectEntity(entity)
+          })
+        }
+      })
+      this.entityComponentRefs[index] = instance
+
+      instance.$mount()
+      instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
+      shelfEl.appendChild(instance.$el)
+
+      if (this.entities[index]) {
+        instance.setEntity(this.entities[index])
+      }
+      if (this.isSelectionMode) {
+        instance.setSelectionMode(true)
+        if (this.selectedAudiobooks.includes(instance.audiobookId) || this.isSelectAll) {
+          instance.selected = true
+        }
+      }
+    },
+  }
+}
\ No newline at end of file
diff --git a/client/pages/library/_library/bookshelf/_id.vue b/client/pages/library/_library/bookshelf/_id.vue
index d695fee87..1276d2853 100644
--- a/client/pages/library/_library/bookshelf/_id.vue
+++ b/client/pages/library/_library/bookshelf/_id.vue
@@ -4,7 +4,7 @@
       
       
     
diff --git a/client/pages/library/_library/search.vue b/client/pages/library/_library/search.vue
new file mode 100644
index 000000000..e5e656c2c
--- /dev/null
+++ b/client/pages/library/_library/search.vue
@@ -0,0 +1,62 @@
+
+  
+    
+      
+      
+        
+        
+        
+          
No Search results for "{{ query }}"
+          
+            Back
+          
+        
+      
+    
+  
 
+
+
+
diff --git a/client/store/audiobooks.js b/client/store/audiobooks.js
index e8b6ed7e9..bbde887c2 100644
--- a/client/store/audiobooks.js
+++ b/client/store/audiobooks.js
@@ -155,6 +155,7 @@ export const getters = {
     return [...new Set(_genres)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
   },
   getBookCoverSrc: (state, getters, rootState, rootGetters) => (bookItem, placeholder = '/book_placeholder.jpg') => {
+    if (!bookItem) return placeholder
     var book = bookItem.book
     if (!book || !book.cover || book.cover === placeholder) return placeholder
     var cover = book.cover
diff --git a/client/store/globals.js b/client/store/globals.js
index 2d5ce86b0..d42fa52d8 100644
--- a/client/store/globals.js
+++ b/client/store/globals.js
@@ -33,6 +33,5 @@ export const mutations = {
   },
   setShowBookshelfTextureModal(state, val) {
     state.showBookshelfTextureModal = val
-    console.log('shopw', val)
   }
 }
\ No newline at end of file
diff --git a/client/store/libraries.js b/client/store/libraries.js
index 8abc49960..401951a64 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -4,7 +4,8 @@ export const state = () => ({
   listeners: [],
   currentLibraryId: 'main',
   folders: [],
-  folderLastUpdate: 0
+  folderLastUpdate: 0,
+  filterData: null
 })
 
 export const getters = {
@@ -53,16 +54,19 @@ export const actions = {
       return false
     }
 
-    var library = state.libraries.find(lib => lib.id === libraryId)
-    if (library) {
-      commit('setCurrentLibrary', libraryId)
-      return library
-    }
+    // var library = state.libraries.find(lib => lib.id === libraryId)
+    // if (library) {
+    //   commit('setCurrentLibrary', libraryId)
+    //   return library
+    // }
 
     return this.$axios
-      .$get(`/api/libraries/${libraryId}`)
+      .$get(`/api/libraries/${libraryId}?include=filterdata`)
       .then((data) => {
-        commit('addUpdate', data)
+        var library = data.library
+        var filterData = data.filterdata
+        commit('addUpdate', library)
+        commit('setLibraryFilterData', filterData)
         commit('setCurrentLibrary', libraryId)
         return data
       })
@@ -97,7 +101,22 @@ export const actions = {
       })
     return true
   },
+  loadLibraryFilterData({ state, commit, rootState }) {
+    if (!rootState.user || !rootState.user.user) {
+      console.error('libraries/loadLibraryFilterData - User not set')
+      return false
+    }
 
+    this.$axios
+      .$get(`/api/libraries/${state.currentLibraryId}/filters`)
+      .then((data) => {
+        commit('setLibraryFilterData', data)
+      })
+      .catch((error) => {
+        console.error('Failed', error)
+        commit('setLibraryFilterData', null)
+      })
+  }
 }
 
 export const mutations = {
@@ -145,5 +164,8 @@ export const mutations = {
   },
   removeListener(state, listenerId) {
     state.listeners = state.listeners.filter(l => l.id !== listenerId)
+  },
+  setLibraryFilterData(state, filterData) {
+    state.filterData = filterData
   }
 }
\ No newline at end of file
diff --git a/client/store/user.js b/client/store/user.js
index d1ba925c2..301cdb827 100644
--- a/client/store/user.js
+++ b/client/store/user.js
@@ -25,7 +25,7 @@ export const getters = {
     return state.user && state.user.audiobooks ? state.user.audiobooks[audiobookId] || null : null
   },
   getUserSetting: (state) => (key) => {
-    return state.settings ? state.settings[key] || null : null
+    return state.settings ? state.settings[key] : null
   },
   getUserCanUpdate: (state) => {
     return state.user && state.user.permissions ? !!state.user.permissions.update : false
diff --git a/server/ApiController.js b/server/ApiController.js
index 2be9c6b24..9c28945ca 100644
--- a/server/ApiController.js
+++ b/server/ApiController.js
@@ -49,13 +49,17 @@ class ApiController {
     //
     this.router.post('/libraries', LibraryController.create.bind(this))
     this.router.get('/libraries', LibraryController.findAll.bind(this))
-    this.router.get('/libraries/:id', LibraryController.findOne.bind(this))
-    this.router.patch('/libraries/:id', LibraryController.update.bind(this))
-    this.router.delete('/libraries/:id', LibraryController.delete.bind(this))
+    this.router.get('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.findOne.bind(this))
+    this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this))
+    this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this))
 
-    this.router.get('/libraries/:id/books/all', LibraryController.getBooksForLibrary2.bind(this))
-    this.router.get('/libraries/:id/books', LibraryController.getBooksForLibrary.bind(this))
-    this.router.get('/libraries/:id/search', LibraryController.search.bind(this))
+    this.router.get('/libraries/:id/books/all', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary2.bind(this))
+    this.router.get('/libraries/:id/books', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary.bind(this))
+    this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this))
+    this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
+    this.router.get('/libraries/:id/categories', LibraryController.middleware.bind(this), LibraryController.getLibraryCategories.bind(this))
+    this.router.get('/libraries/:id/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this))
+    this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
     this.router.patch('/libraries/order', LibraryController.reorder.bind(this))
 
     // TEMP: Support old syntax for mobile app
@@ -491,43 +495,103 @@ class ApiController {
   }
 
 
-  decode(text) {
-    return Buffer.from(decodeURIComponent(text), 'base64').toString()
-  }
+  // decode(text) {
+  //   return Buffer.from(decodeURIComponent(text), 'base64').toString()
+  // }
 
-  getFiltered(audiobooks, filterBy, user) {
-    var filtered = audiobooks
+  // getFiltered(audiobooks, filterBy, user) {
+  //   var filtered = audiobooks
 
-    var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators']
-    var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
-    if (group) {
-      var filterVal = filterBy.replace(`${group}.`, '')
-      var filter = this.decode(filterVal)
-      if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
-      else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
-      else if (group === 'series') {
-        if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
-        else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
-      }
-      else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.authorFL && ab.book.authorFL.split(', ').includes(filter))
-      else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narratorFL && ab.book.narratorFL.split(', ').includes(filter))
-      else if (group === 'progress') {
-        filtered = filtered.filter(ab => {
-          var userAudiobook = user.getAudiobookJSON(ab.id)
-          var isRead = userAudiobook && userAudiobook.isRead
-          if (filter === 'Read' && isRead) return true
-          if (filter === 'Unread' && !isRead) return true
-          if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
-          return false
-        })
-      }
-    } else if (filterBy === 'issues') {
-      filtered = filtered.filter(ab => {
-        return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
-      })
-    }
+  //   var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators']
+  //   var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
+  //   if (group) {
+  //     var filterVal = filterBy.replace(`${group}.`, '')
+  //     var filter = this.decode(filterVal)
+  //     if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
+  //     else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
+  //     else if (group === 'series') {
+  //       if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
+  //       else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
+  //     }
+  //     else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.authorFL && ab.book.authorFL.split(', ').includes(filter))
+  //     else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narratorFL && ab.book.narratorFL.split(', ').includes(filter))
+  //     else if (group === 'progress') {
+  //       filtered = filtered.filter(ab => {
+  //         var userAudiobook = user.getAudiobookJSON(ab.id)
+  //         var isRead = userAudiobook && userAudiobook.isRead
+  //         if (filter === 'Read' && isRead) return true
+  //         if (filter === 'Unread' && !isRead) return true
+  //         if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
+  //         return false
+  //       })
+  //     }
+  //   } else if (filterBy === 'issues') {
+  //     filtered = filtered.filter(ab => {
+  //       return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
+  //     })
+  //   }
 
-    return filtered
-  }
+  //   return filtered
+  // }
+
+  // getDistinctFilterData(audiobooks) {
+  //   var data = {
+  //     authors: [],
+  //     genres: [],
+  //     tags: [],
+  //     series: [],
+  //     narrators: []
+  //   }
+  //   audiobooks.forEach((ab) => {
+  //     if (ab.book._authorsList.length) {
+  //       ab.book._authorsList.forEach((author) => {
+  //         if (author && !data.authors.includes(author)) data.authors.push(author)
+  //       })
+  //     }
+  //     if (ab.book._genres.length) {
+  //       ab.book._genres.forEach((genre) => {
+  //         if (genre && !data.genres.includes(genre)) data.genres.push(genre)
+  //       })
+  //     }
+  //     if (ab.tags.length) {
+  //       ab.tags.forEach((tag) => {
+  //         if (tag && !data.tags.includes(tag)) data.tags.push(tag)
+  //       })
+  //     }
+  //     if (ab.book._series && !data.series.includes(ab.book._series)) data.series.push(ab.book._series)
+  //     if (ab.book._narratorsList.length) {
+  //       ab.book._narratorsList.forEach((narrator) => {
+  //         if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
+  //       })
+  //     }
+  //   })
+  //   return data
+  // }
+
+  // getBooksMostRecentlyRead(user, books, limit) {
+  //   var booksWithProgress = books.map(book => {
+  //     return {
+  //       userAudiobook: user.getAudiobookJSON(book.id),
+  //       book
+  //     }
+  //   }).filter((data) => data.userAudiobook && !data.userAudiobook.isRead)
+  //   booksWithProgress.sort((a, b) => {
+  //     return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
+  //   })
+  //   return booksWithProgress.map(b => b.book).slice(0, limit)
+  // }
+
+  // getBooksMostRecentlyAdded(user, books, limit) {
+  //   var booksWithProgress = books.map(book => {
+  //     return {
+  //       userAudiobook: user.getAudiobookJSON(book.id),
+  //       book
+  //     }
+  //   }).filter((data) => data.userAudiobook && !data.userAudiobook.isRead)
+  //   booksWithProgress.sort((a, b) => {
+  //     return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
+  //   })
+  //   return booksWithProgress.map(b => b.book).slice(0, limit)
+  // }
 }
 module.exports = ApiController
\ No newline at end of file
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index e4e0ec6c4..892251ed4 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -1,6 +1,7 @@
 const Logger = require('../Logger')
 const Library = require('../objects/Library')
 const { sort } = require('fast-sort')
+const libraryHelpers = require('../utils/libraryHelpers')
 
 class LibraryController {
   constructor() { }
@@ -29,21 +30,19 @@ class LibraryController {
     res.json(this.db.libraries.map(lib => lib.toJSON()))
   }
 
-  findOne(req, res) {
-    if (!req.params.id) return res.status(500).send('Invalid id parameter')
-
-    var library = this.db.libraries.find(lib => lib.id === req.params.id)
-    if (!library) {
-      return res.status(404).send('Library not found')
+  async findOne(req, res) {
+    if (req.query.include && req.query.include === 'filterdata') {
+      var books = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
+      return res.json({
+        filterdata: libraryHelpers.getDistinctFilterData(books),
+        library: req.library
+      })
     }
-    return res.json(library.toJSON())
+    return res.json(req.library)
   }
 
   async update(req, res) {
-    var library = this.db.libraries.find(lib => lib.id === req.params.id)
-    if (!library) {
-      return res.status(404).send('Library not found')
-    }
+    var library = req.library
     var hasUpdates = library.update(req.body)
     if (hasUpdates) {
       // Update watcher
@@ -64,10 +63,7 @@ class LibraryController {
   }
 
   async delete(req, res) {
-    var library = this.db.libraries.find(lib => lib.id === req.params.id)
-    if (!library) {
-      return res.status(404).send('Library not found')
-    }
+    var library = req.library
 
     // Remove library watcher
     this.watcher.removeLibrary(library)
@@ -87,11 +83,7 @@ class LibraryController {
 
   // api/libraries/:id/books
   getBooksForLibrary(req, res) {
-    var libraryId = req.params.id
-    var library = this.db.libraries.find(lib => lib.id === libraryId)
-    if (!library) {
-      return res.status(400).send('Library does not exist')
-    }
+    var libraryId = req.library.id
     var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
     // if (req.query.q) {
     //   audiobooks = this.db.audiobooks.filter(ab => {
@@ -102,7 +94,7 @@ class LibraryController {
     // }
 
     if (req.query.filter) {
-      audiobooks = this.getFiltered(this.db.audiobooks, req.query.filter, req.user)
+      audiobooks = libraryHelpers.getFiltered(audiobooks, req.query.filter, req.user)
     }
 
 
@@ -126,13 +118,9 @@ class LibraryController {
     res.json(audiobooks)
   }
 
-  // api/libraries/:id/books/fs
+  // api/libraries/:id/books/all
   getBooksForLibrary2(req, res) {
-    var libraryId = req.params.id
-    var library = this.db.libraries.find(lib => lib.id === libraryId)
-    if (!library) {
-      return res.status(400).send('Library does not exist')
-    }
+    var libraryId = req.library.id
 
     var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
     var payload = {
@@ -146,7 +134,8 @@ class LibraryController {
     }
 
     if (payload.filterBy) {
-      audiobooks = this.getFiltered(this.db.audiobooks, payload.filterBy, req.user)
+      audiobooks = libraryHelpers.getFiltered(audiobooks, payload.filterBy, req.user)
+      payload.total = audiobooks.length
     }
 
     if (payload.sortBy) {
@@ -170,6 +159,110 @@ class LibraryController {
     res.json(payload)
   }
 
+  // api/libraries/:id/series
+  async getSeriesForLibrary(req, res) {
+    var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
+
+    var 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,
+      sortBy: req.query.sort,
+      sortDesc: req.query.desc === '1',
+      filterBy: req.query.filter
+    }
+
+    var series = libraryHelpers.getSeriesFromBooks(audiobooks)
+    payload.total = series.length
+
+    if (payload.limit) {
+      var startIndex = payload.page * payload.limit
+      series = series.slice(startIndex, startIndex + payload.limit)
+    }
+
+    payload.results = series
+    console.log('returning series', series.length)
+
+    res.json(payload)
+  }
+
+  // api/libraries/:id/series
+  async getCollectionsForLibrary(req, res) {
+    var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
+
+    var 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,
+      sortBy: req.query.sort,
+      sortDesc: req.query.desc === '1',
+      filterBy: req.query.filter
+    }
+
+    var collections = this.db.collections.filter(c => c.libraryId === req.library.id).map(c => c.toJSONExpanded(audiobooks))
+    payload.total = collections.length
+
+    if (payload.limit) {
+      var startIndex = payload.page * payload.limit
+      collections = collections.slice(startIndex, startIndex + payload.limit)
+    }
+
+    payload.results = collections
+    console.log('returning collections', collections.length)
+
+    res.json(payload)
+  }
+
+  // api/libraries/:id/books/filters
+  async getLibraryFilters(req, res) {
+    var library = req.library
+    var books = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
+    res.json(libraryHelpers.getDistinctFilterData(books))
+  }
+
+  // api/libraries/:id/books/categories
+  async getLibraryCategories(req, res) {
+    var library = req.library
+    var books = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
+    var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
+
+    var booksWithUserAb = libraryHelpers.getBooksWithUserAudiobook(req.user, books)
+    var series = libraryHelpers.getSeriesFromBooks(books)
+
+    var categories = [
+      {
+        id: 'continue-reading',
+        label: 'Continue Reading',
+        type: 'books',
+        entities: libraryHelpers.getBooksMostRecentlyRead(booksWithUserAb, limitPerShelf)
+      },
+      {
+        id: 'recently-added',
+        label: 'Recently Added',
+        type: 'books',
+        entities: libraryHelpers.getBooksMostRecentlyAdded(books, limitPerShelf)
+      },
+      {
+        id: 'read-again',
+        label: 'Read Again',
+        type: 'books',
+        entities: libraryHelpers.getBooksMostRecentlyFinished(booksWithUserAb, limitPerShelf)
+      },
+      {
+        id: 'recent-series',
+        label: 'Recent Series',
+        type: 'series',
+        entities: libraryHelpers.getSeriesMostRecentlyAdded(series, limitPerShelf)
+      }
+    ].filter(cats => { // Remove categories with no items
+      return cats.entities.length
+    })
+
+    res.json(categories)
+  }
+
   // PATCH: Change the order of libraries
   async reorder(req, res) {
     if (!req.user.isRoot) {
@@ -203,10 +296,7 @@ class LibraryController {
 
   // GET: Global library search
   search(req, res) {
-    var library = this.db.libraries.find(lib => lib.id === req.params.id)
-    if (!library) {
-      return res.status(404).send('Library not found')
-    }
+    var library = req.library
     if (!req.query.q) {
       return res.status(400).send('No query string')
     }
@@ -268,5 +358,14 @@ class LibraryController {
       series: Object.values(seriesMatches).slice(0, maxResults)
     })
   }
+
+  middleware(req, res, next) {
+    var library = this.db.libraries.find(lib => lib.id === req.params.id)
+    if (!library) {
+      return res.status(404).send('Library not found')
+    }
+    req.library = library
+    next()
+  }
 }
 module.exports = new LibraryController()
\ No newline at end of file
diff --git a/server/objects/Book.js b/server/objects/Book.js
index bd168611a..ec250d75e 100644
--- a/server/objects/Book.js
+++ b/server/objects/Book.js
@@ -11,6 +11,7 @@ class Book {
     this.authorLF = null
     this.authors = []
     this.narrator = null
+    this.narratorFL = null
     this.series = null
     this.volumeNumber = null
     this.publishYear = null
@@ -40,6 +41,7 @@ class Book {
   get _author() { return this.authorFL || '' }
   get _series() { return this.series || '' }
   get _authorsList() { return this._author.split(', ') }
+  get _narratorsList() { return this._narrator.split(', ') }
   get _genres() { return this.genres || [] }
 
   get shouldSearchForCover() {
diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js
new file mode 100644
index 000000000..5fbbf6aa7
--- /dev/null
+++ b/server/utils/libraryHelpers.js
@@ -0,0 +1,132 @@
+const { sort } = require('fast-sort')
+
+module.exports = {
+  decode(text) {
+    return Buffer.from(decodeURIComponent(text), 'base64').toString()
+  },
+
+  getFiltered(audiobooks, filterBy, user) {
+    var filtered = audiobooks
+
+    var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators']
+    var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
+    if (group) {
+      var filterVal = filterBy.replace(`${group}.`, '')
+      var filter = this.decode(filterVal)
+      if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
+      else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
+      else if (group === 'series') {
+        if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
+        else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
+      }
+      else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.authorFL && ab.book.authorFL.split(', ').includes(filter))
+      else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narratorFL && ab.book.narratorFL.split(', ').includes(filter))
+      else if (group === 'progress') {
+        filtered = filtered.filter(ab => {
+          var userAudiobook = user.getAudiobookJSON(ab.id)
+          var isRead = userAudiobook && userAudiobook.isRead
+          if (filter === 'Read' && isRead) return true
+          if (filter === 'Unread' && !isRead) return true
+          if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
+          return false
+        })
+      }
+    } else if (filterBy === 'issues') {
+      filtered = filtered.filter(ab => {
+        return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
+      })
+    }
+
+    return filtered
+  },
+
+  getDistinctFilterData(audiobooks) {
+    var data = {
+      authors: [],
+      genres: [],
+      tags: [],
+      series: [],
+      narrators: []
+    }
+    audiobooks.forEach((ab) => {
+      if (ab.book._authorsList.length) {
+        ab.book._authorsList.forEach((author) => {
+          if (author && !data.authors.includes(author)) data.authors.push(author)
+        })
+      }
+      if (ab.book._genres.length) {
+        ab.book._genres.forEach((genre) => {
+          if (genre && !data.genres.includes(genre)) data.genres.push(genre)
+        })
+      }
+      if (ab.tags.length) {
+        ab.tags.forEach((tag) => {
+          if (tag && !data.tags.includes(tag)) data.tags.push(tag)
+        })
+      }
+      if (ab.book._series && !data.series.includes(ab.book._series)) data.series.push(ab.book._series)
+      if (ab.book._narratorsList.length) {
+        ab.book._narratorsList.forEach((narrator) => {
+          if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
+        })
+      }
+    })
+    return data
+  },
+
+  getSeriesFromBooks(books) {
+    var _series = {}
+    books.forEach((audiobook) => {
+      if (audiobook.book.series) {
+        if (!_series[audiobook.book.series]) {
+          _series[audiobook.book.series] = {
+            id: audiobook.book.series,
+            name: audiobook.book.series,
+            books: [audiobook.toJSONExpanded()]
+          }
+        } else {
+          _series[audiobook.book.series].books.push(audiobook.toJSONExpanded())
+        }
+      }
+    })
+    return Object.values(_series)
+  },
+
+  getBooksWithUserAudiobook(user, books) {
+    return books.map(book => {
+      return {
+        userAudiobook: user.getAudiobookJSON(book.id),
+        book
+      }
+    })
+  },
+
+  getBooksMostRecentlyRead(booksWithUserAb, limit) {
+    var booksWithProgress = booksWithUserAb.filter((data) => data.userAudiobook && !data.userAudiobook.isRead)
+    booksWithProgress.sort((a, b) => {
+      return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
+    })
+    return booksWithProgress.map(b => b.book).slice(0, limit)
+  },
+
+  getBooksMostRecentlyAdded(books, limit) {
+    var booksSortedByAddedAt = sort(books).desc(book => book.addedAt)
+    return booksSortedByAddedAt.slice(0, limit)
+  },
+
+  getBooksMostRecentlyFinished(booksWithUserAb, limit) {
+    var booksRead = booksWithUserAb.filter((data) => data.userAudiobook && data.userAudiobook.isRead)
+    booksRead.sort((a, b) => {
+      return b.userAudiobook.finishedAt - a.userAudiobook.finishedAt
+    })
+    return booksRead.map(b => b.book).slice(0, limit)
+  },
+
+  getSeriesMostRecentlyAdded(series, limit) {
+    var seriesSortedByAddedAt = sort(series).desc(_series => {
+      var booksSortedByMostRecent = sort(_series.books).desc(b => b.addedAt)
+      return booksSortedByMostRecent[0].addedAt
+    })
+    return seriesSortedByAddedAt.slice(0, limit)
+  }
+}
\ No newline at end of file