-
{{ title }}
+
diff --git a/client/components/ui/LibrariesDropdown.vue b/client/components/ui/LibrariesDropdown.vue
index 74d4a674..757c9fd0 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 00000000..91a49b6d
--- /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 d695fee8..1276d285 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 00000000..e5e656c2
--- /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 e8b6ed7e..bbde887c 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 2d5ce86b..d42fa52d 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 8abc4996..401951a6 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 d1ba925c..301cdb82 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 2be9c6b2..9c28945c 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 e4e0ec6c..892251ed 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 bd168611..ec250d75 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 00000000..5fbbf6aa
--- /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