github Xentraxx/GooglePhotosTakeoutHelper_Neo v6.3.0

4 hours ago

🐛 Bug Fixes

  • still mode no longer double-moves a sidecar claimed by a .MP entity (race: PathNotFoundException + "Primary file is still unaccounted for") — In PixelMpTransformService._transformPixelPrimariesToStill, when a .MP entity was redirected to a sidecar (e.g. photo.MPphoto.MP.jpg), _removeEntityDuplicates() was supposed to drop the separate entity that owned that sidecar so only one entity would move it. But the owner-exclusion guard used value equality (e == owner), and MediaEntity.== keys on sourcePath/targetPath/isShortcut. Because the owner's primaryFile.sourcePath had already been mutated to the sidecar path before the dedup call, the owner and the genuine sidecar entity now compared equal under ==, so the guard if (e == owner) return false; wrongly skipped the duplicate. Two entities therefore remained pointing at the same file; the parallel mover raced — one move succeeded, the other threw PathNotFoundException (source already gone, errno 2 / "The system cannot find the file specified") and the post-finalize integrity check flagged it as "Primary file is still unaccounted for after strategy finalization". This was reproducible cross-platform (seen on macOS /var/folders/... and Windows %TEMP%..., notably in the sidecar_priority test). The owner-exclusion check now uses identity (identical(e, owner)) in both the primary-duplicate scan and the secondary-strip loop, which correctly excludes the exact owner object regardless of its mutated sourcePath. The fix is platform-agnostic.
  • --transform-pixel-mp jpg now produces a valid Google Motion Photo V2 container — Previously LivePhotoCreatorService._createLivePhotoContainer() concatenated the JPEG and MP4 bytes with no XMP offset marker, so while the file played as a plain JPEG, other apps (Google Photos, Android gallery) could not locate the embedded video and treated it as a static image. The container builder now strips any stale motion-photo XMP from the still image (via MotionPhotoExtractorService.stripMotionPhotoXmp), injects a fresh XMP APP1 segment immediately after the JPEG SOI marker declaring GCamera:MotionPhoto="1" and GCamera:MicroVideoOffset (the byte count from the end of the file to the appended MP4 — the exact semantics the existing _parseGoogleMotionPhotoV2 reader consumes), and then appends the MP4 verbatim. This mirrors what Google Photos natively exports and round-trips through GPTH's own MotionPhotos.isMotionPhoto() detector. The interactive prompt's "experimental / known issues" warning for the jpg mode has been softened accordingly.
  • still mode sidecar selection now prefers .jpg over .jpeg_findPreferredStillImagePath() previously picked the largest file among all sidecar candidates, which could select a larger .jpeg duplicate over the real .jpg still (e.g. a 150 KB photo.MP.jpeg duplicate wrongly beat a 100 KB photo.MP.jpg actual still). Candidates are now grouped into priority tiers by extension (.jpg/.JPG before .jpeg/.JPEG); the first tier containing an existing file wins, and size is only used as a tiebreaker within a tier.

🛠️ Maintenance

  • Regression test for issue #120 — An e2e test (test/e2e/e2e_missing_coverage_test.dart) reproduces the issue #120 input layout (a Pixel .MP file with its .MP.jpg sidecar) under --transform-pixel-mp mp4 and asserts the output contains exactly one .mp4, the .MP.jpg sidecar, and no stray (1).jpg collision file. The double-move that produced the stray file (the sidecar being moved once as a secondary of the .MP entity and again as its own primary) was already prevented by _removeEntityDuplicates() in PixelMpTransformService; this test guards against its return.
  • removed some dead code
  • fixed comments where the comments did not reflect the real behaviour

New Features

  • Local timezone conversion for Google Photos re-upload (--local-timezone) — Google Photos Takeout stores photo capture times as UTC in the photoTakenTime.timestamp field. GPTH wrote this UTC clock to the EXIF DateTimeOriginal tag together with OffsetTime=+00:00. However, Google Photos ignores the OffsetTime tag when re-uploading and interprets the naive DateTimeOriginal clock as local time, so photos appear hours off (e.g. 8 hours early for a GMT+8 user — issue #145). The new --local-timezone flag (e.g. --local-timezone +08:00) and interactive prompt convert UTC photo dates to the user's local timezone: the local clock (UTC instant + offset) is written to the EXIF date tags together with the correct OffsetTime (e.g. +08:00), and date-based output folders use the local date, so the re-uploaded timeline matches the original. File timestamps (Step 8) are intentionally left as the true UTC instant — the OS already displays them as local time on the user's own machine. The flag accepts ±HH:MM (e.g. +08:00, -05:30), ±H (e.g. -5), HHMM (e.g. +0530), and Z/UTC aliases, and is also available as an interactive prompt. When not set, the previous UTC behaviour is preserved (backward compatible).
  • Custom date folder format for --divide-to-dates (issue #142) — Previously --divide-to-dates only accepted the four presets 0 (one folder), 1 (year), 2 (year/month), 3 (year/month/day). You can now pass an arbitrary folder-structure template using the tokens yyyy/yy/mm/dd, e.g. --divide-to-dates "yyyy/yyyy-mm" produces 2026/2026-08/ (two folder levels). / separates folder levels; any other characters (-, _, ., …) are kept as literal text within a folder name. The legacy 0-3 presets continue to work unchanged (backward compatible). The interactive mode gained a new [4] - custom format menu option with a validation loop. Albums remain flattened regardless of the chosen format. A null date still falls back to the date-unknown folder.

Full Changelog: v6.2.1...v6.3.0

Don't miss a new GooglePhotosTakeoutHelper_Neo release

NewReleases is sending notifications on new releases.