mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-03-04 06:59:41 +00:00
Add review and rating features with sorting and filtering options
- Implemented a new ReviewController to handle review creation, updates, and retrieval for library items. - Added pagination, sorting, and filtering capabilities for reviews in the API. - Updated frontend components to support review display, including a new ReviewsTable and enhanced ratings UI. - Introduced new strings for user interface elements related to reviews and ratings. - Added tests for the ReviewController and Review model to ensure functionality and validation. - Enabled the option to toggle the review feature in server settings.
This commit is contained in:
parent
e4e2770fbd
commit
41e8906312
12 changed files with 603 additions and 43 deletions
|
|
@ -387,6 +387,7 @@ class Server {
|
|||
'/library/:library/authors',
|
||||
'/library/:library/narrators',
|
||||
'/library/:library/stats',
|
||||
'/library/:library/ratings',
|
||||
'/library/:library/series/:id?',
|
||||
'/library/:library/podcast/search',
|
||||
'/library/:library/podcast/latest',
|
||||
|
|
|
|||
|
|
@ -180,6 +180,110 @@ class ReviewController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/libraries/:id/reviews
|
||||
* Get all reviews for items in a library.
|
||||
* Supports sorting by newest, oldest, highest, lowest.
|
||||
* Supports filtering by user or rating.
|
||||
* Supports pagination with limit and page.
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async findAllForLibrary(req, res) {
|
||||
if (!Database.serverSettings.enableReviews) {
|
||||
return res.status(403).send('Review feature is disabled')
|
||||
}
|
||||
|
||||
const libraryId = req.params.id
|
||||
const { sort, filter, limit, page } = req.query
|
||||
|
||||
try {
|
||||
const where = {}
|
||||
const include = [
|
||||
{
|
||||
model: Database.userModel,
|
||||
attributes: ['id', 'username']
|
||||
},
|
||||
{
|
||||
model: Database.libraryItemModel,
|
||||
where: { libraryId },
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
model: Database.bookModel,
|
||||
include: [
|
||||
{ model: Database.authorModel, through: { attributes: [] } },
|
||||
{ model: Database.seriesModel, through: { attributes: ['id', 'sequence'] } }
|
||||
]
|
||||
},
|
||||
{
|
||||
model: Database.podcastModel,
|
||||
include: { model: Database.podcastEpisodeModel }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
if (filter) {
|
||||
const [filterType, filterValue] = filter.split('.')
|
||||
if (filterType === 'user' && filterValue) {
|
||||
where.userId = filterValue
|
||||
} else if (filterType === 'rating' && filterValue) {
|
||||
where.rating = filterValue
|
||||
}
|
||||
}
|
||||
|
||||
let order = [['createdAt', 'DESC']]
|
||||
if (sort === 'oldest') order = [['createdAt', 'ASC']]
|
||||
else if (sort === 'highest') order = [['rating', 'DESC'], ['createdAt', 'DESC']]
|
||||
else if (sort === 'lowest') order = [['rating', 'ASC'], ['createdAt', 'DESC']]
|
||||
|
||||
const limitNum = limit ? parseInt(limit) : 50
|
||||
const pageNum = page ? parseInt(page) : 0
|
||||
const offset = pageNum * limitNum
|
||||
|
||||
const { count, rows: reviews } = await Database.reviewModel.findAndCountAll({
|
||||
where,
|
||||
include,
|
||||
order,
|
||||
limit: limitNum,
|
||||
offset
|
||||
})
|
||||
|
||||
const results = reviews.map((r) => {
|
||||
const json = r.toOldJSON()
|
||||
if (r.libraryItem) {
|
||||
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
|
||||
})
|
||||
|
||||
res.json({
|
||||
reviews: results,
|
||||
total: count,
|
||||
page: pageNum,
|
||||
limit: limitNum
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error(`[ReviewController] Failed to fetch reviews for library ${libraryId}`, error)
|
||||
res.status(500).send('Failed to fetch reviews')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware for review routes.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,18 +1,32 @@
|
|||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
/**
|
||||
* @typedef ReviewJSON
|
||||
* @property {string} id
|
||||
* @property {number} rating
|
||||
* @property {string} reviewText
|
||||
* @property {string} userId
|
||||
* @property {string} libraryItemId
|
||||
* @property {number} updatedAt
|
||||
* @property {number} createdAt
|
||||
* @property {Object} [user]
|
||||
* @property {string} user.id
|
||||
* @property {string} user.username
|
||||
*/
|
||||
|
||||
class Review extends Model {
|
||||
constructor(values, options) {
|
||||
super(values, options)
|
||||
|
||||
/** @type {UUIDV4} */
|
||||
/** @type {string} */
|
||||
this.id
|
||||
/** @type {number} */
|
||||
this.rating
|
||||
/** @type {string} */
|
||||
this.reviewText
|
||||
/** @type {UUIDV4} */
|
||||
/** @type {string} */
|
||||
this.userId
|
||||
/** @type {UUIDV4} */
|
||||
/** @type {string} */
|
||||
this.libraryItemId
|
||||
/** @type {Date} */
|
||||
this.updatedAt
|
||||
|
|
@ -20,6 +34,12 @@ class Review extends Model {
|
|||
this.createdAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Review model and associations.
|
||||
* A user can have only one review per library item.
|
||||
*
|
||||
* @param {import('sequelize').Sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init(
|
||||
{
|
||||
|
|
@ -62,6 +82,11 @@ class Review extends Model {
|
|||
Review.belongsTo(libraryItem)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to the old JSON format for the browser.
|
||||
*
|
||||
* @returns {ReviewJSON}
|
||||
*/
|
||||
toOldJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
|
|
@ -79,4 +104,4 @@ class Review extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = Review
|
||||
module.exports = Review
|
||||
|
|
@ -55,7 +55,9 @@ class ServerSettings {
|
|||
this.language = 'en-us'
|
||||
this.allowedOrigins = []
|
||||
|
||||
/** @type {boolean} If true, users can rate and review library items */
|
||||
this.enableReviews = true
|
||||
/** @type {boolean} If true, the Ratings page link is shown in the library sidebar */
|
||||
this.showReviewsInSidebar = true
|
||||
|
||||
this.logLevel = Logger.logLevel
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ class ApiRouter {
|
|||
this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this))
|
||||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
||||
this.router.get('/libraries/:id/reviews', LibraryController.middleware.bind(this), ReviewController.findAllForLibrary.bind(this))
|
||||
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
||||
this.router.get('/libraries/:id/narrators', LibraryController.middleware.bind(this), LibraryController.getNarrators.bind(this))
|
||||
this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.updateNarrator.bind(this))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue