// Package playback manages video playback, including episode sources and subtitle management. package playback import ( "context" "fmt" "mal/integrations/jikan" "mal/internal/domain" "mal/internal/observability" errlog "mal/pkg" netutil "mal/pkg/net" "net/http" "time" ) type playbackService struct { repo domain.PlaybackRepository providers []domain.Provider jikan *jikan.Client episodes domain.EpisodeService httpClient *http.Client proxyTokenKey string proxyTokens *proxyTokenStore auditSvc domain.AuditService } type ProxyTokenKey string func NewPlaybackService(repo domain.PlaybackRepository, providers []domain.Provider, jikan *jikan.Client, episodes domain.EpisodeService, auditSvc domain.AuditService, proxyTokenKey ProxyTokenKey) domain.PlaybackService { return &playbackService{ repo: repo, providers: providers, jikan: jikan, episodes: episodes, auditSvc: auditSvc, httpClient: netutil.NewClient(), proxyTokenKey: string(proxyTokenKey), proxyTokens: newProxyTokenStore(), } } func (s *playbackService) SignProxyToken(targetURL, referer, scope string) (string, error) { if s.proxyTokenKey == "" { return "", nil } return s.proxyTokens.create(targetURL, referer, scope, 2*time.Hour, time.Now()) } func (s *playbackService) ResolveProxyToken(token string, scope string) (string, string, error) { if s.proxyTokenKey == "" { return "", "", fmt.Errorf("proxy token key not configured") } target, err := s.proxyTokens.resolve(token, time.Now()) if err != nil { return "", "", err } if target.scope != scope { return "", "", fmt.Errorf("invalid proxy token scope") } return target.targetURL, target.referer, nil } func (s *playbackService) warmStreamURL(targetURL, referer string) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil) if err != nil { return } if referer != "" { req.Header.Set("Referer", referer) } req.Header.Set("User-Agent", netutil.Firefox121) resp, err := s.httpClient.Do(req) if err != nil { if resp != nil { errlog.Close(resp.Body, "failed to close warm stream error response body") } observability.LogJSON(observability.LogLevelWarn, "warm_stream_failed", "playback", err.Error(), map[string]any{"url": targetURL}, nil) return } errlog.Log("failed to close warm stream response body", resp.Body.Close()) }