ライブラリを番組一覧・番組ページ・再生の3段にする(ステップ12)
This commit is contained in:
@@ -16,7 +16,7 @@ import (
|
||||
func libraryJob(id int64, relative string) store.Job {
|
||||
size := int64(10)
|
||||
finished := time.Date(2026, 10, 3, 1, 2, 3, 0, time.UTC)
|
||||
return store.Job{ID: id, EpisodeID: "ep1", SeriesTitle: `<script>番組</script>`, Title: `<b>第1話</b>`,
|
||||
return store.Job{ID: id, EpisodeID: "ep1", SeriesID: "series1", SeriesTitle: `<script>番組</script>`, Title: `<b>第1話</b>`,
|
||||
BroadcasterName: "放送局", DurationSeconds: 61, ThumbnailPath: "/thumb.jpg", Status: "succeeded",
|
||||
FilePath: &relative, FileSizeBytes: &size, FinishedAt: &finished}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func TestLibraryAndPlaybackPage(t *testing.T) {
|
||||
t.Fatalf("library status=%d", page.Code)
|
||||
}
|
||||
body := page.Body.String()
|
||||
for _, want := range []string{"<script>番組</script>", "<b>第1話</b>", "放送局", "長さ: 2分", "10.0 B", "2026-10-03 10:02 JST", "https://statics.tver.jp/thumb.jpg", `/library/1`} {
|
||||
for _, want := range []string{"<script>番組</script>", "<b>第1話</b>", "放送局", "1番組・1本・0.00 GB", "10.0 B", "2026-10-03 10:02 JST", "https://statics.tver.jp/thumb.jpg", `/library/series/series1`, "1本"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("library missing %q", want)
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func TestLibraryAndPlaybackPage(t *testing.T) {
|
||||
t.Fatal("library fields were not escaped")
|
||||
}
|
||||
video := getPage(handler, "/library/1")
|
||||
if video.Code != http.StatusOK || !strings.Contains(video.Body.String(), `<video controls`) || !strings.Contains(video.Body.String(), `src="/videos/1"`) || !strings.Contains(video.Body.String(), `hx-confirm="動画ファイルと記録を削除しますか?"`) {
|
||||
if video.Code != http.StatusOK || !strings.Contains(video.Body.String(), `<video controls`) || !strings.Contains(video.Body.String(), `src="/videos/1"`) || !strings.Contains(video.Body.String(), `hx-confirm="動画ファイルと記録を削除しますか?"`) || !strings.Contains(video.Body.String(), `/library/series/series1`) || !strings.Contains(video.Body.String(), `前の回はありません`) || !strings.Contains(video.Body.String(), `次の回はありません`) {
|
||||
t.Fatalf("video page: %d %s", video.Code, video.Body.String())
|
||||
}
|
||||
for _, path := range []string{"/library/bad", "/library/99", "/library/1/delete"} {
|
||||
@@ -57,6 +57,111 @@ func TestLibraryAndPlaybackPage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibrarySeriesPageSortingSubscriptionAndNotFound(t *testing.T) {
|
||||
old := libraryJob(2, "show/old.mp4")
|
||||
old.EpisodeID = "ep2"
|
||||
old.Title = "第2話"
|
||||
old.SeriesTitle = "旧番組名"
|
||||
old.BroadcasterName = "旧局"
|
||||
oldFinished := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
|
||||
old.FinishedAt = &oldFinished
|
||||
latest := libraryJob(10, "show/latest.mp4")
|
||||
latest.EpisodeID = "ep10"
|
||||
latest.Title = "第10話"
|
||||
latest.SeriesTitle = `<em>新番組名</em>`
|
||||
latest.BroadcasterName = "新局"
|
||||
latestFinished := oldFinished.Add(time.Hour)
|
||||
latest.FinishedAt = &latestFinished
|
||||
jobs := &fakeJobStore{
|
||||
jobs: map[string]store.Job{"ep2": old, "ep10": latest},
|
||||
subscriptions: map[string]store.Subscription{"series1": {SeriesID: "series1"}},
|
||||
}
|
||||
handler := newTestHandlerWithVideoDir(&fakeFetcher{}, jobs, t.TempDir(), nil)
|
||||
library := getPage(handler, "/library")
|
||||
if library.Code != http.StatusOK || !strings.Contains(library.Body.String(), "登録中") || !strings.Contains(library.Body.String(), "2本") {
|
||||
t.Fatalf("library series card: status=%d body=%s", library.Code, library.Body.String())
|
||||
}
|
||||
|
||||
page := getPage(handler, "/library/series/series1")
|
||||
if page.Code != http.StatusOK {
|
||||
t.Fatalf("series status=%d body=%s", page.Code, page.Body.String())
|
||||
}
|
||||
body := page.Body.String()
|
||||
for _, want := range []string{"ライブラリ", "<em>新番組名</em>", "登録中", "新局・2本・20.0 B", `/library/10`, `/library/2`, `?sort=natural`} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("series page missing %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Index(body, `/library/10`) > strings.Index(body, `/library/2`) {
|
||||
t.Fatal("default order is not latest first")
|
||||
}
|
||||
natural := getPage(handler, "/library/series/series1?sort=natural")
|
||||
if natural.Code != http.StatusOK || strings.Index(natural.Body.String(), `/library/2`) > strings.Index(natural.Body.String(), `/library/10`) {
|
||||
t.Fatalf("natural order: status=%d body=%s", natural.Code, natural.Body.String())
|
||||
}
|
||||
if got := getPage(handler, "/library/series/missing").Code; got != http.StatusNotFound {
|
||||
t.Fatalf("missing series status=%d", got)
|
||||
}
|
||||
if got := getPage(handler, "/library/series/bad-id").Code; got != http.StatusNotFound {
|
||||
t.Fatalf("invalid series status=%d", got)
|
||||
}
|
||||
if got := getPage(handler, "/library/series/series1?sort=bad").Code; got != http.StatusBadRequest {
|
||||
t.Fatalf("invalid sort status=%d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryOmitsSubscriptionMarkerForUnregisteredSeries(t *testing.T) {
|
||||
jobs := &fakeJobStore{jobs: map[string]store.Job{"ep1": libraryJob(1, "show/video.mp4")}}
|
||||
page := getPage(newTestHandlerWithVideoDir(&fakeFetcher{}, jobs, t.TempDir(), nil), "/library")
|
||||
if page.Code != http.StatusOK || strings.Contains(page.Body.String(), "登録中") {
|
||||
t.Fatalf("unregistered series: status=%d body=%s", page.Code, page.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryPagesDoNotInspectVideoDirectory(t *testing.T) {
|
||||
missingVideoDir := filepath.Join(t.TempDir(), "missing-video-root")
|
||||
jobs := &fakeJobStore{jobs: map[string]store.Job{"ep1": libraryJob(1, "missing/video.mp4")}}
|
||||
handler := newTestHandlerWithVideoDir(&fakeFetcher{}, jobs, missingVideoDir, nil)
|
||||
for _, path := range []string{"/library", "/library/series/series1"} {
|
||||
response := getPage(handler, path)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Errorf("DB由来の一覧 %s: status=%d body=%s", path, response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(missingVideoDir); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("一覧表示が動画ルートを作成または走査した: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaybackPreviousAndNextNaturalOrder(t *testing.T) {
|
||||
jobs := make(map[string]store.Job)
|
||||
for _, data := range []struct {
|
||||
id int64
|
||||
epid string
|
||||
title string
|
||||
}{{2, "ep2", "第2話"}, {10, "ep10", "第10話"}, {9, "ep9", "第9話"}} {
|
||||
job := libraryJob(data.id, "show/video.mp4")
|
||||
job.EpisodeID = data.epid
|
||||
job.Title = data.title
|
||||
jobs[data.epid] = job
|
||||
}
|
||||
handler := newTestHandlerWithVideoDir(&fakeFetcher{}, &fakeJobStore{jobs: jobs}, t.TempDir(), nil)
|
||||
middle := getPage(handler, "/library/9")
|
||||
for _, want := range []string{`href="/library/2"`, `前の回: 第2話`, `href="/library/10"`, `次の回: 第10話`} {
|
||||
if !strings.Contains(middle.Body.String(), want) {
|
||||
t.Errorf("middle playback missing %q", want)
|
||||
}
|
||||
}
|
||||
first := getPage(handler, "/library/2")
|
||||
if !strings.Contains(first.Body.String(), "前の回はありません") || !strings.Contains(first.Body.String(), `href="/library/9"`) {
|
||||
t.Fatalf("first playback navigation: %s", first.Body.String())
|
||||
}
|
||||
last := getPage(handler, "/library/10")
|
||||
if !strings.Contains(last.Body.String(), "次の回はありません") || !strings.Contains(last.Body.String(), `href="/library/9"`) {
|
||||
t.Fatalf("last playback navigation: %s", last.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryRejectsInvalidJobIDs(t *testing.T) {
|
||||
jobs := &fakeJobStore{jobs: map[string]store.Job{"ep1": libraryJob(1, "show/video.mp4")}}
|
||||
handler := newTestHandlerWithVideoDir(&fakeFetcher{}, jobs, t.TempDir(), nil)
|
||||
@@ -270,12 +375,58 @@ func TestDeleteVideoFileThenRecord(t *testing.T) {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("file remains: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "show")); !os.IsNotExist(err) {
|
||||
t.Fatalf("empty series folder remains: %v", err)
|
||||
}
|
||||
jobs.jobs["ep1"] = libraryJob(1, "show/missing.mp4")
|
||||
if got := postForm(handler, "/library/1/delete", nil).Code; got != http.StatusSeeOther || len(jobs.jobs) != 0 {
|
||||
t.Fatalf("missing file delete: status=%d jobs=%d", got, len(jobs.jobs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRedirectsToSeriesWhenEpisodesRemainAndKeepsNonemptyFolder(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.Mkdir(filepath.Join(dir, "show"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "show", "one.mp4"), []byte("one"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "show", "two.mp4"), []byte("two"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := libraryJob(1, "show/one.mp4")
|
||||
second := libraryJob(2, "show/two.mp4")
|
||||
second.EpisodeID = "ep2"
|
||||
jobs := &fakeJobStore{jobs: map[string]store.Job{"ep1": first, "ep2": second}}
|
||||
handler := newTestHandlerWithVideoDir(&fakeFetcher{}, jobs, dir, nil)
|
||||
response := postForm(handler, "/library/1/delete", nil)
|
||||
if response.Code != http.StatusSeeOther || response.Header().Get("Location") != "/library/series/series1" || len(jobs.jobs) != 1 {
|
||||
t.Fatalf("delete: status=%d location=%q jobs=%d", response.Code, response.Header().Get("Location"), len(jobs.jobs))
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "show")); err != nil {
|
||||
t.Fatalf("nonempty series folder removed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "show", "two.mp4")); err != nil {
|
||||
t.Fatalf("remaining video removed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteVideoAtRootNeverRemovesVideoRoot(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "video.mp4"), []byte("video"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jobs := &fakeJobStore{jobs: map[string]store.Job{"ep1": libraryJob(1, "video.mp4")}}
|
||||
response := postForm(newTestHandlerWithVideoDir(&fakeFetcher{}, jobs, dir, nil), "/library/1/delete", nil)
|
||||
if response.Code != http.StatusSeeOther {
|
||||
t.Fatalf("delete status=%d", response.Code)
|
||||
}
|
||||
if info, err := os.Stat(dir); err != nil || !info.IsDir() {
|
||||
t.Fatalf("video root was removed: info=%v err=%v", info, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteFailureKeepsRecord(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
blocked := filepath.Join(dir, "blocked")
|
||||
|
||||
Reference in New Issue
Block a user