0.2.68
This commit is contained in:
@@ -76,6 +76,7 @@ type LoadGauge interface {
|
||||
// missing from the image.
|
||||
type Deps struct {
|
||||
Repository Repository
|
||||
History HistoryRepository
|
||||
Resolver MediaResolver
|
||||
Source CandidateSource
|
||||
Detector Detector
|
||||
@@ -87,6 +88,7 @@ type Deps struct {
|
||||
|
||||
type Service struct {
|
||||
repo Repository
|
||||
history HistoryRepository
|
||||
resolver MediaResolver
|
||||
source CandidateSource
|
||||
detector Detector
|
||||
@@ -94,6 +96,7 @@ type Service struct {
|
||||
load LoadGauge
|
||||
log *slog.Logger
|
||||
cfg Config
|
||||
cfgMu sync.RWMutex
|
||||
|
||||
queue *Queue
|
||||
flight *flightGroup
|
||||
@@ -107,10 +110,7 @@ type Service struct {
|
||||
}
|
||||
|
||||
func New(deps Deps) *Service {
|
||||
cfg := deps.Config
|
||||
if cfg.QueueLimit <= 0 {
|
||||
cfg = DefaultConfig()
|
||||
}
|
||||
cfg := NormaliseConfig(deps.Config)
|
||||
detector := deps.Detector
|
||||
if detector == nil {
|
||||
detector = noopDetector{}
|
||||
@@ -121,6 +121,7 @@ func New(deps Deps) *Service {
|
||||
}
|
||||
return &Service{
|
||||
repo: deps.Repository,
|
||||
history: deps.History,
|
||||
resolver: deps.Resolver,
|
||||
source: deps.Source,
|
||||
detector: detector,
|
||||
@@ -181,9 +182,37 @@ func (s *Service) Refresh(ctx context.Context) (string, error) {
|
||||
}
|
||||
wanted = append(wanted, candidate)
|
||||
}
|
||||
deferred := 0
|
||||
cfg := s.Configuration()
|
||||
if s.history != nil && cfg.RetryCooldown > 0 && len(wanted) > 0 {
|
||||
ids := make([]string, 0, len(wanted))
|
||||
for _, candidate := range wanted {
|
||||
ids = append(ids, candidate.ItemID)
|
||||
}
|
||||
recent, historyErr := s.history.RecentScanTimes(
|
||||
ctx, ids, time.Now().UTC().Add(-cfg.RetryCooldown),
|
||||
)
|
||||
if historyErr != nil {
|
||||
s.log.Debug("credits scan history unavailable", "error", historyErr)
|
||||
} else if len(recent) > 0 {
|
||||
eligible := wanted[:0]
|
||||
for _, candidate := range wanted {
|
||||
if _, coolingDown := recent[candidate.ItemID]; coolingDown {
|
||||
deferred++
|
||||
continue
|
||||
}
|
||||
eligible = append(eligible, candidate)
|
||||
}
|
||||
wanted = eligible
|
||||
}
|
||||
}
|
||||
s.queue.Replace(wanted)
|
||||
|
||||
if len(wanted) == 0 {
|
||||
if deferred > 0 {
|
||||
return fmt.Sprintf("%d candidate%s cooling down, %d already known",
|
||||
deferred, plural(deferred), skipped), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
for _, candidate := range wanted {
|
||||
@@ -191,8 +220,8 @@ func (s *Service) Refresh(ctx context.Context) (string, error) {
|
||||
"item", candidate.ItemID, "reason", candidate.Reason,
|
||||
"priority", candidate.Priority, "users", candidate.UserCount)
|
||||
}
|
||||
return fmt.Sprintf("%d candidate%s queued, %d already known",
|
||||
len(wanted), plural(len(wanted)), skipped), nil
|
||||
return fmt.Sprintf("%d candidate%s queued, %d cooling down, %d already known",
|
||||
len(wanted), plural(len(wanted)), deferred, skipped), nil
|
||||
}
|
||||
|
||||
// NotePlayback is the live signal, and the strongest one there is: somebody is watching this
|
||||
@@ -303,10 +332,12 @@ func (s *Service) Run(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
started := time.Now().UTC()
|
||||
scanCtx, cancel := context.WithTimeout(ctx, scanBudget)
|
||||
detection, stored, err := s.Process(scanCtx, candidate.ItemID)
|
||||
cancel()
|
||||
s.queue.Release(candidate.ItemID)
|
||||
s.recordAttempt(ctx, candidate, detection, stored, err, started)
|
||||
|
||||
switch {
|
||||
case err != nil && errors.Is(err, context.Canceled):
|
||||
@@ -496,6 +527,61 @@ func (s *Service) busy(ctx context.Context) bool {
|
||||
return s.load != nil && s.load.Busy(ctx)
|
||||
}
|
||||
|
||||
// Configuration is a snapshot safe to expose through the admin API.
|
||||
func (s *Service) Configuration() Config {
|
||||
s.cfgMu.RLock()
|
||||
defer s.cfgMu.RUnlock()
|
||||
return s.cfg
|
||||
}
|
||||
|
||||
// Configure applies persisted operator tuning to future candidate refreshes and trims an
|
||||
// over-full queue immediately. A scan already in flight is deliberately left alone.
|
||||
func (s *Service) Configure(cfg Config) {
|
||||
cfg = NormaliseConfig(cfg)
|
||||
s.cfgMu.Lock()
|
||||
s.cfg = cfg
|
||||
s.cfgMu.Unlock()
|
||||
if source, ok := s.source.(ConfigurableCandidateSource); ok {
|
||||
source.Configure(cfg)
|
||||
}
|
||||
s.queue.SetLimit(cfg.QueueLimit)
|
||||
}
|
||||
|
||||
func (s *Service) recordAttempt(
|
||||
ctx context.Context, candidate Candidate, detection Detection, stored bool, scanErr error,
|
||||
started time.Time,
|
||||
) {
|
||||
if s.history == nil {
|
||||
return
|
||||
}
|
||||
outcome := "no_match"
|
||||
switch {
|
||||
case scanErr != nil:
|
||||
outcome = "failed"
|
||||
case stored:
|
||||
outcome = "detected"
|
||||
case detection.Found:
|
||||
outcome = "unchanged"
|
||||
}
|
||||
errorText := ""
|
||||
if scanErr != nil {
|
||||
errorText = scanErr.Error()
|
||||
}
|
||||
finished := time.Now().UTC()
|
||||
writeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := s.history.SaveScanAttempt(writeCtx, ScanAttempt{
|
||||
ItemID: candidate.ItemID, SeriesID: candidate.SeriesID,
|
||||
Season: candidate.Season, Episode: candidate.Episode,
|
||||
Reason: candidate.Reason, Priority: candidate.Priority, Outcome: outcome,
|
||||
MarkerMs: detection.StartMs, Confidence: detection.Confidence,
|
||||
Method: detection.Method, Frames: detection.FramesSampled, Error: errorText,
|
||||
StartedAt: started, FinishedAt: finished,
|
||||
}); err != nil {
|
||||
s.log.Debug("credits scan history write failed", "item", candidate.ItemID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// QueueDepth is what the console reads.
|
||||
func (s *Service) QueueDepth() int { return s.queue.Len() }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user