Wait for full scan to finish before library sync to avoid Master pruning.

This commit is contained in:
Jos Vooges | STH 2026-08-25 21:50:35 +02:00
parent a0a6430f39
commit 68440f4081
4 changed files with 62 additions and 40 deletions

View file

@ -7,8 +7,8 @@ WORKDIR /src
RUN apk add --no-cache git ca-certificates RUN apk add --no-cache git ca-certificates
COPY node/media-node/ ./ COPY node/media-node/ ./
RUN go mod tidy \ 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=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.0" -o /media-node-linux-arm64 ./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 FROM base AS builder
COPY package.json pnpm-workspace.yaml ./ COPY package.json pnpm-workspace.yaml ./

View file

@ -24,7 +24,7 @@ import (
"github.com/sthmedia/media-node/internal/streaming" "github.com/sthmedia/media-node/internal/streaming"
) )
var version = "1.2.0" var version = "1.2.1"
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {

View file

@ -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() files := c.scanner.AllMediaFiles()
rev, _ := c.store.MaxRevision() rev, _ := c.store.MaxRevision()
syncID := uuid.New().String() syncID := uuid.New().String()

View file

@ -47,6 +47,7 @@ type Scanner struct {
onEvents EventHandler onEvents EventHandler
mu sync.Mutex mu sync.Mutex
scanning bool scanning bool
rescanQueued bool
status string status string
resetSchedule chan struct{} resetSchedule chan struct{}
reloadWatch chan struct{} reloadWatch chan struct{}
@ -69,6 +70,19 @@ func (s *Scanner) Status() string {
return s.status 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() { func (s *Scanner) Start() {
// One inventory pass at boot, then realtime watch + scheduled catch-up. // One inventory pass at boot, then realtime watch + scheduled catch-up.
go s.runStartupScan() go s.runStartupScan()
@ -204,51 +218,55 @@ func nextLocalClock(now time.Time, hour, minute int) time.Time {
} }
func (s *Scanner) FullScan() { func (s *Scanner) FullScan() {
s.mu.Lock() for {
if s.scanning { 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() 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.mu.Lock()
s.scanning = false s.scanning = false
s.status = "idle" s.status = "idle"
again := s.rescanQueued
s.mu.Unlock() s.mu.Unlock()
}() if !again {
return
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"
} }
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) { func (s *Scanner) walkRoot(dir, mediaType, shelfID string, found map[string]bool) {