From 05e16a0867e4b43e842f3ab7d965ea2e53e32157 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 3 Aug 2024 23:24:15 +0100 Subject: [PATCH] i think that's all the api endpoints! Signed-off-by: ari melody --- admin/http.go | 7 + api/api.go | 73 ++++++-- api/artist.go | 138 ++++++++++++--- api/musiccredit.go | 65 ------- api/musiclink.go | 66 ------- api/release.go | 315 ++++++++++++++++++++++++++++++--- api/track.go | 157 +++++++++++++++- global/data.go | 9 + main.go | 16 +- music/controller/link.go | 6 +- music/controller/release.go | 130 +++++++++++++- music/controller/track.go | 15 ++ music/model/track.go | 11 +- music/view/release.go | 23 ++- public/style/music-gateway.css | 2 +- views/admin/index.html | 2 +- views/music-gateway.html | 6 +- 17 files changed, 810 insertions(+), 231 deletions(-) delete mode 100644 api/musiccredit.go delete mode 100644 api/musiclink.go diff --git a/admin/http.go b/admin/http.go index dac0462..da25279 100644 --- a/admin/http.go +++ b/admin/http.go @@ -39,6 +39,13 @@ func Handler() http.Handler { Tracks []*musicModel.Track } + var orphan_tracks = []*musicModel.Track{} + for _, track := range global.Tracks { + if track.Release == nil { + orphan_tracks = append(orphan_tracks, track) + } + } + serveTemplate("index.html", IndexData{ Releases: global.Releases, Artists: global.Artists, diff --git a/api/api.go b/api/api.go index 8dab6a4..5876e3a 100644 --- a/api/api.go +++ b/api/api.go @@ -10,54 +10,95 @@ import ( func Handler() http.Handler { mux := http.NewServeMux() - mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", ServeArtist())) + // ARTIST ENDPOINTS + + mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + // GET /api/v1/artist/{id} + ServeArtist().ServeHTTP(w, r) + case http.MethodPut: + // PUT /api/v1/artist/{id} (admin) + admin.MustAuthorise(UpdateArtist()).ServeHTTP(w, r) + case http.MethodDelete: + // DELETE /api/v1/artist/{id} (admin) + admin.MustAuthorise(DeleteArtist()).ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }))) mux.Handle("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: + // GET /api/v1/artist ServeAllArtists().ServeHTTP(w, r) - return case http.MethodPost: + // POST /api/v1/artist (admin) admin.MustAuthorise(CreateArtist()).ServeHTTP(w, r) - return default: http.NotFound(w, r) - return } })) + // RELEASE ENDPOINTS + mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: + // GET /api/v1/music/{id} music.ServeRelease().ServeHTTP(w, r) - return + case http.MethodPut: + // PUT /api/v1/music/{id} (admin) + admin.MustAuthorise(UpdateRelease()).ServeHTTP(w, r) case http.MethodDelete: + // DELETE /api/v1/music/{id} (admin) admin.MustAuthorise(DeleteRelease()).ServeHTTP(w, r) - return default: http.NotFound(w, r) - return } }))) mux.Handle("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: + // GET /api/v1/music ServeCatalog().ServeHTTP(w, r) - return case http.MethodPost: + // POST /api/v1/music (admin) admin.MustAuthorise(CreateRelease()).ServeHTTP(w, r) - return - case http.MethodDelete: - admin.MustAuthorise(DeleteRelease()).ServeHTTP(w, r) - return default: http.NotFound(w, r) - return } })) - mux.Handle("/v1/musiccredit", CreateMusicCredit()) - mux.Handle("/v1/musiclink", CreateMusicLink()) - mux.Handle("/v1/track", CreateTrack()) + // TRACK ENDPOINTS + + mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + // GET /api/v1/track/{id} (admin) + admin.MustAuthorise(ServeTrack()).ServeHTTP(w, r) + case http.MethodPut: + // PUT /api/v1/track/{id} (admin) + admin.MustAuthorise(UpdateTrack()).ServeHTTP(w, r) + case http.MethodDelete: + // DELETE /api/v1/track/{id} (admin) + admin.MustAuthorise(DeleteTrack()).ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }))) + mux.Handle("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + // GET /api/v1/track (admin) + admin.MustAuthorise(ServeAllTracks()).ServeHTTP(w, r) + case http.MethodPost: + // POST /api/v1/track (admin) + admin.MustAuthorise(CreateTrack()).ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + })) return mux } diff --git a/api/artist.go b/api/artist.go index 067f1f3..e0e984b 100644 --- a/api/artist.go +++ b/api/artist.go @@ -12,25 +12,8 @@ import ( func ServeAllArtists() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - type ( - creditJSON struct { - Role string `json:"role"` - Primary bool `json:"primary"` - } - ) - - var artists = []model.Artist{} - for _, artist := range global.Artists { - artists = append(artists, model.Artist{ - ID: artist.ID, - Name: artist.Name, - Website: artist.Website, - Avatar: artist.Avatar, - }) - } - w.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(artists) + err := json.NewEncoder(w).Encode(global.Artists) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -55,24 +38,24 @@ func ServeArtist() http.Handler { Credits map[string]creditJSON `json:"credits"` } ) - var res = artistJSON{} + var artist = artistJSON{} - res.ID = r.URL.Path[1:] - var artist = global.GetArtist(res.ID) - if artist == nil { + artist.ID = r.URL.Path[1:] + var a = global.GetArtist(artist.ID) + if a == nil { http.NotFound(w, r) return } - res.Name = artist.Name - res.Website = artist.Website - res.Credits = make(map[string]creditJSON) + 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 != res.ID { + if credit.Artist.ID != artist.ID { continue } - res.Credits[release.ID] = creditJSON{ + artist.Credits[release.ID] = creditJSON{ Role: credit.Role, Primary: credit.Primary, } @@ -80,7 +63,7 @@ func ServeArtist() http.Handler { } w.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(res) + err := json.NewEncoder(w).Encode(artist) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -136,5 +119,104 @@ func CreateArtist() http.Handler { 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 { + 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 model.Artist + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + fmt.Printf("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 == "" { data.ID = artist.ID } + + if data.Name == "" { + http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest) + return + } + + err = controller.UpdateArtistDB(global.DB, &data) + if err != nil { + fmt.Printf("Failed to update artist %s: %s\n", artist.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + artist.ID = data.ID + artist.Name = data.Name + artist.Website = data.Website + artist.Avatar = data.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 { + 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) + if err != nil { + fmt.Printf("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/musiccredit.go b/api/musiccredit.go deleted file mode 100644 index af0c3b1..0000000 --- a/api/musiccredit.go +++ /dev/null @@ -1,65 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "net/http" - - "arimelody.me/arimelody.me/global" - "arimelody.me/arimelody.me/music/model" - controller "arimelody.me/arimelody.me/music/controller" -) - -func CreateMusicCredit() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.NotFound(w, r) - return - } - - type creditJSON struct { - Release string - Artist string - Role string - Primary bool - } - - var data creditJSON - err := json.NewDecoder(r.Body).Decode(&data) - if err != nil { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - var release = global.GetRelease(data.Release) - if release == nil { - http.Error(w, fmt.Sprintf("Release %s does not exist\n", data.Release), 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 - } - - var credit = model.Credit{ - Artist: artist, - Role: data.Role, - Primary: data.Primary, - } - - err = controller.CreateCreditDB(global.DB, release.ID, artist.ID, &credit) - if err != nil { - fmt.Printf("Failed to create credit: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - release.Credits = append(release.Credits, &credit) - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - err = json.NewEncoder(w).Encode(credit) - }) -} diff --git a/api/musiclink.go b/api/musiclink.go deleted file mode 100644 index 95482ba..0000000 --- a/api/musiclink.go +++ /dev/null @@ -1,66 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "net/http" - - "arimelody.me/arimelody.me/global" - "arimelody.me/arimelody.me/music/model" - controller "arimelody.me/arimelody.me/music/controller" -) - -func CreateMusicLink() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.NotFound(w, r) - return - } - - type linkJSON struct { - Release string - Name string - URL string - } - - var data linkJSON - err := json.NewDecoder(r.Body).Decode(&data) - if err != nil { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - if data.Release == "" { - http.Error(w, "Release cannot be empty\n", http.StatusBadRequest) - return - } - if data.Name == "" { - http.Error(w, "Link name cannot be empty\n", http.StatusBadRequest) - return - } - - var release = global.GetRelease(data.Release) - if release == nil { - http.Error(w, fmt.Sprintf("Release %s does not exist\n", data.Release), http.StatusBadRequest) - return - } - - var link = model.Link{ - Name: data.Name, - URL: data.URL, - } - - err = controller.CreateLinkDB(global.DB, release.ID, &link) - if err != nil { - fmt.Printf("Failed to create link: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - release.Links = append(release.Links, &link) - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - err = json.NewEncoder(w).Encode(release.Links) - }) -} diff --git a/api/release.go b/api/release.go index ea0c862..3011c9f 100644 --- a/api/release.go +++ b/api/release.go @@ -4,27 +4,62 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "time" "arimelody.me/arimelody.me/admin" "arimelody.me/arimelody.me/global" - "arimelody.me/arimelody.me/music/model" controller "arimelody.me/arimelody.me/music/controller" + "arimelody.me/arimelody.me/music/model" ) +type releaseBodyJSON struct { + ID string `json:"id"` + Visible bool `json:"visible"` + 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"` +} + func ServeCatalog() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - releases := []*model.Release{} + 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{} authorised := admin.GetSession(r) != nil for _, release := range global.Releases { - if !release.IsReleased() && !authorised { + if !release.Visible && !authorised { continue } - releases = append(releases, release) + 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(releases) + err := json.NewEncoder(w).Encode(catalog) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -39,19 +74,7 @@ func CreateRelease() http.Handler { return } - type PostReleaseBody struct { - ID string `json:"id"` - Visible bool `json:"visible"` - 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"` - } - - var data PostReleaseBody + var data releaseBodyJSON err := json.NewDecoder(r.Body).Decode(&data) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) @@ -66,10 +89,6 @@ func CreateRelease() http.Handler { http.Error(w, "Release title cannot be empty\n", http.StatusBadRequest) return } - if data.ReleaseDate.Unix() == 0 { - http.Error(w, "Release date cannot be empty or 0\n", http.StatusBadRequest) - return - } if global.GetRelease(data.ID) != nil { http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest) @@ -110,18 +129,259 @@ func CreateRelease() http.Handler { }) } -func DeleteRelease() http.Handler { +func UpdateRelease() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodDelete { + if r.URL.Path == "/" { http.NotFound(w, r) return } - if r.URL.Path == "/" { + 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 + } + + if data.ID == "" { data.ID = release.ID } + + if data.Title == "" { + http.Error(w, "Release title cannot be blank\n", http.StatusBadRequest) + return + } + + var new_release = model.Release{ + ID: data.ID, + Visible: data.Visible, + Title: data.Title, + Description: data.Description, + ReleaseType: data.ReleaseType, + ReleaseDate: data.ReleaseDate, + Artwork: data.Artwork, + Buyname: data.Buyname, + Buylink: data.Buylink, + } + + err = controller.UpdateReleaseDB(global.DB, release) + 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 = new_release.ID + release.Visible = new_release.Visible + release.Title = new_release.Title + release.Description = new_release.Description + release.ReleaseType = new_release.ReleaseType + release.ReleaseDate = new_release.ReleaseDate + release.Artwork = new_release.Artwork + release.Buyname = new_release.Buyname + release.Buylink = new_release.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 + } + } + + if len(segments) == 2 { + switch segments[1] { + case "tracks": + UpdateReleaseTracks(release).ServeHTTP(w, r) + case "credits": + UpdateReleaseCredits(release).ServeHTTP(w, r) + case "links": + UpdateReleaseLinks(release).ServeHTTP(w, r) + } + return + } + + 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 { + http.NotFound(w, r) + return + } + + var trackIDs = []string{} + err := json.NewDecoder(r.Body).Decode(&trackIDs) + if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 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) + } + + err = controller.UpdateReleaseTracksDB(global.DB, release, new_tracks) + 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 { + 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) + 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 + } + + 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 + } + + release.Credits = credits + + 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 UpdateReleaseLinks(release *model.Release) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + http.NotFound(w, r) + return + } + + var links = []*model.Link{} + err := json.NewDecoder(r.Body).Decode(&links) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + err = 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 + } + + release.Links = links + + 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 DeleteRelease() 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 { @@ -131,6 +391,7 @@ func DeleteRelease() http.Handler { err := controller.DeleteReleaseDB(global.DB, release) 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 } @@ -138,13 +399,13 @@ func DeleteRelease() http.Handler { global.Releases = func () []*model.Release { var releases = []*model.Release{} for _, r := range global.Releases { - if r.ID == releaseID { continue } + if r.ID == release.ID { continue } releases = append(releases, r) } return releases }() - w.WriteHeader(200) + 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 b9e8de5..cf0cab3 100644 --- a/api/track.go +++ b/api/track.go @@ -10,6 +10,54 @@ import ( 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 + // }) + // } + + w.Header().Add("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(global.Tracks) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + }) +} + +func ServeTrack() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + ServeAllTracks().ServeHTTP(w, r) + return + } + + var trackID = r.URL.Path[1:] + var track = global.GetTrack(trackID) + if track == nil { + http.NotFound(w, r) + return + } + + 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 CreateTrack() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -31,7 +79,7 @@ func CreateTrack() http.Handler { trackID, err := controller.CreateTrackDB(global.DB, &track) if err != nil { - fmt.Printf("Failed to create credit: %s\n", err) + fmt.Printf("Failed to create track: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -44,3 +92,110 @@ func CreateTrack() http.Handler { err = json.NewEncoder(w).Encode(track) }) } + +func UpdateTrack() 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 model.Track + err := json.NewDecoder(r.Body).Decode(&data) + 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) + return + } + + data.ID = trackID + + if data.Title == "" { + http.Error(w, "Track title cannot be blank\n", http.StatusBadRequest) + return + } + + err = controller.UpdateTrackDB(global.DB, &data) + 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 { + 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 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) + if err != nil { + fmt.Printf("Failed to delete track %s: %s\n", track.ID, 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/global/data.go b/global/data.go index 1ab90fa..20ec30e 100644 --- a/global/data.go +++ b/global/data.go @@ -38,3 +38,12 @@ func GetArtist(id string) *model.Artist { } return nil } + +func GetTrack(id string) *model.Track { + for _, track := range Tracks { + if track.ID == id { + return track + } + } + return nil +} diff --git a/main.go b/main.go index 73733ef..7707d50 100644 --- a/main.go +++ b/main.go @@ -41,14 +41,6 @@ func main() { } fmt.Printf("%d artists loaded successfully.\n", len(global.Artists)) - // 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)) - // pull track data from DB global.Tracks, err = musicController.PullAllTracks(global.DB) if err != nil { @@ -57,6 +49,14 @@ func main() { } 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 diff --git a/music/controller/link.go b/music/controller/link.go index 81e77b7..1c300bf 100644 --- a/music/controller/link.go +++ b/music/controller/link.go @@ -1,8 +1,8 @@ package music import ( - "arimelody.me/arimelody.me/music/model" - "github.com/jmoiron/sqlx" + "arimelody.me/arimelody.me/music/model" + "github.com/jmoiron/sqlx" ) // DATABASE @@ -12,7 +12,7 @@ func PullReleaseLinks(db *sqlx.DB, releaseID string) ([]*model.Link, error) { err := db.Select( &links, - "SELECT * FROM musiclink WHERE release=$1", + "SELECT name, url FROM musiclink WHERE release=$1", releaseID, ) if err != nil { diff --git a/music/controller/release.go b/music/controller/release.go index 0a672cd..55eb51a 100644 --- a/music/controller/release.go +++ b/music/controller/release.go @@ -1,6 +1,7 @@ package music import ( + "errors" "fmt" "arimelody.me/arimelody.me/global" @@ -11,30 +12,60 @@ import ( // DATABASE func PullAllReleases(db *sqlx.DB) ([]*model.Release, error) { - var release_rows = []*model.Release{} var releases = []*model.Release{} - err := db.Select(&release_rows, "SELECT * FROM musicrelease ORDER BY release_date DESC") + err := db.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") if err != nil { return nil, err } - for _, release := range release_rows { + 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, _ = PullReleaseLinks(global.DB, release.ID) + 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{} } - release.Tracks = make([]*model.Track, 0) - releases = append(releases, release) } return releases, nil } +func PullReleaseTracksDB(db *sqlx.DB, release *model.Release) ([]*model.Track, error) { + var track_rows = []string{} + var tracks = []*model.Track{} + + err := db.Select(&track_rows, + "SELECT track FROM musicreleasetrack "+ + "WHERE release=$1 "+ + "ORDER BY number DESC", + release.ID, + ) + if err != nil { + 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 { _, err := db.Exec( "INSERT INTO musicrelease "+ @@ -60,10 +91,11 @@ func CreateReleaseDB(db *sqlx.DB, release *model.Release) error { func UpdateReleaseDB(db *sqlx.DB, release *model.Release) error { _, err := db.Exec( "UPDATE musicrelease SET "+ - "title=$2, description=$3, type=$4, release_date=$5, artwork=$6, buyname=$7, buylink=$8) "+ - "VALUES ($2, $3, $4, $5, $6, $7, $8) "+ + "visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9) "+ + "VALUES ($2, $3, $4, $5, $6, $7, $8, $9) "+ "WHERE id=$1", release.ID, + release.Visible, release.Title, release.Description, release.ReleaseType, @@ -79,6 +111,88 @@ func UpdateReleaseDB(db *sqlx.DB, release *model.Release) error { return nil } +func UpdateReleaseTracksDB(db *sqlx.DB, release *model.Release, new_tracks []*model.Track) error { + _, err := db.Exec( + "DELETE FROM musicreleasetrack "+ + "WHERE release=$1", + release.ID, + ) + if err != nil { + return err + } + + for i, track := range new_tracks { + _, err = db.Exec( + "INSERT INTO musicreleasetrack "+ + "(release, track, number) "+ + "VALUES ($1, $2, $3)", + release.ID, + track.ID, + i, + ) + if err != nil { + return err + } + } + + return nil +} + +func UpdateReleaseCreditsDB(db *sqlx.DB, release *model.Release, new_credits []*model.Credit) error { + _, err := db.Exec( + "DELETE FROM musiccredit "+ + "WHERE release=$1", + release.ID, + ) + if err != nil { + return err + } + + for _, credit := range new_credits { + _, err = db.Exec( + "INSERT INTO musiccredit "+ + "(release, artist, role, is_primary) "+ + "VALUES ($1, $2, $3, $4)", + release.ID, + credit.Artist.ID, + credit.Role, + credit.Primary, + ) + if err != nil { + return err + } + } + + return nil +} + +func UpdateReleaseLinksDB(db *sqlx.DB, release *model.Release, new_links []*model.Link) error { + _, err := db.Exec( + "DELETE FROM musiclink "+ + "WHERE release=$1", + release.ID, + ) + if err != nil { + return err + } + + for _, link := range new_links { + _, err = db.Exec( + "INSERT INTO musiclink "+ + "(release, name, url) "+ + "VALUES ($1, $2, $3)", + release.ID, + link.Name, + link.URL, + ) + if err != nil { + return err + } + } + + return nil +} + func DeleteReleaseDB(db *sqlx.DB, release *model.Release) error { _, err := db.Exec( "DELETE FROM musicrelease "+ diff --git a/music/controller/track.go b/music/controller/track.go index a5f0163..b5a329e 100644 --- a/music/controller/track.go +++ b/music/controller/track.go @@ -18,6 +18,21 @@ func PullAllTracks(db *sqlx.DB) ([]*model.Track, error) { return tracks, nil } +func PullOrphanTracks(db *sqlx.DB) ([]*model.Track, error) { + var tracks = []*model.Track{} + + err := db.Select(&tracks, + "SELECT id, title, description, lyrics, preview_url FROM musictrack "+ + "WHERE id NOT IN "+ + "(SELECT track FROM musicreleasetrack)", + ) + if err != nil { + return nil, err + } + + return tracks, nil +} + func CreateTrackDB(db *sqlx.DB, track *model.Track) (string, error) { var trackID string err := db.QueryRow( diff --git a/music/model/track.go b/music/model/track.go index 176056e..4870d9e 100644 --- a/music/model/track.go +++ b/music/model/track.go @@ -1,9 +1,10 @@ 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"` + 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:"-"` } diff --git a/music/view/release.go b/music/view/release.go index 0b480ce..7c236fa 100644 --- a/music/view/release.go +++ b/music/view/release.go @@ -7,6 +7,7 @@ import ( "arimelody.me/arimelody.me/admin" "arimelody.me/arimelody.me/global" + "arimelody.me/arimelody.me/music/model" ) // HTTP HANDLERS @@ -48,6 +49,18 @@ func ServeGateway() http.Handler { return } + type ( + GatewayTrack struct { + *model.Track + Number int + } + + GatewayRelease struct { + *model.Release + Tracks []GatewayTrack + } + ) + id := r.URL.Path[1:] release := global.GetRelease(id) if release == nil { @@ -55,6 +68,14 @@ func ServeGateway() http.Handler { return } + tracks := []GatewayTrack{} + for i, track := range release.Tracks { + tracks = append([]GatewayTrack{GatewayTrack{ + Track: track, + Number: len(release.Tracks) - i, + }}, tracks...) + } + // only allow authorised users to view unreleased releases authorised := admin.GetSession(r) != nil if !release.IsReleased() && !authorised { @@ -64,7 +85,7 @@ func ServeGateway() http.Handler { lrw := global.LoggingResponseWriter{w, http.StatusOK} - global.ServeTemplate("music-gateway.html", release).ServeHTTP(&lrw, r) + global.ServeTemplate("music-gateway.html", GatewayRelease{release, tracks}).ServeHTTP(&lrw, r) if lrw.Code != http.StatusOK { fmt.Printf("Error rendering music gateway for %s\n", id) diff --git a/public/style/music-gateway.css b/public/style/music-gateway.css index 27df484..a95610a 100644 --- a/public/style/music-gateway.css +++ b/public/style/music-gateway.css @@ -429,7 +429,7 @@ div#extras ul li a.active { } #tracks details[open] { - margin-bottom: 2em; + margin-bottom: 1.5em; } #tracks summary { diff --git a/views/admin/index.html b/views/admin/index.html index a5bbef3..8724996 100644 --- a/views/admin/index.html +++ b/views/admin/index.html @@ -26,7 +26,7 @@

{{$Release.Title}} {{$Release.GetReleaseYear}}

{{$Release.PrintArtists true true}}

-

{{$Release.ReleaseType}}

+

{{$Release.ReleaseType}} ({{len $Release.Tracks}} tracks)

Edit Gateway diff --git a/views/music-gateway.html b/views/music-gateway.html index 193134b..821a552 100644 --- a/views/music-gateway.html +++ b/views/music-gateway.html @@ -1,6 +1,6 @@ {{define "head"}} {{.Title}} - {{.PrintArtists true true}} - + @@ -112,7 +112,11 @@ {{range $i, $track := .Tracks}}
{{$track.Number}}. {{$track.Title}} + {{if $track.Lyrics}} {{$track.Lyrics}} + {{else}} + No lyrics. + {{end}}
{{end}}