MORE REFACTORING!! + some improvements
Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
parent
151b2d8fd9
commit
cba791deba
47
admin/admin.go
Normal file
47
admin/admin.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
Session struct {
|
||||
UserID string
|
||||
Token string
|
||||
Expires int64
|
||||
}
|
||||
)
|
||||
|
||||
const TOKEN_LENGTH = 64
|
||||
const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
var ADMIN_ID_DISCORD = func() string {
|
||||
envvar := os.Getenv("DISCORD_ADMIN_ID")
|
||||
if envvar == "" {
|
||||
fmt.Printf("DISCORD_ADMIN_ID was not provided. Admin login will be unavailable.\n")
|
||||
}
|
||||
return envvar
|
||||
}()
|
||||
|
||||
var sessions []*Session
|
||||
|
||||
func createSession(UserID string) Session {
|
||||
return Session{
|
||||
UserID: UserID,
|
||||
Token: string(generateToken()),
|
||||
Expires: time.Now().Add(24 * time.Hour).Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func generateToken() string {
|
||||
var token []byte
|
||||
|
||||
for i := 0; i < TOKEN_LENGTH; i++ {
|
||||
token = append(token, TOKEN_CHARS[rand.Intn(len(TOKEN_CHARS))])
|
||||
}
|
||||
|
||||
return string(token)
|
||||
}
|
|
@ -3,45 +3,17 @@ package admin
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
// "strings"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"arimelody.me/arimelody.me/discord"
|
||||
"arimelody.me/arimelody.me/global"
|
||||
)
|
||||
|
||||
type (
|
||||
Session struct {
|
||||
UserID string
|
||||
Token string
|
||||
Expires int64
|
||||
}
|
||||
)
|
||||
|
||||
const TOKEN_LENGTH = 64
|
||||
const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
var ADMIN_ID_DISCORD = func() string {
|
||||
envvar := os.Getenv("DISCORD_ADMIN_ID")
|
||||
if envvar == "" {
|
||||
fmt.Printf("DISCORD_ADMIN_ID was not provided. Admin login will be unavailable.\n")
|
||||
}
|
||||
return envvar
|
||||
}()
|
||||
|
||||
var sessions []*Session
|
||||
|
||||
func CreateSession(UserID string) Session {
|
||||
return Session{
|
||||
UserID: UserID,
|
||||
Token: string(generateToken()),
|
||||
Expires: time.Now().Add(24 * time.Hour).Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func Handler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
|
@ -53,18 +25,13 @@ func Handler() http.Handler {
|
|||
mux.Handle("/login", global.HTTPLog(LoginHandler()))
|
||||
mux.Handle("/verify", global.HTTPLog(MustAuthorise(VerifyHandler())))
|
||||
mux.Handle("/logout", global.HTTPLog(MustAuthorise(LogoutHandler())))
|
||||
mux.Handle("/static", global.HTTPLog(MustAuthorise(staticHandler())))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func MustAuthorise(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TEMPORARY
|
||||
ctx := context.WithValue(r.Context(), "role", "admin")
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
|
||||
/*
|
||||
auth := r.Header.Get("Authorization")
|
||||
if strings.HasPrefix(auth, "Bearer ") {
|
||||
auth = auth[7:]
|
||||
|
@ -104,7 +71,6 @@ func MustAuthorise(next http.Handler) http.Handler {
|
|||
|
||||
ctx := context.WithValue(r.Context(), "role", "admin")
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
*/
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -143,7 +109,7 @@ func LoginHandler() http.Handler {
|
|||
}
|
||||
|
||||
// login success!
|
||||
session := CreateSession(discord_user.Username)
|
||||
session := createSession(discord_user.Username)
|
||||
sessions = append(sessions, &session)
|
||||
|
||||
cookie := http.Cookie{}
|
||||
|
@ -197,12 +163,60 @@ func VerifyHandler() http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func generateToken() string {
|
||||
var token []byte
|
||||
func ServeTemplate(page string, data any) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
lp_layout := filepath.Join("views", "layout.html")
|
||||
lp_header := filepath.Join("views", "header.html")
|
||||
lp_footer := filepath.Join("views", "footer.html")
|
||||
lp_prideflag := filepath.Join("views", "prideflag.html")
|
||||
fp := filepath.Join("views", filepath.Clean(page))
|
||||
|
||||
for i := 0; i < TOKEN_LENGTH; i++ {
|
||||
token = append(token, TOKEN_CHARS[rand.Intn(len(TOKEN_CHARS))])
|
||||
info, err := os.Stat(fp)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return string(token)
|
||||
if info.IsDir() {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
template, err := template.ParseFiles(lp_layout, lp_header, lp_footer, lp_prideflag, fp)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing template files: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = template.ExecuteTemplate(w, "layout.html", data)
|
||||
if err != nil {
|
||||
fmt.Printf("Error executing template: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func staticHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
info, err := os.Stat(filepath.Join("admin", "static", filepath.Clean(r.URL.Path)))
|
||||
// does the file exist?
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// is thjs a directory? (forbidden)
|
||||
if info.IsDir() {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.FileServer(http.Dir(filepath.Join("admin", "static"))).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
11
colour/colour.go
Normal file
11
colour/colour.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package colour
|
||||
|
||||
var Reset = "\033[0m"
|
||||
var Red = "\033[31m"
|
||||
var Green = "\033[32m"
|
||||
var Yellow = "\033[33m"
|
||||
var Blue = "\033[34m"
|
||||
var Purple = "\033[35m"
|
||||
var Cyan = "\033[36m"
|
||||
var Gray = "\033[37m"
|
||||
var White = "\033[97m"
|
|
@ -6,19 +6,40 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const API_ENDPOINT = "https://discord.com/api/v10"
|
||||
const CLIENT_ID = "1268013769578119208"
|
||||
|
||||
// TODO: good GOD change this later please i beg you. we've already broken
|
||||
// the rules by doing this at all
|
||||
const CLIENT_SECRET = "JUEZnixhN7BxmLIHmbECiKETMP85VT0E"
|
||||
const REDIRECT_URI = "https://discord.com/oauth2/authorize?client_id=1268013769578119208&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Fapi%2Fv1%2Fadmin%2Flogin&scope=identify"
|
||||
|
||||
// TODO: change before prod
|
||||
const MY_REDIRECT_URI = "http://127.0.0.1:8080/api/v1/admin/login"
|
||||
var CLIENT_ID = func() string {
|
||||
envvar := os.Getenv("DISCORD_CLIENT_ID")
|
||||
if envvar == "" {
|
||||
fmt.Printf("DISCORD_CLIENT_ID was not provided. Admin login will be unavailable.\n")
|
||||
}
|
||||
return envvar
|
||||
}()
|
||||
var CLIENT_SECRET = func() string {
|
||||
envvar := os.Getenv("DISCORD_CLIENT_SECRET")
|
||||
if envvar == "" {
|
||||
fmt.Printf("DISCORD_CLIENT_SECRET was not provided. Admin login will be unavailable.\n")
|
||||
}
|
||||
return envvar
|
||||
}()
|
||||
var REDIRECT_URI = func() string {
|
||||
envvar := os.Getenv("DISCORD_REDIRECT_URI")
|
||||
if envvar == "" {
|
||||
fmt.Printf("DISCORD_REDIRECT_URI was not provided. Admin login will be unavailable.\n")
|
||||
}
|
||||
return envvar
|
||||
}()
|
||||
var OAUTH_CALLBACK_URI = func() string {
|
||||
envvar := os.Getenv("OAUTH_CALLBACK_URI")
|
||||
if envvar == "" {
|
||||
fmt.Printf("OAUTH_CALLBACK_URI was not provided. Admin login will be unavailable.\n")
|
||||
}
|
||||
return envvar
|
||||
}()
|
||||
|
||||
type (
|
||||
AccessTokenResponse struct {
|
||||
|
@ -63,7 +84,7 @@ func GetOAuthTokenFromCode(code string) (string, error) {
|
|||
"client_secret": {CLIENT_SECRET},
|
||||
"grant_type": {"authorization_code"},
|
||||
"code": {code},
|
||||
"redirect_uri": {MY_REDIRECT_URI},
|
||||
"redirect_uri": {OAUTH_CALLBACK_URI},
|
||||
}.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
|
|
|
@ -8,19 +8,9 @@ import (
|
|||
"html/template"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var MimeTypes = map[string]string{
|
||||
"css": "text/css; charset=utf-8",
|
||||
"png": "image/png",
|
||||
"jpg": "image/jpg",
|
||||
"webp": "image/webp",
|
||||
"html": "text/html",
|
||||
"asc": "text/plain",
|
||||
"pub": "text/plain",
|
||||
"txt": "text/plain",
|
||||
"js": "application/javascript",
|
||||
}
|
||||
"arimelody.me/arimelody.me/colour"
|
||||
)
|
||||
|
||||
func DefaultHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -54,11 +44,20 @@ func HTTPLog(next http.Handler) http.Handler {
|
|||
elapsed = strconv.Itoa(difference)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] %s %s - %d (%sms) (%s)\n",
|
||||
codeColour := colour.Reset
|
||||
|
||||
if lrw.Code - 600 <= 0 { codeColour = colour.Red }
|
||||
if lrw.Code - 500 <= 0 { codeColour = colour.Yellow }
|
||||
if lrw.Code - 400 <= 0 { codeColour = colour.White }
|
||||
if lrw.Code - 300 <= 0 { codeColour = colour.Green }
|
||||
|
||||
fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n",
|
||||
after.Format(time.UnixDate),
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
codeColour,
|
||||
lrw.Code,
|
||||
colour.Reset,
|
||||
elapsed,
|
||||
r.Header["User-Agent"][0])
|
||||
})
|
||||
|
|
6
main.go
6
main.go
|
@ -7,8 +7,8 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"arimelody.me/arimelody.me/api/v1/admin"
|
||||
"arimelody.me/arimelody.me/api/v1/music"
|
||||
"arimelody.me/arimelody.me/admin"
|
||||
"arimelody.me/arimelody.me/music"
|
||||
"arimelody.me/arimelody.me/db"
|
||||
"arimelody.me/arimelody.me/global"
|
||||
)
|
||||
|
@ -44,7 +44,7 @@ func main() {
|
|||
func createServeMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.Handle("/api/v1/admin/", global.HTTPLog(http.StripPrefix("/api/v1/admin", admin.Handler())))
|
||||
mux.Handle("/admin/", global.HTTPLog(http.StripPrefix("/admin", admin.Handler())))
|
||||
|
||||
mux.Handle("/api/v1/music/artist/", global.HTTPLog(http.StripPrefix("/api/v1/music/artist", music.ServeArtist())))
|
||||
mux.Handle("/api/v1/music/", global.HTTPLog(http.StripPrefix("/api/v1/music", music.ServeRelease())))
|
||||
|
|
|
@ -6,12 +6,20 @@ import (
|
|||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Credit struct {
|
||||
type (
|
||||
Credit struct {
|
||||
Artist *Artist `json:"artist"`
|
||||
Role string `json:"role"`
|
||||
Primary bool `json:"primary"`
|
||||
}
|
||||
|
||||
PostCreditBody struct {
|
||||
Artist string `json:"artist"`
|
||||
Role string `json:"role"`
|
||||
Primary bool `json:"primary"`
|
||||
}
|
||||
)
|
||||
|
||||
// GETTERS
|
||||
|
||||
func (credit Credit) GetArtist() Artist {
|
|
@ -7,19 +7,10 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"arimelody.me/arimelody.me/api/v1/admin"
|
||||
"arimelody.me/arimelody.me/admin"
|
||||
"arimelody.me/arimelody.me/global"
|
||||
)
|
||||
|
||||
// func make_date_work(date string) time.Time {
|
||||
// res, err := time.Parse("2-Jan-2006", date)
|
||||
// if err != nil {
|
||||
// fmt.Printf("somehow we failed to parse %s! falling back to epoch :]\n", date)
|
||||
// return time.Unix(0, 0)
|
||||
// }
|
||||
// return res
|
||||
// }
|
||||
|
||||
// HTTP HANDLER METHODS
|
||||
|
||||
func ServeCatalog() http.Handler {
|
||||
|
@ -33,7 +24,7 @@ func ServeCatalog() http.Handler {
|
|||
releases = append(releases, release)
|
||||
}
|
||||
|
||||
global.ServeTemplate("music.html", releases).ServeHTTP(w, r)
|
||||
global.ServeTemplate("music.html", Releases).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -96,7 +87,6 @@ func ServeArtwork() http.Handler {
|
|||
}
|
||||
|
||||
fp := filepath.Join("data", "music-artwork", releaseID + ".png")
|
||||
fmt.Println(fp)
|
||||
info, err := os.Stat(fp)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
|
@ -8,7 +8,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"arimelody.me/arimelody.me/api/v1/admin"
|
||||
"arimelody.me/arimelody.me/admin"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
|
@ -21,7 +21,8 @@ const (
|
|||
Compilation ReleaseType = "Compilation"
|
||||
)
|
||||
|
||||
type Release struct {
|
||||
type (
|
||||
Release struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
|
@ -35,6 +36,21 @@ type Release struct {
|
|||
Tracks []Track `json:"tracks"`
|
||||
}
|
||||
|
||||
PostReleaseBody struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
ReleaseType ReleaseType `json:"type"`
|
||||
ReleaseDate time.Time `json:"releaseDate"`
|
||||
Artwork string `json:"artwork"`
|
||||
Buyname string `json:"buyname"`
|
||||
Buylink string `json:"buylink"`
|
||||
Links []Link `json:"links"`
|
||||
Credits []PostCreditBody `json:"credits"`
|
||||
Tracks []Track `json:"tracks"`
|
||||
}
|
||||
)
|
||||
|
||||
var Releases []Release;
|
||||
|
||||
// GETTERS
|
||||
|
@ -337,7 +353,7 @@ func (release Release) DeleteFromDB(db *sqlx.DB) error {
|
|||
func PullAllReleases(db *sqlx.DB) ([]Release, error) {
|
||||
releases := []Release{}
|
||||
|
||||
rows, err := db.Query("SELECT id, title, description, type, release_date, artwork, buyname, buylink FROM musicreleases")
|
||||
rows, err := db.Query("SELECT id, title, description, type, release_date, artwork, buyname, buylink FROM musicreleases ORDER BY release_date DESC")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -423,19 +439,50 @@ func PostRelease() http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
var release Release
|
||||
err := json.NewDecoder(r.Body).Decode(&release)
|
||||
var data PostReleaseBody
|
||||
err := json.NewDecoder(r.Body).Decode(&data)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if GetRelease(release.ID) != nil {
|
||||
http.Error(w, fmt.Sprintf("Release %s already exists", release.ID), http.StatusBadRequest)
|
||||
if GetRelease(data.ID) != nil {
|
||||
http.Error(w, fmt.Sprintf("Release %s already exists", data.ID), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
Releases = append(Releases, release)
|
||||
var credits = []Credit{}
|
||||
|
||||
for _, credit := range data.Credits {
|
||||
var artist = GetArtist(credit.Artist)
|
||||
|
||||
if artist == nil {
|
||||
http.Error(w, fmt.Sprintf("Artist %s does not exist", credit.Artist), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
credits = append(credits, Credit{
|
||||
Artist: artist,
|
||||
Role: credit.Role,
|
||||
Primary: credit.Primary,
|
||||
})
|
||||
}
|
||||
|
||||
var release = Release{
|
||||
ID: data.ID,
|
||||
Title: data.Title,
|
||||
Description: data.Description,
|
||||
ReleaseType: data.ReleaseType,
|
||||
ReleaseDate: data.ReleaseDate,
|
||||
Artwork: data.Artwork,
|
||||
Buyname: data.Buyname,
|
||||
Buylink: data.Buylink,
|
||||
Links: data.Links,
|
||||
Credits: credits,
|
||||
Tracks: data.Tracks,
|
||||
}
|
||||
|
||||
Releases = append([]Release{release}, Releases...)
|
||||
|
||||
jsonBytes, err := json.Marshal(release)
|
||||
w.Header().Add("Content-Type", "application/json")
|
|
@ -7,6 +7,6 @@ document.addEventListener("click", event => {
|
|||
}
|
||||
});
|
||||
|
||||
hamburger.addEventListener("click", event => {
|
||||
hamburger.addEventListener("click", () => {
|
||||
header_links.classList.toggle("open");
|
||||
});
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import "./main.js";
|
||||
|
||||
document.addEventListener("swap", () => {
|
||||
document.querySelectorAll("h1.music-title").forEach(element => {
|
||||
element.href = "";
|
||||
document.querySelectorAll("div.music").forEach(container => {
|
||||
const link = container.querySelector(".music-title a").href
|
||||
|
||||
container.addEventListener("click", event => {
|
||||
if (event.target.href) return;
|
||||
|
||||
event.preventDefault();
|
||||
location = link;
|
||||
});
|
||||
});
|
||||
|
|
61
schema.sql
61
schema.sql
|
@ -1,17 +1,18 @@
|
|||
--
|
||||
-- Artists (should be applicable to all art)
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS artists (
|
||||
id text NOT NULL,
|
||||
name text,
|
||||
website text
|
||||
CREATE TABLE artist (
|
||||
id uuid DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL,
|
||||
website text,
|
||||
avatar text
|
||||
);
|
||||
ALTER TABLE artists ADD CONSTRAINT artists_pk PRIMARY KEY (id);
|
||||
ALTER TABLE artist ADD CONSTRAINT artist_pk PRIMARY KEY (id);
|
||||
|
||||
--
|
||||
-- Music releases
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS musicreleases (
|
||||
CREATE TABLE musicrelease (
|
||||
id character varying(64) NOT NULL,
|
||||
title text NOT NULL,
|
||||
description text,
|
||||
|
@ -21,50 +22,56 @@ CREATE TABLE IF NOT EXISTS musicreleases (
|
|||
buyname text,
|
||||
buylink text
|
||||
);
|
||||
ALTER TABLE musicreleases ADD CONSTRAINT musicreleases_pk PRIMARY KEY (id);
|
||||
ALTER TABLE musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id);
|
||||
|
||||
--
|
||||
-- Music links (external platform links under a release)
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS musiclinks (
|
||||
CREATE TABLE musiclink (
|
||||
release character varying(64) NOT NULL,
|
||||
name text NOT NULL,
|
||||
url text
|
||||
url text NOT NULL
|
||||
);
|
||||
ALTER TABLE musiclinks ADD CONSTRAINT musiclinks_pk PRIMARY KEY (release, name);
|
||||
ALTER TABLE musiclink ADD CONSTRAINT musiclink_pk PRIMARY KEY (release, name);
|
||||
|
||||
--
|
||||
-- Music credits (artist credits under a release)
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS musiccredits (
|
||||
CREATE TABLE musiccredit (
|
||||
release character varying(64) NOT NULL,
|
||||
artist text NOT NULL,
|
||||
role text,
|
||||
is_primary boolean
|
||||
artist uuid NOT NULL,
|
||||
role text NOT NULL,
|
||||
is_primary boolean DEFAULT false
|
||||
);
|
||||
ALTER TABLE musiccredits ADD CONSTRAINT musiccredits_pk PRIMARY KEY (release, artist);
|
||||
ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist);
|
||||
|
||||
--
|
||||
-- Music tracks (tracks under a release)
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS musictracks (
|
||||
release character varying(64) NOT NULL,
|
||||
number integer NOT NULL,
|
||||
CREATE TABLE musictrack (
|
||||
id uuid DEFAULT gen_random_uuid(),
|
||||
title text NOT NULL,
|
||||
description text,
|
||||
lyrics text,
|
||||
preview_url text
|
||||
);
|
||||
ALTER TABLE musictracks ADD CONSTRAINT musictracks_pk PRIMARY KEY (release, number);
|
||||
ALTER TABLE musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id);
|
||||
|
||||
--
|
||||
-- Music release/track pairs
|
||||
--
|
||||
CREATE TABLE musicreleasetrack (
|
||||
release character varying(64) NOT NULL,
|
||||
track uuid NOT NULL,
|
||||
number integer NOT NULL
|
||||
);
|
||||
ALTER TABLE musicreleasetrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (release, track);
|
||||
|
||||
--
|
||||
-- Foreign keys
|
||||
--
|
||||
|
||||
ALTER TABLE musiccredits ADD CONSTRAINT IF NOT EXISTS musiccredits_artist_fk FOREIGN KEY (artist) REFERENCES artists(id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE musiccredits ADD CONSTRAINT IF NOT EXISTS musiccredits_release_fk FOREIGN KEY (release) REFERENCES musicreleases(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE musiclinks ADD CONSTRAINT IF NOT EXISTS musiclinks_release_fk FOREIGN KEY (release) REFERENCES musicreleases(id) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE musictracks ADD CONSTRAINT IF NOT EXISTS musictracks_release_fk FOREIGN KEY (release) REFERENCES musicreleases(id) ON DELETE CASCADE;
|
||||
ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE;
|
||||
ALTER TABLE musiclink ADD CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
ALTER TABLE musicreleasetrack ADD CONSTRAINT music_pair_trackref_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE;
|
||||
ALTER TABLE musicreleasetrack ADD CONSTRAINT music_pair_releaseref_fk FOREIGN KEY (track) REFERENCES musictrack(id) ON DELETE CASCADE;
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
<h2>tracks:</h2>
|
||||
{{range $i, $track := .GetTracks}}
|
||||
<details>
|
||||
<summary class="album-track-title">{{$i}}. {{$track.GetTitle}}</summary>
|
||||
<summary class="album-track-title">{{$track.GetNumber}}. {{$track.GetTitle}}</summary>
|
||||
{{$track.GetLyrics}}
|
||||
</details>
|
||||
{{end}}
|
||||
|
|
|
@ -31,7 +31,11 @@
|
|||
<img src="{{$Release.GetArtwork}}" alt="{{$Release.GetTitle}} artwork" width="128" loading="lazy">
|
||||
</div>
|
||||
<div class="music-details">
|
||||
<a href="/music/{{$Release.GetID}}"><h1 class="music-title">{{$Release.GetTitle}}</h1></a>
|
||||
<h1 class="music-title">
|
||||
<a href="/music/{{$Release.GetID}}">
|
||||
{{$Release.GetTitle}}
|
||||
</a>
|
||||
</h1>
|
||||
<h2 class="music-artist">{{$Release.PrintArtists true true}}</h2>
|
||||
<h3 class="music-type-{{$Release.GetType}}">{{$Release.GetType}}</h3>
|
||||
<ul class="music-links">
|
||||
|
|
Loading…
Reference in a new issue