worker を追加(yt-dlp によるダウンロード、段階別進捗、ffprobe 検査、実行中の取り消し)

This commit is contained in:
2026-10-03 22:56:14 +09:00
parent ebf1cfcbc2
commit 6f150495d5
19 changed files with 1657 additions and 33 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ type JobStore interface {
GetJob(context.Context, int64) (store.Job, error)
Retry(context.Context, int64) error
RetryEpisode(context.Context, string) error
CancelQueued(context.Context, int64) error
Cancel(context.Context, int64) error
SubscribedSeries(context.Context, []string) (map[string]bool, error)
RegisterSubscription(context.Context, string, string, []store.Episode, bool) (bool, error)
ListSubscriptions(context.Context) ([]store.Subscription, error)
+23 -20
View File
@@ -145,34 +145,37 @@ func errorKindText(kind string) string {
}
func (s *server) retryJob(w http.ResponseWriter, r *http.Request) {
s.queueMutation(w, r, s.jobs.Retry, false)
s.queueMutation(w, r, s.jobs.Retry)
}
func (s *server) cancelJob(w http.ResponseWriter, r *http.Request) {
s.queueMutation(w, r, s.jobs.CancelQueued, true)
}
func (s *server) queueMutation(w http.ResponseWriter, r *http.Request, change func(context.Context, int64) error, queuedOnly bool) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || id <= 0 {
http.Error(w, "ジョブ ID が不正です", http.StatusBadRequest)
return
}
if queuedOnly {
job, err := s.jobs.GetJob(r.Context(), id)
if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "ジョブが見つかりません", http.StatusNotFound)
return
}
if err != nil {
s.logger.Printf("ジョブ状態の取得に失敗: %v", err)
http.Error(w, "操作できませんでした", http.StatusInternalServerError)
return
}
if job.Status != "queued" {
http.Error(w, "待機中のジョブだけ取り消せます", http.StatusConflict)
return
}
job, err := s.jobs.GetJob(r.Context(), id)
if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "ジョブが見つかりません", http.StatusNotFound)
return
}
if err != nil {
s.logger.Printf("ジョブ状態の取得に失敗: %v", err)
http.Error(w, "操作できませんでした", http.StatusInternalServerError)
return
}
if job.Status != "queued" && job.Status != "running" {
http.Error(w, "待機中またはダウンロード中のジョブだけ取り消せます", http.StatusConflict)
return
}
s.queueMutation(w, r, s.jobs.Cancel)
}
func (s *server) queueMutation(w http.ResponseWriter, r *http.Request, change func(context.Context, int64) error) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || id <= 0 {
http.Error(w, "ジョブ ID が不正です", http.StatusBadRequest)
return
}
if err := change(r.Context(), id); err != nil {
if errors.Is(err, store.ErrInvalidTransition) {
+8 -4
View File
@@ -155,7 +155,7 @@ func TestQueueGroupsProgressAndPolling(t *testing.T) {
t.Errorf("待ち行列に %q がない", want)
}
}
if strings.Contains(body, errorMessage) || strings.Contains(body, `action="/jobs/1/cancel"`) || strings.Contains(body, "全体") {
if strings.Contains(body, errorMessage) || !strings.Contains(body, `action="/jobs/1/cancel"`) || strings.Contains(body, "全体") {
t.Errorf("不正な詳細・操作・全体進捗: %s", body)
}
fragment := getPage(handler, "/queue/list")
@@ -195,7 +195,7 @@ func TestQueueJobsAppearInCorrectSectionsWithLimit(t *testing.T) {
cancelAction := `action="/jobs/` + string(rune('1'+i)) + `/cancel"`
retryAction := `action="/jobs/` + string(rune('1'+i)) + `/retry"`
switch statuses[i] {
case "queued":
case "running", "queued":
if !strings.Contains(section[1], cancelAction) || strings.Contains(section[1], retryAction) {
t.Errorf("%s の操作が不正: %s", statuses[i], section[1])
}
@@ -246,9 +246,13 @@ func TestQueueCancelRetryAndStopPolling(t *testing.T) {
if got := postForm(handler, "/jobs/bad/cancel", nil).Code; got != http.StatusBadRequest {
t.Fatalf("不正な ID: %d", got)
}
if got := postForm(handler, "/jobs/99/cancel", nil).Code; got != http.StatusNotFound {
t.Fatalf("存在しないジョブ: %d", got)
}
jobs.jobs["ep"] = store.Job{ID: 1, Status: "running"}
if got := postForm(handler, "/jobs/1/cancel", nil).Code; got != http.StatusConflict {
t.Fatalf("実行中の取り消し: %d", got)
runningCanceled := postForm(handler, "/jobs/1/cancel", nil)
if runningCanceled.Code != http.StatusOK || jobs.jobs["ep"].Status != "canceled" || !strings.Contains(runningCanceled.Body.String(), `action="/jobs/1/retry"`) {
t.Fatalf("実行中の取り消し: %d %+v", runningCanceled.Code, jobs.jobs["ep"])
}
}
+1 -1
View File
@@ -124,7 +124,7 @@ func (f *fakeJobStore) Cancel(_ context.Context, id int64) error {
return f.err
}
for ep, job := range f.jobs {
if job.ID == id && job.Status == "queued" {
if job.ID == id && (job.Status == "queued" || job.Status == "running") {
job.Status = "canceled"
f.jobs[ep] = job
return nil
+3
View File
@@ -38,6 +38,9 @@
{{if .SpeedText}}/{{.SpeedText}}{{end}}
{{if .ETAText}}/{{.ETAText}}{{end}}
{{if .FragmentText}}/{{.FragmentText}}{{end}}</p>
<form action="/jobs/{{.ID}}/cancel" method="post" hx-post="/jobs/{{.ID}}/cancel" hx-target="#queue-list" hx-swap="outerHTML">
<button type="submit">取り消し</button>
</form>
{{end}}
{{if eq .Status "failed"}}
<p>失敗理由: {{.ErrorKindText}}</p>