mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Adds genres to gloabl search
This commit is contained in:
parent
9cd0ac80b1
commit
bff56220c2
34
client/components/cards/GenreSearchCard.vue
Normal file
34
client/components/cards/GenreSearchCard.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<div class="w-10 h-10 flex items-center justify-center">
|
||||
<span class="material-symbols text-2xl text-gray-200">category</span>
|
||||
</div>
|
||||
<div class="flex-grow px-2 tagSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ genre }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
genre: String
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tagSearchCardContent {
|
||||
width: calc(100% - 40px);
|
||||
height: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
@ -59,13 +59,22 @@
|
||||
|
||||
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelTags }}</p>
|
||||
<template v-for="item in tagResults">
|
||||
<li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<li :key="`tag.${item.name}`" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
|
||||
<cards-tag-search-card :tag="item.name" />
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<p v-if="genreResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelGenres }}</p>
|
||||
<template v-for="item in genreResults">
|
||||
<li :key="`genre.${item.name}`" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(item.name)}`">
|
||||
<cards-genre-search-card :genre="item.name" />
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<p v-if="narratorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelNarrators }}</p>
|
||||
<template v-for="narrator in narratorResults">
|
||||
<li :key="narrator.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
@ -95,6 +104,7 @@ export default {
|
||||
authorResults: [],
|
||||
seriesResults: [],
|
||||
tagResults: [],
|
||||
genreResults: [],
|
||||
narratorResults: [],
|
||||
searchTimeout: null,
|
||||
lastSearch: null
|
||||
@ -105,7 +115,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 + this.narratorResults.length
|
||||
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.genreResults.length + this.podcastResults.length + this.narratorResults.length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -126,6 +136,7 @@ export default {
|
||||
this.authorResults = []
|
||||
this.seriesResults = []
|
||||
this.tagResults = []
|
||||
this.genreResults = []
|
||||
this.narratorResults = []
|
||||
this.showMenu = false
|
||||
this.isFetching = false
|
||||
@ -168,6 +179,7 @@ export default {
|
||||
this.authorResults = searchResults.authors || []
|
||||
this.seriesResults = searchResults.series || []
|
||||
this.tagResults = searchResults.tags || []
|
||||
this.genreResults = searchResults.genres || []
|
||||
this.narratorResults = searchResults.narrators || []
|
||||
|
||||
this.isFetching = false
|
||||
@ -203,4 +215,4 @@ export default {
|
||||
.globalSearchMenu {
|
||||
max-height: calc(100vh - 75px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1079,7 +1079,7 @@ module.exports = {
|
||||
|
||||
// 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;`, {
|
||||
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 ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, {
|
||||
replacements: {
|
||||
query: `%${query}%`,
|
||||
libraryId: oldLibrary.id,
|
||||
@ -1095,6 +1095,24 @@ module.exports = {
|
||||
})
|
||||
}
|
||||
|
||||
// Search genres
|
||||
const genreMatches = []
|
||||
const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM books b, libraryItems li, json_each(b.genres) WHERE json_valid(b.genres) AND json_each.value LIKE :query AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, {
|
||||
replacements: {
|
||||
query: `%${query}%`,
|
||||
libraryId: oldLibrary.id,
|
||||
limit,
|
||||
offset
|
||||
},
|
||||
raw: true
|
||||
})
|
||||
for (const row of genreResults) {
|
||||
genreMatches.push({
|
||||
name: row.value,
|
||||
numItems: row.numItems
|
||||
})
|
||||
}
|
||||
|
||||
// Search series
|
||||
const allSeries = await Database.seriesModel.findAll({
|
||||
where: {
|
||||
@ -1140,6 +1158,7 @@ module.exports = {
|
||||
book: itemMatches,
|
||||
narrators: narratorMatches,
|
||||
tags: tagMatches,
|
||||
genres: genreMatches,
|
||||
series: seriesMatches,
|
||||
authors: authorMatches
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
const Sequelize = require('sequelize')
|
||||
const Database = require('../../Database')
|
||||
const Logger = require('../../Logger')
|
||||
@ -7,7 +6,7 @@ const { asciiOnlyToLowerCase } = require('../index')
|
||||
module.exports = {
|
||||
/**
|
||||
* User permissions to restrict podcasts for explicit content & tags
|
||||
* @param {import('../../objects/user/User')} user
|
||||
* @param {import('../../objects/user/User')} user
|
||||
* @returns {{ podcastWhere:Sequelize.WhereOptions, replacements:object }}
|
||||
*/
|
||||
getUserPermissionPodcastWhereQuery(user) {
|
||||
@ -23,9 +22,11 @@ module.exports = {
|
||||
if (user.permissions.selectedTagsNotAccessible) {
|
||||
podcastWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), 0))
|
||||
} else {
|
||||
podcastWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), {
|
||||
[Sequelize.Op.gte]: 1
|
||||
}))
|
||||
podcastWhere.push(
|
||||
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), {
|
||||
[Sequelize.Op.gte]: 1
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
return {
|
||||
@ -36,8 +37,8 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get where options for Podcast model
|
||||
* @param {string} group
|
||||
* @param {[string]} value
|
||||
* @param {string} group
|
||||
* @param {[string]} value
|
||||
* @returns {object} { Sequelize.WhereOptions, string[] }
|
||||
*/
|
||||
getMediaGroupQuery(group, value) {
|
||||
@ -63,8 +64,8 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get sequelize order
|
||||
* @param {string} sortBy
|
||||
* @param {boolean} sortDesc
|
||||
* @param {string} sortBy
|
||||
* @param {boolean} sortDesc
|
||||
* @returns {Sequelize.order}
|
||||
*/
|
||||
getOrder(sortBy, sortDesc) {
|
||||
@ -94,15 +95,15 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get library items for podcast media type using filter and sort
|
||||
* @param {string} libraryId
|
||||
* @param {string} libraryId
|
||||
* @param {oldUser} user
|
||||
* @param {[string]} filterGroup
|
||||
* @param {[string]} filterValue
|
||||
* @param {string} sortBy
|
||||
* @param {string} sortDesc
|
||||
* @param {[string]} filterGroup
|
||||
* @param {[string]} filterValue
|
||||
* @param {string} sortBy
|
||||
* @param {string} sortDesc
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @returns {object} { libraryItems:LibraryItem[], count:number }
|
||||
*/
|
||||
async getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset) {
|
||||
@ -130,7 +131,7 @@ module.exports = {
|
||||
]
|
||||
} else if (filterGroup === 'recent') {
|
||||
libraryItemWhere['createdAt'] = {
|
||||
[Sequelize.Op.gte]: new Date(new Date() - (60 * 24 * 60 * 60 * 1000)) // 60 days ago
|
||||
[Sequelize.Op.gte]: new Date(new Date() - 60 * 24 * 60 * 60 * 1000) // 60 days ago
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,10 +155,7 @@ module.exports = {
|
||||
replacements,
|
||||
distinct: true,
|
||||
attributes: {
|
||||
include: [
|
||||
[Sequelize.literal(`(SELECT count(*) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'numEpisodes'],
|
||||
...podcastIncludes
|
||||
]
|
||||
include: [[Sequelize.literal(`(SELECT count(*) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'numEpisodes'], ...podcastIncludes]
|
||||
},
|
||||
include: [
|
||||
{
|
||||
@ -199,14 +197,14 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get podcast episodes filtered and sorted
|
||||
* @param {string} libraryId
|
||||
* @param {oldUser} user
|
||||
* @param {[string]} filterGroup
|
||||
* @param {[string]} filterValue
|
||||
* @param {string} sortBy
|
||||
* @param {string} sortDesc
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @param {string} libraryId
|
||||
* @param {oldUser} user
|
||||
* @param {[string]} filterGroup
|
||||
* @param {[string]} filterValue
|
||||
* @param {string} sortBy
|
||||
* @param {string} sortDesc
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @param {boolean} isHomePage for home page shelves
|
||||
* @returns {object} {libraryItems:LibraryItem[], count:number}
|
||||
*/
|
||||
@ -251,7 +249,7 @@ module.exports = {
|
||||
}
|
||||
} else if (filterGroup === 'recent') {
|
||||
podcastEpisodeWhere['createdAt'] = {
|
||||
[Sequelize.Op.gte]: new Date(new Date() - (60 * 24 * 60 * 60 * 1000)) // 60 days ago
|
||||
[Sequelize.Op.gte]: new Date(new Date() - 60 * 24 * 60 * 60 * 1000) // 60 days ago
|
||||
}
|
||||
}
|
||||
|
||||
@ -305,10 +303,10 @@ module.exports = {
|
||||
/**
|
||||
* Search podcasts
|
||||
* @param {import('../../objects/user/User')} oldUser
|
||||
* @param {import('../../objects/Library')} oldLibrary
|
||||
* @param {string} query
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @param {import('../../objects/Library')} oldLibrary
|
||||
* @param {string} query
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @returns {{podcast:object[], tags:object[]}}
|
||||
*/
|
||||
async search(oldUser, oldLibrary, query, limit, offset) {
|
||||
@ -386,7 +384,7 @@ module.exports = {
|
||||
|
||||
// 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;`, {
|
||||
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 ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, {
|
||||
replacements: {
|
||||
query: `%${query}%`,
|
||||
libraryId: oldLibrary.id,
|
||||
@ -402,18 +400,37 @@ module.exports = {
|
||||
})
|
||||
}
|
||||
|
||||
// Search genres
|
||||
const genreMatches = []
|
||||
const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM podcasts p, libraryItems li, json_each(p.genres) WHERE json_valid(p.genres) AND json_each.value LIKE :query AND p.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, {
|
||||
replacements: {
|
||||
query: `%${query}%`,
|
||||
libraryId: oldLibrary.id,
|
||||
limit,
|
||||
offset
|
||||
},
|
||||
raw: true
|
||||
})
|
||||
for (const row of genreResults) {
|
||||
genreMatches.push({
|
||||
name: row.value,
|
||||
numItems: row.numItems
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
podcast: itemMatches,
|
||||
tags: tagMatches
|
||||
tags: tagMatches,
|
||||
genres: genreMatches
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Most recent podcast episodes not finished
|
||||
* @param {import('../../objects/user/User')} oldUser
|
||||
* @param {import('../../objects/Library')} oldLibrary
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @param {import('../../objects/user/User')} oldUser
|
||||
* @param {import('../../objects/Library')} oldLibrary
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @returns {Promise<object[]>}
|
||||
*/
|
||||
async getRecentEpisodes(oldUser, oldLibrary, limit, offset) {
|
||||
@ -446,9 +463,7 @@ module.exports = {
|
||||
required: false
|
||||
}
|
||||
],
|
||||
order: [
|
||||
['publishedAt', 'DESC']
|
||||
],
|
||||
order: [['publishedAt', 'DESC']],
|
||||
subQuery: false,
|
||||
limit,
|
||||
offset
|
||||
@ -469,7 +484,7 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get stats for podcast library
|
||||
* @param {string} libraryId
|
||||
* @param {string} libraryId
|
||||
* @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>}
|
||||
*/
|
||||
async getPodcastLibraryStats(libraryId) {
|
||||
@ -491,7 +506,7 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Genres with num podcasts
|
||||
* @param {string} libraryId
|
||||
* @param {string} libraryId
|
||||
* @returns {{genre:string, count:number}[]}
|
||||
*/
|
||||
async getGenresWithCount(libraryId) {
|
||||
@ -513,17 +528,13 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get longest podcasts in library
|
||||
* @param {string} libraryId
|
||||
* @param {number} limit
|
||||
* @param {string} libraryId
|
||||
* @param {number} limit
|
||||
* @returns {Promise<{ id:string, title:string, duration:number }[]>}
|
||||
*/
|
||||
async getLongestPodcasts(libraryId, limit) {
|
||||
const podcasts = await Database.podcastModel.findAll({
|
||||
attributes: [
|
||||
'id',
|
||||
'title',
|
||||
[Sequelize.literal(`(SELECT SUM(json_extract(pe.audioFile, '$.duration')) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'duration']
|
||||
],
|
||||
attributes: ['id', 'title', [Sequelize.literal(`(SELECT SUM(json_extract(pe.audioFile, '$.duration')) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'duration']],
|
||||
include: {
|
||||
model: Database.libraryItemModel,
|
||||
attributes: ['id', 'libraryId'],
|
||||
@ -531,12 +542,10 @@ module.exports = {
|
||||
libraryId
|
||||
}
|
||||
},
|
||||
order: [
|
||||
['duration', 'DESC']
|
||||
],
|
||||
order: [['duration', 'DESC']],
|
||||
limit
|
||||
})
|
||||
return podcasts.map(podcast => {
|
||||
return podcasts.map((podcast) => {
|
||||
return {
|
||||
id: podcast.libraryItem.id,
|
||||
title: podcast.title,
|
||||
@ -544,4 +553,4 @@ module.exports = {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user