diff --git a/yt_dlp/extractor/niconico.py b/yt_dlp/extractor/niconico.py index fc050c383b..0d0f7ceef0 100644 --- a/yt_dlp/extractor/niconico.py +++ b/yt_dlp/extractor/niconico.py @@ -32,7 +32,7 @@ from ..utils import ( urlencode_postdata, urljoin, ) -from ..utils.traversal import find_element, traverse_obj +from ..utils.traversal import find_element, require, traverse_obj class NiconicoBaseIE(InfoExtractor): @@ -283,35 +283,54 @@ class NiconicoIE(NiconicoBaseIE): lambda _, v: v['id'] == video_fmt['format_id'], 'qualityLevel', {int_or_none}, any)) or -1 yield video_fmt + def _extract_server_response(self, webpage, video_id, fatal=True): + try: + return traverse_obj( + self._parse_json(self._html_search_meta('server-response', webpage) or '', video_id), + ('data', 'response', {dict}, {require('server response')})) + except ExtractorError: + if not fatal: + return {} + raise + def _real_extract(self, url): video_id = self._match_id(url) try: webpage, handle = self._download_webpage_handle( - 'https://www.nicovideo.jp/watch/' + video_id, video_id) + f'https://www.nicovideo.jp/watch/{video_id}', video_id, + headers=self.geo_verification_headers()) if video_id.startswith('so'): video_id = self._match_id(handle.url) - api_data = traverse_obj( - self._parse_json(self._html_search_meta('server-response', webpage) or '', video_id), - ('data', 'response', {dict})) - if not api_data: - raise ExtractorError('Server response data not found') + api_data = self._extract_server_response(webpage, video_id) except ExtractorError as e: try: api_data = self._download_json( - f'https://www.nicovideo.jp/api/watch/v3/{video_id}?_frontendId=6&_frontendVersion=0&actionTrackId=AAAAAAAAAA_{round(time.time() * 1000)}', video_id, - note='Downloading API JSON', errnote='Unable to fetch data')['data'] + f'https://www.nicovideo.jp/api/watch/v3/{video_id}', video_id, + 'Downloading API JSON', 'Unable to fetch data', query={ + '_frontendId': '6', + '_frontendVersion': '0', + 'actionTrackId': f'AAAAAAAAAA_{round(time.time() * 1000)}', + }, headers=self.geo_verification_headers())['data'] except ExtractorError: if not isinstance(e.cause, HTTPError): + # Raise if original exception was from _parse_json or utils.traversal.require raise + # The webpage server response has more detailed error info than the API response webpage = e.cause.response.read().decode('utf-8', 'replace') - error_msg = self._html_search_regex( - r'(?s)(.+?)', - webpage, 'error reason', default=None) - if not error_msg: + reason_code = self._extract_server_response( + webpage, video_id, fatal=False).get('reasonCode') + if not reason_code: raise - raise ExtractorError(clean_html(error_msg), expected=True) + if reason_code in ('DOMESTIC_VIDEO', 'HIGH_RISK_COUNTRY_VIDEO'): + self.raise_geo_restricted(countries=self._GEO_COUNTRIES) + elif reason_code == 'HIDDEN_VIDEO': + raise ExtractorError( + 'The viewing period of this video has expired', expected=True) + elif reason_code == 'DELETED_VIDEO': + raise ExtractorError('This video has been deleted', expected=True) + raise ExtractorError(f'Niconico says: {reason_code}') availability = self._availability(**(traverse_obj(api_data, ('payment', 'video', { 'needs_premium': ('isPremium', {bool}),