diff --git a/admin/http.go b/admin/http.go index 219e62e..dac0462 100644 --- a/admin/http.go +++ b/admin/http.go @@ -34,13 +34,15 @@ func Handler() http.Handler { } type IndexData struct { - Releases []musicModel.Release - Artists []musicModel.Artist + Releases []*musicModel.Release + Artists []*musicModel.Artist + Tracks []*musicModel.Track } serveTemplate("index.html", IndexData{ Releases: global.Releases, Artists: global.Artists, + Tracks: global.Tracks, }).ServeHTTP(w, r) })) diff --git a/admin/static/admin.css b/admin/static/admin.css index 453c4d6..db0a020 100644 --- a/admin/static/admin.css +++ b/admin/static/admin.css @@ -77,6 +77,7 @@ a:hover { } .release { + margin-bottom: 1em; padding: 1em; display: flex; flex-direction: row; @@ -164,6 +165,7 @@ a:hover { } .artist { + margin-bottom: .5em; padding: .5em; display: flex; flex-direction: row; @@ -185,3 +187,27 @@ a:hover { object-fit: cover; border-radius: 100%; } + +.track { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: column; + gap: .5em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +h2.track-title { + margin: 0 +} + +.track-description { + font-style: italic; +} + +.track .empty { + opacity: 0.75; +} diff --git a/api/api.go b/api/api.go index e44c261..add01fa 100644 --- a/api/api.go +++ b/api/api.go @@ -24,6 +24,7 @@ func Handler() http.Handler { return } })) + mux.Handle("/v1/music/", http.StripPrefix("/v1/music", music.ServeRelease())) mux.Handle("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { @@ -39,5 +40,8 @@ func Handler() http.Handler { } })) + mux.Handle("/v1/musiccredit", CreateCredit()) + mux.Handle("/v1/track", CreateTrack()) + return mux } diff --git a/api/artist.go b/api/artist.go index 7011ff5..14f329b 100644 --- a/api/artist.go +++ b/api/artist.go @@ -98,10 +98,20 @@ func CreateArtist() http.Handler { var data model.Artist err := json.NewDecoder(r.Body).Decode(&data) if err != nil { + fmt.Printf("Failed to create artist: %s\n", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } + if data.ID == "" { + http.Error(w, "Artist ID cannot be blank", http.StatusBadRequest) + return + } + if data.Name == "" { + http.Error(w, "Artist name cannot be blank", http.StatusBadRequest) + return + } + if global.GetArtist(data.ID) != nil { http.Error(w, fmt.Sprintf("Artist %s already exists", data.ID), http.StatusBadRequest) return @@ -114,8 +124,6 @@ func CreateArtist() http.Handler { Avatar: data.Avatar, } - global.Artists = append(global.Artists, artist) - err = controller.CreateArtistDB(global.DB, &artist) if err != nil { fmt.Printf("Failed to create artist %s: %s\n", artist.ID, err) @@ -123,6 +131,8 @@ func CreateArtist() http.Handler { return } + global.Artists = append(global.Artists, &artist) + w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) err = json.NewEncoder(w).Encode(artist) diff --git a/api/credit.go b/api/credit.go new file mode 100644 index 0000000..f246ee3 --- /dev/null +++ b/api/credit.go @@ -0,0 +1,65 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + + "arimelody.me/arimelody.me/global" + "arimelody.me/arimelody.me/music/model" + controller "arimelody.me/arimelody.me/music/controller" +) + +func CreateCredit() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + type creditJSON struct { + Release string + Artist string + Role string + Primary bool + } + + var data creditJSON + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + var release = global.GetRelease(data.Release) + if release == nil { + http.Error(w, fmt.Sprintf("Release %s does not exist", data.Release), http.StatusBadRequest) + return + } + + var artist = global.GetArtist(data.Artist) + if artist == nil { + http.Error(w, fmt.Sprintf("Artist %s does not exist", data.Artist), http.StatusBadRequest) + return + } + + var credit = model.Credit{ + Artist: artist, + Role: data.Role, + Primary: data.Primary, + } + + err = controller.CreateCreditDB(global.DB, release.ID, artist.ID, &credit) + if err != nil { + fmt.Printf("Failed to create credit: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + release.Credits = append(release.Credits, &credit) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(credit) + }) +} diff --git a/api/music.go b/api/release.go similarity index 92% rename from api/music.go rename to api/release.go index 22695cb..cf29ce2 100644 --- a/api/music.go +++ b/api/release.go @@ -14,7 +14,7 @@ import ( func ServeCatalog() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - releases := []model.Release{} + releases := []*model.Release{} authorised := admin.GetSession(r) != nil for _, release := range global.Releases { if !release.IsReleased() && !authorised { @@ -71,13 +71,11 @@ func CreateRelease() http.Handler { Artwork: data.Artwork, Buyname: data.Buyname, Buylink: data.Buylink, - Links: []model.Link{}, - Credits: []model.Credit{}, - Tracks: []model.Track{}, + Links: []*model.Link{}, + Credits: []*model.Credit{}, + Tracks: []*model.Track{}, } - global.Releases = append([]model.Release{release}, global.Releases...) - err = controller.CreateReleaseDB(global.DB, &release) if err != nil { fmt.Printf("Failed to create release %s: %s\n", release.ID, err) @@ -85,6 +83,8 @@ func CreateRelease() http.Handler { return } + global.Releases = append([]*model.Release{&release}, global.Releases...) + w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) err = json.NewEncoder(w).Encode(release) diff --git a/api/track.go b/api/track.go new file mode 100644 index 0000000..b311956 --- /dev/null +++ b/api/track.go @@ -0,0 +1,46 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + + "arimelody.me/arimelody.me/global" + "arimelody.me/arimelody.me/music/model" + controller "arimelody.me/arimelody.me/music/controller" +) + +func CreateTrack() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + var track model.Track + err := json.NewDecoder(r.Body).Decode(&track) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + if track.Title == "" { + http.Error(w, "Track title cannot be empty", http.StatusBadRequest) + return + } + + trackID, err := controller.CreateTrackDB(global.DB, &track) + if err != nil { + fmt.Printf("Failed to create credit: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + track.ID = trackID + global.Tracks = append(global.Tracks, &track) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(track) + }) +} diff --git a/global/data.go b/global/data.go index df5a3b0..1ab90fa 100644 --- a/global/data.go +++ b/global/data.go @@ -17,14 +17,14 @@ var HTTP_DOMAIN = func() string { var DB *sqlx.DB -var Releases []model.Release -var Artists []model.Artist -var Tracks []model.Track +var Releases []*model.Release +var Artists []*model.Artist +var Tracks []*model.Track func GetRelease(id string) *model.Release { for _, release := range Releases { if release.ID == id { - return &release + return release } } return nil @@ -33,7 +33,7 @@ func GetRelease(id string) *model.Release { func GetArtist(id string) *model.Artist { for _, artist := range Artists { if artist.ID == id { - return &artist + return artist } } return nil diff --git a/main.go b/main.go index 453e992..73733ef 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ import ( musicView "arimelody.me/arimelody.me/music/view" "github.com/jmoiron/sqlx" - _ "github.com/lib/pq" + _ "github.com/lib/pq" ) const DEFAULT_PORT int = 8080 @@ -49,6 +49,14 @@ func main() { } fmt.Printf("%d releases loaded successfully.\n", len(global.Releases)) + // pull track data from DB + global.Tracks, err = musicController.PullAllTracks(global.DB) + if err != nil { + fmt.Printf("Failed to pull tracks from database: %v\n", err); + panic(1) + } + fmt.Printf("%d tracks loaded successfully.\n", len(global.Tracks)) + // start the web server! mux := createServeMux() port := DEFAULT_PORT diff --git a/music/controller/artist.go b/music/controller/artist.go index 17f8ddd..887d1d7 100644 --- a/music/controller/artist.go +++ b/music/controller/artist.go @@ -7,8 +7,8 @@ import ( // DATABASE -func PullAllArtists(db *sqlx.DB) ([]model.Artist, error) { - var artists = []model.Artist{} +func PullAllArtists(db *sqlx.DB) ([]*model.Artist, error) { + var artists = []*model.Artist{} err := db.Select(&artists, "SELECT * FROM artist") if err != nil { diff --git a/music/controller/credit.go b/music/controller/credit.go index 4ea987e..d37e3b8 100644 --- a/music/controller/credit.go +++ b/music/controller/credit.go @@ -1,24 +1,39 @@ package music import ( + "arimelody.me/arimelody.me/global" "arimelody.me/arimelody.me/music/model" "github.com/jmoiron/sqlx" ) // DATABASE -func PullReleaseCredits(db *sqlx.DB, releaseID string) ([]model.Credit, error) { - var credits = []model.Credit{} +func PullReleaseCredits(db *sqlx.DB, releaseID string) ([]*model.Credit, error) { + type creditDB struct { + Artist string + Role string + Primary bool `db:"is_primary"` + } + var credit_rows = []creditDB{} + var credits = []*model.Credit{} err := db.Select( - &credits, - "SELECT * FROM musiccredit WHERE release=$1", + &credit_rows, + "SELECT artist, role, is_primary FROM musiccredit WHERE release=$1", releaseID, ) if err != nil { return nil, err } + for _, c := range credit_rows { + credits = append(credits, &model.Credit{ + Artist: global.GetArtist(c.Artist), + Role: c.Role, + Primary: c.Primary, + }) + } + return credits, nil } diff --git a/music/controller/link.go b/music/controller/link.go index b1d399a..81e77b7 100644 --- a/music/controller/link.go +++ b/music/controller/link.go @@ -7,8 +7,8 @@ import ( // DATABASE -func PullReleaseLinks(db *sqlx.DB, releaseID string) ([]model.Link, error) { - var links = []model.Link{} +func PullReleaseLinks(db *sqlx.DB, releaseID string) ([]*model.Link, error) { + var links = []*model.Link{} err := db.Select( &links, diff --git a/music/controller/release.go b/music/controller/release.go index 4806d03..6de8aec 100644 --- a/music/controller/release.go +++ b/music/controller/release.go @@ -1,20 +1,37 @@ package music import ( + "fmt" + + "arimelody.me/arimelody.me/global" "arimelody.me/arimelody.me/music/model" "github.com/jmoiron/sqlx" ) // DATABASE -func PullAllReleases(db *sqlx.DB) ([]model.Release, error) { - var releases = []model.Release{} +func PullAllReleases(db *sqlx.DB) ([]*model.Release, error) { + var release_rows = []*model.Release{} + var releases = []*model.Release{} - err := db.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") + err := db.Select(&release_rows, "SELECT * FROM musicrelease ORDER BY release_date DESC") if err != nil { return nil, err } + for _, release := range release_rows { + release.Credits, err = PullReleaseCredits(global.DB, release.ID) + if err != nil { + fmt.Printf("Error pulling credits for %s: %s\n", release.ID, err) + } + release.Links, _ = PullReleaseLinks(global.DB, release.ID) + if err != nil { + fmt.Printf("Error pulling links for %s: %s\n", release.ID, err) + } + release.Tracks = make([]*model.Track, 0) + releases = append(releases, release) + } + return releases, nil } diff --git a/music/controller/track.go b/music/controller/track.go index 0a352ca..c9d5685 100644 --- a/music/controller/track.go +++ b/music/controller/track.go @@ -7,10 +7,10 @@ import ( // DATABASE -func PullAllTracks(db *sqlx.DB) ([]model.Track, error) { - var tracks = []model.Track{} +func PullAllTracks(db *sqlx.DB) ([]*model.Track, error) { + var tracks = []*model.Track{} - err := db.Select(&tracks, "SELECT id, title, description, lyrics, preview_url FROM musictrack RETURNING id") + err := db.Select(&tracks, "SELECT id, title, description, lyrics, preview_url FROM musictrack") if err != nil { return nil, err } diff --git a/music/model/release.go b/music/model/release.go index 5cd6099..a23166d 100644 --- a/music/model/release.go +++ b/music/model/release.go @@ -17,9 +17,9 @@ type ( Artwork string `json:"artwork"` Buyname string `json:"buyname"` Buylink string `json:"buylink"` - Links []Link `json:"links"` - Credits []Credit `json:"credits"` - Tracks []Track `json:"tracks"` + Links []*Link `json:"links"` + Credits []*Credit `json:"credits"` + Tracks []*Track `json:"tracks"` } ) diff --git a/music/view/music.go b/music/view/music.go index 50b9d36..bcfcf29 100644 --- a/music/view/music.go +++ b/music/view/music.go @@ -26,7 +26,7 @@ func Handler() http.Handler { func ServeCatalog() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - releases := []model.Release{} + releases := []*model.Release{} authorised := admin.GetSession(r) != nil for _, release := range global.Releases { if !release.IsReleased() && !authorised { diff --git a/public/img/default-avatar.png b/public/img/default-avatar.png new file mode 100644 index 0000000..8803a91 Binary files /dev/null and b/public/img/default-avatar.png differ diff --git a/schema.sql b/schema.sql index ab75620..a8dd65e 100644 --- a/schema.sql +++ b/schema.sql @@ -2,7 +2,7 @@ -- Artists (should be applicable to all art) -- CREATE TABLE public.artist ( - id character varying(64) DEFAULT gen_random_uuid(), + id character varying(64), name text NOT NULL, website text, avatar text diff --git a/views/admin/index.html b/views/admin/index.html index ab7e407..47f37b1 100644 --- a/views/admin/index.html +++ b/views/admin/index.html @@ -40,8 +40,30 @@
{{range $Artist := .Artists}}
- - ari melody + + {{$Artist.Name}} +
+ {{end}} + {{if not .Artists}} +

There are no artists.

+ {{end}} +
+ +

Tracks

+
+ {{range $Track := .Tracks}} +
+

{{$Track.Title}}

+ {{if $Track.Description}} +

{{$Track.Description}}

+ {{else}} +

No description provided.

+ {{end}} + {{if $Track.Lyrics}} +

{{$Track.Lyrics}}

+ {{else}} +

There are no lyrics.

+ {{end}}
{{end}} {{if not .Artists}}