v0.2.1: add status messages, fix parallel export progress

- mustAuth: print 'requesting photo library access...' / 'access granted'
- cmdBackupAll: print 'loading photo library tree...' / 'found N albums'
- cmdExport: print 'loading assets...' / 'exporting N assets (mode)...'
- backupTree: print 'album: Name (N assets)' per album
- exportAssetsParallel: slot-based progress shows each asset as it completes
  instead of waiting for all to finish
- Fix data race: use jobs channel instead of shared range iteration
This commit is contained in:
Ein Anderssono
2026-06-11 21:18:34 +02:00
parent 85eaa3ea37
commit 27ff1b5c83
2 changed files with 41 additions and 36 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
BINARY := ./bin/photoscli BINARY := ./bin/photoscli
MODULE := gitea.k3s.k0.nu/tools/photocli MODULE := gitea.k3s.k0.nu/tools/photocli
VERSION := 0.2.0 VERSION := 0.2.1
BRIDGE_DIR := bridge BRIDGE_DIR := bridge
LDFLAGS := -X main.version=$(VERSION) LDFLAGS := -X main.version=$(VERSION)
OBJ := $(BRIDGE_DIR)/photokit_bridge.o OBJ := $(BRIDGE_DIR)/photokit_bridge.o
+39 -34
View File
@@ -67,10 +67,12 @@ Flags:
} }
func mustAuth(stderr io.Writer, bridge photos.Bridge) int { func mustAuth(stderr io.Writer, bridge photos.Bridge) int {
fmt.Fprintln(stderr, "requesting photo library access...")
if err := bridge.RequestAccess(); err != nil { if err := bridge.RequestAccess(); err != nil {
fmt.Fprintf(stderr, "error: %v\n", err) fmt.Fprintf(stderr, "error: %v\n", err)
return 1 return 1
} }
fmt.Fprintln(stderr, "access granted")
return 0 return 0
} }
@@ -78,6 +80,7 @@ func cmdAlbums(stdout, stderr io.Writer, bridge photos.Bridge) int {
if rc := mustAuth(stderr, bridge); rc != 0 { if rc := mustAuth(stderr, bridge); rc != 0 {
return rc return rc
} }
fmt.Fprintln(stderr, "loading albums...")
albums, err := bridge.ListAlbums() albums, err := bridge.ListAlbums()
if err != nil { if err != nil {
fmt.Fprintf(stderr, "error: %v\n", err) fmt.Fprintf(stderr, "error: %v\n", err)
@@ -179,12 +182,14 @@ func cmdExport(args []string, stdout, stderr io.Writer, bridge photos.Bridge) in
} }
} }
fmt.Fprintf(stderr, "loading assets for album %s...\n", albumID)
assets, total, err := bridge.ListAssets(resolved) assets, total, err := bridge.ListAssets(resolved)
if err != nil { if err != nil {
fmt.Fprintf(stderr, "error: %v\n", err) fmt.Fprintf(stderr, "error: %v\n", err)
return 1 return 1
} }
fmt.Fprintf(stderr, "exporting %d assets (%s) to %s...\n", total, exportMode(originals), outDir)
exported, failed := exportAssets(assets, outDir, size, originals, total, stderr, bridge, "") exported, failed := exportAssets(assets, outDir, size, originals, total, stderr, bridge, "")
if exported == 0 && failed > 0 { if exported == 0 && failed > 0 {
@@ -226,12 +231,14 @@ func cmdBackupAll(args []string, stdout, stderr io.Writer, bridge photos.Bridge)
} }
} }
fmt.Fprintln(stderr, "loading photo library tree...")
nodes, err := bridge.ListTree() nodes, err := bridge.ListTree()
if err != nil { if err != nil {
fmt.Fprintf(stderr, "error: %v\n", err) fmt.Fprintf(stderr, "error: %v\n", err)
return 1 return 1
} }
albumCount := countAlbums(nodes) albumCount := countAlbums(nodes)
fmt.Fprintf(stderr, "found %d albums, exporting to %s...\n", albumCount, outDir)
totalAssets, _, failed, err := backupTree(nodes, outDir, size, originals, stderr, bridge) totalAssets, _, failed, err := backupTree(nodes, outDir, size, originals, stderr, bridge)
if err != nil { if err != nil {
@@ -273,6 +280,7 @@ func backupTree(nodes []photos.CollectionNode, outDir string, targetSize int, or
fmt.Fprintf(stderr, "\n skipped album %s: %v\n", node.Name, err) fmt.Fprintf(stderr, "\n skipped album %s: %v\n", node.Name, err)
continue continue
} }
fmt.Fprintf(stderr, "\nalbum: %s (%d assets)\n", node.Name, assetTotal)
total += assetTotal total += assetTotal
n, f := exportAssets(assets, path, targetSize, originals, total, stderr, bridge, path+"/") n, f := exportAssets(assets, path, targetSize, originals, total, stderr, bridge, path+"/")
exported += n exported += n
@@ -282,17 +290,6 @@ func backupTree(nodes []photos.CollectionNode, outDir string, targetSize int, or
return exported, total, failed, nil return exported, total, failed, nil
} }
type exportJob struct {
asset photos.Asset
index int
}
type exportResult struct {
index int
result photos.ExportResult
err error
}
func exportAssets(assets []photos.Asset, outDir string, targetSize int, originals bool, total int, stderr io.Writer, bridge photos.Bridge, dirPrefix string) (int, int) { func exportAssets(assets []photos.Asset, outDir string, targetSize int, originals bool, total int, stderr io.Writer, bridge photos.Bridge, dirPrefix string) (int, int) {
if len(assets) == 0 { if len(assets) == 0 {
return 0, 0 return 0, 0
@@ -322,50 +319,51 @@ func exportAssetsSerial(assets []photos.Asset, outDir string, targetSize int, or
} }
func exportAssetsParallel(assets []photos.Asset, outDir string, targetSize int, originals bool, total int, stderr io.Writer, bridge photos.Bridge, workers int, dirPrefix string) (int, int) { func exportAssetsParallel(assets []photos.Asset, outDir string, targetSize int, originals bool, total int, stderr io.Writer, bridge photos.Bridge, workers int, dirPrefix string) (int, int) {
jobs := make(chan exportJob, len(assets)) type slot struct {
results := make(chan exportResult, len(assets)) result photos.ExportResult
err error
done chan struct{}
}
slots := make([]slot, len(assets))
for i := range slots {
slots[i].done = make(chan struct{})
}
jobs := make(chan int, len(assets))
var wg sync.WaitGroup var wg sync.WaitGroup
for w := 0; w < workers; w++ { for w := 0; w < workers; w++ {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
for job := range jobs { for i := range jobs {
result, exportErr := exportOne(bridge, job.asset, outDir, targetSize, originals, job.index) result, exportErr := exportOne(bridge, assets[i], outDir, targetSize, originals, i)
results <- exportResult{index: job.index, result: result, err: exportErr} slots[i].result = result
slots[i].err = exportErr
close(slots[i].done)
} }
}() }()
} }
go func() { for i := range assets {
for i, a := range assets { jobs <- i
jobs <- exportJob{asset: a, index: i}
} }
close(jobs) close(jobs)
}()
go func() {
wg.Wait()
close(results)
}()
ordered := make([]exportResult, len(assets))
for r := range results {
ordered[r.index] = r
}
exported := 0 exported := 0
failed := 0 failed := 0
for i, a := range assets { for i, a := range assets {
r := ordered[i] <-slots[i].done
progressBar(stderr, exported+failed+1, total, dirPrefix+r.result.Filename, r.result.Size, r.result.Cloud) s := slots[i]
if r.err != nil { progressBar(stderr, exported+failed+1, total, dirPrefix+s.result.Filename, s.result.Size, s.result.Cloud)
fmt.Fprintf(stderr, "\n failed: %s: %v\n", a.Filename, r.err) if s.err != nil {
fmt.Fprintf(stderr, "\n failed: %s: %v\n", a.Filename, s.err)
failed++ failed++
continue continue
} }
exported++ exported++
} }
wg.Wait()
return exported, failed return exported, failed
} }
@@ -452,3 +450,10 @@ func formatSize(bytes int64) string {
} }
return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb)) return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb))
} }
func exportMode(originals bool) string {
if originals {
return "originals"
}
return "previews"
}