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:
ShukeBta
2026-06-11 18:31:01 +08:00
parent c22d8271ae
commit 23d1fbfddb
35 changed files with 2797 additions and 139 deletions
+30 -18
View File
@@ -78,7 +78,6 @@ func main() {
if err := services.Auth.SeedAdmin(context.Background()); err != nil {
logger.Warn("seed admin failed", zap.Error(err))
}
services.Boot()
router := buildRouter(cfg, logger, services)
@@ -88,27 +87,29 @@ func main() {
ReadHeaderTimeout: 15 * time.Second,
}
ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
logger.Fatal("listen failed", zap.String("addr", srv.Addr), zap.Error(err))
}
localIP := getLocalIP()
logger.Info("server is ready",
zap.String("local", fmt.Sprintf("http://%s:%d", localIP, cfg.App.Port)),
zap.String("listen", srv.Addr),
)
go func() {
localIP := getLocalIP()
publicIP := getPublicIP(3 * time.Second)
if publicIP != "" {
logger.Info("server is ready",
zap.String("local", fmt.Sprintf("http://%s:%d", localIP, cfg.App.Port)),
zap.String("public", fmt.Sprintf("http://%s:%d", publicIP, cfg.App.Port)),
zap.String("listen", srv.Addr),
)
} else {
logger.Info("server is ready",
zap.String("local", fmt.Sprintf("http://%s:%d", localIP, cfg.App.Port)),
zap.String("listen", srv.Addr),
)
}
// 自动启动 Telegram 长轮询(无需公网 Webhook)
services.TelegramBot.StartPolling(context.Background())
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Fatal("listen failed", zap.Error(err))
}
}()
go func() {
if publicIP := getPublicIP(3 * time.Second); publicIP != "" {
logger.Info("server public endpoint",
zap.String("public", fmt.Sprintf("http://%s:%d", publicIP, cfg.App.Port)),
)
}
}()
go services.Boot()
go services.TelegramBot.StartPolling(context.Background())
// Graceful shutdown.
stop := make(chan os.Signal, 1)
@@ -200,6 +201,13 @@ func setNoCacheHeaders(c *gin.Context) {
func shouldBypassSPAFallback(path string) bool {
lower := strings.ToLower(path)
for _, exact := range []string{
"/emby",
} {
if lower == exact {
return true
}
}
for _, prefix := range []string{
"/api/",
"/emby/",
@@ -213,6 +221,10 @@ func shouldBypassSPAFallback(path string) bool {
"/displaypreferences/",
"/branding/",
"/localization/",
"/startup/",
"/quickconnect/",
"/socket",
"/embywebsocket",
} {
if strings.HasPrefix(lower, prefix) {
return true
+17 -8
View File
@@ -72,14 +72,23 @@ func TestServeSPAServesAssetsImmutableAndBypassesAPIRoutes(t *testing.T) {
t.Fatalf("asset Cache-Control = %q, want immutable", got)
}
apiReq := httptest.NewRequest(http.MethodGet, "/api/missing", nil)
apiResp := httptest.NewRecorder()
router.ServeHTTP(apiResp, apiReq)
if apiResp.Code != http.StatusNotFound {
t.Fatalf("api fallback status = %d, want 404", apiResp.Code)
}
if strings.Contains(apiResp.Body.String(), "index") {
t.Fatalf("api route should not serve SPA index: %q", apiResp.Body.String())
for _, path := range []string{
"/api/missing",
"/emby",
"/emby/missing",
"/Startup/Configuration",
"/QuickConnect/Enabled",
"/embywebsocket",
} {
req := httptest.NewRequest(http.MethodGet, path, nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusNotFound {
t.Fatalf("%s fallback status = %d, want 404", path, resp.Code)
}
if strings.Contains(resp.Body.String(), "index") {
t.Fatalf("%s should not serve SPA index: %q", path, resp.Body.String())
}
}
}