diff --git a/client/assets/app.css b/client/assets/app.css index 25b8b451..3b61789a 100644 --- a/client/assets/app.css +++ b/client/assets/app.css @@ -4,6 +4,7 @@ :root { --bookshelf-texture-img: url(/textures/wood_default.jpg); + --bookshelf-divider-bg: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%); } .page { diff --git a/client/components/app/BookShelf.vue b/client/components/app/BookShelf.vue index c06661a7..f0e486b2 100644 --- a/client/components/app/BookShelf.vue +++ b/client/components/app/BookShelf.vue @@ -442,17 +442,6 @@ export default { this.init() this.initIO() - - setTimeout(() => { - var ids = {} - this.audiobooks.forEach((ab) => { - if (ids[ab.id]) { - console.error('FOUDN DUPLICATE ID', ids[ab.id], ab) - } else { - ids[ab.id] = ab - } - }) - }, 5000) }, beforeDestroy() { window.removeEventListener('resize', this.resize) diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue new file mode 100644 index 00000000..5a2f0b76 --- /dev/null +++ b/client/components/app/LazyBookshelf.vue @@ -0,0 +1,233 @@ + + + + + \ No newline at end of file diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue new file mode 100644 index 00000000..62529835 --- /dev/null +++ b/client/components/cards/LazyBookCard.vue @@ -0,0 +1,302 @@ + + + \ No newline at end of file diff --git a/client/components/covers/BookCover.vue b/client/components/covers/BookCover.vue index 82203c6e..1d934d12 100644 --- a/client/components/covers/BookCover.vue +++ b/client/components/covers/BookCover.vue @@ -4,8 +4,8 @@
- -
+ +

{{ title }}

@@ -67,6 +67,7 @@ export default { }, computed: { book() { + if (!this.audiobook) return {} return this.audiobook.book || {} }, title() { @@ -92,7 +93,9 @@ export default { return '/book_placeholder.jpg' }, fullCoverUrl() { - return this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook, this.placeholderUrl) + if (!this.audiobook) return null + var store = this.$store || this.$nuxt.$store + return store.getters['audiobooks/getBookCoverSrc'](this.audiobook, this.placeholderUrl) }, cover() { return this.book.cover || this.placeholderUrl diff --git a/client/pages/library/_library/bookshelf/_id.vue b/client/pages/library/_library/bookshelf/_id.vue index caaabfdc..d695fee8 100644 --- a/client/pages/library/_library/bookshelf/_id.vue +++ b/client/pages/library/_library/bookshelf/_id.vue @@ -4,7 +4,8 @@
diff --git a/package-lock.json b/package-lock.json index 4ec1c410..781a1988 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "1.6.29", + "version": "1.6.30", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -673,6 +673,11 @@ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.3.0.tgz", "integrity": "sha512-qJhfEgCnmteSeZAeuOKQ2WEIFTX5ajrzE0xS6gCOBCoRQcU+xEzQmgYQQTpzCcqUAAzTEtu4YEih4pnLfvNtew==" }, + "fast-sort": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-sort/-/fast-sort-3.1.1.tgz", + "integrity": "sha512-EA3PVIYj8uyyJc2Mma7GHjMrE74N/ClKkBj5gVUmY+8JePrc/ognCk4bhszVGYazu9Qk2aUTHnBF38QDSHcjkg==" + }, "file-type": { "version": "10.11.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", diff --git a/package.json b/package.json index d53d37af..d88e53a5 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "express": "^4.17.1", "express-fileupload": "^1.2.1", "express-rate-limit": "^5.3.0", + "fast-sort": "^3.1.1", "fluent-ffmpeg": "^2.1.2", "fs-extra": "^10.0.0", "image-type": "^4.1.0", @@ -50,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 4a835aee..2be9c6b2 100644 --- a/server/ApiController.js +++ b/server/ApiController.js @@ -53,6 +53,7 @@ class ApiController { this.router.patch('/libraries/:id', LibraryController.update.bind(this)) this.router.delete('/libraries/:id', 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.patch('/libraries/order', LibraryController.reorder.bind(this)) @@ -488,5 +489,45 @@ 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 + } } module.exports = ApiController \ No newline at end of file diff --git a/server/controllers/BookController.js b/server/controllers/BookController.js index 1b140519..18bd5f56 100644 --- a/server/controllers/BookController.js +++ b/server/controllers/BookController.js @@ -1,16 +1,11 @@ const Logger = require('../Logger') class BookController { - constructor(db, emitter, clientEmitter, streamManager, coverController) { - this.db = db - this.emitter = emitter - this.clientEmitter = clientEmitter - this.streamManager = streamManager - this.coverController = coverController - } + constructor() { } findAll(req, res) { var audiobooks = [] + if (req.query.q) { audiobooks = this.db.audiobooks.filter(ab => { return ab.isSearchMatch(req.query.q) diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index aa717525..e4e0ec6c 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -1,5 +1,6 @@ const Logger = require('../Logger') const Library = require('../objects/Library') +const { sort } = require('fast-sort') class LibraryController { constructor() { } @@ -91,18 +92,84 @@ class LibraryController { if (!library) { return res.status(400).send('Library does not exist') } + 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()) + // } - var audiobooks = [] - 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 = this.getFiltered(this.db.audiobooks, req.query.filter, req.user) + } + + + if (req.query.sort) { + var orderByNumber = req.query.sort === 'book.volumeNumber' + var direction = req.query.desc === '1' ? 'desc' : 'asc' + audiobooks = sort(audiobooks)[direction]((ab) => { + // Supports dot notation strings i.e. "book.title" + var value = req.query.sort.split('.').reduce((a, b) => a[b], ab) + if (orderByNumber && !isNaN(value)) return Number(value) + return value + }) + } + + if (req.query.limit && !isNaN(req.query.limit)) { + var page = req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0 + var limit = Number(req.query.limit) + var startIndex = page * limit + audiobooks = audiobooks.slice(startIndex, startIndex + limit) } res.json(audiobooks) } + // api/libraries/:id/books/fs + 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 audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId) + var payload = { + results: [], + total: audiobooks.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, + sortBy: req.query.sort, + sortDesc: req.query.desc === '1', + filterBy: req.query.filter + } + + if (payload.filterBy) { + audiobooks = this.getFiltered(this.db.audiobooks, payload.filterBy, req.user) + } + + if (payload.sortBy) { + var orderByNumber = payload.sortBy === 'book.volumeNumber' + var direction = payload.sortDesc ? 'desc' : 'asc' + audiobooks = sort(audiobooks)[direction]((ab) => { + // Supports dot notation strings i.e. "book.title" + var value = payload.sortBy.split('.').reduce((a, b) => a[b], ab) + if (orderByNumber && !isNaN(value)) return Number(value) + return value + }) + } + + if (payload.limit) { + var startIndex = payload.page * payload.limit + audiobooks = audiobooks.slice(startIndex, startIndex + payload.limit) + } + payload.results = audiobooks.map(ab => ab.toJSONExpanded()) + console.log('returning books', audiobooks.length) + + res.json(payload) + } + // PATCH: Change the order of libraries async reorder(req, res) { if (!req.user.isRoot) {