Repository navigation
ποΈ CHANGELOG
Planned Roadmap for the following releases
Changelog for the past releases
Release: v4.7.0
Release Date: 2026-08-27
π Enhancements:
- Hardened Google Takeout orphan album-sidecar recovery. Before GPTH removes source JSON files, PhotoMigrator captures their album, exact filename,
photoTakenTime, andcreationTime; recovery then runs only after post-processing has created the configured non-flatALL_PHOTOSlayout. It resolves an exact filename only inside the expectedyear,year/month, oryear-monthfolder, triesphotoTakenTimefirst andcreationTimeonly when needed, and leaves unresolved matches untouched.flattenlayouts skip recovery because same-name assets cannot be safely disambiguated. The processor writesorphan_album_asset_recovery_manifest.jsonwith the source sidecar, result, and candidates for later audit. Existing valid album shortcuts are treated as resolved without being counted as orphans or generating year-mismatch warnings; only actual missing entries and shortcut-strategy inconsistencies, such as a physical copy or an external/broken link where a shortcut is expected, require review. - Moved Automatic Migration confirmation ahead of source detection, ZIP extraction, destination authentication, and temporary-folder preparation. Cancelling now exits before any long-running preprocessing begins; the Google Takeout and iCloud Takeout processors already confirm before their processing and extraction phases.
- Made manual Immich duplicate location merging opt-in with
-mergeDupLoc, --merge-duplicate-locations(default:false). The guarded merge now preserves the keeper GPS by default; when explicitly enabled, it copies a location only if the keeper has no coordinates and at least two redundant assets agree on one complete coordinate pair. Native Immich duplicate deletion remains server-managed. - Improved Google Takeout processing-time summaries. Step labels now use the longest terminal display width, including emoji presentation width, as their shared width, keeping the duration separator and values aligned when optional steps have long names.
- Hardened Google Takeout shortcut-album recovery for media GPTH leaves as physical copies. After the final non-flat
ALL_PHOTOSlayout is created, a physical album entry is replaced with the platform-native shortcut only when its JSON sidecar resolves one exact canonical asset in the configured date folder and both files match byte-for-byte. POSIX links are relative; Windows uses hard links. Different or ambiguous files are preserved and reported for review with the album, filename, shortcut strategy, and decision. - Optimized Google Takeout album-entry reconciliation. Step 5.5, now named
Reconcile Album Entries, indexes an album once while processing all of its sidecars and updates that index in memory after creating entries, avoiding repeated recursive scans of the same album without weakening filename, date-folder, or byte-for-byte validation. - Clarified Google Takeout Step 5.5 reconciliation reporting. Successful physical-copy-to-shortcut replacements are now
INFOrather than review warnings; repeated sidecars no longer repeat the same physical-entry warning; affected albums each emit one compact counter line; and the phase ends with a global, line-by-line summary separating evaluated sidecars, existing mappings, missing entries, recovered entries, repaired shortcuts, retained review items, unresolved entries, and affected albums. - Moved the visible Google Takeout output analysis after post-processing. Post-processing is now Phase 5, final disk analysis is Phase 6, and final cleanup remains Phase 7. The post-process date index is prepared before any moves and reused by Phase 6, which rescans the final filesystem for accurate physical-file, symlink, folder, and size totals without repeating the full metadata extraction. The help now notes that file explorers may omit symlinks from their file count.
- Updated GPTH to v6.3.0 which includes new enhancements.
π GPTH Enhancements:
β¨ GPTH 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.
π GPTH Bug Fixes
.cover(album cover) and.mp~<digits>(edited alternate) files are now recognized as Pixel motion photos β Both are MP4-container motion-photo files that Google Photos exports with non-standard extensions:.coverfor album cover images and.mp~2(.mp~1,.mp~3, β¦) for edited alternate versions of.mpfiles. Neither was recognized bydart:mimeor the hardcodedMediaExtensions.additionallist, sowherePhotoVideo()silently dropped them in Step 1 & Step 2 β they were never discovered, never extension-fixed, and never transformed. A new centralizedMediaExtensions.isMotionPhotoExtension()helper (the single source of truth for motion-photo extension recognition) now matches.mp,.mv,.cover, and the.mp~<digits>regex family, and all hardcoded== '.mp' || == '.mv'/endsWith('.mp')checks in the discovery gates (Step 1 & Step 2), the Step 1 skip guard, and the Step 6 transform dispatcher (mp4/jpg/still modes + secondary extension update) delegate to it. Files keep their original names (no input mutation, no collision risk whenvideo.mp+video.mp~2coexist), so JSON sidecar name-matching stays intact. A related fix in the Step 4 truncated-filename fixer ensures the title extension-stripping regex also handles.mp~<digits>(the~is not alphanumeric, so the old regex did not strip it, causing a false truncation match that doubled the extension). This resolves issue #138.- GPS coordinates and DateTime from an unrelated photo are no longer written to a file's EXIF β When a media file's own JSON sidecar was missing, the matcher fell back to aggressive strategies (
-editedsuffix removal, cross-extension MP4βHEIC/JPG pairing, and the numbered cross-extension tier) that can match a different photo's sidecar. Because date and GPS were read from that one (wrong) matched sidecar together, the wrong photo's location β differing by tens to hundreds of kilometers β and its timestamp ended up in the output EXIF. This was the issue #139 symptom (~34.5% of GPS-tagged outputs had the wrong location, and a cluster of videos were mis-dated with another real video's timestamp). The matcher now exposes a confidence flag (isOwnSidecar) distinguishing a match that names this file (exact name, filesystem-truncated, Takeout bracket-swap, Google-added extension, Pixel.MPβ.MP.jpg, same-file numbered forms) from a heuristic match that can point at a different photo. Date and GPS are now only trusted from an own-sidecar match; a heuristic match yields neither, so the file falls through to EXIF / guess / folder-year instead of inheriting a sibling's metadata. A related photo's date is not an acceptable heuristic here, so both fields are gated (not just GPS). This resolves issue #139. - Issue #133 album recovery is preserved β The cross-extension / numbered matching tiers that #133's orphan-recovery work added (and which #139's fix gates for date/GPS) are not used by the album-recovery path itself. Album association recovery reads sidecar
title/photoTakenTimeviagetMediaNameCandidatesForJsonNamein Step 2 and attaches membership as metadata; it does not callfindJsonForFile, so the new confidence gate does not affect it. The matcher still finds cross-photo sidecars (for Step 1 extension fixing and album recovery) β it only refuses to use their date/GPS for EXIF writing. E2E tests confirm every album mode still materializes recovered orphan memberships on disk. - DateTime EXIF data is no longer overwritten from a different video in batched writes β When Step 7 flushed several files needing EXIF writes in a single ExifTool batch, the batch args were built as
commonWriteArgs() + [tagsβ, fileβ, tagsβ, fileβ, β¦]on the assumption that each file's-Tag=Valueblock applied only to the filename immediately following it. ExifTool does not work that way: in argv, argfile (-@), and stay-open modes, all-Tag=Valueassignments accumulate and the final set is applied to every file in the invocation. So the last file's tags overwrote every earlier file's tags β the exact issue #140 symptom, whereIMG_5948.MOVended up with its siblingIMG_9304.MOV's date in its XMP tags (DateTimeOriginal,DateTimeDigitized,ModifyDate) whileQuickTime:CreateDatesurvived (written via a different path) and GPS stayed correct (ruling out the #139 cross-photo JSON-matching bug, which would have corrupted GPS too; the per-entitydateTakeninprogress.jsonwas already correct).ExifToolService.writeExifDataBatchnow isolates each file by routing the batch through per-filewriteExifDataSinglecalls (each its own stay-open-executeround-trip, so there is no Perl-startup cost per file), andwriteExifDataBatchViaArgFiledelegates to it (the temp-argfile fallback, which had the same global-assignment problem, was removed). Step 7 also snapshots each file's tag map (Map<String, dynamic>.from(tagsToWrite)) when queueing, so later in-place mutation in the InteropIFD retry path (_stripOffsetTags/_retagEntryToXmpIfJpeg) cannot alter a sibling's queued tags. This resolves issue #140. - Shortcut mode no longer retains physical album videos β In
--albums=shortcutmode,.MP4and.MOVcompanion files (and other non-image media) were left as regular files in theAlbums/folder instead of being replaced by symlinks to the canonical file inALL_PHOTOS. The root cause was the shortcut reuse check: it treated any existing filesystem entry at the target name (file, link, or directory) as a reusable shortcut, so a stale physical file from a previous or interrupted run was silently kept instead of being replaced by a link. Shortcut reuse now only accepts actual symbolic links (Link.exists()), never regular files. When a regular file is found at the intended shortcut location, identical duplicates (same content hash as the canonical ALL_PHOTOS file) are removed, and distinct files are preserved under aShortcut Conflicts/subfolder before the expected link is created β soAlbums/contains only shortcuts in shortcut mode. A newShortcutIntegrityServiceruns after the EXIF write phase (Step 7) to verify and restore any album shortcuts that later stages may have broken or turned back into regular files: missing links are recreated from the entity's canonical output file, broken links are deleted and relinked, and unexpected regular files are moved toShortcut Conflicts/before the link is restored. This resolves issue #143. - Step 8 no longer fails withCreateFile failed ... (error=123)on every file β On Windows, Step 8 wraps each output path with the\\?\extended-length prefix via_toExtendedLengthPathbefore callingCreateFileto set the creation/write timestamps. That prefix disables Win32 path normalization, which means forward slashes (/) are not translated to backslashes and are treated as illegal characters, causingCreateFileto fail withERROR_INVALID_NAME(error 123) for every file. Forward slashes were leaking in from album-path construction inmoving_strategy_utils.dart(e.g.Untitled Albums/Unbenannt(109)/...), which joins path segments with/; those paths worked fine in Step 6 (where Dart'sFileAPI normalizes them) but broke once Step 8 applied the\\?\prefix on top._toExtendedLengthPathnow normalizes all/to\before applying the prefix, so the extended-length path is well-formed regardless of which upstream code produced the path. The fix is a no-op on POSIX (the function returns early). - Step 6 no longer logs a spurious "Could not verify existing album entry" warning during parallel moves β In
--albums=shortcutmode,moveMediaEntitiesParallelprocesses multiple media entities concurrently through a shared pool. When two entities shared the same album and the same desired shortcut basename (e.g. two distinct photos both namedphoto.jpgin albumTrip), and a previous or interrupted run had left a stale physical file at the intended shortcut location, both entities enteredclearRegularCandidateForShortcutand sawcandidate.exists() == true. The first entity moved the stale file toShortcut Conflicts/(or deleted it if identical), but the second entity's subsequentrename/deletethen threwPathNotFoundException(errno 2 β "The system cannot find the file specified") because the candidate was already gone. That exception was caught by the generic handler and logged as a[WARNING] Could not verify existing album entry before creating shortcuteven though the album location was now correctly clear.clearRegularCandidateForShortcutnow has a dedicatedon PathNotFoundExceptionclause that silently returns, since the candidate being absent is the desired outcome β the shortcut is then created normally bycreateSymlinkWithPreferredName(which handles name collisions via_resolveUniqueBasename). A regression test (test/integration/media_entity_moving_strategies_test.dart) reproduces the race with two parallel entities targeting the same album shortcut location and asserts no failures and no stale physical file remains. 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.
β‘ GPTH Performance
- Removed the
Pool(1)mutex fromMediaHashServicecache access β Every LRU cache read and write inMediaHashServicewas serialized through a single-resourcePool(1)lock, adding a pool-scheduling hop to every hash lookup and store. Dart's cooperative async model is single-threaded per isolate, so map access betweenawaitpoints is already atomic β the lock provided no safety benefit and was a serialization point across all concurrent hash operations in Step 3 (merge/duplicate detection). Removing it eliminates the scheduling overhead for everycalculateFileHash,calculateHashAndSize, and cache lookup call. For a 50,000-file takeout this removes ~100,000 pool scheduling cycles during Step 3. - Eliminated double directory traversal in Step 2 discovery β The discovery step traversed every year and album directory twice: once to count media files (for the progress bar total) and again to actually collect them. Both passes performed the same recursive
directory.list()+ 512-byte header read + MIME detection per file. The count pass has been removed entirely; files are now collected in a single traversal per directory, and the progress bar total is derived from the collected list. The bar now advances during the partner-sharing JSON batch phase (the slower part of discovery) instead of during file collection. For a 50,000-file takeout this eliminates ~50,000 redundant file header reads and halves the number of recursive directory listings. - JSON sidecar path and content caching across Steps 2 and 4 β Every media file's JSON sidecar was previously located and parsed twice: once in Step 2 (partner-sharing detection) and again in Step 4 (date + GPS extraction). Each lookup involved 5β10+
File.exists()calls trying different naming patterns (supplemental-metadata, truncated variants, numbered forms, etc.), and each parse read and decoded the full JSON file. Step 2 now caches the resolved sidecar path and theisOwnSidecarconfidence flag directly on theFileEntity, and Step 4 reuses both β skipping the expensivefindJsonForFileWithConfidencelookup entirely. A new LRU cache (50,000 entries) inJsonMetadataMatcherServicestores parsed JSON content keyed by sidecar path, so each sidecar file is read and decoded exactly once across both steps. For a 50,000-file takeout this eliminates ~100,000 redundant filesystem stat calls and ~50,000 duplicate JSON reads. TheisOwnSidecarflag is preserved throughMediaEntitynormalization (which creates newFileEntityinstances during ranking), ensuring the issue #139 cross-photo GPS guard remains intact. - Parallelized Step 8 creation-time updates β File creation timestamps were updated one file at a time in a sequential loop, each call opening a Win32 handle (
CreateFile+SetFileTime+CloseHandle) or POSIXutimensatcall. These operations are independent and are now batched withFuture.waitusing the fileIO concurrency limit. For a 50,000-file takeout this turns the final step from fully single-threaded to parallel, significantly reducing wall-clock time on Windows. - Extension-first filter in
wherePhotoVideo()β The discovery filter calledlookupMimeType()for every file during recursive directory listing, including non-media files (JSON sidecars, etc.). The extension check (MediaExtensions.additionalset lookup +isMotionPhotoExtension) is now performed first; only files that don't match the special-extension fast path fall through to the more expensivelookupMimeTypecall. For a 100,000-file takeout this avoids ~100,000 unnecessary MIME map lookups during discovery. The fast path only matches extensions thatlookupMimeTypedoes not recognize (.mp,.mv,.dng,.cr2,.cover,.mp~N), so standard extensions like.jpg/.png/.mp4still go throughlookupMimeTypeexactly as before β Step 1's content-based extension fixing (which reads file headers, not the filter's MIME result) is unaffected. - Eliminated redundant
stat()+length()inMediaHashService.calculateFileHashβ The hash calculation calledfile.stat()to generate the cache key, thenfile.length()separately to get the file size β two syscalls per file. The size is now reused from the already-fetchedFileStat, eliminating ~50,000 redundant syscalls during Step 3 for a 50,000-file takeout. - Partial JPEG read for EXIF date-tag check in Step 7 β
hasExistingExifDateTagloaded the entire JPEG into memory (file.readAsBytes()) just to check whether EXIF date tags exist. EXIF data is always near the start of a JPEG (the APP1 marker follows SOI + JFIF), so only the first 64KB is now read (file.openRead(0, 65536)). For a 10MB JPEG this reduces memory allocation from 10MB to 64KB per file.
π οΈ GPTH Maintenance
- Regression test suites for issue #138 at three levels β Unit tests (
test/unit/motion_photo_extension_recognition_test.dart) coverMediaExtensions.isMotionPhotoExtension()across recognized extensions (.mp,.mv,.cover,.mp~1/.mp~2/.mp~12, mixed case, full paths) and rejections (.jpg,.mp4,.mpx, no extension,.mp~without digits,.mp~abc,.cover.jpg); extendedtest/unit/invalid_extensions_test.dartverifies Step 1's skip guard does not rename.cover/.mp~2/.mp~12/.MP/.MVfiles to.mp4(deferring to Step 6); an e2e test (test/e2e/e2e_issue_138_cover_and_mp_tilde_test.dart) drives the full pipeline with--transform-pixel-mp mp4and default extension fixing, confirming.cover,.mp~2, and a.MPcontrol are all discovered, transformed to.mp4, and placed inALL_PHOTOSwith metadata applied. - Regression test suites for issue #139 at three levels β Unit tests (
test/unit/issue_139_cross_photo_gps_test.dart,test/unit/metadata_matcher_confidence_test.dart) cover the single-file gate and theisOwnSidecarconfidence flag across own-sidecar cases (exact, shortened, bracket-swap, no-extension, Pixel.MP, numbered same-file) and not-own cases (-editedremoval, cross-extension MP4βHEIC/JPG, numbered cross-extension, partial-ed). Integration tests (test/integration/issue_139_cross_photo_contamination_test.dart) cover multi-file folder layouts where own-sidecar and cross-photo candidates coexist, numbered duplicates each resolving to their own sidecar, tryhard-escalation guards, and GPS value round-trip correctness. An e2e test (test/e2e/e2e_issue_139_cross_photo_gps_test.dart) drives the full pipeline with--write-exifand reads back the output EXIF via ExifTool, confirming an MP4 motion companion does not inherit the JPG still's GPS/DateTime while the still itself keeps its own. - Regression test suites for issue #140 at two levels β Unit tests (
test/unit/issue_140_video_batch_date_contamination_test.dart) capture the per-file tags emitted bywriteExifDataSingle(the mock deliberately does not overridewriteExifDataBatch, so the real isolation logic runs) and assert two videos with different dates each receive their own XMP/EXIF date tags, plus that each file's queued tag map is a distinct instance (the defensive-copy snapshot property). An e2e test (test/e2e/e2e_issue_140_video_batch_date_test.dart) drives the full pipeline with--write-exifusing the issue's exact scenario (two.MOVfiles, each with its own JSON sidecar carrying a distinct date) and reads back the XMP date tags via real ExifTool, confirming each video keeps its own date rather than inheriting the sibling's. - Regression test suites for issue #143 β Integration tests (
test/integration/media_entity_moving_strategies_test.dart) cover HEIC, MP4, and MOV shortcut output (each album entry must remain a symlink), missing-link restoration after later processing stages, and physical-video recovery (a stale regular file at the shortcut location is removed when identical to the canonical file, or preserved underShortcut Conflicts/when distinct). - Test suite for JSON sidecar caching β Unit tests (
test/unit/json_sidecar_cache_test.dart) cover theFileEntitysidecar path and confidence flag fields (defaults, set/get, JSON round-trip),extractAllFromJsonCachedwith cached own-sidecar (returns date+GPS), cachedisOwnSidecar=false(drops date+GPS per issue #139), fallback to full lookup when no cached path exists (secondary files), graceful handling of deleted sidecar files,readJsonContentCachedcontent caching and invalid-input handling, and Step 2 β Step 4 integration verifying the cached path is populated during discovery and reused during date extraction. - 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
π Bug fixes:
- Fixed
Merge Duplicate Locationsin the Immich duplicate-cleanup form so it is displayed in the Web UI and is cleared and disabled consistently in Web, GUI, and TUI while native Immich deletion is enabled. - Fixed Web interface path sanitization for folder-structure selectors. Google Takeout, iCloud Takeout, and local-folder organization layout values such as
flattenandyear/monthare no longer converted into filesystem paths before execution.
π Documentation:
- Documented conservative Takeout JSON-only album recovery, its audit manifest, and the opt-in manual Immich duplicate-location merge control.
πΎ Download
Download the tool either for Linux, MacOS or Windows (for both x64 and arm64 architectures) or Docker version (platform & architecture independent) as you prefer, directly from following links:
Linux::
Mac OS:
Windows:
Docker Launcher:
What's Changed
- Add correct PhotoMigrator web app icon by @jaimetur in #1236
- Fix dead documentation links and anchors in README and help guides by @dajiaohuang in #1240
- Document the Google Photos OAuth scope required for uploads by @dajiaohuang in #1242
New Contributors
- @dajiaohuang made their first contribution in #1240
Full Changelog: v4.6.2...v4.7.0