arimelody.me/api/release.go

282 lines
10 KiB
Go

package api
import (
"encoding/json"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"arimelody-web/admin"
"arimelody-web/global"
music "arimelody-web/music/controller"
"arimelody-web/music/model"
)
func ServeCatalog() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
releases, err := music.GetAllReleases(global.DB, false, 0, true)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
type Release struct {
ID string `json:"id"`
Title string `json:"title"`
ReleaseType model.ReleaseType `json:"type" db:"type"`
ReleaseDate time.Time `json:"releaseDate" db:"release_date"`
Artwork string `json:"artwork"`
Buylink string `json:"buylink"`
Copyright string `json:"copyright" db:"copyright"`
}
catalog := []Release{}
authorised := admin.GetSession(r) != nil
for _, release := range releases {
if !release.Visible && !authorised {
continue
}
catalog = append(catalog, Release{
ID: release.ID,
Title: release.Title,
ReleaseType: release.ReleaseType,
ReleaseDate: release.ReleaseDate,
Artwork: release.Artwork,
Buylink: release.Buylink,
Copyright: release.Copyright,
})
}
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("FATAL: 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 {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
return
}
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.ID, trackIDs)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: 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.ID, credits)
if err != nil {
if strings.Contains(err.Error(), "duplicate key") {
http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest)
return
}
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: 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
}
err = music.UpdateReleaseLinks(global.DB, release.ID, links)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: 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 := music.DeleteRelease(global.DB, release.ID)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: Failed to delete release %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
}