home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

MORE REFACTORING!! + some improvements

Signed-off-by: ari melody <ari@arimelody.me>

+376 -223
+47
admin/admin.go
··· 1 + package admin 2 + 3 + import ( 4 + "fmt" 5 + "math/rand" 6 + "os" 7 + "time" 8 + ) 9 + 10 + type ( 11 + Session struct { 12 + UserID string 13 + Token string 14 + Expires int64 15 + } 16 + ) 17 + 18 + const TOKEN_LENGTH = 64 19 + const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 20 + 21 + var ADMIN_ID_DISCORD = func() string { 22 + envvar := os.Getenv("DISCORD_ADMIN_ID") 23 + if envvar == "" { 24 + fmt.Printf("DISCORD_ADMIN_ID was not provided. Admin login will be unavailable.\n") 25 + } 26 + return envvar 27 + }() 28 + 29 + var sessions []*Session 30 + 31 + func createSession(UserID string) Session { 32 + return Session{ 33 + UserID: UserID, 34 + Token: string(generateToken()), 35 + Expires: time.Now().Add(24 * time.Hour).Unix(), 36 + } 37 + } 38 + 39 + func generateToken() string { 40 + var token []byte 41 + 42 + for i := 0; i < TOKEN_LENGTH; i++ { 43 + token = append(token, TOKEN_CHARS[rand.Intn(len(TOKEN_CHARS))]) 44 + } 45 + 46 + return string(token) 47 + }
+59 -45
api/v1/admin/admin.go admin/http.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 - "math/rand" 6 + "html/template" 7 7 "net/http" 8 8 "os" 9 - // "strings" 9 + "path/filepath" 10 + "strings" 10 11 "time" 11 12 12 13 "arimelody.me/arimelody.me/discord" 13 14 "arimelody.me/arimelody.me/global" 14 15 ) 15 16 16 - type ( 17 - Session struct { 18 - UserID string 19 - Token string 20 - Expires int64 21 - } 22 - ) 23 - 24 - const TOKEN_LENGTH = 64 25 - const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 26 - 27 - var ADMIN_ID_DISCORD = func() string { 28 - envvar := os.Getenv("DISCORD_ADMIN_ID") 29 - if envvar == "" { 30 - fmt.Printf("DISCORD_ADMIN_ID was not provided. Admin login will be unavailable.\n") 31 - } 32 - return envvar 33 - }() 34 - 35 - var sessions []*Session 36 - 37 - func CreateSession(UserID string) Session { 38 - return Session{ 39 - UserID: UserID, 40 - Token: string(generateToken()), 41 - Expires: time.Now().Add(24 * time.Hour).Unix(), 42 - } 43 - } 44 - 45 17 func Handler() http.Handler { 46 18 mux := http.NewServeMux() 47 19 ··· 53 25 mux.Handle("/login", global.HTTPLog(LoginHandler())) 54 26 mux.Handle("/verify", global.HTTPLog(MustAuthorise(VerifyHandler()))) 55 27 mux.Handle("/logout", global.HTTPLog(MustAuthorise(LogoutHandler()))) 28 + mux.Handle("/static", global.HTTPLog(MustAuthorise(staticHandler()))) 56 29 57 30 return mux 58 31 } 59 32 60 33 func MustAuthorise(next http.Handler) http.Handler { 61 34 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 62 - // TEMPORARY 63 - ctx := context.WithValue(r.Context(), "role", "admin") 64 - next.ServeHTTP(w, r.WithContext(ctx)) 65 - return 66 - 67 - /* 68 35 auth := r.Header.Get("Authorization") 69 36 if strings.HasPrefix(auth, "Bearer ") { 70 37 auth = auth[7:] ··· 104 71 105 72 ctx := context.WithValue(r.Context(), "role", "admin") 106 73 next.ServeHTTP(w, r.WithContext(ctx)) 107 - */ 108 74 }) 109 75 } 110 76 ··· 143 109 } 144 110 145 111 // login success! 146 - session := CreateSession(discord_user.Username) 112 + session := createSession(discord_user.Username) 147 113 sessions = append(sessions, &session) 148 114 149 115 cookie := http.Cookie{} ··· 197 163 }) 198 164 } 199 165 200 - func generateToken() string { 201 - var token []byte 166 + func ServeTemplate(page string, data any) http.Handler { 167 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 168 + lp_layout := filepath.Join("views", "layout.html") 169 + lp_header := filepath.Join("views", "header.html") 170 + lp_footer := filepath.Join("views", "footer.html") 171 + lp_prideflag := filepath.Join("views", "prideflag.html") 172 + fp := filepath.Join("views", filepath.Clean(page)) 202 173 203 - for i := 0; i < TOKEN_LENGTH; i++ { 204 - token = append(token, TOKEN_CHARS[rand.Intn(len(TOKEN_CHARS))]) 205 - } 174 + info, err := os.Stat(fp) 175 + if err != nil { 176 + if os.IsNotExist(err) { 177 + http.NotFound(w, r) 178 + return 179 + } 180 + } 206 181 207 - return string(token) 182 + if info.IsDir() { 183 + http.NotFound(w, r) 184 + return 185 + } 186 + 187 + template, err := template.ParseFiles(lp_layout, lp_header, lp_footer, lp_prideflag, fp) 188 + if err != nil { 189 + fmt.Printf("Error parsing template files: %s\n", err) 190 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 191 + return 192 + } 193 + 194 + err = template.ExecuteTemplate(w, "layout.html", data) 195 + if err != nil { 196 + fmt.Printf("Error executing template: %s\n", err) 197 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 198 + return 199 + } 200 + }) 201 + } 202 + 203 + func staticHandler() http.Handler { 204 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 205 + info, err := os.Stat(filepath.Join("admin", "static", filepath.Clean(r.URL.Path))) 206 + // does the file exist? 207 + if err != nil { 208 + if os.IsNotExist(err) { 209 + http.NotFound(w, r) 210 + return 211 + } 212 + } 213 + 214 + // is thjs a directory? (forbidden) 215 + if info.IsDir() { 216 + http.NotFound(w, r) 217 + return 218 + } 219 + 220 + http.FileServer(http.Dir(filepath.Join("admin", "static"))).ServeHTTP(w, r) 221 + }) 208 222 }
api/v1/music/artist.go music/artist.go
+13 -5
api/v1/music/credit.go music/credit.go
··· 6 6 "github.com/jmoiron/sqlx" 7 7 ) 8 8 9 - type Credit struct { 10 - Artist *Artist `json:"artist"` 11 - Role string `json:"role"` 12 - Primary bool `json:"primary"` 13 - } 9 + type ( 10 + Credit struct { 11 + Artist *Artist `json:"artist"` 12 + Role string `json:"role"` 13 + Primary bool `json:"primary"` 14 + } 15 + 16 + PostCreditBody struct { 17 + Artist string `json:"artist"` 18 + Role string `json:"role"` 19 + Primary bool `json:"primary"` 20 + } 21 + ) 14 22 15 23 // GETTERS 16 24
api/v1/music/link.go music/link.go
+2 -12
api/v1/music/music.go music/music.go
··· 7 7 "path/filepath" 8 8 "strings" 9 9 10 - "arimelody.me/arimelody.me/api/v1/admin" 10 + "arimelody.me/arimelody.me/admin" 11 11 "arimelody.me/arimelody.me/global" 12 12 ) 13 - 14 - // func make_date_work(date string) time.Time { 15 - // res, err := time.Parse("2-Jan-2006", date) 16 - // if err != nil { 17 - // fmt.Printf("somehow we failed to parse %s! falling back to epoch :]\n", date) 18 - // return time.Unix(0, 0) 19 - // } 20 - // return res 21 - // } 22 13 23 14 // HTTP HANDLER METHODS 24 15 ··· 33 24 releases = append(releases, release) 34 25 } 35 26 36 - global.ServeTemplate("music.html", releases).ServeHTTP(w, r) 27 + global.ServeTemplate("music.html", Releases).ServeHTTP(w, r) 37 28 }) 38 29 } 39 30 ··· 96 87 } 97 88 98 89 fp := filepath.Join("data", "music-artwork", releaseID + ".png") 99 - fmt.Println(fp) 100 90 info, err := os.Stat(fp) 101 91 if err != nil { 102 92 if os.IsNotExist(err) {
+67 -20
api/v1/music/release.go music/release.go
··· 8 8 "strings" 9 9 "time" 10 10 11 - "arimelody.me/arimelody.me/api/v1/admin" 11 + "arimelody.me/arimelody.me/admin" 12 12 "github.com/jmoiron/sqlx" 13 13 ) 14 14 ··· 21 21 Compilation ReleaseType = "Compilation" 22 22 ) 23 23 24 - type Release struct { 25 - ID string `json:"id"` 26 - Title string `json:"title"` 27 - Description string `json:"description"` 28 - ReleaseType ReleaseType `json:"type"` 29 - ReleaseDate time.Time `json:"releaseDate"` 30 - Artwork string `json:"artwork"` 31 - Buyname string `json:"buyname"` 32 - Buylink string `json:"buylink"` 33 - Links []Link `json:"links"` 34 - Credits []Credit `json:"credits"` 35 - Tracks []Track `json:"tracks"` 36 - } 24 + type ( 25 + Release struct { 26 + ID string `json:"id"` 27 + Title string `json:"title"` 28 + Description string `json:"description"` 29 + ReleaseType ReleaseType `json:"type"` 30 + ReleaseDate time.Time `json:"releaseDate"` 31 + Artwork string `json:"artwork"` 32 + Buyname string `json:"buyname"` 33 + Buylink string `json:"buylink"` 34 + Links []Link `json:"links"` 35 + Credits []Credit `json:"credits"` 36 + Tracks []Track `json:"tracks"` 37 + } 38 + 39 + PostReleaseBody struct { 40 + ID string `json:"id"` 41 + Title string `json:"title"` 42 + Description string `json:"description"` 43 + ReleaseType ReleaseType `json:"type"` 44 + ReleaseDate time.Time `json:"releaseDate"` 45 + Artwork string `json:"artwork"` 46 + Buyname string `json:"buyname"` 47 + Buylink string `json:"buylink"` 48 + Links []Link `json:"links"` 49 + Credits []PostCreditBody `json:"credits"` 50 + Tracks []Track `json:"tracks"` 51 + } 52 + ) 37 53 38 54 var Releases []Release; 39 55 ··· 337 353 func PullAllReleases(db *sqlx.DB) ([]Release, error) { 338 354 releases := []Release{} 339 355 340 - rows, err := db.Query("SELECT id, title, description, type, release_date, artwork, buyname, buylink FROM musicreleases") 356 + rows, err := db.Query("SELECT id, title, description, type, release_date, artwork, buyname, buylink FROM musicreleases ORDER BY release_date DESC") 341 357 if err != nil { 342 358 return nil, err 343 359 } ··· 423 439 return 424 440 } 425 441 426 - var release Release 427 - err := json.NewDecoder(r.Body).Decode(&release) 442 + var data PostReleaseBody 443 + err := json.NewDecoder(r.Body).Decode(&data) 428 444 if err != nil { 429 445 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 430 446 return 431 447 } 432 448 433 - if GetRelease(release.ID) != nil { 434 - http.Error(w, fmt.Sprintf("Release %s already exists", release.ID), http.StatusBadRequest) 449 + if GetRelease(data.ID) != nil { 450 + http.Error(w, fmt.Sprintf("Release %s already exists", data.ID), http.StatusBadRequest) 435 451 return 436 452 } 437 453 438 - Releases = append(Releases, release) 454 + var credits = []Credit{} 455 + 456 + for _, credit := range data.Credits { 457 + var artist = GetArtist(credit.Artist) 458 + 459 + if artist == nil { 460 + http.Error(w, fmt.Sprintf("Artist %s does not exist", credit.Artist), http.StatusBadRequest) 461 + return 462 + } 463 + 464 + credits = append(credits, Credit{ 465 + Artist: artist, 466 + Role: credit.Role, 467 + Primary: credit.Primary, 468 + }) 469 + } 470 + 471 + var release = Release{ 472 + ID: data.ID, 473 + Title: data.Title, 474 + Description: data.Description, 475 + ReleaseType: data.ReleaseType, 476 + ReleaseDate: data.ReleaseDate, 477 + Artwork: data.Artwork, 478 + Buyname: data.Buyname, 479 + Buylink: data.Buylink, 480 + Links: data.Links, 481 + Credits: credits, 482 + Tracks: data.Tracks, 483 + } 484 + 485 + Releases = append([]Release{release}, Releases...) 439 486 440 487 jsonBytes, err := json.Marshal(release) 441 488 w.Header().Add("Content-Type", "application/json")
api/v1/music/track.go music/track.go
+11
colour/colour.go
··· 1 + package colour 2 + 3 + var Reset = "\033[0m" 4 + var Red = "\033[31m" 5 + var Green = "\033[32m" 6 + var Yellow = "\033[33m" 7 + var Blue = "\033[34m" 8 + var Purple = "\033[35m" 9 + var Cyan = "\033[36m" 10 + var Gray = "\033[37m" 11 + var White = "\033[97m"
+36 -15
discord/discord.go
··· 1 1 package discord 2 2 3 3 import ( 4 - "encoding/json" 5 - "errors" 6 - "fmt" 7 - "net/http" 8 - "net/url" 9 - "strings" 4 + "encoding/json" 5 + "errors" 6 + "fmt" 7 + "net/http" 8 + "net/url" 9 + "os" 10 + "strings" 10 11 ) 11 12 12 13 const API_ENDPOINT = "https://discord.com/api/v10" 13 - const CLIENT_ID = "1268013769578119208" 14 14 15 - // TODO: good GOD change this later please i beg you. we've already broken 16 - // the rules by doing this at all 17 - const CLIENT_SECRET = "JUEZnixhN7BxmLIHmbECiKETMP85VT0E" 18 - 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" 19 - 20 - // TODO: change before prod 21 - const MY_REDIRECT_URI = "http://127.0.0.1:8080/api/v1/admin/login" 15 + var CLIENT_ID = func() string { 16 + envvar := os.Getenv("DISCORD_CLIENT_ID") 17 + if envvar == "" { 18 + fmt.Printf("DISCORD_CLIENT_ID was not provided. Admin login will be unavailable.\n") 19 + } 20 + return envvar 21 + }() 22 + var CLIENT_SECRET = func() string { 23 + envvar := os.Getenv("DISCORD_CLIENT_SECRET") 24 + if envvar == "" { 25 + fmt.Printf("DISCORD_CLIENT_SECRET was not provided. Admin login will be unavailable.\n") 26 + } 27 + return envvar 28 + }() 29 + var REDIRECT_URI = func() string { 30 + envvar := os.Getenv("DISCORD_REDIRECT_URI") 31 + if envvar == "" { 32 + fmt.Printf("DISCORD_REDIRECT_URI was not provided. Admin login will be unavailable.\n") 33 + } 34 + return envvar 35 + }() 36 + var OAUTH_CALLBACK_URI = func() string { 37 + envvar := os.Getenv("OAUTH_CALLBACK_URI") 38 + if envvar == "" { 39 + fmt.Printf("OAUTH_CALLBACK_URI was not provided. Admin login will be unavailable.\n") 40 + } 41 + return envvar 42 + }() 22 43 23 44 type ( 24 45 AccessTokenResponse struct { ··· 63 84 "client_secret": {CLIENT_SECRET}, 64 85 "grant_type": {"authorization_code"}, 65 86 "code": {code}, 66 - "redirect_uri": {MY_REDIRECT_URI}, 87 + "redirect_uri": {OAUTH_CALLBACK_URI}, 67 88 }.Encode())) 68 89 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 69 90
+19 -20
global/global.go
··· 1 1 package global 2 2 3 3 import ( 4 - "fmt" 5 - "net/http" 6 - "os" 7 - "path/filepath" 8 - "html/template" 9 - "strconv" 10 - "time" 11 - ) 4 + "fmt" 5 + "net/http" 6 + "os" 7 + "path/filepath" 8 + "html/template" 9 + "strconv" 10 + "time" 12 11 13 - var MimeTypes = map[string]string{ 14 - "css": "text/css; charset=utf-8", 15 - "png": "image/png", 16 - "jpg": "image/jpg", 17 - "webp": "image/webp", 18 - "html": "text/html", 19 - "asc": "text/plain", 20 - "pub": "text/plain", 21 - "txt": "text/plain", 22 - "js": "application/javascript", 23 - } 12 + "arimelody.me/arimelody.me/colour" 13 + ) 24 14 25 15 func DefaultHeaders(next http.Handler) http.Handler { 26 16 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ··· 54 44 elapsed = strconv.Itoa(difference) 55 45 } 56 46 57 - fmt.Printf("[%s] %s %s - %d (%sms) (%s)\n", 47 + codeColour := colour.Reset 48 + 49 + if lrw.Code - 600 <= 0 { codeColour = colour.Red } 50 + if lrw.Code - 500 <= 0 { codeColour = colour.Yellow } 51 + if lrw.Code - 400 <= 0 { codeColour = colour.White } 52 + if lrw.Code - 300 <= 0 { codeColour = colour.Green } 53 + 54 + fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n", 58 55 after.Format(time.UnixDate), 59 56 r.Method, 60 57 r.URL.Path, 58 + codeColour, 61 59 lrw.Code, 60 + colour.Reset, 62 61 elapsed, 63 62 r.Header["User-Agent"][0]) 64 63 })
+3 -3
main.go
··· 7 7 "os" 8 8 "path/filepath" 9 9 10 - "arimelody.me/arimelody.me/api/v1/admin" 11 - "arimelody.me/arimelody.me/api/v1/music" 10 + "arimelody.me/arimelody.me/admin" 11 + "arimelody.me/arimelody.me/music" 12 12 "arimelody.me/arimelody.me/db" 13 13 "arimelody.me/arimelody.me/global" 14 14 ) ··· 44 44 func createServeMux() *http.ServeMux { 45 45 mux := http.NewServeMux() 46 46 47 - mux.Handle("/api/v1/admin/", global.HTTPLog(http.StripPrefix("/api/v1/admin", admin.Handler()))) 47 + mux.Handle("/admin/", global.HTTPLog(http.StripPrefix("/admin", admin.Handler()))) 48 48 49 49 mux.Handle("/api/v1/music/artist/", global.HTTPLog(http.StripPrefix("/api/v1/music/artist", music.ServeArtist()))) 50 50 mux.Handle("/api/v1/music/", global.HTTPLog(http.StripPrefix("/api/v1/music", music.ServeRelease())))
+5 -5
public/script/header.js
··· 2 2 const hamburger = document.getElementById("header-links-toggle"); 3 3 4 4 document.addEventListener("click", event => { 5 - if (!header_links.contains(event.target) && !hamburger.contains(event.target) && !header_links.href) { 6 - header_links.classList.remove("open"); 7 - } 5 + if (!header_links.contains(event.target) && !hamburger.contains(event.target) && !header_links.href) { 6 + header_links.classList.remove("open"); 7 + } 8 8 }); 9 9 10 - hamburger.addEventListener("click", event => { 11 - header_links.classList.toggle("open"); 10 + hamburger.addEventListener("click", () => { 11 + header_links.classList.toggle("open"); 12 12 });
+9 -4
public/script/music.js
··· 1 1 import "./main.js"; 2 2 3 - document.addEventListener("swap", () => { 4 - document.querySelectorAll("h1.music-title").forEach(element => { 5 - element.href = ""; 6 - }); 3 + document.querySelectorAll("div.music").forEach(container => { 4 + const link = container.querySelector(".music-title a").href 5 + 6 + container.addEventListener("click", event => { 7 + if (event.target.href) return; 8 + 9 + event.preventDefault(); 10 + location = link; 11 + }); 7 12 });
+34 -27
schema.sql
··· 1 1 -- 2 2 -- Artists (should be applicable to all art) 3 3 -- 4 - CREATE TABLE IF NOT EXISTS artists ( 5 - id text NOT NULL, 6 - name text, 7 - website text 4 + CREATE TABLE artist ( 5 + id uuid DEFAULT gen_random_uuid(), 6 + name text NOT NULL, 7 + website text, 8 + avatar text 8 9 ); 9 - ALTER TABLE artists ADD CONSTRAINT artists_pk PRIMARY KEY (id); 10 + ALTER TABLE artist ADD CONSTRAINT artist_pk PRIMARY KEY (id); 10 11 11 12 -- 12 13 -- Music releases 13 14 -- 14 - CREATE TABLE IF NOT EXISTS musicreleases ( 15 + CREATE TABLE musicrelease ( 15 16 id character varying(64) NOT NULL, 16 17 title text NOT NULL, 17 18 description text, ··· 21 22 buyname text, 22 23 buylink text 23 24 ); 24 - ALTER TABLE musicreleases ADD CONSTRAINT musicreleases_pk PRIMARY KEY (id); 25 + ALTER TABLE musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); 25 26 26 27 -- 27 28 -- Music links (external platform links under a release) 28 29 -- 29 - CREATE TABLE IF NOT EXISTS musiclinks ( 30 + CREATE TABLE musiclink ( 30 31 release character varying(64) NOT NULL, 31 32 name text NOT NULL, 32 - url text 33 + url text NOT NULL 33 34 ); 34 - ALTER TABLE musiclinks ADD CONSTRAINT musiclinks_pk PRIMARY KEY (release, name); 35 + ALTER TABLE musiclink ADD CONSTRAINT musiclink_pk PRIMARY KEY (release, name); 35 36 36 37 -- 37 38 -- Music credits (artist credits under a release) 38 39 -- 39 - CREATE TABLE IF NOT EXISTS musiccredits ( 40 + CREATE TABLE musiccredit ( 40 41 release character varying(64) NOT NULL, 41 - artist text NOT NULL, 42 - role text, 43 - is_primary boolean 42 + artist uuid NOT NULL, 43 + role text NOT NULL, 44 + is_primary boolean DEFAULT false 44 45 ); 45 - ALTER TABLE musiccredits ADD CONSTRAINT musiccredits_pk PRIMARY KEY (release, artist); 46 + ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist); 46 47 47 48 -- 48 49 -- Music tracks (tracks under a release) 49 50 -- 50 - CREATE TABLE IF NOT EXISTS musictracks ( 51 - release character varying(64) NOT NULL, 52 - number integer NOT NULL, 51 + CREATE TABLE musictrack ( 52 + id uuid DEFAULT gen_random_uuid(), 53 53 title text NOT NULL, 54 54 description text, 55 55 lyrics text, 56 56 preview_url text 57 57 ); 58 - ALTER TABLE musictracks ADD CONSTRAINT musictracks_pk PRIMARY KEY (release, number); 58 + ALTER TABLE musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id); 59 59 60 60 -- 61 - -- Foreign keys 61 + -- Music release/track pairs 62 62 -- 63 - 64 - ALTER TABLE musiccredits ADD CONSTRAINT IF NOT EXISTS musiccredits_artist_fk FOREIGN KEY (artist) REFERENCES artists(id) ON DELETE CASCADE ON UPDATE CASCADE; 65 - 66 - ALTER TABLE musiccredits ADD CONSTRAINT IF NOT EXISTS musiccredits_release_fk FOREIGN KEY (release) REFERENCES musicreleases(id) ON DELETE CASCADE; 67 - 68 - ALTER TABLE musiclinks ADD CONSTRAINT IF NOT EXISTS musiclinks_release_fk FOREIGN KEY (release) REFERENCES musicreleases(id) ON UPDATE CASCADE ON DELETE CASCADE; 63 + CREATE TABLE musicreleasetrack ( 64 + release character varying(64) NOT NULL, 65 + track uuid NOT NULL, 66 + number integer NOT NULL 67 + ); 68 + ALTER TABLE musicreleasetrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (release, track); 69 69 70 - ALTER TABLE musictracks ADD CONSTRAINT IF NOT EXISTS musictracks_release_fk FOREIGN KEY (release) REFERENCES musicreleases(id) ON DELETE CASCADE; 70 + -- 71 + -- Foreign keys 72 + -- 73 + ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE; 74 + ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; 75 + ALTER TABLE musiclink ADD CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON UPDATE CASCADE ON DELETE CASCADE; 76 + ALTER TABLE musicreleasetrack ADD CONSTRAINT music_pair_trackref_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; 77 + ALTER TABLE musicreleasetrack ADD CONSTRAINT music_pair_releaseref_fk FOREIGN KEY (track) REFERENCES musictrack(id) ON DELETE CASCADE;
+1 -1
views/music-gateway.html
··· 111 111 <h2>tracks:</h2> 112 112 {{range $i, $track := .GetTracks}} 113 113 <details> 114 - <summary class="album-track-title">{{$i}}. {{$track.GetTitle}}</summary> 114 + <summary class="album-track-title">{{$track.GetNumber}}. {{$track.GetTitle}}</summary> 115 115 {{$track.GetLyrics}} 116 116 </details> 117 117 {{end}}
+70 -66
views/music.html
··· 18 18 19 19 {{define "content"}} 20 20 <main> 21 - <script type="module" src="/script/music.js"></script> 21 + <script type="module" src="/script/music.js"></script> 22 22 23 - <h1> 24 - # my music 25 - </h1> 23 + <h1> 24 + # my music 25 + </h1> 26 26 27 - <div id="music-container"> 28 - {{range $Release := .}} 29 - <div class="music" id="{{$Release.GetID}}" swap-url="/music/{{$Release.GetID}}"> 30 - <div class="music-artwork"> 31 - <img src="{{$Release.GetArtwork}}" alt="{{$Release.GetTitle}} artwork" width="128" loading="lazy"> 32 - </div> 33 - <div class="music-details"> 34 - <a href="/music/{{$Release.GetID}}"><h1 class="music-title">{{$Release.GetTitle}}</h1></a> 35 - <h2 class="music-artist">{{$Release.PrintArtists true true}}</h2> 36 - <h3 class="music-type-{{$Release.GetType}}">{{$Release.GetType}}</h3> 37 - <ul class="music-links"> 38 - {{range $Link := $Release.GetLinks}} 39 - <li> 40 - <a href="{{$Link.GetURL}}" class="link-button">{{$Link.GetName}}</a> 41 - </li> 42 - {{end}} 43 - </ul> 44 - </div> 45 - </div> 46 - {{end}} 27 + <div id="music-container"> 28 + {{range $Release := .}} 29 + <div class="music" id="{{$Release.GetID}}" swap-url="/music/{{$Release.GetID}}"> 30 + <div class="music-artwork"> 31 + <img src="{{$Release.GetArtwork}}" alt="{{$Release.GetTitle}} artwork" width="128" loading="lazy"> 32 + </div> 33 + <div class="music-details"> 34 + <h1 class="music-title"> 35 + <a href="/music/{{$Release.GetID}}"> 36 + {{$Release.GetTitle}} 37 + </a> 38 + </h1> 39 + <h2 class="music-artist">{{$Release.PrintArtists true true}}</h2> 40 + <h3 class="music-type-{{$Release.GetType}}">{{$Release.GetType}}</h3> 41 + <ul class="music-links"> 42 + {{range $Link := $Release.GetLinks}} 43 + <li> 44 + <a href="{{$Link.GetURL}}" class="link-button">{{$Link.GetName}}</a> 45 + </li> 46 + {{end}} 47 + </ul> 48 + </div> 47 49 </div> 50 + {{end}} 51 + </div> 48 52 49 - <h2 id="usage" class="question"> 50 - <a href="#usage"> 51 - &gt; "can i use your music in my content?" 52 - </a> 53 - </h2> 54 - <div class="answer"> 55 - <p> 56 - <strong class="big">yes!</strong> well, in most cases... 57 - </p> 58 - <p> 59 - from <a href="/music/dream">Dream (2022)</a> onward, all of my <em>self-released</em> songs are 60 - licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">Creative Commons Attribution-ShareAlike 4.0</a>. 61 - anyone may use and remix these songs freely, so long as they provide credit back to me and link back to this license! 62 - please note that all derivative works must inherit this license. 63 - </p> 64 - <p> 65 - a great example of some credit text would be as follows: 66 - </p> 67 - <blockquote> 68 - music used: mellodoot - Dream<br> 69 - <a href="/music/dream">https://arimelody.me/music/dream</a><br> 70 - licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>. 71 - </blockquote> 72 - <p> 73 - for any songs prior to this, they were all either released by me (in which case, i honestly 74 - don't mind), or in collaboration with chill people who i don't see having an issue with it. 75 - do be sure to ask them about it, though! 76 - </p> 77 - <p> 78 - in the event the song you want to use is released under some other label, their usage rights 79 - will more than likely trump whatever i'd otherwise have in mind. i'll try to negotiate some 80 - nice terms, though! ;3 81 - </p> 82 - <p> 83 - i love the idea of other creators using my songs in their work, so if you do happen to use 84 - my stuff in a work you're particularly proud of, feel free to send it my way! 85 - </p> 86 - <p> 87 - &gt; <a href="mailto:ari@arimelody.me">ari@arimelody.me</a> 88 - </p> 89 - </div> 53 + <h2 id="usage" class="question"> 54 + <a href="#usage"> 55 + &gt; "can i use your music in my content?" 56 + </a> 57 + </h2> 58 + <div class="answer"> 59 + <p> 60 + <strong class="big">yes!</strong> well, in most cases... 61 + </p> 62 + <p> 63 + from <a href="/music/dream">Dream (2022)</a> onward, all of my <em>self-released</em> songs are 64 + licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">Creative Commons Attribution-ShareAlike 4.0</a>. 65 + anyone may use and remix these songs freely, so long as they provide credit back to me and link back to this license! 66 + please note that all derivative works must inherit this license. 67 + </p> 68 + <p> 69 + a great example of some credit text would be as follows: 70 + </p> 71 + <blockquote> 72 + music used: mellodoot - Dream<br> 73 + <a href="/music/dream">https://arimelody.me/music/dream</a><br> 74 + licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>. 75 + </blockquote> 76 + <p> 77 + for any songs prior to this, they were all either released by me (in which case, i honestly 78 + don't mind), or in collaboration with chill people who i don't see having an issue with it. 79 + do be sure to ask them about it, though! 80 + </p> 81 + <p> 82 + in the event the song you want to use is released under some other label, their usage rights 83 + will more than likely trump whatever i'd otherwise have in mind. i'll try to negotiate some 84 + nice terms, though! ;3 85 + </p> 86 + <p> 87 + i love the idea of other creators using my songs in their work, so if you do happen to use 88 + my stuff in a work you're particularly proud of, feel free to send it my way! 89 + </p> 90 + <p> 91 + &gt; <a href="mailto:ari@arimelody.me">ari@arimelody.me</a> 92 + </p> 93 + </div> 90 94 91 - <a href="#" id="backtotop">back to top</a> 95 + <a href="#" id="backtotop">back to top</a> 92 96 </main> 93 97 {{end}}