mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Update search endpoints to search db directly
This commit is contained in:
parent
b334d40998
commit
c77cead9ae
@ -36,7 +36,7 @@ export default {
|
||||
return this.narrator?.name || ''
|
||||
},
|
||||
numBooks() {
|
||||
return this.narrator?.books?.length || 0
|
||||
return this.narrator?.numBooks || this.narrator?.books?.length || 0
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
|
@ -103,7 +103,7 @@ export default {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
totalResults() {
|
||||
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length
|
||||
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length + this.narratorResults.length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -44,6 +44,21 @@ class Database {
|
||||
return this.models.series
|
||||
}
|
||||
|
||||
/** @type {typeof import('./models/Book')} */
|
||||
get bookModel() {
|
||||
return this.models.book
|
||||
}
|
||||
|
||||
/** @type {typeof import('./models/Podcast')} */
|
||||
get podcastModel() {
|
||||
return this.models.podcast
|
||||
}
|
||||
|
||||
/** @type {typeof import('./models/LibraryItem')} */
|
||||
get libraryItemModel() {
|
||||
return this.models.libraryItem
|
||||
}
|
||||
|
||||
async checkHasDb() {
|
||||
if (!await fs.pathExists(this.dbPath)) {
|
||||
Logger.info(`[Database] absdatabase.sqlite not found at ${this.dbPath}`)
|
||||
|
@ -790,80 +790,22 @@ class LibraryController {
|
||||
})
|
||||
}
|
||||
|
||||
// GET: Global library search
|
||||
search(req, res) {
|
||||
/**
|
||||
* GET: /api/libraries/:id/search
|
||||
* Search library items with query
|
||||
* ?q=search
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async search(req, res) {
|
||||
if (!req.query.q) {
|
||||
return res.status(400).send('No query string')
|
||||
}
|
||||
const libraryItems = req.libraryItems
|
||||
const maxResults = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||
const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||
const query = req.query.q.trim().toLowerCase()
|
||||
|
||||
const itemMatches = []
|
||||
const authorMatches = {}
|
||||
const narratorMatches = {}
|
||||
const seriesMatches = {}
|
||||
const tagMatches = {}
|
||||
|
||||
libraryItems.forEach((li) => {
|
||||
const queryResult = li.searchQuery(req.query.q)
|
||||
if (queryResult.matchKey) {
|
||||
itemMatches.push({
|
||||
libraryItem: li.toJSONExpanded(),
|
||||
matchKey: queryResult.matchKey,
|
||||
matchText: queryResult.matchText
|
||||
})
|
||||
}
|
||||
if (queryResult.series?.length) {
|
||||
queryResult.series.forEach((se) => {
|
||||
if (!seriesMatches[se.id]) {
|
||||
const _series = Database.series.find(_se => _se.id === se.id)
|
||||
if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] }
|
||||
} else {
|
||||
seriesMatches[se.id].books.push(li.toJSON())
|
||||
}
|
||||
})
|
||||
}
|
||||
if (queryResult.authors?.length) {
|
||||
queryResult.authors.forEach((au) => {
|
||||
if (!authorMatches[au.id]) {
|
||||
const _author = Database.authors.find(_au => _au.id === au.id)
|
||||
if (_author) {
|
||||
authorMatches[au.id] = _author.toJSON()
|
||||
authorMatches[au.id].numBooks = 1
|
||||
}
|
||||
} else {
|
||||
authorMatches[au.id].numBooks++
|
||||
}
|
||||
})
|
||||
}
|
||||
if (queryResult.tags?.length) {
|
||||
queryResult.tags.forEach((tag) => {
|
||||
if (!tagMatches[tag]) {
|
||||
tagMatches[tag] = { name: tag, books: [li.toJSON()] }
|
||||
} else {
|
||||
tagMatches[tag].books.push(li.toJSON())
|
||||
}
|
||||
})
|
||||
}
|
||||
if (queryResult.narrators?.length) {
|
||||
queryResult.narrators.forEach((narrator) => {
|
||||
if (!narratorMatches[narrator]) {
|
||||
narratorMatches[narrator] = { name: narrator, books: [li.toJSON()] }
|
||||
} else {
|
||||
narratorMatches[narrator].books.push(li.toJSON())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const itemKey = req.library.mediaType
|
||||
const results = {
|
||||
[itemKey]: itemMatches.slice(0, maxResults),
|
||||
tags: Object.values(tagMatches).slice(0, maxResults),
|
||||
authors: Object.values(authorMatches).slice(0, maxResults),
|
||||
series: Object.values(seriesMatches).slice(0, maxResults),
|
||||
narrators: Object.values(narratorMatches).slice(0, maxResults)
|
||||
}
|
||||
res.json(results)
|
||||
const matches = await libraryItemFilters.search(req.library, query, limit)
|
||||
res.json(matches)
|
||||
}
|
||||
|
||||
async stats(req, res) {
|
||||
|
@ -86,7 +86,7 @@ class ApiRouter {
|
||||
this.router.get('/libraries/:id/personalized2', LibraryController.middlewareNew.bind(this), LibraryController.getUserPersonalizedShelves.bind(this))
|
||||
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this))
|
||||
this.router.get('/libraries/:id/filterdata', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryFilterData.bind(this))
|
||||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||
this.router.get('/libraries/:id/search', LibraryController.middlewareNew.bind(this), LibraryController.search.bind(this))
|
||||
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
||||
this.router.get('/libraries/:id/authors', LibraryController.middlewareNew.bind(this), LibraryController.getAuthors.bind(this))
|
||||
this.router.get('/libraries/:id/narrators', LibraryController.middlewareNew.bind(this), LibraryController.getNarrators.bind(this))
|
||||
|
@ -1,5 +1,7 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const Database = require('../../Database')
|
||||
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
||||
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@ -127,7 +129,7 @@ module.exports = {
|
||||
/**
|
||||
* Get all library items that have narrators
|
||||
* @param {string[]} narrators
|
||||
* @returns {Promise<LibraryItem[]>}
|
||||
* @returns {Promise<import('../../models/LibraryItem')[]>}
|
||||
*/
|
||||
async getAllLibraryItemsWithNarrators(narrators) {
|
||||
const libraryItems = []
|
||||
@ -162,5 +164,20 @@ module.exports = {
|
||||
libraryItems.push(libraryItem)
|
||||
}
|
||||
return libraryItems
|
||||
},
|
||||
|
||||
/**
|
||||
* Search library items
|
||||
* @param {import('../../objects/Library')} oldLibrary
|
||||
* @param {string} query
|
||||
* @param {number} limit
|
||||
* @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[], podcast:object[]}}
|
||||
*/
|
||||
search(oldLibrary, query, limit) {
|
||||
if (oldLibrary.isBook) {
|
||||
return libraryItemsBookFilters.search(oldLibrary, query, limit, 0)
|
||||
} else {
|
||||
return libraryItemsPodcastFilters.search(oldLibrary, query, limit, 0)
|
||||
}
|
||||
}
|
||||
}
|
@ -918,12 +918,205 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get library items for series
|
||||
* @param {oldSeries} oldSeries
|
||||
* @param {[oldUser]} oldUser
|
||||
* @returns {Promise<oldLibraryItem[]>}
|
||||
* @param {import('../../objects/entities/Series')} oldSeries
|
||||
* @param {import('../../objects/user/User')} [oldUser]
|
||||
* @returns {Promise<import('../../objects/LibraryItem')[]>}
|
||||
*/
|
||||
async getLibraryItemsForSeries(oldSeries, oldUser) {
|
||||
const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, oldUser, 'series', oldSeries.id, null, null, false, [], null, null)
|
||||
return libraryItems.map(li => Database.models.libraryItem.getOldLibraryItem(li))
|
||||
},
|
||||
|
||||
/**
|
||||
* Search books, authors, series
|
||||
* @param {import('../../objects/Library')} oldLibrary
|
||||
* @param {string} query
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[]}}
|
||||
*/
|
||||
async search(oldLibrary, query, limit, offset) {
|
||||
// Search title, subtitle, asin, isbn
|
||||
const books = await Database.bookModel.findAll({
|
||||
where: {
|
||||
[Sequelize.Op.or]: [
|
||||
{
|
||||
title: {
|
||||
[Sequelize.Op.substring]: query
|
||||
}
|
||||
},
|
||||
{
|
||||
subtitle: {
|
||||
[Sequelize.Op.substring]: query
|
||||
}
|
||||
},
|
||||
{
|
||||
asin: {
|
||||
[Sequelize.Op.substring]: query
|
||||
}
|
||||
},
|
||||
{
|
||||
isbn: {
|
||||
[Sequelize.Op.substring]: query
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Database.libraryItemModel,
|
||||
where: {
|
||||
libraryId: oldLibrary.id
|
||||
}
|
||||
},
|
||||
{
|
||||
model: Database.models.bookSeries,
|
||||
include: {
|
||||
model: Database.seriesModel
|
||||
},
|
||||
separate: true
|
||||
},
|
||||
{
|
||||
model: Database.models.bookAuthor,
|
||||
include: {
|
||||
model: Database.authorModel
|
||||
},
|
||||
separate: true
|
||||
}
|
||||
],
|
||||
subQuery: false,
|
||||
distinct: true,
|
||||
limit,
|
||||
offset
|
||||
})
|
||||
|
||||
const itemMatches = []
|
||||
|
||||
for (const book of books) {
|
||||
const libraryItem = book.libraryItem
|
||||
delete book.libraryItem
|
||||
libraryItem.media = book
|
||||
|
||||
let matchText = null
|
||||
let matchKey = null
|
||||
for (const key of ['title', 'subtitle', 'asin', 'isbn']) {
|
||||
if (book[key]?.toLowerCase().includes(query)) {
|
||||
matchText = book[key]
|
||||
matchKey = key
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (matchKey) {
|
||||
itemMatches.push({
|
||||
matchText,
|
||||
matchKey,
|
||||
libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Search narrators
|
||||
const narratorMatches = []
|
||||
const [narratorResults] = await Database.sequelize.query(`SELECT value, count(*) AS numBooks FROM books b, libraryItems li, json_each(b.narrators) WHERE json_valid(b.narrators) AND json_each.value LIKE :query AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value LIMIT :limit OFFSET :offset;`, {
|
||||
replacements: {
|
||||
query: `%${query}%`,
|
||||
libraryId: oldLibrary.id,
|
||||
limit,
|
||||
offset
|
||||
},
|
||||
raw: true
|
||||
})
|
||||
for (const row of narratorResults) {
|
||||
narratorMatches.push({
|
||||
name: row.value,
|
||||
numBooks: row.numBooks
|
||||
})
|
||||
}
|
||||
|
||||
// Search tags
|
||||
const tagMatches = []
|
||||
const [tagResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM books b, libraryItems li, json_each(b.tags) WHERE json_valid(b.tags) AND json_each.value LIKE :query AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value LIMIT :limit OFFSET :offset;`, {
|
||||
replacements: {
|
||||
query: `%${query}%`,
|
||||
libraryId: oldLibrary.id,
|
||||
limit,
|
||||
offset
|
||||
},
|
||||
raw: true
|
||||
})
|
||||
for (const row of tagResults) {
|
||||
tagMatches.push({
|
||||
name: row.value,
|
||||
numItems: row.numItems
|
||||
})
|
||||
}
|
||||
|
||||
// Search series
|
||||
const allSeries = await Database.seriesModel.findAll({
|
||||
where: {
|
||||
name: {
|
||||
[Sequelize.Op.substring]: query
|
||||
},
|
||||
libraryId: oldLibrary.id
|
||||
},
|
||||
include: {
|
||||
separate: true,
|
||||
model: Database.models.bookSeries,
|
||||
include: {
|
||||
model: Database.bookModel,
|
||||
include: {
|
||||
model: Database.libraryItemModel
|
||||
}
|
||||
}
|
||||
},
|
||||
subQuery: false,
|
||||
distinct: true,
|
||||
limit,
|
||||
offset
|
||||
})
|
||||
const seriesMatches = []
|
||||
for (const series of allSeries) {
|
||||
const books = series.bookSeries.map((bs) => {
|
||||
const libraryItem = bs.book.libraryItem
|
||||
libraryItem.media = bs.book
|
||||
return Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSON()
|
||||
})
|
||||
seriesMatches.push({
|
||||
series: series.getOldSeries().toJSON(),
|
||||
books
|
||||
})
|
||||
}
|
||||
|
||||
// Search authors
|
||||
const authors = await Database.authorModel.findAll({
|
||||
where: {
|
||||
name: {
|
||||
[Sequelize.Op.substring]: query
|
||||
},
|
||||
libraryId: oldLibrary.id
|
||||
},
|
||||
attributes: {
|
||||
include: [
|
||||
[Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'numBooks']
|
||||
]
|
||||
},
|
||||
limit,
|
||||
offset
|
||||
})
|
||||
const authorMatches = []
|
||||
for (const author of authors) {
|
||||
const oldAuthor = author.getOldAuthor().toJSON()
|
||||
oldAuthor.numBooks = author.dataValues.numBooks
|
||||
authorMatches.push(oldAuthor)
|
||||
}
|
||||
|
||||
return {
|
||||
book: itemMatches,
|
||||
narrators: narratorMatches,
|
||||
tags: tagMatches,
|
||||
series: seriesMatches,
|
||||
authors: authorMatches
|
||||
}
|
||||
}
|
||||
}
|
@ -291,5 +291,104 @@ module.exports = {
|
||||
libraryItems,
|
||||
count
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Search podcasts
|
||||
* @param {import('../../objects/Library')} oldLibrary
|
||||
* @param {string} query
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @returns {{podcast:object[], tags:object[]}}
|
||||
*/
|
||||
async search(oldLibrary, query, limit, offset) {
|
||||
// Search title, author, itunesId, itunesArtistId
|
||||
const podcasts = await Database.podcastModel.findAll({
|
||||
where: {
|
||||
[Sequelize.Op.or]: [
|
||||
{
|
||||
title: {
|
||||
[Sequelize.Op.substring]: query
|
||||
}
|
||||
},
|
||||
{
|
||||
author: {
|
||||
[Sequelize.Op.substring]: query
|
||||
}
|
||||
},
|
||||
{
|
||||
itunesId: {
|
||||
[Sequelize.Op.substring]: query
|
||||
}
|
||||
},
|
||||
{
|
||||
itunesArtistId: {
|
||||
[Sequelize.Op.substring]: query
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Database.libraryItemModel,
|
||||
where: {
|
||||
libraryId: oldLibrary.id
|
||||
}
|
||||
}
|
||||
],
|
||||
subQuery: false,
|
||||
distinct: true,
|
||||
limit,
|
||||
offset
|
||||
})
|
||||
|
||||
const itemMatches = []
|
||||
|
||||
for (const podcast of podcasts) {
|
||||
const libraryItem = podcast.libraryItem
|
||||
delete podcast.libraryItem
|
||||
libraryItem.media = podcast
|
||||
|
||||
let matchText = null
|
||||
let matchKey = null
|
||||
for (const key of ['title', 'author', 'itunesId', 'itunesArtistId']) {
|
||||
if (podcast[key]?.toLowerCase().includes(query)) {
|
||||
matchText = podcast[key]
|
||||
matchKey = key
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (matchKey) {
|
||||
itemMatches.push({
|
||||
matchText,
|
||||
matchKey,
|
||||
libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Search tags
|
||||
const tagMatches = []
|
||||
const [tagResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM podcasts p, libraryItems li, json_each(p.tags) WHERE json_valid(p.tags) AND json_each.value LIKE :query AND p.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value LIMIT :limit OFFSET :offset;`, {
|
||||
replacements: {
|
||||
query: `%${query}%`,
|
||||
libraryId: oldLibrary.id,
|
||||
limit,
|
||||
offset
|
||||
},
|
||||
raw: true
|
||||
})
|
||||
for (const row of tagResults) {
|
||||
tagMatches.push({
|
||||
name: row.value,
|
||||
numItems: row.numItems
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
podcast: itemMatches,
|
||||
tags: tagMatches
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user