package web
import (
"context"
"embed"
"html/template"
"io/fs"
"log"
"net/http"
"sync"
"time"
"gitea.kazu634.com/kazu634/tver/internal/store"
)
//go:embed templates/*.html static/*
var assets embed.FS
type page struct {
Title string
Newer *newerView
Queue *queueView
Subscriptions *subscriptionsView
}
type JobStore interface {
Enqueue(context.Context, store.Episode, string) (store.Job, bool, error)
EpisodeStatuses(context.Context, []string) (map[string]string, error)
ListJobs(context.Context, string, int) ([]store.Job, error)
GetJob(context.Context, int64) (store.Job, error)
Retry(context.Context, int64) error
RetryEpisode(context.Context, string) error
CancelQueued(context.Context, int64) error
SubscribedSeries(context.Context, []string) (map[string]bool, error)
RegisterSubscription(context.Context, string, string, []store.Episode, bool) (bool, error)
ListSubscriptions(context.Context) ([]store.Subscription, error)
SetSubscriptionEnabled(context.Context, string, bool) error
DeleteSubscription(context.Context, string) (bool, error)
}
type server struct {
fetcher NewerFetcher
jobs JobStore
now func() time.Time
logger *log.Logger
zone *time.Location
tmpl *template.Template
cacheMu sync.Mutex
cache map[string]cacheEntry
}
func NewHandler(fetcher NewerFetcher, jobs JobStore, now func() time.Time, logger *log.Logger) http.Handler {
tmpl := template.Must(template.ParseFS(assets, "templates/*.html"))
staticFiles, err := fs.Sub(assets, "static")
if err != nil {
panic(err)
}
zone, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
s := &server{fetcher: fetcher, jobs: jobs, now: now, logger: logger, zone: zone, tmpl: tmpl, cache: make(map[string]cacheEntry)}
mux := http.NewServeMux()
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(staticFiles)))
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte("ok\n"))
})
mux.HandleFunc("GET /{$}", s.home)
mux.HandleFunc("GET /newer", s.newerFragment)
mux.HandleFunc("POST /episodes/enqueue", s.enqueue)
mux.HandleFunc("POST /episodes/retry", s.retryEpisode)
mux.HandleFunc("GET /queue", s.queuePage)
mux.HandleFunc("GET /queue/list", s.queueFragment)
mux.HandleFunc("POST /jobs/{id}/retry", s.retryJob)
mux.HandleFunc("POST /jobs/{id}/cancel", s.cancelJob)
mux.HandleFunc("GET /subscriptions", s.subscriptionsPage)
mux.HandleFunc("GET /subscriptions/list", s.subscriptionsFragment)
mux.HandleFunc("GET /subscriptions/choice", s.subscriptionChoice)
mux.HandleFunc("POST /subscriptions/register", s.registerSubscription)
mux.HandleFunc("POST /subscriptions/{id}/pause", s.pauseSubscription)
mux.HandleFunc("POST /subscriptions/{id}/resume", s.resumeSubscription)
mux.HandleFunc("POST /subscriptions/{id}/delete", s.deleteSubscription)
for path, title := range map[string]string{
"GET /library": "ライブラリ",
} {
mux.HandleFunc(path, func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.tmpl.ExecuteTemplate(w, "layout", page{Title: title}); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
}
})
}
return mux
}