my god...it's finally done
This commit is contained in:
parent
2baf71214e
commit
19d76ebc47
|
@ -7,7 +7,7 @@ tmp_dir = "tmp"
|
|||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
delay = 1000
|
||||
exclude_dir = ["admin\\static", "public", "uploads"]
|
||||
exclude_dir = ["admin\\static", "public", "uploads", "test"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
|
|
11
.dockerignore
Normal file
11
.dockerignore
Normal file
|
@ -0,0 +1,11 @@
|
|||
**/.DS_Store
|
||||
.git/
|
||||
.air.toml/
|
||||
.gitattributes
|
||||
.gitignore
|
||||
uploads/*
|
||||
test/
|
||||
tmp/
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
schema.sql
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@
|
|||
tmp/
|
||||
test/
|
||||
uploads/*
|
||||
docker-compose-test.yml
|
||||
|
|
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
@ -0,0 +1,23 @@
|
|||
FROM golang:1.22 AS build-stage
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /arimelody-web
|
||||
|
||||
# ---
|
||||
|
||||
FROM build-stage AS build-release-stage
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build-stage /arimelody-web /arimelody-web
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["/arimelody-web"]
|
|
@ -3,9 +3,10 @@ package admin
|
|||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"arimelody.me/arimelody.me/global"
|
||||
"arimelody-web/global"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -28,9 +29,9 @@ var ADMIN_BYPASS = func() bool {
|
|||
}()
|
||||
|
||||
var ADMIN_ID_DISCORD = func() string {
|
||||
id := global.Args["discordAdmin"]
|
||||
id := os.Getenv("DISCORD_ADMIN")
|
||||
if id == "" {
|
||||
fmt.Printf("WARN: Discord admin ID (-discordAdmin) was not provided. Admin login will be unavailable.\n")
|
||||
fmt.Printf("WARN: Discord admin ID (DISCORD_ADMIN) was not provided. Admin login will be unavailable.\n")
|
||||
}
|
||||
return id
|
||||
}()
|
||||
|
|
47
admin/artisthttp.go
Normal file
47
admin/artisthttp.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"arimelody-web/global"
|
||||
"arimelody-web/music/model"
|
||||
"arimelody-web/music/controller"
|
||||
)
|
||||
|
||||
func serveArtist() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
slices := strings.Split(r.URL.Path[1:], "/")
|
||||
id := slices[0]
|
||||
artist, err := music.GetArtist(global.DB, id)
|
||||
if err != nil {
|
||||
if artist == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Error rendering admin artist page for %s: %s\n", id, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
credits, err := music.GetArtistCredits(global.DB, artist.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering admin track page for %s: %s\n", id, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
type Artist struct {
|
||||
*model.Artist
|
||||
Credits []*model.Credit
|
||||
}
|
||||
|
||||
err = pages["artist"].Execute(w, Artist{ Artist: artist, Credits: credits })
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering admin track page for %s: %s\n", id, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -61,6 +61,7 @@
|
|||
el.remove();
|
||||
});
|
||||
|
||||
el.draggable = true;
|
||||
el.addEventListener("dragstart", () => { el.classList.add("moving") });
|
||||
el.addEventListener("dragend", () => { el.classList.remove("moving") });
|
||||
}
|
||||
|
|
|
@ -3,17 +3,16 @@ package admin
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"arimelody.me/arimelody.me/discord"
|
||||
"arimelody.me/arimelody.me/global"
|
||||
musicModel "arimelody.me/arimelody.me/music/model"
|
||||
musicDB "arimelody.me/arimelody.me/music/controller"
|
||||
"arimelody-web/discord"
|
||||
"arimelody-web/global"
|
||||
musicDB "arimelody-web/music/controller"
|
||||
musicModel "arimelody-web/music/model"
|
||||
)
|
||||
|
||||
type loginData struct {
|
||||
|
@ -28,6 +27,7 @@ func Handler() http.Handler {
|
|||
mux.Handle("/logout", MustAuthorise(LogoutHandler()))
|
||||
mux.Handle("/static/", http.StripPrefix("/static", staticHandler()))
|
||||
mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease())))
|
||||
mux.Handle("/artist/", MustAuthorise(http.StripPrefix("/artist", serveArtist())))
|
||||
mux.Handle("/track/", MustAuthorise(http.StripPrefix("/track", serveTrack())))
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
|
@ -41,30 +41,12 @@ func Handler() http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
type (
|
||||
IndexData struct {
|
||||
Releases []musicModel.FullRelease
|
||||
Artists []*musicModel.Artist
|
||||
Tracks []musicModel.DisplayTrack
|
||||
}
|
||||
)
|
||||
|
||||
dbReleases, err := musicDB.GetAllReleases(global.DB)
|
||||
releases, err := musicDB.GetAllReleases(global.DB, false, 0, true)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull releases: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
releases := []musicModel.FullRelease{}
|
||||
for _, release := range dbReleases {
|
||||
fullRelease, err := musicDB.GetFullRelease(global.DB, release.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
releases = append(releases, *fullRelease)
|
||||
}
|
||||
|
||||
artists, err := musicDB.GetAllArtists(global.DB)
|
||||
if err != nil {
|
||||
|
@ -73,19 +55,17 @@ func Handler() http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
dbTracks, err := musicDB.GetOrphanTracks(global.DB)
|
||||
tracks, err := musicDB.GetOrphanTracks(global.DB)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull orphan tracks: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var tracks = []musicModel.DisplayTrack{}
|
||||
for _, track := range dbTracks {
|
||||
tracks = append(tracks, musicModel.DisplayTrack{
|
||||
Track: track,
|
||||
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
||||
})
|
||||
type IndexData struct {
|
||||
Releases []*musicModel.Release
|
||||
Artists []*musicModel.Artist
|
||||
Tracks []*musicModel.Track
|
||||
}
|
||||
|
||||
err = pages["index"].Execute(w, IndexData{
|
||||
|
@ -125,8 +105,9 @@ func GetSession(r *http.Request) *Session {
|
|||
// is the session token in context?
|
||||
var ctx_session = r.Context().Value("session")
|
||||
if ctx_session != nil {
|
||||
token = ctx_session.(string)
|
||||
token = ctx_session.(*Session).Token
|
||||
}
|
||||
|
||||
// okay, is it in the auth header?
|
||||
if token == "" {
|
||||
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
||||
|
@ -168,11 +149,15 @@ func GetSession(r *http.Request) *Session {
|
|||
|
||||
func LoginHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if discord.CREDENTIALS_PROVIDED && ADMIN_ID_DISCORD == "" {
|
||||
if !discord.CREDENTIALS_PROVIDED || ADMIN_ID_DISCORD == "" {
|
||||
http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(discord.CLIENT_ID)
|
||||
fmt.Println(discord.API_ENDPOINT)
|
||||
fmt.Println(discord.REDIRECT_URI)
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
|
||||
if code == "" {
|
||||
|
@ -209,8 +194,9 @@ func LoginHandler() http.Handler {
|
|||
cookie.Name = "token"
|
||||
cookie.Value = session.Token
|
||||
cookie.Expires = time.Now().Add(24 * time.Hour)
|
||||
// TODO: uncomment this probably that might be nice i think
|
||||
// cookie.Secure = true
|
||||
if strings.HasPrefix(global.HTTP_DOMAIN, "https") {
|
||||
cookie.Secure = true
|
||||
}
|
||||
cookie.HttpOnly = true
|
||||
cookie.Path = "/"
|
||||
http.SetCookie(w, &cookie)
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"arimelody.me/arimelody.me/global"
|
||||
db "arimelody.me/arimelody.me/music/controller"
|
||||
"arimelody.me/arimelody.me/music/model"
|
||||
"arimelody-web/global"
|
||||
db "arimelody-web/music/controller"
|
||||
"arimelody-web/music/model"
|
||||
)
|
||||
|
||||
func serveRelease() http.Handler {
|
||||
|
@ -15,13 +15,13 @@ func serveRelease() http.Handler {
|
|||
slices := strings.Split(r.URL.Path[1:], "/")
|
||||
releaseID := slices[0]
|
||||
|
||||
release, err := db.GetFullRelease(global.DB, releaseID)
|
||||
release, err := db.GetRelease(global.DB, releaseID, true)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err)
|
||||
fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", releaseID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ func serveRelease() http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func serveEditCredits(release *model.FullRelease) http.Handler {
|
||||
func serveEditCredits(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
err := components["editcredits"].Execute(w, release)
|
||||
|
@ -79,9 +79,9 @@ func serveEditCredits(release *model.FullRelease) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func serveAddCredit(release *model.FullRelease) http.Handler {
|
||||
func serveAddCredit(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
artists, err := db.GetArtistsNotOnRelease(global.DB, release.Release.ID)
|
||||
artists, err := db.GetArtistsNotOnRelease(global.DB, release.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull artists not on %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
@ -128,7 +128,7 @@ func serveNewCredit() http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func serveEditLinks(release *model.FullRelease) http.Handler {
|
||||
func serveEditLinks(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
err := components["editlinks"].Execute(w, release)
|
||||
|
@ -139,7 +139,7 @@ func serveEditLinks(release *model.FullRelease) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func serveEditTracks(release *model.FullRelease) http.Handler {
|
||||
func serveEditTracks(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
err := components["edittracks"].Execute(w, release)
|
||||
|
@ -150,9 +150,9 @@ func serveEditTracks(release *model.FullRelease) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func serveAddTrack(release *model.FullRelease) http.Handler {
|
||||
func serveAddTrack(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tracks, err := db.GetTracksNotOnRelease(global.DB, release.Release.ID)
|
||||
tracks, err := db.GetTracksNotOnRelease(global.DB, release.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull tracks not on %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
|
151
admin/static/edit-artist.css
Normal file
151
admin/static/edit-artist.css
Normal file
|
@ -0,0 +1,151 @@
|
|||
h1 {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
#artist {
|
||||
margin-bottom: 1em;
|
||||
padding: 1.5em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.2em;
|
||||
|
||||
border-radius: .5em;
|
||||
background: #f8f8f8f8;
|
||||
border: 1px solid #808080;
|
||||
}
|
||||
|
||||
.artist-avatar {
|
||||
width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
.artist-avatar img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
.artist-avatar img:hover {
|
||||
outline: 1px solid #808080;
|
||||
cursor: pointer;
|
||||
}
|
||||
.artist-avatar #remove-avatar {
|
||||
padding: .3em .4em;
|
||||
}
|
||||
|
||||
.artist-info {
|
||||
margin: -1em 0 0 0;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.attribute-header {
|
||||
margin: 1em 0 .2em 0;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.artist-name {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: calc(100% - .4em);
|
||||
padding: .1em .2em;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
background: #ffffff;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
}
|
||||
input[type="text"]:hover {
|
||||
border-color: #80808080;
|
||||
}
|
||||
input[type="text"]:active,
|
||||
input[type="text"]:focus {
|
||||
border-color: #808080;
|
||||
}
|
||||
|
||||
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 {
|
||||
color: inherit;
|
||||
}
|
||||
button.save {
|
||||
background: #6fd7ff;
|
||||
border-color: #6f9eb0;
|
||||
}
|
||||
button.delete {
|
||||
background: #ff7171;
|
||||
border-color: #7d3535;
|
||||
}
|
||||
button:hover {
|
||||
background: #fff;
|
||||
border-color: #d0d0d0;
|
||||
}
|
||||
button:active {
|
||||
background: #d0d0d0;
|
||||
border-color: #808080;
|
||||
}
|
||||
button[disabled] {
|
||||
background: #d0d0d0 !important;
|
||||
border-color: #808080 !important;
|
||||
opacity: .5;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
a.delete {
|
||||
color: #d22828;
|
||||
}
|
||||
|
||||
.artist-actions {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
gap: .5em;
|
||||
flex-direction: row;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.card-title a.button {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.credit {
|
||||
margin: 1em 0;
|
||||
padding: .5em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
background: #f8f8f8;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #808080;
|
||||
}
|
||||
|
||||
.release-artwork {
|
||||
width: 64px;
|
||||
height: min-content;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.credit-info h3,
|
||||
.credit-info p {
|
||||
margin: 0;
|
||||
font-size: .9em;
|
||||
}
|
79
admin/static/edit-artist.js
Normal file
79
admin/static/edit-artist.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
const artistID = document.getElementById("artist").dataset.id;
|
||||
const nameInput = document.getElementById("name");
|
||||
const avatarImg = document.getElementById("avatar");
|
||||
const removeAvatarBtn = document.getElementById("remove-avatar");
|
||||
const avatarInput = document.getElementById("avatar-file");
|
||||
const websiteInput = document.getElementById("website");
|
||||
const saveBtn = document.getElementById("save");
|
||||
const deleteBtn = document.getElementById("delete");
|
||||
|
||||
saveBtn.addEventListener("click", () => {
|
||||
fetch("/api/v1/artist/" + artistID, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
name: nameInput.value,
|
||||
website: websiteInput.value,
|
||||
avatar: avatarImg.src,
|
||||
}),
|
||||
headers: { "Content-Type": "application/json" }
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
res.text().then(error => {
|
||||
console.error(error);
|
||||
alert("Failed to update release: " + error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
location = location;
|
||||
});
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener("click", () => {
|
||||
if (artistID != prompt(
|
||||
"You are about to permanently delete " + artistID + ". " +
|
||||
"This action is irreversible. " +
|
||||
"Please enter \"" + artistID + "\" to continue.")) return;
|
||||
fetch("/api/v1/artist/" + artistID, {
|
||||
method: "DELETE",
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
res.text().then(error => {
|
||||
console.error(error);
|
||||
alert("Failed to delete release: " + error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
location = "/admin";
|
||||
});
|
||||
});
|
||||
|
||||
[nameInput, websiteInput].forEach(input => {
|
||||
input.addEventListener("change", () => {
|
||||
saveBtn.disabled = false;
|
||||
});
|
||||
input.addEventListener("keypress", () => {
|
||||
saveBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
avatarImg.addEventListener("click", () => {
|
||||
avatarInput.addEventListener("change", () => {
|
||||
if (avatarInput.files.length > 0) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const data = e.target.result;
|
||||
avatarImg.src = data;
|
||||
saveBtn.disabled = false;
|
||||
};
|
||||
reader.readAsDataURL(avatarInput.files[0]);
|
||||
}
|
||||
});
|
||||
avatarInput.click();
|
||||
});
|
||||
|
||||
removeAvatarBtn.addEventListener("click", () => {
|
||||
avatarImg.src = "/img/default-avatar.png"
|
||||
saveBtn.disabled = false;
|
||||
});
|
|
@ -1,5 +1,3 @@
|
|||
import Stateful from "/script/silver.min.js"
|
||||
|
||||
const releaseID = document.getElementById("release").dataset.id;
|
||||
const titleInput = document.getElementById("title");
|
||||
const artworkImg = document.getElementById("artwork");
|
||||
|
|
|
@ -6,7 +6,7 @@ h1 {
|
|||
|
||||
#track {
|
||||
margin-bottom: 1em;
|
||||
padding: 1.5em;
|
||||
padding: .5em 1.5em 1.5em 1.5em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.2em;
|
||||
|
@ -34,7 +34,7 @@ h1 {
|
|||
}
|
||||
|
||||
#title {
|
||||
width: 100%;
|
||||
width: calc(100% - .4em);
|
||||
padding: .1em .2em;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const newReleaseBtn = document.getElementById("create-release");
|
||||
const newArtistBtn = document.getElementById("create-artist");
|
||||
const newTrackBtn = document.getElementById("create-track");
|
||||
|
||||
newReleaseBtn.addEventListener("click", event => {
|
||||
|
@ -24,6 +25,30 @@ newReleaseBtn.addEventListener("click", event => {
|
|||
});
|
||||
});
|
||||
|
||||
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/artist/" + 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:");
|
||||
|
@ -43,7 +68,7 @@ newTrackBtn.addEventListener("click", event => {
|
|||
}
|
||||
})
|
||||
}).catch(err => {
|
||||
alert("Failed to create release. Check the console for details.");
|
||||
alert("Failed to create track. Check the console for details.");
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,6 +29,11 @@ var pages = map[string]*template.Template{
|
|||
filepath.Join("views", "prideflag.html"),
|
||||
filepath.Join("admin", "views", "edit-release.html"),
|
||||
)),
|
||||
"artist": template.Must(template.ParseFiles(
|
||||
filepath.Join("admin", "views", "layout.html"),
|
||||
filepath.Join("views", "prideflag.html"),
|
||||
filepath.Join("admin", "views", "edit-artist.html"),
|
||||
)),
|
||||
"track": template.Must(template.ParseFiles(
|
||||
filepath.Join("admin", "views", "layout.html"),
|
||||
filepath.Join("views", "prideflag.html"),
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"arimelody.me/arimelody.me/global"
|
||||
"arimelody.me/arimelody.me/music/model"
|
||||
"arimelody.me/arimelody.me/music/controller"
|
||||
"arimelody-web/global"
|
||||
"arimelody-web/music/model"
|
||||
"arimelody-web/music/controller"
|
||||
)
|
||||
|
||||
func serveTrack() http.Handler {
|
||||
|
@ -25,26 +25,16 @@ func serveTrack() http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
dbReleases, err := music.GetTrackReleases(global.DB, track.ID)
|
||||
releases, err := music.GetTrackReleases(global.DB, track.ID, true)
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering admin track page for %s: %s\n", id, err)
|
||||
fmt.Printf("FATAL: Failed to pull releases for %s: %s\n", id, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
releases := []model.FullRelease{}
|
||||
for _, release := range dbReleases {
|
||||
fullRelease, err := music.GetFullRelease(global.DB, release.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
releases = append(releases, *fullRelease)
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
*model.Track
|
||||
Releases []model.FullRelease
|
||||
Releases []*model.Release
|
||||
}
|
||||
|
||||
err = pages["track"].Execute(w, Track{ Track: track, Releases: releases })
|
||||
|
|
72
admin/views/edit-artist.html
Normal file
72
admin/views/edit-artist.html
Normal file
|
@ -0,0 +1,72 @@
|
|||
{{define "head"}}
|
||||
<title>Editing {{.Name}} - ari melody 💫</title>
|
||||
|
||||
<link rel="stylesheet" href="/admin/static/edit-artist.css">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<h1>Editing Artist</h1>
|
||||
|
||||
<div id="artist" data-id="{{.ID}}">
|
||||
<div class="artist-avatar">
|
||||
<img src="{{.Avatar}}" alt="" width="256" loading="lazy" id="avatar">
|
||||
<input type="file" id="avatar-file" name="Artwork" accept=".png,.jpg,.jpeg" hidden>
|
||||
<button id="remove-avatar">Remove</button>
|
||||
</div>
|
||||
<div class="artist-info">
|
||||
<p class="attribute-header">Name</p>
|
||||
<h2 class="artist-name">
|
||||
<input type="text" id="name" name="artist-name" value="{{.Name}}">
|
||||
</h2>
|
||||
|
||||
<p class="attribute-header">Website</p>
|
||||
<input type="text" id="website" name="website" value="{{.Website}}">
|
||||
|
||||
<div class="artist-actions">
|
||||
<button type="submit" class="save" id="save" disabled>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-title">
|
||||
<h2>Featured in</h2>
|
||||
</div>
|
||||
<div class="card releases">
|
||||
{{if .Credits}}
|
||||
{{range .Credits}}
|
||||
<div class="credit">
|
||||
<img src="{{.Release.Artwork}}" alt="" width="64" loading="lazy" class="release-artwork">
|
||||
<div class="credit-info">
|
||||
<h3 class="credit-name"><a href="/admin/release/{{.Release.ID}}">{{.Release.Title}}</a></h3>
|
||||
<p class="credit-artists">{{.Release.PrintArtists true true}}</p>
|
||||
<p class="artist-role">
|
||||
Role: {{.Role}}
|
||||
{{if .Primary}}
|
||||
<small>(Primary)</small>
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>This artist has no credits.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card-title">
|
||||
<h2>Danger Zone</h2>
|
||||
</div>
|
||||
<div class="card danger">
|
||||
<p>
|
||||
Clicking the button below will delete this artist.
|
||||
This action is <strong>irreversible</strong>.
|
||||
You will be prompted to confirm this decision.
|
||||
</p>
|
||||
<button class="delete" id="delete">Delete Artist</button>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script type="module" src="/admin/static/edit-artist.js" defer></script>
|
||||
{{end}}
|
|
@ -112,7 +112,7 @@
|
|||
<div class="credit">
|
||||
<img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
||||
<div class="credit-info">
|
||||
<p class="artist-name"><a href="/admin/artists/{{.Artist.ID}}">{{.Artist.Name}}</a></p>
|
||||
<p class="artist-name"><a href="/admin/artist/{{.Artist.ID}}">{{.Artist.Name}}</a></p>
|
||||
<p class="artist-role">
|
||||
{{.Role}}
|
||||
{{if .Primary}}
|
||||
|
@ -152,23 +152,23 @@
|
|||
>Edit</a>
|
||||
</div>
|
||||
<div class="card tracks">
|
||||
{{range .Tracks}}
|
||||
<div class="track" data-id="{{.ID}}">
|
||||
{{range $i, $track := .Tracks}}
|
||||
<div class="track" data-id="{{$track.ID}}">
|
||||
<h2 class="track-title">
|
||||
<span class="track-number">{{.Number}}</span>
|
||||
<a href="/admin/track/{{.ID}}">{{.Title}}</a>
|
||||
<span class="track-number">{{.Add $i 1}}</span>
|
||||
<a href="/admin/track/{{$track.ID}}">{{$track.Title}}</a>
|
||||
</h2>
|
||||
|
||||
<h3>Description</h3>
|
||||
{{if .Description}}
|
||||
<p class="track-description">{{.Description}}</p>
|
||||
{{if $track.Description}}
|
||||
<p class="track-description">{{$track.GetDescriptionHTML}}</p>
|
||||
{{else}}
|
||||
<p class="track-description empty">No description provided.</p>
|
||||
{{end}}
|
||||
|
||||
<h3>Lyrics</h3>
|
||||
{{if .Lyrics}}
|
||||
<p class="track-lyrics">{{.Lyrics}}</p>
|
||||
{{if $track.Lyrics}}
|
||||
<p class="track-lyrics">{{$track.GetLyricsHTML}}</p>
|
||||
{{else}}
|
||||
<p class="track-lyrics empty">There are no lyrics.</p>
|
||||
{{end}}
|
||||
|
|
|
@ -22,13 +22,13 @@
|
|||
|
||||
<div class="card-title">
|
||||
<h1>Artists</h1>
|
||||
<a class="create-btn">Create New</a>
|
||||
<a class="create-btn" id="create-artist">Create New</a>
|
||||
</div>
|
||||
<div class="card artists">
|
||||
{{range $Artist := .Artists}}
|
||||
<div class="artist">
|
||||
<img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
||||
<a href="/admin/artists/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a>
|
||||
<a href="/admin/artist/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if not .Artists}}
|
||||
|
@ -49,12 +49,12 @@
|
|||
<a href="/admin/track/{{$Track.ID}}">{{$Track.Title}}</a>
|
||||
</h2>
|
||||
{{if $Track.Description}}
|
||||
<p class="track-description">{{$Track.Description}}</p>
|
||||
<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.Lyrics}}</p>
|
||||
<p class="track-lyrics">{{$Track.GetLyricsHTML}}</p>
|
||||
{{else}}
|
||||
<p class="track-lyrics empty">There are no lyrics.</p>
|
||||
{{end}}
|
||||
|
|
19
api/api.go
19
api/api.go
|
@ -5,10 +5,10 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"arimelody.me/arimelody.me/admin"
|
||||
"arimelody.me/arimelody.me/global"
|
||||
"arimelody.me/arimelody.me/music/model"
|
||||
music "arimelody.me/arimelody.me/music/view"
|
||||
"arimelody-web/admin"
|
||||
"arimelody-web/global"
|
||||
music "arimelody-web/music/controller"
|
||||
musicView "arimelody-web/music/view"
|
||||
)
|
||||
|
||||
func Handler() http.Handler {
|
||||
|
@ -18,8 +18,7 @@ func Handler() http.Handler {
|
|||
|
||||
mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var artistID = strings.Split(r.URL.Path[1:], "/")[0]
|
||||
var artist model.Artist
|
||||
err := global.DB.Get(&artist, "SELECT * FROM artist WHERE id=$1", artistID)
|
||||
artist, err := music.GetArtist(global.DB, artistID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
|
@ -61,8 +60,7 @@ func Handler() http.Handler {
|
|||
|
||||
mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var releaseID = strings.Split(r.URL.Path[1:], "/")[0]
|
||||
var release model.Release
|
||||
err := global.DB.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", releaseID)
|
||||
release, err := music.GetRelease(global.DB, releaseID, true)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
|
@ -76,7 +74,7 @@ func Handler() http.Handler {
|
|||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
// GET /api/v1/music/{id}
|
||||
music.ServeRelease(release).ServeHTTP(w, r)
|
||||
musicView.ServeRelease(release).ServeHTTP(w, r)
|
||||
case http.MethodPut:
|
||||
// PUT /api/v1/music/{id} (admin)
|
||||
admin.MustAuthorise(UpdateRelease(release)).ServeHTTP(w, r)
|
||||
|
@ -104,8 +102,7 @@ func Handler() http.Handler {
|
|||
|
||||
mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var trackID = strings.Split(r.URL.Path[1:], "/")[0]
|
||||
var track model.Track
|
||||
err := global.DB.Get(&track, "SELECT * FROM musictrack WHERE id=$1", trackID)
|
||||
track, err := music.GetTrack(global.DB, trackID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
|
|
105
api/artist.go
105
api/artist.go
|
@ -3,21 +3,18 @@ package api
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"arimelody.me/arimelody.me/global"
|
||||
"arimelody.me/arimelody.me/music/model"
|
||||
db "arimelody.me/arimelody.me/music/controller"
|
||||
"arimelody-web/global"
|
||||
db "arimelody-web/music/controller"
|
||||
music "arimelody-web/music/controller"
|
||||
"arimelody-web/music/model"
|
||||
)
|
||||
|
||||
type artistJSON struct {
|
||||
ID string `json:"id"`
|
||||
Name *string `json:"name"`
|
||||
Website *string `json:"website"`
|
||||
Avatar *string `json:"avatar"`
|
||||
}
|
||||
|
||||
func ServeAllArtists() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var artists = []*model.Artist{}
|
||||
|
@ -36,7 +33,7 @@ func ServeAllArtists() http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func ServeArtist(artist model.Artist) http.Handler {
|
||||
func ServeArtist(artist *model.Artist) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
type (
|
||||
creditJSON struct {
|
||||
|
@ -44,7 +41,7 @@ func ServeArtist(artist model.Artist) http.Handler {
|
|||
Primary bool `json:"primary"`
|
||||
}
|
||||
artistJSON struct {
|
||||
model.Artist
|
||||
*model.Artist
|
||||
Credits map[string]creditJSON `json:"credits"`
|
||||
}
|
||||
)
|
||||
|
@ -78,39 +75,23 @@ func ServeArtist(artist model.Artist) http.Handler {
|
|||
|
||||
func CreateArtist() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var data artistJSON
|
||||
err := json.NewDecoder(r.Body).Decode(&data)
|
||||
var artist model.Artist
|
||||
err := json.NewDecoder(r.Body).Decode(&artist)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if data.ID == "" {
|
||||
if artist.ID == "" {
|
||||
http.Error(w, "Artist ID cannot be blank\n", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if data.Name == nil || *data.Name == "" {
|
||||
http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if artist.Name == "" { artist.Name = artist.ID }
|
||||
|
||||
var artist = model.Artist{
|
||||
ID: data.ID,
|
||||
Name: *data.Name,
|
||||
Website: *data.Website,
|
||||
Avatar: *data.Avatar,
|
||||
}
|
||||
|
||||
_, err = global.DB.Exec(
|
||||
"INSERT INTO artist (id, name, website, avatar) "+
|
||||
"VALUES ($1, $2, $3, $4)",
|
||||
artist.ID,
|
||||
artist.Name,
|
||||
artist.Website,
|
||||
artist.Avatar)
|
||||
err = music.CreateArtist(global.DB, &artist)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate key") {
|
||||
http.Error(w, fmt.Sprintf("Artist %s already exists\n", data.ID), http.StatusBadRequest)
|
||||
http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fmt.Printf("FATAL: Failed to create artist %s: %s\n", artist.ID, err)
|
||||
|
@ -122,43 +103,59 @@ func CreateArtist() http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func UpdateArtist(artist model.Artist) http.Handler {
|
||||
func UpdateArtist(artist *model.Artist) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var data artistJSON
|
||||
err := json.NewDecoder(r.Body).Decode(&data)
|
||||
err := json.NewDecoder(r.Body).Decode(&artist)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to update artist: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if data.ID != "" { artist.ID = data.ID }
|
||||
if data.Name != nil { artist.Name = *data.Name }
|
||||
if data.Website != nil { artist.Website = *data.Website }
|
||||
if data.Avatar != nil { artist.Avatar = *data.Avatar }
|
||||
if artist.Avatar == "" {
|
||||
artist.Avatar = "/img/default-avatar.png"
|
||||
} else {
|
||||
if strings.Contains(artist.Avatar, ";base64,") {
|
||||
var artworkDirectory = filepath.Join("uploads", "avatar")
|
||||
filename, err := HandleImageUpload(&artist.Avatar, artworkDirectory, artist.ID)
|
||||
|
||||
_, err = global.DB.Exec(
|
||||
"UPDATE artist "+
|
||||
"SET name=$2, website=$3, avatar=$4 "+
|
||||
"WHERE id=$1",
|
||||
artist.ID,
|
||||
artist.Name,
|
||||
artist.Website,
|
||||
artist.Avatar)
|
||||
// clean up files with this ID and different extensions
|
||||
err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error {
|
||||
if path == filepath.Join(artworkDirectory, filename) { return nil }
|
||||
|
||||
withoutExt := strings.TrimSuffix(path, filepath.Ext(path))
|
||||
if withoutExt != filepath.Join(artworkDirectory, artist.ID) { return nil }
|
||||
|
||||
return os.Remove(path)
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("WARN: Error while cleaning up avatar files: %s\n", err)
|
||||
}
|
||||
|
||||
artist.Avatar = fmt.Sprintf("/uploads/avatar/%s", filename)
|
||||
}
|
||||
}
|
||||
|
||||
err = music.UpdateArtist(global.DB, artist)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Printf("FATAL: Failed to update artist %s: %s\n", artist.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteArtist(artist model.Artist) http.Handler {
|
||||
func DeleteArtist(artist *model.Artist) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := global.DB.Exec(
|
||||
"DELETE FROM artist "+
|
||||
"WHERE id=$1",
|
||||
artist.ID)
|
||||
err := music.DeleteArtist(global.DB, artist.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Printf("FATAL: Failed to delete artist %s: %s\n", artist.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
|
@ -10,35 +10,44 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"arimelody.me/arimelody.me/admin"
|
||||
"arimelody.me/arimelody.me/global"
|
||||
music "arimelody.me/arimelody.me/music/controller"
|
||||
"arimelody.me/arimelody.me/music/model"
|
||||
"arimelody-web/admin"
|
||||
"arimelody-web/global"
|
||||
music "arimelody-web/music/controller"
|
||||
"arimelody-web/music/model"
|
||||
)
|
||||
|
||||
func ServeCatalog() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
releases := []*model.Release{}
|
||||
err := global.DB.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC")
|
||||
releases, err := music.GetAllReleases(global.DB, false, 0, true)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
catalog := []model.ReleaseShorthand{}
|
||||
type Release struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
ReleaseType model.ReleaseType `json:"type" db:"type"`
|
||||
ReleaseDate time.Time `json:"releaseDate" db:"release_date"`
|
||||
Artwork string `json:"artwork"`
|
||||
Buylink string `json:"buylink"`
|
||||
Copyright string `json:"copyright" db:"copyright"`
|
||||
}
|
||||
|
||||
catalog := []Release{}
|
||||
authorised := admin.GetSession(r) != nil
|
||||
for _, release := range releases {
|
||||
if !release.Visible && !authorised {
|
||||
continue
|
||||
}
|
||||
catalog = append(catalog, model.ReleaseShorthand{
|
||||
catalog = append(catalog, Release{
|
||||
ID: release.ID,
|
||||
Title: release.Title,
|
||||
ReleaseType: release.ReleaseType,
|
||||
ReleaseDate: release.ReleaseDate,
|
||||
Artwork: release.Artwork,
|
||||
Buylink: release.Buylink,
|
||||
Copyright: release.Copyright,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -85,7 +94,7 @@ func CreateRelease() http.Handler {
|
|||
http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Failed to create release %s: %s\n", release.ID, err)
|
||||
fmt.Printf("FATAL: Failed to create release %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@ -100,7 +109,7 @@ func CreateRelease() http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func UpdateRelease(release model.Release) http.Handler {
|
||||
func UpdateRelease(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
http.NotFound(w, r)
|
||||
|
@ -157,15 +166,19 @@ func UpdateRelease(release model.Release) http.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
err = music.UpdateRelease(global.DB, &release)
|
||||
err = music.UpdateRelease(global.DB, release)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Printf("FATAL: Failed to update release %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateReleaseTracks(release model.Release) http.Handler {
|
||||
func UpdateReleaseTracks(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var trackIDs = []string{}
|
||||
err := json.NewDecoder(r.Body).Decode(&trackIDs)
|
||||
|
@ -174,15 +187,19 @@ func UpdateReleaseTracks(release model.Release) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
err = music.UpdateReleaseTracks(global.DB, &release, trackIDs)
|
||||
err = music.UpdateReleaseTracks(global.DB, release.ID, trackIDs)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to update tracks for %s: %s\n", release.ID, err)
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Printf("FATAL: Failed to update tracks for %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateReleaseCredits(release model.Release) http.Handler {
|
||||
func UpdateReleaseCredits(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
type creditJSON struct {
|
||||
Artist string
|
||||
|
@ -196,9 +213,9 @@ func UpdateReleaseCredits(release model.Release) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
var credits []model.Credit
|
||||
var credits []*model.Credit
|
||||
for _, credit := range data {
|
||||
credits = append(credits, model.Credit{
|
||||
credits = append(credits, &model.Credit{
|
||||
Artist: model.Artist{
|
||||
ID: credit.Artist,
|
||||
},
|
||||
|
@ -207,19 +224,23 @@ func UpdateReleaseCredits(release model.Release) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
err = music.UpdateReleaseCredits(global.DB, &release, credits)
|
||||
err = music.UpdateReleaseCredits(global.DB, release.ID, credits)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate key") {
|
||||
http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Failed to update links for %s: %s\n", release.ID, err)
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Printf("FATAL: Failed to update links for %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateReleaseLinks(release model.Release) http.Handler {
|
||||
func UpdateReleaseLinks(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
http.NotFound(w, r)
|
||||
|
@ -233,30 +254,27 @@ func UpdateReleaseLinks(release model.Release) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
tx := global.DB.MustBegin()
|
||||
tx.MustExec("DELETE FROM musiclink WHERE release=$1", release.ID)
|
||||
for _, link := range links {
|
||||
tx.MustExec(
|
||||
"INSERT INTO musiclink "+
|
||||
"(release, name, url) "+
|
||||
"VALUES ($1, $2, $3)",
|
||||
release.ID,
|
||||
link.Name,
|
||||
link.URL)
|
||||
}
|
||||
err = tx.Commit()
|
||||
err = music.UpdateReleaseLinks(global.DB, release.ID, links)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to update links for %s: %s\n", release.ID, err)
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Printf("FATAL: Failed to update links for %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteRelease(release model.Release) http.Handler {
|
||||
func DeleteRelease(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := global.DB.Exec("DELETE FROM musicrelease WHERE id=$1", release.ID)
|
||||
err := music.DeleteRelease(global.DB, release.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to delete release %s: %s\n", release.ID, err)
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Printf("FATAL: Failed to delete release %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
|
|
25
api/track.go
25
api/track.go
|
@ -5,15 +5,15 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"arimelody.me/arimelody.me/global"
|
||||
music "arimelody.me/arimelody.me/music/controller"
|
||||
"arimelody.me/arimelody.me/music/model"
|
||||
"arimelody-web/global"
|
||||
music "arimelody-web/music/controller"
|
||||
"arimelody-web/music/model"
|
||||
)
|
||||
|
||||
type (
|
||||
Track struct {
|
||||
model.Track
|
||||
Releases []model.ReleaseShorthand
|
||||
*model.Track
|
||||
Releases []string `json:"releases"`
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -48,14 +48,19 @@ func ServeAllTracks() http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func ServeTrack(track model.Track) http.Handler {
|
||||
func ServeTrack(track *model.Track) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
releases, err := music.GetTrackReleases(global.DB, track.ID)
|
||||
dbReleases, err := music.GetTrackReleases(global.DB, track.ID, false)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", track.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
releases := []string{}
|
||||
for _, release := range dbReleases {
|
||||
releases = append(releases, release.ID)
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(Track{ track, releases })
|
||||
if err != nil {
|
||||
|
@ -97,7 +102,7 @@ func CreateTrack() http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func UpdateTrack(track model.Track) http.Handler {
|
||||
func UpdateTrack(track *model.Track) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut || r.URL.Path == "/" {
|
||||
http.NotFound(w, r)
|
||||
|
@ -115,7 +120,7 @@ func UpdateTrack(track model.Track) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
err = music.UpdateTrack(global.DB, &track)
|
||||
err = music.UpdateTrack(global.DB, track)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to update track %s: %s\n", track.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
@ -130,7 +135,7 @@ func UpdateTrack(track model.Track) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func DeleteTrack(track model.Track) http.Handler {
|
||||
func DeleteTrack(track *model.Track) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete || r.URL.Path == "/" {
|
||||
http.NotFound(w, r)
|
||||
|
|
|
@ -35,6 +35,8 @@ func HandleImageUpload(data *string, directory string, filename string) (string,
|
|||
}
|
||||
defer file.Close()
|
||||
|
||||
// TODO: generate compressed versions of image (512x512?)
|
||||
|
||||
buffer := bufio.NewWriter(file)
|
||||
_, err = buffer.Write(imageData)
|
||||
if err != nil {
|
||||
|
|
|
@ -6,26 +6,27 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"arimelody.me/arimelody.me/global"
|
||||
"arimelody-web/global"
|
||||
)
|
||||
|
||||
const API_ENDPOINT = "https://discord.com/api/v10"
|
||||
|
||||
var CREDENTIALS_PROVIDED = true
|
||||
var CLIENT_ID = func() string {
|
||||
id := global.Args["discordClient"]
|
||||
id := os.Getenv("DISCORD_CLIENT")
|
||||
if id == "" {
|
||||
fmt.Printf("WARN: Discord client ID (-discordClient) was not provided. Admin login will be unavailable.\n")
|
||||
fmt.Printf("WARN: Discord client ID (DISCORD_CLIENT) was not provided. Admin login will be unavailable.\n")
|
||||
CREDENTIALS_PROVIDED = false
|
||||
}
|
||||
return id
|
||||
}()
|
||||
var CLIENT_SECRET = func() string {
|
||||
secret := global.Args["discordSecret"]
|
||||
if secret== "" {
|
||||
fmt.Printf("WARN: Discord secret (-discordSecret) was not provided. Admin login will be unavailable.\n")
|
||||
secret := os.Getenv("DISCORD_SECRET")
|
||||
if secret == "" {
|
||||
fmt.Printf("WARN: Discord secret (DISCORD_SECRET) was not provided. Admin login will be unavailable.\n")
|
||||
CREDENTIALS_PROVIDED = false
|
||||
}
|
||||
return secret
|
||||
|
@ -107,7 +108,6 @@ func GetDiscordUserFromAuth(token string) (DiscordUser, error) {
|
|||
}
|
||||
|
||||
auth_info := AuthInfoResponse{}
|
||||
|
||||
err = json.NewDecoder(res.Body).Decode(&auth_info)
|
||||
if err != nil {
|
||||
return DiscordUser{}, errors.New(fmt.Sprintf("Failed to parse auth information from discord: %s\n", err))
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
version: '3.9'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16.1-alpine3.18
|
||||
container_name: arimelody.me-db
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- arimelody-db:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: arimelody
|
||||
POSTGRES_USER: arimelody
|
||||
POSTGRES_PASSWORD: fuckingpassword
|
||||
|
||||
volumes:
|
||||
arimelody-db:
|
||||
external: true
|
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
|
@ -0,0 +1,22 @@
|
|||
services:
|
||||
web:
|
||||
image: docker.arimelody.me/arimelody.me:latest
|
||||
build: .
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
environment:
|
||||
HTTP_DOMAIN: "https://arimelody.me"
|
||||
ARIMELODY_DB_HOST: db
|
||||
DISCORD_ADMIN: # your discord user ID.
|
||||
DISCORD_CLIENT: # your discord OAuth client ID.
|
||||
DISCORD_SECRET: # your discord OAuth secret.
|
||||
db:
|
||||
image: postgres:16.1-alpine3.18
|
||||
volumes:
|
||||
- ./db:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: arimelody
|
||||
POSTGRES_USER: arimelody
|
||||
POSTGRES_PASSWORD: fuckingpassword
|
|
@ -35,11 +35,11 @@ var Args = func() map[string]string {
|
|||
}()
|
||||
|
||||
var HTTP_DOMAIN = func() string {
|
||||
domain := Args["httpDomain"]
|
||||
if domain != "" {
|
||||
return domain
|
||||
}
|
||||
domain := os.Getenv("HTTP_DOMAIN")
|
||||
if domain == "" {
|
||||
return "https://arimelody.me"
|
||||
}
|
||||
return domain
|
||||
}()
|
||||
|
||||
var DB *sqlx.DB
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"arimelody.me/arimelody.me/colour"
|
||||
"arimelody-web/colour"
|
||||
)
|
||||
|
||||
func DefaultHeaders(next http.Handler) http.Handler {
|
||||
|
|
15
go.mod
15
go.mod
|
@ -1,19 +1,8 @@
|
|||
module arimelody.me/arimelody.me
|
||||
module arimelody-web
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/lib/pq v1.10.9
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
|
36
go.sum
36
go.sum
|
@ -1,30 +1,10 @@
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k=
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
|
|
19
main.go
19
main.go
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
@ -8,11 +9,11 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"arimelody.me/arimelody.me/admin"
|
||||
"arimelody.me/arimelody.me/api"
|
||||
"arimelody.me/arimelody.me/global"
|
||||
musicView "arimelody.me/arimelody.me/music/view"
|
||||
"arimelody.me/arimelody.me/templates"
|
||||
"arimelody-web/admin"
|
||||
"arimelody-web/api"
|
||||
"arimelody-web/global"
|
||||
musicView "arimelody-web/music/view"
|
||||
"arimelody-web/templates"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
|
@ -22,8 +23,11 @@ const DEFAULT_PORT int = 8080
|
|||
|
||||
func main() {
|
||||
// initialise database connection
|
||||
var dbHost = os.Getenv("ARIMELODY_DB_HOST")
|
||||
if dbHost == "" { dbHost = "127.0.0.1" }
|
||||
|
||||
var err error
|
||||
global.DB, err = sqlx.Connect("postgres", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable")
|
||||
global.DB, err = sqlx.Connect("postgres", "host=" + dbHost + " user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "FATAL: Unable to create database connection pool: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
@ -64,9 +68,10 @@ func createServeMux() *http.ServeMux {
|
|||
func staticHandler(directory string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
|
||||
|
||||
// does the file exist?
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package music
|
||||
|
||||
import (
|
||||
"arimelody.me/arimelody.me/music/model"
|
||||
"arimelody-web/music/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
|
@ -45,29 +45,56 @@ func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, err
|
|||
}
|
||||
|
||||
func GetArtistCredits(db *sqlx.DB, artistID string) ([]*model.Credit, error) {
|
||||
type DBCredit struct {
|
||||
Release string
|
||||
Artist string
|
||||
Role string
|
||||
Primary bool `db:"is_primary"`
|
||||
}
|
||||
var dbCredits []DBCredit
|
||||
|
||||
err := db.Select(&dbCredits, "SELECT * FROM musiccredit WHERE artist=$1", artistID)
|
||||
rows, err := db.Query(
|
||||
"SELECT release.id,release.title,release.artwork,artist.id,artist.name,artist.website,artist.avatar,role,is_primary "+
|
||||
"FROM musiccredit "+
|
||||
"JOIN musicrelease AS release ON release=release.id "+
|
||||
"JOIN artist ON artist=artist.id "+
|
||||
"WHERE artist=$1 "+
|
||||
"ORDER BY release_date DESC",
|
||||
artistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type NamePrimary struct {
|
||||
Name string `json:"name"`
|
||||
Primary bool `json:"primary" db:"is_primary"`
|
||||
}
|
||||
var credits []*model.Credit
|
||||
for _, credit := range dbCredits {
|
||||
credits = append(credits, &model.Credit{
|
||||
Release: model.Release{ ID: credit.Release },
|
||||
Artist: model.Artist{ ID: credit.Artist },
|
||||
Role: credit.Role,
|
||||
Primary: credit.Primary,
|
||||
for rows.Next() {
|
||||
var credit model.Credit
|
||||
err = rows.Scan(
|
||||
&credit.Release.ID,
|
||||
&credit.Release.Title,
|
||||
&credit.Release.Artwork,
|
||||
&credit.Artist.ID,
|
||||
&credit.Artist.Name,
|
||||
&credit.Artist.Website,
|
||||
&credit.Artist.Avatar,
|
||||
&credit.Role,
|
||||
&credit.Primary,
|
||||
)
|
||||
|
||||
otherArtists := []NamePrimary{}
|
||||
err = db.Select(&otherArtists,
|
||||
"SELECT name,is_primary FROM artist "+
|
||||
"JOIN musiccredit ON artist=id "+
|
||||
"WHERE release=$1",
|
||||
credit.Release.ID)
|
||||
for _, otherCredit := range otherArtists {
|
||||
credit.Release.Credits = append(credit.Release.Credits, &model.Credit{
|
||||
Artist: model.Artist{
|
||||
Name: otherCredit.Name,
|
||||
},
|
||||
Primary: otherCredit.Primary,
|
||||
})
|
||||
}
|
||||
|
||||
credits = append(credits, &credit)
|
||||
}
|
||||
|
||||
return credits, nil
|
||||
}
|
||||
|
||||
|
@ -104,11 +131,11 @@ func UpdateArtist(db *sqlx.DB, artist *model.Artist) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func DeleteArtist(db *sqlx.DB, artist *model.Artist) error {
|
||||
func DeleteArtist(db *sqlx.DB, artistID string) error {
|
||||
_, err := db.Exec(
|
||||
"DELETE FROM artist "+
|
||||
"WHERE id=$1",
|
||||
artist.ID,
|
||||
artistID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -4,29 +4,99 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"arimelody.me/arimelody.me/music/model"
|
||||
"arimelody-web/music/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
func GetRelease(db *sqlx.DB, id string) (*model.Release, error) {
|
||||
var releases = model.Release{}
|
||||
func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
|
||||
var release = model.Release{}
|
||||
|
||||
err := db.Get(&releases, "SELECT * FROM musicrelease WHERE id=$1", id)
|
||||
err := db.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &releases, nil
|
||||
if full {
|
||||
// get credits
|
||||
credits, err := GetReleaseCredits(db, id)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Credits: %s", err))
|
||||
}
|
||||
for _, credit := range credits {
|
||||
release.Credits = append(release.Credits, credit)
|
||||
}
|
||||
|
||||
// get tracks
|
||||
tracks, err := GetReleaseTracks(db, id)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Tracks: %s", err))
|
||||
}
|
||||
for _, track := range tracks {
|
||||
release.Tracks = append(release.Tracks, track)
|
||||
}
|
||||
|
||||
// get links
|
||||
links, err := GetReleaseLinks(db, id)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Links: %s", err))
|
||||
}
|
||||
for _, link := range links {
|
||||
release.Links = append(release.Links, link)
|
||||
}
|
||||
}
|
||||
|
||||
return &release, nil
|
||||
}
|
||||
|
||||
func GetAllReleases(db *sqlx.DB) ([]*model.Release, error) {
|
||||
func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*model.Release, error) {
|
||||
var releases = []*model.Release{}
|
||||
|
||||
err := db.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC")
|
||||
query := "SELECT * FROM musicrelease"
|
||||
if onlyVisible {
|
||||
query += " WHERE visible=true"
|
||||
}
|
||||
query += " ORDER BY release_date DESC"
|
||||
var err error
|
||||
if limit > 0 {
|
||||
err = db.Select(&releases, query + " LIMIT $1", limit)
|
||||
} else {
|
||||
err = db.Select(&releases, query)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if full {
|
||||
for _, release := range releases {
|
||||
// get credits
|
||||
credits, err := GetReleaseCredits(db, release.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Credits: %s", err))
|
||||
}
|
||||
for _, credit := range credits {
|
||||
release.Credits = append(release.Credits, credit)
|
||||
}
|
||||
|
||||
// get tracks
|
||||
tracks, err := GetReleaseTracks(db, release.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Tracks: %s", err))
|
||||
}
|
||||
for _, track := range tracks {
|
||||
release.Tracks = append(release.Tracks, track)
|
||||
}
|
||||
|
||||
// get links
|
||||
links, err := GetReleaseLinks(db, release.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Links: %s", err))
|
||||
}
|
||||
for _, link := range links {
|
||||
release.Links = append(release.Links, link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
|
@ -78,49 +148,53 @@ func UpdateRelease(db *sqlx.DB, release *model.Release) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func UpdateReleaseTracks(db *sqlx.DB, release *model.Release, new_tracks []string) error {
|
||||
_, err := db.Exec(
|
||||
"DELETE FROM musicreleasetrack "+
|
||||
"WHERE release=$1",
|
||||
release.ID,
|
||||
)
|
||||
func UpdateReleaseTracks(db *sqlx.DB, releaseID string, new_tracks []string) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, trackID := range new_tracks {
|
||||
_, err = db.Exec(
|
||||
"INSERT INTO musicreleasetrack "+
|
||||
"(release, track, number) "+
|
||||
"VALUES ($1, $2, $3)",
|
||||
release.ID,
|
||||
trackID,
|
||||
i,
|
||||
)
|
||||
_, err = tx.Exec("DELETE FROM musicreleasetrack WHERE release=$1", releaseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, trackID := range new_tracks {
|
||||
_, err = tx.Exec(
|
||||
"INSERT INTO musicreleasetrack "+
|
||||
"(release, track, number) "+
|
||||
"VALUES ($1, $2, $3)",
|
||||
releaseID,
|
||||
trackID,
|
||||
i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateReleaseCredits(db *sqlx.DB, release *model.Release, new_credits []model.Credit) error {
|
||||
_, err := db.Exec(
|
||||
"DELETE FROM musiccredit "+
|
||||
"WHERE release=$1",
|
||||
release.ID,
|
||||
)
|
||||
func UpdateReleaseCredits(db *sqlx.DB, releaseID string, new_credits []*model.Credit) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DELETE FROM musiccredit WHERE release=$1", releaseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, credit := range new_credits {
|
||||
_, err = db.Exec(
|
||||
_, err = tx.Exec(
|
||||
"INSERT INTO musiccredit "+
|
||||
"(release, artist, role, is_primary) "+
|
||||
"VALUES ($1, $2, $3, $4)",
|
||||
release.ID,
|
||||
releaseID,
|
||||
credit.Artist.ID,
|
||||
credit.Role,
|
||||
credit.Primary,
|
||||
|
@ -130,25 +204,31 @@ func UpdateReleaseCredits(db *sqlx.DB, release *model.Release, new_credits []mod
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateReleaseLinks(db *sqlx.DB, release *model.Release, new_links []*model.Link) error {
|
||||
_, err := db.Exec(
|
||||
"DELETE FROM musiclink "+
|
||||
"WHERE release=$1",
|
||||
release.ID,
|
||||
)
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateReleaseLinks(db *sqlx.DB, releaseID string, new_links []*model.Link) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DELETE FROM musiclink WHERE release=$1", releaseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, link := range new_links {
|
||||
_, err = db.Exec(
|
||||
fmt.Printf("%s: %s\n", link.Name, link.URL)
|
||||
_, err := tx.Exec(
|
||||
"INSERT INTO musiclink "+
|
||||
"(release, name, url) "+
|
||||
"VALUES ($1, $2, $3)",
|
||||
release.ID,
|
||||
releaseID,
|
||||
link.Name,
|
||||
link.URL,
|
||||
)
|
||||
|
@ -157,14 +237,19 @@ func UpdateReleaseLinks(db *sqlx.DB, release *model.Release, new_links []*model.
|
|||
}
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteRelease(db *sqlx.DB, release *model.Release) error {
|
||||
func DeleteRelease(db *sqlx.DB, releaseID string) error {
|
||||
_, err := db.Exec(
|
||||
"DELETE FROM musicrelease "+
|
||||
"WHERE id=$1",
|
||||
release.ID,
|
||||
releaseID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -173,54 +258,8 @@ func DeleteRelease(db *sqlx.DB, release *model.Release) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func GetFullRelease(db *sqlx.DB, releaseID string) (*model.FullRelease, error) {
|
||||
// get release
|
||||
release, err := GetRelease(db, releaseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get credits
|
||||
credits, err := GetReleaseCredits(db, releaseID)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Credits: %s", err))
|
||||
}
|
||||
|
||||
// get artists
|
||||
for i, credit := range credits {
|
||||
artist, err := GetArtist(db, credit.Artist.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("FATAL: Failed to serve release %s: Artists: %s\n", release.ID, err))
|
||||
}
|
||||
credits[i].Artist = *artist
|
||||
}
|
||||
|
||||
// get tracks
|
||||
dbTracks, err := GetReleaseTracks(db, releaseID)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Tracks: %s", err))
|
||||
}
|
||||
tracks := []model.DisplayTrack{}
|
||||
for i, track := range dbTracks {
|
||||
tracks = append(tracks, track.MakeDisplay(i + 1))
|
||||
}
|
||||
|
||||
// get links
|
||||
links, err := GetReleaseLinks(db, releaseID)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Links: %s", err))
|
||||
}
|
||||
|
||||
return &model.FullRelease{
|
||||
Release: release,
|
||||
Tracks: tracks,
|
||||
Credits: credits,
|
||||
Links: links,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetReleaseTracks(db *sqlx.DB, releaseID string) ([]model.Track, error) {
|
||||
var tracks = []model.Track{}
|
||||
func GetReleaseTracks(db *sqlx.DB, releaseID string) ([]*model.Track, error) {
|
||||
var tracks = []*model.Track{}
|
||||
|
||||
err := db.Select(&tracks,
|
||||
"SELECT musictrack.* FROM musictrack "+
|
||||
|
@ -236,40 +275,38 @@ func GetReleaseTracks(db *sqlx.DB, releaseID string) ([]model.Track, error) {
|
|||
return tracks, nil
|
||||
}
|
||||
|
||||
func GetReleaseCredits(db *sqlx.DB, releaseID string) ([]model.Credit, error) {
|
||||
type DBCredit struct {
|
||||
Release string
|
||||
Artist string
|
||||
Role string
|
||||
Primary bool `db:"is_primary"`
|
||||
}
|
||||
var dbCredits []DBCredit
|
||||
|
||||
err := db.Select(&dbCredits,
|
||||
"SELECT musiccredit.* FROM musiccredit "+
|
||||
"JOIN artist ON artist=id "+
|
||||
"WHERE release=$1",
|
||||
func GetReleaseCredits(db *sqlx.DB, releaseID string) ([]*model.Credit, error) {
|
||||
rows, err := db.Query(
|
||||
"SELECT artist.id,artist.name,artist.website,artist.avatar,role,is_primary "+
|
||||
"FROM musiccredit "+
|
||||
"JOIN artist ON artist=artist.id "+
|
||||
"JOIN musicrelease ON release=musicrelease.id "+
|
||||
"WHERE musicrelease.id=$1 "+
|
||||
"ORDER BY is_primary DESC",
|
||||
releaseID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var credits []model.Credit
|
||||
for _, credit := range dbCredits {
|
||||
credits = append(credits, model.Credit{
|
||||
Release: model.Release{ ID: credit.Release },
|
||||
Artist: model.Artist{ ID: credit.Artist },
|
||||
Role: credit.Role,
|
||||
Primary: credit.Primary,
|
||||
})
|
||||
var credits []*model.Credit
|
||||
for rows.Next() {
|
||||
credit := model.Credit{}
|
||||
rows.Scan(
|
||||
&credit.Artist.ID,
|
||||
&credit.Artist.Name,
|
||||
&credit.Artist.Website,
|
||||
&credit.Artist.Avatar,
|
||||
&credit.Role,
|
||||
&credit.Primary)
|
||||
credits = append(credits, &credit)
|
||||
}
|
||||
|
||||
return credits, nil
|
||||
}
|
||||
|
||||
func GetReleaseLinks(db *sqlx.DB, releaseID string) ([]model.Link, error) {
|
||||
var links = []model.Link{}
|
||||
func GetReleaseLinks(db *sqlx.DB, releaseID string) ([]*model.Link, error) {
|
||||
var links = []*model.Link{}
|
||||
|
||||
err := db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", releaseID)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package music
|
||||
|
||||
import (
|
||||
"arimelody.me/arimelody.me/music/model"
|
||||
"arimelody-web/music/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
|
@ -55,11 +55,12 @@ func GetTracksNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Track, error
|
|||
return tracks, nil
|
||||
}
|
||||
|
||||
func GetTrackReleases(db *sqlx.DB, trackID string) ([]model.ReleaseShorthand, error) {
|
||||
var releases = []model.ReleaseShorthand{}
|
||||
func GetTrackReleases(db *sqlx.DB, trackID string, full bool) ([]*model.Release, error) {
|
||||
var releases = []*model.Release{}
|
||||
|
||||
err := db.Select(&releases,
|
||||
"SELECT id,title,type,release_date,artwork,buylink FROM musicrelease "+
|
||||
"SELECT id,title,type,release_date,artwork,buylink "+
|
||||
"FROM musicrelease "+
|
||||
"JOIN musicreleasetrack ON release=id "+
|
||||
"WHERE track=$1 "+
|
||||
"ORDER BY release_date",
|
||||
|
@ -69,6 +70,43 @@ func GetTrackReleases(db *sqlx.DB, trackID string) ([]model.ReleaseShorthand, er
|
|||
return nil, err
|
||||
}
|
||||
|
||||
type NamePrimary struct {
|
||||
Name string `json:"name"`
|
||||
Primary bool `json:"primary" db:"is_primary"`
|
||||
}
|
||||
for _, release := range releases {
|
||||
// get artists
|
||||
credits := []NamePrimary{}
|
||||
err := db.Select(&credits,
|
||||
"SELECT name,is_primary FROM artist "+
|
||||
"JOIN musiccredit ON artist=artist.id "+
|
||||
"JOIN musicrelease ON release=musicrelease.id "+
|
||||
"WHERE musicrelease.id=$1", release.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, credit := range credits {
|
||||
release.Credits = append(release.Credits, &model.Credit{
|
||||
Artist: model.Artist{
|
||||
Name: credit.Name,
|
||||
},
|
||||
Primary: credit.Primary,
|
||||
})
|
||||
}
|
||||
|
||||
// get tracks
|
||||
tracks := []string{}
|
||||
err = db.Select(&tracks, "SELECT track FROM musicreleasetrack WHERE release=$1", release.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, trackID := range tracks {
|
||||
release.Tracks = append(release.Tracks, &model.Track{
|
||||
ID: trackID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package model
|
||||
|
||||
import "strings"
|
||||
|
||||
type (
|
||||
Artist struct {
|
||||
ID string `json:"id"`
|
||||
|
@ -21,56 +19,3 @@ func (artist Artist) GetAvatar() string {
|
|||
}
|
||||
return artist.Avatar
|
||||
}
|
||||
|
||||
func (release FullRelease) GetUniqueArtists(only_primary bool) []*Artist {
|
||||
var artists = []*Artist{}
|
||||
|
||||
for _, credit := range release.Credits {
|
||||
if only_primary && !credit.Primary {
|
||||
continue
|
||||
}
|
||||
|
||||
exists := false
|
||||
for _, a := range artists {
|
||||
if a.ID == credit.Artist.ID {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
|
||||
artists = append(artists, &credit.Artist)
|
||||
}
|
||||
|
||||
return artists
|
||||
}
|
||||
|
||||
func (release FullRelease) GetUniqueArtistNames(only_primary bool) []string {
|
||||
var names = []string{}
|
||||
for _, artist := range release.GetUniqueArtists(only_primary) {
|
||||
names = append(names, artist.Name)
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func (release FullRelease) PrintArtists(only_primary bool, ampersand bool) string {
|
||||
names := release.GetUniqueArtistNames(only_primary)
|
||||
|
||||
if len(names) == 0 {
|
||||
return "Unknown Artist"
|
||||
} else if len(names) == 1 {
|
||||
return names[0]
|
||||
}
|
||||
|
||||
if ampersand {
|
||||
res := strings.Join(names[:len(names)-1], ", ")
|
||||
res += " & " + names[len(names)-1]
|
||||
return res
|
||||
} else {
|
||||
return strings.Join(names[:], ", ")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package model
|
||||
|
||||
type Credit struct {
|
||||
type (
|
||||
Credit struct {
|
||||
Release Release `json:"release"`
|
||||
Artist Artist `json:"artist"`
|
||||
Role string `json:"role"`
|
||||
Primary bool `json:"primary" db:"is_primary"`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -19,22 +21,9 @@ type (
|
|||
Buylink string `json:"buylink"`
|
||||
Copyright string `json:"copyright" db:"copyright"`
|
||||
CopyrightURL string `json:"copyrightURL" db:"copyrighturl"`
|
||||
}
|
||||
|
||||
FullRelease struct {
|
||||
*Release
|
||||
Tracks []DisplayTrack `json:"tracks"`
|
||||
Credits []Credit `json:"credits"`
|
||||
Links []Link `json:"links"`
|
||||
}
|
||||
|
||||
ReleaseShorthand struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
ReleaseType ReleaseType `json:"type" db:"type"`
|
||||
ReleaseDate time.Time `json:"releaseDate" db:"release_date"`
|
||||
Artwork string `json:"artwork"`
|
||||
Buylink string `json:"buylink"`
|
||||
Tracks []*Track `json:"tracks"`
|
||||
Credits []*Credit `json:"credits"`
|
||||
Links []*Link `json:"links"`
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -48,6 +37,10 @@ const (
|
|||
|
||||
// GETTERS
|
||||
|
||||
func (release Release) GetDescriptionHTML() template.HTML {
|
||||
return template.HTML(strings.Replace(release.Description, "\n", "<br>", -1))
|
||||
}
|
||||
|
||||
func (release Release) TextReleaseDate() string {
|
||||
return release.ReleaseDate.Format("2006-01-02T15:04")
|
||||
}
|
||||
|
@ -67,10 +60,39 @@ func (release Release) GetArtwork() string {
|
|||
return release.Artwork
|
||||
}
|
||||
|
||||
func (release FullRelease) IsSingle() bool {
|
||||
func (release Release) IsSingle() bool {
|
||||
return len(release.Tracks) == 1;
|
||||
}
|
||||
|
||||
func (release Release) IsReleased() bool {
|
||||
return release.ReleaseDate.Before(time.Now())
|
||||
}
|
||||
|
||||
func (release Release) GetUniqueArtistNames(only_primary bool) []string {
|
||||
names := []string{}
|
||||
|
||||
for _, credit := range release.Credits {
|
||||
if only_primary && !credit.Primary { continue }
|
||||
names = append(names, credit.Artist.Name)
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func (release Release) PrintArtists(only_primary bool, ampersand bool) string {
|
||||
names := release.GetUniqueArtistNames(only_primary)
|
||||
|
||||
if len(names) == 0 {
|
||||
return "Unknown Artist"
|
||||
} else if len(names) == 1 {
|
||||
return names[0]
|
||||
}
|
||||
|
||||
if ampersand {
|
||||
res := strings.Join(names[:len(names)-1], ", ")
|
||||
res += " & " + names[len(names)-1]
|
||||
return res
|
||||
} else {
|
||||
return strings.Join(names[:], ", ")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,18 +13,17 @@ type (
|
|||
Lyrics string `json:"lyrics" db:"lyrics"`
|
||||
PreviewURL string `json:"previewURL" db:"preview_url"`
|
||||
}
|
||||
|
||||
DisplayTrack struct {
|
||||
*Track
|
||||
Lyrics template.HTML `json:"lyrics"`
|
||||
Number int `json:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func (track Track) MakeDisplay(number int) DisplayTrack {
|
||||
return DisplayTrack{
|
||||
Track: &track,
|
||||
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
||||
Number: number,
|
||||
}
|
||||
func (track Track) GetDescriptionHTML() template.HTML {
|
||||
return template.HTML(strings.Replace(track.Description, "\n", "<br>", -1))
|
||||
}
|
||||
|
||||
func (track Track) GetLyricsHTML() template.HTML {
|
||||
return template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1))
|
||||
}
|
||||
|
||||
// this function is stupid and i hate that i need it
|
||||
func (track Track) Add(a int, b int) int {
|
||||
return a + b
|
||||
}
|
||||
|
|
|
@ -4,10 +4,10 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"arimelody.me/arimelody.me/global"
|
||||
music "arimelody.me/arimelody.me/music/controller"
|
||||
"arimelody.me/arimelody.me/music/model"
|
||||
"arimelody.me/arimelody.me/templates"
|
||||
"arimelody-web/global"
|
||||
music "arimelody-web/music/controller"
|
||||
"arimelody-web/music/model"
|
||||
"arimelody-web/templates"
|
||||
)
|
||||
|
||||
// HTTP HANDLER METHODS
|
||||
|
@ -21,8 +21,7 @@ func Handler() http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
var release model.Release
|
||||
err := global.DB.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", r.URL.Path[1:])
|
||||
release, err := music.GetRelease(global.DB, r.URL.Path[1:], true)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
|
@ -36,25 +35,17 @@ func Handler() http.Handler {
|
|||
|
||||
func ServeCatalog() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
dbReleases, err := music.GetAllReleases(global.DB)
|
||||
releases, err := music.GetAllReleases(global.DB, true, 0, true)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull releases for catalog: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
releases := []*model.FullRelease{}
|
||||
for _, dbRelease := range dbReleases {
|
||||
if !dbRelease.Visible { continue }
|
||||
if !dbRelease.IsReleased() {
|
||||
dbRelease.ReleaseType = model.Upcoming
|
||||
|
||||
for _, release := range releases {
|
||||
if !release.IsReleased() {
|
||||
release.ReleaseType = model.Upcoming
|
||||
}
|
||||
release, err := music.GetFullRelease(global.DB, dbRelease.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull full release for %s: %s\n", dbRelease.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
releases = append(releases, release)
|
||||
}
|
||||
|
||||
err = templates.Pages["music"].Execute(w, releases)
|
||||
|
|
|
@ -5,11 +5,11 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"arimelody.me/arimelody.me/admin"
|
||||
"arimelody.me/arimelody.me/global"
|
||||
"arimelody.me/arimelody.me/music/model"
|
||||
db "arimelody.me/arimelody.me/music/controller"
|
||||
"arimelody.me/arimelody.me/templates"
|
||||
"arimelody-web/admin"
|
||||
"arimelody-web/global"
|
||||
"arimelody-web/music/model"
|
||||
db "arimelody-web/music/controller"
|
||||
"arimelody-web/templates"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -26,14 +26,14 @@ type (
|
|||
}
|
||||
|
||||
Release struct {
|
||||
model.Release
|
||||
*model.Release
|
||||
Tracks []Track `json:"tracks"`
|
||||
Credits []Credit `json:"credits"`
|
||||
Links map[string]string `json:"links"`
|
||||
}
|
||||
)
|
||||
|
||||
func ServeRelease(release model.Release) http.Handler {
|
||||
func ServeRelease(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// only allow authorised users to view hidden releases
|
||||
authorised := admin.GetSession(r) != nil
|
||||
|
@ -108,7 +108,7 @@ func ServeRelease(release model.Release) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func ServeGateway(release model.Release) http.Handler {
|
||||
func ServeGateway(release *model.Release) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// only allow authorised users to view hidden releases
|
||||
authorised := admin.GetSession(r) != nil
|
||||
|
@ -117,21 +117,15 @@ func ServeGateway(release model.Release) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
fullRelease := &model.FullRelease{
|
||||
Release: &release,
|
||||
}
|
||||
response := *release
|
||||
|
||||
if authorised || release.IsReleased() {
|
||||
fullerRelease, err := db.GetFullRelease(global.DB, release.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fullRelease = fullerRelease
|
||||
response.Tracks = release.Tracks
|
||||
response.Credits = release.Credits
|
||||
response.Links = release.Links
|
||||
}
|
||||
|
||||
err := templates.Pages["music-gateway"].Execute(w, fullRelease)
|
||||
err := templates.Pages["music-gateway"].Execute(w, response)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err)
|
||||
|
|
|
@ -296,6 +296,13 @@ div#info p {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
#upcoming-release {
|
||||
width: fit-content;
|
||||
padding: .3em 1em;
|
||||
font-size: 1em;
|
||||
background: #101010;
|
||||
}
|
||||
|
||||
ul#links {
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
|
@ -357,7 +364,12 @@ ul#links a:hover {
|
|||
}
|
||||
|
||||
#description {
|
||||
font-size: 1.2em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
#copyright {
|
||||
margin-bottom: 0;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
#share {
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
<p id="type" class="{{.ReleaseType}}">{{.ReleaseType}}</p>
|
||||
{{else}}
|
||||
<p id="type" class="upcoming">upcoming</p>
|
||||
<p>Releases: {{.PrintReleaseDate}}</p>
|
||||
<p id="upcoming-release">Releases: {{.PrintReleaseDate}}</p>
|
||||
{{end}}
|
||||
|
||||
{{if .IsReleased}}
|
||||
|
@ -79,7 +79,7 @@
|
|||
|
||||
{{if .Description}}
|
||||
|
||||
<p id="description">{{.Description}}</p>
|
||||
<p id="description">{{.GetDescriptionHTML}}</p>
|
||||
|
||||
{{else if .IsSingle}}
|
||||
|
||||
|
@ -90,6 +90,10 @@
|
|||
|
||||
{{end}}
|
||||
|
||||
{{if and .Copyright .CopyrightURL}}
|
||||
<p id="copyright">{{.Title}} © {{.GetReleaseYear}} by {{.PrintArtists true true}} is licensed under <a href="{{.CopyrightURL}}" target="_blank">{{.Copyright}}</a></p>
|
||||
{{end}}
|
||||
|
||||
<button id="share">share</button>
|
||||
</div>
|
||||
|
||||
|
@ -118,7 +122,7 @@
|
|||
<div id="lyrics">
|
||||
<p class="album-track-subheading">LYRICS</p>
|
||||
{{if $Track.Lyrics}}
|
||||
{{$Track.Lyrics}}
|
||||
{{$Track.GetLyricsHTML}}
|
||||
{{else}}
|
||||
<span class="empty">No lyrics.</span>
|
||||
{{end}}
|
||||
|
@ -130,7 +134,7 @@
|
|||
<h2>TRACKS</h2>
|
||||
{{range $i, $track := .Tracks}}
|
||||
<details>
|
||||
<summary class="album-track-title">{{$track.Number}}. {{$track.Title}}</summary>
|
||||
<summary class="album-track-title">{{$track.Add $i 1}}. {{$track.Title}}</summary>
|
||||
|
||||
{{if $track.Description}}
|
||||
<p class="album-track-subheading">DESCRIPTION</p>
|
||||
|
@ -139,7 +143,7 @@
|
|||
|
||||
<p class="album-track-subheading">LYRICS</p>
|
||||
{{if $track.Lyrics}}
|
||||
{{$track.Lyrics}}
|
||||
{{$track.GetLyricsHTML}}
|
||||
{{else}}
|
||||
<span class="empty">No lyrics.</span>
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in a new issue