diff --git a/conf/configuration.go b/conf/configuration.go
index 29e6582e3..9bc07e639 100644
--- a/conf/configuration.go
+++ b/conf/configuration.go
@@ -152,6 +152,7 @@ type subsonicOptions struct {
AppendSubtitle bool
ArtistParticipations bool
DefaultReportRealPath bool
+ EnableAverageRating bool
LegacyClients string
MinimalClients string
}
@@ -605,6 +606,7 @@ func setViperDefaults() {
viper.SetDefault("subsonic.appendsubtitle", true)
viper.SetDefault("subsonic.artistparticipations", false)
viper.SetDefault("subsonic.defaultreportrealpath", false)
+ viper.SetDefault("subsonic.enableaveragerating", true)
viper.SetDefault("subsonic.legacyclients", "DSub,SubMusic")
viper.SetDefault("agents", "lastfm,spotify,deezer")
viper.SetDefault("lastfm.enabled", true)
diff --git a/db/migrations/20260117201522_add_avg_rating_column.sql b/db/migrations/20260117201522_add_avg_rating_column.sql
new file mode 100644
index 000000000..f5c8d4522
--- /dev/null
+++ b/db/migrations/20260117201522_add_avg_rating_column.sql
@@ -0,0 +1,23 @@
+-- +goose Up
+ALTER TABLE album ADD COLUMN average_rating REAL NOT NULL DEFAULT 0;
+ALTER TABLE media_file ADD COLUMN average_rating REAL NOT NULL DEFAULT 0;
+ALTER TABLE artist ADD COLUMN average_rating REAL NOT NULL DEFAULT 0;
+
+-- Populate average_rating from existing ratings
+UPDATE album SET average_rating = coalesce(
+ (SELECT round(avg(rating), 2) FROM annotation WHERE item_id = album.id AND item_type = 'album' AND rating > 0),
+ 0
+);
+UPDATE media_file SET average_rating = coalesce(
+ (SELECT round(avg(rating), 2) FROM annotation WHERE item_id = media_file.id AND item_type = 'media_file' AND rating > 0),
+ 0
+);
+UPDATE artist SET average_rating = coalesce(
+ (SELECT round(avg(rating), 2) FROM annotation WHERE item_id = artist.id AND item_type = 'artist' AND rating > 0),
+ 0
+);
+
+-- +goose Down
+ALTER TABLE artist DROP COLUMN average_rating;
+ALTER TABLE media_file DROP COLUMN average_rating;
+ALTER TABLE album DROP COLUMN average_rating;
diff --git a/model/annotation.go b/model/annotation.go
index fbff5f178..5228028a6 100644
--- a/model/annotation.go
+++ b/model/annotation.go
@@ -3,12 +3,13 @@ package model
import "time"
type Annotations struct {
- PlayCount int64 `structs:"play_count" json:"playCount,omitempty"`
- PlayDate *time.Time `structs:"play_date" json:"playDate,omitempty" `
- Rating int `structs:"rating" json:"rating,omitempty" `
- RatedAt *time.Time `structs:"rated_at" json:"ratedAt,omitempty" `
- Starred bool `structs:"starred" json:"starred,omitempty" `
- StarredAt *time.Time `structs:"starred_at" json:"starredAt,omitempty"`
+ PlayCount int64 `structs:"play_count" json:"playCount,omitempty"`
+ PlayDate *time.Time `structs:"play_date" json:"playDate,omitempty" `
+ Rating int `structs:"rating" json:"rating,omitempty" `
+ RatedAt *time.Time `structs:"rated_at" json:"ratedAt,omitempty" `
+ Starred bool `structs:"starred" json:"starred,omitempty" `
+ StarredAt *time.Time `structs:"starred_at" json:"starredAt,omitempty"`
+ AverageRating float64 `structs:"average_rating" json:"averageRating,omitempty"`
}
type AnnotatedRepository interface {
diff --git a/persistence/album_repository_test.go b/persistence/album_repository_test.go
index 284d4dc5e..612e459f0 100644
--- a/persistence/album_repository_test.go
+++ b/persistence/album_repository_test.go
@@ -126,6 +126,89 @@ var _ = Describe("AlbumRepository", func() {
)
})
+ Describe("Album.AverageRating", func() {
+ It("returns 0 when no ratings exist", func() {
+ newID := id.NewRandom()
+ Expect(albumRepo.Put(&model.Album{LibraryID: 1, ID: newID, Name: "no ratings album"})).To(Succeed())
+
+ album, err := albumRepo.Get(newID)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(album.AverageRating).To(Equal(0.0))
+
+ _, _ = albumRepo.executeSQL(squirrel.Delete("album").Where(squirrel.Eq{"id": newID}))
+ })
+
+ It("returns the user's rating as average when only one user rated", func() {
+ newID := id.NewRandom()
+ Expect(albumRepo.Put(&model.Album{LibraryID: 1, ID: newID, Name: "single rating album"})).To(Succeed())
+ Expect(albumRepo.SetRating(4, newID)).To(Succeed())
+
+ album, err := albumRepo.Get(newID)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(album.AverageRating).To(Equal(4.0))
+
+ _, _ = albumRepo.executeSQL(squirrel.Delete("annotation").Where(squirrel.Eq{"item_id": newID}))
+ _, _ = albumRepo.executeSQL(squirrel.Delete("album").Where(squirrel.Eq{"id": newID}))
+ })
+
+ It("calculates average across multiple users", func() {
+ newID := id.NewRandom()
+ Expect(albumRepo.Put(&model.Album{LibraryID: 1, ID: newID, Name: "multi rating album"})).To(Succeed())
+
+ Expect(albumRepo.SetRating(4, newID)).To(Succeed())
+
+ user2Ctx := request.WithUser(GinkgoT().Context(), regularUser)
+ user2Repo := NewAlbumRepository(user2Ctx, GetDBXBuilder()).(*albumRepository)
+ Expect(user2Repo.SetRating(5, newID)).To(Succeed())
+
+ album, err := albumRepo.Get(newID)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(album.AverageRating).To(Equal(4.5))
+
+ _, _ = albumRepo.executeSQL(squirrel.Delete("annotation").Where(squirrel.Eq{"item_id": newID}))
+ _, _ = albumRepo.executeSQL(squirrel.Delete("album").Where(squirrel.Eq{"id": newID}))
+ })
+
+ It("excludes zero ratings from average calculation", func() {
+ newID := id.NewRandom()
+ Expect(albumRepo.Put(&model.Album{LibraryID: 1, ID: newID, Name: "zero rating excluded album"})).To(Succeed())
+ Expect(albumRepo.SetRating(3, newID)).To(Succeed())
+
+ user2Ctx := request.WithUser(GinkgoT().Context(), regularUser)
+ user2Repo := NewAlbumRepository(user2Ctx, GetDBXBuilder()).(*albumRepository)
+ Expect(user2Repo.SetRating(0, newID)).To(Succeed())
+
+ album, err := albumRepo.Get(newID)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(album.AverageRating).To(Equal(3.0))
+
+ _, _ = albumRepo.executeSQL(squirrel.Delete("annotation").Where(squirrel.Eq{"item_id": newID}))
+ _, _ = albumRepo.executeSQL(squirrel.Delete("album").Where(squirrel.Eq{"id": newID}))
+ })
+
+ It("rounds to 2 decimal places", func() {
+ newID := id.NewRandom()
+ Expect(albumRepo.Put(&model.Album{LibraryID: 1, ID: newID, Name: "rounding test album"})).To(Succeed())
+
+ Expect(albumRepo.SetRating(5, newID)).To(Succeed())
+
+ user2Ctx := request.WithUser(GinkgoT().Context(), regularUser)
+ user2Repo := NewAlbumRepository(user2Ctx, GetDBXBuilder()).(*albumRepository)
+ Expect(user2Repo.SetRating(4, newID)).To(Succeed())
+
+ user3Ctx := request.WithUser(GinkgoT().Context(), thirdUser)
+ user3Repo := NewAlbumRepository(user3Ctx, GetDBXBuilder()).(*albumRepository)
+ Expect(user3Repo.SetRating(4, newID)).To(Succeed())
+
+ album, err := albumRepo.Get(newID)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(album.AverageRating).To(Equal(4.33)) // (5 + 4 + 4) / 3 = 4.333...
+
+ _, _ = albumRepo.executeSQL(squirrel.Delete("annotation").Where(squirrel.Eq{"item_id": newID}))
+ _, _ = albumRepo.executeSQL(squirrel.Delete("album").Where(squirrel.Eq{"id": newID}))
+ })
+ })
+
Describe("dbAlbum mapping", func() {
var (
a model.Album
diff --git a/persistence/mediafile_repository_test.go b/persistence/mediafile_repository_test.go
index 35fda0873..e33639721 100644
--- a/persistence/mediafile_repository_test.go
+++ b/persistence/mediafile_repository_test.go
@@ -157,6 +157,74 @@ var _ = Describe("MediaRepository", func() {
Expect(mf.PlayCount).To(Equal(int64(1)))
})
+ Describe("AverageRating", func() {
+ var raw *mediaFileRepository
+
+ BeforeEach(func() {
+ raw = mr.(*mediaFileRepository)
+ })
+
+ It("returns 0 when no ratings exist", func() {
+ newID := id.NewRandom()
+ Expect(mr.Put(&model.MediaFile{LibraryID: 1, ID: newID, Path: "/test/no-rating.mp3"})).To(Succeed())
+
+ mf, err := mr.Get(newID)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(mf.AverageRating).To(Equal(0.0))
+
+ _, _ = raw.executeSQL(squirrel.Delete("media_file").Where(squirrel.Eq{"id": newID}))
+ })
+
+ It("returns the user's rating as average when only one user rated", func() {
+ newID := id.NewRandom()
+ Expect(mr.Put(&model.MediaFile{LibraryID: 1, ID: newID, Path: "/test/single-rating.mp3"})).To(Succeed())
+ Expect(mr.SetRating(5, newID)).To(Succeed())
+
+ mf, err := mr.Get(newID)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(mf.AverageRating).To(Equal(5.0))
+
+ _, _ = raw.executeSQL(squirrel.Delete("annotation").Where(squirrel.Eq{"item_id": newID}))
+ _, _ = raw.executeSQL(squirrel.Delete("media_file").Where(squirrel.Eq{"id": newID}))
+ })
+
+ It("calculates average across multiple users", func() {
+ newID := id.NewRandom()
+ Expect(mr.Put(&model.MediaFile{LibraryID: 1, ID: newID, Path: "/test/multi-rating.mp3"})).To(Succeed())
+
+ Expect(mr.SetRating(3, newID)).To(Succeed())
+
+ user2Ctx := request.WithUser(GinkgoT().Context(), regularUser)
+ user2Repo := NewMediaFileRepository(user2Ctx, GetDBXBuilder())
+ Expect(user2Repo.SetRating(5, newID)).To(Succeed())
+
+ mf, err := mr.Get(newID)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(mf.AverageRating).To(Equal(4.0))
+
+ _, _ = raw.executeSQL(squirrel.Delete("annotation").Where(squirrel.Eq{"item_id": newID}))
+ _, _ = raw.executeSQL(squirrel.Delete("media_file").Where(squirrel.Eq{"id": newID}))
+ })
+
+ It("excludes zero ratings from average calculation", func() {
+ newID := id.NewRandom()
+ Expect(mr.Put(&model.MediaFile{LibraryID: 1, ID: newID, Path: "/test/zero-excluded.mp3"})).To(Succeed())
+
+ Expect(mr.SetRating(4, newID)).To(Succeed())
+
+ user2Ctx := request.WithUser(GinkgoT().Context(), regularUser)
+ user2Repo := NewMediaFileRepository(user2Ctx, GetDBXBuilder())
+ Expect(user2Repo.SetRating(0, newID)).To(Succeed())
+
+ mf, err := mr.Get(newID)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(mf.AverageRating).To(Equal(4.0))
+
+ _, _ = raw.executeSQL(squirrel.Delete("annotation").Where(squirrel.Eq{"item_id": newID}))
+ _, _ = raw.executeSQL(squirrel.Delete("media_file").Where(squirrel.Eq{"id": newID}))
+ })
+ })
+
It("preserves play date if and only if provided date is older", func() {
id := "incplay.playdate"
Expect(mr.Put(&model.MediaFile{LibraryID: 1, ID: id})).To(BeNil())
diff --git a/persistence/persistence_suite_test.go b/persistence/persistence_suite_test.go
index f3cb4f3d0..559ca3d4c 100644
--- a/persistence/persistence_suite_test.go
+++ b/persistence/persistence_suite_test.go
@@ -130,7 +130,8 @@ var (
var (
adminUser = model.User{ID: "userid", UserName: "userid", Name: "admin", Email: "admin@email.com", IsAdmin: true}
regularUser = model.User{ID: "2222", UserName: "regular-user", Name: "Regular User", Email: "regular@example.com"}
- testUsers = model.Users{adminUser, regularUser}
+ thirdUser = model.User{ID: "3333", UserName: "third-user", Name: "Third User", Email: "third@example.com"}
+ testUsers = model.Users{adminUser, regularUser, thirdUser}
)
func p(path string) string {
diff --git a/persistence/sql_annotations.go b/persistence/sql_annotations.go
index 108e9be94..fac519829 100644
--- a/persistence/sql_annotations.go
+++ b/persistence/sql_annotations.go
@@ -17,7 +17,7 @@ const annotationTable = "annotation"
func (r sqlRepository) withAnnotation(query SelectBuilder, idField string) SelectBuilder {
userID := loggedUser(r.ctx).ID
if userID == invalidUserId {
- return query
+ return query.Columns(fmt.Sprintf("%s.average_rating", r.tableName))
}
query = query.
LeftJoin("annotation on ("+
@@ -38,6 +38,8 @@ func (r sqlRepository) withAnnotation(query SelectBuilder, idField string) Selec
query = query.Columns("coalesce(play_count, 0) as play_count")
}
+ query = query.Columns(fmt.Sprintf("%s.average_rating", r.tableName))
+
return query
}
@@ -79,7 +81,22 @@ func (r sqlRepository) SetStar(starred bool, ids ...string) error {
func (r sqlRepository) SetRating(rating int, itemID string) error {
ratedAt := time.Now()
- return r.annUpsert(map[string]interface{}{"rating": rating, "rated_at": ratedAt}, itemID)
+ err := r.annUpsert(map[string]interface{}{"rating": rating, "rated_at": ratedAt}, itemID)
+ if err != nil {
+ return err
+ }
+ return r.updateAvgRating(itemID)
+}
+
+func (r sqlRepository) updateAvgRating(itemID string) error {
+ upd := Update(r.tableName).
+ Where(Eq{"id": itemID}).
+ Set("average_rating", Expr(
+ "coalesce((select round(avg(rating), 2) from annotation where item_id = ? and item_type = ? and rating > 0), 0)",
+ itemID, r.tableName,
+ ))
+ _, err := r.executeSQL(upd)
+ return err
}
func (r sqlRepository) IncPlayCount(itemID string, ts time.Time) error {
diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go
index ba3fb058a..30779e420 100644
--- a/server/subsonic/browsing.go
+++ b/server/subsonic/browsing.go
@@ -410,6 +410,9 @@ func (api *Router) buildArtistDirectory(ctx context.Context, artist *model.Artis
}
dir.AlbumCount = getArtistAlbumCount(artist)
dir.UserRating = int32(artist.Rating)
+ if conf.Server.Subsonic.EnableAverageRating {
+ dir.AverageRating = artist.AverageRating
+ }
if artist.Starred {
dir.Starred = artist.StarredAt
}
@@ -447,6 +450,9 @@ func (api *Router) buildAlbumDirectory(ctx context.Context, album *model.Album)
dir.Played = album.PlayDate
}
dir.UserRating = int32(album.Rating)
+ if conf.Server.Subsonic.EnableAverageRating {
+ dir.AverageRating = album.AverageRating
+ }
dir.SongCount = int32(album.SongCount)
dir.CoverArt = album.CoverArtID().String()
if album.Starred {
diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go
index 2303914d6..dfeb4c6dd 100644
--- a/server/subsonic/helpers.go
+++ b/server/subsonic/helpers.go
@@ -101,6 +101,9 @@ func toArtist(r *http.Request, a model.Artist) responses.Artist {
CoverArt: a.CoverArtID().String(),
ArtistImageUrl: publicurl.ImageURL(r, a.CoverArtID(), 600),
}
+ if conf.Server.Subsonic.EnableAverageRating {
+ artist.AverageRating = a.AverageRating
+ }
if a.Starred {
artist.Starred = a.StarredAt
}
@@ -116,6 +119,9 @@ func toArtistID3(r *http.Request, a model.Artist) responses.ArtistID3 {
ArtistImageUrl: publicurl.ImageURL(r, a.CoverArtID(), 600),
UserRating: int32(a.Rating),
}
+ if conf.Server.Subsonic.EnableAverageRating {
+ artist.AverageRating = a.AverageRating
+ }
if a.Starred {
artist.Starred = a.StarredAt
}
@@ -218,6 +224,9 @@ func childFromMediaFile(ctx context.Context, mf model.MediaFile) responses.Child
child.Starred = mf.StarredAt
}
child.UserRating = int32(mf.Rating)
+ if conf.Server.Subsonic.EnableAverageRating {
+ child.AverageRating = mf.AverageRating
+ }
format, _ := getTranscoding(ctx)
if mf.Suffix != "" && format != "" && mf.Suffix != format {
@@ -329,6 +338,9 @@ func childFromAlbum(ctx context.Context, al model.Album) responses.Child {
}
child.PlayCount = al.PlayCount
child.UserRating = int32(al.Rating)
+ if conf.Server.Subsonic.EnableAverageRating {
+ child.AverageRating = al.AverageRating
+ }
child.OpenSubsonicChild = osChildFromAlbum(ctx, al)
return child
}
@@ -422,6 +434,9 @@ func buildOSAlbumID3(ctx context.Context, album model.Album) *responses.OpenSubs
dir.Played = album.PlayDate
}
dir.UserRating = int32(album.Rating)
+ if conf.Server.Subsonic.EnableAverageRating {
+ dir.AverageRating = album.AverageRating
+ }
dir.RecordLabels = slice.Map(album.Tags.Values(model.TagRecordLabel), func(s string) responses.RecordLabel {
return responses.RecordLabel{Name: s}
})
diff --git a/server/subsonic/helpers_test.go b/server/subsonic/helpers_test.go
index 2a5f43765..8d86b0010 100644
--- a/server/subsonic/helpers_test.go
+++ b/server/subsonic/helpers_test.go
@@ -456,4 +456,131 @@ var _ = Describe("helpers", func() {
})
})
})
+
+ Describe("AverageRating in responses", func() {
+ var ctx context.Context
+
+ BeforeEach(func() {
+ ctx = context.Background()
+ conf.Server.Subsonic.EnableAverageRating = true
+ })
+
+ Describe("childFromMediaFile", func() {
+ It("includes averageRating when set", func() {
+ mf := model.MediaFile{
+ ID: "mf-avg-1",
+ Title: "Test Song",
+ Annotations: model.Annotations{
+ AverageRating: 4.5,
+ },
+ }
+ child := childFromMediaFile(ctx, mf)
+ Expect(child.AverageRating).To(Equal(4.5))
+ })
+
+ It("returns 0 for averageRating when not set", func() {
+ mf := model.MediaFile{
+ ID: "mf-avg-2",
+ Title: "Test Song No Rating",
+ }
+ child := childFromMediaFile(ctx, mf)
+ Expect(child.AverageRating).To(Equal(0.0))
+ })
+ })
+
+ Describe("childFromAlbum", func() {
+ It("includes averageRating when set", func() {
+ al := model.Album{
+ ID: "al-avg-1",
+ Name: "Test Album",
+ Annotations: model.Annotations{
+ AverageRating: 3.75,
+ },
+ }
+ child := childFromAlbum(ctx, al)
+ Expect(child.AverageRating).To(Equal(3.75))
+ })
+
+ It("returns 0 for averageRating when not set", func() {
+ al := model.Album{
+ ID: "al-avg-2",
+ Name: "Test Album No Rating",
+ }
+ child := childFromAlbum(ctx, al)
+ Expect(child.AverageRating).To(Equal(0.0))
+ })
+ })
+
+ Describe("toArtist", func() {
+ It("includes averageRating when set", func() {
+ conf.Server.Subsonic.EnableAverageRating = true
+ r := httptest.NewRequest("GET", "/test", nil)
+ a := model.Artist{
+ ID: "ar-avg-1",
+ Name: "Test Artist",
+ Annotations: model.Annotations{
+ AverageRating: 5.0,
+ },
+ }
+ artist := toArtist(r, a)
+ Expect(artist.AverageRating).To(Equal(5.0))
+ })
+ })
+
+ Describe("toArtistID3", func() {
+ It("includes averageRating when set", func() {
+ conf.Server.Subsonic.EnableAverageRating = true
+ r := httptest.NewRequest("GET", "/test", nil)
+ a := model.Artist{
+ ID: "ar-avg-2",
+ Name: "Test Artist ID3",
+ Annotations: model.Annotations{
+ AverageRating: 2.5,
+ },
+ }
+ artist := toArtistID3(r, a)
+ Expect(artist.AverageRating).To(Equal(2.5))
+ })
+ })
+
+ Describe("EnableAverageRating config", func() {
+ It("excludes averageRating when disabled", func() {
+ conf.Server.Subsonic.EnableAverageRating = false
+
+ mf := model.MediaFile{
+ ID: "mf-cfg-1",
+ Title: "Test Song",
+ Annotations: model.Annotations{
+ AverageRating: 4.5,
+ },
+ }
+ child := childFromMediaFile(ctx, mf)
+ Expect(child.AverageRating).To(Equal(0.0))
+
+ al := model.Album{
+ ID: "al-cfg-1",
+ Name: "Test Album",
+ Annotations: model.Annotations{
+ AverageRating: 3.75,
+ },
+ }
+ albumChild := childFromAlbum(ctx, al)
+ Expect(albumChild.AverageRating).To(Equal(0.0))
+
+ r := httptest.NewRequest("GET", "/test", nil)
+ a := model.Artist{
+ ID: "ar-cfg-1",
+ Name: "Test Artist",
+ Annotations: model.Annotations{
+ AverageRating: 5.0,
+ },
+ }
+ artist := toArtist(r, a)
+ Expect(artist.AverageRating).To(Equal(0.0))
+
+ artistID3 := toArtistID3(r, a)
+ Expect(artistID3.AverageRating).To(Equal(0.0))
+ })
+ })
+ })
})
diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go
index de47c54e2..c5e07395e 100644
--- a/server/subsonic/responses/responses.go
+++ b/server/subsonic/responses/responses.go
@@ -95,11 +95,9 @@ type Artist struct {
Name string `xml:"name,attr" json:"name"`
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
UserRating int32 `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
+ AverageRating float64 `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"`
CoverArt string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
ArtistImageUrl string `xml:"artistImageUrl,attr,omitempty" json:"artistImageUrl,omitempty"`
- /* TODO:
-
- */
}
type Index struct {
@@ -160,13 +158,11 @@ type Child struct {
ArtistId string `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
Type string `xml:"type,attr,omitempty" json:"type,omitempty"`
UserRating int32 `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
+ AverageRating float64 `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"`
SongCount int32 `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
IsVideo bool `xml:"isVideo,attr,omitempty" json:"isVideo,omitempty"`
BookmarkPosition int64 `xml:"bookmarkPosition,attr,omitempty" json:"bookmarkPosition,omitempty"`
- /*
-
- */
- *OpenSubsonicChild `xml:",omitempty" json:",omitempty"`
+ *OpenSubsonicChild `xml:",omitempty" json:",omitempty"`
}
type OpenSubsonicChild struct {
@@ -198,14 +194,15 @@ type Songs struct {
}
type Directory struct {
- Child []Child `xml:"child" json:"child,omitempty"`
- Id string `xml:"id,attr" json:"id"`
- Name string `xml:"name,attr" json:"name"`
- Parent string `xml:"parent,attr,omitempty" json:"parent,omitempty"`
- Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
- PlayCount int64 `xml:"playCount,attr,omitempty" json:"playCount,omitempty"`
- Played *time.Time `xml:"played,attr,omitempty" json:"played,omitempty"`
- UserRating int32 `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
+ Child []Child `xml:"child" json:"child,omitempty"`
+ Id string `xml:"id,attr" json:"id"`
+ Name string `xml:"name,attr" json:"name"`
+ Parent string `xml:"parent,attr,omitempty" json:"parent,omitempty"`
+ Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
+ PlayCount int64 `xml:"playCount,attr,omitempty" json:"playCount,omitempty"`
+ Played *time.Time `xml:"played,attr,omitempty" json:"played,omitempty"`
+ UserRating int32 `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
+ AverageRating float64 `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"`
// ID3
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
@@ -217,10 +214,6 @@ type Directory struct {
Created *time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
Year int32 `xml:"year,attr,omitempty" json:"year,omitempty"`
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
-
- /*
-
- */
}
// ArtistID3Ref is a reference to an artist, a simplified version of ArtistID3. This is used to resolve the
@@ -237,6 +230,7 @@ type ArtistID3 struct {
AlbumCount int32 `xml:"albumCount,attr" json:"albumCount"`
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
UserRating int32 `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
+ AverageRating float64 `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"`
ArtistImageUrl string `xml:"artistImageUrl,attr,omitempty" json:"artistImageUrl,omitempty"`
*OpenSubsonicArtistID3 `xml:",omitempty" json:",omitempty"`
}
@@ -268,6 +262,7 @@ type OpenSubsonicAlbumID3 struct {
// OpenSubsonic extensions
Played *time.Time `xml:"played,attr,omitempty" json:"played,omitempty"`
UserRating int32 `xml:"userRating,attr,omitempty" json:"userRating"`
+ AverageRating float64 `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"`
Genres Array[ItemGenre] `xml:"genres,omitempty" json:"genres"`
MusicBrainzId string `xml:"musicBrainzId,attr,omitempty" json:"musicBrainzId"`
IsCompilation bool `xml:"isCompilation,attr,omitempty" json:"isCompilation"`