mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-22 00:07:52 +01:00
Update package-lock
This commit is contained in:
commit
56c574c928
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,11 +7,12 @@
|
||||
/podcasts/
|
||||
/media/
|
||||
/metadata/
|
||||
test/
|
||||
/client/.nuxt/
|
||||
/client/dist/
|
||||
/dist/
|
||||
/deploy/
|
||||
/coverage/
|
||||
/.nyc_output/
|
||||
|
||||
sw.*
|
||||
.DS_STORE
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -16,5 +16,6 @@
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.detectIndentation": true,
|
||||
"editor.tabSize": 2
|
||||
"editor.tabSize": 2,
|
||||
"javascript.format.semicolons": "remove"
|
||||
}
|
@ -36,7 +36,7 @@
|
||||
</svg>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p class="text-sm">{{ $strings.ButtonSearch }}</p>
|
||||
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
||||
|
@ -82,7 +82,7 @@
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="abs-icons icon-podcast text-xl"></span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSearch }}</p>
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAdd }}</p>
|
||||
|
||||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
4490
package-lock.json
generated
4490
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -15,7 +15,9 @@
|
||||
"docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local",
|
||||
"docker-arm64-local": "docker buildx build --platform linux/arm64 --load . -t advplyr/audiobookshelf-arm64-local",
|
||||
"docker-armv7-local": "docker buildx build --platform linux/arm/v7 --load . -t advplyr/audiobookshelf-armv7-local",
|
||||
"deploy-linux": "node deploy/linux"
|
||||
"deploy-linux": "node deploy/linux",
|
||||
"test": "mocha",
|
||||
"coverage": "nyc mocha"
|
||||
},
|
||||
"bin": "prod.js",
|
||||
"pkg": {
|
||||
@ -28,6 +30,9 @@
|
||||
"server/**/*.js"
|
||||
]
|
||||
},
|
||||
"mocha": {
|
||||
"recursive": true
|
||||
},
|
||||
"author": "advplyr",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
@ -49,6 +54,10 @@
|
||||
"xml2js": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.20"
|
||||
"chai": "^4.3.10",
|
||||
"mocha": "^10.2.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"nyc": "^15.1.0",
|
||||
"sinon": "^17.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -31,52 +31,11 @@ class BookFinder {
|
||||
return book
|
||||
}
|
||||
|
||||
stripSubtitle(title) {
|
||||
if (title.includes(':')) {
|
||||
return title.split(':')[0].trim()
|
||||
} else if (title.includes(' - ')) {
|
||||
return title.split(' - ')[0].trim()
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
replaceAccentedChars(str) {
|
||||
try {
|
||||
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
|
||||
} catch (error) {
|
||||
Logger.error('[BookFinder] str normalize error', error)
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
cleanTitleForCompares(title) {
|
||||
if (!title) return ''
|
||||
// Remove subtitle if there (i.e. "Cool Book: Coolest Ever" becomes "Cool Book")
|
||||
let stripped = this.stripSubtitle(title)
|
||||
|
||||
// Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game")
|
||||
let cleaned = stripped.replace(/ *\([^)]*\) */g, "")
|
||||
|
||||
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
|
||||
cleaned = cleaned.replace(/'/g, '')
|
||||
return this.replaceAccentedChars(cleaned).toLowerCase()
|
||||
}
|
||||
|
||||
cleanAuthorForCompares(author) {
|
||||
if (!author) return ''
|
||||
let cleanAuthor = this.replaceAccentedChars(author).toLowerCase()
|
||||
// separate initials
|
||||
cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2')
|
||||
// remove middle initials
|
||||
cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '')
|
||||
return cleanAuthor
|
||||
}
|
||||
|
||||
filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) {
|
||||
var searchTitle = this.cleanTitleForCompares(title)
|
||||
var searchAuthor = this.cleanAuthorForCompares(author)
|
||||
var searchTitle = cleanTitleForCompares(title)
|
||||
var searchAuthor = cleanAuthorForCompares(author)
|
||||
return books.map(b => {
|
||||
b.cleanedTitle = this.cleanTitleForCompares(b.title)
|
||||
b.cleanedTitle = cleanTitleForCompares(b.title)
|
||||
b.titleDistance = levenshteinDistance(b.cleanedTitle, title)
|
||||
|
||||
// Total length of search (title or both title & author)
|
||||
@ -87,7 +46,7 @@ class BookFinder {
|
||||
b.authorDistance = author.length
|
||||
} else {
|
||||
b.totalPossibleDistance += b.author.length
|
||||
b.cleanedAuthor = this.cleanAuthorForCompares(b.author)
|
||||
b.cleanedAuthor = cleanAuthorForCompares(b.author)
|
||||
|
||||
var cleanedAuthorDistance = levenshteinDistance(b.cleanedAuthor, searchAuthor)
|
||||
var authorDistance = levenshteinDistance(b.author || '', author)
|
||||
@ -190,20 +149,17 @@ class BookFinder {
|
||||
|
||||
static TitleCandidates = class {
|
||||
|
||||
constructor(bookFinder, cleanAuthor) {
|
||||
this.bookFinder = bookFinder
|
||||
constructor(cleanAuthor) {
|
||||
this.candidates = new Set()
|
||||
this.cleanAuthor = cleanAuthor
|
||||
this.priorities = {}
|
||||
this.positions = {}
|
||||
this.currentPosition = 0
|
||||
}
|
||||
|
||||
add(title, position = 0) {
|
||||
add(title) {
|
||||
// if title contains the author, remove it
|
||||
if (this.cleanAuthor) {
|
||||
const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g")
|
||||
title = this.bookFinder.cleanAuthorForCompares(title).replace(authorRe, '').trim()
|
||||
}
|
||||
title = this.#removeAuthorFromTitle(title)
|
||||
|
||||
const titleTransformers = [
|
||||
[/([,:;_]| by ).*/g, ''], // Remove subtitle
|
||||
@ -215,11 +171,11 @@ class BookFinder {
|
||||
]
|
||||
|
||||
// Main variant
|
||||
const cleanTitle = this.bookFinder.cleanTitleForCompares(title).trim()
|
||||
const cleanTitle = cleanTitleForCompares(title).trim()
|
||||
if (!cleanTitle) return
|
||||
this.candidates.add(cleanTitle)
|
||||
this.priorities[cleanTitle] = 0
|
||||
this.positions[cleanTitle] = position
|
||||
this.positions[cleanTitle] = this.currentPosition
|
||||
|
||||
let candidate = cleanTitle
|
||||
|
||||
@ -230,10 +186,11 @@ class BookFinder {
|
||||
if (candidate) {
|
||||
this.candidates.add(candidate)
|
||||
this.priorities[candidate] = 0
|
||||
this.positions[candidate] = position
|
||||
this.positions[candidate] = this.currentPosition
|
||||
}
|
||||
this.priorities[cleanTitle] = 1
|
||||
}
|
||||
this.currentPosition++
|
||||
}
|
||||
|
||||
get size() {
|
||||
@ -243,23 +200,16 @@ class BookFinder {
|
||||
getCandidates() {
|
||||
var candidates = [...this.candidates]
|
||||
candidates.sort((a, b) => {
|
||||
// Candidates that include the author are likely low quality
|
||||
const includesAuthorDiff = !b.includes(this.cleanAuthor) - !a.includes(this.cleanAuthor)
|
||||
if (includesAuthorDiff) return includesAuthorDiff
|
||||
// Candidates that include only digits are also likely low quality
|
||||
const onlyDigits = /^\d+$/
|
||||
const includesOnlyDigitsDiff = !onlyDigits.test(b) - !onlyDigits.test(a)
|
||||
const includesOnlyDigitsDiff = onlyDigits.test(a) - onlyDigits.test(b)
|
||||
if (includesOnlyDigitsDiff) return includesOnlyDigitsDiff
|
||||
// transformed candidates receive higher priority
|
||||
const priorityDiff = this.priorities[a] - this.priorities[b]
|
||||
if (priorityDiff) return priorityDiff
|
||||
// if same priorirty, prefer candidates that are closer to the beginning (e.g. titles before subtitles)
|
||||
const positionDiff = this.positions[a] - this.positions[b]
|
||||
if (positionDiff) return positionDiff
|
||||
// Start with longer candidaets, as they are likely more specific
|
||||
const lengthDiff = b.length - a.length
|
||||
if (lengthDiff) return lengthDiff
|
||||
return b.localeCompare(a)
|
||||
return positionDiff // candidates with same priority always have different positions
|
||||
})
|
||||
Logger.debug(`[${this.constructor.name}] Found ${candidates.length} fuzzy title candidates`)
|
||||
Logger.debug(candidates)
|
||||
@ -269,21 +219,32 @@ class BookFinder {
|
||||
delete(title) {
|
||||
return this.candidates.delete(title)
|
||||
}
|
||||
|
||||
#removeAuthorFromTitle(title) {
|
||||
if (!this.cleanAuthor) return title
|
||||
const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g")
|
||||
const authorCleanedTitle = cleanAuthorForCompares(title)
|
||||
const authorCleanedTitleWithoutAuthor = authorCleanedTitle.replace(authorRe, '')
|
||||
if (authorCleanedTitleWithoutAuthor !== authorCleanedTitle) {
|
||||
return authorCleanedTitleWithoutAuthor.trim()
|
||||
}
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
static AuthorCandidates = class {
|
||||
constructor(bookFinder, cleanAuthor) {
|
||||
this.bookFinder = bookFinder
|
||||
constructor(cleanAuthor, audnexus) {
|
||||
this.audnexus = audnexus
|
||||
this.candidates = new Set()
|
||||
this.cleanAuthor = cleanAuthor
|
||||
if (cleanAuthor) this.candidates.add(cleanAuthor)
|
||||
}
|
||||
|
||||
validateAuthor(name, region = '', maxLevenshtein = 2) {
|
||||
return this.bookFinder.audnexus.authorASINsRequest(name, region).then((asins) => {
|
||||
return this.audnexus.authorASINsRequest(name, region).then((asins) => {
|
||||
for (const [i, asin] of asins.entries()) {
|
||||
if (i > 10) break
|
||||
let cleanName = this.bookFinder.cleanAuthorForCompares(asin.name)
|
||||
let cleanName = cleanAuthorForCompares(asin.name)
|
||||
if (!cleanName) continue
|
||||
if (cleanName.includes(name)) return name
|
||||
if (name.includes(cleanName)) return cleanName
|
||||
@ -294,7 +255,7 @@ class BookFinder {
|
||||
}
|
||||
|
||||
add(author) {
|
||||
const cleanAuthor = this.bookFinder.cleanAuthorForCompares(author).trim()
|
||||
const cleanAuthor = cleanAuthorForCompares(author).trim()
|
||||
if (!cleanAuthor) return
|
||||
this.candidates.add(cleanAuthor)
|
||||
}
|
||||
@ -362,10 +323,10 @@ class BookFinder {
|
||||
title = title.trim().toLowerCase()
|
||||
author = author?.trim().toLowerCase() || ''
|
||||
|
||||
const cleanAuthor = this.cleanAuthorForCompares(author)
|
||||
const cleanAuthor = cleanAuthorForCompares(author)
|
||||
|
||||
// Now run up to maxFuzzySearches fuzzy searches
|
||||
let authorCandidates = new BookFinder.AuthorCandidates(this, cleanAuthor)
|
||||
let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus)
|
||||
|
||||
// Remove underscores and parentheses with their contents, and replace with a separator
|
||||
const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, " - ")
|
||||
@ -375,9 +336,9 @@ class BookFinder {
|
||||
authorCandidates.add(titlePart)
|
||||
authorCandidates = await authorCandidates.getCandidates()
|
||||
for (const authorCandidate of authorCandidates) {
|
||||
let titleCandidates = new BookFinder.TitleCandidates(this, authorCandidate)
|
||||
for (const [position, titlePart] of titleParts.entries())
|
||||
titleCandidates.add(titlePart, position)
|
||||
let titleCandidates = new BookFinder.TitleCandidates(authorCandidate)
|
||||
for (const titlePart of titleParts)
|
||||
titleCandidates.add(titlePart)
|
||||
titleCandidates = titleCandidates.getCandidates()
|
||||
for (const titleCandidate of titleCandidates) {
|
||||
if (titleCandidate == title && authorCandidate == author) continue // We already tried this
|
||||
@ -457,3 +418,52 @@ class BookFinder {
|
||||
}
|
||||
}
|
||||
module.exports = new BookFinder()
|
||||
|
||||
function stripSubtitle(title) {
|
||||
if (title.includes(':')) {
|
||||
return title.split(':')[0].trim()
|
||||
} else if (title.includes(' - ')) {
|
||||
return title.split(' - ')[0].trim()
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
function replaceAccentedChars(str) {
|
||||
try {
|
||||
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
|
||||
} catch (error) {
|
||||
Logger.error('[BookFinder] str normalize error', error)
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
function cleanTitleForCompares(title) {
|
||||
if (!title) return ''
|
||||
title = stripRedundantSpaces(title)
|
||||
|
||||
// Remove subtitle if there (i.e. "Cool Book: Coolest Ever" becomes "Cool Book")
|
||||
let stripped = stripSubtitle(title)
|
||||
|
||||
// Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game")
|
||||
let cleaned = stripped.replace(/ *\([^)]*\) */g, "")
|
||||
|
||||
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
|
||||
cleaned = cleaned.replace(/'/g, '')
|
||||
return replaceAccentedChars(cleaned).toLowerCase()
|
||||
}
|
||||
|
||||
function cleanAuthorForCompares(author) {
|
||||
if (!author) return ''
|
||||
author = stripRedundantSpaces(author)
|
||||
|
||||
let cleanAuthor = replaceAccentedChars(author).toLowerCase()
|
||||
// separate initials
|
||||
cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2')
|
||||
// remove middle initials
|
||||
cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '')
|
||||
return cleanAuthor
|
||||
}
|
||||
|
||||
function stripRedundantSpaces(str) {
|
||||
return str.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
344
test/server/finders/BookFinder.test.js
Normal file
344
test/server/finders/BookFinder.test.js
Normal file
@ -0,0 +1,344 @@
|
||||
const sinon = require('sinon')
|
||||
const chai = require('chai')
|
||||
const expect = chai.expect
|
||||
const bookFinder = require('../../../server/finders/BookFinder')
|
||||
const { LogLevel } = require('../../../server/utils/constants')
|
||||
const Logger = require('../../../server/Logger')
|
||||
Logger.setLogLevel(LogLevel.INFO)
|
||||
|
||||
describe('TitleCandidates', () => {
|
||||
describe('cleanAuthor non-empty', () => {
|
||||
let titleCandidates
|
||||
const cleanAuthor = 'leo tolstoy'
|
||||
|
||||
beforeEach(() => {
|
||||
titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor)
|
||||
})
|
||||
|
||||
describe('no adds', () => {
|
||||
it('returns no candidates', () => {
|
||||
expect(titleCandidates.getCandidates()).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('single add', () => {
|
||||
[
|
||||
['adds candidate', 'anna karenina', ['anna karenina']],
|
||||
['adds lowercased candidate', 'ANNA KARENINA', ['anna karenina']],
|
||||
['adds candidate, removing redundant spaces', 'anna karenina', ['anna karenina']],
|
||||
['adds candidate, removing author', `anna karenina by ${cleanAuthor}`, ['anna karenina']],
|
||||
['does not add empty candidate after removing author', cleanAuthor, []],
|
||||
['adds candidate, removing subtitle', 'anna karenina: subtitle', ['anna karenina']],
|
||||
['adds candidate + variant, removing "by ..."', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']],
|
||||
['adds candidate + variant, removing bitrate', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']],
|
||||
['adds candidate + variant, removing edition 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']],
|
||||
['adds candidate + variant, removing edition 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']],
|
||||
['adds candidate + variant, removing fie type', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']],
|
||||
['adds candidate + variant, removing "a novel"', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']],
|
||||
['adds candidate + variant, removing preceding/trailing numbers', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']],
|
||||
['does not add empty candidate', '', []],
|
||||
['does not add spaces-only candidate', ' ', []],
|
||||
['does not add empty variant', '1984', ['1984']],
|
||||
].forEach(([name, title, expected]) => it(name, () => {
|
||||
titleCandidates.add(title)
|
||||
expect(titleCandidates.getCandidates()).to.deep.equal(expected)
|
||||
}))
|
||||
})
|
||||
|
||||
describe('multiple adds', () => {
|
||||
[
|
||||
['demotes digits-only candidates', ['01', 'anna karenina'], ['anna karenina', '01']],
|
||||
['promotes transformed variants', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']],
|
||||
['orders by position', ['title2', 'title1'], ['title2', 'title1']],
|
||||
['dedupes candidates', ['title1', 'title1'], ['title1']],
|
||||
].forEach(([name, titles, expected]) => it(name, () => {
|
||||
for (const title of titles) titleCandidates.add(title)
|
||||
expect(titleCandidates.getCandidates()).to.deep.equal(expected)
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanAuthor empty', () => {
|
||||
let titleCandidates
|
||||
let cleanAuthor = ''
|
||||
|
||||
beforeEach(() => {
|
||||
titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor)
|
||||
})
|
||||
|
||||
describe('single add', () => {
|
||||
[
|
||||
['adds a candidate', 'leo tolstoy', ['leo tolstoy']],
|
||||
].forEach(([name, title, expected]) => it(name, () => {
|
||||
titleCandidates.add(title)
|
||||
expect(titleCandidates.getCandidates()).to.deep.equal(expected)
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('AuthorCandidates', () => {
|
||||
let authorCandidates
|
||||
const audnexus = {
|
||||
authorASINsRequest: sinon.stub().resolves([
|
||||
{ name: 'Leo Tolstoy' },
|
||||
{ name: 'Nikolai Gogol' },
|
||||
{ name: 'J. K. Rowling' },
|
||||
]),
|
||||
}
|
||||
|
||||
describe('cleanAuthor is null', () => {
|
||||
beforeEach(() => {
|
||||
authorCandidates = new bookFinder.constructor.AuthorCandidates(null, audnexus)
|
||||
})
|
||||
|
||||
describe('no adds', () => {
|
||||
[
|
||||
['returns empty author candidate', []],
|
||||
].forEach(([name, expected]) => it(name, async () => {
|
||||
expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
|
||||
}))
|
||||
})
|
||||
|
||||
describe('single add', () => {
|
||||
[
|
||||
['adds recognized candidate', 'nikolai gogol', ['nikolai gogol']],
|
||||
['does not add unrecognized candidate', 'fyodor dostoevsky', []],
|
||||
['adds recognized author if candidate is a superstring', 'dr. nikolai gogol', ['nikolai gogol']],
|
||||
['adds candidate if it is a substring of recognized author', 'gogol', ['gogol']],
|
||||
['adds recognized author if edit distance from candidate is small', 'nicolai gogol', ['nikolai gogol']],
|
||||
['does not add candidate if edit distance from any recognized author is large', 'nikolai google', []],
|
||||
['adds normalized recognized candidate (contains redundant spaces)', 'nikolai gogol', ['nikolai gogol']],
|
||||
['adds normalized recognized candidate (normalized initials)', 'j.k. rowling', ['j. k. rowling']],
|
||||
].forEach(([name, author, expected]) => it(name, async () => {
|
||||
authorCandidates.add(author)
|
||||
expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
|
||||
}))
|
||||
})
|
||||
|
||||
describe('multi add', () => {
|
||||
[
|
||||
['adds recognized author candidates', ['nikolai gogol', 'leo tolstoy'], ['nikolai gogol', 'leo tolstoy']],
|
||||
['dedupes author candidates', ['nikolai gogol', 'nikolai gogol'], ['nikolai gogol']],
|
||||
].forEach(([name, authors, expected]) => it(name, async () => {
|
||||
for (const author of authors) authorCandidates.add(author)
|
||||
expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanAuthor is a recognized author', () => {
|
||||
const cleanAuthor = 'leo tolstoy'
|
||||
|
||||
beforeEach(() => {
|
||||
authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus)
|
||||
})
|
||||
|
||||
describe('no adds', () => {
|
||||
[
|
||||
['adds cleanAuthor as candidate', [cleanAuthor]],
|
||||
].forEach(([name, expected]) => it(name, async () => {
|
||||
expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
|
||||
}))
|
||||
})
|
||||
|
||||
describe('single add', () => {
|
||||
[
|
||||
['adds recognized candidate', 'nikolai gogol', [cleanAuthor, 'nikolai gogol']],
|
||||
['does not add candidate if it is a dupe of cleanAuthor', cleanAuthor, [cleanAuthor]],
|
||||
].forEach(([name, author, expected]) => it(name, async () => {
|
||||
authorCandidates.add(author)
|
||||
expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanAuthor is an unrecognized author', () => {
|
||||
const cleanAuthor = 'Fyodor Dostoevsky'
|
||||
|
||||
beforeEach(() => {
|
||||
authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus)
|
||||
})
|
||||
|
||||
describe('no adds', () => {
|
||||
[
|
||||
['adds cleanAuthor as candidate', [cleanAuthor]],
|
||||
].forEach(([name, expected]) => it(name, async () => {
|
||||
expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
|
||||
}))
|
||||
})
|
||||
|
||||
describe('single add', () => {
|
||||
[
|
||||
['adds recognized candidate and removes cleanAuthor', 'nikolai gogol', ['nikolai gogol']],
|
||||
['does not add unrecognized candidate', 'jackie chan', [cleanAuthor]],
|
||||
].forEach(([name, author, expected]) => it(name, async () => {
|
||||
authorCandidates.add(author)
|
||||
expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanAuthor is unrecognized and dirty', () => {
|
||||
describe('no adds', () => {
|
||||
[
|
||||
['adds aggressively cleaned cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', ['fyodor dostoevsky']],
|
||||
['adds cleanAuthor if aggresively cleaned cleanAuthor is empty', ', jackie chan', [', jackie chan']],
|
||||
].forEach(([name, cleanAuthor, expected]) => it(name, async () => {
|
||||
authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus)
|
||||
expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
|
||||
}))
|
||||
})
|
||||
|
||||
describe('single add', () => {
|
||||
[
|
||||
['adds recognized candidate and removes cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', 'nikolai gogol', ['nikolai gogol']],
|
||||
].forEach(([name, cleanAuthor, author, expected]) => it(name, async () => {
|
||||
authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus)
|
||||
authorCandidates.add(author)
|
||||
expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, ''])
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('search', () => {
|
||||
const t = 'title'
|
||||
const a = 'author'
|
||||
const u = 'unrecognized'
|
||||
const r = ['book']
|
||||
|
||||
const runSearchStub = sinon.stub(bookFinder, 'runSearch')
|
||||
runSearchStub.resolves([])
|
||||
runSearchStub.withArgs(t, a).resolves(r)
|
||||
runSearchStub.withArgs(t, u).resolves(r)
|
||||
|
||||
const audnexusStub = sinon.stub(bookFinder.audnexus, 'authorASINsRequest')
|
||||
audnexusStub.resolves([{ name: a }])
|
||||
|
||||
beforeEach(() => {
|
||||
bookFinder.runSearch.resetHistory()
|
||||
})
|
||||
|
||||
describe('search title is empty', () => {
|
||||
it('returns empty result', async () => {
|
||||
expect(await bookFinder.search('', '', a)).to.deep.equal([])
|
||||
sinon.assert.callCount(bookFinder.runSearch, 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('search title is a recognized title and search author is a recognized author', () => {
|
||||
it('returns non-empty result (no fuzzy searches)', async () => {
|
||||
expect(await bookFinder.search('', t, a)).to.deep.equal(r)
|
||||
sinon.assert.callCount(bookFinder.runSearch, 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('search title contains recognized title and search author is a recognized author', () => {
|
||||
[
|
||||
[`${t} -`],
|
||||
[`${t} - ${a}`],
|
||||
[`${a} - ${t}`],
|
||||
[`${t}- ${a}`],
|
||||
[`${t} -${a}`],
|
||||
[`${t} ${a}`],
|
||||
[`${a} - ${t} (unabridged)`],
|
||||
[`${a} - ${t} (subtitle) - mp3`],
|
||||
[`${t} {narrator} - series-01 64kbps 10:00:00`],
|
||||
[`${a} - ${t} (2006) narrated by narrator [unabridged]`],
|
||||
[`${t} - ${a} 2022 mp3`],
|
||||
[`01 ${t}`],
|
||||
[`2022_${t}_HQ`],
|
||||
].forEach(([searchTitle]) => {
|
||||
it(`search('${searchTitle}', '${a}') returns non-empty result (with 1 fuzzy search)`, async () => {
|
||||
expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r)
|
||||
sinon.assert.callCount(bookFinder.runSearch, 2)
|
||||
})
|
||||
});
|
||||
|
||||
[
|
||||
[`s-01 - ${t} (narrator) 64kbps 10:00:00`],
|
||||
[`${a} - series 01 - ${t}`],
|
||||
].forEach(([searchTitle]) => {
|
||||
it(`search('${searchTitle}', '${a}') returns non-empty result (with 2 fuzzy searches)`, async () => {
|
||||
expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r)
|
||||
sinon.assert.callCount(bookFinder.runSearch, 3)
|
||||
})
|
||||
});
|
||||
|
||||
[
|
||||
[`${t}-${a}`],
|
||||
[`${t} junk`],
|
||||
].forEach(([searchTitle]) => {
|
||||
it(`search('${searchTitle}', '${a}') returns an empty result`, async () => {
|
||||
expect(await bookFinder.search('', searchTitle, a)).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('maxFuzzySearches = 0', () => {
|
||||
[
|
||||
[`${t} - ${a}`],
|
||||
].forEach(([searchTitle]) => {
|
||||
it(`search('${searchTitle}', '${a}') returns an empty result (with no fuzzy searches)`, async () => {
|
||||
expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 0 })).to.deep.equal([])
|
||||
sinon.assert.callCount(bookFinder.runSearch, 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('maxFuzzySearches = 1', () => {
|
||||
[
|
||||
[`s-01 - ${t} (narrator) 64kbps 10:00:00`],
|
||||
[`${a} - series 01 - ${t}`],
|
||||
].forEach(([searchTitle]) => {
|
||||
it(`search('${searchTitle}', '${a}') returns an empty result (1 fuzzy search)`, async () => {
|
||||
expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 1 })).to.deep.equal([])
|
||||
sinon.assert.callCount(bookFinder.runSearch, 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('search title contains recognized title and search author is empty', () => {
|
||||
[
|
||||
[`${t} - ${a}`],
|
||||
[`${a} - ${t}`],
|
||||
].forEach(([searchTitle]) => {
|
||||
it(`search('${searchTitle}', '') returns a non-empty result (1 fuzzy search)`, async () => {
|
||||
expect(await bookFinder.search('', searchTitle, '')).to.deep.equal(r)
|
||||
sinon.assert.callCount(bookFinder.runSearch, 2)
|
||||
})
|
||||
});
|
||||
|
||||
[
|
||||
[`${t}`],
|
||||
[`${t} - ${u}`],
|
||||
[`${u} - ${t}`]
|
||||
].forEach(([searchTitle]) => {
|
||||
it(`search('${searchTitle}', '') returns an empty result`, async () => {
|
||||
expect(await bookFinder.search('', searchTitle, '')).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('search title contains recognized title and search author is an unrecognized author', () => {
|
||||
[
|
||||
[`${t} - ${u}`],
|
||||
[`${u} - ${t}`]
|
||||
].forEach(([searchTitle]) => {
|
||||
it(`search('${searchTitle}', '${u}') returns a non-empty result (1 fuzzy search)`, async () => {
|
||||
expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r)
|
||||
sinon.assert.callCount(bookFinder.runSearch, 2)
|
||||
})
|
||||
});
|
||||
|
||||
[
|
||||
[`${t}`]
|
||||
].forEach(([searchTitle]) => {
|
||||
it(`search('${searchTitle}', '${u}') returns a non-empty result (no fuzzy search)`, async () => {
|
||||
expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r)
|
||||
sinon.assert.callCount(bookFinder.runSearch, 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user