diff --git a/admin/http.go b/admin/http.go index 05a20e9..1fdd237 100644 --- a/admin/http.go +++ b/admin/http.go @@ -15,12 +15,18 @@ import ( musicModel "arimelody.me/arimelody.me/music/model" ) +type loginData struct { + DiscordURI string + Token string +} + func Handler() http.Handler { mux := http.NewServeMux() mux.Handle("/login", LoginHandler()) mux.Handle("/logout", MustAuthorise(LogoutHandler())) mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) + mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease()))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) @@ -46,9 +52,9 @@ func Handler() http.Handler { } ) - var tracks = []Track{} for _, track := range global.Tracks { + if track.Release != nil { continue } tracks = append(tracks, Track{ Track: track, Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
", -1)), @@ -79,8 +85,9 @@ func MustAuthorise(next http.Handler) http.Handler { } func GetSession(r *http.Request) *Session { - // TODO: remove later- this bypasses auth! - return &Session{} + if os.Getenv("ARIMELODY_ADMIN_BYPASS") == "true" { + return &Session{} + } var token = "" // is the session token in context? @@ -137,7 +144,7 @@ func LoginHandler() http.Handler { code := r.URL.Query().Get("code") if code == "" { - serveTemplate("login.html", discord.REDIRECT_URI).ServeHTTP(w, r) + serveTemplate("login.html", loginData{DiscordURI: discord.REDIRECT_URI}).ServeHTTP(w, r) return } @@ -155,8 +162,9 @@ func LoginHandler() http.Handler { return } - if discord_user.Id != ADMIN_ID_DISCORD { + if discord_user.ID != ADMIN_ID_DISCORD { // TODO: unauthorized user; revoke the token + fmt.Printf("Unauthorized login attempted: %s\n", discord_user.ID) http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } @@ -174,16 +182,17 @@ func LoginHandler() http.Handler { cookie.Path = "/" http.SetCookie(w, &cookie) - w.WriteHeader(http.StatusOK) - w.Header().Add("Content-Type", "text/html") - w.Write([]byte( - ""+ - ""+ - ""+ - "Logged in successfully. "+ - "You should be redirected to /admin/ in 5 seconds."+ - ""), - ) + serveTemplate("login.html", loginData{Token: session.Token}).ServeHTTP(w, r) + // w.WriteHeader(http.StatusOK) + // w.Header().Add("Content-Type", "text/html") + // w.Write([]byte( + // ""+ + // ""+ + // ""+ + // "Logged in successfully. "+ + // "You should be redirected to /admin/ in 5 seconds."+ + // ""), + // ) }) } @@ -194,36 +203,28 @@ func LogoutHandler() http.Handler { return } - token := r.Context().Value("token").(string) - - if token == "" { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } + session := GetSession(r) // remove this session from the list sessions = func (token string) []*Session { new_sessions := []*Session{} for _, session := range sessions { - new_sessions = append(new_sessions, session) + if session.Token != token { + new_sessions = append(new_sessions, session) + } } return new_sessions - }(token) + }(session.Token) - w.WriteHeader(http.StatusOK) - w.Write([]byte( - ""+ - "Logged out successfully. "+ - "You should be redirected to / in 5 seconds."), - ) + serveTemplate("logout.html", nil).ServeHTTP(w, r) }) } func serveTemplate(page string, data any) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - lp_layout := filepath.Join("views", "admin", "layout.html") + lp_layout := filepath.Join("admin", "views", "layout.html") lp_prideflag := filepath.Join("views", "prideflag.html") - fp := filepath.Join("views", "admin", filepath.Clean(page)) + fp := filepath.Join("admin", "views", filepath.Clean(page)) info, err := os.Stat(fp) if err != nil { diff --git a/admin/releasehttp.go b/admin/releasehttp.go new file mode 100644 index 0000000..4dd54e4 --- /dev/null +++ b/admin/releasehttp.go @@ -0,0 +1,58 @@ +package admin + +import ( + "fmt" + "html/template" + "net/http" + "strings" + + "arimelody.me/arimelody.me/global" + "arimelody.me/arimelody.me/music/model" +) + +type ( + gatewayTrack struct { + *model.Track + Lyrics template.HTML + Number int + } + + gatewayRelease struct { + *model.Release + Tracks []gatewayTrack + } +) + +func serveRelease() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + http.NotFound(w, r) + return + } + + id := r.URL.Path[1:] + release := global.GetRelease(id) + if release == nil { + http.NotFound(w, r) + return + } + + tracks := []gatewayTrack{} + for i, track := range release.Tracks { + tracks = append([]gatewayTrack{{ + Track: track, + Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
", -1)), + Number: len(release.Tracks) - i, + }}, tracks...) + } + + lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK} + + serveTemplate("edit-release.html", gatewayRelease{release, tracks}).ServeHTTP(&lrw, r) + + if lrw.Code != http.StatusOK { + fmt.Printf("Error rendering admin release page for %s\n", id) + return + } + }) +} diff --git a/admin/static/admin.css b/admin/static/admin.css index 9ec1e36..436866e 100644 --- a/admin/static/admin.css +++ b/admin/static/admin.css @@ -21,7 +21,7 @@ header { margin: 1em auto; display: flex; flex-direction: row; - justify-content: center; + justify-content: left; background: #f8f8f8; border-radius: .5em; @@ -29,13 +29,23 @@ header { } header .icon { height: 100%; - - margin-right: 1em; } - -header a { - height: 100%; +header .title { width: auto; + height: 100%; + + margin: 0 1em 0 0; + + display: flex; + + line-height: 2em; + text-decoration: none; + + color: inherit; +} +header a { + width: auto; + height: 100%; margin: 0px; padding: 0 1em; @@ -47,9 +57,12 @@ header a { color: inherit; } - header a:hover { background: #00000010; + text-decoration: none; +} +header #logout { + margin-left: auto; } main { @@ -67,6 +80,10 @@ a:hover { text-decoration: underline; } +.card { + margin-bottom: 2em; +} + .card h2 { margin: 0 0 .5em 0; } @@ -77,184 +94,18 @@ a:hover { } .card-title { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; -} - -.create-btn { - background: #c4ff6a; - padding: .5em .8em; - border-radius: .5em; - border: 1px solid #84b141; - text-decoration: none; -} -.create-btn:hover { - background: #fff; - border-color: #d0d0d0; - text-decoration: inherit; -} -.create-btn:active { - background: #d0d0d0; - border-color: #808080; - text-decoration: inherit; -} - -.release { margin-bottom: 1em; - padding: 1em; display: flex; - flex-direction: row; gap: 1em; - - border-radius: .5em; - background: #f8f8f8f8; - border: 1px solid #808080; -} - -.release-artwork { - width: 96px; - - display: flex; - justify-content: center; - align-items: center; -} - -.release-artwork img { - width: 100%; - aspect-ratio: 1; -} - -.latest-release .release-info { - width: 300px; - flex-direction: column; -} - -.release-title small { - opacity: .75; -} - -.release-links { - margin: .5em 0; - padding: 0; - display: flex; - flex-direction: row; - list-style: none; - flex-wrap: wrap; - gap: .5em; -} - -.release-links li { - flex-grow: 1; -} - -.release-links a { - padding: .5em; - display: block; - - border-radius: .5em; - text-decoration: none; - color: #f0f0f0; - background: #303030; - text-align: center; - - transition: color .1s, background .1s; -} - -.release-links a:hover { - color: #303030; - background: #f0f0f0; -} - -.release-actions { - margin-top: .5em; -} - -.release-actions a { - margin-right: .3em; - padding: .3em .5em; - display: inline-block; - - border-radius: .3em; - background: #e0e0e0; - - transition: color .1s, background .1s; -} - -.release-actions a:hover { - color: #303030; - background: #f0f0f0; - - text-decoration: none; -} - -.artist { - margin-bottom: .5em; - padding: .5em; - display: flex; flex-direction: row; align-items: center; - gap: .5em; - - border-radius: .5em; - background: #f8f8f8f8; - border: 1px solid #808080; -} - -.artist:hover { - text-decoration: hover; -} - -.artist-avatar { - width: 32px; - height: 32px; - 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; - display: flex; - flex-direction: row; justify-content: space-between; } -.track-album { - margin-left: auto; - font-style: italic; - font-size: .75em; - opacity: .5; -} - -.track-album.empty { - color: #ff2020; - opacity: 1; -} - -.track-description { - font-style: italic; -} - -.track-lyrics { - max-height: 10em; - overflow-y: scroll; -} - -.track .empty { - opacity: 0.75; +.card-title h1, +.card-title h2, +.card-title h3 { + margin: 0; } @media screen and (max-width: 520px) { diff --git a/admin/static/edit-release.js b/admin/static/edit-release.js new file mode 100644 index 0000000..7be9069 --- /dev/null +++ b/admin/static/edit-release.js @@ -0,0 +1,96 @@ +import Stateful from "/script/silver.min.js" + +const releaseID = document.getElementById("release").dataset.id; +const artwork_input = document.getElementById("artwork"); +const type_input = document.getElementById("type"); +const desc_input = document.getElementById("description"); +const date_input = document.getElementById("release-date"); +const buyname_input = document.getElementById("buyname"); +const buylink_input = document.getElementById("buylink"); +const vis_input = document.getElementById("visibility"); +const save_btn = document.getElementById("save"); + +let token = atob(localStorage.getItem("arime-token")); + +let edited = new Stateful(false); + +let release_data = update_data(undefined); + +function update_data(old) { + let release_data = { + visible: vis_input.value === "true", + title: undefined, + description: desc_input.value, + type: type_input.value, + releaseDate: date_input.value, + artwork: artwork_input.attributes.src.value, + buyname: buyname_input.value, + buylink: buylink_input.value, + }; + + if (release_data && release_data != old) { + edited.set(true); + } + + return release_data; +} + +function save_release() { + console.table(release_data); + + edited.set(false); + + (async () => { + const res = await fetch( + "/api/v1/music/" + releaseID, { + method: "PUT", + body: JSON.stringify(release_data), + headers: { + "Content-Type": "application/json", + "Authorisation": "Bearer " + token, + }, + }); + + if (!res.ok) { + const text = await res.text(); + console.error(text); + alert(text); + return; + } + + location = location; + })(); +} +window.save_release = save_release; + +edited.onUpdate(edited => { + save_btn.disabled = !edited; +}) + +artwork_input.addEventListener("click", () => { + release_data = update_data(release_data); +}); +type_input.addEventListener("change", () => { + release_data = update_data(release_data); +}); +desc_input.addEventListener("change", () => { + release_data = update_data(release_data); +}); +date_input.addEventListener("change", () => { + release_data = update_data(release_data); +}); +buyname_input.addEventListener("change", () => { + release_data = update_data(release_data); +}); +buylink_input.addEventListener("change", () => { + release_data = update_data(release_data); +}); +vis_input.addEventListener("change", () => { + release_data = update_data(release_data); +}); + +save_btn.addEventListener("click", () => { + if (!edited.get()) return; + + save_release(); +}) diff --git a/admin/static/index.css b/admin/static/index.css new file mode 100644 index 0000000..37bde08 --- /dev/null +++ b/admin/static/index.css @@ -0,0 +1,183 @@ +.create-btn { + background: #c4ff6a; + padding: .5em .8em; + border-radius: .5em; + border: 1px solid #84b141; + text-decoration: none; +} +.create-btn:hover { + background: #fff; + border-color: #d0d0d0; + text-decoration: inherit; +} +.create-btn:active { + background: #d0d0d0; + border-color: #808080; + text-decoration: inherit; +} + +.release { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: row; + gap: 1em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.release-artwork { + width: 96px; + + display: flex; + justify-content: center; + align-items: center; +} + +.release-artwork img { + width: 100%; + aspect-ratio: 1; +} + +.latest-release .release-info { + width: 300px; + flex-direction: column; +} + +.release-title small { + opacity: .75; +} + +.release-links { + margin: .5em 0; + padding: 0; + display: flex; + flex-direction: row; + list-style: none; + flex-wrap: wrap; + gap: .5em; +} + +.release-links li { + flex-grow: 1; +} + +.release-links a { + padding: .5em; + display: block; + + border-radius: .5em; + text-decoration: none; + color: #f0f0f0; + background: #303030; + text-align: center; + + transition: color .1s, background .1s; +} + +.release-links a:hover { + color: #303030; + background: #f0f0f0; +} + +.release-actions { + margin-top: .5em; +} + +.release-actions a { + margin-right: .3em; + padding: .3em .5em; + display: inline-block; + + border-radius: .3em; + background: #e0e0e0; + + transition: color .1s, background .1s; +} + +.release-actions a:hover { + color: #303030; + background: #f0f0f0; + + text-decoration: none; +} + +.artist { + margin-bottom: .5em; + padding: .5em; + display: flex; + flex-direction: row; + align-items: center; + gap: .5em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.artist:hover { + text-decoration: hover; +} + +.artist-avatar { + width: 32px; + height: 32px; + 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; +} + +.card h2.track-title { + margin: 0; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.track-id { + width: fit-content; + font-family: "Monaspace Argon", monospace; + font-size: .8em; + font-style: italic; + line-height: 1em; + user-select: all; +} + +.track-album { + margin-left: auto; + font-style: italic; + font-size: .75em; + opacity: .5; +} + +.track-album.empty { + color: #ff2020; + opacity: 1; +} + +.track-description { + font-style: italic; +} + +.track-lyrics { + max-height: 10em; + overflow-y: scroll; +} + +.track .empty { + opacity: 0.75; +} + diff --git a/admin/static/release.css b/admin/static/release.css new file mode 100644 index 0000000..fdcab51 --- /dev/null +++ b/admin/static/release.css @@ -0,0 +1,219 @@ +#release { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: row; + gap: 1.2em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.release-artwork { + width: 200px; + + display: flex; + justify-content: center; + align-items: start; +} + +.release-artwork img { + width: 100%; + aspect-ratio: 1; +} +.release-artwork img:hover { + outline: 1px solid #808080; + cursor: pointer; +} + +.release-info { + margin: .5em 0; + flex-grow: 1; + display: flex; + flex-direction: column; +} + +.release-title { + margin: 0; +} + +.release-title small { + opacity: .75; +} + +.release-info table { + width: 100%; + margin: .5em 0; + border-collapse: collapse; +} +.release-info table td { + padding: .2em; + border-bottom: 1px solid #d0d0d0; +} +.release-info table tr td:first-child { + vertical-align: top; + opacity: .66; +} +.release-info table tr td:not(:first-child):hover { + background: #e8e8e8; + cursor: pointer; +} +.release-info table td select, +.release-info table td input, +.release-info table td textarea { + padding: .2em; + resize: none; + width: 100%; + font-family: inherit; + font-size: inherit; + color: inherit; + border: none; + background: none; + outline: none; +} +.release-info table td:has(select), +.release-info table td:has(input), +.release-info table td:has(textarea) { + padding: 0; +} + +button, .button { + padding: .5em .8em; + font-family: inherit; + font-size: inherit; + border-radius: .5em; + border: 1px solid #a0a0a0; + background: #f0f0f0; + color: inherit; +} +button:hover, .button:hover { + background: #fff; + border-color: #d0d0d0; +} +button:active, .button:active { + background: #d0d0d0; + border-color: #808080; +} + +button.edit { + color: inherit; + background: #c4ff6a; + border-color: #84b141; +} +button.edit:hover { + background: #fff; + border-color: #d0d0d0; +} +button.edit:active { + background: #d0d0d0; + border-color: #808080; +} + +button.save { + background: #6fd7ff; + border-color: #6f9eb0; +} +button.save:hover { + background: #fff; + border-color: #d0d0d0; +} +button.save:active { + background: #d0d0d0; + border-color: #808080; +} +button[disabled] { + background: #d0d0d0 !important; + border-color: #808080 !important; + opacity: .5; + cursor: not-allowed !important; +} + +.release-actions { + margin-top: auto; + display: flex; + gap: .5em; + flex-direction: row; + justify-content: right; +} + +.credit { + margin-bottom: .5em; + padding: .5em; + display: flex; + flex-direction: row; + align-items: center; + gap: 1em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.credit .artist-avatar { + border-radius: .5em; +} + +.credit .artist-name { + font-weight: bold; +} + +.credit .artist-role small { + font-size: inherit; + opacity: .66; +} + +.track { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: column; + gap: .5em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.card h2.track-title { + margin: 0; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.track-id { + width: fit-content; + font-family: "Monaspace Argon", monospace; + font-size: .8em; + font-style: italic; + line-height: 1em; + user-select: all; + -webkit-user-select: all; +} + +.track-album { + margin-left: auto; + font-style: italic; + font-size: .75em; + opacity: .5; +} + +.track-album.empty { + color: #ff2020; + opacity: 1; +} + +.track-description { + font-style: italic; +} + +.track-lyrics { + max-height: 10em; + overflow-y: scroll; +} + +.track .empty { + opacity: 0.75; +} + diff --git a/admin/views/edit-release.html b/admin/views/edit-release.html new file mode 100644 index 0000000..7ab74cc --- /dev/null +++ b/admin/views/edit-release.html @@ -0,0 +1,143 @@ +{{define "head"}} +editing {{.Title}} - ari melody 💫 + + + +{{end}} + +{{define "content"}} +
+ +
+
+ +
+
+

+ + {{.Title}} + {{.GetReleaseYear}} +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Artists{{.PrintArtists true true}}
Type + {{$t := .ReleaseType}} + +
Description + +
Release Date + +
Buy Name + +
Buy Link + +
Visible + +
+
+ Gateway + +
+
+
+ +
+

Credits ({{len .Credits}})

+ +
+
+ {{range $Credit := .Credits}} +
+ +
+

{{$Credit.Artist.Name}}

+

+ {{$Credit.Role}} + {{if $Credit.Primary}} + (Primary) + {{end}} +

+
+
+ {{end}} + {{if not .Credits}} +

There are no credits.

+ {{end}} +
+ +
+

Tracklist ({{len .Tracks}})

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

{{$Track.Number}}. {{$Track.Title}}

+

{{$Track.ID}}

+ {{if $Track.Description}} +

{{$Track.Description}}

+ {{else}} +

No description provided.

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

{{$Track.Lyrics}}

+ {{else}} +

There are no lyrics.

+ {{end}} +
+ {{end}} +
+ +
+ + +{{end}} diff --git a/views/admin/index.html b/admin/views/index.html similarity index 78% rename from views/admin/index.html rename to admin/views/index.html index 660866f..8847492 100644 --- a/views/admin/index.html +++ b/admin/views/index.html @@ -1,16 +1,10 @@ {{define "head"}} admin - ari melody 💫 + {{end}} {{define "content"}} -
- - home - releases - artists -
-
@@ -24,11 +18,18 @@
-

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

+

+ {{$Release.Title}} + + {{$Release.GetReleaseYear}} + {{if not $Release.Visible}}(hidden){{end}} + +

{{$Release.PrintArtists true true}}

-

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

+

{{$Release.ReleaseType}} + ({{len $Release.Tracks}} track{{if not (eq (len $Release.Tracks) 1)}}s{{end}})

@@ -60,6 +61,8 @@ Create New
+

"Orphaned" tracks that have not yet been bound to a release.

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

@@ -67,9 +70,10 @@ {{if $Track.Release}} {{$Track.Release.Title}} {{else}} - (no album) + (no release) {{end}}

+

{{$Track.ID}}

{{if $Track.Description}}

{{$Track.Description}}

{{else}} diff --git a/views/admin/layout.html b/admin/views/layout.html similarity index 68% rename from views/admin/layout.html rename to admin/views/layout.html index 06c15c9..961dfc6 100644 --- a/views/admin/layout.html +++ b/admin/views/layout.html @@ -13,6 +13,13 @@ +
+ + arimelody.me + home + log out +
+ {{block "content" .}} {{end}} diff --git a/admin/views/login.html b/admin/views/login.html new file mode 100644 index 0000000..0edb29e --- /dev/null +++ b/admin/views/login.html @@ -0,0 +1,35 @@ +{{define "head"}} +login - ari melody 💫 + + + +{{end}} + +{{define "content"}} +
+ + {{if .Token}} + + +

+ Logged in successfully. + You should be redirected to /admin in 5 seconds. + +

+ {{else}} +

Log in with Discord.

+ {{end}} + +
+{{end}} diff --git a/admin/views/logout.html b/admin/views/logout.html new file mode 100644 index 0000000..8f8f801 --- /dev/null +++ b/admin/views/logout.html @@ -0,0 +1,25 @@ +{{define "head"}} +admin - ari melody 💫 + + + +{{end}} + +{{define "content"}} +
+ + +

+ Logged out successfully. + You should be redirected to / in 5 seconds. + +

+ +
+{{end}} diff --git a/api/artist.go b/api/artist.go index e0e984b..03c69f7 100644 --- a/api/artist.go +++ b/api/artist.go @@ -10,6 +10,13 @@ import ( controller "arimelody.me/arimelody.me/music/controller" ) +type artistJSON struct { + ID string `json:"id"` + Name *string `json:"name"` + Website *string `json:"website"` + Avatar *string `json:"avatar"` +} + func ServeAllArtists() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") @@ -78,7 +85,7 @@ func CreateArtist() http.Handler { return } - var data model.Artist + var data artistJSON err := json.NewDecoder(r.Body).Decode(&data) if err != nil { fmt.Printf("Failed to create artist: %s\n", err) @@ -90,7 +97,7 @@ func CreateArtist() http.Handler { http.Error(w, "Artist ID cannot be blank\n", http.StatusBadRequest) return } - if data.Name == "" { + if data.Name == nil || *data.Name == "" { http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest) return } @@ -102,9 +109,9 @@ func CreateArtist() http.Handler { var artist = model.Artist{ ID: data.ID, - Name: data.Name, - Website: data.Website, - Avatar: data.Avatar, + Name: *data.Name, + Website: *data.Website, + Avatar: *data.Avatar, } err = controller.CreateArtistDB(global.DB, &artist) @@ -138,7 +145,7 @@ func UpdateArtist() http.Handler { return } - var data model.Artist + var data artistJSON err := json.NewDecoder(r.Body).Decode(&data) if err != nil { fmt.Printf("Failed to update artist: %s\n", err) @@ -153,24 +160,24 @@ func UpdateArtist() http.Handler { return } - if data.ID == "" { data.ID = artist.ID } + var update = *artist - if data.Name == "" { - http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest) - return - } + if data.ID != "" { update.ID = data.ID } + if data.Name != nil { update.Name = *data.Name } + if data.Website != nil { update.Website = *data.Website } + if data.Avatar != nil { update.Avatar = *data.Avatar } - err = controller.UpdateArtistDB(global.DB, &data) + err = controller.UpdateArtistDB(global.DB, &update) if err != nil { fmt.Printf("Failed to update artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - artist.ID = data.ID - artist.Name = data.Name - artist.Website = data.Website - artist.Avatar = data.Avatar + artist.ID = update.ID + artist.Name = update.Name + artist.Website = update.Website + artist.Avatar = update.Avatar w.Header().Add("Content-Type", "application/json") err = json.NewEncoder(w).Encode(artist) diff --git a/api/release.go b/api/release.go index 3011c9f..6d02733 100644 --- a/api/release.go +++ b/api/release.go @@ -15,14 +15,14 @@ import ( type releaseBodyJSON struct { ID string `json:"id"` - Visible bool `json:"visible"` - Title string `json:"title"` - Description string `json:"description"` - ReleaseType model.ReleaseType `json:"type"` - ReleaseDate time.Time `json:"releaseDate"` - Artwork string `json:"artwork"` - Buyname string `json:"buyname"` - Buylink string `json:"buylink"` + Visible *bool `json:"visible"` + Title *string `json:"title"` + Description *string `json:"description"` + ReleaseType *model.ReleaseType `json:"type"` + ReleaseDate *string `json:"releaseDate"` + Artwork *string `json:"artwork"` + Buyname *string `json:"buyname"` + Buylink *string `json:"buylink"` } func ServeCatalog() http.Handler { @@ -36,7 +36,7 @@ func ServeCatalog() http.Handler { Artwork string `json:"artwork"` Buyname string `json:"buyname"` Buylink string `json:"buylink"` - Links []*model.Link `json:"links"` + Links []*model.Link `json:"links"` } catalog := []CatalogItem{} @@ -85,26 +85,40 @@ func CreateRelease() http.Handler { http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest) return } - if data.Title == "" { + if *data.Title == "" { http.Error(w, "Release title cannot be empty\n", http.StatusBadRequest) return } + if data.Buyname == nil || *data.Buyname == "" { *data.Buyname = "buy" } + if data.Buylink == nil || *data.Buylink == "" { *data.Buylink = "https://arimelody.me" } if global.GetRelease(data.ID) != nil { http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest) return } + releaseDate := time.Time{} + if *data.ReleaseDate == "" { + http.Error(w, "Release date cannot be empty\n", http.StatusBadRequest) + return + } else if data.ReleaseDate != nil { + releaseDate, err = time.Parse("2006-01-02T15:04", *data.ReleaseDate) + if err != nil { + http.Error(w, "Invalid release date", http.StatusBadRequest) + return + } + } + var release = model.Release{ ID: data.ID, - Visible: data.Visible, - Title: data.Title, - Description: data.Description, - ReleaseType: data.ReleaseType, - ReleaseDate: data.ReleaseDate, - Artwork: data.Artwork, - Buyname: data.Buyname, - Buylink: data.Buylink, + Visible: *data.Visible, + Title: *data.Title, + Description: *data.Description, + ReleaseType: *data.ReleaseType, + ReleaseDate: releaseDate, + Artwork: *data.Artwork, + Buyname: *data.Buyname, + Buylink: *data.Buylink, Links: []*model.Link{}, Credits: []*model.Credit{}, Tracks: []*model.Track{}, @@ -153,41 +167,46 @@ func UpdateRelease() http.Handler { return } - if data.ID == "" { data.ID = release.ID } - - if data.Title == "" { - http.Error(w, "Release title cannot be blank\n", http.StatusBadRequest) - return + var update = *release + if data.ID != "" { update.ID = data.ID } + if data.Visible != nil { update.Visible = *data.Visible } + if data.Title != nil { update.Title = *data.Title } + if data.Description != nil { update.Description = *data.Description } + if data.ReleaseType != nil { update.ReleaseType = *data.ReleaseType } + if data.ReleaseDate != nil { + newDate, err := time.Parse("2006-01-02T15:04", *data.ReleaseDate) + if err != nil { + http.Error(w, "Invalid release date", http.StatusBadRequest) + return + } + update.ReleaseDate = newDate } - - var new_release = model.Release{ - ID: data.ID, - Visible: data.Visible, - Title: data.Title, - Description: data.Description, - ReleaseType: data.ReleaseType, - ReleaseDate: data.ReleaseDate, - Artwork: data.Artwork, - Buyname: data.Buyname, - Buylink: data.Buylink, + if data.Artwork != nil { update.Artwork = *data.Artwork } + if data.Buyname != nil { + if *data.Buyname == "" { + http.Error(w, "Release buy name cannot be empty", http.StatusBadRequest) + return + } + update.Buyname = *data.Buyname } + if data.Buylink != nil { update.Buylink = *data.Buylink } - err = controller.UpdateReleaseDB(global.DB, release) + err = controller.UpdateReleaseDB(global.DB, &update) if err != nil { fmt.Printf("Failed to update release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - release.ID = new_release.ID - release.Visible = new_release.Visible - release.Title = new_release.Title - release.Description = new_release.Description - release.ReleaseType = new_release.ReleaseType - release.ReleaseDate = new_release.ReleaseDate - release.Artwork = new_release.Artwork - release.Buyname = new_release.Buyname - release.Buylink = new_release.Buylink + release.ID = update.ID + release.Visible = update.Visible + release.Title = update.Title + release.Description = update.Description + release.ReleaseType = update.ReleaseType + release.ReleaseDate = update.ReleaseDate + release.Artwork = update.Artwork + release.Buyname = update.Buyname + release.Buylink = update.Buylink w.Header().Add("Content-Type", "application/json") err = json.NewEncoder(w).Encode(release) @@ -195,6 +214,7 @@ func UpdateRelease() http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } + return } if len(segments) == 2 { diff --git a/api/track.go b/api/track.go index cf0cab3..a5fc03d 100644 --- a/api/track.go +++ b/api/track.go @@ -122,10 +122,7 @@ func UpdateTrack() http.Handler { data.ID = trackID - if data.Title == "" { - http.Error(w, "Track title cannot be blank\n", http.StatusBadRequest) - return - } + if data.Title == "" { data.Title = track.Title } err = controller.UpdateTrackDB(global.DB, &data) if err != nil { diff --git a/discord/discord.go b/discord/discord.go index 5190b09..46ca7f7 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -57,7 +57,7 @@ type ( AuthInfoResponse struct { Application struct { - Id string `json:"id"` + ID string `json:"id"` Name string `json:"name"` Icon string `json:"icon"` Description string `json:"description"` @@ -72,7 +72,7 @@ type ( } DiscordUser struct { - Id string `json:"id"` + ID string `json:"id"` Username string `json:"username"` Avatar string `json:"avatar"` Discriminator string `json:"discriminator"` diff --git a/music/controller/release.go b/music/controller/release.go index 55eb51a..9e70aa7 100644 --- a/music/controller/release.go +++ b/music/controller/release.go @@ -76,7 +76,7 @@ func CreateReleaseDB(db *sqlx.DB, release *model.Release) error { release.Title, release.Description, release.ReleaseType, - release.ReleaseDate.Format("2-Jan-2006"), + release.ReleaseDate.Format("2006-01-02 15:04:05"), release.Artwork, release.Buyname, release.Buylink, @@ -91,15 +91,14 @@ func CreateReleaseDB(db *sqlx.DB, release *model.Release) error { func UpdateReleaseDB(db *sqlx.DB, release *model.Release) error { _, err := db.Exec( "UPDATE musicrelease SET "+ - "visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9) "+ - "VALUES ($2, $3, $4, $5, $6, $7, $8, $9) "+ + "visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9 "+ "WHERE id=$1", release.ID, release.Visible, release.Title, release.Description, release.ReleaseType, - release.ReleaseDate.Format("2-Jan-2006"), + release.ReleaseDate.Format("2006-01-02 15:04:05"), release.Artwork, release.Buyname, release.Buylink, diff --git a/music/model/artist.go b/music/model/artist.go index 1956e3a..64e52d9 100644 --- a/music/model/artist.go +++ b/music/model/artist.go @@ -2,13 +2,17 @@ package model type ( Artist struct { - ID string `json:"id"` + ID string `json:"id"` Name string `json:"name"` Website string `json:"website"` Avatar string `json:"avatar"` } ) +func (artist Artist) GetWebsite() string { + return artist.Website +} + func (artist Artist) GetAvatar() string { if artist.Avatar == "" { return "/img/default-avatar.png" diff --git a/music/model/release.go b/music/model/release.go index a23166d..47dce6e 100644 --- a/music/model/release.go +++ b/music/model/release.go @@ -8,18 +8,18 @@ import ( type ( ReleaseType string Release struct { - ID string `json:"id"` - Visible bool `json:"visible"` - Title string `json:"title"` - Description string `json:"description"` - ReleaseType ReleaseType `json:"type" db:"type"` - ReleaseDate time.Time `json:"releaseDate" db:"release_date"` - Artwork string `json:"artwork"` - Buyname string `json:"buyname"` - Buylink string `json:"buylink"` - Links []*Link `json:"links"` - Credits []*Credit `json:"credits"` - Tracks []*Track `json:"tracks"` + ID string `json:"id"` + Visible bool `json:"visible"` + Title string `json:"title"` + Description string `json:"description"` + ReleaseType ReleaseType `json:"type" db:"type"` + ReleaseDate time.Time `json:"releaseDate" db:"release_date"` + Artwork string `json:"artwork"` + Buyname string `json:"buyname"` + Buylink string `json:"buylink"` + Links []*Link `json:"links"` + Credits []*Credit `json:"credits"` + Tracks []*Track `json:"tracks"` } ) @@ -32,11 +32,8 @@ const ( // GETTERS -func (release Release) GetArtwork() string { - if release.Artwork == "" { - return "/img/default-cover-art.png" - } - return release.Artwork +func (release Release) TextReleaseDate() string { + return release.ReleaseDate.Format("2006-01-02T15:04") } func (release Release) PrintReleaseDate() string { @@ -47,6 +44,13 @@ func (release Release) GetReleaseYear() int { return release.ReleaseDate.Year() } +func (release Release) GetArtwork() string { + if release.Artwork == "" { + return "/img/default-cover-art.png" + } + return release.Artwork +} + func (release Release) IsSingle() bool { return len(release.Tracks) == 1; } diff --git a/music/view/release.go b/music/view/release.go index 65ddc3a..11b18e8 100644 --- a/music/view/release.go +++ b/music/view/release.go @@ -12,6 +12,19 @@ import ( "arimelody.me/arimelody.me/music/model" ) +type ( + gatewayTrack struct { + *model.Track + Lyrics template.HTML + Number int + } + + gatewayRelease struct { + *model.Release + Tracks []gatewayTrack + } +) + // HTTP HANDLERS func ServeRelease() http.Handler { @@ -51,19 +64,6 @@ func ServeGateway() http.Handler { return } - type ( - GatewayTrack struct { - *model.Track - Lyrics template.HTML - Number int - } - - GatewayRelease struct { - *model.Release - Tracks []GatewayTrack - } - ) - id := r.URL.Path[1:] release := global.GetRelease(id) if release == nil { @@ -71,9 +71,9 @@ func ServeGateway() http.Handler { return } - tracks := []GatewayTrack{} + tracks := []gatewayTrack{} for i, track := range release.Tracks { - tracks = append([]GatewayTrack{GatewayTrack{ + tracks = append([]gatewayTrack{{ Track: track, Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
", -1)), Number: len(release.Tracks) - i, @@ -87,9 +87,9 @@ func ServeGateway() http.Handler { return } - lrw := global.LoggingResponseWriter{w, http.StatusOK} + lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK} - global.ServeTemplate("music-gateway.html", GatewayRelease{release, tracks}).ServeHTTP(&lrw, r) + global.ServeTemplate("music-gateway.html", gatewayRelease{release, tracks}).ServeHTTP(&lrw, r) if lrw.Code != http.StatusOK { fmt.Printf("Error rendering music gateway for %s\n", id) diff --git a/public/script/silver.min.js b/public/script/silver.min.js new file mode 100644 index 0000000..47d4cbb --- /dev/null +++ b/public/script/silver.min.js @@ -0,0 +1 @@ +export default class Stateful{#e;#t=[];constructor(e){this.#e=e}get(){return this.#e}set(e){let t=this.#e;this.#e=e;for(let s in this.#t)this.#t[s](e,t)}update(e){this.set(e(this.#e))}onUpdate(e){return this.#t.push(e),e}removeListener(e){this.#t=this.#t.filter((t=>t!==e))}} diff --git a/schema.sql b/schema.sql index a8dd65e..1a7533c 100644 --- a/schema.sql +++ b/schema.sql @@ -18,7 +18,7 @@ CREATE TABLE public.musicrelease ( title text NOT NULL, description text, type text, - release_date DATE NOT NULL, + release_date TIMESTAMP NOT NULL, artwork text, buyname text, buylink text diff --git a/views/admin/login.html b/views/admin/login.html deleted file mode 100644 index af35e2e..0000000 --- a/views/admin/login.html +++ /dev/null @@ -1,20 +0,0 @@ -{{define "head"}} -admin - ari melody 💫 - - - -{{end}} - -{{define "content"}} -
- -

Log in with Discord.

- -
- - -{{end}} diff --git a/views/music-gateway.html b/views/music-gateway.html index 821a552..92e3b21 100644 --- a/views/music-gateway.html +++ b/views/music-gateway.html @@ -88,8 +88,8 @@
    {{range .Credits}} {{$Artist := .Artist}} - {{if $Artist.Website}} -
  • {{$Artist.Name}}: {{.Role}}
  • + {{if $Artist.GetWebsite}} +
  • {{$Artist.Name}}: {{.Role}}
  • {{else}}
  • {{$Artist.Name}}: {{.Role}}
  • {{end}}