package web import ( "errors" "net/http" "net/url" "regexp" "strings" "gitea.kazu634.com/kazu634/tver/internal/store" "gitea.kazu634.com/kazu634/tver/internal/tver" ) const seriesURLInputError = "番組ページ(https://tver.jp/series/…)の URL を入力してください" var seriesURLPathPattern = regexp.MustCompile(`^/series/([A-Za-z0-9]+)/?$`) var seriesIDPattern = regexp.MustCompile(`^[A-Za-z0-9]+$`) type subscriptionURLResultView struct { SeriesID, Title, ErrorText string EpisodeCount int Subscribed bool } type subscriptionURLRegisteredView struct { Subscriptions subscriptionsView } func parseSeriesURL(raw string) (string, error) { parsed, err := url.Parse(strings.TrimSpace(raw)) if err != nil || parsed.Scheme != "https" || parsed.User != nil || (!strings.EqualFold(parsed.Host, "tver.jp") && !strings.EqualFold(parsed.Host, "www.tver.jp")) { return "", errors.New(seriesURLInputError) } match := seriesURLPathPattern.FindStringSubmatch(parsed.Path) if len(match) != 2 { return "", errors.New(seriesURLInputError) } return match[1], nil } func (s *server) confirmSubscriptionURL(w http.ResponseWriter, r *http.Request) { if r.Header.Get("HX-Request") != "true" { http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) return } if err := r.ParseForm(); err != nil { http.Error(w, "入力が不正です", http.StatusBadRequest) return } seriesID, err := parseSeriesURL(r.Form.Get("series_url")) if err != nil { s.render(w, "subscription_url_result", subscriptionURLResultView{ErrorText: seriesURLInputError}) return } title, episodes, ok := s.fetchSubscriptionSeries(w, r, seriesID) if !ok { return } subscribed, err := s.isSubscribed(r.Context(), seriesID) if err != nil { s.logger.Printf("番組の登録状態取得に失敗: %v", err) http.Error(w, "登録状態を取得できませんでした", http.StatusInternalServerError) return } s.render(w, "subscription_url_result", subscriptionURLResultView{ SeriesID: seriesID, Title: title, EpisodeCount: len(episodes), Subscribed: subscribed, }) } func (s *server) registerSubscriptionURL(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "入力が不正です", http.StatusBadRequest) return } seriesID := r.Form.Get("series_id") if !seriesIDPattern.MatchString(seriesID) { http.Error(w, "番組 ID が不正です", http.StatusBadRequest) return } choice := r.Form.Get("download_existing") if choice != "" && choice != "1" { http.Error(w, "公開中の回の選択が不正です", http.StatusBadRequest) return } subscribed, err := s.isSubscribed(r.Context(), seriesID) if err != nil { s.logger.Printf("番組の登録状態取得に失敗: %v", err) http.Error(w, "登録状態を取得できませんでした", http.StatusInternalServerError) return } if subscribed { s.subscriptionURLRegisteredResponse(w, r) return } title, episodes, ok := s.fetchSubscriptionSeries(w, r, seriesID) if !ok { return } items := make([]store.Episode, 0, len(episodes)) for _, episode := range episodes { items = append(items, storeEpisode(episode)) } _, err = s.jobs.RegisterSubscription(r.Context(), seriesID, title, items, choice == "1") if err != nil { s.logger.Printf("番組URLからの登録に失敗: %v", err) s.subscriptionURLFailure(w, r, "番組を登録できませんでした。再試行してください") return } s.subscriptionURLRegisteredResponse(w, r) } func (s *server) cancelSubscriptionURL(w http.ResponseWriter, _ *http.Request) { s.render(w, "subscription_url_result", subscriptionURLResultView{}) } func (s *server) fetchSubscriptionSeries(w http.ResponseWriter, r *http.Request, seriesID string) (string, []tver.Episode, bool) { title, err := s.fetcher.FetchSeries(r.Context(), seriesID) if err != nil { if errors.Is(err, tver.ErrSeriesNotFound) { s.subscriptionURLFailure(w, r, "番組が見つかりません") return "", nil, false } s.logSubscriptionFetchError(err) s.subscriptionURLFailure(w, r, "番組情報を取得できませんでした。再試行してください") return "", nil, false } episodes, err := s.fetcher.FetchSeriesEpisodes(r.Context(), seriesID) if err != nil { s.logSubscriptionFetchError(err) s.subscriptionURLFailure(w, r, "番組情報を取得できませんでした。再試行してください") return "", nil, false } return title, episodes, true } func (s *server) logSubscriptionFetchError(err error) { s.logger.Printf("番組URLの情報取得に失敗: %s", platformTokenPattern.ReplaceAllString(err.Error(), "${1}[REDACTED]")) } func (s *server) subscriptionURLFailure(w http.ResponseWriter, r *http.Request, message string) { if r.Header.Get("HX-Request") != "true" { http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) return } s.render(w, "subscription_url_result", subscriptionURLResultView{ErrorText: message}) } func (s *server) subscriptionURLRegisteredResponse(w http.ResponseWriter, r *http.Request) { if r.Header.Get("HX-Request") != "true" { http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) return } view, err := s.loadSubscriptions(r.Context()) if err != nil { s.logger.Printf("登録番組の取得に失敗: %v", err) http.Error(w, "登録番組を取得できませんでした", http.StatusInternalServerError) return } view.OutOfBand = true s.render(w, "subscription_url_registered", subscriptionURLRegisteredView{Subscriptions: view}) }