Files
tapir/internal/web/processing.go
T
mathiasandClaude Opus 4.8 62acfee2ed feat(web): paste-a-URL handler — add an arbitrary video + summarize (Feature 2)
POST /paste: parse the video id, fetch metadata via the VideoFetcher port (Data
API, ungated), upsert a subscription-less row scoped to the user (idempotent =
dedup), and — unless already summarized — RequestSummarize + start immediate
processing through the SAME globalFetchGate as the Summarize button. Explicit
paste overrides the recency window; a captionless video degrades to the honest
'no transcript' terminal state via the engine (ADR-010). Invalid URL -> 400,
not-found -> 404, both add nothing. Route mounts only when a Fetcher is wired.

Moves the video-not-found sentinel to domain (shared by adapter + web, no
cross-adapter coupling). Tests: valid add+queue, invalid, not-found, dedup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:54:28 +02:00

53 lines
2.0 KiB
Go

package web
import (
"context"
"sync"
"gitea.d-ma.be/mathias/tapir/internal/domain"
)
// Processor runs the core summarization use case for a single already-discovered
// video — resolve its transcript, summarize, deliver to the store. *usecase.Engine
// wrapped with the store satisfies it (wired in cmd/tapir). Optional on App: a nil
// Processor means queue-only — the "Summarize" button only flips the DB flag and
// the next `tapir run` does the work.
type Processor interface {
ProcessVideo(ctx context.Context, userID, videoID string) error
}
// VideoFetcher resolves an arbitrary YouTube video id to its metadata for the
// paste-a-URL flow (Feature 2). It is a Data API call, NOT the rate-limited
// caption path. Returns domain.ErrVideoNotFound for a deleted/private/typo'd id.
// cmd/tapir wires a per-user YouTube adapter; nil disables the paste route.
type VideoFetcher interface {
FetchVideo(ctx context.Context, userID, videoID string) (domain.Video, error)
}
// ProcessingSet tracks the (user, video) ids currently being summarized in-process
// so the status endpoint can show the animation until the summary lands. It is
// ephemeral (single-instance Stage-1): a restart drops it, and the DB holds the
// durable state — the summary is present, or summarize_requested is still set so
// `tapir run` retries. The zero value is ready to use; methods are concurrency-safe.
type ProcessingSet struct {
m sync.Map
}
// Add marks a key in-flight.
func (p *ProcessingSet) Add(key string) { p.m.Store(key, struct{}{}) }
// Remove clears a key once its summarization finishes (success or failure).
func (p *ProcessingSet) Remove(key string) { p.m.Delete(key) }
// Has reports whether a key is currently in-flight.
func (p *ProcessingSet) Has(key string) bool {
_, ok := p.m.Load(key)
return ok
}
// processingKey scopes the in-flight key by user so one user's summarization is
// never confused with another's for the same video id.
func processingKey(userID, videoID string) string {
return userID + "|" + videoID
}