Files
tver/internal/web/subscription_url.go

162 lines
5.4 KiB
Go

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})
}