Merge pull request #3185 from mikiher/genre-search

Adds genres to gloabl search
This commit is contained in:
advplyr 2024-07-21 11:07:43 -05:00 committed by GitHub
commit 604ae080ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 165 additions and 78 deletions

View File

@ -5,6 +5,7 @@
</div> </div>
<div class="flex-grow px-2 authorSearchCardContent h-full"> <div class="flex-grow px-2 authorSearchCardContent h-full">
<p class="truncate text-sm">{{ name }}</p> <p class="truncate text-sm">{{ name }}</p>
<p class="text-xs text-gray-400">{{ $getString('LabelXBooks', [numBooks]) }}</p>
</div> </div>
</div> </div>
</template> </template>
@ -23,6 +24,9 @@ export default {
computed: { computed: {
name() { name() {
return this.author.name return this.author.name
},
numBooks() {
return this.author.numBooks
} }
}, },
methods: {}, methods: {},
@ -33,9 +37,9 @@ export default {
<style> <style>
.authorSearchCardContent { .authorSearchCardContent {
width: calc(100% - 80px); width: calc(100% - 80px);
height: 40px; height: 44px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
</style> </style>

View File

@ -0,0 +1,36 @@
<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>
<p class="text-xs text-gray-400">{{ $getString('LabelXItems', [numItems]) }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
genre: String,
numItems: Number
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
<style>
.tagSearchCardContent {
width: calc(100% - 40px);
height: 44px;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>

View File

@ -5,6 +5,7 @@
</div> </div>
<div class="flex-grow px-2 narratorSearchCardContent h-full"> <div class="flex-grow px-2 narratorSearchCardContent h-full">
<p class="truncate text-sm">{{ narrator }}</p> <p class="truncate text-sm">{{ narrator }}</p>
<p class="text-xs text-gray-400">{{ $getString('LabelXBooks', [numBooks]) }}</p>
</div> </div>
</div> </div>
</template> </template>
@ -12,7 +13,8 @@
<script> <script>
export default { export default {
props: { props: {
narrator: String narrator: String,
numBooks: Number
}, },
data() { data() {
return {} return {}
@ -26,9 +28,9 @@ export default {
<style scoped> <style scoped>
.narratorSearchCardContent { .narratorSearchCardContent {
width: calc(100% - 40px); width: calc(100% - 40px);
height: 40px; height: 44px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
</style> </style>

View File

@ -5,6 +5,7 @@
</div> </div>
<div class="flex-grow px-2 tagSearchCardContent h-full"> <div class="flex-grow px-2 tagSearchCardContent h-full">
<p class="truncate text-sm">{{ tag }}</p> <p class="truncate text-sm">{{ tag }}</p>
<p class="text-xs text-gray-400">{{ $getString('LabelXItems', [numItems]) }}</p>
</div> </div>
</div> </div>
</template> </template>
@ -12,7 +13,8 @@
<script> <script>
export default { export default {
props: { props: {
tag: String tag: String,
numItems: Number
}, },
data() { data() {
return {} return {}
@ -26,9 +28,9 @@ export default {
<style> <style>
.tagSearchCardContent { .tagSearchCardContent {
width: calc(100% - 40px); width: calc(100% - 40px);
height: 40px; height: 44px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
</style> </style>

View File

@ -59,9 +59,18 @@
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelTags }}</p> <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"> <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)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
<cards-tag-search-card :tag="item.name" /> <cards-tag-search-card :tag="item.name" :num-items="item.numItems" />
</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" :num-items="item.numItems" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
@ -70,7 +79,7 @@
<template v-for="narrator in narratorResults"> <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"> <li :key="narrator.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=narrators.${$encode(narrator.name)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
<cards-narrator-search-card :narrator="narrator.name" /> <cards-narrator-search-card :narrator="narrator.name" :num-books="narrator.numBooks" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
@ -95,6 +104,7 @@ export default {
authorResults: [], authorResults: [],
seriesResults: [], seriesResults: [],
tagResults: [], tagResults: [],
genreResults: [],
narratorResults: [], narratorResults: [],
searchTimeout: null, searchTimeout: null,
lastSearch: null lastSearch: null
@ -105,7 +115,7 @@ export default {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
totalResults() { 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: { methods: {
@ -116,7 +126,7 @@ export default {
if (!this.search) return if (!this.search) return
var search = this.search var search = this.search
this.clearResults() this.clearResults()
this.$router.push(`/library/${this.currentLibraryId}/search?q=${search}`) this.$router.push(`/library/${this.currentLibraryId}/search?q=${encodeURIComponent(search)}`)
}, },
clearResults() { clearResults() {
this.search = null this.search = null
@ -126,6 +136,7 @@ export default {
this.authorResults = [] this.authorResults = []
this.seriesResults = [] this.seriesResults = []
this.tagResults = [] this.tagResults = []
this.genreResults = []
this.narratorResults = [] this.narratorResults = []
this.showMenu = false this.showMenu = false
this.isFetching = false this.isFetching = false
@ -155,7 +166,7 @@ export default {
} }
this.isFetching = true this.isFetching = true
const searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=3`).catch((error) => { const searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${encodeURIComponent(value)}&limit=3`).catch((error) => {
console.error('Search error', error) console.error('Search error', error)
return [] return []
}) })
@ -168,6 +179,7 @@ export default {
this.authorResults = searchResults.authors || [] this.authorResults = searchResults.authors || []
this.seriesResults = searchResults.series || [] this.seriesResults = searchResults.series || []
this.tagResults = searchResults.tags || [] this.tagResults = searchResults.tags || []
this.genreResults = searchResults.genres || []
this.narratorResults = searchResults.narrators || [] this.narratorResults = searchResults.narrators || []
this.isFetching = false this.isFetching = false
@ -203,4 +215,4 @@ export default {
.globalSearchMenu { .globalSearchMenu {
max-height: calc(100vh - 75px); max-height: calc(100vh - 75px);
} }
</style> </style>

View File

@ -92,12 +92,13 @@ export default {
if (this.$route.name.startsWith('config')) { if (this.$route.name.startsWith('config')) {
// No need to refresh // No need to refresh
} else if (this.$route.name.startsWith('library') && this.$route.name !== 'library-library-series-id') {
const newRoute = this.$route.path.replace(currLibraryId, library.id)
this.$router.push(newRoute)
} else if (this.$route.name === 'library-library-series-id' && library.mediaType === 'book') { } else if (this.$route.name === 'library-library-series-id' && library.mediaType === 'book') {
// For series item page redirect to root series page // For series item page redirect to root series page
this.$router.push(`/library/${library.id}/bookshelf/series`) this.$router.push(`/library/${library.id}/bookshelf/series`)
} else if (this.$route.name === 'library-library-search') {
this.$router.push(this.$route.fullPath.replace(currLibraryId, library.id))
} else if (this.$route.name.startsWith('library')) {
this.$router.push(this.$route.path.replace(currLibraryId, library.id))
} else { } else {
this.$router.push(`/library/${library.id}`) this.$router.push(`/library/${library.id}`)
} }

View File

@ -16,7 +16,7 @@ export default {
if (!library) { if (!library) {
return redirect('/oops?message=Library not found') return redirect('/oops?message=Library not found')
} }
let results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${query.q}`).catch((error) => { let results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${encodeURIComponent(query.q)}`).catch((error) => {
console.error('Failed to search library', error) console.error('Failed to search library', error)
return null return null
}) })
@ -55,7 +55,7 @@ export default {
}, },
methods: { methods: {
async search() { async search() {
const results = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${this.query}`).catch((error) => { const results = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${encodeURIComponent(this.query)}`).catch((error) => {
console.error('Failed to search library', error) console.error('Failed to search library', error)
return null return null
}) })

View File

@ -611,6 +611,8 @@
"LabelViewQueue": "View player queue", "LabelViewQueue": "View player queue",
"LabelVolume": "Volume", "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Weekdays to run", "LabelWeekdaysToRun": "Weekdays to run",
"LabelXBooks": "{0} books",
"LabelXItems": "{0} items",
"LabelYearReviewHide": "Hide Year in Review", "LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review", "LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Your audiobook duration", "LabelYourAudiobookDuration": "Your audiobook duration",

View File

@ -1079,7 +1079,7 @@ module.exports = {
// Search tags // Search tags
const tagMatches = [] 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: { replacements: {
query: `%${query}%`, query: `%${query}%`,
libraryId: oldLibrary.id, 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 // Search series
const allSeries = await Database.seriesModel.findAll({ const allSeries = await Database.seriesModel.findAll({
where: { where: {
@ -1140,6 +1158,7 @@ module.exports = {
book: itemMatches, book: itemMatches,
narrators: narratorMatches, narrators: narratorMatches,
tags: tagMatches, tags: tagMatches,
genres: genreMatches,
series: seriesMatches, series: seriesMatches,
authors: authorMatches authors: authorMatches
} }

View File

@ -1,4 +1,3 @@
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const Database = require('../../Database') const Database = require('../../Database')
const Logger = require('../../Logger') const Logger = require('../../Logger')
@ -7,7 +6,7 @@ const { asciiOnlyToLowerCase } = require('../index')
module.exports = { module.exports = {
/** /**
* User permissions to restrict podcasts for explicit content & tags * 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 }} * @returns {{ podcastWhere:Sequelize.WhereOptions, replacements:object }}
*/ */
getUserPermissionPodcastWhereQuery(user) { getUserPermissionPodcastWhereQuery(user) {
@ -23,9 +22,11 @@ module.exports = {
if (user.permissions.selectedTagsNotAccessible) { 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)) podcastWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), 0))
} else { } else {
podcastWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), { podcastWhere.push(
[Sequelize.Op.gte]: 1 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 { return {
@ -36,8 +37,8 @@ module.exports = {
/** /**
* Get where options for Podcast model * Get where options for Podcast model
* @param {string} group * @param {string} group
* @param {[string]} value * @param {[string]} value
* @returns {object} { Sequelize.WhereOptions, string[] } * @returns {object} { Sequelize.WhereOptions, string[] }
*/ */
getMediaGroupQuery(group, value) { getMediaGroupQuery(group, value) {
@ -63,8 +64,8 @@ module.exports = {
/** /**
* Get sequelize order * Get sequelize order
* @param {string} sortBy * @param {string} sortBy
* @param {boolean} sortDesc * @param {boolean} sortDesc
* @returns {Sequelize.order} * @returns {Sequelize.order}
*/ */
getOrder(sortBy, sortDesc) { getOrder(sortBy, sortDesc) {
@ -94,15 +95,15 @@ module.exports = {
/** /**
* Get library items for podcast media type using filter and sort * Get library items for podcast media type using filter and sort
* @param {string} libraryId * @param {string} libraryId
* @param {oldUser} user * @param {oldUser} user
* @param {[string]} filterGroup * @param {[string]} filterGroup
* @param {[string]} filterValue * @param {[string]} filterValue
* @param {string} sortBy * @param {string} sortBy
* @param {string} sortDesc * @param {string} sortDesc
* @param {string[]} include * @param {string[]} include
* @param {number} limit * @param {number} limit
* @param {number} offset * @param {number} offset
* @returns {object} { libraryItems:LibraryItem[], count:number } * @returns {object} { libraryItems:LibraryItem[], count:number }
*/ */
async getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset) { async getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset) {
@ -130,7 +131,7 @@ module.exports = {
] ]
} else if (filterGroup === 'recent') { } else if (filterGroup === 'recent') {
libraryItemWhere['createdAt'] = { 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, replacements,
distinct: true, distinct: true,
attributes: { attributes: {
include: [ include: [[Sequelize.literal(`(SELECT count(*) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'numEpisodes'], ...podcastIncludes]
[Sequelize.literal(`(SELECT count(*) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'numEpisodes'],
...podcastIncludes
]
}, },
include: [ include: [
{ {
@ -199,14 +197,14 @@ module.exports = {
/** /**
* Get podcast episodes filtered and sorted * Get podcast episodes filtered and sorted
* @param {string} libraryId * @param {string} libraryId
* @param {oldUser} user * @param {oldUser} user
* @param {[string]} filterGroup * @param {[string]} filterGroup
* @param {[string]} filterValue * @param {[string]} filterValue
* @param {string} sortBy * @param {string} sortBy
* @param {string} sortDesc * @param {string} sortDesc
* @param {number} limit * @param {number} limit
* @param {number} offset * @param {number} offset
* @param {boolean} isHomePage for home page shelves * @param {boolean} isHomePage for home page shelves
* @returns {object} {libraryItems:LibraryItem[], count:number} * @returns {object} {libraryItems:LibraryItem[], count:number}
*/ */
@ -251,7 +249,7 @@ module.exports = {
} }
} else if (filterGroup === 'recent') { } else if (filterGroup === 'recent') {
podcastEpisodeWhere['createdAt'] = { 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 * Search podcasts
* @param {import('../../objects/user/User')} oldUser * @param {import('../../objects/user/User')} oldUser
* @param {import('../../objects/Library')} oldLibrary * @param {import('../../objects/Library')} oldLibrary
* @param {string} query * @param {string} query
* @param {number} limit * @param {number} limit
* @param {number} offset * @param {number} offset
* @returns {{podcast:object[], tags:object[]}} * @returns {{podcast:object[], tags:object[]}}
*/ */
async search(oldUser, oldLibrary, query, limit, offset) { async search(oldUser, oldLibrary, query, limit, offset) {
@ -386,7 +384,7 @@ module.exports = {
// Search tags // Search tags
const tagMatches = [] 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: { replacements: {
query: `%${query}%`, query: `%${query}%`,
libraryId: oldLibrary.id, 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 { return {
podcast: itemMatches, podcast: itemMatches,
tags: tagMatches tags: tagMatches,
genres: genreMatches
} }
}, },
/** /**
* Most recent podcast episodes not finished * Most recent podcast episodes not finished
* @param {import('../../objects/user/User')} oldUser * @param {import('../../objects/user/User')} oldUser
* @param {import('../../objects/Library')} oldLibrary * @param {import('../../objects/Library')} oldLibrary
* @param {number} limit * @param {number} limit
* @param {number} offset * @param {number} offset
* @returns {Promise<object[]>} * @returns {Promise<object[]>}
*/ */
async getRecentEpisodes(oldUser, oldLibrary, limit, offset) { async getRecentEpisodes(oldUser, oldLibrary, limit, offset) {
@ -446,9 +463,7 @@ module.exports = {
required: false required: false
} }
], ],
order: [ order: [['publishedAt', 'DESC']],
['publishedAt', 'DESC']
],
subQuery: false, subQuery: false,
limit, limit,
offset offset
@ -469,7 +484,7 @@ module.exports = {
/** /**
* Get stats for podcast library * Get stats for podcast library
* @param {string} libraryId * @param {string} libraryId
* @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>} * @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>}
*/ */
async getPodcastLibraryStats(libraryId) { async getPodcastLibraryStats(libraryId) {
@ -491,7 +506,7 @@ module.exports = {
/** /**
* Genres with num podcasts * Genres with num podcasts
* @param {string} libraryId * @param {string} libraryId
* @returns {{genre:string, count:number}[]} * @returns {{genre:string, count:number}[]}
*/ */
async getGenresWithCount(libraryId) { async getGenresWithCount(libraryId) {
@ -513,17 +528,13 @@ module.exports = {
/** /**
* Get longest podcasts in library * Get longest podcasts in library
* @param {string} libraryId * @param {string} libraryId
* @param {number} limit * @param {number} limit
* @returns {Promise<{ id:string, title:string, duration:number }[]>} * @returns {Promise<{ id:string, title:string, duration:number }[]>}
*/ */
async getLongestPodcasts(libraryId, limit) { async getLongestPodcasts(libraryId, limit) {
const podcasts = await Database.podcastModel.findAll({ const podcasts = await Database.podcastModel.findAll({
attributes: [ attributes: ['id', 'title', [Sequelize.literal(`(SELECT SUM(json_extract(pe.audioFile, '$.duration')) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'duration']],
'id',
'title',
[Sequelize.literal(`(SELECT SUM(json_extract(pe.audioFile, '$.duration')) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'duration']
],
include: { include: {
model: Database.libraryItemModel, model: Database.libraryItemModel,
attributes: ['id', 'libraryId'], attributes: ['id', 'libraryId'],
@ -531,12 +542,10 @@ module.exports = {
libraryId libraryId
} }
}, },
order: [ order: [['duration', 'DESC']],
['duration', 'DESC']
],
limit limit
}) })
return podcasts.map(podcast => { return podcasts.map((podcast) => {
return { return {
id: podcast.libraryItem.id, id: podcast.libraryItem.id,
title: podcast.title, title: podcast.title,
@ -544,4 +553,4 @@ module.exports = {
} }
}) })
} }
} }