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:
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
58
db/model.go
58
db/model.go
@@ -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
|
||||||
|
|
||||||
@@ -42,12 +43,15 @@ func joinInt(in []int, sep string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Artist struct {
|
type Artist struct {
|
||||||
ID int `gorm:"primary_key"`
|
ID int `gorm:"primary_key"`
|
||||||
Name string `gorm:"not null; unique_index"`
|
Name string `gorm:"not null; unique_index"`
|
||||||
NameUDec string `sql:"default: null"`
|
NameUDec string `sql:"default: null"`
|
||||||
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 (
|
||||||
|
|||||||
@@ -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":
|
||||||
@@ -120,14 +131,13 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
|
|||||||
if fromYear > toYear {
|
if fromYear > toYear {
|
||||||
toYear, fromYear = fromYear, toYear
|
toYear, fromYear = fromYear, toYear
|
||||||
}
|
}
|
||||||
q = q.Where("tag_year BETWEEN ? AND ?", fromYear, toYear)
|
q = q.Where("tag_year BETWEEN ? AND ?", fromYear, toYear)
|
||||||
q = q.Order("tag_year")
|
q = q.Order("tag_year")
|
||||||
case "byGenre":
|
case "byGenre":
|
||||||
genre, _ := params.Get("genre")
|
genre, _ := params.Get("genre")
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -130,7 +146,7 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
|
|||||||
if fromYear > toYear {
|
if fromYear > toYear {
|
||||||
toYear, fromYear = fromYear, toYear
|
toYear, fromYear = fromYear, toYear
|
||||||
}
|
}
|
||||||
q = q.Where("tag_year BETWEEN ? AND ?", fromYear, toYear)
|
q = q.Where("tag_year BETWEEN ? AND ?", fromYear, toYear)
|
||||||
q = q.Order("tag_year")
|
q = q.Order("tag_year")
|
||||||
case "byGenre":
|
case "byGenre":
|
||||||
genre, _ := params.Get("genre")
|
genre, _ := params.Get("genre")
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -9,14 +9,21 @@ import (
|
|||||||
|
|
||||||
func NewAlbumByFolder(f *db.Album) *Album {
|
func NewAlbumByFolder(f *db.Album) *Album {
|
||||||
a := &Album{
|
a := &Album{
|
||||||
Artist: f.Parent.RightPath,
|
Artist: f.Parent.RightPath,
|
||||||
ID: f.SID(),
|
ID: f.SID(),
|
||||||
IsDir: true,
|
IsDir: true,
|
||||||
ParentID: f.ParentSID(),
|
ParentID: f.ParentSID(),
|
||||||
Title: f.RightPath,
|
Title: f.RightPath,
|
||||||
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()
|
||||||
@@ -26,11 +33,18 @@ func NewAlbumByFolder(f *db.Album) *Album {
|
|||||||
|
|
||||||
func NewTCAlbumByFolder(f *db.Album) *TrackChild {
|
func NewTCAlbumByFolder(f *db.Album) *TrackChild {
|
||||||
trCh := &TrackChild{
|
trCh := &TrackChild{
|
||||||
ID: f.SID(),
|
ID: f.SID(),
|
||||||
IsDir: true,
|
IsDir: true,
|
||||||
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()
|
||||||
@@ -53,14 +67,15 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild {
|
|||||||
parent.RightPath,
|
parent.RightPath,
|
||||||
t.Filename,
|
t.Filename,
|
||||||
),
|
),
|
||||||
ParentID: parent.SID(),
|
ParentID: parent.SID(),
|
||||||
Duration: t.Length,
|
Duration: t.Length,
|
||||||
Genre: strings.Join(t.GenreStrings(), ", "),
|
Genre: strings.Join(t.GenreStrings(), ", "),
|
||||||
Year: parent.TagYear,
|
Year: parent.TagYear,
|
||||||
Bitrate: t.Bitrate,
|
Bitrate: t.Bitrate,
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +101,16 @@ func NewArtistByFolder(f *db.Album) *Artist {
|
|||||||
// from an "album" where
|
// from an "album" where
|
||||||
// maybe TODO: rename the Album model to Folder
|
// maybe TODO: rename the Album model to Folder
|
||||||
a := &Artist{
|
a := &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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,24 @@ import (
|
|||||||
|
|
||||||
func NewAlbumByTags(a *db.Album, artist *db.Artist) *Album {
|
func NewAlbumByTags(a *db.Album, artist *db.Artist) *Album {
|
||||||
ret := &Album{
|
ret := &Album{
|
||||||
Created: a.CreatedAt,
|
Created: a.CreatedAt,
|
||||||
ID: a.SID(),
|
ID: a.SID(),
|
||||||
Name: a.TagTitle,
|
Name: a.TagTitle,
|
||||||
Year: a.TagYear,
|
Year: a.TagYear,
|
||||||
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()
|
||||||
@@ -44,17 +51,24 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild {
|
|||||||
album.RightPath,
|
album.RightPath,
|
||||||
t.Filename,
|
t.Filename,
|
||||||
),
|
),
|
||||||
Album: album.TagTitle,
|
Album: album.TagTitle,
|
||||||
AlbumID: album.SID(),
|
AlbumID: album.SID(),
|
||||||
Genre: strings.Join(t.GenreStrings(), ", "),
|
Genre: strings.Join(t.GenreStrings(), ", "),
|
||||||
Duration: t.Length,
|
Duration: t.Length,
|
||||||
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()
|
||||||
}
|
}
|
||||||
@@ -73,13 +87,20 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild {
|
|||||||
|
|
||||||
func NewArtistByTags(a *db.Artist) *Artist {
|
func NewArtistByTags(a *db.Artist) *Artist {
|
||||||
r := &Artist{
|
r := &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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,48 +14,48 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SubsonicResponse struct {
|
type SubsonicResponse struct {
|
||||||
Response Response `xml:"subsonic-response" json:"subsonic-response"`
|
Response Response `xml:"subsonic-response" json:"subsonic-response"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Status string `xml:"status,attr" json:"status"`
|
Status string `xml:"status,attr" json:"status"`
|
||||||
Version string `xml:"version,attr" json:"version"`
|
Version string `xml:"version,attr" json:"version"`
|
||||||
XMLNS string `xml:"xmlns,attr" json:"-"`
|
XMLNS string `xml:"xmlns,attr" json:"-"`
|
||||||
Type string `xml:"type,attr" json:"type"`
|
Type string `xml:"type,attr" json:"type"`
|
||||||
Error *Error `xml:"error" json:"error,omitempty"`
|
Error *Error `xml:"error" json:"error,omitempty"`
|
||||||
Albums *Albums `xml:"albumList" json:"albumList,omitempty"`
|
Albums *Albums `xml:"albumList" json:"albumList,omitempty"`
|
||||||
AlbumsTwo *Albums `xml:"albumList2" json:"albumList2,omitempty"`
|
AlbumsTwo *Albums `xml:"albumList2" json:"albumList2,omitempty"`
|
||||||
Album *Album `xml:"album" json:"album,omitempty"`
|
Album *Album `xml:"album" json:"album,omitempty"`
|
||||||
Track *TrackChild `xml:"song" json:"song,omitempty"`
|
Track *TrackChild `xml:"song" json:"song,omitempty"`
|
||||||
Indexes *Indexes `xml:"indexes" json:"indexes,omitempty"`
|
Indexes *Indexes `xml:"indexes" json:"indexes,omitempty"`
|
||||||
Artists *Artists `xml:"artists" json:"artists,omitempty"`
|
Artists *Artists `xml:"artists" json:"artists,omitempty"`
|
||||||
Artist *Artist `xml:"artist" json:"artist,omitempty"`
|
Artist *Artist `xml:"artist" json:"artist,omitempty"`
|
||||||
Directory *Directory `xml:"directory" json:"directory,omitempty"`
|
Directory *Directory `xml:"directory" json:"directory,omitempty"`
|
||||||
RandomTracks *RandomTracks `xml:"randomSongs" json:"randomSongs,omitempty"`
|
RandomTracks *RandomTracks `xml:"randomSongs" json:"randomSongs,omitempty"`
|
||||||
TracksByGenre *TracksByGenre `xml:"songsByGenre" json:"songsByGenre,omitempty"`
|
TracksByGenre *TracksByGenre `xml:"songsByGenre" json:"songsByGenre,omitempty"`
|
||||||
MusicFolders *MusicFolders `xml:"musicFolders" json:"musicFolders,omitempty"`
|
MusicFolders *MusicFolders `xml:"musicFolders" json:"musicFolders,omitempty"`
|
||||||
ScanStatus *ScanStatus `xml:"scanStatus" json:"scanStatus,omitempty"`
|
ScanStatus *ScanStatus `xml:"scanStatus" json:"scanStatus,omitempty"`
|
||||||
Licence *Licence `xml:"license" json:"license,omitempty"`
|
Licence *Licence `xml:"license" json:"license,omitempty"`
|
||||||
SearchResultTwo *SearchResultTwo `xml:"searchResult2" json:"searchResult2,omitempty"`
|
SearchResultTwo *SearchResultTwo `xml:"searchResult2" json:"searchResult2,omitempty"`
|
||||||
SearchResultThree *SearchResultThree `xml:"searchResult3" json:"searchResult3,omitempty"`
|
SearchResultThree *SearchResultThree `xml:"searchResult3" json:"searchResult3,omitempty"`
|
||||||
User *User `xml:"user" json:"user,omitempty"`
|
User *User `xml:"user" json:"user,omitempty"`
|
||||||
Playlists *Playlists `xml:"playlists" json:"playlists,omitempty"`
|
Playlists *Playlists `xml:"playlists" json:"playlists,omitempty"`
|
||||||
Playlist *Playlist `xml:"playlist" json:"playlist,omitempty"`
|
Playlist *Playlist `xml:"playlist" json:"playlist,omitempty"`
|
||||||
ArtistInfo *ArtistInfo `xml:"artistInfo" json:"artistInfo,omitempty"`
|
ArtistInfo *ArtistInfo `xml:"artistInfo" json:"artistInfo,omitempty"`
|
||||||
ArtistInfoTwo *ArtistInfo `xml:"artistInfo2" json:"artistInfo2,omitempty"`
|
ArtistInfoTwo *ArtistInfo `xml:"artistInfo2" json:"artistInfo2,omitempty"`
|
||||||
Genres *Genres `xml:"genres" json:"genres,omitempty"`
|
Genres *Genres `xml:"genres" json:"genres,omitempty"`
|
||||||
PlayQueue *PlayQueue `xml:"playQueue" json:"playQueue,omitempty"`
|
PlayQueue *PlayQueue `xml:"playQueue" json:"playQueue,omitempty"`
|
||||||
JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus" json:"jukeboxStatus,omitempty"`
|
JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus" json:"jukeboxStatus,omitempty"`
|
||||||
JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist" json:"jukeboxPlaylist,omitempty"`
|
JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist" json:"jukeboxPlaylist,omitempty"`
|
||||||
Podcasts *Podcasts `xml:"podcasts" json:"podcasts,omitempty"`
|
Podcasts *Podcasts `xml:"podcasts" json:"podcasts,omitempty"`
|
||||||
NewestPodcasts *NewestPodcasts `xml:"newestPodcasts" json:"newestPodcasts,omitempty"`
|
NewestPodcasts *NewestPodcasts `xml:"newestPodcasts" json:"newestPodcasts,omitempty"`
|
||||||
Bookmarks *Bookmarks `xml:"bookmarks" json:"bookmarks,omitempty"`
|
Bookmarks *Bookmarks `xml:"bookmarks" json:"bookmarks,omitempty"`
|
||||||
Starred *Starred `xml:"starred" json:"starred,omitempty"`
|
Starred *Starred `xml:"starred" json:"starred,omitempty"`
|
||||||
StarredTwo *StarredTwo `xml:"starred2" json:"starred2,omitempty"`
|
StarredTwo *StarredTwo `xml:"starred2" json:"starred2,omitempty"`
|
||||||
TopSongs *TopSongs `xml:"topSongs" json:"topSongs,omitempty"`
|
TopSongs *TopSongs `xml:"topSongs" json:"topSongs,omitempty"`
|
||||||
SimilarSongs *SimilarSongs `xml:"similarSongs" json:"similarSongs,omitempty"`
|
SimilarSongs *SimilarSongs `xml:"similarSongs" json:"similarSongs,omitempty"`
|
||||||
SimilarSongsTwo *SimilarSongsTwo `xml:"similarSongs2" json:"similarSongs2,omitempty"`
|
SimilarSongsTwo *SimilarSongsTwo `xml:"similarSongs2" json:"similarSongs2,omitempty"`
|
||||||
InternetRadioStations *InternetRadioStations `xml:"internetRadioStations" json:"internetRadioStations,omitempty"`
|
InternetRadioStations *InternetRadioStations `xml:"internetRadioStations" json:"internetRadioStations,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResponse() *Response {
|
func NewResponse() *Response {
|
||||||
@@ -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 {
|
||||||
@@ -178,11 +192,13 @@ type Index struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Directory struct {
|
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"`
|
||||||
Children []*TrackChild `xml:"child,omitempty" json:"child,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 {
|
type MusicFolders struct {
|
||||||
@@ -383,9 +399,15 @@ type InternetRadioStations struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InternetRadioStation struct {
|
type InternetRadioStation struct {
|
||||||
ID *specid.ID `xml:"id,attr" json:"id"`
|
ID *specid.ID `xml:"id,attr" json:"id"`
|
||||||
Name string `xml:"name,attr" json:"name"`
|
Name string `xml:"name,attr" json:"name"`
|
||||||
StreamURL string `xml:"streamUrl,attr" json:"streamUrl"`
|
StreamURL string `xml:"streamUrl,attr" json:"streamUrl"`
|
||||||
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user