diff --git a/addon.xml b/addon.xml index af725f56a..b31803092 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index e55c54b05..a8f12f650 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,9 @@ +## v7.0.5+beta.3 +### Fixed +- Fix error message when rating video #666 +- Fix various issues with Kodi 18 and Python 2 #668 +- Fix issues with video playback #654, #659, #663 + ## v7.0.5+beta.2 ### Fixed - Fix typos #661 diff --git a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py index 84e94a399..39805884d 100644 --- a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py +++ b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py @@ -15,6 +15,7 @@ 'parse_qsl', 'quote', 'string_type', + 'to_str', 'unescape', 'unquote', 'urlencode', @@ -52,6 +53,7 @@ string_type = str byte_string_type = bytes + to_str = str # Compatibility shims for Kodi v18 and Python v2.7 except ImportError: import BaseHTTPServer @@ -79,23 +81,21 @@ def quote(data, *args, **kwargs): - return _quote(data.encode('utf-8'), *args, **kwargs) + return _quote(to_str(data), *args, **kwargs) def unquote(data): - return _unquote(data.encode('utf-8')) + return _unquote(to_str(data)) def urlencode(data, *args, **kwargs): if isinstance(data, dict): data = data.items() return _urlencode({ - key.encode('utf-8'): ( - [part.encode('utf-8') if isinstance(part, unicode) - else str(part) - for part in value] if isinstance(value, (list, tuple)) - else value.encode('utf-8') if isinstance(value, unicode) - else str(value) + to_str(key): ( + [to_str(part) for part in value] + if isinstance(value, (list, tuple)) else + to_str(value) ) for key, value in data }, *args, **kwargs) @@ -121,6 +121,11 @@ def _file_closer(*args, **kwargs): string_type = basestring byte_string_type = (bytes, str) + def to_str(value): + if isinstance(value, unicode): + return value.encode('utf-8') + return str(value) + # Kodi v20+ if hasattr(xbmcgui.ListItem, 'setDateTime'): def datetime_infolabel(datetime_obj): diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 24d3bb5f0..f436aaef0 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -13,7 +13,7 @@ import os from .. import logger -from ..compatibility import urlencode +from ..compatibility import to_str, urlencode from ..json_store import AccessManager from ..sql_store import ( DataCache, @@ -265,7 +265,7 @@ def parse_params(self, params=None): val for val in value.split(',') if val ] elif param in self._STRING_PARAMS: - parsed_value = str(value) + parsed_value = to_str(value) # process and translate deprecated parameters if param == 'action': if parsed_value in ('play_all', 'play_video'): @@ -385,3 +385,6 @@ def get_infolabel(name): @staticmethod def get_listitem_detail(detail_name, attr=False): raise NotImplementedError() + + def tear_down(self): + pass diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index d62419169..45bd5d7ff 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -648,3 +648,8 @@ def get_listitem_detail(detail_name, attr=False): if attr else 'Container.ListItem(0).Property({0})'.format(detail_name) ) + + def tear_down(self): + self._settings.flush() + del self._addon + self._addon = None diff --git a/resources/lib/youtube_plugin/kodion/items/audio_item.py b/resources/lib/youtube_plugin/kodion/items/audio_item.py index 4147fd5aa..fc4dcf15a 100644 --- a/resources/lib/youtube_plugin/kodion/items/audio_item.py +++ b/resources/lib/youtube_plugin/kodion/items/audio_item.py @@ -11,7 +11,7 @@ from __future__ import absolute_import, division, unicode_literals from .base_item import BaseItem -from ..compatibility import unescape +from ..compatibility import to_str, unescape class AudioItem(BaseItem): @@ -54,7 +54,7 @@ def add_artist(self, artist): if self._artists is None: self._artists = [] if artist: - self._artists.append(str(artist)) + self._artists.append(to_str(artist)) def get_artists(self): return self._artists @@ -72,7 +72,7 @@ def add_genre(self, genre): if self._genres is None: self._genres = [] if genre: - self._genres.append(str(genre)) + self._genres.append(to_str(genre)) def get_genres(self): return self._genres diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 036a06d89..0d610e245 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -14,13 +14,12 @@ from datetime import date, datetime from hashlib import md5 -from ..compatibility import datetime_infolabel, string_type, unescape +from ..compatibility import datetime_infolabel, string_type, to_str, unescape from ..constants import MEDIA_PATH class BaseItem(object): VERSION = 3 - INFO_DATE = 'date' # (string) iso 8601 _playable = False @@ -48,43 +47,19 @@ def __init__(self, name, uri, image='', fanart=''): self._next_page = False def __str__(self): - name = self._name - uri = self._uri - image = self._image - obj_str = "------------------------------\n'%s'\nURI: %s\nImage: %s\n------------------------------" % (name, uri, image) - return obj_str + return ('------------------------------\n' + 'Name: |{0}|\n' + 'URI: |{1}|\n' + 'Image: |{2}|\n' + '------------------------------'.format(self._name, + self._uri, + self._image)) def to_dict(self): return {'type': self.__class__.__name__, 'data': self.__dict__} def dumps(self): - def _encoder(obj): - if isinstance(obj, (date, datetime)): - class_name = obj.__class__.__name__ - - if 'fromisoformat' in dir(obj): - return { - '__class__': class_name, - '__isoformat__': obj.isoformat(), - } - - if class_name == 'datetime': - if obj.tzinfo: - format_string = '%Y-%m-%dT%H:%M:%S%z' - else: - format_string = '%Y-%m-%dT%H:%M:%S' - else: - format_string = '%Y-%m-%d' - - return { - '__class__': class_name, - '__format_string__': format_string, - '__value__': obj.strftime(format_string) - } - - return json.JSONEncoder().default(obj) - - return json.dumps(self.to_dict(), ensure_ascii=False, default=_encoder) + return json.dumps(self.to_dict(), ensure_ascii=False, cls=_Encoder) def get_id(self): """ @@ -230,5 +205,43 @@ def next_page(self, value): self._next_page = bool(value) @property - def playable(cls): - return cls._playable + def playable(self): + return self._playable + + +class _Encoder(json.JSONEncoder): + def encode(self, obj): + if isinstance(obj, string_type): + return to_str(obj) + + if isinstance(obj, dict): + return {to_str(key): self.encode(value) + for key, value in obj.items()} + + if isinstance(obj, (list, tuple)): + return [self.encode(item) for item in obj] + + if isinstance(obj, (date, datetime)): + class_name = obj.__class__.__name__ + + if 'fromisoformat' in dir(obj): + return { + '__class__': class_name, + '__isoformat__': obj.isoformat(), + } + + if class_name == 'datetime': + if obj.tzinfo: + format_string = '%Y-%m-%dT%H:%M:%S%z' + else: + format_string = '%Y-%m-%dT%H:%M:%S' + else: + format_string = '%Y-%m-%d' + + return { + '__class__': class_name, + '__format_string__': format_string, + '__value__': obj.strftime(format_string) + } + + return self.iterencode(obj) diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 76f52da34..acfabd368 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -14,7 +14,7 @@ import re from .base_item import BaseItem -from ..compatibility import datetime_infolabel, unescape +from ..compatibility import datetime_infolabel, to_str, unescape from ..utils import duration_to_seconds, seconds_to_duration @@ -71,7 +71,7 @@ def add_artist(self, artist): if self._artists is None: self._artists = [] if artist: - self._artists.append(str(artist)) + self._artists.append(to_str(artist)) def get_artists(self): return self._artists @@ -83,7 +83,7 @@ def add_studio(self, studio): if self._studios is None: self._studios = [] if studio: - self._studios.append(str(studio)) + self._studios.append(to_str(studio)) def get_studios(self): return self._studios @@ -156,7 +156,7 @@ def add_directors(self, director): if self._directors is None: self._directors = [] if director: - self._directors.append(str(director)) + self._directors.append(to_str(director)) def get_directors(self): return self._directors @@ -169,10 +169,10 @@ def add_cast(self, member, role=None, order=None, thumbnail=None): self._cast = [] if member: self._cast.append({ - 'member': str(member), - 'role': str(role) if role else '', + 'member': to_str(member), + 'role': to_str(role) if role else '', 'order': int(order) if order else len(self._cast) + 1, - 'thumbnail': str(thumbnail) if thumbnail else '', + 'thumbnail': to_str(thumbnail) if thumbnail else '', }) def get_cast(self): @@ -262,7 +262,7 @@ def add_genre(self, genre): if self._genres is None: self._genres = [] if genre: - self._genres.append(str(genre)) + self._genres.append(to_str(genre)) def get_genres(self): return self._genres diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 20c85d157..4f0a18904 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -49,8 +49,6 @@ def run(self, provider, context): context.log_warning('Multiple busy dialogs active - ' 'playlist cleared to avoid Kodi crash') - ui.show_notification('Multiple busy dialogs active - ' - 'Kodi may crash') num_items = 0 items = ui.get_property('playlist') diff --git a/resources/lib/youtube_plugin/kodion/plugin_runner.py b/resources/lib/youtube_plugin/kodion/plugin_runner.py index aa3bc066e..45a659881 100644 --- a/resources/lib/youtube_plugin/kodion/plugin_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin_runner.py @@ -58,8 +58,10 @@ def run(provider, context=None): path=context.get_path(), params=params)) - plugin.run(provider, context) - provider.tear_down(context) + try: + plugin.run(provider, context) + finally: + if profiler: + profiler.print_stats() - if profiler: - profiler.print_stats() + provider.tear_down(context) diff --git a/resources/lib/youtube_plugin/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index 7fbc038a7..872e2ecc2 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -347,4 +347,5 @@ def run(argv): _user_actions(context, action, params) return finally: + context.tear_down() ui.clear_property(WAIT_FLAG) diff --git a/resources/lib/youtube_plugin/kodion/service_runner.py b/resources/lib/youtube_plugin/kodion/service_runner.py index 70a0c18f4..f48e6c77d 100644 --- a/resources/lib/youtube_plugin/kodion/service_runner.py +++ b/resources/lib/youtube_plugin/kodion/service_runner.py @@ -61,3 +61,5 @@ def run(): if monitor.httpd: monitor.shutdown_httpd() # shutdown http server + + context.tear_down() diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 2bbf796f5..c924301a0 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -13,7 +13,7 @@ import sys from ..constants import settings -from ..utils import validate_ip_address +from ..utils import current_system_version, validate_ip_address class AbstractSettings(object): @@ -383,6 +383,17 @@ def get_history_playlist(self): def set_history_playlist(self, value): return self.set_string(settings.HISTORY_PLAYLIST, value) - def get_label_color(self, label_part): - setting_name = '.'.join((settings.LABEL_COLOR, label_part)) - return self.get_string(setting_name, 'white') + if current_system_version.compatible(20, 0): + def get_label_color(self, label_part): + setting_name = '.'.join((settings.LABEL_COLOR, label_part)) + return self.get_string(setting_name, 'white') + else: + _COLOR_MAP = { + 'commentCount': 'cyan', + 'favoriteCount': 'gold', + 'likeCount': 'lime', + 'viewCount': 'lightblue', + } + + def get_label_color(self, label_part): + return self._COLOR_MAP.get(label_part, 'white') diff --git a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py index ac6f0b2e3..a2f3dfa38 100644 --- a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py @@ -65,7 +65,12 @@ def _set_string_list(store, setting, value): }) @classmethod - def flush(cls, xbmc_addon): + def flush(cls, xbmc_addon=None): + if not xbmc_addon: + del cls._instance + cls._instance = None + return + cls._echo = get_kodi_setting_bool('debug.showloginfo') cls._cache = {} if current_system_version.compatible(21, 0): diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 9b99e76da..3f5de4c02 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -15,6 +15,11 @@ class YouTubeRequestClient(BaseRequestsClass): + _ANDROID_PARAMS = 'CgIIAdgDAQ==', + # yt-dlp has chosen the following value, but this results in the android + # player response not including adaptive formats + # _ANDROID_PARAMS = 'CgIIAQ==', + CLIENTS = { # 4k no VP9 HDR # Limited subtitle availability @@ -22,7 +27,7 @@ class YouTubeRequestClient(BaseRequestsClass): '_id': 30, '_query_subtitles': True, 'json': { - 'params': '2AMBCgIQBg', + 'params': _ANDROID_PARAMS, 'context': { 'client': { 'clientName': 'ANDROID_TESTSUITE', @@ -51,7 +56,10 @@ class YouTubeRequestClient(BaseRequestsClass): '_id': 3, '_query_subtitles': True, 'json': { - 'params': '2AMBCgIQBg', + 'params': _ANDROID_PARAMS, + # yt-dlp has chosen the following value, but this results in the + # player response not including adaptive formats + # 'params': 'CgIIAQ==', 'context': { 'client': { 'clientName': 'ANDROID', @@ -82,7 +90,7 @@ class YouTubeRequestClient(BaseRequestsClass): '_id': 55, '_query_subtitles': True, 'json': { - 'params': '2AMBCgIQBg', + 'params': _ANDROID_PARAMS, 'context': { 'client': { 'clientName': 'ANDROID_EMBEDDED_PLAYER', @@ -118,7 +126,7 @@ class YouTubeRequestClient(BaseRequestsClass): '_id': 29, '_query_subtitles': True, 'json': { - 'params': '2AMBCgIQBg', + 'params': _ANDROID_PARAMS, 'context': { 'client': { 'clientName': 'ANDROID_UNPLUGGED', @@ -350,6 +358,7 @@ def build_client(cls, client_name, data=None): if data: client = merge_dicts(client, data) client = merge_dicts(cls.CLIENTS['_common'], client, templates) + client['_name'] = client_name if client.get('_access_token'): del client['params']['key'] diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 0382975ea..0e9f65a3c 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -20,7 +20,7 @@ from .login_client import LoginClient from ..helper.video_info import VideoInfo from ..youtube_exceptions import InvalidJSON, YouTubeException -from ...kodion.compatibility import string_type +from ...kodion.compatibility import string_type, to_str from ...kodion.utils import ( current_system_version, datetime_parser, @@ -1542,13 +1542,15 @@ def fetch_xml(_url, _responses): for thread in threads: thread.join(30) + do_encode = not current_system_version.compatible(19, 0) + for response in responses: if response: response.encoding = 'utf-8' xml_data = to_unicode(response.content) xml_data = xml_data.replace('\n', '') - if not current_system_version.compatible(19, 0): - xml_data = xml_data.encode('utf-8') + if do_encode: + xml_data = to_str(xml_data) root = ET.fromstring(xml_data) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 2edb68ab8..e770396f6 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1146,6 +1146,18 @@ def _get_video_info(self): for _ in range(2): for client_name in self._prioritised_clients: + if status and status != 'OK': + self._context.log_warning( + 'Failed to retrieved video info - ' + 'video_id: {0}, client: {1}, auth: {2},\n' + 'status: {3}, reason: {4}'.format( + video_id, + client['_name'], + bool(client.get('_access_token')), + status, + reason or 'UNKNOWN', + ) + ) client = self.build_client(client_name, client_data) result = self.request( @@ -1197,8 +1209,11 @@ def _get_video_info(self): ) ) if url and url.startswith('//support.google.com/youtube/answer/12318250'): + status = 'CONTENT_NOT_AVAILABLE_IN_THIS_APP' continue if video_id != video_details.get('videoId'): + status = 'CONTENT_NOT_AVAILABLE_IN_THIS_APP' + reason = 'WATCH_ON_LATEST_VERSION_OF_YOUTUBE' continue break # Only attempt to remove Authorization header if clients iterable @@ -1236,7 +1251,6 @@ def _get_video_info(self): ) ) self._selected_client = client.copy() - self._selected_client['_name'] = client_name if 'Authorization' in client['headers']: del client['headers']['Authorization'] diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index 9154a1f54..e2c6b2db2 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -65,10 +65,7 @@ def _process_rate_video(provider, context, re_match): response = provider.get_client(context).rate_video(video_id, result) - if response.get('status_code') != 204: - notify_message = context.localize('failed') - - elif response.get('status_code') == 204: + if response: # this will be set if we are in the 'Liked Video' playlist if context.get_param('refresh'): context.get_ui().refresh_container() @@ -79,6 +76,8 @@ def _process_rate_video(provider, context, re_match): notify_message = context.localize('liked.video') elif result == 'dislike': notify_message = context.localize('disliked.video') + else: + notify_message = context.localize('failed') if notify_message: context.get_ui().show_notification( diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index a08fb10c3..fd8e165a3 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1479,3 +1479,6 @@ def handle_exception(self, context, exception_to_handle): return False return True + + def tear_down(self, context): + context.tear_down()