275 lines
7.2 KiB
Go
275 lines
7.2 KiB
Go
// 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(`<div class="grid">`)
|
|
|
|
for i < len(albums) {
|
|
b.WriteString(`<div class="row">`)
|
|
for range 3 {
|
|
fmt.Fprintf(&b, `<a href="/%s"><img width="300" height="300" src="/media/%s"></a>`,
|
|
util.EscapePath(albums[i].RelativeDirectory()),
|
|
util.EscapePath(albums[i].RelativeCoverPath()))
|
|
i++
|
|
if i >= len(albums) {
|
|
break
|
|
}
|
|
}
|
|
b.WriteString(`</div>`)
|
|
}
|
|
|
|
b.WriteString(`</div>`)
|
|
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(`<div class="sortby">sorting %d %s by %s;`,
|
|
count, itemDesc, sortBy))
|
|
for _, v := range sortOptions {
|
|
if v == sortBy {
|
|
continue
|
|
}
|
|
b.WriteString(` <a href="?by=` + v + `">` + v + `</a>`)
|
|
}
|
|
b.WriteString(`</div>`)
|
|
}
|
|
|
|
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(`<div class="artists"><ul>`)
|
|
|
|
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(`<li class="artist"><a href="/%s">%s</a>`,
|
|
url.QueryEscape(artist.Name), artist.Name))
|
|
b.WriteString(fmt.Sprintf("<br>%d album%s, %d songs</li><br>",
|
|
albums, util.OptionalString(albums != 1, "s"), artist.Songs))
|
|
}
|
|
|
|
b.WriteString(`</div>`)
|
|
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(`<br><div class="albums"><table><tbody>`)
|
|
|
|
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(`<tr class="album"><td><img `)
|
|
b.WriteString(fmt.Sprintf(`src="/media/%s" width="100" height="100"></td>`,
|
|
url.QueryEscape(album.RelativeCoverPath())))
|
|
b.WriteString(fmt.Sprintf(`<td><a href="/%s">%s</a><br><br>%s<br>%d songs</td></tr>`,
|
|
util.EscapePath(album.RelativeDirectory()),
|
|
album.Name, album.Artist.Name, len(album.TrackFiles)))
|
|
}
|
|
b.WriteString(`</tbody></table></div>`)
|
|
}
|
|
|
|
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(`<h3>` + artist.Name + `</h3>`)
|
|
|
|
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(`<br><div class="album"><div class="album-cover">`)
|
|
b.WriteString(`<img width="300" height="300" src="/media/`)
|
|
b.WriteString(album.RelativeCoverPath() + `"></div>`)
|
|
b.WriteString(`<div class="album-data"><div class="album-meta"><h3>`)
|
|
b.WriteString(album.Name + `</h3><a class="artist" href="/`)
|
|
b.WriteString(url.QueryEscape(album.Artist.Name) + `">` + album.Artist.Name)
|
|
b.WriteString(`</a></div><br><div class="album-tracks"><ul>`)
|
|
|
|
for _, track := range album.TrackFiles {
|
|
b.WriteString(`<li class="trackfile"><a href="/media/`)
|
|
b.WriteString(album.RelativeDirectory() + `/` + track)
|
|
b.WriteString(`">` + track + `</a></li>`)
|
|
}
|
|
|
|
tarball := album.Name + ".tar.gz"
|
|
tarballURI := "/" + url.QueryEscape(album.Artist.Name) + "/" +
|
|
url.QueryEscape(tarball)
|
|
|
|
b.WriteString(`</ul><h4>Download album</h4><a class="tarball" href="`)
|
|
b.WriteString(tarballURI + `">⤥ ` + tarball)
|
|
b.WriteString(fmt.Sprintf(` (%d MiB)</a></div></div></div>`,
|
|
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) }
|