mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
fix cloud library scale and playback compatibility
Root cause: display-only cloud library filtering was reused by scan jobs, merged cloud mounts could be skipped, and OpenList listing relied on WebDAV/first-page behavior that could cap huge directories around 100 items. Cloud scans also let new items consume probe budget before existing rows with missing track/artwork metadata.\n\nChanges:\n- split display filtering from scannable cloud filtering so merged cloud mounts still scan\n- add OpenList API pagination with WebDAV fallback\n- prioritize existing cloud media missing metadata before new imports\n- restrict automatic cloud sync to one successful 19:00-21:00 daily window and keep manual scan immediate\n- disable startup cloud full-scan by default, with an explicit opt-in setting\n- improve Emby/cloud playback compatibility and cache/search/test coverage from the continued work
This commit is contained in:
+37
-15
@@ -74,7 +74,7 @@ func cloudMountHandler(svc *service.Container) gin.HandlerFunc {
|
||||
path := service.BuildCloudLibraryPath(typ, in.Dir, in.DirPath)
|
||||
name := strings.TrimSpace(in.Name)
|
||||
if name == "" {
|
||||
name = cloudMountLibraryName(typ, strings.TrimSpace(in.Dir))
|
||||
name = cloudMountLibraryName(typ, strings.TrimSpace(in.Dir), strings.TrimSpace(in.DirPath))
|
||||
}
|
||||
mediaType := strings.TrimSpace(in.MediaType)
|
||||
if mediaType == "" || strings.EqualFold(mediaType, "auto") {
|
||||
@@ -120,7 +120,8 @@ func cloudMountHandler(svc *service.Container) gin.HandlerFunc {
|
||||
updates["type"] = mediaType
|
||||
lib.Type = mediaType
|
||||
}
|
||||
if name != "" && name != lib.Name && !strings.Contains(lib.Name, " · ") {
|
||||
currentDisplayName, _ := service.CloudLibraryDisplayName(*lib)
|
||||
if name != "" && name != lib.Name && (currentDisplayName == "" || currentDisplayName != name || strings.Contains(lib.Name, " · ")) {
|
||||
updates["name"] = name
|
||||
lib.Name = name
|
||||
}
|
||||
@@ -211,22 +212,31 @@ func cloudScanStatusHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func cloudMountLibraryName(typ, dir string) string {
|
||||
base := typ
|
||||
switch typ {
|
||||
case cloud.TypeQuark:
|
||||
base = "夸克网盘"
|
||||
case cloud.Type115:
|
||||
base = "115 网盘"
|
||||
case cloud.TypeCloudDrive2:
|
||||
base = "CloudDrive2"
|
||||
case cloud.TypeOpenList:
|
||||
base = "OpenList"
|
||||
func cloudMountLibraryName(typ, dir, displayDir string) string {
|
||||
base := service.CloudMountProviderLabel(typ)
|
||||
displayDir = strings.Trim(strings.TrimSpace(strings.ReplaceAll(displayDir, "\\", "/")), "/")
|
||||
if displayDir != "" {
|
||||
parts := strings.Split(displayDir, "/")
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
if part := strings.TrimSpace(parts[i]); part != "" {
|
||||
return part
|
||||
}
|
||||
}
|
||||
}
|
||||
if dir == "" || dir == "0" {
|
||||
return base
|
||||
}
|
||||
return base + " · " + dir
|
||||
dir = strings.Trim(strings.TrimSpace(strings.ReplaceAll(dir, "\\", "/")), "/")
|
||||
if dir == "" {
|
||||
return base
|
||||
}
|
||||
parts := strings.Split(dir, "/")
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
if part := strings.TrimSpace(parts[i]); part != "" {
|
||||
return part
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// cloud115QRStartHandler begins a 115 QR-code login and returns the session +
|
||||
@@ -329,17 +339,29 @@ func serveCloudResolvedLink(svc *service.Container, c *gin.Context, typ, ref str
|
||||
if rng := c.GetHeader("Range"); rng != "" {
|
||||
req.Header.Set("Range", rng)
|
||||
}
|
||||
if accept := c.GetHeader("Accept"); accept != "" {
|
||||
req.Header.Set("Accept", accept)
|
||||
}
|
||||
if c.GetHeader("Accept-Encoding") == "" {
|
||||
req.Header.Set("Accept-Encoding", "identity")
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
for _, h := range []string{"Content-Type", "Content-Length", "Content-Range", "Accept-Ranges"} {
|
||||
for _, h := range []string{"Content-Type", "Content-Length", "Content-Range", "Accept-Ranges", "ETag", "Last-Modified"} {
|
||||
if v := resp.Header.Get(h); v != "" {
|
||||
c.Header(h, v)
|
||||
}
|
||||
}
|
||||
if c.Writer.Header().Get("Accept-Ranges") == "" {
|
||||
c.Header("Accept-Ranges", "bytes")
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
c.Header("Cache-Control", "no-store")
|
||||
}
|
||||
c.Status(resp.StatusCode)
|
||||
if c.Request.Method != http.MethodHead {
|
||||
_, _ = io.Copy(c.Writer, resp.Body)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package handler
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCloudMountLibraryNameDefaultsToDirectoryBaseName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
provider string
|
||||
dir string
|
||||
displayDir string
|
||||
want string
|
||||
}{
|
||||
{name: "openlist directory", provider: "openlist", dir: "/国产剧", displayDir: "/国产剧", want: "国产剧"},
|
||||
{name: "nested directory", provider: "openlist", dir: "id-123", displayDir: "剧集/国产剧", want: "国产剧"},
|
||||
{name: "provider root", provider: "openlist", dir: "", displayDir: "", want: "OpenList"},
|
||||
{name: "115 root id", provider: "cloud115", dir: "0", displayDir: "", want: "115 网盘"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := cloudMountLibraryName(tt.provider, tt.dir, tt.displayDir); got != tt.want {
|
||||
t.Fatalf("cloudMountLibraryName() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,30 @@ func embyPingHandler(_ *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func embyRootHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, embyPublicSystemInfoPayload(c, svc))
|
||||
}
|
||||
}
|
||||
|
||||
func embyPublicSystemInfoPayload(c *gin.Context, svc *service.Container) map[string]any {
|
||||
if svc != nil && svc.Emby != nil {
|
||||
return embyWithRequestAddress(c, svc.Emby.SystemInfoPublic())
|
||||
}
|
||||
return embyWithRequestAddress(c, map[string]any{
|
||||
"Id": "mediastation-go-001",
|
||||
"ServerId": "mediastation-go-001",
|
||||
"ServerName": "MediaStationGo",
|
||||
"Version": "4.8.10.0",
|
||||
"ServerVersion": "4.8.10.0",
|
||||
"ProductName": "Emby Server",
|
||||
"OperatingSystem": "Windows",
|
||||
"SupportsHttps": false,
|
||||
"SupportsAutoDiscovery": true,
|
||||
"StartupWizardCompleted": true,
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Users / Auth ────────────────────────────────────────────────────────────
|
||||
|
||||
type embyAuthByNameReq struct {
|
||||
@@ -446,6 +470,7 @@ func embyVirtualFoldersHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
libs = service.FilterDisplayCloudLibraries(c.Request.Context(), svc.Repo, libs)
|
||||
uid := embyUserID(c)
|
||||
visibility := service.UserDefaultMediaVisibility(c.Request.Context(), svc.Repo, uid)
|
||||
out := make([]gin.H, 0, len(libs))
|
||||
@@ -986,6 +1011,40 @@ func embyServerConfigurationHandler(_ *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func embyPublicServerConfigurationHandler(_ *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"IsStartupWizardCompleted": true,
|
||||
"EnableRemoteAccess": true,
|
||||
"EnableUPnP": false,
|
||||
"EnableHttps": false,
|
||||
"RequireHttps": false,
|
||||
"LocalNetworkSubnets": []string{},
|
||||
"LocalNetworkAddresses": []string{},
|
||||
"RemoteClientBitrateLimit": 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func embyStartupConfigurationHandler(_ *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"IsStartupWizardCompleted": true,
|
||||
"StartupWizardCompleted": true,
|
||||
"EnableRemoteAccess": true,
|
||||
"UICulture": "zh-CN",
|
||||
"MetadataCountryCode": "CN",
|
||||
"PreferredMetadataLanguage": "zh-CN",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func embyQuickConnectEnabledHandler(_ *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, false)
|
||||
}
|
||||
}
|
||||
|
||||
func embyEmptyItemsHandler(_ *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"Items": []any{}, "TotalRecordCount": 0})
|
||||
@@ -1002,6 +1061,12 @@ func embyBrandingConfigHandler(_ *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func embyBrandingCSSHandler(_ *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "text/css; charset=utf-8", []byte(""))
|
||||
}
|
||||
}
|
||||
|
||||
func embyLocalizationOptionsHandler(_ *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, []map[string]any{
|
||||
@@ -1022,6 +1087,13 @@ func registerEmbyRoutes(r *gin.Engine, jwtSecret string, svc *service.Container)
|
||||
c.Next()
|
||||
})
|
||||
|
||||
if prefix == "/emby" {
|
||||
grp.GET("", embyRootHandler(svc))
|
||||
grp.HEAD("", embyRootHandler(svc))
|
||||
grp.GET("/", embyRootHandler(svc))
|
||||
grp.HEAD("/", embyRootHandler(svc))
|
||||
}
|
||||
|
||||
// 公开端点
|
||||
for _, path := range []string{"/System/Info/Public", "/system/info/public"} {
|
||||
grp.GET(path, embySystemInfoPublicHandler(svc))
|
||||
@@ -1034,11 +1106,29 @@ func registerEmbyRoutes(r *gin.Engine, jwtSecret string, svc *service.Container)
|
||||
for _, path := range []string{"/System/Endpoint", "/system/endpoint"} {
|
||||
grp.GET(path, embySystemEndpointHandler(svc))
|
||||
}
|
||||
for _, path := range []string{"/System/Configuration/Public", "/system/configuration/public"} {
|
||||
grp.GET(path, embyPublicServerConfigurationHandler(svc))
|
||||
grp.HEAD(path, embyPublicServerConfigurationHandler(svc))
|
||||
}
|
||||
for _, path := range []string{"/Startup/Configuration", "/startup/configuration"} {
|
||||
grp.GET(path, embyStartupConfigurationHandler(svc))
|
||||
grp.HEAD(path, embyStartupConfigurationHandler(svc))
|
||||
}
|
||||
for _, path := range []string{"/Startup/Complete", "/startup/complete"} {
|
||||
grp.POST(path, embyNoContentHandler(svc))
|
||||
}
|
||||
for _, path := range []string{"/QuickConnect/Enabled", "/quickconnect/enabled"} {
|
||||
grp.GET(path, embyQuickConnectEnabledHandler(svc))
|
||||
grp.HEAD(path, embyQuickConnectEnabledHandler(svc))
|
||||
}
|
||||
for _, path := range []string{"/System/Ping", "/system/ping"} {
|
||||
grp.GET(path, embyPingHandler(svc))
|
||||
grp.HEAD(path, embyPingHandler(svc))
|
||||
grp.POST(path, embyPingHandler(svc))
|
||||
}
|
||||
for _, path := range []string{"/Sessions/Capabilities", "/Sessions/Capabilities/Full", "/sessions/capabilities", "/sessions/capabilities/full"} {
|
||||
grp.POST(path, embyNoContentHandler(svc))
|
||||
}
|
||||
// 30/min per IP: many Emby clients sit behind a single NAT/reverse-proxy
|
||||
// IP, so a low limit would throttle legitimate logins into 429s.
|
||||
embyLoginLimiter := middleware.NewRateLimiter(30, 1*time.Minute)
|
||||
@@ -1051,6 +1141,10 @@ func registerEmbyRoutes(r *gin.Engine, jwtSecret string, svc *service.Container)
|
||||
for _, path := range []string{"/Branding/Configuration", "/branding/configuration"} {
|
||||
grp.GET(path, embyBrandingConfigHandler(svc))
|
||||
}
|
||||
for _, path := range []string{"/Branding/Css", "/branding/css"} {
|
||||
grp.GET(path, embyBrandingCSSHandler(svc))
|
||||
grp.HEAD(path, embyBrandingCSSHandler(svc))
|
||||
}
|
||||
for _, path := range []string{"/Localization/Options", "/localization/options"} {
|
||||
grp.GET(path, embyLocalizationOptionsHandler(svc))
|
||||
}
|
||||
@@ -1109,8 +1203,6 @@ func registerEmbyRoutes(r *gin.Engine, jwtSecret string, svc *service.Container)
|
||||
auth.POST("/Sessions/Playing", embyPlayingProgressHandler(svc))
|
||||
auth.POST("/Sessions/Playing/Progress", embyPlayingProgressHandler(svc))
|
||||
auth.POST("/Sessions/Playing/Stopped", embyPlayingProgressHandler(svc))
|
||||
auth.POST("/Sessions/Capabilities", embyNoContentHandler(svc))
|
||||
auth.POST("/Sessions/Capabilities/Full", embyNoContentHandler(svc))
|
||||
|
||||
auth.POST("/Users/:userId/FavoriteItems/:itemId", embyFavoriteHandler(svc, true))
|
||||
auth.DELETE("/Users/:userId/FavoriteItems/:itemId", embyFavoriteHandler(svc, false))
|
||||
@@ -1175,8 +1267,6 @@ func registerLowercaseEmbyAuthRoutes(auth *gin.RouterGroup, svc *service.Contain
|
||||
auth.POST("/sessions/playing", embyPlayingProgressHandler(svc))
|
||||
auth.POST("/sessions/playing/progress", embyPlayingProgressHandler(svc))
|
||||
auth.POST("/sessions/playing/stopped", embyPlayingProgressHandler(svc))
|
||||
auth.POST("/sessions/capabilities", embyNoContentHandler(svc))
|
||||
auth.POST("/sessions/capabilities/full", embyNoContentHandler(svc))
|
||||
|
||||
auth.POST("/users/:userId/favoriteitems/:itemId", embyFavoriteHandler(svc, true))
|
||||
auth.DELETE("/users/:userId/favoriteitems/:itemId", embyFavoriteHandler(svc, false))
|
||||
|
||||
@@ -179,6 +179,60 @@ func TestEmbyPublicSystemInfoLooksLikeModernEmbyServer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbySenPlayerDiscoveryRoutesReturnProtocolResponses(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
cfg := &config.Config{}
|
||||
cfg.App.Port = 9011
|
||||
repos := repository.New(db)
|
||||
router := gin.New()
|
||||
registerEmbyRoutes(router, "test-secret", &service.Container{
|
||||
Repo: repos,
|
||||
Emby: service.NewEmbyService(cfg, zap.NewNop(), repos),
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
contentType string
|
||||
contains string
|
||||
}{
|
||||
{path: "/emby", contentType: "application/json", contains: "Emby Server"},
|
||||
{path: "/emby/", contentType: "application/json", contains: "Emby Server"},
|
||||
{path: "/Startup/Configuration", contentType: "application/json", contains: "StartupWizardCompleted"},
|
||||
{path: "/emby/Startup/Configuration", contentType: "application/json", contains: "StartupWizardCompleted"},
|
||||
{path: "/System/Configuration/Public", contentType: "application/json", contains: "IsStartupWizardCompleted"},
|
||||
{path: "/emby/System/Configuration/Public", contentType: "application/json", contains: "IsStartupWizardCompleted"},
|
||||
{path: "/QuickConnect/Enabled", contentType: "application/json", contains: "false"},
|
||||
{path: "/emby/QuickConnect/Enabled", contentType: "application/json", contains: "false"},
|
||||
{path: "/Branding/Css", contentType: "text/css", contains: ""},
|
||||
{path: "/emby/Branding/Css", contentType: "text/css", contains: ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if contentType := w.Header().Get("Content-Type"); !strings.Contains(contentType, tt.contentType) {
|
||||
t.Fatalf("Content-Type = %q, want %q", contentType, tt.contentType)
|
||||
}
|
||||
if tt.contains != "" && !strings.Contains(w.Body.String(), tt.contains) {
|
||||
t.Fatalf("body = %q, want contains %q", w.Body.String(), tt.contains)
|
||||
}
|
||||
if strings.Contains(w.Body.String(), "<html") {
|
||||
t.Fatalf("protocol discovery route served SPA HTML: %q", w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbyUppercaseSessionCapabilitiesRouteNoContent(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
@@ -215,6 +269,25 @@ func TestEmbyUppercaseSessionCapabilitiesRouteNoContent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbySessionCapabilitiesRouteAllowsPreAuthProbe(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
registerEmbyRoutes(router, "test-secret", &service.Container{})
|
||||
|
||||
for _, path := range []string{"/Sessions/Capabilities", "/Sessions/Capabilities/Full", "/emby/Sessions/Capabilities", "/emby/Sessions/Capabilities/Full"} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbyVirtualFoldersRouteReturnsJSON(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
|
||||
@@ -29,7 +29,7 @@ func listLibrariesHandler(svc *service.Container) gin.HandlerFunc {
|
||||
role, _ := c.Get(middleware.CtxUserRole)
|
||||
includeHidden := role == "admin" && (c.Query("include_hidden") == "1" || c.Query("all") == "1")
|
||||
if !includeHidden {
|
||||
libs = service.FilterShadowedCloudLibraries(libs)
|
||||
libs = service.FilterDisplayCloudLibraries(c.Request.Context(), svc.Repo, libs)
|
||||
visibility := mediaVisibilityForRequest(c, svc)
|
||||
filtered := libs[:0]
|
||||
for _, lib := range libs {
|
||||
@@ -38,6 +38,8 @@ func listLibrariesHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
libs = filtered
|
||||
} else {
|
||||
libs = service.NormalizeCloudLibraryDisplayNames(libs)
|
||||
}
|
||||
c.JSON(http.StatusOK, libs)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,34 @@ func TestListLibrariesHidesAdultDirectoriesUnlessAdminRequestsAll(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLibrariesIncludeHiddenNormalizesCloudDisplayNames(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.Library{}, &model.Media{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repos := repository.New(db)
|
||||
cloud := model.Library{Name: "OpenList · 国产剧", Path: service.BuildCloudLibraryPath("openlist", "/国产剧", "/国产剧"), Type: "tv", Enabled: true}
|
||||
if err := repos.Library.Create(t.Context(), &cloud); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
svc := &service.Container{
|
||||
Repo: repos,
|
||||
Media: service.NewMediaService(&config.Config{}, zap.NewNop(), repos),
|
||||
}
|
||||
|
||||
all := requestLibraries(t, svc, "admin", "admin", "/api/libraries?include_hidden=1")
|
||||
if len(all) != 1 {
|
||||
t.Fatalf("include_hidden list = %#v, want one library", all)
|
||||
}
|
||||
if all[0].Name != "国产剧" {
|
||||
t.Fatalf("cloud display name = %q, want stripped directory name", all[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func requestLibraries(t *testing.T, svc *service.Container, userID, role, path string) []model.Library {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -32,7 +32,7 @@ func applyStatsVisibility(c *gin.Context, svc *service.Container, snap *service.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
libs = service.FilterShadowedCloudLibraries(libs)
|
||||
libs = service.FilterDisplayCloudLibraries(c.Request.Context(), svc.Repo, libs)
|
||||
var visibleLibraries int64
|
||||
activeLibraryIDs := make([]string, 0, len(libs))
|
||||
for _, lib := range libs {
|
||||
|
||||
@@ -134,7 +134,7 @@ func statsLibrariesHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
libs = service.FilterShadowedCloudLibraries(libs)
|
||||
libs = service.FilterDisplayCloudLibraries(c.Request.Context(), svc.Repo, libs)
|
||||
out := make([]gin.H, 0, len(libs))
|
||||
visibility := mediaVisibilityForRequest(c, svc)
|
||||
for _, l := range libs {
|
||||
|
||||
@@ -104,8 +104,9 @@ func schemaHandler(_ *service.Container) gin.HandlerFunc {
|
||||
"key": "cloud-upload",
|
||||
"label": "网盘转存",
|
||||
"items": []gin.H{
|
||||
{"key": "cloud.auto_sync_enabled", "type": "toggle", "label": "自动同步网盘媒体库"},
|
||||
{"key": "cloud.sync_interval_seconds", "type": "number", "label": "网盘媒体库同步间隔秒数"},
|
||||
{"key": "cloud.auto_sync_enabled", "type": "toggle", "label": "夜间自动同步网盘媒体库"},
|
||||
{"key": "cloud.sync_interval_seconds", "type": "number", "label": "夜间窗口检查间隔秒数"},
|
||||
{"key": "cloud.boot_scan_enabled", "type": "toggle", "label": "启动后立即扫描网盘"},
|
||||
{"key": "cloud.upload_auto_enabled", "type": "toggle", "label": "启用自动转存"},
|
||||
{"key": "cloud.upload_provider", "type": "select", "label": "转存目标", "options": []gin.H{
|
||||
{"value": "openlist", "label": "OpenList(推荐,可桥接 115/123/阿里/夸克)"},
|
||||
|
||||
Reference in New Issue
Block a user