mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 09:51:37 +00:00
Merge c0ed3afe30 into a5362de9cc
This commit is contained in:
commit
3b013cd7cf
19 changed files with 243 additions and 7 deletions
|
|
@ -18,6 +18,10 @@
|
|||
<div>
|
||||
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">{{ $getString('LabelByAuthor', [book.author]) }}</p>
|
||||
<p v-if="book.narrator" class="text-gray-400 text-xs">{{ $strings.LabelNarrators }}: {{ book.narrator }}</p>
|
||||
<div v-if="bookRatingValue > 0" class="flex items-center text-xs text-gray-400 mt-0.5">
|
||||
<span v-for="i in 5" :key="i" class="material-symbols text-xs mr-0.5" :class="bookRatingValue >= i - 0.5 ? 'text-warning fill' : 'text-gray-500'">{{ bookRatingValue >= i ? 'star' : bookRatingValue >= i - 0.5 ? 'star_half' : 'star_border' }}</span>
|
||||
<span class="ml-1">{{ bookRatingValue.toFixed(1) }}</span>
|
||||
</div>
|
||||
<p v-if="book.duration" class="text-gray-400 text-xs">{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</p>
|
||||
</div>
|
||||
<div class="grow" />
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -174,6 +174,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedMatchOrig.rating" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.rating" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.rating" type="number" step="0.1" min="0" max="5" :disabled="!selectedMatchUsage.rating" label="Rating" />
|
||||
<p v-if="mediaMetadata.rating" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('rating', Number(mediaMetadata.rating))">{{ Number(mediaMetadata.rating).toFixed(1) }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedMatchOrig.itunesId" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.itunesId" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="grow ml-4">
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||
</label>
|
||||
</slot>
|
||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :min="min" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
|
||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :min="min" :step="step || undefined" :max="max || undefined" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -22,6 +22,8 @@ export default {
|
|||
default: 'text'
|
||||
},
|
||||
min: [String, Number],
|
||||
max: [String, Number],
|
||||
step: [String, Number],
|
||||
readonly: Boolean,
|
||||
disabled: Boolean,
|
||||
inputClass: String,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@
|
|||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="ratingInput" v-model="details.rating" type="number" step="0.1" min="0" max="5" label="Rating" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
|
|
@ -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 || [])]
|
||||
|
|
|
|||
|
|
@ -130,6 +130,16 @@
|
|||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div role="article" :aria-label="$strings.LabelSettingsEnableBookRatingsHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsEnableBookRatings" v-model="newServerSettings.enableBookRatings" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableBookRatings', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsEnableBookRatingsHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-enable-book-ratings">{{ $strings.LabelSettingsEnableBookRatings }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="grow py-2">
|
||||
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-72" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
||||
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ dateExample }}</p>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@
|
|||
</p>
|
||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||
|
||||
<div v-if="ratingValue > 0 && !isPodcast && serverSettings.enableBookRatings" class="flex items-center mb-2 mt-1">
|
||||
<span v-for="i in 5" :key="i" class="material-symbols text-sm mr-0.5" :class="ratingValue >= i - 0.5 ? 'text-warning fill' : 'text-gray-500'">{{ ratingValue >= i ? 'star' : ratingValue >= i - 0.5 ? 'star_half' : 'star_border' }}</span>
|
||||
<span class="ml-1 text-gray-300">{{ ratingValue.toFixed(1) }}</span>
|
||||
</div>
|
||||
|
||||
<content-library-item-details :library-item="libraryItem" />
|
||||
</div>
|
||||
<div class="hidden md:block grow" />
|
||||
|
|
@ -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 || []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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.<br>Note: This will extend scan time",
|
||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
68
server/migrations/v2.31.0-add-book-rating.js
Normal file
68
server/migrations/v2.31.0-add-book-rating.js
Normal file
|
|
@ -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<void>} - 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<void>} - 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 }
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue