Repository: yt-dlp/yt-dlp
Pull request #9582 (2024-03-31 09:46:47)

[build] Add SHA2-SUMS to GHA logs
In light of what happened with xz's tarballs, I think it would be good to provide a verifiable "chain of custody" from our git repo to our release assets (tarball, binaries).

Our GitHub Actions logs for the build workflow will have a section that looks like this now:

<https://github.com/bashonly/yt-dlp/actions/runs/8497175700/job/23275527118>

```
Make SHA2-SUMS files

Run cd ./artifact/
  cd ./artifact/
  sha256sum * | tee ../SHA2-256SUMS
  sha512sum * | tee ../SHA2-512SUMS
  shell: /usr/bin/bash -e {0}
cd1214ccda412c384c0850443dfab2db81e382ee23e6e5fe638a7f2fbd15ee3a  yt-dlp
5d658358b341a90d6c99f56e404b9e4e42f55181d5ed59b5143b75eb98699b92  yt-dlp.exe
8a303ed7e113b1870591d4fc9ddbf7a5d1c8d602c5370914e753cbe81c825a92  yt-dlp.tar.gz
d7484dcc965c308dfaaaa44e2f659ab001f9d65d55112725d34e21df1bc15e93  yt-dlp_linux
28767a12b06e47a523f57c7520c0bf2ef5dde9d301b5bdccb75049a2d2286b09  yt-dlp_linux.zip
6f34dd1638cbf24e8bafd98f2e2c95556d4548199c4aafc444ebf5e4e4905c28  yt-dlp_linux_aarch64
6c14c77988ce494cea2b9b12d9cdc98bb5e526fb6811600c8ae5917fe259f3dc  yt-dlp_linux_armv7l
1aae9f5f8b85d79d33563fe88096676c81ddba8e8766945f4cb42cdb47572f80  yt-dlp_macos
3f83944bae3306b3ff94b6bb83090c1a146f63160c6998e707c2a3ae0a48133f  yt-dlp_macos.zip
7ecfe36e77f4f19f242f8afb39518bdf441ca386be870088bf1f6a11099444e7  yt-dlp_macos_legacy
727b2cf3b4f426f3a9d5cae357eff18f87c21137475d37e0d74b848f875c547f  yt-dlp_min.exe
7aa0582c24f03bc04bbb11b9f3b7faed21618d5715921c86819a0717fdf4bbb0  yt-dlp_win.zip
b048f7fcd9f6b3987616fca6590067e98e9bfcda7a24de336a835b778f81ed43  yt-dlp_x86.exe
1c438729262c7416b313ffd577bcbf867c227aaf27b869c2f2f07f65f3f819b9b931d023b8627dc9039e79e550a0efa0d53a4eb82253752130b53e7bafbcb387  yt-dlp
258ce97c012f70388b906915549db0c31db98e66e83df56ceff3d31eea498c92a2f299f49afb2de8fca4664d4d072e435fe357a3ca2885f4cea00cb40aae7bda  yt-dlp.exe
f757ec9e7767d5aaddf8dc381a0b05659ded3a27c8164ae35ce509959df72f80da9b4d63fba12b6d4346fff7f74c0ec57cbad34c65362cb257d27af42389b080  yt-dlp.tar.gz
272fca3809d1df1c49168727a6c666716a8eb061ed7e171fb33c4226e87759bfe0557cb1c024b94a89f22c9c23a2e18790ce15d9671e9e5bb4c7e9f3c8916d26  yt-dlp_linux
12e37a4ede23f3ca022c961ab7ae65fbd196ce02da7afc1138fbc808c19ee93208e41a6d9df3fbd8a1077f0233a4c78e7ad752fd7ce78723a321f750ff6d048e  yt-dlp_linux.zip
1643f3e553b76413d07c483c84d48a0f2e311b2785d104f79f5f1533b52663ccbb6ecd44a8e2e7657b232391bc99dc2947ad872482136afb0a0bb97d3d10b716  yt-dlp_linux_aarch64
98a6c8c6397965336b7905ac786a455469f6bb1c9152ece7f191dba96b4f969a96e88a068121e4ec3c6acc9dcc0a5696af15962669ab687c26e0589509089a19  yt-dlp_linux_armv7l
136eec8ce6afb64ab1a7887faee9637a9408336cf3f60676422e4159603fd546b6d4698a56374ba268628504cca89f68045034dbfe498cdc6f65cd4b74a1c068  yt-dlp_macos
39de44e968808eeea07417841a15a7bfe58c53d77bfc7a65574d2658eea607c343d9fb2af0237094a244f6884e8dc5bad1216560efbed9ce7d14158eeff8296a  yt-dlp_macos.zip
a485d2904169274ecf07dcdfec4a0963740721952dc6dbc29c9f1a346f38457f0c2b4b7c6a2ded7b19ecb4dce65c262a81198271c33c2dae2765e96fdb529172  yt-dlp_macos_legacy
080e226839223a4be61f81fd325cb6616e45b3a6337e764a671a906a646dfd0e01cedb3e929004c806ee63e09debca774f91490572f28cd353076af70357b467  yt-dlp_min.exe
4ca017bae87b8a519a2b7fced7a6626411b1835d839afe37d21f21233f1f952e180685f7c7571dc0b3db6787609585db57f41c7b5af1243e4e7de93d9fc433db  yt-dlp_win.zip
0e2b0c1294143af9937cac1e20832e1603bc4452a3d777d401cb137501f013d38c443cf79f1e38b2894f74ad865dd5bb077866d80a2de99dd52c0edfab383be0  yt-dlp_x86.exe
```

Users and downstream packagers can compare the sums in the log with those in the `SHA256SUMS` and `SHA512SUMS` release assets to verify that the release assets are indeed the product of our build workflow and have not been tampered with.



<details open><summary>Template</summary> <!-- OPEN is intentional -->


### Before submitting a *pull request* make sure you have:
- [x] At least skimmed through [contributing guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) including [yt-dlp coding conventions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#yt-dlp-coding-conventions)
- [x] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests
- [x] Checked the code with [flake8](https://pypi.python.org/pypi/flake8) and [ran relevant tests](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions)

### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check all of the following options that apply:
- [x] I am the original author of this code and I am willing to release it under [Unlicense](http://unlicense.org/)

### What is the purpose of your *pull request*?
- [x] Core bug fix/improvement

</details>


pukkandan: We used to print it in old workflow. Was accidentally lost during rework
pukkandan: Maybe add a comment so the mistake don't happen again

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d773d5a1c..5285923e7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -467,8 +467,9 @@ jobs:
       - name: Make SHA2-SUMS files
         run: |
           cd ./artifact/
-          sha256sum * > ../SHA2-256SUMS
-          sha512sum * > ../SHA2-512SUMS
+          # make sure SHA sums are also printed to stdout
+          sha256sum * | tee ../SHA2-256SUMS
+          sha512sum * | tee ../SHA2-512SUMS
 
       - name: Make Update spec
         run: |
diff --git a/README.md b/README.md
index d0683a34a..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.
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/tomlparse.py b/devscripts/tomlparse.py
index 85ac4eef7..ac9ea3170 100755
--- a/devscripts/tomlparse.py
+++ b/devscripts/tomlparse.py
@@ -11,7 +11,7 @@
 
 from __future__ import annotations
 
-import datetime
+import datetime as dt
 import json
 import re
 
@@ -115,9 +115,9 @@ def parse_value(data: str, index: int):
     for func in [
         int,
         float,
-        datetime.time.fromisoformat,
-        datetime.date.fromisoformat,
-        datetime.datetime.fromisoformat,
+        dt.time.fromisoformat,
+        dt.date.fromisoformat,
+        dt.datetime.fromisoformat,
         {'true': True, 'false': False}.get,
     ]:
         try:
@@ -179,7 +179,7 @@ def main():
         data = file.read()
 
     def default(obj):
-        if isinstance(obj, (datetime.date, datetime.time, datetime.datetime)):
+        if isinstance(obj, (dt.date, dt.time, dt.datetime)):
             return obj.isoformat()
 
     print(json.dumps(parse_toml(data), default=default))
diff --git a/devscripts/update-version.py b/devscripts/update-version.py
index da54a6a25..07a071745 100644
--- a/devscripts/update-version.py
+++ b/devscripts/update-version.py
@@ -9,15 +9,15 @@
 
 import argparse
 import contextlib
+import datetime as dt
 import sys
-from datetime import datetime, timezone
 
 from devscripts.utils import read_version, run_process, write_file
 
 
 def get_new_version(version, revision):
     if not version:
-        version = datetime.now(timezone.utc).strftime('%Y.%m.%d')
+        version = dt.datetime.now(dt.timezone.utc).strftime('%Y.%m.%d')
 
     if revision:
         assert revision.isdecimal(), 'Revision must be a number'
diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py
index e3d1db376..291fc8d00 100644
--- a/yt_dlp/YoutubeDL.py
+++ b/yt_dlp/YoutubeDL.py
@@ -1,7 +1,7 @@
 import collections
 import contextlib
 import copy
-import datetime
+import datetime as dt
 import errno
 import fileinput
 import http.cookiejar
@@ -146,6 +146,7 @@
     subtitles_filename,
     supports_terminal_sequences,
     system_identifier,
+    filesize_from_tbr,
     timetuple_from_msec,
     to_high_limit_path,
     traverse_obj,
@@ -2628,7 +2629,7 @@ def _fill_common_fields(self, info_dict, final=True):
                 # Working around out-of-range timestamp values (e.g. negative ones on Windows,
                 # see http://bugs.python.org/issue1646728)
                 with contextlib.suppress(ValueError, OverflowError, OSError):
-                    upload_date = datetime.datetime.fromtimestamp(info_dict[ts_key], datetime.timezone.utc)
+                    upload_date = dt.datetime.fromtimestamp(info_dict[ts_key], dt.timezone.utc)
                     info_dict[date_key] = upload_date.strftime('%Y%m%d')
 
         if not info_dict.get('release_year'):
@@ -2782,7 +2783,7 @@ def sanitize_numeric_fields(info):
 
         get_from_start = not info_dict.get('is_live') or bool(self.params.get('live_from_start'))
         if not get_from_start:
-            info_dict['title'] += ' ' + datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
+            info_dict['title'] += ' ' + dt.datetime.now().strftime('%Y-%m-%d %H:%M')
         if info_dict.get('is_live') and formats:
             formats = [f for f in formats if bool(f.get('is_from_start')) == get_from_start]
             if get_from_start and not formats:
@@ -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/cookies.py b/yt_dlp/cookies.py
index 28d174a09..85d6dd182 100644
--- a/yt_dlp/cookies.py
+++ b/yt_dlp/cookies.py
@@ -1,6 +1,7 @@
 import base64
 import collections
 import contextlib
+import datetime as dt
 import glob
 import http.cookiejar
 import http.cookies
@@ -15,7 +16,6 @@
 import tempfile
 import time
 import urllib.request
-from datetime import datetime, timedelta, timezone
 from enum import Enum, auto
 from hashlib import pbkdf2_hmac
 
@@ -594,7 +594,7 @@ def skip_to_end(self, description='unknown'):
 
 
 def _mac_absolute_time_to_posix(timestamp):
-    return int((datetime(2001, 1, 1, 0, 0, tzinfo=timezone.utc) + timedelta(seconds=timestamp)).timestamp())
+    return int((dt.datetime(2001, 1, 1, 0, 0, tzinfo=dt.timezone.utc) + dt.timedelta(seconds=timestamp)).timestamp())
 
 
 def _parse_safari_cookies_header(data, logger):
diff --git a/yt_dlp/extractor/ard.py b/yt_dlp/extractor/ard.py
index 46e68d61e..3db59c5ca 100644
--- a/yt_dlp/extractor/ard.py
+++ b/yt_dlp/extractor/ard.py
@@ -1,5 +1,5 @@
+import functools
 import re
-from functools import partial
 
 from .common import InfoExtractor
 from ..utils import (
@@ -349,7 +349,7 @@ def _extract_episode_info(self, title):
             r'(?P<title>.*)',
         ]
 
-        return traverse_obj(patterns, (..., {partial(re.match, string=title)}, {
+        return traverse_obj(patterns, (..., {functools.partial(re.match, string=title)}, {
             'season_number': ('season_number', {int_or_none}),
             'episode_number': ('episode_number', {int_or_none}),
             'episode': ((
diff --git a/yt_dlp/extractor/atvat.py b/yt_dlp/extractor/atvat.py
index d6ed9e495..d60feba31 100644
--- a/yt_dlp/extractor/atvat.py
+++ b/yt_dlp/extractor/atvat.py
@@ -1,4 +1,4 @@
-import datetime
+import datetime as dt
 
 from .common import InfoExtractor
 from ..utils import (
@@ -71,9 +71,9 @@ def _real_extract(self, url):
         content_ids = [{'id': id, 'subclip_start': content['start'], 'subclip_end': content['end']}
                        for id, content in enumerate(contentResource)]
 
-        time_of_request = datetime.datetime.now()
-        not_before = time_of_request - datetime.timedelta(minutes=5)
-        expire = time_of_request + datetime.timedelta(minutes=5)
+        time_of_request = dt.datetime.now()
+        not_before = time_of_request - dt.timedelta(minutes=5)
+        expire = time_of_request + dt.timedelta(minutes=5)
         payload = {
             'content_ids': {
                 content_id: content_ids,
diff --git a/yt_dlp/extractor/aws.py b/yt_dlp/extractor/aws.py
index c4741a6a1..4ebef9295 100644
--- a/yt_dlp/extractor/aws.py
+++ b/yt_dlp/extractor/aws.py
@@ -1,4 +1,4 @@
-import datetime
+import datetime as dt
 import hashlib
 import hmac
 
@@ -12,7 +12,7 @@ class AWSIE(InfoExtractor):  # XXX: Conventionally, base classes should end with
 
     def _aws_execute_api(self, aws_dict, video_id, query=None):
         query = query or {}
-        amz_date = datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
+        amz_date = dt.datetime.now(dt.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
         date = amz_date[:8]
         headers = {
             'Accept': 'application/json',
diff --git a/yt_dlp/extractor/bibeltv.py b/yt_dlp/extractor/bibeltv.py
index 34464daa1..666b51c56 100644
--- a/yt_dlp/extractor/bibeltv.py
+++ b/yt_dlp/extractor/bibeltv.py
@@ -1,4 +1,4 @@
-from functools import partial
+import functools
 
 from .common import InfoExtractor
 from ..utils import (
@@ -50,7 +50,7 @@ def _extract_base_info(data):
             **traverse_obj(data, {
                 'title': 'title',
                 'description': 'description',
-                'duration': ('duration', {partial(int_or_none, scale=1000)}),
+                'duration': ('duration', {functools.partial(int_or_none, scale=1000)}),
                 'timestamp': ('schedulingStart', {parse_iso8601}),
                 'season_number': 'seasonNumber',
                 'episode_number': 'episodeNumber',
diff --git a/yt_dlp/extractor/bundestag.py b/yt_dlp/extractor/bundestag.py
index 9fd7c7de1..71f772665 100644
--- a/yt_dlp/extractor/bundestag.py
+++ b/yt_dlp/extractor/bundestag.py
@@ -1,5 +1,5 @@
+import functools
 import re
-from functools import partial
 
 from .common import InfoExtractor
 from ..networking.exceptions import HTTPError
@@ -115,9 +115,9 @@ def _real_extract(self, url):
             note='Downloading metadata overlay', fatal=False,
         ), {
             'title': (
-                {partial(get_element_text_and_html_by_tag, 'h3')}, 0,
-                {partial(re.sub, r'<span[^>]*>[^<]+</span>', '')}, {clean_html}),
-            'description': ({partial(get_element_text_and_html_by_tag, 'p')}, 0, {clean_html}),
+                {functools.partial(get_element_text_and_html_by_tag, 'h3')}, 0,
+                {functools.partial(re.sub, r'<span[^>]*>[^<]+</span>', '')}, {clean_html}),
+            'description': ({functools.partial(get_element_text_and_html_by_tag, 'p')}, 0, {clean_html}),
         }))
 
         return result
diff --git a/yt_dlp/extractor/cda.py b/yt_dlp/extractor/cda.py
index 1157114b2..90b4d082e 100644
--- a/yt_dlp/extractor/cda.py
+++ b/yt_dlp/extractor/cda.py
@@ -1,6 +1,6 @@
 import base64
 import codecs
-import datetime
+import datetime as dt
 import hashlib
 import hmac
 import json
@@ -134,7 +134,7 @@ def _perform_login(self, username, password):
         self._API_HEADERS['User-Agent'] = f'pl.cda 1.0 (version {app_version}; Android {android_version}; {phone_model})'
 
         cached_bearer = self.cache.load(self._BEARER_CACHE, username) or {}
-        if cached_bearer.get('valid_until', 0) > datetime.datetime.now().timestamp() + 5:
+        if cached_bearer.get('valid_until', 0) > dt.datetime.now().timestamp() + 5:
             self._API_HEADERS['Authorization'] = f'Bearer {cached_bearer["token"]}'
             return
 
@@ -154,7 +154,7 @@ def _perform_login(self, username, password):
             })
         self.cache.store(self._BEARER_CACHE, username, {
             'token': token_res['access_token'],
-            'valid_until': token_res['expires_in'] + datetime.datetime.now().timestamp(),
+            'valid_until': token_res['expires_in'] + dt.datetime.now().timestamp(),
         })
         self._API_HEADERS['Authorization'] = f'Bearer {token_res["access_token"]}'
 
diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py
index bd318a7f4..57bbf9bdf 100644
--- a/yt_dlp/extractor/common.py
+++ b/yt_dlp/extractor/common.py
@@ -171,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
diff --git a/yt_dlp/extractor/dtube.py b/yt_dlp/extractor/dtube.py
index bb06c42be..5ea014cf0 100644
--- a/yt_dlp/extractor/dtube.py
+++ b/yt_dlp/extractor/dtube.py
@@ -1,5 +1,5 @@
 import json
-from socket import timeout
+import socket
 
 from .common import InfoExtractor
 from ..utils import (
@@ -56,7 +56,7 @@ def canonical_url(h):
             try:
                 self.to_screen('%s: Checking %s video format URL' % (video_id, format_id))
                 self._downloader._opener.open(video_url, timeout=5).close()
-            except timeout:
+            except socket.timeout:
                 self.to_screen(
                     '%s: %s URL is invalid, skipping' % (video_id, format_id))
                 continue
diff --git a/yt_dlp/extractor/goplay.py b/yt_dlp/extractor/goplay.py
index 74aad1192..7a98e0f31 100644
--- a/yt_dlp/extractor/goplay.py
+++ b/yt_dlp/extractor/goplay.py
@@ -1,6 +1,6 @@
 import base64
 import binascii
-import datetime
+import datetime as dt
 import hashlib
 import hmac
 import json
@@ -422,7 +422,7 @@ def __get_current_timestamp():
         months = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
         days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
 
-        time_now = datetime.datetime.now(datetime.timezone.utc)
+        time_now = dt.datetime.now(dt.timezone.utc)
         format_string = "{} {} {} %H:%M:%S UTC %Y".format(days[time_now.weekday()], months[time_now.month], time_now.day)
         time_string = time_now.strftime(format_string)
         return time_string
diff --git a/yt_dlp/extractor/joqrag.py b/yt_dlp/extractor/joqrag.py
index 3bb28af94..c68ad8cb5 100644
--- a/yt_dlp/extractor/joqrag.py
+++ b/yt_dlp/extractor/joqrag.py
@@ -1,4 +1,4 @@
-import datetime
+import datetime as dt
 import urllib.parse
 
 from .common import InfoExtractor
@@ -50,8 +50,8 @@ def _extract_metadata(self, variable, html):
 
     def _extract_start_timestamp(self, video_id, is_live):
         def extract_start_time_from(date_str):
-            dt = datetime_from_str(date_str) + datetime.timedelta(hours=9)
-            date = dt.strftime('%Y%m%d')
+            dt_ = datetime_from_str(date_str) + dt.timedelta(hours=9)
+            date = dt_.strftime('%Y%m%d')
             start_time = self._search_regex(
                 r'<h3[^>]+\bclass="dailyProgram-itemHeaderTime"[^>]*>[\s\d:]+–\s*(\d{1,2}:\d{1,2})',
                 self._download_webpage(
@@ -60,7 +60,7 @@ def extract_start_time_from(date_str):
                     errnote=f'Failed to download program list of {date}') or '',
                 'start time', default=None)
             if start_time:
-                return unified_timestamp(f'{dt.strftime("%Y/%m/%d")} {start_time} +09:00')
+                return unified_timestamp(f'{dt_.strftime("%Y/%m/%d")} {start_time} +09:00')
             return None
 
         start_timestamp = extract_start_time_from('today')
@@ -87,7 +87,7 @@ def _real_extract(self, url):
             msg = 'This stream is not currently live'
             if release_timestamp:
                 msg += (' and will start at '
-                        + datetime.datetime.fromtimestamp(release_timestamp).strftime('%Y-%m-%d %H:%M:%S'))
+                        + dt.datetime.fromtimestamp(release_timestamp).strftime('%Y-%m-%d %H:%M:%S'))
             self.raise_no_formats(msg, expected=True)
         else:
             m3u8_path = self._search_regex(
diff --git a/yt_dlp/extractor/leeco.py b/yt_dlp/extractor/leeco.py
index 85033b8f8..5d61a607f 100644
--- a/yt_dlp/extractor/leeco.py
+++ b/yt_dlp/extractor/leeco.py
@@ -1,4 +1,4 @@
-import datetime
+import datetime as dt
 import hashlib
 import re
 import time
@@ -185,7 +185,7 @@ def get_flash_urls(media_url, format_id):
 
         publish_time = parse_iso8601(self._html_search_regex(
             r'发布时间&nbsp;([^<>]+) ', page, 'publish time', default=None),
-            delimiter=' ', timezone=datetime.timedelta(hours=8))
+            delimiter=' ', timezone=dt.timedelta(hours=8))
         description = self._html_search_meta('description', page, fatal=False)
 
         return {
diff --git a/yt_dlp/extractor/linkedin.py b/yt_dlp/extractor/linkedin.py
index ad41c0e20..e12f467ef 100644
--- a/yt_dlp/extractor/linkedin.py
+++ b/yt_dlp/extractor/linkedin.py
@@ -1,4 +1,4 @@
-from itertools import zip_longest
+import itertools
 import re
 
 from .common import InfoExtractor
@@ -156,7 +156,7 @@ class LinkedInLearningIE(LinkedInLearningBaseIE):
 
     def json2srt(self, transcript_lines, duration=None):
         srt_data = ''
-        for line, (line_dict, next_dict) in enumerate(zip_longest(transcript_lines, transcript_lines[1:])):
+        for line, (line_dict, next_dict) in enumerate(itertools.zip_longest(transcript_lines, transcript_lines[1:])):
             start_time, caption = line_dict['transcriptStartAt'] / 1000, line_dict['caption']
             end_time = next_dict['transcriptStartAt'] / 1000 if next_dict else duration or start_time + 1
             srt_data += '%d\n%s --> %s\n%s\n\n' % (line + 1, srt_subtitles_timecode(start_time),
diff --git a/yt_dlp/extractor/masters.py b/yt_dlp/extractor/masters.py
index 716f1c961..c3c58d7d0 100644
--- a/yt_dlp/extractor/masters.py
+++ b/yt_dlp/extractor/masters.py
@@ -1,4 +1,3 @@
-from __future__ import unicode_literals
 from .common import InfoExtractor
 from ..utils import (
     traverse_obj,
diff --git a/yt_dlp/extractor/microsoftstream.py b/yt_dlp/extractor/microsoftstream.py
index 9b50996b7..5f5f16087 100644
--- a/yt_dlp/extractor/microsoftstream.py
+++ b/yt_dlp/extractor/microsoftstream.py
@@ -1,4 +1,4 @@
-from base64 import b64decode
+import base64
 
 from .common import InfoExtractor
 from ..utils import (
@@ -81,7 +81,7 @@ def _real_extract(self, url):
                 'url': thumbnail_url,
             }
             thumb_name = url_basename(thumbnail_url)
-            thumb_name = str(b64decode(thumb_name + '=' * (-len(thumb_name) % 4)))
+            thumb_name = str(base64.b64decode(thumb_name + '=' * (-len(thumb_name) % 4)))
             thumb.update(parse_resolution(thumb_name))
             thumbnails.append(thumb)
 
diff --git a/yt_dlp/extractor/motherless.py b/yt_dlp/extractor/motherless.py
index 160150a7b..b6c18fe5b 100644
--- a/yt_dlp/extractor/motherless.py
+++ b/yt_dlp/extractor/motherless.py
@@ -1,4 +1,4 @@
-import datetime
+import datetime as dt
 import re
 import urllib.parse
 
@@ -151,7 +151,7 @@ def _real_extract(self, url):
                     'd': 'days',
                 }
                 kwargs = {_AGO_UNITS.get(uploaded_ago[-1]): delta}
-                upload_date = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(**kwargs)).strftime('%Y%m%d')
+                upload_date = (dt.datetime.now(dt.timezone.utc) - dt.timedelta(**kwargs)).strftime('%Y%m%d')
 
         comment_count = len(re.findall(r'''class\s*=\s*['"]media-comment-contents\b''', webpage))
         uploader_id = self._html_search_regex(
diff --git a/yt_dlp/extractor/naver.py b/yt_dlp/extractor/naver.py
index 806b79082..885557e91 100644
--- a/yt_dlp/extractor/naver.py
+++ b/yt_dlp/extractor/naver.py
@@ -4,8 +4,8 @@
 import itertools
 import json
 import re
+import urllib.parse
 import time
-from urllib.parse import parse_qs, urlparse
 
 from .common import InfoExtractor
 from ..utils import (
@@ -388,7 +388,7 @@ def _extract_highlight(self, show_id, highlight_id):
 
     def _real_extract(self, url):
         show_id = self._match_id(url)
-        qs = parse_qs(urlparse(url).query)
+        qs = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
 
         if not self._yes_playlist(show_id, qs.get('shareHightlight')):
             return self._extract_highlight(show_id, qs['shareHightlight'][0])
diff --git a/yt_dlp/extractor/neteasemusic.py b/yt_dlp/extractor/neteasemusic.py
index d332b840c..73b33a9f9 100644
--- a/yt_dlp/extractor/neteasemusic.py
+++ b/yt_dlp/extractor/neteasemusic.py
@@ -1,9 +1,9 @@
+import hashlib
 import itertools
 import json
+import random
 import re
 import time
-from hashlib import md5
-from random import randint
 
 from .common import InfoExtractor
 from ..aes import aes_ecb_encrypt, pkcs7_padding
@@ -34,7 +34,7 @@ def _create_eapi_cipher(self, api_path, query_body, cookies):
         request_text = json.dumps({**query_body, 'header': cookies}, separators=(',', ':'))
 
         message = f'nobody{api_path}use{request_text}md5forencrypt'.encode('latin1')
-        msg_digest = md5(message).hexdigest()
+        msg_digest = hashlib.md5(message).hexdigest()
 
         data = pkcs7_padding(list(str.encode(
             f'{api_path}-36cd479b6b5-{request_text}-36cd479b6b5-{msg_digest}')))
@@ -53,7 +53,7 @@ def _download_eapi_json(self, path, video_id, query_body, headers={}, **kwargs):
             '__csrf': '',
             'os': 'pc',
             'channel': 'undefined',
-            'requestId': f'{int(time.time() * 1000)}_{randint(0, 1000):04}',
+            'requestId': f'{int(time.time() * 1000)}_{random.randint(0, 1000):04}',
             **traverse_obj(self._get_cookies(self._API_BASE), {
                 'MUSIC_U': ('MUSIC_U', {lambda i: i.value}),
             })
diff --git a/yt_dlp/extractor/niconico.py b/yt_dlp/extractor/niconico.py
index 6a4624602..b04ce9615 100644
--- a/yt_dlp/extractor/niconico.py
+++ b/yt_dlp/extractor/niconico.py
@@ -1,11 +1,10 @@
-import datetime
+import datetime as dt
 import functools
 import itertools
 import json
 import re
 import time
-
-from urllib.parse import urlparse
+import urllib.parse
 
 from .common import InfoExtractor, SearchInfoExtractor
 from ..networking import Request
@@ -820,12 +819,12 @@ class NicovideoSearchDateIE(NicovideoSearchBaseIE, SearchInfoExtractor):
         'playlist_mincount': 1610,
     }]
 
-    _START_DATE = datetime.date(2007, 1, 1)
+    _START_DATE = dt.date(2007, 1, 1)
     _RESULTS_PER_PAGE = 32
     _MAX_PAGES = 50
 
     def _entries(self, url, item_id, start_date=None, end_date=None):
-        start_date, end_date = start_date or self._START_DATE, end_date or datetime.datetime.now().date()
+        start_date, end_date = start_date or self._START_DATE, end_date or dt.datetime.now().date()
 
         # If the last page has a full page of videos, we need to break down the query interval further
         last_page_len = len(list(self._get_entries_for_date(
@@ -957,7 +956,7 @@ def _real_extract(self, url):
             'frontend_id': traverse_obj(embedded_data, ('site', 'frontendId')) or '9',
         })
 
-        hostname = remove_start(urlparse(urlh.url).hostname, 'sp.')
+        hostname = remove_start(urllib.parse.urlparse(urlh.url).hostname, 'sp.')
         latency = try_get(self._configuration_arg('latency'), lambda x: x[0])
         if latency not in self._KNOWN_LATENCY:
             latency = 'high'
diff --git a/yt_dlp/extractor/panopto.py b/yt_dlp/extractor/panopto.py
index ddea32d70..63c5fd68f 100644
--- a/yt_dlp/extractor/panopto.py
+++ b/yt_dlp/extractor/panopto.py
@@ -1,8 +1,8 @@
 import calendar
-import json
+import datetime as dt
 import functools
-from datetime import datetime, timezone
-from random import random
+import json
+import random
 
 from .common import InfoExtractor
 from ..compat import (
@@ -243,7 +243,7 @@ def _mark_watched(self, base_url, video_id, delivery_info):
         invocation_id = delivery_info.get('InvocationId')
         stream_id = traverse_obj(delivery_info, ('Delivery', 'Streams', ..., 'PublicID'), get_all=False, expected_type=str)
         if invocation_id and stream_id and duration:
-            timestamp_str = f'/Date({calendar.timegm(datetime.now(timezone.utc).timetuple())}000)/'
+            timestamp_str = f'/Date({calendar.timegm(dt.datetime.now(dt.timezone.utc).timetuple())}000)/'
             data = {
                 'streamRequests': [
                     {
@@ -415,7 +415,7 @@ def _real_extract(self, url):
             'cast': traverse_obj(delivery, ('Contributors', ..., 'DisplayName'), expected_type=lambda x: x or None),
             'timestamp': session_start_time - 11640000000 if session_start_time else None,
             'duration': delivery.get('Duration'),
-            'thumbnail': base_url + f'/Services/FrameGrabber.svc/FrameRedirect?objectId={video_id}&mode=Delivery&random={random()}',
+            'thumbnail': base_url + f'/Services/FrameGrabber.svc/FrameRedirect?objectId={video_id}&mode=Delivery&random={random.random()}',
             'average_rating': delivery.get('AverageRating'),
             'chapters': self._extract_chapters(timestamps),
             'uploader': delivery.get('OwnerDisplayName') or None,
diff --git a/yt_dlp/extractor/polsatgo.py b/yt_dlp/extractor/polsatgo.py
index 1524a1fb9..1cebb365e 100644
--- a/yt_dlp/extractor/polsatgo.py
+++ b/yt_dlp/extractor/polsatgo.py
@@ -1,5 +1,5 @@
-from uuid import uuid4
 import json
+import uuid
 
 from .common import InfoExtractor
 from ..utils import (
@@ -51,7 +51,7 @@ def _real_extract(self, url):
         }
 
     def _call_api(self, endpoint, media_id, method, params):
-        rand_uuid = str(uuid4())
+        rand_uuid = str(uuid.uuid4())
         res = self._download_json(
             f'https://b2c-mobile.redefine.pl/rpc/{endpoint}/', media_id,
             note=f'Downloading {method} JSON metadata',
diff --git a/yt_dlp/extractor/pr0gramm.py b/yt_dlp/extractor/pr0gramm.py
index 66f8a5f44..3e0ccba17 100644
--- a/yt_dlp/extractor/pr0gramm.py
+++ b/yt_dlp/extractor/pr0gramm.py
@@ -1,5 +1,6 @@
+import datetime as dt
 import json
-from urllib.parse import unquote
+import urllib.parse
 
 from .common import InfoExtractor
 from ..compat import functools
@@ -114,7 +115,7 @@ def _maximum_flags(self):
             cookies = self._get_cookies(self.BASE_URL)
             if 'me' not in cookies:
                 self._download_webpage(self.BASE_URL, None, 'Refreshing verification information')
-            if traverse_obj(cookies, ('me', {lambda x: x.value}, {unquote}, {json.loads}, 'verified')):
+            if traverse_obj(cookies, ('me', {lambda x: x.value}, {urllib.parse.unquote}, {json.loads}, 'verified')):
                 flags |= 0b00110
 
         return flags
@@ -196,6 +197,7 @@ def _real_extract(self, url):
                 'like_count': ('up', {int}),
                 'dislike_count': ('down', {int}),
                 'timestamp': ('created', {int}),
+                'upload_date': ('created', {int}, {dt.date.fromtimestamp}, {lambda x: x.strftime('%Y%m%d')}),
                 'thumbnail': ('thumb', {lambda x: urljoin('https://thumb.pr0gramm.com', x)})
             }),
         }
diff --git a/yt_dlp/extractor/prosiebensat1.py b/yt_dlp/extractor/prosiebensat1.py
index 46e2e8a8f..4c33baec5 100644
--- a/yt_dlp/extractor/prosiebensat1.py
+++ b/yt_dlp/extractor/prosiebensat1.py
@@ -1,6 +1,6 @@
+import hashlib
 import re
 
-from hashlib import sha1
 from .common import InfoExtractor
 from ..compat import compat_str
 from ..utils import (
@@ -42,7 +42,7 @@ def _extract_video_info(self, url, clip_id):
                 'Downloading protocols JSON',
                 headers=self.geo_verification_headers(), query={
                     'access_id': self._ACCESS_ID,
-                    'client_token': sha1((raw_ct).encode()).hexdigest(),
+                    'client_token': hashlib.sha1((raw_ct).encode()).hexdigest(),
                     'video_id': clip_id,
                 }, fatal=False, expected_status=(403,)) or {}
             error = protocols.get('error') or {}
@@ -53,7 +53,7 @@ def _extract_video_info(self, url, clip_id):
                 urls = (self._download_json(
                     self._V4_BASE_URL + 'urls', clip_id, 'Downloading urls JSON', query={
                         'access_id': self._ACCESS_ID,
-                        'client_token': sha1((raw_ct + server_token + self._SUPPORTED_PROTOCOLS).encode()).hexdigest(),
+                        'client_token': hashlib.sha1((raw_ct + server_token + self._SUPPORTED_PROTOCOLS).encode()).hexdigest(),
                         'protocols': self._SUPPORTED_PROTOCOLS,
                         'server_token': server_token,
                         'video_id': clip_id,
@@ -77,7 +77,7 @@ def _extract_video_info(self, url, clip_id):
         if not formats:
             source_ids = [compat_str(source['id']) for source in video['sources']]
 
-            client_id = self._SALT[:2] + sha1(''.join([clip_id, self._SALT, self._TOKEN, client_location, self._SALT, self._CLIENT_NAME]).encode('utf-8')).hexdigest()
+            client_id = self._SALT[:2] + hashlib.sha1(''.join([clip_id, self._SALT, self._TOKEN, client_location, self._SALT, self._CLIENT_NAME]).encode('utf-8')).hexdigest()
 
             sources = self._download_json(
                 'http://vas.sim-technik.de/vas/live/v2/videos/%s/sources' % clip_id,
@@ -96,7 +96,7 @@ def fix_bitrate(bitrate):
                 return (bitrate // 1000) if bitrate % 1000 == 0 else bitrate
 
             for source_id in source_ids:
-                client_id = self._SALT[:2] + sha1(''.join([self._SALT, clip_id, self._TOKEN, server_id, client_location, source_id, self._SALT, self._CLIENT_NAME]).encode('utf-8')).hexdigest()
+                client_id = self._SALT[:2] + hashlib.sha1(''.join([self._SALT, clip_id, self._TOKEN, server_id, client_location, source_id, self._SALT, self._CLIENT_NAME]).encode('utf-8')).hexdigest()
                 urls = self._download_json(
                     'http://vas.sim-technik.de/vas/live/v2/videos/%s/sources/url' % clip_id,
                     clip_id, 'Downloading urls JSON', fatal=False, query={
diff --git a/yt_dlp/extractor/radiokapital.py b/yt_dlp/extractor/radiokapital.py
index 8f9737ac3..5d7d3ddeb 100644
--- a/yt_dlp/extractor/radiokapital.py
+++ b/yt_dlp/extractor/radiokapital.py
@@ -1,18 +1,14 @@
-from .common import InfoExtractor
-from ..utils import (
-    clean_html,
-    traverse_obj,
-    unescapeHTML,
-)
-
 import itertools
-from urllib.parse import urlencode
+import urllib.parse
+
+from .common import InfoExtractor
+from ..utils import clean_html, traverse_obj, unescapeHTML
 
 
 class RadioKapitalBaseIE(InfoExtractor):
     def _call_api(self, resource, video_id, note='Downloading JSON metadata', qs={}):
         return self._download_json(
-            f'https://www.radiokapital.pl/wp-json/kapital/v1/{resource}?{urlencode(qs)}',
+            f'https://www.radiokapital.pl/wp-json/kapital/v1/{resource}?{urllib.parse.urlencode(qs)}',
             video_id, note=note)
 
     def _parse_episode(self, data):
diff --git a/yt_dlp/extractor/rokfin.py b/yt_dlp/extractor/rokfin.py
index 5099f3ae4..3bc5f3cab 100644
--- a/yt_dlp/extractor/rokfin.py
+++ b/yt_dlp/extractor/rokfin.py
@@ -1,8 +1,8 @@
+import datetime as dt
 import itertools
 import json
 import re
 import urllib.parse
-from datetime import datetime
 
 from .common import InfoExtractor, SearchInfoExtractor
 from ..utils import (
@@ -156,7 +156,7 @@ def _real_extract(self, url):
                 self.raise_login_required('This video is only available to premium users', True, method='cookies')
             elif scheduled:
                 self.raise_no_formats(
-                    f'Stream is offline; scheduled for {datetime.fromtimestamp(scheduled).strftime("%Y-%m-%d %H:%M:%S")}',
+                    f'Stream is offline; scheduled for {dt.datetime.fromtimestamp(scheduled).strftime("%Y-%m-%d %H:%M:%S")}',
                     video_id=video_id, expected=True)
 
         uploader = traverse_obj(metadata, ('createdBy', 'username'), ('creator', 'username'))
diff --git a/yt_dlp/extractor/sejmpl.py b/yt_dlp/extractor/sejmpl.py
index 29cb0152a..eb433d2ac 100644
--- a/yt_dlp/extractor/sejmpl.py
+++ b/yt_dlp/extractor/sejmpl.py
@@ -1,4 +1,4 @@
-import datetime
+import datetime as dt
 
 from .common import InfoExtractor
 from .redge import RedCDNLivxIE
@@ -13,16 +13,16 @@
 
 
 def is_dst(date):
-    last_march = datetime.datetime(date.year, 3, 31)
-    last_october = datetime.datetime(date.year, 10, 31)
-    last_sunday_march = last_march - datetime.timedelta(days=last_march.isoweekday() % 7)
-    last_sunday_october = last_october - datetime.timedelta(days=last_october.isoweekday() % 7)
+    last_march = dt.datetime(date.year, 3, 31)
+    last_october = dt.datetime(date.year, 10, 31)
+    last_sunday_march = last_march - dt.timedelta(days=last_march.isoweekday() % 7)
+    last_sunday_october = last_october - dt.timedelta(days=last_october.isoweekday() % 7)
     return last_sunday_march.replace(hour=2) <= date <= last_sunday_october.replace(hour=3)
 
 
 def rfc3339_to_atende(date):
-    date = datetime.datetime.fromisoformat(date)
-    date = date + datetime.timedelta(hours=1 if is_dst(date) else 0)
+    date = dt.datetime.fromisoformat(date)
+    date = date + dt.timedelta(hours=1 if is_dst(date) else 0)
     return int((date.timestamp() - 978307200) * 1000)
 
 
diff --git a/yt_dlp/extractor/sonyliv.py b/yt_dlp/extractor/sonyliv.py
index a6da44525..7c914acbe 100644
--- a/yt_dlp/extractor/sonyliv.py
+++ b/yt_dlp/extractor/sonyliv.py
@@ -1,4 +1,4 @@
-import datetime
+import datetime as dt
 import itertools
 import json
 import math
@@ -94,7 +94,7 @@ def _perform_login(self, username, password):
                 'mobileNumber': username,
                 'channelPartnerID': 'MSMIND',
                 'country': 'IN',
-                'timestamp': datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%MZ'),
+                'timestamp': dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%MZ'),
                 'otpSize': 6,
                 'loginType': 'REGISTERORSIGNIN',
                 'isMobileMandatory': True,
@@ -111,7 +111,7 @@ def _perform_login(self, username, password):
                 'otp': self._get_tfa_info('OTP'),
                 'dmaId': 'IN',
                 'ageConfirmation': True,
-                'timestamp': datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%MZ'),
+                'timestamp': dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%MZ'),
                 'isMobileMandatory': True,
             }).encode())
         if otp_verify_json['resultCode'] == 'KO':
diff --git a/yt_dlp/extractor/soundcloud.py b/yt_dlp/extractor/soundcloud.py
index a7c2afd49..c9ed645eb 100644
--- a/yt_dlp/extractor/soundcloud.py
+++ b/yt_dlp/extractor/soundcloud.py
@@ -1,30 +1,27 @@
 import itertools
-import re
 import json
-# import random
+import re
 
-from .common import (
-    InfoExtractor,
-    SearchInfoExtractor
-)
+from .common import InfoExtractor, SearchInfoExtractor
 from ..compat import compat_str
-from ..networking import HEADRequest, Request
+from ..networking import HEADRequest
 from ..networking.exceptions import HTTPError
 from ..utils import (
-    error_to_compat_str,
+    KNOWN_EXTENSIONS,
     ExtractorError,
+    error_to_compat_str,
     float_or_none,
     int_or_none,
-    KNOWN_EXTENSIONS,
     mimetype2ext,
     parse_qs,
     str_or_none,
-    try_get,
+    try_call,
     unified_timestamp,
     update_url_query,
     url_or_none,
     urlhandle_detect_ext,
 )
+from ..utils.traversal import traverse_obj
 
 
 class SoundcloudEmbedIE(InfoExtractor):
@@ -54,7 +51,6 @@ class SoundcloudBaseIE(InfoExtractor):
     _API_AUTH_QUERY_TEMPLATE = '?client_id=%s'
     _API_AUTH_URL_PW = 'https://api-auth.soundcloud.com/web-auth/sign-in/password%s'
     _API_VERIFY_AUTH_TOKEN = 'https://api-auth.soundcloud.com/connect/session%s'
-    _access_token = None
     _HEADERS = {}
 
     _IMAGE_REPL_RE = r'-([0-9a-z]+)\.jpg'
@@ -112,21 +108,31 @@ def _download_json(self, *args, **kwargs):
     def _initialize_pre_login(self):
         self._CLIENT_ID = self.cache.load('soundcloud', 'client_id') or 'a3e059563d7fd3372b49b37f00a00bcf'
 
+    def _verify_oauth_token(self, token):
+        if self._request_webpage(
+                self._API_VERIFY_AUTH_TOKEN % (self._API_AUTH_QUERY_TEMPLATE % self._CLIENT_ID),
+                None, note='Verifying login token...', fatal=False,
+                data=json.dumps({'session': {'access_token': token}}).encode()):
+            self._HEADERS['Authorization'] = f'OAuth {token}'
+            self.report_login()
+        else:
+            self.report_warning('Provided authorization token is invalid. Continuing as guest')
+
+    def _real_initialize(self):
+        if self._HEADERS:
+            return
+        if token := try_call(lambda: self._get_cookies(self._BASE_URL)['oauth_token'].value):
+            self._verify_oauth_token(token)
+
     def _perform_login(self, username, password):
         if username != 'oauth':
-            self.report_warning(
+            raise ExtractorError(
                 'Login using username and password is not currently supported. '
-                'Use "--username oauth --password <oauth_token>" to login using an oauth token')
-        self._access_token = password
-        query = self._API_AUTH_QUERY_TEMPLATE % self._CLIENT_ID
-        payload = {'session': {'access_token': self._access_token}}
-        token_verification = Request(self._API_VERIFY_AUTH_TOKEN % query, json.dumps(payload).encode('utf-8'))
-        response = self._download_json(token_verification, None, note='Verifying login token...', fatal=False)
-        if response is not False:
-            self._HEADERS = {'Authorization': 'OAuth ' + self._access_token}
-            self.report_login()
-        else:
-            self.report_warning('Provided authorization token seems to be invalid. Continue as guest')
+                'Use "--username oauth --password <oauth_token>" to login using an oauth token, '
+                f'or else {self._login_hint(method="cookies")}', expected=True)
+        if self._HEADERS:
+            return
+        self._verify_oauth_token(password)
 
         r'''
         def genDevId():
@@ -147,14 +153,17 @@ def genNumBlock():
             'user_agent': self._USER_AGENT
         }
 
-        query = self._API_AUTH_QUERY_TEMPLATE % self._CLIENT_ID
-        login = sanitized_Request(self._API_AUTH_URL_PW % query, json.dumps(payload).encode('utf-8'))
-        response = self._download_json(login, None)
-        self._access_token = response.get('session').get('access_token')
-        if not self._access_token:
-            self.report_warning('Unable to get access token, login may has failed')
-        else:
-            self._HEADERS = {'Authorization': 'OAuth ' + self._access_token}
+        response = self._download_json(
+            self._API_AUTH_URL_PW % (self._API_AUTH_QUERY_TEMPLATE % self._CLIENT_ID),
+            None, note='Verifying login token...', fatal=False,
+            data=json.dumps(payload).encode())
+
+        if token := traverse_obj(response, ('session', 'access_token', {str})):
+            self._HEADERS['Authorization'] = f'OAuth {token}'
+            self.report_login()
+            return
+
+        raise ExtractorError('Unable to get access token, login may have failed', expected=True)
         '''
 
     # signature generation
@@ -217,6 +226,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 +243,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'])
@@ -256,16 +270,25 @@ def add_format(f, protocol, is_preview=False):
             formats.append(f)
 
         # New API
-        transcodings = try_get(
-            info, lambda x: x['media']['transcodings'], list) or []
-        for t in transcodings:
-            if not isinstance(t, dict):
-                continue
-            format_url = url_or_none(t.get('url'))
-            if not format_url:
-                continue
-            stream = None if extract_flat else self._download_json(
-                format_url, track_id, query=query, fatal=False, headers=self._HEADERS)
+        for t in traverse_obj(info, ('media', 'transcodings', lambda _, v: url_or_none(v['url']))):
+            if extract_flat:
+                break
+            format_url = t['url']
+            stream = None
+
+            for retry in self.RetryManager(fatal=False):
+                try:
+                    stream = self._download_json(format_url, track_id, query=query, headers=self._HEADERS)
+                except ExtractorError as e:
+                    if isinstance(e.cause, HTTPError) and e.cause.status == 429:
+                        self.report_warning(
+                            'You have reached the API rate limit, which is ~600 requests per '
+                            '10 minutes. Use the --extractor-retries and --retry-sleep options '
+                            'to configure an appropriate retry count and wait time', only_once=True)
+                        retry.error = e.cause
+                    else:
+                        self.report_warning(e.msg)
+
             if not isinstance(stream, dict):
                 continue
             stream_url = url_or_none(stream.get('url'))
diff --git a/yt_dlp/extractor/telewebion.py b/yt_dlp/extractor/telewebion.py
index 9378ed021..5fdcddd8b 100644
--- a/yt_dlp/extractor/telewebion.py
+++ b/yt_dlp/extractor/telewebion.py
@@ -1,8 +1,7 @@
 from __future__ import annotations
-
+import functools
 import json
-from functools import partial
-from textwrap import dedent
+import textwrap
 
 from .common import InfoExtractor
 from ..utils import ExtractorError, format_field, int_or_none, parse_iso8601
@@ -10,7 +9,7 @@
 
 
 def _fmt_url(url):
-    return partial(format_field, template=url, default=None)
+    return functools.partial(format_field, template=url, default=None)
 
 
 class TelewebionIE(InfoExtractor):
@@ -88,7 +87,7 @@ def _real_extract(self, url):
         if not video_id.startswith('0x'):
             video_id = hex(int(video_id))
 
-        episode_data = self._call_graphql_api('getEpisodeDetail', video_id, dedent('''
+        episode_data = self._call_graphql_api('getEpisodeDetail', video_id, textwrap.dedent('''
             queryEpisode(filter: {EpisodeID: $EpisodeId}, first: 1) {
               title
               program {
@@ -127,7 +126,7 @@ def _real_extract(self, url):
             'formats': (
                 'channel', 'descriptor', {str},
                 {_fmt_url(f'https://cdna.telewebion.com/%s/episode/{video_id}/playlist.m3u8')},
-                {partial(self._extract_m3u8_formats, video_id=video_id, ext='mp4', m3u8_id='hls')}),
+                {functools.partial(self._extract_m3u8_formats, video_id=video_id, ext='mp4', m3u8_id='hls')}),
         }))
         info_dict['id'] = video_id
         return info_dict
diff --git a/yt_dlp/extractor/tenplay.py b/yt_dlp/extractor/tenplay.py
index a98275d86..11cc5705e 100644
--- a/yt_dlp/extractor/tenplay.py
+++ b/yt_dlp/extractor/tenplay.py
@@ -1,7 +1,7 @@
 import base64
+import datetime as dt
 import functools
 import itertools
-from datetime import datetime
 
 from .common import InfoExtractor
 from ..networking import HEADRequest
@@ -70,7 +70,7 @@ def _get_bearer_token(self, video_id):
         username, password = self._get_login_info()
         if username is None or password is None:
             self.raise_login_required('Your 10play account\'s details must be provided with --username and --password.')
-        _timestamp = datetime.now().strftime('%Y%m%d000000')
+        _timestamp = dt.datetime.now().strftime('%Y%m%d000000')
         _auth_header = base64.b64encode(_timestamp.encode('ascii')).decode('ascii')
         data = self._download_json('https://10play.com.au/api/user/auth', video_id, 'Getting bearer token', headers={
             'X-Network-Ten-Auth': _auth_header,
diff --git a/yt_dlp/extractor/wistia.py b/yt_dlp/extractor/wistia.py
index bce5e8326..f2256fdc6 100644
--- a/yt_dlp/extractor/wistia.py
+++ b/yt_dlp/extractor/wistia.py
@@ -1,6 +1,6 @@
+import base64
 import re
 import urllib.parse
-from base64 import b64decode
 
 from .common import InfoExtractor
 from ..networking import HEADRequest
@@ -371,7 +371,7 @@ def _real_extract(self, url):
             webpage = self._download_webpage(f'https://fast.wistia.net/embed/channel/{channel_id}', channel_id)
             data = self._parse_json(
                 self._search_regex(r'wchanneljsonp-%s\'\]\s*=[^\"]*\"([A-Za-z0-9=/]*)' % channel_id, webpage, 'jsonp', channel_id),
-                channel_id, transform_source=lambda x: urllib.parse.unquote_plus(b64decode(x).decode('utf-8')))
+                channel_id, transform_source=lambda x: urllib.parse.unquote_plus(base64.b64decode(x).decode('utf-8')))
 
         # XXX: can there be more than one series?
         series = traverse_obj(data, ('series', 0), default={})
diff --git a/yt_dlp/extractor/youtube.py b/yt_dlp/extractor/youtube.py
index 589cd9b59..e553fff9f 100644
--- a/yt_dlp/extractor/youtube.py
+++ b/yt_dlp/extractor/youtube.py
@@ -2,7 +2,7 @@
 import calendar
 import collections
 import copy
-import datetime
+import datetime as dt
 import enum
 import hashlib
 import itertools
@@ -33,6 +33,7 @@
     clean_html,
     datetime_from_str,
     dict_get,
+    filesize_from_tbr,
     filter_dict,
     float_or_none,
     format_field,
@@ -55,6 +56,7 @@
     str_to_int,
     strftime_or_none,
     traverse_obj,
+    try_call,
     try_get,
     unescapeHTML,
     unified_strdate,
@@ -922,10 +924,10 @@ def extract_relative_time(relative_time_text):
     def _parse_time_text(self, text):
         if not text:
             return
-        dt = self.extract_relative_time(text)
+        dt_ = self.extract_relative_time(text)
         timestamp = None
-        if isinstance(dt, datetime.datetime):
-            timestamp = calendar.timegm(dt.timetuple())
+        if isinstance(dt_, dt.datetime):
+            timestamp = calendar.timegm(dt_.timetuple())
 
         if timestamp is None:
             timestamp = (
@@ -3602,8 +3604,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,16 +3836,17 @@ 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
                 else -1)
+            format_duration = traverse_obj(fmt, ('approxDurationMs', {lambda x: float_or_none(x, 1000)}))
             # Some formats may have much smaller duration than others (possibly damaged during encoding)
             # E.g. 2-nOtRESiUc Ref: https://github.com/yt-dlp/yt-dlp/issues/2823
             # Make sure to avoid false positives with small duration differences.
             # E.g. __2ABJjxzNo, ySuUZEjARPY
-            is_damaged = try_get(fmt, lambda x: float(x['approxDurationMs']) / duration < 500)
+            is_damaged = try_call(lambda: format_duration < duration // 2)
             if is_damaged:
                 self.report_warning(
                     f'{video_id}: Some formats are possibly damaged. They will be deprioritized', only_once=True)
@@ -3873,6 +3876,7 @@ def build_fragments(f):
                 'quality': q(quality) - bool(fmt.get('isDrc')) / 2,
                 'has_drm': bool(fmt.get('drmFamilies')),
                 'tbr': tbr,
+                'filesize_approx': filesize_from_tbr(tbr, format_duration),
                 'url': fmt_url,
                 'width': int_or_none(fmt.get('width')),
                 'language': join_nonempty(audio_track.get('id', '').split('.')[0],
@@ -4564,7 +4568,7 @@ def process_language(container, base_url, lang_code, sub_name, query):
 
         if upload_date and live_status not in ('is_live', 'post_live', 'is_upcoming'):
             # Newly uploaded videos' HLS formats are potentially problematic and need to be checked
-            upload_datetime = datetime_from_str(upload_date).replace(tzinfo=datetime.timezone.utc)
+            upload_datetime = datetime_from_str(upload_date).replace(tzinfo=dt.timezone.utc)
             if upload_datetime >= datetime_from_str('today-2days'):
                 for fmt in info['formats']:
                     if fmt.get('protocol') == 'm3u8_native':
diff --git a/yt_dlp/extractor/zattoo.py b/yt_dlp/extractor/zattoo.py
index 6bd9ea064..5cc9c5f7a 100644
--- a/yt_dlp/extractor/zattoo.py
+++ b/yt_dlp/extractor/zattoo.py
@@ -1,5 +1,5 @@
 import re
-from uuid import uuid4
+import uuid
 
 from .common import InfoExtractor
 from ..compat import compat_str
@@ -53,7 +53,7 @@ def _initialize_pre_login(self):
         self._request_webpage(
             '%s/zapi/v3/session/hello' % self._host_url(), None,
             'Opening session', data=urlencode_postdata({
-                'uuid': compat_str(uuid4()),
+                'uuid': compat_str(uuid.uuid4()),
                 'lang': 'en',
                 'app_version': '1.8.2',
                 'format': 'json',
diff --git a/yt_dlp/utils/_utils.py b/yt_dlp/utils/_utils.py
index 9efeb6a1c..dec514674 100644
--- a/yt_dlp/utils/_utils.py
+++ b/yt_dlp/utils/_utils.py
@@ -5,7 +5,7 @@
 import collections
 import collections.abc
 import contextlib
-import datetime
+import datetime as dt
 import email.header
 import email.utils
 import errno
@@ -1150,14 +1150,14 @@ def extract_timezone(date_str):
         timezone = TIMEZONE_NAMES.get(m and m.group('tz').strip())
         if timezone is not None:
             date_str = date_str[:-len(m.group('tz'))]
-        timezone = datetime.timedelta(hours=timezone or 0)
+        timezone = dt.timedelta(hours=timezone or 0)
     else:
         date_str = date_str[:-len(m.group('tz'))]
         if not m.group('sign'):
-            timezone = datetime.timedelta()
+            timezone = dt.timedelta()
         else:
             sign = 1 if m.group('sign') == '+' else -1
-            timezone = datetime.timedelta(
+            timezone = dt.timedelta(
                 hours=sign * int(m.group('hours')),
                 minutes=sign * int(m.group('minutes')))
     return timezone, date_str
@@ -1176,8 +1176,8 @@ def parse_iso8601(date_str, delimiter='T', timezone=None):
 
     with contextlib.suppress(ValueError):
         date_format = f'%Y-%m-%d{delimiter}%H:%M:%S'
-        dt = datetime.datetime.strptime(date_str, date_format) - timezone
-        return calendar.timegm(dt.timetuple())
+        dt_ = dt.datetime.strptime(date_str, date_format) - timezone
+        return calendar.timegm(dt_.timetuple())
 
 
 def date_formats(day_first=True):
@@ -1198,12 +1198,12 @@ def unified_strdate(date_str, day_first=True):
 
     for expression in date_formats(day_first):
         with contextlib.suppress(ValueError):
-            upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
+            upload_date = dt.datetime.strptime(date_str, expression).strftime('%Y%m%d')
     if upload_date is None:
         timetuple = email.utils.parsedate_tz(date_str)
         if timetuple:
             with contextlib.suppress(ValueError):
-                upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d')
+                upload_date = dt.datetime(*timetuple[:6]).strftime('%Y%m%d')
     if upload_date is not None:
         return str(upload_date)
 
@@ -1233,8 +1233,8 @@ def unified_timestamp(date_str, day_first=True):
 
     for expression in date_formats(day_first):
         with contextlib.suppress(ValueError):
-            dt = datetime.datetime.strptime(date_str, expression) - timezone + datetime.timedelta(hours=pm_delta)
-            return calendar.timegm(dt.timetuple())
+            dt_ = dt.datetime.strptime(date_str, expression) - timezone + dt.timedelta(hours=pm_delta)
+            return calendar.timegm(dt_.timetuple())
 
     timetuple = email.utils.parsedate_tz(date_str)
     if timetuple:
@@ -1272,11 +1272,11 @@ def datetime_from_str(date_str, precision='auto', format='%Y%m%d'):
     if precision == 'auto':
         auto_precision = True
         precision = 'microsecond'
-    today = datetime_round(datetime.datetime.now(datetime.timezone.utc), precision)
+    today = datetime_round(dt.datetime.now(dt.timezone.utc), precision)
     if date_str in ('now', 'today'):
         return today
     if date_str == 'yesterday':
-        return today - datetime.timedelta(days=1)
+        return today - dt.timedelta(days=1)
     match = re.match(
         r'(?P<start>.+)(?P<sign>[+-])(?P<time>\d+)(?P<unit>microsecond|second|minute|hour|day|week|month|year)s?',
         date_str)
@@ -1291,13 +1291,13 @@ def datetime_from_str(date_str, precision='auto', format='%Y%m%d'):
             if unit == 'week':
                 unit = 'day'
                 time *= 7
-            delta = datetime.timedelta(**{unit + 's': time})
+            delta = dt.timedelta(**{unit + 's': time})
             new_date = start_time + delta
         if auto_precision:
             return datetime_round(new_date, unit)
         return new_date
 
-    return datetime_round(datetime.datetime.strptime(date_str, format), precision)
+    return datetime_round(dt.datetime.strptime(date_str, format), precision)
 
 
 def date_from_str(date_str, format='%Y%m%d', strict=False):
@@ -1312,21 +1312,21 @@ def date_from_str(date_str, format='%Y%m%d', strict=False):
     return datetime_from_str(date_str, precision='microsecond', format=format).date()
 
 
-def datetime_add_months(dt, months):
+def datetime_add_months(dt_, months):
     """Increment/Decrement a datetime object by months."""
-    month = dt.month + months - 1
-    year = dt.year + month // 12
+    month = dt_.month + months - 1
+    year = dt_.year + month // 12
     month = month % 12 + 1
-    day = min(dt.day, calendar.monthrange(year, month)[1])
-    return dt.replace(year, month, day)
+    day = min(dt_.day, calendar.monthrange(year, month)[1])
+    return dt_.replace(year, month, day)
 
 
-def datetime_round(dt, precision='day'):
+def datetime_round(dt_, precision='day'):
     """
     Round a datetime object's time to a specific precision
     """
     if precision == 'microsecond':
-        return dt
+        return dt_
 
     unit_seconds = {
         'day': 86400,
@@ -1335,8 +1335,8 @@ def datetime_round(dt, precision='day'):
         'second': 1,
     }
     roundto = lambda x, n: ((x + n / 2) // n) * n
-    timestamp = roundto(calendar.timegm(dt.timetuple()), unit_seconds[precision])
-    return datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc)
+    timestamp = roundto(calendar.timegm(dt_.timetuple()), unit_seconds[precision])
+    return dt.datetime.fromtimestamp(timestamp, dt.timezone.utc)
 
 
 def hyphenate_date(date_str):
@@ -1357,11 +1357,11 @@ def __init__(self, start=None, end=None):
         if start is not None:
             self.start = date_from_str(start, strict=True)
         else:
-            self.start = datetime.datetime.min.date()
+            self.start = dt.datetime.min.date()
         if end is not None:
             self.end = date_from_str(end, strict=True)
         else:
-            self.end = datetime.datetime.max.date()
+            self.end = dt.datetime.max.date()
         if self.start > self.end:
             raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
 
@@ -1372,7 +1372,7 @@ def day(cls, day):
 
     def __contains__(self, date):
         """Check if the date is in the range"""
-        if not isinstance(date, datetime.date):
+        if not isinstance(date, dt.date):
             date = date_from_str(date)
         return self.start <= date <= self.end
 
@@ -1996,12 +1996,12 @@ def strftime_or_none(timestamp, date_format='%Y%m%d', default=None):
         if isinstance(timestamp, (int, float)):  # unix timestamp
             # Using naive datetime here can break timestamp() in Windows
             # Ref: https://github.com/yt-dlp/yt-dlp/issues/5185, https://github.com/python/cpython/issues/94414
-            # Also, datetime.datetime.fromtimestamp breaks for negative timestamps
+            # Also, dt.datetime.fromtimestamp breaks for negative timestamps
             # Ref: https://github.com/yt-dlp/yt-dlp/issues/6706#issuecomment-1496842642
-            datetime_object = (datetime.datetime.fromtimestamp(0, datetime.timezone.utc)
-                               + datetime.timedelta(seconds=timestamp))
+            datetime_object = (dt.datetime.fromtimestamp(0, dt.timezone.utc)
+                               + dt.timedelta(seconds=timestamp))
         elif isinstance(timestamp, str):  # assume YYYYMMDD
-            datetime_object = datetime.datetime.strptime(timestamp, '%Y%m%d')
+            datetime_object = dt.datetime.strptime(timestamp, '%Y%m%d')
         date_format = re.sub(  # Support %s on windows
             r'(?<!%)(%%)*%s', rf'\g<1>{int(datetime_object.timestamp())}', date_format)
         return datetime_object.strftime(date_format)
@@ -4490,10 +4490,10 @@ def write_xattr(path, key, value):
 
 
 def random_birthday(year_field, month_field, day_field):
-    start_date = datetime.date(1950, 1, 1)
-    end_date = datetime.date(1995, 12, 31)
+    start_date = dt.date(1950, 1, 1)
+    end_date = dt.date(1995, 12, 31)
     offset = random.randint(0, (end_date - start_date).days)
-    random_date = start_date + datetime.timedelta(offset)
+    random_date = start_date + dt.timedelta(offset)
     return {
         year_field: str(random_date.year),
         month_field: str(random_date.month),
@@ -4672,7 +4672,7 @@ def time_seconds(**kwargs):
     """
     Returns TZ-aware time in seconds since the epoch (1970-01-01T00:00:00Z)
     """
-    return time.time() + datetime.timedelta(**kwargs).total_seconds()
+    return time.time() + dt.timedelta(**kwargs).total_seconds()
 
 
 # create a JSON Web Signature (jws) with HS256 algorithm
@@ -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 926a3d0a1..96eb2eddf 100644
--- a/yt_dlp/utils/traversal.py
+++ b/yt_dlp/utils/traversal.py
@@ -1,5 +1,6 @@
 import collections.abc
 import contextlib
+import http.cookies
 import inspect
 import itertools
 import re
@@ -28,7 +29,8 @@ def traverse_obj(
 
     Each of the provided `paths` is tested and the first producing a valid result will be returned.
     The next path will also be tested if the path branched but no results could be found.
-    Supported values for traversal are `Mapping`, `Iterable` and `re.Match`.
+    Supported values for traversal are `Mapping`, `Iterable`, `re.Match`,
+    `xml.etree.ElementTree` (xpath) and `http.cookies.Morsel`.
     Unhelpful values (`{}`, `None`) are treated as the absence of a value and discarded.
 
     The paths will be wrapped in `variadic`, so that `'key'` is conveniently the same as `('key', )`.
@@ -36,8 +38,8 @@ def traverse_obj(
     The keys in the path can be one of:
         - `None`:           Return the current object.
         - `set`:            Requires the only item in the set to be a type or function,
-                            like `{type}`/`{func}`. If a `type`, returns only values
-                            of this type. If a function, returns `func(obj)`.
+                            like `{type}`/`{type, type, ...}/`{func}`. If a `type`, return only
+                            values of this type. If a function, returns `func(obj)`.
         - `str`/`int`:      Return `obj[key]`. For `re.Match`, return `obj.group(key)`.
         - `slice`:          Branch out and return all values in `obj[key]`.
         - `Ellipsis`:       Branch out and return a list of all values.
@@ -48,8 +50,10 @@ def traverse_obj(
                             For `Iterable`s, `key` is the index of the value.
                             For `re.Match`es, `key` is the group number (0 = full match)
                             as well as additionally any group names, if given.
-        - `dict`            Transform the current object and return a matching dict.
+        - `dict`:           Transform the current object and return a matching dict.
                             Read as: `{key: traverse_obj(obj, path) for key, path in dct.items()}`.
+        - `any`-builtin:    Take the first matching object and return it, resetting branching.
+        - `all`-builtin:    Take all matching objects and return them as a list, resetting branching.
 
         `tuple`, `list`, and `dict` all support nested paths and branches.
 
@@ -102,10 +106,10 @@ def apply_key(key, obj, is_last):
             result = obj
 
         elif isinstance(key, set):
-            assert len(key) == 1, 'Set should only be used to wrap a single item'
             item = next(iter(key))
-            if isinstance(item, type):
-                if isinstance(obj, item):
+            if len(key) > 1 or isinstance(item, type):
+                assert all(isinstance(item, type) for item in key)
+                if isinstance(obj, tuple(key)):
                     result = obj
             else:
                 result = try_call(item, args=(obj,))
@@ -117,6 +121,8 @@ def apply_key(key, obj, is_last):
 
         elif key is ...:
             branching = True
+            if isinstance(obj, http.cookies.Morsel):
+                obj = dict(obj, key=obj.key, value=obj.value)
             if isinstance(obj, collections.abc.Mapping):
                 result = obj.values()
             elif is_iterable_like(obj) or isinstance(obj, xml.etree.ElementTree.Element):
@@ -131,6 +137,8 @@ def apply_key(key, obj, is_last):
 
         elif callable(key):
             branching = True
+            if isinstance(obj, http.cookies.Morsel):
+                obj = dict(obj, key=obj.key, value=obj.value)
             if isinstance(obj, collections.abc.Mapping):
                 iter_obj = obj.items()
             elif is_iterable_like(obj) or isinstance(obj, xml.etree.ElementTree.Element):
@@ -157,6 +165,8 @@ def apply_key(key, obj, is_last):
             } or None
 
         elif isinstance(obj, collections.abc.Mapping):
+            if isinstance(obj, http.cookies.Morsel):
+                obj = dict(obj, key=obj.key, value=obj.value)
             result = (try_call(obj.get, args=(key,)) if casesense or try_call(obj.__contains__, args=(key,)) else
                       next((v for k, v in obj.items() if casefold(k) == key), None))
 
@@ -179,7 +189,7 @@ def apply_key(key, obj, is_last):
 
         elif isinstance(obj, xml.etree.ElementTree.Element) and isinstance(key, str):
             xpath, _, special = key.rpartition('/')
-            if not special.startswith('@') and special != 'text()':
+            if not special.startswith('@') and not special.endswith('()'):
                 xpath = key
                 special = None
 
@@ -198,7 +208,7 @@ def apply_specials(element):
                     return try_call(element.attrib.get, args=(special[1:],))
                 if special == 'text()':
                     return element.text
-                assert False, f'apply_specials is missing case for {special!r}'
+                raise SyntaxError(f'apply_specials is missing case for {special!r}')
 
             if xpath:
                 result = list(map(apply_specials, obj.iterfind(xpath)))


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_cookies.py b/test/test_cookies.py
index 5282ef621..bd61f30a6 100644
--- a/test/test_cookies.py
+++ b/test/test_cookies.py
@@ -1,5 +1,5 @@
+import datetime as dt
 import unittest
-from datetime import datetime, timezone
 
 from yt_dlp import cookies
 from yt_dlp.cookies import (
@@ -138,7 +138,7 @@ def test_safari_cookie_parsing(self):
         self.assertEqual(cookie.name, 'foo')
         self.assertEqual(cookie.value, 'test%20%3Bcookie')
         self.assertFalse(cookie.secure)
-        expected_expiration = datetime(2021, 6, 18, 21, 39, 19, tzinfo=timezone.utc)
+        expected_expiration = dt.datetime(2021, 6, 18, 21, 39, 19, tzinfo=dt.timezone.utc)
         self.assertEqual(cookie.expires, int(expected_expiration.timestamp()))
 
     def test_pbkdf2_sha1(self):
diff --git a/test/test_traversal.py b/test/test_traversal.py
index 0b2f3fb5d..ed29d03ad 100644
--- a/test/test_traversal.py
+++ b/test/test_traversal.py
@@ -1,3 +1,4 @@
+import http.cookies
 import re
 import xml.etree.ElementTree
 
@@ -94,6 +95,8 @@ def test_traversal_set(self):
             '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, (..., {str, int})) == [100, 'str'], \
+            'Multiple types 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'], \
@@ -103,7 +106,7 @@ def test_traversal_set(self):
             '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
+        # Sets with length < 1 or > 1 not including only types should raise
         with pytest.raises(Exception):
             traverse_obj(_TEST_DATA, set())
         with pytest.raises(Exception):
@@ -409,3 +412,31 @@ def test_traversal_unbranching(self):
             '`all` should allow further branching'
         assert traverse_obj(_TEST_DATA, [('dict', 'None', 'urls', 'data'), any, ..., 'index']) == [0, 1], \
             '`any` should allow further branching'
+
+    def test_traversal_morsel(self):
+        values = {
+            'expires': 'a',
+            'path': 'b',
+            'comment': 'c',
+            'domain': 'd',
+            'max-age': 'e',
+            'secure': 'f',
+            'httponly': 'g',
+            'version': 'h',
+            'samesite': 'i',
+        }
+        morsel = http.cookies.Morsel()
+        morsel.set('item_key', 'item_value', 'coded_value')
+        morsel.update(values)
+        values['key'] = 'item_key'
+        values['value'] = 'item_value'
+
+        for key, value in values.items():
+            assert traverse_obj(morsel, key) == value, \
+                'Morsel should provide access to all values'
+        assert traverse_obj(morsel, ...) == list(values.values()), \
+            '`...` should yield all values'
+        assert traverse_obj(morsel, lambda k, v: True) == list(values.values()), \
+            'function key should yield all values'
+        assert traverse_obj(morsel, [(None,), any]) == morsel, \
+            'Morsel should not be implicitly changed to dict on usage'