Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c40b1d3ba | |||
| 700d8ef05a | |||
| fbc37b8d8d |
@@ -2,6 +2,30 @@
|
||||
|
||||
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.6
|
||||
|
||||
XMP keyword and rating controls release.
|
||||
|
||||
- Add `--xmp-keywords album-path|album|none` for generated sidecars.
|
||||
- Add `--xmp-rating favorite|none` for generated sidecars.
|
||||
- Keep existing keyword/rating behavior as defaults with `album-path` and `favorite`.
|
||||
|
||||
## v0.8.5
|
||||
|
||||
XMP privacy controls release.
|
||||
|
||||
- Add `--xmp-privacy keep|strip-location|strip-address` for generated sidecars.
|
||||
- Keep existing XMP location/address behavior as the default with `keep`.
|
||||
- Allow GPS coordinates to be kept while reverse-geocoded address fields are omitted with `strip-address`.
|
||||
- Allow both GPS coordinates and address fields to be omitted with `strip-location`.
|
||||
|
||||
## v0.8.4
|
||||
|
||||
Strict XMP sidecar verification release.
|
||||
|
||||
- Add `verify --sidecar --strict` to require photoscli XMP schema metadata, sidecar generator metadata, and matching exported filename metadata.
|
||||
- Keep existing `verify --sidecar` behavior unchanged for backup-wide existence, readability, and asset-ID checks.
|
||||
|
||||
## v0.8.3
|
||||
|
||||
XMP sidecar inspection release.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
BINARY := ./bin/photoscli
|
||||
MODULE := gitea.k3s.k0.nu/tools/photocli
|
||||
VERSION := 0.8.3
|
||||
VERSION := 0.8.6
|
||||
RELEASE_ZIP := ./bin/photoscli-$(VERSION)-macos-arm64.zip
|
||||
RELEASE_NOTES := RELEASE_NOTES.md
|
||||
BRIDGE_DIR := bridge
|
||||
|
||||
@@ -357,12 +357,22 @@ The XMP contains photoscli metadata such as asset ID, filenames, album, manifest
|
||||
|
||||
Sidecars also include richer public PhotoKit metadata where available: modification date, duration, hidden state, adjustment state, media subtypes, source type, playback style, burst data, GPS coordinates, adjustment info, structured asset resources, standard XMP dates, EXIF GPS coordinates, favorite rating, and album/folder keywords. Add `--reverse-geocode` to include cached address fields from Apple MapKit for assets with GPS coordinates. Reverse geocoding requires macOS 26 or newer; on older macOS versions the export continues and XMP still includes GPS coordinates.
|
||||
|
||||
Control XMP location metadata with `--xmp-privacy keep|strip-location|strip-address`. The default is `keep`. Use `strip-address` to omit reverse-geocoded address fields while keeping GPS coordinates, or `strip-location` to omit both GPS and address fields.
|
||||
|
||||
Control generated XMP keywords and ratings with `--xmp-keywords album-path|album|none` and `--xmp-rating favorite|none`. Defaults preserve existing behavior: album/folder keywords and favorite assets mapped to `xmp:Rating="5"`.
|
||||
|
||||
Verify generated sidecars with:
|
||||
|
||||
```bash
|
||||
photoscli verify --out ./backup --sidecar
|
||||
```
|
||||
|
||||
For stricter checks against recent photoscli-generated XMP sidecars:
|
||||
|
||||
```bash
|
||||
photoscli verify --out ./backup --sidecar --strict
|
||||
```
|
||||
|
||||
Inspect one generated sidecar with:
|
||||
|
||||
```bash
|
||||
|
||||
+7
-6
@@ -1,17 +1,18 @@
|
||||
# v0.8.3
|
||||
# v0.8.6
|
||||
|
||||
This release adds XMP sidecar inspection for quick validation and scripting.
|
||||
This release adds keyword and rating controls for generated XMP sidecars.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Add `sidecar inspect <file.xmp>` to print photoscli metadata embedded in a generated sidecar.
|
||||
- Add `sidecar inspect <file.xmp> --json` for scriptable output.
|
||||
- Keep `verify --sidecar` for backup-wide checks and use `sidecar inspect` for one-file investigation.
|
||||
- Add `--xmp-keywords album-path|album|none`.
|
||||
- Add `--xmp-rating favorite|none`.
|
||||
- Keep existing behavior by default with album-path keywords and favorite-to-rating mapping.
|
||||
- Use these flags when another DAM or metadata tool should own keywords or ratings.
|
||||
|
||||
## Assets
|
||||
|
||||
- `photoscli`: Apple Silicon macOS binary (`darwin/arm64`).
|
||||
- `photoscli-0.8.3-macos-arm64.zip`: Apple Silicon binary plus README, USERGUIDE, and CHANGELOG.
|
||||
- `photoscli-0.8.6-macos-arm64.zip`: Apple Silicon binary plus README, USERGUIDE, and CHANGELOG.
|
||||
- `USERGUIDE.md`: standalone user guide.
|
||||
|
||||
Intel Macs are not currently a supported release target.
|
||||
|
||||
@@ -557,6 +557,24 @@ IMG_0001.HEIC -> IMG_0001.xmp
|
||||
|
||||
The XMP includes photoscli archive metadata such as asset ID, original filename, exported filename, album, manifest path, media type, dimensions, favorite state, hidden state, cloud state, export mode, version, exported time, size, creation date, modification date, duration, adjustment state, media subtypes, source type, playback style, burst data, GPS coordinates, adjustment info, structured asset resources, standard XMP date fields, EXIF GPS fields, favorite rating, and album/folder keywords when PhotoKit exposes them.
|
||||
|
||||
Control location privacy in generated sidecars:
|
||||
|
||||
```bash
|
||||
./bin/photoscli export --album-id "Vacation" --out ./Vacation --sidecar xmp --xmp-privacy strip-address
|
||||
./bin/photoscli export --album-id "Vacation" --out ./Vacation --sidecar xmp --xmp-privacy strip-location
|
||||
```
|
||||
|
||||
`keep` is the default. `strip-address` omits reverse-geocoded address fields while preserving GPS coordinates. `strip-location` omits both GPS coordinates and address fields.
|
||||
|
||||
Control generated keywords and ratings:
|
||||
|
||||
```bash
|
||||
./bin/photoscli export --album-id "Vacation" --out ./Vacation --sidecar xmp --xmp-keywords album
|
||||
./bin/photoscli export --album-id "Vacation" --out ./Vacation --sidecar xmp --xmp-keywords none --xmp-rating none
|
||||
```
|
||||
|
||||
`--xmp-keywords album-path` is the default and writes album/folder keywords. `album` writes only the album name. `none` omits generated `dc:subject` keywords. `--xmp-rating favorite` is the default and maps favorite assets to `xmp:Rating="5"`; `none` omits that generated rating.
|
||||
|
||||
For address metadata from GPS coordinates, opt in to Apple's reverse geocoder:
|
||||
|
||||
```bash
|
||||
@@ -573,6 +591,14 @@ Verify sidecars after an export:
|
||||
|
||||
This reports missing, zero-byte, unreadable, or asset-ID mismatched `.xmp` files.
|
||||
|
||||
Use strict verification for sidecars generated by recent photoscli versions:
|
||||
|
||||
```bash
|
||||
./bin/photoscli verify --out ./PhotosBackup --sidecar --strict
|
||||
```
|
||||
|
||||
Strict mode also checks photoscli schema metadata, generator metadata, and the exported filename recorded inside the sidecar.
|
||||
|
||||
Inspect one generated sidecar when troubleshooting or scripting:
|
||||
|
||||
```bash
|
||||
|
||||
+82
-7
@@ -42,6 +42,9 @@ type exportOptions struct {
|
||||
verify bool
|
||||
format string
|
||||
sidecar string
|
||||
xmpPrivacy string
|
||||
xmpKeywords string
|
||||
xmpRating string
|
||||
metadataOnly bool
|
||||
reverseGeocode bool
|
||||
minSize int64
|
||||
@@ -172,7 +175,8 @@ COMMANDS
|
||||
verify --out <dir> [--manifest jsonl|sqlite]
|
||||
Verify that manifest entries point to files that exist on disk. Missing
|
||||
files are printed as <asset-id><TAB><filename>. Exits 2 on missing files.
|
||||
Add --sidecar to verify expected XMP sidecars too.
|
||||
Add --sidecar to verify expected XMP sidecars too. Add --strict with
|
||||
--sidecar to require photoscli schema/generator and exported filename metadata.
|
||||
|
||||
retry-failed --out <dir>
|
||||
Retry assets previously written to failures.jsonl.
|
||||
@@ -228,6 +232,15 @@ COMMON EXPORT FLAGS
|
||||
Write opt-in XMP sidecar metadata next to each exported file. Default:
|
||||
none. If XMP writing fails, the asset is counted as failed.
|
||||
|
||||
--xmp-privacy keep|strip-location|strip-address
|
||||
Control location/address metadata in generated XMP sidecars. Default: keep.
|
||||
|
||||
--xmp-keywords album-path|album|none
|
||||
Control dc:subject keywords in generated XMP sidecars. Default: album-path.
|
||||
|
||||
--xmp-rating favorite|none
|
||||
Control favorite-to-rating mapping in generated XMP sidecars. Default: favorite.
|
||||
|
||||
--metadata-only
|
||||
With --sidecar xmp, write or refresh XMP sidecars for files already in
|
||||
the manifest without exporting media files. Requires a manifest.
|
||||
@@ -1090,17 +1103,48 @@ func writeSidecarIfNeeded(pa pendingAsset, result photos.ExportResult, originals
|
||||
if pa.asset.ModificationDate != nil {
|
||||
modifyDate = *pa.asset.ModificationDate
|
||||
}
|
||||
location := pa.asset.Location
|
||||
xmpPrivacy := opts.xmpPrivacy
|
||||
if xmpPrivacy == "" {
|
||||
xmpPrivacy = "keep"
|
||||
}
|
||||
xmpKeywords := opts.xmpKeywords
|
||||
if xmpKeywords == "" {
|
||||
xmpKeywords = "album-path"
|
||||
}
|
||||
xmpRating := opts.xmpRating
|
||||
if xmpRating == "" {
|
||||
xmpRating = "favorite"
|
||||
}
|
||||
var placemark *photos.Placemark
|
||||
if opts.reverseGeocode && pa.asset.Location != nil && cache != nil {
|
||||
if opts.reverseGeocode && location != nil && cache != nil && xmpPrivacy == "keep" {
|
||||
placemark = cache.lookup(pa.asset.Location.Latitude, pa.asset.Location.Longitude, bridge)
|
||||
}
|
||||
if xmpPrivacy == "strip-location" {
|
||||
location = nil
|
||||
placemark = nil
|
||||
}
|
||||
if xmpPrivacy == "strip-address" {
|
||||
placemark = nil
|
||||
}
|
||||
keywords := keywordsFromAlbumPath(pa.album, relDir)
|
||||
if xmpKeywords == "album" {
|
||||
keywords = keywordsFromAlbumPath(pa.album, "")
|
||||
}
|
||||
if xmpKeywords == "none" {
|
||||
keywords = nil
|
||||
}
|
||||
isFavorite := pa.asset.IsFavorite
|
||||
if xmpRating == "none" {
|
||||
isFavorite = false
|
||||
}
|
||||
return writeXMPSidecar(sidecarPath(fullPath), xmpSidecarData{
|
||||
AssetID: pa.asset.ID,
|
||||
OriginalFilename: pa.asset.Filename,
|
||||
ExportedFilename: result.Filename,
|
||||
Album: pa.album,
|
||||
AlbumPath: pa.path,
|
||||
Keywords: keywordsFromAlbumPath(pa.album, relDir),
|
||||
Keywords: keywords,
|
||||
ManifestPath: relPath,
|
||||
MediaType: pa.asset.MediaType,
|
||||
MediaSubtypes: pa.asset.MediaSubtypes,
|
||||
@@ -1109,7 +1153,7 @@ func writeSidecarIfNeeded(pa pendingAsset, result photos.ExportResult, originals
|
||||
PixelWidth: pa.asset.PixelWidth,
|
||||
PixelHeight: pa.asset.PixelHeight,
|
||||
Duration: pa.asset.Duration,
|
||||
IsFavorite: pa.asset.IsFavorite,
|
||||
IsFavorite: isFavorite,
|
||||
IsHidden: pa.asset.IsHidden,
|
||||
HasAdjustments: pa.asset.HasAdjustments,
|
||||
Cloud: result.Cloud,
|
||||
@@ -1119,7 +1163,7 @@ func writeSidecarIfNeeded(pa pendingAsset, result photos.ExportResult, originals
|
||||
Size: result.Size,
|
||||
CreateDate: createDate,
|
||||
ModifyDate: modifyDate,
|
||||
Location: pa.asset.Location,
|
||||
Location: location,
|
||||
Placemark: placemark,
|
||||
BurstIdentifier: pa.asset.BurstIdentifier,
|
||||
RepresentsBurst: pa.asset.RepresentsBurst,
|
||||
@@ -1874,6 +1918,9 @@ func parseExportOptions(args []string, stderr io.Writer) (exportOptions, bool) {
|
||||
verify: hasFlag(args, "--verify"),
|
||||
format: flagValWithDefault(args, "--format", "jpeg"),
|
||||
sidecar: flagValWithDefault(args, "--sidecar", "none"),
|
||||
xmpPrivacy: flagValWithDefault(args, "--xmp-privacy", "keep"),
|
||||
xmpKeywords: flagValWithDefault(args, "--xmp-keywords", "album-path"),
|
||||
xmpRating: flagValWithDefault(args, "--xmp-rating", "favorite"),
|
||||
metadataOnly: hasFlag(args, "--metadata-only"),
|
||||
reverseGeocode: hasFlag(args, "--reverse-geocode"),
|
||||
dateTemplate: flagVal(args, "--date-template"),
|
||||
@@ -1890,6 +1937,18 @@ func parseExportOptions(args []string, stderr io.Writer) (exportOptions, bool) {
|
||||
fmt.Fprintf(stderr, "error: --sidecar must be none or xmp, got %q\n", opts.sidecar)
|
||||
return opts, false
|
||||
}
|
||||
if opts.xmpPrivacy != "keep" && opts.xmpPrivacy != "strip-location" && opts.xmpPrivacy != "strip-address" {
|
||||
fmt.Fprintf(stderr, "error: --xmp-privacy must be keep, strip-location, or strip-address, got %q\n", opts.xmpPrivacy)
|
||||
return opts, false
|
||||
}
|
||||
if opts.xmpKeywords != "album-path" && opts.xmpKeywords != "album" && opts.xmpKeywords != "none" {
|
||||
fmt.Fprintf(stderr, "error: --xmp-keywords must be album-path, album, or none, got %q\n", opts.xmpKeywords)
|
||||
return opts, false
|
||||
}
|
||||
if opts.xmpRating != "favorite" && opts.xmpRating != "none" {
|
||||
fmt.Fprintf(stderr, "error: --xmp-rating must be favorite or none, got %q\n", opts.xmpRating)
|
||||
return opts, false
|
||||
}
|
||||
if opts.metadataOnly && opts.sidecar != "xmp" {
|
||||
fmt.Fprintln(stderr, "error: --metadata-only requires --sidecar xmp")
|
||||
return opts, false
|
||||
@@ -2117,6 +2176,7 @@ func cmdDiff(args []string, stdout, stderr io.Writer, bridge photos.Bridge) int
|
||||
func cmdVerify(args []string, stdout, stderr io.Writer) int {
|
||||
outDir := flagVal(args, "--out")
|
||||
checkSidecar := hasFlag(args, "--sidecar")
|
||||
strictSidecar := hasFlag(args, "--strict")
|
||||
if outDir == "" {
|
||||
fmt.Fprintln(stderr, "error: --out is required")
|
||||
return exitErr
|
||||
@@ -2156,7 +2216,7 @@ func cmdVerify(args []string, stdout, stderr io.Writer) int {
|
||||
fmt.Fprintf(stdout, "%s\t%s\tsize-mismatch\tmanifest=%d\tdisk=%d\n", id, checkPath, e.Size, info.Size())
|
||||
}
|
||||
if checkSidecar {
|
||||
bad += verifySidecar(stdout, outDir, id, checkPath)
|
||||
bad += verifySidecar(stdout, outDir, id, checkPath, strictSidecar)
|
||||
}
|
||||
}
|
||||
if bad > 0 {
|
||||
@@ -2166,7 +2226,7 @@ func cmdVerify(args []string, stdout, stderr io.Writer) int {
|
||||
return exitOK
|
||||
}
|
||||
|
||||
func verifySidecar(stdout io.Writer, outDir, id, checkPath string) int {
|
||||
func verifySidecar(stdout io.Writer, outDir, id, checkPath string, strict bool) int {
|
||||
xmpPath := sidecarPath(filepath.Join(outDir, checkPath))
|
||||
rel, err := filepath.Rel(outDir, xmpPath)
|
||||
if err != nil || strings.HasPrefix(rel, "..") {
|
||||
@@ -2191,6 +2251,21 @@ func verifySidecar(stdout io.Writer, outDir, id, checkPath string) int {
|
||||
fmt.Fprintf(stdout, "%s\t%s\tsidecar-asset-mismatch\n", id, rel)
|
||||
return 1
|
||||
}
|
||||
if strict {
|
||||
meta := inspectXMP(data)
|
||||
if meta["xmpSchemaVersion"] != "2" {
|
||||
fmt.Fprintf(stdout, "%s\t%s\tsidecar-schema-missing\n", id, rel)
|
||||
return 1
|
||||
}
|
||||
if meta["sidecarGenerator"] == "" {
|
||||
fmt.Fprintf(stdout, "%s\t%s\tsidecar-generator-missing\n", id, rel)
|
||||
return 1
|
||||
}
|
||||
if meta["exportedFilename"] != filepath.Base(checkPath) {
|
||||
fmt.Fprintf(stdout, "%s\t%s\tsidecar-filename-mismatch\n", id, rel)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
+131
-1
@@ -4217,12 +4217,48 @@ func TestVerifySidecarBranches(t *testing.T) {
|
||||
oldRead := readFileFunc
|
||||
readFileFunc = func(string) ([]byte, error) { return nil, fmt.Errorf("read") }
|
||||
var out bytes.Buffer
|
||||
if got := verifySidecar(&out, subdir, "x1", "../asset.jpg"); got != 1 || !strings.Contains(out.String(), "sidecar-unreadable") {
|
||||
if got := verifySidecar(&out, subdir, "x1", "../asset.jpg", false); got != 1 || !strings.Contains(out.String(), "sidecar-unreadable") {
|
||||
t.Fatalf("expected unreadable with rel fallback, got=%d out=%q", got, out.String())
|
||||
}
|
||||
readFileFunc = oldRead
|
||||
}
|
||||
|
||||
func TestVerifySidecarStrict(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
media := filepath.Join(dir, "photo.jpg")
|
||||
if err := os.WriteFile(media, []byte("data"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeXMPSidecar(sidecarPath(media), xmpSidecarData{AssetID: "x1", ExportedFilename: "photo.jpg"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out bytes.Buffer
|
||||
if got := verifySidecar(&out, dir, "x1", "photo.jpg", true); got != 0 || out.Len() != 0 {
|
||||
t.Fatalf("strict valid got=%d out=%q", got, out.String())
|
||||
}
|
||||
if err := os.WriteFile(sidecarPath(media), []byte(`photoscli:assetID="x1"`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out.Reset()
|
||||
if got := verifySidecar(&out, dir, "x1", "photo.jpg", true); got != 1 || !strings.Contains(out.String(), "sidecar-schema-missing") {
|
||||
t.Fatalf("strict schema got=%d out=%q", got, out.String())
|
||||
}
|
||||
if err := os.WriteFile(sidecarPath(media), []byte(`<?xpacket begin=""?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description xmlns:photoscli="https://gitea.k3s.k0.nu/tools/photocli/ns/1.0/" photoscli:assetID="x1" photoscli:xmpSchemaVersion="2" photoscli:exportedFilename="photo.jpg" /></rdf:RDF></x:xmpmeta>`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out.Reset()
|
||||
if got := verifySidecar(&out, dir, "x1", "photo.jpg", true); got != 1 || !strings.Contains(out.String(), "sidecar-generator-missing") {
|
||||
t.Fatalf("strict generator got=%d out=%q", got, out.String())
|
||||
}
|
||||
if err := os.WriteFile(sidecarPath(media), []byte(`<?xpacket begin=""?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description xmlns:photoscli="https://gitea.k3s.k0.nu/tools/photocli/ns/1.0/" photoscli:assetID="x1" photoscli:xmpSchemaVersion="2" photoscli:sidecarGenerator="photoscli" photoscli:exportedFilename="other.jpg" /></rdf:RDF></x:xmpmeta>`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out.Reset()
|
||||
if got := verifySidecar(&out, dir, "x1", "photo.jpg", true); got != 1 || !strings.Contains(out.String(), "sidecar-filename-mismatch") {
|
||||
t.Fatalf("strict filename got=%d out=%q", got, out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSidecarInspect(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "photo.xmp")
|
||||
@@ -4548,6 +4584,18 @@ func TestSidecarConfigAndErrors(t *testing.T) {
|
||||
if _, ok := parseExportOptions([]string{"--sidecar", "bad"}, &stderr); ok || !strings.Contains(stderr.String(), "--sidecar") {
|
||||
t.Fatalf("expected sidecar validation error, stderr=%q", stderr.String())
|
||||
}
|
||||
stderr.Reset()
|
||||
if _, ok := parseExportOptions([]string{"--xmp-privacy", "bad"}, &stderr); ok || !strings.Contains(stderr.String(), "--xmp-privacy") {
|
||||
t.Fatalf("expected xmp privacy validation error, stderr=%q", stderr.String())
|
||||
}
|
||||
stderr.Reset()
|
||||
if _, ok := parseExportOptions([]string{"--xmp-keywords", "bad"}, &stderr); ok || !strings.Contains(stderr.String(), "--xmp-keywords") {
|
||||
t.Fatalf("expected xmp keywords validation error, stderr=%q", stderr.String())
|
||||
}
|
||||
stderr.Reset()
|
||||
if _, ok := parseExportOptions([]string{"--xmp-rating", "bad"}, &stderr); ok || !strings.Contains(stderr.String(), "--xmp-rating") {
|
||||
t.Fatalf("expected xmp rating validation error, stderr=%q", stderr.String())
|
||||
}
|
||||
|
||||
b := &mockBridge{assets: []photos.Asset{{ID: "x1", Filename: "photo.jpg"}}}
|
||||
b.exportPreviewFn = func(assetID, out string, targetSize, quality, index int) (photos.ExportResult, error) {
|
||||
@@ -4562,6 +4610,88 @@ func TestSidecarConfigAndErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMPSidecarPrivacy(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
asset := photos.Asset{ID: "x1", Filename: "geo.jpg", Location: &photos.AssetLocation{Latitude: 59.3293, Longitude: 18.0686}}
|
||||
bridge := &mockBridge{}
|
||||
bridge.reverseGeocodeFn = func(float64, float64) (photos.Placemark, error) {
|
||||
return photos.Placemark{Country: "Sweden", Locality: "Stockholm"}, nil
|
||||
}
|
||||
pa := pendingAsset{asset: asset, root: dir, path: dir, album: "Album"}
|
||||
for _, tc := range []struct {
|
||||
privacy string
|
||||
wantGPS bool
|
||||
wantAddress bool
|
||||
}{
|
||||
{privacy: "keep", wantGPS: true, wantAddress: true},
|
||||
{privacy: "strip-address", wantGPS: true, wantAddress: false},
|
||||
{privacy: "strip-location", wantGPS: false, wantAddress: false},
|
||||
} {
|
||||
path := filepath.Join(dir, tc.privacy+".jpg")
|
||||
if err := os.WriteFile(path, []byte("data"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeSidecarIfNeeded(pa, photos.ExportResult{Filename: filepath.Base(path), Size: 4}, false, exportOptions{sidecar: "xmp", reverseGeocode: true, xmpPrivacy: tc.privacy}, newGeocodeCache(dir), bridge); err != nil {
|
||||
t.Fatalf("%s write sidecar: %v", tc.privacy, err)
|
||||
}
|
||||
data, err := os.ReadFile(sidecarPath(path))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := string(data)
|
||||
if strings.Contains(content, "photoscli:latitude") != tc.wantGPS {
|
||||
t.Fatalf("%s GPS presence mismatch in %s", tc.privacy, content)
|
||||
}
|
||||
if strings.Contains(content, "photoscli:addressCountry") != tc.wantAddress {
|
||||
t.Fatalf("%s address presence mismatch in %s", tc.privacy, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMPSidecarKeywordAndRatingOptions(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
asset := photos.Asset{ID: "x1", Filename: "photo.jpg", IsFavorite: true}
|
||||
pa := pendingAsset{asset: asset, root: dir, path: filepath.Join(dir, "Trips", "Beach"), album: "Beach"}
|
||||
if err := os.MkdirAll(pa.path, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
keywords string
|
||||
rating string
|
||||
wantTrip bool
|
||||
wantBeach bool
|
||||
wantRate bool
|
||||
}{
|
||||
{name: "default", wantTrip: true, wantBeach: true, wantRate: true},
|
||||
{name: "album", keywords: "album", wantTrip: false, wantBeach: true, wantRate: true},
|
||||
{name: "none", keywords: "none", rating: "none", wantTrip: false, wantBeach: false, wantRate: false},
|
||||
} {
|
||||
filename := tc.name + ".jpg"
|
||||
path := filepath.Join(pa.path, filename)
|
||||
if err := os.WriteFile(path, []byte("data"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeSidecarIfNeeded(pa, photos.ExportResult{Filename: filename, Size: 4}, false, exportOptions{sidecar: "xmp", xmpKeywords: tc.keywords, xmpRating: tc.rating}, nil, &mockBridge{}); err != nil {
|
||||
t.Fatalf("%s write sidecar: %v", tc.name, err)
|
||||
}
|
||||
data, err := os.ReadFile(sidecarPath(path))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := string(data)
|
||||
if strings.Contains(content, "<rdf:li>Trips</rdf:li>") != tc.wantTrip {
|
||||
t.Fatalf("%s Trips keyword mismatch in %s", tc.name, content)
|
||||
}
|
||||
if strings.Contains(content, "<rdf:li>Beach</rdf:li>") != tc.wantBeach {
|
||||
t.Fatalf("%s Beach keyword mismatch in %s", tc.name, content)
|
||||
}
|
||||
if strings.Contains(content, "xmp:Rating=\"5\"") != tc.wantRate {
|
||||
t.Fatalf("%s rating mismatch in %s", tc.name, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataOnlyExport(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m := manifest.LoadJSONL(dir)
|
||||
|
||||
Reference in New Issue
Block a user