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)