home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

turns out rewriting all of your database code takes a while

+929 -1018
+8 -3
admin/components/credits/editcredits.html
··· 51 51 52 52 makeMagicList(creditList, ".credit"); 53 53 54 - creditList.addEventListener("htmx:afterSwap", e => { 55 - const el = creditList.children[creditList.children.length - 1]; 56 - 54 + function rigCredit(el) { 55 + console.log(el); 57 56 const artistID = el.dataset.artist; 58 57 const deleteBtn = el.querySelector("a.delete"); 59 58 ··· 64 63 65 64 el.addEventListener("dragstart", () => { el.classList.add("moving") }); 66 65 el.addEventListener("dragend", () => { el.classList.remove("moving") }); 66 + } 67 + 68 + [...creditList.querySelectorAll(".credit")].map(rigCredit); 69 + 70 + creditList.addEventListener("htmx:afterSwap", () => { 71 + rigCredit(creditList.children[creditList.children.length - 1]); 67 72 }); 68 73 69 74 container.showModal();
+2 -2
admin/components/tracks/newtrack.html
··· 1 - <li class="track" data-track="{{.ID}}" data-title="{{.Title}}" data-number="{{.Number}}" draggable="true"> 1 + <li class="track" data-track="{{.ID}}" data-title="{{.Title}}" data-number="0" draggable="true"> 2 2 <div> 3 3 <p class="track-name"> 4 - <span class="track-number">{{.Number}}</span> 4 + <span class="track-number">0</span> 5 5 {{.Title}} 6 6 </p> 7 7 <a class="delete">Delete</a>
+40 -27
admin/http.go
··· 12 12 13 13 "arimelody.me/arimelody.me/discord" 14 14 "arimelody.me/arimelody.me/global" 15 - musicController "arimelody.me/arimelody.me/music/controller" 16 15 musicModel "arimelody.me/arimelody.me/music/model" 16 + musicDB "arimelody.me/arimelody.me/music/controller" 17 17 ) 18 18 19 19 type loginData struct { ··· 29 29 mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) 30 30 mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease()))) 31 31 mux.Handle("/track/", MustAuthorise(http.StripPrefix("/track", serveTrack()))) 32 - mux.Handle("/createtrack", MustAuthorise(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 - track := musicModel.Track{ Title: "Untitled Track" } 34 - trackID, err := musicController.CreateTrackDB(global.DB, &track) 35 - if err != nil { 36 - fmt.Printf("Failed to create track: %s\n", err) 37 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 38 - return 39 - } 40 - track.ID = trackID 41 - global.Tracks = append(global.Tracks, &track) 42 - http.Redirect(w, r, fmt.Sprintf("/admin/track/%s", trackID), http.StatusTemporaryRedirect) 43 - }))) 44 32 mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 45 33 if r.URL.Path != "/" { 46 34 http.NotFound(w, r) ··· 54 42 } 55 43 56 44 type ( 57 - Track struct { 58 - *musicModel.Track 59 - Lyrics template.HTML 60 - // Number int 61 - } 62 45 IndexData struct { 63 - Releases []*musicModel.Release 46 + Releases []musicModel.FullRelease 64 47 Artists []*musicModel.Artist 65 - Tracks []Track 48 + Tracks []musicModel.DisplayTrack 66 49 } 67 50 ) 68 51 69 - var tracks = []Track{} 70 - for _, track := range global.Tracks { 71 - if track.Release != nil { continue } 72 - tracks = append(tracks, Track{ 52 + dbReleases, err := musicDB.GetAllReleases(global.DB) 53 + if err != nil { 54 + fmt.Printf("FATAL: Failed to pull releases: %s\n", err) 55 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 56 + return 57 + } 58 + releases := []musicModel.FullRelease{} 59 + for _, release := range dbReleases { 60 + fullRelease, err := musicDB.GetFullRelease(global.DB, release) 61 + if err != nil { 62 + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) 63 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 64 + return 65 + } 66 + releases = append(releases, *fullRelease) 67 + } 68 + 69 + artists, err := musicDB.GetAllArtists(global.DB) 70 + if err != nil { 71 + fmt.Printf("FATAL: Failed to pull artists: %s\n", err) 72 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 73 + return 74 + } 75 + 76 + dbTracks, err := musicDB.GetOrphanTracks(global.DB) 77 + if err != nil { 78 + fmt.Printf("FATAL: Failed to pull orphan tracks: %s\n", err) 79 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 80 + return 81 + } 82 + 83 + var tracks = []musicModel.DisplayTrack{} 84 + for _, track := range dbTracks { 85 + tracks = append(tracks, musicModel.DisplayTrack{ 73 86 Track: track, 74 87 Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)), 75 88 }) 76 89 } 77 90 78 - err := pages["index"].Execute(w, IndexData{ 79 - Releases: global.Releases, 80 - Artists: global.Artists, 91 + err = pages["index"].Execute(w, IndexData{ 92 + Releases: releases, 93 + Artists: artists, 81 94 Tracks: tracks, 82 95 }) 83 96 if err != nil {
+64 -94
admin/releasehttp.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 - "html/template" 6 5 "net/http" 7 6 "strings" 8 7 9 8 "arimelody.me/arimelody.me/global" 10 9 "arimelody.me/arimelody.me/music/model" 11 - ) 12 - 13 - type ( 14 - gatewayTrack struct { 15 - *model.Track 16 - Lyrics template.HTML 17 - Number int 18 - } 19 - 20 - gatewayRelease struct { 21 - *model.Release 22 - Tracks []gatewayTrack 23 - } 10 + controller "arimelody.me/arimelody.me/music/controller" 24 11 ) 25 12 26 13 func serveRelease() http.Handler { 27 14 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 15 slices := strings.Split(r.URL.Path[1:], "/") 29 - id := slices[0] 30 - release := global.GetRelease(id) 16 + releaseID := slices[0] 17 + release, err := controller.GetRelease(global.DB, releaseID) 18 + if err != nil { 19 + fmt.Printf("FATAL: Failed to pull release %s: %s\n", releaseID, err) 20 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 21 + return 22 + } 31 23 if release == nil { 32 24 http.NotFound(w, r) 33 25 return 34 26 } 35 27 28 + authorised := GetSession(r) != nil 29 + if !authorised && !release.Visible { 30 + http.NotFound(w, r) 31 + return 32 + } 33 + 34 + fullRelease, err := controller.GetFullRelease(global.DB, release) 35 + if err != nil { 36 + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) 37 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 38 + return 39 + } 40 + 36 41 if len(slices) > 1 { 37 42 switch slices[1] { 38 43 case "editcredits": 39 - serveEditCredits(release).ServeHTTP(w, r) 44 + serveEditCredits(fullRelease).ServeHTTP(w, r) 40 45 return 41 46 case "addcredit": 42 - serveAddCredit(release).ServeHTTP(w, r) 47 + serveAddCredit(fullRelease).ServeHTTP(w, r) 43 48 return 44 49 case "newcredit": 45 50 serveNewCredit().ServeHTTP(w, r) 46 51 return 47 52 case "editlinks": 48 - serveEditLinks(release).ServeHTTP(w, r) 53 + serveEditLinks(fullRelease).ServeHTTP(w, r) 49 54 return 50 55 case "edittracks": 51 - serveEditTracks(release).ServeHTTP(w, r) 56 + serveEditTracks(fullRelease).ServeHTTP(w, r) 52 57 return 53 58 case "addtrack": 54 - serveAddTrack(release).ServeHTTP(w, r) 59 + serveAddTrack(fullRelease).ServeHTTP(w, r) 55 60 return 56 61 case "newtrack": 57 - serveNewTrack(release).ServeHTTP(w, r) 62 + serveNewTrack().ServeHTTP(w, r) 58 63 return 59 64 } 60 65 http.NotFound(w, r) 61 66 return 62 67 } 63 68 64 - tracks := []gatewayTrack{} 65 - for i, track := range release.Tracks { 66 - tracks = append(tracks, gatewayTrack{ 67 - Track: track, 68 - Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)), 69 - Number: i + 1, 70 - }) 71 - } 72 - 73 - err := pages["release"].Execute(w, gatewayRelease{release, tracks}) 69 + err = pages["release"].Execute(w, fullRelease) 74 70 if err != nil { 75 - fmt.Printf("Error rendering admin release page for %s: %s\n", id, err) 71 + fmt.Printf("Error rendering admin release page for %s: %s\n", release.ID, err) 76 72 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 77 73 } 78 74 }) 79 75 } 80 76 81 - func serveEditCredits(release *model.Release) http.Handler { 77 + func serveEditCredits(release *model.FullRelease) http.Handler { 82 78 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 83 79 w.Header().Set("Content-Type", "text/html") 84 80 err := components["editcredits"].Execute(w, release) ··· 89 85 }) 90 86 } 91 87 92 - func serveAddCredit(release *model.Release) http.Handler { 88 + func serveAddCredit(release *model.FullRelease) http.Handler { 93 89 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 94 - var artists = []*model.Artist{} 95 - for _, artist := range global.Artists { 96 - var exists = false 97 - for _, credit := range release.Credits { 98 - if credit.Artist == artist { 99 - exists = true 100 - break 101 - } 102 - } 103 - if !exists { 104 - artists = append(artists, artist) 105 - } 90 + artists, err := controller.GetArtistsNotOnRelease(global.DB, release.Release) 91 + if err != nil { 92 + fmt.Printf("FATAL: Failed to pull artists not on %s: %s\n", release.ID, err) 93 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 94 + return 106 95 } 107 96 108 97 type response struct { ··· 111 100 } 112 101 113 102 w.Header().Set("Content-Type", "text/html") 114 - err := components["addcredit"].Execute(w, response{ 103 + err = components["addcredit"].Execute(w, response{ 115 104 ReleaseID: release.ID, 116 105 Artists: artists, 117 106 }) ··· 124 113 125 114 func serveNewCredit() http.Handler { 126 115 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 127 - artist := global.GetArtist(strings.Split(r.URL.Path, "/")[3]) 116 + artistID := strings.Split(r.URL.Path, "/")[3] 117 + artist, err := controller.GetArtist(global.DB, artistID) 118 + if err != nil { 119 + fmt.Printf("FATAL: Failed to pull artists %s: %s\n", artistID, err) 120 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 121 + return 122 + } 128 123 if artist == nil { 129 124 http.NotFound(w, r) 130 125 return 131 126 } 132 127 133 128 w.Header().Set("Content-Type", "text/html") 134 - err := components["newcredit"].Execute(w, artist) 129 + err = components["newcredit"].Execute(w, artist) 135 130 if err != nil { 136 131 fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err) 137 132 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 138 133 } 139 - return 140 134 }) 141 135 } 142 136 143 - func serveEditLinks(release *model.Release) http.Handler { 137 + func serveEditLinks(release *model.FullRelease) http.Handler { 144 138 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 145 139 w.Header().Set("Content-Type", "text/html") 146 140 err := components["editlinks"].Execute(w, release) ··· 148 142 fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err) 149 143 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 150 144 } 151 - return 152 145 }) 153 146 } 154 147 155 - func serveEditTracks(release *model.Release) http.Handler { 148 + func serveEditTracks(release *model.FullRelease) http.Handler { 156 149 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 157 150 w.Header().Set("Content-Type", "text/html") 158 - type Track struct { 159 - *model.Track 160 - Number int 161 - } 162 - type Release struct { 163 - *model.Release 164 - Tracks []Track 165 - } 166 - var data = Release{ release, []Track{} } 167 - for i, track := range release.Tracks { 168 - data.Tracks = append(data.Tracks, Track{track, i + 1}) 169 - } 170 - 171 - err := components["edittracks"].Execute(w, data) 151 + err := components["edittracks"].Execute(w, release) 172 152 if err != nil { 173 153 fmt.Printf("Error rendering edit tracks component for %s: %s\n", release.ID, err) 174 154 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 175 155 } 176 - return 177 156 }) 178 157 } 179 158 180 - func serveAddTrack(release *model.Release) http.Handler { 159 + func serveAddTrack(release *model.FullRelease) http.Handler { 181 160 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 182 - var tracks = []*model.Track{} 183 - for _, track := range global.Tracks { 184 - var exists = false 185 - for _, t := range release.Tracks { 186 - if t == track { 187 - exists = true 188 - break 189 - } 190 - } 191 - if !exists { 192 - tracks = append(tracks, track) 193 - } 161 + tracks, err := controller.GetTracksNotOnRelease(global.DB, release.Release) 162 + if err != nil { 163 + fmt.Printf("FATAL: Failed to pull tracks not on %s: %s\n", release.ID, err) 164 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 165 + return 194 166 } 195 167 196 168 type response struct { ··· 199 171 } 200 172 201 173 w.Header().Set("Content-Type", "text/html") 202 - err := components["addtrack"].Execute(w, response{ 174 + err = components["addtrack"].Execute(w, response{ 203 175 ReleaseID: release.ID, 204 176 Tracks: tracks, 205 177 }) ··· 211 183 }) 212 184 } 213 185 214 - func serveNewTrack(release *model.Release) http.Handler { 186 + func serveNewTrack() http.Handler { 215 187 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 216 - track := global.GetTrack(strings.Split(r.URL.Path, "/")[3]) 188 + trackID := strings.Split(r.URL.Path, "/")[3] 189 + track, err := controller.GetTrack(global.DB, trackID) 190 + if err != nil { 191 + fmt.Printf("Error rendering new track component for %s: %s\n", trackID, err) 192 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 193 + return 194 + } 217 195 if track == nil { 218 196 http.NotFound(w, r) 219 197 return 220 198 } 221 199 222 - type Track struct { 223 - *model.Track 224 - Number int 225 - } 226 - 227 200 w.Header().Set("Content-Type", "text/html") 228 - err := components["newtrack"].Execute(w, Track{ 229 - track, 230 - len(release.Tracks) + 1, 231 - }) 201 + err = components["newtrack"].Execute(w, track) 232 202 if err != nil { 233 203 fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err) 234 204 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+25
admin/static/index.js
··· 1 1 const newReleaseBtn = document.getElementById("create-release"); 2 + const newTrackBtn = document.getElementById("create-track"); 2 3 3 4 newReleaseBtn.addEventListener("click", event => { 4 5 event.preventDefault(); ··· 22 23 console.error(err); 23 24 }); 24 25 }); 26 + 27 + newTrackBtn.addEventListener("click", event => { 28 + event.preventDefault(); 29 + const title = prompt("Enter an title for this track:"); 30 + if (title == null || title == "") return; 31 + 32 + fetch("/api/v1/track", { 33 + method: "POST", 34 + headers: { "Content-Type": "application/json" }, 35 + body: JSON.stringify({title}) 36 + }).then(res => { 37 + res.text().then(text => { 38 + if (res.ok) { 39 + location = "/admin/track/" + text; 40 + } else { 41 + alert("Request failed: " + text); 42 + console.error(text); 43 + } 44 + }) 45 + }).catch(err => { 46 + alert("Failed to create release. Check the console for details."); 47 + console.error(err); 48 + }); 49 + });
+31 -2
admin/trackhttp.go
··· 6 6 "strings" 7 7 8 8 "arimelody.me/arimelody.me/global" 9 + "arimelody.me/arimelody.me/music/model" 10 + "arimelody.me/arimelody.me/music/controller" 9 11 ) 10 12 11 13 func serveTrack() http.Handler { 12 14 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 15 slices := strings.Split(r.URL.Path[1:], "/") 14 16 id := slices[0] 15 - track := global.GetTrack(id) 17 + track, err := music.GetTrack(global.DB, id) 18 + if err != nil { 19 + fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) 20 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 21 + return 22 + } 16 23 if track == nil { 17 24 http.NotFound(w, r) 18 25 return 19 26 } 20 27 21 - err := pages["track"].Execute(w, track) 28 + dbReleases, err := music.GetTrackReleases(global.DB, track) 29 + if err != nil { 30 + fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) 31 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 32 + return 33 + } 34 + releases := []model.FullRelease{} 35 + for _, release := range dbReleases { 36 + fullRelease, err := music.GetFullRelease(global.DB, release) 37 + if err != nil { 38 + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) 39 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 40 + return 41 + } 42 + releases = append(releases, *fullRelease) 43 + } 44 + 45 + type Track struct { 46 + *model.Track 47 + Releases []model.FullRelease 48 + } 49 + 50 + err = pages["track"].Execute(w, Track{ Track: track, Releases: releases }) 22 51 if err != nil { 23 52 fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) 24 53 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+4 -2
admin/views/edit-track.html
··· 43 43 <h2>Featured in</h2> 44 44 </div> 45 45 <div class="card releases"> 46 - {{if .Release}} 47 - {{block "release" .Release}}{{end}} 46 + {{if .Releases}} 47 + {{range .Releases}} 48 + {{block "release" .}}{{end}} 49 + {{end}} 48 50 {{else}} 49 51 <p>This track isn't bound to a release.</p> 50 52 {{end}}
+3 -8
admin/views/index.html
··· 9 9 10 10 <div class="card-title"> 11 11 <h1>Releases</h1> 12 - <a href="/admin/createrelease" class="create-btn" id="create-release">Create New</a> 12 + <a class="create-btn" id="create-release">Create New</a> 13 13 </div> 14 14 <div class="card releases"> 15 15 {{range .Releases}} ··· 22 22 23 23 <div class="card-title"> 24 24 <h1>Artists</h1> 25 - <a href="/admin/createartist" class="create-btn">Create New</a> 25 + <a class="create-btn">Create New</a> 26 26 </div> 27 27 <div class="card artists"> 28 28 {{range $Artist := .Artists}} ··· 38 38 39 39 <div class="card-title"> 40 40 <h1>Tracks</h1> 41 - <a href="/admin/createtrack" class="create-btn">Create New</a> 41 + <a class="create-btn" id="create-track">Create New</a> 42 42 </div> 43 43 <div class="card tracks"> 44 44 <p><em>"Orphaned" tracks that have not yet been bound to a release.</em></p> ··· 47 47 <div class="track"> 48 48 <h2 class="track-title"> 49 49 <a href="/admin/track/{{$Track.ID}}">{{$Track.Title}}</a> 50 - {{if $Track.Release}} 51 - <small class="track-album">{{$Track.Release.Title}}</small> 52 - {{else}} 53 - <small class="track-album empty">(no release)</small> 54 - {{end}} 55 50 </h2> 56 51 {{if $Track.Description}} 57 52 <p class="track-description">{{$Track.Description}}</p>
+40 -9
api/api.go
··· 1 1 package api 2 2 3 3 import ( 4 + "fmt" 4 5 "net/http" 6 + "strings" 5 7 6 8 "arimelody.me/arimelody.me/admin" 9 + "arimelody.me/arimelody.me/global" 10 + "arimelody.me/arimelody.me/music/model" 7 11 music "arimelody.me/arimelody.me/music/view" 8 12 ) 9 13 ··· 13 17 // ARTIST ENDPOINTS 14 18 15 19 mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 + var artistID = strings.Split(r.URL.Path[1:], "/")[0] 21 + var artist model.Artist 22 + err := global.DB.Get(&artist, "SELECT * FROM artist WHERE id=$1", artistID) 23 + if err != nil { 24 + fmt.Printf("FATAL: Error while retrieving artist %s: %s\n", artistID, err) 25 + http.NotFound(w, r) 26 + return 27 + } 28 + 16 29 switch r.Method { 17 30 case http.MethodGet: 18 31 // GET /api/v1/artist/{id} 19 - ServeArtist().ServeHTTP(w, r) 32 + ServeArtist(artist).ServeHTTP(w, r) 20 33 case http.MethodPut: 21 34 // PUT /api/v1/artist/{id} (admin) 22 - admin.MustAuthorise(UpdateArtist()).ServeHTTP(w, r) 35 + admin.MustAuthorise(UpdateArtist(artist)).ServeHTTP(w, r) 23 36 case http.MethodDelete: 24 37 // DELETE /api/v1/artist/{id} (admin) 25 - admin.MustAuthorise(DeleteArtist()).ServeHTTP(w, r) 38 + admin.MustAuthorise(DeleteArtist(artist)).ServeHTTP(w, r) 26 39 default: 27 40 http.NotFound(w, r) 28 41 } ··· 43 56 // RELEASE ENDPOINTS 44 57 45 58 mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 59 + var releaseID = strings.Split(r.URL.Path[1:], "/")[0] 60 + var release model.Release 61 + err := global.DB.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", releaseID) 62 + if err != nil { 63 + fmt.Printf("FATAL: Error while retrieving release %s: %s\n", releaseID, err) 64 + http.NotFound(w, r) 65 + return 66 + } 67 + 46 68 switch r.Method { 47 69 case http.MethodGet: 48 70 // GET /api/v1/music/{id} 49 - music.ServeRelease().ServeHTTP(w, r) 71 + music.ServeRelease(release).ServeHTTP(w, r) 50 72 case http.MethodPut: 51 73 // PUT /api/v1/music/{id} (admin) 52 - admin.MustAuthorise(UpdateRelease()).ServeHTTP(w, r) 74 + admin.MustAuthorise(UpdateRelease(release)).ServeHTTP(w, r) 53 75 case http.MethodDelete: 54 76 // DELETE /api/v1/music/{id} (admin) 55 - admin.MustAuthorise(DeleteRelease()).ServeHTTP(w, r) 77 + admin.MustAuthorise(DeleteRelease(release)).ServeHTTP(w, r) 56 78 default: 57 79 http.NotFound(w, r) 58 80 } ··· 73 95 // TRACK ENDPOINTS 74 96 75 97 mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 98 + var trackID = strings.Split(r.URL.Path[1:], "/")[0] 99 + var track model.Track 100 + err := global.DB.Get(&track, "SELECT * FROM musictrack WHERE id=$1", trackID) 101 + if err != nil { 102 + fmt.Printf("FATAL: Error while retrieving track %s: %s\n", trackID, err) 103 + http.NotFound(w, r) 104 + return 105 + } 106 + 76 107 switch r.Method { 77 108 case http.MethodGet: 78 109 // GET /api/v1/track/{id} (admin) 79 - admin.MustAuthorise(ServeTrack()).ServeHTTP(w, r) 110 + admin.MustAuthorise(ServeTrack(track)).ServeHTTP(w, r) 80 111 case http.MethodPut: 81 112 // PUT /api/v1/track/{id} (admin) 82 - admin.MustAuthorise(UpdateTrack()).ServeHTTP(w, r) 113 + admin.MustAuthorise(UpdateTrack(track)).ServeHTTP(w, r) 83 114 case http.MethodDelete: 84 115 // DELETE /api/v1/track/{id} (admin) 85 - admin.MustAuthorise(DeleteTrack()).ServeHTTP(w, r) 116 + admin.MustAuthorise(DeleteTrack(track)).ServeHTTP(w, r) 86 117 default: 87 118 http.NotFound(w, r) 88 119 }
+56 -127
api/artist.go
··· 4 4 "encoding/json" 5 5 "fmt" 6 6 "net/http" 7 + "strings" 7 8 8 9 "arimelody.me/arimelody.me/global" 9 10 "arimelody.me/arimelody.me/music/model" 10 - controller "arimelody.me/arimelody.me/music/controller" 11 11 ) 12 12 13 13 type artistJSON struct { ··· 19 19 20 20 func ServeAllArtists() http.Handler { 21 21 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 + var artists = []*model.Artist{} 23 + err := global.DB.Select(&artists, "SELECT * FROM artist") 24 + if err != nil { 25 + fmt.Printf("FATAL: Failed to serve all artists: %s\n", err) 26 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 27 + return 28 + } 29 + 22 30 w.Header().Add("Content-Type", "application/json") 23 - err := json.NewEncoder(w).Encode(global.Artists) 31 + err = json.NewEncoder(w).Encode(artists) 24 32 if err != nil { 25 33 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 26 - return 27 34 } 28 35 }) 29 36 } 30 37 31 - func ServeArtist() http.Handler { 38 + func ServeArtist(artist model.Artist) http.Handler { 32 39 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 - if r.URL.Path == "/" { 34 - ServeAllArtists().ServeHTTP(w, r) 35 - return 36 - } 37 - 38 40 type ( 39 41 creditJSON struct { 40 - Role string `json:"role"` 41 - Primary bool `json:"primary"` 42 + Release string `json:"release"` 43 + Role string `json:"role"` 44 + Primary bool `json:"primary"` 42 45 } 43 46 artistJSON struct { 44 47 model.Artist 45 48 Credits map[string]creditJSON `json:"credits"` 46 49 } 47 50 ) 48 - var artist = artistJSON{} 49 51 50 - artist.ID = r.URL.Path[1:] 51 - var a = global.GetArtist(artist.ID) 52 - if a == nil { 53 - http.NotFound(w, r) 52 + var credits = map[string]creditJSON{} 53 + err := global.DB.Select(&credits, "SELECT release,role,is_primary FROM musiccredit WHERE id=$1", artist.ID) 54 + if err != nil { 55 + fmt.Printf("FATAL: Failed to retrieve artist credits for %s: %s\n", artist.ID, err) 56 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 54 57 return 55 58 } 56 - artist.Name = a.Name 57 - artist.Website = a.Website 58 - artist.Credits = make(map[string]creditJSON) 59 - 60 - for _, release := range global.Releases { 61 - for _, credit := range release.Credits { 62 - if credit.Artist.ID != artist.ID { 63 - continue 64 - } 65 - artist.Credits[release.ID] = creditJSON{ 66 - Role: credit.Role, 67 - Primary: credit.Primary, 68 - } 69 - } 70 - } 71 59 72 60 w.Header().Add("Content-Type", "application/json") 73 - err := json.NewEncoder(w).Encode(artist) 61 + err = json.NewEncoder(w).Encode(artistJSON{ 62 + Artist: artist, 63 + Credits: credits, 64 + }) 74 65 if err != nil { 75 66 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 76 - return 77 67 } 78 68 }) 79 69 } 80 70 81 71 func CreateArtist() http.Handler { 82 72 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 83 - if r.Method != http.MethodPost { 84 - http.NotFound(w, r) 85 - return 86 - } 87 - 88 73 var data artistJSON 89 74 err := json.NewDecoder(r.Body).Decode(&data) 90 75 if err != nil { 91 - fmt.Printf("Failed to create artist: %s\n", err) 92 76 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 93 77 return 94 78 } ··· 102 86 return 103 87 } 104 88 105 - if global.GetArtist(data.ID) != nil { 106 - http.Error(w, fmt.Sprintf("Artist %s already exists\n", data.ID), http.StatusBadRequest) 107 - return 108 - } 109 - 110 89 var artist = model.Artist{ 111 90 ID: data.ID, 112 91 Name: *data.Name, ··· 114 93 Avatar: *data.Avatar, 115 94 } 116 95 117 - err = controller.CreateArtistDB(global.DB, &artist) 96 + _, err = global.DB.Exec( 97 + "INSERT INTO artist (id, name, website, avatar) "+ 98 + "VALUES ($1, $2, $3, $4)", 99 + artist.ID, 100 + artist.Name, 101 + artist.Website, 102 + artist.Avatar) 118 103 if err != nil { 119 - fmt.Printf("Failed to create artist %s: %s\n", artist.ID, err) 104 + if strings.Contains(err.Error(), "duplicate key") { 105 + http.Error(w, fmt.Sprintf("Artist %s already exists\n", data.ID), http.StatusBadRequest) 106 + return 107 + } 108 + fmt.Printf("FATAL: Failed to create artist %s: %s\n", artist.ID, err) 120 109 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 121 110 return 122 111 } 123 112 124 - global.Artists = append(global.Artists, &artist) 125 - 126 - w.Header().Add("Content-Type", "application/json") 127 113 w.WriteHeader(http.StatusCreated) 128 - err = json.NewEncoder(w).Encode(artist) 129 - if err != nil { 130 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 131 - return 132 - } 133 114 }) 134 115 } 135 116 136 - func UpdateArtist() http.Handler { 117 + func UpdateArtist(artist model.Artist) http.Handler { 137 118 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 138 - if r.Method != http.MethodPut { 139 - http.NotFound(w, r) 140 - return 141 - } 142 - 143 - if r.URL.Path == "/" { 144 - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 145 - return 146 - } 147 - 148 119 var data artistJSON 149 120 err := json.NewDecoder(r.Body).Decode(&data) 150 121 if err != nil { 151 - fmt.Printf("Failed to update artist: %s\n", err) 122 + fmt.Printf("FATAL: Failed to update artist: %s\n", err) 152 123 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 153 124 return 154 125 } 155 126 156 - var artistID = r.URL.Path[1:] 157 - var artist = global.GetArtist(artistID) 158 - if artist == nil { 159 - http.Error(w, fmt.Sprintf("Artist %s does not exist\n", artistID), http.StatusBadRequest) 160 - return 161 - } 162 - 163 - var update = *artist 164 - 165 - if data.ID != "" { update.ID = data.ID } 166 - if data.Name != nil { update.Name = *data.Name } 167 - if data.Website != nil { update.Website = *data.Website } 168 - if data.Avatar != nil { update.Avatar = *data.Avatar } 127 + if data.ID != "" { artist.ID = data.ID } 128 + if data.Name != nil { artist.Name = *data.Name } 129 + if data.Website != nil { artist.Website = *data.Website } 130 + if data.Avatar != nil { artist.Avatar = *data.Avatar } 169 131 170 - err = controller.UpdateArtistDB(global.DB, &update) 132 + _, err = global.DB.Exec( 133 + "UPDATE artist "+ 134 + "SET name=$2, website=$3, avatar=$4 "+ 135 + "WHERE id=$1", 136 + artist.ID, 137 + artist.Name, 138 + artist.Website, 139 + artist.Avatar) 171 140 if err != nil { 172 - fmt.Printf("Failed to update artist %s: %s\n", artist.ID, err) 141 + fmt.Printf("FATAL: Failed to update artist %s: %s\n", artist.ID, err) 173 142 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 174 - return 175 - } 176 - 177 - artist.ID = update.ID 178 - artist.Name = update.Name 179 - artist.Website = update.Website 180 - artist.Avatar = update.Avatar 181 - 182 - w.Header().Add("Content-Type", "application/json") 183 - err = json.NewEncoder(w).Encode(artist) 184 - if err != nil { 185 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 186 - return 187 143 } 188 144 }) 189 145 } 190 146 191 - func DeleteArtist() http.Handler { 147 + func DeleteArtist(artist model.Artist) http.Handler { 192 148 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 193 - if r.Method != http.MethodDelete { 194 - http.NotFound(w, r) 195 - return 196 - } 197 - 198 - if r.URL.Path == "/" { 199 - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 200 - return 201 - } 202 - 203 - var artistID = r.URL.Path[1:] 204 - var artist = global.GetArtist(artistID) 205 - if artist == nil { 206 - http.Error(w, fmt.Sprintf("Artist %s does not exist\n", artistID), http.StatusBadRequest) 207 - return 208 - } 209 - 210 - err := controller.DeleteArtistDB(global.DB, artist) 149 + _, err := global.DB.Exec( 150 + "DELETE FROM artist "+ 151 + "WHERE id=$1", 152 + artist.ID) 211 153 if err != nil { 212 - fmt.Printf("Failed to delete artist %s: %s\n", artist.ID, err) 154 + fmt.Printf("FATAL: Failed to delete artist %s: %s\n", artist.ID, err) 213 155 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 214 - return 215 156 } 216 - 217 - global.Artists = func () []*model.Artist { 218 - var artists = []*model.Artist{} 219 - for _, a := range global.Artists { 220 - if a.ID == artist.ID { continue } 221 - artists = append(artists, a) 222 - } 223 - return artists 224 - }() 225 - 226 - w.WriteHeader(http.StatusOK) 227 - w.Write([]byte(fmt.Sprintf("Artist %s has been deleted\n", artist.ID))) 228 157 }) 229 158 }
+158 -267
api/release.go
··· 1 1 package api 2 2 3 3 import ( 4 - "bufio" 5 - "encoding/base64" 6 4 "encoding/json" 7 5 "fmt" 8 6 "io/fs" ··· 14 12 15 13 "arimelody.me/arimelody.me/admin" 16 14 "arimelody.me/arimelody.me/global" 17 - controller "arimelody.me/arimelody.me/music/controller" 18 15 "arimelody.me/arimelody.me/music/model" 19 16 ) 20 17 ··· 32 29 33 30 func ServeCatalog() http.Handler { 34 31 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 - type CatalogItem struct { 32 + type catalogItem struct { 36 33 ID string `json:"id"` 37 34 Title string `json:"title"` 38 - Description string `json:"description"` 39 35 ReleaseType model.ReleaseType `json:"type"` 40 36 ReleaseDate time.Time `json:"releaseDate"` 41 37 Artwork string `json:"artwork"` 42 - Buyname string `json:"buyname"` 43 38 Buylink string `json:"buylink"` 44 - Links []*model.Link `json:"links"` 39 + } 40 + 41 + releases := []*model.Release{} 42 + err := global.DB.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") 43 + if err != nil { 44 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 45 + return 45 46 } 46 47 47 - catalog := []CatalogItem{} 48 + catalog := []catalogItem{} 48 49 authorised := admin.GetSession(r) != nil 49 - for _, release := range global.Releases { 50 + for _, release := range releases { 50 51 if !release.Visible && !authorised { 51 52 continue 52 53 } 53 - catalog = append(catalog, CatalogItem{ 54 + catalog = append(catalog, catalogItem{ 54 55 ID: release.ID, 55 56 Title: release.Title, 56 - Description: release.Description, 57 57 ReleaseType: release.ReleaseType, 58 58 ReleaseDate: release.ReleaseDate, 59 59 Artwork: release.Artwork, 60 - Buyname: release.Buyname, 61 60 Buylink: release.Buylink, 62 - Links: release.Links, 63 61 }) 64 62 } 65 63 66 64 w.Header().Add("Content-Type", "application/json") 67 - err := json.NewEncoder(w).Encode(catalog) 65 + err = json.NewEncoder(w).Encode(catalog) 68 66 if err != nil { 69 67 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 70 68 return ··· 122 120 buylink := "" 123 121 if data.Buylink != nil && *data.Buylink != "" { buylink = *data.Buylink } 124 122 125 - if global.GetRelease(data.ID) != nil { 126 - http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest) 127 - return 128 - } 129 - 130 123 var release = model.Release{ 131 124 ID: data.ID, 132 125 Visible: false, ··· 137 130 Artwork: artwork, 138 131 Buyname: buyname, 139 132 Buylink: buylink, 140 - Links: []*model.Link{}, 141 - Credits: []*model.Credit{}, 142 - Tracks: []*model.Track{}, 143 133 } 144 134 145 - err = controller.CreateReleaseDB(global.DB, &release) 135 + _, err = global.DB.Exec( 136 + "INSERT INTO musicrelease "+ 137 + "(id, visible, title, description, type, release_date, artwork, buyname, buylink) "+ 138 + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", 139 + release.ID, 140 + release.Visible, 141 + release.Title, 142 + release.Description, 143 + release.ReleaseType, 144 + release.ReleaseDate.Format("2006-01-02 15:04:05"), 145 + release.Artwork, 146 + release.Buyname, 147 + release.Buylink) 146 148 if err != nil { 149 + if strings.Contains(err.Error(), "duplicate key") { 150 + http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest) 151 + return 152 + } 147 153 fmt.Printf("Failed to create release %s: %s\n", release.ID, err) 148 154 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 149 155 return 150 156 } 151 - 152 - global.Releases = append([]*model.Release{&release}, global.Releases...) 153 157 154 158 w.Header().Add("Content-Type", "application/json") 155 159 w.WriteHeader(http.StatusCreated) 156 160 err = json.NewEncoder(w).Encode(release) 157 161 if err != nil { 162 + fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", release.ID, err) 158 163 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 159 - return 160 164 } 161 165 }) 162 166 } 163 167 164 - func UpdateRelease() http.Handler { 168 + func UpdateRelease(release model.Release) http.Handler { 165 169 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 166 170 if r.URL.Path == "/" { 167 171 http.NotFound(w, r) ··· 170 174 171 175 segments := strings.Split(r.URL.Path[1:], "/") 172 176 var releaseID = segments[0] 173 - var release = global.GetRelease(releaseID) 174 - if release == nil { 175 - http.Error(w, fmt.Sprintf("Release %s does not exist\n", releaseID), http.StatusBadRequest) 176 - return 177 - } 178 - 179 - if len(segments) == 1 { 180 - var data releaseBodyJSON 181 - err := json.NewDecoder(r.Body).Decode(&data) 182 - if err != nil { 183 - fmt.Printf("Failed to update release: %s\n", err) 184 - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 185 - return 186 - } 187 - 188 - var update = *release 189 - if data.ID != "" { update.ID = data.ID } 190 - if data.Visible != nil { update.Visible = *data.Visible } 191 - if data.Title != nil { update.Title = *data.Title } 192 - if data.Description != nil { update.Description = *data.Description } 193 - if data.ReleaseType != nil { update.ReleaseType = *data.ReleaseType } 194 - if data.ReleaseDate != nil { 195 - newDate, err := time.Parse("2006-01-02T15:04", *data.ReleaseDate) 196 - if err != nil { 197 - http.Error(w, "Invalid release date", http.StatusBadRequest) 198 - return 199 - } 200 - update.ReleaseDate = newDate 201 - } 202 - if data.Artwork != nil { 203 - if strings.Contains(*data.Artwork, ";base64,") { 204 - split := strings.Split(*data.Artwork, ";base64,") 205 - header := split[0] 206 - imageData, err := base64.StdEncoding.DecodeString(split[1]) 207 - ext, _ := strings.CutPrefix(header, "data:image/") 208 - 209 - switch ext { 210 - case "png": 211 - case "jpg": 212 - case "jpeg": 213 - default: 214 - http.Error(w, "Invalid image type. Allowed: .png, .jpg, .jpeg", http.StatusBadRequest) 215 - return 216 - } 217 - 218 - artworkDirectory := filepath.Join("uploads", "musicart") 219 - // ensure directory exists 220 - os.MkdirAll(artworkDirectory, os.ModePerm) 221 - 222 - imagePath := filepath.Join(artworkDirectory, fmt.Sprintf("%s.%s", update.ID, ext)) 223 - file, err := os.Create(imagePath) 224 - if err != nil { 225 - fmt.Printf("FATAL: Failed to create file %s: %s\n", imagePath, err) 226 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 227 - return 228 - } 229 - 230 - defer file.Close() 231 - 232 - buffer := bufio.NewWriter(file) 233 - _, err = buffer.Write(imageData) 234 - if err != nil { 235 - fmt.Printf("FATAL: Failed to write to file %s: %s\n", imagePath, err) 236 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 237 - return 238 - } 239 - 240 - if err := buffer.Flush(); err != nil { 241 - fmt.Printf("FATAL: Failed to flush data to file %s: %s\n", imagePath, err) 242 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 243 - return 244 - } 245 - 246 - // clean up files with this ID and different extensions 247 - err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error { 248 - if path == imagePath { return nil } 249 - 250 - withoutExt := strings.TrimSuffix(path, filepath.Ext(path)) 251 - if withoutExt != filepath.Join(artworkDirectory, update.ID) { return nil } 252 - 253 - return os.Remove(path) 254 - }) 255 - if err != nil { 256 - fmt.Printf("WARN: Error while cleaning up artwork files: %s\n", err) 257 - } 258 - 259 - update.Artwork = fmt.Sprintf("/uploads/musicart/%s.%s", update.ID, ext) 260 - } else { 261 - update.Artwork = *data.Artwork 262 - } 263 - } 264 - 265 - if data.Buyname != nil { update.Buyname = *data.Buyname } 266 - if data.Buylink != nil { update.Buylink = *data.Buylink } 267 - 268 - err = controller.UpdateReleaseDB(global.DB, &update) 269 - if err != nil { 270 - fmt.Printf("Failed to update release %s: %s\n", release.ID, err) 271 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 272 - return 273 - } 274 - 275 - release.ID = update.ID 276 - release.Visible = update.Visible 277 - release.Title = update.Title 278 - release.Description = update.Description 279 - release.ReleaseType = update.ReleaseType 280 - release.ReleaseDate = update.ReleaseDate 281 - release.Artwork = update.Artwork 282 - release.Buyname = update.Buyname 283 - release.Buylink = update.Buylink 284 - 285 - w.Header().Add("Content-Type", "application/json") 286 - err = json.NewEncoder(w).Encode(release) 287 - if err != nil { 288 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 289 - return 290 - } 177 + var exists int 178 + err := global.DB.Get(&exists, "SELECT count(*) FROM musicrelease WHERE id=$1", releaseID) 179 + if err != nil { 180 + fmt.Printf("Failed to update release: %s\n", err) 181 + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 291 182 return 292 183 } 293 184 ··· 302 193 } 303 194 return 304 195 } 305 - 306 - http.NotFound(w, r) 307 - }) 308 - } 309 - 310 - func UpdateReleaseTracks(release *model.Release) http.Handler { 311 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 312 - if r.URL.Path == "/" || r.Method != http.MethodPut { 196 + 197 + if len(segments) > 2 { 313 198 http.NotFound(w, r) 314 199 return 315 200 } 316 201 317 - var trackIDs = []string{} 318 - err := json.NewDecoder(r.Body).Decode(&trackIDs) 202 + var data releaseBodyJSON 203 + err = json.NewDecoder(r.Body).Decode(&data) 319 204 if err != nil { 320 205 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 321 206 return 322 207 } 323 208 324 - var old_tracks = (*release).Tracks 325 - var new_tracks = []*model.Track{} 326 - for _, trackID := range trackIDs { 327 - var track = global.GetTrack(trackID) 328 - if track == nil { 329 - http.Error(w, fmt.Sprintf("Track %s does not exist\n", trackID), http.StatusBadRequest) 209 + if data.ID != "" { release.ID = data.ID } 210 + if data.Visible != nil { release.Visible = *data.Visible } 211 + if data.Title != nil { release.Title = *data.Title } 212 + if data.Description != nil { release.Description = *data.Description } 213 + if data.ReleaseType != nil { release.ReleaseType = *data.ReleaseType } 214 + if data.ReleaseDate != nil { 215 + newDate, err := time.Parse("2006-01-02T15:04", *data.ReleaseDate) 216 + if err != nil { 217 + http.Error(w, "Invalid release date", http.StatusBadRequest) 330 218 return 331 219 } 332 - new_tracks = append(new_tracks, track) 333 - track.Release = release 220 + release.ReleaseDate = newDate 334 221 } 222 + if data.Artwork != nil { 223 + if strings.Contains(*data.Artwork, ";base64,") { 224 + var artworkDirectory = filepath.Join("uploads", "musicart") 225 + filename, err := HandleImageUpload(data.Artwork, artworkDirectory, data.ID) 335 226 336 - err = controller.UpdateReleaseTracksDB(global.DB, release, new_tracks) 337 - if err != nil { 338 - fmt.Printf("Failed to update tracks for %s: %s\n", release.ID, err) 339 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 340 - return 341 - } 227 + // clean up files with this ID and different extensions 228 + err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error { 229 + if path == filepath.Join(artworkDirectory, filename) { return nil } 342 230 343 - release.Tracks = new_tracks 231 + withoutExt := strings.TrimSuffix(path, filepath.Ext(path)) 232 + if withoutExt != filepath.Join(artworkDirectory, release.ID) { return nil } 344 233 345 - // remove release from orphaned tracks 346 - for _, old_track := range old_tracks { 347 - var exists = false 348 - for _, track := range new_tracks { 349 - if track.ID == old_track.ID { 350 - exists = true 351 - break 234 + return os.Remove(path) 235 + }) 236 + if err != nil { 237 + fmt.Printf("WARN: Error while cleaning up artwork files: %s\n", err) 352 238 } 353 - } 354 - if !exists { 355 - old_track.Release = nil 239 + 240 + release.Artwork = fmt.Sprintf("/uploads/musicart/%s", filename) 241 + } else { 242 + release.Artwork = *data.Artwork 356 243 } 357 244 } 358 245 359 - w.Header().Add("Content-Type", "application/json") 360 - w.WriteHeader(http.StatusOK) 361 - err = json.NewEncoder(w).Encode(release) 246 + if data.Buyname != nil { release.Buyname = *data.Buyname } 247 + if data.Buylink != nil { release.Buylink = *data.Buylink } 248 + 249 + _, err = global.DB.Exec( 250 + "UPDATE musicrelease SET "+ 251 + "visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9 "+ 252 + "WHERE id=$1", 253 + release.ID, 254 + release.Visible, 255 + release.Title, 256 + release.Description, 257 + release.ReleaseType, 258 + release.ReleaseDate.Format("2006-01-02 15:04:05"), 259 + release.Artwork, 260 + release.Buyname, 261 + release.Buylink) 362 262 if err != nil { 263 + fmt.Printf("FATAL: Failed to update release %s: %s\n", release.ID, err) 363 264 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 364 - return 365 265 } 366 266 }) 367 267 } 368 268 369 - func UpdateReleaseCredits(release *model.Release) http.Handler { 269 + func UpdateReleaseTracks(release model.Release) http.Handler { 370 270 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 371 - if r.Method != http.MethodPut { 372 - http.NotFound(w, r) 271 + var trackIDs = []string{} 272 + err := json.NewDecoder(r.Body).Decode(&trackIDs) 273 + if err != nil { 274 + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 373 275 return 374 276 } 375 277 278 + tx := global.DB.MustBegin() 279 + tx.MustExec("DELETE FROM musicreleasetrack WHERE release=$1", release.ID) 280 + for i, trackID := range trackIDs { 281 + tx.MustExec( 282 + "INSERT INTO musicreleasetrack "+ 283 + "(release, track, number) "+ 284 + "VALUES ($1, $2, $3)", 285 + release.ID, 286 + trackID, 287 + i) 288 + } 289 + err = tx.Commit() 290 + if err != nil { 291 + fmt.Printf("Failed to update tracks for %s: %s\n", release.ID, err) 292 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 293 + } 294 + }) 295 + } 296 + 297 + func UpdateReleaseCredits(release model.Release) http.Handler { 298 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 376 299 type creditJSON struct { 377 300 Artist string 378 301 Role string 379 302 Primary bool 380 303 } 381 - 382 - var list []creditJSON 383 - err := json.NewDecoder(r.Body).Decode(&list) 304 + var data []creditJSON 305 + err := json.NewDecoder(r.Body).Decode(&data) 384 306 if err != nil { 385 307 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 386 308 return 387 309 } 388 310 389 - var credits = []*model.Credit{} 390 - for i, data := range list { 391 - if data.Artist == "" { 392 - http.Error(w, fmt.Sprintf("Artist ID cannot be blank (%d)", i), http.StatusBadRequest) 393 - return 394 - } 395 - 396 - for _, credit := range credits { 397 - if data.Artist == credit.Artist.ID { 398 - http.Error(w, fmt.Sprintf("Artist %s credited more than once", data.Artist), http.StatusBadRequest) 399 - return 400 - } 311 + // clear duplicates 312 + type Credit struct { 313 + Role string 314 + Primary bool 315 + } 316 + var credits = map[string]Credit{} 317 + for _, credit := range data { 318 + credits[credit.Artist] = Credit{ 319 + Role: credit.Role, 320 + Primary: credit.Primary, 401 321 } 322 + } 402 323 403 - if data.Role == "" { 404 - http.Error(w, fmt.Sprintf("Artist role cannot be blank (%d)", i), http.StatusBadRequest) 324 + tx := global.DB.MustBegin() 325 + tx.MustExec("DELETE FROM musiccredit WHERE release=$1", release.ID) 326 + for artistID := range credits { 327 + if credits[artistID].Role == "" { 328 + http.Error(w, fmt.Sprintf("Artist role cannot be blank (%s)", artistID), http.StatusBadRequest) 405 329 return 406 330 } 407 331 408 - var artist = global.GetArtist(data.Artist) 409 - if artist == nil { 410 - http.Error(w, fmt.Sprintf("Artist %s does not exist\n", data.Artist), http.StatusBadRequest) 332 + var exists int 333 + _ = global.DB.Get(&exists, "SELECT count(*) FROM artist WHERE id=$1", artistID) 334 + if exists == 0 { 335 + http.Error(w, fmt.Sprintf("Artist %s does not exist\n", artistID), http.StatusBadRequest) 411 336 return 412 337 } 413 338 414 - credits = append(credits, &model.Credit{ 415 - Artist: artist, 416 - Role: data.Role, 417 - Primary: data.Primary, 418 - }) 419 - } 420 - 421 - err = controller.UpdateReleaseCreditsDB(global.DB, release, credits) 422 - if err != nil { 423 - fmt.Printf("Failed to update links %s: %s\n", release.ID, err) 424 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 425 - return 426 - } 427 - 428 - release.Credits = credits 429 - 430 - w.Header().Add("Content-Type", "application/json") 431 - w.WriteHeader(http.StatusOK) 432 - err = json.NewEncoder(w).Encode(release) 339 + tx.MustExec( 340 + "INSERT INTO musiccredit "+ 341 + "(release, artist, role, is_primary) "+ 342 + "VALUES ($1, $2, $3, $4)", 343 + release.ID, 344 + artistID, 345 + credits[artistID].Role, 346 + credits[artistID].Primary) 347 + } 348 + err = tx.Commit() 433 349 if err != nil { 350 + fmt.Printf("Failed to update links for %s: %s\n", release.ID, err) 434 351 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 435 - return 436 352 } 437 353 }) 438 354 } 439 355 440 - func UpdateReleaseLinks(release *model.Release) http.Handler { 356 + func UpdateReleaseLinks(release model.Release) http.Handler { 441 357 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 442 358 if r.Method != http.MethodPut { 443 359 http.NotFound(w, r) ··· 451 367 return 452 368 } 453 369 454 - err = controller.UpdateReleaseLinksDB(global.DB, release, links) 455 - if err != nil { 456 - fmt.Printf("Failed to update links %s: %s\n", release.ID, err) 457 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 458 - return 370 + tx := global.DB.MustBegin() 371 + tx.MustExec("DELETE FROM musiclink WHERE release=$1", release.ID) 372 + for _, link := range links { 373 + tx.MustExec( 374 + "INSERT INTO musiclink "+ 375 + "(release, name, url) "+ 376 + "VALUES ($1, $2, $3)", 377 + release.ID, 378 + link.Name, 379 + link.URL) 459 380 } 460 - 461 - release.Links = links 462 - 463 - w.Header().Add("Content-Type", "application/json") 464 - w.WriteHeader(http.StatusOK) 465 - err = json.NewEncoder(w).Encode(release) 381 + err = tx.Commit() 466 382 if err != nil { 383 + fmt.Printf("Failed to update links for %s: %s\n", release.ID, err) 467 384 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 468 - return 469 385 } 470 386 }) 471 387 } 472 388 473 - func DeleteRelease() http.Handler { 389 + func DeleteRelease(release model.Release) http.Handler { 474 390 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 475 - if r.Method != http.MethodDelete { 476 - http.NotFound(w, r) 477 - return 478 - } 479 - 480 - var releaseID = r.URL.Path[1:] 481 - var release = global.GetRelease(releaseID) 482 - if release == nil { 483 - http.Error(w, fmt.Sprintf("Release %s does not exist\n", releaseID), http.StatusBadRequest) 484 - return 485 - } 486 - 487 - err := controller.DeleteReleaseDB(global.DB, release) 391 + _, err := global.DB.Exec("DELETE FROM musicrelease WHERE id=$1", release.ID) 488 392 if err != nil { 489 393 fmt.Printf("Failed to delete release %s: %s\n", release.ID, err) 490 394 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 491 - return 492 395 } 493 - 494 - global.Releases = func () []*model.Release { 495 - var releases = []*model.Release{} 496 - for _, r := range global.Releases { 497 - if r.ID == release.ID { continue } 498 - releases = append(releases, r) 499 - } 500 - return releases 501 - }() 502 - 503 - w.WriteHeader(http.StatusOK) 504 - w.Write([]byte(fmt.Sprintf("Release %s has been deleted\n", release.ID))) 505 396 }) 506 397 }
+77 -92
api/track.go
··· 7 7 8 8 "arimelody.me/arimelody.me/global" 9 9 "arimelody.me/arimelody.me/music/model" 10 - controller "arimelody.me/arimelody.me/music/controller" 11 10 ) 12 11 13 12 func ServeAllTracks() http.Handler { 14 13 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 - // type trackJSON struct { 16 - // model.Track 17 - // Release string `json:"release"` 18 - // } 19 - // var tracks = []trackJSON{} 20 - // 21 - // for _, track := range global.Tracks { 22 - // for _, release := range global. { 23 - // tracks = append(tracks, { 24 - // track, 25 - // Release 26 - // }) 27 - // } 14 + type track struct { 15 + ID string `json:"id"` 16 + Title string `json:"title"` 17 + } 18 + var tracks = []track{} 19 + 20 + err := global.DB.Select(&tracks, "SELECT id, title FROM musictrack") 21 + if err != nil { 22 + fmt.Printf("FATAL: Failed to pull tracks from DB: %s\n", err) 23 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 24 + } 28 25 29 26 w.Header().Add("Content-Type", "application/json") 30 - err := json.NewEncoder(w).Encode(global.Tracks) 27 + err = json.NewEncoder(w).Encode(tracks) 31 28 if err != nil { 29 + fmt.Printf("FATAL: Failed to serve all tracks: %s\n", err) 32 30 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 33 - return 34 31 } 35 32 }) 36 33 } 37 34 38 - func ServeTrack() http.Handler { 35 + func ServeTrack(track model.Track) http.Handler { 39 36 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 40 37 if r.URL.Path == "/" { 41 38 ServeAllTracks().ServeHTTP(w, r) ··· 43 40 } 44 41 45 42 var trackID = r.URL.Path[1:] 46 - var track = global.GetTrack(trackID) 47 - if track == nil { 43 + var track = model.Track{} 44 + err := global.DB.Get(&track, "SELECT * from musictrack WHERE id=$1", trackID) 45 + if err != nil { 48 46 http.NotFound(w, r) 49 47 return 50 48 } 51 49 50 + var releases = []*model.Release{} 51 + err = global.DB.Select(&releases, 52 + "SELECT * FROM musicrelease JOIN musicreleasetrack AS mrt "+ 53 + "WHERE mrt.track=$1 "+ 54 + "ORDER BY release_date", 55 + track.ID, 56 + ) 57 + if err != nil { 58 + fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", trackID, err) 59 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 60 + } 61 + 62 + type response struct { 63 + model.Track 64 + Releases []*model.Release 65 + } 66 + 52 67 w.Header().Add("Content-Type", "application/json") 53 - err := json.NewEncoder(w).Encode(track) 68 + err = json.NewEncoder(w).Encode(response{ track, releases }) 54 69 if err != nil { 70 + fmt.Printf("FATAL: Failed to serve track %s: %s\n", trackID, err) 55 71 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 56 - return 57 72 } 58 73 }) 59 74 } ··· 77 92 return 78 93 } 79 94 80 - trackID, err := controller.CreateTrackDB(global.DB, &track) 95 + var trackID string 96 + err = global.DB.Get(&trackID, 97 + "INSERT INTO musictrack (title, description, lyrics, preview_url) "+ 98 + "VALUES ($1, $2, $3, $4) "+ 99 + "RETURNING id", 100 + track.Title, 101 + track.Description, 102 + track.Lyrics, 103 + track.PreviewURL) 81 104 if err != nil { 82 - fmt.Printf("Failed to create track: %s\n", err) 105 + fmt.Printf("FATAL: Failed to create track: %s\n", err) 83 106 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 84 107 return 85 108 } 86 109 87 - track.ID = trackID 88 - global.Tracks = append(global.Tracks, &track) 89 - 90 - w.Header().Add("Content-Type", "application/json") 110 + w.Header().Add("Content-Type", "text/plain") 91 111 w.WriteHeader(http.StatusCreated) 92 - err = json.NewEncoder(w).Encode(track) 112 + w.Write([]byte(trackID)) 93 113 }) 94 114 } 95 115 96 - func UpdateTrack() http.Handler { 116 + func UpdateTrack(track model.Track) http.Handler { 97 117 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 98 - if r.Method != http.MethodPut { 118 + if r.Method != http.MethodPut || r.URL.Path == "/" { 99 119 http.NotFound(w, r) 100 120 return 101 121 } 102 122 103 - if r.URL.Path == "/" { 104 - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 105 - return 106 - } 107 - 108 - var data model.Track 109 - err := json.NewDecoder(r.Body).Decode(&data) 123 + var update model.Track 124 + err := json.NewDecoder(r.Body).Decode(&update) 110 125 if err != nil { 111 - fmt.Printf("Failed to update track: %s\n", err) 112 126 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 113 127 return 114 128 } 115 129 116 - var trackID = r.URL.Path[1:] 117 - var track = global.GetTrack(trackID) 118 - if track == nil { 119 - http.Error(w, fmt.Sprintf("Track %s does not exist\n", trackID), http.StatusBadRequest) 130 + if update.Title == "" { 131 + http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest) 120 132 return 121 133 } 122 134 123 - data.ID = trackID 124 - 125 - if data.Title == "" { data.Title = track.Title } 135 + var trackID = r.URL.Path[1:] 136 + var track = model.Track{} 137 + err = global.DB.Get(&track, "SELECT * from musictrack WHERE id=$1", trackID) 138 + if err != nil { 139 + http.NotFound(w, r) 140 + return 141 + } 126 142 127 - err = controller.UpdateTrackDB(global.DB, &data) 143 + _, err = global.DB.Exec( 144 + "UPDATE musictrack "+ 145 + "SET title=$2, description=$3, lyrics=$4, preview_url=$5 "+ 146 + "WHERE id=$1", 147 + track.ID, 148 + track.Title, 149 + track.Description, 150 + track.Lyrics, 151 + track.PreviewURL) 128 152 if err != nil { 129 153 fmt.Printf("Failed to update track %s: %s\n", track.ID, err) 130 154 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 131 155 return 132 156 } 133 157 134 - track.Title = data.Title 135 - track.Description = data.Description 136 - track.Lyrics = data.Lyrics 137 - track.PreviewURL = data.PreviewURL 138 - 139 158 w.Header().Add("Content-Type", "application/json") 140 159 err = json.NewEncoder(w).Encode(track) 141 160 if err != nil { 142 161 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 143 - return 144 162 } 145 163 }) 146 164 } 147 165 148 - func DeleteTrack() http.Handler { 166 + func DeleteTrack(track model.Track) http.Handler { 149 167 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 150 - if r.Method != http.MethodDelete { 168 + if r.Method != http.MethodDelete || r.URL.Path == "/" { 151 169 http.NotFound(w, r) 152 170 return 153 171 } 154 172 155 - if r.URL.Path == "/" { 156 - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 157 - return 158 - } 159 - 160 173 var trackID = r.URL.Path[1:] 161 - var track = global.GetTrack(trackID) 162 - if track == nil { 163 - http.Error(w, fmt.Sprintf("Track %s does not exist\n", trackID), http.StatusBadRequest) 164 - return 165 - } 166 - 167 - err := controller.DeleteTrackDB(global.DB, track) 174 + _, err := global.DB.Exec( 175 + "DELETE FROM musictrack "+ 176 + "WHERE id=$1", 177 + trackID) 168 178 if err != nil { 169 - fmt.Printf("Failed to delete track %s: %s\n", track.ID, err) 179 + fmt.Printf("Failed to delete track %s: %s\n", trackID, err) 170 180 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 171 - return 172 181 } 173 - 174 - // clear track from releases 175 - for _, release := range global.Releases { 176 - release.Tracks = func () []*model.Track { 177 - var tracks = []*model.Track{} 178 - for _, t := range release.Tracks { 179 - if t.ID == track.ID { continue } 180 - tracks = append(tracks, t) 181 - } 182 - return tracks 183 - }() 184 - } 185 - 186 - global.Tracks = func () []*model.Track { 187 - var tracks = []*model.Track{} 188 - for _, t := range global.Tracks { 189 - if t.ID == track.ID { continue } 190 - tracks = append(tracks, t) 191 - } 192 - return tracks 193 - }() 194 - 195 - w.WriteHeader(http.StatusOK) 196 - w.Write([]byte(fmt.Sprintf("Track %s has been deleted\n", track.ID))) 197 182 }) 198 183 }
+49
api/uploads.go
··· 1 + package api 2 + 3 + import ( 4 + "bufio" 5 + "encoding/base64" 6 + "errors" 7 + "fmt" 8 + "os" 9 + "path/filepath" 10 + "strings" 11 + ) 12 + 13 + func HandleImageUpload(data *string, directory string, filename string) (string, error) { 14 + split := strings.Split(*data, ";base64,") 15 + header := split[0] 16 + imageData, err := base64.StdEncoding.DecodeString(split[1]) 17 + ext, _ := strings.CutPrefix(header, "data:image/") 18 + 19 + switch ext { 20 + case "png": 21 + case "jpg": 22 + case "jpeg": 23 + default: 24 + return "", errors.New("Invalid image type. Allowed: .png, .jpg, .jpeg") 25 + } 26 + filename = fmt.Sprintf("%s.%s", filename, ext) 27 + 28 + // ensure directory exists 29 + os.MkdirAll(directory, os.ModePerm) 30 + 31 + imagePath := filepath.Join(directory, filename) 32 + file, err := os.Create(imagePath) 33 + if err != nil { 34 + return "", err 35 + } 36 + defer file.Close() 37 + 38 + buffer := bufio.NewWriter(file) 39 + _, err = buffer.Write(imageData) 40 + if err != nil { 41 + return "", nil 42 + } 43 + 44 + if err := buffer.Flush(); err != nil { 45 + return "", nil 46 + } 47 + 48 + return filename, nil 49 + }
-32
global/data.go
··· 5 5 "os" 6 6 "strings" 7 7 8 - "arimelody.me/arimelody.me/music/model" 9 8 "github.com/jmoiron/sqlx" 10 9 ) 11 10 ··· 44 43 }() 45 44 46 45 var DB *sqlx.DB 47 - 48 - var Releases []*model.Release 49 - var Artists []*model.Artist 50 - var Tracks []*model.Track 51 - 52 - func GetRelease(id string) *model.Release { 53 - for _, release := range Releases { 54 - if release.ID == id { 55 - return release 56 - } 57 - } 58 - return nil 59 - } 60 - 61 - func GetArtist(id string) *model.Artist { 62 - for _, artist := range Artists { 63 - if artist.ID == id { 64 - return artist 65 - } 66 - } 67 - return nil 68 - } 69 - 70 - func GetTrack(id string) *model.Track { 71 - for _, track := range Tracks { 72 - if track.ID == id { 73 - return track 74 - } 75 - } 76 - return nil 77 - }
-39
global/funcs.go
··· 3 3 import ( 4 4 "fmt" 5 5 "net/http" 6 - "os" 7 - "path/filepath" 8 - "html/template" 9 6 "strconv" 10 7 "time" 11 8 ··· 64 61 }) 65 62 } 66 63 67 - func ServeTemplate(page string, data any) http.Handler { 68 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 69 - lp_layout := filepath.Join("views", "layout.html") 70 - lp_header := filepath.Join("views", "header.html") 71 - lp_footer := filepath.Join("views", "footer.html") 72 - lp_prideflag := filepath.Join("views", "prideflag.html") 73 - fp := filepath.Join("views", filepath.Clean(page)) 74 - 75 - info, err := os.Stat(fp) 76 - if err != nil { 77 - if os.IsNotExist(err) { 78 - http.NotFound(w, r) 79 - return 80 - } 81 - } 82 - 83 - if info.IsDir() { 84 - http.NotFound(w, r) 85 - return 86 - } 87 - 88 - template, err := template.ParseFiles(lp_layout, lp_header, lp_footer, lp_prideflag, fp) 89 - if err != nil { 90 - fmt.Printf("Error parsing template files: %s\n", err) 91 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 92 - return 93 - } 94 - 95 - err = template.ExecuteTemplate(w, "layout.html", data) 96 - if err != nil { 97 - fmt.Printf("Error executing template: %s\n", err) 98 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 99 - return 100 - } 101 - }) 102 - }
+7 -28
main.go
··· 11 11 "arimelody.me/arimelody.me/admin" 12 12 "arimelody.me/arimelody.me/api" 13 13 "arimelody.me/arimelody.me/global" 14 - musicController "arimelody.me/arimelody.me/music/controller" 15 14 musicView "arimelody.me/arimelody.me/music/view" 15 + "arimelody.me/arimelody.me/templates" 16 16 17 17 "github.com/jmoiron/sqlx" 18 18 _ "github.com/lib/pq" ··· 25 25 var err error 26 26 global.DB, err = sqlx.Connect("postgres", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable") 27 27 if err != nil { 28 - fmt.Fprintf(os.Stderr, "unable to create database connection pool: %v\n", err) 28 + fmt.Fprintf(os.Stderr, "FATAL: Unable to create database connection pool: %v\n", err) 29 29 os.Exit(1) 30 30 } 31 31 global.DB.SetConnMaxLifetime(time.Minute * 3) 32 32 global.DB.SetMaxOpenConns(10) 33 33 global.DB.SetMaxIdleConns(10) 34 34 defer global.DB.Close() 35 - 36 - // pull artist data from DB 37 - global.Artists, err = musicController.PullAllArtists(global.DB) 38 - if err != nil { 39 - fmt.Printf("Failed to pull artists from database: %v\n", err); 40 - panic(1) 41 - } 42 - fmt.Printf("%d artists loaded successfully.\n", len(global.Artists)) 43 - 44 - // pull track data from DB 45 - global.Tracks, err = musicController.PullAllTracks(global.DB) 46 - if err != nil { 47 - fmt.Printf("Failed to pull tracks from database: %v\n", err); 48 - panic(1) 49 - } 50 - fmt.Printf("%d tracks loaded successfully.\n", len(global.Tracks)) 51 - 52 - // pull release data from DB 53 - global.Releases, err = musicController.PullAllReleases(global.DB) 54 - if err != nil { 55 - fmt.Printf("Failed to pull releases from database: %v\n", err); 56 - panic(1) 57 - } 58 - fmt.Printf("%d releases loaded successfully.\n", len(global.Releases)) 59 35 60 36 // start the web server! 61 37 mux := createServeMux() 62 38 port := DEFAULT_PORT 63 - fmt.Printf("now serving at http://127.0.0.1:%d\n", port) 39 + fmt.Printf("Now serving at http://127.0.0.1:%d\n", port) 64 40 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), global.HTTPLog(mux))) 65 41 } 66 42 ··· 73 49 mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler("uploads"))) 74 50 mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 75 51 if r.URL.Path == "/" || r.URL.Path == "/index.html" { 76 - global.ServeTemplate("index.html", nil).ServeHTTP(w, r) 52 + err := templates.Pages["index"].Execute(w, nil) 53 + if err != nil { 54 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 55 + } 77 56 return 78 57 } 79 58 staticHandler("public").ServeHTTP(w, r)
+30 -4
music/controller/artist.go
··· 7 7 8 8 // DATABASE 9 9 10 - func PullAllArtists(db *sqlx.DB) ([]*model.Artist, error) { 10 + func GetArtist(db *sqlx.DB, id string) (*model.Artist, error) { 11 + var artist = model.Artist{} 12 + 13 + err := db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id) 14 + if err != nil { 15 + return nil, err 16 + } 17 + 18 + return &artist, nil 19 + } 20 + 21 + func GetAllArtists(db *sqlx.DB) ([]*model.Artist, error) { 11 22 var artists = []*model.Artist{} 12 23 13 24 err := db.Select(&artists, "SELECT * FROM artist") ··· 18 29 return artists, nil 19 30 } 20 31 21 - func CreateArtistDB(db *sqlx.DB, artist *model.Artist) error { 32 + func GetArtistsNotOnRelease(db *sqlx.DB, release *model.Release) ([]*model.Artist, error) { 33 + var artists = []*model.Artist{} 34 + 35 + err := db.Select(&artists, 36 + "SELECT * FROM artist "+ 37 + "WHERE id NOT IN "+ 38 + "(SELECT artist FROM musiccredit WHERE release=$1)", 39 + release.ID) 40 + if err != nil { 41 + return nil, err 42 + } 43 + 44 + return artists, nil 45 + } 46 + 47 + func CreateArtist(db *sqlx.DB, artist *model.Artist) error { 22 48 _, err := db.Exec( 23 49 "INSERT INTO artist (id, name, website, avatar) "+ 24 50 "VALUES ($1, $2, $3, $4)", ··· 34 60 return nil 35 61 } 36 62 37 - func UpdateArtistDB(db *sqlx.DB, artist *model.Artist) error { 63 + func UpdateArtist(db *sqlx.DB, artist *model.Artist) error { 38 64 _, err := db.Exec( 39 65 "UPDATE artist "+ 40 66 "SET name=$2, website=$3, avatar=$4 "+ ··· 51 77 return nil 52 78 } 53 79 54 - func DeleteArtistDB(db *sqlx.DB, artist *model.Artist) error { 80 + func DeleteArtist(db *sqlx.DB, artist *model.Artist) error { 55 81 _, err := db.Exec( 56 82 "DELETE FROM artist "+ 57 83 "WHERE id=$1",
+10 -24
music/controller/credit.go
··· 1 1 package music 2 2 3 3 import ( 4 - "arimelody.me/arimelody.me/global" 5 4 "arimelody.me/arimelody.me/music/model" 6 5 "github.com/jmoiron/sqlx" 7 6 ) 8 7 9 8 // DATABASE 10 9 11 - func PullReleaseCredits(db *sqlx.DB, releaseID string) ([]*model.Credit, error) { 12 - type creditDB struct { 13 - Artist string 14 - Role string 15 - Primary bool `db:"is_primary"` 16 - } 17 - var credit_rows = []creditDB{} 18 - var credits = []*model.Credit{} 10 + func GetReleaseCredits(db *sqlx.DB, release *model.Release) ([]model.Credit, error) { 11 + var credits = []model.Credit{} 19 12 20 - err := db.Select( 21 - &credit_rows, 22 - "SELECT artist, role, is_primary FROM musiccredit WHERE release=$1", 23 - releaseID, 13 + err := db.Select(&credits, 14 + "SELECT artist.*,role,is_primary FROM musiccredit "+ 15 + "JOIN artist ON artist=id "+ 16 + "WHERE release=$1", 17 + release.ID, 24 18 ) 25 19 if err != nil { 26 20 return nil, err 27 21 } 28 22 29 - for _, c := range credit_rows { 30 - credits = append(credits, &model.Credit{ 31 - Artist: global.GetArtist(c.Artist), 32 - Role: c.Role, 33 - Primary: c.Primary, 34 - }) 35 - } 36 - 37 23 return credits, nil 38 24 } 39 25 40 - func CreateCreditDB(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { 26 + func CreateCredit(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { 41 27 _, err := db.Exec( 42 28 "INSERT INTO musiccredit (release, artist, role, is_primary) "+ 43 29 "VALUES ($1, $2, $3, $4)", ··· 53 39 return nil 54 40 } 55 41 56 - func UpdateCreditDB(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { 42 + func UpdateCredit(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { 57 43 _, err := db.Exec( 58 44 "UPDATE musiccredit SET "+ 59 45 "role=$3, is_primary=$4 "+ ··· 70 56 return nil 71 57 } 72 58 73 - func DeleteCreditDB(db *sqlx.DB, releaseID string, artistID string) (error) { 59 + func DeleteCredit(db *sqlx.DB, releaseID string, artistID string) (error) { 74 60 _, err := db.Exec( 75 61 "DELETE FROM musiccredit "+ 76 62 "WHERE release=$1, artist=$2",
+6 -10
music/controller/link.go
··· 7 7 8 8 // DATABASE 9 9 10 - func PullReleaseLinks(db *sqlx.DB, releaseID string) ([]*model.Link, error) { 11 - var links = []*model.Link{} 10 + func GetReleaseLinks(db *sqlx.DB, release *model.Release) ([]model.Link, error) { 11 + var links = []model.Link{} 12 12 13 - err := db.Select( 14 - &links, 15 - "SELECT name, url FROM musiclink WHERE release=$1", 16 - releaseID, 17 - ) 13 + err := db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", release.ID) 18 14 if err != nil { 19 15 return nil, err 20 16 } ··· 22 18 return links, nil 23 19 } 24 20 25 - func CreateLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { 21 + func CreateLink(db *sqlx.DB, releaseID string, link *model.Link) (error) { 26 22 _, err := db.Exec( 27 23 "INSERT INTO musiclink (release, name, url) "+ 28 24 "VALUES ($1, $2, $3)", ··· 37 33 return nil 38 34 } 39 35 40 - func UpdateLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { 36 + func UpdateLink(db *sqlx.DB, releaseID string, link *model.Link) (error) { 41 37 _, err := db.Exec( 42 38 "UPDATE musiclink SET "+ 43 39 "name=$2, url=$3 "+ ··· 53 49 return nil 54 50 } 55 51 56 - func DeleteLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { 52 + func DeleteLink(db *sqlx.DB, releaseID string, link *model.Link) (error) { 57 53 _, err := db.Exec( 58 54 "DELETE FROM musiclink "+ 59 55 "WHERE release=$1, name=$2",
+52 -43
music/controller/release.go
··· 1 1 package music 2 2 3 3 import ( 4 - "errors" 5 - "fmt" 6 - 7 - "arimelody.me/arimelody.me/global" 8 4 "arimelody.me/arimelody.me/music/model" 9 5 "github.com/jmoiron/sqlx" 10 6 ) 11 7 12 - // DATABASE 8 + func GetRelease(db *sqlx.DB, id string) (*model.Release, error) { 9 + var releases = model.Release{} 13 10 14 - func PullAllReleases(db *sqlx.DB) ([]*model.Release, error) { 11 + err := db.Get(&releases, "SELECT * FROM musicrelease WHERE id=$1", id) 12 + if err != nil { 13 + return nil, err 14 + } 15 + 16 + return &releases, nil 17 + } 18 + 19 + func GetAllReleases(db *sqlx.DB) ([]*model.Release, error) { 15 20 var releases = []*model.Release{} 16 21 17 22 err := db.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") ··· 19 24 return nil, err 20 25 } 21 26 22 - for _, release := range releases { 23 - release.Credits, err = PullReleaseCredits(global.DB, release.ID) 24 - if err != nil { 25 - fmt.Printf("Error pulling credits for %s: %s\n", release.ID, err) 26 - release.Credits = []*model.Credit{} 27 - } 28 - release.Links, err = PullReleaseLinks(global.DB, release.ID) 29 - if err != nil { 30 - fmt.Printf("Error pulling links for %s: %s\n", release.ID, err) 31 - release.Links = []*model.Link{} 32 - } 33 - release.Tracks, err = PullReleaseTracksDB(global.DB, release) 34 - if err != nil { 35 - fmt.Printf("Error pulling tracks for %s: %s\n", release.ID, err) 36 - release.Tracks = []*model.Track{} 37 - } 38 - } 39 - 40 27 return releases, nil 41 28 } 42 29 43 - func PullReleaseTracksDB(db *sqlx.DB, release *model.Release) ([]*model.Track, error) { 44 - var track_rows = []string{} 30 + func GetReleaseTracks(db *sqlx.DB, release *model.Release) ([]*model.Track, error) { 45 31 var tracks = []*model.Track{} 46 32 47 - err := db.Select(&track_rows, 48 - "SELECT track FROM musicreleasetrack "+ 33 + err := db.Select(&tracks, 34 + "SELECT musictrack.* FROM musictrack "+ 35 + "JOIN musicreleasetrack ON track=id "+ 49 36 "WHERE release=$1 "+ 50 37 "ORDER BY number ASC", 51 38 release.ID, ··· 54 41 return nil, err 55 42 } 56 43 57 - for _, trackID := range track_rows { 58 - var track = global.GetTrack(trackID) 59 - if track == nil { 60 - return nil, errors.New("Recieved a track from the DB that does not exist in memory") 61 - } 62 - track.Release = release 63 - tracks = append(tracks, track) 64 - } 65 - 66 44 return tracks, nil 67 45 } 68 46 69 - func CreateReleaseDB(db *sqlx.DB, release *model.Release) error { 47 + func CreateRelease(db *sqlx.DB, release *model.Release) error { 70 48 _, err := db.Exec( 71 49 "INSERT INTO musicrelease "+ 72 50 "(id, visible, title, description, type, release_date, artwork, buyname, buylink) "+ ··· 88 66 return nil 89 67 } 90 68 91 - func UpdateReleaseDB(db *sqlx.DB, release *model.Release) error { 69 + func UpdateRelease(db *sqlx.DB, release *model.Release) error { 92 70 _, err := db.Exec( 93 71 "UPDATE musicrelease SET "+ 94 72 "visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9 "+ ··· 110 88 return nil 111 89 } 112 90 113 - func UpdateReleaseTracksDB(db *sqlx.DB, release *model.Release, new_tracks []*model.Track) error { 91 + func UpdateReleaseTracks(db *sqlx.DB, release *model.Release, new_tracks []*model.Track) error { 114 92 _, err := db.Exec( 115 93 "DELETE FROM musicreleasetrack "+ 116 94 "WHERE release=$1", ··· 137 115 return nil 138 116 } 139 117 140 - func UpdateReleaseCreditsDB(db *sqlx.DB, release *model.Release, new_credits []*model.Credit) error { 118 + func UpdateReleaseCredits(db *sqlx.DB, release *model.Release, new_credits []*model.Credit) error { 141 119 _, err := db.Exec( 142 120 "DELETE FROM musiccredit "+ 143 121 "WHERE release=$1", ··· 165 143 return nil 166 144 } 167 145 168 - func UpdateReleaseLinksDB(db *sqlx.DB, release *model.Release, new_links []*model.Link) error { 146 + func UpdateReleaseLinks(db *sqlx.DB, release *model.Release, new_links []*model.Link) error { 169 147 _, err := db.Exec( 170 148 "DELETE FROM musiclink "+ 171 149 "WHERE release=$1", ··· 192 170 return nil 193 171 } 194 172 195 - func DeleteReleaseDB(db *sqlx.DB, release *model.Release) error { 173 + func DeleteRelease(db *sqlx.DB, release *model.Release) error { 196 174 _, err := db.Exec( 197 175 "DELETE FROM musicrelease "+ 198 176 "WHERE id=$1", ··· 204 182 205 183 return nil 206 184 } 185 + 186 + func GetFullRelease(db *sqlx.DB, release *model.Release) (*model.FullRelease, error) { 187 + // get credits 188 + credits, err := GetReleaseCredits(db, release) 189 + if err != nil { 190 + return nil, err 191 + } 192 + 193 + // get tracks 194 + dbTracks, err := GetReleaseTracks(db, release) 195 + if err != nil { 196 + return nil, err 197 + } 198 + tracks := []model.DisplayTrack{} 199 + for i, track := range dbTracks { 200 + tracks = append(tracks, track.MakeDisplay(i + 1)) 201 + } 202 + 203 + // get links 204 + links, err := GetReleaseLinks(db, release) 205 + if err != nil { 206 + return nil, err 207 + } 208 + 209 + return &model.FullRelease{ 210 + Release: release, 211 + Tracks: tracks, 212 + Credits: credits, 213 + Links: links, 214 + }, nil 215 + }
+59 -5
music/controller/track.go
··· 7 7 8 8 // DATABASE 9 9 10 - func PullAllTracks(db *sqlx.DB) ([]*model.Track, error) { 10 + func GetTrack(db *sqlx.DB, id string) (*model.Track, error) { 11 + var track = model.Track{} 12 + 13 + stmt, _ := db.Preparex("SELECT * FROM musictrack WHERE id=$1") 14 + err := stmt.Get(&track, id) 15 + if err != nil { 16 + return nil, err 17 + } 18 + return &track, nil 19 + } 20 + 21 + func GetAllTracks(db *sqlx.DB) ([]*model.Track, error) { 11 22 var tracks = []*model.Track{} 12 23 13 - err := db.Select(&tracks, "SELECT id, title, description, lyrics, preview_url FROM musictrack") 24 + err := db.Select(&tracks, "SELECT * FROM musictrack") 14 25 if err != nil { 15 26 return nil, err 16 27 } ··· 18 29 return tracks, nil 19 30 } 20 31 32 + func GetOrphanTracks(db *sqlx.DB) ([]*model.Track, error) { 33 + var tracks = []*model.Track{} 34 + 35 + err := db.Select(&tracks, "SELECT * FROM musictrack WHERE id NOT IN (SELECT track FROM musicreleasetrack)") 36 + if err != nil { 37 + return nil, err 38 + } 39 + 40 + return tracks, nil 41 + } 42 + 43 + func GetTracksNotOnRelease(db *sqlx.DB, release *model.Release) ([]*model.Track, error) { 44 + var tracks = []*model.Track{} 45 + 46 + err := db.Select(&tracks, 47 + "SELECT * FROM musictrack "+ 48 + "WHERE id NOT IN "+ 49 + "(SELECT track FROM musicreleasetrack WHERE release=$1)", 50 + release.ID) 51 + if err != nil { 52 + return nil, err 53 + } 54 + 55 + return tracks, nil 56 + } 57 + 58 + func GetTrackReleases(db *sqlx.DB, track *model.Track) ([]*model.Release, error) { 59 + var releases = []*model.Release{} 60 + 61 + err := db.Select(&releases, 62 + "SELECT musicrelease.* FROM musicrelease "+ 63 + "JOIN musicreleasetrack ON release=id "+ 64 + "WHERE track=$1 "+ 65 + "ORDER BY release_date", 66 + track.ID, 67 + ) 68 + if err != nil { 69 + return nil, err 70 + } 71 + 72 + return releases, nil 73 + } 74 + 21 75 func PullOrphanTracks(db *sqlx.DB) ([]*model.Track, error) { 22 76 var tracks = []*model.Track{} 23 77 ··· 33 87 return tracks, nil 34 88 } 35 89 36 - func CreateTrackDB(db *sqlx.DB, track *model.Track) (string, error) { 90 + func CreateTrack(db *sqlx.DB, track *model.Track) (string, error) { 37 91 var trackID string 38 92 err := db.QueryRow( 39 93 "INSERT INTO musictrack (title, description, lyrics, preview_url) "+ ··· 51 105 return trackID, nil 52 106 } 53 107 54 - func UpdateTrackDB(db *sqlx.DB, track *model.Track) error { 108 + func UpdateTrack(db *sqlx.DB, track *model.Track) error { 55 109 _, err := db.Exec( 56 110 "UPDATE musictrack "+ 57 111 "SET title=$2, description=$3, lyrics=$4, preview_url=$5 "+ ··· 69 123 return nil 70 124 } 71 125 72 - func DeleteTrackDB(db *sqlx.DB, track *model.Track) error { 126 + func DeleteTrack(db *sqlx.DB, track *model.Track) error { 73 127 _, err := db.Exec( 74 128 "DELETE FROM musictrack "+ 75 129 "WHERE id=$1",
+59 -4
music/model/artist.go
··· 1 1 package model 2 2 3 + import "strings" 4 + 3 5 type ( 4 6 Artist struct { 5 - ID string `json:"id"` 7 + ID string `json:"id"` 6 8 Name string `json:"name"` 7 9 Website string `json:"website"` 8 10 Avatar string `json:"avatar"` ··· 14 16 } 15 17 16 18 func (artist Artist) GetAvatar() string { 17 - if artist.Avatar == "" { 18 - return "/img/default-avatar.png" 19 - } 19 + if artist.Avatar == "" { 20 + return "/img/default-avatar.png" 21 + } 20 22 return artist.Avatar 21 23 } 24 + 25 + func (release FullRelease) GetUniqueArtists(only_primary bool) []*Artist { 26 + var artists = []*Artist{} 27 + 28 + for _, credit := range release.Credits { 29 + if only_primary && !credit.Primary { 30 + continue 31 + } 32 + 33 + exists := false 34 + for _, a := range artists { 35 + if a.ID == credit.Artist.ID { 36 + exists = true 37 + break 38 + } 39 + } 40 + 41 + if exists { 42 + continue 43 + } 44 + 45 + artists = append(artists, &credit.Artist) 46 + } 47 + 48 + return artists 49 + } 50 + 51 + func (release FullRelease) GetUniqueArtistNames(only_primary bool) []string { 52 + var names = []string{} 53 + for _, artist := range release.GetUniqueArtists(only_primary) { 54 + names = append(names, artist.Name) 55 + } 56 + 57 + return names 58 + } 59 + 60 + func (release FullRelease) PrintArtists(only_primary bool, ampersand bool) string { 61 + names := release.GetUniqueArtistNames(only_primary) 62 + 63 + if len(names) == 0 { 64 + return "Unknown Artist" 65 + } else if len(names) == 1 { 66 + return names[0] 67 + } 68 + 69 + if ampersand { 70 + res := strings.Join(names[:len(names)-1], ", ") 71 + res += " & " + names[len(names)-1] 72 + return res 73 + } else { 74 + return strings.Join(names[:], ", ") 75 + } 76 + }
+1 -1
music/model/credit.go
··· 1 1 package model 2 2 3 3 type Credit struct { 4 - Artist *Artist `json:"artist"` 4 + Artist `json:"artist"` 5 5 Role string `json:"role"` 6 6 Primary bool `json:"primary" db:"is_primary"` 7 7 }
+2 -2
music/model/link.go
··· 6 6 ) 7 7 8 8 type Link struct { 9 - Name string `json:"name"` 10 - URL string `json:"url"` 9 + Name string `json:"name"` 10 + URL string `json:"url"` 11 11 } 12 12 13 13 func (link Link) NormaliseName() string {
+21 -68
music/model/release.go
··· 1 1 package model 2 2 3 3 import ( 4 - "strings" 5 4 "time" 6 5 ) 7 6 8 7 type ( 9 8 ReleaseType string 10 - Release struct { 11 - ID string `json:"id"` 12 - Visible bool `json:"visible"` 13 - Title string `json:"title"` 14 - Description string `json:"description"` 15 - ReleaseType ReleaseType `json:"type" db:"type"` 16 - ReleaseDate time.Time `json:"releaseDate" db:"release_date"` 17 - Artwork string `json:"artwork"` 18 - Buyname string `json:"buyname"` 19 - Buylink string `json:"buylink"` 20 - Links []*Link `json:"links"` 21 - Credits []*Credit `json:"credits"` 22 - Tracks []*Track `json:"tracks"` 9 + 10 + Release struct { 11 + ID string `json:"id"` 12 + Visible bool `json:"visible"` 13 + Title string `json:"title"` 14 + Description string `json:"description"` 15 + ReleaseType ReleaseType `json:"type" db:"type"` 16 + ReleaseDate time.Time `json:"releaseDate" db:"release_date"` 17 + Artwork string `json:"artwork"` 18 + Buyname string `json:"buyname"` 19 + Buylink string `json:"buylink"` 20 + Copyright string `json:"copyright" db:"copyright"` 21 + CopyrightURL string `json:"copyrightURL" db:"copyrighturl"` 22 + } 23 + 24 + FullRelease struct { 25 + *Release 26 + Tracks []DisplayTrack 27 + Credits []Credit 28 + Links []Link 23 29 } 24 30 ) 25 31 ··· 52 58 return release.Artwork 53 59 } 54 60 55 - func (release Release) IsSingle() bool { 61 + func (release FullRelease) IsSingle() bool { 56 62 return len(release.Tracks) == 1; 57 63 } 58 64 59 65 func (release Release) IsReleased() bool { 60 66 return release.ReleaseDate.Before(time.Now()) 61 67 } 62 - 63 - func (release Release) GetUniqueArtists(only_primary bool) []Artist { 64 - var artists = []Artist{} 65 - 66 - for _, credit := range release.Credits { 67 - if only_primary && !credit.Primary { 68 - continue 69 - } 70 - 71 - exists := false 72 - for _, a := range artists { 73 - if a.ID == credit.Artist.ID { 74 - exists = true 75 - break 76 - } 77 - } 78 - 79 - if exists { 80 - continue 81 - } 82 - 83 - artists = append(artists, *credit.Artist) 84 - } 85 - 86 - return artists 87 - } 88 - 89 - func (release Release) GetUniqueArtistNames(only_primary bool) []string { 90 - var names = []string{} 91 - for _, artist := range release.GetUniqueArtists(only_primary) { 92 - names = append(names, artist.Name) 93 - } 94 - 95 - return names 96 - } 97 - 98 - func (release Release) PrintArtists(only_primary bool, ampersand bool) string { 99 - names := release.GetUniqueArtistNames(only_primary) 100 - 101 - if len(names) == 0 { 102 - return "Unknown Artist" 103 - } else if len(names) == 1 { 104 - return names[0] 105 - } 106 - 107 - if ampersand { 108 - res := strings.Join(names[:len(names)-1], ", ") 109 - res += " & " + names[len(names)-1] 110 - return res 111 - } else { 112 - return strings.Join(names[:], ", ") 113 - } 114 - }
+27 -7
music/model/track.go
··· 1 1 package model 2 2 3 - type Track struct { 4 - ID string `json:"id"` 5 - Title string `json:"title"` 6 - Description string `json:"description"` 7 - Lyrics string `json:"lyrics"` 8 - PreviewURL string `json:"previewURL" db:"preview_url"` 9 - Release *Release `json:"-" db:"-"` 3 + import ( 4 + "html/template" 5 + "strings" 6 + ) 7 + 8 + type ( 9 + Track struct { 10 + ID string `json:"id"` 11 + Title string `json:"title"` 12 + Description string `json:"description"` 13 + Lyrics string `json:"lyrics"` 14 + PreviewURL string `json:"previewURL" db:"preview_url"` 15 + } 16 + 17 + DisplayTrack struct { 18 + *Track 19 + Lyrics template.HTML 20 + Number int 21 + } 22 + ) 23 + 24 + func (track Track) MakeDisplay(number int) DisplayTrack { 25 + return DisplayTrack{ 26 + Track: &track, 27 + Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)), 28 + Number: number, 29 + } 10 30 }
+28 -58
music/view/music.go
··· 1 1 package view 2 2 3 3 import ( 4 + "fmt" 4 5 "net/http" 5 6 6 7 "arimelody.me/arimelody.me/global" 8 + music "arimelody.me/arimelody.me/music/controller" 7 9 "arimelody.me/arimelody.me/music/model" 10 + "arimelody.me/arimelody.me/templates" 8 11 ) 9 12 10 13 // HTTP HANDLER METHODS ··· 17 20 ServeCatalog().ServeHTTP(w, r) 18 21 return 19 22 } 20 - ServeGateway().ServeHTTP(w, r) 21 - })) 22 23 23 - return mux 24 - } 25 - 26 - func ServeCatalog() http.Handler { 27 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 - releases := []model.Release{} 29 - for _, r := range global.Releases { 30 - if r.Visible { 31 - release := *r 32 - if !release.IsReleased() { 33 - release.ReleaseType = model.Upcoming 34 - } 35 - releases = append(releases, release) 36 - } 37 - } 38 - 39 - global.ServeTemplate("music.html", releases).ServeHTTP(w, r) 40 - }) 41 - } 42 - 43 - /* 44 - func ServeArtwork() http.Handler { 45 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 - if r.URL.Path == "/" { 24 + var release model.Release 25 + err := global.DB.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", r.URL.Path[1:]) 26 + if err != nil { 47 27 http.NotFound(w, r) 48 28 return 49 29 } 50 30 51 - if !strings.HasSuffix(r.URL.Path, ".png") { 52 - http.NotFound(w, r) 53 - return 54 - } 31 + ServeGateway(release).ServeHTTP(w, r) 32 + })) 55 33 56 - releaseID := r.URL.Path[1:len(r.URL.Path) - 4] 57 - var release = GetRelease(releaseID) 58 - if release == nil { 59 - http.NotFound(w, r) 60 - return 61 - } 34 + return mux 35 + } 62 36 63 - // only allow authorised users to view unreleased releases 64 - authorised := r.Context().Value("role") != nil && r.Context().Value("role") == "admin" 65 - if !release.IsReleased() && !authorised { 66 - admin.MustAuthorise(ServeArtwork()).ServeHTTP(w, r) 37 + func ServeCatalog() http.Handler { 38 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 + dbReleases, err := music.GetAllReleases(global.DB) 40 + if err != nil { 41 + fmt.Printf("FATAL: Failed to pull releases for catalog: %s\n", err) 42 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 67 43 return 68 44 } 69 - 70 - fp := filepath.Join("data", "music-artwork", releaseID + ".png") 71 - info, err := os.Stat(fp) 72 - if err != nil { 73 - if os.IsNotExist(err) { 74 - http.NotFound(w, r) 45 + releases := []*model.FullRelease{} 46 + for _, dbRelease := range dbReleases { 47 + if !dbRelease.Visible { continue } 48 + if !dbRelease.IsReleased() { 49 + dbRelease.ReleaseType = model.Upcoming 50 + } 51 + release, err := music.GetFullRelease(global.DB, dbRelease) 52 + if err != nil { 53 + fmt.Printf("FATAL: Failed to pull full release for %s: %s\n", dbRelease.ID, err) 54 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 75 55 return 76 56 } 57 + releases = append(releases, release) 77 58 } 78 - length := info.Size() 79 59 80 - file, err := os.Open(fp) 60 + err = templates.Pages["music"].Execute(w, releases) 81 61 if err != nil { 82 62 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 83 - return 84 63 } 85 - defer file.Close() 86 - 87 - var bytes = make([]byte, length) 88 - file.Read(bytes) 89 - 90 - w.Header().Add("Content-Type", "image/png") 91 - w.WriteHeader(http.StatusOK) 92 - w.Write(bytes) 93 64 }) 94 65 } 95 - */
+34 -59
music/view/release.go
··· 3 3 import ( 4 4 "encoding/json" 5 5 "fmt" 6 - "html/template" 7 6 "net/http" 8 - "strings" 9 7 10 8 "arimelody.me/arimelody.me/admin" 11 9 "arimelody.me/arimelody.me/global" 12 10 "arimelody.me/arimelody.me/music/model" 13 - ) 14 - 15 - type ( 16 - gatewayTrack struct { 17 - *model.Track 18 - Lyrics template.HTML 19 - Number int 20 - } 21 - 22 - gatewayRelease struct { 23 - *model.Release 24 - Tracks []gatewayTrack 25 - Authorised bool 26 - } 11 + db "arimelody.me/arimelody.me/music/controller" 12 + "arimelody.me/arimelody.me/templates" 27 13 ) 28 14 29 15 // HTTP HANDLERS 30 16 31 - func ServeRelease() http.Handler { 17 + func ServeRelease(release model.Release) http.Handler { 32 18 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 - if r.URL.Path == "/" { 34 - http.NotFound(w, r) 35 - return 36 - } 37 - 38 - releaseID := r.URL.Path[1:] 39 - var releaseRef = global.GetRelease(releaseID) 40 - if releaseRef == nil { 41 - http.NotFound(w, r) 42 - return 43 - } 44 - var release = *releaseRef 45 - 46 19 // only allow authorised users to view hidden releases 47 20 authorised := admin.GetSession(r) != nil 48 21 if !authorised && !release.Visible { 49 22 http.NotFound(w, r) 50 23 return 51 24 } 52 - if !authorised && !release.IsReleased() { 53 - release.Tracks = nil 54 - release.Credits = nil 25 + 26 + fullRelease := &model.FullRelease{ 27 + Release: &release, 28 + } 29 + 30 + if authorised || release.IsReleased() { 31 + fullerRelease, err := db.GetFullRelease(global.DB, &release) 32 + if err != nil { 33 + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) 34 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 35 + return 36 + } 37 + fullRelease = fullerRelease 55 38 } 56 39 57 40 w.Header().Add("Content-Type", "application/json") 58 - err := json.NewEncoder(w).Encode(release) 41 + err := json.NewEncoder(w).Encode(fullRelease) 59 42 if err != nil { 60 43 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 61 44 return ··· 63 46 }) 64 47 } 65 48 66 - func ServeGateway() http.Handler { 49 + func ServeGateway(release model.Release) http.Handler { 67 50 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 68 - if r.URL.Path == "/" { 69 - http.Redirect(w, r, "/music", http.StatusPermanentRedirect) 70 - return 71 - } 72 - 73 - id := r.URL.Path[1:] 74 - release := global.GetRelease(id) 75 - if release == nil { 76 - http.NotFound(w, r) 77 - return 78 - } 79 - 80 51 // only allow authorised users to view hidden releases 81 52 authorised := admin.GetSession(r) != nil 82 - if !release.Visible && !authorised { 53 + if !authorised && !release.Visible { 83 54 http.NotFound(w, r) 84 55 return 85 56 } 86 57 87 - tracks := []gatewayTrack{} 88 - for i, track := range release.Tracks { 89 - tracks = append(tracks, gatewayTrack{ 90 - Track: track, 91 - Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)), 92 - Number: i + 1, 93 - }) 58 + fullRelease := &model.FullRelease{ 59 + Release: &release, 94 60 } 95 61 96 - lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK} 62 + if authorised || release.IsReleased() { 63 + fullerRelease, err := db.GetFullRelease(global.DB, &release) 64 + if err != nil { 65 + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) 66 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 67 + return 68 + } 69 + fullRelease = fullerRelease 70 + } 97 71 98 - global.ServeTemplate("music-gateway.html", gatewayRelease{release, tracks, authorised}).ServeHTTP(&lrw, r) 72 + err := templates.Pages["music-gateway"].Execute(w, fullRelease) 99 73 100 - if lrw.Code != http.StatusOK { 101 - fmt.Printf("Error rendering music gateway for %s\n", id) 74 + if err != nil { 75 + fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err) 76 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 102 77 return 103 78 } 104 79 })
+3 -1
schema.sql
··· 21 21 release_date TIMESTAMP NOT NULL, 22 22 artwork text, 23 23 buyname text, 24 - buylink text 24 + buylink text, 25 + copyright text, 26 + copyrightURL text 25 27 ); 26 28 ALTER TABLE public.musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); 27 29
+33
templates/templates.go
··· 1 + package templates 2 + 3 + import ( 4 + "html/template" 5 + "path/filepath" 6 + ) 7 + 8 + var Pages = map[string]*template.Template{ 9 + "index": template.Must(template.ParseFiles( 10 + filepath.Join("views", "layout.html"), 11 + filepath.Join("views", "header.html"), 12 + filepath.Join("views", "footer.html"), 13 + filepath.Join("views", "prideflag.html"), 14 + filepath.Join("views", "index.html"), 15 + )), 16 + "music": template.Must(template.ParseFiles( 17 + filepath.Join("views", "layout.html"), 18 + filepath.Join("views", "header.html"), 19 + filepath.Join("views", "footer.html"), 20 + filepath.Join("views", "prideflag.html"), 21 + filepath.Join("views", "music.html"), 22 + )), 23 + "music-gateway": template.Must(template.ParseFiles( 24 + filepath.Join("views", "layout.html"), 25 + filepath.Join("views", "header.html"), 26 + filepath.Join("views", "footer.html"), 27 + filepath.Join("views", "prideflag.html"), 28 + filepath.Join("views", "music-gateway.html"), 29 + )), 30 + } 31 + 32 + var Components = map[string]*template.Template{ 33 + }