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.
@@ -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.
@@ -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,7 @@
1
+ """Sync and async HTTP clients for api.gopro.com."""
2
+
3
+ from .gopro import GoProAPI
4
+ from .async_gopro import AsyncGoProAPI
5
+
6
+
7
+ __all__ = ["GoProAPI", "AsyncGoProAPI"]
@@ -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)