登録番組の定期確認を追加(24 時間ごと、予告の除外、確認エラーの記録)

This commit is contained in:
2026-10-03 23:42:25 +09:00
parent 82536c6398
commit 944086c8ae
14 changed files with 739 additions and 18 deletions
+2 -3
View File
@@ -14,8 +14,7 @@ import (
)
const (
previewMaxSeconds = 180
newerCacheTTL = 10 * time.Minute
newerCacheTTL = 10 * time.Minute
)
var platformTokenPattern = regexp.MustCompile(`(?i)(platform_token=)[^&\s]+`)
@@ -137,7 +136,7 @@ func (s *server) newerView(ctx context.Context, options newerOptions) newerView
}
visible := make([]tver.Episode, 0, len(episodes))
for _, episode := range episodes {
if options.hidePreviews && episode.Duration <= previewMaxSeconds {
if options.hidePreviews && episode.Duration <= tver.PreviewMaxSeconds {
continue
}
visible = append(visible, episode)
+5
View File
@@ -15,6 +15,7 @@ type subscriptionActionView struct {
type subscriptionView struct {
ID, Title, CreatedAtText, LastCheckedText string
CheckErrorAtText, CheckErrorText string
Enabled bool
Jobs int64
}
@@ -136,6 +137,10 @@ func (s *server) loadSubscriptions(ctx context.Context) (subscriptionsView, erro
if sub.LastCheckedAt != nil {
item.LastCheckedText = sub.LastCheckedAt.In(s.zone).Format("2006-01-02 15:04 MST")
}
if sub.LastCheckErrorAt != nil && sub.LastCheckError != nil {
item.CheckErrorAtText = sub.LastCheckErrorAt.In(s.zone).Format("2006-01-02 15:04 MST")
item.CheckErrorText = *sub.LastCheckError
}
view.Items = append(view.Items, item)
}
return view, nil
+7 -4
View File
@@ -159,9 +159,12 @@ func TestSubscriptionMutationsWithoutHtmxRedirect(t *testing.T) {
func TestSubscriptionsPagePauseResumeDeleteAndEscaping(t *testing.T) {
checked := time.Date(2026, 10, 4, 2, 30, 0, 0, time.UTC)
failedAt := time.Date(2026, 10, 4, 3, 45, 0, 0, time.UTC)
checkError := `<script>alert("error")</script> token=[REDACTED]`
jobs := &fakeJobStore{subscriptions: map[string]store.Subscription{
"sr1": {SeriesID: "sr1", SeriesTitle: `<script>alert(1)</script>`, Enabled: true,
CreatedAt: time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC), LastCheckedAt: &checked, JobCount: 3},
CreatedAt: time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC), LastCheckedAt: &checked,
LastCheckErrorAt: &failedAt, LastCheckError: &checkError, JobCount: 3},
"sr2": {SeriesID: "sr2", SeriesTitle: "別番組", Enabled: false, JobCount: 0},
}}
handler := newTestHandlerWithStore(&fakeFetcher{}, jobs, nil)
@@ -170,13 +173,13 @@ func TestSubscriptionsPagePauseResumeDeleteAndEscaping(t *testing.T) {
t.Fatalf("登録番組ページ: %d", page.Code)
}
body := page.Body.String()
for _, want := range []string{"&lt;script&gt;", "登録日時: 2026-10-03 09:00 JST", "最終確認: 2026-10-04 11:30 JST", "最終確認: 未確認", "関連ジョブ: 3件", "有効", "一時停止", `hx-confirm="番組の登録を解除しますか?"`} {
for _, want := range []string{"&lt;script&gt;", "登録日時: 2026-10-03 09:00 JST", "最終確認: 2026-10-04 11:30 JST", "最終確認: 未確認", "最後の確認エラー(2026-10-04 12:45 JST)", `&lt;script&gt;alert(&#34;error&#34;)&lt;/script&gt; token=[REDACTED]`, "関連ジョブ: 3件", "有効", "一時停止", `hx-confirm="番組の登録を解除しますか?"`} {
if !strings.Contains(body, want) {
t.Errorf("登録番組の情報 %q がない", want)
}
}
if strings.Contains(body, `<script>alert(1)</script>`) {
t.Fatal("番組名がエスケープされていない")
if strings.Contains(body, `<script>`) {
t.Fatal("番組名または確認エラーがエスケープされていない")
}
paused := postForm(handler, "/subscriptions/sr1/pause", nil)
if paused.Code != http.StatusOK || jobs.subscriptions["sr1"].Enabled || !strings.Contains(paused.Body.String(), `action="/subscriptions/sr1/resume"`) || strings.Contains(paused.Body.String(), "<html") {
@@ -27,6 +27,9 @@
<article class="subscription-item">
<h2>{{.Title}}</h2>
<p>登録日時: {{.CreatedAtText}}/最終確認: {{.LastCheckedText}}/状態: {{if .Enabled}}有効{{else}}一時停止{{end}}/関連ジョブ: {{.Jobs}}件</p>
{{if .CheckErrorText}}
<details class="error"><summary>最後の確認エラー({{.CheckErrorAtText}})</summary><p>{{.CheckErrorText}}</p></details>
{{end}}
{{if .Enabled}}
<form action="/subscriptions/{{.ID}}/pause" method="post" hx-post="/subscriptions/{{.ID}}/pause" hx-target="#subscriptions-list" hx-swap="outerHTML"><button type="submit">一時停止</button></form>
{{else}}