diff --git a/admin/components/credits/editcredits.html b/admin/components/credits/editcredits.html index 1ab9cb2..cf743e1 100644 --- a/admin/components/credits/editcredits.html +++ b/admin/components/credits/editcredits.html @@ -51,9 +51,8 @@ makeMagicList(creditList, ".credit"); - creditList.addEventListener("htmx:afterSwap", e => { - const el = creditList.children[creditList.children.length - 1]; - + function rigCredit(el) { + console.log(el); const artistID = el.dataset.artist; const deleteBtn = el.querySelector("a.delete"); @@ -64,6 +63,12 @@ el.addEventListener("dragstart", () => { el.classList.add("moving") }); el.addEventListener("dragend", () => { el.classList.remove("moving") }); + } + + [...creditList.querySelectorAll(".credit")].map(rigCredit); + + creditList.addEventListener("htmx:afterSwap", () => { + rigCredit(creditList.children[creditList.children.length - 1]); }); container.showModal(); diff --git a/admin/components/tracks/newtrack.html b/admin/components/tracks/newtrack.html index 3e76959..681110e 100644 --- a/admin/components/tracks/newtrack.html +++ b/admin/components/tracks/newtrack.html @@ -1,7 +1,7 @@ -
  • +
  • - {{.Number}} + 0 {{.Title}}

    Delete diff --git a/admin/http.go b/admin/http.go index b69d8dd..f9b26f9 100644 --- a/admin/http.go +++ b/admin/http.go @@ -12,8 +12,8 @@ import ( "arimelody.me/arimelody.me/discord" "arimelody.me/arimelody.me/global" - musicController "arimelody.me/arimelody.me/music/controller" musicModel "arimelody.me/arimelody.me/music/model" + musicDB "arimelody.me/arimelody.me/music/controller" ) type loginData struct { @@ -29,18 +29,6 @@ func Handler() http.Handler { mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease()))) mux.Handle("/track/", MustAuthorise(http.StripPrefix("/track", serveTrack()))) - mux.Handle("/createtrack", MustAuthorise(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - track := musicModel.Track{ Title: "Untitled Track" } - trackID, err := musicController.CreateTrackDB(global.DB, &track) - if err != nil { - fmt.Printf("Failed to create track: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - track.ID = trackID - global.Tracks = append(global.Tracks, &track) - http.Redirect(w, r, fmt.Sprintf("/admin/track/%s", trackID), http.StatusTemporaryRedirect) - }))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) @@ -54,30 +42,55 @@ func Handler() http.Handler { } type ( - Track struct { - *musicModel.Track - Lyrics template.HTML - // Number int - } IndexData struct { - Releases []*musicModel.Release + Releases []musicModel.FullRelease Artists []*musicModel.Artist - Tracks []Track + Tracks []musicModel.DisplayTrack } ) - var tracks = []Track{} - for _, track := range global.Tracks { - if track.Release != nil { continue } - tracks = append(tracks, Track{ + dbReleases, err := musicDB.GetAllReleases(global.DB) + if err != nil { + fmt.Printf("FATAL: Failed to pull releases: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + releases := []musicModel.FullRelease{} + for _, release := range dbReleases { + fullRelease, err := musicDB.GetFullRelease(global.DB, release) + if err != nil { + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + releases = append(releases, *fullRelease) + } + + artists, err := musicDB.GetAllArtists(global.DB) + if err != nil { + fmt.Printf("FATAL: Failed to pull artists: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + dbTracks, err := musicDB.GetOrphanTracks(global.DB) + if err != nil { + fmt.Printf("FATAL: Failed to pull orphan tracks: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + var tracks = []musicModel.DisplayTrack{} + for _, track := range dbTracks { + tracks = append(tracks, musicModel.DisplayTrack{ Track: track, Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
    ", -1)), }) } - err := pages["index"].Execute(w, IndexData{ - Releases: global.Releases, - Artists: global.Artists, + err = pages["index"].Execute(w, IndexData{ + Releases: releases, + Artists: artists, Tracks: tracks, }) if err != nil { diff --git a/admin/releasehttp.go b/admin/releasehttp.go index 2932c26..86b4aa1 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -2,83 +2,79 @@ package admin import ( "fmt" - "html/template" "net/http" "strings" "arimelody.me/arimelody.me/global" "arimelody.me/arimelody.me/music/model" -) - -type ( - gatewayTrack struct { - *model.Track - Lyrics template.HTML - Number int - } - - gatewayRelease struct { - *model.Release - Tracks []gatewayTrack - } + controller "arimelody.me/arimelody.me/music/controller" ) func serveRelease() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { slices := strings.Split(r.URL.Path[1:], "/") - id := slices[0] - release := global.GetRelease(id) + releaseID := slices[0] + release, err := controller.GetRelease(global.DB, releaseID) + if err != nil { + fmt.Printf("FATAL: Failed to pull release %s: %s\n", releaseID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } if release == nil { http.NotFound(w, r) return } + authorised := GetSession(r) != nil + if !authorised && !release.Visible { + http.NotFound(w, r) + return + } + + fullRelease, err := controller.GetFullRelease(global.DB, release) + if err != nil { + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + if len(slices) > 1 { switch slices[1] { case "editcredits": - serveEditCredits(release).ServeHTTP(w, r) + serveEditCredits(fullRelease).ServeHTTP(w, r) return case "addcredit": - serveAddCredit(release).ServeHTTP(w, r) + serveAddCredit(fullRelease).ServeHTTP(w, r) return case "newcredit": serveNewCredit().ServeHTTP(w, r) return case "editlinks": - serveEditLinks(release).ServeHTTP(w, r) + serveEditLinks(fullRelease).ServeHTTP(w, r) return case "edittracks": - serveEditTracks(release).ServeHTTP(w, r) + serveEditTracks(fullRelease).ServeHTTP(w, r) return case "addtrack": - serveAddTrack(release).ServeHTTP(w, r) + serveAddTrack(fullRelease).ServeHTTP(w, r) return case "newtrack": - serveNewTrack(release).ServeHTTP(w, r) + serveNewTrack().ServeHTTP(w, r) return } http.NotFound(w, r) return } - tracks := []gatewayTrack{} - for i, track := range release.Tracks { - tracks = append(tracks, gatewayTrack{ - Track: track, - Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
    ", -1)), - Number: i + 1, - }) - } - - err := pages["release"].Execute(w, gatewayRelease{release, tracks}) + err = pages["release"].Execute(w, fullRelease) if err != nil { - fmt.Printf("Error rendering admin release page for %s: %s\n", id, err) + fmt.Printf("Error rendering admin release page for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } -func serveEditCredits(release *model.Release) http.Handler { +func serveEditCredits(release *model.FullRelease) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") err := components["editcredits"].Execute(w, release) @@ -89,20 +85,13 @@ func serveEditCredits(release *model.Release) http.Handler { }) } -func serveAddCredit(release *model.Release) http.Handler { +func serveAddCredit(release *model.FullRelease) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var artists = []*model.Artist{} - for _, artist := range global.Artists { - var exists = false - for _, credit := range release.Credits { - if credit.Artist == artist { - exists = true - break - } - } - if !exists { - artists = append(artists, artist) - } + artists, err := controller.GetArtistsNotOnRelease(global.DB, release.Release) + if err != nil { + fmt.Printf("FATAL: Failed to pull artists not on %s: %s\n", release.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } type response struct { @@ -111,7 +100,7 @@ func serveAddCredit(release *model.Release) http.Handler { } w.Header().Set("Content-Type", "text/html") - err := components["addcredit"].Execute(w, response{ + err = components["addcredit"].Execute(w, response{ ReleaseID: release.ID, Artists: artists, }) @@ -124,23 +113,28 @@ func serveAddCredit(release *model.Release) http.Handler { func serveNewCredit() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - artist := global.GetArtist(strings.Split(r.URL.Path, "/")[3]) + artistID := strings.Split(r.URL.Path, "/")[3] + artist, err := controller.GetArtist(global.DB, artistID) + if err != nil { + fmt.Printf("FATAL: Failed to pull artists %s: %s\n", artistID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } if artist == nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html") - err := components["newcredit"].Execute(w, artist) + err = components["newcredit"].Execute(w, artist) if err != nil { fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } - return }) } -func serveEditLinks(release *model.Release) http.Handler { +func serveEditLinks(release *model.FullRelease) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") err := components["editlinks"].Execute(w, release) @@ -148,49 +142,27 @@ func serveEditLinks(release *model.Release) http.Handler { fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } - return }) } -func serveEditTracks(release *model.Release) http.Handler { +func serveEditTracks(release *model.FullRelease) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - type Track struct { - *model.Track - Number int - } - type Release struct { - *model.Release - Tracks []Track - } - var data = Release{ release, []Track{} } - for i, track := range release.Tracks { - data.Tracks = append(data.Tracks, Track{track, i + 1}) - } - - err := components["edittracks"].Execute(w, data) + err := components["edittracks"].Execute(w, release) if err != nil { fmt.Printf("Error rendering edit tracks component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } - return }) } -func serveAddTrack(release *model.Release) http.Handler { +func serveAddTrack(release *model.FullRelease) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var tracks = []*model.Track{} - for _, track := range global.Tracks { - var exists = false - for _, t := range release.Tracks { - if t == track { - exists = true - break - } - } - if !exists { - tracks = append(tracks, track) - } + tracks, err := controller.GetTracksNotOnRelease(global.DB, release.Release) + if err != nil { + fmt.Printf("FATAL: Failed to pull tracks not on %s: %s\n", release.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } type response struct { @@ -199,7 +171,7 @@ func serveAddTrack(release *model.Release) http.Handler { } w.Header().Set("Content-Type", "text/html") - err := components["addtrack"].Execute(w, response{ + err = components["addtrack"].Execute(w, response{ ReleaseID: release.ID, Tracks: tracks, }) @@ -211,24 +183,22 @@ func serveAddTrack(release *model.Release) http.Handler { }) } -func serveNewTrack(release *model.Release) http.Handler { +func serveNewTrack() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - track := global.GetTrack(strings.Split(r.URL.Path, "/")[3]) + trackID := strings.Split(r.URL.Path, "/")[3] + track, err := controller.GetTrack(global.DB, trackID) + if err != nil { + fmt.Printf("Error rendering new track component for %s: %s\n", trackID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } if track == nil { http.NotFound(w, r) return } - type Track struct { - *model.Track - Number int - } - w.Header().Set("Content-Type", "text/html") - err := components["newtrack"].Execute(w, Track{ - track, - len(release.Tracks) + 1, - }) + err = components["newtrack"].Execute(w, track) if err != nil { fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/static/index.js b/admin/static/index.js index 709f902..8eec511 100644 --- a/admin/static/index.js +++ b/admin/static/index.js @@ -1,4 +1,5 @@ const newReleaseBtn = document.getElementById("create-release"); +const newTrackBtn = document.getElementById("create-track"); newReleaseBtn.addEventListener("click", event => { event.preventDefault(); @@ -22,3 +23,27 @@ newReleaseBtn.addEventListener("click", event => { console.error(err); }); }); + +newTrackBtn.addEventListener("click", event => { + event.preventDefault(); + const title = prompt("Enter an title for this track:"); + if (title == null || title == "") return; + + fetch("/api/v1/track", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({title}) + }).then(res => { + res.text().then(text => { + if (res.ok) { + location = "/admin/track/" + text; + } else { + alert("Request failed: " + text); + console.error(text); + } + }) + }).catch(err => { + alert("Failed to create release. Check the console for details."); + console.error(err); + }); +}); diff --git a/admin/trackhttp.go b/admin/trackhttp.go index 3af672d..3400f21 100644 --- a/admin/trackhttp.go +++ b/admin/trackhttp.go @@ -6,19 +6,48 @@ import ( "strings" "arimelody.me/arimelody.me/global" + "arimelody.me/arimelody.me/music/model" + "arimelody.me/arimelody.me/music/controller" ) func serveTrack() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { slices := strings.Split(r.URL.Path[1:], "/") id := slices[0] - track := global.GetTrack(id) + track, err := music.GetTrack(global.DB, id) + if err != nil { + fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } if track == nil { http.NotFound(w, r) return } - err := pages["track"].Execute(w, track) + dbReleases, err := music.GetTrackReleases(global.DB, track) + if err != nil { + fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + releases := []model.FullRelease{} + for _, release := range dbReleases { + fullRelease, err := music.GetFullRelease(global.DB, release) + if err != nil { + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + releases = append(releases, *fullRelease) + } + + type Track struct { + *model.Track + Releases []model.FullRelease + } + + err = pages["track"].Execute(w, Track{ Track: track, Releases: releases }) if err != nil { fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/views/edit-track.html b/admin/views/edit-track.html index c8b99d0..2b0fed5 100644 --- a/admin/views/edit-track.html +++ b/admin/views/edit-track.html @@ -43,8 +43,10 @@

    Featured in

    - {{if .Release}} - {{block "release" .Release}}{{end}} + {{if .Releases}} + {{range .Releases}} + {{block "release" .}}{{end}} + {{end}} {{else}}

    This track isn't bound to a release.

    {{end}} diff --git a/admin/views/index.html b/admin/views/index.html index 3a332a2..f88576d 100644 --- a/admin/views/index.html +++ b/admin/views/index.html @@ -9,7 +9,7 @@

    Releases

    - Create New + Create New
    {{range .Releases}} @@ -22,7 +22,7 @@

    Artists

    - Create New + Create New
    {{range $Artist := .Artists}} @@ -38,7 +38,7 @@

    Tracks

    - Create New + Create New

    "Orphaned" tracks that have not yet been bound to a release.

    @@ -47,11 +47,6 @@

    {{$Track.Title}} - {{if $Track.Release}} - {{$Track.Release.Title}} - {{else}} - (no release) - {{end}}

    {{if $Track.Description}}

    {{$Track.Description}}

    diff --git a/api/api.go b/api/api.go index 5876e3a..c0fcf12 100644 --- a/api/api.go +++ b/api/api.go @@ -1,9 +1,13 @@ package api import ( + "fmt" "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" ) @@ -13,16 +17,25 @@ func Handler() http.Handler { // ARTIST ENDPOINTS 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) + if err != nil { + fmt.Printf("FATAL: Error while retrieving artist %s: %s\n", artistID, err) + http.NotFound(w, r) + return + } + switch r.Method { case http.MethodGet: // GET /api/v1/artist/{id} - ServeArtist().ServeHTTP(w, r) + ServeArtist(artist).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/artist/{id} (admin) - admin.MustAuthorise(UpdateArtist()).ServeHTTP(w, r) + admin.MustAuthorise(UpdateArtist(artist)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/artist/{id} (admin) - admin.MustAuthorise(DeleteArtist()).ServeHTTP(w, r) + admin.MustAuthorise(DeleteArtist(artist)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -43,16 +56,25 @@ func Handler() http.Handler { // RELEASE ENDPOINTS 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) + if err != nil { + fmt.Printf("FATAL: Error while retrieving release %s: %s\n", releaseID, err) + http.NotFound(w, r) + return + } + switch r.Method { case http.MethodGet: // GET /api/v1/music/{id} - music.ServeRelease().ServeHTTP(w, r) + music.ServeRelease(release).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/music/{id} (admin) - admin.MustAuthorise(UpdateRelease()).ServeHTTP(w, r) + admin.MustAuthorise(UpdateRelease(release)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/music/{id} (admin) - admin.MustAuthorise(DeleteRelease()).ServeHTTP(w, r) + admin.MustAuthorise(DeleteRelease(release)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -73,16 +95,25 @@ func Handler() http.Handler { // TRACK ENDPOINTS 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) + if err != nil { + fmt.Printf("FATAL: Error while retrieving track %s: %s\n", trackID, err) + http.NotFound(w, r) + return + } + switch r.Method { case http.MethodGet: // GET /api/v1/track/{id} (admin) - admin.MustAuthorise(ServeTrack()).ServeHTTP(w, r) + admin.MustAuthorise(ServeTrack(track)).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/track/{id} (admin) - admin.MustAuthorise(UpdateTrack()).ServeHTTP(w, r) + admin.MustAuthorise(UpdateTrack(track)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/track/{id} (admin) - admin.MustAuthorise(DeleteTrack()).ServeHTTP(w, r) + admin.MustAuthorise(DeleteTrack(track)).ServeHTTP(w, r) default: http.NotFound(w, r) } diff --git a/api/artist.go b/api/artist.go index 03c69f7..b4b4faf 100644 --- a/api/artist.go +++ b/api/artist.go @@ -4,10 +4,10 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "arimelody.me/arimelody.me/global" "arimelody.me/arimelody.me/music/model" - controller "arimelody.me/arimelody.me/music/controller" ) type artistJSON struct { @@ -19,76 +19,60 @@ type artistJSON struct { func ServeAllArtists() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(global.Artists) + var artists = []*model.Artist{} + err := global.DB.Select(&artists, "SELECT * FROM artist") if err != nil { + fmt.Printf("FATAL: 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") + err = json.NewEncoder(w).Encode(artists) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } }) } -func ServeArtist() http.Handler { +func ServeArtist(artist model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - ServeAllArtists().ServeHTTP(w, r) - return - } - type ( creditJSON struct { - Role string `json:"role"` - Primary bool `json:"primary"` + Release string `json:"release"` + Role string `json:"role"` + Primary bool `json:"primary"` } artistJSON struct { model.Artist Credits map[string]creditJSON `json:"credits"` } ) - var artist = artistJSON{} - artist.ID = r.URL.Path[1:] - var a = global.GetArtist(artist.ID) - if a == nil { - http.NotFound(w, r) + var credits = map[string]creditJSON{} + err := global.DB.Select(&credits, "SELECT release,role,is_primary FROM musiccredit WHERE id=$1", artist.ID) + if err != nil { + fmt.Printf("FATAL: Failed to retrieve artist credits for %s: %s\n", artist.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - artist.Name = a.Name - artist.Website = a.Website - artist.Credits = make(map[string]creditJSON) - - for _, release := range global.Releases { - for _, credit := range release.Credits { - if credit.Artist.ID != artist.ID { - continue - } - artist.Credits[release.ID] = creditJSON{ - Role: credit.Role, - Primary: credit.Primary, - } - } - } w.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(artist) + err = json.NewEncoder(w).Encode(artistJSON{ + Artist: artist, + Credits: credits, + }) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return } }) } func CreateArtist() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.NotFound(w, r) - return - } - var data artistJSON err := json.NewDecoder(r.Body).Decode(&data) if err != nil { - fmt.Printf("Failed to create artist: %s\n", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } @@ -102,11 +86,6 @@ func CreateArtist() http.Handler { return } - if global.GetArtist(data.ID) != nil { - http.Error(w, fmt.Sprintf("Artist %s already exists\n", data.ID), http.StatusBadRequest) - return - } - var artist = model.Artist{ ID: data.ID, Name: *data.Name, @@ -114,116 +93,66 @@ func CreateArtist() http.Handler { Avatar: *data.Avatar, } - err = controller.CreateArtistDB(global.DB, &artist) + _, err = global.DB.Exec( + "INSERT INTO artist (id, name, website, avatar) "+ + "VALUES ($1, $2, $3, $4)", + artist.ID, + artist.Name, + artist.Website, + artist.Avatar) if err != nil { - fmt.Printf("Failed to create artist %s: %s\n", artist.ID, err) + if strings.Contains(err.Error(), "duplicate key") { + http.Error(w, fmt.Sprintf("Artist %s already exists\n", data.ID), http.StatusBadRequest) + return + } + fmt.Printf("FATAL: Failed to create artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - global.Artists = append(global.Artists, &artist) - - w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - err = json.NewEncoder(w).Encode(artist) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } }) } -func UpdateArtist() http.Handler { +func UpdateArtist(artist model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPut { - http.NotFound(w, r) - return - } - - if r.URL.Path == "/" { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - var data artistJSON err := json.NewDecoder(r.Body).Decode(&data) if err != nil { - fmt.Printf("Failed to update artist: %s\n", err) + fmt.Printf("FATAL: Failed to update artist: %s\n", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - var artistID = r.URL.Path[1:] - var artist = global.GetArtist(artistID) - if artist == nil { - http.Error(w, fmt.Sprintf("Artist %s does not exist\n", artistID), 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 } - var update = *artist - - if data.ID != "" { update.ID = data.ID } - if data.Name != nil { update.Name = *data.Name } - if data.Website != nil { update.Website = *data.Website } - if data.Avatar != nil { update.Avatar = *data.Avatar } - - err = controller.UpdateArtistDB(global.DB, &update) + _, err = global.DB.Exec( + "UPDATE artist "+ + "SET name=$2, website=$3, avatar=$4 "+ + "WHERE id=$1", + artist.ID, + artist.Name, + artist.Website, + artist.Avatar) if err != nil { - fmt.Printf("Failed to update artist %s: %s\n", artist.ID, err) + fmt.Printf("FATAL: Failed to update artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - artist.ID = update.ID - artist.Name = update.Name - artist.Website = update.Website - artist.Avatar = update.Avatar - - w.Header().Add("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(artist) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return } }) } -func DeleteArtist() http.Handler { +func DeleteArtist(artist model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodDelete { - http.NotFound(w, r) - return - } - - if r.URL.Path == "/" { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - var artistID = r.URL.Path[1:] - var artist = global.GetArtist(artistID) - if artist == nil { - http.Error(w, fmt.Sprintf("Artist %s does not exist\n", artistID), http.StatusBadRequest) - return - } - - err := controller.DeleteArtistDB(global.DB, artist) + _, err := global.DB.Exec( + "DELETE FROM artist "+ + "WHERE id=$1", + artist.ID) if err != nil { - fmt.Printf("Failed to delete artist %s: %s\n", artist.ID, err) + fmt.Printf("FATAL: Failed to delete artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return } - - global.Artists = func () []*model.Artist { - var artists = []*model.Artist{} - for _, a := range global.Artists { - if a.ID == artist.ID { continue } - artists = append(artists, a) - } - return artists - }() - - w.WriteHeader(http.StatusOK) - w.Write([]byte(fmt.Sprintf("Artist %s has been deleted\n", artist.ID))) }) } diff --git a/api/release.go b/api/release.go index eeae08f..088fe98 100644 --- a/api/release.go +++ b/api/release.go @@ -1,8 +1,6 @@ package api import ( - "bufio" - "encoding/base64" "encoding/json" "fmt" "io/fs" @@ -14,7 +12,6 @@ import ( "arimelody.me/arimelody.me/admin" "arimelody.me/arimelody.me/global" - controller "arimelody.me/arimelody.me/music/controller" "arimelody.me/arimelody.me/music/model" ) @@ -32,39 +29,40 @@ type releaseBodyJSON struct { func ServeCatalog() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - type CatalogItem struct { + type catalogItem struct { ID string `json:"id"` Title string `json:"title"` - Description string `json:"description"` ReleaseType model.ReleaseType `json:"type"` ReleaseDate time.Time `json:"releaseDate"` Artwork string `json:"artwork"` - Buyname string `json:"buyname"` Buylink string `json:"buylink"` - Links []*model.Link `json:"links"` } - catalog := []CatalogItem{} + 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 := []catalogItem{} authorised := admin.GetSession(r) != nil - for _, release := range global.Releases { + for _, release := range releases { if !release.Visible && !authorised { continue } - catalog = append(catalog, CatalogItem{ + catalog = append(catalog, catalogItem{ ID: release.ID, Title: release.Title, - Description: release.Description, ReleaseType: release.ReleaseType, ReleaseDate: release.ReleaseDate, Artwork: release.Artwork, - Buyname: release.Buyname, Buylink: release.Buylink, - Links: release.Links, }) } w.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(catalog) + err = json.NewEncoder(w).Encode(catalog) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -122,11 +120,6 @@ func CreateRelease() http.Handler { buylink := "" if data.Buylink != nil && *data.Buylink != "" { buylink = *data.Buylink } - if global.GetRelease(data.ID) != nil { - http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest) - return - } - var release = model.Release{ ID: data.ID, Visible: false, @@ -137,31 +130,42 @@ func CreateRelease() http.Handler { Artwork: artwork, Buyname: buyname, Buylink: buylink, - Links: []*model.Link{}, - Credits: []*model.Credit{}, - Tracks: []*model.Track{}, } - err = controller.CreateReleaseDB(global.DB, &release) + _, err = global.DB.Exec( + "INSERT INTO musicrelease "+ + "(id, visible, title, description, type, release_date, artwork, buyname, buylink) "+ + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + release.ID, + release.Visible, + release.Title, + release.Description, + release.ReleaseType, + release.ReleaseDate.Format("2006-01-02 15:04:05"), + release.Artwork, + release.Buyname, + release.Buylink) if err != nil { + if strings.Contains(err.Error(), "duplicate key") { + http.Error(w, fmt.Sprintf("Release %s already exists\n", data.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 } - global.Releases = append([]*model.Release{&release}, global.Releases...) - 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) - return } }) } -func UpdateRelease() 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) @@ -170,124 +174,11 @@ func UpdateRelease() http.Handler { segments := strings.Split(r.URL.Path[1:], "/") var releaseID = segments[0] - var release = global.GetRelease(releaseID) - if release == nil { - http.Error(w, fmt.Sprintf("Release %s does not exist\n", releaseID), http.StatusBadRequest) - return - } - - if len(segments) == 1 { - var data releaseBodyJSON - err := json.NewDecoder(r.Body).Decode(&data) - if err != nil { - fmt.Printf("Failed to update release: %s\n", err) - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - var update = *release - if data.ID != "" { update.ID = data.ID } - if data.Visible != nil { update.Visible = *data.Visible } - if data.Title != nil { update.Title = *data.Title } - if data.Description != nil { update.Description = *data.Description } - if data.ReleaseType != nil { update.ReleaseType = *data.ReleaseType } - if data.ReleaseDate != nil { - newDate, err := time.Parse("2006-01-02T15:04", *data.ReleaseDate) - if err != nil { - http.Error(w, "Invalid release date", http.StatusBadRequest) - return - } - update.ReleaseDate = newDate - } - if data.Artwork != nil { - if strings.Contains(*data.Artwork, ";base64,") { - split := strings.Split(*data.Artwork, ";base64,") - header := split[0] - imageData, err := base64.StdEncoding.DecodeString(split[1]) - ext, _ := strings.CutPrefix(header, "data:image/") - - switch ext { - case "png": - case "jpg": - case "jpeg": - default: - http.Error(w, "Invalid image type. Allowed: .png, .jpg, .jpeg", http.StatusBadRequest) - return - } - - artworkDirectory := filepath.Join("uploads", "musicart") - // ensure directory exists - os.MkdirAll(artworkDirectory, os.ModePerm) - - imagePath := filepath.Join(artworkDirectory, fmt.Sprintf("%s.%s", update.ID, ext)) - file, err := os.Create(imagePath) - if err != nil { - fmt.Printf("FATAL: Failed to create file %s: %s\n", imagePath, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - defer file.Close() - - buffer := bufio.NewWriter(file) - _, err = buffer.Write(imageData) - if err != nil { - fmt.Printf("FATAL: Failed to write to file %s: %s\n", imagePath, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - if err := buffer.Flush(); err != nil { - fmt.Printf("FATAL: Failed to flush data to file %s: %s\n", imagePath, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - // clean up files with this ID and different extensions - err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error { - if path == imagePath { return nil } - - withoutExt := strings.TrimSuffix(path, filepath.Ext(path)) - if withoutExt != filepath.Join(artworkDirectory, update.ID) { return nil } - - return os.Remove(path) - }) - if err != nil { - fmt.Printf("WARN: Error while cleaning up artwork files: %s\n", err) - } - - update.Artwork = fmt.Sprintf("/uploads/musicart/%s.%s", update.ID, ext) - } else { - update.Artwork = *data.Artwork - } - } - - if data.Buyname != nil { update.Buyname = *data.Buyname } - if data.Buylink != nil { update.Buylink = *data.Buylink } - - err = controller.UpdateReleaseDB(global.DB, &update) - if err != nil { - fmt.Printf("Failed to update release %s: %s\n", release.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - release.ID = update.ID - release.Visible = update.Visible - release.Title = update.Title - release.Description = update.Description - release.ReleaseType = update.ReleaseType - release.ReleaseDate = update.ReleaseDate - release.Artwork = update.Artwork - release.Buyname = update.Buyname - release.Buylink = update.Buylink - - w.Header().Add("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(release) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } + var exists int + err := global.DB.Get(&exists, "SELECT count(*) FROM musicrelease WHERE id=$1", releaseID) + if err != nil { + fmt.Printf("Failed to update release: %s\n", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } @@ -302,18 +193,81 @@ func UpdateRelease() http.Handler { } return } - - http.NotFound(w, r) - }) -} - -func UpdateReleaseTracks(release *model.Release) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" || r.Method != http.MethodPut { + + if len(segments) > 2 { http.NotFound(w, r) return } + var data releaseBodyJSON + err = json.NewDecoder(r.Body).Decode(&data) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + if data.ID != "" { release.ID = data.ID } + if data.Visible != nil { release.Visible = *data.Visible } + if data.Title != nil { release.Title = *data.Title } + if data.Description != nil { release.Description = *data.Description } + if data.ReleaseType != nil { release.ReleaseType = *data.ReleaseType } + if data.ReleaseDate != nil { + newDate, err := time.Parse("2006-01-02T15:04", *data.ReleaseDate) + if err != nil { + http.Error(w, "Invalid release date", http.StatusBadRequest) + return + } + release.ReleaseDate = newDate + } + if data.Artwork != nil { + if strings.Contains(*data.Artwork, ";base64,") { + var artworkDirectory = filepath.Join("uploads", "musicart") + filename, err := HandleImageUpload(data.Artwork, artworkDirectory, data.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) + } else { + release.Artwork = *data.Artwork + } + } + + if data.Buyname != nil { release.Buyname = *data.Buyname } + if data.Buylink != nil { release.Buylink = *data.Buylink } + + _, err = global.DB.Exec( + "UPDATE musicrelease SET "+ + "visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9 "+ + "WHERE id=$1", + release.ID, + release.Visible, + release.Title, + release.Description, + release.ReleaseType, + release.ReleaseDate.Format("2006-01-02 15:04:05"), + release.Artwork, + release.Buyname, + release.Buylink) + 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 { @@ -321,123 +275,85 @@ func UpdateReleaseTracks(release *model.Release) http.Handler { return } - var old_tracks = (*release).Tracks - var new_tracks = []*model.Track{} - for _, trackID := range trackIDs { - var track = global.GetTrack(trackID) - if track == nil { - http.Error(w, fmt.Sprintf("Track %s does not exist\n", trackID), http.StatusBadRequest) - return - } - new_tracks = append(new_tracks, track) - track.Release = release + tx := global.DB.MustBegin() + tx.MustExec("DELETE FROM musicreleasetrack WHERE release=$1", release.ID) + for i, trackID := range trackIDs { + tx.MustExec( + "INSERT INTO musicreleasetrack "+ + "(release, track, number) "+ + "VALUES ($1, $2, $3)", + release.ID, + trackID, + i) } - - err = controller.UpdateReleaseTracksDB(global.DB, release, new_tracks) + err = tx.Commit() 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) - return - } - - release.Tracks = new_tracks - - // remove release from orphaned tracks - for _, old_track := range old_tracks { - var exists = false - for _, track := range new_tracks { - if track.ID == old_track.ID { - exists = true - break - } - } - if !exists { - old_track.Release = nil - } - } - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(release) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return } }) } -func UpdateReleaseCredits(release *model.Release) http.Handler { +func UpdateReleaseCredits(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 - } - type creditJSON struct { Artist string Role string Primary bool } - - var list []creditJSON - err := json.NewDecoder(r.Body).Decode(&list) + 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 i, data := range list { - if data.Artist == "" { - http.Error(w, fmt.Sprintf("Artist ID cannot be blank (%d)", i), http.StatusBadRequest) - return + // clear duplicates + type Credit struct { + Role string + Primary bool + } + var credits = map[string]Credit{} + for _, credit := range data { + credits[credit.Artist] = Credit{ + Role: credit.Role, + Primary: credit.Primary, } - - for _, credit := range credits { - if data.Artist == credit.Artist.ID { - http.Error(w, fmt.Sprintf("Artist %s credited more than once", data.Artist), http.StatusBadRequest) - return - } - } - - if data.Role == "" { - http.Error(w, fmt.Sprintf("Artist role cannot be blank (%d)", i), http.StatusBadRequest) - return - } - - var artist = global.GetArtist(data.Artist) - if artist == nil { - http.Error(w, fmt.Sprintf("Artist %s does not exist\n", data.Artist), http.StatusBadRequest) - return - } - - credits = append(credits, &model.Credit{ - Artist: artist, - Role: data.Role, - Primary: data.Primary, - }) } - err = controller.UpdateReleaseCreditsDB(global.DB, release, credits) - if err != nil { - fmt.Printf("Failed to update links %s: %s\n", release.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } + tx := global.DB.MustBegin() + tx.MustExec("DELETE FROM musiccredit WHERE release=$1", release.ID) + for artistID := range credits { + if credits[artistID].Role == "" { + http.Error(w, fmt.Sprintf("Artist role cannot be blank (%s)", artistID), http.StatusBadRequest) + return + } - release.Credits = credits + var exists int + _ = global.DB.Get(&exists, "SELECT count(*) FROM artist WHERE id=$1", artistID) + if exists == 0 { + http.Error(w, fmt.Sprintf("Artist %s does not exist\n", artistID), http.StatusBadRequest) + return + } - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(release) + tx.MustExec( + "INSERT INTO musiccredit "+ + "(release, artist, role, is_primary) "+ + "VALUES ($1, $2, $3, $4)", + release.ID, + artistID, + credits[artistID].Role, + credits[artistID].Primary) + } + 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) - return } }) } -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) @@ -451,56 +367,31 @@ func UpdateReleaseLinks(release *model.Release) http.Handler { return } - err = controller.UpdateReleaseLinksDB(global.DB, release, links) - if err != nil { - fmt.Printf("Failed to update links %s: %s\n", release.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - 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) } - - release.Links = links - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(release) + 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) - return } }) } -func DeleteRelease() http.Handler { +func DeleteRelease(release model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodDelete { - http.NotFound(w, r) - return - } - - var releaseID = r.URL.Path[1:] - var release = global.GetRelease(releaseID) - if release == nil { - http.Error(w, fmt.Sprintf("Release %s does not exist\n", releaseID), http.StatusBadRequest) - return - } - - err := controller.DeleteReleaseDB(global.DB, release) + _, 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) - return } - - global.Releases = func () []*model.Release { - var releases = []*model.Release{} - for _, r := range global.Releases { - if r.ID == release.ID { continue } - releases = append(releases, r) - } - return releases - }() - - w.WriteHeader(http.StatusOK) - w.Write([]byte(fmt.Sprintf("Release %s has been deleted\n", release.ID))) }) } diff --git a/api/track.go b/api/track.go index a5fc03d..3b7f184 100644 --- a/api/track.go +++ b/api/track.go @@ -7,35 +7,32 @@ import ( "arimelody.me/arimelody.me/global" "arimelody.me/arimelody.me/music/model" - controller "arimelody.me/arimelody.me/music/controller" ) func ServeAllTracks() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // type trackJSON struct { - // model.Track - // Release string `json:"release"` - // } - // var tracks = []trackJSON{} - // - // for _, track := range global.Tracks { - // for _, release := range global. { - // tracks = append(tracks, { - // track, - // Release - // }) - // } + type track struct { + ID string `json:"id"` + Title string `json:"title"` + } + var tracks = []track{} + + err := global.DB.Select(&tracks, "SELECT id, title FROM musictrack") + if err != nil { + fmt.Printf("FATAL: Failed to pull tracks from DB: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } w.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(global.Tracks) + err = json.NewEncoder(w).Encode(tracks) if err != nil { + fmt.Printf("FATAL: Failed to serve all tracks: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return } }) } -func ServeTrack() http.Handler { +func ServeTrack(track model.Track) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { ServeAllTracks().ServeHTTP(w, r) @@ -43,17 +40,35 @@ func ServeTrack() http.Handler { } var trackID = r.URL.Path[1:] - var track = global.GetTrack(trackID) - if track == nil { + var track = model.Track{} + err := global.DB.Get(&track, "SELECT * from musictrack WHERE id=$1", trackID) + if err != nil { http.NotFound(w, r) return } - w.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(track) - if err != nil { + var releases = []*model.Release{} + err = global.DB.Select(&releases, + "SELECT * FROM musicrelease JOIN musicreleasetrack AS mrt "+ + "WHERE mrt.track=$1 "+ + "ORDER BY release_date", + track.ID, + ) + if err != nil { + fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", trackID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + + type response struct { + model.Track + Releases []*model.Release + } + + w.Header().Add("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(response{ track, releases }) + if err != nil { + fmt.Printf("FATAL: Failed to serve track %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return } }) } @@ -77,122 +92,92 @@ func CreateTrack() http.Handler { return } - trackID, err := controller.CreateTrackDB(global.DB, &track) + var trackID string + err = global.DB.Get(&trackID, + "INSERT INTO musictrack (title, description, lyrics, preview_url) "+ + "VALUES ($1, $2, $3, $4) "+ + "RETURNING id", + track.Title, + track.Description, + track.Lyrics, + track.PreviewURL) if err != nil { - fmt.Printf("Failed to create track: %s\n", err) + fmt.Printf("FATAL: Failed to create track: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - track.ID = trackID - global.Tracks = append(global.Tracks, &track) - - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", "text/plain") w.WriteHeader(http.StatusCreated) - err = json.NewEncoder(w).Encode(track) + w.Write([]byte(trackID)) }) } -func UpdateTrack() http.Handler { +func UpdateTrack(track model.Track) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPut { + if r.Method != http.MethodPut || r.URL.Path == "/" { http.NotFound(w, r) return } - if r.URL.Path == "/" { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - var data model.Track - err := json.NewDecoder(r.Body).Decode(&data) + var update model.Track + err := json.NewDecoder(r.Body).Decode(&update) if err != nil { - fmt.Printf("Failed to update track: %s\n", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - var trackID = r.URL.Path[1:] - var track = global.GetTrack(trackID) - if track == nil { - http.Error(w, fmt.Sprintf("Track %s does not exist\n", trackID), http.StatusBadRequest) + if update.Title == "" { + http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest) return } - data.ID = trackID + var trackID = r.URL.Path[1:] + var track = model.Track{} + err = global.DB.Get(&track, "SELECT * from musictrack WHERE id=$1", trackID) + if err != nil { + http.NotFound(w, r) + return + } - if data.Title == "" { data.Title = track.Title } - - err = controller.UpdateTrackDB(global.DB, &data) + _, err = global.DB.Exec( + "UPDATE musictrack "+ + "SET title=$2, description=$3, lyrics=$4, preview_url=$5 "+ + "WHERE id=$1", + track.ID, + track.Title, + track.Description, + track.Lyrics, + track.PreviewURL) if err != nil { fmt.Printf("Failed to update track %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - track.Title = data.Title - track.Description = data.Description - track.Lyrics = data.Lyrics - track.PreviewURL = data.PreviewURL - w.Header().Add("Content-Type", "application/json") err = json.NewEncoder(w).Encode(track) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return } }) } -func DeleteTrack() http.Handler { +func DeleteTrack(track model.Track) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodDelete { + if r.Method != http.MethodDelete || r.URL.Path == "/" { http.NotFound(w, r) return } - if r.URL.Path == "/" { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - var trackID = r.URL.Path[1:] - var track = global.GetTrack(trackID) - if track == nil { - http.Error(w, fmt.Sprintf("Track %s does not exist\n", trackID), http.StatusBadRequest) - return - } - - err := controller.DeleteTrackDB(global.DB, track) + _, err := global.DB.Exec( + "DELETE FROM musictrack "+ + "WHERE id=$1", + trackID) if err != nil { - fmt.Printf("Failed to delete track %s: %s\n", track.ID, err) + fmt.Printf("Failed to delete track %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return } - - // clear track from releases - for _, release := range global.Releases { - release.Tracks = func () []*model.Track { - var tracks = []*model.Track{} - for _, t := range release.Tracks { - if t.ID == track.ID { continue } - tracks = append(tracks, t) - } - return tracks - }() - } - - global.Tracks = func () []*model.Track { - var tracks = []*model.Track{} - for _, t := range global.Tracks { - if t.ID == track.ID { continue } - tracks = append(tracks, t) - } - return tracks - }() - - w.WriteHeader(http.StatusOK) - w.Write([]byte(fmt.Sprintf("Track %s has been deleted\n", track.ID))) }) } diff --git a/api/uploads.go b/api/uploads.go new file mode 100644 index 0000000..23479b7 --- /dev/null +++ b/api/uploads.go @@ -0,0 +1,49 @@ +package api + +import ( + "bufio" + "encoding/base64" + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +func HandleImageUpload(data *string, directory string, filename string) (string, error) { + split := strings.Split(*data, ";base64,") + header := split[0] + imageData, err := base64.StdEncoding.DecodeString(split[1]) + ext, _ := strings.CutPrefix(header, "data:image/") + + switch ext { + case "png": + case "jpg": + case "jpeg": + default: + return "", errors.New("Invalid image type. Allowed: .png, .jpg, .jpeg") + } + filename = fmt.Sprintf("%s.%s", filename, ext) + + // ensure directory exists + os.MkdirAll(directory, os.ModePerm) + + imagePath := filepath.Join(directory, filename) + file, err := os.Create(imagePath) + if err != nil { + return "", err + } + defer file.Close() + + buffer := bufio.NewWriter(file) + _, err = buffer.Write(imageData) + if err != nil { + return "", nil + } + + if err := buffer.Flush(); err != nil { + return "", nil + } + + return filename, nil +} diff --git a/global/data.go b/global/data.go index 755951b..b8d0c02 100644 --- a/global/data.go +++ b/global/data.go @@ -5,7 +5,6 @@ import ( "os" "strings" - "arimelody.me/arimelody.me/music/model" "github.com/jmoiron/sqlx" ) @@ -44,34 +43,3 @@ var HTTP_DOMAIN = func() string { }() var DB *sqlx.DB - -var Releases []*model.Release -var Artists []*model.Artist -var Tracks []*model.Track - -func GetRelease(id string) *model.Release { - for _, release := range Releases { - if release.ID == id { - return release - } - } - return nil -} - -func GetArtist(id string) *model.Artist { - for _, artist := range Artists { - if artist.ID == id { - return artist - } - } - return nil -} - -func GetTrack(id string) *model.Track { - for _, track := range Tracks { - if track.ID == id { - return track - } - } - return nil -} diff --git a/global/funcs.go b/global/funcs.go index 5d3f8fc..f0f3633 100644 --- a/global/funcs.go +++ b/global/funcs.go @@ -3,9 +3,6 @@ package global import ( "fmt" "net/http" - "os" - "path/filepath" - "html/template" "strconv" "time" @@ -64,39 +61,3 @@ func HTTPLog(next http.Handler) http.Handler { }) } -func ServeTemplate(page string, data any) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - lp_layout := filepath.Join("views", "layout.html") - lp_header := filepath.Join("views", "header.html") - lp_footer := filepath.Join("views", "footer.html") - lp_prideflag := filepath.Join("views", "prideflag.html") - fp := filepath.Join("views", filepath.Clean(page)) - - info, err := os.Stat(fp) - if err != nil { - if os.IsNotExist(err) { - http.NotFound(w, r) - return - } - } - - if info.IsDir() { - http.NotFound(w, r) - return - } - - template, err := template.ParseFiles(lp_layout, lp_header, lp_footer, lp_prideflag, fp) - if err != nil { - fmt.Printf("Error parsing template files: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - err = template.ExecuteTemplate(w, "layout.html", data) - if err != nil { - fmt.Printf("Error executing template: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - }) -} diff --git a/main.go b/main.go index 7707d50..a60524f 100644 --- a/main.go +++ b/main.go @@ -11,8 +11,8 @@ import ( "arimelody.me/arimelody.me/admin" "arimelody.me/arimelody.me/api" "arimelody.me/arimelody.me/global" - musicController "arimelody.me/arimelody.me/music/controller" musicView "arimelody.me/arimelody.me/music/view" + "arimelody.me/arimelody.me/templates" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" @@ -25,42 +25,18 @@ func main() { var err error global.DB, err = sqlx.Connect("postgres", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable") if err != nil { - fmt.Fprintf(os.Stderr, "unable to create database connection pool: %v\n", err) + fmt.Fprintf(os.Stderr, "FATAL: Unable to create database connection pool: %v\n", err) os.Exit(1) } global.DB.SetConnMaxLifetime(time.Minute * 3) global.DB.SetMaxOpenConns(10) global.DB.SetMaxIdleConns(10) defer global.DB.Close() - - // pull artist data from DB - global.Artists, err = musicController.PullAllArtists(global.DB) - if err != nil { - fmt.Printf("Failed to pull artists from database: %v\n", err); - panic(1) - } - fmt.Printf("%d artists loaded successfully.\n", len(global.Artists)) - - // pull track data from DB - global.Tracks, err = musicController.PullAllTracks(global.DB) - if err != nil { - fmt.Printf("Failed to pull tracks from database: %v\n", err); - panic(1) - } - fmt.Printf("%d tracks loaded successfully.\n", len(global.Tracks)) - - // pull release data from DB - global.Releases, err = musicController.PullAllReleases(global.DB) - if err != nil { - fmt.Printf("Failed to pull releases from database: %v\n", err); - panic(1) - } - fmt.Printf("%d releases loaded successfully.\n", len(global.Releases)) // start the web server! mux := createServeMux() port := DEFAULT_PORT - fmt.Printf("now serving at http://127.0.0.1:%d\n", port) + fmt.Printf("Now serving at http://127.0.0.1:%d\n", port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), global.HTTPLog(mux))) } @@ -73,7 +49,10 @@ func createServeMux() *http.ServeMux { mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler("uploads"))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" || r.URL.Path == "/index.html" { - global.ServeTemplate("index.html", nil).ServeHTTP(w, r) + err := templates.Pages["index"].Execute(w, nil) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } return } staticHandler("public").ServeHTTP(w, r) diff --git a/music/controller/artist.go b/music/controller/artist.go index 433e784..58b2d63 100644 --- a/music/controller/artist.go +++ b/music/controller/artist.go @@ -7,7 +7,18 @@ import ( // DATABASE -func PullAllArtists(db *sqlx.DB) ([]*model.Artist, error) { +func GetArtist(db *sqlx.DB, id string) (*model.Artist, error) { + var artist = model.Artist{} + + err := db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id) + if err != nil { + return nil, err + } + + return &artist, nil +} + +func GetAllArtists(db *sqlx.DB) ([]*model.Artist, error) { var artists = []*model.Artist{} err := db.Select(&artists, "SELECT * FROM artist") @@ -18,7 +29,22 @@ func PullAllArtists(db *sqlx.DB) ([]*model.Artist, error) { return artists, nil } -func CreateArtistDB(db *sqlx.DB, artist *model.Artist) error { +func GetArtistsNotOnRelease(db *sqlx.DB, release *model.Release) ([]*model.Artist, error) { + var artists = []*model.Artist{} + + err := db.Select(&artists, + "SELECT * FROM artist "+ + "WHERE id NOT IN "+ + "(SELECT artist FROM musiccredit WHERE release=$1)", + release.ID) + if err != nil { + return nil, err + } + + return artists, nil +} + +func CreateArtist(db *sqlx.DB, artist *model.Artist) error { _, err := db.Exec( "INSERT INTO artist (id, name, website, avatar) "+ "VALUES ($1, $2, $3, $4)", @@ -34,7 +60,7 @@ func CreateArtistDB(db *sqlx.DB, artist *model.Artist) error { return nil } -func UpdateArtistDB(db *sqlx.DB, artist *model.Artist) error { +func UpdateArtist(db *sqlx.DB, artist *model.Artist) error { _, err := db.Exec( "UPDATE artist "+ "SET name=$2, website=$3, avatar=$4 "+ @@ -51,7 +77,7 @@ func UpdateArtistDB(db *sqlx.DB, artist *model.Artist) error { return nil } -func DeleteArtistDB(db *sqlx.DB, artist *model.Artist) error { +func DeleteArtist(db *sqlx.DB, artist *model.Artist) error { _, err := db.Exec( "DELETE FROM artist "+ "WHERE id=$1", diff --git a/music/controller/credit.go b/music/controller/credit.go index d37e3b8..59f2fdb 100644 --- a/music/controller/credit.go +++ b/music/controller/credit.go @@ -1,43 +1,29 @@ package music import ( - "arimelody.me/arimelody.me/global" "arimelody.me/arimelody.me/music/model" "github.com/jmoiron/sqlx" ) // DATABASE -func PullReleaseCredits(db *sqlx.DB, releaseID string) ([]*model.Credit, error) { - type creditDB struct { - Artist string - Role string - Primary bool `db:"is_primary"` - } - var credit_rows = []creditDB{} - var credits = []*model.Credit{} +func GetReleaseCredits(db *sqlx.DB, release *model.Release) ([]model.Credit, error) { + var credits = []model.Credit{} - err := db.Select( - &credit_rows, - "SELECT artist, role, is_primary FROM musiccredit WHERE release=$1", - releaseID, + err := db.Select(&credits, + "SELECT artist.*,role,is_primary FROM musiccredit "+ + "JOIN artist ON artist=id "+ + "WHERE release=$1", + release.ID, ) if err != nil { return nil, err } - for _, c := range credit_rows { - credits = append(credits, &model.Credit{ - Artist: global.GetArtist(c.Artist), - Role: c.Role, - Primary: c.Primary, - }) - } - return credits, nil } -func CreateCreditDB(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { +func CreateCredit(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { _, err := db.Exec( "INSERT INTO musiccredit (release, artist, role, is_primary) "+ "VALUES ($1, $2, $3, $4)", @@ -53,7 +39,7 @@ func CreateCreditDB(db *sqlx.DB, releaseID string, artistID string, credit *mode return nil } -func UpdateCreditDB(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { +func UpdateCredit(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { _, err := db.Exec( "UPDATE musiccredit SET "+ "role=$3, is_primary=$4 "+ @@ -70,7 +56,7 @@ func UpdateCreditDB(db *sqlx.DB, releaseID string, artistID string, credit *mode return nil } -func DeleteCreditDB(db *sqlx.DB, releaseID string, artistID string) (error) { +func DeleteCredit(db *sqlx.DB, releaseID string, artistID string) (error) { _, err := db.Exec( "DELETE FROM musiccredit "+ "WHERE release=$1, artist=$2", diff --git a/music/controller/link.go b/music/controller/link.go index 1c300bf..5e0f6b1 100644 --- a/music/controller/link.go +++ b/music/controller/link.go @@ -7,14 +7,10 @@ import ( // DATABASE -func PullReleaseLinks(db *sqlx.DB, releaseID string) ([]*model.Link, error) { - var links = []*model.Link{} +func GetReleaseLinks(db *sqlx.DB, release *model.Release) ([]model.Link, error) { + var links = []model.Link{} - err := db.Select( - &links, - "SELECT name, url FROM musiclink WHERE release=$1", - releaseID, - ) + err := db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", release.ID) if err != nil { return nil, err } @@ -22,7 +18,7 @@ func PullReleaseLinks(db *sqlx.DB, releaseID string) ([]*model.Link, error) { return links, nil } -func CreateLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { +func CreateLink(db *sqlx.DB, releaseID string, link *model.Link) (error) { _, err := db.Exec( "INSERT INTO musiclink (release, name, url) "+ "VALUES ($1, $2, $3)", @@ -37,7 +33,7 @@ func CreateLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { return nil } -func UpdateLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { +func UpdateLink(db *sqlx.DB, releaseID string, link *model.Link) (error) { _, err := db.Exec( "UPDATE musiclink SET "+ "name=$2, url=$3 "+ @@ -53,7 +49,7 @@ func UpdateLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { return nil } -func DeleteLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { +func DeleteLink(db *sqlx.DB, releaseID string, link *model.Link) (error) { _, err := db.Exec( "DELETE FROM musiclink "+ "WHERE release=$1, name=$2", diff --git a/music/controller/release.go b/music/controller/release.go index 167f4b6..36c5ac6 100644 --- a/music/controller/release.go +++ b/music/controller/release.go @@ -1,17 +1,22 @@ package music import ( - "errors" - "fmt" - - "arimelody.me/arimelody.me/global" "arimelody.me/arimelody.me/music/model" "github.com/jmoiron/sqlx" ) -// DATABASE +func GetRelease(db *sqlx.DB, id string) (*model.Release, error) { + var releases = model.Release{} -func PullAllReleases(db *sqlx.DB) ([]*model.Release, error) { + err := db.Get(&releases, "SELECT * FROM musicrelease WHERE id=$1", id) + if err != nil { + return nil, err + } + + return &releases, nil +} + +func GetAllReleases(db *sqlx.DB) ([]*model.Release, error) { var releases = []*model.Release{} err := db.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") @@ -19,33 +24,15 @@ func PullAllReleases(db *sqlx.DB) ([]*model.Release, error) { return nil, err } - for _, release := range releases { - release.Credits, err = PullReleaseCredits(global.DB, release.ID) - if err != nil { - fmt.Printf("Error pulling credits for %s: %s\n", release.ID, err) - release.Credits = []*model.Credit{} - } - release.Links, err = PullReleaseLinks(global.DB, release.ID) - if err != nil { - fmt.Printf("Error pulling links for %s: %s\n", release.ID, err) - release.Links = []*model.Link{} - } - release.Tracks, err = PullReleaseTracksDB(global.DB, release) - if err != nil { - fmt.Printf("Error pulling tracks for %s: %s\n", release.ID, err) - release.Tracks = []*model.Track{} - } - } - return releases, nil } -func PullReleaseTracksDB(db *sqlx.DB, release *model.Release) ([]*model.Track, error) { - var track_rows = []string{} +func GetReleaseTracks(db *sqlx.DB, release *model.Release) ([]*model.Track, error) { var tracks = []*model.Track{} - err := db.Select(&track_rows, - "SELECT track FROM musicreleasetrack "+ + err := db.Select(&tracks, + "SELECT musictrack.* FROM musictrack "+ + "JOIN musicreleasetrack ON track=id "+ "WHERE release=$1 "+ "ORDER BY number ASC", release.ID, @@ -54,19 +41,10 @@ func PullReleaseTracksDB(db *sqlx.DB, release *model.Release) ([]*model.Track, e return nil, err } - for _, trackID := range track_rows { - var track = global.GetTrack(trackID) - if track == nil { - return nil, errors.New("Recieved a track from the DB that does not exist in memory") - } - track.Release = release - tracks = append(tracks, track) - } - return tracks, nil } -func CreateReleaseDB(db *sqlx.DB, release *model.Release) error { +func CreateRelease(db *sqlx.DB, release *model.Release) error { _, err := db.Exec( "INSERT INTO musicrelease "+ "(id, visible, title, description, type, release_date, artwork, buyname, buylink) "+ @@ -88,7 +66,7 @@ func CreateReleaseDB(db *sqlx.DB, release *model.Release) error { return nil } -func UpdateReleaseDB(db *sqlx.DB, release *model.Release) error { +func UpdateRelease(db *sqlx.DB, release *model.Release) error { _, err := db.Exec( "UPDATE musicrelease SET "+ "visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9 "+ @@ -110,7 +88,7 @@ func UpdateReleaseDB(db *sqlx.DB, release *model.Release) error { return nil } -func UpdateReleaseTracksDB(db *sqlx.DB, release *model.Release, new_tracks []*model.Track) error { +func UpdateReleaseTracks(db *sqlx.DB, release *model.Release, new_tracks []*model.Track) error { _, err := db.Exec( "DELETE FROM musicreleasetrack "+ "WHERE release=$1", @@ -137,7 +115,7 @@ func UpdateReleaseTracksDB(db *sqlx.DB, release *model.Release, new_tracks []*mo return nil } -func UpdateReleaseCreditsDB(db *sqlx.DB, release *model.Release, new_credits []*model.Credit) error { +func UpdateReleaseCredits(db *sqlx.DB, release *model.Release, new_credits []*model.Credit) error { _, err := db.Exec( "DELETE FROM musiccredit "+ "WHERE release=$1", @@ -165,7 +143,7 @@ func UpdateReleaseCreditsDB(db *sqlx.DB, release *model.Release, new_credits []* return nil } -func UpdateReleaseLinksDB(db *sqlx.DB, release *model.Release, new_links []*model.Link) error { +func UpdateReleaseLinks(db *sqlx.DB, release *model.Release, new_links []*model.Link) error { _, err := db.Exec( "DELETE FROM musiclink "+ "WHERE release=$1", @@ -192,7 +170,7 @@ func UpdateReleaseLinksDB(db *sqlx.DB, release *model.Release, new_links []*mode return nil } -func DeleteReleaseDB(db *sqlx.DB, release *model.Release) error { +func DeleteRelease(db *sqlx.DB, release *model.Release) error { _, err := db.Exec( "DELETE FROM musicrelease "+ "WHERE id=$1", @@ -204,3 +182,34 @@ func DeleteReleaseDB(db *sqlx.DB, release *model.Release) error { return nil } + +func GetFullRelease(db *sqlx.DB, release *model.Release) (*model.FullRelease, error) { + // get credits + credits, err := GetReleaseCredits(db, release) + if err != nil { + return nil, err + } + + // get tracks + dbTracks, err := GetReleaseTracks(db, release) + if err != nil { + return nil, err + } + tracks := []model.DisplayTrack{} + for i, track := range dbTracks { + tracks = append(tracks, track.MakeDisplay(i + 1)) + } + + // get links + links, err := GetReleaseLinks(db, release) + if err != nil { + return nil, err + } + + return &model.FullRelease{ + Release: release, + Tracks: tracks, + Credits: credits, + Links: links, + }, nil +} diff --git a/music/controller/track.go b/music/controller/track.go index ffa2c61..43627a7 100644 --- a/music/controller/track.go +++ b/music/controller/track.go @@ -7,10 +7,21 @@ import ( // DATABASE -func PullAllTracks(db *sqlx.DB) ([]*model.Track, error) { +func GetTrack(db *sqlx.DB, id string) (*model.Track, error) { + var track = model.Track{} + + stmt, _ := db.Preparex("SELECT * FROM musictrack WHERE id=$1") + err := stmt.Get(&track, id) + if err != nil { + return nil, err + } + return &track, nil +} + +func GetAllTracks(db *sqlx.DB) ([]*model.Track, error) { var tracks = []*model.Track{} - err := db.Select(&tracks, "SELECT id, title, description, lyrics, preview_url FROM musictrack") + err := db.Select(&tracks, "SELECT * FROM musictrack") if err != nil { return nil, err } @@ -18,6 +29,49 @@ func PullAllTracks(db *sqlx.DB) ([]*model.Track, error) { return tracks, nil } +func GetOrphanTracks(db *sqlx.DB) ([]*model.Track, error) { + var tracks = []*model.Track{} + + err := db.Select(&tracks, "SELECT * FROM musictrack WHERE id NOT IN (SELECT track FROM musicreleasetrack)") + if err != nil { + return nil, err + } + + return tracks, nil +} + +func GetTracksNotOnRelease(db *sqlx.DB, release *model.Release) ([]*model.Track, error) { + var tracks = []*model.Track{} + + err := db.Select(&tracks, + "SELECT * FROM musictrack "+ + "WHERE id NOT IN "+ + "(SELECT track FROM musicreleasetrack WHERE release=$1)", + release.ID) + if err != nil { + return nil, err + } + + return tracks, nil +} + +func GetTrackReleases(db *sqlx.DB, track *model.Track) ([]*model.Release, error) { + var releases = []*model.Release{} + + err := db.Select(&releases, + "SELECT musicrelease.* FROM musicrelease "+ + "JOIN musicreleasetrack ON release=id "+ + "WHERE track=$1 "+ + "ORDER BY release_date", + track.ID, + ) + if err != nil { + return nil, err + } + + return releases, nil +} + func PullOrphanTracks(db *sqlx.DB) ([]*model.Track, error) { var tracks = []*model.Track{} @@ -33,7 +87,7 @@ func PullOrphanTracks(db *sqlx.DB) ([]*model.Track, error) { return tracks, nil } -func CreateTrackDB(db *sqlx.DB, track *model.Track) (string, error) { +func CreateTrack(db *sqlx.DB, track *model.Track) (string, error) { var trackID string err := db.QueryRow( "INSERT INTO musictrack (title, description, lyrics, preview_url) "+ @@ -51,7 +105,7 @@ func CreateTrackDB(db *sqlx.DB, track *model.Track) (string, error) { return trackID, nil } -func UpdateTrackDB(db *sqlx.DB, track *model.Track) error { +func UpdateTrack(db *sqlx.DB, track *model.Track) error { _, err := db.Exec( "UPDATE musictrack "+ "SET title=$2, description=$3, lyrics=$4, preview_url=$5 "+ @@ -69,7 +123,7 @@ func UpdateTrackDB(db *sqlx.DB, track *model.Track) error { return nil } -func DeleteTrackDB(db *sqlx.DB, track *model.Track) error { +func DeleteTrack(db *sqlx.DB, track *model.Track) error { _, err := db.Exec( "DELETE FROM musictrack "+ "WHERE id=$1", diff --git a/music/model/artist.go b/music/model/artist.go index 64e52d9..837aee8 100644 --- a/music/model/artist.go +++ b/music/model/artist.go @@ -1,8 +1,10 @@ package model +import "strings" + type ( Artist struct { - ID string `json:"id"` + ID string `json:"id"` Name string `json:"name"` Website string `json:"website"` Avatar string `json:"avatar"` @@ -14,8 +16,61 @@ func (artist Artist) GetWebsite() string { } func (artist Artist) GetAvatar() string { - if artist.Avatar == "" { - return "/img/default-avatar.png" - } + if artist.Avatar == "" { + return "/img/default-avatar.png" + } return artist.Avatar } + +func (release FullRelease) GetUniqueArtists(only_primary bool) []*Artist { + var artists = []*Artist{} + + for _, credit := range release.Credits { + if only_primary && !credit.Primary { + continue + } + + exists := false + for _, a := range artists { + if a.ID == credit.Artist.ID { + exists = true + break + } + } + + if exists { + continue + } + + artists = append(artists, &credit.Artist) + } + + return artists +} + +func (release FullRelease) GetUniqueArtistNames(only_primary bool) []string { + var names = []string{} + for _, artist := range release.GetUniqueArtists(only_primary) { + names = append(names, artist.Name) + } + + return names +} + +func (release FullRelease) PrintArtists(only_primary bool, ampersand bool) string { + names := release.GetUniqueArtistNames(only_primary) + + if len(names) == 0 { + return "Unknown Artist" + } else if len(names) == 1 { + return names[0] + } + + if ampersand { + res := strings.Join(names[:len(names)-1], ", ") + res += " & " + names[len(names)-1] + return res + } else { + return strings.Join(names[:], ", ") + } +} diff --git a/music/model/credit.go b/music/model/credit.go index cb12e7d..df83326 100644 --- a/music/model/credit.go +++ b/music/model/credit.go @@ -1,7 +1,7 @@ package model type Credit struct { - Artist *Artist `json:"artist"` + Artist `json:"artist"` Role string `json:"role"` Primary bool `json:"primary" db:"is_primary"` } diff --git a/music/model/link.go b/music/model/link.go index fd38556..8b48ced 100644 --- a/music/model/link.go +++ b/music/model/link.go @@ -6,8 +6,8 @@ import ( ) type Link struct { - Name string `json:"name"` - URL string `json:"url"` + Name string `json:"name"` + URL string `json:"url"` } func (link Link) NormaliseName() string { diff --git a/music/model/release.go b/music/model/release.go index 12eb15c..9df6a3b 100644 --- a/music/model/release.go +++ b/music/model/release.go @@ -1,25 +1,31 @@ package model import ( - "strings" "time" ) type ( ReleaseType string - Release struct { - ID string `json:"id"` - Visible bool `json:"visible"` - Title string `json:"title"` - Description string `json:"description"` - ReleaseType ReleaseType `json:"type" db:"type"` - ReleaseDate time.Time `json:"releaseDate" db:"release_date"` - Artwork string `json:"artwork"` - Buyname string `json:"buyname"` - Buylink string `json:"buylink"` - Links []*Link `json:"links"` - Credits []*Credit `json:"credits"` - Tracks []*Track `json:"tracks"` + + Release struct { + ID string `json:"id"` + Visible bool `json:"visible"` + Title string `json:"title"` + Description string `json:"description"` + ReleaseType ReleaseType `json:"type" db:"type"` + ReleaseDate time.Time `json:"releaseDate" db:"release_date"` + Artwork string `json:"artwork"` + Buyname string `json:"buyname"` + Buylink string `json:"buylink"` + Copyright string `json:"copyright" db:"copyright"` + CopyrightURL string `json:"copyrightURL" db:"copyrighturl"` + } + + FullRelease struct { + *Release + Tracks []DisplayTrack + Credits []Credit + Links []Link } ) @@ -52,63 +58,10 @@ func (release Release) GetArtwork() string { return release.Artwork } -func (release Release) IsSingle() bool { +func (release FullRelease) IsSingle() bool { return len(release.Tracks) == 1; } func (release Release) IsReleased() bool { return release.ReleaseDate.Before(time.Now()) } - -func (release Release) GetUniqueArtists(only_primary bool) []Artist { - var artists = []Artist{} - - for _, credit := range release.Credits { - if only_primary && !credit.Primary { - continue - } - - exists := false - for _, a := range artists { - if a.ID == credit.Artist.ID { - exists = true - break - } - } - - if exists { - continue - } - - artists = append(artists, *credit.Artist) - } - - return artists -} - -func (release Release) GetUniqueArtistNames(only_primary bool) []string { - var names = []string{} - for _, artist := range release.GetUniqueArtists(only_primary) { - names = append(names, artist.Name) - } - - return names -} - -func (release Release) PrintArtists(only_primary bool, ampersand bool) string { - names := release.GetUniqueArtistNames(only_primary) - - if len(names) == 0 { - return "Unknown Artist" - } else if len(names) == 1 { - return names[0] - } - - if ampersand { - res := strings.Join(names[:len(names)-1], ", ") - res += " & " + names[len(names)-1] - return res - } else { - return strings.Join(names[:], ", ") - } -} diff --git a/music/model/track.go b/music/model/track.go index 4870d9e..2bb8bf4 100644 --- a/music/model/track.go +++ b/music/model/track.go @@ -1,10 +1,30 @@ package model -type Track struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Lyrics string `json:"lyrics"` - PreviewURL string `json:"previewURL" db:"preview_url"` - Release *Release `json:"-" db:"-"` +import ( + "html/template" + "strings" +) + +type ( + Track struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Lyrics string `json:"lyrics"` + PreviewURL string `json:"previewURL" db:"preview_url"` + } + + DisplayTrack struct { + *Track + Lyrics template.HTML + Number int + } +) + +func (track Track) MakeDisplay(number int) DisplayTrack { + return DisplayTrack{ + Track: &track, + Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
    ", -1)), + Number: number, + } } diff --git a/music/view/music.go b/music/view/music.go index aa9e255..49bfb52 100644 --- a/music/view/music.go +++ b/music/view/music.go @@ -1,10 +1,13 @@ package view import ( + "fmt" "net/http" "arimelody.me/arimelody.me/global" + music "arimelody.me/arimelody.me/music/controller" "arimelody.me/arimelody.me/music/model" + "arimelody.me/arimelody.me/templates" ) // HTTP HANDLER METHODS @@ -17,7 +20,15 @@ func Handler() http.Handler { ServeCatalog().ServeHTTP(w, r) return } - ServeGateway().ServeHTTP(w, r) + + var release model.Release + err := global.DB.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", r.URL.Path[1:]) + if err != nil { + http.NotFound(w, r) + return + } + + ServeGateway(release).ServeHTTP(w, r) })) return mux @@ -25,71 +36,30 @@ func Handler() http.Handler { func ServeCatalog() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - releases := []model.Release{} - for _, r := range global.Releases { - if r.Visible { - release := *r - if !release.IsReleased() { - release.ReleaseType = model.Upcoming - } - releases = append(releases, release) - } - } - - global.ServeTemplate("music.html", releases).ServeHTTP(w, r) - }) -} - -/* -func ServeArtwork() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - http.NotFound(w, r) - return - } - - if !strings.HasSuffix(r.URL.Path, ".png") { - http.NotFound(w, r) - return - } - - releaseID := r.URL.Path[1:len(r.URL.Path) - 4] - var release = GetRelease(releaseID) - if release == nil { - http.NotFound(w, r) - return - } - - // only allow authorised users to view unreleased releases - authorised := r.Context().Value("role") != nil && r.Context().Value("role") == "admin" - if !release.IsReleased() && !authorised { - admin.MustAuthorise(ServeArtwork()).ServeHTTP(w, r) - return - } - - fp := filepath.Join("data", "music-artwork", releaseID + ".png") - info, err := os.Stat(fp) - if err != nil { - if os.IsNotExist(err) { - http.NotFound(w, r) - return - } - } - length := info.Size() - - file, err := os.Open(fp) + dbReleases, err := music.GetAllReleases(global.DB) if err != nil { + fmt.Printf("FATAL: Failed to pull releases for catalog: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - defer file.Close() + releases := []*model.FullRelease{} + for _, dbRelease := range dbReleases { + if !dbRelease.Visible { continue } + if !dbRelease.IsReleased() { + dbRelease.ReleaseType = model.Upcoming + } + release, err := music.GetFullRelease(global.DB, dbRelease) + if err != nil { + fmt.Printf("FATAL: Failed to pull full release for %s: %s\n", dbRelease.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + releases = append(releases, release) + } - var bytes = make([]byte, length) - file.Read(bytes) - - w.Header().Add("Content-Type", "image/png") - w.WriteHeader(http.StatusOK) - w.Write(bytes) + err = templates.Pages["music"].Execute(w, releases) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } }) } -*/ diff --git a/music/view/release.go b/music/view/release.go index 6b9b181..266f06e 100644 --- a/music/view/release.go +++ b/music/view/release.go @@ -3,59 +3,42 @@ package view import ( "encoding/json" "fmt" - "html/template" "net/http" - "strings" "arimelody.me/arimelody.me/admin" "arimelody.me/arimelody.me/global" "arimelody.me/arimelody.me/music/model" -) - -type ( - gatewayTrack struct { - *model.Track - Lyrics template.HTML - Number int - } - - gatewayRelease struct { - *model.Release - Tracks []gatewayTrack - Authorised bool - } + db "arimelody.me/arimelody.me/music/controller" + "arimelody.me/arimelody.me/templates" ) // HTTP HANDLERS -func ServeRelease() http.Handler { +func ServeRelease(release model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - http.NotFound(w, r) - return - } - - releaseID := r.URL.Path[1:] - var releaseRef = global.GetRelease(releaseID) - if releaseRef == nil { - http.NotFound(w, r) - return - } - var release = *releaseRef - // only allow authorised users to view hidden releases authorised := admin.GetSession(r) != nil if !authorised && !release.Visible { http.NotFound(w, r) return } - if !authorised && !release.IsReleased() { - release.Tracks = nil - release.Credits = nil + + fullRelease := &model.FullRelease{ + Release: &release, + } + + if authorised || release.IsReleased() { + fullerRelease, err := db.GetFullRelease(global.DB, &release) + if err != nil { + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + fullRelease = fullerRelease } w.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(release) + err := json.NewEncoder(w).Encode(fullRelease) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -63,42 +46,34 @@ func ServeRelease() http.Handler { }) } -func ServeGateway() http.Handler { +func ServeGateway(release model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - http.Redirect(w, r, "/music", http.StatusPermanentRedirect) - return - } - - id := r.URL.Path[1:] - release := global.GetRelease(id) - if release == nil { - http.NotFound(w, r) - return - } - // only allow authorised users to view hidden releases authorised := admin.GetSession(r) != nil - if !release.Visible && !authorised { + if !authorised && !release.Visible { http.NotFound(w, r) return } - tracks := []gatewayTrack{} - for i, track := range release.Tracks { - tracks = append(tracks, gatewayTrack{ - Track: track, - Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
    ", -1)), - Number: i + 1, - }) + fullRelease := &model.FullRelease{ + Release: &release, } - lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK} + if authorised || release.IsReleased() { + fullerRelease, err := db.GetFullRelease(global.DB, &release) + if err != nil { + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + fullRelease = fullerRelease + } - global.ServeTemplate("music-gateway.html", gatewayRelease{release, tracks, authorised}).ServeHTTP(&lrw, r) + err := templates.Pages["music-gateway"].Execute(w, fullRelease) - if lrw.Code != http.StatusOK { - fmt.Printf("Error rendering music gateway for %s\n", id) + if err != nil { + fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } }) diff --git a/schema.sql b/schema.sql index 1a7533c..746511a 100644 --- a/schema.sql +++ b/schema.sql @@ -21,7 +21,9 @@ CREATE TABLE public.musicrelease ( release_date TIMESTAMP NOT NULL, artwork text, buyname text, - buylink text + buylink text, + copyright text, + copyrightURL text ); ALTER TABLE public.musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); diff --git a/templates/templates.go b/templates/templates.go new file mode 100644 index 0000000..094e61d --- /dev/null +++ b/templates/templates.go @@ -0,0 +1,33 @@ +package templates + +import ( + "html/template" + "path/filepath" +) + +var Pages = map[string]*template.Template{ + "index": template.Must(template.ParseFiles( + filepath.Join("views", "layout.html"), + filepath.Join("views", "header.html"), + filepath.Join("views", "footer.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("views", "index.html"), + )), + "music": template.Must(template.ParseFiles( + filepath.Join("views", "layout.html"), + filepath.Join("views", "header.html"), + filepath.Join("views", "footer.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("views", "music.html"), + )), + "music-gateway": template.Must(template.ParseFiles( + filepath.Join("views", "layout.html"), + filepath.Join("views", "header.html"), + filepath.Join("views", "footer.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("views", "music-gateway.html"), + )), +} + +var Components = map[string]*template.Template{ +}