From 27ff1b5c83f5d80b96add812bd690f11089bf3aa Mon Sep 17 00:00:00 2001 From: Ein Anderssono Date: Thu, 11 Jun 2026 21:18:34 +0200 Subject: [PATCH] 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 --- Makefile | 2 +- cmd/photoscli/main.go | 75 +++++++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/Makefile b/Makefile index 10a42c9..caf44cf 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ BINARY := ./bin/photoscli MODULE := gitea.k3s.k0.nu/tools/photocli -VERSION := 0.2.0 +VERSION := 0.2.1 BRIDGE_DIR := bridge LDFLAGS := -X main.version=$(VERSION) OBJ := $(BRIDGE_DIR)/photokit_bridge.o diff --git a/cmd/photoscli/main.go b/cmd/photoscli/main.go index e3bcb5b..fcb5294 100644 --- a/cmd/photoscli/main.go +++ b/cmd/photoscli/main.go @@ -67,10 +67,12 @@ Flags: } func mustAuth(stderr io.Writer, bridge photos.Bridge) int { + fmt.Fprintln(stderr, "requesting photo library access...") if err := bridge.RequestAccess(); err != nil { fmt.Fprintf(stderr, "error: %v\n", err) return 1 } + fmt.Fprintln(stderr, "access granted") return 0 } @@ -78,6 +80,7 @@ func cmdAlbums(stdout, stderr io.Writer, bridge photos.Bridge) int { if rc := mustAuth(stderr, bridge); rc != 0 { return rc } + fmt.Fprintln(stderr, "loading albums...") albums, err := bridge.ListAlbums() if err != nil { 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) if err != nil { fmt.Fprintf(stderr, "error: %v\n", err) 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, "") 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() if err != nil { fmt.Fprintf(stderr, "error: %v\n", err) return 1 } 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) 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) continue } + fmt.Fprintf(stderr, "\nalbum: %s (%d assets)\n", node.Name, assetTotal) total += assetTotal n, f := exportAssets(assets, path, targetSize, originals, total, stderr, bridge, path+"/") exported += n @@ -282,17 +290,6 @@ func backupTree(nodes []photos.CollectionNode, outDir string, targetSize int, or 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) { if len(assets) == 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) { - jobs := make(chan exportJob, len(assets)) - results := make(chan exportResult, len(assets)) + type slot struct { + 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 for w := 0; w < workers; w++ { wg.Add(1) go func() { defer wg.Done() - for job := range jobs { - result, exportErr := exportOne(bridge, job.asset, outDir, targetSize, originals, job.index) - results <- exportResult{index: job.index, result: result, err: exportErr} + for i := range jobs { + result, exportErr := exportOne(bridge, assets[i], outDir, targetSize, originals, i) + slots[i].result = result + slots[i].err = exportErr + close(slots[i].done) } }() } - go func() { - for i, a := range assets { - jobs <- exportJob{asset: a, index: i} - } - close(jobs) - }() - - go func() { - wg.Wait() - close(results) - }() - - ordered := make([]exportResult, len(assets)) - for r := range results { - ordered[r.index] = r + for i := range assets { + jobs <- i } + close(jobs) exported := 0 failed := 0 for i, a := range assets { - r := ordered[i] - progressBar(stderr, exported+failed+1, total, dirPrefix+r.result.Filename, r.result.Size, r.result.Cloud) - if r.err != nil { - fmt.Fprintf(stderr, "\n failed: %s: %v\n", a.Filename, r.err) + <-slots[i].done + s := slots[i] + progressBar(stderr, exported+failed+1, total, dirPrefix+s.result.Filename, s.result.Size, s.result.Cloud) + if s.err != nil { + fmt.Fprintf(stderr, "\n failed: %s: %v\n", a.Filename, s.err) failed++ continue } exported++ } + wg.Wait() return exported, failed } @@ -452,3 +450,10 @@ func formatSize(bytes int64) string { } return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb)) } + +func exportMode(originals bool) string { + if originals { + return "originals" + } + return "previews" +}