Compare commits
No commits in common. "main" and "fullDB" have entirely different histories.
|
@ -7,7 +7,7 @@ tmp_dir = "tmp"
|
||||||
bin = "./tmp/main"
|
bin = "./tmp/main"
|
||||||
cmd = "go build -o ./tmp/main ."
|
cmd = "go build -o ./tmp/main ."
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = ["admin/static", "public", "uploads", "test", "db"]
|
exclude_dir = ["admin\\static", "public", "uploads"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
exclude_regex = ["_test.go"]
|
exclude_regex = ["_test.go"]
|
||||||
exclude_unchanged = false
|
exclude_unchanged = false
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
**/.DS_Store
|
|
||||||
.git/
|
|
||||||
.air.toml/
|
|
||||||
.gitattributes
|
|
||||||
.gitignore
|
|
||||||
uploads/*
|
|
||||||
test/
|
|
||||||
tmp/
|
|
||||||
docker-compose.yml
|
|
||||||
Dockerfile
|
|
||||||
schema.sql
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,7 +1,5 @@
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
.idea/
|
.idea/
|
||||||
db/
|
|
||||||
tmp/
|
tmp/
|
||||||
test/
|
test/
|
||||||
uploads/*
|
uploads/*
|
||||||
docker-compose-test.yml
|
|
||||||
|
|
23
Dockerfile
23
Dockerfile
|
@ -1,23 +0,0 @@
|
||||||
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"]
|
|
28
README.md
28
README.md
|
@ -1,28 +0,0 @@
|
||||||
# arimelody.me
|
|
||||||
|
|
||||||
home to your local SPACEGIRL! 💫
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
built up from the initial [static](https://git.arimelody.me/ari/arimelody.me-static) branch, this powerful, server-side rendered version comes complete with live updates, powered by a new database and super handy admin panel!
|
|
||||||
|
|
||||||
the admin panel currently facilitates live updating of my music discography, though i plan to expand it towards art portfolio and blog posts in the future. if all goes well, i'd like to later separate these components into their own library for others to use in their own sites. exciting stuff!
|
|
||||||
|
|
||||||
## build
|
|
||||||
|
|
||||||
easy! just `git clone` this repo and `go build` from the root. `arimelody-web(.exe)` should be generated.
|
|
||||||
|
|
||||||
## running
|
|
||||||
|
|
||||||
the webserver depends on some environment variables (don't worry about forgetting some; it'll be sure to bug you about them):
|
|
||||||
|
|
||||||
- `HTTP_DOMAIN`: the domain the webserver will use for generating oauth redirect URIs (default `https://arimelody.me`)
|
|
||||||
- `DISCORD_ADMIN`[^1]: the user ID of your discord account (discord auth is intended to be temporary, and will be replaced with its own auth system later)
|
|
||||||
- `DISCORD_CLIENT`[^1]: the client ID of your discord OAuth application.
|
|
||||||
- `DISCORD_SECRET`[^1]: the client secret of your discord OAuth application.
|
|
||||||
|
|
||||||
[^1]: not required, but the admin panel will be **disabled** if these are not provided.
|
|
||||||
|
|
||||||
the webserver requires a database to run. in this case, postgres.
|
|
||||||
|
|
||||||
the [docker compose script](docker-compose.yml) contains the basic requirements to get you up and running, though it does not currently initialise the schema on first run. you'll need to `docker compose exec -it arimelody.me-db-1` to access the database container while it's running, run `psql -U arimelody` to get a postgres shell, and copy/paste the contents of [schema.sql](schema.sql) to initialise the database. i'll build an automated initialisation script later ;p
|
|
|
@ -3,10 +3,9 @@ package admin
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/global"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -29,9 +28,9 @@ var ADMIN_BYPASS = func() bool {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var ADMIN_ID_DISCORD = func() string {
|
var ADMIN_ID_DISCORD = func() string {
|
||||||
id := os.Getenv("DISCORD_ADMIN")
|
id := global.Args["discordAdmin"]
|
||||||
if id == "" {
|
if id == "" {
|
||||||
fmt.Printf("WARN: Discord admin ID (DISCORD_ADMIN) was not provided. Admin login will be unavailable.\n")
|
fmt.Printf("WARN: Discord admin ID (-discordAdmin) was not provided. Admin login will be unavailable.\n")
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
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, true)
|
|
||||||
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,7 +61,6 @@
|
||||||
el.remove();
|
el.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.draggable = true;
|
|
||||||
el.addEventListener("dragstart", () => { el.classList.add("moving") });
|
el.addEventListener("dragstart", () => { el.classList.add("moving") });
|
||||||
el.addEventListener("dragend", () => { el.classList.remove("moving") });
|
el.addEventListener("dragend", () => { el.classList.remove("moving") });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{{define "release"}}
|
{{define "release"}}
|
||||||
<div class="release">
|
<div class="release">
|
||||||
<div class="release-artwork">
|
<div class="release-artwork">
|
||||||
<img src="{{.GetArtwork}}" alt="" width="128" loading="lazy">
|
<img src="{{.Artwork}}" alt="" width="128" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<div class="release-info">
|
<div class="release-info">
|
||||||
<h3 class="release-title">
|
<h3 class="release-title">
|
||||||
|
|
|
@ -3,16 +3,17 @@ package admin
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody-web/discord"
|
"arimelody.me/arimelody.me/discord"
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/global"
|
||||||
musicDB "arimelody-web/music/controller"
|
musicModel "arimelody.me/arimelody.me/music/model"
|
||||||
musicModel "arimelody-web/music/model"
|
musicDB "arimelody.me/arimelody.me/music/controller"
|
||||||
)
|
)
|
||||||
|
|
||||||
type loginData struct {
|
type loginData struct {
|
||||||
|
@ -27,7 +28,6 @@ func Handler() http.Handler {
|
||||||
mux.Handle("/logout", MustAuthorise(LogoutHandler()))
|
mux.Handle("/logout", MustAuthorise(LogoutHandler()))
|
||||||
mux.Handle("/static/", http.StripPrefix("/static", staticHandler()))
|
mux.Handle("/static/", http.StripPrefix("/static", staticHandler()))
|
||||||
mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease())))
|
mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease())))
|
||||||
mux.Handle("/artist/", MustAuthorise(http.StripPrefix("/artist", serveArtist())))
|
|
||||||
mux.Handle("/track/", MustAuthorise(http.StripPrefix("/track", serveTrack())))
|
mux.Handle("/track/", MustAuthorise(http.StripPrefix("/track", serveTrack())))
|
||||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
|
@ -41,12 +41,30 @@ func Handler() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
releases, err := musicDB.GetAllReleases(global.DB, false, 0, true)
|
type (
|
||||||
|
IndexData struct {
|
||||||
|
Releases []musicModel.FullRelease
|
||||||
|
Artists []*musicModel.Artist
|
||||||
|
Tracks []musicModel.DisplayTrack
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dbReleases, err := musicDB.GetAllReleases(global.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to pull releases: %s\n", err)
|
fmt.Printf("FATAL: Failed to pull releases: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
releases := []musicModel.FullRelease{}
|
||||||
|
for _, release := range dbReleases {
|
||||||
|
fullRelease, err := musicDB.GetFullRelease(global.DB, release)
|
||||||
|
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)
|
artists, err := musicDB.GetAllArtists(global.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -55,17 +73,19 @@ func Handler() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks, err := musicDB.GetOrphanTracks(global.DB)
|
dbTracks, err := musicDB.GetOrphanTracks(global.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to pull orphan tracks: %s\n", err)
|
fmt.Printf("FATAL: Failed to pull orphan tracks: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type IndexData struct {
|
var tracks = []musicModel.DisplayTrack{}
|
||||||
Releases []*musicModel.Release
|
for _, track := range dbTracks {
|
||||||
Artists []*musicModel.Artist
|
tracks = append(tracks, musicModel.DisplayTrack{
|
||||||
Tracks []*musicModel.Track
|
Track: track,
|
||||||
|
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pages["index"].Execute(w, IndexData{
|
err = pages["index"].Execute(w, IndexData{
|
||||||
|
@ -105,9 +125,8 @@ func GetSession(r *http.Request) *Session {
|
||||||
// is the session token in context?
|
// is the session token in context?
|
||||||
var ctx_session = r.Context().Value("session")
|
var ctx_session = r.Context().Value("session")
|
||||||
if ctx_session != nil {
|
if ctx_session != nil {
|
||||||
token = ctx_session.(*Session).Token
|
token = ctx_session.(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// okay, is it in the auth header?
|
// okay, is it in the auth header?
|
||||||
if token == "" {
|
if token == "" {
|
||||||
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
||||||
|
@ -149,15 +168,11 @@ func GetSession(r *http.Request) *Session {
|
||||||
|
|
||||||
func LoginHandler() http.Handler {
|
func LoginHandler() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(discord.CLIENT_ID)
|
|
||||||
fmt.Println(discord.API_ENDPOINT)
|
|
||||||
fmt.Println(discord.REDIRECT_URI)
|
|
||||||
|
|
||||||
code := r.URL.Query().Get("code")
|
code := r.URL.Query().Get("code")
|
||||||
|
|
||||||
if code == "" {
|
if code == "" {
|
||||||
|
@ -194,9 +209,8 @@ func LoginHandler() http.Handler {
|
||||||
cookie.Name = "token"
|
cookie.Name = "token"
|
||||||
cookie.Value = session.Token
|
cookie.Value = session.Token
|
||||||
cookie.Expires = time.Now().Add(24 * time.Hour)
|
cookie.Expires = time.Now().Add(24 * time.Hour)
|
||||||
if strings.HasPrefix(global.HTTP_DOMAIN, "https") {
|
// TODO: uncomment this probably that might be nice i think
|
||||||
cookie.Secure = true
|
// cookie.Secure = true
|
||||||
}
|
|
||||||
cookie.HttpOnly = true
|
cookie.HttpOnly = true
|
||||||
cookie.Path = "/"
|
cookie.Path = "/"
|
||||||
http.SetCookie(w, &cookie)
|
http.SetCookie(w, &cookie)
|
||||||
|
|
|
@ -5,24 +5,23 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/global"
|
||||||
db "arimelody-web/music/controller"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
"arimelody-web/music/model"
|
controller "arimelody.me/arimelody.me/music/controller"
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveRelease() http.Handler {
|
func serveRelease() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
slices := strings.Split(r.URL.Path[1:], "/")
|
slices := strings.Split(r.URL.Path[1:], "/")
|
||||||
releaseID := slices[0]
|
releaseID := slices[0]
|
||||||
|
release, err := controller.GetRelease(global.DB, releaseID)
|
||||||
release, err := db.GetRelease(global.DB, releaseID, true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
fmt.Printf("FATAL: Failed to pull release %s: %s\n", releaseID, err)
|
||||||
http.NotFound(w, r)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", releaseID, err)
|
if release == nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,25 +31,32 @@ func serveRelease() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fullRelease, err := controller.GetFullRelease(global.DB, release)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
if len(slices) > 1 {
|
if len(slices) > 1 {
|
||||||
switch slices[1] {
|
switch slices[1] {
|
||||||
case "editcredits":
|
case "editcredits":
|
||||||
serveEditCredits(release).ServeHTTP(w, r)
|
serveEditCredits(fullRelease).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "addcredit":
|
case "addcredit":
|
||||||
serveAddCredit(release).ServeHTTP(w, r)
|
serveAddCredit(fullRelease).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "newcredit":
|
case "newcredit":
|
||||||
serveNewCredit().ServeHTTP(w, r)
|
serveNewCredit().ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "editlinks":
|
case "editlinks":
|
||||||
serveEditLinks(release).ServeHTTP(w, r)
|
serveEditLinks(fullRelease).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "edittracks":
|
case "edittracks":
|
||||||
serveEditTracks(release).ServeHTTP(w, r)
|
serveEditTracks(fullRelease).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "addtrack":
|
case "addtrack":
|
||||||
serveAddTrack(release).ServeHTTP(w, r)
|
serveAddTrack(fullRelease).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "newtrack":
|
case "newtrack":
|
||||||
serveNewTrack().ServeHTTP(w, r)
|
serveNewTrack().ServeHTTP(w, r)
|
||||||
|
@ -60,7 +66,7 @@ func serveRelease() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pages["release"].Execute(w, release)
|
err = pages["release"].Execute(w, fullRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error rendering admin release page for %s: %s\n", release.ID, err)
|
fmt.Printf("Error rendering admin release page 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)
|
||||||
|
@ -68,7 +74,7 @@ func serveRelease() http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveEditCredits(release *model.Release) http.Handler {
|
func serveEditCredits(release *model.FullRelease) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
err := components["editcredits"].Execute(w, release)
|
err := components["editcredits"].Execute(w, release)
|
||||||
|
@ -79,9 +85,9 @@ func serveEditCredits(release *model.Release) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveAddCredit(release *model.Release) http.Handler {
|
func serveAddCredit(release *model.FullRelease) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
artists, err := db.GetArtistsNotOnRelease(global.DB, release.ID)
|
artists, err := controller.GetArtistsNotOnRelease(global.DB, release.Release)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to pull artists not on %s: %s\n", release.ID, err)
|
fmt.Printf("FATAL: Failed to pull artists not on %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
@ -108,7 +114,7 @@ func serveAddCredit(release *model.Release) http.Handler {
|
||||||
func serveNewCredit() http.Handler {
|
func serveNewCredit() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
artistID := strings.Split(r.URL.Path, "/")[3]
|
artistID := strings.Split(r.URL.Path, "/")[3]
|
||||||
artist, err := db.GetArtist(global.DB, artistID)
|
artist, err := controller.GetArtist(global.DB, artistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to pull artists %s: %s\n", artistID, err)
|
fmt.Printf("FATAL: Failed to pull artists %s: %s\n", artistID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
@ -128,7 +134,7 @@ func serveNewCredit() http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveEditLinks(release *model.Release) http.Handler {
|
func serveEditLinks(release *model.FullRelease) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
err := components["editlinks"].Execute(w, release)
|
err := components["editlinks"].Execute(w, release)
|
||||||
|
@ -139,7 +145,7 @@ func serveEditLinks(release *model.Release) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveEditTracks(release *model.Release) http.Handler {
|
func serveEditTracks(release *model.FullRelease) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
err := components["edittracks"].Execute(w, release)
|
err := components["edittracks"].Execute(w, release)
|
||||||
|
@ -150,9 +156,9 @@ func serveEditTracks(release *model.Release) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveAddTrack(release *model.Release) http.Handler {
|
func serveAddTrack(release *model.FullRelease) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
tracks, err := db.GetTracksNotOnRelease(global.DB, release.ID)
|
tracks, err := controller.GetTracksNotOnRelease(global.DB, release.Release)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to pull tracks not on %s: %s\n", release.ID, err)
|
fmt.Printf("FATAL: Failed to pull tracks not on %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
@ -180,7 +186,7 @@ func serveAddTrack(release *model.Release) http.Handler {
|
||||||
func serveNewTrack() http.Handler {
|
func serveNewTrack() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
trackID := strings.Split(r.URL.Path, "/")[3]
|
trackID := strings.Split(r.URL.Path, "/")[3]
|
||||||
track, err := db.GetTrack(global.DB, trackID)
|
track, err := controller.GetTrack(global.DB, trackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error rendering new track component for %s: %s\n", trackID, err)
|
fmt.Printf("Error rendering new track component for %s: %s\n", trackID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
|
@ -1,151 +0,0 @@
|
||||||
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: 64px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credit-info h3,
|
|
||||||
.credit-info p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: .9em;
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
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;
|
|
||||||
});
|
|
|
@ -18,8 +18,8 @@ input[type="text"] {
|
||||||
|
|
||||||
.release-artwork {
|
.release-artwork {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-artwork img {
|
.release-artwork img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
|
@ -28,9 +28,6 @@ input[type="text"] {
|
||||||
outline: 1px solid #808080;
|
outline: 1px solid #808080;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.release-artwork #remove-artwork {
|
|
||||||
padding: .3em .4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-info {
|
.release-info {
|
||||||
width: 0;
|
width: 0;
|
||||||
|
@ -345,7 +342,7 @@ dialog div.dialog-actions {
|
||||||
background-color: #8cff83
|
background-color: #8cff83
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.links a.button[data-name="apple music"] {
|
.card.links a.button[data-name="applemusic"] {
|
||||||
background-color: #8cd9ff
|
background-color: #8cd9ff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -551,14 +548,3 @@ dialog div.dialog-actions {
|
||||||
#addtrack ul li.new-track:hover {
|
#addtrack ul li.new-track:hover {
|
||||||
background: #e0e0e0;
|
background: #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 1105px) {
|
|
||||||
#release {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-info {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,36 +1,47 @@
|
||||||
|
import Stateful from "/script/silver.min.js"
|
||||||
|
|
||||||
const releaseID = document.getElementById("release").dataset.id;
|
const releaseID = document.getElementById("release").dataset.id;
|
||||||
const titleInput = document.getElementById("title");
|
const titleInput = document.getElementById("title");
|
||||||
const artworkImg = document.getElementById("artwork");
|
const artworkImg = document.getElementById("artwork");
|
||||||
const removeArtworkBtn = document.getElementById("remove-artwork");
|
|
||||||
const artworkInput = document.getElementById("artwork-file");
|
const artworkInput = document.getElementById("artwork-file");
|
||||||
const typeInput = document.getElementById("type");
|
const typeInput = document.getElementById("type");
|
||||||
const descInput = document.getElementById("description");
|
const descInput = document.getElementById("description");
|
||||||
const dateInput = document.getElementById("release-date");
|
const dateInput = document.getElementById("release-date");
|
||||||
const buynameInput = document.getElementById("buyname");
|
const buynameInput = document.getElementById("buyname");
|
||||||
const buylinkInput = document.getElementById("buylink");
|
const buylinkInput = document.getElementById("buylink");
|
||||||
const copyrightInput = document.getElementById("copyright");
|
|
||||||
const copyrightURLInput = document.getElementById("copyright-url");
|
|
||||||
const visInput = document.getElementById("visibility");
|
const visInput = document.getElementById("visibility");
|
||||||
const saveBtn = document.getElementById("save");
|
const saveBtn = document.getElementById("save");
|
||||||
const deleteBtn = document.getElementById("delete");
|
const deleteBtn = document.getElementById("delete");
|
||||||
|
|
||||||
var artworkData = artworkImg.attributes.src.value;
|
var artworkData = artworkImg.attributes.src.value;
|
||||||
|
var edited = new Stateful(false);
|
||||||
|
var releaseData = updateData(undefined);
|
||||||
|
|
||||||
saveBtn.addEventListener("click", () => {
|
function updateData(old) {
|
||||||
fetch("/api/v1/music/" + releaseID, {
|
var releaseData = {
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify({
|
|
||||||
visible: visInput.value === "true",
|
visible: visInput.value === "true",
|
||||||
title: titleInput.value,
|
title: titleInput.value,
|
||||||
description: descInput.value,
|
description: descInput.value,
|
||||||
type: typeInput.value,
|
type: typeInput.value,
|
||||||
releaseDate: dateInput.value + ":00Z",
|
releaseDate: dateInput.value,
|
||||||
artwork: artworkData,
|
artwork: artworkData,
|
||||||
buyname: buynameInput.value,
|
buyname: buynameInput.value,
|
||||||
buylink: buylinkInput.value,
|
buylink: buylinkInput.value,
|
||||||
copyright: copyrightInput.value,
|
};
|
||||||
copyrightURL: copyrightURLInput.value,
|
|
||||||
}),
|
if (releaseData && releaseData != old) {
|
||||||
|
edited.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return releaseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRelease() {
|
||||||
|
console.table(releaseData);
|
||||||
|
|
||||||
|
fetch("/api/v1/music/" + releaseID, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(releaseData),
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
@ -43,13 +54,9 @@ saveBtn.addEventListener("click", () => {
|
||||||
|
|
||||||
location = location;
|
location = location;
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
deleteBtn.addEventListener("click", () => {
|
function deleteRelease() {
|
||||||
if (releaseID != prompt(
|
|
||||||
"You are about to permanently delete " + releaseID + ". " +
|
|
||||||
"This action is irreversible. " +
|
|
||||||
"Please enter \"" + releaseID + "\" to continue.")) return;
|
|
||||||
fetch("/api/v1/music/" + releaseID, {
|
fetch("/api/v1/music/" + releaseID, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
|
@ -63,17 +70,15 @@ deleteBtn.addEventListener("click", () => {
|
||||||
|
|
||||||
location = "/admin";
|
location = "/admin";
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
[titleInput, typeInput, descInput, dateInput, buynameInput, buylinkInput, copyrightInput, copyrightURLInput, visInput].forEach(input => {
|
edited.onUpdate(edited => {
|
||||||
input.addEventListener("change", () => {
|
saveBtn.disabled = !edited;
|
||||||
saveBtn.disabled = false;
|
})
|
||||||
});
|
|
||||||
input.addEventListener("keypress", () => {
|
|
||||||
saveBtn.disabled = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
titleInput.addEventListener("change", () => {
|
||||||
|
releaseData = updateData(releaseData);
|
||||||
|
});
|
||||||
artworkImg.addEventListener("click", () => {
|
artworkImg.addEventListener("click", () => {
|
||||||
artworkInput.addEventListener("change", () => {
|
artworkInput.addEventListener("change", () => {
|
||||||
if (artworkInput.files.length > 0) {
|
if (artworkInput.files.length > 0) {
|
||||||
|
@ -82,17 +87,41 @@ artworkImg.addEventListener("click", () => {
|
||||||
const data = e.target.result;
|
const data = e.target.result;
|
||||||
artworkImg.src = data;
|
artworkImg.src = data;
|
||||||
artworkData = data;
|
artworkData = data;
|
||||||
saveBtn.disabled = false;
|
releaseData = updateData(releaseData);
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(artworkInput.files[0]);
|
reader.readAsDataURL(artworkInput.files[0]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
artworkInput.click();
|
artworkInput.click();
|
||||||
});
|
});
|
||||||
|
typeInput.addEventListener("change", () => {
|
||||||
|
releaseData = updateData(releaseData);
|
||||||
removeArtworkBtn.addEventListener("click", () => {
|
});
|
||||||
artworkImg.src = "/img/default-cover-art.png"
|
descInput.addEventListener("change", () => {
|
||||||
artworkData = "";
|
releaseData = updateData(releaseData);
|
||||||
saveBtn.disabled = false;
|
});
|
||||||
|
dateInput.addEventListener("change", () => {
|
||||||
|
releaseData = updateData(releaseData);
|
||||||
|
});
|
||||||
|
buynameInput.addEventListener("change", () => {
|
||||||
|
releaseData = updateData(releaseData);
|
||||||
|
});
|
||||||
|
buylinkInput.addEventListener("change", () => {
|
||||||
|
releaseData = updateData(releaseData);
|
||||||
|
});
|
||||||
|
visInput.addEventListener("change", () => {
|
||||||
|
releaseData = updateData(releaseData);
|
||||||
|
});
|
||||||
|
|
||||||
|
saveBtn.addEventListener("click", () => {
|
||||||
|
if (!edited.get()) return;
|
||||||
|
saveRelease();
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteBtn.addEventListener("click", () => {
|
||||||
|
if (releaseID != prompt(
|
||||||
|
"You are about to permanently delete " + releaseID + ". " +
|
||||||
|
"This action is irreversible. " +
|
||||||
|
"Please enter \"" + releaseID + "\" to continue.")) return;
|
||||||
|
deleteRelease();
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ h1 {
|
||||||
|
|
||||||
#track {
|
#track {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
padding: .5em 1.5em 1.5em 1.5em;
|
padding: 1.5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 1.2em;
|
gap: 1.2em;
|
||||||
|
@ -34,7 +34,7 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
#title {
|
#title {
|
||||||
width: calc(100% - .4em);
|
width: 100%;
|
||||||
padding: .1em .2em;
|
padding: .1em .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
const newReleaseBtn = document.getElementById("create-release");
|
const newReleaseBtn = document.getElementById("create-release");
|
||||||
const newArtistBtn = document.getElementById("create-artist");
|
|
||||||
const newTrackBtn = document.getElementById("create-track");
|
const newTrackBtn = document.getElementById("create-track");
|
||||||
|
|
||||||
newReleaseBtn.addEventListener("click", event => {
|
newReleaseBtn.addEventListener("click", event => {
|
||||||
|
@ -25,30 +24,6 @@ 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 => {
|
newTrackBtn.addEventListener("click", event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const title = prompt("Enter an title for this track:");
|
const title = prompt("Enter an title for this track:");
|
||||||
|
@ -68,7 +43,7 @@ newTrackBtn.addEventListener("click", event => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
alert("Failed to create track. Check the console for details.");
|
alert("Failed to create release. Check the console for details.");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,11 +29,6 @@ var pages = map[string]*template.Template{
|
||||||
filepath.Join("views", "prideflag.html"),
|
filepath.Join("views", "prideflag.html"),
|
||||||
filepath.Join("admin", "views", "edit-release.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(
|
"track": template.Must(template.ParseFiles(
|
||||||
filepath.Join("admin", "views", "layout.html"),
|
filepath.Join("admin", "views", "layout.html"),
|
||||||
filepath.Join("views", "prideflag.html"),
|
filepath.Join("views", "prideflag.html"),
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/global"
|
||||||
"arimelody-web/music/model"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
"arimelody-web/music/controller"
|
"arimelody.me/arimelody.me/music/controller"
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveTrack() http.Handler {
|
func serveTrack() http.Handler {
|
||||||
|
@ -25,16 +25,26 @@ func serveTrack() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
releases, err := music.GetTrackReleases(global.DB, track.ID, true)
|
dbReleases, err := music.GetTrackReleases(global.DB, track)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to pull releases for %s: %s\n", id, err)
|
fmt.Printf("Error rendering admin track page for %s: %s\n", id, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
releases := []model.FullRelease{}
|
||||||
|
for _, release := range dbReleases {
|
||||||
|
fullRelease, err := music.GetFullRelease(global.DB, release)
|
||||||
|
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 {
|
type Track struct {
|
||||||
*model.Track
|
*model.Track
|
||||||
Releases []*model.Release
|
Releases []model.FullRelease
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pages["track"].Execute(w, Track{ Track: track, Releases: releases })
|
err = pages["track"].Execute(w, Track{ Track: track, Releases: releases })
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
{{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}}
|
|
|
@ -12,11 +12,10 @@
|
||||||
<div class="release-artwork">
|
<div class="release-artwork">
|
||||||
<img src="{{.Artwork}}" alt="" width="256" loading="lazy" id="artwork">
|
<img src="{{.Artwork}}" alt="" width="256" loading="lazy" id="artwork">
|
||||||
<input type="file" id="artwork-file" name="Artwork" accept=".png,.jpg,.jpeg" hidden>
|
<input type="file" id="artwork-file" name="Artwork" accept=".png,.jpg,.jpeg" hidden>
|
||||||
<button id="remove-artwork">Remove</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="release-info">
|
<div class="release-info">
|
||||||
<h1 class="release-title">
|
<h1 class="release-title">
|
||||||
<input type="text" id="title" name="Title" value="{{.Title}}" autocomplete="on">
|
<input type="text" id="title" name="Title" value="{{.Title}}">
|
||||||
</h1>
|
</h1>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -54,31 +53,19 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>Release Date</td>
|
<td>Release Date</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="datetime-local" name="release-date" id="release-date" value="{{.TextReleaseDate}}">
|
<input type="datetime-local" name="Release Date" id="release-date" value="{{.TextReleaseDate}}">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Buy Name</td>
|
<td>Buy Name</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="buyname" id="buyname" value="{{.Buyname}}" autocomplete="on">
|
<input type="text" name="Buy Name" id="buyname" value="{{.Buyname}}">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Buy Link</td>
|
<td>Buy Link</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="buylink" id="buylink" value="{{.Buylink}}" autocomplete="on">
|
<input type="text" name="Buy Link" id="buylink" value="{{.Buylink}}">
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Copyright</td>
|
|
||||||
<td>
|
|
||||||
<input type="text" name="copyright" id="copyright" value="{{.Copyright}}" autocomplete="on">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Copyright URL</td>
|
|
||||||
<td>
|
|
||||||
<input type="text" name="copyright-url" id="copyright-url" value="{{.CopyrightURL}}" autocomplete="on">
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -112,7 +99,7 @@
|
||||||
<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">
|
||||||
<div class="credit-info">
|
<div class="credit-info">
|
||||||
<p class="artist-name"><a href="/admin/artist/{{.Artist.ID}}">{{.Artist.Name}}</a></p>
|
<p class="artist-name"><a href="/admin/artists/{{.Artist.ID}}">{{.Artist.Name}}</a></p>
|
||||||
<p class="artist-role">
|
<p class="artist-role">
|
||||||
{{.Role}}
|
{{.Role}}
|
||||||
{{if .Primary}}
|
{{if .Primary}}
|
||||||
|
@ -152,23 +139,23 @@
|
||||||
>Edit</a>
|
>Edit</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card tracks">
|
<div class="card tracks">
|
||||||
{{range $i, $track := .Tracks}}
|
{{range .Tracks}}
|
||||||
<div class="track" data-id="{{$track.ID}}">
|
<div class="track" data-id="{{.ID}}">
|
||||||
<h2 class="track-title">
|
<h2 class="track-title">
|
||||||
<span class="track-number">{{.Add $i 1}}</span>
|
<span class="track-number">{{.Number}}</span>
|
||||||
<a href="/admin/track/{{$track.ID}}">{{$track.Title}}</a>
|
<a href="/admin/track/{{.ID}}">{{.Title}}</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
{{if $track.Description}}
|
{{if .Description}}
|
||||||
<p class="track-description">{{$track.GetDescriptionHTML}}</p>
|
<p class="track-description">{{.Description}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="track-description empty">No description provided.</p>
|
<p class="track-description empty">No description provided.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<h3>Lyrics</h3>
|
<h3>Lyrics</h3>
|
||||||
{{if $track.Lyrics}}
|
{{if .Lyrics}}
|
||||||
<p class="track-lyrics">{{$track.GetLyricsHTML}}</p>
|
<p class="track-lyrics">{{.Lyrics}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="track-lyrics empty">There are no lyrics.</p>
|
<p class="track-lyrics empty">There are no lyrics.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -22,13 +22,13 @@
|
||||||
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h1>Artists</h1>
|
<h1>Artists</h1>
|
||||||
<a class="create-btn" id="create-artist">Create New</a>
|
<a class="create-btn">Create New</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card artists">
|
<div class="card artists">
|
||||||
{{range $Artist := .Artists}}
|
{{range $Artist := .Artists}}
|
||||||
<div class="artist">
|
<div class="artist">
|
||||||
<img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
<img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
||||||
<a href="/admin/artist/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a>
|
<a href="/admin/artists/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not .Artists}}
|
{{if not .Artists}}
|
||||||
|
@ -49,12 +49,12 @@
|
||||||
<a href="/admin/track/{{$Track.ID}}">{{$Track.Title}}</a>
|
<a href="/admin/track/{{$Track.ID}}">{{$Track.Title}}</a>
|
||||||
</h2>
|
</h2>
|
||||||
{{if $Track.Description}}
|
{{if $Track.Description}}
|
||||||
<p class="track-description">{{$Track.GetDescriptionHTML}}</p>
|
<p class="track-description">{{$Track.Description}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="track-description empty">No description provided.</p>
|
<p class="track-description empty">No description provided.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if $Track.Lyrics}}
|
{{if $Track.Lyrics}}
|
||||||
<p class="track-lyrics">{{$Track.GetLyricsHTML}}</p>
|
<p class="track-lyrics">{{$Track.Lyrics}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="track-lyrics empty">There are no lyrics.</p>
|
<p class="track-lyrics empty">There are no lyrics.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
37
api/api.go
37
api/api.go
|
@ -5,10 +5,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody-web/admin"
|
"arimelody.me/arimelody.me/admin"
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/global"
|
||||||
music "arimelody-web/music/controller"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
musicView "arimelody-web/music/view"
|
music "arimelody.me/arimelody.me/music/view"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Handler() http.Handler {
|
func Handler() http.Handler {
|
||||||
|
@ -18,14 +18,11 @@ func Handler() http.Handler {
|
||||||
|
|
||||||
mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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 artistID = strings.Split(r.URL.Path[1:], "/")[0]
|
||||||
artist, err := music.GetArtist(global.DB, artistID)
|
var artist model.Artist
|
||||||
|
err := global.DB.Get(&artist, "SELECT * FROM artist WHERE id=$1", artistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("FATAL: Error while retrieving artist %s: %s\n", artistID, err)
|
fmt.Printf("FATAL: Error while retrieving artist %s: %s\n", artistID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,21 +57,18 @@ func Handler() http.Handler {
|
||||||
|
|
||||||
mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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 releaseID = strings.Split(r.URL.Path[1:], "/")[0]
|
||||||
release, err := music.GetRelease(global.DB, releaseID, true)
|
var release model.Release
|
||||||
|
err := global.DB.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", releaseID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("FATAL: Error while retrieving release %s: %s\n", releaseID, err)
|
fmt.Printf("FATAL: Error while retrieving release %s: %s\n", releaseID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
// GET /api/v1/music/{id}
|
// GET /api/v1/music/{id}
|
||||||
musicView.ServeRelease(release).ServeHTTP(w, r)
|
music.ServeRelease(release).ServeHTTP(w, r)
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
// PUT /api/v1/music/{id} (admin)
|
// PUT /api/v1/music/{id} (admin)
|
||||||
admin.MustAuthorise(UpdateRelease(release)).ServeHTTP(w, r)
|
admin.MustAuthorise(UpdateRelease(release)).ServeHTTP(w, r)
|
||||||
|
@ -102,14 +96,11 @@ func Handler() http.Handler {
|
||||||
|
|
||||||
mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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 trackID = strings.Split(r.URL.Path[1:], "/")[0]
|
||||||
track, err := music.GetTrack(global.DB, trackID)
|
var track model.Track
|
||||||
|
err := global.DB.Get(&track, "SELECT * FROM musictrack WHERE id=$1", trackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("FATAL: Error while retrieving track %s: %s\n", trackID, err)
|
fmt.Printf("FATAL: Error while retrieving track %s: %s\n", trackID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
122
api/artist.go
122
api/artist.go
|
@ -3,23 +3,24 @@ package api
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody-web/admin"
|
"arimelody.me/arimelody.me/global"
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
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 {
|
func ServeAllArtists() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var artists = []*model.Artist{}
|
var artists = []*model.Artist{}
|
||||||
artists, err := db.GetAllArtists(global.DB)
|
err := global.DB.Select(&artists, "SELECT * FROM artist")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to serve all artists: %s\n", err)
|
fmt.Printf("FATAL: Failed to serve all artists: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
@ -34,37 +35,28 @@ 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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
type (
|
type (
|
||||||
creditJSON struct {
|
creditJSON struct {
|
||||||
|
Release string `json:"release"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Primary bool `json:"primary"`
|
Primary bool `json:"primary"`
|
||||||
}
|
}
|
||||||
artistJSON struct {
|
artistJSON struct {
|
||||||
*model.Artist
|
model.Artist
|
||||||
Credits map[string]creditJSON `json:"credits"`
|
Credits map[string]creditJSON `json:"credits"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
show_hidden_releases := admin.GetSession(r) != nil
|
var credits = map[string]creditJSON{}
|
||||||
|
err := global.DB.Select(&credits, "SELECT release,role,is_primary FROM musiccredit WHERE id=$1", artist.ID)
|
||||||
var dbCredits []*model.Credit
|
|
||||||
dbCredits, err := db.GetArtistCredits(global.DB, artist.ID, show_hidden_releases)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to retrieve artist credits for %s: %s\n", artist.ID, err)
|
fmt.Printf("FATAL: Failed to retrieve artist credits for %s: %s\n", artist.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var credits = map[string]creditJSON{}
|
|
||||||
for _, credit := range dbCredits {
|
|
||||||
credits[credit.Release.ID] = creditJSON{
|
|
||||||
Role: credit.Role,
|
|
||||||
Primary: credit.Primary,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
err = json.NewEncoder(w).Encode(artistJSON{
|
err = json.NewEncoder(w).Encode(artistJSON{
|
||||||
Artist: artist,
|
Artist: artist,
|
||||||
|
@ -78,23 +70,39 @@ func ServeArtist(artist *model.Artist) http.Handler {
|
||||||
|
|
||||||
func CreateArtist() http.Handler {
|
func CreateArtist() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var artist model.Artist
|
var data artistJSON
|
||||||
err := json.NewDecoder(r.Body).Decode(&artist)
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if artist.ID == "" {
|
if data.ID == "" {
|
||||||
http.Error(w, "Artist ID cannot be blank\n", http.StatusBadRequest)
|
http.Error(w, "Artist ID cannot be blank\n", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if artist.Name == "" { artist.Name = artist.ID }
|
if data.Name == nil || *data.Name == "" {
|
||||||
|
http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = music.CreateArtist(global.DB, &artist)
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Artist %s already exists\n", data.ID), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("FATAL: Failed to create artist %s: %s\n", artist.ID, err)
|
fmt.Printf("FATAL: Failed to create artist %s: %s\n", artist.ID, err)
|
||||||
|
@ -106,59 +114,43 @@ 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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
err := json.NewDecoder(r.Body).Decode(&artist)
|
var data artistJSON
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to update artist: %s\n", err)
|
fmt.Printf("FATAL: Failed to update artist: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if artist.Avatar == "" {
|
if data.ID != "" { artist.ID = data.ID }
|
||||||
artist.Avatar = "/img/default-avatar.png"
|
if data.Name != nil { artist.Name = *data.Name }
|
||||||
} else {
|
if data.Website != nil { artist.Website = *data.Website }
|
||||||
if strings.Contains(artist.Avatar, ";base64,") {
|
if data.Avatar != nil { artist.Avatar = *data.Avatar }
|
||||||
var artworkDirectory = filepath.Join("uploads", "avatar")
|
|
||||||
filename, err := HandleImageUpload(&artist.Avatar, artworkDirectory, artist.ID)
|
|
||||||
|
|
||||||
// clean up files with this ID and different extensions
|
_, err = global.DB.Exec(
|
||||||
err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error {
|
"UPDATE artist "+
|
||||||
if path == filepath.Join(artworkDirectory, filename) { return nil }
|
"SET name=$2, website=$3, avatar=$4 "+
|
||||||
|
"WHERE id=$1",
|
||||||
withoutExt := strings.TrimSuffix(path, filepath.Ext(path))
|
artist.ID,
|
||||||
if withoutExt != filepath.Join(artworkDirectory, artist.ID) { return nil }
|
artist.Name,
|
||||||
|
artist.Website,
|
||||||
return os.Remove(path)
|
artist.Avatar)
|
||||||
})
|
|
||||||
if err != nil {
|
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)
|
fmt.Printf("FATAL: Failed to update artist %s: %s\n", artist.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteArtist(artist *model.Artist) http.Handler {
|
func DeleteArtist(artist model.Artist) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
err := music.DeleteArtist(global.DB, artist.ID)
|
_, err := global.DB.Exec(
|
||||||
|
"DELETE FROM artist "+
|
||||||
|
"WHERE id=$1",
|
||||||
|
artist.ID)
|
||||||
if err != nil {
|
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)
|
fmt.Printf("FATAL: Failed to delete artist %s: %s\n", artist.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
287
api/release.go
287
api/release.go
|
@ -10,51 +10,54 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody-web/admin"
|
"arimelody.me/arimelody.me/admin"
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/global"
|
||||||
music "arimelody-web/music/controller"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
"arimelody-web/music/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type releaseBodyJSON struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Visible *bool `json:"visible"`
|
||||||
|
Title *string `json:"title"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
ReleaseType *model.ReleaseType `json:"type"`
|
||||||
|
ReleaseDate *string `json:"releaseDate"`
|
||||||
|
Artwork *string `json:"artwork"`
|
||||||
|
Buyname *string `json:"buyname"`
|
||||||
|
Buylink *string `json:"buylink"`
|
||||||
|
}
|
||||||
|
|
||||||
func ServeCatalog() http.Handler {
|
func ServeCatalog() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
releases, err := music.GetAllReleases(global.DB, false, 0, true)
|
type catalogItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
ReleaseType model.ReleaseType `json:"type"`
|
||||||
|
ReleaseDate time.Time `json:"releaseDate"`
|
||||||
|
Artwork string `json:"artwork"`
|
||||||
|
Buylink string `json:"buylink"`
|
||||||
|
}
|
||||||
|
|
||||||
|
releases := []*model.Release{}
|
||||||
|
err := global.DB.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type Release struct {
|
catalog := []catalogItem{}
|
||||||
ID string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Artists []string `json:"artists"`
|
|
||||||
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
|
authorised := admin.GetSession(r) != nil
|
||||||
for _, release := range releases {
|
for _, release := range releases {
|
||||||
if !release.Visible && !authorised {
|
if !release.Visible && !authorised {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
artists := []string{}
|
catalog = append(catalog, catalogItem{
|
||||||
for _, credit := range release.Credits {
|
|
||||||
if !credit.Primary { continue }
|
|
||||||
artists = append(artists, credit.Artist.Name)
|
|
||||||
}
|
|
||||||
catalog = append(catalog, Release{
|
|
||||||
ID: release.ID,
|
ID: release.ID,
|
||||||
Title: release.Title,
|
Title: release.Title,
|
||||||
Artists: artists,
|
|
||||||
ReleaseType: release.ReleaseType,
|
ReleaseType: release.ReleaseType,
|
||||||
ReleaseDate: release.ReleaseDate,
|
ReleaseDate: release.ReleaseDate,
|
||||||
Artwork: release.Artwork,
|
Artwork: release.Artwork,
|
||||||
Buylink: release.Buylink,
|
Buylink: release.Buylink,
|
||||||
Copyright: release.Copyright,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,34 +77,80 @@ func CreateRelease() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var release model.Release
|
var data releaseBodyJSON
|
||||||
err := json.NewDecoder(r.Body).Decode(&release)
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if release.ID == "" {
|
if data.ID == "" {
|
||||||
http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest)
|
http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if release.Title == "" { release.Title = release.ID }
|
title := data.ID
|
||||||
if release.ReleaseType == "" { release.ReleaseType = model.Single }
|
if data.Title != nil && *data.Title != "" {
|
||||||
|
title = *data.Title
|
||||||
if release.ReleaseDate != time.Unix(0, 0) {
|
|
||||||
release.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" }
|
description := ""
|
||||||
|
if data.Description != nil && *data.Description != "" { description = *data.Description }
|
||||||
|
|
||||||
err = music.CreateRelease(global.DB, &release)
|
releaseType := model.Single
|
||||||
|
if data.ReleaseType != nil && *data.ReleaseType != "" { releaseType = *data.ReleaseType }
|
||||||
|
|
||||||
|
releaseDate := time.Time{}
|
||||||
|
if data.ReleaseDate != nil && *data.ReleaseDate != "" {
|
||||||
|
releaseDate, err = time.Parse("2006-01-02T15:04", *data.ReleaseDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
http.Error(w, "Invalid release date", http.StatusBadRequest)
|
||||||
http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("FATAL: Failed to create release %s: %s\n", release.ID, err)
|
} else {
|
||||||
|
releaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
artwork := "/img/default-cover-art.png"
|
||||||
|
if data.Artwork != nil && *data.Artwork != "" { artwork = *data.Artwork }
|
||||||
|
|
||||||
|
buyname := ""
|
||||||
|
if data.Buyname != nil && *data.Buyname != "" { buyname = *data.Buyname }
|
||||||
|
|
||||||
|
buylink := ""
|
||||||
|
if data.Buylink != nil && *data.Buylink != "" { buylink = *data.Buylink }
|
||||||
|
|
||||||
|
var release = model.Release{
|
||||||
|
ID: data.ID,
|
||||||
|
Visible: false,
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
ReleaseType: releaseType,
|
||||||
|
ReleaseDate: releaseDate,
|
||||||
|
Artwork: artwork,
|
||||||
|
Buyname: buyname,
|
||||||
|
Buylink: buylink,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = global.DB.Exec(
|
||||||
|
"INSERT INTO musicrelease "+
|
||||||
|
"(id, visible, title, description, type, release_date, artwork, buyname, buylink) "+
|
||||||
|
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
||||||
|
release.ID,
|
||||||
|
release.Visible,
|
||||||
|
release.Title,
|
||||||
|
release.Description,
|
||||||
|
release.ReleaseType,
|
||||||
|
release.ReleaseDate.Format("2006-01-02 15:04:05"),
|
||||||
|
release.Artwork,
|
||||||
|
release.Buyname,
|
||||||
|
release.Buylink)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
|
http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Failed to create release %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -116,7 +165,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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path == "/" {
|
if r.URL.Path == "/" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
@ -124,6 +173,14 @@ func UpdateRelease(release *model.Release) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
segments := strings.Split(r.URL.Path[1:], "/")
|
segments := strings.Split(r.URL.Path[1:], "/")
|
||||||
|
var releaseID = segments[0]
|
||||||
|
var exists int
|
||||||
|
err := global.DB.Get(&exists, "SELECT count(*) FROM musicrelease WHERE id=$1", releaseID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to update release: %s\n", err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if len(segments) == 2 {
|
if len(segments) == 2 {
|
||||||
switch segments[1] {
|
switch segments[1] {
|
||||||
|
@ -142,19 +199,30 @@ func UpdateRelease(release *model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&release)
|
var data releaseBodyJSON
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if release.Artwork == "" {
|
if data.ID != "" { release.ID = data.ID }
|
||||||
release.Artwork = "/img/default-cover-art.png"
|
if data.Visible != nil { release.Visible = *data.Visible }
|
||||||
} else {
|
if data.Title != nil { release.Title = *data.Title }
|
||||||
if strings.Contains(release.Artwork, ";base64,") {
|
if data.Description != nil { release.Description = *data.Description }
|
||||||
|
if data.ReleaseType != nil { release.ReleaseType = *data.ReleaseType }
|
||||||
|
if data.ReleaseDate != nil {
|
||||||
|
newDate, err := time.Parse("2006-01-02T15:04", *data.ReleaseDate)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid release date", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
release.ReleaseDate = newDate
|
||||||
|
}
|
||||||
|
if data.Artwork != nil {
|
||||||
|
if strings.Contains(*data.Artwork, ";base64,") {
|
||||||
var artworkDirectory = filepath.Join("uploads", "musicart")
|
var artworkDirectory = filepath.Join("uploads", "musicart")
|
||||||
filename, err := HandleImageUpload(&release.Artwork, artworkDirectory, release.ID)
|
filename, err := HandleImageUpload(data.Artwork, artworkDirectory, data.ID)
|
||||||
|
|
||||||
// clean up files with this ID and different extensions
|
// clean up files with this ID and different extensions
|
||||||
err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error {
|
err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error {
|
||||||
|
@ -170,22 +238,35 @@ func UpdateRelease(release *model.Release) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
release.Artwork = fmt.Sprintf("/uploads/musicart/%s", filename)
|
release.Artwork = fmt.Sprintf("/uploads/musicart/%s", filename)
|
||||||
|
} else {
|
||||||
|
release.Artwork = *data.Artwork
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = music.UpdateRelease(global.DB, release)
|
if data.Buyname != nil { release.Buyname = *data.Buyname }
|
||||||
|
if data.Buylink != nil { release.Buylink = *data.Buylink }
|
||||||
|
|
||||||
|
_, err = global.DB.Exec(
|
||||||
|
"UPDATE musicrelease SET "+
|
||||||
|
"visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9 "+
|
||||||
|
"WHERE id=$1",
|
||||||
|
release.ID,
|
||||||
|
release.Visible,
|
||||||
|
release.Title,
|
||||||
|
release.Description,
|
||||||
|
release.ReleaseType,
|
||||||
|
release.ReleaseDate.Format("2006-01-02 15:04:05"),
|
||||||
|
release.Artwork,
|
||||||
|
release.Buyname,
|
||||||
|
release.Buylink)
|
||||||
if err != nil {
|
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)
|
fmt.Printf("FATAL: Failed to update release %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateReleaseTracks(release *model.Release) http.Handler {
|
func UpdateReleaseTracks(release model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var trackIDs = []string{}
|
var trackIDs = []string{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&trackIDs)
|
err := json.NewDecoder(r.Body).Decode(&trackIDs)
|
||||||
|
@ -194,19 +275,26 @@ func UpdateReleaseTracks(release *model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = music.UpdateReleaseTracks(global.DB, release.ID, trackIDs)
|
tx := global.DB.MustBegin()
|
||||||
if err != nil {
|
tx.MustExec("DELETE FROM musicreleasetrack WHERE release=$1", release.ID)
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
for i, trackID := range trackIDs {
|
||||||
http.NotFound(w, r)
|
tx.MustExec(
|
||||||
return
|
"INSERT INTO musicreleasetrack "+
|
||||||
|
"(release, track, number) "+
|
||||||
|
"VALUES ($1, $2, $3)",
|
||||||
|
release.ID,
|
||||||
|
trackID,
|
||||||
|
i)
|
||||||
}
|
}
|
||||||
fmt.Printf("FATAL: Failed to update tracks for %s: %s\n", release.ID, err)
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to update tracks 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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateReleaseCredits(release *model.Release) http.Handler {
|
func UpdateReleaseCredits(release model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
type creditJSON struct {
|
type creditJSON struct {
|
||||||
Artist string
|
Artist string
|
||||||
|
@ -220,34 +308,52 @@ func UpdateReleaseCredits(release *model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var credits []*model.Credit
|
// clear duplicates
|
||||||
|
type Credit struct {
|
||||||
|
Role string
|
||||||
|
Primary bool
|
||||||
|
}
|
||||||
|
var credits = map[string]Credit{}
|
||||||
for _, credit := range data {
|
for _, credit := range data {
|
||||||
credits = append(credits, &model.Credit{
|
credits[credit.Artist] = Credit{
|
||||||
Artist: model.Artist{
|
|
||||||
ID: credit.Artist,
|
|
||||||
},
|
|
||||||
Role: credit.Role,
|
Role: credit.Role,
|
||||||
Primary: credit.Primary,
|
Primary: credit.Primary,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = music.UpdateReleaseCredits(global.DB, release.ID, credits)
|
tx := global.DB.MustBegin()
|
||||||
|
tx.MustExec("DELETE FROM musiccredit WHERE release=$1", release.ID)
|
||||||
|
for artistID := range credits {
|
||||||
|
if credits[artistID].Role == "" {
|
||||||
|
http.Error(w, fmt.Sprintf("Artist role cannot be blank (%s)", artistID), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var exists int
|
||||||
|
_ = global.DB.Get(&exists, "SELECT count(*) FROM artist WHERE id=$1", artistID)
|
||||||
|
if exists == 0 {
|
||||||
|
http.Error(w, fmt.Sprintf("Artist %s does not exist\n", artistID), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.MustExec(
|
||||||
|
"INSERT INTO musiccredit "+
|
||||||
|
"(release, artist, role, is_primary) "+
|
||||||
|
"VALUES ($1, $2, $3, $4)",
|
||||||
|
release.ID,
|
||||||
|
artistID,
|
||||||
|
credits[artistID].Role,
|
||||||
|
credits[artistID].Primary)
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
fmt.Printf("Failed to update links for %s: %s\n", release.ID, err)
|
||||||
http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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)
|
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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPut {
|
if r.Method != http.MethodPut {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
@ -261,27 +367,30 @@ func UpdateReleaseLinks(release *model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = music.UpdateReleaseLinks(global.DB, release.ID, links)
|
tx := global.DB.MustBegin()
|
||||||
if err != nil {
|
tx.MustExec("DELETE FROM musiclink WHERE release=$1", release.ID)
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
for _, link := range links {
|
||||||
http.NotFound(w, r)
|
tx.MustExec(
|
||||||
return
|
"INSERT INTO musiclink "+
|
||||||
|
"(release, name, url) "+
|
||||||
|
"VALUES ($1, $2, $3)",
|
||||||
|
release.ID,
|
||||||
|
link.Name,
|
||||||
|
link.URL)
|
||||||
}
|
}
|
||||||
fmt.Printf("FATAL: Failed to update links for %s: %s\n", release.ID, err)
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteRelease(release *model.Release) http.Handler {
|
func DeleteRelease(release model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
err := music.DeleteRelease(global.DB, release.ID)
|
_, err := global.DB.Exec("DELETE FROM musicrelease WHERE id=$1", release.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
fmt.Printf("Failed to delete release %s: %s\n", release.ID, err)
|
||||||
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)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
105
api/track.go
105
api/track.go
|
@ -5,40 +5,24 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/global"
|
||||||
music "arimelody-web/music/controller"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
"arimelody-web/music/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Track struct {
|
|
||||||
*model.Track
|
|
||||||
Releases []string `json:"releases"`
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ServeAllTracks() http.Handler {
|
func ServeAllTracks() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
type Track struct {
|
type track struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
}
|
}
|
||||||
var tracks = []Track{}
|
var tracks = []track{}
|
||||||
|
|
||||||
var dbTracks = []*model.Track{}
|
err := global.DB.Select(&tracks, "SELECT id, title FROM musictrack")
|
||||||
dbTracks, err := music.GetAllTracks(global.DB)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to pull tracks from DB: %s\n", err)
|
fmt.Printf("FATAL: Failed to pull tracks from DB: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, track := range dbTracks {
|
|
||||||
tracks = append(tracks, Track{
|
|
||||||
ID: track.ID,
|
|
||||||
Title: track.Title,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
err = json.NewEncoder(w).Encode(tracks)
|
err = json.NewEncoder(w).Encode(tracks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -48,23 +32,42 @@ 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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
dbReleases, err := music.GetTrackReleases(global.DB, track.ID, false)
|
if r.URL.Path == "/" {
|
||||||
|
ServeAllTracks().ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var trackID = r.URL.Path[1:]
|
||||||
|
var track = model.Track{}
|
||||||
|
err := global.DB.Get(&track, "SELECT * from musictrack WHERE id=$1", trackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", track.ID, err)
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var releases = []*model.Release{}
|
||||||
|
err = global.DB.Select(&releases,
|
||||||
|
"SELECT * FROM musicrelease JOIN musicreleasetrack AS mrt "+
|
||||||
|
"WHERE mrt.track=$1 "+
|
||||||
|
"ORDER BY release_date",
|
||||||
|
track.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", trackID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
releases := []string{}
|
type response struct {
|
||||||
for _, release := range dbReleases {
|
model.Track
|
||||||
releases = append(releases, release.ID)
|
Releases []*model.Release
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
err = json.NewEncoder(w).Encode(Track{ track, releases })
|
err = json.NewEncoder(w).Encode(response{ track, releases })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to serve track %s: %s\n", track.ID, err)
|
fmt.Printf("FATAL: Failed to serve track %s: %s\n", trackID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -89,7 +92,15 @@ func CreateTrack() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := music.CreateTrack(global.DB, &track)
|
var trackID string
|
||||||
|
err = global.DB.Get(&trackID,
|
||||||
|
"INSERT INTO musictrack (title, description, lyrics, preview_url) "+
|
||||||
|
"VALUES ($1, $2, $3, $4) "+
|
||||||
|
"RETURNING id",
|
||||||
|
track.Title,
|
||||||
|
track.Description,
|
||||||
|
track.Lyrics,
|
||||||
|
track.PreviewURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to create track: %s\n", err)
|
fmt.Printf("FATAL: Failed to create track: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
@ -98,29 +109,46 @@ func CreateTrack() http.Handler {
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/plain")
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
w.Write([]byte(id))
|
w.Write([]byte(trackID))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateTrack(track *model.Track) http.Handler {
|
func UpdateTrack(track model.Track) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPut || r.URL.Path == "/" {
|
if r.Method != http.MethodPut || r.URL.Path == "/" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&track)
|
var update model.Track
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if track.Title == "" {
|
if update.Title == "" {
|
||||||
http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest)
|
http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = music.UpdateTrack(global.DB, track)
|
var trackID = r.URL.Path[1:]
|
||||||
|
var track = model.Track{}
|
||||||
|
err = global.DB.Get(&track, "SELECT * from musictrack WHERE id=$1", trackID)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = global.DB.Exec(
|
||||||
|
"UPDATE musictrack "+
|
||||||
|
"SET title=$2, description=$3, lyrics=$4, preview_url=$5 "+
|
||||||
|
"WHERE id=$1",
|
||||||
|
track.ID,
|
||||||
|
track.Title,
|
||||||
|
track.Description,
|
||||||
|
track.Lyrics,
|
||||||
|
track.PreviewURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to update track %s: %s\n", track.ID, err)
|
fmt.Printf("Failed to update track %s: %s\n", track.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
@ -135,7 +163,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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodDelete || r.URL.Path == "/" {
|
if r.Method != http.MethodDelete || r.URL.Path == "/" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
@ -143,7 +171,10 @@ func DeleteTrack(track *model.Track) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
var trackID = r.URL.Path[1:]
|
var trackID = r.URL.Path[1:]
|
||||||
err := music.DeleteTrack(global.DB, trackID)
|
_, err := global.DB.Exec(
|
||||||
|
"DELETE FROM musictrack "+
|
||||||
|
"WHERE id=$1",
|
||||||
|
trackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to delete track %s: %s\n", trackID, err)
|
fmt.Printf("Failed to delete track %s: %s\n", trackID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
|
@ -35,8 +35,6 @@ func HandleImageUpload(data *string, directory string, filename string) (string,
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// TODO: generate compressed versions of image (512x512?)
|
|
||||||
|
|
||||||
buffer := bufio.NewWriter(file)
|
buffer := bufio.NewWriter(file)
|
||||||
_, err = buffer.Write(imageData)
|
_, err = buffer.Write(imageData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -6,27 +6,26 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/global"
|
||||||
)
|
)
|
||||||
|
|
||||||
const API_ENDPOINT = "https://discord.com/api/v10"
|
const API_ENDPOINT = "https://discord.com/api/v10"
|
||||||
|
|
||||||
var CREDENTIALS_PROVIDED = true
|
var CREDENTIALS_PROVIDED = true
|
||||||
var CLIENT_ID = func() string {
|
var CLIENT_ID = func() string {
|
||||||
id := os.Getenv("DISCORD_CLIENT")
|
id := global.Args["discordClient"]
|
||||||
if id == "" {
|
if id == "" {
|
||||||
fmt.Printf("WARN: Discord client ID (DISCORD_CLIENT) was not provided. Admin login will be unavailable.\n")
|
fmt.Printf("WARN: Discord client ID (-discordClient) was not provided. Admin login will be unavailable.\n")
|
||||||
CREDENTIALS_PROVIDED = false
|
CREDENTIALS_PROVIDED = false
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
}()
|
}()
|
||||||
var CLIENT_SECRET = func() string {
|
var CLIENT_SECRET = func() string {
|
||||||
secret := os.Getenv("DISCORD_SECRET")
|
secret := global.Args["discordSecret"]
|
||||||
if secret == "" {
|
if secret== "" {
|
||||||
fmt.Printf("WARN: Discord secret (DISCORD_SECRET) was not provided. Admin login will be unavailable.\n")
|
fmt.Printf("WARN: Discord secret (-discordSecret) was not provided. Admin login will be unavailable.\n")
|
||||||
CREDENTIALS_PROVIDED = false
|
CREDENTIALS_PROVIDED = false
|
||||||
}
|
}
|
||||||
return secret
|
return secret
|
||||||
|
@ -108,6 +107,7 @@ func GetDiscordUserFromAuth(token string) (DiscordUser, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
auth_info := AuthInfoResponse{}
|
auth_info := AuthInfoResponse{}
|
||||||
|
|
||||||
err = json.NewDecoder(res.Body).Decode(&auth_info)
|
err = json.NewDecoder(res.Body).Decode(&auth_info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return DiscordUser{}, errors.New(fmt.Sprintf("Failed to parse auth information from discord: %s\n", err))
|
return DiscordUser{}, errors.New(fmt.Sprintf("Failed to parse auth information from discord: %s\n", err))
|
||||||
|
|
18
docker-compose-db.yml
Normal file
18
docker-compose-db.yml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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
|
|
@ -1,22 +0,0 @@
|
||||||
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 {
|
var HTTP_DOMAIN = func() string {
|
||||||
domain := os.Getenv("HTTP_DOMAIN")
|
domain := Args["httpDomain"]
|
||||||
if domain == "" {
|
if domain != "" {
|
||||||
return "https://arimelody.me"
|
|
||||||
}
|
|
||||||
return domain
|
return domain
|
||||||
|
}
|
||||||
|
return "https://arimelody.me"
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var DB *sqlx.DB
|
var DB *sqlx.DB
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody-web/colour"
|
"arimelody.me/arimelody.me/colour"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DefaultHeaders(next http.Handler) http.Handler {
|
func DefaultHeaders(next http.Handler) http.Handler {
|
||||||
|
|
15
go.mod
15
go.mod
|
@ -1,8 +1,19 @@
|
||||||
module arimelody-web
|
module arimelody.me/arimelody.me
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47
|
||||||
|
github.com/jmoiron/sqlx v1.3.5
|
||||||
github.com/lib/pq v1.10.9
|
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,10 +1,30 @@
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
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=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
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=
|
||||||
|
|
19
main.go
19
main.go
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -9,11 +8,11 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody-web/admin"
|
"arimelody.me/arimelody.me/admin"
|
||||||
"arimelody-web/api"
|
"arimelody.me/arimelody.me/api"
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/global"
|
||||||
musicView "arimelody-web/music/view"
|
musicView "arimelody.me/arimelody.me/music/view"
|
||||||
"arimelody-web/templates"
|
"arimelody.me/arimelody.me/templates"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
|
@ -23,11 +22,8 @@ const DEFAULT_PORT int = 8080
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// initialise database connection
|
// initialise database connection
|
||||||
var dbHost = os.Getenv("ARIMELODY_DB_HOST")
|
|
||||||
if dbHost == "" { dbHost = "127.0.0.1" }
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
global.DB, err = sqlx.Connect("postgres", "host=" + dbHost + " user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable")
|
global.DB, err = sqlx.Connect("postgres", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "FATAL: Unable to create database connection pool: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Unable to create database connection pool: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -68,10 +64,9 @@ func createServeMux() *http.ServeMux {
|
||||||
func staticHandler(directory string) http.Handler {
|
func staticHandler(directory string) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
|
info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
|
||||||
|
|
||||||
// does the file exist?
|
// does the file exist?
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if os.IsNotExist(err) {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package music
|
package music
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/music/model"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,14 +29,14 @@ func GetAllArtists(db *sqlx.DB) ([]*model.Artist, error) {
|
||||||
return artists, nil
|
return artists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, error) {
|
func GetArtistsNotOnRelease(db *sqlx.DB, release *model.Release) ([]*model.Artist, error) {
|
||||||
var artists = []*model.Artist{}
|
var artists = []*model.Artist{}
|
||||||
|
|
||||||
err := db.Select(&artists,
|
err := db.Select(&artists,
|
||||||
"SELECT * FROM artist "+
|
"SELECT * FROM artist "+
|
||||||
"WHERE id NOT IN "+
|
"WHERE id NOT IN "+
|
||||||
"(SELECT artist FROM musiccredit WHERE release=$1)",
|
"(SELECT artist FROM musiccredit WHERE release=$1)",
|
||||||
releaseID)
|
release.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -44,60 +44,6 @@ func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, err
|
||||||
return artists, nil
|
return artists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.Credit, error) {
|
|
||||||
var query string = "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 "
|
|
||||||
if !show_hidden { query += "AND visible=true " }
|
|
||||||
query += "ORDER BY release_date DESC"
|
|
||||||
rows, err := db.Query(query, 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 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateArtist(db *sqlx.DB, artist *model.Artist) error {
|
func CreateArtist(db *sqlx.DB, artist *model.Artist) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"INSERT INTO artist (id, name, website, avatar) "+
|
"INSERT INTO artist (id, name, website, avatar) "+
|
||||||
|
@ -131,11 +77,11 @@ func UpdateArtist(db *sqlx.DB, artist *model.Artist) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteArtist(db *sqlx.DB, artistID string) error {
|
func DeleteArtist(db *sqlx.DB, artist *model.Artist) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"DELETE FROM artist "+
|
"DELETE FROM artist "+
|
||||||
"WHERE id=$1",
|
"WHERE id=$1",
|
||||||
artistID,
|
artist.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
71
music/controller/credit.go
Normal file
71
music/controller/credit.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package music
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody.me/arimelody.me/music/model"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DATABASE
|
||||||
|
|
||||||
|
func GetReleaseCredits(db *sqlx.DB, release *model.Release) ([]model.Credit, error) {
|
||||||
|
var credits = []model.Credit{}
|
||||||
|
|
||||||
|
err := db.Select(&credits,
|
||||||
|
"SELECT artist.*,role,is_primary FROM musiccredit "+
|
||||||
|
"JOIN artist ON artist=id "+
|
||||||
|
"WHERE release=$1",
|
||||||
|
release.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return credits, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateCredit(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"INSERT INTO musiccredit (release, artist, role, is_primary) "+
|
||||||
|
"VALUES ($1, $2, $3, $4)",
|
||||||
|
releaseID,
|
||||||
|
artistID,
|
||||||
|
credit.Role,
|
||||||
|
credit.Primary,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateCredit(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"UPDATE musiccredit SET "+
|
||||||
|
"role=$3, is_primary=$4 "+
|
||||||
|
"WHERE release=$1, artist=$2",
|
||||||
|
releaseID,
|
||||||
|
artistID,
|
||||||
|
credit.Role,
|
||||||
|
credit.Primary,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteCredit(db *sqlx.DB, releaseID string, artistID string) (error) {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"DELETE FROM musiccredit "+
|
||||||
|
"WHERE release=$1, artist=$2",
|
||||||
|
releaseID,
|
||||||
|
artistID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
64
music/controller/link.go
Normal file
64
music/controller/link.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package music
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody.me/arimelody.me/music/model"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DATABASE
|
||||||
|
|
||||||
|
func GetReleaseLinks(db *sqlx.DB, release *model.Release) ([]model.Link, error) {
|
||||||
|
var links = []model.Link{}
|
||||||
|
|
||||||
|
err := db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", release.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateLink(db *sqlx.DB, releaseID string, link *model.Link) (error) {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"INSERT INTO musiclink (release, name, url) "+
|
||||||
|
"VALUES ($1, $2, $3)",
|
||||||
|
releaseID,
|
||||||
|
link.Name,
|
||||||
|
link.URL,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateLink(db *sqlx.DB, releaseID string, link *model.Link) (error) {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"UPDATE musiclink SET "+
|
||||||
|
"name=$2, url=$3 "+
|
||||||
|
"WHERE release=$1",
|
||||||
|
releaseID,
|
||||||
|
link.Name,
|
||||||
|
link.URL,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteLink(db *sqlx.DB, releaseID string, link *model.Link) (error) {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"DELETE FROM musiclink "+
|
||||||
|
"WHERE release=$1, name=$2",
|
||||||
|
releaseID,
|
||||||
|
link.Name,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,110 +1,54 @@
|
||||||
package music
|
package music
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"arimelody-web/music/model"
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
|
func GetRelease(db *sqlx.DB, id string) (*model.Release, error) {
|
||||||
var release = model.Release{}
|
var releases = model.Release{}
|
||||||
|
|
||||||
err := db.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", id)
|
err := db.Get(&releases, "SELECT * FROM musicrelease WHERE id=$1", id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if full {
|
return &releases, nil
|
||||||
// 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, onlyVisible bool, limit int, full bool) ([]*model.Release, error) {
|
func GetAllReleases(db *sqlx.DB) ([]*model.Release, error) {
|
||||||
var releases = []*model.Release{}
|
var releases = []*model.Release{}
|
||||||
|
|
||||||
query := "SELECT * FROM musicrelease"
|
err := db.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC")
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if full {
|
|
||||||
// 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
|
return releases, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetReleaseTracks(db *sqlx.DB, release *model.Release) ([]*model.Track, error) {
|
||||||
|
var tracks = []*model.Track{}
|
||||||
|
|
||||||
|
err := db.Select(&tracks,
|
||||||
|
"SELECT musictrack.* FROM musictrack "+
|
||||||
|
"JOIN musicreleasetrack ON track=id "+
|
||||||
|
"WHERE release=$1 "+
|
||||||
|
"ORDER BY number ASC",
|
||||||
|
release.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracks, nil
|
||||||
|
}
|
||||||
|
|
||||||
func CreateRelease(db *sqlx.DB, release *model.Release) error {
|
func CreateRelease(db *sqlx.DB, release *model.Release) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"INSERT INTO musicrelease "+
|
"INSERT INTO musicrelease "+
|
||||||
"(id, visible, title, description, type, release_date, artwork, buyname, buylink, copyright, copyrighturl) "+
|
"(id, visible, title, description, type, release_date, artwork, buyname, buylink) "+
|
||||||
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
|
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
||||||
release.ID,
|
release.ID,
|
||||||
release.Visible,
|
release.Visible,
|
||||||
release.Title,
|
release.Title,
|
||||||
|
@ -114,8 +58,6 @@ func CreateRelease(db *sqlx.DB, release *model.Release) error {
|
||||||
release.Artwork,
|
release.Artwork,
|
||||||
release.Buyname,
|
release.Buyname,
|
||||||
release.Buylink,
|
release.Buylink,
|
||||||
release.Copyright,
|
|
||||||
release.CopyrightURL,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -127,7 +69,7 @@ func CreateRelease(db *sqlx.DB, release *model.Release) error {
|
||||||
func UpdateRelease(db *sqlx.DB, release *model.Release) error {
|
func UpdateRelease(db *sqlx.DB, release *model.Release) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"UPDATE musicrelease SET "+
|
"UPDATE musicrelease SET "+
|
||||||
"visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9, copyright=$10, copyrighturl=$11 "+
|
"visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9 "+
|
||||||
"WHERE id=$1",
|
"WHERE id=$1",
|
||||||
release.ID,
|
release.ID,
|
||||||
release.Visible,
|
release.Visible,
|
||||||
|
@ -138,8 +80,6 @@ func UpdateRelease(db *sqlx.DB, release *model.Release) error {
|
||||||
release.Artwork,
|
release.Artwork,
|
||||||
release.Buyname,
|
release.Buyname,
|
||||||
release.Buylink,
|
release.Buylink,
|
||||||
release.Copyright,
|
|
||||||
release.CopyrightURL,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -148,53 +88,49 @@ func UpdateRelease(db *sqlx.DB, release *model.Release) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateReleaseTracks(db *sqlx.DB, releaseID string, new_tracks []string) error {
|
func UpdateReleaseTracks(db *sqlx.DB, release *model.Release, new_tracks []*model.Track) error {
|
||||||
tx, err := db.Begin()
|
_, err := db.Exec(
|
||||||
|
"DELETE FROM musicreleasetrack "+
|
||||||
|
"WHERE release=$1",
|
||||||
|
release.ID,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Exec("DELETE FROM musicreleasetrack WHERE release=$1", releaseID)
|
for i, track := range new_tracks {
|
||||||
if err != nil {
|
_, err = db.Exec(
|
||||||
return err
|
|
||||||
}
|
|
||||||
for i, trackID := range new_tracks {
|
|
||||||
_, err = tx.Exec(
|
|
||||||
"INSERT INTO musicreleasetrack "+
|
"INSERT INTO musicreleasetrack "+
|
||||||
"(release, track, number) "+
|
"(release, track, number) "+
|
||||||
"VALUES ($1, $2, $3)",
|
"VALUES ($1, $2, $3)",
|
||||||
releaseID,
|
release.ID,
|
||||||
trackID,
|
track.ID,
|
||||||
i)
|
i,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateReleaseCredits(db *sqlx.DB, releaseID string, new_credits []*model.Credit) error {
|
func UpdateReleaseCredits(db *sqlx.DB, release *model.Release, new_credits []*model.Credit) error {
|
||||||
tx, err := db.Begin()
|
_, err := db.Exec(
|
||||||
|
"DELETE FROM musiccredit "+
|
||||||
|
"WHERE release=$1",
|
||||||
|
release.ID,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Exec("DELETE FROM musiccredit WHERE release=$1", releaseID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, credit := range new_credits {
|
for _, credit := range new_credits {
|
||||||
_, err = tx.Exec(
|
_, err = db.Exec(
|
||||||
"INSERT INTO musiccredit "+
|
"INSERT INTO musiccredit "+
|
||||||
"(release, artist, role, is_primary) "+
|
"(release, artist, role, is_primary) "+
|
||||||
"VALUES ($1, $2, $3, $4)",
|
"VALUES ($1, $2, $3, $4)",
|
||||||
releaseID,
|
release.ID,
|
||||||
credit.Artist.ID,
|
credit.Artist.ID,
|
||||||
credit.Role,
|
credit.Role,
|
||||||
credit.Primary,
|
credit.Primary,
|
||||||
|
@ -204,31 +140,25 @@ func UpdateReleaseCredits(db *sqlx.DB, releaseID string, new_credits []*model.Cr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateReleaseLinks(db *sqlx.DB, releaseID string, new_links []*model.Link) error {
|
func UpdateReleaseLinks(db *sqlx.DB, release *model.Release, new_links []*model.Link) error {
|
||||||
tx, err := db.Begin()
|
_, err := db.Exec(
|
||||||
|
"DELETE FROM musiclink "+
|
||||||
|
"WHERE release=$1",
|
||||||
|
release.ID,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Exec("DELETE FROM musiclink WHERE release=$1", releaseID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, link := range new_links {
|
for _, link := range new_links {
|
||||||
fmt.Printf("%s: %s\n", link.Name, link.URL)
|
_, err = db.Exec(
|
||||||
_, err := tx.Exec(
|
|
||||||
"INSERT INTO musiclink "+
|
"INSERT INTO musiclink "+
|
||||||
"(release, name, url) "+
|
"(release, name, url) "+
|
||||||
"VALUES ($1, $2, $3)",
|
"VALUES ($1, $2, $3)",
|
||||||
releaseID,
|
release.ID,
|
||||||
link.Name,
|
link.Name,
|
||||||
link.URL,
|
link.URL,
|
||||||
)
|
)
|
||||||
|
@ -237,19 +167,14 @@ func UpdateReleaseLinks(db *sqlx.DB, releaseID string, new_links []*model.Link)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteRelease(db *sqlx.DB, releaseID string) error {
|
func DeleteRelease(db *sqlx.DB, release *model.Release) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"DELETE FROM musicrelease "+
|
"DELETE FROM musicrelease "+
|
||||||
"WHERE id=$1",
|
"WHERE id=$1",
|
||||||
releaseID,
|
release.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -258,60 +183,33 @@ func DeleteRelease(db *sqlx.DB, releaseID string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetReleaseTracks(db *sqlx.DB, releaseID string) ([]*model.Track, error) {
|
func GetFullRelease(db *sqlx.DB, release *model.Release) (*model.FullRelease, error) {
|
||||||
var tracks = []*model.Track{}
|
// get credits
|
||||||
|
credits, err := GetReleaseCredits(db, release)
|
||||||
err := db.Select(&tracks,
|
|
||||||
"SELECT musictrack.* FROM musictrack "+
|
|
||||||
"JOIN musicreleasetrack ON track=id "+
|
|
||||||
"WHERE release=$1 "+
|
|
||||||
"ORDER BY number ASC",
|
|
||||||
releaseID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return tracks, nil
|
// get tracks
|
||||||
}
|
dbTracks, err := GetReleaseTracks(db, release)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tracks := []model.DisplayTrack{}
|
||||||
|
for i, track := range dbTracks {
|
||||||
|
tracks = append(tracks, track.MakeDisplay(i + 1))
|
||||||
|
}
|
||||||
|
|
||||||
func GetReleaseCredits(db *sqlx.DB, releaseID string) ([]*model.Credit, error) {
|
// get links
|
||||||
rows, err := db.Query(
|
links, err := GetReleaseLinks(db, release)
|
||||||
"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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var credits []*model.Credit
|
return &model.FullRelease{
|
||||||
for rows.Next() {
|
Release: release,
|
||||||
credit := model.Credit{}
|
Tracks: tracks,
|
||||||
rows.Scan(
|
Credits: credits,
|
||||||
&credit.Artist.ID,
|
Links: links,
|
||||||
&credit.Artist.Name,
|
}, nil
|
||||||
&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{}
|
|
||||||
|
|
||||||
err := db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", releaseID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return links, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package music
|
package music
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/music/model"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -40,14 +40,14 @@ func GetOrphanTracks(db *sqlx.DB) ([]*model.Track, error) {
|
||||||
return tracks, nil
|
return tracks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTracksNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Track, error) {
|
func GetTracksNotOnRelease(db *sqlx.DB, release *model.Release) ([]*model.Track, error) {
|
||||||
var tracks = []*model.Track{}
|
var tracks = []*model.Track{}
|
||||||
|
|
||||||
err := db.Select(&tracks,
|
err := db.Select(&tracks,
|
||||||
"SELECT * FROM musictrack "+
|
"SELECT * FROM musictrack "+
|
||||||
"WHERE id NOT IN "+
|
"WHERE id NOT IN "+
|
||||||
"(SELECT track FROM musicreleasetrack WHERE release=$1)",
|
"(SELECT track FROM musicreleasetrack WHERE release=$1)",
|
||||||
releaseID)
|
release.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -55,58 +55,20 @@ func GetTracksNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Track, error
|
||||||
return tracks, nil
|
return tracks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTrackReleases(db *sqlx.DB, trackID string, full bool) ([]*model.Release, error) {
|
func GetTrackReleases(db *sqlx.DB, track *model.Track) ([]*model.Release, error) {
|
||||||
var releases = []*model.Release{}
|
var releases = []*model.Release{}
|
||||||
|
|
||||||
err := db.Select(&releases,
|
err := db.Select(&releases,
|
||||||
"SELECT id,title,type,release_date,artwork,buylink "+
|
"SELECT musicrelease.* FROM musicrelease "+
|
||||||
"FROM musicrelease "+
|
|
||||||
"JOIN musicreleasetrack ON release=id "+
|
"JOIN musicreleasetrack ON release=id "+
|
||||||
"WHERE track=$1 "+
|
"WHERE track=$1 "+
|
||||||
"ORDER BY release_date",
|
"ORDER BY release_date",
|
||||||
trackID,
|
track.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return releases, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,11 +123,11 @@ func UpdateTrack(db *sqlx.DB, track *model.Track) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteTrack(db *sqlx.DB, trackID string) error {
|
func DeleteTrack(db *sqlx.DB, track *model.Track) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"DELETE FROM musictrack "+
|
"DELETE FROM musictrack "+
|
||||||
"WHERE id=$1",
|
"WHERE id=$1",
|
||||||
trackID,
|
track.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Artist struct {
|
Artist struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
@ -19,3 +21,56 @@ func (artist Artist) GetAvatar() string {
|
||||||
}
|
}
|
||||||
return artist.Avatar
|
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,10 +1,7 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
type (
|
type Credit struct {
|
||||||
Credit struct {
|
Artist `json:"artist"`
|
||||||
Release Release `json:"release"`
|
|
||||||
Artist Artist `json:"artist"`
|
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Primary bool `json:"primary" db:"is_primary"`
|
Primary bool `json:"primary" db:"is_primary"`
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,9 +19,13 @@ type (
|
||||||
Buylink string `json:"buylink"`
|
Buylink string `json:"buylink"`
|
||||||
Copyright string `json:"copyright" db:"copyright"`
|
Copyright string `json:"copyright" db:"copyright"`
|
||||||
CopyrightURL string `json:"copyrightURL" db:"copyrighturl"`
|
CopyrightURL string `json:"copyrightURL" db:"copyrighturl"`
|
||||||
Tracks []*Track `json:"tracks"`
|
}
|
||||||
Credits []*Credit `json:"credits"`
|
|
||||||
Links []*Link `json:"links"`
|
FullRelease struct {
|
||||||
|
*Release
|
||||||
|
Tracks []DisplayTrack
|
||||||
|
Credits []Credit
|
||||||
|
Links []Link
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,10 +39,6 @@ const (
|
||||||
|
|
||||||
// GETTERS
|
// GETTERS
|
||||||
|
|
||||||
func (release Release) GetDescriptionHTML() template.HTML {
|
|
||||||
return template.HTML(strings.Replace(release.Description, "\n", "<br>", -1))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (release Release) TextReleaseDate() string {
|
func (release Release) TextReleaseDate() string {
|
||||||
return release.ReleaseDate.Format("2006-01-02T15:04")
|
return release.ReleaseDate.Format("2006-01-02T15:04")
|
||||||
}
|
}
|
||||||
|
@ -60,39 +58,10 @@ func (release Release) GetArtwork() string {
|
||||||
return release.Artwork
|
return release.Artwork
|
||||||
}
|
}
|
||||||
|
|
||||||
func (release Release) IsSingle() bool {
|
func (release FullRelease) IsSingle() bool {
|
||||||
return len(release.Tracks) == 1;
|
return len(release.Tracks) == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
func (release Release) IsReleased() bool {
|
func (release Release) IsReleased() bool {
|
||||||
return release.ReleaseDate.Before(time.Now())
|
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[:], ", ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,20 +10,21 @@ type (
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Lyrics string `json:"lyrics" db:"lyrics"`
|
Lyrics string `json:"lyrics"`
|
||||||
PreviewURL string `json:"previewURL" db:"preview_url"`
|
PreviewURL string `json:"previewURL" db:"preview_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisplayTrack struct {
|
||||||
|
*Track
|
||||||
|
Lyrics template.HTML
|
||||||
|
Number int
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (track Track) GetDescriptionHTML() template.HTML {
|
func (track Track) MakeDisplay(number int) DisplayTrack {
|
||||||
return template.HTML(strings.Replace(track.Description, "\n", "<br>", -1))
|
return DisplayTrack{
|
||||||
}
|
Track: &track,
|
||||||
|
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
||||||
func (track Track) GetLyricsHTML() template.HTML {
|
Number: number,
|
||||||
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"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/global"
|
||||||
music "arimelody-web/music/controller"
|
music "arimelody.me/arimelody.me/music/controller"
|
||||||
"arimelody-web/music/model"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
"arimelody-web/templates"
|
"arimelody.me/arimelody.me/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTP HANDLER METHODS
|
// HTTP HANDLER METHODS
|
||||||
|
@ -21,7 +21,8 @@ func Handler() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
release, err := music.GetRelease(global.DB, r.URL.Path[1:], true)
|
var release model.Release
|
||||||
|
err := global.DB.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", r.URL.Path[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
|
@ -35,17 +36,25 @@ func Handler() http.Handler {
|
||||||
|
|
||||||
func ServeCatalog() http.Handler {
|
func ServeCatalog() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
releases, err := music.GetAllReleases(global.DB, true, 0, true)
|
dbReleases, err := music.GetAllReleases(global.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to pull releases for catalog: %s\n", err)
|
fmt.Printf("FATAL: Failed to pull releases for catalog: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
releases := []*model.FullRelease{}
|
||||||
for _, release := range releases {
|
for _, dbRelease := range dbReleases {
|
||||||
if !release.IsReleased() {
|
if !dbRelease.Visible { continue }
|
||||||
release.ReleaseType = model.Upcoming
|
if !dbRelease.IsReleased() {
|
||||||
|
dbRelease.ReleaseType = model.Upcoming
|
||||||
}
|
}
|
||||||
|
release, err := music.GetFullRelease(global.DB, dbRelease)
|
||||||
|
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)
|
err = templates.Pages["music"].Execute(w, releases)
|
||||||
|
|
|
@ -5,35 +5,16 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"arimelody-web/admin"
|
"arimelody.me/arimelody.me/admin"
|
||||||
"arimelody-web/global"
|
"arimelody.me/arimelody.me/global"
|
||||||
"arimelody-web/music/model"
|
"arimelody.me/arimelody.me/music/model"
|
||||||
db "arimelody-web/music/controller"
|
db "arimelody.me/arimelody.me/music/controller"
|
||||||
"arimelody-web/templates"
|
"arimelody.me/arimelody.me/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
// HTTP HANDLERS
|
||||||
Track struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Lyrics string `json:"lyrics"`
|
|
||||||
}
|
|
||||||
|
|
||||||
Credit struct {
|
func ServeRelease(release model.Release) http.Handler {
|
||||||
*model.Artist
|
|
||||||
Role string `json:"role"`
|
|
||||||
Primary bool `json:"primary"`
|
|
||||||
}
|
|
||||||
|
|
||||||
Release struct {
|
|
||||||
*model.Release
|
|
||||||
Tracks []Track `json:"tracks"`
|
|
||||||
Credits []Credit `json:"credits"`
|
|
||||||
Links map[string]string `json:"links"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func ServeRelease(release *model.Release) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// only allow authorised users to view hidden releases
|
// only allow authorised users to view hidden releases
|
||||||
authorised := admin.GetSession(r) != nil
|
authorised := admin.GetSession(r) != nil
|
||||||
|
@ -42,65 +23,22 @@ func ServeRelease(release *model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := Release{
|
fullRelease := &model.FullRelease{
|
||||||
Release: release,
|
Release: &release,
|
||||||
Tracks: []Track{},
|
|
||||||
Credits: []Credit{},
|
|
||||||
Links: make(map[string]string),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if authorised || release.IsReleased() {
|
if authorised || release.IsReleased() {
|
||||||
// get credits
|
fullerRelease, err := db.GetFullRelease(global.DB, &release)
|
||||||
credits, err := db.GetReleaseCredits(global.DB, release.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to serve release %s: Credits: %s\n", release.ID, err)
|
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)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, credit := range credits {
|
fullRelease = fullerRelease
|
||||||
artist, err := db.GetArtist(global.DB, credit.Artist.ID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("FATAL: Failed to serve release %s: Artists: %s\n", release.ID, err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Credits = append(response.Credits, Credit{
|
|
||||||
Artist: artist,
|
|
||||||
Role: credit.Role,
|
|
||||||
Primary: credit.Primary,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// get tracks
|
|
||||||
tracks, err := db.GetReleaseTracks(global.DB, release.ID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("FATAL: Failed to serve release %s: Tracks: %s\n", release.ID, err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, track := range tracks {
|
|
||||||
response.Tracks = append(response.Tracks, Track{
|
|
||||||
Title: track.Title,
|
|
||||||
Description: track.Description,
|
|
||||||
Lyrics: track.Lyrics,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// get links
|
|
||||||
links, err := db.GetReleaseLinks(global.DB, release.ID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("FATAL: Failed to serve release %s: Links: %s\n", release.ID, err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, link := range links {
|
|
||||||
response.Links[link.Name] = link.URL
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
err := json.NewEncoder(w).Encode(response)
|
err := json.NewEncoder(w).Encode(fullRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -108,7 +46,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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// only allow authorised users to view hidden releases
|
// only allow authorised users to view hidden releases
|
||||||
authorised := admin.GetSession(r) != nil
|
authorised := admin.GetSession(r) != nil
|
||||||
|
@ -117,15 +55,21 @@ func ServeGateway(release *model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := *release
|
fullRelease := &model.FullRelease{
|
||||||
|
Release: &release,
|
||||||
if authorised || release.IsReleased() {
|
|
||||||
response.Tracks = release.Tracks
|
|
||||||
response.Credits = release.Credits
|
|
||||||
response.Links = release.Links
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := templates.Pages["music-gateway"].Execute(w, response)
|
if authorised || release.IsReleased() {
|
||||||
|
fullerRelease, err := db.GetFullRelease(global.DB, &release)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
err := templates.Pages["music-gateway"].Execute(w, fullRelease)
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -296,13 +296,6 @@ div#info p {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#upcoming-release {
|
|
||||||
width: fit-content;
|
|
||||||
padding: .3em 1em;
|
|
||||||
font-size: 1em;
|
|
||||||
background: #101010;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul#links {
|
ul#links {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
@ -318,7 +311,6 @@ ul#links li {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#buylink,
|
|
||||||
ul#links a {
|
ul#links a {
|
||||||
width: calc(100% - 1.6em);
|
width: calc(100% - 1.6em);
|
||||||
padding: .5em .8em;
|
padding: .5em .8em;
|
||||||
|
@ -332,9 +324,7 @@ ul#links a {
|
||||||
transition: filter .1s,-webkit-filter .1s
|
transition: filter .1s,-webkit-filter .1s
|
||||||
}
|
}
|
||||||
|
|
||||||
#buylink {
|
ul#links a.buy {
|
||||||
margin-top: 1em;
|
|
||||||
margin-bottom: -.5em;
|
|
||||||
background-color: #ff94e9
|
background-color: #ff94e9
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,12 +354,7 @@ ul#links a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
#description {
|
#description {
|
||||||
font-size: 1.1em;
|
font-size: 1.2em;
|
||||||
}
|
|
||||||
|
|
||||||
#copyright {
|
|
||||||
margin-bottom: 0;
|
|
||||||
font-size: .8em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#share {
|
#share {
|
||||||
|
@ -405,7 +390,7 @@ ul#links a:hover {
|
||||||
animation: share-after 2s cubic-bezier(.5,0,1,.5) forwards
|
animation: share-after 2s cubic-bezier(.5,0,1,.5) forwards
|
||||||
}
|
}
|
||||||
|
|
||||||
#info h2 {
|
h2 {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding: .3em 1em;
|
padding: .3em 1em;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
@ -532,11 +517,12 @@ footer a:hover {
|
||||||
|
|
||||||
#art-container {
|
#art-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#artwork {
|
#artwork {
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 60vw;
|
max-width: 50vw;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
|
@ -550,25 +536,21 @@ footer a:hover {
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
height: auto;
|
height: auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
mask-image: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div#info > div {
|
div#info > div {
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 2em 0 0 0;
|
margin: 0;
|
||||||
overflow-y: unset;
|
overflow-y: unset;
|
||||||
|
mask-image: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div#info p {
|
div#info p {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
div#overview p {
|
|
||||||
margin: .5em auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
div#extras {
|
div#extras {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -601,18 +583,10 @@ footer a:hover {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#share.active::after {
|
#share.active: : after {
|
||||||
transform: translate(calc(-50% - .6em),1.5em);
|
transform: translate(calc(-50% - .6em),1.5em);
|
||||||
}
|
}
|
||||||
|
|
||||||
#credits h2 {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#lyrics p.album-track-subheading {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
|
@ -622,9 +596,6 @@ footer a:hover {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#overlay {
|
|
||||||
background-size: 100vw 4px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes background-init {
|
@keyframes background-init {
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<main >
|
<main>
|
||||||
<script type="module" src="/script/music-gateway.js"></script>
|
<script type="module" src="/script/music-gateway.js"></script>
|
||||||
|
|
||||||
<div id="background" style="background-image: url({{.GetArtwork}})"></div>
|
<div id="background" style="background-image: url({{.GetArtwork}})"></div>
|
||||||
|
@ -61,14 +61,17 @@
|
||||||
<p id="type" class="{{.ReleaseType}}">{{.ReleaseType}}</p>
|
<p id="type" class="{{.ReleaseType}}">{{.ReleaseType}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p id="type" class="upcoming">upcoming</p>
|
<p id="type" class="upcoming">upcoming</p>
|
||||||
<p id="upcoming-release">Releases: {{.PrintReleaseDate}}</p>
|
<p>Releases: {{.PrintReleaseDate}}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .IsReleased}}
|
{{if .IsReleased}}
|
||||||
{{if .Buylink}}
|
|
||||||
<a href="{{.Buylink}}" id="buylink">{{or .Buyname "buy"}}</a>
|
|
||||||
{{end}}
|
|
||||||
<ul id="links">
|
<ul id="links">
|
||||||
|
{{if .Buylink}}
|
||||||
|
<li>
|
||||||
|
<a href="{{.Buylink}}" class="buy">{{or .Buyname "buy"}}</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{range .Links}}
|
{{range .Links}}
|
||||||
<li>
|
<li>
|
||||||
<a class="{{.NormaliseName}}" href="{{.URL}}">{{.Name}}</a>
|
<a class="{{.NormaliseName}}" href="{{.URL}}">{{.Name}}</a>
|
||||||
|
@ -79,7 +82,7 @@
|
||||||
|
|
||||||
{{if .Description}}
|
{{if .Description}}
|
||||||
|
|
||||||
<p id="description">{{.GetDescriptionHTML}}</p>
|
<p id="description">{{.Description}}</p>
|
||||||
|
|
||||||
{{else if .IsSingle}}
|
{{else if .IsSingle}}
|
||||||
|
|
||||||
|
@ -90,10 +93,6 @@
|
||||||
|
|
||||||
{{end}}
|
{{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>
|
<button id="share">share</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -122,7 +121,7 @@
|
||||||
<div id="lyrics">
|
<div id="lyrics">
|
||||||
<p class="album-track-subheading">LYRICS</p>
|
<p class="album-track-subheading">LYRICS</p>
|
||||||
{{if $Track.Lyrics}}
|
{{if $Track.Lyrics}}
|
||||||
{{$Track.GetLyricsHTML}}
|
{{$Track.Lyrics}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="empty">No lyrics.</span>
|
<span class="empty">No lyrics.</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -134,7 +133,7 @@
|
||||||
<h2>TRACKS</h2>
|
<h2>TRACKS</h2>
|
||||||
{{range $i, $track := .Tracks}}
|
{{range $i, $track := .Tracks}}
|
||||||
<details>
|
<details>
|
||||||
<summary class="album-track-title">{{$track.Add $i 1}}. {{$track.Title}}</summary>
|
<summary class="album-track-title">{{$track.Number}}. {{$track.Title}}</summary>
|
||||||
|
|
||||||
{{if $track.Description}}
|
{{if $track.Description}}
|
||||||
<p class="album-track-subheading">DESCRIPTION</p>
|
<p class="album-track-subheading">DESCRIPTION</p>
|
||||||
|
@ -143,7 +142,7 @@
|
||||||
|
|
||||||
<p class="album-track-subheading">LYRICS</p>
|
<p class="album-track-subheading">LYRICS</p>
|
||||||
{{if $track.Lyrics}}
|
{{if $track.Lyrics}}
|
||||||
{{$track.GetLyricsHTML}}
|
{{$track.Lyrics}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="empty">No lyrics.</span>
|
<span class="empty">No lyrics.</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in a new issue