1package subsonic 2 3import ( 4 "context" 5 "fmt" 6 "net/http" 7 "time" 8 9 "github.com/navidrome/navidrome/core" 10 "github.com/navidrome/navidrome/log" 11 "github.com/navidrome/navidrome/model" 12 "github.com/navidrome/navidrome/model/request" 13 "github.com/navidrome/navidrome/server/subsonic/responses" 14 "github.com/navidrome/navidrome/utils" 15) 16 17type MediaAnnotationController struct { 18 ds model.DataStore 19 npRepo core.NowPlaying 20} 21 22func NewMediaAnnotationController(ds model.DataStore, npr core.NowPlaying) *MediaAnnotationController { 23 return &MediaAnnotationController{ds: ds, npRepo: npr} 24} 25 26func (c *MediaAnnotationController) SetRating(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { 27 id, err := requiredParamString(r, "id") 28 if err != nil { 29 return nil, err 30 } 31 rating, err := requiredParamInt(r, "rating") 32 if err != nil { 33 return nil, err 34 } 35 36 log.Debug(r, "Setting rating", "rating", rating, "id", id) 37 err = c.setRating(r.Context(), id, rating) 38 39 switch { 40 case err == model.ErrNotFound: 41 log.Error(r, err) 42 return nil, newError(responses.ErrorDataNotFound, "ID not found") 43 case err != nil: 44 log.Error(r, err) 45 return nil, err 46 } 47 48 return newResponse(), nil 49} 50 51func (c *MediaAnnotationController) setRating(ctx context.Context, id string, rating int) error { 52 exist, err := c.ds.Album(ctx).Exists(id) 53 if err != nil { 54 return err 55 } 56 if exist { 57 return c.ds.Album(ctx).SetRating(rating, id) 58 } 59 return c.ds.MediaFile(ctx).SetRating(rating, id) 60} 61 62func (c *MediaAnnotationController) Star(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { 63 ids := utils.ParamStrings(r, "id") 64 albumIds := utils.ParamStrings(r, "albumId") 65 artistIds := utils.ParamStrings(r, "artistId") 66 if len(ids)+len(albumIds)+len(artistIds) == 0 { 67 return nil, newError(responses.ErrorMissingParameter, "Required id parameter is missing") 68 } 69 ids = append(ids, albumIds...) 70 ids = append(ids, artistIds...) 71 72 err := c.setStar(r.Context(), true, ids...) 73 if err != nil { 74 return nil, err 75 } 76 77 return newResponse(), nil 78} 79 80func (c *MediaAnnotationController) Unstar(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { 81 ids := utils.ParamStrings(r, "id") 82 albumIds := utils.ParamStrings(r, "albumId") 83 artistIds := utils.ParamStrings(r, "artistId") 84 if len(ids)+len(albumIds)+len(artistIds) == 0 { 85 return nil, newError(responses.ErrorMissingParameter, "Required id parameter is missing") 86 } 87 ids = append(ids, albumIds...) 88 ids = append(ids, artistIds...) 89 90 err := c.setStar(r.Context(), false, ids...) 91 if err != nil { 92 return nil, err 93 } 94 95 return newResponse(), nil 96} 97 98func (c *MediaAnnotationController) Scrobble(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { 99 ids, err := requiredParamStrings(r, "id") 100 if err != nil { 101 return nil, err 102 } 103 times := utils.ParamTimes(r, "time") 104 if len(times) > 0 && len(times) != len(ids) { 105 return nil, newError(responses.ErrorGeneric, "Wrong number of timestamps: %d, should be %d", len(times), len(ids)) 106 } 107 submission := utils.ParamBool(r, "submission", true) 108 playerId := 1 // TODO Multiple players, based on playerName/username/clientIP(?) 109 playerName := utils.ParamString(r, "c") 110 username := utils.ParamString(r, "u") 111 112 log.Debug(r, "Scrobbling tracks", "ids", ids, "times", times, "submission", submission) 113 for i, id := range ids { 114 var t time.Time 115 if len(times) > 0 { 116 t = times[i] 117 } else { 118 t = time.Now() 119 } 120 if submission { 121 _, err := c.scrobblerRegister(r.Context(), playerId, id, t) 122 if err != nil { 123 log.Error(r, "Error scrobbling track", "id", id, err) 124 continue 125 } 126 } else { 127 _, err := c.scrobblerNowPlaying(r.Context(), playerId, playerName, id, username) 128 if err != nil { 129 log.Error(r, "Error setting current song", "id", id, err) 130 continue 131 } 132 } 133 } 134 return newResponse(), nil 135} 136 137func (c *MediaAnnotationController) scrobblerRegister(ctx context.Context, playerId int, trackId string, playTime time.Time) (*model.MediaFile, error) { 138 var mf *model.MediaFile 139 var err error 140 err = c.ds.WithTx(func(tx model.DataStore) error { 141 mf, err = c.ds.MediaFile(ctx).Get(trackId) 142 if err != nil { 143 return err 144 } 145 err = c.ds.MediaFile(ctx).IncPlayCount(trackId, playTime) 146 if err != nil { 147 return err 148 } 149 err = c.ds.Album(ctx).IncPlayCount(mf.AlbumID, playTime) 150 if err != nil { 151 return err 152 } 153 err = c.ds.Artist(ctx).IncPlayCount(mf.ArtistID, playTime) 154 return err 155 }) 156 157 username, _ := request.UsernameFrom(ctx) 158 if err != nil { 159 log.Error("Error while scrobbling", "trackId", trackId, "user", username, err) 160 } else { 161 log.Info("Scrobbled", "title", mf.Title, "artist", mf.Artist, "user", username) 162 } 163 164 return mf, err 165} 166 167func (c *MediaAnnotationController) scrobblerNowPlaying(ctx context.Context, playerId int, playerName, trackId, username string) (*model.MediaFile, error) { 168 mf, err := c.ds.MediaFile(ctx).Get(trackId) 169 if err != nil { 170 return nil, err 171 } 172 173 if mf == nil { 174 return nil, fmt.Errorf(`ID "%s" not found`, trackId) 175 } 176 177 log.Info("Now Playing", "title", mf.Title, "artist", mf.Artist, "user", username) 178 179 info := &core.NowPlayingInfo{TrackID: trackId, Username: username, Start: time.Now(), PlayerId: playerId, PlayerName: playerName} 180 return mf, c.npRepo.Enqueue(info) 181} 182 183func (c *MediaAnnotationController) setStar(ctx context.Context, star bool, ids ...string) error { 184 if len(ids) == 0 { 185 return nil 186 } 187 log.Debug(ctx, "Changing starred", "ids", ids, "starred", star) 188 if len(ids) == 0 { 189 log.Warn(ctx, "Cannot star/unstar an empty list of ids") 190 return nil 191 } 192 193 err := c.ds.WithTx(func(tx model.DataStore) error { 194 for _, id := range ids { 195 exist, err := tx.Album(ctx).Exists(id) 196 if err != nil { 197 return err 198 } 199 if exist { 200 err = tx.Album(ctx).SetStar(star, ids...) 201 if err != nil { 202 return err 203 } 204 continue 205 } 206 exist, err = tx.Artist(ctx).Exists(id) 207 if err != nil { 208 return err 209 } 210 if exist { 211 err = tx.Artist(ctx).SetStar(star, ids...) 212 if err != nil { 213 return err 214 } 215 continue 216 } 217 err = tx.MediaFile(ctx).SetStar(star, ids...) 218 if err != nil { 219 return err 220 } 221 } 222 return nil 223 }) 224 225 switch { 226 case err == model.ErrNotFound: 227 log.Error(ctx, err) 228 return newError(responses.ErrorDataNotFound, "ID not found") 229 case err != nil: 230 log.Error(ctx, err) 231 return err 232 } 233 return nil 234} 235