Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
ari melody | 1846203076 | ||
ari melody | e69cf78e57 | ||
ari melody | bc1b6c5872 | ||
ari melody | dc619821bd | ||
ari melody | 19d76ebc47 | ||
ari melody | 2baf71214e | ||
ari melody | c9d950d2b2 |
|
@ -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"]
|
exclude_dir = ["admin/static", "public", "uploads", "test", "db"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
exclude_regex = ["_test.go"]
|
exclude_regex = ["_test.go"]
|
||||||
exclude_unchanged = false
|
exclude_unchanged = false
|
||||||
|
|
11
.dockerignore
Normal file
11
.dockerignore
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
**/.DS_Store
|
||||||
|
.git/
|
||||||
|
.air.toml/
|
||||||
|
.gitattributes
|
||||||
|
.gitignore
|
||||||
|
uploads/*
|
||||||
|
test/
|
||||||
|
tmp/
|
||||||
|
docker-compose.yml
|
||||||
|
Dockerfile
|
||||||
|
schema.sql
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,5 +1,7 @@
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
.idea/
|
.idea/
|
||||||
|
db/
|
||||||
tmp/
|
tmp/
|
||||||
test/
|
test/
|
||||||
uploads/*
|
uploads/*
|
||||||
|
docker-compose-test.yml
|
||||||
|
|
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
FROM golang:1.22 AS build-stage
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o /arimelody-web
|
||||||
|
|
||||||
|
# ---
|
||||||
|
|
||||||
|
FROM build-stage AS build-release-stage
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build-stage /arimelody-web /arimelody-web
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["/arimelody-web"]
|
28
README.md
Normal file
28
README.md
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# 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,9 +3,10 @@ package admin
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody.me/arimelody.me/global"
|
"arimelody-web/global"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -28,9 +29,9 @@ var ADMIN_BYPASS = func() bool {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var ADMIN_ID_DISCORD = func() string {
|
var ADMIN_ID_DISCORD = func() string {
|
||||||
id := global.Args["discordAdmin"]
|
id := os.Getenv("DISCORD_ADMIN")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
fmt.Printf("WARN: Discord admin ID (-discordAdmin) was not provided. Admin login will be unavailable.\n")
|
fmt.Printf("WARN: Discord admin ID (DISCORD_ADMIN) was not provided. Admin login will be unavailable.\n")
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
}()
|
}()
|
||||||
|
|
47
admin/artisthttp.go
Normal file
47
admin/artisthttp.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"arimelody-web/global"
|
||||||
|
"arimelody-web/music/model"
|
||||||
|
"arimelody-web/music/controller"
|
||||||
|
)
|
||||||
|
|
||||||
|
func serveArtist() http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
slices := strings.Split(r.URL.Path[1:], "/")
|
||||||
|
id := slices[0]
|
||||||
|
artist, err := music.GetArtist(global.DB, id)
|
||||||
|
if err != nil {
|
||||||
|
if artist == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Error rendering admin artist page for %s: %s\n", id, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
credits, err := music.GetArtistCredits(global.DB, artist.ID, 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,6 +61,7 @@
|
||||||
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="{{.Artwork}}" alt="" width="128" loading="lazy">
|
<img src="{{.GetArtwork}}" alt="" width="128" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<div class="release-info">
|
<div class="release-info">
|
||||||
<h3 class="release-title">
|
<h3 class="release-title">
|
||||||
|
|
|
@ -3,17 +3,16 @@ 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.me/arimelody.me/discord"
|
"arimelody-web/discord"
|
||||||
"arimelody.me/arimelody.me/global"
|
"arimelody-web/global"
|
||||||
musicModel "arimelody.me/arimelody.me/music/model"
|
musicDB "arimelody-web/music/controller"
|
||||||
musicDB "arimelody.me/arimelody.me/music/controller"
|
musicModel "arimelody-web/music/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type loginData struct {
|
type loginData struct {
|
||||||
|
@ -28,6 +27,7 @@ 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,30 +41,12 @@ func Handler() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
releases, err := musicDB.GetAllReleases(global.DB, false, 0, true)
|
||||||
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 {
|
||||||
|
@ -73,19 +55,17 @@ func Handler() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dbTracks, err := musicDB.GetOrphanTracks(global.DB)
|
tracks, 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
|
||||||
}
|
}
|
||||||
|
|
||||||
var tracks = []musicModel.DisplayTrack{}
|
type IndexData struct {
|
||||||
for _, track := range dbTracks {
|
Releases []*musicModel.Release
|
||||||
tracks = append(tracks, musicModel.DisplayTrack{
|
Artists []*musicModel.Artist
|
||||||
Track: track,
|
Tracks []*musicModel.Track
|
||||||
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pages["index"].Execute(w, IndexData{
|
err = pages["index"].Execute(w, IndexData{
|
||||||
|
@ -125,8 +105,9 @@ 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.(string)
|
token = ctx_session.(*Session).Token
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 ") {
|
||||||
|
@ -168,11 +149,15 @@ 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 == "" {
|
||||||
|
@ -209,8 +194,9 @@ 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)
|
||||||
// TODO: uncomment this probably that might be nice i think
|
if strings.HasPrefix(global.HTTP_DOMAIN, "https") {
|
||||||
// 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,25 +5,26 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody.me/arimelody.me/global"
|
"arimelody-web/global"
|
||||||
"arimelody.me/arimelody.me/music/model"
|
db "arimelody-web/music/controller"
|
||||||
controller "arimelody.me/arimelody.me/music/controller"
|
"arimelody-web/music/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
||||||
fmt.Printf("FATAL: Failed to pull release %s: %s\n", releaseID, err)
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", releaseID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if release == nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
authorised := GetSession(r) != nil
|
authorised := GetSession(r) != nil
|
||||||
if !authorised && !release.Visible {
|
if !authorised && !release.Visible {
|
||||||
|
@ -31,32 +32,25 @@ 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(fullRelease).ServeHTTP(w, r)
|
serveEditCredits(release).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "addcredit":
|
case "addcredit":
|
||||||
serveAddCredit(fullRelease).ServeHTTP(w, r)
|
serveAddCredit(release).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "newcredit":
|
case "newcredit":
|
||||||
serveNewCredit().ServeHTTP(w, r)
|
serveNewCredit().ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "editlinks":
|
case "editlinks":
|
||||||
serveEditLinks(fullRelease).ServeHTTP(w, r)
|
serveEditLinks(release).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "edittracks":
|
case "edittracks":
|
||||||
serveEditTracks(fullRelease).ServeHTTP(w, r)
|
serveEditTracks(release).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "addtrack":
|
case "addtrack":
|
||||||
serveAddTrack(fullRelease).ServeHTTP(w, r)
|
serveAddTrack(release).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case "newtrack":
|
case "newtrack":
|
||||||
serveNewTrack().ServeHTTP(w, r)
|
serveNewTrack().ServeHTTP(w, r)
|
||||||
|
@ -66,7 +60,7 @@ func serveRelease() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pages["release"].Execute(w, fullRelease)
|
err = pages["release"].Execute(w, release)
|
||||||
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)
|
||||||
|
@ -74,7 +68,7 @@ func serveRelease() http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveEditCredits(release *model.FullRelease) http.Handler {
|
func serveEditCredits(release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
||||||
|
@ -85,9 +79,9 @@ func serveEditCredits(release *model.FullRelease) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveAddCredit(release *model.FullRelease) http.Handler {
|
func serveAddCredit(release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
artists, err := controller.GetArtistsNotOnRelease(global.DB, release.Release)
|
artists, err := db.GetArtistsNotOnRelease(global.DB, release.ID)
|
||||||
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)
|
||||||
|
@ -114,7 +108,7 @@ func serveAddCredit(release *model.FullRelease) 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 := controller.GetArtist(global.DB, artistID)
|
artist, err := db.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)
|
||||||
|
@ -134,7 +128,7 @@ func serveNewCredit() http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveEditLinks(release *model.FullRelease) http.Handler {
|
func serveEditLinks(release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
||||||
|
@ -145,7 +139,7 @@ func serveEditLinks(release *model.FullRelease) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveEditTracks(release *model.FullRelease) http.Handler {
|
func serveEditTracks(release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
||||||
|
@ -156,9 +150,9 @@ func serveEditTracks(release *model.FullRelease) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveAddTrack(release *model.FullRelease) http.Handler {
|
func serveAddTrack(release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
tracks, err := controller.GetTracksNotOnRelease(global.DB, release.Release)
|
tracks, err := db.GetTracksNotOnRelease(global.DB, release.ID)
|
||||||
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)
|
||||||
|
@ -186,7 +180,7 @@ func serveAddTrack(release *model.FullRelease) 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 := controller.GetTrack(global.DB, trackID)
|
track, err := db.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)
|
||||||
|
|
151
admin/static/edit-artist.css
Normal file
151
admin/static/edit-artist.css
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#artist {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding: 1.5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1.2em;
|
||||||
|
|
||||||
|
border-radius: .5em;
|
||||||
|
background: #f8f8f8f8;
|
||||||
|
border: 1px solid #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-avatar {
|
||||||
|
width: 200px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.artist-avatar img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
.artist-avatar img:hover {
|
||||||
|
outline: 1px solid #808080;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.artist-avatar #remove-avatar {
|
||||||
|
padding: .3em .4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-info {
|
||||||
|
margin: -1em 0 0 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-header {
|
||||||
|
margin: 1em 0 .2em 0;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-name {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: calc(100% - .4em);
|
||||||
|
padding: .1em .2em;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input[type="text"]:hover {
|
||||||
|
border-color: #80808080;
|
||||||
|
}
|
||||||
|
input[type="text"]:active,
|
||||||
|
input[type="text"]:focus {
|
||||||
|
border-color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, .button {
|
||||||
|
padding: .5em .8em;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
border-radius: .5em;
|
||||||
|
border: 1px solid #a0a0a0;
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
button:hover, .button:hover {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #d0d0d0;
|
||||||
|
}
|
||||||
|
button:active, .button:active {
|
||||||
|
background: #d0d0d0;
|
||||||
|
border-color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
button.save {
|
||||||
|
background: #6fd7ff;
|
||||||
|
border-color: #6f9eb0;
|
||||||
|
}
|
||||||
|
button.delete {
|
||||||
|
background: #ff7171;
|
||||||
|
border-color: #7d3535;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #d0d0d0;
|
||||||
|
}
|
||||||
|
button:active {
|
||||||
|
background: #d0d0d0;
|
||||||
|
border-color: #808080;
|
||||||
|
}
|
||||||
|
button[disabled] {
|
||||||
|
background: #d0d0d0 !important;
|
||||||
|
border-color: #808080 !important;
|
||||||
|
opacity: .5;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.delete {
|
||||||
|
color: #d22828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-actions {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: .5em;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title a.button {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: .5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1em;
|
||||||
|
align-items: center;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-artwork {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit-info h3,
|
||||||
|
.credit-info p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: .9em;
|
||||||
|
}
|
79
admin/static/edit-artist.js
Normal file
79
admin/static/edit-artist.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
const artistID = document.getElementById("artist").dataset.id;
|
||||||
|
const nameInput = document.getElementById("name");
|
||||||
|
const avatarImg = document.getElementById("avatar");
|
||||||
|
const removeAvatarBtn = document.getElementById("remove-avatar");
|
||||||
|
const avatarInput = document.getElementById("avatar-file");
|
||||||
|
const websiteInput = document.getElementById("website");
|
||||||
|
const saveBtn = document.getElementById("save");
|
||||||
|
const deleteBtn = document.getElementById("delete");
|
||||||
|
|
||||||
|
saveBtn.addEventListener("click", () => {
|
||||||
|
fetch("/api/v1/artist/" + artistID, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: nameInput.value,
|
||||||
|
website: websiteInput.value,
|
||||||
|
avatar: avatarImg.src,
|
||||||
|
}),
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
}).then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
res.text().then(error => {
|
||||||
|
console.error(error);
|
||||||
|
alert("Failed to update release: " + error);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = location;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteBtn.addEventListener("click", () => {
|
||||||
|
if (artistID != prompt(
|
||||||
|
"You are about to permanently delete " + artistID + ". " +
|
||||||
|
"This action is irreversible. " +
|
||||||
|
"Please enter \"" + artistID + "\" to continue.")) return;
|
||||||
|
fetch("/api/v1/artist/" + artistID, {
|
||||||
|
method: "DELETE",
|
||||||
|
}).then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
res.text().then(error => {
|
||||||
|
console.error(error);
|
||||||
|
alert("Failed to delete release: " + error);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = "/admin";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[nameInput, websiteInput].forEach(input => {
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
});
|
||||||
|
input.addEventListener("keypress", () => {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
avatarImg.addEventListener("click", () => {
|
||||||
|
avatarInput.addEventListener("change", () => {
|
||||||
|
if (avatarInput.files.length > 0) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
const data = e.target.result;
|
||||||
|
avatarImg.src = data;
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(avatarInput.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
avatarInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
removeAvatarBtn.addEventListener("click", () => {
|
||||||
|
avatarImg.src = "/img/default-avatar.png"
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
});
|
|
@ -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,6 +28,9 @@ 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;
|
||||||
|
@ -342,7 +345,7 @@ dialog div.dialog-actions {
|
||||||
background-color: #8cff83
|
background-color: #8cff83
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.links a.button[data-name="applemusic"] {
|
.card.links a.button[data-name="apple music"] {
|
||||||
background-color: #8cd9ff
|
background-color: #8cd9ff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -548,3 +551,14 @@ 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,47 +1,36 @@
|
||||||
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);
|
|
||||||
|
|
||||||
function updateData(old) {
|
|
||||||
var releaseData = {
|
|
||||||
visible: visInput.value === "true",
|
|
||||||
title: titleInput.value,
|
|
||||||
description: descInput.value,
|
|
||||||
type: typeInput.value,
|
|
||||||
releaseDate: dateInput.value,
|
|
||||||
artwork: artworkData,
|
|
||||||
buyname: buynameInput.value,
|
|
||||||
buylink: buylinkInput.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (releaseData && releaseData != old) {
|
|
||||||
edited.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return releaseData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveRelease() {
|
|
||||||
console.table(releaseData);
|
|
||||||
|
|
||||||
|
saveBtn.addEventListener("click", () => {
|
||||||
fetch("/api/v1/music/" + releaseID, {
|
fetch("/api/v1/music/" + releaseID, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(releaseData),
|
body: JSON.stringify({
|
||||||
|
visible: visInput.value === "true",
|
||||||
|
title: titleInput.value,
|
||||||
|
description: descInput.value,
|
||||||
|
type: typeInput.value,
|
||||||
|
releaseDate: dateInput.value + ":00Z",
|
||||||
|
artwork: artworkData,
|
||||||
|
buyname: buynameInput.value,
|
||||||
|
buylink: buylinkInput.value,
|
||||||
|
copyright: copyrightInput.value,
|
||||||
|
copyrightURL: copyrightURLInput.value,
|
||||||
|
}),
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
@ -54,9 +43,13 @@ function saveRelease() {
|
||||||
|
|
||||||
location = location;
|
location = location;
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
function deleteRelease() {
|
deleteBtn.addEventListener("click", () => {
|
||||||
|
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 => {
|
||||||
|
@ -70,15 +63,17 @@ function deleteRelease() {
|
||||||
|
|
||||||
location = "/admin";
|
location = "/admin";
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
edited.onUpdate(edited => {
|
|
||||||
saveBtn.disabled = !edited;
|
|
||||||
})
|
|
||||||
|
|
||||||
titleInput.addEventListener("change", () => {
|
|
||||||
releaseData = updateData(releaseData);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[titleInput, typeInput, descInput, dateInput, buynameInput, buylinkInput, copyrightInput, copyrightURLInput, visInput].forEach(input => {
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
});
|
||||||
|
input.addEventListener("keypress", () => {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
artworkImg.addEventListener("click", () => {
|
artworkImg.addEventListener("click", () => {
|
||||||
artworkInput.addEventListener("change", () => {
|
artworkInput.addEventListener("change", () => {
|
||||||
if (artworkInput.files.length > 0) {
|
if (artworkInput.files.length > 0) {
|
||||||
|
@ -87,41 +82,17 @@ artworkImg.addEventListener("click", () => {
|
||||||
const data = e.target.result;
|
const data = e.target.result;
|
||||||
artworkImg.src = data;
|
artworkImg.src = data;
|
||||||
artworkData = data;
|
artworkData = data;
|
||||||
releaseData = updateData(releaseData);
|
saveBtn.disabled = false;
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(artworkInput.files[0]);
|
reader.readAsDataURL(artworkInput.files[0]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
artworkInput.click();
|
artworkInput.click();
|
||||||
});
|
});
|
||||||
typeInput.addEventListener("change", () => {
|
|
||||||
releaseData = updateData(releaseData);
|
|
||||||
});
|
|
||||||
descInput.addEventListener("change", () => {
|
|
||||||
releaseData = updateData(releaseData);
|
|
||||||
});
|
|
||||||
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", () => {
|
removeArtworkBtn.addEventListener("click", () => {
|
||||||
if (releaseID != prompt(
|
artworkImg.src = "/img/default-cover-art.png"
|
||||||
"You are about to permanently delete " + releaseID + ". " +
|
artworkData = "";
|
||||||
"This action is irreversible. " +
|
saveBtn.disabled = false;
|
||||||
"Please enter \"" + releaseID + "\" to continue.")) return;
|
|
||||||
deleteRelease();
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ h1 {
|
||||||
|
|
||||||
#track {
|
#track {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
padding: 1.5em;
|
padding: .5em 1.5em 1.5em 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: 100%;
|
width: calc(100% - .4em);
|
||||||
padding: .1em .2em;
|
padding: .1em .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
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 => {
|
||||||
|
@ -24,6 +25,30 @@ newReleaseBtn.addEventListener("click", event => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
newArtistBtn.addEventListener("click", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const id = prompt("Enter an ID for this artist:");
|
||||||
|
if (id == null || id == "") return;
|
||||||
|
|
||||||
|
fetch("/api/v1/artist", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({id})
|
||||||
|
}).then(res => {
|
||||||
|
res.text().then(text => {
|
||||||
|
if (res.ok) {
|
||||||
|
location = "/admin/artist/" + id;
|
||||||
|
} else {
|
||||||
|
alert("Request failed: " + text);
|
||||||
|
console.error(text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Failed to create artist. Check the console for details.");
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
newTrackBtn.addEventListener("click", event => {
|
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:");
|
||||||
|
@ -43,7 +68,7 @@ newTrackBtn.addEventListener("click", event => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
alert("Failed to create release. Check the console for details.");
|
alert("Failed to create track. Check the console for details.");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,6 +29,11 @@ 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.me/arimelody.me/global"
|
"arimelody-web/global"
|
||||||
"arimelody.me/arimelody.me/music/model"
|
"arimelody-web/music/model"
|
||||||
"arimelody.me/arimelody.me/music/controller"
|
"arimelody-web/music/controller"
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveTrack() http.Handler {
|
func serveTrack() http.Handler {
|
||||||
|
@ -25,26 +25,16 @@ func serveTrack() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dbReleases, err := music.GetTrackReleases(global.DB, track)
|
releases, err := music.GetTrackReleases(global.DB, track.ID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error rendering admin track page for %s: %s\n", id, err)
|
fmt.Printf("FATAL: Failed to pull releases for %s: %s\n", id, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
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.FullRelease
|
Releases []*model.Release
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pages["track"].Execute(w, Track{ Track: track, Releases: releases })
|
err = pages["track"].Execute(w, Track{ Track: track, Releases: releases })
|
||||||
|
|
72
admin/views/edit-artist.html
Normal file
72
admin/views/edit-artist.html
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
{{define "head"}}
|
||||||
|
<title>Editing {{.Name}} - ari melody 💫</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/admin/static/edit-artist.css">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<main>
|
||||||
|
<h1>Editing Artist</h1>
|
||||||
|
|
||||||
|
<div id="artist" data-id="{{.ID}}">
|
||||||
|
<div class="artist-avatar">
|
||||||
|
<img src="{{.Avatar}}" alt="" width="256" loading="lazy" id="avatar">
|
||||||
|
<input type="file" id="avatar-file" name="Artwork" accept=".png,.jpg,.jpeg" hidden>
|
||||||
|
<button id="remove-avatar">Remove</button>
|
||||||
|
</div>
|
||||||
|
<div class="artist-info">
|
||||||
|
<p class="attribute-header">Name</p>
|
||||||
|
<h2 class="artist-name">
|
||||||
|
<input type="text" id="name" name="artist-name" value="{{.Name}}">
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="attribute-header">Website</p>
|
||||||
|
<input type="text" id="website" name="website" value="{{.Website}}">
|
||||||
|
|
||||||
|
<div class="artist-actions">
|
||||||
|
<button type="submit" class="save" id="save" disabled>Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>Featured in</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card releases">
|
||||||
|
{{if .Credits}}
|
||||||
|
{{range .Credits}}
|
||||||
|
<div class="credit">
|
||||||
|
<img src="{{.Release.Artwork}}" alt="" width="64" loading="lazy" class="release-artwork">
|
||||||
|
<div class="credit-info">
|
||||||
|
<h3 class="credit-name"><a href="/admin/release/{{.Release.ID}}">{{.Release.Title}}</a></h3>
|
||||||
|
<p class="credit-artists">{{.Release.PrintArtists true true}}</p>
|
||||||
|
<p class="artist-role">
|
||||||
|
Role: {{.Role}}
|
||||||
|
{{if .Primary}}
|
||||||
|
<small>(Primary)</small>
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<p>This artist has no credits.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>Danger Zone</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card danger">
|
||||||
|
<p>
|
||||||
|
Clicking the button below will delete this artist.
|
||||||
|
This action is <strong>irreversible</strong>.
|
||||||
|
You will be prompted to confirm this decision.
|
||||||
|
</p>
|
||||||
|
<button class="delete" id="delete">Delete Artist</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="/admin/static/edit-artist.js" defer></script>
|
||||||
|
{{end}}
|
|
@ -12,10 +12,11 @@
|
||||||
<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}}">
|
<input type="text" id="title" name="Title" value="{{.Title}}" autocomplete="on">
|
||||||
</h1>
|
</h1>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -53,19 +54,31 @@
|
||||||
<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="Buy Name" id="buyname" value="{{.Buyname}}">
|
<input type="text" name="buyname" id="buyname" value="{{.Buyname}}" autocomplete="on">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Buy Link</td>
|
<td>Buy Link</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="Buy Link" id="buylink" value="{{.Buylink}}">
|
<input type="text" name="buylink" id="buylink" value="{{.Buylink}}" autocomplete="on">
|
||||||
|
</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>
|
||||||
|
@ -99,7 +112,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/artists/{{.Artist.ID}}">{{.Artist.Name}}</a></p>
|
<p class="artist-name"><a href="/admin/artist/{{.Artist.ID}}">{{.Artist.Name}}</a></p>
|
||||||
<p class="artist-role">
|
<p class="artist-role">
|
||||||
{{.Role}}
|
{{.Role}}
|
||||||
{{if .Primary}}
|
{{if .Primary}}
|
||||||
|
@ -139,23 +152,23 @@
|
||||||
>Edit</a>
|
>Edit</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card tracks">
|
<div class="card tracks">
|
||||||
{{range .Tracks}}
|
{{range $i, $track := .Tracks}}
|
||||||
<div class="track" data-id="{{.ID}}">
|
<div class="track" data-id="{{$track.ID}}">
|
||||||
<h2 class="track-title">
|
<h2 class="track-title">
|
||||||
<span class="track-number">{{.Number}}</span>
|
<span class="track-number">{{.Add $i 1}}</span>
|
||||||
<a href="/admin/track/{{.ID}}">{{.Title}}</a>
|
<a href="/admin/track/{{$track.ID}}">{{$track.Title}}</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
{{if .Description}}
|
{{if $track.Description}}
|
||||||
<p class="track-description">{{.Description}}</p>
|
<p class="track-description">{{$track.GetDescriptionHTML}}</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 .Lyrics}}
|
{{if $track.Lyrics}}
|
||||||
<p class="track-lyrics">{{.Lyrics}}</p>
|
<p class="track-lyrics">{{$track.GetLyricsHTML}}</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">Create New</a>
|
<a class="create-btn" id="create-artist">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/artists/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a>
|
<a href="/admin/artist/{{$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.Description}}</p>
|
<p class="track-description">{{$Track.GetDescriptionHTML}}</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.Lyrics}}</p>
|
<p class="track-lyrics">{{$Track.GetLyricsHTML}}</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.me/arimelody.me/admin"
|
"arimelody-web/admin"
|
||||||
"arimelody.me/arimelody.me/global"
|
"arimelody-web/global"
|
||||||
"arimelody.me/arimelody.me/music/model"
|
music "arimelody-web/music/controller"
|
||||||
music "arimelody.me/arimelody.me/music/view"
|
musicView "arimelody-web/music/view"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Handler() http.Handler {
|
func Handler() http.Handler {
|
||||||
|
@ -18,11 +18,14 @@ 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]
|
||||||
var artist model.Artist
|
artist, err := music.GetArtist(global.DB, artistID)
|
||||||
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.NotFound(w, r)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,18 +60,21 @@ 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]
|
||||||
var release model.Release
|
release, err := music.GetRelease(global.DB, releaseID, true)
|
||||||
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.NotFound(w, r)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
// GET /api/v1/music/{id}
|
// GET /api/v1/music/{id}
|
||||||
music.ServeRelease(release).ServeHTTP(w, r)
|
musicView.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)
|
||||||
|
@ -96,11 +102,14 @@ 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]
|
||||||
var track model.Track
|
track, err := music.GetTrack(global.DB, trackID)
|
||||||
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.NotFound(w, r)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
122
api/artist.go
122
api/artist.go
|
@ -3,24 +3,23 @@ package api
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody.me/arimelody.me/global"
|
"arimelody-web/admin"
|
||||||
"arimelody.me/arimelody.me/music/model"
|
"arimelody-web/global"
|
||||||
|
db "arimelody-web/music/controller"
|
||||||
|
music "arimelody-web/music/controller"
|
||||||
|
"arimelody-web/music/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type artistJSON struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name *string `json:"name"`
|
|
||||||
Website *string `json:"website"`
|
|
||||||
Avatar *string `json:"avatar"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeAllArtists() http.Handler {
|
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{}
|
||||||
err := global.DB.Select(&artists, "SELECT * FROM artist")
|
artists, err := db.GetAllArtists(global.DB)
|
||||||
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)
|
||||||
|
@ -35,28 +34,37 @@ 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"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var credits = map[string]creditJSON{}
|
show_hidden_releases := admin.GetSession(r) != nil
|
||||||
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,
|
||||||
|
@ -70,39 +78,23 @@ 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 data artistJSON
|
var artist model.Artist
|
||||||
err := json.NewDecoder(r.Body).Decode(&data)
|
err := json.NewDecoder(r.Body).Decode(&artist)
|
||||||
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 data.ID == "" {
|
if artist.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 data.Name == nil || *data.Name == "" {
|
if artist.Name == "" { artist.Name = artist.ID }
|
||||||
http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var artist = model.Artist{
|
err = music.CreateArtist(global.DB, &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", data.ID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.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)
|
||||||
|
@ -114,43 +106,59 @@ func CreateArtist() http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateArtist(artist model.Artist) http.Handler {
|
func UpdateArtist(artist *model.Artist) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var data artistJSON
|
err := json.NewDecoder(r.Body).Decode(&artist)
|
||||||
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 data.ID != "" { artist.ID = data.ID }
|
if artist.Avatar == "" {
|
||||||
if data.Name != nil { artist.Name = *data.Name }
|
artist.Avatar = "/img/default-avatar.png"
|
||||||
if data.Website != nil { artist.Website = *data.Website }
|
} else {
|
||||||
if data.Avatar != nil { artist.Avatar = *data.Avatar }
|
if strings.Contains(artist.Avatar, ";base64,") {
|
||||||
|
var artworkDirectory = filepath.Join("uploads", "avatar")
|
||||||
|
filename, err := HandleImageUpload(&artist.Avatar, artworkDirectory, artist.ID)
|
||||||
|
|
||||||
_, err = global.DB.Exec(
|
// clean up files with this ID and different extensions
|
||||||
"UPDATE artist "+
|
err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error {
|
||||||
"SET name=$2, website=$3, avatar=$4 "+
|
if path == filepath.Join(artworkDirectory, filename) { return nil }
|
||||||
"WHERE id=$1",
|
|
||||||
artist.ID,
|
withoutExt := strings.TrimSuffix(path, filepath.Ext(path))
|
||||||
artist.Name,
|
if withoutExt != filepath.Join(artworkDirectory, artist.ID) { return nil }
|
||||||
artist.Website,
|
|
||||||
artist.Avatar)
|
return os.Remove(path)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Error while cleaning up avatar files: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
artist.Avatar = fmt.Sprintf("/uploads/avatar/%s", filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = music.UpdateArtist(global.DB, artist)
|
||||||
if err != nil {
|
if 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 := global.DB.Exec(
|
err := music.DeleteArtist(global.DB, artist.ID)
|
||||||
"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)
|
||||||
}
|
}
|
||||||
|
|
283
api/release.go
283
api/release.go
|
@ -10,54 +10,51 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody.me/arimelody.me/admin"
|
"arimelody-web/admin"
|
||||||
"arimelody.me/arimelody.me/global"
|
"arimelody-web/global"
|
||||||
"arimelody.me/arimelody.me/music/model"
|
music "arimelody-web/music/controller"
|
||||||
|
"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) {
|
||||||
type catalogItem struct {
|
releases, err := music.GetAllReleases(global.DB, false, 0, true)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
catalog := []catalogItem{}
|
type Release struct {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
catalog = append(catalog, catalogItem{
|
artists := []string{}
|
||||||
|
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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,80 +74,34 @@ func CreateRelease() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var data releaseBodyJSON
|
var release model.Release
|
||||||
err := json.NewDecoder(r.Body).Decode(&data)
|
err := json.NewDecoder(r.Body).Decode(&release)
|
||||||
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 data.ID == "" {
|
if release.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
|
||||||
}
|
}
|
||||||
|
|
||||||
title := data.ID
|
if release.Title == "" { release.Title = release.ID }
|
||||||
if data.Title != nil && *data.Title != "" {
|
if release.ReleaseType == "" { release.ReleaseType = model.Single }
|
||||||
title = *data.Title
|
|
||||||
}
|
|
||||||
|
|
||||||
description := ""
|
|
||||||
if data.Description != nil && *data.Description != "" { description = *data.Description }
|
|
||||||
|
|
||||||
releaseType := model.Single
|
if release.ReleaseDate != time.Unix(0, 0) {
|
||||||
if data.ReleaseType != nil && *data.ReleaseType != "" { releaseType = *data.ReleaseType }
|
release.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
releaseDate := time.Time{}
|
|
||||||
if data.ReleaseDate != nil && *data.ReleaseDate != "" {
|
|
||||||
releaseDate, err = time.Parse("2006-01-02T15:04", *data.ReleaseDate)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid release date", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} 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 release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" }
|
||||||
if data.Artwork != nil && *data.Artwork != "" { artwork = *data.Artwork }
|
|
||||||
|
|
||||||
buyname := ""
|
err = music.CreateRelease(global.DB, &release)
|
||||||
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 err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("Failed to create release %s: %s\n", release.ID, err)
|
fmt.Printf("FATAL: Failed to create release %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -165,7 +116,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)
|
||||||
|
@ -173,14 +124,6 @@ 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] {
|
||||||
|
@ -199,30 +142,19 @@ func UpdateRelease(release model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var data releaseBodyJSON
|
err := json.NewDecoder(r.Body).Decode(&release)
|
||||||
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 data.ID != "" { release.ID = data.ID }
|
if release.Artwork == "" {
|
||||||
if data.Visible != nil { release.Visible = *data.Visible }
|
release.Artwork = "/img/default-cover-art.png"
|
||||||
if data.Title != nil { release.Title = *data.Title }
|
} else {
|
||||||
if data.Description != nil { release.Description = *data.Description }
|
if strings.Contains(release.Artwork, ";base64,") {
|
||||||
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(data.Artwork, artworkDirectory, data.ID)
|
filename, err := HandleImageUpload(&release.Artwork, artworkDirectory, release.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 {
|
||||||
|
@ -238,35 +170,22 @@ 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Buyname != nil { release.Buyname = *data.Buyname }
|
err = music.UpdateRelease(global.DB, release)
|
||||||
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)
|
||||||
|
@ -275,26 +194,19 @@ func UpdateReleaseTracks(release model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tx := global.DB.MustBegin()
|
err = music.UpdateReleaseTracks(global.DB, release.ID, trackIDs)
|
||||||
tx.MustExec("DELETE FROM musicreleasetrack WHERE release=$1", release.ID)
|
|
||||||
for i, trackID := range trackIDs {
|
|
||||||
tx.MustExec(
|
|
||||||
"INSERT INTO musicreleasetrack "+
|
|
||||||
"(release, track, number) "+
|
|
||||||
"VALUES ($1, $2, $3)",
|
|
||||||
release.ID,
|
|
||||||
trackID,
|
|
||||||
i)
|
|
||||||
}
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to update tracks for %s: %s\n", release.ID, err)
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("FATAL: Failed to update tracks for %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
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
|
||||||
|
@ -308,52 +220,34 @@ func UpdateReleaseCredits(release model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear duplicates
|
var credits []*model.Credit
|
||||||
type Credit struct {
|
|
||||||
Role string
|
|
||||||
Primary bool
|
|
||||||
}
|
|
||||||
var credits = map[string]Credit{}
|
|
||||||
for _, credit := range data {
|
for _, credit := range data {
|
||||||
credits[credit.Artist] = Credit{
|
credits = append(credits, &model.Credit{
|
||||||
|
Artist: model.Artist{
|
||||||
|
ID: credit.Artist,
|
||||||
|
},
|
||||||
Role: credit.Role,
|
Role: credit.Role,
|
||||||
Primary: credit.Primary,
|
Primary: credit.Primary,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
tx := global.DB.MustBegin()
|
err = music.UpdateReleaseCredits(global.DB, release.ID, credits)
|
||||||
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 {
|
||||||
fmt.Printf("Failed to update links for %s: %s\n", release.ID, err)
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
|
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)
|
||||||
|
@ -367,30 +261,27 @@ func UpdateReleaseLinks(release model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tx := global.DB.MustBegin()
|
err = music.UpdateReleaseLinks(global.DB, release.ID, links)
|
||||||
tx.MustExec("DELETE FROM musiclink WHERE release=$1", release.ID)
|
|
||||||
for _, link := range links {
|
|
||||||
tx.MustExec(
|
|
||||||
"INSERT INTO musiclink "+
|
|
||||||
"(release, name, url) "+
|
|
||||||
"VALUES ($1, $2, $3)",
|
|
||||||
release.ID,
|
|
||||||
link.Name,
|
|
||||||
link.URL)
|
|
||||||
}
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to update links for %s: %s\n", release.ID, err)
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("FATAL: Failed to update links for %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
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 := global.DB.Exec("DELETE FROM musicrelease WHERE id=$1", release.ID)
|
err := music.DeleteRelease(global.DB, release.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to delete release %s: %s\n", release.ID, err)
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("FATAL: Failed to delete release %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
107
api/track.go
107
api/track.go
|
@ -5,24 +5,40 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"arimelody.me/arimelody.me/global"
|
"arimelody-web/global"
|
||||||
"arimelody.me/arimelody.me/music/model"
|
music "arimelody-web/music/controller"
|
||||||
|
"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{}
|
||||||
|
|
||||||
err := global.DB.Select(&tracks, "SELECT id, title FROM musictrack")
|
var dbTracks = []*model.Track{}
|
||||||
|
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 {
|
||||||
|
@ -32,42 +48,23 @@ 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) {
|
||||||
if r.URL.Path == "/" {
|
dbReleases, err := music.GetTrackReleases(global.DB, track.ID, false)
|
||||||
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 {
|
|
||||||
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 {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", trackID, err)
|
fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", track.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
type response struct {
|
releases := []string{}
|
||||||
model.Track
|
for _, release := range dbReleases {
|
||||||
Releases []*model.Release
|
releases = append(releases, release.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
err = json.NewEncoder(w).Encode(response{ track, releases })
|
err = json.NewEncoder(w).Encode(Track{ track, releases })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to serve track %s: %s\n", trackID, err)
|
fmt.Printf("FATAL: Failed to serve 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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -92,15 +89,7 @@ func CreateTrack() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var trackID string
|
id, err := music.CreateTrack(global.DB, &track)
|
||||||
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)
|
||||||
|
@ -109,46 +98,29 @@ 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(trackID))
|
w.Write([]byte(id))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
var update model.Track
|
err := json.NewDecoder(r.Body).Decode(&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 update.Title == "" {
|
if track.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
|
||||||
}
|
}
|
||||||
|
|
||||||
var trackID = r.URL.Path[1:]
|
err = music.UpdateTrack(global.DB, track)
|
||||||
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)
|
||||||
|
@ -163,7 +135,7 @@ func UpdateTrack(track model.Track) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteTrack(track model.Track) http.Handler {
|
func DeleteTrack(track *model.Track) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
||||||
|
@ -171,10 +143,7 @@ func DeleteTrack(track model.Track) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
var trackID = r.URL.Path[1:]
|
var trackID = r.URL.Path[1:]
|
||||||
_, err := global.DB.Exec(
|
err := music.DeleteTrack(global.DB, trackID)
|
||||||
"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,6 +35,8 @@ 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,26 +6,27 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody.me/arimelody.me/global"
|
"arimelody-web/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 := global.Args["discordClient"]
|
id := os.Getenv("DISCORD_CLIENT")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
fmt.Printf("WARN: Discord client ID (-discordClient) was not provided. Admin login will be unavailable.\n")
|
fmt.Printf("WARN: Discord client ID (DISCORD_CLIENT) was not provided. Admin login will be unavailable.\n")
|
||||||
CREDENTIALS_PROVIDED = false
|
CREDENTIALS_PROVIDED = false
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
}()
|
}()
|
||||||
var CLIENT_SECRET = func() string {
|
var CLIENT_SECRET = func() string {
|
||||||
secret := global.Args["discordSecret"]
|
secret := os.Getenv("DISCORD_SECRET")
|
||||||
if secret== "" {
|
if secret == "" {
|
||||||
fmt.Printf("WARN: Discord secret (-discordSecret) was not provided. Admin login will be unavailable.\n")
|
fmt.Printf("WARN: Discord secret (DISCORD_SECRET) was not provided. Admin login will be unavailable.\n")
|
||||||
CREDENTIALS_PROVIDED = false
|
CREDENTIALS_PROVIDED = false
|
||||||
}
|
}
|
||||||
return secret
|
return secret
|
||||||
|
@ -107,7 +108,6 @@ 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))
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
version: '3.9'
|
|
||||||
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
image: postgres:16.1-alpine3.18
|
|
||||||
container_name: arimelody.me-db
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
volumes:
|
|
||||||
- arimelody-db:/var/lib/postgresql/data
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: arimelody
|
|
||||||
POSTGRES_USER: arimelody
|
|
||||||
POSTGRES_PASSWORD: fuckingpassword
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
arimelody-db:
|
|
||||||
external: true
|
|
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: docker.arimelody.me/arimelody.me:latest
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
environment:
|
||||||
|
HTTP_DOMAIN: "https://arimelody.me"
|
||||||
|
ARIMELODY_DB_HOST: db
|
||||||
|
DISCORD_ADMIN: # your discord user ID.
|
||||||
|
DISCORD_CLIENT: # your discord OAuth client ID.
|
||||||
|
DISCORD_SECRET: # your discord OAuth secret.
|
||||||
|
db:
|
||||||
|
image: postgres:16.1-alpine3.18
|
||||||
|
volumes:
|
||||||
|
- ./db:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: arimelody
|
||||||
|
POSTGRES_USER: arimelody
|
||||||
|
POSTGRES_PASSWORD: fuckingpassword
|
|
@ -35,11 +35,11 @@ var Args = func() map[string]string {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var HTTP_DOMAIN = func() string {
|
var HTTP_DOMAIN = func() string {
|
||||||
domain := Args["httpDomain"]
|
domain := os.Getenv("HTTP_DOMAIN")
|
||||||
if domain != "" {
|
if domain == "" {
|
||||||
return domain
|
return "https://arimelody.me"
|
||||||
}
|
}
|
||||||
return "https://arimelody.me"
|
return domain
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var DB *sqlx.DB
|
var DB *sqlx.DB
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody.me/arimelody.me/colour"
|
"arimelody-web/colour"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DefaultHeaders(next http.Handler) http.Handler {
|
func DefaultHeaders(next http.Handler) http.Handler {
|
||||||
|
|
15
go.mod
15
go.mod
|
@ -1,19 +1,8 @@
|
||||||
module arimelody.me/arimelody.me
|
module arimelody-web
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
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,30 +1,10 @@
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
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.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
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,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -8,11 +9,11 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody.me/arimelody.me/admin"
|
"arimelody-web/admin"
|
||||||
"arimelody.me/arimelody.me/api"
|
"arimelody-web/api"
|
||||||
"arimelody.me/arimelody.me/global"
|
"arimelody-web/global"
|
||||||
musicView "arimelody.me/arimelody.me/music/view"
|
musicView "arimelody-web/music/view"
|
||||||
"arimelody.me/arimelody.me/templates"
|
"arimelody-web/templates"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
|
@ -22,8 +23,11 @@ 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", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable")
|
global.DB, err = sqlx.Connect("postgres", "host=" + dbHost + " user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable")
|
||||||
if err != nil {
|
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)
|
||||||
|
@ -64,9 +68,10 @@ 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 os.IsNotExist(err) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package music
|
package music
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody.me/arimelody.me/music/model"
|
"arimelody-web/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, release *model.Release) ([]*model.Artist, error) {
|
func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*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)",
|
||||||
release.ID)
|
releaseID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,60 @@ func GetArtistsNotOnRelease(db *sqlx.DB, release *model.Release) ([]*model.Artis
|
||||||
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) "+
|
||||||
|
@ -77,11 +131,11 @@ func UpdateArtist(db *sqlx.DB, artist *model.Artist) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteArtist(db *sqlx.DB, artist *model.Artist) error {
|
func DeleteArtist(db *sqlx.DB, artistID string) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"DELETE FROM artist "+
|
"DELETE FROM artist "+
|
||||||
"WHERE id=$1",
|
"WHERE id=$1",
|
||||||
artist.ID,
|
artistID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
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,54 +1,110 @@
|
||||||
package music
|
package music
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody.me/arimelody.me/music/model"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"arimelody-web/music/model"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetRelease(db *sqlx.DB, id string) (*model.Release, error) {
|
func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
|
||||||
var releases = model.Release{}
|
var release = model.Release{}
|
||||||
|
|
||||||
err := db.Get(&releases, "SELECT * FROM musicrelease WHERE id=$1", id)
|
err := db.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &releases, nil
|
if full {
|
||||||
|
// get credits
|
||||||
|
credits, err := GetReleaseCredits(db, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(fmt.Sprintf("Credits: %s", err))
|
||||||
|
}
|
||||||
|
for _, credit := range credits {
|
||||||
|
release.Credits = append(release.Credits, credit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get tracks
|
||||||
|
tracks, err := GetReleaseTracks(db, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(fmt.Sprintf("Tracks: %s", err))
|
||||||
|
}
|
||||||
|
for _, track := range tracks {
|
||||||
|
release.Tracks = append(release.Tracks, track)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get links
|
||||||
|
links, err := GetReleaseLinks(db, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(fmt.Sprintf("Links: %s", err))
|
||||||
|
}
|
||||||
|
for _, link := range links {
|
||||||
|
release.Links = append(release.Links, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &release, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllReleases(db *sqlx.DB) ([]*model.Release, error) {
|
func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*model.Release, error) {
|
||||||
var releases = []*model.Release{}
|
var releases = []*model.Release{}
|
||||||
|
|
||||||
err := db.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC")
|
query := "SELECT * FROM musicrelease"
|
||||||
|
if onlyVisible {
|
||||||
|
query += " WHERE visible=true"
|
||||||
|
}
|
||||||
|
query += " ORDER BY release_date DESC"
|
||||||
|
var err error
|
||||||
|
if limit > 0 {
|
||||||
|
err = db.Select(&releases, query + " LIMIT $1", limit)
|
||||||
|
} else {
|
||||||
|
err = db.Select(&releases, query)
|
||||||
|
}
|
||||||
if err != nil {
|
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) "+
|
"(id, visible, title, description, type, release_date, artwork, buyname, buylink, copyright, copyrighturl) "+
|
||||||
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
|
||||||
release.ID,
|
release.ID,
|
||||||
release.Visible,
|
release.Visible,
|
||||||
release.Title,
|
release.Title,
|
||||||
|
@ -58,6 +114,8 @@ 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
|
||||||
|
@ -69,7 +127,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 "+
|
"visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9, copyright=$10, copyrighturl=$11 "+
|
||||||
"WHERE id=$1",
|
"WHERE id=$1",
|
||||||
release.ID,
|
release.ID,
|
||||||
release.Visible,
|
release.Visible,
|
||||||
|
@ -80,6 +138,8 @@ 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
|
||||||
|
@ -88,49 +148,53 @@ func UpdateRelease(db *sqlx.DB, release *model.Release) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateReleaseTracks(db *sqlx.DB, release *model.Release, new_tracks []*model.Track) error {
|
func UpdateReleaseTracks(db *sqlx.DB, releaseID string, new_tracks []string) error {
|
||||||
_, err := db.Exec(
|
tx, err := db.Begin()
|
||||||
"DELETE FROM musicreleasetrack "+
|
|
||||||
"WHERE release=$1",
|
|
||||||
release.ID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, track := range new_tracks {
|
_, err = tx.Exec("DELETE FROM musicreleasetrack WHERE release=$1", releaseID)
|
||||||
_, err = db.Exec(
|
if err != nil {
|
||||||
|
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)",
|
||||||
release.ID,
|
releaseID,
|
||||||
track.ID,
|
trackID,
|
||||||
i,
|
i)
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
err = tx.Commit()
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateReleaseCredits(db *sqlx.DB, release *model.Release, new_credits []*model.Credit) error {
|
|
||||||
_, err := db.Exec(
|
|
||||||
"DELETE FROM musiccredit "+
|
|
||||||
"WHERE release=$1",
|
|
||||||
release.ID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateReleaseCredits(db *sqlx.DB, releaseID string, new_credits []*model.Credit) error {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec("DELETE FROM musiccredit WHERE release=$1", releaseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for _, credit := range new_credits {
|
for _, credit := range new_credits {
|
||||||
_, err = db.Exec(
|
_, err = tx.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)",
|
||||||
release.ID,
|
releaseID,
|
||||||
credit.Artist.ID,
|
credit.Artist.ID,
|
||||||
credit.Role,
|
credit.Role,
|
||||||
credit.Primary,
|
credit.Primary,
|
||||||
|
@ -140,25 +204,31 @@ func UpdateReleaseCredits(db *sqlx.DB, release *model.Release, new_credits []*mo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
err = tx.Commit()
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateReleaseLinks(db *sqlx.DB, release *model.Release, new_links []*model.Link) error {
|
|
||||||
_, err := db.Exec(
|
|
||||||
"DELETE FROM musiclink "+
|
|
||||||
"WHERE release=$1",
|
|
||||||
release.ID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateReleaseLinks(db *sqlx.DB, releaseID string, new_links []*model.Link) error {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec("DELETE FROM musiclink WHERE release=$1", releaseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for _, link := range new_links {
|
for _, link := range new_links {
|
||||||
_, err = db.Exec(
|
fmt.Printf("%s: %s\n", link.Name, link.URL)
|
||||||
|
_, err := tx.Exec(
|
||||||
"INSERT INTO musiclink "+
|
"INSERT INTO musiclink "+
|
||||||
"(release, name, url) "+
|
"(release, name, url) "+
|
||||||
"VALUES ($1, $2, $3)",
|
"VALUES ($1, $2, $3)",
|
||||||
release.ID,
|
releaseID,
|
||||||
link.Name,
|
link.Name,
|
||||||
link.URL,
|
link.URL,
|
||||||
)
|
)
|
||||||
|
@ -167,14 +237,19 @@ func UpdateReleaseLinks(db *sqlx.DB, release *model.Release, new_links []*model.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteRelease(db *sqlx.DB, release *model.Release) error {
|
func DeleteRelease(db *sqlx.DB, releaseID string) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"DELETE FROM musicrelease "+
|
"DELETE FROM musicrelease "+
|
||||||
"WHERE id=$1",
|
"WHERE id=$1",
|
||||||
release.ID,
|
releaseID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -183,33 +258,60 @@ func DeleteRelease(db *sqlx.DB, release *model.Release) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFullRelease(db *sqlx.DB, release *model.Release) (*model.FullRelease, error) {
|
func GetReleaseTracks(db *sqlx.DB, releaseID string) ([]*model.Track, error) {
|
||||||
// get credits
|
var tracks = []*model.Track{}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// get tracks
|
return tracks, nil
|
||||||
dbTracks, err := GetReleaseTracks(db, release)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
func GetReleaseCredits(db *sqlx.DB, releaseID string) ([]*model.Credit, error) {
|
||||||
}
|
rows, err := db.Query(
|
||||||
tracks := []model.DisplayTrack{}
|
"SELECT artist.id,artist.name,artist.website,artist.avatar,role,is_primary "+
|
||||||
for i, track := range dbTracks {
|
"FROM musiccredit "+
|
||||||
tracks = append(tracks, track.MakeDisplay(i + 1))
|
"JOIN artist ON artist=artist.id "+
|
||||||
}
|
"JOIN musicrelease ON release=musicrelease.id "+
|
||||||
|
"WHERE musicrelease.id=$1 "+
|
||||||
// get links
|
"ORDER BY is_primary DESC",
|
||||||
links, err := GetReleaseLinks(db, release)
|
releaseID,
|
||||||
if err != nil {
|
)
|
||||||
return nil, err
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
|
}
|
||||||
return &model.FullRelease{
|
|
||||||
Release: release,
|
var credits []*model.Credit
|
||||||
Tracks: tracks,
|
for rows.Next() {
|
||||||
Credits: credits,
|
credit := model.Credit{}
|
||||||
Links: links,
|
rows.Scan(
|
||||||
}, nil
|
&credit.Artist.ID,
|
||||||
|
&credit.Artist.Name,
|
||||||
|
&credit.Artist.Website,
|
||||||
|
&credit.Artist.Avatar,
|
||||||
|
&credit.Role,
|
||||||
|
&credit.Primary)
|
||||||
|
credits = append(credits, &credit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return credits, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetReleaseLinks(db *sqlx.DB, releaseID string) ([]*model.Link, error) {
|
||||||
|
var links = []*model.Link{}
|
||||||
|
|
||||||
|
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.me/arimelody.me/music/model"
|
"arimelody-web/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, release *model.Release) ([]*model.Track, error) {
|
func GetTracksNotOnRelease(db *sqlx.DB, releaseID string) ([]*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)",
|
||||||
release.ID)
|
releaseID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -55,20 +55,58 @@ func GetTracksNotOnRelease(db *sqlx.DB, release *model.Release) ([]*model.Track,
|
||||||
return tracks, nil
|
return tracks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTrackReleases(db *sqlx.DB, track *model.Track) ([]*model.Release, error) {
|
func GetTrackReleases(db *sqlx.DB, trackID string, full bool) ([]*model.Release, error) {
|
||||||
var releases = []*model.Release{}
|
var releases = []*model.Release{}
|
||||||
|
|
||||||
err := db.Select(&releases,
|
err := db.Select(&releases,
|
||||||
"SELECT musicrelease.* FROM musicrelease "+
|
"SELECT id,title,type,release_date,artwork,buylink "+
|
||||||
|
"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",
|
||||||
track.ID,
|
trackID,
|
||||||
)
|
)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,11 +161,11 @@ func UpdateTrack(db *sqlx.DB, track *model.Track) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteTrack(db *sqlx.DB, track *model.Track) error {
|
func DeleteTrack(db *sqlx.DB, trackID string) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"DELETE FROM musictrack "+
|
"DELETE FROM musictrack "+
|
||||||
"WHERE id=$1",
|
"WHERE id=$1",
|
||||||
track.ID,
|
trackID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Artist struct {
|
Artist struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
@ -21,56 +19,3 @@ 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,7 +1,10 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
type Credit struct {
|
type (
|
||||||
Artist `json:"artist"`
|
Credit struct {
|
||||||
Role string `json:"role"`
|
Release Release `json:"release"`
|
||||||
Primary bool `json:"primary" db:"is_primary"`
|
Artist Artist `json:"artist"`
|
||||||
}
|
Role string `json:"role"`
|
||||||
|
Primary bool `json:"primary" db:"is_primary"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html/template"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -8,24 +10,20 @@ type (
|
||||||
ReleaseType string
|
ReleaseType string
|
||||||
|
|
||||||
Release struct {
|
Release struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Visible bool `json:"visible"`
|
Visible bool `json:"visible"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
ReleaseType ReleaseType `json:"type" db:"type"`
|
ReleaseType ReleaseType `json:"type" db:"type"`
|
||||||
ReleaseDate time.Time `json:"releaseDate" db:"release_date"`
|
ReleaseDate time.Time `json:"releaseDate" db:"release_date"`
|
||||||
Artwork string `json:"artwork"`
|
Artwork string `json:"artwork"`
|
||||||
Buyname string `json:"buyname"`
|
Buyname string `json:"buyname"`
|
||||||
Buylink string `json:"buylink"`
|
Buylink string `json:"buylink"`
|
||||||
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"`
|
||||||
FullRelease struct {
|
Links []*Link `json:"links"`
|
||||||
*Release
|
|
||||||
Tracks []DisplayTrack
|
|
||||||
Credits []Credit
|
|
||||||
Links []Link
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,6 +37,10 @@ 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")
|
||||||
}
|
}
|
||||||
|
@ -58,10 +60,39 @@ func (release Release) GetArtwork() string {
|
||||||
return release.Artwork
|
return release.Artwork
|
||||||
}
|
}
|
||||||
|
|
||||||
func (release FullRelease) IsSingle() bool {
|
func (release Release) 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[:], ", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,24 +7,23 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Track struct {
|
Track struct {
|
||||||
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"`
|
Lyrics string `json:"lyrics" db:"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) MakeDisplay(number int) DisplayTrack {
|
func (track Track) GetDescriptionHTML() template.HTML {
|
||||||
return DisplayTrack{
|
return template.HTML(strings.Replace(track.Description, "\n", "<br>", -1))
|
||||||
Track: &track,
|
}
|
||||||
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
|
||||||
Number: number,
|
func (track Track) GetLyricsHTML() template.HTML {
|
||||||
}
|
return template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function is stupid and i hate that i need it
|
||||||
|
func (track Track) Add(a int, b int) int {
|
||||||
|
return a + b
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"arimelody.me/arimelody.me/global"
|
"arimelody-web/global"
|
||||||
music "arimelody.me/arimelody.me/music/controller"
|
music "arimelody-web/music/controller"
|
||||||
"arimelody.me/arimelody.me/music/model"
|
"arimelody-web/music/model"
|
||||||
"arimelody.me/arimelody.me/templates"
|
"arimelody-web/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTP HANDLER METHODS
|
// HTTP HANDLER METHODS
|
||||||
|
@ -21,8 +21,7 @@ func Handler() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var release model.Release
|
release, err := music.GetRelease(global.DB, r.URL.Path[1:], true)
|
||||||
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
|
||||||
|
@ -36,25 +35,17 @@ 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) {
|
||||||
dbReleases, err := music.GetAllReleases(global.DB)
|
releases, err := music.GetAllReleases(global.DB, true, 0, true)
|
||||||
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 _, dbRelease := range dbReleases {
|
for _, release := range releases {
|
||||||
if !dbRelease.Visible { continue }
|
if !release.IsReleased() {
|
||||||
if !dbRelease.IsReleased() {
|
release.ReleaseType = model.Upcoming
|
||||||
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,16 +5,35 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"arimelody.me/arimelody.me/admin"
|
"arimelody-web/admin"
|
||||||
"arimelody.me/arimelody.me/global"
|
"arimelody-web/global"
|
||||||
"arimelody.me/arimelody.me/music/model"
|
"arimelody-web/music/model"
|
||||||
db "arimelody.me/arimelody.me/music/controller"
|
db "arimelody-web/music/controller"
|
||||||
"arimelody.me/arimelody.me/templates"
|
"arimelody-web/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTP HANDLERS
|
type (
|
||||||
|
Track struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Lyrics string `json:"lyrics"`
|
||||||
|
}
|
||||||
|
|
||||||
func ServeRelease(release model.Release) http.Handler {
|
Credit struct {
|
||||||
|
*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
|
||||||
|
@ -23,22 +42,65 @@ func ServeRelease(release model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fullRelease := &model.FullRelease{
|
response := Release{
|
||||||
Release: &release,
|
Release: release,
|
||||||
|
Tracks: []Track{},
|
||||||
|
Credits: []Credit{},
|
||||||
|
Links: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
if authorised || release.IsReleased() {
|
if authorised || release.IsReleased() {
|
||||||
fullerRelease, err := db.GetFullRelease(global.DB, &release)
|
// get credits
|
||||||
|
credits, err := db.GetReleaseCredits(global.DB, release.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err)
|
fmt.Printf("FATAL: Failed to serve release %s: Credits: %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
|
||||||
}
|
}
|
||||||
fullRelease = fullerRelease
|
for _, credit := range credits {
|
||||||
|
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(fullRelease)
|
err := json.NewEncoder(w).Encode(response)
|
||||||
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
|
||||||
|
@ -46,7 +108,7 @@ func ServeRelease(release model.Release) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServeGateway(release model.Release) http.Handler {
|
func ServeGateway(release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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
|
||||||
|
@ -55,21 +117,15 @@ func ServeGateway(release model.Release) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fullRelease := &model.FullRelease{
|
response := *release
|
||||||
Release: &release,
|
|
||||||
}
|
|
||||||
|
|
||||||
if authorised || release.IsReleased() {
|
if authorised || release.IsReleased() {
|
||||||
fullerRelease, err := db.GetFullRelease(global.DB, &release)
|
response.Tracks = release.Tracks
|
||||||
if err != nil {
|
response.Credits = release.Credits
|
||||||
fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err)
|
response.Links = release.Links
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fullRelease = fullerRelease
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := templates.Pages["music-gateway"].Execute(w, fullRelease)
|
err := templates.Pages["music-gateway"].Execute(w, response)
|
||||||
|
|
||||||
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,6 +296,13 @@ 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;
|
||||||
|
@ -311,6 +318,7 @@ 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;
|
||||||
|
@ -324,7 +332,9 @@ ul#links a {
|
||||||
transition: filter .1s,-webkit-filter .1s
|
transition: filter .1s,-webkit-filter .1s
|
||||||
}
|
}
|
||||||
|
|
||||||
ul#links a.buy {
|
#buylink {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: -.5em;
|
||||||
background-color: #ff94e9
|
background-color: #ff94e9
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,7 +364,12 @@ ul#links a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
#description {
|
#description {
|
||||||
font-size: 1.2em;
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#copyright {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: .8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#share {
|
#share {
|
||||||
|
@ -390,7 +405,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
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
#info h2 {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding: .3em 1em;
|
padding: .3em 1em;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
@ -517,12 +532,11 @@ footer a:hover {
|
||||||
|
|
||||||
#art-container {
|
#art-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#artwork {
|
#artwork {
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 50vw;
|
max-width: 60vw;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
|
@ -536,21 +550,25 @@ 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: 0;
|
margin: 2em 0 0 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;
|
||||||
}
|
}
|
||||||
|
@ -583,10 +601,18 @@ 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;
|
||||||
|
@ -596,6 +622,9 @@ 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,17 +61,14 @@
|
||||||
<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>Releases: {{.PrintReleaseDate}}</p>
|
<p id="upcoming-release">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>
|
||||||
|
@ -82,7 +79,7 @@
|
||||||
|
|
||||||
{{if .Description}}
|
{{if .Description}}
|
||||||
|
|
||||||
<p id="description">{{.Description}}</p>
|
<p id="description">{{.GetDescriptionHTML}}</p>
|
||||||
|
|
||||||
{{else if .IsSingle}}
|
{{else if .IsSingle}}
|
||||||
|
|
||||||
|
@ -93,6 +90,10 @@
|
||||||
|
|
||||||
{{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>
|
||||||
|
|
||||||
|
@ -121,7 +122,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.Lyrics}}
|
{{$Track.GetLyricsHTML}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="empty">No lyrics.</span>
|
<span class="empty">No lyrics.</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -133,7 +134,7 @@
|
||||||
<h2>TRACKS</h2>
|
<h2>TRACKS</h2>
|
||||||
{{range $i, $track := .Tracks}}
|
{{range $i, $track := .Tracks}}
|
||||||
<details>
|
<details>
|
||||||
<summary class="album-track-title">{{$track.Number}}. {{$track.Title}}</summary>
|
<summary class="album-track-title">{{$track.Add $i 1}}. {{$track.Title}}</summary>
|
||||||
|
|
||||||
{{if $track.Description}}
|
{{if $track.Description}}
|
||||||
<p class="album-track-subheading">DESCRIPTION</p>
|
<p class="album-track-subheading">DESCRIPTION</p>
|
||||||
|
@ -142,7 +143,7 @@
|
||||||
|
|
||||||
<p class="album-track-subheading">LYRICS</p>
|
<p class="album-track-subheading">LYRICS</p>
|
||||||
{{if $track.Lyrics}}
|
{{if $track.Lyrics}}
|
||||||
{{$track.Lyrics}}
|
{{$track.GetLyricsHTML}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="empty">No lyrics.</span>
|
<span class="empty">No lyrics.</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in a new issue