home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

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

create support for releases, artists, tracks, and credits

Signed-off-by: ari melody <ari@arimelody.me>

+252 -37
+4 -2
admin/http.go
··· 34 34 } 35 35 36 36 type IndexData struct { 37 - Releases []musicModel.Release 38 - Artists []musicModel.Artist 37 + Releases []*musicModel.Release 38 + Artists []*musicModel.Artist 39 + Tracks []*musicModel.Track 39 40 } 40 41 41 42 serveTemplate("index.html", IndexData{ 42 43 Releases: global.Releases, 43 44 Artists: global.Artists, 45 + Tracks: global.Tracks, 44 46 }).ServeHTTP(w, r) 45 47 })) 46 48
+26
admin/static/admin.css
··· 77 77 } 78 78 79 79 .release { 80 + margin-bottom: 1em; 80 81 padding: 1em; 81 82 display: flex; 82 83 flex-direction: row; ··· 164 165 } 165 166 166 167 .artist { 168 + margin-bottom: .5em; 167 169 padding: .5em; 168 170 display: flex; 169 171 flex-direction: row; ··· 185 187 object-fit: cover; 186 188 border-radius: 100%; 187 189 } 190 + 191 + .track { 192 + margin-bottom: 1em; 193 + padding: 1em; 194 + display: flex; 195 + flex-direction: column; 196 + gap: .5em; 197 + 198 + border-radius: .5em; 199 + background: #f8f8f8f8; 200 + border: 1px solid #808080; 201 + } 202 + 203 + h2.track-title { 204 + margin: 0 205 + } 206 + 207 + .track-description { 208 + font-style: italic; 209 + } 210 + 211 + .track .empty { 212 + opacity: 0.75; 213 + }
+4
api/api.go
··· 24 24 return 25 25 } 26 26 })) 27 + 27 28 mux.Handle("/v1/music/", http.StripPrefix("/v1/music", music.ServeRelease())) 28 29 mux.Handle("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 29 30 switch r.Method { ··· 38 39 return 39 40 } 40 41 })) 42 + 43 + mux.Handle("/v1/musiccredit", CreateCredit()) 44 + mux.Handle("/v1/track", CreateTrack()) 41 45 42 46 return mux 43 47 }
+12 -2
api/artist.go
··· 98 98 var data model.Artist 99 99 err := json.NewDecoder(r.Body).Decode(&data) 100 100 if err != nil { 101 + fmt.Printf("Failed to create artist: %s\n", err) 101 102 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 102 103 return 103 104 } 104 105 106 + if data.ID == "" { 107 + http.Error(w, "Artist ID cannot be blank", http.StatusBadRequest) 108 + return 109 + } 110 + if data.Name == "" { 111 + http.Error(w, "Artist name cannot be blank", http.StatusBadRequest) 112 + return 113 + } 114 + 105 115 if global.GetArtist(data.ID) != nil { 106 116 http.Error(w, fmt.Sprintf("Artist %s already exists", data.ID), http.StatusBadRequest) 107 117 return ··· 114 124 Avatar: data.Avatar, 115 125 } 116 126 117 - global.Artists = append(global.Artists, artist) 118 - 119 127 err = controller.CreateArtistDB(global.DB, &artist) 120 128 if err != nil { 121 129 fmt.Printf("Failed to create artist %s: %s\n", artist.ID, err) 122 130 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 123 131 return 124 132 } 133 + 134 + global.Artists = append(global.Artists, &artist) 125 135 126 136 w.Header().Add("Content-Type", "application/json") 127 137 w.WriteHeader(http.StatusCreated)
+65
api/credit.go
··· 1 + package api 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + 8 + "arimelody.me/arimelody.me/global" 9 + "arimelody.me/arimelody.me/music/model" 10 + controller "arimelody.me/arimelody.me/music/controller" 11 + ) 12 + 13 + func CreateCredit() http.Handler { 14 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 + if r.Method != http.MethodPost { 16 + http.NotFound(w, r) 17 + return 18 + } 19 + 20 + type creditJSON struct { 21 + Release string 22 + Artist string 23 + Role string 24 + Primary bool 25 + } 26 + 27 + var data creditJSON 28 + err := json.NewDecoder(r.Body).Decode(&data) 29 + if err != nil { 30 + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 31 + return 32 + } 33 + 34 + var release = global.GetRelease(data.Release) 35 + if release == nil { 36 + http.Error(w, fmt.Sprintf("Release %s does not exist", data.Release), http.StatusBadRequest) 37 + return 38 + } 39 + 40 + var artist = global.GetArtist(data.Artist) 41 + if artist == nil { 42 + http.Error(w, fmt.Sprintf("Artist %s does not exist", data.Artist), http.StatusBadRequest) 43 + return 44 + } 45 + 46 + var credit = model.Credit{ 47 + Artist: artist, 48 + Role: data.Role, 49 + Primary: data.Primary, 50 + } 51 + 52 + err = controller.CreateCreditDB(global.DB, release.ID, artist.ID, &credit) 53 + if err != nil { 54 + fmt.Printf("Failed to create credit: %s\n", err) 55 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 56 + return 57 + } 58 + 59 + release.Credits = append(release.Credits, &credit) 60 + 61 + w.Header().Add("Content-Type", "application/json") 62 + w.WriteHeader(http.StatusCreated) 63 + err = json.NewEncoder(w).Encode(credit) 64 + }) 65 + }
+6 -6
api/music.go api/release.go
··· 14 14 15 15 func ServeCatalog() http.Handler { 16 16 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 - releases := []model.Release{} 17 + releases := []*model.Release{} 18 18 authorised := admin.GetSession(r) != nil 19 19 for _, release := range global.Releases { 20 20 if !release.IsReleased() && !authorised { ··· 71 71 Artwork: data.Artwork, 72 72 Buyname: data.Buyname, 73 73 Buylink: data.Buylink, 74 - Links: []model.Link{}, 75 - Credits: []model.Credit{}, 76 - Tracks: []model.Track{}, 74 + Links: []*model.Link{}, 75 + Credits: []*model.Credit{}, 76 + Tracks: []*model.Track{}, 77 77 } 78 - 79 - global.Releases = append([]model.Release{release}, global.Releases...) 80 78 81 79 err = controller.CreateReleaseDB(global.DB, &release) 82 80 if err != nil { ··· 84 82 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 85 83 return 86 84 } 85 + 86 + global.Releases = append([]*model.Release{&release}, global.Releases...) 87 87 88 88 w.Header().Add("Content-Type", "application/json") 89 89 w.WriteHeader(http.StatusCreated)
+46
api/track.go
··· 1 + package api 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + 8 + "arimelody.me/arimelody.me/global" 9 + "arimelody.me/arimelody.me/music/model" 10 + controller "arimelody.me/arimelody.me/music/controller" 11 + ) 12 + 13 + func CreateTrack() http.Handler { 14 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 + if r.Method != http.MethodPost { 16 + http.NotFound(w, r) 17 + return 18 + } 19 + 20 + var track model.Track 21 + err := json.NewDecoder(r.Body).Decode(&track) 22 + if err != nil { 23 + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 24 + return 25 + } 26 + 27 + if track.Title == "" { 28 + http.Error(w, "Track title cannot be empty", http.StatusBadRequest) 29 + return 30 + } 31 + 32 + trackID, err := controller.CreateTrackDB(global.DB, &track) 33 + if err != nil { 34 + fmt.Printf("Failed to create credit: %s\n", err) 35 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 36 + return 37 + } 38 + 39 + track.ID = trackID 40 + global.Tracks = append(global.Tracks, &track) 41 + 42 + w.Header().Add("Content-Type", "application/json") 43 + w.WriteHeader(http.StatusCreated) 44 + err = json.NewEncoder(w).Encode(track) 45 + }) 46 + }
+5 -5
global/data.go
··· 17 17 18 18 var DB *sqlx.DB 19 19 20 - var Releases []model.Release 21 - var Artists []model.Artist 22 - var Tracks []model.Track 20 + var Releases []*model.Release 21 + var Artists []*model.Artist 22 + var Tracks []*model.Track 23 23 24 24 func GetRelease(id string) *model.Release { 25 25 for _, release := range Releases { 26 26 if release.ID == id { 27 - return &release 27 + return release 28 28 } 29 29 } 30 30 return nil ··· 33 33 func GetArtist(id string) *model.Artist { 34 34 for _, artist := range Artists { 35 35 if artist.ID == id { 36 - return &artist 36 + return artist 37 37 } 38 38 } 39 39 return nil
+9 -1
main.go
··· 15 15 musicView "arimelody.me/arimelody.me/music/view" 16 16 17 17 "github.com/jmoiron/sqlx" 18 - _ "github.com/lib/pq" 18 + _ "github.com/lib/pq" 19 19 ) 20 20 21 21 const DEFAULT_PORT int = 8080 ··· 48 48 panic(1) 49 49 } 50 50 fmt.Printf("%d releases loaded successfully.\n", len(global.Releases)) 51 + 52 + // pull track data from DB 53 + global.Tracks, err = musicController.PullAllTracks(global.DB) 54 + if err != nil { 55 + fmt.Printf("Failed to pull tracks from database: %v\n", err); 56 + panic(1) 57 + } 58 + fmt.Printf("%d tracks loaded successfully.\n", len(global.Tracks)) 51 59 52 60 // start the web server! 53 61 mux := createServeMux()
+2 -2
music/controller/artist.go
··· 7 7 8 8 // DATABASE 9 9 10 - func PullAllArtists(db *sqlx.DB) ([]model.Artist, error) { 11 - var artists = []model.Artist{} 10 + func PullAllArtists(db *sqlx.DB) ([]*model.Artist, error) { 11 + var artists = []*model.Artist{} 12 12 13 13 err := db.Select(&artists, "SELECT * FROM artist") 14 14 if err != nil {
+19 -4
music/controller/credit.go
··· 1 1 package music 2 2 3 3 import ( 4 + "arimelody.me/arimelody.me/global" 4 5 "arimelody.me/arimelody.me/music/model" 5 6 "github.com/jmoiron/sqlx" 6 7 ) 7 8 8 9 // DATABASE 9 10 10 - func PullReleaseCredits(db *sqlx.DB, releaseID string) ([]model.Credit, error) { 11 - var credits = []model.Credit{} 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{} 12 19 13 20 err := db.Select( 14 - &credits, 15 - "SELECT * FROM musiccredit WHERE release=$1", 21 + &credit_rows, 22 + "SELECT artist, role, is_primary FROM musiccredit WHERE release=$1", 16 23 releaseID, 17 24 ) 18 25 if err != nil { 19 26 return nil, err 27 + } 28 + 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 + }) 20 35 } 21 36 22 37 return credits, nil
+2 -2
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 PullReleaseLinks(db *sqlx.DB, releaseID string) ([]*model.Link, error) { 11 + var links = []*model.Link{} 12 12 13 13 err := db.Select( 14 14 &links,
+20 -3
music/controller/release.go
··· 1 1 package music 2 2 3 3 import ( 4 + "fmt" 5 + 6 + "arimelody.me/arimelody.me/global" 4 7 "arimelody.me/arimelody.me/music/model" 5 8 "github.com/jmoiron/sqlx" 6 9 ) 7 10 8 11 // DATABASE 9 12 10 - func PullAllReleases(db *sqlx.DB) ([]model.Release, error) { 11 - var releases = []model.Release{} 13 + func PullAllReleases(db *sqlx.DB) ([]*model.Release, error) { 14 + var release_rows = []*model.Release{} 15 + var releases = []*model.Release{} 12 16 13 - err := db.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") 17 + err := db.Select(&release_rows, "SELECT * FROM musicrelease ORDER BY release_date DESC") 14 18 if err != nil { 15 19 return nil, err 20 + } 21 + 22 + for _, release := range release_rows { 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 + } 27 + release.Links, _ = PullReleaseLinks(global.DB, release.ID) 28 + if err != nil { 29 + fmt.Printf("Error pulling links for %s: %s\n", release.ID, err) 30 + } 31 + release.Tracks = make([]*model.Track, 0) 32 + releases = append(releases, release) 16 33 } 17 34 18 35 return releases, nil
+3 -3
music/controller/track.go
··· 7 7 8 8 // DATABASE 9 9 10 - func PullAllTracks(db *sqlx.DB) ([]model.Track, error) { 11 - var tracks = []model.Track{} 10 + func PullAllTracks(db *sqlx.DB) ([]*model.Track, error) { 11 + var tracks = []*model.Track{} 12 12 13 - err := db.Select(&tracks, "SELECT id, title, description, lyrics, preview_url FROM musictrack RETURNING id") 13 + err := db.Select(&tracks, "SELECT id, title, description, lyrics, preview_url FROM musictrack") 14 14 if err != nil { 15 15 return nil, err 16 16 }
+3 -3
music/model/release.go
··· 17 17 Artwork string `json:"artwork"` 18 18 Buyname string `json:"buyname"` 19 19 Buylink string `json:"buylink"` 20 - Links []Link `json:"links"` 21 - Credits []Credit `json:"credits"` 22 - Tracks []Track `json:"tracks"` 20 + Links []*Link `json:"links"` 21 + Credits []*Credit `json:"credits"` 22 + Tracks []*Track `json:"tracks"` 23 23 } 24 24 ) 25 25
+1 -1
music/view/music.go
··· 26 26 27 27 func ServeCatalog() http.Handler { 28 28 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 29 - releases := []model.Release{} 29 + releases := []*model.Release{} 30 30 authorised := admin.GetSession(r) != nil 31 31 for _, release := range global.Releases { 32 32 if !release.IsReleased() && !authorised {
public/img/default-avatar.png

This is a binary file and will not be displayed.

+1 -1
schema.sql
··· 2 2 -- Artists (should be applicable to all art) 3 3 -- 4 4 CREATE TABLE public.artist ( 5 - id character varying(64) DEFAULT gen_random_uuid(), 5 + id character varying(64), 6 6 name text NOT NULL, 7 7 website text, 8 8 avatar text
+24 -2
views/admin/index.html
··· 40 40 <div class="card artists"> 41 41 {{range $Artist := .Artists}} 42 42 <div class="artist"> 43 - <img src="https://arimelody.me/img/favicon.png" alt="" width="64" loading="lazy" class="artist-avatar"> 44 - <a href="/admin/artists/arimelody" class="artist-name">ari melody</a> 43 + <img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar"> 44 + <a href="/admin/artists/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a> 45 + </div> 46 + {{end}} 47 + {{if not .Artists}} 48 + <p>There are no artists.</p> 49 + {{end}} 50 + </div> 51 + 52 + <h1>Tracks</h1> 53 + <div class="card tracks"> 54 + {{range $Track := .Tracks}} 55 + <div class="track"> 56 + <h2 class="track-title">{{$Track.Title}}</h2> 57 + {{if $Track.Description}} 58 + <p class="track-description">{{$Track.Description}}</p> 59 + {{else}} 60 + <p class="track-description empty">No description provided.</p> 61 + {{end}} 62 + {{if $Track.Lyrics}} 63 + <p class="track-lyrics">{{$Track.Lyrics}}</p> 64 + {{else}} 65 + <p class="track-lyrics empty">There are no lyrics.</p> 66 + {{end}} 45 67 </div> 46 68 {{end}} 47 69 {{if not .Artists}}