[Enhancement] Simple ratings from audible.com

This commit is contained in:
danny.rich 2025-11-03 13:46:51 -05:00
parent 0c7b738b7c
commit 831d50320c
13 changed files with 202 additions and 3 deletions

View file

@ -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 |

View 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 }

View file

@ -97,6 +97,8 @@ class Book extends Model {
this.isbn
/** @type {string} */
this.asin
/** @type {number} */
this.rating
/** @type {string} */
this.language
/** @type {boolean} */
@ -153,6 +155,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,
@ -355,6 +358,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
@ -373,6 +377,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])
@ -567,6 +592,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
@ -589,6 +615,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

View file

@ -592,6 +592,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

View file

@ -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

View file

@ -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()]