my god...it's finally done

This commit is contained in:
ari melody 2024-09-03 08:07:45 +01:00
parent 2baf71214e
commit 19d76ebc47
Signed by: ari
GPG key ID: CF99829C92678188
43 changed files with 1008 additions and 550 deletions

View file

@ -5,10 +5,10 @@ import (
"net/http"
"strings"
"arimelody.me/arimelody.me/admin"
"arimelody.me/arimelody.me/global"
"arimelody.me/arimelody.me/music/model"
music "arimelody.me/arimelody.me/music/view"
"arimelody-web/admin"
"arimelody-web/global"
music "arimelody-web/music/controller"
musicView "arimelody-web/music/view"
)
func Handler() http.Handler {
@ -18,8 +18,7 @@ func Handler() http.Handler {
mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var artistID = strings.Split(r.URL.Path[1:], "/")[0]
var artist model.Artist
err := global.DB.Get(&artist, "SELECT * FROM artist WHERE id=$1", artistID)
artist, err := music.GetArtist(global.DB, artistID)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
@ -61,8 +60,7 @@ func Handler() http.Handler {
mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var releaseID = strings.Split(r.URL.Path[1:], "/")[0]
var release model.Release
err := global.DB.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", releaseID)
release, err := music.GetRelease(global.DB, releaseID, true)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
@ -76,7 +74,7 @@ func Handler() http.Handler {
switch r.Method {
case http.MethodGet:
// GET /api/v1/music/{id}
music.ServeRelease(release).ServeHTTP(w, r)
musicView.ServeRelease(release).ServeHTTP(w, r)
case http.MethodPut:
// PUT /api/v1/music/{id} (admin)
admin.MustAuthorise(UpdateRelease(release)).ServeHTTP(w, r)
@ -104,8 +102,7 @@ func Handler() http.Handler {
mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var trackID = strings.Split(r.URL.Path[1:], "/")[0]
var track model.Track
err := global.DB.Get(&track, "SELECT * FROM musictrack WHERE id=$1", trackID)
track, err := music.GetTrack(global.DB, trackID)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)

View file

@ -3,21 +3,18 @@ package api
import (
"encoding/json"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"arimelody.me/arimelody.me/global"
"arimelody.me/arimelody.me/music/model"
db "arimelody.me/arimelody.me/music/controller"
"arimelody-web/global"
db "arimelody-web/music/controller"
music "arimelody-web/music/controller"
"arimelody-web/music/model"
)
type artistJSON struct {
ID string `json:"id"`
Name *string `json:"name"`
Website *string `json:"website"`
Avatar *string `json:"avatar"`
}
func ServeAllArtists() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var artists = []*model.Artist{}
@ -36,7 +33,7 @@ func ServeAllArtists() http.Handler {
})
}
func ServeArtist(artist model.Artist) http.Handler {
func ServeArtist(artist *model.Artist) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
type (
creditJSON struct {
@ -44,7 +41,7 @@ func ServeArtist(artist model.Artist) http.Handler {
Primary bool `json:"primary"`
}
artistJSON struct {
model.Artist
*model.Artist
Credits map[string]creditJSON `json:"credits"`
}
)
@ -78,39 +75,23 @@ func ServeArtist(artist model.Artist) http.Handler {
func CreateArtist() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var data artistJSON
err := json.NewDecoder(r.Body).Decode(&data)
var artist model.Artist
err := json.NewDecoder(r.Body).Decode(&artist)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
if data.ID == "" {
if artist.ID == "" {
http.Error(w, "Artist ID cannot be blank\n", http.StatusBadRequest)
return
}
if data.Name == nil || *data.Name == "" {
http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest)
return
}
if artist.Name == "" { artist.Name = artist.ID }
var artist = model.Artist{
ID: data.ID,
Name: *data.Name,
Website: *data.Website,
Avatar: *data.Avatar,
}
_, err = global.DB.Exec(
"INSERT INTO artist (id, name, website, avatar) "+
"VALUES ($1, $2, $3, $4)",
artist.ID,
artist.Name,
artist.Website,
artist.Avatar)
err = music.CreateArtist(global.DB, &artist)
if err != nil {
if strings.Contains(err.Error(), "duplicate key") {
http.Error(w, fmt.Sprintf("Artist %s already exists\n", data.ID), http.StatusBadRequest)
http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest)
return
}
fmt.Printf("FATAL: Failed to create artist %s: %s\n", artist.ID, err)
@ -122,43 +103,59 @@ func CreateArtist() http.Handler {
})
}
func UpdateArtist(artist model.Artist) http.Handler {
func UpdateArtist(artist *model.Artist) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var data artistJSON
err := json.NewDecoder(r.Body).Decode(&data)
err := json.NewDecoder(r.Body).Decode(&artist)
if err != nil {
fmt.Printf("FATAL: Failed to update artist: %s\n", err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
if data.ID != "" { artist.ID = data.ID }
if data.Name != nil { artist.Name = *data.Name }
if data.Website != nil { artist.Website = *data.Website }
if data.Avatar != nil { artist.Avatar = *data.Avatar }
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(&artist.Avatar, artworkDirectory, artist.ID)
_, err = global.DB.Exec(
"UPDATE artist "+
"SET name=$2, website=$3, avatar=$4 "+
"WHERE id=$1",
artist.ID,
artist.Name,
artist.Website,
artist.Avatar)
// 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 = music.UpdateArtist(global.DB, artist)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: Failed to update artist %s: %s\n", artist.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
}
func DeleteArtist(artist model.Artist) http.Handler {
func DeleteArtist(artist *model.Artist) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := global.DB.Exec(
"DELETE FROM artist "+
"WHERE id=$1",
artist.ID)
err := music.DeleteArtist(global.DB, artist.ID)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: Failed to delete artist %s: %s\n", artist.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

View file

@ -10,35 +10,44 @@ import (
"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"
"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 := []*model.Release{}
err := global.DB.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC")
releases, err := music.GetAllReleases(global.DB, false, 0, true)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
catalog := []model.ReleaseShorthand{}
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, model.ReleaseShorthand{
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,
})
}
@ -85,7 +94,7 @@ func CreateRelease() http.Handler {
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)
fmt.Printf("FATAL: Failed to create release %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@ -100,7 +109,7 @@ func CreateRelease() http.Handler {
})
}
func UpdateRelease(release model.Release) http.Handler {
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)
@ -157,15 +166,19 @@ func UpdateRelease(release model.Release) http.Handler {
}
}
err = music.UpdateRelease(global.DB, &release)
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 {
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)
@ -174,15 +187,19 @@ func UpdateReleaseTracks(release model.Release) http.Handler {
return
}
err = music.UpdateReleaseTracks(global.DB, &release, trackIDs)
err = music.UpdateReleaseTracks(global.DB, release.ID, trackIDs)
if err != nil {
fmt.Printf("Failed to update tracks for %s: %s\n", release.ID, err)
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 {
func UpdateReleaseCredits(release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
type creditJSON struct {
Artist string
@ -196,9 +213,9 @@ func UpdateReleaseCredits(release model.Release) http.Handler {
return
}
var credits []model.Credit
var credits []*model.Credit
for _, credit := range data {
credits = append(credits, model.Credit{
credits = append(credits, &model.Credit{
Artist: model.Artist{
ID: credit.Artist,
},
@ -207,19 +224,23 @@ func UpdateReleaseCredits(release model.Release) http.Handler {
})
}
err = music.UpdateReleaseCredits(global.DB, &release, credits)
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
}
fmt.Printf("Failed to update links for %s: %s\n", release.ID, err)
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 {
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)
@ -233,30 +254,27 @@ func UpdateReleaseLinks(release model.Release) http.Handler {
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()
err = music.UpdateReleaseLinks(global.DB, release.ID, links)
if err != nil {
fmt.Printf("Failed to update links for %s: %s\n", release.ID, err)
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 {
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)
err := music.DeleteRelease(global.DB, release.ID)
if err != nil {
fmt.Printf("Failed to delete release %s: %s\n", release.ID, err)
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)
}
})

View file

@ -5,15 +5,15 @@ import (
"fmt"
"net/http"
"arimelody.me/arimelody.me/global"
music "arimelody.me/arimelody.me/music/controller"
"arimelody.me/arimelody.me/music/model"
"arimelody-web/global"
music "arimelody-web/music/controller"
"arimelody-web/music/model"
)
type (
Track struct {
model.Track
Releases []model.ReleaseShorthand
*model.Track
Releases []string `json:"releases"`
}
)
@ -48,13 +48,18 @@ func ServeAllTracks() http.Handler {
})
}
func ServeTrack(track model.Track) http.Handler {
func ServeTrack(track *model.Track) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
releases, err := music.GetTrackReleases(global.DB, track.ID)
dbReleases, err := music.GetTrackReleases(global.DB, track.ID, false)
if err != nil {
fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", track.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
releases := []string{}
for _, release := range dbReleases {
releases = append(releases, release.ID)
}
w.Header().Add("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(Track{ track, releases })
@ -97,7 +102,7 @@ func CreateTrack() http.Handler {
})
}
func UpdateTrack(track model.Track) http.Handler {
func UpdateTrack(track *model.Track) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut || r.URL.Path == "/" {
http.NotFound(w, r)
@ -115,7 +120,7 @@ func UpdateTrack(track model.Track) http.Handler {
return
}
err = music.UpdateTrack(global.DB, &track)
err = music.UpdateTrack(global.DB, track)
if err != nil {
fmt.Printf("Failed to update track %s: %s\n", track.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -130,7 +135,7 @@ func UpdateTrack(track model.Track) http.Handler {
})
}
func DeleteTrack(track model.Track) http.Handler {
func DeleteTrack(track *model.Track) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete || r.URL.Path == "/" {
http.NotFound(w, r)

View file

@ -35,6 +35,8 @@ func HandleImageUpload(data *string, directory string, filename string) (string,
}
defer file.Close()
// TODO: generate compressed versions of image (512x512?)
buffer := bufio.NewWriter(file)
_, err = buffer.Write(imageData)
if err != nil {