@@ -40,7 +40,7 @@
diff --git a/client/components/widgets/ItemSlider.vue b/client/components/widgets/ItemSlider.vue
index 15051fb1..fd14a202 100644
--- a/client/components/widgets/ItemSlider.vue
+++ b/client/components/widgets/ItemSlider.vue
@@ -65,7 +65,7 @@ export default {
},
authors: {
component: 'cards-author-card',
- itemPropName: 'author',
+ itemPropName: 'author-mount',
itemIdFunc: (item) => item.id
},
narrators: {
diff --git a/client/cypress/tests/components/cards/AuthorCard.cy.js b/client/cypress/tests/components/cards/AuthorCard.cy.js
index 420678ab..21c638e1 100644
--- a/client/cypress/tests/components/cards/AuthorCard.cy.js
+++ b/client/cypress/tests/components/cards/AuthorCard.cy.js
@@ -5,14 +5,14 @@ import Tooltip from '@/components/ui/Tooltip.vue'
import LoadingSpinner from '@/components/widgets/LoadingSpinner.vue'
describe('AuthorCard', () => {
- const author = {
+ const authorMount = {
id: 1,
name: 'John Doe',
numBooks: 5
}
const propsData = {
- author,
+ authorMount,
nameBelow: false
}
diff --git a/client/mixins/bookshelfCardsHelpers.js b/client/mixins/bookshelfCardsHelpers.js
index 31fa76ba..802b2cc8 100644
--- a/client/mixins/bookshelfCardsHelpers.js
+++ b/client/mixins/bookshelfCardsHelpers.js
@@ -4,6 +4,7 @@ import LazySeriesCard from '@/components/cards/LazySeriesCard'
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
import LazyAlbumCard from '@/components/cards/LazyAlbumCard'
+import AuthorCard from '@/components/cards/AuthorCard'
export default {
data() {
@@ -20,6 +21,7 @@ export default {
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard)
+ if (this.entityName === 'authors') return Vue.extend(AuthorCard)
return Vue.extend(LazyBookCard)
},
getComponentName() {
@@ -27,6 +29,7 @@ export default {
if (this.entityName === 'collections') return 'cards-lazy-collection-card'
if (this.entityName === 'playlists') return 'cards-lazy-playlist-card'
if (this.entityName === 'albums') return 'cards-lazy-album-card'
+ if (this.entityName === 'authors') return 'cards-author-card'
return 'cards-lazy-book-card'
},
async setCardSize() {
@@ -46,13 +49,14 @@ export default {
props.orderBy = this.seriesSortBy
}
const instance = new ComponentClass({
- propsData: props
+ propsData: props,
+ parent: this
})
instance.$mount()
this.resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
- this.cardWidth = entry.contentRect.width
- this.cardHeight = entry.contentRect.height
+ this.cardWidth = entry.borderBoxSize[0].inlineSize
+ this.cardHeight = entry.borderBoxSize[0].blockSize
this.resizeObserver.disconnect()
this.$refs.bookshelf.removeChild(instance.$el)
}
@@ -72,7 +76,7 @@ export default {
})
const timeAfter = performance.now()
},
- async mountEntityCard(index) {
+ mountEntityCard(index) {
var shelf = Math.floor(index / this.entitiesPerShelf)
var shelfEl = document.getElementById(`shelf-${shelf}`)
if (!shelfEl) {
@@ -114,6 +118,7 @@ export default {
const _this = this
const instance = new ComponentClass({
propsData: props,
+ parent: this,
created() {
this.$on('edit', (entity) => {
if (_this.editEntity) _this.editEntity(entity)
diff --git a/client/pages/author/_id.vue b/client/pages/author/_id.vue
index 0618f8a7..950312ab 100644
--- a/client/pages/author/_id.vue
+++ b/client/pages/author/_id.vue
@@ -53,7 +53,7 @@ export default {
})
if (!author) {
- return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`)
+ return redirect(`/library/${store.state.libraries.currentLibraryId}/bookshelf/authors`)
}
if (store.state.libraries.currentLibraryId !== author.libraryId || !store.state.libraries.filterData) {
@@ -109,7 +109,7 @@ export default {
authorRemoved(author) {
if (author.id === this.author.id) {
console.warn('Author was removed')
- this.$router.replace(`/library/${this.currentLibraryId}/authors`)
+ this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/authors`)
}
}
},
diff --git a/client/pages/library/_library/authors/index.vue b/client/pages/library/_library/authors/index.vue
deleted file mode 100644
index 9820fbce..00000000
--- a/client/pages/library/_library/authors/index.vue
+++ /dev/null
@@ -1,115 +0,0 @@
-
-
-
-
-
diff --git a/client/pages/library/_library/bookshelf/_id.vue b/client/pages/library/_library/bookshelf/_id.vue
index 7037a012..72cfba4a 100644
--- a/client/pages/library/_library/bookshelf/_id.vue
+++ b/client/pages/library/_library/bookshelf/_id.vue
@@ -27,7 +27,7 @@ export default {
// Redirect podcast libraries
const library = libraryData.library
- if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series')) {
+ if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series' || params.id === 'authors')) {
return redirect(`/library/${libraryId}`)
}
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index a6026ab4..862ed117 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -920,6 +920,7 @@
"ToastLibraryScanFailedToStart": "Failed to start scan",
"ToastLibraryScanStarted": "Library scan started",
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
+ "ToastMatchAllAuthorsFailed": "Failed to match all authors",
"ToastNameEmailRequired": "Name and email are required",
"ToastNameRequired": "Name is required",
"ToastNewUserCreatedFailed": "Failed to create account: \"{0}\"",
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 7705428b..d1e2becb 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -493,8 +493,8 @@ class LibraryController {
const payload = {
results: [],
total: undefined,
- limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
- page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
+ limit: req.query.limit,
+ page: req.query.page,
sortBy: req.query.sort,
sortDesc: req.query.desc === '1',
filterBy: req.query.filter,
@@ -504,13 +504,6 @@ class LibraryController {
include: include.join(',')
}
- if (!Number.isInteger(payload.limit) || payload.limit < 0) {
- return res.status(400).send('Invalid request. Limit must be a positive integer')
- }
- if (!Number.isInteger(payload.page) || payload.page < 0) {
- return res.status(400).send('Invalid request. Page must be a positive integer')
- }
-
payload.offset = payload.page * payload.limit
// TODO: Temporary way of handling collapse sub-series. Either remove feature or handle through sql queries
@@ -602,8 +595,8 @@ class LibraryController {
const payload = {
results: [],
total: 0,
- limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
- page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
+ limit: req.query.limit,
+ page: req.query.page,
sortBy: req.query.sort,
sortDesc: req.query.desc === '1',
filterBy: req.query.filter,
@@ -674,8 +667,8 @@ class LibraryController {
const payload = {
results: [],
total: 0,
- limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
- page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
+ limit: req.query.limit,
+ page: req.query.page,
sortBy: req.query.sort,
sortDesc: req.query.desc === '1',
filterBy: req.query.filter,
@@ -710,8 +703,8 @@ class LibraryController {
const payload = {
results: [],
total: playlistsForUser.length,
- limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
- page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
+ limit: req.query.limit,
+ page: req.query.page
}
if (payload.limit) {
@@ -742,7 +735,7 @@ class LibraryController {
* @param {Response} res
*/
async getUserPersonalizedShelves(req, res) {
- const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
+ const limitPerShelf = req.query.limit || 10
const include = (req.query.include || '')
.split(',')
.map((v) => v.trim().toLowerCase())
@@ -815,7 +808,7 @@ class LibraryController {
return res.status(400).send('Invalid request. Query param "q" must be a string')
}
- const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
+ const limit = req.query.limit || 12
const query = asciiOnlyToLowerCase(req.query.q.trim())
const matches = await libraryItemFilters.search(req.user, req.library, query, limit)
@@ -873,8 +866,40 @@ class LibraryController {
* @param {Response} res
*/
async getAuthors(req, res) {
+ const isPaginated = req.query.limit && !isNaN(req.query.limit) && !isNaN(req.query.page)
+
+ const payload = {
+ results: [],
+ total: 0,
+ limit: isPaginated ? Number(req.query.limit) : 0,
+ page: isPaginated ? Number(req.query.page) : 0,
+ sortBy: req.query.sort,
+ sortDesc: req.query.desc === '1',
+ filterBy: req.query.filter,
+ minified: req.query.minified === '1',
+ include: req.query.include
+ }
+
+ // create order, limit and offset for pagination
+ let offset = isPaginated ? payload.page * payload.limit : undefined
+ let limit = isPaginated ? payload.limit : undefined
+ let order = undefined
+ const direction = payload.sortDesc ? 'DESC' : 'ASC'
+ if (payload.sortBy === 'name') {
+ order = [[Sequelize.literal('name COLLATE NOCASE'), direction]]
+ } else if (payload.sortBy === 'lastFirst') {
+ order = [[Sequelize.literal('lastFirst COLLATE NOCASE'), direction]]
+ } else if (payload.sortBy === 'addedAt') {
+ order = [['createdAt', direction]]
+ } else if (payload.sortBy === 'updatedAt') {
+ order = [['updatedAt', direction]]
+ } else if (payload.sortBy === 'numBooks') {
+ offset = undefined
+ limit = undefined
+ }
+
const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.user)
- const authors = await Database.authorModel.findAll({
+ const { rows: authors, count } = await Database.authorModel.findAndCountAll({
where: {
libraryId: req.library.id
},
@@ -888,10 +913,13 @@ class LibraryController {
attributes: []
}
},
- order: [[Sequelize.literal('name COLLATE NOCASE'), 'ASC']]
+ order: order,
+ limit: limit,
+ offset: offset,
+ distinct: true
})
- const oldAuthors = []
+ let oldAuthors = []
for (const author of authors) {
const oldAuthor = author.toOldJSONExpanded(author.books.length)
@@ -899,9 +927,25 @@ class LibraryController {
oldAuthors.push(oldAuthor)
}
- res.json({
- authors: oldAuthors
- })
+ // numBooks sort is handled post-query
+ if (payload.sortBy === 'numBooks') {
+ oldAuthors.sort((a, b) => (payload.sortDesc ? b.numBooks - a.numBooks : a.numBooks - b.numBooks))
+ if (isPaginated) {
+ const startIndex = payload.page * payload.limit
+ const endIndex = startIndex + payload.limit
+ oldAuthors = oldAuthors.slice(startIndex, endIndex)
+ }
+ }
+
+ payload.results = oldAuthors
+ if (isPaginated) {
+ payload.total = count
+ res.json(payload)
+ } else {
+ res.json({
+ authors: payload.results
+ })
+ }
}
/**
@@ -1096,8 +1140,8 @@ class LibraryController {
const payload = {
episodes: [],
- limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
- page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
+ limit: req.query.limit,
+ page: req.query.page
}
const offset = payload.page * payload.limit
@@ -1200,6 +1244,17 @@ class LibraryController {
return res.status(404).send('Library not found')
}
req.library = library
+
+ // Ensure pagination query params are positive integers
+ for (const queryKey of ['limit', 'page']) {
+ if (req.query[queryKey] !== undefined) {
+ req.query[queryKey] = !isNaN(req.query[queryKey]) ? Number(req.query[queryKey]) : 0
+ if (!Number.isInteger(req.query[queryKey]) || req.query[queryKey] < 0) {
+ return res.status(400).send(`Invalid request. ${queryKey} must be a positive integer`)
+ }
+ }
+ }
+
next()
}
}