Browser-automation tooling for pulling recent media from Google Photos.
Project description
gphoto-pull
gphoto-pull is a Python CLI for downloading original Google Photos photos and videos
uploaded or shared inside a date/time window. It uses Playwright with an app-owned persistent
Chromium profile, so the normal flow is manual Google login once and headless pulls after that.
This tool is intended to complement, not replace, Google Takeout. Use Google Takeout first to make
a full backup of your Google Photos library, then use gphoto-pull for incremental syncs on top of
that baseline.
Google Photos is not a stable public API, and automating interaction with Google Photos is probably
against Google's terms of service. The workflow is brittle and can break when Google changes the
site. The project maintainers take no responsibility for what happens to your Google account if you
use gphoto-pull. This project treats the browser session, downloaded files, diagnostics, and sync
database as local runtime state and keeps enough diagnostics to debug site drift.
Install
Install from PyPI:
pipx install gphoto-pull
Or install directly from GitHub:
pipx install git+https://github.com/ebrevdo/gphoto-pull.git
First Run
gphoto-pull config --defaults
gphoto-pull install-browser
gphoto-pull doctor
gphoto-pull login
gphoto-pull pull --after "Jan 1 2026" --before "Feb 1 2026"
gphoto-pull login opens a normal Chromium window using the persistent profile, without attaching
Playwright to the login browser. Complete Google login and MFA in that browser, then press Enter in
the terminal. Later pull runs reuse that same profile with Playwright and run headless by default.
Runtime Directory
By default, everything lives under:
~/.local/state/gphoto-pull/
That directory owns:
gphoto-pull.toml: optional local defaults.browsers/: Playwright-installed Chromium binaries.chrome-profile/: persistent Google login profile.diagnostics/: HTML, screenshots, RPC payloads, and download traces.state/pull-state.sqlite3: local media index.
Use --config-dir path for a fully isolated second setup. Relative paths in gphoto-pull.toml
resolve under that config directory.
By default, downloaded originals are written to the current working directory. If
download_dir is set in gphoto-pull.toml, it must be absolute or start with ~/. Relative
download paths are only allowed with the --download-dir CLI option, where they resolve from the
current working directory.
Commands
gphoto-pull config: interactively writegphoto-pull.toml.gphoto-pull config --defaults --force: write defaults without prompts.gphoto-pull install-browser: install Playwright Chromium into the configured runtime directory.gphoto-pull doctor: check config, browser binaries, and runtime paths.gphoto-pull login: launch the persistent headed browser profile for manual login.gphoto-pull refresh --after TIMESTAMP: refresh the local media index without downloading.gphoto-pull refresh --after TIMESTAMP --reset: delete and rebuild the local media index.gphoto-pull pull --after START --before END: enumerate and download originals in a window.gphoto-pull reset --yes: delete the persistent browser profile and local media index.gphoto-pull reset --target index --yes: delete only the local media index.
--after is inclusive and --before is exclusive. Both accept common date/time text.
Valid examples include 2026-01-01, 2026/01/01 00:00 -0800, and
Jan 1 2026 12:00 AM. If no time is provided, midnight is used. If no timezone is
provided, the local machine timezone is used.
When --before bounds a window already covered by the local media index, pull queries
SQLite directly and skips the live Recently added and /updates refresh. Open-ended windows,
or bounded windows newer than indexed coverage, still refresh live diagnostics first.
Useful pull overrides:
gphoto-pull --config-dir ~/.local/state/gphoto-pull-work pull \
--after "Jan 1 2026" \
--before "Feb 1 2026" \
--download-dir downloads \
--concurrency 4 \
--headed
Configuration
Example gphoto-pull.toml:
# Omit this to download into the current working directory.
# download_dir = "downloads"
download_concurrency = 3
enrichment_concurrency = 5
sync_db_path = "state/pull-state.sqlite3"
diagnostics_dir = "diagnostics"
browsers_path = "browsers"
browser_profile_dir = "chrome-profile"
headless = true
enrich_metadata = true
# Usually pass these on the command line instead.
# after = "Jan 1 2026"
# before = "Feb 1 2026"
# Optional when Playwright Chromium is not enough.
# browser_binary = "/path/to/chromium"
Do not use environment variables for normal configuration. Put durable defaults in
gphoto-pull.toml and use CLI flags for one-off overrides.
Use --no-enrich-metadata to skip post-download detail-page metadata enrichment for a faster
pull, or --enrich-metadata to force it on when the config disables it. Enrichment runs on a
separate worker pool controlled by enrichment_concurrency.
How Pull Works
- Launches the persistent browser profile.
- For bounded windows already covered by the local index, queries SQLite directly.
- Otherwise captures fresh
Recently addedand/updatesdiagnostics. - Parses Google Photos payload shapes for exact upload/share times when available.
- Persists candidates in SQLite and skips already-downloaded files.
- Downloads originals with concurrent workers, preferring direct URLs when proven and falling back to the Google Photos detail-page Download action.
- Finalizes downloads atomically, writes sidecar metadata, and optionally enriches sidecars from the detail page.
Troubleshooting
- If
doctorreports missing browser binaries, rungphoto-pull install-browser. - If
pulllands on a Google marketing page or account chooser, rungphoto-pull loginagain. - If
logincrashes because the browser profile is stale or locked, rungphoto-pull reset --yes. - If downloads fail, inspect
diagnostics/pull_failures/anddiagnostics/download_traces/. - If a run is interrupted, rerun the same command. The SQLite state skips finalized files.
- If older UUID-named duplicate files are reported by
doctor, they are leftovers from early builds and can be deleted after you confirm the named downloads exist.
Development
Install just, or use the direct uv run ... commands in CONTRIBUTING.md.
uv sync --dev
uv run gphoto-pull --help
just --list
just lint
just test
just check
CI runs the same checks on pushes to main, pull requests, and manual dispatches:
Ruff format, Ruff lint, Pyright, unit tests, and uv build. The same local commands are
available in justfile; CI keeps the underlying uv commands expanded so the workflow does not
need to install just.
To prepare a release:
uv sync --dev
just publish-dry-run
Publishing from GitHub uses PyPI Trusted Publishing through .github/workflows/release.yml.
Configure the PyPI project publisher for this repository, workflow filename release.yml, and
GitHub environment pypi. Release tags use a v prefix, starting with v0.0.1. After PyPI is
configured, publishing is tokenless: publish a GitHub release or run the Release workflow manually,
and the workflow will build, test, and run
uv publish --trusted-publishing always dist/*.
The source uses a src/ layout. Main modules are:
cli.py: typed Tyro CLI.config.py:msgspecTOML config and path resolution.browser.py: Playwright profile/session lifecycle.automation.py: high-level pull orchestration.photos_ui.py: Google Photos selectors and route classification.rpc_payloads.py: Google Photos payload parsers.enumeration.py: candidate enumeration and cutoff matching.state.py: SQLite sync state.download.py: final path planning and atomic file moves.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gphoto_pull-0.0.3.tar.gz.
File metadata
- Download URL: gphoto_pull-0.0.3.tar.gz
- Upload date:
- Size: 105.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79b6804d54d9c6857725993ff9e0272d97fa6e0c3c33cbd0f659c674b06d9177
|
|
| MD5 |
29a81a095a51ba7138fbbe08b6fbde40
|
|
| BLAKE2b-256 |
fe5b6d82676e090f70fdb4b34bbc8377155b04af581dd931154ce96bf8a262c1
|
File details
Details for the file gphoto_pull-0.0.3-py3-none-any.whl.
File metadata
- Download URL: gphoto_pull-0.0.3-py3-none-any.whl
- Upload date:
- Size: 86.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
550dea87e6e3ff006bcfc1d61e989188bb802200ed057e568ce84396dbcfe85c
|
|
| MD5 |
49b1f3d1473cb701ed9b149603a07247
|
|
| BLAKE2b-256 |
9f15e28b9103b33e66eeac4f22d7a89bf6c1e925019715b0780d7ce3f45f136e
|