Repository: yt-dlp/yt-dlp
Pull request #9474 (2024-03-17 06:44:04)

[ie] Add extractor impersonate API
Add an impersonate API for the extractors.

Proposed implementation:

Add `impersonate` and `require_impersonation` params to the following extractor helper methods:

- `_request_webpage()`
- `_download_webage()` and `_download_webpage_handle()`
- `_download_json()` and `_download_json_handle()`
- `_download_xml()` and `_download_xml_handle()`
- `_download_socket_json()` and `_download_socket_json_handle()`

```
        impersonate -- the impersonate target. Can be any of the following entities:
                - an instance of yt_dlp.networking.impersonate.ImpersonateTarget
                - a string in the format of CLIENT[:OS]
                - a list or a tuple of CLIENT[:OS] strings or ImpersonateTarget instances
                - a boolean value; True means any impersonate target is sufficient

        require_impersonation -- flag to toggle whether the request should raise an error
            if impersonation is not possible (bool, default: False)
```

IMO we shouldn't always force impersonation for a request, since a manual bypass of cloudflare fingerprinting is possible with the fresh cookies/UA combo, and since some IPs/users may not experience any blockage at all. 

~~Though maybe we should add a warning if an impersonate target is specified (but not forced) by the extractor and the target is not available.~~ (done)

~~Something else we could consider is allowing a preferred order of multiple impersonate targets for a particular request, though I don't know of any sites where that would be necessary.~~ (done)


Example usage: https://github.com/bashonly/yt-dlp/commit/0691d1c9fc344b915d011df94378c7a256550c3f


<details open><summary>Template</summary> <!-- OPEN is intentional -->


### Before submitting a *pull request* make sure you have:
- [x] At least skimmed through [contributing guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) including [yt-dlp coding conventions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#yt-dlp-coding-conventions)
- [x] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests
- [x] Checked the code with [flake8](https://pypi.python.org/pypi/flake8) and [ran relevant tests](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions)

### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check all of the following options that apply:
- [x] I am the original author of this code and I am willing to release it under [Unlicense](http://unlicense.org/)

### What is the purpose of your *pull request*?
- [x] New feature (It is strongly [recommended to open an issue first](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#adding-new-feature-or-making-overarching-changes))

</details>


pukkandan: > Though maybe we should add a warning if an impersonate target is specified (but not forced) by the extractor and the target is not available.

We should add at least a debug message. 

Btw, would the extractor want to know whether impersonation was used or not? For example, to provide better error message?

Patch:
diff --git a/.github/banner.svg b/.github/banner.svg
index 35dc93eae..ea7f9e306 100644
--- a/.github/banner.svg
+++ b/.github/banner.svg
@@ -1,4 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" width="699.935" height="173.764" viewBox="0 0 717 178">
+<svg xmlns="http://www.w3.org/2000/svg" width="746" height="176" viewBox="0 0 746 176">
   <defs>
     <style>
       .cls-1, .cls-4 {
@@ -24,8 +24,8 @@
       }
     </style>
   </defs>
-  <path d="M89.846,166.601 L87.111,166.601 L87.111,172.000 L82.173,172.000 L82.173,153.812 L90.024,153.812 C94.064,153.812 96.773,156.370 96.773,160.242 C96.773,164.158 93.993,166.601 89.846,166.601 ZM88.851,157.755 L87.111,157.755 L87.111,162.764 L88.851,162.764 C90.583,162.764 91.622,161.796 91.622,160.242 C91.622,158.679 90.583,157.755 88.851,157.755 ZM67.898,153.812 L72.835,153.812 L72.835,168.021 L80.189,168.021 L80.189,172.000 L67.898,172.000 L67.898,153.812 ZM56.572,172.000 L49.574,172.000 L49.574,153.812 L56.501,153.812 C62.113,153.812 65.630,157.223 65.630,162.906 C65.630,168.590 62.113,172.000 56.572,172.000 ZM56.252,158.004 L54.511,158.004 L54.511,167.808 L56.394,167.808 C59.094,167.808 60.657,166.707 60.657,162.906 C60.657,159.105 59.094,158.004 56.252,158.004 ZM38.211,162.906 L46.736,162.906 L46.736,166.601 L38.211,166.601 L38.211,162.906 ZM31.253,172.000 L26.387,172.000 L26.387,157.791 L20.916,157.791 L20.916,153.812 L36.724,153.812 L36.724,157.791 L31.253,157.791 L31.253,172.000 ZM12.007,172.000 L7.104,172.000 L7.104,166.281 L0.426,153.812 L5.932,153.812 L9.484,161.201 L9.627,161.201 L13.179,153.812 L18.685,153.812 L12.007,166.281 L12.007,172.000 Z" class="cls-1"/>
-  <path d="M714.317,161.947 C714.104,160.988 713.536,159.993 711.689,159.993 C710.019,159.993 708.634,160.846 708.456,162.018 C708.278,163.048 708.918,163.617 710.445,164.007 L712.399,164.505 C714.743,165.109 715.738,166.281 715.418,168.199 C715.028,170.544 712.577,172.284 709.415,172.284 C706.609,172.284 704.904,171.041 704.797,168.732 L706.893,168.235 C707.000,169.691 707.959,170.437 709.664,170.437 C711.617,170.437 713.038,169.478 713.216,168.306 C713.394,167.347 712.861,166.707 711.511,166.387 L709.344,165.855 C706.928,165.251 706.005,164.007 706.325,162.125 C706.715,159.816 709.131,158.182 712.008,158.182 C714.708,158.182 715.951,159.461 716.306,161.414 L714.317,161.947 ZM702.671,165.890 L692.751,165.890 C692.245,169.229 693.648,170.401 696.276,170.401 C697.955,170.401 699.269,169.691 700.042,168.270 L701.960,168.838 C700.974,170.899 698.736,172.284 695.957,172.284 C692.023,172.284 690.069,169.478 690.770,165.286 C691.454,161.095 694.403,158.182 698.088,158.182 C700.939,158.182 703.674,159.922 702.813,165.002 L702.671,165.890 ZM697.768,160.064 C695.477,160.064 693.461,162.143 693.044,164.078 L700.823,164.078 C701.223,161.770 700.051,160.064 697.768,160.064 ZM687.862,172.000 L685.446,172.000 L683.066,166.707 L678.910,172.000 L676.494,172.000 L681.965,165.180 L678.768,158.359 L681.183,158.359 L683.528,163.936 L687.720,158.359 L690.135,158.359 L684.594,165.180 L687.862,172.000 ZM673.886,154.630 C673.886,153.848 674.560,153.209 675.377,153.209 C676.194,153.209 676.869,153.848 676.869,154.630 C676.869,155.411 676.194,156.050 675.377,156.050 C674.560,156.050 673.886,155.411 673.886,154.630 ZM673.513,172.000 L671.417,172.000 L673.690,158.359 L675.786,158.359 L673.513,172.000 ZM670.212,154.914 C668.826,154.914 668.151,155.624 667.903,156.974 L667.672,158.359 L670.745,158.359 L670.460,160.135 L667.379,160.135 L665.416,172.000 L663.320,172.000 L665.301,160.135 L663.107,160.135 L663.391,158.359 L665.603,158.359 L665.914,156.477 C666.269,154.132 668.365,152.960 670.318,152.960 C671.348,152.960 671.952,153.173 672.237,153.315 L671.348,155.127 C671.135,155.056 670.816,154.914 670.212,154.914 ZM649.225,172.000 L649.580,169.904 L649.332,169.904 C648.745,170.650 647.582,172.284 644.962,172.284 C641.543,172.284 639.616,169.549 640.327,165.215 C641.046,160.917 643.879,158.182 647.324,158.182 C649.989,158.182 650.539,159.816 650.877,160.526 L651.054,160.526 L652.173,153.812 L654.269,153.812 L651.250,172.000 L649.225,172.000 ZM647.182,160.064 C644.527,160.064 642.911,162.302 642.440,165.180 C641.952,168.093 642.849,170.401 645.477,170.401 C647.999,170.401 649.811,168.270 650.326,165.180 C650.832,162.125 649.749,160.064 647.182,160.064 ZM635.980,172.000 L633.884,172.000 L635.305,163.475 C635.660,161.343 634.701,160.064 632.747,160.064 C630.723,160.064 629.053,161.414 628.627,163.794 L627.277,172.000 L625.181,172.000 L627.454,158.359 L629.479,158.359 L629.124,160.491 L629.302,160.491 C630.154,159.105 631.611,158.182 633.671,158.182 C636.406,158.182 638.005,159.851 637.436,163.333 L635.980,172.000 ZM621.349,172.000 L619.253,172.000 L619.573,170.153 L619.466,170.153 C618.898,171.041 617.442,172.320 615.062,172.320 C612.468,172.320 610.657,170.792 611.083,168.128 C611.616,165.002 614.458,164.434 617.051,164.114 C619.573,163.794 620.603,163.865 620.781,162.871 L620.781,162.800 C621.065,161.059 620.354,160.029 618.436,160.029 C616.447,160.029 615.097,161.095 614.458,162.089 L612.611,161.379 C614.067,158.892 616.554,158.182 618.614,158.182 C620.354,158.182 623.551,158.679 622.841,163.013 L621.349,172.000 ZM616.660,165.926 C614.991,166.139 613.428,166.636 613.179,168.235 C612.930,169.691 613.996,170.437 615.665,170.437 C618.152,170.437 619.786,168.767 620.070,167.062 L620.390,165.144 C619.964,165.570 617.548,165.819 616.660,165.926 ZM597.804,159.993 C596.135,159.993 594.749,160.846 594.572,162.018 C594.394,163.048 595.033,163.617 596.561,164.007 L598.515,164.505 C600.859,165.109 601.854,166.281 601.534,168.199 C601.143,170.544 598.692,172.284 595.531,172.284 C592.724,172.284 591.019,171.041 590.913,168.732 L593.009,168.235 C593.115,169.691 594.074,170.437 595.779,170.437 C597.733,170.437 599.154,169.478 599.332,168.306 C599.509,167.347 598.976,166.707 597.627,166.387 L595.460,165.855 C593.044,165.251 592.121,164.007 592.440,162.125 C592.831,159.816 595.247,158.182 598.124,158.182 C600.824,158.182 602.067,159.461 602.422,161.414 L600.433,161.947 C600.220,160.988 599.651,159.993 597.804,159.993 ZM588.786,165.890 L578.866,165.890 C578.360,169.229 579.763,170.401 582.392,170.401 C584.071,170.401 585.385,169.691 586.157,168.270 L588.076,168.838 C587.090,170.899 584.852,172.284 582.072,172.284 C578.138,172.284 576.185,169.478 576.886,165.286 C577.570,161.095 580.518,158.182 584.204,158.182 C587.054,158.182 589.790,159.922 588.928,165.002 L588.786,165.890 ZM583.884,160.064 C581.593,160.064 579.577,162.143 579.160,164.078 L586.939,164.078 C587.339,161.770 586.166,160.064 583.884,160.064 ZM574.722,160.171 C572.733,160.171 571.046,161.530 570.744,163.368 L569.323,172.000 L567.227,172.000 L569.500,158.359 L571.525,158.359 L571.170,160.420 L571.312,160.420 C572.023,159.070 573.586,158.146 575.255,158.146 C576.001,158.146 576.534,158.324 576.889,158.644 L575.894,160.384 C575.646,160.242 575.255,160.171 574.722,160.171 ZM561.299,172.000 L561.690,169.691 L561.548,169.691 C560.695,171.076 559.132,172.178 557.072,172.178 C554.515,172.178 552.952,170.508 553.520,167.027 L554.976,158.359 L557.072,158.359 L555.651,166.885 C555.332,168.874 556.362,170.153 558.102,170.153 C559.665,170.153 561.797,168.981 562.223,166.423 L563.573,158.359 L565.669,158.359 L563.395,172.000 L561.299,172.000 ZM551.534,160.135 L548.594,160.135 L547.271,168.093 C546.987,169.869 547.839,170.153 548.763,170.153 C549.225,170.153 549.509,170.082 549.686,170.046 L549.829,171.929 C549.509,172.036 548.976,172.178 548.195,172.178 C546.418,172.178 544.713,171.041 545.104,168.661 L546.507,160.135 L544.465,160.135 L544.749,158.359 L546.800,158.359 L547.342,155.091 L549.438,155.091 L548.896,158.359 L551.818,158.359 L551.534,160.135 ZM539.780,172.000 L537.684,172.000 L538.004,170.153 L537.897,170.153 C537.329,171.041 535.873,172.320 533.493,172.320 C530.900,172.320 529.088,170.792 529.514,168.128 C530.047,165.002 532.889,164.434 535.482,164.114 C538.004,163.794 539.034,163.865 539.212,162.871 L539.212,162.800 C539.496,161.059 538.786,160.029 536.867,160.029 C534.878,160.029 533.528,161.095 532.889,162.089 L531.042,161.379 C532.498,158.892 534.985,158.182 537.045,158.182 C538.786,158.182 541.983,158.679 541.272,163.013 L539.780,172.000 ZM535.091,165.926 C533.422,166.139 531.859,166.636 531.610,168.235 C531.361,169.691 532.427,170.437 534.097,170.437 C536.583,170.437 538.217,168.767 538.501,167.062 L538.821,165.144 C538.395,165.570 535.979,165.819 535.091,165.926 ZM527.316,165.890 L517.397,165.890 C516.891,169.229 518.294,170.401 520.922,170.401 C522.601,170.401 523.915,169.691 524.688,168.270 L526.606,168.838 C525.620,170.899 523.382,172.284 520.603,172.284 C516.669,172.284 514.715,169.478 515.416,165.286 C516.100,161.095 519.049,158.182 522.734,158.182 C525.585,158.182 528.320,159.922 527.459,165.002 L527.316,165.890 ZM522.414,160.064 C520.123,160.064 518.107,162.143 517.690,164.078 L525.469,164.078 C525.869,161.770 524.697,160.064 522.414,160.064 ZM514.282,154.914 C512.897,154.914 512.222,155.624 511.973,156.974 L511.742,158.359 L514.815,158.359 L514.531,160.135 L511.449,160.135 L509.487,172.000 L507.391,172.000 L509.371,160.135 L507.178,160.135 L507.462,158.359 L509.673,158.359 L509.984,156.477 C510.339,154.132 512.435,152.960 514.389,152.960 C515.419,152.960 516.023,153.173 516.307,153.315 L515.419,155.127 C515.206,155.056 514.886,154.914 514.282,154.914 ZM493.506,172.000 L496.525,153.812 L498.621,153.812 L495.601,172.000 L493.506,172.000 ZM489.674,172.000 L487.578,172.000 L487.898,170.153 L487.791,170.153 C487.223,171.041 485.766,172.320 483.386,172.320 C480.793,172.320 478.981,170.792 479.408,168.128 C479.941,165.002 482.782,164.434 485.375,164.114 C487.898,163.794 488.928,163.865 489.105,162.871 L489.105,162.800 C489.390,161.059 488.679,160.029 486.761,160.029 C484.772,160.029 483.422,161.095 482.782,162.089 L480.935,161.379 C482.392,158.892 484.878,158.182 486.938,158.182 C488.679,158.182 491.876,158.679 491.166,163.013 L489.674,172.000 ZM484.985,165.926 C483.315,166.139 481.752,166.636 481.504,168.235 C481.255,169.691 482.321,170.437 483.990,170.437 C486.477,170.437 488.111,168.767 488.395,167.062 L488.715,165.144 C488.288,165.570 485.873,165.819 484.985,165.926 ZM475.576,172.000 L473.480,172.000 L474.901,163.475 C475.256,161.343 474.297,160.064 472.343,160.064 C470.319,160.064 468.649,161.414 468.223,163.794 L466.873,172.000 L464.777,172.000 L467.051,158.359 L469.075,158.359 L468.720,160.491 L468.898,160.491 C469.750,159.105 471.207,158.182 473.267,158.182 C476.002,158.182 477.601,159.851 477.032,163.333 L475.576,172.000 ZM455.511,172.284 C451.745,172.284 449.703,169.407 450.395,165.109 C451.070,160.917 453.948,158.182 457.571,158.182 C461.336,158.182 463.388,161.059 462.686,165.393 C462.011,169.549 459.134,172.284 455.511,172.284 ZM457.535,160.064 C454.658,160.064 452.873,162.587 452.420,165.393 C451.994,168.057 452.811,170.401 455.546,170.401 C458.423,170.401 460.208,167.924 460.661,165.109 C461.088,162.444 460.271,160.064 457.535,160.064 ZM446.401,154.630 C446.401,153.848 447.076,153.209 447.893,153.209 C448.710,153.209 449.385,153.848 449.385,154.630 C449.385,155.411 448.710,156.050 447.893,156.050 C447.076,156.050 446.401,155.411 446.401,154.630 ZM446.028,172.000 L443.932,172.000 L446.206,158.359 L448.301,158.359 L446.028,172.000 ZM442.763,160.135 L439.823,160.135 L438.500,168.093 C438.216,169.869 439.069,170.153 439.992,170.153 C440.454,170.153 440.738,170.082 440.916,170.046 L441.058,171.929 C440.738,172.036 440.205,172.178 439.424,172.178 C437.648,172.178 435.943,171.041 436.333,168.661 L437.736,160.135 L435.694,160.135 L435.978,158.359 L438.030,158.359 L438.571,155.091 L440.667,155.091 L440.125,158.359 L443.047,158.359 L442.763,160.135 ZM431.380,154.630 C431.380,153.848 432.055,153.209 432.872,153.209 C433.689,153.209 434.364,153.848 434.364,154.630 C434.364,155.411 433.689,156.050 432.872,156.050 C432.055,156.050 431.380,155.411 431.380,154.630 ZM431.007,172.000 L428.911,172.000 L431.184,158.359 L433.280,158.359 L431.007,172.000 ZM422.770,172.000 L423.126,169.904 L422.877,169.904 C422.291,170.650 421.128,172.284 418.508,172.284 C415.089,172.284 413.162,169.549 413.872,165.215 C414.591,160.917 417.424,158.182 420.870,158.182 C423.534,158.182 424.085,159.816 424.422,160.526 L424.600,160.526 L425.719,153.812 L427.815,153.812 L424.795,172.000 L422.770,172.000 ZM420.728,160.064 C418.073,160.064 416.456,162.302 415.986,165.180 C415.497,168.093 416.394,170.401 419.023,170.401 C421.545,170.401 423.357,168.270 423.872,165.180 C424.378,162.125 423.294,160.064 420.728,160.064 ZM407.216,172.000 L407.572,169.904 L407.323,169.904 C406.737,170.650 405.573,172.284 402.954,172.284 C399.535,172.284 397.608,169.549 398.318,165.215 C399.037,160.917 401.870,158.182 405.316,158.182 C407.980,158.182 408.531,159.816 408.868,160.526 L409.046,160.526 L410.165,153.812 L412.261,153.812 L409.241,172.000 L407.216,172.000 ZM405.174,160.064 C402.519,160.064 400.902,162.302 400.432,165.180 C399.943,168.093 400.840,170.401 403.469,170.401 C405.991,170.401 407.803,168.270 408.318,165.180 C408.824,162.125 407.740,160.064 405.174,160.064 ZM393.971,172.000 L391.875,172.000 L392.195,170.153 L392.088,170.153 C391.520,171.041 390.063,172.320 387.683,172.320 C385.090,172.320 383.279,170.792 383.705,168.128 C384.238,165.002 387.080,164.434 389.673,164.114 C392.195,163.794 393.225,163.865 393.403,162.871 L393.403,162.800 C393.687,161.059 392.976,160.029 391.058,160.029 C389.069,160.029 387.719,161.095 387.080,162.089 L385.232,161.379 C386.689,158.892 389.175,158.182 391.236,158.182 C392.976,158.182 396.173,158.679 395.463,163.013 L393.971,172.000 ZM389.282,165.926 C387.612,166.139 386.049,166.636 385.801,168.235 C385.552,169.691 386.618,170.437 388.287,170.437 C390.774,170.437 392.408,168.767 392.692,167.062 L393.012,165.144 C392.586,165.570 390.170,165.819 389.282,165.926 ZM372.842,172.000 L370.746,172.000 L372.167,163.475 C372.522,161.308 371.528,160.064 369.574,160.064 C367.513,160.064 365.773,161.414 365.347,163.794 L363.997,172.000 L361.901,172.000 L364.920,153.812 L367.016,153.812 L365.915,160.491 L366.093,160.491 C366.945,159.070 368.330,158.182 370.497,158.182 C373.268,158.182 374.867,159.816 374.298,163.333 L372.842,172.000 ZM360.448,160.135 L357.508,160.135 L356.185,168.093 C355.901,169.869 356.753,170.153 357.677,170.153 C358.139,170.153 358.423,170.082 358.601,170.046 L358.743,171.929 C358.423,172.036 357.890,172.178 357.109,172.178 C355.333,172.178 353.627,171.041 354.018,168.661 L355.421,160.135 L353.379,160.135 L353.663,158.359 L355.714,158.359 L356.256,155.091 L358.352,155.091 L357.810,158.359 L360.732,158.359 L360.448,160.135 ZM349.065,154.630 C349.065,153.848 349.740,153.209 350.557,153.209 C351.374,153.209 352.049,153.848 352.049,154.630 C352.049,155.411 351.374,156.050 350.557,156.050 C349.740,156.050 349.065,155.411 349.065,154.630 ZM348.692,172.000 L346.596,172.000 L348.869,158.359 L350.965,158.359 L348.692,172.000 ZM337.615,172.000 L336.372,161.521 L336.159,161.521 L331.434,172.000 L329.374,172.000 L327.491,158.359 L329.694,158.359 L330.901,168.803 L331.043,168.803 L335.697,158.359 L337.935,158.359 L339.072,168.767 L339.214,168.767 L343.903,158.359 L346.105,158.359 L339.675,172.000 L337.615,172.000 ZM316.983,172.000 L314.319,172.000 L310.296,165.526 L308.600,166.885 L307.747,172.000 L305.651,172.000 L308.671,153.812 L310.767,153.812 L308.999,164.434 L309.239,164.434 L316.237,158.359 L318.830,158.359 L312.090,164.203 L316.983,172.000 ZM303.559,160.171 C301.569,160.171 299.882,161.530 299.580,163.368 L298.159,172.000 L296.063,172.000 L298.337,158.359 L300.362,158.359 L300.006,160.420 L300.149,160.420 C300.859,159.070 302.422,158.146 304.091,158.146 C304.837,158.146 305.370,158.324 305.726,158.644 L304.731,160.384 C304.482,160.242 304.091,160.171 303.559,160.171 ZM286.797,172.284 C283.031,172.284 280.989,169.407 281.682,165.109 C282.356,160.917 285.234,158.182 288.857,158.182 C292.622,158.182 294.674,161.059 293.972,165.393 C293.297,169.549 290.420,172.284 286.797,172.284 ZM288.822,160.064 C285.944,160.064 284.159,162.587 283.706,165.393 C283.280,168.057 284.097,170.401 286.832,170.401 C289.710,170.401 291.495,167.924 291.948,165.109 C292.374,162.444 291.557,160.064 288.822,160.064 ZM280.512,154.914 C279.126,154.914 278.452,155.624 278.203,156.974 L277.972,158.359 L281.045,158.359 L280.760,160.135 L277.679,160.135 L275.716,172.000 L273.620,172.000 L275.601,160.135 L273.407,160.135 L273.691,158.359 L275.903,158.359 L276.214,156.477 C276.569,154.132 278.665,152.960 280.618,152.960 C281.649,152.960 282.252,153.173 282.537,153.315 L281.649,155.127 C281.435,155.056 281.116,154.914 280.512,154.914 ZM259.735,172.000 L262.755,153.812 L264.851,153.812 L261.831,172.000 L259.735,172.000 ZM253.595,172.000 L253.950,169.904 L253.701,169.904 C253.115,170.650 251.952,172.284 249.332,172.284 C245.913,172.284 243.986,169.549 244.696,165.215 C245.416,160.917 248.249,158.182 251.694,158.182 C254.358,158.182 254.909,159.816 255.246,160.526 L255.424,160.526 L256.543,153.812 L258.639,153.812 L255.619,172.000 L253.595,172.000 ZM251.552,160.064 C248.897,160.064 247.281,162.302 246.810,165.180 C246.321,168.093 247.218,170.401 249.847,170.401 C252.369,170.401 254.181,168.270 254.696,165.180 C255.202,162.125 254.119,160.064 251.552,160.064 ZM233.670,165.180 L233.990,163.226 L241.947,163.226 L241.627,165.180 L233.670,165.180 ZM230.478,165.890 L220.558,165.890 C220.052,169.229 221.455,170.401 224.084,170.401 C225.762,170.401 227.077,169.691 227.849,168.270 L229.768,168.838 C228.782,170.899 226.544,172.284 223.764,172.284 C219.830,172.284 217.876,169.478 218.578,165.286 C219.262,161.095 222.210,158.182 225.896,158.182 C228.746,158.182 231.482,159.922 230.620,165.002 L230.478,165.890 ZM225.576,160.064 C223.285,160.064 221.269,162.143 220.851,164.078 L228.631,164.078 C229.030,161.770 227.858,160.064 225.576,160.064 ZM209.063,172.284 C206.434,172.284 205.830,170.650 205.475,169.904 L205.226,169.904 L204.871,172.000 L202.846,172.000 L205.866,153.812 L207.962,153.812 L206.860,160.526 L207.038,160.526 C207.606,159.816 208.708,158.182 211.372,158.182 C214.817,158.182 216.736,160.917 216.025,165.215 C215.315,169.549 212.473,172.284 209.063,172.284 ZM210.803,160.064 C208.246,160.064 206.541,162.125 206.043,165.180 C205.546,168.270 206.576,170.401 209.098,170.401 C211.727,170.401 213.432,168.093 213.929,165.180 C214.391,162.302 213.468,160.064 210.803,160.064 ZM196.634,172.000 L197.025,169.691 L196.883,169.691 C196.031,171.076 194.468,172.178 192.407,172.178 C189.850,172.178 188.287,170.508 188.855,167.027 L190.311,158.359 L192.407,158.359 L190.986,166.885 C190.667,168.874 191.697,170.153 193.437,170.153 C195.000,170.153 197.132,168.981 197.558,166.423 L198.908,158.359 L201.004,158.359 L198.730,172.000 L196.634,172.000 ZM186.869,160.135 L183.929,160.135 L182.606,168.093 C182.322,169.869 183.174,170.153 184.098,170.153 C184.560,170.153 184.844,170.082 185.022,170.046 L185.164,171.929 C184.844,172.036 184.311,172.178 183.530,172.178 C181.754,172.178 180.048,171.041 180.439,168.661 L181.842,160.135 L179.800,160.135 L180.084,158.359 L182.135,158.359 L182.677,155.091 L184.773,155.091 L184.231,158.359 L187.153,158.359 L186.869,160.135 ZM173.020,172.000 L173.410,169.691 L173.268,169.691 C172.416,171.076 170.853,172.178 168.792,172.178 C166.235,172.178 164.672,170.508 165.240,167.027 L166.697,158.359 L168.792,158.359 L167.372,166.885 C167.052,168.874 168.082,170.153 169.823,170.153 C171.386,170.153 173.517,168.981 173.943,166.423 L175.293,158.359 L177.389,158.359 L175.115,172.000 L173.020,172.000 ZM155.157,172.284 C151.391,172.284 149.349,169.407 150.041,165.109 C150.716,160.917 153.594,158.182 157.217,158.182 C160.982,158.182 163.034,161.059 162.332,165.393 C161.657,169.549 158.780,172.284 155.157,172.284 ZM157.181,160.064 C154.304,160.064 152.519,162.587 152.066,165.393 C151.640,168.057 152.457,170.401 155.192,170.401 C158.069,170.401 159.854,167.924 160.307,165.109 C160.734,162.444 159.917,160.064 157.181,160.064 ZM136.511,177.293 C135.801,177.293 135.197,177.151 135.019,177.044 L135.836,175.197 C137.293,175.588 138.207,175.366 139.317,173.350 L140.063,172.000 L137.293,158.359 L139.495,158.359 L141.449,169.229 L141.591,169.229 L147.168,158.359 L149.512,158.359 L141.023,174.202 C139.886,176.298 138.429,177.293 136.511,177.293 ZM123.225,166.849 L115.871,166.849 L113.181,172.000 L110.872,172.000 L120.569,153.812 L122.843,153.812 L126.501,172.000 L124.192,172.000 L123.225,166.849 ZM121.315,156.690 L121.173,156.690 L116.893,164.895 L122.860,164.895 L121.315,156.690 Z" class="cls-2"/>
-  <path d="M252.245,116.350 L252.245,102.200 L309.303,102.200 L309.303,116.350 L252.245,116.350 ZM208.254,81.088 L245.342,59.291 L208.254,38.180 L216.242,25.227 L260.862,52.844 L260.862,65.739 L216.413,93.355 L208.254,81.088 Z" class="cls-3"/>
-  <path d="M508.108,52.635 C507.921,55.093 507.643,57.527 507.274,59.937 L504.214,78.017 C503.658,81.170 502.754,84.324 501.502,87.475 C500.250,90.628 498.464,93.479 496.147,96.028 C493.829,98.579 491.047,100.503 487.802,101.800 C484.556,103.097 481.311,103.747 478.067,103.747 C476.211,103.747 474.357,103.491 472.504,102.982 C470.648,102.474 469.072,101.615 467.775,100.409 C466.475,99.205 465.410,97.767 464.576,96.098 C463.741,94.429 463.092,92.714 462.629,90.952 L455.953,131.146 L436.482,131.146 L453.310,28.922 L472.921,28.922 L471.391,38.240 C472.504,36.665 473.777,35.180 475.216,33.790 C476.652,32.399 478.228,31.240 479.944,30.313 C481.659,29.387 483.467,28.737 485.369,28.365 C487.268,27.996 489.145,27.809 491.001,27.809 C493.411,27.809 495.706,28.226 497.886,29.061 C500.063,29.895 501.871,31.171 503.310,32.886 C504.746,34.602 505.835,36.549 506.578,38.727 C507.319,40.907 507.806,43.156 508.039,45.472 C508.269,47.791 508.293,50.179 508.108,52.635 ZM487.455,48.184 C486.851,46.841 485.877,45.798 484.534,45.055 C483.189,44.314 481.729,43.942 480.153,43.942 C478.762,43.942 477.393,44.151 476.050,44.568 C474.705,44.986 473.499,45.681 472.434,46.655 C471.367,47.628 470.556,48.765 470.000,50.062 C469.444,51.362 469.027,52.659 468.748,53.956 L465.828,72.037 C465.641,73.149 465.480,74.286 465.341,75.444 C465.202,76.605 465.178,77.717 465.271,78.782 C465.363,79.849 465.526,80.916 465.758,81.981 C465.988,83.048 466.384,84.022 466.940,84.902 C467.497,85.784 468.283,86.456 469.305,86.918 C470.324,87.383 471.391,87.614 472.504,87.614 C474.079,87.614 475.633,87.314 477.163,86.710 C478.693,86.108 480.036,85.204 481.196,83.998 C482.354,82.794 483.235,81.425 483.839,79.895 C484.441,78.365 484.882,76.859 485.160,75.375 L488.081,57.294 C488.359,55.719 488.474,54.143 488.428,52.565 C488.381,50.990 488.057,49.530 487.455,48.184 ZM433.422,86.501 L433.422,102.635 L384.744,102.635 L384.744,86.501 L401.433,86.501 L412.977,16.544 L400.460,16.544 L400.460,0.410 L435.230,0.410 L420.905,86.501 L433.422,86.501 ZM354.285,102.635 L355.815,93.177 C354.793,94.846 353.542,96.354 352.060,97.697 C350.575,99.042 348.976,100.179 347.261,101.105 C345.545,102.033 343.736,102.704 341.837,103.121 C339.936,103.539 338.058,103.747 336.204,103.747 C333.792,103.747 331.497,103.330 329.320,102.495 C327.140,101.661 325.332,100.388 323.896,98.671 C322.457,96.956 321.368,94.985 320.627,92.760 C319.884,90.535 319.397,88.264 319.167,85.945 C318.934,83.628 318.911,81.264 319.097,78.852 C319.282,76.442 319.606,74.032 320.071,71.620 L322.992,53.539 C323.548,50.295 324.452,47.096 325.704,43.942 C326.955,40.791 328.764,37.962 331.128,35.459 C333.492,32.955 336.274,31.056 339.473,29.756 C342.672,28.459 345.892,27.809 349.139,27.809 C351.086,27.809 352.964,28.066 354.772,28.574 C356.580,29.085 358.155,29.943 359.500,31.147 C360.843,32.353 361.934,33.790 362.769,35.459 C363.603,37.128 364.205,38.844 364.577,40.605 L371.253,0.410 L390.724,0.410 L373.895,102.635 L354.285,102.635 ZM362.004,52.705 C361.956,51.592 361.795,50.503 361.517,49.436 C361.239,48.371 360.822,47.419 360.265,46.585 C359.709,45.751 358.920,45.103 357.901,44.638 C356.880,44.175 355.815,43.942 354.702,43.942 C353.124,43.942 351.573,44.245 350.043,44.846 C348.513,45.451 347.168,46.355 346.010,47.559 C344.849,48.765 343.969,50.110 343.367,51.592 C342.763,53.076 342.370,54.606 342.185,56.182 L339.125,74.262 C338.847,75.840 338.730,77.415 338.777,78.991 C338.823,80.569 339.147,82.029 339.751,83.372 C340.353,84.717 341.326,85.760 342.672,86.501 C344.015,87.244 345.475,87.614 347.053,87.614 C348.443,87.614 349.810,87.405 351.156,86.988 C352.499,86.571 353.705,85.875 354.772,84.902 C355.836,83.928 356.671,82.794 357.275,81.494 C357.877,80.197 358.270,78.900 358.457,77.600 L361.517,59.520 C361.702,58.407 361.841,57.272 361.934,56.112 C362.026,54.954 362.049,53.817 362.004,52.705 Z" class="cls-4"/>
+  <path class="cls-1" d="M0.426,154.812L7.1,167.281V173h4.9v-5.719l6.678-12.469H13.179L9.627,162.2H9.485l-3.552-7.389H0.426Zm20.49,3.979h5.471V173h4.866V158.791h5.47v-3.979H20.916v3.979Zm25.82,5.115H38.211V167.6h8.525v-3.695ZM56.572,173c5.541,0,9.058-3.41,9.058-9.094s-3.517-9.094-9.129-9.094H49.574V173h7Zm-2.06-4.192V159h1.741c2.842,0,4.4,1.1,4.4,4.9s-1.563,4.9-4.263,4.9H54.512ZM67.9,173H80.189v-3.979H72.835V154.812H67.9V173Zm14.275,0h4.938v-5.4h2.735c4.147,0,6.927-2.443,6.927-6.359,0-3.872-2.709-6.43-6.749-6.43h-7.85V173Zm4.938-9.236v-5.009h1.741c1.732,0,2.771.924,2.771,2.487a2.452,2.452,0,0,1-2.771,2.522H87.111Z"/>
+  <path class="cls-2" d="M113.281,173l2.691-5.151h7.353L124.293,173H126.6l-3.659-18.188H120.67L110.972,173h2.309Zm3.712-7.1,4.281-8.206h0.142l1.545,8.206h-5.968Zm27.54-6.537h-3.072l0.231-1.385a2.139,2.139,0,0,1,2.309-2.06,2.987,2.987,0,0,1,1.136.213l0.888-1.812a4.324,4.324,0,0,0-1.918-.355,4.271,4.271,0,0,0-4.405,3.517l-0.31,1.882H137.18l-0.284,1.776h2.194L137.109,173h2.1l1.963-11.865h3.081Zm5.788,13.925a6.444,6.444,0,0,0,6-3.445l-1.918-.569a4.022,4.022,0,0,1-3.765,2.131c-2.629,0-4.032-1.172-3.526-4.511h9.92l0.142-.888c0.861-5.08-1.874-6.82-4.725-6.82-3.685,0-6.633,2.913-7.317,7.1C144.433,170.478,146.387,173.284,150.321,173.284Zm-2.913-8.206a5.27,5.27,0,0,1,4.725-4.014c2.282,0,3.454,1.706,3.055,4.014h-7.78Zm15.8,8.242a5.173,5.173,0,0,0,4.405-2.167h0.106L167.4,173h2.1l1.492-8.987c0.71-4.334-2.487-4.831-4.228-4.831a6.658,6.658,0,0,0-6,3.2l1.847,0.71a4.673,4.673,0,0,1,3.979-2.06c1.918,0,2.628,1.03,2.344,2.771v0.071c-0.177.994-1.208,0.923-3.73,1.243-2.593.32-5.435,0.888-5.967,4.014C158.806,171.792,160.618,173.32,163.211,173.32Zm0.6-1.883c-1.67,0-2.735-.746-2.487-2.2,0.249-1.6,1.812-2.1,3.482-2.309,0.888-.107,3.3-0.356,3.729-0.782l-0.319,1.918A4.289,4.289,0,0,1,163.815,171.437Zm17.721-12.078h-2.921l0.541-3.268h-2.1l-0.541,3.268h-2.052l-0.284,1.776h2.043l-1.4,8.526a2.88,2.88,0,0,0,3.091,3.517,5.026,5.026,0,0,0,1.634-.249l-0.142-1.883a4.017,4.017,0,0,1-.924.107c-0.923,0-1.776-.284-1.492-2.06l1.324-7.958h2.939Zm10.405,8.064a4.329,4.329,0,0,1-4.12,3.73c-1.741,0-2.771-1.279-2.451-3.268l1.421-8.526h-2.1l-1.457,8.668c-0.568,3.481,1,5.151,3.553,5.151a5.185,5.185,0,0,0,4.475-2.487h0.143L191.018,173h2.1l2.273-13.641h-2.1Zm5,5.577h2.1l1.421-8.632a3.953,3.953,0,0,1,3.979-3.2,2.457,2.457,0,0,1,1.172.213l1-1.74a2.365,2.365,0,0,0-1.634-.5,4.5,4.5,0,0,0-3.943,2.274h-0.143l0.356-2.061h-2.025Zm14.846,0.284a6.446,6.446,0,0,0,6-3.445l-1.918-.569a4.023,4.023,0,0,1-3.766,2.131c-2.628,0-4.031-1.172-3.525-4.511h9.92l0.142-.888c0.861-5.08-1.874-6.82-4.725-6.82-3.685,0-6.634,2.913-7.317,7.1C205.9,170.478,207.857,173.284,211.791,173.284Zm-2.913-8.206a5.268,5.268,0,0,1,4.724-4.014c2.283,0,3.455,1.706,3.055,4.014h-7.779Zm21.1-.852h-7.957l-0.32,1.954h7.957ZM232.208,173h2.1l1.42-8.632a3.959,3.959,0,0,1,3.979-3.2,6.363,6.363,0,0,1,1.279.142l0.355-2.131a10.544,10.544,0,0,0-1.1-.036,4.538,4.538,0,0,0-3.943,2.274h-0.142l0.355-2.061h-2.025Zm9.339,0h2.1l2.274-13.641h-2.1Zm3.961-15.95a1.422,1.422,0,1,0-1.492-1.42A1.464,1.464,0,0,0,245.508,157.05Zm7.476,16.234a6.244,6.244,0,0,0,6.074-4.263h-2.1a3.9,3.9,0,0,1-3.659,2.38c-2.5,0-3.774-2.06-3.232-5.221,0.488-3.091,2.477-5.116,4.937-5.116a2.628,2.628,0,0,1,2.878,2.38h2.1c0.062-2.557-1.821-4.262-4.689-4.262-3.7,0-6.652,2.913-7.318,7.069C247.291,170.336,249.138,173.284,252.984,173.284Zm11.9-8.49a4.229,4.229,0,0,1,4.228-3.73c1.953,0,2.948,1.244,2.593,3.411L270.279,173h2.1l1.456-8.667c0.568-3.517-1.03-5.151-3.8-5.151a4.741,4.741,0,0,0-4.4,2.309h-0.177l1.1-6.679h-2.1L261.434,173h2.095Zm23.865,8.49a6.244,6.244,0,0,0,6.074-4.263h-2.1a3.894,3.894,0,0,1-3.659,2.38c-2.5,0-3.774-2.06-3.232-5.221,0.488-3.091,2.478-5.116,4.937-5.116a2.628,2.628,0,0,1,2.878,2.38h2.1c0.062-2.557-1.821-4.262-4.689-4.262-3.7,0-6.652,2.913-7.318,7.069C283.051,170.336,284.9,173.284,288.744,173.284Zm14.1,0c3.623,0,6.5-2.735,7.175-6.891,0.7-4.334-1.349-7.211-5.115-7.211-3.623,0-6.5,2.735-7.175,6.927C297.034,170.407,299.076,173.284,302.842,173.284Zm0.035-1.883c-2.735,0-3.552-2.344-3.126-5.008,0.453-2.806,2.238-5.329,5.116-5.329,2.735,0,3.552,2.38,3.126,5.045C307.54,168.924,305.755,171.4,302.877,171.4Zm9.231,1.6h2.1l1.421-8.525a3.978,3.978,0,0,1,3.659-3.411,2.092,2.092,0,0,1,2.273,2.594L319.994,173h2.132l1.492-8.881a3.45,3.45,0,0,1,3.516-3.055c1.492,0,2.629.782,2.309,2.807L327.916,173h2.1l1.527-9.129c0.533-3.2-.959-4.689-3.374-4.689a5.191,5.191,0,0,0-4.476,2.309h-0.142a3.076,3.076,0,0,0-3.268-2.309,4.487,4.487,0,0,0-4.05,2.309h-0.178l0.356-2.132h-2.025Zm21.734,0h2.1l1.421-8.525a3.978,3.978,0,0,1,3.659-3.411,2.093,2.093,0,0,1,2.274,2.594L341.728,173h2.131l1.492-8.881a3.45,3.45,0,0,1,3.517-3.055c1.491,0,2.628.782,2.309,2.807L349.649,173h2.1l1.527-9.129c0.533-3.2-.959-4.689-3.374-4.689a5.191,5.191,0,0,0-4.476,2.309H345.28a3.075,3.075,0,0,0-3.268-2.309,4.487,4.487,0,0,0-4.05,2.309h-0.177l0.355-2.132h-2.025Zm25.711,0.32a5.173,5.173,0,0,0,4.405-2.167h0.107L363.745,173h2.1l1.492-8.987c0.71-4.334-2.487-4.831-4.228-4.831a6.659,6.659,0,0,0-6,3.2l1.847,0.71a4.674,4.674,0,0,1,3.979-2.06c1.918,0,2.628,1.03,2.344,2.771v0.071c-0.177.994-1.207,0.923-3.73,1.243-2.593.32-5.435,0.888-5.967,4.014C355.148,171.792,356.96,173.32,359.553,173.32Zm0.6-1.883c-1.67,0-2.735-.746-2.487-2.2,0.249-1.6,1.812-2.1,3.482-2.309,0.888-.107,3.3-0.356,3.73-0.782l-0.32,1.918A4.289,4.289,0,0,1,360.157,171.437Zm12.961-6.643a4.154,4.154,0,0,1,4.121-3.73c1.954,0,2.913,1.279,2.558,3.411L378.376,173h2.1l1.457-8.667c0.568-3.482-1.03-5.151-3.765-5.151a4.869,4.869,0,0,0-4.37,2.309h-0.177l0.355-2.132h-2.025L369.673,173h2.095Zm16.336,8.49a5.166,5.166,0,0,0,4.369-2.38h0.249l-0.355,2.1h2.024l3.02-18.188h-2.1l-1.119,6.714h-0.178a3.382,3.382,0,0,0-3.552-2.344c-3.446,0-6.278,2.735-7,7.033C384.108,170.549,386.035,173.284,389.454,173.284Zm0.515-1.883c-2.629,0-3.526-2.308-3.037-5.221,0.47-2.878,2.087-5.116,4.742-5.116,2.567,0,3.65,2.061,3.144,5.116C394.3,169.27,392.491,171.4,389.969,171.4Zm19.16-7.175h-7.957l-0.32,1.954h7.957Zm7.349-9.414h-2.1L411.363,173h2.1ZM417.293,173h2.1l2.273-13.641h-2.1Zm3.961-15.95a1.422,1.422,0,1,0-1.492-1.42A1.464,1.464,0,0,0,421.254,157.05Zm5.415,7.744a4.153,4.153,0,0,1,4.121-3.73c1.953,0,2.912,1.279,2.557,3.411L431.926,173h2.1l1.457-8.667c0.568-3.482-1.031-5.151-3.766-5.151a4.869,4.869,0,0,0-4.369,2.309h-0.178l0.356-2.132H425.5L423.223,173h2.1Zm16.868,8.49a6.446,6.446,0,0,0,6-3.445l-1.918-.569a4.023,4.023,0,0,1-3.766,2.131c-2.629,0-4.032-1.172-3.525-4.511h9.919l0.142-.888c0.862-5.08-1.874-6.82-4.724-6.82-3.686,0-6.634,2.913-7.318,7.1C437.65,170.478,439.6,173.284,443.537,173.284Zm-2.912-8.206a5.268,5.268,0,0,1,4.724-4.014c2.282,0,3.455,1.706,3.055,4.014h-7.779Zm22.834,8.242a5.171,5.171,0,0,0,4.4-2.167h0.107L467.65,173h2.1l1.492-8.987c0.711-4.334-2.487-4.831-4.227-4.831a6.658,6.658,0,0,0-6,3.2l1.847,0.71a4.671,4.671,0,0,1,3.978-2.06c1.918,0,2.629,1.03,2.345,2.771v0.071c-0.178.994-1.208,0.923-3.73,1.243-2.593.32-5.435,0.888-5.968,4.014C459.054,171.792,460.866,173.32,463.459,173.32Zm0.6-1.883c-1.67,0-2.736-.746-2.487-2.2,0.249-1.6,1.812-2.1,3.481-2.309,0.888-.107,3.3-0.356,3.73-0.782l-0.32,1.918A4.289,4.289,0,0,1,464.063,171.437Zm19.035-4.014a4.329,4.329,0,0,1-4.12,3.73c-1.741,0-2.771-1.279-2.452-3.268l1.421-8.526h-2.1l-1.457,8.668c-0.568,3.481,1,5.151,3.552,5.151a5.185,5.185,0,0,0,4.476-2.487h0.142L482.175,173h2.095l2.274-13.641h-2.1Zm10.155,5.861a5.166,5.166,0,0,0,4.369-2.38h0.249l-0.355,2.1h2.024l3.02-18.188h-2.1l-1.119,6.714h-0.178a3.382,3.382,0,0,0-3.552-2.344c-3.446,0-6.278,2.735-7,7.033C487.907,170.549,489.834,173.284,493.253,173.284Zm0.515-1.883c-2.629,0-3.526-2.308-3.037-5.221,0.471-2.878,2.087-5.116,4.742-5.116,2.567,0,3.65,2.061,3.144,5.116C498.1,169.27,496.29,171.4,493.768,171.4Zm9.888,1.6h2.1l2.273-13.641H505.93Zm3.961-15.95a1.422,1.422,0,1,0-1.492-1.42A1.464,1.464,0,0,0,507.617,157.05Zm7.618,16.234c3.623,0,6.5-2.735,7.175-6.891,0.7-4.334-1.35-7.211-5.115-7.211-3.623,0-6.5,2.735-7.176,6.927C509.427,170.407,511.469,173.284,515.235,173.284Zm0.035-1.883c-2.735,0-3.552-2.344-3.126-5.008,0.453-2.806,2.238-5.329,5.115-5.329,2.736,0,3.553,2.38,3.126,5.045C519.932,168.924,518.147,171.4,515.27,171.4Zm18.822-17.441h-1.918l-9.449,21.775h1.918Zm12.75,5.4H544.5l-5.577,10.87h-0.142l-1.954-10.87h-2.2L537.393,173h2.132ZM548.188,173h2.1l2.274-13.641h-2.1Zm3.96-15.95a1.422,1.422,0,1,0-1.492-1.42A1.464,1.464,0,0,0,552.148,157.05Zm7.121,16.234a5.167,5.167,0,0,0,4.369-2.38h0.249l-0.356,2.1h2.025l3.02-18.188h-2.1l-1.119,6.714h-0.178a3.382,3.382,0,0,0-3.552-2.344c-3.446,0-6.279,2.735-7,7.033C553.922,170.549,555.85,173.284,559.269,173.284Zm0.515-1.883c-2.629,0-3.526-2.308-3.037-5.221,0.47-2.878,2.086-5.116,4.742-5.116,2.566,0,3.65,2.061,3.144,5.116C564.117,169.27,562.306,171.4,559.784,171.4Zm15.572,1.883a6.446,6.446,0,0,0,6-3.445l-1.918-.569a4.023,4.023,0,0,1-3.766,2.131c-2.628,0-4.032-1.172-3.525-4.511h9.919l0.142-.888c0.862-5.08-1.873-6.82-4.724-6.82-3.686,0-6.634,2.913-7.318,7.1C569.468,170.478,571.421,173.284,575.356,173.284Zm-2.913-8.206a5.268,5.268,0,0,1,4.724-4.014c2.282,0,3.455,1.706,3.055,4.014h-7.779Zm17.472,8.206c3.623,0,6.5-2.735,7.176-6.891,0.7-4.334-1.35-7.211-5.116-7.211-3.623,0-6.5,2.735-7.175,6.927C584.107,170.407,586.15,173.284,589.915,173.284Zm0.036-1.883c-2.736,0-3.553-2.344-3.126-5.008,0.452-2.806,2.237-5.329,5.115-5.329,2.735,0,3.552,2.38,3.126,5.045C594.613,168.924,592.828,171.4,589.951,171.4Zm21.413,1.883a5.166,5.166,0,0,0,4.369-2.38h0.249l-0.356,2.1h2.025l3.02-18.188h-2.1l-1.119,6.714h-0.178a3.382,3.382,0,0,0-3.552-2.344c-3.446,0-6.279,2.735-7,7.033C606.018,170.549,607.945,173.284,611.364,173.284Zm0.515-1.883c-2.629,0-3.526-2.308-3.037-5.221,0.47-2.878,2.087-5.116,4.742-5.116,2.566,0,3.65,2.061,3.144,5.116C616.213,169.27,614.4,171.4,611.879,171.4Zm15.536,1.883c3.623,0,6.5-2.735,7.176-6.891,0.7-4.334-1.35-7.211-5.116-7.211-3.623,0-6.5,2.735-7.175,6.927C621.607,170.407,623.65,173.284,627.415,173.284Zm0.036-1.883c-2.736,0-3.553-2.344-3.126-5.008,0.453-2.806,2.238-5.329,5.115-5.329,2.735,0,3.552,2.38,3.126,5.045C632.113,168.924,630.328,171.4,627.451,171.4Zm11.86,1.6h2.06l4.724-10.479h0.213L647.552,173h2.06l6.43-13.641h-2.2l-4.689,10.408h-0.142l-1.137-10.408h-2.237L640.98,169.8h-0.142l-1.208-10.444h-2.2Zm20.667-8.206a4.154,4.154,0,0,1,4.121-3.73c1.954,0,2.913,1.279,2.557,3.411L665.236,173h2.1l1.457-8.667c0.568-3.482-1.03-5.151-3.766-5.151a4.869,4.869,0,0,0-4.369,2.309h-0.177l0.355-2.132h-2.025L656.533,173h2.1Zm16.3-9.982h-2.1L671.163,173h2.1Zm6.463,18.472c3.624,0,6.5-2.735,7.176-6.891,0.7-4.334-1.35-7.211-5.115-7.211-3.624,0-6.5,2.735-7.176,6.927C676.934,170.407,678.976,173.284,682.741,173.284Zm0.036-1.883c-2.735,0-3.552-2.344-3.126-5.008,0.453-2.806,2.238-5.329,5.115-5.329,2.735,0,3.552,2.38,3.126,5.045C687.439,168.924,685.654,171.4,682.777,171.4Zm13.21,1.919a5.171,5.171,0,0,0,4.4-2.167H700.5L700.178,173h2.1l1.492-8.987c0.71-4.334-2.487-4.831-4.227-4.831a6.658,6.658,0,0,0-6,3.2l1.847,0.71a4.671,4.671,0,0,1,3.978-2.06c1.918,0,2.629,1.03,2.345,2.771v0.071c-0.178.994-1.208,0.923-3.73,1.243-2.593.32-5.435,0.888-5.968,4.014C691.582,171.792,693.393,173.32,695.987,173.32Zm0.6-1.883c-1.669,0-2.735-.746-2.486-2.2,0.249-1.6,1.812-2.1,3.481-2.309,0.888-.107,3.3-0.356,3.73-0.782L701,168.062A4.289,4.289,0,0,1,696.59,171.437Zm14.667,1.847a5.166,5.166,0,0,0,4.369-2.38h0.249L715.52,173h2.024l3.02-18.188h-2.1l-1.119,6.714h-0.178a3.382,3.382,0,0,0-3.552-2.344c-3.446,0-6.279,2.735-7,7.033C705.911,170.549,707.838,173.284,711.257,173.284Zm0.515-1.883c-2.629,0-3.526-2.308-3.037-5.221,0.47-2.878,2.087-5.116,4.742-5.116,2.566,0,3.65,2.061,3.144,5.116C716.106,169.27,714.294,171.4,711.772,171.4Zm15.572,1.883a6.446,6.446,0,0,0,6-3.445l-1.918-.569a4.023,4.023,0,0,1-3.766,2.131c-2.628,0-4.031-1.172-3.525-4.511h9.919L734.2,166c0.862-5.08-1.873-6.82-4.724-6.82-3.685,0-6.634,2.913-7.318,7.1C721.456,170.478,723.41,173.284,727.344,173.284Zm-2.913-8.206a5.268,5.268,0,0,1,4.724-4.014c2.283,0,3.455,1.706,3.055,4.014h-7.779ZM736.255,173h2.1l1.421-8.632a3.953,3.953,0,0,1,3.978-3.2,2.462,2.462,0,0,1,1.173.213l0.994-1.74a2.361,2.361,0,0,0-1.634-.5,4.5,4.5,0,0,0-3.943,2.274H740.2l0.355-2.061h-2.024Z"/>
+  <path class="cls-3" d="M223.055,40.6l36.161,20.584L223.055,82.439,231.011,94.4l43.337-26.926V54.9l-43.5-26.926Zm97.968,76.216v-13.8H265.391v13.8h55.632Z"/>
+  <path class="cls-4" d="M344.32,103.5a12.975,12.975,0,0,1-5.424-3.824,17.885,17.885,0,0,1-3.269-5.911,31.164,31.164,0,0,1-1.46-6.815,40.017,40.017,0,0,1-.07-7.093,64.082,64.082,0,0,1,.974-7.232l2.921-18.081a47.854,47.854,0,0,1,2.712-9.6,26.529,26.529,0,0,1,5.424-8.484,23.518,23.518,0,0,1,8.345-5.7,25.528,25.528,0,0,1,9.666-1.947,20.709,20.709,0,0,1,5.633.765,11.788,11.788,0,0,1,4.728,2.573,14.385,14.385,0,0,1,3.269,4.312,21.361,21.361,0,0,1,1.808,5.146L386.253,1.41h19.471L388.9,103.635h-19.61l1.53-9.458a20.581,20.581,0,0,1-3.755,4.52,24.418,24.418,0,0,1-4.8,3.408,20.667,20.667,0,0,1-5.424,2.016,26.173,26.173,0,0,1-5.633.626A19.058,19.058,0,0,1,344.32,103.5Zm21.836-15.507a9.49,9.49,0,0,0,3.616-2.086,10.662,10.662,0,0,0,2.5-3.407,13.948,13.948,0,0,0,1.182-3.894l3.06-18.08q0.277-1.669.417-3.407A28.276,28.276,0,0,0,377,53.7a15.584,15.584,0,0,0-.487-3.268,9.346,9.346,0,0,0-1.252-2.851,5.5,5.5,0,0,0-2.364-1.947,7.659,7.659,0,0,0-3.2-.7,12.622,12.622,0,0,0-4.659.9,11.3,11.3,0,0,0-4.033,2.712,12.64,12.64,0,0,0-2.643,4.033,17.554,17.554,0,0,0-1.182,4.59l-3.06,18.081a23.192,23.192,0,0,0-.348,4.729,11.38,11.38,0,0,0,.974,4.381,6.567,6.567,0,0,0,2.921,3.129,8.9,8.9,0,0,0,4.381,1.113A13.776,13.776,0,0,0,366.156,87.988Zm33.588-.487h16.689l11.544-69.958H415.46V1.41h34.77L435.905,87.5h12.517v16.134H399.744V87.5ZM468.31,29.922h19.611l-1.53,9.319a31.287,31.287,0,0,1,3.825-4.451,21.413,21.413,0,0,1,4.728-3.477,19.049,19.049,0,0,1,5.425-1.947A29.445,29.445,0,0,1,506,28.809a19.093,19.093,0,0,1,6.885,1.252,13.041,13.041,0,0,1,5.424,3.825,18.154,18.154,0,0,1,3.268,5.841,30.317,30.317,0,0,1,1.461,6.746,41.152,41.152,0,0,1,.069,7.163q-0.28,3.687-.834,7.3l-3.06,18.081a48.3,48.3,0,0,1-2.712,9.457,27.978,27.978,0,0,1-5.355,8.553A22.306,22.306,0,0,1,502.8,102.8a26.069,26.069,0,0,1-9.735,1.947,20.92,20.92,0,0,1-5.563-.765,11.135,11.135,0,0,1-4.729-2.573,14.869,14.869,0,0,1-3.2-4.311,26.622,26.622,0,0,1-1.947-5.146l-6.676,40.194H451.482ZM492.163,87.71A11.256,11.256,0,0,0,496.2,85a12.482,12.482,0,0,0,2.643-4.1,24.835,24.835,0,0,0,1.321-4.52l2.921-18.081a23.24,23.24,0,0,0,.347-4.729,11.453,11.453,0,0,0-.973-4.381,6.61,6.61,0,0,0-2.921-3.129,8.932,8.932,0,0,0-4.381-1.113,13.761,13.761,0,0,0-4.1.626,9.46,9.46,0,0,0-3.616,2.086A9.791,9.791,0,0,0,485,51.062a21.249,21.249,0,0,0-1.252,3.894l-2.92,18.081q-0.28,1.669-.487,3.407a16.193,16.193,0,0,0-.07,3.338,25.184,25.184,0,0,0,.487,3.2A8.979,8.979,0,0,0,481.94,85.9a5.3,5.3,0,0,0,2.365,2.017,7.619,7.619,0,0,0,3.2.7A12.589,12.589,0,0,0,492.163,87.71Z"/>
 </svg>
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index da5f26257..d773d5a1c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -125,11 +125,12 @@ jobs:
           sudo apt -y install zip pandoc man sed
           cat > ./requirements.txt << EOF
           python=3.10.*
+          pyinstaller
           brotli-python
           EOF
           python devscripts/install_deps.py --print \
             --exclude brotli --exclude brotlicffi \
-            --include secretstorage --include pyinstaller >> ./requirements.txt
+            --include secretstorage >> ./requirements.txt
           mamba create -n build --file ./requirements.txt
 
       - name: Prepare
@@ -247,7 +248,7 @@ jobs:
         run: |
           brew install coreutils
           python3 devscripts/install_deps.py --user -o --include build
-          python3 devscripts/install_deps.py --print --include pyinstaller_macos > requirements.txt
+          python3 devscripts/install_deps.py --print --include pyinstaller > requirements.txt
           # We need to ignore wheels otherwise we break universal2 builds
           python3 -m pip install -U --user --no-binary :all: -r requirements.txt
           # We need to fuse our own universal2 wheels for curl_cffi
@@ -319,7 +320,7 @@ jobs:
         run: |
           brew install coreutils
           python3 devscripts/install_deps.py --user -o --include build
-          python3 devscripts/install_deps.py --user --include pyinstaller_macos --include curl_cffi
+          python3 devscripts/install_deps.py --user --include pyinstaller --include curl_cffi
 
       - name: Prepare
         run: |
diff --git a/README.md b/README.md
index f1b133438..d0683a34a 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
 </div>
 <!-- MANPAGE: END EXCLUDED SECTION -->
 
-yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on the now inactive [youtube-dlc](https://github.com/blackjack4494/yt-dlc). The main focus of this project is adding new features and patches while also keeping up to date with the original project
+yt-dlp is a feature-rich command-line audio/video downloader with support for [thousands of sites](supportedsites.md). The project is a fork of [youtube-dl](https://github.com/ytdl-org/youtube-dl) based on the now inactive [youtube-dlc](https://github.com/blackjack4494/yt-dlc).
 
 <!-- MANPAGE: MOVE "USAGE AND OPTIONS" SECTION HERE -->
 
@@ -1805,9 +1805,12 @@ #### niconicochannelplus
 * `max_comments`: Maximum number of comments to extract - default is `120`
 
 #### tiktok
-* `api_hostname`: Hostname to use for mobile API requests, e.g. `api-h2.tiktokv.com`
-* `app_version`: App version to call mobile APIs with - should be set along with `manifest_app_version`, e.g. `20.2.1`
-* `manifest_app_version`: Numeric app version to call mobile APIs with, e.g. `221`
+* `api_hostname`: Hostname to use for mobile API calls, e.g. `api22-normal-c-alisg.tiktokv.com`
+* `app_name`: Default app name to use with mobile API calls, e.g. `trill`
+* `app_version`: Default app version to use with mobile API calls - should be set along with `manifest_app_version`, e.g. `34.1.2`
+* `manifest_app_version`: Default numeric app version to use with mobile API calls, e.g. `2023401020`
+* `aid`: Default app ID to use with API calls, e.g. `1180`
+* `app_info`: One or more app info strings in the format of `<iid>/[app_name]/[app_version]/[manifest_app_version]/[aid]`, where `iid` is the unique app install ID. `iid` is the only required value; all other values and their `/` separators can be omitted, e.g. `tiktok:app_info=1234567890123456789` or `tiktok:app_info=123,456/trill///1180,789//34.0.1/340001`
 
 #### rokfinchannel
 * `tab`: Which tab to download - one of `new`, `top`, `videos`, `podcasts`, `streams`, `stacks`
diff --git a/bundle/py2exe.py b/bundle/py2exe.py
index ccb52eaa2..281167492 100755
--- a/bundle/py2exe.py
+++ b/bundle/py2exe.py
@@ -28,7 +28,7 @@ def main():
         }],
         version_info={
             'version': VERSION,
-            'description': 'A youtube-dl fork with additional features and patches',
+            'description': 'A feature-rich command-line audio/video downloader',
             'comments': 'Official repository: <https://github.com/yt-dlp/yt-dlp>',
             'product_name': 'yt-dlp',
             'product_version': VERSION,
diff --git a/devscripts/install_deps.py b/devscripts/install_deps.py
index 889d9abeb..d33fc637c 100755
--- a/devscripts/install_deps.py
+++ b/devscripts/install_deps.py
@@ -10,6 +10,8 @@
 import re
 import subprocess
 
+from pathlib import Path
+
 from devscripts.tomlparse import parse_toml
 from devscripts.utils import read_file
 
@@ -17,17 +19,23 @@
 def parse_args():
     parser = argparse.ArgumentParser(description='Install dependencies for yt-dlp')
     parser.add_argument(
-        'input', nargs='?', metavar='TOMLFILE', default='pyproject.toml', help='Input file (default: %(default)s)')
+        'input', nargs='?', metavar='TOMLFILE', default=Path(__file__).parent.parent / 'pyproject.toml',
+        help='input file (default: %(default)s)')
     parser.add_argument(
-        '-e', '--exclude', metavar='DEPENDENCY', action='append', help='Exclude a dependency')
+        '-e', '--exclude', metavar='DEPENDENCY', action='append',
+        help='exclude a dependency')
     parser.add_argument(
-        '-i', '--include', metavar='GROUP', action='append', help='Include an optional dependency group')
+        '-i', '--include', metavar='GROUP', action='append',
+        help='include an optional dependency group')
     parser.add_argument(
-        '-o', '--only-optional', action='store_true', help='Only install optional dependencies')
+        '-o', '--only-optional', action='store_true',
+        help='only install optional dependencies')
     parser.add_argument(
-        '-p', '--print', action='store_true', help='Only print a requirements.txt to stdout')
+        '-p', '--print', action='store_true',
+        help='only print requirements to stdout')
     parser.add_argument(
-        '-u', '--user', action='store_true', help='Install with pip as --user')
+        '-u', '--user', action='store_true',
+        help='install with pip as --user')
     return parser.parse_args()
 
 
@@ -37,24 +45,16 @@ def main():
     optional_groups = project_table['optional-dependencies']
     excludes = args.exclude or []
 
-    deps = []
+    targets = []
     if not args.only_optional:  # `-o` should exclude 'dependencies' and the 'default' group
-        deps.extend(project_table['dependencies'])
+        targets.extend(project_table['dependencies'])
         if 'default' not in excludes:  # `--exclude default` should exclude entire 'default' group
-            deps.extend(optional_groups['default'])
-
-    def name(dependency):
-        return re.match(r'[\w-]+', dependency)[0].lower()
-
-    target_map = {name(dep): dep for dep in deps}
+            targets.extend(optional_groups['default'])
 
     for include in filter(None, map(optional_groups.get, args.include or [])):
-        target_map.update(zip(map(name, include), include))
-
-    for exclude in map(name, excludes):
-        target_map.pop(exclude, None)
+        targets.extend(include)
 
-    targets = list(target_map.values())
+    targets = [t for t in targets if re.match(r'[\w-]+', t).group(0).lower() not in excludes]
 
     if args.print:
         for target in targets:
diff --git a/devscripts/prepare_manpage.py b/devscripts/prepare_manpage.py
index 9b12e71e5..009e7bba1 100644
--- a/devscripts/prepare_manpage.py
+++ b/devscripts/prepare_manpage.py
@@ -24,7 +24,7 @@
 
 # NAME
 
-yt\-dlp \- A youtube-dl fork with additional features and patches
+yt\-dlp \- A feature\-rich command\-line audio/video downloader
 
 # SYNOPSIS
 
diff --git a/pyproject.toml b/pyproject.toml
index aebbadbcb..9faf53b9c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ maintainers = [
     {name = "bashonly", email = "bashonly@protonmail.com"},
     {name = "coletdjnz", email = "coletdjnz@protonmail.com"},
 ]
-description = "A youtube-dl fork with additional features and patches"
+description = "A feature-rich command-line audio/video downloader"
 readme = "README.md"
 requires-python = ">=3.8"
 keywords = [
@@ -69,8 +69,10 @@ dev = [
     "isort",
     "pytest",
 ]
-pyinstaller = ["pyinstaller>=6.3"]
-pyinstaller_macos = ["pyinstaller==5.13.2"]  # needed for curl_cffi builds
+pyinstaller = [
+    "pyinstaller>=6.3; sys_platform!='darwin'",
+    "pyinstaller==5.13.2; sys_platform=='darwin'",  # needed for curl_cffi
+]
 py2exe = ["py2exe>=0.12"]
 
 [project.urls]
diff --git a/yt_dlp/downloader/external.py b/yt_dlp/downloader/external.py
index ce5eeb0a9..8b0b94e72 100644
--- a/yt_dlp/downloader/external.py
+++ b/yt_dlp/downloader/external.py
@@ -491,7 +491,7 @@ def _call_downloader(self, tmpfilename, info_dict):
         if not self.params.get('verbose'):
             args += ['-hide_banner']
 
-        args += traverse_obj(info_dict, ('downloader_options', 'ffmpeg_args'), default=[])
+        args += traverse_obj(info_dict, ('downloader_options', 'ffmpeg_args', ...))
 
         # These exists only for compatibility. Extractors should use
         # info_dict['downloader_options']['ffmpeg_args'] instead
@@ -615,6 +615,8 @@ def _call_downloader(self, tmpfilename, info_dict):
         else:
             args += ['-f', EXT_TO_OUT_FORMATS.get(ext, ext)]
 
+        args += traverse_obj(info_dict, ('downloader_options', 'ffmpeg_args_out', ...))
+
         args += self._configuration_args(('_o1', '_o', ''))
 
         args = [encodeArgument(opt) for opt in args]
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index c75365536..36d0853a0 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -590,6 +590,7 @@
     FacebookReelIE,
     FacebookAdsIE,
 )
+from .fathom import FathomIE
 from .fancode import (
     FancodeVodIE,
     FancodeLiveIE
@@ -989,6 +990,10 @@
     LnkGoIE,
     LnkIE,
 )
+from .loom import (
+    LoomIE,
+    LoomFolderIE,
+)
 from .lovehomeporn import LoveHomePornIE
 from .lrt import (
     LRTVODIE,
@@ -1750,6 +1755,7 @@
     ShahidIE,
     ShahidShowIE,
 )
+from .sharepoint import SharePointIE
 from .sharevideos import ShareVideosEmbedIE
 from .sibnet import SibnetEmbedIE
 from .shemaroome import ShemarooMeIE
diff --git a/yt_dlp/extractor/box.py b/yt_dlp/extractor/box.py
index 7281b3c6a..008c011cc 100644
--- a/yt_dlp/extractor/box.py
+++ b/yt_dlp/extractor/box.py
@@ -3,6 +3,7 @@
 
 from .common import InfoExtractor
 from ..utils import (
+    ExtractorError,
     parse_iso8601,
     update_url_query,
     url_or_none,
@@ -11,8 +12,8 @@
 
 
 class BoxIE(InfoExtractor):
-    _VALID_URL = r'https?://(?:[^.]+\.)?app\.box\.com/s/(?P<shared_name>[^/?#]+)/file/(?P<id>\d+)'
-    _TEST = {
+    _VALID_URL = r'https?://(?:[^.]+\.)?app\.box\.com/s/(?P<shared_name>[^/?#]+)(?:/file/(?P<id>\d+))?'
+    _TESTS = [{
         'url': 'https://mlssoccer.app.box.com/s/0evd2o3e08l60lr4ygukepvnkord1o1x/file/510727257538',
         'md5': '1f81b2fd3960f38a40a3b8823e5fcd43',
         'info_dict': {
@@ -25,14 +26,36 @@ class BoxIE(InfoExtractor):
             'uploader_id': '235196876',
         },
         'params': {'skip_download': 'dash fragment too small'},
-    }
+    }, {
+        'url': 'https://utexas.app.box.com/s/2x6vanv85fdl8j2eqlcxmv0gp1wvps6e',
+        'info_dict': {
+            'id': '787379022466',
+            'ext': 'mp4',
+            'title': 'Webinar recording: Take the Leap!.mp4',
+            'uploader': 'Patricia Mosele',
+            'timestamp': 1615824864,
+            'upload_date': '20210315',
+            'uploader_id': '239068974',
+        },
+        'params': {'skip_download': 'dash fragment too small'},
+    }]
 
     def _real_extract(self, url):
         shared_name, file_id = self._match_valid_url(url).groups()
-        webpage = self._download_webpage(url, file_id)
-        request_token = self._parse_json(self._search_regex(
-            r'Box\.config\s*=\s*({.+?});', webpage,
-            'Box config'), file_id)['requestToken']
+        webpage = self._download_webpage(url, file_id or shared_name)
+
+        if not file_id:
+            post_stream_data = self._search_json(
+                r'Box\.postStreamData\s*=', webpage, 'Box post-stream data', shared_name)
+            shared_item = traverse_obj(
+                post_stream_data, ('/app-api/enduserapp/shared-item', {dict})) or {}
+            if shared_item.get('itemType') != 'file':
+                raise ExtractorError('The requested resource is not a file', expected=True)
+
+            file_id = str(shared_item['itemID'])
+
+        request_token = self._search_json(
+            r'Box\.config\s*=', webpage, 'Box config', file_id)['requestToken']
         access_token = self._download_json(
             'https://app.box.com/app-api/enduserapp/elements/tokens', file_id,
             'Downloading token JSON metadata',
diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py
index e776ccae9..bd318a7f4 100644
--- a/yt_dlp/extractor/common.py
+++ b/yt_dlp/extractor/common.py
@@ -37,6 +37,7 @@
     IncompleteRead,
     network_exceptions,
 )
+from ..networking.impersonate import ImpersonateTarget
 from ..utils import (
     IDENTITY,
     JSON_LD_RE,
@@ -246,7 +247,8 @@ class InfoExtractor:
                     * downloader_options  A dictionary of downloader options
                                  (For internal use only)
                                  * http_chunk_size Chunk size for HTTP downloads
-                                 * ffmpeg_args     Extra arguments for ffmpeg downloader
+                                 * ffmpeg_args     Extra arguments for ffmpeg downloader (input)
+                                 * ffmpeg_args_out Extra arguments for ffmpeg downloader (output)
                     * is_dash_periods  Whether the format is a result of merging
                                  multiple DASH periods.
                     RTMP formats can also have the additional fields: page_url,
@@ -817,7 +819,7 @@ def __can_accept_status_code(err, expected_status):
         else:
             return err.status in variadic(expected_status)
 
-    def _create_request(self, url_or_request, data=None, headers=None, query=None):
+    def _create_request(self, url_or_request, data=None, headers=None, query=None, extensions=None):
         if isinstance(url_or_request, urllib.request.Request):
             self._downloader.deprecation_warning(
                 'Passing a urllib.request.Request to _create_request() is deprecated. '
@@ -826,10 +828,11 @@ def _create_request(self, url_or_request, data=None, headers=None, query=None):
         elif not isinstance(url_or_request, Request):
             url_or_request = Request(url_or_request)
 
-        url_or_request.update(data=data, headers=headers, query=query)
+        url_or_request.update(data=data, headers=headers, query=query, extensions=extensions)
         return url_or_request
 
-    def _request_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, data=None, headers=None, query=None, expected_status=None):
+    def _request_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, data=None,
+                         headers=None, query=None, expected_status=None, impersonate=None, require_impersonation=False):
         """
         Return the response handle.
 
@@ -860,8 +863,31 @@ def _request_webpage(self, url_or_request, video_id, note=None, errnote=None, fa
             headers = (headers or {}).copy()
             headers.setdefault('X-Forwarded-For', self._x_forwarded_for_ip)
 
+        extensions = {}
+
+        if impersonate in (True, ''):
+            impersonate = ImpersonateTarget()
+        requested_targets = [
+            t if isinstance(t, ImpersonateTarget) else ImpersonateTarget.from_str(t)
+            for t in variadic(impersonate)
+        ] if impersonate else []
+
+        available_target = next(filter(self._downloader._impersonate_target_available, requested_targets), None)
+        if available_target:
+            extensions['impersonate'] = available_target
+        elif requested_targets:
+            message = 'The extractor is attempting impersonation, but '
+            message += (
+                'no impersonate target is available' if not str(impersonate)
+                else f'none of these impersonate targets are available: "{", ".join(map(str, requested_targets))}"')
+            info_msg = ('see  https://github.com/yt-dlp/yt-dlp#impersonation  '
+                        'for information on installing the required dependencies')
+            if require_impersonation:
+                raise ExtractorError(f'{message}; {info_msg}', expected=True)
+            self.report_warning(f'{message}; if you encounter errors, then {info_msg}', only_once=True)
+
         try:
-            return self._downloader.urlopen(self._create_request(url_or_request, data, headers, query))
+            return self._downloader.urlopen(self._create_request(url_or_request, data, headers, query, extensions))
         except network_exceptions as err:
             if isinstance(err, HTTPError):
                 if self.__can_accept_status_code(err, expected_status):
@@ -880,13 +906,14 @@ def _request_webpage(self, url_or_request, video_id, note=None, errnote=None, fa
                 return False
 
     def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None, fatal=True,
-                                 encoding=None, data=None, headers={}, query={}, expected_status=None):
+                                 encoding=None, data=None, headers={}, query={}, expected_status=None,
+                                 impersonate=None, require_impersonation=False):
         """
         Return a tuple (page content as string, URL handle).
 
         Arguments:
         url_or_request -- plain text URL as a string or
-            a urllib.request.Request object
+            a yt_dlp.networking.Request object
         video_id -- Video/playlist/item identifier (string)
 
         Keyword arguments:
@@ -911,13 +938,22 @@ def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=
                   returning True if it should be accepted
             Note that this argument does not affect success status codes (2xx)
             which are always accepted.
+        impersonate -- the impersonate target. Can be any of the following entities:
+                - an instance of yt_dlp.networking.impersonate.ImpersonateTarget
+                - a string in the format of CLIENT[:OS]
+                - a list or a tuple of CLIENT[:OS] strings or ImpersonateTarget instances
+                - a boolean value; True means any impersonate target is sufficient
+        require_impersonation -- flag to toggle whether the request should raise an error
+            if impersonation is not possible (bool, default: False)
         """
 
         # Strip hashes from the URL (#1038)
         if isinstance(url_or_request, str):
             url_or_request = url_or_request.partition('#')[0]
 
-        urlh = self._request_webpage(url_or_request, video_id, note, errnote, fatal, data=data, headers=headers, query=query, expected_status=expected_status)
+        urlh = self._request_webpage(url_or_request, video_id, note, errnote, fatal, data=data,
+                                     headers=headers, query=query, expected_status=expected_status,
+                                     impersonate=impersonate, require_impersonation=require_impersonation)
         if urlh is False:
             assert not fatal
             return False
@@ -1046,17 +1082,20 @@ def parse(ie, content, *args, errnote=errnote, **kwargs):
             return getattr(ie, parser)(content, *args, **kwargs)
 
         def download_handle(self, url_or_request, video_id, note=note, errnote=errnote, transform_source=None,
-                            fatal=True, encoding=None, data=None, headers={}, query={}, expected_status=None):
+                            fatal=True, encoding=None, data=None, headers={}, query={}, expected_status=None,
+                            impersonate=None, require_impersonation=False):
             res = self._download_webpage_handle(
                 url_or_request, video_id, note=note, errnote=errnote, fatal=fatal, encoding=encoding,
-                data=data, headers=headers, query=query, expected_status=expected_status)
+                data=data, headers=headers, query=query, expected_status=expected_status,
+                impersonate=impersonate, require_impersonation=require_impersonation)
             if res is False:
                 return res
             content, urlh = res
             return parse(self, content, video_id, transform_source=transform_source, fatal=fatal, errnote=errnote), urlh
 
         def download_content(self, url_or_request, video_id, note=note, errnote=errnote, transform_source=None,
-                             fatal=True, encoding=None, data=None, headers={}, query={}, expected_status=None):
+                             fatal=True, encoding=None, data=None, headers={}, query={}, expected_status=None,
+                             impersonate=None, require_impersonation=False):
             if self.get_param('load_pages'):
                 url_or_request = self._create_request(url_or_request, data, headers, query)
                 filename = self._request_dump_filename(url_or_request.url, video_id)
@@ -1079,6 +1118,8 @@ def download_content(self, url_or_request, video_id, note=note, errnote=errnote,
                 'headers': headers,
                 'query': query,
                 'expected_status': expected_status,
+                'impersonate': impersonate,
+                'require_impersonation': require_impersonation,
             }
             if parser is None:
                 kwargs.pop('transform_source')
diff --git a/yt_dlp/extractor/fathom.py b/yt_dlp/extractor/fathom.py
new file mode 100644
index 000000000..1df7d96fe
--- /dev/null
+++ b/yt_dlp/extractor/fathom.py
@@ -0,0 +1,54 @@
+import json
+
+from .common import InfoExtractor
+from ..utils import (
+    extract_attributes,
+    float_or_none,
+    get_element_html_by_id,
+    parse_iso8601,
+)
+from ..utils.traversal import traverse_obj
+
+
+class FathomIE(InfoExtractor):
+    _VALID_URL = r'https?://(?:www\.)?fathom\.video/share/(?P<id>[^/?#&]+)'
+    _TESTS = [{
+        'url': 'https://fathom.video/share/G9mkjkspnohVVZ_L5nrsoPycyWcB8y7s',
+        'md5': '0decd5343b8f30ae268625e79a02b60f',
+        'info_dict': {
+            'id': '47200596',
+            'ext': 'mp4',
+            'title': 'eCom Inucbator - Coaching Session',
+            'duration': 8125.380507,
+            'timestamp': 1699048914,
+            'upload_date': '20231103',
+        },
+    }, {
+        'url': 'https://fathom.video/share/mEws3bybftHL2QLymxYEDeE21vtLxGVm',
+        'md5': '4f5cb382126c22d1aba8a939f9c49690',
+        'info_dict': {
+            'id': '46812957',
+            'ext': 'mp4',
+            'title': 'Jon, Lawrence, Neman chat about practice',
+            'duration': 3571.517847,
+            'timestamp': 1698933600,
+            'upload_date': '20231102',
+        },
+    }]
+
+    def _real_extract(self, url):
+        display_id = self._match_id(url)
+        webpage = self._download_webpage(url, display_id)
+        props = traverse_obj(
+            get_element_html_by_id('app', webpage), ({extract_attributes}, 'data-page', {json.loads}, 'props'))
+        video_id = str(props['call']['id'])
+
+        return {
+            'id': video_id,
+            'formats': self._extract_m3u8_formats(props['call']['video_url'], video_id, 'mp4'),
+            **traverse_obj(props, {
+                'title': ('head', 'title', {str}),
+                'duration': ('duration', {float_or_none}),
+                'timestamp': ('call', 'started_at', {parse_iso8601}),
+            }),
+        }
diff --git a/yt_dlp/extractor/loom.py b/yt_dlp/extractor/loom.py
new file mode 100644
index 000000000..1191aa17e
--- /dev/null
+++ b/yt_dlp/extractor/loom.py
@@ -0,0 +1,461 @@
+import json
+import textwrap
+import urllib.parse
+import uuid
+
+from .common import InfoExtractor
+from ..utils import (
+    ExtractorError,
+    determine_ext,
+    filter_dict,
+    get_first,
+    int_or_none,
+    parse_iso8601,
+    update_url,
+    url_or_none,
+    variadic,
+)
+from ..utils.traversal import traverse_obj
+
+
+class LoomIE(InfoExtractor):
+    IE_NAME = 'loom'
+    _VALID_URL = r'https?://(?:www\.)?loom\.com/(?:share|embed)/(?P<id>[\da-f]{32})'
+    _EMBED_REGEX = [rf'<iframe[^>]+\bsrc=["\'](?P<url>{_VALID_URL})']
+    _TESTS = [{
+        # m3u8 raw-url, mp4 transcoded-url, cdn url == raw-url, json subs only
+        'url': 'https://www.loom.com/share/43d05f362f734614a2e81b4694a3a523',
+        'md5': 'bfc2d7e9c2e0eb4813212230794b6f42',
+        'info_dict': {
+            'id': '43d05f362f734614a2e81b4694a3a523',
+            'ext': 'mp4',
+            'title': 'A Ruler for Windows - 28 March 2022',
+            'uploader': 'wILLIAM PIP',
+            'upload_date': '20220328',
+            'timestamp': 1648454238,
+            'duration': 27,
+        },
+    }, {
+        # webm raw-url, mp4 transcoded-url, cdn url == transcoded-url, no subs
+        'url': 'https://www.loom.com/share/c43a642f815f4378b6f80a889bb73d8d',
+        'md5': '70f529317be8cf880fcc2c649a531900',
+        'info_dict': {
+            'id': 'c43a642f815f4378b6f80a889bb73d8d',
+            'ext': 'webm',
+            'title': 'Lilah Nielsen Intro Video',
+            'uploader': 'Lilah Nielsen',
+            'upload_date': '20200826',
+            'timestamp': 1598480716,
+            'duration': 20,
+        },
+    }, {
+        # m3u8 raw-url, mp4 transcoded-url, cdn url == raw-url, vtt sub and json subs
+        'url': 'https://www.loom.com/share/9458bcbf79784162aa62ffb8dd66201b',
+        'md5': '51737ec002969dd28344db4d60b9cbbb',
+        'info_dict': {
+            'id': '9458bcbf79784162aa62ffb8dd66201b',
+            'ext': 'mp4',
+            'title': 'Sharing screen with gpt-4',
+            'description': 'Sharing screen with GPT 4 vision model and asking questions to guide through blender.',
+            'uploader': 'Suneel Matham',
+            'chapters': 'count:3',
+            'upload_date': '20231109',
+            'timestamp': 1699518978,
+            'duration': 93,
+        },
+    }, {
+        # mpd raw-url, mp4 transcoded-url, cdn url == raw-url, no subs
+        'url': 'https://www.loom.com/share/24351eb8b317420289b158e4b7e96ff2',
+        'info_dict': {
+            'id': '24351eb8b317420289b158e4b7e96ff2',
+            'ext': 'webm',
+            'title': 'OMFG clown',
+            'description': 'md5:285c5ee9d62aa087b7e3271b08796815',
+            'uploader': 'MrPumkin B',
+            'upload_date': '20210924',
+            'timestamp': 1632519618,
+            'duration': 210,
+        },
+        'params': {'skip_download': 'dash'},
+    }, {
+        # password-protected
+        'url': 'https://www.loom.com/share/50e26e8aeb7940189dff5630f95ce1f4',
+        'md5': '5cc7655e7d55d281d203f8ffd14771f7',
+        'info_dict': {
+            'id': '50e26e8aeb7940189dff5630f95ce1f4',
+            'ext': 'mp4',
+            'title': 'iOS Mobile Upload',
+            'uploader': 'Simon Curran',
+            'upload_date': '20200520',
+            'timestamp': 1590000123,
+            'duration': 35,
+        },
+        'params': {'videopassword': 'seniorinfants2'},
+    }, {
+        # embed, transcoded-url endpoint sends empty JSON response
+        'url': 'https://www.loom.com/embed/ddcf1c1ad21f451ea7468b1e33917e4e',
+        'md5': '8488817242a0db1cb2ad0ea522553cf6',
+        'info_dict': {
+            'id': 'ddcf1c1ad21f451ea7468b1e33917e4e',
+            'ext': 'mp4',
+            'title': 'CF Reset User\'s Password',
+            'uploader': 'Aimee Heintz',
+            'upload_date': '20220707',
+            'timestamp': 1657216459,
+            'duration': 181,
+        },
+        'expected_warnings': ['Failed to parse JSON'],
+    }]
+    _WEBPAGE_TESTS = [{
+        'url': 'https://www.loom.com/community/e1229802a8694a09909e8ba0fbb6d073-pg',
+        'md5': 'ec838cd01b576cf0386f32e1ae424609',
+        'info_dict': {
+            'id': 'e1229802a8694a09909e8ba0fbb6d073',
+            'ext': 'mp4',
+            'title': 'Rexie Jane Cimafranca - Founder\'s Presentation',
+            'uploader': 'Rexie Cimafranca',
+            'upload_date': '20230213',
+            'duration': 247,
+            'timestamp': 1676274030,
+        },
+    }]
+
+    _GRAPHQL_VARIABLES = {
+        'GetVideoSource': {
+            'acceptableMimes': ['DASH', 'M3U8', 'MP4'],
+        },
+    }
+    _GRAPHQL_QUERIES = {
+        'GetVideoSSR': textwrap.dedent('''\
+            query GetVideoSSR($videoId: ID!, $password: String) {
+              getVideo(id: $videoId, password: $password) {
+                __typename
+                ... on PrivateVideo {
+                  id
+                  status
+                  message
+                  __typename
+                }
+                ... on VideoPasswordMissingOrIncorrect {
+                  id
+                  message
+                  __typename
+                }
+                ... on RegularUserVideo {
+                  id
+                  __typename
+                  createdAt
+                  description
+                  download_enabled
+                  folder_id
+                  is_protected
+                  needs_password
+                  owner {
+                    display_name
+                    __typename
+                  }
+                  privacy
+                  s3_id
+                  name
+                  video_properties {
+                    avgBitRate
+                    client
+                    camera_enabled
+                    client_version
+                    duration
+                    durationMs
+                    format
+                    height
+                    microphone_enabled
+                    os
+                    os_version
+                    recordingClient
+                    recording_type
+                    recording_version
+                    screen_type
+                    tab_audio
+                    trim_duration
+                    width
+                    __typename
+                  }
+                  playable_duration
+                  source_duration
+                  visibility
+                }
+              }
+            }\n'''),
+        'GetVideoSource': textwrap.dedent('''\
+            query GetVideoSource($videoId: ID!, $password: String, $acceptableMimes: [CloudfrontVideoAcceptableMime]) {
+              getVideo(id: $videoId, password: $password) {
+                ... on RegularUserVideo {
+                  id
+                  nullableRawCdnUrl(acceptableMimes: $acceptableMimes, password: $password) {
+                    url
+                    __typename
+                  }
+                  __typename
+                }
+                __typename
+              }
+            }\n'''),
+        'FetchVideoTranscript': textwrap.dedent('''\
+            query FetchVideoTranscript($videoId: ID!, $password: String) {
+              fetchVideoTranscript(videoId: $videoId, password: $password) {
+                ... on VideoTranscriptDetails {
+                  id
+                  video_id
+                  source_url
+                  captions_source_url
+                  __typename
+                }
+                ... on GenericError {
+                  message
+                  __typename
+                }
+                __typename
+              }
+            }\n'''),
+        'FetchChapters': textwrap.dedent('''\
+            query FetchChapters($videoId: ID!, $password: String) {
+              fetchVideoChapters(videoId: $videoId, password: $password) {
+                ... on VideoChapters {
+                  video_id
+                  content
+                  __typename
+                }
+                ... on EmptyChaptersPayload {
+                  content
+                  __typename
+                }
+                ... on InvalidRequestWarning {
+                  message
+                  __typename
+                }
+                ... on Error {
+                  message
+                  __typename
+                }
+                __typename
+              }
+            }\n'''),
+    }
+    _APOLLO_GRAPHQL_VERSION = '0a1856c'
+
+    def _call_graphql_api(self, operations, video_id, note=None, errnote=None):
+        password = self.get_param('videopassword')
+        return self._download_json(
+            'https://www.loom.com/graphql', video_id, note or 'Downloading GraphQL JSON',
+            errnote or 'Failed to download GraphQL JSON', headers={
+                'Accept': 'application/json',
+                'Content-Type': 'application/json',
+                'x-loom-request-source': f'loom_web_{self._APOLLO_GRAPHQL_VERSION}',
+                'apollographql-client-name': 'web',
+                'apollographql-client-version': self._APOLLO_GRAPHQL_VERSION,
+            }, data=json.dumps([{
+                'operationName': operation_name,
+                'variables': {
+                    'videoId': video_id,
+                    'password': password,
+                    **self._GRAPHQL_VARIABLES.get(operation_name, {}),
+                },
+                'query': self._GRAPHQL_QUERIES[operation_name],
+            } for operation_name in variadic(operations)], separators=(',', ':')).encode())
+
+    def _call_url_api(self, endpoint, video_id):
+        response = self._download_json(
+            f'https://www.loom.com/api/campaigns/sessions/{video_id}/{endpoint}', video_id,
+            f'Downloading {endpoint} JSON', f'Failed to download {endpoint} JSON', fatal=False,
+            headers={'Accept': 'application/json', 'Content-Type': 'application/json'},
+            data=json.dumps({
+                'anonID': str(uuid.uuid4()),
+                'deviceID': None,
+                'force_original': False,  # HTTP error 401 if True
+                'password': self.get_param('videopassword'),
+            }, separators=(',', ':')).encode())
+        return traverse_obj(response, ('url', {url_or_none}))
+
+    def _extract_formats(self, video_id, metadata, gql_data):
+        formats = []
+        video_properties = traverse_obj(metadata, ('video_properties', {
+            'width': ('width', {int_or_none}),
+            'height': ('height', {int_or_none}),
+            'acodec': ('microphone_enabled', {lambda x: 'none' if x is False else None}),
+        }))
+
+        def get_formats(format_url, format_id, quality):
+            if not format_url:
+                return
+            ext = determine_ext(format_url)
+            query = urllib.parse.urlparse(format_url).query
+
+            if ext == 'm3u8':
+                # Extract pre-merged HLS formats to avoid buggy parsing of metadata in split playlists
+                format_url = format_url.replace('-split.m3u8', '.m3u8')
+                m3u8_formats = self._extract_m3u8_formats(
+                    format_url, video_id, 'mp4', m3u8_id=f'hls-{format_id}', fatal=False, quality=quality)
+                for fmt in m3u8_formats:
+                    yield {
+                        **fmt,
+                        'url': update_url(fmt['url'], query=query),
+                        'extra_param_to_segment_url': query,
+                    }
+
+            elif ext == 'mpd':
+                dash_formats = self._extract_mpd_formats(
+                    format_url, video_id, mpd_id=f'dash-{format_id}', fatal=False)
+                for fmt in dash_formats:
+                    yield {
+                        **fmt,
+                        'extra_param_to_segment_url': query,
+                        'quality': quality,
+                    }
+
+            else:
+                yield {
+                    'url': format_url,
+                    'ext': ext,
+                    'format_id': f'http-{format_id}',
+                    'quality': quality,
+                    **video_properties,
+                }
+
+        raw_url = self._call_url_api('raw-url', video_id)
+        formats.extend(get_formats(raw_url, 'raw', quality=1))  # original quality
+
+        transcoded_url = self._call_url_api('transcoded-url', video_id)
+        formats.extend(get_formats(transcoded_url, 'transcoded', quality=-1))  # transcoded quality
+
+        cdn_url = get_first(gql_data, ('data', 'getVideo', 'nullableRawCdnUrl', 'url', {url_or_none}))
+        # cdn_url is usually a dupe, but the raw-url/transcoded-url endpoints could return errors
+        valid_urls = [update_url(url, query=None) for url in (raw_url, transcoded_url) if url]
+        if cdn_url and update_url(cdn_url, query=None) not in valid_urls:
+            formats.extend(get_formats(cdn_url, 'cdn', quality=0))  # could be original or transcoded
+
+        return formats
+
+    def _real_extract(self, url):
+        video_id = self._match_id(url)
+        metadata = get_first(
+            self._call_graphql_api('GetVideoSSR', video_id, 'Downloading GraphQL metadata JSON'),
+            ('data', 'getVideo', {dict})) or {}
+
+        if metadata.get('__typename') == 'VideoPasswordMissingOrIncorrect':
+            if not self.get_param('videopassword'):
+                raise ExtractorError(
+                    'This video is password-protected, use the --video-password option', expected=True)
+            raise ExtractorError('Invalid video password', expected=True)
+
+        gql_data = self._call_graphql_api(['FetchChapters', 'FetchVideoTranscript', 'GetVideoSource'], video_id)
+        duration = traverse_obj(metadata, ('video_properties', 'duration', {int_or_none}))
+
+        return {
+            'id': video_id,
+            'duration': duration,
+            'chapters': self._extract_chapters_from_description(
+                get_first(gql_data, ('data', 'fetchVideoChapters', 'content', {str})), duration) or None,
+            'formats': self._extract_formats(video_id, metadata, gql_data),
+            'subtitles': filter_dict({
+                'en': traverse_obj(gql_data, (
+                    ..., 'data', 'fetchVideoTranscript',
+                    ('source_url', 'captions_source_url'), {
+                        'url': {url_or_none},
+                    })) or None,
+            }),
+            **traverse_obj(metadata, {
+                'title': ('name', {str}),
+                'description': ('description', {str}),
+                'uploader': ('owner', 'display_name', {str}),
+                'timestamp': ('createdAt', {parse_iso8601}),
+            }),
+        }
+
+
+class LoomFolderIE(InfoExtractor):
+    IE_NAME = 'loom:folder'
+    _VALID_URL = r'https?://(?:www\.)?loom\.com/share/folder/(?P<id>[\da-f]{32})'
+    _TESTS = [{
+        # 2 subfolders, no videos in root
+        'url': 'https://www.loom.com/share/folder/997db4db046f43e5912f10dc5f817b5c',
+        'playlist_mincount': 16,
+        'info_dict': {
+            'id': '997db4db046f43e5912f10dc5f817b5c',
+            'title': 'Blending Lessons',
+        },
+    }, {
+        # only videos, no subfolders
+        'url': 'https://www.loom.com/share/folder/9a8a87f6b6f546d9a400c8e7575ff7f2',
+        'playlist_mincount': 12,
+        'info_dict': {
+            'id': '9a8a87f6b6f546d9a400c8e7575ff7f2',
+            'title': 'List A- a, i, o',
+        },
+    }, {
+        # videos in root and empty subfolder
+        'url': 'https://www.loom.com/share/folder/886e534218c24fd292e97e9563078cc4',
+        'playlist_mincount': 21,
+        'info_dict': {
+            'id': '886e534218c24fd292e97e9563078cc4',
+            'title': 'Medicare Agent Training videos',
+        },
+    }, {
+        # videos in root and videos in subfolders
+        'url': 'https://www.loom.com/share/folder/b72c4ecdf04745da9403926d80a40c38',
+        'playlist_mincount': 21,
+        'info_dict': {
+            'id': 'b72c4ecdf04745da9403926d80a40c38',
+            'title': 'Quick Altos Q & A Tutorials',
+        },
+    }, {
+        # recursive folder extraction
+        'url': 'https://www.loom.com/share/folder/8b458a94e0e4449b8df9ea7a68fafc4e',
+        'playlist_count': 23,
+        'info_dict': {
+            'id': '8b458a94e0e4449b8df9ea7a68fafc4e',
+            'title': 'Sezer Texting Guide',
+        },
+    }, {
+        # more than 50 videos in 1 folder
+        'url': 'https://www.loom.com/share/folder/e056a91d290d47ca9b00c9d1df56c463',
+        'playlist_mincount': 61,
+        'info_dict': {
+            'id': 'e056a91d290d47ca9b00c9d1df56c463',
+            'title': 'User Videos',
+        },
+    }, {
+        # many subfolders
+        'url': 'https://www.loom.com/share/folder/c2dde8cc67454f0e99031677279d8954',
+        'playlist_mincount': 75,
+        'info_dict': {
+            'id': 'c2dde8cc67454f0e99031677279d8954',
+            'title': 'Honors 1',
+        },
+    }, {
+        'url': 'https://www.loom.com/share/folder/bae17109a68146c7803454f2893c8cf8/Edpuzzle',
+        'only_matching': True,
+    }]
+
+    def _extract_folder_data(self, folder_id):
+        return self._download_json(
+            f'https://www.loom.com/v1/folders/{folder_id}', folder_id,
+            'Downloading folder info JSON', query={'limit': '10000'})
+
+    def _extract_folder_entries(self, folder_id, initial_folder_data=None):
+        folder_data = initial_folder_data or self._extract_folder_data(folder_id)
+
+        for video in traverse_obj(folder_data, ('videos', lambda _, v: v['id'])):
+            video_id = video['id']
+            yield self.url_result(
+                f'https://www.loom.com/share/{video_id}', LoomIE, video_id, video.get('name'))
+
+        # Recurse into subfolders
+        for subfolder_id in traverse_obj(folder_data, (
+                'folders', lambda _, v: v['id'] != folder_id, 'id', {str})):
+            yield from self._extract_folder_entries(subfolder_id)
+
+    def _real_extract(self, url):
+        playlist_id = self._match_id(url)
+        playlist_data = self._extract_folder_data(playlist_id)
+
+        return self.playlist_result(
+            self._extract_folder_entries(playlist_id, playlist_data), playlist_id,
+            traverse_obj(playlist_data, ('folder', 'name', {str.strip})))
diff --git a/yt_dlp/extractor/sharepoint.py b/yt_dlp/extractor/sharepoint.py
new file mode 100644
index 000000000..d4d5af04f
--- /dev/null
+++ b/yt_dlp/extractor/sharepoint.py
@@ -0,0 +1,112 @@
+import json
+import urllib.parse
+
+from .common import InfoExtractor
+from ..utils import determine_ext, int_or_none, url_or_none
+from ..utils.traversal import traverse_obj
+
+
+class SharePointIE(InfoExtractor):
+    _BASE_URL_RE = r'https?://[\w-]+\.sharepoint\.com/'
+    _VALID_URL = [
+        rf'{_BASE_URL_RE}:v:/[a-z]/(?:[^/?#]+/)*(?P<id>[^/?#]{{46}})/?(?:$|[?#])',
+        rf'{_BASE_URL_RE}(?!:v:)(?:[^/?#]+/)*stream\.aspx\?(?:[^#]+&)?id=(?P<id>[^&#]+)',
+    ]
+    _TESTS = [{
+        'url': 'https://lut-my.sharepoint.com/:v:/g/personal/juha_eerola_student_lab_fi/EUrAmrktb4ZMhUcY9J2PqMEBD_9x_l0DyYWVgAvp-TTOMw?e=ZpQOOw',
+        'md5': '2950821d0d4937a0a76373782093b435',
+        'info_dict': {
+            'id': '01EQRS7EKKYCNLSLLPQZGIKRYY6SOY7KGB',
+            'display_id': 'EUrAmrktb4ZMhUcY9J2PqMEBD_9x_l0DyYWVgAvp-TTOMw',
+            'ext': 'mp4',
+            'title': 'CmvpJST',
+            'duration': 54.567,
+            'thumbnail': r're:https://.+/thumbnail',
+            'uploader_id': '8dcec565-a956-4b91-95e5-bacfb8bc015f',
+        },
+    }, {
+        'url': 'https://greaternyace.sharepoint.com/:v:/s/acementornydrive/ETski5eAfNVEoPRZUAyy1wEBpLgVFYWso5bjbZjfBLlPUg?e=PQUfVb',
+        'md5': 'c496a01644223273bff12e93e501afd1',
+        'info_dict': {
+            'id': '01QI4AVTZ3ESFZPAD42VCKB5CZKAGLFVYB',
+            'display_id': 'ETski5eAfNVEoPRZUAyy1wEBpLgVFYWso5bjbZjfBLlPUg',
+            'ext': 'mp4',
+            'title': '930103681233985536',
+            'duration': 3797.326,
+            'thumbnail': r're:https://.+/thumbnail',
+        },
+    }, {
+        'url': 'https://lut-my.sharepoint.com/personal/juha_eerola_student_lab_fi/_layouts/15/stream.aspx?id=%2Fpersonal%2Fjuha_eerola_student_lab_fi%2FDocuments%2FM-DL%2FCmvpJST.mp4&ga=1&referrer=StreamWebApp.Web&referrerScenario=AddressBarCopied.view',
+        'info_dict': {
+            'id': '01EQRS7EKKYCNLSLLPQZGIKRYY6SOY7KGB',
+            'display_id': '/personal/juha_eerola_student_lab_fi/Documents/M-DL/CmvpJST.mp4',
+            'ext': 'mp4',
+            'title': 'CmvpJST',
+            'duration': 54.567,
+            'thumbnail': r're:https://.+/thumbnail',
+            'uploader_id': '8dcec565-a956-4b91-95e5-bacfb8bc015f',
+        },
+        'skip': 'Session cookies needed',
+    }, {
+        'url': 'https://izoobasisschool.sharepoint.com/:v:/g/Eaqleq8COVBIvIPvod0U27oBypC6aWOkk8ptuDpmJ6arHw',
+        'only_matching': True,
+    }, {
+        'url': 'https://uskudaredutr-my.sharepoint.com/:v:/g/personal/songul_turkaydin_uskudar_edu_tr/EbTf-VRUIbtGuIN73tx1MuwBCHBOmNcWNqSLw61Fd2_o0g?e=n5Vkof',
+        'only_matching': True,
+    }, {
+        'url': 'https://epam-my.sharepoint.com/:v:/p/dzmitry_tamashevich/Ec4ZOs-rATZHjFYZWVxjczEB649FCoYFKDV_x3RxZiWAGA?e=4hswgA',
+        'only_matching': True,
+    }, {
+        'url': 'https://microsoft.sharepoint.com/:v:/t/MicrosoftSPARKRecordings-MSFTInternal/EWCyeqByVWBAt8wDvNZdV-UB0BvU5YVbKm0UHgdrUlI6dg?e=QbPck6',
+        'only_matching': True,
+    }]
+
+    def _real_extract(self, url):
+        display_id = urllib.parse.unquote(self._match_id(url))
+        webpage, urlh = self._download_webpage_handle(url, display_id)
+        if urllib.parse.urlparse(urlh.url).hostname == 'login.microsoftonline.com':
+            self.raise_login_required(
+                'Session cookies are required for this URL and can be passed '
+                'with the --cookies option. The --cookies-from-browser option will not work', method=None)
+
+        video_data = self._search_json(r'g_fileInfo\s*=', webpage, 'player config', display_id)
+        video_id = video_data['VroomItemId']
+
+        parsed_url = urllib.parse.urlparse(video_data['.transformUrl'])
+        base_media_url = urllib.parse.urlunparse(parsed_url._replace(
+            path=urllib.parse.urljoin(f'{parsed_url.path}/', '../videomanifest'),
+            query=urllib.parse.urlencode({
+                **urllib.parse.parse_qs(parsed_url.query),
+                'cTag': video_data['.ctag'],
+                'action': 'Access',
+                'part': 'index',
+            }, doseq=True)))
+
+        # Web player adds more params to the format URLs but we still get all formats without them
+        formats = self._extract_mpd_formats(
+            base_media_url, video_id, mpd_id='dash', query={'format': 'dash'}, fatal=False)
+        for hls_type in ('hls', 'hls-vnext'):
+            formats.extend(self._extract_m3u8_formats(
+                base_media_url, video_id, 'mp4', m3u8_id=hls_type,
+                query={'format': hls_type}, fatal=False, quality=-2))
+
+        if video_url := traverse_obj(video_data, ('downloadUrl', {url_or_none})):
+            formats.append({
+                'url': video_url,
+                'ext': determine_ext(video_data.get('extension') or video_data.get('name')),
+                'quality': 1,
+                'format_id': 'source',
+                'filesize': int_or_none(video_data.get('size')),
+                'vcodec': 'none' if video_data.get('isAudio') is True else None,
+            })
+
+        return {
+            'id': video_id,
+            'formats': formats,
+            'title': video_data.get('title') or video_data.get('displayName'),
+            'display_id': display_id,
+            'uploader_id': video_data.get('authorId'),
+            'duration': traverse_obj(video_data, (
+                'MediaServiceFastMetadata', {json.loads}, 'media', 'duration', {lambda x: x / 10000000})),
+            'thumbnail': url_or_none(video_data.get('thumbnailUrl')),
+        }
diff --git a/yt_dlp/extractor/thisoldhouse.py b/yt_dlp/extractor/thisoldhouse.py
index 15f8380d3..fbc12d55d 100644
--- a/yt_dlp/extractor/thisoldhouse.py
+++ b/yt_dlp/extractor/thisoldhouse.py
@@ -1,5 +1,6 @@
 import json
 
+from .brightcove import BrightcoveNewIE
 from .common import InfoExtractor
 from .zype import ZypeIE
 from ..networking import HEADRequest
@@ -8,6 +9,7 @@
     ExtractorError,
     filter_dict,
     parse_qs,
+    smuggle_url,
     try_call,
     urlencode_postdata,
 )
@@ -17,23 +19,43 @@ class ThisOldHouseIE(InfoExtractor):
     _NETRC_MACHINE = 'thisoldhouse'
     _VALID_URL = r'https?://(?:www\.)?thisoldhouse\.com/(?:watch|how-to|tv-episode|(?:[^/?#]+/)?\d+)/(?P<id>[^/?#]+)'
     _TESTS = [{
+        # Unresolved Brightcove URL embed (formerly Zype), free
         'url': 'https://www.thisoldhouse.com/furniture/21017078/how-to-build-a-storage-bench',
         'info_dict': {
-            'id': '5dcdddf673c3f956ef5db202',
+            'id': '6325298523112',
             'ext': 'mp4',
             'title': 'How to Build a Storage Bench',
             'description': 'In the workshop, Tom Silva and Kevin O\'Connor build a storage bench for an entryway.',
-            'timestamp': 1442548800,
-            'upload_date': '20150918',
-            'duration': 674,
-            'view_count': int,
-            'average_rating': 0,
-            'thumbnail': r're:^https?://.*\.jpg\?\d+$',
-            'display_id': 'how-to-build-a-storage-bench',
+            'timestamp': 1681793639,
+            'upload_date': '20230418',
+            'duration': 674.54,
+            'tags': 'count:11',
+            'uploader_id': '6314471934001',
+            'thumbnail': r're:^https?://.*\.jpg',
         },
         'params': {
             'skip_download': True,
         },
+    }, {
+        # Brightcove embed, authwalled
+        'url': 'https://www.thisoldhouse.com/glen-ridge-generational/99537/s45-e17-multi-generational',
+        'info_dict': {
+            'id': '6349675446112',
+            'ext': 'mp4',
+            'title': 'E17 | Glen Ridge Generational | Multi-Generational',
+            'description': 'md5:53c6bc2e8031f3033d693d9a3563222c',
+            'timestamp': 1711382202,
+            'upload_date': '20240325',
+            'duration': 1422.229,
+            'tags': 'count:13',
+            'uploader_id': '6314471934001',
+            'thumbnail': r're:^https?://.*\.jpg',
+        },
+        'expected_warnings': ['Login with password is not supported for this website'],
+        'params': {
+            'skip_download': True,
+        },
+        'skip': 'Requires subscription',
     }, {
         # Page no longer has video
         'url': 'https://www.thisoldhouse.com/watch/arlington-arts-crafts-arts-and-crafts-class-begins',
@@ -98,7 +120,15 @@ def _real_extract(self, url):
 
         video_url, video_id = self._search_regex(
             r'<iframe[^>]+src=[\'"]((?:https?:)?//(?:www\.)?thisoldhouse\.(?:chorus\.build|com)/videos/zype/([0-9a-f]{24})[^\'"]*)[\'"]',
-            webpage, 'video url', group=(1, 2))
-        video_url = self._request_webpage(HEADRequest(video_url), video_id, 'Resolving Zype URL').url
+            webpage, 'zype url', group=(1, 2), default=(None, None))
+        if video_url:
+            video_url = self._request_webpage(HEADRequest(video_url), video_id, 'Resolving Zype URL').url
+            return self.url_result(video_url, ZypeIE, video_id)
 
-        return self.url_result(video_url, ZypeIE, video_id)
+        video_url, video_id = self._search_regex([
+            r'<iframe[^>]+src=[\'"]((?:https?:)?//players\.brightcove\.net/\d+/\w+/index\.html\?videoId=(\d+))',
+            r'<iframe[^>]+src=[\'"]((?:https?:)?//(?:www\.)thisoldhouse\.com/videos/brightcove/(\d+))'],
+            webpage, 'iframe url', group=(1, 2))
+        if not parse_qs(video_url).get('videoId'):
+            video_url = self._request_webpage(HEADRequest(video_url), video_id, 'Resolving Brightcove URL').url
+        return self.url_result(smuggle_url(video_url, {'referrer': url}), BrightcoveNewIE, video_id)
diff --git a/yt_dlp/extractor/tiktok.py b/yt_dlp/extractor/tiktok.py
index 02545bc79..295e14932 100644
--- a/yt_dlp/extractor/tiktok.py
+++ b/yt_dlp/extractor/tiktok.py
@@ -4,6 +4,7 @@
 import re
 import string
 import time
+import uuid
 
 from .common import InfoExtractor
 from ..compat import compat_urllib_parse_urlparse
@@ -30,19 +31,65 @@
 
 
 class TikTokBaseIE(InfoExtractor):
-    _APP_VERSIONS = [('26.1.3', '260103'), ('26.1.2', '260102'), ('26.1.1', '260101'), ('25.6.2', '250602')]
-    _WORKING_APP_VERSION = None
-    _APP_NAME = 'trill'
-    _AID = 1180
     _UPLOADER_URL_FORMAT = 'https://www.tiktok.com/@%s'
     _WEBPAGE_HOST = 'https://www.tiktok.com/'
     QUALITIES = ('360p', '540p', '720p', '1080p')
 
+    _APP_INFO_DEFAULTS = {
+        # unique "install id"
+        'iid': None,
+        # TikTok (KR/PH/TW/TH/VN) = trill, TikTok (rest of world) = musical_ly, Douyin = aweme
+        'app_name': 'musical_ly',
+        'app_version': '34.1.2',
+        'manifest_app_version': '2023401020',
+        # "app id": aweme = 1128, trill = 1180, musical_ly = 1233, universal = 0
+        'aid': '0',
+    }
+    _KNOWN_APP_INFO = [
+        '7351144126450059040',
+        '7351149742343391009',
+        '7351153174894626592',
+    ]
+    _APP_INFO_POOL = None
+    _APP_INFO = None
+    _APP_USER_AGENT = None
+
     @property
     def _API_HOSTNAME(self):
         return self._configuration_arg(
             'api_hostname', ['api22-normal-c-useast2a.tiktokv.com'], ie_key=TikTokIE)[0]
 
+    def _get_next_app_info(self):
+        if self._APP_INFO_POOL is None:
+            defaults = {
+                key: self._configuration_arg(key, [default], ie_key=TikTokIE)[0]
+                for key, default in self._APP_INFO_DEFAULTS.items()
+                if key != 'iid'
+            }
+            app_info_list = (
+                self._configuration_arg('app_info', ie_key=TikTokIE)
+                or random.sample(self._KNOWN_APP_INFO, len(self._KNOWN_APP_INFO)))
+            self._APP_INFO_POOL = [
+                {**defaults, **dict(
+                    (k, v) for k, v in zip(self._APP_INFO_DEFAULTS, app_info.split('/')) if v
+                )} for app_info in app_info_list
+            ]
+
+        if not self._APP_INFO_POOL:
+            return False
+
+        self._APP_INFO = self._APP_INFO_POOL.pop(0)
+
+        app_name = self._APP_INFO['app_name']
+        version = self._APP_INFO['manifest_app_version']
+        if app_name == 'musical_ly':
+            package = f'com.zhiliaoapp.musically/{version}'
+        else:  # trill, aweme
+            package = f'com.ss.android.ugc.{app_name}/{version}'
+        self._APP_USER_AGENT = f'{package} (Linux; U; Android 13; en_US; Pixel 7; Build/TD1A.220804.031; Cronet/58.0.2991.0)'
+
+        return True
+
     @staticmethod
     def _create_url(user_id, video_id):
         return f'https://www.tiktok.com/@{user_id or "_"}/video/{video_id}'
@@ -58,7 +105,7 @@ def _get_universal_data(self, webpage, display_id):
             'universal data', display_id, end_pattern=r'</script>', default={}),
             ('__DEFAULT_SCOPE__', {dict})) or {}
 
-    def _call_api_impl(self, ep, query, manifest_app_version, video_id, fatal=True,
+    def _call_api_impl(self, ep, query, video_id, fatal=True,
                        note='Downloading API JSON', errnote='Unable to download API page'):
         self._set_cookie(self._API_HOSTNAME, 'odin_tt', ''.join(random.choices('0123456789abcdef', k=160)))
         webpage_cookies = self._get_cookies(self._WEBPAGE_HOST)
@@ -67,80 +114,84 @@ def _call_api_impl(self, ep, query, manifest_app_version, video_id, fatal=True,
         return self._download_json(
             'https://%s/aweme/v1/%s/' % (self._API_HOSTNAME, ep), video_id=video_id,
             fatal=fatal, note=note, errnote=errnote, headers={
-                'User-Agent': f'com.ss.android.ugc.{self._APP_NAME}/{manifest_app_version} (Linux; U; Android 13; en_US; Pixel 7; Build/TD1A.220804.031; Cronet/58.0.2991.0)',
+                'User-Agent': self._APP_USER_AGENT,
                 'Accept': 'application/json',
             }, query=query)
 
-    def _build_api_query(self, query, app_version, manifest_app_version):
+    def _build_api_query(self, query):
         return {
             **query,
-            'version_name': app_version,
-            'version_code': manifest_app_version,
-            'build_number': app_version,
-            'manifest_version_code': manifest_app_version,
-            'update_version_code': manifest_app_version,
-            'openudid': ''.join(random.choices('0123456789abcdef', k=16)),
-            'uuid': ''.join(random.choices(string.digits, k=16)),
-            '_rticket': int(time.time() * 1000),
-            'ts': int(time.time()),
-            'device_brand': 'Google',
-            'device_type': 'Pixel 7',
             'device_platform': 'android',
+            'os': 'android',
+            'ssmix': 'a',
+            '_rticket': int(time.time() * 1000),
+            'cdid': str(uuid.uuid4()),
+            'channel': 'googleplay',
+            'aid': self._APP_INFO['aid'],
+            'app_name': self._APP_INFO['app_name'],
+            'version_code': ''.join((f'{int(v):02d}' for v in self._APP_INFO['app_version'].split('.'))),
+            'version_name': self._APP_INFO['app_version'],
+            'manifest_version_code': self._APP_INFO['manifest_app_version'],
+            'update_version_code': self._APP_INFO['manifest_app_version'],
+            'ab_version': self._APP_INFO['app_version'],
             'resolution': '1080*2400',
             'dpi': 420,
-            'os_version': '13',
+            'device_type': 'Pixel 7',
+            'device_brand': 'Google',
+            'language': 'en',
             'os_api': '29',
-            'carrier_region': 'US',
+            'os_version': '13',
+            'ac': 'wifi',
+            'is_pad': '0',
+            'current_region': 'US',
+            'app_type': 'normal',
             'sys_region': 'US',
-            'region': 'US',
-            'app_name': self._APP_NAME,
-            'app_language': 'en',
-            'language': 'en',
+            'last_install_time': int(time.time()) - random.randint(86400, 1123200),
             'timezone_name': 'America/New_York',
+            'residence': 'US',
+            'app_language': 'en',
             'timezone_offset': '-14400',
-            'channel': 'googleplay',
-            'ac': 'wifi',
-            'mcc_mnc': '310260',
-            'is_my_cn': 0,
-            'aid': self._AID,
-            'ssmix': 'a',
-            'as': 'a1qwert123',
-            'cp': 'cbfhckdckkde1',
+            'host_abi': 'armeabi-v7a',
+            'locale': 'en',
+            'ac2': 'wifi5g',
+            'uoo': '1',
+            'op_region': 'US',
+            'build_number': self._APP_INFO['app_version'],
+            'region': 'US',
+            'ts': int(time.time()),
+            'iid': self._APP_INFO['iid'],
+            'device_id': random.randint(7250000000000000000, 7351147085025500000),
+            'openudid': ''.join(random.choices('0123456789abcdef', k=16)),
         }
 
     def _call_api(self, ep, query, video_id, fatal=True,
                   note='Downloading API JSON', errnote='Unable to download API page'):
-        if not self._WORKING_APP_VERSION:
-            app_version = self._configuration_arg('app_version', [''], ie_key=TikTokIE.ie_key())[0]
-            manifest_app_version = self._configuration_arg('manifest_app_version', [''], ie_key=TikTokIE.ie_key())[0]
-            if app_version and manifest_app_version:
-                self._WORKING_APP_VERSION = (app_version, manifest_app_version)
-                self.write_debug('Imported app version combo from extractor arguments')
-            elif app_version or manifest_app_version:
-                self.report_warning('Only one of the two required version params are passed as extractor arguments', only_once=True)
-
-        if self._WORKING_APP_VERSION:
-            app_version, manifest_app_version = self._WORKING_APP_VERSION
-            real_query = self._build_api_query(query, app_version, manifest_app_version)
-            return self._call_api_impl(ep, real_query, manifest_app_version, video_id, fatal, note, errnote)
-
-        for count, (app_version, manifest_app_version) in enumerate(self._APP_VERSIONS, start=1):
-            real_query = self._build_api_query(query, app_version, manifest_app_version)
+        if not self._APP_INFO and not self._get_next_app_info():
+            message = 'No working app info is available'
+            if fatal:
+                raise ExtractorError(message, expected=True)
+            else:
+                self.report_warning(message)
+                return
+
+        max_tries = len(self._APP_INFO_POOL) + 1  # _APP_INFO_POOL + _APP_INFO
+        for count in itertools.count(1):
+            self.write_debug(str(self._APP_INFO))
+            real_query = self._build_api_query(query)
             try:
-                res = self._call_api_impl(ep, real_query, manifest_app_version, video_id, fatal, note, errnote)
-                self._WORKING_APP_VERSION = (app_version, manifest_app_version)
-                return res
+                return self._call_api_impl(ep, real_query, video_id, fatal, note, errnote)
             except ExtractorError as e:
                 if isinstance(e.cause, json.JSONDecodeError) and e.cause.pos == 0:
-                    if count == len(self._APP_VERSIONS):
+                    message = str(e.cause or e.msg)
+                    if not self._get_next_app_info():
                         if fatal:
-                            raise e
+                            raise
                         else:
-                            self.report_warning(str(e.cause or e.msg))
+                            self.report_warning(message)
                             return
-                    self.report_warning('%s. Retrying... (attempt %s of %s)' % (str(e.cause or e.msg), count, len(self._APP_VERSIONS)))
+                    self.report_warning(f'{message}. Retrying... (attempt {count} of {max_tries})')
                     continue
-                raise e
+                raise
 
     def _extract_aweme_app(self, aweme_id):
         feed_list = self._call_api(
@@ -223,6 +274,7 @@ def audio_meta(url):
 
         def extract_addr(addr, add_meta={}):
             parsed_meta, res = parse_url_key(addr.get('url_key', ''))
+            is_bytevc2 = parsed_meta.get('vcodec') == 'bytevc2'
             if res:
                 known_resolutions.setdefault(res, {}).setdefault('height', int_or_none(addr.get('height')))
                 known_resolutions[res].setdefault('width', int_or_none(addr.get('width')))
@@ -235,8 +287,11 @@ def extract_addr(addr, add_meta={}):
                 'acodec': 'aac',
                 'source_preference': -2 if 'aweme/v1' in url else -1,  # Downloads from API might get blocked
                 **add_meta, **parsed_meta,
+                # bytevc2 is bytedance's proprietary (unplayable) video codec
+                'preference': -100 if is_bytevc2 else -1,
                 'format_note': join_nonempty(
-                    add_meta.get('format_note'), '(API)' if 'aweme/v1' in url else None, delim=' '),
+                    add_meta.get('format_note'), '(API)' if 'aweme/v1' in url else None,
+                    '(UNPLAYABLE)' if is_bytevc2 else None, delim=' '),
                 **audio_meta(url),
             } for url in addr.get('url_list') or []]
 
diff --git a/yt_dlp/extractor/xvideos.py b/yt_dlp/extractor/xvideos.py
index 5df071503..59eef8490 100644
--- a/yt_dlp/extractor/xvideos.py
+++ b/yt_dlp/extractor/xvideos.py
@@ -15,35 +15,35 @@ class XVideosIE(InfoExtractor):
     _VALID_URL = r'''(?x)
                     https?://
                         (?:
-                            (?:[^/]+\.)?xvideos2?\.com/video|
-                            (?:www\.)?xvideos\.es/video|
+                            (?:[^/]+\.)?xvideos2?\.com/video\.?|
+                            (?:www\.)?xvideos\.es/video\.?|
                             (?:www|flashservice)\.xvideos\.com/embedframe/|
                             static-hw\.xvideos\.com/swf/xv-player\.swf\?.*?\bid_video=
                         )
-                        (?P<id>[0-9]+)
+                        (?P<id>[0-9a-z]+)
                     '''
     _TESTS = [{
-        'url': 'https://www.xvideos.com/video4588838/motorcycle_guy_cucks_influencer_steals_his_gf',
-        'md5': '14cea69fcb84db54293b1e971466c2e1',
+        'url': 'http://xvideos.com/video.ucuvbkfda4e/a_beautiful_red-haired_stranger_was_refused_but_still_came_to_my_room_for_sex',
+        'md5': '396255a900a6bddb3e98985f0b86c3fd',
         'info_dict': {
-            'id': '4588838',
+            'id': 'ucuvbkfda4e',
             'ext': 'mp4',
-            'title': 'Motorcycle Guy Cucks Influencer, Steals his GF',
-            'duration': 108,
+            'title': 'A Beautiful Red-Haired Stranger Was Refused, But Still Came To My Room For Sex',
+            'duration': 1238,
             'age_limit': 18,
-            'thumbnail': r're:^https://img-hw.xvideos-cdn.com/.+\.jpg',
+            'thumbnail': r're:^https://cdn\d+-pic.xvideos-cdn.com/.+\.jpg',
         }
     }, {
         # Broken HLS formats
         'url': 'https://www.xvideos.com/video65982001/what_s_her_name',
-        'md5': 'b82d7d7ef7d65a84b1fa6965f81f95a5',
+        'md5': '56742808292c8fa1418e4538c262c58b',
         'info_dict': {
             'id': '65982001',
             'ext': 'mp4',
             'title': 'what\'s her name?',
             'duration': 120,
             'age_limit': 18,
-            'thumbnail': r're:^https://img-hw.xvideos-cdn.com/.+\.jpg',
+            'thumbnail': r're:^https://cdn\d+-pic.xvideos-cdn.com/.+\.jpg',
         }
     }, {
         'url': 'https://flashservice.xvideos.com/embedframe/4588838',
@@ -90,6 +90,18 @@ class XVideosIE(InfoExtractor):
     }, {
         'url': 'https://de.xvideos.com/video4588838/biker_takes_his_girl',
         'only_matching': True
+    }, {
+        'url': 'https://flashservice.xvideos.com/embedframe/ucuvbkfda4e',
+        'only_matching': True,
+    }, {
+        'url': 'https://www.xvideos.com/embedframe/ucuvbkfda4e',
+        'only_matching': True,
+    }, {
+        'url': 'http://static-hw.xvideos.com/swf/xv-player.swf?id_video=ucuvbkfda4e',
+        'only_matching': True,
+    }, {
+        'url': 'https://xvideos.es/video.ucuvbkfda4e/a_beautiful_red-haired_stranger_was_refused_but_still_came_to_my_room_for_sex',
+        'only_matching': True
     }]
 
     def _real_extract(self, url):
diff --git a/yt_dlp/extractor/youtube.py b/yt_dlp/extractor/youtube.py
index 33fd3b490..589cd9b59 100644
--- a/yt_dlp/extractor/youtube.py
+++ b/yt_dlp/extractor/youtube.py
@@ -3834,7 +3834,7 @@ def build_fragments(f):
                             video_id=video_id, only_once=True)
                     throttled = True
 
-            tbr = float_or_none(fmt.get('averageBitrate') or fmt.get('bitrate'), 1000)
+            tbr = float_or_none(fmt.get('averageBitrate') or fmt.get('bitrate'), 1024)
             language_preference = (
                 10 if audio_track.get('audioIsDefault') and 10
                 else -10 if 'descriptive' in (audio_track.get('displayName') or '').lower() and -10
@@ -6965,7 +6965,7 @@ class YoutubeSearchIE(YoutubeTabBaseInfoExtractor, SearchInfoExtractor):
     IE_DESC = 'YouTube search'
     IE_NAME = 'youtube:search'
     _SEARCH_KEY = 'ytsearch'
-    _SEARCH_PARAMS = 'EgIQAQ%3D%3D'  # Videos only
+    _SEARCH_PARAMS = 'EgIQAfABAQ=='  # Videos only
     _TESTS = [{
         'url': 'ytsearch5:youtube-dl test video',
         'playlist_count': 5,
@@ -6973,6 +6973,14 @@ class YoutubeSearchIE(YoutubeTabBaseInfoExtractor, SearchInfoExtractor):
             'id': 'youtube-dl test video',
             'title': 'youtube-dl test video',
         }
+    }, {
+        'note': 'Suicide/self-harm search warning',
+        'url': 'ytsearch1:i hate myself and i wanna die',
+        'playlist_count': 1,
+        'info_dict': {
+            'id': 'i hate myself and i wanna die',
+            'title': 'i hate myself and i wanna die',
+        }
     }]
 
 
@@ -6980,7 +6988,7 @@ class YoutubeSearchDateIE(YoutubeTabBaseInfoExtractor, SearchInfoExtractor):
     IE_NAME = YoutubeSearchIE.IE_NAME + ':date'
     _SEARCH_KEY = 'ytsearchdate'
     IE_DESC = 'YouTube search, newest videos first'
-    _SEARCH_PARAMS = 'CAISAhAB'  # Videos only, sorted by date
+    _SEARCH_PARAMS = 'CAISAhAB8AEB'  # Videos only, sorted by date
     _TESTS = [{
         'url': 'ytsearchdate5:youtube-dl test video',
         'playlist_count': 5,
diff --git a/yt_dlp/networking/_websockets.py b/yt_dlp/networking/_websockets.py
index 43bdd7045..6e235b0c6 100644
--- a/yt_dlp/networking/_websockets.py
+++ b/yt_dlp/networking/_websockets.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import contextlib
 import io
 import logging
 import ssl
@@ -38,27 +39,40 @@
 import websockets.sync.client
 from websockets.uri import parse_uri
 
+# In websockets Connection, recv_exc and recv_events_exc are defined
+# after the recv events handler thread is started [1].
+# On our CI using PyPy, in some cases a race condition may occur
+# where the recv events handler thread tries to use these attributes before they are defined [2].
+# 1: https://github.com/python-websockets/websockets/blame/de768cf65e7e2b1a3b67854fb9e08816a5ff7050/src/websockets/sync/connection.py#L93
+# 2: "AttributeError: 'ClientConnection' object has no attribute 'recv_events_exc'. Did you mean: 'recv_events'?"
+import websockets.sync.connection  # isort: split
+with contextlib.suppress(Exception):
+    # > 12.0
+    websockets.sync.connection.Connection.recv_exc = None
+    # 12.0
+    websockets.sync.connection.Connection.recv_events_exc = None
+
 
 class WebsocketsResponseAdapter(WebSocketResponse):
 
-    def __init__(self, wsw: websockets.sync.client.ClientConnection, url):
+    def __init__(self, ws: websockets.sync.client.ClientConnection, url):
         super().__init__(
-            fp=io.BytesIO(wsw.response.body or b''),
+            fp=io.BytesIO(ws.response.body or b''),
             url=url,
-            headers=wsw.response.headers,
-            status=wsw.response.status_code,
-            reason=wsw.response.reason_phrase,
+            headers=ws.response.headers,
+            status=ws.response.status_code,
+            reason=ws.response.reason_phrase,
         )
-        self.wsw = wsw
+        self._ws = ws
 
     def close(self):
-        self.wsw.close()
+        self._ws.close()
         super().close()
 
     def send(self, message):
         # https://websockets.readthedocs.io/en/stable/reference/sync/client.html#websockets.sync.client.ClientConnection.send
         try:
-            return self.wsw.send(message)
+            return self._ws.send(message)
         except (websockets.exceptions.WebSocketException, RuntimeError, TimeoutError) as e:
             raise TransportError(cause=e) from e
         except SocksProxyError as e:
@@ -69,7 +83,7 @@ def send(self, message):
     def recv(self):
         # https://websockets.readthedocs.io/en/stable/reference/sync/client.html#websockets.sync.client.ClientConnection.recv
         try:
-            return self.wsw.recv()
+            return self._ws.recv()
         except SocksProxyError as e:
             raise ProxyError(cause=e) from e
         except (websockets.exceptions.WebSocketException, RuntimeError, TimeoutError) as e:
diff --git a/yt_dlp/networking/common.py b/yt_dlp/networking/common.py
index b8c6a62c0..4c66ba66a 100644
--- a/yt_dlp/networking/common.py
+++ b/yt_dlp/networking/common.py
@@ -463,9 +463,10 @@ def headers(self, new_headers: Mapping):
         else:
             raise TypeError('headers must be a mapping')
 
-    def update(self, url=None, data=None, headers=None, query=None):
+    def update(self, url=None, data=None, headers=None, query=None, extensions=None):
         self.data = data if data is not None else self.data
         self.headers.update(headers or {})
+        self.extensions.update(extensions or {})
         self.url = update_url_query(url or self.url, query or {})
 
     def copy(self):
diff --git a/yt_dlp/utils/traversal.py b/yt_dlp/utils/traversal.py
index 8938f4c78..926a3d0a1 100644
--- a/yt_dlp/utils/traversal.py
+++ b/yt_dlp/utils/traversal.py
@@ -228,6 +228,15 @@ def apply_path(start_obj, path, test_type):
             if not casesense and isinstance(key, str):
                 key = key.casefold()
 
+            if key in (any, all):
+                has_branched = False
+                filtered_objs = (obj for obj in objs if obj not in (None, {}))
+                if key is any:
+                    objs = (next(filtered_objs, None),)
+                else:
+                    objs = (list(filtered_objs),)
+                continue
+
             if __debug__ and callable(key):
                 # Verify function signature
                 inspect.signature(key).bind(None, None)


Test patch:
diff --git a/test/test_traversal.py b/test/test_traversal.py
new file mode 100644
index 000000000..0b2f3fb5d
--- /dev/null
+++ b/test/test_traversal.py
@@ -0,0 +1,411 @@
+import re
+import xml.etree.ElementTree
+
+import pytest
+
+from yt_dlp.utils import dict_get, int_or_none, str_or_none
+from yt_dlp.utils.traversal import traverse_obj
+
+_TEST_DATA = {
+    100: 100,
+    1.2: 1.2,
+    'str': 'str',
+    'None': None,
+    '...': ...,
+    'urls': [
+        {'index': 0, 'url': 'https://www.example.com/0'},
+        {'index': 1, 'url': 'https://www.example.com/1'},
+    ],
+    'data': (
+        {'index': 2},
+        {'index': 3},
+    ),
+    'dict': {},
+}
+
+
+class TestTraversal:
+    def test_dict_get(self):
+        FALSE_VALUES = {
+            'none': None,
+            'false': False,
+            'zero': 0,
+            'empty_string': '',
+            'empty_list': [],
+        }
+        d = {**FALSE_VALUES, 'a': 42}
+        assert dict_get(d, 'a') == 42
+        assert dict_get(d, 'b') is None
+        assert dict_get(d, 'b', 42) == 42
+        assert dict_get(d, ('a',)) == 42
+        assert dict_get(d, ('b', 'a')) == 42
+        assert dict_get(d, ('b', 'c', 'a', 'd')) == 42
+        assert dict_get(d, ('b', 'c')) is None
+        assert dict_get(d, ('b', 'c'), 42) == 42
+        for key, false_value in FALSE_VALUES.items():
+            assert dict_get(d, ('b', 'c', key)) is None
+            assert dict_get(d, ('b', 'c', key), skip_false_values=False) == false_value
+
+    def test_traversal_base(self):
+        assert traverse_obj(_TEST_DATA, ('str',)) == 'str', \
+            'allow tuple path'
+        assert traverse_obj(_TEST_DATA, ['str']) == 'str', \
+            'allow list path'
+        assert traverse_obj(_TEST_DATA, (value for value in ("str",))) == 'str', \
+            'allow iterable path'
+        assert traverse_obj(_TEST_DATA, 'str') == 'str', \
+            'single items should be treated as a path'
+        assert traverse_obj(_TEST_DATA, 100) == 100, \
+            'allow int path'
+        assert traverse_obj(_TEST_DATA, 1.2) == 1.2, \
+            'allow float path'
+        assert traverse_obj(_TEST_DATA, None) == _TEST_DATA, \
+            '`None` should not perform any modification'
+
+    def test_traversal_ellipsis(self):
+        assert traverse_obj(_TEST_DATA, ...) == [x for x in _TEST_DATA.values() if x not in (None, {})], \
+            '`...` should give all non discarded values'
+        assert traverse_obj(_TEST_DATA, ('urls', 0, ...)) == list(_TEST_DATA['urls'][0].values()), \
+            '`...` selection for dicts should select all values'
+        assert traverse_obj(_TEST_DATA, (..., ..., 'url')) == ['https://www.example.com/0', 'https://www.example.com/1'], \
+            'nested `...` queries should work'
+        assert traverse_obj(_TEST_DATA, (..., ..., 'index')) == list(range(4)), \
+            '`...` query result should be flattened'
+        assert traverse_obj(iter(range(4)), ...) == list(range(4)), \
+            '`...` should accept iterables'
+
+    def test_traversal_function(self):
+        filter_func = lambda x, y: x == 'urls' and isinstance(y, list)
+        assert traverse_obj(_TEST_DATA, filter_func) == [_TEST_DATA['urls']], \
+            'function as query key should perform a filter based on (key, value)'
+        assert traverse_obj(_TEST_DATA, lambda _, x: isinstance(x[0], str)) == ['str'], \
+            'exceptions in the query function should be catched'
+        assert traverse_obj(iter(range(4)), lambda _, x: x % 2 == 0) == [0, 2], \
+            'function key should accept iterables'
+        # Wrong function signature should raise (debug mode)
+        with pytest.raises(Exception):
+            traverse_obj(_TEST_DATA, lambda a: ...)
+        with pytest.raises(Exception):
+            traverse_obj(_TEST_DATA, lambda a, b, c: ...)
+
+    def test_traversal_set(self):
+        # transformation/type, like `expected_type`
+        assert traverse_obj(_TEST_DATA, (..., {str.upper}, )) == ['STR'], \
+            'Function in set should be a transformation'
+        assert traverse_obj(_TEST_DATA, (..., {str})) == ['str'], \
+            'Type in set should be a type filter'
+        assert traverse_obj(_TEST_DATA, {dict}) == _TEST_DATA, \
+            'A single set should be wrapped into a path'
+        assert traverse_obj(_TEST_DATA, (..., {str.upper})) == ['STR'], \
+            'Transformation function should not raise'
+        expected = [x for x in map(str_or_none, _TEST_DATA.values()) if x is not None]
+        assert traverse_obj(_TEST_DATA, (..., {str_or_none})) == expected, \
+            'Function in set should be a transformation'
+        assert traverse_obj(_TEST_DATA, ('fail', {lambda _: 'const'})) == 'const', \
+            'Function in set should always be called'
+        # Sets with length != 1 should raise in debug
+        with pytest.raises(Exception):
+            traverse_obj(_TEST_DATA, set())
+        with pytest.raises(Exception):
+            traverse_obj(_TEST_DATA, {str.upper, str})
+
+    def test_traversal_slice(self):
+        _SLICE_DATA = [0, 1, 2, 3, 4]
+
+        assert traverse_obj(_TEST_DATA, ('dict', slice(1))) is None, \
+            'slice on a dictionary should not throw'
+        assert traverse_obj(_SLICE_DATA, slice(1)) == _SLICE_DATA[:1], \
+            'slice key should apply slice to sequence'
+        assert traverse_obj(_SLICE_DATA, slice(1, 2)) == _SLICE_DATA[1:2], \
+            'slice key should apply slice to sequence'
+        assert traverse_obj(_SLICE_DATA, slice(1, 4, 2)) == _SLICE_DATA[1:4:2], \
+            'slice key should apply slice to sequence'
+
+    def test_traversal_alternatives(self):
+        assert traverse_obj(_TEST_DATA, 'fail', 'str') == 'str', \
+            'multiple `paths` should be treated as alternative paths'
+        assert traverse_obj(_TEST_DATA, 'str', 100) == 'str', \
+            'alternatives should exit early'
+        assert traverse_obj(_TEST_DATA, 'fail', 'fail') is None, \
+            'alternatives should return `default` if exhausted'
+        assert traverse_obj(_TEST_DATA, (..., 'fail'), 100) == 100, \
+            'alternatives should track their own branching return'
+        assert traverse_obj(_TEST_DATA, ('dict', ...), ('data', ...)) == list(_TEST_DATA['data']), \
+            'alternatives on empty objects should search further'
+
+    def test_traversal_branching_nesting(self):
+        assert traverse_obj(_TEST_DATA, ('urls', (3, 0), 'url')) == ['https://www.example.com/0'], \
+            'tuple as key should be treated as branches'
+        assert traverse_obj(_TEST_DATA, ('urls', [3, 0], 'url')) == ['https://www.example.com/0'], \
+            'list as key should be treated as branches'
+        assert traverse_obj(_TEST_DATA, ('urls', ((1, 'fail'), (0, 'url')))) == ['https://www.example.com/0'], \
+            'double nesting in path should be treated as paths'
+        assert traverse_obj(['0', [1, 2]], [(0, 1), 0]) == [1], \
+            'do not fail early on branching'
+        expected = ['https://www.example.com/0', 'https://www.example.com/1']
+        assert traverse_obj(_TEST_DATA, ('urls', ((0, ('fail', 'url')), (1, 'url')))) == expected, \
+            'tripple nesting in path should be treated as branches'
+        assert traverse_obj(_TEST_DATA, ('urls', ('fail', (..., 'url')))) == expected, \
+            'ellipsis as branch path start gets flattened'
+
+    def test_traversal_dict(self):
+        assert traverse_obj(_TEST_DATA, {0: 100, 1: 1.2}) == {0: 100, 1: 1.2}, \
+            'dict key should result in a dict with the same keys'
+        expected = {0: 'https://www.example.com/0'}
+        assert traverse_obj(_TEST_DATA, {0: ('urls', 0, 'url')}) == expected, \
+            'dict key should allow paths'
+        expected = {0: ['https://www.example.com/0']}
+        assert traverse_obj(_TEST_DATA, {0: ('urls', (3, 0), 'url')}) == expected, \
+            'tuple in dict path should be treated as branches'
+        assert traverse_obj(_TEST_DATA, {0: ('urls', ((1, 'fail'), (0, 'url')))}) == expected, \
+            'double nesting in dict path should be treated as paths'
+        expected = {0: ['https://www.example.com/1', 'https://www.example.com/0']}
+        assert traverse_obj(_TEST_DATA, {0: ('urls', ((1, ('fail', 'url')), (0, 'url')))}) == expected, \
+            'tripple nesting in dict path should be treated as branches'
+        assert traverse_obj(_TEST_DATA, {0: 'fail'}) == {}, \
+            'remove `None` values when top level dict key fails'
+        assert traverse_obj(_TEST_DATA, {0: 'fail'}, default=...) == {0: ...}, \
+            'use `default` if key fails and `default`'
+        assert traverse_obj(_TEST_DATA, {0: 'dict'}) == {}, \
+            'remove empty values when dict key'
+        assert traverse_obj(_TEST_DATA, {0: 'dict'}, default=...) == {0: ...}, \
+            'use `default` when dict key and `default`'
+        assert traverse_obj(_TEST_DATA, {0: {0: 'fail'}}) == {}, \
+            'remove empty values when nested dict key fails'
+        assert traverse_obj(None, {0: 'fail'}) == {}, \
+            'default to dict if pruned'
+        assert traverse_obj(None, {0: 'fail'}, default=...) == {0: ...}, \
+            'default to dict if pruned and default is given'
+        assert traverse_obj(_TEST_DATA, {0: {0: 'fail'}}, default=...) == {0: {0: ...}}, \
+            'use nested `default` when nested dict key fails and `default`'
+        assert traverse_obj(_TEST_DATA, {0: ('dict', ...)}) == {}, \
+            'remove key if branch in dict key not successful'
+
+    def test_traversal_default(self):
+        _DEFAULT_DATA = {'None': None, 'int': 0, 'list': []}
+
+        assert traverse_obj(_DEFAULT_DATA, 'fail') is None, \
+            'default value should be `None`'
+        assert traverse_obj(_DEFAULT_DATA, 'fail', 'fail', default=...) == ..., \
+            'chained fails should result in default'
+        assert traverse_obj(_DEFAULT_DATA, 'None', 'int') == 0, \
+            'should not short cirquit on `None`'
+        assert traverse_obj(_DEFAULT_DATA, 'fail', default=1) == 1, \
+            'invalid dict key should result in `default`'
+        assert traverse_obj(_DEFAULT_DATA, 'None', default=1) == 1, \
+            '`None` is a deliberate sentinel and should become `default`'
+        assert traverse_obj(_DEFAULT_DATA, ('list', 10)) is None, \
+            '`IndexError` should result in `default`'
+        assert traverse_obj(_DEFAULT_DATA, (..., 'fail'), default=1) == 1, \
+            'if branched but not successful return `default` if defined, not `[]`'
+        assert traverse_obj(_DEFAULT_DATA, (..., 'fail'), default=None) is None, \
+            'if branched but not successful return `default` even if `default` is `None`'
+        assert traverse_obj(_DEFAULT_DATA, (..., 'fail')) == [], \
+            'if branched but not successful return `[]`, not `default`'
+        assert traverse_obj(_DEFAULT_DATA, ('list', ...)) == [], \
+            'if branched but object is empty return `[]`, not `default`'
+        assert traverse_obj(None, ...) == [], \
+            'if branched but object is `None` return `[]`, not `default`'
+        assert traverse_obj({0: None}, (0, ...)) == [], \
+            'if branched but state is `None` return `[]`, not `default`'
+
+    @pytest.mark.parametrize('path', [
+        ('fail', ...),
+        (..., 'fail'),
+        100 * ('fail',) + (...,),
+        (...,) + 100 * ('fail',),
+    ])
+    def test_traversal_branching(self, path):
+        assert traverse_obj({}, path) == [], \
+            'if branched but state is `None`, return `[]` (not `default`)'
+        assert traverse_obj({}, 'fail', path) == [], \
+            'if branching in last alternative and previous did not match, return `[]` (not `default`)'
+        assert traverse_obj({0: 'x'}, 0, path) == 'x', \
+            'if branching in last alternative and previous did match, return single value'
+        assert traverse_obj({0: 'x'}, path, 0) == 'x', \
+            'if branching in first alternative and non-branching path does match, return single value'
+        assert traverse_obj({}, path, 'fail') is None, \
+            'if branching in first alternative and non-branching path does not match, return `default`'
+
+    def test_traversal_expected_type(self):
+        _EXPECTED_TYPE_DATA = {'str': 'str', 'int': 0}
+
+        assert traverse_obj(_EXPECTED_TYPE_DATA, 'str', expected_type=str) == 'str', \
+            'accept matching `expected_type` type'
+        assert traverse_obj(_EXPECTED_TYPE_DATA, 'str', expected_type=int) is None, \
+            'reject non matching `expected_type` type'
+        assert traverse_obj(_EXPECTED_TYPE_DATA, 'int', expected_type=lambda x: str(x)) == '0', \
+            'transform type using type function'
+        assert traverse_obj(_EXPECTED_TYPE_DATA, 'str', expected_type=lambda _: 1 / 0) is None, \
+            'wrap expected_type fuction in try_call'
+        assert traverse_obj(_EXPECTED_TYPE_DATA, ..., expected_type=str) == ['str'], \
+            'eliminate items that expected_type fails on'
+        assert traverse_obj(_TEST_DATA, {0: 100, 1: 1.2}, expected_type=int) == {0: 100}, \
+            'type as expected_type should filter dict values'
+        assert traverse_obj(_TEST_DATA, {0: 100, 1: 1.2, 2: 'None'}, expected_type=str_or_none) == {0: '100', 1: '1.2'}, \
+            'function as expected_type should transform dict values'
+        assert traverse_obj(_TEST_DATA, ({0: 1.2}, 0, {int_or_none}), expected_type=int) == 1, \
+            'expected_type should not filter non final dict values'
+        assert traverse_obj(_TEST_DATA, {0: {0: 100, 1: 'str'}}, expected_type=int) == {0: {0: 100}}, \
+            'expected_type should transform deep dict values'
+        assert traverse_obj(_TEST_DATA, [({0: '...'}, {0: '...'})], expected_type=type(...)) == [{0: ...}, {0: ...}], \
+            'expected_type should transform branched dict values'
+        assert traverse_obj({1: {3: 4}}, [(1, 2), 3], expected_type=int) == [4], \
+            'expected_type regression for type matching in tuple branching'
+        assert traverse_obj(_TEST_DATA, ['data', ...], expected_type=int) == [], \
+            'expected_type regression for type matching in dict result'
+
+    def test_traversal_get_all(self):
+        _GET_ALL_DATA = {'key': [0, 1, 2]}
+
+        assert traverse_obj(_GET_ALL_DATA, ('key', ...), get_all=False) == 0, \
+            'if not `get_all`, return only first matching value'
+        assert traverse_obj(_GET_ALL_DATA, ..., get_all=False) == [0, 1, 2], \
+            'do not overflatten if not `get_all`'
+
+    def test_traversal_casesense(self):
+        _CASESENSE_DATA = {
+            'KeY': 'value0',
+            0: {
+                'KeY': 'value1',
+                0: {'KeY': 'value2'},
+            },
+        }
+
+        assert traverse_obj(_CASESENSE_DATA, 'key') is None, \
+            'dict keys should be case sensitive unless `casesense`'
+        assert traverse_obj(_CASESENSE_DATA, 'keY', casesense=False) == 'value0', \
+            'allow non matching key case if `casesense`'
+        assert traverse_obj(_CASESENSE_DATA, [0, ('keY',)], casesense=False) == ['value1'], \
+            'allow non matching key case in branch if `casesense`'
+        assert traverse_obj(_CASESENSE_DATA, [0, ([0, 'keY'],)], casesense=False) == ['value2'], \
+            'allow non matching key case in branch path if `casesense`'
+
+    def test_traversal_traverse_string(self):
+        _TRAVERSE_STRING_DATA = {'str': 'str', 1.2: 1.2}
+
+        assert traverse_obj(_TRAVERSE_STRING_DATA, ('str', 0)) is None, \
+            'do not traverse into string if not `traverse_string`'
+        assert traverse_obj(_TRAVERSE_STRING_DATA, ('str', 0), traverse_string=True) == 's', \
+            'traverse into string if `traverse_string`'
+        assert traverse_obj(_TRAVERSE_STRING_DATA, (1.2, 1), traverse_string=True) == '.', \
+            'traverse into converted data if `traverse_string`'
+        assert traverse_obj(_TRAVERSE_STRING_DATA, ('str', ...), traverse_string=True) == 'str', \
+            '`...` should result in string (same value) if `traverse_string`'
+        assert traverse_obj(_TRAVERSE_STRING_DATA, ('str', slice(0, None, 2)), traverse_string=True) == 'sr', \
+            '`slice` should result in string if `traverse_string`'
+        assert traverse_obj(_TRAVERSE_STRING_DATA, ('str', lambda i, v: i or v == "s"), traverse_string=True) == 'str', \
+            'function should result in string if `traverse_string`'
+        assert traverse_obj(_TRAVERSE_STRING_DATA, ('str', (0, 2)), traverse_string=True) == ['s', 'r'], \
+            'branching should result in list if `traverse_string`'
+        assert traverse_obj({}, (0, ...), traverse_string=True) == [], \
+            'branching should result in list if `traverse_string`'
+        assert traverse_obj({}, (0, lambda x, y: True), traverse_string=True) == [], \
+            'branching should result in list if `traverse_string`'
+        assert traverse_obj({}, (0, slice(1)), traverse_string=True) == [], \
+            'branching should result in list if `traverse_string`'
+
+    def test_traversal_re(self):
+        mobj = re.fullmatch(r'0(12)(?P<group>3)(4)?', '0123')
+        assert traverse_obj(mobj, ...) == [x for x in mobj.groups() if x is not None], \
+            '`...` on a `re.Match` should give its `groups()`'
+        assert traverse_obj(mobj, lambda k, _: k in (0, 2)) == ['0123', '3'], \
+            'function on a `re.Match` should give groupno, value starting at 0'
+        assert traverse_obj(mobj, 'group') == '3', \
+            'str key on a `re.Match` should give group with that name'
+        assert traverse_obj(mobj, 2) == '3', \
+            'int key on a `re.Match` should give group with that name'
+        assert traverse_obj(mobj, 'gRoUp', casesense=False) == '3', \
+            'str key on a `re.Match` should respect casesense'
+        assert traverse_obj(mobj, 'fail') is None, \
+            'failing str key on a `re.Match` should return `default`'
+        assert traverse_obj(mobj, 'gRoUpS', casesense=False) is None, \
+            'failing str key on a `re.Match` should return `default`'
+        assert traverse_obj(mobj, 8) is None, \
+            'failing int key on a `re.Match` should return `default`'
+        assert traverse_obj(mobj, lambda k, _: k in (0, 'group')) == ['0123', '3'], \
+            'function on a `re.Match` should give group name as well'
+
+    def test_traversal_xml_etree(self):
+        etree = xml.etree.ElementTree.fromstring('''<?xml version="1.0"?>
+        <data>
+            <country name="Liechtenstein">
+                <rank>1</rank>
+                <year>2008</year>
+                <gdppc>141100</gdppc>
+                <neighbor name="Austria" direction="E"/>
+                <neighbor name="Switzerland" direction="W"/>
+            </country>
+            <country name="Singapore">
+                <rank>4</rank>
+                <year>2011</year>
+                <gdppc>59900</gdppc>
+                <neighbor name="Malaysia" direction="N"/>
+            </country>
+            <country name="Panama">
+                <rank>68</rank>
+                <year>2011</year>
+                <gdppc>13600</gdppc>
+                <neighbor name="Costa Rica" direction="W"/>
+                <neighbor name="Colombia" direction="E"/>
+            </country>
+        </data>''')
+        assert traverse_obj(etree, '') == etree, \
+            'empty str key should return the element itself'
+        assert traverse_obj(etree, 'country') == list(etree), \
+            'str key should lead all children with that tag name'
+        assert traverse_obj(etree, ...) == list(etree), \
+            '`...` as key should return all children'
+        assert traverse_obj(etree, lambda _, x: x[0].text == '4') == [etree[1]], \
+            'function as key should get element as value'
+        assert traverse_obj(etree, lambda i, _: i == 1) == [etree[1]], \
+            'function as key should get index as key'
+        assert traverse_obj(etree, 0) == etree[0], \
+            'int key should return the nth child'
+        expected = ['Austria', 'Switzerland', 'Malaysia', 'Costa Rica', 'Colombia']
+        assert traverse_obj(etree, './/neighbor/@name') == expected, \
+            '`@<attribute>` at end of path should give that attribute'
+        assert traverse_obj(etree, '//neighbor/@fail') == [None, None, None, None, None], \
+            '`@<nonexistant>` at end of path should give `None`'
+        assert traverse_obj(etree, ('//neighbor/@', 2)) == {'name': 'Malaysia', 'direction': 'N'}, \
+            '`@` should give the full attribute dict'
+        assert traverse_obj(etree, '//year/text()') == ['2008', '2011', '2011'], \
+            '`text()` at end of path should give the inner text'
+        assert traverse_obj(etree, '//*[@direction]/@direction') == ['E', 'W', 'N', 'W', 'E'], \
+            'full Python xpath features should be supported'
+        assert traverse_obj(etree, (0, '@name')) == 'Liechtenstein', \
+            'special transformations should act on current element'
+        assert traverse_obj(etree, ('country', 0, ..., 'text()', {int_or_none})) == [1, 2008, 141100], \
+            'special transformations should act on current element'
+
+    def test_traversal_unbranching(self):
+        assert traverse_obj(_TEST_DATA, [(100, 1.2), all]) == [100, 1.2], \
+            '`all` should give all results as list'
+        assert traverse_obj(_TEST_DATA, [(100, 1.2), any]) == 100, \
+            '`any` should give the first result'
+        assert traverse_obj(_TEST_DATA, [100, all]) == [100], \
+            '`all` should give list if non branching'
+        assert traverse_obj(_TEST_DATA, [100, any]) == 100, \
+            '`any` should give single item if non branching'
+        assert traverse_obj(_TEST_DATA, [('dict', 'None', 100), all]) == [100], \
+            '`all` should filter `None` and empty dict'
+        assert traverse_obj(_TEST_DATA, [('dict', 'None', 100), any]) == 100, \
+            '`any` should filter `None` and empty dict'
+        assert traverse_obj(_TEST_DATA, [{
+            'all': [('dict', 'None', 100, 1.2), all],
+            'any': [('dict', 'None', 100, 1.2), any],
+        }]) == {'all': [100, 1.2], 'any': 100}, \
+            '`all`/`any` should apply to each dict path separately'
+        assert traverse_obj(_TEST_DATA, [{
+            'all': [('dict', 'None', 100, 1.2), all],
+            'any': [('dict', 'None', 100, 1.2), any],
+        }], get_all=False) == {'all': [100, 1.2], 'any': 100}, \
+            '`all`/`any` should apply to dict regardless of `get_all`'
+        assert traverse_obj(_TEST_DATA, [('dict', 'None', 100, 1.2), all, {float}]) is None, \
+            '`all` should reset branching status'
+        assert traverse_obj(_TEST_DATA, [('dict', 'None', 100, 1.2), any, {float}]) is None, \
+            '`any` should reset branching status'
+        assert traverse_obj(_TEST_DATA, [('dict', 'None', 100, 1.2), all, ..., {float}]) == [1.2], \
+            '`all` should allow further branching'
+        assert traverse_obj(_TEST_DATA, [('dict', 'None', 'urls', 'data'), any, ..., 'index']) == [0, 1], \
+            '`any` should allow further branching'
diff --git a/test/test_utils.py b/test/test_utils.py
index a3073f0e0..71febeefd 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -2,7 +2,6 @@
 
 # Allow direct execution
 import os
-import re
 import sys
 import unittest
 import warnings
@@ -45,7 +44,6 @@
     determine_ext,
     determine_file_encoding,
     dfxp2srt,
-    dict_get,
     encode_base_n,
     encode_compat_str,
     encodeFilename,
@@ -106,13 +104,11 @@
     sanitize_url,
     shell_quote,
     smuggle_url,
-    str_or_none,
     str_to_int,
     strip_jsonp,
     strip_or_none,
     subtitles_filename,
     timeconvert,
-    traverse_obj,
     try_call,
     unescapeHTML,
     unified_strdate,
@@ -755,28 +751,6 @@ def test_multipart_encode(self):
         self.assertRaises(
             ValueError, multipart_encode, {b'field': b'value'}, boundary='value')
 
-    def test_dict_get(self):
-        FALSE_VALUES = {
-            'none': None,
-            'false': False,
-            'zero': 0,
-            'empty_string': '',
-            'empty_list': [],
-        }
-        d = FALSE_VALUES.copy()
-        d['a'] = 42
-        self.assertEqual(dict_get(d, 'a'), 42)
-        self.assertEqual(dict_get(d, 'b'), None)
-        self.assertEqual(dict_get(d, 'b', 42), 42)
-        self.assertEqual(dict_get(d, ('a', )), 42)
-        self.assertEqual(dict_get(d, ('b', 'a', )), 42)
-        self.assertEqual(dict_get(d, ('b', 'c', 'a', 'd', )), 42)
-        self.assertEqual(dict_get(d, ('b', 'c', )), None)
-        self.assertEqual(dict_get(d, ('b', 'c', ), 42), 42)
-        for key, false_value in FALSE_VALUES.items():
-            self.assertEqual(dict_get(d, ('b', 'c', key, )), None)
-            self.assertEqual(dict_get(d, ('b', 'c', key, ), skip_false_values=False), false_value)
-
     def test_merge_dicts(self):
         self.assertEqual(merge_dicts({'a': 1}, {'b': 2}), {'a': 1, 'b': 2})
         self.assertEqual(merge_dicts({'a': 1}, {'a': 2}), {'a': 1})
@@ -2039,359 +2013,6 @@ def test_variadic(self):
             warnings.simplefilter('ignore')
             self.assertEqual(variadic('spam', allowed_types=[dict]), 'spam')
 
-    def test_traverse_obj(self):
-        _TEST_DATA = {
-            100: 100,
-            1.2: 1.2,
-            'str': 'str',
-            'None': None,
-            '...': ...,
-            'urls': [
-                {'index': 0, 'url': 'https://www.example.com/0'},
-                {'index': 1, 'url': 'https://www.example.com/1'},
-            ],
-            'data': (
-                {'index': 2},
-                {'index': 3},
-            ),
-            'dict': {},
-        }
-
-        # Test base functionality
-        self.assertEqual(traverse_obj(_TEST_DATA, ('str',)), 'str',
-                         msg='allow tuple path')
-        self.assertEqual(traverse_obj(_TEST_DATA, ['str']), 'str',
-                         msg='allow list path')
-        self.assertEqual(traverse_obj(_TEST_DATA, (value for value in ("str",))), 'str',
-                         msg='allow iterable path')
-        self.assertEqual(traverse_obj(_TEST_DATA, 'str'), 'str',
-                         msg='single items should be treated as a path')
-        self.assertEqual(traverse_obj(_TEST_DATA, None), _TEST_DATA)
-        self.assertEqual(traverse_obj(_TEST_DATA, 100), 100)
-        self.assertEqual(traverse_obj(_TEST_DATA, 1.2), 1.2)
-
-        # Test Ellipsis behavior
-        self.assertCountEqual(traverse_obj(_TEST_DATA, ...),
-                              (item for item in _TEST_DATA.values() if item not in (None, {})),
-                              msg='`...` should give all non discarded values')
-        self.assertCountEqual(traverse_obj(_TEST_DATA, ('urls', 0, ...)), _TEST_DATA['urls'][0].values(),
-                              msg='`...` selection for dicts should select all values')
-        self.assertEqual(traverse_obj(_TEST_DATA, (..., ..., 'url')),
-                         ['https://www.example.com/0', 'https://www.example.com/1'],
-                         msg='nested `...` queries should work')
-        self.assertCountEqual(traverse_obj(_TEST_DATA, (..., ..., 'index')), range(4),
-                              msg='`...` query result should be flattened')
-        self.assertEqual(traverse_obj(iter(range(4)), ...), list(range(4)),
-                         msg='`...` should accept iterables')
-
-        # Test function as key
-        self.assertEqual(traverse_obj(_TEST_DATA, lambda x, y: x == 'urls' and isinstance(y, list)),
-                         [_TEST_DATA['urls']],
-                         msg='function as query key should perform a filter based on (key, value)')
-        self.assertCountEqual(traverse_obj(_TEST_DATA, lambda _, x: isinstance(x[0], str)), {'str'},
-                              msg='exceptions in the query function should be catched')
-        self.assertEqual(traverse_obj(iter(range(4)), lambda _, x: x % 2 == 0), [0, 2],
-                         msg='function key should accept iterables')
-        if __debug__:
-            with self.assertRaises(Exception, msg='Wrong function signature should raise in debug'):
-                traverse_obj(_TEST_DATA, lambda a: ...)
-            with self.assertRaises(Exception, msg='Wrong function signature should raise in debug'):
-                traverse_obj(_TEST_DATA, lambda a, b, c: ...)
-
-        # Test set as key (transformation/type, like `expected_type`)
-        self.assertEqual(traverse_obj(_TEST_DATA, (..., {str.upper}, )), ['STR'],
-                         msg='Function in set should be a transformation')
-        self.assertEqual(traverse_obj(_TEST_DATA, (..., {str})), ['str'],
-                         msg='Type in set should be a type filter')
-        self.assertEqual(traverse_obj(_TEST_DATA, {dict}), _TEST_DATA,
-                         msg='A single set should be wrapped into a path')
-        self.assertEqual(traverse_obj(_TEST_DATA, (..., {str.upper})), ['STR'],
-                         msg='Transformation function should not raise')
-        self.assertEqual(traverse_obj(_TEST_DATA, (..., {str_or_none})),
-                         [item for item in map(str_or_none, _TEST_DATA.values()) if item is not None],
-                         msg='Function in set should be a transformation')
-        self.assertEqual(traverse_obj(_TEST_DATA, ('fail', {lambda _: 'const'})), 'const',
-                         msg='Function in set should always be called')
-        if __debug__:
-            with self.assertRaises(Exception, msg='Sets with length != 1 should raise in debug'):
-                traverse_obj(_TEST_DATA, set())
-            with self.assertRaises(Exception, msg='Sets with length != 1 should raise in debug'):
-                traverse_obj(_TEST_DATA, {str.upper, str})
-
-        # Test `slice` as a key
-        _SLICE_DATA = [0, 1, 2, 3, 4]
-        self.assertEqual(traverse_obj(_TEST_DATA, ('dict', slice(1))), None,
-                         msg='slice on a dictionary should not throw')
-        self.assertEqual(traverse_obj(_SLICE_DATA, slice(1)), _SLICE_DATA[:1],
-                         msg='slice key should apply slice to sequence')
-        self.assertEqual(traverse_obj(_SLICE_DATA, slice(1, 2)), _SLICE_DATA[1:2],
-                         msg='slice key should apply slice to sequence')
-        self.assertEqual(traverse_obj(_SLICE_DATA, slice(1, 4, 2)), _SLICE_DATA[1:4:2],
-                         msg='slice key should apply slice to sequence')
-
-        # Test alternative paths
-        self.assertEqual(traverse_obj(_TEST_DATA, 'fail', 'str'), 'str',
-                         msg='multiple `paths` should be treated as alternative paths')
-        self.assertEqual(traverse_obj(_TEST_DATA, 'str', 100), 'str',
-                         msg='alternatives should exit early')
-        self.assertEqual(traverse_obj(_TEST_DATA, 'fail', 'fail'), None,
-                         msg='alternatives should return `default` if exhausted')
-        self.assertEqual(traverse_obj(_TEST_DATA, (..., 'fail'), 100), 100,
-                         msg='alternatives should track their own branching return')
-        self.assertEqual(traverse_obj(_TEST_DATA, ('dict', ...), ('data', ...)), list(_TEST_DATA['data']),
-                         msg='alternatives on empty objects should search further')
-
-        # Test branch and path nesting
-        self.assertEqual(traverse_obj(_TEST_DATA, ('urls', (3, 0), 'url')), ['https://www.example.com/0'],
-                         msg='tuple as key should be treated as branches')
-        self.assertEqual(traverse_obj(_TEST_DATA, ('urls', [3, 0], 'url')), ['https://www.example.com/0'],
-                         msg='list as key should be treated as branches')
-        self.assertEqual(traverse_obj(_TEST_DATA, ('urls', ((1, 'fail'), (0, 'url')))), ['https://www.example.com/0'],
-                         msg='double nesting in path should be treated as paths')
-        self.assertEqual(traverse_obj(['0', [1, 2]], [(0, 1), 0]), [1],
-                         msg='do not fail early on branching')
-        self.assertCountEqual(traverse_obj(_TEST_DATA, ('urls', ((1, ('fail', 'url')), (0, 'url')))),
-                              ['https://www.example.com/0', 'https://www.example.com/1'],
-                              msg='tripple nesting in path should be treated as branches')
-        self.assertEqual(traverse_obj(_TEST_DATA, ('urls', ('fail', (..., 'url')))),
-                         ['https://www.example.com/0', 'https://www.example.com/1'],
-                         msg='ellipsis as branch path start gets flattened')
-
-        # Test dictionary as key
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: 100, 1: 1.2}), {0: 100, 1: 1.2},
-                         msg='dict key should result in a dict with the same keys')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', 0, 'url')}),
-                         {0: 'https://www.example.com/0'},
-                         msg='dict key should allow paths')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', (3, 0), 'url')}),
-                         {0: ['https://www.example.com/0']},
-                         msg='tuple in dict path should be treated as branches')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', ((1, 'fail'), (0, 'url')))}),
-                         {0: ['https://www.example.com/0']},
-                         msg='double nesting in dict path should be treated as paths')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', ((1, ('fail', 'url')), (0, 'url')))}),
-                         {0: ['https://www.example.com/1', 'https://www.example.com/0']},
-                         msg='tripple nesting in dict path should be treated as branches')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: 'fail'}), {},
-                         msg='remove `None` values when top level dict key fails')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: 'fail'}, default=...), {0: ...},
-                         msg='use `default` if key fails and `default`')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: 'dict'}), {},
-                         msg='remove empty values when dict key')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: 'dict'}, default=...), {0: ...},
-                         msg='use `default` when dict key and `default`')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: {0: 'fail'}}), {},
-                         msg='remove empty values when nested dict key fails')
-        self.assertEqual(traverse_obj(None, {0: 'fail'}), {},
-                         msg='default to dict if pruned')
-        self.assertEqual(traverse_obj(None, {0: 'fail'}, default=...), {0: ...},
-                         msg='default to dict if pruned and default is given')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: {0: 'fail'}}, default=...), {0: {0: ...}},
-                         msg='use nested `default` when nested dict key fails and `default`')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: ('dict', ...)}), {},
-                         msg='remove key if branch in dict key not successful')
-
-        # Testing default parameter behavior
-        _DEFAULT_DATA = {'None': None, 'int': 0, 'list': []}
-        self.assertEqual(traverse_obj(_DEFAULT_DATA, 'fail'), None,
-                         msg='default value should be `None`')
-        self.assertEqual(traverse_obj(_DEFAULT_DATA, 'fail', 'fail', default=...), ...,
-                         msg='chained fails should result in default')
-        self.assertEqual(traverse_obj(_DEFAULT_DATA, 'None', 'int'), 0,
-                         msg='should not short cirquit on `None`')
-        self.assertEqual(traverse_obj(_DEFAULT_DATA, 'fail', default=1), 1,
-                         msg='invalid dict key should result in `default`')
-        self.assertEqual(traverse_obj(_DEFAULT_DATA, 'None', default=1), 1,
-                         msg='`None` is a deliberate sentinel and should become `default`')
-        self.assertEqual(traverse_obj(_DEFAULT_DATA, ('list', 10)), None,
-                         msg='`IndexError` should result in `default`')
-        self.assertEqual(traverse_obj(_DEFAULT_DATA, (..., 'fail'), default=1), 1,
-                         msg='if branched but not successful return `default` if defined, not `[]`')
-        self.assertEqual(traverse_obj(_DEFAULT_DATA, (..., 'fail'), default=None), None,
-                         msg='if branched but not successful return `default` even if `default` is `None`')
-        self.assertEqual(traverse_obj(_DEFAULT_DATA, (..., 'fail')), [],
-                         msg='if branched but not successful return `[]`, not `default`')
-        self.assertEqual(traverse_obj(_DEFAULT_DATA, ('list', ...)), [],
-                         msg='if branched but object is empty return `[]`, not `default`')
-        self.assertEqual(traverse_obj(None, ...), [],
-                         msg='if branched but object is `None` return `[]`, not `default`')
-        self.assertEqual(traverse_obj({0: None}, (0, ...)), [],
-                         msg='if branched but state is `None` return `[]`, not `default`')
-
-        branching_paths = [
-            ('fail', ...),
-            (..., 'fail'),
-            100 * ('fail',) + (...,),
-            (...,) + 100 * ('fail',),
-        ]
-        for branching_path in branching_paths:
-            self.assertEqual(traverse_obj({}, branching_path), [],
-                             msg='if branched but state is `None`, return `[]` (not `default`)')
-            self.assertEqual(traverse_obj({}, 'fail', branching_path), [],
-                             msg='if branching in last alternative and previous did not match, return `[]` (not `default`)')
-            self.assertEqual(traverse_obj({0: 'x'}, 0, branching_path), 'x',
-                             msg='if branching in last alternative and previous did match, return single value')
-            self.assertEqual(traverse_obj({0: 'x'}, branching_path, 0), 'x',
-                             msg='if branching in first alternative and non-branching path does match, return single value')
-            self.assertEqual(traverse_obj({}, branching_path, 'fail'), None,
-                             msg='if branching in first alternative and non-branching path does not match, return `default`')
-
-        # Testing expected_type behavior
-        _EXPECTED_TYPE_DATA = {'str': 'str', 'int': 0}
-        self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'str', expected_type=str),
-                         'str', msg='accept matching `expected_type` type')
-        self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'str', expected_type=int),
-                         None, msg='reject non matching `expected_type` type')
-        self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'int', expected_type=lambda x: str(x)),
-                         '0', msg='transform type using type function')
-        self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'str', expected_type=lambda _: 1 / 0),
-                         None, msg='wrap expected_type fuction in try_call')
-        self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, ..., expected_type=str),
-                         ['str'], msg='eliminate items that expected_type fails on')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: 100, 1: 1.2}, expected_type=int),
-                         {0: 100}, msg='type as expected_type should filter dict values')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: 100, 1: 1.2, 2: 'None'}, expected_type=str_or_none),
-                         {0: '100', 1: '1.2'}, msg='function as expected_type should transform dict values')
-        self.assertEqual(traverse_obj(_TEST_DATA, ({0: 1.2}, 0, {int_or_none}), expected_type=int),
-                         1, msg='expected_type should not filter non final dict values')
-        self.assertEqual(traverse_obj(_TEST_DATA, {0: {0: 100, 1: 'str'}}, expected_type=int),
-                         {0: {0: 100}}, msg='expected_type should transform deep dict values')
-        self.assertEqual(traverse_obj(_TEST_DATA, [({0: '...'}, {0: '...'})], expected_type=type(...)),
-                         [{0: ...}, {0: ...}], msg='expected_type should transform branched dict values')
-        self.assertEqual(traverse_obj({1: {3: 4}}, [(1, 2), 3], expected_type=int),
-                         [4], msg='expected_type regression for type matching in tuple branching')
-        self.assertEqual(traverse_obj(_TEST_DATA, ['data', ...], expected_type=int),
-                         [], msg='expected_type regression for type matching in dict result')
-
-        # Test get_all behavior
-        _GET_ALL_DATA = {'key': [0, 1, 2]}
-        self.assertEqual(traverse_obj(_GET_ALL_DATA, ('key', ...), get_all=False), 0,
-                         msg='if not `get_all`, return only first matching value')
-        self.assertEqual(traverse_obj(_GET_ALL_DATA, ..., get_all=False), [0, 1, 2],
-                         msg='do not overflatten if not `get_all`')
-
-        # Test casesense behavior
-        _CASESENSE_DATA = {
-            'KeY': 'value0',
-            0: {
-                'KeY': 'value1',
-                0: {'KeY': 'value2'},
-            },
-        }
-        self.assertEqual(traverse_obj(_CASESENSE_DATA, 'key'), None,
-                         msg='dict keys should be case sensitive unless `casesense`')
-        self.assertEqual(traverse_obj(_CASESENSE_DATA, 'keY',
-                                      casesense=False), 'value0',
-                         msg='allow non matching key case if `casesense`')
-        self.assertEqual(traverse_obj(_CASESENSE_DATA, (0, ('keY',)),
-                                      casesense=False), ['value1'],
-                         msg='allow non matching key case in branch if `casesense`')
-        self.assertEqual(traverse_obj(_CASESENSE_DATA, (0, ((0, 'keY'),)),
-                                      casesense=False), ['value2'],
-                         msg='allow non matching key case in branch path if `casesense`')
-
-        # Test traverse_string behavior
-        _TRAVERSE_STRING_DATA = {'str': 'str', 1.2: 1.2}
-        self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', 0)), None,
-                         msg='do not traverse into string if not `traverse_string`')
-        self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', 0),
-                                      traverse_string=True), 's',
-                         msg='traverse into string if `traverse_string`')
-        self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, (1.2, 1),
-                                      traverse_string=True), '.',
-                         msg='traverse into converted data if `traverse_string`')
-        self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', ...),
-                                      traverse_string=True), 'str',
-                         msg='`...` should result in string (same value) if `traverse_string`')
-        self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', slice(0, None, 2)),
-                                      traverse_string=True), 'sr',
-                         msg='`slice` should result in string if `traverse_string`')
-        self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', lambda i, v: i or v == "s"),
-                                      traverse_string=True), 'str',
-                         msg='function should result in string if `traverse_string`')
-        self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', (0, 2)),
-                                      traverse_string=True), ['s', 'r'],
-                         msg='branching should result in list if `traverse_string`')
-        self.assertEqual(traverse_obj({}, (0, ...), traverse_string=True), [],
-                         msg='branching should result in list if `traverse_string`')
-        self.assertEqual(traverse_obj({}, (0, lambda x, y: True), traverse_string=True), [],
-                         msg='branching should result in list if `traverse_string`')
-        self.assertEqual(traverse_obj({}, (0, slice(1)), traverse_string=True), [],
-                         msg='branching should result in list if `traverse_string`')
-
-        # Test re.Match as input obj
-        mobj = re.fullmatch(r'0(12)(?P<group>3)(4)?', '0123')
-        self.assertEqual(traverse_obj(mobj, ...), [x for x in mobj.groups() if x is not None],
-                         msg='`...` on a `re.Match` should give its `groups()`')
-        self.assertEqual(traverse_obj(mobj, lambda k, _: k in (0, 2)), ['0123', '3'],
-                         msg='function on a `re.Match` should give groupno, value starting at 0')
-        self.assertEqual(traverse_obj(mobj, 'group'), '3',
-                         msg='str key on a `re.Match` should give group with that name')
-        self.assertEqual(traverse_obj(mobj, 2), '3',
-                         msg='int key on a `re.Match` should give group with that name')
-        self.assertEqual(traverse_obj(mobj, 'gRoUp', casesense=False), '3',
-                         msg='str key on a `re.Match` should respect casesense')
-        self.assertEqual(traverse_obj(mobj, 'fail'), None,
-                         msg='failing str key on a `re.Match` should return `default`')
-        self.assertEqual(traverse_obj(mobj, 'gRoUpS', casesense=False), None,
-                         msg='failing str key on a `re.Match` should return `default`')
-        self.assertEqual(traverse_obj(mobj, 8), None,
-                         msg='failing int key on a `re.Match` should return `default`')
-        self.assertEqual(traverse_obj(mobj, lambda k, _: k in (0, 'group')), ['0123', '3'],
-                         msg='function on a `re.Match` should give group name as well')
-
-        # Test xml.etree.ElementTree.Element as input obj
-        etree = xml.etree.ElementTree.fromstring('''<?xml version="1.0"?>
-        <data>
-            <country name="Liechtenstein">
-                <rank>1</rank>
-                <year>2008</year>
-                <gdppc>141100</gdppc>
-                <neighbor name="Austria" direction="E"/>
-                <neighbor name="Switzerland" direction="W"/>
-            </country>
-            <country name="Singapore">
-                <rank>4</rank>
-                <year>2011</year>
-                <gdppc>59900</gdppc>
-                <neighbor name="Malaysia" direction="N"/>
-            </country>
-            <country name="Panama">
-                <rank>68</rank>
-                <year>2011</year>
-                <gdppc>13600</gdppc>
-                <neighbor name="Costa Rica" direction="W"/>
-                <neighbor name="Colombia" direction="E"/>
-            </country>
-        </data>''')
-        self.assertEqual(traverse_obj(etree, ''), etree,
-                         msg='empty str key should return the element itself')
-        self.assertEqual(traverse_obj(etree, 'country'), list(etree),
-                         msg='str key should lead all children with that tag name')
-        self.assertEqual(traverse_obj(etree, ...), list(etree),
-                         msg='`...` as key should return all children')
-        self.assertEqual(traverse_obj(etree, lambda _, x: x[0].text == '4'), [etree[1]],
-                         msg='function as key should get element as value')
-        self.assertEqual(traverse_obj(etree, lambda i, _: i == 1), [etree[1]],
-                         msg='function as key should get index as key')
-        self.assertEqual(traverse_obj(etree, 0), etree[0],
-                         msg='int key should return the nth child')
-        self.assertEqual(traverse_obj(etree, './/neighbor/@name'),
-                         ['Austria', 'Switzerland', 'Malaysia', 'Costa Rica', 'Colombia'],
-                         msg='`@<attribute>` at end of path should give that attribute')
-        self.assertEqual(traverse_obj(etree, '//neighbor/@fail'), [None, None, None, None, None],
-                         msg='`@<nonexistant>` at end of path should give `None`')
-        self.assertEqual(traverse_obj(etree, ('//neighbor/@', 2)), {'name': 'Malaysia', 'direction': 'N'},
-                         msg='`@` should give the full attribute dict')
-        self.assertEqual(traverse_obj(etree, '//year/text()'), ['2008', '2011', '2011'],
-                         msg='`text()` at end of path should give the inner text')
-        self.assertEqual(traverse_obj(etree, '//*[@direction]/@direction'), ['E', 'W', 'N', 'W', 'E'],
-                         msg='full Python xpath features should be supported')
-        self.assertEqual(traverse_obj(etree, (0, '@name')), 'Liechtenstein',
-                         msg='special transformations should act on current element')
-        self.assertEqual(traverse_obj(etree, ('country', 0, ..., 'text()', {int_or_none})), [1, 2008, 141100],
-                         msg='special transformations should act on current element')
-
     def test_http_header_dict(self):
         headers = HTTPHeaderDict()
         headers['ytdl-test'] = b'0'