From ed86aff2a24657f5ee1d4f204c6ceaf97132d419 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Mon, 31 Mar 2025 13:36:02 +0100
Subject: [PATCH 01/57] fix config not saving with broken listeners, fix cursor
ReferenceError
---
cursor/cursor.go | 10 +++++-----
public/script/config.js | 7 +++----
public/script/cursor.js | 4 +---
3 files changed, 9 insertions(+), 12 deletions(-)
diff --git a/cursor/cursor.go b/cursor/cursor.go
index 4e4504f..4ed59e3 100644
--- a/cursor/cursor.go
+++ b/cursor/cursor.go
@@ -88,13 +88,13 @@ func handleClient(client *CursorClient) {
client.Route = args[1]
mutex.Lock()
- for _, otherClient := range clients {
- if otherClient.ID == client.ID { continue }
- if otherClient.Route != client.Route { continue }
- client.Send([]byte(fmt.Sprintf("join:%d", otherClient.ID)))
- client.Send([]byte(fmt.Sprintf("pos:%d:%f:%f", otherClient.ID, otherClient.X, otherClient.Y)))
+ for otherClientID, otherClient := range clients {
+ if otherClientID == client.ID || otherClient.Route != client.Route { continue }
+ client.Send([]byte(fmt.Sprintf("join:%d", otherClientID)))
+ client.Send([]byte(fmt.Sprintf("pos:%d:%f:%f", otherClientID, otherClient.X, otherClient.Y)))
}
mutex.Unlock()
+
broadcast <- CursorMessage{
[]byte(fmt.Sprintf("join:%d", client.ID)),
client.Route,
diff --git a/public/script/config.js b/public/script/config.js
index 1ab8b5a..402a74b 100644
--- a/public/script/config.js
+++ b/public/script/config.js
@@ -55,6 +55,7 @@ class Config {
get crt() { return this._crt }
set crt(/** @type boolean */ enabled) {
this._crt = enabled;
+ this.save();
if (enabled) {
document.body.classList.add("crt");
@@ -66,26 +67,24 @@ class Config {
this.#listeners.get('crt').forEach(callback => {
callback(this._crt);
})
-
- this.save();
}
get cursor() { return this._cursor }
set cursor(/** @type boolean */ value) {
this._cursor = value;
+ this.save();
this.#listeners.get('cursor').forEach(callback => {
callback(this._cursor);
})
- this.save();
}
get cursorFunMode() { return this._cursorFunMode }
set cursorFunMode(/** @type boolean */ value) {
this._cursorFunMode = value;
+ this.save();
this.#listeners.get('cursorFunMode').forEach(callback => {
callback(this._cursorFunMode);
})
- this.save();
}
}
diff --git a/public/script/cursor.js b/public/script/cursor.js
index 188cdbb..79637fe 100644
--- a/public/script/cursor.js
+++ b/public/script/cursor.js
@@ -328,7 +328,7 @@ function cursorSetup() {
switch (args[0]) {
case 'id': {
- myCursor.id = Number(args[1]);
+ myCursor.id = id;
break;
}
case 'join': {
@@ -385,8 +385,6 @@ function cursorDestroy() {
cursors.clear();
myCursor = null;
- cursorContainer.remove();
-
console.log(`Cursor no longer tracking.`);
running = false;
}
From 5cc9a1ca7653351841c216d4b29e50941d72de53 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Mon, 31 Mar 2025 13:44:29 +0100
Subject: [PATCH 02/57] clear cursor display when shutting down
---
public/script/cursor.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/public/script/cursor.js b/public/script/cursor.js
index 79637fe..ff87068 100644
--- a/public/script/cursor.js
+++ b/public/script/cursor.js
@@ -381,6 +381,8 @@ function cursorDestroy() {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('keypress', handleKeyPress);
document.removeEventListener('keyup', handleKeyUp);
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
cursors.clear();
myCursor = null;
From 1c0e541c8914229f74d402690a05ee1a2c6168fd Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 29 Apr 2025 11:32:48 +0100
Subject: [PATCH 03/57] lock accounts after enough failed login attempts
---
admin/http.go | 49 ++++++++++++++++++++--
controller/account.go | 23 +++++++++++
controller/migrator.go | 6 ++-
main.go | 66 +++++++++++++++++++++++++++++-
model/account.go | 17 ++++----
schema-migration/000-init.sql | 2 +
schema-migration/003-fail-lock.sql | 3 ++
7 files changed, 153 insertions(+), 13 deletions(-)
create mode 100644 schema-migration/003-fail-lock.sql
diff --git a/admin/http.go b/admin/http.go
index b16c209..4d09264 100644
--- a/admin/http.go
+++ b/admin/http.go
@@ -274,11 +274,20 @@ func loginHandler(app *model.AppState) http.Handler {
render()
return
}
+ if account.Locked {
+ controller.SetSessionError(app.DB, session, "This account is locked.")
+ render()
+ return
+ }
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password))
if err != nil {
app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(app, r))
- controller.SetSessionError(app.DB, session, "Invalid username or password.")
+ if locked := handleFailedLogin(app, account); locked {
+ controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
+ } else {
+ controller.SetSessionError(app.DB, session, "Invalid username or password.")
+ }
render()
return
}
@@ -299,6 +308,8 @@ func loginHandler(app *model.AppState) http.Handler {
render()
return
}
+ controller.SetSessionMessage(app.DB, session, "")
+ controller.SetSessionError(app.DB, session, "")
http.Redirect(w, r, "/admin/totp", http.StatusFound)
return
}
@@ -377,8 +388,14 @@ func loginTOTPHandler(app *model.AppState) http.Handler {
return
}
if totpMethod == nil {
- app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r))
- controller.SetSessionError(app.DB, session, "Invalid TOTP.")
+ app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Incorrect TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r))
+ if locked := handleFailedLogin(app, session.AttemptAccount); locked {
+ controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
+ controller.SetSessionAttemptAccount(app.DB, session, nil)
+ http.Redirect(w, r, "/admin", http.StatusFound)
+ } else {
+ controller.SetSessionError(app.DB, session, "Incorrect TOTP.")
+ }
render()
return
}
@@ -496,3 +513,29 @@ func enforceSession(app *model.AppState, next http.Handler) http.Handler {
next.ServeHTTP(w, r.WithContext(ctx))
})
}
+
+func handleFailedLogin(app *model.AppState, account *model.Account) bool {
+ locked, err := controller.IncrementAccountFails(app.DB, account.ID)
+ if err != nil {
+ fmt.Fprintf(
+ os.Stderr,
+ "WARN: Failed to increment login failures for \"%s\": %v\n",
+ account.Username,
+ err,
+ )
+ app.Log.Warn(
+ log.TYPE_ACCOUNT,
+ "Failed to increment login failures for \"%s\"",
+ account.Username,
+ )
+ }
+ if locked {
+ app.Log.Warn(
+ log.TYPE_ACCOUNT,
+ "Account \"%s\" was locked: %d failed login attempts",
+ account.Username,
+ model.MAX_LOGIN_FAIL_ATTEMPTS,
+ )
+ }
+ return locked
+}
diff --git a/controller/account.go b/controller/account.go
index 9c7c1e1..e4f5dc4 100644
--- a/controller/account.go
+++ b/controller/account.go
@@ -110,3 +110,26 @@ func DeleteAccount(db *sqlx.DB, accountID string) error {
_, err := db.Exec("DELETE FROM account WHERE id=$1", accountID)
return err
}
+
+func IncrementAccountFails(db *sqlx.DB, accountID string) (bool, error) {
+ failAttempts := 0
+ err := db.Get(&failAttempts, "UPDATE account SET fail_attempts = fail_attempts + 1 WHERE id=$1 RETURNING fail_attempts", accountID)
+ if err != nil { return false, err }
+ locked := false
+ if failAttempts >= model.MAX_LOGIN_FAIL_ATTEMPTS {
+ err = LockAccount(db, accountID)
+ if err != nil { return false, err }
+ locked = true
+ }
+ return locked, err
+}
+
+func LockAccount(db *sqlx.DB, accountID string) error {
+ _, err := db.Exec("UPDATE account SET locked = true WHERE id=$1", accountID)
+ return err
+}
+
+func UnlockAccount(db *sqlx.DB, accountID string) error {
+ _, err := db.Exec("UPDATE account SET locked = false, fail_attempts = 0 WHERE id=$1", accountID)
+ return err
+}
diff --git a/controller/migrator.go b/controller/migrator.go
index b053a27..b970a1b 100644
--- a/controller/migrator.go
+++ b/controller/migrator.go
@@ -8,7 +8,7 @@ import (
"github.com/jmoiron/sqlx"
)
-const DB_VERSION int = 3
+const DB_VERSION int = 4
func CheckDBVersionAndMigrate(db *sqlx.DB) {
db.MustExec("CREATE SCHEMA IF NOT EXISTS arimelody")
@@ -45,6 +45,10 @@ func CheckDBVersionAndMigrate(db *sqlx.DB) {
ApplyMigration(db, "002-audit-logs")
oldDBVersion = 3
+ case 3:
+ ApplyMigration(db, "003-fail-lock")
+ oldDBVersion = 4
+
}
}
diff --git a/main.go b/main.go
index 2252622..360f7ac 100644
--- a/main.go
+++ b/main.go
@@ -276,11 +276,13 @@ func main() {
"User: %s\n" +
"\tID: %s\n" +
"\tEmail: %s\n" +
- "\tCreated: %s\n",
+ "\tCreated: %s\n" +
+ "\tLocked: %t\n",
account.Username,
account.ID,
email,
account.CreatedAt,
+ account.Locked,
)
}
return
@@ -355,6 +357,64 @@ func main() {
fmt.Printf("Account \"%s\" deleted successfully.\n", account.Username)
return
+ case "lockAccount":
+ if len(os.Args) < 3 {
+ fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for lockAccount\n")
+ os.Exit(1)
+ }
+ username := os.Args[2]
+ fmt.Printf("Unlocking account \"%s\"...\n", username)
+
+ account, err := controller.GetAccountByUsername(app.DB, username)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
+ os.Exit(1)
+ }
+
+ if account == nil {
+ fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
+ os.Exit(1)
+ }
+
+ err = controller.LockAccount(app.DB, account.ID)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "FATAL: Failed to lock account: %v\n", err)
+ os.Exit(1)
+ }
+
+ app.Log.Info(log.TYPE_ACCOUNT, "Account '%s' locked via config utility.", account.Username)
+ fmt.Printf("Account \"%s\" locked successfully.\n", account.Username)
+ return
+
+ case "unlockAccount":
+ if len(os.Args) < 3 {
+ fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for unlockAccount\n")
+ os.Exit(1)
+ }
+ username := os.Args[2]
+ fmt.Printf("Unlocking account \"%s\"...\n", username)
+
+ account, err := controller.GetAccountByUsername(app.DB, username)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
+ os.Exit(1)
+ }
+
+ if account == nil {
+ fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
+ os.Exit(1)
+ }
+
+ err = controller.UnlockAccount(app.DB, account.ID)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "FATAL: Failed to unlock account: %v\n", err)
+ os.Exit(1)
+ }
+
+ app.Log.Info(log.TYPE_ACCOUNT, "Account '%s' unlocked via config utility.", account.Username)
+ fmt.Printf("Account \"%s\" unlocked successfully.\n", account.Username)
+ return
+
case "logs":
// TODO: add log search parameters
logs, err := app.Log.Search([]log.LogLevel{}, []string{}, "", 100, 0)
@@ -389,7 +449,9 @@ func main() {
"createInvite:\n\tCreates an invite code to register new accounts.\n" +
"purgeInvites:\n\tDeletes all available invite codes.\n" +
"listAccounts:\n\tLists all active accounts.\n",
- "deleteAccount :\n\tDeletes an account with a given `username`.\n",
+ "deleteAccount :\n\tDeletes the account under `username`.\n",
+ "unlockAccount :\n\tUnlocks the account under `username`.\n",
+ "logs:\n\tShows system logs.\n",
)
return
}
diff --git a/model/account.go b/model/account.go
index 166e880..ad65b82 100644
--- a/model/account.go
+++ b/model/account.go
@@ -6,15 +6,18 @@ import (
)
const COOKIE_TOKEN string = "AM_SESSION"
+const MAX_LOGIN_FAIL_ATTEMPTS int = 3
type (
- Account struct {
- ID string `json:"id" db:"id"`
- Username string `json:"username" db:"username"`
- Password string `json:"password" db:"password"`
- Email sql.NullString `json:"email" db:"email"`
- AvatarURL sql.NullString `json:"avatar_url" db:"avatar_url"`
- CreatedAt time.Time `json:"created_at" db:"created_at"`
+ Account struct {
+ ID string `json:"id" db:"id"`
+ Username string `json:"username" db:"username"`
+ Password string `json:"password" db:"password"`
+ Email sql.NullString `json:"email" db:"email"`
+ AvatarURL sql.NullString `json:"avatar_url" db:"avatar_url"`
+ CreatedAt time.Time `json:"created_at" db:"created_at"`
+ FailAttempts int `json:"fail_attempts" db:"fail_attempts"`
+ Locked bool `json:"locked" db:"locked"`
Privileges []AccountPrivilege `json:"privileges"`
}
diff --git a/schema-migration/000-init.sql b/schema-migration/000-init.sql
index b56c6b6..2174109 100644
--- a/schema-migration/000-init.sql
+++ b/schema-migration/000-init.sql
@@ -19,6 +19,8 @@ CREATE TABLE arimelody.account (
email TEXT,
avatar_url TEXT,
created_at TIMESTAMP NOT NULL DEFAULT current_timestamp
+ fail_attempts INT NOT NULL DEFAULT 0,
+ locked BOOLEAN DEFAULT false,
);
ALTER TABLE arimelody.account ADD CONSTRAINT account_pk PRIMARY KEY (id);
diff --git a/schema-migration/003-fail-lock.sql b/schema-migration/003-fail-lock.sql
new file mode 100644
index 0000000..32d19bf
--- /dev/null
+++ b/schema-migration/003-fail-lock.sql
@@ -0,0 +1,3 @@
+-- it would be nice to prevent brute-forcing
+ALTER TABLE arimelody.account ADD COLUMN fail_attempts INT NOT NULL DEFAULT 0;
+ALTER TABLE arimelody.account ADD COLUMN locked BOOLEAN DEFAULT false;
From 562ed2e0150331aff4937a456ee6904bfc2c676f Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 29 Apr 2025 16:31:39 +0100
Subject: [PATCH 04/57] session validation/invalidation
---
admin/http.go | 2 +-
api/release.go | 2 +-
controller/session.go | 19 ++++++++++++++++---
view/music.go | 2 +-
4 files changed, 19 insertions(+), 6 deletions(-)
diff --git a/admin/http.go b/admin/http.go
index 4d09264..bdacad3 100644
--- a/admin/http.go
+++ b/admin/http.go
@@ -483,7 +483,7 @@ func staticHandler() http.Handler {
func enforceSession(app *model.AppState, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- session, err := controller.GetSessionFromRequest(app.DB, r)
+ session, err := controller.GetSessionFromRequest(app, r)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
diff --git a/api/release.go b/api/release.go
index efed8dd..5cb87b0 100644
--- a/api/release.go
+++ b/api/release.go
@@ -20,7 +20,7 @@ func ServeRelease(app *model.AppState, release *model.Release) http.Handler {
// only allow authorised users to view hidden releases
privileged := false
if !release.Visible {
- session, err := controller.GetSessionFromRequest(app.DB, r)
+ session, err := controller.GetSessionFromRequest(app, r)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
diff --git a/controller/session.go b/controller/session.go
index cf423fe..dce7ad0 100644
--- a/controller/session.go
+++ b/controller/session.go
@@ -8,6 +8,7 @@ import (
"strings"
"time"
+ "arimelody-web/log"
"arimelody-web/model"
"github.com/jmoiron/sqlx"
@@ -15,7 +16,7 @@ import (
const TOKEN_LEN = 64
-func GetSessionFromRequest(db *sqlx.DB, r *http.Request) (*model.Session, error) {
+func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session, error) {
sessionCookie, err := r.Cookie(model.COOKIE_TOKEN)
if err != nil && err != http.ErrNoCookie {
return nil, errors.New(fmt.Sprintf("Failed to retrieve session cookie: %v", err))
@@ -25,14 +26,26 @@ func GetSessionFromRequest(db *sqlx.DB, r *http.Request) (*model.Session, error)
if sessionCookie != nil {
// fetch existing session
- session, err = GetSession(db, sessionCookie.Value)
+ session, err = GetSession(app.DB, sessionCookie.Value)
if err != nil && !strings.Contains(err.Error(), "no rows") {
return nil, errors.New(fmt.Sprintf("Failed to retrieve session: %v", err))
}
if session != nil {
- // TODO: consider running security checks here (i.e. user agent mismatches)
+ if session.UserAgent != r.UserAgent() {
+ msg := "Session user agent mismatch. A cookie may have been hijacked!"
+ if session.Account != nil {
+ account, _ := GetAccountByID(app.DB, session.Account.ID)
+ msg += " (Account \"" + account.Username + "\")"
+ }
+ app.Log.Warn(log.TYPE_ACCOUNT, msg)
+ err = DeleteSession(app.DB, session.Token)
+ if err != nil {
+ app.Log.Warn(log.TYPE_ACCOUNT, "Failed to delete affected session")
+ }
+ return nil, nil
+ }
}
}
diff --git a/view/music.go b/view/music.go
index 2d40ef0..dfe884e 100644
--- a/view/music.go
+++ b/view/music.go
@@ -60,7 +60,7 @@ func ServeGateway(app *model.AppState, release *model.Release) http.Handler {
// only allow authorised users to view hidden releases
privileged := false
if !release.Visible {
- session, err := controller.GetSessionFromRequest(app.DB, r)
+ session, err := controller.GetSessionFromRequest(app, r)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
From fe4a7888989192548cab21f2137f8f7a372d7451 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 29 Apr 2025 23:22:48 +0100
Subject: [PATCH 05/57] update cli utility docs
---
README.md | 3 +++
main.go | 3 ++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index e5df7f6..464379e 100644
--- a/README.md
+++ b/README.md
@@ -47,3 +47,6 @@ need to be up for this, making this ideal for some offline maintenance.
- `purgeInvites`: Deletes all available invite codes.
- `listAccounts`: Lists all active accounts.
- `deleteAccount `: Deletes an account with a given `username`.
+- `lockAccount `: Locks the account under `username`.
+- `unlockAccount `: Unlocks the account under `username`.
+- `logs`: Shows system logs.
diff --git a/main.go b/main.go
index 360f7ac..c0f6ee2 100644
--- a/main.go
+++ b/main.go
@@ -450,8 +450,9 @@ func main() {
"purgeInvites:\n\tDeletes all available invite codes.\n" +
"listAccounts:\n\tLists all active accounts.\n",
"deleteAccount :\n\tDeletes the account under `username`.\n",
+ "lockAccount :\n\tLocks the account under `username`.\n",
"unlockAccount :\n\tUnlocks the account under `username`.\n",
- "logs:\n\tShows system logs.\n",
+ "logs:\n\tShows system logs.\n",
)
return
}
From 23a02617f91384953b0d24813a75d4a32e3a4e14 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 29 Apr 2025 23:25:32 +0100
Subject: [PATCH 06/57] fix indentation (tabs to 4 spaces) (oops)
---
admin/accounthttp.go | 18 +++----
admin/artisthttp.go | 6 +--
admin/http.go | 108 ++++++++++++++++++++---------------------
admin/logshttp.go | 12 ++---
admin/releasehttp.go | 10 ++--
admin/templates.go | 12 ++---
admin/trackhttp.go | 6 +--
api/api.go | 16 +++---
api/artist.go | 78 ++++++++++++++---------------
api/release.go | 36 +++++++-------
api/track.go | 36 +++++++-------
api/uploads.go | 70 +++++++++++++-------------
controller/account.go | 34 ++++++-------
controller/artist.go | 56 ++++++++++-----------
controller/config.go | 12 ++---
controller/invite.go | 10 ++--
controller/ip.go | 8 +--
controller/migrator.go | 8 +--
controller/qr.go | 14 +++---
controller/release.go | 8 +--
controller/session.go | 44 ++++++++---------
controller/totp.go | 26 +++++-----
controller/track.go | 44 ++++++++---------
cursor/cursor.go | 18 +++----
discord/discord.go | 14 +++---
log/log.go | 8 +--
main.go | 52 ++++++++++----------
model/account.go | 22 ++++-----
model/appstate.go | 2 +-
model/artist.go | 20 ++++----
model/link.go | 12 ++---
model/release.go | 32 ++++++------
model/release_test.go | 4 +-
model/session.go | 4 +-
model/totp.go | 2 +-
model/track.go | 16 +++---
templates/templates.go | 4 +-
view/music.go | 12 ++---
38 files changed, 447 insertions(+), 447 deletions(-)
diff --git a/admin/accounthttp.go b/admin/accounthttp.go
index 125abdc..945a507 100644
--- a/admin/accounthttp.go
+++ b/admin/accounthttp.go
@@ -1,17 +1,17 @@
package admin
import (
- "database/sql"
- "fmt"
- "net/http"
- "net/url"
- "os"
+ "database/sql"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
- "arimelody-web/controller"
- "arimelody-web/log"
- "arimelody-web/model"
+ "arimelody-web/controller"
+ "arimelody-web/log"
+ "arimelody-web/model"
- "golang.org/x/crypto/bcrypt"
+ "golang.org/x/crypto/bcrypt"
)
func accountHandler(app *model.AppState) http.Handler {
diff --git a/admin/artisthttp.go b/admin/artisthttp.go
index 6dfbbfd..9fa6bb2 100644
--- a/admin/artisthttp.go
+++ b/admin/artisthttp.go
@@ -1,9 +1,9 @@
package admin
import (
- "fmt"
- "net/http"
- "strings"
+ "fmt"
+ "net/http"
+ "strings"
"arimelody-web/model"
"arimelody-web/controller"
diff --git a/admin/http.go b/admin/http.go
index bdacad3..76eb5f2 100644
--- a/admin/http.go
+++ b/admin/http.go
@@ -1,20 +1,20 @@
package admin
import (
- "context"
- "database/sql"
- "fmt"
- "net/http"
- "os"
- "path/filepath"
- "strings"
- "time"
+ "context"
+ "database/sql"
+ "fmt"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
- "arimelody-web/controller"
- "arimelody-web/log"
- "arimelody-web/model"
+ "arimelody-web/controller"
+ "arimelody-web/log"
+ "arimelody-web/model"
- "golang.org/x/crypto/bcrypt"
+ "golang.org/x/crypto/bcrypt"
)
func Handler(app *model.AppState) http.Handler {
@@ -274,20 +274,20 @@ func loginHandler(app *model.AppState) http.Handler {
render()
return
}
- if account.Locked {
- controller.SetSessionError(app.DB, session, "This account is locked.")
- render()
- return
- }
+ if account.Locked {
+ controller.SetSessionError(app.DB, session, "This account is locked.")
+ render()
+ return
+ }
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password))
if err != nil {
app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(app, r))
- if locked := handleFailedLogin(app, account); locked {
- controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
- } else {
- controller.SetSessionError(app.DB, session, "Invalid username or password.")
- }
+ if locked := handleFailedLogin(app, account); locked {
+ controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
+ } else {
+ controller.SetSessionError(app.DB, session, "Invalid username or password.")
+ }
render()
return
}
@@ -308,8 +308,8 @@ func loginHandler(app *model.AppState) http.Handler {
render()
return
}
- controller.SetSessionMessage(app.DB, session, "")
- controller.SetSessionError(app.DB, session, "")
+ controller.SetSessionMessage(app.DB, session, "")
+ controller.SetSessionError(app.DB, session, "")
http.Redirect(w, r, "/admin/totp", http.StatusFound)
return
}
@@ -389,13 +389,13 @@ func loginTOTPHandler(app *model.AppState) http.Handler {
}
if totpMethod == nil {
app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Incorrect TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r))
- if locked := handleFailedLogin(app, session.AttemptAccount); locked {
- controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
- controller.SetSessionAttemptAccount(app.DB, session, nil)
- http.Redirect(w, r, "/admin", http.StatusFound)
- } else {
- controller.SetSessionError(app.DB, session, "Incorrect TOTP.")
- }
+ if locked := handleFailedLogin(app, session.AttemptAccount); locked {
+ controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
+ controller.SetSessionAttemptAccount(app.DB, session, nil)
+ http.Redirect(w, r, "/admin", http.StatusFound)
+ } else {
+ controller.SetSessionError(app.DB, session, "Incorrect TOTP.")
+ }
render()
return
}
@@ -515,27 +515,27 @@ func enforceSession(app *model.AppState, next http.Handler) http.Handler {
}
func handleFailedLogin(app *model.AppState, account *model.Account) bool {
- locked, err := controller.IncrementAccountFails(app.DB, account.ID)
- if err != nil {
- fmt.Fprintf(
- os.Stderr,
- "WARN: Failed to increment login failures for \"%s\": %v\n",
- account.Username,
- err,
- )
- app.Log.Warn(
- log.TYPE_ACCOUNT,
- "Failed to increment login failures for \"%s\"",
- account.Username,
- )
- }
- if locked {
- app.Log.Warn(
- log.TYPE_ACCOUNT,
- "Account \"%s\" was locked: %d failed login attempts",
- account.Username,
- model.MAX_LOGIN_FAIL_ATTEMPTS,
- )
- }
- return locked
+ locked, err := controller.IncrementAccountFails(app.DB, account.ID)
+ if err != nil {
+ fmt.Fprintf(
+ os.Stderr,
+ "WARN: Failed to increment login failures for \"%s\": %v\n",
+ account.Username,
+ err,
+ )
+ app.Log.Warn(
+ log.TYPE_ACCOUNT,
+ "Failed to increment login failures for \"%s\"",
+ account.Username,
+ )
+ }
+ if locked {
+ app.Log.Warn(
+ log.TYPE_ACCOUNT,
+ "Account \"%s\" was locked: %d failed login attempts",
+ account.Username,
+ model.MAX_LOGIN_FAIL_ATTEMPTS,
+ )
+ }
+ return locked
}
diff --git a/admin/logshttp.go b/admin/logshttp.go
index 93dc5b7..7249b16 100644
--- a/admin/logshttp.go
+++ b/admin/logshttp.go
@@ -1,12 +1,12 @@
package admin
import (
- "arimelody-web/log"
- "arimelody-web/model"
- "fmt"
- "net/http"
- "os"
- "strings"
+ "arimelody-web/log"
+ "arimelody-web/model"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
)
func logsHandler(app *model.AppState) http.Handler {
diff --git a/admin/releasehttp.go b/admin/releasehttp.go
index 7ef4d37..c6b68ab 100644
--- a/admin/releasehttp.go
+++ b/admin/releasehttp.go
@@ -1,12 +1,12 @@
package admin
import (
- "fmt"
- "net/http"
- "strings"
+ "fmt"
+ "net/http"
+ "strings"
- "arimelody-web/controller"
- "arimelody-web/model"
+ "arimelody-web/controller"
+ "arimelody-web/model"
)
func serveRelease(app *model.AppState) http.Handler {
diff --git a/admin/templates.go b/admin/templates.go
index 12cdf08..606d569 100644
--- a/admin/templates.go
+++ b/admin/templates.go
@@ -1,12 +1,12 @@
package admin
import (
- "arimelody-web/log"
- "fmt"
- "html/template"
- "path/filepath"
- "strings"
- "time"
+ "arimelody-web/log"
+ "fmt"
+ "html/template"
+ "path/filepath"
+ "strings"
+ "time"
)
var indexTemplate = template.Must(template.ParseFiles(
diff --git a/admin/trackhttp.go b/admin/trackhttp.go
index a92f81a..93eacdb 100644
--- a/admin/trackhttp.go
+++ b/admin/trackhttp.go
@@ -1,9 +1,9 @@
package admin
import (
- "fmt"
- "net/http"
- "strings"
+ "fmt"
+ "net/http"
+ "strings"
"arimelody-web/model"
"arimelody-web/controller"
diff --git a/api/api.go b/api/api.go
index d3c83ce..398db4b 100644
--- a/api/api.go
+++ b/api/api.go
@@ -1,15 +1,15 @@
package api
import (
- "context"
- "errors"
- "fmt"
- "net/http"
- "os"
- "strings"
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
- "arimelody-web/controller"
- "arimelody-web/model"
+ "arimelody-web/controller"
+ "arimelody-web/model"
)
func Handler(app *model.AppState) http.Handler {
diff --git a/api/artist.go b/api/artist.go
index 9006cc3..01899a6 100644
--- a/api/artist.go
+++ b/api/artist.go
@@ -1,66 +1,66 @@
package api
import (
- "encoding/json"
- "fmt"
- "io/fs"
- "net/http"
- "os"
- "path/filepath"
- "strings"
- "time"
+ "encoding/json"
+ "fmt"
+ "io/fs"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
- "arimelody-web/controller"
- "arimelody-web/log"
- "arimelody-web/model"
+ "arimelody-web/controller"
+ "arimelody-web/log"
+ "arimelody-web/model"
)
func ServeAllArtists(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var artists = []*model.Artist{}
artists, err := controller.GetAllArtists(app.DB)
- if err != nil {
+ if err != nil {
fmt.Printf("WARN: Failed to serve all artists: %s\n", err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
- w.Header().Add("Content-Type", "application/json")
+ w.Header().Add("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(artists)
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- }
+ if err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ }
})
}
func ServeArtist(app *model.AppState, artist *model.Artist) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- type (
- creditJSON struct {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ type (
+ creditJSON struct {
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
- Credits map[string]creditJSON `json:"credits"`
- }
- )
+ Role string `json:"role"`
+ Primary bool `json:"primary"`
+ }
+ artistJSON struct {
+ *model.Artist
+ Credits map[string]creditJSON `json:"credits"`
+ }
+ )
session := r.Context().Value("session").(*model.Session)
show_hidden_releases := session != nil && session.Account != nil
dbCredits, err := controller.GetArtistCredits(app.DB, artist.ID, show_hidden_releases)
- if err != nil {
+ if err != nil {
fmt.Printf("WARN: Failed to retrieve artist credits for %s: %v\n", artist.ID, err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
var credits = map[string]creditJSON{}
for _, credit := range dbCredits {
@@ -74,17 +74,17 @@ func ServeArtist(app *model.AppState, artist *model.Artist) http.Handler {
}
}
- w.Header().Add("Content-Type", "application/json")
+ w.Header().Add("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(artistJSON{
Artist: artist,
Credits: credits,
})
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- }
- })
+ if err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ }
+ })
}
func CreateArtist(app *model.AppState) http.Handler {
diff --git a/api/release.go b/api/release.go
index 5cb87b0..e07f0d7 100644
--- a/api/release.go
+++ b/api/release.go
@@ -1,22 +1,22 @@
package api
import (
- "encoding/json"
- "fmt"
- "io/fs"
- "net/http"
- "os"
- "path/filepath"
- "strings"
- "time"
+ "encoding/json"
+ "fmt"
+ "io/fs"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
- "arimelody-web/controller"
- "arimelody-web/log"
- "arimelody-web/model"
+ "arimelody-web/controller"
+ "arimelody-web/log"
+ "arimelody-web/model"
)
func ServeRelease(app *model.AppState, release *model.Release) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// only allow authorised users to view hidden releases
privileged := false
if !release.Visible {
@@ -116,15 +116,15 @@ func ServeRelease(app *model.AppState, release *model.Release) http.Handler {
}
}
- w.Header().Add("Content-Type", "application/json")
+ w.Header().Add("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err := encoder.Encode(response)
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
- })
+ if err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+ })
}
func ServeCatalog(app *model.AppState) http.Handler {
diff --git a/api/track.go b/api/track.go
index e7d7c07..4e48418 100644
--- a/api/track.go
+++ b/api/track.go
@@ -1,13 +1,13 @@
package api
import (
- "encoding/json"
- "fmt"
- "net/http"
+ "encoding/json"
+ "fmt"
+ "net/http"
- "arimelody-web/controller"
- "arimelody-web/log"
- "arimelody-web/model"
+ "arimelody-web/controller"
+ "arimelody-web/log"
+ "arimelody-web/model"
)
type (
@@ -29,7 +29,7 @@ func ServeAllTracks(app *model.AppState) http.Handler {
dbTracks, err := controller.GetAllTracks(app.DB)
if err != nil {
fmt.Printf("WARN: Failed to pull tracks from DB: %s\n", err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
for _, track := range dbTracks {
@@ -39,23 +39,23 @@ func ServeAllTracks(app *model.AppState) http.Handler {
})
}
- w.Header().Add("Content-Type", "application/json")
+ w.Header().Add("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(tracks)
- if err != nil {
+ if err != nil {
fmt.Printf("WARN: Failed to serve all tracks: %s\n", err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- }
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ }
})
}
func ServeTrack(app *model.AppState, track *model.Track) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dbReleases, err := controller.GetTrackReleases(app.DB, track.ID, false)
if err != nil {
fmt.Printf("WARN: Failed to pull track releases for %s from DB: %s\n", track.ID, err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
releases := []string{}
@@ -63,15 +63,15 @@ func ServeTrack(app *model.AppState, track *model.Track) http.Handler {
releases = append(releases, release.ID)
}
- w.Header().Add("Content-Type", "application/json")
+ w.Header().Add("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(Track{ track, releases })
- if err != nil {
+ if err != nil {
fmt.Printf("WARN: Failed to serve track %s: %s\n", track.ID, err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- }
- })
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ }
+ })
}
func CreateTrack(app *model.AppState) http.Handler {
diff --git a/api/uploads.go b/api/uploads.go
index 60ab7dd..4678f22 100644
--- a/api/uploads.go
+++ b/api/uploads.go
@@ -1,56 +1,56 @@
package api
import (
- "arimelody-web/log"
- "arimelody-web/model"
- "bufio"
- "encoding/base64"
- "errors"
- "fmt"
- "os"
- "path/filepath"
- "strings"
+ "arimelody-web/log"
+ "arimelody-web/model"
+ "bufio"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
)
func HandleImageUpload(app *model.AppState, data *string, directory string, filename string) (string, error) {
- split := strings.Split(*data, ";base64,")
- header := split[0]
- imageData, err := base64.StdEncoding.DecodeString(split[1])
- ext, _ := strings.CutPrefix(header, "data:image/")
+ split := strings.Split(*data, ";base64,")
+ header := split[0]
+ imageData, err := base64.StdEncoding.DecodeString(split[1])
+ ext, _ := strings.CutPrefix(header, "data:image/")
directory = filepath.Join(app.Config.DataDirectory, directory)
- switch ext {
- case "png":
- case "jpg":
- case "jpeg":
- default:
- return "", errors.New("Invalid image type. Allowed: .png, .jpg, .jpeg")
- }
+ switch ext {
+ case "png":
+ case "jpg":
+ case "jpeg":
+ default:
+ return "", errors.New("Invalid image type. Allowed: .png, .jpg, .jpeg")
+ }
filename = fmt.Sprintf("%s.%s", filename, ext)
- // ensure directory exists
- os.MkdirAll(directory, os.ModePerm)
+ // ensure directory exists
+ os.MkdirAll(directory, os.ModePerm)
- imagePath := filepath.Join(directory, filename)
- file, err := os.Create(imagePath)
- if err != nil {
- return "", err
- }
- defer file.Close()
+ imagePath := filepath.Join(directory, filename)
+ file, err := os.Create(imagePath)
+ if err != nil {
+ return "", err
+ }
+ defer file.Close()
// TODO: generate compressed versions of image (512x512?)
- buffer := bufio.NewWriter(file)
- _, err = buffer.Write(imageData)
- if err != nil {
+ buffer := bufio.NewWriter(file)
+ _, err = buffer.Write(imageData)
+ if err != nil {
return "", nil
- }
+ }
- if err := buffer.Flush(); err != nil {
+ if err := buffer.Flush(); err != nil {
return "", nil
- }
+ }
app.Log.Info(log.TYPE_FILES, "\"%s/%s.%s\" created.", directory, filename, ext)
- return filename, nil
+ return filename, nil
}
diff --git a/controller/account.go b/controller/account.go
index e4f5dc4..ab64ca5 100644
--- a/controller/account.go
+++ b/controller/account.go
@@ -1,10 +1,10 @@
package controller
import (
- "arimelody-web/model"
- "strings"
+ "arimelody-web/model"
+ "strings"
- "github.com/jmoiron/sqlx"
+ "github.com/jmoiron/sqlx"
)
func GetAllAccounts(db *sqlx.DB) ([]model.Account, error) {
@@ -112,24 +112,24 @@ func DeleteAccount(db *sqlx.DB, accountID string) error {
}
func IncrementAccountFails(db *sqlx.DB, accountID string) (bool, error) {
- failAttempts := 0
- err := db.Get(&failAttempts, "UPDATE account SET fail_attempts = fail_attempts + 1 WHERE id=$1 RETURNING fail_attempts", accountID)
- if err != nil { return false, err }
- locked := false
- if failAttempts >= model.MAX_LOGIN_FAIL_ATTEMPTS {
- err = LockAccount(db, accountID)
- if err != nil { return false, err }
- locked = true
- }
- return locked, err
+ failAttempts := 0
+ err := db.Get(&failAttempts, "UPDATE account SET fail_attempts = fail_attempts + 1 WHERE id=$1 RETURNING fail_attempts", accountID)
+ if err != nil { return false, err }
+ locked := false
+ if failAttempts >= model.MAX_LOGIN_FAIL_ATTEMPTS {
+ err = LockAccount(db, accountID)
+ if err != nil { return false, err }
+ locked = true
+ }
+ return locked, err
}
func LockAccount(db *sqlx.DB, accountID string) error {
- _, err := db.Exec("UPDATE account SET locked = true WHERE id=$1", accountID)
- return err
+ _, err := db.Exec("UPDATE account SET locked = true WHERE id=$1", accountID)
+ return err
}
func UnlockAccount(db *sqlx.DB, accountID string) error {
- _, err := db.Exec("UPDATE account SET locked = false, fail_attempts = 0 WHERE id=$1", accountID)
- return err
+ _, err := db.Exec("UPDATE account SET locked = false, fail_attempts = 0 WHERE id=$1", accountID)
+ return err
}
diff --git a/controller/artist.go b/controller/artist.go
index 1a613aa..f086778 100644
--- a/controller/artist.go
+++ b/controller/artist.go
@@ -1,48 +1,48 @@
package controller
import (
- "arimelody-web/model"
+ "arimelody-web/model"
- "github.com/jmoiron/sqlx"
+ "github.com/jmoiron/sqlx"
)
// DATABASE
func GetArtist(db *sqlx.DB, id string) (*model.Artist, error) {
- var artist = model.Artist{}
+ var artist = model.Artist{}
- err := db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id)
- if err != nil {
- return nil, err
- }
+ err := db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id)
+ if err != nil {
+ return nil, err
+ }
- return &artist, nil
+ return &artist, nil
}
func GetAllArtists(db *sqlx.DB) ([]*model.Artist, error) {
- var artists = []*model.Artist{}
+ var artists = []*model.Artist{}
- err := db.Select(&artists, "SELECT * FROM artist")
- if err != nil {
- return nil, err
- }
+ err := db.Select(&artists, "SELECT * FROM artist")
+ if err != nil {
+ return nil, err
+ }
- return artists, nil
+ return artists, nil
}
func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, error) {
- var artists = []*model.Artist{}
+ var artists = []*model.Artist{}
- err := db.Select(&artists,
+ err := db.Select(&artists,
"SELECT * FROM artist "+
"WHERE id NOT IN "+
"(SELECT artist FROM musiccredit WHERE release=$1)",
releaseID)
- if err != nil {
- return nil, err
- }
+ if err != nil {
+ return nil, err
+ }
- return artists, nil
+ return artists, nil
}
func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.Credit, error) {
@@ -54,9 +54,9 @@ func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.
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
- }
+ if err != nil {
+ return nil, err
+ }
defer rows.Close()
type NamePrimary struct {
@@ -102,13 +102,13 @@ func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.
func CreateArtist(db *sqlx.DB, artist *model.Artist) error {
_, err := db.Exec(
- "INSERT INTO artist (id, name, website, avatar) "+
+ "INSERT INTO artist (id, name, website, avatar) "+
"VALUES ($1, $2, $3, $4)",
- artist.ID,
- artist.Name,
- artist.Website,
+ artist.ID,
+ artist.Name,
+ artist.Website,
artist.Avatar,
- )
+ )
if err != nil {
return err
}
diff --git a/controller/config.go b/controller/config.go
index 5a69d49..1d3cbbb 100644
--- a/controller/config.go
+++ b/controller/config.go
@@ -1,14 +1,14 @@
package controller
import (
- "errors"
- "fmt"
- "os"
- "strconv"
+ "errors"
+ "fmt"
+ "os"
+ "strconv"
- "arimelody-web/model"
+ "arimelody-web/model"
- "github.com/pelletier/go-toml/v2"
+ "github.com/pelletier/go-toml/v2"
)
func GetConfig() model.Config {
diff --git a/controller/invite.go b/controller/invite.go
index f30db64..a7bde40 100644
--- a/controller/invite.go
+++ b/controller/invite.go
@@ -1,12 +1,12 @@
package controller
import (
- "arimelody-web/model"
- "math/rand"
- "strings"
- "time"
+ "arimelody-web/model"
+ "math/rand"
+ "strings"
+ "time"
- "github.com/jmoiron/sqlx"
+ "github.com/jmoiron/sqlx"
)
var inviteChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
diff --git a/controller/ip.go b/controller/ip.go
index 233d76a..cbc3054 100644
--- a/controller/ip.go
+++ b/controller/ip.go
@@ -1,10 +1,10 @@
package controller
import (
- "arimelody-web/model"
- "net/http"
- "slices"
- "strings"
+ "arimelody-web/model"
+ "net/http"
+ "slices"
+ "strings"
)
// Returns the request's original IP address, resolving the `x-forwarded-for`
diff --git a/controller/migrator.go b/controller/migrator.go
index b970a1b..4b99b9c 100644
--- a/controller/migrator.go
+++ b/controller/migrator.go
@@ -1,11 +1,11 @@
package controller
import (
- "fmt"
- "os"
- "time"
+ "fmt"
+ "os"
+ "time"
- "github.com/jmoiron/sqlx"
+ "github.com/jmoiron/sqlx"
)
const DB_VERSION int = 4
diff --git a/controller/qr.go b/controller/qr.go
index 7ada0f8..6b04e69 100644
--- a/controller/qr.go
+++ b/controller/qr.go
@@ -1,13 +1,13 @@
package controller
import (
- "bytes"
- "encoding/base64"
- "errors"
- "fmt"
- "image"
- "image/color"
- "image/png"
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "image"
+ "image/color"
+ "image/png"
"github.com/skip2/go-qrcode"
)
diff --git a/controller/release.go b/controller/release.go
index 362669a..3dcad26 100644
--- a/controller/release.go
+++ b/controller/release.go
@@ -1,12 +1,12 @@
package controller
import (
- "errors"
- "fmt"
+ "errors"
+ "fmt"
- "arimelody-web/model"
+ "arimelody-web/model"
- "github.com/jmoiron/sqlx"
+ "github.com/jmoiron/sqlx"
)
func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
diff --git a/controller/session.go b/controller/session.go
index dce7ad0..b037575 100644
--- a/controller/session.go
+++ b/controller/session.go
@@ -1,17 +1,17 @@
package controller
import (
- "database/sql"
- "errors"
- "fmt"
- "net/http"
- "strings"
- "time"
+ "database/sql"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
- "arimelody-web/log"
- "arimelody-web/model"
+ "arimelody-web/log"
+ "arimelody-web/model"
- "github.com/jmoiron/sqlx"
+ "github.com/jmoiron/sqlx"
)
const TOKEN_LEN = 64
@@ -33,19 +33,19 @@ func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session
}
if session != nil {
- if session.UserAgent != r.UserAgent() {
- msg := "Session user agent mismatch. A cookie may have been hijacked!"
- if session.Account != nil {
- account, _ := GetAccountByID(app.DB, session.Account.ID)
- msg += " (Account \"" + account.Username + "\")"
- }
- app.Log.Warn(log.TYPE_ACCOUNT, msg)
- err = DeleteSession(app.DB, session.Token)
- if err != nil {
- app.Log.Warn(log.TYPE_ACCOUNT, "Failed to delete affected session")
- }
- return nil, nil
- }
+ if session.UserAgent != r.UserAgent() {
+ msg := "Session user agent mismatch. A cookie may have been hijacked!"
+ if session.Account != nil {
+ account, _ := GetAccountByID(app.DB, session.Account.ID)
+ msg += " (Account \"" + account.Username + "\")"
+ }
+ app.Log.Warn(log.TYPE_ACCOUNT, msg)
+ err = DeleteSession(app.DB, session.Token)
+ if err != nil {
+ app.Log.Warn(log.TYPE_ACCOUNT, "Failed to delete affected session")
+ }
+ return nil, nil
+ }
}
}
diff --git a/controller/totp.go b/controller/totp.go
index 88f6bc3..076d3a1 100644
--- a/controller/totp.go
+++ b/controller/totp.go
@@ -1,20 +1,20 @@
package controller
import (
- "arimelody-web/model"
- "crypto/hmac"
- "crypto/rand"
- "crypto/sha1"
- "encoding/base32"
- "encoding/binary"
- "fmt"
- "math"
- "net/url"
- "os"
- "strings"
- "time"
+ "arimelody-web/model"
+ "crypto/hmac"
+ "crypto/rand"
+ "crypto/sha1"
+ "encoding/base32"
+ "encoding/binary"
+ "fmt"
+ "math"
+ "net/url"
+ "os"
+ "strings"
+ "time"
- "github.com/jmoiron/sqlx"
+ "github.com/jmoiron/sqlx"
)
const TOTP_SECRET_LENGTH = 32
diff --git a/controller/track.go b/controller/track.go
index fa4efc1..ee7581c 100644
--- a/controller/track.go
+++ b/controller/track.go
@@ -1,9 +1,9 @@
package controller
import (
- "arimelody-web/model"
+ "arimelody-web/model"
- "github.com/jmoiron/sqlx"
+ "github.com/jmoiron/sqlx"
)
// DATABASE
@@ -13,19 +13,19 @@ func GetTrack(db *sqlx.DB, id string) (*model.Track, error) {
stmt, _ := db.Preparex("SELECT * FROM musictrack WHERE id=$1")
err := stmt.Get(&track, id)
- if err != nil {
+ if err != nil {
return nil, err
- }
+ }
return &track, nil
}
func GetAllTracks(db *sqlx.DB) ([]*model.Track, error) {
var tracks = []*model.Track{}
- err := db.Select(&tracks, "SELECT * FROM musictrack")
- if err != nil {
+ err := db.Select(&tracks, "SELECT * FROM musictrack")
+ if err != nil {
return nil, err
- }
+ }
return tracks, nil
}
@@ -33,33 +33,33 @@ func GetAllTracks(db *sqlx.DB) ([]*model.Track, error) {
func GetOrphanTracks(db *sqlx.DB) ([]*model.Track, error) {
var tracks = []*model.Track{}
- err := db.Select(&tracks, "SELECT * FROM musictrack WHERE id NOT IN (SELECT track FROM musicreleasetrack)")
- if err != nil {
+ err := db.Select(&tracks, "SELECT * FROM musictrack WHERE id NOT IN (SELECT track FROM musicreleasetrack)")
+ if err != nil {
return nil, err
- }
+ }
return tracks, nil
}
func GetTracksNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Track, error) {
- var tracks = []*model.Track{}
+ var tracks = []*model.Track{}
- err := db.Select(&tracks,
+ err := db.Select(&tracks,
"SELECT * FROM musictrack "+
"WHERE id NOT IN "+
"(SELECT track FROM musicreleasetrack WHERE release=$1)",
releaseID)
- if err != nil {
- return nil, err
- }
+ if err != nil {
+ return nil, err
+ }
- return tracks, nil
+ return tracks, nil
}
func GetTrackReleases(db *sqlx.DB, trackID string, full bool) ([]*model.Release, error) {
var releases = []*model.Release{}
- err := db.Select(&releases,
+ err := db.Select(&releases,
"SELECT id,title,type,release_date,artwork,buylink "+
"FROM musicrelease "+
"JOIN musicreleasetrack ON release=id "+
@@ -67,9 +67,9 @@ func GetTrackReleases(db *sqlx.DB, trackID string, full bool) ([]*model.Release,
"ORDER BY release_date",
trackID,
)
- if err != nil {
+ if err != nil {
return nil, err
- }
+ }
type NamePrimary struct {
Name string `json:"name"`
@@ -114,14 +114,14 @@ func GetTrackReleases(db *sqlx.DB, trackID string, full bool) ([]*model.Release,
func PullOrphanTracks(db *sqlx.DB) ([]*model.Track, error) {
var tracks = []*model.Track{}
- err := db.Select(&tracks,
+ err := db.Select(&tracks,
"SELECT id, title, description, lyrics, preview_url FROM musictrack "+
"WHERE id NOT IN "+
"(SELECT track FROM musicreleasetrack)",
)
- if err != nil {
+ if err != nil {
return nil, err
- }
+ }
return tracks, nil
}
diff --git a/cursor/cursor.go b/cursor/cursor.go
index 4ed59e3..56edb56 100644
--- a/cursor/cursor.go
+++ b/cursor/cursor.go
@@ -1,16 +1,16 @@
package cursor
import (
- "arimelody-web/model"
- "fmt"
- "math/rand"
- "net/http"
- "strconv"
- "strings"
- "sync"
- "time"
+ "arimelody-web/model"
+ "fmt"
+ "math/rand"
+ "net/http"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
- "github.com/gorilla/websocket"
+ "github.com/gorilla/websocket"
)
type CursorClient struct {
diff --git a/discord/discord.go b/discord/discord.go
index d46f32d..0eb9b97 100644
--- a/discord/discord.go
+++ b/discord/discord.go
@@ -1,13 +1,13 @@
package discord
import (
- "arimelody-web/model"
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "net/url"
- "strings"
+ "arimelody-web/model"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
)
const API_ENDPOINT = "https://discord.com/api/v10"
diff --git a/log/log.go b/log/log.go
index 2d1c0c2..88d328b 100644
--- a/log/log.go
+++ b/log/log.go
@@ -1,11 +1,11 @@
package log
import (
- "fmt"
- "os"
- "time"
+ "fmt"
+ "os"
+ "time"
- "github.com/jmoiron/sqlx"
+ "github.com/jmoiron/sqlx"
)
type (
diff --git a/main.go b/main.go
index c0f6ee2..23c7dc4 100644
--- a/main.go
+++ b/main.go
@@ -1,33 +1,33 @@
package main
import (
- "bufio"
- "errors"
- "fmt"
- stdLog "log"
- "math"
- "math/rand"
- "net"
- "net/http"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "time"
+ "bufio"
+ "errors"
+ "fmt"
+ stdLog "log"
+ "math"
+ "math/rand"
+ "net"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
- "arimelody-web/admin"
- "arimelody-web/api"
- "arimelody-web/colour"
- "arimelody-web/controller"
- "arimelody-web/cursor"
- "arimelody-web/log"
- "arimelody-web/model"
- "arimelody-web/templates"
- "arimelody-web/view"
+ "arimelody-web/admin"
+ "arimelody-web/api"
+ "arimelody-web/colour"
+ "arimelody-web/controller"
+ "arimelody-web/cursor"
+ "arimelody-web/log"
+ "arimelody-web/model"
+ "arimelody-web/templates"
+ "arimelody-web/view"
- "github.com/jmoiron/sqlx"
- _ "github.com/lib/pq"
- "golang.org/x/crypto/bcrypt"
+ "github.com/jmoiron/sqlx"
+ _ "github.com/lib/pq"
+ "golang.org/x/crypto/bcrypt"
)
// used for database migrations
@@ -282,7 +282,7 @@ func main() {
account.ID,
email,
account.CreatedAt,
- account.Locked,
+ account.Locked,
)
}
return
diff --git a/model/account.go b/model/account.go
index ad65b82..67424b7 100644
--- a/model/account.go
+++ b/model/account.go
@@ -1,23 +1,23 @@
package model
import (
- "database/sql"
- "time"
+ "database/sql"
+ "time"
)
const COOKIE_TOKEN string = "AM_SESSION"
const MAX_LOGIN_FAIL_ATTEMPTS int = 3
type (
- Account struct {
- ID string `json:"id" db:"id"`
- Username string `json:"username" db:"username"`
- Password string `json:"password" db:"password"`
- Email sql.NullString `json:"email" db:"email"`
- AvatarURL sql.NullString `json:"avatar_url" db:"avatar_url"`
- CreatedAt time.Time `json:"created_at" db:"created_at"`
- FailAttempts int `json:"fail_attempts" db:"fail_attempts"`
- Locked bool `json:"locked" db:"locked"`
+ Account struct {
+ ID string `json:"id" db:"id"`
+ Username string `json:"username" db:"username"`
+ Password string `json:"password" db:"password"`
+ Email sql.NullString `json:"email" db:"email"`
+ AvatarURL sql.NullString `json:"avatar_url" db:"avatar_url"`
+ CreatedAt time.Time `json:"created_at" db:"created_at"`
+ FailAttempts int `json:"fail_attempts" db:"fail_attempts"`
+ Locked bool `json:"locked" db:"locked"`
Privileges []AccountPrivilege `json:"privileges"`
}
diff --git a/model/appstate.go b/model/appstate.go
index 233e0db..a910a29 100644
--- a/model/appstate.go
+++ b/model/appstate.go
@@ -1,7 +1,7 @@
package model
import (
- "github.com/jmoiron/sqlx"
+ "github.com/jmoiron/sqlx"
"arimelody-web/log"
)
diff --git a/model/artist.go b/model/artist.go
index 63871c7..746a7dd 100644
--- a/model/artist.go
+++ b/model/artist.go
@@ -1,17 +1,17 @@
package model
type (
- Artist struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Website string `json:"website"`
- Avatar string `json:"avatar"`
- }
+ Artist struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Website string `json:"website"`
+ Avatar string `json:"avatar"`
+ }
)
func (artist Artist) GetAvatar() string {
- if artist.Avatar == "" {
- return "/img/default-avatar.png"
- }
- return artist.Avatar
+ if artist.Avatar == "" {
+ return "/img/default-avatar.png"
+ }
+ return artist.Avatar
}
diff --git a/model/link.go b/model/link.go
index 1a5bb8f..ba83e22 100644
--- a/model/link.go
+++ b/model/link.go
@@ -1,16 +1,16 @@
package model
import (
- "regexp"
- "strings"
+ "regexp"
+ "strings"
)
type Link struct {
- Name string `json:"name"`
- URL string `json:"url"`
+ Name string `json:"name"`
+ URL string `json:"url"`
}
func (link Link) NormaliseName() string {
- rgx := regexp.MustCompile(`[^a-z0-9\-]`)
- return rgx.ReplaceAllString(strings.ToLower(link.Name), "")
+ rgx := regexp.MustCompile(`[^a-z0-9\-]`)
+ return rgx.ReplaceAllString(strings.ToLower(link.Name), "")
}
diff --git a/model/release.go b/model/release.go
index afaacca..e64317b 100644
--- a/model/release.go
+++ b/model/release.go
@@ -1,9 +1,9 @@
package model
import (
- "html/template"
- "strings"
- "time"
+ "html/template"
+ "strings"
+ "time"
)
type (
@@ -73,23 +73,23 @@ func (release Release) GetUniqueArtistNames(only_primary bool) []string {
names = append(names, credit.Artist.Name)
}
- return names
+ return names
}
func (release Release) PrintArtists(only_primary bool, ampersand bool) string {
names := release.GetUniqueArtistNames(only_primary)
- if len(names) == 0 {
- return "Unknown Artist"
- } else if len(names) == 1 {
- return names[0]
- }
+ if len(names) == 0 {
+ return "Unknown Artist"
+ } else if len(names) == 1 {
+ return names[0]
+ }
- if ampersand {
- res := strings.Join(names[:len(names)-1], ", ")
- res += " & " + names[len(names)-1]
- return res
- } else {
- return strings.Join(names[:], ", ")
- }
+ if ampersand {
+ res := strings.Join(names[:len(names)-1], ", ")
+ res += " & " + names[len(names)-1]
+ return res
+ } else {
+ return strings.Join(names[:], ", ")
+ }
}
diff --git a/model/release_test.go b/model/release_test.go
index 11a58a1..b0ddaf5 100644
--- a/model/release_test.go
+++ b/model/release_test.go
@@ -1,8 +1,8 @@
package model
import (
- "testing"
- "time"
+ "testing"
+ "time"
)
func Test_Release_DescriptionHTML(t *testing.T) {
diff --git a/model/session.go b/model/session.go
index de016e1..7382de3 100644
--- a/model/session.go
+++ b/model/session.go
@@ -1,8 +1,8 @@
package model
import (
- "database/sql"
- "time"
+ "database/sql"
+ "time"
)
type Session struct {
diff --git a/model/totp.go b/model/totp.go
index cfad10a..108dae5 100644
--- a/model/totp.go
+++ b/model/totp.go
@@ -1,7 +1,7 @@
package model
import (
- "time"
+ "time"
)
type TOTP struct {
diff --git a/model/track.go b/model/track.go
index ca54ddd..deaf086 100644
--- a/model/track.go
+++ b/model/track.go
@@ -1,20 +1,20 @@
package model
import (
- "html/template"
- "strings"
+ "html/template"
+ "strings"
)
type (
- Track struct {
- ID string `json:"id"`
- Title string `json:"title"`
- Description string `json:"description"`
+ Track struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ Description string `json:"description"`
Lyrics string `json:"lyrics" db:"lyrics"`
- PreviewURL string `json:"previewURL" db:"preview_url"`
+ PreviewURL string `json:"previewURL" db:"preview_url"`
Number int
- }
+ }
)
func (track Track) GetDescriptionHTML() template.HTML {
diff --git a/templates/templates.go b/templates/templates.go
index 8d1a5ca..752c78d 100644
--- a/templates/templates.go
+++ b/templates/templates.go
@@ -1,8 +1,8 @@
package templates
import (
- "html/template"
- "path/filepath"
+ "html/template"
+ "path/filepath"
)
var IndexTemplate = template.Must(template.ParseFiles(
diff --git a/view/music.go b/view/music.go
index dfe884e..8ed5279 100644
--- a/view/music.go
+++ b/view/music.go
@@ -1,13 +1,13 @@
package view
import (
- "fmt"
- "net/http"
- "os"
+ "fmt"
+ "net/http"
+ "os"
- "arimelody-web/controller"
- "arimelody-web/model"
- "arimelody-web/templates"
+ "arimelody-web/controller"
+ "arimelody-web/model"
+ "arimelody-web/templates"
)
// HTTP HANDLER METHODS
From 37fa1f4fa878fbd21cbe4fbb0bcc6933c2194203 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 29 Apr 2025 23:42:08 +0100
Subject: [PATCH 07/57] and she never dealt with indentation issues ever again
---
.editorconfig | 7 +++++++
main.go | 4 ++--
2 files changed, 9 insertions(+), 2 deletions(-)
create mode 100644 .editorconfig
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..a882442
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
diff --git a/main.go b/main.go
index 23c7dc4..29539ac 100644
--- a/main.go
+++ b/main.go
@@ -223,7 +223,7 @@ func main() {
code := controller.GenerateTOTP(totp.Secret, 0)
fmt.Printf("%s\n", code)
return
-
+
case "cleanTOTP":
err := controller.DeleteUnconfirmedTOTPs(app.DB)
if err != nil {
@@ -346,7 +346,7 @@ func main() {
if !strings.HasPrefix(res, "y") {
return
}
-
+
err = controller.DeleteAccount(app.DB, account.ID)
if err != nil {
fmt.Fprintf(os.Stderr, "FATAL: Failed to delete account: %v\n", err)
From 76cf1bb0d53c5f394e69860bb1325b0e6a3551bc Mon Sep 17 00:00:00 2001
From: ari melody
Date: Wed, 30 Apr 2025 18:21:47 +0100
Subject: [PATCH 08/57] log IP address for account locks :troll:
---
admin/http.go | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/admin/http.go b/admin/http.go
index 76eb5f2..245a152 100644
--- a/admin/http.go
+++ b/admin/http.go
@@ -283,7 +283,7 @@ func loginHandler(app *model.AppState) http.Handler {
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password))
if err != nil {
app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(app, r))
- if locked := handleFailedLogin(app, account); locked {
+ if locked := handleFailedLogin(app, account, r); locked {
controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
} else {
controller.SetSessionError(app.DB, session, "Invalid username or password.")
@@ -389,7 +389,7 @@ func loginTOTPHandler(app *model.AppState) http.Handler {
}
if totpMethod == nil {
app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Incorrect TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r))
- if locked := handleFailedLogin(app, session.AttemptAccount); locked {
+ if locked := handleFailedLogin(app, session.AttemptAccount, r); locked {
controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
controller.SetSessionAttemptAccount(app.DB, session, nil)
http.Redirect(w, r, "/admin", http.StatusFound)
@@ -514,7 +514,7 @@ func enforceSession(app *model.AppState, next http.Handler) http.Handler {
})
}
-func handleFailedLogin(app *model.AppState, account *model.Account) bool {
+func handleFailedLogin(app *model.AppState, account *model.Account, r *http.Request) bool {
locked, err := controller.IncrementAccountFails(app.DB, account.ID)
if err != nil {
fmt.Fprintf(
@@ -532,9 +532,10 @@ func handleFailedLogin(app *model.AppState, account *model.Account) bool {
if locked {
app.Log.Warn(
log.TYPE_ACCOUNT,
- "Account \"%s\" was locked: %d failed login attempts",
+ "Account \"%s\" was locked: %d failed login attempts (IP: %s)",
account.Username,
model.MAX_LOGIN_FAIL_ATTEMPTS,
+ controller.ResolveIP(app, r),
)
}
return locked
From 35e149e186fbaa8f0e5f4a5429c61e5e9289eeb2 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Mon, 5 May 2025 17:54:44 +0100
Subject: [PATCH 09/57] oops
---
schema-migration/000-init.sql | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/schema-migration/000-init.sql b/schema-migration/000-init.sql
index 2174109..90385ac 100644
--- a/schema-migration/000-init.sql
+++ b/schema-migration/000-init.sql
@@ -18,9 +18,9 @@ CREATE TABLE arimelody.account (
password TEXT NOT NULL,
email TEXT,
avatar_url TEXT,
- created_at TIMESTAMP NOT NULL DEFAULT current_timestamp
+ created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
fail_attempts INT NOT NULL DEFAULT 0,
- locked BOOLEAN DEFAULT false,
+ locked BOOLEAN DEFAULT false
);
ALTER TABLE arimelody.account ADD CONSTRAINT account_pk PRIMARY KEY (id);
From 30c4252e40a5d2671ae78288a85695f855a71a7a Mon Sep 17 00:00:00 2001
From: ari melody
Date: Wed, 21 May 2025 15:49:21 +0100
Subject: [PATCH 10/57] hamburger dropdown: match theme background colour
---
public/style/header.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/public/style/header.css b/public/style/header.css
index 48971b5..531649a 100644
--- a/public/style/header.css
+++ b/public/style/header.css
@@ -154,7 +154,7 @@ header ul li a:hover {
flex-direction: column;
gap: 1rem;
border-bottom: 1px solid #888;
- background: #080808;
+ background: var(--background);
display: none;
}
From da1cd0204e37ebae2f526d6f8a8f5766c19daa2c Mon Sep 17 00:00:00 2001
From: ari melody
Date: Fri, 23 May 2025 12:23:05 +0100
Subject: [PATCH 11/57] update socials and projects
---
views/index.html | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/views/index.html b/views/index.html
index ba0ca15..f18d87a 100644
--- a/views/index.html
+++ b/views/index.html
@@ -91,14 +91,17 @@
twitch
-
- spotify
-
bandcamp
- github
+ codeberg
+
+
+ bluesky
+
+
+ discord
@@ -106,6 +109,11 @@
projects i've worked on 🛠️
+
+
+ mcstatusface
+
+
catdance
From fe84a59326f4806001fa0b09da6d535fd4fcf1a6 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Sun, 15 Jun 2025 02:51:23 +0100
Subject: [PATCH 12/57] add thermia web button
---
public/img/buttons/thermia.gif | Bin 0 -> 10184 bytes
views/index.html | 4 ++--
2 files changed, 2 insertions(+), 2 deletions(-)
create mode 100644 public/img/buttons/thermia.gif
diff --git a/public/img/buttons/thermia.gif b/public/img/buttons/thermia.gif
new file mode 100644
index 0000000000000000000000000000000000000000..a7ee70ae2577b21a6a6f29d8a20e7ce4477bfcbf
GIT binary patch
literal 10184
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
-
-
+
+
From 69e2e22e47c452151ba686ed6528b10a145b3227 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Mon, 16 Jun 2025 20:32:46 +0100
Subject: [PATCH 13/57] homepage rework for socials and projects
---
controller/qr.go | 67 --------------
public/img/brand/bandcamp.svg | 10 +++
public/img/brand/bluesky.svg | 10 +++
public/img/brand/codeberg.svg | 164 ++++++++++++++++++++++++++++++++++
public/img/brand/discord.svg | 1 +
public/img/brand/twitch.svg | 21 +++++
public/img/brand/youtube.svg | 10 +++
public/script/index.js | 7 ++
public/script/main.js | 17 ++++
public/script/music.js | 12 +--
public/style/index.css | 88 ++++++++++++++++--
views/index.html | 162 ++++++++++++++++++++++-----------
12 files changed, 432 insertions(+), 137 deletions(-)
create mode 100644 public/img/brand/bandcamp.svg
create mode 100644 public/img/brand/bluesky.svg
create mode 100644 public/img/brand/codeberg.svg
create mode 100644 public/img/brand/discord.svg
create mode 100644 public/img/brand/twitch.svg
create mode 100644 public/img/brand/youtube.svg
diff --git a/controller/qr.go b/controller/qr.go
index 6b04e69..dd08637 100644
--- a/controller/qr.go
+++ b/controller/qr.go
@@ -1,13 +1,9 @@
package controller
import (
- "bytes"
"encoding/base64"
- "errors"
- "fmt"
"image"
"image/color"
- "image/png"
"github.com/skip2/go-qrcode"
)
@@ -33,69 +29,6 @@ const (
HIGH
)
-func noDepsGenerateQRCode() (string, error) {
- version := 1
-
- size := 0
- size = 21 + version * 4
- if version > 10 {
- return "", errors.New(fmt.Sprintf("QR version %d not supported", version))
- }
-
- img := image.NewGray(image.Rect(0, 0, size + margin * 2, size + margin * 2))
-
- // fill white
- for y := range size + margin * 2 {
- for x := range size + margin * 2 {
- img.Set(x, y, color.White)
- }
- }
-
- // draw alignment squares
- drawLargeAlignmentSquare(margin, margin, img)
- drawLargeAlignmentSquare(margin, margin + size - 7, img)
- drawLargeAlignmentSquare(margin + size - 7, margin, img)
- drawSmallAlignmentSquare(size - 5, size - 5, img)
- /*
- if version > 4 {
- space := version * 3 - 2
- end := size / space
- for y := range size / space + 1 {
- for x := range size / space + 1 {
- if x == 0 && y == 0 { continue }
- if x == 0 && y == end { continue }
- if x == end && y == 0 { continue }
- if x == end && y == end { continue }
- drawSmallAlignmentSquare(
- x * space + margin + 4,
- y * space + margin + 4,
- img,
- )
- }
- }
- }
- */
-
- // draw timing bits
- for i := margin + 6; i < size - 4; i++ {
- if (i % 2 == 0) {
- img.Set(i, margin + 6, color.Black)
- img.Set(margin + 6, i, color.Black)
- }
- }
- img.Set(margin + 8, size - 4, color.Black)
-
- var imgBuf bytes.Buffer
- err := png.Encode(&imgBuf, img)
- if err != nil {
- return "", err
- }
-
- base64Img := base64.StdEncoding.EncodeToString(imgBuf.Bytes())
-
- return "data:image/png;base64," + base64Img, nil
-}
-
func drawLargeAlignmentSquare(x int, y int, img *image.Gray) {
for yi := range 7 {
for xi := range 7 {
diff --git a/public/img/brand/bandcamp.svg b/public/img/brand/bandcamp.svg
new file mode 100644
index 0000000..9623ec2
--- /dev/null
+++ b/public/img/brand/bandcamp.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/public/img/brand/bluesky.svg b/public/img/brand/bluesky.svg
new file mode 100644
index 0000000..d77fafe
--- /dev/null
+++ b/public/img/brand/bluesky.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/public/img/brand/codeberg.svg b/public/img/brand/codeberg.svg
new file mode 100644
index 0000000..028b729
--- /dev/null
+++ b/public/img/brand/codeberg.svg
@@ -0,0 +1,164 @@
+
+
+ Codeberg logo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+ Codeberg logo
+
+
+
+ Robert Martinez
+
+
+
+
+ Codeberg and the Codeberg Logo are trademarks of Codeberg e.V.
+
+
+ 2020-04-09
+
+
+ Codeberg e.V.
+
+
+ codeberg.org
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/img/brand/discord.svg b/public/img/brand/discord.svg
new file mode 100644
index 0000000..b636d15
--- /dev/null
+++ b/public/img/brand/discord.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/brand/twitch.svg b/public/img/brand/twitch.svg
new file mode 100644
index 0000000..3120fea
--- /dev/null
+++ b/public/img/brand/twitch.svg
@@ -0,0 +1,21 @@
+
+
+
+
+Asset 2
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/img/brand/youtube.svg b/public/img/brand/youtube.svg
new file mode 100644
index 0000000..3286071
--- /dev/null
+++ b/public/img/brand/youtube.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/public/script/index.js b/public/script/index.js
index 512ed8f..2197bd4 100644
--- a/public/script/index.js
+++ b/public/script/index.js
@@ -1,3 +1,5 @@
+import { hijackClickEvent } from "./main.js";
+
const hexPrimary = document.getElementById("hex-primary");
const hexSecondary = document.getElementById("hex-secondary");
const hexTertiary = document.getElementById("hex-tertiary");
@@ -14,3 +16,8 @@ updateHexColours();
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
updateHexColours();
});
+
+document.querySelectorAll("ul#projects li.project-item").forEach(projectItem => {
+ const link = projectItem.querySelector('a');
+ hijackClickEvent(projectItem, link);
+});
diff --git a/public/script/main.js b/public/script/main.js
index c1d5101..19eddc2 100644
--- a/public/script/main.js
+++ b/public/script/main.js
@@ -45,6 +45,23 @@ function fill_list(list) {
});
}
+export function hijackClickEvent(container, link) {
+ container.addEventListener('click', event => {
+ if (event.target.tagName.toLowerCase() === 'a') return;
+ event.preventDefault();
+ link.dispatchEvent(new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ ctrlKey: event.ctrlKey,
+ metaKey: event.metaKey,
+ shiftKey: event.shiftKey,
+ altKey: event.altKey,
+ button: event.button,
+ }));
+ });
+}
+
document.addEventListener("DOMContentLoaded", () => {
[...document.querySelectorAll(".typeout")]
.filter((e) => e.innerText != "")
diff --git a/public/script/music.js b/public/script/music.js
index 273ce2b..91f34ab 100644
--- a/public/script/music.js
+++ b/public/script/music.js
@@ -1,12 +1,6 @@
-import "./main.js";
+import { hijackClickEvent } from "./main.js";
document.querySelectorAll("div.music").forEach(container => {
- const link = container.querySelector(".music-title a").href
-
- container.addEventListener("click", event => {
- if (event.target.href) return;
-
- event.preventDefault();
- location = link;
- });
+ const link = container.querySelector(".music-title a")
+ hijackClickEvent(container, link);
});
diff --git a/public/style/index.css b/public/style/index.css
index f3cb761..6edb362 100644
--- a/public/style/index.css
+++ b/public/style/index.css
@@ -96,30 +96,36 @@ hr {
overflow: visible;
}
-ul.links {
+ul.platform-links {
+ padding-left: 1em;
display: flex;
- gap: 1em .5em;
+ gap: .5em;
flex-wrap: wrap;
}
-ul.links li {
+ul.platform-links li {
list-style: none;
}
-ul.links li a {
+ul.platform-links li a {
padding: .4em .5em;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ gap: .5em;
border: 1px solid var(--links);
color: var(--links);
border-radius: 2px;
background-color: transparent;
- transition-property: color, border-color, background-color;
+ transition-property: color, border-color, background-color, box-shadow;
transition-duration: .2s;
animation-delay: 0s;
animation: list-item-fadein .2s forwards;
opacity: 0;
}
-ul.links li a:hover {
+ul.platform-links li a:hover {
color: #eee;
border-color: #eee;
background-color: var(--links) !important;
@@ -127,6 +133,75 @@ ul.links li a:hover {
box-shadow: 0 0 1em var(--links);
}
+ul.platform-links li a img {
+ height: 1em;
+ width: 1em;
+}
+
+ul#projects {
+ padding: 0;
+ list-style: none;
+}
+
+li.project-item {
+ padding: .5em;
+ border: 1px solid var(--links);
+ margin: 1em 0;
+ display: flex;
+ flex-direction: row;
+ gap: .5em;
+ border-radius: 2px;
+ transition-property: color, border-color, background-color, box-shadow;
+ transition-duration: .2s;
+ cursor: pointer;
+}
+li.project-item a {
+ transition: color .2s linear;
+}
+
+li.project-item:hover {
+ color: #eee;
+ border-color: #eee;
+ background-color: var(--links) !important;
+ text-decoration: none;
+ box-shadow: 0 0 1em var(--links);
+}
+li.project-item:hover a {
+ color: #eee;
+}
+
+li.project-item .project-info {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+li.project-item img.project-icon {
+ width: 2.5em;
+ height: 2.5em;
+ object-fit: cover;
+ border-radius: 2px;
+}
+
+li.project-item span.project-icon {
+ font-size: 2em;
+ display: block;
+ width: 45px;
+ height: 45px;
+ text-align: center;
+ /* background: #0004; */
+ /* border: 1px solid var(--on-background); */
+ border-radius: 2px;
+}
+
+li.project-item a {
+ text-decoration: none;
+}
+
+li.project-item p {
+ margin: 0;
+}
+
div#web-buttons {
margin: 2rem 0;
}
@@ -147,4 +222,3 @@ div#web-buttons {
transform: translate(-2px, -2px);
box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee;
}
-
diff --git a/views/index.html b/views/index.html
index 070d648..f639c55 100644
--- a/views/index.html
+++ b/views/index.html
@@ -17,7 +17,7 @@
-
+
{{end}}
{{define "content"}}
@@ -33,7 +33,7 @@
- i'm a musician , developer ,
+ i'm a musician , developer ,
streamer , youtuber ,
and probably a bunch of other things i forgot to mention!
@@ -44,7 +44,7 @@
if you're looking to support me financially, that's so cool of you!!
if you like, you can buy some of my music over on
- bandcamp
+ bandcamp
so you can at least get something for your money.
thank you very much either way!! 💕
@@ -84,65 +84,119 @@
where to find me 🛰️
+
+
projects i've worked on 🛠️
-
-
-
- mcstatusface
-
+
@@ -156,40 +210,40 @@
-
+
-
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -205,16 +259,16 @@
-
+
-
+
-
+
From f7b3faf8e89b240ca1e65eb698734039fcae9ce3 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Mon, 16 Jun 2025 22:41:54 +0100
Subject: [PATCH 14/57] move source link from header to footer
---
public/style/header.css | 5 ++---
views/footer.html | 7 ++++++-
views/header.html | 3 ---
3 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/public/style/header.css b/public/style/header.css
index 531649a..f399d4d 100644
--- a/public/style/header.css
+++ b/public/style/header.css
@@ -25,7 +25,6 @@ nav {
flex-grow: 1;
display: flex;
gap: .5em;
- cursor: pointer;
}
img#header-icon {
@@ -36,19 +35,19 @@ img#header-icon {
}
#header-text {
- width: 11em;
display: flex;
flex-direction: column;
justify-content: center;
- flex-grow: 1;
}
#header-text h1 {
+ width: fit-content;
margin: 0;
font-size: 1em;
}
#header-text h2 {
+ width: fit-content;
height: 1.2em;
line-height: 1.2em;
margin: 0;
diff --git a/views/footer.html b/views/footer.html
index 217c4b5..eccf125 100644
--- a/views/footer.html
+++ b/views/footer.html
@@ -2,7 +2,12 @@
diff --git a/views/header.html b/views/header.html
index 291b8ac..03a8384 100644
--- a/views/header.html
+++ b/views/header.html
@@ -23,9 +23,6 @@
music
-
- source
-
blog
From 9274796729a56290d788106e0ed5327643103e4a Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 17 Jun 2025 01:15:08 +0100
Subject: [PATCH 15/57] early implementation of ari melody LIVE tracker
---
controller/config.go | 4 ++
controller/twitch.go | 97 ++++++++++++++++++++++++++++++++++++++++
main.go | 47 ++++---------------
model/appstate.go | 8 ++++
model/twitch.go | 43 ++++++++++++++++++
public/style/colours.css | 1 +
public/style/index.css | 81 +++++++++++++++++++++++++++++++++
view/index.go | 45 +++++++++++++++++++
view/static.go | 31 +++++++++++++
views/index.html | 17 +++++++
10 files changed, 335 insertions(+), 39 deletions(-)
create mode 100644 controller/twitch.go
create mode 100644 model/twitch.go
create mode 100644 view/index.go
create mode 100644 view/static.go
diff --git a/controller/config.go b/controller/config.go
index 1d3cbbb..fdfa756 100644
--- a/controller/config.go
+++ b/controller/config.go
@@ -77,5 +77,9 @@ func handleConfigOverrides(config *model.Config) error {
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 }
+ if env, has := os.LookupEnv("ARIMELODY_TWITCH_BROADCASTER"); has { config.Twitch.Broadcaster = env }
+ if env, has := os.LookupEnv("ARIMELODY_TWITCH_CLIENT_ID"); has { config.Twitch.ClientID = env }
+ if env, has := os.LookupEnv("ARIMELODY_TWITCH_SECRET"); has { config.Twitch.Secret = env }
+
return nil
}
diff --git a/controller/twitch.go b/controller/twitch.go
new file mode 100644
index 0000000..f7dc509
--- /dev/null
+++ b/controller/twitch.go
@@ -0,0 +1,97 @@
+package controller
+
+import (
+ "arimelody-web/model"
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+const TWITCH_API_BASE = "https://api.twitch.tv/helix/"
+
+func TwitchSetup(app *model.AppState) error {
+ app.Twitch = &model.TwitchState{}
+ err := RefreshTwitchToken(app)
+ return err
+}
+
+func RefreshTwitchToken(app *model.AppState) error {
+ if app.Twitch != nil && app.Twitch.Token != nil && time.Now().UTC().After(app.Twitch.Token.ExpiresAt) {
+ return nil
+ }
+
+ requestUrl, _ := url.Parse("https://id.twitch.tv/oauth2/token")
+ req, _ := http.NewRequest(http.MethodPost, requestUrl.String(), bytes.NewBuffer([]byte(url.Values{
+ "client_id": []string{ app.Config.Twitch.ClientID },
+ "client_secret": []string{ app.Config.Twitch.Secret },
+ "grant_type": []string{ "client_credentials" },
+ }.Encode())))
+
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ type TwitchOAuthToken struct {
+ AccessToken string `json:"access_token"`
+ ExpiresIn int `json:"expires_in"`
+ TokenType string `json:"token_type"`
+ }
+ oauthResponse := TwitchOAuthToken{}
+ err = json.NewDecoder(res.Body).Decode(&oauthResponse)
+ if err != nil {
+ return err
+ }
+
+ app.Twitch.Token = &model.TwitchOAuthToken{
+ AccessToken: oauthResponse.AccessToken,
+ ExpiresAt: time.Now().UTC().Add(time.Second * time.Duration(oauthResponse.ExpiresIn)).UTC(),
+ TokenType: oauthResponse.TokenType,
+ }
+
+ return nil
+}
+
+var lastStreamState *model.TwitchStreamInfo
+var lastStreamStateAt time.Time
+
+func GetTwitchStatus(app *model.AppState, broadcaster string) (*model.TwitchStreamInfo, error) {
+ if lastStreamState != nil && time.Now().UTC().Before(lastStreamStateAt.Add(time.Minute)) {
+ return lastStreamState, nil
+ }
+
+ fmt.Print("MAKING COSTLY REQUEST TO TWITCH.TV API...\n")
+
+ requestUrl, _ := url.Parse(TWITCH_API_BASE + "streams")
+ requestUrl.RawQuery = url.Values{
+ "user_login": []string{ broadcaster },
+ }.Encode()
+ req, _ := http.NewRequest(http.MethodGet, requestUrl.String(), nil)
+ req.Header.Set("Client-Id", app.Config.Twitch.ClientID)
+ req.Header.Set("Authorization", "Bearer " + app.Twitch.Token.AccessToken)
+
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ type StreamsResponse struct {
+ Data []model.TwitchStreamInfo `json:"data"`
+ }
+ streamInfo := StreamsResponse{}
+ err = json.NewDecoder(res.Body).Decode(&streamInfo)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(streamInfo.Data) == 0 {
+ return nil, nil
+ }
+
+ lastStreamState = &streamInfo.Data[0]
+ lastStreamStateAt = time.Now().UTC()
+ return lastStreamState, nil
+}
diff --git a/main.go b/main.go
index 29539ac..8f37639 100644
--- a/main.go
+++ b/main.go
@@ -22,7 +22,6 @@ import (
"arimelody-web/cursor"
"arimelody-web/log"
"arimelody-web/model"
- "arimelody-web/templates"
"arimelody-web/view"
"github.com/jmoiron/sqlx"
@@ -40,6 +39,7 @@ func main() {
app := model.AppState{
Config: controller.GetConfig(),
+ Twitch: nil,
}
// initialise database connection
@@ -460,6 +460,11 @@ func main() {
// handle DB migrations
controller.CheckDBVersionAndMigrate(app.DB)
+ err = controller.TwitchSetup(&app)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Failed to set up Twitch integration: %v\n", err)
+ }
+
// initial invite code
accountsCount := 0
err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account")
@@ -511,49 +516,13 @@ func createServeMux(app *model.AppState) *http.ServeMux {
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app)))
mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app)))
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app)))
- mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(app.Config.DataDirectory, "uploads"))))
+ mux.Handle("/uploads/", http.StripPrefix("/uploads", view.StaticHandler(filepath.Join(app.Config.DataDirectory, "uploads"))))
mux.Handle("/cursor-ws", cursor.Handler(app))
- 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.IndexTemplate.Execute(w, nil)
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- }
- return
- }
- staticHandler("public").ServeHTTP(w, r)
- }))
+ mux.Handle("/", view.IndexHandler(app))
return mux
}
-func staticHandler(directory string) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
-
- // does the file exist?
- if err != nil {
- if errors.Is(err, os.ErrNotExist) {
- http.NotFound(w, r)
- return
- }
- }
-
- // is thjs a directory? (forbidden)
- if info.IsDir() {
- http.NotFound(w, r)
- return
- }
-
- http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
- })
-}
-
var PoweredByStrings = []string{
"nerd rage",
"estrogen",
diff --git a/model/appstate.go b/model/appstate.go
index a910a29..861a991 100644
--- a/model/appstate.go
+++ b/model/appstate.go
@@ -21,6 +21,12 @@ type (
Secret string `toml:"secret"`
}
+ TwitchConfig struct {
+ Broadcaster string `toml:"broadcaster"`
+ ClientID string `toml:"client_id"`
+ Secret string `toml:"secret"`
+ }
+
Config struct {
BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."`
Host string `toml:"host"`
@@ -29,11 +35,13 @@ type (
TrustedProxies []string `toml:"trusted_proxies"`
DB DBConfig `toml:"db"`
Discord DiscordConfig `toml:"discord"`
+ Twitch TwitchConfig `toml:"twitch"`
}
AppState struct {
DB *sqlx.DB
Config Config
Log log.Logger
+ Twitch *TwitchState
}
)
diff --git a/model/twitch.go b/model/twitch.go
new file mode 100644
index 0000000..6bca17d
--- /dev/null
+++ b/model/twitch.go
@@ -0,0 +1,43 @@
+package model
+
+import (
+ "fmt"
+ "strings"
+ "time"
+)
+
+type (
+ TwitchOAuthToken struct {
+ AccessToken string
+ ExpiresAt time.Time
+ TokenType string
+ }
+
+ TwitchState struct {
+ Token *TwitchOAuthToken
+ }
+
+ TwitchStreamInfo struct {
+ ID string `json:"id"`
+ UserID string `json:"user_id"`
+ UserLogin string `json:"user_login"`
+ UserName string `json:"user_name"`
+ GameID string `json:"game_id"`
+ GameName string `json:"game_name"`
+ Type string `json:"type"`
+ Title string `json:"title"`
+ ViewerCount int `json:"viewer_count"`
+ StartedAt string `json:"started_at"`
+ Language string `json:"language"`
+ ThumbnailURL string `json:"thumbnail_url"`
+ TagIDs []string `json:"tag_ids"`
+ Tags []string `json:"tags"`
+ IsMature bool `json:"is_mature"`
+ }
+)
+
+func (info *TwitchStreamInfo) Thumbnail(width int, height int) string {
+ res := strings.Replace(info.ThumbnailURL, "{width}", fmt.Sprintf("%d", width), 1)
+ res = strings.Replace(res, "{height}", fmt.Sprintf("%d", height), 1)
+ return res
+}
diff --git a/public/style/colours.css b/public/style/colours.css
index 2bf607d..de63198 100644
--- a/public/style/colours.css
+++ b/public/style/colours.css
@@ -6,6 +6,7 @@
--secondary: #f8e05b;
--tertiary: #f788fe;
--links: #5eb2ff;
+ --live: #fd3737;
}
@media (prefers-color-scheme: light) {
diff --git a/public/style/index.css b/public/style/index.css
index 6edb362..f60fdb7 100644
--- a/public/style/index.css
+++ b/public/style/index.css
@@ -222,3 +222,84 @@ div#web-buttons {
transform: translate(-2px, -2px);
box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee;
}
+
+#live-banner {
+ margin: 1em 0 2em 0;
+ padding: 1em;
+ border-radius: 4px;
+ border: 1px solid var(--primary);
+ box-shadow: 0 0 8px var(--primary);
+}
+
+#live-banner h2 {
+ margin: 0 0 .4em 0;
+ color: var(--on-background);
+}
+
+#live-banner p {
+ margin: 0;
+}
+
+.live-highlight {
+ color: var(--primary);
+}
+
+.live-preview {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ gap: 1em;
+}
+
+.live-preview div:first-of-type {
+ text-align: center;
+}
+
+.live-thumbnail {
+ border-radius: 4px;
+}
+
+.live-button {
+ margin: .2em;
+ padding: .4em .5em;
+ display: inline-block;
+ color: var(--primary);
+ border: 1px solid var(--primary);
+ border-radius: 4px;
+ transition: color .1s linear, background-color .1s linear, box-shadow .1s linear;
+}
+
+.live-button:hover {
+ color: var(--background);
+ background-color: var(--primary);
+ box-shadow: 0 0 8px var(--primary);
+ text-decoration: none;
+}
+
+.live-info {
+ display: flex;
+ flex-direction: column;
+ gap: .3em;
+ overflow-x: hidden;
+}
+
+.live-game {
+ overflow: hidden;
+ text-wrap: nowrap;
+ text-overflow: ellipsis;
+}
+
+.live-game .live-game-prefix {
+ opacity: .8;
+}
+
+.live-title {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.live-viewers {
+ opacity: .5;
+}
diff --git a/view/index.go b/view/index.go
new file mode 100644
index 0000000..995de44
--- /dev/null
+++ b/view/index.go
@@ -0,0 +1,45 @@
+package view
+
+import (
+ "arimelody-web/controller"
+ "arimelody-web/model"
+ "arimelody-web/templates"
+ "fmt"
+ "net/http"
+ "os"
+)
+
+func IndexHandler(app *model.AppState) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ type IndexData struct {
+ TwitchStatus *model.TwitchStreamInfo
+ }
+
+ var err error
+ var twitchStatus *model.TwitchStreamInfo = nil
+ if len(app.Config.Twitch.Broadcaster) > 0 {
+ twitchStatus, err = controller.GetTwitchStatus(app, app.Config.Twitch.Broadcaster)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Failed to get Twitch status for %s: %v\n", app.Config.Twitch.Broadcaster, err)
+ }
+ }
+
+ if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+ err := templates.IndexTemplate.Execute(w, IndexData{
+ TwitchStatus: twitchStatus,
+ })
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Failed to render index page: %v\n", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ }
+ return
+ }
+
+ StaticHandler("public").ServeHTTP(w, r)
+ })
+}
diff --git a/view/static.go b/view/static.go
new file mode 100644
index 0000000..52263a2
--- /dev/null
+++ b/view/static.go
@@ -0,0 +1,31 @@
+package view
+
+import (
+ "errors"
+ "net/http"
+ "os"
+ "path/filepath"
+)
+
+func StaticHandler(directory string) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
+
+ // does the file exist?
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ http.NotFound(w, r)
+ return
+ }
+ }
+
+ // is thjs a directory? (forbidden)
+ if info.IsDir() {
+ http.NotFound(w, r)
+ return
+ }
+
+ http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
+ })
+}
+
diff --git a/views/index.html b/views/index.html
index f639c55..98a862c 100644
--- a/views/index.html
+++ b/views/index.html
@@ -22,6 +22,23 @@
{{define "content"}}
+ {{if .TwitchStatus}}
+
+
ari is LIVE right now!
+
+
+
+
streaming: {{.TwitchStatus.GameName}}
+
{{.TwitchStatus.Title}}
+
{{.TwitchStatus.ViewerCount}} viewers
+
+
+
+ {{end}}
+
# hello, world!
From 581273370de2029c8258564ab2a180eea93f0a0d Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 17 Jun 2025 02:01:06 +0100
Subject: [PATCH 16/57] improvements to LIVE tracker
---
controller/twitch.go | 3 ---
main.go | 8 +++++---
model/appstate.go | 4 ++--
public/style/index.css | 41 +++++++++++++++++++++++++++++++++++------
public/style/main.css | 1 +
view/index.go | 2 +-
views/index.html | 2 +-
7 files changed, 45 insertions(+), 16 deletions(-)
diff --git a/controller/twitch.go b/controller/twitch.go
index f7dc509..98d3b9a 100644
--- a/controller/twitch.go
+++ b/controller/twitch.go
@@ -4,7 +4,6 @@ import (
"arimelody-web/model"
"bytes"
"encoding/json"
- "fmt"
"net/http"
"net/url"
"time"
@@ -63,8 +62,6 @@ func GetTwitchStatus(app *model.AppState, broadcaster string) (*model.TwitchStre
return lastStreamState, nil
}
- fmt.Print("MAKING COSTLY REQUEST TO TWITCH.TV API...\n")
-
requestUrl, _ := url.Parse(TWITCH_API_BASE + "streams")
requestUrl.RawQuery = url.Values{
"user_login": []string{ broadcaster },
diff --git a/main.go b/main.go
index 8f37639..0234d79 100644
--- a/main.go
+++ b/main.go
@@ -460,9 +460,11 @@ func main() {
// handle DB migrations
controller.CheckDBVersionAndMigrate(app.DB)
- err = controller.TwitchSetup(&app)
- if err != nil {
- fmt.Fprintf(os.Stderr, "WARN: Failed to set up Twitch integration: %v\n", err)
+ if app.Config.Twitch != nil {
+ err = controller.TwitchSetup(&app)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Failed to set up Twitch integration: %v\n", err)
+ }
}
// initial invite code
diff --git a/model/appstate.go b/model/appstate.go
index 861a991..3a1c230 100644
--- a/model/appstate.go
+++ b/model/appstate.go
@@ -34,8 +34,8 @@ type (
DataDirectory string `toml:"data_dir"`
TrustedProxies []string `toml:"trusted_proxies"`
DB DBConfig `toml:"db"`
- Discord DiscordConfig `toml:"discord"`
- Twitch TwitchConfig `toml:"twitch"`
+ Discord *DiscordConfig `toml:"discord"`
+ Twitch *TwitchConfig `toml:"twitch"`
}
AppState struct {
diff --git a/public/style/index.css b/public/style/index.css
index f60fdb7..6efe241 100644
--- a/public/style/index.css
+++ b/public/style/index.css
@@ -231,11 +231,6 @@ div#web-buttons {
box-shadow: 0 0 8px var(--primary);
}
-#live-banner h2 {
- margin: 0 0 .4em 0;
- color: var(--on-background);
-}
-
#live-banner p {
margin: 0;
}
@@ -252,7 +247,11 @@ div#web-buttons {
}
.live-preview div:first-of-type {
- text-align: center;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ gap: .3em;
}
.live-thumbnail {
@@ -283,6 +282,36 @@ div#web-buttons {
overflow-x: hidden;
}
+#live-banner h2 {
+ margin: 0;
+ color: var(--on-background);
+ font-family: 'Inter', sans-serif;
+ font-weight: 800;
+ font-style: italic;
+}
+
+.live-pinger {
+ width: .5em;
+ height: .5em;
+ margin: .1em .2em;
+ display: inline-block;
+ border-radius: 100%;
+ background-color: var(--primary);
+ box-shadow: 0 0 4px var(--primary);
+ animation: live-pinger-pulse 1s infinite alternate ease-in-out;
+}
+
+@keyframes live-pinger-pulse {
+ from {
+ opacity: .8;
+ transform: scale(1.0);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1.1);
+ }
+}
+
.live-game {
overflow: hidden;
text-wrap: nowrap;
diff --git a/public/style/main.css b/public/style/main.css
index 4e9e113..680ee2b 100644
--- a/public/style/main.css
+++ b/public/style/main.css
@@ -3,6 +3,7 @@
@import url("/style/footer.css");
@import url("/style/prideflag.css");
@import url("/style/cursor.css");
+@import url("/font/inter/inter.css");
@font-face {
font-family: "Monaspace Argon";
diff --git a/view/index.go b/view/index.go
index 995de44..b6e3891 100644
--- a/view/index.go
+++ b/view/index.go
@@ -22,7 +22,7 @@ func IndexHandler(app *model.AppState) http.Handler {
var err error
var twitchStatus *model.TwitchStreamInfo = nil
- if len(app.Config.Twitch.Broadcaster) > 0 {
+ if app.Twitch != nil && len(app.Config.Twitch.Broadcaster) > 0 {
twitchStatus, err = controller.GetTwitchStatus(app, app.Config.Twitch.Broadcaster)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to get Twitch status for %s: %v\n", app.Config.Twitch.Broadcaster, err)
diff --git a/views/index.html b/views/index.html
index 98a862c..6985f6c 100644
--- a/views/index.html
+++ b/views/index.html
@@ -24,13 +24,13 @@
{{if .TwitchStatus}}
-
ari is LIVE right now!
+
ari melody LIVE
streaming: {{.TwitchStatus.GameName}}
{{.TwitchStatus.Title}}
{{.TwitchStatus.ViewerCount}} viewers
From 375ae84ae33d352421dffcfbe44bbbdff32a3b11 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Sun, 22 Jun 2025 18:00:08 +0100
Subject: [PATCH 17/57] update some arimelody.me references to .space
---
views/index.html | 30 ++++--------------------------
1 file changed, 4 insertions(+), 26 deletions(-)
diff --git a/views/index.html b/views/index.html
index 6985f6c..37bd2a3 100644
--- a/views/index.html
+++ b/views/index.html
@@ -6,8 +6,8 @@
-
-
+
+
@@ -101,28 +101,6 @@
where to find me 🛰️
-
-
+
-
+
diff --git a/views/music-gateway.html b/views/music-gateway.html
index 0f4441c..9007c02 100644
--- a/views/music-gateway.html
+++ b/views/music-gateway.html
@@ -6,22 +6,22 @@
-
+
-
+
-
-
+
+
-
+
@@ -34,7 +34,7 @@
-
<
+
<
diff --git a/views/music.html b/views/music.html
index 6f84672..51f712d 100644
--- a/views/music.html
+++ b/views/music.html
@@ -6,8 +6,8 @@
-
-
+
+
@@ -72,7 +72,7 @@
music used: ari melody - free2play
- https://arimelody.me/music/free2play
+ https://arimelody.space/music/free2play
licensed under CC BY-SA 4.0 .
From c63a090569d6ef8d12743f802b0361e22c4422cd Mon Sep 17 00:00:00 2001
From: ari melody
Date: Sat, 16 Aug 2025 22:35:49 +0100
Subject: [PATCH 24/57] fix HTTPLog panic with no User-Agent
---
main.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/main.go b/main.go
index 53f2883..edd4c87 100644
--- a/main.go
+++ b/main.go
@@ -626,6 +626,6 @@ func HTTPLog(next http.Handler) http.Handler {
lrw.Status,
colour.Reset,
elapsed,
- r.Header["User-Agent"][0])
+ r.Header.Get("User-Agent"))
})
}
From 5a330ad7fa631ea331dbf02846262acabbd9ab3e Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 19 Aug 2025 15:22:59 +0100
Subject: [PATCH 25/57] fix some opengraph
---
views/index.html | 2 +-
views/music-gateway.html | 6 +++---
views/music.html | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/views/index.html b/views/index.html
index 23f40ea..23764e1 100644
--- a/views/index.html
+++ b/views/index.html
@@ -8,7 +8,7 @@
-
+
diff --git a/views/music-gateway.html b/views/music-gateway.html
index 9007c02..febef4d 100644
--- a/views/music-gateway.html
+++ b/views/music-gateway.html
@@ -9,8 +9,8 @@
-
-
+
+
@@ -19,7 +19,7 @@
-
+
diff --git a/views/music.html b/views/music.html
index 51f712d..e0a5110 100644
--- a/views/music.html
+++ b/views/music.html
@@ -8,7 +8,7 @@
-
+
From c82709084b32837956a86d2aa0ccdc4031f4dabf Mon Sep 17 00:00:00 2001
From: ari melody
Date: Wed, 20 Aug 2025 12:41:55 +0100
Subject: [PATCH 26/57] add quick security check to requests
---
main.go | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
diff --git a/main.go b/main.go
index edd4c87..9133958 100644
--- a/main.go
+++ b/main.go
@@ -515,7 +515,7 @@ func main() {
fmt.Printf("Now serving at http://%s:%d\n", app.Config.Host, app.Config.Port)
stdLog.Fatal(
http.ListenAndServe(fmt.Sprintf("%s:%d", app.Config.Host, app.Config.Port),
- HTTPLog(DefaultHeaders(mux)),
+ CheckRequest(&app, HTTPLog(DefaultHeaders(mux))),
))
}
@@ -562,6 +562,37 @@ var PoweredByStrings = []string{
"30 billion dollars in VC funding",
}
+func CheckRequest(app *model.AppState, next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // requests with empty user agents are considered suspicious.
+ // every browser supplies them; hell, even curl supplies them.
+ // i only ever see null user-agents paired with malicious requests,
+ // so i'm canning them altogether.
+ if len(r.Header.Get("User-Agent")) == 0 {
+ http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+ return
+ }
+
+ // same with .php and awkward double-slash requests.
+ // obviously these don't affect me, but these tend to be lazy intrusion
+ // attempts. if that's what you're about, i don't want you on my site.
+ if strings.HasPrefix(r.URL.Path, "//") ||
+ strings.HasSuffix(r.URL.Path, ".php") ||
+ strings.HasSuffix(r.URL.Path, ".php7") {
+ http.NotFound(w, r)
+ fmt.Fprintf(
+ os.Stderr,
+ "WARN: Suspicious activity blocked: {\"path\":\"%s\",\"address\":\"%s\"}\n",
+ r.URL.Path,
+ r.RemoteAddr,
+ )
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+}
+
func DefaultHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Server", "ari melody webbed site")
From d13cfc74ad7c1f8d68c11adeeaf633fae38f9f4e Mon Sep 17 00:00:00 2001
From: ari melody
Date: Fri, 22 Aug 2025 01:06:37 +0100
Subject: [PATCH 27/57] =?UTF-8?q?complete=20arimelody.space=20migration!?=
=?UTF-8?q?=20=F0=9F=8E=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 11 +---
docker-compose.example.yml | 2 +-
public/keys/ari melody_0x92678188_public.asc | 26 --------
public/keys/ari@arimelody.space_public.asc | 66 ++++++++++++++++++++
views/index.html | 4 +-
views/music.html | 2 +-
6 files changed, 73 insertions(+), 38 deletions(-)
delete mode 100644 public/keys/ari melody_0x92678188_public.asc
create mode 100644 public/keys/ari@arimelody.space_public.asc
diff --git a/README.md b/README.md
index 75b2095..f1fd392 100644
--- a/README.md
+++ b/README.md
@@ -4,14 +4,9 @@ home to your local SPACEGIRL! 💫
---
-built up from the initial [static](https://forge.arimelody.space/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!
+a slightly-overcomplicated webserver built to show off everything i've worked
+on, and then some! this server comes complete with twitch live status tracking,
+a portfolio database, and a full-fledged admin CMS panel to manage it all!
## build
diff --git a/docker-compose.example.yml b/docker-compose.example.yml
index 62843b9..5ba8cfa 100644
--- a/docker-compose.example.yml
+++ b/docker-compose.example.yml
@@ -1,6 +1,6 @@
services:
web:
- image: docker.arimelody.space/arimelody.me:latest
+ image: docker.arimelody.space/arimelody-web:latest
build: .
ports:
- 8080:8080
diff --git a/public/keys/ari melody_0x92678188_public.asc b/public/keys/ari melody_0x92678188_public.asc
deleted file mode 100644
index 80a4676..0000000
--- a/public/keys/ari melody_0x92678188_public.asc
+++ /dev/null
@@ -1,26 +0,0 @@
------BEGIN PGP PUBLIC KEY BLOCK-----
-
-mDMEZNW03RYJKwYBBAHaRw8BAQdAuMUNVjXT7m/YisePPnSYY6lc1Xmm3oS79ZEO
-JriRCZy0HWFyaSBtZWxvZHkgPGFyaUBhcmltZWxvZHkubWU+iJkEExYKAEECGwMF
-CwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AWIQTujeuNYocuegkeKt/PmYKckmeB
-iAUCZ7UqUAUJCIMP8wAKCRDPmYKckmeBiO/NAP0SoJL4aKZqCeYiSoDF/Uw6nMmZ
-+oR1Uig41wQ/IDbhCAEApP2vbjSIu6pcp0AQlL7qcoyPWv+XkqPSFqW9KEZZVwqI
-kwQTFgoAOxYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJk1bTdAhsDBQsJCAcCAiIC
-BhUKCQgLAgQWAgMBAh4HAheAAAoJEM+ZgpySZ4GIYJsA/jBNwsJTlmV9JMmsW0aF
-ApYDoPG1Q7sJ6CRW7xKCRjcqAQDX9iqNnW9Jqo8M3jXfu+aGSF926hg6M3SKm02P
-f27bAbgzBGe1JooWCSsGAQQB2kcPAQEHQJbfh5iLHEpZndMgekqYzqTrUoAJ8ZIL
-d4WH0dcw9tOaiPUEGBYKACYCGwIWIQTujeuNYocuegkeKt/PmYKckmeBiAUCZ7Uq
-VgUJBaOeTACBdiAEGRYKAB0WIQQlu5dWmBR/P3ZxngxgtfA4bj3bfgUCZ7UmigAK
-CRBgtfA4bj3bfux+AP4y5ydrjnGBMX7GuB2nh55SRdscSiXsZ66ntnjXyQcbWgEA
-pDuu7FqXzXcnluuZxNFDT740Rnzs60tTeplDqGGWcAQJEM+ZgpySZ4GIc0kA/iSw
-Nw+r3FC75omwrPpJF13B5fq93FweFx+oSaES6qzkAQDvgCK77qKKbvCju0g8zSsK
-EZnv6xR4uvtGdVkvLpBdC7gzBGe1JpkWCSsGAQQB2kcPAQEHQGnU4lXFLchhKYkC
-PshP+jvuRsNoedaDOK2p4dkQC8JuiH4EGBYKACYCGyAWIQTujeuNYocuegkeKt/P
-mYKckmeBiAUCZ7UqXgUJBaOeRQAKCRDPmYKckmeBiL9KAQCJZIBhuSsoYa61I0XZ
-cKzGZbB0h9pD6eg1VRswNIgHtQEAwu9Hgs1rs9cySvKbO7WgK6Qh6EfrvGgGOXCO
-m3wVsg24OARntSo5EgorBgEEAZdVAQUBAQdA+/k586W1OHxndzDJNpbd+wqjyjr0
-D5IXxfDs00advB0DAQgHiH4EGBYKACYWIQTujeuNYocuegkeKt/PmYKckmeBiAUC
-Z7UqOQIbDAUJBaOagAAKCRDPmYKckmeBiEFxAQCgziQt2l3u7jnZVij4zop+K2Lv
-TVFtkbG61tf6brRzBgD/X6c6X5BRyQC51JV1I1RFRBdeMAIXzcLFg2v3WUMccQs=
-=YmHI
------END PGP PUBLIC KEY BLOCK-----
diff --git a/public/keys/ari@arimelody.space_public.asc b/public/keys/ari@arimelody.space_public.asc
new file mode 100644
index 0000000..4323eba
--- /dev/null
+++ b/public/keys/ari@arimelody.space_public.asc
@@ -0,0 +1,66 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEZNW03RYJKwYBBAHaRw8BAQdAuMUNVjXT7m/YisePPnSYY6lc1Xmm3oS79ZEO
+JriRCZy0IGFyaSBtZWxvZHkgPGFyaUBhcmltZWxvZHkuc3BhY2U+iQJJBBMWCgHx
+AhsDBQkIgw/zBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAhkBFiEE7o3rjWKH
+LnoJHirfz5mCnJJngYgFAmino5w1FIAAAAAAEAAccHJvb2ZAYXJpYWRuZS5pZGRu
+czphcmltZWxvZHkuc3BhY2U/dHlwZT1UWFQ6FIAAAAAAEAAhcHJvb2ZAYXJpYWRu
+ZS5pZGh0dHBzOi8vZmVkaS5hcmltZWxvZHkuc3BhY2UvQGFyaUQUgAAAAAAQACtw
+cm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9mb3JnZS5ibGlzcy50b3duL2FyaS9rZXlv
+eGlkZS1wcm9vZkkUgAAAAAAQADBwcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9mb3Jn
+ZS5hcmltZWxvZHkuc3BhY2UvYXJpL2tleW94aWRlLXByb29mRhSAAAAAABAALXBy
+b29mQGFyaWFkbmUuaWRodHRwczovL2NvZGViZXJnLm9yZy9hcmltZWxvZHkva2V5
+b3hpZGUtcHJvb2ZlFIAAAAAAEABMcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vYnNr
+eS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBuZ2l6cnk1dW16a3hyMy9w
+b3N0LzNsaWlucW90cXRjMjIACgkQz5mCnJJngYjDpQEAgFn3bXcxw3xF0dwrSURh
+qpciMY31bkQy9eDMSKcbloIA/1hX1MnUKETdiAtrrK08z4udIXaJr52E5D7IAZk1
+pZwBtB1hcmkgbWVsb2R5IDxhcmlAYXJpbWVsb2R5Lm1lPoiTBBMWCgA7FiEE7o3r
+jWKHLnoJHirfz5mCnJJngYgFAmTVtN0CGwMFCwkIBwICIgIGFQoJCAsCBBYCAwEC
+HgcCF4AACgkQz5mCnJJngYhgmwD+ME3CwlOWZX0kyaxbRoUClgOg8bVDuwnoJFbv
+EoJGNyoBANf2Ko2db0mqjwzeNd+75oZIX3bqGDozdIqbTY9/btsBiQIKBBMWCgGy
+AhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheABQkIgw/zFiEE7o3rjWKHLnoJ
+Hirfz5mCnJJngYgFAme1wUA2FIAAAAAAEAAdcHJvb2ZAYXJpYWRuZS5pZGh0dHBz
+Oi8vaWNlLmFyaW1lbG9keS5tZS9AYXJpWxSAAAAAABAAQnByb29mQGFyaWFkbmUu
+aWRodHRwczovL2dpc3QuZ2l0aHViLmNvbS9hcmltZWxvZHkvMzY2ZGMyYjZhYWVk
+ZWMxOWU2MTRiN2NlY2U5Yzg2OWQyFIAAAAAAEAAZcHJvb2ZAYXJpYWRuZS5pZGRu
+czphcmltZWxvZHkubWU/dHlwZT1UWFRlFIAAAAAAEABMcHJvb2ZAYXJpYWRuZS5p
+ZGh0dHBzOi8vYnNreS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBuZ2l6
+cnk1dW16a3hyMy9wb3N0LzNsaWlucW90cXRjMjJEFIAAAAAAEAArcHJvb2ZAYXJp
+YWRuZS5pZGh0dHBzOi8vZ2l0LmFyaW1lbG9keS5tZS9hcmkva2V5b3hpZGVfcHJv
+b2YACgkQz5mCnJJngYh3+QD+Pbo3bM4oWtUicGUGEp4jiFoBqSNlyl9rFPY0ODDS
+DxEBANaXz/No/Hn3mEwNdrFigj/YPm7TH/4UBbHAxN6hDggPiQJGBBMWCgHuAhsD
+BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheABQkIgw/zFiEE7o3rjWKHLnoJHirf
+z5mCnJJngYgFAmino5VJFIAAAAAAEAAwcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8v
+Zm9yZ2UuYXJpbWVsb2R5LnNwYWNlL2FyaS9rZXlveGlkZS1wcm9vZkYUgAAAAAAQ
+AC1wcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9jb2RlYmVyZy5vcmcvYXJpbWVsb2R5
+L2tleW94aWRlLXByb29mOhSAAAAAABAAIXByb29mQGFyaWFkbmUuaWRodHRwczov
+L2ZlZGkuYXJpbWVsb2R5LnNwYWNlL0BhcmllFIAAAAAAEABMcHJvb2ZAYXJpYWRu
+ZS5pZGh0dHBzOi8vYnNreS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBu
+Z2l6cnk1dW16a3hyMy9wb3N0LzNsaWlucW90cXRjMjI1FIAAAAAAEAAccHJvb2ZA
+YXJpYWRuZS5pZGRuczphcmltZWxvZHkuc3BhY2U/dHlwZT1UWFREFIAAAAAAEAAr
+cHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vZm9yZ2UuYmxpc3MudG93bi9hcmkva2V5
+b3hpZGUtcHJvb2YACgkQz5mCnJJngYgKNQD/UA2THttICUvz2p5cbPlJIm/QStRE
+6crttsTeFSsyocgBAPDXpkdssPNNnxxVvCNATTTxiS08Cy+xxQVrjWztjlUCuDME
+Z7UmihYJKwYBBAHaRw8BAQdAlt+HmIscSlmd0yB6SpjOpOtSgAnxkgt3hYfR1zD2
+05qI9QQYFgoAJgIbAhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntSpWBQkFo55M
+AIF2IAQZFgoAHRYhBCW7l1aYFH8/dnGeDGC18DhuPdt+BQJntSaKAAoJEGC18Dhu
+Pdt+7H4A/jLnJ2uOcYExfsa4HaeHnlJF2xxKJexnrqe2eNfJBxtaAQCkO67sWpfN
+dyeW65nE0UNPvjRGfOzrS1N6mUOoYZZwBAkQz5mCnJJngYhzSQD+JLA3D6vcULvm
+ibCs+kkXXcHl+r3cXB4XH6hJoRLqrOQBAO+AIrvuoopu8KO7SDzNKwoRme/rFHi6
++0Z1WS8ukF0LuDMEZ7UmmRYJKwYBBAHaRw8BAQdAadTiVcUtyGEpiQI+yE/6O+5G
+w2h51oM4ranh2RALwm6IfgQYFgoAJgIbIBYhBO6N641ihy56CR4q38+ZgpySZ4GI
+BQJntSpeBQkFo55FAAoJEM+ZgpySZ4GIv0oBAIlkgGG5KyhhrrUjRdlwrMZlsHSH
+2kPp6DVVGzA0iAe1AQDC70eCzWuz1zJK8ps7taArpCHoR+u8aAY5cI6bfBWyDbg4
+BGe1KjkSCisGAQQBl1UBBQEBB0D7+TnzpbU4fGd3MMk2lt37CqPKOvQPkhfF8OzT
+Rp28HQMBCAeIfgQYFgoAJhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntSo5AhsM
+BQkFo5qAAAoJEM+ZgpySZ4GIQXEBAKDOJC3aXe7uOdlWKPjOin4rYu9NUW2RsbrW
+1/putHMGAP9fpzpfkFHJALnUlXUjVEVEF14wAhfNwsWDa/dZQxxxC7g4BGTVtN0S
+CisGAQQBl1UBBQEBB0CcDZ2s/NAGhc13AisWei+4XQKNf7z7xBK6AIXhrlkRcQMB
+CAeIeAQoFgoAIBYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntT6fAh0DAAoJEM+Z
+gpySZ4GIgX8A/1d8CZFSRB0TRU8h6ijTS1+O2bKJ0uwydfQHL5b3fA4OAQDOU6eG
+Ml82IKGhbFoJl7wm5X4+l5+lNqwZymNoZjVhBIh4BBgWCgAgFiEE7o3rjWKHLnoJ
+Hirfz5mCnJJngYgFAmTVtN0CGwwACgkQz5mCnJJngYgv8QEA9YbuFnLLUeNJZFMT
+KoWeOMJos6wwPnhgnYexntxsu/cBAMd/ORp2KDaZTEwOAUxrO6K1eFkn0pKAcdPq
+cdVDnsIL
+=Mzcq
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/views/index.html b/views/index.html
index 23764e1..6d0af2c 100644
--- a/views/index.html
+++ b/views/index.html
@@ -66,7 +66,7 @@
for anything else, you can reach me for any and all communications through
- ari@arimelody.me . if your message
+ ari@arimelody.space . if your message
contains anything beyond a silly gag, i strongly recommend encrypting
your message using my public pgp key, listed below!
@@ -93,7 +93,7 @@
my keys 🔑
diff --git a/views/music.html b/views/music.html
index e0a5110..e7b4bd9 100644
--- a/views/music.html
+++ b/views/music.html
@@ -84,7 +84,7 @@
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
+ > ari@arimelody.space
From 6bd0b5ec0e853d13af0f4b8ed7827d7944ee0efc Mon Sep 17 00:00:00 2001
From: ari melody
Date: Wed, 27 Aug 2025 18:42:30 +0100
Subject: [PATCH 28/57] update pgp key
---
public/keys/ari@arimelody.space_public.asc | 77 ++++++++++------------
1 file changed, 35 insertions(+), 42 deletions(-)
diff --git a/public/keys/ari@arimelody.space_public.asc b/public/keys/ari@arimelody.space_public.asc
index 4323eba..9e6fea2 100644
--- a/public/keys/ari@arimelody.space_public.asc
+++ b/public/keys/ari@arimelody.space_public.asc
@@ -2,19 +2,19 @@
mDMEZNW03RYJKwYBBAHaRw8BAQdAuMUNVjXT7m/YisePPnSYY6lc1Xmm3oS79ZEO
JriRCZy0IGFyaSBtZWxvZHkgPGFyaUBhcmltZWxvZHkuc3BhY2U+iQJJBBMWCgHx
-AhsDBQkIgw/zBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAhkBFiEE7o3rjWKH
-LnoJHirfz5mCnJJngYgFAmino5w1FIAAAAAAEAAccHJvb2ZAYXJpYWRuZS5pZGRu
-czphcmltZWxvZHkuc3BhY2U/dHlwZT1UWFQ6FIAAAAAAEAAhcHJvb2ZAYXJpYWRu
-ZS5pZGh0dHBzOi8vZmVkaS5hcmltZWxvZHkuc3BhY2UvQGFyaUQUgAAAAAAQACtw
-cm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9mb3JnZS5ibGlzcy50b3duL2FyaS9rZXlv
-eGlkZS1wcm9vZkkUgAAAAAAQADBwcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9mb3Jn
-ZS5hcmltZWxvZHkuc3BhY2UvYXJpL2tleW94aWRlLXByb29mRhSAAAAAABAALXBy
-b29mQGFyaWFkbmUuaWRodHRwczovL2NvZGViZXJnLm9yZy9hcmltZWxvZHkva2V5
-b3hpZGUtcHJvb2ZlFIAAAAAAEABMcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vYnNr
-eS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBuZ2l6cnk1dW16a3hyMy9w
-b3N0LzNsaWlucW90cXRjMjIACgkQz5mCnJJngYjDpQEAgFn3bXcxw3xF0dwrSURh
-qpciMY31bkQy9eDMSKcbloIA/1hX1MnUKETdiAtrrK08z4udIXaJr52E5D7IAZk1
-pZwBtB1hcmkgbWVsb2R5IDxhcmlAYXJpbWVsb2R5Lm1lPoiTBBMWCgA7FiEE7o3r
+AhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAhkBNRSAAAAAABAAHHByb29m
+QGFyaWFkbmUuaWRkbnM6YXJpbWVsb2R5LnNwYWNlP3R5cGU9VFhUOhSAAAAAABAA
+IXByb29mQGFyaWFkbmUuaWRodHRwczovL2ZlZGkuYXJpbWVsb2R5LnNwYWNlL0Bh
+cmlEFIAAAAAAEAArcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vZm9yZ2UuYmxpc3Mu
+dG93bi9hcmkva2V5b3hpZGUtcHJvb2ZJFIAAAAAAEAAwcHJvb2ZAYXJpYWRuZS5p
+ZGh0dHBzOi8vZm9yZ2UuYXJpbWVsb2R5LnNwYWNlL2FyaS9rZXlveGlkZS1wcm9v
+ZkYUgAAAAAAQAC1wcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9jb2RlYmVyZy5vcmcv
+YXJpbWVsb2R5L2tleW94aWRlLXByb29mZRSAAAAAABAATHByb29mQGFyaWFkbmUu
+aWRodHRwczovL2Jza3kuYXBwL3Byb2ZpbGUvZGlkOnBsYzp5Y3Q2Y3ZnZmlwbmdp
+enJ5NXVtemt4cjMvcG9zdC8zbGlpbnFvdHF0YzIyFiEE7o3rjWKHLnoJHirfz5mC
+nJJngYgFAmivQJsFCQ0/jT4ACgkQz5mCnJJngYgdtQD+K8AMkLvR1ZKxl0tw8/FO
+vwS9HknEW13GajSAY/W1/NoA/17mnVnlTFhepKo1ETnxe2BpdOaKR85K0n2qffzC
+8SAAtB1hcmkgbWVsb2R5IDxhcmlAYXJpbWVsb2R5Lm1lPoiTBBMWCgA7FiEE7o3r
jWKHLnoJHirfz5mCnJJngYgFAmTVtN0CGwMFCwkIBwICIgIGFQoJCAsCBBYCAwEC
HgcCF4AACgkQz5mCnJJngYhgmwD+ME3CwlOWZX0kyaxbRoUClgOg8bVDuwnoJFbv
EoJGNyoBANf2Ko2db0mqjwzeNd+75oZIX3bqGDozdIqbTY9/btsBiQIKBBMWCgGy
@@ -29,38 +29,31 @@ cnk1dW16a3hyMy9wb3N0LzNsaWlucW90cXRjMjJEFIAAAAAAEAArcHJvb2ZAYXJp
YWRuZS5pZGh0dHBzOi8vZ2l0LmFyaW1lbG9keS5tZS9hcmkva2V5b3hpZGVfcHJv
b2YACgkQz5mCnJJngYh3+QD+Pbo3bM4oWtUicGUGEp4jiFoBqSNlyl9rFPY0ODDS
DxEBANaXz/No/Hn3mEwNdrFigj/YPm7TH/4UBbHAxN6hDggPiQJGBBMWCgHuAhsD
-BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheABQkIgw/zFiEE7o3rjWKHLnoJHirf
-z5mCnJJngYgFAmino5VJFIAAAAAAEAAwcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8v
-Zm9yZ2UuYXJpbWVsb2R5LnNwYWNlL2FyaS9rZXlveGlkZS1wcm9vZkYUgAAAAAAQ
-AC1wcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9jb2RlYmVyZy5vcmcvYXJpbWVsb2R5
-L2tleW94aWRlLXByb29mOhSAAAAAABAAIXByb29mQGFyaWFkbmUuaWRodHRwczov
-L2ZlZGkuYXJpbWVsb2R5LnNwYWNlL0BhcmllFIAAAAAAEABMcHJvb2ZAYXJpYWRu
-ZS5pZGh0dHBzOi8vYnNreS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBu
-Z2l6cnk1dW16a3hyMy9wb3N0LzNsaWlucW90cXRjMjI1FIAAAAAAEAAccHJvb2ZA
-YXJpYWRuZS5pZGRuczphcmltZWxvZHkuc3BhY2U/dHlwZT1UWFREFIAAAAAAEAAr
-cHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vZm9yZ2UuYmxpc3MudG93bi9hcmkva2V5
-b3hpZGUtcHJvb2YACgkQz5mCnJJngYgKNQD/UA2THttICUvz2p5cbPlJIm/QStRE
-6crttsTeFSsyocgBAPDXpkdssPNNnxxVvCNATTTxiS08Cy+xxQVrjWztjlUCuDME
+BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheASRSAAAAAABAAMHByb29mQGFyaWFk
+bmUuaWRodHRwczovL2ZvcmdlLmFyaW1lbG9keS5zcGFjZS9hcmkva2V5b3hpZGUt
+cHJvb2ZGFIAAAAAAEAAtcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vY29kZWJlcmcu
+b3JnL2FyaW1lbG9keS9rZXlveGlkZS1wcm9vZjoUgAAAAAAQACFwcm9vZkBhcmlh
+ZG5lLmlkaHR0cHM6Ly9mZWRpLmFyaW1lbG9keS5zcGFjZS9AYXJpZRSAAAAAABAA
+THByb29mQGFyaWFkbmUuaWRodHRwczovL2Jza3kuYXBwL3Byb2ZpbGUvZGlkOnBs
+Yzp5Y3Q2Y3ZnZmlwbmdpenJ5NXVtemt4cjMvcG9zdC8zbGlpbnFvdHF0YzIyNRSA
+AAAAABAAHHByb29mQGFyaWFkbmUuaWRkbnM6YXJpbWVsb2R5LnNwYWNlP3R5cGU9
+VFhURBSAAAAAABAAK3Byb29mQGFyaWFkbmUuaWRodHRwczovL2ZvcmdlLmJsaXNz
+LnRvd24vYXJpL2tleW94aWRlLXByb29mFiEE7o3rjWKHLnoJHirfz5mCnJJngYgF
+AmivQJsFCQ0/jT4ACgkQz5mCnJJngYhk/wEAuQMYpUgyLqcYvOh1A+7f/t+DUXjz
+YjQtLYw37oAESREA/074iJNi9GHGIjxYfp5lBkZxqGew1GAFIKx7Yzp64WQFuDME
Z7UmihYJKwYBBAHaRw8BAQdAlt+HmIscSlmd0yB6SpjOpOtSgAnxkgt3hYfR1zD2
-05qI9QQYFgoAJgIbAhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntSpWBQkFo55M
+05qI9QQYFgoAJgIbAhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJor0AjBQkKYBsZ
AIF2IAQZFgoAHRYhBCW7l1aYFH8/dnGeDGC18DhuPdt+BQJntSaKAAoJEGC18Dhu
Pdt+7H4A/jLnJ2uOcYExfsa4HaeHnlJF2xxKJexnrqe2eNfJBxtaAQCkO67sWpfN
-dyeW65nE0UNPvjRGfOzrS1N6mUOoYZZwBAkQz5mCnJJngYhzSQD+JLA3D6vcULvm
-ibCs+kkXXcHl+r3cXB4XH6hJoRLqrOQBAO+AIrvuoopu8KO7SDzNKwoRme/rFHi6
-+0Z1WS8ukF0LuDMEZ7UmmRYJKwYBBAHaRw8BAQdAadTiVcUtyGEpiQI+yE/6O+5G
+dyeW65nE0UNPvjRGfOzrS1N6mUOoYZZwBAkQz5mCnJJngYgc2gD/cFhjrwPdex9g
+ZYk7jH29wQ9RpR9dEhf0C20nFZJLawgBAOBbzw4/O7OslSoIjhGs4pw9hJIBK7ds
+PI6g3CeX0DUFuDMEZ7UmmRYJKwYBBAHaRw8BAQdAadTiVcUtyGEpiQI+yE/6O+5G
w2h51oM4ranh2RALwm6IfgQYFgoAJgIbIBYhBO6N641ihy56CR4q38+ZgpySZ4GI
-BQJntSpeBQkFo55FAAoJEM+ZgpySZ4GIv0oBAIlkgGG5KyhhrrUjRdlwrMZlsHSH
-2kPp6DVVGzA0iAe1AQDC70eCzWuz1zJK8ps7taArpCHoR+u8aAY5cI6bfBWyDbg4
+BQJor0AjBQkKYBsKAAoJEM+ZgpySZ4GIoMAA/jB/exnjGvsMKuNW09bI29bsKHNW
+SQLjEnuuByN6Spq6AP9yPUumsSEHr0W71iefMuNFJZnF+8qSk+uywQ/5ET+PBbg4
BGe1KjkSCisGAQQBl1UBBQEBB0D7+TnzpbU4fGd3MMk2lt37CqPKOvQPkhfF8OzT
-Rp28HQMBCAeIfgQYFgoAJhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntSo5AhsM
-BQkFo5qAAAoJEM+ZgpySZ4GIQXEBAKDOJC3aXe7uOdlWKPjOin4rYu9NUW2RsbrW
-1/putHMGAP9fpzpfkFHJALnUlXUjVEVEF14wAhfNwsWDa/dZQxxxC7g4BGTVtN0S
-CisGAQQBl1UBBQEBB0CcDZ2s/NAGhc13AisWei+4XQKNf7z7xBK6AIXhrlkRcQMB
-CAeIeAQoFgoAIBYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntT6fAh0DAAoJEM+Z
-gpySZ4GIgX8A/1d8CZFSRB0TRU8h6ijTS1+O2bKJ0uwydfQHL5b3fA4OAQDOU6eG
-Ml82IKGhbFoJl7wm5X4+l5+lNqwZymNoZjVhBIh4BBgWCgAgFiEE7o3rjWKHLnoJ
-Hirfz5mCnJJngYgFAmTVtN0CGwwACgkQz5mCnJJngYgv8QEA9YbuFnLLUeNJZFMT
-KoWeOMJos6wwPnhgnYexntxsu/cBAMd/ORp2KDaZTEwOAUxrO6K1eFkn0pKAcdPq
-cdVDnsIL
-=Mzcq
+Rp28HQMBCAeIfgQYFgoAJgIbDBYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJor0Ak
+BQkKYBdqAAoJEM+ZgpySZ4GIlIoA/0fv2UQyhixu7Vkq7IeQ+NxUuEVCIGmrAu6k
+ScT13ikjAQCPpIubU848yXcDUvxgcGAS7yNADU1dAWAZOi34WxajAQ==
+=caf3
-----END PGP PUBLIC KEY BLOCK-----
From 89bb46c49e434bcb97d0d799252d30bec71bb034 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Fri, 29 Aug 2025 16:29:28 +0100
Subject: [PATCH 29/57] day 1 patch
---
main.go | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/main.go b/main.go
index 9133958..f3c9f66 100644
--- a/main.go
+++ b/main.go
@@ -33,6 +33,7 @@ import (
const DB_VERSION = 1
const DEFAULT_PORT int64 = 8080
+const HRT_DATE int64 = 1756478697
func main() {
fmt.Printf("made with <3 by ari melody\n\n")
@@ -605,6 +606,10 @@ func DefaultHeaders(next http.Handler) http.Handler {
"X-Powered-By",
PoweredByStrings[rand.Intn(len(PoweredByStrings))],
)
+ w.Header().Add(
+ "X-Days-Since-HRT",
+ fmt.Sprint(math.Round(time.Since(time.Unix(HRT_DATE, 0)).Hours() / 24)),
+ )
next.ServeHTTP(w, r)
})
}
From fd4335ced470306fe7263e45ca5505a857680042 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Sun, 7 Sep 2025 16:18:20 +0100
Subject: [PATCH 30/57] update security checks
---
main.go | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/main.go b/main.go
index f3c9f66..eaed7c6 100644
--- a/main.go
+++ b/main.go
@@ -574,11 +574,10 @@ func CheckRequest(app *model.AppState, next http.Handler) http.Handler {
return
}
- // same with .php and awkward double-slash requests.
- // obviously these don't affect me, but these tend to be lazy intrusion
- // attempts. if that's what you're about, i don't want you on my site.
- if strings.HasPrefix(r.URL.Path, "//") ||
- strings.HasSuffix(r.URL.Path, ".php") ||
+ // obviously .php requests these don't affect me, but these tend to be
+ // lazy wordpress intrusion attempts. if that's what you're about, i
+ // don't want you on my site.
+ if strings.HasSuffix(r.URL.Path, ".php") ||
strings.HasSuffix(r.URL.Path, ".php7") {
http.NotFound(w, r)
fmt.Fprintf(
From b150fa491cf668174e6f41bc579b8e08a18cc786 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Wed, 10 Sep 2025 13:50:46 +0100
Subject: [PATCH 31/57] update web buttons
---
public/img/buttons/wangleline.png | Bin 0 -> 6479 bytes
views/index.html | 3 +++
2 files changed, 3 insertions(+)
create mode 100644 public/img/buttons/wangleline.png
diff --git a/public/img/buttons/wangleline.png b/public/img/buttons/wangleline.png
new file mode 100644
index 0000000000000000000000000000000000000000..b26ea3f59c6185ff6d8a1008316b4b03706c71db
GIT binary patch
literal 6479
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
+
+
+
From e5dcc4b8847b08f0fb6efb8587660a16da5955cd Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 30 Sep 2025 19:03:35 +0100
Subject: [PATCH 32/57] embed html template and static files
---
Makefile | 4 +-
admin/accounthttp.go | 29 ++--
admin/artisthttp.go | 13 +-
admin/http.go | 57 ++++----
admin/logshttp.go | 15 +-
admin/releasehttp.go | 29 ++--
admin/templates.go | 125 -----------------
.../html}/components/credits/addcredit.html | 0
.../html}/components/credits/editcredits.html | 0
.../html}/components/credits/newcredit.html | 0
.../html}/components/links/editlinks.html | 0
.../components/release/release-list-item.html | 0
.../html}/components/tracks/addtrack.html | 0
.../html}/components/tracks/edittracks.html | 0
.../html}/components/tracks/newtrack.html | 0
.../html}/edit-account.html | 0
.../html}/edit-artist.html | 0
.../html}/edit-release.html | 0
.../{views => templates/html}/edit-track.html | 0
admin/{views => templates/html}/index.html | 0
admin/{views => templates/html}/layout.html | 0
.../{views => templates/html}/login-totp.html | 0
admin/{views => templates/html}/login.html | 0
admin/{views => templates/html}/logout.html | 0
admin/{views => templates/html}/logs.html | 0
.../templates/html}/prideflag.html | 0
admin/{views => templates/html}/register.html | 0
.../html}/totp-confirm.html | 0
.../{views => templates/html}/totp-setup.html | 0
admin/templates/templates.go | 128 ++++++++++++++++++
admin/trackhttp.go | 13 +-
main.go | 55 ++++----
model/appstate.go | 7 +-
{views => templates/html}/404.html | 0
{views => templates/html}/footer.html | 0
{views => templates/html}/header.html | 0
{views => templates/html}/index.html | 0
{views => templates/html}/layout.html | 0
{views => templates/html}/music-gateway.html | 0
{views => templates/html}/music.html | 0
templates/html/prideflag.html | 21 +++
templates/templates.go | 52 ++++---
view/{static.go => files.go} | 21 ++-
view/index.go | 2 +-
44 files changed, 316 insertions(+), 255 deletions(-)
delete mode 100644 admin/templates.go
rename admin/{ => templates/html}/components/credits/addcredit.html (100%)
rename admin/{ => templates/html}/components/credits/editcredits.html (100%)
rename admin/{ => templates/html}/components/credits/newcredit.html (100%)
rename admin/{ => templates/html}/components/links/editlinks.html (100%)
rename admin/{ => templates/html}/components/release/release-list-item.html (100%)
rename admin/{ => templates/html}/components/tracks/addtrack.html (100%)
rename admin/{ => templates/html}/components/tracks/edittracks.html (100%)
rename admin/{ => templates/html}/components/tracks/newtrack.html (100%)
rename admin/{views => templates/html}/edit-account.html (100%)
rename admin/{views => templates/html}/edit-artist.html (100%)
rename admin/{views => templates/html}/edit-release.html (100%)
rename admin/{views => templates/html}/edit-track.html (100%)
rename admin/{views => templates/html}/index.html (100%)
rename admin/{views => templates/html}/layout.html (100%)
rename admin/{views => templates/html}/login-totp.html (100%)
rename admin/{views => templates/html}/login.html (100%)
rename admin/{views => templates/html}/logout.html (100%)
rename admin/{views => templates/html}/logs.html (100%)
rename {views => admin/templates/html}/prideflag.html (100%)
rename admin/{views => templates/html}/register.html (100%)
rename admin/{views => templates/html}/totp-confirm.html (100%)
rename admin/{views => templates/html}/totp-setup.html (100%)
create mode 100644 admin/templates/templates.go
rename {views => templates/html}/404.html (100%)
rename {views => templates/html}/footer.html (100%)
rename {views => templates/html}/header.html (100%)
rename {views => templates/html}/index.html (100%)
rename {views => templates/html}/layout.html (100%)
rename {views => templates/html}/music-gateway.html (100%)
rename {views => templates/html}/music.html (100%)
create mode 100644 templates/html/prideflag.html
rename view/{static.go => files.go} (54%)
diff --git a/Makefile b/Makefile
index 11e565a..f96321c 100644
--- a/Makefile
+++ b/Makefile
@@ -2,10 +2,10 @@ EXEC = arimelody-web
.PHONY: $(EXEC)
-$(EXEC):
+build:
GOOS=linux GOARCH=amd64 go build -o $(EXEC)
-bundle: $(EXEC)
+bundle: build
tar czf $(EXEC).tar.gz $(EXEC) admin/components/ admin/views/ admin/static/ views/ public/ schema-migration/
clean:
diff --git a/admin/accounthttp.go b/admin/accounthttp.go
index 945a507..b2c3b0d 100644
--- a/admin/accounthttp.go
+++ b/admin/accounthttp.go
@@ -1,17 +1,18 @@
package admin
import (
- "database/sql"
- "fmt"
- "net/http"
- "net/url"
- "os"
+ "database/sql"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
- "arimelody-web/controller"
- "arimelody-web/log"
- "arimelody-web/model"
+ "arimelody-web/admin/templates"
+ "arimelody-web/controller"
+ "arimelody-web/log"
+ "arimelody-web/model"
- "golang.org/x/crypto/bcrypt"
+ "golang.org/x/crypto/bcrypt"
)
func accountHandler(app *model.AppState) http.Handler {
@@ -64,7 +65,7 @@ func accountIndexHandler(app *model.AppState) http.Handler {
session.Message = sessionMessage
session.Error = sessionError
- err = accountTemplate.Execute(w, accountResponse{
+ err = templates.AccountTemplate.Execute(w, accountResponse{
Session: session,
TOTPs: totps,
})
@@ -184,7 +185,7 @@ func totpSetupHandler(app *model.AppState) http.Handler {
session := r.Context().Value("session").(*model.Session)
- err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session })
+ err := templates.TOTPSetupTemplate.Execute(w, totpSetupData{ Session: session })
if err != nil {
fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -221,7 +222,7 @@ func totpSetupHandler(app *model.AppState) http.Handler {
if err != nil {
fmt.Printf("WARN: Failed to create TOTP method: %s\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
- err := totpSetupTemplate.Execute(w, totpConfirmData{ Session: session })
+ err := templates.TOTPSetupTemplate.Execute(w, totpConfirmData{ Session: session })
if err != nil {
fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -235,7 +236,7 @@ func totpSetupHandler(app *model.AppState) http.Handler {
fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err)
}
- err = totpConfirmTemplate.Execute(w, totpConfirmData{
+ err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{
Session: session,
TOTP: &totp,
NameEscaped: url.PathEscape(totp.Name),
@@ -296,7 +297,7 @@ func totpConfirmHandler(app *model.AppState) http.Handler {
confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1)
if code != confirmCodeOffset {
session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." }
- err = totpConfirmTemplate.Execute(w, totpConfirmData{
+ err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{
Session: session,
TOTP: totp,
NameEscaped: url.PathEscape(totp.Name),
diff --git a/admin/artisthttp.go b/admin/artisthttp.go
index 9fa6bb2..67ea7d2 100644
--- a/admin/artisthttp.go
+++ b/admin/artisthttp.go
@@ -1,12 +1,13 @@
package admin
import (
- "fmt"
- "net/http"
- "strings"
+ "fmt"
+ "net/http"
+ "strings"
- "arimelody-web/model"
- "arimelody-web/controller"
+ "arimelody-web/admin/templates"
+ "arimelody-web/controller"
+ "arimelody-web/model"
)
func serveArtist(app *model.AppState) http.Handler {
@@ -39,7 +40,7 @@ func serveArtist(app *model.AppState) http.Handler {
session := r.Context().Value("session").(*model.Session)
- err = artistTemplate.Execute(w, ArtistResponse{
+ err = templates.EditArtistTemplate.Execute(w, ArtistResponse{
Session: session,
Artist: artist,
Credits: credits,
diff --git a/admin/http.go b/admin/http.go
index 245a152..05e181d 100644
--- a/admin/http.go
+++ b/admin/http.go
@@ -1,20 +1,24 @@
package admin
import (
- "context"
- "database/sql"
- "fmt"
- "net/http"
- "os"
- "path/filepath"
- "strings"
- "time"
+ "context"
+ "database/sql"
+ "embed"
+ "fmt"
+ "mime"
+ "net/http"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "time"
- "arimelody-web/controller"
- "arimelody-web/log"
- "arimelody-web/model"
+ "arimelody-web/admin/templates"
+ "arimelody-web/controller"
+ "arimelody-web/log"
+ "arimelody-web/model"
- "golang.org/x/crypto/bcrypt"
+ "golang.org/x/crypto/bcrypt"
)
func Handler(app *model.AppState) http.Handler {
@@ -91,7 +95,7 @@ func AdminIndexHandler(app *model.AppState) http.Handler {
Tracks []*model.Track
}
- err = indexTemplate.Execute(w, IndexData{
+ err = templates.IndexTemplate.Execute(w, IndexData{
Session: session,
Releases: releases,
Artists: artists,
@@ -120,7 +124,7 @@ func registerAccountHandler(app *model.AppState) http.Handler {
}
render := func() {
- err := registerTemplate.Execute(w, registerData{ Session: session })
+ err := templates.RegisterTemplate.Execute(w, registerData{ Session: session })
if err != nil {
fmt.Printf("WARN: Error rendering create account page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -230,7 +234,7 @@ func loginHandler(app *model.AppState) http.Handler {
}
render := func() {
- err := loginTemplate.Execute(w, loginData{ Session: session })
+ err := templates.LoginTemplate.Execute(w, loginData{ Session: session })
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -346,7 +350,7 @@ func loginTOTPHandler(app *model.AppState) http.Handler {
}
render := func() {
- err := loginTOTPTemplate.Execute(w, loginTOTPData{ Session: session })
+ err := templates.LoginTOTPTemplate.Execute(w, loginTOTPData{ Session: session })
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -440,7 +444,7 @@ func logoutHandler(app *model.AppState) http.Handler {
Path: "/",
})
- err = logoutTemplate.Execute(w, nil)
+ err = templates.LogoutTemplate.Execute(w, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to render logout page: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -460,24 +464,21 @@ func requireAccount(next http.Handler) http.HandlerFunc {
})
}
+//go:embed "static"
+var staticFS embed.FS
+
func staticHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- info, err := os.Stat(filepath.Join("admin", "static", filepath.Clean(r.URL.Path)))
- // does the file exist?
+ file, err := staticFS.ReadFile(filepath.Join("static", filepath.Clean(r.URL.Path)))
if err != nil {
- if os.IsNotExist(err) {
- http.NotFound(w, r)
- return
- }
- }
-
- // is thjs a directory? (forbidden)
- if info.IsDir() {
http.NotFound(w, r)
return
}
- http.FileServer(http.Dir(filepath.Join("admin", "static"))).ServeHTTP(w, r)
+ w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path)))
+ w.WriteHeader(http.StatusOK)
+
+ w.Write(file)
})
}
diff --git a/admin/logshttp.go b/admin/logshttp.go
index 7249b16..99f0ef1 100644
--- a/admin/logshttp.go
+++ b/admin/logshttp.go
@@ -1,12 +1,13 @@
package admin
import (
- "arimelody-web/log"
- "arimelody-web/model"
- "fmt"
- "net/http"
- "os"
- "strings"
+ "arimelody-web/admin/templates"
+ "arimelody-web/log"
+ "arimelody-web/model"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
)
func logsHandler(app *model.AppState) http.Handler {
@@ -54,7 +55,7 @@ func logsHandler(app *model.AppState) http.Handler {
Logs []*log.Log
}
- err = logsTemplate.Execute(w, LogsResponse{
+ err = templates.LogsTemplate.Execute(w, LogsResponse{
Session: session,
Logs: logs,
})
diff --git a/admin/releasehttp.go b/admin/releasehttp.go
index c6b68ab..30c967b 100644
--- a/admin/releasehttp.go
+++ b/admin/releasehttp.go
@@ -1,12 +1,13 @@
package admin
import (
- "fmt"
- "net/http"
- "strings"
+ "fmt"
+ "net/http"
+ "strings"
- "arimelody-web/controller"
- "arimelody-web/model"
+ "arimelody-web/admin/templates"
+ "arimelody-web/controller"
+ "arimelody-web/model"
)
func serveRelease(app *model.AppState) http.Handler {
@@ -60,7 +61,7 @@ func serveRelease(app *model.AppState) http.Handler {
Release *model.Release
}
- err = releaseTemplate.Execute(w, ReleaseResponse{
+ err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{
Session: session,
Release: release,
})
@@ -74,7 +75,7 @@ func serveRelease(app *model.AppState) http.Handler {
func serveEditCredits(release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
- err := editCreditsTemplate.Execute(w, release)
+ err := templates.EditCreditsTemplate.Execute(w, release)
if err != nil {
fmt.Printf("Error rendering edit credits component for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -97,7 +98,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler {
}
w.Header().Set("Content-Type", "text/html")
- err = addCreditTemplate.Execute(w, response{
+ err = templates.AddCreditTemplate.Execute(w, response{
ReleaseID: release.ID,
Artists: artists,
})
@@ -123,7 +124,7 @@ func serveNewCredit(app *model.AppState) http.Handler {
}
w.Header().Set("Content-Type", "text/html")
- err = newCreditTemplate.Execute(w, artist)
+ err = templates.NewCreditTemplate.Execute(w, artist)
if err != nil {
fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -134,7 +135,7 @@ func serveNewCredit(app *model.AppState) http.Handler {
func serveEditLinks(release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
- err := editLinksTemplate.Execute(w, release)
+ err := templates.EditCreditsTemplate.Execute(w, release)
if err != nil {
fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -151,7 +152,7 @@ func serveEditTracks(release *model.Release) http.Handler {
Add func(a int, b int) int
}
- err := editTracksTemplate.Execute(w, editTracksData{
+ err := templates.EditTracksTemplate.Execute(w, editTracksData{
Release: release,
Add: func(a, b int) int { return a + b },
})
@@ -177,7 +178,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler {
}
w.Header().Set("Content-Type", "text/html")
- err = addTrackTemplate.Execute(w, response{
+ err = templates.AddTrackTemplate.Execute(w, response{
ReleaseID: release.ID,
Tracks: tracks,
})
@@ -185,7 +186,6 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler {
fmt.Printf("Error rendering add tracks component for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
- return
})
}
@@ -204,11 +204,10 @@ func serveNewTrack(app *model.AppState) http.Handler {
}
w.Header().Set("Content-Type", "text/html")
- err = newTrackTemplate.Execute(w, track)
+ err = templates.NewTrackTemplate.Execute(w, track)
if err != nil {
fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
- return
})
}
diff --git a/admin/templates.go b/admin/templates.go
deleted file mode 100644
index 606d569..0000000
--- a/admin/templates.go
+++ /dev/null
@@ -1,125 +0,0 @@
-package admin
-
-import (
- "arimelody-web/log"
- "fmt"
- "html/template"
- "path/filepath"
- "strings"
- "time"
-)
-
-var indexTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "components", "release", "release-list-item.html"),
- filepath.Join("admin", "views", "index.html"),
-))
-
-var loginTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "login.html"),
-))
-var loginTOTPTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "login-totp.html"),
-))
-var registerTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "register.html"),
-))
-var logoutTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "logout.html"),
-))
-var accountTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "edit-account.html"),
-))
-var totpSetupTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "totp-setup.html"),
-))
-var totpConfirmTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "totp-confirm.html"),
-))
-
-var logsTemplate = template.Must(template.New("layout.html").Funcs(template.FuncMap{
- "parseLevel": func(level log.LogLevel) string {
- switch level {
- case log.LEVEL_INFO:
- return "INFO"
- case log.LEVEL_WARN:
- return "WARN"
- }
- return fmt.Sprintf("%d?", level)
- },
- "titleCase": func(logType string) string {
- runes := []rune(logType)
- for i, r := range runes {
- if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' {
- runes[i] = r + ('A' - 'a')
- }
- }
- return string(runes)
- },
- "lower": func(str string) string { return strings.ToLower(str) },
- "prettyTime": func(t time.Time) string {
- // return t.Format("2006-01-02 15:04:05")
- // return t.Format("15:04:05, 2 Jan 2006")
- return t.Format("02 Jan 2006, 15:04:05")
- },
-}).ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "logs.html"),
-))
-
-var releaseTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "edit-release.html"),
-))
-var artistTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "edit-artist.html"),
-))
-var trackTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "components", "release", "release-list-item.html"),
- filepath.Join("admin", "views", "edit-track.html"),
-))
-
-var editCreditsTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "components", "credits", "editcredits.html"),
-))
-var addCreditTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "components", "credits", "addcredit.html"),
-))
-var newCreditTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "components", "credits", "newcredit.html"),
-))
-
-var editLinksTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "components", "links", "editlinks.html"),
-))
-
-var editTracksTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "components", "tracks", "edittracks.html"),
-))
-var addTrackTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "components", "tracks", "addtrack.html"),
-))
-var newTrackTemplate = template.Must(template.ParseFiles(
- filepath.Join("admin", "components", "tracks", "newtrack.html"),
-))
diff --git a/admin/components/credits/addcredit.html b/admin/templates/html/components/credits/addcredit.html
similarity index 100%
rename from admin/components/credits/addcredit.html
rename to admin/templates/html/components/credits/addcredit.html
diff --git a/admin/components/credits/editcredits.html b/admin/templates/html/components/credits/editcredits.html
similarity index 100%
rename from admin/components/credits/editcredits.html
rename to admin/templates/html/components/credits/editcredits.html
diff --git a/admin/components/credits/newcredit.html b/admin/templates/html/components/credits/newcredit.html
similarity index 100%
rename from admin/components/credits/newcredit.html
rename to admin/templates/html/components/credits/newcredit.html
diff --git a/admin/components/links/editlinks.html b/admin/templates/html/components/links/editlinks.html
similarity index 100%
rename from admin/components/links/editlinks.html
rename to admin/templates/html/components/links/editlinks.html
diff --git a/admin/components/release/release-list-item.html b/admin/templates/html/components/release/release-list-item.html
similarity index 100%
rename from admin/components/release/release-list-item.html
rename to admin/templates/html/components/release/release-list-item.html
diff --git a/admin/components/tracks/addtrack.html b/admin/templates/html/components/tracks/addtrack.html
similarity index 100%
rename from admin/components/tracks/addtrack.html
rename to admin/templates/html/components/tracks/addtrack.html
diff --git a/admin/components/tracks/edittracks.html b/admin/templates/html/components/tracks/edittracks.html
similarity index 100%
rename from admin/components/tracks/edittracks.html
rename to admin/templates/html/components/tracks/edittracks.html
diff --git a/admin/components/tracks/newtrack.html b/admin/templates/html/components/tracks/newtrack.html
similarity index 100%
rename from admin/components/tracks/newtrack.html
rename to admin/templates/html/components/tracks/newtrack.html
diff --git a/admin/views/edit-account.html b/admin/templates/html/edit-account.html
similarity index 100%
rename from admin/views/edit-account.html
rename to admin/templates/html/edit-account.html
diff --git a/admin/views/edit-artist.html b/admin/templates/html/edit-artist.html
similarity index 100%
rename from admin/views/edit-artist.html
rename to admin/templates/html/edit-artist.html
diff --git a/admin/views/edit-release.html b/admin/templates/html/edit-release.html
similarity index 100%
rename from admin/views/edit-release.html
rename to admin/templates/html/edit-release.html
diff --git a/admin/views/edit-track.html b/admin/templates/html/edit-track.html
similarity index 100%
rename from admin/views/edit-track.html
rename to admin/templates/html/edit-track.html
diff --git a/admin/views/index.html b/admin/templates/html/index.html
similarity index 100%
rename from admin/views/index.html
rename to admin/templates/html/index.html
diff --git a/admin/views/layout.html b/admin/templates/html/layout.html
similarity index 100%
rename from admin/views/layout.html
rename to admin/templates/html/layout.html
diff --git a/admin/views/login-totp.html b/admin/templates/html/login-totp.html
similarity index 100%
rename from admin/views/login-totp.html
rename to admin/templates/html/login-totp.html
diff --git a/admin/views/login.html b/admin/templates/html/login.html
similarity index 100%
rename from admin/views/login.html
rename to admin/templates/html/login.html
diff --git a/admin/views/logout.html b/admin/templates/html/logout.html
similarity index 100%
rename from admin/views/logout.html
rename to admin/templates/html/logout.html
diff --git a/admin/views/logs.html b/admin/templates/html/logs.html
similarity index 100%
rename from admin/views/logs.html
rename to admin/templates/html/logs.html
diff --git a/views/prideflag.html b/admin/templates/html/prideflag.html
similarity index 100%
rename from views/prideflag.html
rename to admin/templates/html/prideflag.html
diff --git a/admin/views/register.html b/admin/templates/html/register.html
similarity index 100%
rename from admin/views/register.html
rename to admin/templates/html/register.html
diff --git a/admin/views/totp-confirm.html b/admin/templates/html/totp-confirm.html
similarity index 100%
rename from admin/views/totp-confirm.html
rename to admin/templates/html/totp-confirm.html
diff --git a/admin/views/totp-setup.html b/admin/templates/html/totp-setup.html
similarity index 100%
rename from admin/views/totp-setup.html
rename to admin/templates/html/totp-setup.html
diff --git a/admin/templates/templates.go b/admin/templates/templates.go
new file mode 100644
index 0000000..58cd1d0
--- /dev/null
+++ b/admin/templates/templates.go
@@ -0,0 +1,128 @@
+package templates
+
+import (
+ "arimelody-web/log"
+ "fmt"
+ "html/template"
+ "strings"
+ "time"
+ _ "embed"
+)
+
+//go:embed "html/layout.html"
+var layoutHTML string
+//go:embed "html/prideflag.html"
+var prideflagHTML string
+
+//go:embed "html/index.html"
+var indexHTML string
+
+//go:embed "html/register.html"
+var registerHTML string
+//go:embed "html/login.html"
+var loginHTML string
+//go:embed "html/login-totp.html"
+var loginTotpHTML string
+//go:embed "html/totp-confirm.html"
+var totpConfirmHTML string
+//go:embed "html/totp-setup.html"
+var totpSetupHTML string
+//go:embed "html/logout.html"
+var logoutHTML string
+
+//go:embed "html/logs.html"
+var logsHTML string
+
+//go:embed "html/edit-account.html"
+var editAccountHTML string
+//go:embed "html/edit-artist.html"
+var editArtistHTML string
+//go:embed "html/edit-release.html"
+var editReleaseHTML string
+//go:embed "html/edit-track.html"
+var editTrackHTML string
+
+//go:embed "html/components/credits/newcredit.html"
+var componentNewCreditHTML string
+//go:embed "html/components/credits/addcredit.html"
+var componentAddCreditHTML string
+//go:embed "html/components/credits/editcredits.html"
+var componentEditCreditsHTML string
+
+//go:embed "html/components/links/editlinks.html"
+var componentEditLinksHTML string
+
+//go:embed "html/components/release/release-list-item.html"
+var componentReleaseListItemHTML string
+
+//go:embed "html/components/tracks/newtrack.html"
+var componentNewTrackHTML string
+//go:embed "html/components/tracks/addtrack.html"
+var componentAddTrackHTML string
+//go:embed "html/components/tracks/edittracks.html"
+var componentEditTracksHTML string
+
+var BaseTemplate = template.Must(template.New("base").Parse(
+ strings.Join([]string{ layoutHTML, prideflagHTML }, "\n"),
+))
+
+var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
+ strings.Join([]string{
+ indexHTML,
+ componentReleaseListItemHTML,
+ }, "\n"),
+))
+
+var LoginTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginHTML))
+var LoginTOTPTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginTotpHTML))
+var RegisterTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(registerHTML))
+var LogoutTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(logoutHTML))
+var AccountTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editAccountHTML))
+var TOTPSetupTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpSetupHTML))
+var TOTPConfirmTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpConfirmHTML))
+
+var LogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Funcs(template.FuncMap{
+ "parseLevel": func(level log.LogLevel) string {
+ switch level {
+ case log.LEVEL_INFO:
+ return "INFO"
+ case log.LEVEL_WARN:
+ return "WARN"
+ }
+ return fmt.Sprintf("%d?", level)
+ },
+ "titleCase": func(logType string) string {
+ runes := []rune(logType)
+ for i, r := range runes {
+ if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' {
+ runes[i] = r + ('A' - 'a')
+ }
+ }
+ return string(runes)
+ },
+ "lower": func(str string) string { return strings.ToLower(str) },
+ "prettyTime": func(t time.Time) string {
+ // return t.Format("2006-01-02 15:04:05")
+ // return t.Format("15:04:05, 2 Jan 2006")
+ return t.Format("02 Jan 2006, 15:04:05")
+ },
+}).Parse(logsHTML))
+
+var EditReleaseTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editReleaseHTML))
+var EditArtistTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editArtistHTML))
+var EditTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
+ strings.Join([]string{
+ editTrackHTML,
+ componentReleaseListItemHTML,
+ }, "\n"),
+))
+
+var EditCreditsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditCreditsHTML))
+var AddCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddCreditHTML))
+var NewCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewCreditHTML))
+
+var EditLinksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditLinksHTML))
+
+var EditTracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditTracksHTML))
+var AddTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddTrackHTML))
+var NewTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewTrackHTML))
diff --git a/admin/trackhttp.go b/admin/trackhttp.go
index 93eacdb..e93d1bb 100644
--- a/admin/trackhttp.go
+++ b/admin/trackhttp.go
@@ -1,12 +1,13 @@
package admin
import (
- "fmt"
- "net/http"
- "strings"
+ "fmt"
+ "net/http"
+ "strings"
- "arimelody-web/model"
- "arimelody-web/controller"
+ "arimelody-web/admin/templates"
+ "arimelody-web/controller"
+ "arimelody-web/model"
)
func serveTrack(app *model.AppState) http.Handler {
@@ -39,7 +40,7 @@ func serveTrack(app *model.AppState) http.Handler {
session := r.Context().Value("session").(*model.Session)
- err = trackTemplate.Execute(w, TrackResponse{
+ err = templates.EditTrackTemplate.Execute(w, TrackResponse{
Session: session,
Track: track,
Releases: releases,
diff --git a/main.go b/main.go
index eaed7c6..553f109 100644
--- a/main.go
+++ b/main.go
@@ -1,32 +1,33 @@
package main
import (
- "bufio"
- "errors"
- "fmt"
- stdLog "log"
- "math"
- "math/rand"
- "net"
- "net/http"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "time"
+ "bufio"
+ "embed"
+ "errors"
+ "fmt"
+ stdLog "log"
+ "math"
+ "math/rand"
+ "net"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
- "arimelody-web/admin"
- "arimelody-web/api"
- "arimelody-web/colour"
- "arimelody-web/controller"
- "arimelody-web/cursor"
- "arimelody-web/log"
- "arimelody-web/model"
- "arimelody-web/view"
+ "arimelody-web/admin"
+ "arimelody-web/api"
+ "arimelody-web/colour"
+ "arimelody-web/controller"
+ "arimelody-web/cursor"
+ "arimelody-web/log"
+ "arimelody-web/model"
+ "arimelody-web/view"
- "github.com/jmoiron/sqlx"
- _ "github.com/lib/pq"
- "golang.org/x/crypto/bcrypt"
+ "github.com/jmoiron/sqlx"
+ _ "github.com/lib/pq"
+ "golang.org/x/crypto/bcrypt"
)
// used for database migrations
@@ -35,12 +36,16 @@ const DB_VERSION = 1
const DEFAULT_PORT int64 = 8080
const HRT_DATE int64 = 1756478697
+//go:embed "public"
+var publicFS embed.FS
+
func main() {
fmt.Printf("made with <3 by ari melody\n\n")
app := model.AppState{
Config: controller.GetConfig(),
Twitch: nil,
+ PublicFS: publicFS,
}
// initialise database connection
@@ -526,7 +531,7 @@ func createServeMux(app *model.AppState) *http.ServeMux {
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app)))
mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app)))
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app)))
- mux.Handle("/uploads/", http.StripPrefix("/uploads", view.StaticHandler(filepath.Join(app.Config.DataDirectory, "uploads"))))
+ mux.Handle("/uploads/", http.StripPrefix("/uploads", view.ServeFiles(filepath.Join(app.Config.DataDirectory, "uploads"))))
mux.Handle("/cursor-ws", cursor.Handler(app))
mux.Handle("/", view.IndexHandler(app))
diff --git a/model/appstate.go b/model/appstate.go
index 3a1c230..1a13be9 100644
--- a/model/appstate.go
+++ b/model/appstate.go
@@ -1,9 +1,11 @@
package model
import (
- "github.com/jmoiron/sqlx"
+ "embed"
- "arimelody-web/log"
+ "github.com/jmoiron/sqlx"
+
+ "arimelody-web/log"
)
type (
@@ -43,5 +45,6 @@ type (
Config Config
Log log.Logger
Twitch *TwitchState
+ PublicFS embed.FS
}
)
diff --git a/views/404.html b/templates/html/404.html
similarity index 100%
rename from views/404.html
rename to templates/html/404.html
diff --git a/views/footer.html b/templates/html/footer.html
similarity index 100%
rename from views/footer.html
rename to templates/html/footer.html
diff --git a/views/header.html b/templates/html/header.html
similarity index 100%
rename from views/header.html
rename to templates/html/header.html
diff --git a/views/index.html b/templates/html/index.html
similarity index 100%
rename from views/index.html
rename to templates/html/index.html
diff --git a/views/layout.html b/templates/html/layout.html
similarity index 100%
rename from views/layout.html
rename to templates/html/layout.html
diff --git a/views/music-gateway.html b/templates/html/music-gateway.html
similarity index 100%
rename from views/music-gateway.html
rename to templates/html/music-gateway.html
diff --git a/views/music.html b/templates/html/music.html
similarity index 100%
rename from views/music.html
rename to templates/html/music.html
diff --git a/templates/html/prideflag.html b/templates/html/prideflag.html
new file mode 100644
index 0000000..47ce4c7
--- /dev/null
+++ b/templates/html/prideflag.html
@@ -0,0 +1,21 @@
+{{define "prideflag"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/templates/templates.go b/templates/templates.go
index 752c78d..c6ab41e 100644
--- a/templates/templates.go
+++ b/templates/templates.go
@@ -1,28 +1,36 @@
package templates
import (
- "html/template"
- "path/filepath"
+ _ "embed"
+ "html/template"
+ "strings"
)
-var IndexTemplate = template.Must(template.ParseFiles(
- filepath.Join("views", "layout.html"),
- filepath.Join("views", "header.html"),
- filepath.Join("views", "footer.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("views", "index.html"),
-))
-var MusicTemplate = template.Must(template.ParseFiles(
- filepath.Join("views", "layout.html"),
- filepath.Join("views", "header.html"),
- filepath.Join("views", "footer.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("views", "music.html"),
-))
-var MusicGatewayTemplate = template.Must(template.ParseFiles(
- filepath.Join("views", "layout.html"),
- filepath.Join("views", "header.html"),
- filepath.Join("views", "footer.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("views", "music-gateway.html"),
+//go:embed "html/layout.html"
+var layoutHTML string
+//go:embed "html/header.html"
+var headerHTML string
+//go:embed "html/footer.html"
+var footerHTML string
+//go:embed "html/prideflag.html"
+var prideflagHTML string
+//go:embed "html/index.html"
+var indexHTML string
+//go:embed "html/music.html"
+var musicHTML string
+//go:embed "html/music-gateway.html"
+var musicGatewayHTML string
+// //go:embed "html/404.html"
+// var error404HTML string
+
+var BaseTemplate = template.Must(template.New("base").Parse(
+ strings.Join([]string{
+ layoutHTML,
+ headerHTML,
+ footerHTML,
+ prideflagHTML,
+ }, "\n"),
))
+var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(indexHTML))
+var MusicTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(musicHTML))
+var MusicGatewayTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(musicGatewayHTML))
diff --git a/view/static.go b/view/files.go
similarity index 54%
rename from view/static.go
rename to view/files.go
index 52263a2..c1b0e29 100644
--- a/view/static.go
+++ b/view/files.go
@@ -1,13 +1,31 @@
package view
import (
+ "embed"
"errors"
+ "mime"
"net/http"
"os"
+ "path"
"path/filepath"
)
-func StaticHandler(directory string) http.Handler {
+func ServeEmbedFS(fs embed.FS, dir string) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ file, err := fs.ReadFile(filepath.Join(dir, filepath.Clean(r.URL.Path)))
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+
+ w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path)))
+ w.WriteHeader(http.StatusOK)
+
+ w.Write(file)
+ })
+}
+
+func ServeFiles(directory string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
@@ -28,4 +46,3 @@ func StaticHandler(directory string) http.Handler {
http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
})
}
-
diff --git a/view/index.go b/view/index.go
index b6e3891..bcd0d06 100644
--- a/view/index.go
+++ b/view/index.go
@@ -40,6 +40,6 @@ func IndexHandler(app *model.AppState) http.Handler {
return
}
- StaticHandler("public").ServeHTTP(w, r)
+ ServeEmbedFS(app.PublicFS, "public").ServeHTTP(w, r)
})
}
From 1999ab7d2c493f9440f5b50950e665059d33f6a2 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 30 Sep 2025 22:29:37 +0100
Subject: [PATCH 33/57] embed schema migration scripts
---
controller/migrator.go | 20 +++++++++++++------
.../schema-migration}/000-init.sql | 0
.../schema-migration}/001-pre-versioning.sql | 0
.../schema-migration}/002-audit-logs.sql | 0
.../schema-migration}/003-fail-lock.sql | 0
controller/schema-migration/004-test.sql | 1 +
6 files changed, 15 insertions(+), 6 deletions(-)
rename {schema-migration => controller/schema-migration}/000-init.sql (100%)
rename {schema-migration => controller/schema-migration}/001-pre-versioning.sql (100%)
rename {schema-migration => controller/schema-migration}/002-audit-logs.sql (100%)
rename {schema-migration => controller/schema-migration}/003-fail-lock.sql (100%)
create mode 100644 controller/schema-migration/004-test.sql
diff --git a/controller/migrator.go b/controller/migrator.go
index 4b99b9c..1a70e62 100644
--- a/controller/migrator.go
+++ b/controller/migrator.go
@@ -1,14 +1,15 @@
package controller
import (
- "fmt"
- "os"
- "time"
+ "embed"
+ "fmt"
+ "os"
+ "time"
- "github.com/jmoiron/sqlx"
+ "github.com/jmoiron/sqlx"
)
-const DB_VERSION int = 4
+const DB_VERSION int = 5
func CheckDBVersionAndMigrate(db *sqlx.DB) {
db.MustExec("CREATE SCHEMA IF NOT EXISTS arimelody")
@@ -49,16 +50,23 @@ func CheckDBVersionAndMigrate(db *sqlx.DB) {
ApplyMigration(db, "003-fail-lock")
oldDBVersion = 4
+ case 4:
+ ApplyMigration(db, "004-test")
+ oldDBVersion = 5
+
}
}
fmt.Printf("Database schema up to date.\n")
}
+//go:embed "schema-migration"
+var schemaFS embed.FS
+
func ApplyMigration(db *sqlx.DB, scriptFile string) {
fmt.Printf("Applying schema migration %s...\n", scriptFile)
- bytes, err := os.ReadFile("schema-migration/" + scriptFile + ".sql")
+ bytes, err := schemaFS.ReadFile("schema-migration/" + scriptFile + ".sql")
if err != nil {
fmt.Fprintf(os.Stderr, "FATAL: Failed to open schema file \"%s\": %v\n", scriptFile, err)
os.Exit(1)
diff --git a/schema-migration/000-init.sql b/controller/schema-migration/000-init.sql
similarity index 100%
rename from schema-migration/000-init.sql
rename to controller/schema-migration/000-init.sql
diff --git a/schema-migration/001-pre-versioning.sql b/controller/schema-migration/001-pre-versioning.sql
similarity index 100%
rename from schema-migration/001-pre-versioning.sql
rename to controller/schema-migration/001-pre-versioning.sql
diff --git a/schema-migration/002-audit-logs.sql b/controller/schema-migration/002-audit-logs.sql
similarity index 100%
rename from schema-migration/002-audit-logs.sql
rename to controller/schema-migration/002-audit-logs.sql
diff --git a/schema-migration/003-fail-lock.sql b/controller/schema-migration/003-fail-lock.sql
similarity index 100%
rename from schema-migration/003-fail-lock.sql
rename to controller/schema-migration/003-fail-lock.sql
diff --git a/controller/schema-migration/004-test.sql b/controller/schema-migration/004-test.sql
new file mode 100644
index 0000000..3733de2
--- /dev/null
+++ b/controller/schema-migration/004-test.sql
@@ -0,0 +1 @@
+INSERT INTO arimelody.auditlog (level, type, content) VALUES (0, "test", "this is a db schema migration test!");
From 42c6540ac3adbed4ab889d60bedf5805fd22ab7d Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 30 Sep 2025 22:30:06 +0100
Subject: [PATCH 34/57] fix upload info log line
---
api/uploads.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/uploads.go b/api/uploads.go
index 4678f22..3c3c58a 100644
--- a/api/uploads.go
+++ b/api/uploads.go
@@ -50,7 +50,7 @@ func HandleImageUpload(app *model.AppState, data *string, directory string, file
return "", nil
}
- app.Log.Info(log.TYPE_FILES, "\"%s/%s.%s\" created.", directory, filename, ext)
+ app.Log.Info(log.TYPE_FILES, "\"%s\" created.", imagePath)
return filename, nil
}
From ef655744bb36e2b93c315accdf7e8b15824b06ea Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 30 Sep 2025 22:30:31 +0100
Subject: [PATCH 35/57] comment out deprecated QR code. uh. code
---
controller/qr.go | 69 ++++++++++++++++++++++++------------------------
1 file changed, 34 insertions(+), 35 deletions(-)
diff --git a/controller/qr.go b/controller/qr.go
index dd08637..21ed3e6 100644
--- a/controller/qr.go
+++ b/controller/qr.go
@@ -2,8 +2,8 @@ package controller
import (
"encoding/base64"
- "image"
- "image/color"
+ // "image"
+ // "image/color"
"github.com/skip2/go-qrcode"
)
@@ -18,36 +18,35 @@ func GenerateQRCode(data string) (string, error) {
}
// vvv DEPRECATED vvv
-
-const margin = 4
-
-type QRCodeECCLevel int64
-const (
- LOW QRCodeECCLevel = iota
- MEDIUM
- QUARTILE
- HIGH
-)
-
-func drawLargeAlignmentSquare(x int, y int, img *image.Gray) {
- for yi := range 7 {
- for xi := range 7 {
- if (xi == 0 || xi == 6) || (yi == 0 || yi == 6) {
- img.Set(x + xi, y + yi, color.Black)
- } else if (xi > 1 && xi < 5) && (yi > 1 && yi < 5) {
- img.Set(x + xi, y + yi, color.Black)
- }
- }
- }
-}
-
-func drawSmallAlignmentSquare(x int, y int, img *image.Gray) {
- for yi := range 5 {
- for xi := range 5 {
- if (xi == 0 || xi == 4) || (yi == 0 || yi == 4) {
- img.Set(x + xi, y + yi, color.Black)
- }
- }
- }
- img.Set(x + 2, y + 2, color.Black)
-}
+// const margin = 4
+//
+// type QRCodeECCLevel int64
+// const (
+// LOW QRCodeECCLevel = iota
+// MEDIUM
+// QUARTILE
+// HIGH
+// )
+//
+// func drawLargeAlignmentSquare(x int, y int, img *image.Gray) {
+// for yi := range 7 {
+// for xi := range 7 {
+// if (xi == 0 || xi == 6) || (yi == 0 || yi == 6) {
+// img.Set(x + xi, y + yi, color.Black)
+// } else if (xi > 1 && xi < 5) && (yi > 1 && yi < 5) {
+// img.Set(x + xi, y + yi, color.Black)
+// }
+// }
+// }
+// }
+//
+// func drawSmallAlignmentSquare(x int, y int, img *image.Gray) {
+// for yi := range 5 {
+// for xi := range 5 {
+// if (xi == 0 || xi == 4) || (yi == 0 || yi == 4) {
+// img.Set(x + xi, y + yi, color.Black)
+// }
+// }
+// }
+// img.Set(x + 2, y + 2, color.Black)
+// }
From 419781988aede87258eb06cb702db88c7c591dec Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 30 Sep 2025 22:30:52 +0100
Subject: [PATCH 36/57] refactor errors.New to fmt.Errorf
---
api/api.go | 5 ++---
controller/release.go | 13 ++++++-------
controller/session.go | 5 ++---
3 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/api/api.go b/api/api.go
index 398db4b..c5156c2 100644
--- a/api/api.go
+++ b/api/api.go
@@ -2,7 +2,6 @@ package api
import (
"context"
- "errors"
"fmt"
"net/http"
"os"
@@ -173,7 +172,7 @@ func getSession(app *model.AppState, r *http.Request) (*model.Session, error) {
// check cookies first
sessionCookie, err := r.Cookie(model.COOKIE_TOKEN)
if err != nil && err != http.ErrNoCookie {
- return nil, errors.New(fmt.Sprintf("Failed to retrieve session cookie: %v\n", err))
+ return nil, fmt.Errorf("Failed to retrieve session cookie: %v\n", err)
}
if sessionCookie != nil {
token = sessionCookie.Value
@@ -188,7 +187,7 @@ func getSession(app *model.AppState, r *http.Request) (*model.Session, error) {
session, err := controller.GetSession(app.DB, token)
if err != nil && !strings.Contains(err.Error(), "no rows") {
- return nil, errors.New(fmt.Sprintf("Failed to retrieve session: %v\n", err))
+ return nil, fmt.Errorf("Failed to retrieve session: %v\n", err)
}
if session != nil {
diff --git a/controller/release.go b/controller/release.go
index 3dcad26..afd09fb 100644
--- a/controller/release.go
+++ b/controller/release.go
@@ -1,7 +1,6 @@
package controller
import (
- "errors"
"fmt"
"arimelody-web/model"
@@ -21,7 +20,7 @@ func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
// get credits
credits, err := GetReleaseCredits(db, id)
if err != nil {
- return nil, errors.New(fmt.Sprintf("Credits: %s", err))
+ return nil, fmt.Errorf("Credits: %s", err)
}
for _, credit := range credits {
release.Credits = append(release.Credits, credit)
@@ -30,7 +29,7 @@ func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
// get tracks
tracks, err := GetReleaseTracks(db, id)
if err != nil {
- return nil, errors.New(fmt.Sprintf("Tracks: %s", err))
+ return nil, fmt.Errorf("Tracks: %s", err)
}
for _, track := range tracks {
release.Tracks = append(release.Tracks, track)
@@ -39,7 +38,7 @@ func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
// get links
links, err := GetReleaseLinks(db, id)
if err != nil {
- return nil, errors.New(fmt.Sprintf("Links: %s", err))
+ return nil, fmt.Errorf("Links: %s", err)
}
for _, link := range links {
release.Links = append(release.Links, link)
@@ -71,7 +70,7 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod
// get credits
credits, err := GetReleaseCredits(db, release.ID)
if err != nil {
- return nil, errors.New(fmt.Sprintf("Credits: %s", err))
+ return nil, fmt.Errorf("Credits: %s", err)
}
for _, credit := range credits {
release.Credits = append(release.Credits, credit)
@@ -81,7 +80,7 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod
// get tracks
tracks, err := GetReleaseTracks(db, release.ID)
if err != nil {
- return nil, errors.New(fmt.Sprintf("Tracks: %s", err))
+ return nil, fmt.Errorf("Tracks: %s", err)
}
for _, track := range tracks {
release.Tracks = append(release.Tracks, track)
@@ -90,7 +89,7 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod
// get links
links, err := GetReleaseLinks(db, release.ID)
if err != nil {
- return nil, errors.New(fmt.Sprintf("Links: %s", err))
+ return nil, fmt.Errorf("Links: %s", err)
}
for _, link := range links {
release.Links = append(release.Links, link)
diff --git a/controller/session.go b/controller/session.go
index 5028789..dfae551 100644
--- a/controller/session.go
+++ b/controller/session.go
@@ -2,7 +2,6 @@ package controller
import (
"database/sql"
- "errors"
"fmt"
"net/http"
"strings"
@@ -19,7 +18,7 @@ const TOKEN_LEN = 64
func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session, error) {
sessionCookie, err := r.Cookie(model.COOKIE_TOKEN)
if err != nil && err != http.ErrNoCookie {
- return nil, errors.New(fmt.Sprintf("Failed to retrieve session cookie: %v", err))
+ return nil, fmt.Errorf("Failed to retrieve session cookie: %v", err)
}
var session *model.Session
@@ -29,7 +28,7 @@ func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session
session, err = GetSession(app.DB, sessionCookie.Value)
if err != nil && !strings.Contains(err.Error(), "no rows") {
- return nil, errors.New(fmt.Sprintf("Failed to retrieve session: %v", err))
+ return nil, fmt.Errorf("Failed to retrieve session: %v", err)
}
if session != nil {
From 028ed6029354a394e68b602aac8635dd3f24702c Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 30 Sep 2025 22:34:46 +0100
Subject: [PATCH 37/57] oops
---
controller/controller.go | 2 +-
controller/migrator.go | 6 +-----
controller/schema-migration/004-test.sql | 1 -
3 files changed, 2 insertions(+), 7 deletions(-)
delete mode 100644 controller/schema-migration/004-test.sql
diff --git a/controller/controller.go b/controller/controller.go
index 44194e4..95615fb 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -5,7 +5,7 @@ import "math/rand"
func GenerateAlnumString(length int) []byte {
const CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
res := []byte{}
- for i := 0; i < length; i++ {
+ for range length {
res = append(res, CHARS[rand.Intn(len(CHARS))])
}
return res
diff --git a/controller/migrator.go b/controller/migrator.go
index 1a70e62..3be8c21 100644
--- a/controller/migrator.go
+++ b/controller/migrator.go
@@ -9,7 +9,7 @@ import (
"github.com/jmoiron/sqlx"
)
-const DB_VERSION int = 5
+const DB_VERSION int = 4
func CheckDBVersionAndMigrate(db *sqlx.DB) {
db.MustExec("CREATE SCHEMA IF NOT EXISTS arimelody")
@@ -50,10 +50,6 @@ func CheckDBVersionAndMigrate(db *sqlx.DB) {
ApplyMigration(db, "003-fail-lock")
oldDBVersion = 4
- case 4:
- ApplyMigration(db, "004-test")
- oldDBVersion = 5
-
}
}
diff --git a/controller/schema-migration/004-test.sql b/controller/schema-migration/004-test.sql
deleted file mode 100644
index 3733de2..0000000
--- a/controller/schema-migration/004-test.sql
+++ /dev/null
@@ -1 +0,0 @@
-INSERT INTO arimelody.auditlog (level, type, content) VALUES (0, "test", "this is a db schema migration test!");
From 13a84f7f25b8208bcccf40faf1208e27a964c5cd Mon Sep 17 00:00:00 2001
From: ari melody
Date: Sun, 19 Oct 2025 05:01:55 +0100
Subject: [PATCH 38/57] admin dashboard early UI refresh
---
.air.toml | 4 +-
admin/static/admin.css | 160 ++++++++++++++++++++---------
admin/static/admin.js | 17 +++
admin/static/edit-artist.css | 18 ++--
admin/static/edit-release.css | 67 +++++++-----
admin/static/edit-track.css | 9 +-
admin/static/index.css | 11 +-
admin/static/index.js | 8 ++
admin/static/logs.css | 42 +++++---
admin/static/release-list-item.css | 43 +++-----
admin/templates/html/layout.html | 7 +-
admin/templates/html/logs.html | 2 +-
12 files changed, 249 insertions(+), 139 deletions(-)
diff --git a/.air.toml b/.air.toml
index 070166a..c6d499b 100644
--- a/.air.toml
+++ b/.air.toml
@@ -7,14 +7,14 @@ tmp_dir = "tmp"
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
- exclude_dir = ["admin/static", "admin\\static", "public", "uploads", "test", "db", "res"]
+ exclude_dir = ["uploads", "test", "db", "res"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
- include_ext = ["go", "tpl", "tmpl", "html"]
+ include_ext = ["go", "tpl", "tmpl", "html", "css"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
diff --git a/admin/static/admin.css b/admin/static/admin.css
index 877b5da..1f5a1fb 100644
--- a/admin/static/admin.css
+++ b/admin/static/admin.css
@@ -1,6 +1,71 @@
@import url("/style/prideflag.css");
@import url("/font/inter/inter.css");
+:root {
+ --bg-0: #101010;
+ --bg-1: #141414;
+ --bg-2: #181818;
+ --bg-3: #202020;
+
+ --fg-0: #b0b0b0;
+ --fg-1: #c0c0c0;
+ --fg-2: #d0d0d0;
+ --fg-3: #e0e0e0;
+
+ --col-shadow-0: #0002;
+ --col-shadow-1: #0004;
+ --col-shadow-2: #0006;
+ --col-highlight-0: #ffffff08;
+ --col-highlight-1: #fff1;
+ --col-highlight-2: #fff2;
+
+ --col-new: #b3ee5b;
+ --col-on-new: #1b2013;
+ --col-save: #6fd7ff;
+ --col-on-save: #283f48;
+ --col-delete: #ff7171;
+ --col-on-delete: #371919;
+
+ --col-warn: #ffe86a;
+ --col-on-warn: var(--bg-0);
+ --col-warn-hover: #ffec81;
+
+ --shadow-sm:
+ 0 1px 2px var(--col-shadow-2),
+ inset 0 1px 1px var(--col-highlight-2);
+ --shadow-md:
+ 0 2px 4px var(--col-shadow-1),
+ inset 0 2px 2px var(--col-highlight-1);
+ --shadow-lg:
+ 0 4px 8px var(--col-shadow-0),
+ inset 0 4px 4px var(--col-highlight-0);
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --bg-0: #e8e8e8;
+ --bg-1: #f0f0f0;
+ --bg-2: #f8f8f8;
+ --bg-3: #ffffff;
+
+ --fg-0: #606060;
+ --fg-1: #404040;
+ --fg-2: #303030;
+ --fg-3: #202020;
+
+ --col-shadow-0: #0002;
+ --col-shadow-1: #0004;
+ --col-shadow-2: #0008;
+ --col-highlight-0: #fff2;
+ --col-highlight-1: #fff4;
+ --col-highlight-2: #fff8;
+
+ --col-warn: #ffe86a;
+ --col-on-warn: var(--fg-3);
+ --col-warn-hover: #ffec81;
+ }
+}
+
body {
width: 100%;
height: calc(100vh - 1em);
@@ -11,8 +76,12 @@ body {
font-family: "Inter", sans-serif;
font-size: 16px;
- color: #303030;
- background: #f0f0f0;
+ color: var(--fg-0);
+ background: var(--bg-0);
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: var(--fg-3);
}
nav {
@@ -22,40 +91,35 @@ nav {
display: flex;
flex-direction: row;
justify-content: left;
-
- background: #f8f8f8;
- border-radius: 4px;
- border: 1px solid #808080;
+ gap: .5em;
+ user-select: none;
}
nav .icon {
height: 100%;
+ border-radius: 100%;
+ box-shadow: var(--shadow-sm);
+ overflow: hidden;
}
-nav .title {
- width: auto;
+nav .icon img {
+ width: 100%;
height: 100%;
-
- margin: 0 1em 0 0;
-
- display: flex;
-
- line-height: 2em;
- text-decoration: none;
-
- color: inherit;
}
.nav-item {
width: auto;
height: 100%;
-
- margin: 0px;
padding: 0 1em;
-
display: flex;
+ color: var(--fg-2);
+ background: var(--bg-2);
+ border-radius: 10em;
+ box-shadow: var(--shadow-sm);
+
line-height: 2em;
+ font-weight: 500;
}
.nav-item:hover {
- background: #00000010;
+ background: var(--bg-1);
text-decoration: none;
}
nav a {
@@ -77,9 +141,11 @@ a {
text-decoration: none;
}
+/*
a:hover {
text-decoration: underline;
}
+*/
a img.icon {
height: .8em;
@@ -133,17 +199,14 @@ code {
#error {
margin: 0 0 1em 0;
padding: 1em;
- border-radius: 4px;
+ border-radius: 8px;
background: #ffffff;
- border: 1px solid #888;
}
#message {
background: #a9dfff;
- border-color: #599fdc;
}
#error {
background: #ffa9b8;
- border-color: #dc5959;
}
@@ -152,52 +215,54 @@ a.delete:not(.button) {
color: #d22828;
}
-button, .button {
+.button, button {
padding: .5em .8em;
font-family: inherit;
font-size: inherit;
- border-radius: 4px;
- border: 1px solid #a0a0a0;
- background: #f0f0f0;
+
color: inherit;
+ background: var(--bg-2);
+ border: none;
+ border-radius: 10em;
+ box-shadow: var(--shadow-sm);
+ font-weight: 500;
+ transition: background .1s ease-out, color .1s ease-out;
+
+ cursor: pointer;
+ user-select: none;
}
button:hover, .button:hover {
background: #fff;
- border-color: #d0d0d0;
}
button:active, .button:active {
background: #d0d0d0;
- border-color: #808080;
}
-.button, button {
- color: inherit;
-}
.button.new, button.new {
- background: #c4ff6a;
- border-color: #84b141;
+ color: var(--col-on-new);
+ background: var(--col-new);
}
.button.save, button.save {
- background: #6fd7ff;
- border-color: #6f9eb0;
+ color: var(--col-on-save);
+ background: var(--col-save);
}
.button.delete, button.delete {
- background: #ff7171;
- border-color: #7d3535;
+ color: var(--col-on-delete);
+ background: var(--col-delete);
}
.button:hover, button:hover {
- background: #fff;
- border-color: #d0d0d0;
+ color: var(--bg-3);
+ background: var(--fg-3);
}
.button:active, button:active {
- background: #d0d0d0;
- border-color: #808080;
+ color: var(--bg-2);
+ background: var(--fg-0);
}
.button[disabled], button[disabled] {
- background: #d0d0d0 !important;
- border-color: #808080 !important;
+ color: var(--fg-0) !important;
+ background: var(--bg-3) !important;
opacity: .5;
- cursor: not-allowed !important;
+ cursor: default !important;
}
@@ -217,7 +282,6 @@ form input {
padding: .3rem .5rem;
display: block;
border-radius: 4px;
- border: 1px solid #808080;
font-size: inherit;
font-family: inherit;
color: inherit;
diff --git a/admin/static/admin.js b/admin/static/admin.js
index 0763ab7..140efe0 100644
--- a/admin/static/admin.js
+++ b/admin/static/admin.js
@@ -69,3 +69,20 @@ export function makeMagicList(container, itemSelector, callback) {
if (callback) callback();
});
}
+
+export function hijackClickEvent(container, link) {
+ container.addEventListener('click', event => {
+ if (event.target.tagName.toLowerCase() === 'a') return;
+ event.preventDefault();
+ link.dispatchEvent(new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ ctrlKey: event.ctrlKey,
+ metaKey: event.metaKey,
+ shiftKey: event.shiftKey,
+ altKey: event.altKey,
+ button: event.button,
+ }));
+ });
+}
diff --git a/admin/static/edit-artist.css b/admin/static/edit-artist.css
index 5627e64..1bab082 100644
--- a/admin/static/edit-artist.css
+++ b/admin/static/edit-artist.css
@@ -9,9 +9,9 @@ h1 {
flex-direction: row;
gap: 1.2em;
- border-radius: 8px;
- background: #f8f8f8f8;
- border: 1px solid #808080;
+ border-radius: 16px;
+ background: var(--bg-2);
+ box-shadow: var(--shadow-md);
}
.artist-avatar {
@@ -27,6 +27,7 @@ h1 {
cursor: pointer;
}
.artist-avatar #remove-avatar {
+ margin-top: .5em;
padding: .3em .4em;
}
@@ -53,8 +54,8 @@ input[type="text"] {
font-family: inherit;
font-weight: inherit;
color: inherit;
- background: #ffffff;
- border: 1px solid transparent;
+ background: var(--bg-0);
+ border: none;
border-radius: 4px;
outline: none;
}
@@ -85,9 +86,10 @@ input[type="text"]:focus {
flex-direction: row;
gap: 1em;
align-items: center;
- background: #f8f8f8;
- border-radius: 8px;
- border: 1px solid #808080;
+
+ border-radius: 16px;
+ background: var(--bg-2);
+ box-shadow: var(--shadow-md);
}
.release-artwork {
diff --git a/admin/static/edit-release.css b/admin/static/edit-release.css
index aa70e34..d30db84 100644
--- a/admin/static/edit-release.css
+++ b/admin/static/edit-release.css
@@ -12,8 +12,8 @@ input[type="text"] {
gap: 1.2em;
border-radius: 8px;
- background: #f8f8f8f8;
- border: 1px solid #808080;
+ background: var(--bg-2);
+ box-shadow: var(--shadow-md);
}
.release-artwork {
@@ -29,7 +29,8 @@ input[type="text"] {
cursor: pointer;
}
.release-artwork #remove-artwork {
- padding: .3em .4em;
+ margin-top: .5em;
+ padding: .3em .6em;
}
.release-info {
@@ -54,17 +55,17 @@ input[type="text"] {
background: transparent;
outline: none;
cursor: pointer;
+ transition: background .1s ease-out, border-color .1s ease-out;
}
#title:hover {
- background: #ffffff;
- border-color: #80808080;
+ background: var(--bg-3);
+ border-color: var(--fg-0);
}
#title:active,
#title:focus {
- background: #ffffff;
- border-color: #808080;
+ background: var(--bg-3);
}
.release-title small {
@@ -75,19 +76,21 @@ input[type="text"] {
width: 100%;
margin: .5em 0;
border-collapse: collapse;
+ color: var(--fg-2);
}
.release-info table td {
padding: .2em;
- border-bottom: 1px solid #d0d0d0;
+ border-bottom: 1px solid color-mix(in srgb, var(--fg-0) 25%, transparent);
+ transition: background .1s ease-out, border-color .1s ease-out;
}
.release-info table tr td:first-child {
vertical-align: top;
- opacity: .66;
+ opacity: .5;
}
.release-info table tr td:not(:first-child) select:hover,
.release-info table tr td:not(:first-child) input:hover,
.release-info table tr td:not(:first-child) textarea:hover {
- background: #e8e8e8;
+ background: var(--bg-3);
cursor: pointer;
}
.release-info table td select,
@@ -117,11 +120,19 @@ input[type="text"] {
justify-content: right;
}
+.release-actions button,
+.release-actions .button {
+ color: var(--fg-2);
+ background: var(--bg-3);
+}
+
dialog {
width: min(720px, calc(100% - 2em));
padding: 2em;
border: 1px solid #101010;
- border-radius: 8px;
+ border-radius: 16px;
+ color: var(--fg-0);
+ background: var(--bg-0);
}
dialog header {
@@ -160,9 +171,9 @@ dialog div.dialog-actions {
align-items: center;
gap: 1em;
- border-radius: 8px;
- background: #f8f8f8f8;
- border: 1px solid #808080;
+ border-radius: 16px;
+ background: var(--bg-2);
+ box-shadow: var(--shadow-md);
}
.card.credits .credit p {
@@ -170,10 +181,11 @@ dialog div.dialog-actions {
}
.card.credits .credit .artist-avatar {
- border-radius: 8px;
+ border-radius: 12px;
}
.card.credits .credit .artist-name {
+ color: var(--fg-3);
font-weight: bold;
}
@@ -197,8 +209,8 @@ dialog div.dialog-actions {
gap: 1em;
border-radius: 8px;
- background: #f8f8f8f8;
- border: 1px solid #808080;
+ background: var(--bg-2);
+ box-shadow: var(--shadow-md);
}
#editcredits .credit {
@@ -232,6 +244,7 @@ dialog div.dialog-actions {
margin: 0;
display: flex;
align-items: center;
+ color: inherit;
}
#editcredits .credit .credit-info .credit-attribute input[type="text"] {
@@ -239,15 +252,17 @@ dialog div.dialog-actions {
padding: .2em .4em;
flex-grow: 1;
font-family: inherit;
- border: 1px solid #8888;
+ border: none;
border-radius: 4px;
- color: inherit;
+ color: var(--fg-2);
+ background: var(--bg-0);
}
#editcredits .credit .credit-info .credit-attribute input[type="checkbox"] {
margin: 0 .3em;
}
#editcredits .credit .artist-name {
+ color: var(--fg-2);
font-weight: bold;
}
@@ -256,8 +271,12 @@ dialog div.dialog-actions {
opacity: .66;
}
-#editcredits .credit button.delete {
- margin-left: auto;
+#editcredits .credit .delete {
+ margin-right: .5em;
+ cursor: pointer;
+}
+#editcredits .credit .delete:hover {
+ text-decoration: underline;
}
#addcredit ul {
@@ -400,9 +419,9 @@ dialog div.dialog-actions {
flex-direction: column;
gap: .5em;
- border-radius: 8px;
- background: #f8f8f8f8;
- border: 1px solid #808080;
+ border-radius: 16px;
+ background: var(--bg-2);
+ box-shadow: var(--shadow-md);
}
.card.tracks .track h3,
diff --git a/admin/static/edit-track.css b/admin/static/edit-track.css
index 600b680..1a9323f 100644
--- a/admin/static/edit-track.css
+++ b/admin/static/edit-track.css
@@ -11,9 +11,9 @@ h1 {
flex-direction: row;
gap: 1.2em;
- border-radius: 8px;
- background: #f8f8f8f8;
- border: 1px solid #808080;
+ border-radius: 16px;
+ background: var(--bg-2);
+ box-shadow: var(--shadow-md);
}
.track-info {
@@ -49,7 +49,8 @@ h1 {
font-weight: inherit;
font-family: inherit;
font-size: inherit;
- border: 1px solid transparent;
+ background: var(--bg-0);
+ border: none;
border-radius: 4px;
outline: none;
color: inherit;
diff --git a/admin/static/index.css b/admin/static/index.css
index 9fcd731..278224e 100644
--- a/admin/static/index.css
+++ b/admin/static/index.css
@@ -7,13 +7,18 @@
flex-direction: row;
align-items: center;
gap: .5em;
+ color: var(--fg-3);
- border-radius: 8px;
- background: #f8f8f8f8;
- border: 1px solid #808080;
+ border-radius: 16px;
+ background: var(--bg-2);
+ box-shadow: var(--shadow-md);
+
+ transition: background .1s ease-out;
+ cursor: pointer;
}
.artist:hover {
+ background: var(--bg-1);
text-decoration: hover;
}
diff --git a/admin/static/index.js b/admin/static/index.js
index e251802..0091fa4 100644
--- a/admin/static/index.js
+++ b/admin/static/index.js
@@ -1,3 +1,5 @@
+import { hijackClickEvent } from "./admin.js";
+
const newReleaseBtn = document.getElementById("create-release");
const newArtistBtn = document.getElementById("create-artist");
const newTrackBtn = document.getElementById("create-track");
@@ -72,3 +74,9 @@ newTrackBtn.addEventListener("click", event => {
console.error(err);
});
});
+
+document.addEventListener("readystatechange", () => {
+ document.querySelectorAll(".card.artists .artist").forEach(el => {
+ hijackClickEvent(el, el.querySelector("a.artist-name"))
+ });
+});
diff --git a/admin/static/logs.css b/admin/static/logs.css
index f0df299..4a66038 100644
--- a/admin/static/logs.css
+++ b/admin/static/logs.css
@@ -2,8 +2,14 @@ main {
width: min(1080px, calc(100% - 2em))!important
}
-form {
+form#search-form {
+ width: calc(100% - 2em);
margin: 1em 0;
+ padding: 1em;
+ border-radius: 16px;
+ color: var(--fg-0);
+ background: var(--bg-2);
+ box-shadow: var(--shadow-md);
}
div#search {
@@ -12,24 +18,25 @@ div#search {
#search input {
margin: 0;
+ padding: .3em .8em;
flex-grow: 1;
-
- border-right: none;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
+ border: none;
+ border-radius: 16px;
+ color: var(--fg-1);
+ background: var(--bg-0);
+ box-shadow: var(--shadow-sm);
}
#search button {
- padding: 0 .5em;
-
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
+ margin-left: .5em;
+ padding: 0 .8em;
}
form #filters p {
margin: .5em 0 0 0;
}
form #filters label {
+ color: inherit;
display: inline;
}
form #filters input {
@@ -57,6 +64,10 @@ form #filters input {
padding: .4em .8em;
}
+#logs .log {
+ color: var(--fg-2);
+}
+
td, th {
width: 1%;
text-align: left;
@@ -74,13 +85,14 @@ td.log-content {
white-space: collapse;
}
-.log:hover {
- background: #fff8;
+#logs .log:hover {
+ background: color-mix(in srgb, var(--fg-3) 10%, transparent);
}
-.log.warn {
- background: #ffe86a;
+#logs .log.warn {
+ color: var(--col-on-warn);
+ background: var(--col-warn);
}
-.log.warn:hover {
- background: #ffec81;
+#logs .log.warn:hover {
+ background: var(--col-warn-hover);
}
diff --git a/admin/static/release-list-item.css b/admin/static/release-list-item.css
index 638eac0..fb5d2d4 100644
--- a/admin/static/release-list-item.css
+++ b/admin/static/release-list-item.css
@@ -5,9 +5,9 @@
flex-direction: row;
gap: 1em;
- border-radius: 8px;
- background: #f8f8f8f8;
- border: 1px solid #808080;
+ border-radius: 16px;
+ background: var(--bg-2);
+ box-shadow: var(--shadow-md);
}
.release h3,
@@ -16,11 +16,15 @@
}
.release-artwork {
+ margin: auto 0;
width: 96px;
display: flex;
justify-content: center;
align-items: center;
+ border-radius: 4px;
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
}
.release-artwork img {
@@ -42,30 +46,9 @@
gap: .5em;
}
-.release-links li {
- flex-grow: 1;
-}
-
-.release-links a {
- padding: .5em;
- display: block;
-
- border-radius: 8px;
- text-decoration: none;
- color: #f0f0f0;
- background: #303030;
- text-align: center;
-
- transition: color .1s, background .1s;
-}
-
-.release-links a:hover {
- color: #303030;
- background: #f0f0f0;
-}
-
.release-actions {
margin-top: .5em;
+ user-select: none;
}
.release-actions a {
@@ -74,14 +57,14 @@
display: inline-block;
border-radius: 4px;
- background: #e0e0e0;
+ background: var(--bg-3);
+ box-shadow: var(--shadow-sm);
- transition: color .1s, background .1s;
+ transition: color .1s ease-out, background .1s ease-out;
}
.release-actions a:hover {
- color: #303030;
- background: #f0f0f0;
-
+ background: var(--bg-0);
+ color: var(--fg-3);
text-decoration: none;
}
diff --git a/admin/templates/html/layout.html b/admin/templates/html/layout.html
index fdeda9b..4925ce9 100644
--- a/admin/templates/html/layout.html
+++ b/admin/templates/html/layout.html
@@ -16,10 +16,9 @@
-
-
+
+
+
diff --git a/admin/templates/html/logs.html b/admin/templates/html/logs.html
index e3a3ccb..94146a7 100644
--- a/admin/templates/html/logs.html
+++ b/admin/templates/html/logs.html
@@ -9,7 +9,7 @@
Audit Logs
-