diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml
index 76e1c82ef..8e8d45bab 100644
--- a/.github/workflows/make-release.yml
+++ b/.github/workflows/make-release.yml
@@ -20,7 +20,7 @@ jobs:
id: release
run: |
version=${GITHUB_REF/refs\/tags\//}
- if [[ $version == *-dev ]] ;
+ if [[ $version == *[-+]@(alpha|beta|dev)*([.0-9a-z]) ]] ;
then
echo "pre-release=true" >> $GITHUB_OUTPUT
else
@@ -64,11 +64,14 @@ jobs:
- name: Create Zip (Nexus-Unofficial)
id: zip-unofficial-nexus
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git reset
git checkout .
+ git fetch origin nexus-unofficial
+ git cherry-pick ...origin/nexus-unofficial -n
git clean -fdx
- git apply .patches/unofficial.patch
mv .git ..
rm -rf .??*
rm *.md
@@ -104,11 +107,14 @@ jobs:
- name: Create Zip (Matrix-Unofficial)
id: zip-unofficial-matrix
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git reset
git checkout .
+ git fetch origin nexus-unofficial
+ git cherry-pick ...origin/nexus-unofficial -n
git clean -fdx
- git apply .patches/unofficial.patch
mv .git ..
rm -rf .??*
rm *.md
diff --git a/.github/workflows/submit-release.yml b/.github/workflows/submit-release.yml
index 5057f6dd3..320950dc0 100644
--- a/.github/workflows/submit-release.yml
+++ b/.github/workflows/submit-release.yml
@@ -74,7 +74,7 @@ jobs:
version=$(xmlstarlet sel -t -v 'string(/addon/@version)' addon.xml)
xmlstarlet ed -L -u '/addon/@version' -v "${version}+matrix.1" addon.xml
xmlstarlet ed -L -u '/addon/requires/import[@addon="xbmc.python"]/@version' -v '3.0.0' addon.xml
- xmlstarlet ed -L -u '/addon/requires/import[@addon="inputstream.adaptive"]/@version' -v '19.0.7' addon.xml
+ xmlstarlet ed -L -u '/addon/requires/import[@addon="inputstream.adaptive"]/@version' -v '19.0.0' addon.xml
xmlstarlet ed -L -d '/addon/requires/import[@addon="script.module.infotagger"]' addon.xml
git add .
git commit -m "Kodi 19 Patch"
diff --git a/.patches/unofficial.patch b/.patches/unofficial.patch
deleted file mode 100644
index 32e255cce..000000000
--- a/.patches/unofficial.patch
+++ /dev/null
@@ -1,615 +0,0 @@
- addon.xml | 1 +
- .../kodion/abstract_provider.py | 21 ++-
- .../kodion/constants/const_settings.py | 4 +
- .../kodion/impl/abstract_context_ui.py | 6 +
- .../kodion/impl/abstract_settings.py | 3 +
- .../kodion/impl/xbmc/xbmc_context.py | 1 +
- .../kodion/impl/xbmc/xbmc_context_ui.py | 12 ++
- .../kodion/impl/xbmc/xbmc_runner.py | 8 +
- .../youtube_plugin/kodion/utils/__init__.py | 3 +-
- .../kodion/utils/view_manager.py | 166 ++++++++++++++++++
- .../youtube_plugin/youtube/helper/utils.py | 2 +-
- .../youtube/helper/yt_specials.py | 18 +-
- .../lib/youtube_plugin/youtube/provider.py | 22 +--
- resources/settings.xml | 33 +++-
- 14 files changed, 276 insertions(+), 24 deletions(-)
- create mode 100644 resources/lib/youtube_plugin/kodion/utils/view_manager.py
-
-diff --git a/addon.xml b/addon.xml
-index 57d0b3c6..13eba70c 100644
---- a/addon.xml
-+++ b/addon.xml
-@@ -107,3 +107,4 @@
- 此附加元件未由Google支持
-
-
-+
-diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py
-index ff8ffd44..1306ca2c 100644
---- a/resources/lib/youtube_plugin/kodion/abstract_provider.py
-+++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py
-@@ -72,10 +72,29 @@ class AbstractProvider(object):
- self._dict_path[re_path] = method_name
-
- def _process_wizard(self, context):
-+ def _setup_views(_context, _view):
-+ view_manager = utils.ViewManager(_context)
-+ if not view_manager.update_view_mode(_context.localize('setup_wizard.view.%s' % _view]),
-+ _view):
-+ return
-+
-+ _context.get_settings().set_bool(constants.setting.VIEW_OVERRIDE, True)
-+
- # start the setup wizard
- wizard_steps = []
- if context.get_settings().is_setup_wizard_enabled():
- context.get_settings().set_bool(constants.setting.SETUP_WIZARD, False)
-+ if utils.ViewManager(context).has_supported_views():
-+ views = self.get_wizard_supported_views()
-+ for view in views:
-+ if view in utils.ViewManager.SUPPORTED_VIEWS:
-+ wizard_steps.append((_setup_views, [context, view]))
-+ else:
-+ context.log_warning('[Setup-Wizard] Unsupported view "%s"' % view)
-+ else:
-+ skin_id = context.get_ui().get_skin_id()
-+ context.log("ViewManager: Unknown skin id '%s'" % skin_id)
-+
- wizard_steps.extend(self.get_wizard_steps(context))
-
- if wizard_steps and context.get_ui().on_yes_no_input(context.get_name(),
-@@ -278,7 +297,7 @@ class AbstractProvider(object):
- query = query.decode('utf-8')
- return self.on_search(query, context, re_match)
- else:
-- context.set_content_type(constants.content_type.FILES)
-+ context.set_content_type(constants.content_type.VIDEOS)
- result = []
-
- location = str(context.get_param('location', False)).lower() == 'true'
-diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py
-index 6f64ca46..a751a1c1 100644
---- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py
-+++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py
-@@ -31,6 +31,10 @@ HIDE_SHORT_VIDEOS = 'youtube.hide_shorts' # (bool)
- SUPPORT_ALTERNATIVE_PLAYER = 'kodion.support.alternative_player' # (bool)
- ALTERNATIVE_PLAYER_WEB_URLS = 'kodion.alternative_player.web.urls' # (bool)
-
-+VIEW_OVERRIDE = 'kodion.view.override' # (bool)
-+VIEW_DEFAULT = 'kodion.view.default' # (int)
-+VIEW_X = 'kodion.view.%s' # (int)
-+
- ALLOW_DEV_KEYS = 'youtube.allow.dev.keys' # (bool)
-
- VIDEO_QUALITY = 'kodion.video.quality' # (int)
-diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py b/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py
-index 7ed3feff..46eabf26 100644
---- a/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py
-+++ b/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py
-@@ -16,6 +16,12 @@ class AbstractContextUI(object):
- def create_progress_dialog(self, heading, text=None, background=False):
- raise NotImplementedError()
-
-+ def set_view_mode(self, view_mode):
-+ raise NotImplementedError()
-+
-+ def get_view_mode(self):
-+ raise NotImplementedError()
-+
- def get_skin_id(self):
- raise NotImplementedError()
-
-diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py
-index 47ec7ed2..4a314d65 100644
---- a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py
-+++ b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py
-@@ -90,6 +90,9 @@ class AbstractSettings(object):
- def is_setup_wizard_enabled(self):
- return self.get_bool(SETTINGS.SETUP_WIZARD, False)
-
-+ def is_override_view_enabled(self):
-+ return self.get_bool(SETTINGS.VIEW_OVERRIDE, False)
-+
- def is_support_alternative_player_enabled(self):
- return self.get_bool(SETTINGS.SUPPORT_ALTERNATIVE_PLAYER, False)
-
-diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py
-index 03b9cee7..6704d974 100644
---- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py
-+++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py
-@@ -215,6 +215,7 @@ class XbmcContext(AbstractContext):
- def set_content_type(self, content_type):
- self.log_debug('Setting content-type: "%s" for "%s"' % (content_type, self.get_path()))
- xbmcplugin.setContent(self._plugin_handle, content_type)
-+ self.get_ui().set_view_mode(content_type)
-
- def add_sort_method(self, *sort_methods):
- for sort_method in sort_methods:
-diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py
-index 147d4e5e..b83f101b 100644
---- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py
-+++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py
-@@ -33,6 +33,18 @@ class XbmcContextUI(AbstractContextUI):
-
- return XbmcProgressDialog(heading, text)
-
-+ def set_view_mode(self, view_mode):
-+ if isinstance(view_mode, str):
-+ view_mode = self._context.get_settings().get_int(constants.setting.VIEW_X % view_mode, self._context.get_settings().get_int(constants.setting.VIEW_DEFAULT, 50))
-+
-+ self._view_mode = view_mode
-+
-+ def get_view_mode(self):
-+ if self._view_mode is not None:
-+ return self._view_mode
-+
-+ return self._context.get_settings().get_int(constants.setting.VIEW_DEFAULT, 50)
-+
- def get_skin_id(self):
- return xbmc.getSkinDir()
-
-diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py
-index 46fda8a7..1f35935a 100644
---- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py
-+++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py
-@@ -8,6 +8,7 @@
- See LICENSES/GPL-2.0-only for more information.
- """
-
-+import xbmc
- import xbmcgui
- import xbmcplugin
-
-@@ -66,6 +67,13 @@ class XbmcRunner(AbstractProviderRunner):
- self.handle, succeeded=True,
- updateListing=options.get(AbstractProvider.RESULT_UPDATE_LISTING, False),
- cacheToDisc=options.get(AbstractProvider.RESULT_CACHE_TO_DISC, True))
-+
-+ # set alternative view mode
-+ if context.get_settings().is_override_view_enabled():
-+ view_mode = context.get_ui().get_view_mode()
-+ if view_mode is not None:
-+ context.log_debug('Override view mode to "%d"' % view_mode)
-+ xbmc.executebuiltin('Container.SetViewMode(%d)' % view_mode)
- else:
- # handle exception
- pass
-diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py
-index 717fb4dc..da0c45b1 100644
---- a/resources/lib/youtube_plugin/kodion/utils/__init__.py
-+++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py
-@@ -15,6 +15,7 @@ from .favorite_list import FavoriteList
- from .watch_later_list import WatchLaterList
- from .function_cache import FunctionCache
- from .access_manager import AccessManager
-+from .view_manager import ViewManager
- from .http_server import get_http_server, is_httpd_live, get_client_ip_address
- from .monitor import YouTubeMonitor
- from .player import YouTubePlayer
-@@ -24,7 +25,7 @@ from .system_version import SystemVersion
- from . import ip_api
-
-
--__all__ = ['SearchHistory', 'FavoriteList', 'WatchLaterList', 'FunctionCache', 'AccessManager',
-+__all__ = ['SearchHistory', 'FavoriteList', 'WatchLaterList', 'FunctionCache', 'AccessManager', 'ViewManager',
- 'strip_html_from_text', 'create_path', 'create_uri_path', 'find_best_fit', 'to_unicode', 'to_utf8',
- 'datetime_parser', 'select_stream', 'get_http_server', 'is_httpd_live', 'YouTubeMonitor',
- 'make_dirs', 'loose_version', 'ip_api', 'PlaybackHistory', 'DataCache', 'get_client_ip_address',
-diff --git a/resources/lib/youtube_plugin/kodion/utils/view_manager.py b/resources/lib/youtube_plugin/kodion/utils/view_manager.py
-new file mode 100644
-index 00000000..9a5fe5ae
---- /dev/null
-+++ b/resources/lib/youtube_plugin/kodion/utils/view_manager.py
-@@ -0,0 +1,166 @@
-+# -*- coding: utf-8 -*-
-+"""
-+
-+ Copyright (C) 2014-2016 bromix (plugin.video.youtube)
-+ Copyright (C) 2016-2018 plugin.video.youtube
-+
-+ SPDX-License-Identifier: GPL-2.0-only
-+ See LICENSES/GPL-2.0-only for more information.
-+"""
-+
-+from .. import constants
-+
-+
-+class ViewManager(object):
-+ SUPPORTED_VIEWS = ['default', 'movies', 'tvshows', 'episodes', 'musicvideos', 'songs', 'albums', 'artists']
-+ SKIN_DATA = {
-+ 'skin.confluence': {
-+ 'default': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Big List', 'id': 51},
-+ {'name': 'Thumbnail', 'id': 500}
-+ ],
-+ 'movies': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Big List', 'id': 51},
-+ {'name': 'Thumbnail', 'id': 500},
-+ {'name': 'Media info', 'id': 504},
-+ {'name': 'Media info 2', 'id': 503}
-+ ],
-+ 'episodes': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Big List', 'id': 51},
-+ {'name': 'Thumbnail', 'id': 500},
-+ {'name': 'Media info', 'id': 504},
-+ {'name': 'Media info 2', 'id': 503}
-+ ],
-+ 'tvshows': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Big List', 'id': 51},
-+ {'name': 'Thumbnail', 'id': 500},
-+ {'name': 'Poster', 'id': 500},
-+ {'name': 'Wide', 'id': 505},
-+ {'name': 'Media info', 'id': 504},
-+ {'name': 'Media info 2', 'id': 503},
-+ {'name': 'Fanart', 'id': 508}
-+ ],
-+ 'musicvideos': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Big List', 'id': 51},
-+ {'name': 'Thumbnail', 'id': 500},
-+ {'name': 'Media info', 'id': 504},
-+ {'name': 'Media info 2', 'id': 503}
-+ ],
-+ 'songs': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Big List', 'id': 51},
-+ {'name': 'Thumbnail', 'id': 500},
-+ {'name': 'Media info', 'id': 506}
-+ ],
-+ 'albums': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Big List', 'id': 51},
-+ {'name': 'Thumbnail', 'id': 500},
-+ {'name': 'Media info', 'id': 506}
-+ ],
-+ 'artists': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Big List', 'id': 51},
-+ {'name': 'Thumbnail', 'id': 500},
-+ {'name': 'Media info', 'id': 506}
-+ ]
-+ },
-+ 'skin.aeon.nox.5': {
-+ 'default': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Episodes', 'id': 502},
-+ {'name': 'LowList', 'id': 501},
-+ {'name': 'BannerWall', 'id': 58},
-+ {'name': 'Shift', 'id': 57},
-+ {'name': 'Posters', 'id': 56},
-+ {'name': 'ShowCase', 'id': 53},
-+ {'name': 'Landscape', 'id': 52},
-+ {'name': 'InfoWall', 'id': 51}
-+ ]
-+ },
-+ 'skin.xperience1080+': {
-+ 'default': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Thumbnail', 'id': 500},
-+ ],
-+ 'episodes': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Info list', 'id': 52},
-+ {'name': 'Fanart', 'id': 502},
-+ {'name': 'Landscape', 'id': 54},
-+ {'name': 'Poster', 'id': 55},
-+ {'name': 'Thumbnail', 'id': 500},
-+ {'name': 'Banner', 'id': 60}
-+ ],
-+ },
-+ 'skin.xperience1080': {
-+ 'default': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Thumbnail', 'id': 500},
-+ ],
-+ 'episodes': [
-+ {'name': 'List', 'id': 50},
-+ {'name': 'Info list', 'id': 52},
-+ {'name': 'Fanart', 'id': 502},
-+ {'name': 'Landscape', 'id': 54},
-+ {'name': 'Poster', 'id': 55},
-+ {'name': 'Thumbnail', 'id': 500},
-+ {'name': 'Banner', 'id': 60}
-+ ],
-+ },
-+ 'skin.estuary': {
-+ 'default': [
-+ {'name': 'Shift', 'id': 53},
-+ {'name': 'InfoWall', 'id': 54},
-+ {'name': 'Wall', 'id': 500},
-+ {'name': 'WideList', 'id': 55},
-+ ],
-+ 'episodes': [
-+ {'name': 'InfoWall', 'id': 54},
-+ {'name': 'Wall', 'id': 500},
-+ {'name': 'WideList', 'id': 55},
-+ ]
-+ }
-+ }
-+
-+ def __init__(self, context):
-+ self._context = context
-+
-+ def has_supported_views(self):
-+ """
-+ Returns True if the View of the current skin are supported
-+ :return: True if the View of the current skin are supported
-+ """
-+ return self._context.get_ui().get_skin_id() in self.SKIN_DATA
-+
-+ def update_view_mode(self, title, view='default'):
-+ view_id = -1
-+ settings = self._context.get_settings()
-+
-+ skin_id = self._context.get_ui().get_skin_id()
-+ skin_data = self.SKIN_DATA.get(skin_id, {}).get(view, [])
-+ if skin_data:
-+ items = []
-+ for view_data in skin_data:
-+ items.append((view_data['name'], view_data['id']))
-+ view_id = self._context.get_ui().on_select(title, items)
-+ else:
-+ self._context.log_notice("ViewManager: Unknown skin id '%s'" % skin_id)
-+
-+ if view_id == -1:
-+ old_value = settings.get_string(constants.setting.VIEW_X % view, '')
-+ if old_value:
-+ result, view_id = self._context.get_ui().on_numeric_input(title, old_value)
-+ if not result:
-+ view_id = -1
-+
-+ if view_id > -1:
-+ settings.set_int(constants.setting.VIEW_X % view, view_id)
-+ return True
-+
-+ return False
-diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py
-index 734e0a12..784468b2 100644
---- a/resources/lib/youtube_plugin/youtube/helper/utils.py
-+++ b/resources/lib/youtube_plugin/youtube/helper/utils.py
-@@ -254,7 +254,7 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict=N
- video_item = video_id_dict[video_id]
-
- # set mediatype
-- video_item.set_mediatype('video') # using video
-+ video_item.set_mediatype('episode') # using video
-
- if not yt_item:
- continue
-diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py
-index 89ad2b11..c5c8fec3 100644
---- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py
-+++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py
-@@ -15,7 +15,7 @@ from . import utils
-
-
- def _process_related_videos(provider, context):
-- provider.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ provider.set_content_type(context, kodion.constants.content_type.EPISODES)
- result = []
-
- page_token = context.get_param('page_token', '')
-@@ -60,7 +60,7 @@ def _process_child_comments(provider, context):
-
-
- def _process_recommendations(provider, context):
-- provider.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ provider.set_content_type(context, kodion.constants.content_type.EPISODES)
- result = []
-
- page_token = context.get_param('page_token', '')
-@@ -72,7 +72,7 @@ def _process_recommendations(provider, context):
-
-
- def _process_popular_right_now(provider, context):
-- provider.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ provider.set_content_type(context, kodion.constants.content_type.EPISODES)
- result = []
-
- page_token = context.get_param('page_token', '')
-@@ -85,7 +85,7 @@ def _process_popular_right_now(provider, context):
-
-
- def _process_browse_channels(provider, context):
-- provider.set_content_type(context, kodion.constants.content_type.FILES)
-+ provider.set_content_type(context, kodion.constants.content_type.VIDEOS)
- result = []
-
- # page_token = context.get_param('page_token', '')
-@@ -107,7 +107,7 @@ def _process_browse_channels(provider, context):
-
-
- def _process_disliked_videos(provider, context):
-- provider.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ provider.set_content_type(context, kodion.constants.content_type.EPISODES)
- result = []
-
- page_token = context.get_param('page_token', '')
-@@ -122,7 +122,7 @@ def _process_live_events(provider, context, event_type='live'):
- def _sort(x):
- return x.get_aired()
-
-- provider.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ provider.set_content_type(context, kodion.constants.content_type.EPISODES)
- result = []
-
- # TODO: cache result
-@@ -142,7 +142,7 @@ def _process_description_links(provider, context):
- addon_id = context.get_param('addon_id', '')
-
- def _extract_urls(_video_id):
-- provider.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ provider.set_content_type(context, kodion.constants.content_type.EPISODES)
- url_resolver = UrlResolver(context)
-
- result = []
-@@ -304,7 +304,7 @@ def _process_purchases_tv(provider, context):
-
-
- def _process_new_uploaded_videos_tv(provider, context):
-- provider.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ provider.set_content_type(context, kodion.constants.content_type.EPISODES)
-
- result = []
- next_page_token = context.get_param('next_page_token', '')
-@@ -316,7 +316,7 @@ def _process_new_uploaded_videos_tv(provider, context):
-
-
- def _process_new_uploaded_videos_tv_filtered(provider, context):
-- provider.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ provider.set_content_type(context, kodion.constants.content_type.EPISODES)
-
- result = []
- next_page_token = context.get_param('next_page_token', '')
-diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py
-index bdab6658..2503e380 100644
---- a/resources/lib/youtube_plugin/youtube/provider.py
-+++ b/resources/lib/youtube_plugin/youtube/provider.py
-@@ -437,7 +437,7 @@ class Provider(kodion.AbstractProvider):
-
- @kodion.RegisterProviderPath('^/playlist/(?P[^/]+)/$')
- def _on_playlist(self, context, re_match):
-- self.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ self.set_content_type(context, kodion.constants.content_type.EPISODES)
-
- result = []
-
-@@ -461,7 +461,7 @@ class Provider(kodion.AbstractProvider):
-
- @kodion.RegisterProviderPath('^/channel/(?P[^/]+)/playlist/(?P[^/]+)/$')
- def _on_channel_playlist(self, context, re_match):
-- self.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ self.set_content_type(context, kodion.constants.content_type.EPISODES)
- client = self.get_client(context)
- result = []
-
-@@ -484,7 +484,7 @@ class Provider(kodion.AbstractProvider):
-
- @kodion.RegisterProviderPath('^/channel/(?P[^/]+)/playlists/$')
- def _on_channel_playlists(self, context, re_match):
-- self.set_content_type(context, kodion.constants.content_type.FILES)
-+ self.set_content_type(context, kodion.constants.content_type.VIDEOS)
- result = []
-
- channel_id = re_match.group('channel_id')
-@@ -525,7 +525,7 @@ class Provider(kodion.AbstractProvider):
-
- @kodion.RegisterProviderPath('^/channel/(?P[^/]+)/live/$')
- def _on_channel_live(self, context, re_match):
-- self.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ self.set_content_type(context, kodion.constants.content_type.EPISODES)
- result = []
-
- channel_id = re_match.group('channel_id')
-@@ -560,7 +560,7 @@ class Provider(kodion.AbstractProvider):
- if method == 'channel' and not channel_id:
- return False
-
-- self.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ self.set_content_type(context, kodion.constants.content_type.EPISODES)
-
- resource_manager = self.get_resource_manager(context)
-
-@@ -648,7 +648,7 @@ class Provider(kodion.AbstractProvider):
- # noinspection PyUnusedLocal
- @kodion.RegisterProviderPath('^/location/mine/$')
- def _on_my_location(self, context, re_match):
-- self.set_content_type(context, kodion.constants.content_type.FILES)
-+ self.set_content_type(context, kodion.constants.content_type.VIDEOS)
-
- settings = context.get_settings()
- result = list()
-@@ -793,7 +793,7 @@ class Provider(kodion.AbstractProvider):
- subscriptions = yt_subscriptions.process(method, self, context)
-
- if method == 'list':
-- self.set_content_type(context, kodion.constants.content_type.FILES)
-+ self.set_content_type(context, kodion.constants.content_type.VIDEOS)
- channel_ids = []
- for subscription in subscriptions:
- channel_ids.append(subscription.get_channel_id())
-@@ -1025,9 +1025,9 @@ class Provider(kodion.AbstractProvider):
- context.set_param('q', search_text)
-
- if search_type == 'video':
-- self.set_content_type(context, kodion.constants.content_type.VIDEOS)
-+ self.set_content_type(context, kodion.constants.content_type.EPISODES)
- else:
-- self.set_content_type(context, kodion.constants.content_type.FILES)
-+ self.set_content_type(context, kodion.constants.content_type.VIDEOS)
-
- if page == 1 and search_type == 'video' and not event_type and not hide_folders:
- if not channel_id and not location:
-@@ -1360,7 +1360,7 @@ class Provider(kodion.AbstractProvider):
- settings = context.get_settings()
- _ = self.get_client(context) # required for self.is_logged_in()
-
-- self.set_content_type(context, kodion.constants.content_type.FILES)
-+ self.set_content_type(context, kodion.constants.content_type.VIDEOS)
-
- result = []
-
-@@ -1595,7 +1595,7 @@ class Provider(kodion.AbstractProvider):
- @staticmethod
- def set_content_type(context, content_type):
- context.set_content_type(content_type)
-- if content_type == kodion.constants.content_type.VIDEOS:
-+ if content_type == kodion.constants.content_type.EPISODES:
- context.add_sort_method(kodion.constants.sort_method.UNSORTED,
- kodion.constants.sort_method.VIDEO_RUNTIME,
- kodion.constants.sort_method.DATE_ADDED,
-diff --git a/resources/settings.xml b/resources/settings.xml
-index 70429df9..65206514 100644
---- a/resources/settings.xml
-+++ b/resources/settings.xml
-@@ -663,6 +663,37 @@
-
-
-
-+
-+ 0
-+ false
-+
-+
-+
-+ 0
-+ 50
-+
-+
-+ true
-+
-+
-+
-+ 30027
-+
-+
-+
-+ 0
-+ 50
-+
-+
-+ true
-+
-+
-+
-+ 30028
-+
-+
-+
-+
-
- 0
- 10
-@@ -681,7 +712,7 @@
-
-
-
--
-+
-
- 0
- en-US
diff --git a/addon.xml b/addon.xml
index b23e1c0ae..29f59368b 100644
--- a/addon.xml
+++ b/addon.xml
@@ -7,22 +7,48 @@
-
+
video
-
+
+
+ executable
+
-[new] quality and stream feature selection
-[new] enable HLS live streams
-[new] stream and language labelling
-[chg] separately enable local/remote history
-[chg] restore unsupported compatibility for Kodi v19 (Matrix)
-[fix] fix various playback issues |contrib: various|
-[fix] Limit host connections getting subscription feeds |contrib: cas--|
-[fix] fix handling of connection failures
-[upd] Translations updated from Kodi Weblate
+### New
+- Add display of extra video information (premieres, views, comments, likes) #18, #464, #503
+- Add support for Clips #450
+- Add ability to combine playlists #480
+- Add support for timestamps in links #502
+- Add initial support for higher bitrate streams #505
+- Add ability to limit video FPS at max resolution #539
+- Add local Watch Later and History for use when not logged in or custom playlist not set
+- Update main menu items:
+ - New Recommended videos (similar to YouTube home page, will use login if available)
+ - Old Recommended videos renamed to Related videos (requires local/remote history enabled)
+ - Popular right now renamed to Trending
+
+### Changed
+- Local history made optional and enabled by default
+- Existing user data will be lost due to changes in data format:
+ - Search, local history, and local watch later is stored per user
+ - Function and data cache will be wiped (will also become per user in future)
+- Disable OPUS audio by default #537
+
+### Fixed
+- Fix sharing links #115, #250, #538
+- Fix date and sorting issues #411, #425, #434
+- Fix issue with switching between H264/AV1 streams #532
+- Fix prompt for subtitles #534
+- Fix issues with corrupt user data #536
+- Fix issues with live streams #530, #540
+- Fix issues with loading large playlists #545
+- Fix Recommendations, Related Videos, and Auto-play next #508
+- Fix queuing from current playlist #549
+- Fix issues with randomising playlists #485
+- Workaround for crashes #113, #540
resources/media/icon.png
diff --git a/resources/language/resource.language.en_au/strings.po b/resources/language/resource.language.en_au/strings.po
index c6e325355..ecf6f327a 100644
--- a/resources/language/resource.language.en_au/strings.po
+++ b/resources/language/resource.language.en_au/strings.po
@@ -31,10 +31,13 @@ msgstr ""
# msgctxt "Addon Summary"
# msgid "Plugin for YouTube"
# msgstr ""
+
# msgctxt "Addon Description"
# msgid "YouTube is a one of the biggest video-sharing websites of the world."
# msgstr ""
+
# Kodion Settings
+
msgctxt "#30000"
msgid "General"
msgstr ""
@@ -47,7 +50,12 @@ msgctxt "#30002"
msgid "Password"
msgstr ""
-# empty strings from id 30003 to 30006
+msgctxt "#30003"
+msgid "YouTube"
+msgstr ""
+
+# empty strings from id 30004 to 30006
+
msgctxt "#30007"
msgid "Use InputStream Adaptive"
msgstr ""
@@ -178,6 +186,7 @@ msgstr ""
# Kodion Common
# empty strings from id 30039 to 30099
+
msgctxt "#30100"
msgid "Favourites"
msgstr ""
@@ -264,6 +273,7 @@ msgstr ""
# YouTube
# empty strings from id 30121 to 30199
+
msgctxt "#30200"
msgid "API"
msgstr ""
@@ -290,6 +300,7 @@ msgstr ""
# YouTube
# empty strings from id 30206 to 30499
+
msgctxt "#30500"
msgid "Channels"
msgstr ""
@@ -343,7 +354,7 @@ msgid "Browse Channels"
msgstr ""
msgctxt "#30513"
-msgid "Popular right now"
+msgid "Trending"
msgstr ""
msgctxt "#30514"
diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po
index fb9fd6986..9c994c905 100644
--- a/resources/language/resource.language.en_gb/strings.po
+++ b/resources/language/resource.language.en_gb/strings.po
@@ -50,7 +50,11 @@ msgctxt "#30002"
msgid "Password"
msgstr ""
-# empty strings from id 30003 to 30006
+msgctxt "#30003"
+msgid "YouTube"
+msgstr ""
+
+# empty strings from id 30004 to 30006
msgctxt "#30007"
msgid "Use InputStream Adaptive"
@@ -350,7 +354,7 @@ msgid "Browse Channels"
msgstr ""
msgctxt "#30513"
-msgid "Popular right now"
+msgid "Trending"
msgstr ""
msgctxt "#30514"
diff --git a/resources/language/resource.language.en_nz/strings.po b/resources/language/resource.language.en_nz/strings.po
index 7d15e52e8..872d3f7fe 100644
--- a/resources/language/resource.language.en_nz/strings.po
+++ b/resources/language/resource.language.en_nz/strings.po
@@ -34,7 +34,9 @@ msgstr ""
# msgctxt "Addon Description"
# msgid "YouTube is a one of the biggest video-sharing websites of the world."
# msgstr ""
+
# Kodion Settings
+
msgctxt "#30000"
msgid "General"
msgstr ""
@@ -47,7 +49,12 @@ msgctxt "#30002"
msgid "Password"
msgstr ""
-# empty strings from id 30003 to 30006
+msgctxt "#30003"
+msgid "YouTube"
+msgstr ""
+
+# empty strings from id 30004 to 30006
+
msgctxt "#30007"
msgid "Use InputStream Adaptive"
msgstr ""
@@ -343,7 +350,7 @@ msgid "Browse Channels"
msgstr ""
msgctxt "#30513"
-msgid "Popular right now"
+msgid "Trending"
msgstr ""
msgctxt "#30514"
diff --git a/resources/language/resource.language.en_us/strings.po b/resources/language/resource.language.en_us/strings.po
index be70fa46c..e60a50d0a 100644
--- a/resources/language/resource.language.en_us/strings.po
+++ b/resources/language/resource.language.en_us/strings.po
@@ -32,10 +32,13 @@ msgstr ""
# msgctxt "Addon Summary"
# msgid "Plugin for YouTube"
# msgstr ""
+
# msgctxt "Addon Description"
# msgid "YouTube is a one of the biggest video-sharing websites of the world."
# msgstr ""
+
# Kodion Settings
+
msgctxt "#30000"
msgid "General"
msgstr "General"
@@ -48,7 +51,12 @@ msgctxt "#30002"
msgid "Password"
msgstr "Password"
-# empty strings from id 30003 to 30006
+msgctxt "#30003"
+msgid "YouTube"
+msgstr ""
+
+# empty strings from id 30004 to 30006
+
msgctxt "#30007"
msgid "Use InputStream Adaptive"
msgstr ""
@@ -179,6 +187,7 @@ msgstr ""
# Kodion Common
# empty strings from id 30039 to 30099
+
msgctxt "#30100"
msgid "Favourites"
msgstr "Favorites"
@@ -265,6 +274,7 @@ msgstr ""
# YouTube
# empty strings from id 30121 to 30199
+
msgctxt "#30200"
msgid "API"
msgstr ""
@@ -291,6 +301,7 @@ msgstr ""
# YouTube
# empty strings from id 30206 to 30499
+
msgctxt "#30500"
msgid "Channels"
msgstr "Channels"
@@ -344,7 +355,7 @@ msgid "Browse Channels"
msgstr ""
msgctxt "#30513"
-msgid "Popular right now"
+msgid "Trending"
msgstr ""
msgctxt "#30514"
diff --git a/resources/lib/default.py b/resources/lib/plugin.py
similarity index 100%
rename from resources/lib/default.py
rename to resources/lib/plugin.py
diff --git a/resources/lib/startup.py b/resources/lib/service.py
similarity index 100%
rename from resources/lib/startup.py
rename to resources/lib/service.py
diff --git a/resources/lib/youtube_plugin/__init__.py b/resources/lib/youtube_plugin/__init__.py
index 84fc92d21..44b1ba27b 100644
--- a/resources/lib/youtube_plugin/__init__.py
+++ b/resources/lib/youtube_plugin/__init__.py
@@ -26,4 +26,4 @@
}
}
-__all__ = ['kodion', 'youtube', 'key_sets', 'refresh']
+__all__ = ['kodion', 'youtube', 'key_sets', 'script']
diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py
index 0fbbb337f..5f85ea08b 100644
--- a/resources/lib/youtube_plugin/kodion/abstract_provider.py
+++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py
@@ -12,7 +12,7 @@
import re
-from .constants import settings, paths, content
+from .constants import paths, content
from .compatibility import quote, unquote
from .exceptions import KodionException
from .items import (
@@ -85,28 +85,26 @@ def register_path(self, re_path, method_name):
"""
self._dict_path[re_path] = method_name
- def _process_wizard(self, context):
- # start the setup wizard
- wizard_steps = []
- if context.get_settings().is_setup_wizard_enabled():
- context.get_settings().set_bool(settings.SETUP_WIZARD, False)
- wizard_steps.extend(self.get_wizard_steps(context))
+ def run_wizard(self, context):
+ settings = context.get_settings()
+ ui = context.get_ui()
+
+ settings.set_bool(settings.SETUP_WIZARD, False)
+
+ wizard_steps = self.get_wizard_steps(context)
+ wizard_steps.extend(ui.get_view_manager().get_wizard_steps())
- if wizard_steps and context.get_ui().on_yes_no_input(context.get_name(),
- context.localize('setup_wizard.execute')):
+ if (wizard_steps and ui.on_yes_no_input(
+ context.get_name(), context.localize('setup_wizard.execute')
+ )):
for wizard_step in wizard_steps:
wizard_step[0](*wizard_step[1])
- def get_wizard_supported_views(self):
- return ['default']
-
def get_wizard_steps(self, context):
# can be overridden by the derived class
return []
def navigate(self, context):
- self._process_wizard(context)
-
path = context.get_path()
for key in self._dict_path:
@@ -195,6 +193,7 @@ def _internal_watch_later(context, re_match):
return False
if command == 'list':
+ context.set_content(content.VIDEO_CONTENT, sub_type='watch_later')
video_items = context.get_watch_later_list().get_items()
for video_item in video_items:
@@ -290,7 +289,7 @@ def _internal_search(self, context, re_match):
if command == 'input':
self.data_cache = context
- folder_path = ui.get_info_label('Container.FolderPath')
+ folder_path = context.get_infolabel('Container.FolderPath')
query = None
# came from page 1 of search query by '..'/back
# user doesn't want to input on this path
@@ -327,7 +326,7 @@ def _internal_search(self, context, re_match):
query = query.decode('utf-8')
return self.on_search(query, context, re_match)
- context.set_content_type(content.VIDEOS)
+ context.set_content(content.VIDEO_CONTENT)
result = []
location = context.get_param('location', False)
diff --git a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py
index b8cc23a3f..19cc2ebcd 100644
--- a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py
+++ b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py
@@ -26,6 +26,13 @@
import xbmcplugin
import xbmcvfs
+ from infotagger.listitem import set_info_tag
+
+ xbmc.LOGNOTICE = xbmc.LOGINFO
+ xbmc.LOGSEVERE = xbmc.LOGFATAL
+
+ string_type = str
+
except ImportError:
import BaseHTTPServer
from contextlib import contextmanager as _contextmanager
@@ -92,11 +99,39 @@ def _file_closer(*args, **kwargs):
xbmcvfs.translatePath = xbmc.translatePath
+ def set_info_tag(listitem, infolabels, tag_type, *_args, **_kwargs):
+ listitem.setInfo(tag_type, infolabels)
+ return ListItemInfoTag(listitem, tag_type)
+
+
+ class ListItemInfoTag(object):
+ __slots__ = ('__li__',)
+
+ def __init__(self, listitem, *_args, **_kwargs):
+ self.__li__ = listitem
+
+ def add_stream_info(self, *args, **kwargs):
+ return self.__li__.addStreamInfo(*args, **kwargs)
+
+ def set_resume_point(self,
+ infoproperties,
+ resume_key='ResumeTime',
+ total_key='TotalTime'):
+ if resume_key in infoproperties:
+ infoproperties[resume_key] = str(infoproperties[resume_key])
+ if total_key in infoproperties:
+ infoproperties[total_key] = str(infoproperties[total_key])
+
+
+ string_type = basestring
+
__all__ = (
'BaseHTTPServer',
'parse_qs',
'parse_qsl',
'quote',
+ 'set_info_tag',
+ 'string_type',
'unescape',
'unquote',
'urlencode',
diff --git a/resources/lib/youtube_plugin/kodion/constants/const_content_types.py b/resources/lib/youtube_plugin/kodion/constants/const_content_types.py
index 6436882bb..0003408a7 100644
--- a/resources/lib/youtube_plugin/kodion/constants/const_content_types.py
+++ b/resources/lib/youtube_plugin/kodion/constants/const_content_types.py
@@ -10,6 +10,9 @@
from __future__ import absolute_import, division, unicode_literals
+VIDEO_CONTENT = 'videos'
+LIST_CONTENT = 'files'
+VIDEO_TYPE = 'video'
FILES = 'files'
SONGS = 'songs'
diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py
index 9f232632b..ee74bdb40 100644
--- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py
+++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py
@@ -58,11 +58,6 @@
API_KEY = 'youtube.api.key' # (string)
API_ID = 'youtube.api.id' # (string)
API_SECRET = 'youtube.api.secret' # (string)
-API_LAST_HASH = 'youtube.api.last.hash' # (string)
-
-USER_ACCESS_TOKEN = 'kodion.access_token' # (string)
-USER_REFRESH_TOKEN = 'kodion.refresh_token' # (string)
-USER_TOKEN_EXPIRATION = 'kodion.access_token.expires' # (int)
CLIENT_SELECTION = 'youtube.client.selection' # (int)
diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py
index 4cee1fe2e..19c8d5d78 100644
--- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py
+++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py
@@ -14,7 +14,6 @@
from .. import logger
from ..compatibility import urlencode
-from ..constants import settings
from ..json_store import AccessManager
from ..sql_store import (
DataCache,
@@ -67,10 +66,12 @@ class AbstractContext(object):
'api_key',
'action',
'addon_id',
+ 'category_label',
'channel_id',
'channel_name',
'client_id',
'client_secret',
+ 'click_tracking',
'event_type',
'item',
'item_id',
@@ -84,17 +85,16 @@ class AbstractContext(object):
'rating',
'search_type',
'subscription_id',
+ 'uri',
'videoid', # deprecated
'video_id',
- 'uri',
+ 'visitor',
}
def __init__(self, path='/', params=None, plugin_name='', plugin_id=''):
if not params:
params = {}
- self._system_version = None
-
self._cache_path = None
self._debug_path = None
@@ -112,7 +112,6 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id=''):
self._path = create_path(path)
self._params = params
self._utils = None
- self._view_mode = None
# create valid uri
self.parse_params()
@@ -143,16 +142,17 @@ def get_cache_path(self):
def get_playback_history(self):
if not self._playback_history:
uuid = self.get_access_manager().get_current_user_id()
- filename = ''.join((uuid, '.sqlite'))
- filepath = os.path.join(self.get_data_path(), 'playback', filename)
+ filename = 'history.sqlite'
+ filepath = os.path.join(self.get_data_path(), uuid, filename)
self._playback_history = PlaybackHistory(filepath)
return self._playback_history
def get_data_cache(self):
if not self._data_cache:
- cache_size = self.get_settings().get_int(settings.CACHE_SIZE, -1)
+ settings = self.get_settings()
+ cache_size = settings.get_int(settings.CACHE_SIZE, -1)
if cache_size <= 0:
- cache_size = 5
+ cache_size = 10
else:
cache_size /= 2.0
filename = 'data_cache.sqlite'
@@ -162,9 +162,10 @@ def get_data_cache(self):
def get_function_cache(self):
if not self._function_cache:
- cache_size = self.get_settings().get_int(settings.CACHE_SIZE, -1)
+ settings = self.get_settings()
+ cache_size = settings.get_int(settings.CACHE_SIZE, -1)
if cache_size <= 0:
- cache_size = 5
+ cache_size = 10
else:
cache_size /= 2.0
filename = 'cache.sqlite'
@@ -175,24 +176,28 @@ def get_function_cache(self):
def get_search_history(self):
if not self._search_history:
- search_size = self.get_settings().get_int(settings.SEARCH_SIZE, 50)
+ settings = self.get_settings()
+ search_size = settings.get_int(settings.SEARCH_SIZE, 50)
+ uuid = self.get_access_manager().get_current_user_id()
filename = 'search.sqlite'
- filepath = os.path.join(self.get_cache_path(), filename)
+ filepath = os.path.join(self.get_data_path(), uuid, filename)
self._search_history = SearchHistory(filepath,
max_item_count=search_size)
return self._search_history
def get_favorite_list(self):
if not self._favorite_list:
+ uuid = self.get_access_manager().get_current_user_id()
filename = 'favorites.sqlite'
- filepath = os.path.join(self.get_cache_path(), filename)
+ filepath = os.path.join(self.get_data_path(), uuid, filename)
self._favorite_list = FavoriteList(filepath)
return self._favorite_list
def get_watch_later_list(self):
if not self._watch_later_list:
+ uuid = self.get_access_manager().get_current_user_id()
filename = 'watch_later.sqlite'
- filepath = os.path.join(self.get_cache_path(), filename)
+ filepath = os.path.join(self.get_data_path(), uuid, filename)
self._watch_later_list = WatchLaterList(filepath)
return self._watch_later_list
@@ -217,10 +222,7 @@ def get_ui(self):
raise NotImplementedError()
def get_system_version(self):
- if not self._system_version:
- self._system_version = current_system_version
-
- return self._system_version
+ return current_system_version
def create_uri(self, path='/', params=None):
if not params:
@@ -330,10 +332,10 @@ def get_handle(self):
def get_settings(self):
raise NotImplementedError()
- def localize(self, text_id, default_text=''):
+ def localize(self, text_id, default_text=None):
raise NotImplementedError()
- def set_content_type(self, content_type):
+ def set_content(self, content_type, sub_type=None, category_label=None):
raise NotImplementedError()
def add_sort_method(self, *sort_methods):
@@ -360,8 +362,14 @@ def log_info(self, text):
def clone(self, new_path=None, new_params=None):
raise NotImplementedError()
- def execute(self, command):
+ @staticmethod
+ def execute(command):
raise NotImplementedError()
- def sleep(self, milli_seconds):
+ @staticmethod
+ def sleep(milli_seconds):
+ raise NotImplementedError()
+
+ @staticmethod
+ def get_infolabel(name):
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 9e6e30525..9e77ae4e1 100644
--- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py
+++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py
@@ -26,7 +26,7 @@
xbmcplugin,
xbmcvfs,
)
-from ...constants import ADDON_ID
+from ...constants import ADDON_ID, content, sort
from ...player.xbmc.xbmc_player import XbmcPlayer
from ...player.xbmc.xbmc_playlist import XbmcPlaylist
from ...settings.xbmc.xbmc_plugin_settings import XbmcPluginSettings
@@ -139,7 +139,6 @@ class XbmcContext(AbstractContext):
'playlist.select': 30521,
'playlists': 30501,
'please_wait': 30119,
- 'popular_right_now': 30513,
'prompt': 30566,
'purchases': 30622,
'recommendations': 30551,
@@ -165,8 +164,6 @@ class XbmcContext(AbstractContext):
'select.listen.ip': 30644,
'select_video_quality': 30010,
'settings': 30577,
- 'setup.view_default': 30027,
- 'setup.view_videos': 30028,
'setup_wizard.adjust': 30526,
'setup_wizard.adjust.language_and_region': 30527,
'setup_wizard.execute': 30030,
@@ -199,6 +196,7 @@ class XbmcContext(AbstractContext):
'subtitles.no_auto_generated': 30602,
'subtitles.with_fallback': 30601,
'succeeded': 30575,
+ 'trending': 30513,
'unrated.video': 30718,
'unsubscribe': 30505,
'unsubscribed.from.channel': 30720,
@@ -245,17 +243,15 @@ class XbmcContext(AbstractContext):
'watch_later.list.set.confirm': 30570,
'watch_later.remove': 30108,
'watch_later.retrieval_page': 30711,
- # For unofficial setup wizard
- 'setup_wizard.view.episodes': 30028,
- 'setup_wizard.view.movies': 30029,
- 'setup_wizard.view.tvshows': 30032,
- 'setup_wizard.view.default': 30027,
- 'setup_wizard.view.songs': 30033,
- 'setup_wizard.view.artists': 30034,
- 'setup_wizard.view.albums': 30035
+ 'youtube': 30003,
}
- def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override=True):
+ def __init__(self,
+ path='/',
+ params=None,
+ plugin_name='',
+ plugin_id='',
+ override=True):
super(XbmcContext, self).__init__(path, params, plugin_name, plugin_id)
if plugin_id:
@@ -269,29 +265,37 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override
Also we extract the path and parameters - man, that would be so simple with the normal url-parsing routines.
"""
num_args = len(sys.argv)
-
- # first the path of the uri
- if override:
- self._uri = sys.argv[0]
- parsed_url = urlsplit(self._uri)
- self._path = unquote(parsed_url.path)
-
- # after that try to get the params
- if num_args > 2:
- params = sys.argv[2][1:]
- if params:
- self._uri = '?'.join((self._uri, params))
- self.parse_params(dict(parse_qsl(params)))
-
- if num_args > 3 and sys.argv[3].lower() == 'resume:true':
- self._params['resume'] = True
+ if override and num_args:
+ uri = sys.argv[0]
+ is_plugin_invocation = uri.startswith('plugin://')
+ if is_plugin_invocation:
+ # first the path of the uri
+ self._uri = uri
+ parsed_url = urlsplit(uri)
+ self._path = unquote(parsed_url.path)
+
+ # after that try to get the params
+ if num_args > 2:
+ params = sys.argv[2][1:]
+ if params:
+ self._uri = '?'.join((self._uri, params))
+ self.parse_params(dict(parse_qsl(params)))
+
+ # then Kodi resume status
+ if num_args > 3 and sys.argv[3].lower() == 'resume:true':
+ self._params['resume'] = True
+ elif num_args:
+ uri = sys.argv[0]
+ is_plugin_invocation = uri.startswith('plugin://')
+ else:
+ is_plugin_invocation = False
self._ui = None
self._video_playlist = None
self._audio_playlist = None
self._video_player = None
self._audio_player = None
- self._plugin_handle = int(sys.argv[1]) if num_args > 1 else -1
+ self._plugin_handle = int(sys.argv[1]) if is_plugin_invocation else -1
self._plugin_id = plugin_id or ADDON_ID
self._plugin_name = plugin_name or self._addon.getAddonInfo('name')
self._version = self._addon.getAddonInfo('version')
@@ -327,7 +331,7 @@ def get_language(self):
The xbmc.getLanguage() method is fucked up!!! We always return 'en-US' for now
"""
- '''
+ """
if self.get_system_version().get_release_name() == 'Frodo':
return 'en-US'
@@ -339,7 +343,7 @@ def get_language(self):
except Exception as exc:
self.log_error('Failed to get system language (%s)', exc.__str__())
return 'en-US'
- '''
+ """
return 'en-US'
@@ -392,7 +396,10 @@ def get_addon_path(self):
def get_settings(self):
return self._settings
- def localize(self, text_id, default_text=''):
+ def localize(self, text_id, default_text=None):
+ if default_text is None:
+ default_text = 'Undefined string ID: |{0}|'.format(text_id)
+
if not isinstance(text_id, int):
try:
text_id = self.LOCAL_MAP[text_id]
@@ -406,17 +413,45 @@ def localize(self, text_id, default_text=''):
"""
We want to use all localization strings!
- Addons should only use the range 30000 thru 30999 (see: http://kodi.wiki/view/Language_support) but we
- do it anyway. I want some of the localized strings for the views of a skin.
+ Addons should only use the range 30000 thru 30999
+ (see: http://kodi.wiki/view/Language_support) but we do it anyway.
+ I want some of the localized strings for the views of a skin.
"""
source = self._addon if 30000 <= text_id < 31000 else xbmc
result = source.getLocalizedString(text_id)
result = to_unicode(result) if result else default_text
return result
- def set_content_type(self, content_type):
- self.log_debug('Setting content-type: "%s" for "%s"' % (content_type, self.get_path()))
+ def set_content(self, content_type, sub_type=None, category_label=None):
+ self.log_debug('Setting content-type: |{type}| for |{path}|'.format(
+ type=(sub_type or content_type), path=self.get_path()
+ ))
xbmcplugin.setContent(self._plugin_handle, content_type)
+ if category_label is None:
+ category_label = self.get_param('category_label')
+ if category_label:
+ xbmcplugin.setPluginCategory(self._plugin_handle, category_label)
+ if sub_type == 'history':
+ self.add_sort_method(
+ (sort.LASTPLAYED, '%T \u2022 %P', '%D | %J'),
+ (sort.PLAYCOUNT, '%T \u2022 %P', '%D | %J'),
+ (sort.UNSORTED, '%T \u2022 %P', '%D | %J'),
+ (sort.LABEL_IGNORE_THE, '%T \u2022 %P', '%D | %J'),
+ )
+ else:
+ self.add_sort_method(
+ (sort.UNSORTED, '%T \u2022 %P', '%D | %J'),
+ (sort.LABEL_IGNORE_THE, '%T \u2022 %P', '%D | %J'),
+ )
+ if content_type == content.VIDEO_CONTENT:
+ self.add_sort_method(
+ (sort.PROGRAM_COUNT, '%T \u2022 %P | %D | %J', '%C'),
+ (sort.VIDEO_RATING, '%T \u2022 %P | %D | %J', '%R'),
+ (sort.DATE, '%T \u2022 %P | %D', '%J'),
+ (sort.DATEADDED, '%T \u2022 %P | %D', '%a'),
+ (sort.VIDEO_RUNTIME, '%T \u2022 %P | %J', '%D'),
+ (sort.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'),
+ )
def add_sort_method(self, *sort_methods):
args = slice(None if current_system_version.compatible(19, 0) else 2)
@@ -446,10 +481,12 @@ def clone(self, new_path=None, new_params=None):
return new_context
- def execute(self, command):
+ @staticmethod
+ def execute(command):
xbmc.executebuiltin(command)
- def sleep(self, milli_seconds):
+ @staticmethod
+ def sleep(milli_seconds):
xbmc.sleep(milli_seconds)
def addon_enabled(self, addon_id):
@@ -466,7 +503,7 @@ def addon_enabled(self, addon_id):
message = response['error']['message']
code = response['error']['code']
error = 'Requested |%s| received error |%s| and code: |%s|' % (rpc_request, message, code)
- self.log_debug(error)
+ self.log_error(error)
return False
def set_addon_enabled(self, addon_id, enabled=True):
@@ -483,7 +520,7 @@ def set_addon_enabled(self, addon_id, enabled=True):
message = response['error']['message']
code = response['error']['code']
error = 'Requested |%s| received error |%s| and code: |%s|' % (rpc_request, message, code)
- self.log_debug(error)
+ self.log_error(error)
return False
def send_notification(self, method, data):
@@ -496,7 +533,9 @@ def use_inputstream_adaptive(self):
if self._settings.use_isa():
if self.addon_enabled('inputstream.adaptive'):
success = True
- elif self.get_ui().on_yes_no_input(self.get_name(), self.localize('isa.enable.confirm')):
+ elif self.get_ui().on_yes_no_input(
+ self.get_name(), self.localize('isa.enable.confirm')
+ ):
success = self.set_addon_enabled('inputstream.adaptive')
else:
success = False
@@ -505,15 +544,16 @@ def use_inputstream_adaptive(self):
return success
# Values of capability map can be any of the following:
- # - required version number as string for comparison with actual installed InputStream.Adaptive version
- # - any Falsey value to exclude capability regardless of version
+ # - required version number, as string for comparison with actual installed
+ # InputStream.Adaptive version
+ # - any Falsy value to exclude capability regardless of version
# - True to include capability regardless of version
_ISA_CAPABILITIES = {
'live': '2.0.12',
'drm': '2.2.12',
# audio codecs
'vorbis': '2.3.14',
- 'opus': '19.0.0', # unknown when Opus audio support was first implemented
+ 'opus': '19.0.0', # unknown when Opus audio support was implemented
'mp4a': True,
'ac-3': '2.1.15',
'ec-3': '2.1.15',
@@ -526,10 +566,13 @@ def use_inputstream_adaptive(self):
}
def inputstream_adaptive_capabilities(self, capability=None):
- # return a list inputstream.adaptive capabilities, if capability set return version required
+ # Returns a list of inputstream.adaptive capabilities
+ # If capability param is provided, returns version of ISA where the
+ # capability is available
try:
- inputstream_version = xbmcaddon.Addon('inputstream.adaptive').getAddonInfo('version')
+ addon = xbmcaddon.Addon('inputstream.adaptive')
+ inputstream_version = addon.getAddonInfo('version')
except RuntimeError:
inputstream_version = ''
@@ -539,20 +582,27 @@ def inputstream_adaptive_capabilities(self, capability=None):
isa_loose_version = loose_version(inputstream_version)
if capability is None:
capabilities = frozenset(
- capability for capability, version in self._ISA_CAPABILITIES.items()
+ capability
+ for (capability, version) in self._ISA_CAPABILITIES.items()
if version is True
or version and isa_loose_version >= loose_version(version)
)
return capabilities
version = self._ISA_CAPABILITIES.get(capability)
- return version is True or version and isa_loose_version >= loose_version(version)
+ return (version is True
+ or version and isa_loose_version >= loose_version(version))
@staticmethod
def inputstream_adaptive_auto_stream_selection():
try:
- return xbmcaddon.Addon('inputstream.adaptive').getSetting('STREAMSELECTION') == '0'
+ addon = xbmcaddon.Addon('inputstream.adaptive')
+ return addon.getSetting('STREAMSELECTION') == '0'
except RuntimeError:
return False
def abort_requested(self):
return self.get_ui().get_property('abort_requested').lower() == 'true'
+
+ @staticmethod
+ def get_infolabel(name):
+ return xbmc.getInfoLabel(name)
diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py
index a919f8e68..ea0cdfa5d 100644
--- a/resources/lib/youtube_plugin/kodion/items/base_item.py
+++ b/resources/lib/youtube_plugin/kodion/items/base_item.py
@@ -14,7 +14,7 @@
from datetime import date, datetime
from hashlib import md5
-from ..compatibility import unescape
+from ..compatibility import string_type, unescape
from ..constants import MEDIA_PATH
@@ -106,7 +106,7 @@ def get_name(self):
return self._name
def set_uri(self, uri):
- self._uri = uri if uri and isinstance(uri, str) else ''
+ self._uri = uri if uri and isinstance(uri, string_type) else ''
def get_uri(self):
"""
diff --git a/resources/lib/youtube_plugin/kodion/items/directory_item.py b/resources/lib/youtube_plugin/kodion/items/directory_item.py
index 8b26ae4e5..8f74a8793 100644
--- a/resources/lib/youtube_plugin/kodion/items/directory_item.py
+++ b/resources/lib/youtube_plugin/kodion/items/directory_item.py
@@ -11,10 +11,24 @@
from __future__ import absolute_import, division, unicode_literals
from .base_item import BaseItem
+from ..compatibility import urlencode
class DirectoryItem(BaseItem):
- def __init__(self, name, uri, image='', fanart='', action=False):
+ def __init__(self,
+ name,
+ uri,
+ image='',
+ fanart='',
+ action=False,
+ category_label=None):
+ if category_label is None:
+ category_label = name
+ if category_label:
+ uri = ('&' if '?' in uri else '?').join((
+ uri,
+ urlencode({'category_label': category_label}),
+ ))
super(DirectoryItem, self).__init__(name, uri, image, fanart)
self._plot = self.get_name()
self._is_action = action
diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py
index ba51775bd..24a252919 100644
--- a/resources/lib/youtube_plugin/kodion/items/menu_items.py
+++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py
@@ -17,7 +17,7 @@ def more_for_video(context, video_id, logged_in=False, refresh_container=False):
return (
context.localize('video.more'),
'RunPlugin({0})'.format(context.create_uri(
- ['video', 'more'],
+ ('video', 'more',),
{
'video_id': video_id,
'logged_in': logged_in,
@@ -27,11 +27,35 @@ def more_for_video(context, video_id, logged_in=False, refresh_container=False):
)
+def related_videos(context, video_id):
+ return (
+ context.localize('related_videos'),
+ 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri(
+ ('special', 'related_videos',),
+ {
+ 'video_id': video_id,
+ }
+ ))
+ )
+
+
+def video_comments(context, video_id):
+ return (
+ context.localize('video.comments'),
+ 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri(
+ ('special', 'parent_comments',),
+ {
+ 'video_id': video_id,
+ }
+ ))
+ )
+
+
def content_from_description(context, video_id):
return (
context.localize('video.description.links'),
- 'RunPlugin({0})'.format(context.create_uri(
- ['special', 'description_links'],
+ 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri(
+ ('special', 'description_links',),
{
'video_id': video_id,
}
@@ -46,6 +70,13 @@ def play_with(context):
)
+def refresh(context):
+ return (
+ context.localize('refresh'),
+ 'Container.Refresh'
+ )
+
+
def queue_video(context):
return (
context.localize('video.queue'),
@@ -58,7 +89,7 @@ def play_all_from_playlist(context, playlist_id, video_id=''):
return (
context.localize('playlist.play.from_here'),
'RunPlugin({0})'.format(context.create_uri(
- ['play'],
+ ('play',),
{
'playlist_id': playlist_id,
'video_id': video_id,
@@ -69,7 +100,7 @@ def play_all_from_playlist(context, playlist_id, video_id=''):
return (
context.localize('playlist.play.all'),
'RunPlugin({0})'.format(context.create_uri(
- ['play'],
+ ('play',),
{
'playlist_id': playlist_id,
'play': True,
@@ -82,7 +113,7 @@ def add_video_to_playlist(context, video_id):
return (
context.localize('video.add_to_playlist'),
'RunPlugin({0})'.format(context.create_uri(
- ['playlist', 'select', 'playlist'],
+ ('playlist', 'select', 'playlist',),
{
'video_id': video_id,
}
@@ -94,7 +125,7 @@ def remove_video_from_playlist(context, playlist_id, video_id, video_name):
return (
context.localize('remove'),
'RunPlugin({0})'.format(context.create_uri(
- ['playlist', 'remove', 'video'],
+ ('playlist', 'remove', 'video',),
{
'playlist_id': playlist_id,
'video_id': video_id,
@@ -108,7 +139,7 @@ def rename_playlist(context, playlist_id, playlist_name):
return (
context.localize('rename'),
'RunPlugin({0})'.format(context.create_uri(
- ['playlist', 'rename', 'playlist'],
+ ('playlist', 'rename', 'playlist',),
{
'playlist_id': playlist_id,
'playlist_name': playlist_name
@@ -121,7 +152,7 @@ def delete_playlist(context, playlist_id, playlist_name):
return (
context.localize('delete'),
'RunPlugin({0})'.format(context.create_uri(
- ['playlist', 'remove', 'playlist'],
+ ('playlist', 'remove', 'playlist',),
{
'playlist_id': playlist_id,
'playlist_name': playlist_name
@@ -130,11 +161,11 @@ def delete_playlist(context, playlist_id, playlist_name):
)
-def remove_as_watchlater(context, playlist_id, playlist_name):
+def remove_as_watch_later(context, playlist_id, playlist_name):
return (
context.localize('watch_later.list.remove'),
'RunPlugin({0})'.format(context.create_uri(
- ['playlist', 'remove', 'watchlater'],
+ ('playlist', 'remove', 'watch_later',),
{
'playlist_id': playlist_id,
'playlist_name': playlist_name
@@ -143,11 +174,11 @@ def remove_as_watchlater(context, playlist_id, playlist_name):
)
-def set_as_watchlater(context, playlist_id, playlist_name):
+def set_as_watch_later(context, playlist_id, playlist_name):
return (
context.localize('watch_later.list.set'),
'RunPlugin({0})'.format(context.create_uri(
- ['playlist', 'set', 'watchlater'],
+ ('playlist', 'set', 'watch_later',),
{
'playlist_id': playlist_id,
'playlist_name': playlist_name
@@ -160,7 +191,7 @@ def remove_as_history(context, playlist_id, playlist_name):
return (
context.localize('history.list.remove'),
'RunPlugin({0})'.format(context.create_uri(
- ['playlist', 'remove', 'history'],
+ ('playlist', 'remove', 'history',),
{
'playlist_id': playlist_id,
'playlist_name': playlist_name
@@ -173,7 +204,7 @@ def set_as_history(context, playlist_id, playlist_name):
return (
context.localize('history.list.set'),
'RunPlugin({0})'.format(context.create_uri(
- ['playlist', 'set', 'history'],
+ ('playlist', 'set', 'history',),
{
'playlist_id': playlist_id,
'playlist_name': playlist_name
@@ -186,7 +217,7 @@ def remove_my_subscriptions_filter(context, channel_name):
return (
context.localize('my_subscriptions.filter.remove'),
'RunPlugin({0})'.format(context.create_uri(
- ['my_subscriptions', 'filter'],
+ ('my_subscriptions', 'filter',),
{
'channel_name': channel_name,
'action': 'remove'
@@ -199,7 +230,7 @@ def add_my_subscriptions_filter(context, channel_name):
return (
context.localize('my_subscriptions.filter.add'),
'RunPlugin({0})'.format(context.create_uri(
- ['my_subscriptions', 'filter'],
+ ('my_subscriptions', 'filter',),
{
'channel_name': channel_name,
'action': 'add',
@@ -212,7 +243,7 @@ def rate_video(context, video_id, refresh_container=False):
return (
context.localize('video.rate'),
'RunPlugin({0})'.format(context.create_uri(
- ['video', 'rate'],
+ ('video', 'rate',),
{
'video_id': video_id,
'refresh_container': refresh_container,
@@ -225,7 +256,7 @@ def watch_later_add(context, playlist_id, video_id):
return (
context.localize('watch_later.add'),
'RunPlugin({0})'.format(context.create_uri(
- ['playlist', 'add', 'video'],
+ ('playlist', 'add', 'video',),
{
'playlist_id': playlist_id,
'video_id': video_id,
@@ -238,7 +269,7 @@ def watch_later_local_add(context, item):
return (
context.localize('watch_later.add'),
'RunPlugin({0})'.format(context.create_uri(
- [paths.WATCH_LATER, 'add'],
+ (paths.WATCH_LATER, 'add',),
{
'video_id': item.video_id,
'item': item.dumps(),
@@ -251,7 +282,7 @@ def watch_later_local_remove(context, video_id):
return (
context.localize('watch_later.remove'),
'RunPlugin({0})'.format(context.create_uri(
- [paths.WATCH_LATER, 'remove'],
+ (paths.WATCH_LATER, 'remove',),
{
'video_id': video_id,
}
@@ -263,7 +294,7 @@ def watch_later_local_clear(context):
return (
context.localize('watch_later.clear'),
'RunPlugin({0})'.format(context.create_uri(
- [paths.WATCH_LATER, 'clear']
+ (paths.WATCH_LATER, 'clear',)
))
)
@@ -271,37 +302,18 @@ def watch_later_local_clear(context):
def go_to_channel(context, channel_id, channel_name):
return (
context.localize('go_to_channel') % context.get_ui().bold(channel_name),
- 'Container.Update({0})'.format(context.create_uri(
- ['channel', channel_id]
+ 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri(
+ ('channel', channel_id,)
))
)
-def related_videos(context, video_id):
- return (
- context.localize('related_videos'),
- 'Container.Update({0})'.format(context.create_uri(
- ['special', 'related_videos'],
- {
- 'video_id': video_id,
- }
- ))
- )
-
-
-def refresh(context):
- return (
- context.localize('refresh'),
- 'Container.Refresh'
- )
-
-
def subscribe_to_channel(context, channel_id, channel_name=''):
if not channel_name:
return (
context.localize('subscribe'),
'RunPlugin({0})'.format(context.create_uri(
- ['subscriptions', 'add'],
+ ('subscriptions', 'add',),
{
'subscription_id': channel_id,
}
@@ -310,7 +322,7 @@ def subscribe_to_channel(context, channel_id, channel_name=''):
return (
context.localize('subscribe_to') % context.get_ui().bold(channel_name),
'RunPlugin({0})'.format(context.create_uri(
- ['subscriptions', 'add'],
+ ('subscriptions', 'add',),
{
'subscription_id': channel_id,
}
@@ -322,7 +334,7 @@ def unsubscribe_from_channel(context, channel_id):
return (
context.localize('unsubscribe'),
'RunPlugin({0})'.format(context.create_uri(
- ['subscriptions', 'remove'],
+ ('subscriptions', 'remove',),
{
'subscription_id': channel_id,
}
@@ -334,7 +346,7 @@ def play_with_subtitles(context, video_id):
return (
context.localize('video.play.with_subtitles'),
'RunPlugin({0})'.format(context.create_uri(
- ['play'],
+ ('play',),
{
'video_id': video_id,
'prompt_for_subtitles': True,
@@ -347,7 +359,7 @@ def play_audio_only(context, video_id):
return (
context.localize('video.play.audio_only'),
'RunPlugin({0})'.format(context.create_uri(
- ['play'],
+ ('play',),
{
'video_id': video_id,
'audio_only': True,
@@ -360,7 +372,7 @@ def play_ask_for_quality(context, video_id):
return (
context.localize('video.play.ask_for_quality'),
'RunPlugin({0})'.format(context.create_uri(
- ['play'],
+ ('play',),
{
'video_id': video_id,
'ask_for_quality': True,
@@ -437,7 +449,7 @@ def favorites_add(context, item):
return (
context.localize('favorites.add'),
'RunPlugin({0})'.format(context.create_uri(
- [paths.FAVORITES, 'add'],
+ (paths.FAVORITES, 'add',),
{
'video_id': item.video_id,
'item': item.dumps(),
@@ -450,7 +462,7 @@ def favorites_remove(context, video_id):
return (
context.localize('favorites.remove'),
'RunPlugin({0})'.format(context.create_uri(
- [paths.FAVORITES, 'remove'],
+ (paths.FAVORITES, 'remove',),
{
'vide_id': video_id,
}
@@ -462,7 +474,7 @@ def search_remove(context, query):
return (
context.localize('search.remove'),
'RunPlugin({0})'.format(context.create_uri(
- [paths.SEARCH, 'remove'],
+ (paths.SEARCH, 'remove',),
{
'q': query,
}
@@ -474,7 +486,7 @@ def search_rename(context, query):
return (
context.localize('search.rename'),
'RunPlugin({0})'.format(context.create_uri(
- [paths.SEARCH, 'rename'],
+ (paths.SEARCH, 'rename',),
{
'q': query,
}
@@ -486,6 +498,6 @@ def search_clear(context):
return (
context.localize('search.clear'),
'RunPlugin({0})'.format(context.create_uri(
- [paths.SEARCH, 'clear']
+ (paths.SEARCH, 'clear',)
))
)
diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py
index a73acb1c9..556dd8aa7 100644
--- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py
+++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py
@@ -15,17 +15,17 @@
class NextPageItem(DirectoryItem):
def __init__(self, context, current_page=1, image=None, fanart=None):
- new_params = dict(context.get_params(), page=(current_page + 1))
- name = context.localize('next_page', 'Next Page')
- if name.find('%d') != -1:
- name %= current_page + 1
+ next_page = current_page + 1
+ new_params = dict(context.get_params(), page=next_page)
+ name = context.localize('next_page') % next_page
super(NextPageItem, self).__init__(name,
context.create_uri(
context.get_path(),
new_params
),
- image=image)
+ image=image,
+ category_label=False)
if fanart:
self.set_fanart(fanart)
diff --git a/resources/lib/youtube_plugin/kodion/items/utils.py b/resources/lib/youtube_plugin/kodion/items/utils.py
index e85e7bd5c..d277802d3 100644
--- a/resources/lib/youtube_plugin/kodion/items/utils.py
+++ b/resources/lib/youtube_plugin/kodion/items/utils.py
@@ -17,6 +17,7 @@
from .directory_item import DirectoryItem
from .image_item import ImageItem
from .video_item import VideoItem
+from ..compatibility import string_type
from ..utils.datetime_parser import strptime
@@ -52,7 +53,7 @@ def from_json(json_data, *_args):
:param json_data:
:return:
"""
- if isinstance(json_data, str):
+ if isinstance(json_data, string_type):
json_data = json.loads(json_data, object_hook=_decoder)
item_type = json_data.get('type')
diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py
index 87b3483c1..258b6ab05 100644
--- a/resources/lib/youtube_plugin/kodion/items/video_item.py
+++ b/resources/lib/youtube_plugin/kodion/items/video_item.py
@@ -279,7 +279,7 @@ def get_license_key(self):
return self.license_key
def set_last_played(self, last_played):
- self._last_played = last_played or ''
+ self._last_played = last_played
def get_last_played(self):
return self._last_played
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 544a830b6..fc723091d 100644
--- a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py
+++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py
@@ -26,7 +26,7 @@ class AccessManager(JSONStore):
'token_expires': -1,
'last_key_hash': '',
'name': 'Default',
- 'watch_later': ' WL',
+ 'watch_later': 'WL',
'watch_history': 'HL'
}
@@ -85,7 +85,7 @@ def set_defaults(self, reset=False):
current_user = data['access_manager']['current_user']
if 'watch_later' not in data['access_manager']['users'][current_user]:
- data['access_manager']['users'][current_user]['watch_later'] = ' WL'
+ data['access_manager']['users'][current_user]['watch_later'] = 'WL'
if 'watch_history' not in data['access_manager']['users'][current_user]:
data['access_manager']['users'][current_user][
'watch_history'] = 'HL'
@@ -131,7 +131,7 @@ def get_current_user_details(self):
"""
:return: current user
"""
- return self.get_users()[self._user].copy()
+ return self.get_users()[self._user]
def get_current_user_id(self):
"""
@@ -158,7 +158,7 @@ def get_new_user(self, username=''):
'last_key_hash': '',
'name': username,
'id': new_uuid,
- 'watch_later': ' WL',
+ 'watch_later': 'WL',
'watch_history': 'HL'
}
@@ -179,11 +179,12 @@ def add_user(self, username='', user=None):
users = self.get_users()
new_user_details = self.get_new_user(username)
new_user = max(users) + 1 if users and user is None else user or 0
- users[new_user] = new_user_details
data = {
'access_manager': {
- 'users': users
- }
+ 'users': {
+ new_user: new_user_details,
+ },
+ },
}
self.save(data, update=True)
return new_user, new_user_details
@@ -196,13 +197,14 @@ def remove_user(self, user):
"""
users = self.get_users()
if user in users:
- del users[user]
- data = {
- 'access_manager': {
- 'users': users
+ data = {
+ 'access_manager': {
+ 'users': {
+ user: KeyError,
+ },
+ },
}
- }
- self.save(data, update=True)
+ self.save(data, update=True)
def set_users(self, users):
"""
@@ -230,8 +232,8 @@ def set_user(self, user, switch_to=False):
if switch_to:
data = {
'access_manager': {
- 'current_user': user
- }
+ 'current_user': user,
+ },
}
self.save(data, update=True)
@@ -261,11 +263,14 @@ def set_username(self, user, username):
"""
users = self.get_users()
if user in users:
- users[user]['name'] = username
data = {
'access_manager': {
- 'users': users
- }
+ 'users': {
+ user: {
+ 'name': username,
+ },
+ },
+ },
}
self.save(data, update=True)
return True
@@ -276,39 +281,16 @@ def get_watch_later_id(self):
Returns the current users watch later playlist id
:return: the current users watch later playlist id
"""
- updated = False
- watch_later_ids = ('wl', ' wl')
-
current_user = self.get_current_user_details()
- current_playlist_id = current_user.get('watch_later', '')
- settings_playlist_id = self._settings.get_watch_later_playlist()
+ current_id = current_user.get('watch_later', 'WL')
+ settings_id = self._settings.get_watch_later_playlist()
- if settings_playlist_id.lower().startswith(watch_later_ids):
- self._settings.set_watch_later_playlist('')
- settings_playlist_id = ''
+ if settings_id and current_id != settings_id:
+ current_id = self.set_watch_later_id(settings_id)
- if current_playlist_id.lower().startswith(watch_later_ids):
- updated = True
- current_user['watch_later'] = settings_playlist_id
- self._settings.set_watch_later_playlist('')
- settings_playlist_id = ''
-
- if settings_playlist_id and current_playlist_id != settings_playlist_id:
- updated = True
- current_user['watch_later'] = settings_playlist_id
- self._settings.set_watch_later_playlist('')
-
- if updated:
- data = {
- 'access_manager': {
- 'users': {
- self._user: current_user
- }
- }
- }
- self.save(data, update=True)
-
- return current_user.get('watch_later', '')
+ if current_id and current_id.lower().strip() == 'wl':
+ return ''
+ return current_id
def set_watch_later_id(self, playlist_id):
"""
@@ -316,20 +298,21 @@ def set_watch_later_id(self, playlist_id):
:param playlist_id: string, watch later playlist id
:return:
"""
- if playlist_id.lower() == 'wl' or playlist_id.lower() == ' wl':
+ if playlist_id.lower().strip() == 'wl':
playlist_id = ''
- current_user = self.get_current_user_details()
- current_user['watch_later'] = playlist_id
- self._settings.set_watch_later_playlist('')
+ self._settings.set_watch_later_playlist(playlist_id)
data = {
'access_manager': {
'users': {
- self._user: current_user
- }
- }
+ self._user: {
+ 'watch_later': playlist_id,
+ },
+ },
+ },
}
self.save(data, update=True)
+ return playlist_id
def get_watch_history_id(self):
"""
@@ -337,22 +320,15 @@ def get_watch_history_id(self):
:return: the current users watch history playlist id
"""
current_user = self.get_current_user_details()
- current_playlist_id = current_user.get('watch_history', 'HL')
- settings_playlist_id = self._settings.get_history_playlist()
+ current_id = current_user.get('watch_history', 'HL')
+ settings_id = self._settings.get_history_playlist()
- if settings_playlist_id and current_playlist_id != settings_playlist_id:
- current_user['watch_history'] = settings_playlist_id
- self._settings.set_history_playlist('')
- data = {
- 'access_manager': {
- 'users': {
- self._user: current_user
- }
- }
- }
- self.save(data, update=True)
+ if settings_id and current_id != settings_id:
+ current_id = self.set_watch_history_id(settings_id)
- return current_user.get('watch_history', 'HL')
+ if current_id and current_id.lower().strip() == 'hl':
+ return ''
+ return current_id
def set_watch_history_id(self, playlist_id):
"""
@@ -360,17 +336,21 @@ def set_watch_history_id(self, playlist_id):
:param playlist_id: string, watch history playlist id
:return:
"""
- current_user = self.get_current_user_details()
- current_user['watch_history'] = playlist_id
- self._settings.set_history_playlist('')
+ if playlist_id.lower().strip() == 'hl':
+ playlist_id = ''
+
+ self._settings.set_history_playlist(playlist_id)
data = {
'access_manager': {
'users': {
- self._user: current_user
- }
- }
+ self._user: {
+ 'watch_history': playlist_id,
+ },
+ },
+ },
}
self.save(data, update=True)
+ return playlist_id
def set_last_origin(self, origin):
"""
@@ -381,8 +361,8 @@ def set_last_origin(self, origin):
self._last_origin = origin
data = {
'access_manager': {
- 'last_origin': origin
- }
+ 'last_origin': origin,
+ },
}
self.save(data, update=True)
@@ -443,8 +423,9 @@ def update_access_token(self,
:param refresh_token:
:return:
"""
- current_user = self.get_current_user_details()
- current_user['access_token'] = access_token
+ current_user = {
+ 'access_token': access_token,
+ }
if unix_timestamp is not None:
current_user['token_expires'] = int(unix_timestamp)
@@ -455,21 +436,21 @@ def update_access_token(self,
data = {
'access_manager': {
'users': {
- self._user: current_user
- }
- }
+ self._user: current_user,
+ },
+ },
}
self.save(data, update=True)
def set_last_key_hash(self, key_hash):
- current_user = self.get_current_user_details()
- current_user['last_key_hash'] = key_hash
data = {
'access_manager': {
'users': {
- self._user: current_user
- }
- }
+ self._user: {
+ 'last_key_hash': key_hash,
+ },
+ },
+ },
}
self.save(data, update=True)
@@ -558,8 +539,9 @@ def update_dev_access_token(self,
:param refresh_token:
:return:
"""
- developer = self.get_developer(addon_id)
- developer['access_token'] = access_token
+ developer = {
+ 'access_token': access_token
+ }
if unix_timestamp is not None:
developer['token_expires'] = int(unix_timestamp)
@@ -570,9 +552,9 @@ def update_dev_access_token(self,
data = {
'access_manager': {
'developers': {
- addon_id: developer
- }
- }
+ addon_id: developer,
+ },
+ },
}
self.save(data, update=True)
@@ -580,14 +562,14 @@ def get_dev_last_key_hash(self, addon_id):
return self.get_developer(addon_id).get('last_key_hash', '')
def set_dev_last_key_hash(self, addon_id, key_hash):
- developer = self.get_developer(addon_id)
- developer['last_key_hash'] = key_hash
data = {
'access_manager': {
'developers': {
- addon_id: developer
- }
- }
+ addon_id: {
+ 'last_key_hash': key_hash,
+ },
+ },
+ },
}
self.save(data, update=True)
diff --git a/resources/lib/youtube_plugin/kodion/logger.py b/resources/lib/youtube_plugin/kodion/logger.py
index 03cde49ab..b3968f2d6 100644
--- a/resources/lib/youtube_plugin/kodion/logger.py
+++ b/resources/lib/youtube_plugin/kodion/logger.py
@@ -16,11 +16,11 @@
DEBUG = xbmc.LOGDEBUG
INFO = xbmc.LOGINFO
-NOTICE = INFO
+NOTICE = xbmc.LOGNOTICE
WARNING = xbmc.LOGWARNING
ERROR = xbmc.LOGERROR
FATAL = xbmc.LOGFATAL
-SEVERE = FATAL
+SEVERE = xbmc.LOGSEVERE
NONE = xbmc.LOGNONE
diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py
index 327789099..aa20ca176 100644
--- a/resources/lib/youtube_plugin/kodion/network/http_server.py
+++ b/resources/lib/youtube_plugin/kodion/network/http_server.py
@@ -27,7 +27,7 @@
xbmcvfs,
)
from ..constants import ADDON_ID, TEMP_PATH, paths
-from ..logger import log_debug
+from ..logger import log_debug, log_error
from ..settings import Settings
@@ -245,6 +245,9 @@ def do_POST(self):
headers=li_headers,
data=post_data,
stream=True)
+ if not response or not response.ok:
+ self.send_error(response and response.status_code or 500)
+ return
response_length = int(response.headers.get('content-length'))
content = response.raw.read(response_length)
@@ -529,7 +532,7 @@ def get_http_server(address=None, port=None):
server = BaseHTTPServer.HTTPServer((address, port), RequestHandler)
return server
except socket_error as exc:
- log_debug('HTTPServer: Failed to start |{address}:{port}| |{response}|'
+ log_error('HTTPServer: Failed to start |{address}:{port}| |{response}|'
.format(address=address, port=port, response=str(exc)))
xbmcgui.Dialog().notification(_addon_name,
str(exc),
@@ -545,30 +548,27 @@ def is_httpd_live(address=None, port=None):
url = 'http://{address}:{port}{path}'.format(address=address,
port=port,
path=paths.PING)
- try:
- response = _server_requests.request(url)
- result = response.status_code == 204
- if not result:
- log_debug('HTTPServer: Ping |{address}:{port}| |{response}|'
- .format(address=address,
- port=port,
- response=response.status_code))
- return result
- except:
- log_debug('HTTPServer: Ping |{address}:{port}| |{response}|'
- .format(address=address, port=port, response='failed'))
- return False
+ response = _server_requests.request(url)
+ result = response and response.status_code
+ if result == 204:
+ return True
+
+ log_debug('HTTPServer: Ping |{address}:{port}| |{response}|'
+ .format(address=address,
+ port=port,
+ response=result or 'failed'))
+ return False
def get_client_ip_address(address=None, port=None):
+ ip_address = None
address = _settings.httpd_listen(for_request=True, ip_address=address)
port = _settings.httpd_port(port=port)
url = 'http://{address}:{port}{path}'.format(address=address,
port=port,
path=paths.IP)
response = _server_requests.request(url)
- ip_address = None
- if response.status_code == 200:
+ if response and response.status_code == 200:
response_json = response.json()
if response_json:
ip_address = response_json.get('ip')
diff --git a/resources/lib/youtube_plugin/kodion/network/ip_api.py b/resources/lib/youtube_plugin/kodion/network/ip_api.py
index 93a2cd2be..4c6ea9062 100644
--- a/resources/lib/youtube_plugin/kodion/network/ip_api.py
+++ b/resources/lib/youtube_plugin/kodion/network/ip_api.py
@@ -27,7 +27,7 @@ def response(self):
def locate_requester(self):
request_url = '/'.join((self._base_url, 'json'))
response = self.request(request_url)
- self._response = response.json()
+ self._response = response and response.json() or {}
def success(self):
successful = self.response().get('status', 'fail') == 'success'
diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py
index fe3912dd3..70abae383 100644
--- a/resources/lib/youtube_plugin/kodion/network/requests.py
+++ b/resources/lib/youtube_plugin/kodion/network/requests.py
@@ -54,7 +54,7 @@ def __init__(self, exc_type=None):
elif exc_type:
self._default_exc = (RequestException, exc_type)
else:
- self._default_exc = RequestException
+ self._default_exc = (RequestException,)
def __del__(self):
self._session.close()
@@ -101,6 +101,9 @@ def request(self, url, method='GET',
verify=verify,
cert=cert,
json=json)
+ if not getattr(response, 'status_code', None):
+ raise self._default_exc[0](response=response)
+
if response_hook:
if response_hook_kwargs is None:
response_hook_kwargs = {}
@@ -110,7 +113,8 @@ def request(self, url, method='GET',
response.raise_for_status()
except self._default_exc as exc:
- response_text = exc.response and exc.response.text
+ exc_response = exc.response or response
+ response_text = exc_response and exc_response.text
stack_trace = format_stack()
error_details = {'exc': exc}
@@ -118,7 +122,7 @@ def request(self, url, method='GET',
if error_hook_kwargs is None:
error_hook_kwargs = {}
error_hook_kwargs['exc'] = exc
- error_hook_kwargs['response'] = response
+ error_hook_kwargs['response'] = exc_response
error_response = error_hook(**error_hook_kwargs)
_title, _info, _detail, _response, _trace, _exc = error_response
if _title is not None:
@@ -139,7 +143,12 @@ def request(self, url, method='GET',
error_title = 'Request failed'
if error_info is None:
- error_info = str(exc)
+ try:
+ error_info = 'Status: {0.status_code} - {0.reason}'.format(
+ exc.response
+ )
+ except AttributeError:
+ error_info = str(exc)
elif '{' in error_info:
try:
error_info = error_info.format(**error_details)
@@ -161,10 +170,13 @@ def request(self, url, method='GET',
] if part]))
if raise_exc:
+ if not callable(raise_exc):
+ raise_exc = self._default_exc[-1]
+ raise_exc = raise_exc(error_title)
+
if isinstance(raise_exc, BaseException):
- raise raise_exc(exc)
- if callable(raise_exc):
- raise raise_exc(error_title)(exc)
- raise self._default_exc(error_title)(exc)
+ raise_exc.__cause__ = exc
+ raise raise_exc
+ raise exc
return response
diff --git a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py
index e6bdc14c2..e3b61af48 100644
--- a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py
+++ b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py
@@ -33,9 +33,9 @@ def clear(self):
self._playlist.clear()
def add(self, base_item):
- item = xbmc_items.video_listitem(self._context, base_item)
+ uri, item, _ = xbmc_items.video_listitem(self._context, base_item)
if item:
- self._playlist.add(base_item.get_uri(), listitem=item)
+ self._playlist.add(uri, listitem=item)
def shuffle(self):
self._playlist.shuffle()
@@ -72,7 +72,7 @@ def get_items(self, properties=None, dumps=False):
error = 'Requested |%s| received error |%s| and code: |%s|' % (rpc_request, message, code)
else:
error = 'Requested |%s| received error |%s|' % (rpc_request, str(response))
- self._context.log_debug(error)
+ self._context.log_error(error)
return '[]' if dumps else []
def add_items(self, items, loads=False):
diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py
index 6300f1bd4..b94d781a7 100644
--- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py
+++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py
@@ -17,17 +17,23 @@
from ...exceptions import KodionException
from ...items import AudioItem, DirectoryItem, ImageItem, UriItem, VideoItem
from ...player import Playlist
-from ...ui.xbmc import info_labels, xbmc_items
+from ...ui.xbmc.xbmc_items import (
+ audio_listitem,
+ directory_listitem,
+ image_listitem,
+ playback_item,
+ video_listitem
+)
class XbmcRunner(AbstractProviderRunner):
def __init__(self):
super(XbmcRunner, self).__init__()
self.handle = None
- self.settings = None
def run(self, provider, context):
self.handle = context.get_handle()
+ settings = context.get_settings()
ui = context.get_ui()
if ui.get_property('busy').lower() == 'true':
@@ -46,6 +52,9 @@ def run(self, provider, context):
playlist.add_items(items, loads=True)
return False
+ if settings.is_setup_wizard_enabled():
+ provider.run_wizard(context)
+
try:
results = provider.navigate(context)
except KodionException as exc:
@@ -57,32 +66,29 @@ def run(self, provider, context):
xbmcplugin.endOfDirectory(self.handle, succeeded=False)
return False
- self.settings = context.get_settings()
-
result, options = results
-
if isinstance(result, bool):
xbmcplugin.endOfDirectory(self.handle, succeeded=result)
return result
- if isinstance(result, (VideoItem, AudioItem, UriItem)):
- return self._set_resolved_url(context, result)
+ show_fanart = settings.show_fanart()
- show_fanart = self.settings.show_fanart()
+ if isinstance(result, (VideoItem, AudioItem, UriItem)):
+ return self._set_resolved_url(context, result, show_fanart)
if isinstance(result, DirectoryItem):
item_count = 1
- items = [self._add_directory(result, show_fanart)]
+ items = [directory_listitem(context, result, show_fanart)]
elif isinstance(result, (list, tuple)):
item_count = len(result)
items = [
- self._add_directory(item, show_fanart)
+ directory_listitem(context, item, show_fanart)
if isinstance(item, DirectoryItem)
- else self._add_video(context, item)
+ else video_listitem(context, item, show_fanart)
if isinstance(item, VideoItem)
- else self._add_audio(context, item)
+ else audio_listitem(context, item, show_fanart)
if isinstance(item, AudioItem)
- else self._add_image(item, show_fanart)
+ else image_listitem(context, item, show_fanart)
if isinstance(item, ImageItem)
else None
for item in result
@@ -102,7 +108,7 @@ def run(self, provider, context):
)
return succeeded
- def _set_resolved_url(self, context, base_item):
+ def _set_resolved_url(self, context, base_item, show_fanart):
uri = base_item.get_uri()
if base_item.playable:
@@ -112,94 +118,21 @@ def _set_resolved_url(self, context, base_item):
playlist = Playlist('video', context)
ui.set_property('playlist', playlist.get_items(dumps=True))
- item = xbmc_items.to_playback_item(context, base_item)
+ item = playback_item(context, base_item, show_fanart)
xbmcplugin.setResolvedUrl(self.handle,
succeeded=True,
listitem=item)
return True
if context.is_plugin_path(uri):
- context.log_debug('Redirecting to |{0}|'.format(uri))
+ context.log_debug('Redirecting to: |{0}|'.format(uri))
context.execute('RunPlugin({0})'.format(uri))
+ else:
+ context.log_debug('Running script: |{0}|'.format(uri))
+ context.execute('RunScript({0})'.format(uri))
xbmcplugin.endOfDirectory(self.handle,
succeeded=False,
updateListing=False,
cacheToDisc=False)
return False
-
- @staticmethod
- def _add_directory(directory_item, show_fanart=False):
- art = {'icon': 'DefaultFolder.png',
- 'thumb': directory_item.get_image()}
-
- item = xbmcgui.ListItem(label=directory_item.get_name(), offscreen=True)
- item_info = info_labels.create_from_item(directory_item)
- xbmc_items.set_info_tag(item, item_info, 'video')
-
- # only set fanart if enabled
- if show_fanart:
- fanart = directory_item.get_fanart()
- if fanart:
- art['fanart'] = fanart
-
- item.setArt(art)
-
- context_menu = directory_item.get_context_menu()
- if context_menu is not None:
- item.addContextMenuItems(
- context_menu, replaceItems=directory_item.replace_context_menu()
- )
-
- item.setPath(directory_item.get_uri())
-
- is_folder = not directory_item.is_action()
-
- if directory_item.next_page:
- item.setProperty('specialSort', 'bottom')
-
- # make channel_subscription_id property available for keymapping
- subscription_id = directory_item.get_channel_subscription_id()
- if subscription_id:
- item.setProperty('channel_subscription_id', subscription_id)
-
- return directory_item.get_uri(), item, is_folder
-
- @staticmethod
- def _add_video(context, video_item):
- item = xbmc_items.video_listitem(context, video_item)
- item.setPath(video_item.get_uri())
- return video_item.get_uri(), item, False
-
- @staticmethod
- def _add_image(image_item, show_fanart=False):
- art = {'icon': 'DefaultPicture.png',
- 'thumb': image_item.get_image()}
-
- item = xbmcgui.ListItem(label=image_item.get_name(), offscreen=True)
-
- # only set fanart is enabled
- if show_fanart:
- fanart = image_item.get_fanart()
- if fanart:
- art['fanart'] = fanart
-
- item.setArt(art)
-
- context_menu = image_item.get_context_menu()
- if context_menu is not None:
- item.addContextMenuItems(
- context_menu, replaceItems=image_item.replace_context_menu()
- )
-
- item.setInfo(type='picture',
- infoLabels=info_labels.create_from_item(image_item))
-
- item.setPath(image_item.get_uri())
- return image_item.get_uri(), item, False
-
- @staticmethod
- def _add_audio(context, audio_item):
- item = xbmc_items.audio_listitem(context, audio_item)
- item.setPath(audio_item.get_uri())
- return audio_item.get_uri(), item, False
diff --git a/resources/lib/youtube_plugin/kodion/runner.py b/resources/lib/youtube_plugin/kodion/runner.py
index 458fe9e97..9f62065e2 100644
--- a/resources/lib/youtube_plugin/kodion/runner.py
+++ b/resources/lib/youtube_plugin/kodion/runner.py
@@ -11,6 +11,7 @@
from __future__ import absolute_import, division, unicode_literals
import copy
+import platform
import timeit
from . import debug
@@ -33,31 +34,28 @@ def run(provider, context=None):
context = Context()
context.log_debug('Starting Kodion framework by bromix...')
- python_version = 'Unknown version of Python'
- try:
- import platform
-
- python_version = str(platform.python_version())
- python_version = 'Python %s' % python_version
- except:
- # do nothing
- pass
-
- version = context.get_system_version()
- name = context.get_name()
+
addon_version = context.get_version()
+ python_version = 'Python {0}'.format(platform.python_version())
+
redacted = ''
- context_params = copy.deepcopy(context.get_params())
- if 'api_key' in context_params:
- context_params['api_key'] = redacted
- if 'client_id' in context_params:
- context_params['client_id'] = redacted
- if 'client_secret' in context_params:
- context_params['client_secret'] = redacted
-
- context.log_notice('Running: %s (%s) on %s with %s\n\tPath: %s\n\tParams: %s' %
- (name, addon_version, version, python_version,
- context.get_path(), context_params))
+ params = copy.deepcopy(context.get_params())
+ if 'api_key' in params:
+ params['api_key'] = redacted
+ if 'client_id' in params:
+ params['client_id'] = redacted
+ if 'client_secret' in params:
+ params['client_secret'] = redacted
+
+ context.log_notice('Running: {plugin} ({version}) on {kodi} with {python}\n'
+ 'Path: {path}\n'
+ 'Params: {params}'
+ .format(plugin=context.get_name(),
+ version=addon_version,
+ kodi=context.get_system_version(),
+ python=python_version,
+ path=context.get_path(),
+ params=params))
__RUNNER__.run(provider, context)
provider.tear_down(context)
@@ -65,6 +63,10 @@ def run(provider, context=None):
elapsed = timeit.default_timer() - start_time
if __DEBUG_RUNTIME:
- debug.runtime(context, addon_version, elapsed, single_file=__DEBUG_RUNTIME_SINGLE_FILE)
+ debug.runtime(context,
+ addon_version,
+ elapsed,
+ single_file=__DEBUG_RUNTIME_SINGLE_FILE)
- context.log_debug('Shutdown of Kodion after |%s| seconds' % str(round(elapsed, 4)))
+ context.log_debug('Shutdown of Kodion after |{elapsed:.4}| seconds'
+ .format(elapsed=elapsed))
diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py
index 3dac23250..2582a9bb3 100644
--- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py
+++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py
@@ -16,6 +16,11 @@
class AbstractSettings(object):
+ _vars = vars()
+ for name, value in settings.__dict__.items():
+ _vars[name] = value
+ del _vars
+
VALUE_FROM_STR = {
'false': False,
'true': True,
@@ -229,30 +234,6 @@ def api_secret(self, new_secret=None):
return new_secret
return self.get_string(settings.API_SECRET)
- def api_last_hash(self, new_hash=None):
- if new_hash is not None:
- self.set_string(settings.API_LAST_HASH, new_hash)
- return new_hash
- return self.get_string(settings.API_LAST_HASH, '')
-
- def user_access_token(self, new_access_token=None):
- if new_access_token is not None:
- self.set_string(settings.USER_ACCESS_TOKEN, new_access_token)
- return new_access_token
- return self.get_string(settings.USER_ACCESS_TOKEN, '')
-
- def user_refresh_token(self, new_refresh_token=None):
- if new_refresh_token is not None:
- self.set_string(settings.USER_REFRESH_TOKEN, new_refresh_token)
- return new_refresh_token
- return self.get_string(settings.USER_REFRESH_TOKEN, '')
-
- def user_token_expiration(self, new_token_expiration=None):
- if new_token_expiration is not None:
- self.set_int(settings.USER_TOKEN_EXPIRATION, new_token_expiration)
- return new_token_expiration
- return self.get_int(settings.USER_TOKEN_EXPIRATION, -1)
-
def get_location(self):
location = self.get_string(settings.LOCATION, '').replace(' ', '').strip()
coords = location.split(',')
@@ -275,7 +256,7 @@ def set_location(self, value):
self.set_string(settings.LOCATION, value)
def get_location_radius(self):
- return ''.join((str(self.get_int(settings.LOCATION_RADIUS, 500)), 'km'))
+ return ''.join((self.get_int(settings.LOCATION_RADIUS, 500, str), 'km'))
def get_play_count_min_percent(self):
return self.get_int(settings.PLAY_COUNT_MIN_PERCENT, 0)
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 682208539..710e6cef2 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
@@ -73,10 +73,14 @@ def get_bool(self, setting, default=None, echo=None):
error = False
try:
value = bool(self._get_bool(self._type(), setting))
- except (AttributeError, TypeError) as exc:
+ except (TypeError, ValueError) as exc:
error = exc
- value = self.get_string(setting, echo=False)
- value = AbstractSettings.VALUE_FROM_STR.get(value.lower(), default)
+ try:
+ value = self.get_string(setting, echo=False).lower()
+ value = AbstractSettings.VALUE_FROM_STR.get(value, default)
+ except TypeError as exc:
+ error = exc
+ value = default
except RuntimeError as exc:
error = exc
value = default
@@ -93,9 +97,11 @@ def get_bool(self, setting, default=None, echo=None):
def set_bool(self, setting, value, echo=None):
try:
error = not self._set_bool(self._type(), setting, value)
- if not error:
+ if error:
+ error = 'failed'
+ else:
self._cache[setting] = value
- except RuntimeError as exc:
+ except (RuntimeError, TypeError) as exc:
error = exc
if self._echo and echo is not False:
@@ -115,10 +121,10 @@ def get_int(self, setting, default=-1, process=None, echo=None):
value = int(self._get_int(self._type(), setting))
if process:
value = process(value)
- except (AttributeError, TypeError, ValueError) as exc:
+ except (TypeError, ValueError) as exc:
error = exc
- value = self.get_string(setting, echo=False)
try:
+ value = self.get_string(setting, echo=False)
value = int(value)
except (TypeError, ValueError) as exc:
error = exc
@@ -139,9 +145,11 @@ def get_int(self, setting, default=-1, process=None, echo=None):
def set_int(self, setting, value, echo=None):
try:
error = not self._set_int(self._type(), setting, value)
- if not error:
+ if error:
+ error = 'failed'
+ else:
self._cache[setting] = value
- except RuntimeError as exc:
+ except (RuntimeError, TypeError) as exc:
error = exc
if self._echo and echo is not False:
@@ -159,7 +167,7 @@ def get_string(self, setting, default='', echo=None):
error = False
try:
value = self._get_str(self._type(), setting) or default
- except RuntimeError as exc:
+ except (RuntimeError, TypeError) as exc:
error = exc
value = default
@@ -175,9 +183,11 @@ def get_string(self, setting, default='', echo=None):
def set_string(self, setting, value, echo=None):
try:
error = not self._set_str(self._type(), setting, value)
- if not error:
+ if error:
+ error = 'failed'
+ else:
self._cache[setting] = value
- except RuntimeError as exc:
+ except (RuntimeError, TypeError) as exc:
error = exc
if self._echo and echo is not False:
@@ -197,7 +207,7 @@ def get_string_list(self, setting, default=None, echo=None):
value = self._get_str_list(self._type(), setting)
if not value:
value = [] if default is None else default
- except RuntimeError as exc:
+ except (RuntimeError, TypeError) as exc:
error = exc
value = default
@@ -213,9 +223,11 @@ def get_string_list(self, setting, default=None, echo=None):
def set_string_list(self, setting, value, echo=None):
try:
error = not self._set_str_list(self._type(), setting, value)
- if not error:
+ if error:
+ error = 'failed'
+ else:
self._cache[setting] = value
- except RuntimeError as exc:
+ except (RuntimeError, TypeError) as exc:
error = exc
if self._echo and echo is not False:
diff --git a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py
index 880fac4da..9b49ac196 100644
--- a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py
+++ b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py
@@ -9,7 +9,7 @@
from __future__ import absolute_import, division, unicode_literals
-from .storage import Storage
+from .storage import Storage, fromtimestamp
class PlaybackHistory(Storage):
@@ -21,15 +21,17 @@ class PlaybackHistory(Storage):
def __init__(self, filepath):
super(PlaybackHistory, self).__init__(filepath)
- def _add_last_played(self, value, item):
- value['last_played'] = self._convert_timestamp(item[1])
+ @staticmethod
+ def _add_last_played(value, item):
+ value['last_played'] = fromtimestamp(item[1])
return value
- def get_items(self, keys=None):
+ def get_items(self, keys=None, limit=-1):
result = self._get_by_ids(keys,
oldest_first=False,
process=self._add_last_played,
- as_dict=True)
+ as_dict=True,
+ limit=limit)
return result
def get_item(self, key):
diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py
index 3508b45b3..ce76b3bcb 100644
--- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py
+++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py
@@ -14,11 +14,10 @@
import pickle
import sqlite3
import time
-from datetime import datetime
from traceback import format_stack
from ..logger import log_error
-from ..utils.datetime_parser import since_epoch
+from ..utils.datetime_parser import fromtimestamp, since_epoch
from ..utils.methods import make_dirs
@@ -317,7 +316,7 @@ def _set(self, item_id, item):
self._execute(cursor, self._sql['set'], values=values)
def _set_many(self, items, flatten=False):
- now = since_epoch(datetime.now())
+ now = since_epoch()
num_items = len(items)
if flatten:
@@ -368,7 +367,7 @@ def _decode(obj, process=None, item=None):
@staticmethod
def _encode(key, obj, timestamp=None):
- timestamp = timestamp or since_epoch(datetime.now())
+ timestamp = timestamp or since_epoch()
blob = sqlite3.Binary(pickle.dumps(
obj, protocol=pickle.HIGHEST_PROTOCOL
))
@@ -383,7 +382,7 @@ def _get(self, item_id, process=None, seconds=None):
item = result.fetchone() if result else None
if not item:
return None
- cut_off = since_epoch(datetime.now()) - seconds if seconds else 0
+ cut_off = since_epoch() - seconds if seconds else 0
if not cut_off or item[1] >= cut_off:
return self._decode(item[2], process, item)
return None
@@ -402,7 +401,7 @@ def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1,
query = self._sql['get_by_key'].format('?,' * (num_ids - 1) + '?')
item_ids = tuple(item_ids)
- cut_off = since_epoch(datetime.now()) - seconds if seconds else 0
+ cut_off = since_epoch() - seconds if seconds else 0
with self as (db, cursor), db:
result = self._execute(cursor, query, item_ids)
if as_dict:
@@ -418,7 +417,7 @@ def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1,
else:
result = [
(item[0],
- self._convert_timestamp(item[1]),
+ fromtimestamp(item[1]),
self._decode(item[2], process, item))
for item in result if not cut_off or item[1] >= cut_off
]
@@ -434,7 +433,3 @@ def _remove_many(self, item_ids):
with self as (db, cursor), db:
self._execute(cursor, query, tuple(item_ids))
self._execute(cursor, 'VACUUM')
-
- @classmethod
- def _convert_timestamp(cls, val):
- return datetime.fromtimestamp(val)
diff --git a/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py
index 1f5c05769..73e8da332 100644
--- a/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py
+++ b/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py
@@ -8,6 +8,8 @@
See LICENSES/GPL-2.0-only for more information.
"""
+from __future__ import absolute_import, division, unicode_literals
+
class AbstractContextUI(object):
def __init__(self):
@@ -16,9 +18,6 @@ def __init__(self):
def create_progress_dialog(self, heading, text=None, background=False):
raise NotImplementedError()
- def get_skin_id(self):
- raise NotImplementedError()
-
def on_keyboard_input(self, title, default='', hidden=False):
raise NotImplementedError()
diff --git a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py
index dd6eb24f3..83ab3ac57 100644
--- a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py
+++ b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py
@@ -8,6 +8,10 @@
See LICENSES/GPL-2.0-only for more information.
"""
+from __future__ import absolute_import, division, unicode_literals
+
+from ..compatibility import string_type
+
class AbstractProgressDialog(object):
def __init__(self, dialog, heading, text, total=100):
@@ -49,7 +53,7 @@ def update(self, steps=1, text=None):
else:
position = int(100 * self._position / self._total)
- if isinstance(text, str):
+ if isinstance(text, string_type):
self._dialog.update(percent=position, message=text)
else:
self._dialog.update(percent=position)
diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py
index 0a2715bd4..191a4379f 100644
--- a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py
+++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py
@@ -20,10 +20,11 @@ def _process_date_value(info_labels, name, param):
def _process_datetime_value(info_labels, name, param):
- if param:
- info_labels[name] = (param.isoformat('T')
- if current_system_version.compatible(19, 0) else
- param.strftime('%d.%d.%Y'))
+ if not param:
+ return
+ info_labels[name] = (param.replace(microsecond=0, tzinfo=None).isoformat()
+ if current_system_version.compatible(19, 0) else
+ param.strftime('%d.%m.%Y'))
def _process_int_value(info_labels, name, param):
@@ -81,25 +82,12 @@ def _process_mediatype(info_labels, name, param):
info_labels[name] = param
-def _process_last_played(info_labels, name, param):
- if param:
- try:
- info_labels[name] = param.strftime('%Y-%m-%d %H:%M:%S')
- except AttributeError:
- info_labels[name] = param
-
-
def create_from_item(base_item):
info_labels = {}
# 'date' = '1982-03-09' (string)
_process_datetime_value(info_labels, 'date', base_item.get_date())
- # 'count' = 12 (integer)
- # Can be used to store an id for later, or for sorting purposes
- # Used for video view count
- _process_int_value(info_labels, 'count', base_item.get_count())
-
# Directory
if isinstance(base_item, DirectoryItem):
_process_string_value(info_labels, 'plot', base_item.get_plot())
@@ -131,6 +119,11 @@ def create_from_item(base_item):
# play count
_process_int_value(info_labels, 'playcount', base_item.get_play_count())
+ # 'count' = 12 (integer)
+ # Can be used to store an id for later, or for sorting purposes
+ # Used for Youtube video view count
+ _process_int_value(info_labels, 'count', base_item.get_count())
+
# studio
_process_studios(info_labels, 'studio', base_item.get_studio())
@@ -144,7 +137,7 @@ def create_from_item(base_item):
# 'duration' = '3:18' (string)
_process_video_duration(info_labels, base_item.get_duration())
- _process_last_played(info_labels, 'lastplayed', base_item.get_last_played())
+ _process_datetime_value(info_labels, 'lastplayed', base_item.get_last_played())
# 'rating' = 4.5 (float)
_process_video_rating(info_labels, base_item.get_rating())
diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py
index 718805ff0..b6fc6a7e2 100644
--- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py
+++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py
@@ -13,7 +13,7 @@
from .xbmc_progress_dialog import XbmcProgressDialog, XbmcProgressDialogBG
from ..abstract_context_ui import AbstractContextUI
from ...compatibility import xbmc, xbmcgui
-from ...constants import ADDON_ID, ADDON_PATH
+from ...constants import ADDON_ID
from ...utils import to_unicode
@@ -24,27 +24,15 @@ def __init__(self, xbmc_addon, context):
self._xbmc_addon = xbmc_addon
self._context = context
- self._view_mode = None
def create_progress_dialog(self, heading, text=None, background=False):
- if background and self._context.get_system_version().get_version() > (12, 3):
+ if background:
return XbmcProgressDialogBG(heading, text)
return XbmcProgressDialog(heading, text)
- def get_skin_id(self):
- return xbmc.getSkinDir()
def on_keyboard_input(self, title, default='', hidden=False):
- # fallback for Frodo
- if self._context.get_system_version().get_version() <= (12, 3):
- keyboard = xbmc.Keyboard(default, title, hidden)
- keyboard.doModal()
- if keyboard.isConfirmed() and keyboard.getText():
- text = to_unicode(keyboard.getText())
- return True, text
- return False, ''
-
# Starting with Gotham (13.X > ...)
dialog = xbmcgui.Dialog()
result = dialog.input(title, to_unicode(default), type=xbmcgui.INPUT_ALPHANUM)
@@ -137,14 +125,11 @@ def open_settings(self):
self._xbmc_addon.openSettings()
def refresh_container(self):
- xbmc.executebuiltin(
- 'RunScript({path}/resources/lib/youtube_plugin/refresh.py)'
- .format(path=ADDON_PATH)
- )
-
- @staticmethod
- def get_info_label(value):
- return xbmc.getInfoLabel(value)
+ # TODO: find out why the RunScript call is required
+ # xbmc.executebuiltin("Container.Refresh")
+ xbmc.executebuiltin('RunScript({addon_id},action/refresh)'.format(
+ addon_id=ADDON_ID
+ ))
@staticmethod
def set_property(property_id, value):
@@ -220,10 +205,13 @@ def new_line(value=1, cr_before=0, cr_after=0):
))
def set_focus_next_item(self):
- cid = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()
+ list_id = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()
try:
- current_position = int(self.get_info_label('Container.Position')) + 1
- self._context.execute('SetFocus(%s,%s)' % (cid, str(current_position)))
+ position = self._context.get_infolabel('Container.Position')
+ next_position = int(position) + 1
+ self._context.execute('SetFocus({list_id},{position})'.format(
+ list_id=list_id, position=next_position
+ ))
except ValueError:
pass
diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py
index 5ede3e8c1..3cf8fdb7a 100644
--- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py
+++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py
@@ -11,39 +11,12 @@
from __future__ import absolute_import, division, unicode_literals
from . import info_labels
-from ...compatibility import xbmcgui
+from ...compatibility import set_info_tag, xbmcgui
from ...items import AudioItem, UriItem, VideoItem
from ...utils import current_system_version, datetime_parser
-try:
- from infotagger.listitem import set_info_tag
-except ImportError:
- def set_info_tag(listitem, infolabels, tag_type, *_args, **_kwargs):
- listitem.setInfo(tag_type, infolabels)
- return ListItemInfoTag(listitem, tag_type)
-
-
- class ListItemInfoTag(object):
- __slots__ = ('__li__',)
-
- def __init__(self, listitem, *_args, **_kwargs):
- self.__li__ = listitem
-
- def add_stream_info(self, *args, **kwargs):
- return self.__li__.addStreamInfo(*args, **kwargs)
-
- def set_resume_point(self,
- infoproperties,
- resume_key='ResumeTime',
- total_key='TotalTime'):
- if resume_key in infoproperties:
- infoproperties[resume_key] = str(infoproperties[resume_key])
- if total_key in infoproperties:
- infoproperties[total_key] = str(infoproperties[total_key])
-
-
-def video_playback_item(context, video_item):
+def video_playback_item(context, video_item, show_fanart=None):
uri = video_item.get_uri()
context.log_debug('Converting VideoItem |%s|' % uri)
@@ -68,7 +41,6 @@ def video_playback_item(context, video_item):
}
props = {
'isPlayable': str(video_item.playable).lower(),
- 'ForceResolvePlugin': 'true',
}
if (alternative_player
@@ -117,6 +89,7 @@ def video_playback_item(context, video_item):
video_item.set_uri('|'.join((uri, headers)))
list_item = xbmcgui.ListItem(**kwargs)
+
if mime_type:
list_item.setContentLookup(False)
list_item.setMimeType(mime_type)
@@ -136,9 +109,14 @@ def video_playback_item(context, video_item):
if prop_value:
props['TotalTime'] = prop_value
- fanart = settings.show_fanart() and video_item.get_fanart() or ''
- thumb = video_item.get_image() or 'DefaultVideo.png'
- list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart})
+ if show_fanart is None:
+ show_fanart = settings.show_fanart()
+ image = video_item.get_image() or 'DefaultVideo.png'
+ list_item.setArt({
+ 'icon': image,
+ 'fanart': show_fanart and video_item.get_fanart() or '',
+ 'thumb': image,
+ })
if video_item.subtitles:
list_item.setSubtitles(video_item.subtitles)
@@ -156,7 +134,7 @@ def video_playback_item(context, video_item):
return list_item
-def audio_listitem(context, audio_item):
+def audio_listitem(context, audio_item, show_fanart=None):
uri = audio_item.get_uri()
context.log_debug('Converting AudioItem |%s|' % uri)
@@ -173,11 +151,14 @@ def audio_listitem(context, audio_item):
list_item = xbmcgui.ListItem(**kwargs)
- fanart = (context.get_settings().show_fanart()
- and audio_item.get_fanart()
- or '')
- thumb = audio_item.get_image() or 'DefaultAudio.png'
- list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart})
+ if show_fanart is None:
+ show_fanart = context.get_settings().show_fanart()
+ image = audio_item.get_image() or 'DefaultAudio.png'
+ list_item.setArt({
+ 'icon': image,
+ 'fanart': show_fanart and audio_item.get_fanart() or '',
+ 'thumb': image,
+ })
item_info = info_labels.create_from_item(audio_item)
set_info_tag(list_item, item_info, 'music')
@@ -190,7 +171,101 @@ def audio_listitem(context, audio_item):
context_menu, replaceItems=audio_item.replace_context_menu()
)
- return list_item
+ return uri, list_item, False
+
+
+def directory_listitem(context, directory_item, show_fanart=None):
+ uri = directory_item.get_uri()
+ context.log_debug('Converting DirectoryItem |%s|' % uri)
+
+ kwargs = {
+ 'label': directory_item.get_name(),
+ 'path': uri,
+ 'offscreen': True,
+ }
+ props = {
+ 'specialSort': 'bottom' if directory_item.next_page else 'top',
+ 'ForceResolvePlugin': 'true',
+ }
+
+ list_item = xbmcgui.ListItem(**kwargs)
+
+ # make channel_subscription_id property available for keymapping
+ prop_value = directory_item.get_channel_subscription_id()
+ if prop_value:
+ props['channel_subscription_id'] = prop_value
+
+ if show_fanart is None:
+ show_fanart = context.get_settings().show_fanart()
+ image = directory_item.get_image() or 'DefaultFolder.png'
+ list_item.setArt({
+ 'icon': image,
+ 'fanart': show_fanart and directory_item.get_fanart() or '',
+ 'thumb': image,
+ })
+
+ item_info = info_labels.create_from_item(directory_item)
+ set_info_tag(list_item, item_info, 'video')
+
+ """
+ # ListItems that do not open a lower level list should have the isFolder
+ # parameter of the xbmcplugin.addDirectoryItem set to False, however this
+ # now appears to mark the ListItem as playable, even if the IsPlayable
+ # property is not set or set to "false".
+ # Set isFolder to True as a workaround, regardless of whether the ListItem
+ # is actually a folder.
+ is_folder = not directory_item.is_action()
+ """
+ is_folder = True
+
+ list_item.setProperties(props)
+
+ context_menu = directory_item.get_context_menu()
+ if context_menu is not None:
+ list_item.addContextMenuItems(
+ context_menu, replaceItems=directory_item.replace_context_menu()
+ )
+
+ return uri, list_item, is_folder
+
+
+def image_listitem(context, image_item, show_fanart=None):
+ uri = image_item.get_uri()
+ context.log_debug('Converting ImageItem |%s|' % uri)
+
+ kwargs = {
+ 'label': image_item.get_name(),
+ 'path': uri,
+ 'offscreen': True,
+ }
+ props = {
+ 'isPlayable': str(image_item.playable).lower(),
+ 'ForceResolvePlugin': 'true',
+ }
+
+ list_item = xbmcgui.ListItem(**kwargs)
+
+ if show_fanart is None:
+ show_fanart = context.get_settings().show_fanart()
+ image = image_item.get_image() or 'DefaultPicture.png'
+ list_item.setArt({
+ 'icon': image,
+ 'fanart': show_fanart and image_item.get_fanart() or '',
+ 'thumb': image,
+ })
+
+ item_info = info_labels.create_from_item(image_item)
+ set_info_tag(list_item, item_info, 'picture')
+
+ list_item.setProperties(props)
+
+ context_menu = image_item.get_context_menu()
+ if context_menu is not None:
+ list_item.addContextMenuItems(
+ context_menu, replaceItems=image_item.replace_context_menu()
+ )
+
+ return uri, list_item, False
def uri_listitem(context, uri_item):
@@ -212,7 +287,7 @@ def uri_listitem(context, uri_item):
return list_item
-def video_listitem(context, video_item):
+def video_listitem(context, video_item, show_fanart=None):
uri = video_item.get_uri()
context.log_debug('Converting VideoItem |%s|' % uri)
@@ -271,11 +346,14 @@ def video_listitem(context, video_item):
if prop_value:
props['playlist_item_id'] = prop_value
- fanart = (context.get_settings().show_fanart()
- and video_item.get_fanart()
- or '')
- thumb = video_item.get_image() or 'DefaultVideo.png'
- list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart})
+ if show_fanart is None:
+ show_fanart = context.get_settings().show_fanart()
+ image = video_item.get_image()
+ list_item.setArt({
+ 'icon': image or 'DefaultVideo.png',
+ 'fanart': show_fanart and video_item.get_fanart() or '',
+ 'thumb': image,
+ })
if video_item.subtitles:
list_item.setSubtitles(video_item.subtitles)
@@ -296,17 +374,18 @@ def video_listitem(context, video_item):
context_menu, replaceItems=video_item.replace_context_menu()
)
- return list_item
+ return uri, list_item, False
-def to_playback_item(context, base_item):
+def playback_item(context, base_item, show_fanart=None):
if isinstance(base_item, UriItem):
return uri_listitem(context, base_item)
if isinstance(base_item, AudioItem):
- return audio_listitem(context, base_item)
+ _, item, _ = audio_listitem(context, base_item, show_fanart)
+ return item
if isinstance(base_item, VideoItem):
- return video_playback_item(context, base_item)
+ return video_playback_item(context, base_item, show_fanart)
return None
diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py
index 42791ccd7..df1487afe 100644
--- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py
+++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py
@@ -16,96 +16,134 @@
from sys import modules
from ..exceptions import KodionException
-
+from ..logger import log_error
+
+try:
+ from datetime import timezone
+except ImportError:
+ timezone = None
+
+
+__RE_MATCH_TIME_ONLY__ = re.compile(
+ r'^(?P[0-9]{2})(:?(?P[0-9]{2})(:?(?P[0-9]{2}))?)?$'
+)
+__RE_MATCH_DATE_ONLY__ = re.compile(
+ r'^(?P[0-9]{4})[-/.]?(?P[0-9]{2})[-/.]?(?P[0-9]{2})$'
+)
+__RE_MATCH_DATETIME__ = re.compile(
+ r'^(?P[0-9]{4})[-/.]?(?P[0-9]{2})[-/.]?(?P[0-9]{2})'
+ r'["T ](?P[0-9]{2}):?(?P[0-9]{2}):?(?P[0-9]{2})'
+)
+__RE_MATCH_PERIOD__ = re.compile(
+ r'P((?P\d+)Y)?((?P\d+)M)?((?P\d+)D)?'
+ r'(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?'
+)
+__RE_MATCH_ABBREVIATED__ = re.compile(
+ r'\w+, (?P\d+) (?P\w+) (?P\d+)'
+ r' (?P\d+):(?P\d+):(?P\d+)'
+)
+
+__INTERNAL_CONSTANTS__ = {
+ 'epoch_dt': (
+ datetime.fromtimestamp(0, tz=timezone.utc) if timezone
+ else datetime.fromtimestamp(0)
+ ),
+ 'local_offset': None,
+ 'Jan': 1,
+ 'Feb': 2,
+ 'Mar': 3,
+ 'Apr': 4,
+ 'May': 5,
+ 'June': 6,
+ 'Jun': 6,
+ 'July': 7,
+ 'Jul': 7,
+ 'Aug': 8,
+ 'Sept': 9,
+ 'Sep': 9,
+ 'Oct': 10,
+ 'Nov': 11,
+ 'Dec': 12,
+}
now = datetime.now
-
-__RE_MATCH_TIME_ONLY__ = re.compile(r'^(?P[0-9]{2})(:?(?P[0-9]{2})(:?(?P[0-9]{2}))?)?$')
-__RE_MATCH_DATE_ONLY__ = re.compile(r'^(?P[0-9]{4})[-/.]?(?P[0-9]{2})[-/.]?(?P[0-9]{2})$')
-__RE_MATCH_DATETIME__ = re.compile(r'^(?P[0-9]{4})[-/.]?(?P[0-9]{2})[-/.]?(?P[0-9]{2})["T ](?P[0-9]{2}):?(?P[0-9]{2}):?(?P[0-9]{2})')
-__RE_MATCH_PERIOD__ = re.compile(r'P((?P\d+)Y)?((?P\d+)M)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?')
-__RE_MATCH_ABBREVIATED__ = re.compile(r'(\w+), (?P\d+) (?P\w+) (?P\d+) (?P\d+):(?P\d+):(?P\d+)')
-
-__LOCAL_OFFSET__ = now() - datetime.utcnow()
-
-__EPOCH_DT__ = datetime.fromtimestamp(0)
-
-
-def parse(datetime_string, as_utc=True):
- offset = 0 if as_utc else None
-
- def _to_int(value):
- if value is None:
- return 0
- return int(value)
-
- # match time only '00:45:10'
- time_only_match = __RE_MATCH_TIME_ONLY__.match(datetime_string)
- if time_only_match:
- return utc_to_local(
- dt=datetime.combine(
- date.today(),
- dt_time(hour=_to_int(time_only_match.group('hour')),
- minute=_to_int(time_only_match.group('minute')),
- second=_to_int(time_only_match.group('second')))
- ),
- offset=offset
+fromtimestamp = datetime.fromtimestamp
+
+
+def parse(datetime_string):
+ if not datetime_string:
+ return None
+
+ # match time only "00:45:10"
+ match = __RE_MATCH_TIME_ONLY__.match(datetime_string)
+ if match:
+ match = {
+ group: int(value)
+ for group, value in match.groupdict().items()
+ if value
+ }
+ return datetime.combine(
+ date=date.today(),
+ time=dt_time(**match)
).time()
# match date only '2014-11-08'
- date_only_match = __RE_MATCH_DATE_ONLY__.match(datetime_string)
- if date_only_match:
- return utc_to_local(
- dt=datetime(_to_int(date_only_match.group('year')),
- _to_int(date_only_match.group('month')),
- _to_int(date_only_match.group('day'))),
- offset=offset
- )
+ match = __RE_MATCH_DATE_ONLY__.match(datetime_string)
+ if match:
+ match = {
+ group: int(value)
+ for group, value in match.groupdict().items()
+ if value
+ }
+ return datetime(**match)
# full date time
- date_time_match = __RE_MATCH_DATETIME__.match(datetime_string)
- if date_time_match:
- return utc_to_local(
- dt=datetime(_to_int(date_time_match.group('year')),
- _to_int(date_time_match.group('month')),
- _to_int(date_time_match.group('day')),
- _to_int(date_time_match.group('hour')),
- _to_int(date_time_match.group('minute')),
- _to_int(date_time_match.group('second'))),
- offset=offset
- )
+ match = __RE_MATCH_DATETIME__.match(datetime_string)
+ if match:
+ match = {
+ group: int(value)
+ for group, value in match.groupdict().items()
+ if value
+ }
+ return datetime(**match)
# period - at the moment we support only hours, minutes and seconds
# e.g. videos and audio
- period_match = __RE_MATCH_PERIOD__.match(datetime_string)
- if period_match:
- return timedelta(hours=_to_int(period_match.group('hours')),
- minutes=_to_int(period_match.group('minutes')),
- seconds=_to_int(period_match.group('seconds')))
+ match = __RE_MATCH_PERIOD__.match(datetime_string)
+ if match:
+ match = {
+ group: int(value)
+ for group, value in match.groupdict().items()
+ if value
+ }
+ return timedelta(**match)
# abbreviated match
- abbreviated_match = __RE_MATCH_ABBREVIATED__.match(datetime_string)
- if abbreviated_match:
- month = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'June': 6,
- 'Jun': 6, 'July': 7, 'Jul': 7, 'Aug': 8, 'Sept': 9, 'Sep': 9,
- 'Oct': 10, 'Nov': 11, 'Dec': 12}
- return utc_to_local(
- dt=datetime(year=_to_int(abbreviated_match.group('year')),
- month=month[abbreviated_match.group('month')],
- day=_to_int(abbreviated_match.group('day')),
- hour=_to_int(abbreviated_match.group('hour')),
- minute=_to_int(abbreviated_match.group('minute')),
- second=_to_int(abbreviated_match.group('second'))),
- offset=offset
- )
+ match = __RE_MATCH_ABBREVIATED__.match(datetime_string)
+ if match:
+ match = {
+ group: (
+ __INTERNAL_CONSTANTS__.get(value, 0) if group == 'month'
+ else int(value)
+ )
+ for group, value in match.groupdict().items()
+ if value
+ }
+ return datetime(**match)
raise KodionException('Could not parse |{datetime}| as ISO 8601'
.format(datetime=datetime_string))
def get_scheduled_start(context, datetime_object, local=True):
- _now = now() if local else datetime.utcnow()
- if datetime_object.date() == _now:
+ if timezone:
+ _now = now(tz=timezone.utc)
+ if local:
+ _now = _now.astimezone(None)
+ else:
+ _now = now() if local else datetime.utcnow()
+
+ if datetime_object.date() == _now.date():
return '@ {start_time}'.format(
start_time=context.format_time(datetime_object.time())
)
@@ -115,13 +153,27 @@ def get_scheduled_start(context, datetime_object, local=True):
)
-def utc_to_local(dt, offset=None):
- offset = __LOCAL_OFFSET__ if offset is None else timedelta(hours=offset)
+def utc_to_local(dt):
+ if timezone:
+ return dt.astimezone(None)
+
+ if __INTERNAL_CONSTANTS__['local_offset']:
+ offset = __INTERNAL_CONSTANTS__['local_offset']
+ else:
+ offset = now() - datetime.utcnow()
+ __INTERNAL_CONSTANTS__['local_offset'] = offset
+
return dt + offset
-def datetime_to_since(context, dt):
- _now = now()
+def datetime_to_since(context, dt, local=True):
+ if timezone:
+ _now = now(tz=timezone.utc)
+ if local:
+ _now = _now.astimezone(None)
+ else:
+ _now = now() if local else datetime.utcnow()
+
diff = _now - dt
yesterday = _now - timedelta(days=1)
yyesterday = _now - timedelta(days=2)
@@ -176,19 +228,55 @@ def datetime_to_since(context, dt):
return ' '.join((context.format_date_short(dt), context.format_time(dt)))
-def strptime(datetime_str, fmt='%Y-%m-%dT%H:%M:%S'):
- if '.' in datetime_str[-5:]:
- fmt.replace('%S', '%S.%f')
+def strptime(datetime_str, fmt=None):
+ if fmt is None:
+ fmt = '%Y-%m-%dT%H%M%S'
+
+ if ' ' in datetime_str:
+ date_part, time_part = datetime_str.split(' ')
+ elif 'T' in datetime_str:
+ date_part, time_part = datetime_str.split('T')
+
+ if ':' in time_part:
+ time_part = time_part.replace(':', '')
+
+ if '+' in time_part:
+ time_part, offset, timezone_part = time_part.partition('+')
+ elif '-' in time_part:
+ time_part, offset, timezone_part = time_part.partition('+')
else:
- fmt.replace('%S.%f', '%S')
+ offset = timezone_part = ''
+
+ if timezone and timezone_part and offset:
+ fmt = fmt.replace('%S', '%S%z')
+ else:
+ fmt = fmt.replace('%S%z', '%S')
+
+ if '.' in time_part:
+ fmt = fmt.replace('%S', '%S.%f')
+ else:
+ fmt = fmt.replace('%S.%f', '%S')
+
+ if timezone and timezone_part and offset:
+ time_part = offset.join((time_part, timezone_part))
+ datetime_str = 'T'.join((date_part, time_part))
+
+ try:
+ return datetime.strptime(datetime_str, fmt)
+ except TypeError:
+ log_error('Python strptime bug workaround.\n'
+ 'Refer to https://github.com/python/cpython/issues/71587')
- if not datetime.strptime:
- if '_strptime' in modules:
- del modules['_strptime']
- modules['_strptime'] = import_module('_strptime')
+ if '_strptime' not in modules:
+ modules['_strptime'] = import_module('_strptime')
+ _strptime = modules['_strptime']
- return datetime.strptime(datetime_str, fmt)
+ if timezone:
+ return _strptime._strptime_datetime(datetime, datetime_str, fmt)
+ return datetime(*(_strptime._strptime(datetime_str, fmt)[0][0:6]))
-def since_epoch(dt_object):
- return (dt_object - __EPOCH_DT__).total_seconds()
+def since_epoch(dt_object=None):
+ if dt_object is None:
+ dt_object = now(tz=timezone.utc) if timezone else datetime.utcnow()
+ return (dt_object - __INTERNAL_CONSTANTS__['epoch_dt']).total_seconds()
diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py
index cb469ee47..41c040515 100644
--- a/resources/lib/youtube_plugin/kodion/utils/methods.py
+++ b/resources/lib/youtube_plugin/kodion/utils/methods.py
@@ -18,7 +18,7 @@
from datetime import timedelta
from math import floor, log
-from ..compatibility import quote, xbmc, xbmcvfs
+from ..compatibility import quote, string_type, xbmc, xbmcvfs
from ..logger import log_error
@@ -274,15 +274,16 @@ def find_video_id(plugin_path):
return ''
-def friendly_number(number, precision=3, scale=('', 'K', 'M', 'B')):
+def friendly_number(input, precision=3, scale=('', 'K', 'M', 'B'), as_str=True):
_input = float('{input:.{precision}g}'.format(
- input=float(number), precision=precision
+ input=float(input), precision=precision
))
_abs_input = abs(_input)
magnitude = 0 if _abs_input < 1000 else int(log(floor(_abs_input), 1000))
- return '{output:f}'.format(
+ output = '{output:f}'.format(
output=_input / 1000 ** magnitude
- ).rstrip('0').rstrip('.') + scale[magnitude], _input
+ ).rstrip('0').rstrip('.') + scale[magnitude]
+ return output if as_str else (output, _input)
_RE_PERIODS = re.compile(r'([\d.]+)(d|h|m|s|$)')
@@ -314,7 +315,7 @@ def seconds_to_duration(seconds):
def merge_dicts(item1, item2, templates=None, _=Ellipsis):
if not isinstance(item1, dict) or not isinstance(item2, dict):
- return item1 if item2 is _ else item2
+ return item1 if item2 is _ else _ if item2 is KeyError else item2
new = {}
keys = set(item1)
keys.update(item2)
@@ -323,7 +324,7 @@ def merge_dicts(item1, item2, templates=None, _=Ellipsis):
if value is _:
continue
if (templates is not None
- and isinstance(value, str) and '{' in value):
+ and isinstance(value, string_type) and '{' in value):
templates['{0}.{1}'.format(id(new), key)] = (new, key, value)
new[key] = value
return new or _
diff --git a/resources/lib/youtube_plugin/kodion/utils/player_monitor.py b/resources/lib/youtube_plugin/kodion/utils/player_monitor.py
index e3e4f44d2..d2e5fafa8 100644
--- a/resources/lib/youtube_plugin/kodion/utils/player_monitor.py
+++ b/resources/lib/youtube_plugin/kodion/utils/player_monitor.py
@@ -58,6 +58,7 @@ def abort_now(self):
or self.stopped())
def run(self):
+ playing_file = self.playback_json.get('playing_file')
play_count = self.playback_json.get('play_count', 0)
use_remote_history = self.playback_json.get('use_remote_history', False)
use_local_history = self.playback_json.get('use_local_history', False)
@@ -130,8 +131,10 @@ def run(self):
except RuntimeError:
current_file = None
- if (not current_file or video_id_param not in current_file
- or not self._context.is_plugin_path(current_file, 'play/')
+ if (not current_file
+ or (current_file != playing_file and not (
+ self._context.is_plugin_path(current_file, 'play/')
+ and video_id_param in current_file))
or self.stopped()):
self.stop()
break
@@ -278,12 +281,11 @@ def run(self):
})
self._context.log_debug('Playback stopped [{video_id}]:'
' {current:.3f} secs of {total:.3f}'
- ' @ {percent}%'.format(
- video_id=self.video_id,
- current=self.current_time,
- total=self.total_time,
- percent=self.percent_complete,
- ))
+ ' @ {percent}%'
+ .format(video_id=self.video_id,
+ current=self.current_time,
+ total=self.total_time,
+ percent=self.percent_complete))
state = 'stopped'
# refresh client, tokens may need refreshing
@@ -344,10 +346,9 @@ def run(self):
self._context.get_watch_later_list().remove(self.video_id)
if logged_in and not refresh_only:
- history_playlist_id = access_manager.get_watch_history_id()
- if history_playlist_id and history_playlist_id != 'HL':
- _ = client.add_video_to_playlist(history_playlist_id,
- self.video_id)
+ history_id = access_manager.get_watch_history_id()
+ if history_id:
+ _ = client.add_video_to_playlist(history_id, self.video_id)
# rate video
if settings.get_bool('youtube.post.play.rate', False):
@@ -375,13 +376,7 @@ def run(self):
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
do_refresh = playlist.size() < 2 or playlist.getposition() == -1
- if (do_refresh and settings.get_bool('youtube.post.play.refresh', False)
- and not xbmc.getInfoLabel('Container.FolderPath').startswith(
- self._context.create_uri(('kodion', 'search', 'input'))
- )):
- # don't refresh search input it causes request for new input,
- # (Container.Update in abstract_provider /kodion/search/input/
- # would resolve this but doesn't work with Remotes(Yatse))
+ if do_refresh and settings.get_bool('youtube.post.play.refresh', False):
self.ui.refresh_container()
self.end()
diff --git a/resources/lib/youtube_plugin/kodion/utils/system_version.py b/resources/lib/youtube_plugin/kodion/utils/system_version.py
index b758dacb5..b38e3fa9e 100644
--- a/resources/lib/youtube_plugin/kodion/utils/system_version.py
+++ b/resources/lib/youtube_plugin/kodion/utils/system_version.py
@@ -12,7 +12,7 @@
import json
-from ..compatibility import xbmc
+from ..compatibility import string_type, xbmc
class SystemVersion(object):
@@ -23,12 +23,12 @@ def __init__(self, version=None, releasename=None, appname=None):
)
self._releasename = (
- releasename if releasename and isinstance(releasename, str)
+ releasename if releasename and isinstance(releasename, string_type)
else 'UNKNOWN'
)
self._appname = (
- appname if appname and isinstance(appname, str)
+ appname if appname and isinstance(appname, string_type)
else 'UNKNOWN'
)
diff --git a/resources/lib/youtube_plugin/refresh.py b/resources/lib/youtube_plugin/refresh.py
deleted file mode 100644
index 981231c3a..000000000
--- a/resources/lib/youtube_plugin/refresh.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-
- Copyright (C) 2018-2018 plugin.video.youtube
-
- SPDX-License-Identifier: GPL-2.0-only
- See LICENSES/GPL-2.0-only for more information.
-"""
-
-from __future__ import absolute_import, division, unicode_literals
-
-import xbmc
-
-
-if __name__ == '__main__':
- xbmc.executebuiltin("Container.Refresh")
diff --git a/resources/lib/youtube_plugin/script.py b/resources/lib/youtube_plugin/script.py
new file mode 100644
index 000000000..3ff79f8df
--- /dev/null
+++ b/resources/lib/youtube_plugin/script.py
@@ -0,0 +1,292 @@
+# -*- coding: utf-8 -*-
+"""
+
+ Copyright (C) 2018-2018 plugin.video.youtube
+
+ SPDX-License-Identifier: GPL-2.0-only
+ See LICENSES/GPL-2.0-only for more information.
+"""
+
+from __future__ import absolute_import, division, unicode_literals
+
+import os
+import socket
+import sys
+
+from kodion.compatibility import parse_qsl, xbmc, xbmcaddon, xbmcvfs
+from kodion.constants import DATA_PATH, TEMP_PATH
+from kodion.context import Context
+from kodion.network import get_client_ip_address, is_httpd_live
+from kodion.utils import rm_dir
+
+
+def _config_actions(action, *_args):
+ context = Context()
+ localize = context.localize
+ settings = context.get_settings()
+ ui = context.get_ui()
+
+ if action == 'youtube':
+ xbmcaddon.Addon().openSettings()
+ xbmc.executebuiltin('Container.Refresh')
+
+ elif action == 'isa':
+ if context.use_inputstream_adaptive():
+ xbmcaddon.Addon(id='inputstream.adaptive').openSettings()
+ else:
+ settings.set_bool('kodion.video.quality.isa', False)
+
+ elif action == 'inputstreamhelper':
+ try:
+ xbmcaddon.Addon('script.module.inputstreamhelper')
+ ui.show_notification(localize('inputstreamhelper.is_installed'))
+ except RuntimeError:
+ xbmc.executebuiltin('InstallAddon(script.module.inputstreamhelper)')
+
+ elif action == 'subtitles':
+ language = settings.get_string('youtube.language', 'en-US')
+ sub_setting = settings.subtitle_languages()
+
+ sub_opts = [
+ localize('none'),
+ localize('prompt'),
+ (localize('subtitles.with_fallback') % (
+ ('en', 'en-US/en-GB') if language.startswith('en') else
+ (language, 'en')
+ )),
+ language,
+ '%s (%s)' % (language, localize('subtitles.no_auto_generated'))
+ ]
+ sub_opts[sub_setting] = ui.bold(sub_opts[sub_setting])
+
+ result = ui.on_select(localize('subtitles.language'), sub_opts)
+ if result > -1:
+ settings.set_subtitle_languages(result)
+
+ result = ui.on_yes_no_input(
+ localize('subtitles.download'),
+ localize('subtitles.download.pre')
+ )
+ if result > -1:
+ settings.set_subtitle_download(result == 1)
+
+ elif action == 'listen_ip':
+ local_ranges = ('10.', '172.16.', '192.168.')
+ addresses = [iface[4][0]
+ for iface in socket.getaddrinfo(socket.gethostname(), None)
+ if iface[4][0].startswith(local_ranges)]
+ addresses += ['127.0.0.1', '0.0.0.0']
+ selected_address = ui.on_select(localize('select.listen.ip'), addresses)
+ if selected_address != -1:
+ settings.set_httpd_listen(addresses[selected_address])
+
+ elif action == 'show_client_ip':
+ port = settings.httpd_port()
+
+ if is_httpd_live(port=port):
+ client_ip = get_client_ip_address(port=port)
+ if client_ip:
+ ui.on_ok(context.get_name(),
+ context.localize('client.ip') % client_ip)
+ else:
+ ui.show_notification(context.localize('client.ip.failed'))
+ else:
+ ui.show_notification(context.localize('httpd.not.running'))
+
+
+def _maintenance_actions(action, target):
+ context = Context()
+ ui = context.get_ui()
+ localize = context.localize
+
+ if action == 'clear':
+ if target == 'function_cache':
+ if ui.on_remove_content(localize('cache.function')):
+ context.get_function_cache().clear()
+ ui.show_notification(localize('succeeded'))
+ elif target == 'data_cache':
+ if ui.on_remove_content(localize('cache.data')):
+ context.get_data_cache().clear()
+ ui.show_notification(localize('succeeded'))
+ elif target == 'search_cache':
+ if ui.on_remove_content(localize('search.history')):
+ context.get_search_history().clear()
+ ui.show_notification(localize('succeeded'))
+ elif (target == 'playback_history' and ui.on_remove_content(
+ localize('playback.history')
+ )):
+ context.get_playback_history().clear()
+ ui.show_notification(localize('succeeded'))
+
+ elif action == 'delete':
+ _maint_files = {'function_cache': 'cache.sqlite',
+ 'search_cache': 'search.sqlite',
+ 'data_cache': 'data_cache.sqlite',
+ 'playback_history': 'playback_history',
+ 'settings_xml': 'settings.xml',
+ 'api_keys': 'api_keys.json',
+ 'access_manager': 'access_manager.json',
+ 'temp_files': TEMP_PATH}
+ _file = _maint_files.get(target)
+ succeeded = False
+
+ if not _file:
+ return
+
+ data_path = xbmcvfs.translatePath(DATA_PATH)
+ if 'sqlite' in _file:
+ _file_w_path = os.path.join(data_path, 'kodion', _file)
+ elif target == 'temp_files':
+ _file_w_path = _file
+ elif target == 'playback_history':
+ _file = ''.join((
+ context.get_access_manager().get_current_user_id(),
+ '.sqlite'
+ ))
+ _file_w_path = os.path.join(data_path, 'playback', _file)
+ else:
+ _file_w_path = os.path.join(data_path, _file)
+
+ if not ui.on_delete_content(_file):
+ return
+
+ if target == 'temp_files':
+ succeeded = rm_dir(_file_w_path)
+
+ elif _file_w_path:
+ succeeded = xbmcvfs.delete(_file_w_path)
+
+ if succeeded:
+ ui.show_notification(localize('succeeded'))
+ else:
+ ui.show_notification(localize('failed'))
+
+
+def _user_actions(action, params):
+ context = Context()
+ if params:
+ context.parse_params(dict(parse_qsl(params)))
+ localize = context.localize
+ access_manager = context.get_access_manager()
+ ui = context.get_ui()
+
+ def select_user(reason, new_user=False):
+ current_users = access_manager.get_users()
+ current_user = access_manager.get_current_user()
+ usernames = []
+ for user, details in sorted(current_users.items()):
+ username = details.get('name') or localize('user.unnamed')
+ if user == current_user:
+ username = '> ' + ui.bold(username)
+ if details.get('access_token') or details.get('refresh_token'):
+ username = ui.color('limegreen', username)
+ usernames.append(username)
+ if new_user:
+ usernames.append(ui.italic(localize('user.new')))
+ return ui.on_select(reason, usernames), sorted(current_users.keys())
+
+ def add_user():
+ results = ui.on_keyboard_input(localize('user.enter_name'))
+ if results[0] is False:
+ return None, None
+ new_username = results[1].strip()
+ if not new_username:
+ new_username = localize('user.unnamed')
+ return access_manager.add_user(new_username)
+
+ def switch_to_user(user):
+ access_manager.set_user(user, switch_to=True)
+ ui.show_notification(
+ localize('user.changed') % access_manager.get_username(user),
+ localize('user.switch')
+ )
+ context.get_data_cache().clear()
+ context.get_function_cache().clear()
+ if context.get_param('refresh') is not False:
+ ui.refresh_container()
+
+ if action == 'switch':
+ result, user_index_map = select_user(localize('user.switch'),
+ new_user=True)
+ if result == -1:
+ return False
+ if result == len(user_index_map):
+ user, _ = add_user()
+ else:
+ user = user_index_map[result]
+
+ if user is not None and user != access_manager.get_current_user():
+ switch_to_user(user)
+
+ elif action == 'add':
+ user, details = add_user()
+ if user is not None:
+ result = ui.on_yes_no_input(
+ localize('user.switch'),
+ localize('user.switch.now') % details.get('name')
+ )
+ if result:
+ switch_to_user(user)
+
+ elif action == 'remove':
+ result, user_index_map = select_user(localize('user.remove'))
+ if result == -1:
+ return False
+
+ user = user_index_map[result]
+ username = access_manager.get_username(user)
+ if ui.on_remove_content(username):
+ access_manager.remove_user(user)
+ if user == 0:
+ access_manager.add_user(username=localize('user.default'),
+ user=0)
+ if user == access_manager.get_current_user():
+ access_manager.set_user(0, switch_to=True)
+ ui.show_notification(localize('removed') % username,
+ localize('remove'))
+
+ elif action == 'rename':
+ result, user_index_map = select_user(localize('user.rename'))
+ if result == -1:
+ return False
+
+ user = user_index_map[result]
+ old_username = access_manager.get_username(user)
+ results = ui.on_keyboard_input(localize('user.enter_name'),
+ default=old_username)
+ if results[0] is False:
+ return False
+ new_username = results[1].strip()
+ if not new_username:
+ new_username = localize('user.unnamed')
+ if old_username == new_username:
+ return False
+
+ if access_manager.set_username(user, new_username):
+ ui.show_notification(
+ localize('renamed') % (old_username, new_username),
+ localize('rename')
+ )
+
+ return True
+
+
+if __name__ == '__main__':
+ args = sys.argv[1:]
+ if args:
+ args = args[0].split('/')
+ num_args = len(args)
+ category = args[0] if num_args else None
+ action = args[1] if num_args > 1 else None
+ params = args[2] if num_args > 2 else None
+
+ if not category:
+ xbmcaddon.Addon().openSettings()
+ elif action == 'refresh':
+ xbmc.executebuiltin('Container.Refresh')
+ elif category == 'config':
+ _config_actions(action, params)
+ elif category == 'maintenance':
+ _maintenance_actions(action, params)
+ elif category == 'users':
+ _user_actions(action, params)
diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py
index ca4652c42..f23853dda 100644
--- a/resources/lib/youtube_plugin/youtube/client/login_client.py
+++ b/resources/lib/youtube_plugin/youtube/client/login_client.py
@@ -89,6 +89,7 @@ def _response_hook(**kwargs):
try:
json_data = response.json()
if 'error' in json_data:
+ json_data.setdefault('code', response.status_code)
raise LoginException('"error" in response JSON data',
json_data=json_data,
response=response)
@@ -162,9 +163,10 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''):
config_type = self._get_config_type(client_id, client_secret)
client = ''.join([
- '(config_type: |', config_type, '|',
- ' client_id: |', client_id[:5], '...|',
- ' client_secret: |', client_secret[:5], '...|)'
+ '(config_type: |', config_type,
+ '| client_id: |', client_id[:5], '...', client_id[-5:],
+ '| client_secret: |', client_secret[:5], '...', client_secret[-5:],
+ '|)'
])
log_debug('Refresh token for {0}'.format(client))
@@ -176,7 +178,7 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''):
error_hook=LoginClient._error_hook,
error_title='Login Failed',
error_info=('Refresh token failed'
- ' {client}: {{exc}}'
+ ' {client}:\n{{exc}}'
.format(client=client)),
raise_exc=True)
@@ -210,9 +212,10 @@ def request_access_token(self, code, client_id='', client_secret=''):
config_type = self._get_config_type(client_id, client_secret)
client = ''.join([
- '(config_type: |', config_type, '|',
- ' client_id: |', client_id[:5], '...|',
- ' client_secret: |', client_secret[:5], '...|)'
+ '(config_type: |', config_type,
+ '| client_id: |', client_id[:5], '...', client_id[-5:],
+ '| client_secret: |', client_secret[:5], '...', client_secret[-5:],
+ '|)'
])
log_debug('Requesting access token for {0}'.format(client))
@@ -224,7 +227,7 @@ def request_access_token(self, code, client_id='', client_secret=''):
error_hook=LoginClient._error_hook,
error_title='Login Failed: Unknown response',
error_info=('Access token request failed'
- ' {client}: {{exc}}'
+ ' {client}:\n{{exc}}'
.format(client=client)),
raise_exc=True)
return json_data
@@ -260,7 +263,7 @@ def request_device_and_user_code(self, client_id=''):
error_hook=LoginClient._error_hook,
error_title='Login Failed: Unknown response',
error_info=('Device/user code request failed'
- ' {client}: {{exc}}'
+ ' {client}:\n{{exc}}'
.format(client=client)),
raise_exc=True)
return json_data
diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py
index 3fd740386..442178cef 100644
--- a/resources/lib/youtube_plugin/youtube/client/request_client.py
+++ b/resources/lib/youtube_plugin/youtube/client/request_client.py
@@ -264,32 +264,41 @@ def __init__(self, exc_type=None):
elif exc_type:
exc_type = (YouTubeException, exc_type)
else:
- exc_type = YouTubeException
+ exc_type = (YouTubeException,)
super(YouTubeRequestClient, self).__init__(exc_type=exc_type)
- self._access_token = None
- self.video_id = None
-
- @staticmethod
- def json_traverse(json_data, path):
+ @classmethod
+ def json_traverse(cls, json_data, path):
if not json_data or not path:
return None
result = json_data
- for keys in path:
- is_dict = isinstance(result, dict)
- if not is_dict and not isinstance(result, (list, tuple)):
+ for idx, keys in enumerate(path):
+ if not isinstance(result, (dict, list, tuple)):
return None
+ if isinstance(keys, slice):
+ return [
+ cls.json_traverse(part, path[idx + 1:])
+ for part in result[keys]
+ if part
+ ]
+
if not isinstance(keys, (list, tuple)):
keys = [keys]
+
for key in keys:
- if is_dict:
- if key not in result:
- continue
- elif not isinstance(key, int) or len(result) <= key:
+ if isinstance(key, (list, tuple)):
+ new_result = cls.json_traverse(result, key)
+ if new_result:
+ result = new_result
+ break
+ continue
+
+ try:
+ result = result[key]
+ except (KeyError, IndexError):
continue
- result = result[key]
break
else:
return None
@@ -298,18 +307,18 @@ def json_traverse(json_data, path):
return None
return result
- def build_client(self, client_name, auth_header=False, data=None):
+ @classmethod
+ def build_client(cls, client_name, data=None):
templates = {}
- client = (self.CLIENTS.get(client_name) or self.CLIENTS['web']).copy()
- client = merge_dicts(self.CLIENTS['_common'], client, templates)
-
+ client = (cls.CLIENTS.get(client_name)
+ or YouTubeRequestClient.CLIENTS['web']).copy()
if data:
- client.update(data)
- client['json']['videoId'] = self.video_id
- if auth_header and self._access_token:
- client['_access_token'] = self._access_token
- client['params'] = None
+ client = merge_dicts(client, data)
+ client = merge_dicts(cls.CLIENTS['_common'], client, templates)
+
+ if data and '_access_token' in data:
+ del client['params']['key']
elif 'Authorization' in client['headers']:
del client['headers']['Authorization']
diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py
index f6f9317b5..ccb49226f 100644
--- a/resources/lib/youtube_plugin/youtube/client/youtube.py
+++ b/resources/lib/youtube_plugin/youtube/client/youtube.py
@@ -10,18 +10,66 @@
from __future__ import absolute_import, division, unicode_literals
-import copy
-import re
import threading
import xml.etree.ElementTree as ET
+from copy import deepcopy
+from itertools import chain, islice
+from random import randint
from .login_client import LoginClient
from ..helper.video_info import VideoInfo
from ..youtube_exceptions import InvalidJSON, YouTubeException
+from ...kodion.compatibility import string_type
from ...kodion.utils import datetime_parser, strip_html_from_text, to_unicode
class YouTube(LoginClient):
+ CLIENTS = {
+ 1: {
+ 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}',
+ 'method': None,
+ 'json': {
+ 'context': {
+ 'client': {
+ 'clientName': 'WEB',
+ 'clientVersion': '2.20220801.00.00',
+ },
+ },
+ },
+ 'headers': {
+ 'Host': 'www.youtube.com',
+ },
+ 'params': {
+ 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
+ },
+ },
+ 3: {
+ 'url': 'https://www.googleapis.com/youtube/v3/{_endpoint}',
+ 'method': None,
+ 'headers': {
+ 'Host': 'www.googleapis.com',
+ },
+ },
+ '_common': {
+ '_access_token': None,
+ 'headers': {
+ 'Accept-Encoding': 'gzip, deflate',
+ 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'Accept': '*/*',
+ 'Accept-Language': 'en-US,en;q=0.5',
+ 'Authorization': 'Bearer {_access_token}',
+ 'DNT': '1',
+ 'User-Agent': ('Mozilla/5.0 (Linux; Android 10; SM-G981B)'
+ ' AppleWebKit/537.36 (KHTML, like Gecko)'
+ ' Chrome/80.0.3987.162 Mobile Safari/537.36'),
+ },
+ 'params': {
+ 'key': None,
+ 'prettyPrint': 'false'
+ },
+ },
+ }
+
def __init__(self, context, **kwargs):
self._context = context
if not kwargs.get('config'):
@@ -153,10 +201,10 @@ def get_video_streams(self, context, video_id):
def remove_playlist(self, playlist_id, **kwargs):
params = {'id': playlist_id,
'mine': 'true'}
- return self.perform_v3_request(method='DELETE',
- path='playlists',
- params=params,
- **kwargs)
+ return self.api_request(method='DELETE',
+ path='playlists',
+ params=params,
+ **kwargs)
def get_supported_languages(self, language=None, **kwargs):
_language = language
@@ -165,10 +213,10 @@ def get_supported_languages(self, language=None, **kwargs):
_language = _language.replace('-', '_')
params = {'part': 'snippet',
'hl': _language}
- return self.perform_v3_request(method='GET',
- path='i18nLanguages',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='i18nLanguages',
+ params=params,
+ **kwargs)
def get_supported_regions(self, language=None, **kwargs):
_language = language
@@ -177,10 +225,10 @@ def get_supported_regions(self, language=None, **kwargs):
_language = _language.replace('-', '_')
params = {'part': 'snippet',
'hl': _language}
- return self.perform_v3_request(method='GET',
- path='i18nRegions',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='i18nRegions',
+ params=params,
+ **kwargs)
def rename_playlist(self,
playlist_id,
@@ -192,32 +240,32 @@ def rename_playlist(self,
'id': playlist_id,
'snippet': {'title': new_title},
'status': {'privacyStatus': privacy_status}}
- return self.perform_v3_request(method='PUT',
- path='playlists',
- params=params,
- post_data=post_data,
- **kwargs)
+ return self.api_request(method='PUT',
+ path='playlists',
+ params=params,
+ post_data=post_data,
+ **kwargs)
def create_playlist(self, title, privacy_status='private', **kwargs):
params = {'part': 'snippet,status'}
post_data = {'kind': 'youtube#playlist',
'snippet': {'title': title},
'status': {'privacyStatus': privacy_status}}
- return self.perform_v3_request(method='POST',
- path='playlists',
- params=params,
- post_data=post_data,
- **kwargs)
+ return self.api_request(method='POST',
+ path='playlists',
+ params=params,
+ post_data=post_data,
+ **kwargs)
def get_video_rating(self, video_id, **kwargs):
- if not isinstance(video_id, str):
+ if not isinstance(video_id, string_type):
video_id = ','.join(video_id)
params = {'id': video_id}
- return self.perform_v3_request(method='GET',
- path='videos/getRating',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='videos/getRating',
+ params=params,
+ **kwargs)
def rate_video(self, video_id, rating='like', **kwargs):
"""
@@ -228,10 +276,10 @@ def rate_video(self, video_id, rating='like', **kwargs):
"""
params = {'id': video_id,
'rating': rating}
- return self.perform_v3_request(method='POST',
- path='videos/rate',
- params=params,
- **kwargs)
+ return self.api_request(method='POST',
+ path='videos/rate',
+ params=params,
+ **kwargs)
def add_video_to_playlist(self, playlist_id, video_id, **kwargs):
params = {'part': 'snippet',
@@ -240,11 +288,11 @@ def add_video_to_playlist(self, playlist_id, video_id, **kwargs):
'snippet': {'playlistId': playlist_id,
'resourceId': {'kind': 'youtube#video',
'videoId': video_id}}}
- return self.perform_v3_request(method='POST',
- path='playlistItems',
- params=params,
- post_data=post_data,
- **kwargs)
+ return self.api_request(method='POST',
+ path='playlistItems',
+ params=params,
+ post_data=post_data,
+ **kwargs)
# noinspection PyUnusedLocal
def remove_video_from_playlist(self,
@@ -252,28 +300,28 @@ def remove_video_from_playlist(self,
playlist_item_id,
**kwargs):
params = {'id': playlist_item_id}
- return self.perform_v3_request(method='DELETE',
- path='playlistItems',
- params=params,
- **kwargs)
+ return self.api_request(method='DELETE',
+ path='playlistItems',
+ params=params,
+ **kwargs)
def unsubscribe(self, subscription_id, **kwargs):
params = {'id': subscription_id}
- return self.perform_v3_request(method='DELETE',
- path='subscriptions',
- params=params,
- **kwargs)
+ return self.api_request(method='DELETE',
+ path='subscriptions',
+ params=params,
+ **kwargs)
def subscribe(self, channel_id, **kwargs):
params = {'part': 'snippet'}
post_data = {'kind': 'youtube#subscription',
'snippet': {'resourceId': {'kind': 'youtube#channel',
'channelId': channel_id}}}
- return self.perform_v3_request(method='POST',
- path='subscriptions',
- params=params,
- post_data=post_data,
- **kwargs)
+ return self.api_request(method='POST',
+ path='subscriptions',
+ params=params,
+ post_data=post_data,
+ **kwargs)
def get_subscription(self,
channel_id,
@@ -297,10 +345,10 @@ def get_subscription(self,
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='subscriptions',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='subscriptions',
+ params=params,
+ **kwargs)
def get_guide_category(self, guide_category_id, page_token='', **kwargs):
params = {'part': 'snippet,contentDetails,brandingSettings',
@@ -310,10 +358,10 @@ def get_guide_category(self, guide_category_id, page_token='', **kwargs):
'hl': self._language}
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='channels',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='channels',
+ params=params,
+ **kwargs)
def get_guide_categories(self, page_token='', **kwargs):
params = {'part': 'snippet',
@@ -323,12 +371,12 @@ def get_guide_categories(self, page_token='', **kwargs):
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='guideCategories',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='guideCategories',
+ params=params,
+ **kwargs)
- def get_popular_videos(self, page_token='', **kwargs):
+ def get_trending_videos(self, page_token='', **kwargs):
params = {'part': 'snippet,status',
'maxResults': str(self._max_results),
'regionCode': self._region,
@@ -336,10 +384,10 @@ def get_popular_videos(self, page_token='', **kwargs):
'chart': 'mostPopular'}
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='videos',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='videos',
+ params=params,
+ **kwargs)
def get_video_category(self, video_category_id, page_token='', **kwargs):
params = {'part': 'snippet,contentDetails,status',
@@ -350,10 +398,10 @@ def get_video_category(self, video_category_id, page_token='', **kwargs):
'hl': self._language}
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='videos',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='videos',
+ params=params,
+ **kwargs)
def get_video_categories(self, page_token='', **kwargs):
params = {'part': 'snippet',
@@ -363,135 +411,381 @@ def get_video_categories(self, page_token='', **kwargs):
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='videoCategories',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='videoCategories',
+ params=params,
+ **kwargs)
- def _get_recommendations_for_home(self):
- # YouTube has deprecated this API, so use history and related items to form
- # a recommended set. We cache aggressively because searches incur a high
- # quota cost of 100 on the YouTube API.
- # Note this is a first stab attempt and can be refined a lot more.
+ def get_recommended_for_home(self,
+ visitor='',
+ page_token='',
+ click_tracking=''):
payload = {
'kind': 'youtube#activityListResponse',
'items': []
}
- history_id = self._context.get_access_manager().get_watch_history_id()
- if not history_id or history_id == 'HL':
+ post_data = {'browseId': 'FEwhat_to_watch'}
+ if page_token:
+ post_data['continuation'] = page_token
+ if click_tracking or visitor:
+ context = {}
+ if click_tracking:
+ context['clickTracking'] = {
+ 'clickTrackingParams': click_tracking,
+ }
+ if visitor:
+ context['client'] = {
+ 'visitorData': visitor,
+ }
+ post_data['context'] = context
+
+ result = self.api_request(version=1,
+ method='POST',
+ path='browse',
+ post_data=post_data)
+ if not result:
return payload
- cache = self._context.get_data_cache()
+ recommended_videos = self.json_traverse(
+ result,
+ path=(
+ (
+ (
+ 'onResponseReceivedEndpoints',
+ 'onResponseReceivedActions',
+ ),
+ 0,
+ 'appendContinuationItemsAction',
+ 'continuationItems',
+ ) if page_token else (
+ 'contents',
+ 'twoColumnBrowseResultsRenderer',
+ 'tabs',
+ 0,
+ 'tabRenderer',
+ 'content',
+ 'richGridRenderer',
+ 'contents',
+ )
+ ) + (
+ slice(None),
+ (
+ (
+ 'richItemRenderer',
+ 'content',
+ 'videoRenderer',
+ # 'videoId',
+ ),
+ (
+ 'richSectionRenderer',
+ 'content',
+ 'richShelfRenderer',
+ 'contents',
+ slice(None),
+ 'richItemRenderer',
+ 'content',
+ (
+ 'videoRenderer',
+ 'reelItemRenderer'
+ ),
+ # 'videoId',
+ ),
+ (
+ 'continuationItemRenderer',
+ 'continuationEndpoint',
+ ),
+ ),
+ )
+ )
+ if not recommended_videos:
+ return payload
- # Do we have a cached result?
- cache_home_key = 'get-activities-home'
- cached = cache.get_item(cache_home_key, cache.ONE_HOUR * 4)
- cached = cached and cached.get('items')
- if cached:
- return cached
+ v3_response = {
+ 'kind': 'youtube#activityListResponse',
+ 'items': [
+ {
+ 'kind': "youtube#video",
+ 'id': video['videoId'],
+ 'partial': True,
+ 'snippet': {
+ 'title': self.json_traverse(video, (
+ ('title', 'runs', 0, 'text'),
+ ('headline', 'simpleText'),
+ )),
+ 'thumbnails': dict(zip(
+ ('default', 'high'),
+ video['thumbnail']['thumbnails'],
+ )),
+ 'channelId': self.json_traverse(video, (
+ ('longBylineText', 'shortBylineText'),
+ 'runs',
+ 0,
+ 'navigationEndpoint',
+ 'browseEndpoint',
+ 'browseId',
+ )),
+ }
+ }
+ for videos in recommended_videos
+ for video in
+ (videos if isinstance(videos, list) else (videos,))
+ if video and 'videoId' in video
+ ]
+ }
- # Fetch existing list of items, if any
- cache_items_key = 'get-activities-home-items'
- cached = cache.get_item(cache_items_key, cache.ONE_WEEK * 2)
- items = cached if cached else []
-
- # Fetch history and recommended items. Use threads for faster execution.
- def helper(video_id, responses):
- self._context.log_debug(
- 'Method get_activities: doing expensive API fetch for related'
- 'items for video %s' % video_id
- )
- di = self.get_related_videos(video_id, max_results=10)
- if 'items' in di:
- # Record for which video we fetched the items
- for item in di['items']:
- item['plugin_fetched_for'] = video_id
- responses.extend(di['items'])
+ last_item = recommended_videos[-1]
+ if last_item and 'continuationCommand' in last_item:
+ if 'clickTrackingParams' in last_item:
+ v3_response['clickTracking'] = last_item['clickTrackingParams']
+ token = last_item['continuationCommand'].get('token')
+ if token:
+ v3_response['nextPageToken'] = token
+ visitor = self.json_traverse(result, (
+ 'responseContext',
+ 'visitorData',
+ )) or visitor
+ if visitor:
+ v3_response['visitorData'] = visitor
+
+ return v3_response
+
+ def get_related_for_home(self, page_token=''):
+ """
+ YouTube has deprecated this API, so we use history and related items to
+ form a recommended set.
+ We cache aggressively because searches can be slow.
+ Note this is a naive implementation and can be refined a lot more.
+ """
- history = self.get_playlist_items(history_id, max_results=50)
+ payload = {
+ 'kind': 'youtube#activityListResponse',
+ 'items': []
+ }
- if not history.get('items'):
- return payload
+ # Related videos are retrieved for the following num_items from history
+ num_items = 10
+ local_history = self._context.get_settings().use_local_history()
+ history_id = self._context.get_access_manager().get_watch_history_id()
+ if not history_id:
+ if local_history:
+ history = self._context.get_playback_history()
+ video_ids = history.get_items(limit=num_items)
+ else:
+ return payload
+ else:
+ history = self.get_playlist_items(history_id, max_results=num_items)
+ if history and 'items' in history:
+ history_items = history['items'] or []
+ video_ids = []
+ else:
+ return payload
- threads = []
- candidates = []
- already_fetched_for_video_ids = [item['plugin_fetched_for'] for item in items]
- history_items = [item for item in history['items']
- if re.match(r'(?P[\w-]{11})',
- item['snippet']['resourceId']['videoId'])]
+ for item in history_items:
+ try:
+ video_ids.append(item['snippet']['resourceId']['videoId'])
+ except KeyError:
+ continue
- # TODO:
- # It would be nice to make this 8 user configurable
- for item in history_items[:8]:
- video_id = item['snippet']['resourceId']['videoId']
- if video_id not in already_fetched_for_video_ids:
- thread = threading.Thread(target=helper, args=(video_id, candidates))
- threads.append(thread)
- thread.start()
+ # Fetch existing list of items, if any
+ cache = self._context.get_data_cache()
+ cache_items_key = 'get-activities-home-items'
+ cached = cache.get_item(cache_items_key, None) or []
+
+ # Increase value to recursively retrieve recommendations for the first
+ # recommended video, up to the set maximum recursion depth
+ max_depth = 2
+ items_per_page = self._max_results
+ diversity_limits = items_per_page // (num_items * max_depth)
+ items = [[] for _ in range(max_depth * len(video_ids))]
+ counts = {
+ '_counter': 0,
+ '_pages': {},
+ '_related': {},
+ }
- for thread in threads:
- thread.join()
+ def index_items(items, index,
+ item_store=None,
+ original_ids=None,
+ group=None,
+ depth=1,
+ original_related=None,
+ original_channel=None):
+ if original_ids is not None:
+ original_ids = list(original_ids)
+
+ running = 0
+ threads = []
+
+ for idx, item in enumerate(items):
+ if original_related is not None:
+ related = item['related_video_id'] = original_related
+ else:
+ related = item['related_video_id']
+ if original_channel is not None:
+ channel = item['related_channel_id'] = original_channel
+ else:
+ channel = item['related_channel_id']
+ video_id = item['id']
+
+ index['_related'].setdefault(related, 0)
+ index['_related'][related] += 1
+
+ if video_id in index:
+ item_count = index[video_id]
+ item_count['related'].setdefault(related, 0)
+ item_count['related'][related] += 1
+ item_count['channels'].setdefault(channel, 0)
+ item_count['channels'][channel] += 1
+ continue
+
+ index[video_id] = {
+ 'related': {related: 1},
+ 'channels': {channel: 1}
+ }
- # Prepend new candidates to items
- seen = [item['id']['videoId'] for item in items]
- for candidate in candidates:
- vid = candidate['id']['videoId']
- if vid not in seen:
- seen.append(vid)
- candidate['plugin_created_date'] = datetime_parser.now().strftime('%Y-%m-%dT%H:%M:%SZ')
- items.insert(0, candidate)
+ if item_store is None:
+ if original_ids and related not in original_ids:
+ items[idx] = None
+ continue
+
+ if group is not None:
+ pass
+ elif original_ids and related in original_ids:
+ group = max_depth * original_ids.index(related)
+ else:
+ group = 0
+
+ num_stored = len(item_store[group])
+ item['order'] = items_per_page * group + num_stored
+ item_store[group].append(item)
+
+ if num_stored or depth <= 1:
+ continue
+
+ running += 1
+ thread = threading.Thread(
+ target=threaded_get_related,
+ args=(video_id, index_items, counts),
+ kwargs={'item_store': item_store,
+ 'group': (group + 1),
+ 'depth': (depth - 1),
+ 'original_related': related,
+ 'original_channel': channel},
+ )
+ thread.daemon = True
+ threads.append(thread)
+ thread.start()
- # Truncate items to keep it manageable, and cache
- items = items[:500]
- cache.set_item(cache_items_key, items)
+ while running:
+ for thread in threads:
+ thread.join(5)
+ if not thread.is_alive():
+ running -= 1
- # Build the result set
- items.sort(
- key=lambda a: datetime_parser.parse(a['plugin_created_date']),
- reverse=True
- )
- sorted_items = []
- counter = 0
- channel_counts = {}
- while items:
- counter += 1
-
- # Hard stop on iteration. Good enough for our purposes.
- if counter >= 1000:
- break
+ index_items(cached, counts, original_ids=video_ids)
- # Reset channel counts on a new page
- if counter % 50 == 0:
- channel_counts = {}
+ # Fetch related videos. Use threads for faster execution.
+ def threaded_get_related(video_id, func, *args, **kwargs):
+ related_videos = self.get_related_videos(video_id).get('items')
+ if related_videos:
+ func(related_videos[:items_per_page], *args, **kwargs)
- # Ensure a single channel isn't hogging the page
- item = items.pop()
- channel_id = item.get('snippet', {}).get('channelId')
- if not channel_id:
+ running = 0
+ threads = []
+ candidates = []
+ for video_id in video_ids:
+ if video_id in counts['_related']:
continue
+ running += 1
+ thread = threading.Thread(
+ target=threaded_get_related,
+ args=(video_id, candidates.extend),
+ )
+ thread.daemon = True
+ threads.append(thread)
+ thread.start()
+
+ while running:
+ for thread in threads:
+ thread.join(5)
+ if not thread.is_alive():
+ running -= 1
+
+ num_items = items_per_page * num_items * max_depth
+ index_items(candidates[:num_items], counts,
+ item_store=items,
+ original_ids=video_ids,
+ depth=max_depth)
- channel_counts.setdefault(channel_id, 0)
- if channel_counts[channel_id] <= 3:
- # Use the item
- channel_counts[channel_id] += 1
- item["page_number"] = counter // 50
- sorted_items.append(item)
- else:
- # Move the item to the end of the list
- items.append(item)
-
- # Finally sort items per page by date for a better distribution
- def _sort_by_date_time(item):
- return (item['page_number'],
- -datetime_parser.since_epoch(datetime_parser.parse(
- item['snippet']['publishedAt']
- )))
+ # Truncate items to keep it manageable, and cache
+ items = list(chain.from_iterable(items))
+ counts['_counter'] = len(items)
+ remaining = num_items - counts['_counter']
+ if remaining > 0:
+ items.extend(islice(filter(None, cached), remaining))
+ elif remaining:
+ items = items[:num_items]
+
+ # Finally sort items per page by rank and date for a better distribution
+ def rank_and_sort(item):
+ if 'order' not in item:
+ counts['_counter'] += 1
+ item['order'] = counts['_counter']
+
+ page = 1 + item['order'] // (items_per_page * max_depth)
+ page_count = counts['_pages'].setdefault(page, {'_counter': 0})
+ while page_count['_counter'] < items_per_page and page > 1:
+ page -= 1
+ page_count = counts['_pages'].setdefault(page, {'_counter': 0})
+
+ related_video = item['related_video_id']
+ related_channel = item['related_channel_id']
+ channel_id = item.get('snippet', {}).get('channelId')
+ """
+ # Video channel and related channel can be the same which can double
+ # up the channel count. Checking for this allows more similar videos
+ # in the recommendation, ignoring it allows for more variety.
+ # Currently prefer not to check for this to allow more variety.
+ if channel_id == related_channel:
+ channel_id = None
+ """
+ while (page_count['_counter'] >= items_per_page
+ or (related_video in page_count
+ and page_count[related_video] >= diversity_limits)
+ or (related_channel and related_channel in page_count
+ and page_count[related_channel] >= diversity_limits)
+ or (channel_id and channel_id in page_count
+ and page_count[channel_id] >= diversity_limits)
+ ):
+ page += 1
+ page_count = counts['_pages'].setdefault(page, {'_counter': 0})
+
+ page_count.setdefault(related_video, 0)
+ page_count[related_video] += 1
+ if related_channel:
+ page_count.setdefault(related_channel, 0)
+ page_count[related_channel] += 1
+ if channel_id:
+ page_count.setdefault(channel_id, 0)
+ page_count[channel_id] += 1
+ page_count['_counter'] += 1
+ item['page'] = page
+
+ item_count = counts[item['id']]
+ item['rank'] = (2 * sum(item_count['channels'].values())
+ + sum(item_count['related'].values()))
+
+ return (
+ -item['page'],
+ item['rank'],
+ -randint(0, item['order'])
+ )
- sorted_items.sort(key=_sort_by_date_time)
+ items.sort(key=rank_and_sort, reverse=True)
# Finalize result
- payload['items'] = sorted_items
+ payload['items'] = items
"""
# TODO:
# Enable pagination
@@ -500,10 +794,10 @@ def _sort_by_date_time(item):
'totalResults': len(sorted_items)
}
"""
+
# Update cache
- cache.set_item(cache_home_key, payload)
+ cache.set_item(cache_items_key, items)
- # If there are no sorted_items we fall back to default API behaviour
return payload
def get_activities(self, channel_id, page_token='', **kwargs):
@@ -512,10 +806,6 @@ def get_activities(self, channel_id, page_token='', **kwargs):
'regionCode': self._region,
'hl': self._language}
- if channel_id == 'home':
- recommended = self._get_recommendations_for_home()
- if 'items' in recommended and recommended.get('items'):
- return recommended
if channel_id == 'home':
params['home'] = 'true'
elif channel_id == 'mine':
@@ -525,10 +815,10 @@ def get_activities(self, channel_id, page_token='', **kwargs):
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='activities',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='activities',
+ params=params,
+ **kwargs)
def get_channel_sections(self, channel_id, **kwargs):
params = {'part': 'snippet,contentDetails',
@@ -538,10 +828,10 @@ def get_channel_sections(self, channel_id, **kwargs):
params['mine'] = 'true'
else:
params['channelId'] = channel_id
- return self.perform_v3_request(method='GET',
- path='channelSections',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='channelSections',
+ params=params,
+ **kwargs)
def get_playlists_of_channel(self, channel_id, page_token='', **kwargs):
params = {'part': 'snippet',
@@ -553,10 +843,10 @@ def get_playlists_of_channel(self, channel_id, page_token='', **kwargs):
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='playlists',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='playlists',
+ params=params,
+ **kwargs)
def get_playlist_item_id_of_video_id(self, playlist_id, video_id, page_token=''):
old_max_results = self._max_results
@@ -591,10 +881,10 @@ def get_playlist_items(self,
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='playlistItems',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='playlistItems',
+ params=params,
+ **kwargs)
def get_channel_by_username(self, username, **kwargs):
"""
@@ -608,10 +898,10 @@ def get_channel_by_username(self, username, **kwargs):
else:
params['forUsername'] = username
- return self.perform_v3_request(method='GET',
- path='channels',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='channels',
+ params=params,
+ **kwargs)
def get_channels(self, channel_id, **kwargs):
"""
@@ -619,7 +909,7 @@ def get_channels(self, channel_id, **kwargs):
:param channel_id: list or comma-separated list of the YouTube channel ID(s)
:return:
"""
- if not isinstance(channel_id, str):
+ if not isinstance(channel_id, string_type):
channel_id = ','.join(channel_id)
params = {'part': 'snippet,contentDetails,brandingSettings'}
@@ -627,10 +917,10 @@ def get_channels(self, channel_id, **kwargs):
params['id'] = channel_id
else:
params['mine'] = 'true'
- return self.perform_v3_request(method='GET',
- path='channels',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='channels',
+ params=params,
+ **kwargs)
def get_disliked_videos(self, page_token='', **kwargs):
# prepare page token
@@ -644,10 +934,10 @@ def get_disliked_videos(self, page_token='', **kwargs):
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='videos',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='videos',
+ params=params,
+ **kwargs)
def get_videos(self, video_id, live_details=False, **kwargs):
"""
@@ -656,7 +946,7 @@ def get_videos(self, video_id, live_details=False, **kwargs):
:param live_details: also retrieve liveStreamingDetails
:return:
"""
- if not isinstance(video_id, str):
+ if not isinstance(video_id, string_type):
video_id = ','.join(video_id)
parts = ['snippet', 'contentDetails', 'status', 'statistics']
@@ -665,21 +955,21 @@ def get_videos(self, video_id, live_details=False, **kwargs):
params = {'part': ','.join(parts),
'id': video_id}
- return self.perform_v3_request(method='GET',
- path='videos',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='videos',
+ params=params,
+ **kwargs)
def get_playlists(self, playlist_id, **kwargs):
- if not isinstance(playlist_id, str):
+ if not isinstance(playlist_id, string_type):
playlist_id = ','.join(playlist_id)
params = {'part': 'snippet,contentDetails',
'id': playlist_id}
- return self.perform_v3_request(method='GET',
- path='playlists',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='playlists',
+ params=params,
+ **kwargs)
def get_live_events(self,
event_type='live',
@@ -718,36 +1008,123 @@ def get_live_events(self,
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='search',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='search',
+ params=params,
+ **kwargs)
def get_related_videos(self,
video_id,
page_token='',
max_results=0,
**kwargs):
- # prepare page token
- if not page_token:
- page_token = ''
-
+ # TODO: Improve handling of InnerTube requests, including automatic
+ # continuation processing to retrieve max_results number of results
+ # See Youtube.get_saved_playlists for existing implementation
max_results = self._max_results if max_results <= 0 else max_results
- # prepare params
- params = {'relatedToVideoId': video_id,
- 'part': 'snippet',
- 'type': 'video',
- 'regionCode': self._region,
- 'hl': self._language,
- 'maxResults': str(max_results)}
+ post_data = {'videoId': video_id}
if page_token:
- params['pageToken'] = page_token
+ post_data['continuation'] = page_token
+
+ result = self.api_request(version=1,
+ method='POST',
+ path='next',
+ post_data=post_data,
+ no_login=True)
+ if not result:
+ return []
+
+ related_videos = self.json_traverse(
+ result,
+ path=(
+ (
+ 'onResponseReceivedEndpoints',
+ 0,
+ 'appendContinuationItemsAction',
+ 'continuationItems',
+ ) if page_token else (
+ 'contents',
+ 'twoColumnWatchNextResults',
+ 'secondaryResults',
+ 'secondaryResults',
+ 'results',
+ )
+ ) + (
+ slice(None),
+ (
+ (
+ 'compactVideoRenderer',
+ # 'videoId',
+ ),
+ (
+ 'continuationItemRenderer',
+ 'continuationEndpoint',
+ 'continuationCommand',
+ # 'token',
+ ),
+ ),
+ )
+ )
+ if not related_videos:
+ return []
+
+ channel_id = self.json_traverse(
+ result,
+ path=(
+ 'contents',
+ 'twoColumnWatchNextResults',
+ 'results',
+ 'results',
+ 'contents',
+ 1,
+ 'videoSecondaryInfoRenderer',
+ 'owner',
+ 'videoOwnerRenderer',
+ 'title',
+ 'runs',
+ 0,
+ 'navigationEndpoint',
+ 'browseEndpoint',
+ 'browseId'
+ )
+ )
- return self.perform_v3_request(method='GET',
- path='search',
- params=params,
- **kwargs)
+ v3_response = {
+ 'kind': 'youtube#videoListResponse',
+ 'items': [
+ {
+ 'kind': "youtube#video",
+ 'id': video['videoId'],
+ 'related_video_id': video_id,
+ 'related_channel_id': channel_id,
+ 'partial': True,
+ 'snippet': {
+ 'title': video['title']['simpleText'],
+ 'thumbnails': dict(zip(
+ ('default', 'high'),
+ video['thumbnail']['thumbnails'],
+ )),
+ 'channelId': self.json_traverse(video, (
+ ('longBylineText', 'shortBylineText'),
+ 'runs',
+ 0,
+ 'navigationEndpoint',
+ 'browseEndpoint',
+ 'browseId',
+ )),
+ }
+ }
+ for video in related_videos
+ if video and 'videoId' in video
+ ]
+ }
+
+ last_item = related_videos[-1]
+ if last_item and 'token' in last_item:
+ v3_response['nextPageToken'] = last_item['token']
+
+ return v3_response
def get_parent_comments(self,
video_id,
@@ -765,11 +1142,11 @@ def get_parent_comments(self,
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='commentThreads',
- params=params,
- no_login=True,
- **kwargs)
+ return self.api_request(method='GET',
+ path='commentThreads',
+ params=params,
+ no_login=True,
+ **kwargs)
def get_child_comments(self,
parent_id,
@@ -786,11 +1163,11 @@ def get_child_comments(self,
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='comments',
- params=params,
- no_login=True,
- **kwargs)
+ return self.api_request(method='GET',
+ path='comments',
+ params=params,
+ no_login=True,
+ **kwargs)
def get_channel_videos(self, channel_id, page_token='', **kwargs):
"""
@@ -812,10 +1189,10 @@ def get_channel_videos(self, channel_id, page_token='', **kwargs):
if page_token:
params['pageToken'] = page_token
- return self.perform_v3_request(method='GET',
- path='search',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='search',
+ params=params,
+ **kwargs)
def search(self,
q,
@@ -848,7 +1225,7 @@ def search(self,
# prepare search type
if not search_type:
search_type = ''
- if not isinstance(search_type, str):
+ if not isinstance(search_type, string_type):
search_type = ','.join(search_type)
# prepare page token
@@ -891,10 +1268,10 @@ def search(self,
params['location'] = location
params['locationRadius'] = settings.get_location_radius()
- return self.perform_v3_request(method='GET',
- path='search',
- params=params,
- **kwargs)
+ return self.api_request(method='GET',
+ path='search',
+ params=params,
+ **kwargs)
def get_my_subscriptions(self, page_token=None, offset=0, **kwargs):
"""
@@ -921,7 +1298,7 @@ def _perform(_page_token, _offset, _result):
# if new uploads is cached
cache_items_key = 'my-subscriptions-items'
- cached = cache.get_item(cache_items_key, cache.ONE_HOUR)
+ cached = cache.get_item(cache_items_key, cache.ONE_HOUR) or []
if cached:
_result['items'] = cached
@@ -945,10 +1322,10 @@ def _perform(_page_token, _offset, _result):
if sub_page_token:
params['pageToken'] = sub_page_token
- json_data = self.perform_v3_request(method='GET',
- path='subscriptions',
- params=params,
- **kwargs)
+ json_data = self.api_request(method='GET',
+ path='subscriptions',
+ params=params,
+ **kwargs)
if not json_data:
json_data = {}
@@ -1021,7 +1398,7 @@ def fetch_xml(_url, _responses):
# sorting by publish date
def _sort_by_date_time(item):
return datetime_parser.since_epoch(
- datetime_parser.strptime(item['published'][0:19])
+ datetime_parser.strptime(item['published'])
)
_result['items'].sort(reverse=True, key=_sort_by_date_time)
@@ -1087,7 +1464,10 @@ def _perform(_playlist_idx, _page_token, _offset, _result):
else:
_post_data['browseId'] = 'FEmy_youtube'
- _json_data = self.perform_v1_tv_request(method='POST', path='browse', post_data=_post_data)
+ _json_data = self.api_request(version=1,
+ method='POST',
+ path='browse',
+ post_data=_post_data)
_data = {}
if 'continuationContents' in _json_data:
_data = _json_data.get('continuationContents', {}).get('horizontalListContinuation', {})
@@ -1172,7 +1552,10 @@ def _perform(_playlist_idx, _page_token, _offset, _result):
}
playlist_index = None
- json_data = self.perform_v1_tv_request(method='POST', path='browse', post_data=_en_post_data)
+ json_data = self.api_request(version=1,
+ method='POST',
+ path='browse',
+ post_data=_en_post_data)
contents = json_data.get('contents', {}).get('sectionListRenderer', {}).get('contents', [{}])
for idx, shelf in enumerate(contents):
@@ -1190,8 +1573,8 @@ def _perform(_playlist_idx, _page_token, _offset, _result):
def _response_hook(self, **kwargs):
response = kwargs['response']
- self._context.log_debug('[data] v3 response: |{0.status_code}|\n'
- '\theaders: |{0.headers}|'.format(response))
+ self._context.log_debug('API response: |{0.status_code}|\n'
+ 'headers: |{0.headers}|'.format(response))
try:
json_data = response.json()
if 'error' in json_data:
@@ -1206,8 +1589,8 @@ def _response_hook(self, **kwargs):
def _error_hook(self, **kwargs):
exc = kwargs['exc']
json_data = getattr(exc, 'json_data', None)
- data = getattr(exc, 'pass_data', False) and json_data
- exception = getattr(exc, 'raise_exc', False) and YouTubeException
+ data = getattr(exc, 'pass_data', None) and json_data
+ exception = getattr(exc, 'raise_exc', None) and YouTubeException
if not json_data or 'error' not in json_data:
return None, None, None, data, None, exception
@@ -1240,129 +1623,78 @@ def _error_hook(self, **kwargs):
title,
time_ms=timeout)
- info = ('[data] v3 error: {reason}\n'
- '\texc: |{exc}|\n'
- '\tmessage: |{message}|')
+ info = ('API error: {reason}\n'
+ 'exc: |{exc}|\n'
+ 'message: |{message}|')
details = {'reason': reason, 'message': message}
return '', info, details, data, False, exception
- def perform_v3_request(self, method='GET', headers=None, path=None,
- post_data=None, params=None, no_login=False,
- **kwargs):
- # params
- _params = {}
-
- # headers
- _headers = {'Host': 'www.googleapis.com',
- 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64)'
- ' AppleWebKit/537.36 (KHTML, like Gecko)'
- ' Chrome/39.0.2171.36 Safari/537.36',
- 'Accept-Encoding': 'gzip, deflate'}
+ def api_request(self,
+ version=3,
+ method='GET',
+ path=None,
+ params=None,
+ post_data=None,
+ headers=None,
+ no_login=False,
+ **kwargs):
+ client_data = {
+ '_endpoint': path.strip('/'),
+ 'method': method,
+ }
+ if headers:
+ client_data['headers'] = headers
+ if post_data:
+ client_data['json'] = post_data
+ if params:
+ client_data['params'] = params
# a config can decide if a token is allowed
if (not no_login and self._access_token
and self._config.get('token-allowed', True)):
- _headers['Authorization'] = 'Bearer %s' % self._access_token
- else:
- _params['key'] = self._config_tv['key']
+ client_data['_access_token'] = self._access_token
- # url
- _url = 'https://www.googleapis.com/youtube/v3/%s' % path.strip('/')
+ client = self.build_client(version, client_data)
- if headers:
- _headers.update(headers)
+ if 'key' in client['params'] and not client['params']['key']:
+ client['params']['key'] = self._config_tv['key']
+
+ params = client.get('params')
if params:
- _params.update(params)
- log_params = copy.deepcopy(params)
+ log_params = deepcopy(params)
if 'location' in log_params:
log_params['location'] = 'xx.xxxx,xx.xxxx'
+ if 'key' in log_params:
+ key = list(log_params['key'])
+ key[5:-5] = '...'
+ log_params['key'] = ''.join(key)
else:
log_params = None
- self._context.log_debug('[data] v3 request: |{method}|\n'
- '\tpath: |{path}|\n'
- '\tparams: |{params}|\n'
- '\tpost_data: |{data}|\n'
- '\theaders: |{headers}|'
- .format(method=method,
- path=path,
- params=log_params,
- data=post_data,
- headers=_headers))
-
- json_data = self.request(_url,
- method=method,
- headers=_headers,
- json=post_data,
- params=_params,
- response_hook=self._response_hook,
- response_hook_kwargs=kwargs,
- error_hook=self._error_hook)
- return json_data
-
- def perform_v1_tv_request(self, method='GET', headers=None, path=None,
- post_data=None, params=None, no_login=False):
-
- # params
- _params = {}
-
- # headers
- _headers = {
- 'User-Agent': ('Mozilla/5.0 (Linux; Android 7.0; SM-G892A Build/NRD90M;'
- ' wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0'
- ' Chrome/67.0.3396.87 Mobile Safari/537.36'),
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
- 'DNT': '1',
- 'Accept-Encoding': 'gzip, deflate',
- 'Accept-Language': 'en-US,en;q=0.5',
- }
-
- if self._access_token and self._config.get('token-allowed', True) and not no_login:
- _headers['Authorization'] = 'Bearer %s' % self._access_token
- else:
- _params = {'key': self._config_tv['key']}
-
- # url
- _url = 'https://www.googleapis.com/youtubei/v1/%s' % path.strip('/')
-
+ headers = client.get('headers')
if headers:
- _headers.update(headers)
- if params:
- _params.update(params)
- log_params = copy.deepcopy(params)
- if 'location' in log_params:
- log_params['location'] = 'xx.xxxx,xx.xxxx'
+ log_headers = deepcopy(headers)
+ if 'Authorization' in log_headers:
+ log_headers['Authorization'] = 'logged in'
else:
- log_params = None
- self._context.log_debug('[data] v1 request: |{method}|\n'
- '\tpath: |{path}|\n'
- '\tparams: |{params}|\n'
- '\tpost_data: |{data}|\n'
- '\theaders: |{headers}|'
- .format(method=method,
+ log_headers = None
+
+ self._context.log_debug('API request:\n'
+ 'version: |{version}|\n'
+ 'method: |{method}|\n'
+ 'path: |{path}|\n'
+ 'params: |{params}|\n'
+ 'post_data: |{data}|\n'
+ 'headers: |{headers}|'
+ .format(version=version,
+ method=method,
path=path,
params=log_params,
- data=post_data,
- headers=_headers))
-
- result = self.request(_url,
- method=method,
- headers=_headers,
- json=post_data,
- params=_params)
- if result is None:
- return {}
-
- self._context.log_debug('[data] v1 response: |{0.status_code}|\n'
- '\theaders: |{0.headers}|'.format(result))
-
- result_type = result.headers.get('content-type')
- if result_type and result_type.startswith('application/json'):
- try:
- return result.json()
- except ValueError:
- return {
- 'status_code': result.status_code,
- 'payload': result.text
- }
- return {}
+ data=client.get('json'),
+ headers=log_headers))
+
+ json_data = self.request(response_hook=self._response_hook,
+ response_hook_kwargs=kwargs,
+ error_hook=self._error_hook,
+ **client)
+ return json_data
diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py
index ffcbb7cf9..01f52ee4b 100644
--- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py
+++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py
@@ -52,12 +52,13 @@ def get_channels(self, ids, defer_cache=False):
channel_id = items[0]['id']
updated.append(channel_id)
except IndexError:
- self._context.log_error('Channel not found:\n\t{data}'
+ self._context.log_error('Channel not found:\n{data}'
.format(data=data))
ids = updated
result = self._data_cache.get_items(ids, self._data_cache.ONE_MONTH)
- to_update = [id_ for id_ in ids if id_ not in result]
+ to_update = [id_ for id_ in ids
+ if id_ not in result or result[id_].get('partial')]
if result:
self._context.log_debug('Found cached data for channels:\n|{ids}|'
@@ -119,7 +120,8 @@ def get_fanarts(self, channel_ids, defer_cache=False):
def get_playlists(self, ids, defer_cache=False):
ids = tuple(ids)
result = self._data_cache.get_items(ids, self._data_cache.ONE_MONTH)
- to_update = [id_ for id_ in ids if id_ not in result]
+ to_update = [id_ for id_ in ids
+ if id_ not in result or result[id_].get('partial')]
if result:
self._context.log_debug('Found cached data for playlists:\n|{ids}|'
@@ -249,7 +251,8 @@ def get_videos(self,
defer_cache=False):
ids = tuple(ids)
result = self._data_cache.get_items(ids, self._data_cache.ONE_MONTH)
- to_update = [id_ for id_ in ids if id_ not in result]
+ to_update = [id_ for id_ in ids
+ if id_ not in result or result[id_].get('partial')]
if result:
self._context.log_debug('Found cached data for videos:\n|{ids}|'
@@ -270,11 +273,13 @@ def get_videos(self,
if new_data:
self._context.log_debug('Got data for videos:\n|{ids}|'
.format(ids=to_update))
- new_data = dict(dict.fromkeys(to_update, {}), **{
- yt_item['id']: yt_item or {}
+ new_data = {
+ yt_item['id']: yt_item
for batch in new_data
for yt_item in batch.get('items', [])
- })
+ if yt_item
+ }
+ new_data = dict(dict.fromkeys(to_update, {}), **new_data)
result.update(new_data)
self.cache_data(new_data, defer=defer_cache)
@@ -291,7 +296,8 @@ def get_videos(self,
playback_history = self._context.get_playback_history()
played_items = playback_history.get_items(ids)
for video_id, play_data in played_items.items():
- result[video_id]['play_data'] = play_data
+ if video_id in result:
+ result[video_id]['play_data'] = play_data
return result
diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py
index 5a011008b..8effeada9 100644
--- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py
+++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py
@@ -262,10 +262,11 @@ def _get(self, lang_code='en', language=None, no_asr=False, download=None):
error_info=('Failed to retrieve subtitles for: {lang}: {{exc}}'
.format(lang=lang_code))
)
- if not response.text:
+ response = response and response.text
+ if not response:
return []
- output = bytearray(self._unescape(response.text),
+ output = bytearray(self._unescape(response),
encoding='utf8',
errors='ignore')
try:
diff --git a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py
index a6d94cfa8..73b4eeb4a 100644
--- a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py
+++ b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py
@@ -127,7 +127,7 @@ def resolve(self, url, url_components, method='HEAD'):
method=method,
headers=self._HEADERS,
allow_redirects=True)
- if response.status_code != 200:
+ if not response or not response.ok:
return url
if path.startswith('/clip'):
@@ -197,7 +197,7 @@ def resolve(self, url, url_components, method='HEAD'):
method=method,
headers=self._HEADERS,
allow_redirects=True)
- if response.status_code != 200:
+ if not response or not response.ok:
return url
return response.url
diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py
index 6d0d7e1f2..29c4ddba0 100644
--- a/resources/lib/youtube_plugin/youtube/helper/utils.py
+++ b/resources/lib/youtube_plugin/youtube/helper/utils.py
@@ -14,6 +14,7 @@
import time
from math import log10
+from ...kodion.constants import content
from ...kodion.items import DirectoryItem, menu_items
from ...kodion.utils import (
create_path,
@@ -22,12 +23,12 @@
strip_html_from_text,
)
-
try:
from inputstreamhelper import Helper as ISHelper
except ImportError:
ISHelper = None
+
__COLOR_MAP = {
'commentCount': 'cyan',
'favoriteCount': 'gold',
@@ -74,7 +75,7 @@ def make_comment_item(context, snippet, uri, total_replies=0):
like_count = snippet['likeCount']
if like_count:
- like_count, _ = friendly_number(like_count)
+ like_count = friendly_number(like_count)
color = __COLOR_MAP['likeCount']
label_likes = ui.color(color, ui.bold(like_count))
plot_likes = ui.color(color, ui.bold(' '.join((
@@ -84,7 +85,7 @@ def make_comment_item(context, snippet, uri, total_replies=0):
plot_props.append(plot_likes)
if total_replies:
- total_replies, _ = friendly_number(total_replies)
+ total_replies = friendly_number(total_replies)
color = __COLOR_MAP['commentCount']
label_replies = ui.color(color, ui.bold(total_replies))
plot_replies = ui.color(color, ui.bold(' '.join((
@@ -127,12 +128,12 @@ def make_comment_item(context, snippet, uri, total_replies=0):
comment_item = DirectoryItem(label, uri)
comment_item.set_plot(plot)
- datetime = datetime_parser.parse(published_at, as_utc=True)
+ datetime = datetime_parser.parse(published_at)
comment_item.set_added_utc(datetime)
local_datetime = datetime_parser.utc_to_local(datetime)
comment_item.set_dateadded_from_datetime(local_datetime)
if edited:
- datetime = datetime_parser.parse(updated_at, as_utc=True)
+ datetime = datetime_parser.parse(updated_at)
local_datetime = datetime_parser.utc_to_local(datetime)
comment_item.set_date_from_datetime(local_datetime)
@@ -311,11 +312,11 @@ def update_playlist_infos(provider, context, playlist_id_dict,
context, playlist_id, title
),
# remove as my custom watch later playlist
- menu_items.remove_as_watchlater(
+ menu_items.remove_as_watch_later(
context, playlist_id, title
) if playlist_id == custom_watch_later_id else
# set as my custom watch later playlist
- menu_items.set_as_watchlater(
+ menu_items.set_as_watch_later(
context, playlist_id, title
),
# remove as custom history playlist
@@ -362,9 +363,9 @@ def update_video_infos(provider, context, video_id_dict,
logged_in = provider.is_logged_in()
if logged_in:
- wl_playlist_id = context.get_access_manager().get_watch_later_id()
+ watch_later_id = context.get_access_manager().get_watch_later_id()
else:
- wl_playlist_id = None
+ watch_later_id = None
settings = context.get_settings()
hide_shorts = settings.hide_short_videos()
@@ -380,7 +381,7 @@ def update_video_infos(provider, context, video_id_dict,
video_item = video_id_dict[video_id]
# set mediatype
- video_item.set_mediatype('video') # using video
+ video_item.set_mediatype(content.VIDEO_TYPE)
if not yt_item or 'snippet' not in yt_item:
continue
@@ -427,7 +428,7 @@ def update_video_infos(provider, context, video_id_dict,
else:
start_at = None
if start_at:
- datetime = datetime_parser.parse(start_at, as_utc=True)
+ datetime = datetime_parser.parse(start_at)
video_item.set_scheduled_start_utc(datetime)
local_datetime = datetime_parser.utc_to_local(datetime)
video_item.set_year_from_datetime(local_datetime)
@@ -452,7 +453,7 @@ def update_video_infos(provider, context, video_id_dict,
if not label:
continue
- str_value, value = friendly_number(value)
+ str_value, value = friendly_number(value, as_str=False)
if not value:
continue
@@ -488,7 +489,7 @@ def update_video_infos(provider, context, video_id_dict,
video_item.set_short_details(label_stats)
# Hack to force a custom label mask containing production code,
# activated on sort order selection, to display details
- # Refer Provider.set_content_type for usage
+ # Refer XbmcContext.set_content for usage
video_item.set_code(label_stats)
# update and set the title
@@ -531,7 +532,7 @@ def update_video_infos(provider, context, video_id_dict,
# date time
published_at = snippet.get('publishedAt')
if published_at:
- datetime = datetime_parser.parse(published_at, as_utc=True)
+ datetime = datetime_parser.parse(published_at)
video_item.set_added_utc(datetime)
local_datetime = datetime_parser.utc_to_local(datetime)
video_item.set_dateadded_from_datetime(local_datetime)
@@ -591,26 +592,12 @@ def update_video_infos(provider, context, video_id_dict,
if alternate_player:
context_menu.append(menu_items.play_with(context))
- if logged_in:
- # add 'Watch Later' only if we are not in my 'Watch Later' list
- if wl_playlist_id and playlist_id and wl_playlist_id != playlist_id:
+ # add 'Watch Later' only if we are not in my 'Watch Later' list
+ if watch_later_id:
+ if not playlist_id or watch_later_id != playlist_id:
context_menu.append(
menu_items.watch_later_add(
- context, wl_playlist_id, video_id
- )
- )
-
- # provide 'remove' for videos in my playlists
- # we support all playlist except 'Watch History'
- if (video_id in playlist_item_id_dict and playlist_id
- and playlist_channel_id == 'mine'
- and playlist_id.strip().lower() not in ('hl', 'wl')):
- playlist_item_id = playlist_item_id_dict[video_id]
- video_item.set_playlist_id(playlist_id)
- video_item.set_playlist_item_id(playlist_item_id)
- context_menu.append(
- menu_items.remove_video_from_playlist(
- context, playlist_id, video_id, video_item.get_name()
+ context, watch_later_id, video_id
)
)
else:
@@ -620,6 +607,21 @@ def update_video_infos(provider, context, video_id_dict,
)
)
+ # provide 'remove' for videos in my playlists
+ # we support all playlist except 'Watch History'
+ if (logged_in and video_id in playlist_item_id_dict and playlist_id
+ and playlist_channel_id == 'mine'
+ and playlist_id.strip().lower() not in ('hl', 'wl')):
+ playlist_item_id = playlist_item_id_dict[video_id]
+ video_item.set_playlist_id(playlist_id)
+ video_item.set_playlist_item_id(playlist_item_id)
+ context_menu.append(
+ menu_items.remove_video_from_playlist(
+ context, playlist_id, video_id, video_item.get_name()
+ )
+ )
+
+
# got to [CHANNEL] only if we are not directly in the channel
if (channel_id and channel_name and
create_path('channel', channel_id) != path):
diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py
index 0cc753aea..439ce29c9 100644
--- a/resources/lib/youtube_plugin/youtube/helper/v3.py
+++ b/resources/lib/youtube_plugin/youtube/helper/v3.py
@@ -39,8 +39,13 @@ def _process_list_response(provider, context, json_data):
result = []
+ item_params = {}
incognito = context.get_param('incognito', False)
+ if incognito:
+ item_params['incognito'] = incognito
addon_id = context.get_param('addon_id', '')
+ if addon_id:
+ item_params['addon_id'] = addon_id
settings = context.get_settings()
thumb_size = settings.use_thumbnail_size()
@@ -52,223 +57,138 @@ def _process_list_response(provider, context, json_data):
context.log_debug('v3 response: Item discarded, is_youtube=False')
continue
+ item_id = yt_item.get('id')
+ snippet = yt_item.get('snippet', {})
+ title = snippet.get('title', context.localize('untitled'))
+ image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
+
+ if kind == 'searchresult':
+ _, kind = _parse_kind(item_id)
+ if kind == 'video':
+ item_id = item_id['videoId']
+ elif kind == 'playlist':
+ item_id = item_id['playlistId']
+ elif kind == 'channel':
+ item_id = item_id['channelId']
+
if kind == 'video':
- video_id = yt_item['id']
- snippet = yt_item['snippet']
- title = snippet.get('title', context.localize('untitled'))
- image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
- item_params = {'video_id': video_id}
- if incognito:
- item_params['incognito'] = incognito
- if addon_id:
- item_params['addon_id'] = addon_id
- item_uri = context.create_uri(['play'], item_params)
- video_item = VideoItem(title, item_uri, image=image)
- video_item.video_id = video_id
- if incognito:
- video_item.set_play_count(0)
- result.append(video_item)
- video_id_dict[video_id] = video_item
- elif kind == 'channel':
- channel_id = yt_item['id']
- snippet = yt_item['snippet']
- title = snippet.get('title', context.localize('untitled'))
- image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
- item_params = {}
- if incognito:
- item_params['incognito'] = incognito
- if addon_id:
- item_params['addon_id'] = addon_id
- item_uri = context.create_uri(['channel', channel_id], item_params)
- channel_item = DirectoryItem(title, item_uri, image=image)
+ item_uri = context.create_uri(
+ ('play',),
+ dict(item_params, video_id=item_id),
+ )
+ item = VideoItem(title, item_uri, image=image)
+ video_id_dict[item_id] = item
+ elif kind == 'channel':
+ item_uri = context.create_uri(
+ ('channel', item_id),
+ item_params,
+ )
+ item = DirectoryItem(title, item_uri, image=image)
+ channel_id_dict[item_id] = item
# if logged in => provide subscribing to the channel
if provider.is_logged_in():
- context_menu = [
+ context_menu = (
menu_items.subscribe_to_channel(
- context, channel_id
+ context, item_id
),
- ]
- channel_item.set_context_menu(context_menu)
- result.append(channel_item)
- channel_id_dict[channel_id] = channel_item
+ )
+ item.set_context_menu(context_menu)
+
elif kind == 'guidecategory':
- guide_id = yt_item['id']
- snippet = yt_item['snippet']
- title = snippet.get('title', context.localize('untitled'))
- item_params = {'guide_id': guide_id}
- if incognito:
- item_params['incognito'] = incognito
- if addon_id:
- item_params['addon_id'] = addon_id
- item_uri = context.create_uri(['special', 'browse_channels'], item_params)
- guide_item = DirectoryItem(title, item_uri)
- result.append(guide_item)
- elif kind == 'subscription':
- snippet = yt_item['snippet']
- title = snippet.get('title', context.localize('untitled'))
- image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
- channel_id = snippet['resourceId']['channelId']
- item_params = {}
- if incognito:
- item_params['incognito'] = incognito
- if addon_id:
- item_params['addon_id'] = addon_id
- item_uri = context.create_uri(['channel', channel_id], item_params)
- channel_item = DirectoryItem(title, item_uri, image=image)
- channel_item.set_channel_id(channel_id)
- # map channel id with subscription id - we need it for the unsubscription
- subscription_id_dict[channel_id] = yt_item['id']
-
- result.append(channel_item)
- channel_id_dict[channel_id] = channel_item
- elif kind == 'playlist':
- playlist_id = yt_item['id']
- snippet = yt_item['snippet']
- title = snippet.get('title', context.localize('untitled'))
- image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
+ item_uri = context.create_uri(
+ ('special', 'browse_channels'),
+ dict(item_params, guide_id=item_id),
+ )
+ item = DirectoryItem(title, item_uri)
- channel_id = snippet['channelId']
+ elif kind == 'subscription':
+ subscription_id = item_id
+ item_id = snippet['resourceId']['channelId']
+ # map channel id with subscription id - needed to unsubscribe
+ subscription_id_dict[item_id] = subscription_id
+
+ item_uri = context.create_uri(
+ ('channel', item_id),
+ item_params
+ )
+ item = DirectoryItem(title, item_uri, image=image)
+ channel_id_dict[item_id] = item
+ item.set_channel_id(item_id)
- # if the path directs to a playlist of our own, we correct the channel id to 'mine'
+ elif kind == 'playlist':
+ # set channel id to 'mine' if the path is for a playlist of our own
if context.get_path() == '/channel/mine/playlists/':
channel_id = 'mine'
- item_params = {}
- if incognito:
- item_params['incognito'] = incognito
- if addon_id:
- item_params['addon_id'] = addon_id
- item_uri = context.create_uri(['channel', channel_id, 'playlist', playlist_id], item_params)
- playlist_item = DirectoryItem(title, item_uri, image=image)
- result.append(playlist_item)
- playlist_id_dict[playlist_id] = playlist_item
- elif kind == 'playlistitem':
- snippet = yt_item['snippet']
- video_id = snippet['resourceId']['videoId']
-
- # store the id of the playlistItem - for deleting this item we need this item
- playlist_item_id_dict[video_id] = yt_item['id']
+ else:
+ channel_id = snippet['channelId']
+ item_uri = context.create_uri(
+ ('channel', channel_id, 'playlist', item_id),
+ item_params,
+ )
+ item = DirectoryItem(title, item_uri, image=image)
+ playlist_id_dict[item_id] = item
- title = snippet.get('title', context.localize('untitled'))
- image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
- item_params = {'video_id': video_id}
- if incognito:
- item_params['incognito'] = incognito
- if addon_id:
- item_params['addon_id'] = addon_id
- item_uri = context.create_uri(['play'], item_params)
- video_item = VideoItem(title, item_uri, image=image)
- video_item.video_id = video_id
- if incognito:
- video_item.set_play_count(0)
- # Get Track-ID from Playlist
- video_item.set_track_number(snippet['position'] + 1)
- result.append(video_item)
- video_id_dict[video_id] = video_item
+ elif kind == 'playlistitem':
+ playlistitem_id = item_id
+ item_id = snippet['resourceId']['videoId']
+ # store the id of the playlistItem - needed for deleting item
+ playlist_item_id_dict[item_id] = playlistitem_id
+
+ item_uri = context.create_uri(
+ ('play',),
+ dict(item_params, video_id=item_id),
+ )
+ item = VideoItem(title, item_uri, image=image)
+ video_id_dict[item_id] = item
elif kind == 'activity':
- snippet = yt_item['snippet']
details = yt_item['contentDetails']
activity_type = snippet['type']
-
- # recommendations
if activity_type == 'recommendation':
- video_id = details['recommendation']['resourceId']['videoId']
+ item_id = details['recommendation']['resourceId']['videoId']
elif activity_type == 'upload':
- video_id = details['upload']['videoId']
+ item_id = details['upload']['videoId']
else:
continue
- title = snippet.get('title', context.localize('untitled'))
- image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
- item_params = {'video_id': video_id}
- if incognito:
- item_params['incognito'] = incognito
- if addon_id:
- item_params['addon_id'] = addon_id
- item_uri = context.create_uri(['play'], item_params)
- video_item = VideoItem(title, item_uri, image=image)
- video_item.video_id = video_id
- if incognito:
- video_item.set_play_count(0)
- result.append(video_item)
- video_id_dict[video_id] = video_item
+ item_uri = context.create_uri(
+ ('play',),
+ dict(item_params, video_id=item_id),
+ )
+ item = VideoItem(title, item_uri, image=image)
+ video_id_dict[item_id] = item
elif kind == 'commentthread':
- thread_snippet = yt_item['snippet']
- total_replies = thread_snippet['totalReplyCount']
- snippet = thread_snippet['topLevelComment']['snippet']
- item_params = {'parent_id': yt_item['id']}
+ total_replies = snippet['totalReplyCount']
+ snippet = snippet['topLevelComment']['snippet']
if total_replies:
- item_uri = context.create_uri(['special', 'child_comments'], item_params)
+ item_uri = context.create_uri(
+ ('special', 'child_comments'),
+ {'parent_id': item_id}
+ )
else:
item_uri = ''
- result.append(make_comment_item(context, snippet, item_uri, total_replies))
+ item = make_comment_item(context, snippet, item_uri, total_replies)
elif kind == 'comment':
- result.append(make_comment_item(context, yt_item['snippet'], uri=''))
-
- elif kind == 'searchresult':
- _, kind = _parse_kind(yt_item.get('id', {}))
+ item = make_comment_item(context, snippet, uri='')
- # video
- if kind == 'video':
- video_id = yt_item['id']['videoId']
- snippet = yt_item.get('snippet', {})
- title = snippet.get('title', context.localize('untitled'))
- image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
- item_params = {'video_id': video_id}
- if incognito:
- item_params['incognito'] = incognito
- if addon_id:
- item_params['addon_id'] = addon_id
- item_uri = context.create_uri(['play'], item_params)
- video_item = VideoItem(title, item_uri, image=image)
- video_item.video_id = video_id
- if incognito:
- video_item.set_play_count(0)
- result.append(video_item)
- video_id_dict[video_id] = video_item
- # playlist
- elif kind == 'playlist':
- playlist_id = yt_item['id']['playlistId']
- snippet = yt_item['snippet']
- title = snippet.get('title', context.localize('untitled'))
- image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
-
- channel_id = snippet['channelId']
- # if the path directs to a playlist of our own, we correct the channel id to 'mine'
- if context.get_path() == '/channel/mine/playlists/':
- channel_id = 'mine'
- # channel_name = snippet.get('channelTitle', '')
- item_params = {}
- if incognito:
- item_params['incognito'] = incognito
- if addon_id:
- item_params['addon_id'] = addon_id
- item_uri = context.create_uri(['channel', channel_id, 'playlist', playlist_id], item_params)
- playlist_item = DirectoryItem(title, item_uri, image=image)
- result.append(playlist_item)
- playlist_id_dict[playlist_id] = playlist_item
- elif kind == 'channel':
- channel_id = yt_item['id']['channelId']
- snippet = yt_item['snippet']
- title = snippet.get('title', context.localize('untitled'))
- image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
- item_params = {}
- if incognito:
- item_params['incognito'] = incognito
- if addon_id:
- item_params['addon_id'] = addon_id
- item_uri = context.create_uri(['channel', channel_id], item_params)
- channel_item = DirectoryItem(title, item_uri, image=image)
- result.append(channel_item)
- channel_id_dict[channel_id] = channel_item
- else:
- raise KodionException("Unknown kind '%s'" % kind)
else:
raise KodionException("Unknown kind '%s'" % kind)
+ if not item:
+ continue
+ if isinstance(item, VideoItem):
+ item.video_id = item_id
+ if incognito:
+ item.set_play_count(0)
+ # Set track number from playlist, or set to current list length to
+ # match "Default" (unsorted) sort order
+ position = snippet.get('position') or len(result)
+ item.set_track_number(position + 1)
+ result.append(item)
+
# this will also update the channel_id_dict with the correct channel_id
# for each video.
channel_items_dict = {}
@@ -399,7 +319,7 @@ def _fetch(resource):
thread = resource['thread']
if thread:
- thread.join(1)
+ thread.join(5)
if not thread.is_alive():
resource['thread'] = None
resource['complete'] = True
@@ -413,16 +333,29 @@ def _fetch(resource):
return result
-def response_to_items(provider, context, json_data, sort=None, reverse=False, process_next_page=True):
+def response_to_items(provider,
+ context,
+ json_data,
+ sort=None,
+ reverse=False,
+ process_next_page=True):
is_youtube, kind = _parse_kind(json_data)
if not is_youtube:
context.log_debug('v3 response: Response discarded, is_youtube=False')
return []
- if kind in ['searchlistresponse', 'playlistitemlistresponse', 'playlistlistresponse',
- 'subscriptionlistresponse', 'guidecategorylistresponse', 'channellistresponse',
- 'videolistresponse', 'activitylistresponse', 'commentthreadlistresponse',
- 'commentlistresponse']:
+ if kind in (
+ 'activitylistresponse',
+ 'channellistresponse',
+ 'commentlistresponse',
+ 'commentthreadlistresponse',
+ 'guidecategorylistresponse',
+ 'playlistitemlistresponse',
+ 'playlistlistresponse',
+ 'searchlistresponse',
+ 'subscriptionlistresponse',
+ 'videolistresponse',
+ ):
result = _process_list_response(provider, context, json_data)
else:
raise KodionException("Unknown kind '%s'" % kind)
@@ -439,21 +372,32 @@ def response_to_items(provider, context, json_data, sort=None, reverse=False, pr
# next page
"""
- This will try to prevent the issue 7163 (https://code.google.com/p/gdata-issues/issues/detail?id=7163).
- Somehow the APIv3 is missing the token for the next page. We implemented our own calculation for the token
- into the YouTube client...this should work for up to ~2000 entries.
+ This will try to prevent the issue 7163
+ https://code.google.com/p/gdata-issues/issues/detail?id=7163
+ Somehow the APIv3 is missing the token for the next page.
+ We implemented our own calculation for the token into the YouTube client
+ This should work for up to ~2000 entries.
"""
- yt_total_results = int(json_data.get('pageInfo', {}).get('totalResults', 0))
- yt_results_per_page = int(json_data.get('pageInfo', {}).get('resultsPerPage', 0))
+ page_info = json_data.get('pageInfo', {})
+ yt_total_results = int(page_info.get('totalResults', 0))
+ yt_results_per_page = int(page_info.get('resultsPerPage', 0))
page = int(context.get_param('page', 1))
+ yt_visitor_data = json_data.get('visitorData', '')
yt_next_page_token = json_data.get('nextPageToken', '')
+ yt_click_tracking = json_data.get('clickTracking', '')
if yt_next_page_token or (page * yt_results_per_page < yt_total_results):
if not yt_next_page_token:
client = provider.get_client(context)
- yt_next_page_token = client.calculate_next_page_token(page + 1, yt_results_per_page)
+ yt_next_page_token = client.calculate_next_page_token(
+ page + 1, yt_results_per_page
+ )
new_params = dict(context.get_params(),
page_token=yt_next_page_token)
+ if yt_click_tracking:
+ new_params['visitor'] = yt_visitor_data
+ if yt_click_tracking:
+ new_params['click_tracking'] = yt_click_tracking
new_context = context.clone(new_params=new_params)
current_page = new_context.get_param('page', 1)
next_page_item = NextPageItem(new_context, current_page)
diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py
index d2072f770..64088ff27 100644
--- a/resources/lib/youtube_plugin/youtube/helper/video_info.py
+++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py
@@ -665,7 +665,6 @@ def load_stream_infos(self, video_id):
return self._get_video_info()
def _get_player_page(self, client='web', embed=False):
- client = self.build_client(client)
if embed:
url = 'https://www.youtube.com/embed/{0}'.format(self.video_id)
else:
@@ -673,7 +672,7 @@ def _get_player_page(self, client='web', embed=False):
cookies = {'CONSENT': 'YES+cb.20210615-14-p0.en+FX+294'}
result = self.request(
- url, cookies=cookies, headers=client['headers'],
+ url, cookies=cookies, headers=self.build_client(client)['headers'],
error_msg=('Failed to get player html for video_id: {0}'
.format(self.video_id))
)
@@ -746,18 +745,17 @@ def _get_player_js(self):
if cached:
return cached
- client = self.build_client('web')
result = self.request(
- js_url, headers=client['headers'],
+ js_url, headers=self.build_client('web')['headers'],
error_msg=('Failed to get player js for video_id: {0}'
.format(self.video_id))
)
+ result = result and result.text
if not result:
return ''
- javascript = result.text
- self._data_cache.set_item(js_cache_key, {'js': javascript})
- return javascript
+ self._data_cache.set_item(js_cache_key, {'js': result})
+ return result
@staticmethod
def _make_curl_headers(headers, cookies=None):
@@ -856,7 +854,11 @@ def _load_hls_manifest(self, url, live_type=None, meta_info=None,
stream_list.append(stream)
return stream_list
- def _create_stream_list(self, streams, meta_info=None, headers=None, playback_stats=None):
+ def _create_stream_list(self,
+ streams,
+ meta_info=None,
+ headers=None,
+ playback_stats=None):
if not headers and self._selected_client:
headers = self._selected_client['headers'].copy()
if 'Authorization' in headers:
@@ -991,8 +993,16 @@ def _get_error_details(self, playability_status, details=None):
if not details:
details = (
'errorScreen',
- ('playerErrorMessageRenderer', 'confirmDialogRenderer'),
- ('reason', 'title')
+ (
+ (
+ 'playerErrorMessageRenderer',
+ 'reason',
+ ),
+ (
+ 'confirmDialogRenderer',
+ 'title',
+ ),
+ )
)
result = self.json_traverse(playability_status, details)
@@ -1012,14 +1022,18 @@ def _get_error_details(self, playability_status, details=None):
return None
def _get_video_info(self):
- auth_header = bool(self._access_token)
video_info_url = 'https://www.youtube.com/youtubei/v1/player'
_settings = self._context.get_settings()
playability_status = status = reason = None
+
+ client_data = {'json': {'videoId': self.video_id}}
+ if self._access_token:
+ client_data['_access_token'] = self._access_token
+
for _ in range(2):
for client_name in self._prioritised_clients:
- client = self.build_client(client_name, auth_header)
+ client = self.build_client(client_name, client_data)
result = self.request(
video_info_url, 'POST',
@@ -1028,7 +1042,8 @@ def _get_video_info(self):
' using {1} client ({2})'
.format(self.video_id,
client_name,
- 'logged in' if auth_header else 'logged out')
+ 'logged in' if '_access_token' in client_data
+ else 'logged out')
),
raise_error=True,
**client
@@ -1074,8 +1089,8 @@ def _get_video_info(self):
# Only attempt to remove Authorization header if clients iterable
# was exhausted i.e. request attempted using all clients
else:
- if auth_header:
- auth_header = False
+ if '_access_token' in client_data:
+ del client_data['_access_token']
continue
# Otherwise skip retrying clients without Authorization header
break
@@ -1100,7 +1115,8 @@ def _get_video_info(self):
self._context.log_debug(
'Retrieved video info for video_id: {0}, using {1} client ({2})'
.format(self.video_id, client_name,
- 'logged in' if auth_header else 'logged out')
+ 'logged in' if '_access_token' in client_data
+ else 'logged out')
)
self._selected_client = client.copy()
@@ -1126,10 +1142,9 @@ def _get_video_info(self):
video_info_url, 'POST',
error_msg=('Caption request failed to get player response for'
'video_id: {0}'.format(self.video_id)),
- **self.build_client('smarttv_embedded', True)
+ **self.build_client('smarttv_embedded', client_data)
)
-
- response = result.json()
+ response = result and result.json() or {}
captions = response.get('captions')
if captions:
captions['headers'] = result.request.headers
diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/resources/lib/youtube_plugin/youtube/helper/yt_login.py
index 4afb9992a..ac7b2bbf8 100644
--- a/resources/lib/youtube_plugin/youtube/helper/yt_login.py
+++ b/resources/lib/youtube_plugin/youtube/helper/yt_login.py
@@ -11,7 +11,6 @@
from __future__ import absolute_import, division, unicode_literals
import copy
-import json
import time
from ..youtube_exceptions import LoginException
@@ -67,49 +66,53 @@ def _do_login(_for_tv=False):
'[CR]%s %s' % (context.localize('sign.enter_code'),
context.get_ui().bold(user_code))]
text = ''.join(text)
- dialog = context.get_ui().create_progress_dialog(
- heading=context.localize('sign.in'), text=text, background=False)
-
- steps = ((10 * 60 * 1000) // interval) # 10 Minutes
- dialog.set_total(steps)
- for _ in range(steps):
- dialog.update()
- try:
- if _for_tv:
- json_data = _client.request_access_token_tv(device_code)
- else:
- json_data = _client.request_access_token(device_code)
- except LoginException:
- _do_logout()
- raise
-
- log_data = copy.deepcopy(json_data)
- if 'access_token' in log_data:
- log_data['access_token'] = ''
- if 'refresh_token' in log_data:
- log_data['refresh_token'] = ''
- context.log_debug('Requesting access token: |%s|' % json.dumps(log_data))
-
- if 'error' not in json_data:
- _access_token = json_data.get('access_token', '')
- _expires_in = time.time() + int(json_data.get('expires_in', 3600))
- _refresh_token = json_data.get('refresh_token', '')
- dialog.close()
- if not _access_token and not _refresh_token:
- _expires_in = 0
- return _access_token, _expires_in, _refresh_token
-
- if json_data['error'] != 'authorization_pending':
- message = json_data['error']
- title = '%s: %s' % (context.get_name(), message)
- context.get_ui().show_notification(message, title)
- context.log_error('Error requesting access token: |%s|' % message)
-
- if dialog.is_aborted():
- break
-
- context.sleep(interval)
- dialog.close()
+
+ with context.get_ui().create_progress_dialog(
+ heading=context.localize('sign.in'), text=text, background=False
+ ) as dialog:
+ steps = ((10 * 60 * 1000) // interval) # 10 Minutes
+ dialog.set_total(steps)
+ for _ in range(steps):
+ dialog.update()
+ try:
+ if _for_tv:
+ json_data = _client.request_access_token_tv(device_code)
+ else:
+ json_data = _client.request_access_token(device_code)
+ except LoginException:
+ _do_logout()
+ raise
+
+ log_data = copy.deepcopy(json_data)
+ if 'access_token' in log_data:
+ log_data['access_token'] = ''
+ if 'refresh_token' in log_data:
+ log_data['refresh_token'] = ''
+ context.log_debug('Requesting access token: |{data}|'.format(
+ data=log_data
+ ))
+
+ if 'error' not in json_data:
+ _access_token = json_data.get('access_token', '')
+ _refresh_token = json_data.get('refresh_token', '')
+ if not _access_token and not _refresh_token:
+ _expires_in = 0
+ else:
+ _expires_in = (int(json_data.get('expires_in', 3600))
+ + time.time())
+ return _access_token, _expires_in, _refresh_token
+
+ if json_data['error'] != 'authorization_pending':
+ message = json_data['error']
+ title = '%s: %s' % (context.get_name(), message)
+ context.get_ui().show_notification(message, title)
+ context.log_error('Error requesting access token: |error|'
+ .format(error=message))
+
+ if dialog.is_aborted():
+ break
+
+ context.sleep(interval)
return '', 0, ''
if mode == 'out':
diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py
index 526a0af28..c3cf87cf0 100644
--- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py
+++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py
@@ -15,55 +15,58 @@
def _process_add_video(provider, context, keymap_action=False):
- listitem_path = context.get_ui().get_info_label('Container.ListItem(0).FileNameAndPath')
+ path = context.get_infolabel('Container.ListItem(0).FileNameAndPath')
client = provider.get_client(context)
+ logged_in = provider.is_logged_in()
+ if not logged_in:
+ raise KodionException('Playlist/Add: not logged in')
+
watch_later_id = context.get_access_manager().get_watch_later_id()
playlist_id = context.get_param('playlist_id', '')
- if not playlist_id:
- raise KodionException('Playlist/Add: missing playlist_id')
-
if playlist_id.lower() == 'watch_later':
playlist_id = watch_later_id
+ if not playlist_id:
+ raise KodionException('Playlist/Add: missing playlist_id')
+
video_id = context.get_param('video_id', '')
if not video_id:
- if context.is_plugin_path(listitem_path, 'play/'):
- video_id = find_video_id(listitem_path)
+ if context.is_plugin_path(path, 'play/'):
+ video_id = find_video_id(path)
keymap_action = True
if not video_id:
raise KodionException('Playlist/Add: missing video_id')
- if playlist_id != 'HL':
- json_data = client.add_video_to_playlist(playlist_id=playlist_id, video_id=video_id)
- if not json_data:
- return False
-
- if playlist_id == watch_later_id:
- notify_message = context.localize('watch_later.added_to')
- else:
- notify_message = context.localize('playlist.added_to')
+ json_data = client.add_video_to_playlist(playlist_id=playlist_id,
+ video_id=video_id)
+ if not json_data:
+ context.log_debug('Playlist/Add: failed for playlist |{playlist_id}|'
+ .format(playlist_id=playlist_id))
+ return False
- context.get_ui().show_notification(
- message=notify_message,
- time_ms=2500,
- audible=False
- )
+ if playlist_id == watch_later_id:
+ notify_message = context.localize('watch_later.added_to')
+ else:
+ notify_message = context.localize('playlist.added_to')
- if keymap_action:
- context.get_ui().set_focus_next_item()
+ context.get_ui().show_notification(
+ message=notify_message,
+ time_ms=2500,
+ audible=False
+ )
- return True
+ if keymap_action:
+ context.get_ui().set_focus_next_item()
- context.log_debug('Cannot add to playlist id |%s|' % playlist_id)
- return False
+ return True
def _process_remove_video(provider, context):
- listitem_playlist_id = context.get_ui().get_info_label('Container.ListItem(0).Property(playlist_id)')
- listitem_playlist_item_id = context.get_ui().get_info_label('Container.ListItem(0).Property(playlist_item_id)')
- listitem_title = context.get_ui().get_info_label('Container.ListItem(0).Title')
+ listitem_playlist_id = context.get_infolabel('Container.ListItem(0).Property(playlist_id)')
+ listitem_playlist_item_id = context.get_infolabel('Container.ListItem(0).Property(playlist_item_id)')
+ listitem_title = context.get_infolabel('Container.ListItem(0).Title')
keymap_action = False
playlist_id = context.get_param('playlist_id', '')
@@ -91,7 +94,7 @@ def _process_remove_video(provider, context):
else:
raise KodionException('Playlist/Remove: missing video_name')
- if playlist_id != 'HL' and playlist_id.strip().lower() != 'wl':
+ if playlist_id.strip().lower() not in ('wl', 'hl'):
if context.get_ui().on_remove_content(video_name):
json_data = provider.get_client(context).remove_video_from_playlist(playlist_id=playlist_id,
playlist_item_id=video_id)
@@ -135,16 +138,18 @@ def _process_remove_playlist(provider, context):
def _process_select_playlist(provider, context):
+ # Get listitem path asap, relies on listitems focus
+ path = context.get_infolabel('Container.ListItem(0).FileNameAndPath')
+
ui = context.get_ui()
- listitem_path = ui.get_info_label('Container.ListItem(0).FileNameAndPath') # do this asap, relies on listitems focus
keymap_action = False
page_token = ''
current_page = 0
video_id = context.get_param('video_id', '')
if not video_id:
- if context.is_plugin_path(listitem_path, 'play/'):
- video_id = find_video_id(listitem_path)
+ if context.is_plugin_path(path, 'play/'):
+ video_id = find_video_id(path)
if video_id:
context.set_param('video_id', video_id)
keymap_action = True
@@ -181,11 +186,11 @@ def _process_select_playlist(provider, context):
resource_manager = provider.get_resource_manager(context)
my_playlists = resource_manager.get_related_playlists(channel_id='mine')
if 'watchLater' in my_playlists:
- watch_later_playlist_id = context.get_access_manager().get_watch_later_id()
- if watch_later_playlist_id:
+ watch_later_id = context.get_access_manager().get_watch_later_id()
+ if watch_later_id:
items.append((
ui.bold(context.localize('watch_later')), '',
- watch_later_playlist_id,
+ watch_later_id,
context.create_resource_path('media', 'watch_later.png')
))
@@ -249,7 +254,7 @@ def _process_rename_playlist(provider, context):
context.get_ui().refresh_container()
-def _watchlater_playlist_id_change(context, method):
+def _watch_later_playlist_id_change(context, method):
playlist_id = context.get_param('playlist_id', '')
if not playlist_id:
raise KodionException('watchlater_list/%s: missing playlist_id' % method)
@@ -264,7 +269,7 @@ def _watchlater_playlist_id_change(context, method):
return
elif method == 'remove':
if context.get_ui().on_yes_no_input(context.get_name(), context.localize('watch_later.list.remove.confirm') % playlist_name):
- context.get_access_manager().set_watch_later_id(' WL')
+ context.get_access_manager().set_watch_later_id('WL')
else:
return
else:
@@ -306,8 +311,8 @@ def process(method, category, provider, context):
return _process_select_playlist(provider, context)
if method == 'rename' and category == 'playlist':
return _process_rename_playlist(provider, context)
- if method in {'set', 'remove'} and category == 'watchlater':
- return _watchlater_playlist_id_change(context, method)
+ if method in {'set', 'remove'} and category == 'watch_later':
+ return _watch_later_playlist_id_change(context, method)
if method in {'set', 'remove'} and category == 'history':
return _history_playlist_id_change(context, method)
raise KodionException("Unknown category '%s' or method '%s'" % (category, method))
diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py
index ec3f19393..1f5cc53b3 100644
--- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py
+++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py
@@ -25,24 +25,32 @@
def _process_related_videos(provider, context):
- provider.set_content_type(context, content.VIDEOS)
+ context.set_content(content.VIDEO_CONTENT)
+ function_cache = context.get_function_cache()
+
video_id = context.get_param('video_id', '')
- if not video_id:
- return []
+ if video_id:
+ json_data = function_cache.get(
+ provider.get_client(context).get_related_videos,
+ function_cache.ONE_HOUR,
+ video_id=video_id,
+ page_token=context.get_param('page_token', ''),
+ )
+ else:
+ json_data = function_cache.get(
+ provider.get_client(context).get_related_for_home,
+ function_cache.ONE_HOUR,
+ page_token=context.get_param('page_token', ''),
+ )
- json_data = provider.get_client(context).get_related_videos(
- video_id=video_id, page_token=context.get_param('page_token', '')
- )
if not json_data:
return False
- return v3.response_to_items(provider,
- context,
- json_data,
- process_next_page=False)
+ return v3.response_to_items(provider, context, json_data)
def _process_parent_comments(provider, context):
- provider.set_content_type(context, content.FILES)
+ context.set_content(content.LIST_CONTENT)
+
video_id = context.get_param('video_id', '')
if not video_id:
return []
@@ -50,13 +58,15 @@ def _process_parent_comments(provider, context):
json_data = provider.get_client(context).get_parent_comments(
video_id=video_id, page_token=context.get_param('page_token', '')
)
+
if not json_data:
return False
return v3.response_to_items(provider, context, json_data)
def _process_child_comments(provider, context):
- provider.set_content_type(context, content.FILES)
+ context.set_content(content.LIST_CONTENT)
+
parent_id = context.get_param('parent_id', '')
if not parent_id:
return []
@@ -64,54 +74,66 @@ def _process_child_comments(provider, context):
json_data = provider.get_client(context).get_child_comments(
parent_id=parent_id, page_token=context.get_param('page_token', '')
)
+
if not json_data:
return False
return v3.response_to_items(provider, context, json_data)
def _process_recommendations(provider, context):
- provider.set_content_type(context, content.VIDEOS)
- json_data = provider.get_client(context).get_activities(
- channel_id='home', page_token=context.get_param('page_token', '')
+ context.set_content(content.VIDEO_CONTENT)
+ params = context.get_params()
+ function_cache = context.get_function_cache()
+
+ json_data = function_cache.get(
+ provider.get_client(context).get_recommended_for_home,
+ function_cache.ONE_HOUR,
+ visitor=params.get('visitor', ''),
+ page_token=params.get('page_token', ''),
+ click_tracking=params.get('click_tracking', ''),
)
+
if not json_data:
return False
return v3.response_to_items(provider, context, json_data)
-def _process_popular_right_now(provider, context):
- provider.set_content_type(context, content.VIDEOS)
- json_data = provider.get_client(context).get_popular_videos(
+def _process_trending(provider, context):
+ context.set_content(content.VIDEO_CONTENT)
+
+ json_data = provider.get_client(context).get_trending_videos(
page_token=context.get_param('page_token', '')
)
+
if not json_data:
return False
return v3.response_to_items(provider, context, json_data)
def _process_browse_channels(provider, context):
- provider.set_content_type(context, content.FILES)
+ context.set_content(content.LIST_CONTENT)
client = provider.get_client(context)
+
guide_id = context.get_param('guide_id', '')
if guide_id:
json_data = client.get_guide_category(guide_id)
- if not json_data:
- return False
- return v3.response_to_items(provider, context, json_data)
+ else:
+ function_cache = context.get_function_cache()
+ json_data = function_cache.get(client.get_guide_categories,
+ function_cache.ONE_MONTH)
- function_cache = context.get_function_cache()
- json_data = function_cache.get(client.get_guide_categories,
- function_cache.ONE_MONTH)
if not json_data:
return False
return v3.response_to_items(provider, context, json_data)
def _process_disliked_videos(provider, context):
- provider.set_content_type(context, content.VIDEOS)
+ context.set_content(content.VIDEO_CONTENT)
+
json_data = provider.get_client(context).get_disliked_videos(
page_token=context.get_param('page_token', '')
)
+
if not json_data:
return False
return v3.response_to_items(provider, context, json_data)
@@ -121,13 +143,15 @@ def _process_live_events(provider, context, event_type='live'):
def _sort(x):
return x.get_date()
- provider.set_content_type(context, content.VIDEOS)
+ context.set_content(content.VIDEO_CONTENT)
+
# TODO: cache result
json_data = provider.get_client(context).get_live_events(
event_type=event_type,
page_token=context.get_param('page_token', ''),
location=context.get_param('location', False),
)
+
if not json_data:
return False
return v3.response_to_items(provider, context, json_data, sort=_sort)
@@ -139,50 +163,45 @@ def _process_description_links(provider, context):
addon_id = params.get('addon_id', '')
def _extract_urls(video_id):
- provider.set_content_type(context, content.VIDEOS)
+ context.set_content(content.VIDEO_CONTENT)
url_resolver = UrlResolver(context)
- progress_dialog = context.get_ui().create_progress_dialog(
+ with context.get_ui().create_progress_dialog(
heading=context.localize('please_wait'), background=False
- )
-
- resource_manager = provider.get_resource_manager(context)
-
- video_data = resource_manager.get_videos((video_id, ))
- yt_item = video_data[video_id]
- if not yt_item or 'snippet' not in yt_item:
- context.get_ui().on_ok(
- title=context.localize('video.description.links'),
- text=context.localize('video.description.links.not_found')
- )
- return False
- snippet = yt_item['snippet']
- description = strip_html_from_text(snippet['description'])
-
- function_cache = context.get_function_cache()
- urls = function_cache.get(extract_urls,
- function_cache.ONE_WEEK,
- description)
-
- progress_dialog.set_total(len(urls))
-
- res_urls = []
- for url in urls:
- progress_dialog.update(steps=1, text=url)
- resolved_url = url_resolver.resolve(url)
- res_urls.append(resolved_url)
-
- if progress_dialog.is_aborted():
- context.log_debug('Resolving urls aborted')
- break
-
- context.sleep(50)
-
- url_to_item_converter = UrlToItemConverter()
- url_to_item_converter.add_urls(res_urls, context)
- result = url_to_item_converter.get_items(provider, context)
-
- progress_dialog.close()
+ ) as progress_dialog:
+ resource_manager = provider.get_resource_manager(context)
+
+ video_data = resource_manager.get_videos((video_id,))
+ yt_item = video_data[video_id]
+ if not yt_item or 'snippet' not in yt_item:
+ context.get_ui().on_ok(
+ title=context.localize('video.description.links'),
+ text=context.localize('video.description.links.not_found')
+ )
+ return False
+ snippet = yt_item['snippet']
+ description = strip_html_from_text(snippet['description'])
+
+ function_cache = context.get_function_cache()
+ urls = function_cache.get(extract_urls,
+ function_cache.ONE_WEEK,
+ description)
+
+ progress_dialog.set_total(len(urls))
+
+ res_urls = []
+ for url in urls:
+ progress_dialog.update(steps=1, text=url)
+ resolved_url = url_resolver.resolve(url)
+ res_urls.append(resolved_url)
+
+ if progress_dialog.is_aborted():
+ context.log_debug('Resolving urls aborted')
+ break
+
+ url_to_item_converter = UrlToItemConverter()
+ url_to_item_converter.add_urls(res_urls, context)
+ result = url_to_item_converter.get_items(provider, context)
if result:
return result
@@ -260,47 +279,46 @@ def _display_playlists(playlist_ids):
def _process_saved_playlists_tv(provider, context):
- provider.set_content_type(context, content.FILES)
+ context.set_content(content.LIST_CONTENT)
+
json_data = provider.get_client(context).get_saved_playlists(
page_token=context.get_param('next_page_token', ''),
offset=context.get_param('offset', 0)
)
- return tv.saved_playlists_to_items(provider, context, json_data)
+ if not json_data:
+ return False
+ return tv.saved_playlists_to_items(provider, context, json_data)
-def _process_new_uploaded_videos_tv(provider, context):
- provider.set_content_type(context, content.VIDEOS)
- json_data = provider.get_client(context).get_my_subscriptions(
- page_token=context.get_param('next_page_token', ''),
- offset=context.get_param('offset', 0)
- )
- return tv.my_subscriptions_to_items(provider, context, json_data)
+def _process_new_uploaded_videos_tv(provider, context, filtered=False):
+ context.set_content(content.VIDEO_CONTENT)
-def _process_new_uploaded_videos_tv_filtered(provider, context):
- provider.set_content_type(context, content.VIDEOS)
json_data = provider.get_client(context).get_my_subscriptions(
page_token=context.get_param('next_page_token', ''),
offset=context.get_param('offset', 0)
)
+
+ if not json_data:
+ return False
return tv.my_subscriptions_to_items(provider,
context,
json_data,
- do_filter=True)
+ do_filter=filtered)
def process(category, provider, context):
_ = provider.get_client(context) # required for provider.is_logged_in()
if (not provider.is_logged_in()
- and category in ['new_uploaded_videos_tv',
+ and category in ('new_uploaded_videos_tv',
'new_uploaded_videos_tv_filtered',
- 'disliked_videos']):
+ 'disliked_videos')):
return UriItem(context.create_uri(('sign', 'in')))
if category == 'related_videos':
return _process_related_videos(provider, context)
if category == 'popular_right_now':
- return _process_popular_right_now(provider, context)
+ return _process_trending(provider, context)
if category == 'recommendations':
return _process_recommendations(provider, context)
if category == 'browse_channels':
@@ -308,7 +326,7 @@ def process(category, provider, context):
if category == 'new_uploaded_videos_tv':
return _process_new_uploaded_videos_tv(provider, context)
if category == 'new_uploaded_videos_tv_filtered':
- return _process_new_uploaded_videos_tv_filtered(provider, context)
+ return _process_new_uploaded_videos_tv(provider, context, filtered=True)
if category == 'disliked_videos':
return _process_disliked_videos(provider, context)
if category == 'live':
diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py
index 9a7a774c3..2d40944b4 100644
--- a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py
+++ b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py
@@ -29,7 +29,7 @@ def _process_list(provider, context):
def _process_add(provider, context):
- listitem_subscription_id = context.get_ui().get_info_label('Container.ListItem(0).Property(subscription_id)')
+ listitem_subscription_id = context.get_infolabel('Container.ListItem(0).Property(subscription_id)')
subscription_id = context.get_param('subscription_id', '')
if (not subscription_id and listitem_subscription_id
@@ -53,7 +53,7 @@ def _process_add(provider, context):
def _process_remove(provider, context):
- listitem_subscription_id = context.get_ui().get_info_label('Container.ListItem(0).Property(channel_subscription_id)')
+ listitem_subscription_id = context.get_infolabel('Container.ListItem(0).Property(channel_subscription_id)')
subscription_id = context.get_param('subscription_id', '')
if not subscription_id and listitem_subscription_id:
diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py
index b1e2511a7..c1e3bd527 100644
--- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py
+++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py
@@ -11,11 +11,12 @@
from __future__ import absolute_import, division, unicode_literals
from ...kodion import KodionException
+from ...kodion.items import menu_items
from ...kodion.utils import find_video_id
def _process_rate_video(provider, context, re_match):
- listitem_path = context.get_ui().get_info_label('Container.ListItem(0).FileNameAndPath')
+ listitem_path = context.get_infolabel('Container.ListItem(0).FileNameAndPath')
ratings = ['like', 'dislike', 'none']
rating_param = context.get_param('rating', '')
@@ -90,33 +91,25 @@ def _process_rate_video(provider, context, re_match):
def _process_more_for_video(context):
- video_id = context.get_param('video_id', '')
+ params = context.get_params()
+
+ video_id = params.get('video_id')
if not video_id:
raise KodionException('video/more/: missing video_id')
- items = []
-
- logged_in = context.get_param('logged_in', '0')
- if logged_in == '1':
- # add video to a playlist
- items.append((context.localize('video.add_to_playlist'),
- 'RunPlugin(%s)' % context.create_uri(['playlist', 'select', 'playlist'], {'video_id': video_id})))
-
- # default items
- items.extend([(context.localize('related_videos'),
- 'Container.Update(%s)' % context.create_uri(['special', 'related_videos'], {'video_id': video_id})),
- (context.localize('video.comments'),
- 'Container.Update(%s)' % context.create_uri(['special', 'parent_comments'], {'video_id': video_id})),
- (context.localize('video.description.links'),
- 'Container.Update(%s)' % context.create_uri(['special', 'description_links'],
- {'video_id': video_id}))])
-
- if logged_in == '1':
- # rate a video
- refresh_container = context.get_param('refresh_container', '0')
- items.append((context.localize('video.rate'),
- 'RunPlugin(%s)' % context.create_uri(['video', 'rate'], {'video_id': video_id,
- 'refresh_container': refresh_container})))
+ items = [
+ menu_items.add_video_to_playlist(context, video_id),
+ menu_items.related_videos(context, video_id),
+ menu_items.video_comments(context, video_id),
+ menu_items.content_from_description(context, video_id),
+ menu_items.rate_video(context,
+ video_id,
+ params.get('refresh_container')),
+ ] if params.get('logged_in') else [
+ menu_items.related_videos(context, video_id),
+ menu_items.video_comments(context, video_id),
+ menu_items.content_from_description(context, video_id),
+ ]
result = context.get_ui().on_select(context.localize('video.more'), items)
if result != -1:
diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py
index c1c72e2c2..26517f7c9 100644
--- a/resources/lib/youtube_plugin/youtube/provider.py
+++ b/resources/lib/youtube_plugin/youtube/provider.py
@@ -11,9 +11,7 @@
from __future__ import absolute_import, division, unicode_literals
import json
-import os
import re
-import socket
from base64 import b64decode
from .client import YouTube
@@ -33,18 +31,19 @@
)
from .youtube_exceptions import InvalidGrant, LoginException
from ..kodion import AbstractProvider, RegisterProviderPath
-from ..kodion.compatibility import xbmcaddon, xbmcvfs
from ..kodion.constants import (
ADDON_ID,
- DATA_PATH,
- TEMP_PATH,
content,
paths,
- sort,
)
-from ..kodion.items import DirectoryItem, NewSearchItem, SearchItem, menu_items
-from ..kodion.network import get_client_ip_address, is_httpd_live
-from ..kodion.utils import find_video_id, rm_dir, strip_html_from_text
+from ..kodion.items import (
+ DirectoryItem,
+ NewSearchItem,
+ SearchItem,
+ UriItem,
+ menu_items,
+)
+from ..kodion.utils import find_video_id, strip_html_from_text
class Provider(AbstractProvider):
@@ -57,11 +56,8 @@ def __init__(self):
self.yt_video = yt_video
- def get_wizard_supported_views(self):
- return ['default', 'episodes']
-
def get_wizard_steps(self, context):
- return [(yt_setup_wizard.process, [self, context])]
+ return [(yt_setup_wizard.process, (self, context))]
def is_logged_in(self):
return self._logged_in
@@ -285,7 +281,7 @@ def on_uri2addon(self, context, re_match):
@RegisterProviderPath('^(?:/channel/(?P[^/]+))?/playlist/(?P[^/]+)/$')
def _on_playlist(self, context, re_match):
- self.set_content_type(context, content.VIDEOS)
+ context.set_content(content.VIDEO_CONTENT)
resource_manager = self.get_resource_manager(context)
batch_id = (re_match.group('playlist_id'),
@@ -305,7 +301,7 @@ def _on_playlist(self, context, re_match):
@RegisterProviderPath('^/channel/(?P[^/]+)/playlists/$')
def _on_channel_playlists(self, context, re_match):
- self.set_content_type(context, content.FILES)
+ context.set_content(content.LIST_CONTENT)
result = []
channel_id = re_match.group('channel_id')
@@ -352,7 +348,7 @@ def _on_channel_playlists(self, context, re_match):
@RegisterProviderPath('^/channel/(?P[^/]+)/live/$')
def _on_channel_live(self, context, re_match):
- self.set_content_type(context, content.VIDEOS)
+ context.set_content(content.VIDEO_CONTENT)
result = []
channel_id = re_match.group('channel_id')
@@ -380,25 +376,31 @@ def _on_channel_live(self, context, re_match):
@RegisterProviderPath('^/(?P(channel|user))/(?P[^/]+)/$')
def _on_channel(self, context, re_match):
+ listitem_channel_id = context.get_infolabel(
+ 'Container.ListItem(0).Property(channel_id)'
+ )
+
client = self.get_client(context)
localize = context.localize
create_uri = context.create_uri
function_cache = context.get_function_cache()
ui = context.get_ui()
- listitem_channel_id = ui.get_info_label('Container.ListItem(0).Property(channel_id)')
-
method = re_match.group('method')
channel_id = re_match.group('channel_id')
- if (method == 'channel' and channel_id and channel_id.lower() == 'property'
- and listitem_channel_id and listitem_channel_id.lower().startswith(('mine', 'uc'))):
- context.execute('Container.Update(%s)' % create_uri(('channel', listitem_channel_id))) # redirect if keymap, without redirect results in 'invalid handle -1'
+ if (method == 'channel' and channel_id
+ and channel_id.lower() == 'property'
+ and listitem_channel_id
+ and listitem_channel_id.lower().startswith(('mine', 'uc'))):
+ context.execute('ActivateWindow(Videos, {channel}, return)'.format(
+ channel=create_uri(('channel', listitem_channel_id))
+ ))
if method == 'channel' and not channel_id:
return False
- self.set_content_type(context, content.VIDEOS)
+ context.set_content(content.VIDEO_CONTENT)
resource_manager = self.get_resource_manager(context)
@@ -496,7 +498,7 @@ def _on_channel(self, context, re_match):
# noinspection PyUnusedLocal
@RegisterProviderPath('^/location/mine/$')
def _on_my_location(self, context, re_match):
- self.set_content_type(context, content.FILES)
+ context.set_content(content.LIST_CONTENT)
create_uri = context.create_uri
localize = context.localize
@@ -571,7 +573,7 @@ def on_play(self, context, re_match):
if ({'channel_id', 'live', 'playlist_id', 'playlist_ids', 'video_id'}
.isdisjoint(params.keys())):
- path = ui.get_info_label('Container.ListItem(0).FileNameAndPath')
+ path = context.get_infolabel('Container.ListItem(0).FileNameAndPath')
if context.is_plugin_path(path, 'play/'):
video_id = find_video_id(path)
if video_id:
@@ -655,14 +657,14 @@ def _on_subscriptions(self, context, re_match):
subscriptions = yt_subscriptions.process(method, self, context)
if method == 'list':
- self.set_content_type(context, content.FILES)
+ context.set_content(content.LIST_CONTENT)
channel_ids = []
for subscription in subscriptions:
channel_ids.append(subscription.get_channel_id())
channel_ids = {subscription.get_channel_id(): subscription
for subscription in subscriptions}
channel_fanarts = resource_manager.get_fanarts(channel_ids)
- for channel_id, fanart in channel_fanarts:
+ for channel_id, fanart in channel_fanarts.items():
channel_ids[channel_id].set_fanart(fanart)
return subscriptions
@@ -675,109 +677,9 @@ def _on_yt_specials(self, context, re_match):
@RegisterProviderPath('^/users/(?P[^/]+)/$')
def _on_users(self, context, re_match):
action = re_match.group('action')
- refresh = context.get_param('refresh')
-
- localize = context.localize
- access_manager = context.get_access_manager()
- ui = context.get_ui()
-
- def select_user(reason, new_user=False):
- current_users = access_manager.get_users()
- current_user = access_manager.get_current_user()
- usernames = []
- for user, details in sorted(current_users.items()):
- username = details.get('name') or localize('user.unnamed')
- if user == current_user:
- username = '> ' + ui.bold(username)
- if details.get('access_token') or details.get('refresh_token'):
- username = ui.color('limegreen', username)
- usernames.append(username)
- if new_user:
- usernames.append(ui.italic(localize('user.new')))
- return ui.on_select(reason, usernames), sorted(current_users.keys())
-
- def add_user():
- results = ui.on_keyboard_input(localize('user.enter_name'))
- if results[0] is False:
- return None, None
- new_username = results[1].strip()
- if not new_username:
- new_username = localize('user.unnamed')
- return access_manager.add_user(new_username)
-
- def switch_to_user(user):
- access_manager.set_user(user, switch_to=True)
- ui.show_notification(
- localize('user.changed') % access_manager.get_username(user),
- localize('user.switch')
- )
- self.get_resource_manager(context).clear()
- if refresh:
- ui.refresh_container()
-
- if action == 'switch':
- result, user_index_map = select_user(localize('user.switch'),
- new_user=True)
- if result == -1:
- return True
- if result == len(user_index_map):
- user, _ = add_user()
- else:
- user = user_index_map[result]
-
- if user is not None and user != access_manager.get_current_user():
- switch_to_user(user)
-
- elif action == 'add':
- user, details = add_user()
- if user is not None:
- result = ui.on_yes_no_input(
- localize('user.switch'),
- localize('user.switch.now') % details.get('name')
- )
- if result:
- switch_to_user(user)
-
- elif action == 'remove':
- result, user_index_map = select_user(localize('user.remove'))
- if result == -1:
- return True
-
- user = user_index_map[result]
- username = access_manager.get_username(user)
- if ui.on_remove_content(username):
- access_manager.remove_user(user)
- if user == 0:
- access_manager.add_user(username=localize('user.default'),
- user=0)
- if user == access_manager.get_current_user():
- access_manager.set_user(0, switch_to=True)
- ui.show_notification(localize('removed') % username,
- localize('remove'))
-
- elif action == 'rename':
- result, user_index_map = select_user(localize('user.rename'))
- if result == -1:
- return True
-
- user = user_index_map[result]
- old_username = access_manager.get_username(user)
- results = ui.on_keyboard_input(localize('user.enter_name'),
- default=old_username)
- if results[0] is False:
- return True
- new_username = results[1].strip()
- if not new_username:
- new_username = localize('user.unnamed')
- if old_username == new_username:
- return True
-
- if access_manager.set_username(user, new_username):
- ui.show_notification(localize('renamed') % (old_username,
- new_username),
- localize('rename'))
-
- return True
+ return UriItem('{addon},users/{action}'.format(
+ addon=ADDON_ID, action=action
+ ))
@RegisterProviderPath('^/sign/(?P[^/]+)/$')
def _on_sign(self, context, re_match):
@@ -826,6 +728,7 @@ def on_search(self, search_text, context, re_match):
return result
context.set_param('q', search_text)
+ context.set_param('category_label', search_text)
params = context.get_params()
channel_id = params.get('channel_id')
@@ -838,9 +741,9 @@ def on_search(self, search_text, context, re_match):
safe_search = context.get_settings().safe_search()
if search_type == 'video':
- self.set_content_type(context, content.VIDEOS)
+ context.set_content(content.VIDEO_CONTENT)
else:
- self.set_content_type(context, content.FILES)
+ context.set_content(content.LIST_CONTENT)
if page == 1 and search_type == 'video' and not event_type and not hide_folders:
if not channel_id and not location:
@@ -891,61 +794,12 @@ def on_search(self, search_text, context, re_match):
result.extend(v3.response_to_items(self, context, json_data))
return result
- @RegisterProviderPath('^/config/(?P[^/]+)/$')
+ @RegisterProviderPath('^/config/(?P[^/]+)/$')
def configure_addon(self, context, re_match):
- switch = re_match.group('switch')
- localize = context.localize
- settings = context.get_settings()
- ui = context.get_ui()
-
- if switch == 'youtube':
- context.addon().openSettings()
- ui.refresh_container()
- elif switch == 'isa':
- if context.use_inputstream_adaptive():
- xbmcaddon.Addon(id='inputstream.adaptive').openSettings()
- else:
- settings.set_bool('kodion.video.quality.isa', False)
- elif switch == 'subtitles':
- yt_language = settings.get_string('youtube.language', 'en-US')
- sub_setting = settings.subtitle_languages()
-
- if yt_language.startswith('en'):
- sub_opts = [localize('none'),
- localize('prompt'),
- localize('subtitles.with_fallback') % ('en', 'en-US/en-GB'),
- yt_language,
- '%s (%s)' % (yt_language, localize('subtitles.no_auto_generated'))]
-
- else:
- sub_opts = [localize('none'),
- localize('prompt'),
- localize('subtitles.with_fallback') % (yt_language, 'en'),
- yt_language,
- '%s (%s)' % (yt_language, localize('subtitles.no_auto_generated'))]
-
- sub_opts[sub_setting] = ui.bold(sub_opts[sub_setting])
-
- result = ui.on_select(localize('subtitles.language'), sub_opts)
- if result > -1:
- settings.set_subtitle_languages(result)
-
- result = ui.on_yes_no_input(
- localize('subtitles.download'),
- localize('subtitles.download.pre')
- )
- if result > -1:
- settings.set_subtitle_download(result == 1)
- elif switch == 'listen_ip':
- local_ranges = ('10.', '172.16.', '192.168.')
- addresses = [iface[4][0]
- for iface in socket.getaddrinfo(socket.gethostname(), None)
- if iface[4][0].startswith(local_ranges)]
- addresses += ['127.0.0.1', '0.0.0.0']
- selected_address = ui.on_select(localize('select.listen.ip'), addresses)
- if selected_address != -1:
- settings.set_httpd_listen(addresses[selected_address])
- return False
+ action = re_match.group('action')
+ return UriItem('{addon},config/{action}'.format(
+ addon=ADDON_ID, action=action
+ ))
# noinspection PyUnusedLocal
@RegisterProviderPath('^/my_subscriptions/filter/$')
@@ -989,101 +843,41 @@ def manage_my_subscription_filter(self, context, re_match):
ui.show_notification(message=message)
ui.refresh_container()
- @RegisterProviderPath('^/maintain/(?P[^/]+)/(?P[^/]+)/$')
+ @RegisterProviderPath('^/maintenance/(?P[^/]+)/(?P[^/]+)/$')
def maintenance_actions(self, context, re_match):
- maint_type = re_match.group('maint_type')
+ target = re_match.group('target')
action = re_match.group('action')
+ if action != 'reset':
+ return UriItem('{addon},maintenance/{action}/{target}'.format(
+ addon=ADDON_ID, action=action, target=target
+ ))
+
ui = context.get_ui()
localize = context.localize
- if action == 'clear':
- if maint_type == 'function_cache':
- if ui.on_remove_content(localize('cache.function')):
- context.get_function_cache().clear()
- ui.show_notification(localize('succeeded'))
- elif maint_type == 'data_cache':
- if ui.on_remove_content(localize('cache.data')):
- context.get_data_cache().clear()
- ui.show_notification(localize('succeeded'))
- elif maint_type == 'search_cache':
- if ui.on_remove_content(localize('search.history')):
- context.get_search_history().clear()
- ui.show_notification(localize('succeeded'))
- elif maint_type == 'playback_history' and ui.on_remove_content(localize('playback.history')):
- context.get_playback_history().clear()
- ui.show_notification(localize('succeeded'))
- elif action == 'reset':
- if maint_type == 'access_manager' and ui.on_yes_no_input(context.get_name(), localize('reset.access_manager.confirm')):
- try:
- context.get_function_cache().clear()
- access_manager = context.get_access_manager()
- client = self.get_client(context)
- if access_manager.has_refresh_token():
- refresh_tokens = access_manager.get_refresh_token().split('|')
- for refresh_token in set(refresh_tokens):
- try:
- client.revoke(refresh_token)
- except:
- pass
- self.reset_client()
- access_manager.update_access_token(access_token='', refresh_token='')
- ui.refresh_container()
- ui.show_notification(localize('succeeded'))
- except:
- ui.show_notification(localize('failed'))
- elif action == 'delete':
- _maint_files = {'function_cache': 'cache.sqlite',
- 'search_cache': 'search.sqlite',
- 'data_cache': 'data_cache.sqlite',
- 'playback_history': 'playback_history',
- 'settings_xml': 'settings.xml',
- 'api_keys': 'api_keys.json',
- 'access_manager': 'access_manager.json',
- 'temp_files': TEMP_PATH}
- _file = _maint_files.get(maint_type)
- succeeded = False
-
- if not _file:
- return
-
- data_path = xbmcvfs.translatePath(DATA_PATH)
- if 'sqlite' in _file:
- _file_w_path = os.path.join(data_path, 'kodion', _file)
- elif maint_type == 'temp_files':
- _file_w_path = _file
- elif maint_type == 'playback_history':
- _file = ''.join((
- context.get_access_manager().get_current_user_id(),
- '.sqlite'
- ))
- _file_w_path = os.path.join(data_path, 'playback', _file)
- else:
- _file_w_path = os.path.join(data_path, _file)
-
- if not ui.on_delete_content(_file):
- return
-
- if maint_type == 'temp_files':
- succeeded = rm_dir(_file_w_path)
-
- elif _file_w_path:
- succeeded = xbmcvfs.delete(_file_w_path)
-
- if succeeded:
+ if (target == 'access_manager' and ui.on_yes_no_input(
+ context.get_name(), localize('reset.access_manager.confirm')
+ )):
+ try:
+ context.get_function_cache().clear()
+ access_manager = context.get_access_manager()
+ client = self.get_client(context)
+ if access_manager.has_refresh_token():
+ refresh_tokens = access_manager.get_refresh_token()
+ for refresh_token in set(refresh_tokens.split('|')):
+ try:
+ client.revoke(refresh_token)
+ except:
+ pass
+ self.reset_client()
+ access_manager.update_access_token(access_token='',
+ refresh_token='')
+ ui.refresh_container()
ui.show_notification(localize('succeeded'))
- else:
+ except:
ui.show_notification(localize('failed'))
- elif action == 'install' and maint_type == 'inputstreamhelper':
- if context.get_system_version().get_version()[0] >= 17:
- try:
- xbmcaddon.Addon('script.module.inputstreamhelper')
- ui.show_notification(localize('inputstreamhelper.is_installed'))
- except RuntimeError:
- context.execute('InstallAddon(script.module.inputstreamhelper)')
- else:
- ui.show_notification(localize('requires.krypton'))
# noinspection PyUnusedLocal
@RegisterProviderPath('^/api/update/$')
@@ -1139,20 +933,6 @@ def api_key_update(self, context, re_match):
ui.show_notification(localize('api.personal.failed') % ', '.join(missing_list))
context.log_debug('Failed to enable personal API keys. Missing: %s' % ', '.join(log_list))
- # noinspection PyUnusedLocal
- @RegisterProviderPath('^/show_client_ip/$')
- def show_client_ip(self, context, re_match):
- port = context.get_settings().httpd_port()
-
- if is_httpd_live(port=port):
- client_ip = get_client_ip_address(port=port)
- if client_ip:
- context.get_ui().on_ok(context.get_name(), context.localize('client.ip') % client_ip)
- else:
- context.get_ui().show_notification(context.localize('client.ip.failed'))
- else:
- context.get_ui().show_notification(context.localize('httpd.not.running'))
-
# noinspection PyUnusedLocal
def on_playback_history(self, context, re_match):
params = context.get_params()
@@ -1163,6 +943,7 @@ def on_playback_history(self, context, re_match):
playback_history = context.get_playback_history()
if action == 'list':
+ context.set_content(content.VIDEO_CONTENT, sub_type='history')
play_data = playback_history.get_items()
if not play_data:
return True
@@ -1251,7 +1032,7 @@ def on_root(self, context, re_match):
_ = self.get_client(context) # required for self.is_logged_in()
logged_in = self.is_logged_in()
- self.set_content_type(context, content.FILES)
+ # context.set_content(content.LIST_CONTENT)
result = []
@@ -1289,26 +1070,37 @@ def on_root(self, context, re_match):
result.append(my_subscriptions_filtered_item)
access_manager = context.get_access_manager()
-
- # Recommendations
- if logged_in and settings.get_bool('youtube.folder.recommendations.show', True):
- watch_history_playlist_id = access_manager.get_watch_history_id()
- if watch_history_playlist_id != 'HL':
- recommendations_item = DirectoryItem(
- localize('recommendations'),
- create_uri(('special', 'recommendations')),
- image='{media}/popular.png',
+ watch_later_id = logged_in and access_manager.get_watch_later_id()
+ history_id = logged_in and access_manager.get_watch_history_id()
+ local_history = settings.use_local_history()
+
+ # Home / Recommendations
+ if settings.get_bool('youtube.folder.recommendations.show', True):
+ recommendations_item = DirectoryItem(
+ localize('recommendations'),
+ create_uri(('special', 'recommendations')),
+ image='{media}/home.png',
+ )
+ result.append(recommendations_item)
+
+ # Related
+ if settings.get_bool('youtube.folder.related.show', True):
+ if history_id or local_history:
+ related_item = DirectoryItem(
+ localize('related_videos'),
+ create_uri(('special', 'related_videos')),
+ image='{media}/related_videos.png',
)
- result.append(recommendations_item)
+ result.append(related_item)
- # what to watch
+ # Trending
if settings.get_bool('youtube.folder.popular_right_now.show', True):
- what_to_watch_item = DirectoryItem(
- localize('popular_right_now'),
+ trending_item = DirectoryItem(
+ localize('trending'),
create_uri(('special', 'popular_right_now')),
- image='{media}/popular.png',
+ image='{media}/trending.png',
)
- result.append(what_to_watch_item)
+ result.append(trending_item)
# search
if settings.get_bool('youtube.folder.search.show', True):
@@ -1354,16 +1146,15 @@ def on_root(self, context, re_match):
# watch later
if settings.get_bool('youtube.folder.watch_later.show', True):
- playlist_id = logged_in and access_manager.get_watch_later_id()
- if playlist_id:
+ if watch_later_id:
watch_later_item = DirectoryItem(
localize('watch_later'),
- create_uri(('channel', 'mine', 'playlist', playlist_id)),
+ create_uri(('channel', 'mine', 'playlist', watch_later_id)),
image='{media}/watch_later.png',
)
context_menu = [
menu_items.play_all_from_playlist(
- context, playlist_id
+ context, watch_later_id
)
]
watch_later_item.set_context_menu(context_menu)
@@ -1405,21 +1196,20 @@ def on_root(self, context, re_match):
# history
if settings.get_bool('youtube.folder.history.show', False):
- playlist_id = logged_in and access_manager.get_watch_history_id()
- if playlist_id and playlist_id != 'HL':
+ if history_id:
watch_history_item = DirectoryItem(
localize('history'),
- create_uri(('channel', 'mine', 'playlist', playlist_id)),
+ create_uri(('channel', 'mine', 'playlist', history_id)),
image='{media}/history.png',
)
context_menu = [
menu_items.play_all_from_playlist(
- context, playlist_id
+ context, history_id
)
]
watch_history_item.set_context_menu(context_menu)
result.append(watch_history_item)
- elif settings.use_local_history():
+ elif local_history:
watch_history_item = DirectoryItem(
localize('history'),
create_uri([paths.HISTORY], params={'action': 'list'}),
@@ -1521,24 +1311,6 @@ def on_root(self, context, re_match):
return result
- @staticmethod
- def set_content_type(context, content_type):
- context.set_content_type(content_type)
- context.add_sort_method(
- (sort.UNSORTED, '%T \u2022 %P', '%D | %J'),
- (sort.LABEL_IGNORE_THE, '%T \u2022 %P', '%D | %J'),
- )
- if content_type != content.VIDEOS:
- return
- context.add_sort_method(
- (sort.PROGRAM_COUNT, '%T \u2022 %P | %D | %J', '%C'),
- (sort.VIDEO_RATING, '%T \u2022 %P | %D | %J', '%R'),
- (sort.DATE, '%T \u2022 %P | %D', '%J'),
- (sort.DATEADDED, '%T \u2022 %P | %D', '%a'),
- (sort.VIDEO_RUNTIME, '%T \u2022 %P | %J', '%D'),
- (sort.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'),
- )
-
def handle_exception(self, context, exception_to_handle):
if isinstance(exception_to_handle, (InvalidGrant, LoginException)):
ok_dialog = False
diff --git a/resources/media/what_to_watch.png b/resources/media/home.png
similarity index 100%
rename from resources/media/what_to_watch.png
rename to resources/media/home.png
diff --git a/resources/media/related_videos.png b/resources/media/related_videos.png
new file mode 100644
index 000000000..fdcee48ac
Binary files /dev/null and b/resources/media/related_videos.png differ
diff --git a/resources/media/popular.png b/resources/media/trending.png
similarity index 100%
rename from resources/media/popular.png
rename to resources/media/trending.png
diff --git a/resources/settings.xml b/resources/settings.xml
index bf0da9d9b..fbe786f5c 100644
--- a/resources/settings.xml
+++ b/resources/settings.xml
@@ -39,7 +39,6 @@
0
- RunPlugin(plugin://plugin.video.youtube/config/subtitles/)
true
@@ -51,6 +50,7 @@
true
+ RunScript($ID,config/subtitles)
true
@@ -214,7 +214,6 @@
0
- RunPlugin(plugin://plugin.video.youtube/config/isa/)
true
@@ -223,6 +222,7 @@
true
+ RunScript($ID,config/isa)
true
@@ -351,7 +351,6 @@
0
- RunPlugin(plugin://plugin.video.youtube/maintain/inputstreamhelper/install/)
true
@@ -364,6 +363,7 @@
+ RunScript($ID,config/inputstreamhelper)
@@ -421,6 +421,11 @@
true
+
+ 0
+ true
+
+
0
true
@@ -578,34 +583,34 @@
0
- RunPlugin(plugin://plugin.video.youtube/users/add/?refresh=false)
true
+ RunScript($ID,users/add)
0
- RunPlugin(plugin://plugin.video.youtube/users/remove/?refresh=false)
true
+ RunScript($ID,users/remove)
0
- RunPlugin(plugin://plugin.video.youtube/users/rename/?refresh=false)
true
+ RunScript($ID,users/rename)
0
- RunPlugin(plugin://plugin.video.youtube/users/switch/?refresh=false)
true
+ RunScript($ID,users/switch/refresh=True)
@@ -679,7 +684,7 @@
0
- false
+ true
@@ -691,7 +696,7 @@
0
- 10
+ 20
5
1
@@ -782,10 +787,10 @@
0
- RunPlugin(plugin://plugin.video.youtube/config/listen_ip/)
true
+ RunScript($ID,config/listen_ip)
true
@@ -823,10 +828,10 @@
0
- RunPlugin(plugin://plugin.video.youtube/show_client_ip/)
true
+ RunScript($ID,config/show_client_ip)
@@ -881,78 +886,78 @@
0
- RunPlugin(plugin://plugin.video.youtube/maintain/function_cache/clear/)
true
+ RunScript($ID,maintenance/clear/function_cache)
0
- RunPlugin(plugin://plugin.video.youtube/maintain/data_cache/clear/)
true
+ RunScript($ID,maintenance/clear/data_cache)
0
- RunPlugin(plugin://plugin.video.youtube/maintain/search_cache/clear/)
true
+ RunScript($ID,maintenance/clear/search_cache)
0
- RunPlugin(plugin://plugin.video.youtube/maintain/playback_history/clear/)
true
+ RunScript($ID,maintenance/clear/playback_history)
0
- RunPlugin(plugin://plugin.video.youtube/maintain/function_cache/delete/)
true
+ RunScript($ID,maintenance/delete/function_cache)
0
- RunPlugin(plugin://plugin.video.youtube/maintain/data_cache/delete/)
true
+ RunScript($ID,maintenance/delete/data_cache)
0
- RunPlugin(plugin://plugin.video.youtube/maintain/search_cache/delete/)
true
+ RunScript($ID,maintenance/delete/search_cache)
0
- RunPlugin(plugin://plugin.video.youtube/maintain/playback_history/delete/)
true
+ RunScript($ID,maintenance/delete/playback_history)
0
- RunPlugin(plugin://plugin.video.youtube/maintain/access_manager/reset/)
true
+ RunPlugin(plugin://$ID/maintenance/reset/access_manager)
true
@@ -961,95 +966,38 @@
0
- RunPlugin(plugin://plugin.video.youtube/maintain/settings_xml/delete/)
true
+ RunScript($ID,maintenance/delete/settings_xml)
true
0
- RunPlugin(plugin://plugin.video.youtube/maintain/api_keys/delete/)
true
+ RunScript($ID,maintenance/delete/api_keys)
0
- RunPlugin(plugin://plugin.video.youtube/maintain/access_manager/delete/)
true
+ RunScript($ID,maintenance/delete/access_manager)
0
- RunPlugin(plugin://plugin.video.youtube/maintain/temp_files/delete/)
true
+ RunScript($ID,maintenance/delete/temp_files)
-
- 0
-
-
- true
-
-
-
- false
-
-
-
-
-
-
-
- 0
-
-
- true
-
-
-
- false
-
-
-
-
-
-
-
- 0
- 0
-
-
- false
-
-
-
-
-
-
-
- 0
-
-
- true
-
-
-
- false
-
-
-
-
-
-