Repository: yt-dlp/yt-dlp
Pull request #9028 (2024-01-19 22:05:29)

[eporner] Add support for AV1 codec
**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

-->

Some videos are available with AV1 codec, that information is not returned in JSON from `xhr/video/{video_id}` but can be extracted from webpage. URL to download is identical as h264 file but with addition of `-av1` before file extension (`.mp4`).

Fixes #


<details open><summary>Add AV1 codec</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))
- [ ] 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>



Patch:
diff --git a/README.md b/README.md
index b6a79667c..7dc3bb2f6 100644
--- a/README.md
+++ b/README.md
@@ -167,7 +167,8 @@ ### Differences in default behavior
 * `--compat-options youtube-dl`: Same as `--compat-options all,-multistreams,-playlist-match-filter,-manifest-filesize-approx`
 * `--compat-options youtube-dlc`: Same as `--compat-options all,-no-live-chat,-no-youtube-channel-redirect,-playlist-match-filter,-manifest-filesize-approx`
 * `--compat-options 2021`: Same as `--compat-options 2022,no-certifi,filename-sanitization,no-youtube-prefer-utc-upload-date`
-* `--compat-options 2022`: Same as `--compat-options playlist-match-filter,no-external-downloader-progress,prefer-legacy-http-handler,manifest-filesize-approx`. Use this to enable all future compat options
+* `--compat-options 2022`: Same as `--compat-options 2023,playlist-match-filter,no-external-downloader-progress`
+* `--compat-options 2023`: Same as `--compat-options prefer-legacy-http-handler,manifest-filesize-approx`. Use this to enable all future compat options
 
 
 # INSTALLATION
diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py
index 8d96498a6..e7d654d0f 100644
--- a/yt_dlp/YoutubeDL.py
+++ b/yt_dlp/YoutubeDL.py
@@ -40,7 +40,6 @@
     NoSupportingHandlers,
     RequestError,
     SSLError,
-    _CompatHTTPError,
     network_exceptions,
 )
 from .plugins import directories as plugin_directories
@@ -2452,7 +2451,7 @@ def selector_function(ctx):
                                 # for extractors with incomplete formats (audio only (soundcloud)
                                 # or video only (imgur)) best/worst will fallback to
                                 # best/worst {video,audio}-only format
-                                matches = formats
+                                matches = list(filter(lambda f: f.get('vcodec') != 'none' or f.get('acodec') != 'none', formats))
                             elif seperate_fallback and not ctx['has_merged_format']:
                                 # for compatibility with youtube-dl when there is no pre-merged format
                                 matches = list(filter(seperate_fallback, formats))
@@ -4110,8 +4109,6 @@ def urlopen(self, req):
                     'SSLV3_ALERT_HANDSHAKE_FAILURE: The server may not support the current cipher list. '
                     'Try using --legacy-server-connect', cause=e) from e
             raise
-        except HTTPError as e:  # TODO: Remove in a future release
-            raise _CompatHTTPError(e) from e
 
     def build_request_director(self, handlers, preferences=None):
         logger = _YDLLogger(self)
diff --git a/yt_dlp/compat/_legacy.py b/yt_dlp/compat/_legacy.py
index 90ccf0f14..7ea5d0812 100644
--- a/yt_dlp/compat/_legacy.py
+++ b/yt_dlp/compat/_legacy.py
@@ -35,6 +35,7 @@
 from ..dependencies import brotli as compat_brotli  # noqa: F401
 from ..dependencies import websockets as compat_websockets  # noqa: F401
 from ..dependencies.Cryptodome import AES as compat_pycrypto_AES  # noqa: F401
+from ..networking.exceptions import HTTPError as compat_HTTPError  # noqa: F401
 
 passthrough_module(__name__, '...utils', ('WINDOWS_VT_MODE', 'windows_enable_vt_mode'))
 
@@ -70,7 +71,6 @@ def compat_setenv(key, value, env=os.environ):
 compat_HTMLParser = compat_html_parser_HTMLParser = html.parser.HTMLParser
 compat_http_client = http.client
 compat_http_server = http.server
-compat_HTTPError = urllib.error.HTTPError
 compat_input = input
 compat_integer_types = (int, )
 compat_itertools_count = itertools.count
@@ -88,7 +88,7 @@ def compat_setenv(key, value, env=os.environ):
 compat_subprocess_get_DEVNULL = lambda: subprocess.DEVNULL
 compat_tokenize_tokenize = tokenize.tokenize
 compat_urllib_error = urllib.error
-compat_urllib_HTTPError = urllib.error.HTTPError
+compat_urllib_HTTPError = compat_HTTPError
 compat_urllib_parse = urllib.parse
 compat_urllib_parse_parse_qs = urllib.parse.parse_qs
 compat_urllib_parse_quote = urllib.parse.quote
diff --git a/yt_dlp/cookies.py b/yt_dlp/cookies.py
index eac033e39..deb2e35f2 100644
--- a/yt_dlp/cookies.py
+++ b/yt_dlp/cookies.py
@@ -1,6 +1,7 @@
 import base64
 import collections
 import contextlib
+import glob
 import http.cookiejar
 import http.cookies
 import io
@@ -23,7 +24,8 @@
     aes_gcm_decrypt_and_verify_bytes,
     unpad_pkcs7,
 )
-from .compat import functools
+from .compat import functools  # isort: split
+from .compat import compat_os_name
 from .dependencies import (
     _SECRETSTORAGE_UNAVAILABLE_REASON,
     secretstorage,
@@ -31,6 +33,7 @@
 )
 from .minicurses import MultilinePrinter, QuietMultilinePrinter
 from .utils import (
+    DownloadError,
     Popen,
     error_to_str,
     expand_path,
@@ -122,13 +125,14 @@ def _extract_firefox_cookies(profile, container, logger):
         return YoutubeDLCookieJar()
 
     if profile is None:
-        search_root = _firefox_browser_dir()
+        search_roots = list(_firefox_browser_dirs())
     elif _is_path(profile):
-        search_root = profile
+        search_roots = [profile]
     else:
-        search_root = os.path.join(_firefox_browser_dir(), profile)
+        search_roots = [os.path.join(path, profile) for path in _firefox_browser_dirs()]
+    search_root = ', '.join(map(repr, search_roots))
 
-    cookie_database_path = _find_most_recently_used_file(search_root, 'cookies.sqlite', logger)
+    cookie_database_path = _newest(_firefox_cookie_dbs(search_roots))
     if cookie_database_path is None:
         raise FileNotFoundError(f'could not find firefox cookies database in {search_root}')
     logger.debug(f'Extracting cookies from: "{cookie_database_path}"')
@@ -182,12 +186,21 @@ def _extract_firefox_cookies(profile, container, logger):
                 cursor.connection.close()
 
 
-def _firefox_browser_dir():
+def _firefox_browser_dirs():
     if sys.platform in ('cygwin', 'win32'):
-        return os.path.expandvars(R'%APPDATA%\Mozilla\Firefox\Profiles')
+        yield os.path.expandvars(R'%APPDATA%\Mozilla\Firefox\Profiles')
+
     elif sys.platform == 'darwin':
-        return os.path.expanduser('~/Library/Application Support/Firefox/Profiles')
-    return os.path.expanduser('~/.mozilla/firefox')
+        yield os.path.expanduser('~/Library/Application Support/Firefox/Profiles')
+
+    else:
+        yield from map(os.path.expanduser, ('~/.mozilla/firefox', '~/snap/firefox/common/.mozilla/firefox'))
+
+
+def _firefox_cookie_dbs(roots):
+    for root in map(os.path.abspath, roots):
+        for pattern in ('', '*/', 'Profiles/*/'):
+            yield from glob.iglob(os.path.join(root, pattern, 'cookies.sqlite'))
 
 
 def _get_chromium_based_browser_settings(browser_name):
@@ -268,7 +281,7 @@ def _extract_chrome_cookies(browser_name, profile, keyring, logger):
             logger.error(f'{browser_name} does not support profiles')
             search_root = config['browser_dir']
 
-    cookie_database_path = _find_most_recently_used_file(search_root, 'Cookies', logger)
+    cookie_database_path = _newest(_find_files(search_root, 'Cookies', logger))
     if cookie_database_path is None:
         raise FileNotFoundError(f'could not find {browser_name} cookies database in "{search_root}"')
     logger.debug(f'Extracting cookies from: "{cookie_database_path}"')
@@ -307,6 +320,12 @@ def _extract_chrome_cookies(browser_name, profile, keyring, logger):
             counts['unencrypted'] = unencrypted_cookies
             logger.debug(f'cookie version breakdown: {counts}')
             return jar
+        except PermissionError as error:
+            if compat_os_name == 'nt' and error.errno == 13:
+                message = 'Could not copy Chrome cookie database. See  https://github.com/yt-dlp/yt-dlp/issues/7271  for more info'
+                logger.error(message)
+                raise DownloadError(message)  # force exit
+            raise
         finally:
             if cursor is not None:
                 cursor.connection.close()
@@ -947,7 +966,7 @@ def _get_windows_v10_key(browser_root, logger):
     References:
         - [1] https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/os_crypt_win.cc
     """
-    path = _find_most_recently_used_file(browser_root, 'Local State', logger)
+    path = _newest(_find_files(browser_root, 'Local State', logger))
     if path is None:
         logger.error('could not find local state file')
         return None
@@ -1049,17 +1068,20 @@ def _get_column_names(cursor, table_name):
     return [row[1].decode() for row in table_info]
 
 
-def _find_most_recently_used_file(root, filename, logger):
+def _newest(files):
+    return max(files, key=lambda path: os.lstat(path).st_mtime, default=None)
+
+
+def _find_files(root, filename, logger):
     # if there are multiple browser profiles, take the most recently used one
-    i, paths = 0, []
+    i = 0
     with _create_progress_bar(logger) as progress_bar:
-        for curr_root, dirs, files in os.walk(root):
+        for curr_root, _, files in os.walk(root):
             for file in files:
                 i += 1
                 progress_bar.print(f'Searching for "{filename}": {i: 6d} files searched')
                 if file == filename:
-                    paths.append(os.path.join(curr_root, file))
-    return None if not paths else max(paths, key=lambda path: os.lstat(path).st_mtime)
+                    yield os.path.join(curr_root, file)
 
 
 def _merge_cookie_jars(jars):
@@ -1073,7 +1095,7 @@ def _merge_cookie_jars(jars):
 
 
 def _is_path(value):
-    return os.path.sep in value
+    return any(sep in value for sep in (os.path.sep, os.path.altsep) if sep)
 
 
 def _parse_browser_specification(browser_name, profile=None, keyring=None, container=None):
diff --git a/yt_dlp/downloader/hls.py b/yt_dlp/downloader/hls.py
index d4b3f0320..4ac5d99dc 100644
--- a/yt_dlp/downloader/hls.py
+++ b/yt_dlp/downloader/hls.py
@@ -369,7 +369,10 @@ def fin_fragments():
 
                 return output.getvalue().encode()
 
-            self.download_and_append_fragments(
-                ctx, fragments, info_dict, pack_func=pack_fragment, finish_func=fin_fragments)
+            if len(fragments) == 1:
+                self.download_and_append_fragments(ctx, fragments, info_dict)
+            else:
+                self.download_and_append_fragments(
+                    ctx, fragments, info_dict, pack_func=pack_fragment, finish_func=fin_fragments)
         else:
             return self.download_and_append_fragments(ctx, fragments, info_dict)
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index 3d5c3eb60..04318a716 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -138,6 +138,10 @@
     ARDMediathekCollectionIE,
     ARDIE,
 )
+from .art19 import (
+    Art19IE,
+    Art19ShowIE,
+)
 from .arte import (
     ArteTVIE,
     ArteTVEmbedIE,
@@ -369,6 +373,7 @@
 from .cliprs import ClipRsIE
 from .closertotruth import CloserToTruthIE
 from .cloudflarestream import CloudflareStreamIE
+from .cloudycdn import CloudyCDNIE
 from .clubic import ClubicIE
 from .clyp import ClypIE
 from .cmt import CMTIE
@@ -564,6 +569,7 @@
     EroProfileIE,
     EroProfileAlbumIE,
 )
+from .err import ERRJupiterIE
 from .ertgr import (
     ERTFlixCodenameIE,
     ERTFlixIE,
@@ -588,6 +594,7 @@
     FacebookPluginsVideoIE,
     FacebookRedirectURLIE,
     FacebookReelIE,
+    FacebookAdsIE,
 )
 from .fancode import (
     FancodeVodIE,
@@ -1000,6 +1007,11 @@
     LRTVODIE,
     LRTStreamIE
 )
+from .lsm import (
+    LSMLREmbedIE,
+    LSMLTVEmbedIE,
+    LSMReplayIE
+)
 from .lumni import (
     LumniIE
 )
@@ -1111,6 +1123,7 @@
     MotherlessIE,
     MotherlessGroupIE,
     MotherlessGalleryIE,
+    MotherlessUploaderIE,
 )
 from .motorsport import MotorsportIE
 from .moviepilot import MoviepilotIE
@@ -1137,6 +1150,11 @@
     MusicdexArtistIE,
     MusicdexPlaylistIE,
 )
+from .mx3 import (
+    Mx3IE,
+    Mx3NeoIE,
+    Mx3VolksmusikIE,
+)
 from .mxplayer import (
     MxplayerIE,
     MxplayerShowIE,
@@ -1229,7 +1247,10 @@
     NexxIE,
     NexxEmbedIE,
 )
-from .nfb import NFBIE
+from .nfb import (
+    NFBIE,
+    NFBSeriesIE,
+)
 from .nfhsnetwork import NFHSNetworkIE
 from .nfl import (
     NFLIE,
@@ -1266,6 +1287,7 @@
     NicovideoTagURLIE,
     NiconicoLiveIE,
 )
+from .ninaprotocol import NinaProtocolIE
 from .ninecninemedia import (
     NineCNineMediaIE,
     CPTwentyFourIE,
@@ -1372,6 +1394,7 @@
 from .orf import (
     ORFTVthekIE,
     ORFFM4StoryIE,
+    ORFONIE,
     ORFRadioIE,
     ORFPodcastIE,
     ORFIPTVIE,
@@ -1496,7 +1519,7 @@
     PuhuTVSerieIE,
 )
 from .pr0gramm import Pr0grammIE
-from .prankcast import PrankCastIE
+from .prankcast import PrankCastIE, PrankCastPostIE
 from .premiershiprugby import PremiershipRugbyIE
 from .presstv import PressTVIE
 from .projectveritas import ProjectVeritasIE
@@ -1593,6 +1616,7 @@
     RedBullIE,
 )
 from .reddit import RedditIE
+from .redge import RedCDNLivxIE
 from .redgifs import (
     RedGifsIE,
     RedGifsSearchIE,
@@ -1727,6 +1751,7 @@
 )
 from .scrolller import ScrolllerIE
 from .seeker import SeekerIE
+from .sejmpl import SejmIE
 from .senalcolombia import SenalColombiaLiveIE
 from .senategov import SenateISVPIE, SenateGovIE
 from .sendtonews import SendtoNewsIE
@@ -2472,6 +2497,7 @@
     Zee5SeriesIE,
 )
 from .zeenews import ZeeNewsIE
+from .zetland import ZetlandDKArticleIE
 from .zhihu import ZhihuIE
 from .zingmp3 import (
     ZingMp3IE,
diff --git a/yt_dlp/extractor/adn.py b/yt_dlp/extractor/adn.py
index ed23226a3..898d37298 100644
--- a/yt_dlp/extractor/adn.py
+++ b/yt_dlp/extractor/adn.py
@@ -3,6 +3,7 @@
 import json
 import os
 import random
+import time
 
 from .common import InfoExtractor
 from ..aes import aes_cbc_decrypt_bytes, unpad_pkcs7
@@ -17,6 +18,7 @@
     int_or_none,
     intlist_to_bytes,
     long_to_bytes,
+    parse_iso8601,
     pkcs1pad,
     strip_or_none,
     str_or_none,
@@ -185,7 +187,10 @@ def _real_extract(self, url):
 
         user = options['user']
         if not user.get('hasAccess'):
-            self.raise_login_required()
+            start_date = traverse_obj(options, ('video', 'startDate', {str}))
+            if (parse_iso8601(start_date) or 0) > time.time():
+                raise ExtractorError(f'This video is not available yet. Release date: {start_date}', expected=True)
+            self.raise_login_required('This video requires a subscription', method='password')
 
         token = self._download_json(
             user.get('refreshTokenUrl') or (self._PLAYER_BASE_URL + 'refresh/token'),
@@ -267,6 +272,9 @@ def _real_extract(self, url):
                         f['language'] = 'de'
                 formats.extend(m3u8_formats)
 
+        if not formats:
+            self.raise_login_required('This video requires a subscription', method='password')
+
         video = (self._download_json(
             self._API_BASE_URL + 'video/%s' % video_id, video_id,
             'Downloading additional video metadata', fatal=False) or {}).get('video') or {}
diff --git a/yt_dlp/extractor/ard.py b/yt_dlp/extractor/ard.py
index f4b1cd075..46e68d61e 100644
--- a/yt_dlp/extractor/ard.py
+++ b/yt_dlp/extractor/ard.py
@@ -8,6 +8,7 @@
     determine_ext,
     int_or_none,
     join_nonempty,
+    jwt_decode_hs256,
     make_archive_id,
     parse_duration,
     parse_iso8601,
@@ -238,6 +239,7 @@ class ARDBetaMediathekIE(InfoExtractor):
         (?P<id>[a-zA-Z0-9]+)
         /?(?:[?#]|$)'''
     _GEO_COUNTRIES = ['DE']
+    _TOKEN_URL = 'https://sso.ardmediathek.de/sso/token'
 
     _TESTS = [{
         'url': 'https://www.ardmediathek.de/video/filme-im-mdr/liebe-auf-vier-pfoten/mdr-fernsehen/Y3JpZDovL21kci5kZS9zZW5kdW5nLzI4MjA0MC80MjIwOTEtNDAyNTM0',
@@ -359,12 +361,27 @@ def _extract_episode_info(self, title):
 
     def _real_extract(self, url):
         display_id = self._match_id(url)
+        query = {'embedded': 'false', 'mcV6': 'true'}
+        headers = {}
+
+        if self._get_cookies(self._TOKEN_URL).get('ams'):
+            token = self._download_json(
+                self._TOKEN_URL, display_id, 'Fetching token for age verification',
+                'Unable to fetch age verification token', fatal=False)
+            id_token = traverse_obj(token, ('idToken', {str}))
+            decoded_token = traverse_obj(id_token, ({jwt_decode_hs256}, {dict}))
+            user_id = traverse_obj(decoded_token, (('user_id', 'sub'), {str}), get_all=False)
+            if not user_id:
+                self.report_warning('Unable to extract token, continuing without authentication')
+            else:
+                headers['x-authorization'] = f'Bearer {id_token}'
+                query['userId'] = user_id
+                if decoded_token.get('age_rating') != 18:
+                    self.report_warning('Account is not verified as 18+; video may be unavailable')
 
         page_data = self._download_json(
-            f'https://api.ardmediathek.de/page-gateway/pages/ard/item/{display_id}', display_id, query={
-                'embedded': 'false',
-                'mcV6': 'true',
-            })
+            f'https://api.ardmediathek.de/page-gateway/pages/ard/item/{display_id}',
+            display_id, query=query, headers=headers)
 
         # For user convenience we use the old contentId instead of the longer crid
         # Ref: https://github.com/yt-dlp/yt-dlp/issues/8731#issuecomment-1874398283
@@ -383,7 +400,7 @@ def _real_extract(self, url):
         media_data = traverse_obj(player_data, ('mediaCollection', 'embedded', {dict}))
 
         if player_data.get('blockedByFsk'):
-            self.raise_no_formats('This video is only available after 22:00', expected=True)
+            self.raise_login_required('This video is only available for age verified users or after 22:00')
 
         formats = []
         subtitles = {}
diff --git a/yt_dlp/extractor/art19.py b/yt_dlp/extractor/art19.py
new file mode 100644
index 000000000..271c505da
--- /dev/null
+++ b/yt_dlp/extractor/art19.py
@@ -0,0 +1,303 @@
+import re
+
+from .common import InfoExtractor
+from ..utils import float_or_none, int_or_none, parse_iso8601, url_or_none
+from ..utils.traversal import traverse_obj
+
+
+class Art19IE(InfoExtractor):
+    _UUID_REGEX = r'[\da-f]{8}-?[\da-f]{4}-?[\da-f]{4}-?[\da-f]{4}-?[\da-f]{12}'
+    _VALID_URL = [
+        rf'https?://(?:www\.)?art19\.com/shows/[^/#?]+/episodes/(?P<id>{_UUID_REGEX})',
+        rf'https?://rss\.art19\.com/episodes/(?P<id>{_UUID_REGEX})\.mp3',
+    ]
+    _EMBED_REGEX = [rf'<iframe[^>]+\bsrc=[\'"](?P<url>{_VALID_URL[0]})']
+
+    _TESTS = [{
+        'url': 'https://rss.art19.com/episodes/5ba1413c-48b8-472b-9cc3-cfd952340bdb.mp3',
+        'info_dict': {
+            'id': '5ba1413c-48b8-472b-9cc3-cfd952340bdb',
+            'ext': 'mp3',
+            'title': 'Why Did DeSantis Drop Out?',
+            'series': 'The Daily Briefing',
+            'release_timestamp': 1705941275,
+            'description': 'md5:da38961da4a3f7e419471365e3c6b49f',
+            'episode': 'Episode 582',
+            'thumbnail': r're:^https?://content\.production\.cdn\.art19\.com.*\.jpeg$',
+            'series_id': 'ed52a0ab-08b1-4def-8afc-549e4d93296d',
+            'upload_date': '20240122',
+            'timestamp': 1705940815,
+            'episode_number': 582,
+            'modified_date': '20240122',
+            'episode_id': '5ba1413c-48b8-472b-9cc3-cfd952340bdb',
+            'modified_timestamp': 1705941275,
+            'release_date': '20240122',
+            'duration': 527.4,
+        },
+    }, {
+        'url': 'https://art19.com/shows/scamfluencers/episodes/8319b776-4153-4d22-8630-631f204a03dd',
+        'info_dict': {
+            'id': '8319b776-4153-4d22-8630-631f204a03dd',
+            'ext': 'mp3',
+            'title': 'Martha Stewart: The Homemaker Hustler Part 2',
+            'modified_date': '20240116',
+            'upload_date': '20240105',
+            'modified_timestamp': 1705435802,
+            'episode_id': '8319b776-4153-4d22-8630-631f204a03dd',
+            'series_id': 'd3c9b8ca-26b3-42f4-9bd8-21d1a9031e75',
+            'thumbnail': r're:^https?://content\.production\.cdn\.art19\.com.*\.jpeg$',
+            'description': 'md5:4aa7cfd1358dc57e729835bc208d7893',
+            'release_timestamp': 1705305660,
+            'release_date': '20240115',
+            'timestamp': 1704481536,
+            'episode_number': 88,
+            'series': 'Scamfluencers',
+            'duration': 2588.37501,
+            'episode': 'Episode 88',
+        },
+    }]
+    _WEBPAGE_TESTS = [{
+        'url': 'https://www.nu.nl/formule-1/6291456/verstappen-wordt-een-synoniem-voor-formule-1.html',
+        'info_dict': {
+            'id': '7d42626a-7301-47db-bb8a-3b6f054d77d7',
+            'ext': 'mp3',
+            'title': "'Verstappen wordt een synoniem voor Formule 1'",
+            'season': 'Seizoen 6',
+            'description': 'md5:39a7159a31c4cda312b2e893bdd5c071',
+            'episode_id': '7d42626a-7301-47db-bb8a-3b6f054d77d7',
+            'duration': 3061.82111,
+            'series_id': '93f4e113-2a60-4609-a564-755058fa40d8',
+            'release_date': '20231126',
+            'modified_timestamp': 1701156004,
+            'thumbnail': r're:^https?://content\.production\.cdn\.art19\.com.*\.jpeg$',
+            'season_number': 6,
+            'episode_number': 52,
+            'modified_date': '20231128',
+            'upload_date': '20231126',
+            'timestamp': 1701025981,
+            'season_id': '36097c1e-7455-490d-a2fe-e2f10b4d5f26',
+            'series': 'De Boordradio',
+            'release_timestamp': 1701026308,
+            'episode': 'Episode 52',
+        },
+    }, {
+        'url': 'https://www.wishtv.com/podcast-episode/larry-bucshon-announces-retirement-from-congress/',
+        'info_dict': {
+            'id': '8da368bd-08d1-46d0-afaa-c134a4af7dc0',
+            'ext': 'mp3',
+            'title': 'Larry Bucshon announces retirement from congress',
+            'upload_date': '20240115',
+            'episode_number': 148,
+            'episode': 'Episode 148',
+            'thumbnail': r're:^https?://content\.production\.cdn\.art19\.com.*\.jpeg$',
+            'release_date': '20240115',
+            'timestamp': 1705328205,
+            'release_timestamp': 1705329275,
+            'series': 'All INdiana Politics',
+            'modified_date': '20240117',
+            'modified_timestamp': 1705458901,
+            'series_id': 'c4af6c27-b10f-4ff2-9f84-0f407df86ff1',
+            'episode_id': '8da368bd-08d1-46d0-afaa-c134a4af7dc0',
+            'description': 'md5:53b5239e4d14973a87125c217c255b2a',
+            'duration': 1256.18848,
+        },
+    }]
+
+    @classmethod
+    def _extract_embed_urls(cls, url, webpage):
+        yield from super()._extract_embed_urls(url, webpage)
+        for episode_id in re.findall(
+                rf'<div[^>]+\bclass=[\'"][^\'"]*art19-web-player[^\'"]*[\'"][^>]+\bdata-episode-id=[\'"]({cls._UUID_REGEX})[\'"]', webpage):
+            yield f'https://rss.art19.com/episodes/{episode_id}.mp3'
+
+    def _real_extract(self, url):
+        episode_id = self._match_id(url)
+
+        player_metadata = self._download_json(
+            f'https://art19.com/episodes/{episode_id}', episode_id,
+            note='Downloading player metadata', fatal=False,
+            headers={'Accept': 'application/vnd.art19.v0+json'})
+        rss_metadata = self._download_json(
+            f'https://rss.art19.com/episodes/{episode_id}.json', episode_id, fatal=False,
+            note='Downloading RSS metadata')
+
+        formats = [{
+            'format_id': 'direct',
+            'url': f'https://rss.art19.com/episodes/{episode_id}.mp3',
+            'vcodec': 'none',
+            'acodec': 'mp3',
+        }]
+        for fmt_id, fmt_data in traverse_obj(rss_metadata, ('content', 'media', {dict.items}, ...)):
+            if fmt_id == 'waveform_bin':
+                continue
+            fmt_url = traverse_obj(fmt_data, ('url', {url_or_none}))
+            if not fmt_url:
+                continue
+            formats.append({
+                'format_id': fmt_id,
+                'url': fmt_url,
+                'vcodec': 'none',
+                'acodec': fmt_id,
+                'quality': -2 if fmt_id == 'ogg' else -1,
+            })
+
+        return {
+            'id': episode_id,
+            'formats': formats,
+            **traverse_obj(player_metadata, ('episode', {
+                'title': ('title', {str}),
+                'description': ('description_plain', {str}),
+                'episode_id': ('id', {str}),
+                'episode_number': ('episode_number', {int_or_none}),
+                'season_id': ('season_id', {str}),
+                'series_id': ('series_id', {str}),
+                'timestamp': ('created_at', {parse_iso8601}),
+                'release_timestamp': ('released_at', {parse_iso8601}),
+                'modified_timestamp': ('updated_at', {parse_iso8601})
+            })),
+            **traverse_obj(rss_metadata, ('content', {
+                'title': ('episode_title', {str}),
+                'description': ('episode_description_plain', {str}),
+                'episode_id': ('episode_id', {str}),
+                'episode_number': ('episode_number', {int_or_none}),
+                'season': ('season_title', {str}),
+                'season_id': ('season_id', {str}),
+                'season_number': ('season_number', {int_or_none}),
+                'series': ('series_title', {str}),
+                'series_id': ('series_id', {str}),
+                'thumbnail': ('cover_image', {url_or_none}),
+                'duration': ('duration', {float_or_none}),
+            })),
+        }
+
+
+class Art19ShowIE(InfoExtractor):
+    _VALID_URL_BASE = r'https?://(?:www\.)?art19\.com/shows/(?P<id>[\w-]+)(?:/embed)?/?'
+    _VALID_URL = [
+        rf'{_VALID_URL_BASE}(?:$|[#?])',
+        r'https?://rss\.art19\.com/(?P<id>[\w-]+)/?(?:$|[#?])',
+    ]
+    _EMBED_REGEX = [rf'<iframe[^>]+\bsrc=[\'"](?P<url>{_VALID_URL_BASE}[^\'"])']
+
+    _TESTS = [{
+        'url': 'https://www.art19.com/shows/5898c087-a14f-48dc-b6fc-a2280a1ff6e0/',
+        'info_dict': {
+            '_type': 'playlist',
+            'id': '5898c087-a14f-48dc-b6fc-a2280a1ff6e0',
+            'display_id': 'echt-gebeurd',
+            'title': 'Echt Gebeurd',
+            'description': 'md5:5fd11dc80b76e51ffd34b6067fd5e560',
+            'timestamp': 1492642167,
+            'upload_date': '20170419',
+            'modified_timestamp': int,
+            'modified_date': str,
+            'tags': 'count:7',
+        },
+        'playlist_mincount': 425,
+    }, {
+        'url': 'https://www.art19.com/shows/echt-gebeurd',
+        'info_dict': {
+            '_type': 'playlist',
+            'id': '5898c087-a14f-48dc-b6fc-a2280a1ff6e0',
+            'display_id': 'echt-gebeurd',
+            'title': 'Echt Gebeurd',
+            'description': 'md5:5fd11dc80b76e51ffd34b6067fd5e560',
+            'timestamp': 1492642167,
+            'upload_date': '20170419',
+            'modified_timestamp': int,
+            'modified_date': str,
+            'tags': 'count:7',
+        },
+        'playlist_mincount': 425,
+    }, {
+        'url': 'https://rss.art19.com/scamfluencers',
+        'info_dict': {
+            '_type': 'playlist',
+            'id': 'd3c9b8ca-26b3-42f4-9bd8-21d1a9031e75',
+            'display_id': 'scamfluencers',
+            'title': 'Scamfluencers',
+            'description': 'md5:7d239d670c0ced6dadbf71c4caf764b7',
+            'timestamp': 1647368573,
+            'upload_date': '20220315',
+            'modified_timestamp': int,
+            'modified_date': str,
+            'tags': [],
+        },
+        'playlist_mincount': 90,
+    }, {
+        'url': 'https://art19.com/shows/enthuellt/embed',
+        'info_dict': {
+            '_type': 'playlist',
+            'id': 'e2cacf57-bb8a-4263-aa81-719bcdd4f80c',
+            'display_id': 'enthuellt',
+            'title': 'Enthüllt',
+            'description': 'md5:17752246643414a2fd51744fc9a1c08e',
+            'timestamp': 1601645860,
+            'upload_date': '20201002',
+            'modified_timestamp': int,
+            'modified_date': str,
+            'tags': 'count:10',
+        },
+        'playlist_mincount': 10,
+    }]
+    _WEBPAGE_TESTS = [{
+        'url': 'https://deconstructingyourself.com/deconstructing-yourself-podcast',
+        'info_dict': {
+            '_type': 'playlist',
+            'id': 'cfbb9b01-c295-4adb-8726-adde7c03cf21',
+            'display_id': 'deconstructing-yourself',
+            'title': 'Deconstructing Yourself',
+            'description': 'md5:dab5082b28b248a35476abf64768854d',
+            'timestamp': 1570581181,
+            'upload_date': '20191009',
+            'modified_timestamp': int,
+            'modified_date': str,
+            'tags': 'count:5',
+        },
+        'playlist_mincount': 80,
+    }, {
+        'url': 'https://chicagoreader.com/columns-opinion/podcasts/ben-joravsky-show-podcast-episodes/',
+        'info_dict': {
+            '_type': 'playlist',
+            'id': '9dfa2c37-ab87-4c13-8388-4897914313ec',
+            'display_id': 'the-ben-joravsky-show',
+            'title': 'The Ben Joravsky Show',
+            'description': 'md5:c0f3ec0ee0dbea764390e521adc8780a',
+            'timestamp': 1550875095,
+            'upload_date': '20190222',
+            'modified_timestamp': int,
+            'modified_date': str,
+            'tags': ['Chicago Politics', 'chicago', 'Ben Joravsky'],
+        },
+        'playlist_mincount': 1900,
+    }]
+
+    @classmethod
+    def _extract_embed_urls(cls, url, webpage):
+        yield from super()._extract_embed_urls(url, webpage)
+        for series_id in re.findall(
+                r'<div[^>]+\bclass=[\'"][^\'"]*art19-web-player[^\'"]*[\'"][^>]+\bdata-series-id=[\'"]([\w-]+)[\'"]', webpage):
+            yield f'https://art19.com/shows/{series_id}'
+
+    def _real_extract(self, url):
+        series_id = self._match_id(url)
+        series_metadata = self._download_json(
+            f'https://art19.com/series/{series_id}', series_id, note='Downloading series metadata',
+            headers={'Accept': 'application/vnd.art19.v0+json'})
+
+        return {
+            '_type': 'playlist',
+            'entries': [
+                self.url_result(f'https://rss.art19.com/episodes/{episode_id}.mp3', Art19IE)
+                for episode_id in traverse_obj(series_metadata, ('series', 'episode_ids', ..., {str}))
+            ],
+            **traverse_obj(series_metadata, ('series', {
+                'id': ('id', {str}),
+                'display_id': ('slug', {str}),
+                'title': ('title', {str}),
+                'description': ('description_plain', {str}),
+                'timestamp': ('created_at', {parse_iso8601}),
+                'modified_timestamp': ('updated_at', {parse_iso8601}),
+            })),
+            'tags': traverse_obj(series_metadata, ('tags', ..., 'name', {str})),
+        }
diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py
index cd7df69ef..4ed9e2af7 100644
--- a/yt_dlp/extractor/bilibili.py
+++ b/yt_dlp/extractor/bilibili.py
@@ -7,6 +7,7 @@
 import re
 import time
 import urllib.parse
+import uuid
 
 from .common import InfoExtractor, SearchInfoExtractor
 from ..dependencies import Cryptodome
@@ -1464,8 +1465,37 @@ class BiliBiliSearchIE(SearchInfoExtractor):
     IE_DESC = 'Bilibili video search'
     _MAX_RESULTS = 100000
     _SEARCH_KEY = 'bilisearch'
+    _TESTS = [{
+        'url': 'bilisearch3:靡烟 出道一年，我怎么还在等你单推的女人睡觉后开播啊',
+        'playlist_count': 3,
+        'info_dict': {
+            'id': '靡烟 出道一年，我怎么还在等你单推的女人睡觉后开播啊',
+            'title': '靡烟 出道一年，我怎么还在等你单推的女人睡觉后开播啊',
+        },
+        'playlist': [{
+            'info_dict': {
+                'id': 'BV1n44y1Q7sc',
+                'ext': 'mp4',
+                'title': '“出道一年，我怎么还在等你单推的女人睡觉后开播啊？”【一分钟了解靡烟miya】',
+                'timestamp': 1669889987,
+                'upload_date': '20221201',
+                'description': 'md5:43343c0973defff527b5a4b403b4abf9',
+                'tags': list,
+                'uploader': '靡烟miya',
+                'duration': 123.156,
+                'uploader_id': '1958703906',
+                'comment_count': int,
+                'view_count': int,
+                'like_count': int,
+                'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
+                '_old_archive_ids': ['bilibili 988222410_part1'],
+            },
+        }],
+    }]
 
     def _search_results(self, query):
+        if not self._get_cookies('https://api.bilibili.com').get('buvid3'):
+            self._set_cookie('.bilibili.com', 'buvid3', f'{uuid.uuid4()}infoc')
         for page_num in itertools.count(1):
             videos = self._download_json(
                 'https://api.bilibili.com/x/web-interface/search/type', query,
diff --git a/yt_dlp/extractor/ccma.py b/yt_dlp/extractor/ccma.py
index 88ff82f6e..ab840f301 100644
--- a/yt_dlp/extractor/ccma.py
+++ b/yt_dlp/extractor/ccma.py
@@ -1,6 +1,7 @@
 from .common import InfoExtractor
 from ..utils import (
     clean_html,
+    determine_ext,
     int_or_none,
     parse_duration,
     parse_resolution,
@@ -60,6 +61,7 @@ def _real_extract(self, url):
             'http://dinamics.ccma.cat/pvideo/media.jsp', media_id, query={
                 'media': media_type,
                 'idint': media_id,
+                'format': 'dm',
             })
 
         formats = []
@@ -69,6 +71,10 @@ def _real_extract(self, url):
                 format_url = url_or_none(format_.get('file'))
                 if not format_url:
                     continue
+                if determine_ext(format_url) == 'mpd':
+                    formats.extend(self._extract_mpd_formats(
+                        format_url, media_id, mpd_id='dash', fatal=False))
+                    continue
                 label = format_.get('label')
                 f = parse_resolution(label)
                 f.update({
diff --git a/yt_dlp/extractor/cineverse.py b/yt_dlp/extractor/cineverse.py
index c9fa789b7..032c4334b 100644
--- a/yt_dlp/extractor/cineverse.py
+++ b/yt_dlp/extractor/cineverse.py
@@ -67,7 +67,10 @@ def _real_extract(self, url):
         html = self._download_webpage(url, video_id)
         idetails = self._search_nextjs_data(html, video_id)['props']['pageProps']['idetails']
 
-        if idetails.get('err_code') == 1200:
+        err_code = idetails.get('err_code')
+        if err_code == 1002:
+            self.raise_login_required()
+        elif err_code == 1200:
             self.raise_geo_restricted(
                 'This video is not available from your location due to geo restriction. '
                 'You may be able to bypass it by using the /details/ page instead of the /watch/ page',
diff --git a/yt_dlp/extractor/cloudycdn.py b/yt_dlp/extractor/cloudycdn.py
new file mode 100644
index 000000000..e6e470e07
--- /dev/null
+++ b/yt_dlp/extractor/cloudycdn.py
@@ -0,0 +1,79 @@
+from .common import InfoExtractor
+from ..utils import (
+    int_or_none,
+    parse_iso8601,
+    url_or_none,
+    urlencode_postdata,
+)
+from ..utils.traversal import traverse_obj
+
+
+class CloudyCDNIE(InfoExtractor):
+    _VALID_URL = r'(?:https?:)?//embed\.cloudycdn\.services/(?P<site_id>[^/?#]+)/media/(?P<id>[\w-]+)'
+    _EMBED_REGEX = [rf'<iframe[^>]+\bsrc=[\'"](?P<url>{_VALID_URL})']
+    _TESTS = [{
+        'url': 'https://embed.cloudycdn.services/ltv/media/46k_d23-6000-105?',
+        'md5': '64f72a360ca530d5ed89c77646c9eee5',
+        'info_dict': {
+            'id': '46k_d23-6000-105',
+            'ext': 'mp4',
+            'timestamp': 1700589151,
+            'duration': 1442,
+            'upload_date': '20231121',
+            'title': 'D23-6000-105_cetstud',
+            'thumbnail': 'https://store.cloudycdn.services/tmsp00060/assets/media/660858/placeholder1700589200.jpg',
+        }
+    }, {
+        'url': 'https://embed.cloudycdn.services/izm/media/26e_lv-8-5-1',
+        'md5': '798828a479151e2444d8dcfbec76e482',
+        'info_dict': {
+            'id': '26e_lv-8-5-1',
+            'ext': 'mp4',
+            'title': 'LV-8-5-1',
+            'timestamp': 1669767167,
+            'thumbnail': 'https://store.cloudycdn.services/tmsp00120/assets/media/488306/placeholder1679423604.jpg',
+            'duration': 1205,
+            'upload_date': '20221130',
+        }
+    }]
+    _WEBPAGE_TESTS = [{
+        'url': 'https://www.tavaklase.lv/video/es-esmu-mina-um-2/',
+        'md5': '63074e8e6c84ac2a01f2fb8bf03b8f43',
+        'info_dict': {
+            'id': 'cqd_lib-2',
+            'ext': 'mp4',
+            'upload_date': '20230223',
+            'duration': 629,
+            'thumbnail': 'https://store.cloudycdn.services/tmsp00120/assets/media/518407/placeholder1678748124.jpg',
+            'timestamp': 1677181513,
+            'title': 'LIB-2',
+        }
+    }]
+
+    def _real_extract(self, url):
+        site_id, video_id = self._match_valid_url(url).group('site_id', 'id')
+
+        data = self._download_json(
+            f'https://player.cloudycdn.services/player/{site_id}/media/{video_id}/',
+            video_id, data=urlencode_postdata({
+                'version': '6.4.0',
+                'referer': url,
+            }))
+
+        formats, subtitles = [], {}
+        for m3u8_url in traverse_obj(data, ('source', 'sources', ..., 'src', {url_or_none})):
+            fmts, subs = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, fatal=False)
+            formats.extend(fmts)
+            self._merge_subtitles(subs, target=subtitles)
+
+        return {
+            'id': video_id,
+            'formats': formats,
+            'subtitles': subtitles,
+            **traverse_obj(data, {
+                'title': ('name', {str}),
+                'duration': ('duration', {int_or_none}),
+                'timestamp': ('upload_date', {parse_iso8601}),
+                'thumbnail': ('source', 'poster', {url_or_none}),
+            }),
+        }
diff --git a/yt_dlp/extractor/eporner.py b/yt_dlp/extractor/eporner.py
index aee2dee58..b18a76c7c 100644
--- a/yt_dlp/extractor/eporner.py
+++ b/yt_dlp/extractor/eporner.py
@@ -1,8 +1,10 @@
 from .common import InfoExtractor
 from ..utils import (
-    encode_base_n,
     ExtractorError,
+    encode_base_n,
+    get_elements_by_class,
     int_or_none,
+    join_nonempty,
     merge_dicts,
     parse_duration,
     str_to_int,
@@ -81,6 +83,7 @@ def calc_hash(s):
         sources = video['sources']
 
         formats = []
+        has_av1 = bool(get_elements_by_class('download-av1', webpage))
         for kind, formats_dict in sources.items():
             if not isinstance(formats_dict, dict):
                 continue
@@ -106,6 +109,14 @@ def calc_hash(s):
                         'height': height,
                         'fps': fps,
                     })
+                    if has_av1:
+                        formats.append({
+                            'url': src.replace('.mp4', '-av1.mp4'),
+                            'format_id': join_nonempty('av1', format_id),
+                            'height': height,
+                            'fps': fps,
+                            'vcodec': 'av1',
+                        })
 
         json_ld = self._search_json_ld(webpage, display_id, default={})
 
diff --git a/yt_dlp/extractor/err.py b/yt_dlp/extractor/err.py
new file mode 100644
index 000000000..129f39ad6
--- /dev/null
+++ b/yt_dlp/extractor/err.py
@@ -0,0 +1,199 @@
+from .common import InfoExtractor
+from ..utils import (
+    clean_html,
+    int_or_none,
+    str_or_none,
+    url_or_none,
+)
+from ..utils.traversal import traverse_obj
+
+
+class ERRJupiterIE(InfoExtractor):
+    _VALID_URL = r'https?://jupiter(?:pluss)?\.err\.ee/(?P<id>\d+)'
+    _TESTS = [{
+        'note': 'Jupiter: Movie: siin-me-oleme',
+        'url': 'https://jupiter.err.ee/1211107/siin-me-oleme',
+        'md5': '9b45d1682a98853acaa1e1b0c791f425',
+        'info_dict': {
+            'id': '1211107',
+            'ext': 'mp4',
+            'title': 'Siin me oleme!',
+            'alt_title': '',
+            'description': 'md5:1825b795f5f7584241aeb59e5bbb4f70',
+            'release_date': '20231226',
+            'upload_date': '20201217',
+            'modified_date': '20201217',
+            'release_timestamp': 1703577600,
+            'timestamp': 1608210000,
+            'modified_timestamp': 1608220800,
+            'release_year': 1978,
+        },
+    }, {
+        'note': 'Jupiter: Series: Impulss',
+        'url': 'https://jupiter.err.ee/1609145945/impulss',
+        'md5': 'a378486df07ed1ba74e46cc861886243',
+        'info_dict': {
+            'id': '1609145945',
+            'ext': 'mp4',
+            'title': 'Impulss',
+            'alt_title': 'Loteriipilet hooldekodusse',
+            'description': 'md5:fa8a2ed0cdccb130211513443ee4d571',
+            'release_date': '20231107',
+            'upload_date': '20231026',
+            'modified_date': '20231118',
+            'release_timestamp': 1699380000,
+            'timestamp': 1698327601,
+            'modified_timestamp': 1700311802,
+            'series': 'Impulss',
+            'season': 'Season 1',
+            'season_number': 1,
+            'episode': 'Loteriipilet hooldekodusse',
+            'episode_number': 6,
+            'series_id': '1609108187',
+            'release_year': 2023,
+            'episode_id': '1609145945',
+        },
+    }, {
+        'note': 'Jupiter: Radio Show: mnemoturniir episode',
+        'url': 'https://jupiter.err.ee/1037919/mnemoturniir',
+        'md5': 'f1eb95fe66f9620ff84e81bbac37076a',
+        'info_dict': {
+            'id': '1037919',
+            'ext': 'm4a',
+            'title': 'Mnemoturniir',
+            'alt_title': '',
+            'description': 'md5:626db52394e7583c26ab74d6a34d9982',
+            'release_date': '20240121',
+            'upload_date': '20240108',
+            'modified_date': '20240121',
+            'release_timestamp': 1705827900,
+            'timestamp': 1704675602,
+            'modified_timestamp': 1705827601,
+            'series': 'Mnemoturniir',
+            'season': 'Season 0',
+            'season_number': 0,
+            'episode': 'Episode 0',
+            'episode_number': 0,
+            'series_id': '1037919',
+            'release_year': 2024,
+            'episode_id': '1609215101',
+        },
+    }, {
+        'note': 'Jupiter+: Clip: bolee-zelenyj-tallinn',
+        'url': 'https://jupiterpluss.err.ee/1609180445/bolee-zelenyj-tallinn',
+        'md5': '1b812270c4daf6ce51c06bfeaf33ed95',
+        'info_dict': {
+            'id': '1609180445',
+            'ext': 'mp4',
+            'title': 'Более зеленый Таллинн',
+            'alt_title': '',
+            'description': 'md5:fd34d9bf939c28c4a725b19a7f0d6320',
+            'release_date': '20231224',
+            'upload_date': '20231130',
+            'modified_date': '20231207',
+            'release_timestamp': 1703423400,
+            'timestamp': 1701338400,
+            'modified_timestamp': 1701967200,
+            'release_year': 2023,
+        },
+    }, {
+        'note': 'Jupiter+: Series: The Sniffer',
+        'url': 'https://jupiterpluss.err.ee/1608311387/njuhach',
+        'md5': '2abdeb7131ce551bce49e8d0cea08536',
+        'info_dict': {
+            'id': '1608311387',
+            'ext': 'mp4',
+            'title': 'Нюхач',
+            'alt_title': '',
+            'description': 'md5:8c5c7d8f32ec6e54cd498c9e59ca83bc',
+            'release_date': '20230601',
+            'upload_date': '20210818',
+            'modified_date': '20210903',
+            'release_timestamp': 1685633400,
+            'timestamp': 1629318000,
+            'modified_timestamp': 1630686000,
+            'release_year': 2013,
+            'episode': 'Episode 1',
+            'episode_id': '1608311390',
+            'episode_number': 1,
+            'season': 'Season 1',
+            'season_number': 1,
+            'series': 'Нюхач',
+            'series_id': '1608311387',
+        },
+    }, {
+        'note': 'Jupiter+: Podcast: lesnye-istorii-aisty',
+        'url': 'https://jupiterpluss.err.ee/1608990335/lesnye-istorii-aisty',
+        'md5': '8b46d7e4510b254a14b7a52211b5bf96',
+        'info_dict': {
+            'id': '1608990335',
+            'ext': 'm4a',
+            'title': 'Лесные истории | Аисты',
+            'alt_title': '',
+            'description': 'md5:065e721623e271e7a63e6540d409ca6b',
+            'release_date': '20230609',
+            'upload_date': '20230527',
+            'modified_date': '20230608',
+            'release_timestamp': 1686308700,
+            'timestamp': 1685145600,
+            'modified_timestamp': 1686252600,
+            'release_year': 2023,
+            'episode': 'Episode 0',
+            'episode_id': '1608990335',
+            'episode_number': 0,
+            'season': 'Season 0',
+            'season_number': 0,
+            'series': 'Лесные истории | Аисты',
+            'series_id': '1037497',
+        },
+    }]
+
+    def _real_extract(self, url):
+        video_id = self._match_id(url)
+        data = self._download_json(
+            'https://services.err.ee/api/v2/vodContent/getContentPageData', video_id,
+            query={'contentId': video_id})['data']['mainContent']
+
+        media_data = traverse_obj(data, ('medias', ..., {dict}), get_all=False)
+        if traverse_obj(media_data, ('restrictions', 'drm', {bool})):
+            self.report_drm(video_id)
+
+        formats, subtitles = [], {}
+        for format_url in set(traverse_obj(media_data, ('src', ('hls', 'hls2', 'hlsNew'), {url_or_none}))):
+            fmts, subs = self._extract_m3u8_formats_and_subtitles(
+                format_url, video_id, 'mp4', m3u8_id='hls', fatal=False)
+            formats.extend(fmts)
+            self._merge_subtitles(subs, target=subtitles)
+        for format_url in set(traverse_obj(media_data, ('src', ('dash', 'dashNew'), {url_or_none}))):
+            fmts, subs = self._extract_mpd_formats_and_subtitles(
+                format_url, video_id, mpd_id='dash', fatal=False)
+            formats.extend(fmts)
+            self._merge_subtitles(subs, target=subtitles)
+        if format_url := traverse_obj(media_data, ('src', 'file', {url_or_none})):
+            formats.append({
+                'url': format_url,
+                'format_id': 'http',
+            })
+
+        return {
+            'id': video_id,
+            'formats': formats,
+            'subtitles': subtitles,
+            **traverse_obj(data, {
+                'title': ('heading', {str}),
+                'alt_title': ('subHeading', {str}),
+                'description': (('lead', 'body'), {clean_html}, {lambda x: x or None}),
+                'timestamp': ('created', {int_or_none}),
+                'modified_timestamp': ('updated', {int_or_none}),
+                'release_timestamp': (('scheduleStart', 'publicStart'), {int_or_none}),
+                'release_year': ('year', {int_or_none}),
+            }, get_all=False),
+            **(traverse_obj(data, {
+                'series': ('heading', {str}),
+                'series_id': ('rootContentId', {str_or_none}),
+                'episode': ('subHeading', {str}),
+                'season_number': ('season', {int_or_none}),
+                'episode_number': ('episode', {int_or_none}),
+                'episode_id': ('id', {str_or_none}),
+            }) if data.get('type') == 'episode' else {}),
+        }
diff --git a/yt_dlp/extractor/facebook.py b/yt_dlp/extractor/facebook.py
index a16a067ab..830bbcc3c 100644
--- a/yt_dlp/extractor/facebook.py
+++ b/yt_dlp/extractor/facebook.py
@@ -20,6 +20,7 @@
     get_element_by_id,
     get_first,
     int_or_none,
+    join_nonempty,
     js_to_json,
     merge_dicts,
     parse_count,
@@ -43,6 +44,7 @@ class FacebookIE(InfoExtractor):
                         (?:[^#]*?\#!/)?
                         (?:
                             (?:
+                                permalink\.php|
                                 video/video\.php|
                                 photo\.php|
                                 video\.php|
@@ -52,6 +54,7 @@ class FacebookIE(InfoExtractor):
                             )\?(?:.*?)(?:v|video_id|story_fbid)=|
                             [^/]+/videos/(?:[^/]+/)?|
                             [^/]+/posts/|
+                            events/(?:[^/]+/)?|
                             groups/[^/]+/(?:permalink|posts)/|
                             watchparty/
                         )|
@@ -248,6 +251,7 @@ class FacebookIE(InfoExtractor):
             'duration': 148.435,
         },
     }, {
+        # data.node.comet_sections.content.story.attachments[].styles.attachment.media
         'url': 'https://www.facebook.com/attn/posts/pfbid0j1Czf2gGDVqeQ8KiMLFm3pWN8GxsQmeRrVhimWDzMuKQoR8r4b1knNsejELmUgyhl',
         'info_dict': {
             'id': '6968553779868435',
@@ -262,6 +266,22 @@ class FacebookIE(InfoExtractor):
             'thumbnail': r're:^https?://.*',
             'timestamp': 1701975646,
         },
+    }, {
+        # data.node.comet_sections.content.story.attachments[].styles.attachment.media
+        'url': 'https://www.facebook.com/permalink.php?story_fbid=pfbid0fqQuVEQyXRa9Dp4RcaTR14KHU3uULHV1EK7eckNXSH63JMuoALsAvVCJ97zAGitil&id=100068861234290',
+        'info_dict': {
+            'id': '270103405756416',
+            'ext': 'mp4',
+            'title': 'Lela Evans',
+            'description': 'Today Makkovik\'s own Pilot Mandy Smith made her inaugural landing on the airstrip in her hometown. What a proud moment as we all cheered and...',
+            'thumbnail': r're:^https?://.*',
+            'uploader': 'Lela Evans',
+            'uploader_id': 'pfbid0shZJipuigyy5mqrUJn9ub5LJFWNHvan5prtyi3LrDuuuJ4NwrURgnQHYR9fywBepl',
+            'upload_date': '20231228',
+            'timestamp': 1703804085,
+            'duration': 394.347,
+            'view_count': int,
+        },
     }, {
         'url': 'https://www.facebook.com/story.php?story_fbid=pfbid0Fnzhm8UuzjBYpPMNFzaSpFE9UmLdU4fJN8qTANi1Dmtj5q7DNrL5NERXfsAzDEV7l&id=100073071055552',
         'only_matching': True,
@@ -380,6 +400,18 @@ class FacebookIE(InfoExtractor):
         },
         'playlist_count': 1,
         'skip': 'Requires logging in',
+    }, {
+        # data.event.cover_media_renderer.cover_video
+        'url': 'https://m.facebook.com/events/1509582499515440',
+        'info_dict': {
+            'id': '637246984455045',
+            'ext': 'mp4',
+            'title': 'ANALISI IN CAMPO OSCURO " Coaguli nel sangue dei vaccinati"',
+            'description': 'Other event by Comitato Liberi Pensatori on Tuesday, October 18 2022',
+            'thumbnail': r're:^https?://.*',
+            'uploader': 'Comitato Liberi Pensatori',
+            'uploader_id': '100065709540881',
+        },
     }]
     _SUPPORTED_PAGLETS_REGEX = r'(?:pagelet_group_mall|permalink_video_pagelet|hyperfeed_story_id_[0-9a-f]+)'
     _api_config = {
@@ -454,38 +486,10 @@ def extract_metadata(webpage):
                 r'data-sjs>({.*?ScheduledServerJS.*?})</script>', webpage)]
             post = traverse_obj(post_data, (
                 ..., 'require', ..., ..., ..., '__bbox', 'require', ..., ..., ..., '__bbox', 'result', 'data'), expected_type=dict) or []
-
-            automatic_captions, subtitles = {}, {}
-            subs_data = traverse_obj(post, (..., 'video', ..., 'attachments', ..., lambda k, v: (
-                k == 'media' and str(v['id']) == video_id and v['__typename'] == 'Video')))
-            is_video_broadcast = get_first(subs_data, 'is_video_broadcast', expected_type=bool)
-            captions = get_first(subs_data, 'video_available_captions_locales', 'captions_url')
-            if url_or_none(captions):  # if subs_data only had a 'captions_url'
-                locale = self._html_search_meta(['og:locale', 'twitter:locale'], webpage, 'locale', default='en_US')
-                subtitles[locale] = [{'url': captions}]
-            # or else subs_data had 'video_available_captions_locales', a list of dicts
-            for caption in traverse_obj(captions, (
-                {lambda x: sorted(x, key=lambda c: c['locale'])}, lambda _, v: v['captions_url'])
-            ):
-                lang = caption.get('localized_language') or ''
-                subs = {
-                    'url': caption['captions_url'],
-                    'name': format_field(caption, 'localized_country', f'{lang} (%s)', default=lang),
-                }
-                if caption.get('localized_creation_method') or is_video_broadcast:
-                    automatic_captions.setdefault(caption['locale'], []).append(subs)
-                else:
-                    subtitles.setdefault(caption['locale'], []).append(subs)
-
             media = traverse_obj(post, (..., 'attachments', ..., lambda k, v: (
                 k == 'media' and str(v['id']) == video_id and v['__typename'] == 'Video')), expected_type=dict)
             title = get_first(media, ('title', 'text'))
             description = get_first(media, ('creation_story', 'comet_sections', 'message', 'story', 'message', 'text'))
-            uploader_data = (
-                get_first(media, ('owner', {dict}))
-                or get_first(post, (..., 'video', lambda k, v: k == 'owner' and v['name']))
-                or get_first(post, ('node', 'actors', ..., {dict})) or {})
-
             page_title = title or self._html_search_regex((
                 r'<h2\s+[^>]*class="uiHeaderTitle"[^>]*>(?P<content>[^<]*)</h2>',
                 r'(?s)<span class="fbPhotosPhotoCaption".*?id="fbPhotoPageCaption"><span class="hasCaption">(?P<content>.*?)</span>',
@@ -494,11 +498,15 @@ def extract_metadata(webpage):
             description = description or self._html_search_meta(
                 ['description', 'og:description', 'twitter:description'],
                 webpage, 'description', default=None)
+            uploader_data = (
+                get_first(media, ('owner', {dict}))
+                or get_first(post, (..., 'video', lambda k, v: k == 'owner' and v['name']))
+                or get_first(post, ('node', 'actors', ..., {dict}))
+                or get_first(post, ('event', 'event_creator', {dict})) or {})
             uploader = uploader_data.get('name') or (
                 clean_html(get_element_by_id('fbPhotoPageAuthorName', webpage))
                 or self._search_regex(
                     (r'ownerName\s*:\s*"([^"]+)"', *self._og_regexes('title')), webpage, 'uploader', fatal=False))
-
             timestamp = int_or_none(self._search_regex(
                 r'<abbr[^>]+data-utime=["\'](\d+)', webpage,
                 'timestamp', default=None))
@@ -520,8 +528,6 @@ def extract_metadata(webpage):
                     webpage, 'view count', default=None)),
                 'concurrent_view_count': get_first(post, (
                     ('video', (..., ..., 'attachments', ..., 'media')), 'liveViewerCount', {int_or_none})),
-                'automatic_captions': automatic_captions,
-                'subtitles': subtitles,
             }
 
             info_json_ld = self._search_json_ld(webpage, video_id, default={})
@@ -563,7 +569,11 @@ def process_formats(info):
             # Downloads with browser's User-Agent are rate limited. Working around
             # with non-browser User-Agent.
             for f in info['formats']:
+                # Downloads with browser's User-Agent are rate limited. Working around
+                # with non-browser User-Agent.
                 f.setdefault('http_headers', {})['User-Agent'] = 'facebookexternalhit/1.1'
+                # Formats larger than ~500MB will return error 403 unless chunk size is regulated
+                f.setdefault('downloader_options', {})['http_chunk_size'] = 250 << 20
 
         def extract_relay_data(_filter):
             return self._parse_json(self._search_regex(
@@ -615,6 +625,29 @@ def parse_graphql_video(video):
                                 'url': playable_url,
                             })
                     extract_dash_manifest(video, formats)
+
+                    automatic_captions, subtitles = {}, {}
+                    is_broadcast = traverse_obj(video, ('is_video_broadcast', {bool}))
+                    for caption in traverse_obj(video, (
+                        'video_available_captions_locales',
+                        {lambda x: sorted(x, key=lambda c: c['locale'])},
+                        lambda _, v: url_or_none(v['captions_url'])
+                    )):
+                        lang = caption.get('localized_language') or 'und'
+                        subs = {
+                            'url': caption['captions_url'],
+                            'name': format_field(caption, 'localized_country', f'{lang} (%s)', default=lang),
+                        }
+                        if caption.get('localized_creation_method') or is_broadcast:
+                            automatic_captions.setdefault(caption['locale'], []).append(subs)
+                        else:
+                            subtitles.setdefault(caption['locale'], []).append(subs)
+                    captions_url = traverse_obj(video, ('captions_url', {url_or_none}))
+                    if captions_url and not automatic_captions and not subtitles:
+                        locale = self._html_search_meta(
+                            ['og:locale', 'twitter:locale'], webpage, 'locale', default='en_US')
+                        (automatic_captions if is_broadcast else subtitles)[locale] = [{'url': captions_url}]
+
                     info = {
                         'id': v_id,
                         'formats': formats,
@@ -624,6 +657,8 @@ def parse_graphql_video(video):
                         'timestamp': traverse_obj(video, 'publish_time', 'creation_time', expected_type=int_or_none),
                         'duration': (float_or_none(video.get('playable_duration_in_ms'), 1000)
                                      or float_or_none(video.get('length_in_second'))),
+                        'automatic_captions': automatic_captions,
+                        'subtitles': subtitles,
                     }
                     process_formats(info)
                     description = try_get(video, lambda x: x['savable_description']['text'])
@@ -658,7 +693,8 @@ def parse_attachment(attachment, key='media'):
                 for edge in edges:
                     parse_attachment(edge, key='node')
 
-                video = data.get('video') or {}
+                video = traverse_obj(data, (
+                    'event', 'cover_media_renderer', 'cover_video'), 'video', expected_type=dict) or {}
                 if video:
                     attachments = try_get(video, [
                         lambda x: x['story']['attachments'],
@@ -677,6 +713,9 @@ def parse_attachment(attachment, key='media'):
                 # honor precise duration in video info
                 if video_info.get('duration'):
                     webpage_info['duration'] = video_info['duration']
+                # preserve preferred_thumbnail in video info
+                if video_info.get('thumbnail'):
+                    webpage_info['thumbnail'] = video_info['thumbnail']
                 return merge_dicts(webpage_info, video_info)
 
         if not video_data:
@@ -907,3 +946,114 @@ def _real_extract(self, url):
         video_id = self._match_id(url)
         return self.url_result(
             f'https://m.facebook.com/watch/?v={video_id}&_rdr', FacebookIE, video_id)
+
+
+class FacebookAdsIE(InfoExtractor):
+    _VALID_URL = r'https?://(?:[\w-]+\.)?facebook\.com/ads/library/?\?(?:[^#]+&)?id=(?P<id>\d+)'
+    IE_NAME = 'facebook:ads'
+
+    _TESTS = [{
+        'url': 'https://www.facebook.com/ads/library/?id=899206155126718',
+        'info_dict': {
+            'id': '899206155126718',
+            'ext': 'mp4',
+            'title': 'video by Kandao',
+            'uploader': 'Kandao',
+            'uploader_id': '774114102743284',
+            'uploader_url': r're:^https?://.*',
+            'timestamp': 1702548330,
+            'thumbnail': r're:^https?://.*',
+            'upload_date': '20231214',
+            'like_count': int,
+        }
+    }, {
+        'url': 'https://www.facebook.com/ads/library/?id=893637265423481',
+        'info_dict': {
+            'id': '893637265423481',
+            'title': 'Jusqu\u2019\u00e0 -25% sur une s\u00e9lection de vins p\u00e9tillants italiens ',
+            'uploader': 'Eataly Paris Marais',
+            'uploader_id': '2086668958314152',
+            'uploader_url': r're:^https?://.*',
+            'timestamp': 1703571529,
+            'upload_date': '20231226',
+            'like_count': int,
+        },
+        'playlist_count': 3,
+    }, {
+        'url': 'https://es-la.facebook.com/ads/library/?id=901230958115569',
+        'only_matching': True,
+    }, {
+        'url': 'https://m.facebook.com/ads/library/?id=901230958115569',
+        'only_matching': True,
+    }]
+
+    _FORMATS_MAP = {
+        'watermarked_video_sd_url': ('sd-wmk', 'SD, watermarked'),
+        'video_sd_url': ('sd', None),
+        'watermarked_video_hd_url': ('hd-wmk', 'HD, watermarked'),
+        'video_hd_url': ('hd', None),
+    }
+
+    def _extract_formats(self, video_dict):
+        formats = []
+        for format_key, format_url in traverse_obj(video_dict, (
+            {dict.items}, lambda _, v: v[0] in self._FORMATS_MAP and url_or_none(v[1])
+        )):
+            formats.append({
+                'format_id': self._FORMATS_MAP[format_key][0],
+                'format_note': self._FORMATS_MAP[format_key][1],
+                'url': format_url,
+                'ext': 'mp4',
+                'quality': qualities(tuple(self._FORMATS_MAP))(format_key),
+            })
+        return formats
+
+    def _real_extract(self, url):
+        video_id = self._match_id(url)
+        webpage = self._download_webpage(url, video_id)
+
+        post_data = [self._parse_json(j, video_id, fatal=False)
+                     for j in re.findall(r's\.handle\(({.*})\);requireLazy\(', webpage)]
+        data = traverse_obj(post_data, (
+            ..., 'require', ..., ..., ..., 'props', 'deeplinkAdCard', 'snapshot', {dict}), get_all=False)
+        if not data:
+            raise ExtractorError('Unable to extract ad data')
+
+        title = data.get('title')
+        if not title or title == '{{product.name}}':
+            title = join_nonempty('display_format', 'page_name', delim=' by ', from_dict=data)
+
+        info_dict = traverse_obj(data, {
+            'description': ('link_description', {str}, {lambda x: x if x != '{{product.description}}' else None}),
+            'uploader': ('page_name', {str}),
+            'uploader_id': ('page_id', {str_or_none}),
+            'uploader_url': ('page_profile_uri', {url_or_none}),
+            'timestamp': ('creation_time', {int_or_none}),
+            'like_count': ('page_like_count', {int_or_none}),
+        })
+
+        entries = []
+        for idx, entry in enumerate(traverse_obj(
+            data, (('videos', 'cards'), lambda _, v: any([url_or_none(v[f]) for f in self._FORMATS_MAP]))), 1
+        ):
+            entries.append({
+                'id': f'{video_id}_{idx}',
+                'title': entry.get('title') or title,
+                'description': entry.get('link_description') or info_dict.get('description'),
+                'thumbnail': url_or_none(entry.get('video_preview_image_url')),
+                'formats': self._extract_formats(entry),
+            })
+
+        if len(entries) == 1:
+            info_dict.update(entries[0])
+
+        elif len(entries) > 1:
+            info_dict.update({
+                'title': entries[0]['title'],
+                'entries': entries,
+                '_type': 'playlist',
+            })
+
+        info_dict['id'] = video_id
+
+        return info_dict
diff --git a/yt_dlp/extractor/floatplane.py b/yt_dlp/extractor/floatplane.py
index 2cf4d4e64..8676d73f6 100644
--- a/yt_dlp/extractor/floatplane.py
+++ b/yt_dlp/extractor/floatplane.py
@@ -11,6 +11,7 @@
     join_nonempty,
     parse_codecs,
     parse_iso8601,
+    url_or_none,
     urljoin,
 )
 from ..utils.traversal import traverse_obj
@@ -108,6 +109,64 @@ class FloatplaneIE(InfoExtractor):
             'availability': 'subscriber_only',
         },
         'params': {'skip_download': 'm3u8'},
+    }, {
+        'url': 'https://www.floatplane.com/post/65B5PNoBtf',
+        'info_dict': {
+            'id': '65B5PNoBtf',
+            'description': 'I recorded the inbuilt demo mode for your 90\'s enjoyment, thanks for being Floaties!',
+            'display_id': '65B5PNoBtf',
+            'like_count': int,
+            'release_timestamp': 1701249480,
+            'uploader': 'The Trash Network',
+            'availability': 'subscriber_only',
+            'uploader_id': '61bc20c9a131fb692bf2a513',
+            'uploader_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home',
+            'channel_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home/thedrumthing',
+            'comment_count': int,
+            'title': 'The $50 electronic drum kit.',
+            'channel_id': '64424fe73cd58cbcf8d8e131',
+            'thumbnail': 'https://pbs.floatplane.com/blogPost_thumbnails/65B5PNoBtf/725555379422705_1701247052743.jpeg',
+            'dislike_count': int,
+            'channel': 'The Drum Thing',
+            'release_date': '20231129',
+        },
+        'playlist_count': 2,
+        'playlist': [{
+            'info_dict': {
+                'id': 'ISPJjexylS',
+                'ext': 'mp4',
+                'release_date': '20231129',
+                'release_timestamp': 1701249480,
+                'title': 'The $50 electronic drum kit. .mov',
+                'channel_id': '64424fe73cd58cbcf8d8e131',
+                'thumbnail': 'https://pbs.floatplane.com/video_thumbnails/ISPJjexylS/335202812134041_1701249383392.jpeg',
+                'availability': 'subscriber_only',
+                'uploader': 'The Trash Network',
+                'duration': 622,
+                'channel': 'The Drum Thing',
+                'uploader_id': '61bc20c9a131fb692bf2a513',
+                'channel_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home/thedrumthing',
+                'uploader_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home',
+            },
+        }, {
+            'info_dict': {
+                'id': 'qKfxu6fEpu',
+                'ext': 'aac',
+                'release_date': '20231129',
+                'release_timestamp': 1701249480,
+                'title': 'Roland TD-7 Demo.m4a',
+                'channel_id': '64424fe73cd58cbcf8d8e131',
+                'availability': 'subscriber_only',
+                'uploader': 'The Trash Network',
+                'duration': 114,
+                'channel': 'The Drum Thing',
+                'uploader_id': '61bc20c9a131fb692bf2a513',
+                'channel_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home/thedrumthing',
+                'uploader_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home',
+            },
+        }],
+        'skip': 'requires subscription: "The Trash Network"',
+        'params': {'skip_download': 'm3u8'},
     }]
 
     def _real_initialize(self):
@@ -124,6 +183,22 @@ def _real_extract(self, url):
         if not any(traverse_obj(post_data, ('metadata', ('hasVideo', 'hasAudio')))):
             raise ExtractorError('Post does not contain a video or audio track', expected=True)
 
+        uploader_url = format_field(
+            post_data, [('creator', 'urlname')], 'https://www.floatplane.com/channel/%s/home') or None
+
+        common_info = {
+            'uploader_url': uploader_url,
+            'channel_url': urljoin(f'{uploader_url}/', traverse_obj(post_data, ('channel', 'urlname'))),
+            'availability': self._availability(needs_subscription=True),
+            **traverse_obj(post_data, {
+                'uploader': ('creator', 'title', {str}),
+                'uploader_id': ('creator', 'id', {str}),
+                'channel': ('channel', 'title', {str}),
+                'channel_id': ('channel', 'id', {str}),
+                'release_timestamp': ('releaseDate', {parse_iso8601}),
+            }),
+        }
+
         items = []
         for media in traverse_obj(post_data, (('videoAttachments', 'audioAttachments'), ...)):
             media_id = media['id']
@@ -150,11 +225,11 @@ def format_path(params):
             formats = []
             for quality in traverse_obj(stream, ('resource', 'data', 'qualityLevels', ...)):
                 url = urljoin(stream['cdn'], format_path(traverse_obj(
-                    stream, ('resource', 'data', 'qualityLevelParams', quality['name']))))
+                    stream, ('resource', 'data', 'qualityLevelParams', quality['name'], {dict}))))
                 formats.append({
                     **traverse_obj(quality, {
-                        'format_id': 'name',
-                        'format_note': 'label',
+                        'format_id': ('name', {str}),
+                        'format_note': ('label', {str}),
                         'width': ('width', {int}),
                         'height': ('height', {int}),
                     }),
@@ -164,38 +239,28 @@ def format_path(params):
                 })
 
             items.append({
+                **common_info,
                 'id': media_id,
                 **traverse_obj(metadata, {
-                    'title': 'title',
+                    'title': ('title', {str}),
                     'duration': ('duration', {int_or_none}),
-                    'thumbnail': ('thumbnail', 'path'),
+                    'thumbnail': ('thumbnail', 'path', {url_or_none}),
                 }),
                 'formats': formats,
             })
 
-        uploader_url = format_field(
-            post_data, [('creator', 'urlname')], 'https://www.floatplane.com/channel/%s/home') or None
-        channel_url = urljoin(f'{uploader_url}/', traverse_obj(post_data, ('channel', 'urlname')))
-
         post_info = {
+            **common_info,
             'id': post_id,
             'display_id': post_id,
             **traverse_obj(post_data, {
-                'title': 'title',
+                'title': ('title', {str}),
                 'description': ('text', {clean_html}),
-                'uploader': ('creator', 'title'),
-                'uploader_id': ('creator', 'id'),
-                'channel': ('channel', 'title'),
-                'channel_id': ('channel', 'id'),
                 'like_count': ('likes', {int_or_none}),
                 'dislike_count': ('dislikes', {int_or_none}),
                 'comment_count': ('comments', {int_or_none}),
-                'release_timestamp': ('releaseDate', {parse_iso8601}),
-                'thumbnail': ('thumbnail', 'path'),
+                'thumbnail': ('thumbnail', 'path', {url_or_none}),
             }),
-            'uploader_url': uploader_url,
-            'channel_url': channel_url,
-            'availability': self._availability(needs_subscription=True),
         }
 
         if len(items) > 1:
diff --git a/yt_dlp/extractor/gofile.py b/yt_dlp/extractor/gofile.py
index ef14b57d0..eb1dcf85f 100644
--- a/yt_dlp/extractor/gofile.py
+++ b/yt_dlp/extractor/gofile.py
@@ -66,7 +66,7 @@ def _entries(self, file_id):
         query_params = {
             'contentId': file_id,
             'token': self._TOKEN,
-            'websiteToken': '7fd94ds12fds4',  # From https://gofile.io/dist/js/alljs.js
+            'wt': '4fd6sg89d7s6',  # From https://gofile.io/dist/js/alljs.js
         }
         password = self.get_param('videopassword')
         if password:
diff --git a/yt_dlp/extractor/lsm.py b/yt_dlp/extractor/lsm.py
new file mode 100644
index 000000000..35a831fa2
--- /dev/null
+++ b/yt_dlp/extractor/lsm.py
@@ -0,0 +1,282 @@
+import re
+import urllib.parse
+
+from .common import InfoExtractor
+from ..utils import (
+    ExtractorError,
+    determine_ext,
+    int_or_none,
+    js_to_json,
+    parse_iso8601,
+    parse_qs,
+    str_or_none,
+    url_or_none,
+    urljoin,
+)
+from ..utils.traversal import traverse_obj
+
+
+class LSMLREmbedIE(InfoExtractor):
+    _VALID_URL = r'''(?x)
+        https?://(?:
+            (?:latvijasradio|lr1|lr2|klasika|lr4|naba|radioteatris)\.lsm|
+            pieci
+        )\.lv/[^/?#]+/(?:
+            pleijeris|embed
+        )/?\?(?:[^#]+&)?(?:show|id)=(?P<id>\d+)'''
+    _TESTS = [{
+        'url': 'https://latvijasradio.lsm.lv/lv/embed/?theme=black&size=16x9&showCaptions=0&id=183522',
+        'md5': '719b33875cd1429846eeeaeec6df2830',
+        'info_dict': {
+            'id': 'a342781',
+            'ext': 'mp3',
+            'duration': 1823,
+            'title': '#138 Nepilnīgā kompensējamo zāļu sistēma pat mēnešiem dzenā pacientus pa aptiekām',
+            'thumbnail': 'https://pic.latvijasradio.lv/public/assets/media/9/d/gallery_fd4675ac.jpg',
+        }
+    }, {
+        'url': 'https://radioteatris.lsm.lv/lv/embed/?id=&show=1270&theme=white&size=16x9',
+        'info_dict': {
+            'id': '1270',
+        },
+        'playlist_count': 3,
+        'playlist': [{
+            'md5': '2e61b6eceff00d14d57fdbbe6ab24cac',
+            'info_dict': {
+                'id': 'a297397',
+                'ext': 'mp3',
+                'title': 'Eriks Emanuels Šmits "Pilāta evaņģēlijs". 1. daļa',
+                'thumbnail': 'https://radioteatris.lsm.lv/public/assets/shows/62f131ae81e3c.jpg',
+                'duration': 3300,
+            },
+        }],
+    }, {
+        'url': 'https://radioteatris.lsm.lv/lv/embed/?id=&show=1269&theme=white&size=16x9',
+        'md5': '24810d4a961da2295d9860afdcaf4f5a',
+        'info_dict': {
+            'id': 'a230690',
+            'ext': 'mp3',
+            'title': 'Jens Ahlboms "Spārni". Radioizrāde ar Mārtiņa Freimaņa mūziku',
+            'thumbnail': 'https://radioteatris.lsm.lv/public/assets/shows/62f13023a457c.jpg',
+            'duration': 1788,
+        }
+    }, {
+        'url': 'https://lr1.lsm.lv/lv/embed/?id=166557&show=0&theme=white&size=16x9',
+        'info_dict': {
+            'id': '166557',
+        },
+        'playlist_count': 2,
+        'playlist': [{
+            'md5': '6a8b0927572f443f09c6e50a3ad65f2d',
+            'info_dict': {
+                'id': 'a303104',
+                'ext': 'mp3',
+                'thumbnail': 'https://pic.latvijasradio.lv/public/assets/media/c/5/gallery_a83ad2c2.jpg',
+                'title': 'Krustpunktā Lielā intervija: Valsts prezidents Egils Levits',
+                'duration': 3222,
+            },
+        }, {
+            'md5': '5d5e191e718b7644e5118b7b4e093a6d',
+            'info_dict': {
+                'id': 'v303104',
+                'ext': 'mp4',
+                'thumbnail': 'https://pic.latvijasradio.lv/public/assets/media/c/5/gallery_a83ad2c2.jpg',
+                'title': 'Krustpunktā Lielā intervija: Valsts prezidents Egils Levits - Video Version',
+                'duration': 3222,
+            },
+        }],
+    }, {
+        'url': 'https://lr1.lsm.lv/lv/embed/?id=183522&show=0&theme=white&size=16x9',
+        'only_matching': True,
+    }, {
+        'url': 'https://lr2.lsm.lv/lv/embed/?id=182126&show=0&theme=white&size=16x9',
+        'only_matching': True,
+    }, {
+        'url': 'https://klasika.lsm.lv/lv/embed/?id=110806&show=0&theme=white&size=16x9',
+        'only_matching': True,
+    }, {
+        'url': 'https://lr4.lsm.lv/lv/embed/?id=184282&show=0&theme=white&size=16x9',
+        'only_matching': True,
+    }, {
+        'url': 'https://pieci.lv/lv/embed/?id=168896&show=0&theme=white&size=16x9',
+        'only_matching': True,
+    }, {
+        'url': 'https://naba.lsm.lv/lv/embed/?id=182901&show=0&theme=white&size=16x9',
+        'only_matching': True,
+    }, {
+        'url': 'https://radioteatris.lsm.lv/lv/embed/?id=176439&show=0&theme=white&size=16x9',
+        'only_matching': True,
+    }, {
+        'url': 'https://lr1.lsm.lv/lv/pleijeris/?embed=0&id=48205&time=00%3A00&idx=0',
+        'only_matching': True,
+    }]
+
+    def _real_extract(self, url):
+        query = parse_qs(url)
+        video_id = traverse_obj(query, (
+            ('show', 'id'), 0, {int_or_none}, {lambda x: x or None}, {str_or_none}), get_all=False)
+        webpage = self._download_webpage(url, video_id)
+
+        player_data, media_data = self._search_regex(
+            r'LR\.audio\.Player\s*\([^{]*(?P<player>\{.*?\}),(?P<media>\{.*\})\);',
+            webpage, 'player json', group=('player', 'media'))
+
+        player_json = self._parse_json(
+            player_data, video_id, transform_source=js_to_json, fatal=False) or {}
+        media_json = self._parse_json(media_data, video_id, transform_source=js_to_json)
+
+        entries = []
+        for item in traverse_obj(media_json, (('audio', 'video'), lambda _, v: v['id'])):
+            formats = []
+            for source_url in traverse_obj(item, ('sources', ..., 'file', {url_or_none})):
+                if determine_ext(source_url) == 'm3u8':
+                    formats.extend(self._extract_m3u8_formats(source_url, video_id, fatal=False))
+                else:
+                    formats.append({'url': source_url})
+
+            id_ = item['id']
+            title = item.get('title')
+            if id_.startswith('v') and not title:
+                title = traverse_obj(
+                    media_json, ('audio', lambda _, v: v['id'][1:] == id_[1:], 'title',
+                                 {lambda x: x and f'{x} - Video Version'}), get_all=False)
+
+            entries.append({
+                'formats': formats,
+                'thumbnail': urljoin(url, player_json.get('poster')),
+                'id': id_,
+                'title': title,
+                'duration': traverse_obj(item, ('duration', {int_or_none})),
+            })
+
+        if len(entries) == 1:
+            return entries[0]
+
+        return self.playlist_result(entries, video_id)
+
+
+class LSMLTVEmbedIE(InfoExtractor):
+    _VALID_URL = r'https?://ltv\.lsm\.lv/embed\?(?:[^#]+&)?c=(?P<id>[^#&]+)'
+    _TESTS = [{
+        'url': 'https://ltv.lsm.lv/embed?c=eyJpdiI6IjQzbHVUeHAyaDJiamFjcjdSUUFKdnc9PSIsInZhbHVlIjoiMHl3SnJNRmd2TmFIdnZwOGtGUUpzODFzUEZ4SVVsN2xoRjliSW9vckUyMWZIWG8vbWVzaFFkY0lhNmRjbjRpaCIsIm1hYyI6ImMzNjdhMzFhNTFhZmY1ZmE0NWI5YmFjZGI1YmJiNGEyNjgzNDM4MjUzMWEwM2FmMDMyZDMwYWM1MDFjZmM5MGIiLCJ0YWciOiIifQ==',
+        'md5': '64f72a360ca530d5ed89c77646c9eee5',
+        'info_dict': {
+            'id': '46k_d23-6000-105',
+            'ext': 'mp4',
+            'timestamp': 1700589151,
+            'duration': 1442,
+            'upload_date': '20231121',
+            'title': 'D23-6000-105_cetstud',
+            'thumbnail': 'https://store.cloudycdn.services/tmsp00060/assets/media/660858/placeholder1700589200.jpg',
+        }
+    }, {
+        'url': 'https://ltv.lsm.lv/embed?enablesdkjs=1&c=eyJpdiI6IncwVzZmUFk2MU12enVWK1I3SUcwQ1E9PSIsInZhbHVlIjoid3FhV29vamc3T2sxL1RaRmJ5Rm1GTXozU0o2dVczdUtLK0cwZEZJMDQ2a3ZIRG5DK2pneGlnbktBQy9uazVleHN6VXhxdWIweWNvcHRDSnlISlNYOHlVZ1lpcTUrcWZSTUZPQW14TVdkMW9aOUtRWVNDcFF4eWpHNGcrT0VZbUNFQStKQk91cGpndW9FVjJIa0lpbkh3PT0iLCJtYWMiOiIyZGI1NDJlMWRlM2QyMGNhOGEwYTM2MmNlN2JlOGRhY2QyYjdkMmEzN2RlOTEzYTVkNzI1ODlhZDlhZjU4MjQ2IiwidGFnIjoiIn0=',
+        'md5': 'a1711e190fe680fdb68fd8413b378e87',
+        'info_dict': {
+            'id': 'wUnFArIPDSY',
+            'ext': 'mp4',
+            'uploader': 'LTV_16plus',
+            'release_date': '20220514',
+            'channel_url': 'https://www.youtube.com/channel/UCNMrnafwXD2XKeeQOyfkFCw',
+            'view_count': int,
+            'availability': 'public',
+            'thumbnail': 'https://i.ytimg.com/vi/wUnFArIPDSY/maxresdefault.jpg',
+            'release_timestamp': 1652544074,
+            'title': 'EIROVĪZIJA SALĀTOS',
+            'live_status': 'was_live',
+            'uploader_id': '@LTV16plus',
+            'comment_count': int,
+            'channel_id': 'UCNMrnafwXD2XKeeQOyfkFCw',
+            'channel_follower_count': int,
+            'categories': ['Entertainment'],
+            'duration': 5269,
+            'upload_date': '20220514',
+            'age_limit': 0,
+            'channel': 'LTV_16plus',
+            'playable_in_embed': True,
+            'tags': [],
+            'uploader_url': 'https://www.youtube.com/@LTV16plus',
+            'like_count': int,
+            'description': 'md5:7ff0c42ba971e3c13e4b8a2ff03b70b5',
+        }
+    }]
+
+    def _real_extract(self, url):
+        video_id = urllib.parse.unquote(self._match_id(url))
+        webpage = self._download_webpage(url, video_id)
+        data = self._search_json(
+            r'window\.ltvEmbedPayload\s*=', webpage, 'embed json', video_id)
+        embed_type = traverse_obj(data, ('source', 'name', {str}))
+
+        if embed_type == 'telia':
+            ie_key = 'CloudyCDN'
+            embed_url = traverse_obj(data, ('source', 'embed_url', {url_or_none}))
+        elif embed_type == 'youtube':
+            ie_key = 'Youtube'
+            embed_url = traverse_obj(data, ('source', 'id', {str}))
+        else:
+            raise ExtractorError(f'Unsupported embed type {embed_type!r}')
+
+        return self.url_result(
+            embed_url, ie_key, video_id, **traverse_obj(data, {
+                'title': ('parentInfo', 'title'),
+                'duration': ('parentInfo', 'duration', {int_or_none}),
+                'thumbnail': ('source', 'poster', {url_or_none}),
+            }))
+
+
+class LSMReplayIE(InfoExtractor):
+    _VALID_URL = r'https?://replay\.lsm\.lv/[^/?#]+/(?:ieraksts|statja)/[^/?#]+/(?P<id>\d+)'
+    _TESTS = [{
+        'url': 'https://replay.lsm.lv/lv/ieraksts/ltv/311130/4-studija-zolitudes-tragedija-un-incupes-stacija',
+        'md5': '64f72a360ca530d5ed89c77646c9eee5',
+        'info_dict': {
+            'id': '46k_d23-6000-105',
+            'ext': 'mp4',
+            'timestamp': 1700586300,
+            'description': 'md5:0f1b14798cc39e1ae578bd0eb268f759',
+            'duration': 1442,
+            'upload_date': '20231121',
+            'title': '4. studija. Zolitūdes traģēdija un Inčupes stacija',
+            'thumbnail': 'https://ltv.lsm.lv/storage/media/8/7/large/5/1f9604e1.jpg',
+        }
+    }, {
+        'url': 'https://replay.lsm.lv/lv/ieraksts/lr/183522/138-nepilniga-kompensejamo-zalu-sistema-pat-menesiem-dzena-pacientus-pa-aptiekam',
+        'md5': '719b33875cd1429846eeeaeec6df2830',
+        'info_dict': {
+            'id': 'a342781',
+            'ext': 'mp3',
+            'duration': 1823,
+            'title': '#138 Nepilnīgā kompensējamo zāļu sistēma pat mēnešiem dzenā pacientus pa aptiekām',
+            'thumbnail': 'https://pic.latvijasradio.lv/public/assets/media/9/d/large_fd4675ac.jpg',
+            'upload_date': '20231102',
+            'timestamp': 1698921060,
+            'description': 'md5:7bac3b2dd41e44325032943251c357b1',
+        }
+    }, {
+        'url': 'https://replay.lsm.lv/ru/statja/ltv/311130/4-studija-zolitudes-tragedija-un-incupes-stacija',
+        'only_matching': True,
+    }]
+
+    def _fix_nuxt_data(self, webpage):
+        return re.sub(r'Object\.create\(null(?:,(\{.+\}))?\)', lambda m: m.group(1) or 'null', webpage)
+
+    def _real_extract(self, url):
+        video_id = self._match_id(url)
+        webpage = self._download_webpage(url, video_id)
+
+        data = self._search_nuxt_data(
+            self._fix_nuxt_data(webpage), video_id, context_name='__REPLAY__')
+
+        return {
+            '_type': 'url_transparent',
+            'id': video_id,
+            **traverse_obj(data, {
+                'url': ('playback', 'service', 'url', {url_or_none}),
+                'title': ('mediaItem', 'title'),
+                'description': ('mediaItem', ('lead', 'body')),
+                'duration': ('mediaItem', 'duration', {int_or_none}),
+                'timestamp': ('mediaItem', 'aired_at', {parse_iso8601}),
+                'thumbnail': ('mediaItem', 'largeThumbnail', {url_or_none}),
+            }, get_all=False),
+        }
diff --git a/yt_dlp/extractor/medaltv.py b/yt_dlp/extractor/medaltv.py
index 9e57ee21a..eeb5b85f3 100644
--- a/yt_dlp/extractor/medaltv.py
+++ b/yt_dlp/extractor/medaltv.py
@@ -8,7 +8,8 @@
     float_or_none,
     int_or_none,
     str_or_none,
-    traverse_obj
+    traverse_obj,
+    update_url_query,
 )
 
 
@@ -16,7 +17,7 @@ class MedalTVIE(InfoExtractor):
     _VALID_URL = r'https?://(?:www\.)?medal\.tv/games/[^/?#&]+/clips/(?P<id>[^/?#&]+)'
     _TESTS = [{
         'url': 'https://medal.tv/games/valorant/clips/jTBFnLKdLy15K',
-        'md5': '6930f8972914b6b9fdc2bb3918098ba0',
+        'md5': '03e4911fdcf7fce563090705c2e79267',
         'info_dict': {
             'id': 'jTBFnLKdLy15K',
             'ext': 'mp4',
@@ -33,8 +34,8 @@ class MedalTVIE(InfoExtractor):
             'duration': 13,
         }
     }, {
-        'url': 'https://medal.tv/games/cod%20cold%20war/clips/2mA60jWAGQCBH',
-        'md5': '3d19d426fe0b2d91c26e412684e66a06',
+        'url': 'https://medal.tv/games/cod-cold-war/clips/2mA60jWAGQCBH',
+        'md5': 'fc7a3e4552ae8993c1c4006db46be447',
         'info_dict': {
             'id': '2mA60jWAGQCBH',
             'ext': 'mp4',
@@ -52,7 +53,7 @@ class MedalTVIE(InfoExtractor):
             'duration': 23,
         }
     }, {
-        'url': 'https://medal.tv/games/cod%20cold%20war/clips/2um24TWdty0NA',
+        'url': 'https://medal.tv/games/cod-cold-war/clips/2um24TWdty0NA',
         'md5': 'b6dc76b78195fff0b4f8bf4a33ec2148',
         'info_dict': {
             'id': '2um24TWdty0NA',
@@ -81,7 +82,7 @@ class MedalTVIE(InfoExtractor):
     def _real_extract(self, url):
         video_id = self._match_id(url)
 
-        webpage = self._download_webpage(url, video_id)
+        webpage = self._download_webpage(update_url_query(url, {'mobilebypass': 'true'}), video_id)
 
         hydration_data = self._search_json(
             r'<script[^>]*>[^<]*\bhydrationData\s*=', webpage,
diff --git a/yt_dlp/extractor/motherless.py b/yt_dlp/extractor/motherless.py
index e359c44e9..160150a7b 100644
--- a/yt_dlp/extractor/motherless.py
+++ b/yt_dlp/extractor/motherless.py
@@ -177,6 +177,7 @@ def _real_extract(self, url):
 
 
 class MotherlessPaginatedIE(InfoExtractor):
+    _EXTRA_QUERY = {}
     _PAGE_SIZE = 60
 
     def _correct_path(self, url, item_id):
@@ -199,7 +200,7 @@ def _real_extract(self, url):
         def get_page(idx):
             page = idx + 1
             current_page = webpage if not idx else self._download_webpage(
-                real_url, item_id, note=f'Downloading page {page}', query={'page': page})
+                real_url, item_id, note=f'Downloading page {page}', query={'page': page, **self._EXTRA_QUERY})
             yield from self._extract_entries(current_page, real_url)
 
         return self.playlist_result(
@@ -213,7 +214,7 @@ class MotherlessGroupIE(MotherlessPaginatedIE):
         'url': 'http://motherless.com/gv/movie_scenes',
         'info_dict': {
             'id': 'movie_scenes',
-            'title': 'Movie Scenes',
+            'title': 'Movie Scenes - Videos - Hot and sexy scenes from "regular" movies... Beautiful actresses fully',
         },
         'playlist_mincount': 540,
     }, {
@@ -244,7 +245,7 @@ class MotherlessGalleryIE(MotherlessPaginatedIE):
             'id': '338999F',
             'title': 'Random',
         },
-        'playlist_mincount': 190,
+        'playlist_mincount': 171,
     }, {
         'url': 'https://motherless.com/GVABD6213',
         'info_dict': {
@@ -270,3 +271,27 @@ class MotherlessGalleryIE(MotherlessPaginatedIE):
 
     def _correct_path(self, url, item_id):
         return urllib.parse.urljoin(url, f'/GV{item_id}')
+
+
+class MotherlessUploaderIE(MotherlessPaginatedIE):
+    _VALID_URL = r'https?://(?:www\.)?motherless\.com/u/(?P<id>\w+)/?(?:$|[?#])'
+    _TESTS = [{
+        'url': 'https://motherless.com/u/Mrgo4hrs2023',
+        'info_dict': {
+            'id': 'Mrgo4hrs2023',
+            'title': "Mrgo4hrs2023's Uploads - Videos",
+        },
+        'playlist_mincount': 32,
+    }, {
+        'url': 'https://motherless.com/u/Happy_couple?t=v',
+        'info_dict': {
+            'id': 'Happy_couple',
+            'title': "Happy_couple's Uploads - Videos",
+        },
+        'playlist_mincount': 8,
+    }]
+
+    _EXTRA_QUERY = {'t': 'v'}
+
+    def _correct_path(self, url, item_id):
+        return urllib.parse.urljoin(url, f'/u/{item_id}?t=v')
diff --git a/yt_dlp/extractor/mx3.py b/yt_dlp/extractor/mx3.py
new file mode 100644
index 000000000..cb9f50e0c
--- /dev/null
+++ b/yt_dlp/extractor/mx3.py
@@ -0,0 +1,171 @@
+import re
+
+from .common import InfoExtractor
+from ..networking import HEADRequest
+from ..utils import (
+    get_element_by_class,
+    int_or_none,
+    try_call,
+    url_or_none,
+    urlhandle_detect_ext,
+)
+from ..utils.traversal import traverse_obj
+
+
+class Mx3BaseIE(InfoExtractor):
+    _VALID_URL_TMPL = r'https?://(?:www\.)?%s/t/(?P<id>\w+)'
+    _FORMATS = [{
+        'url': 'player_asset',
+        'format_id': 'default',
+        'quality': 0,
+    }, {
+        'url': 'player_asset?quality=hd',
+        'format_id': 'hd',
+        'quality': 1,
+    }, {
+        'url': 'download',
+        'format_id': 'download',
+        'quality': 2,
+    }, {
+        'url': 'player_asset?quality=source',
+        'format_id': 'source',
+        'quality': 2,
+    }]
+
+    def _extract_formats(self, track_id):
+        formats = []
+        for fmt in self._FORMATS:
+            format_url = f'https://{self._DOMAIN}/tracks/{track_id}/{fmt["url"]}'
+            urlh = self._request_webpage(
+                HEADRequest(format_url), track_id, fatal=False, expected_status=404,
+                note=f'Checking for format {fmt["format_id"]}')
+            if urlh and urlh.status == 200:
+                formats.append({
+                    **fmt,
+                    'url': format_url,
+                    'ext': urlhandle_detect_ext(urlh),
+                    'filesize': int_or_none(urlh.headers.get('Content-Length')),
+                })
+        return formats
+
+    def _real_extract(self, url):
+        track_id = self._match_id(url)
+        webpage = self._download_webpage(url, track_id)
+        more_info = get_element_by_class('single-more-info', webpage)
+        data = self._download_json(f'https://{self._DOMAIN}/t/{track_id}.json', track_id, fatal=False)
+
+        def get_info_field(name):
+            return self._html_search_regex(
+                rf'<dt[^>]*>\s*{name}\s*</dt>\s*<dd[^>]*>(.*?)</dd>',
+                more_info, name, default=None, flags=re.DOTALL)
+
+        return {
+            'id': track_id,
+            'formats': self._extract_formats(track_id),
+            'genre': self._html_search_regex(
+                r'<div\b[^>]+class="single-band-genre"[^>]*>([^<]+)</div>', webpage, 'genre', default=None),
+            'release_year': int_or_none(get_info_field('Year of creation')),
+            'description': get_info_field('Description'),
+            'tags': try_call(lambda: get_info_field('Tag').split(', '), list),
+            **traverse_obj(data, {
+                'title': ('title', {str}),
+                'artist': (('performer_name', 'artist'), {str}),
+                'album_artist': ('artist', {str}),
+                'composer': ('composer_name', {str}),
+                'thumbnail': (('picture_url_xlarge', 'picture_url'), {url_or_none}),
+            }, get_all=False),
+        }
+
+
+class Mx3IE(Mx3BaseIE):
+    _DOMAIN = 'mx3.ch'
+    _VALID_URL = Mx3BaseIE._VALID_URL_TMPL % re.escape(_DOMAIN)
+    _TESTS = [{
+        'url': 'https://mx3.ch/t/1Cru',
+        'md5': '7ba09e9826b4447d4e1ce9d69e0e295f',
+        'info_dict': {
+            'id': '1Cru',
+            'ext': 'wav',
+            'artist': 'Godina',
+            'album_artist': 'Tortue Tortue',
+            'composer': 'Olivier Godinat',
+            'genre': 'Rock',
+            'thumbnail': 'https://mx3.ch/pictures/mx3/file/0101/4643/square_xlarge/1-s-envoler-1.jpg?1630272813',
+            'title': "S'envoler",
+            'release_year': 2021,
+            'tags': [],
+        }
+    }, {
+        'url': 'https://mx3.ch/t/1LIY',
+        'md5': '48293cb908342547827f963a5a2e9118',
+        'info_dict': {
+            'id': '1LIY',
+            'ext': 'mov',
+            'artist': 'Tania Kimfumu',
+            'album_artist': 'The Broots',
+            'composer': 'Emmanuel Diserens',
+            'genre': 'Electro',
+            'thumbnail': 'https://mx3.ch/pictures/mx3/file/0110/0003/video_xlarge/frame_0000.png?1686963670',
+            'title': 'The Broots-Larytta remix "Begging For Help"',
+            'release_year': 2023,
+            'tags': ['the broots', 'cassata records', 'larytta'],
+            'description': '"Begging for Help" Larytta Remix Official Video\nRealized By Kali Donkilie in 2023',
+        }
+    }, {
+        'url': 'https://mx3.ch/t/1C6E',
+        'md5': '1afcd578493ddb8e5008e94bb6d97e25',
+        'info_dict': {
+            'id': '1C6E',
+            'ext': 'wav',
+            'artist': 'Alien Bubblegum',
+            'album_artist': 'Alien Bubblegum',
+            'composer': 'Alien Bubblegum',
+            'genre': 'Punk',
+            'thumbnail': 'https://mx3.ch/pictures/mx3/file/0101/1551/square_xlarge/pandora-s-box-cover-with-title.png?1627054733',
+            'title': 'Wide Awake',
+            'release_year': 2021,
+            'tags': ['alien bubblegum', 'bubblegum', 'alien', 'pop punk', 'poppunk'],
+        }
+    }]
+
+
+class Mx3NeoIE(Mx3BaseIE):
+    _DOMAIN = 'neo.mx3.ch'
+    _VALID_URL = Mx3BaseIE._VALID_URL_TMPL % re.escape(_DOMAIN)
+    _TESTS = [{
+        'url': 'https://neo.mx3.ch/t/1hpd',
+        'md5': '6d9986bbae5cac3296ec8813bf965eb2',
+        'info_dict': {
+            'id': '1hpd',
+            'ext': 'wav',
+            'artist': 'Baptiste Lopez',
+            'album_artist': 'Kammerorchester Basel',
+            'composer': 'Jannik Giger',
+            'genre': 'Composition, Orchestra',
+            'title': 'Troisième œil. Für Kammerorchester (2023)',
+            'thumbnail': 'https://neo.mx3.ch/pictures/neo/file/0000/0241/square_xlarge/kammerorchester-basel-group-photo-2_c_-lukasz-rajchert.jpg?1560341252',
+            'release_year': 2023,
+            'tags': [],
+        }
+    }]
+
+
+class Mx3VolksmusikIE(Mx3BaseIE):
+    _DOMAIN = 'volksmusik.mx3.ch'
+    _VALID_URL = Mx3BaseIE._VALID_URL_TMPL % re.escape(_DOMAIN)
+    _TESTS = [{
+        'url': 'https://volksmusik.mx3.ch/t/Zx',
+        'md5': 'dd967a7b0c1ef898f3e072cf9c2eae3c',
+        'info_dict': {
+            'id': 'Zx',
+            'ext': 'mp3',
+            'artist': 'Ländlerkapelle GrischArt',
+            'album_artist': 'Ländlerkapelle GrischArt',
+            'composer': 'Urs Glauser',
+            'genre': 'Instrumental, Graubünden',
+            'title': 'Chämilouf',
+            'thumbnail': 'https://volksmusik.mx3.ch/pictures/vxm/file/0000/3815/square_xlarge/grischart1.jpg?1450530120',
+            'release_year': 2012,
+            'tags': [],
+        }
+    }]
diff --git a/yt_dlp/extractor/newgrounds.py b/yt_dlp/extractor/newgrounds.py
index 9e3286dfe..9601cd10e 100644
--- a/yt_dlp/extractor/newgrounds.py
+++ b/yt_dlp/extractor/newgrounds.py
@@ -3,15 +3,15 @@
 
 from .common import InfoExtractor
 from ..utils import (
+    OnDemandPagedList,
     clean_html,
     extract_attributes,
     get_element_by_id,
     int_or_none,
     parse_count,
     parse_duration,
+    traverse_obj,
     unified_timestamp,
-    OnDemandPagedList,
-    try_get,
 )
 
 
@@ -263,19 +263,16 @@ class NewgroundsUserIE(InfoExtractor):
     def _fetch_page(self, channel_id, url, page):
         page += 1
         posts_info = self._download_json(
-            f'{url}/page/{page}', channel_id,
+            f'{url}?page={page}', channel_id,
             note=f'Downloading page {page}', headers={
                 'Accept': 'application/json, text/javascript, */*; q = 0.01',
                 'X-Requested-With': 'XMLHttpRequest',
             })
-        sequence = posts_info.get('sequence', [])
-        for year in sequence:
-            posts = try_get(posts_info, lambda x: x['years'][str(year)]['items'])
-            for post in posts:
-                path, media_id = self._search_regex(
-                    r'<a[^>]+\bhref=["\'][^"\']+((?:portal/view|audio/listen)/(\d+))[^>]+>',
-                    post, 'url', group=(1, 2))
-                yield self.url_result(f'https://www.newgrounds.com/{path}', NewgroundsIE.ie_key(), media_id)
+        for post in traverse_obj(posts_info, ('items', ..., ..., {str})):
+            path, media_id = self._search_regex(
+                r'<a[^>]+\bhref=["\'][^"\']+((?:portal/view|audio/listen)/(\d+))[^>]+>',
+                post, 'url', group=(1, 2))
+            yield self.url_result(f'https://www.newgrounds.com/{path}', NewgroundsIE.ie_key(), media_id)
 
     def _real_extract(self, url):
         channel_id = self._match_id(url)
diff --git a/yt_dlp/extractor/nfb.py b/yt_dlp/extractor/nfb.py
index 38e068af4..6f7872825 100644
--- a/yt_dlp/extractor/nfb.py
+++ b/yt_dlp/extractor/nfb.py
@@ -1,10 +1,54 @@
 from .common import InfoExtractor
-from ..utils import int_or_none
+from ..utils import (
+    int_or_none,
+    join_nonempty,
+    merge_dicts,
+    parse_count,
+    url_or_none,
+    urljoin,
+)
+from ..utils.traversal import traverse_obj
 
 
-class NFBIE(InfoExtractor):
-    _VALID_URL = r'https?://(?:www\.)?nfb\.ca/film/(?P<id>[^/?#&]+)'
+class NFBBaseIE(InfoExtractor):
+    _VALID_URL_BASE = r'https?://(?:www\.)?(?P<site>nfb|onf)\.ca'
+    _GEO_COUNTRIES = ['CA']
+
+    def _extract_ep_data(self, webpage, video_id, fatal=False):
+        return self._search_json(
+            r'const\s+episodesData\s*=', webpage, 'episode data', video_id,
+            contains_pattern=r'\[\s*{(?s:.+)}\s*\]', fatal=fatal) or []
+
+    def _extract_ep_info(self, data, video_id, slug=None):
+        info = traverse_obj(data, (lambda _, v: video_id in v['embed_url'], {
+            'description': ('description', {str}),
+            'thumbnail': ('thumbnail_url', {url_or_none}),
+            'uploader': ('data_layer', 'episodeMaker', {str}),
+            'release_year': ('data_layer', 'episodeYear', {int_or_none}),
+            'episode': ('data_layer', 'episodeTitle', {str}),
+            'season': ('data_layer', 'seasonTitle', {str}),
+            'season_number': ('data_layer', 'seasonTitle', {parse_count}),
+            'series': ('data_layer', 'seriesTitle', {str}),
+        }), get_all=False)
+
+        return {
+            **info,
+            'id': video_id,
+            'title': join_nonempty('series', 'episode', from_dict=info, delim=' - '),
+            'episode_number': int_or_none(self._search_regex(
+                r'[/-]e(?:pisode)?-?(\d+)(?:[/-]|$)', slug or video_id, 'episode number', default=None)),
+        }
+
+
+class NFBIE(NFBBaseIE):
+    IE_NAME = 'nfb'
+    IE_DESC = 'nfb.ca and onf.ca films and episodes'
+    _VALID_URL = [
+        rf'{NFBBaseIE._VALID_URL_BASE}/(?P<type>film)/(?P<id>[^/?#&]+)',
+        rf'{NFBBaseIE._VALID_URL_BASE}/(?P<type>series?)/(?P<id>[^/?#&]+/s(?:ea|ai)son\d+/episode\d+)',
+    ]
     _TESTS = [{
+        'note': 'NFB film',
         'url': 'https://www.nfb.ca/film/trafficopter/',
         'info_dict': {
             'id': 'trafficopter',
@@ -14,29 +58,192 @@ class NFBIE(InfoExtractor):
             'thumbnail': r're:^https?://.*\.jpg$',
             'uploader': 'Barrie Howells',
             'release_year': 1972,
+            'duration': 600.0,
+        },
+        'params': {'skip_download': 'm3u8'},
+    }, {
+        'note': 'ONF film',
+        'url': 'https://www.onf.ca/film/mal-du-siecle/',
+        'info_dict': {
+            'id': 'mal-du-siecle',
+            'ext': 'mp4',
+            'title': 'Le mal du siècle',
+            'description': 'md5:1abf774d77569ebe603419f2d344102b',
+            'thumbnail': r're:^https?://.*\.jpg$',
+            'uploader': 'Catherine Lepage',
+            'release_year': 2019,
+            'duration': 300.0,
+        },
+        'params': {'skip_download': 'm3u8'},
+    }, {
+        'note': 'NFB episode with English title',
+        'url': 'https://www.nfb.ca/series/true-north-inside-the-rise-of-toronto-basketball/season1/episode9/',
+        'info_dict': {
+            'id': 'true-north-episode9-true-north-finale-making-it',
+            'ext': 'mp4',
+            'title': 'True North: Inside the Rise of Toronto Basketball - Finale: Making It',
+            'description': 'We catch up with each player in the midst of their journey as they reflect on their road ahead.',
+            'series': 'True North: Inside the Rise of Toronto Basketball',
+            'release_year': 2018,
+            'season': 'Season 1',
+            'season_number': 1,
+            'episode': 'Finale: Making It',
+            'episode_number': 9,
+            'uploader': 'Ryan Sidhoo',
+            'thumbnail': r're:^https?://.*\.jpg$',
         },
+        'params': {'skip_download': 'm3u8'},
+    }, {
+        'note': 'ONF episode with French title',
+        'url': 'https://www.onf.ca/serie/direction-nord-la-montee-du-basketball-a-toronto/saison1/episode9/',
+        'info_dict': {
+            'id': 'direction-nord-episode-9',
+            'ext': 'mp4',
+            'title': 'Direction nord – La montée du basketball à Toronto - Finale : Réussir',
+            'description': 'md5:349a57419b71432b97bf6083d92b029d',
+            'series': 'Direction nord – La montée du basketball à Toronto',
+            'release_year': 2018,
+            'season': 'Saison 1',
+            'season_number': 1,
+            'episode': 'Finale : Réussir',
+            'episode_number': 9,
+            'uploader': 'Ryan Sidhoo',
+            'thumbnail': r're:^https?://.*\.jpg$',
+        },
+        'params': {'skip_download': 'm3u8'},
+    }, {
+        'note': 'NFB episode with French title (needs geo-bypass)',
+        'url': 'https://www.nfb.ca/series/etoile-du-nord/saison1/episode1/',
+        'info_dict': {
+            'id': 'etoile-du-nord-episode-1-lobservation',
+            'ext': 'mp4',
+            'title': 'Étoile du Nord - L\'observation',
+            'description': 'md5:161a4617260dee3de70f509b2c9dd21b',
+            'series': 'Étoile du Nord',
+            'release_year': 2023,
+            'season': 'Saison 1',
+            'season_number': 1,
+            'episode': 'L\'observation',
+            'episode_number': 1,
+            'uploader': 'Patrick Bossé',
+            'thumbnail': r're:^https?://.*\.jpg$',
+        },
+        'params': {'skip_download': 'm3u8'},
+    }, {
+        'note': 'ONF episode with English title (needs geo-bypass)',
+        'url': 'https://www.onf.ca/serie/north-star/season1/episode1/',
+        'info_dict': {
+            'id': 'north-star-episode-1-observation',
+            'ext': 'mp4',
+            'title': 'North Star - Observation',
+            'description': 'md5:c727f370839d8a817392b9e3f23655c7',
+            'series': 'North Star',
+            'release_year': 2023,
+            'season': 'Season 1',
+            'season_number': 1,
+            'episode': 'Observation',
+            'episode_number': 1,
+            'uploader': 'Patrick Bossé',
+            'thumbnail': r're:^https?://.*\.jpg$',
+        },
+        'params': {'skip_download': 'm3u8'},
+    }, {
+        'note': 'NFB episode with /film/ URL and English title (needs geo-bypass)',
+        'url': 'https://www.nfb.ca/film/north-star-episode-1-observation/',
+        'info_dict': {
+            'id': 'north-star-episode-1-observation',
+            'ext': 'mp4',
+            'title': 'North Star - Observation',
+            'description': 'md5:c727f370839d8a817392b9e3f23655c7',
+            'series': 'North Star',
+            'release_year': 2023,
+            'season': 'Season 1',
+            'season_number': 1,
+            'episode': 'Observation',
+            'episode_number': 1,
+            'uploader': 'Patrick Bossé',
+            'thumbnail': r're:^https?://.*\.jpg$',
+        },
+        'params': {'skip_download': 'm3u8'},
+    }, {
+        'note': 'ONF episode with /film/ URL and French title (needs geo-bypass)',
+        'url': 'https://www.onf.ca/film/etoile-du-nord-episode-1-lobservation/',
+        'info_dict': {
+            'id': 'etoile-du-nord-episode-1-lobservation',
+            'ext': 'mp4',
+            'title': 'Étoile du Nord - L\'observation',
+            'description': 'md5:161a4617260dee3de70f509b2c9dd21b',
+            'series': 'Étoile du Nord',
+            'release_year': 2023,
+            'season': 'Saison 1',
+            'season_number': 1,
+            'episode': 'L\'observation',
+            'episode_number': 1,
+            'uploader': 'Patrick Bossé',
+            'thumbnail': r're:^https?://.*\.jpg$',
+        },
+        'params': {'skip_download': 'm3u8'},
+    }, {
+        'note': 'Season 2 episode w/o episode num in id, extract from json ld',
+        'url': 'https://www.onf.ca/film/liste-des-choses-qui-existent-saison-2-ours',
+        'info_dict': {
+            'id': 'liste-des-choses-qui-existent-saison-2-ours',
+            'ext': 'mp4',
+            'title': 'La liste des choses qui existent - L\'ours en peluche',
+            'description': 'md5:d5e8d8fc5f3a7385a9cf0f509b37e28a',
+            'series': 'La liste des choses qui existent',
+            'release_year': 2022,
+            'season': 'Saison 2',
+            'season_number': 2,
+            'episode': 'L\'ours en peluche',
+            'episode_number': 12,
+            'uploader': 'Francis Papillon',
+            'thumbnail': r're:^https?://.*\.jpg$',
+        },
+        'params': {'skip_download': 'm3u8'},
+    }, {
+        'note': 'NFB film /embed/player/ page',
+        'url': 'https://www.nfb.ca/film/afterlife/embed/player/',
+        'info_dict': {
+            'id': 'afterlife',
+            'ext': 'mp4',
+            'title': 'Afterlife',
+            'description': 'md5:84951394f594f1fb1e62d9c43242fdf5',
+            'release_year': 1978,
+            'duration': 420.0,
+            'uploader': 'Ishu Patel',
+            'thumbnail': r're:^https?://.*\.jpg$',
+        },
+        'params': {'skip_download': 'm3u8'},
     }]
 
     def _real_extract(self, url):
-        video_id = self._match_id(url)
-
-        webpage = self._download_webpage('https://www.nfb.ca/film/%s/' % video_id, video_id)
-
-        iframe = self._html_search_regex(
-            r'<[^>]+\bid=["\']player-iframe["\'][^>]*src=["\']([^"\']+)',
-            webpage, 'iframe', default=None, fatal=True)
-        if iframe.startswith('/'):
-            iframe = f'https://www.nfb.ca{iframe}'
+        site, type_, slug = self._match_valid_url(url).group('site', 'type', 'id')
+        # Need to construct the URL since we match /embed/player/ URLs as well
+        webpage, urlh = self._download_webpage_handle(f'https://www.{site}.ca/{type_}/{slug}/', slug)
+        # type_ can change from film to serie(s) after redirect; new slug may have episode number
+        type_, slug = self._match_valid_url(urlh.url).group('type', 'id')
 
-        player = self._download_webpage(iframe, video_id)
+        embed_url = urljoin(f'https://www.{site}.ca', self._html_search_regex(
+            r'<[^>]+\bid=["\']player-iframe["\'][^>]*\bsrc=["\']([^"\']+)', webpage, 'embed url'))
+        video_id = self._match_id(embed_url)  # embed url has unique slug
+        player = self._download_webpage(embed_url, video_id, 'Downloading player page')
+        if 'MESSAGE_GEOBLOCKED' in player:
+            self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
 
-        source = self._html_search_regex(
-            r'source:\s*\'([^\']+)',
-            player, 'source', default=None, fatal=True)
+        formats, subtitles = self._extract_m3u8_formats_and_subtitles(
+            self._html_search_regex(r'source:\s*\'([^\']+)', player, 'm3u8 url'),
+            video_id, 'mp4', m3u8_id='hls')
 
-        formats, subtitles = self._extract_m3u8_formats_and_subtitles(source, video_id, ext='mp4')
+        if dv_source := self._html_search_regex(r'dvSource:\s*\'([^\']+)', player, 'dv', default=None):
+            fmts, subs = self._extract_m3u8_formats_and_subtitles(
+                dv_source, video_id, 'mp4', m3u8_id='dv', preference=-2, fatal=False)
+            for fmt in fmts:
+                fmt['format_note'] = 'described video'
+            formats.extend(fmts)
+            self._merge_subtitles(subs, target=subtitles)
 
-        return {
+        info = {
             'id': video_id,
             'title': self._html_search_regex(
                 r'<[^>]+\bid=["\']titleHeader["\'][^>]*>\s*<h1[^>]*>\s*([^<]+?)\s*</h1>',
@@ -45,14 +252,49 @@ def _real_extract(self, url):
                 r'<[^>]+\bid=["\']tabSynopsis["\'][^>]*>\s*<p[^>]*>\s*([^<]+)',
                 webpage, 'description', default=None),
             'thumbnail': self._html_search_regex(
-                r'poster:\s*\'([^\']+)',
-                player, 'thumbnail', default=None),
+                r'poster:\s*\'([^\']+)', player, 'thumbnail', default=None),
             'uploader': self._html_search_regex(
-                r'<[^>]+\bitemprop=["\']name["\'][^>]*>([^<]+)',
-                webpage, 'uploader', default=None),
+                r'<[^>]+\bitemprop=["\']name["\'][^>]*>([^<]+)', webpage, 'uploader', default=None),
             'release_year': int_or_none(self._html_search_regex(
                 r'<[^>]+\bitemprop=["\']datePublished["\'][^>]*>([^<]+)',
                 webpage, 'release_year', default=None)),
+        } if type_ == 'film' else self._extract_ep_info(self._extract_ep_data(webpage, video_id, slug), video_id)
+
+        return merge_dicts({
             'formats': formats,
             'subtitles': subtitles,
-        }
+        }, info, self._search_json_ld(webpage, video_id, default={}))
+
+
+class NFBSeriesIE(NFBBaseIE):
+    IE_NAME = 'nfb:series'
+    IE_DESC = 'nfb.ca and onf.ca series'
+    _VALID_URL = rf'{NFBBaseIE._VALID_URL_BASE}/(?P<type>series?)/(?P<id>[^/?#&]+)/?(?:[?#]|$)'
+    _TESTS = [{
+        'url': 'https://www.nfb.ca/series/true-north-inside-the-rise-of-toronto-basketball/',
+        'playlist_mincount': 9,
+        'info_dict': {
+            'id': 'true-north-inside-the-rise-of-toronto-basketball',
+        },
+    }, {
+        'url': 'https://www.onf.ca/serie/la-liste-des-choses-qui-existent-serie/',
+        'playlist_mincount': 26,
+        'info_dict': {
+            'id': 'la-liste-des-choses-qui-existent-serie',
+        },
+    }]
+
+    def _entries(self, episodes):
+        for episode in traverse_obj(episodes, lambda _, v: NFBIE.suitable(v['embed_url'])):
+            mobj = NFBIE._match_valid_url(episode['embed_url'])
+            yield self.url_result(
+                mobj[0], NFBIE, **self._extract_ep_info([episode], mobj.group('id')))
+
+    def _real_extract(self, url):
+        site, type_, series_id = self._match_valid_url(url).group('site', 'type', 'id')
+        season_path = 'saison' if type_ == 'serie' else 'season'
+        webpage = self._download_webpage(
+            f'https://www.{site}.ca/{type_}/{series_id}/{season_path}1/episode1', series_id)
+        episodes = self._extract_ep_data(webpage, series_id, fatal=True)
+
+        return self.playlist_result(self._entries(episodes), series_id)
diff --git a/yt_dlp/extractor/ninaprotocol.py b/yt_dlp/extractor/ninaprotocol.py
new file mode 100644
index 000000000..ea57c5f38
--- /dev/null
+++ b/yt_dlp/extractor/ninaprotocol.py
@@ -0,0 +1,225 @@
+from .common import InfoExtractor
+from ..utils import int_or_none, mimetype2ext, parse_iso8601, url_or_none
+from ..utils.traversal import traverse_obj
+
+
+class NinaProtocolIE(InfoExtractor):
+    _VALID_URL = r'https?://(?:www\.)?ninaprotocol\.com/releases/(?P<id>[^/#?]+)'
+    _TESTS = [{
+        'url': 'https://www.ninaprotocol.com/releases/3SvsMM3y4oTPZ5DXFJnLkCAqkxz34hjzFxqms1vu9XBJ',
+        'info_dict': {
+            'id': '3SvsMM3y4oTPZ5DXFJnLkCAqkxz34hjzFxqms1vu9XBJ',
+            'title': 'The Spatulas - March Chant',
+            'tags': ['punk', 'postpresentmedium', 'cambridge'],
+            'uploader_id': '2bGjgdKUddJoj2shYGqfNcUfoSoABP21RJoiwGMZDq3A',
+            'channel': 'ppm',
+            'description': 'md5:bb9f9d39d8f786449cd5d0ff7c5772db',
+            'album': 'The Spatulas - March Chant',
+            'thumbnail': 'https://www.arweave.net/VyZA6CBeUuqP174khvSrD44Eosi3MLVyWN42uaQKg50',
+            'timestamp': 1701417610,
+            'uploader': 'ppmrecs',
+            'channel_id': '4ceG4zsb7VVxBTGPtZMqDZWGHo3VUg2xRvzC2b17ymWP',
+            'display_id': 'the-spatulas-march-chant',
+            'upload_date': '20231201',
+            'album_artist': 'Post Present Medium ',
+        },
+        'playlist': [{
+            'info_dict': {
+                'id': '3SvsMM3y4oTPZ5DXFJnLkCAqkxz34hjzFxqms1vu9XBJ_1',
+                'title': 'March Chant In April',
+                'track': 'March Chant In April',
+                'ext': 'mp3',
+                'duration': 152,
+                'track_number': 1,
+                'uploader_id': '2bGjgdKUddJoj2shYGqfNcUfoSoABP21RJoiwGMZDq3A',
+                'uploader': 'ppmrecs',
+                'thumbnail': 'https://www.arweave.net/VyZA6CBeUuqP174khvSrD44Eosi3MLVyWN42uaQKg50',
+                'timestamp': 1701417610,
+                'channel': 'ppm',
+                'album': 'The Spatulas - March Chant',
+                'tags': ['punk', 'postpresentmedium', 'cambridge'],
+                'channel_id': '4ceG4zsb7VVxBTGPtZMqDZWGHo3VUg2xRvzC2b17ymWP',
+                'upload_date': '20231201',
+                'album_artist': 'Post Present Medium ',
+            }
+        }, {
+            'info_dict': {
+                'id': '3SvsMM3y4oTPZ5DXFJnLkCAqkxz34hjzFxqms1vu9XBJ_2',
+                'title': 'Rescue Mission',
+                'track': 'Rescue Mission',
+                'ext': 'mp3',
+                'duration': 212,
+                'track_number': 2,
+                'album_artist': 'Post Present Medium ',
+                'uploader': 'ppmrecs',
+                'tags': ['punk', 'postpresentmedium', 'cambridge'],
+                'thumbnail': 'https://www.arweave.net/VyZA6CBeUuqP174khvSrD44Eosi3MLVyWN42uaQKg50',
+                'channel': 'ppm',
+                'upload_date': '20231201',
+                'channel_id': '4ceG4zsb7VVxBTGPtZMqDZWGHo3VUg2xRvzC2b17ymWP',
+                'timestamp': 1701417610,
+                'album': 'The Spatulas - March Chant',
+                'uploader_id': '2bGjgdKUddJoj2shYGqfNcUfoSoABP21RJoiwGMZDq3A',
+            }
+        }, {
+            'info_dict': {
+                'id': '3SvsMM3y4oTPZ5DXFJnLkCAqkxz34hjzFxqms1vu9XBJ_3',
+                'title': 'Slinger Style',
+                'track': 'Slinger Style',
+                'ext': 'mp3',
+                'duration': 179,
+                'track_number': 3,
+                'timestamp': 1701417610,
+                'upload_date': '20231201',
+                'channel_id': '4ceG4zsb7VVxBTGPtZMqDZWGHo3VUg2xRvzC2b17ymWP',
+                'uploader_id': '2bGjgdKUddJoj2shYGqfNcUfoSoABP21RJoiwGMZDq3A',
+                'thumbnail': 'https://www.arweave.net/VyZA6CBeUuqP174khvSrD44Eosi3MLVyWN42uaQKg50',
+                'album_artist': 'Post Present Medium ',
+                'album': 'The Spatulas - March Chant',
+                'tags': ['punk', 'postpresentmedium', 'cambridge'],
+                'uploader': 'ppmrecs',
+                'channel': 'ppm',
+            }
+        }, {
+            'info_dict': {
+                'id': '3SvsMM3y4oTPZ5DXFJnLkCAqkxz34hjzFxqms1vu9XBJ_4',
+                'title': 'Psychic Signal',
+                'track': 'Psychic Signal',
+                'ext': 'mp3',
+                'duration': 220,
+                'track_number': 4,
+                'tags': ['punk', 'postpresentmedium', 'cambridge'],
+                'upload_date': '20231201',
+                'album': 'The Spatulas - March Chant',
+                'thumbnail': 'https://www.arweave.net/VyZA6CBeUuqP174khvSrD44Eosi3MLVyWN42uaQKg50',
+                'timestamp': 1701417610,
+                'album_artist': 'Post Present Medium ',
+                'channel_id': '4ceG4zsb7VVxBTGPtZMqDZWGHo3VUg2xRvzC2b17ymWP',
+                'channel': 'ppm',
+                'uploader_id': '2bGjgdKUddJoj2shYGqfNcUfoSoABP21RJoiwGMZDq3A',
+                'uploader': 'ppmrecs',
+            }
+        }, {
+            'info_dict': {
+                'id': '3SvsMM3y4oTPZ5DXFJnLkCAqkxz34hjzFxqms1vu9XBJ_5',
+                'title': 'Curvy Color',
+                'track': 'Curvy Color',
+                'ext': 'mp3',
+                'duration': 148,
+                'track_number': 5,
+                'timestamp': 1701417610,
+                'uploader_id': '2bGjgdKUddJoj2shYGqfNcUfoSoABP21RJoiwGMZDq3A',
+                'thumbnail': 'https://www.arweave.net/VyZA6CBeUuqP174khvSrD44Eosi3MLVyWN42uaQKg50',
+                'album': 'The Spatulas - March Chant',
+                'album_artist': 'Post Present Medium ',
+                'channel': 'ppm',
+                'tags': ['punk', 'postpresentmedium', 'cambridge'],
+                'uploader': 'ppmrecs',
+                'channel_id': '4ceG4zsb7VVxBTGPtZMqDZWGHo3VUg2xRvzC2b17ymWP',
+                'upload_date': '20231201',
+            }
+        }, {
+            'info_dict': {
+                'id': '3SvsMM3y4oTPZ5DXFJnLkCAqkxz34hjzFxqms1vu9XBJ_6',
+                'title': 'Caveman Star',
+                'track': 'Caveman Star',
+                'ext': 'mp3',
+                'duration': 121,
+                'track_number': 6,
+                'channel_id': '4ceG4zsb7VVxBTGPtZMqDZWGHo3VUg2xRvzC2b17ymWP',
+                'thumbnail': 'https://www.arweave.net/VyZA6CBeUuqP174khvSrD44Eosi3MLVyWN42uaQKg50',
+                'tags': ['punk', 'postpresentmedium', 'cambridge'],
+                'album_artist': 'Post Present Medium ',
+                'uploader': 'ppmrecs',
+                'timestamp': 1701417610,
+                'uploader_id': '2bGjgdKUddJoj2shYGqfNcUfoSoABP21RJoiwGMZDq3A',
+                'album': 'The Spatulas - March Chant',
+                'channel': 'ppm',
+                'upload_date': '20231201',
+            },
+        }],
+    }, {
+        'url': 'https://www.ninaprotocol.com/releases/f-g-s-american-shield',
+        'info_dict': {
+            'id': '76PZnJwaMgViQHYfA4NYJXds7CmW6vHQKAtQUxGene6J',
+            'description': 'md5:63f08d5db558b4b36e1896f317062721',
+            'title': 'F.G.S. - American Shield',
+            'uploader_id': 'Ej3rozs11wYqFk1Gs6oggGCkGLz8GzBhmJfnUxf6gPci',
+            'channel_id': '6JuksCZPXuP16wJ1BUfwuukJzh42C7guhLrFPPkVJfyE',
+            'channel': 'tinkscough',
+            'tags': [],
+            'album_artist': 'F.G.S.',
+            'album': 'F.G.S. - American Shield',
+            'thumbnail': 'https://www.arweave.net/YJpgImkXLT9SbpFb576KuZ5pm6bdvs452LMs3Rx6lm8',
+            'display_id': 'f-g-s-american-shield',
+            'uploader': 'flannerysilva',
+            'timestamp': 1702395858,
+            'upload_date': '20231212',
+        },
+        'playlist_count': 1,
+    }, {
+        'url': 'https://www.ninaprotocol.com/releases/time-to-figure-things-out',
+        'info_dict': {
+            'id': '6Zi1nC5hj6b13NkpxVYwRhFy6mYA7oLBbe9DMrgGDcYh',
+            'display_id': 'time-to-figure-things-out',
+            'description': 'md5:960202ed01c3134bb8958f1008527e35',
+            'timestamp': 1706283607,
+            'title': 'DJ STEPDAD - time to figure things out',
+            'album_artist': 'DJ STEPDAD',
+            'uploader': 'tddvsss',
+            'upload_date': '20240126',
+            'album': 'time to figure things out',
+            'uploader_id': 'AXQNRgTyYsySyAMFDwxzumuGjfmoXshorCesjpquwCBi',
+            'thumbnail': 'https://www.arweave.net/O4i8bcKVqJVZvNeHHFp6r8knpFGh9ZwEgbeYacr4nss',
+            'tags': [],
+        },
+        'playlist_count': 4,
+    }]
+
+    def _real_extract(self, url):
+        video_id = self._match_id(url)
+        release = self._download_json(
+            f'https://api.ninaprotocol.com/v1/releases/{video_id}', video_id)['release']
+
+        video_id = release.get('publicKey') or video_id
+
+        common_info = traverse_obj(release, {
+            'album': ('metadata', 'properties', 'title', {str}),
+            'album_artist': ((('hub', 'data'), 'publisherAccount'), 'displayName', {str}),
+            'timestamp': ('datetime', {parse_iso8601}),
+            'thumbnail': ('metadata', 'image', {url_or_none}),
+            'uploader': ('publisherAccount', 'handle', {str}),
+            'uploader_id': ('publisherAccount', 'publicKey', {str}),
+            'channel': ('hub', 'handle', {str}),
+            'channel_id': ('hub', 'publicKey', {str}),
+        }, get_all=False)
+        common_info['tags'] = traverse_obj(release, ('metadata', 'properties', 'tags', ..., {str}))
+
+        entries = []
+        for track_num, track in enumerate(traverse_obj(release, (
+                'metadata', 'properties', 'files', lambda _, v: url_or_none(v['uri']))), 1):
+            entries.append({
+                'id': f'{video_id}_{track_num}',
+                'url': track['uri'],
+                **traverse_obj(track, {
+                    'title': ('track_title', {str}),
+                    'track': ('track_title', {str}),
+                    'ext': ('type', {mimetype2ext}),
+                    'track_number': ('track', {int_or_none}),
+                    'duration': ('duration', {int_or_none}),
+                }),
+                'vcodec': 'none',
+                **common_info,
+            })
+
+        return {
+            '_type': 'playlist',
+            'id': video_id,
+            'entries': entries,
+            **traverse_obj(release, {
+                'display_id': ('slug', {str}),
+                'title': ('metadata', 'name', {str}),
+                'description': ('metadata', 'description', {str}),
+            }),
+            **common_info,
+        }
diff --git a/yt_dlp/extractor/orf.py b/yt_dlp/extractor/orf.py
index 9a48ae1b3..1b2a79a62 100644
--- a/yt_dlp/extractor/orf.py
+++ b/yt_dlp/extractor/orf.py
@@ -1,3 +1,4 @@
+import base64
 import functools
 import re
 
@@ -565,3 +566,66 @@ def _real_extract(self, url):
             })
 
         return self.playlist_result(entries)
+
+
+class ORFONIE(InfoExtractor):
+    IE_NAME = 'orf:on'
+    _VALID_URL = r'https?://on\.orf\.at/video/(?P<id>\d{8})/(?P<slug>[\w-]+)'
+    _TESTS = [{
+        'url': 'https://on.orf.at/video/14210000/school-of-champions-48',
+        'info_dict': {
+            'id': '14210000',
+            'ext': 'mp4',
+            'duration': 2651.08,
+            'thumbnail': 'https://api-tvthek.orf.at/assets/segments/0167/98/thumb_16697671_segments_highlight_teaser.jpeg',
+            'title': 'School of Champions (4/8)',
+            'description': 'md5:d09ad279fc2e8502611e7648484b6afd',
+            'media_type': 'episode',
+            'timestamp': 1706472362,
+            'upload_date': '20240128',
+        }
+    }]
+
+    def _extract_video(self, video_id, display_id):
+        encrypted_id = base64.b64encode(f'3dSlfek03nsLKdj4Jsd{video_id}'.encode()).decode()
+        api_json = self._download_json(
+            f'https://api-tvthek.orf.at/api/v4.3/public/episode/encrypted/{encrypted_id}', display_id)
+
+        formats, subtitles = [], {}
+        for manifest_type in traverse_obj(api_json, ('sources', {dict.keys}, ...)):
+            for manifest_url in traverse_obj(api_json, ('sources', manifest_type, ..., 'src', {url_or_none})):
+                if manifest_type == 'hls':
+                    fmts, subs = self._extract_m3u8_formats_and_subtitles(
+                        manifest_url, display_id, fatal=False, m3u8_id='hls')
+                elif manifest_type == 'dash':
+                    fmts, subs = self._extract_mpd_formats_and_subtitles(
+                        manifest_url, display_id, fatal=False, mpd_id='dash')
+                else:
+                    continue
+                formats.extend(fmts)
+                self._merge_subtitles(subs, target=subtitles)
+
+        return {
+            'id': video_id,
+            'formats': formats,
+            'subtitles': subtitles,
+            **traverse_obj(api_json, {
+                'duration': ('duration_second', {float_or_none}),
+                'title': (('title', 'headline'), {str}),
+                'description': (('description', 'teaser_text'), {str}),
+                'media_type': ('video_type', {str}),
+            }, get_all=False),
+        }
+
+    def _real_extract(self, url):
+        video_id, display_id = self._match_valid_url(url).group('id', 'slug')
+        webpage = self._download_webpage(url, display_id)
+
+        return {
+            'id': video_id,
+            'title': self._html_search_meta(['og:title', 'twitter:title'], webpage, default=None),
+            'description': self._html_search_meta(
+                ['description', 'og:description', 'twitter:description'], webpage, default=None),
+            **self._search_json_ld(webpage, display_id, fatal=False),
+            **self._extract_video(video_id, display_id),
+        }
diff --git a/yt_dlp/extractor/patreon.py b/yt_dlp/extractor/patreon.py
index 9316789df..d2ddb72cd 100644
--- a/yt_dlp/extractor/patreon.py
+++ b/yt_dlp/extractor/patreon.py
@@ -275,7 +275,7 @@ def _real_extract(self, url):
                     'ext': ext,
                     'url': post_file['url'],
                 }
-            elif name == 'video':
+            elif name == 'video' or determine_ext(post_file.get('url')) == 'm3u8':
                 formats, subtitles = self._extract_m3u8_formats_and_subtitles(post_file['url'], video_id)
                 return {
                     **info,
diff --git a/yt_dlp/extractor/playsuisse.py b/yt_dlp/extractor/playsuisse.py
index 76288c778..7c5cad1be 100644
--- a/yt_dlp/extractor/playsuisse.py
+++ b/yt_dlp/extractor/playsuisse.py
@@ -1,10 +1,18 @@
 import json
 
 from .common import InfoExtractor
-from ..utils import int_or_none, traverse_obj
+from ..utils import (
+    ExtractorError,
+    int_or_none,
+    parse_qs,
+    traverse_obj,
+    update_url_query,
+    urlencode_postdata,
+)
 
 
 class PlaySuisseIE(InfoExtractor):
+    _NETRC_MACHINE = 'playsuisse'
     _VALID_URL = r'https?://(?:www\.)?playsuisse\.ch/(?:watch|detail)/(?:[^#]*[?&]episodeId=)?(?P<id>[0-9]+)'
     _TESTS = [
         {
@@ -134,12 +142,47 @@ class PlaySuisseIE(InfoExtractor):
             id
             url
         }'''
+    _LOGIN_BASE_URL = 'https://login.srgssr.ch/srgssrlogin.onmicrosoft.com'
+    _LOGIN_PATH = 'B2C_1A__SignInV2'
+    _ID_TOKEN = None
+
+    def _perform_login(self, username, password):
+        login_page = self._download_webpage(
+            'https://www.playsuisse.ch/api/sso/login', None, note='Downloading login page',
+            query={'x': 'x', 'locale': 'de', 'redirectUrl': 'https://www.playsuisse.ch/'})
+        settings = self._search_json(r'var\s+SETTINGS\s*=', login_page, 'settings', None)
+
+        csrf_token = settings['csrf']
+        query = {'tx': settings['transId'], 'p': self._LOGIN_PATH}
+
+        status = traverse_obj(self._download_json(
+            f'{self._LOGIN_BASE_URL}/{self._LOGIN_PATH}/SelfAsserted', None, 'Logging in',
+            query=query, headers={'X-CSRF-TOKEN': csrf_token}, data=urlencode_postdata({
+                'request_type': 'RESPONSE',
+                'signInName': username,
+                'password': password
+            }), expected_status=400), ('status', {int_or_none}))
+        if status == 400:
+            raise ExtractorError('Invalid username or password', expected=True)
+
+        urlh = self._request_webpage(
+            f'{self._LOGIN_BASE_URL}/{self._LOGIN_PATH}/api/CombinedSigninAndSignup/confirmed',
+            None, 'Downloading ID token', query={
+                'rememberMe': 'false',
+                'csrf_token': csrf_token,
+                **query,
+                'diags': '',
+            })
+
+        self._ID_TOKEN = traverse_obj(parse_qs(urlh.url), ('id_token', 0))
+        if not self._ID_TOKEN:
+            raise ExtractorError('Login failed')
 
     def _get_media_data(self, media_id):
         # NOTE In the web app, the "locale" header is used to switch between languages,
         # However this doesn't seem to take effect when passing the header here.
         response = self._download_json(
-            'https://4bbepzm4ef.execute-api.eu-central-1.amazonaws.com/prod/graphql',
+            'https://www.playsuisse.ch/api/graphql',
             media_id, data=json.dumps({
                 'operationName': 'AssetWatch',
                 'query': self._GRAPHQL_QUERY,
@@ -150,6 +193,9 @@ def _get_media_data(self, media_id):
         return response['data']['assetV2']
 
     def _real_extract(self, url):
+        if not self._ID_TOKEN:
+            self.raise_login_required(method='password')
+
         media_id = self._match_id(url)
         media_data = self._get_media_data(media_id)
         info = self._extract_single(media_data)
@@ -168,7 +214,8 @@ def _extract_single(self, media_data):
             if not media.get('url') or media.get('type') != 'HLS':
                 continue
             f, subs = self._extract_m3u8_formats_and_subtitles(
-                media['url'], media_data['id'], 'mp4', m3u8_id='HLS', fatal=False)
+                update_url_query(media['url'], {'id_token': self._ID_TOKEN}),
+                media_data['id'], 'mp4', m3u8_id='HLS', fatal=False)
             formats.extend(f)
             self._merge_subtitles(subs, target=subtitles)
 
diff --git a/yt_dlp/extractor/pr0gramm.py b/yt_dlp/extractor/pr0gramm.py
index 2a6794208..36e415f4a 100644
--- a/yt_dlp/extractor/pr0gramm.py
+++ b/yt_dlp/extractor/pr0gramm.py
@@ -18,7 +18,6 @@
 class Pr0grammIE(InfoExtractor):
     _VALID_URL = r'https?://pr0gramm\.com\/(?:[^/?#]+/)+(?P<id>[\d]+)(?:[/?#:]|$)'
     _TESTS = [{
-        # Tags require account
         'url': 'https://pr0gramm.com/new/video/5466437',
         'info_dict': {
             'id': '5466437',
@@ -36,7 +35,6 @@ class Pr0grammIE(InfoExtractor):
             '_old_archive_ids': ['pr0grammstatic 5466437'],
         },
     }, {
-        # Tags require account
         'url': 'https://pr0gramm.com/new/3052805:comment28391322',
         'info_dict': {
             'id': '3052805',
@@ -71,6 +69,23 @@ class Pr0grammIE(InfoExtractor):
             'thumbnail': r're:^https://thumb\.pr0gramm\.com/.*\.jpg',
             '_old_archive_ids': ['pr0grammstatic 5848332'],
         },
+    }, {
+        'url': 'https://pr0gramm.com/top/5895149',
+        'info_dict': {
+            'id': '5895149',
+            'ext': 'mp4',
+            'title': 'pr0gramm-5895149 by algoholigSeeManThrower',
+            'tags': 'count:19',
+            'uploader': 'algoholigSeeManThrower',
+            'uploader_id': 457556,
+            'upload_timestamp': 1697580902,
+            'upload_date': '20231018',
+            'like_count': int,
+            'dislike_count': int,
+            'age_limit': 0,
+            'thumbnail': 'https://thumb.pr0gramm.com/2023/10/18/db47bb3db5e1a1b3.jpg',
+            '_old_archive_ids': ['pr0grammstatic 5895149'],
+        },
     }, {
         'url': 'https://pr0gramm.com/static/5466437',
         'only_matching': True,
@@ -92,15 +107,15 @@ def _is_logged_in(self):
     def _maximum_flags(self):
         # We need to guess the flags for the content otherwise the api will raise an error
         # We can guess the maximum allowed flags for the account from the cookies
-        # Bitflags are (msbf): nsfp, nsfl, nsfw, sfw
-        flags = 0b0001
+        # Bitflags are (msbf): pol, nsfp, nsfl, nsfw, sfw
+        flags = 0b10001
         if self._is_logged_in:
-            flags |= 0b1000
+            flags |= 0b01000
             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')):
-                flags |= 0b0110
+                flags |= 0b00110
 
         return flags
 
@@ -134,14 +149,12 @@ def _real_extract(self, url):
         if not source or not source.endswith('mp4'):
             self.raise_no_formats('Could not extract a video', expected=bool(source), video_id=video_id)
 
-        tags = None
-        if self._is_logged_in:
-            metadata = self._call_api('info', video_id, {'itemId': video_id}, note='Downloading tags')
-            tags = traverse_obj(metadata, ('tags', ..., 'tag', {str}))
-            # Sorted by "confidence", higher confidence = earlier in list
-            confidences = traverse_obj(metadata, ('tags', ..., 'confidence', ({int}, {float})))
-            if confidences:
-                tags = [tag for _, tag in sorted(zip(confidences, tags), reverse=True)]
+        metadata = self._call_api('info', video_id, {'itemId': video_id}, note='Downloading tags')
+        tags = traverse_obj(metadata, ('tags', ..., 'tag', {str}))
+        # Sorted by "confidence", higher confidence = earlier in list
+        confidences = traverse_obj(metadata, ('tags', ..., 'confidence', ({int}, {float})))
+        if confidences:
+            tags = [tag for _, tag in sorted(zip(confidences, tags), reverse=True)]
 
         formats = traverse_obj(video_info, ('variants', ..., {
             'format_id': ('name', {str}),
diff --git a/yt_dlp/extractor/prankcast.py b/yt_dlp/extractor/prankcast.py
index b2ec5bbb8..562aca0ff 100644
--- a/yt_dlp/extractor/prankcast.py
+++ b/yt_dlp/extractor/prankcast.py
@@ -1,5 +1,8 @@
+import json
+
 from .common import InfoExtractor
-from ..utils import parse_iso8601, traverse_obj, try_call
+from ..utils import float_or_none, parse_iso8601, str_or_none, try_call
+from ..utils.traversal import traverse_obj
 
 
 class PrankCastIE(InfoExtractor):
@@ -64,3 +67,71 @@ def _real_extract(self, url):
             'categories': [json_info.get('broadcast_category')],
             'tags': try_call(lambda: json_info['broadcast_tags'].split(','))
         }
+
+
+class PrankCastPostIE(InfoExtractor):
+    _VALID_URL = r'https?://(?:www\.)?prankcast\.com/[^/?#]+/posts/(?P<id>\d+)-(?P<display_id>[^/?#]+)'
+    _TESTS = [{
+        'url': 'https://prankcast.com/devonanustart/posts/6214-happy-national-rachel-day-',
+        'info_dict': {
+            'id': '6214',
+            'ext': 'mp3',
+            'title': 'Happy National Rachel Day!',
+            'display_id': 'happy-national-rachel-day-',
+            'timestamp': 1704333938,
+            'uploader': 'Devonanustart',
+            'channel_id': '4',
+            'duration': 13175,
+            'cast': ['Devonanustart'],
+            'description': '',
+            'categories': ['prank call'],
+            'upload_date': '20240104'
+        }
+    }, {
+        'url': 'https://prankcast.com/despicabledogs/posts/6217-jake-the-work-crow-',
+        'info_dict': {
+            'id': '6217',
+            'ext': 'mp3',
+            'title': 'Jake the Work Crow!',
+            'display_id': 'jake-the-work-crow-',
+            'timestamp': 1704346592,
+            'uploader': 'despicabledogs',
+            'channel_id': '957',
+            'duration': 263.287,
+            'cast': ['despicabledogs'],
+            'description': 'https://imgur.com/a/vtxLvKU',
+            'categories': [],
+            'upload_date': '20240104'
+        }
+    }]
+
+    def _real_extract(self, url):
+        video_id, display_id = self._match_valid_url(url).group('id', 'display_id')
+
+        webpage = self._download_webpage(url, video_id)
+        post = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['ssr_data_posts']
+        content = self._parse_json(post['post_contents_json'], video_id)[0]
+
+        uploader = post.get('user_name')
+        guests_json = traverse_obj(content, ('guests_json', {json.loads}, {dict})) or {}
+
+        return {
+            'id': video_id,
+            'title': post.get('post_title') or self._og_search_title(webpage),
+            'display_id': display_id,
+            'url': content.get('url'),
+            'timestamp': parse_iso8601(content.get('start_date') or content.get('crdate'), ' '),
+            'uploader': uploader,
+            'channel_id': str_or_none(post.get('user_id')),
+            'duration': float_or_none(content.get('duration')),
+            'cast': list(filter(None, [uploader] + traverse_obj(guests_json, (..., 'name')))),
+            'description': post.get('post_body'),
+            'categories': list(filter(None, [content.get('category')])),
+            'tags': try_call(lambda: list(filter('', post['post_tags'].split(',')))),
+            'subtitles': {
+                'live_chat': [{
+                    'url': f'https://prankcast.com/api/private/chat/select-broadcast?id={post["content_id"]}&cache=',
+                    'ext': 'json',
+                }],
+            } if post.get('content_id') else None
+        }
diff --git a/yt_dlp/extractor/radiko.py b/yt_dlp/extractor/radiko.py
index c363d9ba5..2b6405999 100644
--- a/yt_dlp/extractor/radiko.py
+++ b/yt_dlp/extractor/radiko.py
@@ -1,5 +1,6 @@
 import base64
 import random
+import re
 import urllib.parse
 
 from .common import InfoExtractor
@@ -11,6 +12,7 @@
     unified_timestamp,
     update_url_query,
 )
+from ..utils.traversal import traverse_obj
 
 
 class RadikoBaseIE(InfoExtractor):
@@ -159,6 +161,12 @@ def _extract_formats(self, video_id, station, is_onair, ft, cursor, auth_token,
 
         return formats
 
+    def _extract_performers(self, prog):
+        performers = traverse_obj(prog, (
+            'pfm/text()', ..., {lambda x: re.split(r'[/／、　,，]', x)}, ..., {str.strip}))
+        # TODO: change 'artist' fields to 'artists' and return traversal list instead of str
+        return ', '.join(performers) or None
+
 
 class RadikoIE(RadikoBaseIE):
     _VALID_URL = r'https?://(?:www\.)?radiko\.jp/#!/ts/(?P<station>[A-Z0-9-]+)/(?P<id>\d+)'
@@ -186,10 +194,12 @@ def _real_extract(self, url):
         return {
             'id': video_id,
             'title': try_call(lambda: prog.find('title').text),
+            'artist': self._extract_performers(prog),
             'description': clean_html(try_call(lambda: prog.find('info').text)),
             'uploader': try_call(lambda: station_program.find('.//name').text),
             'uploader_id': station,
             'timestamp': vid_int,
+            'duration': try_call(lambda: unified_timestamp(radio_end, False) - unified_timestamp(radio_begin, False)),
             'is_live': True,
             'formats': self._extract_formats(
                 video_id=video_id, station=station, is_onair=False,
@@ -243,6 +253,7 @@ def _real_extract(self, url):
         return {
             'id': station,
             'title': title,
+            'artist': self._extract_performers(prog),
             'description': description,
             'uploader': station_name,
             'uploader_id': station,
diff --git a/yt_dlp/extractor/redge.py b/yt_dlp/extractor/redge.py
new file mode 100644
index 000000000..875d6f8aa
--- /dev/null
+++ b/yt_dlp/extractor/redge.py
@@ -0,0 +1,135 @@
+import functools
+
+from .common import InfoExtractor
+from ..networking import HEADRequest
+from ..utils import (
+    float_or_none,
+    int_or_none,
+    join_nonempty,
+    parse_qs,
+    update_url_query,
+)
+from ..utils.traversal import traverse_obj
+
+
+class RedCDNLivxIE(InfoExtractor):
+    _VALID_URL = r'https?://[^.]+\.(?:dcs\.redcdn|atmcdn)\.pl/(?:live(?:dash|hls|ss)|nvr)/o2/(?P<tenant>[^/?#]+)/(?P<id>[^?#]+)\.livx'
+    IE_NAME = 'redcdnlivx'
+
+    _TESTS = [{
+        'url': 'https://r.dcs.redcdn.pl/livedash/o2/senat/ENC02/channel.livx?indexMode=true&startTime=638272860000&stopTime=638292544000',
+        'info_dict': {
+            'id': 'ENC02-638272860000-638292544000',
+            'ext': 'mp4',
+            'title': 'ENC02',
+            'duration': 19683.982,
+            'live_status': 'was_live',
+        },
+    }, {
+        'url': 'https://r.dcs.redcdn.pl/livedash/o2/sejm/ENC18/live.livx?indexMode=true&startTime=722333096000&stopTime=722335562000',
+        'info_dict': {
+            'id': 'ENC18-722333096000-722335562000',
+            'ext': 'mp4',
+            'title': 'ENC18',
+            'duration': 2463.995,
+            'live_status': 'was_live',
+        },
+    }, {
+        'url': 'https://r.dcs.redcdn.pl/livehls/o2/sportevolution/live/triathlon2018/warsaw.livx/playlist.m3u8?startTime=550305000000&stopTime=550327620000',
+        'info_dict': {
+            'id': 'triathlon2018-warsaw-550305000000-550327620000',
+            'ext': 'mp4',
+            'title': 'triathlon2018/warsaw',
+            'duration': 22619.98,
+            'live_status': 'was_live',
+        },
+    }, {
+        'url': 'https://n-25-12.dcs.redcdn.pl/nvr/o2/sejm/Migacz-ENC01/1.livx?startTime=722347200000&stopTime=722367345000',
+        'only_matching': True,
+    }, {
+        'url': 'https://redir.atmcdn.pl/nvr/o2/sejm/ENC08/1.livx?startTime=503831270000&stopTime=503840040000',
+        'only_matching': True,
+    }]
+
+    """
+    Known methods (first in url path):
+    - `livedash` - DASH MPD
+    - `livehls` - HTTP Live Streaming
+    - `livess` - IIS Smooth Streaming
+    - `nvr` - CCTV mode, directly returns a file, typically flv, avc1, aac
+    - `sc` - shoutcast/icecast (audio streams, like radio)
+    """
+
+    def _real_extract(self, url):
+        tenant, path = self._match_valid_url(url).group('tenant', 'id')
+        qs = parse_qs(url)
+        start_time = traverse_obj(qs, ('startTime', 0, {int_or_none}))
+        stop_time = traverse_obj(qs, ('stopTime', 0, {int_or_none}))
+
+        def livx_mode(mode):
+            suffix = ''
+            if mode == 'livess':
+                suffix = '/manifest'
+            elif mode == 'livehls':
+                suffix = '/playlist.m3u8'
+            file_qs = {}
+            if start_time:
+                file_qs['startTime'] = start_time
+            if stop_time:
+                file_qs['stopTime'] = stop_time
+            if mode == 'nvr':
+                file_qs['nolimit'] = 1
+            elif mode != 'sc':
+                file_qs['indexMode'] = 'true'
+            return update_url_query(f'https://r.dcs.redcdn.pl/{mode}/o2/{tenant}/{path}.livx{suffix}', file_qs)
+
+        # no id or title for a transmission. making ones up.
+        title = path \
+            .replace('/live', '').replace('live/', '') \
+            .replace('/channel', '').replace('channel/', '') \
+            .strip('/')
+        video_id = join_nonempty(title.replace('/', '-'), start_time, stop_time)
+
+        formats = []
+        # downloading the manifest separately here instead of _extract_ism_formats to also get some stream metadata
+        ism_res = self._download_xml_handle(
+            livx_mode('livess'), video_id,
+            note='Downloading ISM manifest',
+            errnote='Failed to download ISM manifest',
+            fatal=False)
+        ism_doc = None
+        if ism_res is not False:
+            ism_doc, ism_urlh = ism_res
+            formats, _ = self._parse_ism_formats_and_subtitles(ism_doc, ism_urlh.url, 'ss')
+
+        nvr_urlh = self._request_webpage(
+            HEADRequest(livx_mode('nvr')), video_id, 'Follow flv file redirect', fatal=False,
+            expected_status=lambda _: True)
+        if nvr_urlh and nvr_urlh.status == 200:
+            formats.append({
+                'url': nvr_urlh.url,
+                'ext': 'flv',
+                'format_id': 'direct-0',
+                'preference': -1,   # might be slow
+            })
+        formats.extend(self._extract_mpd_formats(livx_mode('livedash'), video_id, mpd_id='dash', fatal=False))
+        formats.extend(self._extract_m3u8_formats(
+            livx_mode('livehls'), video_id, m3u8_id='hls', ext='mp4', fatal=False))
+
+        time_scale = traverse_obj(ism_doc, ('@TimeScale', {int_or_none})) or 10000000
+        duration = traverse_obj(
+            ism_doc, ('@Duration', {functools.partial(float_or_none, scale=time_scale)})) or None
+
+        live_status = None
+        if traverse_obj(ism_doc, '@IsLive') == 'TRUE':
+            live_status = 'is_live'
+        elif duration:
+            live_status = 'was_live'
+
+        return {
+            'id': video_id,
+            'title': title,
+            'formats': formats,
+            'duration': duration,
+            'live_status': live_status,
+        }
diff --git a/yt_dlp/extractor/redtube.py b/yt_dlp/extractor/redtube.py
index 172c31b39..965abbee8 100644
--- a/yt_dlp/extractor/redtube.py
+++ b/yt_dlp/extractor/redtube.py
@@ -7,11 +7,12 @@
     str_to_int,
     unified_strdate,
     url_or_none,
+    urljoin,
 )
 
 
 class RedTubeIE(InfoExtractor):
-    _VALID_URL = r'https?://(?:(?:\w+\.)?redtube\.com/|embed\.redtube\.com/\?.*?\bid=)(?P<id>[0-9]+)'
+    _VALID_URL = r'https?://(?:(?:\w+\.)?redtube\.com(?:\.br)?/|embed\.redtube\.com/\?.*?\bid=)(?P<id>[0-9]+)'
     _EMBED_REGEX = [r'<iframe[^>]+?src=["\'](?P<url>(?:https?:)?//embed\.redtube\.com/\?.*?\bid=\d+)']
     _TESTS = [{
         'url': 'https://www.redtube.com/38864951',
@@ -34,6 +35,9 @@ class RedTubeIE(InfoExtractor):
     }, {
         'url': 'http://it.redtube.com/66418',
         'only_matching': True,
+    }, {
+        'url': 'https://www.redtube.com.br/103224331',
+        'only_matching': True,
     }]
 
     def _real_extract(self, url):
@@ -79,7 +83,7 @@ def _real_extract(self, url):
                 'media definitions', default='{}'),
             video_id, fatal=False)
         for media in medias if isinstance(medias, list) else []:
-            format_url = url_or_none(media.get('videoUrl'))
+            format_url = urljoin('https://www.redtube.com', media.get('videoUrl'))
             if not format_url:
                 continue
             format_id = media.get('format')
diff --git a/yt_dlp/extractor/rule34video.py b/yt_dlp/extractor/rule34video.py
index e6bb4258e..85ad7e2ff 100644
--- a/yt_dlp/extractor/rule34video.py
+++ b/yt_dlp/extractor/rule34video.py
@@ -18,10 +18,10 @@
 
 
 class Rule34VideoIE(InfoExtractor):
-    _VALID_URL = r'https?://(?:www\.)?rule34video\.com/videos/(?P<id>\d+)'
+    _VALID_URL = r'https?://(?:www\.)?rule34video\.com/videos?/(?P<id>\d+)'
     _TESTS = [
         {
-            'url': 'https://rule34video.com/videos/3065157/shot-it-mmd-hmv/',
+            'url': 'https://rule34video.com/video/3065157/shot-it-mmd-hmv/',
             'md5': 'ffccac2c23799dabbd192621ae4d04f3',
             'info_dict': {
                 'id': '3065157',
diff --git a/yt_dlp/extractor/rumble.py b/yt_dlp/extractor/rumble.py
index 85567d9a2..1dc049ac8 100644
--- a/yt_dlp/extractor/rumble.py
+++ b/yt_dlp/extractor/rumble.py
@@ -383,7 +383,7 @@ def entries(self, url, playlist_id):
                 if isinstance(e.cause, HTTPError) and e.cause.status == 404:
                     break
                 raise
-            for video_url in re.findall(r'class=video-item--a\s?href=([^>]+\.html)', webpage):
+            for video_url in re.findall(r'class="[^>"]*videostream__link[^>]+href="([^"]+\.html)"', webpage):
                 yield self.url_result('https://rumble.com' + video_url)
 
     def _real_extract(self, url):
diff --git a/yt_dlp/extractor/sejmpl.py b/yt_dlp/extractor/sejmpl.py
new file mode 100644
index 000000000..29cb0152a
--- /dev/null
+++ b/yt_dlp/extractor/sejmpl.py
@@ -0,0 +1,218 @@
+import datetime
+
+from .common import InfoExtractor
+from .redge import RedCDNLivxIE
+from ..utils import (
+    clean_html,
+    join_nonempty,
+    js_to_json,
+    strip_or_none,
+    update_url_query,
+)
+from ..utils.traversal import traverse_obj
+
+
+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)
+    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)
+    return int((date.timestamp() - 978307200) * 1000)
+
+
+class SejmIE(InfoExtractor):
+    _VALID_URL = (
+        r'https?://(?:www\.)?sejm\.gov\.pl/[Ss]ejm(?P<term>\d+)\.nsf/transmisje(?:_arch)?\.xsp(?:\?[^#]*)?#(?P<id>[\dA-F]+)',
+        r'https?://(?:www\.)?sejm\.gov\.pl/[Ss]ejm(?P<term>\d+)\.nsf/transmisje(?:_arch)?\.xsp\?(?:[^#]+&)?unid=(?P<id>[\dA-F]+)',
+        r'https?://sejm-embed\.redcdn\.pl/[Ss]ejm(?P<term>\d+)\.nsf/VideoFrame\.xsp/(?P<id>[\dA-F]+)',
+    )
+    IE_NAME = 'sejm'
+
+    _TESTS = [{
+        # multiple cameras, polish SL iterpreter
+        'url': 'https://www.sejm.gov.pl/Sejm10.nsf/transmisje_arch.xsp#6181EF1AD9CEEBB5C1258A6D006452B5',
+        'info_dict': {
+            'id': '6181EF1AD9CEEBB5C1258A6D006452B5',
+            'title': '1. posiedzenie Sejmu X kadencji',
+            'duration': 20145,
+            'live_status': 'was_live',
+            'location': 'Sala Posiedzeń',
+        },
+        'playlist': [{
+            'info_dict': {
+                'id': 'ENC01-722340000000-722360145000',
+                'ext': 'mp4',
+                'duration': 20145,
+                'title': '1. posiedzenie Sejmu X kadencji - ENC01',
+                'live_status': 'was_live',
+            },
+        }, {
+            'info_dict': {
+                'id': 'ENC30-722340000000-722360145000',
+                'ext': 'mp4',
+                'duration': 20145,
+                'title': '1. posiedzenie Sejmu X kadencji - ENC30',
+                'live_status': 'was_live',
+            },
+        }, {
+            'info_dict': {
+                'id': 'ENC31-722340000000-722360145000',
+                'ext': 'mp4',
+                'duration': 20145,
+                'title': '1. posiedzenie Sejmu X kadencji - ENC31',
+                'live_status': 'was_live',
+            },
+        }, {
+            'info_dict': {
+                'id': 'ENC32-722340000000-722360145000',
+                'ext': 'mp4',
+                'duration': 20145,
+                'title': '1. posiedzenie Sejmu X kadencji - ENC32',
+                'live_status': 'was_live',
+            },
+        }, {
+            # sign lang interpreter
+            'info_dict': {
+                'id': 'Migacz-ENC01-1-722340000000-722360145000',
+                'ext': 'mp4',
+                'duration': 20145,
+                'title': '1. posiedzenie Sejmu X kadencji - Migacz-ENC01',
+                'live_status': 'was_live',
+            },
+        }],
+    }, {
+        'url': 'https://www.sejm.gov.pl/Sejm8.nsf/transmisje.xsp?unid=9377A9D65518E9A5C125808E002E9FF2',
+        'info_dict': {
+            'id': '9377A9D65518E9A5C125808E002E9FF2',
+            'title': 'Debata "Lepsza Polska: obywatelska"',
+            'description': 'KP .Nowoczesna',
+            'duration': 8770,
+            'live_status': 'was_live',
+            'location': 'sala kolumnowa im. Kazimierza Pużaka (bud. C-D)',
+        },
+        'playlist': [{
+            'info_dict': {
+                'id': 'ENC08-1-503831270000-503840040000',
+                'ext': 'mp4',
+                'duration': 8770,
+                'title': 'Debata "Lepsza Polska: obywatelska" - ENC08',
+                'live_status': 'was_live',
+            },
+        }],
+    }, {
+        # 7th term is very special, since it does not use redcdn livx
+        'url': 'https://www.sejm.gov.pl/sejm7.nsf/transmisje_arch.xsp?rok=2015&month=11#A6E6D475ECCC6FE5C1257EF90034817F',
+        'info_dict': {
+            'id': 'A6E6D475ECCC6FE5C1257EF90034817F',
+            'title': 'Konferencja prasowa - Stanowisko SLD ws. składu nowego rządu',
+            'description': 'SLD - Biuro Prasowe Klubu',
+            'duration': 514,
+            'location': 'sala 101/bud. C',
+            'live_status': 'was_live',
+        },
+        'playlist': [{
+            'info_dict': {
+                'id': 'A6E6D475ECCC6FE5C1257EF90034817F',
+                'ext': 'mp4',
+                'title': 'Konferencja prasowa - Stanowisko SLD ws. składu nowego rządu',
+                'duration': 514,
+            },
+        }],
+    }, {
+        'url': 'https://sejm-embed.redcdn.pl/Sejm10.nsf/VideoFrame.xsp/FED58EABB97FBD53C1258A7400386492',
+        'only_matching': True,
+    }]
+
+    def _real_extract(self, url):
+        term, video_id = self._match_valid_url(url).group('term', 'id')
+        frame = self._download_webpage(
+            f'https://sejm-embed.redcdn.pl/Sejm{term}.nsf/VideoFrame.xsp/{video_id}',
+            video_id)
+        # despite it says "transmisje_arch", it works for live streams too!
+        data = self._download_json(
+            f'https://www.sejm.gov.pl/Sejm{term}.nsf/transmisje_arch.xsp/json/{video_id}',
+            video_id)
+        params = data['params']
+
+        title = strip_or_none(data.get('title'))
+
+        if data.get('status') == 'VIDEO_ENDED':
+            live_status = 'was_live'
+        elif data.get('status') == 'VIDEO_PLAYING':
+            live_status = 'is_live'
+        else:
+            live_status = None
+            self.report_warning(f'unknown status: {data.get("status")}')
+
+        start_time = rfc3339_to_atende(params['start'])
+        # current streams have a stop time of *expected* end of session, but actual times
+        # can change during the transmission. setting a stop_time would artificially
+        # end the stream at that time, while the session actually keeps going.
+        if live_status == 'was_live':
+            stop_time = rfc3339_to_atende(params['stop'])
+            duration = (stop_time - start_time) // 1000
+        else:
+            stop_time, duration = None, None
+
+        entries = []
+
+        def add_entry(file, legacy_file=False):
+            if not file:
+                return
+            file = self._proto_relative_url(file)
+            if not legacy_file:
+                file = update_url_query(file, {'startTime': start_time})
+                if stop_time is not None:
+                    file = update_url_query(file, {'stopTime': stop_time})
+                stream_id = self._search_regex(r'/o2/sejm/([^/]+)/[^./]+\.livx', file, 'stream id')
+            common_info = {
+                'url': file,
+                'duration': duration,
+            }
+            if legacy_file:
+                entries.append({
+                    **common_info,
+                    'id': video_id,
+                    'title': title,
+                })
+            else:
+                entries.append({
+                    **common_info,
+                    '_type': 'url_transparent',
+                    'ie_key': RedCDNLivxIE.ie_key(),
+                    'id': stream_id,
+                    'title': join_nonempty(title, stream_id, delim=' - '),
+                })
+
+        cameras = self._search_json(
+            r'var\s+cameras\s*=', frame, 'camera list', video_id,
+            contains_pattern=r'\[(?s:.+)\]', transform_source=js_to_json,
+            fatal=False) or []
+        for camera_file in traverse_obj(cameras, (..., 'file', {dict})):
+            if camera_file.get('flv'):
+                add_entry(camera_file['flv'])
+            elif camera_file.get('mp4'):
+                # this is only a thing in 7th term. no streams before, and starting 8th it's redcdn livx
+                add_entry(camera_file['mp4'], legacy_file=True)
+            else:
+                self.report_warning('Unknown camera stream type found')
+
+        if params.get('mig'):
+            add_entry(self._search_regex(r"var sliUrl\s*=\s*'([^']+)'", frame, 'sign language interpreter url', fatal=False))
+
+        return {
+            '_type': 'playlist',
+            'entries': entries,
+            'id': video_id,
+            'title': title,
+            'description': clean_html(data.get('desc')) or None,
+            'duration': duration,
+            'live_status': live_status,
+            'location': strip_or_none(data.get('location')),
+        }
diff --git a/yt_dlp/extractor/txxx.py b/yt_dlp/extractor/txxx.py
index fff7a5d76..77dabbc82 100644
--- a/yt_dlp/extractor/txxx.py
+++ b/yt_dlp/extractor/txxx.py
@@ -10,6 +10,7 @@
     parse_duration,
     traverse_obj,
     try_call,
+    url_or_none,
     urljoin,
     variadic,
 )
@@ -83,6 +84,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.txxx.tube/contents/videos_sources/16574000/16574965/screenshots/1.jpg',
         }
     }, {
         'url': 'https://txxx.tube/videos/16574965/digital-desire-malena-morgan/',
@@ -98,6 +100,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.txxx.tube/contents/videos_sources/16574000/16574965/screenshots/1.jpg',
         }
     }, {
         'url': 'https://vxxx.com/video-68925/',
@@ -113,6 +116,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.vxxx.com/contents/videos_sources/68000/68925/screenshots/1.jpg',
         }
     }, {
         'url': 'https://hclips.com/videos/6291073/malena-morgan-masturbates-her-sweet/',
@@ -128,6 +132,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://hctn.nv7s.com/contents/videos_sources/6291000/6291073/screenshots/1.jpg',
         }
     }, {
         'url': 'https://hdzog.com/videos/67063/gorgeous-malena-morgan-will-seduce-you-at-the-first-glance/',
@@ -143,6 +148,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.hdzog.com/contents/videos_sources/67000/67063/screenshots/1.jpg',
         }
     }, {
         'url': 'https://hdzog.tube/videos/67063/gorgeous-malena-morgan-will-seduce-you-at-the-first-glance/',
@@ -158,6 +164,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.hdzog.com/contents/videos_sources/67000/67063/screenshots/1.jpg',
         }
     }, {
         'url': 'https://hotmovs.com/videos/8789287/unbelievable-malena-morgan-performing-in-incredible-masturantion/',
@@ -173,6 +180,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.hotmovs.com/contents/videos_sources/8789000/8789287/screenshots/10.jpg',
         }
     }, {
         'url': 'https://hotmovs.tube/videos/8789287/unbelievable-malena-morgan-performing-in-incredible-masturantion/',
@@ -188,6 +196,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.hotmovs.com/contents/videos_sources/8789000/8789287/screenshots/10.jpg',
         }
     }, {
         'url': 'https://inporn.com/video/517897/malena-morgan-solo/',
@@ -203,6 +212,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://iptn.m3pd.com/media/tn/sources/517897_1.jpg',
         }
     }, {
         'url': 'https://privatehomeclips.com/videos/3630599/malena-morgan-cam-show/',
@@ -218,6 +228,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://hctn.nv7s.com/contents/videos_sources/3630000/3630599/screenshots/15.jpg',
         }
     }, {
         'url': 'https://tubepornclassic.com/videos/1015455/mimi-rogers-full-body-massage-nude-compilation/',
@@ -233,6 +244,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.tubepornclassic.com/contents/videos_sources/1015000/1015455/screenshots/6.jpg',
         }
     }, {
         'url': 'https://upornia.com/videos/1498858/twistys-malena-morgan-starring-at-dr-morgan-baller/',
@@ -248,6 +260,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.upornia.com/contents/videos_sources/1498000/1498858/screenshots/1.jpg',
         }
     }, {
         'url': 'https://upornia.tube/videos/1498858/twistys-malena-morgan-starring-at-dr-morgan-baller/',
@@ -263,6 +276,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.upornia.com/contents/videos_sources/1498000/1498858/screenshots/1.jpg',
         }
     }, {
         'url': 'https://vjav.com/videos/11761/yui-hatano-in-if-yui-was-my-girlfriend2/',
@@ -278,6 +292,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.vjav.com/contents/videos_sources/11000/11761/screenshots/23.jpg',
         }
     }, {
         'url': 'https://vjav.tube/videos/11761/yui-hatano-in-if-yui-was-my-girlfriend2/',
@@ -293,6 +308,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.vjav.com/contents/videos_sources/11000/11761/screenshots/23.jpg',
         }
     }, {
         'url': 'https://voyeurhit.com/videos/332875/charlotte-stokely-elle-alexandra-malena-morgan-lingerie/',
@@ -308,6 +324,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.voyeurhit.com/contents/videos_sources/332000/332875/screenshots/1.jpg',
         }
     }, {
         'url': 'https://voyeurhit.tube/videos/332875/charlotte-stokely-elle-alexandra-malena-morgan-lingerie/',
@@ -323,6 +340,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://tn.voyeurhit.com/contents/videos_sources/332000/332875/screenshots/1.jpg',
         }
     }]
     _WEBPAGE_TESTS = [{
@@ -338,6 +356,7 @@ class TxxxIE(InfoExtractor):
             'like_count': int,
             'dislike_count': int,
             'age_limit': 18,
+            'thumbnail': 'https://hctn.nv7s.com/contents/videos_sources/5119000/5119660/screenshots/1.jpg',
         }
     }]
 
@@ -371,6 +390,7 @@ def _real_extract(self, url):
             'like_count': int_or_none(traverse_obj(video_info, ('video', 'statistics', 'likes'))),
             'dislike_count': int_or_none(traverse_obj(video_info, ('video', 'statistics', 'dislikes'))),
             'age_limit': 18,
+            'thumbnail': traverse_obj(video_info, ('video', 'thumbsrc', {url_or_none})),
             'formats': get_formats(host, video_file),
         }
 
diff --git a/yt_dlp/extractor/vbox7.py b/yt_dlp/extractor/vbox7.py
index be35dad1c..21bf4232b 100644
--- a/yt_dlp/extractor/vbox7.py
+++ b/yt_dlp/extractor/vbox7.py
@@ -1,5 +1,6 @@
 from .common import InfoExtractor
-from ..utils import ExtractorError
+from ..utils import ExtractorError, base_url, int_or_none, url_basename
+from ..utils.traversal import traverse_obj
 
 
 class Vbox7IE(InfoExtractor):
@@ -19,7 +20,7 @@ class Vbox7IE(InfoExtractor):
     _GEO_COUNTRIES = ['BG']
     _TESTS = [{
         'url': 'http://vbox7.com/play:0946fff23c',
-        'md5': 'a60f9ab3a3a2f013ef9a967d5f7be5bf',
+        'md5': '50ca1f78345a9c15391af47d8062d074',
         'info_dict': {
             'id': '0946fff23c',
             'ext': 'mp4',
@@ -29,19 +30,25 @@ class Vbox7IE(InfoExtractor):
             'timestamp': 1470982814,
             'upload_date': '20160812',
             'uploader': 'zdraveibulgaria',
-        },
-        'params': {
-            'proxy': '127.0.0.1:8118',
+            'view_count': int,
+            'duration': 2640,
         },
     }, {
         'url': 'http://vbox7.com/play:249bb972c2',
-        'md5': '99f65c0c9ef9b682b97313e052734c3f',
+        'md5': 'da1dd2eb245200cb86e6d09d43232116',
         'info_dict': {
             'id': '249bb972c2',
             'ext': 'mp4',
             'title': 'Смях! Чудо - чист за секунди - Скрита камера',
+            'uploader': 'svideteliat_ot_varshava',
+            'view_count': int,
+            'timestamp': 1360215023,
+            'thumbnail': 'https://i49.vbox7.com/design/iconci/png/noimg6.png',
+            'description': 'Смях! Чудо - чист за секунди - Скрита камера',
+            'upload_date': '20130207',
+            'duration': 83,
         },
-        'skip': 'georestricted',
+        'expected_warnings': ['Failed to download m3u8 information'],
     }, {
         'url': 'http://vbox7.com/emb/external.php?vid=a240d20f9c&autoplay=1',
         'only_matching': True,
@@ -53,41 +60,38 @@ class Vbox7IE(InfoExtractor):
     def _real_extract(self, url):
         video_id = self._match_id(url)
 
-        response = self._download_json(
-            'https://www.vbox7.com/ajax/video/nextvideo.php?vid=%s' % video_id,
-            video_id)
-
-        if 'error' in response:
-            raise ExtractorError(
-                '%s said: %s' % (self.IE_NAME, response['error']), expected=True)
-
-        video = response['options']
-
-        title = video['title']
-        video_url = video['src']
-
-        if '/na.mp4' in video_url:
-            self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
+        data = self._download_json(
+            'https://www.vbox7.com/aj/player/item/options', video_id,
+            query={'vid': video_id})['options']
 
-        uploader = video.get('uploader')
+        src_url = data.get('src')
+        if src_url in (None, '', 'blank'):
+            raise ExtractorError('Video is unavailable', expected=True)
 
-        webpage = self._download_webpage(
-            'http://vbox7.com/play:%s' % video_id, video_id, fatal=None)
+        fmt_base = url_basename(src_url).rsplit('.', 1)[0].rsplit('_', 1)[0]
+        if fmt_base == 'vn':
+            self.raise_geo_restricted()
 
-        info = {}
+        fmt_base = base_url(src_url) + fmt_base
 
-        if webpage:
-            info = self._search_json_ld(
-                webpage.replace('"/*@context"', '"@context"'), video_id,
-                fatal=False)
+        formats = self._extract_m3u8_formats(
+            f'{fmt_base}.m3u8', video_id, m3u8_id='hls', fatal=False)
+        # TODO: Add MPD formats, when dash range support is added
+        for res in traverse_obj(data, ('resolutions', lambda _, v: v != 0, {int})):
+            formats.append({
+                'url': f'{fmt_base}_{res}.mp4',
+                'format_id': f'http-{res}',
+                'height': res,
+            })
 
-        info.update({
+        return {
             'id': video_id,
-            'title': title,
-            'url': video_url,
-            'uploader': uploader,
-            'thumbnail': self._proto_relative_url(
-                info.get('thumbnail') or self._og_search_thumbnail(webpage),
-                'http:'),
-        })
-        return info
+            'formats': formats,
+            **self._search_json_ld(self._download_webpage(
+                f'https://www.vbox7.com/play:{video_id}', video_id, fatal=False) or '', video_id, fatal=False),
+            **traverse_obj(data, {
+                'title': ('title', {str}),
+                'uploader': ('uploader', {str}),
+                'duration': ('duration', {int_or_none}),
+            }),
+        }
diff --git a/yt_dlp/extractor/viewlift.py b/yt_dlp/extractor/viewlift.py
index 8f686f05d..c93be5f3d 100644
--- a/yt_dlp/extractor/viewlift.py
+++ b/yt_dlp/extractor/viewlift.py
@@ -12,7 +12,7 @@
 
 class ViewLiftBaseIE(InfoExtractor):
     _API_BASE = 'https://prod-api.viewlift.com/'
-    _DOMAINS_REGEX = r'(?:(?:main\.)?snagfilms|snagxtreme|funnyforfree|kiddovid|winnersview|(?:monumental|lax)sportsnetwork|vayafilm|failarmy|ftfnext|lnppass\.legapallacanestro|moviespree|app\.myoutdoortv|neoufitness|pflmma|theidentitytb)\.com|(?:hoichoi|app\.horseandcountry|kronon|marquee|supercrosslive)\.tv'
+    _DOMAINS_REGEX = r'(?:(?:main\.)?snagfilms|snagxtreme|funnyforfree|kiddovid|winnersview|(?:monumental|lax)sportsnetwork|vayafilm|failarmy|ftfnext|lnppass\.legapallacanestro|moviespree|app\.myoutdoortv|neoufitness|pflmma|theidentitytb|chorki)\.com|(?:hoichoi|app\.horseandcountry|kronon|marquee|supercrosslive)\.tv'
     _SITE_MAP = {
         'ftfnext': 'lax',
         'funnyforfree': 'snagfilms',
@@ -27,6 +27,7 @@ class ViewLiftBaseIE(InfoExtractor):
         'snagxtreme': 'snagfilms',
         'theidentitytb': 'tampabay',
         'vayafilm': 'snagfilms',
+        'chorki': 'prothomalo',
     }
     _TOKENS = {}
 
@@ -296,6 +297,33 @@ class ViewLiftIE(ViewLiftBaseIE):
     }, {  # Premium movie
         'url': 'https://www.hoichoi.tv/movies/detective-2020',
         'only_matching': True
+    }, {  # Chorki Premium series
+        'url': 'https://www.chorki.com/bn/series/sinpaat',
+        'playlist_mincount': 7,
+        'info_dict': {
+            'id': 'bn/series/sinpaat',
+        },
+    }, {  # Chorki free movie
+        'url': 'https://www.chorki.com/bn/videos/bangla-movie-bikkhov',
+        'info_dict': {
+            'id': '564e755b-f5c7-4515-aee6-8959bee18c93',
+            'title': 'Bikkhov',
+            'ext': 'mp4',
+            'upload_date': '20230824',
+            'timestamp': 1692860553,
+            'categories': ['Action Movies', 'Salman Special'],
+            'tags': 'count:14',
+            'thumbnail': 'https://snagfilms-a.akamaihd.net/dd078ff5-b16e-45e4-9723-501b56b9df0a/images/2023/08/24/1692860450729_1920x1080_16x9Images.jpg',
+            'display_id': 'bn/videos/bangla-movie-bikkhov',
+            'description': 'md5:71492b086450625f4374a3eb824f27dc',
+            'duration': 8002,
+        },
+        'params': {
+            'skip_download': True,
+        },
+    }, {  # Chorki Premium movie
+        'url': 'https://www.chorki.com/bn/videos/something-like-an-autobiography',
+        'only_matching': True,
     }]
 
     @classmethod
diff --git a/yt_dlp/extractor/vimeo.py b/yt_dlp/extractor/vimeo.py
index e5e8144bb..208e11184 100644
--- a/yt_dlp/extractor/vimeo.py
+++ b/yt_dlp/extractor/vimeo.py
@@ -269,7 +269,7 @@ def _extract_original_format(self, url, video_id, unlisted_hash=None):
             'https://vimeo.com/_rv/viewer', video_id, note='Downloading jwt token', fatal=False) or {}
         if not jwt_response.get('jwt'):
             return
-        headers = {'Authorization': 'jwt %s' % jwt_response['jwt']}
+        headers = {'Authorization': 'jwt %s' % jwt_response['jwt'], 'Accept': 'application/json'}
         original_response = self._download_json(
             f'https://api.vimeo.com/videos/{video_id}', video_id,
             headers=headers, fatal=False, expected_status=(403, 404)) or {}
@@ -751,6 +751,7 @@ def _extract_from_api(self, video_id, unlisted_hash=None):
         video = self._download_json(
             api_url, video_id, headers={
                 'Authorization': 'jwt ' + token,
+                'Accept': 'application/json',
             }, query={
                 'fields': 'config_url,created_time,description,license,metadata.connections.comments.total,metadata.connections.likes.total,release_time,stats.plays',
             })
@@ -785,7 +786,7 @@ def _try_album_password(self, url):
         jwt = viewer['jwt']
         album = self._download_json(
             'https://api.vimeo.com/albums/' + album_id,
-            album_id, headers={'Authorization': 'jwt ' + jwt},
+            album_id, headers={'Authorization': 'jwt ' + jwt, 'Accept': 'application/json'},
             query={'fields': 'description,name,privacy'})
         if try_get(album, lambda x: x['privacy']['view']) == 'password':
             password = self.get_param('videopassword')
@@ -1147,10 +1148,12 @@ def _fetch_page(self, album_id, authorization, hashed_pass, page):
                 'https://api.vimeo.com/albums/%s/videos' % album_id,
                 album_id, 'Downloading page %d' % api_page, query=query, headers={
                     'Authorization': 'jwt ' + authorization,
+                    'Accept': 'application/json',
                 })['data']
         except ExtractorError as e:
             if isinstance(e.cause, HTTPError) and e.cause.status == 400:
                 return
+            raise
         for video in videos:
             link = video.get('link')
             if not link:
@@ -1171,7 +1174,7 @@ def _real_extract(self, url):
         jwt = viewer['jwt']
         album = self._download_json(
             'https://api.vimeo.com/albums/' + album_id,
-            album_id, headers={'Authorization': 'jwt ' + jwt},
+            album_id, headers={'Authorization': 'jwt ' + jwt, 'Accept': 'application/json'},
             query={'fields': 'description,name,privacy'})
         hashed_pass = None
         if try_get(album, lambda x: x['privacy']['view']) == 'password':
diff --git a/yt_dlp/extractor/zetland.py b/yt_dlp/extractor/zetland.py
new file mode 100644
index 000000000..055a643b3
--- /dev/null
+++ b/yt_dlp/extractor/zetland.py
@@ -0,0 +1,71 @@
+from .common import InfoExtractor
+from ..utils import merge_dicts, unified_timestamp, url_or_none
+from ..utils.traversal import traverse_obj
+
+
+class ZetlandDKArticleIE(InfoExtractor):
+    _VALID_URL = r'https?://www\.zetland\.dk/\w+/(?P<id>(?P<story_id>\w{8})-(?P<uploader_id>\w{8})-(?:\w{5}))'
+    _TESTS = [{
+        'url': 'https://www.zetland.dk/historie/sO9aq2MY-a81VP3BY-66e69?utm_source=instagram&utm_medium=linkibio&utm_campaign=artikel',
+        'info_dict': {
+            'id': 'sO9aq2MY-a81VP3BY-66e69',
+            'ext': 'mp3',
+            'modified_date': '20240118',
+            'title': 'Afsnit 1: “Det føltes som en kidnapning.” ',
+            'upload_date': '20240116',
+            'uploader_id': 'a81VP3BY',
+            'modified_timestamp': 1705568739,
+            'release_timestamp': 1705377592,
+            'uploader_url': 'https://www.zetland.dk/skribent/a81VP3BY',
+            'uploader': 'Helle Fuusager',
+            'release_date': '20240116',
+            'thumbnail': r're:https://zetland\.imgix\.net/2aafe500-b14e-11ee-bf83-65d5e1283a57/Zetland_Image_1\.jpg',
+            'description': 'md5:9619d426772c133f5abb26db27f26a01',
+            'timestamp': 1705377592,
+            'series_id': '62d54630-e87b-4ab1-a255-8de58dbe1b14',
+        }
+
+    }]
+
+    def _real_extract(self, url):
+        display_id, uploader_id = self._match_valid_url(url).group('id', 'uploader_id')
+        webpage = self._download_webpage(url, display_id)
+
+        next_js_data = self._search_nextjs_data(webpage, display_id)['props']['pageProps']
+        story_data = traverse_obj(next_js_data, ('initialState', 'consume', 'story', 'story'))
+
+        formats = []
+        for audio_url in traverse_obj(story_data, ('story_content', 'meta', 'audioFiles', ..., {url_or_none})):
+            formats.append({
+                'url': audio_url,
+                'vcodec': 'none',
+            })
+
+        return merge_dicts({
+            'id': display_id,
+            'formats': formats,
+            'uploader_id': uploader_id
+        }, traverse_obj(story_data, {
+            'title': ((('story_content', 'content', 'title'), 'title'), {str}),
+            'uploader': ('sharer', 'name'),
+            'uploader_id': ('sharer', 'sharer_id'),
+            'description': ('story_content', 'content', 'socialDescription'),
+            'series_id': ('story_content', 'meta', 'seriesId'),
+            'release_timestamp': ('published_at', {unified_timestamp}),
+            'modified_timestamp': ('revised_at', {unified_timestamp}),
+        }, get_all=False), traverse_obj(next_js_data, ('metaInfo', {
+            'title': ((('meta', 'title'), ('ld', 'headline'), ('og', 'og:title'), ('og', 'twitter:title')), {str}),
+            'description': ((('meta', 'description'), ('ld', 'description'), ('og', 'og:description'), ('og', 'twitter:description')), {str}),
+            'uploader': ((('meta', 'author'), ('ld', 'author', 'name')), {str}),
+            'uploader_url': ('ld', 'author', 'url', {url_or_none}),
+            'thumbnail': ((('ld', 'image'), ('og', 'og:image'), ('og', 'twitter:image')), {url_or_none}),
+            'modified_timestamp': ('ld', 'dateModified', {unified_timestamp}),
+            'release_timestamp': ('ld', 'datePublished', {unified_timestamp}),
+            'timestamp': ('ld', 'dateCreated', {unified_timestamp}),
+        }), get_all=False), {
+            'title': self._html_search_meta(['title', 'og:title', 'twitter:title'], webpage),
+            'description': self._html_search_meta(['description', 'og:description', 'twitter:description'], webpage),
+            'thumbnail': self._html_search_meta(['og:image', 'twitter:image'], webpage),
+            'uploader': self._html_search_meta(['author'], webpage),
+            'release_timestamp': unified_timestamp(self._html_search_meta(['article:published_time'], webpage)),
+        }, self._search_json_ld(webpage, display_id, fatal=False))
diff --git a/yt_dlp/networking/_requests.py b/yt_dlp/networking/_requests.py
index e129110ca..00e4bdb49 100644
--- a/yt_dlp/networking/_requests.py
+++ b/yt_dlp/networking/_requests.py
@@ -8,6 +8,7 @@
 
 from ..dependencies import brotli, requests, urllib3
 from ..utils import bug_reports_message, int_or_none, variadic
+from ..utils.networking import normalize_url
 
 if requests is None:
     raise ImportError('requests module is not installed')
@@ -199,6 +200,10 @@ def rebuild_method(self, prepared_request, response):
 
         prepared_request.method = new_method
 
+        # Requests fails to resolve dot segments on absolute redirect locations
+        # See: https://github.com/yt-dlp/yt-dlp/issues/9020
+        prepared_request.url = normalize_url(prepared_request.url)
+
     def rebuild_auth(self, prepared_request, response):
         # HACK: undo status code change from rebuild_method, if applicable.
         # rebuild_auth runs after requests would remove headers/body based on status code
diff --git a/yt_dlp/networking/exceptions.py b/yt_dlp/networking/exceptions.py
index 12441901c..9037f18e2 100644
--- a/yt_dlp/networking/exceptions.py
+++ b/yt_dlp/networking/exceptions.py
@@ -1,9 +1,8 @@
 from __future__ import annotations
 
 import typing
-import urllib.error
 
-from ..utils import YoutubeDLError, deprecation_warning
+from ..utils import YoutubeDLError
 
 if typing.TYPE_CHECKING:
     from .common import RequestHandler, Response
@@ -101,117 +100,4 @@ class ProxyError(TransportError):
     pass
 
 
-class _CompatHTTPError(urllib.error.HTTPError, HTTPError):
-    """
-    Provides backwards compatibility with urllib.error.HTTPError.
-    Do not use this class directly, use HTTPError instead.
-    """
-
-    def __init__(self, http_error: HTTPError):
-        super().__init__(
-            url=http_error.response.url,
-            code=http_error.status,
-            msg=http_error.msg,
-            hdrs=http_error.response.headers,
-            fp=http_error.response
-        )
-        self._closer.close_called = True  # Disable auto close
-        self._http_error = http_error
-        HTTPError.__init__(self, http_error.response, redirect_loop=http_error.redirect_loop)
-
-    @property
-    def status(self):
-        return self._http_error.status
-
-    @status.setter
-    def status(self, value):
-        return
-
-    @property
-    def reason(self):
-        return self._http_error.reason
-
-    @reason.setter
-    def reason(self, value):
-        return
-
-    @property
-    def headers(self):
-        deprecation_warning('HTTPError.headers is deprecated, use HTTPError.response.headers instead')
-        return self._http_error.response.headers
-
-    @headers.setter
-    def headers(self, value):
-        return
-
-    def info(self):
-        deprecation_warning('HTTPError.info() is deprecated, use HTTPError.response.headers instead')
-        return self.response.headers
-
-    def getcode(self):
-        deprecation_warning('HTTPError.getcode is deprecated, use HTTPError.status instead')
-        return self.status
-
-    def geturl(self):
-        deprecation_warning('HTTPError.geturl is deprecated, use HTTPError.response.url instead')
-        return self.response.url
-
-    @property
-    def code(self):
-        deprecation_warning('HTTPError.code is deprecated, use HTTPError.status instead')
-        return self.status
-
-    @code.setter
-    def code(self, value):
-        return
-
-    @property
-    def url(self):
-        deprecation_warning('HTTPError.url is deprecated, use HTTPError.response.url instead')
-        return self.response.url
-
-    @url.setter
-    def url(self, value):
-        return
-
-    @property
-    def hdrs(self):
-        deprecation_warning('HTTPError.hdrs is deprecated, use HTTPError.response.headers instead')
-        return self.response.headers
-
-    @hdrs.setter
-    def hdrs(self, value):
-        return
-
-    @property
-    def filename(self):
-        deprecation_warning('HTTPError.filename is deprecated, use HTTPError.response.url instead')
-        return self.response.url
-
-    @filename.setter
-    def filename(self, value):
-        return
-
-    def __getattr__(self, name):
-        # File operations are passed through the response.
-        # Warn for some commonly used ones
-        passthrough_warnings = {
-            'read': 'response.read()',
-            # technically possibly due to passthrough, but we should discourage this
-            'get_header': 'response.get_header()',
-            'readable': 'response.readable()',
-            'closed': 'response.closed',
-            'tell': 'response.tell()',
-        }
-        if name in passthrough_warnings:
-            deprecation_warning(f'HTTPError.{name} is deprecated, use HTTPError.{passthrough_warnings[name]} instead')
-        return super().__getattr__(name)
-
-    def __str__(self):
-        return str(self._http_error)
-
-    def __repr__(self):
-        return repr(self._http_error)
-
-
 network_exceptions = (HTTPError, TransportError)
diff --git a/yt_dlp/options.py b/yt_dlp/options.py
index e9d927717..9bea6549d 100644
--- a/yt_dlp/options.py
+++ b/yt_dlp/options.py
@@ -476,7 +476,8 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
                 'youtube-dl': ['all', '-multistreams', '-playlist-match-filter', '-manifest-filesize-approx'],
                 'youtube-dlc': ['all', '-no-youtube-channel-redirect', '-no-live-chat', '-playlist-match-filter', '-manifest-filesize-approx'],
                 '2021': ['2022', 'no-certifi', 'filename-sanitization', 'no-youtube-prefer-utc-upload-date'],
-                '2022': ['no-external-downloader-progress', 'playlist-match-filter', 'prefer-legacy-http-handler', 'manifest-filesize-approx'],
+                '2022': ['2023', 'no-external-downloader-progress', 'playlist-match-filter'],
+                '2023': ['prefer-legacy-http-handler', 'manifest-filesize-approx'],
             }
         }, help=(
             'Options that can help keep compatibility with youtube-dl or youtube-dlc '


Test patch:
diff --git a/test/test_networking.py b/test/test_networking.py
index 62325aa8e..8cadd86f5 100644
--- a/test/test_networking.py
+++ b/test/test_networking.py
@@ -180,6 +180,12 @@ def do_GET(self):
             self.send_header('Location', '/a/b/./../../headers')
             self.send_header('Content-Length', '0')
             self.end_headers()
+        elif self.path == '/redirect_dotsegments_absolute':
+            self.send_response(301)
+            # redirect to /headers but with dot segments before - absolute url
+            self.send_header('Location', f'http://127.0.0.1:{http_server_port(self.server)}/a/b/./../../headers')
+            self.send_header('Content-Length', '0')
+            self.end_headers()
         elif self.path.startswith('/redirect_'):
             self._redirect()
         elif self.path.startswith('/method'):
@@ -345,16 +351,17 @@ def test_percent_encode(self, handler):
             res.close()
 
     @pytest.mark.parametrize('handler', ['Urllib', 'Requests'], indirect=True)
-    def test_remove_dot_segments(self, handler):
-        with handler() as rh:
+    @pytest.mark.parametrize('path', [
+        '/a/b/./../../headers',
+        '/redirect_dotsegments',
+        # https://github.com/yt-dlp/yt-dlp/issues/9020
+        '/redirect_dotsegments_absolute',
+    ])
+    def test_remove_dot_segments(self, handler, path):
+        with handler(verbose=True) as rh:
             # This isn't a comprehensive test,
-            # but it should be enough to check whether the handler is removing dot segments
-            res = validate_and_send(rh, Request(f'http://127.0.0.1:{self.http_port}/a/b/./../../headers'))
-            assert res.status == 200
-            assert res.url == f'http://127.0.0.1:{self.http_port}/headers'
-            res.close()
-
-            res = validate_and_send(rh, Request(f'http://127.0.0.1:{self.http_port}/redirect_dotsegments'))
+            # but it should be enough to check whether the handler is removing dot segments in required scenarios
+            res = validate_and_send(rh, Request(f'http://127.0.0.1:{self.http_port}{path}'))
             assert res.status == 200
             assert res.url == f'http://127.0.0.1:{self.http_port}/headers'
             res.close()
diff --git a/test/test_networking_utils.py b/test/test_networking_utils.py
index 419aae1e4..b7b71430e 100644
--- a/test/test_networking_utils.py
+++ b/test/test_networking_utils.py
@@ -8,13 +8,9 @@
 
 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 
-import contextlib
 import io
-import platform
 import random
 import ssl
-import urllib.error
-import warnings
 
 from yt_dlp.cookies import YoutubeDLCookieJar
 from yt_dlp.dependencies import certifi
@@ -30,7 +26,6 @@
 from yt_dlp.networking.exceptions import (
     HTTPError,
     IncompleteRead,
-    _CompatHTTPError,
 )
 from yt_dlp.socks import ProxyType
 from yt_dlp.utils.networking import HTTPHeaderDict
@@ -179,11 +174,10 @@ class TestNetworkingExceptions:
     def create_response(status):
         return Response(fp=io.BytesIO(b'test'), url='http://example.com', headers={'tesT': 'test'}, status=status)
 
-    @pytest.mark.parametrize('http_error_class', [HTTPError, lambda r: _CompatHTTPError(HTTPError(r))])
-    def test_http_error(self, http_error_class):
+    def test_http_error(self):
 
         response = self.create_response(403)
-        error = http_error_class(response)
+        error = HTTPError(response)
 
         assert error.status == 403
         assert str(error) == error.msg == 'HTTP Error 403: Forbidden'
@@ -194,80 +188,12 @@ def test_http_error(self, http_error_class):
         assert data == b'test'
         assert repr(error) == '<HTTPError 403: Forbidden>'
 
-    @pytest.mark.parametrize('http_error_class', [HTTPError, lambda *args, **kwargs: _CompatHTTPError(HTTPError(*args, **kwargs))])
-    def test_redirect_http_error(self, http_error_class):
+    def test_redirect_http_error(self):
         response = self.create_response(301)
-        error = http_error_class(response, redirect_loop=True)
+        error = HTTPError(response, redirect_loop=True)
         assert str(error) == error.msg == 'HTTP Error 301: Moved Permanently (redirect loop detected)'
         assert error.reason == 'Moved Permanently'
 
-    def test_compat_http_error(self):
-        response = self.create_response(403)
-        error = _CompatHTTPError(HTTPError(response))
-        assert isinstance(error, HTTPError)
-        assert isinstance(error, urllib.error.HTTPError)
-
-        @contextlib.contextmanager
-        def raises_deprecation_warning():
-            with warnings.catch_warnings(record=True) as w:
-                warnings.simplefilter('always')
-                yield
-
-                if len(w) == 0:
-                    pytest.fail('Did not raise DeprecationWarning')
-                if len(w) > 1:
-                    pytest.fail(f'Raised multiple warnings: {w}')
-
-                if not issubclass(w[-1].category, DeprecationWarning):
-                    pytest.fail(f'Expected DeprecationWarning, got {w[-1].category}')
-                w.clear()
-
-        with raises_deprecation_warning():
-            assert error.code == 403
-
-        with raises_deprecation_warning():
-            assert error.getcode() == 403
-
-        with raises_deprecation_warning():
-            assert error.hdrs is error.response.headers
-
-        with raises_deprecation_warning():
-            assert error.info() is error.response.headers
-
-        with raises_deprecation_warning():
-            assert error.headers is error.response.headers
-
-        with raises_deprecation_warning():
-            assert error.filename == error.response.url
-
-        with raises_deprecation_warning():
-            assert error.url == error.response.url
-
-        with raises_deprecation_warning():
-            assert error.geturl() == error.response.url
-
-        # Passthrough file operations
-        with raises_deprecation_warning():
-            assert error.read() == b'test'
-
-        with raises_deprecation_warning():
-            assert not error.closed
-
-        with raises_deprecation_warning():
-            # Technically Response operations are also passed through, which should not be used.
-            assert error.get_header('test') == 'test'
-
-        # Should not raise a warning
-        error.close()
-
-    @pytest.mark.skipif(
-        platform.python_implementation() == 'PyPy', reason='garbage collector works differently in pypy')
-    def test_compat_http_error_autoclose(self):
-        # Compat HTTPError should not autoclose response
-        response = self.create_response(403)
-        _CompatHTTPError(HTTPError(response))
-        assert not response.closed
-
     def test_incomplete_read_error(self):
         error = IncompleteRead(4, 3, cause='test')
         assert isinstance(error, IncompleteRead)