mirror of
https://github.com/anxdpanic/plugin.video.youtube.git
synced 2025-12-05 18:20:41 -08:00
- Avoid using dir() - Remove custom url quote methods that are no longer faster than urllib.parse methods in newer Python versions - Reduce polling intervals when checking if Kodi is busy - Use custom requests.Session class to avoid creation of unused default https adapter and ssl context
707 lines
21 KiB
Python
707 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
|
|
Copyright (C) 2014-2016 bromix (plugin.video.youtube)
|
|
Copyright (C) 2016-2025 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
|
|
|
|
from .. import logging
|
|
from ..compatibility import (
|
|
parse_qsl,
|
|
quote,
|
|
string_type,
|
|
to_str,
|
|
unquote,
|
|
urlencode,
|
|
urlsplit,
|
|
)
|
|
from ..constants import (
|
|
ACTION,
|
|
ADDON_ID_PARAM,
|
|
BOOL_FROM_STR,
|
|
CHANNEL_ID,
|
|
CHANNEL_IDS,
|
|
CLIP,
|
|
CONTEXT_MENU,
|
|
END,
|
|
FANART_TYPE,
|
|
HIDE_CHANNELS,
|
|
HIDE_FOLDERS,
|
|
HIDE_LIVE,
|
|
HIDE_MEMBERS,
|
|
HIDE_NEXT_PAGE,
|
|
HIDE_PLAYLISTS,
|
|
HIDE_PROGRESS,
|
|
HIDE_SEARCH,
|
|
HIDE_SHORTS,
|
|
HIDE_VIDEOS,
|
|
INCOGNITO,
|
|
ITEMS_PER_PAGE,
|
|
ITEM_FILTER,
|
|
KEYMAP,
|
|
LIVE,
|
|
ORDER,
|
|
PAGE,
|
|
PATHS,
|
|
PLAYLIST_ID,
|
|
PLAYLIST_IDS,
|
|
PLAYLIST_ITEM_ID,
|
|
PLAY_FORCE_AUDIO,
|
|
PLAY_PROMPT_QUALITY,
|
|
PLAY_PROMPT_SUBTITLES,
|
|
PLAY_STRM,
|
|
PLAY_TIMESHIFT,
|
|
PLAY_USING,
|
|
SCREENSAVER,
|
|
SEEK,
|
|
START,
|
|
SUBSCRIPTION_ID,
|
|
VIDEO_ID,
|
|
VIDEO_IDS,
|
|
WINDOW_CACHE,
|
|
WINDOW_FALLBACK,
|
|
WINDOW_REPLACE,
|
|
WINDOW_RETURN,
|
|
)
|
|
from ..sql_store import (
|
|
BookmarksList,
|
|
DataCache,
|
|
FeedHistory,
|
|
FunctionCache,
|
|
PlaybackHistory,
|
|
RequestCache,
|
|
SearchHistory,
|
|
WatchLaterList,
|
|
)
|
|
from ..utils.system_version import current_system_version
|
|
|
|
|
|
class AbstractContext(object):
|
|
log = logging.getLogger(__name__)
|
|
|
|
_initialized = False
|
|
_addon = None
|
|
_settings = None
|
|
|
|
_BOOL_PARAMS = frozenset((
|
|
CONTEXT_MENU,
|
|
KEYMAP,
|
|
PLAY_FORCE_AUDIO,
|
|
PLAY_PROMPT_SUBTITLES,
|
|
PLAY_PROMPT_QUALITY,
|
|
PLAY_STRM,
|
|
PLAY_TIMESHIFT,
|
|
PLAY_USING,
|
|
'confirmed',
|
|
CLIP,
|
|
'enable',
|
|
HIDE_CHANNELS,
|
|
HIDE_FOLDERS,
|
|
HIDE_LIVE,
|
|
HIDE_MEMBERS,
|
|
HIDE_NEXT_PAGE,
|
|
HIDE_PLAYLISTS,
|
|
HIDE_PROGRESS,
|
|
HIDE_SEARCH,
|
|
HIDE_SHORTS,
|
|
HIDE_VIDEOS,
|
|
INCOGNITO,
|
|
'location',
|
|
'logged_in',
|
|
'resume',
|
|
SCREENSAVER,
|
|
WINDOW_CACHE,
|
|
WINDOW_FALLBACK,
|
|
WINDOW_REPLACE,
|
|
WINDOW_RETURN,
|
|
))
|
|
_INT_PARAMS = frozenset((
|
|
FANART_TYPE,
|
|
'filtered',
|
|
ITEMS_PER_PAGE,
|
|
LIVE,
|
|
'next_page_token',
|
|
PAGE,
|
|
'refresh',
|
|
))
|
|
_INT_BOOL_PARAMS = frozenset((
|
|
'refresh',
|
|
))
|
|
_FLOAT_PARAMS = frozenset((
|
|
END,
|
|
'recent_days',
|
|
SEEK,
|
|
START,
|
|
))
|
|
_LIST_PARAMS = frozenset((
|
|
CHANNEL_IDS,
|
|
'exclude',
|
|
ITEM_FILTER,
|
|
PLAYLIST_IDS,
|
|
VIDEO_IDS,
|
|
))
|
|
_STRING_PARAMS = frozenset((
|
|
'api_key',
|
|
ACTION,
|
|
ADDON_ID_PARAM,
|
|
'category_label',
|
|
CHANNEL_ID,
|
|
'client_id',
|
|
'client_secret',
|
|
'click_tracking',
|
|
'event_type',
|
|
'item',
|
|
'item_id',
|
|
'item_name',
|
|
ORDER,
|
|
'page_token',
|
|
'parent_id',
|
|
'playlist', # deprecated
|
|
PLAYLIST_ITEM_ID,
|
|
PLAYLIST_ID,
|
|
'q',
|
|
'rating',
|
|
'reload_path',
|
|
'search_type',
|
|
SUBSCRIPTION_ID,
|
|
'uri',
|
|
'videoid', # deprecated
|
|
VIDEO_ID,
|
|
'visitor',
|
|
))
|
|
_STRING_BOOL_PARAMS = frozenset((
|
|
'logged_in',
|
|
'reload_path',
|
|
))
|
|
_STRING_INT_PARAMS = frozenset((
|
|
))
|
|
_NON_EMPTY_STRING_PARAMS = set()
|
|
|
|
def __init__(self, path='/', params=None, plugin_id=''):
|
|
self._access_manager = None
|
|
self._uuid = None
|
|
self._api_store = None
|
|
|
|
self._bookmarks_list = None
|
|
self._data_cache = None
|
|
self._feed_history = None
|
|
self._function_cache = None
|
|
self._playback_history = None
|
|
self._requests_cache = None
|
|
self._search_history = None
|
|
self._watch_later_list = None
|
|
|
|
self._plugin_handle = -1
|
|
self._plugin_id = plugin_id
|
|
self._plugin_name = None
|
|
self._plugin_icon = None
|
|
self._version = 'UNKNOWN'
|
|
|
|
self._param_string = ''
|
|
self._params = params or {}
|
|
if params:
|
|
self.parse_params(params)
|
|
|
|
self._uri = None
|
|
self._path = None
|
|
self._path_parts = []
|
|
self.set_path(path, force=True)
|
|
|
|
@staticmethod
|
|
def format_date_short(date_obj, str_format=None):
|
|
raise NotImplementedError()
|
|
|
|
@staticmethod
|
|
def format_time(time_obj, str_format=None):
|
|
raise NotImplementedError()
|
|
|
|
@staticmethod
|
|
def get_language():
|
|
raise NotImplementedError()
|
|
|
|
def get_language_name(self, lang_id=None):
|
|
raise NotImplementedError()
|
|
|
|
def get_player_language(self):
|
|
raise NotImplementedError()
|
|
|
|
def get_subtitle_language(self):
|
|
raise NotImplementedError()
|
|
|
|
def get_region(self):
|
|
raise NotImplementedError()
|
|
|
|
def get_playback_history(self):
|
|
uuid = self.get_uuid()
|
|
playback_history = self._playback_history
|
|
if not playback_history or playback_history.uuid != uuid:
|
|
filepath = (self.get_data_path(), uuid, 'history.sqlite')
|
|
playback_history = PlaybackHistory(filepath)
|
|
self._playback_history = playback_history
|
|
return playback_history
|
|
|
|
def get_feed_history(self):
|
|
uuid = self.get_uuid()
|
|
feed_history = self._feed_history
|
|
if not feed_history or feed_history.uuid != uuid:
|
|
filepath = (self.get_data_path(), uuid, 'feeds.sqlite')
|
|
feed_history = FeedHistory(filepath)
|
|
self._feed_history = feed_history
|
|
return feed_history
|
|
|
|
def get_data_cache(self):
|
|
uuid = self.get_uuid()
|
|
data_cache = self._data_cache
|
|
if not data_cache or data_cache.uuid != uuid:
|
|
filepath = (self.get_data_path(), uuid, 'data_cache.sqlite')
|
|
data_cache = DataCache(
|
|
filepath,
|
|
max_file_size_mb=self.get_settings().cache_size() / 2,
|
|
)
|
|
self._data_cache = data_cache
|
|
return data_cache
|
|
|
|
def get_function_cache(self):
|
|
uuid = self.get_uuid()
|
|
function_cache = self._function_cache
|
|
if not function_cache or function_cache.uuid != uuid:
|
|
filepath = (self.get_data_path(), uuid, 'cache.sqlite')
|
|
function_cache = FunctionCache(
|
|
filepath,
|
|
max_file_size_mb=self.get_settings().cache_size() / 2,
|
|
)
|
|
self._function_cache = function_cache
|
|
return function_cache
|
|
|
|
def get_requests_cache(self):
|
|
uuid = self.get_uuid()
|
|
requests_cache = self._requests_cache
|
|
if not requests_cache or requests_cache.uuid != uuid:
|
|
filepath = (self.get_data_path(), uuid, 'requests_cache.sqlite')
|
|
requests_cache = RequestCache(
|
|
filepath,
|
|
max_file_size_mb=self.get_settings().requests_cache_size(),
|
|
)
|
|
self._requests_cache = requests_cache
|
|
return requests_cache
|
|
|
|
def get_search_history(self):
|
|
uuid = self.get_uuid()
|
|
search_history = self._search_history
|
|
if not search_history or search_history.uuid != uuid:
|
|
filepath = (self.get_data_path(), uuid, 'search.sqlite')
|
|
search_history = SearchHistory(
|
|
filepath,
|
|
max_item_count=self.get_settings().get_search_history_size(),
|
|
)
|
|
self._search_history = search_history
|
|
return search_history
|
|
|
|
def get_bookmarks_list(self):
|
|
uuid = self.get_uuid()
|
|
bookmarks_list = self._bookmarks_list
|
|
if not bookmarks_list or bookmarks_list.uuid != uuid:
|
|
filepath = (self.get_data_path(), uuid, 'bookmarks.sqlite')
|
|
bookmarks_list = BookmarksList(filepath)
|
|
self._bookmarks_list = bookmarks_list
|
|
return bookmarks_list
|
|
|
|
def get_watch_later_list(self):
|
|
uuid = self.get_uuid()
|
|
watch_later_list = self._watch_later_list
|
|
if not watch_later_list or watch_later_list.uuid != uuid:
|
|
filepath = (self.get_data_path(), uuid, 'watch_later.sqlite')
|
|
watch_later_list = WatchLaterList(filepath)
|
|
self._watch_later_list = watch_later_list
|
|
return watch_later_list
|
|
|
|
def get_uuid(self):
|
|
uuid = self._uuid
|
|
if not uuid:
|
|
uuid = self.get_access_manager().get_current_user_id()
|
|
self._uuid = uuid
|
|
return uuid
|
|
|
|
def get_access_manager(self):
|
|
access_manager = self._access_manager
|
|
if access_manager:
|
|
return access_manager
|
|
return self.reload_access_manager()
|
|
|
|
def reload_access_manager(self):
|
|
raise NotImplementedError()
|
|
|
|
def get_api_store(self):
|
|
api_store = self._api_store
|
|
if api_store:
|
|
return api_store
|
|
return self.reload_api_store()
|
|
|
|
def reload_api_store(self):
|
|
raise NotImplementedError()
|
|
|
|
def get_playlist_player(self, playlist_type=None):
|
|
raise NotImplementedError()
|
|
|
|
def get_ui(self):
|
|
raise NotImplementedError()
|
|
|
|
@staticmethod
|
|
def get_system_version():
|
|
return current_system_version
|
|
|
|
def create_uri(self,
|
|
path=None,
|
|
params=None,
|
|
parse_params=False,
|
|
run=False,
|
|
play=None,
|
|
window=None,
|
|
command=False):
|
|
if isinstance(path, (list, tuple)):
|
|
uri = self.create_path(*path, is_uri=True)
|
|
elif path:
|
|
uri = path
|
|
else:
|
|
uri = '/'
|
|
|
|
if not uri.startswith('plugin://'):
|
|
uri = self._plugin_id.join(('plugin://', uri))
|
|
|
|
if params:
|
|
if isinstance(params, string_type):
|
|
if parse_params:
|
|
params = dict(parse_qsl(params, keep_blank_values=True))
|
|
else:
|
|
parse_params = True
|
|
if parse_params:
|
|
if isinstance(params, dict):
|
|
params = params.items()
|
|
params = urlencode([
|
|
(
|
|
('%' + param,
|
|
','.join([quote(item) for item in value]))
|
|
if len(value) > 1 else
|
|
(param, value[0])
|
|
)
|
|
if value and isinstance(value, (list, tuple)) else
|
|
(param, value)
|
|
for param, value in params
|
|
])
|
|
uri = '?'.join((uri, params))
|
|
|
|
command = 'command://' if command else ''
|
|
if run:
|
|
return ''.join((command,
|
|
'RunAddon('
|
|
if run == 'addon' else
|
|
'RunScript('
|
|
if run == 'script' else
|
|
'RunPlugin(',
|
|
uri,
|
|
')'))
|
|
if play is not None:
|
|
return ''.join((
|
|
command,
|
|
'PlayMedia(',
|
|
uri,
|
|
',playlist_type_hint=', str(play), ')',
|
|
))
|
|
if window:
|
|
if not isinstance(window, dict):
|
|
window = {}
|
|
if window.setdefault('refresh', False):
|
|
method = 'Container.Refresh('
|
|
if not window.setdefault('replace', False):
|
|
uri = ''
|
|
history_replace = False
|
|
window_return = False
|
|
elif window.setdefault('update', False):
|
|
method = 'Container.Update('
|
|
history_replace = window.setdefault('replace', False)
|
|
window_return = False
|
|
else:
|
|
history_replace = False
|
|
window_name = window.setdefault('name', 'Videos')
|
|
if window.setdefault('replace', False):
|
|
method = 'ReplaceWindow(%s,' % window_name
|
|
window_return = window.setdefault('return', False)
|
|
else:
|
|
method = 'ActivateWindow(%s,' % window_name
|
|
window_return = window.setdefault('return', True)
|
|
return ''.join((
|
|
command,
|
|
method,
|
|
uri,
|
|
',return' if window_return else '',
|
|
',replace' if history_replace else '',
|
|
')'
|
|
))
|
|
return uri
|
|
|
|
def get_parent_uri(self, **kwargs):
|
|
return self.create_uri(self._path_parts[:-1], **kwargs)
|
|
|
|
@staticmethod
|
|
def create_path(*args, **kwargs):
|
|
include_parts = kwargs.get('parts')
|
|
parser = kwargs.get('parser')
|
|
parts = [
|
|
parser(part[6:-1])
|
|
if parser and part.startswith('$INFO[') else
|
|
part
|
|
for part in [
|
|
to_str(arg).strip('/').replace('\\', '/').replace('//', '/')
|
|
for arg in args
|
|
]
|
|
if part
|
|
]
|
|
if parts:
|
|
path = '/'.join(parts).join(('/', '/'))
|
|
if path.startswith(PATHS.ROUTE):
|
|
parts = parts[2:]
|
|
elif path.startswith(PATHS.COMMAND):
|
|
parts = []
|
|
elif path.startswith(PATHS.GOTO_PAGE):
|
|
parts = parts[2:]
|
|
if parts:
|
|
try:
|
|
int(parts[0])
|
|
except (TypeError, ValueError):
|
|
pass
|
|
else:
|
|
parts = parts[1:]
|
|
else:
|
|
return ('/', parts) if include_parts else '/'
|
|
|
|
if kwargs.get('is_uri'):
|
|
path = quote(path)
|
|
return (path, parts) if include_parts else path
|
|
|
|
def get_path(self):
|
|
return self._path
|
|
|
|
def set_path(self, *path, **kwargs):
|
|
if kwargs.get('force'):
|
|
parts = kwargs.get('parts')
|
|
path = unquote(path[0])
|
|
if parts is None:
|
|
path = path.split('/')
|
|
path, parts = self.create_path(
|
|
*path,
|
|
parts=True,
|
|
parser=kwargs.get('parser')
|
|
)
|
|
else:
|
|
path, parts = self.create_path(*path, parts=True)
|
|
|
|
self._path = path
|
|
self._path_parts = parts
|
|
if kwargs.get('update_uri', True):
|
|
self.update_uri()
|
|
|
|
def get_original_params(self):
|
|
return self._param_string
|
|
|
|
def get_params(self):
|
|
return self._params
|
|
|
|
def get_param(self, name, default=None):
|
|
return self._params.get(name, default)
|
|
|
|
def pop_param(self, name, default=None):
|
|
return self._params.pop(name, default)
|
|
|
|
def parse_uri(self, uri, parse_params=True, update=False):
|
|
uri = urlsplit(uri)
|
|
path = uri.path
|
|
if parse_params:
|
|
params = self.parse_params(
|
|
dict(parse_qsl(uri.query, keep_blank_values=True)),
|
|
update=False,
|
|
)
|
|
if update:
|
|
self._params = params
|
|
self.set_path(path)
|
|
else:
|
|
params = uri.query
|
|
return path, params
|
|
|
|
def parse_params(self, params, update=True, parser=None):
|
|
to_delete = []
|
|
output = self._params if update else {}
|
|
|
|
for param, value in params.items():
|
|
if param.startswith('%'):
|
|
param = param[1:]
|
|
value = unquote(value)
|
|
try:
|
|
if param in self._BOOL_PARAMS:
|
|
parsed_value = BOOL_FROM_STR.get(
|
|
str(value),
|
|
bool(value)
|
|
if param in self._STRING_BOOL_PARAMS else
|
|
False
|
|
)
|
|
elif param in self._INT_PARAMS:
|
|
parsed_value = int(
|
|
(BOOL_FROM_STR.get(str(value), value) or 0)
|
|
if param in self._INT_BOOL_PARAMS else
|
|
value
|
|
)
|
|
elif param in self._FLOAT_PARAMS:
|
|
parsed_value = float(value)
|
|
elif param in self._LIST_PARAMS:
|
|
parsed_value = (
|
|
list(value)
|
|
if isinstance(value, (list, tuple)) else
|
|
[unquote(val) for val in value.split(',') if val]
|
|
)
|
|
elif param in self._STRING_PARAMS:
|
|
if parser and value.startswith('$INFO['):
|
|
parsed_value = parser(value[6:-1])
|
|
else:
|
|
parsed_value = value
|
|
if param in self._STRING_BOOL_PARAMS:
|
|
parsed_value = BOOL_FROM_STR.get(
|
|
parsed_value, parsed_value
|
|
)
|
|
elif param in self._STRING_INT_PARAMS:
|
|
try:
|
|
parsed_value = int(parsed_value)
|
|
except (TypeError, ValueError):
|
|
pass
|
|
# process and translate deprecated parameters
|
|
elif param == 'action':
|
|
if parsed_value in {'play_all', 'play_video'}:
|
|
to_delete.append(param)
|
|
self.set_path(PATHS.PLAY, update_uri=False)
|
|
continue
|
|
elif param == 'videoid':
|
|
to_delete.append(param)
|
|
param = VIDEO_ID
|
|
elif params == 'playlist':
|
|
to_delete.append(param)
|
|
param = PLAYLIST_ID
|
|
elif param in self._NON_EMPTY_STRING_PARAMS:
|
|
parsed_value = BOOL_FROM_STR.get(value, value)
|
|
if not parsed_value:
|
|
raise ValueError
|
|
else:
|
|
self.log.debug('Unknown parameter {param!r}: {value!r}',
|
|
param=param,
|
|
value=value)
|
|
to_delete.append(param)
|
|
continue
|
|
except (TypeError, ValueError):
|
|
self.log.exception('Invalid value for {param!r}: {value!r}',
|
|
param=param,
|
|
value=value)
|
|
to_delete.append(param)
|
|
continue
|
|
|
|
output[param] = parsed_value
|
|
|
|
for param in to_delete:
|
|
del params[param]
|
|
|
|
return output
|
|
|
|
def set_params(self, **kwargs):
|
|
self.parse_params(kwargs)
|
|
|
|
def get_data_path(self):
|
|
"""
|
|
Returns the path for read/write access of files
|
|
:return:
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def get_addon_path(self):
|
|
raise NotImplementedError()
|
|
|
|
def get_icon(self):
|
|
return self._plugin_icon
|
|
|
|
def get_fanart(self):
|
|
return self.create_resource_path('media/fanart.jpg')
|
|
|
|
def create_resource_path(self, *args):
|
|
path_comps = []
|
|
for arg in args:
|
|
path_comps.extend(arg.split('/'))
|
|
path = os.path.join(self.get_addon_path(), 'resources', *path_comps)
|
|
return path
|
|
|
|
def get_uri(self):
|
|
return self._uri
|
|
|
|
def update_uri(self):
|
|
self._uri = self.create_uri(self._path, self._params)
|
|
|
|
def get_name(self):
|
|
return self._plugin_name
|
|
|
|
def get_version(self):
|
|
return self._version
|
|
|
|
def get_id(self):
|
|
return self._plugin_id
|
|
|
|
def get_handle(self):
|
|
return self._plugin_handle
|
|
|
|
def get_settings(self, refresh=False):
|
|
raise NotImplementedError()
|
|
|
|
def localize(self, text_id, args=None, default_text=None):
|
|
raise NotImplementedError()
|
|
|
|
def apply_content(self,
|
|
content_type=None,
|
|
sub_type=None,
|
|
category_label=None):
|
|
raise NotImplementedError()
|
|
|
|
def add_sort_method(self, *sort_methods):
|
|
raise NotImplementedError()
|
|
|
|
def clone(self, new_path=None, new_params=None):
|
|
raise NotImplementedError()
|
|
|
|
def execute(self,
|
|
command,
|
|
wait=False,
|
|
wait_for=None,
|
|
wait_for_set=True,
|
|
block_ui=None):
|
|
raise NotImplementedError()
|
|
|
|
@staticmethod
|
|
def sleep(timeout=None):
|
|
raise NotImplementedError()
|
|
|
|
def tear_down(self):
|
|
pass
|
|
|
|
def ipc_exec(self, target, timeout=None, payload=None, raise_exc=False):
|
|
raise NotImplementedError()
|
|
|
|
@staticmethod
|
|
def is_plugin_folder(folder_name=None):
|
|
raise NotImplementedError()
|
|
|
|
def refresh_requested(self, force=False, on=False, off=False, params=None):
|
|
raise NotImplementedError
|
|
|
|
def parse_item_ids(self,
|
|
uri=None,
|
|
from_listitem=True):
|
|
raise NotImplementedError()
|