feat(ratings): enhance user rating stats retrieval with user-specific filtering and improve error messages

This commit is contained in:
Платон Петров 2026-06-13 17:15:18 +03:00
parent 85a285ac7c
commit b564acfa18
7 changed files with 33 additions and 24 deletions

View File

@ -81,6 +81,6 @@ type UserRepository interface {
GetUserLibraries(userID string) (Libraries, error)
SetUserLibraries(userID string, libraryIDs []int) error
RatingStats(ctx context.Context) ([]UserRatingStats, error)
RatingStats(ctx context.Context, userID string) ([]UserRatingStats, error)
RatingItems(ctx context.Context, userID, itemType string, rating int) ([]RatedItem, error)
}

View File

@ -478,7 +478,7 @@ func (r *userRepository) SetUserLibraries(userID string, libraryIDs []int) error
return nil
}
func (r *userRepository) RatingStats(ctx context.Context) ([]model.UserRatingStats, error) {
func (r *userRepository) RatingStats(ctx context.Context, userID string) ([]model.UserRatingStats, error) {
type row struct {
UserID string `db:"user_id"`
UserName string `db:"user_name"`
@ -487,10 +487,14 @@ func (r *userRepository) RatingStats(ctx context.Context) ([]model.UserRatingSta
Count int `db:"cnt"`
}
where := And{Gt{"a.rating": 0}, Eq{"a.item_type": []interface{}{"media_file", "album"}}}
if userID != "" {
where = append(where, Eq{"a.user_id": userID})
}
sel := Select(`a.user_id`, `u.user_name`, `a.item_type`, `a.rating`, `count(*) as cnt`).
From(`annotation a`).
Join(`"user" u ON a.user_id = u.id`).
Where(And{Gt{"a.rating": 0}, Eq{"a.item_type": []interface{}{"media_file", "album"}}}).
Where(where).
GroupBy(`a.user_id`, `u.user_name`, `a.item_type`, `a.rating`).
OrderBy(`u.user_name`, `a.item_type`, `a.rating DESC`)

View File

@ -19,22 +19,16 @@ func (api *Router) addRatingStatsRoute(r chi.Router) {
func getRatingStats(ds model.DataStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
currentUser, _ := request.UserFrom(r.Context())
stats, err := ds.User(r.Context()).RatingStats(r.Context())
filterUserID := ""
if !currentUser.IsAdmin {
filterUserID = currentUser.ID
}
stats, err := ds.User(r.Context()).RatingStats(r.Context(), filterUserID)
if err != nil {
log.Error(r.Context(), "Error getting rating stats", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !currentUser.IsAdmin {
filtered := make([]model.UserRatingStats, 0, 1)
for _, s := range stats {
if s.UserID == currentUser.ID {
filtered = append(filtered, s)
break
}
}
stats = filtered
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(stats); err != nil {
log.Error(r.Context(), "Error encoding rating stats", err)
@ -50,13 +44,21 @@ func getRatingItems(ds model.DataStore) http.HandlerFunc {
ratingStr := r.URL.Query().Get("rating")
rating, err := strconv.Atoi(ratingStr)
if err != nil || rating < 1 || rating > 5 || userID == "" || (itemType != "album" && itemType != "song") {
http.Error(w, "invalid parameters", http.StatusBadRequest)
if err != nil || rating < 1 || rating > 5 {
http.Error(w, "rating must be between 1 and 5", http.StatusBadRequest)
return
}
if userID == "" {
http.Error(w, "userId is required", http.StatusBadRequest)
return
}
if itemType != "album" && itemType != "song" {
http.Error(w, "type must be 'album' or 'song'", http.StatusBadRequest)
return
}
if !currentUser.IsAdmin && currentUser.ID != userID {
http.Error(w, "forbidden", http.StatusForbidden)
http.Error(w, "non-admin users can only query their own ratings", http.StatusForbidden)
return
}

View File

@ -137,7 +137,7 @@ func (u *MockedUserRepo) SetUserLibraries(userID string, libraryIDs []int) error
return nil
}
func (u *MockedUserRepo) RatingStats(ctx context.Context) ([]model.UserRatingStats, error) {
func (u *MockedUserRepo) RatingStats(ctx context.Context, userID string) ([]model.UserRatingStats, error) {
if u.Error != nil {
return nil, u.Error
}

View File

@ -17,7 +17,7 @@ export const AverageRatingField = ({ className, size, record = {}, ...rest }) =>
const classes = useStyles()
const translate = useTranslate()
const avg = record.averageRating || 0
const avg = Number(record.averageRating) || 0
if (avg <= 0) return null
return (

View File

@ -62,10 +62,8 @@ const UserRatingItems = ({ match }) => {
const title = `${userName} · ${typeLabel} · ${rating}★`
const getCoverUrl = (item) => {
return subsonic.getCoverArtUrl(
{ id: item.id, albumArtist: type === 'album' ? item.artist : undefined, album: type === 'song' ? item.name : undefined, updatedAt: item.updatedAt },
40,
)
const coverArtId = type === 'album' ? item.id : item.albumId
return subsonic.getCoverArtUrl({ id: coverArtId, updatedAt: item.updatedAt }, 40)
}
return (

View File

@ -131,7 +131,12 @@ const RatingTable = ({ stats, label, userId, userName, type }) => {
onClick={() => count > 0 && history.push(linkTo)}
tabIndex={count > 0 ? 0 : undefined}
role={count > 0 ? 'button' : undefined}
onKeyDown={count > 0 ? (e) => (e.key === 'Enter' || e.key === ' ') && history.push(linkTo) : undefined}
onKeyDown={count > 0 ? (e) => {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
e.preventDefault()
history.push(linkTo)
}
} : undefined}
>
<td className={classes.ratingCell}>{r}.0</td>
<td className={classes.countCell}>{count}</td>