diff --git a/deploy/docker/Dockerfile.master-api b/deploy/docker/Dockerfile.master-api index a644ba5..65c3e52 100644 --- a/deploy/docker/Dockerfile.master-api +++ b/deploy/docker/Dockerfile.master-api @@ -7,8 +7,8 @@ WORKDIR /src RUN apk add --no-cache git ca-certificates COPY node/media-node/ ./ RUN go mod tidy \ - && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=1.2.0" -o /media-node-linux-amd64 ./cmd/media-node \ - && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=1.2.0" -o /media-node-linux-arm64 ./cmd/media-node + && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=1.2.1" -o /media-node-linux-amd64 ./cmd/media-node \ + && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=1.2.1" -o /media-node-linux-arm64 ./cmd/media-node FROM base AS builder COPY package.json pnpm-workspace.yaml ./ diff --git a/node/media-node/cmd/media-node/main.go b/node/media-node/cmd/media-node/main.go index 1fdef04..76b88ae 100644 --- a/node/media-node/cmd/media-node/main.go +++ b/node/media-node/cmd/media-node/main.go @@ -24,7 +24,7 @@ import ( "github.com/sthmedia/media-node/internal/streaming" ) -var version = "1.2.0" +var version = "1.2.1" func main() { if len(os.Args) < 2 { diff --git a/node/media-node/internal/control/client.go b/node/media-node/internal/control/client.go index 41fcdfc..e10b666 100644 --- a/node/media-node/internal/control/client.go +++ b/node/media-node/internal/control/client.go @@ -614,6 +614,10 @@ func (c *Client) sendFullSync() { } }() + // Never sync a half-built inventory — Master would prune the rest as missing. + log.Println("Waiting for scanner to finish before full library sync...") + c.scanner.WaitUntilIdle() + files := c.scanner.AllMediaFiles() rev, _ := c.store.MaxRevision() syncID := uuid.New().String() diff --git a/node/media-node/internal/scanner/scanner.go b/node/media-node/internal/scanner/scanner.go index 6cba727..a0c54b9 100644 --- a/node/media-node/internal/scanner/scanner.go +++ b/node/media-node/internal/scanner/scanner.go @@ -47,6 +47,7 @@ type Scanner struct { onEvents EventHandler mu sync.Mutex scanning bool + rescanQueued bool status string resetSchedule chan struct{} reloadWatch chan struct{} @@ -69,6 +70,19 @@ func (s *Scanner) Status() string { return s.status } +// WaitUntilIdle blocks until no full scan is running or queued. +func (s *Scanner) WaitUntilIdle() { + for { + s.mu.Lock() + busy := s.scanning || s.rescanQueued + s.mu.Unlock() + if !busy { + return + } + time.Sleep(500 * time.Millisecond) + } +} + func (s *Scanner) Start() { // One inventory pass at boot, then realtime watch + scheduled catch-up. go s.runStartupScan() @@ -204,51 +218,55 @@ func nextLocalClock(now time.Time, hour, minute int) time.Time { } func (s *Scanner) FullScan() { - s.mu.Lock() - if s.scanning { + for { + s.mu.Lock() + if s.scanning { + s.rescanQueued = true + s.mu.Unlock() + return + } + s.scanning = true + s.rescanQueued = false + s.status = "scanning" + roots := s.cfg.Media.EffectiveRoots() s.mu.Unlock() - return - } - s.scanning = true - s.status = "scanning" - s.mu.Unlock() - defer func() { + log.Println("Starting full media scan...") + found := make(map[string]bool) + + for _, root := range roots { + kind := root.Kind + if kind == "series" { + kind = "episode" + } + s.walkRoot(root.Path, kind, root.ShelfID, found) + } + + existing, _ := s.store.AllFiles() + for _, f := range existing { + if !found[f.Path] { + rev, _ := s.store.NextRevision() + event := LibraryEvent{ + Type: "FILE_REMOVED", + NodeRevision: rev, + File: fileToInfo(f), + } + _ = s.store.DeleteFile(f.LocalFileID) + s.onEvents([]LibraryEvent{event}) + } + } + log.Printf("Full scan completed (%d files)", len(found)) + s.mu.Lock() s.scanning = false s.status = "idle" + again := s.rescanQueued s.mu.Unlock() - }() - - log.Println("Starting full media scan...") - found := make(map[string]bool) - - s.mu.Lock() - roots := s.cfg.Media.EffectiveRoots() - s.mu.Unlock() - - for _, root := range roots { - kind := root.Kind - if kind == "series" { - kind = "episode" + if !again { + return } - s.walkRoot(root.Path, kind, root.ShelfID, found) + log.Println("Re-running full scan after path/config change...") } - - existing, _ := s.store.AllFiles() - for _, f := range existing { - if !found[f.Path] { - rev, _ := s.store.NextRevision() - event := LibraryEvent{ - Type: "FILE_REMOVED", - NodeRevision: rev, - File: fileToInfo(f), - } - _ = s.store.DeleteFile(f.LocalFileID) - s.onEvents([]LibraryEvent{event}) - } - } - log.Println("Full scan completed") } func (s *Scanner) walkRoot(dir, mediaType, shelfID string, found map[string]bool) {