From d15264832dfc2a9ce418846eb2a4e4b8ecad1f50 Mon Sep 17 00:00:00 2001
From: Kaldigo <6437691+kaldigo@users.noreply.github.com>
Date: Mon, 23 May 2022 03:56:51 +0100
Subject: [PATCH] Updated matching with latest changes, Added override toggle
for quickmatch, added asin and isbn to quickmatch query, updated audible
provider to use audnexus
---
client/components/modals/item/tabs/Match.vue | 88 ++++++++++++----
client/pages/config/index.vue | 11 ++
server/finders/BookFinder.js | 8 +-
server/objects/entities/Series.js | 2 +-
server/objects/settings/ServerSettings.js | 5 +-
server/providers/Audible.js | 100 +++++++++----------
server/scanner/ScanOptions.js | 5 +-
server/scanner/Scanner.js | 71 +++++++++----
8 files changed, 192 insertions(+), 98 deletions(-)
diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue
index 22b68cee..5ce43ed3 100644
--- a/client/components/modals/item/tabs/Match.vue
+++ b/client/components/modals/item/tabs/Match.vue
@@ -87,7 +87,7 @@
@@ -177,6 +198,10 @@ export default {
publishedYear: true,
series: true,
volumeNumber: true,
+ genres: true,
+ tags: true,
+ language: true,
+ explicit: true,
asin: true,
isbn: true,
// Podcast specific
@@ -204,6 +229,19 @@ export default {
this.$emit('update:processing', val)
}
},
+ seriesItems: {
+ get() {
+ return this.selectedMatch.series.map((se) => {
+ return {
+ displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series,
+ ...se
+ }
+ })
+ },
+ set(val) {
+ this.selectedMatch.series = val
+ }
+ },
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
},
@@ -294,6 +332,10 @@ export default {
publishedYear: true,
series: true,
volumeNumber: true,
+ genres: true,
+ tags: true,
+ language: true,
+ explicit: true,
asin: true,
isbn: true,
// Podcast specific
@@ -324,32 +366,42 @@ export default {
},
buildMatchUpdatePayload() {
var updatePayload = {}
+ updatePayload.metadata = {}
var volumeNumber = this.selectedMatchUsage.volumeNumber ? this.selectedMatch.volumeNumber || null : null
for (const key in this.selectedMatchUsage) {
if (this.selectedMatchUsage[key] && this.selectedMatch[key]) {
if (key === 'series') {
- var seriesItem = {
+ if(!Array.isArray(this.selectedMatch[key])) this.selectedMatch[key] = [{ series: this.selectedMatch[key], volumeNumber: volumeNumber }]
+ var seriesPayload = []
+ this.selectedMatch[key].forEach(seriesItem => seriesPayload.push({
id: `new-${Math.floor(Math.random() * 10000)}`,
- name: this.selectedMatch[key],
- sequence: volumeNumber
- }
- updatePayload.series = [seriesItem]
+ name: seriesItem.series,
+ sequence: seriesItem.volumeNumber
+ }))
+ updatePayload.metadata.series = seriesPayload
} else if (key === 'author' && !this.isPodcast) {
- var authorItem = {
+ if(!Array.isArray(this.selectedMatch[key])) this.selectedMatch[key] = [this.selectedMatch[key]]
+ var authorPayload = []
+ this.selectedMatch[key].forEach(authorName => authorPayload.push({
id: `new-${Math.floor(Math.random() * 10000)}`,
- name: this.selectedMatch[key]
- }
- updatePayload.authors = [authorItem]
+ name: authorName
+ }))
+ updatePayload.metadata.authors = authorPayload
} else if (key === 'narrator') {
- updatePayload.narrators = [this.selectedMatch[key]]
+ updatePayload.metadata.narrators = [this.selectedMatch[key]]
+ } else if (key === 'genres') {
+ updatePayload.metadata.genres = this.selectedMatch[key].split(',')
+ } else if (key === 'tags') {
+ updatePayload.tags = this.selectedMatch[key].split(',')
} else if (key === 'itunesId') {
- updatePayload.itunesId = Number(this.selectedMatch[key])
- } else if (key !== 'volumeNumber') {
- updatePayload[key] = this.selectedMatch[key]
+ updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
+ } else {
+ updatePayload.metadata[key] = this.selectedMatch[key]
}
}
}
+ console.log(updatePayload)
return updatePayload
},
async submitMatchUpdate() {
@@ -361,7 +413,7 @@ export default {
if (updatePayload.cover) {
var coverPayload = {
- url: updatePayload.cover
+ url: updatePayload.metadata.cover
}
var success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
console.error('Failed to update', error)
@@ -373,13 +425,11 @@ export default {
this.$toast.error('Item Cover Failed to Update')
}
console.log('Updated cover')
- delete updatePayload.cover
+ delete updatePayload.metadata.cover
}
if (Object.keys(updatePayload).length) {
- var mediaUpdatePayload = {
- metadata: updatePayload
- }
+ var mediaUpdatePayload = updatePayload
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
console.error('Failed to update', error)
return false
diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue
index 87a5d969..a037ac71 100644
--- a/client/pages/config/index.vue
+++ b/client/pages/config/index.vue
@@ -113,6 +113,16 @@
+
+
updateSettingsKey('scannerPreferMatchedMetadata', val)" />
+
+
+ Scanner prefer matched metadata
+ info_outlined
+
+
+
+
updateSettingsKey('scannerDisableWatcher', val)" />
@@ -226,6 +236,7 @@ export default {
experimentalFeatures: 'Features in development that could use your feedback and help testing. Click to open github discussion.',
scannerDisableWatcher: 'Disables the automatic adding/updating of items when file changes are detected. *Requires server restart',
scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names',
+ scannerPreferMatchedMetadata: 'Matched data will overide book details when using Quick Match',
scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names',
scannerParseSubtitle: 'Extract subtitles from audiobook folder names.
Subtitle must be seperated by " - "
i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
sortingIgnorePrefix: 'i.e. for prefix "the" book title "The Book Title" would sort as "Book Title, The"',
diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js
index 60645747..60b39838 100644
--- a/server/finders/BookFinder.js
+++ b/server/finders/BookFinder.js
@@ -166,14 +166,14 @@ class BookFinder {
return this.iTunesApi.searchAudiobooks(title)
}
- async getAudibleResults(title, author) {
- var books = await this.audible.search(title, author);
+ async getAudibleResults(title, author, asin) {
+ var books = await this.audible.search(title, author, asin);
if (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
if (!books) return []
return books
}
- async search(provider, title, author, options = {}) {
+ async search(provider, title, author, isbn, asin, options = {}) {
var books = []
var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
@@ -182,7 +182,7 @@ class BookFinder {
if (provider === 'google') {
return this.getGoogleBooksResults(title, author)
} else if (provider === 'audible') {
- return this.getAudibleResults(title, author)
+ return this.getAudibleResults(title, author, asin)
} else if (provider === 'itunes') {
return this.getiTunesAudiobooksResults(title, author)
} else if (provider === 'libgen') {
diff --git a/server/objects/entities/Series.js b/server/objects/entities/Series.js
index 18f3b4a3..9cb38a0b 100644
--- a/server/objects/entities/Series.js
+++ b/server/objects/entities/Series.js
@@ -48,7 +48,7 @@ class Series {
}
checkNameEquals(name) {
- if (!name) return false
+ if (!name || !this.name) return false
return this.name.toLowerCase() == name.toLowerCase().trim()
}
}
diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js
index bea7b6ed..286e15c0 100644
--- a/server/objects/settings/ServerSettings.js
+++ b/server/objects/settings/ServerSettings.js
@@ -11,7 +11,8 @@ class ServerSettings {
this.scannerCoverProvider = 'google'
this.scannerPreferAudioMetadata = false
this.scannerPreferOpfMetadata = false
- this.scannerDisableWatcher = false
+ this.scannerPreferMatchedMetadata = false
+ this.scannerDisableWatcher = false
// Metadata - choose to store inside users library item folder
this.storeCoverWithItem = false
@@ -62,6 +63,7 @@ class ServerSettings {
this.scannerParseSubtitle = settings.scannerParseSubtitle
this.scannerPreferAudioMetadata = !!settings.scannerPreferAudioMetadata
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
+ this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
this.storeCoverWithItem = !!settings.storeCoverWithItem
@@ -107,6 +109,7 @@ class ServerSettings {
scannerParseSubtitle: this.scannerParseSubtitle,
scannerPreferAudioMetadata: this.scannerPreferAudioMetadata,
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
+ scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata,
scannerDisableWatcher: this.scannerDisableWatcher,
storeCoverWithItem: this.storeCoverWithItem,
storeMetadataWithItem: this.storeMetadataWithItem,
diff --git a/server/providers/Audible.js b/server/providers/Audible.js
index c6d6836e..21521623 100644
--- a/server/providers/Audible.js
+++ b/server/providers/Audible.js
@@ -6,83 +6,81 @@ class Audible {
constructor() { }
cleanResult(item) {
- var { title, subtitle, asin, authors, narrators, publisher_name, publisher_summary, release_date, series, product_images, publication_name } = item;
+ var { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language } = item;
- var primarySeries = this.getPrimarySeries(series, publication_name);
+ var series = []
+ if(seriesPrimary) series.push(seriesPrimary)
+ if(seriesSecondary) series.push(seriesSecondary)
+
+ var genresFiltered = genres ? genres.filter(g => g.type == "genre") : []
+ var tagsFiltered = genres ? genres.filter(g => g.type == "tag") : []
return {
title,
subtitle: subtitle || null,
author: authors ? authors.map(({ name }) => name).join(', ') : null,
narrator: narrators ? narrators.map(({ name }) => name).join(', ') : null,
- publisher: publisher_name,
- publishedYear: release_date ? release_date.split('-')[0] : null,
- description: publisher_summary ? stripHtml(publisher_summary).result : null,
- cover: this.getBestImageLink(product_images),
+ publisher: publisherName,
+ publishedYear: releaseDate ? releaseDate.split('-')[0] : null,
+ description: summary ? stripHtml(summary).result : null,
+ cover: image,
asin,
- series: primarySeries ? primarySeries.title : null,
- volumeNumber: primarySeries ? primarySeries.sequence : null
+ genres: genresFiltered.length > 0 ? genresFiltered.map(({ name }) => name).join(', ') : null,
+ tags: tagsFiltered.length > 0 ? tagsFiltered.map(({ name }) => name).join(', ') : null,
+ series: series != [] ? series.map(({name, position}) => ({ series: name, volumeNumber: position })) : null,
+ language: language ? language.charAt(0).toUpperCase() + language.slice(1) : null
}
}
- getBestImageLink(images) {
- if (!images) return null
- var keys = Object.keys(images)
- if (!keys.length) return null
- return images[keys[keys.length - 1]]
- }
-
- getPrimarySeries(series, publication_name) {
- return (series && series.length > 0) ? series.find((s) => s.title == publication_name) || series[0] : null
- }
-
isProbablyAsin(title) {
return /^[0-9A-Z]{10}$/.test(title)
}
asinSearch(asin) {
- var queryObj = {
- response_groups: 'rating,series,contributors,product_desc,media,product_extended_attrs',
- image_sizes: '500,1024,2000'
- };
- var queryString = (new URLSearchParams(queryObj)).toString();
asin = encodeURIComponent(asin);
- var url = `https://api.audible.com/1.0/catalog/products/${asin}?${queryString}`
+ var url = `https://api.audnex.us/books/${asin}`
Logger.debug(`[Audible] ASIN url: ${url}`)
return axios.get(url).then((res) => {
- if (!res || !res.data || !res.data.product || !res.data.product.authors) return []
- return [res.data.product]
+ if (!res || !res.data || !res.data.asin) return null
+ return res.data
}).catch(error => {
- Logger.error('[Audible] search error', error)
+ Logger.error('[Audible] ASIN search error', error)
return []
})
}
- async search(title, author) {
- if (this.isProbablyAsin(title)) {
- var items = await this.asinSearch(title)
- if (items.length > 0) return items.map(item => this.cleanResult(item))
+ async search(title, author, asin) {
+ var items
+ if(asin) {
+ items = [await this.asinSearch(asin)]
+ }
+
+ if (!items && this.isProbablyAsin(title)) {
+ items = [await this.asinSearch(title)]
}
- var queryObj = {
- response_groups: 'rating,series,contributors,product_desc,media,product_extended_attrs',
- image_sizes: '500,1024,2000',
- num_results: '25',
- products_sort_by: 'Relevance',
- title: title
- };
- if (author) queryObj.author = author
- var queryString = (new URLSearchParams(queryObj)).toString();
- var url = `https://api.audible.com/1.0/catalog/products?${queryString}`
- Logger.debug(`[Audible] Search url: ${url}`)
- var items = await axios.get(url).then((res) => {
- if (!res || !res.data || !res.data.products) return []
- return res.data.products
- }).catch(error => {
- Logger.error('[Audible] search error', error)
- return []
- })
- return items.map(item => this.cleanResult(item))
+ if(!items) {
+ var queryObj = {
+ response_groups: 'rating,series,contributors,product_desc,media,product_extended_attrs',
+ image_sizes: '500,1024,2000',
+ num_results: '25',
+ products_sort_by: 'Relevance',
+ title: title
+ };
+ if (author) queryObj.author = author
+ var queryString = (new URLSearchParams(queryObj)).toString();
+ var url = `https://api.audible.com/1.0/catalog/products?${queryString}`
+ Logger.debug(`[Audible] Search url: ${url}`)
+ items = await axios.get(url).then((res) => {
+ if (!res || !res.data || !res.data.products) return null
+ return Promise.all(res.data.products.map(result => this.asinSearch(result.asin)))
+ }).catch(error => {
+ Logger.error('[Audible] query search error', error)
+ return []
+ })
+ }
+
+ return items ? items.map(item => this.cleanResult(item)) : []
}
}
diff --git a/server/scanner/ScanOptions.js b/server/scanner/ScanOptions.js
index ccc6ca2f..04adf2f9 100644
--- a/server/scanner/ScanOptions.js
+++ b/server/scanner/ScanOptions.js
@@ -8,6 +8,7 @@ class ScanOptions {
this.storeCoverWithItem = false
this.preferAudioMetadata = false
this.preferOpfMetadata = false
+ this.preferMatchedMetadata = false
if (options) {
this.construct(options)
@@ -32,7 +33,8 @@ class ScanOptions {
findCovers: this.findCovers,
storeCoverWithItem: this.storeCoverWithItem,
preferAudioMetadata: this.preferAudioMetadata,
- preferOpfMetadata: this.preferOpfMetadata
+ preferOpfMetadata: this.preferOpfMetadata,
+ preferOpfMetadata: this.preferMatchedMetadata
}
}
@@ -44,6 +46,7 @@ class ScanOptions {
this.storeCoverWithItem = serverSettings.storeCoverWithItem
this.preferAudioMetadata = serverSettings.scannerPreferAudioMetadata
this.preferOpfMetadata = serverSettings.scannerPreferOpfMetadata
+ this.preferOpfMetadata = serverSettings.scannerPreferMatchedMetadata
}
}
module.exports = ScanOptions
\ No newline at end of file
diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js
index 7e9ffc2e..3ded0c10 100644
--- a/server/scanner/Scanner.js
+++ b/server/scanner/Scanner.js
@@ -632,8 +632,10 @@ class Scanner {
var provider = options.provider || 'google'
var searchTitle = options.title || libraryItem.media.metadata.title
var searchAuthor = options.author || libraryItem.media.metadata.authorName
+ var searchISBN = options.isbn || libraryItem.media.metadata.isbn
+ var searchASIN = options.asin || libraryItem.media.metadata.asin
- var results = await this.bookFinder.search(provider, searchTitle, searchAuthor)
+ var results = await this.bookFinder.search(provider, searchTitle, searchAuthor, searchISBN, searchASIN)
if (!results.length) {
return {
warning: `No ${provider} match found`
@@ -641,6 +643,12 @@ class Scanner {
}
var matchData = results[0]
+ // Set to override existing metadata if scannerPreferMatchedMetadata setting is true
+ if(this.db.serverSettings.scannerPreferMatchedMetadata) {
+ options.overrideCover = true
+ options.overrideDetails = true
+ }
+
// Update cover if not set OR overrideCover flag
var hasUpdated = false
if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
@@ -654,47 +662,68 @@ class Scanner {
}
// Update media metadata if not set OR overrideDetails flag
- const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'asin', 'isbn']
+ const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'genres', 'tags', 'language', 'explicit', 'asin', 'isbn']
const updatePayload = {}
+ updatePayload.metadata = {}
for (const key in matchData) {
if (matchData[key] && detailKeysToUpdate.includes(key)) {
if (key === 'narrator') {
if ((!libraryItem.media.metadata.narratorName || options.overrideDetails)) {
- updatePayload.narrators = [matchData[key]]
+ updatePayload.metadata.narrators = matchData[key].split(',')
+ }
+ } else if (key === 'genres') {
+ if ((!libraryItem.media.metadata.genres || options.overrideDetails)) {
+ updatePayload.metadata[key] = matchData[key].split(',')
+ }
+ } else if (key === 'tags') {
+ if ((!libraryItem.media.tags || options.overrideDetails)) {
+ updatePayload[key] = matchData[key].split(',')
}
} else if ((!libraryItem.media.metadata[key] || options.overrideDetails)) {
- updatePayload[key] = matchData[key]
+ updatePayload.metadata[key] = matchData[key]
}
}
}
// Add or set author if not set
- if (matchData.author && !libraryItem.media.metadata.authorName) {
- var author = this.db.authors.find(au => au.checkNameEquals(matchData.author))
- if (!author) {
- author = new Author()
- author.setData({ name: matchData.author })
- await this.db.insertEntity('author', author)
- this.emitter('author_added', author)
+ if (matchData.author && !libraryItem.media.metadata.authorName || options.overrideDetails) {
+ if(!Array.isArray(matchData.author)) matchData.author = [matchData.author]
+ const authorPayload = []
+ for (let index = 0; index < matchData.author.length; index++) {
+ const authorName = matchData.author[index]
+ var author = this.db.authors.find(au => au.checkNameEquals(authorName))
+ if (!author) {
+ author = new Author()
+ author.setData({ name: authorName })
+ await this.db.insertEntity('author', author)
+ this.emitter('author_added', author)
+ }
+ authorPayload.push(author.toJSONMinimal())
}
- updatePayload.authors = [author.toJSONMinimal()]
+ updatePayload.metadata.authors = authorPayload
}
// Add or set series if not set
- if (matchData.series && !libraryItem.media.metadata.seriesName) {
- var seriesItem = this.db.series.find(au => au.checkNameEquals(matchData.series))
- if (!seriesItem) {
- seriesItem = new Series()
- seriesItem.setData({ name: matchData.series })
- await this.db.insertEntity('series', seriesItem)
- this.emitter('series_added', seriesItem)
+ if (matchData.series && !libraryItem.media.metadata.seriesName || options.overrideDetails) {
+ if(!Array.isArray(matchData.series)) matchData.series = [{ series: matchData.series, volumeNumber: matchData.volumeNumber }]
+ const seriesPayload = []
+ for (let index = 0; index < matchData.series.length; index++) {
+ const seriesMatchItem = matchData.series[index]
+ var seriesItem = this.db.series.find(au => au.checkNameEquals(seriesMatchItem.series))
+ if (!seriesItem) {
+ seriesItem = new Series()
+ seriesItem.setData({ name: seriesMatchItem.series })
+ await this.db.insertEntity('series', seriesItem)
+ this.emitter('series_added', seriesItem)
+ }
+ seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.volumeNumber))
}
- updatePayload.series = [seriesItem.toJSONMinimal(matchData.volumeNumber)]
+ updatePayload.metadata.series = seriesPayload
}
if (Object.keys(updatePayload).length) {
Logger.debug('[Scanner] Updating details', updatePayload)
- if (libraryItem.media.update({ metadata: updatePayload })) {
+ if (libraryItem.media.update(updatePayload)) {
hasUpdated = true
}
}