package web import ( "context" "sort" "time" "gitea.kazu634.com/kazu634/tver/internal/store" "github.com/jackc/pgx/v5" ) type fakeJobStore struct { jobs map[string]store.Job subscriptions map[string]store.Subscription nextID int64 lastEpisode store.Episode enqueueCalls, statusCalls int statusIDs []string listStatuses []string listLimits []int registerCalls int registerEpisodes []store.Episode registerDownload bool registerErr error subscribedCalls int subscribedIDs []string beforeDeleteSucceeded func() err error } func (f *fakeJobStore) ensure() { if f.jobs == nil { f.jobs = make(map[string]store.Job) } } func (f *fakeJobStore) Enqueue(_ context.Context, ep store.Episode, source string) (store.Job, bool, error) { f.enqueueCalls++ f.lastEpisode = ep if f.err != nil { return store.Job{}, false, f.err } f.ensure() if job, ok := f.jobs[ep.ID]; ok { return job, false, nil } f.nextID++ job := store.Job{ID: f.nextID, EpisodeID: ep.ID, SeriesTitle: ep.SeriesTitle, Title: ep.Title, BroadcasterName: ep.BroadcasterName, Source: source, Status: "queued", CreatedAt: time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC)} f.jobs[ep.ID] = job return job, true, nil } func (f *fakeJobStore) EpisodeStatuses(_ context.Context, ids []string) (map[string]string, error) { f.statusCalls++ f.statusIDs = append([]string(nil), ids...) if f.err != nil { return nil, f.err } result := make(map[string]string) for _, id := range ids { if job, ok := f.jobs[id]; ok { result[id] = job.Status } } return result, nil } func (f *fakeJobStore) ListJobs(_ context.Context, status string, limit int) ([]store.Job, error) { f.listStatuses = append(f.listStatuses, status) f.listLimits = append(f.listLimits, limit) if f.err != nil { return nil, f.err } var result []store.Job for _, job := range f.jobs { if job.Status == status { result = append(result, job) } } sort.Slice(result, func(i, j int) bool { return result[i].ID > result[j].ID }) if len(result) > limit { result = result[:limit] } return result, nil } func (f *fakeJobStore) GetJob(_ context.Context, id int64) (store.Job, error) { if f.err != nil { return store.Job{}, f.err } for _, job := range f.jobs { if job.ID == id { return job, nil } } return store.Job{}, pgx.ErrNoRows } func (f *fakeJobStore) Retry(_ context.Context, id int64) error { for ep, job := range f.jobs { if job.ID == id && (job.Status == "failed" || job.Status == "canceled") { job.Status = "queued" f.jobs[ep] = job return nil } } return store.ErrInvalidTransition } func (f *fakeJobStore) RetryEpisode(_ context.Context, id string) error { job, ok := f.jobs[id] if !ok || (job.Status != "failed" && job.Status != "canceled") { return store.ErrInvalidTransition } job.Status = "queued" f.jobs[id] = job return nil } func (f *fakeJobStore) Cancel(_ context.Context, id int64) error { if f.err != nil { return f.err } for ep, job := range f.jobs { if job.ID == id && (job.Status == "queued" || job.Status == "running") { job.Status = "canceled" f.jobs[ep] = job return nil } } return store.ErrInvalidTransition } func (f *fakeJobStore) CancelQueued(ctx context.Context, id int64) error { return f.Cancel(ctx, id) } var _ JobStore = (*fakeJobStore)(nil) func (f *fakeJobStore) SubscribedSeries(_ context.Context, ids []string) (map[string]bool, error) { f.subscribedCalls++ f.subscribedIDs = append([]string(nil), ids...) if f.err != nil { return nil, f.err } result := make(map[string]bool) for _, id := range ids { _, result[id] = f.subscriptions[id] } return result, nil } func (f *fakeJobStore) RegisterSubscription(_ context.Context, seriesID, title string, episodes []store.Episode, download bool) (bool, error) { f.registerCalls++ f.registerEpisodes = append([]store.Episode(nil), episodes...) f.registerDownload = download if f.registerErr != nil { return false, f.registerErr } if f.err != nil { return false, f.err } if f.subscriptions == nil { f.subscriptions = make(map[string]store.Subscription) } if _, exists := f.subscriptions[seriesID]; exists { return false, nil } var count int64 if download { count = int64(len(episodes)) } checked := time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC) f.subscriptions[seriesID] = store.Subscription{SeriesID: seriesID, SeriesTitle: title, Enabled: true, CreatedAt: checked, LastCheckedAt: &checked, JobCount: count} return true, nil } func (f *fakeJobStore) ListSubscriptions(_ context.Context) ([]store.Subscription, error) { if f.err != nil { return nil, f.err } var result []store.Subscription for _, sub := range f.subscriptions { result = append(result, sub) } sort.Slice(result, func(i, j int) bool { return result[i].SeriesID < result[j].SeriesID }) return result, nil } func (f *fakeJobStore) SetSubscriptionEnabled(_ context.Context, seriesID string, enabled bool) error { if f.err != nil { return f.err } sub, exists := f.subscriptions[seriesID] if !exists { return store.ErrInvalidTransition } sub.Enabled = enabled f.subscriptions[seriesID] = sub return nil } func (f *fakeJobStore) DeleteSubscription(_ context.Context, seriesID string) (bool, error) { if f.err != nil { return false, f.err } if _, exists := f.subscriptions[seriesID]; !exists { return false, nil } delete(f.subscriptions, seriesID) return true, nil } func (f *fakeJobStore) ListSucceededJobs(_ context.Context, limit int) ([]store.Job, error) { if f.err != nil { return nil, f.err } var result []store.Job for _, job := range f.jobs { if job.Status == "succeeded" { result = append(result, job) } } sort.Slice(result, func(i, j int) bool { return result[i].ID > result[j].ID }) if len(result) > limit { result = result[:limit] } return result, nil } func (f *fakeJobStore) ListLibrarySeries(_ context.Context) ([]store.LibrarySeries, error) { if f.err != nil { return nil, f.err } bySeries := make(map[string]*store.LibrarySeries) latestIDs := make(map[string]int64) for _, job := range f.jobs { if job.Status != "succeeded" { continue } item := bySeries[job.SeriesID] if item == nil { item = &store.LibrarySeries{SeriesID: job.SeriesID} bySeries[job.SeriesID] = item } item.EpisodeCount++ if job.FileSizeBytes != nil { item.TotalSizeBytes += *job.FileSizeBytes } current := store.Job{ID: latestIDs[job.SeriesID], FinishedAt: item.LatestFinishedAt} if item.SeriesTitle == "" || jobFinishedLater(job, current) { item.SeriesTitle = job.SeriesTitle item.LatestTitle = job.Title item.BroadcasterName = job.BroadcasterName item.ThumbnailPath = job.ThumbnailPath if job.FinishedAt != nil { finished := *job.FinishedAt item.LatestFinishedAt = &finished } latestIDs[job.SeriesID] = job.ID } _, item.Subscribed = f.subscriptions[job.SeriesID] } result := make([]store.LibrarySeries, 0, len(bySeries)) for _, item := range bySeries { result = append(result, *item) } sort.Slice(result, func(i, j int) bool { left, right := result[i].LatestFinishedAt, result[j].LatestFinishedAt if left == nil || right == nil { if left != right { return left != nil } } else if !left.Equal(*right) { return left.After(*right) } return result[i].SeriesID < result[j].SeriesID }) return result, nil } func (f *fakeJobStore) ListSucceededJobsBySeries(_ context.Context, seriesID string) ([]store.Job, error) { if f.err != nil { return nil, f.err } var result []store.Job for _, job := range f.jobs { if job.Status == "succeeded" && job.SeriesID == seriesID { result = append(result, job) } } sortJobsLatest(result) return result, nil } func (f *fakeJobStore) GetSucceededJob(_ context.Context, id int64) (store.Job, error) { if f.err != nil { return store.Job{}, f.err } for _, job := range f.jobs { if job.ID == id && job.Status == "succeeded" { return job, nil } } return store.Job{}, pgx.ErrNoRows } func (f *fakeJobStore) DeleteSucceededJob(_ context.Context, id int64) error { if f.beforeDeleteSucceeded != nil { f.beforeDeleteSucceeded() } if f.err != nil { return f.err } for episodeID, job := range f.jobs { if job.ID == id && job.Status == "succeeded" { delete(f.jobs, episodeID) return nil } } return store.ErrInvalidTransition }