mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
fix media organize and subscription workflows
This commit is contained in:
@@ -8,11 +8,14 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
@@ -38,6 +41,9 @@ var discoverSectionCatalog = []discoverSectionDef{
|
||||
{Key: "bangumi_calendar", Label: "Bangumi 每日放送", Provider: "bangumi"},
|
||||
}
|
||||
|
||||
const discoverFeedSectionTimeout = 15 * time.Second
|
||||
const discoverFeedSlowSectionThreshold = 2 * time.Second
|
||||
|
||||
// discoverSectionsHandler returns the catalog of sections the UI can
|
||||
// pick from. The names match the upstream Vue UI so existing settings
|
||||
// keep working.
|
||||
@@ -76,24 +82,155 @@ func discoverFeedHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
if provider := discoverSectionProvider(k); provider != "" && !discoverProviderEnabled(c.Request.Context(), svc, provider) {
|
||||
out[k] = []service.ExternalMediaResult{}
|
||||
meta[k] = gin.H{"page": page, "has_next": false}
|
||||
meta[k] = gin.H{"page": page, "has_next": false, "disabled": true}
|
||||
continue
|
||||
}
|
||||
items, err := discoverSectionItems(c.Request.Context(), svc, k, page)
|
||||
sectionCtx, cancel := context.WithTimeout(c.Request.Context(), discoverFeedSectionTimeout)
|
||||
started := time.Now()
|
||||
items, err := discoverSectionItems(sectionCtx, svc, k, page)
|
||||
elapsed := time.Since(started)
|
||||
cancel()
|
||||
metaEntry := gin.H{"page": page, "has_next": false, "duration_ms": elapsed.Milliseconds()}
|
||||
if err != nil {
|
||||
svc.Log.Debug("discover fetch failed")
|
||||
items = nil
|
||||
logDiscoverFetchFailed(svc, k, page, elapsed, err)
|
||||
if cached, ok := cachedDiscoverSection(svc, k, page); ok {
|
||||
items = cached
|
||||
metaEntry["stale"] = true
|
||||
metaEntry["warning"] = discoverFeedStaleMessage(err)
|
||||
} else if fallbackItems, fallbackKey, ok := fallbackDiscoverSectionItems(c.Request.Context(), svc, k, page); ok {
|
||||
items = fallbackItems
|
||||
metaEntry["fallback"] = fallbackKey
|
||||
metaEntry["warning"] = discoverFeedFallbackMessage(fallbackKey, err)
|
||||
rememberDiscoverSection(svc, k, page, items)
|
||||
} else {
|
||||
metaEntry["error"] = discoverFeedErrorMessage(err)
|
||||
items = nil
|
||||
}
|
||||
} else {
|
||||
logDiscoverFetchSlow(svc, k, page, elapsed, len(items))
|
||||
rememberDiscoverSection(svc, k, page, items)
|
||||
}
|
||||
artworkItems = append(artworkItems, items...)
|
||||
out[k] = items
|
||||
meta[k] = gin.H{"page": page, "has_next": discoverSectionHasNext(k, len(items))}
|
||||
metaEntry["has_next"] = discoverSectionHasNext(k, len(items))
|
||||
meta[k] = metaEntry
|
||||
}
|
||||
out["_meta"] = meta
|
||||
svc.Discover.WarmExternalArtwork(artworkItems)
|
||||
if svc != nil && svc.Discover != nil {
|
||||
svc.Discover.WarmExternalArtwork(artworkItems)
|
||||
}
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
func cachedDiscoverSection(svc *service.Container, key string, page int) ([]service.ExternalMediaResult, bool) {
|
||||
if svc == nil || svc.Discover == nil {
|
||||
return nil, false
|
||||
}
|
||||
return svc.Discover.CachedSection(key, page)
|
||||
}
|
||||
|
||||
func rememberDiscoverSection(svc *service.Container, key string, page int, items []service.ExternalMediaResult) {
|
||||
if svc == nil || svc.Discover == nil {
|
||||
return
|
||||
}
|
||||
svc.Discover.RememberSection(key, page, items)
|
||||
}
|
||||
|
||||
func fallbackDiscoverSectionItems(parent context.Context, svc *service.Container, key string, page int) ([]service.ExternalMediaResult, string, bool) {
|
||||
fallbackKey := fallbackDiscoverSectionKey(key)
|
||||
if fallbackKey == "" || svc == nil || svc.Discover == nil {
|
||||
return nil, "", false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(parent, discoverFeedSectionTimeout)
|
||||
defer cancel()
|
||||
items, err := discoverSectionItems(ctx, svc, fallbackKey, page)
|
||||
if err != nil || len(items) == 0 {
|
||||
return nil, fallbackKey, false
|
||||
}
|
||||
if svc.Log != nil {
|
||||
svc.Log.Info("discover section fallback used",
|
||||
zap.String("section", key),
|
||||
zap.String("fallback_section", fallbackKey),
|
||||
zap.Int("page", page),
|
||||
zap.Int("items", len(items)))
|
||||
}
|
||||
return items, fallbackKey, true
|
||||
}
|
||||
|
||||
func fallbackDiscoverSectionKey(key string) string {
|
||||
switch key {
|
||||
case "douban_hot_movie":
|
||||
return "tmdb_popular_movie"
|
||||
case "douban_hot_tv":
|
||||
return "tmdb_popular_tv"
|
||||
case "douban_top_movie":
|
||||
return "tmdb_top_rated_movie"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func logDiscoverFetchFailed(svc *service.Container, key string, page int, elapsed time.Duration, err error) {
|
||||
if svc == nil || svc.Log == nil || err == nil {
|
||||
return
|
||||
}
|
||||
svc.Log.Warn("discover section fetch failed",
|
||||
zap.String("section", key),
|
||||
zap.String("provider", discoverSectionProvider(key)),
|
||||
zap.Int("page", page),
|
||||
zap.Duration("duration", elapsed),
|
||||
zap.Int64("duration_ms", elapsed.Milliseconds()),
|
||||
zap.Duration("timeout", discoverFeedSectionTimeout),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
func logDiscoverFetchSlow(svc *service.Container, key string, page int, elapsed time.Duration, itemCount int) {
|
||||
if svc == nil || svc.Log == nil || elapsed < discoverFeedSlowSectionThreshold {
|
||||
return
|
||||
}
|
||||
svc.Log.Info("discover section fetch slow",
|
||||
zap.String("section", key),
|
||||
zap.String("provider", discoverSectionProvider(key)),
|
||||
zap.Int("page", page),
|
||||
zap.Int("items", itemCount),
|
||||
zap.Duration("duration", elapsed),
|
||||
zap.Int64("duration_ms", elapsed.Milliseconds()),
|
||||
zap.Duration("slow_threshold", discoverFeedSlowSectionThreshold))
|
||||
}
|
||||
|
||||
func discoverFeedErrorMessage(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return "推荐源响应超时,已跳过本次加载"
|
||||
}
|
||||
var timeout interface{ Timeout() bool }
|
||||
if errors.As(err, &timeout) && timeout.Timeout() {
|
||||
return "推荐源响应超时,已跳过本次加载"
|
||||
}
|
||||
msg := strings.ToLower(err.Error())
|
||||
if strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline exceeded") || strings.Contains(msg, "context deadline exceeded") {
|
||||
return "推荐源响应超时,已跳过本次加载"
|
||||
}
|
||||
return "推荐源暂时不可用,已跳过本次加载"
|
||||
}
|
||||
|
||||
func discoverFeedStaleMessage(err error) string {
|
||||
if discoverFeedErrorMessage(err) == "推荐源响应超时,已跳过本次加载" {
|
||||
return "推荐源响应超时,已显示上次成功结果"
|
||||
}
|
||||
return "推荐源暂时不可用,已显示上次成功结果"
|
||||
}
|
||||
|
||||
func discoverFeedFallbackMessage(fallbackKey string, err error) string {
|
||||
if strings.TrimSpace(fallbackKey) == "" {
|
||||
return discoverFeedErrorMessage(err)
|
||||
}
|
||||
return "推荐源暂时不可用,已显示同类备用榜单"
|
||||
}
|
||||
|
||||
func enabledDiscoverSections(ctx context.Context, svc *service.Container) []discoverSectionDef {
|
||||
sections := make([]discoverSectionDef, 0, len(discoverSectionCatalog))
|
||||
for _, section := range discoverSectionCatalog {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest/observer"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
@@ -36,6 +40,81 @@ func TestDiscoverProviderEnabledHonorsAPIConfigToggle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverFetchFailureLogIncludesDiagnostics(t *testing.T) {
|
||||
core, observed := observer.New(zap.WarnLevel)
|
||||
logger := zap.New(core)
|
||||
|
||||
logDiscoverFetchFailed(
|
||||
&service.Container{Log: logger},
|
||||
"tmdb_latest_movie",
|
||||
2,
|
||||
1500*time.Millisecond,
|
||||
context.DeadlineExceeded,
|
||||
)
|
||||
|
||||
entries := observed.FilterMessage("discover section fetch failed").All()
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("expected one failure log entry, got %d", len(entries))
|
||||
}
|
||||
fields := entries[0].ContextMap()
|
||||
if fields["section"] != "tmdb_latest_movie" || fields["provider"] != "tmdb" {
|
||||
t.Fatalf("unexpected section/provider fields: %#v", fields)
|
||||
}
|
||||
if fields["page"] != int64(2) && fields["page"] != 2 {
|
||||
t.Fatalf("page field missing or wrong: %#v", fields["page"])
|
||||
}
|
||||
if fields["duration_ms"] != int64(1500) && fields["duration_ms"] != 1500 {
|
||||
t.Fatalf("duration_ms field missing or wrong: %#v", fields["duration_ms"])
|
||||
}
|
||||
if _, ok := fields["timeout"]; !ok {
|
||||
t.Fatalf("timeout field missing: %#v", fields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverSlowFetchLogIncludesSectionTiming(t *testing.T) {
|
||||
core, observed := observer.New(zap.InfoLevel)
|
||||
logger := zap.New(core)
|
||||
|
||||
logDiscoverFetchSlow(&service.Container{Log: logger}, "douban_hot_movie", 1, discoverFeedSlowSectionThreshold-time.Millisecond, 24)
|
||||
if got := observed.FilterMessage("discover section fetch slow").Len(); got != 0 {
|
||||
t.Fatalf("fast section should not log, got %d entries", got)
|
||||
}
|
||||
|
||||
logDiscoverFetchSlow(&service.Container{Log: logger}, "douban_hot_movie", 1, discoverFeedSlowSectionThreshold, 24)
|
||||
entries := observed.FilterMessage("discover section fetch slow").All()
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("expected one slow log entry, got %d", len(entries))
|
||||
}
|
||||
fields := entries[0].ContextMap()
|
||||
if fields["section"] != "douban_hot_movie" || fields["provider"] != "douban" {
|
||||
t.Fatalf("unexpected section/provider fields: %#v", fields)
|
||||
}
|
||||
if fields["items"] != int64(24) && fields["items"] != 24 {
|
||||
t.Fatalf("items field missing or wrong: %#v", fields["items"])
|
||||
}
|
||||
if _, ok := fields["duration_ms"]; !ok {
|
||||
t.Fatalf("duration_ms field missing: %#v", fields)
|
||||
}
|
||||
if _, ok := fields["slow_threshold"]; !ok {
|
||||
t.Fatalf("slow_threshold field missing: %#v", fields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverFeedErrorMessageHidesTechnicalTimeout(t *testing.T) {
|
||||
for _, err := range []error{
|
||||
context.DeadlineExceeded,
|
||||
errors.New("timeout of 30000ms exceeded"),
|
||||
} {
|
||||
got := discoverFeedErrorMessage(err)
|
||||
if got != "推荐源响应超时,已跳过本次加载" {
|
||||
t.Fatalf("message for %q = %q", err, got)
|
||||
}
|
||||
}
|
||||
if got := discoverFeedErrorMessage(errors.New("upstream 503")); got != "推荐源暂时不可用,已跳过本次加载" {
|
||||
t.Fatalf("generic message = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDiscoverSectionKeysSkipDisabledProviders(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
@@ -78,3 +157,17 @@ func TestDefaultDiscoverSectionKeysIncludeLatestTMDbRails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackDiscoverSectionKeyUsesTMDbForDoubanRails(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"douban_hot_movie": "tmdb_popular_movie",
|
||||
"douban_hot_tv": "tmdb_popular_tv",
|
||||
"douban_top_movie": "tmdb_top_rated_movie",
|
||||
"tmdb_latest_tv": "",
|
||||
}
|
||||
for key, want := range cases {
|
||||
if got := fallbackDiscoverSectionKey(key); got != want {
|
||||
t.Fatalf("fallbackDiscoverSectionKey(%q) = %q, want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ func registerAuthedSTRMRoutes(authed *gin.RouterGroup, svc *service.Container) {
|
||||
authed.DELETE("/media/:id/strm", middleware.AdminRequired(), clearSTRMHandler(svc))
|
||||
authed.POST("/strm/import", middleware.AdminRequired(), importSTRMHandler(svc))
|
||||
authed.POST("/strm/generate", middleware.AdminRequired(), generateSTRMHandler(svc))
|
||||
authed.POST("/strm/generate-from-tree", middleware.AdminRequired(), generateSTRMFromTreeHandler(svc))
|
||||
}
|
||||
|
||||
func registerAuthedDuplicateRoutes(authed *gin.RouterGroup, svc *service.Container) {
|
||||
|
||||
@@ -104,7 +104,20 @@ type generateSTRMReq struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
IncludeLocal bool `json:"include_local"`
|
||||
IncludeLocal *bool `json:"include_local"`
|
||||
PreserveTree bool `json:"preserve_tree"`
|
||||
}
|
||||
|
||||
type generateSTRMTreeReq struct {
|
||||
Provider string `json:"provider"`
|
||||
TreeText string `json:"tree_text"`
|
||||
Paths []string `json:"paths"`
|
||||
SourceRoot string `json:"source_root"`
|
||||
OutputPrefix string `json:"output_prefix"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
Cleanup bool `json:"cleanup"`
|
||||
}
|
||||
|
||||
func generateSTRMHandler(svc *service.Container) gin.HandlerFunc {
|
||||
@@ -122,13 +135,18 @@ func generateSTRMHandler(svc *service.Container) gin.HandlerFunc {
|
||||
if baseURL == "" {
|
||||
baseURL = strings.TrimRight(absoluteRequestURL(c, "/"), "/")
|
||||
}
|
||||
includeLocal := true
|
||||
if req.IncludeLocal != nil {
|
||||
includeLocal = *req.IncludeLocal
|
||||
}
|
||||
options := service.GenerateSTRMOptions{
|
||||
LibraryID: req.LibraryID,
|
||||
OutputDir: req.OutputDir,
|
||||
BaseURL: baseURL,
|
||||
Enabled: req.Enabled,
|
||||
Overwrite: req.Overwrite,
|
||||
IncludeLocal: true,
|
||||
IncludeLocal: includeLocal,
|
||||
PreserveTree: req.PreserveTree,
|
||||
PlaybackToken: strmPlaybackTokenForRequest(c, svc),
|
||||
}
|
||||
var res *service.GenerateSTRMResult
|
||||
@@ -146,6 +164,40 @@ func generateSTRMHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func generateSTRMFromTreeHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req generateSTRMTreeReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
strmSvc := svc.STRM
|
||||
if strmSvc == nil {
|
||||
strmSvc = service.NewSTRMService(svc.Log, svc.Repo, svc.Cfg)
|
||||
}
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(req.BaseURL), "/")
|
||||
if baseURL == "" {
|
||||
baseURL = strings.TrimRight(absoluteRequestURL(c, "/"), "/")
|
||||
}
|
||||
res, err := strmSvc.GenerateFromTree(c.Request.Context(), service.GenerateSTRMTreeOptions{
|
||||
Provider: req.Provider,
|
||||
TreeText: req.TreeText,
|
||||
Paths: req.Paths,
|
||||
SourceRoot: req.SourceRoot,
|
||||
OutputPrefix: req.OutputPrefix,
|
||||
OutputDir: req.OutputDir,
|
||||
BaseURL: baseURL,
|
||||
Overwrite: req.Overwrite,
|
||||
Cleanup: req.Cleanup,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, res)
|
||||
}
|
||||
}
|
||||
|
||||
func strmPlaybackTokenForRequest(c *gin.Context, svc *service.Container) string {
|
||||
if svc == nil || svc.Auth == nil || svc.Repo == nil || svc.Repo.User == nil {
|
||||
return ""
|
||||
|
||||
@@ -33,6 +33,11 @@ type subscriptionPatchReq struct {
|
||||
Effects *string `json:"effects"`
|
||||
ReleaseGroups *string `json:"release_groups"`
|
||||
ExcludeWords *string `json:"exclude_words"`
|
||||
MinSeeders *int `json:"min_seeders"`
|
||||
MaxSeeders *int `json:"max_seeders"`
|
||||
MinSizeGB *float64 `json:"min_size_gb"`
|
||||
MaxSizeGB *float64 `json:"max_size_gb"`
|
||||
FreeOnly *bool `json:"free_only"`
|
||||
WashEnabled *bool `json:"wash_enabled"`
|
||||
WashPriority *string `json:"wash_priority"`
|
||||
TotalEpisodes *int `json:"total_episodes"`
|
||||
@@ -137,6 +142,21 @@ func subscriptionPatchUpdates(patch subscriptionPatchReq) map[string]any {
|
||||
if patch.ExcludeWords != nil {
|
||||
updates["exclude_words"] = *patch.ExcludeWords
|
||||
}
|
||||
if patch.MinSeeders != nil {
|
||||
updates["min_seeders"] = *patch.MinSeeders
|
||||
}
|
||||
if patch.MaxSeeders != nil {
|
||||
updates["max_seeders"] = *patch.MaxSeeders
|
||||
}
|
||||
if patch.MinSizeGB != nil {
|
||||
updates["min_size_gb"] = *patch.MinSizeGB
|
||||
}
|
||||
if patch.MaxSizeGB != nil {
|
||||
updates["max_size_gb"] = *patch.MaxSizeGB
|
||||
}
|
||||
if patch.FreeOnly != nil {
|
||||
updates["free_only"] = *patch.FreeOnly
|
||||
}
|
||||
if patch.WashEnabled != nil {
|
||||
updates["wash_enabled"] = *patch.WashEnabled
|
||||
}
|
||||
|
||||
@@ -14,30 +14,35 @@ import (
|
||||
)
|
||||
|
||||
type subscriptionReq struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
FeedURL string `json:"feed_url" binding:"required"`
|
||||
Filter string `json:"filter"`
|
||||
MediaType string `json:"media_type"`
|
||||
MediaCategory string `json:"media_category"`
|
||||
SavePath string `json:"save_path"`
|
||||
SearchMode string `json:"search_mode"`
|
||||
IMDBID string `json:"imdb_id"`
|
||||
Source string `json:"source"`
|
||||
PosterURL string `json:"poster_url"`
|
||||
BackdropURL string `json:"backdrop_url"`
|
||||
Overview string `json:"overview"`
|
||||
OriginalName string `json:"original_name"`
|
||||
Year int `json:"year"`
|
||||
Resolution string `json:"resolution"`
|
||||
Quality string `json:"quality"`
|
||||
Effects string `json:"effects"`
|
||||
ReleaseGroups string `json:"release_groups"`
|
||||
ExcludeWords string `json:"exclude_words"`
|
||||
WashEnabled bool `json:"wash_enabled"`
|
||||
WashPriority string `json:"wash_priority"`
|
||||
TotalEpisodes int `json:"total_episodes"`
|
||||
Priority int `json:"priority"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
FeedURL string `json:"feed_url" binding:"required"`
|
||||
Filter string `json:"filter"`
|
||||
MediaType string `json:"media_type"`
|
||||
MediaCategory string `json:"media_category"`
|
||||
SavePath string `json:"save_path"`
|
||||
SearchMode string `json:"search_mode"`
|
||||
IMDBID string `json:"imdb_id"`
|
||||
Source string `json:"source"`
|
||||
PosterURL string `json:"poster_url"`
|
||||
BackdropURL string `json:"backdrop_url"`
|
||||
Overview string `json:"overview"`
|
||||
OriginalName string `json:"original_name"`
|
||||
Year int `json:"year"`
|
||||
Resolution string `json:"resolution"`
|
||||
Quality string `json:"quality"`
|
||||
Effects string `json:"effects"`
|
||||
ReleaseGroups string `json:"release_groups"`
|
||||
ExcludeWords string `json:"exclude_words"`
|
||||
MinSeeders int `json:"min_seeders"`
|
||||
MaxSeeders int `json:"max_seeders"`
|
||||
MinSizeGB float64 `json:"min_size_gb"`
|
||||
MaxSizeGB float64 `json:"max_size_gb"`
|
||||
FreeOnly bool `json:"free_only"`
|
||||
WashEnabled bool `json:"wash_enabled"`
|
||||
WashPriority string `json:"wash_priority"`
|
||||
TotalEpisodes int `json:"total_episodes"`
|
||||
Priority int `json:"priority"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func createSubscriptionHandler(svc *service.Container) gin.HandlerFunc {
|
||||
@@ -73,6 +78,11 @@ func createSubscriptionHandler(svc *service.Container) gin.HandlerFunc {
|
||||
Effects: req.Effects,
|
||||
ReleaseGroups: req.ReleaseGroups,
|
||||
ExcludeWords: req.ExcludeWords,
|
||||
MinSeeders: req.MinSeeders,
|
||||
MaxSeeders: req.MaxSeeders,
|
||||
MinSizeGB: req.MinSizeGB,
|
||||
MaxSizeGB: req.MaxSizeGB,
|
||||
FreeOnly: req.FreeOnly,
|
||||
WashEnabled: req.WashEnabled,
|
||||
WashPriority: req.WashPriority,
|
||||
TotalEpisodes: req.TotalEpisodes,
|
||||
|
||||
Reference in New Issue
Block a user