package api import ( "encoding/json" "fmt" "io/fs" "net/http" "os" "path/filepath" "strings" "time" "arimelody.me/arimelody.me/admin" "arimelody.me/arimelody.me/global" music "arimelody.me/arimelody.me/music/controller" "arimelody.me/arimelody.me/music/model" ) func ServeCatalog() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { releases := []*model.Release{} err := global.DB.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } catalog := []model.ReleaseShorthand{} authorised := admin.GetSession(r) != nil for _, release := range releases { if !release.Visible && !authorised { continue } catalog = append(catalog, model.ReleaseShorthand{ ID: release.ID, Title: release.Title, ReleaseType: release.ReleaseType, ReleaseDate: release.ReleaseDate, Artwork: release.Artwork, Buylink: release.Buylink, }) } w.Header().Add("Content-Type", "application/json") err = json.NewEncoder(w).Encode(catalog) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } }) } func CreateRelease() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) return } var release model.Release err := json.NewDecoder(r.Body).Decode(&release) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if release.ID == "" { http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest) return } if release.Title == "" { release.Title = release.ID } if release.ReleaseType == "" { release.ReleaseType = model.Single } if release.ReleaseDate != time.Unix(0, 0) { release.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC) } if release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" } err = music.CreateRelease(global.DB, &release) if err != nil { if strings.Contains(err.Error(), "duplicate key") { http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest) return } fmt.Printf("Failed to create release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) err = json.NewEncoder(w).Encode(release) if err != nil { fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } func UpdateRelease(release model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { http.NotFound(w, r) return } segments := strings.Split(r.URL.Path[1:], "/") if len(segments) == 2 { switch segments[1] { case "tracks": UpdateReleaseTracks(release).ServeHTTP(w, r) case "credits": UpdateReleaseCredits(release).ServeHTTP(w, r) case "links": UpdateReleaseLinks(release).ServeHTTP(w, r) } return } if len(segments) > 2 { http.NotFound(w, r) return } err := json.NewDecoder(r.Body).Decode(&release) if err != nil { fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" } else { if strings.Contains(release.Artwork, ";base64,") { var artworkDirectory = filepath.Join("uploads", "musicart") filename, err := HandleImageUpload(&release.Artwork, artworkDirectory, release.ID) // clean up files with this ID and different extensions err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error { if path == filepath.Join(artworkDirectory, filename) { return nil } withoutExt := strings.TrimSuffix(path, filepath.Ext(path)) if withoutExt != filepath.Join(artworkDirectory, release.ID) { return nil } return os.Remove(path) }) if err != nil { fmt.Printf("WARN: Error while cleaning up artwork files: %s\n", err) } release.Artwork = fmt.Sprintf("/uploads/musicart/%s", filename) } } err = music.UpdateRelease(global.DB, &release) if err != nil { fmt.Printf("FATAL: Failed to update release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } func UpdateReleaseTracks(release model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var trackIDs = []string{} err := json.NewDecoder(r.Body).Decode(&trackIDs) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } err = music.UpdateReleaseTracks(global.DB, &release, trackIDs) if err != nil { fmt.Printf("Failed to update tracks for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } func UpdateReleaseCredits(release model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { type creditJSON struct { Artist string Role string Primary bool } var data []creditJSON err := json.NewDecoder(r.Body).Decode(&data) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } var credits []model.Credit for _, credit := range data { credits = append(credits, model.Credit{ Artist: model.Artist{ ID: credit.Artist, }, Role: credit.Role, Primary: credit.Primary, }) } err = music.UpdateReleaseCredits(global.DB, &release, credits) if err != nil { if strings.Contains(err.Error(), "duplicate key") { http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest) return } fmt.Printf("Failed to update links for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } func UpdateReleaseLinks(release model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut { http.NotFound(w, r) return } var links = []*model.Link{} err := json.NewDecoder(r.Body).Decode(&links) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } tx := global.DB.MustBegin() tx.MustExec("DELETE FROM musiclink WHERE release=$1", release.ID) for _, link := range links { tx.MustExec( "INSERT INTO musiclink "+ "(release, name, url) "+ "VALUES ($1, $2, $3)", release.ID, link.Name, link.URL) } err = tx.Commit() if err != nil { fmt.Printf("Failed to update links for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } func DeleteRelease(release model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := global.DB.Exec("DELETE FROM musicrelease WHERE id=$1", release.ID) if err != nil { fmt.Printf("Failed to delete release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) }