fix(media): scan new library roots immediately

This commit is contained in:
ShukeBta
2026-08-10 19:15:58 +08:00
parent a64a98be23
commit 60d2cfe2f1
4 changed files with 100 additions and 5 deletions
+15 -2
View File
@@ -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)
}
}
+15 -3
View File
@@ -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)
}
}
+27
View File
@@ -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
+43
View File
@@ -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{})