gopro-api 0.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gopro_api-0.0.7/LICENSE +21 -0
- gopro_api-0.0.7/PKG-INFO +261 -0
- gopro_api-0.0.7/README.md +212 -0
- gopro_api-0.0.7/gopro_api/__init__.py +26 -0
- gopro_api-0.0.7/gopro_api/api/__init__.py +7 -0
- gopro_api-0.0.7/gopro_api/api/async_gopro.py +93 -0
- gopro_api-0.0.7/gopro_api/api/gopro.py +86 -0
- gopro_api-0.0.7/gopro_api/api/models.py +184 -0
- gopro_api-0.0.7/gopro_api/cli.py +339 -0
- gopro_api-0.0.7/gopro_api/client.py +299 -0
- gopro_api-0.0.7/gopro_api/config.py +26 -0
- gopro_api-0.0.7/gopro_api/exceptions.py +9 -0
- gopro_api-0.0.7/gopro_api/utils.py +104 -0
- gopro_api-0.0.7/gopro_api.egg-info/PKG-INFO +261 -0
- gopro_api-0.0.7/gopro_api.egg-info/SOURCES.txt +20 -0
- gopro_api-0.0.7/gopro_api.egg-info/dependency_links.txt +1 -0
- gopro_api-0.0.7/gopro_api.egg-info/entry_points.txt +2 -0
- gopro_api-0.0.7/gopro_api.egg-info/requires.txt +9 -0
- gopro_api-0.0.7/gopro_api.egg-info/top_level.txt +1 -0
- gopro_api-0.0.7/pyproject.toml +54 -0
- gopro_api-0.0.7/setup.cfg +4 -0
- gopro_api-0.0.7/setup.py +61 -0
gopro_api-0.0.7/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 himewel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
gopro_api-0.0.7/PKG-INFO
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gopro-api
|
|
3
|
+
Version: 0.0.7
|
|
4
|
+
Summary: Unofficial Python client for the GoPro cloud API (api.gopro.com): sync and async clients, Pydantic models, and a CLI.
|
|
5
|
+
Home-page: https://github.com/himewel/gopro-api
|
|
6
|
+
Author: himewel
|
|
7
|
+
Author-email: welberthime@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/himewel/gopro-api/issues
|
|
10
|
+
Project-URL: Source, https://github.com/himewel/gopro-api
|
|
11
|
+
Keywords: gopro quik cloud api async aiohttp media gopro-api
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Topic :: Multimedia :: Graphics :: Capture :: Digital Camera
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: aiohttp~=3.11.14
|
|
28
|
+
Requires-Dist: pydantic~=2.10.6
|
|
29
|
+
Requires-Dist: pydantic-settings~=2.14
|
|
30
|
+
Requires-Dist: requests~=2.32.3
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build~=1.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: black~=24.10.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pylint~=3.3.0; extra == "dev"
|
|
35
|
+
Dynamic: author
|
|
36
|
+
Dynamic: author-email
|
|
37
|
+
Dynamic: classifier
|
|
38
|
+
Dynamic: description
|
|
39
|
+
Dynamic: description-content-type
|
|
40
|
+
Dynamic: home-page
|
|
41
|
+
Dynamic: keywords
|
|
42
|
+
Dynamic: license
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
Dynamic: project-url
|
|
45
|
+
Dynamic: provides-extra
|
|
46
|
+
Dynamic: requires-dist
|
|
47
|
+
Dynamic: requires-python
|
|
48
|
+
Dynamic: summary
|
|
49
|
+
|
|
50
|
+
# gopro-api
|
|
51
|
+
|
|
52
|
+
Unofficial Python client for the **GoPro cloud / Quik** HTTP API at [`api.gopro.com`](https://api.gopro.com): **search** your library and **fetch download metadata** (CDN URLs, filenames, variants). Built with **Pydantic** models, plus **sync** (`requests`) and **async** (`aiohttp`) clients and a small **`gopro-api`** CLI.
|
|
53
|
+
|
|
54
|
+
This project is not affiliated with or endorsed by GoPro.
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- **`GoProAPI`** — synchronous client (`requests`), `with` context manager
|
|
59
|
+
- **`AsyncGoProAPI`** — async client (`aiohttp`), `async with` context manager
|
|
60
|
+
- **Pydantic** request/response types in `gopro_api.api.models`
|
|
61
|
+
- **CLI** — `gopro-api search`, `gopro-api info`, `gopro-api pull`
|
|
62
|
+
- **`GP_ACCESS_TOKEN`** from environment / `.env` (browser cookie value)
|
|
63
|
+
|
|
64
|
+
## Requirements
|
|
65
|
+
|
|
66
|
+
- Python **3.10+**
|
|
67
|
+
- **`GP_ACCESS_TOKEN`** — see [Configuration](#configuration)
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
From the repository root:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python -m venv .venv
|
|
75
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
76
|
+
pip install -r requirements.txt
|
|
77
|
+
pip install -e .
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
From a local wheel (name matches your build):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install ./dist/gopro_api-*-py3-none-any.whl
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Published package on PyPI (distribution name **`gopro-api`**, import **`gopro_api`**):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install gopro-api
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## CLI
|
|
93
|
+
|
|
94
|
+
After install, **`gopro-api`** is on your `PATH`:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
gopro-api --help
|
|
98
|
+
gopro-api --version
|
|
99
|
+
gopro-api search --start 2026-03-01 --end 2026-03-03 --per-page 30
|
|
100
|
+
gopro-api search --start 2026-03-01 --end 2026-03-03 --all-pages
|
|
101
|
+
gopro-api search --start 2026-03-01 --end 2026-03-03 --json
|
|
102
|
+
gopro-api info MEDIA_ID
|
|
103
|
+
gopro-api info MEDIA_ID --json
|
|
104
|
+
gopro-api pull MEDIA_ID ./downloads
|
|
105
|
+
gopro-api pull MEDIA_ID ./downloads --height 1080
|
|
106
|
+
gopro-api pull MEDIA_ID ./downloads --width 1920 --height 1080
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Command | Purpose |
|
|
110
|
+
|--------|---------|
|
|
111
|
+
| **`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. |
|
|
112
|
+
| **`info`** | Show download metadata for one media id (filename + file lines with size and URL), or **`--json`** for the full payload. |
|
|
113
|
+
| **`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). |
|
|
114
|
+
|
|
115
|
+
Global **`--timeout`** (seconds, default **`60`**) applies to API calls and to **`pull`** CDN downloads (`requests.get`).
|
|
116
|
+
|
|
117
|
+
Run without an installed script:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python -m gopro_api.cli search --start 2026-03-01 --end 2026-03-02
|
|
121
|
+
python -m gopro_api.cli info MEDIA_ID
|
|
122
|
+
python -m gopro_api.cli pull MEDIA_ID ./out
|
|
123
|
+
python -m gopro_api.cli pull MEDIA_ID ./out --height 720
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
`gopro_api.config` reads settings from the environment and from a `.env` file in the current working directory via **pydantic-settings**. The only required setting is **`GP_ACCESS_TOKEN`**.
|
|
129
|
+
|
|
130
|
+
Example `.env`:
|
|
131
|
+
|
|
132
|
+
```env
|
|
133
|
+
GP_ACCESS_TOKEN=your_token_here
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The clients send it as a cookie: `gp_access_token=<value>`. Put **only the token string** in `GP_ACCESS_TOKEN` (not the `gp_access_token=` prefix).
|
|
137
|
+
|
|
138
|
+
You can override the token in code: `GoProAPI(access_token="...")` or `AsyncGoProAPI(access_token="...")`.
|
|
139
|
+
|
|
140
|
+
### Retrieving `gp_access_token` from your browser
|
|
141
|
+
|
|
142
|
+
Sign in to the GoPro web app (e.g. [gopro.com](https://gopro.com) media / Quik). The site sets a cookie **`gp_access_token`**.
|
|
143
|
+
|
|
144
|
+
**Chrome / Edge / Brave**
|
|
145
|
+
|
|
146
|
+
1. Open the site while logged in.
|
|
147
|
+
2. **F12** → **Application** → **Cookies** → choose the origin (often `https://quik.gopro.com` or another `*.gopro.com` host).
|
|
148
|
+
3. Copy the **Value** of **`gp_access_token`**.
|
|
149
|
+
|
|
150
|
+
**Firefox**
|
|
151
|
+
|
|
152
|
+
**F12** → **Storage** → **Cookies** → same idea.
|
|
153
|
+
|
|
154
|
+
**Network panel (Chromium)**
|
|
155
|
+
|
|
156
|
+
1. **Network** → trigger requests to **`api.gopro.com`**.
|
|
157
|
+
2. Pick a request → **Headers** → **Cookie**.
|
|
158
|
+
3. Copy the value after `gp_access_token=` up to the next `;` (or end of string).
|
|
159
|
+
|
|
160
|
+
**Notes**
|
|
161
|
+
|
|
162
|
+
- If the cookie is **HttpOnly**, use the **Network** method.
|
|
163
|
+
- Tokens **expire**; refresh from the browser if you get **401**.
|
|
164
|
+
- Treat the token like a password.
|
|
165
|
+
|
|
166
|
+
**Security:** Do not commit `.env` or tokens. Keep `.env` in `.gitignore`.
|
|
167
|
+
|
|
168
|
+
## Library usage
|
|
169
|
+
|
|
170
|
+
### Async (`AsyncGoProAPI`)
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import asyncio
|
|
174
|
+
from datetime import datetime
|
|
175
|
+
|
|
176
|
+
from gopro_api.api import AsyncGoProAPI
|
|
177
|
+
from gopro_api.api.models import CapturedRange, GoProMediaSearchParams
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def main() -> None:
|
|
181
|
+
params = GoProMediaSearchParams(
|
|
182
|
+
captured_range=CapturedRange(
|
|
183
|
+
start=datetime.fromisoformat("2026-03-01"),
|
|
184
|
+
end=datetime.fromisoformat("2026-03-02"),
|
|
185
|
+
),
|
|
186
|
+
per_page=50,
|
|
187
|
+
page=1,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async with AsyncGoProAPI() as api:
|
|
191
|
+
search = await api.search(params)
|
|
192
|
+
for item in search.embedded.media:
|
|
193
|
+
meta = await api.download(item.id)
|
|
194
|
+
print(meta.filename, len(meta.embedded.files), "files")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
asyncio.run(main())
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Sync (`GoProAPI`)
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from datetime import datetime
|
|
205
|
+
|
|
206
|
+
from gopro_api.api import GoProAPI
|
|
207
|
+
from gopro_api.api.models import CapturedRange, GoProMediaSearchParams
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def main() -> None:
|
|
211
|
+
params = GoProMediaSearchParams(
|
|
212
|
+
captured_range=CapturedRange(
|
|
213
|
+
start=datetime.fromisoformat("2026-03-01"),
|
|
214
|
+
end=datetime.fromisoformat("2026-03-02"),
|
|
215
|
+
),
|
|
216
|
+
per_page=50,
|
|
217
|
+
page=1,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
with GoProAPI() as api:
|
|
221
|
+
search = api.search(params)
|
|
222
|
+
for item in search.embedded.media:
|
|
223
|
+
meta = api.download(item.id)
|
|
224
|
+
print(meta.filename, len(meta.embedded.files), "files")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
main()
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Models
|
|
232
|
+
|
|
233
|
+
- **Requests:** `GoProMediaSearchParams`, `CapturedRange`, etc. in **`gopro_api.api.models`**.
|
|
234
|
+
- **Responses:** search and download JSON shapes (including `_embedded` / `_pages` aliases).
|
|
235
|
+
|
|
236
|
+
List fields in search params are serialized to comma-separated strings when you call **`model_dump()`** (used by the HTTP clients).
|
|
237
|
+
|
|
238
|
+
## Project layout
|
|
239
|
+
|
|
240
|
+
| Path | Role |
|
|
241
|
+
|------|------|
|
|
242
|
+
| `gopro_api/api/gopro.py` | `GoProAPI` — sync `search`, `download` |
|
|
243
|
+
| `gopro_api/api/async_gopro.py` | `AsyncGoProAPI` — async `search`, `download` |
|
|
244
|
+
| `gopro_api/api/models.py` | Pydantic request/response models |
|
|
245
|
+
| `gopro_api/api/__init__.py` | Re-exports `GoProAPI`, `AsyncGoProAPI` |
|
|
246
|
+
| `gopro_api/config.py` | pydantic-settings `Settings`, `GP_ACCESS_TOKEN` |
|
|
247
|
+
| `gopro_api/cli.py` | `gopro-api` CLI |
|
|
248
|
+
| `setup.py` | Package metadata, dependencies, console entry point |
|
|
249
|
+
|
|
250
|
+
## CI and releases
|
|
251
|
+
|
|
252
|
+
[`.github/workflows/release.yml`](.github/workflows/release.yml):
|
|
253
|
+
|
|
254
|
+
- **Push to `main`** — builds wheel + source `.zip`, uploads **workflow artifacts**.
|
|
255
|
+
- **Push tag `v*`** (e.g. `v0.0.5`) — attaches the same files to a **GitHub Release**.
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
[MIT License](LICENSE).
|
|
260
|
+
|
|
261
|
+
GoPro, Quik, and related marks are trademarks of their respective owners.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# gopro-api
|
|
2
|
+
|
|
3
|
+
Unofficial Python client for the **GoPro cloud / Quik** HTTP API at [`api.gopro.com`](https://api.gopro.com): **search** your library and **fetch download metadata** (CDN URLs, filenames, variants). Built with **Pydantic** models, plus **sync** (`requests`) and **async** (`aiohttp`) clients and a small **`gopro-api`** CLI.
|
|
4
|
+
|
|
5
|
+
This project is not affiliated with or endorsed by GoPro.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **`GoProAPI`** — synchronous client (`requests`), `with` context manager
|
|
10
|
+
- **`AsyncGoProAPI`** — async client (`aiohttp`), `async with` context manager
|
|
11
|
+
- **Pydantic** request/response types in `gopro_api.api.models`
|
|
12
|
+
- **CLI** — `gopro-api search`, `gopro-api info`, `gopro-api pull`
|
|
13
|
+
- **`GP_ACCESS_TOKEN`** from environment / `.env` (browser cookie value)
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Python **3.10+**
|
|
18
|
+
- **`GP_ACCESS_TOKEN`** — see [Configuration](#configuration)
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
From the repository root:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python -m venv .venv
|
|
26
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
27
|
+
pip install -r requirements.txt
|
|
28
|
+
pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
From a local wheel (name matches your build):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install ./dist/gopro_api-*-py3-none-any.whl
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Published package on PyPI (distribution name **`gopro-api`**, import **`gopro_api`**):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install gopro-api
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## CLI
|
|
44
|
+
|
|
45
|
+
After install, **`gopro-api`** is on your `PATH`:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
gopro-api --help
|
|
49
|
+
gopro-api --version
|
|
50
|
+
gopro-api search --start 2026-03-01 --end 2026-03-03 --per-page 30
|
|
51
|
+
gopro-api search --start 2026-03-01 --end 2026-03-03 --all-pages
|
|
52
|
+
gopro-api search --start 2026-03-01 --end 2026-03-03 --json
|
|
53
|
+
gopro-api info MEDIA_ID
|
|
54
|
+
gopro-api info MEDIA_ID --json
|
|
55
|
+
gopro-api pull MEDIA_ID ./downloads
|
|
56
|
+
gopro-api pull MEDIA_ID ./downloads --height 1080
|
|
57
|
+
gopro-api pull MEDIA_ID ./downloads --width 1920 --height 1080
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| Command | Purpose |
|
|
61
|
+
|--------|---------|
|
|
62
|
+
| **`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. |
|
|
63
|
+
| **`info`** | Show download metadata for one media id (filename + file lines with size and URL), or **`--json`** for the full payload. |
|
|
64
|
+
| **`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). |
|
|
65
|
+
|
|
66
|
+
Global **`--timeout`** (seconds, default **`60`**) applies to API calls and to **`pull`** CDN downloads (`requests.get`).
|
|
67
|
+
|
|
68
|
+
Run without an installed script:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python -m gopro_api.cli search --start 2026-03-01 --end 2026-03-02
|
|
72
|
+
python -m gopro_api.cli info MEDIA_ID
|
|
73
|
+
python -m gopro_api.cli pull MEDIA_ID ./out
|
|
74
|
+
python -m gopro_api.cli pull MEDIA_ID ./out --height 720
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
`gopro_api.config` reads settings from the environment and from a `.env` file in the current working directory via **pydantic-settings**. The only required setting is **`GP_ACCESS_TOKEN`**.
|
|
80
|
+
|
|
81
|
+
Example `.env`:
|
|
82
|
+
|
|
83
|
+
```env
|
|
84
|
+
GP_ACCESS_TOKEN=your_token_here
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The clients send it as a cookie: `gp_access_token=<value>`. Put **only the token string** in `GP_ACCESS_TOKEN` (not the `gp_access_token=` prefix).
|
|
88
|
+
|
|
89
|
+
You can override the token in code: `GoProAPI(access_token="...")` or `AsyncGoProAPI(access_token="...")`.
|
|
90
|
+
|
|
91
|
+
### Retrieving `gp_access_token` from your browser
|
|
92
|
+
|
|
93
|
+
Sign in to the GoPro web app (e.g. [gopro.com](https://gopro.com) media / Quik). The site sets a cookie **`gp_access_token`**.
|
|
94
|
+
|
|
95
|
+
**Chrome / Edge / Brave**
|
|
96
|
+
|
|
97
|
+
1. Open the site while logged in.
|
|
98
|
+
2. **F12** → **Application** → **Cookies** → choose the origin (often `https://quik.gopro.com` or another `*.gopro.com` host).
|
|
99
|
+
3. Copy the **Value** of **`gp_access_token`**.
|
|
100
|
+
|
|
101
|
+
**Firefox**
|
|
102
|
+
|
|
103
|
+
**F12** → **Storage** → **Cookies** → same idea.
|
|
104
|
+
|
|
105
|
+
**Network panel (Chromium)**
|
|
106
|
+
|
|
107
|
+
1. **Network** → trigger requests to **`api.gopro.com`**.
|
|
108
|
+
2. Pick a request → **Headers** → **Cookie**.
|
|
109
|
+
3. Copy the value after `gp_access_token=` up to the next `;` (or end of string).
|
|
110
|
+
|
|
111
|
+
**Notes**
|
|
112
|
+
|
|
113
|
+
- If the cookie is **HttpOnly**, use the **Network** method.
|
|
114
|
+
- Tokens **expire**; refresh from the browser if you get **401**.
|
|
115
|
+
- Treat the token like a password.
|
|
116
|
+
|
|
117
|
+
**Security:** Do not commit `.env` or tokens. Keep `.env` in `.gitignore`.
|
|
118
|
+
|
|
119
|
+
## Library usage
|
|
120
|
+
|
|
121
|
+
### Async (`AsyncGoProAPI`)
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import asyncio
|
|
125
|
+
from datetime import datetime
|
|
126
|
+
|
|
127
|
+
from gopro_api.api import AsyncGoProAPI
|
|
128
|
+
from gopro_api.api.models import CapturedRange, GoProMediaSearchParams
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def main() -> None:
|
|
132
|
+
params = GoProMediaSearchParams(
|
|
133
|
+
captured_range=CapturedRange(
|
|
134
|
+
start=datetime.fromisoformat("2026-03-01"),
|
|
135
|
+
end=datetime.fromisoformat("2026-03-02"),
|
|
136
|
+
),
|
|
137
|
+
per_page=50,
|
|
138
|
+
page=1,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
async with AsyncGoProAPI() as api:
|
|
142
|
+
search = await api.search(params)
|
|
143
|
+
for item in search.embedded.media:
|
|
144
|
+
meta = await api.download(item.id)
|
|
145
|
+
print(meta.filename, len(meta.embedded.files), "files")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
asyncio.run(main())
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Sync (`GoProAPI`)
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from datetime import datetime
|
|
156
|
+
|
|
157
|
+
from gopro_api.api import GoProAPI
|
|
158
|
+
from gopro_api.api.models import CapturedRange, GoProMediaSearchParams
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def main() -> None:
|
|
162
|
+
params = GoProMediaSearchParams(
|
|
163
|
+
captured_range=CapturedRange(
|
|
164
|
+
start=datetime.fromisoformat("2026-03-01"),
|
|
165
|
+
end=datetime.fromisoformat("2026-03-02"),
|
|
166
|
+
),
|
|
167
|
+
per_page=50,
|
|
168
|
+
page=1,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
with GoProAPI() as api:
|
|
172
|
+
search = api.search(params)
|
|
173
|
+
for item in search.embedded.media:
|
|
174
|
+
meta = api.download(item.id)
|
|
175
|
+
print(meta.filename, len(meta.embedded.files), "files")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Models
|
|
183
|
+
|
|
184
|
+
- **Requests:** `GoProMediaSearchParams`, `CapturedRange`, etc. in **`gopro_api.api.models`**.
|
|
185
|
+
- **Responses:** search and download JSON shapes (including `_embedded` / `_pages` aliases).
|
|
186
|
+
|
|
187
|
+
List fields in search params are serialized to comma-separated strings when you call **`model_dump()`** (used by the HTTP clients).
|
|
188
|
+
|
|
189
|
+
## Project layout
|
|
190
|
+
|
|
191
|
+
| Path | Role |
|
|
192
|
+
|------|------|
|
|
193
|
+
| `gopro_api/api/gopro.py` | `GoProAPI` — sync `search`, `download` |
|
|
194
|
+
| `gopro_api/api/async_gopro.py` | `AsyncGoProAPI` — async `search`, `download` |
|
|
195
|
+
| `gopro_api/api/models.py` | Pydantic request/response models |
|
|
196
|
+
| `gopro_api/api/__init__.py` | Re-exports `GoProAPI`, `AsyncGoProAPI` |
|
|
197
|
+
| `gopro_api/config.py` | pydantic-settings `Settings`, `GP_ACCESS_TOKEN` |
|
|
198
|
+
| `gopro_api/cli.py` | `gopro-api` CLI |
|
|
199
|
+
| `setup.py` | Package metadata, dependencies, console entry point |
|
|
200
|
+
|
|
201
|
+
## CI and releases
|
|
202
|
+
|
|
203
|
+
[`.github/workflows/release.yml`](.github/workflows/release.yml):
|
|
204
|
+
|
|
205
|
+
- **Push to `main`** — builds wheel + source `.zip`, uploads **workflow artifacts**.
|
|
206
|
+
- **Push tag `v*`** (e.g. `v0.0.5`) — attaches the same files to a **GitHub Release**.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
[MIT License](LICENSE).
|
|
211
|
+
|
|
212
|
+
GoPro, Quik, and related marks are trademarks of their respective owners.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Unofficial Python client for the GoPro cloud API (api.gopro.com)."""
|
|
2
|
+
|
|
3
|
+
from gopro_api.api.async_gopro import AsyncGoProAPI
|
|
4
|
+
from gopro_api.api.gopro import GoProAPI
|
|
5
|
+
from gopro_api.api.models import (
|
|
6
|
+
CapturedRange,
|
|
7
|
+
GoProMediaDownloadResponse,
|
|
8
|
+
GoProMediaDownloadVariation,
|
|
9
|
+
GoProMediaSearchParams,
|
|
10
|
+
GoProMediaSearchResponse,
|
|
11
|
+
)
|
|
12
|
+
from gopro_api.client import AsyncGoProClient, GoProClient
|
|
13
|
+
from gopro_api.exceptions import NoVariationsError
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"GoProAPI",
|
|
17
|
+
"AsyncGoProAPI",
|
|
18
|
+
"GoProClient",
|
|
19
|
+
"AsyncGoProClient",
|
|
20
|
+
"NoVariationsError",
|
|
21
|
+
"GoProMediaSearchParams",
|
|
22
|
+
"GoProMediaDownloadResponse",
|
|
23
|
+
"GoProMediaSearchResponse",
|
|
24
|
+
"GoProMediaDownloadVariation",
|
|
25
|
+
"CapturedRange",
|
|
26
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Asynchronous GoPro cloud API client (``aiohttp``)."""
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
|
|
5
|
+
from gopro_api.config import GP_ACCESS_TOKEN
|
|
6
|
+
from gopro_api.api.models import (
|
|
7
|
+
GoProMediaSearchParams,
|
|
8
|
+
GoProMediaDownloadResponse,
|
|
9
|
+
GoProMediaSearchResponse,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncGoProAPI:
|
|
14
|
+
"""Async client for ``https://api.gopro.com`` (Quik / cloud library).
|
|
15
|
+
|
|
16
|
+
Use as an async context manager so an ``aiohttp.ClientSession`` is opened
|
|
17
|
+
and closed around ``search`` and ``download``. Pass ``access_token`` to
|
|
18
|
+
override ``gopro_api.config.GP_ACCESS_TOKEN``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, access_token: str | None = None, timeout: float = 10.0) -> None:
|
|
22
|
+
"""Create an async client.
|
|
23
|
+
|
|
24
|
+
``access_token``: cookie value; defaults to ``GP_ACCESS_TOKEN``.
|
|
25
|
+
``timeout``: total HTTP client timeout in seconds.
|
|
26
|
+
"""
|
|
27
|
+
self.access_token = access_token or GP_ACCESS_TOKEN
|
|
28
|
+
self._timeout = aiohttp.ClientTimeout(total=timeout)
|
|
29
|
+
self._session: aiohttp.ClientSession | None = None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def base_url(self) -> str:
|
|
33
|
+
"""API origin (``https://api.gopro.com``)."""
|
|
34
|
+
return "https://api.gopro.com"
|
|
35
|
+
|
|
36
|
+
def get_headers(self, accept: str) -> dict[str, str]:
|
|
37
|
+
"""Build ``Cookie`` and ``Accept`` headers for an API call."""
|
|
38
|
+
return {
|
|
39
|
+
"Cookie": "gp_access_token=" + self.access_token,
|
|
40
|
+
"Accept": accept,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async def __aenter__(self) -> "AsyncGoProAPI":
|
|
44
|
+
"""Open an ``aiohttp.ClientSession`` for the ``async with`` body."""
|
|
45
|
+
self._session = aiohttp.ClientSession(
|
|
46
|
+
base_url=self.base_url,
|
|
47
|
+
timeout=self._timeout,
|
|
48
|
+
)
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
52
|
+
"""Close the session."""
|
|
53
|
+
if self._session is not None:
|
|
54
|
+
await self._session.close()
|
|
55
|
+
self._session = None
|
|
56
|
+
|
|
57
|
+
def _session_or_raise(self) -> aiohttp.ClientSession:
|
|
58
|
+
"""Return the active session or raise if not inside ``async with``."""
|
|
59
|
+
session = self._session
|
|
60
|
+
if session is None:
|
|
61
|
+
msg = (
|
|
62
|
+
"Use AsyncGoProAPI as an async context manager: "
|
|
63
|
+
"async with AsyncGoProAPI() as api: ..."
|
|
64
|
+
)
|
|
65
|
+
raise RuntimeError(msg)
|
|
66
|
+
return session
|
|
67
|
+
|
|
68
|
+
async def download(self, media_id: str) -> GoProMediaDownloadResponse:
|
|
69
|
+
"""``GET /media/{media_id}/download`` — metadata and CDN URLs for files."""
|
|
70
|
+
headers = self.get_headers("application/vnd.gopro.jk.media+json; version=2.0.0")
|
|
71
|
+
session = self._session_or_raise()
|
|
72
|
+
async with session.get(
|
|
73
|
+
f"/media/{media_id}/download",
|
|
74
|
+
headers=headers,
|
|
75
|
+
) as response:
|
|
76
|
+
response.raise_for_status()
|
|
77
|
+
body = await response.text()
|
|
78
|
+
return GoProMediaDownloadResponse.model_validate_json(body)
|
|
79
|
+
|
|
80
|
+
async def search(self, params: GoProMediaSearchParams) -> GoProMediaSearchResponse:
|
|
81
|
+
"""``GET /media/search`` using ``params.model_dump()`` as query string."""
|
|
82
|
+
headers = self.get_headers(
|
|
83
|
+
"application/vnd.gopro.jk.media.search+json; version=2.0.0",
|
|
84
|
+
)
|
|
85
|
+
session = self._session_or_raise()
|
|
86
|
+
async with session.get(
|
|
87
|
+
"/media/search",
|
|
88
|
+
headers=headers,
|
|
89
|
+
params=params.model_dump(),
|
|
90
|
+
) as response:
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
body = await response.text()
|
|
93
|
+
return GoProMediaSearchResponse.model_validate_json(body)
|