MORE REFACTORING!! + some improvements

Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
ari melody 2024-08-02 00:53:19 +01:00
parent 151b2d8fd9
commit cba791deba
17 changed files with 376 additions and 223 deletions

47
admin/admin.go Normal file
View 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)
}

View file

@ -3,45 +3,17 @@ package admin
import ( import (
"context" "context"
"fmt" "fmt"
"math/rand" "html/template"
"net/http" "net/http"
"os" "os"
// "strings" "path/filepath"
"strings"
"time" "time"
"arimelody.me/arimelody.me/discord" "arimelody.me/arimelody.me/discord"
"arimelody.me/arimelody.me/global" "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 { func Handler() http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
@ -53,18 +25,13 @@ func Handler() http.Handler {
mux.Handle("/login", global.HTTPLog(LoginHandler())) mux.Handle("/login", global.HTTPLog(LoginHandler()))
mux.Handle("/verify", global.HTTPLog(MustAuthorise(VerifyHandler()))) mux.Handle("/verify", global.HTTPLog(MustAuthorise(VerifyHandler())))
mux.Handle("/logout", global.HTTPLog(MustAuthorise(LogoutHandler()))) mux.Handle("/logout", global.HTTPLog(MustAuthorise(LogoutHandler())))
mux.Handle("/static", global.HTTPLog(MustAuthorise(staticHandler())))
return mux return mux
} }
func MustAuthorise(next http.Handler) http.Handler { func MustAuthorise(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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") auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, "Bearer ") { if strings.HasPrefix(auth, "Bearer ") {
auth = auth[7:] auth = auth[7:]
@ -104,7 +71,6 @@ func MustAuthorise(next http.Handler) http.Handler {
ctx := context.WithValue(r.Context(), "role", "admin") ctx := context.WithValue(r.Context(), "role", "admin")
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
*/
}) })
} }
@ -143,7 +109,7 @@ func LoginHandler() http.Handler {
} }
// login success! // login success!
session := CreateSession(discord_user.Username) session := createSession(discord_user.Username)
sessions = append(sessions, &session) sessions = append(sessions, &session)
cookie := http.Cookie{} cookie := http.Cookie{}
@ -197,12 +163,60 @@ func VerifyHandler() http.Handler {
}) })
} }
func generateToken() string { func ServeTemplate(page string, data any) http.Handler {
var token []byte 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++ { info, err := os.Stat(fp)
token = append(token, TOKEN_CHARS[rand.Intn(len(TOKEN_CHARS))]) 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
View 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"

View file

@ -1,24 +1,45 @@
package discord package discord
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings" "os"
"strings"
) )
const API_ENDPOINT = "https://discord.com/api/v10" 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 var CLIENT_ID = func() string {
// the rules by doing this at all envvar := os.Getenv("DISCORD_CLIENT_ID")
const CLIENT_SECRET = "JUEZnixhN7BxmLIHmbECiKETMP85VT0E" if envvar == "" {
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" fmt.Printf("DISCORD_CLIENT_ID was not provided. Admin login will be unavailable.\n")
}
// TODO: change before prod return envvar
const MY_REDIRECT_URI = "http://127.0.0.1:8080/api/v1/admin/login" }()
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 ( type (
AccessTokenResponse struct { AccessTokenResponse struct {
@ -63,7 +84,7 @@ func GetOAuthTokenFromCode(code string) (string, error) {
"client_secret": {CLIENT_SECRET}, "client_secret": {CLIENT_SECRET},
"grant_type": {"authorization_code"}, "grant_type": {"authorization_code"},
"code": {code}, "code": {code},
"redirect_uri": {MY_REDIRECT_URI}, "redirect_uri": {OAUTH_CALLBACK_URI},
}.Encode())) }.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

View file

@ -1,26 +1,16 @@
package global package global
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"html/template" "html/template"
"strconv" "strconv"
"time" "time"
)
var MimeTypes = map[string]string{ "arimelody.me/arimelody.me/colour"
"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",
}
func DefaultHeaders(next http.Handler) http.Handler { func DefaultHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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) 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), after.Format(time.UnixDate),
r.Method, r.Method,
r.URL.Path, r.URL.Path,
codeColour,
lrw.Code, lrw.Code,
colour.Reset,
elapsed, elapsed,
r.Header["User-Agent"][0]) r.Header["User-Agent"][0])
}) })

View file

@ -7,8 +7,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"arimelody.me/arimelody.me/api/v1/admin" "arimelody.me/arimelody.me/admin"
"arimelody.me/arimelody.me/api/v1/music" "arimelody.me/arimelody.me/music"
"arimelody.me/arimelody.me/db" "arimelody.me/arimelody.me/db"
"arimelody.me/arimelody.me/global" "arimelody.me/arimelody.me/global"
) )
@ -44,7 +44,7 @@ func main() {
func createServeMux() *http.ServeMux { func createServeMux() *http.ServeMux {
mux := http.NewServeMux() 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/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()))) mux.Handle("/api/v1/music/", global.HTTPLog(http.StripPrefix("/api/v1/music", music.ServeRelease())))

View file

@ -6,11 +6,19 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
type Credit struct { type (
Artist *Artist `json:"artist"` Credit struct {
Role string `json:"role"` Artist *Artist `json:"artist"`
Primary bool `json:"primary"` Role string `json:"role"`
} Primary bool `json:"primary"`
}
PostCreditBody struct {
Artist string `json:"artist"`
Role string `json:"role"`
Primary bool `json:"primary"`
}
)
// GETTERS // GETTERS

View file

@ -7,19 +7,10 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"arimelody.me/arimelody.me/api/v1/admin" "arimelody.me/arimelody.me/admin"
"arimelody.me/arimelody.me/global" "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 // HTTP HANDLER METHODS
func ServeCatalog() http.Handler { func ServeCatalog() http.Handler {
@ -33,7 +24,7 @@ func ServeCatalog() http.Handler {
releases = append(releases, release) 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") fp := filepath.Join("data", "music-artwork", releaseID + ".png")
fmt.Println(fp)
info, err := os.Stat(fp) info, err := os.Stat(fp)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {

View file

@ -8,7 +8,7 @@ import (
"strings" "strings"
"time" "time"
"arimelody.me/arimelody.me/api/v1/admin" "arimelody.me/arimelody.me/admin"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
@ -21,19 +21,35 @@ const (
Compilation ReleaseType = "Compilation" Compilation ReleaseType = "Compilation"
) )
type Release struct { type (
ID string `json:"id"` Release struct {
Title string `json:"title"` ID string `json:"id"`
Description string `json:"description"` Title string `json:"title"`
ReleaseType ReleaseType `json:"type"` Description string `json:"description"`
ReleaseDate time.Time `json:"releaseDate"` ReleaseType ReleaseType `json:"type"`
Artwork string `json:"artwork"` ReleaseDate time.Time `json:"releaseDate"`
Buyname string `json:"buyname"` Artwork string `json:"artwork"`
Buylink string `json:"buylink"` Buyname string `json:"buyname"`
Links []Link `json:"links"` Buylink string `json:"buylink"`
Credits []Credit `json:"credits"` Links []Link `json:"links"`
Tracks []Track `json:"tracks"` Credits []Credit `json:"credits"`
} 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; var Releases []Release;
@ -337,7 +353,7 @@ func (release Release) DeleteFromDB(db *sqlx.DB) error {
func PullAllReleases(db *sqlx.DB) ([]Release, error) { func PullAllReleases(db *sqlx.DB) ([]Release, error) {
releases := []Release{} 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 { if err != nil {
return nil, err return nil, err
} }
@ -423,19 +439,50 @@ func PostRelease() http.Handler {
return return
} }
var release Release var data PostReleaseBody
err := json.NewDecoder(r.Body).Decode(&release) err := json.NewDecoder(r.Body).Decode(&data)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
if GetRelease(release.ID) != nil { if GetRelease(data.ID) != nil {
http.Error(w, fmt.Sprintf("Release %s already exists", release.ID), http.StatusBadRequest) http.Error(w, fmt.Sprintf("Release %s already exists", data.ID), http.StatusBadRequest)
return 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) jsonBytes, err := json.Marshal(release)
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")

View file

@ -2,11 +2,11 @@ const header_links = document.getElementById("header-links");
const hamburger = document.getElementById("header-links-toggle"); const hamburger = document.getElementById("header-links-toggle");
document.addEventListener("click", event => { document.addEventListener("click", event => {
if (!header_links.contains(event.target) && !hamburger.contains(event.target) && !header_links.href) { if (!header_links.contains(event.target) && !hamburger.contains(event.target) && !header_links.href) {
header_links.classList.remove("open"); header_links.classList.remove("open");
} }
}); });
hamburger.addEventListener("click", event => { hamburger.addEventListener("click", () => {
header_links.classList.toggle("open"); header_links.classList.toggle("open");
}); });

View file

@ -1,7 +1,12 @@
import "./main.js"; import "./main.js";
document.addEventListener("swap", () => { document.querySelectorAll("div.music").forEach(container => {
document.querySelectorAll("h1.music-title").forEach(element => { const link = container.querySelector(".music-title a").href
element.href = "";
}); container.addEventListener("click", event => {
if (event.target.href) return;
event.preventDefault();
location = link;
});
}); });

View file

@ -1,17 +1,18 @@
-- --
-- Artists (should be applicable to all art) -- Artists (should be applicable to all art)
-- --
CREATE TABLE IF NOT EXISTS artists ( CREATE TABLE artist (
id text NOT NULL, id uuid DEFAULT gen_random_uuid(),
name text, name text NOT NULL,
website text 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 -- Music releases
-- --
CREATE TABLE IF NOT EXISTS musicreleases ( CREATE TABLE musicrelease (
id character varying(64) NOT NULL, id character varying(64) NOT NULL,
title text NOT NULL, title text NOT NULL,
description text, description text,
@ -21,50 +22,56 @@ CREATE TABLE IF NOT EXISTS musicreleases (
buyname text, buyname text,
buylink 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) -- Music links (external platform links under a release)
-- --
CREATE TABLE IF NOT EXISTS musiclinks ( CREATE TABLE musiclink (
release character varying(64) NOT NULL, release character varying(64) NOT NULL,
name text 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) -- Music credits (artist credits under a release)
-- --
CREATE TABLE IF NOT EXISTS musiccredits ( CREATE TABLE musiccredit (
release character varying(64) NOT NULL, release character varying(64) NOT NULL,
artist text NOT NULL, artist uuid NOT NULL,
role text, role text NOT NULL,
is_primary boolean 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) -- Music tracks (tracks under a release)
-- --
CREATE TABLE IF NOT EXISTS musictracks ( CREATE TABLE musictrack (
release character varying(64) NOT NULL, id uuid DEFAULT gen_random_uuid(),
number integer NOT NULL,
title text NOT NULL, title text NOT NULL,
description text, description text,
lyrics text, lyrics text,
preview_url 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 -- Foreign keys
-- --
ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE;
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 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 musiccredits ADD CONSTRAINT IF NOT EXISTS musiccredits_release_fk FOREIGN KEY (release) REFERENCES musicreleases(id) 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;
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;

View file

@ -111,7 +111,7 @@
<h2>tracks:</h2> <h2>tracks:</h2>
{{range $i, $track := .GetTracks}} {{range $i, $track := .GetTracks}}
<details> <details>
<summary class="album-track-title">{{$i}}. {{$track.GetTitle}}</summary> <summary class="album-track-title">{{$track.GetNumber}}. {{$track.GetTitle}}</summary>
{{$track.GetLyrics}} {{$track.GetLyrics}}
</details> </details>
{{end}} {{end}}

View file

@ -18,76 +18,80 @@
{{define "content"}} {{define "content"}}
<main> <main>
<script type="module" src="/script/music.js"></script> <script type="module" src="/script/music.js"></script>
<h1> <h1>
# my music # my music
</h1> </h1>
<div id="music-container"> <div id="music-container">
{{range $Release := .}} {{range $Release := .}}
<div class="music" id="{{$Release.GetID}}" swap-url="/music/{{$Release.GetID}}"> <div class="music" id="{{$Release.GetID}}" swap-url="/music/{{$Release.GetID}}">
<div class="music-artwork"> <div class="music-artwork">
<img src="{{$Release.GetArtwork}}" alt="{{$Release.GetTitle}} artwork" width="128" loading="lazy"> <img src="{{$Release.GetArtwork}}" alt="{{$Release.GetTitle}} artwork" width="128" loading="lazy">
</div> </div>
<div class="music-details"> <div class="music-details">
<a href="/music/{{$Release.GetID}}"><h1 class="music-title">{{$Release.GetTitle}}</h1></a> <h1 class="music-title">
<h2 class="music-artist">{{$Release.PrintArtists true true}}</h2> <a href="/music/{{$Release.GetID}}">
<h3 class="music-type-{{$Release.GetType}}">{{$Release.GetType}}</h3> {{$Release.GetTitle}}
<ul class="music-links"> </a>
{{range $Link := $Release.GetLinks}} </h1>
<li> <h2 class="music-artist">{{$Release.PrintArtists true true}}</h2>
<a href="{{$Link.GetURL}}" class="link-button">{{$Link.GetName}}</a> <h3 class="music-type-{{$Release.GetType}}">{{$Release.GetType}}</h3>
</li> <ul class="music-links">
{{end}} {{range $Link := $Release.GetLinks}}
</ul> <li>
</div> <a href="{{$Link.GetURL}}" class="link-button">{{$Link.GetName}}</a>
</div> </li>
{{end}} {{end}}
</ul>
</div>
</div> </div>
{{end}}
</div>
<h2 id="usage" class="question"> <h2 id="usage" class="question">
<a href="#usage"> <a href="#usage">
&gt; "can i use your music in my content?" &gt; "can i use your music in my content?"
</a> </a>
</h2> </h2>
<div class="answer"> <div class="answer">
<p> <p>
<strong class="big">yes!</strong> well, in most cases... <strong class="big">yes!</strong> well, in most cases...
</p> </p>
<p> <p>
from <a href="/music/dream">Dream (2022)</a> onward, all of my <em>self-released</em> songs are from <a href="/music/dream">Dream (2022)</a> onward, all of my <em>self-released</em> songs are
licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">Creative Commons Attribution-ShareAlike 4.0</a>. licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">Creative Commons Attribution-ShareAlike 4.0</a>.
anyone may use and remix these songs freely, so long as they provide credit back to me and link back to this license! anyone may use and remix these songs freely, so long as they provide credit back to me and link back to this license!
please note that all derivative works must inherit this license. please note that all derivative works must inherit this license.
</p> </p>
<p> <p>
a great example of some credit text would be as follows: a great example of some credit text would be as follows:
</p> </p>
<blockquote> <blockquote>
music used: mellodoot - Dream<br> music used: mellodoot - Dream<br>
<a href="/music/dream">https://arimelody.me/music/dream</a><br> <a href="/music/dream">https://arimelody.me/music/dream</a><br>
licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>. licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>.
</blockquote> </blockquote>
<p> <p>
for any songs prior to this, they were all either released by me (in which case, i honestly for any songs prior to this, they were all either released by me (in which case, i honestly
don't mind), or in collaboration with chill people who i don't see having an issue with it. don't mind), or in collaboration with chill people who i don't see having an issue with it.
do be sure to ask them about it, though! do be sure to ask them about it, though!
</p> </p>
<p> <p>
in the event the song you want to use is released under some other label, their usage rights in the event the song you want to use is released under some other label, their usage rights
will more than likely trump whatever i'd otherwise have in mind. i'll try to negotiate some will more than likely trump whatever i'd otherwise have in mind. i'll try to negotiate some
nice terms, though! ;3 nice terms, though! ;3
</p> </p>
<p> <p>
i love the idea of other creators using my songs in their work, so if you do happen to use i love the idea of other creators using my songs in their work, so if you do happen to use
my stuff in a work you're particularly proud of, feel free to send it my way! my stuff in a work you're particularly proud of, feel free to send it my way!
</p> </p>
<p> <p>
&gt; <a href="mailto:ari@arimelody.me">ari@arimelody.me</a> &gt; <a href="mailto:ari@arimelody.me">ari@arimelody.me</a>
</p> </p>
</div> </div>
<a href="#" id="backtotop">back to top</a> <a href="#" id="backtotop">back to top</a>
</main> </main>
{{end}} {{end}}