Skip to content

gphotosmobile: add Google Photos mobile API backend#9135

Open
xob0t wants to merge 31 commits intorclone:masterfrom
xob0t:backend/gphotosmobile
Open

gphotosmobile: add Google Photos mobile API backend#9135
xob0t wants to merge 31 commits intorclone:masterfrom
xob0t:backend/gphotosmobile

Conversation

@xob0t
Copy link

@xob0t xob0t commented Feb 1, 2026

What is the purpose of this change?

Add new gphotosmobile backend that interfaces with Google Photos using the reverse-engineered mobile API (the same API the Android Google Photos app uses). This bypasses all restrictions of the official REST API — download any photo/video at original quality, upload, trash items, full metadata with SHA1 hashes, and read/write descriptions (captions).

Was the change discussed in an issue or in the forum before?

No

Checklist

  • I have read the contribution guidelines.
  • I have added tests for all changes in this PR if appropriate.
  • I have added documentation for the changes if appropriate.
  • All commit messages are in house style.
  • I'm done, this Pull Request is ready for review :-)

Summary

  • Add new gphotosmobile backend that interfaces with Google Photos using the reverse-engineered mobile API (the same API the Android Google Photos app uses)
  • This bypasses all restrictions of the official REST API -- download any photo/video at original quality, upload, trash items, full metadata
  • Rich metadata support: read location, camera info, EXIF, timestamps, flags; write description (caption)
  • Backend command set-description for standalone caption editing
  • Includes full documentation (with autogenerated options via make backenddocs), backend YAML, integration test scaffolding
  • Unit tests for protowire encode/decode, parser, float conversions, and helpers
  • Extensive in-code protocol documentation for maintainers: API endpoints, auth flow, sync protocol, protobuf field maps, request/response structures

What it does

Feature Official googlephotos backend This backend (gphotosmobile)
Download any photo Only rclone-uploaded (since March 2025) All photos
Download quality Compressed Original
Video quality Heavily compressed Original
EXIF data Stripped (no location) Preserved
Upload Yes Yes
Delete Only from rclone-created albums Any item (trash)
Albums Yes Not yet
Auth method OAuth (browser) Android device token (ReVanced)
SHA1 hashes No Yes
Metadata (read) No Yes (description, location, camera, EXIF, timestamps, flags)
Metadata (write) No Yes (description/caption)

Architecture

rclone <-> gphotosmobile backend <-> Google Photos Mobile API (protobuf)
                |
                +-> SQLite cache (<cache-dir>/gphotosmobile/<remote>.db)
  • Auth: Android device tokens -> bearer tokens via android.googleapis.com/auth
  • Library sync: Full initial sync + fast incremental deltas, cached in local SQLite
  • Protobuf: Raw wire format encode/decode (no compiled .proto files)
  • Downloads: Direct HTTP stream by default; optional temp file download cache for seeking (opt-in via download_cache = true)
  • Uploads: SHA1 dedup check -> streaming upload via temp file (no in-memory buffering)
  • Metadata: Read rich metadata (location, EXIF, timestamps, flags) from cache; write description via SetCaption API
  • Retry: fs.Pacer with pacer.NewGoogleDrive for all API calls; standard shouldRetry(ctx, err) pattern with retryErrorCodes (429, 500, 502, 503, 509)
  • Types: api/types.go subpackage with MediaItem and Error types (standard rclone convention used by 25+ backends)
  • No lib/rest: Justified — all API calls use raw protobuf over HTTPS, not JSON/XML REST

Metadata support

The backend implements fs.Metadataer and fs.SetMetadataer:

Key Type Writable Description
description string Yes Caption/description shown in Google Photos
media-type string No photo or video
width int No Width in pixels
height int No Height in pixels
duration int No Duration in ms (videos)
latitude float No GPS latitude
longitude float No GPS longitude
location-name string No Reverse-geocoded location
camera-make string No Camera manufacturer
camera-model string No Camera model
aperture float No f-number
shutter-speed float No Shutter speed in seconds
iso int No ISO sensitivity
focal-length float No Focal length in mm
taken-at RFC 3339 No Capture timestamp
uploaded-at RFC 3339 No Upload timestamp
is-favorite bool No Favorite flag
is-archived bool No Archive flag
origin string No self, partner, or shared

Backend commands

The backend implements fs.Commander with:

  • set-description: Set or clear the caption of a media item
    rclone backend set-description remote:media/photo.jpg -o description="Sunset at the beach"
    rclone backend set-description remote:media/photo.jpg -o description=""  # clear
    

Configuration options

Standard options

Option Env Var Type Description
auth_data RCLONE_GPHOTOSMOBILE_AUTH_DATA string (required, sensitive) Google Photos mobile API auth data. The auth string containing Android device credentials, starting with androidId=. Obtained via Google Photos ReVanced + ADB logcat.

Advanced options

Option Env Var Type Default Description
cache_db_path RCLONE_GPHOTOSMOBILE_CACHE_DB_PATH string <cache-dir>/gphotosmobile/<remote>.db Path to SQLite cache database. The library index is cached locally for fast listing. Initial sync may take a few minutes for large libraries; subsequent runs use fast incremental sync. If deleted, rclone re-syncs the full library on next run.
device_model RCLONE_GPHOTOSMOBILE_DEVICE_MODEL string Pixel 9a Device model reported to Google Photos in user agent and upload metadata.
device_make RCLONE_GPHOTOSMOBILE_DEVICE_MAKE string Google Device manufacturer reported to Google Photos in upload metadata.
apk_version RCLONE_GPHOTOSMOBILE_APK_VERSION int64 49029607 Google Photos APK version code to impersonate. Sent in the user agent and protobuf requests. Update if Google blocks older versions.
android_api RCLONE_GPHOTOSMOBILE_ANDROID_API int64 35 Android API level to report (35 = Android 15, matching the default Pixel 9a device). Included in upload metadata.
download_cache RCLONE_GPHOTOSMOBILE_DOWNLOAD_CACHE bool false Enable download cache for opened files. When enabled, each file is downloaded once to a temp file and shared across concurrent readers with seeking support. Off by default -- the backend streams directly from the API. Enable for rclone mount if you want backend-level seeking in addition to VFS caching.
encoding RCLONE_GPHOTOSMOBILE_ENCODING MultiEncoder Slash,CrLf,InvalidUtf8,Dot Filename encoding.

Files added

File Lines Purpose
gphotosmobile.go 1318 Main backend: fs.Fs, fs.Object, fs.Shutdowner, fs.Commander, fs.Metadataer, fs.SetMetadataer, registration, cache sync orchestration
api.go 716 MobileAPI: auth, all API methods (including SetCaption), fs.Pacer with pacer.NewGoogleDrive, standard shouldRetry/retryErrorCodes
api/types.go 78 api.MediaItem struct (38+ fields with db: tags) and api.Error type (standard rclone api/ subpackage convention)
request_builders.go 992 Builds raw protobuf request bodies (including SetCaption)
protowire_utils.go 300 Raw protobuf wire encoder/decoder
parser.go 389 Parses library sync responses into MediaItem structs
cache.go 594 SQLite cache (media index + sync state)
download_cache.go 294 Optional shared temp-file download cache for mount/seek (opt-in)
gphotosmobile_test.go 18 Integration test via fstests.Run()
gphotosmobile_internal_test.go 371 Unit tests: protowire encode/decode, parser, float conversions, helpers

Protocol documentation (in-code)

Every source file has a detailed package-level or file-level doc comment explaining the inner workings for maintainers:

  • api.go -- Full protocol overview: authentication flow (device tokens to bearer tokens), list of all API endpoints with their numeric path IDs (including SetCaption), transport details (raw protobuf over HTTPS), justification for not using lib/rest
  • gphotosmobile.go -- Library sync protocol: initial sync vs incremental sync state machine, state_token/page_token semantics, sync throttling, virtual filesystem layout, duplicate filename handling, metadata field mapping
  • parser.go -- Complete protobuf field map for sync responses: every field path for media items (metadata, type info, location, EXIF), deletion entries, and pagination tokens
  • request_builders.go -- Request structure: field mask pattern (how the client tells the server which fields to return), top-level request layout shared across the three sync request types, upload commit, trash, and caption request formats
  • download_cache.go -- Download cache architecture: why it exists (no HTTP Range support), how shared downloads work, reference counting, known limitations
  • protowire_utils.go -- Why raw wire format instead of compiled protos, encoding/decoding approach, relationship to Python's blackboxprotobuf
  • cache.go -- SQLite schema, WAL mode, cache location, crash recovery via persisted page_token
  • api/types.go -- Key identifier types (media_key vs dedup_key vs SHA1Hash) and their uses

Contributing guide compliance

  • Uses fshttp.NewClient(ctx) for HTTP transport (respects --proxy, --timeout, --dump, --tpslimit)
  • context.Context propagated to all HTTP requests (supports cancellation)
  • fs.Shutdowner implemented -- closes SQLite cache and cleans up temp files
  • fs.Commander implemented -- set-description backend command for standalone caption editing
  • fs.Pacer with pacer.NewGoogleDrive for all API retry logic (standard rclone pattern)
  • shouldRetry(ctx, err) (bool, error) with retryErrorCodes -- matches standard naming used across all backends
  • api/types.go subpackage with MediaItem and Error types (matches 25+ other backends)
  • All HTTP methods (GetUploadToken, DownloadFile, getAuthToken) wrapped in pacer.Call() with shouldRetry
  • All non-2xx HTTP responses return *api.Error for consistent retry logic
  • NewObject properly distinguishes "not found" (sql.ErrNoRows) from real database errors, handles dedup-suffixed filenames
  • SetModTime returns fs.ErrorCantSetModTime (API does not support modtime changes)
  • Precision returns fs.ModTimeNotSupported
  • Put populates full metadata from cache after upload
  • lib/encoder support for path encoding
  • golangci-lint v2 passes with 0 issues
  • Backend YAML (docs/data/backends/gphotosmobile.yaml) with correct features and precision
  • Doc file with autogenerated options (populated via bin/make_backend_docs.py)
  • Added to README.md, docs.md, _index.md, navbar.html, make_manual.py -- all in correct alphabetical order by full name
  • Integration test file + fstest/test_all/config.yaml entry
  • Unit tests for protowire encode/decode, parser, float conversions, helpers (26 tests)
  • Cache stored in rclone's --cache-dir (not custom location)
  • Option Help texts follow single-sentence-with-period format
  • Package name gphotosmobile (no underscores), matches directory name and FileName() output
  • Extensive in-code protocol documentation for maintainability
  • Download cache is opt-in (download_cache config option, default false)
  • APK version and Android API level are configurable (apk_version, android_api)
  • Sentinel errors for non-removable virtual directories (matching googlephotos pattern)
  • Download cache background goroutine uses detached context (won't cancel shared downloads)
  • fs.Metadataer + fs.SetMetadataer implemented -- rich read metadata, writable description
  • MetadataInfo registered with systemMetadataInfo map listing all 19 metadata keys
  • ReadMetadata + WriteMetadata feature flags set
  • lib/rest not used -- justified (protobuf binary protocol, not JSON REST), documented on MobileAPI struct

Testing done

  • rclone about mygphotos: -- quota display works
  • rclone ls mygphotos:media/ -- lists all ~35K items
  • rclone copy mygphotos:media/video.mp4 /local/ -- downloads at original quality
  • rclone copy /local/photo.jpg mygphotos:media/ -- uploads with streaming SHA1 + dedup
  • rclone delete mygphotos:media/file.mp4 -- moves to trash
  • rclone mount mygphotos: G: --vfs-cache-mode full -- mount works, file browsing and sequential reads work. Seeking requires --gphotosmobile-download-cache=true
  • rclone lsjson mygphotos:media/photo.jpg --metadata -- returns all metadata fields
  • rclone backend set-description mygphotos: media/photo.jpg -o description="test" -- sets caption
  • go test -v -run "Test[^I]" ./backend/gphotosmobile/... -- 26 unit tests pass
  • golangci-lint v2 run ./backend/gphotosmobile/... -- 0 issues
  • go vet ./backend/gphotosmobile/... -- passes
  • Full binary build with -tags cmount -- passes
  • bin/make_backend_docs.py gphotosmobile -- autogenerated options populated successfully

xob0t added 27 commits February 1, 2026 10:42
… and quota support

Implement a full rclone backend using the reverse-engineered Google Photos
mobile API (same as Android app), bypassing official REST API restrictions.

Features: library sync with SQLite cache, original quality download/upload,
SHA1 hash verification, shared download cache with seeking for FUSE mount,
trash support, and About/quota reporting for mounted drive space display.
Replace gms_auth references with step-by-step ReVanced method that
doesn't require root. Update help text, comparison table, and
troubleshooting to match.
…ding

- Rename backend/gphotos_mobile -> backend/gphotosmobile (Go package naming)
- Use fshttp.NewClient(ctx) instead of custom http.Client, remove proxy option
- Fix all 38 golangci-lint issues (errcheck, var-naming, unused, unconvert, staticcheck)
- Add autogenerated options markers to docs
- Add backend YAML, test file, doc entries, and test_all config
…add to README

- Use config.GetCacheDir()/gphotosmobile/<remote>.db instead of ~/.gpmc/<email>/
- Add lib/encoder support for path encoding (Base, EncodeCrLf, EncodeInvalidUtf8)
- Rename doc/yaml files from gphotos_mobile to gphotosmobile to match package dir
- Add backend entry to README.md storage providers list
- Update all doc references to use consistent gphotosmobile naming
Remove GetFixed32, GetRepeatedStrings, AddSignedVarint, AddFixed64,
AddRepeatedVarint from protowire_utils.go and GetByMediaKey, Count
from cache.go. None of these were called anywhere in the codebase.
…-ops

Mkdir now returns nil only for existing virtual directories (root, media)
and fs.ErrorDirNotFound for anything else. Rmdir returns an error for
virtual directories (can't remove them) and fs.ErrorDirNotFound for
unknown paths.
When multiple items share the same filename, append media_key suffix
to ALL of them (not just the 2nd+). This ensures filenames are stable
regardless of database ordering between syncs.
…e filenames

Media keys share a long common prefix (AF1QipN...) making short
prefixes useless for disambiguation. Dedup keys are short (~27 chars)
and unique per item.
…to memory

Put() now streams the input through a SHA1 hasher into a temp file,
then uploads from the temp file. This avoids loading the entire file
into memory, making multi-GB video uploads feasible.

UploadFile() now accepts io.Reader + size instead of []byte.
…, populate Put metadata from cache, use sentinel errors
@xob0t xob0t marked this pull request as draft February 1, 2026 13:24
@xob0t xob0t marked this pull request as ready for review February 1, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant