// Copyright (c) 2026 Jeremy Baxter.
// HTML builders
package server
import (
"cmp"
"embed"
"fmt"
"slices"
"strings"
"html/template"
"math/rand"
"net/http"
"net/url"
"git.baxters.nz/jeremy/records/musicindex"
"git.baxters.nz/jeremy/records/util"
)
//go:embed static/*
var staticFS embed.FS
type Page interface {
Body() template.HTML
SourceFile() string
Title() string
}
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)
return
}
err = t.Execute(w, p)
if err != nil {
w.Write([]byte(err.Error()))
}
}
type IndexPage struct {
Showcase string
}
func MakeIndexPage() (p IndexPage) {
albums := musicindex.Albums()
rand.Shuffle(len(albums), func(i, j int) {
albums[i], albums[j] = albums[j], albums[i]
})
i := 0
var b strings.Builder
b.WriteString(`
`)
for i < len(albums) {
b.WriteString(`
`)
for range 3 {
fmt.Fprintf(&b, `

`,
util.EscapePath(albums[i].RelativeDirectory()),
util.EscapePath(albums[i].RelativeCoverPath()))
i++
if i >= len(albums) {
break
}
}
b.WriteString(`
`)
}
b.WriteString(`
`)
p.Showcase = b.String()
return
}
func (p IndexPage) SourceFile() string { return "base.html" }
func (p IndexPage) Title() string { return "records.baxters.nz" }
func (p IndexPage) Body() template.HTML {
return template.HTML(p.Showcase)
}
func writeSortingWidget(b *strings.Builder, sortBy string, sortOptions []string, count int, itemDesc string) {
b.WriteString(fmt.Sprintf(`sorting %d %s by %s;`,
count, itemDesc, sortBy))
for _, v := range sortOptions {
if v == sortBy {
continue
}
b.WriteString(`
` + v + ``)
}
b.WriteString(`
`)
}
type ArtistsPage struct {
bodyHTML string
}
func ArtistSortOptions() []string {
return []string{"name", "albums", "songs"}
}
func MakeArtistsPage(sortBy string) (p ArtistsPage) {
var b strings.Builder
artistNames := musicindex.Artists()
writeSortingWidget(&b, sortBy, ArtistSortOptions(), len(artistNames),
"artist" + util.OptionalString(len(artistNames) != 1, "s"))
b.WriteString(``)
var artists []musicindex.Artist
for _, name := range artistNames {
artists = append(artists, musicindex.FindArtist(name))
}
switch sortBy {
case "albums":
slices.SortFunc(artists, func(a, b musicindex.Artist) int {
return cmp.Compare(len(b.Albums), len(a.Albums))
})
case "songs":
slices.SortFunc(artists, func(a, b musicindex.Artist) int {
return cmp.Compare(b.Songs, a.Songs)
})
case "name": // already sorted
fallthrough
default:
}
for _, artist := range artists {
albums := len(artist.Albums)
b.WriteString(fmt.Sprintf(`- %s`,
url.QueryEscape(artist.Name), artist.Name))
b.WriteString(fmt.Sprintf("
%d album%s, %d songs
",
albums, util.OptionalString(albums != 1, "s"), artist.Songs))
}
b.WriteString(`
`)
p.bodyHTML = b.String()
return
}
func (p ArtistsPage) SourceFile() string { return "base.html" }
func (p ArtistsPage) Title() string { return "records.baxters.nz - artists" }
func (p ArtistsPage) Body() template.HTML { return template.HTML(p.bodyHTML) }
type AlbumsPage struct {
bodyHTML string
}
func AlbumSortOptions() []string {
return []string{"name", "artist", "songs"}
}
func writeAlbums(b *strings.Builder, sortBy string, albums []musicindex.Album, artistAlbums bool) {
sortOptions := AlbumSortOptions()
if artistAlbums {
// remove the option to sort by "artist"
sortOptions = slices.DeleteFunc(sortOptions, func(s string) bool {
return s == "artist"
})
}
writeSortingWidget(b, sortBy, sortOptions, len(albums),
"album" + util.OptionalString(len(albums) != 1, "s"))
b.WriteString(`
`)
switch sortBy {
case "artist":
slices.SortFunc(albums, func(a, b musicindex.Album) int {
return cmp.Compare(a.Artist.Name, b.Artist.Name)
})
case "songs":
slices.SortFunc(albums, func(a, b musicindex.Album) int {
return cmp.Compare(len(b.TrackFiles), len(a.TrackFiles))
})
case "name": // already sorted
fallthrough
default:
slices.SortFunc(albums, func(a, b musicindex.Album) int {
return cmp.Compare(a.Name, b.Name)
})
}
for _, album := range albums {
b.WriteString(`![]() | `,
url.QueryEscape(album.RelativeCoverPath())))
b.WriteString(fmt.Sprintf(`%s
%s %d songs |
`,
util.EscapePath(album.RelativeDirectory()),
album.Name, album.Artist.Name, len(album.TrackFiles)))
}
b.WriteString(`
`)
}
func MakeAlbumsPage(sortBy string) (p AlbumsPage) {
var b strings.Builder
writeAlbums(&b, sortBy, musicindex.Albums(), false)
p.bodyHTML = b.String()
return
}
func (p AlbumsPage) SourceFile() string { return "base.html" }
func (p AlbumsPage) Title() string { return "records.baxters.nz - albums" }
func (p AlbumsPage) Body() template.HTML { return template.HTML(p.bodyHTML) }
type ArtistPage struct {
Artist musicindex.Artist
bodyHTML string
}
func MakeArtistPage(sortBy string, name string) (p ArtistPage) {
var b strings.Builder
artist := musicindex.FindArtist(name)
b.WriteString(`` + artist.Name + `
`)
writeAlbums(&b, sortBy, util.MapToValues(artist.Albums), true)
p.Artist = artist
p.bodyHTML = b.String()
return
}
func (p ArtistPage) SourceFile() string { return "base.html" }
func (p ArtistPage) Title() string { return "records.baxters.nz - artist " + p.Artist.Name }
func (p ArtistPage) Body() template.HTML { return template.HTML(p.bodyHTML) }
type AlbumPage struct {
Album musicindex.Album
bodyHTML string
}
func MakeAlbumPage(album musicindex.Album) (p AlbumPage) {
var b strings.Builder
b.WriteString(`
`)
b.WriteString(`

b.WriteString(album.RelativeCoverPath() + `)
`)
b.WriteString(`
`,
album.TarballSize / (1024 * 1024)))
p.Album = album
p.bodyHTML = b.String()
return
}
func (p AlbumPage) SourceFile() string { return "base.html" }
func (p AlbumPage) Title() string { return "records.baxters.nz - " + p.Album.Name }
func (p AlbumPage) Body() template.HTML { return template.HTML(p.bodyHTML) }