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 Library *libraryView Video *videoView } 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 Cancel(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) ListSucceededJobs(context.Context, int) ([]store.Job, error) GetSucceededJob(context.Context, int64) (store.Job, error) DeleteSucceededJob(context.Context, int64) error } type server struct { fetcher NewerFetcher jobs JobStore videoDir string 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, videoDir string, 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) } if videoDir == "" { videoDir = "/videos" } s := &server{fetcher: fetcher, jobs: jobs, videoDir: videoDir, 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("POST /subscriptions/from-url/confirm", s.confirmSubscriptionURL) mux.HandleFunc("POST /subscriptions/from-url/register", s.registerSubscriptionURL) mux.HandleFunc("GET /subscriptions/from-url/cancel", s.cancelSubscriptionURL) 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) mux.HandleFunc("GET /library", s.libraryPage) mux.HandleFunc("GET /library/{id}", s.videoPage) mux.HandleFunc("GET /videos/{id}", s.serveVideo) mux.HandleFunc("POST /library/{id}/delete", s.deleteVideo) return mux }