From ec27323a6496c988f36eba9488477ac29c28aec5 Mon Sep 17 00:00:00 2001 From: "danny.rich" Date: Mon, 3 Nov 2025 16:03:36 -0500 Subject: [PATCH] add rating support on custom metadata --- custom-metadata-provider-specification.yaml | 6 ++++++ server/providers/CustomProviderAdapter.js | 5 +++-- server/scanner/Scanner.js | 18 ++++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/custom-metadata-provider-specification.yaml b/custom-metadata-provider-specification.yaml index 71cbba23a..d85e4a895 100644 --- a/custom-metadata-provider-specification.yaml +++ b/custom-metadata-provider-specification.yaml @@ -128,6 +128,12 @@ components: type: integer format: int64 description: Duration in seconds + rating: + type: number + format: float + minimum: 0 + maximum: 5 + description: Star rating (0-5, typically 0.5 increments) SeriesMetadata: type: object diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js index c079a1280..994eb5e6d 100644 --- a/server/providers/CustomProviderAdapter.js +++ b/server/providers/CustomProviderAdapter.js @@ -92,7 +92,7 @@ class CustomProviderAdapter { // re-map keys to throw out return matches.map((match) => { - const { title, subtitle, author, narrator, publisher, publishedYear, description, cover, isbn, asin, genres, tags, series, language, duration } = match + const { title, subtitle, author, narrator, publisher, publishedYear, description, cover, isbn, asin, genres, tags, series, language, duration, rating } = match const payload = { title: toStringOrUndefined(title), @@ -109,7 +109,8 @@ class CustomProviderAdapter { tags: toStringOrUndefined(tags), series: validateSeriesArray(series), language: toStringOrUndefined(language), - duration: !isNaN(duration) && duration !== null ? Number(duration) : undefined + duration: !isNaN(duration) && duration !== null ? Number(duration) : undefined, + rating: rating !== undefined && rating !== null ? (!isNaN(Number(rating)) && Number(rating) > 0 ? Number(rating) : undefined) : undefined } // Remove undefined values diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 206068cc4..a986d2252 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -193,11 +193,11 @@ class Scanner { */ async quickMatchBookBuildUpdatePayload(apiRouterCtx, libraryItem, matchData, options) { // Update media metadata if not set OR overrideDetails flag - const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'genres', 'tags', 'language', 'explicit', 'abridged', 'asin', 'isbn'] + const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'genres', 'tags', 'language', 'explicit', 'abridged', 'asin', 'isbn', 'rating'] const updatePayload = {} for (const key in matchData) { - if (matchData[key] && detailKeysToUpdate.includes(key)) { + if ((matchData[key] || key === 'rating') && detailKeysToUpdate.includes(key)) { if (key === 'narrator') { if (!libraryItem.media.narrators?.length || options.overrideDetails) { updatePayload.narrators = matchData[key] @@ -230,6 +230,20 @@ class Scanner { .filter((v) => !!v) updatePayload[key] = tagsArray } + } else if (key === 'rating') { + // Normalize rating: convert object with average to number, or number to number + let ratingValue = matchData[key] + if (ratingValue && typeof ratingValue === 'object' && ratingValue.average) { + ratingValue = Number(ratingValue.average) || null + } else if (ratingValue !== undefined && ratingValue !== null) { + ratingValue = Number(ratingValue) || null + } + if (ratingValue === 0) ratingValue = null + if ((!libraryItem.media.rating || options.overrideDetails) && ratingValue !== null && !isNaN(ratingValue) && ratingValue > 0) { + updatePayload[key] = ratingValue + } else if (ratingValue === null && libraryItem.media.rating && options.overrideDetails) { + updatePayload[key] = null + } } else if (!libraryItem.media[key] || options.overrideDetails) { updatePayload[key] = matchData[key] }