]> jfr.im git - yt-dlp.git/blobdiff - yt_dlp/extractor/generic.py
[extractor/generic] Add `fragment_query` extractor arg for DASH and HLS (#5528)
[yt-dlp.git] / yt_dlp / extractor / generic.py
index 828c8a6cff4ef1a8fadf0ee4aa988ea517dc2e8c..2fcbc6f43f830c68f51547b95d7de9952f89b43a 100644 (file)
@@ -1,15 +1,16 @@
 import os
 import re
+import types
 import urllib.parse
 import xml.etree.ElementTree
 
 from .common import InfoExtractor  # isort: split
-from .brightcove import BrightcoveLegacyIE, BrightcoveNewIE
 from .commonprotocols import RtmpIE
 from .youtube import YoutubeIE
 from ..compat import compat_etree_fromstring
 from ..utils import (
     KNOWN_EXTENSIONS,
+    MEDIA_EXTENSIONS,
     ExtractorError,
     UnsupportedError,
     determine_ext,
@@ -31,6 +32,7 @@
     unified_timestamp,
     unsmuggle_url,
     url_or_none,
+    variadic,
     xpath_attr,
     xpath_text,
     xpath_with_ns,
@@ -359,188 +361,6 @@ class GenericIE(InfoExtractor):
             },
             'skip': 'There is a limit of 200 free downloads / month for the test song',
         },
-        {
-            # embedded brightcove video
-            # it also tests brightcove videos that need to set the 'Referer'
-            # in the http requests
-            'add_ie': ['BrightcoveLegacy'],
-            'url': 'http://www.bfmtv.com/video/bfmbusiness/cours-bourse/cours-bourse-l-analyse-technique-154522/',
-            'info_dict': {
-                'id': '2765128793001',
-                'ext': 'mp4',
-                'title': 'Le cours de bourse : l’analyse technique',
-                'description': 'md5:7e9ad046e968cb2d1114004aba466fd9',
-                'uploader': 'BFM BUSINESS',
-            },
-            'params': {
-                'skip_download': True,
-            },
-        },
-        {
-            # embedded with itemprop embedURL and video id spelled as `idVideo`
-            'add_id': ['BrightcoveLegacy'],
-            'url': 'http://bfmbusiness.bfmtv.com/mediaplayer/chroniques/olivier-delamarche/',
-            'info_dict': {
-                'id': '5255628253001',
-                'ext': 'mp4',
-                'title': 'md5:37c519b1128915607601e75a87995fc0',
-                'description': 'md5:37f7f888b434bb8f8cc8dbd4f7a4cf26',
-                'uploader': 'BFM BUSINESS',
-                'uploader_id': '876450612001',
-                'timestamp': 1482255315,
-                'upload_date': '20161220',
-            },
-            'params': {
-                'skip_download': True,
-            },
-        },
-        {
-            # https://github.com/ytdl-org/youtube-dl/issues/2253
-            'url': 'http://bcove.me/i6nfkrc3',
-            'md5': '0ba9446db037002366bab3b3eb30c88c',
-            'info_dict': {
-                'id': '3101154703001',
-                'ext': 'mp4',
-                'title': 'Still no power',
-                'uploader': 'thestar.com',
-                'description': 'Mississauga resident David Farmer is still out of power as a result of the ice storm a month ago. To keep the house warm, Farmer cuts wood from his property for a wood burning stove downstairs.',
-            },
-            'add_ie': ['BrightcoveLegacy'],
-            'skip': 'video gone',
-        },
-        {
-            'url': 'http://www.championat.com/video/football/v/87/87499.html',
-            'md5': 'fb973ecf6e4a78a67453647444222983',
-            'info_dict': {
-                'id': '3414141473001',
-                'ext': 'mp4',
-                'title': 'Видео. Удаление Дзагоева (ЦСКА)',
-                'description': 'Онлайн-трансляция матча ЦСКА - "Волга"',
-                'uploader': 'Championat',
-            },
-        },
-        {
-            # https://github.com/ytdl-org/youtube-dl/issues/3541
-            'add_ie': ['BrightcoveLegacy'],
-            'url': 'http://www.kijk.nl/sbs6/leermijvrouwenkennen/videos/jqMiXKAYan2S/aflevering-1',
-            'info_dict': {
-                'id': '3866516442001',
-                'ext': 'mp4',
-                'title': 'Leer mij vrouwen kennen: Aflevering 1',
-                'description': 'Leer mij vrouwen kennen: Aflevering 1',
-                'uploader': 'SBS Broadcasting',
-            },
-            'skip': 'Restricted to Netherlands',
-            'params': {
-                'skip_download': True,  # m3u8 download
-            },
-        },
-        {
-            # Brightcove video in <iframe>
-            'url': 'http://www.un.org/chinese/News/story.asp?NewsID=27724',
-            'md5': '36d74ef5e37c8b4a2ce92880d208b968',
-            'info_dict': {
-                'id': '5360463607001',
-                'ext': 'mp4',
-                'title': '叙利亚失明儿童在废墟上演唱《心跳》  呼吁获得正常童年生活',
-                'description': '联合国儿童基金会中东和北非区域大使、作曲家扎德·迪拉尼(Zade Dirani)在3月15日叙利亚冲突爆发7周年纪念日之际发布了为叙利亚谱写的歌曲《心跳》(HEARTBEAT),为受到六年冲突影响的叙利亚儿童发出强烈呐喊,呼吁世界做出共同努力,使叙利亚儿童重新获得享有正常童年生活的权利。',
-                'uploader': 'United Nations',
-                'uploader_id': '1362235914001',
-                'timestamp': 1489593889,
-                'upload_date': '20170315',
-            },
-            'add_ie': ['BrightcoveLegacy'],
-        },
-        {
-            # Brightcove with alternative playerID key
-            'url': 'http://www.nature.com/nmeth/journal/v9/n7/fig_tab/nmeth.2062_SV1.html',
-            'info_dict': {
-                'id': 'nmeth.2062_SV1',
-                'title': 'Simultaneous multiview imaging of the Drosophila syncytial blastoderm : Quantitative high-speed imaging of entire developing embryos with simultaneous multiview light-sheet microscopy : Nature Methods : Nature Research',
-            },
-            'playlist': [{
-                'info_dict': {
-                    'id': '2228375078001',
-                    'ext': 'mp4',
-                    'title': 'nmeth.2062-sv1',
-                    'description': 'nmeth.2062-sv1',
-                    'timestamp': 1363357591,
-                    'upload_date': '20130315',
-                    'uploader': 'Nature Publishing Group',
-                    'uploader_id': '1964492299001',
-                },
-            }],
-        },
-        {
-            # Brightcove with UUID in videoPlayer
-            'url': 'http://www8.hp.com/cn/zh/home.html',
-            'info_dict': {
-                'id': '5255815316001',
-                'ext': 'mp4',
-                'title': 'Sprocket Video - China',
-                'description': 'Sprocket Video - China',
-                'uploader': 'HP-Video Gallery',
-                'timestamp': 1482263210,
-                'upload_date': '20161220',
-                'uploader_id': '1107601872001',
-            },
-            'params': {
-                'skip_download': True,  # m3u8 download
-            },
-            'skip': 'video rotates...weekly?',
-        },
-        {
-            # Brightcove:new type [2].
-            'url': 'http://www.delawaresportszone.com/video-st-thomas-more-earns-first-trip-to-basketball-semis',
-            'md5': '2b35148fcf48da41c9fb4591650784f3',
-            'info_dict': {
-                'id': '5348741021001',
-                'ext': 'mp4',
-                'upload_date': '20170306',
-                'uploader_id': '4191638492001',
-                'timestamp': 1488769918,
-                'title': 'VIDEO:  St. Thomas More earns first trip to basketball semis',
-
-            },
-        },
-        {
-            # Alternative brightcove <video> attributes
-            'url': 'http://www.programme-tv.net/videos/extraits/81095-guillaume-canet-evoque-les-rumeurs-d-infidelite-de-marion-cotillard-avec-brad-pitt-dans-vivement-dimanche/',
-            'info_dict': {
-                'id': '81095-guillaume-canet-evoque-les-rumeurs-d-infidelite-de-marion-cotillard-avec-brad-pitt-dans-vivement-dimanche',
-                'title': "Guillaume Canet évoque les rumeurs d'infidélité de Marion Cotillard avec Brad Pitt dans Vivement Dimanche, Extraits : toutes les vidéos avec Télé-Loisirs",
-            },
-            'playlist': [{
-                'md5': '732d22ba3d33f2f3fc253c39f8f36523',
-                'info_dict': {
-                    'id': '5311302538001',
-                    'ext': 'mp4',
-                    'title': "Guillaume Canet évoque les rumeurs d'infidélité de Marion Cotillard avec Brad Pitt dans Vivement Dimanche",
-                    'description': "Guillaume Canet évoque les rumeurs d'infidélité de Marion Cotillard avec Brad Pitt dans Vivement Dimanche (France 2, 5 février 2017)",
-                    'timestamp': 1486321708,
-                    'upload_date': '20170205',
-                    'uploader_id': '800000640001',
-                },
-                'only_matching': True,
-            }],
-        },
-        {
-            # Brightcove with UUID in videoPlayer
-            'url': 'http://www8.hp.com/cn/zh/home.html',
-            'info_dict': {
-                'id': '5255815316001',
-                'ext': 'mp4',
-                'title': 'Sprocket Video - China',
-                'description': 'Sprocket Video - China',
-                'uploader': 'HP-Video Gallery',
-                'timestamp': 1482263210,
-                'upload_date': '20161220',
-                'uploader_id': '1107601872001',
-            },
-            'params': {
-                'skip_download': True,  # m3u8 download
-            },
-        },
         # ooyala video
         {
             'url': 'http://www.rollingstone.com/music/videos/norwegian-dj-cashmere-cat-goes-spartan-on-with-me-premiere-20131219',
@@ -844,20 +664,6 @@ class GenericIE(InfoExtractor):
                 'title': 'Busty Blonde Siri Tit Fuck While Wank at HandjobHub.com',
             }
         },
-        # Multiple brightcove videos
-        # https://github.com/ytdl-org/youtube-dl/issues/2283
-        {
-            'url': 'http://www.newyorker.com/online/blogs/newsdesk/2014/01/always-never-nuclear-command-and-control.html',
-            'info_dict': {
-                'id': 'always-never',
-                'title': 'Always / Never - The New Yorker',
-            },
-            'playlist_count': 3,
-            'params': {
-                'extract_flat': False,
-                'skip_download': True,
-            }
-        },
         # MLB embed
         {
             'url': 'http://umpire-empire.com/index.php/topic/58125-laz-decides-no-thats-low/',
@@ -873,22 +679,6 @@ class GenericIE(InfoExtractor):
                 'thumbnail': r're:^https?://.*\.jpg$',
             },
         },
-        # Wistia embed
-        {
-            'url': 'http://study.com/academy/lesson/north-american-exploration-failed-colonies-of-spain-france-england.html#lesson',
-            'md5': '1953f3a698ab51cfc948ed3992a0b7ff',
-            'info_dict': {
-                'id': '6e2wtrbdaf',
-                'ext': 'mov',
-                'title': 'paywall_north-american-exploration-failed-colonies-of-spain-france-england',
-                'description': 'a Paywall Videos video from Remilon',
-                'duration': 644.072,
-                'uploader': 'study.com',
-                'timestamp': 1459678540,
-                'upload_date': '20160403',
-                'filesize': 24687186,
-            },
-        },
         # Wistia standard embed (async)
         {
             'url': 'https://www.getdrip.com/university/brennan-dunn-drip-workshop/',
@@ -903,7 +693,8 @@ class GenericIE(InfoExtractor):
             },
             'params': {
                 'skip_download': True,
-            }
+            },
+            'skip': 'webpage 404 not found',
         },
         # Soundcloud embed
         {
@@ -1086,18 +877,6 @@ class GenericIE(InfoExtractor):
                 'skip_download': True,
             }
         },
-        {
-            # JWPlatform iframe
-            'url': 'https://www.covermagazine.co.uk/feature/2465255/business-protection-involved',
-            'info_dict': {
-                'id': 'AG26UQXM',
-                'ext': 'mp4',
-                'upload_date': '20160719',
-                'timestamp': 468923808,
-                'title': '2016_05_18 Cover L&G Business Protection V1 FINAL.mp4',
-            },
-            'add_ie': ['JWPlatform'],
-        },
         {
             # Video.js embed, multiple formats
             'url': 'http://ortcam.com/solidworks-урок-6-настройка-чертежа_33f9b7351.html',
@@ -1377,21 +1156,6 @@ class GenericIE(InfoExtractor):
             },
             'expected_warnings': ['Failed to parse JSON Expecting value'],
         },
-        # Brightcove URL in single quotes
-        {
-            'url': 'http://www.sportsnet.ca/baseball/mlb/sn-presents-russell-martin-world-citizen/',
-            'md5': '4ae374f1f8b91c889c4b9203c8c752af',
-            'info_dict': {
-                'id': '4255764656001',
-                'ext': 'mp4',
-                'title': 'SN Presents: Russell Martin, World Citizen',
-                'description': 'To understand why he was the Toronto Blue Jays’ top off-season priority is to appreciate his background and upbringing in Montreal, where he first developed his baseball skills. Written and narrated by Stephen Brunt.',
-                'uploader': 'Rogers Sportsnet',
-                'uploader_id': '1704050871',
-                'upload_date': '20150525',
-                'timestamp': 1432570283,
-            },
-        },
         # Kinja embed
         {
             'url': 'http://www.clickhole.com/video/dont-understand-bitcoin-man-will-mumble-explanatio-2537',
@@ -1427,52 +1191,6 @@ class GenericIE(InfoExtractor):
                 'duration': 248.667,
             },
         },
-        # BrightcoveInPageEmbed embed
-        {
-            'url': 'http://www.geekandsundry.com/tabletop-bonus-wils-final-thoughts-on-dread/',
-            'info_dict': {
-                'id': '4238694884001',
-                'ext': 'flv',
-                'title': 'Tabletop: Dread, Last Thoughts',
-                'description': 'Tabletop: Dread, Last Thoughts',
-                'duration': 51690,
-            },
-        },
-        # Brightcove embed, with no valid 'renditions' but valid 'IOSRenditions'
-        # This video can't be played in browsers if Flash disabled and UA set to iPhone, which is actually a false alarm
-        {
-            'url': 'https://dl.dropboxusercontent.com/u/29092637/interview.html',
-            'info_dict': {
-                'id': '4785848093001',
-                'ext': 'mp4',
-                'title': 'The Cardinal Pell Interview',
-                'description': 'Sky News Contributor Andrew Bolt interviews George Pell in Rome, following the Cardinal\'s evidence before the Royal Commission into Child Abuse. ',
-                'uploader': 'GlobeCast Australia - GlobeStream',
-                'uploader_id': '2733773828001',
-                'upload_date': '20160304',
-                'timestamp': 1457083087,
-            },
-            'params': {
-                # m3u8 downloads
-                'skip_download': True,
-            },
-        },
-        {
-            # Brightcove embed with whitespace around attribute names
-            'url': 'http://www.stack.com/video/3167554373001/learn-to-hit-open-three-pointers-with-damian-lillard-s-baseline-drift-drill',
-            'info_dict': {
-                'id': '3167554373001',
-                'ext': 'mp4',
-                'title': "Learn to Hit Open Three-Pointers With Damian Lillard's Baseline Drift Drill",
-                'description': 'md5:57bacb0e0f29349de4972bfda3191713',
-                'uploader_id': '1079349493',
-                'upload_date': '20140207',
-                'timestamp': 1391810548,
-            },
-            'params': {
-                'skip_download': True,
-            },
-        },
         # Another form of arte.tv embed
         {
             'url': 'http://www.tv-replay.fr/redirection/09-04-16/arte-reportage-arte-11508975.html',
@@ -1523,7 +1241,7 @@ class GenericIE(InfoExtractor):
                 'timestamp': 1464107587,
                 'uploader': 'TheAtlantic',
             },
-            'add_ie': ['BrightcoveLegacy'],
+            'skip': 'Private Youtube video',
         },
         # Facebook <iframe> embed
         {
@@ -2006,22 +1724,6 @@ class GenericIE(InfoExtractor):
             },
             'playlist_count': 6,
         },
-        {
-            # Squarespace video embed, 2019-08-28
-            'url': 'http://ootboxford.com',
-            'info_dict': {
-                'id': 'Tc7b_JGdZfw',
-                'title': 'Out of the Blue, at Childish Things 10',
-                'ext': 'mp4',
-                'description': 'md5:a83d0026666cf5ee970f8bd1cfd69c7f',
-                'uploader_id': 'helendouglashouse',
-                'uploader': 'Helen & Douglas House',
-                'upload_date': '20140328',
-            },
-            'params': {
-                'skip_download': True,
-            },
-        },
         # {
         #     # Zype embed
         #     'url': 'https://www.cookscountry.com/episode/554-smoky-barbecue-favorites',
@@ -2440,40 +2142,6 @@ class GenericIE(InfoExtractor):
                 'upload_date': '20210111',
             }
         },
-        {
-            'note': 'Rumble embed',
-            'url': 'https://rumble.com/vdmum1-moose-the-dog-helps-girls-dig-a-snow-fort.html',
-            'md5': '53af34098a7f92c4e51cf0bd1c33f009',
-            'info_dict': {
-                'id': 'vb0ofn',
-                'ext': 'mp4',
-                'timestamp': 1612662578,
-                'uploader': 'LovingMontana',
-                'channel': 'LovingMontana',
-                'upload_date': '20210207',
-                'title': 'Winter-loving dog helps girls dig a snow fort ',
-                'channel_url': 'https://rumble.com/c/c-546523',
-                'thumbnail': 'https://sp.rmbl.ws/s8/1/5/f/x/x/5fxxb.OvCc.1-small-Moose-The-Dog-Helps-Girls-D.jpg',
-                'duration': 103,
-            }
-        },
-        {
-            'note': 'Rumble JS embed',
-            'url': 'https://therightscoop.com/what-does-9-plus-1-plus-1-equal-listen-to-this-audio-of-attempted-kavanaugh-assassins-call-and-youll-get-it',
-            'md5': '4701209ac99095592e73dbba21889690',
-            'info_dict': {
-                'id': 'v15eqxl',
-                'ext': 'mp4',
-                'channel': 'Mr Producer Media',
-                'duration': 92,
-                'title': '911 Audio From The Man Who Wanted To Kill Supreme Court Justice Kavanaugh',
-                'channel_url': 'https://rumble.com/c/RichSementa',
-                'thumbnail': 'https://sp.rmbl.ws/s8/1/P/j/f/A/PjfAe.OvCc-small-911-Audio-From-The-Man-Who-.jpg',
-                'timestamp': 1654892716,
-                'uploader': 'Mr Producer Media',
-                'upload_date': '20220610',
-            }
-        },
         {
             'note': 'JSON LD with multiple @type',
             'url': 'https://www.nu.nl/280161/video/hoe-een-bladvlo-dit-verwoestende-japanse-onkruid-moet-vernietigen.html',
@@ -2490,6 +2158,21 @@ class GenericIE(InfoExtractor):
                 'duration': 111.0,
             }
         },
+        {
+            'note': 'JSON LD with unexpected data type',
+            'url': 'https://www.autoweek.nl/autotests/artikel/porsche-911-gt3-rs-rij-impressie-2/',
+            'info_dict': {
+                'id': 'porsche-911-gt3-rs-rij-impressie-2',
+                'ext': 'mp4',
+                'title': 'Test: Porsche 911 GT3 RS',
+                'description': 'Je ziet het niet, maar het is er wel. Downforce, hebben we het dan over. En in de nieuwe Porsche 911 GT3 RS is er zelfs heel veel downforce.',
+                'timestamp': 1664920902,
+                'upload_date': '20221004',
+                'thumbnail': r're:^https://media.autoweek.nl/m/.+\.jpg$',
+                'age_limit': 0,
+                'direct': True,
+            }
+        }
     ]
 
     def report_following_redirect(self, new_url):
@@ -2506,6 +2189,13 @@ def report_detected(self, name, num=1, note=None):
 
         self._downloader.write_debug(f'Identified {num} {name}{format_field(note, None, "; %s")}')
 
+    def _fragment_query(self, url):
+        if self._configuration_arg('fragment_query'):
+            query_string = urllib.parse.urlparse(url).query
+            if query_string:
+                return {'extra_param_to_segment_url': query_string}
+        return {}
+
     def _extract_rss(self, url, video_id, doc):
         NS_MAP = {
             'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd',
@@ -2621,6 +2311,7 @@ def _real_extract(self, url):
                     default_search += ':'
                 return self.url_result(default_search + url)
 
+        original_url = url
         url, smuggled_data = unsmuggle_url(url, {})
         force_videoid = None
         is_intentional = smuggled_data.get('to_generic')
@@ -2643,7 +2334,9 @@ def _real_extract(self, url):
             **smuggled_data.get('http_headers', {})
         })
         new_url = full_response.geturl()
-        if url != new_url:
+        if new_url == urllib.parse.urlparse(url)._replace(scheme='https').geturl():
+            url = new_url
+        elif url != new_url:
             self.report_following_redirect(new_url)
             if force_videoid:
                 new_url = smuggle_url(new_url, {'force_videoid': force_videoid})
@@ -2665,8 +2358,10 @@ def _real_extract(self, url):
             subtitles = {}
             if format_id.endswith('mpegurl'):
                 formats, subtitles = self._extract_m3u8_formats_and_subtitles(url, video_id, 'mp4', headers=headers)
+                info_dict.update(self._fragment_query(url))
             elif format_id.endswith('mpd') or format_id.endswith('dash+xml'):
                 formats, subtitles = self._extract_mpd_formats_and_subtitles(url, video_id, headers=headers)
+                info_dict.update(self._fragment_query(url))
             elif format_id == 'f4m':
                 formats = self._extract_f4m_formats(url, video_id, headers=headers)
             else:
@@ -2676,7 +2371,6 @@ def _real_extract(self, url):
                     'vcodec': 'none' if m.group('type') == 'audio' else None
                 }]
                 info_dict['direct'] = True
-            self._sort_formats(formats)
             info_dict.update({
                 'formats': formats,
                 'subtitles': subtitles,
@@ -2694,7 +2388,7 @@ def _real_extract(self, url):
         if first_bytes.startswith(b'#EXTM3U'):
             self.report_detected('M3U playlist')
             info_dict['formats'], info_dict['subtitles'] = self._extract_m3u8_formats_and_subtitles(url, video_id, 'mp4')
-            self._sort_formats(info_dict['formats'])
+            info_dict.update(self._fragment_query(url))
             return info_dict
 
         # Maybe it's a direct link to a video?
@@ -2728,12 +2422,10 @@ def _real_extract(self, url):
             elif doc.tag == 'SmoothStreamingMedia':
                 info_dict['formats'], info_dict['subtitles'] = self._parse_ism_formats_and_subtitles(doc, url)
                 self.report_detected('ISM manifest')
-                self._sort_formats(info_dict['formats'])
                 return info_dict
             elif re.match(r'^(?:{[^}]+})?smil$', doc.tag):
                 smil = self._parse_smil(doc, url, video_id)
                 self.report_detected('SMIL file')
-                self._sort_formats(smil['formats'])
                 return smil
             elif doc.tag == '{http://xspf.org/ns/0/}playlist':
                 self.report_detected('XSPF playlist')
@@ -2747,13 +2439,12 @@ def _real_extract(self, url):
                     doc,
                     mpd_base_url=full_response.geturl().rpartition('/')[0],
                     mpd_url=url)
+                info_dict.update(self._fragment_query(url))
                 self.report_detected('DASH manifest')
-                self._sort_formats(info_dict['formats'])
                 return info_dict
             elif re.match(r'^{http://ns\.adobe\.com/f4m/[12]\.0}manifest$', doc.tag):
                 info_dict['formats'] = self._parse_f4m_formats(doc, url, video_id)
                 self.report_detected('F4M manifest')
-                self._sort_formats(info_dict['formats'])
                 return info_dict
         except xml.etree.ElementTree.ParseError:
             pass
@@ -2765,14 +2456,26 @@ def _real_extract(self, url):
             #   Site Name | Video Title
             #   Video Title - Tagline | Site Name
             # and so on and so forth; it's just not practical
-            'title': (self._og_search_title(webpage, default=None)
-                      or self._html_extract_title(webpage, 'video title', default='video')),
+            'title': self._generic_title('', webpage, default='video'),
             'description': self._og_search_description(webpage, default=None),
             'thumbnail': self._og_search_thumbnail(webpage, default=None),
             'age_limit': self._rta_search(webpage),
         })
 
-        domain_name = self._search_regex(r'^(?:https?://)?([^/]*)/.*', url, 'video uploader', default=None)
+        self._downloader.write_debug('Looking for embeds')
+        embeds = list(self._extract_embeds(original_url, webpage, urlh=full_response, info_dict=info_dict))
+        if len(embeds) == 1:
+            return {**info_dict, **embeds[0]}
+        elif embeds:
+            return self.playlist_result(embeds, **info_dict)
+        raise UnsupportedError(url)
+
+    def _extract_embeds(self, url, webpage, *, urlh=None, info_dict={}):
+        """Returns an iterator of video entries"""
+        info_dict = types.MappingProxyType(info_dict)  # Prevents accidental mutation
+        video_id = traverse_obj(info_dict, 'display_id', 'id') or self._generic_id(url)
+        url, smuggled_data = unsmuggle_url(url, {})
+        actual_url = urlh.geturl() if urlh else url
 
         # Sometimes embedded video player is hidden behind percent encoding
         # (e.g. https://github.com/ytdl-org/youtube-dl/issues/2448)
@@ -2781,38 +2484,10 @@ def _real_extract(self, url):
         # There probably should be a second run of generic extractor on unescaped webpage.
         # webpage = urllib.parse.unquote(webpage)
 
-        # Unescape squarespace embeds to be detected by generic extractor,
-        # see https://github.com/ytdl-org/youtube-dl/issues/21294
-        webpage = re.sub(
-            r'<div[^>]+class=[^>]*?\bsqs-video-wrapper\b[^>]*>',
-            lambda x: unescapeHTML(x.group(0)), webpage)
-
-        # TODO: Move to respective extractors
-        self._downloader.write_debug('Looking for Brightcove embeds')
-        bc_urls = BrightcoveLegacyIE._extract_brightcove_urls(webpage)
-        if bc_urls:
-            entries = [{
-                '_type': 'url',
-                'url': smuggle_url(bc_url, {'Referer': url}),
-                'ie_key': 'BrightcoveLegacy'
-            } for bc_url in bc_urls]
-
-            return {
-                '_type': 'playlist',
-                'title': info_dict['title'],
-                'id': video_id,
-                'entries': entries,
-            }
-        bc_urls = BrightcoveNewIE._extract_brightcove_urls(self, webpage)
-        if bc_urls:
-            return self.playlist_from_matches(
-                bc_urls, video_id, info_dict['title'],
-                getter=lambda x: smuggle_url(x, {'referrer': url}),
-                ie='BrightcoveNew')
-
-        self._downloader.write_debug('Looking for embeds')
         embeds = []
         for ie in self._downloader._ies.values():
+            if ie.ie_key() in smuggled_data.get('block_ies', []):
+                continue
             gen = ie.extract_from_webpage(self._downloader, url, webpage)
             current_embeds = []
             try:
@@ -2821,35 +2496,26 @@ def _real_extract(self, url):
             except self.StopExtraction:
                 self.report_detected(f'{ie.IE_NAME} exclusive embed', len(current_embeds),
                                      embeds and 'discarding other embeds')
-                embeds = current_embeds
-                break
+                return current_embeds
             except StopIteration:
                 self.report_detected(f'{ie.IE_NAME} embed', len(current_embeds))
                 embeds.extend(current_embeds)
 
-        del current_embeds
-        if len(embeds) == 1:
-            return {**info_dict, **embeds[0]}
-        elif embeds:
-            return self.playlist_result(embeds, **info_dict)
+        if embeds:
+            return embeds
 
         jwplayer_data = self._find_jwplayer_data(
             webpage, video_id, transform_source=js_to_json)
         if jwplayer_data:
             if isinstance(jwplayer_data.get('playlist'), str):
                 self.report_detected('JW Player playlist')
-                return {
-                    **info_dict,
-                    '_type': 'url',
-                    'ie_key': 'JWPlatform',
-                    'url': jwplayer_data['playlist'],
-                }
+                return [self.url_result(jwplayer_data['playlist'], 'JWPlatform')]
             try:
                 info = self._parse_jwplayer_data(
                     jwplayer_data, video_id, require_title=False, base_url=url)
                 if traverse_obj(info, 'formats', ('entries', ..., 'formats')):
                     self.report_detected('JW Player data')
-                    return merge_dicts(info, info_dict)
+                    return [info]
             except ExtractorError:
                 # See https://github.com/ytdl-org/youtube-dl/pull/16735
                 pass
@@ -2860,11 +2526,8 @@ def _real_extract(self, url):
             webpage)
         if mobj is not None:
             varname = mobj.group(1)
-            sources = self._parse_json(
-                mobj.group(2), video_id, transform_source=js_to_json,
-                fatal=False) or []
-            if not isinstance(sources, list):
-                sources = [sources]
+            sources = variadic(self._parse_json(
+                mobj.group(2), video_id, transform_source=js_to_json, fatal=False) or [])
             formats = []
             subtitles = {}
             for source in sources:
@@ -2877,7 +2540,7 @@ def _real_extract(self, url):
                     src_type = src_type.lower()
                 ext = determine_ext(src).lower()
                 if src_type == 'video/youtube':
-                    return self.url_result(src, YoutubeIE.ie_key())
+                    return [self.url_result(src, YoutubeIE.ie_key())]
                 if src_type == 'application/dash+xml' or ext == 'mpd':
                     fmts, subs = self._extract_mpd_formats_and_subtitles(
                         src, video_id, mpd_id='dash', fatal=False)
@@ -2889,13 +2552,16 @@ def _real_extract(self, url):
                         m3u8_id='hls', fatal=False)
                     formats.extend(fmts)
                     self._merge_subtitles(subs, target=subtitles)
-                else:
+                for fmt in formats:
+                    fmt.update(self._fragment_query(src))
+
+                if not formats:
                     formats.append({
                         'url': src,
                         'ext': (mimetype2ext(src_type)
                                 or ext if ext in KNOWN_EXTENSIONS else 'mp4'),
                         'http_headers': {
-                            'Referer': full_response.geturl(),
+                            'Referer': actual_url,
                         },
                     })
             # https://docs.videojs.com/player#addRemoteTextTrack
@@ -2910,28 +2576,26 @@ def _real_extract(self, url):
                     'url': urllib.parse.urljoin(url, src),
                     'name': sub.get('label'),
                     'http_headers': {
-                        'Referer': full_response.geturl(),
+                        'Referer': actual_url,
                     },
                 })
             if formats or subtitles:
                 self.report_detected('video.js embed')
-                self._sort_formats(formats)
-                info_dict['formats'] = formats
-                info_dict['subtitles'] = subtitles
-                return info_dict
+                return [{'formats': formats, 'subtitles': subtitles}]
 
         # Looking for http://schema.org/VideoObject
         json_ld = self._search_json_ld(webpage, video_id, default={})
         if json_ld.get('url') not in (url, None):
             self.report_detected('JSON LD')
-            return merge_dicts({
-                '_type': 'url_transparent',
+            is_direct = json_ld.get('ext') not in (None, *MEDIA_EXTENSIONS.manifests)
+            return [merge_dicts({
+                '_type': 'video' if is_direct else 'url_transparent',
                 'url': smuggle_url(json_ld['url'], {
                     'force_videoid': video_id,
                     'to_generic': True,
                     'http_headers': {'Referer': url},
                 }),
-            }, json_ld, info_dict)
+            }, json_ld)]
 
         def check_video(vurl):
             if YoutubeIE.suitable(vurl):
@@ -3000,15 +2664,13 @@ def filter_video(urls):
                     if not formats[-1].get('height'):
                         formats[-1]['quality'] = 1
 
-                self._sort_formats(formats)
-
-                return {
+                return [{
                     'id': flashvars['video_id'],
                     'display_id': display_id,
                     'title': title,
                     'thumbnail': thumbnail,
                     'formats': formats,
-                }
+                }]
         if not found:
             # Broaden the search a little bit
             found = filter_video(re.findall(r'[^A-Za-z0-9]?(?:file|source)=(http[^\'"&]*)', webpage))
@@ -3062,17 +2724,14 @@ def filter_video(urls):
                 webpage)
             if not found:
                 # Look also in Refresh HTTP header
-                refresh_header = full_response.headers.get('Refresh')
+                refresh_header = urlh and urlh.headers.get('Refresh')
                 if refresh_header:
                     found = re.search(REDIRECT_REGEX, refresh_header)
             if found:
                 new_url = urllib.parse.urljoin(url, unescapeHTML(found.group(1)))
                 if new_url != url:
                     self.report_following_redirect(new_url)
-                    return {
-                        '_type': 'url',
-                        'url': new_url,
-                    }
+                    return [self.url_result(new_url)]
                 else:
                     found = None
 
@@ -3083,10 +2742,12 @@ def filter_video(urls):
             embed_url = self._html_search_meta('twitter:player', webpage, default=None)
             if embed_url and embed_url != url:
                 self.report_detected('twitter:player iframe')
-                return self.url_result(embed_url)
+                return [self.url_result(embed_url)]
 
         if not found:
-            raise UnsupportedError(url)
+            return []
+
+        domain_name = self._search_regex(r'^(?:https?://)?([^/]*)/.*', url, 'video uploader', default=None)
 
         entries = []
         for video_url in orderedSet(found):
@@ -3102,7 +2763,7 @@ def filter_video(urls):
 
             video_id = os.path.splitext(video_id)[0]
             headers = {
-                'referer': full_response.geturl()
+                'referer': actual_url
             }
 
             entry_info_dict = {
@@ -3126,11 +2787,13 @@ def filter_video(urls):
             if ext == 'smil':
                 entry_info_dict = {**self._extract_smil_info(video_url, video_id), **entry_info_dict}
             elif ext == 'xspf':
-                return self.playlist_result(self._extract_xspf_playlist(video_url, video_id), video_id)
+                return [self._extract_xspf_playlist(video_url, video_id)]
             elif ext == 'm3u8':
                 entry_info_dict['formats'], entry_info_dict['subtitles'] = self._extract_m3u8_formats_and_subtitles(video_url, video_id, ext='mp4', headers=headers)
+                entry_info_dict.update(self._fragment_query(video_url))
             elif ext == 'mpd':
                 entry_info_dict['formats'], entry_info_dict['subtitles'] = self._extract_mpd_formats_and_subtitles(video_url, video_id, headers=headers)
+                entry_info_dict.update(self._fragment_query(video_url))
             elif ext == 'f4m':
                 entry_info_dict['formats'] = self._extract_f4m_formats(video_url, video_id, headers=headers)
             elif re.search(r'(?i)\.(?:ism|smil)/manifest', video_url) and video_url != url:
@@ -3151,19 +2814,11 @@ def filter_video(urls):
             else:
                 entry_info_dict['url'] = video_url
 
-            if entry_info_dict.get('formats'):
-                self._sort_formats(entry_info_dict['formats'])
-
             entries.append(entry_info_dict)
 
-        if len(entries) == 1:
-            return merge_dicts(entries[0], info_dict)
-        else:
+        if len(entries) > 1:
             for num, e in enumerate(entries, start=1):
                 # 'url' results don't have a title
                 if e.get('title') is not None:
                     e['title'] = '%s (%d)' % (e['title'], num)
-            return {
-                '_type': 'playlist',
-                'entries': entries,
-            }
+        return entries