diff --git a/api/album_lists.go b/api/album_lists.go index dd56d0b0..0f8d1975 100644 --- a/api/album_lists.go +++ b/api/album_lists.go @@ -81,7 +81,7 @@ func (c *AlbumListController) GetAlbumList2(w http.ResponseWriter, r *http.Reque } func (c *AlbumListController) GetStarred(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { - albums, mediaFiles, err := c.listGen.GetAllStarred() + artists, albums, mediaFiles, err := c.listGen.GetAllStarred() if err != nil { log.Error(r, "Error retrieving starred media", "error", err) return nil, NewError(responses.ErrorGeneric, "Internal Error") @@ -89,13 +89,14 @@ func (c *AlbumListController) GetStarred(w http.ResponseWriter, r *http.Request) response := NewResponse() response.Starred = &responses.Starred{} + response.Starred.Artist = ToArtists(artists) response.Starred.Album = ToChildren(albums) response.Starred.Song = ToChildren(mediaFiles) return response, nil } func (c *AlbumListController) GetStarred2(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { - albums, mediaFiles, err := c.listGen.GetAllStarred() + artists, albums, mediaFiles, err := c.listGen.GetAllStarred() if err != nil { log.Error(r, "Error retrieving starred media", "error", err) return nil, NewError(responses.ErrorGeneric, "Internal Error") @@ -103,6 +104,7 @@ func (c *AlbumListController) GetStarred2(w http.ResponseWriter, r *http.Request response := NewResponse() response.Starred2 = &responses.Starred{} + response.Starred2.Artist = ToArtists(artists) response.Starred2.Album = ToAlbums(albums) response.Starred2.Song = ToChildren(mediaFiles) return response, nil diff --git a/api/helpers.go b/api/helpers.go index a8bdfaba..f607d814 100644 --- a/api/helpers.go +++ b/api/helpers.go @@ -145,6 +145,21 @@ func ToAlbum(entry engine.Entry) responses.Child { return album } +func ToArtists(entries engine.Entries) []responses.Artist { + artists := make([]responses.Artist, len(entries)) + for i, entry := range entries { + artists[i] = responses.Artist{ + Id: entry.Id, + Name: entry.Title, + AlbumCount: entry.AlbumCount, + } + if !entry.Starred.IsZero() { + artists[i].Starred = &entry.Starred + } + } + return artists +} + func ToChildren(entries engine.Entries) []responses.Child { children := make([]responses.Child, len(entries)) for i, entry := range entries { diff --git a/api/media_annotation.go b/api/media_annotation.go index d54edb0e..52c2f332 100644 --- a/api/media_annotation.go +++ b/api/media_annotation.go @@ -50,12 +50,15 @@ func (c *MediaAnnotationController) SetRating(w http.ResponseWriter, r *http.Req func (c *MediaAnnotationController) getIds(r *http.Request) ([]string, error) { ids := ParamStrings(r, "id") albumIds := ParamStrings(r, "albumId") + artistIds := ParamStrings(r, "artistId") - if len(ids) == 0 && len(albumIds) == 0 { + if len(ids)+len(albumIds)+len(artistIds) == 0 { return nil, NewError(responses.ErrorMissingParameter, "Required id parameter is missing") } - return append(ids, albumIds...), nil + ids = append(ids, albumIds...) + ids = append(ids, artistIds...) + return ids, nil } func (c *MediaAnnotationController) Star(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { diff --git a/api/responses/.snapshots/responses-snapshotMatcher-Match-Responses Indexes with data should match .JSON b/api/responses/.snapshots/responses-snapshotMatcher-Match-Responses Indexes with data should match .JSON index b1f56886..5eac46ab 100644 --- a/api/responses/.snapshots/responses-snapshotMatcher-Match-Responses Indexes with data should match .JSON +++ b/api/responses/.snapshots/responses-snapshotMatcher-Match-Responses Indexes with data should match .JSON @@ -1 +1 @@ -{"status":"ok","version":"1.8.0","indexes":{"index":[{"name":"A","artist":[{"id":"111","name":"aaa"}]}],"lastModified":"1","ignoredArticles":"A"}} +{"status":"ok","version":"1.8.0","indexes":{"index":[{"name":"A","artist":[{"id":"111","name":"aaa","starred":"2016-03-02T20:30:00Z"}]}],"lastModified":"1","ignoredArticles":"A"}} diff --git a/api/responses/.snapshots/responses-snapshotMatcher-Match-Responses Indexes with data should match .XML b/api/responses/.snapshots/responses-snapshotMatcher-Match-Responses Indexes with data should match .XML index b5493f87..f89fb21a 100644 --- a/api/responses/.snapshots/responses-snapshotMatcher-Match-Responses Indexes with data should match .XML +++ b/api/responses/.snapshots/responses-snapshotMatcher-Match-Responses Indexes with data should match .XML @@ -1 +1 @@ - + diff --git a/api/responses/responses.go b/api/responses/responses.go index 50e79e78..901297ce 100644 --- a/api/responses/responses.go +++ b/api/responses/responses.go @@ -57,11 +57,11 @@ type MusicFolders struct { } type Artist struct { - Id string `xml:"id,attr" json:"id"` - Name string `xml:"name,attr" json:"name"` - AlbumCount int `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"` + Id string `xml:"id,attr" json:"id"` + Name string `xml:"name,attr" json:"name"` + AlbumCount int `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"` + Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"` /* - */ diff --git a/api/responses/responses_test.go b/api/responses/responses_test.go index af17701f..9b7998b7 100644 --- a/api/responses/responses_test.go +++ b/api/responses/responses_test.go @@ -90,7 +90,8 @@ var _ = Describe("Responses", func() { Context("with data", func() { BeforeEach(func() { artists := make([]Artist, 1) - artists[0] = Artist{Id: "111", Name: "aaa"} + t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC) + artists[0] = Artist{Id: "111", Name: "aaa", Starred: &t} index := make([]Index, 1) index[0] = Index{Name: "A", Artists: artists} response.Indexes.Index = index diff --git a/api/searching.go b/api/searching.go index 19c0641a..2d5f89f0 100644 --- a/api/searching.go +++ b/api/searching.go @@ -70,10 +70,7 @@ func (c *SearchingController) Search2(w http.ResponseWriter, r *http.Request) (* response := NewResponse() searchResult2 := &responses.SearchResult2{} - searchResult2.Artist = make([]responses.Artist, len(as)) - for i, e := range as { - searchResult2.Artist[i] = responses.Artist{Id: e.Id, Name: e.Title} - } + searchResult2.Artist = ToArtists(as) searchResult2.Album = ToChildren(als) searchResult2.Song = ToChildren(mfs) response.SearchResult2 = searchResult2 @@ -97,6 +94,9 @@ func (c *SearchingController) Search3(w http.ResponseWriter, r *http.Request) (* CoverArt: e.CoverArt, AlbumCount: e.AlbumCount, } + if !e.Starred.IsZero() { + searchResult3.Artist[i].Starred = &e.Starred + } } searchResult3.Album = ToAlbums(als) searchResult3.Song = ToChildren(mfs) diff --git a/engine/common.go b/engine/common.go index 336e8787..3b865208 100644 --- a/engine/common.go +++ b/engine/common.go @@ -50,6 +50,7 @@ func FromArtist(ar *model.Artist) Entry { e.Id = ar.ID e.Title = ar.Name e.AlbumCount = ar.AlbumCount + e.Starred = ar.StarredAt e.IsDir = true return e } @@ -137,3 +138,11 @@ func FromMediaFiles(mfs model.MediaFiles) Entries { } return entries } + +func FromArtists(ars model.Artists) Entries { + entries := make(Entries, len(ars)) + for i, ar := range ars { + entries[i] = FromArtist(&ar) + } + return entries +} diff --git a/engine/list_generator.go b/engine/list_generator.go index 4a07f86f..f9eec81f 100644 --- a/engine/list_generator.go +++ b/engine/list_generator.go @@ -17,16 +17,17 @@ type ListGenerator interface { GetByName(offset int, size int) (Entries, error) GetByArtist(offset int, size int) (Entries, error) GetStarred(offset int, size int) (Entries, error) - GetAllStarred() (albums Entries, mediaFiles Entries, err error) + GetAllStarred() (artists Entries, albums Entries, mediaFiles Entries, err error) GetNowPlaying() (Entries, error) GetRandomSongs(size int) (Entries, error) } -func NewListGenerator(alr model.AlbumRepository, mfr model.MediaFileRepository, npr model.NowPlayingRepository) ListGenerator { - return &listGenerator{alr, mfr, npr} +func NewListGenerator(arr model.ArtistRepository, alr model.AlbumRepository, mfr model.MediaFileRepository, npr model.NowPlayingRepository) ListGenerator { + return &listGenerator{arr, alr, mfr, npr} } type listGenerator struct { + artistRepo model.ArtistRepository albumRepo model.AlbumRepository mfRepository model.MediaFileRepository npRepo model.NowPlayingRepository @@ -111,7 +112,7 @@ func (g *listGenerator) GetRandomSongs(size int) (Entries, error) { } func (g *listGenerator) GetStarred(offset int, size int) (Entries, error) { - qo := model.QueryOptions{Offset: offset, Size: size, Desc: true} + qo := model.QueryOptions{Offset: offset, Size: size, SortBy: "starred_at", Desc: true} albums, err := g.albumRepo.GetStarred(qo) if err != nil { return nil, err @@ -120,15 +121,24 @@ func (g *listGenerator) GetStarred(offset int, size int) (Entries, error) { return FromAlbums(albums), nil } -func (g *listGenerator) GetAllStarred() (Entries, Entries, error) { - albums, err := g.GetStarred(0, -1) +// TODO Return is confusing +func (g *listGenerator) GetAllStarred() (Entries, Entries, Entries, error) { + artists, err := g.artistRepo.GetStarred(model.QueryOptions{SortBy: "starred_at", Desc: true}) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - mediaFiles, err := g.mfRepository.GetStarred(model.QueryOptions{Desc: true}) + albums, err := g.GetStarred(0, -1) + if err != nil { + return nil, nil, nil, err + } - return albums, FromMediaFiles(mediaFiles), err + mediaFiles, err := g.mfRepository.GetStarred(model.QueryOptions{SortBy: "starred_at", Desc: true}) + if err != nil { + return nil, nil, nil, err + } + + return FromArtists(artists), albums, FromMediaFiles(mediaFiles), err } func (g *listGenerator) GetNowPlaying() (Entries, error) { diff --git a/engine/ratings.go b/engine/ratings.go index 465fea05..2018cffe 100644 --- a/engine/ratings.go +++ b/engine/ratings.go @@ -3,6 +3,7 @@ package engine import ( "context" + "github.com/cloudsonic/sonic-server/conf" "github.com/cloudsonic/sonic-server/itunesbridge" "github.com/cloudsonic/sonic-server/log" "github.com/cloudsonic/sonic-server/model" @@ -55,6 +56,19 @@ func (r ratings) SetRating(ctx context.Context, id string, rating int) error { } func (r ratings) SetStar(ctx context.Context, star bool, ids ...string) error { + if conf.Sonic.DevUseFileScanner { + err := r.mfRepo.SetStar(star, ids...) + if err != nil { + return err + } + err = r.albumRepo.SetStar(star, ids...) + if err != nil { + return err + } + err = r.artistRepo.SetStar(star, ids...) + return err + } + for _, id := range ids { isAlbum, _ := r.albumRepo.Exists(id) if isAlbum { diff --git a/model/album.go b/model/album.go index b452caa4..b96cd009 100644 --- a/model/album.go +++ b/model/album.go @@ -36,6 +36,7 @@ type AlbumRepository interface { PurgeInactive(active Albums) error GetAllIds() ([]string, error) GetStarred(...QueryOptions) (Albums, error) + SetStar(star bool, ids ...string) error Search(q string, offset int, size int) (Albums, error) Refresh(ids ...string) error PurgeEmpty() error diff --git a/model/artist.go b/model/artist.go index a2af148b..ef4b1615 100644 --- a/model/artist.go +++ b/model/artist.go @@ -1,9 +1,13 @@ package model +import "time" + type Artist struct { ID string Name string AlbumCount int + Starred bool + StarredAt time.Time } type Artists []Artist @@ -19,6 +23,8 @@ type ArtistRepository interface { Put(m *Artist) error Get(id string) (*Artist, error) PurgeInactive(active Artists) error + GetStarred(...QueryOptions) (Artists, error) + SetStar(star bool, ids ...string) error Search(q string, offset int, size int) (Artists, error) Refresh(ids ...string) error GetIndex() (ArtistIndexes, error) diff --git a/model/mediafile.go b/model/mediafile.go index 118c0d77..ea2a7a53 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -52,4 +52,6 @@ type MediaFileRepository interface { Search(q string, offset int, size int) (MediaFiles, error) Delete(id string) error DeleteByPath(path string) error + SetStar(star bool, ids ...string) error + SetRating(rating int, ids ...string) error } diff --git a/persistence/album_repository.go b/persistence/album_repository.go index aa584e82..50fd12c7 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -27,7 +27,7 @@ type album struct { Duration int `` Rating int `orm:"index"` Genre string `orm:"index"` - StarredAt time.Time `orm:"null"` + StarredAt time.Time `orm:"index;null"` CreatedAt time.Time `orm:"null"` UpdatedAt time.Time `orm:"null"` } @@ -118,6 +118,9 @@ group by album_id order by f.id`, strings.Join(ids, "','")) if al.Compilation { al.AlbumArtist = "Various Artists" } + if al.AlbumArtist == "" { + al.AlbumArtist = al.Artist + } if al.CurrentId != "" { toUpdate = append(toUpdate, al.album) } else { @@ -172,6 +175,21 @@ func (r *albumRepository) GetStarred(options ...model.QueryOptions) (model.Album return r.toAlbums(starred), nil } +func (r *albumRepository) SetStar(starred bool, ids ...string) error { + if len(ids) == 0 { + return model.ErrNotFound + } + var starredAt time.Time + if starred { + starredAt = time.Now() + } + _, err := r.newQuery(Db()).Filter("id__in", ids).Update(orm.Params{ + "starred": starred, + "starred_at": starredAt, + }) + return err +} + func (r *albumRepository) Search(q string, offset int, size int) (model.Albums, error) { if len(q) <= 2 { return nil, nil diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index abe9d88c..345edba7 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -4,6 +4,7 @@ import ( "fmt" "sort" "strings" + "time" "github.com/astaxie/beego/orm" "github.com/cloudsonic/sonic-server/conf" @@ -13,9 +14,11 @@ import ( ) type artist struct { - ID string `orm:"pk;column(id)"` - Name string `orm:"index"` - AlbumCount int `orm:"column(album_count)"` + ID string `orm:"pk;column(id)"` + Name string `orm:"index"` + AlbumCount int `orm:"column(album_count)"` + Starred bool `orm:"index"` + StarredAt time.Time `orm:"index;null"` } type artistRepository struct { @@ -152,6 +155,30 @@ where f.artist_id in ('%s') group by f.artist_id order by f.id`, strings.Join(id return err } +func (r *artistRepository) GetStarred(options ...model.QueryOptions) (model.Artists, error) { + var starred []artist + _, err := r.newQuery(Db(), options...).Filter("starred", true).All(&starred) + if err != nil { + return nil, err + } + return r.toArtists(starred), nil +} + +func (r *artistRepository) SetStar(starred bool, ids ...string) error { + if len(ids) == 0 { + return model.ErrNotFound + } + var starredAt time.Time + if starred { + starredAt = time.Now() + } + _, err := r.newQuery(Db()).Filter("id__in", ids).Update(orm.Params{ + "starred": starred, + "starred_at": starredAt, + }) + return err +} + func (r *artistRepository) PurgeInactive(activeList model.Artists) error { return withTx(func(o orm.Ormer) error { _, err := r.purgeInactive(o, activeList, func(item interface{}) string { diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index ee72537a..d342be95 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -32,7 +32,7 @@ type mediaFile struct { PlayDate time.Time `orm:"null"` Rating int `orm:"index"` Starred bool `orm:"index"` - StarredAt time.Time `orm:"null"` + StarredAt time.Time `orm:"index;null"` CreatedAt time.Time `orm:"null"` UpdatedAt time.Time `orm:"null"` } @@ -135,6 +135,29 @@ func (r *mediaFileRepository) GetStarred(options ...model.QueryOptions) (model.M return r.toMediaFiles(starred), nil } +func (r *mediaFileRepository) SetStar(starred bool, ids ...string) error { + if len(ids) == 0 { + return model.ErrNotFound + } + var starredAt time.Time + if starred { + starredAt = time.Now() + } + _, err := r.newQuery(Db()).Filter("id__in", ids).Update(orm.Params{ + "starred": starred, + "starred_at": starredAt, + }) + return err +} + +func (r *mediaFileRepository) SetRating(rating int, ids ...string) error { + if len(ids) == 0 { + return model.ErrNotFound + } + _, err := r.newQuery(Db()).Filter("id__in", ids).Update(orm.Params{"rating": rating}) + return err +} + func (r *mediaFileRepository) PurgeInactive(activeList model.MediaFiles) error { return withTx(func(o orm.Ormer) error { _, err := r.purgeInactive(o, activeList, func(item interface{}) string { diff --git a/wire_gen.go b/wire_gen.go index 60f05167..cf601d31 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -43,7 +43,7 @@ func CreateSubsonicAPIRouter() *api.Router { browser := engine.NewBrowser(propertyRepository, mediaFolderRepository, artistRepository, albumRepository, mediaFileRepository, genreRepository) cover := engine.NewCover(mediaFileRepository, albumRepository) nowPlayingRepository := persistence.NewNowPlayingRepository() - listGenerator := engine.NewListGenerator(albumRepository, mediaFileRepository, nowPlayingRepository) + listGenerator := engine.NewListGenerator(artistRepository, albumRepository, mediaFileRepository, nowPlayingRepository) itunesControl := itunesbridge.NewItunesControl() playlistRepository := persistence.NewPlaylistRepository() playlists := engine.NewPlaylists(itunesControl, playlistRepository, mediaFileRepository)