package api import ( "encoding/json" "fmt" "io/fs" "net/http" "os" "path/filepath" "strings" "time" "arimelody-web/model" "arimelody-web/model/app" "arimelody-web/errors" ) func ServeAllArtists(app *app.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var artists = []*model.Artist{} artists, err := app.MusicService.GetAllArtists() if err != nil { fmt.Printf("WARN: Failed to serve all artists: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } w.Header().Add("Content-Type", "application/json") encoder := json.NewEncoder(w) encoder.SetIndent("", "\t") err = encoder.Encode(artists) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } func ServeArtist(app *app.AppState, artist *model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { type ( creditJSON struct { ID string `json:"id"` Title string `json:"title"` ReleaseDate time.Time `json:"releaseDate" db:"release_date"` Artwork string `json:"artwork"` Role string `json:"role"` Primary bool `json:"primary"` } artistJSON struct { *model.Artist Credits map[string]creditJSON `json:"credits"` } ) session := r.Context().Value("session").(*model.Session) showHiddenReleases := session != nil && session.Account != nil dbCredits, err := app.MusicService.GetArtistCredits(artist.ID, showHiddenReleases) if err != nil { fmt.Printf("WARN: Failed to retrieve artist credits for %s: %v\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } var credits = map[string]creditJSON{} for _, credit := range dbCredits { credits[credit.Release.ID] = creditJSON{ ID: credit.Release.ID, Title: credit.Release.Title, ReleaseDate: credit.Release.ReleaseDate, Artwork: credit.Release.Artwork, Role: credit.Role, Primary: credit.Primary, } } w.Header().Add("Content-Type", "application/json") encoder := json.NewEncoder(w) encoder.SetIndent("", "\t") err = encoder.Encode(artistJSON{ Artist: artist, Credits: credits, }) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } func CreateArtist(app *app.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*model.Session) type CreateArtistDTO struct { ID string `json:"id"` Name string `json:"name"` } dto := &CreateArtistDTO{} err := json.NewDecoder(r.Body).Decode(dto) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } err = app.MusicService.CreateArtist(dto.ID, dto.Name, "", "") if err != nil { if errors.IsValidationError(err) { http.Error(w, err.Error(), http.StatusBadRequest) return } if strings.Contains(err.Error(), "duplicate key") { http.Error(w, fmt.Sprintf("Artist %s already exists\n", dto.ID), http.StatusBadRequest) return } fmt.Printf("WARN: Failed to create artist %s: %s\n", dto.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } app.LogService.Info(model.LOG_ARTIST, "Artist \"%s\" created by \"%s\".", dto.Name, session.Account.Username) w.WriteHeader(http.StatusCreated) }) } func UpdateArtist(app *app.AppState, artist *model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*model.Session) err := json.NewDecoder(r.Body).Decode(&artist) if err != nil { fmt.Printf("WARN: Failed to update artist: %s\n", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if artist.Avatar == "" { artist.Avatar = "/img/default-avatar.png" } else { if strings.Contains(artist.Avatar, ";base64,") { var artworkDirectory = filepath.Join("uploads", "avatar") filename, err := HandleImageUpload(app, &artist.Avatar, artworkDirectory, artist.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, artist.ID) { return nil } return os.Remove(path) }) if err != nil { fmt.Printf("WARN: Error while cleaning up avatar files: %s\n", err) } artist.Avatar = fmt.Sprintf("/uploads/avatar/%s", filename) } } err = app.MusicService.UpdateArtist(artist) if err != nil { if errors.IsValidationError(err) { http.Error(w, err.Error(), http.StatusBadRequest) return } if errors.IsNotExistError(err) { http.NotFound(w, r) return } fmt.Printf("WARN: Failed to update artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } app.LogService.Info(model.LOG_ARTIST, "Artist \"%s\" updated by \"%s\".", artist.Name, session.Account.Username) }) } func DeleteArtist(app *app.AppState, artist *model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*model.Session) err := app.MusicService.DeleteArtist(artist.ID) if err != nil { if errors.IsValidationError(err) { http.Error(w, err.Error(), http.StatusBadRequest) return } if errors.IsNotExistError(err) { http.NotFound(w, r) return } fmt.Printf("WARN: Failed to delete artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } app.LogService.Info(model.LOG_ARTIST, "Artist \"%s\" deleted by \"%s\".", artist.Name, session.Account.Username) }) }