From b7da73eb19e00e4eab43ec7de129e9aa12f6d5d3 Mon Sep 17 00:00:00 2001
From: pukkandan <pukkandan.ytdlp@gmail.com>
Date: Sat, 17 Apr 2021 05:39:58 +0530
Subject: [PATCH] Add option `--ignore-no-formats-error` * Ignores the "no
 video format" and similar errors * Experimental - Some extractors may still
 throw these errors

---
 yt_dlp/YoutubeDL.py                     | 36 +++++++++++++++++--------
 yt_dlp/__init__.py                      |  1 +
 yt_dlp/extractor/afreecatv.py           |  2 +-
 yt_dlp/extractor/ard.py                 |  4 +--
 yt_dlp/extractor/bbc.py                 |  2 +-
 yt_dlp/extractor/brightcove.py          |  4 +--
 yt_dlp/extractor/channel9.py            |  6 ++---
 yt_dlp/extractor/common.py              | 26 +++++++++++++-----
 yt_dlp/extractor/corus.py               |  2 +-
 yt_dlp/extractor/disney.py              |  3 +--
 yt_dlp/extractor/facebook.py            |  2 --
 yt_dlp/extractor/googledrive.py         |  2 +-
 yt_dlp/extractor/hotstar.py             |  2 +-
 yt_dlp/extractor/iprima.py              |  2 +-
 yt_dlp/extractor/keezmovies.py          |  2 +-
 yt_dlp/extractor/line.py                |  5 ++--
 yt_dlp/extractor/medaltv.py             |  4 +--
 yt_dlp/extractor/mixcloud.py            |  2 +-
 yt_dlp/extractor/npo.py                 |  2 +-
 yt_dlp/extractor/odnoklassniki.py       |  2 +-
 yt_dlp/extractor/ooyala.py              |  3 +--
 yt_dlp/extractor/philharmoniedeparis.py |  2 +-
 yt_dlp/extractor/rai.py                 |  2 +-
 yt_dlp/extractor/ruutu.py               |  2 +-
 yt_dlp/extractor/soundcloud.py          |  2 +-
 yt_dlp/extractor/sportdeutschland.py    |  2 +-
 yt_dlp/extractor/steam.py               |  2 +-
 yt_dlp/extractor/svt.py                 |  2 +-
 yt_dlp/extractor/toggle.py              |  6 ++---
 yt_dlp/extractor/tv2.py                 |  2 +-
 yt_dlp/extractor/tv4.py                 |  2 +-
 yt_dlp/extractor/tvplay.py              |  3 ++-
 yt_dlp/extractor/videomore.py           |  5 ++--
 yt_dlp/extractor/vube.py                |  7 +++--
 yt_dlp/extractor/wat.py                 |  2 +-
 yt_dlp/extractor/yahoo.py               |  2 +-
 yt_dlp/extractor/youtube.py             |  7 +++--
 yt_dlp/extractor/zingmp3.py             |  5 ++--
 yt_dlp/options.py                       | 10 +++++++
 39 files changed, 103 insertions(+), 76 deletions(-)

diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py
index d7cbdd047..7cda3fc8f 100644
--- a/yt_dlp/YoutubeDL.py
+++ b/yt_dlp/YoutubeDL.py
@@ -191,6 +191,9 @@ class YoutubeDL(object):
     simulate:          Do not download the video files.
     format:            Video format code. see "FORMAT SELECTION" for more details.
     allow_unplayable_formats:   Allow unplayable formats to be extracted and downloaded.
+    ignore_no_formats_error: Ignore "No video formats" error. Usefull for
+                       extracting metadata even if the video is not actually
+                       available for download (experimental)
     format_sort:       How to sort the video formats. see "Sorting Formats"
                        for more details.
     format_sort_force: Force the given format_sort. see "Sorting Formats"
@@ -1884,7 +1887,10 @@ def sanitize_numeric_fields(info):
             formats = info_dict['formats']
 
         if not formats:
-            raise ExtractorError('No video formats found!')
+            if not self.params.get('ignore_no_formats_error'):
+                raise ExtractorError('No video formats found!')
+            else:
+                self.report_warning('No video formats found!')
 
         def is_wellformed(f):
             url = f.get('url')
@@ -1948,13 +1954,15 @@ def is_wellformed(f):
 
         # TODO Central sorting goes here
 
-        if formats[0] is not info_dict:
+        if formats and formats[0] is not info_dict:
             # only set the 'formats' fields if the original info_dict list them
             # otherwise we end up with a circular reference, the first (and unique)
             # element in the 'formats' field in info_dict is info_dict itself,
             # which can't be exported to json
             info_dict['formats'] = formats
         if self.params.get('listformats'):
+            if not info_dict.get('formats'):
+                raise ExtractorError('No video formats found', expected=True)
             self.list_formats(info_dict)
             return
 
@@ -1994,19 +2002,25 @@ def is_wellformed(f):
 
         formats_to_download = list(format_selector(ctx))
         if not formats_to_download:
-            raise ExtractorError('requested format not available',
-                                 expected=True)
-
-        if download:
-            self.to_screen('[info] Downloading format(s) %s' % ", ".join([f['format_id'] for f in formats_to_download]))
+            if not self.params.get('ignore_no_formats_error'):
+                raise ExtractorError('Requested format is not available', expected=True)
+            else:
+                self.report_warning('Requested format is not available')
+        elif download:
+            self.to_screen(
+                '[info] %s: Downloading format(s) %s'
+                % (info_dict['id'], ", ".join([f['format_id'] for f in formats_to_download])))
             if len(formats_to_download) > 1:
-                self.to_screen('[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download)))
-            for format in formats_to_download:
+                self.to_screen(
+                    '[info] %s: Downloading video in %s formats'
+                    % (info_dict['id'], len(formats_to_download)))
+            for fmt in formats_to_download:
                 new_info = dict(info_dict)
-                new_info.update(format)
+                new_info.update(fmt)
                 self.process_info(new_info)
         # We update the info dict with the best quality format (backwards compatibility)
-        info_dict.update(formats_to_download[-1])
+        if formats_to_download:
+            info_dict.update(formats_to_download[-1])
         return info_dict
 
     def process_subtitles(self, video_id, normal_subtitles, automatic_captions):
diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py
index 90a3116ea..4f0684236 100644
--- a/yt_dlp/__init__.py
+++ b/yt_dlp/__init__.py
@@ -466,6 +466,7 @@ def report_args_compat(arg, name):
         'skip_download': opts.skip_download,
         'format': opts.format,
         'allow_unplayable_formats': opts.allow_unplayable_formats,
+        'ignore_no_formats_error': opts.ignore_no_formats_error,
         'format_sort': opts.format_sort,
         'format_sort_force': opts.format_sort_force,
         'allow_multiple_video_streams': opts.allow_multiple_video_streams,
diff --git a/yt_dlp/extractor/afreecatv.py b/yt_dlp/extractor/afreecatv.py
index af0587ae6..016a4d24a 100644
--- a/yt_dlp/extractor/afreecatv.py
+++ b/yt_dlp/extractor/afreecatv.py
@@ -323,7 +323,7 @@ def _real_extract(self, url):
                         'url': file_url,
                         'format_id': 'http',
                     }]
-                if not formats:
+                if not formats and not self._downloader.params.get('ignore_no_formats'):
                     continue
                 self._sort_formats(formats)
                 file_info = common_entry.copy()
diff --git a/yt_dlp/extractor/ard.py b/yt_dlp/extractor/ard.py
index 294da7c51..4d90be714 100644
--- a/yt_dlp/extractor/ard.py
+++ b/yt_dlp/extractor/ard.py
@@ -36,12 +36,12 @@ def _parse_media_info(self, media_info, video_id, fsk):
 
         if not formats:
             if fsk:
-                raise ExtractorError(
+                self.raise_no_formats(
                     'This video is only available after 20:00', expected=True)
             elif media_info.get('_geoblocked'):
                 self.raise_geo_restricted(
                     'This video is not available due to geoblocking',
-                    countries=self._GEO_COUNTRIES)
+                    countries=self._GEO_COUNTRIES, metadata_available=True)
 
         self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/bbc.py b/yt_dlp/extractor/bbc.py
index e8d000bbb..333796c80 100644
--- a/yt_dlp/extractor/bbc.py
+++ b/yt_dlp/extractor/bbc.py
@@ -1242,7 +1242,7 @@ def extract_all(pattern):
         entries = []
         for num, media_meta in enumerate(medias, start=1):
             formats, subtitles = self._extract_from_media_meta(media_meta, playlist_id)
-            if not formats:
+            if not formats and not self._downloader.params.get('ignore_no_formats'):
                 continue
             self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/brightcove.py b/yt_dlp/extractor/brightcove.py
index 8b29ca993..d2fd10064 100644
--- a/yt_dlp/extractor/brightcove.py
+++ b/yt_dlp/extractor/brightcove.py
@@ -545,9 +545,9 @@ def build_format_id(kind):
             errors = json_data.get('errors')
             if errors:
                 error = errors[0]
-                raise ExtractorError(
+                self.raise_no_formats(
                     error.get('message') or error.get('error_subcode') or error['error_code'], expected=True)
-            if (not self._downloader.params.get('allow_unplayable_formats')
+            elif (not self._downloader.params.get('allow_unplayable_formats')
                     and sources and num_drm_sources == len(sources)):
                 raise ExtractorError('This video is DRM protected.', expected=True)
 
diff --git a/yt_dlp/extractor/channel9.py b/yt_dlp/extractor/channel9.py
index 09cacf6d3..258e96ca6 100644
--- a/yt_dlp/extractor/channel9.py
+++ b/yt_dlp/extractor/channel9.py
@@ -5,7 +5,6 @@
 from .common import InfoExtractor
 from ..utils import (
     clean_html,
-    ExtractorError,
     int_or_none,
     parse_iso8601,
     qualities,
@@ -187,14 +186,13 @@ def quality(quality_id, format_url):
                     'quality': quality(q, q_url),
                 })
 
-            self._sort_formats(formats)
-
             slides = content_data.get('Slides')
             zip_file = content_data.get('ZipFile')
 
             if not formats and not slides and not zip_file:
-                raise ExtractorError(
+                self.raise_no_formats(
                     'None of recording, slides or zip are available for %s' % content_path)
+            self._sort_formats(formats)
 
             subtitles = {}
             for caption in content_data.get('Captions', []):
diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py
index 40ea9339f..9ead6db2d 100644
--- a/yt_dlp/extractor/common.py
+++ b/yt_dlp/extractor/common.py
@@ -968,15 +968,27 @@ def report_login(self):
         """Report attempt to log in."""
         self.to_screen('Logging in')
 
-    @staticmethod
-    def raise_login_required(msg='This video is only available for registered users'):
+    def raise_login_required(
+            self, msg='This video is only available for registered users', metadata_available=False):
+        if metadata_available and self._downloader.params.get('ignore_no_formats_error'):
+            self.report_warning(msg)
         raise ExtractorError(
-            '%s. Use --username and --password or --netrc to provide account credentials.' % msg,
+            '%s. Use --cookies, --username and --password or --netrc to provide account credentials' % msg,
             expected=True)
 
-    @staticmethod
-    def raise_geo_restricted(msg='This video is not available from your location due to geo restriction', countries=None):
-        raise GeoRestrictedError(msg, countries=countries)
+    def raise_geo_restricted(
+            self, msg='This video is not available from your location due to geo restriction',
+            countries=None, metadata_available=False):
+        if metadata_available and self._downloader.params.get('ignore_no_formats_error'):
+            self.report_warning(msg)
+        else:
+            raise GeoRestrictedError(msg, countries=countries)
+
+    def raise_no_formats(self, msg, expected=False, video_id=None):
+        if expected and self._downloader.params.get('ignore_no_formats_error'):
+            self.report_warning(msg, video_id)
+        else:
+            raise ExtractorError(msg, expected=expected, video_id=video_id)
 
     # Methods for following #608
     @staticmethod
@@ -1670,6 +1682,8 @@ def calculate_preference(self, format):
 
     def _sort_formats(self, formats, field_preference=[]):
         if not formats:
+            if self._downloader.params.get('ignore_no_formats_error'):
+                return
             raise ExtractorError('No video formats found')
         format_sort = self.FormatSort()  # params and to_screen are taken from the downloader
         format_sort.evaluate_params(self._downloader.params, field_preference)
diff --git a/yt_dlp/extractor/corus.py b/yt_dlp/extractor/corus.py
index e11aadf14..de61f42e4 100644
--- a/yt_dlp/extractor/corus.py
+++ b/yt_dlp/extractor/corus.py
@@ -131,7 +131,7 @@ def _real_extract(self, url):
             formats.extend(self._parse_smil_formats(
                 smil, smil_url, video_id, namespace))
         if not formats and video.get('drm'):
-            raise ExtractorError('This video is DRM protected.', expected=True)
+            self.raise_no_formats('This video is DRM protected.', expected=True)
         self._sort_formats(formats)
 
         subtitles = {}
diff --git a/yt_dlp/extractor/disney.py b/yt_dlp/extractor/disney.py
index 0eee82fd6..e1ae62ac6 100644
--- a/yt_dlp/extractor/disney.py
+++ b/yt_dlp/extractor/disney.py
@@ -9,7 +9,6 @@
     unified_strdate,
     compat_str,
     determine_ext,
-    ExtractorError,
     update_url_query,
 )
 
@@ -140,7 +139,7 @@ def _real_extract(self, url):
                 'vcodec': 'none' if (width == 0 and height == 0) else None,
             })
         if not formats and video_data.get('expired'):
-            raise ExtractorError(
+            self.raise_no_formats(
                 '%s said: %s' % (self.IE_NAME, page_data['translations']['video_expired']),
                 expected=True)
         self._sort_formats(formats)
diff --git a/yt_dlp/extractor/facebook.py b/yt_dlp/extractor/facebook.py
index 7a76dbb22..b68b90bd3 100644
--- a/yt_dlp/extractor/facebook.py
+++ b/yt_dlp/extractor/facebook.py
@@ -625,8 +625,6 @@ def parse_attachment(attachment, key='media'):
             subtitles_src = f[0].get('subtitles_src')
             if subtitles_src:
                 subtitles.setdefault('en', []).append({'url': subtitles_src})
-        if not formats:
-            raise ExtractorError('Cannot find video formats')
 
         process_formats(formats)
 
diff --git a/yt_dlp/extractor/googledrive.py b/yt_dlp/extractor/googledrive.py
index 4eefcb70c..7b5bf280f 100644
--- a/yt_dlp/extractor/googledrive.py
+++ b/yt_dlp/extractor/googledrive.py
@@ -253,7 +253,7 @@ def add_source_format(urlh):
                             or 'unable to extract confirmation code')
 
         if not formats and reason:
-            raise ExtractorError(reason, expected=True)
+            self.raise_no_formats(reason, expected=True)
 
         self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/hotstar.py b/yt_dlp/extractor/hotstar.py
index e2e923539..22cccf2b2 100644
--- a/yt_dlp/extractor/hotstar.py
+++ b/yt_dlp/extractor/hotstar.py
@@ -184,7 +184,7 @@ def _real_extract(self, url):
                     geo_restricted = True
                 continue
         if not formats and geo_restricted:
-            self.raise_geo_restricted(countries=['IN'])
+            self.raise_geo_restricted(countries=['IN'], metadata_available=True)
         self._sort_formats(formats)
 
         for f in formats:
diff --git a/yt_dlp/extractor/iprima.py b/yt_dlp/extractor/iprima.py
index 648ae6741..28e660972 100644
--- a/yt_dlp/extractor/iprima.py
+++ b/yt_dlp/extractor/iprima.py
@@ -136,7 +136,7 @@ def extract_formats(format_url, format_key=None, lang=None):
                 extract_formats(src)
 
         if not formats and '>GEO_IP_NOT_ALLOWED<' in playerpage:
-            self.raise_geo_restricted(countries=['CZ'])
+            self.raise_geo_restricted(countries=['CZ'], metadata_available=True)
 
         self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/keezmovies.py b/yt_dlp/extractor/keezmovies.py
index c3eb74c17..cfdd0eb8e 100644
--- a/yt_dlp/extractor/keezmovies.py
+++ b/yt_dlp/extractor/keezmovies.py
@@ -101,7 +101,7 @@ def extract_format(format_url, height=None):
 
         if not formats:
             if 'title="This video is no longer available"' in webpage:
-                raise ExtractorError(
+                self.raise_no_formats(
                     'Video %s is no longer available' % video_id, expected=True)
 
         try:
diff --git a/yt_dlp/extractor/line.py b/yt_dlp/extractor/line.py
index 2526daa77..41ac8d422 100644
--- a/yt_dlp/extractor/line.py
+++ b/yt_dlp/extractor/line.py
@@ -6,7 +6,6 @@
 from .common import InfoExtractor
 from ..compat import compat_str
 from ..utils import (
-    ExtractorError,
     int_or_none,
     js_to_json,
     str_or_none,
@@ -77,7 +76,7 @@ def _real_extract(self, url):
 
         self._sort_formats(formats)
 
-        if not formats[0].get('width'):
+        if formats and not formats[0].get('width'):
             formats[0]['vcodec'] = 'none'
 
         title = self._og_search_title(webpage)
@@ -183,7 +182,7 @@ def _real_extract(self, url):
         if not formats:
             archive_status = item.get('archiveStatus')
             if archive_status != 'ARCHIVED':
-                raise ExtractorError('this video has been ' + archive_status.lower(), expected=True)
+                self.raise_no_formats('this video has been ' + archive_status.lower(), expected=True)
         self._sort_formats(formats)
         info['formats'] = formats
         return info
diff --git a/yt_dlp/extractor/medaltv.py b/yt_dlp/extractor/medaltv.py
index 1603b55f6..4bca6f053 100644
--- a/yt_dlp/extractor/medaltv.py
+++ b/yt_dlp/extractor/medaltv.py
@@ -97,11 +97,11 @@ def add_item(container, item_url, height, id_key='format_id', item_id=None):
         error = clip.get('error')
         if not formats and error:
             if error == 404:
-                raise ExtractorError(
+                self.raise_no_formats(
                     'That clip does not exist.',
                     expected=True, video_id=video_id)
             else:
-                raise ExtractorError(
+                self.raise_no_formats(
                     'An unknown error occurred ({0}).'.format(error),
                     video_id=video_id)
 
diff --git a/yt_dlp/extractor/mixcloud.py b/yt_dlp/extractor/mixcloud.py
index 69319857d..b8ccd0ab4 100644
--- a/yt_dlp/extractor/mixcloud.py
+++ b/yt_dlp/extractor/mixcloud.py
@@ -157,7 +157,7 @@ def _real_extract(self, url):
                 })
 
         if not formats and cloudcast.get('isExclusive'):
-            self.raise_login_required()
+            self.raise_login_required(metadata_available=True)
 
         self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/npo.py b/yt_dlp/extractor/npo.py
index ca6dbfc81..573a89092 100644
--- a/yt_dlp/extractor/npo.py
+++ b/yt_dlp/extractor/npo.py
@@ -247,7 +247,7 @@ def _get_info(self, url, video_id):
 
         if not formats:
             if not self._downloader.params.get('allow_unplayable_formats') and drm:
-                raise ExtractorError('This video is DRM protected.', expected=True)
+                self.raise_no_formats('This video is DRM protected.', expected=True)
             return
 
         self._sort_formats(formats)
diff --git a/yt_dlp/extractor/odnoklassniki.py b/yt_dlp/extractor/odnoklassniki.py
index 7ed9fac55..0ce2e3776 100644
--- a/yt_dlp/extractor/odnoklassniki.py
+++ b/yt_dlp/extractor/odnoklassniki.py
@@ -260,7 +260,7 @@ def _real_extract(self, url):
         if not formats:
             payment_info = metadata.get('paymentInfo')
             if payment_info:
-                raise ExtractorError('This video is paid, subscribe to download it', expected=True)
+                self.raise_no_formats('This video is paid, subscribe to download it', expected=True)
 
         self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/ooyala.py b/yt_dlp/extractor/ooyala.py
index eb957b8fe..7204dfecd 100644
--- a/yt_dlp/extractor/ooyala.py
+++ b/yt_dlp/extractor/ooyala.py
@@ -10,7 +10,6 @@
 )
 from ..utils import (
     determine_ext,
-    ExtractorError,
     float_or_none,
     int_or_none,
     try_get,
@@ -85,7 +84,7 @@ def _extract(self, content_tree_url, video_id, domain=None, supportedformats=Non
                     'fps': float_or_none(stream.get('framerate')),
                 })
         if not formats and not auth_data.get('authorized'):
-            raise ExtractorError('%s said: %s' % (
+            self.raise_no_formats('%s said: %s' % (
                 self.IE_NAME, auth_data['message']), expected=True)
         self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/philharmoniedeparis.py b/yt_dlp/extractor/philharmoniedeparis.py
index 03da64b11..9545adebf 100644
--- a/yt_dlp/extractor/philharmoniedeparis.py
+++ b/yt_dlp/extractor/philharmoniedeparis.py
@@ -79,7 +79,7 @@ def extract_entry(source):
                 formats.extend(self._extract_m3u8_formats(
                     m3u8_url, video_id, 'mp4', entry_protocol='m3u8_native',
                     m3u8_id='hls', fatal=False))
-            if not formats:
+            if not formats and not self._downloader.params.get('ignore_no_formats'):
                 return
             self._sort_formats(formats)
             return {
diff --git a/yt_dlp/extractor/rai.py b/yt_dlp/extractor/rai.py
index 6c2191bb3..05cf84ba5 100644
--- a/yt_dlp/extractor/rai.py
+++ b/yt_dlp/extractor/rai.py
@@ -94,7 +94,7 @@ def _extract_relinker_info(self, relinker_url, video_id):
                 })
 
         if not formats and geoprotection is True:
-            self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
+            self.raise_geo_restricted(countries=self._GEO_COUNTRIES, metadata_available=True)
 
         return dict((k, v) for k, v in {
             'is_live': is_live,
diff --git a/yt_dlp/extractor/ruutu.py b/yt_dlp/extractor/ruutu.py
index f9f30e3dd..5030c01cd 100644
--- a/yt_dlp/extractor/ruutu.py
+++ b/yt_dlp/extractor/ruutu.py
@@ -202,7 +202,7 @@ def pv(name):
         if not formats:
             if (not self._downloader.params.get('allow_unplayable_formats')
                     and xpath_text(video_xml, './Clip/DRM', default=None)):
-                raise ExtractorError('This video is DRM protected.', expected=True)
+                self.raise_no_formats('This video is DRM protected.', expected=True)
             ns_st_cds = pv('ns_st_cds')
             if ns_st_cds != 'free':
                 raise ExtractorError('This video is %s.' % ns_st_cds, expected=True)
diff --git a/yt_dlp/extractor/soundcloud.py b/yt_dlp/extractor/soundcloud.py
index 103b23bf7..35d34af02 100644
--- a/yt_dlp/extractor/soundcloud.py
+++ b/yt_dlp/extractor/soundcloud.py
@@ -498,7 +498,7 @@ def add_format(f, protocol, is_preview=False):
             f['vcodec'] = 'none'
 
         if not formats and info.get('policy') == 'BLOCK':
-            self.raise_geo_restricted()
+            self.raise_geo_restricted(metadata_available=True)
         self._sort_formats(formats)
 
         user = info.get('user') or {}
diff --git a/yt_dlp/extractor/sportdeutschland.py b/yt_dlp/extractor/sportdeutschland.py
index 3e497a939..e70d1a477 100644
--- a/yt_dlp/extractor/sportdeutschland.py
+++ b/yt_dlp/extractor/sportdeutschland.py
@@ -77,7 +77,7 @@ def entries():
                         continue
                     formats = self._extract_m3u8_formats(
                         video_url.replace('.smil', '.m3u8'), video_id, 'mp4', fatal=False)
-                    if not formats:
+                    if not formats and not self._downloader.params.get('ignore_no_formats'):
                         continue
                     yield {
                         'id': video_id,
diff --git a/yt_dlp/extractor/steam.py b/yt_dlp/extractor/steam.py
index a6a191ceb..c70bdefe2 100644
--- a/yt_dlp/extractor/steam.py
+++ b/yt_dlp/extractor/steam.py
@@ -139,7 +139,7 @@ def _real_extract(self, url):
                                         'format_id': ext + quality,
                                         'url': video_url,
                                     })
-                if not formats:
+                if not formats and not self._downloader.params.get('ignore_no_formats'):
                     continue
                 entry['formats'] = formats
                 entries.append(entry)
diff --git a/yt_dlp/extractor/svt.py b/yt_dlp/extractor/svt.py
index aba9bb447..5b377ea83 100644
--- a/yt_dlp/extractor/svt.py
+++ b/yt_dlp/extractor/svt.py
@@ -49,7 +49,7 @@ def _extract_video(self, video_info, video_id):
         if not formats and rights.get('geoBlockedSweden'):
             self.raise_geo_restricted(
                 'This video is only available in Sweden',
-                countries=self._GEO_COUNTRIES)
+                countries=self._GEO_COUNTRIES, metadata_available=True)
         self._sort_formats(formats)
 
         subtitles = {}
diff --git a/yt_dlp/extractor/toggle.py b/yt_dlp/extractor/toggle.py
index 1e2a2d819..fe1841081 100644
--- a/yt_dlp/extractor/toggle.py
+++ b/yt_dlp/extractor/toggle.py
@@ -7,7 +7,6 @@
 from .common import InfoExtractor
 from ..utils import (
     determine_ext,
-    ExtractorError,
     float_or_none,
     int_or_none,
     parse_iso8601,
@@ -156,10 +155,9 @@ def _real_extract(self, url):
             for meta in (info.get('Metas') or []):
                 if (not self._downloader.params.get('allow_unplayable_formats')
                         and meta.get('Key') == 'Encryption' and meta.get('Value') == '1'):
-                    raise ExtractorError(
+                    self.raise_no_formats(
                         'This video is DRM protected.', expected=True)
-            # Most likely because geo-blocked
-            raise ExtractorError('No downloadable videos found', expected=True)
+            # Most likely because geo-blocked if no formats and no DRM
         self._sort_formats(formats)
 
         thumbnails = []
diff --git a/yt_dlp/extractor/tv2.py b/yt_dlp/extractor/tv2.py
index 334b7d540..f3480de56 100644
--- a/yt_dlp/extractor/tv2.py
+++ b/yt_dlp/extractor/tv2.py
@@ -103,7 +103,7 @@ def _real_extract(self, url):
                         'filesize': int_or_none(item.get('fileSize')),
                     })
         if not formats and data.get('drmProtected'):
-            raise ExtractorError('This video is DRM protected.', expected=True)
+            self.raise_no_formats('This video is DRM protected.', expected=True)
         self._sort_formats(formats)
 
         thumbnails = [{
diff --git a/yt_dlp/extractor/tv4.py b/yt_dlp/extractor/tv4.py
index b73bab9a8..b8ad4fafc 100644
--- a/yt_dlp/extractor/tv4.py
+++ b/yt_dlp/extractor/tv4.py
@@ -107,7 +107,7 @@ def _real_extract(self, url):
             video_id, ism_id='mss', fatal=False))
 
         if not formats and info.get('is_geo_restricted'):
-            self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
+            self.raise_geo_restricted(countries=self._GEO_COUNTRIES, metadata_available=True)
 
         self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/tvplay.py b/yt_dlp/extractor/tvplay.py
index 0d858c025..739c61cdd 100644
--- a/yt_dlp/extractor/tvplay.py
+++ b/yt_dlp/extractor/tvplay.py
@@ -298,7 +298,8 @@ def _real_extract(self, url):
 
         if not formats and video.get('is_geo_blocked'):
             self.raise_geo_restricted(
-                'This content might not be available in your country due to copyright reasons')
+                'This content might not be available in your country due to copyright reasons',
+                metadata_available=True)
 
         self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/videomore.py b/yt_dlp/extractor/videomore.py
index e0c10aa5b..05ae74e33 100644
--- a/yt_dlp/extractor/videomore.py
+++ b/yt_dlp/extractor/videomore.py
@@ -10,7 +10,6 @@
     compat_urllib_parse_urlparse,
 )
 from ..utils import (
-    ExtractorError,
     int_or_none,
 )
 
@@ -193,8 +192,8 @@ def _real_extract(self, url):
             error = item.get('error')
             if error:
                 if error in ('Данное видео недоступно для просмотра на территории этой страны', 'Данное видео доступно для просмотра только на территории России'):
-                    self.raise_geo_restricted(countries=['RU'])
-                raise ExtractorError(error, expected=True)
+                    self.raise_geo_restricted(countries=['RU'], metadata_available=True)
+                self.raise_no_formats(error, expected=True)
         self._sort_formats(formats)
 
         return {
diff --git a/yt_dlp/extractor/vube.py b/yt_dlp/extractor/vube.py
index 8ce3a6b81..c92b47e63 100644
--- a/yt_dlp/extractor/vube.py
+++ b/yt_dlp/extractor/vube.py
@@ -8,7 +8,6 @@
 )
 from ..utils import (
     int_or_none,
-    ExtractorError,
 )
 
 
@@ -125,13 +124,13 @@ def _real_extract(self, url):
                 })
             formats.append(fmt)
 
-        self._sort_formats(formats)
-
         if not formats and video.get('vst') == 'dmca':
-            raise ExtractorError(
+            self.raise_no_formats(
                 'This video has been removed in response to a complaint received under the US Digital Millennium Copyright Act.',
                 expected=True)
 
+        self._sort_formats(formats)
+
         title = video['title']
         description = video.get('description')
         thumbnail = self._proto_relative_url(video.get('thumbnail_src'), scheme='http:')
diff --git a/yt_dlp/extractor/wat.py b/yt_dlp/extractor/wat.py
index f1bccc2d6..05dcc1f17 100644
--- a/yt_dlp/extractor/wat.py
+++ b/yt_dlp/extractor/wat.py
@@ -87,7 +87,7 @@ def extract_formats(manifest_urls):
         extract_formats({delivery.get('format'): delivery.get('url')})
         if not formats:
             if delivery.get('drm'):
-                raise ExtractorError('This video is DRM protected.', expected=True)
+                self.raise_no_formats('This video is DRM protected.', expected=True)
             manifest_urls = self._download_json(
                 'http://www.wat.tv/get/webhtml/' + video_id, video_id, fatal=False)
             if manifest_urls:
diff --git a/yt_dlp/extractor/yahoo.py b/yt_dlp/extractor/yahoo.py
index a17b10d6e..ecf2f5f48 100644
--- a/yt_dlp/extractor/yahoo.py
+++ b/yt_dlp/extractor/yahoo.py
@@ -239,7 +239,7 @@ def _extract_yahoo_video(self, video_id, country):
                 'm3u8_native', m3u8_id='hls', fatal=False))
 
         if not formats and msg == 'geo restricted':
-            self.raise_geo_restricted()
+            self.raise_geo_restricted(metadata_available=True)
 
         self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/youtube.py b/yt_dlp/extractor/youtube.py
index 6d5ef0193..6c1a5b881 100644
--- a/yt_dlp/extractor/youtube.py
+++ b/yt_dlp/extractor/youtube.py
@@ -2050,7 +2050,7 @@ def feed_entry(name):
 
         if not formats:
             if not self._downloader.params.get('allow_unplayable_formats') and streaming_data.get('licenseInfos'):
-                raise ExtractorError(
+                self.raise_no_formats(
                     'This video is DRM protected.', expected=True)
             pemr = try_get(
                 playability_status,
@@ -2065,11 +2065,10 @@ def feed_entry(name):
                     if not countries:
                         regions_allowed = search_meta('regionsAllowed')
                         countries = regions_allowed.split(',') if regions_allowed else None
-                    self.raise_geo_restricted(
-                        subreason, countries)
+                    self.raise_geo_restricted(subreason, countries, metadata_available=True)
                 reason += '\n' + subreason
             if reason:
-                raise ExtractorError(reason, expected=True)
+                self.raise_no_formats(reason, expected=True)
 
         self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/zingmp3.py b/yt_dlp/extractor/zingmp3.py
index 207c04f5e..a3edc158f 100644
--- a/yt_dlp/extractor/zingmp3.py
+++ b/yt_dlp/extractor/zingmp3.py
@@ -3,7 +3,6 @@
 
 from .common import InfoExtractor
 from ..utils import (
-    ExtractorError,
     int_or_none,
 )
 
@@ -48,8 +47,8 @@ def _extract_item(self, item, fatal):
                 return
             msg = item['msg']
             if msg == 'Sorry, this content is not available in your country.':
-                self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
-            raise ExtractorError(msg, expected=True)
+                self.raise_geo_restricted(countries=self._GEO_COUNTRIES, metadata_available=True)
+            self.raise_no_formats(msg, expected=True)
         self._sort_formats(formats)
 
         subtitles = None
diff --git a/yt_dlp/options.py b/yt_dlp/options.py
index cced9fb89..fef1e4b15 100644
--- a/yt_dlp/options.py
+++ b/yt_dlp/options.py
@@ -749,6 +749,16 @@ def _dict_from_multiple_values_options_callback(
         '-s', '--simulate',
         action='store_true', dest='simulate', default=False,
         help='Do not download the video and do not write anything to disk')
+    verbosity.add_option(
+        '--ignore-no-formats-error',
+        action='store_true', dest='ignore_no_formats_error', default=False,
+        help=(
+            'Ignore "No video formats" error. Usefull for extracting metadata '
+            'even if the video is not actually available for download (experimental)'))
+    verbosity.add_option(
+        '--no-ignore-no-formats-error',
+        action='store_false', dest='ignore_no_formats_error',
+        help='Throw error when no downloadable video formats are found (default)')
     verbosity.add_option(
         '--skip-download', '--no-download',
         action='store_true', dest='skip_download', default=False,