release edit page! + a lot of other stuff oml
Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
parent
f276ef1ff2
commit
10f19d46db
|
@ -15,12 +15,18 @@ import (
|
||||||
musicModel "arimelody.me/arimelody.me/music/model"
|
musicModel "arimelody.me/arimelody.me/music/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type loginData struct {
|
||||||
|
DiscordURI string
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
func Handler() http.Handler {
|
func Handler() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/login", LoginHandler())
|
mux.Handle("/login", LoginHandler())
|
||||||
mux.Handle("/logout", MustAuthorise(LogoutHandler()))
|
mux.Handle("/logout", MustAuthorise(LogoutHandler()))
|
||||||
mux.Handle("/static/", http.StripPrefix("/static", staticHandler()))
|
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) {
|
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
@ -46,9 +52,9 @@ func Handler() http.Handler {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
var tracks = []Track{}
|
var tracks = []Track{}
|
||||||
for _, track := range global.Tracks {
|
for _, track := range global.Tracks {
|
||||||
|
if track.Release != nil { continue }
|
||||||
tracks = append(tracks, Track{
|
tracks = append(tracks, Track{
|
||||||
Track: track,
|
Track: track,
|
||||||
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
||||||
|
@ -79,8 +85,9 @@ func MustAuthorise(next http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSession(r *http.Request) *Session {
|
func GetSession(r *http.Request) *Session {
|
||||||
// TODO: remove later- this bypasses auth!
|
if os.Getenv("ARIMELODY_ADMIN_BYPASS") == "true" {
|
||||||
return &Session{}
|
return &Session{}
|
||||||
|
}
|
||||||
|
|
||||||
var token = ""
|
var token = ""
|
||||||
// is the session token in context?
|
// is the session token in context?
|
||||||
|
@ -137,7 +144,7 @@ func LoginHandler() http.Handler {
|
||||||
code := r.URL.Query().Get("code")
|
code := r.URL.Query().Get("code")
|
||||||
|
|
||||||
if code == "" {
|
if code == "" {
|
||||||
serveTemplate("login.html", discord.REDIRECT_URI).ServeHTTP(w, r)
|
serveTemplate("login.html", loginData{DiscordURI: discord.REDIRECT_URI}).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,8 +162,9 @@ func LoginHandler() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if discord_user.Id != ADMIN_ID_DISCORD {
|
if discord_user.ID != ADMIN_ID_DISCORD {
|
||||||
// TODO: unauthorized user; revoke the token
|
// 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)
|
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -174,16 +182,17 @@ func LoginHandler() http.Handler {
|
||||||
cookie.Path = "/"
|
cookie.Path = "/"
|
||||||
http.SetCookie(w, &cookie)
|
http.SetCookie(w, &cookie)
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
serveTemplate("login.html", loginData{Token: session.Token}).ServeHTTP(w, r)
|
||||||
w.Header().Add("Content-Type", "text/html")
|
// w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(
|
// w.Header().Add("Content-Type", "text/html")
|
||||||
"<!DOCTYPE html><html><head>"+
|
// w.Write([]byte(
|
||||||
"<meta http-equiv=\"refresh\" content=\"5;url=/admin/\" />"+
|
// "<!DOCTYPE html><html><head>"+
|
||||||
"</head><body>"+
|
// "<meta http-equiv=\"refresh\" content=\"5;url=/admin/\" />"+
|
||||||
"Logged in successfully. "+
|
// "</head><body>"+
|
||||||
"You should be redirected to <a href=\"/admin/\">/admin/</a> in 5 seconds."+
|
// "Logged in successfully. "+
|
||||||
"</body></html>"),
|
// "You should be redirected to <a href=\"/admin/\">/admin/</a> in 5 seconds."+
|
||||||
)
|
// "</body></html>"),
|
||||||
|
// )
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,36 +203,28 @@ func LogoutHandler() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token := r.Context().Value("token").(string)
|
session := GetSession(r)
|
||||||
|
|
||||||
if token == "" {
|
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove this session from the list
|
// remove this session from the list
|
||||||
sessions = func (token string) []*Session {
|
sessions = func (token string) []*Session {
|
||||||
new_sessions := []*Session{}
|
new_sessions := []*Session{}
|
||||||
for _, session := range sessions {
|
for _, session := range sessions {
|
||||||
new_sessions = append(new_sessions, session)
|
if session.Token != token {
|
||||||
|
new_sessions = append(new_sessions, session)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new_sessions
|
return new_sessions
|
||||||
}(token)
|
}(session.Token)
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
serveTemplate("logout.html", nil).ServeHTTP(w, r)
|
||||||
w.Write([]byte(
|
|
||||||
"<meta http-equiv=\"refresh\" content=\"5;url=/\" />"+
|
|
||||||
"Logged out successfully. "+
|
|
||||||
"You should be redirected to <a href=\"/\">/</a> in 5 seconds."),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveTemplate(page string, data any) http.Handler {
|
func serveTemplate(page string, data any) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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")
|
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)
|
info, err := os.Stat(fp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
58
admin/releasehttp.go
Normal file
58
admin/releasehttp.go
Normal file
|
@ -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", "<br>", -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ header {
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: left;
|
||||||
|
|
||||||
background: #f8f8f8;
|
background: #f8f8f8;
|
||||||
border-radius: .5em;
|
border-radius: .5em;
|
||||||
|
@ -29,13 +29,23 @@ header {
|
||||||
}
|
}
|
||||||
header .icon {
|
header .icon {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
}
|
||||||
|
header .title {
|
||||||
header a {
|
|
||||||
height: 100%;
|
|
||||||
width: auto;
|
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;
|
margin: 0px;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
|
@ -47,9 +57,12 @@ header a {
|
||||||
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
header a:hover {
|
header a:hover {
|
||||||
background: #00000010;
|
background: #00000010;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
header #logout {
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
@ -67,6 +80,10 @@ a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
.card h2 {
|
.card h2 {
|
||||||
margin: 0 0 .5em 0;
|
margin: 0 0 .5em 0;
|
||||||
}
|
}
|
||||||
|
@ -77,184 +94,18 @@ a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.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;
|
margin-bottom: 1em;
|
||||||
padding: 1em;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
gap: 1em;
|
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;
|
flex-direction: row;
|
||||||
align-items: center;
|
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;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-album {
|
.card-title h1,
|
||||||
margin-left: auto;
|
.card-title h2,
|
||||||
font-style: italic;
|
.card-title h3 {
|
||||||
font-size: .75em;
|
margin: 0;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 520px) {
|
@media screen and (max-width: 520px) {
|
||||||
|
|
96
admin/static/edit-release.js
Normal file
96
admin/static/edit-release.js
Normal file
|
@ -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();
|
||||||
|
})
|
183
admin/static/index.css
Normal file
183
admin/static/index.css
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
|
219
admin/static/release.css
Normal file
219
admin/static/release.css
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
|
143
admin/views/edit-release.html
Normal file
143
admin/views/edit-release.html
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
{{define "head"}}
|
||||||
|
<title>editing {{.Title}} - ari melody 💫</title>
|
||||||
|
<link rel="shortcut icon" href="{{.GetArtwork}}" type="image/x-icon">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/admin/static/release.css">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<div id="release" data-id="{{.ID}}">
|
||||||
|
<div class="release-artwork">
|
||||||
|
<img src="{{.Artwork}}" alt="" width="256" loading="lazy" id="artwork">
|
||||||
|
</div>
|
||||||
|
<div class="release-info">
|
||||||
|
<h1 class="release-title">
|
||||||
|
<!-- <input type="text" name="Title" value="{{.Title}}"> -->
|
||||||
|
<span id="title" editable="true">{{.Title}}</span>
|
||||||
|
<small>{{.GetReleaseYear}}</small>
|
||||||
|
</h1>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Artists</td>
|
||||||
|
<td>{{.PrintArtists true true}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Type</td>
|
||||||
|
<td>
|
||||||
|
{{$t := .ReleaseType}}
|
||||||
|
<select name="Type" id="type">
|
||||||
|
<option value="single" {{if eq $t "single"}}selected{{end}}>
|
||||||
|
Single
|
||||||
|
</option>
|
||||||
|
<option value="album" {{if eq $t "album"}}selected{{end}}>
|
||||||
|
Album
|
||||||
|
</option>
|
||||||
|
<option value="ep" {{if eq $t "ep"}}selected{{end}}>
|
||||||
|
EP
|
||||||
|
</option>
|
||||||
|
<option value="compilation" {{if eq $t "compilation"}}selected{{end}}>
|
||||||
|
Compilation
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Description</td>
|
||||||
|
<td>
|
||||||
|
<textarea
|
||||||
|
name="Description"
|
||||||
|
value="{{.Description}}"
|
||||||
|
placeholder="No description provided."
|
||||||
|
rows="3"
|
||||||
|
id="description"
|
||||||
|
>{{.Description}}</textarea>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Release Date</td>
|
||||||
|
<td>
|
||||||
|
<input type="datetime-local" name="Release Date" id="release-date" value="{{.TextReleaseDate}}">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Buy Name</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="Buy Name" id="buyname" value="{{.Buyname}}">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Buy Link</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="Buy Link" id="buylink" value="{{.Buylink}}">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Visible</td>
|
||||||
|
<td>
|
||||||
|
<select name="Visibility" id="visibility">
|
||||||
|
<option value="true" {{if .Visible}}selected{{end}}>True</option>
|
||||||
|
<option value="false" {{if not .Visible}}selected{{end}}>False</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="release-actions">
|
||||||
|
<a href="/music/{{.ID}}" class="button">Gateway</a>
|
||||||
|
<button type="submit" class="save" id="save" disabled>Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>Credits ({{len .Credits}})</h2>
|
||||||
|
<button id="update-credits" class="edit">Edit</button>
|
||||||
|
</div>
|
||||||
|
<div class="card credits">
|
||||||
|
{{range $Credit := .Credits}}
|
||||||
|
<div class="credit">
|
||||||
|
<img src="{{$Credit.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
||||||
|
<div class="credit-info">
|
||||||
|
<p class="artist-name"><a href="/admin/artists/{{$Credit.Artist.ID}}">{{$Credit.Artist.Name}}</a></p>
|
||||||
|
<p class="artist-role">
|
||||||
|
{{$Credit.Role}}
|
||||||
|
{{if $Credit.Primary}}
|
||||||
|
<small>(Primary)</small>
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if not .Credits}}
|
||||||
|
<p>There are no credits.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>Tracklist ({{len .Tracks}})</h2>
|
||||||
|
<button id="update-tracks" class="edit">Edit</button>
|
||||||
|
</div>
|
||||||
|
<div class="card tracks">
|
||||||
|
{{range $Track := .Tracks}}
|
||||||
|
<div class="track" data-id="{{$Track.ID}}">
|
||||||
|
<h2 class="track-title">{{$Track.Number}}. {{$Track.Title}}</h2>
|
||||||
|
<p class="track-id">{{$Track.ID}}</p>
|
||||||
|
{{if $Track.Description}}
|
||||||
|
<p class="track-description">{{$Track.Description}}</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="track-description empty">No description provided.</p>
|
||||||
|
{{end}}
|
||||||
|
{{if $Track.Lyrics}}
|
||||||
|
<p class="track-lyrics">{{$Track.Lyrics}}</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="track-lyrics empty">There are no lyrics.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="/admin/static/edit-release.js" defer></script>
|
||||||
|
{{end}}
|
|
@ -1,16 +1,10 @@
|
||||||
{{define "head"}}
|
{{define "head"}}
|
||||||
<title>admin - ari melody 💫</title>
|
<title>admin - ari melody 💫</title>
|
||||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="/admin/static/index.css">
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<header>
|
|
||||||
<img src="/img/favicon.png" alt="" class="icon">
|
|
||||||
<a href="/admin">home</a>
|
|
||||||
<a href="/admin/releases">releases</a>
|
|
||||||
<a href="/admin/artists">artists</a>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
|
@ -24,11 +18,18 @@
|
||||||
<img src="{{$Release.Artwork}}" alt="" width="128" loading="lazy">
|
<img src="{{$Release.Artwork}}" alt="" width="128" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<div class="release-info">
|
<div class="release-info">
|
||||||
<h3 class="release-title">{{$Release.Title}} <small>{{$Release.GetReleaseYear}}</small></h3>
|
<h3 class="release-title">
|
||||||
|
{{$Release.Title}}
|
||||||
|
<small>
|
||||||
|
{{$Release.GetReleaseYear}}
|
||||||
|
{{if not $Release.Visible}}(hidden){{end}}
|
||||||
|
</small>
|
||||||
|
</h3>
|
||||||
<p class="release-artists">{{$Release.PrintArtists true true}}</p>
|
<p class="release-artists">{{$Release.PrintArtists true true}}</p>
|
||||||
<p class="release-type-single">{{$Release.ReleaseType}} ({{len $Release.Tracks}} tracks)</p>
|
<p class="release-type-single">{{$Release.ReleaseType}}
|
||||||
|
({{len $Release.Tracks}} track{{if not (eq (len $Release.Tracks) 1)}}s{{end}})</p>
|
||||||
<div class="release-actions">
|
<div class="release-actions">
|
||||||
<a href="/admin/releases/{{$Release.ID}}">Edit</a>
|
<a href="/admin/release/{{$Release.ID}}">Edit</a>
|
||||||
<a href="/music/{{$Release.ID}}" target="_blank">Gateway</a>
|
<a href="/music/{{$Release.ID}}" target="_blank">Gateway</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,6 +61,8 @@
|
||||||
<a href="/admin/createtrack" class="create-btn">Create New</a>
|
<a href="/admin/createtrack" class="create-btn">Create New</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card tracks">
|
<div class="card tracks">
|
||||||
|
<p><em>"Orphaned" tracks that have not yet been bound to a release.</em></p>
|
||||||
|
<br>
|
||||||
{{range $Track := .Tracks}}
|
{{range $Track := .Tracks}}
|
||||||
<div class="track">
|
<div class="track">
|
||||||
<h2 class="track-title">
|
<h2 class="track-title">
|
||||||
|
@ -67,9 +70,10 @@
|
||||||
{{if $Track.Release}}
|
{{if $Track.Release}}
|
||||||
<small class="track-album">{{$Track.Release.Title}}</small>
|
<small class="track-album">{{$Track.Release.Title}}</small>
|
||||||
{{else}}
|
{{else}}
|
||||||
<small class="track-album empty">(no album)</small>
|
<small class="track-album empty">(no release)</small>
|
||||||
{{end}}
|
{{end}}
|
||||||
</h2>
|
</h2>
|
||||||
|
<p class="track-id">{{$Track.ID}}</p>
|
||||||
{{if $Track.Description}}
|
{{if $Track.Description}}
|
||||||
<p class="track-description">{{$Track.Description}}</p>
|
<p class="track-description">{{$Track.Description}}</p>
|
||||||
{{else}}
|
{{else}}
|
|
@ -13,6 +13,13 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<header>
|
||||||
|
<img src="/img/favicon.png" alt="" class="icon">
|
||||||
|
<a href="/">arimelody.me</a>
|
||||||
|
<a href="/admin">home</a>
|
||||||
|
<a href="/admin/logout" id="logout">log out</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
{{block "content" .}}
|
{{block "content" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
35
admin/views/login.html
Normal file
35
admin/views/login.html
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{{define "head"}}
|
||||||
|
<title>login - ari melody 💫</title>
|
||||||
|
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
p a {
|
||||||
|
color: #2a67c8;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.discord {
|
||||||
|
color: #5865F2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<main>
|
||||||
|
|
||||||
|
{{if .Token}}
|
||||||
|
<meta http-equiv="refresh" content="5;url=/admin/" />
|
||||||
|
<meta name="token" content="{{.Token}}" />
|
||||||
|
<p>
|
||||||
|
Logged in successfully.
|
||||||
|
You should be redirected to <a href="/admin">/admin</a> in 5 seconds.
|
||||||
|
<script>
|
||||||
|
const token = document.querySelector("meta[name=token]").content;
|
||||||
|
localStorage.setItem("arime-token", btoa(token));
|
||||||
|
</script>
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p>Log in with <a href="{{.DiscordURI}}" class="discord">Discord</a>.</p>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
</main>
|
||||||
|
{{end}}
|
25
admin/views/logout.html
Normal file
25
admin/views/logout.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{{define "head"}}
|
||||||
|
<title>admin - ari melody 💫</title>
|
||||||
|
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
p a {
|
||||||
|
color: #2a67c8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<meta http-equiv="refresh" content="5;url=/" />
|
||||||
|
<p>
|
||||||
|
Logged out successfully.
|
||||||
|
You should be redirected to <a href="/">/</a> in 5 seconds.
|
||||||
|
<script>
|
||||||
|
localStorage.removeItem("arime-token");
|
||||||
|
</script>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
{{end}}
|
|
@ -10,6 +10,13 @@ import (
|
||||||
controller "arimelody.me/arimelody.me/music/controller"
|
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 {
|
func ServeAllArtists() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
@ -78,7 +85,7 @@ func CreateArtist() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var data model.Artist
|
var data artistJSON
|
||||||
err := json.NewDecoder(r.Body).Decode(&data)
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to create artist: %s\n", err)
|
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)
|
http.Error(w, "Artist ID cannot be blank\n", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if data.Name == "" {
|
if data.Name == nil || *data.Name == "" {
|
||||||
http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest)
|
http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -102,9 +109,9 @@ func CreateArtist() http.Handler {
|
||||||
|
|
||||||
var artist = model.Artist{
|
var artist = model.Artist{
|
||||||
ID: data.ID,
|
ID: data.ID,
|
||||||
Name: data.Name,
|
Name: *data.Name,
|
||||||
Website: data.Website,
|
Website: *data.Website,
|
||||||
Avatar: data.Avatar,
|
Avatar: *data.Avatar,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = controller.CreateArtistDB(global.DB, &artist)
|
err = controller.CreateArtistDB(global.DB, &artist)
|
||||||
|
@ -138,7 +145,7 @@ func UpdateArtist() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var data model.Artist
|
var data artistJSON
|
||||||
err := json.NewDecoder(r.Body).Decode(&data)
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to update artist: %s\n", err)
|
fmt.Printf("Failed to update artist: %s\n", err)
|
||||||
|
@ -153,24 +160,24 @@ func UpdateArtist() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.ID == "" { data.ID = artist.ID }
|
var update = *artist
|
||||||
|
|
||||||
if data.Name == "" {
|
if data.ID != "" { update.ID = data.ID }
|
||||||
http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest)
|
if data.Name != nil { update.Name = *data.Name }
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
fmt.Printf("Failed to update artist %s: %s\n", artist.ID, err)
|
fmt.Printf("Failed to update artist %s: %s\n", artist.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
artist.ID = data.ID
|
artist.ID = update.ID
|
||||||
artist.Name = data.Name
|
artist.Name = update.Name
|
||||||
artist.Website = data.Website
|
artist.Website = update.Website
|
||||||
artist.Avatar = data.Avatar
|
artist.Avatar = update.Avatar
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
err = json.NewEncoder(w).Encode(artist)
|
err = json.NewEncoder(w).Encode(artist)
|
||||||
|
|
108
api/release.go
108
api/release.go
|
@ -15,14 +15,14 @@ import (
|
||||||
|
|
||||||
type releaseBodyJSON struct {
|
type releaseBodyJSON struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Visible bool `json:"visible"`
|
Visible *bool `json:"visible"`
|
||||||
Title string `json:"title"`
|
Title *string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description *string `json:"description"`
|
||||||
ReleaseType model.ReleaseType `json:"type"`
|
ReleaseType *model.ReleaseType `json:"type"`
|
||||||
ReleaseDate time.Time `json:"releaseDate"`
|
ReleaseDate *string `json:"releaseDate"`
|
||||||
Artwork string `json:"artwork"`
|
Artwork *string `json:"artwork"`
|
||||||
Buyname string `json:"buyname"`
|
Buyname *string `json:"buyname"`
|
||||||
Buylink string `json:"buylink"`
|
Buylink *string `json:"buylink"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServeCatalog() http.Handler {
|
func ServeCatalog() http.Handler {
|
||||||
|
@ -36,7 +36,7 @@ func ServeCatalog() http.Handler {
|
||||||
Artwork string `json:"artwork"`
|
Artwork string `json:"artwork"`
|
||||||
Buyname string `json:"buyname"`
|
Buyname string `json:"buyname"`
|
||||||
Buylink string `json:"buylink"`
|
Buylink string `json:"buylink"`
|
||||||
Links []*model.Link `json:"links"`
|
Links []*model.Link `json:"links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
catalog := []CatalogItem{}
|
catalog := []CatalogItem{}
|
||||||
|
@ -85,26 +85,40 @@ func CreateRelease() http.Handler {
|
||||||
http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest)
|
http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if data.Title == "" {
|
if *data.Title == "" {
|
||||||
http.Error(w, "Release title cannot be empty\n", http.StatusBadRequest)
|
http.Error(w, "Release title cannot be empty\n", http.StatusBadRequest)
|
||||||
return
|
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 {
|
if global.GetRelease(data.ID) != nil {
|
||||||
http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest)
|
||||||
return
|
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{
|
var release = model.Release{
|
||||||
ID: data.ID,
|
ID: data.ID,
|
||||||
Visible: data.Visible,
|
Visible: *data.Visible,
|
||||||
Title: data.Title,
|
Title: *data.Title,
|
||||||
Description: data.Description,
|
Description: *data.Description,
|
||||||
ReleaseType: data.ReleaseType,
|
ReleaseType: *data.ReleaseType,
|
||||||
ReleaseDate: data.ReleaseDate,
|
ReleaseDate: releaseDate,
|
||||||
Artwork: data.Artwork,
|
Artwork: *data.Artwork,
|
||||||
Buyname: data.Buyname,
|
Buyname: *data.Buyname,
|
||||||
Buylink: data.Buylink,
|
Buylink: *data.Buylink,
|
||||||
Links: []*model.Link{},
|
Links: []*model.Link{},
|
||||||
Credits: []*model.Credit{},
|
Credits: []*model.Credit{},
|
||||||
Tracks: []*model.Track{},
|
Tracks: []*model.Track{},
|
||||||
|
@ -153,41 +167,46 @@ func UpdateRelease() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.ID == "" { data.ID = release.ID }
|
var update = *release
|
||||||
|
if data.ID != "" { update.ID = data.ID }
|
||||||
if data.Title == "" {
|
if data.Visible != nil { update.Visible = *data.Visible }
|
||||||
http.Error(w, "Release title cannot be blank\n", http.StatusBadRequest)
|
if data.Title != nil { update.Title = *data.Title }
|
||||||
return
|
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
|
||||||
}
|
}
|
||||||
|
if data.Artwork != nil { update.Artwork = *data.Artwork }
|
||||||
var new_release = model.Release{
|
if data.Buyname != nil {
|
||||||
ID: data.ID,
|
if *data.Buyname == "" {
|
||||||
Visible: data.Visible,
|
http.Error(w, "Release buy name cannot be empty", http.StatusBadRequest)
|
||||||
Title: data.Title,
|
return
|
||||||
Description: data.Description,
|
}
|
||||||
ReleaseType: data.ReleaseType,
|
update.Buyname = *data.Buyname
|
||||||
ReleaseDate: data.ReleaseDate,
|
|
||||||
Artwork: data.Artwork,
|
|
||||||
Buyname: data.Buyname,
|
|
||||||
Buylink: data.Buylink,
|
|
||||||
}
|
}
|
||||||
|
if data.Buylink != nil { update.Buylink = *data.Buylink }
|
||||||
|
|
||||||
err = controller.UpdateReleaseDB(global.DB, release)
|
err = controller.UpdateReleaseDB(global.DB, &update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to update release %s: %s\n", release.ID, err)
|
fmt.Printf("Failed to update release %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
release.ID = new_release.ID
|
release.ID = update.ID
|
||||||
release.Visible = new_release.Visible
|
release.Visible = update.Visible
|
||||||
release.Title = new_release.Title
|
release.Title = update.Title
|
||||||
release.Description = new_release.Description
|
release.Description = update.Description
|
||||||
release.ReleaseType = new_release.ReleaseType
|
release.ReleaseType = update.ReleaseType
|
||||||
release.ReleaseDate = new_release.ReleaseDate
|
release.ReleaseDate = update.ReleaseDate
|
||||||
release.Artwork = new_release.Artwork
|
release.Artwork = update.Artwork
|
||||||
release.Buyname = new_release.Buyname
|
release.Buyname = update.Buyname
|
||||||
release.Buylink = new_release.Buylink
|
release.Buylink = update.Buylink
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
err = json.NewEncoder(w).Encode(release)
|
err = json.NewEncoder(w).Encode(release)
|
||||||
|
@ -195,6 +214,7 @@ func UpdateRelease() http.Handler {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(segments) == 2 {
|
if len(segments) == 2 {
|
||||||
|
|
|
@ -122,10 +122,7 @@ func UpdateTrack() http.Handler {
|
||||||
|
|
||||||
data.ID = trackID
|
data.ID = trackID
|
||||||
|
|
||||||
if data.Title == "" {
|
if data.Title == "" { data.Title = track.Title }
|
||||||
http.Error(w, "Track title cannot be blank\n", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = controller.UpdateTrackDB(global.DB, &data)
|
err = controller.UpdateTrackDB(global.DB, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -57,7 +57,7 @@ type (
|
||||||
|
|
||||||
AuthInfoResponse struct {
|
AuthInfoResponse struct {
|
||||||
Application struct {
|
Application struct {
|
||||||
Id string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
@ -72,7 +72,7 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscordUser struct {
|
DiscordUser struct {
|
||||||
Id string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Discriminator string `json:"discriminator"`
|
Discriminator string `json:"discriminator"`
|
||||||
|
|
|
@ -76,7 +76,7 @@ func CreateReleaseDB(db *sqlx.DB, release *model.Release) error {
|
||||||
release.Title,
|
release.Title,
|
||||||
release.Description,
|
release.Description,
|
||||||
release.ReleaseType,
|
release.ReleaseType,
|
||||||
release.ReleaseDate.Format("2-Jan-2006"),
|
release.ReleaseDate.Format("2006-01-02 15:04:05"),
|
||||||
release.Artwork,
|
release.Artwork,
|
||||||
release.Buyname,
|
release.Buyname,
|
||||||
release.Buylink,
|
release.Buylink,
|
||||||
|
@ -91,15 +91,14 @@ func CreateReleaseDB(db *sqlx.DB, release *model.Release) error {
|
||||||
func UpdateReleaseDB(db *sqlx.DB, release *model.Release) error {
|
func UpdateReleaseDB(db *sqlx.DB, release *model.Release) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"UPDATE musicrelease SET "+
|
"UPDATE musicrelease SET "+
|
||||||
"visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9) "+
|
"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) "+
|
|
||||||
"WHERE id=$1",
|
"WHERE id=$1",
|
||||||
release.ID,
|
release.ID,
|
||||||
release.Visible,
|
release.Visible,
|
||||||
release.Title,
|
release.Title,
|
||||||
release.Description,
|
release.Description,
|
||||||
release.ReleaseType,
|
release.ReleaseType,
|
||||||
release.ReleaseDate.Format("2-Jan-2006"),
|
release.ReleaseDate.Format("2006-01-02 15:04:05"),
|
||||||
release.Artwork,
|
release.Artwork,
|
||||||
release.Buyname,
|
release.Buyname,
|
||||||
release.Buylink,
|
release.Buylink,
|
||||||
|
|
|
@ -2,13 +2,17 @@ package model
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Artist struct {
|
Artist struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Website string `json:"website"`
|
Website string `json:"website"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (artist Artist) GetWebsite() string {
|
||||||
|
return artist.Website
|
||||||
|
}
|
||||||
|
|
||||||
func (artist Artist) GetAvatar() string {
|
func (artist Artist) GetAvatar() string {
|
||||||
if artist.Avatar == "" {
|
if artist.Avatar == "" {
|
||||||
return "/img/default-avatar.png"
|
return "/img/default-avatar.png"
|
||||||
|
|
|
@ -8,18 +8,18 @@ import (
|
||||||
type (
|
type (
|
||||||
ReleaseType string
|
ReleaseType string
|
||||||
Release struct {
|
Release struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Visible bool `json:"visible"`
|
Visible bool `json:"visible"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
ReleaseType ReleaseType `json:"type" db:"type"`
|
ReleaseType ReleaseType `json:"type" db:"type"`
|
||||||
ReleaseDate time.Time `json:"releaseDate" db:"release_date"`
|
ReleaseDate time.Time `json:"releaseDate" db:"release_date"`
|
||||||
Artwork string `json:"artwork"`
|
Artwork string `json:"artwork"`
|
||||||
Buyname string `json:"buyname"`
|
Buyname string `json:"buyname"`
|
||||||
Buylink string `json:"buylink"`
|
Buylink string `json:"buylink"`
|
||||||
Links []*Link `json:"links"`
|
Links []*Link `json:"links"`
|
||||||
Credits []*Credit `json:"credits"`
|
Credits []*Credit `json:"credits"`
|
||||||
Tracks []*Track `json:"tracks"`
|
Tracks []*Track `json:"tracks"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,11 +32,8 @@ const (
|
||||||
|
|
||||||
// GETTERS
|
// GETTERS
|
||||||
|
|
||||||
func (release Release) GetArtwork() string {
|
func (release Release) TextReleaseDate() string {
|
||||||
if release.Artwork == "" {
|
return release.ReleaseDate.Format("2006-01-02T15:04")
|
||||||
return "/img/default-cover-art.png"
|
|
||||||
}
|
|
||||||
return release.Artwork
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (release Release) PrintReleaseDate() string {
|
func (release Release) PrintReleaseDate() string {
|
||||||
|
@ -47,6 +44,13 @@ func (release Release) GetReleaseYear() int {
|
||||||
return release.ReleaseDate.Year()
|
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 {
|
func (release Release) IsSingle() bool {
|
||||||
return len(release.Tracks) == 1;
|
return len(release.Tracks) == 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,19 @@ import (
|
||||||
"arimelody.me/arimelody.me/music/model"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
gatewayTrack struct {
|
||||||
|
*model.Track
|
||||||
|
Lyrics template.HTML
|
||||||
|
Number int
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayRelease struct {
|
||||||
|
*model.Release
|
||||||
|
Tracks []gatewayTrack
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// HTTP HANDLERS
|
// HTTP HANDLERS
|
||||||
|
|
||||||
func ServeRelease() http.Handler {
|
func ServeRelease() http.Handler {
|
||||||
|
@ -51,19 +64,6 @@ func ServeGateway() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
|
||||||
GatewayTrack struct {
|
|
||||||
*model.Track
|
|
||||||
Lyrics template.HTML
|
|
||||||
Number int
|
|
||||||
}
|
|
||||||
|
|
||||||
GatewayRelease struct {
|
|
||||||
*model.Release
|
|
||||||
Tracks []GatewayTrack
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
id := r.URL.Path[1:]
|
id := r.URL.Path[1:]
|
||||||
release := global.GetRelease(id)
|
release := global.GetRelease(id)
|
||||||
if release == nil {
|
if release == nil {
|
||||||
|
@ -71,9 +71,9 @@ func ServeGateway() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks := []GatewayTrack{}
|
tracks := []gatewayTrack{}
|
||||||
for i, track := range release.Tracks {
|
for i, track := range release.Tracks {
|
||||||
tracks = append([]GatewayTrack{GatewayTrack{
|
tracks = append([]gatewayTrack{{
|
||||||
Track: track,
|
Track: track,
|
||||||
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
||||||
Number: len(release.Tracks) - i,
|
Number: len(release.Tracks) - i,
|
||||||
|
@ -87,9 +87,9 @@ func ServeGateway() http.Handler {
|
||||||
return
|
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 {
|
if lrw.Code != http.StatusOK {
|
||||||
fmt.Printf("Error rendering music gateway for %s\n", id)
|
fmt.Printf("Error rendering music gateway for %s\n", id)
|
||||||
|
|
1
public/script/silver.min.js
vendored
Normal file
1
public/script/silver.min.js
vendored
Normal file
|
@ -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))}}
|
|
@ -18,7 +18,7 @@ CREATE TABLE public.musicrelease (
|
||||||
title text NOT NULL,
|
title text NOT NULL,
|
||||||
description text,
|
description text,
|
||||||
type text,
|
type text,
|
||||||
release_date DATE NOT NULL,
|
release_date TIMESTAMP NOT NULL,
|
||||||
artwork text,
|
artwork text,
|
||||||
buyname text,
|
buyname text,
|
||||||
buylink text
|
buylink text
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
{{define "head"}}
|
|
||||||
<title>admin - ari melody 💫</title>
|
|
||||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.discord {
|
|
||||||
color: #5865F2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{define "content"}}
|
|
||||||
<main>
|
|
||||||
|
|
||||||
<p>Log in with <a href="{{.}}" class="discord">Discord</a>.</p>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script type="module" src="/admin/static/admin.js" defer></script>
|
|
||||||
{{end}}
|
|
|
@ -88,8 +88,8 @@
|
||||||
<ul>
|
<ul>
|
||||||
{{range .Credits}}
|
{{range .Credits}}
|
||||||
{{$Artist := .Artist}}
|
{{$Artist := .Artist}}
|
||||||
{{if $Artist.Website}}
|
{{if $Artist.GetWebsite}}
|
||||||
<li><strong><a href="{{$Artist.Website}}">{{$Artist.Name}}</a></strong>: {{.Role}}</li>
|
<li><strong><a href="{{$Artist.GetWebsite}}">{{$Artist.Name}}</a></strong>: {{.Role}}</li>
|
||||||
{{else}}
|
{{else}}
|
||||||
<li><strong>{{$Artist.Name}}</strong>: {{.Role}}</li>
|
<li><strong>{{$Artist.Name}}</strong>: {{.Role}}</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in a new issue