course-archiver 2.2.1__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 João Caetano Cardoso Ramos
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,514 @@
1
+ Metadata-Version: 2.4
2
+ Name: course-archiver
3
+ Version: 2.2.1
4
+ Summary: Download online course videos for offline viewing using your own session cookie.
5
+ Author: joaocaetanoramos
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/joaocaetanoramos/course-archiver
8
+ Keywords: course,video,downloader,hls,m3u8,bunny,pandavideo,scaleup,hotmart,kiwify,astron,offline
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Multimedia :: Video
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: requests
24
+ Requires-Dist: beautifulsoup4
25
+ Requires-Dist: yt-dlp
26
+ Requires-Dist: pycryptodomex
27
+ Requires-Dist: rich
28
+ Dynamic: license-file
29
+
30
+ <p align="center">
31
+ <img src="course-archiver.png" alt="course-archiver" width="460">
32
+ </p>
33
+
34
+ <h3 align="center">
35
+ Multi-platform CLI to download online course videos for offline viewing 🎬
36
+ <br/>
37
+ <sub>Lossless remux (-c copy) · Parallel downloads · Pluggable platform adapters</sub>
38
+ </h3>
39
+
40
+ <p align="center">
41
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square" alt="License: MIT"></a>
42
+ &nbsp;
43
+ <a href="https://www.python.org/"><img src="https://img.shields.io/badge/Python-3.10+-blue.svg?style=flat-square&logo=python&logoColor=white" alt="Python 3.10+"></a>
44
+ &nbsp;
45
+ <a href="https://github.com/joaocaetanoramos/course-archiver/stargazers"><img src="https://img.shields.io/github/stars/joaocaetanoramos/course-archiver?style=flat-square&logo=github" alt="GitHub stars"></a>
46
+ &nbsp;
47
+ <a href="https://github.com/joaocaetanoramos/course-archiver/releases"><img src="https://img.shields.io/github/v/release/joaocaetanoramos/course-archiver?style=flat-square" alt="GitHub release"></a>
48
+ </p>
49
+
50
+ ---
51
+
52
+ ## Table of contents
53
+
54
+ - [Why?](#why)
55
+ - [Features](#features)
56
+ - [Supported platforms](#supported-platforms)
57
+ - [Installation](#installation)
58
+ - [How to obtain the cookie](#how-to-obtain-the-cookie)
59
+ - [Usage](#usage)
60
+ - [Quick start](#quick-start)
61
+ - [All arguments](#all-arguments)
62
+ - [Examples](#examples)
63
+ - [Output structure](#output-structure)
64
+ - [Architecture](#architecture)
65
+ - [Module map](#module-map)
66
+ - [Request flow](#request-flow)
67
+ - [Adding a new platform](#adding-a-new-platform)
68
+ - [Performance tuning](#performance-tuning)
69
+ - [Troubleshooting & FAQ](#troubleshooting--faq)
70
+ - [Known limitations](#known-limitations)
71
+ - [Legal notice](#legal-notice)
72
+ - [Roadmap](#roadmap)
73
+ - [License](#license)
74
+
75
+ ---
76
+
77
+ ## Why?
78
+
79
+ Watching courses online requires a stable connection, and most platforms do not provide a built-in "download for offline" option. This tool fills that gap by downloading videos **you already have access to** (via your logged-in session cookie) to your local machine, organized in folders, with metadata, at maximum quality.
80
+
81
+ It is the same model as `yt-dlp --cookies-from-browser` or `Streamlink`, but with **automatic discovery** of every course/module/lesson from the cookie, multi-platform support, and built-in organization.
82
+
83
+ ## Features
84
+
85
+ - 🎓 **Automatic course discovery** — point it at a dashboard/course URL and it walks the sidebar.
86
+ - 🌐 **Multi-platform** — pluggable adapters for Astron Members, Memberkit, Hotmart Club, Kiwify, Curseduca, plus a generic fallback.
87
+ - 🎥 **Multi-host video support** — Bunny Stream, PandaVideo, Scaleup (Smart Player), Hotmart AES-128 HLS, YouTube, direct m3u8 / mp4 — all routed through the same download path.
88
+ - ⚡ **Parallel downloads** — concurrent segment fetches per video (`--concurrent`) and concurrent lessons (`--parallel`).
89
+ - 🔁 **Resilient** — automatic retry with exponential backoff for transient network errors; auto-throttles `concurrent` when the server resets connections; auto-regenerates the cookie file on the rare "Netscape format" error.
90
+ - 📁 **Organized output** — `course-archiver/<platform>/<course>/<module>/NN - Lesson X.mp4` with embedded title/album/artist/comment metadata.
91
+ - 🪶 **Lossless** — direct remux (`-c copy`) to MP4. No re-encoding, no quality loss.
92
+ - 💻 **Clean CLI** — rich-powered progress bars (no overlap), colored status lines, per-chapter headers.
93
+ - 📊 **Size & duration estimates** — every `--ls` listing shows per-lesson size/duration plus per-chapter, per-course and grand totals; the download bar shows total size, speed, elapsed and ETA. Fully generic (probes the resolved stream: HLS bandwidth × `EXTINF` durations, or `Content-Range` for direct URLs), so it works for current and future platforms without changes. Best-effort — unknown values print `n/d` (e.g. YouTube).
94
+ - 🐢 **Throttle-aware by default** — every request to the same host is paced (per-host minimum interval) and an HTTP `429` triggers a cooldown + one retry. If a host keeps answering `429` three times in a row, the probe stops and the remaining lessons of that course show `n/d` instead of the whole course disappearing; the next course starts fresh. So large listings just get slower instead of failing.
95
+ - 📎 **Per-lesson attachments** — besides the video, downloads each lesson's supplementary files (PDFs, spreadsheets, audio…) into an `Anexos/` folder inside the chapter, with name+size dedup; complementary links (`complementaryReadings`) become `.url` shortcuts. Attachments are counted in `--ls` estimates (`+N anexo(s) · X`). Supported on Hotmart (`v1/pages/{hash}/complementary-content`) and Memberkit (best-effort scan of the lesson page); DRM-protected (lambda) files are resolved automatically. On Memberkit, the lesson description (text + links, when present) is also saved as `Descrição - <Lesson Title>.txt`.
96
+
97
+ ## Supported platforms
98
+
99
+ | Platform | Auto-discovery | Video download | Notes |
100
+ |---|---|---|---|
101
+ | **Astron Members** (`*.astronmembers.com`) | ✅ | ✅ Bunny / PandaVideo / Scaleup / YouTube | Full course + module + lesson discovery from the dashboard sidebar. |
102
+ | **Hotmart Club** (`*.hotmart.com`) | ✅ | ✅ HLS master m3u8 | AES-128 + separate audio track merged automatically. Authenticates via `Authorization: Bearer <hmVlcIntegration>` cookie from `consumer.hotmart.com`. Downloads attachments (`complementary-content`) into `Anexos/`. |
103
+ | **Kiwify** (`*.kiwify.com`) | ✅ | ✅ HLS stream / direct download | May require a refresh token from localStorage. |
104
+ | **Memberkit** (`*.memberkit.com.br`) | ✅ | ✅ HLS (Vimeo player) | Videos hosted on Vimeo with signed URLs; HLS resolved via the player config. Cookie of the domain only. Attachments: best-effort scan of downloadable files on the lesson page. |
105
+ | **Curseduca** (`*.curseduca.pro`) | ⚠️ detection only | — | Lesson listing pending. |
106
+ | **Generic video URL** | — | ✅ via `yt-dlp` | Any m3u8 / mp4 / YouTube / Vimeo / Wistia link. |
107
+
108
+ > To request a new platform, open an issue or PR — see [Adding a new platform](#adding-a-new-platform).
109
+
110
+ ## Installation
111
+
112
+ ### Recommended: install as a CLI tool (run `course-archiver` from anywhere)
113
+
114
+ ```bash
115
+ git clone https://github.com/joaocaetanoramos/course-archiver.git
116
+ cd course-archiver
117
+ pip install .
118
+ ```
119
+
120
+ This installs the `course-archiver` command on your `PATH` (typically `~/.local/bin`), so you can run it from any directory:
121
+
122
+ ```bash
123
+ course-archiver --cookies /path/to/cookie.txt "<COURSE_URL>"
124
+ ```
125
+
126
+ Dependencies (Python packages + `ffmpeg`) are installed automatically.
127
+
128
+ ### Alternative: run from source without installing
129
+
130
+ ```bash
131
+ git clone https://github.com/joaocaetanoramos/course-archiver.git
132
+ cd course-archiver
133
+ pip install -r requirements.txt
134
+ python extrator.py --cookies cookie.txt "<COURSE_URL>"
135
+ ```
136
+
137
+ ### Requirements
138
+
139
+ - **Python 3.10+**
140
+ - **ffmpeg** (`sudo dnf install ffmpeg` / `brew install ffmpeg` / `apt install ffmpeg` / `pip install static-ffmpeg`)
141
+ - **pycryptodomex** (installed automatically via `pip install .` or `pip install -r requirements.txt`) — used for native AES-128 HLS decryption.
142
+
143
+ ### Updating
144
+
145
+ If you installed via `pip install .` from a local clone:
146
+
147
+ ```bash
148
+ cd /path/to/course-archiver
149
+ git pull
150
+ pip install --upgrade .
151
+ ```
152
+
153
+ If you installed directly from GitHub (no clone):
154
+
155
+ ```bash
156
+ pip install --upgrade --force-reinstall "git+https://github.com/joaocaetanoramos/course-archiver.git"
157
+ ```
158
+
159
+ To check the installed version:
160
+
161
+ ```bash
162
+ pip show course-archiver
163
+ ```
164
+
165
+ If you run from source without installing:
166
+
167
+ ```bash
168
+ cd /path/to/course-archiver
169
+ git pull
170
+ ```
171
+
172
+ Every release is published as a **GitHub Release** (with a `vX.Y.Z` tag) — see [CHANGELOG.md](CHANGELOG.md) for what changed in each version.
173
+
174
+ (Run `pip install -r requirements.txt` again only if the dependencies change.)
175
+
176
+ ## How to obtain the cookie
177
+
178
+ The tool **requires** the cookie of your logged-in session. Authentication is your responsibility — the tool never embeds credentials.
179
+
180
+ 1. **Install a cookie-export browser extension:**
181
+ - **Chrome / Edge / Brave:** [Get cookies.txt LOCALLY](https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbldhenkjhdlgphoegibknjl) or [Cookie-Editor](https://cookie-editor.com/)
182
+ - **Firefox:** [cookies.txt](https://addons.mozilla.org/en-US/firefox/addon/cookies-txt/)
183
+ 2. **Log in** to the course platform in your browser.
184
+ 3. **Export the cookies** of that domain in **JSON** (Cookie-Editor) or **Netscape** format. The tool auto-detects both.
185
+ 4. **Pass the file** to the tool:
186
+ ```bash
187
+ course-archiver --cookies ~/Downloads/cookies.json "<COURSE_URL>"
188
+ ```
189
+
190
+ The tool also accepts a raw `Cookie:` header string (e.g., copied from DevTools) instead of a file.
191
+
192
+ > **Security:** the cookie file contains your session — treat it like a password. Do not share it. The `.gitignore` in this repo already excludes `cookie*.txt`.
193
+
194
+ > **Hotmart:** the Club gateway does not authenticate with session cookies — it requires the `Authorization: Bearer <hmVlcIntegration>` header (value of the **hmVlcIntegration** cookie on `consumer.hotmart.com`). Export cookies logged in on that domain (DevTools → Application → Cookies). The value is URL-encoded; the tool uses it **raw** as exported (decoding it corrupts the token). Export manually and pass with `--cookies` — Chrome 127+ encrypts this cookie (App-Bound Encryption), so direct browser reads may return it empty.
195
+
196
+ ## Usage
197
+
198
+ ### Quick start
199
+
200
+ ```bash
201
+ # Download a full course dashboard
202
+ course-archiver --cookies cookie.txt "<DASHBOARD_URL>"
203
+
204
+ # Download one specific course
205
+ course-archiver --cookies cookie.txt "<COURSE_URL>"
206
+
207
+ # List all available courses (id + slug of each)
208
+ course-archiver --cookies cookie.txt "<URL>" --ls courses
209
+
210
+ # List a course's modules/chapters
211
+ course-archiver --cookies cookie.txt "<URL>" --ls chapters --course <id>
212
+
213
+ # List a course's lessons (id + title)
214
+ course-archiver --cookies cookie.txt "<URL>" --ls lessons --course <id>
215
+
216
+ # List everything (courses + chapters + lessons) in one command
217
+ course-archiver --cookies cookie.txt "<URL>" --ls
218
+ ```
219
+
220
+ ### All arguments
221
+
222
+ | Flag | Description | Default |
223
+ |---|---|---|
224
+ | `url` | URL of the dashboard, course or lesson (positional, **required**) | — |
225
+ | `--cookies` `PATH` | Cookie file (JSON / Netscape) or raw `Cookie:` header (**required**) | — |
226
+ | `--output` `DIR` | Output directory | `./course-archiver` |
227
+ | `--parallel` `N` | Number of lessons downloaded concurrently | `1` |
228
+ | `--concurrent` `N` | HLS fragments downloaded in parallel **per video** | `8` |
229
+ | `--retries` `N` | Retries per lesson on transient network errors | `3` |
230
+ | `--ls` `LEVEL` | List without downloading: `courses` \| `chapters` \| `lessons`. No value lists everything (same as `lessons`). Shows size + duration per lesson and totals per chapter/course/all courses | — |
231
+ | `--course` `IDS` | Filter by course slug or id (comma-separated) | — |
232
+ | `--lesson` `IDS` | Filter by lesson id (comma-separated) | — |
233
+ | `--lang` `CODE` | UI language: `auto` \| `en` \| `pt`. `auto` uses the system locale (`LC_ALL` / `LC_MESSAGES` / `LANG`); unknown/other locales fall back to English | `auto` |
234
+ | `--ffmpeg` `PATH` | Path to ffmpeg executable | auto-detect |
235
+
236
+ ### Examples
237
+
238
+ ```bash
239
+ # Conservative: one lesson at a time, slow but safe
240
+ course-archiver --cookies cookie.txt "<URL>" --parallel 1
241
+
242
+ # Fast: 4 lessons at once, 8 parallel segment fetches each
243
+ course-archiver --cookies cookie.txt "<URL>" --parallel 4 --concurrent 8
244
+
245
+ # Aggressive (use only if your connection and the server can handle it)
246
+ course-archiver --cookies cookie.txt "<URL>" --parallel 8 --concurrent 16
247
+
248
+ # Unstable network: more retries with backoff
249
+ course-archiver --cookies cookie.txt "<URL>" --retries 5
250
+
251
+ # Filter to one course
252
+ course-archiver --cookies cookie.txt "<URL>" --course my-course-slug
253
+
254
+ # Filter to specific lessons
255
+ course-archiver --cookies cookie.txt "<COURSE_URL>" --lesson 123,456,789
256
+
257
+ # Force the UI language (English here, even if the system is in Portuguese)
258
+ course-archiver --cookies cookie.txt "<URL>" --lang en
259
+ ```
260
+
261
+ ### Language
262
+
263
+ The UI detects your system language and follows it automatically:
264
+
265
+ 1. `--lang <code>` — explicit override (highest priority).
266
+ 2. `LC_ALL` / `LC_MESSAGES` / `LANG` environment variables (e.g. `pt_BR.UTF-8` → Portuguese, `de_DE.UTF-8` → English fallback).
267
+ 3. `locale.getlocale()` — as last resort.
268
+ 4. Unknown or unset locale → **English** (the default).
269
+
270
+ Supported languages today: **English** (default) and **Portuguese**. Messages are centralized in `lib/i18n.py` — to add a language, add a dictionary to `MESSAGES`, an entry to `LANGUAGES`, and its plural-form count to `PLURAL_FORM_COUNT`. Untranslated keys gracefully fall back to English. Structural names that are part of the output layout (e.g. the `Anexos/` folder) stay fixed.
271
+
272
+ ## Output structure
273
+
274
+ Files are organized by **platform → course → module (chapter) → lesson**:
275
+
276
+ ```
277
+ course-archiver/
278
+ └── memberkit/
279
+ └── <Course Name>/
280
+ └── <Module Name>/
281
+ ├── 01 - Aula 1.1 - Introduction.mp4
282
+ ├── 02 - Aula 1.2 - Concepts.mp4
283
+ ├── 03 - Trilha: SUMMARY (sem vídeo) # skipped (Trilha/track dividers)
284
+ ├── 04 - Aula 1.3 - Deep dive.mp4
285
+ └── Anexos/ # lessons' supplementary materials
286
+ ├── Lesson 1 handout.pdf # each lesson's files
287
+ ├── Descrição - Aula 1.1.txt # lesson description (text + links), when present
288
+ └── External material.url # complementary links (.url shortcut)
289
+ ```
290
+
291
+ Other platforms create their own roots (`hotmart/`, `astron/`, `kiwify/`, `generic/`), each followed by the course name and its modules/chapters. Courses without modules skip that level.
292
+
293
+ Each chapter's attachments are downloaded into `Anexos/` inside that chapter's folder; files with the same name+size are not re-downloaded on subsequent runs.
294
+
295
+ Each `.mp4` has embedded metadata:
296
+
297
+ | Tag | Value |
298
+ |---|---|
299
+ | `title` | Lesson title |
300
+ | `album` | Course (module) name |
301
+ | `artist` | Group name |
302
+ | `comment` | Original lesson URL |
303
+
304
+ Lessons marked **"sem vídeo"** (`--ls` / live) are section/track dividers in the platform's sidebar (no actual video) and are skipped automatically.
305
+
306
+ ## Architecture
307
+
308
+ ### Module map
309
+
310
+ ```
311
+ extrator.py # CLI entry point (argparse), orchestration
312
+ lib/
313
+ cookies.py # Cookie loading: JSON / Netscape / raw Cookie header + TolerantSession (latin-1 redirect tolerance)
314
+ platforms.py # Pluggable platform adapters (detect / discover / list_lessons / extract_video)
315
+ streams.py # Resolves a video-host embed URL → master m3u8 URL (Bunny / PandaVideo / Scaleup / Hotmart / YouTube)
316
+ downloader.py # The actual download: yt-dlp native HLS, `concurrent_fragment_downloads`, `-c copy` remux, metadata tags
317
+ estimate.py # Size/duration estimates: HLS (bandwidth × EXTINF) or direct (Content-Range) probe of the resolved stream
318
+ materials.py # Attachment download: download_file (streamed .part), name+size dedup, .url shortcuts
319
+ progress.py # Rich-based progress bars (one per video, stacked, no overlap)
320
+ ```
321
+
322
+ ### Request flow
323
+
324
+ ```
325
+ +--------------------+
326
+ | 1. Parse URL & detect platform (lib/platforms.py)
327
+ | (Astron / Memberkit / Hotmart / Kiwify / Curseduca / generic)
328
+ +----------+---------+
329
+ |
330
+ v
331
+ +--------------------+
332
+ | 2. Discover courses (platform.discover)
333
+ | GET dashboard, parse sidebar <a href="curso/...">
334
+ +----------+---------+
335
+ |
336
+ v
337
+ +--------------------+
338
+ | 3. List lessons for each course (platform.list_lessons)
339
+ | GET course page, parse module + lesson <a> tags
340
+ +----------+---------+
341
+ |
342
+ v
343
+ +--------------------+
344
+ | 4. Extract video embed URL per lesson (platform.extract_video)
345
+ | Astron: GET lesson page, regex data-streaming-video / data-original-url
346
+ | Hotm: GET gateway v2/web/lessons/{hash} (Bearer) → medias[].url embed
347
+ | Anexos: platform.materials() (Hotm: v1/pages/{hash}/complementary-content) → processed at download time
348
+ +----------+---------+
349
+ |
350
+ v
351
+ +--------------------+
352
+ | 5. Resolve embed → master m3u8 (lib/streams.py)
353
+ | Bunny: fetch embed page → regex vz-*.b-cdn.net/playlist.m3u8
354
+ | Panda: build b-{pullzone}.tv.pandavideo.com.br/{id}/playlist.m3u8
355
+ | Scale: fetch embed → meta hls-prefetch-url
356
+ | Hotm: fetch cf-embed → regex vod-akm.play.hotmart.com/.../master-pkg-*.m3u8
357
+ | Member: player.vimeo.com → config → vod-adaptive.vimeocdn.com/playlist.m3u8
358
+ | YT: normalize /embed/{id} → /watch?v={id}
359
+ +----------+---------+
360
+ |
361
+ v
362
+ +--------------------+
363
+ | 6. Download via yt-dlp native (lib/downloader.py)
364
+ | - cookiefile (auto-regenerated if "Netscape format" error)
365
+ | - concurrent_fragment_downloads (auto-halved on ConnectionReset)
366
+ | - retries=10, fragment_retries=10 (internal)
367
+ | - AES-128 via pycryptodomex (native, parallel)
368
+ | - remux to MP4 via ffmpeg -c copy + metadata tags
369
+ +----------+---------+
370
+ |
371
+ v
372
+ +--------------------+
373
+ | 7. Save to course-archiver/<group>/<course>/<chapter>/<NN> - <title>.mp4
374
+ | and download attachments into <chapter>/Anexos/ (name+size dedup; links → .url)
375
+ +--------------------+
376
+ ```
377
+
378
+ ### Adding a new platform
379
+
380
+ To add support for a new platform (e.g., Udemy), implement an adapter in `lib/platforms.py`:
381
+
382
+ ```python
383
+ class UdemyPlatform(Platform):
384
+ name = "udemy"
385
+
386
+ def detect(self, url):
387
+ return "udemy.com" in urlparse(url).netloc
388
+
389
+ def discover(self, url, session):
390
+ # Return list of {"id", "title", "group", "slug", "url"}
391
+ ...
392
+
393
+ def list_lessons(self, course, session):
394
+ # Return list of {"id", "title", "url", "group", "chapter"}
395
+ ...
396
+
397
+ def extract_video(self, lesson, session):
398
+ # Return the embed URL (e.g., https://player.vimeo.com/... or direct m3u8)
399
+ ...
400
+ ```
401
+
402
+ Then register it in `PLATFORMS`:
403
+
404
+ ```python
405
+ PLATFORMS = [AstronPlatform(), HotmartPlatform(), KiwifyPlatform(), CurseducaPlatform(), UdemyPlatform()]
406
+ ```
407
+
408
+ The video resolution layer (`lib/streams.py`) is **shared** — if your platform serves a known host (Bunny / PandaVideo / Scaleup / Hotmart / YouTube), the stream is auto-resolved. If it uses a custom host, add a `resolve_yourhost` function in `streams.py`.
409
+
410
+ ## Performance tuning
411
+
412
+ The two concurrency knobs are independent and serve different purposes:
413
+
414
+ | Knob | Scope | Recommendation |
415
+ |---|---|---|
416
+ | `--concurrent N` | HLS fragments downloaded **per video** in parallel (via yt-dlp's native HLS) | `8` is a good default. Higher = more bandwidth but more server stress. The tool **auto-halves** this on connection-reset errors. |
417
+ | `--parallel N` | Number of **lessons** downloaded concurrently (thread pool) | `1` (sequential) is safest. `2–4` is reasonable. `6+` risks triggering Cloudflare rate-limits. |
418
+
419
+ ### Why "very large" videos trigger connection resets
420
+
421
+ A 30-minute 1080p video can have **300+ HLS segments**. With `concurrent_fragment_downloads: 8`, that's 8 parallel HTTP connections per video. With `--parallel 6`, you can hit **48 concurrent connections** to the same CDN host. Cloudflare's per-IP connection limits and rate-limits start kicking in, and it begins resetting connections mid-download.
422
+
423
+ The tool's defenses:
424
+ - `--concurrent 8` default (not higher).
425
+ - **Adaptive throttling**: on every `ConnectionResetError`, the tool **halves `concurrent`** automatically and shows a warning. Subsequent videos use the reduced value.
426
+ - **Retry with exponential backoff + jitter** (2s, 4s, 8s, capped at 30s) — `ConnectionResetError`, `ReadTimeout`, etc. are caught and retried up to `--retries` times.
427
+ - **yt-dlp internal retries**: `retries: 10`, `fragment_retries: 10` so individual segment failures retry inside yt-dlp before bubbling up.
428
+
429
+ ### Recommended settings by network / server
430
+
431
+ | Scenario | `--parallel` | `--concurrent` | `--retries` |
432
+ |---|---|---|---|
433
+ | Stable, fast connection | `4` | `8` | `3` |
434
+ | Unstable / mobile | `2` | `4` | `5` |
435
+ | Server rate-limits aggressively | `1` | `2` (let it auto-throttle) | `5` |
436
+ | Large downloads (>1 GB each) | `2` | `4` | `5` |
437
+
438
+ ## Troubleshooting & FAQ
439
+
440
+ ### "ERROR: '/tmp/cookies_...txt' does not look like a Netscape format cookies file"
441
+
442
+ The cookie file was valid at startup but became unreadable mid-run (rare filesystem glitch). **The tool auto-detects this and regenerates the file from the in-memory jar, then retries.** If it still fails, your cookie is likely expired — re-export it from the browser.
443
+
444
+ ### Connection reset / timeout mid-download
445
+
446
+ The tool **automatically retries** with exponential backoff (2s → 4s → 8s, capped at 30s, plus jitter). It also **halves `concurrent`** after the first reset, so subsequent videos use fewer parallel segment connections. Just re-run the tool — lessons that succeeded are skipped (resume), only the failed ones retry.
447
+
448
+ ### A lesson shows "sem vídeo" (no video)
449
+
450
+ The platform's sidebar may include section dividers ("Trilha: …", "Nota de atualização", etc.) that have no actual video. These are **skipped automatically**. If a real lesson shows "sem vídeo", the page returned no `data-streaming-video` — usually a transient rate-limit or an auth issue.
451
+
452
+ ### Hotmart: empty auth error (401) right after a fresh export
453
+
454
+ The gateway rejects requests when the `hmVlcIntegration` token is missing, expired, or was truncated during export. Re-export the cookies logged in on `consumer.hotmart.com` (not the course page domain) and pass them with `--cookies`. The tool reads the `hmVlcIntegration` cookie value and sends it as `Authorization: Bearer <value>` (used raw, URL-encoded as exported — do not decode).
455
+
456
+ ### ffmpeg not found
457
+
458
+ Install it: `sudo dnf install ffmpeg` / `brew install ffmpeg` / `apt install ffmpeg`. The tool auto-detects via `shutil.which('ffmpeg')`. If it's in a non-standard location, pass `--ffmpeg /path/to/ffmpeg`.
459
+
460
+ ### Lesson download is huge / slow
461
+
462
+ For 1-hour+ 1080p videos, expect 1–3 GB files. The download speed is bounded by:
463
+ 1. Your connection bandwidth.
464
+ 2. The CDN's per-connection speed limit (Cloudflare often caps per-connection).
465
+ 3. Your `--concurrent` setting (more segments = more bandwidth, up to your ceiling).
466
+
467
+ Reduce `--concurrent` to 2 or 4 if downloads are timing out mid-way on large files.
468
+
469
+ ### Resume support
470
+
471
+ The tool **skips** lessons whose output `.mp4` already exists (checked by file path). If a run is interrupted (Ctrl+C), re-run the same command — completed lessons are skipped, failed ones retry.
472
+
473
+ ### Curseduca lessons aren't listed
474
+
475
+ Curseduca lesson discovery is **not yet implemented** (see [ROADMAP.md](ROADMAP.md)). The tool detects the platform but prints a clear error. Workaround: extract the lesson IDs from `clas.curseduca.pro/contents/{id}` manually.
476
+
477
+ ### Kiwify: "token de autenticação não encontrado"
478
+
479
+ Kiwify's auth uses a Firebase JWT (`id_token` / `access_token` / `refresh_token`) that lives in **localStorage**, not in cookies. Export the cookie file *and* include those tokens (Cookie-Editor can export them as `__session` cookies, or extract them from DevTools → Application → Local Storage). See the platform's notes in `ROADMAP.md`.
480
+
481
+ ## Known limitations
482
+
483
+ - **HLS `.dts` extension (Bunny Stream)**: handled natively by yt-dlp, no issue.
484
+ - **Separate audio tracks (Scaleup, Hotmart)**: yt-dlp merges automatically with `bestvideo+bestaudio`.
485
+ - **DRM-protected content**: not supported (the tool only handles the video URLs returned by the platform's player, not DRM-encrypted streams).
486
+ - **Login required**: the tool does not bypass authentication. A valid cookie from your logged-in session is always required.
487
+ - **Curseduca**: lesson listing not yet implemented (see ROADMAP).
488
+ - **Live streams**: not tested; yt-dlp supports them but the tool assumes VOD (`.endswith('.m3u8')` / master playlist).
489
+
490
+ ## Legal notice
491
+
492
+ **Use this tool only to download content you have purchased and have the right to access**, for personal offline use (travel, poor connection, accessibility).
493
+
494
+ - ❌ Do **not** redistribute the downloaded material.
495
+ - ❌ Do **not** use it to access unauthorized content.
496
+ - ❌ Do **not** share your cookies.
497
+
498
+ You are solely responsible for the use you make of the tool. The authors are not responsible for misuse.
499
+
500
+ ## Roadmap
501
+
502
+ See [ROADMAP.md](ROADMAP.md) for planned features (Chrome extension, supplementary materials, more adapters, packaging).
503
+
504
+ ## License
505
+
506
+ MIT — see [LICENSE](LICENSE).
507
+
508
+ ## Disclaimer
509
+
510
+ This tool is not affiliated with any of the supported platforms. It is an independent personal-utility project.
511
+
512
+ ---
513
+
514
+ 🇧🇷 **Versão em português:** [README.pt-BR.md](README.pt-BR.md)