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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This project is not affiliated with or endorsed by GoPro.
- **`GoProAPI`** — synchronous client (`requests`), `with` context manager
- **`AsyncGoProAPI`** — async client (`aiohttp`), `async with` context manager
- **Pydantic** request/response types in `gopro_api.api.models`
- **CLI** — `gopro-api search`, `gopro-api info`, `gopro-api pull`
- **CLI** — `gopro-api search`, `gopro-api info`, `gopro-api pull`, `gopro-api auth`
- **`GP_ACCESS_TOKEN`** from environment / `.env` (browser cookie value)

## Requirements
Expand Down Expand Up @@ -62,13 +62,16 @@ gopro-api info MEDIA_ID --json
gopro-api pull MEDIA_ID ./downloads
gopro-api pull MEDIA_ID ./downloads --height 1080
gopro-api pull MEDIA_ID ./downloads --width 1920 --height 1080
gopro-api auth
gopro-api auth --json
```

| Command | Purpose |
|--------|---------|
| **`search`** | List media in a capture range. Default: a **`# _pages`** summary line, a tab-separated header (`id`, `type`, `captured_at`, `filename`, …; not `gopro_user_id` / `source_gumi` / `source_mgumi`), then one row per item (other API fields in an **`extra`** JSON column). **`--json`**: full API-shaped response; with **`--all-pages`**, a JSON array of every page. |
| **`info`** | Show download metadata for one media id (filename + file lines with size and URL), or **`--json`** for the full payload. |
| **`pull`** | Download asset(s) for a media id into **`destination`** (directory; created if missing). Videos (`.mp4` extension, case-insensitive): one **`variations`** entry — **tallest** by default, or closest to **`--height`** / **`--width`** (sum of squared pixel deltas; ties broken by larger resolution). Photos: uses **`files`** (one request per file). |
| **`auth`** | Verify that **`GP_ACCESS_TOKEN`** is configured and accepted by the API. Default: Rich panel; **`--json`** / **`--tsv`** for scripting. Exit code **`0`** when authenticated, **`2`** when the token is missing, **`1`** otherwise. |

Global **`--timeout`** (seconds, default **`60`**) applies to API calls and to **`pull`** CDN downloads (`requests.get`).

Expand All @@ -79,6 +82,7 @@ python -m gopro_api.cli search --start 2026-03-01 --end 2026-03-02
python -m gopro_api.cli info MEDIA_ID
python -m gopro_api.cli pull MEDIA_ID ./out
python -m gopro_api.cli pull MEDIA_ID ./out --height 720
python -m gopro_api.cli auth
```

## Configuration
Expand Down
27 changes: 25 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ flowchart TD
| `gopro_api/config.py` | pydantic-settings `Settings` (`GP_ACCESS_TOKEN`) |
| `gopro_api/exceptions.py` | Custom exception hierarchy |
| `gopro_api/utils.py` | Shared helpers (resolution scoring, etc.) |
| `gopro_api/cli/` | Typer application — `search`, `info`, `pull` commands |
| `gopro_api/cli/` | Typer application — `search`, `info`, `pull`, `auth` commands |

## Layers

Expand Down Expand Up @@ -65,7 +65,7 @@ async with AsyncGoProAPI() as api: # opens an aiohttp.ClientSession
All request parameters and API responses are typed with [Pydantic v2](https://docs.pydantic.dev/) models:

- **Requests** — `GoProMediaSearchParams`, `CapturedRange`.
- **Responses** — `GoProMediaSearchResponse`, `GoProMediaDownloadResponse`, and their `_embedded` / `_pages` children (with field aliases for the GoPro API's underscore-prefixed keys).
- **Responses** — `GoProMediaSearchResponse`, `GoProMediaDownloadResponse`, `GoProAuthStatus`, and their `_embedded` / `_pages` children (with field aliases for the GoPro API's underscore-prefixed keys).

List fields in request models are serialised to comma-separated strings automatically when calling `model_dump()`.

Expand All @@ -78,6 +78,8 @@ A single `Settings` class (pydantic-settings) reads `GP_ACCESS_TOKEN` from:

Clients accept an explicit `access_token` parameter that overrides `Settings`.

`get_token_info()` reports whether a token is configured and whether it came from the environment or a `.env` file.

### 5. CLI (`gopro_api.cli`)

The CLI is built with [Typer](https://typer.tiangolo.com/) and [Rich](https://github.com/Textualize/rich):
Expand All @@ -86,6 +88,7 @@ The CLI is built with [Typer](https://typer.tiangolo.com/) and [Rich](https://gi
- `gopro_api/cli/search.py` — `search` command + `SearchPrinter`.
- `gopro_api/cli/info.py` — `info` command + `InfoPrinter`.
- `gopro_api/cli/pull.py` — `pull` command + `PullPrinter`.
- `gopro_api/cli/auth.py` — `auth` command + `AuthPrinter`.
- `gopro_api/cli/_common.py` — shared helpers.

Each command delegates to `GoProClient`/`AsyncGoProClient` and passes the result to a dedicated `*Printer` class for Rich-formatted output.
Expand All @@ -110,6 +113,26 @@ sequenceDiagram
CLI->>User: Rich table or raw JSON (stdout)
```

## Data flow — `gopro-api auth`

```mermaid
sequenceDiagram
actor User
participant CLI
participant AsyncGoProClient
participant AsyncGoProAPI
participant GoproCom as api.gopro.com

User->>CLI: gopro-api auth
CLI->>AsyncGoProClient: check_auth()
AsyncGoProClient->>AsyncGoProAPI: check_auth()
AsyncGoProAPI->>GoproCom: GET /media/search?per_page=1
GoproCom-->>AsyncGoProAPI: HTTP 200 or 401
AsyncGoProAPI-->>AsyncGoProClient: GoProAuthStatus
AsyncGoProClient-->>CLI: GoProAuthStatus
CLI->>User: Rich panel or JSON (stdout)
```

## Error handling

```mermaid
Expand Down
4 changes: 4 additions & 0 deletions docs/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ Built with [Typer](https://typer.tiangolo.com/); the Typer application is expose

::: gopro_api.cli.pull_command

::: gopro_api.cli.auth_command

## Printers

::: gopro_api.cli.search.SearchPrinter

::: gopro_api.cli.info.InfoPrinter

::: gopro_api.cli.pull.PullPrinter

::: gopro_api.cli.auth.AuthPrinter
4 changes: 4 additions & 0 deletions docs/api/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ Pydantic models for GoPro cloud media search and download responses.
::: gopro_api.api.models.GoProMediaDownloadFile

::: gopro_api.api.models.GoProMediaDownloadSidecarFile

## Auth

::: gopro_api.api.models.GoProAuthStatus
37 changes: 37 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,50 @@ gopro-api pull MEDIA_ID ./downloads --height 1080
gopro-api pull MEDIA_ID ./downloads --width 1920 --height 1080
```

---

### `auth`

Verify that `GP_ACCESS_TOKEN` is configured and accepted by the GoPro cloud API.

```bash
gopro-api auth
```

**Options**

| Option | Description |
|--------|-------------|
| `--json` | Print structured JSON instead of the Rich panel. |
| `--tsv` | Print tab-separated key/value rows for scripting. |

**Default output** — a Rich panel showing whether the token is configured, where it came from (`environment`, `.env`, or an explicit client argument), whether the API accepted it, and a short message.

**Exit codes**

| Code | Meaning |
|------|---------|
| `0` | Token configured and authenticated. |
| `1` | Token present but rejected or the verification request failed. |
| `2` | `GP_ACCESS_TOKEN` is not set. |

```bash
# Rich panel (default)
gopro-api auth

# Scripting
gopro-api auth --json
gopro-api auth --tsv
```

## Running without an installed entry point

```bash
python -m gopro_api.cli search --start 2026-03-01 --end 2026-03-02
python -m gopro_api.cli info MEDIA_ID
python -m gopro_api.cli pull MEDIA_ID ./out
python -m gopro_api.cli pull MEDIA_ID ./out --height 720
python -m gopro_api.cli auth
```

## API reference
Expand Down
7 changes: 7 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ Sign in to [gopro.com](https://gopro.com) or [quik.gopro.com](https://quik.gopro

Tokens expire. If you receive a **401 Unauthorized** error, return to your browser and copy a fresh token value.

Check whether your current token is still valid:

```bash
gopro-api auth
gopro-api auth --json
```

## API reference

See [`gopro_api.config`](api/utils.md) for the `Settings` class that loads these values.
6 changes: 6 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ GP_ACCESS_TOKEN=your_token_here

The library loads it automatically via **pydantic-settings**. See [Configuration](configuration.md) for how to retrieve the token from your browser.

Verify the token before searching or downloading:

```bash
gopro-api auth
```

## Quick start — CLI

After installing, `gopro-api` is available on your `PATH`:
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

---

Full reference for `gopro-api search`, `info`, and `pull` with all flags and examples.
Full reference for `gopro-api search`, `info`, `pull`, and `auth` with all flags and examples.

[:octicons-arrow-right-24: CLI](cli.md)

Expand Down
32 changes: 32 additions & 0 deletions gopro_api/api/async_gopro.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import aiohttp

from gopro_api.config import get_settings
from gopro_api.api.auth_status import AuthCheckRequest, AuthStatusResolver
from gopro_api.api.models import (
GoProAuthStatus,
GoProMediaSearchParams,
GoProMediaDownloadResponse,
GoProMediaSearchResponse,
Expand All @@ -26,6 +28,7 @@ def __init__(self, access_token: str | None = None, timeout: float = 10.0) -> No
:attr:`~gopro_api.config.Settings.gp_access_token` from settings.
timeout: Total client timeout in seconds for ``aiohttp``.
"""
self._explicit_token = access_token is not None
self.access_token = access_token or get_settings().gp_access_token
self._timeout = aiohttp.ClientTimeout(total=timeout)
self._session: aiohttp.ClientSession | None = None
Expand Down Expand Up @@ -149,3 +152,32 @@ async def search(self, params: GoProMediaSearchParams) -> GoProMediaSearchRespon
response.raise_for_status()
body = await response.text()
return GoProMediaSearchResponse.model_validate_json(body)

async def check_auth(self) -> GoProAuthStatus:
"""Verify that the configured access token is accepted by the API.

Performs a lightweight ``GET /media/search`` request with ``per_page=1``.

Returns:
Structured authentication status; never raises for HTTP failures.

Raises:
RuntimeError: If used outside ``async with AsyncGoProAPI() as api``.
"""

async def perform_request(prepared: AuthCheckRequest) -> int:
session = self._session_or_raise()
async with session.get(
"/media/search",
headers=prepared.headers,
params=prepared.params,
) as response:
return response.status

return await AuthStatusResolver.verify_async(
access_token=self.access_token,
explicit_token=self._explicit_token,
get_headers=self.get_headers,
perform_request=perform_request,
request_error_type=aiohttp.ClientError,
)
Loading
Loading