diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index f3ed9d9b0..a0a47974e 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1254,15 +1254,15 @@ msgid "Shorts (1 minute or less)" msgstr "" msgctxt "#30737" -msgid "Use alternate client details" +msgid "" msgstr "" msgctxt "#30738" -msgid "Alternate #1" +msgid "" msgstr "" msgctxt "#30739" -msgid "Alternate #2" +msgid "" msgstr "" msgctxt "#30740" diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index bc9759721..68b624bfa 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -41,7 +41,6 @@ WATCH_LATER_PLAYLIST = 'youtube.folder.watch_later.playlist' # (str) HISTORY_PLAYLIST = 'youtube.folder.history.playlist' # (str) -CLIENT_SELECTION = 'youtube.client.selection' # (int) SUPPORT_ALTERNATIVE_PLAYER = 'kodion.support.alternative_player' # (bool) DEFAULT_PLAYER_WEB_URLS = 'kodion.default_player.web_urls' # (bool) ALTERNATIVE_PLAYER_WEB_URLS = 'kodion.alternative_player.web_urls' # (bool) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 3e51fa107..e6baeafac 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -426,11 +426,6 @@ def item_filter(self, update=None): types.update(update) return types - def client_selection(self, value=None): - if value is not None: - return self.set_int(SETTINGS.CLIENT_SELECTION, value) - return self.get_int(SETTINGS.CLIENT_SELECTION, 0) - def show_detailed_description(self, value=None): if value is not None: return self.set_bool(SETTINGS.DETAILED_DESCRIPTION, value) diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 2997a7b03..b46887155 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -58,15 +58,10 @@ def to_unicode(text): def select_stream(context, stream_data_list, - ask_for_quality=None, - audio_only=None, + ask_for_quality, + audio_only, use_adaptive_formats=True): settings = context.get_settings() - if ask_for_quality is None: - ask_for_quality = settings.ask_for_video_quality() - if audio_only is None: - audio_only = settings.audio_only() - isa_capabilities = context.inputstream_adaptive_capabilities() use_adaptive = (use_adaptive_formats and settings.use_isa() diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index e13f42e9e..aa80b0b06 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -195,10 +195,17 @@ def update_watch_history(self, context, video_id, url, status=None): self.request(url, params=params, headers=headers, error_msg='Failed to update watch history') - def get_streams(self, context, video_id, audio_only=False): + def get_streams(self, + context, + video_id, + ask_for_quality=False, + audio_only=False, + use_mpd=True): return StreamInfo(context, access_token=self._access_token_tv, - audio_only=audio_only).load_stream_infos(video_id) + ask_for_quality=ask_for_quality, + audio_only=audio_only, + use_mpd=use_mpd).load_stream_info(video_id) def remove_playlist(self, playlist_id, **kwargs): params = {'id': playlist_id, diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index f07a66f82..35b3901f5 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -684,61 +684,44 @@ def __init__(self, context, access_token='', clients=None, + ask_for_quality=False, audio_only=False, + use_mpd=True, **kwargs): self.video_id = None self._context = context - self._language_base = kwargs.get('language', 'en_US')[0:2] + self._access_token = access_token + self._ask_for_quality = ask_for_quality self._audio_only = audio_only + self._language_base = kwargs.get('language', 'en_US')[0:2] + self._use_mpd = use_mpd + self._player_js = None self._calculate_n = True self._cipher = None self._selected_client = None - client_selection = context.get_settings().client_selection() - self._prioritised_clients = clients if clients else () - - # Default client selection uses the Android or iOS client as the first - # option to ensure that the age gate setting is enforced, regardless of - # login status - - # Alternate #1 - # Prefer iOS client to access premium streams, however other stream - # types are limited - if client_selection == 1: - self._prioritised_clients += ( + self._client_groups = { + 'custom': clients if clients else (), + # Access "premium" streams, HLS and DASH + # Limited video stream availability + 'default': ( 'ios', - 'android', + ), + # Will play most videos with subtitles at full resolution with HDR + # Some restricted videos require additional requests for subtitles + # Limited audio stream availability + 'mpd': ( 'android_youtube_tv', 'android_testsuite', + ), + # Progressive streams + # Limited video and audio stream availability + 'ask': ( 'media_connect_frontend', - 'android_embedded', - ) - # Alternate #2 - # Prefer use of non-adaptive formats. - elif client_selection == 2: - self._prioritised_clients += ( - 'media_connect_frontend', - 'android', - 'android_youtube_tv', - 'android_testsuite', - 'ios', - 'android_embedded', - ) - # Default - # Will play most videos with subtitles at full resolution with HDR - # Some restricted videos require additional requests for subtitles - # Fallback to iOS, media connect, and embedded clients - else: - self._prioritised_clients += ( - 'android', - 'android_youtube_tv', - 'android_testsuite', - 'ios', - 'media_connect_frontend', - 'android_embedded', - ) + ), + } super(StreamInfo, self).__init__(context=context, **kwargs) @@ -1014,12 +997,13 @@ def _normalize_url(url): url = urljoin('https://www.youtube.com', url) return url - def _load_hls_manifest(self, - url, - is_live=False, - meta_info=None, - headers=None, - playback_stats=None): + def _update_from_hls(self, + stream_list, + url, + is_live=False, + meta_info=None, + headers=None, + playback_stats=None): if not url: return [] @@ -1059,30 +1043,27 @@ def _load_hls_manifest(self, playback_stats = {} if is_live: - stream_list = [ - self._get_stream_format( + for yt_format in ('9995', '9996'): + stream_list[yt_format] = self._get_stream_format( itag=yt_format, title='', url=url, meta=meta_info, headers=curl_headers, playback_stats=playback_stats, - ) for yt_format in ('9995', '9996') - ] - else: - stream_list = [ - self._get_stream_format( - itag='9994', - title='', - url=url, - meta=meta_info, - headers=curl_headers, - playback_stats=playback_stats, ) - ] + else: + stream_list['9994'] = self._get_stream_format( + itag='9994', + title='', + url=url, + meta=meta_info, + headers=curl_headers, + playback_stats=playback_stats, + ) settings = self._context.get_settings() - if settings.use_mpd_videos(): + if self._use_mpd: qualities = settings.mpd_video_qualities() selected_height = qualities[0]['nom_height'] else: @@ -1097,6 +1078,9 @@ def _load_hls_manifest(self, ) for match in re_playlist_data.finditer(result): itag = match.group('itag') + if itag in stream_list: + continue + yt_format = self._get_stream_format( itag=itag, max_height=selected_height, @@ -1118,16 +1102,16 @@ def _load_hls_manifest(self, if is_live: yt_format['live'] = True yt_format['title'] = 'Live ' + yt_format['title'] - stream_list.append(yt_format) - return stream_list + stream_list[itag] = yt_format - def _create_stream_list(self, - streams, - is_live=False, - meta_info=None, - headers=None, - playback_stats=None): + def _update_from_streams(self, + stream_list, + streams, + is_live=False, + meta_info=None, + headers=None, + playback_stats=None): if not headers and self._selected_client: headers = self._selected_client['headers'].copy() if 'Authorization' in headers: @@ -1147,21 +1131,24 @@ def _create_stream_list(self, playback_stats = {} settings = self._context.get_settings() - if settings.use_mpd_videos(): + if self._use_mpd: qualities = settings.mpd_video_qualities() selected_height = qualities[0]['nom_height'] else: selected_height = settings.fixed_video_quality() - stream_list = [] for stream_map in streams: + itag = str(stream_map['itag']) + if itag in stream_list: + continue + url = stream_map.get('url') conn = stream_map.get('conn') stream = stream_map.get('stream') if not url and conn and stream: new_url = '%s?%s' % (conn, unquote(stream)) - elif not url and self._cipher and 'signatureCipher' in stream_map: + elif not url and 'signatureCipher' in stream_map: new_url = self._process_signature_cipher(stream_map) else: new_url = url @@ -1170,7 +1157,6 @@ def _create_stream_list(self, continue new_url, _ = self._process_url_params(new_url) - itag = str(stream_map['itag']) stream_map['itag'] = itag yt_format = self._get_stream_format( itag=itag, @@ -1200,22 +1186,35 @@ def _create_stream_list(self, yt_format['live'] = True yt_format['title'] = 'Live ' + yt_format['title'] - if 'audioTrack' in stream_map: - audio_track = stream_map['audioTrack'] - display_name = audio_track['displayName'] - yt_format['title'] = '{0} {1}'.format( - yt_format['title'], display_name - ).strip() + audio_track = stream_map.get('audioTrack') + if audio_track: + track_id = audio_track['id'] + track_name = audio_track['displayName'] + itag = '.'.join(( + itag, + track_id, + )) + yt_format['title'] = ' '.join(( + yt_format['title'], + track_name, + )).strip() yt_format['sort'].extend(( - audio_track['id'].startswith(self._language_base), - 'original' in display_name, - display_name + track_id.startswith(self._language_base), + 'original' in track_name or audio_track['audioIsDefault'], + track_name, )) - stream_list.append(yt_format) - return stream_list + stream_list[itag] = yt_format def _process_signature_cipher(self, stream_map): + if self._cipher is None: + self._context.log_debug('signatureCipher detected') + if self._player_js is None: + self._player_js = self._get_player_js() + self._cipher = Cipher(self._context, javascript=self._player_js) + if not self._cipher: + return None + signature_cipher = parse_qs(stream_map['signatureCipher']) url = signature_cipher.get('url', [None])[0] encrypted_signature = signature_cipher.get('s', [None])[0] @@ -1239,6 +1238,7 @@ def _process_signature_cipher(self, stream_map): exc=exc, details=''.join(format_stack()) )) + self._cipher = False return None data_cache.set_item(encrypted_signature, {'sig': signature}) @@ -1257,7 +1257,8 @@ def _process_url_params(self, url): update_url = {} if self._calculate_n and 'n' in query: - self._player_js = self._player_js or self._get_player_js() + if self._player_js is None: + self._player_js = self._get_player_js() if self._calculate_n is True: self._context.log_debug('nsig detected') self._calculate_n = ratebypass.CalculateN(self._player_js) @@ -1339,22 +1340,44 @@ def _get_error_details(self, playability_status, details=None): return result['simpleText'] return None - def load_stream_infos(self, video_id): + def load_stream_info(self, video_id): self.video_id = video_id settings = self._context.get_settings() age_gate_enabled = settings.age_gate() + audio_only = self._audio_only + ask_for_quality = self._ask_for_quality + use_mpd = self._use_mpd + + client_name = None + _client = None + _result = None + playability = None + status = None + reason = None - client_name = reason = status = None - client = playability = result = None + stream_list = {} + streaming_data = {} + adaptive_fmts = [] + progressive_fmts = [] video_info_url = 'https://www.youtube.com/youtubei/v1/player' - reasons = { + abort_reasons = { 'country', - 'available', + 'not available', + } + skip_reasons = { + 'age', + 'inappropriate', 'latest version', } + retry_reasons = { + 'try again later', + 'unavailable', + 'unknown', + } + abort = False client_data = {'json': {'videoId': video_id}} if self._access_token: @@ -1363,68 +1386,114 @@ def load_stream_infos(self, video_id): else: auth = False - for client_name in self._prioritised_clients: - client = self.build_client(client_name, client_data) - if not client: + for name, clients in self._client_groups.items(): + if not clients: + continue + if name == 'mpd' and not use_mpd: + continue + if name == 'ask' and not ask_for_quality and self._selected_client: continue - result = self.request( - video_info_url, - 'POST', - response_hook=self._response_hook_json, - error_title='Player request failed', - error_hook=self._error_hook, - error_hook_kwargs={ - 'video_id': video_id, - 'client': client_name, - 'auth': bool(client.get('_access_token')), - }, - **client - ) + status = None + + for client_name in clients: + _client = self.build_client(client_name, client_data) + if not _client: + continue - video_details = result.get('videoDetails', {}) - playability = result.get('playabilityStatus', {}) - status = playability.get('status', 'ERROR').upper() - reason = playability.get('reason', '') + _result = self.request( + video_info_url, + 'POST', + response_hook=self._response_hook_json, + error_title='Player request failed', + error_hook=self._error_hook, + error_hook_kwargs={ + 'video_id': video_id, + 'client': client_name, + 'auth': bool(_client.get('_access_token')), + }, + **_client + ) - if video_details and video_id != video_details.get('videoId'): - status = 'CONTENT_NOT_AVAILABLE_IN_THIS_APP' - reason = 'Watch on the latest version of YouTube' + video_details = _result.get('videoDetails', {}) + playability = _result.get('playabilityStatus', {}) + status = playability.get('status', 'ERROR').upper() + reason = playability.get('reason', 'UNKNOWN') - if (age_gate_enabled - and playability.get('desktopLegacyAgeGateReason')): - break - elif status == 'OK': + if video_details and video_id != video_details.get('videoId'): + status = 'CONTENT_NOT_AVAILABLE_IN_THIS_APP' + reason = 'Watch on the latest version of YouTube' + + if (age_gate_enabled + and playability.get('desktopLegacyAgeGateReason')): + abort = True + break + elif status == 'OK': + break + elif status in { + 'AGE_CHECK_REQUIRED', + 'AGE_VERIFICATION_REQUIRED', + 'CONTENT_CHECK_REQUIRED', + 'LOGIN_REQUIRED', + 'CONTENT_NOT_AVAILABLE_IN_THIS_APP', + 'ERROR', + 'UNPLAYABLE', + }: + self._context.log_warning( + 'Failed to retrieve video info - ' + 'video_id: {0}, client: {1}, auth: {2},\n' + 'status: {3}, reason: {4}'.format( + video_id, + _client['_name'], + auth, + status, + reason or 'UNKNOWN', + ) + ) + compare_reason = reason.lower() + if any(why in compare_reason for why in retry_reasons): + continue + if any(why in compare_reason for why in skip_reasons): + break + if any(why in compare_reason for why in abort_reasons): + abort = True + break + else: + self._context.log_debug( + 'Unknown playabilityStatus in player response:\n|{0}|' + .format(playability) + ) + + if abort: break - elif status in { - 'AGE_CHECK_REQUIRED', - 'AGE_VERIFICATION_REQUIRED', - 'CONTENT_CHECK_REQUIRED', - 'LOGIN_REQUIRED', - 'CONTENT_NOT_AVAILABLE_IN_THIS_APP', - 'ERROR', - 'UNPLAYABLE', - }: - self._context.log_warning( - 'Failed to retrieve video info - ' - 'video_id: {0}, client: {1}, auth: {2},\n' - 'status: {3}, reason: {4}'.format( + + if status == 'OK': + self._context.log_debug( + 'Retrieved video info - ' + 'video_id: {0}, client: {1}, auth: {2}'.format( video_id, - client['_name'], - auth, - status, - reason or 'UNKNOWN', + client_name, + bool(_client.get('_access_token')), ) ) - if any(why in reason for why in reasons): - break - else: - self._context.log_debug( - 'Unknown playabilityStatus in player response:\n|{0}|' - .format(playability) - ) + if not self._selected_client: + client = self._selected_client = _client.copy() + result = _result + video_details = result.get('videoDetails', {}) + playability = result.get('playabilityStatus', {}) + + _streaming_data = _result.get('streamingData', {}) + if audio_only or ask_for_quality or not use_mpd: + progressive_fmts.extend( + _streaming_data.get('formats', []) + ) + if use_mpd: + adaptive_fmts.extend( + _streaming_data.get('adaptiveFormats', []) + ) + streaming_data.update(_streaming_data) - if status != 'OK': + if not self._selected_client: if status == 'LIVE_STREAM_OFFLINE': if not reason: reason = self._get_error_details( @@ -1441,16 +1510,6 @@ def load_stream_infos(self, video_id): reason = self._get_error_details(playability) raise YouTubeException(reason or 'UNKNOWN') - self._context.log_debug( - 'Retrieved video info - ' - 'video_id: {0}, client: {1}, auth: {2}'.format( - video_id, - client_name, - bool(client.get('_access_token')), - ) - ) - self._selected_client = client.copy() - if 'Authorization' in client['headers']: del client['headers']['Authorization'] # Make a set of URL-quoted headers to be sent to Kodi when requesting @@ -1461,7 +1520,6 @@ def load_stream_infos(self, video_id): microformat = (result.get('microformat', {}) .get('playerMicroformatRenderer', {})) - streaming_data = result.get('streamingData', {}) is_live = video_details.get('isLiveContent', False) if is_live: is_live = video_details.get('isLive', False) @@ -1522,11 +1580,8 @@ def load_stream_infos(self, video_id): 'watchtime_url': '', } - use_mpd_vod = settings.use_mpd_videos() - use_isa = settings.use_isa() - pa_li_info = streaming_data.get('licenseInfos', []) - if any(pa_li_info) and not use_isa: + if any(pa_li_info) and not settings.use_isa(): raise YouTubeException('InputStream.Adaptive not enabled') for li_info in pa_li_info: if li_info.get('drmFamily') != 'WIDEVINE': @@ -1557,20 +1612,6 @@ def load_stream_infos(self, video_id): 'token': None } - stream_list = [] - if use_isa and use_mpd_vod: - adaptive_fmts = streaming_data.get('adaptiveFormats', []) - all_fmts = streaming_data.get('formats', []) + adaptive_fmts - else: - adaptive_fmts = None - all_fmts = streaming_data.get('formats', []) - - if any(True for fmt in all_fmts - if fmt and 'url' not in fmt and 'signatureCipher' in fmt): - self._context.log_debug('signatureCipher detected') - self._player_js = self._get_player_js() - self._cipher = Cipher(self._context, javascript=self._player_js) - if 'dashManifestUrl' in streaming_data: manifest_url = streaming_data['dashManifestUrl'] if '?' in manifest_url: @@ -1580,7 +1621,7 @@ def load_stream_infos(self, video_id): else: manifest_url += '/mpd_version/5' - stream_list.append(self._get_stream_format( + stream_list['9998'] = self._get_stream_format( itag='9998', title='', url=manifest_url, @@ -1588,15 +1629,21 @@ def load_stream_infos(self, video_id): headers=curl_headers, license_info=license_info, playback_stats=playback_stats, - )) - if 'hlsManifestUrl' in streaming_data: - stream_list.extend(self._load_hls_manifest( + ) + if 'hlsManifestUrl' in streaming_data and ( + is_live + or live_dvr + or ask_for_quality + or not use_mpd + ): + self._update_from_hls( + stream_list, streaming_data['hlsManifestUrl'], is_live, meta_info, client['headers'], playback_stats, - )) + ) subtitles = Subtitles(self._context, video_id) query_subtitles = client.get('_query_subtitles') @@ -1631,7 +1678,7 @@ def load_stream_infos(self, video_id): subtitles.load(captions, caption_client['headers']) default_lang = subtitles.get_lang_details() subs_data = subtitles.get_subtitles() - if subs_data and (not use_mpd_vod or subtitles.pre_download): + if subs_data and (not use_mpd or subtitles.pre_download): meta_info['subtitles'] = [ subtitle['url'] for subtitle in subs_data.values() ] @@ -1645,7 +1692,7 @@ def load_stream_infos(self, video_id): subs_data = None # extract adaptive streams and create MPEG-DASH manifest - if adaptive_fmts and not self._audio_only: + if adaptive_fmts and not audio_only: video_data, audio_data = self._process_stream_data( adaptive_fmts, default_lang['default'] @@ -1688,18 +1735,25 @@ def load_stream_infos(self, video_id): if len(title) > 1: yt_format['title'] = ''.join(yt_format['title']) - stream_list.append(yt_format) + stream_list['9999'] = yt_format # extract non-adaptive streams - if all_fmts: - stream_list.extend(self._create_stream_list( - all_fmts, is_live, meta_info, client['headers'], playback_stats - )) + if adaptive_fmts and (audio_only or ask_for_quality): + progressive_fmts.extend(adaptive_fmts) + if progressive_fmts: + self._update_from_streams( + stream_list, + progressive_fmts, + is_live, + meta_info, + client['headers'], + playback_stats, + ) if not stream_list: raise YouTubeException('No streams found') - return stream_list + return stream_list.values() def _process_stream_data(self, stream_data, default_lang_code='und'): _settings = self._context.get_settings() @@ -1741,7 +1795,7 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): continue url = stream.get('url') - if not url and self._cipher and 'signatureCipher' in stream: + if not url and 'signatureCipher' in stream: url = self._process_signature_cipher(stream) if not url: continue @@ -1981,6 +2035,7 @@ def _generate_mpd_manifest(self, audio_data, subs_data, license_url): + # Following line can be uncommented if needed to use mpd for audio only # if (not video_data and not self._audio_only) or not audio_data: if not video_data or not audio_data: return None, None diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 1f70a0ac8..ff6645da7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -55,16 +55,20 @@ def _play_stream(provider, context): 'url': 'https://youtu.be/{0}'.format(video_id), } else: - ask_for_quality = None + ask_for_quality = settings.ask_for_video_quality() if ui.pop_property(PLAY_PROMPT_QUALITY) and not screensaver: ask_for_quality = True - audio_only = None + audio_only = settings.audio_only() if ui.pop_property(PLAY_FORCE_AUDIO): audio_only = True try: - streams = client.get_streams(context, video_id, audio_only) + streams = client.get_streams(context, + video_id, + ask_for_quality, + audio_only, + settings.use_mpd_videos()) except YouTubeException as exc: context.log_error('yt_play.play_video - {exc}:\n{details}'.format( exc=exc, details=''.join(format_stack()) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py index 755b749ff..d0917caa7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -316,7 +316,6 @@ def process_default_settings(context, step, steps, **_kwargs): (localize('setup_wizard.prompt') % localize('setup_wizard.prompt.settings.defaults')) ): - settings.client_selection(0) settings.use_isa(True) settings.use_mpd_videos(True) settings.stream_select(4 if settings.ask_for_video_quality() else 3) @@ -370,7 +369,6 @@ def process_performance_settings(context, step, steps, **_kwargs): 'num_items': 10, 'settings': ( (settings.use_isa, (False,)), - (settings.client_selection, (2,)), (settings.use_mpd_videos, (False,)), (settings.set_subtitle_download, (True,)), ), @@ -379,9 +377,6 @@ def process_performance_settings(context, step, steps, **_kwargs): 'max_resolution': 4, # 1080p 'stream_features': ('avc1', 'vorbis', 'mp4a', 'filter'), 'num_items': 10, - 'settings': ( - (settings.client_selection, (2,)), - ), }, '1080p30': { 'max_resolution': 4, # 1080p diff --git a/resources/settings.xml b/resources/settings.xml index 1bd96ae23..3150c4105 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -575,18 +575,6 @@ - - 0 - 0 - - - - - - - - - 0 false