From d7116eebd47a36cf93d12791ef6e78332fdd8fe9 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 21 Jan 2020 23:01:43 -0500 Subject: [PATCH] Implement annotations per user --- engine/browser.go | 78 +++++++++---- engine/common.go | 50 ++++++--- engine/list_generator.go | 141 +++++++++++++++-------- engine/playlists.go | 14 ++- engine/ratings.go | 54 +++++++-- engine/scrobbler.go | 6 +- engine/search.go | 54 ++++++--- model/album.go | 9 +- model/annotation.go | 32 ++++++ model/artist.go | 6 +- model/datastore.go | 1 + model/mediafile.go | 10 +- persistence/album_repository.go | 50 +++------ persistence/album_repository_test.go | 2 +- persistence/annotation_repository.go | 154 ++++++++++++++++++++++++++ persistence/artist_repository.go | 19 ++-- persistence/mediafile_repository.go | 50 ++------- persistence/mock_persistence.go | 4 + persistence/persistence.go | 5 + persistence/persistence_suite_test.go | 17 ++- server/subsonic/album_lists.go | 13 ++- server/subsonic/album_lists_test.go | 3 +- server/subsonic/browsing.go | 2 +- server/subsonic/media_annotation.go | 56 +++++----- server/subsonic/playlists.go | 2 +- server/subsonic/stream.go | 2 +- 26 files changed, 572 insertions(+), 262 deletions(-) create mode 100644 model/annotation.go create mode 100644 persistence/annotation_repository.go diff --git a/engine/browser.go b/engine/browser.go index 60a096f67..962e56490 100644 --- a/engine/browser.go +++ b/engine/browser.go @@ -19,7 +19,7 @@ type Browser interface { Directory(ctx context.Context, id string) (*DirectoryInfo, error) Artist(ctx context.Context, id string) (*DirectoryInfo, error) Album(ctx context.Context, id string) (*DirectoryInfo, error) - GetSong(id string) (*Entry, error) + GetSong(ctx context.Context, id string) (*Entry, error) GetGenres() (model.Genres, error) } @@ -77,7 +77,12 @@ func (b *browser) Artist(ctx context.Context, id string) (*DirectoryInfo, error) return nil, err } log.Debug(ctx, "Found Artist", "id", id, "name", a.Name) - return b.buildArtistDir(a, albums), nil + var albumIds []string + for _, al := range albums { + albumIds = append(albumIds, al.ID) + } + annMap, err := b.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds) + return b.buildArtistDir(a, albums, annMap), nil } func (b *browser) Album(ctx context.Context, id string) (*DirectoryInfo, error) { @@ -86,7 +91,21 @@ func (b *browser) Album(ctx context.Context, id string) (*DirectoryInfo, error) return nil, err } log.Debug(ctx, "Found Album", "id", id, "name", al.Name) - return b.buildAlbumDir(al, tracks), nil + var mfIds []string + for _, mf := range tracks { + mfIds = append(mfIds, mf.ID) + } + + userID := getUserID(ctx) + trackAnnMap, err := b.ds.Annotation().GetMap(userID, model.MediaItemType, mfIds) + if err != nil { + return nil, err + } + ann, err := b.ds.Annotation().Get(userID, model.AlbumItemType, al.ID) + if err != nil { + return nil, err + } + return b.buildAlbumDir(al, ann, tracks, trackAnnMap), nil } func (b *browser) Directory(ctx context.Context, id string) (*DirectoryInfo, error) { @@ -101,13 +120,19 @@ func (b *browser) Directory(ctx context.Context, id string) (*DirectoryInfo, err } } -func (b *browser) GetSong(id string) (*Entry, error) { +func (b *browser) GetSong(ctx context.Context, id string) (*Entry, error) { mf, err := b.ds.MediaFile().Get(id) if err != nil { return nil, err } - entry := FromMediaFile(mf) + userId := getUserID(ctx) + ann, err := b.ds.Annotation().Get(userId, model.MediaItemType, id) + if err != nil { + return nil, err + } + + entry := FromMediaFile(mf, ann) return &entry, nil } @@ -124,7 +149,7 @@ func (b *browser) GetGenres() (model.Genres, error) { return genres, err } -func (b *browser) buildArtistDir(a *model.Artist, albums model.Albums) *DirectoryInfo { +func (b *browser) buildArtistDir(a *model.Artist, albums model.Albums, albumAnnMap model.AnnotationMap) *DirectoryInfo { dir := &DirectoryInfo{ Id: a.ID, Name: a.Name, @@ -133,33 +158,38 @@ func (b *browser) buildArtistDir(a *model.Artist, albums model.Albums) *Director dir.Entries = make(Entries, len(albums)) for i, al := range albums { - dir.Entries[i] = FromAlbum(&al) - dir.PlayCount += int32(al.PlayCount) + ann := albumAnnMap[al.ID] + dir.Entries[i] = FromAlbum(&al, &ann) + dir.PlayCount += int32(ann.PlayCount) } return dir } -func (b *browser) buildAlbumDir(al *model.Album, tracks model.MediaFiles) *DirectoryInfo { +func (b *browser) buildAlbumDir(al *model.Album, albumAnn *model.Annotation, tracks model.MediaFiles, trackAnnMap model.AnnotationMap) *DirectoryInfo { dir := &DirectoryInfo{ - Id: al.ID, - Name: al.Name, - Parent: al.ArtistID, - PlayCount: int32(al.PlayCount), - UserRating: al.Rating, - Starred: al.StarredAt, - Artist: al.Artist, - ArtistId: al.ArtistID, - SongCount: al.SongCount, - Duration: al.Duration, - Created: al.CreatedAt, - Year: al.Year, - Genre: al.Genre, - CoverArt: al.CoverArtId, + Id: al.ID, + Name: al.Name, + Parent: al.ArtistID, + Artist: al.Artist, + ArtistId: al.ArtistID, + SongCount: al.SongCount, + Duration: al.Duration, + Created: al.CreatedAt, + Year: al.Year, + Genre: al.Genre, + CoverArt: al.CoverArtId, + } + if albumAnn != nil { + dir.PlayCount = int32(albumAnn.PlayCount) + dir.Starred = albumAnn.StarredAt + dir.UserRating = albumAnn.Rating } dir.Entries = make(Entries, len(tracks)) for i, mf := range tracks { - dir.Entries[i] = FromMediaFile(&mf) + mfId := mf.ID + ann := trackAnnMap[mfId] + dir.Entries[i] = FromMediaFile(&mf, &ann) } return dir } diff --git a/engine/common.go b/engine/common.go index 3b865208f..991fcfc87 100644 --- a/engine/common.go +++ b/engine/common.go @@ -1,6 +1,7 @@ package engine import ( + "context" "fmt" "time" @@ -45,17 +46,19 @@ type Entry struct { type Entries []Entry -func FromArtist(ar *model.Artist) Entry { +func FromArtist(ar *model.Artist, ann *model.Annotation) Entry { e := Entry{} e.Id = ar.ID e.Title = ar.Name e.AlbumCount = ar.AlbumCount - e.Starred = ar.StarredAt e.IsDir = true + if ann != nil { + e.Starred = ann.StarredAt + } return e } -func FromAlbum(al *model.Album) Entry { +func FromAlbum(al *model.Album, ann *model.Annotation) Entry { e := Entry{} e.Id = al.ID e.Title = al.Name @@ -66,18 +69,20 @@ func FromAlbum(al *model.Album) Entry { e.Artist = al.AlbumArtist e.Genre = al.Genre e.CoverArt = al.CoverArtId - e.Starred = al.StarredAt - e.PlayCount = int32(al.PlayCount) e.Created = al.CreatedAt e.AlbumId = al.ID e.ArtistId = al.ArtistID - e.UserRating = al.Rating e.Duration = al.Duration e.SongCount = al.SongCount + if ann != nil { + e.Starred = ann.StarredAt + e.PlayCount = int32(ann.PlayCount) + e.UserRating = ann.Rating + } return e } -func FromMediaFile(mf *model.MediaFile) Entry { +func FromMediaFile(mf *model.MediaFile, ann *model.Annotation) Entry { e := Entry{} e.Id = mf.ID e.Title = mf.Title @@ -92,7 +97,6 @@ func FromMediaFile(mf *model.MediaFile) Entry { e.Size = mf.Size e.Suffix = mf.Suffix e.BitRate = mf.BitRate - e.Starred = mf.StarredAt if mf.HasCoverArt { e.CoverArt = mf.ID } @@ -102,13 +106,16 @@ func FromMediaFile(mf *model.MediaFile) Entry { if mf.Path != "" { e.Path = fmt.Sprintf("%s/%s/%s.%s", realArtistName(mf), mf.Album, mf.Title, mf.Suffix) } - e.PlayCount = int32(mf.PlayCount) e.DiscNumber = mf.DiscNumber e.Created = mf.CreatedAt e.AlbumId = mf.AlbumID e.ArtistId = mf.ArtistID e.Type = "music" // TODO Hardcoded for now - e.UserRating = mf.Rating + if ann != nil { + e.PlayCount = int32(ann.PlayCount) + e.Starred = ann.StarredAt + e.UserRating = ann.Rating + } return e } @@ -123,26 +130,37 @@ func realArtistName(mf *model.MediaFile) string { return mf.Artist } -func FromAlbums(albums model.Albums) Entries { +func FromAlbums(albums model.Albums, annMap model.AnnotationMap) Entries { entries := make(Entries, len(albums)) for i, al := range albums { - entries[i] = FromAlbum(&al) + ann := annMap[al.ID] + entries[i] = FromAlbum(&al, &ann) } return entries } -func FromMediaFiles(mfs model.MediaFiles) Entries { +func FromMediaFiles(mfs model.MediaFiles, annMap model.AnnotationMap) Entries { entries := make(Entries, len(mfs)) for i, mf := range mfs { - entries[i] = FromMediaFile(&mf) + ann := annMap[mf.ID] + entries[i] = FromMediaFile(&mf, &ann) } return entries } -func FromArtists(ars model.Artists) Entries { +func FromArtists(ars model.Artists, annMap model.AnnotationMap) Entries { entries := make(Entries, len(ars)) for i, ar := range ars { - entries[i] = FromArtist(&ar) + ann := annMap[ar.ID] + entries[i] = FromArtist(&ar, &ann) } return entries } + +func getUserID(ctx context.Context) string { + user, ok := ctx.Value("user").(*model.User) + if ok { + return user.ID + } + return "" +} diff --git a/engine/list_generator.go b/engine/list_generator.go index b7468f65d..1bbddf6c1 100644 --- a/engine/list_generator.go +++ b/engine/list_generator.go @@ -1,23 +1,24 @@ package engine import ( + "context" "time" "github.com/cloudsonic/sonic-server/model" ) type ListGenerator interface { - GetNewest(offset int, size int) (Entries, error) - GetRecent(offset int, size int) (Entries, error) - GetFrequent(offset int, size int) (Entries, error) - GetHighest(offset int, size int) (Entries, error) - GetRandom(offset int, size int) (Entries, error) - GetByName(offset int, size int) (Entries, error) - GetByArtist(offset int, size int) (Entries, error) - GetStarred(offset int, size int) (Entries, error) - GetAllStarred() (artists Entries, albums Entries, mediaFiles Entries, err error) - GetNowPlaying() (Entries, error) - GetRandomSongs(size int, genre string) (Entries, error) + GetNewest(ctx context.Context, offset int, size int) (Entries, error) + GetRecent(ctx context.Context, offset int, size int) (Entries, error) + GetFrequent(ctx context.Context, offset int, size int) (Entries, error) + GetHighest(ctx context.Context, offset int, size int) (Entries, error) + GetRandom(ctx context.Context, offset int, size int) (Entries, error) + GetByName(ctx context.Context, offset int, size int) (Entries, error) + GetByArtist(ctx context.Context, offset int, size int) (Entries, error) + GetStarred(ctx context.Context, offset int, size int) (Entries, error) + GetAllStarred(ctx context.Context) (artists Entries, albums Entries, mediaFiles Entries, err error) + GetNowPlaying(ctx context.Context) (Entries, error) + GetRandomSongs(ctx context.Context, size int, genre string) (Entries, error) } func NewListGenerator(ds model.DataStore, npRepo NowPlayingRepository) ListGenerator { @@ -30,58 +31,76 @@ type listGenerator struct { } // TODO: Only return albums that have the Sort field != empty -func (g *listGenerator) query(qo model.QueryOptions, offset int, size int) (Entries, error) { +func (g *listGenerator) query(ctx context.Context, qo model.QueryOptions, offset int, size int) (Entries, error) { qo.Offset = offset qo.Max = size albums, err := g.ds.Album().GetAll(qo) - - return FromAlbums(albums), err + if err != nil { + return nil, err + } + albumIds := make([]string, len(albums)) + for i, al := range albums { + albumIds[i] = al.ID + } + annMap, err := g.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds) + if err != nil { + return nil, err + } + return FromAlbums(albums, annMap), err } -func (g *listGenerator) GetNewest(offset int, size int) (Entries, error) { +func (g *listGenerator) GetNewest(ctx context.Context, offset int, size int) (Entries, error) { qo := model.QueryOptions{Sort: "CreatedAt", Order: "desc"} - return g.query(qo, offset, size) + return g.query(ctx, qo, offset, size) } -func (g *listGenerator) GetRecent(offset int, size int) (Entries, error) { +func (g *listGenerator) GetRecent(ctx context.Context, offset int, size int) (Entries, error) { qo := model.QueryOptions{Sort: "PlayDate", Order: "desc"} - return g.query(qo, offset, size) + return g.query(ctx, qo, offset, size) } -func (g *listGenerator) GetFrequent(offset int, size int) (Entries, error) { +func (g *listGenerator) GetFrequent(ctx context.Context, offset int, size int) (Entries, error) { qo := model.QueryOptions{Sort: "PlayCount", Order: "desc"} - return g.query(qo, offset, size) + return g.query(ctx, qo, offset, size) } -func (g *listGenerator) GetHighest(offset int, size int) (Entries, error) { +func (g *listGenerator) GetHighest(ctx context.Context, offset int, size int) (Entries, error) { qo := model.QueryOptions{Sort: "Rating", Order: "desc"} - return g.query(qo, offset, size) + return g.query(ctx, qo, offset, size) } -func (g *listGenerator) GetByName(offset int, size int) (Entries, error) { +func (g *listGenerator) GetByName(ctx context.Context, offset int, size int) (Entries, error) { qo := model.QueryOptions{Sort: "Name"} - return g.query(qo, offset, size) + return g.query(ctx, qo, offset, size) } -func (g *listGenerator) GetByArtist(offset int, size int) (Entries, error) { +func (g *listGenerator) GetByArtist(ctx context.Context, offset int, size int) (Entries, error) { qo := model.QueryOptions{Sort: "Artist"} - return g.query(qo, offset, size) + return g.query(ctx, qo, offset, size) } -func (g *listGenerator) GetRandom(offset int, size int) (Entries, error) { +func (g *listGenerator) GetRandom(ctx context.Context, offset int, size int) (Entries, error) { albums, err := g.ds.Album().GetRandom(model.QueryOptions{Max: size, Offset: offset}) if err != nil { return nil, err } - r := make(Entries, len(albums)) - for i, al := range albums { - r[i] = FromAlbum(&al) + annMap, err := g.getAnnotationsForAlbums(ctx, albums) + if err != nil { + return nil, err } - return r, nil + return FromAlbums(albums, annMap), nil } -func (g *listGenerator) GetRandomSongs(size int, genre string) (Entries, error) { +func (g *listGenerator) getAnnotationsForAlbums(ctx context.Context, albums model.Albums) (model.AnnotationMap, error) { + albumIds := make([]string, len(albums)) + for i, al := range albums { + albumIds[i] = al.ID + } + return g.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds) +} + +func (g *listGenerator) GetRandomSongs(ctx context.Context, size int, genre string) (Entries, error) { options := model.QueryOptions{Max: size} if genre != "" { options.Filters = map[string]interface{}{"genre": genre} @@ -93,47 +112,78 @@ func (g *listGenerator) GetRandomSongs(size int, genre string) (Entries, error) r := make(Entries, len(mediaFiles)) for i, mf := range mediaFiles { - r[i] = FromMediaFile(&mf) + ann, err := g.ds.Annotation().Get(getUserID(ctx), model.MediaItemType, mf.ID) + if err != nil { + return nil, err + } + r[i] = FromMediaFile(&mf, ann) } return r, nil } -func (g *listGenerator) GetStarred(offset int, size int) (Entries, error) { +func (g *listGenerator) GetStarred(ctx context.Context, offset int, size int) (Entries, error) { qo := model.QueryOptions{Offset: offset, Max: size, Sort: "starred_at", Order: "desc"} - albums, err := g.ds.Album().GetStarred(qo) + albums, err := g.ds.Album().GetStarred(getUserID(ctx), qo) if err != nil { return nil, err } - return FromAlbums(albums), nil + annMap, err := g.getAnnotationsForAlbums(ctx, albums) + if err != nil { + return nil, err + } + return FromAlbums(albums, annMap), nil } -func (g *listGenerator) GetAllStarred() (artists Entries, albums Entries, mediaFiles Entries, err error) { +func (g *listGenerator) GetAllStarred(ctx context.Context) (artists Entries, albums Entries, mediaFiles Entries, err error) { options := model.QueryOptions{Sort: "starred_at", Order: "desc"} - ars, err := g.ds.Artist().GetStarred(options) + ars, err := g.ds.Artist().GetStarred(getUserID(ctx), options) if err != nil { return nil, nil, nil, err } - als, err := g.ds.Album().GetStarred(options) + als, err := g.ds.Album().GetStarred(getUserID(ctx), options) if err != nil { return nil, nil, nil, err } - mfs, err := g.ds.MediaFile().GetStarred(options) + mfs, err := g.ds.MediaFile().GetStarred(getUserID(ctx), options) if err != nil { return nil, nil, nil, err } - artists = FromArtists(ars) - albums = FromAlbums(als) - mediaFiles = FromMediaFiles(mfs) + var mfIds []string + for _, mf := range mfs { + mfIds = append(mfIds, mf.ID) + } + trackAnnMap, err := g.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, mfIds) + if err != nil { + return nil, nil, nil, err + } + + albumAnnMap, err := g.getAnnotationsForAlbums(ctx, als) + if err != nil { + return nil, nil, nil, err + } + + var artistIds []string + for _, ar := range ars { + artistIds = append(artistIds, ar.ID) + } + artistAnnMap, err := g.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, artistIds) + if err != nil { + return nil, nil, nil, err + } + + artists = FromArtists(ars, artistAnnMap) + albums = FromAlbums(als, albumAnnMap) + mediaFiles = FromMediaFiles(mfs, trackAnnMap) return } -func (g *listGenerator) GetNowPlaying() (Entries, error) { +func (g *listGenerator) GetNowPlaying(ctx context.Context) (Entries, error) { npInfo, err := g.npRepo.GetAll() if err != nil { return nil, err @@ -144,7 +194,8 @@ func (g *listGenerator) GetNowPlaying() (Entries, error) { if err != nil { return nil, err } - entries[i] = FromMediaFile(mf) + ann, err := g.ds.Annotation().Get(getUserID(ctx), model.MediaItemType, mf.ID) + entries[i] = FromMediaFile(mf, ann) entries[i].UserName = np.Username entries[i].MinutesAgo = int(time.Now().Sub(np.Start).Minutes()) entries[i].PlayerId = np.PlayerId diff --git a/engine/playlists.go b/engine/playlists.go index 0b6a905c6..14b2ec157 100644 --- a/engine/playlists.go +++ b/engine/playlists.go @@ -10,7 +10,7 @@ import ( type Playlists interface { GetAll() (model.Playlists, error) - Get(id string) (*PlaylistInfo, error) + Get(ctx context.Context, id string) (*PlaylistInfo, error) Create(ctx context.Context, playlistId, name string, ids []string) error Delete(ctx context.Context, playlistId string) error Update(ctx context.Context, playlistId string, name *string, idsToAdd []string, idxToRemove []int) error @@ -118,7 +118,7 @@ type PlaylistInfo struct { Comment string } -func (p *playlists) Get(id string) (*PlaylistInfo, error) { +func (p *playlists) Get(ctx context.Context, id string) (*PlaylistInfo, error) { pl, err := p.ds.Playlist().GetWithTracks(id) if err != nil { return nil, err @@ -136,8 +136,16 @@ func (p *playlists) Get(id string) (*PlaylistInfo, error) { } pinfo.Entries = make(Entries, len(pl.Tracks)) + var mfIds []string + for _, mf := range pl.Tracks { + mfIds = append(mfIds, mf.ID) + } + + annMap, err := p.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, mfIds) + for i, mf := range pl.Tracks { - pinfo.Entries[i] = FromMediaFile(&mf) + ann := annMap[mf.ID] + pinfo.Entries[i] = FromMediaFile(&mf, &ann) } return pinfo, nil diff --git a/engine/ratings.go b/engine/ratings.go index d2cf58122..06f86c41c 100644 --- a/engine/ratings.go +++ b/engine/ratings.go @@ -3,6 +3,7 @@ package engine import ( "context" + "github.com/cloudsonic/sonic-server/log" "github.com/cloudsonic/sonic-server/model" ) @@ -20,21 +21,52 @@ type ratings struct { } func (r ratings) SetRating(ctx context.Context, id string, rating int) error { - // TODO - return model.ErrNotFound + exist, err := r.ds.Album().Exists(id) + if err != nil { + return err + } + if exist { + return r.ds.Annotation().SetRating(rating, getUserID(ctx), model.AlbumItemType, id) + } + return r.ds.Annotation().SetRating(rating, getUserID(ctx), model.MediaItemType, id) } func (r ratings) SetStar(ctx context.Context, star bool, ids ...string) error { + if len(ids) == 0 { + log.Warn(ctx, "Cannot star/unstar an empty list of ids") + return nil + } + userId := getUserID(ctx) + return r.ds.WithTx(func(tx model.DataStore) error { - err := tx.MediaFile().SetStar(star, ids...) - if err != nil { - return err + for _, id := range ids { + exist, err := r.ds.Album().Exists(id) + if err != nil { + return err + } + if exist { + err = tx.Annotation().SetStar(star, userId, model.AlbumItemType, ids...) + if err != nil { + return err + } + continue + } + exist, err = r.ds.Artist().Exists(id) + if err != nil { + return err + } + if exist { + err = tx.Annotation().SetStar(star, userId, model.ArtistItemType, ids...) + if err != nil { + return err + } + continue + } + err = tx.Annotation().SetStar(star, userId, model.MediaItemType, ids...) + if err != nil { + return err + } } - err = tx.Album().SetStar(star, ids...) - if err != nil { - return err - } - err = tx.Artist().SetStar(star, ids...) - return err + return nil }) } diff --git a/engine/scrobbler.go b/engine/scrobbler.go index 85e46282f..99bc20a51 100644 --- a/engine/scrobbler.go +++ b/engine/scrobbler.go @@ -24,6 +24,8 @@ type scrobbler struct { } func (s *scrobbler) Register(ctx context.Context, playerId int, trackId string, playTime time.Time) (*model.MediaFile, error) { + userId := getUserID(ctx) + var mf *model.MediaFile var err error err = s.ds.WithTx(func(tx model.DataStore) error { @@ -31,11 +33,11 @@ func (s *scrobbler) Register(ctx context.Context, playerId int, trackId string, if err != nil { return err } - err = s.ds.MediaFile().MarkAsPlayed(trackId, playTime) + err = s.ds.Annotation().IncPlayCount(userId, model.MediaItemType, trackId, playTime) if err != nil { return err } - err = s.ds.Album().MarkAsPlayed(mf.AlbumID, playTime) + err = s.ds.Annotation().IncPlayCount(userId, model.AlbumItemType, mf.AlbumID, playTime) return err }) return mf, err diff --git a/engine/search.go b/engine/search.go index 295abf118..6ba8364f8 100644 --- a/engine/search.go +++ b/engine/search.go @@ -25,39 +25,57 @@ func NewSearch(ds model.DataStore) Search { func (s *search) SearchArtist(ctx context.Context, q string, offset int, size int) (Entries, error) { q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*"))) - resp, err := s.ds.Artist().Search(q, offset, size) + artists, err := s.ds.Artist().Search(q, offset, size) + if len(artists) == 0 || err != nil { + return nil, nil + } + + artistIds := make([]string, len(artists)) + for i, al := range artists { + artistIds[i] = al.ID + } + annMap, err := s.ds.Annotation().GetMap(getUserID(ctx), model.ArtistItemType, artistIds) if err != nil { return nil, nil } - res := make(Entries, 0, len(resp)) - for _, ar := range resp { - res = append(res, FromArtist(&ar)) - } - return res, nil + + return FromArtists(artists, annMap), nil } func (s *search) SearchAlbum(ctx context.Context, q string, offset int, size int) (Entries, error) { q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*"))) - resp, err := s.ds.Album().Search(q, offset, size) + albums, err := s.ds.Album().Search(q, offset, size) + if len(albums) == 0 || err != nil { + return nil, nil + } + + albumIds := make([]string, len(albums)) + for i, al := range albums { + albumIds[i] = al.ID + } + annMap, err := s.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds) if err != nil { return nil, nil } - res := make(Entries, 0, len(resp)) - for _, al := range resp { - res = append(res, FromAlbum(&al)) - } - return res, nil + + return FromAlbums(albums, annMap), nil } func (s *search) SearchSong(ctx context.Context, q string, offset int, size int) (Entries, error) { q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*"))) - resp, err := s.ds.MediaFile().Search(q, offset, size) + mediaFiles, err := s.ds.MediaFile().Search(q, offset, size) + if len(mediaFiles) == 0 || err != nil { + return nil, nil + } + + trackIds := make([]string, len(mediaFiles)) + for i, mf := range mediaFiles { + trackIds[i] = mf.ID + } + annMap, err := s.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, trackIds) if err != nil { return nil, nil } - res := make(Entries, 0, len(resp)) - for _, mf := range resp { - res = append(res, FromMediaFile(&mf)) - } - return res, nil + + return FromMediaFiles(mediaFiles, annMap), nil } diff --git a/model/album.go b/model/album.go index 22c420552..c8862a6df 100644 --- a/model/album.go +++ b/model/album.go @@ -12,14 +12,9 @@ type Album struct { AlbumArtist string Year int Compilation bool - Starred bool - PlayCount int - PlayDate time.Time SongCount int Duration int - Rating int Genre string - StarredAt time.Time CreatedAt time.Time UpdatedAt time.Time } @@ -34,10 +29,8 @@ type AlbumRepository interface { FindByArtist(artistId string) (Albums, error) GetAll(...QueryOptions) (Albums, error) GetRandom(...QueryOptions) (Albums, error) - GetStarred(...QueryOptions) (Albums, error) + GetStarred(userId string, options ...QueryOptions) (Albums, error) Search(q string, offset int, size int) (Albums, error) Refresh(ids ...string) error PurgeEmpty() error - SetStar(star bool, ids ...string) error - MarkAsPlayed(id string, playDate time.Time) error } diff --git a/model/annotation.go b/model/annotation.go new file mode 100644 index 000000000..4c2cd48f7 --- /dev/null +++ b/model/annotation.go @@ -0,0 +1,32 @@ +package model + +import "time" + +const ( + ArtistItemType = "artist" + AlbumItemType = "album" + MediaItemType = "mediaFile" +) + +type Annotation struct { + AnnotationID string + UserID string + ItemID string + ItemType string + PlayCount int + PlayDate time.Time + Rating int + Starred bool + StarredAt time.Time +} + +type AnnotationMap map[string]Annotation + +type AnnotationRepository interface { + Get(userID, itemType string, itemID string) (*Annotation, error) + GetMap(userID, itemType string, itemID []string) (AnnotationMap, error) + Delete(userID, itemType string, itemID ...string) error + IncPlayCount(userID, itemType string, itemID string, ts time.Time) error + SetStar(starred bool, userID, itemType string, ids ...string) error + SetRating(rating int, userID, itemType string, itemID string) error +} diff --git a/model/artist.go b/model/artist.go index b084ca6c5..16d325577 100644 --- a/model/artist.go +++ b/model/artist.go @@ -1,13 +1,9 @@ package model -import "time" - type Artist struct { ID string Name string AlbumCount int - Starred bool - StarredAt time.Time } type Artists []Artist @@ -22,7 +18,7 @@ type ArtistRepository interface { Exists(id string) (bool, error) Put(m *Artist) error Get(id string) (*Artist, error) - GetStarred(...QueryOptions) (Artists, error) + GetStarred(userId string, options ...QueryOptions) (Artists, error) SetStar(star bool, ids ...string) error Search(q string, offset int, size int) (Artists, error) Refresh(ids ...string) error diff --git a/model/datastore.go b/model/datastore.go index a6d8ba0cc..493a8cb69 100644 --- a/model/datastore.go +++ b/model/datastore.go @@ -30,6 +30,7 @@ type DataStore interface { Playlist() PlaylistRepository Property() PropertyRepository User() UserRepository + Annotation() AnnotationRepository Resource(model interface{}) ResourceRepository diff --git a/model/mediafile.go b/model/mediafile.go index 8e3d15e93..395e5d41e 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -24,11 +24,6 @@ type MediaFile struct { BitRate int Genre string Compilation bool - PlayCount int - PlayDate time.Time - Rating int - Starred bool - StarredAt time.Time CreatedAt time.Time UpdatedAt time.Time } @@ -46,12 +41,9 @@ type MediaFileRepository interface { Get(id string) (*MediaFile, error) FindByAlbum(albumId string) (MediaFiles, error) FindByPath(path string) (MediaFiles, error) - GetStarred(options ...QueryOptions) (MediaFiles, error) + GetStarred(userId string, options ...QueryOptions) (MediaFiles, error) GetRandom(options ...QueryOptions) (MediaFiles, error) 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 - MarkAsPlayed(id string, playTime time.Time) error } diff --git a/persistence/album_repository.go b/persistence/album_repository.go index 2f17ff5b7..de45be7b0 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/Masterminds/squirrel" "github.com/astaxie/beego/orm" "github.com/cloudsonic/sonic-server/log" "github.com/cloudsonic/sonic-server/model" @@ -20,14 +21,9 @@ type album struct { AlbumArtist string `` Year int `orm:"index"` Compilation bool `` - Starred bool `orm:"index"` - PlayCount int `orm:"index"` - PlayDate time.Time `orm:"null;index"` SongCount int `` Duration int `` - Rating int `orm:"index"` Genre string `orm:"index"` - StarredAt time.Time `orm:"index;null"` CreatedAt time.Time `orm:"null"` UpdatedAt time.Time `orm:"null"` } @@ -115,9 +111,9 @@ func (r *albumRepository) Refresh(ids ...string) error { o := r.ormer sql := fmt.Sprintf(` select album_id as id, album as name, f.artist, f.album_artist, f.artist_id, f.compilation, f.genre, - max(f.year) as year, sum(f.play_count) as play_count, max(f.play_date) as play_date, sum(f.duration) as duration, - max(f.updated_at) as updated_at, min(f.created_at) as created_at, count(*) as song_count, - a.id as current_id, f.id as cover_art_id, f.path as cover_art_path, f.has_cover_art + max(f.year) as year, sum(f.duration) as duration, max(f.updated_at) as updated_at, + min(f.created_at) as created_at, count(*) as song_count, a.id as current_id, f.id as cover_art_id, + f.path as cover_art_path, f.has_cover_art from media_file f left outer join album a on f.album_id = a.id where f.album_id in ('%s') group by album_id order by f.id`, strings.Join(ids, "','")) @@ -157,9 +153,8 @@ group by album_id order by f.id`, strings.Join(ids, "','")) } if len(toUpdate) > 0 { for _, al := range toUpdate { - // Don't update Starred/Rating _, err := o.Update(&al, "name", "artist_id", "cover_art_path", "cover_art_id", "artist", "album_artist", - "year", "compilation", "play_count", "play_date", "song_count", "duration", "updated_at", "created_at") + "year", "compilation", "song_count", "duration", "updated_at", "created_at") if err != nil { return err } @@ -174,45 +169,28 @@ func (r *albumRepository) PurgeEmpty() error { return err } -func (r *albumRepository) GetStarred(options ...model.QueryOptions) (model.Albums, error) { +func (r *albumRepository) GetStarred(userId string, options ...model.QueryOptions) (model.Albums, error) { var starred []album - _, err := r.newQuery(options...).Filter("starred", true).All(&starred) + sq := r.newRawQuery(options...).Join("annotation").Where("annotation.item_id = " + r.tableName + ".id") + sq = sq.Where(squirrel.Eq{"annotation.user_id": userId}) + sql, args, err := sq.ToSql() + if err != nil { + return nil, err + } + _, err = r.ormer.Raw(sql, args...).QueryRows(&starred) if err != nil { return nil, err } 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().Filter("id__in", ids).Update(orm.Params{ - "starred": starred, - "starred_at": starredAt, - }) - return err -} - -func (r *albumRepository) MarkAsPlayed(id string, playDate time.Time) error { - _, err := r.newQuery().Filter("id", id).Update(orm.Params{ - "play_count": orm.ColValue(orm.ColAdd, 1), - "play_date": playDate, - }) - return err -} - func (r *albumRepository) Search(q string, offset int, size int) (model.Albums, error) { if len(q) <= 2 { return nil, nil } var results []album - err := r.doSearch(r.tableName, q, offset, size, &results, "rating desc", "starred desc", "play_count desc", "name") + err := r.doSearch(r.tableName, q, offset, size, &results, "name") if err != nil { return nil, err } diff --git a/persistence/album_repository_test.go b/persistence/album_repository_test.go index 59a37e660..0346c8093 100644 --- a/persistence/album_repository_test.go +++ b/persistence/album_repository_test.go @@ -44,7 +44,7 @@ var _ = Describe("AlbumRepository", func() { Describe("GetStarred", func() { It("returns all starred records", func() { - Expect(repo.GetStarred(model.QueryOptions{})).To(Equal(model.Albums{ + Expect(repo.GetStarred("userid", model.QueryOptions{})).To(Equal(model.Albums{ albumRadioactivity, })) }) diff --git a/persistence/annotation_repository.go b/persistence/annotation_repository.go new file mode 100644 index 000000000..8271efda3 --- /dev/null +++ b/persistence/annotation_repository.go @@ -0,0 +1,154 @@ +package persistence + +import ( + "time" + + "github.com/astaxie/beego/orm" + "github.com/cloudsonic/sonic-server/model" + "github.com/google/uuid" +) + +type annotation struct { + AnnotationID string `orm:"pk;column(ann_id)"` + UserID string `orm:"column(user_id)"` + ItemID string `orm:"column(item_id)"` + ItemType string `orm:"column(item_type)"` + PlayCount int `orm:"index;null"` + PlayDate time.Time `orm:"index;null"` + Rating int `orm:"index;null"` + Starred bool `orm:"index"` + StarredAt time.Time `orm:"null"` +} + +func (u *annotation) TableUnique() [][]string { + return [][]string{ + []string{"UserID", "ItemID", "ItemType"}, + } +} + +type annotationRepository struct { + sqlRepository +} + +func NewAnnotationRepository(o orm.Ormer) model.AnnotationRepository { + r := &annotationRepository{} + r.ormer = o + r.tableName = "annotation" + return r +} + +func (r *annotationRepository) Get(userID, itemType string, itemID string) (*model.Annotation, error) { + if userID == "" { + return nil, model.ErrInvalidAuth + } + q := r.newQuery().Filter("user_id", userID).Filter("item_type", itemType).Filter("item_id", itemID) + var ann annotation + err := q.One(&ann) + if err == orm.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + resp := model.Annotation(ann) + return &resp, nil +} + +func (r *annotationRepository) GetMap(userID, itemType string, itemID []string) (model.AnnotationMap, error) { + if userID == "" { + return nil, model.ErrInvalidAuth + } + if len(itemID) == 0 { + return nil, nil + } + q := r.newQuery().Filter("user_id", userID).Filter("item_type", itemType).Filter("item_id__in", itemID) + var res []annotation + _, err := q.All(&res) + if err != nil { + return nil, err + } + + m := make(model.AnnotationMap) + for _, a := range res { + m[a.ItemID] = model.Annotation(a) + } + return m, nil +} + +func (r *annotationRepository) new(userID, itemType string, itemID string) *annotation { + id, _ := uuid.NewRandom() + return &annotation{ + AnnotationID: id.String(), + UserID: userID, + ItemID: itemID, + ItemType: itemType, + } +} + +func (r *annotationRepository) IncPlayCount(userID, itemType string, itemID string, ts time.Time) error { + if userID == "" { + return model.ErrInvalidAuth + } + q := r.newQuery().Filter("user_id", userID).Filter("item_type", itemType).Filter("item_id", itemID) + c, err := q.Update(orm.Params{ + "play_count": orm.ColValue(orm.ColAdd, 1), + "play_date": ts, + }) + if c == 0 || err == orm.ErrNoRows { + ann := r.new(userID, itemType, itemID) + ann.PlayCount = 1 + ann.PlayDate = ts + _, err = r.ormer.Insert(ann) + } + return err +} + +func (r *annotationRepository) SetStar(starred bool, userID, itemType string, ids ...string) error { + if userID == "" { + return model.ErrInvalidAuth + } + q := r.newQuery().Filter("user_id", userID).Filter("item_type", itemType).Filter("item_id__in", ids) + var starredAt time.Time + if starred { + starredAt = time.Now() + } + c, err := q.Update(orm.Params{ + "starred": starred, + "starred_at": starredAt, + }) + if c == 0 || err == orm.ErrNoRows { + for _, id := range ids { + ann := r.new(userID, itemType, id) + ann.Starred = starred + ann.StarredAt = starredAt + _, err = r.ormer.Insert(ann) + if err != nil { + return err + } + } + } + return nil +} + +func (r *annotationRepository) SetRating(rating int, userID, itemType string, itemID string) error { + if userID == "" { + return model.ErrInvalidAuth + } + q := r.newQuery().Filter("user_id", userID).Filter("item_type", itemType).Filter("item_id", itemID) + c, err := q.Update(orm.Params{ + "rating": rating, + }) + if c == 0 || err == orm.ErrNoRows { + ann := r.new(userID, itemType, itemID) + ann.Rating = rating + _, err = r.ormer.Insert(ann) + } + return err + +} + +func (r *annotationRepository) Delete(userID, itemType string, itemID ...string) error { + q := r.newQuery().Filter("user_id", userID).Filter("item_type", itemType).Filter("item_id__in", itemID) + _, err := q.Delete() + return err +} diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index 15392ab80..08804b47a 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/Masterminds/squirrel" "github.com/astaxie/beego/orm" "github.com/cloudsonic/sonic-server/conf" "github.com/cloudsonic/sonic-server/log" @@ -14,11 +15,9 @@ import ( ) type artist struct { - 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"` + ID string `orm:"pk;column(id)"` + Name string `orm:"index"` + AlbumCount int `orm:"column(album_count)"` } type artistRepository struct { @@ -155,9 +154,15 @@ 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) { +func (r *artistRepository) GetStarred(userId string, options ...model.QueryOptions) (model.Artists, error) { var starred []artist - _, err := r.newQuery(options...).Filter("starred", true).All(&starred) + sq := r.newRawQuery(options...).Join("annotation").Where("annotation.item_id = " + r.tableName + ".id") + sq = sq.Where(squirrel.Eq{"annotation.user_id": userId}) + sql, args, err := sq.ToSql() + if err != nil { + return nil, err + } + _, err = r.ormer.Raw(sql, args...).QueryRows(&starred) if err != nil { return nil, err } diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index 39756f091..76d3f3d14 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/Masterminds/squirrel" "github.com/astaxie/beego/orm" "github.com/cloudsonic/sonic-server/model" ) @@ -28,11 +29,6 @@ type mediaFile struct { BitRate int `` Genre string `orm:"index"` Compilation bool `` - PlayCount int `orm:"index"` - PlayDate time.Time `orm:"null"` - Rating int `orm:"index"` - Starred bool `orm:"index"` - StarredAt time.Time `orm:"index;null"` CreatedAt time.Time `orm:"null"` UpdatedAt time.Time `orm:"null"` } @@ -51,6 +47,7 @@ func NewMediaFileRepository(o orm.Ormer) model.MediaFileRepository { func (r *mediaFileRepository) Put(m *model.MediaFile) error { tm := mediaFile(*m) // Don't update media annotation fields (playcount, starred, etc..) + // TODO Validate if this is still necessary, now that we don't have annotations in the mediafile model return r.put(m.ID, m.Title, &tm, "path", "title", "album", "artist", "artist_id", "album_artist", "album_id", "has_cover_art", "track_number", "disc_number", "year", "size", "suffix", "duration", "bit_rate", "genre", "compilation", "updated_at") @@ -144,53 +141,28 @@ func (r *mediaFileRepository) GetRandom(options ...model.QueryOptions) (model.Me return r.toMediaFiles(results), err } -func (r *mediaFileRepository) GetStarred(options ...model.QueryOptions) (model.MediaFiles, error) { +func (r *mediaFileRepository) GetStarred(userId string, options ...model.QueryOptions) (model.MediaFiles, error) { var starred []mediaFile - _, err := r.newQuery(options...).Filter("starred", true).All(&starred) + sq := r.newRawQuery(options...).Join("annotation").Where("annotation.item_id = " + r.tableName + ".id") + sq = sq.Where(squirrel.Eq{"annotation.user_id": userId}) + sql, args, err := sq.ToSql() + if err != nil { + return nil, err + } + _, err = r.ormer.Raw(sql, args...).QueryRows(&starred) if err != nil { return nil, err } 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().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().Filter("id__in", ids).Update(orm.Params{"rating": rating}) - return err -} - -func (r *mediaFileRepository) MarkAsPlayed(id string, playDate time.Time) error { - _, err := r.newQuery().Filter("id", id).Update(orm.Params{ - "play_count": orm.ColValue(orm.ColAdd, 1), - "play_date": playDate, - }) - return err -} - func (r *mediaFileRepository) Search(q string, offset int, size int) (model.MediaFiles, error) { if len(q) <= 2 { return nil, nil } var results []mediaFile - err := r.doSearch(r.tableName, q, offset, size, &results, "rating desc", "starred desc", "play_count desc", "title") + err := r.doSearch(r.tableName, q, offset, size, &results, "title") if err != nil { return nil, err } diff --git a/persistence/mock_persistence.go b/persistence/mock_persistence.go index 7ae65e2f8..cd2f64f84 100644 --- a/persistence/mock_persistence.go +++ b/persistence/mock_persistence.go @@ -57,6 +57,10 @@ func (db *MockDataStore) User() model.UserRepository { return db.MockedUser } +func (db *MockDataStore) Annotation() model.AnnotationRepository { + return struct{ model.AnnotationRepository }{} +} + func (db *MockDataStore) WithTx(block func(db model.DataStore) error) error { return block(db) } diff --git a/persistence/persistence.go b/persistence/persistence.go index 67b37d638..35cc1f302 100644 --- a/persistence/persistence.go +++ b/persistence/persistence.go @@ -73,6 +73,10 @@ func (db *SQLStore) User() model.UserRepository { return NewUserRepository(db.getOrmer()) } +func (db *SQLStore) Annotation() model.AnnotationRepository { + return NewAnnotationRepository(db.getOrmer()) +} + func (db *SQLStore) Resource(model interface{}) model.ResourceRepository { return NewResource(db.getOrmer(), model, getMappedModel(model)) } @@ -159,6 +163,7 @@ func init() { registerModel(model.Property{}, new(property)) registerModel(model.Playlist{}, new(playlist)) registerModel(model.User{}, new(user)) + registerModel(model.Annotation{}, new(annotation)) orm.RegisterModel(new(checksum)) orm.RegisterModel(new(search)) diff --git a/persistence/persistence_suite_test.go b/persistence/persistence_suite_test.go index 02260954a..8bd684a7b 100644 --- a/persistence/persistence_suite_test.go +++ b/persistence/persistence_suite_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/astaxie/beego/orm" "github.com/cloudsonic/sonic-server/conf" "github.com/cloudsonic/sonic-server/log" "github.com/cloudsonic/sonic-server/model" @@ -29,13 +30,18 @@ var testArtists = model.Artists{ var albumSgtPeppers = model.Album{ID: "1", Name: "Sgt Peppers", Artist: "The Beatles", ArtistID: "1", Genre: "Rock"} var albumAbbeyRoad = model.Album{ID: "2", Name: "Abbey Road", Artist: "The Beatles", ArtistID: "1", Genre: "Rock"} -var albumRadioactivity = model.Album{ID: "3", Name: "Radioactivity", Artist: "Kraftwerk", ArtistID: "2", Starred: true, Genre: "Electronic"} +var albumRadioactivity = model.Album{ID: "3", Name: "Radioactivity", Artist: "Kraftwerk", ArtistID: "2", Genre: "Electronic"} var testAlbums = model.Albums{ albumSgtPeppers, albumAbbeyRoad, albumRadioactivity, } +var annRadioactivity = model.Annotation{AnnotationID: "1", UserID: "userid", ItemType: model.AlbumItemType, ItemID: "3", Starred: true} +var testAnnotations = []model.Annotation{ + annRadioactivity, +} + var songDayInALife = model.MediaFile{ID: "1", Title: "A Day In A Life", ArtistID: "3", AlbumID: "1", Genre: "Rock", Path: P("/beatles/1/sgt/a day.mp3")} var songComeTogether = model.MediaFile{ID: "2", Title: "Come Together", ArtistID: "3", AlbumID: "2", Genre: "Rock", Path: P("/beatles/1/come together.mp3")} var songRadioactivity = model.MediaFile{ID: "3", Title: "Radioactivity", ArtistID: "2", AlbumID: "3", Genre: "Electronic", Path: P("/kraft/radio/radio.mp3")} @@ -76,5 +82,14 @@ var _ = Describe("Initialize test DB", func() { panic(err) } } + + o := orm.NewOrm() + for _, a := range testAnnotations { + ann := annotation(a) + _, err := o.Insert(&ann) + if err != nil { + panic(err) + } + } }) }) diff --git a/server/subsonic/album_lists.go b/server/subsonic/album_lists.go index f5f18924d..8de9b8416 100644 --- a/server/subsonic/album_lists.go +++ b/server/subsonic/album_lists.go @@ -1,6 +1,7 @@ package subsonic import ( + "context" "errors" "net/http" @@ -32,7 +33,7 @@ func NewAlbumListController(listGen engine.ListGenerator) *AlbumListController { return c } -type strategy func(offset int, size int) (engine.Entries, error) +type strategy func(ctx context.Context, offset int, size int) (engine.Entries, error) func (c *AlbumListController) getAlbumList(r *http.Request) (engine.Entries, error) { typ, err := RequiredParamString(r, "type", "Required string parameter 'type' is not present") @@ -49,7 +50,7 @@ func (c *AlbumListController) getAlbumList(r *http.Request) (engine.Entries, err offset := ParamInt(r, "offset", 0) size := utils.MinInt(ParamInt(r, "size", 10), 500) - albums, err := listFunc(offset, size) + albums, err := listFunc(r.Context(), offset, size) if err != nil { log.Error(r, "Error retrieving albums", "error", err) return nil, errors.New("Internal Error") @@ -81,7 +82,7 @@ func (c *AlbumListController) GetAlbumList2(w http.ResponseWriter, r *http.Reque } func (c *AlbumListController) GetStarred(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { - artists, albums, mediaFiles, err := c.listGen.GetAllStarred() + artists, albums, mediaFiles, err := c.listGen.GetAllStarred(r.Context()) if err != nil { log.Error(r, "Error retrieving starred media", "error", err) return nil, NewError(responses.ErrorGeneric, "Internal Error") @@ -96,7 +97,7 @@ func (c *AlbumListController) GetStarred(w http.ResponseWriter, r *http.Request) } func (c *AlbumListController) GetStarred2(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { - artists, albums, mediaFiles, err := c.listGen.GetAllStarred() + artists, albums, mediaFiles, err := c.listGen.GetAllStarred(r.Context()) if err != nil { log.Error(r, "Error retrieving starred media", "error", err) return nil, NewError(responses.ErrorGeneric, "Internal Error") @@ -111,7 +112,7 @@ func (c *AlbumListController) GetStarred2(w http.ResponseWriter, r *http.Request } func (c *AlbumListController) GetNowPlaying(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { - npInfos, err := c.listGen.GetNowPlaying() + npInfos, err := c.listGen.GetNowPlaying(r.Context()) if err != nil { log.Error(r, "Error retrieving now playing list", "error", err) return nil, NewError(responses.ErrorGeneric, "Internal Error") @@ -134,7 +135,7 @@ func (c *AlbumListController) GetRandomSongs(w http.ResponseWriter, r *http.Requ size := utils.MinInt(ParamInt(r, "size", 10), 500) genre := ParamString(r, "genre") - songs, err := c.listGen.GetRandomSongs(size, genre) + songs, err := c.listGen.GetRandomSongs(r.Context(), size, genre) if err != nil { log.Error(r, "Error retrieving random songs", "error", err) return nil, NewError(responses.ErrorGeneric, "Internal Error") diff --git a/server/subsonic/album_lists_test.go b/server/subsonic/album_lists_test.go index 9632c1db7..62a854ab8 100644 --- a/server/subsonic/album_lists_test.go +++ b/server/subsonic/album_lists_test.go @@ -1,6 +1,7 @@ package subsonic import ( + "context" "errors" "net/http/httptest" @@ -17,7 +18,7 @@ type fakeListGen struct { recvSize int } -func (lg *fakeListGen) GetNewest(offset int, size int) (engine.Entries, error) { +func (lg *fakeListGen) GetNewest(ctx context.Context, offset int, size int) (engine.Entries, error) { if lg.err != nil { return nil, lg.err } diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index c08962bbd..a191c5329 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -135,7 +135,7 @@ func (c *BrowsingController) GetAlbum(w http.ResponseWriter, r *http.Request) (* func (c *BrowsingController) GetSong(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { id := ParamString(r, "id") - song, err := c.browser.GetSong(id) + song, err := c.browser.GetSong(r.Context(), id) switch { case err == model.ErrNotFound: log.Error(r, "Requested ID not found ", "id", id) diff --git a/server/subsonic/media_annotation.go b/server/subsonic/media_annotation.go index 7ab0d54da..cfd36d820 100644 --- a/server/subsonic/media_annotation.go +++ b/server/subsonic/media_annotation.go @@ -1,6 +1,7 @@ package subsonic import ( + "context" "net/http" "time" @@ -47,53 +48,54 @@ func (c *MediaAnnotationController) SetRating(w http.ResponseWriter, r *http.Req return NewResponse(), nil } -func (c *MediaAnnotationController) getIds(r *http.Request) ([]string, error) { +func (c *MediaAnnotationController) Star(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { ids := ParamStrings(r, "id") albumIds := ParamStrings(r, "albumId") artistIds := ParamStrings(r, "artistId") - if len(ids)+len(albumIds)+len(artistIds) == 0 { return nil, NewError(responses.ErrorMissingParameter, "Required id parameter is missing") } - ids = append(ids, albumIds...) ids = append(ids, artistIds...) - return ids, nil -} -func (c *MediaAnnotationController) Star(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { - ids, err := c.getIds(r) + err := c.star(r.Context(), true, ids...) if err != nil { return nil, err } - log.Debug(r, "Starring items", "ids", ids) - err = c.ratings.SetStar(r.Context(), true, ids...) - switch { - case err == model.ErrNotFound: - log.Error(r, err) - return nil, NewError(responses.ErrorDataNotFound, "ID not found") - case err != nil: - log.Error(r, err) - return nil, NewError(responses.ErrorGeneric, "Internal Error") - } return NewResponse(), nil } -func (c *MediaAnnotationController) Unstar(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { - ids, err := c.getIds(r) - if err != nil { - return nil, err +func (c *MediaAnnotationController) star(ctx context.Context, starred bool, ids ...string) error { + if len(ids) == 0 { + return nil } - log.Debug(r, "Unstarring items", "ids", ids) - err = c.ratings.SetStar(r.Context(), false, ids...) + log.Debug(ctx, "Changing starred", "ids", ids, "starred", starred) + err := c.ratings.SetStar(ctx, starred, ids...) switch { case err == model.ErrNotFound: - log.Error(r, err) - return nil, NewError(responses.ErrorDataNotFound, "Directory not found") + log.Error(ctx, err) + return NewError(responses.ErrorDataNotFound, "ID not found") case err != nil: - log.Error(r, err) - return nil, NewError(responses.ErrorGeneric, "Internal Error") + log.Error(ctx, err) + return NewError(responses.ErrorGeneric, "Internal Error") + } + return nil +} + +func (c *MediaAnnotationController) Unstar(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { + ids := ParamStrings(r, "id") + albumIds := ParamStrings(r, "albumId") + artistIds := ParamStrings(r, "artistId") + if len(ids)+len(albumIds)+len(artistIds) == 0 { + return nil, NewError(responses.ErrorMissingParameter, "Required id parameter is missing") + } + ids = append(ids, albumIds...) + ids = append(ids, artistIds...) + + err := c.star(r.Context(), false, ids...) + if err != nil { + return nil, err } return NewResponse(), nil diff --git a/server/subsonic/playlists.go b/server/subsonic/playlists.go index f4f82bf39..39f050c80 100644 --- a/server/subsonic/playlists.go +++ b/server/subsonic/playlists.go @@ -45,7 +45,7 @@ func (c *PlaylistsController) GetPlaylist(w http.ResponseWriter, r *http.Request if err != nil { return nil, err } - pinfo, err := c.pls.Get(id) + pinfo, err := c.pls.Get(r.Context(), id) switch { case err == model.ErrNotFound: log.Error(r, err.Error(), "id", id) diff --git a/server/subsonic/stream.go b/server/subsonic/stream.go index f37548034..3218a5f4b 100644 --- a/server/subsonic/stream.go +++ b/server/subsonic/stream.go @@ -24,7 +24,7 @@ func (c *StreamController) getMediaFile(r *http.Request) (mf *engine.Entry, err return nil, err } - mf, err = c.browser.GetSong(id) + mf, err = c.browser.GetSong(r.Context(), id) switch { case err == model.ErrNotFound: log.Error(r, "Mediafile not found", "id", id)