yt-dlp 2.7.2

๐ŸŽฌ๏ธ A Rust library (with auto dependencies installation) for Youtube downloading
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
# ๐Ÿค Contributing to yt-dlp

Thank you for your interest in contributing! This guide will help you understand our codebase conventions and write code that feels like it belongs here. Every rule exists because it's already applied consistently across the entire codebase โ€” please follow them to keep things uniform.

---

## ๐Ÿ“‹ Table of Contents

- [๐Ÿš€ Getting Started]#-getting-started
- [๐Ÿ—๏ธ Project Architecture]#๏ธ-project-architecture
- [โœ๏ธ Code Style]#๏ธ-code-style
  - [๐Ÿช† Nesting depth]#-nesting-depth
- [๐Ÿšจ Error Handling]#-error-handling
- [๐Ÿ”ง Builder Patterns]#-builder-patterns
- [๐Ÿ“ฆ Model & Data Types]#-model--data-types
- [๐Ÿงฌ Trait Design]#-trait-design
- [๐Ÿ”’ Shared State & Concurrency]#-shared-state--concurrency
- [โšก Async Programming]#-async-programming
- [๐Ÿ”” Event System]#-event-system
- [๐ŸŽฏ Feature Flags]#-feature-flags
- [๐Ÿ“ Tracing & Logging]#-tracing--logging
- [๐Ÿ“– Documentation]#-documentation
- [๐Ÿ” Contributing to media-seek]#-contributing-to-media-seek
- [โœ… Verification Checklist]#-verification-checklist

---

## ๐Ÿš€ Getting Started

### Prerequisites

- **Rust** (edition 2024) โ€” install via [rustup]https://rustup.rs/
- **Rust nightly** (for rustfmt) โ€” `rustup toolchain install nightly --component rustfmt`
- **cargo-hack** โ€” `cargo install cargo-hack`
- **cargo-deny** โ€” `cargo install cargo-deny`

### Running the checks

Every PR must pass these commands:
```bash
# Lint all features combined (all backends in a single pass)
cargo clippy --workspace --all-features -- -D warnings

# Check formatting (requires nightly)
cargo +nightly fmt --all -- --check

# Run all doc-tests (workspace-wide)
cargo test --doc --workspace --all-features

# Check dependencies (licenses, advisories, bans)
cargo deny check

# Check for unused dependencies
cargo machete
```

### Branch workflow

1. Fork the repository and create a branch from `develop`
2. Make your changes following the guidelines below
3. Run the verification checks above
4. Open a PR against `develop`

---

## ๐Ÿ—๏ธ Project Architecture

The codebase is a Cargo workspace with two crates. Understanding the layout is essential before making changes:

```
yt-dlp/
โ”œโ”€โ”€ Cargo.toml               โ† workspace manifest ([workspace] + [package])
โ”œโ”€โ”€ src/                     โ† yt-dlp crate source
โ””โ”€โ”€ crates/
    โ””โ”€โ”€ media-seek/          โ† standalone container index parsing crate
        โ”œโ”€โ”€ Cargo.toml
        โ””โ”€โ”€ src/
            โ”œโ”€โ”€ lib.rs       โ€” RangeFetcher trait + parse() dispatch
            โ”œโ”€โ”€ error.rs     โ€” Error enum + Result<T> alias
            โ”œโ”€โ”€ detect.rs    โ€” magic-byte format detection
            โ”œโ”€โ”€ index.rs     โ€” ContainerIndex, SegmentEntry, Inner
            โ”œโ”€โ”€ audio/       โ€” mp3, ogg, flac, pcm (wav+aiff), adts
            โ””โ”€โ”€ video/       โ€” mp4, webm, flv, avi, ts
```

The `yt-dlp` crate module hierarchy:

```
src/
โ”œโ”€โ”€ lib.rs              # ๐Ÿ  Crate root โ€” Downloader struct lives here (NOT in a submodule)
โ”œโ”€โ”€ prelude.rs          # ๐Ÿ“ค Convenience re-exports for `use yt_dlp::prelude::*`
โ”œโ”€โ”€ macros.rs           # ๐Ÿงฉ Macros: youtube!, ytdlp_args!, install_libraries!, ternary!
โ”œโ”€โ”€ error.rs            # ๐Ÿšจ Single unified Error enum + type Result<T>
โ”‚
โ”œโ”€โ”€ client/             # ๐Ÿ”ง Builder, download builder, proxy, deps, stream orchestration
โ”‚   โ”œโ”€โ”€ builder.rs      #    DownloaderBuilder (fluent builder)
โ”‚   โ”œโ”€โ”€ download_builder.rs  # DownloadBuilder<'a> (fluent download API)
โ”‚   โ”œโ”€โ”€ proxy.rs        #    ProxyConfig, ProxyType
โ”‚   โ”œโ”€โ”€ deps/           #    ๐Ÿ“ฆ Auto-installation of yt-dlp & ffmpeg from GitHub releases
โ”‚   โ””โ”€โ”€ streams/        #    ๐Ÿงฉ Format selection (VideoSelection trait), orchestration
โ”‚
โ”œโ”€โ”€ download/           # ๐Ÿ“ฅ DownloadManager, Fetcher, segment-based parallel downloads
โ”œโ”€โ”€ events/             # ๐Ÿ”” EventBus, DownloadEvent, EventFilter, hooks, webhooks
โ”œโ”€โ”€ executor/           # โš™๏ธ Process runner, FfmpegArgs builder, temp-file+rename
โ”œโ”€โ”€ extractor/          # ๐Ÿ“ก VideoExtractor trait, Youtube & Generic extractors
โ”œโ”€โ”€ metadata/           # ๐Ÿท๏ธ MP3/MP4/FFmpeg/Lofty metadata writing, chapter injection
โ”œโ”€โ”€ model/              # ๐Ÿ“Š Data types: Video, Format, Chapter, Playlist, Caption, etc.
โ”‚   โ”œโ”€โ”€ utils/          #    Serde helpers
โ”‚   โ””โ”€โ”€ selector.rs     #    VideoQuality, AudioQuality, StoryboardQuality enums
โ”œโ”€โ”€ cache/              # ๐Ÿ” VideoCache, DownloadCache, PlaylistCache (feature-gated)
โ”‚   โ””โ”€โ”€ backend/        #    Backend trait + implementations (memory/moka, json, redb, redis)
โ”œโ”€โ”€ live/               # ๐Ÿ”ด Live recording/streaming (features: live-recording, live-streaming)
โ”‚   โ”œโ”€โ”€ hls.rs          #    HLS manifest parsing via m3u8-rs
โ”‚   โ”œโ”€โ”€ recording.rs    #    Reqwest-based HLS segment recorder (primary)
โ”‚   โ””โ”€โ”€ ffmpeg_recording.rs  # FFmpeg-based recorder (fallback)
โ”œโ”€โ”€ stats/              # ๐Ÿ“Š StatisticsTracker, GlobalSnapshot (feature: statistics)
โ””โ”€โ”€ utils/              # ๐Ÿ› ๏ธ fs, http, platform, retry, validation, url_expiry, subtitle
```

### ๐Ÿ“ Module conventions

| Rule | Example |
|------|---------|
| Each directory has a `mod.rs` that declares submodules and re-exports public types | `pub use video::VideoCache;` in `cache/mod.rs` |
| `lib.rs` re-exports the most-used types to crate root | `pub use client::{DownloadBuilder, DownloaderBuilder};` |
| `prelude.rs` re-exports everything for basic usage | Feature-gated with `#[cfg(feature = "...")]` |
| Module-level `//!` doc comments on every `mod.rs` | Describes the module's purpose and architecture |
| Feature-gated modules in `lib.rs` | `#[cfg(feature = "statistics")] pub mod stats;` |

### ๐Ÿ‘๏ธ Visibility rules

| Visibility | When to use | Example |
|-----------|-------------|---------|
| `pub` | Types and methods exposed to library users | `pub fn fetch_video_infos(...)` |
| `pub(crate)` | All fields of `Downloader`, internal helpers | `pub(crate) youtube_extractor: Youtube` |
| Private | Implementation details | `fn audio_codec_for_mux(...)` |

> ๐Ÿ’ก Builder struct fields are always **private**. `TypedBuilder` config struct fields are always **`pub`**.

---

## โœ๏ธ Code Style

### ๐ŸŒ Language

All comments, docs, variable names, error messages, and log messages must be in **English**. No exceptions.

### ๐Ÿ“ฅ Imports

```rust
// โœ… GOOD โ€” All imports at the top of the file
use crate::error::Result;
use crate::model::Video;
use std::path::PathBuf;

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

// โŒ BAD โ€” Never import inside function bodies
fn my_function() {
    use std::collections::HashMap; // WRONG
}
```

> ๐Ÿงฉ **Exception**: inside `macro_rules!` definitions, `$crate::` paths may require local imports.

### ๐Ÿท๏ธ Naming conventions

| Item | Convention | Example |
|------|-----------|---------|
| Variables & functions | `snake_case` | `download_video`, `is_ready` |
| Types & structs | `PascalCase` | `DownloaderBuilder`, `VideoQuality` |
| Constants | `SCREAMING_SNAKE_CASE` | `DEFAULT_RETRY_ATTEMPTS`, `FORMAT_URL_LIFETIME` |
| Constants prefix | Context prefix | `DEFAULT_`, `CONSERVATIVE_`, `BALANCED_`, `AGGRESSIVE_` |
| Booleans | Intent-driven | `is_ready`, `has_data`, `include_full_data` |

### ๐Ÿ”€ Conditional logic

**No more than two raw conditions directly in an `if` (or `while`) guard.** When three or more sub-expressions are combined with `&&` or `||`, each sub-expression must first be bound to a short, descriptively-named `let` boolean before the guard. Boolean variable names must be short and intent-revealing: `is_year`, `is_endlist`, `is_timeout`, etc.

```rust
// โœ… single condition โ€” OK
if probe.len() < 4 { โ€ฆ }

// โœ… two raw conditions combined โ€” OK
if e.starts_with("HTTP 4") && !e.starts_with("HTTP 429") { โ€ฆ }

// โœ… named booleans combined โ€” OK (required when โ‰ฅ 3 conditions)
let is_timeout = error.is_timeout();
let is_connect = error.is_connect();
let is_request = error.is_request();
if is_timeout || is_connect || is_request { โ€ฆ }

// โŒ three or more raw expressions inline โ€” NOT OK
if error.is_timeout() || error.is_connect() || error.is_request() { โ€ฆ }
```

### ๐Ÿšซ Lint suppressions

`#[allow(โ€ฆ)]` attributes are **forbidden** in this codebase, with one explicit exception:

- `#[allow(clippy::large_enum_variant)]` on `DownloadEvent` โ€” boxing all variants for one large variant would add unnecessary indirection throughout the event system.

**Fix the root cause instead of suppressing the lint:**

| Lint | Preferred fix |
|---|---|
| `dead_code` | Remove the item, or gate with `#[cfg(feature = "โ€ฆ")]` |
| `unreachable_code` | Use `unreachable!("โ€ฆ")` or gate the fallback with `#[cfg(not(โ€ฆ))]` |
| `clippy::too_many_arguments` | Group related parameters into a dedicated struct |
| `unused_*` | Remove unused imports/variables, or prefix with `_` for intentional non-use |

### ๐Ÿช† Nesting depth

**Maximum two levels of nesting inside any function body.** Each loop (`for`, `while`, `loop`), conditional (`if`, `else if`, `match`), or closure that contains control flow counts as one level. Exceeding two levels raises the [SonarCloud Cognitive Complexity](https://www.sonarsource.com/docs/CognitiveComplexity.pdf) above the enforced threshold of 15 and will block your PR.

When a third level is needed, **extract the inner logic into a private helper function** that returns an `Option`, `Result`, or a dedicated struct.

```rust
// โŒ BAD โ€” three levels of nesting (loop โ†’ if โ†’ if)
fn scan_tags(probe: &[u8]) {
    while let Some(tag) = next_tag(probe) {           // level 1
        if tag.kind == TagKind::Video {               // level 2
            if tag.frame_type == FrameType::Key {     // level 3 โ† NOT allowed
                keyframes.push(tag.offset);
            }
        }
    }
}

// โœ… GOOD โ€” max two levels; the inner predicate is extracted
fn is_video_keyframe(tag: &Tag) -> bool {
    tag.kind == TagKind::Video && tag.frame_type == FrameType::Key
}

fn scan_tags(probe: &[u8]) {
    while let Some(tag) = next_tag(probe) {           // level 1
        if is_video_keyframe(&tag) {                  // level 2
            keyframes.push(tag.offset);
        }
    }
}
```

The same rule applies to `match` arms that contain their own `if`/`loop`/`match`:

```rust
// โŒ BAD โ€” match arm body itself opens a new level
match block_type {
    BlockType::StreamInfo => {
        if block_len >= MIN_SIZE {    // level 3 when already inside a loop + match
            parse_stream_info(block);
        }
    }
}

// โœ… GOOD โ€” delegate to a helper that handles the guard internally
match block_type {
    BlockType::StreamInfo => parse_stream_info(block), // helper does its own guard
}
```

| Rule | Detail |
|------|--------|
| Hard limit | 2 nesting levels per function |
| What counts | `for`, `while`, `loop`, `if`/`else if`/`else`, `match`, closures with control flow |
| Remedy | Extract inner body into a private `fn`, or use early-return / guard-clause patterns |
| SonarCloud | Max Cognitive Complexity per function: **15** |

### ๐ŸŽฏ Parameter types

Use the most appropriate type for public API parameters:

```rust
// โœ… GOOD โ€” Flexible public API
pub fn new(url: impl Into<String>) -> Self { ... }
pub fn with_cookies(mut self, path: impl Into<PathBuf>) -> Self { ... }
pub fn input(mut self, path: impl AsRef<str>) -> Self { ... }

// โŒ BAD โ€” Too restrictive
pub fn new(url: String) -> Self { ... }
pub fn new(url: &str) -> Self { ... }
```

For internal functions, use the most optimized type for the operations applied:
- `&str` if you only read the string
- `String` if you need ownership
- `&Path` if you only read the path
- `PathBuf` if you need ownership

### ๐Ÿงช Testing

There are **no `#[cfg(test)]` modules** in `src/`. No tests live in `tests/common/` (only shared helpers).

**Test harnesses** โ€” three separate binaries under `tests/`:

| Harness | Command | Scope |
|---------|---------|-------|
| Unit | `cargo test --test unit --all-features` | Pure logic, no I/O, no network |
| Integration | `cargo test --test integration --all-features` | wiremock servers, tempdir I/O, async flows |
| E2E | `cargo test --test e2e --all-features -- --test-threads=1` | Full download pipeline with wiremock |
| Doctests | `cargo test --doc --workspace` | Code examples in rustdoc |

**Directory conventions** โ€” test directories mirror `src/` module hierarchy:
```
tests/unit/model/       โ† matches src/model/
tests/unit/download/    โ† matches src/download/
tests/integration/cache/ โ† matches src/cache/
```
Create a subdirectory when a domain has โ‰ฅ 2 test files.

**Adding a new test:**
1. Create the test file in the appropriate subdirectory (e.g. `tests/unit/download/new_test.rs`)
2. Register it in the harness entry point (`tests/unit.rs`) with `#[path = "unit/download/new_test.rs"] mod new_test;`
3. Feature-gated tests use `#[cfg(feature = "...")]` on the module declaration in the entry point

**Conventions:**
- Test names follow `fn verb_noun_condition()` (e.g. `fn parse_format_returns_video_type()`)
- All test output goes to `tempfile::tempdir()`, never to project root
- Use `assert_matches!` for error variant checks, `pretty_assertions` for struct comparisons
- Mock servers use `wiremock::MockServer` (dev-dependency)
- Fixtures: JSON in `tests/fixtures/json/`, media in `tests/fixtures/media/`
- ๐Ÿ“Š **Benchmarks** โ€” `benches/benchmarks.rs` with [criterion]https://crates.io/crates/criterion
- ๐Ÿงช **Integration examples** โ€” `examples/` directory

### ๐Ÿ”ข Magic Numbers & Constants

**Never use raw numeric or byte literals in logic.** Every literal must be extracted to a named `const` at the top of the file.

```rust
// โœ… GOOD โ€” Named constants with clear intent
/// ID3v2 header fixed size in bytes.
const ID3V2_HEADER_SIZE: usize = 10;
/// Maximum bytes to scan for the first sync word.
const SYNC_SEARCH_LIMIT: usize = 8192;

fn skip_id3(data: &[u8]) -> usize {
    if data.len() < ID3V2_HEADER_SIZE { return 0; }
    // ...
}

// โŒ BAD โ€” What does 10 mean? What about 8192?
fn skip_id3(data: &[u8]) -> usize {
    if data.len() < 10 { return 0; }
    // ...
}
```

| Rule | Detail |
|------|--------|
| Location | File top, before any `fn` or `impl` |
| Naming | `SCREAMING_SNAKE_CASE` with context prefix (`DEFAULT_`, `BALANCED_`, etc.) |
| Lookup tables | Bitrate tables, sample rate tables โ†’ `const` arrays at file top |
| Magic bytes | `const EBML_MAGIC: &[u8] = &[0x1A, 0x45, 0xDF, 0xA5];` โ€” never raw in conditionals |

### ๐Ÿ“ฆ Return Types (No Tuples)

**Never return tuples from functions.** Use a named struct instead โ€” even for two fields.

```rust
// โœ… GOOD โ€” Clear field semantics at call site
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ByteRange {
    pub start: u64,
    pub end: u64,
}

fn find_range(&self, time: f64) -> Option<ByteRange> {
    // ...
}

// โŒ BAD โ€” Opaque meaning, easy to swap fields
fn find_range(&self, time: f64) -> Option<(u64, u64)> {
    // ...
}
```

| Rule | Detail |
|------|--------|
| Scope | Module-private structs are fine if only used internally |
| Derives | At minimum `Debug, Clone` โ€” add `Copy, PartialEq, Eq` when applicable |
| Fields | Descriptive names that convey semantics |

### ๐Ÿ”— Function Call & Type Qualification

Qualify function calls with **at most one `::`** โ€” import deeper paths at the top of the file.

```rust
// โœ… GOOD โ€” Import then use short paths
use reqwest::header::{self, HeaderMap, HeaderValue};

let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain"));

// โŒ BAD โ€” Double-qualified paths
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(reqwest::header::CONTENT_TYPE, reqwest::header::HeaderValue::from_static("text/plain"));
```

| Rule | Example |
|------|---------|
| `Self::` for associated fns in `impl` | `Self::new()`, `Self::parse_header(data)` |
| `module::function()` | `detect::probe(data)` |
| `Type::method()` | `String::from("hello")` |
| Import heavily-used types directly | `use std::collections::HashMap;` then `HashMap::new()` |

---

## ๐Ÿšจ Error Handling

We use a **single unified error type** in `src/error.rs`. Never introduce new error enums (except `HookError` which already exists for hook-specific failures).

### Rules

| Rule | Detail |
|------|--------|
| **One `Error` enum** | All variants in one enum, grouped by `// === Category ===` comment banners |
| **Type alias** | `pub type Result<T> = std::result::Result<T, Error>;` โ€” import as `use crate::error::Result;` |
| **Structured fields** | Every variant uses named fields (`operation`, `url`, `reason`, `path`, `source`) โ€” never just a string |
| **`#[source]`** | Always on the inner error field for proper chaining |
| **Helper constructors** | `Error::io(...)`, `Error::http(...)` โ€” each logs `tracing::warn!`/`tracing::error!` before constructing |
| **`From` impls** | For `std::io::Error`, `reqwest::Error`, `serde_json::Error`, `JoinError`, `ZipError` โ€” each logs with `"(automatic conversion)"` suffix |
| **Parameter style** | `impl Into<String>` โ€” not concrete types |
| **Feature-gated** | `#[cfg(feature = "cache-redb")] Database { ... }`, `#[cfg(feature = "cache-redis")] Redis { ... }` |
| **No `anyhow`** | Always use the crate's own `Error` / `Result` |

### Example: Adding a new error variant

```rust
// In src/error.rs, add to the appropriate category section:

// ==================== Video & Format Errors ====================

/// My new error description.
#[error("Something failed for {video_id}: {reason}")]
MyNewError {
    video_id: String,
    reason: String,
},
```

And add a helper constructor:
```rust
pub fn my_new_error(video_id: impl Into<String>, reason: impl Into<String>) -> Self {
    let video_id = video_id.into();
    let reason = reason.into();

    tracing::warn!(video_id = video_id, reason = reason, "Something failed");

    Self::MyNewError { video_id, reason }
}
```

---

## ๐Ÿ”ง Builder Patterns

Two builder styles coexist โ€” use the right one for the right job:

### A) Manual builder (consuming `mut self`)

Used for: `DownloaderBuilder`, `DownloadBuilder`, `WebhookConfig`, `FfmpegArgs`

```rust
// โœ… Builder methods prefixed with `with_` and consuming `mut self`
pub fn with_timeout(mut self, timeout: Duration) -> Self {
    self.timeout = timeout;
    self
}

// โœ… Terminal method
pub async fn build(self) -> Result<Downloader> { ... }
```

| Rule | Detail |
|------|--------|
| Method prefix | `with_` (e.g. `with_args`, `with_timeout`, `with_proxy`, `with_cache`) |
| Self parameter | Always `mut self` (consuming) โ€” **never `&mut self`** |
| Terminal method | `.build()` or `.execute()` |
| Field visibility | Private |

### B) `TypedBuilder` derive

Used for: config structs (`ManagerConfig`, `RetryPolicy`, `ExpiryConfig`)

```rust
#[derive(Debug, Clone, TypedBuilder)]
pub struct ManagerConfig {
    #[builder(default = SpeedProfile::default().max_concurrent_downloads())]
    pub max_concurrent_downloads: usize,
}
```

| Rule | Detail |
|------|--------|
| Field visibility | `pub` |
| Defaults | `#[builder(default = ...)]` |

### C) Post-build mutation on `Downloader`

After `.build()`, use `set_*`/`add_*` methods (not `with_*`) to mutate the `Downloader` instance:

```rust
downloader.set_user_agent("my-agent");
downloader.set_timeout(Duration::from_secs(30));
downloader.set_args(vec!["--no-playlist".into()]);
downloader.add_arg("--flat-playlist");
downloader.set_cookies("cookies.txt");
downloader.set_cookies_from_browser("chrome");
downloader.set_netrc();
```

| Rule | Detail |
|------|--------|
| Self parameter | `&mut self` (borrowing) โ€” returns `&mut Self` for chaining |
| Prefix for replacing | `set_` (e.g. `set_cookies`, `set_user_agent`, `set_timeout`) |
| Prefix for appending | `add_` (e.g. `add_arg`) |

> ๐Ÿ’ก **Don't confuse** builder `with_*` methods (consuming `mut self`, used before `.build()`) with post-build `set_*`/`add_*` methods (borrowing `&mut self`, used after `.build()`).

---

## ๐Ÿ“ฆ Model & Data Types

### Standard derive sets

| Type | Derives |
|------|---------|
| **Simple enums** | `Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize` + `Default` with `#[default]` |
| **Complex structs** (with `f64`) | `Debug, Clone, PartialEq, Serialize, Deserialize` โ€” manual `Eq`/`Hash` |
| **Simple structs** (no floats) | `Debug, Clone, PartialEq, Eq, Serialize, Deserialize` |

### Serde patterns

| Pattern | Usage |
|---------|-------|
| `#[serde(flatten)]` | Struct composition (e.g. `Format` flattens `CodecInfo`, `VideoResolution`, etc.) |
| `#[serde(rename = "...")]` | Field name mapping from JSON (`"timestamp"`, `"acodec"`) |
| `#[serde(rename_all = "snake_case")]` | Enum variant renaming |
| `#[serde(default)]` | Optional collections and fields |
| `#[serde(other)]` | `Unknown` variant for forward compatibility |
| `#[serde(skip)]` | Derived/internal fields (e.g. `video_id` on `Format`) |
| `json_none` deserializer | Turns `"none"` strings to `Option::None` (in `model/utils/serde.rs`) |
| `#[serde_as(deserialize_as = "DefaultOnNull")]` | From `serde_with`, for nullable JSON fields |
| Custom `Deserialize` visitor | Polymorphic types (e.g. `DrmStatus` accepts bool or string) |
| `ordered_float::OrderedFloat<f64>` | Only when `f64` needs `Hash`/`Eq` |

### ๐Ÿ–จ๏ธ Display format

**Always** use the format `TypeName(key=value, key=value)`:

```rust
impl fmt::Display for Video {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Video(id={}, title={:?}, channel={:?}, formats={})",
            self.id, self.title, self.channel.as_deref().unwrap_or("Unknown"), self.formats.len())
    }
}
```

| Rule | Detail |
|------|--------|
| Only essential fields | Never full serialization |
| `Option` fields | `as_deref().unwrap_or("none")` or `unwrap_or("unknown")` |
| Enum constant variants | `f.write_str("VariantName")` |
| Enum variants with fields | `write!(f, "Variant(key={})", val)` |

### ๐Ÿ”‘ Custom `Hash` implementations

Hash **only identity fields** โ€” not all struct fields:

```rust
impl Hash for Video {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.id.hash(state);
        self.title.hash(state);
        self.channel.hash(state);
        self.channel_id.hash(state);
    }
}
```

---

## ๐Ÿงฌ Trait Design

### Which pattern to use?

| Pattern | When | Example |
|---------|------|---------|
| `#[async_trait]` | Trait used as `dyn Trait` (trait objects) | `VideoExtractor`, `EventHook` |
| RPITIT (`impl Future + Send`) | Dispatched via concrete enum, never `dyn` | Cache backend traits |
| `DynClone + clone_trait_object!` | Need to clone trait objects | `EventHook` |
| `Downcast + impl_downcast!` | Runtime downcasting of trait objects | `VideoExtractor` |

### `#[async_trait]` example

```rust
#[async_trait]
pub trait VideoExtractor: Downcast + Send + Sync + fmt::Debug {
    async fn fetch_video(&self, url: &str) -> Result<Video>;
    fn name(&self) -> ExtractorName;
    fn supports_url(&self, url: &str) -> bool;
}
impl_downcast!(VideoExtractor);
```

### RPITIT example

```rust
pub trait VideoBackend: Send + Sync + std::fmt::Debug {
    fn get(&self, url: &str) -> impl Future<Output = Result<Option<Video>>> + Send;
    fn put(&self, url: String, video: Video) -> impl Future<Output = Result<()>> + Send;
}
```

> ๐Ÿ“– Trait method declarations carry **full rustdoc**; implementations may add only a brief clarifying comment.

---

## ๐Ÿ”’ Shared State & Concurrency

### Primitives used

| Primitive | Purpose |
|-----------|---------|
| `Arc<reqwest::Client>` | Shared HTTP client with connection pooling |
| `Arc<Mutex<...>>` | Mutable shared state (download queues, task maps, next_id counter) |
| `Arc<Semaphore>` | Concurrency limit for parallel downloads |
| `Arc<AtomicU64>` / `Arc<AtomicBool>` | Lock-free counters and flags |
| `Arc<RwLock<...>>` | Read-heavy shared state (hook registry, stats, webhooks) |
| `Arc<DownloadEvent>` | Events in broadcast channel (efficient cloning) |
| `Arc<dyn Fn(...) + Send + Sync>` | Callbacks and filter predicates |
| `tokio_util::sync::CancellationToken` | Graceful shutdown |

### โš ๏ธ Important rules

| Rule | Detail |
|------|--------|
| Async locks | Use `tokio::sync::Mutex` and `tokio::sync::RwLock` |
| Sync locks | `std::sync::Mutex` **only** for progress counters and non-async contexts |
| Lock safety | **Never** hold a `tokio` lock across `.await` points |
| Simple counters | Prefer `Arc<AtomicU64>` over `Arc<Mutex<u64>>` |
| Caches on Downloader | `Option<Arc<VideoCache>>` |

---

## โšก Async Programming

| Rule | Detail |
|------|--------|
| Runtime | `tokio` (multi-threaded) |
| Task spawning | `tokio::spawn` for concurrency |
| Multiple tasks | `tokio::select!` for managing cancellations |
| Structured concurrency | Prefer scoped tasks and clean cancellation paths |
| Timeouts | `tokio::time::timeout` with kill on timeout |
| Blocking work | Offload to `tokio::task::spawn_blocking` (used for `serde_json::from_reader`, CPU-intensive parsing) |
| Time operations | `tokio::time::sleep` and `tokio::time::interval` |
| HTTP | `reqwest` with `Arc<Client>` connection pooling |

### Channels

| Channel | Usage |
|---------|-------|
| `tokio::sync::mpsc` | Webhook delivery queue (bounded, backpressure) |
| `tokio::sync::broadcast` | Event broadcasting to multiple subscribers |
| `tokio::sync::oneshot` | One-time task communication |

---

## ๐Ÿ”” Event System

The event system lives in `src/events/` and follows a three-phase delivery pattern:

### Architecture

| Component | Role |
|-----------|------|
| `EventBus` | Wraps `broadcast::Sender<Arc<DownloadEvent>>` |
| `DownloadEvent` | Large enum โ€” **all variants use named fields** (no tuple variants) |
| `EventFilter` | Predicate-based with `Vec<Arc<dyn Fn(&DownloadEvent) -> bool + Send + Sync>>` |
| `HookRegistry` | `Arc<RwLock<Vec<Box<dyn EventHook>>>>` |
| `simple_hook!` | Macro to create hooks from closures |

### Event emission order (in `Downloader::emit_event()`)

1. ๐Ÿช **Hooks** โ€” with timeout (`#[cfg(feature = "hooks")]`)
2. ๐Ÿ“ก **Webhooks** โ€” non-blocking (`#[cfg(feature = "webhooks")]`)
3. ๐Ÿ“ข **Broadcast bus** โ€” always

### Adding a new event variant

```rust
// In DownloadEvent โ€” always use named fields:
// โœ… GOOD
MyNewEvent {
    download_id: u64,
    reason: String,
},

// โŒ BAD โ€” No tuple variants
MyNewEvent(u64, String),
```

---

## ๐ŸŽฏ Feature Flags

### Available features

| Feature | Purpose | Dependencies |
|---------|---------|-------------|
| `hooks` | Rust event callbacks | None |
| `webhooks` | HTTP event delivery | None |
| `statistics` | Real-time analytics | None |
| `cache-memory` *(default)* | In-memory Moka cache | `moka` |
| `cache-json` | JSON file backend | None |
| `cache-redb` | Embedded redb backend | `redb` |
| `cache-redis` | Distributed Redis backend | `redis` |
| `live-recording` | Live stream recording (HLS) | `m3u8-rs` |
| `live-streaming` | Live fragment streaming (HLS) | `m3u8-rs` |
| `rustls` | TLS backend | `reqwest/rustls` |
| `hickory-dns` | Async DNS resolver | `reqwest/hickory-dns` |
| `profiling` | Heap profiler | `dhat` |

### โš™๏ธ `cache` cfg is emitted by `build.rs`

The `cache` cfg is **not** a Cargo feature โ€” it is a custom `cfg` emitted by `build.rs` when any cache backend
(`cache-memory`, `cache-json`, `cache-redb`, or `cache-redis`) is enabled. Users cannot activate it directly,
and it is invisible in `Cargo.toml`. Use `#[cfg(cache)]` to guard code that requires any cache backend.

### Backend selection

`build.rs` emits `persistent_cache` when any of `cache-json`, `cache-redb`, or `cache-redis` is enabled. Multiple persistent features may be active simultaneously โ€” the `multiple_persistent_backends` cfg and its associated `compile_error!` have been removed.

When exactly one persistent feature is compiled in, `CacheConfig::persistent_backend` is auto-deduced and may be left as `None`. When more than one is compiled in, `persistent_backend` **must** be set explicitly to a `PersistentBackendKind` variant; leaving it `None` causes `CacheLayer::from_config` to return `Error::AmbiguousCacheBackend` at runtime.

```rust
use yt_dlp::prelude::*;

// Multiple backends compiled in โ€” pick one at runtime:
let config = CacheConfig::builder()
    .cache_dir("cache")
    .persistent_backend(PersistentBackendKind::Redb) // required when multiple compiled in
    .build();
```

### Conditional compilation patterns

```rust
// Module-level guard for all cache code (cfg emitted by build.rs)
#[cfg(cache)]

// Backend-specific modules
#[cfg(feature = "cache-json")]
pub mod json;

// Persistent backend guard (any of json/redb/redis)
#[cfg(persistent_cache)]

// Feature-gated struct fields
#[cfg(feature = "hooks")]
pub(crate) hook_registry: Option<events::HookRegistry>,
```

### โŒ Forbidden patterns

- **Never use `#[cfg(...)]` on function parameters.** It makes function signatures unreadable and call sites overly complex. If a parameter is feature-dependent, either feature-gate the entire function, or use a config struct / builder pattern where the specific field is feature-gated.

---

## ๐Ÿ“ Tracing & Logging

Tracing is an **unconditional dependency** โ€” every important function must have tracing.

### Rules at a glance

| Rule | Detail |
|------|--------|
| Macro style | Always fully-qualified: `tracing::debug!(...)` โ€” **never import the macros** |
| No `#[instrument]` | Never use the `#[instrument]` attribute |
| Structured fields | `key = value`, `key = ?value` (Debug), `key = %value` (Display) |
| No interpolation | Never `tracing::debug!("msg {}", var)` โ€” always structured fields |

### Log levels

| Level | Usage | Emoji? |
|-------|-------|--------|
| `trace` | Hot paths, data transforms (rare โ€” prefer deleting) | โœ… Yes |
| `debug` | Function entry/exit, parameters, config, internal ops | โœ… Yes |
| `info` | Key milestones (download start/end, fetch, install, shutdown) | โœ… Yes |
| `warn` | Recoverable failures, retries, fallbacks | โŒ No emoji |
| `error` | Unrecoverable per-item failures | โŒ No emoji |

### ๐ŸŽจ Emoji prefixes

Every `trace`/`debug`/`info` message **must** start with one domain emoji:

| Emoji | Domain |
|-------|--------|
| ๐Ÿ“ฆ | Install / dependencies |
| ๐Ÿ“ก | Fetch / extract |
| ๐Ÿ“ฅ | Download |
| ๐ŸŽฌ | Combine / mux |
| โœ‚๏ธ | Postprocess / ffmpeg |
| ๐Ÿท๏ธ | Metadata |
| ๐Ÿ’ฌ | Subtitle |
| ๐Ÿ–ผ๏ธ | Thumbnail |
| ๐Ÿ“‹ | Playlist |
| โœ… | Success / completion |
| ๐Ÿ”„ | Retry / update |
| ๐Ÿ”ง | Config / setup / builder |
| ๐Ÿ” | Cache / lookup |
| โš™๏ธ | Internal / utility |
| ๐Ÿ“Š | Statistics |
| ๐Ÿ”” | Events |
| ๐Ÿงฉ | Format selection |
| ๐Ÿ›‘ | Shutdown |

### Example

```rust
// โœ… GOOD
tracing::debug!(url = %url, timeout = ?timeout, "๐Ÿ“ฅ Starting download");
tracing::info!(video_id = video_id, formats = formats.len(), "๐Ÿ“ก Video fetched");
tracing::warn!(url = %url, attempt = attempt, "Retry after failure");

// โŒ BAD
tracing::debug!("Starting download for {}", url);  // No interpolation
tracing::info!("Video fetched");                     // No structured fields
tracing::warn!("โš ๏ธ Retry");                          // No emoji on warn
```

### What NOT to trace

- โŒ Trivial getters/setters that just return or set a field
- โŒ Pure transforms (`to_ffmpeg_name`, `is_empty`, enum-to-string)
- โŒ Simple constant lookups / match on enum returning a value

---

## ๐Ÿ“– Documentation

Every public function, method, and trait method must have a **rustdoc comment**:

### Template

```rust
/// Brief one-line description.
///
/// Optional extended description.
///
/// # Arguments
///
/// * `param` - Description
///
/// # Errors
///
/// Returns an error if ...
///
/// # Returns
///
/// Description of return value.
///
/// # Examples
///
/// ```rust,no_run
/// # use yt_dlp::prelude::*;
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let downloader = Downloader::builder(libraries, "output").build().await?;
/// # Ok(())
/// # }
/// ```
```

### Section rules

| Section | When to include |
|---------|----------------|
| `# Arguments` | Only if params beyond `&self`/`&mut self` |
| `# Errors` | Only if returns `Result` |
| `# Returns` | Only if returns a value (not `()`) |
| `# Examples` | Main public API entry points (`Downloader::new`, `download`, `fetch`, etc.) |

### Additional rules

| Rule | Detail |
|------|--------|
| Trait methods | Full rustdoc on the **trait declaration**; impls may add only a brief comment |
| Getters | Minimum one-liner + `# Returns` |
| Setters | Minimum one-liner + `# Arguments` |
| Builder methods | Minimum one-liner + `# Arguments` |
| Examples | Use `no_run` or `ignore` for network/binary-dependent code |

---

## โš™๏ธ Process Execution

The crate runs external processes (`yt-dlp`, `ffmpeg`) through a controlled abstraction:

| Component | Location | Purpose |
|-----------|----------|---------|
| `Executor` | `src/executor/mod.rs` | Wraps `tokio::process::Command` with piped I/O and timeout |
| `ProcessOutput` | `src/executor/process.rs` | `{ stdout, stderr, code }` |
| `FfmpegArgs` | `src/executor/ffmpeg.rs` | Fluent builder: `.input()`, `.codec_copy()`, `.args()`, `.output()`, `.build()` |
| `run_ffmpeg_with_tempfile()` | `src/executor/ffmpeg.rs` | Temp file + rename pattern for atomic writes |

### Key patterns

- โฑ๏ธ **Timeout**: `tokio::time::timeout` + `process.kill()` on timeout
- ๐ŸชŸ **Windows**: `command.creation_flags(0x08000000)` (CREATE_NO_WINDOW) behind `#[cfg(target_os = "windows")]`
- ๐Ÿ”„ **Temp + rename**: FFmpeg writes to a temp file, then renames atomically โ€” never write directly to the final output
- ๐Ÿงต **CPU-heavy parsing**: `tokio::task::spawn_blocking` for `serde_json::from_reader` and other CPU-intensive work

---

## ๐Ÿงฉ Macros

Defined in `src/macros.rs` and `src/events/hooks.rs`:

| Macro | Purpose |
|-------|---------|
| `youtube!($yt_dlp, $ffmpeg, $output)` | Convenience `Downloader` constructor |
| `ytdlp_args![...]` | Args builder (string list or key-value pairs) |
| `install_libraries!($dir)` | Async binary installation |
| `ternary!($cond, $true, $false)` | Ternary operator |
| `simple_hook!` | Create an `EventHook` from a closure |

All macros must use `$crate::` fully-qualified paths for robustness. The `use` inside `macro_rules!` bodies is the **only** exception to the "imports at module top" rule.

---

## ๐Ÿ” Contributing to media-seek

`crates/media-seek/` is a standalone crate published independently to [crates.io](https://crates.io/crates/media-seek). Changes to it follow the same code conventions as the main crate, with a few important constraints.

### Constraints

| Rule | Detail |
|------|--------|
| **No feature flags** | All formats are always compiled in โ€” no conditional compilation inside `media-seek` |
| **No `reqwest`** | The crate is transport-agnostic. Callers implement `RangeFetcher`. |
| **No `serde`** | No serialization โ€” pure parsing only |
| **No `async_trait`** | `RangeFetcher` uses RPITIT (`impl Future + Send`), not `#[async_trait]` |
| **No tuples** | `ByteRange { start, end }` instead of `(u64, u64)` |
| **Named constants** | All magic numbers (sync bytes, header sizes, bitrate tables) as `const` at file top |
| **dedup safety** | `dedup_by_key` only after sorting by the **same key**; re-sort after dedup if needed |

### Where to make changes

| Change | Location |
|--------|---------|
| Audio format parser | `crates/media-seek/src/audio/` (`mp3.rs`, `ogg.rs`, `flac.rs`, `pcm.rs`, `adts.rs`) |
| Video format parser | `crates/media-seek/src/video/` (`mp4.rs`, `webm.rs`, `flv.rs`, `avi.rs`, `ts.rs`) |
| Format detection | `crates/media-seek/src/detect.rs` |
| Index data types | `crates/media-seek/src/index.rs` |
| Error handling | `crates/media-seek/src/error.rs` |
| Public API | `crates/media-seek/src/lib.rs` |

### Tracing conventions

Every `pub(crate) fn parse()` / `pub(crate) async fn parse()` must have entry and success tracing:

```rust
// At function start:
tracing::debug!(probe_len = probe.len(), "โš™๏ธ Parsing <Format> stream");

// Just before each successful return:
tracing::debug!(segments = result.len(), "โœ… <Format> index parsed");
```

Use `โš™๏ธ` for internal operations and `โœ…` for success โ€” same as the main crate. No emoji on `warn!` or `error!`.

### Checking your changes

```bash
# media-seek standalone lint
cargo clippy -p media-seek -- -D warnings

# Run media-seek unit + integration tests
cargo test --test unit --all-features -- media_seek
cargo test --test integration --all-features -- media_seek

# Doc-tests (both crates)
cargo test --doc --workspace
```

---

## โœ… Verification Checklist

Before submitting your PR, make sure:

- [ ] ๐Ÿ” `cargo clippy --workspace --all-features -- -D warnings` โ€” zero warnings
- [ ] ๐Ÿ’„ `cargo +nightly fmt --all -- --check` โ€” properly formatted
- [ ] ๐Ÿงช `cargo test --test unit --all-features` โ€” all unit tests pass
- [ ] ๐Ÿงช `cargo test --test integration --all-features` โ€” all integration tests pass
- [ ] ๐Ÿงช `cargo test --test e2e --all-features -- --test-threads=1` โ€” all E2E tests pass
- [ ] ๐Ÿงช `cargo test --doc --workspace --all-features` โ€” all doc-tests pass
- [ ] ๐Ÿ” `cargo deny check` โ€” no dependency issues
- [ ] ๐Ÿงน `cargo machete` โ€” no unused dependencies
- [ ] ๐Ÿ“ All new public items have rustdoc following the template
- [ ] ๐ŸŽจ All tracing uses structured fields + emoji prefix
- [ ] ๐Ÿšจ Errors use the existing `Error` enum with structured fields
- [ ] ๐Ÿ“ฅ All `use` imports are at the top of the file
- [ ] ๐Ÿ”ข No magic numbers โ€” all literals extracted to named `const` at file top
- [ ] ๐Ÿ“ฆ No tuple return types โ€” use named structs instead
- [ ] ๐Ÿ”— No double-qualified paths โ€” import types and use short names
- [ ] ๐ŸŒ All text (comments, docs, logs) is in English
- [ ] ๐Ÿช† No function exceeds 2 nesting levels โ€” extract deeper logic into private helpers

---

<div align="center">
  <strong>Thank you for contributing! ๐ŸŽ‰</strong>
  <br>
  <sub>If you have questions, open a <a href="https://github.com/boul2gom/yt-dlp/discussions">Discussion</a> โ€” we're happy to help.</sub>
</div>