package web import ( "context" "errors" "net/http" "gitea.kazu634.com/kazu634/tver/internal/store" ) type subscriptionActionView struct { ID, Genre, ErrorText string Subscribed, Choosing, DownloadExisting bool } type subscriptionView struct { ID, Title, CreatedAtText, LastCheckedText string CheckErrorAtText, CheckErrorText string Enabled bool Jobs int64 } type subscriptionsView struct { Items []subscriptionView } func (s *server) subscriptionChoice(w http.ResponseWriter, r *http.Request) { genre, id, ok := episodeForm(w, r) if !ok { return } ep, found := s.cachedEpisode(genre, id) if !found || ep.SeriesID == "" { http.Error(w, "新着キャッシュに番組がありません。新着を再表示してください", http.StatusNotFound) return } registered, err := s.isSubscribed(r.Context(), ep.SeriesID) if err != nil { s.logger.Printf("番組の登録状態取得に失敗: %v", err) http.Error(w, "登録状態を取得できませんでした", http.StatusInternalServerError) return } if registered { s.subscriptionRegisteredResponse(w, r, id, genre) return } s.render(w, "subscription_action", subscriptionActionView{ID: id, Genre: genre, Choosing: true}) } func (s *server) registerSubscription(w http.ResponseWriter, r *http.Request) { genre, id, ok := episodeForm(w, r) if !ok { return } choice := r.Form.Get("download_existing") if choice != "" && choice != "1" { http.Error(w, "公開中の回の選択が不正です", http.StatusBadRequest) return } ep, found := s.cachedEpisode(genre, id) if !found || ep.SeriesID == "" || ep.SeriesTitle == "" { http.Error(w, "新着キャッシュに番組がありません。新着を再表示してください", http.StatusNotFound) return } registered, err := s.isSubscribed(r.Context(), ep.SeriesID) if err != nil { s.logger.Printf("番組の登録状態取得に失敗: %v", err) http.Error(w, "登録状態を取得できませんでした", http.StatusInternalServerError) return } if registered { s.subscriptionRegisteredResponse(w, r, id, genre) return } episodes, err := s.fetcher.FetchSeriesEpisodes(r.Context(), ep.SeriesID) if err != nil { s.logger.Printf("番組の公開中エピソード取得に失敗: %s", platformTokenPattern.ReplaceAllString(err.Error(), "${1}[REDACTED]")) s.render(w, "subscription_action", subscriptionActionView{ID: id, Genre: genre, Choosing: true, DownloadExisting: choice == "1", ErrorText: "公開中の回を取得できませんでした。再試行してください"}) return } items := make([]store.Episode, 0, len(episodes)) for _, episode := range episodes { items = append(items, storeEpisode(episode)) } _, err = s.jobs.RegisterSubscription(r.Context(), ep.SeriesID, ep.SeriesTitle, items, choice == "1") if err != nil { s.logger.Printf("番組登録に失敗: %v", err) s.render(w, "subscription_action", subscriptionActionView{ID: id, Genre: genre, Choosing: true, DownloadExisting: choice == "1", ErrorText: "番組を登録できませんでした。再試行してください"}) return } s.subscriptionRegisteredResponse(w, r, id, genre) } func (s *server) subscriptionRegisteredResponse(w http.ResponseWriter, r *http.Request, id, genre string) { if r.Header.Get("HX-Request") != "true" && r.Method == http.MethodPost { http.Redirect(w, r, "/?genre="+genre, http.StatusSeeOther) return } s.render(w, "subscription_action", subscriptionActionView{ID: id, Genre: genre, Subscribed: true}) } func (s *server) isSubscribed(ctx context.Context, seriesID string) (bool, error) { result, err := s.jobs.SubscribedSeries(ctx, []string{seriesID}) return result[seriesID], err } func (s *server) subscriptionsPage(w http.ResponseWriter, r *http.Request) { view, err := s.loadSubscriptions(r.Context()) if err != nil { s.logger.Printf("登録番組の取得に失敗: %v", err) http.Error(w, "登録番組を取得できませんでした", http.StatusInternalServerError) return } s.render(w, "layout", page{Title: "登録番組", Subscriptions: &view}) } func (s *server) subscriptionsFragment(w http.ResponseWriter, r *http.Request) { view, err := s.loadSubscriptions(r.Context()) if err != nil { s.logger.Printf("登録番組の取得に失敗: %v", err) http.Error(w, "登録番組を取得できませんでした", http.StatusInternalServerError) return } s.render(w, "subscriptions_list", view) } func (s *server) loadSubscriptions(ctx context.Context) (subscriptionsView, error) { subs, err := s.jobs.ListSubscriptions(ctx) if err != nil { return subscriptionsView{}, err } view := subscriptionsView{Items: make([]subscriptionView, 0, len(subs))} for _, sub := range subs { item := subscriptionView{ID: sub.SeriesID, Title: sub.SeriesTitle, CreatedAtText: sub.CreatedAt.In(s.zone).Format("2006-01-02 15:04 MST"), LastCheckedText: "未確認", Enabled: sub.Enabled, Jobs: sub.JobCount} 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 } func (s *server) pauseSubscription(w http.ResponseWriter, r *http.Request) { s.subscriptionMutation(w, r, func(ctx context.Context, id string) error { return s.jobs.SetSubscriptionEnabled(ctx, id, false) }) } func (s *server) resumeSubscription(w http.ResponseWriter, r *http.Request) { s.subscriptionMutation(w, r, func(ctx context.Context, id string) error { return s.jobs.SetSubscriptionEnabled(ctx, id, true) }) } func (s *server) deleteSubscription(w http.ResponseWriter, r *http.Request) { s.subscriptionMutation(w, r, func(ctx context.Context, id string) error { deleted, err := s.jobs.DeleteSubscription(ctx, id) if err == nil && !deleted { return store.ErrInvalidTransition } return err }) } func (s *server) subscriptionMutation(w http.ResponseWriter, r *http.Request, change func(context.Context, string) error) { id := r.PathValue("id") if id == "" { http.Error(w, "番組 ID が不正です", http.StatusBadRequest) return } if err := change(r.Context(), id); err != nil { if errors.Is(err, store.ErrInvalidTransition) { http.Error(w, "番組が見つかりません", http.StatusNotFound) } else { s.logger.Printf("登録番組の操作に失敗: %v", err) http.Error(w, "登録番組を変更できませんでした", http.StatusInternalServerError) } return } if r.Header.Get("HX-Request") != "true" { http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) return } s.subscriptionsFragment(w, r) }