diff --git a/main.go b/main.go index 1a7d925..c60295a 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,6 @@ +// Copyright (c) 2026 Jeremy Baxter. +// entry point for records + package main import ( @@ -16,18 +19,33 @@ const version = "0-pre" var addr *string var port *uint16 +var tempDir *string +var deleteAndExit *bool var regenerate *bool 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') if err := getopt.Getopt(nil); err != nil { util.Die(err.Error()) } + if len(*tempDir) != 0 { + musicindex.TempDir = *tempDir + } + + 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) @@ -35,13 +53,20 @@ 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) } 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) diff --git a/musicindex/index.go b/musicindex/index.go index 3c034c1..8a41d95 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 ( @@ -113,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 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..5bc4e72 100644 --- a/server/pages.go +++ b/server/pages.go @@ -1,3 +1,6 @@ +// Copyright (c) 2026 Jeremy Baxter. +// HTML builders + package server import ( @@ -28,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 } @@ -38,13 +42,26 @@ 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 } -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] @@ -68,9 +85,11 @@ func MakeIndexPage() IndexPage { } b.WriteString(``) } + b.WriteString(``) p.Showcase = b.String() - return p + + return } func (p IndexPage) SourceFile() string { return "base.html" } @@ -99,8 +118,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() @@ -135,9 +153,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" } @@ -193,14 +213,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" } @@ -212,8 +231,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) @@ -224,7 +242,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" } @@ -236,8 +254,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(`
`) @@ -266,7 +283,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" } diff --git a/server/server.go b/server/server.go index ea8040d..a0a8ca9 100644 --- a/server/server.go +++ b/server/server.go @@ -1,3 +1,6 @@ +// Copyright (c) 2026 Jeremy Baxter. +// HTTP music server + package server import ( @@ -25,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) } @@ -61,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 } @@ -83,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() @@ -117,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) { @@ -139,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 } @@ -154,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 } @@ -168,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() @@ -184,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 } @@ -199,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 } @@ -217,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) { diff --git a/util/util.go b/util/util.go index 77149eb..abe7e6c 100644 --- a/util/util.go +++ b/util/util.go @@ -1,3 +1,6 @@ +// Copyright (c) 2026 Jeremy Baxter. +// Reusable utility functions + package util import ( @@ -83,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...) } }