ライブラリを番組一覧・番組ページ・再生の3段にする(ステップ12)
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
@@ -29,6 +30,70 @@ func completeTestJob(t *testing.T, s *Store, id string) Job {
|
||||
return result
|
||||
}
|
||||
|
||||
func TestLibrarySeriesAggregationAndJobs(t *testing.T) {
|
||||
s := New(testPool(t, true))
|
||||
ctx := context.Background()
|
||||
complete := func(ep Episode, size int64, finished time.Time) Job {
|
||||
t.Helper()
|
||||
job, _, err := s.Enqueue(ctx, ep, "manual")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
claimed, err := s.ClaimNext(ctx)
|
||||
if err != nil || claimed.ID != job.ID {
|
||||
t.Fatalf("claim: %+v %v", claimed, err)
|
||||
}
|
||||
if err := s.Complete(ctx, job.ID, ep.SeriesTitle+"/video.mp4", size); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.pool.Exec(ctx, `UPDATE download_jobs SET finished_at=$2 WHERE id=$1`, job.ID, finished); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := s.GetSucceededJob(ctx, job.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
older := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
|
||||
newer := older.Add(time.Hour)
|
||||
newest := newer.Add(time.Hour)
|
||||
first := complete(Episode{ID: "aggregate-a1", SeriesID: "seriesA", SeriesTitle: "旧番組名", Title: "第1話", BroadcasterName: "旧局", ThumbnailPath: "/old.jpg"}, 100, older)
|
||||
second := complete(Episode{ID: "aggregate-a2", SeriesID: "seriesA", SeriesTitle: "新番組名", Title: "第2話", BroadcasterName: "新局", ThumbnailPath: "/new.jpg"}, 250, newer)
|
||||
complete(Episode{ID: "aggregate-b1", SeriesID: "seriesB", SeriesTitle: "別番組", Title: "第1話", BroadcasterName: "別局", ThumbnailPath: "/other.jpg"}, 500, newest)
|
||||
if _, err := s.pool.Exec(ctx, `UPDATE download_jobs SET file_size_bytes=NULL WHERE id=$1`, first.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.pool.Exec(ctx, `INSERT INTO subscriptions (series_id, series_title) VALUES ('seriesA', '登録名')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
series, err := s.ListLibrarySeries(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(series) != 2 || series[0].SeriesID != "seriesB" || series[1].SeriesID != "seriesA" {
|
||||
t.Fatalf("番組の並び: %+v", series)
|
||||
}
|
||||
got := series[1]
|
||||
if got.SeriesTitle != "新番組名" || got.LatestTitle != "第2話" || got.BroadcasterName != "新局" || got.ThumbnailPath != "/new.jpg" || got.EpisodeCount != 2 || got.TotalSizeBytes != 250 || got.LatestFinishedAt == nil || !got.LatestFinishedAt.Equal(newer) || !got.Subscribed {
|
||||
t.Fatalf("番組集計: %+v", got)
|
||||
}
|
||||
if series[0].Subscribed || series[0].TotalSizeBytes != 500 {
|
||||
t.Fatalf("未登録番組: %+v", series[0])
|
||||
}
|
||||
jobs, err := s.ListSucceededJobsBySeries(ctx, "seriesA")
|
||||
if err != nil || len(jobs) != 2 || jobs[0].ID != second.ID || jobs[1].ID != first.ID || jobs[1].FileSizeBytes != nil {
|
||||
t.Fatalf("番組の回: %+v %v", jobs, err)
|
||||
}
|
||||
if jobs, err := s.ListSucceededJobsBySeries(ctx, "missing"); err != nil || len(jobs) != 0 {
|
||||
t.Fatalf("存在しない番組: %+v %v", jobs, err)
|
||||
}
|
||||
if _, err := s.ListSucceededJobsBySeries(ctx, ""); err == nil {
|
||||
t.Fatal("空の番組IDを許した")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryStoreOperations(t *testing.T) {
|
||||
s := New(testPool(t, true))
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -42,6 +42,14 @@ type Job struct {
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type LibrarySeries struct {
|
||||
SeriesID, SeriesTitle, LatestTitle string
|
||||
BroadcasterName, ThumbnailPath string
|
||||
EpisodeCount, TotalSizeBytes int64
|
||||
LatestFinishedAt *time.Time
|
||||
Subscribed bool
|
||||
}
|
||||
|
||||
type Progress struct {
|
||||
Phase string
|
||||
DownloadedBytes *int64
|
||||
@@ -138,6 +146,61 @@ func (s *Store) ListSucceededJobs(ctx context.Context, limit int) ([]Job, error)
|
||||
return jobs, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) ListLibrarySeries(ctx context.Context) ([]LibrarySeries, error) {
|
||||
rows, err := s.pool.Query(ctx, `WITH totals AS (
|
||||
SELECT series_id, count(*) AS episode_count,
|
||||
COALESCE(sum(file_size_bytes), 0) AS total_size_bytes
|
||||
FROM download_jobs WHERE status='succeeded' GROUP BY series_id
|
||||
), latest AS (
|
||||
SELECT DISTINCT ON (series_id) series_id, series_title, title,
|
||||
broadcaster_name, thumbnail_path, finished_at, id
|
||||
FROM download_jobs WHERE status='succeeded'
|
||||
ORDER BY series_id, finished_at DESC NULLS LAST, id DESC
|
||||
)
|
||||
SELECT l.series_id, l.series_title, l.title, l.broadcaster_name,
|
||||
l.thumbnail_path, t.episode_count, t.total_size_bytes, l.finished_at,
|
||||
EXISTS (SELECT 1 FROM subscriptions s WHERE s.series_id=l.series_id)
|
||||
FROM latest l JOIN totals t USING (series_id)
|
||||
ORDER BY l.finished_at DESC NULLS LAST, l.id DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var result []LibrarySeries
|
||||
for rows.Next() {
|
||||
var item LibrarySeries
|
||||
if err := rows.Scan(&item.SeriesID, &item.SeriesTitle, &item.LatestTitle,
|
||||
&item.BroadcasterName, &item.ThumbnailPath, &item.EpisodeCount,
|
||||
&item.TotalSizeBytes, &item.LatestFinishedAt, &item.Subscribed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) ListSucceededJobsBySeries(ctx context.Context, seriesID string) ([]Job, error) {
|
||||
if seriesID == "" {
|
||||
return nil, errors.New("series ID must not be empty")
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `SELECT `+jobColumns+` FROM download_jobs
|
||||
WHERE status='succeeded' AND series_id=$1
|
||||
ORDER BY finished_at DESC NULLS LAST, id DESC`, seriesID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var jobs []Job
|
||||
for rows.Next() {
|
||||
job, err := scanJob(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
return jobs, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSucceededJob(ctx context.Context, id int64) error {
|
||||
tag, err := s.pool.Exec(ctx, `DELETE FROM download_jobs WHERE id=$1 AND status='succeeded'`, id)
|
||||
return transitionResult(tag.RowsAffected(), err)
|
||||
|
||||
Reference in New Issue
Block a user