@@ -20,6 +20,10 @@
export default {
async asyncData({ store, params, redirect, query, app }) {
var libraryId = params.library
+ var library = await store.dispatch('libraries/fetch', libraryId)
+ if (!library) {
+ return redirect('/oops?message=Library not found')
+ }
var query = query.q
var results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${query}`).catch((error) => {
console.error('Failed to search library', error)
@@ -31,8 +35,8 @@ export default {
series: results && results.series.length ? results.series : null,
tags: results && results.tags.length ? results.tags : null
}
- console.log('SEARCH RESULTS', results)
return {
+ libraryId,
results,
query
}
@@ -40,6 +44,14 @@ export default {
data() {
return {}
},
+ watch: {
+ '$route.query'(newVal, oldVal) {
+ if (newVal && newVal.q && newVal.q !== this.query) {
+ this.query = newVal.q
+ this.search()
+ }
+ }
+ },
computed: {
streamAudiobook() {
return this.$store.state.streamAudiobook
@@ -49,6 +61,23 @@ export default {
}
},
methods: {
+ async search() {
+ var results = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${this.query}`).catch((error) => {
+ console.error('Failed to search library', error)
+ return null
+ })
+ this.results = {
+ audiobooks: results && results.audiobooks.length ? results.audiobooks : null,
+ authors: results && results.authors.length ? results.authors : null,
+ series: results && results.series.length ? results.series : null,
+ tags: results && results.tags.length ? results.tags : null
+ }
+ this.$nextTick(() => {
+ if (this.$refs.bookshelf) {
+ this.$refs.bookshelf.setShelvesFromSearch()
+ }
+ })
+ },
async back() {
var popped = await this.$store.dispatch('popRoute')
if (popped) this.$store.commit('setIsRoutingBack', true)
diff --git a/client/pages/library/_library/series/_id.vue b/client/pages/library/_library/series/_id.vue
new file mode 100644
index 00000000..ef513b2d
--- /dev/null
+++ b/client/pages/library/_library/series/_id.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js
index d8108f37..2983b58c 100644
--- a/client/plugins/init.client.js
+++ b/client/plugins/init.client.js
@@ -180,4 +180,5 @@ export {
}
export default ({ app }, inject) => {
app.$decode = decode
+ app.$encode = encode
}
\ No newline at end of file
diff --git a/client/store/audiobooks.js b/client/store/audiobooks.js
index bbde887c..95e9f7a9 100644
--- a/client/store/audiobooks.js
+++ b/client/store/audiobooks.js
@@ -1,159 +1,17 @@
-import { sort } from '@/assets/fastSort'
-import { decode } from '@/plugins/init.client'
-
const STANDARD_GENRES = ['Adventure', 'Autobiography', 'Biography', 'Childrens', 'Comedy', 'Crime', 'Dystopian', 'Fantasy', 'Fiction', 'Health', 'History', 'Horror', 'Mystery', 'New Adult', 'Nonfiction', 'Philosophy', 'Politics', 'Religion', 'Romance', 'Sci-Fi', 'Self-Help', 'Short Story', 'Technology', 'Thriller', 'True Crime', 'Western', 'Young Adult']
export const state = () => ({
audiobooks: [],
loadedLibraryId: '',
- lastLoad: 0,
listeners: [],
genres: [...STANDARD_GENRES],
tags: [],
series: [],
keywordFilter: null,
- selectedSeries: null,
- libraryPage: null,
- searchResults: {},
- searchResultAudiobooks: []
+ selectedSeries: null
})
export const getters = {
- getAudiobook: (state) => id => {
- return state.audiobooks.find(ab => ab.id === id)
- },
- getAudiobooksWithIssues: (state) => {
- return state.audiobooks.filter(ab => {
- return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
- })
- },
- getEntitiesShowing: (state, getters, rootState, rootGetters) => () => {
- if (!state.libraryPage) {
- return getters.getFiltered()
- } else if (state.libraryPage === 'search') {
- return state.searchResultAudiobooks
- } else if (state.libraryPage === 'series') {
- var series = getters.getSeriesGroups()
- if (state.selectedSeries) {
- var _series = series.find(__series => __series.name === state.selectedSeries)
- if (!_series) return []
- return _series.books || []
- }
- return series
- }
- return []
- },
- getFiltered: (state, getters, rootState, rootGetters) => () => {
- var filtered = state.audiobooks
- var settings = rootState.user.settings || {}
- var filterBy = settings.filterBy || ''
-
- 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 = 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 = rootGetters['user/getUserAudiobook'](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
- })
- }
-
- if (state.keywordFilter) {
- const keywordFilterKeys = ['title', 'subtitle', 'author', 'series', 'narrator']
- const keyworkFilter = state.keywordFilter.toLowerCase()
- return filtered.filter(ab => {
- if (!ab.book) return false
- return !!keywordFilterKeys.find(key => (ab.book[key] && ab.book[key].toLowerCase().includes(keyworkFilter)))
- })
- }
- return filtered
- },
- getFilteredAndSorted: (state, getters, rootState) => () => {
- var settings = rootState.user.settings
- var direction = settings.orderDesc ? 'desc' : 'asc'
-
- var filtered = getters.getFiltered()
-
- var orderByNumber = settings.orderBy === 'book.volumeNumber'
- return sort(filtered)[direction]((ab) => {
- // Supports dot notation strings i.e. "book.title"
- var value = settings.orderBy.split('.').reduce((a, b) => a[b], ab)
- if (orderByNumber && !isNaN(value)) return Number(value)
- return value
- })
- },
- getSeriesGroups: (state, getters, rootState) => () => {
- var series = {}
- state.audiobooks.forEach((audiobook) => {
- if (audiobook.book && audiobook.book.series) {
- if (series[audiobook.book.series]) {
- var bookLastUpdate = audiobook.book.lastUpdate
- if (bookLastUpdate > series[audiobook.book.series].lastUpdate) series[audiobook.book.series].lastUpdate = bookLastUpdate
- series[audiobook.book.series].books.push(audiobook)
- } else {
- series[audiobook.book.series] = {
- type: 'series',
- name: audiobook.book.series || '',
- books: [audiobook],
- lastUpdate: audiobook.book.lastUpdate
- }
- }
- }
- })
- var seriesArray = Object.values(series).map((_series) => {
- _series.books = sort(_series.books)['asc']((ab) => {
- return ab.book && ab.book.volumeNumber && !isNaN(ab.book.volumeNumber) ? Number(ab.book.volumeNumber) : null
- })
- return _series
- })
- if (state.keywordFilter) {
- const keywordFilter = state.keywordFilter.toLowerCase()
- return seriesArray.filter((_series) => _series.name.toLowerCase().includes(keywordFilter))
- }
- return seriesArray
- },
- getUniqueAuthors: (state) => {
- var abAuthors = []
- state.audiobooks.forEach((ab) => {
- if (ab.book && ab.book.authorFL) {
- abAuthors = abAuthors.concat(ab.book.authorFL.split(', '))
- }
- })
- return [...new Set(abAuthors)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
- },
- getUniqueNarrators: (state) => {
- var narrators = []
- state.audiobooks.forEach((ab) => {
- if (ab.book && ab.book.narratorFL) {
- narrators = narrators.concat(ab.book.narratorFL.split(', '))
- }
- })
- return [...new Set(narrators)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
- },
- getGenresUsed: (state) => {
- var _genres = []
- state.audiobooks.filter(ab => !!(ab.book && ab.book.genres)).forEach(ab => _genres = _genres.concat(ab.book.genres))
- 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
@@ -192,60 +50,16 @@ export const getters = {
}
export const actions = {
- // Return true if calling load
- load({ state, commit, rootState }) {
- if (!rootState.user || !rootState.user.user) {
- console.error('audiobooks/load - User not set')
- return false
- }
- var currentLibraryId = rootState.libraries.currentLibraryId
-
- if (currentLibraryId === state.loadedLibraryId) {
- // Don't load again if already loaded in the last 5 minutes
- var lastLoadDiff = Date.now() - state.lastLoad
- if (lastLoadDiff < 5 * 60 * 1000) {
- // Already up to date
- return false
- }
- }
- commit('setLoadedLibrary', currentLibraryId)
-
- this.$axios
- .$get(`/api/libraries/${currentLibraryId}/books`)
- .then((data) => {
- commit('set', data)
- commit('setLastLoad')
-
- })
- .catch((error) => {
- console.error('Failed', error)
- commit('set', [])
- })
- return true
- }
}
export const mutations = {
- setLoadedLibrary(state, val) {
- state.loadedLibraryId = val
- },
- setLastLoad(state) {
- state.lastLoad = Date.now()
- },
setKeywordFilter(state, val) {
state.keywordFilter = val
},
setSelectedSeries(state, val) {
state.selectedSeries = val
},
- setLibraryPage(state, val) {
- state.libraryPage = val
- },
- setSearchResults(state, val) {
- state.searchResults = val
- state.searchResultAudiobooks = val && val.audiobooks ? val.audiobooks.map(ab => ab.audiobook) : []
- },
set(state, audiobooks) {
// GENRES
var genres = [...state.genres]
@@ -382,5 +196,12 @@ export const mutations = {
},
removeListener(state, listenerId) {
state.listeners = state.listeners.filter(l => l.id !== listenerId)
+ },
+ audiobookUpdated(state, audiobook) {
+ state.listeners.forEach((listener) => {
+ if (!listener.audiobookId || listener.audiobookId === audiobook.id) {
+ listener.meth()
+ }
+ })
}
}
\ No newline at end of file
diff --git a/client/store/libraries.js b/client/store/libraries.js
index 401951a6..691a53e3 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -4,6 +4,7 @@ export const state = () => ({
listeners: [],
currentLibraryId: 'main',
folders: [],
+ issues: 0,
folderLastUpdate: 0,
filterData: null
})
@@ -53,7 +54,6 @@ export const actions = {
console.warn('Access not allowed to library')
return false
}
-
// var library = state.libraries.find(lib => lib.id === libraryId)
// if (library) {
// commit('setCurrentLibrary', libraryId)
@@ -65,7 +65,9 @@ export const actions = {
.then((data) => {
var library = data.library
var filterData = data.filterdata
+ var issues = data.issues || 0
commit('addUpdate', library)
+ commit('setLibraryIssues', issues)
commit('setLibraryFilterData', filterData)
commit('setCurrentLibrary', libraryId)
return data
@@ -129,6 +131,9 @@ export const mutations = {
setLastLoad(state) {
state.lastLoad = Date.now()
},
+ setLibraryIssues(state, val) {
+ state.issues = val
+ },
setCurrentLibrary(state, val) {
state.currentLibraryId = val
},
diff --git a/package.json b/package.json
index d88e53a5..ce192e07 100644
--- a/package.json
+++ b/package.json
@@ -51,4 +51,4 @@
"xml2js": "^0.4.23"
},
"devDependencies": {}
-}
+}
\ No newline at end of file
diff --git a/server/ApiController.js b/server/ApiController.js
index 9c28945c..cf7b998e 100644
--- a/server/ApiController.js
+++ b/server/ApiController.js
@@ -55,11 +55,13 @@ class ApiController {
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/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
+ this.router.get('/libraries/:id/series/: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.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
this.router.patch('/libraries/order', LibraryController.reorder.bind(this))
// TEMP: Support old syntax for mobile app
@@ -78,6 +80,7 @@ class ApiController {
this.router.delete('/books/all', BookController.deleteAll.bind(this))
this.router.post('/books/batch/delete', BookController.batchDelete.bind(this))
this.router.post('/books/batch/update', BookController.batchUpdate.bind(this))
+ this.router.post('/books/batch/get', BookController.batchGet.bind(this))
this.router.patch('/books/:id/tracks', BookController.updateTracks.bind(this))
this.router.get('/books/:id/stream', BookController.openStream.bind(this))
this.router.post('/books/:id/cover', BookController.uploadCover.bind(this))
@@ -493,105 +496,5 @@ class ApiController {
})
return listeningStats
}
-
-
- // 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
- // }
-
- // 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/Scanner.js b/server/Scanner.js
index cf2e057e..edd80114 100644
--- a/server/Scanner.js
+++ b/server/Scanner.js
@@ -149,13 +149,13 @@ class Scanner {
if (!audiobookData.audioFiles.length && !ebookFiles.length) {
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found - marking as incomplete`)
existingAudiobook.setLastScan(version)
- existingAudiobook.isIncomplete = true
+ existingAudiobook.isInvalid = true
await this.db.updateAudiobook(existingAudiobook)
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
return ScanResult.UPDATED
- } else if (existingAudiobook.isIncomplete) { // Was incomplete but now is not
+ } else if (existingAudiobook.isInvalid) { // Was incomplete but now is not
Logger.info(`[Scanner] "${existingAudiobook.title}" was incomplete but now has book files`)
- existingAudiobook.isIncomplete = false
+ existingAudiobook.isInvalid = false
}
// Check for audio files that were removed
@@ -241,7 +241,7 @@ class Scanner {
if (!existingAudiobook.tracks.length && !ebookFiles.length) {
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found after update - marking as incomplete`)
existingAudiobook.setLastScan(version)
- existingAudiobook.isIncomplete = true
+ existingAudiobook.isInvalid = true
await this.db.updateAudiobook(existingAudiobook)
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
return ScanResult.UPDATED
diff --git a/server/Server.js b/server/Server.js
index 373b7031..cb3db974 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -240,7 +240,6 @@ class Server {
methods: ["GET", "POST"]
}
})
-
this.io.on('connection', (socket) => {
this.clients[socket.id] = {
id: socket.id,
diff --git a/server/Watcher.js b/server/Watcher.js
index 18e81b93..c9fede87 100644
--- a/server/Watcher.js
+++ b/server/Watcher.js
@@ -124,8 +124,6 @@ class FolderWatcher extends EventEmitter {
}
addFileUpdate(libraryId, path, type) {
- console.log('add file update', libraryId, path, type)
- return
path = path.replace(/\\/g, '/')
if (this.pendingFilePaths.includes(path)) return
diff --git a/server/controllers/BookController.js b/server/controllers/BookController.js
index 18bd5f56..1ba8f4d3 100644
--- a/server/controllers/BookController.js
+++ b/server/controllers/BookController.js
@@ -42,7 +42,7 @@ class BookController {
if (hasUpdates) {
await this.db.updateAudiobook(audiobook)
}
- this.emitter('audiobook_updated', audiobook.toJSONMinified())
+ this.emitter('audiobook_updated', audiobook.toJSONExpanded())
res.json(audiobook.toJSON())
}
@@ -118,7 +118,7 @@ class BookController {
Logger.info(`[ApiController] ${audiobooksUpdated} Audiobooks have updates`)
for (let i = 0; i < audiobooks.length; i++) {
await this.db.updateAudiobook(audiobooks[i])
- this.emitter('audiobook_updated', audiobooks[i].toJSONMinified())
+ this.emitter('audiobook_updated', audiobooks[i].toJSONExpanded())
}
}
@@ -128,6 +128,16 @@ class BookController {
})
}
+ // POST: api/books/batch/get
+ async batchGet(req, res) {
+ var bookIds = req.body.books || []
+ if (!bookIds.length) {
+ return res.status(403).send('Invalid payload')
+ }
+ var audiobooks = this.db.audiobooks.filter(ab => bookIds.includes(ab.id)).map((ab) => ab.toJSONExpanded())
+ res.json(audiobooks)
+ }
+
// PATCH: api/books/:id/tracks
async updateTracks(req, res) {
if (!req.user.canUpdate) {
@@ -140,7 +150,7 @@ class BookController {
Logger.info(`Updating audiobook tracks called ${audiobook.id}`)
audiobook.updateAudioTracks(orderedFileData)
await this.db.updateAudiobook(audiobook)
- this.emitter('audiobook_updated', audiobook.toJSONMinified())
+ this.emitter('audiobook_updated', audiobook.toJSONExpanded())
res.json(audiobook.toJSON())
}
@@ -184,7 +194,7 @@ class BookController {
}
await this.db.updateAudiobook(audiobook)
- this.emitter('audiobook_updated', audiobook.toJSONMinified())
+ this.emitter('audiobook_updated', audiobook.toJSONExpanded())
res.json({
success: true,
cover: result.cover
@@ -205,7 +215,7 @@ class BookController {
if (updated) {
await this.db.updateAudiobook(audiobook)
- this.emitter('audiobook_updated', audiobook.toJSONMinified())
+ this.emitter('audiobook_updated', audiobook.toJSONExpanded())
}
if (updated) res.status(200).send('Cover updated successfully')
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 892251ed..71a5f1ef 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -35,6 +35,7 @@ class LibraryController {
var books = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
return res.json({
filterdata: libraryHelpers.getDistinctFilterData(books),
+ issues: libraryHelpers.getNumIssues(books),
library: req.library
})
}
@@ -85,13 +86,6 @@ class LibraryController {
getBooksForLibrary(req, res) {
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 => {
- // return ab.libraryId === libraryId && ab.isSearchMatch(req.query.q)
- // }).map(ab => ab.toJSONMinified())
- // } else {
- // audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId).map(ab => ab.toJSONMinified())
- // }
if (req.query.filter) {
audiobooks = libraryHelpers.getFiltered(audiobooks, req.query.filter, req.user)
@@ -154,13 +148,11 @@ class LibraryController {
audiobooks = audiobooks.slice(startIndex, startIndex + payload.limit)
}
payload.results = audiobooks.map(ab => ab.toJSONExpanded())
- console.log('returning books', audiobooks.length)
-
res.json(payload)
}
// api/libraries/:id/series
- async getSeriesForLibrary(req, res) {
+ async getAllSeriesForLibrary(req, res) {
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
var payload = {
@@ -182,11 +174,28 @@ class LibraryController {
}
payload.results = series
- console.log('returning series', series.length)
-
res.json(payload)
}
+ // GET: api/libraries/:id/series/:series
+ async getSeriesForLibrary(req, res) {
+ var series = libraryHelpers.decode(req.params.series)
+ if (!series) {
+ return res.status(403).send('Invalid series')
+ }
+ var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id && ab.book.series === series)
+ if (!audiobooks.length) {
+ return res.status(404).send('Series not found')
+ }
+ audiobooks = sort(audiobooks).asc(ab => {
+ return ab.book.volumeNumber
+ })
+ res.json({
+ results: audiobooks,
+ total: audiobooks.length
+ })
+ }
+
// api/libraries/:id/series
async getCollectionsForLibrary(req, res) {
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
@@ -210,8 +219,6 @@ class LibraryController {
}
payload.results = collections
- console.log('returning collections', collections.length)
-
res.json(payload)
}
@@ -300,7 +307,7 @@ class LibraryController {
if (!req.query.q) {
return res.status(400).send('No query string')
}
- var maxResults = req.query.max || 3
+ var maxResults = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
var bookMatches = []
var authorMatches = {}
@@ -350,13 +357,30 @@ class LibraryController {
})
}
})
-
- res.json({
+ var results = {
audiobooks: bookMatches.slice(0, maxResults),
tags: Object.values(tagMatches).slice(0, maxResults),
authors: Object.values(authorMatches).slice(0, maxResults),
series: Object.values(seriesMatches).slice(0, maxResults)
- })
+ }
+ res.json(results)
+ }
+
+ async stats(req, res) {
+ var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
+
+ var authorsWithCount = libraryHelpers.getAuthorsWithCount(audiobooksInLibrary)
+ var genresWithCount = libraryHelpers.getGenresWithCount(audiobooksInLibrary)
+ var stats = {
+ totalBooks: audiobooksInLibrary.length,
+ totalAuthors: Object.keys(authorsWithCount).length,
+ totalGenres: Object.keys(genresWithCount).length,
+ totalDuration: libraryHelpers.getAudiobooksTotalDuration(audiobooksInLibrary),
+ totalSize: libraryHelpers.getAudiobooksTotalSize(audiobooksInLibrary),
+ authorsWithCount,
+ genresWithCount
+ }
+ res.json(stats)
}
middleware(req, res, next) {
diff --git a/server/objects/Audiobook.js b/server/objects/Audiobook.js
index 6a03b1f9..f3b4a6f5 100644
--- a/server/objects/Audiobook.js
+++ b/server/objects/Audiobook.js
@@ -124,6 +124,14 @@ class Audiobook {
return this._audioFiles.filter(af => af.invalid).map(af => ({ filename: af.filename, error: af.error || 'Unknown Error' }))
}
+ get numMissingParts() {
+ return this.missingParts ? this.missingParts.length : 0
+ }
+
+ get numInvalidParts() {
+ return this.invalidParts ? this.invalidParts.length : 0
+ }
+
get _audioFiles() { return this.audioFiles || [] }
get _otherFiles() { return this.otherFiles || [] }
get _tracks() { return this.tracks || [] }
@@ -206,8 +214,8 @@ class Audiobook {
chapters: this.chapters || [],
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid,
- hasMissingParts: this.missingParts ? this.missingParts.length : 0,
- hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0
+ hasMissingParts: this.numMissingParts,
+ hasInvalidParts: this.numInvalidParts
}
}
@@ -238,8 +246,8 @@ class Audiobook {
chapters: this.chapters || [],
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid,
- hasMissingParts: this.missingParts ? this.missingParts.length : 0,
- hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0
+ hasMissingParts: this.numMissingParts,
+ hasInvalidParts: this.numInvalidParts
}
}
@@ -419,7 +427,6 @@ class Audiobook {
update(payload) {
var hasUpdates = false
-
if (payload.tags && payload.tags.join(',') !== this.tags.join(',')) {
this.tags = payload.tags
hasUpdates = true
diff --git a/server/objects/User.js b/server/objects/User.js
index bc0bdba5..1a6a8d10 100644
--- a/server/objects/User.js
+++ b/server/objects/User.js
@@ -215,7 +215,7 @@ class User {
}
var wasUpdated = this.audiobooks[audiobook.id].update(updatePayload)
if (wasUpdated) {
- Logger.debug(`[User] UserAudiobookData was updated ${JSON.stringify(this.audiobooks[audiobook.id])}`)
+ // Logger.debug(`[User] UserAudiobookData was updated ${JSON.stringify(this.audiobooks[audiobook.id])}`)
return this.audiobooks[audiobook.id]
}
return false
@@ -276,6 +276,7 @@ class User {
}
getAudiobookJSON(audiobookId) {
+ if (!this.audiobooks) return null
return this.audiobooks[audiobookId] ? this.audiobooks[audiobookId].toJSON() : null
}
diff --git a/server/objects/UserAudiobookData.js b/server/objects/UserAudiobookData.js
index 4043ad8e..ca11e742 100644
--- a/server/objects/UserAudiobookData.js
+++ b/server/objects/UserAudiobookData.js
@@ -85,7 +85,6 @@ class UserAudiobookData {
update(payload) {
var hasUpdates = false
- Logger.debug(`[UserAudiobookData] Update called ${JSON.stringify(payload)}`)
for (const key in payload) {
if (payload[key] !== this[key]) {
if (key === 'isRead') {
diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js
index 5fbbf6aa..4ea7252b 100644
--- a/server/utils/libraryHelpers.js
+++ b/server/utils/libraryHelpers.js
@@ -33,7 +33,7 @@ module.exports = {
}
} else if (filterBy === 'issues') {
filtered = filtered.filter(ab => {
- return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
+ return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
})
}
@@ -82,6 +82,7 @@ module.exports = {
_series[audiobook.book.series] = {
id: audiobook.book.series,
name: audiobook.book.series,
+ type: 'series',
books: [audiobook.toJSONExpanded()]
}
} else {
@@ -102,16 +103,16 @@ module.exports = {
},
getBooksMostRecentlyRead(booksWithUserAb, limit) {
- var booksWithProgress = booksWithUserAb.filter((data) => data.userAudiobook && !data.userAudiobook.isRead)
+ var booksWithProgress = booksWithUserAb.filter((data) => data.userAudiobook && data.userAudiobook.progress > 0 && !data.userAudiobook.isRead)
booksWithProgress.sort((a, b) => {
return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
})
- return booksWithProgress.map(b => b.book).slice(0, limit)
+ return booksWithProgress.map(b => b.book.toJSONExpanded()).slice(0, limit)
},
getBooksMostRecentlyAdded(books, limit) {
var booksSortedByAddedAt = sort(books).desc(book => book.addedAt)
- return booksSortedByAddedAt.slice(0, limit)
+ return booksSortedByAddedAt.map(b => b.toJSONExpanded()).slice(0, limit)
},
getBooksMostRecentlyFinished(booksWithUserAb, limit) {
@@ -119,7 +120,7 @@ module.exports = {
booksRead.sort((a, b) => {
return b.userAudiobook.finishedAt - a.userAudiobook.finishedAt
})
- return booksRead.map(b => b.book).slice(0, limit)
+ return booksRead.map(b => b.book.toJSONExpanded()).slice(0, limit)
},
getSeriesMostRecentlyAdded(series, limit) {
@@ -128,5 +129,59 @@ module.exports = {
return booksSortedByMostRecent[0].addedAt
})
return seriesSortedByAddedAt.slice(0, limit)
+ },
+
+ getGenresWithCount(audiobooks) {
+ var genresMap = {}
+ audiobooks.forEach((ab) => {
+ var genres = ab.book.genres || []
+ genres.forEach((genre) => {
+ if (genresMap[genre]) genresMap[genre].count++
+ else
+ genresMap[genre] = {
+ genre,
+ count: 1
+ }
+ })
+ })
+ return Object.values(genresMap).sort((a, b) => b.count - a.count)
+ },
+
+ getAuthorsWithCount(audiobooks) {
+ var authorsMap = {}
+ audiobooks.forEach((ab) => {
+ var authors = ab.book.authorFL ? ab.book.authorFL.split(', ') : []
+ authors.forEach((author) => {
+ if (authorsMap[author]) authorsMap[author].count++
+ else
+ authorsMap[author] = {
+ author,
+ count: 1
+ }
+ })
+ })
+ return Object.values(authorsMap).sort((a, b) => b.count - a.count)
+ },
+
+ getAudiobooksTotalDuration(audiobooks) {
+ var totalDuration = 0
+ audiobooks.forEach((ab) => {
+ totalDuration += ab.totalDuration
+ })
+ return totalDuration
+ },
+
+ getAudiobooksTotalSize(audiobooks) {
+ var totalSize = 0
+ audiobooks.forEach((ab) => {
+ totalSize += ab.totalSize
+ })
+ return totalSize
+ },
+
+ getNumIssues(books) {
+ return books.filter(ab => {
+ return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
+ }).length
}
}
\ No newline at end of file