feat(subsonic): add support for track/album/artist ratings/stars

fixes #171
fixes #31

* Initial code. Compiles and passes unit tests.

* Moved average rating calculation from rating fetch to set rating function. Still only compiled and unit tested.

* Bug fixes

* Fixed bug in savePlayQueue. Removed unique_index for star / rating entries because it's not valid.

* Changed time format on stars to RFC3339Nano to match created date format.

* Lint fixes.

* More lint fixes.

* Removed add* functions and replaced with Preload.

* Fixed several bugs in handlers for getStarred and getStarred2.

* Fixed bug when using music folder ID.

Co-authored-by: Brian Doherty <brian@hplaptop.dohertyfamily.me>
This commit is contained in:
brian-doherty
2022-10-25 19:37:44 -05:00
committed by sentriz
parent 25b39085d8
commit e8759cb6c1
10 changed files with 666 additions and 133 deletions

View File

@@ -9,14 +9,21 @@ import (
func NewAlbumByFolder(f *db.Album) *Album {
a := &Album{
Artist: f.Parent.RightPath,
ID: f.SID(),
IsDir: true,
ParentID: f.ParentSID(),
Title: f.RightPath,
TrackCount: f.ChildCount,
Duration: f.Duration,
Created: f.CreatedAt,
Artist: f.Parent.RightPath,
ID: f.SID(),
IsDir: true,
ParentID: f.ParentSID(),
Title: f.RightPath,
TrackCount: f.ChildCount,
Duration: f.Duration,
Created: f.CreatedAt,
AverageRating: formatRating(f.AverageRating),
}
if f.AlbumStar != nil {
a.Starred = &f.AlbumStar.StarDate
}
if f.AlbumRating != nil {
a.UserRating = f.AlbumRating.Rating
}
if f.Cover != "" {
a.CoverID = f.SID()
@@ -26,11 +33,18 @@ func NewAlbumByFolder(f *db.Album) *Album {
func NewTCAlbumByFolder(f *db.Album) *TrackChild {
trCh := &TrackChild{
ID: f.SID(),
IsDir: true,
Title: f.RightPath,
ParentID: f.ParentSID(),
CreatedAt: f.CreatedAt,
ID: f.SID(),
IsDir: true,
Title: f.RightPath,
ParentID: f.ParentSID(),
CreatedAt: f.CreatedAt,
AverageRating: formatRating(f.AverageRating),
}
if f.AlbumStar != nil {
trCh.Starred = &f.AlbumStar.StarDate
}
if f.AlbumRating != nil {
trCh.UserRating = f.AlbumRating.Rating
}
if f.Cover != "" {
trCh.CoverID = f.SID()
@@ -53,14 +67,15 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild {
parent.RightPath,
t.Filename,
),
ParentID: parent.SID(),
Duration: t.Length,
Genre: strings.Join(t.GenreStrings(), ", "),
Year: parent.TagYear,
Bitrate: t.Bitrate,
IsDir: false,
Type: "music",
CreatedAt: t.CreatedAt,
ParentID: parent.SID(),
Duration: t.Length,
Genre: strings.Join(t.GenreStrings(), ", "),
Year: parent.TagYear,
Bitrate: t.Bitrate,
IsDir: false,
Type: "music",
CreatedAt: t.CreatedAt,
AverageRating: formatRating(t.AverageRating),
}
if trCh.Title == "" {
trCh.Title = t.Filename
@@ -71,6 +86,12 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild {
if t.Album != nil {
trCh.Album = t.Album.RightPath
}
if t.TrackStar != nil {
trCh.Starred = &t.TrackStar.StarDate
}
if t.TrackRating != nil {
trCh.UserRating = t.TrackRating.Rating
}
return trCh
}
@@ -80,9 +101,16 @@ func NewArtistByFolder(f *db.Album) *Artist {
// from an "album" where
// maybe TODO: rename the Album model to Folder
a := &Artist{
ID: f.SID(),
Name: f.RightPath,
AlbumCount: f.ChildCount,
ID: f.SID(),
Name: f.RightPath,
AlbumCount: f.ChildCount,
AverageRating: formatRating(f.AverageRating),
}
if f.AlbumStar != nil {
a.Starred = &f.AlbumStar.StarDate
}
if f.AlbumRating != nil {
a.UserRating = f.AlbumRating.Rating
}
if f.Cover != "" {
a.CoverID = f.SID()
@@ -91,10 +119,18 @@ func NewArtistByFolder(f *db.Album) *Artist {
}
func NewDirectoryByFolder(f *db.Album, children []*TrackChild) *Directory {
return &Directory{
ID: f.SID(),
Name: f.RightPath,
Children: children,
ParentID: f.ParentSID(),
d := &Directory{
ID: f.SID(),
Name: f.RightPath,
Children: children,
ParentID: f.ParentSID(),
AverageRating: formatRating(f.AverageRating),
}
if f.AlbumStar != nil {
d.Starred = &f.AlbumStar.StarDate
}
if f.AlbumRating != nil {
d.UserRating = f.AlbumRating.Rating
}
return d
}

View File

@@ -9,17 +9,24 @@ import (
func NewAlbumByTags(a *db.Album, artist *db.Artist) *Album {
ret := &Album{
Created: a.CreatedAt,
ID: a.SID(),
Name: a.TagTitle,
Year: a.TagYear,
TrackCount: a.ChildCount,
Genre: strings.Join(a.GenreStrings(), ", "),
Duration: a.Duration,
Created: a.CreatedAt,
ID: a.SID(),
Name: a.TagTitle,
Year: a.TagYear,
TrackCount: a.ChildCount,
Genre: strings.Join(a.GenreStrings(), ", "),
Duration: a.Duration,
AverageRating: formatRating(a.AverageRating),
}
if a.Cover != "" {
ret.CoverID = a.SID()
}
if a.AlbumStar != nil {
ret.Starred = &a.AlbumStar.StarDate
}
if a.AlbumRating != nil {
ret.UserRating = a.AlbumRating.Rating
}
if artist != nil {
ret.Artist = artist.Name
ret.ArtistID = artist.SID()
@@ -44,17 +51,24 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild {
album.RightPath,
t.Filename,
),
Album: album.TagTitle,
AlbumID: album.SID(),
Genre: strings.Join(t.GenreStrings(), ", "),
Duration: t.Length,
Bitrate: t.Bitrate,
Type: "music",
Year: album.TagYear,
Album: album.TagTitle,
AlbumID: album.SID(),
Genre: strings.Join(t.GenreStrings(), ", "),
Duration: t.Length,
Bitrate: t.Bitrate,
Type: "music",
Year: album.TagYear,
AverageRating: formatRating(t.AverageRating),
}
if album.Cover != "" {
ret.CoverID = album.SID()
}
if t.TrackStar != nil {
ret.Starred = &t.TrackStar.StarDate
}
if t.TrackRating != nil {
ret.UserRating = t.TrackRating.Rating
}
if album.TagArtist != nil {
ret.ArtistID = album.TagArtist.SID()
}
@@ -73,13 +87,20 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild {
func NewArtistByTags(a *db.Artist) *Artist {
r := &Artist{
ID: a.SID(),
Name: a.Name,
AlbumCount: a.AlbumCount,
ID: a.SID(),
Name: a.Name,
AlbumCount: a.AlbumCount,
AverageRating: formatRating(a.AverageRating),
}
if a.Cover != "" {
r.CoverID = a.SID()
}
if a.ArtistStar != nil {
r.Starred = &a.ArtistStar.StarDate
}
if a.ArtistRating != nil {
r.UserRating = a.ArtistRating.Rating
}
return r
}

View File

@@ -14,48 +14,48 @@ const (
)
type SubsonicResponse struct {
Response Response `xml:"subsonic-response" json:"subsonic-response"`
Response Response `xml:"subsonic-response" json:"subsonic-response"`
}
type Response struct {
Status string `xml:"status,attr" json:"status"`
Version string `xml:"version,attr" json:"version"`
XMLNS string `xml:"xmlns,attr" json:"-"`
Type string `xml:"type,attr" json:"type"`
Error *Error `xml:"error" json:"error,omitempty"`
Albums *Albums `xml:"albumList" json:"albumList,omitempty"`
AlbumsTwo *Albums `xml:"albumList2" json:"albumList2,omitempty"`
Album *Album `xml:"album" json:"album,omitempty"`
Track *TrackChild `xml:"song" json:"song,omitempty"`
Indexes *Indexes `xml:"indexes" json:"indexes,omitempty"`
Artists *Artists `xml:"artists" json:"artists,omitempty"`
Artist *Artist `xml:"artist" json:"artist,omitempty"`
Directory *Directory `xml:"directory" json:"directory,omitempty"`
RandomTracks *RandomTracks `xml:"randomSongs" json:"randomSongs,omitempty"`
TracksByGenre *TracksByGenre `xml:"songsByGenre" json:"songsByGenre,omitempty"`
MusicFolders *MusicFolders `xml:"musicFolders" json:"musicFolders,omitempty"`
ScanStatus *ScanStatus `xml:"scanStatus" json:"scanStatus,omitempty"`
Licence *Licence `xml:"license" json:"license,omitempty"`
SearchResultTwo *SearchResultTwo `xml:"searchResult2" json:"searchResult2,omitempty"`
SearchResultThree *SearchResultThree `xml:"searchResult3" json:"searchResult3,omitempty"`
User *User `xml:"user" json:"user,omitempty"`
Playlists *Playlists `xml:"playlists" json:"playlists,omitempty"`
Playlist *Playlist `xml:"playlist" json:"playlist,omitempty"`
ArtistInfo *ArtistInfo `xml:"artistInfo" json:"artistInfo,omitempty"`
ArtistInfoTwo *ArtistInfo `xml:"artistInfo2" json:"artistInfo2,omitempty"`
Genres *Genres `xml:"genres" json:"genres,omitempty"`
PlayQueue *PlayQueue `xml:"playQueue" json:"playQueue,omitempty"`
JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus" json:"jukeboxStatus,omitempty"`
JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist" json:"jukeboxPlaylist,omitempty"`
Podcasts *Podcasts `xml:"podcasts" json:"podcasts,omitempty"`
NewestPodcasts *NewestPodcasts `xml:"newestPodcasts" json:"newestPodcasts,omitempty"`
Bookmarks *Bookmarks `xml:"bookmarks" json:"bookmarks,omitempty"`
Starred *Starred `xml:"starred" json:"starred,omitempty"`
StarredTwo *StarredTwo `xml:"starred2" json:"starred2,omitempty"`
TopSongs *TopSongs `xml:"topSongs" json:"topSongs,omitempty"`
SimilarSongs *SimilarSongs `xml:"similarSongs" json:"similarSongs,omitempty"`
SimilarSongsTwo *SimilarSongsTwo `xml:"similarSongs2" json:"similarSongs2,omitempty"`
InternetRadioStations *InternetRadioStations `xml:"internetRadioStations" json:"internetRadioStations,omitempty"`
Status string `xml:"status,attr" json:"status"`
Version string `xml:"version,attr" json:"version"`
XMLNS string `xml:"xmlns,attr" json:"-"`
Type string `xml:"type,attr" json:"type"`
Error *Error `xml:"error" json:"error,omitempty"`
Albums *Albums `xml:"albumList" json:"albumList,omitempty"`
AlbumsTwo *Albums `xml:"albumList2" json:"albumList2,omitempty"`
Album *Album `xml:"album" json:"album,omitempty"`
Track *TrackChild `xml:"song" json:"song,omitempty"`
Indexes *Indexes `xml:"indexes" json:"indexes,omitempty"`
Artists *Artists `xml:"artists" json:"artists,omitempty"`
Artist *Artist `xml:"artist" json:"artist,omitempty"`
Directory *Directory `xml:"directory" json:"directory,omitempty"`
RandomTracks *RandomTracks `xml:"randomSongs" json:"randomSongs,omitempty"`
TracksByGenre *TracksByGenre `xml:"songsByGenre" json:"songsByGenre,omitempty"`
MusicFolders *MusicFolders `xml:"musicFolders" json:"musicFolders,omitempty"`
ScanStatus *ScanStatus `xml:"scanStatus" json:"scanStatus,omitempty"`
Licence *Licence `xml:"license" json:"license,omitempty"`
SearchResultTwo *SearchResultTwo `xml:"searchResult2" json:"searchResult2,omitempty"`
SearchResultThree *SearchResultThree `xml:"searchResult3" json:"searchResult3,omitempty"`
User *User `xml:"user" json:"user,omitempty"`
Playlists *Playlists `xml:"playlists" json:"playlists,omitempty"`
Playlist *Playlist `xml:"playlist" json:"playlist,omitempty"`
ArtistInfo *ArtistInfo `xml:"artistInfo" json:"artistInfo,omitempty"`
ArtistInfoTwo *ArtistInfo `xml:"artistInfo2" json:"artistInfo2,omitempty"`
Genres *Genres `xml:"genres" json:"genres,omitempty"`
PlayQueue *PlayQueue `xml:"playQueue" json:"playQueue,omitempty"`
JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus" json:"jukeboxStatus,omitempty"`
JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist" json:"jukeboxPlaylist,omitempty"`
Podcasts *Podcasts `xml:"podcasts" json:"podcasts,omitempty"`
NewestPodcasts *NewestPodcasts `xml:"newestPodcasts" json:"newestPodcasts,omitempty"`
Bookmarks *Bookmarks `xml:"bookmarks" json:"bookmarks,omitempty"`
Starred *Starred `xml:"starred" json:"starred,omitempty"`
StarredTwo *StarredTwo `xml:"starred2" json:"starred2,omitempty"`
TopSongs *TopSongs `xml:"topSongs" json:"topSongs,omitempty"`
SimilarSongs *SimilarSongs `xml:"similarSongs" json:"similarSongs,omitempty"`
SimilarSongsTwo *SimilarSongsTwo `xml:"similarSongs2" json:"similarSongs2,omitempty"`
InternetRadioStations *InternetRadioStations `xml:"internetRadioStations" json:"internetRadioStations,omitempty"`
}
func NewResponse() *Response {
@@ -68,7 +68,9 @@ func NewResponse() *Response {
}
// Error represents a typed error
// 0 a generic error
//
// 0 a generic error
//
// 10 required parameter is missing
// 20 incompatible subsonic rest protocol version. client must upgrade
// 30 incompatible subsonic rest protocol version. server must upgrade
@@ -118,6 +120,10 @@ type Album struct {
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
Year int `xml:"year,attr,omitempty" json:"year,omitempty"`
Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"`
// star / rating
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
UserRating int `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
AverageRating string `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"`
}
type RandomTracks struct {
@@ -151,6 +157,10 @@ type TrackChild struct {
DiscNumber int `xml:"discNumber,attr,omitempty" json:"discNumber,omitempty"`
Type string `xml:"type,attr,omitempty" json:"type,omitempty"`
Year int `xml:"year,attr,omitempty" json:"year,omitempty"`
// star / rating
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
UserRating int `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
AverageRating string `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"`
}
type Artists struct {
@@ -164,6 +174,10 @@ type Artist struct {
CoverID *specid.ID `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
AlbumCount int `xml:"albumCount,attr" json:"albumCount"`
Albums []*Album `xml:"album,omitempty" json:"album,omitempty"`
// star / rating
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
UserRating int `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
AverageRating string `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"`
}
type Indexes struct {
@@ -178,11 +192,13 @@ type Index struct {
}
type Directory struct {
ID *specid.ID `xml:"id,attr,omitempty" json:"id"`
ParentID *specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"`
Name string `xml:"name,attr,omitempty" json:"name"`
Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"`
Children []*TrackChild `xml:"child,omitempty" json:"child,omitempty"`
ID *specid.ID `xml:"id,attr,omitempty" json:"id"`
ParentID *specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"`
Name string `xml:"name,attr,omitempty" json:"name"`
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
UserRating int `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
AverageRating string `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"`
Children []*TrackChild `xml:"child,omitempty" json:"child,omitempty"`
}
type MusicFolders struct {
@@ -383,9 +399,15 @@ type InternetRadioStations struct {
}
type InternetRadioStation struct {
ID *specid.ID `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
StreamURL string `xml:"streamUrl,attr" json:"streamUrl"`
HomepageURL string `xml:"homepageUrl,attr" json:"homepageUrl"`
ID *specid.ID `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
StreamURL string `xml:"streamUrl,attr" json:"streamUrl"`
HomepageURL string `xml:"homepageUrl,attr" json:"homepageUrl"`
}
func formatRating(rating float64) string {
if rating == 0 {
return ""
}
return fmt.Sprintf("%.2f", rating)
}