From 7c3504fe2b2af2d4b7a387d9af5fb1ccafc43a70 Mon Sep 17 00:00:00 2001 From: Peter BALIVET Date: Fri, 27 Jun 2025 11:02:42 +0200 Subject: [PATCH 1/7] Added server wide audiobook rating (admin only) --- client/assets/logos/audible.svg | 2 + client/components/modals/item/tabs/Match.vue | 34 +++++- client/components/ui/RatingInput.vue | 107 ++++++++++++++++++ client/components/widgets/BookDetailsEdit.vue | 38 +++++-- client/pages/item/_id/index.vue | 31 +++++ client/strings/en-us.json | 1 + docker-compose.yml | 3 +- server/migrations/v2.25.2-add-book-ratings.js | 58 ++++++++++ server/models/Book.js | 48 +++++++- server/scanner/Scanner.js | 8 +- 10 files changed, 312 insertions(+), 18 deletions(-) create mode 100644 client/assets/logos/audible.svg create mode 100644 client/components/ui/RatingInput.vue create mode 100644 server/migrations/v2.25.2-add-book-ratings.js diff --git a/client/assets/logos/audible.svg b/client/assets/logos/audible.svg new file mode 100644 index 00000000..ab3f66b8 --- /dev/null +++ b/client/assets/logos/audible.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 41043be7..b3dcdfef 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -21,9 +21,9 @@

{{ $strings.MessageNoResults }}

- +
+ +
@@ -225,6 +225,16 @@
+
+ +
+ +

+ {{ $strings.LabelCurrently }} {{ mediaMetadata.rating }}/5 +

+
+
+
{{ $strings.ButtonSubmit }}
@@ -234,7 +244,12 @@ + + \ No newline at end of file diff --git a/client/components/widgets/BookDetailsEdit.vue b/client/components/widgets/BookDetailsEdit.vue index db3b86ed..c141feaa 100644 --- a/client/components/widgets/BookDetailsEdit.vue +++ b/client/components/widgets/BookDetailsEdit.vue @@ -50,20 +50,33 @@
-
+
-
+
-
-
- +
+ +
+
+
+
+
+ +
+
+
+
+ +
+
-
-
- +
+
+ +
@@ -72,7 +85,12 @@ diff --git a/client/store/libraries.js b/client/store/libraries.js index 8964d9f1..5d4b7242 100644 --- a/client/store/libraries.js +++ b/client/store/libraries.js @@ -1,3 +1,4 @@ +import Vue from 'vue' const { Constants } = require('../plugins/constants') export const state = () => ({ @@ -12,7 +13,8 @@ export const state = () => ({ numUserPlaylists: 0, collections: [], userPlaylists: [], - ereaderDevices: [] + ereaderDevices: [], + libraryItemsCache: {} }) export const getters = { @@ -170,6 +172,9 @@ export const actions = { } export const mutations = { + UPDATE_LIBRARY_ITEM(state, libraryItem) { + Vue.set(state.libraryItemsCache, libraryItem.id, libraryItem) + }, setFolders(state, folders) { state.folders = folders }, diff --git a/server/Database.js b/server/Database.js index a260e89f..2fddc23c 100644 --- a/server/Database.js +++ b/server/Database.js @@ -333,6 +333,7 @@ class Database { require('./models/Setting').init(this.sequelize) require('./models/CustomMetadataProvider').init(this.sequelize) require('./models/MediaItemShare').init(this.sequelize) + require('./models/UserBookRating').init(this.sequelize) return this.sequelize.sync({ force, alter: false }) } diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 5247dbb0..7d43cfc2 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -53,6 +53,14 @@ class LibraryItemController { if (req.query.expanded == 1) { const item = req.libraryItem.toOldJSONExpanded() + // Include users personal rating + const userBookRating = await Database.models.userBookRating.findOne({ + where: { userId: req.user.id, bookId: req.libraryItem.media.id } + }) + if (userBookRating) { + item.personalRating = userBookRating.rating + } + // Include users media progress if (includeEntities.includes('progress')) { const episodeId = req.query.episode || null @@ -1181,8 +1189,8 @@ class LibraryItemController { } } - if (req.path.includes('/play')) { - // allow POST requests using /play and /play/:episodeId + if (req.path.includes('/play') || req.path.includes('/rate')) { + // allow POST requests using /play and /play/:episodeId OR /rate } else if (req.method == 'DELETE' && !req.user.canDelete) { Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to delete without permission`) return res.sendStatus(403) @@ -1193,5 +1201,31 @@ class LibraryItemController { next() } + + /** + * POST: /api/items/:id/rate + * + * @param {LibraryItemControllerRequest} req + * @param {Response} res + */ + async rate(req, res) { + try { + const { rating } = req.body + if (rating === null || typeof rating !== 'number' || rating < 0 || rating > 5) { + return res.status(400).json({ error: 'Invalid rating' }) + } + + const bookId = req.libraryItem.media.id + const userId = req.user.id + + await Database.models.userBookRating.upsert({ userId, bookId, rating }) + + res.status(200).json({ success: true }) + } catch (err) { + Logger.error(err) + res.status(500).json({ error: 'An error occurred while saving the rating' }) + } + } } + module.exports = new LibraryItemController() diff --git a/server/migrations/v2.25.3-add-user-book-ratings.js b/server/migrations/v2.25.3-add-user-book-ratings.js new file mode 100644 index 00000000..9b63d4b2 --- /dev/null +++ b/server/migrations/v2.25.3-add-user-book-ratings.js @@ -0,0 +1,59 @@ +const { DataTypes } = require('sequelize') + +module.exports = { + up: async ({ context: queryInterface }) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.createTable( + 'userBookRatings', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + references: { model: 'users', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, + bookId: { + type: DataTypes.STRING, + allowNull: false, + references: { model: 'books', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, + rating: { + type: DataTypes.FLOAT, + allowNull: false + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false + } + }, + { transaction } + ) + await queryInterface.addConstraint('userBookRatings', { + fields: ['userId', 'bookId'], + type: 'unique', + name: 'user_book_ratings_unique_constraint', + transaction + }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + }, + down: async ({ context: queryInterface }) => { + await queryInterface.dropTable('userBookRatings') + } +} diff --git a/server/models/UserBookRating.js b/server/models/UserBookRating.js new file mode 100644 index 00000000..c0c92912 --- /dev/null +++ b/server/models/UserBookRating.js @@ -0,0 +1,53 @@ +const { DataTypes, Model } = require('sequelize') + +class UserBookRating extends Model { + static init(sequelize) { + super.init( + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + userId: { + type: DataTypes.STRING, + allowNull: false + }, + bookId: { + type: DataTypes.STRING, + allowNull: false + }, + rating: { + type: DataTypes.FLOAT, + allowNull: false + } + }, + { + sequelize, + modelName: 'userBookRating', + indexes: [ + { + unique: true, + fields: ['userId', 'bookId'] + } + ] + } + ) + + const { user, book } = sequelize.models + + user.hasMany(UserBookRating, { + foreignKey: 'userId' + }) + + this.belongsTo(user, { foreignKey: 'userId' }) + + book.hasMany(this, { + foreignKey: 'bookId' + }) + + this.belongsTo(book, { foreignKey: 'bookId' }) + } +} + +module.exports = UserBookRating diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index ecb1555f..440f8aa6 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -125,6 +125,7 @@ class ApiRouter { this.router.get('/items/:id/file/:fileid/download', LibraryItemController.middleware.bind(this), LibraryItemController.downloadLibraryFile.bind(this)) this.router.get('/items/:id/ebook/:fileid?', LibraryItemController.middleware.bind(this), LibraryItemController.getEBookFile.bind(this)) this.router.patch('/items/:id/ebook/:fileid/status', LibraryItemController.middleware.bind(this), LibraryItemController.updateEbookFileStatus.bind(this)) + this.router.post('/items/:id/rate', LibraryItemController.middleware.bind(this), LibraryItemController.rate.bind(this)) // // User Routes From dba575761ec3a824842a6229674efd83d9107557 Mon Sep 17 00:00:00 2001 From: Peter BALIVET Date: Mon, 30 Jun 2025 13:52:37 +0200 Subject: [PATCH 3/7] Added Explicit user book rating + Community rating --- client/components/modals/item/EditModal.vue | 22 +++- .../components/modals/item/tabs/Details.vue | 1 + client/components/ui/FlameIcon.vue | 32 +++++ client/components/ui/RatingInput.vue | 43 +++--- client/pages/config/index.vue | 34 +++++ client/pages/item/_id/index.vue | 77 +++++++---- client/{assets/logos => static}/audible.svg | 2 +- client/static/flame-icon.svg | 1 + client/store/libraries.js | 8 +- client/strings/en-us.json | 10 +- server/Database.js | 11 ++ server/controllers/LibraryItemController.js | 123 +++++++++++++++--- .../v2.25.4-add-user-book-explicit-ratings.js | 59 +++++++++ server/models/UserBookExplicitRating.js | 54 ++++++++ server/objects/settings/ServerSettings.js | 12 ++ server/routers/ApiRouter.js | 1 + 16 files changed, 426 insertions(+), 64 deletions(-) create mode 100644 client/components/ui/FlameIcon.vue rename client/{assets/logos => static}/audible.svg (97%) create mode 100644 client/static/flame-icon.svg create mode 100644 server/migrations/v2.25.4-add-user-book-explicit-ratings.js create mode 100644 server/models/UserBookExplicitRating.js diff --git a/client/components/modals/item/EditModal.vue b/client/components/modals/item/EditModal.vue index 232c3228..1b73f184 100644 --- a/client/components/modals/item/EditModal.vue +++ b/client/components/modals/item/EditModal.vue @@ -6,8 +6,8 @@
-