v0.8.3: add sidecar inspection
This commit is contained in:
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
This changelog is maintained from git history plus the published Gitea release series. Future releases should update this file and publish matching release notes on the release page.
|
This changelog is maintained from git history plus the published Gitea release series. Future releases should update this file and publish matching release notes on the release page.
|
||||||
|
|
||||||
|
## v0.8.3
|
||||||
|
|
||||||
|
XMP sidecar inspection release.
|
||||||
|
|
||||||
|
- Add `sidecar inspect <file.xmp>` to print key photoscli metadata from generated XMP sidecars.
|
||||||
|
- Add `sidecar inspect <file.xmp> --json` for scriptable inspection output.
|
||||||
|
|
||||||
## v0.8.2
|
## v0.8.2
|
||||||
|
|
||||||
Metadata-only XMP refresh release.
|
Metadata-only XMP refresh release.
|
||||||
|
|||||||
@@ -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.8.2
|
VERSION := 0.8.3
|
||||||
RELEASE_ZIP := ./bin/photoscli-$(VERSION)-macos-arm64.zip
|
RELEASE_ZIP := ./bin/photoscli-$(VERSION)-macos-arm64.zip
|
||||||
RELEASE_NOTES := RELEASE_NOTES.md
|
RELEASE_NOTES := RELEASE_NOTES.md
|
||||||
BRIDGE_DIR := bridge
|
BRIDGE_DIR := bridge
|
||||||
|
|||||||
@@ -363,6 +363,13 @@ Verify generated sidecars with:
|
|||||||
photoscli verify --out ./backup --sidecar
|
photoscli verify --out ./backup --sidecar
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Inspect one generated sidecar with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
photoscli sidecar inspect ./backup/IMG_0001.xmp
|
||||||
|
photoscli sidecar inspect ./backup/IMG_0001.xmp --json
|
||||||
|
```
|
||||||
|
|
||||||
Refresh sidecars for files already present in a manifest-backed export without rewriting media files:
|
Refresh sidecars for files already present in a manifest-backed export without rewriting media files:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+6
-8
@@ -1,19 +1,17 @@
|
|||||||
# v0.8.2
|
# v0.8.3
|
||||||
|
|
||||||
This release adds metadata-only XMP sidecar refresh for existing manifest-backed exports.
|
This release adds XMP sidecar inspection for quick validation and scripting.
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
- Add `--metadata-only` for `export` and `backup-all`.
|
- Add `sidecar inspect <file.xmp>` to print photoscli metadata embedded in a generated sidecar.
|
||||||
- Use manifest paths to find existing media files and write or refresh `.xmp` sidecars without re-exporting media.
|
- Add `sidecar inspect <file.xmp> --json` for scriptable output.
|
||||||
- Require `--sidecar xmp` and an enabled manifest for metadata-only mode.
|
- Keep `verify --sidecar` for backup-wide checks and use `sidecar inspect` for one-file investigation.
|
||||||
- Leave media files untouched; only generated XMP sidecars are written.
|
|
||||||
- Support `--reverse-geocode` during metadata-only refresh.
|
|
||||||
|
|
||||||
## Assets
|
## Assets
|
||||||
|
|
||||||
- `photoscli`: Apple Silicon macOS binary (`darwin/arm64`).
|
- `photoscli`: Apple Silicon macOS binary (`darwin/arm64`).
|
||||||
- `photoscli-0.8.2-macos-arm64.zip`: Apple Silicon binary plus README, USERGUIDE, and CHANGELOG.
|
- `photoscli-0.8.3-macos-arm64.zip`: Apple Silicon binary plus README, USERGUIDE, and CHANGELOG.
|
||||||
- `USERGUIDE.md`: standalone user guide.
|
- `USERGUIDE.md`: standalone user guide.
|
||||||
|
|
||||||
Intel Macs are not currently a supported release target.
|
Intel Macs are not currently a supported release target.
|
||||||
|
|||||||
@@ -573,6 +573,13 @@ Verify sidecars after an export:
|
|||||||
|
|
||||||
This reports missing, zero-byte, unreadable, or asset-ID mismatched `.xmp` files.
|
This reports missing, zero-byte, unreadable, or asset-ID mismatched `.xmp` files.
|
||||||
|
|
||||||
|
Inspect one generated sidecar when troubleshooting or scripting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/photoscli sidecar inspect ./PhotosBackup/IMG_0001.xmp
|
||||||
|
./bin/photoscli sidecar inspect ./PhotosBackup/IMG_0001.xmp --json
|
||||||
|
```
|
||||||
|
|
||||||
Refresh metadata only for an existing manifest-backed backup:
|
Refresh metadata only for an existing manifest-backed backup:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -91,6 +92,8 @@ func run(args []string, stdout, stderr io.Writer, bridge photos.Bridge) int {
|
|||||||
return cmdFailures(args[1:], stdout, stderr)
|
return cmdFailures(args[1:], stdout, stderr)
|
||||||
case "status":
|
case "status":
|
||||||
return cmdStatus(args[1:], stdout, stderr)
|
return cmdStatus(args[1:], stdout, stderr)
|
||||||
|
case "sidecar":
|
||||||
|
return cmdSidecar(args[1:], stdout, stderr)
|
||||||
case "version", "--version", "-v":
|
case "version", "--version", "-v":
|
||||||
fmt.Fprintln(stdout, version)
|
fmt.Fprintln(stdout, version)
|
||||||
return exitOK
|
return exitOK
|
||||||
@@ -132,6 +135,7 @@ USAGE
|
|||||||
photoscli retry-failed --out <dir> --clear-on-success
|
photoscli retry-failed --out <dir> --clear-on-success
|
||||||
photoscli failures list --out <dir>
|
photoscli failures list --out <dir>
|
||||||
photoscli failures clear --out <dir>
|
photoscli failures clear --out <dir>
|
||||||
|
photoscli sidecar inspect <file.xmp> [--json]
|
||||||
photoscli status --out <dir> [--json]
|
photoscli status --out <dir> [--json]
|
||||||
photoscli version
|
photoscli version
|
||||||
photoscli help
|
photoscli help
|
||||||
@@ -176,6 +180,9 @@ COMMANDS
|
|||||||
failures list|clear --out <dir>
|
failures list|clear --out <dir>
|
||||||
List or clear deduplicated failure records.
|
List or clear deduplicated failure records.
|
||||||
|
|
||||||
|
sidecar inspect <file.xmp> [--json]
|
||||||
|
Read a generated XMP sidecar and print key photoscli metadata.
|
||||||
|
|
||||||
status --out <dir> [--manifest jsonl|sqlite] [--json]
|
status --out <dir> [--manifest jsonl|sqlite] [--json]
|
||||||
Show manifest type, entry count, and failure count for a backup.
|
Show manifest type, entry count, and failure count for a backup.
|
||||||
|
|
||||||
@@ -2187,6 +2194,68 @@ func verifySidecar(stdout io.Writer, outDir, id, checkPath string) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cmdSidecar(args []string, stdout, stderr io.Writer) int {
|
||||||
|
if len(args) < 1 || args[0] != "inspect" {
|
||||||
|
fmt.Fprintln(stderr, "error: expected sidecar inspect <file.xmp>")
|
||||||
|
return exitErr
|
||||||
|
}
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Fprintln(stderr, "error: sidecar inspect requires <file.xmp>")
|
||||||
|
return exitErr
|
||||||
|
}
|
||||||
|
path := args[1]
|
||||||
|
data, err := readFileFunc(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return exitErr
|
||||||
|
}
|
||||||
|
meta := inspectXMP(data)
|
||||||
|
if len(meta) == 0 {
|
||||||
|
fmt.Fprintln(stderr, "error: no photoscli metadata found")
|
||||||
|
return exitErr
|
||||||
|
}
|
||||||
|
if hasFlag(args[2:], "--json") {
|
||||||
|
if err := json.NewEncoder(stdout).Encode(meta); err != nil {
|
||||||
|
fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return exitErr
|
||||||
|
}
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
keys := make([]string, 0, len(meta))
|
||||||
|
for k := range meta {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
fmt.Fprintf(stdout, "%s\t%s\n", k, meta[k])
|
||||||
|
}
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func inspectXMP(data []byte) map[string]string {
|
||||||
|
attrs := map[string]string{}
|
||||||
|
dec := xml.NewDecoder(bytes.NewReader(data))
|
||||||
|
for {
|
||||||
|
tok, err := dec.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
start, ok := tok.(xml.StartElement)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, a := range start.Attr {
|
||||||
|
if a.Name.Space == "photoscli" || a.Name.Space == "https://gitea.k3s.k0.nu/tools/photocli/ns/1.0/" {
|
||||||
|
attrs[a.Name.Local] = a.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
func cmdRetryFailed(args []string, stdout, stderr io.Writer, bridge photos.Bridge) int {
|
func cmdRetryFailed(args []string, stdout, stderr io.Writer, bridge photos.Bridge) int {
|
||||||
outDir := flagVal(args, "--out")
|
outDir := flagVal(args, "--out")
|
||||||
clearOnSuccess := hasFlag(args, "--clear-on-success")
|
clearOnSuccess := hasFlag(args, "--clear-on-success")
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ type mockBridge struct {
|
|||||||
cancelled atomic.Bool
|
cancelled atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type errWriter struct{}
|
||||||
|
|
||||||
|
func (errWriter) Write([]byte) (int, error) { return 0, fmt.Errorf("write") }
|
||||||
|
|
||||||
type noEntryManifest struct{}
|
type noEntryManifest struct{}
|
||||||
|
|
||||||
func (noEntryManifest) Has(string) bool { return false }
|
func (noEntryManifest) Has(string) bool { return false }
|
||||||
@@ -4219,6 +4223,50 @@ func TestVerifySidecarBranches(t *testing.T) {
|
|||||||
readFileFunc = oldRead
|
readFileFunc = oldRead
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSidecarInspect(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "photo.xmp")
|
||||||
|
if err := writeXMPSidecar(path, xmpSidecarData{AssetID: "x1", ExportedFilename: "photo.jpg", Album: "Trips"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out, stderr, rc := runWith([]string{"sidecar", "inspect", path}, &mockBridge{})
|
||||||
|
if rc != exitOK || stderr != "" || !strings.Contains(out, "assetID\tx1") || !strings.Contains(out, "album\tTrips") {
|
||||||
|
t.Fatalf("inspect rc=%d out=%q stderr=%q", rc, out, stderr)
|
||||||
|
}
|
||||||
|
out, stderr, rc = runWith([]string{"sidecar", "inspect", path, "--json"}, &mockBridge{})
|
||||||
|
if rc != exitOK || stderr != "" || !strings.Contains(out, `"assetID":"x1"`) || !strings.Contains(out, `"exportedFilename":"photo.jpg"`) {
|
||||||
|
t.Fatalf("inspect json rc=%d out=%q stderr=%q", rc, out, stderr)
|
||||||
|
}
|
||||||
|
_, stderr, rc = runWith([]string{"sidecar"}, &mockBridge{})
|
||||||
|
if rc != exitErr || !strings.Contains(stderr, "expected sidecar inspect") {
|
||||||
|
t.Fatalf("inspect missing subcommand rc=%d stderr=%q", rc, stderr)
|
||||||
|
}
|
||||||
|
_, stderr, rc = runWith([]string{"sidecar", "inspect"}, &mockBridge{})
|
||||||
|
if rc != exitErr || !strings.Contains(stderr, "requires <file.xmp>") {
|
||||||
|
t.Fatalf("inspect missing path rc=%d stderr=%q", rc, stderr)
|
||||||
|
}
|
||||||
|
_, stderr, rc = runWith([]string{"sidecar", "inspect", filepath.Join(dir, "missing.xmp")}, &mockBridge{})
|
||||||
|
if rc != exitErr || !strings.Contains(stderr, "error:") {
|
||||||
|
t.Fatalf("inspect missing file rc=%d stderr=%q", rc, stderr)
|
||||||
|
}
|
||||||
|
plain := filepath.Join(dir, "plain.xmp")
|
||||||
|
if err := os.WriteFile(plain, []byte(`<x:xmpmeta xmlns:x="adobe:ns:meta/"></x:xmpmeta>`), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, stderr, rc = runWith([]string{"sidecar", "inspect", plain}, &mockBridge{})
|
||||||
|
if rc != exitErr || !strings.Contains(stderr, "no photoscli metadata") {
|
||||||
|
t.Fatalf("inspect no metadata rc=%d stderr=%q", rc, stderr)
|
||||||
|
}
|
||||||
|
bad := inspectXMP([]byte(`<x:xmpmeta><rdf:RDF>`))
|
||||||
|
if len(bad) != 0 {
|
||||||
|
t.Fatalf("expected empty metadata on malformed XML, got %#v", bad)
|
||||||
|
}
|
||||||
|
stderrBuf := &bytes.Buffer{}
|
||||||
|
if rc := cmdSidecar([]string{"inspect", path, "--json"}, errWriter{}, stderrBuf); rc != exitErr || !strings.Contains(stderrBuf.String(), "error:") {
|
||||||
|
t.Fatalf("expected json encoder error rc=%d stderr=%q", rc, stderrBuf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestXMPSidecarHelpers(t *testing.T) {
|
func TestXMPSidecarHelpers(t *testing.T) {
|
||||||
if got := sidecarPath("/tmp/IMG_0001.HEIC"); got != "/tmp/IMG_0001.xmp" {
|
if got := sidecarPath("/tmp/IMG_0001.HEIC"); got != "/tmp/IMG_0001.xmp" {
|
||||||
t.Fatalf("sidecar path = %q", got)
|
t.Fatalf("sidecar path = %q", got)
|
||||||
|
|||||||
Reference in New Issue
Block a user