New data model for global search input and search page

This commit is contained in:
advplyr 2022-03-13 12:39:12 -05:00
parent 30f15d3575
commit ea9ec13845
16 changed files with 241 additions and 88 deletions

View File

@ -7,13 +7,16 @@
<div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div> <div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div>
</div> </div>
<div v-if="loaded && !shelves.length && isRootUser" class="w-full flex flex-col items-center justify-center py-12"> <div v-if="loaded && !shelves.length && isRootUser && !search" class="w-full flex flex-col items-center justify-center py-12">
<p class="text-center text-2xl font-book mb-4 py-4">Audiobookshelf is empty!</p> <p class="text-center text-2xl font-book mb-4 py-4">Audiobookshelf is empty!</p>
<div class="flex"> <div class="flex">
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn> <ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
<ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn> <ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn>
</div> </div>
</div> </div>
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
<p class="text-center text-xl font-book py-4">No results for query</p>
</div>
<div v-else class="w-full flex flex-col items-center"> <div v-else class="w-full flex flex-col items-center">
<template v-for="(shelf, index) in shelves"> <template v-for="(shelf, index) in shelves">
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
@ -97,12 +100,12 @@ export default {
}, },
async setShelvesFromSearch() { async setShelvesFromSearch() {
var shelves = [] var shelves = []
if (this.results.audiobooks) { if (this.results.books) {
shelves.push({ shelves.push({
id: 'audiobooks', id: 'books',
label: 'Books', label: 'Books',
type: 'books', type: 'books',
entities: this.results.audiobooks.map((ab) => ab.audiobook) entities: this.results.books.map((res) => res.libraryItem)
}) })
} }
@ -113,8 +116,9 @@ export default {
type: 'series', type: 'series',
entities: this.results.series.map((seriesObj) => { entities: this.results.series.map((seriesObj) => {
return { return {
name: seriesObj.series, name: seriesObj.series.name,
books: seriesObj.audiobooks, series: seriesObj.series,
books: seriesObj.books,
type: 'series' type: 'series'
} }
}) })
@ -127,8 +131,8 @@ export default {
type: 'tags', type: 'tags',
entities: this.results.tags.map((tagObj) => { entities: this.results.tags.map((tagObj) => {
return { return {
name: tagObj.tag, name: tagObj.name,
books: tagObj.audiobooks, books: tagObj.books || [],
type: 'tags' type: 'tags'
} }
}) })
@ -141,9 +145,7 @@ export default {
type: 'authors', type: 'authors',
entities: this.results.authors.map((a) => { entities: this.results.authors.map((a) => {
return { return {
id: a.author, ...a,
name: a.author,
numBooks: a.numBooks,
type: 'author' type: 'author'
} }
}) })

View File

@ -21,8 +21,8 @@
</div> </div>
<div v-if="shelf.type === 'authors'" class="flex items-center"> <div v-if="shelf.type === 'authors'" class="flex items-center">
<template v-for="entity in shelf.entities"> <template v-for="entity in shelf.entities">
<nuxt-link :key="entity.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(entity.name)}`"> <nuxt-link :key="entity.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(entity.id)}`">
<cards-author-card :width="bookCoverWidth" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" /> <cards-author-card :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" />
</nuxt-link> </nuxt-link>
</template> </template>
</div> </div>

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="flex items-center h-full px-1 overflow-hidden"> <div class="flex items-center h-full px-1 overflow-hidden">
<covers-book-cover :audiobook="audiobook" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="flex-grow px-2 audiobookSearchCardContent"> <div class="flex-grow px-2 audiobookSearchCardContent">
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p> <p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
<p v-else class="truncate text-sm" v-html="matchHtml" /> <p v-else class="truncate text-sm" v-html="matchHtml" />
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p> <p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p>
<p v-if="matchKey !== 'authorFL'" class="text-xs text-gray-200 truncate">by {{ authorFL }}</p> <p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" /> <p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" /> <div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
@ -18,7 +18,7 @@
<script> <script>
export default { export default {
props: { props: {
audiobook: { libraryItem: {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
@ -37,17 +37,23 @@ export default {
if (this.bookCoverAspectRatio === 1) return 50 * 1.2 if (this.bookCoverAspectRatio === 1) return 50 * 1.2
return 50 return 50
}, },
book() { media() {
return this.audiobook ? this.audiobook.book || {} : {} return this.libraryItem ? this.libraryItem.media || {} : {}
},
mediaMetadata() {
return this.media.metadata || {}
}, },
title() { title() {
return this.book ? this.book.title : 'No Title' return this.mediaMetadata.title || 'No Title'
}, },
subtitle() { subtitle() {
return this.book ? this.book.subtitle : '' return this.mediaMetadata.subtitle || ''
}, },
authorFL() { authors() {
return this.book ? this.book.authorFL : 'Unknown' return this.mediaMetadata.authors || []
},
authorName() {
return this.authors.map((au) => au.name).join(', ')
}, },
matchHtml() { matchHtml() {
if (!this.matchText || !this.search) return '' if (!this.matchText || !this.search) return ''
@ -69,7 +75,7 @@ export default {
html += lastPart html += lastPart
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>` if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
if (this.matchKey === 'authorFL') return `by ${html}` if (this.matchKey === 'authors') return `by ${html}`
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>` if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>` if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>` if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`

View File

@ -1,8 +1,28 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden"> <div class="flex h-full px-1 overflow-hidden">
<img src="/icons/NoUserPhoto.png" class="w-40 h-40 max-h-40 object-contain" style="max-height: 40px; max-width: 40px" /> <div class="overflow-hidden bg-primary rounded-sm" style="height: 50px; width: 40px">
<svg v-if="!imagePath" width="140%" height="140%" style="margin-left: -20%; margin-top: -20%; opacity: 0.6" viewBox="0 0 177 266" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="white" d="M40.7156 165.47C10.2694 150.865 -31.5407 148.629 -38.0532 155.529L63.3191 204.159L76.9443 190.899C66.828 181.394 54.006 171.846 40.7156 165.47Z" stroke="white" stroke-width="4" transform="translate(-2 -1)" />
<path d="M-38.0532 155.529C-31.5407 148.629 10.2694 150.865 40.7156 165.47C54.006 171.846 66.828 181.394 76.9443 190.899L95.0391 173.37C80.6681 159.403 64.7526 149.155 51.5747 142.834C21.3549 128.337 -46.2471 114.563 -60.6897 144.67L-71.5489 167.307L44.5864 223.019L63.3191 204.159L-38.0532 155.529Z" fill="white" />
<path
d="M105.87 29.6508C80.857 17.6515 50.8784 28.1923 38.879 53.2056C26.8797 78.219 37.4205 108.198 62.4338 120.197C87.4472 132.196 117.426 121.656 129.425 96.6422C141.425 71.6288 130.884 41.6502 105.87 29.6508ZM106.789 85.783C112.761 73.3329 107.461 58.2599 95.0112 52.2874C82.5611 46.3148 67.4881 51.6147 61.5156 64.0648C55.543 76.5149 60.8429 91.5879 73.293 97.5604C85.7431 103.533 100.816 98.2331 106.789 85.783Z"
fill="white"
/>
<path
d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01ZM181.725 108.497C179.624 108.491 177.436 109.326 175.835 110.918L160.415 126.257L191.848 157.856L207.268 142.517C210.554 139.248 210.568 133.954 207.299 130.667L187.685 110.95C186.009 109.264 183.91 108.502 181.725 108.497ZM151.399 135.226L58.2034 227.931L58.1203 259.447L89.6359 259.53L182.831 166.825L151.399 135.226Z"
fill="white"
/>
<path d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01Z" fill="white" stroke="white" stroke-width="10px" />
</svg>
<div v-else class="w-full h-full relative overflow-hidden rounded-sm">
<div v-if="showCoverBg" class="cover-bg absolute" :style="{ backgroundImage: `url(${imgSrc})` }" />
<img ref="img" :src="imgSrc" @load="imageLoaded" class="absolute top-0 left-0 h-full w-full object-contain" />
</div>
</div>
<!-- <img v-if="!imagePath" src="/icons/NoUserPhoto.png" class="w-40 h-40 max-h-40 object-contain" style="max-height: 40px; max-width: 40px" />
<img v-else :src="imgSrc" class="w-40 h-40 max-h-40 object-contain" style="max-height: 40px; max-width: 40px" /> -->
<div class="flex-grow px-2 authorSearchCardContent h-full"> <div class="flex-grow px-2 authorSearchCardContent h-full">
<p class="truncate text-sm">{{ author }}</p> <p class="truncate text-sm">{{ name }}</p>
</div> </div>
</div> </div>
</template> </template>
@ -10,13 +30,56 @@
<script> <script>
export default { export default {
props: { props: {
author: String author: {
type: Object,
default: () => {}
}
}, },
data() { data() {
return {} return {
showCoverBg: false
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
authorId() {
return this.author.id
},
name() {
return this.author.name
},
imagePath() {
return this.author.imagePath
},
updatedAt() {
return this.author.updatedAt
},
imgSrc() {
if (!this.imagePath) return null
if (process.env.NODE_ENV !== 'production') {
// Testing
return `http://localhost:3333/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
}
return `/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
}
},
methods: {
imageLoaded() {
if (this.$refs.img) {
var { naturalWidth, naturalHeight } = this.$refs.img
var aspectRatio = naturalHeight / naturalWidth
var arDiff = Math.abs(aspectRatio - this.aspectRatio)
if (arDiff > 0.15) {
this.showCoverBg = true
} else {
this.showCoverBg = false
}
}
}
}, },
computed: {},
methods: {},
mounted() {} mounted() {}
} }
</script> </script>

View File

@ -74,7 +74,7 @@ export default {
}, },
groupTo() { groupTo() {
if (this.groupType === 'series') { if (this.groupType === 'series') {
return `/library/${this.currentLibraryId}/series/${this.groupEncode}` return `/library/${this.currentLibraryId}/series/${this._group.id}`
} else if (this.groupType === 'collection') { } else if (this.groupType === 'collection') {
return `/collection/${this._group.id}` return `/collection/${this._group.id}`
} else { } else {
@ -119,7 +119,7 @@ export default {
return `${this.groupType}.${this.$encode(this.groupName)}` return `${this.groupType}.${this.$encode(this.groupName)}`
}, },
hasValidCovers() { hasValidCovers() {
var validCovers = this.bookItems.map((bookItem) => bookItem.book.cover) var validCovers = this.bookItems.map((bookItem) => bookItem.media.coverPath)
return !!validCovers.length return !!validCovers.length
}, },
showExperimentalFeatures() { showExperimentalFeatures() {

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden"> <div class="flex h-full px-1 overflow-hidden">
<covers-group-cover :name="series" :book-items="bookItems" :width="60" :height="60" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-group-cover :name="name" :book-items="bookItems" :width="60" :height="60" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="flex-grow px-2 seriesSearchCardContent h-full"> <div class="flex-grow px-2 seriesSearchCardContent h-full">
<p class="truncate text-sm">{{ series }}</p> <p class="truncate text-sm">{{ name }}</p>
</div> </div>
</div> </div>
</template> </template>
@ -10,7 +10,10 @@
<script> <script>
export default { export default {
props: { props: {
series: String, series: {
type: Object,
default: () => {}
},
bookItems: { bookItems: {
type: Array, type: Array,
default: () => [] default: () => []
@ -22,6 +25,9 @@ export default {
computed: { computed: {
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio'] return this.$store.getters['getBookCoverAspectRatio']
},
name() {
return this.series.name
} }
}, },
methods: {}, methods: {},

View File

@ -20,37 +20,37 @@
</li> </li>
<template v-else> <template v-else>
<p class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p> <p class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p>
<template v-for="item in audiobookResults"> <template v-for="item in bookResults">
<li :key="item.audiobook.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option"> <li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
<nuxt-link :to="`/audiobook/${item.audiobook.id}`"> <nuxt-link :to="`/item/${item.id}`">
<cards-audiobook-search-card :audiobook="item.audiobook" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" /> <cards-audiobook-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Authors</p> <p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Authors</p>
<template v-for="item in authorResults"> <template v-for="item in authorResults">
<li :key="item.author" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option"> <li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.author)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${item.id}`">
<cards-author-search-card :author="item.author" /> <cards-author-search-card :author="item" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
<p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p> <p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p>
<template v-for="item in seriesResults"> <template v-for="item in seriesResults">
<li :key="item.series" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option"> <li :key="item.series.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
<nuxt-link :to="`/library/${currentLibraryId}/series/${$encode(item.series)}`"> <nuxt-link :to="`/library/${currentLibraryId}/series/${item.series.id}`">
<cards-series-search-card :series="item.series" :book-items="item.audiobooks" /> <cards-series-search-card :series="item.series" :book-items="item.books" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</p> <p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</p>
<template v-for="item in tagResults"> <template v-for="item in tagResults">
<li :key="item.tag" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option"> <li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.tag)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
<cards-tag-search-card :tag="item.tag" /> <cards-tag-search-card :tag="item.name" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
@ -70,7 +70,7 @@ export default {
isTyping: false, isTyping: false,
isFetching: false, isFetching: false,
search: null, search: null,
audiobookResults: [], bookResults: [],
authorResults: [], authorResults: [],
seriesResults: [], seriesResults: [],
tagResults: [], tagResults: [],
@ -83,7 +83,7 @@ export default {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
totalResults() { totalResults() {
return this.audiobookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length
} }
}, },
methods: { methods: {
@ -96,7 +96,7 @@ export default {
clearResults() { clearResults() {
this.search = null this.search = null
this.lastSearch = null this.lastSearch = null
this.audiobookResults = [] this.bookResults = []
this.authorResults = [] this.authorResults = []
this.seriesResults = [] this.seriesResults = []
this.tagResults = [] this.tagResults = []
@ -136,7 +136,7 @@ export default {
// Search was canceled // Search was canceled
if (!this.isFetching) return if (!this.isFetching) return
this.audiobookResults = searchResults.audiobooks || [] this.bookResults = searchResults.book || []
this.authorResults = searchResults.authors || [] this.authorResults = searchResults.authors || []
this.seriesResults = searchResults.series || [] this.seriesResults = searchResults.series || []
this.tagResults = searchResults.tags || [] this.tagResults = searchResults.tags || []

View File

@ -151,7 +151,6 @@ export default {
.map((bookItem) => { .map((bookItem) => {
return { return {
id: bookItem.id, id: bookItem.id,
volumeNumber: bookItem.book ? bookItem.book.volumeNumber : null,
coverUrl: this.getCoverUrl(bookItem) coverUrl: this.getCoverUrl(bookItem)
} }
}) })

View File

@ -30,7 +30,7 @@ export default {
return null return null
}) })
results = { results = {
audiobooks: results && results.audiobooks.length ? results.audiobooks : null, books: results && results.book.length ? results.book : null,
authors: results && results.authors.length ? results.authors : null, authors: results && results.authors.length ? results.authors : null,
series: results && results.series.length ? results.series : null, series: results && results.series.length ? results.series : null,
tags: results && results.tags.length ? results.tags : null tags: results && results.tags.length ? results.tags : null
@ -67,7 +67,7 @@ export default {
return null return null
}) })
this.results = { this.results = {
audiobooks: results && results.audiobooks.length ? results.audiobooks : null, books: results && results.book.length ? results.book : null,
authors: results && results.authors.length ? results.authors : null, authors: results && results.authors.length ? results.authors : null,
series: results && results.series.length ? results.series : null, series: results && results.series.length ? results.series : null,
tags: results && results.tags.length ? results.tags : null tags: results && results.tags.length ? results.tags : null

View File

@ -355,65 +355,62 @@ class LibraryController {
// GET: Global library search // GET: Global library search
search(req, res) { search(req, res) {
var library = req.library
if (!req.query.q) { if (!req.query.q) {
return res.status(400).send('No query string') return res.status(400).send('No query string')
} }
var libraryItems = req.libraryItems
var maxResults = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12 var maxResults = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
var bookMatches = [] var itemMatches = []
var authorMatches = {} var authorMatches = {}
var seriesMatches = {} var seriesMatches = {}
var tagMatches = {} var tagMatches = {}
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id) libraryItems.forEach((li) => {
audiobooksInLibrary.forEach((ab) => { var queryResult = li.searchQuery(req.query.q)
var queryResult = ab.searchQuery(req.query.q) if (queryResult.matchKey) {
if (queryResult.book) { itemMatches.push({
var bookMatchObj = { libraryItem: li,
audiobook: ab.toJSONExpanded(), matchKey: queryResult.matchKey,
matchKey: queryResult.book, matchText: queryResult.matchText
matchText: queryResult.bookMatchText })
}
bookMatches.push(bookMatchObj)
} }
if (queryResult.authors) { if (queryResult.series && queryResult.series.length) {
queryResult.authors.forEach((author) => { queryResult.series.forEach((se) => {
if (!authorMatches[author]) { if (!seriesMatches[se.id]) {
authorMatches[author] = { var _series = this.db.series.find(_se => _se.id === se.id)
author: author, if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] }
numBooks: 1
}
} else { } else {
authorMatches[author].numBooks++ seriesMatches[se.id].books.push(li.toJSON())
} }
}) })
} }
if (queryResult.series) { if (queryResult.authors && queryResult.authors.length) {
if (!seriesMatches[queryResult.series]) { queryResult.authors.forEach((au) => {
seriesMatches[queryResult.series] = { if (!authorMatches[au.id]) {
series: queryResult.series, var _author = this.db.authors.find(_au => _au.id === au.id)
audiobooks: [ab.toJSONExpanded()] if (_author) {
authorMatches[au.id] = _author.toJSON()
authorMatches[au.id].numBooks = 1
}
} else {
authorMatches[au.id].numBooks++
} }
} else { })
seriesMatches[queryResult.series].audiobooks.push(ab.toJSONExpanded())
}
} }
if (queryResult.tags && queryResult.tags.length) { if (queryResult.tags && queryResult.tags.length) {
queryResult.tags.forEach((tag) => { queryResult.tags.forEach((tag) => {
if (!tagMatches[tag]) { if (!tagMatches[tag]) {
tagMatches[tag] = { tagMatches[tag] = { name: tag, books: [li.toJSON()] }
tag,
audiobooks: [ab.toJSONExpanded()]
}
} else { } else {
tagMatches[tag].audiobooks.push(ab.toJSONExpanded()) tagMatches[tag].books.push(li.toJSON())
} }
}) })
} }
}) })
var itemKey = req.library.itemMediaType
var results = { var results = {
audiobooks: bookMatches.slice(0, maxResults), [itemKey]: itemMatches.slice(0, maxResults),
tags: Object.values(tagMatches).slice(0, maxResults), tags: Object.values(tagMatches).slice(0, maxResults),
authors: Object.values(authorMatches).slice(0, maxResults), authors: Object.values(authorMatches).slice(0, maxResults),
series: Object.values(seriesMatches).slice(0, maxResults) series: Object.values(seriesMatches).slice(0, maxResults)

View File

@ -25,6 +25,9 @@ class Library {
get folderPaths() { get folderPaths() {
return this.folders.map(f => f.fullPath) return this.folders.map(f => f.fullPath)
} }
get itemMediaType() {
return this.mediaType === 'podcast' ? 'podcast' : 'book'
}
construct(library) { construct(library) {
this.id = library.id this.id = library.id

View File

@ -401,5 +401,10 @@ class LibraryItem {
} }
return hasUpdated return hasUpdated
} }
searchQuery(query) {
query = query.toLowerCase()
return this.media.searchQuery(query)
}
} }
module.exports = LibraryItem module.exports = LibraryItem

View File

@ -313,5 +313,33 @@ class Book {
} }
return false return false
} }
searchQuery(query) {
var payload = {
tags: this.tags.filter(t => t.toLowerCase().includes(query)),
series: this.metadata.searchSeries(query),
authors: this.metadata.searchAuthors(query),
matchKey: null,
matchText: null
}
var metadataMatch = this.metadata.searchQuery(query)
if (metadataMatch) {
payload.matchKey = metadataMatch.matchKey
payload.matchText = metadataMatch.matchText
} else {
if (payload.authors.length) {
payload.matchKey = 'authors'
payload.matchText = this.metadata.authorName
} else if (payload.series.length) {
payload.matchKey = 'series'
payload.matchText = this.metadata.seriesName
}
else if (payload.tags.length) {
payload.matchKey = 'tags'
payload.matchText = this.tags.join(', ')
}
}
return payload
}
} }
module.exports = Book module.exports = Book

View File

@ -124,5 +124,10 @@ class Podcast {
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) { async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
return false return false
} }
searchQuery(query) {
var payload = this.metadata.searchQuery(query)
return payload || {}
}
} }
module.exports = Podcast module.exports = Podcast

View File

@ -91,6 +91,13 @@ class BookMetadata {
if (!this.authors.length) return '' if (!this.authors.length) return ''
return this.authors.map(au => au.name).join(', ') return this.authors.map(au => au.name).join(', ')
} }
get seriesName() {
if (!this.series.length) return ''
return this.series.map(se => {
if (!se.sequence) return se.name
return `${se.name} #${se.sequence}`
}).join(', ')
}
get narratorName() { get narratorName() {
return this.narrators.join(', ') return this.narrators.join(', ')
} }
@ -268,5 +275,24 @@ class BookMetadata {
sequence: sequenceTag || '' sequence: sequenceTag || ''
}] }]
} }
searchSeries(query) {
return this.series.filter(se => se.name.toLowerCase().includes(query))
}
searchAuthors(query) {
return this.authors.filter(se => se.name.toLowerCase().includes(query))
}
searchQuery(query) { // Returns key if match is found
var keysToCheck = ['title', 'asin', 'isbn']
for (var key of keysToCheck) {
if (this[key] && this[key].toLowerCase().includes(query)) {
return {
matchKey: key,
matchText: this[key]
}
}
}
return null
}
} }
module.exports = BookMetadata module.exports = BookMetadata

View File

@ -47,5 +47,18 @@ class PodcastMetadata {
toJSONExpanded() { toJSONExpanded() {
return this.toJSON() return this.toJSON()
} }
searchQuery(query) { // Returns key if match is found
var keysToCheck = ['title', 'artist', 'itunesId', 'itunesArtistId']
for (var key of keysToCheck) {
if (this[key] && String(this[key]).toLowerCase().includes(query)) {
return {
matchKey: key,
matchText: this[key]
}
}
}
return null
}
} }
module.exports = PodcastMetadata module.exports = PodcastMetadata