From 1667921f5b552eaf8efa508e4d1a5e7a25f6e153 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 9 Nov 2024 23:36:18 +0000 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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 06/18] 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 07/18] 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 08/18] 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 09/18] 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 10/18] 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 11/18] 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 12/18] 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 13/18] 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 14/18] 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 15/18] 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 16/18] 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 17/18] 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 18/18] 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;