diff --git a/internal/handler/media.go b/internal/handler/media.go index 38c22f5..81d0fb7 100644 --- a/internal/handler/media.go +++ b/internal/handler/media.go @@ -106,8 +106,21 @@ func createLibraryHandler(svc *service.Container) gin.HandlerFunc { } uid, _ := c.Get("ctx_user_id") svc.Audit.Record(c.Request.Context(), toString(uid), "library.create", l.ID, c.ClientIP(), l.Path) - // Refresh fsnotify watcher to pick up the new library root. - go func() { _ = svc.Watcher.Refresh(context.Background()) }() + // Refresh fsnotify watcher to pick up the new library root, then perform + // an initial scan. Without the scan a newly-created library remained + // empty until the operator pressed the separate "扫描" action. + if svc.Watcher != nil { + go func() { _ = svc.Watcher.Refresh(context.Background()) }() + } + if len(l.Roots) == 0 { + queueLibraryRootScan(svc, l.ID, "") + } else { + for _, root := range l.Roots { + if root.Enabled { + queueLibraryRootScan(svc, l.ID, root.ID) + } + } + } c.JSON(http.StatusCreated, l) } } diff --git a/internal/handler/media_roots.go b/internal/handler/media_roots.go index 19b861a..76f2034 100644 --- a/internal/handler/media_roots.go +++ b/internal/handler/media_roots.go @@ -32,7 +32,12 @@ func createLibraryRootHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - go func() { _ = svc.Watcher.Refresh(context.Background()) }() + if svc.Watcher != nil { + go func() { _ = svc.Watcher.Refresh(context.Background()) }() + } + if root.Enabled { + queueLibraryRootScan(svc, c.Param("id"), root.ID) + } c.JSON(http.StatusCreated, root) } } @@ -53,7 +58,12 @@ func updateLibraryRootHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusNotFound, gin.H{"error": "library root not found"}) return } - go func() { _ = svc.Watcher.Refresh(context.Background()) }() + if svc.Watcher != nil { + go func() { _ = svc.Watcher.Refresh(context.Background()) }() + } + if root.Enabled { + queueLibraryRootScan(svc, c.Param("id"), root.ID) + } c.JSON(http.StatusOK, root) } } @@ -64,7 +74,9 @@ func deleteLibraryRootHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - go func() { _ = svc.Watcher.Refresh(context.Background()) }() + if svc.Watcher != nil { + go func() { _ = svc.Watcher.Refresh(context.Background()) }() + } c.Status(http.StatusNoContent) } } diff --git a/internal/handler/media_scan.go b/internal/handler/media_scan.go index 286f256..21fe5d0 100644 --- a/internal/handler/media_scan.go +++ b/internal/handler/media_scan.go @@ -130,6 +130,33 @@ func scanLibraryRootHandler(svc *service.Container) gin.HandlerFunc { } } +// queueLibraryRootScan starts the same background scan used by the manual +// scan endpoints. Library creation/root addition used to refresh only the +// watcher, which left a newly-created library empty until the user clicked +// "扫描" manually. Keeping this helper in the scan handler makes all mutation +// paths use one scan lifecycle and preserves the local-scan de-duplication. +func queueLibraryRootScan(svc *service.Container, libraryID, rootID string) { + if svc == nil || svc.Scan == nil || strings.TrimSpace(libraryID) == "" { + return + } + key := libraryID + if strings.TrimSpace(rootID) != "" { + key += ":" + rootID + } + finish, ok := svc.Scan.TryBeginLocalScan(key) + if !ok { + return + } + go func() { + defer finish() + if strings.TrimSpace(rootID) == "" { + _, _ = svc.Scan.ScanLibrary(context.Background(), libraryID) + return + } + _, _ = svc.Scan.ScanLibraryRoot(context.Background(), libraryID, rootID) + }() +} + func startScanHTTPTask(svc *service.Container, name, libraryName, path string) *service.TaskHandle { if svc == nil || svc.Tasks == nil { return nil diff --git a/internal/handler/media_test.go b/internal/handler/media_test.go index 792576e..3a8a421 100644 --- a/internal/handler/media_test.go +++ b/internal/handler/media_test.go @@ -7,6 +7,8 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "strings" "testing" "time" @@ -23,6 +25,47 @@ import ( "github.com/ShukeBta/MediaStationGo/internal/service" ) +func TestQueueLibraryRootScanImportsNewLibraryMedia(t *testing.T) { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&model.Library{}, &model.LibraryRoot{}, &model.Media{}); err != nil { + t.Fatal(err) + } + rootPath := t.TempDir() + mediaPath := filepath.Join(rootPath, "Example Movie (2026).mkv") + if err := os.WriteFile(mediaPath, []byte("movie"), 0o644); err != nil { + t.Fatal(err) + } + repos := repository.New(db) + lib := model.Library{Name: "电影", Path: rootPath, Type: "movie", Enabled: true} + root := model.LibraryRoot{Name: "电影", Path: rootPath, Enabled: true} + if err := repos.Library.CreateWithRoots(t.Context(), &lib, []model.LibraryRoot{root}); err != nil { + t.Fatal(err) + } + libWithRoots, err := repos.Library.FindByID(t.Context(), lib.ID) + if err != nil || libWithRoots == nil || len(libWithRoots.Roots) != 1 { + t.Fatalf("library roots=%#v err=%v", libWithRoots, err) + } + scanner := service.NewScannerService(&config.Config{}, zap.NewNop(), repos, service.NewHub(zap.NewNop()), nil, nil) + svc := &service.Container{Repo: repos, Scan: scanner} + queueLibraryRootScan(svc, lib.ID, libWithRoots.Roots[0].ID) + + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + var count int64 + if err := repos.DB.Model(&model.Media{}).Where("library_id = ?", lib.ID).Count(&count).Error; err != nil { + t.Fatal(err) + } + if count == 1 { + return + } + time.Sleep(10 * time.Millisecond) + } + t.Fatal("initial library scan did not import the media file") +} + func TestListLibrariesHidesAdultDirectoriesUnlessAdminRequestsAll(t *testing.T) { gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})