From 873ed7bc6d7e9f3c527118a127234996942786ef Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Mon, 5 Jan 2026 09:38:29 +1300 Subject: [PATCH 1/8] tree: add copyright and file headers --- main.go | 3 +++ musicindex/index.go | 3 +++ musicindex/musicindex.go | 6 ++---- server/pages.go | 3 +++ server/server.go | 3 +++ util/util.go | 3 +++ 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 1a7d925..285fbce 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,6 @@ +// Copyright (c) 2026 Jeremy Baxter. +// entry point for records + package main import ( diff --git a/musicindex/index.go b/musicindex/index.go index 3c034c1..41d6809 100644 --- a/musicindex/index.go +++ b/musicindex/index.go @@ -1,3 +1,6 @@ +// Copyright (c) 2026 Jeremy Baxter. +// Music library indexing code + package musicindex import ( diff --git a/musicindex/musicindex.go b/musicindex/musicindex.go index b70b0d3..b78d0ff 100644 --- a/musicindex/musicindex.go +++ b/musicindex/musicindex.go @@ -1,7 +1,5 @@ -// musicindex.go -// Copyright (c) 2025 Jeremy Baxter. - -// Ephemeral music (artist, album, cover & track) index +// Copyright (c) 2026 Jeremy Baxter. +// In-memory album and song database package musicindex diff --git a/server/pages.go b/server/pages.go index 3d21cee..31090fb 100644 --- a/server/pages.go +++ b/server/pages.go @@ -1,3 +1,6 @@ +// Copyright (c) 2026 Jeremy Baxter. +// HTML builders + package server import ( diff --git a/server/server.go b/server/server.go index ea8040d..3408f66 100644 --- a/server/server.go +++ b/server/server.go @@ -1,3 +1,6 @@ +// Copyright (c) 2026 Jeremy Baxter. +// HTTP music server + package server import ( diff --git a/util/util.go b/util/util.go index 77149eb..25018c4 100644 --- a/util/util.go +++ b/util/util.go @@ -1,3 +1,6 @@ +// Copyright (c) 2026 Jeremy Baxter. +// Reusable utility functions + package util import ( From 93c59bba656ca5fca9f7e12d15662c6b71c61c76 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 10 Jan 2026 20:23:05 +1300 Subject: [PATCH 2/8] util: split Iprint() into Interactive() --- util/util.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/util/util.go b/util/util.go index 25018c4..abe7e6c 100644 --- a/util/util.go +++ b/util/util.go @@ -86,9 +86,13 @@ func HashOf(s string) string { return fmt.Sprintf("%x", h.Sum(nil)) } +func Interactive() bool { + return term.IsTerminal(int(os.Stderr.Fd())) +} + // print only if interactive func Iprint(format string, args ...any) { - if term.IsTerminal(int(os.Stderr.Fd())) { + if Interactive() { fmt.Fprintf(os.Stderr, format, args...) } } From b3d02b9476da9d360b44ecabe94b385d814f1fda Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 10 Jan 2026 20:23:55 +1300 Subject: [PATCH 3/8] index: improve caching progress indicator visuals --- musicindex/index.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/musicindex/index.go b/musicindex/index.go index 41d6809..8a41d95 100644 --- a/musicindex/index.go +++ b/musicindex/index.go @@ -116,11 +116,13 @@ func indexAlbums(artist *Artist) (albums map[string]Album) { albums = make(map[string]Album) for _, albumName := range entries { - util.Iprint("* index %s - %s\r", artist.Name, albumName) + util.Iprint("\033[2K\r \033[1;36mcaching\033[0m %s - %s", + artist.Name, albumName) albumDir := artistDir + "/" + albumName album, err := indexAlbum(artist, albumName, albumDir) if err != nil { + util.Iprint("\r") log.Printf("warn: skipping inaccessible album %s: %s", albumName, err.Error()) } else { albums[albumName] = album From 9ffbe09d8d060c4041b56696b6d6e3701b2f93ba Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 10 Jan 2026 20:24:58 +1300 Subject: [PATCH 4/8] main: indicate interactive features --- main.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 285fbce..0e73a9c 100644 --- a/main.go +++ b/main.go @@ -44,7 +44,14 @@ func main() { mediaDir := args[0] - log.Printf("This is records %s\n", version) + { + interactive := "" + if util.Interactive() { + interactive = ", running with interactive features" + } + log.Printf("This is records %s%s", version, interactive) + } + log.Println("https://git.baxters.nz/jeremy/records") musicindex.Init(mediaDir, *regenerate) From fa1faeaa9fd2ef87a17dbd6387073c36e8c38845 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 10 Jan 2026 20:26:08 +1300 Subject: [PATCH 5/8] main: add -d option to delete cache --- main.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 0e73a9c..71c5ee7 100644 --- a/main.go +++ b/main.go @@ -19,18 +19,27 @@ const version = "0-pre" var addr *string var port *uint16 +var deleteAndExit *bool var regenerate *bool var showVersion *bool func main() { addr = getopt.String('a', "0.0.0.0") port = getopt.Uint16('p', 8000) + deleteAndExit = getopt.Bool('d') regenerate = getopt.Bool('r') showVersion = getopt.Bool('V') if err := getopt.Getopt(nil); err != nil { util.Die(err.Error()) } + if *deleteAndExit { + err := os.RemoveAll(musicindex.TempDir) + if err != nil { + util.Die("cannot remove %s: %s", musicindex.TempDir, err.Error()) + } + os.Exit(0) + } if *showVersion { fmt.Printf("records %s\n", version) os.Exit(0) @@ -38,7 +47,7 @@ func main() { args := getopt.Args() if len(args) != 1 { - fmt.Fprintf(os.Stderr, "usage: %s [-rV] [-a address] [-p port] directory\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "usage: %s [-drV] [-a address] [-p port] directory\n", os.Args[0]) os.Exit(1) } From 855365812881aac12d386c8039d4af931e6f2eb2 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 10 Jan 2026 20:27:30 +1300 Subject: [PATCH 6/8] main: add -t option to set temp dir --- main.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main.go b/main.go index 71c5ee7..c60295a 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ const version = "0-pre" var addr *string var port *uint16 +var tempDir *string var deleteAndExit *bool var regenerate *bool var showVersion *bool @@ -26,6 +27,7 @@ var showVersion *bool func main() { addr = getopt.String('a', "0.0.0.0") port = getopt.Uint16('p', 8000) + tempDir = getopt.String('t', "") deleteAndExit = getopt.Bool('d') regenerate = getopt.Bool('r') showVersion = getopt.Bool('V') @@ -33,6 +35,10 @@ func main() { util.Die(err.Error()) } + if len(*tempDir) != 0 { + musicindex.TempDir = *tempDir + } + if *deleteAndExit { err := os.RemoveAll(musicindex.TempDir) if err != nil { From dcb809440351d32450737cdc19a1c38d01186401 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 10 Jan 2026 21:24:52 +1300 Subject: [PATCH 7/8] server: make constructors concise --- server/pages.go | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/server/pages.go b/server/pages.go index 31090fb..c527543 100644 --- a/server/pages.go +++ b/server/pages.go @@ -45,9 +45,7 @@ type IndexPage struct { Showcase string } -func MakeIndexPage() IndexPage { - var p IndexPage - +func MakeIndexPage() (p IndexPage) { albums := musicindex.Albums() rand.Shuffle(len(albums), func(i, j int) { albums[i], albums[j] = albums[j], albums[i] @@ -71,9 +69,11 @@ func MakeIndexPage() IndexPage { } b.WriteString(``) } + b.WriteString(``) p.Showcase = b.String() - return p + + return } func (p IndexPage) SourceFile() string { return "base.html" } @@ -102,8 +102,7 @@ func ArtistSortOptions() []string { return []string{"name", "albums", "songs"} } -func MakeArtistsPage(sortBy string) ArtistsPage { - var p ArtistsPage +func MakeArtistsPage(sortBy string) (p ArtistsPage) { var b strings.Builder artistNames := musicindex.Artists() @@ -138,9 +137,11 @@ func MakeArtistsPage(sortBy string) ArtistsPage { b.WriteString(fmt.Sprintf("
%d album%s, %d songs
", albums, util.OptionalString(albums != 1, "s"), artist.Songs)) } + b.WriteString(``) p.bodyHTML = b.String() - return p + + return } func (p ArtistsPage) SourceFile() string { return "base.html" } @@ -196,14 +197,13 @@ func writeAlbums(b *strings.Builder, sortBy string, albums []musicindex.Album, a b.WriteString(``) } -func MakeAlbumsPage(sortBy string) AlbumsPage { - var p AlbumsPage +func MakeAlbumsPage(sortBy string) (p AlbumsPage) { var b strings.Builder writeAlbums(&b, sortBy, musicindex.Albums(), false) p.bodyHTML = b.String() - return p + return } func (p AlbumsPage) SourceFile() string { return "base.html" } @@ -215,8 +215,7 @@ type ArtistPage struct { bodyHTML string } -func MakeArtistPage(sortBy string, name string) ArtistPage { - var p ArtistPage +func MakeArtistPage(sortBy string, name string) (p ArtistPage) { var b strings.Builder artist := musicindex.FindArtist(name) @@ -227,7 +226,7 @@ func MakeArtistPage(sortBy string, name string) ArtistPage { p.Artist = artist p.bodyHTML = b.String() - return p + return } func (p ArtistPage) SourceFile() string { return "base.html" } @@ -239,8 +238,7 @@ type AlbumPage struct { bodyHTML string } -func MakeAlbumPage(album musicindex.Album) AlbumPage { - var p AlbumPage +func MakeAlbumPage(album musicindex.Album) (p AlbumPage) { var b strings.Builder b.WriteString(`
`) @@ -269,7 +267,7 @@ func MakeAlbumPage(album musicindex.Album) AlbumPage { p.Album = album p.bodyHTML = b.String() - return p + return } func (p AlbumPage) SourceFile() string { return "base.html" } From 3bfcf5794f51f5001f8c40cd2d8b9e1a0f6792f1 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 10 Jan 2026 21:27:43 +1300 Subject: [PATCH 8/8] server: use custom error page --- server/pages.go | 18 +++++++++++++++- server/server.go | 54 ++++++++++++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/server/pages.go b/server/pages.go index c527543..5bc4e72 100644 --- a/server/pages.go +++ b/server/pages.go @@ -31,7 +31,8 @@ type Page interface { func writePage(p Page, w http.ResponseWriter) { t, err := template.ParseFS(staticFS, "static/templates/" + p.SourceFile()) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, "Internal server error: " + err.Error(), + http.StatusInternalServerError) return } @@ -41,6 +42,21 @@ func writePage(p Page, w http.ResponseWriter) { } } +type ErrorPage struct { + Status string + Reason string +} + +func MakeErrorPage(status, reason string) (p ErrorPage) { + p.Status = status + p.Reason = reason + return +} + +func (p ErrorPage) SourceFile() string { return "error.html" } +func (p ErrorPage) Title() string { return "" } +func (p ErrorPage) Body() template.HTML { return template.HTML("") } + type IndexPage struct { Showcase string } diff --git a/server/server.go b/server/server.go index 3408f66..a0a8ca9 100644 --- a/server/server.go +++ b/server/server.go @@ -28,6 +28,16 @@ var artistPages map[string]map[string]ArtistPage // mapping of artist names to album names to pages var albumPages map[string]map[string]AlbumPage +func httpErrorReason(w http.ResponseWriter, error int, reason string) { + errorName := fmt.Sprintf("%d %s", error, http.StatusText(error)) + w.WriteHeader(error) + writePage(MakeErrorPage(errorName, reason), w) +} + +func httpError(w http.ResponseWriter, error int) { + httpErrorReason(w, error, "") +} + func doIndexPage(w http.ResponseWriter, req *http.Request) { writePage(indexPage, w) } @@ -64,13 +74,13 @@ func detectTextType(fileName string) string { func serveStaticFiles(w http.ResponseWriter, req *http.Request) { if strings.HasPrefix(req.RequestURI, "/static/templates/") { - http.Error(w, "forbidden", http.StatusForbidden) + httpError(w, http.StatusForbidden) return } contents, err := staticFS.ReadFile(req.RequestURI[1:]) if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) + httpError(w, http.StatusNotFound) return } @@ -86,30 +96,31 @@ func serveStaticFiles(w http.ResponseWriter, req *http.Request) { func serveMediaDirectory(w http.ResponseWriter, req *http.Request) { path, err := url.QueryUnescape("/" + strings.SplitN(req.RequestURI[1:], "/", 2)[1]) if err != nil { - http.Error(w, "bad request", http.StatusBadRequest) + httpError(w, http.StatusBadRequest) return } filePath := musicindex.MediaDirectory() + path - if strings.Contains(path, "/..") || strings.Contains(path, "/.") { - http.Error(w, "forbidden", http.StatusForbidden) + if strings.Contains(path, "/../") || strings.Contains(path, "/./") { + httpError(w, http.StatusForbidden) return } stat, err := os.Stat(filePath) if err != nil { - http.Error(w, "not found", http.StatusNotFound) + httpError(w, http.StatusNotFound) return } if stat.IsDir() { - http.Error(w, "is a directory; maybe you are looking for " + path, http.StatusForbidden) + httpErrorReason(w, http.StatusForbidden, + "is a directory; maybe you are looking for " + path) return } f, err := os.Open(filePath) if err != nil { - http.Error(w, "not found", http.StatusNotFound) + httpError(w, http.StatusNotFound) return } defer f.Close() @@ -120,14 +131,14 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) { // test artist rx, err := regexp.Compile("^/([^/?]+)/?(\\?.*)?$") if err != nil { - http.Error(w, "internal server error", http.StatusInternalServerError) + httpError(w, http.StatusInternalServerError) return } if (rx.MatchString(req.RequestURI)) { artist, err := url.QueryUnescape(rx.FindStringSubmatch(req.RequestURI)[1]) if err != nil { - http.Error(w, "bad request", http.StatusBadRequest) + httpError(w, http.StatusBadRequest) return } if musicindex.ArtistExists(artist) { @@ -142,14 +153,14 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) { } // artist not found - http.Error(w, "not found", http.StatusNotFound) + httpError(w, http.StatusNotFound) return } // test album tarball rx, err = regexp.Compile("^/([^/?]+)/([^/?]+)\\.tar\\.gz$") if err != nil { - http.Error(w, "internal server error", http.StatusInternalServerError) + httpError(w, http.StatusInternalServerError) return } @@ -157,12 +168,12 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) { captures := rx.FindStringSubmatch(req.RequestURI) artist, err := url.QueryUnescape(captures[1]) if err != nil { - http.Error(w, "bad request", http.StatusBadRequest) + httpError(w, http.StatusBadRequest) return } albumName, err := url.QueryUnescape(captures[2]) if err != nil { - http.Error(w, "bad request", http.StatusBadRequest) + httpError(w, http.StatusBadRequest) return } @@ -171,8 +182,7 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) { if ok { f, err := os.Open(album.Tarball) if err != nil { - http.Error(w, "internal server error: " + err.Error(), - http.StatusInternalServerError) + httpErrorReason(w, http.StatusInternalServerError, err.Error()) return } defer f.Close() @@ -187,14 +197,14 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) { } // artist or album not found - http.Error(w, "not found", http.StatusNotFound) + httpError(w, http.StatusNotFound) return } // test album rx, err = regexp.Compile("^/([^/?]+)/([^/?]+)/?(\\?.*)?$") if err != nil { - http.Error(w, "internal server error", http.StatusInternalServerError) + httpErrorReason(w, http.StatusInternalServerError, err.Error()) return } @@ -202,12 +212,12 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) { captures := rx.FindStringSubmatch(req.RequestURI) artist, err := url.QueryUnescape(captures[1]) if err != nil { - http.Error(w, "bad request", http.StatusBadRequest) + httpError(w, http.StatusBadRequest) return } albumName, err := url.QueryUnescape(captures[2]) if err != nil { - http.Error(w, "bad request", http.StatusBadRequest) + httpError(w, http.StatusBadRequest) return } @@ -220,12 +230,12 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) { } // artist or album not found - http.Error(w, "not found", http.StatusNotFound) + httpError(w, http.StatusNotFound) return } // illegal URI format - http.Error(w, "not found", http.StatusNotFound) + httpError(w, http.StatusNotFound) } func RunOn(address string) {