mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +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