// 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() IndexPage { var 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 p } 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) ArtistsPage { var p ArtistsPage var b strings.Builder artistNames := musicindex.Artists() writeSortingWidget(&b, sortBy, ArtistSortOptions(), len(artistNames), "artist" + util.OptionalString(len(artistNames) != 1, "s")) b.WriteString(`
`) p.bodyHTML = b.String() return p } 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(``, util.EscapePath(album.RelativeDirectory()), album.Name, album.Artist.Name, len(album.TrackFiles))) } b.WriteString(`
%s

%s
%d songs
`) } func MakeAlbumsPage(sortBy string) AlbumsPage { var p AlbumsPage var b strings.Builder writeAlbums(&b, sortBy, musicindex.Albums(), false) p.bodyHTML = b.String() return p } 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) ArtistPage { var 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 p } 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) AlbumPage { var p AlbumPage var b strings.Builder b.WriteString(`
`) b.WriteString(`
`) b.WriteString(`

`) b.WriteString(album.Name + `

` + album.Artist.Name) b.WriteString(`

    `) for _, track := range album.TrackFiles { b.WriteString(`
  • ` + track + `
  • `) } tarball := album.Name + ".tar.gz" tarballURI := "/" + url.QueryEscape(album.Artist.Name) + "/" + url.QueryEscape(tarball) b.WriteString(`

Download album

⤥ ` + tarball) b.WriteString(fmt.Sprintf(` (%d MiB)
`, album.TarballSize / (1024 * 1024))) p.Album = album p.bodyHTML = b.String() return p } 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) }