番組 URL から番組を登録する機能を追加(ステップ11)
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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})
|
||||
}
|
||||
Reference in New Issue
Block a user