home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

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

lots of post-DB cleanup

+409 -547
+1 -1
admin/components/release/release-list-item.html
··· 1 1 {{define "release"}} 2 2 <div class="release"> 3 3 <div class="release-artwork"> 4 - <img src="{{.Artwork}}" alt="" width="128" loading="lazy"> 4 + <img src="{{.GetArtwork}}" alt="" width="128" loading="lazy"> 5 5 </div> 6 6 <div class="release-info"> 7 7 <h3 class="release-title">
+1 -1
admin/http.go
··· 57 57 } 58 58 releases := []musicModel.FullRelease{} 59 59 for _, release := range dbReleases { 60 - fullRelease, err := musicDB.GetFullRelease(global.DB, release) 60 + fullRelease, err := musicDB.GetFullRelease(global.DB, release.ID) 61 61 if err != nil { 62 62 fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) 63 63 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+18 -24
admin/releasehttp.go
··· 6 6 "strings" 7 7 8 8 "arimelody.me/arimelody.me/global" 9 + db "arimelody.me/arimelody.me/music/controller" 9 10 "arimelody.me/arimelody.me/music/model" 10 - controller "arimelody.me/arimelody.me/music/controller" 11 11 ) 12 12 13 13 func serveRelease() http.Handler { 14 14 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 15 slices := strings.Split(r.URL.Path[1:], "/") 16 16 releaseID := slices[0] 17 - release, err := controller.GetRelease(global.DB, releaseID) 17 + 18 + release, err := db.GetFullRelease(global.DB, releaseID) 18 19 if err != nil { 19 - fmt.Printf("FATAL: Failed to pull release %s: %s\n", releaseID, err) 20 + if strings.Contains(err.Error(), "no rows") { 21 + http.NotFound(w, r) 22 + return 23 + } 24 + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) 20 25 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 21 26 return 22 27 } 23 - if release == nil { 24 - http.NotFound(w, r) 25 - return 26 - } 27 28 28 29 authorised := GetSession(r) != nil 29 30 if !authorised && !release.Visible { ··· 31 32 return 32 33 } 33 34 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 - 41 35 if len(slices) > 1 { 42 36 switch slices[1] { 43 37 case "editcredits": 44 - serveEditCredits(fullRelease).ServeHTTP(w, r) 38 + serveEditCredits(release).ServeHTTP(w, r) 45 39 return 46 40 case "addcredit": 47 - serveAddCredit(fullRelease).ServeHTTP(w, r) 41 + serveAddCredit(release).ServeHTTP(w, r) 48 42 return 49 43 case "newcredit": 50 44 serveNewCredit().ServeHTTP(w, r) 51 45 return 52 46 case "editlinks": 53 - serveEditLinks(fullRelease).ServeHTTP(w, r) 47 + serveEditLinks(release).ServeHTTP(w, r) 54 48 return 55 49 case "edittracks": 56 - serveEditTracks(fullRelease).ServeHTTP(w, r) 50 + serveEditTracks(release).ServeHTTP(w, r) 57 51 return 58 52 case "addtrack": 59 - serveAddTrack(fullRelease).ServeHTTP(w, r) 53 + serveAddTrack(release).ServeHTTP(w, r) 60 54 return 61 55 case "newtrack": 62 56 serveNewTrack().ServeHTTP(w, r) ··· 66 60 return 67 61 } 68 62 69 - err = pages["release"].Execute(w, fullRelease) 63 + err = pages["release"].Execute(w, release) 70 64 if err != nil { 71 65 fmt.Printf("Error rendering admin release page for %s: %s\n", release.ID, err) 72 66 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 87 81 88 82 func serveAddCredit(release *model.FullRelease) http.Handler { 89 83 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 90 - artists, err := controller.GetArtistsNotOnRelease(global.DB, release.Release) 84 + artists, err := db.GetArtistsNotOnRelease(global.DB, release.Release.ID) 91 85 if err != nil { 92 86 fmt.Printf("FATAL: Failed to pull artists not on %s: %s\n", release.ID, err) 93 87 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 114 108 func serveNewCredit() http.Handler { 115 109 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 116 110 artistID := strings.Split(r.URL.Path, "/")[3] 117 - artist, err := controller.GetArtist(global.DB, artistID) 111 + artist, err := db.GetArtist(global.DB, artistID) 118 112 if err != nil { 119 113 fmt.Printf("FATAL: Failed to pull artists %s: %s\n", artistID, err) 120 114 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 158 152 159 153 func serveAddTrack(release *model.FullRelease) http.Handler { 160 154 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 161 - tracks, err := controller.GetTracksNotOnRelease(global.DB, release.Release) 155 + tracks, err := db.GetTracksNotOnRelease(global.DB, release.Release.ID) 162 156 if err != nil { 163 157 fmt.Printf("FATAL: Failed to pull tracks not on %s: %s\n", release.ID, err) 164 158 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 186 180 func serveNewTrack() http.Handler { 187 181 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 188 182 trackID := strings.Split(r.URL.Path, "/")[3] 189 - track, err := controller.GetTrack(global.DB, trackID) 183 + track, err := db.GetTrack(global.DB, trackID) 190 184 if err != nil { 191 185 fmt.Printf("Error rendering new track component for %s: %s\n", trackID, err) 192 186 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+5 -2
admin/static/edit-release.css
··· 18 18 19 19 .release-artwork { 20 20 width: 200px; 21 + text-align: center; 21 22 } 22 - 23 23 .release-artwork img { 24 24 width: 100%; 25 25 aspect-ratio: 1; ··· 27 27 .release-artwork img:hover { 28 28 outline: 1px solid #808080; 29 29 cursor: pointer; 30 + } 31 + .release-artwork #remove-artwork { 32 + padding: .3em .4em; 30 33 } 31 34 32 35 .release-info { ··· 342 345 background-color: #8cff83 343 346 } 344 347 345 - .card.links a.button[data-name="applemusic"] { 348 + .card.links a.button[data-name="apple music"] { 346 349 background-color: #8cd9ff 347 350 } 348 351
+36 -63
admin/static/edit-release.js
··· 3 3 const releaseID = document.getElementById("release").dataset.id; 4 4 const titleInput = document.getElementById("title"); 5 5 const artworkImg = document.getElementById("artwork"); 6 + const removeArtworkBtn = document.getElementById("remove-artwork"); 6 7 const artworkInput = document.getElementById("artwork-file"); 7 8 const typeInput = document.getElementById("type"); 8 9 const descInput = document.getElementById("description"); 9 10 const dateInput = document.getElementById("release-date"); 10 11 const buynameInput = document.getElementById("buyname"); 11 12 const buylinkInput = document.getElementById("buylink"); 13 + const copyrightInput = document.getElementById("copyright"); 14 + const copyrightURLInput = document.getElementById("copyright-url"); 12 15 const visInput = document.getElementById("visibility"); 13 16 const saveBtn = document.getElementById("save"); 14 17 const deleteBtn = document.getElementById("delete"); 15 18 16 19 var artworkData = artworkImg.attributes.src.value; 17 - var edited = new Stateful(false); 18 - var releaseData = updateData(undefined); 19 20 20 - function updateData(old) { 21 - var releaseData = { 22 - visible: visInput.value === "true", 23 - title: titleInput.value, 24 - description: descInput.value, 25 - type: typeInput.value, 26 - releaseDate: dateInput.value, 27 - artwork: artworkData, 28 - buyname: buynameInput.value, 29 - buylink: buylinkInput.value, 30 - }; 31 - 32 - if (releaseData && releaseData != old) { 33 - edited.set(true); 34 - } 35 - 36 - return releaseData; 37 - } 38 - 39 - function saveRelease() { 40 - console.table(releaseData); 41 - 21 + saveBtn.addEventListener("click", () => { 42 22 fetch("/api/v1/music/" + releaseID, { 43 23 method: "PUT", 44 - body: JSON.stringify(releaseData), 24 + body: JSON.stringify({ 25 + visible: visInput.value === "true", 26 + title: titleInput.value, 27 + description: descInput.value, 28 + type: typeInput.value, 29 + releaseDate: dateInput.value + ":00Z", 30 + artwork: artworkData, 31 + buyname: buynameInput.value, 32 + buylink: buylinkInput.value, 33 + copyright: copyrightInput.value, 34 + copyrightURL: copyrightURLInput.value, 35 + }), 45 36 headers: { "Content-Type": "application/json" } 46 37 }).then(res => { 47 38 if (!res.ok) { ··· 54 45 55 46 location = location; 56 47 }); 57 - } 48 + }); 58 49 59 - function deleteRelease() { 50 + deleteBtn.addEventListener("click", () => { 51 + if (releaseID != prompt( 52 + "You are about to permanently delete " + releaseID + ". " + 53 + "This action is irreversible. " + 54 + "Please enter \"" + releaseID + "\" to continue.")) return; 60 55 fetch("/api/v1/music/" + releaseID, { 61 56 method: "DELETE", 62 57 }).then(res => { ··· 70 65 71 66 location = "/admin"; 72 67 }); 73 - } 68 + }); 74 69 75 - edited.onUpdate(edited => { 76 - saveBtn.disabled = !edited; 77 - }) 70 + [titleInput, typeInput, descInput, dateInput, buynameInput, buylinkInput, copyrightInput, copyrightURLInput, visInput].forEach(input => { 71 + input.addEventListener("change", () => { 72 + saveBtn.disabled = false; 73 + }); 74 + input.addEventListener("keypress", () => { 75 + saveBtn.disabled = false; 76 + }); 77 + }); 78 78 79 - titleInput.addEventListener("change", () => { 80 - releaseData = updateData(releaseData); 81 - }); 82 79 artworkImg.addEventListener("click", () => { 83 80 artworkInput.addEventListener("change", () => { 84 81 if (artworkInput.files.length > 0) { ··· 87 84 const data = e.target.result; 88 85 artworkImg.src = data; 89 86 artworkData = data; 90 - releaseData = updateData(releaseData); 87 + saveBtn.disabled = false; 91 88 }; 92 89 reader.readAsDataURL(artworkInput.files[0]); 93 90 } 94 91 }); 95 92 artworkInput.click(); 96 93 }); 97 - typeInput.addEventListener("change", () => { 98 - releaseData = updateData(releaseData); 99 - }); 100 - descInput.addEventListener("change", () => { 101 - releaseData = updateData(releaseData); 102 - }); 103 - dateInput.addEventListener("change", () => { 104 - releaseData = updateData(releaseData); 105 - }); 106 - buynameInput.addEventListener("change", () => { 107 - releaseData = updateData(releaseData); 108 - }); 109 - buylinkInput.addEventListener("change", () => { 110 - releaseData = updateData(releaseData); 111 - }); 112 - visInput.addEventListener("change", () => { 113 - releaseData = updateData(releaseData); 114 - }); 115 94 116 - saveBtn.addEventListener("click", () => { 117 - if (!edited.get()) return; 118 - saveRelease(); 119 - }); 120 95 121 - deleteBtn.addEventListener("click", () => { 122 - if (releaseID != prompt( 123 - "You are about to permanently delete " + releaseID + ". " + 124 - "This action is irreversible. " + 125 - "Please enter \"" + releaseID + "\" to continue.")) return; 126 - deleteRelease(); 96 + removeArtworkBtn.addEventListener("click", () => { 97 + artworkImg.src = "/img/default-cover-art.png" 98 + artworkData = ""; 99 + saveBtn.disabled = false; 127 100 });
+2 -2
admin/trackhttp.go
··· 25 25 return 26 26 } 27 27 28 - dbReleases, err := music.GetTrackReleases(global.DB, track) 28 + dbReleases, err := music.GetTrackReleases(global.DB, track.ID) 29 29 if err != nil { 30 30 fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) 31 31 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 33 33 } 34 34 releases := []model.FullRelease{} 35 35 for _, release := range dbReleases { 36 - fullRelease, err := music.GetFullRelease(global.DB, release) 36 + fullRelease, err := music.GetFullRelease(global.DB, release.ID) 37 37 if err != nil { 38 38 fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) 39 39 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+17 -4
admin/views/edit-release.html
··· 12 12 <div class="release-artwork"> 13 13 <img src="{{.Artwork}}" alt="" width="256" loading="lazy" id="artwork"> 14 14 <input type="file" id="artwork-file" name="Artwork" accept=".png,.jpg,.jpeg" hidden> 15 + <button id="remove-artwork">Remove</button> 15 16 </div> 16 17 <div class="release-info"> 17 18 <h1 class="release-title"> 18 - <input type="text" id="title" name="Title" value="{{.Title}}"> 19 + <input type="text" id="title" name="Title" value="{{.Title}}" autocomplete="true"> 19 20 </h1> 20 21 <table> 21 22 <tr> ··· 53 54 <tr> 54 55 <td>Release Date</td> 55 56 <td> 56 - <input type="datetime-local" name="Release Date" id="release-date" value="{{.TextReleaseDate}}"> 57 + <input type="datetime-local" name="release-date" id="release-date" value="{{.TextReleaseDate}}"> 57 58 </td> 58 59 </tr> 59 60 <tr> 60 61 <td>Buy Name</td> 61 62 <td> 62 - <input type="text" name="Buy Name" id="buyname" value="{{.Buyname}}"> 63 + <input type="text" name="buyname" id="buyname" value="{{.Buyname}}" autocomplete="true"> 63 64 </td> 64 65 </tr> 65 66 <tr> 66 67 <td>Buy Link</td> 67 68 <td> 68 - <input type="text" name="Buy Link" id="buylink" value="{{.Buylink}}"> 69 + <input type="text" name="buylink" id="buylink" value="{{.Buylink}}" autocomplete="true"> 70 + </td> 71 + </tr> 72 + <tr> 73 + <td>Copyright</td> 74 + <td> 75 + <input type="text" name="copyright" id="copyright" value="{{.Copyright}}" autocomplete="true"> 76 + </td> 77 + </tr> 78 + <tr> 79 + <td>Copyright URL</td> 80 + <td> 81 + <input type="text" name="copyright-url" id="copyright-url" value="{{.CopyrightURL}}" autocomplete="true"> 69 82 </td> 70 83 </tr> 71 84 <tr>
+15 -3
api/api.go
··· 21 21 var artist model.Artist 22 22 err := global.DB.Get(&artist, "SELECT * FROM artist WHERE id=$1", artistID) 23 23 if err != nil { 24 + if strings.Contains(err.Error(), "no rows") { 25 + http.NotFound(w, r) 26 + return 27 + } 24 28 fmt.Printf("FATAL: Error while retrieving artist %s: %s\n", artistID, err) 25 - http.NotFound(w, r) 29 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 26 30 return 27 31 } 28 32 ··· 60 64 var release model.Release 61 65 err := global.DB.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", releaseID) 62 66 if err != nil { 67 + if strings.Contains(err.Error(), "no rows") { 68 + http.NotFound(w, r) 69 + return 70 + } 63 71 fmt.Printf("FATAL: Error while retrieving release %s: %s\n", releaseID, err) 64 - http.NotFound(w, r) 72 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 65 73 return 66 74 } 67 75 ··· 99 107 var track model.Track 100 108 err := global.DB.Get(&track, "SELECT * FROM musictrack WHERE id=$1", trackID) 101 109 if err != nil { 110 + if strings.Contains(err.Error(), "no rows") { 111 + http.NotFound(w, r) 112 + return 113 + } 102 114 fmt.Printf("FATAL: Error while retrieving track %s: %s\n", trackID, err) 103 - http.NotFound(w, r) 115 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 104 116 return 105 117 } 106 118
+12 -4
api/artist.go
··· 8 8 9 9 "arimelody.me/arimelody.me/global" 10 10 "arimelody.me/arimelody.me/music/model" 11 + db "arimelody.me/arimelody.me/music/controller" 11 12 ) 12 13 13 14 type artistJSON struct { ··· 20 21 func ServeAllArtists() http.Handler { 21 22 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 23 var artists = []*model.Artist{} 23 - err := global.DB.Select(&artists, "SELECT * FROM artist") 24 + artists, err := db.GetAllArtists(global.DB) 24 25 if err != nil { 25 26 fmt.Printf("FATAL: Failed to serve all artists: %s\n", err) 26 27 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 39 40 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 40 41 type ( 41 42 creditJSON struct { 42 - Release string `json:"release"` 43 43 Role string `json:"role"` 44 44 Primary bool `json:"primary"` 45 45 } ··· 49 49 } 50 50 ) 51 51 52 - var credits = map[string]creditJSON{} 53 - err := global.DB.Select(&credits, "SELECT release,role,is_primary FROM musiccredit WHERE id=$1", artist.ID) 52 + var dbCredits []*model.Credit 53 + dbCredits, err := db.GetArtistCredits(global.DB, artist.ID) 54 54 if err != nil { 55 55 fmt.Printf("FATAL: Failed to retrieve artist credits for %s: %s\n", artist.ID, err) 56 56 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 57 57 return 58 58 } 59 + 60 + var credits = map[string]creditJSON{} 61 + for _, credit := range dbCredits { 62 + credits[credit.Release.ID] = creditJSON{ 63 + Role: credit.Role, 64 + Primary: credit.Primary, 65 + } 66 + } 59 67 60 68 w.Header().Add("Content-Type", "application/json") 61 69 err = json.NewEncoder(w).Encode(artistJSON{
+32 -166
api/release.go
··· 12 12 13 13 "arimelody.me/arimelody.me/admin" 14 14 "arimelody.me/arimelody.me/global" 15 + music "arimelody.me/arimelody.me/music/controller" 15 16 "arimelody.me/arimelody.me/music/model" 16 17 ) 17 - 18 - type releaseBodyJSON struct { 19 - ID string `json:"id"` 20 - Visible *bool `json:"visible"` 21 - Title *string `json:"title"` 22 - Description *string `json:"description"` 23 - ReleaseType *model.ReleaseType `json:"type"` 24 - ReleaseDate *string `json:"releaseDate"` 25 - Artwork *string `json:"artwork"` 26 - Buyname *string `json:"buyname"` 27 - Buylink *string `json:"buylink"` 28 - } 29 18 30 19 func ServeCatalog() http.Handler { 31 20 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 32 - type catalogItem struct { 33 - ID string `json:"id"` 34 - Title string `json:"title"` 35 - ReleaseType model.ReleaseType `json:"type"` 36 - ReleaseDate time.Time `json:"releaseDate"` 37 - Artwork string `json:"artwork"` 38 - Buylink string `json:"buylink"` 39 - } 40 21 41 22 releases := []*model.Release{} 42 23 err := global.DB.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") ··· 45 26 return 46 27 } 47 28 48 - catalog := []catalogItem{} 29 + catalog := []model.ReleaseShorthand{} 49 30 authorised := admin.GetSession(r) != nil 50 31 for _, release := range releases { 51 32 if !release.Visible && !authorised { 52 33 continue 53 34 } 54 - catalog = append(catalog, catalogItem{ 35 + catalog = append(catalog, model.ReleaseShorthand{ 55 36 ID: release.ID, 56 37 Title: release.Title, 57 38 ReleaseType: release.ReleaseType, ··· 77 58 return 78 59 } 79 60 80 - var data releaseBodyJSON 81 - err := json.NewDecoder(r.Body).Decode(&data) 61 + var release model.Release 62 + err := json.NewDecoder(r.Body).Decode(&release) 82 63 if err != nil { 83 64 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 84 65 return 85 66 } 86 67 87 - if data.ID == "" { 68 + if release.ID == "" { 88 69 http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest) 89 70 return 90 71 } 91 72 92 - title := data.ID 93 - if data.Title != nil && *data.Title != "" { 94 - title = *data.Title 95 - } 96 - 97 - description := "" 98 - if data.Description != nil && *data.Description != "" { description = *data.Description } 73 + if release.Title == "" { release.Title = release.ID } 74 + if release.ReleaseType == "" { release.ReleaseType = model.Single } 99 75 100 - releaseType := model.Single 101 - if data.ReleaseType != nil && *data.ReleaseType != "" { releaseType = *data.ReleaseType } 102 - 103 - releaseDate := time.Time{} 104 - if data.ReleaseDate != nil && *data.ReleaseDate != "" { 105 - releaseDate, err = time.Parse("2006-01-02T15:04", *data.ReleaseDate) 106 - if err != nil { 107 - http.Error(w, "Invalid release date", http.StatusBadRequest) 108 - return 109 - } 110 - } else { 111 - releaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC) 76 + if release.ReleaseDate != time.Unix(0, 0) { 77 + release.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC) 112 78 } 113 79 114 - artwork := "/img/default-cover-art.png" 115 - if data.Artwork != nil && *data.Artwork != "" { artwork = *data.Artwork } 80 + if release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" } 116 81 117 - buyname := "" 118 - if data.Buyname != nil && *data.Buyname != "" { buyname = *data.Buyname } 119 - 120 - buylink := "" 121 - if data.Buylink != nil && *data.Buylink != "" { buylink = *data.Buylink } 122 - 123 - var release = model.Release{ 124 - ID: data.ID, 125 - Visible: false, 126 - Title: title, 127 - Description: description, 128 - ReleaseType: releaseType, 129 - ReleaseDate: releaseDate, 130 - Artwork: artwork, 131 - Buyname: buyname, 132 - Buylink: buylink, 133 - } 134 - 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) 82 + err = music.CreateRelease(global.DB, &release) 148 83 if err != nil { 149 84 if strings.Contains(err.Error(), "duplicate key") { 150 - http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest) 85 + http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest) 151 86 return 152 87 } 153 88 fmt.Printf("Failed to create release %s: %s\n", release.ID, err) ··· 173 108 } 174 109 175 110 segments := strings.Split(r.URL.Path[1:], "/") 176 - var releaseID = segments[0] 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) 182 - return 183 - } 184 111 185 112 if len(segments) == 2 { 186 113 switch segments[1] { ··· 199 126 return 200 127 } 201 128 202 - var data releaseBodyJSON 203 - err = json.NewDecoder(r.Body).Decode(&data) 129 + err := json.NewDecoder(r.Body).Decode(&release) 204 130 if err != nil { 131 + fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err) 205 132 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 206 133 return 207 134 } 208 135 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) 218 - return 219 - } 220 - release.ReleaseDate = newDate 221 - } 222 - if data.Artwork != nil { 223 - if strings.Contains(*data.Artwork, ";base64,") { 136 + if release.Artwork == "" { 137 + release.Artwork = "/img/default-cover-art.png" 138 + } else { 139 + if strings.Contains(release.Artwork, ";base64,") { 224 140 var artworkDirectory = filepath.Join("uploads", "musicart") 225 - filename, err := HandleImageUpload(data.Artwork, artworkDirectory, data.ID) 141 + filename, err := HandleImageUpload(&release.Artwork, artworkDirectory, release.ID) 226 142 227 143 // clean up files with this ID and different extensions 228 144 err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error { ··· 238 154 } 239 155 240 156 release.Artwork = fmt.Sprintf("/uploads/musicart/%s", filename) 241 - } else { 242 - release.Artwork = *data.Artwork 243 157 } 244 158 } 245 159 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) 160 + err = music.UpdateRelease(global.DB, &release) 262 161 if err != nil { 263 162 fmt.Printf("FATAL: Failed to update release %s: %s\n", release.ID, err) 264 163 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 275 174 return 276 175 } 277 176 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() 177 + err = music.UpdateReleaseTracks(global.DB, &release, trackIDs) 290 178 if err != nil { 291 179 fmt.Printf("Failed to update tracks for %s: %s\n", release.ID, err) 292 180 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 308 196 return 309 197 } 310 198 311 - // clear duplicates 312 - type Credit struct { 313 - Role string 314 - Primary bool 315 - } 316 - var credits = map[string]Credit{} 199 + var credits []model.Credit 317 200 for _, credit := range data { 318 - credits[credit.Artist] = Credit{ 201 + credits = append(credits, model.Credit{ 202 + Artist: model.Artist{ 203 + ID: credit.Artist, 204 + }, 319 205 Role: credit.Role, 320 206 Primary: credit.Primary, 321 - } 207 + }) 322 208 } 323 209 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) 210 + err = music.UpdateReleaseCredits(global.DB, &release, credits) 211 + if err != nil { 212 + if strings.Contains(err.Error(), "duplicate key") { 213 + http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest) 329 214 return 330 215 } 331 - 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) 336 - return 337 - } 338 - 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() 349 - if err != nil { 350 216 fmt.Printf("Failed to update links for %s: %s\n", release.ID, err) 351 217 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 352 218 }
+29 -65
api/track.go
··· 6 6 "net/http" 7 7 8 8 "arimelody.me/arimelody.me/global" 9 + music "arimelody.me/arimelody.me/music/controller" 9 10 "arimelody.me/arimelody.me/music/model" 10 11 ) 11 12 13 + type ( 14 + Track struct { 15 + model.Track 16 + Releases []model.ReleaseShorthand 17 + } 18 + ) 19 + 12 20 func ServeAllTracks() http.Handler { 13 21 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 - type track struct { 22 + type Track struct { 15 23 ID string `json:"id"` 16 24 Title string `json:"title"` 17 25 } 18 - var tracks = []track{} 26 + var tracks = []Track{} 19 27 20 - err := global.DB.Select(&tracks, "SELECT id, title FROM musictrack") 28 + var dbTracks = []*model.Track{} 29 + dbTracks, err := music.GetAllTracks(global.DB) 21 30 if err != nil { 22 31 fmt.Printf("FATAL: Failed to pull tracks from DB: %s\n", err) 23 32 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 33 + } 34 + 35 + for _, track := range dbTracks { 36 + tracks = append(tracks, Track{ 37 + ID: track.ID, 38 + Title: track.Title, 39 + }) 24 40 } 25 41 26 42 w.Header().Add("Content-Type", "application/json") ··· 34 50 35 51 func ServeTrack(track model.Track) http.Handler { 36 52 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 - if r.URL.Path == "/" { 38 - ServeAllTracks().ServeHTTP(w, r) 39 - return 40 - } 41 - 42 - var trackID = r.URL.Path[1:] 43 - var track = model.Track{} 44 - err := global.DB.Get(&track, "SELECT * from musictrack WHERE id=$1", trackID) 45 - if err != nil { 46 - http.NotFound(w, r) 47 - return 48 - } 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 - ) 53 + releases, err := music.GetTrackReleases(global.DB, track.ID) 57 54 if err != nil { 58 - fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", trackID, err) 55 + fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", track.ID, err) 59 56 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 60 57 } 61 58 62 - type response struct { 63 - model.Track 64 - Releases []*model.Release 65 - } 66 - 67 59 w.Header().Add("Content-Type", "application/json") 68 - err = json.NewEncoder(w).Encode(response{ track, releases }) 60 + err = json.NewEncoder(w).Encode(Track{ track, releases }) 69 61 if err != nil { 70 - fmt.Printf("FATAL: Failed to serve track %s: %s\n", trackID, err) 62 + fmt.Printf("FATAL: Failed to serve track %s: %s\n", track.ID, err) 71 63 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 72 64 } 73 65 }) ··· 92 84 return 93 85 } 94 86 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) 87 + id, err := music.CreateTrack(global.DB, &track) 104 88 if err != nil { 105 89 fmt.Printf("FATAL: Failed to create track: %s\n", err) 106 90 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 109 93 110 94 w.Header().Add("Content-Type", "text/plain") 111 95 w.WriteHeader(http.StatusCreated) 112 - w.Write([]byte(trackID)) 96 + w.Write([]byte(id)) 113 97 }) 114 98 } 115 99 ··· 120 104 return 121 105 } 122 106 123 - var update model.Track 124 - err := json.NewDecoder(r.Body).Decode(&update) 107 + err := json.NewDecoder(r.Body).Decode(&track) 125 108 if err != nil { 126 109 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 127 110 return 128 111 } 129 112 130 - if update.Title == "" { 113 + if track.Title == "" { 131 114 http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest) 132 115 return 133 116 } 134 117 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 - } 142 - 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) 118 + err = music.UpdateTrack(global.DB, &track) 152 119 if err != nil { 153 120 fmt.Printf("Failed to update track %s: %s\n", track.ID, err) 154 121 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 171 138 } 172 139 173 140 var trackID = r.URL.Path[1:] 174 - _, err := global.DB.Exec( 175 - "DELETE FROM musictrack "+ 176 - "WHERE id=$1", 177 - trackID) 141 + err := music.DeleteTrack(global.DB, trackID) 178 142 if err != nil { 179 143 fmt.Printf("Failed to delete track %s: %s\n", trackID, err) 180 144 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+29 -2
music/controller/artist.go
··· 29 29 return artists, nil 30 30 } 31 31 32 - func GetArtistsNotOnRelease(db *sqlx.DB, release *model.Release) ([]*model.Artist, error) { 32 + func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, error) { 33 33 var artists = []*model.Artist{} 34 34 35 35 err := db.Select(&artists, 36 36 "SELECT * FROM artist "+ 37 37 "WHERE id NOT IN "+ 38 38 "(SELECT artist FROM musiccredit WHERE release=$1)", 39 - release.ID) 39 + releaseID) 40 40 if err != nil { 41 41 return nil, err 42 42 } 43 43 44 44 return artists, nil 45 + } 46 + 47 + func GetArtistCredits(db *sqlx.DB, artistID string) ([]*model.Credit, error) { 48 + type DBCredit struct { 49 + Release string 50 + Artist string 51 + Role string 52 + Primary bool `db:"is_primary"` 53 + } 54 + var dbCredits []DBCredit 55 + 56 + err := db.Select(&dbCredits, "SELECT * FROM musiccredit WHERE artist=$1", artistID) 57 + if err != nil { 58 + return nil, err 59 + } 60 + 61 + var credits []*model.Credit 62 + for _, credit := range dbCredits { 63 + credits = append(credits, &model.Credit{ 64 + Release: model.Release{ ID: credit.Release }, 65 + Artist: model.Artist{ ID: credit.Artist }, 66 + Role: credit.Role, 67 + Primary: credit.Primary, 68 + }) 69 + } 70 + 71 + return credits, nil 45 72 } 46 73 47 74 func CreateArtist(db *sqlx.DB, artist *model.Artist) error {
-71
music/controller/credit.go
··· 1 - package music 2 - 3 - import ( 4 - "arimelody.me/arimelody.me/music/model" 5 - "github.com/jmoiron/sqlx" 6 - ) 7 - 8 - // DATABASE 9 - 10 - func GetReleaseCredits(db *sqlx.DB, release *model.Release) ([]model.Credit, error) { 11 - var credits = []model.Credit{} 12 - 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, 18 - ) 19 - if err != nil { 20 - return nil, err 21 - } 22 - 23 - return credits, nil 24 - } 25 - 26 - func CreateCredit(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { 27 - _, err := db.Exec( 28 - "INSERT INTO musiccredit (release, artist, role, is_primary) "+ 29 - "VALUES ($1, $2, $3, $4)", 30 - releaseID, 31 - artistID, 32 - credit.Role, 33 - credit.Primary, 34 - ) 35 - if err != nil { 36 - return err 37 - } 38 - 39 - return nil 40 - } 41 - 42 - func UpdateCredit(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { 43 - _, err := db.Exec( 44 - "UPDATE musiccredit SET "+ 45 - "role=$3, is_primary=$4 "+ 46 - "WHERE release=$1, artist=$2", 47 - releaseID, 48 - artistID, 49 - credit.Role, 50 - credit.Primary, 51 - ) 52 - if err != nil { 53 - return err 54 - } 55 - 56 - return nil 57 - } 58 - 59 - func DeleteCredit(db *sqlx.DB, releaseID string, artistID string) (error) { 60 - _, err := db.Exec( 61 - "DELETE FROM musiccredit "+ 62 - "WHERE release=$1, artist=$2", 63 - releaseID, 64 - artistID, 65 - ) 66 - if err != nil { 67 - return err 68 - } 69 - 70 - return nil 71 - }
-64
music/controller/link.go
··· 1 - package music 2 - 3 - import ( 4 - "arimelody.me/arimelody.me/music/model" 5 - "github.com/jmoiron/sqlx" 6 - ) 7 - 8 - // DATABASE 9 - 10 - func GetReleaseLinks(db *sqlx.DB, release *model.Release) ([]model.Link, error) { 11 - var links = []model.Link{} 12 - 13 - err := db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", release.ID) 14 - if err != nil { 15 - return nil, err 16 - } 17 - 18 - return links, nil 19 - } 20 - 21 - func CreateLink(db *sqlx.DB, releaseID string, link *model.Link) (error) { 22 - _, err := db.Exec( 23 - "INSERT INTO musiclink (release, name, url) "+ 24 - "VALUES ($1, $2, $3)", 25 - releaseID, 26 - link.Name, 27 - link.URL, 28 - ) 29 - if err != nil { 30 - return err 31 - } 32 - 33 - return nil 34 - } 35 - 36 - func UpdateLink(db *sqlx.DB, releaseID string, link *model.Link) (error) { 37 - _, err := db.Exec( 38 - "UPDATE musiclink SET "+ 39 - "name=$2, url=$3 "+ 40 - "WHERE release=$1", 41 - releaseID, 42 - link.Name, 43 - link.URL, 44 - ) 45 - if err != nil { 46 - return err 47 - } 48 - 49 - return nil 50 - } 51 - 52 - func DeleteLink(db *sqlx.DB, releaseID string, link *model.Link) (error) { 53 - _, err := db.Exec( 54 - "DELETE FROM musiclink "+ 55 - "WHERE release=$1, name=$2", 56 - releaseID, 57 - link.Name, 58 - ) 59 - if err != nil { 60 - return err 61 - } 62 - 63 - return nil 64 - }
+96 -31
music/controller/release.go
··· 1 1 package music 2 2 3 3 import ( 4 + "errors" 5 + "fmt" 6 + 4 7 "arimelody.me/arimelody.me/music/model" 5 8 "github.com/jmoiron/sqlx" 6 9 ) ··· 27 30 return releases, nil 28 31 } 29 32 30 - func GetReleaseTracks(db *sqlx.DB, release *model.Release) ([]*model.Track, error) { 31 - var tracks = []*model.Track{} 32 - 33 - err := db.Select(&tracks, 34 - "SELECT musictrack.* FROM musictrack "+ 35 - "JOIN musicreleasetrack ON track=id "+ 36 - "WHERE release=$1 "+ 37 - "ORDER BY number ASC", 38 - release.ID, 39 - ) 40 - if err != nil { 41 - return nil, err 42 - } 43 - 44 - return tracks, nil 45 - } 46 - 47 33 func CreateRelease(db *sqlx.DB, release *model.Release) error { 48 34 _, err := db.Exec( 49 35 "INSERT INTO musicrelease "+ 50 - "(id, visible, title, description, type, release_date, artwork, buyname, buylink) "+ 51 - "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", 36 + "(id, visible, title, description, type, release_date, artwork, buyname, buylink, copyright, copyrighturl) "+ 37 + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", 52 38 release.ID, 53 39 release.Visible, 54 40 release.Title, ··· 58 44 release.Artwork, 59 45 release.Buyname, 60 46 release.Buylink, 47 + release.Copyright, 48 + release.CopyrightURL, 61 49 ) 62 50 if err != nil { 63 51 return err ··· 69 57 func UpdateRelease(db *sqlx.DB, release *model.Release) error { 70 58 _, err := db.Exec( 71 59 "UPDATE musicrelease SET "+ 72 - "visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9 "+ 60 + "visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9, copyright=$10, copyrighturl=$11 "+ 73 61 "WHERE id=$1", 74 62 release.ID, 75 63 release.Visible, ··· 80 68 release.Artwork, 81 69 release.Buyname, 82 70 release.Buylink, 71 + release.Copyright, 72 + release.CopyrightURL, 83 73 ) 84 74 if err != nil { 85 75 return err ··· 88 78 return nil 89 79 } 90 80 91 - func UpdateReleaseTracks(db *sqlx.DB, release *model.Release, new_tracks []*model.Track) error { 81 + func UpdateReleaseTracks(db *sqlx.DB, release *model.Release, new_tracks []string) error { 92 82 _, err := db.Exec( 93 83 "DELETE FROM musicreleasetrack "+ 94 84 "WHERE release=$1", ··· 98 88 return err 99 89 } 100 90 101 - for i, track := range new_tracks { 91 + for i, trackID := range new_tracks { 102 92 _, err = db.Exec( 103 93 "INSERT INTO musicreleasetrack "+ 104 94 "(release, track, number) "+ 105 95 "VALUES ($1, $2, $3)", 106 96 release.ID, 107 - track.ID, 97 + trackID, 108 98 i, 109 99 ) 110 100 if err != nil { ··· 115 105 return nil 116 106 } 117 107 118 - func UpdateReleaseCredits(db *sqlx.DB, release *model.Release, new_credits []*model.Credit) error { 108 + func UpdateReleaseCredits(db *sqlx.DB, release *model.Release, new_credits []model.Credit) error { 119 109 _, err := db.Exec( 120 110 "DELETE FROM musiccredit "+ 121 111 "WHERE release=$1", ··· 183 173 return nil 184 174 } 185 175 186 - func GetFullRelease(db *sqlx.DB, release *model.Release) (*model.FullRelease, error) { 176 + func GetFullRelease(db *sqlx.DB, releaseID string) (*model.FullRelease, error) { 177 + // get release 178 + release, err := GetRelease(db, releaseID) 179 + if err != nil { 180 + return nil, err 181 + } 182 + 187 183 // get credits 188 - credits, err := GetReleaseCredits(db, release) 184 + credits, err := GetReleaseCredits(db, releaseID) 189 185 if err != nil { 190 - return nil, err 186 + return nil, errors.New(fmt.Sprintf("Credits: %s", err)) 187 + } 188 + 189 + // get artists 190 + for i, credit := range credits { 191 + artist, err := GetArtist(db, credit.Artist.ID) 192 + if err != nil { 193 + return nil, errors.New(fmt.Sprintf("FATAL: Failed to serve release %s: Artists: %s\n", release.ID, err)) 194 + } 195 + credits[i].Artist = *artist 191 196 } 192 197 193 198 // get tracks 194 - dbTracks, err := GetReleaseTracks(db, release) 199 + dbTracks, err := GetReleaseTracks(db, releaseID) 195 200 if err != nil { 196 - return nil, err 201 + return nil, errors.New(fmt.Sprintf("Tracks: %s", err)) 197 202 } 198 203 tracks := []model.DisplayTrack{} 199 204 for i, track := range dbTracks { ··· 201 206 } 202 207 203 208 // get links 204 - links, err := GetReleaseLinks(db, release) 209 + links, err := GetReleaseLinks(db, releaseID) 205 210 if err != nil { 206 - return nil, err 211 + return nil, errors.New(fmt.Sprintf("Links: %s", err)) 207 212 } 208 213 209 214 return &model.FullRelease{ ··· 213 218 Links: links, 214 219 }, nil 215 220 } 221 + 222 + func GetReleaseTracks(db *sqlx.DB, releaseID string) ([]model.Track, error) { 223 + var tracks = []model.Track{} 224 + 225 + err := db.Select(&tracks, 226 + "SELECT musictrack.* FROM musictrack "+ 227 + "JOIN musicreleasetrack ON track=id "+ 228 + "WHERE release=$1 "+ 229 + "ORDER BY number ASC", 230 + releaseID, 231 + ) 232 + if err != nil { 233 + return nil, err 234 + } 235 + 236 + return tracks, nil 237 + } 238 + 239 + func GetReleaseCredits(db *sqlx.DB, releaseID string) ([]model.Credit, error) { 240 + type DBCredit struct { 241 + Release string 242 + Artist string 243 + Role string 244 + Primary bool `db:"is_primary"` 245 + } 246 + var dbCredits []DBCredit 247 + 248 + err := db.Select(&dbCredits, 249 + "SELECT musiccredit.* FROM musiccredit "+ 250 + "JOIN artist ON artist=id "+ 251 + "WHERE release=$1", 252 + releaseID, 253 + ) 254 + if err != nil { 255 + return nil, err 256 + } 257 + 258 + var credits []model.Credit 259 + for _, credit := range dbCredits { 260 + credits = append(credits, model.Credit{ 261 + Release: model.Release{ ID: credit.Release }, 262 + Artist: model.Artist{ ID: credit.Artist }, 263 + Role: credit.Role, 264 + Primary: credit.Primary, 265 + }) 266 + } 267 + 268 + return credits, nil 269 + } 270 + 271 + func GetReleaseLinks(db *sqlx.DB, releaseID string) ([]model.Link, error) { 272 + var links = []model.Link{} 273 + 274 + err := db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", releaseID) 275 + if err != nil { 276 + return nil, err 277 + } 278 + 279 + return links, nil 280 + }
+8 -8
music/controller/track.go
··· 40 40 return tracks, nil 41 41 } 42 42 43 - func GetTracksNotOnRelease(db *sqlx.DB, release *model.Release) ([]*model.Track, error) { 43 + func GetTracksNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Track, error) { 44 44 var tracks = []*model.Track{} 45 45 46 46 err := db.Select(&tracks, 47 47 "SELECT * FROM musictrack "+ 48 48 "WHERE id NOT IN "+ 49 49 "(SELECT track FROM musicreleasetrack WHERE release=$1)", 50 - release.ID) 50 + releaseID) 51 51 if err != nil { 52 52 return nil, err 53 53 } ··· 55 55 return tracks, nil 56 56 } 57 57 58 - func GetTrackReleases(db *sqlx.DB, track *model.Track) ([]*model.Release, error) { 59 - var releases = []*model.Release{} 58 + func GetTrackReleases(db *sqlx.DB, trackID string) ([]model.ReleaseShorthand, error) { 59 + var releases = []model.ReleaseShorthand{} 60 60 61 61 err := db.Select(&releases, 62 - "SELECT musicrelease.* FROM musicrelease "+ 62 + "SELECT id,title,type,release_date,artwork,buylink FROM musicrelease "+ 63 63 "JOIN musicreleasetrack ON release=id "+ 64 64 "WHERE track=$1 "+ 65 65 "ORDER BY release_date", 66 - track.ID, 66 + trackID, 67 67 ) 68 68 if err != nil { 69 69 return nil, err ··· 123 123 return nil 124 124 } 125 125 126 - func DeleteTrack(db *sqlx.DB, track *model.Track) error { 126 + func DeleteTrack(db *sqlx.DB, trackID string) error { 127 127 _, err := db.Exec( 128 128 "DELETE FROM musictrack "+ 129 129 "WHERE id=$1", 130 - track.ID, 130 + trackID, 131 131 ) 132 132 if err != nil { 133 133 return err
+2 -1
music/model/credit.go
··· 1 1 package model 2 2 3 3 type Credit struct { 4 - Artist `json:"artist"` 4 + Release Release `json:"release"` 5 + Artist Artist `json:"artist"` 5 6 Role string `json:"role"` 6 7 Primary bool `json:"primary" db:"is_primary"` 7 8 }
+23 -14
music/model/release.go
··· 8 8 ReleaseType string 9 9 10 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"` 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 22 } 23 23 24 24 FullRelease struct { 25 25 *Release 26 - Tracks []DisplayTrack 27 - Credits []Credit 28 - Links []Link 26 + Tracks []DisplayTrack `json:"tracks"` 27 + Credits []Credit `json:"credits"` 28 + Links []Link `json:"links"` 29 + } 30 + 31 + ReleaseShorthand struct { 32 + ID string `json:"id"` 33 + Title string `json:"title"` 34 + ReleaseType ReleaseType `json:"type" db:"type"` 35 + ReleaseDate time.Time `json:"releaseDate" db:"release_date"` 36 + Artwork string `json:"artwork"` 37 + Buylink string `json:"buylink"` 29 38 } 30 39 ) 31 40
+3 -3
music/model/track.go
··· 10 10 ID string `json:"id"` 11 11 Title string `json:"title"` 12 12 Description string `json:"description"` 13 - Lyrics string `json:"lyrics"` 13 + Lyrics string `json:"lyrics" db:"lyrics"` 14 14 PreviewURL string `json:"previewURL" db:"preview_url"` 15 15 } 16 16 17 17 DisplayTrack struct { 18 18 *Track 19 - Lyrics template.HTML 20 - Number int 19 + Lyrics template.HTML `json:"lyrics"` 20 + Number int `json:"-"` 21 21 } 22 22 ) 23 23
+1 -1
music/view/music.go
··· 48 48 if !dbRelease.IsReleased() { 49 49 dbRelease.ReleaseType = model.Upcoming 50 50 } 51 - release, err := music.GetFullRelease(global.DB, dbRelease) 51 + release, err := music.GetFullRelease(global.DB, dbRelease.ID) 52 52 if err != nil { 53 53 fmt.Printf("FATAL: Failed to pull full release for %s: %s\n", dbRelease.ID, err) 54 54 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+70 -8
music/view/release.go
··· 12 12 "arimelody.me/arimelody.me/templates" 13 13 ) 14 14 15 - // HTTP HANDLERS 15 + type ( 16 + Track struct { 17 + Title string `json:"title"` 18 + Description string `json:"description"` 19 + Lyrics string `json:"lyrics"` 20 + } 21 + 22 + Credit struct { 23 + *model.Artist 24 + Role string `json:"role"` 25 + Primary bool `json:"primary"` 26 + } 27 + 28 + Release struct { 29 + model.Release 30 + Tracks []Track `json:"tracks"` 31 + Credits []Credit `json:"credits"` 32 + Links map[string]string `json:"links"` 33 + } 34 + ) 16 35 17 36 func ServeRelease(release model.Release) http.Handler { 18 37 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ··· 23 42 return 24 43 } 25 44 26 - fullRelease := &model.FullRelease{ 27 - Release: &release, 45 + response := Release{ 46 + Release: release, 47 + Tracks: []Track{}, 48 + Credits: []Credit{}, 49 + Links: make(map[string]string), 28 50 } 29 51 30 52 if authorised || release.IsReleased() { 31 - fullerRelease, err := db.GetFullRelease(global.DB, &release) 53 + // get credits 54 + credits, err := db.GetReleaseCredits(global.DB, release.ID) 32 55 if err != nil { 33 - fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) 56 + fmt.Printf("FATAL: Failed to serve release %s: Credits: %s\n", release.ID, err) 34 57 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 35 58 return 36 59 } 37 - fullRelease = fullerRelease 60 + for _, credit := range credits { 61 + artist, err := db.GetArtist(global.DB, credit.Artist.ID) 62 + if err != nil { 63 + fmt.Printf("FATAL: Failed to serve release %s: Artists: %s\n", release.ID, err) 64 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 65 + return 66 + } 67 + 68 + response.Credits = append(response.Credits, Credit{ 69 + Artist: artist, 70 + Role: credit.Role, 71 + Primary: credit.Primary, 72 + }) 73 + } 74 + 75 + // get tracks 76 + tracks, err := db.GetReleaseTracks(global.DB, release.ID) 77 + if err != nil { 78 + fmt.Printf("FATAL: Failed to serve release %s: Tracks: %s\n", release.ID, err) 79 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 80 + return 81 + } 82 + for _, track := range tracks { 83 + response.Tracks = append(response.Tracks, Track{ 84 + Title: track.Title, 85 + Description: track.Description, 86 + Lyrics: track.Lyrics, 87 + }) 88 + } 89 + 90 + // get links 91 + links, err := db.GetReleaseLinks(global.DB, release.ID) 92 + if err != nil { 93 + fmt.Printf("FATAL: Failed to serve release %s: Links: %s\n", release.ID, err) 94 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 95 + return 96 + } 97 + for _, link := range links { 98 + response.Links[link.Name] = link.URL 99 + } 38 100 } 39 101 40 102 w.Header().Add("Content-Type", "application/json") 41 - err := json.NewEncoder(w).Encode(fullRelease) 103 + err := json.NewEncoder(w).Encode(response) 42 104 if err != nil { 43 105 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 44 106 return ··· 60 122 } 61 123 62 124 if authorised || release.IsReleased() { 63 - fullerRelease, err := db.GetFullRelease(global.DB, &release) 125 + fullerRelease, err := db.GetFullRelease(global.DB, release.ID) 64 126 if err != nil { 65 127 fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) 66 128 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+5 -2
public/style/music-gateway.css
··· 311 311 flex-grow: 1; 312 312 } 313 313 314 + #buylink, 314 315 ul#links a { 315 316 width: calc(100% - 1.6em); 316 317 padding: .5em .8em; ··· 324 325 transition: filter .1s,-webkit-filter .1s 325 326 } 326 327 327 - ul#links a.buy { 328 + #buylink { 329 + margin-top: 1em; 330 + margin-bottom: -.5em; 328 331 background-color: #ff94e9 329 332 } 330 333 ··· 390 393 animation: share-after 2s cubic-bezier(.5,0,1,.5) forwards 391 394 } 392 395 393 - h2 { 396 + #info h2 { 394 397 width: fit-content; 395 398 padding: .3em 1em; 396 399 font-size: 1em;
+4 -7
views/music-gateway.html
··· 29 29 {{end}} 30 30 31 31 {{define "content"}} 32 - <main> 32 + <main > 33 33 <script type="module" src="/script/music-gateway.js"></script> 34 34 35 35 <div id="background" style="background-image: url({{.GetArtwork}})"></div> ··· 65 65 {{end}} 66 66 67 67 {{if .IsReleased}} 68 + {{if .Buylink}} 69 + <a href="{{.Buylink}}" id="buylink">{{or .Buyname "buy"}}</a> 70 + {{end}} 68 71 <ul id="links"> 69 - {{if .Buylink}} 70 - <li> 71 - <a href="{{.Buylink}}" class="buy">{{or .Buyname "buy"}}</a> 72 - </li> 73 - {{end}} 74 - 75 72 {{range .Links}} 76 73 <li> 77 74 <a class="{{.NormaliseName}}" href="{{.URL}}">{{.Name}}</a>