home to your local SPACEGIRL 馃挮 arimelody.space
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 425 lines 16 kB view raw
1package api 2 3import ( 4 "encoding/json" 5 "fmt" 6 "io/fs" 7 "net/http" 8 "os" 9 "path/filepath" 10 "strings" 11 "time" 12 13 "arimelody-web/controller" 14 "arimelody-web/log" 15 "arimelody-web/model" 16) 17 18func ServeRelease(app *model.AppState, release *model.Release) http.Handler { 19 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 // only allow authorised users to view hidden releases 21 privileged := false 22 if !release.Visible { 23 session, err := controller.GetSessionFromRequest(app, r) 24 if err != nil { 25 fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err) 26 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 27 return 28 } 29 30 if session != nil && session.Account != nil { 31 // TODO: check privilege on release 32 privileged = true 33 } 34 35 if !privileged { 36 http.NotFound(w, r) 37 return 38 } 39 } 40 41 type ( 42 Track struct { 43 Title string `json:"title"` 44 Description string `json:"description"` 45 Lyrics string `json:"lyrics"` 46 } 47 48 Credit struct { 49 *model.Artist 50 Role string `json:"role"` 51 Primary bool `json:"primary"` 52 } 53 54 Release struct { 55 *model.Release 56 Tracks []Track `json:"tracks"` 57 Credits []Credit `json:"credits"` 58 Links map[string]string `json:"links"` 59 } 60 ) 61 62 response := Release{ 63 Release: release, 64 Tracks: []Track{}, 65 Credits: []Credit{}, 66 Links: make(map[string]string), 67 } 68 69 if release.IsReleased() || privileged { 70 // get credits 71 credits, err := controller.GetReleaseCredits(app.DB, release.ID) 72 if err != nil { 73 fmt.Printf("WARN: Failed to serve release %s: Credits: %s\n", release.ID, err) 74 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 75 return 76 } 77 for _, credit := range credits { 78 artist, err := controller.GetArtist(app.DB, credit.Artist.ID) 79 if err != nil { 80 fmt.Printf("WARN: Failed to serve release %s: Artists: %s\n", release.ID, err) 81 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 82 return 83 } 84 85 response.Credits = append(response.Credits, Credit{ 86 Artist: artist, 87 Role: credit.Role, 88 Primary: credit.Primary, 89 }) 90 } 91 92 // get tracks 93 tracks, err := controller.GetReleaseTracks(app.DB, release.ID) 94 if err != nil { 95 fmt.Printf("WARN: Failed to serve release %s: Tracks: %s\n", release.ID, err) 96 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 97 return 98 } 99 for _, track := range tracks { 100 response.Tracks = append(response.Tracks, Track{ 101 Title: track.Title, 102 Description: track.Description, 103 Lyrics: track.Lyrics, 104 }) 105 } 106 107 // get links 108 links, err := controller.GetReleaseLinks(app.DB, release.ID) 109 if err != nil { 110 fmt.Printf("WARN: Failed to serve release %s: Links: %s\n", release.ID, err) 111 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 112 return 113 } 114 for _, link := range links { 115 response.Links[link.Name] = link.URL 116 } 117 } 118 119 w.Header().Add("Content-Type", "application/json") 120 encoder := json.NewEncoder(w) 121 encoder.SetIndent("", "\t") 122 err := encoder.Encode(response) 123 if err != nil { 124 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 125 return 126 } 127 }) 128} 129 130func ServeCatalog(app *model.AppState) http.Handler { 131 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 132 releases, err := controller.GetAllReleases(app.DB, false, 0, true) 133 if err != nil { 134 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 135 return 136 } 137 138 type Release struct { 139 ID string `json:"id"` 140 Title string `json:"title"` 141 Artists []string `json:"artists"` 142 ReleaseType model.ReleaseType `json:"type" db:"type"` 143 ReleaseDate time.Time `json:"releaseDate" db:"release_date"` 144 Artwork string `json:"artwork"` 145 Buylink string `json:"buylink"` 146 Copyright string `json:"copyright" db:"copyright"` 147 } 148 149 catalog := []Release{} 150 session := r.Context().Value("session").(*model.Session) 151 for _, release := range releases { 152 if !release.Visible { 153 privileged := false 154 if session != nil && session.Account != nil { 155 // TODO: check privilege on release 156 privileged = true 157 } 158 if !privileged { 159 continue 160 } 161 } 162 163 artists := []string{} 164 for _, credit := range release.Credits { 165 if !credit.Primary { continue } 166 artists = append(artists, credit.Artist.Name) 167 } 168 catalog = append(catalog, Release{ 169 ID: release.ID, 170 Title: release.Title, 171 Artists: artists, 172 ReleaseType: release.ReleaseType, 173 ReleaseDate: release.ReleaseDate, 174 Artwork: release.Artwork, 175 Buylink: release.Buylink, 176 Copyright: release.Copyright, 177 }) 178 } 179 180 w.Header().Add("Content-Type", "application/json") 181 encoder := json.NewEncoder(w) 182 encoder.SetIndent("", "\t") 183 err = encoder.Encode(catalog) 184 if err != nil { 185 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 186 return 187 } 188 }) 189} 190 191func CreateRelease(app *model.AppState) http.Handler { 192 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 193 session := r.Context().Value("session").(*model.Session) 194 195 var release model.Release 196 err := json.NewDecoder(r.Body).Decode(&release) 197 if err != nil { 198 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 199 return 200 } 201 202 if release.ID == "" { 203 http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest) 204 return 205 } 206 207 if release.Title == "" { release.Title = release.ID } 208 if release.ReleaseType == "" { release.ReleaseType = model.Single } 209 210 if release.ReleaseDate != time.Unix(0, 0) { 211 release.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC) 212 } 213 214 if release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" } 215 216 err = controller.CreateRelease(app.DB, &release) 217 if err != nil { 218 if strings.Contains(err.Error(), "duplicate key") { 219 http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest) 220 return 221 } 222 fmt.Printf("WARN: Failed to create release %s: %s\n", release.ID, err) 223 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 224 return 225 } 226 227 app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" created by \"%s\".", release.ID, session.Account.Username) 228 229 w.Header().Add("Content-Type", "application/json") 230 w.WriteHeader(http.StatusCreated) 231 encoder := json.NewEncoder(w) 232 encoder.SetIndent("", "\t") 233 err = encoder.Encode(release) 234 if err != nil { 235 fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", release.ID, err) 236 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 237 } 238 }) 239} 240 241func UpdateRelease(app *model.AppState, release *model.Release) http.Handler { 242 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 243 session := r.Context().Value("session").(*model.Session) 244 245 if r.URL.Path == "/" { 246 http.NotFound(w, r) 247 return 248 } 249 250 segments := strings.Split(r.URL.Path[1:], "/") 251 252 if len(segments) == 2 { 253 switch segments[1] { 254 case "tracks": 255 UpdateReleaseTracks(app, release).ServeHTTP(w, r) 256 case "credits": 257 UpdateReleaseCredits(app, release).ServeHTTP(w, r) 258 case "links": 259 UpdateReleaseLinks(app, release).ServeHTTP(w, r) 260 } 261 return 262 } 263 264 if len(segments) > 2 { 265 http.NotFound(w, r) 266 return 267 } 268 269 err := json.NewDecoder(r.Body).Decode(&release) 270 if err != nil { 271 fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err) 272 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 273 return 274 } 275 276 if release.Artwork == "" { 277 release.Artwork = "/img/default-cover-art.png" 278 } else { 279 if strings.Contains(release.Artwork, ";base64,") { 280 var artworkDirectory = filepath.Join("uploads", "musicart") 281 filename, err := HandleImageUpload(app, &release.Artwork, artworkDirectory, release.ID) 282 283 // clean up files with this ID and different extensions 284 err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error { 285 if path == filepath.Join(artworkDirectory, filename) { return nil } 286 287 withoutExt := strings.TrimSuffix(path, filepath.Ext(path)) 288 if withoutExt != filepath.Join(artworkDirectory, release.ID) { return nil } 289 290 return os.Remove(path) 291 }) 292 if err != nil { 293 fmt.Printf("WARN: Error while cleaning up artwork files: %s\n", err) 294 } 295 296 release.Artwork = fmt.Sprintf("/uploads/musicart/%s", filename) 297 } 298 } 299 300 err = controller.UpdateRelease(app.DB, release) 301 if err != nil { 302 if strings.Contains(err.Error(), "no rows") { 303 http.NotFound(w, r) 304 return 305 } 306 fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err) 307 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 308 } 309 310 app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) 311 }) 312} 313 314func UpdateReleaseTracks(app *model.AppState, release *model.Release) http.Handler { 315 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 316 session := r.Context().Value("session").(*model.Session) 317 318 var trackIDs = []string{} 319 err := json.NewDecoder(r.Body).Decode(&trackIDs) 320 if err != nil { 321 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 322 return 323 } 324 325 err = controller.UpdateReleaseTracks(app.DB, release.ID, trackIDs) 326 if err != nil { 327 if strings.Contains(err.Error(), "no rows") { 328 http.NotFound(w, r) 329 return 330 } 331 fmt.Printf("WARN: Failed to update tracks for %s: %s\n", release.ID, err) 332 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 333 } 334 335 app.Log.Info(log.TYPE_MUSIC, "Tracklist for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) 336 }) 337} 338 339func UpdateReleaseCredits(app *model.AppState, release *model.Release) http.Handler { 340 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 341 session := r.Context().Value("session").(*model.Session) 342 343 type creditJSON struct { 344 Artist string 345 Role string 346 Primary bool 347 } 348 var data []creditJSON 349 err := json.NewDecoder(r.Body).Decode(&data) 350 if err != nil { 351 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 352 return 353 } 354 355 var credits []*model.Credit 356 for _, credit := range data { 357 credits = append(credits, &model.Credit{ 358 Artist: model.Artist{ 359 ID: credit.Artist, 360 }, 361 Role: credit.Role, 362 Primary: credit.Primary, 363 }) 364 } 365 366 err = controller.UpdateReleaseCredits(app.DB, release.ID, credits) 367 if err != nil { 368 if strings.Contains(err.Error(), "duplicate key") { 369 http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest) 370 return 371 } 372 if strings.Contains(err.Error(), "no rows") { 373 http.NotFound(w, r) 374 return 375 } 376 fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err) 377 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 378 } 379 380 app.Log.Info(log.TYPE_MUSIC, "Credits for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) 381 }) 382} 383 384func UpdateReleaseLinks(app *model.AppState, release *model.Release) http.Handler { 385 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 386 session := r.Context().Value("session").(*model.Session) 387 388 var links = []*model.Link{} 389 err := json.NewDecoder(r.Body).Decode(&links) 390 if err != nil { 391 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 392 return 393 } 394 395 err = controller.UpdateReleaseLinks(app.DB, release.ID, links) 396 if err != nil { 397 if strings.Contains(err.Error(), "no rows") { 398 http.NotFound(w, r) 399 return 400 } 401 fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err) 402 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 403 } 404 405 app.Log.Info(log.TYPE_MUSIC, "Links for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) 406 }) 407} 408 409func DeleteRelease(app *model.AppState, release *model.Release) http.Handler { 410 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 411 session := r.Context().Value("session").(*model.Session) 412 413 err := controller.DeleteRelease(app.DB, release.ID) 414 if err != nil { 415 if strings.Contains(err.Error(), "no rows") { 416 http.NotFound(w, r) 417 return 418 } 419 fmt.Printf("WARN: Failed to delete release %s: %s\n", release.ID, err) 420 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 421 } 422 423 app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" deleted by \"%s\".", release.ID, session.Account.Username) 424 }) 425}