Compare commits
8 commits
cf8959b1d7
...
3bfcf5794f
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bfcf5794f | |||
| dcb8094403 | |||
| 8553658128 | |||
| fa1faeaa9f | |||
| 9ffbe09d8d | |||
| b3d02b9476 | |||
| 93c59bba65 | |||
| 873ed7bc6d |
6 changed files with 112 additions and 47 deletions
29
main.go
29
main.go
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2026 Jeremy Baxter.
|
||||||
|
// entry point for records
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -16,18 +19,33 @@ const version = "0-pre"
|
||||||
|
|
||||||
var addr *string
|
var addr *string
|
||||||
var port *uint16
|
var port *uint16
|
||||||
|
var tempDir *string
|
||||||
|
var deleteAndExit *bool
|
||||||
var regenerate *bool
|
var regenerate *bool
|
||||||
var showVersion *bool
|
var showVersion *bool
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr = getopt.String('a', "0.0.0.0")
|
addr = getopt.String('a', "0.0.0.0")
|
||||||
port = getopt.Uint16('p', 8000)
|
port = getopt.Uint16('p', 8000)
|
||||||
|
tempDir = getopt.String('t', "")
|
||||||
|
deleteAndExit = getopt.Bool('d')
|
||||||
regenerate = getopt.Bool('r')
|
regenerate = getopt.Bool('r')
|
||||||
showVersion = getopt.Bool('V')
|
showVersion = getopt.Bool('V')
|
||||||
if err := getopt.Getopt(nil); err != nil {
|
if err := getopt.Getopt(nil); err != nil {
|
||||||
util.Die(err.Error())
|
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 {
|
if *showVersion {
|
||||||
fmt.Printf("records %s\n", version)
|
fmt.Printf("records %s\n", version)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|
@ -35,13 +53,20 @@ func main() {
|
||||||
|
|
||||||
args := getopt.Args()
|
args := getopt.Args()
|
||||||
if len(args) != 1 {
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaDir := args[0]
|
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")
|
log.Println("https://git.baxters.nz/jeremy/records")
|
||||||
|
|
||||||
musicindex.Init(mediaDir, *regenerate)
|
musicindex.Init(mediaDir, *regenerate)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2026 Jeremy Baxter.
|
||||||
|
// Music library indexing code
|
||||||
|
|
||||||
package musicindex
|
package musicindex
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -113,11 +116,13 @@ func indexAlbums(artist *Artist) (albums map[string]Album) {
|
||||||
|
|
||||||
albums = make(map[string]Album)
|
albums = make(map[string]Album)
|
||||||
for _, albumName := range entries {
|
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
|
albumDir := artistDir + "/" + albumName
|
||||||
album, err := indexAlbum(artist, albumName, albumDir)
|
album, err := indexAlbum(artist, albumName, albumDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
util.Iprint("\r")
|
||||||
log.Printf("warn: skipping inaccessible album %s: %s", albumName, err.Error())
|
log.Printf("warn: skipping inaccessible album %s: %s", albumName, err.Error())
|
||||||
} else {
|
} else {
|
||||||
albums[albumName] = album
|
albums[albumName] = album
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
// musicindex.go
|
// Copyright (c) 2026 Jeremy Baxter.
|
||||||
// Copyright (c) 2025 Jeremy Baxter.
|
// In-memory album and song database
|
||||||
|
|
||||||
// Ephemeral music (artist, album, cover & track) index
|
|
||||||
|
|
||||||
package musicindex
|
package musicindex
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2026 Jeremy Baxter.
|
||||||
|
// HTML builders
|
||||||
|
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -28,7 +31,8 @@ type Page interface {
|
||||||
func writePage(p Page, w http.ResponseWriter) {
|
func writePage(p Page, w http.ResponseWriter) {
|
||||||
t, err := template.ParseFS(staticFS, "static/templates/" + p.SourceFile())
|
t, err := template.ParseFS(staticFS, "static/templates/" + p.SourceFile())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, "Internal server error: " + err.Error(),
|
||||||
|
http.StatusInternalServerError)
|
||||||
return
|
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 {
|
type IndexPage struct {
|
||||||
Showcase string
|
Showcase string
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeIndexPage() IndexPage {
|
func MakeIndexPage() (p IndexPage) {
|
||||||
var p IndexPage
|
|
||||||
|
|
||||||
albums := musicindex.Albums()
|
albums := musicindex.Albums()
|
||||||
rand.Shuffle(len(albums), func(i, j int) {
|
rand.Shuffle(len(albums), func(i, j int) {
|
||||||
albums[i], albums[j] = albums[j], albums[i]
|
albums[i], albums[j] = albums[j], albums[i]
|
||||||
|
|
@ -68,9 +85,11 @@ func MakeIndexPage() IndexPage {
|
||||||
}
|
}
|
||||||
b.WriteString(`</div>`)
|
b.WriteString(`</div>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(`</div>`)
|
b.WriteString(`</div>`)
|
||||||
p.Showcase = b.String()
|
p.Showcase = b.String()
|
||||||
return p
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p IndexPage) SourceFile() string { return "base.html" }
|
func (p IndexPage) SourceFile() string { return "base.html" }
|
||||||
|
|
@ -99,8 +118,7 @@ func ArtistSortOptions() []string {
|
||||||
return []string{"name", "albums", "songs"}
|
return []string{"name", "albums", "songs"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeArtistsPage(sortBy string) ArtistsPage {
|
func MakeArtistsPage(sortBy string) (p ArtistsPage) {
|
||||||
var p ArtistsPage
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
artistNames := musicindex.Artists()
|
artistNames := musicindex.Artists()
|
||||||
|
|
@ -135,9 +153,11 @@ func MakeArtistsPage(sortBy string) ArtistsPage {
|
||||||
b.WriteString(fmt.Sprintf("<br>%d album%s, %d songs</li><br>",
|
b.WriteString(fmt.Sprintf("<br>%d album%s, %d songs</li><br>",
|
||||||
albums, util.OptionalString(albums != 1, "s"), artist.Songs))
|
albums, util.OptionalString(albums != 1, "s"), artist.Songs))
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(`</div>`)
|
b.WriteString(`</div>`)
|
||||||
p.bodyHTML = b.String()
|
p.bodyHTML = b.String()
|
||||||
return p
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p ArtistsPage) SourceFile() string { return "base.html" }
|
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(`</tbody></table></div>`)
|
b.WriteString(`</tbody></table></div>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeAlbumsPage(sortBy string) AlbumsPage {
|
func MakeAlbumsPage(sortBy string) (p AlbumsPage) {
|
||||||
var p AlbumsPage
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
writeAlbums(&b, sortBy, musicindex.Albums(), false)
|
writeAlbums(&b, sortBy, musicindex.Albums(), false)
|
||||||
p.bodyHTML = b.String()
|
p.bodyHTML = b.String()
|
||||||
|
|
||||||
return p
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p AlbumsPage) SourceFile() string { return "base.html" }
|
func (p AlbumsPage) SourceFile() string { return "base.html" }
|
||||||
|
|
@ -212,8 +231,7 @@ type ArtistPage struct {
|
||||||
bodyHTML string
|
bodyHTML string
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeArtistPage(sortBy string, name string) ArtistPage {
|
func MakeArtistPage(sortBy string, name string) (p ArtistPage) {
|
||||||
var p ArtistPage
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
artist := musicindex.FindArtist(name)
|
artist := musicindex.FindArtist(name)
|
||||||
|
|
@ -224,7 +242,7 @@ func MakeArtistPage(sortBy string, name string) ArtistPage {
|
||||||
p.Artist = artist
|
p.Artist = artist
|
||||||
p.bodyHTML = b.String()
|
p.bodyHTML = b.String()
|
||||||
|
|
||||||
return p
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p ArtistPage) SourceFile() string { return "base.html" }
|
func (p ArtistPage) SourceFile() string { return "base.html" }
|
||||||
|
|
@ -236,8 +254,7 @@ type AlbumPage struct {
|
||||||
bodyHTML string
|
bodyHTML string
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeAlbumPage(album musicindex.Album) AlbumPage {
|
func MakeAlbumPage(album musicindex.Album) (p AlbumPage) {
|
||||||
var p AlbumPage
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
b.WriteString(`<br><div class="album"><div class="album-cover">`)
|
b.WriteString(`<br><div class="album"><div class="album-cover">`)
|
||||||
|
|
@ -266,7 +283,7 @@ func MakeAlbumPage(album musicindex.Album) AlbumPage {
|
||||||
p.Album = album
|
p.Album = album
|
||||||
p.bodyHTML = b.String()
|
p.bodyHTML = b.String()
|
||||||
|
|
||||||
return p
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p AlbumPage) SourceFile() string { return "base.html" }
|
func (p AlbumPage) SourceFile() string { return "base.html" }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2026 Jeremy Baxter.
|
||||||
|
// HTTP music server
|
||||||
|
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -25,6 +28,16 @@ var artistPages map[string]map[string]ArtistPage
|
||||||
// mapping of artist names to album names to pages
|
// mapping of artist names to album names to pages
|
||||||
var albumPages map[string]map[string]AlbumPage
|
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) {
|
func doIndexPage(w http.ResponseWriter, req *http.Request) {
|
||||||
writePage(indexPage, w)
|
writePage(indexPage, w)
|
||||||
}
|
}
|
||||||
|
|
@ -61,13 +74,13 @@ func detectTextType(fileName string) string {
|
||||||
|
|
||||||
func serveStaticFiles(w http.ResponseWriter, req *http.Request) {
|
func serveStaticFiles(w http.ResponseWriter, req *http.Request) {
|
||||||
if strings.HasPrefix(req.RequestURI, "/static/templates/") {
|
if strings.HasPrefix(req.RequestURI, "/static/templates/") {
|
||||||
http.Error(w, "forbidden", http.StatusForbidden)
|
httpError(w, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
contents, err := staticFS.ReadFile(req.RequestURI[1:])
|
contents, err := staticFS.ReadFile(req.RequestURI[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
httpError(w, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,30 +96,31 @@ func serveStaticFiles(w http.ResponseWriter, req *http.Request) {
|
||||||
func serveMediaDirectory(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])
|
path, err := url.QueryUnescape("/" + strings.SplitN(req.RequestURI[1:], "/", 2)[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
httpError(w, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath := musicindex.MediaDirectory() + path
|
filePath := musicindex.MediaDirectory() + path
|
||||||
|
|
||||||
if strings.Contains(path, "/..") || strings.Contains(path, "/.") {
|
if strings.Contains(path, "/../") || strings.Contains(path, "/./") {
|
||||||
http.Error(w, "forbidden", http.StatusForbidden)
|
httpError(w, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stat, err := os.Stat(filePath)
|
stat, err := os.Stat(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
httpError(w, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if stat.IsDir() {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Open(filePath)
|
f, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
httpError(w, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
@ -117,14 +131,14 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) {
|
||||||
// test artist
|
// test artist
|
||||||
rx, err := regexp.Compile("^/([^/?]+)/?(\\?.*)?$")
|
rx, err := regexp.Compile("^/([^/?]+)/?(\\?.*)?$")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
httpError(w, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rx.MatchString(req.RequestURI)) {
|
if (rx.MatchString(req.RequestURI)) {
|
||||||
artist, err := url.QueryUnescape(rx.FindStringSubmatch(req.RequestURI)[1])
|
artist, err := url.QueryUnescape(rx.FindStringSubmatch(req.RequestURI)[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
httpError(w, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if musicindex.ArtistExists(artist) {
|
if musicindex.ArtistExists(artist) {
|
||||||
|
|
@ -139,14 +153,14 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// artist not found
|
// artist not found
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
httpError(w, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// test album tarball
|
// test album tarball
|
||||||
rx, err = regexp.Compile("^/([^/?]+)/([^/?]+)\\.tar\\.gz$")
|
rx, err = regexp.Compile("^/([^/?]+)/([^/?]+)\\.tar\\.gz$")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
httpError(w, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,12 +168,12 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) {
|
||||||
captures := rx.FindStringSubmatch(req.RequestURI)
|
captures := rx.FindStringSubmatch(req.RequestURI)
|
||||||
artist, err := url.QueryUnescape(captures[1])
|
artist, err := url.QueryUnescape(captures[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
httpError(w, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
albumName, err := url.QueryUnescape(captures[2])
|
albumName, err := url.QueryUnescape(captures[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
httpError(w, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,8 +182,7 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) {
|
||||||
if ok {
|
if ok {
|
||||||
f, err := os.Open(album.Tarball)
|
f, err := os.Open(album.Tarball)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "internal server error: " + err.Error(),
|
httpErrorReason(w, http.StatusInternalServerError, err.Error())
|
||||||
http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
@ -184,14 +197,14 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// artist or album not found
|
// artist or album not found
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
httpError(w, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// test album
|
// test album
|
||||||
rx, err = regexp.Compile("^/([^/?]+)/([^/?]+)/?(\\?.*)?$")
|
rx, err = regexp.Compile("^/([^/?]+)/([^/?]+)/?(\\?.*)?$")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
httpErrorReason(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,12 +212,12 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) {
|
||||||
captures := rx.FindStringSubmatch(req.RequestURI)
|
captures := rx.FindStringSubmatch(req.RequestURI)
|
||||||
artist, err := url.QueryUnescape(captures[1])
|
artist, err := url.QueryUnescape(captures[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
httpError(w, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
albumName, err := url.QueryUnescape(captures[2])
|
albumName, err := url.QueryUnescape(captures[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
httpError(w, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,12 +230,12 @@ func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// artist or album not found
|
// artist or album not found
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
httpError(w, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// illegal URI format
|
// illegal URI format
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
httpError(w, http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunOn(address string) {
|
func RunOn(address string) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2026 Jeremy Baxter.
|
||||||
|
// Reusable utility functions
|
||||||
|
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -83,9 +86,13 @@ func HashOf(s string) string {
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Interactive() bool {
|
||||||
|
return term.IsTerminal(int(os.Stderr.Fd()))
|
||||||
|
}
|
||||||
|
|
||||||
// print only if interactive
|
// print only if interactive
|
||||||
func Iprint(format string, args ...any) {
|
func Iprint(format string, args ...any) {
|
||||||
if term.IsTerminal(int(os.Stderr.Fd())) {
|
if Interactive() {
|
||||||
fmt.Fprintf(os.Stderr, format, args...)
|
fmt.Fprintf(os.Stderr, format, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue