Files
tver/internal/web/subscriptions.go

193 lines
6.8 KiB
Go

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