Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
ignored_types: '["chore"]'
type_labels: '{"feat":"feature","fix":"fix","perf":"performance","refactor":"refactor","docs":"documentation","test":"test","build":"build","ci":"ci","breaking_change":"breaking change"}'
type_labels: '{"feat":"feature","fix":"fix","perf":"performance","refactor":"refactor","docs":"documentation","test":"test","build":"build","ci":"ci","breaking":"breaking change"}'

lint-and-build:
name: Lint and build
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Sync media from **GoPro cloud** into **Google Photos** for a capture date range
## Requirements

- Python **3.12 or 3.13** (3.14 is excluded until dependent wheels catch up)
- **`GP_ACCESS_TOKEN`** environment variable — GoPro cloud access token (required by `AsyncGoProClient`)
- **`GP_ACCESS_TOKEN`** or **`Q2GOOGLE_GOPRO_ACCESS_TOKEN`** — GoPro cloud access token (loaded by `Q2GoogleSettings` and passed to `AsyncGoProClient`)
- Google OAuth **installed app** credentials (`client_secret.json` from Google Cloud Console)
- A writable path for the user token (`token.json` by default)

Expand Down Expand Up @@ -42,6 +42,7 @@ from q2google import (
GooglePhotosClient,
GooglePhotosOAuth,
JsonFileBackend,
get_settings,
)
from q2google.gphotos.api import GooglePhotosAPI
from q2google.gphotos.models import PhotosScopes
Expand All @@ -54,8 +55,10 @@ async def main() -> None:
token_file="token.json",
)

cfg = get_settings()

async with (
AsyncGoProClient() as gopro,
AsyncGoProClient(access_token=cfg.gopro_access_token) as gopro,
GooglePhotosAPI(credentials=oauth) as api,
):
photos = GooglePhotosClient(api=api)
Expand Down Expand Up @@ -194,6 +197,7 @@ from q2google import (
GooglePhotosClient,
GooglePhotosOAuth,
JsonFileBackend,
get_settings,
)
from q2google.gphotos.api import GooglePhotosAPI
from q2google.gphotos.models import PhotosScopes
Expand All @@ -206,8 +210,10 @@ async def main() -> None:
token_file="token.json",
)

cfg = get_settings()

async with (
AsyncGoProClient() as gopro,
AsyncGoProClient(access_token=cfg.gopro_access_token) as gopro,
GooglePhotosAPI(credentials=oauth) as api,
):
photos = GooglePhotosClient(api=api)
Expand Down Expand Up @@ -315,7 +321,8 @@ All CLI options have environment-variable equivalents. `Q2GoogleSettings` (Pydan

| Variable | Purpose |
|----------|---------|
| `GP_ACCESS_TOKEN` | **GoPro cloud access token** — read by `AsyncGoProClient`; required for discovery |
| `GP_ACCESS_TOKEN` | **GoPro cloud access token** — alias accepted by `Q2GoogleSettings`; passed to `AsyncGoProClient` |
| `Q2GOOGLE_GOPRO_ACCESS_TOKEN` | **GoPro cloud access token** — prefixed alternative to `GP_ACCESS_TOKEN` |
| `Q2GOOGLE_CREDENTIALS_PATH` | Google OAuth client secrets JSON path |
| `Q2GOOGLE_TOKEN_PATH` | Authorized user token path |
| `Q2GOOGLE_STATE_DIR` | JSON session state directory |
Expand Down
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@ q2google sync \

## Environment variable precedence

All options can be set via environment variables. The `Q2GOOGLE_` prefix is used for every setting except `GP_ACCESS_TOKEN` (read directly by `AsyncGoProClient`). CLI flags always override environment variables.
All options can be set via environment variables with the `Q2GOOGLE_` prefix. The GoPro token also accepts `GP_ACCESS_TOKEN` or `Q2GOOGLE_GOPRO_ACCESS_TOKEN` (see [Configuration](configuration.md)). CLI flags always override environment variables.

q2google also reads a `.env` file from the working directory via `pydantic-settings`. See [Configuration](configuration.md) for the full reference.
6 changes: 5 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ All settings are managed by `Q2GoogleSettings` — a [Pydantic `BaseSettings`](h

## GoPro credentials

q2google loads the GoPro token into `Q2GoogleSettings.gopro_access_token` and passes it explicitly to `AsyncGoProClient`. Either variable below satisfies the requirement:

| Variable | Required | Description |
|----------|----------|-------------|
| `GP_ACCESS_TOKEN` | **Yes** | GoPro cloud access token; read directly by `AsyncGoProClient`. |
| `GP_ACCESS_TOKEN` | **Yes** (or alias below) | GoPro cloud access token. Accepted as a legacy alias for compatibility with standalone `gopro-api` usage. |
| `Q2GOOGLE_GOPRO_ACCESS_TOKEN` | **Yes** (or alias above) | Prefixed alternative; useful when you want all q2google settings under the `Q2GOOGLE_` namespace. |

## Google OAuth

Expand Down Expand Up @@ -44,6 +47,7 @@ q2google loads a `.env` file from the current working directory automatically. E

```dotenv
GP_ACCESS_TOKEN=eyJ...
# or: Q2GOOGLE_GOPRO_ACCESS_TOKEN=eyJ...

Q2GOOGLE_CREDENTIALS_PATH=secrets/client_secret.json
Q2GOOGLE_TOKEN_PATH=secrets/token.json
Expand Down
7 changes: 5 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Requirements

- Python **3.12 or 3.13** (3.14 is excluded until dependent wheels catch up)
- **`GP_ACCESS_TOKEN`** environment variable — your GoPro cloud access token
- **`GP_ACCESS_TOKEN`** or **`Q2GOOGLE_GOPRO_ACCESS_TOKEN`** — your GoPro cloud access token (loaded by `Q2GoogleSettings` and passed to `AsyncGoProClient`)
- Google OAuth **installed-app** credentials (`client_secret.json` from [Google Cloud Console](https://console.cloud.google.com))
- A writable path for the authorized user token (`token.json` by default)

Expand Down Expand Up @@ -48,6 +48,7 @@ from q2google import (
GooglePhotosClient,
GooglePhotosOAuth,
JsonFileBackend,
get_settings,
)
from q2google.gphotos.api import GooglePhotosAPI
from q2google.gphotos.models import PhotosScopes
Expand All @@ -60,8 +61,10 @@ async def main() -> None:
token_file="token.json",
)

cfg = get_settings()

async with (
AsyncGoProClient() as gopro,
AsyncGoProClient(access_token=cfg.gopro_access_token) as gopro,
GooglePhotosAPI(credentials=oauth) as api,
):
photos = GooglePhotosClient(api=api)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ dependencies = [
"aiohttp~=3.11.11",
"aiofiles~=25.1.0",
"google-auth-oauthlib~=1.3.0",
"gopro-api~=0.0.7",
"gopro-api~=0.0.9",
"pydantic>=2",
"pydantic-settings>=2",
"typer>=0.12",
Expand Down
1 change: 1 addition & 0 deletions q2google/cli/_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ async def _run_sync(

async with (
AsyncGoProClient(
access_token=cfg.gopro_access_token,
max_items=max_items,
prefer_height=prefer_height,
) as gopro,
Expand Down
8 changes: 7 additions & 1 deletion q2google/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from functools import lru_cache
from pathlib import Path

from pydantic import Field
from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings, SettingsConfigDict

#: Maximum ``newMediaItems`` per ``mediaItems:batchCreate`` request (Google Photos Library API).
Expand All @@ -29,6 +29,7 @@ class Q2GoogleSettings(BaseSettings):
token_path: Path where the authorized user refresh token is stored.
state_dir: Directory containing one JSON file per sync session.
session_id: Optional default session identifier when the CLI omits ``--session-id``.
gopro_access_token: GoPro cloud access token for discovery and CDN URL resolution.
gopro_max_items: Upper bound passed to GoPro cloud listing.
gopro_prefer_height: Preferred pixel height when resolving GoPro CDN assets.
google_photos_timeout_seconds: Total timeout per Library API HTTP request.
Expand All @@ -47,6 +48,7 @@ class Q2GoogleSettings(BaseSettings):
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
populate_by_name=True,
)

credentials_path: Path = Field(
Expand All @@ -65,6 +67,10 @@ class Q2GoogleSettings(BaseSettings):
default=None,
description="Default session id when not passed explicitly (e.g. CLI --session-id).",
)
gopro_access_token: str = Field(
description="GoPro cloud access token (env: ``GP_ACCESS_TOKEN`` or ``Q2GOOGLE_GOPRO_ACCESS_TOKEN``).",
validation_alias=AliasChoices("GP_ACCESS_TOKEN", "Q2GOOGLE_GOPRO_ACCESS_TOKEN"),
)

gopro_max_items: int = Field(default=2000, ge=1, description="Cap for list_media_items.")
gopro_prefer_height: int = Field(
Expand Down
Loading
Loading