arimelody-web/api/release.go

473 lines
18 KiB
Go
Raw Permalink Normal View History

package api
import (
"encoding/json"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"arimelody-web/controller"
"arimelody-web/model"
"arimelody-web/model/app"
"arimelody-web/errors"
)
func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// only allow authorised users to view hidden releases
privileged := false
if !release.Visible {
2025-04-29 16:31:39 +01:00
session, err := controller.GetSessionFromRequest(app, r)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if session != nil && session.Account != nil {
// TODO: check privilege on release
privileged = true
}
if !privileged {
http.NotFound(w, r)
return
}
}
type (
TrackDTO struct {
Title string `json:"title"`
Description string `json:"description"`
Lyrics string `json:"lyrics"`
}
CreditDTO struct {
*model.Artist
Role string `json:"role"`
Primary bool `json:"primary"`
}
ReleaseDTO struct {
*model.Release
Tracks []TrackDTO `json:"tracks"`
Credits []CreditDTO `json:"credits"`
Links map[string]string `json:"links"`
}
)
response := ReleaseDTO{
Release: release,
Tracks: []TrackDTO{},
Credits: []CreditDTO{},
Links: make(map[string]string),
}
if release.IsReleased() || privileged {
// get credits
credits, err := app.MusicService.GetReleaseCredits(release.ID)
if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Credits: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
for _, credit := range credits {
artist, err := app.MusicService.GetArtistByID(credit.Artist.ID)
if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Artists: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
response.Credits = append(response.Credits, CreditDTO{
Artist: artist,
Role: credit.Role,
Primary: credit.Primary,
})
}
// get tracks
tracks, err := app.MusicService.GetReleaseTracks(release.ID)
if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Tracks: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
for _, track := range tracks {
response.Tracks = append(response.Tracks, TrackDTO{
Title: track.Title,
Description: track.Description,
Lyrics: track.Lyrics,
})
}
// get links
links, err := app.MusicService.GetReleaseLinks(release.ID)
if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Links: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
for _, link := range links {
response.Links[link.Name] = link.URL
}
}
w.Header().Add("Content-Type", "application/json")
2024-12-02 12:47:42 +00:00
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err := encoder.Encode(response)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
})
}
func ServeCatalog(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
releases, err := app.MusicService.GetAllFullReleases(false, 0)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
2024-09-03 08:07:45 +01:00
type Release struct {
ID string `json:"id"`
Title string `json:"title"`
2024-09-12 09:46:40 +01:00
Artists []string `json:"artists"`
2024-09-03 08:07:45 +01:00
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{}
session := r.Context().Value("session").(*model.Session)
for _, release := range releases {
if !release.Visible {
privileged := false
if session != nil && session.Account != nil {
// TODO: check privilege on release
privileged = true
}
if !privileged {
continue
}
}
2024-09-12 09:46:40 +01:00
artists := []string{}
for _, credit := range release.Credits {
if !credit.Primary { continue }
artists = append(artists, credit.Artist.Name)
}
2024-09-03 08:07:45 +01:00
catalog = append(catalog, Release{
ID: release.ID,
Title: release.Title,
2024-09-12 09:46:40 +01:00
Artists: artists,
ReleaseType: release.ReleaseType,
ReleaseDate: release.ReleaseDate,
Artwork: release.Artwork,
Buylink: release.Buylink,
2024-09-03 08:07:45 +01:00
Copyright: release.Copyright,
})
}
w.Header().Add("Content-Type", "application/json")
2024-12-02 12:47:42 +00:00
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(catalog)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
})
}
func CreateRelease(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2025-02-07 16:40:58 +00:00
session := r.Context().Value("session").(*model.Session)
type CreateReleaseDTO struct {
ID string `json:"id"`
Title string `json:"title"`
ReleaseType string `json:"type"`
ReleaseDate time.Time `json:"release_date"`
Artwork string `json:"artwork"`
}
var dto CreateReleaseDTO
err := json.NewDecoder(r.Body).Decode(&dto)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
if dto.ReleaseType == "" { dto.ReleaseType = string(model.Single) }
if dto.ReleaseDate != time.Unix(0, 0) {
dto.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
}
if dto.Artwork == "" { dto.Artwork = model.DEFAULT_RELEASE_ARTWORK_URL }
err = app.MusicService.CreateRelease(
dto.ID,
dto.Title,
dto.ReleaseType,
dto.ReleaseDate,
dto.Artwork,
)
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("Release %s already exists\n", dto.ID), http.StatusBadRequest)
return
}
fmt.Printf("WARN: Failed to create release %s: %s\n", dto.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" created by \"%s\".", dto.ID, session.Account.Username)
2025-02-07 16:40:58 +00:00
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
2024-12-02 12:47:42 +00:00
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(dto)
if err != nil {
fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", dto.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
}
func UpdateRelease(app *app.AppState, release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2025-02-07 16:40:58 +00:00
session := r.Context().Value("session").(*model.Session)
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(app, release).ServeHTTP(w, r)
case "credits":
UpdateReleaseCredits(app, release).ServeHTTP(w, r)
case "links":
UpdateReleaseLinks(app, release).ServeHTTP(w, r)
}
return
}
if len(segments) > 2 {
http.NotFound(w, r)
return
}
2024-09-02 00:15:23 +01:00
err := json.NewDecoder(r.Body).Decode(&release)
if err != nil {
2024-09-02 00:15:23 +01:00
fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
2024-09-02 00:15:23 +01:00
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(app, &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 = app.MusicService.UpdateRelease(release)
if err != nil {
if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if errors.IsNotExistError(err) {
2024-09-03 08:07:45 +01:00
http.NotFound(w, r)
return
}
fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
2025-02-07 16:40:58 +00:00
2026-07-31 10:02:34 +01:00
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
})
}
func UpdateReleaseTracks(app *app.AppState, release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2025-02-07 16:40:58 +00:00
session := r.Context().Value("session").(*model.Session)
var newTrackIDs = []string{}
err := json.NewDecoder(r.Body).Decode(&newTrackIDs)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
err = app.MusicService.UpdateReleaseTracks(release.ID, newTrackIDs)
if err != nil {
if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if errors.IsNotExistError(err) {
2024-09-03 08:07:45 +01:00
http.NotFound(w, r)
return
}
if strings.Contains(err.Error(), "duplicate key") {
http.Error(w, "Release cannot have duplicate tracks", http.StatusBadRequest)
return
}
fmt.Printf("WARN: Failed to update tracks for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
2025-02-07 16:40:58 +00:00
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" tracklist updated by \"%s\".", release.ID, session.Account.Username)
})
}
func UpdateReleaseCredits(app *app.AppState, release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2025-02-07 16:40:58 +00:00
session := r.Context().Value("session").(*model.Session)
type CreditDTO struct {
Artist string
Role string
Primary bool
}
var dto []CreditDTO
err := json.NewDecoder(r.Body).Decode(&dto)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
var newCredits []*model.Credit
for _, credit := range dto {
newCredits = append(newCredits, &model.Credit{
Artist: &model.Artist{
2024-09-02 00:15:23 +01:00
ID: credit.Artist,
},
Role: credit.Role,
Primary: credit.Primary,
2024-09-02 00:15:23 +01:00
})
}
err = app.MusicService.UpdateReleaseCredits(release.ID, newCredits)
2024-09-02 00:15:23 +01:00
if err != nil {
if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if errors.IsNotExistError(err) {
2024-09-03 08:07:45 +01:00
http.NotFound(w, r)
return
}
if strings.Contains(err.Error(), "duplicate key") {
http.Error(w, "Artists may only be credited once", http.StatusBadRequest)
return
}
fmt.Printf("WARN: Failed to update credits for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
2025-02-07 16:40:58 +00:00
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" credits updated by \"%s\".", release.ID, session.Account.Username)
})
}
func UpdateReleaseLinks(app *app.AppState, release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2025-02-07 16:40:58 +00:00
session := r.Context().Value("session").(*model.Session)
type LinkDTO struct {
Name string `json:"name"`
URL string `json:"url"`
}
var dto = []LinkDTO{}
err := json.NewDecoder(r.Body).Decode(&dto)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
links := []*model.Link{}
for _, link := range dto {
links = append(links, &model.Link{ Name: link.Name, URL: link.URL })
}
err = app.MusicService.UpdateReleaseLinks(release.ID, links)
if err != nil {
if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if errors.IsNotExistError(err) {
2024-09-03 08:07:45 +01:00
http.NotFound(w, r)
return
}
if strings.Contains(err.Error(), "duplicate key") {
http.Error(w, "Release cannot have duplicate link names", http.StatusBadRequest)
return
}
fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
2025-02-07 16:40:58 +00:00
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" links updated by \"%s\".", release.ID, session.Account.Username)
})
}
func DeleteRelease(app *app.AppState, release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2025-02-07 16:40:58 +00:00
session := r.Context().Value("session").(*model.Session)
err := app.MusicService.DeleteRelease(release.ID)
if err != nil {
if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if errors.IsNotExistError(err) {
2024-09-03 08:07:45 +01:00
http.NotFound(w, r)
return
}
fmt.Printf("WARN: Failed to delete release %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
2025-02-07 16:40:58 +00:00
2026-07-31 10:02:34 +01:00
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" deleted by \"%s\".", release.ID, session.Account.Username)
})
}