mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-01 20:16:36 +08:00
fix media workflows and download organization
This commit is contained in:
@@ -9,6 +9,7 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -60,8 +61,13 @@ func discoverFeedHandler(svc *service.Container) gin.HandlerFunc {
|
||||
if strings.TrimSpace(rawSections) == "" {
|
||||
rawSections = strings.Join(defaultDiscoverSectionKeys(c.Request.Context(), svc), ",")
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
keys := strings.Split(rawSections, ",")
|
||||
out := gin.H{}
|
||||
meta := gin.H{}
|
||||
artworkItems := []service.ExternalMediaResult{}
|
||||
for _, raw := range keys {
|
||||
k := strings.TrimSpace(raw)
|
||||
@@ -70,16 +76,19 @@ 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}
|
||||
continue
|
||||
}
|
||||
items, err := discoverSectionItems(c.Request.Context(), svc, k)
|
||||
items, err := discoverSectionItems(c.Request.Context(), svc, k, page)
|
||||
if err != nil {
|
||||
svc.Log.Debug("discover fetch failed")
|
||||
items = nil
|
||||
}
|
||||
artworkItems = append(artworkItems, items...)
|
||||
out[k] = items
|
||||
meta[k] = gin.H{"page": page, "has_next": discoverSectionHasNext(k, len(items))}
|
||||
}
|
||||
out["_meta"] = meta
|
||||
svc.Discover.WarmExternalArtwork(artworkItems)
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
@@ -145,22 +154,39 @@ func discoverProviderEnabled(ctx context.Context, svc *service.Container, provid
|
||||
return cfg.Enabled
|
||||
}
|
||||
|
||||
func discoverSectionItems(ctx context.Context, svc *service.Container, k string) ([]service.ExternalMediaResult, error) {
|
||||
func discoverSectionItems(ctx context.Context, svc *service.Container, k string, page int) ([]service.ExternalMediaResult, error) {
|
||||
switch k {
|
||||
case "tmdb_trending_day", "tmdb_trending_week", "tmdb_latest_movie", "tmdb_latest_tv", "tmdb_popular_movie", "tmdb_popular_tv", "tmdb_top_rated_movie", "tmdb_upcoming_movie",
|
||||
"trending_day", "trending_week", "latest_movie", "latest_tv", "popular_movie", "popular_tv", "top_rated_movie", "upcoming_movie":
|
||||
return svc.Discover.TMDbSection(ctx, k)
|
||||
return svc.Discover.TMDbSection(ctx, k, page)
|
||||
case "douban_hot_movie", "douban_hot_tv", "douban_top_movie":
|
||||
if svc.Douban == nil {
|
||||
return []service.ExternalMediaResult{}, nil
|
||||
}
|
||||
return svc.Douban.Discover(ctx, k)
|
||||
return svc.Douban.Discover(ctx, k, page)
|
||||
case "bangumi_calendar":
|
||||
if svc.Bangumi == nil {
|
||||
return []service.ExternalMediaResult{}, nil
|
||||
}
|
||||
if page > 1 {
|
||||
return []service.ExternalMediaResult{}, nil
|
||||
}
|
||||
return svc.Bangumi.Calendar(ctx)
|
||||
default:
|
||||
return []service.ExternalMediaResult{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func discoverSectionHasNext(key string, itemCount int) bool {
|
||||
if itemCount <= 0 {
|
||||
return false
|
||||
}
|
||||
switch discoverSectionProvider(key) {
|
||||
case "tmdb":
|
||||
return itemCount >= 20
|
||||
case "douban":
|
||||
return itemCount >= 24
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func embyPlayingProgressHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
clientInfo := embyClientInfoFromRequest(c)
|
||||
if svc.Device != nil && svc.Device.IsDeviceKicked(c.Request.Context(), uid, clientInfo.DeviceID) {
|
||||
if svc.Device != nil && svc.Device.IsTerminalKicked(c.Request.Context(), uid, clientInfo.DeviceID, clientInfo.DeviceName, clientInfo.Client) {
|
||||
c.Status(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -61,11 +61,16 @@ func manualScrapeApplyOneHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
applyCtx, cancel := manualScrapeApplyContext(c)
|
||||
defer cancel()
|
||||
media, err := svc.Scraper.ApplyManualMatch(applyCtx, c.Param("id"), req)
|
||||
mediaID := c.Param("id")
|
||||
media, err := svc.Scraper.ApplyManualMatch(applyCtx, mediaID, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
reclassifyMediaAfterScrape(applyCtx, svc, mediaID)
|
||||
if refreshed, _ := svc.Repo.Media.FindByID(applyCtx, mediaID); refreshed != nil {
|
||||
media = refreshed
|
||||
}
|
||||
c.JSON(http.StatusOK, media)
|
||||
}
|
||||
}
|
||||
@@ -92,6 +97,7 @@ func manualScrapeApplyBatchHandler(svc *service.Container) gin.HandlerFunc {
|
||||
errorsOut = append(errorsOut, id+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
reclassifyMediaAfterScrape(applyCtx, svc, id)
|
||||
applied++
|
||||
}
|
||||
if applied == 0 && len(errorsOut) > 0 {
|
||||
|
||||
@@ -341,7 +341,7 @@ func streamHandler(svc *service.Container) gin.HandlerFunc {
|
||||
if !enforceScopedPlaybackToken(c, m.ID) {
|
||||
return
|
||||
}
|
||||
err = svc.Stream.ServeFileWithCloudMode(c.Writer, c.Request, c.Param("id"), service.CloudPlaybackModeSTRM)
|
||||
err = svc.Stream.ServeFile(c.Writer, c.Request, c.Param("id"))
|
||||
if errors.Is(err, service.ErrMediaNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
@@ -350,6 +350,10 @@ func streamHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrCloudPlaybackUnavailable) {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
@@ -94,6 +94,7 @@ func aiScrapeMediaHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
reclassifyMediaAfterScrape(c.Request.Context(), svc, m.ID)
|
||||
refreshed, _ := svc.Repo.Media.FindByID(c.Request.Context(), m.ID)
|
||||
c.JSON(http.StatusOK, refreshed)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,23 @@ func TestExternalURLUsesMediaScopedPlaybackToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamCloudMediaWithoutSTRMURLReturnsBadGateway(t *testing.T) {
|
||||
router, _, secret := newPlaybackScopeTestRouter(t)
|
||||
loginToken := signedTestToken(t, secret)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://nas.local/api/stream/media-missing-strm", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+loginToken)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadGateway {
|
||||
t.Fatalf("status = %d body=%s, want 502", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), service.ErrCloudPlaybackUnavailable.Error()) {
|
||||
t.Fatalf("body = %q, want cloud playback unavailable error", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalURLPrefersBrowserPublicOriginOverForwardedSource(t *testing.T) {
|
||||
router, _, secret := newPlaybackScopeTestRouter(t)
|
||||
loginToken := signedTestToken(t, secret)
|
||||
@@ -215,6 +232,29 @@ func TestExternalURLPrefersConfiguredPublicServerURLOverLocalBrowserOrigin(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIStreamAllowsRedirectPlaybackWhenSTRMModeDisabled(t *testing.T) {
|
||||
router, svc, secret := newPlaybackScopeTestRouter(t)
|
||||
if err := svc.Repo.Setting.Set(t.Context(), service.CloudPlaybackSTRMEnabledSettingKey, "false"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := svc.Repo.Setting.Set(t.Context(), service.CloudPlaybackRedirectEnabledSettingKey, "true"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loginToken := signedTestToken(t, secret)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://nas.local/api/stream/media-1?api_key="+url.QueryEscape(loginToken), nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusFound {
|
||||
t.Fatalf("status = %d body=%s, want 302", w.Code, w.Body.String())
|
||||
}
|
||||
loc := w.Header().Get("Location")
|
||||
if !strings.Contains(loc, "/api/cloud/play/openlist?") || !strings.Contains(loc, "token="+url.QueryEscape(loginToken)) {
|
||||
t.Fatalf("redirect Location should target tokenized cloud play endpoint, got %q", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopedPlaybackTokenCannotStreamAnotherMedia(t *testing.T) {
|
||||
router, svc, _ := newPlaybackScopeTestRouter(t)
|
||||
user, err := svc.Repo.User.FindByID(t.Context(), "user-1")
|
||||
@@ -235,6 +275,43 @@ func TestScopedPlaybackTokenCannotStreamAnotherMedia(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopedPlaybackTokenCanFollowCloudRedirectForSameMedia(t *testing.T) {
|
||||
router, svc, _ := newPlaybackScopeTestRouter(t)
|
||||
user, err := svc.Repo.User.FindByID(t.Context(), "user-1")
|
||||
if err != nil || user == nil {
|
||||
t.Fatalf("find user: %v", err)
|
||||
}
|
||||
playToken, err := svc.Auth.IssueExternalPlaybackToken(user, "media-1", 2*60*60)
|
||||
if err != nil {
|
||||
t.Fatalf("issue playback token: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://nas.local/api/stream/media-1?token="+url.QueryEscape(playToken), nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusFound {
|
||||
t.Fatalf("stream status = %d body=%s, want 302", w.Code, w.Body.String())
|
||||
}
|
||||
loc := w.Header().Get("Location")
|
||||
if !strings.Contains(loc, "/api/cloud/play/openlist?") ||
|
||||
!strings.Contains(loc, "media_id=media-1") ||
|
||||
!strings.Contains(loc, "token="+url.QueryEscape(playToken)) {
|
||||
t.Fatalf("redirect Location should carry scoped token and media_id, got %q", loc)
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, loc, nil)
|
||||
w = httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code == http.StatusForbidden || w.Code == http.StatusUnauthorized {
|
||||
t.Fatalf("cloud redirect rejected scoped token: status=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if w.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("cloud redirect status = %d body=%s, want storage service fallback 503", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopedPlaybackTokenCannotRetargetCloudRef(t *testing.T) {
|
||||
router, svc, _ := newPlaybackScopeTestRouter(t)
|
||||
user, err := svc.Repo.User.FindByID(t.Context(), "user-1")
|
||||
@@ -370,6 +447,12 @@ func newPlaybackScopeTestRouter(t *testing.T) (*gin.Engine, *service.Container,
|
||||
Path: "cloud://openlist/Movies/Other.mkv",
|
||||
STRMURL: "/api/cloud/play/openlist?ref=/Movies/Other.mkv",
|
||||
},
|
||||
{
|
||||
Base: model.Base{ID: "media-missing-strm"},
|
||||
LibraryID: lib.ID,
|
||||
Title: "Cloud missing STRM",
|
||||
Path: "cloud://openlist/Movies/Missing.mkv",
|
||||
},
|
||||
}
|
||||
if err := repos.DB.Create(&rows).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
func reclassifyMediaAfterScrape(ctx context.Context, svc *service.Container, mediaIDs ...string) int {
|
||||
if svc == nil || svc.Organizer == nil {
|
||||
return 0
|
||||
}
|
||||
res, err := svc.Organizer.ReclassifyMisclassifiedMedia(ctx, service.MediaCategoryReclassifyOptions{MediaIDs: mediaIDs})
|
||||
if err != nil {
|
||||
if svc.Log != nil {
|
||||
svc.Log.Warn("scrape reclassify media failed", zap.Strings("media_ids", mediaIDs), zap.Error(err))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return res.Reclassified
|
||||
}
|
||||
|
||||
func reclassifyLibraryAfterScrape(ctx context.Context, svc *service.Container, libraryIDs ...string) int {
|
||||
if svc == nil || svc.Organizer == nil {
|
||||
return 0
|
||||
}
|
||||
res, err := svc.Organizer.ReclassifyMisclassifiedMedia(ctx, service.MediaCategoryReclassifyOptions{LibraryIDs: libraryIDs})
|
||||
if err != nil {
|
||||
if svc.Log != nil {
|
||||
svc.Log.Warn("scrape reclassify library failed", zap.Strings("library_ids", libraryIDs), zap.Error(err))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return res.Reclassified
|
||||
}
|
||||
@@ -85,6 +85,17 @@ func testStorageConfigHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if service.IsAdminCloudConfigurable(in.Type) {
|
||||
enabled := true
|
||||
if _, err := svc.StorageCfg.Save(c.Request.Context(), service.StorageInput{
|
||||
Type: in.Type,
|
||||
Config: in.Config,
|
||||
Enabled: &enabled,
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
func TestStorageConfigHandlersRejectQuark(t *testing.T) {
|
||||
@@ -26,3 +34,68 @@ func TestStorageConfigHandlersRejectQuark(t *testing.T) {
|
||||
t.Fatalf("body = %s, want unsupported storage type", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageConfigCloudTestSuccessSavesAndEnablesProvider(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
openlist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/fs/list" {
|
||||
t.Fatalf("unexpected openlist path %s", r.URL.Path)
|
||||
}
|
||||
if r.Header.Get("Authorization") != "openlist-token" {
|
||||
t.Fatalf("authorization = %q, want openlist-token", r.Header.Get("Authorization"))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"code": 200,
|
||||
"data": map[string]any{"content": []any{}, "total": 0},
|
||||
})
|
||||
}))
|
||||
defer openlist.Close()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file:storage_config_cloud_test?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.StorageConfig{}, &model.Setting{}, &model.Library{}, &model.Media{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repos := repository.New(db)
|
||||
log := zap.NewNop()
|
||||
storage := service.NewStorageConfigService(log, repos, service.NewCryptoService("", log))
|
||||
enabled := false
|
||||
if _, err := storage.Save(t.Context(), service.StorageInput{
|
||||
Type: "openlist",
|
||||
Config: map[string]any{
|
||||
"server": openlist.URL,
|
||||
"token": "openlist-token",
|
||||
},
|
||||
Enabled: &enabled,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/admin/storage/:type/test", testStorageConfigHandler(&service.Container{StorageCfg: storage}))
|
||||
body := `{"type":"openlist","config":{"server":"` + openlist.URL + `","token":"openlist-token"}}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/admin/storage/openlist/test", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d body=%s, want 200", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), `"ok":true`) {
|
||||
t.Fatalf("body = %s, want ok true", w.Body.String())
|
||||
}
|
||||
view, err := storage.Get(t.Context(), "openlist")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if view == nil || !view.Enabled {
|
||||
t.Fatalf("openlist enabled = %#v, want enabled after successful test", view)
|
||||
}
|
||||
if _, err := storage.CloudProvider(t.Context(), "openlist"); err != nil {
|
||||
t.Fatalf("cloud provider after test: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,11 +182,15 @@ func scrapeOneHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
reclassified := reclassifyMediaAfterScrape(c.Request.Context(), svc, m.ID)
|
||||
refreshed, _ := svc.Repo.Media.FindByID(c.Request.Context(), m.ID)
|
||||
metrics := map[string]int64{"processed": 1}
|
||||
if refreshed != nil && refreshed.ScrapeStatus == "matched" {
|
||||
metrics["matched"] = 1
|
||||
}
|
||||
if reclassified > 0 {
|
||||
metrics["reclassified"] = int64(reclassified)
|
||||
}
|
||||
finishHTTPTask(task, nil, "completed", "手动刮削媒体结束", metrics, nil)
|
||||
c.JSON(http.StatusOK, refreshed)
|
||||
}
|
||||
@@ -212,11 +216,18 @@ func scrapeLibraryHandler(svc *service.Container) gin.HandlerFunc {
|
||||
// pushes per-item progress on the "scrape" topic.
|
||||
go func(libID string, task *service.TaskHandle, options service.ScrapeOptions) {
|
||||
result, err := svc.Scraper.EnrichLibraryDetailedWithOptions(context.Background(), libID, options)
|
||||
reclassified := 0
|
||||
if result.Processed > 0 {
|
||||
reclassified = reclassifyLibraryAfterScrape(context.Background(), svc, libID)
|
||||
}
|
||||
metrics := map[string]int64{
|
||||
"matched": int64(result.Matched),
|
||||
"processed": int64(result.Processed),
|
||||
"candidates": int64(result.Candidates),
|
||||
}
|
||||
if reclassified > 0 {
|
||||
metrics["reclassified"] = int64(reclassified)
|
||||
}
|
||||
if result.Failed > 0 {
|
||||
metrics["errors"] = int64(result.Failed)
|
||||
}
|
||||
|
||||
@@ -11,28 +11,32 @@ import (
|
||||
)
|
||||
|
||||
type subscriptionPatchReq struct {
|
||||
Name *string `json:"name"`
|
||||
FeedURL *string `json:"feed_url"`
|
||||
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"`
|
||||
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"`
|
||||
FeedURL *string `json:"feed_url"`
|
||||
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"`
|
||||
Rating *float32 `json:"rating"`
|
||||
Genres *string `json:"genres"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// updateSubscriptionHandler patches a subscription row.
|
||||
@@ -97,6 +101,18 @@ func subscriptionPatchUpdates(patch subscriptionPatchReq) map[string]any {
|
||||
if patch.Overview != nil {
|
||||
updates["overview"] = *patch.Overview
|
||||
}
|
||||
if patch.OriginalName != nil {
|
||||
updates["original_name"] = *patch.OriginalName
|
||||
}
|
||||
if patch.Year != nil {
|
||||
updates["year"] = *patch.Year
|
||||
}
|
||||
if patch.Rating != nil {
|
||||
updates["rating"] = *patch.Rating
|
||||
}
|
||||
if patch.Genres != nil {
|
||||
updates["genres"] = *patch.Genres
|
||||
}
|
||||
if patch.Resolution != nil {
|
||||
updates["resolution"] = *patch.Resolution
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -96,8 +97,8 @@ func listSubscriptionsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
enrichAndPersistSubscriptions(c.Request.Context(), svc, items)
|
||||
svc.Subscription.EnrichProgress(c.Request.Context(), items)
|
||||
go enrichAndPersistSubscriptions(context.Background(), svc, append([]model.Subscription(nil), items...))
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user