diff --git a/addon.xml b/addon.xml index c76fee920..16419135b 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 7f6f6f6d4..d779cb404 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,22 @@ +## v7.1.1+beta.2 +### Fixed +- Standardise return type of LoginClient.refresh_token #932 +- Fix curl headers not being used when set on path of setResolvedUrl listitem +- Fix HEAD requests to MPD manifests +- Fix various Python2 incompatible changes +- Properly distinguish between VP9 and VP9.2 with HDR info +- Fix http server not running when script shows client IP + +### Changed +- Improve display and update of bookmarks +- Explicitly set http server protocol version to HTTP/1.1 +- Improve logging + +### New +- Add View all and Shuffle context menu items for playlists +- New setting to enable debug logging for addon + - Setting > Advanced > Logging > Enable debug logging + ## v7.1.1+beta.1 ### Fixed - Fix http server not listening on any interface if listen IP is 0.0.0.0 #927 diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index b9b3ba928..d00ce3398 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -554,7 +554,7 @@ msgid "None" msgstr "" msgctxt "#30562" -msgid "" +msgid "View all" msgstr "" msgctxt "#30563" diff --git a/resources/lib/youtube_plugin/kodion/__init__.py b/resources/lib/youtube_plugin/kodion/__init__.py index cb913bc4a..9b3fdb304 100644 --- a/resources/lib/youtube_plugin/kodion/__init__.py +++ b/resources/lib/youtube_plugin/kodion/__init__.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -from . import logger from .abstract_provider import ( # Abstract provider for implementation by the user AbstractProvider, diff --git a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py index 8e702ce5e..58c08a8ba 100644 --- a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py +++ b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py @@ -13,6 +13,7 @@ 'byte_string_type', 'cpu_count', 'datetime_infolabel', + 'entity_escape', 'parse_qs', 'parse_qsl', 'quote', @@ -54,12 +55,25 @@ import xbmcplugin import xbmcvfs + xbmc.LOGNOTICE = xbmc.LOGINFO xbmc.LOGSEVERE = xbmc.LOGFATAL string_type = str byte_string_type = bytes to_str = str + + + def entity_escape(text, + entities=str.maketrans({ + '&': '&', + '"': '"', + '<': '<', + '>': '>', + '\'': ''', + })): + return text.translate(entities) + # Compatibility shims for Kodi v18 and Python v2.7 except ImportError: from BaseHTTPServer import BaseHTTPRequestHandler @@ -130,11 +144,25 @@ 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) + + def entity_escape(text, + entities={ + '&': '&', + '"': '"', + '<': '<', + '>': '>', + '\'': ''', + }): + for key, value in entities.viewitems(): + text = text.replace(key, value) + return text + # Kodi v20+ if hasattr(xbmcgui.ListItem, 'setDateTime'): def datetime_infolabel(datetime_obj, *_args, **_kwargs): diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index b0431d4fe..327bf5db1 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -65,6 +65,7 @@ PLAY_FORCE_AUDIO = 'audio_only' PLAY_PROMPT_QUALITY = 'ask_for_quality' PLAY_PROMPT_SUBTITLES = 'prompt_for_subtitles' +PLAY_STRM = 'strm' PLAY_TIMESHIFT = 'timeshift' PLAY_WITH = 'play_with' @@ -124,6 +125,7 @@ 'PLAY_FORCE_AUDIO', 'PLAY_PROMPT_QUALITY', 'PLAY_PROMPT_SUBTITLES', + 'PLAY_STRM', 'PLAY_TIMESHIFT', 'PLAY_WITH', diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index ed8ffef57..3f12718ee 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -90,3 +90,5 @@ HTTPD_LISTEN = 'kodion.http.listen' # (str) HTTPD_WHITELIST = 'kodion.http.ip.whitelist' # (str) HTTPD_IDLE_SLEEP = 'youtube.http.idle_sleep' # (bool) + +LOGGING_ENABLED = 'kodion.logging.enabled' # (bool) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 5b150e511..add2755ff 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -12,13 +12,14 @@ import os -from .. import logger +from ..logger import Logger from ..compatibility import parse_qsl, quote, to_str, urlencode, urlsplit from ..constants import ( PATHS, PLAY_FORCE_AUDIO, PLAY_PROMPT_QUALITY, PLAY_PROMPT_SUBTITLES, + PLAY_STRM, PLAY_TIMESHIFT, PLAY_WITH, VALUE_FROM_STR, @@ -36,7 +37,7 @@ from ..utils import current_system_version -class AbstractContext(object): +class AbstractContext(Logger): _initialized = False _addon = None _settings = None @@ -45,6 +46,7 @@ class AbstractContext(object): PLAY_FORCE_AUDIO, PLAY_PROMPT_SUBTITLES, PLAY_PROMPT_QUALITY, + PLAY_STRM, PLAY_TIMESHIFT, PLAY_WITH, 'confirmed', @@ -58,10 +60,8 @@ class AbstractContext(object): 'incognito', 'location', 'logged_in', - 'play', 'resume', 'screensaver', - 'strm', 'window_return', } _INT_PARAMS = { @@ -430,24 +430,6 @@ def set_content(self, content_type, sub_type=None, category_label=None): def add_sort_method(self, *sort_methods): raise NotImplementedError() - def log(self, text, log_level=logger.NOTICE): - logger.log(text, log_level, self.get_id()) - - def log_warning(self, text): - self.log(text, logger.WARNING) - - def log_error(self, text): - self.log(text, logger.ERROR) - - def log_notice(self, text): - self.log(text, logger.NOTICE) - - def log_debug(self, text): - self.log(text, logger.DEBUG) - - def log_info(self, text): - self.log(text, logger.INFO) - def clone(self, new_path=None, new_params=None): raise NotImplementedError() 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 c1023005d..1c5651747 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -154,6 +154,7 @@ class XbmcContext(AbstractContext): 'playlist.progress.updating': 30536, 'playlist.removed_from': 30715, 'playlist.select': 30521, + 'playlist.view.all': 30562, 'playlists': 30501, 'please_wait': 30119, 'prompt': 30566, @@ -683,6 +684,7 @@ def use_inputstream_adaptive(self, prompt=False): 'av01': loose_version('20.3.0'), 'vp8': False, 'vp9': loose_version('2.3.14'), + 'vp9.2': loose_version('2.4.0'), } def inputstream_adaptive_capabilities(self, capability=None): diff --git a/resources/lib/youtube_plugin/kodion/debug.py b/resources/lib/youtube_plugin/kodion/debug.py index 076836681..f99a5ad54 100644 --- a/resources/lib/youtube_plugin/kodion/debug.py +++ b/resources/lib/youtube_plugin/kodion/debug.py @@ -13,7 +13,7 @@ import atexit import os -from .logger import log_debug +from .logger import Logger def debug_here(host='localhost'): @@ -145,7 +145,7 @@ def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): if not self._enabled: return - log_debug('Profiling stats: {0}'.format(self.get_stats( + Logger.log_debug('Profiling stats: {0}'.format(self.get_stats( num_lines=self._num_lines, print_callees=self._print_callees, reuse=self._reuse, @@ -270,7 +270,7 @@ def get_stats(self, return output def print_stats(self): - log_debug('Profiling stats: {0}'.format(self.get_stats( + Logger.log_debug('Profiling stats: {0}'.format(self.get_stats( num_lines=self._num_lines, print_callees=self._print_callees, reuse=self._reuse, diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 84aa969d7..7d75c9ac1 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -14,7 +14,14 @@ from datetime import date, datetime from hashlib import md5 -from ..compatibility import datetime_infolabel, string_type, to_str, unescape +from ..compatibility import ( + datetime_infolabel, + parse_qsl, + string_type, + to_str, + unescape, + urlsplit, +) from ..constants import MEDIA_PATH @@ -27,6 +34,8 @@ def __init__(self, name, uri, image=None, fanart=None): self.set_name(name) self._uri = uri + self._available = True + self._callback = None self._image = '' if image: @@ -35,6 +44,7 @@ def __init__(self, name, uri, image=None, fanart=None): if fanart: self.set_fanart(fanart) + self._bookmark_id = None self._bookmark_timestamp = None self._context_menu = None self._added_utc = None @@ -71,6 +81,47 @@ def get_id(self): """ return md5(''.join((self._name, self._uri)).encode('utf-8')).hexdigest() + def parse_item_ids_from_uri(self): + if not self._uri: + return None + + item_ids = {} + + uri = urlsplit(self._uri) + path = uri.path + params = dict(parse_qsl(uri.query)) + + video_id = params.get('video_id') + if video_id: + item_ids['video_id'] = video_id + + channel_id = None + playlist_id = None + + while path: + part, _, next_part = path.partition('/') + if not next_part: + break + + if part == 'channel': + channel_id = next_part.partition('/')[0] + elif part == 'playlist': + playlist_id = next_part.partition('/')[0] + path = next_part + + if channel_id: + item_ids['channel_id'] = channel_id + if playlist_id: + item_ids['playlist_id'] = playlist_id + + for item_id, value in item_ids.items(): + try: + setattr(self, item_id, value) + except AttributeError: + pass + + return item_ids + def set_name(self, name): try: name = unescape(name) @@ -96,6 +147,22 @@ def get_uri(self): """ return self._uri + @property + def available(self): + return self._available + + @available.setter + def available(self, value): + self._available = value + + @property + def callback(self): + return self._callback + + @callback.setter + def callback(self, value): + self._callback = value + def set_image(self, image): if not image: return @@ -190,6 +257,14 @@ def get_count(self): def set_count(self, count): self._count = int(count or 0) + @property + def bookmark_id(self): + return self._bookmark_id + + @bookmark_id.setter + def bookmark_id(self, value): + self._bookmark_id = value + def set_bookmark_timestamp(self, timestamp): self._bookmark_timestamp = timestamp @@ -200,6 +275,10 @@ def get_bookmark_timestamp(self): def playable(self): return self._playable + @playable.setter + def playable(self, value): + self._playable = value + def add_artist(self, artist): if artist: if self._artists is None: @@ -289,3 +368,6 @@ def encode(self, obj, nested=False): if nested: return output return super(_Encoder, self).encode(output) + + def default(self, obj): + pass diff --git a/resources/lib/youtube_plugin/kodion/items/media_item.py b/resources/lib/youtube_plugin/kodion/items/media_item.py index 1b59fbc9c..addaae4d9 100644 --- a/resources/lib/youtube_plugin/kodion/items/media_item.py +++ b/resources/lib/youtube_plugin/kodion/items/media_item.py @@ -14,7 +14,7 @@ from datetime import date from . import BaseItem -from ..compatibility import datetime_infolabel, to_str, unescape +from ..compatibility import datetime_infolabel, to_str, unescape, urlencode from ..constants import CONTENT from ..utils import duration_to_seconds, seconds_to_duration @@ -30,7 +30,8 @@ def __init__(self, uri, image='DefaultFile.png', fanart=None, - plot=None): + plot=None, + video_id=None,): super(MediaItem, self).__init__(name, uri, image, fanart) self._aired = None self._premiered = None @@ -62,7 +63,7 @@ def __init__(self, self._upcoming = False self._vod = False - self._video_id = None + self._video_id = video_id self._channel_id = None self._subscription_id = None self._playlist_id = None @@ -217,7 +218,9 @@ def get_track_number(self): def set_headers(self, value): self._headers = value - def get_headers(self): + def get_headers(self, as_string=False): + if as_string: + return urlencode(self._headers) return self._headers def set_license_key(self, url): @@ -338,8 +341,14 @@ def __init__(self, uri, image='DefaultAudio.png', fanart=None, - plot=None): - super(AudioItem, self).__init__(name, uri, image, fanart, plot) + plot=None, + video_id=None): + super(AudioItem, self).__init__(name, + uri, + image, + fanart, + plot, + video_id) self._album = None def set_album_name(self, album_name): @@ -364,8 +373,14 @@ def __init__(self, uri, image='DefaultVideo.png', fanart=None, - plot=None): - super(VideoItem, self).__init__(name, uri, image, fanart, plot) + plot=None, + video_id=None): + super(VideoItem, self).__init__(name, + uri, + image, + fanart, + plot, + video_id) self._directors = None self._episode = None self._imdb_id = None diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 0654b81d1..98b2069ee 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -109,27 +109,58 @@ def queue_video(context): ) -def play_all_from_playlist(context, playlist_id, video_id=''): - if video_id: - return ( - context.localize('playlist.play.from_here'), - context.create_uri( - (PATHS.PLAY,), - { - 'playlist_id': playlist_id, - 'video_id': video_id, - 'play': True, - }, - run=True, - ), - ) +def play_playlist(context, playlist_id): return ( context.localize('playlist.play.all'), context.create_uri( (PATHS.PLAY,), { 'playlist_id': playlist_id, - 'play': True, + 'order': 'ask', + }, + run=True, + ), + ) + + +def play_playlist_from(context, playlist_id, video_id): + return ( + context.localize('playlist.play.from_here'), + context.create_uri( + (PATHS.PLAY,), + { + 'playlist_id': playlist_id, + 'video_id': video_id, + }, + run=True, + ), + ) + + +def view_playlist(context, playlist_id): + return ( + context.localize('playlist.view.all'), + context.create_uri( + (PATHS.ROUTE, PATHS.PLAY,), + { + 'playlist_id': playlist_id, + 'order': 'normal', + 'action': 'list', + }, + run=True, + ), + ) + + +def shuffle_playlist(context, playlist_id): + return ( + context.localize('playlist.play.shuffle'), + context.create_uri( + (PATHS.ROUTE, PATHS.PLAY,), + { + 'playlist_id': playlist_id, + 'order': 'random', + 'action': 'list', }, run=True, ), diff --git a/resources/lib/youtube_plugin/kodion/items/utils.py b/resources/lib/youtube_plugin/kodion/items/utils.py index f93ab11fa..7b595da42 100644 --- a/resources/lib/youtube_plugin/kodion/items/utils.py +++ b/resources/lib/youtube_plugin/kodion/items/utils.py @@ -52,12 +52,17 @@ def from_json(json_data, *args): :param json_data: :return: """ + if args and args[0] and len(args[0]) == 4: + bookmark_id = args[0][0] + bookmark_timestamp = args[0][1] + else: + bookmark_id = None + bookmark_timestamp = None + if isinstance(json_data, string_type): if json_data == to_str(None): # Channel bookmark that will be updated. Store timestamp for update - if args and args[0] and len(args[0]) == 4: - return args[0][1] - return None + return bookmark_timestamp json_data = json.loads(json_data, object_hook=_decoder) item_type = json_data.get('type') @@ -70,4 +75,9 @@ def from_json(json_data, *args): if hasattr(item, key): setattr(item, key, value) + if bookmark_id: + item.bookmark_id = bookmark_id + if bookmark_timestamp: + item.set_bookmark_timestamp(bookmark_timestamp) + return item diff --git a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index 3e57e76bb..44ab20b2b 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -25,6 +25,7 @@ PLAYLISTITEM_ID, PLAYLIST_ID, PLAY_COUNT, + PLAY_STRM, PLAY_TIMESHIFT, PLAY_WITH, SUBSCRIPTION_ID, @@ -398,10 +399,12 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): context.log_debug('Converting %s |%s|' % (media_item.__class__.__name__, redact_ip(uri))) + params = context.get_params() settings = context.get_settings() ui = context.get_ui() + is_external = ui.get_property(PLAY_WITH) - is_strm = context.get_param('strm') + is_strm = params.get(PLAY_STRM) mime_type = None if is_strm: @@ -464,7 +467,7 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): 'ssl_verify_peer': False, }) - headers = media_item.get_headers() + headers = media_item.get_headers(as_string=True) if headers: props['inputstream.adaptive.manifest_headers'] = headers props['inputstream.adaptive.stream_headers'] = headers @@ -479,11 +482,13 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): mime_type = uri.split('mime=', 1)[1].split('&', 1)[0] mime_type = mime_type.replace('%2F', '/') - headers = media_item.get_headers() + headers = media_item.get_headers(as_string=True) if (headers and uri.startswith('http') and not (is_external or settings.default_player_web_urls())): - kwargs['path'] = '|'.join((uri, headers)) + uri = '|'.join((uri, headers)) + kwargs['path'] = uri + media_item.set_uri(uri) list_item = xbmcgui.ListItem(**kwargs) @@ -508,7 +513,7 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): if media_item.subtitles: list_item.setSubtitles(media_item.subtitles) - resume = context.get_param('resume') + resume = params.get('resume') set_info(list_item, media_item, props, resume=resume) return list_item diff --git a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py index f71467b52..0f836475e 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -455,7 +455,7 @@ def update_access_token(self, } if expiry is not None: - details['token_expires'] = ( + details['token_expires'] = time.time() + ( min(map(int, [val for val in expiry if val])) if isinstance(expiry, (list, tuple)) else int(expiry) diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index baede56d4..f74adff56 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -14,18 +14,18 @@ from io import open from ..constants import DATA_PATH -from ..logger import log_debug, log_error +from ..logger import Logger from ..utils import make_dirs, merge_dicts, to_unicode -class JSONStore(object): +class JSONStore(Logger): BASE_PATH = make_dirs(DATA_PATH) def __init__(self, filename): if self.BASE_PATH: self.filename = os.path.join(self.BASE_PATH, filename) else: - log_error('JSONStore.__init__ - unable to access temp directory') + self.log_error('JSONStore.__init__ - temp directory not available') self.filename = None self._data = {} @@ -42,13 +42,11 @@ def save(self, data, update=False, process=None): if update: data = merge_dicts(self._data, data) if data == self._data: - log_debug('JSONStore.save - data unchanged:\n|{filename}|'.format( - filename=self.filename - )) + self.log_debug('JSONStore.save - data unchanged:\n' + '|{filename}|'.format(filename=self.filename)) return - log_debug('JSONStore.save - saving:\n|{filename}|'.format( - filename=self.filename - )) + self.log_debug('JSONStore.save - saving:\n' + '|{filename}|'.format(filename=self.filename)) try: if not data: raise ValueError @@ -60,23 +58,20 @@ def save(self, data, update=False, process=None): sort_keys=True))) self._data = process(_data) if process is not None else _data except (IOError, OSError): - log_error('JSONStore.save - access error:\n|{filename}|'.format( - filename=self.filename - )) + self.log_error('JSONStore.save - access error:\n' + '|{filename}|'.format(filename=self.filename)) return except (TypeError, ValueError): - log_error('JSONStore.save - invalid data:\n|{data}|'.format( - data=data - )) + self.log_error('JSONStore.save - invalid data:\n' + '|{data}|'.format(data=data)) self.set_defaults(reset=True) def load(self, process=None): if not self.filename: return - log_debug('JSONStore.load - loading:\n|{filename}|'.format( - filename=self.filename - )) + self.log_debug('JSONStore.load - loading:\n' + '|{filename}|'.format(filename=self.filename)) try: with open(self.filename, mode='r', encoding='utf-8') as jsonfile: data = jsonfile.read() @@ -85,13 +80,11 @@ def load(self, process=None): _data = json.loads(data) self._data = process(_data) if process is not None else _data except (IOError, OSError): - log_error('JSONStore.load - access error:\n|{filename}|'.format( - filename=self.filename - )) + self.log_error('JSONStore.load - access error:\n' + '|{filename}|'.format(filename=self.filename)) except (TypeError, ValueError): - log_error('JSONStore.load - invalid data:\n|{data}|'.format( - data=data - )) + self.log_error('JSONStore.load - invalid data:\n' + '|{data}|'.format(data=data)) def get_data(self, process=None): try: @@ -100,9 +93,8 @@ def get_data(self, process=None): _data = json.loads(json.dumps(self._data, ensure_ascii=False)) return process(_data) if process is not None else _data except (TypeError, ValueError): - log_error('JSONStore.get_data - invalid data:\n|{data}|'.format( - data=self._data - )) + self.log_error('JSONStore.get_data - invalid data:\n' + '|{data}|'.format(data=self._data)) self.set_defaults(reset=True) _data = json.loads(json.dumps(self._data, ensure_ascii=False)) return process(_data) if process is not None else _data diff --git a/resources/lib/youtube_plugin/kodion/logger.py b/resources/lib/youtube_plugin/kodion/logger.py index b3968f2d6..063ede6ac 100644 --- a/resources/lib/youtube_plugin/kodion/logger.py +++ b/resources/lib/youtube_plugin/kodion/logger.py @@ -10,42 +10,55 @@ from __future__ import absolute_import, division, unicode_literals -from .compatibility import xbmc, xbmcaddon +from .compatibility import xbmc from .constants import ADDON_ID -DEBUG = xbmc.LOGDEBUG -INFO = xbmc.LOGINFO -NOTICE = xbmc.LOGNOTICE -WARNING = xbmc.LOGWARNING -ERROR = xbmc.LOGERROR -FATAL = xbmc.LOGFATAL -SEVERE = xbmc.LOGSEVERE -NONE = xbmc.LOGNONE -def log(text, log_level=DEBUG, addon_id=ADDON_ID): - if not addon_id: - addon_id = xbmcaddon.Addon().getAddonInfo('id') - log_line = '[%s] %s' % (addon_id, text) - xbmc.log(msg=log_line, level=log_level) - - -def log_debug(text, addon_id=ADDON_ID): - log(text, DEBUG, addon_id) - - -def log_info(text, addon_id=ADDON_ID): - log(text, INFO, addon_id) - - -def log_notice(text, addon_id=ADDON_ID): - log(text, NOTICE, addon_id) - - -def log_warning(text, addon_id=ADDON_ID): - log(text, WARNING, addon_id) - - -def log_error(text, addon_id=ADDON_ID): - log(text, ERROR, addon_id) +class Logger(object): + LOGDEBUG = xbmc.LOGDEBUG + LOGINFO = xbmc.LOGINFO + LOGNOTICE = xbmc.LOGNOTICE + LOGWARNING = xbmc.LOGWARNING + LOGERROR = xbmc.LOGERROR + LOGFATAL = xbmc.LOGFATAL + LOGSEVERE = xbmc.LOGSEVERE + LOGNONE = xbmc.LOGNONE + + @staticmethod + def log(text, log_level=LOGDEBUG, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=log_level) + + @staticmethod + def log_debug(text, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=Logger.LOGDEBUG) + + @staticmethod + def log_info(text, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=Logger.LOGINFO) + + @staticmethod + def log_notice(text, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=Logger.LOGNOTICE) + + @staticmethod + def log_warning(text, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=Logger.LOGWARNING) + + @staticmethod + def log_error(text, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=Logger.LOGERROR) + + @staticmethod + def debug_log(on=False, off=True): + if on: + Logger.LOGDEBUG = Logger.LOGNOTICE + elif off: + Logger.LOGDEBUG = xbmc.LOGDEBUG diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index fea592596..1d502d6cd 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -23,35 +23,33 @@ SERVER_WAKEUP, WAKEUP, ) -from ..logger import log_debug from ..network import get_connect_address, get_http_server, httpd_status class ServiceMonitor(xbmc.Monitor): _settings_changes = 0 - _settings_state = None + _settings_collect = False get_idle_time = xbmc.getGlobalIdleTime def __init__(self, context): self._context = context - settings = context.get_settings() - self._httpd_address, self._httpd_port = get_connect_address(context) - self._old_httpd_address = self._httpd_address - self._old_httpd_port = self._httpd_port - self._whitelist = settings.httpd_whitelist() + self._httpd_address = None + self._httpd_port = None + self._whitelist = None + self._old_httpd_address = None + self._old_httpd_port = None + self._use_httpd = None self.httpd = None self.httpd_thread = None - self.httpd_sleep_allowed = settings.httpd_sleep_allowed() + self.httpd_sleep_allowed = True self.system_idle = False self.refresh = False self.interrupt = False - self._use_httpd = None - if self.httpd_required(settings): - self.start_httpd() + self.onSettingsChanged(force=True) super(ServiceMonitor, self).__init__() @@ -87,38 +85,46 @@ def refresh_container(self, force=False): def onNotification(self, sender, method, data): if sender != ADDON_ID: return + group, separator, event = method.partition('.') + if event == WAKEUP: if not isinstance(data, dict): data = json.loads(data) if not data: return + target = data.get('target') + if target == PLUGIN_WAKEUP: self.interrupt = True + elif target == SERVER_WAKEUP: if not self.httpd and self.httpd_required(): self.start_httpd() if self.httpd_sleep_allowed: self.httpd_sleep_allowed = None + elif target == CHECK_SETTINGS: state = data.get('state') if state == 'defer': - self._settings_state = state + self._settings_collect = True elif state == 'process': - self._settings_state = state - self.onSettingsChanged() - self._settings_state = None + self.onSettingsChanged(force=True) + if data.get('response_required'): self.set_property(WAKEUP, target) + elif event == REFRESH_CONTAINER: self.refresh_container() + elif event == CONTAINER_FOCUS: if data: data = json.loads(data) if not data or not self.is_plugin_container(check_all=True): return xbmc.executebuiltin('SetFocus({0},{1},absolute)'.format(*data)) + elif event == RELOAD_ACCESS_MANAGER: self._context.reload_access_manager() self.refresh_container() @@ -137,26 +143,38 @@ def onDPMSDeactivated(self): self.system_idle = False self.interrupt = True - def onSettingsChanged(self): - self._settings_changes += 1 - if self._settings_state == 'defer': - return - changes = self._settings_changes - if self._settings_state != 'process': + def onSettingsChanged(self, force=False): + context = self._context + + if force: + self._settings_collect = False + self._settings_changes = 0 + else: + self._settings_changes += 1 + if self._settings_collect: + return + + total = self._settings_changes self.waitForAbort(1) - if changes != self._settings_changes: + if total != self._settings_changes: return - log_debug('onSettingsChanged: {0} change(s)'.format(changes)) - self._settings_changes = 0 - settings = self._context.get_settings(refresh=True) + context.log_debug('onSettingsChanged: {0} change(s)'.format(total)) + self._settings_changes = 0 + + settings = context.get_settings(refresh=True) + if settings.logging_enabled(): + context.debug_log(on=True) + else: + context.debug_log(off=True) + self.set_property(CHECK_SETTINGS) self.refresh_container() httpd_started = bool(self.httpd) httpd_restart = False - address, port = get_connect_address(self._context) + address, port = get_connect_address(context) if port != self._httpd_port: self._old_httpd_port = self._httpd_port self._httpd_port = port @@ -191,12 +209,14 @@ def start_httpd(self): if self.httpd: return - log_debug('HTTPServer: Starting |{ip}:{port}|' - .format(ip=self._httpd_address, port=self._httpd_port)) + context = self._context + context.log_debug('HTTPServer: Starting |{ip}:{port}|' + .format(ip=self._httpd_address, + port=self._httpd_port)) self.httpd_address_sync() self.httpd = get_http_server(address=self._httpd_address, port=self._httpd_port, - context=self._context) + context=context) if not self.httpd: return @@ -204,16 +224,17 @@ def start_httpd(self): self.httpd_thread.start() address = self.httpd.socket.getsockname() - log_debug('HTTPServer: Listening on |{ip}:{port}|' - .format(ip=address[0], port=address[1])) + context.log_debug('HTTPServer: Listening on |{ip}:{port}|' + .format(ip=address[0], + port=address[1])) def shutdown_httpd(self, sleep=False): if self.httpd: if sleep and self.httpd_required(while_sleeping=True): return - log_debug('HTTPServer: Shutting down |{ip}:{port}|' - .format(ip=self._old_httpd_address, - port=self._old_httpd_port)) + self._context.log_debug('HTTPServer: Shutting down |{ip}:{port}|' + .format(ip=self._old_httpd_address, + port=self._old_httpd_port)) self.httpd_address_sync() self.httpd.shutdown() self.httpd.server_close() @@ -222,11 +243,12 @@ def shutdown_httpd(self, sleep=False): self.httpd = None def restart_httpd(self): - log_debug('HTTPServer: Restarting |{old_ip}:{old_port}| > |{ip}:{port}|' - .format(old_ip=self._old_httpd_address, - old_port=self._old_httpd_port, - ip=self._httpd_address, - port=self._httpd_port)) + self._context.log_debug('HTTPServer: Restarting' + ' |{old_ip}:{old_port}| > |{ip}:{port}|' + .format(old_ip=self._old_httpd_address, + old_port=self._old_httpd_port, + ip=self._httpd_address, + port=self._httpd_port)) self.shutdown_httpd() self.start_httpd() diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 276a423c3..2542f6f5e 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -34,7 +34,6 @@ PATHS, TEMP_PATH, ) -from ..logger import log_debug, log_error from ..utils import redact_ip, validate_ip_address, wait @@ -51,6 +50,9 @@ def server_close(self): class RequestHandler(BaseHTTPRequestHandler, object): + protocol_version = 'HTTP/1.1' + server_version = 'plugin.video.youtube/1.0' + _context = None requests = None BASE_PATH = xbmcvfs.translatePath(TEMP_PATH) @@ -88,24 +90,25 @@ def connection_allowed(self): log_lines.append('Whitelisted: |%s|' % str(conn_allowed)) if not conn_allowed: - log_debug('HTTPServer: Connection from |{client_ip| not allowed' - .format(client_ip=client_ip)) + self._context.log_debug('HTTPServer: Connection blocked from' + ' |{client_ip|' + .format(client_ip=client_ip)) elif self.path != PATHS.PING: - log_debug(' '.join(log_lines)) + self._context.log_debug(' '.join(log_lines)) return conn_allowed # noinspection PyPep8Naming def do_GET(self): - settings = self._context.get_settings() - localize = self._context.localize + context = self._context + settings = context.get_settings() + localize = context.localize api_config_enabled = settings.api_config_page() # Strip trailing slash if present stripped_path = self.path.rstrip('/') if stripped_path != PATHS.PING: - log_debug('HTTPServer: GET |{path}|'.format( - path=redact_ip(self.path) - )) + context.log_debug('HTTPServer: GET |{path}|' + .format(path=redact_ip(self.path))) if not self.connection_allowed(): self.send_error(403) @@ -223,6 +226,7 @@ def do_GET(self): wait(1) self.send_response(301) self.send_header('Location', url) + self.send_header('Connection', 'close') self.end_headers() else: self.send_error(501) @@ -232,23 +236,30 @@ def do_GET(self): # noinspection PyPep8Naming def do_HEAD(self): - log_debug('HTTPServer: HEAD |{path}|'.format(path=self.path)) + self._context.log_debug('HTTPServer: HEAD |{path}|' + .format(path=self.path)) if not self.connection_allowed(): self.send_error(403) elif self.path.startswith(PATHS.MPD): - filepath = os.path.join(self.BASE_PATH, self.path[len(PATHS.MPD):]) - if not os.path.isfile(filepath): - response = ('File Not Found: |{path}| -> |{filepath}|' - .format(path=self.path, filepath=filepath)) - self.send_error(404, response) - else: + try: + file = dict(parse_qsl(urlsplit(self.path).query)).get('file') + if file: + file_path = os.path.join(self.BASE_PATH, file) + else: + file_path = None + raise IOError + + file_size = os.path.getsize(file_path) self.send_response(200) self.send_header('Content-Type', 'application/dash+xml') - self.send_header('Content-Length', - str(os.path.getsize(filepath))) + self.send_header('Content-Length', str(file_size)) self.end_headers() + except IOError: + response = ('File Not Found: |{path}| -> |{file_path}|' + .format(path=self.path, file_path=file_path)) + self.send_error(404, response) elif self.path.startswith(PATHS.REDIRECT): self.send_error(404) @@ -258,7 +269,8 @@ def do_HEAD(self): # noinspection PyPep8Naming def do_POST(self): - log_debug('HTTPServer: POST |{path}|'.format(path=self.path)) + self._context.log_debug('HTTPServer: POST |{path}|' + .format(path=self.path)) if not self.connection_allowed(): self.send_error(403) @@ -308,8 +320,9 @@ def do_POST(self): re.MULTILINE) if match: authorized_types = match.group('authorized_types').split(',') - log_debug('HTTPServer: Found authorized formats |{auth_fmts}|' - .format(auth_fmts=authorized_types)) + self._context.log_debug('HTTPServer: Found authorized formats' + ' |{auth_fmts}|' + .format(auth_fmts=authorized_types)) fmt_to_px = { 'SD': (1280 * 528) - 1, @@ -580,8 +593,10 @@ def get_http_server(address, port, context): server = HTTPServer((address, port), RequestHandler) return server except socket.error as exc: - log_error('HTTPServer: Failed to start |{address}:{port}| |{response}|' - .format(address=address, port=port, response=exc)) + context.log_error('HTTPServer: Failed to start\n' + 'Address: |{address}:{port}|\n' + 'Response: |{response}|' + .format(address=address, port=port, response=exc)) xbmcgui.Dialog().notification(context.get_name(), str(exc), context.get_icon(), @@ -606,9 +621,9 @@ def httpd_status(context): if result == 204: return True - log_debug('HTTPServer: Ping |{netloc}| - |{response}|' - .format(netloc=netloc, - response=result or 'failed')) + context.log_debug('HTTPServer: Ping |{netloc}| - |{response}|' + .format(netloc=netloc, + response=result or 'failed')) return False diff --git a/resources/lib/youtube_plugin/kodion/network/ip_api.py b/resources/lib/youtube_plugin/kodion/network/ip_api.py index 853ee216e..e3f9bc94b 100644 --- a/resources/lib/youtube_plugin/kodion/network/ip_api.py +++ b/resources/lib/youtube_plugin/kodion/network/ip_api.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals from .requests import BaseRequestsClass -from .. import logger class Locator(BaseRequestsClass): @@ -32,9 +31,10 @@ def locate_requester(self): def success(self): successful = self.response().get('status', 'fail') == 'success' if successful: - logger.log_debug('Location request was successful') + self.log_debug('Location request was successful') else: - logger.log_error(self.response().get('message', 'Location request failed with no error message')) + msg = 'Location request failed with no error message' + self.log_error(self.response().get('message') or msg) return successful def coordinates(self): @@ -44,7 +44,7 @@ def coordinates(self): lat = self._response.get('lat') lon = self._response.get('lon') if lat is None or lon is None: - logger.log_error('No coordinates returned') + self.log_error('No coordinates returned') return None - logger.log_debug('Coordinates found') + self.log_debug('Coordinates found') return {'lat': lat, 'lon': lon} diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index deb19940a..52cd9dfdf 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -19,7 +19,7 @@ from requests.utils import DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths from urllib3.util.ssl_ import create_urllib3_context -from ..logger import log_error +from ..logger import Logger __all__ = ( @@ -63,7 +63,7 @@ def cert_verify(self, conn, url, verify, cert): return super(SSLHTTPAdapter, self).cert_verify(conn, url, verify, cert) -class BaseRequestsClass(object): +class BaseRequestsClass(Logger): _session = Session() _session.mount('https://', SSLHTTPAdapter( pool_maxsize=10, @@ -198,7 +198,7 @@ def request(self, url, method='GET', ) ) - log_error('\n'.join([part for part in [ + self.log_error('\n'.join([part for part in [ error_title, error_info, response_text, stack_trace ] if part])) 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 7e54f32d3..05a6c2a9a 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -16,7 +16,6 @@ from ...compatibility import xbmcplugin from ...constants import ( BUSY_FLAG, - CHECK_SETTINGS, CONTAINER_FOCUS, CONTAINER_ID, CONTAINER_POSITION, @@ -64,10 +63,9 @@ class XbmcPlugin(AbstractPlugin): def __init__(self): super(XbmcPlugin, self).__init__() - self.handle = None def run(self, provider, context, focused=None): - self.handle = context.get_handle() + handle = context.get_handle() ui = context.get_ui() route = ui.pop_property(REROUTE_PATH) @@ -81,7 +79,7 @@ def run(self, provider, context, focused=None): break xbmcplugin.endOfDirectory( - self.handle, + handle, succeeded=False, ) @@ -154,12 +152,7 @@ def run(self, provider, context, focused=None): if ui.pop_property(RELOAD_ACCESS_MANAGER): context.reload_access_manager() - if ui.pop_property(CHECK_SETTINGS): - provider.reset_client() - settings = context.get_settings(refresh=True) - else: - settings = context.get_settings() - + settings = context.get_settings() if settings.setup_wizard_enabled(): provider.run_wizard(context) @@ -177,7 +170,7 @@ def run(self, provider, context, focused=None): except KodionException as exc: result = options = None if provider.handle_exception(context, exc): - context.log_error('XbmcRunner.run - {exc}:\n{details}'.format( + context.log_error('XbmcRunner.run - {exc!r}:\n{details}'.format( exc=exc, details=''.join(format_stack()) )) ui.on_ok('Error in ContentProvider', exc.__str__()) @@ -188,9 +181,9 @@ def run(self, provider, context, focused=None): if not result: result = [ CommandItem( - context.localize('page.back'), - 'Action(ParentDir)', - context, + name=context.localize('page.back'), + command='Action(ParentDir)', + context=context, image='DefaultFolderBack.png', plot=context.localize('page.empty'), ) @@ -211,6 +204,8 @@ def run(self, provider, context, focused=None): if options.get(provider.RESULT_FORCE_RESOLVE): result = result[0] + else: + result = None if result and result.__class__.__name__ in self._PLAY_ITEM_MAP: uri = result.get_uri() @@ -221,7 +216,8 @@ def run(self, provider, context, focused=None): result, show_fanart=context.get_settings().fanart_selection(), ) - result = xbmcplugin.addDirectoryItem(self.handle, + uri = result.get_uri() + result = xbmcplugin.addDirectoryItem(handle, url=uri, listitem=item) if route: @@ -229,7 +225,7 @@ def run(self, provider, context, focused=None): playlist_player.play_item(item=uri, listitem=item) else: context.wakeup(SERVER_WAKEUP, timeout=5) - xbmcplugin.setResolvedUrl(self.handle, + xbmcplugin.setResolvedUrl(handle, succeeded=result, listitem=item) @@ -256,7 +252,7 @@ def run(self, provider, context, focused=None): if item_count: context.apply_content() succeeded = xbmcplugin.addDirectoryItems( - self.handle, items, item_count + handle, items, item_count ) cache_to_disc = options.get(provider.RESULT_CACHE_TO_DISC, True) update_listing = options.get(provider.RESULT_UPDATE_LISTING, False) @@ -268,7 +264,7 @@ def run(self, provider, context, focused=None): update_listing = True xbmcplugin.endOfDirectory( - self.handle, + handle, succeeded=succeeded, updateListing=update_listing, cacheToDisc=cache_to_disc, diff --git a/resources/lib/youtube_plugin/kodion/plugin_runner.py b/resources/lib/youtube_plugin/kodion/plugin_runner.py index 8dcee5dd7..b009e19af 100644 --- a/resources/lib/youtube_plugin/kodion/plugin_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin_runner.py @@ -10,7 +10,9 @@ from __future__ import absolute_import, division, unicode_literals +from .constants import CHECK_SETTINGS from .context import XbmcContext +from .debug import Profiler from .plugin import XbmcPlugin from ..youtube import Provider @@ -20,21 +22,26 @@ _context = XbmcContext() _plugin = XbmcPlugin() _provider = Provider() - -_profiler = _context.get_infobool('System.GetBool(debug.showloginfo)') -_profiler = True -if _profiler: - from .debug import Profiler - - _profiler = Profiler(enabled=False, print_callees=False, num_lines=20) +_profiler = Profiler(enabled=False, print_callees=False, num_lines=20) def run(context=_context, plugin=_plugin, provider=_provider, profiler=_profiler): - if profiler: + + if context.get_ui().pop_property(CHECK_SETTINGS): + provider.reset_client() + settings = context.get_settings(refresh=True) + else: + settings = context.get_settings() + + debug = settings.logging_enabled() + if debug: + context.debug_log(on=True) profiler.enable(flush=True) + else: + context.debug_log(off=True) current_uri = context.get_uri() context.init() @@ -46,11 +53,11 @@ def run(context=_context, params[key] = '' system_version = context.get_system_version() - context.log_notice('Plugin: Running |v{version}|\n' - 'Kodi: |v{kodi}|\n' - 'Python: |v{python}|\n' - 'Path: |{path}|\n' - 'Params: |{params}|' + context.log_notice('Plugin: Running |v{version}|' + '\n\tKodi: |v{kodi}|' + '\n\tPython: |v{python}|' + '\n\tPath: |{path}|' + '\n\tParams: |{params}|' .format(version=context.get_version(), kodi=str(system_version), python=system_version.get_python_version(), @@ -59,5 +66,5 @@ def run(context=_context, plugin.run(provider, context, focused=(current_uri == new_uri)) - if profiler: + if debug: profiler.print_stats() diff --git a/resources/lib/youtube_plugin/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index 403f79b92..4a6f64a7b 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -16,6 +16,7 @@ from .constants import ( DATA_PATH, RELOAD_ACCESS_MANAGER, + SERVER_WAKEUP, TEMP_PATH, WAIT_END_FLAG, ) @@ -289,6 +290,7 @@ def _config_actions(context, action, *_args): settings.httpd_listen(addresses[selected_address]) elif action == 'show_client_ip': + context.wakeup(SERVER_WAKEUP, timeout=5) if httpd_status(context): client_ip = get_client_ip_address(context) if client_ip: @@ -647,12 +649,12 @@ def run(argv): params = dict(parse_qsl(args.query)) system_version = context.get_system_version() - context.log_notice('Script: Running |v{version}|\n' - 'Kodi: |v{kodi}|\n' - 'Python: |v{python}|\n' - 'Category: |{category}|\n' - 'Action: |{action}|\n' - 'Params: |{params}|' + context.log_notice('Script: Running |v{version}|' + '\n\tKodi: |v{kodi}|' + '\n\tPython: |v{python}|' + '\n\tCategory: |{category}|' + '\n\tAction: |{action}|' + '\n\tParams: |{params}|' .format(version=context.get_version(), kodi=str(system_version), python=system_version.get_python_version(), diff --git a/resources/lib/youtube_plugin/kodion/service_runner.py b/resources/lib/youtube_plugin/kodion/service_runner.py index da8403d3d..4666858d7 100644 --- a/resources/lib/youtube_plugin/kodion/service_runner.py +++ b/resources/lib/youtube_plugin/kodion/service_runner.py @@ -30,9 +30,9 @@ def run(): provider = Provider() system_version = context.get_system_version() - context.log_notice('Service: Starting |v{version}|\n' - 'Kodi: |v{kodi}|\n' - 'Python: |v{python}|' + context.log_notice('Service: Starting |v{version}|' + '\n\tKodi: |v{kodi}|' + '\n\tPython: |v{python}|' .format(version=context.get_version(), kodi=str(system_version), python=system_version.get_python_version())) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 81dbc3812..fa47c1a41 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -15,6 +15,7 @@ from ..constants import SETTINGS from ..utils import ( current_system_version, + get_kodi_setting_bool, get_kodi_setting_value, validate_ip_address, ) @@ -653,3 +654,7 @@ def get_label_color(self, label_part): def get_channel_name_aliases(self): return frozenset(self.get_string_list(SETTINGS.CHANNEL_NAME_ALIASES)) + + def logging_enabled(self): + return (self.get_bool(SETTINGS.LOGGING_ENABLED, False) + or get_kodi_setting_bool('debug.showloginfo')) 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 d8e82c413..6065af900 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 @@ -15,8 +15,7 @@ from ..abstract_settings import AbstractSettings from ...compatibility import xbmcaddon from ...constants import ADDON_ID, VALUE_FROM_STR -from ...logger import log_debug -from ...utils.methods import get_kodi_setting_bool +from ...logger import Logger from ...utils.system_version import current_system_version @@ -94,7 +93,7 @@ def ref(self): del self._ref -class XbmcPluginSettings(AbstractSettings): +class XbmcPluginSettings(AbstractSettings, Logger): _instances = set() _proxy = None @@ -119,7 +118,6 @@ def flush(self, xbmc_addon=None, fill=False, flush_all=True): else: fill = False - self._echo = get_kodi_setting_bool('debug.showloginfo') self._cache = {} if current_system_version.compatible(21): self._proxy = SettingsProxy(xbmc_addon.getSettings()) @@ -134,6 +132,8 @@ def flush(self, xbmc_addon=None, fill=False, flush_all=True): self.__class__._instances.add(xbmc_addon) self._proxy = SettingsProxy(xbmc_addon) + self._echo = self.logging_enabled() + def get_bool(self, setting, default=None, echo=None): if setting in self._cache: return self._cache[setting] @@ -154,11 +154,10 @@ def get_bool(self, setting, default=None, echo=None): value = default if self._echo and echo is not False: - log_debug('Get |{setting}|: {value} (bool, {status})'.format( - setting=setting, - value=value, - status=error if error else 'success' - )) + self.log_debug('Get |{setting}|: {value} (bool, {status})' + .format(setting=setting, + value=value, + status=error if error else 'success')) self._cache[setting] = value return value @@ -174,11 +173,10 @@ def set_bool(self, setting, value, echo=None): error = exc if self._echo and echo is not False: - log_debug('Set |{setting}|: {value} (bool, {status})'.format( - setting=setting, - value=value, - status=error if error else 'success' - )) + self.log_debug('Set |{setting}|: {value} (bool, {status})' + .format(setting=setting, + value=value, + status=error if error else 'success')) return not error def get_int(self, setting, default=-1, process=None, echo=None): @@ -203,11 +201,10 @@ def get_int(self, setting, default=-1, process=None, echo=None): value = default if self._echo and echo is not False: - log_debug('Get |{setting}|: {value} (int, {status})'.format( - setting=setting, - value=value, - status=error if error else 'success' - )) + self.log_debug('Get |{setting}|: {value} (int, {status})' + .format(setting=setting, + value=value, + status=error if error else 'success')) self._cache[setting] = value return value @@ -223,11 +220,10 @@ def set_int(self, setting, value, echo=None): error = exc if self._echo and echo is not False: - log_debug('Set |{setting}|: {value} (int, {status})'.format( - setting=setting, - value=value, - status=error if error else 'success' - )) + self.log_debug('Set |{setting}|: {value} (int, {status})' + .format(setting=setting, + value=value, + status=error if error else 'success')) return not error def get_string(self, setting, default='', echo=None): @@ -250,11 +246,10 @@ def get_string(self, setting, default='', echo=None): echo = '...'.join((value[:3], value[-3:])) else: echo = value - log_debug('Get |{setting}|: "{echo}" (str, {status})'.format( - setting=setting, - echo=echo, - status=error if error else 'success' - )) + self.log_debug('Get |{setting}|: "{echo}" (str, {status})' + .format(setting=setting, + echo=echo, + status=error if error else 'success')) self._cache[setting] = value return value @@ -278,11 +273,10 @@ def set_string(self, setting, value, echo=None): echo = '...'.join((value[:3], value[-3:])) else: echo = value - log_debug('Set |{setting}|: "{echo}" (str, {status})'.format( - setting=setting, - echo=echo, - status=error if error else 'success' - )) + self.log_debug('Set |{setting}|: "{echo}" (str, {status})' + .format(setting=setting, + echo=echo, + status=error if error else 'success')) return not error def get_string_list(self, setting, default=None, echo=None): @@ -299,11 +293,10 @@ def get_string_list(self, setting, default=None, echo=None): value = default if self._echo and echo is not False: - log_debug('Get |{setting}|: "{value}" (str list, {status})'.format( - setting=setting, - value=value, - status=error if error else 'success' - )) + self.log_debug('Get |{setting}|: "{value}" (str list, {status})' + .format(setting=setting, + value=value, + status=error if error else 'success')) self._cache[setting] = value return value @@ -319,9 +312,8 @@ def set_string_list(self, setting, value, echo=None): error = exc if self._echo and echo is not False: - log_debug('Set |{setting}|: "{value}" (str list, {status})'.format( - setting=setting, - value=value, - status=error if error else 'success' - )) + self.log_debug('Set |{setting}|: "{value}" (str list, {status})' + .format(setting=setting, + value=value, + status=error if error else 'success')) return not error diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 1b49ec05e..099b22509 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -17,7 +17,7 @@ from threading import Lock from traceback import format_stack -from ..logger import log_warning, log_error +from ..logger import Logger from ..utils.datetime_parser import fromtimestamp, since_epoch from ..utils.methods import make_dirs @@ -236,10 +236,10 @@ def _open(self): exc=exc, details=''.join(format_stack()) ) if isinstance(exc, sqlite3.OperationalError): - log_warning(msg) + Logger.log_warning(msg) time.sleep(0.1) else: - log_error(msg) + Logger.log_error(msg) return False else: @@ -313,10 +313,10 @@ def _execute(cursor, query, values=None, many=False, script=False): exc=exc, details=''.join(format_stack()) ) if isinstance(exc, sqlite3.OperationalError): - log_warning(msg) + Logger.log_warning(msg) time.sleep(0.1) else: - log_error(msg) + Logger.log_error(msg) return [] return [] diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index b09097f73..8b8d7e346 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -17,7 +17,7 @@ from threading import Condition, Lock from ..exceptions import KodionException -from ..logger import log_error +from ..logger import Logger try: from datetime import timezone @@ -283,8 +283,8 @@ def strptime(datetime_str, fmt=None): if strptime.reloaded.acquire(False): _strptime = import_module('_strptime') modules['_strptime'] = _strptime - log_error('Python strptime bug workaround - ' - 'https://github.com/python/cpython/issues/71587') + Logger.log_error('Python strptime bug workaround - ' + 'https://github.com/python/cpython/issues/71587') strptime.reloaded.notify_all() strptime.reloaded.release() else: diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index bdb55f6fa..0730c5374 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -18,7 +18,7 @@ from math import floor, log from ..compatibility import byte_string_type, string_type, xbmc, xbmcvfs -from ..logger import log_error +from ..logger import Logger __all__ = ( @@ -32,6 +32,7 @@ 'make_dirs', 'merge_dicts', 'print_items', + 'redact_ip', 'rm_dir', 'seconds_to_duration', 'select_stream', @@ -166,7 +167,7 @@ def make_dirs(path): if succeeded: return path - log_error('Failed to create directory: |{0}|'.format(path)) + Logger.log_error('Failed to create directory: |{0}|'.format(path)) return False @@ -186,7 +187,7 @@ def rm_dir(path): if succeeded: return True - log_error('Failed to remove directory: {0}'.format(path)) + Logger.log_error('Failed to remove directory: {0}'.format(path)) return False diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index ab14c4421..23f1fdf2b 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -10,16 +10,12 @@ from __future__ import absolute_import, division, unicode_literals -import time - from .request_client import YouTubeRequestClient from ..youtube_exceptions import ( InvalidGrant, InvalidJSON, LoginException, ) -from ...kodion.compatibility import parse_qsl -from ...kodion.logger import log_debug class LoginClient(YouTubeRequestClient): @@ -109,7 +105,7 @@ def revoke(self, refresh_token): response_hook=LoginClient._response_hook, error_hook=LoginClient._error_hook, error_title='Logout Failed', - error_info='Revoke failed: {exc}', + error_info='Revoke failed: {exc!r}', raise_exc=True) def refresh_token(self, token_type, refresh_token=None): @@ -138,15 +134,15 @@ def refresh_token(self, token_type, refresh_token=None): 'grant_type': 'refresh_token'} config_type = self._get_config_type(client_id, client_secret) - client = (('config_type: |{config_type}|\n' - 'client_id: |{id_start}...{id_end}|\n' - 'client_secret: |{secret_start}...{secret_end}|') + client = (('\n\tconfig_type: |{config_type}|' + '\n\tclient_id: |{id_start}...{id_end}|' + '\n\tclient_secret: |{secret_start}...{secret_end}|') .format(config_type=config_type, id_start=client_id[:3], id_end=client_id[-5:], secret_start=client_secret[:3], secret_end=client_secret[-3:])) - log_debug('Refresh token\n{0}'.format(client)) + self.log_debug('Refresh token:{0}'.format(client)) json_data = self.request(self.TOKEN_URL, method='POST', @@ -157,15 +153,10 @@ def refresh_token(self, token_type, refresh_token=None): error_title='Login Failed', error_info=('Refresh token failed\n' '{client}:\n' - '{{exc}}' + '{{exc!r}}' .format(client=client)), raise_exc=True) - - if json_data: - access_token = json_data['access_token'] - expiry = time.time() + int(json_data.get('expires_in', 3600)) - return access_token, expiry - return '', 0 + return json_data def request_access_token(self, token_type, code=None): login_type = self.TOKEN_TYPES.get(token_type) @@ -193,15 +184,15 @@ def request_access_token(self, token_type, code=None): 'grant_type': 'http://oauth.net/grant_type/device/1.0'} config_type = self._get_config_type(client_id, client_secret) - client = (('config_type: |{config_type}|\n' - 'client_id: |{id_start}...{id_end}|\n' - 'client_secret: |{secret_start}...{secret_end}|') + client = (('\n\tconfig_type: |{config_type}|' + '\n\tclient_id: |{id_start}...{id_end}|' + '\n\tclient_secret: |{secret_start}...{secret_end}|') .format(config_type=config_type, id_start=client_id[:3], id_end=client_id[-5:], secret_start=client_secret[:3], secret_end=client_secret[-3:])) - log_debug('Requesting access token\n{0}'.format(client)) + self.log_debug('Requesting access token:{0}'.format(client)) json_data = self.request(self.TOKEN_URL, method='POST', @@ -239,12 +230,12 @@ def request_device_and_user_code(self, token_type): 'scope': 'https://www.googleapis.com/auth/youtube'} config_type = self._get_config_type(client_id) - client = (('config_type: |{config_type}|\n' - 'client_id: |{id_start}...{id_end}|') + client = (('\n\tconfig_type: |{config_type}|' + '\n\tclient_id: |{id_start}...{id_end}|') .format(config_type=config_type, id_start=client_id[:3], id_end=client_id[-5:])) - log_debug('Requesting device and user code\n{0}'.format(client)) + self.log_debug('Requesting device and user code:{0}'.format(client)) json_data = self.request(self.DEVICE_CODE_URL, method='POST', @@ -260,49 +251,6 @@ def request_device_and_user_code(self, token_type): raise_exc=True) return json_data - def authenticate(self, username, password): - headers = {'device': '38c6ee9a82b8b10a', - 'app': 'com.google.android.youtube', - 'User-Agent': 'GoogleAuth/1.4 (GT-I9100 KTU84Q)', - 'content-type': 'application/x-www-form-urlencoded', - 'Host': 'android.clients.google.com', - 'Connection': 'keep-alive', - 'Accept-Encoding': 'gzip'} - - post_data = { - 'device_country': self._region.lower(), - 'operatorCountry': self._region.lower(), - 'lang': self._language, - 'sdk_version': '19', - # 'google_play_services_version': '6188034', - 'accountType': 'HOSTED_OR_GOOGLE', - 'Email': username.encode('utf-8'), - 'service': self.SERVICE_URLS, - 'source': 'android', - 'androidId': '38c6ee9a82b8b10a', - 'app': 'com.google.android.youtube', - # 'client_sig': '24bb24c05e47e0aefa68a58a766179d9b613a600', - 'callerPkg': 'com.google.android.youtube', - # 'callerSig': '24bb24c05e47e0aefa68a58a766179d9b613a600', - 'Passwd': password.encode('utf-8') - } - - result = self.request(self.ANDROID_CLIENT_AUTH_URL, - method='POST', - data=post_data, - headers=headers, - error_title='Login Failed', - raise_exc=True) - - lines = result.text.replace('\n', '&') - params = dict(parse_qsl(lines)) - token = params.get('Auth', '') - expires = int(params.get('Expiry', -1)) - if not token or expires == -1: - raise LoginException('Failed to get token') - - return token, expires - def _get_config_type(self, client_id, client_secret=None): """used for logging""" if client_secret is None: diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 8709aabd1..38a66b238 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -294,7 +294,7 @@ class YouTubeRequestClient(BaseRequestsClass): }, 'params': { 'key': ValueError, - 'prettyPrint': 'false' + 'prettyPrint': False, }, }, } diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 005896c01..295d2f926 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -115,7 +115,7 @@ class YouTube(LoginClient): }, 'params': { 'key': None, - 'prettyPrint': 'false' + 'prettyPrint': False, }, }, } @@ -1944,8 +1944,8 @@ def _perform(_playlist_idx, _page_token, _offset, _result): def _response_hook(self, **kwargs): response = kwargs['response'] - self._context.log_debug('API response: |{0.status_code}|\n' - 'headers: |{0.headers}|'.format(response)) + self._context.log_debug('API response: |{0.status_code}|' + '\n\theaders: |{0.headers}|'.format(response)) if response.status_code == 204 and 'no_content' in kwargs: return True try: @@ -2005,9 +2005,9 @@ def _error_hook(self, **kwargs): title, time_ms=timeout) - info = ('API error: {reason}\n' - 'exc: |{exc!r}|\n' - 'message: |{message}|') + info = ('API error: {reason}' + '\n\texc: |{exc!r}|' + '\n\tmessage: |{message}|') details = {'reason': reason, 'message': message} return '', info, details, data, False, exception @@ -2085,13 +2085,13 @@ def api_request(self, log_headers = None context = self._context - context.log_debug('API request:\n' - 'version: |{version}|\n' - 'method: |{method}|\n' - 'path: |{path}|\n' - 'params: |{params}|\n' - 'post_data: |{data}|\n' - 'headers: |{headers}|' + context.log_debug('API request:' + '\n\tversion: |{version}|' + '\n\tmethod: |{method}|' + '\n\tpath: |{path}|' + '\n\tparams: |{params}|' + '\n\tpost_data: |{data}|' + '\n\theaders: |{headers}|' .format(version=version, method=method, path=path, diff --git a/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py b/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py index d5c4e7473..876d6b43a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py +++ b/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py @@ -14,9 +14,9 @@ import re try: - from ....kodion import logger + from ....kodion.logger import Logger except: - class logger(object): + class Logger(object): @staticmethod def log_debug(txt): print(txt) @@ -259,17 +259,17 @@ def get_throttling_function_code(js): # This pattern is only present in the throttling function code. fiduciary_index = js.find('enhanced_except_') if fiduciary_index == -1: - logger.log_debug('ratebypass: fiduciary_index not found') + Logger.log_debug('ratebypass: fiduciary_index not found') return None start_index = js.rfind('=function(', 0, fiduciary_index) if start_index == -1: - logger.log_debug('ratebypass: function code start not found') + Logger.log_debug('ratebypass: function code start not found') return None end_index = js.find('};', fiduciary_index) if end_index == -1: - logger.log_debug('ratebypass: function code end not found') + Logger.log_debug('ratebypass: function code end not found') return None return js[start_index:end_index].replace('\n', '') @@ -294,7 +294,7 @@ def get_throttling_plan_gen(raw_code): plan_start_pattern = 'try{' plan_start_index = raw_code.find(plan_start_pattern) if plan_start_index == -1: - logger.log_debug('ratebypass: command block start not found') + Logger.log_debug('ratebypass: command block start not found') raise Exception() else: # Skip the whole start pattern, it's not needed. @@ -302,7 +302,7 @@ def get_throttling_plan_gen(raw_code): plan_end_index = raw_code.find('}', plan_start_index) if plan_end_index == -1: - logger.log_debug('ratebypass: command block end not found') + Logger.log_debug('ratebypass: command block end not found') raise Exception() plan_code = raw_code[plan_start_index:plan_end_index] @@ -365,14 +365,14 @@ def get_throttling_function_array(cls, mutable_n_list, raw_code): array_start_pattern = ",c=[" array_start_index = raw_code.find(array_start_pattern) if array_start_index == -1: - logger.log_debug('ratebypass: "c" array pattern not found') + Logger.log_debug('ratebypass: "c" array pattern not found') raise Exception() else: array_start_index += len(array_start_pattern) array_end_index = raw_code.rfind('];') if array_end_index == -1: - logger.log_debug('ratebypass: "c" array end not found') + Logger.log_debug('ratebypass: "c" array end not found') raise Exception() array_code = raw_code[array_start_index:array_end_index] @@ -404,7 +404,7 @@ def get_throttling_function_array(cls, mutable_n_list, raw_code): found = True break else: - logger.log_debug('ratebypass: mapping function not yet ' + Logger.log_debug('ratebypass: mapping function not yet ' 'listed: {unknown}'.format(unknown=el)) if found: continue @@ -428,7 +428,7 @@ def calculate_n(self, mutable_n_list): video stream URL. """ if self.calculated_n: - logger.log_debug('`n` already calculated: {calculated_n}. returning early...' + Logger.log_debug('`n` already calculated: {calculated_n}. returning early...' .format(calculated_n=self.calculated_n)) return self.calculated_n @@ -436,7 +436,7 @@ def calculate_n(self, mutable_n_list): return None initial_n_string = ''.join(mutable_n_list) - logger.log_debug('Attempting to calculate `n` from initial: {initial_n}' + Logger.log_debug('Attempting to calculate `n` from initial: {initial_n}' .format(initial_n=initial_n_string)) # For each step in the plan, get the first item of the step as the @@ -449,8 +449,8 @@ def calculate_n(self, mutable_n_list): for step in self.get_throttling_plan_gen(self.throttling_function_code): curr_func = throttling_array[int(step[0])] if not callable(curr_func): - logger.log_debug('{curr_func} is not callable.'.format(curr_func=curr_func)) - logger.log_debug('Throttling array:\n{throttling_array}\n' + Logger.log_debug('{curr_func} is not callable.'.format(curr_func=curr_func)) + Logger.log_debug('Throttling array:\n{throttling_array}\n' .format(throttling_array=throttling_array)) return None @@ -462,10 +462,10 @@ def calculate_n(self, mutable_n_list): second_arg = throttling_array[int(step[2])] curr_func(first_arg, second_arg) except: - logger.log_debug('Error calculating new `n`') + Logger.log_debug('Error calculating new `n`') return None self.calculated_n = ''.join(mutable_n_list) - logger.log_debug('Calculated `n`: {calculated_n}' + Logger.log_debug('Calculated `n`: {calculated_n}' .format(calculated_n=self.calculated_n)) return self.calculated_n diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 76b1831fe..9feec0d75 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -322,7 +322,8 @@ def get_videos(self, for yt_item in batch.get('items', []) if yt_item } - new_data = dict(dict.fromkeys(to_update, {}), **new_data) + new_data = dict(dict.fromkeys(to_update, {'_unavailable': True}), + **new_data) result.update(new_data) self.cache_data(new_data, defer=defer_cache) diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 71e25b5ad..408a6353e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -23,6 +23,7 @@ from ..client.request_client import YouTubeRequestClient from ..youtube_exceptions import InvalidJSON, YouTubeException from ...kodion.compatibility import ( + entity_escape, parse_qs, quote, unescape, @@ -662,8 +663,8 @@ class StreamInfo(YouTubeRequestClient): QUALITY_FACTOR = { # video - order based on comparative compression ratio 'av01': 1, + 'vp9.2': 0.75, 'vp9': 0.75, - 'vp09': 0.75, 'vp8': 0.55, 'vp08': 0.55, 'avc1': 0.5, @@ -766,18 +767,18 @@ def _error_hook(**kwargs): exception = None if not json_data or 'error' not in json_data: - info = ('exc: |{exc}|\n' - 'video_id: {video_id}, client: {client}, auth: {auth}') + info = ('exc: |{exc!r}|' + '\n\tvideo_id: {video_id}, client: {client}, auth: {auth}') return None, info, kwargs, data, None, exception details = json_data['error'] reason = details.get('errors', [{}])[0].get('reason', 'Unknown') message = details.get('message', 'Unknown error') - info = ('exc: |{exc}|\n' - 'reason: {reason}\n' - 'message: |{message}|\n' - 'video_id: {video_id}, client: {client}, auth: {auth}') + info = ('exc: |{exc!r}|' + '\n\treason: |{reason}|' + '\n\tmessage: |{message}|' + '\n\tvideo_id: {video_id}, client: {client}, auth: {auth}') kwargs['message'] = message kwargs['reason'] = reason return None, info, kwargs, data, None, exception @@ -975,13 +976,16 @@ def _get_player_js(self): return result @staticmethod - def _make_curl_headers(headers, cookies=None): + def _prepare_headers(headers, cookies=None, new_headers=None): + if cookies or new_headers: + headers = headers.copy() if cookies: headers['Cookie'] = '; '.join([ '='.join((cookie.name, cookie.value)) for cookie in cookies ]) - # Headers used in xbmc_items.video_playback_item' - return urlencode(headers, safe='/', quote_via=quote) + if new_headers: + headers.update(new_headers) + return headers @staticmethod def _normalize_url(url): @@ -1014,7 +1018,7 @@ def _update_from_hls(self, client_name = 'web' client_data = {'json': {'videoId': self.video_id}} headers = self.build_client(client_name, client_data)['headers'] - curl_headers = self._make_curl_headers(headers, cookies=None) + curl_headers = self._prepare_headers(headers) if meta_info is None: meta_info = {'video': {}, @@ -1025,12 +1029,14 @@ def _update_from_hls(self, if playback_stats is None: playback_stats = {} - settings = self._context.get_settings() + context = self._context + settings = context.get_settings() if self._use_mpd: qualities = settings.mpd_video_qualities() selected_height = qualities[0]['nom_height'] else: selected_height = settings.fixed_video_quality() + log_debug = context.log_debug for url in urls: result = self.request( @@ -1083,9 +1089,9 @@ def _update_from_hls(self, playback_stats=playback_stats, ) if yt_format is None: - self._context.log_debug('Unknown itag: {itag}\n{stream}' - .format(itag=itag, - stream=redact_ip(match[0]))) + stream_info = redact_ip(match.group(1)) + log_debug('Unknown itag: {itag}\n{stream}' + .format(itag=itag, stream=stream_info)) if (not yt_format or (yt_format.get('hls/video') and not yt_format.get('hls/audio'))): @@ -1112,7 +1118,7 @@ def _update_from_streams(self, client_name = 'web' client_data = {'json': {'videoId': self.video_id}} headers = self.build_client(client_name, client_data)['headers'] - curl_headers = self._make_curl_headers(headers, cookies=None) + curl_headers = self._prepare_headers(headers) if meta_info is None: meta_info = {'video': {}, @@ -1123,12 +1129,14 @@ def _update_from_streams(self, if playback_stats is None: playback_stats = {} - settings = self._context.get_settings() + context = self._context + settings = context.get_settings() if self._use_mpd: qualities = settings.mpd_video_qualities() selected_height = qualities[0]['nom_height'] else: selected_height = settings.fixed_video_quality() + log_debug = context.log_debug for stream_map in streams: itag = str(stream_map['itag']) @@ -1167,9 +1175,8 @@ def _update_from_streams(self, stream_map['conn'] = redact_ip(conn) if stream: stream_map['stream'] = redact_ip(stream) - self._context.log_debug('Unknown itag: {itag}\n{stream}'.format( - itag=itag, stream=stream_map, - )) + log_debug('Unknown itag: {itag}\n{stream}' + .format(itag=itag, stream=stream_map)) if (not yt_format or (yt_format.get('dash/video') and not yt_format.get('dash/audio'))): @@ -1225,8 +1232,9 @@ def _process_signature_cipher(self, stream_map): signature = self._cipher.get_signature(encrypted_signature) except Exception as exc: self._context.log_error('VideoInfo._process_signature_cipher - ' - 'failed to extract URL from |{sig}|\n' - '{exc}:\n{details}'.format( + 'failed to extract URL from |{sig}|' + '\n\texc: |{exc!r}|' + '\n\tdetails: |{details}|'.format( sig=encrypted_signature, exc=exc, details=''.join(format_stack()) @@ -1336,7 +1344,8 @@ def _get_error_details(self, playability_status, details=None): def load_stream_info(self, video_id): self.video_id = video_id - settings = self._context.get_settings() + context = self._context + settings = context.get_settings() age_gate_enabled = settings.age_gate() audio_only = self._audio_only ask_for_quality = self._ask_for_quality @@ -1357,6 +1366,9 @@ def load_stream_info(self, video_id): video_info_url = 'https://www.youtube.com/youtubei/v1/player' + log_debug = context.log_debug + log_warning = context.log_warning + abort_reasons = { 'country', 'not available', @@ -1436,7 +1448,7 @@ def load_stream_info(self, video_id): 'ERROR', 'UNPLAYABLE', }: - self._context.log_warning( + log_warning( 'Failed to retrieve video info - ' 'video_id: {0}, client: {1}, auth: {2},\n' 'status: {3}, reason: {4}'.format( @@ -1456,7 +1468,7 @@ def load_stream_info(self, video_id): abort = True break else: - self._context.log_debug( + log_debug( 'Unknown playabilityStatus in player response:\n|{0}|' .format(playability) ) @@ -1465,7 +1477,7 @@ def load_stream_info(self, video_id): break if status == 'OK': - self._context.log_debug( + log_debug( 'Retrieved video info - ' 'video_id: {0}, client: {1}, auth: {2}'.format( video_id, @@ -1515,7 +1527,7 @@ def load_stream_info(self, video_id): # the stream during playback. The YT player doesn't seem to use any # cookies when doing that, so for now cookies are ignored. # curl_headers = self._make_curl_headers(headers, cookies) - curl_headers = self._make_curl_headers(client['headers'], cookies=None) + curl_headers = self._prepare_headers(client['headers']) microformat = (result.get('microformat', {}) .get('playerMicroformatRenderer', {})) @@ -1640,7 +1652,7 @@ def load_stream_info(self, video_id): playback_stats, ) - subtitles = Subtitles(self._context, video_id) + subtitles = Subtitles(context, video_id) query_subtitles = client.get('_query_subtitles') if (not is_live or live_dvr) and ( query_subtitles is True @@ -1721,11 +1733,12 @@ def load_stream_info(self, video_id): elif default_lang['is_asr']: title.append(' [ASR]') + localize = context.localize for _prop in ('multi_lang', 'multi_audio'): if not main_stream.get(_prop): continue _prop = 'stream.' + _prop - title.extend((' [', self._context.localize(_prop), ']')) + title.extend((' [', localize(_prop), ']')) if len(title) > 1: yt_format['title'] = ''.join(yt_format['title']) @@ -1751,11 +1764,12 @@ def load_stream_info(self, video_id): return stream_list.values() def _process_stream_data(self, stream_data, default_lang_code='und'): - _settings = self._context.get_settings() + context = self._context + settings = context.get_settings() audio_only = self._audio_only - qualities = _settings.mpd_video_qualities() - isa_capabilities = self._context.inputstream_adaptive_capabilities() - stream_features = _settings.stream_features() + qualities = settings.mpd_video_qualities() + isa_capabilities = context.inputstream_adaptive_capabilities() + stream_features = settings.stream_features() allow_hdr = 'hdr' in stream_features allow_hfr = 'hfr' in stream_features disable_hfr_max = 'no_hfr_max' in stream_features @@ -1763,7 +1777,8 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): fps_map = (self.INTEGER_FPS_SCALE if 'no_frac_fr_hint' in stream_features else self.FRACTIONAL_FPS_SCALE) - stream_select = _settings.stream_select() + stream_select = settings.stream_select() + localize = context.localize audio_data = {} video_data = {} @@ -1799,8 +1814,10 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): codec = re.match(r'codecs="([a-z0-9]+([.\-][0-9](?="))?)', codecs) if codec: codec = codec.group(1) - if codec.startswith(('vp9', 'vp09')): + if codec.startswith('vp9'): codec = 'vp9' + elif codec.startswith('vp09'): + codec = 'vp9.2' elif codec.startswith('dts'): codec = 'dts' if codec not in isa_capabilities: @@ -1829,18 +1846,18 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): if role_type == 4 or audio_track.get('audioIsDefault'): role = 'main' - label = self._context.localize('stream.original') + label = localize('stream.original') elif role_type == 3: role = 'dub' - label = self._context.localize('stream.dubbed') + label = localize('stream.dubbed') elif role_type == 2: role = 'description' - label = self._context.localize('stream.descriptive') + label = localize('stream.descriptive') # Unsure of what other audio types are actually available # Role set to "alternate" as default fallback else: role = 'alternate' - label = self._context.localize('stream.alternate') + label = localize('stream.alternate') mime_group = ''.join(( mime_type, '_', language_code, '.', role_str, @@ -1860,12 +1877,12 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): role = 'main' role_type = 4 role_str = '4' - label = self._context.localize('stream.original') + label = localize('stream.original') mime_group = mime_type sample_rate = int(stream.get('audioSampleRate', '0'), 10) height = width = fps = frame_rate = hdr = None - language = self._context.get_language_name(language_code) + language = context.get_language_name(language_code) label = '{0} ({1} kbps)'.format(label, bitrate // 1000) if channels > 2 or 'auto' not in stream_select: quality_group = ''.join(( @@ -1886,7 +1903,8 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): if fps > 30 and not allow_hfr: continue - hdr = 'HDR' in stream.get('qualityLabel', '') + hdr = ('colorInfo' in stream + or 'HDR' in stream.get('qualityLabel', '')) if hdr and not allow_hdr: continue @@ -1945,14 +1963,10 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): url = unquote(url) primary_url, secondary_url = self._process_url_params(url) - primary_url = (primary_url.replace("&", "&") - .replace('"', """) - .replace("<", "<") - .replace(">", ">")) details = { 'mimeType': mime_type, - 'baseUrl': primary_url, + 'baseUrl': entity_escape(primary_url), 'mediaType': media_type, 'container': container, 'codecs': codecs, @@ -1979,15 +1993,11 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): 'channels': channels, } if secondary_url: - secondary_url = (secondary_url.replace("&", "&") - .replace('"', """) - .replace("<", "<") - .replace(">", ">")) - details['baseUrlSecondary'] = secondary_url + details['baseUrlSecondary'] = entity_escape(secondary_url) data[mime_group][itag] = data[quality_group][itag] = details if not video_data and not audio_only: - self._context.log_debug('Generate MPD: No video mime-types found') + context.log_debug('Generate MPD: No video mime-types found') return None, None def _stream_sort(stream): @@ -2042,9 +2052,12 @@ def _generate_mpd_manifest(self, if not video_data or not audio_data: return None, None + context = self._context + log_error = context.log_error + if not self.BASE_PATH: - self._context.log_error('VideoInfo._generate_mpd_manifest - ' - 'unable to access temp directory') + log_error('VideoInfo._generate_mpd_manifest - ' + 'unable to access temp directory') return None, None def _filter_group(previous_group, previous_stream, item): @@ -2091,11 +2104,12 @@ def _filter_group(previous_group, previous_stream, item): ) return skip_group - _settings = self._context.get_settings() - stream_features = _settings.stream_features() + settings = context.get_settings() + stream_features = settings.stream_features() do_filter = 'filter' in stream_features frame_rate_hint = 'no_fr_hint' not in stream_features - stream_select = _settings.stream_select() + stream_select = settings.stream_select() + localize = context.localize main_stream = { 'audio': audio_data[0][1][0], @@ -2142,7 +2156,7 @@ def _filter_group(previous_group, previous_stream, item): if group.startswith(mime_type) and 'auto' in stream_select: label = '{0} [{1}]'.format( stream['langName'] - or self._context.localize('stream.automatic'), + or localize('stream.automatic'), stream['label'] ) if stream == main_stream[media_type]: @@ -2198,11 +2212,7 @@ def _filter_group(previous_group, previous_stream, item): )) if license_url: - license_url = (license_url - .replace("&", "&") - .replace('"', """) - .replace("<", "<") - .replace(">", ">")) + license_url = entity_escape(license_url) output.extend(( '\t\t\t\n' '\t\t\t\n' ).format( - quality=(idx + 1), priority=(num_streams - idx), **stream - ) for idx, stream in enumerate(streams))) + quality=(idx + 1), + priority=(num_streams - idx), + **stream + ) for idx, stream in enumerate(streams)]) elif media_type == 'video': - output.extend((( + output.extend([( '\t\t\t\n' '\t\t\t\n' ).format( - quality=(idx + 1), priority=(num_streams - idx), **stream - ) for idx, stream in enumerate(streams))) + quality=(idx + 1), + priority=(num_streams - idx), + **stream + ) for idx, stream in enumerate(streams)]) output.append('\t\t\n') set_id += 1 if subs_data: - translation_lang = self._context.localize('subtitles.translation') + translation_lang = localize('subtitles.translation') for lang_id, subtitle in subs_data.items(): lang_code = subtitle['lang'] label = language = subtitle['language'] @@ -2287,11 +2301,7 @@ def _filter_group(previous_group, previous_stream, item): else: kind = lang_id - url = (unquote(subtitle['url']) - .replace("&", "&") - .replace('"', """) - .replace("<", "<") - .replace(">", ">")) + url = entity_escape(unquote(subtitle['url'])) output.extend(( '\t\t + + * VIDEO_ID: YouTube Video ID + + Playlist: + plugin://plugin.video.youtube/play/?playlist_id=[&order=][&action=] + + * PLAYLIST_ID: YouTube Playlist ID + * ORDER: [ask(default)|normal|reverse|shuffle] optional playlist order + * ACTION: [list|play|queue|None(default)] optional action to perform + + Channel live streams: + plugin://plugin.video.youtube/play/?channel_id=[&live=X] + + * CHANNEL_ID: YouTube Channel ID + * X: optional index of live stream to play if channel has multiple live streams. 1 (default) for first live stream + """ ui = context.get_ui() params = context.get_params() @@ -345,7 +371,7 @@ def process(provider, context, **_kwargs): if context.get_handle() == -1: context.execute('PlayMedia({0})'.format( - context.create_uri(('play',), params) + context.create_uri((PATHS.PLAY,), params) )) return False diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 52dda63e8..c19958e38 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -224,7 +224,9 @@ def _display_channels(channel_ids): channel_id_dict = {} for channel_id in channel_ids: channel_item = DirectoryItem( - '', context.create_uri(('channel', channel_id,), item_params) + name='', + uri=context.create_uri(('channel', channel_id,), item_params), + channel_id=channel_id, ) channel_id_dict[channel_id] = channel_item @@ -249,7 +251,9 @@ def _display_playlists(playlist_ids): playlist_id_dict = {} for playlist_id in playlist_ids: playlist_item = DirectoryItem( - '', context.create_uri(('playlist', playlist_id,), item_params) + name='', + uri=context.create_uri(('playlist', playlist_id,), item_params), + playlist_id=playlist_id, ) playlist_id_dict[playlist_id] = playlist_item diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 332c050ea..3cfc5c987 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -40,6 +40,7 @@ PATHS, ) from ..kodion.items import ( + BaseItem, DirectoryItem, NewSearchItem, SearchItem, @@ -133,8 +134,8 @@ def get_dev_config(context, addon_id, dev_configs): dev_main = None if not dev_main: - context.log_error('Invalid developer config: |{dev_config}|\n' - 'expected: |{{' + context.log_error('Invalid developer config: |{dev_config}|' + '\n\texpected: |{{' ' "origin": ADDON_ID,' ' "main": {{' ' "system": SYSTEM_NAME,' @@ -260,7 +261,12 @@ def get_client(self, context): if not value: continue - token, expiry = client.refresh_token(token_type, value) + json_data = client.refresh_token(token_type, value) + if not json_data: + continue + + token = json_data.get('access_token') + expiry = int(json_data.get('expires_in', 3600)) if token and expiry > 0: access_tokens[token_type] = token if not token_expiry or expiry < token_expiry: @@ -341,21 +347,24 @@ def on_uri2addon(provider, context, uri=None, **_kwargs): return False - """ - Lists the videos of a playlist. - path : '/channel/(?P[^/]+)/playlist/(?P[^/]+)/' - or - path : '/playlist/(?P[^/]+)/' - channel_id : ['mine'|] - playlist_id: - """ - @AbstractProvider.register_path( r'^(?:/channel/(?P[^/]+))?' r'/playlist/(?P[^/]+)/?$' ) @staticmethod def on_playlist(provider, context, re_match): + """ + Lists the videos of a playlist. + + plugin://plugin.video.youtube/channel//playlist/ + + or + + plugin://plugin.video.youtube/playlist/ + + * CHANNEL_ID: ['mine'|YouTube Channel ID] + * PLAYLIST_ID: YouTube Playlist ID + """ context.set_content(CONTENT.VIDEO_CONTENT) resource_manager = provider.get_resource_manager(context) @@ -368,17 +377,18 @@ def on_playlist(provider, context, re_match): result = v3.response_to_items(provider, context, json_data[batch_id]) return result - """ - Lists all playlists of a channel. - path : '/channel/(?P[^/]+)/playlists/' - channel_id: - """ - @AbstractProvider.register_path( r'^/channel/(?P[^/]+)' r'/playlists/?$') @staticmethod def on_channel_playlists(provider, context, re_match): + """ + Lists all playlists of a channel. + + plugin://plugin.video.youtube/channel//playlists/ + + * CHANNEL_ID: YouTube Channel ID + """ context.set_content(CONTENT.LIST_CONTENT) channel_id = re_match.group('channel_id') @@ -400,19 +410,57 @@ def on_channel_playlists(provider, context, re_match): ).get(channel_id) playlists = resource_manager.get_related_playlists(channel_id) - uploads = playlists.get('uploads') - if uploads: + playlist_id = playlists.get('uploads') + if playlist_id: item_label = context.localize('uploads') uploads = DirectoryItem( context.get_ui().bold(item_label), context.create_uri( - ('channel', channel_id, 'playlist', uploads), + ('channel', channel_id, 'playlist', playlist_id), new_params, ), image='{media}/playlist.png', fanart=fanart, category_label=item_label, + channel_id=channel_id, + playlist_id=playlist_id, ) + + context_menu = [ + menu_items.play_playlist( + context, playlist_id + ), + menu_items.view_playlist( + context, playlist_id + ), + menu_items.shuffle_playlist( + context, playlist_id + ), + menu_items.separator(), + menu_items.bookmark_add( + context, uploads + ) if channel_id != 'mine' else None, + ] + + if channel_id != 'mine': + if provider.is_logged_in: + # subscribe to the channel via the playlist item + context_menu.append( + menu_items.subscribe_to_channel( + context, channel_id, + ) + ) + context_menu.append( + # bookmark channel of the playlist + menu_items.bookmark_add_channel( + context, channel_id, + ) + ) + + if context_menu: + context_menu.append(menu_items.separator()) + uploads.add_context_menu(context_menu) + result = [uploads] else: result = False @@ -428,17 +476,18 @@ def on_channel_playlists(provider, context, re_match): result.extend(v3.response_to_items(provider, context, json_data)) return result - """ - List live streams for channel. - path : '/channel/(?P[^/]+)/live/' - channel_id: - """ - @AbstractProvider.register_path( r'^/channel/(?P[^/]+)' r'/live/?$') @staticmethod def on_channel_live(provider, context, re_match): + """ + List live streams for channel. + + plugin://plugin.video.youtube/channel//live + + * CHANNEL_ID: YouTube Channel ID + """ context.set_content(CONTENT.VIDEO_CONTENT) result = [] @@ -471,17 +520,19 @@ def on_channel_live(provider, context, re_match): return result - """ - Lists a playlist folder and all uploaded videos of a channel. - path :'/channel|handle|user/(?P)[^/]+/' - channel_id: - """ - @AbstractProvider.register_path( r'^/(?P(channel|handle|user))' r'/(?P[^/]+)/?$') @staticmethod def on_channel(provider, context, re_match): + """ + Lists a playlist folder and all uploaded videos of a channel. + + plugin://plugin.video.youtube// + + * ID_TYPE: channel|handle|user + * ID: YouTube ID + """ listitem_channel_id = context.get_listitem_property(CHANNEL_ID) client = provider.get_client(context) @@ -583,6 +634,7 @@ def on_channel(provider, context, re_match): image='{media}/playlist.png', fanart=fanart, category_label=item_label, + channel_id=channel_id, ) result.append(playlists_item) @@ -608,6 +660,7 @@ def on_channel(provider, context, re_match): image='{media}/live.png', fanart=fanart, category_label=item_label, + channel_id=channel_id, ) result.append(live_item) @@ -688,19 +741,6 @@ def on_my_location(context, **_kwargs): return result - """ - Plays a video, playlist, or channel live stream. - Video: '/play/?video_id=XXXXXX' - - Playlist: '/play/?playlist_id=XXXXXX[&order=ORDER][&action=ACTION]' - ORDER: [normal(default)|reverse|shuffle] optional playlist ordering - ACTION: [list|play|queue|None(default)] optional action to perform - - Channel live streams: '/play/?channel_id=UCXXXXXX[&live=X] - X: optional index of live stream to play if channel has multiple live - streams. 1 (default) for first live stream - """ - @AbstractProvider.register_path('^/users/(?P[^/]+)/?$') @staticmethod def on_users(re_match, **_kwargs): @@ -1218,9 +1258,15 @@ def on_root(provider, context, re_match): image='{media}/watch_later.png', ) context_menu = [ - menu_items.play_all_from_playlist( + menu_items.play_playlist( context, watch_later_id - ) + ), + menu_items.view_playlist( + context, watch_later_id + ), + menu_items.shuffle_playlist( + context, watch_later_id + ), ] watch_later_item.add_context_menu(context_menu) result.append(watch_later_item) @@ -1237,15 +1283,22 @@ def on_root(provider, context, re_match): resource_manager = provider.get_resource_manager(context) playlists = resource_manager.get_related_playlists('mine') if playlists and 'likes' in playlists: + liked_list_id = playlists['likes'] liked_videos_item = DirectoryItem( localize('video.liked'), - create_uri(('channel', 'mine', 'playlist', playlists['likes'])), + create_uri(('channel', 'mine', 'playlist', liked_list_id)), image='{media}/likes.png', ) context_menu = [ - menu_items.play_all_from_playlist( - context, playlists['likes'] - ) + menu_items.play_playlist( + context, liked_list_id + ), + menu_items.view_playlist( + context, liked_list_id + ), + menu_items.shuffle_playlist( + context, liked_list_id + ), ] liked_videos_item.add_context_menu(context_menu) result.append(liked_videos_item) @@ -1268,9 +1321,15 @@ def on_root(provider, context, re_match): image='{media}/history.png', ) context_menu = [ - menu_items.play_all_from_playlist( + menu_items.play_playlist( context, history_id - ) + ), + menu_items.view_playlist( + context, history_id + ), + menu_items.shuffle_playlist( + context, history_id + ), ] watch_history_item.add_context_menu(context_menu) result.append(watch_history_item) @@ -1414,35 +1473,80 @@ def on_bookmarks(provider, context, re_match): 'items': [] } - def _update_bookmark(_id, timestamp): + def _update_bookmark(context, _id, old_item): def _update(new_item): - new_item.set_bookmark_timestamp(timestamp) - bookmarks_list.update_item(_id, repr(new_item), timestamp) + if isinstance(old_item, float): + bookmark_timestamp = old_item + elif isinstance(old_item, BaseItem): + bookmark_timestamp = old_item.get_bookmark_timestamp() + else: + return + + if new_item.available: + new_item.bookmark_id = _id + new_item.set_bookmark_timestamp(bookmark_timestamp) + new_item.callback = None + bookmarks_list.update_item( + _id, + repr(new_item), + bookmark_timestamp, + ) + else: + new_item.__dict__.update(old_item.__dict__) + new_item.bookmark_id = _id + new_item.set_bookmark_timestamp(bookmark_timestamp) + new_item.available = False + new_item.playable = False + new_item.set_title(context.get_ui().color( + 'AA808080', new_item.get_title() + )) return _update for item_id, item in items.items(): + callback = _update_bookmark(context, item_id, item) if isinstance(item, float): kind = 'youtube#channel' yt_id = item_id - callback = _update_bookmark(item_id, item) partial = True - else: - callback = None + elif isinstance(item, BaseItem): partial = False + if isinstance(item, VideoItem): kind = 'youtube#video' yt_id = item.video_id else: - yt_id = item.playlist_id + yt_id = getattr(item, 'playlist_id', None) if yt_id: kind = 'youtube#playlist' else: kind = 'youtube#channel' - yt_id = item.channel_id + yt_id = getattr(item, 'channel_id', None) + else: + kind = None + yt_id = None + partial = False if not yt_id: - continue + if isinstance(item, BaseItem): + item_ids = item.parse_item_ids_from_uri() + to_delete = False + for kind in ('video', 'playlist', 'channel'): + yt_id = item_ids.get(kind + '_id') + if not yt_id: + continue + if yt_id == 'None': + to_delete = True + continue + kind = 'youtube#' + kind + partial = True + break + else: + if to_delete: + bookmarks_list.del_item(item_id) + continue + else: + continue item = { 'kind': kind, diff --git a/resources/settings.xml b/resources/settings.xml index 3d27f3270..d11565eb7 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -593,7 +593,7 @@ - + 0 false @@ -647,7 +647,7 @@ - + 0 true @@ -721,7 +721,7 @@ - + 0 true @@ -757,7 +757,7 @@ - + 0 true @@ -857,7 +857,7 @@ - + 0 @@ -917,7 +917,7 @@ - + 0 90 @@ -941,7 +941,7 @@ - + 0 true @@ -1057,7 +1057,7 @@ - + 0 0.0.0.0 @@ -1110,6 +1110,13 @@ + + + 0 + false + + +