vidgrab 0.5.2__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.
vidgrab-0.5.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gsjonio
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.
vidgrab-0.5.2/PKG-INFO ADDED
@@ -0,0 +1,649 @@
1
+ Metadata-Version: 2.4
2
+ Name: vidgrab
3
+ Version: 0.5.2
4
+ Summary: CLI tool to download YouTube videos at maximum quality for video editing
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: gsjonio
8
+ Requires-Python: >=3.11
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Requires-Dist: rich (>=13.7.0)
16
+ Requires-Dist: typer (>=0.12.0)
17
+ Requires-Dist: yt-dlp (>=2024.1.1)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # vidgrab
21
+
22
+ **PT-BR** | [EN](#english)
23
+
24
+ ![Python](https://img.shields.io/badge/python-3.11%2B-blue)
25
+ ![License](https://img.shields.io/badge/license-MIT-green)
26
+ ![Version](https://img.shields.io/badge/version-0.5.1-orange)
27
+ [![Lint](https://github.com/gsjonio/video_grabber/actions/workflows/lint.yml/badge.svg)](https://github.com/gsjonio/video_grabber/actions/workflows/lint.yml)
28
+ [![CodeQL](https://github.com/gsjonio/video_grabber/actions/workflows/codeql.yml/badge.svg)](https://github.com/gsjonio/video_grabber/actions/workflows/codeql.yml)
29
+ [![Coverage](https://codecov.io/gh/gsjonio/video_grabber/branch/main/graph/badge.svg)](https://codecov.io/gh/gsjonio/video_grabber)
30
+ ![Last commit](https://img.shields.io/github/last-commit/gsjonio/video_grabber)
31
+
32
+ CLI para baixar vídeos do YouTube na **maior qualidade técnica disponível** — streams de vídeo e áudio separados (DASH), mesclados via FFmpeg sem nenhum reencode. Feito para quem usa vídeo como material bruto em edição.
33
+
34
+ ---
35
+
36
+ ## PT-BR
37
+
38
+ - [Como funciona](#como-funciona)
39
+ - [Funcionalidades](#funcionalidades)
40
+ - [Dependências](#dependências-externas)
41
+ - [Instalação](#instalação)
42
+ - [Uso](#uso)
43
+ - [Config file](#config-file)
44
+ - [Opções](#referência-de-opções)
45
+ - [Qualidade de código](#qualidade-de-código)
46
+
47
+ ---
48
+
49
+ ### Como funciona
50
+
51
+ A maioria das ferramentas de download aplica reencode para juntar vídeo e áudio — o que degrada a qualidade e desperdiça tempo. O vidgrab faz diferente:
52
+
53
+ ```text
54
+ YouTube → stream de vídeo (H.264 / VP9 / AV1) ─┐
55
+ → stream de áudio (AAC / Opus) ─┴→ FFmpeg mux → arquivo final
56
+ ```
57
+
58
+ Os dois streams são baixados separadamente no formato DASH (maior qualidade disponível) e mesclados em **modo cópia** — sem recodificação, sem perda de qualidade.
59
+
60
+ ---
61
+
62
+ ### Funcionalidades
63
+
64
+ | | Funcionalidade | Detalhe |
65
+ | --- | --- | --- |
66
+ | ⚡ | **Downloads paralelos** | Até 8 simultâneos via `--workers` |
67
+ | 🔁 | **Retry inteligente** | Backoff exponencial em rate-limits (até 5 tentativas) |
68
+ | 🔍 | **Dry run** | Veja título, resolução e tamanho antes de baixar |
69
+ | ⏸ | **Resume automático** | Downloads interrompidos são retomados de onde pararam |
70
+ | 📋 | **Batch download** | Arquivo `.txt` com uma URL por linha |
71
+ | 🎬 | **Playlists** | Expande e baixa todos os vídeos de uma playlist |
72
+ | 📁 | **Skip inteligente** | Detecta arquivo existente pelo ID e pula automaticamente |
73
+ | 📄 | **Metadados JSON** | Sidecar `.json` com título, canal, data, tags e mais |
74
+ | 🔒 | **Conteúdo restrito** | Suporte a cookies (Netscape) para vídeos com age-gate |
75
+ | ⚙️ | **Config file** | Defaults pessoais em `~/.config/vidgrab/config.toml` |
76
+ | 🏷 | **Nomes previsíveis** | `{data}-{slug}-{video_id}.{ext}` em todo download |
77
+ | ⚠️ | **Aviso de licença** | Alerta quando o vídeo não é Creative Commons |
78
+
79
+ ---
80
+
81
+ ### Dependências externas
82
+
83
+ | Ferramenta | Para quê | Como instalar |
84
+ | --- | --- | --- |
85
+ | **Python 3.11+** | Runtime | [python.org](https://www.python.org/downloads/) |
86
+ | **ffmpeg** | Mesclar streams de vídeo e áudio | Veja abaixo |
87
+ | **yt-dlp** | Engine de download | Instalado automaticamente via Poetry |
88
+ | **Deno** *(opcional)* | Acesso a todos os formatos do YouTube, incluindo 4K/HDR | Veja abaixo |
89
+
90
+ <details>
91
+ <summary>Instalando o ffmpeg</summary>
92
+
93
+ #### Windows
94
+
95
+ ```bash
96
+ winget install ffmpeg
97
+ ```
98
+
99
+ #### macOS
100
+
101
+ ```bash
102
+ brew install ffmpeg
103
+ ```
104
+
105
+ #### Linux (Debian/Ubuntu)
106
+
107
+ ```bash
108
+ sudo apt install ffmpeg
109
+ ```
110
+
111
+ Ou baixe o executável em <https://ffmpeg.org/download.html> e adicione ao `PATH`.
112
+
113
+ </details>
114
+
115
+ <details>
116
+ <summary>Instalando o Deno (recomendado para 4K/HDR)</summary>
117
+
118
+ Sem o Deno, o yt-dlp usa um método alternativo que pode não enxergar todos os formatos disponíveis. Com o Deno instalado, a extração é completa.
119
+
120
+ #### Windows
121
+
122
+ ```bash
123
+ winget install DenoLand.Deno
124
+ ```
125
+
126
+ #### macOS
127
+
128
+ ```bash
129
+ brew install deno
130
+ ```
131
+
132
+ #### Linux
133
+
134
+ ```bash
135
+ curl -fsSL https://deno.land/install.sh | sh
136
+ ```
137
+
138
+ </details>
139
+
140
+ ---
141
+
142
+ ### Instalação
143
+
144
+ **Via pip (recomendado):**
145
+
146
+ ```bash
147
+ pip install vidgrab
148
+ ```
149
+
150
+ **Via pipx (isolado):**
151
+
152
+ ```bash
153
+ pipx install vidgrab
154
+ ```
155
+
156
+ **From source (desenvolvimento):**
157
+
158
+ ```bash
159
+ git clone https://github.com/gsjonio/video_grabber.git
160
+ cd video_grabber
161
+ poetry install
162
+ poetry run vidgrab --help
163
+ ```
164
+
165
+ **Automated installers:**
166
+
167
+ - **Linux/macOS:** `bash install.sh`
168
+ - **Windows:** `install.bat`
169
+
170
+ These scripts check for Python 3.11+ and ffmpeg, then install via pip.
171
+
172
+ ---
173
+
174
+ ### Publicando no PyPI
175
+
176
+ Quer distribuir vidgrab para mais pessoas via `pip install vidgrab`?
177
+
178
+ Veja [PUBLISH.md](PUBLISH.md) para instruções completas:
179
+
180
+ ```bash
181
+ # Build
182
+ make build
183
+
184
+ # Configure token PyPI
185
+ poetry config pypi-token.pypi "seu-token"
186
+
187
+ # Publish
188
+ make publish
189
+ ```
190
+
191
+ ---
192
+
193
+ ### Uso
194
+
195
+ ```bash
196
+ # Vídeo único — qualidade máxima
197
+ vidgrab https://youtu.be/dQw4w9WgXcQ
198
+
199
+ # Inspecionar antes de baixar (dry run)
200
+ vidgrab https://youtu.be/dQw4w9WgXcQ --dry-run
201
+
202
+ # Limitar a 1080p
203
+ vidgrab https://youtu.be/dQw4w9WgXcQ --max-height 1080
204
+
205
+ # Salvar em diretório específico
206
+ vidgrab https://youtu.be/dQw4w9WgXcQ --output ~/Videos/raw
207
+
208
+ # Baixar playlist inteira
209
+ vidgrab "https://youtube.com/playlist?list=PLxxxx" --playlist
210
+
211
+ # Múltiplas URLs de um arquivo .txt com 5 workers
212
+ vidgrab --batch urls.txt --workers 5
213
+
214
+ # Forçar re-download mesmo se o arquivo já existir
215
+ vidgrab https://youtu.be/dQw4w9WgXcQ --force
216
+
217
+ # Conteúdo com restrição de idade
218
+ vidgrab https://youtu.be/dQw4w9WgXcQ --cookies ~/cookies.txt
219
+
220
+ # Salvar metadados em JSON
221
+ vidgrab https://youtu.be/dQw4w9WgXcQ --write-json
222
+ ```
223
+
224
+ #### Arquivo `--batch`
225
+
226
+ Uma URL por linha. Linhas com `#` são ignoradas.
227
+
228
+ ```text
229
+ # Meus vídeos
230
+ https://youtu.be/dQw4w9WgXcQ
231
+ https://youtu.be/VIDEO_ID_2
232
+ ```
233
+
234
+ ---
235
+
236
+ ### Config file
237
+
238
+ Salve seus defaults pessoais em `~/.config/vidgrab/config.toml` para não precisar repetir as flags:
239
+
240
+ ```toml
241
+ output = "~/Videos/raw"
242
+ workers = 5
243
+ max_height = 1080
244
+ ```
245
+
246
+ Flags passadas na linha de comando sempre têm prioridade sobre o config file.
247
+
248
+ ---
249
+
250
+ ### Nomeação dos arquivos
251
+
252
+ ```text
253
+ {data_upload}-{slug-do-titulo}-{video_id}.{ext}
254
+ ```
255
+
256
+ Exemplo: `20240315-never-gonna-give-you-up-dQw4w9WgXcQ.mp4`
257
+
258
+ Com `--write-json`, um sidecar `.json` é criado ao lado do vídeo:
259
+
260
+ ```json
261
+ {
262
+ "video_id": "dQw4w9WgXcQ",
263
+ "title": "Rick Astley - Never Gonna Give You Up",
264
+ "channel": "Rick Astley",
265
+ "upload_date": "2009-10-25",
266
+ "duration_seconds": 212,
267
+ "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
268
+ "description": "...",
269
+ "tags": ["pop", "80s"]
270
+ }
271
+ ```
272
+
273
+ ---
274
+
275
+ ### Container e qualidade
276
+
277
+ | Streams disponíveis | Container de saída |
278
+ | --- | --- |
279
+ | H.264 + AAC | `mp4` (sem reencode) |
280
+ | VP9 / AV1 + Opus | `mkv` (sem reencode) |
281
+
282
+ O objetivo é **nunca recodificar** — apenas mesclar os streams.
283
+
284
+ ---
285
+
286
+ ### Referência de opções
287
+
288
+ | Opção | Atalho | Descrição |
289
+ | --- | --- | --- |
290
+ | `[URLS]...` | | Uma ou mais URLs do YouTube |
291
+ | `--batch FILE` | `-b` | Arquivo `.txt` com uma URL por linha |
292
+ | `--output DIR` | `-o` | Diretório de saída (padrão: `~/Downloads`) |
293
+ | `--max-height INT` | | Limitar resolução vertical (ex.: `1080`) |
294
+ | `--playlist` | | Tratar URLs como playlists |
295
+ | `--force` | `-f` | Re-download mesmo se o arquivo já existe |
296
+ | `--cookies FILE` | | Arquivo de cookies (formato Netscape) |
297
+ | `--write-json` | | Salvar metadados em `.json` ao lado do vídeo |
298
+ | `--workers INT` | `-w` | Downloads paralelos (padrão: `3`, máx: `8`) |
299
+ | `--dry-run` | | Mostrar o que seria baixado, sem baixar |
300
+ | `--quiet` | `-q` | Suprimir toda saída exceto erros (útil para scripts) |
301
+ | `--version` | `-V` | Exibir versão |
302
+ | `--install-completion` | | Instalar autocomplete no shell atual |
303
+ | `--help` | | Exibir ajuda |
304
+
305
+ ---
306
+
307
+ ### Qualidade de código
308
+
309
+ O projeto usa uma stack completa de qualidade, integrada ao CI e aos pre-commit hooks:
310
+
311
+ | Ferramenta | Função |
312
+ | --- | --- |
313
+ | **Ruff** | Linter + formatador (9 categorias de regras) |
314
+ | **Pylint** | Análise estática avançada |
315
+ | **Mypy** (strict) | Type checking completo — `dict[str, Any]` em todas as fronteiras com yt-dlp |
316
+ | **pytest + pytest-cov** | 38 testes unitários, cobertura reportada ao Codecov |
317
+ | **pre-commit** | Ruff, Pylint, Mypy e Markdownlint rodando antes de cada commit |
318
+ | **GitHub Actions** | Lint, testes, CodeQL e release automática por tag |
319
+ | **Dependabot** | Atualizações semanais de dependências Python e Actions |
320
+
321
+ <details>
322
+ <summary>Estrutura do projeto</summary>
323
+
324
+ ```text
325
+ vidgrab/
326
+ ├── cli.py # Interface Typer — parsing de argumentos e orquestração
327
+ ├── downloader.py # Lógica core — parallelismo, retry, classificação de erros
328
+ ├── models.py # VideoMetadata e DownloadResult como dataclasses tipadas
329
+ ├── exceptions.py # Hierarquia de exceções (geo-block, age-gate, unavailable…)
330
+ └── config.py # Loader de ~/.config/vidgrab/config.toml via tomllib
331
+
332
+ tests/
333
+ ├── test_downloader.py # _slugify, _classify_error, _format_selector, DownloadConfig
334
+ ├── test_models.py # VideoMetadata serialization / deserialization
335
+ ├── test_cli.py # _collect_urls com batch e URLs posicionais
336
+ └── test_config.py # config.load com TOML válido, inválido e ausente
337
+ ```
338
+
339
+ </details>
340
+
341
+ ---
342
+
343
+ ## English
344
+
345
+ CLI to download YouTube videos at the **highest technically available quality** — separate DASH video and audio streams, muxed via FFmpeg with no re-encoding. Built for raw footage in video editing workflows.
346
+
347
+ - [How it works](#how-it-works)
348
+ - [Features](#features)
349
+ - [Dependencies](#external-dependencies)
350
+ - [Installation](#installation)
351
+ - [Usage](#usage-1)
352
+ - [Config file](#config-file-1)
353
+ - [Options](#option-reference)
354
+ - [Code quality](#code-quality)
355
+
356
+ ---
357
+
358
+ ### How it works
359
+
360
+ Most download tools re-encode to merge video and audio — degrading quality and wasting time. vidgrab does it differently:
361
+
362
+ ```text
363
+ YouTube → video stream (H.264 / VP9 / AV1) ─┐
364
+ → audio stream (AAC / Opus) ─┴→ FFmpeg mux → final file
365
+ ```
366
+
367
+ Both streams are downloaded separately in DASH format (highest quality available) and muxed in **copy mode** — no transcoding, no quality loss.
368
+
369
+ ---
370
+
371
+ ### Features
372
+
373
+ | | Feature | Detail |
374
+ | --- | --- | --- |
375
+ | ⚡ | **Parallel downloads** | Up to 8 simultaneous via `--workers` |
376
+ | 🔁 | **Smart retry** | Exponential backoff on rate-limits (up to 5 attempts) |
377
+ | 🔍 | **Dry run** | Preview title, resolution and size before downloading |
378
+ | ⏸ | **Auto resume** | Interrupted downloads pick up where they left off |
379
+ | 📋 | **Batch download** | `.txt` file with one URL per line |
380
+ | 🎬 | **Playlists** | Expands and downloads every video in a playlist |
381
+ | 📁 | **Smart skip** | Detects existing file by video ID and skips automatically |
382
+ | 📄 | **JSON metadata** | Sidecar `.json` with title, channel, date, tags and more |
383
+ | 🔒 | **Restricted content** | Cookie support (Netscape format) for age-gated videos |
384
+ | ⚙️ | **Config file** | Personal defaults at `~/.config/vidgrab/config.toml` |
385
+ | 🏷 | **Predictable names** | `{date}-{slug}-{video_id}.{ext}` on every download |
386
+ | ⚠️ | **License warning** | Alerts when a video is not under a Creative Commons license |
387
+
388
+ ---
389
+
390
+ ### External dependencies
391
+
392
+ | Tool | Purpose | How to install |
393
+ | --- | --- | --- |
394
+ | **Python 3.11+** | Runtime | [python.org](https://www.python.org/downloads/) |
395
+ | **ffmpeg** | Merge video + audio streams | See below |
396
+ | **yt-dlp** | Download engine | Installed automatically via Poetry |
397
+ | **Deno** *(optional)* | Access all YouTube formats including 4K/HDR | See below |
398
+
399
+ <details>
400
+ <summary>Installing ffmpeg</summary>
401
+
402
+ #### Windows
403
+
404
+ ```bash
405
+ winget install ffmpeg
406
+ ```
407
+
408
+ #### macOS
409
+
410
+ ```bash
411
+ brew install ffmpeg
412
+ ```
413
+
414
+ #### Linux (Debian/Ubuntu)
415
+
416
+ ```bash
417
+ sudo apt install ffmpeg
418
+ ```
419
+
420
+ Or grab the binary from <https://ffmpeg.org/download.html> and add it to your `PATH`.
421
+
422
+ </details>
423
+
424
+ <details>
425
+ <summary>Installing Deno (recommended for 4K/HDR)</summary>
426
+
427
+ Without Deno, yt-dlp falls back to an alternative extraction method that may not expose all available formats. With Deno, extraction is complete.
428
+
429
+ #### Windows
430
+
431
+ ```bash
432
+ winget install DenoLand.Deno
433
+ ```
434
+
435
+ #### macOS
436
+
437
+ ```bash
438
+ brew install deno
439
+ ```
440
+
441
+ #### Linux
442
+
443
+ ```bash
444
+ curl -fsSL https://deno.land/install.sh | sh
445
+ ```
446
+
447
+ </details>
448
+
449
+ ---
450
+
451
+ ### Installation
452
+
453
+ **Via pip (recommended):**
454
+
455
+ ```bash
456
+ pip install vidgrab
457
+ ```
458
+
459
+ **Via pipx (isolated):**
460
+
461
+ ```bash
462
+ pipx install vidgrab
463
+ ```
464
+
465
+ **From source (development):**
466
+
467
+ ```bash
468
+ git clone https://github.com/gsjonio/video_grabber.git
469
+ cd video_grabber
470
+ poetry install
471
+ poetry run vidgrab --help
472
+ ```
473
+
474
+ **Automated installers:**
475
+
476
+ - **Linux/macOS:** `bash install.sh`
477
+ - **Windows:** `install.bat`
478
+
479
+ These scripts check for Python 3.11+ and ffmpeg, then install via pip.
480
+
481
+ ---
482
+
483
+ ### Publishing to PyPI
484
+
485
+ Want to distribute vidgrab via `pip install vidgrab`?
486
+
487
+ See [PUBLISH.md](PUBLISH.md) for complete instructions:
488
+
489
+ ```bash
490
+ # Build
491
+ make build
492
+
493
+ # Configure PyPI token
494
+ poetry config pypi-token.pypi "your-token"
495
+
496
+ # Publish
497
+ make publish
498
+ ```
499
+
500
+ ---
501
+
502
+ ### Usage
503
+
504
+ ```bash
505
+ # Single video — maximum quality
506
+ vidgrab https://youtu.be/dQw4w9WgXcQ
507
+
508
+ # Inspect before downloading (dry run)
509
+ vidgrab https://youtu.be/dQw4w9WgXcQ --dry-run
510
+
511
+ # Cap at 1080p
512
+ vidgrab https://youtu.be/dQw4w9WgXcQ --max-height 1080
513
+
514
+ # Save to a specific directory
515
+ vidgrab https://youtu.be/dQw4w9WgXcQ --output ~/Videos/raw
516
+
517
+ # Download an entire playlist
518
+ vidgrab "https://youtube.com/playlist?list=PLxxxx" --playlist
519
+
520
+ # Download from a .txt file with 5 parallel workers
521
+ vidgrab --batch urls.txt --workers 5
522
+
523
+ # Force re-download even if the file already exists
524
+ vidgrab https://youtu.be/dQw4w9WgXcQ --force
525
+
526
+ # Age-restricted content
527
+ vidgrab https://youtu.be/dQw4w9WgXcQ --cookies ~/cookies.txt
528
+
529
+ # Save metadata as JSON
530
+ vidgrab https://youtu.be/dQw4w9WgXcQ --write-json
531
+ ```
532
+
533
+ #### `--batch` file format
534
+
535
+ One URL per line. Lines starting with `#` are ignored.
536
+
537
+ ```text
538
+ # My videos
539
+ https://youtu.be/dQw4w9WgXcQ
540
+ https://youtu.be/VIDEO_ID_2
541
+ ```
542
+
543
+ ---
544
+
545
+ ### Config file
546
+
547
+ Save your personal defaults at `~/.config/vidgrab/config.toml` to avoid repeating flags:
548
+
549
+ ```toml
550
+ output = "~/Videos/raw"
551
+ workers = 5
552
+ max_height = 1080
553
+ ```
554
+
555
+ Flags passed on the command line always take precedence over the config file.
556
+
557
+ ---
558
+
559
+ ### Output filename pattern
560
+
561
+ ```text
562
+ {upload_date}-{title-slug}-{video_id}.{ext}
563
+ ```
564
+
565
+ Example: `20240315-never-gonna-give-you-up-dQw4w9WgXcQ.mp4`
566
+
567
+ With `--write-json`, a sidecar `.json` is saved next to each video:
568
+
569
+ ```json
570
+ {
571
+ "video_id": "dQw4w9WgXcQ",
572
+ "title": "Rick Astley - Never Gonna Give You Up",
573
+ "channel": "Rick Astley",
574
+ "upload_date": "2009-10-25",
575
+ "duration_seconds": 212,
576
+ "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
577
+ "description": "...",
578
+ "tags": ["pop", "80s"]
579
+ }
580
+ ```
581
+
582
+ ---
583
+
584
+ ### Container and quality
585
+
586
+ | Available streams | Output container |
587
+ | --- | --- |
588
+ | H.264 + AAC | `mp4` (no re-encode) |
589
+ | VP9 / AV1 + Opus | `mkv` (no re-encode) |
590
+
591
+ The goal is to **never re-encode** — only mux the streams.
592
+
593
+ ---
594
+
595
+ ### Option reference
596
+
597
+ | Option | Short | Description |
598
+ | --- | --- | --- |
599
+ | `[URLS]...` | | One or more YouTube URLs |
600
+ | `--batch FILE` | `-b` | `.txt` file with one URL per line |
601
+ | `--output DIR` | `-o` | Output directory (default: `~/Downloads`) |
602
+ | `--max-height INT` | | Cap vertical resolution (e.g. `1080`) |
603
+ | `--playlist` | | Treat URLs as playlists |
604
+ | `--force` | `-f` | Re-download even if the file already exists |
605
+ | `--cookies FILE` | | Cookies file (Netscape format) |
606
+ | `--write-json` | | Save metadata as a `.json` sidecar next to the video |
607
+ | `--workers INT` | `-w` | Parallel downloads (default: `3`, max: `8`) |
608
+ | `--dry-run` | | Show what would be downloaded without downloading |
609
+ | `--quiet` | `-q` | Suppress all output except errors (useful for scripting) |
610
+ | `--version` | `-V` | Show version and exit |
611
+ | `--install-completion` | | Install shell autocomplete for the current shell |
612
+ | `--help` | | Show help |
613
+
614
+ ---
615
+
616
+ ### Code quality
617
+
618
+ The project uses a full quality stack, integrated into CI and pre-commit hooks:
619
+
620
+ | Tool | Role |
621
+ | --- | --- |
622
+ | **Ruff** | Linter + formatter (9 rule categories) |
623
+ | **Pylint** | Advanced static analysis |
624
+ | **Mypy** (strict) | Full type checking — `dict[str, Any]` at every yt-dlp boundary |
625
+ | **pytest + pytest-cov** | 38 unit tests, coverage reported to Codecov |
626
+ | **pre-commit** | Ruff, Pylint, Mypy and Markdownlint run before every commit |
627
+ | **GitHub Actions** | Lint, tests, CodeQL and automatic release on tag push |
628
+ | **Dependabot** | Weekly updates for Python deps and Actions |
629
+
630
+ <details>
631
+ <summary>Project structure</summary>
632
+
633
+ ```text
634
+ vidgrab/
635
+ ├── cli.py # Typer interface — argument parsing and orchestration
636
+ ├── downloader.py # Core logic — parallelism, retry, typed error classification
637
+ ├── models.py # VideoMetadata and DownloadResult as typed dataclasses
638
+ ├── exceptions.py # Exception hierarchy (geo-block, age-gate, unavailable…)
639
+ └── config.py # ~/.config/vidgrab/config.toml loader via tomllib
640
+
641
+ tests/
642
+ ├── test_downloader.py # _slugify, _classify_error, _format_selector, DownloadConfig
643
+ ├── test_models.py # VideoMetadata serialization / deserialization
644
+ ├── test_cli.py # _collect_urls with batch and positional URLs
645
+ └── test_config.py # config.load with valid, invalid and missing TOML
646
+ ```
647
+
648
+ </details>
649
+