Repository: yt-dlp/yt-dlp
Pull request #9560 (2024-03-29 13:34:39)

[core] Fix `filesize_approx` calculation
**IMPORTANT**: PRs without the template will be CLOSED

### Description of your *pull request* and other information

<!--

Explanation of your *pull request* in arbitrary form goes here. Please **make sure the description explains the purpose and effect** of your *pull request* and is worded well enough to be understood. Provide as much **context and examples** as possible

-->
As brought up in https://github.com/yt-dlp/yt-dlp/issues/1400#issuecomment-1988222479 the `filesize_approx` calculation for yt-dlp was inaccurate.

Initially, this was [diagnosed](https://github.com/yt-dlp/yt-dlp/issues/1400#issuecomment-1988317902) as the YouTube extractor falsely returning a bitrate to a scale of 1000 instead of 1024. Due to the norm being to use a scale of 1000 for networking, and 1024 for filesizes, this causes the displayed bitrate of the YouTube extractor to vary from other tooling (see: https://github.com/yt-dlp/yt-dlp/issues/1400#issuecomment-2005080565).
This change was also inconsistent with other extractors (including generic [m3u8](https://github.com/yt-dlp/yt-dlp/blob/e5d4f11104ce7ea1717a90eea82c0f7d230ea5d5/yt_dlp/extractor/common.py#L2145), [mpd](https://github.com/yt-dlp/yt-dlp/blob/e5d4f11104ce7ea1717a90eea82c0f7d230ea5d5/yt_dlp/extractor/common.py#L2768), [smil](https://github.com/yt-dlp/yt-dlp/blob/e5d4f11104ce7ea1717a90eea82c0f7d230ea5d5/yt_dlp/extractor/common.py#L2373), [ism](https://github.com/yt-dlp/yt-dlp/blob/e5d4f11104ce7ea1717a90eea82c0f7d230ea5d5/yt_dlp/extractor/common.py#L3009)) which all use 1000 for bitrates and the interface which displays `k` instead of `ki`.

This PR reverts https://github.com/yt-dlp/yt-dlp/commit/22e4dfacb61f62dfbb3eb41b31c7b69ba1059b80

And instead adjusts the `filesize_approx` calculation to correctly multiply the tbr by 1000.

<details open><summary>Template</summary> <!-- OPEN is intentional -->

<!--

# PLEASE FOLLOW THE GUIDE BELOW

- You will be asked some questions, please read them **carefully** and answer honestly
- Put an `x` into all the boxes `[ ]` relevant to your *pull request* (like [x])
- Use *Preview* tab to see how your *pull request* will actually look like

-->

### 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/)
- [ ] I am not the original author of this code but it is in public domain or released under [Unlicense](http://unlicense.org/) (provide reliable evidence)

### What is the purpose of your *pull request*?
- [x] Fix or improvement to an extractor (Make sure to add/update tests)
- [ ] New extractor ([Piracy websites will not be accepted](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy))
- [x] Core bug fix/improvement
- [ ] 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: We also want to modify the docs to clarify this
pukkandan: Main issue is that the docs say it is `KBit/s`, which is wrong. We definitely should change it to `kbps` /`kbit/s`. We could also add tooltips to clarify? Something like https://github.com/yt-dlp/yt-dlp/commit/bd5ff4cc6d033dce9e3a56721a4fbdd5427df5c9

Patch:
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 d287ed42b..014bf262e 100644
--- a/README.md
+++ b/README.md
@@ -1472,9 +1472,9 @@ ## Filtering Formats
  - `width`: Width of the video, if known
  - `height`: Height of the video, if known
  - `aspect_ratio`: Aspect ratio of the video, if known
- - `tbr`: Average bitrate of audio and video in KBit/s
- - `abr`: Average audio bitrate in KBit/s
- - `vbr`: Average video bitrate in KBit/s
+ - `tbr`: Average bitrate of audio and video in [kbps](## "1000 bits/sec")
+ - `abr`: Average audio bitrate in [kbps](## "1000 bits/sec")
+ - `vbr`: Average video bitrate in [kbps](## "1000 bits/sec")
  - `asr`: Audio sampling rate in Hertz
  - `fps`: Frame rate
  - `audio_channels`: The number of audio channels
@@ -1499,7 +1499,7 @@ ## Filtering Formats
 
 **Note**: None of the aforementioned meta fields are guaranteed to be present since this solely depends on the metadata obtained by particular extractor, i.e. the metadata offered by the website. Any other field made available by the extractor can also be used for filtering.
 
-Formats for which the value is not known are excluded unless you put a question mark (`?`) after the operator. You can combine format filters, so `-f "bv[height<=?720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 KBit/s. You can also use the filters with `all` to download all formats that satisfy the filter, e.g. `-f "all[vcodec=none]"` selects all audio-only formats.
+Formats for which the value is not known are excluded unless you put a question mark (`?`) after the operator. You can combine format filters, so `-f "bv[height<=?720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 kbps. You can also use the filters with `all` to download all formats that satisfy the filter, e.g. `-f "all[vcodec=none]"` selects all audio-only formats.
 
 Format selectors can also be grouped using parentheses; e.g. `-f "(mp4,webm)[height<480]"` will download the best pre-merged mp4 and webm formats with a height lower than 480.
 
@@ -1531,10 +1531,10 @@ ## Sorting Formats
  - `fps`: Framerate of video
  - `hdr`: The dynamic range of the video (`DV` > `HDR12` > `HDR10+` > `HDR10` > `HLG` > `SDR`)
  - `channels`: The number of audio channels
- - `tbr`: Total average bitrate in KBit/s
- - `vbr`: Average video bitrate in KBit/s
- - `abr`: Average audio bitrate in KBit/s
- - `br`: Average bitrate in KBit/s, `tbr`/`vbr`/`abr`
+ - `tbr`: Total average bitrate in [kbps](## "1000 bits/sec")
+ - `vbr`: Average video bitrate in [kbps](## "1000 bits/sec")
+ - `abr`: Average audio bitrate in [kbps](## "1000 bits/sec")
+ - `br`: Average bitrate in [kbps](## "1000 bits/sec"), `tbr`/`vbr`/`abr`
  - `asr`: Audio sample rate in Hz
  
 **Deprecation warning**: Many of these fields have (currently undocumented) aliases, that may be removed in a future version. It is recommended to use only the documented field names.
@@ -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/devscripts/changelog_override.json b/devscripts/changelog_override.json
index 2a34ad071..eaa348cf2 100644
--- a/devscripts/changelog_override.json
+++ b/devscripts/changelog_override.json
@@ -126,5 +126,9 @@
         "when": "4ce57d3b873c2887814cbec03d029533e82f7db5",
         "short": "[ie] Support multi-period MPD streams (#6654)",
         "authors": ["alard", "pukkandan"]
+    },
+    {
+        "action": "remove",
+        "when": "22e4dfacb61f62dfbb3eb41b31c7b69ba1059b80"
     }
 ]
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/pyproject.toml b/pyproject.toml
index c57cac757..9faf53b9c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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/YoutubeDL.py b/yt_dlp/YoutubeDL.py
index e3d1db376..e83108619 100644
--- a/yt_dlp/YoutubeDL.py
+++ b/yt_dlp/YoutubeDL.py
@@ -146,6 +146,7 @@
     subtitles_filename,
     supports_terminal_sequences,
     system_identifier,
+    filesize_from_tbr,
     timetuple_from_msec,
     to_high_limit_path,
     traverse_obj,
@@ -2813,6 +2814,9 @@ def is_wellformed(f):
             format['url'] = sanitize_url(format['url'])
             if format.get('ext') is None:
                 format['ext'] = determine_ext(format['url']).lower()
+            if format['ext'] in ('aac', 'opus', 'mp3', 'flac', 'vorbis'):
+                if format.get('acodec') is None:
+                    format['acodec'] = format['ext']
             if format.get('protocol') is None:
                 format['protocol'] = determine_protocol(format)
             if format.get('resolution') is None:
@@ -2823,9 +2827,8 @@ def is_wellformed(f):
                 format['aspect_ratio'] = try_call(lambda: round(format['width'] / format['height'], 2))
             # For fragmented formats, "tbr" is often max bitrate and not average
             if (('manifest-filesize-approx' in self.params['compat_opts'] or not format.get('manifest_url'))
-                    and info_dict.get('duration') and format.get('tbr')
                     and not format.get('filesize') and not format.get('filesize_approx')):
-                format['filesize_approx'] = int(info_dict['duration'] * format['tbr'] * (1024 / 8))
+                format['filesize_approx'] = filesize_from_tbr(format.get('tbr'), info_dict.get('duration'))
             format['http_headers'] = self._calc_headers(collections.ChainMap(format, info_dict), load_cookies=True)
 
         # Safeguard against old/insecure infojson when using --load-info-json
@@ -3875,8 +3878,8 @@ def simplified_codec(f, field):
                 delim, (
                     format_field(f, 'filesize', ' \t%s', func=format_bytes)
                     or format_field(f, 'filesize_approx', '≈\t%s', func=format_bytes)
-                    or format_field(try_call(lambda: format_bytes(int(info_dict['duration'] * f['tbr'] * (1024 / 8)))),
-                                    None, self._format_out('~\t%s', self.Styles.SUPPRESS))),
+                    or format_field(filesize_from_tbr(f.get('tbr'), info_dict.get('duration')), None,
+                                    self._format_out('~\t%s', self.Styles.SUPPRESS), func=format_bytes)),
                 format_field(f, 'tbr', '\t%dk', func=round),
                 shorten_protocol_name(f.get('protocol', '')),
                 delim,
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/common.py b/yt_dlp/extractor/common.py
index e776ccae9..57bbf9bdf 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,
@@ -170,12 +171,12 @@ class InfoExtractor:
                                  Automatically calculated from width and height
                     * dynamic_range The dynamic range of the video. One of:
                                  "SDR" (None), "HDR10", "HDR10+, "HDR12", "HLG, "DV"
-                    * tbr        Average bitrate of audio and video in KBit/s
-                    * abr        Average audio bitrate in KBit/s
+                    * tbr        Average bitrate of audio and video in kbps (1000 bits/sec)
+                    * abr        Average audio bitrate in kbps (1000 bits/sec)
                     * acodec     Name of the audio codec in use
                     * asr        Audio sampling rate in Hertz
                     * audio_channels  Number of audio channels
-                    * vbr        Average video bitrate in KBit/s
+                    * vbr        Average video bitrate in kbps (1000 bits/sec)
                     * fps        Frame rate
                     * vcodec     Name of the video codec in use
                     * container  Name of the container format
@@ -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/soundcloud.py b/yt_dlp/extractor/soundcloud.py
index a7c2afd49..30cdab40c 100644
--- a/yt_dlp/extractor/soundcloud.py
+++ b/yt_dlp/extractor/soundcloud.py
@@ -217,6 +217,7 @@ def _extract_info_dict(self, info, full_title=None, secret_token=None, extract_f
                         'filesize': int_or_none(urlh.headers.get('Content-Length')),
                         'url': format_url,
                         'quality': 10,
+                        'format_note': 'Original',
                     })
 
         def invalid_url(url):
@@ -233,9 +234,13 @@ def add_format(f, protocol, is_preview=False):
                 format_id_list.append(protocol)
             ext = f.get('ext')
             if ext == 'aac':
-                f['abr'] = '256'
+                f.update({
+                    'abr': 256,
+                    'quality': 5,
+                    'format_note': 'Premium',
+                })
             for k in ('ext', 'abr'):
-                v = f.get(k)
+                v = str_or_none(f.get(k))
                 if v:
                     format_id_list.append(v)
             preview = is_preview or re.search(r'/(?:preview|playlist)/0/30/', f['url'])
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/youtube.py b/yt_dlp/extractor/youtube.py
index 589cd9b59..b41191b7f 100644
--- a/yt_dlp/extractor/youtube.py
+++ b/yt_dlp/extractor/youtube.py
@@ -3602,8 +3602,8 @@ def _extract_player_response(self, client, video_id, master_ytcfg, player_ytcfg,
         yt_query = {
             'videoId': video_id,
         }
-        if _split_innertube_client(client)[0] == 'android':
-            yt_query['params'] = 'CgIQBg=='
+        if _split_innertube_client(client)[0] in ('android', 'android_embedscreen'):
+            yt_query['params'] = 'CgIIAQ=='
 
         pp_arg = self._configuration_arg('player_params', [None], casesense=True)[0]
         if pp_arg:
@@ -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'), 1024)
+            tbr = float_or_none(fmt.get('averageBitrate') or fmt.get('bitrate'), 1000)
             language_preference = (
                 10 if audio_track.get('audioIsDefault') and 10
                 else -10 if 'descriptive' in (audio_track.get('displayName') or '').lower() and -10
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/_utils.py b/yt_dlp/utils/_utils.py
index 9efeb6a1c..648cf0abd 100644
--- a/yt_dlp/utils/_utils.py
+++ b/yt_dlp/utils/_utils.py
@@ -5415,6 +5415,17 @@ def calculate_preference(self, format):
         return tuple(self._calculate_field_preference(format, field) for field in self._order)
 
 
+def filesize_from_tbr(tbr, duration):
+    """
+    @param tbr:      Total bitrate in kbps (1000 bits/sec)
+    @param duration: Duration in seconds
+    @returns         Filesize in bytes
+    """
+    if tbr is None or duration is None:
+        return None
+    return int(duration * tbr * (1000 / 8))
+
+
 # XXX: Temporary
 class _YDLLogger:
     def __init__(self, ydl=None):
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_YoutubeDL.py b/test/test_YoutubeDL.py
index 6be47af97..5242cf88f 100644
--- a/test/test_YoutubeDL.py
+++ b/test/test_YoutubeDL.py
@@ -183,7 +183,7 @@ def test_format_selection_audio_exts(self):
         ]
 
         info_dict = _make_result(formats)
-        ydl = YDL({'format': 'best'})
+        ydl = YDL({'format': 'best', 'format_sort': ['abr', 'ext']})
         ydl.sort_formats(info_dict)
         ydl.process_ie_result(copy.deepcopy(info_dict))
         downloaded = ydl.downloaded_info_dicts[0]
@@ -195,7 +195,7 @@ def test_format_selection_audio_exts(self):
         downloaded = ydl.downloaded_info_dicts[0]
         self.assertEqual(downloaded['format_id'], 'mp3-64')
 
-        ydl = YDL({'prefer_free_formats': True})
+        ydl = YDL({'prefer_free_formats': True, 'format_sort': ['abr', 'ext']})
         ydl.sort_formats(info_dict)
         ydl.process_ie_result(copy.deepcopy(info_dict))
         downloaded = ydl.downloaded_info_dicts[0]
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'