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

@@ -44,6 +44,7 @@ func (db *DB) Migrate(ctx MigrationContext) error {
construct(ctx, "202204270903", migratePodcastDropUserID), construct(ctx, "202204270903", migratePodcastDropUserID),
construct(ctx, "202206011628", migrateInternetRadioStations), construct(ctx, "202206011628", migrateInternetRadioStations),
construct(ctx, "202206101425", migrateUser), construct(ctx, "202206101425", migrateUser),
construct(ctx, "202207251148", migrateStarRating),
} }
return gormigrate. return gormigrate.
@@ -371,3 +372,18 @@ func migrateUser(tx *gorm.DB, _ MigrationContext) error {
). ).
Error Error
} }
func migrateStarRating(tx *gorm.DB, _ MigrationContext) error {
return tx.AutoMigrate(
Album{},
AlbumStar{},
AlbumRating{},
Artist{},
ArtistStar{},
ArtistRating{},
Track{},
TrackStar{},
TrackRating{},
).
Error
}

View File

@@ -1,4 +1,5 @@
// Package db provides database helpers and models // Package db provides database helpers and models
//
//nolint:lll // struct tags get very long and can't be split //nolint:lll // struct tags get very long and can't be split
package db package db
@@ -48,6 +49,9 @@ type Artist struct {
Albums []*Album `gorm:"foreignkey:TagArtistID"` Albums []*Album `gorm:"foreignkey:TagArtistID"`
AlbumCount int `sql:"-"` AlbumCount int `sql:"-"`
Cover string `sql:"default: null"` Cover string `sql:"default: null"`
ArtistStar *ArtistStar
ArtistRating *ArtistRating
AverageRating float64 `sql:"default: null"`
} }
func (a *Artist) SID() *specid.ID { func (a *Artist) SID() *specid.ID {
@@ -98,6 +102,9 @@ type Track struct {
TagTrackNumber int `sql:"default: null"` TagTrackNumber int `sql:"default: null"`
TagDiscNumber int `sql:"default: null"` TagDiscNumber int `sql:"default: null"`
TagBrainzID string `sql:"default: null"` TagBrainzID string `sql:"default: null"`
TrackStar *TrackStar
TrackRating *TrackRating
AverageRating float64 `sql:"default: null"`
} }
func (t *Track) AudioLength() int { return t.Length } func (t *Track) AudioLength() int { return t.Length }
@@ -212,6 +219,9 @@ type Album struct {
Tracks []*Track Tracks []*Track
ChildCount int `sql:"-"` ChildCount int `sql:"-"`
Duration int `sql:"-"` Duration int `sql:"-"`
AlbumStar *AlbumStar
AlbumRating *AlbumRating
AverageRating float64 `sql:"default: null"`
} }
func (a *Album) SID() *specid.ID { func (a *Album) SID() *specid.ID {
@@ -304,6 +314,42 @@ type AlbumGenre struct {
GenreID int `gorm:"not null; unique_index:idx_album_id_genre_id" sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"` GenreID int `gorm:"not null; unique_index:idx_album_id_genre_id" sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"`
} }
type AlbumStar struct {
UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
AlbumID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
StarDate time.Time
}
type AlbumRating struct {
UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
AlbumID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Rating int `gorm:"not null; check:(rating >= 1 AND rating <= 5)"`
}
type ArtistStar struct {
UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
ArtistID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
StarDate time.Time
}
type ArtistRating struct {
UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
ArtistID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
Rating int `gorm:"not null; check:(rating >= 1 AND rating <= 5)"`
}
type TrackStar struct {
UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
TrackID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"`
StarDate time.Time
}
type TrackRating struct {
UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
TrackID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"`
Rating int `gorm:"not null; check:(rating >= 1 AND rating <= 5)"`
}
type PodcastAutoDownload string type PodcastAutoDownload string
const ( const (

View File

@@ -20,6 +20,7 @@ import (
func (c *Controller) ServeGetIndexes(r *http.Request) *spec.Response { func (c *Controller) ServeGetIndexes(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
rootQ := c.DB. rootQ := c.DB.
Select("id"). Select("id").
Model(&db.Album{}). Model(&db.Album{}).
@@ -31,6 +32,8 @@ func (c *Controller) ServeGetIndexes(r *http.Request) *spec.Response {
var folders []*db.Album var folders []*db.Album
c.DB. c.DB.
Select("*, count(sub.id) child_count"). Select("*, count(sub.id) child_count").
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID).
Joins("LEFT JOIN albums sub ON albums.id=sub.parent_id"). Joins("LEFT JOIN albums sub ON albums.id=sub.parent_id").
Where("albums.parent_id IN ?", rootQ.SubQuery()). Where("albums.parent_id IN ?", rootQ.SubQuery()).
Group("albums.id"). Group("albums.id").
@@ -48,8 +51,7 @@ func (c *Controller) ServeGetIndexes(r *http.Request) *spec.Response {
} }
resp = append(resp, indexMap[key]) resp = append(resp, indexMap[key])
} }
indexMap[key].Artists = append(indexMap[key].Artists, indexMap[key].Artists = append(indexMap[key].Artists, spec.NewArtistByFolder(folder))
spec.NewArtistByFolder(folder))
} }
sub := spec.NewResponse() sub := spec.NewResponse()
sub.Indexes = &spec.Indexes{ sub.Indexes = &spec.Indexes{
@@ -65,17 +67,23 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
if err != nil { if err != nil {
return spec.NewError(10, "please provide an `id` parameter") return spec.NewError(10, "please provide an `id` parameter")
} }
user := r.Context().Value(CtxUser).(*db.User)
childrenObj := []*spec.TrackChild{} childrenObj := []*spec.TrackChild{}
folder := &db.Album{} folder := &db.Album{}
c.DB.First(folder, id.Value) c.DB.
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID).
First(folder, id.Value)
// start looking for child childFolders in the current dir // start looking for child childFolders in the current dir
var childFolders []*db.Album var childFolders []*db.Album
c.DB. c.DB.
Where("parent_id=?", id.Value). Where("parent_id=?", id.Value).
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID).
Order("albums.right_path COLLATE NOCASE"). Order("albums.right_path COLLATE NOCASE").
Find(&childFolders) Find(&childFolders)
for _, c := range childFolders { for _, ch := range childFolders {
childrenObj = append(childrenObj, spec.NewTCAlbumByFolder(c)) childrenObj = append(childrenObj, spec.NewTCAlbumByFolder(ch))
} }
// start looking for child childTracks in the current dir // start looking for child childTracks in the current dir
var childTracks []*db.Track var childTracks []*db.Track
@@ -83,10 +91,12 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
Where("album_id=?", id.Value). Where("album_id=?", id.Value).
Preload("Album"). Preload("Album").
Preload("Album.TagArtist"). Preload("Album.TagArtist").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
Order("filename"). Order("filename").
Find(&childTracks) Find(&childTracks)
for _, c := range childTracks { for _, ch := range childTracks {
toAppend := spec.NewTCTrackByFolder(c, folder) toAppend := spec.NewTCTrackByFolder(ch, folder)
if v, _ := params.Get("c"); v == "Jamstash" { if v, _ := params.Get("c"); v == "Jamstash" {
// jamstash thinks it can't play flacs // jamstash thinks it can't play flacs
toAppend.ContentType = "audio/mpeg" toAppend.ContentType = "audio/mpeg"
@@ -105,6 +115,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
// getAlbumListTwo() function // getAlbumListTwo() function
func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response { func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
q := c.DB.DB q := c.DB.DB
switch v, _ := params.Get("type"); v { switch v, _ := params.Get("type"); v {
case "alphabeticalByArtist": case "alphabeticalByArtist":
@@ -127,7 +138,6 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
q = q.Joins("JOIN album_genres ON album_genres.album_id=albums.id") q = q.Joins("JOIN album_genres ON album_genres.album_id=albums.id")
q = q.Joins("JOIN genres ON genres.id=album_genres.genre_id AND genres.name=?", genre) q = q.Joins("JOIN genres ON genres.id=album_genres.genre_id AND genres.name=?", genre)
case "frequent": case "frequent":
user := r.Context().Value(CtxUser).(*db.User)
q = q.Joins(` q = q.Joins(`
JOIN plays JOIN plays
ON albums.id=plays.album_id AND plays.user_id=?`, ON albums.id=plays.album_id AND plays.user_id=?`,
@@ -162,6 +172,8 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
Offset(params.GetOrInt("offset", 0)). Offset(params.GetOrInt("offset", 0)).
Limit(params.GetOrInt("size", 10)). Limit(params.GetOrInt("size", 10)).
Preload("Parent"). Preload("Parent").
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID).
Find(&folders) Find(&folders)
sub := spec.NewResponse() sub := spec.NewResponse()
sub.Albums = &spec.Albums{ sub.Albums = &spec.Albums{
@@ -175,6 +187,7 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response { func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
query, err := params.Get("query") query, err := params.Get("query")
if err != nil { if err != nil {
return spec.NewError(10, "please provide a `query` parameter") return spec.NewError(10, "please provide a `query` parameter")
@@ -195,6 +208,8 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
var artists []*db.Album var artists []*db.Album
q := c.DB. q := c.DB.
Where(`parent_id IN ? AND (right_path LIKE ? OR right_path_u_dec LIKE ?)`, rootQ.SubQuery(), query, query). Where(`parent_id IN ? AND (right_path LIKE ? OR right_path_u_dec LIKE ?)`, rootQ.SubQuery(), query, query).
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID).
Offset(params.GetOrInt("artistOffset", 0)). Offset(params.GetOrInt("artistOffset", 0)).
Limit(params.GetOrInt("artistCount", 20)) Limit(params.GetOrInt("artistCount", 20))
if err := q.Find(&artists).Error; err != nil { if err := q.Find(&artists).Error; err != nil {
@@ -208,6 +223,8 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
var albums []*db.Album var albums []*db.Album
q = c.DB. q = c.DB.
Where(`tag_artist_id IS NOT NULL AND (right_path LIKE ? OR right_path_u_dec LIKE ?)`, query, query). Where(`tag_artist_id IS NOT NULL AND (right_path LIKE ? OR right_path_u_dec LIKE ?)`, query, query).
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID).
Offset(params.GetOrInt("albumOffset", 0)). Offset(params.GetOrInt("albumOffset", 0)).
Limit(params.GetOrInt("albumCount", 20)) Limit(params.GetOrInt("albumCount", 20))
if m := c.getMusicFolder(params); m != "" { if m := c.getMusicFolder(params); m != "" {
@@ -225,6 +242,8 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
q = c.DB. q = c.DB.
Preload("Album"). Preload("Album").
Where("filename LIKE ? OR filename_u_dec LIKE ?", query, query). Where("filename LIKE ? OR filename_u_dec LIKE ?", query, query).
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
Offset(params.GetOrInt("songOffset", 0)). Offset(params.GetOrInt("songOffset", 0)).
Limit(params.GetOrInt("songCount", 20)) Limit(params.GetOrInt("songCount", 20))
if m := c.getMusicFolder(params); m != "" { if m := c.getMusicFolder(params); m != "" {
@@ -249,11 +268,73 @@ func (c *Controller) ServeGetArtistInfo(r *http.Request) *spec.Response {
} }
func (c *Controller) ServeGetStarred(r *http.Request) *spec.Response { func (c *Controller) ServeGetStarred(r *http.Request) *spec.Response {
sub := spec.NewResponse() params := r.Context().Value(CtxParams).(params.Params)
sub.Starred = &spec.Starred{ user := r.Context().Value(CtxUser).(*db.User)
Artists: []*spec.Directory{},
Albums: []*spec.TrackChild{}, results := &spec.Starred{}
Tracks: []*spec.TrackChild{},
// "artists"
rootQ := c.DB.
Select("id").
Model(&db.Album{}).
Where("parent_id IS NULL")
if m := c.getMusicFolder(params); m != "" {
rootQ = rootQ.Where("root_dir=?", m)
} }
var artists []*db.Album
q := c.DB.
Where(`parent_id IN ?`, rootQ.SubQuery()).
Joins("JOIN album_stars ON albums.id=album_stars.album_id").
Where("album_stars.user_id=?", user.ID).
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID)
if err := q.Find(&artists).Error; err != nil {
return spec.NewError(0, "find artists: %v", err)
}
for _, a := range artists {
results.Artists = append(results.Artists, spec.NewDirectoryByFolder(a, nil))
}
// "albums"
var albums []*db.Album
q = c.DB.
Where("tag_artist_id IS NOT NULL").
Joins("JOIN album_stars ON albums.id=album_stars.album_id").
Where("album_stars.user_id=?", user.ID).
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID)
if m := c.getMusicFolder(params); m != "" {
q = q.Where("root_dir=?", m)
}
if err := q.Find(&albums).Error; err != nil {
return spec.NewError(0, "find albums: %v", err)
}
for _, a := range albums {
results.Albums = append(results.Albums, spec.NewTCAlbumByFolder(a))
}
// tracks
var tracks []*db.Track
q = c.DB.
Preload("Album").
Joins("JOIN track_stars ON tracks.id=track_stars.track_id").
Where("track_stars.user_id=?", user.ID).
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID)
if m := c.getMusicFolder(params); m != "" {
q = q.
Joins("JOIN albums ON albums.id=tracks.album_id").
Where("albums.root_dir=?", m)
}
if err := q.Find(&tracks).Error; err != nil {
return spec.NewError(0, "find tracks: %v", err)
}
for _, t := range tracks {
results.Tracks = append(results.Tracks, spec.NewTCTrackByFolder(t, t.Album))
}
sub := spec.NewResponse()
sub.Starred = results
return sub return sub
} }

View File

@@ -3,10 +3,12 @@ package ctrlsubsonic
import ( import (
"errors" "errors"
"fmt" "fmt"
"math"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
@@ -19,10 +21,13 @@ import (
func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response { func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
var artists []*db.Artist var artists []*db.Artist
q := c.DB. q := c.DB.
Select("*, count(sub.id) album_count"). Select("*, count(sub.id) album_count").
Joins("LEFT JOIN albums sub ON artists.id=sub.tag_artist_id"). Joins("LEFT JOIN albums sub ON artists.id=sub.tag_artist_id").
Preload("ArtistStar", "user_id=?", user.ID).
Preload("ArtistRating", "user_id=?", user.ID).
Group("artists.id"). Group("artists.id").
Order("artists.name COLLATE NOCASE") Order("artists.name COLLATE NOCASE")
if m := c.getMusicFolder(params); m != "" { if m := c.getMusicFolder(params); m != "" {
@@ -43,8 +48,7 @@ func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
} }
resp = append(resp, indexMap[key]) resp = append(resp, indexMap[key])
} }
indexMap[key].Artists = append(indexMap[key].Artists, indexMap[key].Artists = append(indexMap[key].Artists, spec.NewArtistByTags(artist))
spec.NewArtistByTags(artist))
} }
sub := spec.NewResponse() sub := spec.NewResponse()
sub.Artists = &spec.Artists{ sub.Artists = &spec.Artists{
@@ -55,6 +59,7 @@ func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response { func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
id, err := params.GetID("id") id, err := params.GetID("id")
if err != nil { if err != nil {
return spec.NewError(10, "please provide an `id` parameter") return spec.NewError(10, "please provide an `id` parameter")
@@ -65,9 +70,13 @@ func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response {
return db. return db.
Select("*, count(sub.id) child_count, sum(sub.length) duration"). Select("*, count(sub.id) child_count, sum(sub.length) duration").
Joins("LEFT JOIN tracks sub ON albums.id=sub.album_id"). Joins("LEFT JOIN tracks sub ON albums.id=sub.album_id").
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID).
Order("albums.right_path"). Order("albums.right_path").
Group("albums.id") Group("albums.id")
}). }).
Preload("ArtistStar", "user_id=?", user.ID).
Preload("ArtistRating", "user_id=?", user.ID).
First(artist, id.Value) First(artist, id.Value)
sub := spec.NewResponse() sub := spec.NewResponse()
sub.Artist = spec.NewArtistByTags(artist) sub.Artist = spec.NewArtistByTags(artist)
@@ -81,6 +90,7 @@ func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response {
func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response { func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
id, err := params.GetID("id") id, err := params.GetID("id")
if err != nil { if err != nil {
return spec.NewError(10, "please provide an `id` parameter") return spec.NewError(10, "please provide an `id` parameter")
@@ -92,8 +102,13 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
Preload("TagArtist"). Preload("TagArtist").
Preload("Genres"). Preload("Genres").
Preload("Tracks", func(db *gorm.DB) *gorm.DB { Preload("Tracks", func(db *gorm.DB) *gorm.DB {
return db.Order("tracks.tag_disc_number, tracks.tag_track_number") return db.
Order("tracks.tag_disc_number, tracks.tag_track_number").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID)
}). }).
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID).
First(album, id.Value). First(album, id.Value).
Error Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -113,6 +128,7 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
// getAlbumList() function // getAlbumList() function
func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response { func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
listType, err := params.Get("type") listType, err := params.Get("type")
if err != nil { if err != nil {
return spec.NewError(10, "please provide a `type` parameter") return spec.NewError(10, "please provide a `type` parameter")
@@ -138,8 +154,7 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
q = q.Joins("JOIN genres ON genres.id=album_genres.genre_id AND genres.name=?", genre) q = q.Joins("JOIN genres ON genres.id=album_genres.genre_id AND genres.name=?", genre)
case "frequent": case "frequent":
user := r.Context().Value(CtxUser).(*db.User) user := r.Context().Value(CtxUser).(*db.User)
q = q.Joins("JOIN plays ON albums.id=plays.album_id AND plays.user_id=?", q = q.Joins("JOIN plays ON albums.id=plays.album_id AND plays.user_id=?", user.ID)
user.ID)
q = q.Order("plays.count DESC") q = q.Order("plays.count DESC")
case "newest": case "newest":
q = q.Order("created_at DESC") q = q.Order("created_at DESC")
@@ -147,8 +162,7 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
q = q.Order(gorm.Expr("random()")) q = q.Order(gorm.Expr("random()"))
case "recent": case "recent":
user := r.Context().Value(CtxUser).(*db.User) user := r.Context().Value(CtxUser).(*db.User)
q = q.Joins("JOIN plays ON albums.id=plays.album_id AND plays.user_id=?", q = q.Joins("JOIN plays ON albums.id=plays.album_id AND plays.user_id=?", user.ID)
user.ID)
q = q.Order("plays.time DESC") q = q.Order("plays.time DESC")
default: default:
return spec.NewError(10, "unknown value `%s` for parameter 'type'", listType) return spec.NewError(10, "unknown value `%s` for parameter 'type'", listType)
@@ -167,6 +181,8 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
Offset(params.GetOrInt("offset", 0)). Offset(params.GetOrInt("offset", 0)).
Limit(params.GetOrInt("size", 10)). Limit(params.GetOrInt("size", 10)).
Preload("TagArtist"). Preload("TagArtist").
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID).
Find(&albums) Find(&albums)
sub := spec.NewResponse() sub := spec.NewResponse()
sub.AlbumsTwo = &spec.Albums{ sub.AlbumsTwo = &spec.Albums{
@@ -180,20 +196,24 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response { func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
query, err := params.Get("query") query, err := params.Get("query")
if err != nil { if err != nil {
return spec.NewError(10, "please provide a `query` parameter") return spec.NewError(10, "please provide a `query` parameter")
} }
query = fmt.Sprintf("%%%s%%", strings.Trim(query, `*"'`)) query = fmt.Sprintf("%%%s%%", strings.Trim(query, `*"'`))
results := &spec.SearchResultThree{} results := &spec.SearchResultThree{}
// search "artists" // search artists
var artists []*db.Artist var artists []*db.Artist
q := c.DB. q := c.DB.
Select("*, count(albums.id) album_count"). Select("*, count(albums.id) album_count").
Group("artists.id"). Group("artists.id").
Where("name LIKE ? OR name_u_dec LIKE ?", query, query). Where("name LIKE ? OR name_u_dec LIKE ?", query, query).
Joins("JOIN albums ON albums.tag_artist_id=artists.id"). Joins("JOIN albums ON albums.tag_artist_id=artists.id").
Preload("ArtistStar", "user_id=?", user.ID).
Preload("ArtistRating", "user_id=?", user.ID).
Offset(params.GetOrInt("artistOffset", 0)). Offset(params.GetOrInt("artistOffset", 0)).
Limit(params.GetOrInt("artistCount", 20)) Limit(params.GetOrInt("artistCount", 20))
if m := c.getMusicFolder(params); m != "" { if m := c.getMusicFolder(params); m != "" {
@@ -206,11 +226,13 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
results.Artists = append(results.Artists, spec.NewArtistByTags(a)) results.Artists = append(results.Artists, spec.NewArtistByTags(a))
} }
// search "albums" // search albums
var albums []*db.Album var albums []*db.Album
q = c.DB. q = c.DB.
Preload("TagArtist"). Preload("TagArtist").
Preload("Genres"). Preload("Genres").
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID).
Where("tag_title LIKE ? OR tag_title_u_dec LIKE ?", query, query). Where("tag_title LIKE ? OR tag_title_u_dec LIKE ?", query, query).
Offset(params.GetOrInt("albumOffset", 0)). Offset(params.GetOrInt("albumOffset", 0)).
Limit(params.GetOrInt("albumCount", 20)) Limit(params.GetOrInt("albumCount", 20))
@@ -230,6 +252,8 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
Preload("Album"). Preload("Album").
Preload("Album.TagArtist"). Preload("Album.TagArtist").
Preload("Genres"). Preload("Genres").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
Where("tag_title LIKE ? OR tag_title_u_dec LIKE ?", query, query). Where("tag_title LIKE ? OR tag_title_u_dec LIKE ?", query, query).
Offset(params.GetOrInt("songOffset", 0)). Offset(params.GetOrInt("songOffset", 0)).
Limit(params.GetOrInt("songCount", 20)) Limit(params.GetOrInt("songCount", 20))
@@ -356,6 +380,7 @@ func (c *Controller) ServeGetGenres(r *http.Request) *spec.Response {
func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response { func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
genre, err := params.Get("genre") genre, err := params.Get("genre")
if err != nil { if err != nil {
return spec.NewError(10, "please provide an `genre` parameter") return spec.NewError(10, "please provide an `genre` parameter")
@@ -367,6 +392,8 @@ func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response {
Joins("JOIN genres ON track_genres.genre_id=genres.id AND genres.name=?", genre). Joins("JOIN genres ON track_genres.genre_id=genres.id AND genres.name=?", genre).
Preload("Album"). Preload("Album").
Preload("Album.TagArtist"). Preload("Album.TagArtist").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
Offset(params.GetOrInt("offset", 0)). Offset(params.GetOrInt("offset", 0)).
Limit(params.GetOrInt("count", 10)) Limit(params.GetOrInt("count", 10))
if m := c.getMusicFolder(params); m != "" { if m := c.getMusicFolder(params); m != "" {
@@ -386,12 +413,70 @@ func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response {
} }
func (c *Controller) ServeGetStarredTwo(r *http.Request) *spec.Response { func (c *Controller) ServeGetStarredTwo(r *http.Request) *spec.Response {
sub := spec.NewResponse() user := r.Context().Value(CtxUser).(*db.User)
sub.StarredTwo = &spec.StarredTwo{ params := r.Context().Value(CtxParams).(params.Params)
Artists: []*spec.Artist{},
Albums: []*spec.Album{}, results := &spec.StarredTwo{}
Tracks: []*spec.TrackChild{},
// artists
var artists []*db.Artist
q := c.DB.
Select("*, count(albums.id) album_count").
Group("artists.id").
Joins("JOIN artist_stars ON artist_stars.artist_id=artists.id").
Where("artist_stars.user_id=?", user.ID).
Preload("ArtistStar", "user_id=?", user.ID).
Preload("ArtistRating", "user_id=?", user.ID)
if m := c.getMusicFolder(params); m != "" {
q = q.Where("albums.root_dir=?", m)
} }
if err := q.Find(&artists).Error; err != nil {
return spec.NewError(0, "find artists: %v", err)
}
for _, a := range artists {
results.Artists = append(results.Artists, spec.NewArtistByTags(a))
}
// albums
var albums []*db.Album
q = c.DB.
Joins("JOIN album_stars ON album_stars.album_id=albums.id").
Where("album_stars.user_id=?", user.ID).
Preload("TagArtist").
Preload("AlbumStar", "user_id=?", user.ID).
Preload("AlbumRating", "user_id=?", user.ID)
if m := c.getMusicFolder(params); m != "" {
q = q.Where("albums.root_dir=?", m)
}
if err := q.Find(&albums).Error; err != nil {
return spec.NewError(0, "find albums: %v", err)
}
for _, a := range albums {
results.Albums = append(results.Albums, spec.NewAlbumByTags(a, a.TagArtist))
}
// tracks
var tracks []*db.Track
q = c.DB.
Joins("JOIN track_stars ON tracks.id=track_stars.track_id").
Where("track_stars.user_id=?", user.ID).
Preload("Album").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID)
if m := c.getMusicFolder(params); m != "" {
q = q.
Joins("JOIN albums ON albums.id=tracks.album_id").
Where("albums.root_dir=?", m)
}
if err := q.Find(&tracks).Error; err != nil {
return spec.NewError(0, "find tracks: %v", err)
}
for _, t := range tracks {
results.Tracks = append(results.Tracks, spec.NewTrackByTags(t, t.Album))
}
sub := spec.NewResponse()
sub.StarredTwo = results
return sub return sub
} }
@@ -409,6 +494,7 @@ func (c *Controller) genArtistCoverURL(r *http.Request, artist *db.Artist, size
func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response { func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
count := params.GetOrInt("count", 10) count := params.GetOrInt("count", 10)
artistName, err := params.Get("artist") artistName, err := params.Get("artist")
if err != nil { if err != nil {
@@ -441,6 +527,8 @@ func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response {
Preload("Album"). Preload("Album").
Where("artist_id=? AND tracks.tag_title IN (?)", artist.ID, topTrackNames). Where("artist_id=? AND tracks.tag_title IN (?)", artist.ID, topTrackNames).
Limit(count). Limit(count).
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
Find(&tracks). Find(&tracks).
Error Error
if err != nil { if err != nil {
@@ -462,6 +550,7 @@ func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response {
func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response { func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
count := params.GetOrInt("count", 10) count := params.GetOrInt("count", 10)
id, err := params.GetID("id") id, err := params.GetID("id")
if err != nil || id.Type != specid.Track { if err != nil || id.Type != specid.Track {
@@ -500,6 +589,8 @@ func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response {
err = c.DB. err = c.DB.
Preload("Artist"). Preload("Artist").
Preload("Album"). Preload("Album").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
Select("tracks.*"). Select("tracks.*").
Where("tracks.tag_title IN (?)", similarTrackNames). Where("tracks.tag_title IN (?)", similarTrackNames).
Order(gorm.Expr("random()")). Order(gorm.Expr("random()")).
@@ -525,6 +616,7 @@ func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response {
func (c *Controller) ServeGetSimilarSongsTwo(r *http.Request) *spec.Response { func (c *Controller) ServeGetSimilarSongsTwo(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
count := params.GetOrInt("count", 10) count := params.GetOrInt("count", 10)
id, err := params.GetID("id") id, err := params.GetID("id")
if err != nil || id.Type != specid.Artist { if err != nil || id.Type != specid.Artist {
@@ -561,6 +653,8 @@ func (c *Controller) ServeGetSimilarSongsTwo(r *http.Request) *spec.Response {
var tracks []*db.Track var tracks []*db.Track
err = c.DB. err = c.DB.
Preload("Album"). Preload("Album").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
Joins("JOIN artists on tracks.artist_id=artists.id"). Joins("JOIN artists on tracks.artist_id=artists.id").
Where("artists.name IN (?)", artistNames). Where("artists.name IN (?)", artistNames).
Order(gorm.Expr("random()")). Order(gorm.Expr("random()")).
@@ -583,3 +677,200 @@ func (c *Controller) ServeGetSimilarSongsTwo(r *http.Request) *spec.Response {
} }
return sub return sub
} }
func starIDsOfType(p params.Params, typ specid.IDT) []int {
var ids []specid.ID
ids = append(ids, p.GetOrIDList("id", nil)...)
ids = append(ids, p.GetOrIDList("albumId", nil)...)
ids = append(ids, p.GetOrIDList("artistId", nil)...)
var out []int
for _, id := range ids {
if id.Type != typ {
continue
}
out = append(out, id.Value)
}
return out
}
func (c *Controller) ServeStar(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
stardate := time.Now()
for _, id := range starIDsOfType(params, specid.Album) {
var albumstar db.AlbumStar
_ = c.DB.Where("user_id=? AND album_id=?", user.ID, id).First(&albumstar).Error
albumstar.UserID = user.ID
albumstar.AlbumID = id
albumstar.StarDate = stardate
if err := c.DB.Save(&albumstar).Error; err != nil {
return spec.NewError(0, "save album star: %v", err)
}
}
for _, id := range starIDsOfType(params, specid.Artist) {
var artiststar db.ArtistStar
_ = c.DB.Where("user_id=? AND artist_id=?", user.ID, id).First(&artiststar).Error
artiststar.UserID = user.ID
artiststar.ArtistID = id
artiststar.StarDate = stardate
if err := c.DB.Save(&artiststar).Error; err != nil {
return spec.NewError(0, "save artist star: %v", err)
}
}
for _, id := range starIDsOfType(params, specid.Track) {
var trackstar db.TrackStar
_ = c.DB.Where("user_id=? AND track_id=?", user.ID, id).First(&trackstar).Error
trackstar.UserID = user.ID
trackstar.TrackID = id
trackstar.StarDate = stardate
if err := c.DB.Save(&trackstar).Error; err != nil {
return spec.NewError(0, "save track star: %v", err)
}
}
return spec.NewResponse()
}
func (c *Controller) ServeUnstar(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
for _, id := range starIDsOfType(params, specid.Album) {
if err := c.DB.Where("user_id=? AND album_id=?", user.ID, id).Delete(db.AlbumStar{}).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return spec.NewError(0, "delete album star: %v", err)
}
}
for _, id := range starIDsOfType(params, specid.Artist) {
if err := c.DB.Where("user_id=? AND artist_id=?", user.ID, id).Delete(db.ArtistStar{}).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return spec.NewError(0, "delete artist star: %v", err)
}
}
for _, id := range starIDsOfType(params, specid.Track) {
if err := c.DB.Where("user_id=? AND track_id=?", user.ID, id).Delete(db.TrackStar{}).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return spec.NewError(0, "delete track star: %v", err)
}
}
return spec.NewResponse()
}
//nolint:gocyclo // we could probably simplify this with some interfaces or generics. but it's fine for now
func (c *Controller) ServeSetRating(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params)
id, err := params.GetID("id")
if err != nil {
return spec.NewError(10, "please provide a valid id")
}
rating, err := params.GetInt("rating")
if err != nil || rating < 0 || rating > 5 {
return spec.NewError(10, "please provide a valid rating")
}
user := r.Context().Value(CtxUser).(*db.User)
switch id.Type {
case specid.Album:
var album db.Album
err := c.DB.Where("id=?", id.Value).First(&album).Error
if err != nil {
return spec.NewError(0, "fetch album: %v", err)
}
var albumRating db.AlbumRating
if err := c.DB.Where("user_id=? AND album_id=?", user.ID, id.Value).First(&albumRating).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return spec.NewError(0, "fetch album rating: %v", err)
}
switch {
case rating == 0 && albumRating.AlbumID == album.ID:
if err := c.DB.Delete(&albumRating).Error; err != nil {
return spec.NewError(0, "delete album rating: %v", err)
}
case rating > 0:
albumRating.UserID = user.ID
albumRating.AlbumID = id.Value
albumRating.Rating = rating
if err := c.DB.Save(&albumRating).Error; err != nil {
return spec.NewError(0, "save album rating: %v", err)
}
}
var averageRating float64
if err := c.DB.Model(db.AlbumRating{}).Select("coalesce(avg(rating), 0)").Where("album_id=?", id.Value).Row().Scan(&averageRating); err != nil {
return spec.NewError(0, "find average album rating: %v", err)
}
album.AverageRating = math.Trunc(averageRating*100) / 100
if err := c.DB.Save(&album).Error; err != nil {
return spec.NewError(0, "save album: %v", err)
}
case specid.Artist:
var artist db.Artist
err := c.DB.Where("id=?", id.Value).First(&artist).Error
if err != nil {
return spec.NewError(0, "fetch artist: %v", err)
}
var artistRating db.ArtistRating
if err := c.DB.Where("user_id=? AND artist_id=?", user.ID, id.Value).First(&artistRating).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return spec.NewError(0, "fetch artist rating: %v", err)
}
switch {
case rating == 0 && artistRating.ArtistID == artist.ID:
if err := c.DB.Delete(&artistRating).Error; err != nil {
return spec.NewError(0, "delete artist rating: %v", err)
}
case rating > 0:
artistRating.UserID = user.ID
artistRating.ArtistID = id.Value
artistRating.Rating = rating
if err := c.DB.Save(&artistRating).Error; err != nil {
return spec.NewError(0, "save artist rating: %v", err)
}
}
var averageRating float64
if err := c.DB.Model(db.ArtistRating{}).Select("coalesce(avg(rating), 0)").Where("artist_id=?", id.Value).Row().Scan(&averageRating); err != nil {
return spec.NewError(0, "find average artist rating: %v", err)
}
artist.AverageRating = math.Trunc(averageRating*100) / 100
if err := c.DB.Save(&artist).Error; err != nil {
return spec.NewError(0, "save artist: %v", err)
}
case specid.Track:
var track db.Track
err := c.DB.Where("id=?", id.Value).First(&track).Error
if err != nil {
return spec.NewError(0, "fetch track: %v", err)
}
var trackRating db.TrackRating
if err := c.DB.Where("user_id=? AND track_id=?", user.ID, id.Value).First(&trackRating).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return spec.NewError(0, "fetch track rating: %v", err)
}
switch {
case rating == 0 && trackRating.TrackID == track.ID:
if err := c.DB.Delete(&trackRating).Error; err != nil {
return spec.NewError(0, "delete track rating: %v", err)
}
case rating > 0:
trackRating.UserID = user.ID
trackRating.TrackID = id.Value
trackRating.Rating = rating
if err := c.DB.Save(&trackRating).Error; err != nil {
return spec.NewError(0, "save track rating: %v", err)
}
}
var averageRating float64
if err := c.DB.Model(db.TrackRating{}).Select("coalesce(avg(rating), 0)").Where("track_id=?", id.Value).Row().Scan(&averageRating); err != nil {
return spec.NewError(0, "find average track rating: %v", err)
}
track.AverageRating = math.Trunc(averageRating*100) / 100
if err := c.DB.Save(&track).Error; err != nil {
return spec.NewError(0, "save track: %v", err)
}
default:
return spec.NewError(0, "non-album non-artist non-track id cannot be rated")
}
return spec.NewResponse()
}

View File

@@ -150,6 +150,8 @@ func (c *Controller) ServeGetPlayQueue(r *http.Request) *spec.Response {
c.DB. c.DB.
Where("id=?", id). Where("id=?", id).
Preload("Album"). Preload("Album").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
Find(&track) Find(&track)
sub.PlayQueue.List[i] = spec.NewTCTrackByFolder(&track, track.Album) sub.PlayQueue.List[i] = spec.NewTCTrackByFolder(&track, track.Album)
} }
@@ -180,12 +182,13 @@ func (c *Controller) ServeSavePlayQueue(r *http.Request) *spec.Response {
queue.Position = params.GetOrInt("position", 0) queue.Position = params.GetOrInt("position", 0)
queue.ChangedBy = params.GetOr("c", "") // must exist, middleware checks queue.ChangedBy = params.GetOr("c", "") // must exist, middleware checks
queue.SetItems(trackIDs) queue.SetItems(trackIDs)
c.DB.Save(queue) c.DB.Save(&queue)
return spec.NewResponse() return spec.NewResponse()
} }
func (c *Controller) ServeGetSong(r *http.Request) *spec.Response { func (c *Controller) ServeGetSong(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
id, err := params.GetID("id") id, err := params.GetID("id")
if err != nil { if err != nil {
return spec.NewError(10, "provide an `id` parameter") return spec.NewError(10, "provide an `id` parameter")
@@ -195,6 +198,8 @@ func (c *Controller) ServeGetSong(r *http.Request) *spec.Response {
Where("id=?", id.Value). Where("id=?", id.Value).
Preload("Album"). Preload("Album").
Preload("Album.TagArtist"). Preload("Album.TagArtist").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
First(&track). First(&track).
Error Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -207,11 +212,14 @@ func (c *Controller) ServeGetSong(r *http.Request) *spec.Response {
func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response { func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
var tracks []*db.Track var tracks []*db.Track
q := c.DB.DB. q := c.DB.DB.
Limit(params.GetOrInt("size", 10)). Limit(params.GetOrInt("size", 10)).
Preload("Album"). Preload("Album").
Preload("Album.TagArtist"). Preload("Album.TagArtist").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
Joins("JOIN albums ON tracks.album_id=albums.id"). Joins("JOIN albums ON tracks.album_id=albums.id").
Order(gorm.Expr("random()")) Order(gorm.Expr("random()"))
if year, err := params.GetInt("fromYear"); err == nil { if year, err := params.GetInt("fromYear"); err == nil {
@@ -241,6 +249,7 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
func (c *Controller) ServeJukebox(r *http.Request) *spec.Response { func (c *Controller) ServeJukebox(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
getTracks := func() []*db.Track { getTracks := func() []*db.Track {
var tracks []*db.Track var tracks []*db.Track
ids, err := params.GetIDList("id") ids, err := params.GetIDList("id")
@@ -249,7 +258,11 @@ func (c *Controller) ServeJukebox(r *http.Request) *spec.Response {
} }
for _, id := range ids { for _, id := range ids {
track := &db.Track{} track := &db.Track{}
c.DB.Preload("Album").First(track, id.Value) c.DB.
Preload("Album").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
First(track, id.Value)
if track.ID != 0 { if track.ID != 0 {
tracks = append(tracks, track) tracks = append(tracks, track)
} }

View File

@@ -8,9 +8,9 @@ import (
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/server/ctrlsubsonic/params" "go.senan.xyz/gonic/server/ctrlsubsonic/params"
"go.senan.xyz/gonic/server/ctrlsubsonic/spec" "go.senan.xyz/gonic/server/ctrlsubsonic/spec"
"go.senan.xyz/gonic/db"
) )
func playlistRender(c *Controller, playlist *db.Playlist) *spec.Playlist { func playlistRender(c *Controller, playlist *db.Playlist) *spec.Playlist {
@@ -35,6 +35,8 @@ func playlistRender(c *Controller, playlist *db.Playlist) *spec.Playlist {
Where("id=?", id). Where("id=?", id).
Preload("Album"). Preload("Album").
Preload("Album.TagArtist"). Preload("Album.TagArtist").
Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID).
Find(&track). Find(&track).
Error Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {

View File

@@ -17,6 +17,13 @@ func NewAlbumByFolder(f *db.Album) *Album {
TrackCount: f.ChildCount, TrackCount: f.ChildCount,
Duration: f.Duration, Duration: f.Duration,
Created: f.CreatedAt, 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 != "" { if f.Cover != "" {
a.CoverID = f.SID() a.CoverID = f.SID()
@@ -31,6 +38,13 @@ func NewTCAlbumByFolder(f *db.Album) *TrackChild {
Title: f.RightPath, Title: f.RightPath,
ParentID: f.ParentSID(), ParentID: f.ParentSID(),
CreatedAt: f.CreatedAt, 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 != "" { if f.Cover != "" {
trCh.CoverID = f.SID() trCh.CoverID = f.SID()
@@ -61,6 +75,7 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild {
IsDir: false, IsDir: false,
Type: "music", Type: "music",
CreatedAt: t.CreatedAt, CreatedAt: t.CreatedAt,
AverageRating: formatRating(t.AverageRating),
} }
if trCh.Title == "" { if trCh.Title == "" {
trCh.Title = t.Filename trCh.Title = t.Filename
@@ -71,6 +86,12 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild {
if t.Album != nil { if t.Album != nil {
trCh.Album = t.Album.RightPath 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 return trCh
} }
@@ -83,6 +104,13 @@ func NewArtistByFolder(f *db.Album) *Artist {
ID: f.SID(), ID: f.SID(),
Name: f.RightPath, Name: f.RightPath,
AlbumCount: f.ChildCount, 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 != "" { if f.Cover != "" {
a.CoverID = f.SID() a.CoverID = f.SID()
@@ -91,10 +119,18 @@ func NewArtistByFolder(f *db.Album) *Artist {
} }
func NewDirectoryByFolder(f *db.Album, children []*TrackChild) *Directory { func NewDirectoryByFolder(f *db.Album, children []*TrackChild) *Directory {
return &Directory{ d := &Directory{
ID: f.SID(), ID: f.SID(),
Name: f.RightPath, Name: f.RightPath,
Children: children, Children: children,
ParentID: f.ParentSID(), 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

@@ -16,10 +16,17 @@ func NewAlbumByTags(a *db.Album, artist *db.Artist) *Album {
TrackCount: a.ChildCount, TrackCount: a.ChildCount,
Genre: strings.Join(a.GenreStrings(), ", "), Genre: strings.Join(a.GenreStrings(), ", "),
Duration: a.Duration, Duration: a.Duration,
AverageRating: formatRating(a.AverageRating),
} }
if a.Cover != "" { if a.Cover != "" {
ret.CoverID = a.SID() 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 { if artist != nil {
ret.Artist = artist.Name ret.Artist = artist.Name
ret.ArtistID = artist.SID() ret.ArtistID = artist.SID()
@@ -51,10 +58,17 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild {
Bitrate: t.Bitrate, Bitrate: t.Bitrate,
Type: "music", Type: "music",
Year: album.TagYear, Year: album.TagYear,
AverageRating: formatRating(t.AverageRating),
} }
if album.Cover != "" { if album.Cover != "" {
ret.CoverID = album.SID() 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 { if album.TagArtist != nil {
ret.ArtistID = album.TagArtist.SID() ret.ArtistID = album.TagArtist.SID()
} }
@@ -76,10 +90,17 @@ func NewArtistByTags(a *db.Artist) *Artist {
ID: a.SID(), ID: a.SID(),
Name: a.Name, Name: a.Name,
AlbumCount: a.AlbumCount, AlbumCount: a.AlbumCount,
AverageRating: formatRating(a.AverageRating),
} }
if a.Cover != "" { if a.Cover != "" {
r.CoverID = a.SID() r.CoverID = a.SID()
} }
if a.ArtistStar != nil {
r.Starred = &a.ArtistStar.StarDate
}
if a.ArtistRating != nil {
r.UserRating = a.ArtistRating.Rating
}
return r return r
} }

View File

@@ -68,7 +68,9 @@ func NewResponse() *Response {
} }
// Error represents a typed error // Error represents a typed error
//
// 0 a generic error // 0 a generic error
//
// 10 required parameter is missing // 10 required parameter is missing
// 20 incompatible subsonic rest protocol version. client must upgrade // 20 incompatible subsonic rest protocol version. client must upgrade
// 30 incompatible subsonic rest protocol version. server 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"` Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
Year int `xml:"year,attr,omitempty" json:"year,omitempty"` Year int `xml:"year,attr,omitempty" json:"year,omitempty"`
Tracks []*TrackChild `xml:"song,omitempty" json:"song,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 { type RandomTracks struct {
@@ -151,6 +157,10 @@ type TrackChild struct {
DiscNumber int `xml:"discNumber,attr,omitempty" json:"discNumber,omitempty"` DiscNumber int `xml:"discNumber,attr,omitempty" json:"discNumber,omitempty"`
Type string `xml:"type,attr,omitempty" json:"type,omitempty"` Type string `xml:"type,attr,omitempty" json:"type,omitempty"`
Year int `xml:"year,attr,omitempty" json:"year,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 { type Artists struct {
@@ -164,6 +174,10 @@ type Artist struct {
CoverID *specid.ID `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"` CoverID *specid.ID `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
AlbumCount int `xml:"albumCount,attr" json:"albumCount"` AlbumCount int `xml:"albumCount,attr" json:"albumCount"`
Albums []*Album `xml:"album,omitempty" json:"album,omitempty"` 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 { type Indexes struct {
@@ -181,7 +195,9 @@ type Directory struct {
ID *specid.ID `xml:"id,attr,omitempty" json:"id"` ID *specid.ID `xml:"id,attr,omitempty" json:"id"`
ParentID *specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"` ParentID *specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"`
Name string `xml:"name,attr,omitempty" json:"name"` Name string `xml:"name,attr,omitempty" json:"name"`
Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"` 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"` Children []*TrackChild `xml:"child,omitempty" json:"child,omitempty"`
} }
@@ -389,3 +405,9 @@ type InternetRadioStation struct {
HomepageURL string `xml:"homepageUrl,attr" json:"homepageUrl"` HomepageURL string `xml:"homepageUrl,attr" json:"homepageUrl"`
} }
func formatRating(rating float64) string {
if rating == 0 {
return ""
}
return fmt.Sprintf("%.2f", rating)
}

View File

@@ -264,6 +264,11 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) {
r.Handle("/getArtistInfo{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetArtistInfo)) r.Handle("/getArtistInfo{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetArtistInfo))
r.Handle("/getStarred{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetStarred)) r.Handle("/getStarred{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetStarred))
// star / rating
r.Handle("/star{_:(?:\\.view)?}", ctrl.H(ctrl.ServeStar))
r.Handle("/unstar{_:(?:\\.view)?}", ctrl.H(ctrl.ServeUnstar))
r.Handle("/setRating{_:(?:\\.view)?}", ctrl.H(ctrl.ServeSetRating))
// podcasts // podcasts
r.Handle("/getPodcasts{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPodcasts)) r.Handle("/getPodcasts{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPodcasts))
r.Handle("/getNewestPodcasts{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetNewestPodcasts)) r.Handle("/getNewestPodcasts{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetNewestPodcasts))