mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-01 00:18:14 +01:00
Update package-lock
This commit is contained in:
commit
56c574c928
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,11 +7,12 @@
|
|||||||
/podcasts/
|
/podcasts/
|
||||||
/media/
|
/media/
|
||||||
/metadata/
|
/metadata/
|
||||||
test/
|
|
||||||
/client/.nuxt/
|
/client/.nuxt/
|
||||||
/client/dist/
|
/client/dist/
|
||||||
/dist/
|
/dist/
|
||||||
/deploy/
|
/deploy/
|
||||||
|
/coverage/
|
||||||
|
/.nyc_output/
|
||||||
|
|
||||||
sw.*
|
sw.*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -16,5 +16,6 @@
|
|||||||
},
|
},
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.detectIndentation": true,
|
"editor.detectIndentation": true,
|
||||||
"editor.tabSize": 2
|
"editor.tabSize": 2,
|
||||||
|
"javascript.format.semicolons": "remove"
|
||||||
}
|
}
|
@ -36,7 +36,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</nuxt-link>
|
</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'">
|
<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>
|
</nuxt-link>
|
||||||
</div>
|
</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">
|
<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'">
|
<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>
|
<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" />
|
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
4488
package-lock.json
generated
4488
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-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-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",
|
"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",
|
"bin": "prod.js",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
@ -28,6 +30,9 @@
|
|||||||
"server/**/*.js"
|
"server/**/*.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"mocha": {
|
||||||
|
"recursive": true
|
||||||
|
},
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -49,6 +54,10 @@
|
|||||||
"xml2js": "^0.5.0"
|
"xml2js": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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
|
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) {
|
filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) {
|
||||||
var searchTitle = this.cleanTitleForCompares(title)
|
var searchTitle = cleanTitleForCompares(title)
|
||||||
var searchAuthor = this.cleanAuthorForCompares(author)
|
var searchAuthor = cleanAuthorForCompares(author)
|
||||||
return books.map(b => {
|
return books.map(b => {
|
||||||
b.cleanedTitle = this.cleanTitleForCompares(b.title)
|
b.cleanedTitle = cleanTitleForCompares(b.title)
|
||||||
b.titleDistance = levenshteinDistance(b.cleanedTitle, title)
|
b.titleDistance = levenshteinDistance(b.cleanedTitle, title)
|
||||||
|
|
||||||
// Total length of search (title or both title & author)
|
// Total length of search (title or both title & author)
|
||||||
@ -87,7 +46,7 @@ class BookFinder {
|
|||||||
b.authorDistance = author.length
|
b.authorDistance = author.length
|
||||||
} else {
|
} else {
|
||||||
b.totalPossibleDistance += b.author.length
|
b.totalPossibleDistance += b.author.length
|
||||||
b.cleanedAuthor = this.cleanAuthorForCompares(b.author)
|
b.cleanedAuthor = cleanAuthorForCompares(b.author)
|
||||||
|
|
||||||
var cleanedAuthorDistance = levenshteinDistance(b.cleanedAuthor, searchAuthor)
|
var cleanedAuthorDistance = levenshteinDistance(b.cleanedAuthor, searchAuthor)
|
||||||
var authorDistance = levenshteinDistance(b.author || '', author)
|
var authorDistance = levenshteinDistance(b.author || '', author)
|
||||||
@ -190,20 +149,17 @@ class BookFinder {
|
|||||||
|
|
||||||
static TitleCandidates = class {
|
static TitleCandidates = class {
|
||||||
|
|
||||||
constructor(bookFinder, cleanAuthor) {
|
constructor(cleanAuthor) {
|
||||||
this.bookFinder = bookFinder
|
|
||||||
this.candidates = new Set()
|
this.candidates = new Set()
|
||||||
this.cleanAuthor = cleanAuthor
|
this.cleanAuthor = cleanAuthor
|
||||||
this.priorities = {}
|
this.priorities = {}
|
||||||
this.positions = {}
|
this.positions = {}
|
||||||
|
this.currentPosition = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
add(title, position = 0) {
|
add(title) {
|
||||||
// if title contains the author, remove it
|
// if title contains the author, remove it
|
||||||
if (this.cleanAuthor) {
|
title = this.#removeAuthorFromTitle(title)
|
||||||
const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g")
|
|
||||||
title = this.bookFinder.cleanAuthorForCompares(title).replace(authorRe, '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const titleTransformers = [
|
const titleTransformers = [
|
||||||
[/([,:;_]| by ).*/g, ''], // Remove subtitle
|
[/([,:;_]| by ).*/g, ''], // Remove subtitle
|
||||||
@ -215,11 +171,11 @@ class BookFinder {
|
|||||||
]
|
]
|
||||||
|
|
||||||
// Main variant
|
// Main variant
|
||||||
const cleanTitle = this.bookFinder.cleanTitleForCompares(title).trim()
|
const cleanTitle = cleanTitleForCompares(title).trim()
|
||||||
if (!cleanTitle) return
|
if (!cleanTitle) return
|
||||||
this.candidates.add(cleanTitle)
|
this.candidates.add(cleanTitle)
|
||||||
this.priorities[cleanTitle] = 0
|
this.priorities[cleanTitle] = 0
|
||||||
this.positions[cleanTitle] = position
|
this.positions[cleanTitle] = this.currentPosition
|
||||||
|
|
||||||
let candidate = cleanTitle
|
let candidate = cleanTitle
|
||||||
|
|
||||||
@ -230,10 +186,11 @@ class BookFinder {
|
|||||||
if (candidate) {
|
if (candidate) {
|
||||||
this.candidates.add(candidate)
|
this.candidates.add(candidate)
|
||||||
this.priorities[candidate] = 0
|
this.priorities[candidate] = 0
|
||||||
this.positions[candidate] = position
|
this.positions[candidate] = this.currentPosition
|
||||||
}
|
}
|
||||||
this.priorities[cleanTitle] = 1
|
this.priorities[cleanTitle] = 1
|
||||||
}
|
}
|
||||||
|
this.currentPosition++
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
@ -243,23 +200,16 @@ class BookFinder {
|
|||||||
getCandidates() {
|
getCandidates() {
|
||||||
var candidates = [...this.candidates]
|
var candidates = [...this.candidates]
|
||||||
candidates.sort((a, b) => {
|
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
|
// Candidates that include only digits are also likely low quality
|
||||||
const onlyDigits = /^\d+$/
|
const onlyDigits = /^\d+$/
|
||||||
const includesOnlyDigitsDiff = !onlyDigits.test(b) - !onlyDigits.test(a)
|
const includesOnlyDigitsDiff = onlyDigits.test(a) - onlyDigits.test(b)
|
||||||
if (includesOnlyDigitsDiff) return includesOnlyDigitsDiff
|
if (includesOnlyDigitsDiff) return includesOnlyDigitsDiff
|
||||||
// transformed candidates receive higher priority
|
// transformed candidates receive higher priority
|
||||||
const priorityDiff = this.priorities[a] - this.priorities[b]
|
const priorityDiff = this.priorities[a] - this.priorities[b]
|
||||||
if (priorityDiff) return priorityDiff
|
if (priorityDiff) return priorityDiff
|
||||||
// if same priorirty, prefer candidates that are closer to the beginning (e.g. titles before subtitles)
|
// if same priorirty, prefer candidates that are closer to the beginning (e.g. titles before subtitles)
|
||||||
const positionDiff = this.positions[a] - this.positions[b]
|
const positionDiff = this.positions[a] - this.positions[b]
|
||||||
if (positionDiff) return positionDiff
|
return positionDiff // candidates with same priority always have different positions
|
||||||
// Start with longer candidaets, as they are likely more specific
|
|
||||||
const lengthDiff = b.length - a.length
|
|
||||||
if (lengthDiff) return lengthDiff
|
|
||||||
return b.localeCompare(a)
|
|
||||||
})
|
})
|
||||||
Logger.debug(`[${this.constructor.name}] Found ${candidates.length} fuzzy title candidates`)
|
Logger.debug(`[${this.constructor.name}] Found ${candidates.length} fuzzy title candidates`)
|
||||||
Logger.debug(candidates)
|
Logger.debug(candidates)
|
||||||
@ -269,21 +219,32 @@ class BookFinder {
|
|||||||
delete(title) {
|
delete(title) {
|
||||||
return this.candidates.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 {
|
static AuthorCandidates = class {
|
||||||
constructor(bookFinder, cleanAuthor) {
|
constructor(cleanAuthor, audnexus) {
|
||||||
this.bookFinder = bookFinder
|
this.audnexus = audnexus
|
||||||
this.candidates = new Set()
|
this.candidates = new Set()
|
||||||
this.cleanAuthor = cleanAuthor
|
this.cleanAuthor = cleanAuthor
|
||||||
if (cleanAuthor) this.candidates.add(cleanAuthor)
|
if (cleanAuthor) this.candidates.add(cleanAuthor)
|
||||||
}
|
}
|
||||||
|
|
||||||
validateAuthor(name, region = '', maxLevenshtein = 2) {
|
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()) {
|
for (const [i, asin] of asins.entries()) {
|
||||||
if (i > 10) break
|
if (i > 10) break
|
||||||
let cleanName = this.bookFinder.cleanAuthorForCompares(asin.name)
|
let cleanName = cleanAuthorForCompares(asin.name)
|
||||||
if (!cleanName) continue
|
if (!cleanName) continue
|
||||||
if (cleanName.includes(name)) return name
|
if (cleanName.includes(name)) return name
|
||||||
if (name.includes(cleanName)) return cleanName
|
if (name.includes(cleanName)) return cleanName
|
||||||
@ -294,7 +255,7 @@ class BookFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
add(author) {
|
add(author) {
|
||||||
const cleanAuthor = this.bookFinder.cleanAuthorForCompares(author).trim()
|
const cleanAuthor = cleanAuthorForCompares(author).trim()
|
||||||
if (!cleanAuthor) return
|
if (!cleanAuthor) return
|
||||||
this.candidates.add(cleanAuthor)
|
this.candidates.add(cleanAuthor)
|
||||||
}
|
}
|
||||||
@ -362,10 +323,10 @@ class BookFinder {
|
|||||||
title = title.trim().toLowerCase()
|
title = title.trim().toLowerCase()
|
||||||
author = author?.trim().toLowerCase() || ''
|
author = author?.trim().toLowerCase() || ''
|
||||||
|
|
||||||
const cleanAuthor = this.cleanAuthorForCompares(author)
|
const cleanAuthor = cleanAuthorForCompares(author)
|
||||||
|
|
||||||
// Now run up to maxFuzzySearches fuzzy searches
|
// 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
|
// Remove underscores and parentheses with their contents, and replace with a separator
|
||||||
const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, " - ")
|
const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, " - ")
|
||||||
@ -375,9 +336,9 @@ class BookFinder {
|
|||||||
authorCandidates.add(titlePart)
|
authorCandidates.add(titlePart)
|
||||||
authorCandidates = await authorCandidates.getCandidates()
|
authorCandidates = await authorCandidates.getCandidates()
|
||||||
for (const authorCandidate of authorCandidates) {
|
for (const authorCandidate of authorCandidates) {
|
||||||
let titleCandidates = new BookFinder.TitleCandidates(this, authorCandidate)
|
let titleCandidates = new BookFinder.TitleCandidates(authorCandidate)
|
||||||
for (const [position, titlePart] of titleParts.entries())
|
for (const titlePart of titleParts)
|
||||||
titleCandidates.add(titlePart, position)
|
titleCandidates.add(titlePart)
|
||||||
titleCandidates = titleCandidates.getCandidates()
|
titleCandidates = titleCandidates.getCandidates()
|
||||||
for (const titleCandidate of titleCandidates) {
|
for (const titleCandidate of titleCandidates) {
|
||||||
if (titleCandidate == title && authorCandidate == author) continue // We already tried this
|
if (titleCandidate == title && authorCandidate == author) continue // We already tried this
|
||||||
@ -457,3 +418,52 @@ class BookFinder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = new 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