vidkraken 0.1.0__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.
- vidkraken-0.1.0/.gitignore +21 -0
- vidkraken-0.1.0/LICENSE +21 -0
- vidkraken-0.1.0/PKG-INFO +297 -0
- vidkraken-0.1.0/README.md +260 -0
- vidkraken-0.1.0/pyproject.toml +72 -0
- vidkraken-0.1.0/src/vidkraken/__init__.py +97 -0
- vidkraken-0.1.0/src/vidkraken/__main__.py +6 -0
- vidkraken-0.1.0/src/vidkraken/_async_client.py +305 -0
- vidkraken-0.1.0/src/vidkraken/_base.py +314 -0
- vidkraken-0.1.0/src/vidkraken/_client.py +295 -0
- vidkraken-0.1.0/src/vidkraken/_exceptions.py +254 -0
- vidkraken-0.1.0/src/vidkraken/_files.py +93 -0
- vidkraken-0.1.0/src/vidkraken/_models.py +496 -0
- vidkraken-0.1.0/src/vidkraken/_version.py +1 -0
- vidkraken-0.1.0/src/vidkraken/cli.py +458 -0
- vidkraken-0.1.0/src/vidkraken/py.typed +0 -0
- vidkraken-0.1.0/tests/conftest.py +180 -0
- vidkraken-0.1.0/tests/test_async.py +164 -0
- vidkraken-0.1.0/tests/test_cli.py +284 -0
- vidkraken-0.1.0/tests/test_client.py +550 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Local virtualenv
|
|
2
|
+
.venv/
|
|
3
|
+
.venv-*/
|
|
4
|
+
.uv-cache/
|
|
5
|
+
.uv-python/
|
|
6
|
+
venv/
|
|
7
|
+
|
|
8
|
+
# Build artifacts
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
*.whl
|
|
13
|
+
|
|
14
|
+
# Caches
|
|
15
|
+
__pycache__/
|
|
16
|
+
*.py[cod]
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
htmlcov/
|
vidkraken-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vid Kraken
|
|
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.
|
vidkraken-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vidkraken
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK and CLI for the Vid Kraken YouTube download API
|
|
5
|
+
Project-URL: Homepage, https://vidkraken.com
|
|
6
|
+
Project-URL: Documentation, https://vidkraken.com/docs
|
|
7
|
+
Project-URL: Source, https://github.com/vidkraken/vidkraken
|
|
8
|
+
Project-URL: Issues, https://github.com/vidkraken/vidkraken/issues
|
|
9
|
+
Author-email: Vid Kraken <hello@vidkraken.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,audio,cli,download,mp3,sdk,video,vidkraken,youtube,youtube-downloader
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Classifier: Topic :: Multimedia :: Video
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.9
|
|
30
|
+
Requires-Dist: httpx>=0.25
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build>=1; extra == 'dev'
|
|
33
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
35
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Vid Kraken Python SDK and CLI
|
|
39
|
+
|
|
40
|
+
The official Python client for [Vid Kraken](https://vidkraken.com), a REST API
|
|
41
|
+
that downloads YouTube videos and audio on its servers and returns a CDN link
|
|
42
|
+
to the finished file. You pay for bandwidth only, and failed downloads are never
|
|
43
|
+
charged.
|
|
44
|
+
|
|
45
|
+
One package gives you:
|
|
46
|
+
|
|
47
|
+
- `VidKraken` and `AsyncVidKraken`: typed sync and async clients built on [httpx](https://www.python-httpx.org/)
|
|
48
|
+
- a `vidkraken` command-line tool for scripts, shells and AI agents
|
|
49
|
+
|
|
50
|
+
> Use only with content you have the right to download.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install vidkraken
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Requires Python 3.9 or newer. The only runtime dependency is `httpx`.
|
|
59
|
+
|
|
60
|
+
## Get an API key
|
|
61
|
+
|
|
62
|
+
Sign in at **https://vidkraken.com/login** and copy your key from the dashboard.
|
|
63
|
+
Then either export it:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
export VIDKRAKEN_API_KEY="vk_..."
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
or pass it to the client with `VidKraken(api_key="vk_...")`.
|
|
70
|
+
|
|
71
|
+
## Quickstart (sync)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from vidkraken import VidKraken
|
|
75
|
+
|
|
76
|
+
with VidKraken() as vk: # reads VIDKRAKEN_API_KEY
|
|
77
|
+
done = vk.download_and_wait(
|
|
78
|
+
"https://youtu.be/dQw4w9WgXcQ",
|
|
79
|
+
format="720", # "1080", "720", "480", "360", "audio", "audio-aac", "audio-mp3"
|
|
80
|
+
on_progress=lambda s: print(s.status, s.progress and s.progress.percent),
|
|
81
|
+
)
|
|
82
|
+
print(done.download_url, done.actual_format, done.file_size)
|
|
83
|
+
path = vk.save_file(done, "downloads/") # named after the video title
|
|
84
|
+
print("saved to", path)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Step by step, if you'd rather poll yourself or use a webhook:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
job = vk.download("https://youtu.be/dQw4w9WgXcQ", format="audio-mp3", start_time=30, end_time=90)
|
|
91
|
+
print(job.job_id, job.title, job.rate_limit.remaining)
|
|
92
|
+
|
|
93
|
+
status = vk.get_download(job.job_id) # IN_QUEUE / IN_PROGRESS / COMPLETED / FAILED
|
|
94
|
+
status = vk.wait_for_download(job.job_id, poll_interval=3.0, timeout=600)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Pass `webhook_url="https://example.com/hooks/vidkraken"` to `download()` and Vid
|
|
98
|
+
Kraken POSTs the final status to that URL. The body is the same as `get_download()`
|
|
99
|
+
returns, so `DownloadStatus.from_dict(payload)` parses it.
|
|
100
|
+
|
|
101
|
+
## Quickstart (async)
|
|
102
|
+
|
|
103
|
+
`AsyncVidKraken` has the same methods. Each one is a coroutine.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import asyncio
|
|
107
|
+
from vidkraken import AsyncVidKraken
|
|
108
|
+
|
|
109
|
+
async def main() -> None:
|
|
110
|
+
async with AsyncVidKraken() as vk:
|
|
111
|
+
urls = ["https://youtu.be/dQw4w9WgXcQ", "https://youtu.be/9bZkp7q19f0"]
|
|
112
|
+
results = await asyncio.gather(*(vk.download_and_wait(u, format="audio") for u in urls))
|
|
113
|
+
for done in results:
|
|
114
|
+
await vk.save_file(done, "downloads/")
|
|
115
|
+
|
|
116
|
+
asyncio.run(main())
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Video info
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
info = vk.info_and_wait("https://youtu.be/dQw4w9WgXcQ", include_audio_tracks=True)
|
|
123
|
+
print(info.title, info.duration)
|
|
124
|
+
for track in info.audio or []:
|
|
125
|
+
print(track.language, track.type) # e.g. "en original", "es dubbed"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`info()`, `get_info()` and `wait_for_info()` are the separate steps behind `info_and_wait()`.
|
|
129
|
+
|
|
130
|
+
## Account and channel listing
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
me = vk.me()
|
|
134
|
+
print(me.plan.id, me.balances["bandwidth_mb"].remaining)
|
|
135
|
+
|
|
136
|
+
listing = vk.list_channel("https://www.youtube.com/@RickAstleyYT", max_videos=25) # paid add-on
|
|
137
|
+
for entry in listing.entries:
|
|
138
|
+
print(entry["id"], entry["title"])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Saving files
|
|
142
|
+
|
|
143
|
+
`save_file(source, path=None)` streams a finished download to disk in chunks.
|
|
144
|
+
|
|
145
|
+
- `source` is a completed `DownloadStatus` or a raw `downloadUrl` string.
|
|
146
|
+
- `path` is a file path, a directory, or `None` for the current directory. When
|
|
147
|
+
you give a directory, the file is named `<title>.<ext>`: `.mp4` for video,
|
|
148
|
+
`.mp3` for `audio-mp3`, `.m4a` for `audio-aac`, and the CDN's own extension
|
|
149
|
+
for `audio`.
|
|
150
|
+
- The file is written to `<name>.part` and renamed once it is complete.
|
|
151
|
+
- Your API key is never sent to the file host.
|
|
152
|
+
|
|
153
|
+
## Responses
|
|
154
|
+
|
|
155
|
+
Responses are frozen dataclasses with snake_case attributes, for example
|
|
156
|
+
`status.download_url` and `status.progress.eta_seconds`. The original JSON is kept
|
|
157
|
+
on `.raw`, and `obj["camelCaseKey"]` / `obj.get(...)` read from it, so fields
|
|
158
|
+
added to the API later are still reachable.
|
|
159
|
+
|
|
160
|
+
## Errors
|
|
161
|
+
|
|
162
|
+
Every error inherits from `vidkraken.VidKrakenError`, which has `.status`, `.code`,
|
|
163
|
+
`.message`, `.details` and `.retry_after`. Branch on `.code`, not on the message.
|
|
164
|
+
|
|
165
|
+
| Exception | When | `code` |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `BadRequestError` | 400 | `INVALID_REQUEST` |
|
|
168
|
+
| `AuthenticationError` | 401 | `UNAUTHORIZED` |
|
|
169
|
+
| `PermissionDeniedError` | 403 | `LIMIT_REACHED` |
|
|
170
|
+
| `NotFoundError` | 404 | `NOT_FOUND`, `VIDEO_NOT_FOUND` |
|
|
171
|
+
| `RateLimitError` | 429 | `TOO_MANY_PENDING`, `RATE_LIMITED` |
|
|
172
|
+
| `ServerError` / `ServiceUnavailableError` | 5xx / 503 | `INTERNAL_ERROR` / `UNAVAILABLE` |
|
|
173
|
+
| `APIError` | any other HTTP error (base class of the above) | as sent |
|
|
174
|
+
| `APIConnectionError` / `APITimeoutError` | no HTTP response | `CONNECTION_ERROR` / `TIMEOUT` |
|
|
175
|
+
| `JobFailedError` | the job ended in `FAILED` / `failed` | `.error_code`, e.g. `AGE_RESTRICTED` |
|
|
176
|
+
| `WaitTimeoutError` | `wait_for_*(timeout=...)` ran out of time | `WAIT_TIMEOUT` |
|
|
177
|
+
| `MissingAPIKeyError` | no key given and `VIDKRAKEN_API_KEY` is unset | `MISSING_API_KEY` |
|
|
178
|
+
|
|
179
|
+
If the API sends a code this SDK doesn't know, the exception class is picked from
|
|
180
|
+
the HTTP status.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from vidkraken import JobFailedError, RateLimitError, VidKrakenError
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
done = vk.download_and_wait(url, format="1080")
|
|
187
|
+
except JobFailedError as e:
|
|
188
|
+
print(e.job_id, e.error_code, e.message)
|
|
189
|
+
if e.retryable: # only NETWORK_ERROR is worth resubmitting
|
|
190
|
+
...
|
|
191
|
+
except RateLimitError as e:
|
|
192
|
+
print("retry in", e.retry_after, "seconds")
|
|
193
|
+
except VidKrakenError as e:
|
|
194
|
+
print(e.status, e.code, e.message)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Retries
|
|
198
|
+
|
|
199
|
+
The API has **no idempotency key**. Retrying a `POST /download` after a timeout
|
|
200
|
+
or a 5xx could therefore start, and bill, a second download. The client only
|
|
201
|
+
retries when that can't happen:
|
|
202
|
+
|
|
203
|
+
| Request | Retried on |
|
|
204
|
+
|---|---|
|
|
205
|
+
| Any `GET` (status polls, `/me`, file download) | network errors, timeouts, 429, 500, 502, 503, 504 |
|
|
206
|
+
| `POST /download`, `/info`, `/list-channel` | 429 and 503 `UNAVAILABLE` only, because in both cases nothing was queued |
|
|
207
|
+
|
|
208
|
+
It never retries a POST on 500, 502, 504, a timeout or a network error. In those
|
|
209
|
+
cases you get the error and can check before resubmitting.
|
|
210
|
+
|
|
211
|
+
- Retries use exponential backoff with jitter (0.5s, 1s, 2s and so on, capped at 8s).
|
|
212
|
+
- A `Retry-After` header is honored if it is 60s or less. If it is longer, the
|
|
213
|
+
error is raised so you can decide what to do.
|
|
214
|
+
- `max_retries` defaults to 2. Set it to 0 to turn retries off:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
vk = VidKraken(max_retries=5, timeout=30.0)
|
|
218
|
+
vk = VidKraken(max_retries=0)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Rate limits
|
|
222
|
+
|
|
223
|
+
Each account can have at most 100 downloads queued or in progress at once.
|
|
224
|
+
Every `download()` result carries the headroom parsed from the IETF `RateLimit`
|
|
225
|
+
headers:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
job = vk.download(url)
|
|
229
|
+
job.rate_limit.limit # 100: how many may be pending at once
|
|
230
|
+
job.rate_limit.remaining # how many more you can queue right now
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Past the cap, `download()` raises `RateLimitError` with code `TOO_MANY_PENDING`
|
|
234
|
+
and `.retry_after` set. The error also has `.rate_limit`.
|
|
235
|
+
|
|
236
|
+
## Bring your own httpx client
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
import httpx
|
|
240
|
+
vk = VidKraken(http_client=httpx.Client(proxy="http://proxy:8080")) # proxy= needs httpx>=0.26
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The client must be an `httpx.Client` for `VidKraken` or an `httpx.AsyncClient`
|
|
244
|
+
for `AsyncVidKraken`. A client you pass in is not closed by `close()`.
|
|
245
|
+
|
|
246
|
+
## CLI
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
vidkraken download https://youtu.be/dQw4w9WgXcQ -f 1080 # waits, saves, prints the path
|
|
250
|
+
vidkraken download https://youtu.be/dQw4w9WgXcQ -f audio-mp3 -o music/
|
|
251
|
+
vidkraken download https://youtu.be/dQw4w9WgXcQ --start 30 --end 90 -o clip.mp4 -f 720
|
|
252
|
+
vidkraken download https://youtu.be/dQw4w9WgXcQ --no-wait # prints the job id
|
|
253
|
+
vidkraken download https://youtu.be/dQw4w9WgXcQ --webhook https://example.com/hook --no-wait
|
|
254
|
+
vidkraken status <job_id>
|
|
255
|
+
vidkraken info https://youtu.be/dQw4w9WgXcQ --audio-tracks
|
|
256
|
+
vidkraken me
|
|
257
|
+
vidkraken --version
|
|
258
|
+
python -m vidkraken --help
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
| Option | Applies to | Effect |
|
|
262
|
+
|---|---|---|
|
|
263
|
+
| `-f/--format` | `download` | one of `1080`, `720`, `480`, `360`, `audio` (default), `audio-aac`, `audio-mp3` |
|
|
264
|
+
| `--start S` / `--end S` | `download` | trim the clip, in seconds |
|
|
265
|
+
| `--language LANG` | `download` | preferred audio track |
|
|
266
|
+
| `-o/--output PATH` | `download` | output file or directory |
|
|
267
|
+
| `--no-wait` | `download` | print the job id and exit |
|
|
268
|
+
| `--webhook URL` | `download` | URL to POST the final status to |
|
|
269
|
+
| `--timeout S`, `--poll-interval S` | `download` | how long to wait and how often to poll |
|
|
270
|
+
| `-q/--quiet` | `download` | no progress output |
|
|
271
|
+
| `--json` | every command | print one JSON document on stdout |
|
|
272
|
+
| `--api-key KEY` | every command | overrides `VIDKRAKEN_API_KEY` |
|
|
273
|
+
|
|
274
|
+
The CLI is designed to be scripted and driven by agents:
|
|
275
|
+
|
|
276
|
+
- **stdout** carries only the result: a path, a job id, a plain report, or one
|
|
277
|
+
JSON document with `--json`. On failure with `--json`, stdout gets
|
|
278
|
+
`{"error", "code", ...}`.
|
|
279
|
+
- **stderr** carries progress and errors such as
|
|
280
|
+
`vidkraken: error: TOO_MANY_PENDING: ...`.
|
|
281
|
+
- It never prompts. Color appears only when stderr is a TTY and `NO_COLOR` is unset.
|
|
282
|
+
- **Exit codes:**
|
|
283
|
+
- `0`: success
|
|
284
|
+
- `1`: API or job error. `status` also exits 1 for a `FAILED` job.
|
|
285
|
+
- `2`: usage error, including a missing API key
|
|
286
|
+
- `130`: interrupted
|
|
287
|
+
|
|
288
|
+
## Links
|
|
289
|
+
|
|
290
|
+
- Documentation: https://vidkraken.com/docs
|
|
291
|
+
- LLM-friendly reference: https://vidkraken.com/llms.txt
|
|
292
|
+
- OpenAPI spec: https://vidkraken.com/openapi.yaml
|
|
293
|
+
- Support: hello@vidkraken.com
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
MIT © Omnivision Solutions
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Vid Kraken Python SDK and CLI
|
|
2
|
+
|
|
3
|
+
The official Python client for [Vid Kraken](https://vidkraken.com), a REST API
|
|
4
|
+
that downloads YouTube videos and audio on its servers and returns a CDN link
|
|
5
|
+
to the finished file. You pay for bandwidth only, and failed downloads are never
|
|
6
|
+
charged.
|
|
7
|
+
|
|
8
|
+
One package gives you:
|
|
9
|
+
|
|
10
|
+
- `VidKraken` and `AsyncVidKraken`: typed sync and async clients built on [httpx](https://www.python-httpx.org/)
|
|
11
|
+
- a `vidkraken` command-line tool for scripts, shells and AI agents
|
|
12
|
+
|
|
13
|
+
> Use only with content you have the right to download.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install vidkraken
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Python 3.9 or newer. The only runtime dependency is `httpx`.
|
|
22
|
+
|
|
23
|
+
## Get an API key
|
|
24
|
+
|
|
25
|
+
Sign in at **https://vidkraken.com/login** and copy your key from the dashboard.
|
|
26
|
+
Then either export it:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
export VIDKRAKEN_API_KEY="vk_..."
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
or pass it to the client with `VidKraken(api_key="vk_...")`.
|
|
33
|
+
|
|
34
|
+
## Quickstart (sync)
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from vidkraken import VidKraken
|
|
38
|
+
|
|
39
|
+
with VidKraken() as vk: # reads VIDKRAKEN_API_KEY
|
|
40
|
+
done = vk.download_and_wait(
|
|
41
|
+
"https://youtu.be/dQw4w9WgXcQ",
|
|
42
|
+
format="720", # "1080", "720", "480", "360", "audio", "audio-aac", "audio-mp3"
|
|
43
|
+
on_progress=lambda s: print(s.status, s.progress and s.progress.percent),
|
|
44
|
+
)
|
|
45
|
+
print(done.download_url, done.actual_format, done.file_size)
|
|
46
|
+
path = vk.save_file(done, "downloads/") # named after the video title
|
|
47
|
+
print("saved to", path)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Step by step, if you'd rather poll yourself or use a webhook:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
job = vk.download("https://youtu.be/dQw4w9WgXcQ", format="audio-mp3", start_time=30, end_time=90)
|
|
54
|
+
print(job.job_id, job.title, job.rate_limit.remaining)
|
|
55
|
+
|
|
56
|
+
status = vk.get_download(job.job_id) # IN_QUEUE / IN_PROGRESS / COMPLETED / FAILED
|
|
57
|
+
status = vk.wait_for_download(job.job_id, poll_interval=3.0, timeout=600)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Pass `webhook_url="https://example.com/hooks/vidkraken"` to `download()` and Vid
|
|
61
|
+
Kraken POSTs the final status to that URL. The body is the same as `get_download()`
|
|
62
|
+
returns, so `DownloadStatus.from_dict(payload)` parses it.
|
|
63
|
+
|
|
64
|
+
## Quickstart (async)
|
|
65
|
+
|
|
66
|
+
`AsyncVidKraken` has the same methods. Each one is a coroutine.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from vidkraken import AsyncVidKraken
|
|
71
|
+
|
|
72
|
+
async def main() -> None:
|
|
73
|
+
async with AsyncVidKraken() as vk:
|
|
74
|
+
urls = ["https://youtu.be/dQw4w9WgXcQ", "https://youtu.be/9bZkp7q19f0"]
|
|
75
|
+
results = await asyncio.gather(*(vk.download_and_wait(u, format="audio") for u in urls))
|
|
76
|
+
for done in results:
|
|
77
|
+
await vk.save_file(done, "downloads/")
|
|
78
|
+
|
|
79
|
+
asyncio.run(main())
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Video info
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
info = vk.info_and_wait("https://youtu.be/dQw4w9WgXcQ", include_audio_tracks=True)
|
|
86
|
+
print(info.title, info.duration)
|
|
87
|
+
for track in info.audio or []:
|
|
88
|
+
print(track.language, track.type) # e.g. "en original", "es dubbed"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`info()`, `get_info()` and `wait_for_info()` are the separate steps behind `info_and_wait()`.
|
|
92
|
+
|
|
93
|
+
## Account and channel listing
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
me = vk.me()
|
|
97
|
+
print(me.plan.id, me.balances["bandwidth_mb"].remaining)
|
|
98
|
+
|
|
99
|
+
listing = vk.list_channel("https://www.youtube.com/@RickAstleyYT", max_videos=25) # paid add-on
|
|
100
|
+
for entry in listing.entries:
|
|
101
|
+
print(entry["id"], entry["title"])
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Saving files
|
|
105
|
+
|
|
106
|
+
`save_file(source, path=None)` streams a finished download to disk in chunks.
|
|
107
|
+
|
|
108
|
+
- `source` is a completed `DownloadStatus` or a raw `downloadUrl` string.
|
|
109
|
+
- `path` is a file path, a directory, or `None` for the current directory. When
|
|
110
|
+
you give a directory, the file is named `<title>.<ext>`: `.mp4` for video,
|
|
111
|
+
`.mp3` for `audio-mp3`, `.m4a` for `audio-aac`, and the CDN's own extension
|
|
112
|
+
for `audio`.
|
|
113
|
+
- The file is written to `<name>.part` and renamed once it is complete.
|
|
114
|
+
- Your API key is never sent to the file host.
|
|
115
|
+
|
|
116
|
+
## Responses
|
|
117
|
+
|
|
118
|
+
Responses are frozen dataclasses with snake_case attributes, for example
|
|
119
|
+
`status.download_url` and `status.progress.eta_seconds`. The original JSON is kept
|
|
120
|
+
on `.raw`, and `obj["camelCaseKey"]` / `obj.get(...)` read from it, so fields
|
|
121
|
+
added to the API later are still reachable.
|
|
122
|
+
|
|
123
|
+
## Errors
|
|
124
|
+
|
|
125
|
+
Every error inherits from `vidkraken.VidKrakenError`, which has `.status`, `.code`,
|
|
126
|
+
`.message`, `.details` and `.retry_after`. Branch on `.code`, not on the message.
|
|
127
|
+
|
|
128
|
+
| Exception | When | `code` |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `BadRequestError` | 400 | `INVALID_REQUEST` |
|
|
131
|
+
| `AuthenticationError` | 401 | `UNAUTHORIZED` |
|
|
132
|
+
| `PermissionDeniedError` | 403 | `LIMIT_REACHED` |
|
|
133
|
+
| `NotFoundError` | 404 | `NOT_FOUND`, `VIDEO_NOT_FOUND` |
|
|
134
|
+
| `RateLimitError` | 429 | `TOO_MANY_PENDING`, `RATE_LIMITED` |
|
|
135
|
+
| `ServerError` / `ServiceUnavailableError` | 5xx / 503 | `INTERNAL_ERROR` / `UNAVAILABLE` |
|
|
136
|
+
| `APIError` | any other HTTP error (base class of the above) | as sent |
|
|
137
|
+
| `APIConnectionError` / `APITimeoutError` | no HTTP response | `CONNECTION_ERROR` / `TIMEOUT` |
|
|
138
|
+
| `JobFailedError` | the job ended in `FAILED` / `failed` | `.error_code`, e.g. `AGE_RESTRICTED` |
|
|
139
|
+
| `WaitTimeoutError` | `wait_for_*(timeout=...)` ran out of time | `WAIT_TIMEOUT` |
|
|
140
|
+
| `MissingAPIKeyError` | no key given and `VIDKRAKEN_API_KEY` is unset | `MISSING_API_KEY` |
|
|
141
|
+
|
|
142
|
+
If the API sends a code this SDK doesn't know, the exception class is picked from
|
|
143
|
+
the HTTP status.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from vidkraken import JobFailedError, RateLimitError, VidKrakenError
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
done = vk.download_and_wait(url, format="1080")
|
|
150
|
+
except JobFailedError as e:
|
|
151
|
+
print(e.job_id, e.error_code, e.message)
|
|
152
|
+
if e.retryable: # only NETWORK_ERROR is worth resubmitting
|
|
153
|
+
...
|
|
154
|
+
except RateLimitError as e:
|
|
155
|
+
print("retry in", e.retry_after, "seconds")
|
|
156
|
+
except VidKrakenError as e:
|
|
157
|
+
print(e.status, e.code, e.message)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Retries
|
|
161
|
+
|
|
162
|
+
The API has **no idempotency key**. Retrying a `POST /download` after a timeout
|
|
163
|
+
or a 5xx could therefore start, and bill, a second download. The client only
|
|
164
|
+
retries when that can't happen:
|
|
165
|
+
|
|
166
|
+
| Request | Retried on |
|
|
167
|
+
|---|---|
|
|
168
|
+
| Any `GET` (status polls, `/me`, file download) | network errors, timeouts, 429, 500, 502, 503, 504 |
|
|
169
|
+
| `POST /download`, `/info`, `/list-channel` | 429 and 503 `UNAVAILABLE` only, because in both cases nothing was queued |
|
|
170
|
+
|
|
171
|
+
It never retries a POST on 500, 502, 504, a timeout or a network error. In those
|
|
172
|
+
cases you get the error and can check before resubmitting.
|
|
173
|
+
|
|
174
|
+
- Retries use exponential backoff with jitter (0.5s, 1s, 2s and so on, capped at 8s).
|
|
175
|
+
- A `Retry-After` header is honored if it is 60s or less. If it is longer, the
|
|
176
|
+
error is raised so you can decide what to do.
|
|
177
|
+
- `max_retries` defaults to 2. Set it to 0 to turn retries off:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
vk = VidKraken(max_retries=5, timeout=30.0)
|
|
181
|
+
vk = VidKraken(max_retries=0)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Rate limits
|
|
185
|
+
|
|
186
|
+
Each account can have at most 100 downloads queued or in progress at once.
|
|
187
|
+
Every `download()` result carries the headroom parsed from the IETF `RateLimit`
|
|
188
|
+
headers:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
job = vk.download(url)
|
|
192
|
+
job.rate_limit.limit # 100: how many may be pending at once
|
|
193
|
+
job.rate_limit.remaining # how many more you can queue right now
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Past the cap, `download()` raises `RateLimitError` with code `TOO_MANY_PENDING`
|
|
197
|
+
and `.retry_after` set. The error also has `.rate_limit`.
|
|
198
|
+
|
|
199
|
+
## Bring your own httpx client
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
import httpx
|
|
203
|
+
vk = VidKraken(http_client=httpx.Client(proxy="http://proxy:8080")) # proxy= needs httpx>=0.26
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The client must be an `httpx.Client` for `VidKraken` or an `httpx.AsyncClient`
|
|
207
|
+
for `AsyncVidKraken`. A client you pass in is not closed by `close()`.
|
|
208
|
+
|
|
209
|
+
## CLI
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
vidkraken download https://youtu.be/dQw4w9WgXcQ -f 1080 # waits, saves, prints the path
|
|
213
|
+
vidkraken download https://youtu.be/dQw4w9WgXcQ -f audio-mp3 -o music/
|
|
214
|
+
vidkraken download https://youtu.be/dQw4w9WgXcQ --start 30 --end 90 -o clip.mp4 -f 720
|
|
215
|
+
vidkraken download https://youtu.be/dQw4w9WgXcQ --no-wait # prints the job id
|
|
216
|
+
vidkraken download https://youtu.be/dQw4w9WgXcQ --webhook https://example.com/hook --no-wait
|
|
217
|
+
vidkraken status <job_id>
|
|
218
|
+
vidkraken info https://youtu.be/dQw4w9WgXcQ --audio-tracks
|
|
219
|
+
vidkraken me
|
|
220
|
+
vidkraken --version
|
|
221
|
+
python -m vidkraken --help
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
| Option | Applies to | Effect |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `-f/--format` | `download` | one of `1080`, `720`, `480`, `360`, `audio` (default), `audio-aac`, `audio-mp3` |
|
|
227
|
+
| `--start S` / `--end S` | `download` | trim the clip, in seconds |
|
|
228
|
+
| `--language LANG` | `download` | preferred audio track |
|
|
229
|
+
| `-o/--output PATH` | `download` | output file or directory |
|
|
230
|
+
| `--no-wait` | `download` | print the job id and exit |
|
|
231
|
+
| `--webhook URL` | `download` | URL to POST the final status to |
|
|
232
|
+
| `--timeout S`, `--poll-interval S` | `download` | how long to wait and how often to poll |
|
|
233
|
+
| `-q/--quiet` | `download` | no progress output |
|
|
234
|
+
| `--json` | every command | print one JSON document on stdout |
|
|
235
|
+
| `--api-key KEY` | every command | overrides `VIDKRAKEN_API_KEY` |
|
|
236
|
+
|
|
237
|
+
The CLI is designed to be scripted and driven by agents:
|
|
238
|
+
|
|
239
|
+
- **stdout** carries only the result: a path, a job id, a plain report, or one
|
|
240
|
+
JSON document with `--json`. On failure with `--json`, stdout gets
|
|
241
|
+
`{"error", "code", ...}`.
|
|
242
|
+
- **stderr** carries progress and errors such as
|
|
243
|
+
`vidkraken: error: TOO_MANY_PENDING: ...`.
|
|
244
|
+
- It never prompts. Color appears only when stderr is a TTY and `NO_COLOR` is unset.
|
|
245
|
+
- **Exit codes:**
|
|
246
|
+
- `0`: success
|
|
247
|
+
- `1`: API or job error. `status` also exits 1 for a `FAILED` job.
|
|
248
|
+
- `2`: usage error, including a missing API key
|
|
249
|
+
- `130`: interrupted
|
|
250
|
+
|
|
251
|
+
## Links
|
|
252
|
+
|
|
253
|
+
- Documentation: https://vidkraken.com/docs
|
|
254
|
+
- LLM-friendly reference: https://vidkraken.com/llms.txt
|
|
255
|
+
- OpenAPI spec: https://vidkraken.com/openapi.yaml
|
|
256
|
+
- Support: hello@vidkraken.com
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT © Omnivision Solutions
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vidkraken"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Official Python SDK and CLI for the Vid Kraken YouTube download API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Vid Kraken", email = "hello@vidkraken.com" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"vidkraken",
|
|
16
|
+
"youtube",
|
|
17
|
+
"youtube-downloader",
|
|
18
|
+
"video",
|
|
19
|
+
"audio",
|
|
20
|
+
"mp3",
|
|
21
|
+
"download",
|
|
22
|
+
"api",
|
|
23
|
+
"sdk",
|
|
24
|
+
"cli",
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 4 - Beta",
|
|
28
|
+
"Environment :: Console",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Programming Language :: Python",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
34
|
+
"Programming Language :: Python :: 3.9",
|
|
35
|
+
"Programming Language :: Python :: 3.10",
|
|
36
|
+
"Programming Language :: Python :: 3.11",
|
|
37
|
+
"Programming Language :: Python :: 3.12",
|
|
38
|
+
"Programming Language :: Python :: 3.13",
|
|
39
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
40
|
+
"Topic :: Multimedia :: Video",
|
|
41
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
42
|
+
"Typing :: Typed",
|
|
43
|
+
]
|
|
44
|
+
dependencies = ["httpx>=0.25"]
|
|
45
|
+
|
|
46
|
+
[project.optional-dependencies]
|
|
47
|
+
dev = ["pytest>=7", "build>=1", "twine>=5", "mypy>=1.8"]
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Homepage = "https://vidkraken.com"
|
|
51
|
+
Documentation = "https://vidkraken.com/docs"
|
|
52
|
+
Source = "https://github.com/vidkraken/vidkraken"
|
|
53
|
+
Issues = "https://github.com/vidkraken/vidkraken/issues"
|
|
54
|
+
|
|
55
|
+
[project.scripts]
|
|
56
|
+
vidkraken = "vidkraken.cli:main"
|
|
57
|
+
|
|
58
|
+
[tool.hatch.version]
|
|
59
|
+
path = "src/vidkraken/_version.py"
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.wheel]
|
|
62
|
+
packages = ["src/vidkraken"]
|
|
63
|
+
|
|
64
|
+
[tool.hatch.build.targets.sdist]
|
|
65
|
+
include = ["src/vidkraken", "tests", "README.md", "LICENSE", "pyproject.toml"]
|
|
66
|
+
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
testpaths = ["tests"]
|
|
69
|
+
|
|
70
|
+
[tool.mypy]
|
|
71
|
+
strict = true
|
|
72
|
+
files = ["src/vidkraken"]
|