diff --git a/client/components/cards/BookMatchCard.vue b/client/components/cards/BookMatchCard.vue
index 09b963c50..acf79802d 100644
--- a/client/components/cards/BookMatchCard.vue
+++ b/client/components/cards/BookMatchCard.vue
@@ -18,6 +18,10 @@
{{ $getString('LabelByAuthor', [book.author]) }}
{{ $strings.LabelNarrators }}: {{ book.narrator }}
+
+ {{ bookRatingValue >= i ? 'star' : bookRatingValue >= i - 0.5 ? 'star_half' : 'star_border' }}
+ {{ bookRatingValue.toFixed(1) }}
+
{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}
@@ -74,6 +78,9 @@ export default {
bookCovers() {
return this.book.covers || []
},
+ serverSettings() {
+ return this.$store.state.serverSettings
+ },
bookDuration() {
return (this.book.duration || 0) * 60
},
@@ -88,7 +95,19 @@ export default {
return this.$getString('LabelDurationComparisonShorter', [this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)])
}
return this.$strings.LabelDurationComparisonExactMatch
- }
+ },
+ bookRatingValue() {
+ if (!this.book.rating) return 0
+ if (typeof this.book.rating === 'string') {
+ const num = Number(this.book.rating)
+ return isNaN(num) ? 0 : num
+ }
+ if (typeof this.book.rating === 'object' && this.book.rating.average) {
+ return Number(this.book.rating.average) || 0
+ }
+ const num = Number(this.book.rating)
+ return isNaN(num) ? 0 : num
+ },
},
methods: {
selectMatch() {
diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue
index 51f657dbc..ed8a2160f 100644
--- a/client/components/cards/LazyBookCard.vue
+++ b/client/components/cards/LazyBookCard.vue
@@ -349,6 +349,12 @@ export default {
if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear])
return '\u00A0'
}
+ if (this.orderBy === 'media.metadata.rating') {
+ if (this.mediaMetadata.rating && !isNaN(this.mediaMetadata.rating) && this.mediaMetadata.rating > 0) {
+ return `Rating: ${Number(this.mediaMetadata.rating).toFixed(1)}`
+ }
+ return '\u00A0'
+ }
if (this.orderBy === 'progress') {
if (!this.userProgressLastUpdated) return '\u00A0'
return this.$getString('LabelLastProgressDate', [this.$formatDatetime(this.userProgressLastUpdated, this.dateFormat, this.timeFormat)])
diff --git a/client/components/controls/LibrarySortSelect.vue b/client/components/controls/LibrarySortSelect.vue
index a0734d6a4..57c1165e7 100644
--- a/client/components/controls/LibrarySortSelect.vue
+++ b/client/components/controls/LibrarySortSelect.vue
@@ -110,6 +110,10 @@ export default {
text: this.$strings.LabelPublishYear,
value: 'media.metadata.publishedYear'
},
+ {
+ text: this.$strings.LabelRating,
+ value: 'media.metadata.rating'
+ },
{
text: this.$strings.LabelAddedAt,
value: 'addedAt'
diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue
index a1fc41091..0205c5dc3 100644
--- a/client/components/modals/item/tabs/Match.vue
+++ b/client/components/modals/item/tabs/Match.vue
@@ -174,6 +174,16 @@
+
+
@@ -270,6 +280,7 @@ export default {
explicit: true,
asin: true,
isbn: true,
+ rating: true,
abridged: true,
// Podcast specific
itunesPageUrl: true,
@@ -325,6 +336,9 @@ export default {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
+ serverSettings() {
+ return this.$store.state.serverSettings
+ },
filterData() {
return this.$store.state.libraries.filterData || {}
},
@@ -464,6 +478,7 @@ export default {
explicit: true,
asin: true,
isbn: true,
+ rating: true,
abridged: true,
// Podcast specific
itunesPageUrl: true,
@@ -559,6 +574,11 @@ export default {
if (match.narrator && !Array.isArray(match.narrator)) {
match.narrator = match.narrator.split(',').map((g) => g.trim())
}
+ if (match.rating && typeof match.rating === 'object' && match.rating.average) {
+ match.rating = Number(match.rating.average) || null
+ } else if (match.rating !== undefined && match.rating !== null) {
+ match.rating = Number(match.rating) || null
+ }
}
console.log('Select Match', match)
@@ -570,7 +590,7 @@ export default {
updatePayload.metadata = {}
for (const key in this.selectedMatchUsage) {
- if (this.selectedMatchUsage[key] && this.selectedMatch[key]) {
+ if (this.selectedMatchUsage[key] && this.selectedMatch[key] !== undefined && this.selectedMatch[key] !== null) {
if (key === 'series') {
if (!Array.isArray(this.selectedMatch[key])) {
console.error('Invalid series in selectedMatch', this.selectedMatch[key])
@@ -609,6 +629,13 @@ export default {
updatePayload.tags = this.selectedMatch[key]
} else if (key === 'itunesId') {
updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
+ } else if (key === 'rating') {
+ const ratingValue = Number(this.selectedMatch[key])
+ if (!isNaN(ratingValue) && ratingValue > 0) {
+ updatePayload.metadata.rating = ratingValue
+ } else if (ratingValue === 0 || isNaN(ratingValue)) {
+ updatePayload.metadata.rating = undefined
+ }
} else {
updatePayload.metadata[key] = this.selectedMatch[key]
}
diff --git a/client/components/ui/TextInputWithLabel.vue b/client/components/ui/TextInputWithLabel.vue
index 81c5516b1..32757713f 100644
--- a/client/components/ui/TextInputWithLabel.vue
+++ b/client/components/ui/TextInputWithLabel.vue
@@ -6,7 +6,7 @@
{{ note }}
-
+
@@ -22,6 +22,8 @@ export default {
default: 'text'
},
min: [String, Number],
+ max: [String, Number],
+ step: [String, Number],
readonly: Boolean,
disabled: Boolean,
inputClass: String,
diff --git a/client/components/widgets/BookDetailsEdit.vue b/client/components/widgets/BookDetailsEdit.vue
index db3b86ed6..4bd85264c 100644
--- a/client/components/widgets/BookDetailsEdit.vue
+++ b/client/components/widgets/BookDetailsEdit.vue
@@ -47,6 +47,9 @@
+
+
+
@@ -93,6 +96,7 @@ export default {
language: null,
isbn: null,
asin: null,
+ rating: null,
genres: [],
explicit: false,
abridged: false
@@ -187,6 +191,7 @@ export default {
if (this.$refs.descriptionInput) this.$refs.descriptionInput.blur()
if (this.$refs.isbnInput) this.$refs.isbnInput.blur()
if (this.$refs.asinInput) this.$refs.asinInput.blur()
+ if (this.$refs.ratingInput) this.$refs.ratingInput.blur()
if (this.$refs.publisherInput) this.$refs.publisherInput.blur()
if (this.$refs.languageInput) this.$refs.languageInput.blur()
@@ -242,6 +247,10 @@ export default {
for (const key in this.details) {
var newValue = this.details[key]
var oldValue = this.mediaMetadata[key]
+ // Convert rating 0 to null
+ if (key === 'rating' && newValue === 0) {
+ newValue = null
+ }
// Key cleared out or key first populated
if ((!newValue && oldValue) || (newValue && !oldValue)) {
metadata[key] = newValue
@@ -254,6 +263,11 @@ export default {
if (!this.objectArrayEqual(newValue, oldValue)) {
metadata[key] = newValue.map((v) => ({ ...v }))
}
+ } else if (key === 'rating') {
+ // Always check rating changes, even if null
+ if (newValue != oldValue) {
+ metadata[key] = newValue
+ }
} else if (newValue && newValue != oldValue) {
// Intentional !=
metadata[key] = newValue
@@ -283,6 +297,12 @@ export default {
this.details.language = this.mediaMetadata.language || null
this.details.isbn = this.mediaMetadata.isbn || null
this.details.asin = this.mediaMetadata.asin || null
+ if (this.mediaMetadata.rating) {
+ const ratingValue = typeof this.mediaMetadata.rating === 'object' && this.mediaMetadata.rating.average ? this.mediaMetadata.rating.average : Number(this.mediaMetadata.rating)
+ this.details.rating = !isNaN(ratingValue) && ratingValue > 0 ? ratingValue : null
+ } else {
+ this.details.rating = null
+ }
this.details.explicit = !!this.mediaMetadata.explicit
this.details.abridged = !!this.mediaMetadata.abridged
this.newTags = [...(this.media.tags || [])]
diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue
index 7fb84d468..2e2287dd8 100644
--- a/client/pages/config/index.vue
+++ b/client/pages/config/index.vue
@@ -130,6 +130,16 @@
+
+
updateSettingsKey('enableBookRatings', val)" />
+
+
+ {{ $strings.LabelSettingsEnableBookRatings }}
+ info
+
+
+
+
updateSettingsKey('dateFormat', val)" />
{{ $strings.LabelExample }}: {{ dateExample }}
diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue
index 1d8f0f20b..1ee142d4d 100644
--- a/client/pages/item/_id/index.vue
+++ b/client/pages/item/_id/index.vue
@@ -45,6 +45,11 @@
by Unknown
+
+ {{ ratingValue >= i ? 'star' : ratingValue >= i - 0.5 ? 'star_half' : 'star_border' }}
+ {{ ratingValue.toFixed(1) }}
+
+
@@ -198,6 +203,9 @@ export default {
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
+ serverSettings() {
+ return this.$store.state.serverSettings
+ },
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
@@ -280,6 +288,14 @@ export default {
authors() {
return this.mediaMetadata.authors || []
},
+ rating() {
+ return this.mediaMetadata?.rating || null
+ },
+ ratingValue() {
+ if (!this.rating) return 0
+ const value = typeof this.rating === 'object' && this.rating.average ? this.rating.average : Number(this.rating)
+ return isNaN(value) || value <= 0 ? 0 : value
+ },
series() {
return this.mediaMetadata.series || []
},
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index fb2bcb281..4512d8b38 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -545,6 +545,7 @@
"LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRSSFeedURL": "RSS Feed URL",
"LabelRandomly": "Randomly",
+ "LabelRating": "Rating",
"LabelReAddSeriesToContinueListening": "Re-add series to Continue Listening",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
@@ -593,8 +594,11 @@
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
"LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
+ "LabelSettingsEnableBookRatings": "Enable book ratings",
"LabelSettingsExperimentalFeatures": "Experimental features",
"LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
+ "LabelSettingsEnableBookRatings": "Show star ratings on book page",
+ "LabelSettingsEnableBookRatingsHelp": "Show star ratings on the book details page when available.",
"LabelSettingsFindCovers": "Find covers",
"LabelSettingsFindCoversHelp": "If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.
Note: This will extend scan time",
"LabelSettingsHideSingleBookSeries": "Hide single book series",
diff --git a/custom-metadata-provider-specification.yaml b/custom-metadata-provider-specification.yaml
index a0ec172ae..0b68c74d9 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 minutes
+ 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/migrations/changelog.md b/server/migrations/changelog.md
index 0fcbe6754..fdc7eabb5 100644
--- a/server/migrations/changelog.md
+++ b/server/migrations/changelog.md
@@ -16,3 +16,5 @@ Please add a record of every database migration that you create to this file. Th
| v2.19.1 | v2.19.1-copy-title-to-library-items | Copies title and titleIgnorePrefix to the libraryItems table, creates update triggers and indices |
| v2.19.4 | v2.19.4-improve-podcast-queries | Adds numEpisodes to podcasts, adds podcastId to mediaProgresses, copies podcast title to libraryItems |
| v2.20.0 | v2.20.0-improve-author-sort-queries | Adds AuthorNames(FirstLast\|LastFirst) to libraryItems to improve author sort queries |
+| v2.26.0 | v2.26.0-create-auth-tables | Creates auth tables for new authentication system |
+| v2.31.0 | v2.31.0-add-book-rating | Adds rating column to books table for Audible star ratings |
diff --git a/server/migrations/v2.31.0-add-book-rating.js b/server/migrations/v2.31.0-add-book-rating.js
new file mode 100644
index 000000000..49ea0b0ae
--- /dev/null
+++ b/server/migrations/v2.31.0-add-book-rating.js
@@ -0,0 +1,68 @@
+/**
+ * @typedef MigrationContext
+ * @property {import('sequelize').QueryInterface} queryInterface - a Sequelize QueryInterface object.
+ * @property {import('../Logger')} logger - a Logger object.
+ *
+ * @typedef MigrationOptions
+ * @property {MigrationContext} context - an object containing the migration context.
+ */
+
+const migrationVersion = '2.31.0'
+const migrationName = `${migrationVersion}-add-book-rating`
+const loggerPrefix = `[${migrationVersion} migration]`
+
+/**
+ * This migration script adds the rating column to the books table.
+ *
+ * @param {MigrationOptions} options - an object containing the migration context.
+ * @returns {Promise} - A promise that resolves when the migration is complete.
+ */
+async function up({ context: { queryInterface, logger } }) {
+ logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
+
+ if (await queryInterface.tableExists('books')) {
+ const tableDescription = await queryInterface.describeTable('books')
+ if (!tableDescription.rating) {
+ logger.info(`${loggerPrefix} Adding rating column to books table`)
+ await queryInterface.addColumn('books', 'rating', {
+ type: queryInterface.sequelize.Sequelize.DataTypes.FLOAT,
+ allowNull: true
+ })
+ logger.info(`${loggerPrefix} Added rating column to books table`)
+ } else {
+ logger.info(`${loggerPrefix} rating column already exists in books table`)
+ }
+ } else {
+ logger.info(`${loggerPrefix} books table does not exist`)
+ }
+
+ logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
+}
+
+/**
+ * This migration script removes the rating column from the books table.
+ *
+ * @param {MigrationOptions} options - an object containing the migration context.
+ * @returns {Promise} - A promise that resolves when the migration is complete.
+ */
+async function down({ context: { queryInterface, logger } }) {
+ logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
+
+ if (await queryInterface.tableExists('books')) {
+ const tableDescription = await queryInterface.describeTable('books')
+ if (tableDescription.rating) {
+ logger.info(`${loggerPrefix} Removing rating column from books table`)
+ await queryInterface.removeColumn('books', 'rating')
+ logger.info(`${loggerPrefix} Removed rating column from books table`)
+ } else {
+ logger.info(`${loggerPrefix} rating column does not exist in books table`)
+ }
+ } else {
+ logger.info(`${loggerPrefix} books table does not exist`)
+ }
+
+ logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
+}
+
+module.exports = { up, down }
+
diff --git a/server/models/Book.js b/server/models/Book.js
index d9f2ff132..80c60cd50 100644
--- a/server/models/Book.js
+++ b/server/models/Book.js
@@ -98,6 +98,8 @@ class Book extends Model {
this.isbn
/** @type {string} */
this.asin
+ /** @type {number} */
+ this.rating
/** @type {string} */
this.language
/** @type {boolean} */
@@ -154,6 +156,7 @@ class Book extends Model {
description: DataTypes.TEXT,
isbn: DataTypes.STRING,
asin: DataTypes.STRING,
+ rating: DataTypes.FLOAT,
language: DataTypes.STRING,
explicit: DataTypes.BOOLEAN,
abridged: DataTypes.BOOLEAN,
@@ -356,6 +359,7 @@ class Book extends Model {
description: this.description,
isbn: this.isbn,
asin: this.asin,
+ rating: this.rating,
language: this.language,
explicit: !!this.explicit,
abridged: !!this.abridged
@@ -374,6 +378,27 @@ class Book extends Model {
if (payload.metadata) {
const metadataStringKeys = ['title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language']
+ if (payload.metadata.rating !== undefined) {
+ let ratingValue = null
+ if (payload.metadata.rating !== null) {
+ if (typeof payload.metadata.rating === 'object' && payload.metadata.rating.average) {
+ ratingValue = Number(payload.metadata.rating.average)
+ } else {
+ ratingValue = Number(payload.metadata.rating)
+ }
+ }
+ // Treat 0 as null (no rating)
+ if (ratingValue === 0) ratingValue = null
+ if (ratingValue !== null && !isNaN(ratingValue) && ratingValue > 0) {
+ if (this.rating !== ratingValue) {
+ this.rating = ratingValue
+ hasUpdates = true
+ }
+ } else if (ratingValue === null && this.rating !== null) {
+ this.rating = null
+ hasUpdates = true
+ }
+ }
metadataStringKeys.forEach((key) => {
if (typeof payload.metadata[key] == 'number') {
payload.metadata[key] = String(payload.metadata[key])
@@ -578,6 +603,7 @@ class Book extends Model {
description: this.description,
isbn: this.isbn,
asin: this.asin,
+ rating: this.rating,
language: this.language,
explicit: this.explicit,
abridged: this.abridged
@@ -600,6 +626,7 @@ class Book extends Model {
description: this.description,
isbn: this.isbn,
asin: this.asin,
+ rating: this.rating,
language: this.language,
explicit: this.explicit,
abridged: this.abridged
diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js
index b33fa585c..a39aceb12 100644
--- a/server/models/LibraryItem.js
+++ b/server/models/LibraryItem.js
@@ -650,6 +650,7 @@ class LibraryItem extends Model {
description: mediaExpanded.description,
isbn: mediaExpanded.isbn,
asin: mediaExpanded.asin,
+ rating: mediaExpanded.rating,
language: mediaExpanded.language,
explicit: !!mediaExpanded.explicit,
abridged: !!mediaExpanded.abridged
diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js
index 99f5b76aa..f4680ad0e 100644
--- a/server/objects/settings/ServerSettings.js
+++ b/server/objects/settings/ServerSettings.js
@@ -41,6 +41,7 @@ class ServerSettings {
// Bookshelf Display
this.homeBookshelfView = BookshelfView.DETAIL
this.bookshelfView = BookshelfView.DETAIL
+ this.enableBookRatings = false
// Podcasts
this.podcastEpisodeSchedule = '0 * * * *' // Every hour
@@ -115,6 +116,7 @@ class ServerSettings {
this.homeBookshelfView = settings.homeBookshelfView || BookshelfView.STANDARD
this.bookshelfView = settings.bookshelfView || BookshelfView.STANDARD
+ this.enableBookRatings = !!settings.enableBookRatings
this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix
this.sortingPrefixes = settings.sortingPrefixes || ['the']
@@ -227,6 +229,7 @@ class ServerSettings {
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
homeBookshelfView: this.homeBookshelfView,
bookshelfView: this.bookshelfView,
+ enableBookRatings: this.enableBookRatings,
podcastEpisodeSchedule: this.podcastEpisodeSchedule,
sortingIgnorePrefix: this.sortingIgnorePrefix,
sortingPrefixes: [...this.sortingPrefixes],
diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js
index 5c8cad75a..0c099f9e8 100644
--- a/server/providers/CustomProviderAdapter.js
+++ b/server/providers/CustomProviderAdapter.js
@@ -113,7 +113,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),
@@ -130,7 +130,8 @@ class CustomProviderAdapter {
tags: validateTagsGenresArray(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
}
// Remove undefined values
diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js
index a1e7ff507..8fa6681ec 100644
--- a/server/scanner/BookScanner.js
+++ b/server/scanner/BookScanner.js
@@ -839,6 +839,7 @@ class BookScanner {
description: libraryItem.media.description,
isbn: libraryItem.media.isbn,
asin: libraryItem.media.asin,
+ rating: libraryItem.media.rating,
language: libraryItem.media.language,
explicit: !!libraryItem.media.explicit,
abridged: !!libraryItem.media.abridged
diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js
index af4405987..c1f6ec529 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,22 @@ class Scanner {
.filter((v) => !!v)
updatePayload[key] = tagsArray
}
+ } else if (key === 'rating') {
+ // Normalize rating (handle object format {average: 4.5} from some providers)
+ let ratingValue = matchData[key]
+ if (ratingValue && typeof ratingValue === 'object' && ratingValue.average) {
+ ratingValue = Number(ratingValue.average)
+ } else if (ratingValue !== undefined && ratingValue !== null) {
+ ratingValue = Number(ratingValue)
+ } else {
+ ratingValue = null
+ }
+ // 0 = no rating, only update if valid rating > 0
+ if (ratingValue !== null && !isNaN(ratingValue) && ratingValue > 0 && (!libraryItem.media.rating || options.overrideDetails)) {
+ 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]
}
diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js
index fbe0c4f0d..e2df87539 100644
--- a/server/utils/queries/libraryItemsBookFilters.js
+++ b/server/utils/queries/libraryItemsBookFilters.js
@@ -274,6 +274,9 @@ module.exports = {
return [['duration', dir]]
} else if (sortBy === 'media.metadata.publishedYear') {
return [[Sequelize.literal(`CAST(\`book\`.\`publishedYear\` AS INTEGER)`), dir]]
+ } else if (sortBy === 'media.metadata.rating') {
+ const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST'
+ return [[Sequelize.literal(`\`book\`.\`rating\` ${nullDir}`)]]
} else if (sortBy === 'media.metadata.authorNameLF') {
// Sort by author name last first, secondary sort by title
return [[Sequelize.literal('`libraryItem`.`authorNamesLastFirst` COLLATE NOCASE'), dir], getTitleOrder()]