package comicanimator
import (
"fmt"
"html/template"
"mime"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
func render(w http.ResponseWriter, src string, data any) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := template.Must(template.New("page").Parse(src)).Execute(w, data); err != nil {
http.Error(w, "render error", 500)
}
}
func fragmentError(w http.ResponseWriter, status int, message string) {
w.WriteHeader(status)
fmt.Fprintf(w, `
%s
`, template.HTMLEscapeString(message))
}
func (t *Tool) renderCard(w http.ResponseWriter, g Generation, instructor bool) {
poll := ""
if g.Status != Completed && g.Status != Failed && !instructor {
poll = ` hx-get="generations/` + template.HTMLEscapeString(g.ID) + `/status" hx-trigger="every 5s" hx-swap="outerHTML"`
}
fmt.Fprintf(w, `%s%s
`, poll, template.HTMLEscapeString(strings.ReplaceAll(string(g.Status), "_", " ")), g.CreatedAt.Format(time.RFC3339))
if instructor {
fmt.Fprintf(w, `Reviewed prompt
%s
`, template.HTMLEscapeString(g.ReviewedPrompt))
if g.ProviderJobID != "" {
fmt.Fprintf(w, `Provider job: %s`, template.HTMLEscapeString(g.ProviderJobID))
}
}
if g.Status == Completed {
prefix := ""
if instructor {
prefix = "generations/"
}
fmt.Fprintf(w, `Download`, prefix, g.ID, prefix, g.ID)
} else if g.Status == Failed {
fmt.Fprintf(w, `%s
`, template.HTMLEscapeString(g.ErrorMessage))
} else {
fmt.Fprint(w, `Working…
`)
}
fmt.Fprint(w, "")
}
func (t *Tool) instructorPage(w http.ResponseWriter, r *http.Request) {
render(w, instructorPage, map[string]any{"Outputs": t.outputs()})
}
func (t *Tool) instructorGenerations(w http.ResponseWriter, r *http.Request) {
for _, g := range t.store.list("") {
t.renderCard(w, g, true)
}
}
func (t *Tool) instructorVideo(w http.ResponseWriter, r *http.Request) {
t.generationFile(w, r, true, false)
}
func (t *Tool) instructorDownload(w http.ResponseWriter, r *http.Request) {
t.generationFile(w, r, true, true)
}
type outputFile struct {
Name string
Size int64
Modified time.Time
}
func (t *Tool) outputs() []outputFile {
entries, err := os.ReadDir(t.cfg.OutputDirectory)
if err != nil {
return nil
}
out := []outputFile{}
for _, e := range entries {
if e.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(e.Name()))
if ext != ".mp4" && ext != ".webm" && ext != ".mov" {
continue
}
info, err := e.Info()
if err == nil {
out = append(out, outputFile{Name: e.Name(), Size: info.Size(), Modified: info.ModTime()})
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Modified.After(out[j].Modified) })
return out
}
func (t *Tool) outputDownload(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("filename")
if name != filepath.Base(name) {
http.NotFound(w, r)
return
}
path := filepath.Join(t.cfg.OutputDirectory, name)
if _, err := os.Stat(path); err != nil {
http.NotFound(w, r)
return
}
serveFile(w, r, path, mime.TypeByExtension(filepath.Ext(name)), r.URL.Query().Get("preview") != "1")
}
const toolPage = ``
const instructorPage = `Current process generations
Downloaded output files
Files survive restarts; this is not a complete audit log.
{{range .Outputs}}{{.Name}}{{.Modified.UTC.Format "2006-01-02 15:04:05Z"}} · {{.Size}} bytesDownload{{else}}No downloaded videos yet.
{{end}}`