🐛 Bug Fixes
stillmode no longer double-moves a sidecar claimed by a.MPentity (race:PathNotFoundException+ "Primary file is still unaccounted for") — InPixelMpTransformService._transformPixelPrimariesToStill, when a.MPentity was redirected to a sidecar (e.g.photo.MP→photo.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), andMediaEntity.==keys onsourcePath/targetPath/isShortcut. Because the owner'sprimaryFile.sourcePathhad already been mutated to the sidecar path before the dedup call, the owner and the genuine sidecar entity now compared equal under==, so the guardif (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 threwPathNotFoundException(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 thesidecar_prioritytest). 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 mutatedsourcePath. The fix is platform-agnostic.--transform-pixel-mp jpgnow produces a valid Google Motion Photo V2 container — PreviouslyLivePhotoCreatorService._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 (viaMotionPhotoExtractorService.stripMotionPhotoXmp), injects a fresh XMP APP1 segment immediately after the JPEG SOI marker declaringGCamera:MotionPhoto="1"andGCamera:MicroVideoOffset(the byte count from the end of the file to the appended MP4 — the exact semantics the existing_parseGoogleMotionPhotoV2reader consumes), and then appends the MP4 verbatim. This mirrors what Google Photos natively exports and round-trips through GPTH's ownMotionPhotos.isMotionPhoto()detector. The interactive prompt's "experimental / known issues" warning for thejpgmode has been softened accordingly.stillmode sidecar selection now prefers.jpgover.jpeg—_findPreferredStillImagePath()previously picked the largest file among all sidecar candidates, which could select a larger.jpegduplicate over the real.jpgstill (e.g. a 150 KBphoto.MP.jpegduplicate wrongly beat a 100 KBphoto.MP.jpgactual still). Candidates are now grouped into priority tiers by extension (.jpg/.JPGbefore.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.MPfile with its.MP.jpgsidecar) under--transform-pixel-mp mp4and asserts the output contains exactly one.mp4, the.MP.jpgsidecar, and no stray(1).jpgcollision file. The double-move that produced the stray file (the sidecar being moved once as a secondary of the.MPentity and again as its own primary) was already prevented by_removeEntityDuplicates()inPixelMpTransformService; 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 thephotoTakenTime.timestampfield. GPTH wrote this UTC clock to the EXIFDateTimeOriginaltag together withOffsetTime=+00:00. However, Google Photos ignores theOffsetTimetag when re-uploading and interprets the naiveDateTimeOriginalclock as local time, so photos appear hours off (e.g. 8 hours early for a GMT+8 user — issue #145). The new--local-timezoneflag (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 correctOffsetTime(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), andZ/UTCaliases, 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-datesonly accepted the four presets0(one folder),1(year),2(year/month),3(year/month/day). You can now pass an arbitrary folder-structure template using the tokensyyyy/yy/mm/dd, e.g.--divide-to-dates "yyyy/yyyy-mm"produces2026/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 formatmenu option with a validation loop. Albums remain flattened regardless of the chosen format. A null date still falls back to thedate-unknownfolder.
Full Changelog: v6.2.1...v6.3.0