@@ -188,6 +188,9 @@ export default {
},
isBookLibrary() {
return this.currentLibraryMediaType === 'book'
+ },
+ enableReviews() {
+ return this.$store.getters['getServerSetting']('enableReviews')
}
},
methods: {
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index e63d83011..7adca596c 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -600,6 +600,10 @@
"LabelSettingsEnableWatcher": "Automatically watch libraries for changes",
"LabelSettingsEnableWatcherForLibrary": "Automatically watch library for changes",
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
+ "LabelSettingsEnableReviews": "Enable Reviews",
+ "LabelSettingsEnableReviewsHelp": "Allow users to rate and review books",
+ "LabelSettingsShowRatingsPage": "Show Ratings Page in Sidebar",
+ "LabelSettingsShowRatingsPageHelp": "Display the Ratings page link in the library sidebar",
"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.",
"LabelSettingsExperimentalFeatures": "Experimental features",
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 10113444c..ccfb1188b 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -997,46 +997,45 @@ class LibraryController {
stats.numAudioTracks = bookStats.numAudioFiles
// Get top 10 rated items
- const topRatedReviews = await Database.reviewModel.findAll({
- attributes: [
- 'libraryItemId',
- [Sequelize.fn('AVG', Sequelize.col('rating')), 'avgRating'],
- [Sequelize.fn('COUNT', Sequelize.col('id')), 'numReviews']
- ],
- include: [
- {
- model: Database.libraryItemModel,
- attributes: ['id'],
- where: { libraryId: req.library.id },
- include: [
- {
- model: Database.bookModel,
- attributes: ['id'],
- include: [
- {
- model: Database.bookMetadataModel,
- attributes: ['title']
- }
- ]
- }
- ]
+ try {
+ const topRatedReviews = await Database.reviewModel.findAll({
+ attributes: [
+ 'libraryItemId',
+ [Sequelize.fn('AVG', Sequelize.col('review.rating')), 'avgRating'],
+ [Sequelize.fn('COUNT', Sequelize.col('review.id')), 'numReviews']
+ ],
+ include: [
+ {
+ model: Database.libraryItemModel,
+ attributes: ['id'],
+ where: { libraryId: req.library.id },
+ include: [
+ {
+ model: Database.bookModel,
+ attributes: ['id', 'title']
+ }
+ ]
+ }
+ ],
+ group: ['libraryItemId', 'libraryItem.id', 'libraryItem.book.id'],
+ order: [
+ [Sequelize.literal('avgRating'), 'DESC'],
+ [Sequelize.literal('numReviews'), 'DESC']
+ ],
+ limit: 10
+ })
+ stats.topRatedItems = topRatedReviews.map((r) => {
+ return {
+ id: r.libraryItemId,
+ title: r.libraryItem?.book?.title || 'Unknown',
+ avgRating: parseFloat(r.getDataValue('avgRating')),
+ numReviews: parseInt(r.getDataValue('numReviews'))
}
- ],
- group: ['libraryItemId', 'libraryItem.id', 'libraryItem.book.id', 'libraryItem.book.bookMetadata.id'],
- order: [
- [Sequelize.literal('avgRating'), 'DESC'],
- [Sequelize.literal('numReviews'), 'DESC']
- ],
- limit: 10
- })
- stats.topRatedItems = topRatedReviews.map((r) => {
- return {
- id: r.libraryItemId,
- title: r.libraryItem?.book?.bookMetadata?.title || 'Unknown',
- avgRating: parseFloat(r.getDataValue('avgRating')),
- numReviews: parseInt(r.getDataValue('numReviews'))
- }
- })
+ })
+ } catch (error) {
+ Logger.error('[LibraryController] Failed to get top rated items for stats', error)
+ stats.topRatedItems = []
+ }
} else {
const genres = await libraryItemsPodcastFilters.getGenresWithCount(req.library.id)
const podcastStats = await libraryItemsPodcastFilters.getPodcastLibraryStats(req.library.id)
diff --git a/server/controllers/ReviewController.js b/server/controllers/ReviewController.js
index 4f34533b4..5ced09704 100644
--- a/server/controllers/ReviewController.js
+++ b/server/controllers/ReviewController.js
@@ -116,6 +116,10 @@ class ReviewController {
* @param {import('express').Response} res
*/
async findAllForUser(req, res) {
+ if (!Database.serverSettings.enableReviews) {
+ return res.status(403).send('Review feature is disabled')
+ }
+
try {
const reviews = await Database.reviewModel.findAll({
where: { userId: req.user.id },
@@ -124,10 +128,23 @@ class ReviewController {
model: Database.libraryItemModel,
include: [
{
- model: Database.bookModel
+ model: Database.bookModel,
+ include: [
+ {
+ model: Database.authorModel,
+ through: { attributes: [] }
+ },
+ {
+ model: Database.seriesModel,
+ through: { attributes: ['id', 'sequence'] }
+ }
+ ]
},
{
- model: Database.podcastModel
+ model: Database.podcastModel,
+ include: {
+ model: Database.podcastEpisodeModel
+ }
}
]
}
@@ -138,7 +155,22 @@ class ReviewController {
res.json(reviews.map((r) => {
const json = r.toOldJSON()
if (r.libraryItem) {
- json.libraryItem = r.libraryItem.toOldJSONMinified()
+ // Manually set media if missing (Sequelize hooks don't run on nested includes)
+ if (!r.libraryItem.media) {
+ if (r.libraryItem.mediaType === 'book' && r.libraryItem.book) {
+ r.libraryItem.media = r.libraryItem.book
+ } else if (r.libraryItem.mediaType === 'podcast' && r.libraryItem.podcast) {
+ r.libraryItem.media = r.libraryItem.podcast
+ }
+ }
+
+ if (r.libraryItem.media) {
+ try {
+ json.libraryItem = r.libraryItem.toOldJSONMinified()
+ } catch (err) {
+ Logger.error(`[ReviewController] Failed to minify library item ${r.libraryItem.id}`, err)
+ }
+ }
}
return json
}))
@@ -156,6 +188,10 @@ class ReviewController {
* @param {import('express').NextFunction} next
*/
async middleware(req, res, next) {
+ if (!Database.serverSettings.enableReviews) {
+ return res.status(403).send('Review feature is disabled')
+ }
+
// Basic library item access check
req.libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id)
if (!req.libraryItem?.media) return res.sendStatus(404)
diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js
index a03e17c75..3f430f99c 100644
--- a/server/objects/settings/ServerSettings.js
+++ b/server/objects/settings/ServerSettings.js
@@ -55,6 +55,9 @@ class ServerSettings {
this.language = 'en-us'
this.allowedOrigins = []
+ this.enableReviews = true
+ this.showReviewsInSidebar = true
+
this.logLevel = Logger.logLevel
this.version = packageJson.version
@@ -122,6 +125,9 @@ class ServerSettings {
this.timeFormat = settings.timeFormat || 'HH:mm'
this.language = settings.language || 'en-us'
this.allowedOrigins = settings.allowedOrigins || []
+
+ this.enableReviews = settings.enableReviews !== false
+ this.showReviewsInSidebar = settings.showReviewsInSidebar !== false
this.logLevel = settings.logLevel || Logger.logLevel
this.version = settings.version || null
this.buildNumber = settings.buildNumber || 0 // Added v2.4.5
@@ -234,6 +240,8 @@ class ServerSettings {
timeFormat: this.timeFormat,
language: this.language,
allowedOrigins: this.allowedOrigins,
+ enableReviews: this.enableReviews,
+ showReviewsInSidebar: this.showReviewsInSidebar,
logLevel: this.logLevel,
version: this.version,
buildNumber: this.buildNumber,