From 9602918a1a378fb4a9f863fd8c93731e8eb5ef2a Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 15 Sep 2024 02:10:13 +0100 Subject: [PATCH 01/24] updated music #usage info --- views/music.html | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/views/music.html b/views/music.html index 7473dc2..6f84672 100644 --- a/views/music.html +++ b/views/music.html @@ -59,11 +59,11 @@

- yes! well, in most cases... + yes!* in most cases...

- from Dream (2022) onward, all of my self-released songs are - licensed under Creative Commons Attribution-ShareAlike 4.0. + all of my self-released songs are licensed under + Creative Commons Attribution-ShareAlike 4.0. 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.

@@ -71,23 +71,17 @@ a great example of some credit text would be as follows:

- music used: mellodoot - Dream
- https://arimelody.me/music/dream
- licensed under CC BY-SA 4.0. + music used: ari melody - free2play
+ https://arimelody.me/music/free2play
+ licensed under CC BY-SA 4.0.

- 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. - do be sure to ask them about it, though! + if the song you want to use is not released by me (i.e. under a record label), their usage rights + will likely trump whatever i'd otherwise have in mind. i'll try to negotiate some nice terms, though!

- 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 - nice terms, though! ;3 -

-

- 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! + i believe that encouraging creative use of artistic works is better than stifling any use at all. + if you do happen to use my work in something you're particularly proud of, feel free to send it my way!

> ari@arimelody.me From f7edece0af6c312886f5bf923499f70da1c4cf52 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 23 Sep 2024 00:57:23 +0100 Subject: [PATCH 02/24] API login/register/delete-account, automatic db schema init --- .dockerignore | 2 + account/controller/account.go | 60 ++++++++++++ account/model/account.go | 54 +++++++++++ admin/admin.go | 2 +- api/account.go | 175 ++++++++++++++++++++++++++++++++++ api/api.go | 6 ++ discord/discord.go | 4 +- global/data.go | 11 ++- go.mod | 2 + go.sum | 2 + main.go | 169 +++++++++++++++++++++++++++++++- 11 files changed, 479 insertions(+), 8 deletions(-) create mode 100644 account/controller/account.go create mode 100644 account/model/account.go create mode 100644 api/account.go diff --git a/.dockerignore b/.dockerignore index 0e34f0f..fc62ba5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,8 @@ uploads/* test/ tmp/ +db/ docker-compose.yml +docker-compose-test.yml Dockerfile schema.sql diff --git a/account/controller/account.go b/account/controller/account.go new file mode 100644 index 0000000..267c43b --- /dev/null +++ b/account/controller/account.go @@ -0,0 +1,60 @@ +package controller + +import ( + "arimelody-web/account/model" + + "github.com/jmoiron/sqlx" +) + +func GetAccount(db *sqlx.DB, username string) (*model.Account, error) { + var account = model.Account{} + + err := db.Get(&account, "SELECT * FROM account WHERE username=$1", username) + if err != nil { + return nil, err + } + + return &account, nil +} + +func GetAccountByEmail(db *sqlx.DB, email string) (*model.Account, error) { + var account = model.Account{} + + err := db.Get(&account, "SELECT * FROM account WHERE email=$1", email) + if err != nil { + return nil, err + } + + return &account, nil +} + +func CreateAccount(db *sqlx.DB, account *model.Account) error { + _, err := db.Exec( + "INSERT INTO account (username, password, email, avatar_url) " + + "VALUES ($1, $2, $3, $4)", + account.Username, + account.Password, + account.Email, + account.AvatarURL) + + return err +} + +func UpdateAccount(db *sqlx.DB, account *model.Account) error { + _, err := db.Exec( + "UPDATE account " + + "SET username=$2, password=$3, email=$4, avatar_url=$5) " + + "WHERE id=$1", + account.ID, + account.Username, + account.Password, + account.Email, + account.AvatarURL) + + return err +} + +func DeleteAccount(db *sqlx.DB, accountID string) error { + _, err := db.Exec("DELETE FROM account WHERE id=$1", accountID) + return err +} diff --git a/account/model/account.go b/account/model/account.go new file mode 100644 index 0000000..db32451 --- /dev/null +++ b/account/model/account.go @@ -0,0 +1,54 @@ +package model + +import ( + "math/rand" + "time" +) + +type ( + Account struct { + ID string `json:"id" db:"id"` + Username string `json:"username" db:"username"` + Password []byte `json:"password" db:"password"` + Email string `json:"email" db:"email"` + AvatarURL string `json:"avatar_url" db:"avatar_url"` + Privileges []AccountPrivilege `json:"privileges"` + } + + AccountPrivilege string + + Invite struct { + Code string `db:"code"` + CreatedByID string `db:"created_by"` + CreatedAt time.Time `db:"created_at"` + ExpiresAt time.Time `db:"expires_at"` + } +) + +const ( + Root AccountPrivilege = "root" // grants all permissions. very dangerous to grant! + + // unused for now + CreateInvites AccountPrivilege = "create_invites" + ReadAccounts AccountPrivilege = "read_accounts" + EditAccounts AccountPrivilege = "edit_accounts" + + ReadReleases AccountPrivilege = "read_releases" + EditReleases AccountPrivilege = "edit_releases" + + ReadTracks AccountPrivilege = "read_tracks" + EditTracks AccountPrivilege = "edit_tracks" + + ReadArtists AccountPrivilege = "read_artists" + EditArtists AccountPrivilege = "edit_artists" +) + +var inviteChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + +func GenerateInviteCode(length int) []byte { + code := []byte{} + for i := 0; i < length; i++ { + code = append(code, inviteChars[rand.Intn(len(inviteChars) - 1)]) + } + return code +} diff --git a/admin/admin.go b/admin/admin.go index 3c2aaae..d2bf80c 100644 --- a/admin/admin.go +++ b/admin/admin.go @@ -31,7 +31,7 @@ var ADMIN_BYPASS = func() bool { var ADMIN_ID_DISCORD = func() string { id := os.Getenv("DISCORD_ADMIN") if id == "" { - fmt.Printf("WARN: Discord admin ID (DISCORD_ADMIN) was not provided. Admin login will be unavailable.\n") + // fmt.Printf("WARN: Discord admin ID (DISCORD_ADMIN) was not provided.\n") } return id }() diff --git a/api/account.go b/api/account.go new file mode 100644 index 0000000..37cf9a1 --- /dev/null +++ b/api/account.go @@ -0,0 +1,175 @@ +package api + +import ( + "arimelody-web/account/controller" + "arimelody-web/account/model" + "arimelody-web/global" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "time" + + "golang.org/x/crypto/bcrypt" +) + +func handleLogin() http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` + } + + credentials := LoginRequest{} + err := json.NewDecoder(r.Body).Decode(&credentials) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + account, err := controller.GetAccount(global.DB, credentials.Username) + if err != nil { + if strings.Contains(err.Error(), "no rows") { + http.Error(w, "Invalid username or password", http.StatusBadRequest) + return + } + fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %s\n", err.Error()) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + err = bcrypt.CompareHashAndPassword(account.Password, []byte(credentials.Password)) + if err != nil { + http.Error(w, "Invalid username or password", http.StatusBadRequest) + return + } + + // TODO: sessions and tokens + + w.WriteHeader(http.StatusOK) + w.Write([]byte("Logged in successfully. TODO: Session tokens\n")) + }) +} + +func handleAccountRegistration() http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + type RegisterRequest struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + Code string `json:"code"` + } + + credentials := RegisterRequest{} + err := json.NewDecoder(r.Body).Decode(&credentials) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + // make sure code exists in DB + invite := model.Invite{} + err = global.DB.Get(&invite, "SELECT * FROM invite WHERE code=$1", credentials.Code) + if err != nil { + if strings.Contains(err.Error(), "no rows") { + http.Error(w, "Invalid invite code", http.StatusBadRequest) + return + } + fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %s\n", err.Error()) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + if time.Now().After(invite.ExpiresAt) { + http.Error(w, "Invalid invite code", http.StatusBadRequest) + _, err = global.DB.Exec("DELETE FROM invite WHERE code=$1", credentials.Code) + if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %s\n", err.Error()) } + return + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %s\n", err.Error()) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + account := model.Account{ + Username: credentials.Username, + Password: hashedPassword, + Email: credentials.Email, + AvatarURL: "/img/default-avatar.png", + } + err = controller.CreateAccount(global.DB, &account) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %s\n", err.Error()) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + _, err = global.DB.Exec("DELETE FROM invite WHERE code=$1", credentials.Code) + if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %s\n", err.Error()) } + + w.WriteHeader(http.StatusCreated) + w.Write([]byte("Account created successfully\n")) + }) +} + +func handleDeleteAccount() http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` + } + + credentials := LoginRequest{} + err := json.NewDecoder(r.Body).Decode(&credentials) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + account, err := controller.GetAccount(global.DB, credentials.Username) + if err != nil { + if strings.Contains(err.Error(), "no rows") { + http.Error(w, "Invalid username or password", http.StatusBadRequest) + return + } + fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %s\n", err.Error()) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + err = bcrypt.CompareHashAndPassword(account.Password, []byte(credentials.Password)) + if err != nil { + http.Error(w, "Invalid username or password", http.StatusBadRequest) + return + } + + err = controller.DeleteAccount(global.DB, account.ID) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to delete account: %s\n", err.Error()) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("Account deleted successfully\n")) + }) +} diff --git a/api/api.go b/api/api.go index 699cf1c..bbeddb3 100644 --- a/api/api.go +++ b/api/api.go @@ -14,6 +14,12 @@ import ( func Handler() http.Handler { mux := http.NewServeMux() + // ACCOUNT ENDPOINTS + + mux.Handle("/v1/login", handleLogin()) + mux.Handle("/v1/register", handleAccountRegistration()) + mux.Handle("/v1/delete-account", handleDeleteAccount()) + // ARTIST ENDPOINTS mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/discord/discord.go b/discord/discord.go index c3b3cec..e517418 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -18,7 +18,7 @@ var CREDENTIALS_PROVIDED = true var CLIENT_ID = func() string { id := os.Getenv("DISCORD_CLIENT") if id == "" { - fmt.Printf("WARN: Discord client ID (DISCORD_CLIENT) was not provided. Admin login will be unavailable.\n") + // fmt.Printf("WARN: Discord client ID (DISCORD_CLIENT) was not provided.\n") CREDENTIALS_PROVIDED = false } return id @@ -26,7 +26,7 @@ var CLIENT_ID = func() string { var CLIENT_SECRET = func() string { secret := os.Getenv("DISCORD_SECRET") if secret == "" { - fmt.Printf("WARN: Discord secret (DISCORD_SECRET) was not provided. Admin login will be unavailable.\n") + // fmt.Printf("WARN: Discord secret (DISCORD_SECRET) was not provided.\n") CREDENTIALS_PROVIDED = false } return secret diff --git a/global/data.go b/global/data.go index fb641fc..11c2814 100644 --- a/global/data.go +++ b/global/data.go @@ -34,7 +34,7 @@ var Args = func() map[string]string { return args }() -var HTTP_DOMAIN = func() string { +var HTTP_DOMAIN = func() string { domain := os.Getenv("HTTP_DOMAIN") if domain == "" { return "https://arimelody.me" @@ -42,4 +42,13 @@ var HTTP_DOMAIN = func() string { return domain }() +var APP_SECRET = func() string { + secret := os.Getenv("ARIMELODY_SECRET") + if secret == "" { + fmt.Fprintln(os.Stderr, "FATAL: ARIMELODY_SECRET was not provided. Cannot continue.") + os.Exit(1) + } + return secret +}() + var DB *sqlx.DB diff --git a/go.mod b/go.mod index e2beaf4..bcb2e16 100644 --- a/go.mod +++ b/go.mod @@ -6,3 +6,5 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 ) + +require golang.org/x/crypto v0.27.0 // indirect diff --git a/go.sum b/go.sum index f4ce337..352b928 100644 --- a/go.sum +++ b/go.sum @@ -8,3 +8,5 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= diff --git a/main.go b/main.go index c103696..5f7e258 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "path/filepath" "time" + "arimelody-web/account/model" "arimelody-web/admin" "arimelody-web/api" "arimelody-web/global" @@ -27,9 +28,9 @@ func main() { if dbHost == "" { dbHost = "127.0.0.1" } var err error - global.DB, err = sqlx.Connect("postgres", "host=" + dbHost + " user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable") + global.DB, err = initDB("postgres", "host=" + dbHost + " user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable") if err != nil { - fmt.Fprintf(os.Stderr, "FATAL: Unable to create database connection pool: %v\n", err) + fmt.Fprintf(os.Stderr, "FATAL: Unable to initialise database: %v\n", err) os.Exit(1) } global.DB.SetConnMaxLifetime(time.Minute * 3) @@ -37,6 +38,41 @@ func main() { global.DB.SetMaxIdleConns(10) defer global.DB.Close() + _, err = global.DB.Exec("DELETE FROM invite WHERE expires_at < CURRENT_TIMESTAMP") + if err != nil { + fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired invite codes: %v\n", err) + os.Exit(1) + } + + accountsCount := 0 + global.DB.Get(&accountsCount, "SELECT count(*) FROM account") + if accountsCount == 0 { + code := model.GenerateInviteCode(8) + + tx, err := global.DB.Begin() + if err != nil { + fmt.Fprintf(os.Stderr, "FATAL: Failed to begin transaction: %v\n", err) + os.Exit(1) + } + _, err = tx.Exec("DELETE FROM invite") + if err != nil { + fmt.Fprintf(os.Stderr, "FATAL: Failed to clear existing invite codes: %v\n", err) + os.Exit(1) + } + _, err = tx.Exec("INSERT INTO invite (code,expires_at) VALUES ($1, $2)", code, time.Now().Add(60 * time.Minute)) + if err != nil { + fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite codes: %v\n", err) + os.Exit(1) + } + err = tx.Commit() + if err != nil { + fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite codes: %v\n", err) + os.Exit(1) + } + + fmt.Fprintln(os.Stdout, "INFO: No accounts exist! Generated invite code: " + string(code) + " (Use this at /register or /api/v1/register)") + } + // start the web server! mux := createServeMux() port := DEFAULT_PORT @@ -44,9 +80,134 @@ func main() { log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), global.HTTPLog(mux))) } +func initDB(driverName string, dataSourceName string) (*sqlx.DB, error) { + db, err := sqlx.Connect(driverName, dataSourceName) + if err != nil { return nil, err } + + // ensure tables exist + // account + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS account (" + + "id uuid PRIMARY KEY DEFAULT gen_random_uuid(), " + + "username text NOT NULL UNIQUE, " + + "password text NOT NULL, " + + "email text, " + + "avatar_url text)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create account table: %s", err.Error())) } + + // privilege + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS privilege (" + + "account uuid NOT NULL, " + + "privilege text NOT NULL, " + + "CONSTRAINT privilege_pk PRIMARY KEY (account, privilege), " + + "CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create privilege table: %s", err.Error())) } + + // totp + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS totp (" + + "account uuid NOT NULL, " + + "name text NOT NULL, " + + "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "CONSTRAINT totp_pk PRIMARY KEY (account, name), " + + "CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create TOTP table: %s", err.Error())) } + + // invites + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS invite (" + + "code text NOT NULL PRIMARY KEY, " + + "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "expires_at TIMESTAMP NOT NULL)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create TOTP table: %s", err.Error())) } + + // artist + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS artist (" + + "id character varying(64) PRIMARY KEY, " + + "name text NOT NULL, " + + "website text, " + + "avatar text)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create artist table: %s", err.Error())) } + + // musicrelease + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS musicrelease (" + + "id character varying(64) PRIMARY KEY, " + + "visible bool DEFAULT false, " + + "title text NOT NULL, " + + "description text, " + + "type text, " + + "release_date TIMESTAMP NOT NULL, " + + "artwork text, " + + "buyname text, " + + "buylink text, " + + "copyright text, " + + "copyrightURL text)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musicrelease table: %s", err.Error())) } + + // musiclink + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS public.musiclink (" + + "release character varying(64) NOT NULL, " + + "name text NOT NULL, " + + "url text NOT NULL, " + + "CONSTRAINT musiclink_pk PRIMARY KEY (release, name), " + + "CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musiclink table: %s", err.Error())) } + + // musiccredit + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS public.musiccredit (" + + "release character varying(64) NOT NULL, " + + "artist character varying(64) NOT NULL, " + + "role text NOT NULL, " + + "is_primary boolean DEFAULT false, " + + "CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist), " + + "CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE, " + + "CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musiccredit table: %s", err.Error())) } + + // musictrack + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS public.musictrack (" + + "id uuid DEFAULT gen_random_uuid() PRIMARY KEY, " + + "title text NOT NULL, " + + "description text, " + + "lyrics text, " + + "preview_url text)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musictrack table: %s", err.Error())) } + + // musicreleasetrack + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS public.musicreleasetrack (" + + "release character varying(64) NOT NULL, " + + "track uuid NOT NULL, " + + "number integer NOT NULL, " + + "CONSTRAINT musicreleasetrack_pk PRIMARY KEY (release, track), " + + "CONSTRAINT musicreleasetrack_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE, " + + "CONSTRAINT musicreleasetrack_artist_fk FOREIGN KEY (track) REFERENCES track(id) ON DELETE CASCADE)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musicreleasetrack table: %s", err.Error())) } + + // TODO: automatic database migration + + return db, nil +} + func createServeMux() *http.ServeMux { mux := http.NewServeMux() - + mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler())) mux.Handle("/api/", http.StripPrefix("/api", api.Handler())) mux.Handle("/music/", http.StripPrefix("/music", musicView.Handler())) @@ -61,7 +222,7 @@ func createServeMux() *http.ServeMux { } staticHandler("public").ServeHTTP(w, r) })) - + return mux } From f0d29126abc186c58c646afc0f970d222f2d2cfe Mon Sep 17 00:00:00 2001 From: ari melody Date: Fri, 1 Nov 2024 19:15:19 +0000 Subject: [PATCH 03/24] add more detail to credits on /api/v1/artist/{id} --- .air.toml | 2 +- admin/artisthttp.go | 2 +- api/artist.go | 18 +++++++++++++++--- api/release.go | 7 +++++++ music/controller/artist.go | 13 +++++++------ music/controller/release.go | 20 ++++++++++---------- 6 files changed, 41 insertions(+), 21 deletions(-) diff --git a/.air.toml b/.air.toml index ea2b4d5..f8a1acf 100644 --- a/.air.toml +++ b/.air.toml @@ -7,7 +7,7 @@ tmp_dir = "tmp" bin = "./tmp/main" cmd = "go build -o ./tmp/main ." delay = 1000 - exclude_dir = ["admin\\static", "public", "uploads", "test"] + exclude_dir = ["admin/static", "public", "uploads", "test", "db"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false diff --git a/admin/artisthttp.go b/admin/artisthttp.go index 31a7c9c..4f66798 100644 --- a/admin/artisthttp.go +++ b/admin/artisthttp.go @@ -25,7 +25,7 @@ func serveArtist() http.Handler { return } - credits, err := music.GetArtistCredits(global.DB, artist.ID) + 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) diff --git a/api/artist.go b/api/artist.go index 2d5f5e6..90bd18a 100644 --- a/api/artist.go +++ b/api/artist.go @@ -8,7 +8,9 @@ import ( "os" "path/filepath" "strings" + "time" + "arimelody-web/admin" "arimelody-web/global" db "arimelody-web/music/controller" music "arimelody-web/music/controller" @@ -37,8 +39,12 @@ func ServeArtist(artist *model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { type ( creditJSON struct { - Role string `json:"role"` - Primary bool `json:"primary"` + ID string `json:"id"` + Title string `json:"title"` + ReleaseDate time.Time `json:"releaseDate" db:"release_date"` + Artwork string `json:"artwork"` + Role string `json:"role"` + Primary bool `json:"primary"` } artistJSON struct { *model.Artist @@ -46,8 +52,10 @@ func ServeArtist(artist *model.Artist) http.Handler { } ) + show_hidden_releases := admin.GetSession(r) != nil + var dbCredits []*model.Credit - dbCredits, err := db.GetArtistCredits(global.DB, artist.ID) + dbCredits, err := db.GetArtistCredits(global.DB, artist.ID, show_hidden_releases) if err != nil { fmt.Printf("FATAL: Failed to retrieve artist credits for %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -57,6 +65,10 @@ func ServeArtist(artist *model.Artist) http.Handler { var credits = map[string]creditJSON{} for _, credit := range dbCredits { credits[credit.Release.ID] = creditJSON{ + ID: credit.Release.ID, + Title: credit.Release.Title, + ReleaseDate: credit.Release.ReleaseDate, + Artwork: credit.Release.Artwork, Role: credit.Role, Primary: credit.Primary, } diff --git a/api/release.go b/api/release.go index 763383f..3027a8c 100644 --- a/api/release.go +++ b/api/release.go @@ -27,6 +27,7 @@ func ServeCatalog() http.Handler { 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"` @@ -40,9 +41,15 @@ func ServeCatalog() http.Handler { if !release.Visible && !authorised { continue } + artists := []string{} + for _, credit := range release.Credits { + if !credit.Primary { continue } + artists = append(artists, credit.Artist.Name) + } catalog = append(catalog, Release{ ID: release.ID, Title: release.Title, + Artists: artists, ReleaseType: release.ReleaseType, ReleaseDate: release.ReleaseDate, Artwork: release.Artwork, diff --git a/music/controller/artist.go b/music/controller/artist.go index 2ae92e7..10824a8 100644 --- a/music/controller/artist.go +++ b/music/controller/artist.go @@ -44,15 +44,15 @@ func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, err return artists, nil } -func GetArtistCredits(db *sqlx.DB, artistID string) ([]*model.Credit, error) { - rows, err := db.Query( - "SELECT release.id,release.title,release.artwork,artist.id,artist.name,artist.website,artist.avatar,role,is_primary "+ +func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.Credit, error) { + var query string = "SELECT release.id,title,artwork,release_date,artist.id,name,website,avatar,role,is_primary "+ "FROM musiccredit "+ "JOIN musicrelease AS release ON release=release.id "+ "JOIN artist ON artist=artist.id "+ - "WHERE artist=$1 "+ - "ORDER BY release_date DESC", - artistID) + "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 } @@ -69,6 +69,7 @@ func GetArtistCredits(db *sqlx.DB, artistID string) ([]*model.Credit, error) { &credit.Release.ID, &credit.Release.Title, &credit.Release.Artwork, + &credit.Release.ReleaseDate, &credit.Artist.ID, &credit.Artist.Name, &credit.Artist.Website, diff --git a/music/controller/release.go b/music/controller/release.go index 4bd2cd6..2aeb236 100644 --- a/music/controller/release.go +++ b/music/controller/release.go @@ -66,17 +66,17 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod return nil, err } - if full { - 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) - } + 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 { From 96cc64464fb581364f485eb7e52013cb1eaf2367 Mon Sep 17 00:00:00 2001 From: ari melody Date: Fri, 1 Nov 2024 19:33:26 +0000 Subject: [PATCH 04/24] move models, views, and controllers to root --- .dockerignore | 1 + .gitignore | 2 +- admin/artisthttp.go | 8 +- admin/http.go | 16 +-- admin/releasehttp.go | 14 +- admin/trackhttp.go | 8 +- api/api.go | 11 +- api/artist.go | 15 +-- api/release.go | 114 ++++++++++++++-- api/track.go | 14 +- {music/controller => controller}/artist.go | 4 +- {music/controller => controller}/release.go | 4 +- {music/controller => controller}/track.go | 4 +- main.go | 4 +- {music/model => model}/artist.go | 0 {music/model => model}/credit.go | 0 {music/model => model}/link.go | 0 {music/model => model}/release.go | 0 {music/model => model}/track.go | 0 music/view/release.go | 136 -------------------- {music/view => view}/music.go | 38 +++++- 21 files changed, 190 insertions(+), 203 deletions(-) rename {music/controller => controller}/artist.go (98%) rename {music/controller => controller}/release.go (99%) rename {music/controller => controller}/track.go (98%) rename {music/model => model}/artist.go (100%) rename {music/model => model}/credit.go (100%) rename {music/model => model}/link.go (100%) rename {music/model => model}/release.go (100%) rename {music/model => model}/track.go (100%) delete mode 100644 music/view/release.go rename {music/view => view}/music.go (51%) diff --git a/.dockerignore b/.dockerignore index 0e34f0f..785d79e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,7 @@ uploads/* test/ tmp/ +res/ docker-compose.yml Dockerfile schema.sql diff --git a/.gitignore b/.gitignore index 108e0e4..781b36e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,5 @@ db/ tmp/ test/ -uploads/* +uploads/ docker-compose-test.yml diff --git a/admin/artisthttp.go b/admin/artisthttp.go index 4f66798..ba26240 100644 --- a/admin/artisthttp.go +++ b/admin/artisthttp.go @@ -6,15 +6,15 @@ import ( "strings" "arimelody-web/global" - "arimelody-web/music/model" - "arimelody-web/music/controller" + "arimelody-web/model" + "arimelody-web/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) + artist, err := controller.GetArtist(global.DB, id) if err != nil { if artist == nil { http.NotFound(w, r) @@ -25,7 +25,7 @@ func serveArtist() http.Handler { return } - credits, err := music.GetArtistCredits(global.DB, artist.ID, true) + credits, err := controller.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) diff --git a/admin/http.go b/admin/http.go index 35ff3f6..da3539a 100644 --- a/admin/http.go +++ b/admin/http.go @@ -11,8 +11,8 @@ import ( "arimelody-web/discord" "arimelody-web/global" - musicDB "arimelody-web/music/controller" - musicModel "arimelody-web/music/model" + "arimelody-web/controller" + "arimelody-web/model" ) type loginData struct { @@ -41,21 +41,21 @@ func Handler() http.Handler { return } - releases, err := musicDB.GetAllReleases(global.DB, false, 0, true) + releases, err := controller.GetAllReleases(global.DB, false, 0, true) if err != nil { fmt.Printf("FATAL: Failed to pull releases: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - artists, err := musicDB.GetAllArtists(global.DB) + artists, err := controller.GetAllArtists(global.DB) if err != nil { fmt.Printf("FATAL: Failed to pull artists: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - tracks, err := musicDB.GetOrphanTracks(global.DB) + tracks, err := controller.GetOrphanTracks(global.DB) if err != nil { fmt.Printf("FATAL: Failed to pull orphan tracks: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -63,9 +63,9 @@ func Handler() http.Handler { } type IndexData struct { - Releases []*musicModel.Release - Artists []*musicModel.Artist - Tracks []*musicModel.Track + Releases []*model.Release + Artists []*model.Artist + Tracks []*model.Track } err = pages["index"].Execute(w, IndexData{ diff --git a/admin/releasehttp.go b/admin/releasehttp.go index bacaf01..54d0fd6 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -6,8 +6,8 @@ import ( "strings" "arimelody-web/global" - db "arimelody-web/music/controller" - "arimelody-web/music/model" + "arimelody-web/controller" + "arimelody-web/model" ) func serveRelease() http.Handler { @@ -15,7 +15,7 @@ func serveRelease() http.Handler { slices := strings.Split(r.URL.Path[1:], "/") releaseID := slices[0] - release, err := db.GetRelease(global.DB, releaseID, true) + release, err := controller.GetRelease(global.DB, releaseID, true) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -81,7 +81,7 @@ func serveEditCredits(release *model.Release) http.Handler { func serveAddCredit(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - artists, err := db.GetArtistsNotOnRelease(global.DB, release.ID) + artists, err := controller.GetArtistsNotOnRelease(global.DB, release.ID) if err != nil { fmt.Printf("FATAL: Failed to pull artists not on %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -108,7 +108,7 @@ func serveAddCredit(release *model.Release) http.Handler { func serveNewCredit() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { artistID := strings.Split(r.URL.Path, "/")[3] - artist, err := db.GetArtist(global.DB, artistID) + artist, err := controller.GetArtist(global.DB, artistID) if err != nil { fmt.Printf("FATAL: Failed to pull artists %s: %s\n", artistID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -152,7 +152,7 @@ func serveEditTracks(release *model.Release) http.Handler { func serveAddTrack(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - tracks, err := db.GetTracksNotOnRelease(global.DB, release.ID) + tracks, err := controller.GetTracksNotOnRelease(global.DB, release.ID) if err != nil { fmt.Printf("FATAL: Failed to pull tracks not on %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -180,7 +180,7 @@ func serveAddTrack(release *model.Release) http.Handler { func serveNewTrack() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { trackID := strings.Split(r.URL.Path, "/")[3] - track, err := db.GetTrack(global.DB, trackID) + track, err := controller.GetTrack(global.DB, trackID) if err != nil { fmt.Printf("Error rendering new track component for %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/trackhttp.go b/admin/trackhttp.go index 732d34a..148b7d8 100644 --- a/admin/trackhttp.go +++ b/admin/trackhttp.go @@ -6,15 +6,15 @@ import ( "strings" "arimelody-web/global" - "arimelody-web/music/model" - "arimelody-web/music/controller" + "arimelody-web/model" + "arimelody-web/controller" ) func serveTrack() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { slices := strings.Split(r.URL.Path[1:], "/") id := slices[0] - track, err := music.GetTrack(global.DB, id) + track, err := controller.GetTrack(global.DB, id) 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) @@ -25,7 +25,7 @@ func serveTrack() http.Handler { return } - releases, err := music.GetTrackReleases(global.DB, track.ID, true) + releases, err := controller.GetTrackReleases(global.DB, track.ID, true) if err != nil { fmt.Printf("FATAL: Failed to pull releases for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/api/api.go b/api/api.go index 699cf1c..4757a42 100644 --- a/api/api.go +++ b/api/api.go @@ -7,8 +7,7 @@ import ( "arimelody-web/admin" "arimelody-web/global" - music "arimelody-web/music/controller" - musicView "arimelody-web/music/view" + "arimelody-web/controller" ) func Handler() http.Handler { @@ -18,7 +17,7 @@ func Handler() http.Handler { 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] - artist, err := music.GetArtist(global.DB, artistID) + artist, err := controller.GetArtist(global.DB, artistID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -60,7 +59,7 @@ func Handler() http.Handler { 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] - release, err := music.GetRelease(global.DB, releaseID, true) + release, err := controller.GetRelease(global.DB, releaseID, true) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -74,7 +73,7 @@ func Handler() http.Handler { switch r.Method { case http.MethodGet: // GET /api/v1/music/{id} - musicView.ServeRelease(release).ServeHTTP(w, r) + ServeRelease(release).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/music/{id} (admin) admin.MustAuthorise(UpdateRelease(release)).ServeHTTP(w, r) @@ -102,7 +101,7 @@ func Handler() http.Handler { 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] - track, err := music.GetTrack(global.DB, trackID) + track, err := controller.GetTrack(global.DB, trackID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) diff --git a/api/artist.go b/api/artist.go index 90bd18a..9c88bc1 100644 --- a/api/artist.go +++ b/api/artist.go @@ -12,15 +12,14 @@ import ( "arimelody-web/admin" "arimelody-web/global" - db "arimelody-web/music/controller" - music "arimelody-web/music/controller" - "arimelody-web/music/model" + "arimelody-web/controller" + "arimelody-web/model" ) func ServeAllArtists() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var artists = []*model.Artist{} - artists, err := db.GetAllArtists(global.DB) + artists, err := controller.GetAllArtists(global.DB) if err != nil { fmt.Printf("FATAL: Failed to serve all artists: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -55,7 +54,7 @@ func ServeArtist(artist *model.Artist) http.Handler { show_hidden_releases := admin.GetSession(r) != nil var dbCredits []*model.Credit - dbCredits, err := db.GetArtistCredits(global.DB, artist.ID, show_hidden_releases) + dbCredits, err := controller.GetArtistCredits(global.DB, artist.ID, show_hidden_releases) if err != nil { fmt.Printf("FATAL: Failed to retrieve artist credits for %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -100,7 +99,7 @@ func CreateArtist() http.Handler { } if artist.Name == "" { artist.Name = artist.ID } - err = music.CreateArtist(global.DB, &artist) + err = controller.CreateArtist(global.DB, &artist) if err != nil { if strings.Contains(err.Error(), "duplicate key") { http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest) @@ -148,7 +147,7 @@ func UpdateArtist(artist *model.Artist) http.Handler { } } - err = music.UpdateArtist(global.DB, artist) + err = controller.UpdateArtist(global.DB, artist) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -162,7 +161,7 @@ func UpdateArtist(artist *model.Artist) http.Handler { func DeleteArtist(artist *model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := music.DeleteArtist(global.DB, artist.ID) + err := controller.DeleteArtist(global.DB, artist.ID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) diff --git a/api/release.go b/api/release.go index 3027a8c..e13bc93 100644 --- a/api/release.go +++ b/api/release.go @@ -12,13 +12,109 @@ import ( "arimelody-web/admin" "arimelody-web/global" - music "arimelody-web/music/controller" - "arimelody-web/music/model" + "arimelody-web/controller" + "arimelody-web/model" ) +func ServeRelease(release *model.Release) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // only allow authorised users to view hidden releases + authorised := admin.GetSession(r) != nil + if !authorised && !release.Visible { + http.NotFound(w, r) + return + } + + type ( + Track struct { + Title string `json:"title"` + Description string `json:"description"` + Lyrics string `json:"lyrics"` + } + + 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"` + } + ) + + response := Release{ + Release: release, + Tracks: []Track{}, + Credits: []Credit{}, + Links: make(map[string]string), + } + + if authorised || release.IsReleased() { + // get credits + credits, err := controller.GetReleaseCredits(global.DB, release.ID) + if err != nil { + fmt.Printf("FATAL: Failed to serve release %s: Credits: %s\n", release.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + for _, credit := range credits { + artist, err := controller.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 := controller.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 := controller.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") + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + }) +} + func ServeCatalog() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - releases, err := music.GetAllReleases(global.DB, false, 0, true) + releases, err := controller.GetAllReleases(global.DB, false, 0, true) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -95,7 +191,7 @@ func CreateRelease() http.Handler { if release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" } - err = music.CreateRelease(global.DB, &release) + err = controller.CreateRelease(global.DB, &release) if err != nil { if strings.Contains(err.Error(), "duplicate key") { http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest) @@ -173,7 +269,7 @@ func UpdateRelease(release *model.Release) http.Handler { } } - err = music.UpdateRelease(global.DB, release) + err = controller.UpdateRelease(global.DB, release) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -194,7 +290,7 @@ func UpdateReleaseTracks(release *model.Release) http.Handler { return } - err = music.UpdateReleaseTracks(global.DB, release.ID, trackIDs) + err = controller.UpdateReleaseTracks(global.DB, release.ID, trackIDs) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -231,7 +327,7 @@ func UpdateReleaseCredits(release *model.Release) http.Handler { }) } - err = music.UpdateReleaseCredits(global.DB, release.ID, credits) + err = controller.UpdateReleaseCredits(global.DB, release.ID, credits) if err != nil { if strings.Contains(err.Error(), "duplicate key") { http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest) @@ -261,7 +357,7 @@ func UpdateReleaseLinks(release *model.Release) http.Handler { return } - err = music.UpdateReleaseLinks(global.DB, release.ID, links) + err = controller.UpdateReleaseLinks(global.DB, release.ID, links) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -275,7 +371,7 @@ func UpdateReleaseLinks(release *model.Release) http.Handler { func DeleteRelease(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := music.DeleteRelease(global.DB, release.ID) + err := controller.DeleteRelease(global.DB, release.ID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) diff --git a/api/track.go b/api/track.go index fd4018b..71a67e9 100644 --- a/api/track.go +++ b/api/track.go @@ -6,8 +6,8 @@ import ( "net/http" "arimelody-web/global" - music "arimelody-web/music/controller" - "arimelody-web/music/model" + "arimelody-web/controller" + "arimelody-web/model" ) type ( @@ -26,7 +26,7 @@ func ServeAllTracks() http.Handler { var tracks = []Track{} var dbTracks = []*model.Track{} - dbTracks, err := music.GetAllTracks(global.DB) + dbTracks, err := controller.GetAllTracks(global.DB) if err != nil { fmt.Printf("FATAL: Failed to pull tracks from DB: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -50,7 +50,7 @@ func ServeAllTracks() http.Handler { func ServeTrack(track *model.Track) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - dbReleases, err := music.GetTrackReleases(global.DB, track.ID, false) + dbReleases, err := controller.GetTrackReleases(global.DB, track.ID, false) if err != nil { 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) @@ -89,7 +89,7 @@ func CreateTrack() http.Handler { return } - id, err := music.CreateTrack(global.DB, &track) + id, err := controller.CreateTrack(global.DB, &track) if err != nil { fmt.Printf("FATAL: Failed to create track: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -120,7 +120,7 @@ func UpdateTrack(track *model.Track) http.Handler { return } - err = music.UpdateTrack(global.DB, track) + err = controller.UpdateTrack(global.DB, track) if err != nil { fmt.Printf("Failed to update track %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -143,7 +143,7 @@ func DeleteTrack(track *model.Track) http.Handler { } var trackID = r.URL.Path[1:] - err := music.DeleteTrack(global.DB, trackID) + err := controller.DeleteTrack(global.DB, trackID) if err != nil { fmt.Printf("Failed to delete track %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/music/controller/artist.go b/controller/artist.go similarity index 98% rename from music/controller/artist.go rename to controller/artist.go index 10824a8..c52b78d 100644 --- a/music/controller/artist.go +++ b/controller/artist.go @@ -1,7 +1,7 @@ -package music +package controller import ( - "arimelody-web/music/model" + "arimelody-web/model" "github.com/jmoiron/sqlx" ) diff --git a/music/controller/release.go b/controller/release.go similarity index 99% rename from music/controller/release.go rename to controller/release.go index 2aeb236..e55fef0 100644 --- a/music/controller/release.go +++ b/controller/release.go @@ -1,10 +1,10 @@ -package music +package controller import ( "errors" "fmt" - "arimelody-web/music/model" + "arimelody-web/model" "github.com/jmoiron/sqlx" ) diff --git a/music/controller/track.go b/controller/track.go similarity index 98% rename from music/controller/track.go rename to controller/track.go index 3187553..d302045 100644 --- a/music/controller/track.go +++ b/controller/track.go @@ -1,7 +1,7 @@ -package music +package controller import ( - "arimelody-web/music/model" + "arimelody-web/model" "github.com/jmoiron/sqlx" ) diff --git a/main.go b/main.go index c103696..f87d36e 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( "arimelody-web/admin" "arimelody-web/api" "arimelody-web/global" - musicView "arimelody-web/music/view" + "arimelody-web/view" "arimelody-web/templates" "github.com/jmoiron/sqlx" @@ -49,7 +49,7 @@ func createServeMux() *http.ServeMux { mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler())) mux.Handle("/api/", http.StripPrefix("/api", api.Handler())) - mux.Handle("/music/", http.StripPrefix("/music", musicView.Handler())) + mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler())) mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler("uploads"))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" || r.URL.Path == "/index.html" { diff --git a/music/model/artist.go b/model/artist.go similarity index 100% rename from music/model/artist.go rename to model/artist.go diff --git a/music/model/credit.go b/model/credit.go similarity index 100% rename from music/model/credit.go rename to model/credit.go diff --git a/music/model/link.go b/model/link.go similarity index 100% rename from music/model/link.go rename to model/link.go diff --git a/music/model/release.go b/model/release.go similarity index 100% rename from music/model/release.go rename to model/release.go diff --git a/music/model/track.go b/model/track.go similarity index 100% rename from music/model/track.go rename to model/track.go diff --git a/music/view/release.go b/music/view/release.go deleted file mode 100644 index fcb2b29..0000000 --- a/music/view/release.go +++ /dev/null @@ -1,136 +0,0 @@ -package view - -import ( - "encoding/json" - "fmt" - "net/http" - - "arimelody-web/admin" - "arimelody-web/global" - "arimelody-web/music/model" - db "arimelody-web/music/controller" - "arimelody-web/templates" -) - -type ( - Track struct { - Title string `json:"title"` - Description string `json:"description"` - Lyrics string `json:"lyrics"` - } - - 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) { - // only allow authorised users to view hidden releases - authorised := admin.GetSession(r) != nil - if !authorised && !release.Visible { - http.NotFound(w, r) - return - } - - response := Release{ - Release: release, - Tracks: []Track{}, - Credits: []Credit{}, - Links: make(map[string]string), - } - - if authorised || release.IsReleased() { - // get credits - credits, err := db.GetReleaseCredits(global.DB, release.ID) - if err != nil { - fmt.Printf("FATAL: Failed to serve release %s: Credits: %s\n", release.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - 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") - err := json.NewEncoder(w).Encode(response) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - }) -} - -func ServeGateway(release *model.Release) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // only allow authorised users to view hidden releases - authorised := admin.GetSession(r) != nil - if !authorised && !release.Visible { - http.NotFound(w, r) - return - } - - response := *release - - if authorised || release.IsReleased() { - response.Tracks = release.Tracks - response.Credits = release.Credits - response.Links = release.Links - } - - err := templates.Pages["music-gateway"].Execute(w, response) - - if err != nil { - fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - }) -} diff --git a/music/view/music.go b/view/music.go similarity index 51% rename from music/view/music.go rename to view/music.go index 6297a42..f38e1c2 100644 --- a/music/view/music.go +++ b/view/music.go @@ -4,15 +4,16 @@ import ( "fmt" "net/http" + "arimelody-web/admin" + "arimelody-web/controller" "arimelody-web/global" - music "arimelody-web/music/controller" - "arimelody-web/music/model" + "arimelody-web/model" "arimelody-web/templates" ) // HTTP HANDLER METHODS -func Handler() http.Handler { +func MusicHandler() http.Handler { mux := http.NewServeMux() mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -21,7 +22,7 @@ func Handler() http.Handler { return } - release, err := music.GetRelease(global.DB, r.URL.Path[1:], true) + release, err := controller.GetRelease(global.DB, r.URL.Path[1:], true) if err != nil { http.NotFound(w, r) return @@ -35,7 +36,7 @@ func Handler() http.Handler { func ServeCatalog() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - releases, err := music.GetAllReleases(global.DB, true, 0, true) + releases, err := controller.GetAllReleases(global.DB, true, 0, true) if err != nil { fmt.Printf("FATAL: Failed to pull releases for catalog: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -54,3 +55,30 @@ func ServeCatalog() http.Handler { } }) } + +func ServeGateway(release *model.Release) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // only allow authorised users to view hidden releases + authorised := admin.GetSession(r) != nil + if !authorised && !release.Visible { + http.NotFound(w, r) + return + } + + response := *release + + if authorised || release.IsReleased() { + response.Tracks = release.Tracks + response.Credits = release.Credits + response.Links = release.Links + } + + err := templates.Pages["music-gateway"].Execute(w, response) + + if err != nil { + fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + }) +} From 34dd280fbac48e81969aa31684e02f928ae0efeb Mon Sep 17 00:00:00 2001 From: ari melody Date: Fri, 1 Nov 2024 21:03:08 +0000 Subject: [PATCH 05/24] moved accounts to MVC directories --- api/account.go | 4 ++-- {account/controller => controller}/account.go | 13 ++++++++++++- main.go | 4 ++-- {account/model => model}/account.go | 11 ----------- 4 files changed, 16 insertions(+), 16 deletions(-) rename {account/controller => controller}/account.go (80%) rename {account/model => model}/account.go (80%) diff --git a/api/account.go b/api/account.go index 37cf9a1..37c4c7f 100644 --- a/api/account.go +++ b/api/account.go @@ -1,8 +1,8 @@ package api import ( - "arimelody-web/account/controller" - "arimelody-web/account/model" + "arimelody-web/controller" + "arimelody-web/model" "arimelody-web/global" "encoding/json" "fmt" diff --git a/account/controller/account.go b/controller/account.go similarity index 80% rename from account/controller/account.go rename to controller/account.go index 267c43b..a34cd27 100644 --- a/account/controller/account.go +++ b/controller/account.go @@ -1,7 +1,8 @@ package controller import ( - "arimelody-web/account/model" + "arimelody-web/model" + "math/rand" "github.com/jmoiron/sqlx" ) @@ -58,3 +59,13 @@ func DeleteAccount(db *sqlx.DB, accountID string) error { _, err := db.Exec("DELETE FROM account WHERE id=$1", accountID) return err } + +var inviteChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + +func GenerateInviteCode(length int) []byte { + code := []byte{} + for i := 0; i < length; i++ { + code = append(code, inviteChars[rand.Intn(len(inviteChars) - 1)]) + } + return code +} diff --git a/main.go b/main.go index ad6db3c..552a83d 100644 --- a/main.go +++ b/main.go @@ -9,11 +9,11 @@ import ( "path/filepath" "time" - "arimelody-web/account/model" "arimelody-web/admin" "arimelody-web/api" "arimelody-web/global" "arimelody-web/view" + "arimelody-web/controller" "arimelody-web/templates" "github.com/jmoiron/sqlx" @@ -47,7 +47,7 @@ func main() { accountsCount := 0 global.DB.Get(&accountsCount, "SELECT count(*) FROM account") if accountsCount == 0 { - code := model.GenerateInviteCode(8) + code := controller.GenerateInviteCode(8) tx, err := global.DB.Begin() if err != nil { diff --git a/account/model/account.go b/model/account.go similarity index 80% rename from account/model/account.go rename to model/account.go index db32451..16b0e3b 100644 --- a/account/model/account.go +++ b/model/account.go @@ -1,7 +1,6 @@ package model import ( - "math/rand" "time" ) @@ -42,13 +41,3 @@ const ( ReadArtists AccountPrivilege = "read_artists" EditArtists AccountPrivilege = "edit_artists" ) - -var inviteChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - -func GenerateInviteCode(length int) []byte { - code := []byte{} - for i := 0; i < length; i++ { - code = append(code, inviteChars[rand.Intn(len(inviteChars) - 1)]) - } - return code -} From ea3a386601c102b685c19364d839dc811bf11c37 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 9 Nov 2024 22:58:35 +0000 Subject: [PATCH 06/24] update docker compose: restart unless stopped --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index e29b006..69f9247 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,9 @@ services: DISCORD_ADMIN: # your discord user ID. DISCORD_CLIENT: # your discord OAuth client ID. DISCORD_SECRET: # your discord OAuth secret. + depends_on: + - db + restart: unless-stopped db: image: postgres:16.1-alpine3.18 volumes: @@ -20,3 +23,4 @@ services: POSTGRES_DB: arimelody POSTGRES_USER: arimelody POSTGRES_PASSWORD: fuckingpassword + restart: unless-stopped From 1667921f5b552eaf8efa508e4d1a5e7a25f6e153 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 9 Nov 2024 23:36:18 +0000 Subject: [PATCH 07/24] move DB credentials to environment variables --- README.md | 6 +++++- docker-compose.yml | 6 +++++- global/data.go | 2 +- main.go | 32 +++++++++++++++++++++++++++----- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7ac1702..64266ec 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,11 @@ easy! just `git clone` this repo and `go build` from the root. `arimelody-web(.e 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`) +- `ARIMELODY_HTTP_DOMAIN`: the domain the webserver will use for generating oauth redirect URIs (default `https://arimelody.me`) +- `ARIMELODY_DB_HOST`: the host address of a postgres database. +- `ARIMELODY_DB_NAME`: the name of the database. +- `ARIMELODY_DB_USER`: the username for the database. +- `ARIMELODY_DB_PASS`: the password for the database. - `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. diff --git a/docker-compose.yml b/docker-compose.yml index e29b006..3f3e4a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,12 @@ services: volumes: - ./uploads:/app/uploads environment: - HTTP_DOMAIN: "https://arimelody.me" + ARIMELODY_PORT: 8080 + ARIMELODY_HTTP_DOMAIN: "https://arimelody.me" ARIMELODY_DB_HOST: db + ARIMELODY_DB_NAME: arimelody + ARIMELODY_DB_USER: arimelody + ARIMELODY_DB_PASS: fuckingpassword DISCORD_ADMIN: # your discord user ID. DISCORD_CLIENT: # your discord OAuth client ID. DISCORD_SECRET: # your discord OAuth secret. diff --git a/global/data.go b/global/data.go index fb641fc..4f56a68 100644 --- a/global/data.go +++ b/global/data.go @@ -35,7 +35,7 @@ var Args = func() map[string]string { }() var HTTP_DOMAIN = func() string { - domain := os.Getenv("HTTP_DOMAIN") + domain := os.Getenv("ARIMELODY_HTTP_DOMAIN") if domain == "" { return "https://arimelody.me" } diff --git a/main.go b/main.go index f87d36e..a783421 100644 --- a/main.go +++ b/main.go @@ -7,27 +7,46 @@ import ( "net/http" "os" "path/filepath" + "strconv" "time" "arimelody-web/admin" "arimelody-web/api" "arimelody-web/global" - "arimelody-web/view" "arimelody-web/templates" + "arimelody-web/view" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) -const DEFAULT_PORT int = 8080 +const DEFAULT_PORT int64 = 8080 func main() { // initialise database connection var dbHost = os.Getenv("ARIMELODY_DB_HOST") - if dbHost == "" { dbHost = "127.0.0.1" } + var dbName = os.Getenv("ARIMELODY_DB_NAME") + var dbUser = os.Getenv("ARIMELODY_DB_USER") + var dbPass = os.Getenv("ARIMELODY_DB_PASS") + if dbHost == "" { + fmt.Fprintf(os.Stderr, "FATAL: ARIMELODY_DB_HOST not provided! Exiting...\n") + os.Exit(1) + } + if dbName == "" { + fmt.Fprintf(os.Stderr, "FATAL: ARIMELODY_DB_NAME not provided! Exiting...\n") + os.Exit(1) + } + if dbUser == "" { + fmt.Fprintf(os.Stderr, "FATAL: ARIMELODY_DB_USER not provided! Exiting...\n") + os.Exit(1) + } + if dbPass == "" { + fmt.Fprintf(os.Stderr, "FATAL: ARIMELODY_DB_PASS not provided! Exiting...\n") + os.Exit(1) + } var err error - global.DB, err = sqlx.Connect("postgres", "host=" + dbHost + " user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable") + global.DB, err = sqlx.Connect("postgres", fmt.Sprintf("host=%s user=%s dbname=%s password=%s sslmode=disable", dbHost, dbUser, dbName, dbPass)) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: Unable to create database connection pool: %v\n", err) os.Exit(1) @@ -39,7 +58,10 @@ func main() { // start the web server! mux := createServeMux() - port := DEFAULT_PORT + port, err := strconv.ParseInt(os.Getenv("ARIMELODY_PORT"), 10, 0) + if err != nil { + port = DEFAULT_PORT + } fmt.Printf("Now serving at http://127.0.0.1:%d\n", port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), global.HTTPLog(mux))) } From d0b392f6a0ce4e683b1e51f33d0a3533565f9d55 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 10 Nov 2024 00:18:52 +0000 Subject: [PATCH 08/24] update schema init script --- schema.sql | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/schema.sql b/schema.sql index 746511a..b2d48ee 100644 --- a/schema.sql +++ b/schema.sql @@ -1,18 +1,18 @@ -- -- Artists (should be applicable to all art) -- -CREATE TABLE public.artist ( +CREATE TABLE artist ( id character varying(64), name text NOT NULL, website text, avatar text ); -ALTER TABLE public.artist ADD CONSTRAINT artist_pk PRIMARY KEY (id); +ALTER TABLE artist ADD CONSTRAINT artist_pk PRIMARY KEY (id); -- -- Music releases -- -CREATE TABLE public.musicrelease ( +CREATE TABLE musicrelease ( id character varying(64) NOT NULL, visible bool DEFAULT false, title text NOT NULL, @@ -25,56 +25,56 @@ CREATE TABLE public.musicrelease ( copyright text, copyrightURL text ); -ALTER TABLE public.musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); +ALTER TABLE musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); -- -- Music links (external platform links under a release) -- -CREATE TABLE public.musiclink ( +CREATE TABLE musiclink ( release character varying(64) NOT NULL, name text NOT NULL, url text NOT NULL ); -ALTER TABLE public.musiclink ADD CONSTRAINT musiclink_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 public.musiccredit ( +CREATE TABLE musiccredit ( release character varying(64) NOT NULL, artist character varying(64) NOT NULL, role text NOT NULL, is_primary boolean DEFAULT false ); -ALTER TABLE public.musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist); +ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist); -- -- Music tracks (tracks under a release) -- -CREATE TABLE public.musictrack ( +CREATE TABLE musictrack ( id uuid DEFAULT gen_random_uuid(), title text NOT NULL, description text, lyrics text, preview_url text ); -ALTER TABLE public.musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id); +ALTER TABLE musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id); -- -- Music release/track pairs -- -CREATE TABLE public.musicreleasetrack ( +CREATE TABLE musicreleasetrack ( release character varying(64) NOT NULL, track uuid NOT NULL, number integer NOT NULL ); -ALTER TABLE public.musicreleasetrack ADD CONSTRAINT musicreleasetrack_pk PRIMARY KEY (release, track); +ALTER TABLE musicreleasetrack ADD CONSTRAINT musicreleasetrack_pk PRIMARY KEY (release, track); -- -- Foreign keys -- -ALTER TABLE public.musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES public.artist(id) ON DELETE CASCADE ON UPDATE CASCADE; -ALTER TABLE public.musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES public.musicrelease(id) ON DELETE CASCADE; -ALTER TABLE public.musiclink ADD CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES public.musicrelease(id) ON UPDATE CASCADE ON DELETE CASCADE; -ALTER TABLE public.musicreleasetrack ADD CONSTRAINT music_pair_trackref_fk FOREIGN KEY (release) REFERENCES public.musicrelease(id) ON DELETE CASCADE; -ALTER TABLE public.musicreleasetrack ADD CONSTRAINT music_pair_releaseref_fk FOREIGN KEY (track) REFERENCES public.musictrack(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; From 5284b8a7cc03f3a05480d43ed6e35f613b47793f Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 10 Nov 2024 00:37:01 +0000 Subject: [PATCH 09/24] dynamic data directory --- api/uploads.go | 2 ++ global/data.go | 22 +++++++++++++++++++++- main.go | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/api/uploads.go b/api/uploads.go index f2fe297..6e348d0 100644 --- a/api/uploads.go +++ b/api/uploads.go @@ -1,6 +1,7 @@ package api import ( + "arimelody-web/global" "bufio" "encoding/base64" "errors" @@ -15,6 +16,7 @@ func HandleImageUpload(data *string, directory string, filename string) (string, header := split[0] imageData, err := base64.StdEncoding.DecodeString(split[1]) ext, _ := strings.CutPrefix(header, "data:image/") + directory = filepath.Join(global.DATA_DIR, directory) switch ext { case "png": diff --git a/global/data.go b/global/data.go index 4f56a68..f88d25f 100644 --- a/global/data.go +++ b/global/data.go @@ -3,6 +3,7 @@ package global import ( "fmt" "os" + "path/filepath" "strings" "github.com/jmoiron/sqlx" @@ -34,7 +35,7 @@ var Args = func() map[string]string { return args }() -var HTTP_DOMAIN = func() string { +var HTTP_DOMAIN = func() string { domain := os.Getenv("ARIMELODY_HTTP_DOMAIN") if domain == "" { return "https://arimelody.me" @@ -42,4 +43,23 @@ var HTTP_DOMAIN = func() string { return domain }() +var DATA_DIR = func() string { + dir, err := filepath.Abs(os.Getenv("ARIMELODY_DATA_DIR")) + if err != nil { + fmt.Printf("FATAL: Failed to get working directory: %s\n", err.Error()) + os.Exit(1) + } + if dir != "" { + os.MkdirAll(dir, os.ModePerm) + } else { + var err error + dir, err = os.Getwd() + if err != nil { + fmt.Printf("FATAL: Failed to get working directory: %s\n", err.Error()) + os.Exit(1) + } + } + return dir +}() + var DB *sqlx.DB diff --git a/main.go b/main.go index a783421..901dcbb 100644 --- a/main.go +++ b/main.go @@ -72,7 +72,7 @@ func createServeMux() *http.ServeMux { mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler())) mux.Handle("/api/", http.StripPrefix("/api", api.Handler())) mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler())) - mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler("uploads"))) + mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(global.DATA_DIR, "uploads")))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" || r.URL.Path == "/index.html" { err := templates.Pages["index"].Execute(w, nil) From 04f7f97b627127fb12036018302bad497d2eb3b6 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 10 Nov 2024 05:34:04 +0000 Subject: [PATCH 10/24] migrate from envars to toml config --- .air.toml | 2 +- .gitignore | 1 + README.md | 37 ++++++++------ admin/admin.go | 5 +- admin/http.go | 6 +-- api/uploads.go | 2 +- discord/discord.go | 11 ++--- global/config.go | 121 +++++++++++++++++++++++++++++++++++++++++++++ global/data.go | 65 ------------------------ go.mod | 2 + go.sum | 2 + main.go | 37 ++++++++------ 12 files changed, 182 insertions(+), 109 deletions(-) create mode 100644 global/config.go delete mode 100644 global/data.go diff --git a/.air.toml b/.air.toml index f8a1acf..070166a 100644 --- a/.air.toml +++ b/.air.toml @@ -7,7 +7,7 @@ tmp_dir = "tmp" bin = "./tmp/main" cmd = "go build -o ./tmp/main ." delay = 1000 - exclude_dir = ["admin/static", "public", "uploads", "test", "db"] + exclude_dir = ["admin/static", "admin\\static", "public", "uploads", "test", "db", "res"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false diff --git a/.gitignore b/.gitignore index 781b36e..c179122 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ tmp/ test/ uploads/ docker-compose-test.yml +config*.toml diff --git a/README.md b/README.md index 64266ec..af88c65 100644 --- a/README.md +++ b/README.md @@ -4,29 +4,36 @@ 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! +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 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! +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. +- `git clone` this repo, and `cd` into it. +- `go build -o arimelody-web .` ## running -the webserver depends on some environment variables (don't worry about forgetting some; it'll be sure to bug you about them): +the server should be run once to generate a default `config.toml` file. +configure as needed. note that a valid DB connection is required, and the admin +panel will be disabled without valid discord app credentials (this can however +be bypassed by running the server with `-adminBypass`). -- `ARIMELODY_HTTP_DOMAIN`: the domain the webserver will use for generating oauth redirect URIs (default `https://arimelody.me`) -- `ARIMELODY_DB_HOST`: the host address of a postgres database. -- `ARIMELODY_DB_NAME`: the name of the database. -- `ARIMELODY_DB_USER`: the username for the database. -- `ARIMELODY_DB_PASS`: the password for the database. -- `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. +the configuration may be overridden using environment variables in the format +`ARIMELODY_

_`. for example, `db.host` in the config may be +overridden with `ARIMELODY_DB_HOST`. -[^1]: not required, but the admin panel will be **disabled** if these are not provided. +the location of the configuration file can also be overridden with +`ARIMELODY_CONFIG`. -the webserver requires a database to run. in this case, postgres. +## database -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 +the server requires a postgres database to run. you can use the +[schema.sql](schema.sql) provided in this repo to generate the required tables. +automatic schema building/migration may come in a future update. diff --git a/admin/admin.go b/admin/admin.go index 3c2aaae..4c07fa0 100644 --- a/admin/admin.go +++ b/admin/admin.go @@ -29,9 +29,10 @@ var ADMIN_BYPASS = func() bool { }() var ADMIN_ID_DISCORD = func() string { - id := os.Getenv("DISCORD_ADMIN") + id := os.Getenv("DISCORD_ADMIN_ID") + if id == "" { id = global.Config.Discord.AdminID } if id == "" { - fmt.Printf("WARN: Discord admin ID (DISCORD_ADMIN) was not provided. Admin login will be unavailable.\n") + fmt.Printf("WARN: discord.admin_id not provided. Admin login will be unavailable.\n") } return id }() diff --git a/admin/http.go b/admin/http.go index da3539a..19afa92 100644 --- a/admin/http.go +++ b/admin/http.go @@ -154,10 +154,6 @@ func LoginHandler() http.Handler { return } - fmt.Println(discord.CLIENT_ID) - fmt.Println(discord.API_ENDPOINT) - fmt.Println(discord.REDIRECT_URI) - code := r.URL.Query().Get("code") if code == "" { @@ -194,7 +190,7 @@ func LoginHandler() http.Handler { cookie.Name = "token" cookie.Value = session.Token cookie.Expires = time.Now().Add(24 * time.Hour) - if strings.HasPrefix(global.HTTP_DOMAIN, "https") { + if strings.HasPrefix(global.Config.BaseUrl, "https") { cookie.Secure = true } cookie.HttpOnly = true diff --git a/api/uploads.go b/api/uploads.go index 6e348d0..6b1c496 100644 --- a/api/uploads.go +++ b/api/uploads.go @@ -16,7 +16,7 @@ func HandleImageUpload(data *string, directory string, filename string) (string, header := split[0] imageData, err := base64.StdEncoding.DecodeString(split[1]) ext, _ := strings.CutPrefix(header, "data:image/") - directory = filepath.Join(global.DATA_DIR, directory) + directory = filepath.Join(global.Config.DataDirectory, directory) switch ext { case "png": diff --git a/discord/discord.go b/discord/discord.go index c3b3cec..27023e7 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "net/url" - "os" "strings" "arimelody-web/global" @@ -16,22 +15,22 @@ const API_ENDPOINT = "https://discord.com/api/v10" var CREDENTIALS_PROVIDED = true var CLIENT_ID = func() string { - id := os.Getenv("DISCORD_CLIENT") + id := global.Config.Discord.ClientID if id == "" { - fmt.Printf("WARN: Discord client ID (DISCORD_CLIENT) was not provided. Admin login will be unavailable.\n") + fmt.Printf("WARN: discord.client_id was not provided. Admin login will be unavailable.\n") CREDENTIALS_PROVIDED = false } return id }() var CLIENT_SECRET = func() string { - secret := os.Getenv("DISCORD_SECRET") + secret := global.Config.Discord.Secret if secret == "" { - fmt.Printf("WARN: Discord secret (DISCORD_SECRET) was not provided. Admin login will be unavailable.\n") + fmt.Printf("WARN: discord.secret not provided. Admin login will be unavailable.\n") CREDENTIALS_PROVIDED = false } return secret }() -var OAUTH_CALLBACK_URI = fmt.Sprintf("%s/admin/login", global.HTTP_DOMAIN) +var OAUTH_CALLBACK_URI = fmt.Sprintf("%s/admin/login", global.Config.BaseUrl) var REDIRECT_URI = fmt.Sprintf("https://discord.com/oauth2/authorize?client_id=%s&response_type=code&redirect_uri=%s&scope=identify", CLIENT_ID, OAUTH_CALLBACK_URI) type ( diff --git a/global/config.go b/global/config.go new file mode 100644 index 0000000..d8c9f3d --- /dev/null +++ b/global/config.go @@ -0,0 +1,121 @@ +package global + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" + + "github.com/jmoiron/sqlx" + "github.com/pelletier/go-toml/v2" +) + +type ( + dbConfig struct { + Host string `toml:"host"` + Name string `toml:"name"` + User string `toml:"user"` + Pass string `toml:"pass"` + } + + discordConfig struct { + AdminID string `toml:"admin_id" comment:"NOTE: admin_id to be deprecated in favour of local accounts and SSO."` + ClientID string `toml:"client_id"` + Secret string `toml:"secret"` + } + + config struct { + BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."` + Port int64 `toml:"port"` + DataDirectory string `toml:"data_dir"` + DB dbConfig `toml:"db"` + Discord discordConfig `toml:"discord"` + } +) + +var Config = func() config { + configFile := os.Getenv("ARIMELODY_CONFIG") + if configFile == "" { + configFile = "config.toml" + } + + config := config{ + BaseUrl: "https://arimelody.me", + Port: 8080, + } + + data, err := os.ReadFile(configFile) + if err != nil { + configOut, _ := toml.Marshal(&config) + os.WriteFile(configFile, configOut, os.ModePerm) + fmt.Printf( + "A default config.toml has been created. " + + "Please configure before running again!\n") + os.Exit(0) + } + + err = toml.Unmarshal([]byte(data), &config) + if err != nil { + fmt.Fprintf(os.Stderr, "FATAL: Failed to parse configuration file: %s\n", err.Error()) + os.Exit(1) + } + + err = handleConfigOverrides(&config) + if err != nil { + fmt.Fprintf(os.Stderr, "FATAL: Failed to parse environment variable %s\n", err.Error()) + os.Exit(1) + } + + return config +}() + +func handleConfigOverrides(config *config) error { + var err error + + if env, has := os.LookupEnv("ARIMELODY_BASE_URL"); has { config.BaseUrl = env } + if env, has := os.LookupEnv("ARIMELODY_PORT"); has { + config.Port, err = strconv.ParseInt(env, 10, 0) + if err != nil { return errors.New("ARIMELODY_PORT: " + err.Error()) } + } + if env, has := os.LookupEnv("ARIMELODY_DATA_DIR"); has { config.DataDirectory = env } + + if env, has := os.LookupEnv("ARIMELODY_DB_HOST"); has { config.DB.Host = env } + if env, has := os.LookupEnv("ARIMELODY_DB_NAME"); has { config.DB.Name = env } + if env, has := os.LookupEnv("ARIMELODY_DB_USER"); has { config.DB.User = env } + if env, has := os.LookupEnv("ARIMELODY_DB_PASS"); has { config.DB.Pass = env } + + if env, has := os.LookupEnv("ARIMELODY_DISCORD_ADMIN_ID"); has { config.Discord.AdminID = env } + if env, has := os.LookupEnv("ARIMELODY_DISCORD_CLIENT_ID"); has { config.Discord.ClientID = env } + if env, has := os.LookupEnv("ARIMELODY_DISCORD_SECRET"); has { config.Discord.Secret = env } + + return nil +} + +var Args = func() map[string]string { + args := map[string]string{} + + index := 0 + for index < len(os.Args[1:]) { + arg := os.Args[index + 1] + if !strings.HasPrefix(arg, "-") { + fmt.Printf("FATAL: Parameters must follow an argument (%s).\n", arg) + os.Exit(1) + } + + if index + 3 > len(os.Args) || strings.HasPrefix(os.Args[index + 2], "-") { + args[arg[1:]] = "true" + index += 1 + continue + } + + val := os.Args[index + 2] + args[arg[1:]] = val + // fmt.Printf("%s: %s\n", arg[1:], val) + index += 2 + } + + return args +}() + +var DB *sqlx.DB diff --git a/global/data.go b/global/data.go deleted file mode 100644 index f88d25f..0000000 --- a/global/data.go +++ /dev/null @@ -1,65 +0,0 @@ -package global - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/jmoiron/sqlx" -) - -var Args = func() map[string]string { - args := map[string]string{} - - index := 0 - for index < len(os.Args[1:]) { - arg := os.Args[index + 1] - if !strings.HasPrefix(arg, "-") { - fmt.Printf("FATAL: Parameters must follow an argument (%s).\n", arg) - os.Exit(1) - } - - if index + 3 > len(os.Args) || strings.HasPrefix(os.Args[index + 2], "-") { - args[arg[1:]] = "true" - index += 1 - continue - } - - val := os.Args[index + 2] - args[arg[1:]] = val - // fmt.Printf("%s: %s\n", arg[1:], val) - index += 2 - } - - return args -}() - -var HTTP_DOMAIN = func() string { - domain := os.Getenv("ARIMELODY_HTTP_DOMAIN") - if domain == "" { - return "https://arimelody.me" - } - return domain -}() - -var DATA_DIR = func() string { - dir, err := filepath.Abs(os.Getenv("ARIMELODY_DATA_DIR")) - if err != nil { - fmt.Printf("FATAL: Failed to get working directory: %s\n", err.Error()) - os.Exit(1) - } - if dir != "" { - os.MkdirAll(dir, os.ModePerm) - } else { - var err error - dir, err = os.Getwd() - if err != nil { - fmt.Printf("FATAL: Failed to get working directory: %s\n", err.Error()) - os.Exit(1) - } - } - return dir -}() - -var DB *sqlx.DB diff --git a/go.mod b/go.mod index e2beaf4..8d116d4 100644 --- a/go.mod +++ b/go.mod @@ -6,3 +6,5 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 ) + +require github.com/pelletier/go-toml/v2 v2.2.3 // indirect diff --git a/go.sum b/go.sum index f4ce337..37a12d4 100644 --- a/go.sum +++ b/go.sum @@ -8,3 +8,5 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= diff --git a/main.go b/main.go index 901dcbb..4e27f4c 100644 --- a/main.go +++ b/main.go @@ -24,29 +24,38 @@ const DEFAULT_PORT int64 = 8080 func main() { // initialise database connection - var dbHost = os.Getenv("ARIMELODY_DB_HOST") - var dbName = os.Getenv("ARIMELODY_DB_NAME") - var dbUser = os.Getenv("ARIMELODY_DB_USER") - var dbPass = os.Getenv("ARIMELODY_DB_PASS") - if dbHost == "" { - fmt.Fprintf(os.Stderr, "FATAL: ARIMELODY_DB_HOST not provided! Exiting...\n") + if env := os.Getenv("ARIMELODY_DB_HOST"); env != "" { global.Config.DB.Host = env } + if env := os.Getenv("ARIMELODY_DB_NAME"); env != "" { global.Config.DB.Name = env } + if env := os.Getenv("ARIMELODY_DB_USER"); env != "" { global.Config.DB.User = env } + if env := os.Getenv("ARIMELODY_DB_PASS"); env != "" { global.Config.DB.Pass = env } + if global.Config.DB.Host == "" { + fmt.Fprintf(os.Stderr, "FATAL: db.host not provided! Exiting...\n") os.Exit(1) } - if dbName == "" { - fmt.Fprintf(os.Stderr, "FATAL: ARIMELODY_DB_NAME not provided! Exiting...\n") + if global.Config.DB.Name == "" { + fmt.Fprintf(os.Stderr, "FATAL: db.name not provided! Exiting...\n") os.Exit(1) } - if dbUser == "" { - fmt.Fprintf(os.Stderr, "FATAL: ARIMELODY_DB_USER not provided! Exiting...\n") + if global.Config.DB.User == "" { + fmt.Fprintf(os.Stderr, "FATAL: db.user not provided! Exiting...\n") os.Exit(1) } - if dbPass == "" { - fmt.Fprintf(os.Stderr, "FATAL: ARIMELODY_DB_PASS not provided! Exiting...\n") + if global.Config.DB.Pass == "" { + fmt.Fprintf(os.Stderr, "FATAL: db.pass not provided! Exiting...\n") os.Exit(1) } var err error - global.DB, err = sqlx.Connect("postgres", fmt.Sprintf("host=%s user=%s dbname=%s password=%s sslmode=disable", dbHost, dbUser, dbName, dbPass)) + global.DB, err = sqlx.Connect( + "postgres", + fmt.Sprintf( + "host=%s user=%s dbname=%s password='%s' sslmode=disable", + global.Config.DB.Host, + global.Config.DB.User, + global.Config.DB.Name, + global.Config.DB.Pass, + ), + ) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: Unable to create database connection pool: %v\n", err) os.Exit(1) @@ -72,7 +81,7 @@ func createServeMux() *http.ServeMux { mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler())) mux.Handle("/api/", http.StripPrefix("/api", api.Handler())) mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler())) - mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(global.DATA_DIR, "uploads")))) + mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(global.Config.DataDirectory, "uploads")))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" || r.URL.Path == "/index.html" { err := templates.Pages["index"].Execute(w, nil) From bace6e7fa4e514b3802921a8da37192b90fde83f Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 10 Nov 2024 05:41:03 +0000 Subject: [PATCH 11/24] respect config port --- main.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 4e27f4c..8e8839a 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,6 @@ import ( "net/http" "os" "path/filepath" - "strconv" "time" "arimelody-web/admin" @@ -67,12 +66,8 @@ func main() { // start the web server! mux := createServeMux() - port, err := strconv.ParseInt(os.Getenv("ARIMELODY_PORT"), 10, 0) - if err != nil { - port = DEFAULT_PORT - } - fmt.Printf("Now serving at http://127.0.0.1:%d\n", port) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), global.HTTPLog(mux))) + fmt.Printf("Now serving at http://127.0.0.1:%d\n", global.Config.Port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", global.Config.Port), global.HTTPLog(mux))) } func createServeMux() *http.ServeMux { From de508582801de669d0c263eacbb2f9214740ae31 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 10 Nov 2024 05:44:45 +0000 Subject: [PATCH 12/24] accept HEAD on / (200) --- main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/main.go b/main.go index 8e8839a..5f2f966 100644 --- a/main.go +++ b/main.go @@ -78,6 +78,11 @@ func createServeMux() *http.ServeMux { mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler())) mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(global.Config.DataDirectory, "uploads")))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodHead { + w.WriteHeader(http.StatusOK) + return + } + if r.URL.Path == "/" || r.URL.Path == "/index.html" { err := templates.Pages["index"].Execute(w, nil) if err != nil { From d3b55f2b3cfe2f09efbce5747ade8c532d13a4f1 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 10 Nov 2024 05:49:00 +0000 Subject: [PATCH 13/24] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index af88c65..df0c351 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ panel will be disabled without valid discord app credentials (this can however be bypassed by running the server with `-adminBypass`). the configuration may be overridden using environment variables in the format -`ARIMELODY_
_`. for example, `db.host` in the config may be -overridden with `ARIMELODY_DB_HOST`. +`ARIMELODY__`. for example, `db.host` in the config may +be overridden with `ARIMELODY_DB_HOST`. the location of the configuration file can also be overridden with `ARIMELODY_CONFIG`. From bb92ba114cc00211471cf9440597f00a3a52c40a Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 10 Nov 2024 05:58:05 +0000 Subject: [PATCH 14/24] update docker compose example --- .gitignore | 3 ++- docker-compose.example.yml | 23 +++++++++++++++++++++++ docker-compose.yml | 26 -------------------------- 3 files changed, 25 insertions(+), 27 deletions(-) create mode 100644 docker-compose.example.yml delete mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore index c179122..cccde2b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ db/ tmp/ test/ uploads/ -docker-compose-test.yml +docker-compose*.yml +!docker-compose.example.yml config*.toml diff --git a/docker-compose.example.yml b/docker-compose.example.yml new file mode 100644 index 0000000..7833201 --- /dev/null +++ b/docker-compose.example.yml @@ -0,0 +1,23 @@ +services: + web: + image: docker.arimelody.me/arimelody.me:latest + build: . + ports: + - 8080:8080 + volumes: + - ./uploads:/app/uploads + - ./config.toml:/app/config.toml + environment: + ARIMELODY_CONFIG: config.toml + db: + image: postgres:16.1-alpine3.18 + volumes: + - arimelody-db:/var/lib/postgresql/data + environment: + POSTGRES_DB: # your database name here! + POSTGRES_USER: # your database user here! + POSTGRES_PASSWORD: # your database password here! + +volumes: + arimelody-db: + external: true diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 3f3e4a4..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,26 +0,0 @@ -services: - web: - image: docker.arimelody.me/arimelody.me:latest - build: . - ports: - - 8080:8080 - volumes: - - ./uploads:/app/uploads - environment: - ARIMELODY_PORT: 8080 - ARIMELODY_HTTP_DOMAIN: "https://arimelody.me" - ARIMELODY_DB_HOST: db - ARIMELODY_DB_NAME: arimelody - ARIMELODY_DB_USER: arimelody - ARIMELODY_DB_PASS: fuckingpassword - 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 From fdfc6b8c3e73cf6b9373433c79218a091138a568 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 18 Nov 2024 05:06:01 +0000 Subject: [PATCH 15/24] add bundler script --- bundle.sh | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100755 bundle.sh diff --git a/bundle.sh b/bundle.sh new file mode 100755 index 0000000..4e7068b --- /dev/null +++ b/bundle.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# simple script to pack up arimelody.me for production distribution + +if [ ! -f arimelody-web ]; then + echo "[FATAL] ./arimelody-web not found! please run \`go build -o arimelody-web\` first." + exit 1 +fi + +tar czvf arimelody-web.tar.gz arimelody-web admin/components/ admin/views/ admin/static/ views/ public/ From ff6d157e6bbc0009e1cb4a5d2f6b611ff5076760 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 2 Dec 2024 12:47:42 +0000 Subject: [PATCH 16/24] pretty-print API json responses --- api/artist.go | 8 ++++++-- api/release.go | 12 +++++++++--- api/track.go | 12 +++++++++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/api/artist.go b/api/artist.go index 9c88bc1..c793e23 100644 --- a/api/artist.go +++ b/api/artist.go @@ -27,7 +27,9 @@ func ServeAllArtists() http.Handler { } w.Header().Add("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(artists) + encoder := json.NewEncoder(w) + encoder.SetIndent("", "\t") + err = encoder.Encode(artists) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } @@ -74,7 +76,9 @@ func ServeArtist(artist *model.Artist) http.Handler { } w.Header().Add("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(artistJSON{ + encoder := json.NewEncoder(w) + encoder.SetIndent("", "\t") + err = encoder.Encode(artistJSON{ Artist: artist, Credits: credits, }) diff --git a/api/release.go b/api/release.go index e13bc93..2288153 100644 --- a/api/release.go +++ b/api/release.go @@ -104,7 +104,9 @@ func ServeRelease(release *model.Release) http.Handler { } w.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(response) + encoder := json.NewEncoder(w) + encoder.SetIndent("", "\t") + err := encoder.Encode(response) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -155,7 +157,9 @@ func ServeCatalog() http.Handler { } w.Header().Add("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(catalog) + encoder := json.NewEncoder(w) + encoder.SetIndent("", "\t") + err = encoder.Encode(catalog) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -204,7 +208,9 @@ func CreateRelease() http.Handler { w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - err = json.NewEncoder(w).Encode(release) + encoder := json.NewEncoder(w) + encoder.SetIndent("", "\t") + err = encoder.Encode(release) if err != nil { fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/api/track.go b/api/track.go index 71a67e9..f6d5578 100644 --- a/api/track.go +++ b/api/track.go @@ -40,7 +40,9 @@ func ServeAllTracks() http.Handler { } w.Header().Add("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(tracks) + encoder := json.NewEncoder(w) + encoder.SetIndent("", "\t") + err = encoder.Encode(tracks) if err != nil { fmt.Printf("FATAL: Failed to serve all tracks: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -62,7 +64,9 @@ func ServeTrack(track *model.Track) http.Handler { } w.Header().Add("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(Track{ track, releases }) + encoder := json.NewEncoder(w) + encoder.SetIndent("", "\t") + err = encoder.Encode(Track{ track, releases }) if err != nil { fmt.Printf("FATAL: Failed to serve track %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -128,7 +132,9 @@ func UpdateTrack(track *model.Track) http.Handler { } w.Header().Add("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(track) + encoder := json.NewEncoder(w) + encoder.SetIndent("", "\t") + err = encoder.Encode(track) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } From 2f2402026369dbd8f5c31c553f7c62323307442c Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 7 Dec 2024 19:25:45 +0000 Subject: [PATCH 17/24] add important headers --- global/funcs.go | 51 +++++++++++++++++++++++++++++++++++++++++++------ main.go | 5 ++++- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/global/funcs.go b/global/funcs.go index 33198ab..c0462db 100644 --- a/global/funcs.go +++ b/global/funcs.go @@ -1,18 +1,57 @@ package global import ( - "fmt" - "net/http" - "strconv" - "time" + "fmt" + "math/rand" + "net/http" + "strconv" + "time" - "arimelody-web/colour" + "arimelody-web/colour" ) +var PoweredByStrings = []string{ + "nerd rage", + "estrogen", + "your mother", + "awesome powers beyond comprehension", + "jared", + "the weight of my sins", + "the arc reactor", + "AA batteries", + "15 euro solar panel from ebay", + "magnets, how do they work", + "a fax machine", + "dell optiplex", + "a trans girl's nintendo wii", + "BASS", + "electricity, duh", + "seven hamsters in a big wheel", + "girls", + "mzungu hosting", + "golang", + "the state of the world right now", + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", + "the good folks at aperture science", + "free2play CDs", + "aridoodle", + "the love of creating", + "not for the sake of art; not for the sake of money; we like painting naked people", + "30 billion dollars in VC funding", +} + func DefaultHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Server", "arimelody.me") - w.Header().Add("Cache-Control", "max-age=2592000") + w.Header().Add("Do-Not-Stab", "1") + w.Header().Add("X-Clacks-Overhead", "GNU Terry Pratchett") + w.Header().Add("X-Hacker", "spare me please") + w.Header().Add("X-Robots-TXT", "'; DROP TABLE pages;") + w.Header().Add("X-Thinking-With", "Portals") + w.Header().Add( + "X-Powered-By", + PoweredByStrings[rand.Intn(len(PoweredByStrings))], + ) next.ServeHTTP(w, r) }) } diff --git a/main.go b/main.go index 5f2f966..3076623 100644 --- a/main.go +++ b/main.go @@ -67,7 +67,10 @@ func main() { // start the web server! mux := createServeMux() fmt.Printf("Now serving at http://127.0.0.1:%d\n", global.Config.Port) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", global.Config.Port), global.HTTPLog(mux))) + log.Fatal( + http.ListenAndServe(fmt.Sprintf(":%d", global.Config.Port), + global.HTTPLog(global.DefaultHeaders(mux)), + )) } func createServeMux() *http.ServeMux { From fad9704a948d5deaefb56a6098b8426ef3d6cfa5 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 28 Dec 2024 02:35:15 +0000 Subject: [PATCH 18/24] update web buttons --- public/img/buttons/ioletsgo.gif | Bin 0 -> 19950 bytes public/img/buttons/ipg.png | Bin 0 -> 1885 bytes public/img/buttons/itzzen.png | Bin 2867 -> 0 bytes public/img/buttons/notnite.png | Bin 0 -> 292 bytes public/img/buttons/retr0id_now.gif | Bin 0 -> 1878 bytes public/style/index.css | 2 +- views/index.html | 15 ++++++++++++--- 7 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 public/img/buttons/ioletsgo.gif create mode 100644 public/img/buttons/ipg.png delete mode 100644 public/img/buttons/itzzen.png create mode 100644 public/img/buttons/notnite.png create mode 100644 public/img/buttons/retr0id_now.gif diff --git a/public/img/buttons/ioletsgo.gif b/public/img/buttons/ioletsgo.gif new file mode 100644 index 0000000000000000000000000000000000000000..8edb51c8881324610bb32fb83936a42c1269bbc1 GIT binary patch literal 19950 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 Y<`3Q3_a6MP4F6LPE`HU|7E0(p01qfEZvX%Q literal 0 HcmV?d00001 diff --git a/public/img/buttons/ipg.png b/public/img/buttons/ipg.png new file mode 100644 index 0000000000000000000000000000000000000000..e64f0ab66313360eedd2ef39832239bf8622509e GIT binary patch literal 1885 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

- ## cool people + ## cool critters

@@ -153,8 +153,17 @@ elke web button - - itzzen web button + + InvoxiPlayGames web button + + + ioletsgo web button + + + notnite web button + + + retr0id web button
From 9d587b0ab9dc2d7a565046f0c986ab5448adae22 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 28 Dec 2024 02:38:33 +0000 Subject: [PATCH 19/24] add silver.js to projects --- views/index.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/views/index.html b/views/index.html index 8505058..85e5f0e 100644 --- a/views/index.html +++ b/views/index.html @@ -129,6 +129,11 @@ OpenTerminal +
  • + + Silver.js + +

  • From e6d7836c6ffa9705274cd7eb75e73e4112aef952 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 28 Dec 2024 03:02:05 +0000 Subject: [PATCH 20/24] add web button --- public/img/buttons/aikoyori.gif | Bin 0 -> 2211 bytes views/index.html | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 public/img/buttons/aikoyori.gif diff --git a/public/img/buttons/aikoyori.gif b/public/img/buttons/aikoyori.gif new file mode 100644 index 0000000000000000000000000000000000000000..e62ef23e9dd6ad6913bef7d7930dd21d5bc40fea GIT binary patch literal 2211 zcmaJ?3piBy8$NS$nQ<8zi;|Jtc{J!^rIs0FB$r%QgNbsyXRH zWe`%iEu(ffyOHQN${&ArW&irep2=2|?b+u%=Q+=FzVCd$_xs-Wd(R=FmGQpg?f@2u z0e~IrC_77MCxW?yrJ+6!1_8iV|AWJk;8PF00YTb1St)gOB^zU%Fh^iLKBx~^?^<7a z2?k;70i207oE4tkX*_F6T_I$jmU_SPv*i;>g&w#EMcM!1yE>vv7OUD>`s;M=Jl{`PDF zPzx4P%MJWa9>& z5aeXEDd_Lf6V~Y?xQEA#mz4U5xlodWbT#SzOx{9pC zX%FPJcr_w*qD1>7*AfC0D&=Oa0-cY!FWWrgcQ$%gteP0qxYz$MZkJ)1eo50)VrJ@2 z{5!is@3tB^=@;Cu?4Q~L;~GMS6ehi8T@kYS8Lg3%^)76&N!#O`7lZOzZmb&V-_!ex zof1W!#4ZPnNgg3gR&bl$9@KryUps4|V@qsI!kXh*&5O_bb9KE(NH{87z&hd+>j(e= z&_IGgzBibLib>E}I&qV(kS`OF0CH2J4E&r%(A)|sHRNIX)$Vscczh7l=hCi#!pAQ* zbOa5dOPB1OdzxL;^f;X}+3`-A@yt5!Ra*I*!DfBMj!1dlUe~9qBfd$xQwZIzJTiq~ z1Gjt>A@2-5ccdcf?QT;x`?<@jWoYY@ns96V#OT*cn6`PEeMD@(ZPmD@5519B(IT&q zr-?AGWaK+;LgOTmH`pks@J-ql$h$?1TeDP8!?Vi_XNTJb@;(vxLEf{s!}WMkAx|kc zc@WwE8}vwx`l9{to{kVYB$qH@7E3}`*XfeCp49KOn5x?+m3xmL!tD6d5JI0cpdPqC zefLzvz`Z~@_b}Q{QhZR-meY!7w!;{Ry*?J?+=J)cBPo%gaoKyA%wsH=TUodV%T=-* zUV^+{?1?=>QEg~c2~}xn-R5*r#4-2gmiXwH zr;k)JcGlhZG+JgT>yAz`_lu5=C@xod9F!AXgKXcHL|57`1W6J+mH4laqCrSYxWRqZ zw?ZxqxAVR>b^)Yzh=ZFV()^ICjFzoVkxf9DII9_VzhasDKDzB_Ai-40t?EVNu3D*h z#jub#TA6=;#B)of5gsuvI5CNy%t%R1JCA~7GPAOoYRXCngb-pugnK}Qf(-zN209)% zF{lYWt4d=|dRnoGnY>A+^QgWh+p)Kq{MC__HE9WC%H7gMEu>cdj~YhD)4VJYpZQP+ zOB#LF+(z|JehLO**TCl2&>6&_4r6|4d&-1n!gPSo%PN!CZ=iGR`;Ejg!ydHh{Ob(r zLJLePu2+q!LV_3c@qS=MkA~M0hl%Ou=JCrD@oY3jc$#90a6xk1ReOWP0 z*dtcx)6!n#aIeFXp=1aDL`XO)Rnq=w-sQaDeEOM!f+*IWv|t<6obt$s_^Kp&ZGA%{ zr|DKbw6(1jiRkRyDKsWk00QgS21&Vz6PO~MbfQ{7sW7dZZI`5^-f#-F|+vgn)j_fqOD4V@yi)A z-(My7^8etzXYyuO{-mz+d~moKgoDr?s{#@r4%v@5{x&Srf8qdL<%;->cIxcd?K0h% zsCdL`(xKHbpdb`@W}b>y&Q$YT5HAA|)dV4va{~)Q0Fh0_VHrD)3X_TgNyU6LlCO}O zEXFmn%^o1KJ(op*I4?tGl;X!1vV|Lw1HmQ+G=B}9T6A@`*;IiLKd!)b0194I Yur7EQ6lMVx+6dt&m;i;+9fAFS0R>)uv;Y7A literal 0 HcmV?d00001 diff --git a/views/index.html b/views/index.html index 85e5f0e..ee234bb 100644 --- a/views/index.html +++ b/views/index.html @@ -170,6 +170,9 @@ retr0id web button + + aikoyori web button +
    From 648e9e7caa2b544d11ee4b1fb518643283861bdd Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 28 Dec 2024 03:54:08 +0000 Subject: [PATCH 21/24] add web button --- public/img/buttons/xenia.png | Bin 0 -> 4963 bytes views/index.html | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 public/img/buttons/xenia.png diff --git a/public/img/buttons/xenia.png b/public/img/buttons/xenia.png new file mode 100644 index 0000000000000000000000000000000000000000..9be1a1fbbf950328338b40fa8d2e9f387ff7bb40 GIT binary patch literal 4963 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 aikoyori web button + + xenia web button +
    From 03c6276ec0a66416dde13de162bd97c2356f4f7e Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 28 Dec 2024 05:28:45 +0000 Subject: [PATCH 22/24] add web button --- public/img/buttons/stardust.png | Bin 0 -> 1248 bytes views/index.html | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 public/img/buttons/stardust.png diff --git a/public/img/buttons/stardust.png b/public/img/buttons/stardust.png new file mode 100644 index 0000000000000000000000000000000000000000..b64fae606e7a355b6a8d7fa5aed2fe2b84578d3f GIT binary patch literal 1248 zcmV<61Rwi}P) z&Ec`c*`v12kgCFgpu3-rm~@q}WQU`8af4EPnm~1xL~e>RX@)>yc^7Vz8)t?fUwTPP zb1_(LAXjcEP+=`dW+h2hIW=x3KwKe1PaZx@B05qWHcK!iZ5J;^FC1qp8DJDAJ`x`| z3>Ga14IvDW^`QU&1Sv^GK~#7F1;Ig*Bu5bdK#$0*>gm~$36lhZ8~^_m9KehNNV1*X z?yk%T+~?mB05kvq002q?006@G|Bhb+J*7x!G;J4I4FKrt1n|d~0RjLywh`w3nAf3e zvL^sgMS%u@E`az!gN6jjfZO=`Q~h)N&EyCz(9IsZfCd00fQA4lva)gyh`;>%KfjdR zlOVt!|8m0yFgA=m006yBM2kw> z=;%Lv-+%ntYpUoBy@>#OI&%Yn&DgdLpbpzGG_i+{N@%ZNe*gT(ubVxAU8G!Z4VulwqeXazy0>FF*ybi4ftx4gb76idpRe<-5`emH1;yuee5usi9`dHc;*v4kC*VV6cFeQMmN38d3Rj)ly*Y&)C%RW@@K2Y1C;A(Xp zMai%N>_QDD0Dr7|d}N&ubfi&HSJ}@v$F-_sURI@g?#%!;}+u^!~ZI0;I8k2hFw4OOVFCyQcPaf*S1l{}b?KT@> z9p>2InMa&!j~T%X`^p@>V|MMl`p5+;QEQJW(&6eezA%BR*X^cJuSw2V>zHxWo=4D` zRaH5AnR{0*x4I~Ru4BYFyg!!PmorR(?)*q~4a9kI46d~ffO@nJFvjRybrYjN!LhGp z{K$cU30)+w?;pO_53ZxGku-qXor(sDXd1e5H#u6@^Bus9U^mpa17JOm6Qa?bE=N8K zfO)Oi1$CVZ&ck{<-@zVCbVL2-X5S0u*bhC182egzI$oEs#=7j+nc}z}k!75({Xo>C zb>0le^Yh!=&oR+>yw-7p^djf08G~c(CP$Q+tvYlV``9ZpGUuuu8PQ$L_c6wotE`HN z;_>bFHk7d|*Ae5K2xGgom@Dora2`YqW8Y43hP*=_vg9#FIDdY)Gm-o8?f#}Dv9^%Y zGBAvW32TCYVRP7HJFWvqiZLSVblb;{iSYV(3#+oyd>j;$jz;X!QzcU+mBDqGFr3>U zA#^CmIAZPzHSZXt%(qu>W7E;l*`gDq=nT5ifnmr*qH1gzuQSy|xQAx!v6=Z8Hljgv zbOY9G(IQSdF!mriWF=+`vN3fy8lne=kyWqW9nfe16D>Lr9qa)iD=}u}6bDQ}HqkKl zW(>FM`)Uvk0HAfyz#gK*Vb9Eo5HLYBL^C!-pS935005#p(7_%wO0R4X#Xne=;#}%&t0000< KMNUMnLSTX;9YZbv literal 0 HcmV?d00001 diff --git a/views/index.html b/views/index.html index 56675b0..3cd5132 100644 --- a/views/index.html +++ b/views/index.html @@ -176,6 +176,9 @@ xenia web button + + stardust web button +
    From 35e862f4be2271d4c4f212b151ce9342efd1b2fd Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 28 Dec 2024 10:03:05 +0000 Subject: [PATCH 23/24] add web button --- public/img/buttons/isabelroses.gif | Bin 0 -> 2623 bytes views/index.html | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 public/img/buttons/isabelroses.gif diff --git a/public/img/buttons/isabelroses.gif b/public/img/buttons/isabelroses.gif new file mode 100644 index 0000000000000000000000000000000000000000..51e9a53ff072e09507eb61bc4000e3c22c2c85aa GIT binary patch literal 2623 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 hO-%qQ00008000000002eQ stardust web button + + isabel roses web button +
    From cdcc7466e5b2ea8fbc0213baf20672e46b248b88 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 20 Jan 2025 11:18:40 +0000 Subject: [PATCH 24/24] update schema.sql: use psql schemas --- schema.sql | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/schema.sql b/schema.sql index b2d48ee..15957ad 100644 --- a/schema.sql +++ b/schema.sql @@ -1,18 +1,20 @@ +CREATE SCHEMA arimelody AUTHORIZATION arimelody; + -- -- Artists (should be applicable to all art) -- -CREATE TABLE artist ( +CREATE TABLE arimelody.artist ( id character varying(64), name text NOT NULL, website text, avatar text ); -ALTER TABLE artist ADD CONSTRAINT artist_pk PRIMARY KEY (id); +ALTER TABLE arimelody.artist ADD CONSTRAINT artist_pk PRIMARY KEY (id); -- -- Music releases -- -CREATE TABLE musicrelease ( +CREATE TABLE arimelody.musicrelease ( id character varying(64) NOT NULL, visible bool DEFAULT false, title text NOT NULL, @@ -25,56 +27,56 @@ CREATE TABLE musicrelease ( copyright text, copyrightURL text ); -ALTER TABLE musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); +ALTER TABLE arimelody.musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); -- -- Music links (external platform links under a release) -- -CREATE TABLE musiclink ( +CREATE TABLE arimelody.musiclink ( release character varying(64) NOT NULL, name text NOT NULL, url text NOT NULL ); -ALTER TABLE musiclink ADD CONSTRAINT musiclink_pk PRIMARY KEY (release, name); +ALTER TABLE arimelody.musiclink ADD CONSTRAINT musiclink_pk PRIMARY KEY (release, name); -- -- Music credits (artist credits under a release) -- -CREATE TABLE musiccredit ( +CREATE TABLE arimelody.musiccredit ( release character varying(64) NOT NULL, artist character varying(64) NOT NULL, role text NOT NULL, is_primary boolean DEFAULT false ); -ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist); +ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist); -- -- Music tracks (tracks under a release) -- -CREATE TABLE musictrack ( +CREATE TABLE arimelody.musictrack ( id uuid DEFAULT gen_random_uuid(), title text NOT NULL, description text, lyrics text, preview_url text ); -ALTER TABLE musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id); +ALTER TABLE arimelody.musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id); -- -- Music release/track pairs -- -CREATE TABLE musicreleasetrack ( +CREATE TABLE arimelody.musicreleasetrack ( release character varying(64) NOT NULL, track uuid NOT NULL, number integer NOT NULL ); -ALTER TABLE musicreleasetrack ADD CONSTRAINT musicreleasetrack_pk PRIMARY KEY (release, track); +ALTER TABLE arimelody.musicreleasetrack ADD CONSTRAINT musicreleasetrack_pk PRIMARY KEY (release, track); -- -- Foreign keys -- -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; +ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; +ALTER TABLE arimelody.musiclink ADD CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER TABLE arimelody.musicreleasetrack ADD CONSTRAINT music_pair_trackref_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; +ALTER TABLE arimelody.musicreleasetrack ADD CONSTRAINT music_pair_releaseref_fk FOREIGN KEY (track) REFERENCES musictrack(id) ON DELETE CASCADE;