Compare commits
No commits in common. "feature/dualstack" and "main" have entirely different histories.
feature/du
...
main
30 changed files with 273 additions and 246 deletions
|
|
@ -103,7 +103,9 @@ func serveRelease(app *model.AppState, releaseID string, action string) http.Han
|
||||||
Release *model.Release
|
Release *model.Release
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, track := range release.Tracks { track.Number = i + 1 }
|
for i, track := range release.Tracks {
|
||||||
|
track.Number = i + 1
|
||||||
|
}
|
||||||
|
|
||||||
err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{
|
err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{
|
||||||
adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
|
adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
|
||||||
|
|
@ -155,8 +157,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler {
|
||||||
|
|
||||||
func serveNewCredit(app *model.AppState) http.Handler {
|
func serveNewCredit(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
split := strings.Split(r.URL.Path, "/")
|
artistID := strings.Split(r.URL.Path, "/")[3]
|
||||||
artistID := split[len(split) - 1]
|
|
||||||
artist, err := controller.GetArtist(app.DB, artistID)
|
artist, err := controller.GetArtist(app.DB, artistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err)
|
fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err)
|
||||||
|
|
@ -194,8 +195,6 @@ func serveEditTracks(release *model.Release) http.Handler {
|
||||||
|
|
||||||
type editTracksData struct { Release *model.Release }
|
type editTracksData struct { Release *model.Release }
|
||||||
|
|
||||||
for i, track := range release.Tracks { track.Number = i + 1 }
|
|
||||||
|
|
||||||
err := templates.EditTracksTemplate.Execute(w, editTracksData{ Release: release })
|
err := templates.EditTracksTemplate.Execute(w, editTracksData{ Release: release })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to serve edit tracks component for %s: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to serve edit tracks component for %s: %s\n", release.ID, err)
|
||||||
|
|
@ -232,8 +231,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler {
|
||||||
|
|
||||||
func serveNewTrack(app *model.AppState) http.Handler {
|
func serveNewTrack(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
split := strings.Split(r.URL.Path, "/")
|
trackID := strings.Split(r.URL.Path, "/")[3]
|
||||||
trackID := split[len(split) - 1]
|
|
||||||
track, err := controller.GetTrack(app.DB, trackID)
|
track, err := controller.GetTrack(app.DB, trackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to fetch track %s: %s\n", trackID, err)
|
fmt.Printf("WARN: Failed to fetch track %s: %s\n", trackID, err)
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ body {
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--fg-0);
|
color: var(--fg-0);
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
@ -252,6 +252,12 @@ a {
|
||||||
transition: color .1s ease-out, background-color .1s ease-out;
|
transition: color .1s ease-out, background-color .1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
img.icon {
|
img.icon {
|
||||||
height: .8em;
|
height: .8em;
|
||||||
transition: filter .1s ease-out;
|
transition: filter .1s ease-out;
|
||||||
|
|
@ -277,7 +283,7 @@ code {
|
||||||
.card {
|
.card {
|
||||||
flex-basis: 40em;
|
flex-basis: 40em;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background-color: var(--bg-1);
|
background: var(--bg-1);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
|
|
||||||
|
|
@ -355,7 +361,7 @@ a.delete:not(.button) {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10em;
|
border-radius: 10em;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
|
@ -374,27 +380,27 @@ button:active, .button:active {
|
||||||
|
|
||||||
.button.new, button.new {
|
.button.new, button.new {
|
||||||
color: var(--col-on-new);
|
color: var(--col-on-new);
|
||||||
background-color: var(--col-new);
|
background: var(--col-new);
|
||||||
}
|
}
|
||||||
.button.save, button.save {
|
.button.save, button.save {
|
||||||
color: var(--col-on-save);
|
color: var(--col-on-save);
|
||||||
background-color: var(--col-save);
|
background: var(--col-save);
|
||||||
}
|
}
|
||||||
.button.delete, button.delete {
|
.button.delete, button.delete {
|
||||||
color: var(--col-on-delete);
|
color: var(--col-on-delete);
|
||||||
background-color: var(--col-delete);
|
background: var(--col-delete);
|
||||||
}
|
}
|
||||||
.button:hover, button:hover {
|
.button:hover, button:hover {
|
||||||
color: var(--bg-3);
|
color: var(--bg-3);
|
||||||
background-color: var(--fg-3);
|
background: var(--fg-3);
|
||||||
}
|
}
|
||||||
.button:active, button:active {
|
.button:active, button:active {
|
||||||
color: var(--bg-2);
|
color: var(--bg-2);
|
||||||
background-color: var(--fg-0);
|
background: var(--fg-0);
|
||||||
}
|
}
|
||||||
.button[disabled], button[disabled] {
|
.button[disabled], button[disabled] {
|
||||||
color: var(--fg-0) !important;
|
color: var(--fg-0) !important;
|
||||||
background-color: var(--bg-3) !important;
|
background: var(--bg-3) !important;
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
|
|
||||||
color: var(--fg-3);
|
color: var(--fg-3);
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.artist:hover {
|
.artist:hover {
|
||||||
background-color: var(--bg-1);
|
background: var(--bg-1);
|
||||||
text-decoration: hover;
|
text-decoration: hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,4 @@ document.addEventListener("readystatechange", () => {
|
||||||
document.querySelectorAll(".artists-group .artist").forEach(el => {
|
document.querySelectorAll(".artists-group .artist").forEach(el => {
|
||||||
hijackClickEvent(el, el.querySelector("a.artist-name"))
|
hijackClickEvent(el, el.querySelector("a.artist-name"))
|
||||||
});
|
});
|
||||||
|
|
||||||
const newArtistBtn = document.getElementById("create-artist");
|
|
||||||
if (newArtistBtn) newArtistBtn.addEventListener("click", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
const id = prompt("Enter an ID for this artist:");
|
|
||||||
if (id == null || id == "") return;
|
|
||||||
|
|
||||||
fetch("/api/v1/artist", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({id})
|
|
||||||
}).then(res => {
|
|
||||||
res.text().then(text => {
|
|
||||||
if (res.ok) {
|
|
||||||
location = "/admin/artists/" + id;
|
|
||||||
} else {
|
|
||||||
alert(text);
|
|
||||||
console.error(text);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).catch(err => {
|
|
||||||
alert("Failed to create artist. Check the console for details.");
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ form#delete-account input {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
color: var(--fg-3);
|
color: var(--fg-3);
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
gap: 1.2em;
|
gap: 1.2em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,11 +50,18 @@ input[type="text"] {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
input[type="text"]:hover {
|
||||||
|
border-color: #80808080;
|
||||||
|
}
|
||||||
|
input[type="text"]:active,
|
||||||
|
input[type="text"]:focus {
|
||||||
|
border-color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
.artist-actions {
|
.artist-actions {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
|
@ -77,7 +84,7 @@ input[type="text"] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -85,7 +92,7 @@ input[type="text"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.credit:hover {
|
.credit:hover {
|
||||||
background-color: var(--bg-1);
|
background: var(--bg-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-artwork {
|
.release-artwork {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ input[type="text"] {
|
||||||
gap: 1.2em;
|
gap: 1.2em;
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
|
|
@ -33,7 +33,7 @@ input[type="text"] {
|
||||||
.release-artwork #remove-artwork {
|
.release-artwork #remove-artwork {
|
||||||
margin-top: .5em;
|
margin-top: .5em;
|
||||||
padding: .3em .6em;
|
padding: .3em .6em;
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-info {
|
.release-info {
|
||||||
|
|
@ -62,13 +62,13 @@ input[type="text"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
#title:hover {
|
#title:hover {
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
border-color: var(--fg-0);
|
border-color: var(--fg-0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#title:active,
|
#title:active,
|
||||||
#title:focus {
|
#title:focus {
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-title small {
|
.release-title small {
|
||||||
|
|
@ -93,7 +93,7 @@ input[type="text"] {
|
||||||
.release-info table tr td:not(:first-child) select:hover,
|
.release-info table tr td:not(:first-child) select:hover,
|
||||||
.release-info table tr td:not(:first-child) input:hover,
|
.release-info table tr td:not(:first-child) input:hover,
|
||||||
.release-info table tr td:not(:first-child) textarea:hover {
|
.release-info table tr td:not(:first-child) textarea:hover {
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.release-info table td select,
|
.release-info table td select,
|
||||||
|
|
@ -127,7 +127,7 @@ input[type="text"] {
|
||||||
.release-actions button,
|
.release-actions button,
|
||||||
.release-actions .button {
|
.release-actions .button {
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
|
|
@ -234,7 +234,7 @@ dialog div.dialog-actions {
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,7 +280,7 @@ dialog div.dialog-actions {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
}
|
}
|
||||||
#editcredits .credit .credit-info .credit-attribute input[type="checkbox"] {
|
#editcredits .credit .credit-info .credit-attribute input[type="checkbox"] {
|
||||||
margin: 0 .3em;
|
margin: 0 .3em;
|
||||||
|
|
@ -299,7 +299,6 @@ dialog div.dialog-actions {
|
||||||
#editcredits .credit .delete {
|
#editcredits .credit .delete {
|
||||||
margin-right: .5em;
|
margin-right: .5em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
#editcredits .credit .delete:hover {
|
#editcredits .credit .delete:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
@ -316,17 +315,14 @@ dialog div.dialog-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--bg-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#addcredit ul li.new-artist:nth-child(even) {
|
#addcredit ul li.new-artist:nth-child(even) {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
background-color: var(--bg-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#addcredit ul li.new-artist:hover {
|
#addcredit ul li.new-artist:hover {
|
||||||
background: #e0e0e0;
|
background: #e0e0e0;
|
||||||
background-color: var(--bg-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#addcredit .new-artist .artist-id {
|
#addcredit .new-artist .artist-id {
|
||||||
|
|
@ -379,8 +375,6 @@ dialog div.dialog-actions {
|
||||||
|
|
||||||
#editlinks tr {
|
#editlinks tr {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: var(--bg-1);
|
|
||||||
transition: background-color .1s ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#editlinks th {
|
#editlinks th {
|
||||||
|
|
@ -391,7 +385,7 @@ dialog div.dialog-actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#editlinks tr:nth-child(odd) {
|
#editlinks tr:nth-child(odd) {
|
||||||
background-color: var(--bg-2);
|
background: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
#editlinks tr th,
|
#editlinks tr th,
|
||||||
|
|
@ -422,11 +416,6 @@ dialog div.dialog-actions {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
#editlinks tr .grabber img {
|
|
||||||
filter: invert();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#editlinks tr .link-name {
|
#editlinks tr .link-name {
|
||||||
width: 8em;
|
width: 8em;
|
||||||
}
|
}
|
||||||
|
|
@ -465,7 +454,6 @@ dialog div.dialog-actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#edittracks .track {
|
#edittracks .track {
|
||||||
background-color: var(--bg-2);
|
|
||||||
transition: transform .2s ease-out, opacity .2s;
|
transition: transform .2s ease-out, opacity .2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -488,7 +476,7 @@ dialog div.dialog-actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#edittracks .track:nth-child(even) {
|
#edittracks .track:nth-child(even) {
|
||||||
background-color: var(--bg-1);
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edittracks .track-number {
|
#edittracks .track-number {
|
||||||
|
|
@ -504,6 +492,7 @@ dialog div.dialog-actions {
|
||||||
#addtrack ul {
|
#addtrack ul {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
background: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
#addtrack ul li.new-track {
|
#addtrack ul li.new-track {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
gap: 1.2em;
|
gap: 1.2em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,13 +45,25 @@
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
outline: none;
|
outline: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-info input[type="text"]:hover,
|
||||||
|
.track-info textarea:hover {
|
||||||
|
border-color: #80808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info input[type="text"]:active,
|
||||||
|
.track-info textarea:active,
|
||||||
|
.track-info input[type="text"]:focus,
|
||||||
|
.track-info textarea:focus {
|
||||||
|
border-color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
.track-actions {
|
.track-actions {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
74
admin/static/index.js
Normal file
74
admin/static/index.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
const newReleaseBtn = document.getElementById("create-release");
|
||||||
|
const newArtistBtn = document.getElementById("create-artist");
|
||||||
|
const newTrackBtn = document.getElementById("create-track");
|
||||||
|
|
||||||
|
newReleaseBtn.addEventListener("click", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const id = prompt("Enter an ID for this release:");
|
||||||
|
if (id == null || id == "") return;
|
||||||
|
|
||||||
|
fetch("/api/v1/music", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({id})
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ok) location = "/admin/releases/" + id;
|
||||||
|
else {
|
||||||
|
res.text().then(err => {
|
||||||
|
alert("Request failed: " + err);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Failed to create release. Check the console for details.");
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
newArtistBtn.addEventListener("click", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const id = prompt("Enter an ID for this artist:");
|
||||||
|
if (id == null || id == "") return;
|
||||||
|
|
||||||
|
fetch("/api/v1/artist", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({id})
|
||||||
|
}).then(res => {
|
||||||
|
res.text().then(text => {
|
||||||
|
if (res.ok) {
|
||||||
|
location = "/admin/artists/" + id;
|
||||||
|
} else {
|
||||||
|
alert("Request failed: " + text);
|
||||||
|
console.error(text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Failed to create artist. Check the console for details.");
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
newTrackBtn.addEventListener("click", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const title = prompt("Enter an title for this track:");
|
||||||
|
if (title == null || title == "") return;
|
||||||
|
|
||||||
|
fetch("/api/v1/track", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({title})
|
||||||
|
}).then(res => {
|
||||||
|
res.text().then(text => {
|
||||||
|
if (res.ok) {
|
||||||
|
location = "/admin/tracks/" + text;
|
||||||
|
} else {
|
||||||
|
alert("Request failed: " + text);
|
||||||
|
console.error(text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Failed to create track. Check the console for details.");
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -8,7 +8,7 @@ form#search-form {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
color: var(--fg-0);
|
color: var(--fg-0);
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ div#search {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
color: var(--fg-1);
|
color: var(--fg-1);
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,8 +100,8 @@ td.log-content {
|
||||||
|
|
||||||
#logs .log.warn {
|
#logs .log.warn {
|
||||||
color: var(--col-on-warn);
|
color: var(--col-on-warn);
|
||||||
background-color: var(--col-warn);
|
background: var(--col-warn);
|
||||||
}
|
}
|
||||||
#logs .log.warn:hover {
|
#logs .log.warn:hover {
|
||||||
background-color: var(--col-warn-hover);
|
background: var(--col-warn-hover);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
|
|
@ -67,14 +67,14 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
|
||||||
transition: color .1s ease-out, background .1s ease-out;
|
transition: color .1s ease-out, background .1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release .release-actions a:hover {
|
.release .release-actions a:hover {
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
color: var(--fg-3);
|
color: var(--fg-3);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
document.addEventListener('readystatechange', () => {
|
|
||||||
const newReleaseBtn = document.getElementById("create-release");
|
|
||||||
if (newReleaseBtn) newReleaseBtn.addEventListener("click", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
const id = prompt("Enter an ID for this release:");
|
|
||||||
if (id == null || id == "") return;
|
|
||||||
|
|
||||||
fetch("/api/v1/music", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({id})
|
|
||||||
}).then(res => {
|
|
||||||
if (res.ok) location = "/admin/releases/" + id;
|
|
||||||
else {
|
|
||||||
res.text().then(err => {
|
|
||||||
alert(err);
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
alert("Failed to create release. Check the console for details.");
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
|
|
@ -44,6 +44,11 @@
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tracks .track-album.empty {
|
||||||
|
color: #ff2020;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#tracks .track-description {
|
#tracks .track-description {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
@ -62,4 +67,61 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
/*
|
||||||
|
justify-content: space-between;
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
.track {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .5em;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--bg-2);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
|
transition: color .1s ease-out, background-color .1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
const newTrackBtn = document.getElementById("create-track");
|
|
||||||
if (newTrackBtn) newTrackBtn.addEventListener("click", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
const title = prompt("Enter an title for this track:");
|
|
||||||
if (title == null || title == "") return;
|
|
||||||
|
|
||||||
fetch("/api/v1/track", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({title})
|
|
||||||
}).then(res => {
|
|
||||||
res.text().then(text => {
|
|
||||||
if (res.ok) {
|
|
||||||
location = "/admin/tracks/" + text;
|
|
||||||
} else {
|
|
||||||
alert(text);
|
|
||||||
console.error(text);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).catch(err => {
|
|
||||||
alert("Failed to create track. Check the console for details.");
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -12,12 +12,12 @@
|
||||||
|
|
||||||
<form action="/api/v1/music/{{.Release.ID}}/tracks">
|
<form action="/api/v1/music/{{.Release.ID}}/tracks">
|
||||||
<ul>
|
<ul>
|
||||||
{{range .Release.Tracks}}
|
{{range $i, $track := .Release.Tracks}}
|
||||||
<li class="track" data-track="{{.ID}}" data-title="{{.Title}}" data-number="{{.Number}}" draggable="true">
|
<li class="track" data-track="{{$track.ID}}" data-title="{{$track.Title}}" data-number="{{$track.Add $i 1}}" draggable="true">
|
||||||
<div>
|
<div>
|
||||||
<p class="track-name">
|
<p class="track-name">
|
||||||
<span class="track-number">{{.Number}}</span>
|
<span class="track-number">{{.Add $i 1}}</span>
|
||||||
{{.Title}}
|
{{$track.Title}}
|
||||||
</p>
|
</p>
|
||||||
<a class="delete">Delete</a>
|
<a class="delete">Delete</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
|
|
||||||
deleteBtn.addEventListener("click", e => {
|
deleteBtn.addEventListener("click", e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!confirm("Are you sure you want to remove " + trackTitle + "?")) return;
|
||||||
trackItem.remove();
|
trackItem.remove();
|
||||||
refreshTrackNumbers();
|
refreshTrackNumbers();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="credits" class="card">
|
<div class="card" id="credits">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>Credits <small>({{len .Release.Credits}} total)</small></h2>
|
<h2>Credits <small>({{len .Release.Credits}} total)</small></h2>
|
||||||
<a class="button edit"
|
<a class="button edit"
|
||||||
|
|
@ -110,7 +110,6 @@
|
||||||
hx-swap="beforeend"
|
hx-swap="beforeend"
|
||||||
>Edit</a>
|
>Edit</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{range .Release.Credits}}
|
{{range .Release.Credits}}
|
||||||
<div class="credit">
|
<div class="credit">
|
||||||
<img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
<img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
||||||
|
|
@ -126,13 +125,13 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not .Release.Credits}}
|
{{if not .Release.Credits}}
|
||||||
<p>This release has no credits.</p>
|
<p>There are no credits.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="links" class="card">
|
<div class="card" id="links">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>Links <small>({{len .Release.Links}} total)</small></h2>
|
<h2>Links ({{len .Release.Links}})</h2>
|
||||||
<a class="button edit"
|
<a class="button edit"
|
||||||
href="/admin/releases/{{.Release.ID}}/editlinks"
|
href="/admin/releases/{{.Release.ID}}/editlinks"
|
||||||
hx-get="/admin/releases/{{.Release.ID}}/editlinks"
|
hx-get="/admin/releases/{{.Release.ID}}/editlinks"
|
||||||
|
|
@ -140,21 +139,16 @@
|
||||||
hx-swap="beforeend"
|
hx-swap="beforeend"
|
||||||
>Edit</a>
|
>Edit</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .Release.Links}}
|
|
||||||
<ul>
|
<ul>
|
||||||
{{range .Release.Links}}
|
{{range .Release.Links}}
|
||||||
<a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img class="icon" src="/img/external-link.svg"/></a>
|
<a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img class="icon" src="/img/external-link.svg"/></a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
|
||||||
<p>This release has no links.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tracks" class="card">
|
<div class="card" id="tracks">
|
||||||
<div class="card-header">
|
<div class="card-header" id="tracks">
|
||||||
<h2>Tracks <small>({{len .Release.Tracks}} total)</small></h2>
|
<h2>Tracklist ({{len .Release.Tracks}})</h2>
|
||||||
<a class="button edit"
|
<a class="button edit"
|
||||||
href="/admin/releases/{{.Release.ID}}/edittracks"
|
href="/admin/releases/{{.Release.ID}}/edittracks"
|
||||||
hx-get="/admin/releases/{{.Release.ID}}/edittracks"
|
hx-get="/admin/releases/{{.Release.ID}}/edittracks"
|
||||||
|
|
@ -162,13 +156,9 @@
|
||||||
hx-swap="beforeend"
|
hx-swap="beforeend"
|
||||||
>Edit</a>
|
>Edit</a>
|
||||||
</div>
|
</div>
|
||||||
|
{{range $i, $track := .Release.Tracks}}
|
||||||
{{range .Release.Tracks}}
|
|
||||||
{{block "track" .}}{{end}}
|
{{block "track" .}}{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not .Release.Tracks}}
|
|
||||||
<p>This release has no tracks.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" id="danger">
|
<div class="card" id="danger">
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script type="module" src="/admin/static/releases.js"></script>
|
|
||||||
<script type="module" src="/admin/static/artists.js"></script>
|
<script type="module" src="/admin/static/artists.js"></script>
|
||||||
<script type="module" src="/admin/static/tracks.js"></script>
|
<script type="module" src="/admin/static/index.js"></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,4 @@
|
||||||
<p>There are no releases.</p>
|
<p>There are no releases.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script type="module" src="/admin/static/releases.js"></script>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,22 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="tracks">
|
<div id="tracks">
|
||||||
{{range .Tracks}}
|
{{range $Track := .Tracks}}
|
||||||
{{block "track" .}}{{end}}
|
<div class="track">
|
||||||
|
<h2 class="track-title">
|
||||||
|
<a href="/admin/tracks/{{$Track.ID}}">{{$Track.Title}}</a>
|
||||||
|
</h2>
|
||||||
|
{{if $Track.Description}}
|
||||||
|
<p class="track-description">{{$Track.GetDescriptionHTML}}</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="track-description empty">No description provided.</p>
|
||||||
|
{{end}}
|
||||||
|
{{if $Track.Lyrics}}
|
||||||
|
<p class="track-lyrics">{{$Track.GetLyricsHTML}}</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="track-lyrics empty">There are no lyrics.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -324,10 +324,6 @@ func UpdateReleaseTracks(app *model.AppState, release *model.Release) http.Handl
|
||||||
|
|
||||||
err = controller.UpdateReleaseTracks(app.DB, release.ID, trackIDs)
|
err = controller.UpdateReleaseTracks(app.DB, release.ID, trackIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
|
||||||
http.Error(w, "Release cannot have duplicate tracks", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
|
|
@ -370,14 +366,14 @@ func UpdateReleaseCredits(app *model.AppState, release *model.Release) http.Hand
|
||||||
err = controller.UpdateReleaseCredits(app.DB, release.ID, credits)
|
err = controller.UpdateReleaseCredits(app.DB, release.ID, credits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
http.Error(w, "Artists may only be credited once", http.StatusBadRequest)
|
http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("WARN: Failed to update credits for %s: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -398,10 +394,6 @@ func UpdateReleaseLinks(app *model.AppState, release *model.Release) http.Handle
|
||||||
|
|
||||||
err = controller.UpdateReleaseLinks(app.DB, release.ID, links)
|
err = controller.UpdateReleaseLinks(app.DB, release.ID, links)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
|
||||||
http.Error(w, "Release cannot have duplicate link names", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,14 @@ import (
|
||||||
// Returns the request's original IP address, resolving the `x-forwarded-for`
|
// Returns the request's original IP address, resolving the `x-forwarded-for`
|
||||||
// header if the request originates from a trusted proxy.
|
// header if the request originates from a trusted proxy.
|
||||||
func ResolveIP(app *model.AppState, r *http.Request) string {
|
func ResolveIP(app *model.AppState, r *http.Request) string {
|
||||||
iSplit := strings.LastIndex(r.RemoteAddr, ":")
|
addr := strings.Split(r.RemoteAddr, ":")[0]
|
||||||
addr := r.RemoteAddr[0:iSplit]
|
|
||||||
if slices.Contains(app.Config.TrustedProxies, addr) {
|
if slices.Contains(app.Config.TrustedProxies, addr) {
|
||||||
forwardedFor := r.Header.Get("x-forwarded-for")
|
forwardedFor := r.Header.Get("x-forwarded-for")
|
||||||
if len(forwardedFor) > 0 {
|
if len(forwardedFor) > 0 {
|
||||||
// discard extra IPs; cloudflare tends to append their nodes
|
// discard extra IPs; cloudflare tends to append their nodes
|
||||||
forwardedFor = strings.Split(forwardedFor, ", ")[0]
|
forwardedFor = strings.Split(forwardedFor, ", ")[0]
|
||||||
addr = forwardedFor
|
return forwardedFor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if addr[0] == '[' && addr[len(addr) - 1] == ']' {
|
|
||||||
addr = addr[1:len(addr) - 1]
|
|
||||||
}
|
|
||||||
return addr
|
return addr
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
main.go
14
main.go
|
|
@ -24,7 +24,6 @@ import (
|
||||||
"arimelody-web/log"
|
"arimelody-web/log"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/view"
|
"arimelody-web/view"
|
||||||
amnet "arimelody-web/net"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
|
|
@ -519,18 +518,7 @@ func main() {
|
||||||
|
|
||||||
// start the web server!
|
// start the web server!
|
||||||
mux := createServeMux(&app)
|
mux := createServeMux(&app)
|
||||||
server := &amnet.DualStackHTTPServer{ Handler: mux }
|
fmt.Printf("Now serving at http://%s:%d\n", app.Config.Host, app.Config.Port)
|
||||||
if err := server.ListenAndServe(app.Config.Host, "::", int(app.Config.Port)); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Printf(
|
|
||||||
"Now listening on port %d (http://%s:%d, http://[%s]:%d)\n",
|
|
||||||
app.Config.Port,
|
|
||||||
app.Config.Host,
|
|
||||||
app.Config.Port,
|
|
||||||
"::",
|
|
||||||
app.Config.Port,
|
|
||||||
)
|
|
||||||
stdLog.Fatal(
|
stdLog.Fatal(
|
||||||
http.ListenAndServe(fmt.Sprintf("%s:%d", app.Config.Host, app.Config.Port),
|
http.ListenAndServe(fmt.Sprintf("%s:%d", app.Config.Host, app.Config.Port),
|
||||||
CheckRequest(&app, HTTPLog(DefaultHeaders(mux))),
|
CheckRequest(&app, HTTPLog(DefaultHeaders(mux))),
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,8 @@ func (track Track) GetDescriptionHTML() template.HTML {
|
||||||
func (track Track) GetLyricsHTML() template.HTML {
|
func (track Track) GetLyricsHTML() template.HTML {
|
||||||
return template.HTML(strings.ReplaceAll(track.Lyrics, "\n", "<br>"))
|
return template.HTML(strings.ReplaceAll(track.Lyrics, "\n", "<br>"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this function is stupid and i hate that i need it
|
||||||
|
func (track Track) Add(a int, b int) int {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,3 +31,13 @@ func Test_Track_LyricsHTML(t *testing.T) {
|
||||||
t.Errorf(`track lyrics incorrectly formatted (want "%s", got "%s")`, want, got)
|
t.Errorf(`track lyrics incorrectly formatted (want "%s", got "%s")`, want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_Track_Add(t *testing.T) {
|
||||||
|
track := Track{}
|
||||||
|
|
||||||
|
want := 4
|
||||||
|
got := track.Add(2, 2)
|
||||||
|
if want != got {
|
||||||
|
t.Errorf(`somehow, we screwed up addition. (want %d, got %d)`, want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DualStackHTTPServer struct {
|
|
||||||
Handler http.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DualStackHTTPServer) ListenAndServe(ipv4 string, ipv6 string, port int) error {
|
|
||||||
v4Listener, err := net.Listen("tcp4", fmt.Sprintf("%s:%d", ipv4, port))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("IPv4 listen: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
v6Listener, err := net.Listen("tcp6", fmt.Sprintf("[%s]:%d", ipv6, port))
|
|
||||||
if err != nil {
|
|
||||||
v4Listener.Close()
|
|
||||||
return fmt.Errorf("IPv6 listen: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := &http.Server{ Handler: s.Handler }
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
errs := make(chan error, 2)
|
|
||||||
|
|
||||||
for _, listener := range []net.Listener{v4Listener, v6Listener} {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(l net.Listener) {
|
|
||||||
defer wg.Done()
|
|
||||||
errs <- srv.Serve(l)
|
|
||||||
}(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <-errs
|
|
||||||
}
|
|
||||||
|
|
@ -153,7 +153,7 @@ header ul li a:hover {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
border-bottom: 1px solid #888;
|
border-bottom: 1px solid #888;
|
||||||
background-color: var(--background);
|
background: var(--background);
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: var(--background);
|
background: var(--background);
|
||||||
color: var(--on-background);
|
color: var(--on-background);
|
||||||
font-family: "Monaspace Argon", monospace;
|
font-family: "Monaspace Argon", monospace;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
|
@ -150,7 +150,7 @@ a#backtotop:hover {
|
||||||
@keyframes list-item-fadein {
|
@keyframes list-item-fadein {
|
||||||
from {
|
from {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background-color: var(--links);
|
background: var(--links);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ header {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
filter: blur(25px) saturate(25%) brightness(0.5);
|
filter: blur(25px) saturate(25%) brightness(0.5);
|
||||||
-webkit-filter: blur(25px) saturate(25%) brightness(0.5);
|
-webkit-filter: blur(25px) saturate(25%) brightness(0.5);;
|
||||||
animation: background-init .5s forwards,background-loop 30s ease-in-out infinite
|
animation: background-init .5s forwards,background-loop 30s ease-in-out infinite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,9 @@
|
||||||
|
|
||||||
{{else if .IsSingle}}
|
{{else if .IsSingle}}
|
||||||
|
|
||||||
{{index .Tracks 0}}
|
{{$Track := index .Tracks 0}}
|
||||||
{{if .Description}}
|
{{if $Track.Description}}
|
||||||
<p id="description">{{.Description}}</p>
|
<p id="description">{{$Track.Description}}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -132,18 +132,18 @@
|
||||||
{{else if .Tracks}}
|
{{else if .Tracks}}
|
||||||
<div id="tracks">
|
<div id="tracks">
|
||||||
<h2>TRACKS</h2>
|
<h2>TRACKS</h2>
|
||||||
{{range .Tracks}}
|
{{range $i, $track := .Tracks}}
|
||||||
<details>
|
<details>
|
||||||
<summary class="album-track-title">{{.Number}}. {{.Title}}</summary>
|
<summary class="album-track-title">{{$track.Add $i 1}}. {{$track.Title}}</summary>
|
||||||
|
|
||||||
{{if .Description}}
|
{{if $track.Description}}
|
||||||
<p class="album-track-subheading">DESCRIPTION</p>
|
<p class="album-track-subheading">DESCRIPTION</p>
|
||||||
{{.Description}}
|
{{$track.Description}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<p class="album-track-subheading">LYRICS</p>
|
<p class="album-track-subheading">LYRICS</p>
|
||||||
{{if .Lyrics}}
|
{{if $track.Lyrics}}
|
||||||
{{.GetLyricsHTML}}
|
{{$track.GetLyricsHTML}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="empty">No lyrics.</span>
|
<span class="empty">No lyrics.</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -78,15 +78,15 @@ func ServeGateway(app *model.AppState, release *model.Release) http.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !release.IsReleased() && !privileged {
|
response := *release
|
||||||
release.Tracks = nil
|
|
||||||
release.Credits = nil
|
if release.IsReleased() || privileged {
|
||||||
release.Links = nil
|
response.Tracks = release.Tracks
|
||||||
|
response.Credits = release.Credits
|
||||||
|
response.Links = release.Links
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, track := range release.Tracks { track.Number = i + 1 }
|
err := templates.MusicGatewayTemplate.Execute(w, response)
|
||||||
|
|
||||||
err := templates.MusicGatewayTemplate.Execute(w, release)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err)
|
fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue