-
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
__all__ = ('youtube_plugin',)
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- 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
|
||||
|
||||
from youtube_plugin.kodion import plugin_runner
|
||||
|
||||
|
||||
plugin_runner.run()
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2024-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 sys
|
||||
|
||||
from youtube_plugin.kodion import script_actions
|
||||
|
||||
|
||||
script_actions.run(sys.argv)
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- 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
|
||||
|
||||
from youtube_plugin.kodion import service_runner
|
||||
|
||||
|
||||
service_runner.run()
|
||||
@@ -0,0 +1,149 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2018-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
|
||||
|
||||
from youtube_plugin.kodion import logging
|
||||
from youtube_plugin.kodion.constants import ADDON_ID
|
||||
from youtube_plugin.kodion.context import XbmcContext
|
||||
from youtube_plugin.youtube.helper import yt_login
|
||||
from youtube_plugin.youtube.provider import Provider
|
||||
from youtube_plugin.youtube.youtube_exceptions import LoginException
|
||||
|
||||
|
||||
__all__ = (
|
||||
'LoginException',
|
||||
'reset_access_tokens',
|
||||
'sign_in',
|
||||
'sign_out',
|
||||
)
|
||||
|
||||
|
||||
def _auth(addon_id, mode=yt_login.SIGN_IN):
|
||||
"""
|
||||
|
||||
:param addon_id: id of the add-on being signed in
|
||||
:param mode: SIGN_IN or SIGN_OUT
|
||||
:return: addon provider, context and client
|
||||
"""
|
||||
if not addon_id or addon_id == ADDON_ID:
|
||||
logging.error_trace('Invalid addon_id: %r', addon_id)
|
||||
return False
|
||||
|
||||
provider = Provider()
|
||||
context = XbmcContext(params={'addon_id': addon_id})
|
||||
|
||||
access_manager = context.get_access_manager()
|
||||
if access_manager.add_new_developer(addon_id):
|
||||
logging.debug('Creating developer user: %r', addon_id)
|
||||
|
||||
client = provider.get_client(context=context)
|
||||
|
||||
if mode == yt_login.SIGN_IN:
|
||||
if client.logged_in:
|
||||
yt_login.process(yt_login.SIGN_OUT,
|
||||
provider,
|
||||
context,
|
||||
client=client,
|
||||
refresh=False)
|
||||
client = None
|
||||
elif mode != yt_login.SIGN_OUT:
|
||||
raise Exception('Unknown mode: %r' % mode)
|
||||
|
||||
yt_login.process(mode, provider, context, client=client, refresh=False)
|
||||
|
||||
logged_in = provider.get_client(context=context).logged_in
|
||||
if mode == yt_login.SIGN_IN:
|
||||
return logged_in
|
||||
return not logged_in
|
||||
|
||||
|
||||
def sign_in(addon_id):
|
||||
"""
|
||||
To use the signed in context, see youtube_registration.py and youtube_requests.py
|
||||
Usage:
|
||||
|
||||
addon.xml
|
||||
---
|
||||
<import addon="plugin.video.youtube" version="6.1.0"/>
|
||||
---
|
||||
|
||||
.py
|
||||
---
|
||||
import youtube_registration
|
||||
import youtube_authentication
|
||||
|
||||
youtube_registration.register_api_keys(addon_id='plugin.video.example',
|
||||
api_key='A1zaSyA0b5sTjgxzTzYLmVtradlFVBfSHNOJKS0',
|
||||
client_id='825419953561-ert5tccq1r0upsuqdf5nm3le39czk23a.apps.googleusercontent.com',
|
||||
client_secret='Y5cE1IKzJQe1NZ0OsOoEqpu3')
|
||||
|
||||
try:
|
||||
signed_in = youtube_authentication.sign_in(addon_id='plugin.video.example') # refreshes access tokens if already signed in
|
||||
except youtube_authentication.LoginException as exc:
|
||||
error_message = exc.get_message()
|
||||
# handle error
|
||||
signed_in = False
|
||||
|
||||
if signed_in:
|
||||
pass # see youtube_registration.py and youtube_requests.py to use the signed in context
|
||||
---
|
||||
|
||||
:param addon_id: id of the add-on being signed in
|
||||
:return: boolean, True when signed in
|
||||
"""
|
||||
|
||||
return _auth(addon_id, mode=yt_login.SIGN_IN)
|
||||
|
||||
|
||||
def sign_out(addon_id):
|
||||
"""
|
||||
Usage:
|
||||
|
||||
addon.xml
|
||||
---
|
||||
<import addon="plugin.video.youtube" version="6.1.0"/>
|
||||
---
|
||||
|
||||
.py
|
||||
---
|
||||
import youtube_registration
|
||||
import youtube_authentication
|
||||
|
||||
youtube_registration.register_api_keys(addon_id='plugin.video.example',
|
||||
api_key='A1zaSyA0b5sTjgxzTzYLmVtradlFVBfSHNOJKS0',
|
||||
client_id='825419953561-ert5tccq1r0upsuqdf5nm3le39czk23a.apps.googleusercontent.com',
|
||||
client_secret='Y5cE1IKzJQe1NZ0OsOoEqpu3')
|
||||
|
||||
signed_out = youtube_authentication.sign_out(addon_id='plugin.video.example')
|
||||
if signed_out:
|
||||
pass
|
||||
---
|
||||
|
||||
:param addon_id: id of the add-on being signed out
|
||||
:return: boolean, True when signed out
|
||||
"""
|
||||
|
||||
return _auth(addon_id, mode=yt_login.SIGN_OUT)
|
||||
|
||||
|
||||
def reset_access_tokens(addon_id):
|
||||
"""
|
||||
|
||||
:param addon_id: id of the add-on having it's access tokens reset
|
||||
:return:
|
||||
"""
|
||||
if not addon_id or addon_id == ADDON_ID:
|
||||
logging.error_trace('Invalid addon_id: %r', addon_id)
|
||||
return
|
||||
|
||||
context = XbmcContext(params={'addon_id': addon_id})
|
||||
context.get_access_manager().update_access_token(
|
||||
addon_id, access_token='', expiry=-1, refresh_token=''
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
key_sets = {
|
||||
'youtube-tv': {
|
||||
'api_key': 'QUl6YVN5QzZmdlpTSkhBN1Z6NWo4akNpS1J0N3RVSU9xakUyTjNn',
|
||||
'client_id': 'ODYxNTU2NzA4NDU0LWQ2ZGxtM2xoMDVpZGQ4bnBlazE4azZiZThiYTNvYzY4',
|
||||
'client_secret': 'U2JvVmhvRzlzMHJOYWZpeENTR0dLWEFU',
|
||||
},
|
||||
'youtube-vr': {
|
||||
'api_key': '',
|
||||
'client_id': 'NjUyNDY5MzEyMTY5LTRsdnM5Ym5ocjlscG5zOXY0NTFqNW9pdmQ4MXZqdnUx',
|
||||
'client_secret': 'M2ZUV3JCSkk1VW9qbTFUSzdfaUpDVzVa',
|
||||
},
|
||||
'provided': {
|
||||
'0': {
|
||||
'api_key': '',
|
||||
'client_id': '',
|
||||
'client_secret': '',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__all__ = ('kodion', 'youtube', 'key_sets',)
|
||||
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
# -*- 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
|
||||
|
||||
from .abstract_provider import (
|
||||
# Abstract provider for implementation by the user
|
||||
AbstractProvider,
|
||||
)
|
||||
# import base exception of kodion directly into the kodion namespace
|
||||
from .exceptions import KodionException
|
||||
|
||||
|
||||
__all__ = (
|
||||
'AbstractProvider',
|
||||
'KodionException',
|
||||
)
|
||||
|
||||
__version__ = '1.5.4'
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,538 @@
|
||||
# -*- 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
|
||||
|
||||
from re import (
|
||||
UNICODE,
|
||||
compile as re_compile,
|
||||
)
|
||||
|
||||
from . import logging
|
||||
from .constants import (
|
||||
CHECK_SETTINGS,
|
||||
CONTENT,
|
||||
FOLDER_URI,
|
||||
ITEMS_PER_PAGE,
|
||||
PATHS,
|
||||
REROUTE_PATH,
|
||||
WINDOW_CACHE,
|
||||
WINDOW_FALLBACK,
|
||||
WINDOW_REPLACE,
|
||||
WINDOW_RETURN,
|
||||
)
|
||||
from .debug import ExecTimeout
|
||||
from .exceptions import KodionException
|
||||
from .items import (
|
||||
DirectoryItem,
|
||||
NewSearchItem,
|
||||
NextPageItem,
|
||||
SearchHistoryItem,
|
||||
UriItem,
|
||||
)
|
||||
from .utils.convert_format import to_unicode
|
||||
|
||||
|
||||
class AbstractProvider(object):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
CACHE_TO_DISC = 'provider_cache_to_disc' # type: bool
|
||||
FALLBACK = 'provider_fallback' # type: bool | str
|
||||
FORCE_PLAY = 'provider_force_play' # type: bool
|
||||
FORCE_REFRESH = 'provider_force_refresh' # type: bool
|
||||
FORCE_RESOLVE = 'provider_force_resolve' # type: bool
|
||||
FORCE_RETURN = 'provider_force_return' # type: bool
|
||||
POST_RUN = 'provider_post_run' # type: bool
|
||||
UPDATE_LISTING = 'provider_update_listing' # type: bool
|
||||
CONTENT_TYPE = 'provider_content_type' # type: tuple[str, str, str]
|
||||
|
||||
# map for regular expression (path) to method (names)
|
||||
_dict_path = {}
|
||||
|
||||
def __init__(self):
|
||||
# register some default paths
|
||||
self.register_path(r''.join((
|
||||
'^',
|
||||
'(?:', PATHS.HOME, ')?/?$'
|
||||
)), self.on_root)
|
||||
|
||||
self.register_path(r''.join((
|
||||
'^',
|
||||
PATHS.ROUTE,
|
||||
'(?P<path>/[^?]+?)(?:/*[?].+|/*)$'
|
||||
)), self.on_reroute)
|
||||
|
||||
self.register_path(r''.join((
|
||||
'^',
|
||||
PATHS.GOTO_PAGE,
|
||||
'(?P<page>/[0-9]+)?'
|
||||
'(?P<path>/[^?]+?)(?:/*[?].+|/*)$'
|
||||
)), self.on_goto_page)
|
||||
|
||||
self.register_path(r''.join((
|
||||
'^',
|
||||
PATHS.COMMAND,
|
||||
'/(?P<command>[^?]+?)(?:/*[?].+|/*)$'
|
||||
)), self.on_command)
|
||||
|
||||
self.register_path(r''.join((
|
||||
'^',
|
||||
PATHS.WATCH_LATER,
|
||||
'/(?P<command>add|clear|list|play|remove)?/?$'
|
||||
)), self.on_watch_later)
|
||||
|
||||
self.register_path(r''.join((
|
||||
'^',
|
||||
PATHS.BOOKMARKS,
|
||||
'/(?P<command>add|add_custom|clear|edit|list|play|remove)?/?$'
|
||||
)), self.on_bookmarks)
|
||||
|
||||
self.register_path(r''.join((
|
||||
'^',
|
||||
'(', PATHS.SEARCH, '|', PATHS.EXTERNAL_SEARCH, ')',
|
||||
'/(?P<command>input|input_prompt|query|list|links|remove|clear|rename)?/?$'
|
||||
)), self.on_search)
|
||||
|
||||
self.register_path(r''.join((
|
||||
'^',
|
||||
PATHS.HISTORY,
|
||||
'/(?P<command>clear|list|mark_as|mark_unwatched|mark_watched|play|remove|reset_resume)?/?$'
|
||||
)), self.on_playback_history)
|
||||
|
||||
self.register_path(r'(?P<path>.*\/)extrafanart\/([\?#].+)?$',
|
||||
self.on_extra_fanart)
|
||||
|
||||
@classmethod
|
||||
def register_path(cls, re_path, command=None):
|
||||
"""
|
||||
Registers a new method for the given regular expression
|
||||
:param re_path: regular expression of the path
|
||||
:param command: command or function to be registered
|
||||
:return:
|
||||
"""
|
||||
|
||||
def wrapper(command):
|
||||
if callable(command):
|
||||
func = command
|
||||
else:
|
||||
func = getattr(command, '__func__', None)
|
||||
if not callable(func):
|
||||
return None
|
||||
|
||||
cls._dict_path[re_compile(re_path, UNICODE)] = func
|
||||
return command
|
||||
|
||||
if command:
|
||||
return wrapper(command)
|
||||
return wrapper
|
||||
|
||||
def run_wizard(self, context, last_run=None):
|
||||
localize = context.localize
|
||||
# ui local variable used for ui.get_view_manager() in unofficial version
|
||||
ui = context.get_ui()
|
||||
|
||||
settings_state = {'state': 'defer'}
|
||||
context.ipc_exec(CHECK_SETTINGS, timeout=5, payload=settings_state)
|
||||
|
||||
if last_run and last_run > 1:
|
||||
self.pre_run_wizard_step(provider=self, context=context)
|
||||
wizard_steps = self.get_wizard_steps()
|
||||
wizard_steps.extend(ui.get_view_manager().get_wizard_steps())
|
||||
|
||||
step = 0
|
||||
steps = len(wizard_steps)
|
||||
|
||||
try:
|
||||
if wizard_steps and ui.on_yes_no_input(
|
||||
' - '.join((localize('youtube'), localize('setup_wizard'))),
|
||||
localize(('setup_wizard.prompt.x',
|
||||
'setup_wizard.prompt.settings')),
|
||||
):
|
||||
for wizard_step in wizard_steps:
|
||||
if callable(wizard_step):
|
||||
step = wizard_step(provider=self,
|
||||
context=context,
|
||||
step=step,
|
||||
steps=steps)
|
||||
else:
|
||||
step += 1
|
||||
finally:
|
||||
settings = context.get_settings(refresh=True)
|
||||
settings.setup_wizard_enabled(False)
|
||||
settings_state['state'] = 'process'
|
||||
context.ipc_exec(CHECK_SETTINGS, timeout=5, payload=settings_state)
|
||||
|
||||
@staticmethod
|
||||
def get_wizard_steps():
|
||||
# can be overridden by the derived class
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def pre_run_wizard_step(provider, context):
|
||||
# can be overridden by the derived class
|
||||
pass
|
||||
|
||||
def navigate(self, context):
|
||||
path = context.get_path()
|
||||
for re_path, handler in self._dict_path.items():
|
||||
re_match = re_path.search(path)
|
||||
if not re_match:
|
||||
continue
|
||||
|
||||
exec_limit = context.get_settings().exec_limit()
|
||||
if exec_limit:
|
||||
handler = ExecTimeout(
|
||||
seconds=exec_limit,
|
||||
# log_only=True,
|
||||
# trace_opcodes=True,
|
||||
# trace_threads=True,
|
||||
log_locals=(-15, None),
|
||||
callback=None,
|
||||
)(handler)
|
||||
|
||||
options = {
|
||||
self.CACHE_TO_DISC: True,
|
||||
self.UPDATE_LISTING: False,
|
||||
}
|
||||
result = handler(provider=self, context=context, re_match=re_match)
|
||||
if isinstance(result, tuple):
|
||||
result, new_options = result
|
||||
if new_options:
|
||||
options.update(new_options)
|
||||
|
||||
if context.refresh_requested():
|
||||
options[self.CACHE_TO_DISC] = True
|
||||
options[self.UPDATE_LISTING] = True
|
||||
|
||||
return result, options
|
||||
|
||||
raise KodionException('Mapping for path "%s" not found' % path)
|
||||
|
||||
def on_extra_fanart_run(self, context, re_match):
|
||||
"""
|
||||
The implementation of the provider can override this behavior.
|
||||
:param context:
|
||||
:param re_match:
|
||||
:return:
|
||||
"""
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def on_extra_fanart(provider, context, re_match):
|
||||
path = re_match.group('path')
|
||||
new_context = context.clone(new_path=path)
|
||||
return provider.on_extra_fanart_run(new_context, re_match)
|
||||
|
||||
@staticmethod
|
||||
def on_playback_history(provider, context, re_match):
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def on_root(provider, context, re_match):
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def on_goto_page(provider, context, re_match):
|
||||
ui = context.get_ui()
|
||||
|
||||
page = re_match.group('page')
|
||||
if page:
|
||||
page = int(page.lstrip('/'))
|
||||
else:
|
||||
result, page = ui.on_numeric_input(
|
||||
title=context.localize('page.choose'),
|
||||
default=1,
|
||||
)
|
||||
if not result:
|
||||
return False
|
||||
|
||||
path = re_match.group('path')
|
||||
params = context.get_params()
|
||||
if 'page_token' in params:
|
||||
page_token = NextPageItem.create_page_token(
|
||||
page, params.get(ITEMS_PER_PAGE, 50)
|
||||
)
|
||||
else:
|
||||
page_token = ''
|
||||
for param in NextPageItem.JUMP_PAGE_PARAM_EXCLUSIONS:
|
||||
if param in params:
|
||||
del params[param]
|
||||
params = dict(params, page=page, page_token=page_token)
|
||||
|
||||
if (not ui.busy_dialog_active()
|
||||
and ui.get_container_info(FOLDER_URI)):
|
||||
return provider.reroute(context=context, path=path, params=params)
|
||||
return provider.navigate(context.clone(path, params))
|
||||
|
||||
@staticmethod
|
||||
def on_reroute(provider, context, re_match):
|
||||
return provider.reroute(
|
||||
context=context,
|
||||
path=re_match.group('path'),
|
||||
params=context.get_params(),
|
||||
)
|
||||
|
||||
def reroute(self, context, path=None, params=None, uri=None):
|
||||
ui = context.get_ui()
|
||||
current_path, current_params = context.parse_uri(
|
||||
ui.get_container_info(FOLDER_URI, container_id=None)
|
||||
)
|
||||
|
||||
if uri is None:
|
||||
if path is None:
|
||||
path = current_path
|
||||
if params is None:
|
||||
params = current_params
|
||||
else:
|
||||
uri = context.parse_uri(uri)
|
||||
if params:
|
||||
uri[1].update(params)
|
||||
path, params = uri
|
||||
|
||||
if not path:
|
||||
self.log.error_trace('No route path')
|
||||
return False
|
||||
elif path.startswith(PATHS.ROUTE):
|
||||
path = path[len(PATHS.ROUTE):]
|
||||
|
||||
window_cache = params.pop(WINDOW_CACHE, True)
|
||||
window_fallback = params.pop(WINDOW_FALLBACK, False)
|
||||
window_replace = params.pop(WINDOW_REPLACE, False)
|
||||
window_return = params.pop(WINDOW_RETURN, True)
|
||||
|
||||
if window_fallback:
|
||||
if ui.get_container_info(FOLDER_URI):
|
||||
self.log.debug('Rerouting - Fallback route not required')
|
||||
return False, {self.FALLBACK: False}
|
||||
|
||||
refresh = context.refresh_requested(params=params)
|
||||
if (refresh or (
|
||||
params == current_params
|
||||
and path.rstrip('/') == current_path.rstrip('/')
|
||||
)):
|
||||
if refresh and refresh < 0:
|
||||
del params['refresh']
|
||||
else:
|
||||
params['refresh'] = context.refresh_requested(
|
||||
force=True,
|
||||
on=True,
|
||||
params=params,
|
||||
)
|
||||
else:
|
||||
params['refresh'] = 0
|
||||
|
||||
result = None
|
||||
uri = context.create_uri(path, params)
|
||||
if window_cache:
|
||||
function_cache = context.get_function_cache()
|
||||
with ui.on_busy():
|
||||
result, options = function_cache.run(
|
||||
self.navigate,
|
||||
_refresh=True,
|
||||
_scope=function_cache.SCOPE_NONE,
|
||||
context=context.clone(path, params),
|
||||
)
|
||||
if not result:
|
||||
self.log.debug(('No results', 'URI: %s'), uri)
|
||||
return False
|
||||
|
||||
self.log.debug(('Success',
|
||||
'URI: {uri}',
|
||||
'Cache: {window_cache!r}',
|
||||
'Fallback: {window_fallback!r}',
|
||||
'Replace: {window_replace!r}',
|
||||
'Return: {window_return!r}'),
|
||||
uri=uri,
|
||||
window_cache=window_cache,
|
||||
window_fallback=window_fallback,
|
||||
window_replace=window_replace,
|
||||
window_return=window_return)
|
||||
|
||||
reroute_path = ui.get_property(REROUTE_PATH)
|
||||
if reroute_path:
|
||||
return True
|
||||
|
||||
if window_cache:
|
||||
ui.set_property(REROUTE_PATH, path)
|
||||
|
||||
action = ''.join((
|
||||
'ReplaceWindow' if window_replace else 'ActivateWindow',
|
||||
'(Videos,',
|
||||
uri,
|
||||
',return)' if window_return else ')',
|
||||
))
|
||||
|
||||
timeout = 30
|
||||
while ui.busy_dialog_active():
|
||||
timeout -= 1
|
||||
if timeout < 0:
|
||||
self.log.warning('Multiple busy dialogs active'
|
||||
' - Rerouting workaround')
|
||||
return UriItem('command://{0}'.format(action))
|
||||
context.sleep(1)
|
||||
else:
|
||||
context.execute(
|
||||
action,
|
||||
# wait=True,
|
||||
# wait_for=(REROUTE_PATH if window_cache else None),
|
||||
# wait_for_set=False,
|
||||
# block_ui=True,
|
||||
)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def on_bookmarks(provider, context, re_match):
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def on_watch_later(provider, context, re_match):
|
||||
raise NotImplementedError()
|
||||
|
||||
def on_search_run(self, context, query):
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def on_search(provider, context, re_match):
|
||||
params = context.get_params()
|
||||
localize = context.localize
|
||||
ui = context.get_ui()
|
||||
|
||||
command = re_match.group('command')
|
||||
search_history = context.get_search_history()
|
||||
|
||||
if not command or command == 'query':
|
||||
query = to_unicode(params.get('q', ''))
|
||||
if query:
|
||||
result, options = provider.on_search_run(context, query=query)
|
||||
if not options:
|
||||
options = {provider.CACHE_TO_DISC: False}
|
||||
if result:
|
||||
fallback = options.setdefault(
|
||||
provider.FALLBACK, context.get_uri()
|
||||
)
|
||||
ui.set_property(provider.FALLBACK, fallback)
|
||||
return result, options
|
||||
command = 'list'
|
||||
context.set_path(PATHS.SEARCH, command)
|
||||
|
||||
if command == 'remove':
|
||||
query = to_unicode(params.get('q', ''))
|
||||
if not ui.on_yes_no_input(
|
||||
localize('content.remove'),
|
||||
localize('content.remove.check.x', query),
|
||||
):
|
||||
return False, None
|
||||
|
||||
search_history.del_item(query)
|
||||
ui.show_notification(localize('removed.name.x', query),
|
||||
time_ms=2500,
|
||||
audible=False)
|
||||
return True, {provider.FORCE_REFRESH: True}
|
||||
|
||||
if command == 'rename':
|
||||
query = to_unicode(params.get('q', ''))
|
||||
result, new_query = ui.on_keyboard_input(
|
||||
localize('search.rename'), query
|
||||
)
|
||||
if not result:
|
||||
return False, None
|
||||
|
||||
search_history.del_item(query)
|
||||
search_history.add_item(new_query)
|
||||
return True, {provider.FORCE_REFRESH: True}
|
||||
|
||||
if command == 'clear':
|
||||
if not ui.on_yes_no_input(
|
||||
localize('search.clear'),
|
||||
localize(('content.clear.check.x', 'search.history'))
|
||||
):
|
||||
return False, None
|
||||
|
||||
search_history.clear()
|
||||
ui.show_notification(localize('completed'),
|
||||
time_ms=2500,
|
||||
audible=False)
|
||||
return True, {provider.FORCE_REFRESH: True}
|
||||
|
||||
if command == 'links':
|
||||
return provider.on_specials_x(
|
||||
provider,
|
||||
context,
|
||||
category='description_links',
|
||||
)
|
||||
|
||||
if command.startswith('input'):
|
||||
result, query = ui.on_keyboard_input(
|
||||
localize('search.title')
|
||||
)
|
||||
if result and query:
|
||||
result = []
|
||||
options = {
|
||||
provider.FALLBACK: context.create_uri(
|
||||
(PATHS.SEARCH, 'query'),
|
||||
dict(params, q=query, category_label=query),
|
||||
window={
|
||||
'replace': False,
|
||||
'return': True,
|
||||
},
|
||||
),
|
||||
provider.FORCE_RETURN: True,
|
||||
provider.POST_RUN: True,
|
||||
provider.CACHE_TO_DISC: True,
|
||||
provider.UPDATE_LISTING: False,
|
||||
}
|
||||
else:
|
||||
result = False
|
||||
options = {
|
||||
provider.FALLBACK: True,
|
||||
}
|
||||
return result, options
|
||||
|
||||
location = context.get_param('location', False)
|
||||
|
||||
result = []
|
||||
options = {
|
||||
provider.CACHE_TO_DISC: False,
|
||||
provider.CONTENT_TYPE: {
|
||||
'content_type': CONTENT.LIST_CONTENT,
|
||||
'sub_type': None,
|
||||
'category_label': localize('search'),
|
||||
},
|
||||
}
|
||||
|
||||
# 'New Search...'
|
||||
new_search_item = NewSearchItem(
|
||||
context, location=location
|
||||
)
|
||||
result.append(new_search_item)
|
||||
|
||||
for search in search_history.get_items():
|
||||
# little fallback for old history entries
|
||||
if isinstance(search, DirectoryItem):
|
||||
search = search.get_name()
|
||||
|
||||
# we create a new instance of the SearchItem
|
||||
search_history_item = SearchHistoryItem(
|
||||
context, search, location=location
|
||||
)
|
||||
result.append(search_history_item)
|
||||
|
||||
return result, options
|
||||
|
||||
@staticmethod
|
||||
def on_command(re_match, **_kwargs):
|
||||
command = re_match.group('command')
|
||||
return UriItem(''.join(('command://', command)))
|
||||
|
||||
def handle_exception(self, context, exception_to_handle):
|
||||
return True
|
||||
|
||||
def tear_down(self):
|
||||
pass
|
||||
@@ -0,0 +1,347 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2023-2025 plugin.video.youtube
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only
|
||||
See LICENSES/GPL-2.0-only for more information.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'BaseHTTPRequestHandler',
|
||||
'StringIO',
|
||||
'TCPServer',
|
||||
'ThreadingMixIn',
|
||||
'available_cpu_count',
|
||||
'byte_string_type',
|
||||
'datetime_infolabel',
|
||||
'default_quote',
|
||||
'default_quote_plus',
|
||||
'entity_escape',
|
||||
'generate_hash',
|
||||
'parse_qs',
|
||||
'parse_qsl',
|
||||
'pickle',
|
||||
'quote',
|
||||
'quote_plus',
|
||||
'range_type',
|
||||
'string_type',
|
||||
'to_str',
|
||||
'unescape',
|
||||
'unquote',
|
||||
'unquote_plus',
|
||||
'urlencode',
|
||||
'urljoin',
|
||||
'urlsplit',
|
||||
'urlunsplit',
|
||||
'xbmc',
|
||||
'xbmcaddon',
|
||||
'xbmcgui',
|
||||
'xbmcplugin',
|
||||
'xbmcvfs',
|
||||
)
|
||||
|
||||
# Kodi v19+ and Python v3.x
|
||||
try:
|
||||
import _pickle as pickle
|
||||
from hashlib import md5
|
||||
from html import unescape
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from io import StringIO
|
||||
from socketserver import TCPServer, ThreadingMixIn
|
||||
from urllib.parse import (
|
||||
parse_qs,
|
||||
parse_qsl,
|
||||
quote,
|
||||
quote_plus,
|
||||
unquote,
|
||||
unquote_plus,
|
||||
urlencode,
|
||||
urljoin,
|
||||
urlsplit,
|
||||
urlunsplit,
|
||||
)
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
import xbmcvfs
|
||||
|
||||
|
||||
xbmc.LOGNOTICE = xbmc.LOGINFO
|
||||
xbmc.LOGSEVERE = xbmc.LOGFATAL
|
||||
|
||||
range_type = (range, list)
|
||||
|
||||
byte_string_type = bytes
|
||||
string_type = str
|
||||
to_str = str
|
||||
|
||||
|
||||
def entity_escape(text,
|
||||
entities=str.maketrans({
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'\'': ''',
|
||||
})):
|
||||
return text.translate(entities)
|
||||
|
||||
|
||||
def generate_hash(*args, **kwargs):
|
||||
return md5(''.join(
|
||||
map(str, args or kwargs.get('iter'))
|
||||
).encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
SAFE_CHARS = frozenset(
|
||||
b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
b'abcdefghijklmnopqrstuvwxyz'
|
||||
b'0123456789'
|
||||
b'_.-~'
|
||||
b'/' # safe character by default
|
||||
)
|
||||
reserved = {
|
||||
chr(ordinal): '%%%x' % ordinal
|
||||
for ordinal in range(0, 128)
|
||||
if ordinal not in SAFE_CHARS
|
||||
}
|
||||
reserved_plus = reserved.copy()
|
||||
reserved_plus.update((
|
||||
('/', '%2f'),
|
||||
(' ', '+'),
|
||||
))
|
||||
reserved = str.maketrans(reserved)
|
||||
reserved_plus = str.maketrans(reserved_plus)
|
||||
non_ascii = str.maketrans({
|
||||
chr(ordinal): '%%%x' % ordinal
|
||||
for ordinal in range(128, 256)
|
||||
})
|
||||
|
||||
|
||||
def default_quote(string,
|
||||
safe='',
|
||||
encoding=None,
|
||||
errors=None,
|
||||
_encoding='utf-8',
|
||||
_errors='strict',
|
||||
_reserved=reserved,
|
||||
_non_ascii=non_ascii,
|
||||
_encode=str.encode,
|
||||
_is_ascii=str.isascii,
|
||||
_replace=str.replace,
|
||||
_old='\\x',
|
||||
_new='%',
|
||||
_slice=slice(2, -1),
|
||||
_str=str,
|
||||
_translate=str.translate):
|
||||
_string = _translate(string, _reserved)
|
||||
if _is_ascii(_string):
|
||||
return _string
|
||||
_string = _str(_encode(_string, _encoding, _errors))[_slice]
|
||||
if _string == string:
|
||||
if _is_ascii(_string):
|
||||
return _string
|
||||
return _translate(_string, _non_ascii)
|
||||
if _is_ascii(_string):
|
||||
return _replace(_string, _old, _new)
|
||||
return _translate(_replace(_string, _old, _new), _non_ascii)
|
||||
|
||||
|
||||
def default_quote_plus(string,
|
||||
safe='',
|
||||
encoding=None,
|
||||
errors=None,
|
||||
_encoding='utf-8',
|
||||
_errors='strict',
|
||||
_reserved=reserved_plus,
|
||||
_non_ascii=non_ascii,
|
||||
_encode=str.encode,
|
||||
_is_ascii=str.isascii,
|
||||
_replace=str.replace,
|
||||
_old='\\x',
|
||||
_new='%',
|
||||
_slice=slice(2, -1),
|
||||
_str=str,
|
||||
_translate=str.translate):
|
||||
if (not safe and encoding is None and errors is None
|
||||
and isinstance(string, str)):
|
||||
_string = _translate(string, _reserved)
|
||||
if _is_ascii(_string):
|
||||
return _string
|
||||
_string = _str(_encode(_string, _encoding, _errors))[_slice]
|
||||
if _string == string:
|
||||
if _is_ascii(_string):
|
||||
return _string
|
||||
return _translate(_string, _non_ascii)
|
||||
if _is_ascii(_string):
|
||||
return _replace(_string, _old, _new)
|
||||
return _translate(_replace(_string, _old, _new), _non_ascii)
|
||||
return quote_plus(string, safe, encoding, errors)
|
||||
|
||||
|
||||
urlencode.__defaults__ = (False, '', None, None, default_quote_plus)
|
||||
|
||||
# Compatibility shims for Kodi v18 and Python v2.7
|
||||
except ImportError:
|
||||
import cPickle as pickle
|
||||
from hashlib import md5
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
||||
from SocketServer import TCPServer, ThreadingMixIn
|
||||
from StringIO import StringIO as _StringIO
|
||||
from urllib import (
|
||||
quote as _quote,
|
||||
quote_plus as _quote_plus,
|
||||
unquote as _unquote,
|
||||
unquote_plus as _unquote_plus,
|
||||
urlencode as _urlencode,
|
||||
)
|
||||
from urlparse import (
|
||||
parse_qs,
|
||||
parse_qsl,
|
||||
urljoin,
|
||||
urlsplit,
|
||||
urlunsplit,
|
||||
)
|
||||
from xml.sax.saxutils import unescape
|
||||
|
||||
from kodi_six import (
|
||||
xbmc,
|
||||
xbmcaddon,
|
||||
xbmcgui,
|
||||
xbmcplugin,
|
||||
xbmcvfs,
|
||||
)
|
||||
|
||||
|
||||
def quote(data, *args, **kwargs):
|
||||
return _quote(to_str(data), *args, **kwargs)
|
||||
|
||||
|
||||
default_quote = quote
|
||||
|
||||
|
||||
def quote_plus(data, *args, **kwargs):
|
||||
return _quote_plus(to_str(data), *args, **kwargs)
|
||||
|
||||
|
||||
default_quote_plus = quote_plus
|
||||
|
||||
|
||||
def unquote(data):
|
||||
return _unquote(to_str(data))
|
||||
|
||||
|
||||
def unquote_plus(data):
|
||||
return _unquote_plus(to_str(data))
|
||||
|
||||
|
||||
def urlencode(data, *args, **kwargs):
|
||||
if isinstance(data, dict):
|
||||
data = data.items()
|
||||
kwargs = {
|
||||
key: value
|
||||
for key, value in kwargs.viewitems()
|
||||
if key in {'query', 'doseq'}
|
||||
}
|
||||
return _urlencode({
|
||||
to_str(key): (
|
||||
[to_str(part) for part in value]
|
||||
if isinstance(value, (list, tuple)) else
|
||||
to_str(value)
|
||||
)
|
||||
for key, value in data
|
||||
}, *args, **kwargs)
|
||||
|
||||
|
||||
class StringIO(_StringIO):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
|
||||
class File(xbmcvfs.File):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
|
||||
xbmcvfs.File = File
|
||||
xbmcvfs.translatePath = xbmc.translatePath
|
||||
|
||||
range_type = (xrange, list)
|
||||
|
||||
byte_string_type = (bytes, str)
|
||||
string_type = basestring
|
||||
|
||||
|
||||
def to_str(value, _format='{0!s}'.format):
|
||||
if not isinstance(value, basestring):
|
||||
value = _format(value)
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode('utf-8')
|
||||
return value
|
||||
|
||||
|
||||
def entity_escape(text,
|
||||
entities={
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'\'': ''',
|
||||
}):
|
||||
for key, value in entities.viewitems():
|
||||
text = text.replace(key, value)
|
||||
return text
|
||||
|
||||
|
||||
def generate_hash(*args, **kwargs):
|
||||
return md5(''.join(
|
||||
map(to_str, args or kwargs.get('iter'))
|
||||
)).hexdigest()
|
||||
|
||||
|
||||
def _loads(string, _loads=pickle.loads):
|
||||
return _loads(to_str(string))
|
||||
|
||||
|
||||
pickle.loads = _loads
|
||||
|
||||
# Kodi v20+
|
||||
if hasattr(xbmcgui.ListItem, 'setDateTime'):
|
||||
def datetime_infolabel(datetime_obj, *_args, **_kwargs):
|
||||
return datetime_obj.replace(microsecond=0, tzinfo=None).isoformat()
|
||||
# Compatibility shims for Kodi v18 and v19
|
||||
else:
|
||||
def datetime_infolabel(datetime_obj, str_format='%Y-%m-%d %H:%M:%S'):
|
||||
return datetime_obj.strftime(str_format)
|
||||
|
||||
try:
|
||||
from os import sched_getaffinity as _sched_get_affinity
|
||||
except ImportError:
|
||||
_sched_get_affinity = None
|
||||
try:
|
||||
from multiprocessing import cpu_count as _cpu_count
|
||||
except ImportError:
|
||||
_cpu_count = None
|
||||
|
||||
|
||||
def available_cpu_count():
|
||||
if _sched_get_affinity:
|
||||
# Equivalent to os.process_cpu_count()
|
||||
return len(_sched_get_affinity(0)) or 1
|
||||
|
||||
if _cpu_count:
|
||||
try:
|
||||
return _cpu_count() or 1
|
||||
except NotImplementedError:
|
||||
return 1
|
||||
|
||||
return 1
|
||||
Binary file not shown.
@@ -0,0 +1,368 @@
|
||||
# -*- 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
|
||||
|
||||
from . import (
|
||||
const_content_types as CONTENT,
|
||||
const_paths as PATHS,
|
||||
const_settings as SETTINGS,
|
||||
const_sort_methods as SORT,
|
||||
)
|
||||
from .const_lang_region import (
|
||||
DEFAULT_LANGUAGES,
|
||||
DEFAULT_REGIONS,
|
||||
TRANSLATION_LANGUAGES,
|
||||
)
|
||||
|
||||
|
||||
# Addon paths
|
||||
ADDON_ID = 'plugin.video.youtube'
|
||||
ADDON_PATH = 'special://home/addons/' + ADDON_ID
|
||||
DATA_PATH = 'special://profile/addon_data/' + ADDON_ID
|
||||
MEDIA_PATH = ADDON_PATH + '/resources/media'
|
||||
RESOURCE_PATH = ADDON_PATH + '/resources'
|
||||
TEMP_PATH = 'special://temp/' + ADDON_ID
|
||||
|
||||
# Const values
|
||||
BOOL_FROM_STR = {
|
||||
'0': False,
|
||||
'1': True,
|
||||
'false': False,
|
||||
'False': False,
|
||||
'true': True,
|
||||
'True': True,
|
||||
'None': None,
|
||||
'null': None,
|
||||
'': None,
|
||||
}
|
||||
|
||||
VALUE_TO_STR = {
|
||||
False: 'false',
|
||||
True: 'true',
|
||||
None: '',
|
||||
-1: '',
|
||||
0: 'false',
|
||||
1: 'true',
|
||||
}
|
||||
|
||||
YOUTUBE_HOSTNAMES = frozenset((
|
||||
'youtube.com',
|
||||
'www.youtube.com',
|
||||
'm.youtube.com',
|
||||
'www.youtubekids.com',
|
||||
'music.youtube.com',
|
||||
))
|
||||
|
||||
# Flags
|
||||
ABORT_FLAG = 'abort_requested'
|
||||
BUSY_FLAG = 'busy'
|
||||
SERVICE_RUNNING_FLAG = 'service_monitor_running'
|
||||
WAIT_END_FLAG = 'builtin_completed'
|
||||
TRAKT_PAUSE_FLAG = 'script.trakt.paused'
|
||||
|
||||
# Container Info
|
||||
CURRENT_CONTAINER_INFO = 'Container.%s'
|
||||
PLUGIN_CONTAINER_INFO = 'Container(%s).%s'
|
||||
CURRENT_ITEM = 'CurrentItem'
|
||||
FOLDER_NAME = 'FolderName'
|
||||
FOLDER_URI = 'FolderPath'
|
||||
HAS_FILES = 'HasFiles'
|
||||
HAS_FOLDERS = 'HasFolders'
|
||||
HAS_PARENT = 'HasParent'
|
||||
SCROLLING = 'Scrolling'
|
||||
UPDATING = 'IsUpdating'
|
||||
|
||||
# ListItem Info
|
||||
CONTAINER_LISTITEM_INFO = 'Container(%s).ListItem(0).%s'
|
||||
LISTITEM_INFO = 'ListItem.%s'
|
||||
ARTIST = 'Artist'
|
||||
LABEL = 'Label'
|
||||
PLAY_COUNT = 'PlayCount'
|
||||
RESUMABLE = 'IsResumable'
|
||||
TITLE = 'Title'
|
||||
URI = 'FileNameAndPath'
|
||||
|
||||
# ListItem Properties
|
||||
CONTAINER_LISTITEM_PROP = 'Container(%s).ListItem(0).Property(%s)'
|
||||
LISTITEM_PROP = 'ListItem.Property(%s)'
|
||||
BOOKMARK_ID = 'bookmark_id'
|
||||
CHANNEL_ID = 'channel_id'
|
||||
PLAY_COUNT_PROP = 'video_play_count'
|
||||
PLAYLIST_ID = 'playlist_id'
|
||||
PLAYLIST_ITEM_ID = 'playlist_item_id'
|
||||
SUBSCRIPTION_ID = 'subscription_id'
|
||||
VIDEO_ID = 'video_id'
|
||||
|
||||
# Events
|
||||
CHECK_SETTINGS = 'check_settings'
|
||||
CONTEXT_MENU = 'cxm_action'
|
||||
FILE_READ = 'file_read'
|
||||
FILE_WRITE = 'file_write'
|
||||
KEYMAP = 'key_action'
|
||||
PLAYBACK_INIT = 'playback_init'
|
||||
PLAYBACK_FAILED = 'playback_failed'
|
||||
PLAYBACK_STARTED = 'playback_started'
|
||||
PLAYBACK_STOPPED = 'playback_stopped'
|
||||
REFRESH_CONTAINER = 'refresh_container'
|
||||
RELOAD_ACCESS_MANAGER = 'reload_access_manager'
|
||||
SERVICE_IPC = 'service_ipc'
|
||||
SYNC_LISTITEM = 'sync_listitem'
|
||||
|
||||
# Sleep/wakeup states
|
||||
PLUGIN_WAKEUP = 'plugin_wakeup'
|
||||
PLUGIN_SLEEPING = 'plugin_sleeping'
|
||||
SERVER_WAKEUP = 'server_wakeup'
|
||||
|
||||
# Play options
|
||||
PLAY_CANCELLED = 'play_cancelled'
|
||||
PLAY_FORCE_AUDIO = 'audio_only'
|
||||
PLAY_FORCED = 'play_forced'
|
||||
PLAY_PROMPT_QUALITY = 'ask_for_quality'
|
||||
PLAY_PROMPT_SUBTITLES = 'prompt_for_subtitles'
|
||||
PLAY_STRM = 'strm'
|
||||
PLAY_TIMESHIFT = 'timeshift'
|
||||
PLAY_USING = 'play_using'
|
||||
FORCE_PLAY_PARAMS = frozenset((
|
||||
PLAY_FORCE_AUDIO,
|
||||
PLAY_TIMESHIFT,
|
||||
PLAY_PROMPT_QUALITY,
|
||||
PLAY_PROMPT_SUBTITLES,
|
||||
PLAY_USING,
|
||||
))
|
||||
|
||||
# Stored data
|
||||
PROPERTY = 'Window(home).Property(%s-%%s)' % ADDON_ID
|
||||
PROPERTY_AS_LABEL = '$INFO[Window(home).Property(%s-%%s)]' % ADDON_ID
|
||||
CONTAINER_ID = 'container_id'
|
||||
CONTAINER_FOCUS = 'container_focus'
|
||||
CONTAINER_POSITION = 'container_position'
|
||||
DEVELOPER_CONFIGS = 'configs'
|
||||
LICENSE_TOKEN = 'license_token'
|
||||
LICENSE_URL = 'license_url'
|
||||
MARK_AS_LABEL = 'mark_as_label'
|
||||
PLAYER_DATA = 'player_json'
|
||||
PLAYER_VIDEO_ID = 'player_video_id'
|
||||
PLAYLIST_PATH = 'playlist_path'
|
||||
PLAYLIST_POSITION = 'playlist_position'
|
||||
REROUTE_PATH = 'reroute_path'
|
||||
|
||||
# Routing parameters
|
||||
WINDOW_CACHE = 'window_cache'
|
||||
WINDOW_FALLBACK = 'window_fallback'
|
||||
WINDOW_REPLACE = 'window_replace'
|
||||
WINDOW_RETURN = 'window_return'
|
||||
|
||||
# Plugin url query parameters
|
||||
ACTION = 'action'
|
||||
ADDON_ID_PARAM = 'addon_id'
|
||||
CHANNEL_IDS = 'channel_ids'
|
||||
CLIP = 'clip'
|
||||
END = 'end'
|
||||
FANART_TYPE = 'fanart_type'
|
||||
HIDE_CHANNELS = 'hide_channels'
|
||||
HIDE_FOLDERS = 'hide_folders'
|
||||
HIDE_LIVE = 'hide_live'
|
||||
HIDE_MEMBERS = 'hide_members'
|
||||
HIDE_NEXT_PAGE = 'hide_next_page'
|
||||
HIDE_PLAYLISTS = 'hide_playlists'
|
||||
HIDE_PROGRESS = 'hide_progress'
|
||||
HIDE_SEARCH = 'hide_search'
|
||||
HIDE_SHORTS = 'hide_shorts'
|
||||
HIDE_VIDEOS = 'hide_videos'
|
||||
INCOGNITO = 'incognito'
|
||||
ITEM_FILTER = 'item_filter'
|
||||
ITEMS_PER_PAGE = 'items_per_page'
|
||||
LIVE = 'live'
|
||||
ORDER = 'order'
|
||||
PAGE = 'page'
|
||||
PLAYLIST_IDS = 'playlist_ids'
|
||||
SCREENSAVER = 'screensaver'
|
||||
SEEK = 'seek'
|
||||
SORT_DIR = 'sort_dir'
|
||||
SORT_METHOD = 'sort_method'
|
||||
START = 'start'
|
||||
VIDEO_IDS = 'video_ids'
|
||||
|
||||
INHERITED_PARAMS = frozenset((
|
||||
ADDON_ID_PARAM,
|
||||
FANART_TYPE,
|
||||
HIDE_CHANNELS,
|
||||
HIDE_FOLDERS,
|
||||
HIDE_LIVE,
|
||||
HIDE_MEMBERS,
|
||||
HIDE_NEXT_PAGE,
|
||||
HIDE_PLAYLISTS,
|
||||
HIDE_PROGRESS,
|
||||
HIDE_SEARCH,
|
||||
HIDE_SHORTS,
|
||||
HIDE_VIDEOS,
|
||||
INCOGNITO,
|
||||
ITEM_FILTER,
|
||||
ITEMS_PER_PAGE,
|
||||
PLAY_FORCE_AUDIO,
|
||||
PLAY_TIMESHIFT,
|
||||
PLAY_PROMPT_QUALITY,
|
||||
PLAY_PROMPT_SUBTITLES,
|
||||
PLAY_USING,
|
||||
))
|
||||
|
||||
|
||||
__all__ = (
|
||||
# Addon paths
|
||||
'ADDON_ID',
|
||||
'ADDON_PATH',
|
||||
'DATA_PATH',
|
||||
'MEDIA_PATH',
|
||||
'RESOURCE_PATH',
|
||||
'TEMP_PATH',
|
||||
|
||||
# Const values
|
||||
'BOOL_FROM_STR',
|
||||
'VALUE_TO_STR',
|
||||
'YOUTUBE_HOSTNAMES',
|
||||
|
||||
# Flags
|
||||
'ABORT_FLAG',
|
||||
'BUSY_FLAG',
|
||||
'SERVICE_RUNNING_FLAG',
|
||||
'TRAKT_PAUSE_FLAG',
|
||||
'WAIT_END_FLAG',
|
||||
|
||||
# Container Info
|
||||
'CURRENT_CONTAINER_INFO',
|
||||
'PLUGIN_CONTAINER_INFO',
|
||||
'CURRENT_ITEM',
|
||||
'FOLDER_NAME',
|
||||
'FOLDER_URI',
|
||||
'HAS_FILES',
|
||||
'HAS_FOLDERS',
|
||||
'HAS_PARENT',
|
||||
'SCROLLING',
|
||||
'UPDATING',
|
||||
|
||||
# ListItem Info
|
||||
'CONTAINER_LISTITEM_INFO',
|
||||
'LISTITEM_INFO',
|
||||
'ARTIST',
|
||||
'LABEL',
|
||||
'PLAY_COUNT',
|
||||
'RESUMABLE',
|
||||
'TITLE',
|
||||
'URI',
|
||||
|
||||
# ListItem Properties
|
||||
'CONTAINER_LISTITEM_PROP',
|
||||
'LISTITEM_PROP',
|
||||
'BOOKMARK_ID',
|
||||
'CHANNEL_ID',
|
||||
'PLAY_COUNT_PROP',
|
||||
'PLAYLIST_ID',
|
||||
'PLAYLIST_ITEM_ID',
|
||||
'SUBSCRIPTION_ID',
|
||||
'VIDEO_ID',
|
||||
|
||||
# Events
|
||||
'CHECK_SETTINGS',
|
||||
'CONTEXT_MENU',
|
||||
'FILE_READ',
|
||||
'FILE_WRITE',
|
||||
'KEYMAP',
|
||||
'PLAYBACK_INIT',
|
||||
'PLAYBACK_FAILED',
|
||||
'PLAYBACK_STARTED',
|
||||
'PLAYBACK_STOPPED',
|
||||
'REFRESH_CONTAINER',
|
||||
'RELOAD_ACCESS_MANAGER',
|
||||
'SERVICE_IPC',
|
||||
'SYNC_LISTITEM',
|
||||
|
||||
# Sleep/wakeup states
|
||||
'PLUGIN_SLEEPING',
|
||||
'PLUGIN_WAKEUP',
|
||||
'SERVER_WAKEUP',
|
||||
|
||||
# Play options
|
||||
'PLAY_CANCELLED',
|
||||
'PLAY_FORCE_AUDIO',
|
||||
'PLAY_FORCED',
|
||||
'PLAY_PROMPT_QUALITY',
|
||||
'PLAY_PROMPT_SUBTITLES',
|
||||
'PLAY_STRM',
|
||||
'PLAY_TIMESHIFT',
|
||||
'PLAY_USING',
|
||||
'FORCE_PLAY_PARAMS',
|
||||
|
||||
# Stored data
|
||||
'PROPERTY',
|
||||
'PROPERTY_AS_LABEL',
|
||||
'CONTAINER_ID',
|
||||
'CONTAINER_FOCUS',
|
||||
'CONTAINER_POSITION',
|
||||
'DEVELOPER_CONFIGS',
|
||||
'LICENSE_TOKEN',
|
||||
'LICENSE_URL',
|
||||
'MARK_AS_LABEL',
|
||||
'PLAYER_DATA',
|
||||
'PLAYER_VIDEO_ID',
|
||||
'PLAYLIST_PATH',
|
||||
'PLAYLIST_POSITION',
|
||||
'REROUTE_PATH',
|
||||
|
||||
# Routing parameters
|
||||
'WINDOW_CACHE',
|
||||
'WINDOW_FALLBACK',
|
||||
'WINDOW_REPLACE',
|
||||
'WINDOW_RETURN',
|
||||
|
||||
# Plugin url query parameters
|
||||
'ACTION',
|
||||
'ADDON_ID_PARAM',
|
||||
'CHANNEL_IDS',
|
||||
'CLIP',
|
||||
'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',
|
||||
'ITEM_FILTER',
|
||||
'ITEMS_PER_PAGE',
|
||||
'LIVE',
|
||||
'ORDER',
|
||||
'PAGE',
|
||||
'PLAYLIST_IDS',
|
||||
'SCREENSAVER',
|
||||
'SEEK',
|
||||
'SORT_DIR',
|
||||
'SORT_METHOD',
|
||||
'START',
|
||||
'VIDEO_IDS',
|
||||
|
||||
'INHERITED_PARAMS',
|
||||
|
||||
# Other constants
|
||||
'CONTENT',
|
||||
'PATHS',
|
||||
'SETTINGS',
|
||||
'SORT',
|
||||
|
||||
# Languages and Regions
|
||||
'DEFAULT_LANGUAGES',
|
||||
'DEFAULT_REGIONS',
|
||||
'TRANSLATION_LANGUAGES',
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,32 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
VIDEO_CONTENT = 'episodes'
|
||||
LIST_CONTENT = 'default'
|
||||
|
||||
COMMENTS = 'comments'
|
||||
HISTORY = 'history'
|
||||
PLAYLIST = 'playlist'
|
||||
|
||||
AUDIO_TYPE = 'music'
|
||||
VIDEO_TYPE = 'video'
|
||||
|
||||
FILES = 'files'
|
||||
SONGS = 'songs'
|
||||
ARTISTS = 'artists'
|
||||
ALBUMS = 'albums'
|
||||
MOVIES = 'movies'
|
||||
TV_SHOWS = 'tvshows'
|
||||
EPISODES = 'episodes'
|
||||
VIDEOS = 'videos'
|
||||
MUSIC_VIDEOS = 'musicvideos'
|
||||
@@ -0,0 +1,772 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
DEFAULT_LANGUAGES = {'items': [
|
||||
{'id': 'af', 'snippet': {'hl': 'af', 'name': 'Afrikaans'}},
|
||||
{'id': 'sq', 'snippet': {'hl': 'sq', 'name': 'Albanian'}},
|
||||
{'id': 'am', 'snippet': {'hl': 'am', 'name': 'Amharic'}},
|
||||
{'id': 'ar', 'snippet': {'hl': 'ar', 'name': 'Arabic'}},
|
||||
{'id': 'hy', 'snippet': {'hl': 'hy', 'name': 'Armenian'}},
|
||||
{'id': 'az', 'snippet': {'hl': 'az', 'name': 'Azerbaijani'}},
|
||||
{'id': 'eu', 'snippet': {'hl': 'eu', 'name': 'Basque'}},
|
||||
{'id': 'bn', 'snippet': {'hl': 'bn', 'name': 'Bengali'}},
|
||||
{'id': 'bg', 'snippet': {'hl': 'bg', 'name': 'Bulgarian'}},
|
||||
{'id': 'ca', 'snippet': {'hl': 'ca', 'name': 'Catalan'}},
|
||||
{'id': 'zh-CN', 'snippet': {'hl': 'zh-CN', 'name': 'Chinese'}},
|
||||
{'id': 'zh-HK', 'snippet': {'hl': 'zh-HK', 'name': 'Chinese (Hong Kong)'}},
|
||||
{'id': 'zh-TW', 'snippet': {'hl': 'zh-TW', 'name': 'Chinese (Taiwan)'}},
|
||||
{'id': 'hr', 'snippet': {'hl': 'hr', 'name': 'Croatian'}},
|
||||
{'id': 'cs', 'snippet': {'hl': 'cs', 'name': 'Czech'}},
|
||||
{'id': 'da', 'snippet': {'hl': 'da', 'name': 'Danish'}},
|
||||
{'id': 'nl', 'snippet': {'hl': 'nl', 'name': 'Dutch'}},
|
||||
{'id': 'en', 'snippet': {'hl': 'en', 'name': 'English'}},
|
||||
{'id': 'en-GB', 'snippet': {'hl': 'en-GB', 'name': 'English (United Kingdom)'}},
|
||||
{'id': 'et', 'snippet': {'hl': 'et', 'name': 'Estonian'}},
|
||||
{'id': 'fil', 'snippet': {'hl': 'fil', 'name': 'Filipino'}},
|
||||
{'id': 'fi', 'snippet': {'hl': 'fi', 'name': 'Finnish'}},
|
||||
{'id': 'fr', 'snippet': {'hl': 'fr', 'name': 'French'}},
|
||||
{'id': 'fr-CA', 'snippet': {'hl': 'fr-CA', 'name': 'French (Canada)'}},
|
||||
{'id': 'gl', 'snippet': {'hl': 'gl', 'name': 'Galician'}},
|
||||
{'id': 'ka', 'snippet': {'hl': 'ka', 'name': 'Georgian'}},
|
||||
{'id': 'de', 'snippet': {'hl': 'de', 'name': 'German'}},
|
||||
{'id': 'el', 'snippet': {'hl': 'el', 'name': 'Greek'}},
|
||||
{'id': 'gu', 'snippet': {'hl': 'gu', 'name': 'Gujarati'}},
|
||||
{'id': 'iw', 'snippet': {'hl': 'iw', 'name': 'Hebrew'}},
|
||||
{'id': 'hi', 'snippet': {'hl': 'hi', 'name': 'Hindi'}},
|
||||
{'id': 'hu', 'snippet': {'hl': 'hu', 'name': 'Hungarian'}},
|
||||
{'id': 'is', 'snippet': {'hl': 'is', 'name': 'Icelandic'}},
|
||||
{'id': 'id', 'snippet': {'hl': 'id', 'name': 'Indonesian'}},
|
||||
{'id': 'it', 'snippet': {'hl': 'it', 'name': 'Italian'}},
|
||||
{'id': 'ja', 'snippet': {'hl': 'ja', 'name': 'Japanese'}},
|
||||
{'id': 'kn', 'snippet': {'hl': 'kn', 'name': 'Kannada'}},
|
||||
{'id': 'kk', 'snippet': {'hl': 'kk', 'name': 'Kazakh'}},
|
||||
{'id': 'km', 'snippet': {'hl': 'km', 'name': 'Khmer'}},
|
||||
{'id': 'ko', 'snippet': {'hl': 'ko', 'name': 'Korean'}},
|
||||
{'id': 'ky', 'snippet': {'hl': 'ky', 'name': 'Kyrgyz'}},
|
||||
{'id': 'lo', 'snippet': {'hl': 'lo', 'name': 'Lao'}},
|
||||
{'id': 'lv', 'snippet': {'hl': 'lv', 'name': 'Latvian'}},
|
||||
{'id': 'lt', 'snippet': {'hl': 'lt', 'name': 'Lithuanian'}},
|
||||
{'id': 'mk', 'snippet': {'hl': 'mk', 'name': 'Macedonian'}},
|
||||
{'id': 'ms', 'snippet': {'hl': 'ms', 'name': 'Malay'}},
|
||||
{'id': 'ml', 'snippet': {'hl': 'ml', 'name': 'Malayalam'}},
|
||||
{'id': 'mr', 'snippet': {'hl': 'mr', 'name': 'Marathi'}},
|
||||
{'id': 'mn', 'snippet': {'hl': 'mn', 'name': 'Mongolian'}},
|
||||
{'id': 'my', 'snippet': {'hl': 'my', 'name': 'Myanmar (Burmese)'}},
|
||||
{'id': 'ne', 'snippet': {'hl': 'ne', 'name': 'Nepali'}},
|
||||
{'id': 'no', 'snippet': {'hl': 'no', 'name': 'Norwegian'}},
|
||||
{'id': 'fa', 'snippet': {'hl': 'fa', 'name': 'Persian'}},
|
||||
{'id': 'pl', 'snippet': {'hl': 'pl', 'name': 'Polish'}},
|
||||
{'id': 'pt', 'snippet': {'hl': 'pt', 'name': 'Portuguese (Brazil)'}},
|
||||
{'id': 'pt-PT', 'snippet': {'hl': 'pt-PT', 'name': 'Portuguese (Portugal)'}},
|
||||
{'id': 'pa', 'snippet': {'hl': 'pa', 'name': 'Punjabi'}},
|
||||
{'id': 'ro', 'snippet': {'hl': 'ro', 'name': 'Romanian'}},
|
||||
{'id': 'ru', 'snippet': {'hl': 'ru', 'name': 'Russian'}},
|
||||
{'id': 'sr', 'snippet': {'hl': 'sr', 'name': 'Serbian'}},
|
||||
{'id': 'si', 'snippet': {'hl': 'si', 'name': 'Sinhala'}},
|
||||
{'id': 'sk', 'snippet': {'hl': 'sk', 'name': 'Slovak'}},
|
||||
{'id': 'sl', 'snippet': {'hl': 'sl', 'name': 'Slovenian'}},
|
||||
{'id': 'es-419', 'snippet': {'hl': 'es-419', 'name': 'Spanish (Latin America)'}},
|
||||
{'id': 'es', 'snippet': {'hl': 'es', 'name': 'Spanish (Spain)'}},
|
||||
{'id': 'sw', 'snippet': {'hl': 'sw', 'name': 'Swahili'}},
|
||||
{'id': 'sv', 'snippet': {'hl': 'sv', 'name': 'Swedish'}},
|
||||
{'id': 'ta', 'snippet': {'hl': 'ta', 'name': 'Tamil'}},
|
||||
{'id': 'te', 'snippet': {'hl': 'te', 'name': 'Telugu'}},
|
||||
{'id': 'th', 'snippet': {'hl': 'th', 'name': 'Thai'}},
|
||||
{'id': 'tr', 'snippet': {'hl': 'tr', 'name': 'Turkish'}},
|
||||
{'id': 'uk', 'snippet': {'hl': 'uk', 'name': 'Ukrainian'}},
|
||||
{'id': 'ur', 'snippet': {'hl': 'ur', 'name': 'Urdu'}},
|
||||
{'id': 'uz', 'snippet': {'hl': 'uz', 'name': 'Uzbek'}},
|
||||
{'id': 'vi', 'snippet': {'hl': 'vi', 'name': 'Vietnamese'}},
|
||||
{'id': 'zu', 'snippet': {'hl': 'zu', 'name': 'Zulu'}},
|
||||
]}
|
||||
|
||||
DEFAULT_REGIONS = {'items': [
|
||||
{'id': 'DZ', 'snippet': {'gl': 'DZ', 'name': 'Algeria'}},
|
||||
{'id': 'AR', 'snippet': {'gl': 'AR', 'name': 'Argentina'}},
|
||||
{'id': 'AU', 'snippet': {'gl': 'AU', 'name': 'Australia'}},
|
||||
{'id': 'AT', 'snippet': {'gl': 'AT', 'name': 'Austria'}},
|
||||
{'id': 'AZ', 'snippet': {'gl': 'AZ', 'name': 'Azerbaijan'}},
|
||||
{'id': 'BH', 'snippet': {'gl': 'BH', 'name': 'Bahrain'}},
|
||||
{'id': 'BY', 'snippet': {'gl': 'BY', 'name': 'Belarus'}},
|
||||
{'id': 'BE', 'snippet': {'gl': 'BE', 'name': 'Belgium'}},
|
||||
{'id': 'BA', 'snippet': {'gl': 'BA', 'name': 'Bosnia and Herzegovina'}},
|
||||
{'id': 'BR', 'snippet': {'gl': 'BR', 'name': 'Brazil'}},
|
||||
{'id': 'BG', 'snippet': {'gl': 'BG', 'name': 'Bulgaria'}},
|
||||
{'id': 'CA', 'snippet': {'gl': 'CA', 'name': 'Canada'}},
|
||||
{'id': 'CL', 'snippet': {'gl': 'CL', 'name': 'Chile'}},
|
||||
{'id': 'CO', 'snippet': {'gl': 'CO', 'name': 'Colombia'}},
|
||||
{'id': 'HR', 'snippet': {'gl': 'HR', 'name': 'Croatia'}},
|
||||
{'id': 'CZ', 'snippet': {'gl': 'CZ', 'name': 'Czech Republic'}},
|
||||
{'id': 'DK', 'snippet': {'gl': 'DK', 'name': 'Denmark'}},
|
||||
{'id': 'EG', 'snippet': {'gl': 'EG', 'name': 'Egypt'}},
|
||||
{'id': 'EE', 'snippet': {'gl': 'EE', 'name': 'Estonia'}},
|
||||
{'id': 'FI', 'snippet': {'gl': 'FI', 'name': 'Finland'}},
|
||||
{'id': 'FR', 'snippet': {'gl': 'FR', 'name': 'France'}},
|
||||
{'id': 'GE', 'snippet': {'gl': 'GE', 'name': 'Georgia'}},
|
||||
{'id': 'DE', 'snippet': {'gl': 'DE', 'name': 'Germany'}},
|
||||
{'id': 'GH', 'snippet': {'gl': 'GH', 'name': 'Ghana'}},
|
||||
{'id': 'GR', 'snippet': {'gl': 'GR', 'name': 'Greece'}},
|
||||
{'id': 'HK', 'snippet': {'gl': 'HK', 'name': 'Hong Kong'}},
|
||||
{'id': 'HU', 'snippet': {'gl': 'HU', 'name': 'Hungary'}},
|
||||
{'id': 'IS', 'snippet': {'gl': 'IS', 'name': 'Iceland'}},
|
||||
{'id': 'IN', 'snippet': {'gl': 'IN', 'name': 'India'}},
|
||||
{'id': 'ID', 'snippet': {'gl': 'ID', 'name': 'Indonesia'}},
|
||||
{'id': 'IQ', 'snippet': {'gl': 'IQ', 'name': 'Iraq'}},
|
||||
{'id': 'IE', 'snippet': {'gl': 'IE', 'name': 'Ireland'}},
|
||||
{'id': 'IL', 'snippet': {'gl': 'IL', 'name': 'Israel'}},
|
||||
{'id': 'IT', 'snippet': {'gl': 'IT', 'name': 'Italy'}},
|
||||
{'id': 'JM', 'snippet': {'gl': 'JM', 'name': 'Jamaica'}},
|
||||
{'id': 'JP', 'snippet': {'gl': 'JP', 'name': 'Japan'}},
|
||||
{'id': 'JO', 'snippet': {'gl': 'JO', 'name': 'Jordan'}},
|
||||
{'id': 'KZ', 'snippet': {'gl': 'KZ', 'name': 'Kazakhstan'}},
|
||||
{'id': 'KE', 'snippet': {'gl': 'KE', 'name': 'Kenya'}},
|
||||
{'id': 'KW', 'snippet': {'gl': 'KW', 'name': 'Kuwait'}},
|
||||
{'id': 'LV', 'snippet': {'gl': 'LV', 'name': 'Latvia'}},
|
||||
{'id': 'LB', 'snippet': {'gl': 'LB', 'name': 'Lebanon'}},
|
||||
{'id': 'LY', 'snippet': {'gl': 'LY', 'name': 'Libya'}},
|
||||
{'id': 'LT', 'snippet': {'gl': 'LT', 'name': 'Lithuania'}},
|
||||
{'id': 'LU', 'snippet': {'gl': 'LU', 'name': 'Luxembourg'}},
|
||||
{'id': 'MK', 'snippet': {'gl': 'MK', 'name': 'Macedonia'}},
|
||||
{'id': 'MY', 'snippet': {'gl': 'MY', 'name': 'Malaysia'}},
|
||||
{'id': 'MX', 'snippet': {'gl': 'MX', 'name': 'Mexico'}},
|
||||
{'id': 'ME', 'snippet': {'gl': 'ME', 'name': 'Montenegro'}},
|
||||
{'id': 'MA', 'snippet': {'gl': 'MA', 'name': 'Morocco'}},
|
||||
{'id': 'NP', 'snippet': {'gl': 'NP', 'name': 'Nepal'}},
|
||||
{'id': 'NL', 'snippet': {'gl': 'NL', 'name': 'Netherlands'}},
|
||||
{'id': 'NZ', 'snippet': {'gl': 'NZ', 'name': 'New Zealand'}},
|
||||
{'id': 'NG', 'snippet': {'gl': 'NG', 'name': 'Nigeria'}},
|
||||
{'id': 'NO', 'snippet': {'gl': 'NO', 'name': 'Norway'}},
|
||||
{'id': 'OM', 'snippet': {'gl': 'OM', 'name': 'Oman'}},
|
||||
{'id': 'PK', 'snippet': {'gl': 'PK', 'name': 'Pakistan'}},
|
||||
{'id': 'PE', 'snippet': {'gl': 'PE', 'name': 'Peru'}},
|
||||
{'id': 'PH', 'snippet': {'gl': 'PH', 'name': 'Philippines'}},
|
||||
{'id': 'PL', 'snippet': {'gl': 'PL', 'name': 'Poland'}},
|
||||
{'id': 'PT', 'snippet': {'gl': 'PT', 'name': 'Portugal'}},
|
||||
{'id': 'PR', 'snippet': {'gl': 'PR', 'name': 'Puerto Rico'}},
|
||||
{'id': 'QA', 'snippet': {'gl': 'QA', 'name': 'Qatar'}},
|
||||
{'id': 'RO', 'snippet': {'gl': 'RO', 'name': 'Romania'}},
|
||||
{'id': 'RU', 'snippet': {'gl': 'RU', 'name': 'Russia'}},
|
||||
{'id': 'SA', 'snippet': {'gl': 'SA', 'name': 'Saudi Arabia'}},
|
||||
{'id': 'SN', 'snippet': {'gl': 'SN', 'name': 'Senegal'}},
|
||||
{'id': 'RS', 'snippet': {'gl': 'RS', 'name': 'Serbia'}},
|
||||
{'id': 'SG', 'snippet': {'gl': 'SG', 'name': 'Singapore'}},
|
||||
{'id': 'SK', 'snippet': {'gl': 'SK', 'name': 'Slovakia'}},
|
||||
{'id': 'SI', 'snippet': {'gl': 'SI', 'name': 'Slovenia'}},
|
||||
{'id': 'ZA', 'snippet': {'gl': 'ZA', 'name': 'South Africa'}},
|
||||
{'id': 'KR', 'snippet': {'gl': 'KR', 'name': 'South Korea'}},
|
||||
{'id': 'ES', 'snippet': {'gl': 'ES', 'name': 'Spain'}},
|
||||
{'id': 'LK', 'snippet': {'gl': 'LK', 'name': 'Sri Lanka'}},
|
||||
{'id': 'SE', 'snippet': {'gl': 'SE', 'name': 'Sweden'}},
|
||||
{'id': 'CH', 'snippet': {'gl': 'CH', 'name': 'Switzerland'}},
|
||||
{'id': 'TW', 'snippet': {'gl': 'TW', 'name': 'Taiwan'}},
|
||||
{'id': 'TZ', 'snippet': {'gl': 'TZ', 'name': 'Tanzania'}},
|
||||
{'id': 'TH', 'snippet': {'gl': 'TH', 'name': 'Thailand'}},
|
||||
{'id': 'TN', 'snippet': {'gl': 'TN', 'name': 'Tunisia'}},
|
||||
{'id': 'TR', 'snippet': {'gl': 'TR', 'name': 'Turkey'}},
|
||||
{'id': 'UG', 'snippet': {'gl': 'UG', 'name': 'Uganda'}},
|
||||
{'id': 'UA', 'snippet': {'gl': 'UA', 'name': 'Ukraine'}},
|
||||
{'id': 'AE', 'snippet': {'gl': 'AE', 'name': 'United Arab Emirates'}},
|
||||
{'id': 'GB', 'snippet': {'gl': 'GB', 'name': 'United Kingdom'}},
|
||||
{'id': 'US', 'snippet': {'gl': 'US', 'name': 'United States'}},
|
||||
{'id': 'VN', 'snippet': {'gl': 'VN', 'name': 'Vietnam'}},
|
||||
{'id': 'YE', 'snippet': {'gl': 'YE', 'name': 'Yemen'}},
|
||||
{'id': 'ZW', 'snippet': {'gl': 'ZW', 'name': 'Zimbabwe'}},
|
||||
]}
|
||||
|
||||
TRANSLATION_LANGUAGES = [
|
||||
{'languageCode': 'aa', 'languageName': {'simpleText': 'Afar'}},
|
||||
{'languageCode': 'ab', 'languageName': {'simpleText': 'Abkhazian'}},
|
||||
{'languageCode': 'ace', 'languageName': {'simpleText': 'Acehnese'}},
|
||||
{'languageCode': 'ach', 'languageName': {'simpleText': 'Acoli'}},
|
||||
{'languageCode': 'ada', 'languageName': {'simpleText': 'Adangme'}},
|
||||
{'languageCode': 'ady', 'languageName': {'simpleText': 'Adyghe'}},
|
||||
{'languageCode': 'ae', 'languageName': {'simpleText': 'Avestan'}},
|
||||
{'languageCode': 'aeb', 'languageName': {'simpleText': 'Tunisian Arabic'}},
|
||||
{'languageCode': 'af', 'languageName': {'simpleText': 'Afrikaans'}},
|
||||
{'languageCode': 'afh', 'languageName': {'simpleText': 'Afrihili'}},
|
||||
{'languageCode': 'agq', 'languageName': {'simpleText': 'Aghem'}},
|
||||
{'languageCode': 'ain', 'languageName': {'simpleText': 'Ainu'}},
|
||||
{'languageCode': 'ak', 'languageName': {'simpleText': 'Akan'}},
|
||||
{'languageCode': 'akk', 'languageName': {'simpleText': 'Akkadian'}},
|
||||
{'languageCode': 'akz', 'languageName': {'simpleText': 'Alabama'}},
|
||||
{'languageCode': 'ale', 'languageName': {'simpleText': 'Aleut'}},
|
||||
{'languageCode': 'aln', 'languageName': {'simpleText': 'Gheg Albanian'}},
|
||||
{'languageCode': 'alt', 'languageName': {'simpleText': 'Southern Altai'}},
|
||||
{'languageCode': 'am', 'languageName': {'simpleText': 'Amharic'}},
|
||||
{'languageCode': 'an', 'languageName': {'simpleText': 'Aragonese'}},
|
||||
{'languageCode': 'ang', 'languageName': {'simpleText': 'Old English'}},
|
||||
{'languageCode': 'anp', 'languageName': {'simpleText': 'Angika'}},
|
||||
{'languageCode': 'ar', 'languageName': {'simpleText': 'Arabic'}},
|
||||
{'languageCode': 'ar_001', 'languageName': {'simpleText': 'Arabic (world)'}},
|
||||
{'languageCode': 'arc', 'languageName': {'simpleText': 'Aramaic'}},
|
||||
{'languageCode': 'arn', 'languageName': {'simpleText': 'Mapuche'}},
|
||||
{'languageCode': 'aro', 'languageName': {'simpleText': 'Araona'}},
|
||||
{'languageCode': 'arp', 'languageName': {'simpleText': 'Arapaho'}},
|
||||
{'languageCode': 'arq', 'languageName': {'simpleText': 'Algerian Arabic'}},
|
||||
{'languageCode': 'ars', 'languageName': {'simpleText': 'Najdi Arabic'}},
|
||||
{'languageCode': 'arw', 'languageName': {'simpleText': 'Arawak'}},
|
||||
{'languageCode': 'ary', 'languageName': {'simpleText': 'Moroccan Arabic'}},
|
||||
{'languageCode': 'arz', 'languageName': {'simpleText': 'Egyptian Arabic'}},
|
||||
{'languageCode': 'as', 'languageName': {'simpleText': 'Assamese'}},
|
||||
{'languageCode': 'asa', 'languageName': {'simpleText': 'Asu'}},
|
||||
{'languageCode': 'ase', 'languageName': {'simpleText': 'American Sign Language'}},
|
||||
{'languageCode': 'ast', 'languageName': {'simpleText': 'Asturian'}},
|
||||
{'languageCode': 'av', 'languageName': {'simpleText': 'Avaric'}},
|
||||
{'languageCode': 'avk', 'languageName': {'simpleText': 'Kotava'}},
|
||||
{'languageCode': 'awa', 'languageName': {'simpleText': 'Awadhi'}},
|
||||
{'languageCode': 'ay', 'languageName': {'simpleText': 'Aymara'}},
|
||||
{'languageCode': 'az', 'languageName': {'simpleText': 'Azerbaijani'}},
|
||||
{'languageCode': 'az_Cyrl', 'languageName': {'simpleText': 'Azerbaijani (Cyrillic)'}},
|
||||
{'languageCode': 'az_Latn', 'languageName': {'simpleText': 'Azerbaijani (Latin)'}},
|
||||
{'languageCode': 'ba', 'languageName': {'simpleText': 'Bashkir'}},
|
||||
{'languageCode': 'bal', 'languageName': {'simpleText': 'Baluchi'}},
|
||||
{'languageCode': 'ban', 'languageName': {'simpleText': 'Balinese'}},
|
||||
{'languageCode': 'bar', 'languageName': {'simpleText': 'Bavarian'}},
|
||||
{'languageCode': 'bas', 'languageName': {'simpleText': 'Basaa'}},
|
||||
{'languageCode': 'bax', 'languageName': {'simpleText': 'Bamun'}},
|
||||
{'languageCode': 'bbc', 'languageName': {'simpleText': 'Batak Toba'}},
|
||||
{'languageCode': 'bbj', 'languageName': {'simpleText': 'Ghomala'}},
|
||||
{'languageCode': 'be', 'languageName': {'simpleText': 'Belarusian'}},
|
||||
{'languageCode': 'bej', 'languageName': {'simpleText': 'Beja'}},
|
||||
{'languageCode': 'bem', 'languageName': {'simpleText': 'Bemba'}},
|
||||
{'languageCode': 'bew', 'languageName': {'simpleText': 'Betawi'}},
|
||||
{'languageCode': 'bez', 'languageName': {'simpleText': 'Bena'}},
|
||||
{'languageCode': 'bfd', 'languageName': {'simpleText': 'Bafut'}},
|
||||
{'languageCode': 'bfq', 'languageName': {'simpleText': 'Badaga'}},
|
||||
{'languageCode': 'bg', 'languageName': {'simpleText': 'Bulgarian'}},
|
||||
{'languageCode': 'bgc', 'languageName': {'simpleText': 'Haryanvi'}},
|
||||
{'languageCode': 'bgn', 'languageName': {'simpleText': 'Western Balochi'}},
|
||||
{'languageCode': 'bho', 'languageName': {'simpleText': 'Bhojpuri'}},
|
||||
{'languageCode': 'bi', 'languageName': {'simpleText': 'Bislama'}},
|
||||
{'languageCode': 'bik', 'languageName': {'simpleText': 'Bikol'}},
|
||||
{'languageCode': 'bin', 'languageName': {'simpleText': 'Bini'}},
|
||||
{'languageCode': 'bjn', 'languageName': {'simpleText': 'Banjar'}},
|
||||
{'languageCode': 'bkm', 'languageName': {'simpleText': 'Kom'}},
|
||||
{'languageCode': 'bla', 'languageName': {'simpleText': 'Siksik\u00e1'}},
|
||||
{'languageCode': 'blo', 'languageName': {'simpleText': 'Anii'}},
|
||||
{'languageCode': 'bm', 'languageName': {'simpleText': 'Bambara'}},
|
||||
{'languageCode': 'bn', 'languageName': {'simpleText': 'Bangla'}},
|
||||
{'languageCode': 'bo', 'languageName': {'simpleText': 'Tibetan'}},
|
||||
{'languageCode': 'bpy', 'languageName': {'simpleText': 'Bishnupriya'}},
|
||||
{'languageCode': 'bqi', 'languageName': {'simpleText': 'Bakhtiari'}},
|
||||
{'languageCode': 'br', 'languageName': {'simpleText': 'Breton'}},
|
||||
{'languageCode': 'bra', 'languageName': {'simpleText': 'Braj'}},
|
||||
{'languageCode': 'brh', 'languageName': {'simpleText': 'Brahui'}},
|
||||
{'languageCode': 'brx', 'languageName': {'simpleText': 'Bodo'}},
|
||||
{'languageCode': 'bs', 'languageName': {'simpleText': 'Bosnian'}},
|
||||
{'languageCode': 'bs_Cyrl', 'languageName': {'simpleText': 'Bosnian (Cyrillic)'}},
|
||||
{'languageCode': 'bs_Latn', 'languageName': {'simpleText': 'Bosnian (Latin)'}},
|
||||
{'languageCode': 'bss', 'languageName': {'simpleText': 'Akoose'}},
|
||||
{'languageCode': 'bua', 'languageName': {'simpleText': 'Buriat'}},
|
||||
{'languageCode': 'bug', 'languageName': {'simpleText': 'Buginese'}},
|
||||
{'languageCode': 'bum', 'languageName': {'simpleText': 'Bulu'}},
|
||||
{'languageCode': 'byn', 'languageName': {'simpleText': 'Blin'}},
|
||||
{'languageCode': 'byv', 'languageName': {'simpleText': 'Medumba'}},
|
||||
{'languageCode': 'ca', 'languageName': {'simpleText': 'Catalan'}},
|
||||
{'languageCode': 'cad', 'languageName': {'simpleText': 'Caddo'}},
|
||||
{'languageCode': 'car', 'languageName': {'simpleText': 'Carib'}},
|
||||
{'languageCode': 'cay', 'languageName': {'simpleText': 'Cayuga'}},
|
||||
{'languageCode': 'cch', 'languageName': {'simpleText': 'Atsam'}},
|
||||
{'languageCode': 'ccp', 'languageName': {'simpleText': 'Chakma'}},
|
||||
{'languageCode': 'ce', 'languageName': {'simpleText': 'Chechen'}},
|
||||
{'languageCode': 'ceb', 'languageName': {'simpleText': 'Cebuano'}},
|
||||
{'languageCode': 'cgg', 'languageName': {'simpleText': 'Chiga'}},
|
||||
{'languageCode': 'ch', 'languageName': {'simpleText': 'Chamorro'}},
|
||||
{'languageCode': 'chb', 'languageName': {'simpleText': 'Chibcha'}},
|
||||
{'languageCode': 'chg', 'languageName': {'simpleText': 'Chagatai'}},
|
||||
{'languageCode': 'chk', 'languageName': {'simpleText': 'Chuukese'}},
|
||||
{'languageCode': 'chm', 'languageName': {'simpleText': 'Mari'}},
|
||||
{'languageCode': 'chn', 'languageName': {'simpleText': 'Chinook Jargon'}},
|
||||
{'languageCode': 'cho', 'languageName': {'simpleText': 'Choctaw'}},
|
||||
{'languageCode': 'chp', 'languageName': {'simpleText': 'Chipewyan'}},
|
||||
{'languageCode': 'chr', 'languageName': {'simpleText': 'Cherokee'}},
|
||||
{'languageCode': 'chy', 'languageName': {'simpleText': 'Cheyenne'}},
|
||||
{'languageCode': 'ckb', 'languageName': {'simpleText': 'Central Kurdish'}},
|
||||
{'languageCode': 'co', 'languageName': {'simpleText': 'Corsican'}},
|
||||
{'languageCode': 'cop', 'languageName': {'simpleText': 'Coptic'}},
|
||||
{'languageCode': 'cps', 'languageName': {'simpleText': 'Capiznon'}},
|
||||
{'languageCode': 'cr', 'languageName': {'simpleText': 'Cree'}},
|
||||
{'languageCode': 'crh', 'languageName': {'simpleText': 'Crimean Tatar'}},
|
||||
{'languageCode': 'cs', 'languageName': {'simpleText': 'Czech'}},
|
||||
{'languageCode': 'csb', 'languageName': {'simpleText': 'Kashubian'}},
|
||||
{'languageCode': 'csw', 'languageName': {'simpleText': 'Swampy Cree'}},
|
||||
{'languageCode': 'cu', 'languageName': {'simpleText': 'Church Slavic'}},
|
||||
{'languageCode': 'cv', 'languageName': {'simpleText': 'Chuvash'}},
|
||||
{'languageCode': 'cy', 'languageName': {'simpleText': 'Welsh'}},
|
||||
{'languageCode': 'da', 'languageName': {'simpleText': 'Danish'}},
|
||||
{'languageCode': 'dak', 'languageName': {'simpleText': 'Dakota'}},
|
||||
{'languageCode': 'dar', 'languageName': {'simpleText': 'Dargwa'}},
|
||||
{'languageCode': 'dav', 'languageName': {'simpleText': 'Taita'}},
|
||||
{'languageCode': 'de', 'languageName': {'simpleText': 'German'}},
|
||||
{'languageCode': 'de_AT', 'languageName': {'simpleText': 'German (Austria)'}},
|
||||
{'languageCode': 'de_CH', 'languageName': {'simpleText': 'German (Switzerland)'}},
|
||||
{'languageCode': 'del', 'languageName': {'simpleText': 'Delaware'}},
|
||||
{'languageCode': 'den', 'languageName': {'simpleText': 'Slave'}},
|
||||
{'languageCode': 'dgr', 'languageName': {'simpleText': 'Dogrib'}},
|
||||
{'languageCode': 'din', 'languageName': {'simpleText': 'Dinka'}},
|
||||
{'languageCode': 'dje', 'languageName': {'simpleText': 'Zarma'}},
|
||||
{'languageCode': 'doi', 'languageName': {'simpleText': 'Dogri'}},
|
||||
{'languageCode': 'dsb', 'languageName': {'simpleText': 'Lower Sorbian'}},
|
||||
{'languageCode': 'dua', 'languageName': {'simpleText': 'Duala'}},
|
||||
{'languageCode': 'dum', 'languageName': {'simpleText': 'Middle Dutch'}},
|
||||
{'languageCode': 'dv', 'languageName': {'simpleText': 'Divehi'}},
|
||||
{'languageCode': 'dyo', 'languageName': {'simpleText': 'Jola-Fonyi'}},
|
||||
{'languageCode': 'dyu', 'languageName': {'simpleText': 'Dyula'}},
|
||||
{'languageCode': 'dz', 'languageName': {'simpleText': 'Dzongkha'}},
|
||||
{'languageCode': 'dzg', 'languageName': {'simpleText': 'Dazaga'}},
|
||||
{'languageCode': 'ebu', 'languageName': {'simpleText': 'Embu'}},
|
||||
{'languageCode': 'ee', 'languageName': {'simpleText': 'Ewe'}},
|
||||
{'languageCode': 'efi', 'languageName': {'simpleText': 'Efik'}},
|
||||
{'languageCode': 'egy', 'languageName': {'simpleText': 'Ancient Egyptian'}},
|
||||
{'languageCode': 'eka', 'languageName': {'simpleText': 'Ekajuk'}},
|
||||
{'languageCode': 'el', 'languageName': {'simpleText': 'Greek'}},
|
||||
{'languageCode': 'elx', 'languageName': {'simpleText': 'Elamite'}},
|
||||
{'languageCode': 'en', 'languageName': {'simpleText': 'English'}},
|
||||
{'languageCode': 'en_AU', 'languageName': {'simpleText': 'English (Australia)'}},
|
||||
{'languageCode': 'en_CA', 'languageName': {'simpleText': 'English (Canada)'}},
|
||||
{'languageCode': 'en_GB', 'languageName': {'simpleText': 'English (United Kingdom)'}},
|
||||
{'languageCode': 'en_US', 'languageName': {'simpleText': 'English (United States)'}},
|
||||
{'languageCode': 'enm', 'languageName': {'simpleText': 'Middle English'}},
|
||||
{'languageCode': 'eo', 'languageName': {'simpleText': 'Esperanto'}},
|
||||
{'languageCode': 'es', 'languageName': {'simpleText': 'Spanish'}},
|
||||
{'languageCode': 'es_419', 'languageName': {'simpleText': 'Spanish (Latin America)'}},
|
||||
{'languageCode': 'es_ES', 'languageName': {'simpleText': 'Spanish (Spain)'}},
|
||||
{'languageCode': 'es_MX', 'languageName': {'simpleText': 'Spanish (Mexico)'}},
|
||||
{'languageCode': 'et', 'languageName': {'simpleText': 'Estonian'}},
|
||||
{'languageCode': 'eu', 'languageName': {'simpleText': 'Basque'}},
|
||||
{'languageCode': 'ewo', 'languageName': {'simpleText': 'Ewondo'}},
|
||||
{'languageCode': 'fa', 'languageName': {'simpleText': 'Persian'}},
|
||||
{'languageCode': 'fa_AF', 'languageName': {'simpleText': 'Persian (Afghanistan)'}},
|
||||
{'languageCode': 'fan', 'languageName': {'simpleText': 'Fang'}},
|
||||
{'languageCode': 'fat', 'languageName': {'simpleText': 'Fanti'}},
|
||||
{'languageCode': 'ff', 'languageName': {'simpleText': 'Fula'}},
|
||||
{'languageCode': 'ff_Adlm', 'languageName': {'simpleText': 'Fula (Adlam)'}},
|
||||
{'languageCode': 'ff_Latn', 'languageName': {'simpleText': 'Fula (Latin)'}},
|
||||
{'languageCode': 'fi', 'languageName': {'simpleText': 'Finnish'}},
|
||||
{'languageCode': 'fil', 'languageName': {'simpleText': 'Filipino'}},
|
||||
{'languageCode': 'fj', 'languageName': {'simpleText': 'Fijian'}},
|
||||
{'languageCode': 'fo', 'languageName': {'simpleText': 'Faroese'}},
|
||||
{'languageCode': 'fon', 'languageName': {'simpleText': 'Fon'}},
|
||||
{'languageCode': 'fr', 'languageName': {'simpleText': 'French'}},
|
||||
{'languageCode': 'fr_CA', 'languageName': {'simpleText': 'French (Canada)'}},
|
||||
{'languageCode': 'fr_CH', 'languageName': {'simpleText': 'French (Switzerland)'}},
|
||||
{'languageCode': 'frm', 'languageName': {'simpleText': 'Middle French'}},
|
||||
{'languageCode': 'fro', 'languageName': {'simpleText': 'Old French'}},
|
||||
{'languageCode': 'frr', 'languageName': {'simpleText': 'Northern Frisian'}},
|
||||
{'languageCode': 'frs', 'languageName': {'simpleText': 'Eastern Frisian'}},
|
||||
{'languageCode': 'fur', 'languageName': {'simpleText': 'Friulian'}},
|
||||
{'languageCode': 'fy', 'languageName': {'simpleText': 'Western Frisian'}},
|
||||
{'languageCode': 'ga', 'languageName': {'simpleText': 'Irish'}},
|
||||
{'languageCode': 'gaa', 'languageName': {'simpleText': 'Ga'}},
|
||||
{'languageCode': 'gay', 'languageName': {'simpleText': 'Gayo'}},
|
||||
{'languageCode': 'gba', 'languageName': {'simpleText': 'Gbaya'}},
|
||||
{'languageCode': 'gd', 'languageName': {'simpleText': 'Scottish Gaelic'}},
|
||||
{'languageCode': 'gez', 'languageName': {'simpleText': 'Geez'}},
|
||||
{'languageCode': 'gil', 'languageName': {'simpleText': 'Gilbertese'}},
|
||||
{'languageCode': 'gl', 'languageName': {'simpleText': 'Galician'}},
|
||||
{'languageCode': 'gmh', 'languageName': {'simpleText': 'Middle High German'}},
|
||||
{'languageCode': 'gn', 'languageName': {'simpleText': 'Guarani'}},
|
||||
{'languageCode': 'goh', 'languageName': {'simpleText': 'Old High German'}},
|
||||
{'languageCode': 'gon', 'languageName': {'simpleText': 'Gondi'}},
|
||||
{'languageCode': 'gor', 'languageName': {'simpleText': 'Gorontalo'}},
|
||||
{'languageCode': 'got', 'languageName': {'simpleText': 'Gothic'}},
|
||||
{'languageCode': 'grb', 'languageName': {'simpleText': 'Grebo'}},
|
||||
{'languageCode': 'grc', 'languageName': {'simpleText': 'Ancient Greek'}},
|
||||
{'languageCode': 'gsw', 'languageName': {'simpleText': 'Swiss German'}},
|
||||
{'languageCode': 'gu', 'languageName': {'simpleText': 'Gujarati'}},
|
||||
{'languageCode': 'guz', 'languageName': {'simpleText': 'Gusii'}},
|
||||
{'languageCode': 'gv', 'languageName': {'simpleText': 'Manx'}},
|
||||
{'languageCode': 'gwi', 'languageName': {'simpleText': 'Gwich\u02bcin'}},
|
||||
{'languageCode': 'ha', 'languageName': {'simpleText': 'Hausa'}},
|
||||
{'languageCode': 'hai', 'languageName': {'simpleText': 'Haida'}},
|
||||
{'languageCode': 'haw', 'languageName': {'simpleText': 'Hawaiian'}},
|
||||
{'languageCode': 'he', 'languageName': {'simpleText': 'Hebrew'}},
|
||||
{'languageCode': 'hi', 'languageName': {'simpleText': 'Hindi'}},
|
||||
{'languageCode': 'hi_Latn', 'languageName': {'simpleText': 'Hindi (Latin)'}},
|
||||
{'languageCode': 'hil', 'languageName': {'simpleText': 'Hiligaynon'}},
|
||||
{'languageCode': 'hit', 'languageName': {'simpleText': 'Hittite'}},
|
||||
{'languageCode': 'hmn', 'languageName': {'simpleText': 'Hmong'}},
|
||||
{'languageCode': 'ho', 'languageName': {'simpleText': 'Hiri Motu'}},
|
||||
{'languageCode': 'hr', 'languageName': {'simpleText': 'Croatian'}},
|
||||
{'languageCode': 'hsb', 'languageName': {'simpleText': 'Upper Sorbian'}},
|
||||
{'languageCode': 'ht', 'languageName': {'simpleText': 'Haitian Creole'}},
|
||||
{'languageCode': 'hu', 'languageName': {'simpleText': 'Hungarian'}},
|
||||
{'languageCode': 'hup', 'languageName': {'simpleText': 'Hupa'}},
|
||||
{'languageCode': 'hy', 'languageName': {'simpleText': 'Armenian'}},
|
||||
{'languageCode': 'hz', 'languageName': {'simpleText': 'Herero'}},
|
||||
{'languageCode': 'ia', 'languageName': {'simpleText': 'Interlingua'}},
|
||||
{'languageCode': 'iba', 'languageName': {'simpleText': 'Iban'}},
|
||||
{'languageCode': 'ibb', 'languageName': {'simpleText': 'Ibibio'}},
|
||||
{'languageCode': 'id', 'languageName': {'simpleText': 'Indonesian'}},
|
||||
{'languageCode': 'ie', 'languageName': {'simpleText': 'Interlingue'}},
|
||||
{'languageCode': 'ig', 'languageName': {'simpleText': 'Igbo'}},
|
||||
{'languageCode': 'ii', 'languageName': {'simpleText': 'Sichuan Yi'}},
|
||||
{'languageCode': 'ik', 'languageName': {'simpleText': 'Inupiaq'}},
|
||||
{'languageCode': 'ilo', 'languageName': {'simpleText': 'Iloko'}},
|
||||
{'languageCode': 'in', 'languageName': {'simpleText': 'Indonesian'}},
|
||||
{'languageCode': 'inh', 'languageName': {'simpleText': 'Ingush'}},
|
||||
{'languageCode': 'io', 'languageName': {'simpleText': 'Ido'}},
|
||||
{'languageCode': 'is', 'languageName': {'simpleText': 'Icelandic'}},
|
||||
{'languageCode': 'it', 'languageName': {'simpleText': 'Italian'}},
|
||||
{'languageCode': 'iu', 'languageName': {'simpleText': 'Inuktitut'}},
|
||||
{'languageCode': 'iw', 'languageName': {'simpleText': 'Hebrew'}},
|
||||
{'languageCode': 'ja', 'languageName': {'simpleText': 'Japanese'}},
|
||||
{'languageCode': 'jbo', 'languageName': {'simpleText': 'Lojban'}},
|
||||
{'languageCode': 'jgo', 'languageName': {'simpleText': 'Ngomba'}},
|
||||
{'languageCode': 'jmc', 'languageName': {'simpleText': 'Machame'}},
|
||||
{'languageCode': 'jpr', 'languageName': {'simpleText': 'Judeo-Persian'}},
|
||||
{'languageCode': 'jrb', 'languageName': {'simpleText': 'Judeo-Arabic'}},
|
||||
{'languageCode': 'jv', 'languageName': {'simpleText': 'Javanese'}},
|
||||
{'languageCode': 'ka', 'languageName': {'simpleText': 'Georgian'}},
|
||||
{'languageCode': 'kaa', 'languageName': {'simpleText': 'Kara-Kalpak'}},
|
||||
{'languageCode': 'kab', 'languageName': {'simpleText': 'Kabyle'}},
|
||||
{'languageCode': 'kac', 'languageName': {'simpleText': 'Kachin'}},
|
||||
{'languageCode': 'kaj', 'languageName': {'simpleText': 'Jju'}},
|
||||
{'languageCode': 'kam', 'languageName': {'simpleText': 'Kamba'}},
|
||||
{'languageCode': 'kaw', 'languageName': {'simpleText': 'Kawi'}},
|
||||
{'languageCode': 'kbd', 'languageName': {'simpleText': 'Kabardian'}},
|
||||
{'languageCode': 'kbl', 'languageName': {'simpleText': 'Kanembu'}},
|
||||
{'languageCode': 'kcg', 'languageName': {'simpleText': 'Tyap'}},
|
||||
{'languageCode': 'kde', 'languageName': {'simpleText': 'Makonde'}},
|
||||
{'languageCode': 'kea', 'languageName': {'simpleText': 'Kabuverdianu'}},
|
||||
{'languageCode': 'kfo', 'languageName': {'simpleText': 'Koro'}},
|
||||
{'languageCode': 'kg', 'languageName': {'simpleText': 'Kongo'}},
|
||||
{'languageCode': 'kgp', 'languageName': {'simpleText': 'Kaingang'}},
|
||||
{'languageCode': 'kha', 'languageName': {'simpleText': 'Khasi'}},
|
||||
{'languageCode': 'kho', 'languageName': {'simpleText': 'Khotanese'}},
|
||||
{'languageCode': 'khq', 'languageName': {'simpleText': 'Koyra Chiini'}},
|
||||
{'languageCode': 'ki', 'languageName': {'simpleText': 'Kikuyu'}},
|
||||
{'languageCode': 'kj', 'languageName': {'simpleText': 'Kuanyama'}},
|
||||
{'languageCode': 'kk', 'languageName': {'simpleText': 'Kazakh'}},
|
||||
{'languageCode': 'kkj', 'languageName': {'simpleText': 'Kako'}},
|
||||
{'languageCode': 'kl', 'languageName': {'simpleText': 'Kalaallisut'}},
|
||||
{'languageCode': 'kln', 'languageName': {'simpleText': 'Kalenjin'}},
|
||||
{'languageCode': 'km', 'languageName': {'simpleText': 'Khmer'}},
|
||||
{'languageCode': 'kmb', 'languageName': {'simpleText': 'Kimbundu'}},
|
||||
{'languageCode': 'kn', 'languageName': {'simpleText': 'Kannada'}},
|
||||
{'languageCode': 'ko', 'languageName': {'simpleText': 'Korean'}},
|
||||
{'languageCode': 'kok', 'languageName': {'simpleText': 'Konkani'}},
|
||||
{'languageCode': 'kos', 'languageName': {'simpleText': 'Kosraean'}},
|
||||
{'languageCode': 'kpe', 'languageName': {'simpleText': 'Kpelle'}},
|
||||
{'languageCode': 'kr', 'languageName': {'simpleText': 'Kanuri'}},
|
||||
{'languageCode': 'krc', 'languageName': {'simpleText': 'Karachay-Balkar'}},
|
||||
{'languageCode': 'krl', 'languageName': {'simpleText': 'Karelian'}},
|
||||
{'languageCode': 'kru', 'languageName': {'simpleText': 'Kurukh'}},
|
||||
{'languageCode': 'ks', 'languageName': {'simpleText': 'Kashmiri'}},
|
||||
{'languageCode': 'ks_Arab', 'languageName': {'simpleText': 'Kashmiri (Arabic)'}},
|
||||
{'languageCode': 'ks_Deva', 'languageName': {'simpleText': 'Kashmiri (Devanagari)'}},
|
||||
{'languageCode': 'ksb', 'languageName': {'simpleText': 'Shambala'}},
|
||||
{'languageCode': 'ksf', 'languageName': {'simpleText': 'Bafia'}},
|
||||
{'languageCode': 'ksh', 'languageName': {'simpleText': 'Colognian'}},
|
||||
{'languageCode': 'ku', 'languageName': {'simpleText': 'Kurdish'}},
|
||||
{'languageCode': 'kum', 'languageName': {'simpleText': 'Kumyk'}},
|
||||
{'languageCode': 'kut', 'languageName': {'simpleText': 'Kutenai'}},
|
||||
{'languageCode': 'kv', 'languageName': {'simpleText': 'Komi'}},
|
||||
{'languageCode': 'kw', 'languageName': {'simpleText': 'Cornish'}},
|
||||
{'languageCode': 'kxv', 'languageName': {'simpleText': 'Kuvi'}},
|
||||
{'languageCode': 'kxv_Deva', 'languageName': {'simpleText': 'Kuvi (Devanagari)'}},
|
||||
{'languageCode': 'kxv_Latn', 'languageName': {'simpleText': 'Kuvi (Latin)'}},
|
||||
{'languageCode': 'kxv_Orya', 'languageName': {'simpleText': 'Kuvi (Odia)'}},
|
||||
{'languageCode': 'kxv_Telu', 'languageName': {'simpleText': 'Kuvi (Telugu)'}},
|
||||
{'languageCode': 'ky', 'languageName': {'simpleText': 'Kyrgyz'}},
|
||||
{'languageCode': 'la', 'languageName': {'simpleText': 'Latin'}},
|
||||
{'languageCode': 'lad', 'languageName': {'simpleText': 'Ladino'}},
|
||||
{'languageCode': 'lag', 'languageName': {'simpleText': 'Langi'}},
|
||||
{'languageCode': 'lah', 'languageName': {'simpleText': 'Western Panjabi'}},
|
||||
{'languageCode': 'lam', 'languageName': {'simpleText': 'Lamba'}},
|
||||
{'languageCode': 'lb', 'languageName': {'simpleText': 'Luxembourgish'}},
|
||||
{'languageCode': 'lez', 'languageName': {'simpleText': 'Lezghian'}},
|
||||
{'languageCode': 'lg', 'languageName': {'simpleText': 'Ganda'}},
|
||||
{'languageCode': 'li', 'languageName': {'simpleText': 'Limburgish'}},
|
||||
{'languageCode': 'lij', 'languageName': {'simpleText': 'Ligurian'}},
|
||||
{'languageCode': 'lkt', 'languageName': {'simpleText': 'Lakota'}},
|
||||
{'languageCode': 'lmo', 'languageName': {'simpleText': 'Lombard'}},
|
||||
{'languageCode': 'ln', 'languageName': {'simpleText': 'Lingala'}},
|
||||
{'languageCode': 'lo', 'languageName': {'simpleText': 'Lao'}},
|
||||
{'languageCode': 'lol', 'languageName': {'simpleText': 'Mongo'}},
|
||||
{'languageCode': 'loz', 'languageName': {'simpleText': 'Lozi'}},
|
||||
{'languageCode': 'lrc', 'languageName': {'simpleText': 'Northern Luri'}},
|
||||
{'languageCode': 'lt', 'languageName': {'simpleText': 'Lithuanian'}},
|
||||
{'languageCode': 'lu', 'languageName': {'simpleText': 'Luba-Katanga'}},
|
||||
{'languageCode': 'lua', 'languageName': {'simpleText': 'Luba-Lulua'}},
|
||||
{'languageCode': 'lui', 'languageName': {'simpleText': 'Luiseno'}},
|
||||
{'languageCode': 'lun', 'languageName': {'simpleText': 'Lunda'}},
|
||||
{'languageCode': 'luo', 'languageName': {'simpleText': 'Luo'}},
|
||||
{'languageCode': 'lus', 'languageName': {'simpleText': 'Mizo'}},
|
||||
{'languageCode': 'luy', 'languageName': {'simpleText': 'Luyia'}},
|
||||
{'languageCode': 'lv', 'languageName': {'simpleText': 'Latvian'}},
|
||||
{'languageCode': 'mad', 'languageName': {'simpleText': 'Madurese'}},
|
||||
{'languageCode': 'maf', 'languageName': {'simpleText': 'Mafa'}},
|
||||
{'languageCode': 'mag', 'languageName': {'simpleText': 'Magahi'}},
|
||||
{'languageCode': 'mai', 'languageName': {'simpleText': 'Maithili'}},
|
||||
{'languageCode': 'mak', 'languageName': {'simpleText': 'Makasar'}},
|
||||
{'languageCode': 'man', 'languageName': {'simpleText': 'Mandingo'}},
|
||||
{'languageCode': 'mas', 'languageName': {'simpleText': 'Masai'}},
|
||||
{'languageCode': 'mde', 'languageName': {'simpleText': 'Maba'}},
|
||||
{'languageCode': 'mdf', 'languageName': {'simpleText': 'Moksha'}},
|
||||
{'languageCode': 'mdr', 'languageName': {'simpleText': 'Mandar'}},
|
||||
{'languageCode': 'men', 'languageName': {'simpleText': 'Mende'}},
|
||||
{'languageCode': 'mer', 'languageName': {'simpleText': 'Meru'}},
|
||||
{'languageCode': 'mfe', 'languageName': {'simpleText': 'Morisyen'}},
|
||||
{'languageCode': 'mg', 'languageName': {'simpleText': 'Malagasy'}},
|
||||
{'languageCode': 'mga', 'languageName': {'simpleText': 'Middle Irish'}},
|
||||
{'languageCode': 'mgh', 'languageName': {'simpleText': 'Makhuwa-Meetto'}},
|
||||
{'languageCode': 'mgo', 'languageName': {'simpleText': 'Meta\u02bc'}},
|
||||
{'languageCode': 'mh', 'languageName': {'simpleText': 'Marshallese'}},
|
||||
{'languageCode': 'mi', 'languageName': {'simpleText': 'M\u0101ori'}},
|
||||
{'languageCode': 'mic', 'languageName': {'simpleText': 'Mi\'kmaw'}},
|
||||
{'languageCode': 'min', 'languageName': {'simpleText': 'Minangkabau'}},
|
||||
{'languageCode': 'mk', 'languageName': {'simpleText': 'Macedonian'}},
|
||||
{'languageCode': 'ml', 'languageName': {'simpleText': 'Malayalam'}},
|
||||
{'languageCode': 'mn', 'languageName': {'simpleText': 'Mongolian'}},
|
||||
{'languageCode': 'mnc', 'languageName': {'simpleText': 'Manchu'}},
|
||||
{'languageCode': 'mni', 'languageName': {'simpleText': 'Manipuri'}},
|
||||
{'languageCode': 'mni_Beng', 'languageName': {'simpleText': 'Manipuri (Bangla)'}},
|
||||
{'languageCode': 'mo', 'languageName': {'simpleText': 'Romanian'}},
|
||||
{'languageCode': 'moh', 'languageName': {'simpleText': 'Mohawk'}},
|
||||
{'languageCode': 'mos', 'languageName': {'simpleText': 'Mossi'}},
|
||||
{'languageCode': 'mr', 'languageName': {'simpleText': 'Marathi'}},
|
||||
{'languageCode': 'ms', 'languageName': {'simpleText': 'Malay'}},
|
||||
{'languageCode': 'mt', 'languageName': {'simpleText': 'Maltese'}},
|
||||
{'languageCode': 'mua', 'languageName': {'simpleText': 'Mundang'}},
|
||||
{'languageCode': 'mul', 'languageName': {'simpleText': 'Multiple languages'}},
|
||||
{'languageCode': 'mus', 'languageName': {'simpleText': 'Muscogee'}},
|
||||
{'languageCode': 'mwl', 'languageName': {'simpleText': 'Mirandese'}},
|
||||
{'languageCode': 'mwr', 'languageName': {'simpleText': 'Marwari'}},
|
||||
{'languageCode': 'my', 'languageName': {'simpleText': 'Burmese'}},
|
||||
{'languageCode': 'mye', 'languageName': {'simpleText': 'Myene'}},
|
||||
{'languageCode': 'myv', 'languageName': {'simpleText': 'Erzya'}},
|
||||
{'languageCode': 'mzn', 'languageName': {'simpleText': 'Mazanderani'}},
|
||||
{'languageCode': 'na', 'languageName': {'simpleText': 'Nauru'}},
|
||||
{'languageCode': 'nap', 'languageName': {'simpleText': 'Neapolitan'}},
|
||||
{'languageCode': 'naq', 'languageName': {'simpleText': 'Nama'}},
|
||||
{'languageCode': 'nb', 'languageName': {'simpleText': 'Norwegian Bokm\u00e5l'}},
|
||||
{'languageCode': 'nd', 'languageName': {'simpleText': 'North Ndebele'}},
|
||||
{'languageCode': 'nds', 'languageName': {'simpleText': 'Low German'}},
|
||||
{'languageCode': 'nds_NL', 'languageName': {'simpleText': 'Low German (Netherlands)'}},
|
||||
{'languageCode': 'ne', 'languageName': {'simpleText': 'Nepali'}},
|
||||
{'languageCode': 'new', 'languageName': {'simpleText': 'Newari'}},
|
||||
{'languageCode': 'ng', 'languageName': {'simpleText': 'Ndonga'}},
|
||||
{'languageCode': 'nia', 'languageName': {'simpleText': 'Nias'}},
|
||||
{'languageCode': 'niu', 'languageName': {'simpleText': 'Niuean'}},
|
||||
{'languageCode': 'nl', 'languageName': {'simpleText': 'Dutch'}},
|
||||
{'languageCode': 'nl_BE', 'languageName': {'simpleText': 'Dutch (Belgium)'}},
|
||||
{'languageCode': 'nmg', 'languageName': {'simpleText': 'Kwasio'}},
|
||||
{'languageCode': 'nn', 'languageName': {'simpleText': 'Norwegian Nynorsk'}},
|
||||
{'languageCode': 'nnh', 'languageName': {'simpleText': 'Ngiemboon'}},
|
||||
{'languageCode': 'no', 'languageName': {'simpleText': 'Norwegian'}},
|
||||
{'languageCode': 'nog', 'languageName': {'simpleText': 'Nogai'}},
|
||||
{'languageCode': 'non', 'languageName': {'simpleText': 'Old Norse'}},
|
||||
{'languageCode': 'nqo', 'languageName': {'simpleText': 'N\u2019Ko'}},
|
||||
{'languageCode': 'nr', 'languageName': {'simpleText': 'South Ndebele'}},
|
||||
{'languageCode': 'nso', 'languageName': {'simpleText': 'Northern Sotho'}},
|
||||
{'languageCode': 'nus', 'languageName': {'simpleText': 'Nuer'}},
|
||||
{'languageCode': 'nv', 'languageName': {'simpleText': 'Navajo'}},
|
||||
{'languageCode': 'nwc', 'languageName': {'simpleText': 'Classical Newari'}},
|
||||
{'languageCode': 'ny', 'languageName': {'simpleText': 'Nyanja'}},
|
||||
{'languageCode': 'nym', 'languageName': {'simpleText': 'Nyamwezi'}},
|
||||
{'languageCode': 'nyn', 'languageName': {'simpleText': 'Nyankole'}},
|
||||
{'languageCode': 'nyo', 'languageName': {'simpleText': 'Nyoro'}},
|
||||
{'languageCode': 'nzi', 'languageName': {'simpleText': 'Nzima'}},
|
||||
{'languageCode': 'oc', 'languageName': {'simpleText': 'Occitan'}},
|
||||
{'languageCode': 'oj', 'languageName': {'simpleText': 'Ojibwa'}},
|
||||
{'languageCode': 'om', 'languageName': {'simpleText': 'Oromo'}},
|
||||
{'languageCode': 'or', 'languageName': {'simpleText': 'Odia'}},
|
||||
{'languageCode': 'os', 'languageName': {'simpleText': 'Ossetic'}},
|
||||
{'languageCode': 'osa', 'languageName': {'simpleText': 'Osage'}},
|
||||
{'languageCode': 'ota', 'languageName': {'simpleText': 'Ottoman Turkish'}},
|
||||
{'languageCode': 'pa', 'languageName': {'simpleText': 'Punjabi'}},
|
||||
{'languageCode': 'pa_Arab', 'languageName': {'simpleText': 'Punjabi (Arabic)'}},
|
||||
{'languageCode': 'pa_Guru', 'languageName': {'simpleText': 'Punjabi (Gurmukhi)'}},
|
||||
{'languageCode': 'pag', 'languageName': {'simpleText': 'Pangasinan'}},
|
||||
{'languageCode': 'pal', 'languageName': {'simpleText': 'Pahlavi'}},
|
||||
{'languageCode': 'pam', 'languageName': {'simpleText': 'Pampanga'}},
|
||||
{'languageCode': 'pap', 'languageName': {'simpleText': 'Papiamento'}},
|
||||
{'languageCode': 'pau', 'languageName': {'simpleText': 'Palauan'}},
|
||||
{'languageCode': 'pcm', 'languageName': {'simpleText': 'Nigerian Pidgin'}},
|
||||
{'languageCode': 'peo', 'languageName': {'simpleText': 'Old Persian'}},
|
||||
{'languageCode': 'phn', 'languageName': {'simpleText': 'Phoenician'}},
|
||||
{'languageCode': 'pi', 'languageName': {'simpleText': 'Pali'}},
|
||||
{'languageCode': 'pl', 'languageName': {'simpleText': 'Polish'}},
|
||||
{'languageCode': 'pon', 'languageName': {'simpleText': 'Pohnpeian'}},
|
||||
{'languageCode': 'prg', 'languageName': {'simpleText': 'Prussian'}},
|
||||
{'languageCode': 'pro', 'languageName': {'simpleText': 'Old Proven\u00e7al'}},
|
||||
{'languageCode': 'ps', 'languageName': {'simpleText': 'Pashto'}},
|
||||
{'languageCode': 'pt', 'languageName': {'simpleText': 'Portuguese'}},
|
||||
{'languageCode': 'pt_BR', 'languageName': {'simpleText': 'Portuguese (Brazil)'}},
|
||||
{'languageCode': 'pt_PT', 'languageName': {'simpleText': 'Portuguese (Portugal)'}},
|
||||
{'languageCode': 'qu', 'languageName': {'simpleText': 'Quechua'}},
|
||||
{'languageCode': 'raj', 'languageName': {'simpleText': 'Rajasthani'}},
|
||||
{'languageCode': 'rap', 'languageName': {'simpleText': 'Rapanui'}},
|
||||
{'languageCode': 'rar', 'languageName': {'simpleText': 'Rarotongan'}},
|
||||
{'languageCode': 'rm', 'languageName': {'simpleText': 'Romansh'}},
|
||||
{'languageCode': 'rn', 'languageName': {'simpleText': 'Rundi'}},
|
||||
{'languageCode': 'ro', 'languageName': {'simpleText': 'Romanian'}},
|
||||
{'languageCode': 'ro_MD', 'languageName': {'simpleText': 'Romanian (Moldova)'}},
|
||||
{'languageCode': 'rof', 'languageName': {'simpleText': 'Rombo'}},
|
||||
{'languageCode': 'rom', 'languageName': {'simpleText': 'Romany'}},
|
||||
{'languageCode': 'ru', 'languageName': {'simpleText': 'Russian'}},
|
||||
{'languageCode': 'rup', 'languageName': {'simpleText': 'Aromanian'}},
|
||||
{'languageCode': 'rw', 'languageName': {'simpleText': 'Kinyarwanda'}},
|
||||
{'languageCode': 'rwk', 'languageName': {'simpleText': 'Rwa'}},
|
||||
{'languageCode': 'sa', 'languageName': {'simpleText': 'Sanskrit'}},
|
||||
{'languageCode': 'sad', 'languageName': {'simpleText': 'Sandawe'}},
|
||||
{'languageCode': 'sah', 'languageName': {'simpleText': 'Yakut'}},
|
||||
{'languageCode': 'sam', 'languageName': {'simpleText': 'Samaritan Aramaic'}},
|
||||
{'languageCode': 'saq', 'languageName': {'simpleText': 'Samburu'}},
|
||||
{'languageCode': 'sas', 'languageName': {'simpleText': 'Sasak'}},
|
||||
{'languageCode': 'sat', 'languageName': {'simpleText': 'Santali'}},
|
||||
{'languageCode': 'sat_Olck', 'languageName': {'simpleText': 'Santali (Ol Chiki)'}},
|
||||
{'languageCode': 'sba', 'languageName': {'simpleText': 'Ngambay'}},
|
||||
{'languageCode': 'sbp', 'languageName': {'simpleText': 'Sangu'}},
|
||||
{'languageCode': 'sc', 'languageName': {'simpleText': 'Sardinian'}},
|
||||
{'languageCode': 'scn', 'languageName': {'simpleText': 'Sicilian'}},
|
||||
{'languageCode': 'sco', 'languageName': {'simpleText': 'Scots'}},
|
||||
{'languageCode': 'sd', 'languageName': {'simpleText': 'Sindhi'}},
|
||||
{'languageCode': 'sd_Arab', 'languageName': {'simpleText': 'Sindhi (Arabic)'}},
|
||||
{'languageCode': 'sd_Deva', 'languageName': {'simpleText': 'Sindhi (Devanagari)'}},
|
||||
{'languageCode': 'se', 'languageName': {'simpleText': 'Northern Sami'}},
|
||||
{'languageCode': 'see', 'languageName': {'simpleText': 'Seneca'}},
|
||||
{'languageCode': 'seh', 'languageName': {'simpleText': 'Sena'}},
|
||||
{'languageCode': 'sel', 'languageName': {'simpleText': 'Selkup'}},
|
||||
{'languageCode': 'ses', 'languageName': {'simpleText': 'Koyraboro Senni'}},
|
||||
{'languageCode': 'sg', 'languageName': {'simpleText': 'Sango'}},
|
||||
{'languageCode': 'sga', 'languageName': {'simpleText': 'Old Irish'}},
|
||||
{'languageCode': 'sh', 'languageName': {'simpleText': 'Serbo-Croatian'}},
|
||||
{'languageCode': 'shi', 'languageName': {'simpleText': 'Tachelhit'}},
|
||||
{'languageCode': 'shi_Latn', 'languageName': {'simpleText': 'Tachelhit (Latin)'}},
|
||||
{'languageCode': 'shi_Tfng', 'languageName': {'simpleText': 'Tachelhit (Tifinagh)'}},
|
||||
{'languageCode': 'shn', 'languageName': {'simpleText': 'Shan'}},
|
||||
{'languageCode': 'shu', 'languageName': {'simpleText': 'Chadian Arabic'}},
|
||||
{'languageCode': 'si', 'languageName': {'simpleText': 'Sinhala'}},
|
||||
{'languageCode': 'sid', 'languageName': {'simpleText': 'Sidamo'}},
|
||||
{'languageCode': 'sk', 'languageName': {'simpleText': 'Slovak'}},
|
||||
{'languageCode': 'sl', 'languageName': {'simpleText': 'Slovenian'}},
|
||||
{'languageCode': 'sm', 'languageName': {'simpleText': 'Samoan'}},
|
||||
{'languageCode': 'sma', 'languageName': {'simpleText': 'Southern Sami'}},
|
||||
{'languageCode': 'smj', 'languageName': {'simpleText': 'Lule Sami'}},
|
||||
{'languageCode': 'smn', 'languageName': {'simpleText': 'Inari Sami'}},
|
||||
{'languageCode': 'sms', 'languageName': {'simpleText': 'Skolt Sami'}},
|
||||
{'languageCode': 'sn', 'languageName': {'simpleText': 'Shona'}},
|
||||
{'languageCode': 'snk', 'languageName': {'simpleText': 'Soninke'}},
|
||||
{'languageCode': 'so', 'languageName': {'simpleText': 'Somali'}},
|
||||
{'languageCode': 'sog', 'languageName': {'simpleText': 'Sogdien'}},
|
||||
{'languageCode': 'sq', 'languageName': {'simpleText': 'Albanian'}},
|
||||
{'languageCode': 'sr', 'languageName': {'simpleText': 'Serbian'}},
|
||||
{'languageCode': 'sr_Cyrl', 'languageName': {'simpleText': 'Serbian (Cyrillic)'}},
|
||||
{'languageCode': 'sr_Latn', 'languageName': {'simpleText': 'Serbian (Latin)'}},
|
||||
{'languageCode': 'srn', 'languageName': {'simpleText': 'Sranan Tongo'}},
|
||||
{'languageCode': 'srr', 'languageName': {'simpleText': 'Serer'}},
|
||||
{'languageCode': 'ss', 'languageName': {'simpleText': 'Swati'}},
|
||||
{'languageCode': 'ssy', 'languageName': {'simpleText': 'Saho'}},
|
||||
{'languageCode': 'st', 'languageName': {'simpleText': 'Southern Sotho'}},
|
||||
{'languageCode': 'su', 'languageName': {'simpleText': 'Sundanese'}},
|
||||
{'languageCode': 'su_Latn', 'languageName': {'simpleText': 'Sundanese (Latin)'}},
|
||||
{'languageCode': 'suk', 'languageName': {'simpleText': 'Sukuma'}},
|
||||
{'languageCode': 'sus', 'languageName': {'simpleText': 'Susu'}},
|
||||
{'languageCode': 'sux', 'languageName': {'simpleText': 'Sumerian'}},
|
||||
{'languageCode': 'sv', 'languageName': {'simpleText': 'Swedish'}},
|
||||
{'languageCode': 'sw', 'languageName': {'simpleText': 'Swahili'}},
|
||||
{'languageCode': 'sw_CD', 'languageName': {'simpleText': 'Swahili (Congo - Kinshasa)'}},
|
||||
{'languageCode': 'swb', 'languageName': {'simpleText': 'Comorian'}},
|
||||
{'languageCode': 'syc', 'languageName': {'simpleText': 'Classical Syriac'}},
|
||||
{'languageCode': 'syr', 'languageName': {'simpleText': 'Syriac'}},
|
||||
{'languageCode': 'szl', 'languageName': {'simpleText': 'Silesian'}},
|
||||
{'languageCode': 'ta', 'languageName': {'simpleText': 'Tamil'}},
|
||||
{'languageCode': 'te', 'languageName': {'simpleText': 'Telugu'}},
|
||||
{'languageCode': 'tem', 'languageName': {'simpleText': 'Timne'}},
|
||||
{'languageCode': 'teo', 'languageName': {'simpleText': 'Teso'}},
|
||||
{'languageCode': 'ter', 'languageName': {'simpleText': 'Tereno'}},
|
||||
{'languageCode': 'tet', 'languageName': {'simpleText': 'Tetum'}},
|
||||
{'languageCode': 'tg', 'languageName': {'simpleText': 'Tajik'}},
|
||||
{'languageCode': 'th', 'languageName': {'simpleText': 'Thai'}},
|
||||
{'languageCode': 'ti', 'languageName': {'simpleText': 'Tigrinya'}},
|
||||
{'languageCode': 'tig', 'languageName': {'simpleText': 'Tigre'}},
|
||||
{'languageCode': 'tiv', 'languageName': {'simpleText': 'Tiv'}},
|
||||
{'languageCode': 'tk', 'languageName': {'simpleText': 'Turkmen'}},
|
||||
{'languageCode': 'tkl', 'languageName': {'simpleText': 'Tokelau'}},
|
||||
{'languageCode': 'tl', 'languageName': {'simpleText': 'Tagalog'}},
|
||||
{'languageCode': 'tlh', 'languageName': {'simpleText': 'Klingon'}},
|
||||
{'languageCode': 'tli', 'languageName': {'simpleText': 'Tlingit'}},
|
||||
{'languageCode': 'tmh', 'languageName': {'simpleText': 'Tamashek'}},
|
||||
{'languageCode': 'tn', 'languageName': {'simpleText': 'Tswana'}},
|
||||
{'languageCode': 'to', 'languageName': {'simpleText': 'Tongan'}},
|
||||
{'languageCode': 'tog', 'languageName': {'simpleText': 'Nyasa Tonga'}},
|
||||
{'languageCode': 'tok', 'languageName': {'simpleText': 'Toki Pona'}},
|
||||
{'languageCode': 'tpi', 'languageName': {'simpleText': 'Tok Pisin'}},
|
||||
{'languageCode': 'tr', 'languageName': {'simpleText': 'Turkish'}},
|
||||
{'languageCode': 'trv', 'languageName': {'simpleText': 'Taroko'}},
|
||||
{'languageCode': 'ts', 'languageName': {'simpleText': 'Tsonga'}},
|
||||
{'languageCode': 'tsi', 'languageName': {'simpleText': 'Tsimshian'}},
|
||||
{'languageCode': 'tt', 'languageName': {'simpleText': 'Tatar'}},
|
||||
{'languageCode': 'tum', 'languageName': {'simpleText': 'Tumbuka'}},
|
||||
{'languageCode': 'tvl', 'languageName': {'simpleText': 'Tuvalu'}},
|
||||
{'languageCode': 'tw', 'languageName': {'simpleText': 'Twi'}},
|
||||
{'languageCode': 'twq', 'languageName': {'simpleText': 'Tasawaq'}},
|
||||
{'languageCode': 'ty', 'languageName': {'simpleText': 'Tahitian'}},
|
||||
{'languageCode': 'tyv', 'languageName': {'simpleText': 'Tuvinian'}},
|
||||
{'languageCode': 'tzm', 'languageName': {'simpleText': 'Central Atlas Tamazight'}},
|
||||
{'languageCode': 'udm', 'languageName': {'simpleText': 'Udmurt'}},
|
||||
{'languageCode': 'ug', 'languageName': {'simpleText': 'Uyghur'}},
|
||||
{'languageCode': 'uga', 'languageName': {'simpleText': 'Ugaritic'}},
|
||||
{'languageCode': 'uk', 'languageName': {'simpleText': 'Ukrainian'}},
|
||||
{'languageCode': 'umb', 'languageName': {'simpleText': 'Umbundu'}},
|
||||
{'languageCode': 'ur', 'languageName': {'simpleText': 'Urdu'}},
|
||||
{'languageCode': 'uz', 'languageName': {'simpleText': 'Uzbek'}},
|
||||
{'languageCode': 'uz_Arab', 'languageName': {'simpleText': 'Uzbek (Arabic)'}},
|
||||
{'languageCode': 'uz_Cyrl', 'languageName': {'simpleText': 'Uzbek (Cyrillic)'}},
|
||||
{'languageCode': 'uz_Latn', 'languageName': {'simpleText': 'Uzbek (Latin)'}},
|
||||
{'languageCode': 'vai', 'languageName': {'simpleText': 'Vai'}},
|
||||
{'languageCode': 'vai_Latn', 'languageName': {'simpleText': 'Vai (Latin)'}},
|
||||
{'languageCode': 'vai_Vaii', 'languageName': {'simpleText': 'Vai (Vai)'}},
|
||||
{'languageCode': 've', 'languageName': {'simpleText': 'Venda'}},
|
||||
{'languageCode': 'vec', 'languageName': {'simpleText': 'Venetian'}},
|
||||
{'languageCode': 'vi', 'languageName': {'simpleText': 'Vietnamese'}},
|
||||
{'languageCode': 'vmw', 'languageName': {'simpleText': 'Makhuwa'}},
|
||||
{'languageCode': 'vo', 'languageName': {'simpleText': 'Volap\u00fck'}},
|
||||
{'languageCode': 'vot', 'languageName': {'simpleText': 'Votic'}},
|
||||
{'languageCode': 'vun', 'languageName': {'simpleText': 'Vunjo'}},
|
||||
{'languageCode': 'wa', 'languageName': {'simpleText': 'Walloon'}},
|
||||
{'languageCode': 'wae', 'languageName': {'simpleText': 'Walser'}},
|
||||
{'languageCode': 'wal', 'languageName': {'simpleText': 'Wolaytta'}},
|
||||
{'languageCode': 'war', 'languageName': {'simpleText': 'Waray'}},
|
||||
{'languageCode': 'was', 'languageName': {'simpleText': 'Washo'}},
|
||||
{'languageCode': 'wo', 'languageName': {'simpleText': 'Wolof'}},
|
||||
{'languageCode': 'xal', 'languageName': {'simpleText': 'Kalmyk'}},
|
||||
{'languageCode': 'xh', 'languageName': {'simpleText': 'Xhosa'}},
|
||||
{'languageCode': 'xnr', 'languageName': {'simpleText': 'Kangri'}},
|
||||
{'languageCode': 'xog', 'languageName': {'simpleText': 'Soga'}},
|
||||
{'languageCode': 'yao', 'languageName': {'simpleText': 'Yao'}},
|
||||
{'languageCode': 'yap', 'languageName': {'simpleText': 'Yapese'}},
|
||||
{'languageCode': 'yav', 'languageName': {'simpleText': 'Yangben'}},
|
||||
{'languageCode': 'ybb', 'languageName': {'simpleText': 'Yemba'}},
|
||||
{'languageCode': 'yi', 'languageName': {'simpleText': 'Yiddish'}},
|
||||
{'languageCode': 'yo', 'languageName': {'simpleText': 'Yoruba'}},
|
||||
{'languageCode': 'yrl', 'languageName': {'simpleText': 'Nheengatu'}},
|
||||
{'languageCode': 'yue', 'languageName': {'simpleText': 'Cantonese'}},
|
||||
{'languageCode': 'yue_Hans', 'languageName': {'simpleText': 'Cantonese (Simplified)'}},
|
||||
{'languageCode': 'yue_Hant', 'languageName': {'simpleText': 'Cantonese (Traditional)'}},
|
||||
{'languageCode': 'za', 'languageName': {'simpleText': 'Zhuang'}},
|
||||
{'languageCode': 'zap', 'languageName': {'simpleText': 'Zapotec'}},
|
||||
{'languageCode': 'zbl', 'languageName': {'simpleText': 'Blissymbols'}},
|
||||
{'languageCode': 'zen', 'languageName': {'simpleText': 'Zenaga'}},
|
||||
{'languageCode': 'zgh', 'languageName': {'simpleText': 'Standard Moroccan Tamazight'}},
|
||||
{'languageCode': 'zh', 'languageName': {'simpleText': 'Chinese'}},
|
||||
{'languageCode': 'zh_Hans', 'languageName': {'simpleText': 'Chinese (Simplified)'}},
|
||||
{'languageCode': 'zh_Hant', 'languageName': {'simpleText': 'Chinese (Traditional)'}},
|
||||
{'languageCode': 'zh_TW', 'languageName': {'simpleText': 'Chinese (Taiwan)'}},
|
||||
{'languageCode': 'zu', 'languageName': {'simpleText': 'Zulu'}},
|
||||
{'languageCode': 'zun', 'languageName': {'simpleText': 'Zuni'}},
|
||||
{'languageCode': 'zxx', 'languageName': {'simpleText': 'No linguistic content'}},
|
||||
{'languageCode': 'zza', 'languageName': {'simpleText': 'Zaza'}},
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- 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
|
||||
|
||||
INTERNAL = '/kodion'
|
||||
BOOKMARKS = INTERNAL + '/bookmarks'
|
||||
COMMAND = INTERNAL + '/command'
|
||||
EXTERNAL_SEARCH = '/search'
|
||||
GOTO_PAGE = INTERNAL + '/goto_page'
|
||||
ROUTE = INTERNAL + '/route'
|
||||
SEARCH = INTERNAL + '/search'
|
||||
WATCH_LATER = INTERNAL + '/watch_later'
|
||||
HISTORY = INTERNAL + '/playback_history'
|
||||
|
||||
CHANNEL = '/channel'
|
||||
MY_CHANNEL = CHANNEL + '/mine'
|
||||
LIKED_VIDEOS = CHANNEL + '/mine/playlist/LL'
|
||||
MY_PLAYLIST = CHANNEL + '/mine/playlist'
|
||||
MY_PLAYLISTS = CHANNEL + '/mine/playlists'
|
||||
|
||||
HOME = '/home'
|
||||
MAINTENANCE = '/maintenance'
|
||||
PLAY = '/play'
|
||||
PLAYLIST = '/playlist'
|
||||
SUBSCRIPTIONS = '/subscriptions'
|
||||
VIDEO = '/video'
|
||||
|
||||
SPECIAL = '/special'
|
||||
DESCRIPTION_LINKS = SPECIAL + '/description_links'
|
||||
DISLIKED_VIDEOS = SPECIAL + '/disliked_videos'
|
||||
LIVE_VIDEOS = SPECIAL + '/live'
|
||||
LIVE_VIDEOS_COMPLETED = SPECIAL + '/completed_live'
|
||||
LIVE_VIDEOS_UPCOMING = SPECIAL + '/upcoming_live'
|
||||
MY_SUBSCRIPTIONS = SPECIAL + '/my_subscriptions'
|
||||
MY_SUBSCRIPTIONS_FILTERED = SPECIAL + '/my_subscriptions_filtered'
|
||||
RECOMMENDATIONS = SPECIAL + '/recommendations'
|
||||
RELATED_VIDEOS = SPECIAL + '/related_videos'
|
||||
SAVED_PLAYLISTS = SPECIAL + '/saved_playlists'
|
||||
TRENDING = SPECIAL + '/popular_right_now'
|
||||
VIDEO_COMMENTS = SPECIAL + '/parent_comments'
|
||||
VIDEO_COMMENTS_THREAD = SPECIAL + '/child_comments'
|
||||
VIRTUAL_PLAYLIST = SPECIAL + '/playlist'
|
||||
|
||||
HTTP_SERVER = '/youtube'
|
||||
API = HTTP_SERVER + '/api'
|
||||
API_SUBMIT = HTTP_SERVER + '/api/submit'
|
||||
DRM = HTTP_SERVER + '/widevine'
|
||||
IP = HTTP_SERVER + '/client_ip'
|
||||
MPD = HTTP_SERVER + '/manifest/dash'
|
||||
PING = HTTP_SERVER + '/ping'
|
||||
REDIRECT = HTTP_SERVER + '/redirect'
|
||||
STREAM_PROXY = HTTP_SERVER + '/stream'
|
||||
@@ -0,0 +1,139 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
SETUP_WIZARD = 'kodion.setup_wizard' # (bool)
|
||||
SETUP_WIZARD_RUNS = 'kodion.setup_wizard.forced_runs' # (int)
|
||||
SETTINGS_END = '|end_settings_marker|' # (bool)
|
||||
|
||||
MPD_VIDEOS = 'kodion.mpd.videos' # (bool)
|
||||
MPD_STREAM_SELECT = 'kodion.mpd.stream.select' # (int)
|
||||
MPD_QUALITY_SELECTION = 'kodion.mpd.quality.selection' # (int)
|
||||
MPD_STREAM_FEATURES = 'kodion.mpd.stream.features' # (list[str])
|
||||
VIDEO_STREAM_SELECT = 'kodion.video.stream.select' # (int)
|
||||
VIDEO_QUALITY_ASK = 'kodion.video.quality.ask' # (bool)
|
||||
VIDEO_QUALITY = 'kodion.video.quality' # (int)
|
||||
AUDIO_ONLY = 'kodion.audio_only' # (bool)
|
||||
|
||||
SUBTITLE_SELECTION = 'kodion.subtitle.languages.num' # (int)
|
||||
SUBTITLE_DOWNLOAD = 'kodion.subtitle.download' # (bool)
|
||||
|
||||
ITEMS_PER_PAGE = 'kodion.content.max_per_page' # (int)
|
||||
HIDE_VIDEOS = 'youtube.view.hide_videos' # (list[str])
|
||||
SHORTS_DURATION = 'youtube.view.shorts.duration' # (int)
|
||||
FILTER_LIST = 'youtube.view.filter.list' # (str)
|
||||
|
||||
SUBSCRIPTIONS_FILTER_ENABLED = 'youtube.folder.my_subscriptions_filtered.show' # (bool)
|
||||
SUBSCRIPTIONS_FILTER_BLACKLIST = 'youtube.filter.my_subscriptions_filtered.blacklist' # (bool)
|
||||
SUBSCRIPTIONS_FILTER_LIST = 'youtube.filter.my_subscriptions_filtered.list' # (str)
|
||||
|
||||
SAFE_SEARCH = 'kodion.safe.search' # (int)
|
||||
AGE_GATE = 'kodion.age.gate' # (bool)
|
||||
|
||||
API_CONFIG_PAGE = 'youtube.api.config.page' # (bool)
|
||||
API_KEY = 'youtube.api.key' # (str)
|
||||
API_ID = 'youtube.api.id' # (str)
|
||||
API_SECRET = 'youtube.api.secret' # (str)
|
||||
ALLOW_DEV_KEYS = 'youtube.allow.dev.keys' # (bool)
|
||||
|
||||
SHOW_SIGN_IN = 'youtube.folder.sign.in.show' # (bool)
|
||||
SHOW_MY_SUBSCRIPTIONS = 'youtube.folder.my_subscriptions.show' # (bool)
|
||||
SHOW_MY_SUBSCRIPTIONS_FILTERED = 'youtube.folder.my_subscriptions_filtered.show' # (bool)
|
||||
SHOW_RECOMMENDATIONS = 'youtube.folder.recommendations.show' # (bool)
|
||||
SHOW_RELATED = 'youtube.folder.related.show' # (bool)
|
||||
SHOW_TRENDING = 'youtube.folder.popular_right_now.show' # (bool)
|
||||
SHOW_SEARCH = 'youtube.folder.search.show' # (bool)
|
||||
SHOW_QUICK_SEARCH = 'youtube.folder.quick_search.show' # (bool)
|
||||
SHOW_INCOGNITO_SEARCH = 'youtube.folder.quick_search_incognito.show' # (bool)
|
||||
SHOW_MY_LOCATION = 'youtube.folder.my_location.show' # (bool)
|
||||
SHOW_MY_CHANNEL = 'youtube.folder.my_channel.show' # (bool)
|
||||
SHOW_WATCH_LATER = 'youtube.folder.watch_later.show' # (bool)
|
||||
SHOW_LIKED = 'youtube.folder.liked_videos.show' # (bool)
|
||||
SHOW_DISLIKED = 'youtube.folder.disliked_videos.show' # (bool)
|
||||
SHOW_HISTORY = 'youtube.folder.history.show' # (bool)
|
||||
SHOW_PLAYLISTS = 'youtube.folder.playlists.show' # (bool)
|
||||
SHOW_SAVED_PLAYLISTS = 'youtube.folder.saved.playlists.show' # (bool)
|
||||
SHOW_SUBSCRIPTIONS = 'youtube.folder.subscriptions.show' # (bool)
|
||||
SHOW_BOOKMARKS = 'youtube.folder.bookmarks.show' # (bool)
|
||||
SHOW_BROWSE_CHANNELS = 'youtube.folder.browse_channels.show' # (bool)
|
||||
SHOW_COMPlETED_LIVE = 'youtube.folder.completed.live.show' # (bool)
|
||||
SHOW_UPCOMING_LIVE = 'youtube.folder.upcoming.live.show' # (bool)
|
||||
SHOW_LIVE = 'youtube.folder.live.show' # (bool)
|
||||
SHOW_SWITCH_USER = 'youtube.folder.switch.user.show' # (bool)
|
||||
SHOW_SIGN_OUT = 'youtube.folder.sign.out.show' # (bool)
|
||||
SHOW_SETUP_WIZARD = 'youtube.folder.settings.show' # (bool)
|
||||
SHOW_SETTINGS = 'youtube.folder.settings.advanced.show' # (bool)
|
||||
|
||||
WATCH_LATER_PLAYLIST = 'youtube.folder.watch_later.playlist' # (str)
|
||||
HISTORY_PLAYLIST = 'youtube.folder.history.playlist' # (str)
|
||||
|
||||
SUPPORT_ALTERNATIVE_PLAYER = 'kodion.support.alternative_player' # (bool)
|
||||
DEFAULT_PLAYER_WEB_URLS = 'kodion.default_player.web_urls' # (bool)
|
||||
ALTERNATIVE_PLAYER_WEB_URLS = 'kodion.alternative_player.web_urls' # (bool)
|
||||
ALTERNATIVE_PLAYER_MPD = 'kodion.alternative_player.mpd' # (bool)
|
||||
|
||||
USE_ISA = 'kodion.video.quality.isa' # (bool)
|
||||
LIVE_STREAMS = 'kodion.live_stream.selection' # (int)
|
||||
|
||||
USE_LOCAL_HISTORY = 'kodion.history.local' # (bool)
|
||||
USE_REMOTE_HISTORY = 'kodion.history.remote' # (bool)
|
||||
|
||||
SEARCH_SIZE = 'kodion.search.size' # (int)
|
||||
CACHE_SIZE = 'kodion.cache.size' # (int)
|
||||
|
||||
CHANNEL_NAME_ALIASES = 'youtube.view.channel_name.aliases' # (list[str])
|
||||
DETAILED_DESCRIPTION = 'youtube.view.description.details' # (bool)
|
||||
DETAILED_LABELS = 'youtube.view.label.details' # (bool)
|
||||
LABEL_COLOR = 'youtube.view.label.color' # (str)
|
||||
|
||||
THUMB_SIZE = 'kodion.thumbnail.size' # (int)
|
||||
THUMB_SIZE_BEST = 2
|
||||
FANART_SELECTION = 'kodion.fanart.selection' # (int)
|
||||
FANART_CHANNEL = 2
|
||||
FANART_THUMBNAIL = 3
|
||||
|
||||
LANGUAGE = 'youtube.language' # (str)
|
||||
REGION = 'youtube.region' # (str)
|
||||
LOCATION = 'youtube.location' # (str)
|
||||
LOCATION_RADIUS = 'youtube.location.radius' # (int)
|
||||
|
||||
PLAY_SUGGESTED = 'youtube.suggested_videos' # (bool)
|
||||
|
||||
PLAY_COUNT_MIN_PERCENT = 'kodion.play_count.percent' # (int)
|
||||
|
||||
RATE_VIDEOS = 'youtube.post.play.rate' # (bool)
|
||||
RATE_PLAYLISTS = 'youtube.post.play.rate.playlists' # (bool)
|
||||
PLAY_REFRESH = 'youtube.post.play.refresh' # (bool)
|
||||
|
||||
WATCH_LATER_REMOVE = 'youtube.playlist.watchlater.autoremove' # (bool)
|
||||
|
||||
VERIFY_SSL = 'requests.ssl.verify' # (bool)
|
||||
CONNECT_TIMEOUT = 'requests.timeout.connect' # (int)
|
||||
READ_TIMEOUT = 'requests.timeout.read' # (int)
|
||||
REQUESTS_CACHE_SIZE = 'requests.cache.size' # (int)
|
||||
|
||||
PROXY_SOURCE = 'requests.proxy.source' # (int)
|
||||
PROXY_ENABLED = 'requests.proxy.enabled' # (bool)
|
||||
PROXY_TYPE = 'requests.proxy.type' # (int)
|
||||
PROXY_SERVER = 'requests.proxy.server' # (str)
|
||||
PROXY_PORT = 'requests.proxy.port' # (int)
|
||||
PROXY_USERNAME = 'requests.proxy.username' # (str)
|
||||
PROXY_PASSWORD = 'requests.proxy.password' # (str)
|
||||
|
||||
HTTPD_PORT = 'kodion.http.port' # (int)
|
||||
HTTPD_LISTEN = 'kodion.http.listen' # (str)
|
||||
HTTPD_WHITELIST = 'kodion.http.ip.whitelist' # (str)
|
||||
HTTPD_IDLE_SLEEP = 'youtube.http.idle_sleep' # (bool)
|
||||
HTTPD_STREAM_REDIRECT = 'youtube.http.stream_redirect' # (bool)
|
||||
|
||||
LOG_LEVEL = 'kodion.debug.log.level' # (int)
|
||||
EXEC_LIMIT = 'kodion.debug.exec.limit' # (int)
|
||||
@@ -0,0 +1,210 @@
|
||||
# -*- 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 sys
|
||||
|
||||
from . import const_content_types as CONTENT
|
||||
from ..compatibility import (
|
||||
xbmc,
|
||||
xbmcplugin,
|
||||
)
|
||||
|
||||
|
||||
# Sort methods exposed via xbmcplugin vary by Kodi version.
|
||||
# Rather than try to access them directly they are hardcoded here and checked
|
||||
# against what is defined in xbmcplugin.
|
||||
# IDs of sort methods exposed via xbmcplugin are defined in SortMethod
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/SortFileItem.h
|
||||
# IDs of sort methods used by the Kodi GUI and builtins are defined in SortBy
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/utils/SortUtils.h
|
||||
# The IDs don't match...
|
||||
# As a workaround they are mapped here using the localised label used in the GUI
|
||||
# to allow the sort methods to be set by the addon and then changed dynamically
|
||||
# using infolabels and builtins
|
||||
methods = [
|
||||
# Sort method name, Label ID, SortBy ID
|
||||
('UNSORTED', 571, 0),
|
||||
('NONE', 16018, 0),
|
||||
('LABEL', 551, 1),
|
||||
('LABEL_IGNORE_THE', 551, None),
|
||||
('DATE', 552, 2),
|
||||
('SIZE', 553, 3),
|
||||
('FILE', 561, 4),
|
||||
('FULLPATH', 573, 5),
|
||||
('DRIVE_TYPE', 564, 6),
|
||||
('TITLE', 556, 7),
|
||||
('TITLE_IGNORE_THE', 556, None),
|
||||
('TRACKNUM', 554, 8),
|
||||
('DURATION', 180, 9),
|
||||
('ARTIST', 557, 10),
|
||||
('ARTIST_IGNORE_THE', 557, None),
|
||||
('ALBUM', 558, 12),
|
||||
('ALBUM_IGNORE_THE', 558, None),
|
||||
('GENRE', 515, 14),
|
||||
('COUNTRY', 574, 15),
|
||||
('VIDEO_YEAR', 562, 16),
|
||||
('VIDEO_RATING', 563, 17),
|
||||
('VIDEO_USER_RATING', 38018, 18),
|
||||
('PROGRAM_COUNT', 567, 21),
|
||||
('PLAYLIST_ORDER', 559, 22),
|
||||
('EPISODE', 20359, 23),
|
||||
('DATEADDED', 570, 40),
|
||||
('VIDEO_TITLE', 556, 7),
|
||||
('VIDEO_SORT_TITLE', 171, 29),
|
||||
('VIDEO_SORT_TITLE_IGNORE_THE', 171, None),
|
||||
('VIDEO_RUNTIME', 180, 9),
|
||||
('PRODUCTIONCODE', 20368, 30),
|
||||
('SONG_RATING', 563, 17),
|
||||
('SONG_USER_RATING', 38018, 18),
|
||||
('MPAA_RATING', 20074, 31),
|
||||
('STUDIO', 572, 39),
|
||||
('STUDIO_IGNORE_THE', 572, None),
|
||||
('LABEL_IGNORE_FOLDERS', 551, None),
|
||||
('LASTPLAYED', 568, 41),
|
||||
('PLAYCOUNT', 567, 42),
|
||||
('LISTENERS', 20455, 43),
|
||||
('BITRATE', 623, 44),
|
||||
('CHANNEL', 19029, 46),
|
||||
('DATE_TAKEN', 577, 48),
|
||||
('VIDEO_ORIGINAL_TITLE', 20376, 57),
|
||||
('VIDEO_ORIGINAL_TITLE_IGNORE_THE', 20376, None),
|
||||
]
|
||||
SORT_ID_MAPPING = {}
|
||||
|
||||
SORT = sys.modules[__name__]
|
||||
name = label_id = sort_by = sort_method = None
|
||||
for name, label_id, sort_by in methods:
|
||||
sort_method = getattr(xbmcplugin, 'SORT_METHOD_' + name, 0)
|
||||
setattr(SORT, name, sort_method)
|
||||
if sort_by is not None:
|
||||
SORT_ID_MAPPING.update((
|
||||
(name, sort_by),
|
||||
(xbmc.getLocalizedString(label_id), sort_by),
|
||||
(sort_method, sort_by if sort_method else 0),
|
||||
))
|
||||
|
||||
SORT_ID_MAPPING.update((
|
||||
(CONTENT.VIDEO_CONTENT.join(('__', '__')), SORT.UNSORTED),
|
||||
(CONTENT.LIST_CONTENT.join(('__', '__')), SORT.LABEL),
|
||||
(CONTENT.COMMENTS.join(('__', '__')), SORT.CHANNEL),
|
||||
(CONTENT.HISTORY.join(('__', '__')), SORT.LASTPLAYED),
|
||||
))
|
||||
|
||||
SORT_DIR = {
|
||||
xbmc.getLocalizedString(584): 'ascending',
|
||||
xbmc.getLocalizedString(585): 'descending',
|
||||
}
|
||||
|
||||
# Label mask token details:
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/utils/LabelFormatter.cpp#L33-L105
|
||||
|
||||
# SORT.TRACKNUM is always included after SORT.UNSORTED to allow for
|
||||
# manual/automatic setting of default sort method
|
||||
|
||||
LIST_CONTENT_DETAILED = (
|
||||
(SORT.UNSORTED, '%T \u2022 %P', '%D | %J'),
|
||||
(SORT.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'),
|
||||
(SORT.LABEL, '%T \u2022 %P', '%D | %J'),
|
||||
)
|
||||
LIST_CONTENT_SIMPLE = (
|
||||
(SORT.UNSORTED, '%T', '%D'),
|
||||
(SORT.TRACKNUM, '[%N. ]%T', '%D'),
|
||||
(SORT.LABEL, '%T', '%D'),
|
||||
)
|
||||
|
||||
PLAYLIST_CONTENT_DETAILED = (
|
||||
(SORT.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'),
|
||||
(SORT.LABEL, '[%N. ]%T \u2022 %P', '%D | %J'),
|
||||
(SORT.CHANNEL, '[%N. ][%A - ]%T \u2022 %P', '%D | %J'),
|
||||
(SORT.ARTIST, '[%N. ]%T \u2022 %P | %D | %J', '%A'),
|
||||
(SORT.PROGRAM_COUNT, '[%N. ]%T \u2022 %P | %D | %J', '%C'),
|
||||
(SORT.VIDEO_RATING, '[%N. ]%T \u2022 %P | %D | %J', '%R'),
|
||||
(SORT.DATE, '[%N. ]%T \u2022 %P | %D', '%J'),
|
||||
(SORT.DATEADDED, '[%N. ]%T \u2022 %P | %D', '%a'),
|
||||
(SORT.VIDEO_RUNTIME, '[%N. ]%T \u2022 %P | %J', '%D'),
|
||||
)
|
||||
PLAYLIST_CONTENT_SIMPLE = (
|
||||
(SORT.TRACKNUM, '[%N. ]%T', '%D'),
|
||||
(SORT.LABEL, '[%N. ]%T', '%D'),
|
||||
(SORT.CHANNEL, '[%N. ][%A - ]%T', '%D'),
|
||||
(SORT.ARTIST, '[%N. ]%T', '%A'),
|
||||
(SORT.PROGRAM_COUNT, '[%N. ]%T', '%C'),
|
||||
(SORT.VIDEO_RATING, '[%N. ]%T', '%R'),
|
||||
(SORT.DATE, '[%N. ]%T', '%J'),
|
||||
(SORT.DATEADDED, '[%N. ]%T', '%a'),
|
||||
(SORT.VIDEO_RUNTIME, '[%N. ]%T', '%D'),
|
||||
)
|
||||
|
||||
VIDEO_CONTENT_DETAILED = (
|
||||
(SORT.UNSORTED, '%T \u2022 %P', '%D | %J'),
|
||||
(SORT.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'),
|
||||
(SORT.LABEL, '%T \u2022 %P', '%D | %J'),
|
||||
(SORT.CHANNEL, '[%A - ]%T \u2022 %P', '%D | %J'),
|
||||
(SORT.ARTIST, '%T \u2022 %P | %D | %J', '%A'),
|
||||
(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'),
|
||||
)
|
||||
VIDEO_CONTENT_SIMPLE = (
|
||||
(SORT.UNSORTED, '%T', '%D'),
|
||||
(SORT.TRACKNUM, '[%N. ]%T', '%D'),
|
||||
(SORT.LABEL, '%T', '%D'),
|
||||
(SORT.CHANNEL, '[%A - ]%T', '%D'),
|
||||
(SORT.ARTIST, '%T', '%A'),
|
||||
(SORT.PROGRAM_COUNT, '%T', '%C'),
|
||||
(SORT.VIDEO_RATING, '%T', '%R'),
|
||||
(SORT.DATE, '%T', '%J'),
|
||||
(SORT.DATEADDED, '%T', '%a'),
|
||||
(SORT.VIDEO_RUNTIME, '%T', '%D'),
|
||||
)
|
||||
|
||||
HISTORY_CONTENT_DETAILED = (
|
||||
(SORT.LASTPLAYED, '%T \u2022 %P | %J', '%D | %p'),
|
||||
(SORT.PLAYCOUNT, '%T \u2022 %P | %J', '%D | %V'),
|
||||
)
|
||||
HISTORY_CONTENT_DETAILED += VIDEO_CONTENT_DETAILED
|
||||
|
||||
HISTORY_CONTENT_SIMPLE = (
|
||||
(SORT.LASTPLAYED, '%T', '%D | %p'),
|
||||
(SORT.PLAYCOUNT, '%T', '%D | %V'),
|
||||
)
|
||||
HISTORY_CONTENT_SIMPLE += VIDEO_CONTENT_SIMPLE
|
||||
|
||||
COMMENTS_CONTENT_DETAILED = (
|
||||
(SORT.CHANNEL, '[%A - ]%P \u2022 %T', '%J'),
|
||||
(SORT.TRACKNUM, '[%N. ][%A - ]%P \u2022 %T', '%J'),
|
||||
(SORT.ARTIST, '[%J - ]%P \u2022 %T', '%A'),
|
||||
(SORT.PROGRAM_COUNT, '[%A - ]%P | %J \u2022 %T', '%C'),
|
||||
(SORT.DATE, '[%A - ]%P \u2022 %T', '%J'),
|
||||
)
|
||||
COMMENTS_CONTENT_SIMPLE = (
|
||||
(SORT.CHANNEL, '[%A - ]%T', '%J'),
|
||||
(SORT.TRACKNUM, '[%N. ][%A - ]%T', '%J'),
|
||||
(SORT.ARTIST, '[%A - ]%T', '%A'),
|
||||
(SORT.PROGRAM_COUNT, '[%A - ]%T', '%C'),
|
||||
(SORT.DATE, '[%A - ]%T', '%J'),
|
||||
)
|
||||
|
||||
del (
|
||||
sys,
|
||||
CONTENT,
|
||||
xbmc,
|
||||
xbmcplugin,
|
||||
methods,
|
||||
SORT,
|
||||
name,
|
||||
label_id,
|
||||
sort_by,
|
||||
sort_method,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2023-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
|
||||
|
||||
from .xbmc.xbmc_context import XbmcContext
|
||||
|
||||
|
||||
__all__ = ('XbmcContext',)
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,704 @@
|
||||
# -*- 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 (
|
||||
default_quote,
|
||||
parse_qsl,
|
||||
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,
|
||||
SORT_DIR,
|
||||
SORT_METHOD,
|
||||
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',
|
||||
SORT_DIR,
|
||||
SORT_METHOD,
|
||||
'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([default_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, '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 = default_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()
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,358 @@
|
||||
# -*- 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 sys
|
||||
import threading
|
||||
import time
|
||||
from atexit import register as atexit_register
|
||||
from cProfile import Profile
|
||||
from functools import wraps
|
||||
from inspect import getargvalues
|
||||
from os.path import normpath
|
||||
from pstats import Stats
|
||||
from traceback import extract_stack, format_list
|
||||
from weakref import ref
|
||||
|
||||
from . import logging
|
||||
from .compatibility import StringIO
|
||||
|
||||
|
||||
def debug_here(host='localhost'):
|
||||
import os
|
||||
import sys
|
||||
|
||||
for comp in sys.path:
|
||||
if comp.find('addons') != -1:
|
||||
pydevd_path = os.path.normpath(os.path.join(
|
||||
comp,
|
||||
os.pardir,
|
||||
'script.module.pydevd',
|
||||
'lib',
|
||||
))
|
||||
sys.path.append(pydevd_path)
|
||||
break
|
||||
|
||||
# noinspection PyUnresolvedReferences,PyPackageRequirements
|
||||
import pydevd
|
||||
|
||||
pydevd.settrace(host, stdoutToServer=True, stderrToServer=True)
|
||||
|
||||
|
||||
class ProfilerProxy(ref):
|
||||
def __call__(self, *args, **kwargs):
|
||||
return super(ProfilerProxy, self).__call__().__call__(
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
def __enter__(self, *args, **kwargs):
|
||||
return super(ProfilerProxy, self).__call__().__enter__(
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
return super(ProfilerProxy, self).__call__().__exit__(
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
def disable(self, *args, **kwargs):
|
||||
return super(ProfilerProxy, self).__call__().disable(
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
def enable(self, *args, **kwargs):
|
||||
return super(ProfilerProxy, self).__call__().enable(
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
def get_stats(self, *args, **kwargs):
|
||||
return super(ProfilerProxy, self).__call__().get_stats(
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
def print_stats(self, *args, **kwargs):
|
||||
return super(ProfilerProxy, self).__call__().print_stats(
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
def tear_down(self, *args, **kwargs):
|
||||
return super(ProfilerProxy, self).__call__().tear_down(
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class Profiler(object):
|
||||
"""Class used to profile a block of code"""
|
||||
|
||||
__slots__ = (
|
||||
'__weakref__',
|
||||
'_enabled',
|
||||
'_num_lines',
|
||||
'_print_callees',
|
||||
'_profiler',
|
||||
'_reuse',
|
||||
'_sort_by',
|
||||
'_timer',
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_instances = set()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
self = super(Profiler, cls).__new__(cls)
|
||||
cls._instances.add(self)
|
||||
if not kwargs.get('enabled') or kwargs.get('lazy'):
|
||||
self.__init__(*args, **kwargs)
|
||||
return ProfilerProxy(self)
|
||||
return self
|
||||
|
||||
def __init__(self,
|
||||
enabled=True,
|
||||
lazy=True,
|
||||
num_lines=20,
|
||||
print_callees=False,
|
||||
reuse=False,
|
||||
sort_by=('cumulative', 'time'),
|
||||
timer=None):
|
||||
self._enabled = enabled
|
||||
self._num_lines = num_lines
|
||||
self._print_callees = print_callees
|
||||
self._profiler = None
|
||||
self._reuse = reuse
|
||||
self._sort_by = sort_by
|
||||
self._timer = timer
|
||||
|
||||
if enabled and not lazy:
|
||||
self._create_profiler()
|
||||
|
||||
atexit_register(self.tear_down)
|
||||
|
||||
def __enter__(self):
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if not self._profiler:
|
||||
self._create_profiler()
|
||||
|
||||
def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
self.print_stats()
|
||||
if not self._reuse:
|
||||
self.tear_down()
|
||||
|
||||
def __call__(self, func=None, name=__name__, reuse=False):
|
||||
"""Decorator used to profile function calls"""
|
||||
|
||||
if not func:
|
||||
self._reuse = reuse
|
||||
return self
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""
|
||||
Wrapper to:
|
||||
1) create a new Profiler instance;
|
||||
2) run the function being profiled;
|
||||
3) print out profiler result to the log; and
|
||||
4) return result of function call
|
||||
"""
|
||||
with self:
|
||||
result = func(*args, **kwargs)
|
||||
return result
|
||||
|
||||
if not self._enabled:
|
||||
self.tear_down()
|
||||
return func
|
||||
return wrapper
|
||||
|
||||
def _create_profiler(self):
|
||||
if self._timer:
|
||||
self._profiler = Profile(timer=self._timer)
|
||||
else:
|
||||
self._profiler = Profile()
|
||||
self._profiler.enable()
|
||||
|
||||
try:
|
||||
elapsed_timer = time.perf_counter
|
||||
process_timer = time.process_time
|
||||
except AttributeError:
|
||||
elapsed_timer = time.clock
|
||||
process_timer = time.clock
|
||||
|
||||
def disable(self):
|
||||
if self._profiler:
|
||||
self._profiler.disable()
|
||||
|
||||
def enable(self, flush=False):
|
||||
self._enabled = True
|
||||
if flush or not self._profiler:
|
||||
self._create_profiler()
|
||||
else:
|
||||
self._profiler.enable()
|
||||
|
||||
def get_stats(self,
|
||||
flush=True,
|
||||
num_lines=20,
|
||||
print_callees=False,
|
||||
reuse=False,
|
||||
sort_by=('cumulative', 'time')):
|
||||
if not (self._enabled and self._profiler):
|
||||
return None
|
||||
|
||||
self.disable()
|
||||
|
||||
output_stream = StringIO()
|
||||
try:
|
||||
stats = Stats(
|
||||
self._profiler,
|
||||
stream=output_stream
|
||||
)
|
||||
stats.strip_dirs().sort_stats(*sort_by)
|
||||
if print_callees:
|
||||
stats.print_callees(num_lines)
|
||||
else:
|
||||
stats.print_stats(num_lines)
|
||||
output = output_stream.getvalue()
|
||||
# Occurs when no stats were able to be generated from profiler
|
||||
except TypeError:
|
||||
output = 'Profiler: unable to generate stats'
|
||||
finally:
|
||||
output_stream.close()
|
||||
|
||||
if reuse:
|
||||
# If stats are accumulating then enable existing/new profiler
|
||||
self.enable(flush)
|
||||
|
||||
return output
|
||||
|
||||
def print_stats(self):
|
||||
self.log.info('Profiling stats: %s',
|
||||
self.get_stats(
|
||||
num_lines=self._num_lines,
|
||||
print_callees=self._print_callees,
|
||||
reuse=self._reuse,
|
||||
sort_by=self._sort_by,
|
||||
),
|
||||
stacklevel=3)
|
||||
|
||||
def tear_down(self):
|
||||
self.__class__._instances.discard(self)
|
||||
|
||||
|
||||
class ExecTimeout(object):
|
||||
log = logging.getLogger('__name__')
|
||||
src_file = None
|
||||
|
||||
def __init__(self,
|
||||
seconds,
|
||||
log_only=False,
|
||||
trace_opcodes=False,
|
||||
trace_threads=False,
|
||||
log_locals=False,
|
||||
callback=None,
|
||||
skip_paths=('\\python\\lib\\',
|
||||
'\\logging.py',
|
||||
'\\addons\\script.')):
|
||||
self._interval = seconds
|
||||
self._log_only = log_only
|
||||
self._last_event = (None, None, None)
|
||||
self._timed_out = False
|
||||
|
||||
self._trace_opcodes = trace_opcodes
|
||||
self._trace_threads = trace_threads
|
||||
self._log_locals = log_locals
|
||||
self._callback = callback if callable(callback) else None
|
||||
|
||||
self._skip_paths = skip_paths
|
||||
|
||||
def __call__(self, function):
|
||||
@wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
timer = threading.Timer(self._interval, self.set_timed_out)
|
||||
timer.daemon = True
|
||||
|
||||
if self._trace_threads:
|
||||
threading.settrace(self.timeout_trace)
|
||||
sys.settrace(self.timeout_trace)
|
||||
timer.start()
|
||||
try:
|
||||
return function(*args, **kwargs)
|
||||
finally:
|
||||
timer.cancel()
|
||||
if self._trace_threads:
|
||||
threading.settrace(None)
|
||||
sys.settrace(None)
|
||||
if self._callback:
|
||||
self._callback()
|
||||
self._last_event = (None, None, None)
|
||||
|
||||
return wrapper
|
||||
|
||||
def timeout_trace(self, frame, event, arg):
|
||||
if self._trace_opcodes and hasattr(frame, 'f_trace_opcodes'):
|
||||
frame.f_trace_opcodes = True
|
||||
if self._timed_out:
|
||||
if not self._log_only:
|
||||
raise RuntimeError('Python execution timed out')
|
||||
else:
|
||||
filename = normpath(frame.f_code.co_filename).lower()
|
||||
skip_event = (
|
||||
filename == self.src_file
|
||||
or (self._skip_paths
|
||||
and any(skip_path in filename
|
||||
for skip_path in self._skip_paths))
|
||||
)
|
||||
if not skip_event:
|
||||
self._last_event = (event, frame, arg)
|
||||
return self.timeout_trace
|
||||
|
||||
def set_timed_out(self):
|
||||
msg, kwargs = self._get_msg(to_log=True)
|
||||
self.log.error(msg, **kwargs)
|
||||
self._timed_out = True
|
||||
|
||||
def _get_msg(self, to_log=False):
|
||||
event, frame, arg = self._last_event
|
||||
out = (
|
||||
'Python execution timed out',
|
||||
'Event: {event!r}',
|
||||
'Frame: {frame!r}',
|
||||
'Arg: {arg!r}',
|
||||
'Locals: {locals!r}',
|
||||
'',
|
||||
'Stack (most recent call last):',
|
||||
'{stack_trace}',
|
||||
)
|
||||
log_locals = self._log_locals
|
||||
if log_locals:
|
||||
_locals = getargvalues(frame).locals
|
||||
if log_locals is not True:
|
||||
_locals = dict(tuple(_locals.items())[slice(*log_locals)])
|
||||
else:
|
||||
_locals = None
|
||||
kwargs = {
|
||||
'event': event,
|
||||
'frame': frame,
|
||||
'arg': arg,
|
||||
'locals': _locals,
|
||||
'stack_trace': ''.join(format_list(extract_stack(frame))),
|
||||
}
|
||||
if to_log:
|
||||
return out, kwargs
|
||||
return '\n'.join(out).format(**kwargs)
|
||||
|
||||
|
||||
ExecTimeout.src_file = normpath(
|
||||
ExecTimeout.__init__.__code__.co_filename
|
||||
).lower()
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- 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
|
||||
|
||||
from .compatibility import to_str
|
||||
|
||||
|
||||
class KodionException(Exception):
|
||||
def __init__(self, message='', **kwargs):
|
||||
super(KodionException, self).__init__(message)
|
||||
attrs = self.__dict__
|
||||
for attr, value in kwargs.items():
|
||||
if attr not in attrs:
|
||||
setattr(self, attr, value)
|
||||
|
||||
def get_message(self):
|
||||
return to_str(self)
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- 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
|
||||
|
||||
from . import menu_items
|
||||
from .base_item import BaseItem
|
||||
from .bookmark_item import BookmarkItem
|
||||
from .command_item import CommandItem
|
||||
from .directory_item import DirectoryItem
|
||||
from .image_item import ImageItem
|
||||
from .media_item import AudioItem, MediaItem, VideoItem
|
||||
from .next_page_item import NextPageItem
|
||||
from .search_items import NewSearchItem, SearchHistoryItem, SearchItem
|
||||
from .uri_item import UriItem
|
||||
from .utils import from_json
|
||||
from .watch_later_item import WatchLaterItem
|
||||
from .xbmc.xbmc_items import (
|
||||
directory_listitem,
|
||||
image_listitem,
|
||||
media_listitem,
|
||||
playback_item,
|
||||
uri_listitem,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
'AudioItem',
|
||||
'BaseItem',
|
||||
'BookmarkItem',
|
||||
'CommandItem',
|
||||
'DirectoryItem',
|
||||
'ImageItem',
|
||||
'MediaItem',
|
||||
'NewSearchItem',
|
||||
'NextPageItem',
|
||||
'SearchHistoryItem',
|
||||
'SearchItem',
|
||||
'UriItem',
|
||||
'VideoItem',
|
||||
'WatchLaterItem',
|
||||
'from_json',
|
||||
'menu_items',
|
||||
'directory_listitem',
|
||||
'image_listitem',
|
||||
'media_listitem',
|
||||
'playback_item',
|
||||
'uri_listitem',
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,385 @@
|
||||
# -*- 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 json
|
||||
from datetime import date, datetime
|
||||
|
||||
from .menu_items import separator
|
||||
from ..compatibility import (
|
||||
datetime_infolabel,
|
||||
string_type,
|
||||
to_str,
|
||||
unescape,
|
||||
)
|
||||
from ..constants import MEDIA_PATH
|
||||
from ..utils.methods import generate_hash
|
||||
|
||||
|
||||
class BaseItem(object):
|
||||
_version = 3
|
||||
_playable = False
|
||||
|
||||
def __init__(self, name, uri, image=None, fanart=None, **kwargs):
|
||||
super(BaseItem, self).__init__()
|
||||
self._name = None
|
||||
self.set_name(name)
|
||||
|
||||
self._uri = uri
|
||||
self._available = True
|
||||
self._callback = None
|
||||
self._filter_reason = None
|
||||
self._special_sort = None
|
||||
|
||||
self._image = ''
|
||||
if image:
|
||||
self.set_image(image)
|
||||
self._fanart = ''
|
||||
if fanart:
|
||||
self.set_fanart(fanart)
|
||||
|
||||
self._bookmark_id = None
|
||||
self._bookmark_timestamp = None
|
||||
self._context_menu = None
|
||||
self._added_utc = None
|
||||
self._count = None
|
||||
self._date = None
|
||||
self._dateadded = None
|
||||
self._short_details = None
|
||||
self._production_code = None
|
||||
self._track_number = None
|
||||
|
||||
self._cast = None
|
||||
self._artists = None
|
||||
self._studios = None
|
||||
|
||||
def __str_parts__(self, as_dict=False):
|
||||
kwargs = {
|
||||
'type': self.__class__.__name__,
|
||||
'name': self._name,
|
||||
'uri': self._uri,
|
||||
'available': self._available,
|
||||
'added': self._added_utc,
|
||||
'filtered': self._filter_reason,
|
||||
}
|
||||
if as_dict:
|
||||
return kwargs
|
||||
out = (
|
||||
'{type}(',
|
||||
'name={name!r}, ',
|
||||
'uri={uri!r}, ',
|
||||
'available={available!r}, ',
|
||||
'added=\'{added!s}\', ',
|
||||
'filtered={filtered!r})',
|
||||
)
|
||||
return out, kwargs
|
||||
|
||||
def __str__(self):
|
||||
out, kwargs = self.__str_parts__()
|
||||
return ''.join(out).format(**kwargs)
|
||||
|
||||
def __repr_data__(self):
|
||||
return {'type': self.__class__.__name__, 'data': self.__dict__}
|
||||
|
||||
def __repr__(self):
|
||||
return json.dumps(
|
||||
self.__repr_data__(),
|
||||
ensure_ascii=False,
|
||||
cls=_Encoder
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def generate_id(*args, **kwargs):
|
||||
prefix = kwargs.get('prefix')
|
||||
if prefix:
|
||||
return '%s.%s' % (prefix, generate_hash(*args))
|
||||
return generate_hash(*args)
|
||||
|
||||
def get_id(self):
|
||||
"""
|
||||
Returns a unique id of the item.
|
||||
:return: unique id of the item.
|
||||
"""
|
||||
return self.generate_id(self._name, self._uri)
|
||||
|
||||
def set_name(self, name):
|
||||
try:
|
||||
name = unescape(name)
|
||||
except Exception:
|
||||
pass
|
||||
self._name = name
|
||||
return name
|
||||
|
||||
def get_name(self):
|
||||
"""
|
||||
Returns the name of the item.
|
||||
:return: name of the item.
|
||||
"""
|
||||
return self._name
|
||||
|
||||
def set_uri(self, uri):
|
||||
self._uri = uri if uri and isinstance(uri, string_type) else ''
|
||||
|
||||
def get_uri(self):
|
||||
"""
|
||||
Returns the path of the item.
|
||||
:return: path of the item.
|
||||
"""
|
||||
return self._uri
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return self._available
|
||||
|
||||
@available.setter
|
||||
def available(self, value):
|
||||
self._available = value
|
||||
|
||||
@property
|
||||
def callback(self):
|
||||
return self._callback
|
||||
|
||||
@callback.setter
|
||||
def callback(self, value):
|
||||
self._callback = value.__get__(self) if callable(value) else None
|
||||
|
||||
def set_image(self, image):
|
||||
if not image:
|
||||
return
|
||||
|
||||
if '{media}/' in image:
|
||||
self._image = image.format(media=MEDIA_PATH)
|
||||
else:
|
||||
self._image = image
|
||||
|
||||
def get_image(self):
|
||||
return self._image
|
||||
|
||||
def set_fanart(self, fanart):
|
||||
if not fanart:
|
||||
return
|
||||
|
||||
if '{media}/' in fanart:
|
||||
self._fanart = fanart.format(media=MEDIA_PATH)
|
||||
else:
|
||||
self._fanart = fanart
|
||||
|
||||
def get_fanart(self, default=True):
|
||||
if self._fanart or not default:
|
||||
return self._fanart
|
||||
return '/'.join((
|
||||
MEDIA_PATH,
|
||||
'fanart.jpg',
|
||||
))
|
||||
|
||||
def add_context_menu(self,
|
||||
context_menu,
|
||||
position='end',
|
||||
replace=False,
|
||||
end_separator=separator()):
|
||||
context_menu = [item for item in context_menu if item]
|
||||
if context_menu and end_separator and context_menu[-1] != end_separator:
|
||||
context_menu.append(end_separator)
|
||||
if replace or not self._context_menu:
|
||||
self._context_menu = context_menu
|
||||
elif position == 'end':
|
||||
self._context_menu.extend(context_menu)
|
||||
else:
|
||||
self._context_menu[position:position] = context_menu
|
||||
|
||||
def get_context_menu(self):
|
||||
return self._context_menu
|
||||
|
||||
def set_date(self, year, month, day, hour=0, minute=0, second=0):
|
||||
self._date = datetime(year, month, day, hour, minute, second)
|
||||
|
||||
def set_date_from_datetime(self, date_time):
|
||||
self._date = date_time
|
||||
|
||||
def get_date(self, as_text=False, short=False, as_info_label=False):
|
||||
if self._date:
|
||||
if as_info_label:
|
||||
return datetime_infolabel(self._date, '%d.%m.%Y')
|
||||
if short:
|
||||
return self._date.date().strftime('%x')
|
||||
if as_text:
|
||||
return self._date.strftime('%x %X')
|
||||
return self._date
|
||||
|
||||
def set_dateadded(self, year, month, day, hour=0, minute=0, second=0):
|
||||
self._dateadded = datetime(year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second)
|
||||
|
||||
def set_dateadded_from_datetime(self, date_time):
|
||||
self._dateadded = date_time
|
||||
|
||||
def get_dateadded(self, as_text=False, as_info_label=False):
|
||||
if self._dateadded:
|
||||
if as_info_label:
|
||||
return datetime_infolabel(self._dateadded)
|
||||
if as_text:
|
||||
return self._dateadded.strftime('%x %X')
|
||||
return self._dateadded
|
||||
|
||||
def set_added_utc(self, date_time):
|
||||
self._added_utc = date_time
|
||||
|
||||
def get_added_utc(self):
|
||||
return self._added_utc
|
||||
|
||||
def get_short_details(self):
|
||||
return self._short_details
|
||||
|
||||
def set_short_details(self, details):
|
||||
self._short_details = details or ''
|
||||
|
||||
def get_count(self):
|
||||
return self._count
|
||||
|
||||
def set_count(self, count):
|
||||
self._count = int(count or 0)
|
||||
|
||||
@property
|
||||
def bookmark_id(self):
|
||||
return self._bookmark_id
|
||||
|
||||
@bookmark_id.setter
|
||||
def bookmark_id(self, value):
|
||||
self._bookmark_id = value
|
||||
|
||||
def set_bookmark_timestamp(self, timestamp):
|
||||
self._bookmark_timestamp = timestamp
|
||||
|
||||
def get_bookmark_timestamp(self):
|
||||
return self._bookmark_timestamp
|
||||
|
||||
@property
|
||||
def playable(self):
|
||||
return self._playable
|
||||
|
||||
@playable.setter
|
||||
def playable(self, value):
|
||||
self._playable = value
|
||||
|
||||
def add_artist(self, artist):
|
||||
if artist:
|
||||
if self._artists is None:
|
||||
self._artists = []
|
||||
self._artists.append(to_str(artist))
|
||||
|
||||
def get_artists(self):
|
||||
return self._artists
|
||||
|
||||
def get_artists_string(self):
|
||||
if self._artists:
|
||||
return ', '.join(self._artists)
|
||||
return None
|
||||
|
||||
def set_artists(self, artists):
|
||||
self._artists = list(artists)
|
||||
|
||||
def set_cast(self, members):
|
||||
self._cast = list(members)
|
||||
|
||||
def add_cast(self, name, role=None, order=None, thumbnail=None):
|
||||
if name:
|
||||
if self._cast is None:
|
||||
self._cast = []
|
||||
self._cast.append({
|
||||
'name': to_str(name),
|
||||
'role': to_str(role) if role else '',
|
||||
'order': int(order) if order else len(self._cast) + 1,
|
||||
'thumbnail': to_str(thumbnail) if thumbnail else '',
|
||||
})
|
||||
|
||||
def get_cast(self):
|
||||
return self._cast
|
||||
|
||||
def add_studio(self, studio):
|
||||
if studio:
|
||||
if self._studios is None:
|
||||
self._studios = []
|
||||
self._studios.append(to_str(studio))
|
||||
|
||||
def get_studios(self):
|
||||
return self._studios
|
||||
|
||||
def set_studios(self, studios):
|
||||
self._studios = list(studios)
|
||||
|
||||
def set_production_code(self, value):
|
||||
self._production_code = value or ''
|
||||
|
||||
def get_production_code(self):
|
||||
return self._production_code
|
||||
|
||||
def set_track_number(self, track_number):
|
||||
self._track_number = int(track_number)
|
||||
|
||||
def get_track_number(self):
|
||||
return self._track_number
|
||||
|
||||
def set_filter_reason(self, reason):
|
||||
self._filter_reason = reason
|
||||
|
||||
def get_filter_reason(self):
|
||||
return self._filter_reason
|
||||
|
||||
def set_special_sort(self, position):
|
||||
self._special_sort = position
|
||||
|
||||
def get_special_sort(self):
|
||||
return self._special_sort
|
||||
|
||||
|
||||
class _Encoder(json.JSONEncoder):
|
||||
def encode(self, obj, nested=False):
|
||||
if isinstance(obj, (date, datetime)):
|
||||
class_name = obj.__class__.__name__
|
||||
if 'fromisoformat' in dir(obj):
|
||||
obj = {
|
||||
'__class__': class_name,
|
||||
'__isoformat__': obj.isoformat(),
|
||||
}
|
||||
else:
|
||||
if class_name == 'datetime':
|
||||
if obj.tzinfo:
|
||||
format_string = '%Y-%m-%dT%H:%M:%S%z'
|
||||
else:
|
||||
format_string = '%Y-%m-%dT%H:%M:%S'
|
||||
else:
|
||||
format_string = '%Y-%m-%d'
|
||||
obj = {
|
||||
'__class__': class_name,
|
||||
'__format_string__': format_string,
|
||||
'__value__': obj.strftime(format_string)
|
||||
}
|
||||
|
||||
if isinstance(obj, string_type):
|
||||
output = to_str(obj)
|
||||
elif isinstance(obj, dict):
|
||||
output = {to_str(key): self.encode(value, nested=True)
|
||||
for key, value in obj.items()}
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
output = [self.encode(item, nested=True) for item in obj]
|
||||
else:
|
||||
output = obj
|
||||
|
||||
if nested:
|
||||
return output
|
||||
return super(_Encoder, self).encode(output)
|
||||
|
||||
def default(self, obj):
|
||||
pass
|
||||
@@ -0,0 +1,55 @@
|
||||
# -*- 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
|
||||
|
||||
from .directory_item import DirectoryItem
|
||||
from .media_item import VideoItem
|
||||
|
||||
|
||||
class BookmarkItem(VideoItem, DirectoryItem):
|
||||
def __init__(self,
|
||||
name,
|
||||
uri,
|
||||
image='{media}/bookmarks.png',
|
||||
fanart=None,
|
||||
plot=None,
|
||||
action=False,
|
||||
playable=None,
|
||||
special_sort=None,
|
||||
date_time=None,
|
||||
category_label=None,
|
||||
bookmark_id=None,
|
||||
video_id=None,
|
||||
channel_id=None,
|
||||
playlist_id=None,
|
||||
playlist_item_id=None,
|
||||
subscription_id=None,
|
||||
**_kwargs):
|
||||
super(BookmarkItem, self).__init__(
|
||||
name=name,
|
||||
uri=uri,
|
||||
image=image,
|
||||
fanart=fanart,
|
||||
plot=plot,
|
||||
action=action,
|
||||
special_sort=special_sort,
|
||||
date_time=date_time,
|
||||
category_label=category_label,
|
||||
bookmark_id=bookmark_id,
|
||||
video_id=video_id,
|
||||
channel_id=channel_id,
|
||||
playlist_id=playlist_id,
|
||||
playlist_item_id=playlist_item_id,
|
||||
subscription_id=subscription_id,
|
||||
)
|
||||
self._bookmark_id = bookmark_id
|
||||
if playable is not None:
|
||||
self._playable = playable
|
||||
@@ -0,0 +1,42 @@
|
||||
# -*- 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
|
||||
|
||||
from . import menu_items
|
||||
from .directory_item import DirectoryItem
|
||||
from ..constants import PATHS
|
||||
|
||||
|
||||
class CommandItem(DirectoryItem):
|
||||
def __init__(self,
|
||||
name,
|
||||
command,
|
||||
context,
|
||||
image=None,
|
||||
fanart=None,
|
||||
plot=None,
|
||||
**_kwargs):
|
||||
super(CommandItem, self).__init__(
|
||||
name,
|
||||
context.create_uri((PATHS.COMMAND, command)),
|
||||
image=image,
|
||||
fanart=fanart,
|
||||
plot=plot,
|
||||
action=True,
|
||||
category_label='__inherit__',
|
||||
)
|
||||
|
||||
context_menu = [
|
||||
menu_items.refresh_listing(context),
|
||||
menu_items.goto_home(context),
|
||||
menu_items.goto_quick_search(context),
|
||||
]
|
||||
self.add_context_menu(context_menu)
|
||||
@@ -0,0 +1,129 @@
|
||||
# -*- 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
|
||||
|
||||
from .base_item import BaseItem
|
||||
from ..compatibility import parse_qsl, unescape, urlencode, urlsplit
|
||||
|
||||
|
||||
class DirectoryItem(BaseItem):
|
||||
def __init__(self,
|
||||
name,
|
||||
uri,
|
||||
image='DefaultFolder.png',
|
||||
fanart=None,
|
||||
plot=None,
|
||||
action=False,
|
||||
special_sort=None,
|
||||
date_time=None,
|
||||
category_label=None,
|
||||
bookmark_id=None,
|
||||
channel_id=None,
|
||||
playlist_id=None,
|
||||
subscription_id=None,
|
||||
**kwargs):
|
||||
super(DirectoryItem, self).__init__(
|
||||
name=name,
|
||||
uri=uri,
|
||||
image=image,
|
||||
fanart=fanart,
|
||||
**kwargs
|
||||
)
|
||||
name = self.get_name()
|
||||
self._category_label = None
|
||||
self.set_category_label(category_label or name)
|
||||
self._plot = plot or name
|
||||
self._is_action = action
|
||||
self._bookmark_id = bookmark_id
|
||||
self._channel_id = channel_id
|
||||
self._playlist_id = playlist_id
|
||||
self._subscription_id = subscription_id
|
||||
self._next_page = False
|
||||
if special_sort is not None:
|
||||
self.set_special_sort(special_sort)
|
||||
if date_time is not None:
|
||||
self.set_date_from_datetime(date_time=date_time)
|
||||
|
||||
def set_name(self, name, category_label=None):
|
||||
name = super(DirectoryItem, self).set_name(name)
|
||||
if hasattr(self, '_category_label'):
|
||||
self.set_category_label(category_label or name)
|
||||
self.set_plot(name)
|
||||
return name
|
||||
|
||||
def set_category_label(self, label):
|
||||
if label == '__inherit__':
|
||||
self._category_label = None
|
||||
return
|
||||
|
||||
current_label = self._category_label
|
||||
if current_label or label and current_label != label:
|
||||
uri = urlsplit(self.get_uri())
|
||||
params = dict(parse_qsl(uri.query))
|
||||
if label:
|
||||
params['category_label'] = label
|
||||
else:
|
||||
del params['category_label']
|
||||
self.set_uri(uri._replace(query=urlencode(params)).geturl())
|
||||
|
||||
self._category_label = label
|
||||
|
||||
def get_category_label(self):
|
||||
return self._category_label
|
||||
|
||||
def set_plot(self, plot):
|
||||
try:
|
||||
plot = unescape(plot)
|
||||
except Exception:
|
||||
pass
|
||||
self._plot = plot
|
||||
|
||||
def get_plot(self):
|
||||
return self._plot
|
||||
|
||||
def is_action(self):
|
||||
return self._is_action
|
||||
|
||||
def set_action(self, value):
|
||||
if isinstance(value, bool):
|
||||
self._is_action = value
|
||||
|
||||
@property
|
||||
def subscription_id(self):
|
||||
return self._subscription_id
|
||||
|
||||
@subscription_id.setter
|
||||
def subscription_id(self, value):
|
||||
self._subscription_id = value
|
||||
|
||||
@property
|
||||
def channel_id(self):
|
||||
return self._channel_id
|
||||
|
||||
@channel_id.setter
|
||||
def channel_id(self, value):
|
||||
self._channel_id = value
|
||||
|
||||
@property
|
||||
def playlist_id(self):
|
||||
return self._playlist_id
|
||||
|
||||
@playlist_id.setter
|
||||
def playlist_id(self, value):
|
||||
self._playlist_id = value
|
||||
|
||||
@property
|
||||
def next_page(self):
|
||||
return self._next_page
|
||||
|
||||
@next_page.setter
|
||||
def next_page(self, value):
|
||||
self._next_page = value
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- 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
|
||||
|
||||
from .base_item import BaseItem
|
||||
|
||||
|
||||
class ImageItem(BaseItem):
|
||||
def __init__(self, name, uri, image='DefaultPicture.png', fanart=None):
|
||||
super(ImageItem, self).__init__(name, uri, image, fanart)
|
||||
@@ -0,0 +1,483 @@
|
||||
# -*- 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
|
||||
|
||||
from datetime import date
|
||||
from re import compile as re_compile
|
||||
|
||||
from . import BaseItem
|
||||
from ..compatibility import datetime_infolabel, to_str, unescape, urlencode
|
||||
from ..constants import CHANNEL_ID, CONTENT, PLAYLIST_ID, VIDEO_ID
|
||||
from ..utils.convert_format import duration_to_seconds, seconds_to_duration
|
||||
|
||||
|
||||
class MediaItem(BaseItem):
|
||||
_ALLOWABLE_MEDIATYPES = frozenset()
|
||||
_DEFAULT_MEDIATYPE = ''
|
||||
|
||||
_playable = True
|
||||
|
||||
def __init__(self,
|
||||
name,
|
||||
uri,
|
||||
image='DefaultFile.png',
|
||||
fanart=None,
|
||||
plot=None,
|
||||
video_id=None,
|
||||
channel_id=None,
|
||||
playlist_id=None,
|
||||
playlist_item_id=None,
|
||||
subscription_id=None,
|
||||
**kwargs):
|
||||
super(MediaItem, self).__init__(
|
||||
name=name,
|
||||
uri=uri,
|
||||
image=image,
|
||||
fanart=fanart,
|
||||
**kwargs
|
||||
)
|
||||
self._aired = None
|
||||
self._premiered = None
|
||||
self._scheduled_start_utc = None
|
||||
self._year = None
|
||||
self._season = None
|
||||
self._episode = None
|
||||
|
||||
self._genres = None
|
||||
|
||||
self._duration = -1
|
||||
self._play_count = None
|
||||
self._last_played = None
|
||||
self._start_percent = None
|
||||
self._start_time = None
|
||||
|
||||
self._mediatype = None
|
||||
self._plot = plot
|
||||
self._rating = None
|
||||
|
||||
self._headers = None
|
||||
self._license_key = None
|
||||
self._uses_isa = None
|
||||
self.subtitles = None
|
||||
|
||||
self._completed = False
|
||||
self._live = False
|
||||
self._short = False
|
||||
self._upcoming = False
|
||||
self._vod = False
|
||||
|
||||
self._video_id = video_id
|
||||
self._channel_id = channel_id
|
||||
self._subscription_id = subscription_id
|
||||
self._playlist_id = playlist_id
|
||||
self._playlist_item_id = playlist_item_id
|
||||
|
||||
def __str_parts__(self, as_dict=False):
|
||||
kwargs = {
|
||||
'type': self.__class__.__name__,
|
||||
'name': self._name,
|
||||
'uri': self._uri,
|
||||
VIDEO_ID: self._video_id,
|
||||
CHANNEL_ID: self._channel_id,
|
||||
PLAYLIST_ID: self._playlist_id,
|
||||
# PLAYLIST_ITEM_ID: self._playlist_item_id,
|
||||
# SUBSCRIPTION_ID: self._subscription_id,
|
||||
'available': self._available,
|
||||
'vod': self._vod,
|
||||
'live': self._live,
|
||||
'completed': self._completed,
|
||||
'upcoming': self._upcoming,
|
||||
'short': self._short,
|
||||
'duration': self._duration,
|
||||
'play_count': self._play_count,
|
||||
'added': self._added_utc,
|
||||
'track_number': self._track_number,
|
||||
'filtered': self._filter_reason,
|
||||
}
|
||||
if as_dict:
|
||||
return kwargs
|
||||
out = (
|
||||
'{type}(',
|
||||
'name={name!r}, ',
|
||||
'uri={uri!r}, ',
|
||||
'video_id={video_id!r}, ',
|
||||
'channel_id={channel_id!r}, ',
|
||||
'playlist_id={playlist_id!r}, ',
|
||||
# 'playlist_item_id={playlist_item_id!r}, ',
|
||||
# 'subscription_id={subscription_id!r}, ',
|
||||
'available={available!r}, ',
|
||||
'vod={vod!r}, ',
|
||||
'live={live!r}, ',
|
||||
'completed={completed!r}, ',
|
||||
'upcoming={upcoming!r}, ',
|
||||
'short={short!r}, ',
|
||||
'duration={duration!r}, ',
|
||||
'play_count={play_count!r}, ',
|
||||
'added=\'{added!s}\', ',
|
||||
'track_number={track_number!r}, ',
|
||||
'filtered={filtered!r})',
|
||||
)
|
||||
return out, kwargs
|
||||
|
||||
def set_aired(self, year, month, day):
|
||||
self._aired = date(year, month, day)
|
||||
|
||||
def set_aired_from_datetime(self, date_time):
|
||||
self._aired = date_time.date()
|
||||
|
||||
def get_aired(self, as_text=True, as_info_label=False):
|
||||
if self._aired:
|
||||
if as_info_label:
|
||||
return self._aired.isoformat()
|
||||
if as_text:
|
||||
return self._aired.strftime('%x')
|
||||
return self._aired
|
||||
|
||||
def set_premiered(self, year, month, day):
|
||||
self._premiered = date(year, month, day)
|
||||
|
||||
def set_premiered_from_datetime(self, date_time):
|
||||
self._premiered = date_time.date()
|
||||
|
||||
def get_premiered(self, as_text=True, as_info_label=False):
|
||||
if self._premiered:
|
||||
if as_info_label:
|
||||
return self._premiered.isoformat()
|
||||
if as_text:
|
||||
return self._premiered.strftime('%x')
|
||||
return self._premiered
|
||||
|
||||
def set_scheduled_start_utc(self, date_time):
|
||||
self._scheduled_start_utc = date_time
|
||||
|
||||
def get_scheduled_start_utc(self):
|
||||
return self._scheduled_start_utc
|
||||
|
||||
def set_year(self, year):
|
||||
self._year = int(year)
|
||||
|
||||
def set_year_from_datetime(self, date_time):
|
||||
self.set_year(date_time.year)
|
||||
|
||||
def get_year(self):
|
||||
return self._year
|
||||
|
||||
def add_genre(self, genre):
|
||||
if genre:
|
||||
if self._genres is None:
|
||||
self._genres = []
|
||||
self._genres.append(to_str(genre))
|
||||
|
||||
def get_genres(self):
|
||||
return self._genres
|
||||
|
||||
def set_genres(self, genres):
|
||||
self._genres = list(genres)
|
||||
|
||||
def set_duration(self, hours=0, minutes=0, seconds=0, duration=''):
|
||||
if duration:
|
||||
_seconds = duration_to_seconds(duration)
|
||||
else:
|
||||
_seconds = seconds + minutes * 60 + hours * 3600
|
||||
self._duration = _seconds or 0
|
||||
|
||||
def set_duration_from_minutes(self, minutes):
|
||||
self._duration = int(minutes) * 60
|
||||
|
||||
def set_duration_from_seconds(self, seconds):
|
||||
self._duration = int(seconds or 0)
|
||||
|
||||
def get_duration(self, as_text=False):
|
||||
if as_text:
|
||||
return seconds_to_duration(self._duration)
|
||||
return self._duration
|
||||
|
||||
def set_play_count(self, play_count):
|
||||
self._play_count = int(play_count or 0)
|
||||
|
||||
def get_play_count(self):
|
||||
return self._play_count
|
||||
|
||||
def set_last_played(self, last_played):
|
||||
self._last_played = last_played
|
||||
|
||||
def get_last_played(self, as_info_label=False):
|
||||
if self._last_played:
|
||||
if as_info_label:
|
||||
return datetime_infolabel(self._last_played)
|
||||
return self._last_played
|
||||
|
||||
def set_start_percent(self, start_percent):
|
||||
self._start_percent = start_percent or 0
|
||||
|
||||
def get_start_percent(self):
|
||||
return self._start_percent
|
||||
|
||||
def set_start_time(self, start_time):
|
||||
self._start_time = start_time or 0.0
|
||||
|
||||
def get_start_time(self):
|
||||
return self._start_time
|
||||
|
||||
def set_mediatype(self, mediatype):
|
||||
if mediatype in self._ALLOWABLE_MEDIATYPES:
|
||||
self._mediatype = mediatype
|
||||
else:
|
||||
self._mediatype = self._DEFAULT_MEDIATYPE
|
||||
|
||||
def get_mediatype(self):
|
||||
return self._mediatype or self._DEFAULT_MEDIATYPE
|
||||
|
||||
def set_plot(self, plot):
|
||||
try:
|
||||
plot = unescape(plot)
|
||||
except Exception:
|
||||
pass
|
||||
self._plot = plot
|
||||
|
||||
def get_plot(self):
|
||||
return self._plot
|
||||
|
||||
def set_rating(self, rating):
|
||||
rating = float(rating)
|
||||
if rating > 10:
|
||||
rating = 10.0
|
||||
elif rating < 0:
|
||||
rating = 0.0
|
||||
self._rating = rating
|
||||
|
||||
def get_rating(self):
|
||||
return self._rating
|
||||
|
||||
def set_headers(self, value):
|
||||
self._headers = value
|
||||
|
||||
def get_headers(self, as_string=False):
|
||||
if as_string:
|
||||
return urlencode(self._headers) if self._headers else ''
|
||||
return self._headers
|
||||
|
||||
def set_license_key(self, url):
|
||||
self._license_key = url
|
||||
|
||||
def get_license_key(self):
|
||||
return self._license_key
|
||||
|
||||
def set_isa(self, value=True):
|
||||
self._uses_isa = value
|
||||
|
||||
def use_isa(self):
|
||||
return self._uses_isa
|
||||
|
||||
def use_hls(self):
|
||||
uri = self.get_uri()
|
||||
if 'manifest/hls' in uri or uri.endswith('.m3u8'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def use_mpd(self):
|
||||
uri = self.get_uri()
|
||||
if 'manifest/dash' in uri or uri.endswith('.mpd'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_subtitles(self, value):
|
||||
if value and isinstance(value, (list, tuple)):
|
||||
self.subtitles = value
|
||||
|
||||
@property
|
||||
def completed(self):
|
||||
return self._completed
|
||||
|
||||
@completed.setter
|
||||
def completed(self, value):
|
||||
self._completed = value
|
||||
|
||||
@property
|
||||
def live(self):
|
||||
return self._live
|
||||
|
||||
@live.setter
|
||||
def live(self, value):
|
||||
self._live = value
|
||||
|
||||
@property
|
||||
def short(self):
|
||||
return self._short
|
||||
|
||||
@short.setter
|
||||
def short(self, value):
|
||||
self._short = value
|
||||
|
||||
@property
|
||||
def upcoming(self):
|
||||
return self._upcoming
|
||||
|
||||
@upcoming.setter
|
||||
def upcoming(self, value):
|
||||
self._upcoming = value
|
||||
|
||||
@property
|
||||
def vod(self):
|
||||
return self._vod
|
||||
|
||||
@vod.setter
|
||||
def vod(self, value):
|
||||
self._vod = value
|
||||
|
||||
@property
|
||||
def video_id(self):
|
||||
return self._video_id
|
||||
|
||||
@video_id.setter
|
||||
def video_id(self, value):
|
||||
self._video_id = value
|
||||
|
||||
@property
|
||||
def channel_id(self):
|
||||
return self._channel_id
|
||||
|
||||
@channel_id.setter
|
||||
def channel_id(self, value):
|
||||
self._channel_id = value
|
||||
|
||||
@property
|
||||
def subscription_id(self):
|
||||
return self._subscription_id
|
||||
|
||||
@subscription_id.setter
|
||||
def subscription_id(self, value):
|
||||
self._subscription_id = value
|
||||
|
||||
@property
|
||||
def playlist_id(self):
|
||||
return self._playlist_id
|
||||
|
||||
@playlist_id.setter
|
||||
def playlist_id(self, value):
|
||||
self._playlist_id = value
|
||||
|
||||
@property
|
||||
def playlist_item_id(self):
|
||||
return self._playlist_item_id
|
||||
|
||||
@playlist_item_id.setter
|
||||
def playlist_item_id(self, value):
|
||||
self._playlist_item_id = value
|
||||
|
||||
def set_episode(self, episode):
|
||||
self._episode = int(episode)
|
||||
|
||||
def get_episode(self):
|
||||
return self._episode
|
||||
|
||||
def set_season(self, season):
|
||||
self._season = int(season)
|
||||
|
||||
def get_season(self):
|
||||
return self._season
|
||||
|
||||
|
||||
class AudioItem(MediaItem):
|
||||
_ALLOWABLE_MEDIATYPES = {CONTENT.AUDIO_TYPE, 'song', 'album', 'artist'}
|
||||
_DEFAULT_MEDIATYPE = CONTENT.AUDIO_TYPE
|
||||
|
||||
def __init__(self,
|
||||
name,
|
||||
uri,
|
||||
image='DefaultAudio.png',
|
||||
fanart=None,
|
||||
plot=None,
|
||||
video_id=None,
|
||||
channel_id=None,
|
||||
playlist_id=None,
|
||||
playlist_item_id=None,
|
||||
subscription_id=None):
|
||||
super(AudioItem, self).__init__(
|
||||
name=name,
|
||||
uri=uri,
|
||||
image=image,
|
||||
fanart=fanart,
|
||||
plot=plot,
|
||||
video_id=video_id,
|
||||
channel_id=channel_id,
|
||||
playlist_id=playlist_id,
|
||||
playlist_item_id=playlist_item_id,
|
||||
subscription_id=subscription_id,
|
||||
)
|
||||
self._album = None
|
||||
|
||||
def set_album_name(self, album_name):
|
||||
self._album = album_name or ''
|
||||
|
||||
def get_album_name(self):
|
||||
return self._album
|
||||
|
||||
|
||||
class VideoItem(MediaItem):
|
||||
_ALLOWABLE_MEDIATYPES = {CONTENT.VIDEO_TYPE,
|
||||
'movie',
|
||||
'tvshow', 'season', 'episode',
|
||||
'musicvideo'}
|
||||
_DEFAULT_MEDIATYPE = CONTENT.VIDEO_TYPE
|
||||
_RE_IMDB = re_compile(
|
||||
r'(http(s)?://)?www.imdb.(com|de)/title/(?P<imdbid>[t0-9]+)(/)?'
|
||||
)
|
||||
|
||||
def __init__(self,
|
||||
name,
|
||||
uri,
|
||||
image='DefaultVideo.png',
|
||||
fanart=None,
|
||||
plot=None,
|
||||
video_id=None,
|
||||
channel_id=None,
|
||||
playlist_id=None,
|
||||
playlist_item_id=None,
|
||||
subscription_id=None,
|
||||
**kwargs):
|
||||
super(VideoItem, self).__init__(
|
||||
name=name,
|
||||
uri=uri,
|
||||
image=image,
|
||||
fanart=fanart,
|
||||
plot=plot,
|
||||
video_id=video_id,
|
||||
channel_id=channel_id,
|
||||
playlist_id=playlist_id,
|
||||
playlist_item_id=playlist_item_id,
|
||||
subscription_id=subscription_id,
|
||||
**kwargs
|
||||
)
|
||||
self._directors = None
|
||||
self._imdb_id = None
|
||||
|
||||
def add_directors(self, director):
|
||||
if director:
|
||||
if self._directors is None:
|
||||
self._directors = []
|
||||
self._directors.append(to_str(director))
|
||||
|
||||
def get_directors(self):
|
||||
return self._directors
|
||||
|
||||
def set_directors(self, directors):
|
||||
self._directors = list(directors)
|
||||
|
||||
def set_imdb_id(self, url_or_id):
|
||||
re_match = self._RE_IMDB.match(url_or_id)
|
||||
if re_match:
|
||||
self._imdb_id = re_match.group('imdbid')
|
||||
else:
|
||||
self._imdb_id = url_or_id
|
||||
|
||||
def get_imdb_id(self):
|
||||
return self._imdb_id
|
||||
@@ -0,0 +1,903 @@
|
||||
# -*- 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
|
||||
|
||||
from ..constants import (
|
||||
ARTIST,
|
||||
BOOKMARK_ID,
|
||||
CHANNEL_ID,
|
||||
CONTEXT_MENU,
|
||||
INCOGNITO,
|
||||
MARK_AS_LABEL,
|
||||
ORDER,
|
||||
PATHS,
|
||||
PLAYLIST_ITEM_ID,
|
||||
PLAYLIST_ID,
|
||||
PLAY_FORCE_AUDIO,
|
||||
PLAY_PROMPT_QUALITY,
|
||||
PLAY_PROMPT_SUBTITLES,
|
||||
PLAY_TIMESHIFT,
|
||||
PLAY_USING,
|
||||
PROPERTY_AS_LABEL,
|
||||
SUBSCRIPTION_ID,
|
||||
TITLE,
|
||||
URI,
|
||||
VIDEO_ID,
|
||||
WINDOW_RETURN,
|
||||
)
|
||||
|
||||
|
||||
ARTIST_INFOLABEL = PROPERTY_AS_LABEL % ARTIST
|
||||
BOOKMARK_ID_INFOLABEL = PROPERTY_AS_LABEL % BOOKMARK_ID
|
||||
CHANNEL_ID_INFOLABEL = PROPERTY_AS_LABEL % CHANNEL_ID
|
||||
PLAYLIST_ID_INFOLABEL = PROPERTY_AS_LABEL % PLAYLIST_ID
|
||||
PLAYLIST_ITEM_ID_INFOLABEL = PROPERTY_AS_LABEL % PLAYLIST_ITEM_ID
|
||||
SUBSCRIPTION_ID_INFOLABEL = PROPERTY_AS_LABEL % SUBSCRIPTION_ID
|
||||
TITLE_INFOLABEL = PROPERTY_AS_LABEL % TITLE
|
||||
URI_INFOLABEL = PROPERTY_AS_LABEL % URI
|
||||
VIDEO_ID_INFOLABEL = PROPERTY_AS_LABEL % VIDEO_ID
|
||||
|
||||
|
||||
def context_menu_uri(context, path, params=None):
|
||||
if params is None:
|
||||
params = {CONTEXT_MENU: True}
|
||||
else:
|
||||
params[CONTEXT_MENU] = True
|
||||
return context.create_uri(path, params, run=True)
|
||||
|
||||
|
||||
def video_more_for(context,
|
||||
video_id=VIDEO_ID_INFOLABEL,
|
||||
video_name=TITLE_INFOLABEL,
|
||||
logged_in=False,
|
||||
refresh=False):
|
||||
params = {
|
||||
VIDEO_ID: video_id,
|
||||
'item_name': video_name,
|
||||
'logged_in': logged_in,
|
||||
}
|
||||
_refresh = context.refresh_requested(force=True, on=refresh)
|
||||
if _refresh:
|
||||
params['refresh'] = _refresh
|
||||
return (
|
||||
context.localize('video.more'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
('video', 'more',),
|
||||
params,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def video_related(context,
|
||||
video_id=VIDEO_ID_INFOLABEL,
|
||||
video_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('video.related'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, PATHS.RELATED_VIDEOS,),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
'item_name': video_name,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def video_comments(context,
|
||||
video_id=VIDEO_ID_INFOLABEL,
|
||||
video_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('video.comments'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, PATHS.VIDEO_COMMENTS),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
'item_name': video_name,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def video_description_links(context,
|
||||
video_id=VIDEO_ID_INFOLABEL,
|
||||
video_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('video.description_links'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, PATHS.DESCRIPTION_LINKS),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
'item_name': video_name,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def media_play_using(context, video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('video.play.using'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAY,),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
PLAY_USING: True,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def refresh_listing(context, path=None, params=None):
|
||||
if path is None:
|
||||
path = (PATHS.ROUTE, context.get_path(),)
|
||||
elif isinstance(path, tuple):
|
||||
path = (PATHS.ROUTE,) + path
|
||||
else:
|
||||
path = (PATHS.ROUTE, path,)
|
||||
if params is None:
|
||||
params = context.get_params()
|
||||
return (
|
||||
context.localize('refresh'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
path,
|
||||
dict(params,
|
||||
refresh=context.refresh_requested(
|
||||
force=True,
|
||||
on=True,
|
||||
params=params,
|
||||
)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def folder_play(context, path, order='normal'):
|
||||
return (
|
||||
context.localize('playlist.play.shuffle')
|
||||
if order == 'shuffle' else
|
||||
context.localize('playlist.play.all'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(path, 'play',),
|
||||
{
|
||||
'order': order,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def media_play(context):
|
||||
return (
|
||||
context.localize('video.play'),
|
||||
'Action(Play)'
|
||||
)
|
||||
|
||||
|
||||
def media_queue(context):
|
||||
return (
|
||||
context.localize('video.queue'),
|
||||
'Action(Queue)'
|
||||
)
|
||||
|
||||
|
||||
def playlist_play(context, playlist_id=PLAYLIST_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('playlist.play.all'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAY,),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
'order': 'ask',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def playlist_play_from(context,
|
||||
playlist_id=PLAYLIST_ID_INFOLABEL,
|
||||
video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('playlist.play.from_here'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAY,),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
VIDEO_ID: video_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def playlist_play_recently_added(context, playlist_id=PLAYLIST_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('playlist.play.recently_added'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAY,),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
'recent_days': 1,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def playlist_view(context, playlist_id=PLAYLIST_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('playlist.view.all'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, PATHS.PLAY,),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
'order': 'normal',
|
||||
'action': 'list',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def playlist_shuffle(context, playlist_id=PLAYLIST_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('playlist.play.shuffle'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAY,),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
'order': 'shuffle',
|
||||
'action': 'play',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def playlist_add_to(context,
|
||||
playlist_id,
|
||||
name_id='playlist',
|
||||
video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize(('add.to.x', name_id)),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAYLIST, 'add', 'video',),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
VIDEO_ID: video_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def playlist_add_to_selected(context, video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('video.add_to_playlist'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAYLIST, 'select', 'playlist',),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def playlist_remove_from(context,
|
||||
playlist_id=PLAYLIST_ID_INFOLABEL,
|
||||
playlist_item_id=PLAYLIST_ITEM_ID_INFOLABEL,
|
||||
video_id=VIDEO_ID_INFOLABEL,
|
||||
video_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('remove'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAYLIST, 'remove', 'video',),
|
||||
dict(
|
||||
context.get_params(),
|
||||
**{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
PLAYLIST_ITEM_ID: playlist_item_id,
|
||||
VIDEO_ID: video_id,
|
||||
'item_name': video_name,
|
||||
'reload_path': context.get_path(),
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def playlist_rename(context,
|
||||
playlist_id=PLAYLIST_ID_INFOLABEL,
|
||||
playlist_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('rename'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAYLIST, 'rename', 'playlist',),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
'item_name': playlist_name
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def playlist_delete(context,
|
||||
playlist_id=PLAYLIST_ID_INFOLABEL,
|
||||
playlist_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('delete'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAYLIST, 'remove', 'playlist',),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
'item_name': playlist_name
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def playlist_save_to_library(context, playlist_id=PLAYLIST_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('save'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAYLIST, 'like', 'playlist',),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def playlist_remove_from_library(context,
|
||||
playlist_id=PLAYLIST_ID_INFOLABEL,
|
||||
playlist_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('remove'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAYLIST, 'unlike', 'playlist',),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
'item_name': playlist_name,
|
||||
'reload_path': context.get_path(),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def watch_later_list_unassign(context,
|
||||
playlist_id=PLAYLIST_ID_INFOLABEL,
|
||||
playlist_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('watch_later.list.unassign'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAYLIST, 'unassign', 'watch_later',),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
'item_name': playlist_name
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def watch_later_list_assign(context,
|
||||
playlist_id=PLAYLIST_ID_INFOLABEL,
|
||||
playlist_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('watch_later.list.assign'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAYLIST, 'assign', 'watch_later',),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
'item_name': playlist_name
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def history_list_unassign(context,
|
||||
playlist_id=PLAYLIST_ID_INFOLABEL,
|
||||
playlist_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('history.list.unassign'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAYLIST, 'unassign', 'history',),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
'item_name': playlist_name
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def history_list_assign(context,
|
||||
playlist_id=PLAYLIST_ID_INFOLABEL,
|
||||
playlist_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('history.list.assign'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAYLIST, 'assign', 'history',),
|
||||
{
|
||||
PLAYLIST_ID: playlist_id,
|
||||
'item_name': playlist_name
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def my_subscriptions_filter_remove(context, channel_name=ARTIST_INFOLABEL):
|
||||
return (
|
||||
context.localize(('remove.from.x', 'my_subscriptions.filtered')),
|
||||
context_menu_uri(
|
||||
context,
|
||||
('my_subscriptions', 'filter', 'remove'),
|
||||
{
|
||||
'item_name': channel_name,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def my_subscriptions_filter_add(context, channel_name=ARTIST_INFOLABEL):
|
||||
return (
|
||||
context.localize(('add.to.x', 'my_subscriptions.filtered')),
|
||||
context_menu_uri(
|
||||
context,
|
||||
('my_subscriptions', 'filter', 'add',),
|
||||
{
|
||||
'item_name': channel_name,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def video_rate(context, video_id=VIDEO_ID_INFOLABEL, refresh=False):
|
||||
params = {
|
||||
VIDEO_ID: video_id,
|
||||
}
|
||||
_refresh = context.refresh_requested(force=True, on=refresh)
|
||||
if _refresh:
|
||||
params['refresh'] = _refresh
|
||||
return (
|
||||
context.localize('video.rate'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
('video', 'rate',),
|
||||
params,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def watch_later_local_add(context, item):
|
||||
return (
|
||||
context.localize('watch_later.add'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.WATCH_LATER, 'add',),
|
||||
{
|
||||
VIDEO_ID: item.video_id,
|
||||
'item': repr(item),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def watch_later_local_remove(context,
|
||||
video_id=VIDEO_ID_INFOLABEL,
|
||||
video_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('watch_later.remove'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.WATCH_LATER, 'remove',),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
'item_name': video_name,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def watch_later_local_clear(context):
|
||||
return (
|
||||
context.localize('watch_later.clear'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.WATCH_LATER, 'clear',),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def channel_go_to(context,
|
||||
channel_id=CHANNEL_ID_INFOLABEL,
|
||||
channel_name=ARTIST_INFOLABEL):
|
||||
return (
|
||||
context.localize('go_to.x', context.get_ui().bold(channel_name)),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, PATHS.CHANNEL, channel_id,),
|
||||
{
|
||||
'category_label': channel_name,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def channel_subscribe_to(context,
|
||||
channel_id=CHANNEL_ID_INFOLABEL,
|
||||
channel_name=ARTIST_INFOLABEL):
|
||||
return (
|
||||
context.localize('subscribe_to.x', context.get_ui().bold(channel_name))
|
||||
if channel_name else
|
||||
context.localize('subscribe'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
('subscriptions', 'add',),
|
||||
{
|
||||
SUBSCRIPTION_ID: channel_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def channel_unsubscribe_from(context, channel_id=None, subscription_id=None):
|
||||
return (
|
||||
context.localize('unsubscribe'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
('subscriptions', 'remove',),
|
||||
{
|
||||
SUBSCRIPTION_ID: subscription_id,
|
||||
},
|
||||
) if subscription_id else
|
||||
context_menu_uri(
|
||||
context,
|
||||
('subscriptions', 'remove',),
|
||||
{
|
||||
CHANNEL_ID: channel_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def media_play_with_subtitles(context,
|
||||
video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('video.play.with_subtitles'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAY,),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
PLAY_PROMPT_SUBTITLES: True,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def media_play_audio_only(context,
|
||||
video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('video.play.audio_only'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAY,),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
PLAY_FORCE_AUDIO: True,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def media_play_ask_for_quality(context,
|
||||
video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('video.play.ask_for_quality'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAY,),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
PLAY_PROMPT_QUALITY: True,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def media_play_timeshift(context,
|
||||
video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('video.play.timeshift'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAY,),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
PLAY_TIMESHIFT: True,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def history_local_remove(context,
|
||||
video_id=VIDEO_ID_INFOLABEL,
|
||||
video_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('history.remove'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.HISTORY, 'remove',),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
'item_name': video_name,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def history_local_clear(context):
|
||||
return (
|
||||
context.localize('history.clear'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.HISTORY, 'clear',),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def history_local_mark_as(context, video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
PROPERTY_AS_LABEL % MARK_AS_LABEL,
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.HISTORY, 'mark_as',),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def history_local_mark_watched(context, video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('history.mark.watched'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.HISTORY, 'mark_watched',),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def history_local_mark_unwatched(context, video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('history.mark.unwatched'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.HISTORY, 'mark_unwatched',),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def history_local_reset_resume(context, video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('history.reset.resume_point'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.HISTORY, 'reset_resume',),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def bookmark_add(context, item):
|
||||
return (
|
||||
context.localize('bookmark'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.BOOKMARKS, 'add',),
|
||||
{
|
||||
'item_id': item.get_id(),
|
||||
'item': repr(item),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def bookmark_add_channel(context,
|
||||
channel_id=CHANNEL_ID_INFOLABEL,
|
||||
channel_name=ARTIST_INFOLABEL):
|
||||
return (
|
||||
context.localize('bookmark.x',
|
||||
context.get_ui().bold(channel_name)
|
||||
if channel_name else
|
||||
context.localize('channel')),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.BOOKMARKS, 'add',),
|
||||
{
|
||||
'item_id': channel_id,
|
||||
'item': None,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def bookmark_edit(context,
|
||||
item_id=BOOKMARK_ID_INFOLABEL,
|
||||
item_name=TITLE_INFOLABEL,
|
||||
item_uri=URI_INFOLABEL):
|
||||
return (
|
||||
context.localize(('edit.x', 'bookmark')),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.BOOKMARKS, 'edit',),
|
||||
{
|
||||
'item_id': item_id,
|
||||
'item_name': item_name,
|
||||
'uri': item_uri,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def bookmark_remove(context,
|
||||
item_id=BOOKMARK_ID_INFOLABEL,
|
||||
item_name=TITLE_INFOLABEL):
|
||||
return (
|
||||
context.localize('bookmark.remove'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.BOOKMARKS, 'remove',),
|
||||
{
|
||||
'item_id': item_id,
|
||||
'item_name': item_name,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def bookmarks_clear(context):
|
||||
return (
|
||||
context.localize('bookmarks.clear'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.BOOKMARKS, 'clear',),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def search_remove(context, query):
|
||||
return (
|
||||
context.localize('search.remove'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.SEARCH, 'remove',),
|
||||
{
|
||||
'q': query,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def search_rename(context, query):
|
||||
return (
|
||||
context.localize('search.rename'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.SEARCH, 'rename',),
|
||||
{
|
||||
'q': query,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def search_clear(context):
|
||||
return (
|
||||
context.localize('search.clear'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.SEARCH, 'clear',),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def search_sort_by(context, params, order):
|
||||
selected = params.get(ORDER, 'relevance') == order
|
||||
order_label = context.localize('search.sort.' + order)
|
||||
return (
|
||||
context.localize('search.sort').format(
|
||||
context.get_ui().bold(order_label) if selected else order_label
|
||||
),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, context.get_path(),),
|
||||
params=dict(params,
|
||||
order=order,
|
||||
page=1,
|
||||
page_token='',
|
||||
pageToken='',
|
||||
window_replace=True,
|
||||
window_return=False),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def separator():
|
||||
return (
|
||||
'--------',
|
||||
'noop'
|
||||
)
|
||||
|
||||
|
||||
def goto_home(context):
|
||||
return (
|
||||
context.localize('home'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, PATHS.HOME,),
|
||||
{
|
||||
WINDOW_RETURN: False,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def goto_quick_search(context, params=None, incognito=None):
|
||||
if params is None:
|
||||
params = {}
|
||||
if incognito is None:
|
||||
incognito = params.get(INCOGNITO)
|
||||
else:
|
||||
params[INCOGNITO] = incognito
|
||||
return (
|
||||
context.localize('search.quick.incognito'
|
||||
if incognito else
|
||||
'search.quick'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, PATHS.SEARCH, 'input',),
|
||||
params,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def goto_page(context, params=None):
|
||||
return (
|
||||
context.localize('page.choose'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.GOTO_PAGE, context.get_path(),),
|
||||
params or context.get_params(),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,112 @@
|
||||
# -*- 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
|
||||
|
||||
from . import menu_items
|
||||
from .directory_item import DirectoryItem
|
||||
from ..constants import ITEMS_PER_PAGE, PAGE, PATHS
|
||||
|
||||
|
||||
class NextPageItem(DirectoryItem):
|
||||
NEXT_PAGE_PARAM_EXCLUSIONS = (
|
||||
'refresh',
|
||||
)
|
||||
JUMP_PAGE_PARAM_EXCLUSIONS = (
|
||||
'click_tracking',
|
||||
'exclude',
|
||||
'filtered',
|
||||
'page',
|
||||
'refresh',
|
||||
'visitor',
|
||||
)
|
||||
|
||||
def __init__(self, context, params, image=None, fanart=None):
|
||||
path = context.get_path()
|
||||
|
||||
page = params.get(PAGE) or 2
|
||||
is_first_page_link = page < 2
|
||||
|
||||
items_per_page = params.get(ITEMS_PER_PAGE) or 50
|
||||
can_jump = ('next_page_token' not in params
|
||||
and not path.startswith(('/channel',
|
||||
PATHS.RECOMMENDATIONS,
|
||||
PATHS.RELATED_VIDEOS,
|
||||
PATHS.VIRTUAL_PLAYLIST)))
|
||||
if can_jump and not is_first_page_link and 'page_token' not in params:
|
||||
params['page_token'] = self.create_page_token(page, items_per_page)
|
||||
|
||||
can_search = not path.startswith(PATHS.SEARCH)
|
||||
|
||||
for param in (
|
||||
self.JUMP_PAGE_PARAM_EXCLUSIONS
|
||||
if is_first_page_link else
|
||||
self.NEXT_PAGE_PARAM_EXCLUSIONS
|
||||
):
|
||||
if param in params:
|
||||
del params[param]
|
||||
|
||||
name = context.localize('page.next', page)
|
||||
filtered = params.get('filtered')
|
||||
if filtered:
|
||||
name = ''.join((
|
||||
name,
|
||||
' (',
|
||||
str(filtered),
|
||||
' ',
|
||||
context.localize('filtered'),
|
||||
')',
|
||||
))
|
||||
|
||||
super(NextPageItem, self).__init__(
|
||||
name,
|
||||
context.create_uri(path, params),
|
||||
image=image,
|
||||
fanart=fanart,
|
||||
category_label='__inherit__',
|
||||
)
|
||||
|
||||
self.next_page = page
|
||||
self.items_per_page = items_per_page
|
||||
|
||||
context_menu = [
|
||||
menu_items.refresh_listing(context),
|
||||
menu_items.goto_page(context, params) if can_jump else None,
|
||||
menu_items.goto_home(context),
|
||||
menu_items.goto_quick_search(context) if can_search else None,
|
||||
]
|
||||
self.add_context_menu(context_menu)
|
||||
|
||||
@classmethod
|
||||
def create_page_token(cls, page, items_per_page=50):
|
||||
low = 'AEIMQUYcgkosw048'
|
||||
high = 'ABCDEFGHIJKLMNOP'
|
||||
len_low = len(low)
|
||||
len_high = len(high)
|
||||
|
||||
position = (page - 1) * items_per_page
|
||||
|
||||
overflow_token = 'Q'
|
||||
if position >= 128:
|
||||
overflow_token_iteration = position // 128
|
||||
overflow_token = '%sE' % high[overflow_token_iteration]
|
||||
low_iteration = position % len_low
|
||||
|
||||
# at this position the iteration starts with 'I' again (after 'P')
|
||||
if position >= 256:
|
||||
multiplier = (position // 128) - 1
|
||||
position -= 128 * multiplier
|
||||
high_iteration = (position // len_low) % len_high
|
||||
|
||||
return 'C{high_token}{low_token}{overflow_token}AA'.format(
|
||||
high_token=high[high_iteration],
|
||||
low_token=low[low_iteration],
|
||||
overflow_token=overflow_token
|
||||
)
|
||||
@@ -0,0 +1,135 @@
|
||||
# -*- 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
|
||||
|
||||
from . import menu_items
|
||||
from .directory_item import DirectoryItem
|
||||
from ..constants import CHANNEL_ID, INCOGNITO, PATHS
|
||||
|
||||
|
||||
class SearchItem(DirectoryItem):
|
||||
def __init__(self,
|
||||
context,
|
||||
name=None,
|
||||
image=None,
|
||||
fanart=None,
|
||||
location=False):
|
||||
if not name:
|
||||
name = context.localize('search')
|
||||
|
||||
if image is None:
|
||||
image = '{media}/search.png'
|
||||
|
||||
params = {}
|
||||
if location:
|
||||
params['location'] = location
|
||||
|
||||
super(SearchItem, self).__init__(name,
|
||||
context.create_uri(
|
||||
(PATHS.SEARCH, 'list',),
|
||||
params=params,
|
||||
),
|
||||
image=image,
|
||||
fanart=fanart)
|
||||
|
||||
context_menu = [
|
||||
menu_items.search_clear(context),
|
||||
menu_items.separator(),
|
||||
menu_items.goto_quick_search(context, params),
|
||||
menu_items.goto_quick_search(context, params, incognito=True)
|
||||
]
|
||||
self.add_context_menu(context_menu)
|
||||
|
||||
|
||||
class SearchHistoryItem(DirectoryItem):
|
||||
def __init__(self, context, query, image=None, fanart=None, location=False):
|
||||
if image is None:
|
||||
image = '{media}/search.png'
|
||||
|
||||
if isinstance(query, dict):
|
||||
params = query
|
||||
query = params['q']
|
||||
else:
|
||||
params = {'q': query}
|
||||
if location:
|
||||
params['location'] = location
|
||||
|
||||
super(SearchHistoryItem, self).__init__(query,
|
||||
context.create_uri(
|
||||
(PATHS.SEARCH, 'query',),
|
||||
params=params,
|
||||
),
|
||||
image=image,
|
||||
fanart=fanart)
|
||||
|
||||
context_menu = [
|
||||
menu_items.search_remove(context, query),
|
||||
menu_items.search_rename(context, query),
|
||||
menu_items.search_clear(context),
|
||||
menu_items.separator(),
|
||||
menu_items.search_sort_by(context, params, 'relevance'),
|
||||
menu_items.search_sort_by(context, params, 'date'),
|
||||
menu_items.search_sort_by(context, params, 'viewCount'),
|
||||
menu_items.search_sort_by(context, params, 'rating'),
|
||||
menu_items.search_sort_by(context, params, 'title'),
|
||||
]
|
||||
self.add_context_menu(context_menu)
|
||||
|
||||
|
||||
class NewSearchItem(DirectoryItem):
|
||||
def __init__(self,
|
||||
context,
|
||||
name=None,
|
||||
title=None,
|
||||
image=None,
|
||||
fanart=None,
|
||||
incognito=False,
|
||||
channel_id='',
|
||||
addon_id='',
|
||||
location=False,
|
||||
**_kwargs):
|
||||
if not name:
|
||||
name = context.get_ui().bold(
|
||||
title or context.localize('search.new')
|
||||
)
|
||||
|
||||
if image is None:
|
||||
image = '{media}/new_search.png'
|
||||
|
||||
params = {}
|
||||
if addon_id:
|
||||
params['addon_id'] = addon_id
|
||||
if incognito:
|
||||
params[INCOGNITO] = incognito
|
||||
if channel_id:
|
||||
params[CHANNEL_ID] = channel_id
|
||||
if location:
|
||||
params['location'] = location
|
||||
|
||||
super(NewSearchItem, self).__init__(name,
|
||||
context.create_uri(
|
||||
(PATHS.SEARCH, 'input',),
|
||||
params=params,
|
||||
),
|
||||
image=image,
|
||||
fanart=fanart)
|
||||
|
||||
if context.is_plugin_path(context.get_uri(), ((PATHS.SEARCH, 'list'),)):
|
||||
context_menu = [
|
||||
menu_items.search_clear(context),
|
||||
menu_items.separator(),
|
||||
menu_items.goto_quick_search(context, params, not incognito)
|
||||
]
|
||||
else:
|
||||
context_menu = [
|
||||
menu_items.goto_quick_search(context, params, not incognito)
|
||||
]
|
||||
self.add_context_menu(context_menu)
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- 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
|
||||
|
||||
from .base_item import BaseItem
|
||||
|
||||
|
||||
class UriItem(BaseItem):
|
||||
def __init__(self, uri, playable=None):
|
||||
super(UriItem, self).__init__(name=uri, uri=uri)
|
||||
if playable is not None:
|
||||
self._playable = playable
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- 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 json
|
||||
from datetime import date, datetime
|
||||
|
||||
from .bookmark_item import BookmarkItem
|
||||
from .directory_item import DirectoryItem
|
||||
from .image_item import ImageItem
|
||||
from .media_item import AudioItem, VideoItem
|
||||
from .. import logging
|
||||
from ..compatibility import string_type, to_str
|
||||
from ..utils.datetime import strptime
|
||||
|
||||
|
||||
_ITEM_TYPES = {
|
||||
'AudioItem': AudioItem,
|
||||
'BookmarkItem': BookmarkItem,
|
||||
'DirectoryItem': DirectoryItem,
|
||||
'ImageItem': ImageItem,
|
||||
'VideoItem': VideoItem,
|
||||
}
|
||||
|
||||
|
||||
def _decoder(obj):
|
||||
date_in_isoformat = obj.get('__isoformat__')
|
||||
if date_in_isoformat:
|
||||
if obj['__class__'] == 'date':
|
||||
return date.fromisoformat(date_in_isoformat)
|
||||
return datetime.fromisoformat(date_in_isoformat)
|
||||
|
||||
format_string = obj.get('__format_string__')
|
||||
if format_string:
|
||||
value = obj['__value__']
|
||||
value = strptime(value, format_string)
|
||||
if obj['__class__'] == 'date':
|
||||
return value.date()
|
||||
return value
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def from_json(json_data, *args):
|
||||
"""
|
||||
Creates an instance of the given json dump or dict.
|
||||
:param json_data:
|
||||
:return:
|
||||
"""
|
||||
if args and args[0] and len(args[0]) == 4:
|
||||
bookmark_id = args[0][0]
|
||||
bookmark_timestamp = args[0][1]
|
||||
else:
|
||||
bookmark_id = None
|
||||
bookmark_timestamp = None
|
||||
|
||||
if isinstance(json_data, string_type):
|
||||
if json_data == to_str(None):
|
||||
# Channel bookmark that will be updated. Store timestamp for update
|
||||
return bookmark_timestamp
|
||||
json_data = json.loads(json_data, object_hook=_decoder)
|
||||
|
||||
item_type = json_data.get('type')
|
||||
if not item_type or item_type not in _ITEM_TYPES:
|
||||
logging.warning_trace(('Unsupported item type', 'Data: {data!r}'),
|
||||
data=json_data)
|
||||
return None
|
||||
|
||||
item_data = json_data.get('data')
|
||||
if not item_data:
|
||||
return None
|
||||
|
||||
item = _ITEM_TYPES[item_type](name='', uri='')
|
||||
item.__dict__.update(item_data)
|
||||
|
||||
if bookmark_id:
|
||||
item.bookmark_id = bookmark_id
|
||||
if bookmark_timestamp:
|
||||
item.set_bookmark_timestamp(bookmark_timestamp)
|
||||
|
||||
return item
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- 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
|
||||
|
||||
from .directory_item import DirectoryItem
|
||||
from ..constants import PATHS
|
||||
|
||||
|
||||
class WatchLaterItem(DirectoryItem):
|
||||
def __init__(self, context, name=None, image=None, fanart=None):
|
||||
if not name:
|
||||
name = context.localize('watch_later')
|
||||
|
||||
if image is None:
|
||||
image = '{media}/watch_later.png'
|
||||
|
||||
super(WatchLaterItem, self).__init__(name,
|
||||
context.create_uri(
|
||||
(PATHS.WATCH_LATER, 'list',),
|
||||
),
|
||||
image=image,
|
||||
fanart=fanart)
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,767 @@
|
||||
# -*- 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
|
||||
|
||||
from json import dumps
|
||||
|
||||
from .. import (
|
||||
AudioItem,
|
||||
DirectoryItem,
|
||||
ImageItem,
|
||||
MediaItem,
|
||||
VideoItem,
|
||||
)
|
||||
from ... import logging
|
||||
from ...compatibility import to_str, xbmc, xbmcgui
|
||||
from ...constants import (
|
||||
ACTION,
|
||||
BOOKMARK_ID,
|
||||
CHANNEL_ID,
|
||||
PATHS,
|
||||
PLAYLIST_ITEM_ID,
|
||||
PLAYLIST_ID,
|
||||
PLAY_COUNT_PROP,
|
||||
PLAY_STRM,
|
||||
PLAY_TIMESHIFT,
|
||||
PLAY_USING,
|
||||
SUBSCRIPTION_ID,
|
||||
VALUE_TO_STR,
|
||||
VIDEO_ID,
|
||||
)
|
||||
from ...utils.datetime import datetime_to_since, utc_to_local
|
||||
from ...utils.redact import redact_ip_in_uri
|
||||
from ...utils.system_version import current_system_version
|
||||
|
||||
|
||||
def set_info(list_item, item, properties, set_play_count=True, resume=True):
|
||||
if not current_system_version.compatible(20):
|
||||
info_labels = {}
|
||||
info_type = None
|
||||
|
||||
if isinstance(item, MediaItem):
|
||||
if isinstance(item, VideoItem):
|
||||
info_type = 'video'
|
||||
|
||||
value = item.get_episode()
|
||||
if value is not None:
|
||||
info_labels['episode'] = value
|
||||
|
||||
value = item.get_season()
|
||||
if value is not None:
|
||||
info_labels['season'] = value
|
||||
|
||||
elif isinstance(item, AudioItem):
|
||||
info_type = 'music'
|
||||
|
||||
value = item.get_album_name()
|
||||
if value is not None:
|
||||
info_labels['album'] = value
|
||||
|
||||
else:
|
||||
return
|
||||
|
||||
value = item.get_aired(as_info_label=True)
|
||||
if value is not None:
|
||||
info_labels['aired'] = value
|
||||
|
||||
value = item.get_premiered(as_info_label=True)
|
||||
if value is not None:
|
||||
info_labels['premiered'] = value
|
||||
|
||||
value = item.get_plot()
|
||||
if value is not None:
|
||||
info_labels['plot'] = value
|
||||
|
||||
value = item.get_last_played(as_info_label=True)
|
||||
if value is not None:
|
||||
info_labels['lastplayed'] = value
|
||||
|
||||
value = item.get_mediatype()
|
||||
if value is not None:
|
||||
info_labels['mediatype'] = value
|
||||
|
||||
value = item.get_play_count()
|
||||
if value is not None:
|
||||
if set_play_count:
|
||||
info_labels['playcount'] = value
|
||||
properties[PLAY_COUNT_PROP] = value
|
||||
|
||||
value = item.get_rating()
|
||||
if value is not None:
|
||||
info_labels['rating'] = value
|
||||
|
||||
value = item.get_name()
|
||||
if value is not None:
|
||||
info_labels['title'] = value
|
||||
|
||||
value = item.get_track_number()
|
||||
if value is not None:
|
||||
info_labels['tracknumber'] = value
|
||||
|
||||
value = item.get_year()
|
||||
if value is not None:
|
||||
info_labels['year'] = value
|
||||
|
||||
resume_time = resume and item.get_start_time()
|
||||
if resume_time is not None:
|
||||
properties['ResumeTime'] = str(resume_time)
|
||||
duration = item.get_duration()
|
||||
if duration > 0:
|
||||
properties['TotalTime'] = str(duration)
|
||||
if info_type == 'video':
|
||||
list_item.addStreamInfo(info_type, {'duration': duration})
|
||||
info_labels['duration'] = duration
|
||||
|
||||
elif isinstance(item, DirectoryItem):
|
||||
info_type = 'video'
|
||||
|
||||
value = item.get_name()
|
||||
if value is not None:
|
||||
info_labels['title'] = value
|
||||
|
||||
value = item.get_plot()
|
||||
if value is not None:
|
||||
info_labels['plot'] = value
|
||||
|
||||
value = item.get_track_number()
|
||||
if value is not None:
|
||||
info_labels['tracknumber'] = value
|
||||
|
||||
elif isinstance(item, ImageItem):
|
||||
info_type = 'picture'
|
||||
|
||||
value = item.get_name()
|
||||
if value is not None:
|
||||
info_labels['title'] = value
|
||||
|
||||
else:
|
||||
return
|
||||
|
||||
value = item.get_production_code()
|
||||
if value is not None:
|
||||
info_labels['code'] = value
|
||||
|
||||
value = item.get_dateadded(as_info_label=True)
|
||||
if value is not None:
|
||||
info_labels['dateadded'] = value
|
||||
|
||||
value = item.get_studios()
|
||||
if value is not None:
|
||||
info_labels['studio'] = value
|
||||
|
||||
value = item.get_cast()
|
||||
if value is not None:
|
||||
info_labels['castandrole'] = [(member['name'], member['role'])
|
||||
for member in value]
|
||||
|
||||
value = item.get_artists()
|
||||
if value is not None:
|
||||
info_labels['artist'] = value
|
||||
|
||||
value = item.get_count()
|
||||
if value is not None:
|
||||
info_labels['count'] = value
|
||||
|
||||
value = item.get_date(as_info_label=True)
|
||||
if value is not None:
|
||||
info_labels['date'] = value
|
||||
|
||||
if properties:
|
||||
list_item.setProperties(properties)
|
||||
|
||||
if info_labels and info_type:
|
||||
list_item.setInfo(info_type, info_labels)
|
||||
return
|
||||
|
||||
if isinstance(item, MediaItem):
|
||||
if isinstance(item, VideoItem):
|
||||
info_tag = list_item.getVideoInfoTag()
|
||||
info_type = 'video'
|
||||
|
||||
# episode: int
|
||||
value = item.get_episode()
|
||||
if value is not None:
|
||||
info_tag.setEpisode(value)
|
||||
|
||||
# season: int
|
||||
value = item.get_season()
|
||||
if value is not None:
|
||||
info_tag.setSeason(value)
|
||||
|
||||
value = item.get_premiered(as_info_label=True)
|
||||
if value is not None:
|
||||
info_tag.setPremiered(value)
|
||||
|
||||
value = item.get_aired(as_info_label=True)
|
||||
if value is not None:
|
||||
info_tag.setFirstAired(value)
|
||||
|
||||
# plot: str
|
||||
value = item.get_plot()
|
||||
if value is not None:
|
||||
info_tag.setPlot(value)
|
||||
|
||||
# tracknumber: int
|
||||
# eg. 12
|
||||
value = item.get_track_number()
|
||||
if value is not None:
|
||||
info_tag.setTrackNumber(value)
|
||||
|
||||
# director: list[str]
|
||||
# eg. "Steven Spielberg"
|
||||
# Currently unused
|
||||
# value = item.get_directors()
|
||||
# if value is not None:
|
||||
# info_tag.setDirectors(value)
|
||||
|
||||
# imdbnumber: str
|
||||
# eg. "tt3458353"
|
||||
# Currently unused
|
||||
# value = item.get_imdb_id()
|
||||
# if value is not None:
|
||||
# info_tag.setIMDBNumber(value)
|
||||
|
||||
elif isinstance(item, AudioItem):
|
||||
info_tag = list_item.getMusicInfoTag()
|
||||
info_type = 'music'
|
||||
|
||||
# album: str
|
||||
# eg. "Buckle Up"
|
||||
value = item.get_album_name()
|
||||
if value is not None:
|
||||
info_tag.setAlbum(value)
|
||||
|
||||
value = item.get_premiered(as_info_label=True)
|
||||
if value is not None:
|
||||
info_tag.setReleaseDate(value)
|
||||
|
||||
# comment: str
|
||||
value = item.get_plot()
|
||||
if value is not None:
|
||||
info_tag.setComment(value)
|
||||
|
||||
# artist: str
|
||||
# eg. "Artist 1, Artist 2"
|
||||
# Used as alias for channel name
|
||||
value = item.get_artists_string()
|
||||
if value is not None:
|
||||
info_tag.setArtist(value)
|
||||
|
||||
# track: int
|
||||
# eg. 12
|
||||
value = item.get_track_number()
|
||||
if value is not None:
|
||||
info_tag.setTrack(value)
|
||||
|
||||
else:
|
||||
return
|
||||
|
||||
value = item.get_last_played(as_info_label=True)
|
||||
if value is not None:
|
||||
info_tag.setLastPlayed(value)
|
||||
|
||||
# mediatype: str
|
||||
value = item.get_mediatype()
|
||||
if value is not None:
|
||||
info_tag.setMediaType(value)
|
||||
|
||||
# playcount: int
|
||||
value = item.get_play_count()
|
||||
if value is not None:
|
||||
if set_play_count:
|
||||
if info_type == 'video':
|
||||
info_tag.setPlaycount(value)
|
||||
elif info_type == 'music':
|
||||
info_tag.setPlayCount(value)
|
||||
properties[PLAY_COUNT_PROP] = value
|
||||
|
||||
# rating: float
|
||||
value = item.get_rating()
|
||||
if value is not None:
|
||||
info_tag.setRating(value)
|
||||
|
||||
# title: str
|
||||
# eg. "Blow Your Head Off"
|
||||
value = item.get_name()
|
||||
if value is not None:
|
||||
info_tag.setTitle(value)
|
||||
|
||||
# year: int
|
||||
# eg. 1994
|
||||
value = item.get_year()
|
||||
if value is not None:
|
||||
info_tag.setYear(value)
|
||||
|
||||
# genre: list[str]
|
||||
# eg. ["Hardcore"]
|
||||
# Currently unused
|
||||
# value = item.get_genres()
|
||||
# if value is not None:
|
||||
# info_tag.setGenres(value)
|
||||
|
||||
resume_time = resume and item.get_start_time()
|
||||
duration = item.get_duration()
|
||||
if info_type == 'video':
|
||||
if resume_time is not None:
|
||||
if duration > 0:
|
||||
info_tag.setResumePoint(resume_time, float(duration))
|
||||
else:
|
||||
info_tag.setResumePoint(resume_time)
|
||||
if duration > 0:
|
||||
info_tag.addVideoStream(xbmc.VideoStreamDetail(
|
||||
duration=duration,
|
||||
))
|
||||
elif info_type == 'music':
|
||||
# These properties are deprecated but there is no other way to set
|
||||
# these details for a ListItem with a MusicInfoTag
|
||||
if resume_time is not None:
|
||||
properties['ResumeTime'] = str(resume_time)
|
||||
if duration > 0:
|
||||
properties['TotalTime'] = str(duration)
|
||||
|
||||
# duration: int
|
||||
# As seconds
|
||||
if duration > 0:
|
||||
info_tag.setDuration(duration)
|
||||
|
||||
elif isinstance(item, DirectoryItem):
|
||||
info_tag = list_item.getVideoInfoTag()
|
||||
info_type = 'video'
|
||||
|
||||
value = item.get_name()
|
||||
if value is not None:
|
||||
info_tag.setTitle(value)
|
||||
|
||||
value = item.get_plot()
|
||||
if value is not None:
|
||||
info_tag.setPlot(value)
|
||||
|
||||
# tracknumber: int
|
||||
# eg. 12
|
||||
value = item.get_track_number()
|
||||
if value is not None:
|
||||
info_tag.setTrackNumber(value)
|
||||
|
||||
elif isinstance(item, ImageItem):
|
||||
info_tag = list_item.getPictureInfoTag()
|
||||
info_type = 'picture'
|
||||
|
||||
value = item.get_name()
|
||||
if value is not None:
|
||||
info_tag.setTitle(value)
|
||||
|
||||
else:
|
||||
return
|
||||
|
||||
if info_type == 'video':
|
||||
# code: str
|
||||
# eg. "466K | 3.9K | 312"
|
||||
# Production code, currently used to store misc video data for label
|
||||
# formatting
|
||||
value = item.get_production_code()
|
||||
if value is not None:
|
||||
info_tag.setProductionCode(value)
|
||||
|
||||
value = item.get_dateadded(as_info_label=True)
|
||||
if value is not None:
|
||||
info_tag.setDateAdded(value)
|
||||
|
||||
# studio: list[str]
|
||||
# Used as alias for channel name if enabled
|
||||
value = item.get_studios()
|
||||
if value is not None:
|
||||
info_tag.setStudios(value)
|
||||
|
||||
# cast: list[xbmc.Actor]
|
||||
# From list[{member: str, role: str, order: int, thumbnail: str}]
|
||||
# Used as alias for channel name if enabled
|
||||
value = item.get_cast()
|
||||
if value is not None:
|
||||
info_tag.setCast([xbmc.Actor(**member) for member in value])
|
||||
|
||||
# artist: list[str]
|
||||
# eg. ["Angerfist"]
|
||||
# Used as alias for channel name
|
||||
value = item.get_artists()
|
||||
if value is not None:
|
||||
info_tag.setArtists(value)
|
||||
|
||||
# count: int
|
||||
# eg. 12
|
||||
# Can be used to store an id for later, or for sorting purposes
|
||||
# Used for Youtube video view count
|
||||
value = item.get_count()
|
||||
if value is not None:
|
||||
list_item.setInfo(info_type, {'count': value})
|
||||
|
||||
value = item.get_date(as_info_label=True)
|
||||
if value is not None:
|
||||
list_item.setDateTime(value)
|
||||
|
||||
if properties:
|
||||
list_item.setProperties(properties)
|
||||
|
||||
|
||||
def playback_item(context, media_item, show_fanart=None, **_kwargs):
|
||||
uri = media_item.get_uri()
|
||||
logging.debug('Converting %s for playback: %r',
|
||||
media_item.__class__.__name__,
|
||||
redact_ip_in_uri(uri))
|
||||
|
||||
params = context.get_params()
|
||||
settings = context.get_settings()
|
||||
ui = context.get_ui()
|
||||
|
||||
is_external = ui.get_property(PLAY_USING)
|
||||
is_strm = params.get(PLAY_STRM)
|
||||
mime_type = None
|
||||
|
||||
if is_strm:
|
||||
kwargs = {
|
||||
'path': uri,
|
||||
'offscreen': True,
|
||||
}
|
||||
props = {}
|
||||
else:
|
||||
kwargs = {
|
||||
'label': media_item.get_name(),
|
||||
'label2': media_item.get_short_details(),
|
||||
'path': uri,
|
||||
'offscreen': True,
|
||||
}
|
||||
props = {
|
||||
'isPlayable': VALUE_TO_STR[media_item.playable],
|
||||
'playlist_type_hint': (
|
||||
xbmc.PLAYLIST_MUSIC
|
||||
if isinstance(media_item, AudioItem) else
|
||||
xbmc.PLAYLIST_VIDEO
|
||||
),
|
||||
}
|
||||
|
||||
if media_item.use_isa() and context.use_inputstream_adaptive():
|
||||
capabilities = context.inputstream_adaptive_capabilities()
|
||||
|
||||
use_mpd = media_item.use_mpd()
|
||||
if use_mpd:
|
||||
manifest_type = 'mpd'
|
||||
mime_type = 'application/dash+xml'
|
||||
else:
|
||||
manifest_type = 'hls'
|
||||
mime_type = 'application/x-mpegURL'
|
||||
|
||||
stream_select = settings.stream_select()
|
||||
if not use_mpd and 'list' in stream_select:
|
||||
props['inputstream.adaptive.stream_selection_type'] = 'manual-osd'
|
||||
elif 'auto' in stream_select:
|
||||
props['inputstream.adaptive.stream_selection_type'] = 'adaptive'
|
||||
props['inputstream.adaptive.chooser_resolution_max'] = 'auto'
|
||||
|
||||
if current_system_version.compatible(19):
|
||||
props['inputstream'] = 'inputstream.adaptive'
|
||||
else:
|
||||
props['inputstreamaddon'] = 'inputstream.adaptive'
|
||||
|
||||
if not current_system_version.compatible(21):
|
||||
props['inputstream.adaptive.manifest_type'] = manifest_type
|
||||
|
||||
if media_item.live:
|
||||
if 'manifest_config_prop' in capabilities:
|
||||
props['inputstream.adaptive.manifest_config'] = dumps({
|
||||
'timeshift_bufferlimit': 4 * 60 * 60,
|
||||
})
|
||||
if ui.pop_property(PLAY_TIMESHIFT) and 'timeshift' in capabilities:
|
||||
props['inputstream.adaptive.play_timeshift_buffer'] = True
|
||||
|
||||
if not settings.verify_ssl() and 'config_prop' in capabilities:
|
||||
props['inputstream.adaptive.config'] = dumps({
|
||||
'ssl_verify_peer': False,
|
||||
})
|
||||
|
||||
headers = media_item.get_headers(as_string=True)
|
||||
if headers:
|
||||
props['inputstream.adaptive.manifest_headers'] = headers
|
||||
props['inputstream.adaptive.stream_headers'] = headers
|
||||
|
||||
license_key = media_item.get_license_key()
|
||||
if license_key:
|
||||
props['inputstream.adaptive.license_type'] = 'com.widevine.alpha'
|
||||
props['inputstream.adaptive.license_key'] = license_key
|
||||
|
||||
else:
|
||||
if 'mime=' in uri:
|
||||
mime_type = uri.split('mime=', 1)[1].split('&', 1)[0]
|
||||
mime_type = mime_type.replace('%2F', '/')
|
||||
|
||||
headers = media_item.get_headers(as_string=True)
|
||||
if (headers and uri.startswith('http')
|
||||
and not (is_external
|
||||
or settings.default_player_web_urls())):
|
||||
uri = '|'.join((uri, headers))
|
||||
kwargs['path'] = uri
|
||||
media_item.set_uri(uri)
|
||||
|
||||
list_item = xbmcgui.ListItem(**kwargs)
|
||||
|
||||
if mime_type or is_external:
|
||||
list_item.setContentLookup(False)
|
||||
list_item.setMimeType(mime_type or '*/*')
|
||||
|
||||
if is_strm:
|
||||
list_item.setProperties(props)
|
||||
return list_item
|
||||
|
||||
if show_fanart is None:
|
||||
show_fanart = settings.fanart_selection()
|
||||
image = media_item.get_image()
|
||||
art = {'icon': image}
|
||||
if image:
|
||||
art['thumb'] = image
|
||||
if show_fanart:
|
||||
art['fanart'] = media_item.get_fanart()
|
||||
list_item.setArt(art)
|
||||
|
||||
if media_item.subtitles:
|
||||
list_item.setSubtitles(media_item.subtitles)
|
||||
|
||||
resume = params.get('resume')
|
||||
set_info(list_item, media_item, props, resume=resume)
|
||||
|
||||
return list_item
|
||||
|
||||
|
||||
def directory_listitem(context, directory_item, show_fanart=None, **_kwargs):
|
||||
uri = directory_item.get_uri()
|
||||
is_action = directory_item.is_action()
|
||||
if not is_action:
|
||||
path, params = context.parse_uri(uri)
|
||||
if path.rstrip('/') == PATHS.PLAY and params.get(ACTION) != 'list':
|
||||
is_action = True
|
||||
if is_action:
|
||||
logging.debug('Converting DirectoryItem action: %r', uri)
|
||||
else:
|
||||
logging.debug('Converting DirectoryItem: %r', uri)
|
||||
|
||||
kwargs = {
|
||||
'label': directory_item.get_name(),
|
||||
'label2': directory_item.get_short_details(),
|
||||
'path': uri,
|
||||
'offscreen': True,
|
||||
}
|
||||
props = {
|
||||
'ForceResolvePlugin': 'true',
|
||||
}
|
||||
|
||||
if directory_item.next_page:
|
||||
props['specialSort'] = 'bottom'
|
||||
else:
|
||||
special_sort = directory_item.get_special_sort()
|
||||
if special_sort is None:
|
||||
special_sort = 'top'
|
||||
elif special_sort is False:
|
||||
special_sort = None
|
||||
|
||||
prop_value = directory_item.subscription_id
|
||||
if prop_value:
|
||||
special_sort = None
|
||||
props[SUBSCRIPTION_ID] = prop_value
|
||||
|
||||
prop_value = directory_item.channel_id
|
||||
if prop_value:
|
||||
special_sort = None
|
||||
props[CHANNEL_ID] = prop_value
|
||||
|
||||
prop_value = directory_item.playlist_id
|
||||
if prop_value:
|
||||
special_sort = None
|
||||
props[PLAYLIST_ID] = prop_value
|
||||
|
||||
prop_value = directory_item.bookmark_id
|
||||
if prop_value:
|
||||
special_sort = None
|
||||
props[BOOKMARK_ID] = prop_value
|
||||
|
||||
if special_sort:
|
||||
props['specialSort'] = special_sort
|
||||
|
||||
list_item = xbmcgui.ListItem(**kwargs)
|
||||
|
||||
if show_fanart is None:
|
||||
show_fanart = context.get_settings().fanart_selection()
|
||||
image = directory_item.get_image()
|
||||
art = {'icon': image}
|
||||
if image:
|
||||
art['thumb'] = image
|
||||
art['poster'] = image
|
||||
if show_fanart:
|
||||
art['fanart'] = directory_item.get_fanart()
|
||||
list_item.setArt(art)
|
||||
|
||||
set_info(list_item, directory_item, props)
|
||||
|
||||
context_menu = directory_item.get_context_menu()
|
||||
if context_menu is not None:
|
||||
list_item.addContextMenuItems(context_menu)
|
||||
|
||||
return uri, list_item, not is_action
|
||||
|
||||
|
||||
def image_listitem(context, image_item, show_fanart=None, **_kwargs):
|
||||
uri = image_item.get_uri()
|
||||
logging.debug('Converting ImageItem: %r', uri)
|
||||
|
||||
kwargs = {
|
||||
'label': image_item.get_name(),
|
||||
'path': uri,
|
||||
'offscreen': True,
|
||||
}
|
||||
props = {
|
||||
'isPlayable': VALUE_TO_STR[image_item.playable],
|
||||
'ForceResolvePlugin': 'true',
|
||||
}
|
||||
|
||||
list_item = xbmcgui.ListItem(**kwargs)
|
||||
|
||||
if show_fanart is None:
|
||||
show_fanart = context.get_settings().fanart_selection()
|
||||
image = image_item.get_image()
|
||||
art = {'icon': image}
|
||||
if image:
|
||||
art['thumb'] = image
|
||||
if show_fanart:
|
||||
art['fanart'] = image_item.get_fanart()
|
||||
list_item.setArt(art)
|
||||
|
||||
set_info(list_item, image_item, props)
|
||||
|
||||
context_menu = image_item.get_context_menu()
|
||||
if context_menu is not None:
|
||||
list_item.addContextMenuItems(context_menu)
|
||||
|
||||
return uri, list_item, False
|
||||
|
||||
|
||||
def uri_listitem(_context, uri_item, **_kwargs):
|
||||
uri = uri_item.get_uri()
|
||||
logging.debug('Converting UriItem: %r', uri)
|
||||
|
||||
kwargs = {
|
||||
'label': uri_item.get_name(),
|
||||
'path': uri,
|
||||
'offscreen': True,
|
||||
}
|
||||
props = {
|
||||
'isPlayable': VALUE_TO_STR[uri_item.playable],
|
||||
'ForceResolvePlugin': 'true',
|
||||
}
|
||||
|
||||
list_item = xbmcgui.ListItem(**kwargs)
|
||||
list_item.setProperties(props)
|
||||
return list_item
|
||||
|
||||
|
||||
def media_listitem(context,
|
||||
media_item,
|
||||
show_fanart=None,
|
||||
to_sync=None,
|
||||
**_kwargs):
|
||||
uri = media_item.get_uri()
|
||||
logging.debug('Converting %s: %r', media_item.__class__.__name__, uri)
|
||||
|
||||
kwargs = {
|
||||
'label': media_item.get_name(),
|
||||
'label2': media_item.get_short_details(),
|
||||
'path': uri,
|
||||
'offscreen': True,
|
||||
}
|
||||
props = {
|
||||
'isPlayable': VALUE_TO_STR[media_item.playable],
|
||||
'ForceResolvePlugin': 'true',
|
||||
'playlist_type_hint': (
|
||||
xbmc.PLAYLIST_MUSIC
|
||||
if isinstance(media_item, AudioItem) else
|
||||
xbmc.PLAYLIST_VIDEO
|
||||
),
|
||||
}
|
||||
|
||||
published_at = media_item.get_added_utc()
|
||||
scheduled_start = media_item.get_scheduled_start_utc()
|
||||
datetime = scheduled_start or published_at
|
||||
local_datetime = None
|
||||
if datetime:
|
||||
local_datetime = utc_to_local(datetime)
|
||||
props['PublishedLocal'] = to_str(local_datetime)
|
||||
if media_item.live:
|
||||
props['PublishedSince'] = context.localize('live')
|
||||
elif local_datetime:
|
||||
props['PublishedSince'] = to_str(datetime_to_since(
|
||||
context, local_datetime
|
||||
))
|
||||
|
||||
set_play_count = True
|
||||
resume = True
|
||||
prop_value = media_item.video_id
|
||||
if prop_value:
|
||||
props[VIDEO_ID] = prop_value
|
||||
|
||||
if to_sync and prop_value in to_sync:
|
||||
set_play_count = False
|
||||
|
||||
# make channel_id property available for keymapping
|
||||
prop_value = media_item.channel_id
|
||||
if prop_value:
|
||||
props[CHANNEL_ID] = prop_value
|
||||
|
||||
# make subscription_id property available for keymapping
|
||||
prop_value = media_item.subscription_id
|
||||
if prop_value:
|
||||
props[SUBSCRIPTION_ID] = prop_value
|
||||
|
||||
# make playlist_id property available for keymapping
|
||||
prop_value = media_item.playlist_id
|
||||
if prop_value:
|
||||
props[PLAYLIST_ID] = prop_value
|
||||
|
||||
# make playlist_item_id property available for keymapping
|
||||
prop_value = media_item.playlist_item_id
|
||||
if prop_value:
|
||||
props[PLAYLIST_ITEM_ID] = prop_value
|
||||
|
||||
# make bookmark_id property available for keymapping
|
||||
prop_value = media_item.bookmark_id
|
||||
if prop_value:
|
||||
props[BOOKMARK_ID] = prop_value
|
||||
|
||||
list_item = xbmcgui.ListItem(**kwargs)
|
||||
|
||||
if show_fanart is None:
|
||||
show_fanart = context.get_settings().fanart_selection()
|
||||
image = media_item.get_image()
|
||||
art = {'icon': image}
|
||||
if image:
|
||||
art['thumb'] = image
|
||||
if show_fanart:
|
||||
art['fanart'] = media_item.get_fanart()
|
||||
list_item.setArt(art)
|
||||
|
||||
if media_item.subtitles:
|
||||
list_item.setSubtitles(media_item.subtitles)
|
||||
|
||||
set_info(list_item,
|
||||
media_item,
|
||||
props,
|
||||
set_play_count=set_play_count,
|
||||
resume=resume)
|
||||
|
||||
context_menu = media_item.get_context_menu()
|
||||
if context_menu:
|
||||
list_item.addContextMenuItems(context_menu)
|
||||
|
||||
return uri, list_item, False
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2018-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
|
||||
|
||||
from .access_manager import AccessManager
|
||||
from .api_keys import APIKeyStore
|
||||
|
||||
|
||||
__all__ = ('AccessManager', 'APIKeyStore',)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,590 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from .json_store import JSONStore
|
||||
from ..compatibility import string_type
|
||||
from ..constants import ADDON_ID
|
||||
from ..utils.methods import generate_hash
|
||||
|
||||
|
||||
class AccessManager(JSONStore):
|
||||
DEFAULT_NEW_USER = {
|
||||
'access_token': '',
|
||||
'refresh_token': '',
|
||||
'token_expires': -1,
|
||||
'last_key_hash': '',
|
||||
'name': 'Default',
|
||||
'id': None,
|
||||
'watch_later': 'WL',
|
||||
'watch_history': 'HL'
|
||||
}
|
||||
DEFAULT_NEW_DEVELOPER = {
|
||||
'access_token': '',
|
||||
'refresh_token': '',
|
||||
'token_expires': -1,
|
||||
'last_key_hash': ''
|
||||
}
|
||||
|
||||
def __init__(self, context):
|
||||
self._user = None
|
||||
self._last_origin = None
|
||||
super(AccessManager, self).__init__('access_manager.json', context)
|
||||
|
||||
def init(self):
|
||||
super(AccessManager, self).init()
|
||||
access_manager_data = self._data['access_manager']
|
||||
self._user = access_manager_data.get('current_user', 0)
|
||||
self._last_origin = access_manager_data.get('last_origin', ADDON_ID)
|
||||
|
||||
def set_defaults(self, reset=False):
|
||||
data = {} if reset else self.get_data()
|
||||
|
||||
access_manager = data.get('access_manager')
|
||||
if not access_manager or not isinstance(access_manager, dict):
|
||||
users = {
|
||||
0: self.DEFAULT_NEW_USER.copy(),
|
||||
}
|
||||
access_manager = {
|
||||
'users': users,
|
||||
'current_user': 0,
|
||||
'last_origin': ADDON_ID,
|
||||
'developers': {},
|
||||
}
|
||||
else:
|
||||
users = access_manager.get('users')
|
||||
if not users or not isinstance(users, dict):
|
||||
users = {
|
||||
0: self.DEFAULT_NEW_USER.copy(),
|
||||
}
|
||||
elif any(not isinstance(user_id, int) for user_id in users):
|
||||
new_users = {}
|
||||
old_users = {}
|
||||
for user_id, user in users.items():
|
||||
if isinstance(user_id, int):
|
||||
new_users[user_id] = user
|
||||
else:
|
||||
try:
|
||||
user_id = int(user_id)
|
||||
if user_id in users:
|
||||
raise ValueError
|
||||
new_users[user_id] = user
|
||||
except (TypeError, ValueError):
|
||||
old_users[user_id] = user
|
||||
if new_users:
|
||||
users = new_users
|
||||
if old_users:
|
||||
new_user_id = max(users) + 1 if users else 0
|
||||
for user in old_users.values():
|
||||
users[new_user_id] = user
|
||||
new_user_id += 1
|
||||
access_manager['users'] = users
|
||||
|
||||
current_id = access_manager.get('current_user')
|
||||
if (not current_id
|
||||
or current_id == 'default'
|
||||
or current_id not in users):
|
||||
current_id = min(users)
|
||||
else:
|
||||
if not isinstance(current_id, int):
|
||||
try:
|
||||
current_id = int(current_id)
|
||||
if current_id not in users:
|
||||
raise ValueError
|
||||
except (TypeError, ValueError):
|
||||
current_id = min(users)
|
||||
access_manager['current_user'] = current_id
|
||||
current_user = users[current_id]
|
||||
current_user.setdefault('watch_later', 'WL')
|
||||
current_user.setdefault('watch_history', 'HL')
|
||||
|
||||
if 'default' in access_manager:
|
||||
default_user = access_manager['default']
|
||||
if (isinstance(default_user, dict)
|
||||
and (default_user.get('access_token')
|
||||
or default_user.get('refresh_token'))
|
||||
and not current_user.get('access_token')
|
||||
and not current_user.get('refresh_token')):
|
||||
default_user.setdefault('name', 'Default')
|
||||
users[current_id] = default_user
|
||||
del access_manager['default']
|
||||
|
||||
if 'access_token' in access_manager:
|
||||
del access_manager['access_token']
|
||||
|
||||
if 'refresh_token' in access_manager:
|
||||
del access_manager['refresh_token']
|
||||
|
||||
if 'token_expires' in access_manager:
|
||||
del access_manager['token_expires']
|
||||
|
||||
last_origin = access_manager.get('last_origin')
|
||||
if not last_origin or not isinstance(last_origin, string_type):
|
||||
access_manager['last_origin'] = ADDON_ID
|
||||
|
||||
developers = access_manager.get('developers')
|
||||
if not developers or not isinstance(developers, dict):
|
||||
access_manager['developers'] = {}
|
||||
data['access_manager'] = access_manager
|
||||
|
||||
# ensure all users have uuid
|
||||
uuids = set()
|
||||
for user in users.values():
|
||||
user_uuid = user.get('id')
|
||||
if user_uuid:
|
||||
if user_uuid in uuids:
|
||||
user['old_id'] = user_uuid
|
||||
user_uuid = None
|
||||
else:
|
||||
uuids.add(user_uuid)
|
||||
continue
|
||||
while not user_uuid or user_uuid in uuids:
|
||||
user_uuid = uuid.uuid4().hex
|
||||
uuids.add(user_uuid)
|
||||
user['id'] = user_uuid
|
||||
# end uuid check
|
||||
|
||||
return self.save(data)
|
||||
|
||||
@staticmethod
|
||||
def _process_data(data):
|
||||
output = {}
|
||||
for key, value in data:
|
||||
if key in output:
|
||||
continue
|
||||
if key == 'current_user':
|
||||
try:
|
||||
value = int(value)
|
||||
except (TypeError, ValueError):
|
||||
value = 0
|
||||
else:
|
||||
try:
|
||||
key = int(key)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
output[key] = value
|
||||
return output
|
||||
|
||||
def get_current_user_details(self, addon_id=None):
|
||||
"""
|
||||
:return: current user
|
||||
"""
|
||||
if addon_id and addon_id != ADDON_ID:
|
||||
return self.get_developers().get(addon_id, {})
|
||||
return self.get_users()[self._user]
|
||||
|
||||
def get_current_user_id(self):
|
||||
"""
|
||||
:return: uuid of the current user
|
||||
"""
|
||||
return self.get_users()[self._user]['id']
|
||||
|
||||
def get_new_user(self, username=''):
|
||||
"""
|
||||
:param username: string, users name
|
||||
:return: a new user dict
|
||||
"""
|
||||
uuids = [
|
||||
user.get('id')
|
||||
for user in self.get_users().values()
|
||||
]
|
||||
new_uuid = None
|
||||
while not new_uuid or new_uuid in uuids:
|
||||
new_uuid = uuid.uuid4().hex
|
||||
return dict(self.DEFAULT_NEW_USER,
|
||||
name=username,
|
||||
id=new_uuid)
|
||||
|
||||
def get_users(self):
|
||||
"""
|
||||
Returns users
|
||||
:return: users
|
||||
"""
|
||||
data = self._data if self._loaded else self.get_data()
|
||||
return data['access_manager'].get('users', {})
|
||||
|
||||
def add_user(self, username='', user=None):
|
||||
"""
|
||||
Add single new user to users collection
|
||||
:param username: str, chosen name of new user
|
||||
:param user: int, optional index for new user
|
||||
:return: tuple, (index, details) of newly added user
|
||||
"""
|
||||
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
|
||||
data = {
|
||||
'access_manager': {
|
||||
'users': {
|
||||
new_user: new_user_details,
|
||||
},
|
||||
},
|
||||
}
|
||||
self.save(data, update=True)
|
||||
return new_user, new_user_details
|
||||
|
||||
def remove_user(self, user):
|
||||
"""
|
||||
Remove user from collection of current users
|
||||
:param user: int, user index
|
||||
:return:
|
||||
"""
|
||||
users = self.get_users()
|
||||
if user in users:
|
||||
data = {
|
||||
'access_manager': {
|
||||
'users': {
|
||||
user: KeyError,
|
||||
},
|
||||
},
|
||||
}
|
||||
self.save(data, update=True)
|
||||
|
||||
def set_users(self, users):
|
||||
"""
|
||||
Updates all users
|
||||
:param users: dict, users
|
||||
:return:
|
||||
"""
|
||||
data = self.get_data()
|
||||
data['access_manager']['users'] = users
|
||||
self.save(data)
|
||||
|
||||
def set_user(self, user, switch_to=False):
|
||||
"""
|
||||
Updates the user
|
||||
:param user: string, username
|
||||
:param switch_to: boolean, change current user
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
user = int(user)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
self._user = user
|
||||
if switch_to:
|
||||
data = {
|
||||
'access_manager': {
|
||||
'current_user': user,
|
||||
},
|
||||
}
|
||||
self.save(data, update=True)
|
||||
|
||||
def get_current_user(self):
|
||||
"""
|
||||
Returns the current user
|
||||
:return: user
|
||||
"""
|
||||
return self._user
|
||||
|
||||
def get_username(self, user=None):
|
||||
"""
|
||||
Returns the username of the current or nominated user
|
||||
:return: username
|
||||
"""
|
||||
if user is None:
|
||||
user = self._user
|
||||
users = self.get_users()
|
||||
if user in users:
|
||||
return users[user].get('name')
|
||||
return ''
|
||||
|
||||
def set_username(self, user, username):
|
||||
"""
|
||||
Sets the username of the nominated user
|
||||
:return: True if username was set, false otherwise
|
||||
"""
|
||||
users = self.get_users()
|
||||
if user in users:
|
||||
data = {
|
||||
'access_manager': {
|
||||
'users': {
|
||||
user: {
|
||||
'name': username,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
self.save(data, update=True)
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_watch_later_id(self):
|
||||
"""
|
||||
Returns the current users watch later playlist id
|
||||
:return: the current users watch later playlist id
|
||||
"""
|
||||
current_id = self.get_current_user_details().get('watch_later', '')
|
||||
current_id = current_id.strip()
|
||||
current_id_lower = current_id.lower()
|
||||
|
||||
settings = self._context.get_settings()
|
||||
settings_id = settings.get_watch_later_playlist()
|
||||
settings_id_lower = settings_id.lower()
|
||||
|
||||
if settings_id_lower == 'local':
|
||||
current_id = self.set_watch_later_id(None)
|
||||
elif settings_id and settings_id_lower != current_id_lower:
|
||||
current_id = self.set_watch_later_id(settings_id)
|
||||
elif current_id_lower == 'local':
|
||||
current_id = ''
|
||||
|
||||
if settings_id:
|
||||
settings.set_watch_later_playlist('')
|
||||
|
||||
return current_id
|
||||
|
||||
def set_watch_later_id(self, playlist_id=None):
|
||||
"""
|
||||
Sets the current users watch later playlist id
|
||||
:param playlist_id: string, watch later playlist id
|
||||
:return:
|
||||
"""
|
||||
if not playlist_id:
|
||||
playlist_id = ''
|
||||
|
||||
self._context.get_settings().set_watch_later_playlist('')
|
||||
|
||||
playlists = {
|
||||
'watch_later': playlist_id,
|
||||
}
|
||||
current_id = self.get_current_user_details().get('watch_later')
|
||||
if current_id:
|
||||
playlists['watch_later_old'] = current_id
|
||||
|
||||
data = {
|
||||
'access_manager': {
|
||||
'users': {
|
||||
self._user: playlists,
|
||||
},
|
||||
},
|
||||
}
|
||||
self.save(data, update=True)
|
||||
return playlist_id
|
||||
|
||||
def get_watch_history_id(self):
|
||||
"""
|
||||
Returns the current users watch history playlist id
|
||||
:return: the current users watch history playlist id
|
||||
"""
|
||||
|
||||
current_id = self.get_current_user_details().get('watch_history', '')
|
||||
current_id = current_id.strip()
|
||||
current_id_lower = current_id.lower()
|
||||
|
||||
settings = self._context.get_settings()
|
||||
settings_id = settings.get_history_playlist()
|
||||
settings_id_lower = settings_id.lower()
|
||||
|
||||
if settings_id_lower == 'local':
|
||||
current_id = self.set_watch_history_id(None)
|
||||
elif settings_id and settings_id_lower != current_id_lower:
|
||||
current_id = self.set_watch_history_id(settings_id)
|
||||
elif current_id_lower == 'local':
|
||||
current_id = ''
|
||||
|
||||
if settings_id:
|
||||
settings.set_history_playlist('')
|
||||
|
||||
return current_id
|
||||
|
||||
def set_watch_history_id(self, playlist_id=None):
|
||||
"""
|
||||
Sets the current users watch history playlist id
|
||||
:param playlist_id: string, watch history playlist id
|
||||
:return:
|
||||
"""
|
||||
if not playlist_id:
|
||||
playlist_id = ''
|
||||
|
||||
self._context.get_settings().set_history_playlist('')
|
||||
|
||||
playlists = {
|
||||
'watch_history': playlist_id,
|
||||
}
|
||||
current_id = self.get_current_user_details().get('watch_history')
|
||||
if current_id:
|
||||
playlists['watch_history_old'] = current_id
|
||||
|
||||
data = {
|
||||
'access_manager': {
|
||||
'users': {
|
||||
self._user: playlists,
|
||||
},
|
||||
},
|
||||
}
|
||||
self.save(data, update=True)
|
||||
return playlist_id
|
||||
|
||||
def set_last_origin(self, origin):
|
||||
"""
|
||||
Updates the origin
|
||||
:param origin: string, origin
|
||||
:return:
|
||||
"""
|
||||
self._last_origin = origin
|
||||
data = {
|
||||
'access_manager': {
|
||||
'last_origin': origin,
|
||||
},
|
||||
}
|
||||
self.save(data, update=True)
|
||||
|
||||
def get_last_origin(self):
|
||||
"""
|
||||
Returns the last origin
|
||||
:return:
|
||||
"""
|
||||
return self._last_origin
|
||||
|
||||
def get_refresh_tokens(self, addon_id=None):
|
||||
"""
|
||||
Returns a tuple containing a list of refresh tokens and the number of
|
||||
valid refresh tokens
|
||||
:return:
|
||||
"""
|
||||
details = self.get_current_user_details(addon_id)
|
||||
refresh_tokens = details.get('refresh_token', '').split('|')
|
||||
num_refresh_tokens = len([1 for token in refresh_tokens if token])
|
||||
return refresh_tokens, num_refresh_tokens
|
||||
|
||||
def get_access_tokens(self, addon_id=None):
|
||||
"""
|
||||
Returns a tuple containing a list of access tokens, the number of valid
|
||||
access tokens, and the token expiry timestamp.
|
||||
:return:
|
||||
"""
|
||||
details = self.get_current_user_details(addon_id)
|
||||
access_tokens = details.get('access_token').split('|')
|
||||
expiry_timestamp = int(details.get('token_expires', -1))
|
||||
if expiry_timestamp > int(time.time()):
|
||||
num_access_tokens = len([1 for token in access_tokens if token])
|
||||
else:
|
||||
access_tokens = [None, None, None, None]
|
||||
num_access_tokens = 0
|
||||
return access_tokens, num_access_tokens, expiry_timestamp
|
||||
|
||||
def update_access_token(self,
|
||||
addon_id,
|
||||
access_token=None,
|
||||
expiry=None,
|
||||
refresh_token=None):
|
||||
"""
|
||||
Updates the old access token with the new one.
|
||||
:param addon_id:
|
||||
:param access_token:
|
||||
:param expiry:
|
||||
:param refresh_token:
|
||||
:return:
|
||||
"""
|
||||
details = {
|
||||
'access_token': (
|
||||
'|'.join([token or '' for token in access_token])
|
||||
if isinstance(access_token, (list, tuple)) else
|
||||
access_token
|
||||
if access_token else
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
if expiry is not None:
|
||||
if isinstance(expiry, (list, tuple)):
|
||||
expiry = [val for val in expiry if val]
|
||||
expiry = min(map(int, expiry)) if expiry else -1
|
||||
else:
|
||||
expiry = int(expiry)
|
||||
details['token_expires'] = time.time() + expiry
|
||||
|
||||
if refresh_token is not None:
|
||||
details['refresh_token'] = (
|
||||
'|'.join([token or '' for token in refresh_token])
|
||||
if isinstance(refresh_token, (list, tuple)) else
|
||||
refresh_token
|
||||
)
|
||||
|
||||
data = {
|
||||
'access_manager': {
|
||||
'developers': {
|
||||
addon_id: details,
|
||||
},
|
||||
} if addon_id and addon_id != ADDON_ID else {
|
||||
'users': {
|
||||
self._user: details,
|
||||
},
|
||||
},
|
||||
}
|
||||
self.save(data, update=True)
|
||||
|
||||
def get_last_key_hash(self, addon_id=None):
|
||||
details = self.get_current_user_details(addon_id)
|
||||
return details.get('last_key_hash', '')
|
||||
|
||||
def set_last_key_hash(self, key_hash, addon_id=None):
|
||||
data = {
|
||||
'access_manager': {
|
||||
'developers': {
|
||||
addon_id: {
|
||||
'last_key_hash': key_hash,
|
||||
},
|
||||
},
|
||||
} if addon_id and addon_id != ADDON_ID else {
|
||||
'users': {
|
||||
self._user: {
|
||||
'last_key_hash': key_hash,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
self.save(data, update=True)
|
||||
|
||||
def get_developers(self):
|
||||
"""
|
||||
Returns developers
|
||||
:return: dict, developers
|
||||
"""
|
||||
data = self._data if self._loaded else self.get_data()
|
||||
return data['access_manager'].get('developers', {})
|
||||
|
||||
def add_new_developer(self, addon_id):
|
||||
"""
|
||||
Updates the developer users
|
||||
:param addon_id: str
|
||||
:return:
|
||||
"""
|
||||
data = self.get_data()
|
||||
developers = data['access_manager'].get('developers', {})
|
||||
if addon_id not in developers:
|
||||
developers[addon_id] = self.DEFAULT_NEW_DEVELOPER.copy()
|
||||
data['access_manager']['developers'] = developers
|
||||
return self.save(data)
|
||||
return False
|
||||
|
||||
def keys_changed(self,
|
||||
addon_id,
|
||||
api_key,
|
||||
client_id,
|
||||
client_secret,
|
||||
update_hash=True):
|
||||
last_hash = self.get_last_key_hash(addon_id)
|
||||
current_hash = generate_hash(api_key, client_id, client_secret)
|
||||
|
||||
keys_changed = False
|
||||
if not last_hash and current_hash:
|
||||
if update_hash:
|
||||
self.set_last_key_hash(current_hash, addon_id)
|
||||
elif (not current_hash and last_hash
|
||||
or last_hash != current_hash):
|
||||
if update_hash:
|
||||
self.set_last_key_hash(current_hash, addon_id)
|
||||
keys_changed = True
|
||||
return keys_changed
|
||||
@@ -0,0 +1,379 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2018-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
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
|
||||
from .json_store import JSONStore
|
||||
from .. import logging
|
||||
from ..constants import DEVELOPER_CONFIGS
|
||||
from ... import key_sets
|
||||
|
||||
|
||||
class APIKeyStore(JSONStore):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN_SUFFIX = '.apps.googleusercontent.com'
|
||||
|
||||
def __init__(self, context):
|
||||
super(APIKeyStore, self).__init__('api_keys.json', context)
|
||||
|
||||
def set_defaults(self, reset=False):
|
||||
data = {} if reset else self.get_data()
|
||||
|
||||
if 'keys' not in data:
|
||||
data = {
|
||||
'keys': {
|
||||
'user': {
|
||||
'api_key': '',
|
||||
'client_id': '',
|
||||
'client_secret': '',
|
||||
},
|
||||
'developer': {},
|
||||
},
|
||||
}
|
||||
else:
|
||||
keys = data['keys'] or {}
|
||||
if 'user' not in keys:
|
||||
keys['user'] = keys.pop('personal', {
|
||||
'api_key': '',
|
||||
'client_id': '',
|
||||
'client_secret': '',
|
||||
})
|
||||
if 'developer' not in keys:
|
||||
keys['developer'] = {}
|
||||
data['keys'] = keys
|
||||
|
||||
self.save(data)
|
||||
|
||||
@staticmethod
|
||||
def get_current_switch():
|
||||
return 'user'
|
||||
|
||||
def has_user_api_keys(self):
|
||||
api_data = self.get_data()
|
||||
try:
|
||||
return (api_data['keys']['user']['api_key']
|
||||
and api_data['keys']['user']['client_id']
|
||||
and api_data['keys']['user']['client_secret'])
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def get_api_keys(self, switch):
|
||||
api_data = self.get_data()
|
||||
if switch == 'developer':
|
||||
return api_data['keys'][switch]
|
||||
|
||||
decode = True
|
||||
if switch == 'youtube-tv':
|
||||
system = 'YouTube TV'
|
||||
key_set_details = key_sets[switch]
|
||||
|
||||
elif switch == 'youtube-vr':
|
||||
system = 'YouTube VR'
|
||||
key_set_details = key_sets[switch]
|
||||
|
||||
elif switch.startswith('user'):
|
||||
decode = False
|
||||
system = 'All'
|
||||
key_set_details = api_data['keys']['user']
|
||||
|
||||
else:
|
||||
system = 'All'
|
||||
if switch not in key_sets['provided']:
|
||||
switch = 0
|
||||
key_set_details = key_sets['provided'][switch]
|
||||
|
||||
key_set = {
|
||||
'system': system,
|
||||
'id': '',
|
||||
'key': '',
|
||||
'secret': ''
|
||||
}
|
||||
for key, value in key_set_details.items():
|
||||
if decode:
|
||||
value = b64decode(value).decode('utf-8')
|
||||
key = key.partition('_')[-1]
|
||||
if key and key in key_set:
|
||||
key_set[key] = value
|
||||
if (key_set['id']
|
||||
and not key_set['id'].endswith(self.DOMAIN_SUFFIX)):
|
||||
key_set['id'] += self.DOMAIN_SUFFIX
|
||||
return key_set
|
||||
|
||||
def get_key_set(self, switch):
|
||||
key_set = self.get_api_keys(switch)
|
||||
if switch.startswith('user'):
|
||||
client_id = key_set['id'].replace(self.DOMAIN_SUFFIX, '')
|
||||
if switch == 'user_old':
|
||||
client_id += self.DOMAIN_SUFFIX
|
||||
key_set['id'] = client_id
|
||||
return key_set
|
||||
|
||||
def strip_details(self, api_key, client_id, client_secret):
|
||||
stripped_key = ''.join(api_key.split())
|
||||
stripped_id = ''.join(client_id.replace(self.DOMAIN_SUFFIX, '').split())
|
||||
stripped_secret = ''.join(client_secret.split())
|
||||
|
||||
if api_key != stripped_key:
|
||||
if stripped_key not in api_key:
|
||||
self.log.debug('Personal API key'
|
||||
' - skipped (mangled by stripping)')
|
||||
return_key = api_key
|
||||
else:
|
||||
self.log.debug('Personal API key'
|
||||
' - whitespace removed')
|
||||
return_key = stripped_key
|
||||
else:
|
||||
return_key = api_key
|
||||
|
||||
if client_id != stripped_id:
|
||||
if stripped_id not in client_id:
|
||||
self.log.debug('Personal API client ID'
|
||||
' - skipped (mangled by stripping)')
|
||||
return_id = client_id
|
||||
elif self.DOMAIN_SUFFIX in client_id:
|
||||
self.log.debug('Personal API client ID'
|
||||
' - whitespace and domain removed')
|
||||
return_id = stripped_id
|
||||
else:
|
||||
self.log.debug('Personal API client ID'
|
||||
' - whitespace removed')
|
||||
return_id = stripped_id
|
||||
else:
|
||||
return_id = client_id
|
||||
|
||||
if client_secret != stripped_secret:
|
||||
if stripped_secret not in client_secret:
|
||||
self.log.debug('Personal API client secret'
|
||||
' - skipped (mangled by stripping)')
|
||||
return_secret = client_secret
|
||||
else:
|
||||
self.log.debug('Personal API client secret'
|
||||
' - whitespace removed')
|
||||
return_secret = stripped_secret
|
||||
else:
|
||||
return_secret = client_secret
|
||||
|
||||
return return_key, return_id, return_secret
|
||||
|
||||
def get_configs(self):
|
||||
return {
|
||||
'tv': self.get_api_keys('youtube-tv'),
|
||||
'user': self.get_api_keys(self.get_current_switch()),
|
||||
'vr': self.get_api_keys('youtube-vr'),
|
||||
'dev': self.get_api_keys('developer'),
|
||||
}
|
||||
|
||||
def get_developer_config(self, developer_id):
|
||||
context = self._context
|
||||
|
||||
developer_configs = self.get_api_keys('developer')
|
||||
if developer_id and developer_configs:
|
||||
config = developer_configs.get(developer_id)
|
||||
else:
|
||||
config = context.get_ui().pop_property(DEVELOPER_CONFIGS)
|
||||
if config:
|
||||
self.log.warning('Storing developer keys in window property'
|
||||
' has been deprecated. Please use the'
|
||||
' youtube_registration module instead')
|
||||
config = self.load_data(config)
|
||||
|
||||
if not config:
|
||||
return {}
|
||||
|
||||
if not context.get_settings().allow_dev_keys():
|
||||
self.log.debug('Developer config ignored')
|
||||
return {}
|
||||
|
||||
origin = config.get('origin', developer_id)
|
||||
key_details = config.get(origin)
|
||||
required_details = {'key', 'id', 'secret'}
|
||||
if (not origin
|
||||
or not key_details
|
||||
or not required_details.issubset(key_details)):
|
||||
self.log.error_trace(('Invalid developer config: {config!r}',
|
||||
'Expected: {{',
|
||||
' "origin": ADDON_ID,',
|
||||
' ADDON_ID: {{',
|
||||
' "system": SYSTEM_NAME,',
|
||||
' "key": API_KEY,',
|
||||
' "id": CLIENT_ID,',
|
||||
' "secret": CLIENT_SECRET',
|
||||
' }},',
|
||||
'}}'),
|
||||
config=config)
|
||||
return {}
|
||||
|
||||
key_system = key_details.get('system')
|
||||
if key_system == 'JSONStore':
|
||||
for key in required_details:
|
||||
key_details[key] = b64decode(key_details[key]).decode('utf-8')
|
||||
|
||||
self.log.debug(('Using developer config',
|
||||
'Origin: {origin!r}',
|
||||
'System: {system!r}'),
|
||||
origin=origin,
|
||||
system=key_system)
|
||||
|
||||
return {
|
||||
'origin': origin,
|
||||
origin: {
|
||||
'system': key_system,
|
||||
'key': key_details['key'],
|
||||
'id': key_details['id'],
|
||||
'secret': key_details['secret'],
|
||||
}
|
||||
}
|
||||
|
||||
def update_developer_config(self,
|
||||
developer_id,
|
||||
api_key,
|
||||
client_id,
|
||||
client_secret):
|
||||
data = self.get_data()
|
||||
existing_config = data['keys']['developer'].get(developer_id, {})
|
||||
|
||||
new_config = {
|
||||
'origin': developer_id,
|
||||
developer_id: {
|
||||
'system': 'JSONStore',
|
||||
'key': b64encode(
|
||||
bytes(api_key, 'utf-8')
|
||||
).decode('ascii'),
|
||||
'id': b64encode(
|
||||
bytes(client_id, 'utf-8')
|
||||
).decode('ascii'),
|
||||
'secret': b64encode(
|
||||
bytes(client_secret, 'utf-8')
|
||||
).decode('ascii'),
|
||||
}
|
||||
}
|
||||
|
||||
if existing_config and existing_config == new_config:
|
||||
return False
|
||||
data['keys']['developer'][developer_id] = new_config
|
||||
return self.save(data)
|
||||
|
||||
def sync(self):
|
||||
api_data = self.get_data()
|
||||
settings = self._context.get_settings()
|
||||
|
||||
update_saved_values = False
|
||||
update_settings_values = False
|
||||
|
||||
saved_details = (
|
||||
api_data['keys']['user'].get('api_key', ''),
|
||||
api_data['keys']['user'].get('client_id', ''),
|
||||
api_data['keys']['user'].get('client_secret', ''),
|
||||
)
|
||||
if all(saved_details):
|
||||
update_settings_values = True
|
||||
# users are now pasting keys into api_keys.json
|
||||
# try stripping whitespace and domain suffix from API details
|
||||
# and save the results if they differ
|
||||
stripped_details = self.strip_details(*saved_details)
|
||||
if all(stripped_details) and saved_details != stripped_details:
|
||||
saved_details = stripped_details
|
||||
api_data['keys']['user'] = {
|
||||
'api_key': saved_details[0],
|
||||
'client_id': saved_details[1],
|
||||
'client_secret': saved_details[2],
|
||||
}
|
||||
update_saved_values = True
|
||||
|
||||
setting_details = (
|
||||
settings.api_key(),
|
||||
settings.api_id(),
|
||||
settings.api_secret(),
|
||||
)
|
||||
if all(setting_details):
|
||||
update_settings_values = False
|
||||
stripped_details = self.strip_details(*setting_details)
|
||||
if all(stripped_details) and setting_details != stripped_details:
|
||||
setting_details = (
|
||||
settings.api_key(stripped_details[0]),
|
||||
settings.api_id(stripped_details[1]),
|
||||
settings.api_secret(stripped_details[2]),
|
||||
)
|
||||
|
||||
if saved_details != setting_details:
|
||||
api_data['keys']['user'] = {
|
||||
'api_key': setting_details[0],
|
||||
'client_id': setting_details[1],
|
||||
'client_secret': setting_details[2],
|
||||
}
|
||||
update_saved_values = True
|
||||
|
||||
if update_settings_values:
|
||||
settings.api_key(saved_details[0])
|
||||
settings.api_id(saved_details[1])
|
||||
settings.api_secret(saved_details[2])
|
||||
|
||||
if update_saved_values:
|
||||
self.save(api_data)
|
||||
return True
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
context = self._context
|
||||
localize = context.localize
|
||||
settings = context.get_settings()
|
||||
ui = context.get_ui()
|
||||
|
||||
params = context.get_params()
|
||||
api_key = params.get('api_key')
|
||||
client_id = params.get('client_id')
|
||||
client_secret = params.get('client_secret')
|
||||
enable = params.get('enable')
|
||||
|
||||
updated_list = []
|
||||
log_list = []
|
||||
|
||||
if api_key:
|
||||
settings.api_key(api_key)
|
||||
updated_list.append(localize('api.key'))
|
||||
log_list.append('api_key')
|
||||
if client_id:
|
||||
settings.api_id(client_id)
|
||||
updated_list.append(localize('api.id'))
|
||||
log_list.append('client_id')
|
||||
if client_secret:
|
||||
settings.api_secret(client_secret)
|
||||
updated_list.append(localize('api.secret'))
|
||||
log_list.append('client_secret')
|
||||
if updated_list:
|
||||
ui.show_notification(localize('updated.x', ', '.join(updated_list)))
|
||||
self.log.debug('Updated API details: %s', log_list)
|
||||
|
||||
client_id = settings.api_id()
|
||||
client_secret = settings.api_secret()
|
||||
api_key = settings.api_key
|
||||
missing_list = []
|
||||
log_list = []
|
||||
|
||||
if enable and client_id and client_secret and api_key:
|
||||
ui.show_notification(localize('api.personal.enabled'))
|
||||
self.log.debug('Personal API keys enabled')
|
||||
elif enable:
|
||||
if not api_key:
|
||||
missing_list.append(localize('api.key'))
|
||||
log_list.append('api_key')
|
||||
if not client_id:
|
||||
missing_list.append(localize('api.id'))
|
||||
log_list.append('client_id')
|
||||
if not client_secret:
|
||||
missing_list.append(localize('api.secret'))
|
||||
log_list.append('client_secret')
|
||||
ui.show_notification(localize('api.personal.failed',
|
||||
', '.join(missing_list)))
|
||||
self.log.error_trace(('Failed to enable personal API keys',
|
||||
'Missing: %s'),
|
||||
log_list)
|
||||
@@ -0,0 +1,193 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2018-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 json
|
||||
import os
|
||||
from io import open
|
||||
|
||||
from .. import logging
|
||||
from ..constants import DATA_PATH, FILE_READ, FILE_WRITE
|
||||
from ..utils.convert_format import to_unicode
|
||||
from ..utils.file_system import make_dirs
|
||||
from ..utils.methods import merge_dicts
|
||||
|
||||
|
||||
class JSONStore(object):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
BASE_PATH = make_dirs(DATA_PATH)
|
||||
|
||||
_process_data = None
|
||||
|
||||
def __init__(self, filename, context):
|
||||
if self.BASE_PATH:
|
||||
self.filepath = os.path.join(self.BASE_PATH, filename)
|
||||
else:
|
||||
self.log.error_trace(('Addon data directory not available',
|
||||
'Path: %s'),
|
||||
DATA_PATH,
|
||||
stacklevel=2)
|
||||
self.filepath = None
|
||||
|
||||
self._context = context
|
||||
self._loaded = False
|
||||
self._data = {}
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
if self.load(stacklevel=4):
|
||||
self._loaded = True
|
||||
self.set_defaults()
|
||||
else:
|
||||
self.set_defaults(reset=True)
|
||||
return self._loaded
|
||||
|
||||
def set_defaults(self, reset=False):
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, data, update=False, process=True, ipc=True, stacklevel=2):
|
||||
filepath = self.filepath
|
||||
if not filepath:
|
||||
return False
|
||||
|
||||
if update:
|
||||
data = merge_dicts(self._data, data)
|
||||
if data == self._data:
|
||||
self.log.debug(('Data unchanged', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
return None
|
||||
self.log.debug(('Saving', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
try:
|
||||
if not data:
|
||||
raise ValueError
|
||||
_data = json.dumps(
|
||||
data, ensure_ascii=False, indent=4, sort_keys=True
|
||||
)
|
||||
self._data = json.loads(
|
||||
_data,
|
||||
object_pairs_hook=(self._process_data if process else None),
|
||||
)
|
||||
|
||||
if ipc:
|
||||
self._context.get_ui().set_property(
|
||||
'-'.join((FILE_WRITE, filepath)),
|
||||
to_unicode(_data),
|
||||
log_value='<redacted>',
|
||||
)
|
||||
response = self._context.ipc_exec(
|
||||
FILE_WRITE,
|
||||
timeout=5,
|
||||
payload={'filepath': filepath},
|
||||
raise_exc=True,
|
||||
)
|
||||
if response is False:
|
||||
raise IOError
|
||||
if response is None:
|
||||
self.log.debug(('Data unchanged', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
return None
|
||||
else:
|
||||
with open(filepath, mode='w', encoding='utf-8') as file:
|
||||
file.write(to_unicode(_data))
|
||||
except (RuntimeError, IOError, OSError):
|
||||
self.log.exception(('Access error', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
except (TypeError, ValueError):
|
||||
self.log.exception(('Invalid data', 'Data: {data!r}'),
|
||||
data=data,
|
||||
stacklevel=stacklevel)
|
||||
self.set_defaults(reset=True)
|
||||
return False
|
||||
return True
|
||||
|
||||
def load(self, process=True, ipc=True, stacklevel=2):
|
||||
filepath = self.filepath
|
||||
if not filepath:
|
||||
return False
|
||||
|
||||
self.log.debug(('Loading', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
try:
|
||||
if ipc:
|
||||
if self._context.ipc_exec(
|
||||
FILE_READ,
|
||||
timeout=5,
|
||||
payload={'filepath': filepath},
|
||||
raise_exc=True,
|
||||
) is not False:
|
||||
data = self._context.get_ui().get_property(
|
||||
'-'.join((FILE_READ, filepath)),
|
||||
log_value='<redacted>',
|
||||
)
|
||||
else:
|
||||
raise IOError
|
||||
else:
|
||||
with open(filepath, mode='r', encoding='utf-8') as file:
|
||||
data = file.read()
|
||||
if not data:
|
||||
raise ValueError
|
||||
self._data = json.loads(
|
||||
data,
|
||||
object_pairs_hook=(self._process_data if process else None),
|
||||
)
|
||||
except (RuntimeError, IOError, OSError):
|
||||
self.log.exception(('Access error', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
except (TypeError, ValueError):
|
||||
self.log.exception(('Invalid data', 'Data: {data!r}'),
|
||||
data=data,
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_data(self, process=True, fallback=True, stacklevel=2):
|
||||
if not self._loaded:
|
||||
self.init()
|
||||
data = self._data
|
||||
|
||||
try:
|
||||
if not data:
|
||||
raise ValueError
|
||||
return json.loads(
|
||||
json.dumps(data, ensure_ascii=False),
|
||||
object_pairs_hook=(self._process_data if process else None),
|
||||
)
|
||||
except (TypeError, ValueError) as exc:
|
||||
self.log.exception(('Invalid data', 'Data: {data!r}'),
|
||||
data=data,
|
||||
stacklevel=stacklevel)
|
||||
if fallback:
|
||||
self.set_defaults(reset=True)
|
||||
return self.get_data(process=process, fallback=False)
|
||||
if self._loaded:
|
||||
raise exc
|
||||
return data
|
||||
|
||||
def load_data(self, data, process=True, stacklevel=2):
|
||||
try:
|
||||
return json.loads(
|
||||
data,
|
||||
object_pairs_hook=(self._process_data if process else None),
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
self.log.exception(('Invalid data', 'Data: {data!r}'),
|
||||
data=data,
|
||||
stacklevel=stacklevel)
|
||||
return {}
|
||||
@@ -0,0 +1,619 @@
|
||||
# -*- 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 logging
|
||||
import sys
|
||||
from os.path import normpath
|
||||
from pprint import PrettyPrinter
|
||||
from string import Formatter
|
||||
from sys import exc_info as sys_exc_info
|
||||
from traceback import extract_stack, format_list
|
||||
|
||||
from .compatibility import StringIO, string_type, to_str, xbmc
|
||||
from .constants import ADDON_ID
|
||||
from .utils.convert_format import to_unicode
|
||||
from .utils.system_version import current_system_version
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
__all__ = (
|
||||
'check_frame',
|
||||
'critical',
|
||||
'debug',
|
||||
'debugging',
|
||||
'error',
|
||||
'exception',
|
||||
'info',
|
||||
'log',
|
||||
'warning',
|
||||
'CRITICAL',
|
||||
'DEBUG',
|
||||
'ERROR',
|
||||
'INFO',
|
||||
'WARNING',
|
||||
)
|
||||
|
||||
|
||||
class RecordFormatter(logging.Formatter):
|
||||
def formatMessage(self, record):
|
||||
record.__dict__['__sep__'] = '\n' if '\n' in record.message else ' - '
|
||||
try:
|
||||
return self._style.format(record)
|
||||
except AttributeError:
|
||||
try:
|
||||
return self._fmt % record.__dict__
|
||||
except UnicodeDecodeError as e:
|
||||
record.__dict__ = {
|
||||
key: to_unicode(value)
|
||||
for key, value in record.__dict__.items()
|
||||
}
|
||||
try:
|
||||
return self._fmt % record.__dict__
|
||||
except UnicodeDecodeError:
|
||||
raise e
|
||||
|
||||
def formatStack(self, stack_info):
|
||||
return stack_info
|
||||
|
||||
def format(self, record):
|
||||
record.message = to_unicode(record.getMessage())
|
||||
if self.usesTime():
|
||||
record.asctime = self.formatTime(record, self.datefmt)
|
||||
s = self.formatMessage(record)
|
||||
if record.exc_info:
|
||||
if not record.exc_text:
|
||||
record.exc_text = self.formatException(record.exc_info)
|
||||
if record.exc_text:
|
||||
if record.stack_info:
|
||||
if s[-1:] != '\n':
|
||||
s += '\n\n'
|
||||
s += self.formatStack(record.stack_info)
|
||||
if s[-1:] != '\n':
|
||||
s += '\n\n'
|
||||
s += record.exc_text
|
||||
elif record.stack_info:
|
||||
if s[-1:] != '\n':
|
||||
s += '\n\n'
|
||||
s += self.formatStack(record.stack_info)
|
||||
return s
|
||||
|
||||
|
||||
class StreamWrapper(object):
|
||||
OPEN = frozenset(('(', '[', '{'))
|
||||
CLOSE = frozenset((')', ']', '}'))
|
||||
|
||||
def __init__(self, stream, indent_per_level, level, indent):
|
||||
self.stream = stream
|
||||
self.indent_per_level = indent_per_level
|
||||
self.level = level
|
||||
self.indent = indent
|
||||
self.previous_indent = 0
|
||||
self.previous_out = ''
|
||||
|
||||
def update_level(self, level, indent):
|
||||
self.level = level
|
||||
self.indent = indent
|
||||
|
||||
def write(self, out):
|
||||
write = self.stream.write
|
||||
indent = self.indent
|
||||
out = to_unicode(out)
|
||||
if '\n' in out:
|
||||
write(to_str(out))
|
||||
elif out in self.OPEN:
|
||||
write(to_str(out))
|
||||
write('\n' + (1 + indent) * ' ')
|
||||
elif out in self.CLOSE:
|
||||
if self.previous_out not in self.CLOSE:
|
||||
if indent == self.previous_indent:
|
||||
indent = (self.level - 1) * self.indent_per_level
|
||||
write('\n' + indent * ' ')
|
||||
write(to_str(out))
|
||||
else:
|
||||
write(to_str(out))
|
||||
self.previous_indent = indent
|
||||
self.previous_out = out
|
||||
|
||||
|
||||
class VariableWidthPrettyPrinter(PrettyPrinter, object):
|
||||
def _format(self, object, stream, indent, allowance, context, level):
|
||||
if not isinstance(object, string_type):
|
||||
indent = level * self._indent_per_level
|
||||
|
||||
if level:
|
||||
stream.update_level(level, indent)
|
||||
else:
|
||||
stream = StreamWrapper(
|
||||
stream,
|
||||
self._indent_per_level,
|
||||
level,
|
||||
indent,
|
||||
)
|
||||
|
||||
super(VariableWidthPrettyPrinter, self)._format(
|
||||
object=object,
|
||||
stream=stream,
|
||||
indent=indent,
|
||||
allowance=allowance,
|
||||
context=context,
|
||||
level=level,
|
||||
)
|
||||
|
||||
|
||||
class PrettyPrintFormatter(Formatter):
|
||||
_pretty_printer = VariableWidthPrettyPrinter(indent=4, width=160)
|
||||
|
||||
def convert_field(self, value, conversion):
|
||||
if conversion == 'r':
|
||||
return self._pretty_printer.pformat(value)
|
||||
if conversion in {'d', 'e', 't', 'w'}:
|
||||
_sort_dicts = sort_dicts = getattr(self._pretty_printer,
|
||||
'_sort_dicts',
|
||||
None)
|
||||
width = self._pretty_printer._width
|
||||
# __dict__
|
||||
if conversion == 'd':
|
||||
if sort_dicts:
|
||||
_sort_dicts = False
|
||||
try:
|
||||
value = getattr(value, '__repr_data__')()
|
||||
except AttributeError:
|
||||
if not isinstance(value, dict):
|
||||
value = {
|
||||
attr: getattr(value, attr, None)
|
||||
for attr in dir(value)
|
||||
}
|
||||
# eval iterators
|
||||
elif conversion == 'e':
|
||||
if (getattr(value, '__iter__', None)
|
||||
and not getattr(value, '__len__', None)):
|
||||
value = tuple(value)
|
||||
if sort_dicts:
|
||||
_sort_dicts = False
|
||||
# text representation
|
||||
elif conversion == 't':
|
||||
try:
|
||||
value = getattr(value, '__str_parts__')(as_dict=True)
|
||||
if sort_dicts:
|
||||
_sort_dicts = False
|
||||
except AttributeError:
|
||||
pass
|
||||
# wide output
|
||||
elif conversion == 'w':
|
||||
self._pretty_printer._width = 2 * width
|
||||
if _sort_dicts != sort_dicts:
|
||||
self._pretty_printer._sort_dicts = _sort_dicts
|
||||
out = self._pretty_printer.pformat(value)
|
||||
if sort_dicts:
|
||||
self._pretty_printer._sort_dicts = sort_dicts
|
||||
self._pretty_printer._width = width
|
||||
return out
|
||||
return super(PrettyPrintFormatter, self).convert_field(
|
||||
value,
|
||||
conversion,
|
||||
)
|
||||
|
||||
if not current_system_version.compatible(19):
|
||||
def parse(self, *args, **kwargs):
|
||||
output = super(PrettyPrintFormatter, self).parse(*args, **kwargs)
|
||||
return (
|
||||
(to_str(literal_text), field_name, format_spec, conversion)
|
||||
for literal_text, field_name, format_spec, conversion in output
|
||||
)
|
||||
|
||||
def format_field(self, *args, **kwargs):
|
||||
return to_str(
|
||||
super(PrettyPrintFormatter, self).format_field(*args, **kwargs)
|
||||
)
|
||||
|
||||
|
||||
class MessageFormatter(object):
|
||||
_formatter = PrettyPrintFormatter()
|
||||
|
||||
__slots__ = (
|
||||
'args',
|
||||
'kwargs',
|
||||
'msg',
|
||||
)
|
||||
|
||||
def __init__(self, msg, *args, **kwargs):
|
||||
self.msg = msg
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __str__(self):
|
||||
return self._formatter.vformat(self.msg, self.args, self.kwargs)
|
||||
|
||||
|
||||
class Handler(logging.Handler):
|
||||
LEVELS = {
|
||||
logging.NOTSET: xbmc.LOGNONE,
|
||||
logging.DEBUG: xbmc.LOGDEBUG,
|
||||
# logging.INFO: xbmc.LOGINFO,
|
||||
logging.INFO: xbmc.LOGNOTICE,
|
||||
logging.WARN: xbmc.LOGWARNING,
|
||||
logging.WARNING: xbmc.LOGWARNING,
|
||||
logging.ERROR: xbmc.LOGERROR,
|
||||
logging.CRITICAL: xbmc.LOGFATAL,
|
||||
}
|
||||
STANDARD_FORMATTER = RecordFormatter(
|
||||
fmt='[%(addon_id)s] %(module)s:%(lineno)d(%(funcName)s)'
|
||||
'%(__sep__)s%(message)s',
|
||||
)
|
||||
DEBUG_FORMATTER = RecordFormatter(
|
||||
fmt='[%(addon_id)s] %(module)s, line %(lineno)d, in %(funcName)s'
|
||||
'\n%(message)s',
|
||||
)
|
||||
|
||||
_stack_info = False
|
||||
|
||||
def __init__(self, level):
|
||||
super(Handler, self).__init__(level=level)
|
||||
self.setFormatter(self.STANDARD_FORMATTER)
|
||||
|
||||
def emit(self, record):
|
||||
record.addon_id = ADDON_ID
|
||||
xbmc.log(
|
||||
msg=self.format(record),
|
||||
level=self.LEVELS.get(record.levelno, xbmc.LOGDEBUG),
|
||||
)
|
||||
|
||||
def format(self, record):
|
||||
if self.stack_info:
|
||||
fmt = self.DEBUG_FORMATTER
|
||||
else:
|
||||
fmt = self.STANDARD_FORMATTER
|
||||
return fmt.format(record)
|
||||
|
||||
@property
|
||||
def stack_info(self):
|
||||
return self._stack_info
|
||||
|
||||
@stack_info.setter
|
||||
def stack_info(self, value):
|
||||
type(self)._stack_info = value
|
||||
|
||||
|
||||
class LogRecord(logging.LogRecord):
|
||||
def __init__(self, name, level, pathname, lineno, msg, args, exc_info,
|
||||
func=None, **kwargs):
|
||||
stack_info = kwargs.pop('sinfo', None)
|
||||
super(LogRecord, self).__init__(name,
|
||||
level,
|
||||
pathname,
|
||||
lineno,
|
||||
msg,
|
||||
args,
|
||||
exc_info,
|
||||
func=func,
|
||||
**kwargs)
|
||||
self.stack_info = stack_info
|
||||
|
||||
if not current_system_version.compatible(19):
|
||||
def getMessage(self):
|
||||
msg = self.msg
|
||||
if isinstance(msg, MessageFormatter):
|
||||
msg = msg.__str__()
|
||||
else:
|
||||
msg = to_str(msg)
|
||||
if self.args:
|
||||
msg = msg % self.args
|
||||
return msg
|
||||
|
||||
|
||||
class KodiLogger(logging.Logger):
|
||||
_verbose_logging = False
|
||||
_stack_info = False
|
||||
|
||||
def __init__(self, name, level=logging.DEBUG):
|
||||
super(KodiLogger, self).__init__(name=name, level=level)
|
||||
self.propagate = False
|
||||
self.addHandler(Handler(level=logging.DEBUG))
|
||||
|
||||
def _log(self,
|
||||
level,
|
||||
msg,
|
||||
args,
|
||||
exc_info=None,
|
||||
extra=None,
|
||||
stack_info=False,
|
||||
stacklevel=1,
|
||||
**kwargs):
|
||||
if isinstance(msg, (list, tuple)):
|
||||
msg = '\n'.join(map(to_str, msg))
|
||||
if kwargs:
|
||||
msg = MessageFormatter(msg, *args, **kwargs)
|
||||
args = ()
|
||||
elif args and args[0] == '*(' and args[-1] == ')':
|
||||
msg = MessageFormatter(msg, *args[1:-1], **kwargs)
|
||||
args = ()
|
||||
|
||||
stack_info = stack_info and (exc_info or self.stack_info)
|
||||
sinfo = None
|
||||
if _srcfiles:
|
||||
try:
|
||||
fn, lno, func, sinfo = self.findCaller(stack_info, stacklevel)
|
||||
except ValueError:
|
||||
fn, lno, func = '(unknown file)', 0, '(unknown function)'
|
||||
else:
|
||||
fn, lno, func = '(unknown file)', 0, '(unknown function)'
|
||||
|
||||
if exc_info:
|
||||
if isinstance(exc_info, BaseException):
|
||||
exc_info = (type(exc_info), exc_info, exc_info.__traceback__)
|
||||
elif not isinstance(exc_info, tuple):
|
||||
exc_info = sys_exc_info()
|
||||
|
||||
record = self.makeRecord(self.name, level, fn, lno, msg, args,
|
||||
exc_info, func, extra, sinfo)
|
||||
self.handle(record)
|
||||
|
||||
def findCaller(self, stack_info=False, stacklevel=1):
|
||||
target_frame = logging.currentframe()
|
||||
if target_frame is None:
|
||||
return '(unknown file)', 0, '(unknown function)', None
|
||||
|
||||
last_frame = None
|
||||
while stacklevel > 0:
|
||||
next_frame = target_frame.f_back
|
||||
if next_frame is None:
|
||||
break
|
||||
target_frame = next_frame
|
||||
stacklevel, is_internal = check_frame(target_frame, stacklevel)
|
||||
if is_internal:
|
||||
continue
|
||||
if last_frame is None:
|
||||
last_frame = target_frame
|
||||
stacklevel -= 1
|
||||
|
||||
if stack_info:
|
||||
with StringIO() as output:
|
||||
output.write('Stack (most recent call last):\n')
|
||||
for item in format_list(extract_stack(last_frame)):
|
||||
output.write(item)
|
||||
stack_info = output.getvalue()
|
||||
if stack_info[-1] == '\n':
|
||||
stack_info = stack_info[:-1]
|
||||
else:
|
||||
stack_info = None
|
||||
|
||||
target_frame_code = target_frame.f_code
|
||||
return (target_frame_code.co_filename,
|
||||
target_frame.f_lineno,
|
||||
target_frame_code.co_name,
|
||||
stack_info)
|
||||
|
||||
def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
|
||||
func=None, extra=None, sinfo=None):
|
||||
rv = LogRecord(name,
|
||||
level,
|
||||
fn,
|
||||
lno,
|
||||
msg,
|
||||
args,
|
||||
exc_info,
|
||||
func=func,
|
||||
sinfo=sinfo)
|
||||
if extra is not None:
|
||||
for key in extra:
|
||||
if (key in ["message", "asctime"]) or (key in rv.__dict__):
|
||||
raise KeyError("Attempt to overwrite %r in LogRecord" % key)
|
||||
rv.__dict__[key] = extra[key]
|
||||
return rv
|
||||
|
||||
def exception(self, msg, *args, **kwargs):
|
||||
if self.isEnabledFor(ERROR):
|
||||
self._log(
|
||||
ERROR,
|
||||
msg,
|
||||
args,
|
||||
exc_info=kwargs.pop('exc_info', True),
|
||||
stack_info=kwargs.pop('stack_info', True),
|
||||
stacklevel=kwargs.pop('stacklevel', 1),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def error_trace(self, msg, *args, **kwargs):
|
||||
if self.isEnabledFor(ERROR):
|
||||
self._log(
|
||||
ERROR,
|
||||
msg,
|
||||
args,
|
||||
stack_info=kwargs.pop('stack_info', True),
|
||||
stacklevel=kwargs.pop('stacklevel', 1),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def warning_trace(self, msg, *args, **kwargs):
|
||||
if self.isEnabledFor(WARNING):
|
||||
self._log(
|
||||
WARNING,
|
||||
msg,
|
||||
args,
|
||||
stack_info=kwargs.pop('stack_info', True),
|
||||
stacklevel=kwargs.pop('stacklevel', 1),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def debug_trace(self, msg, *args, **kwargs):
|
||||
if self.isEnabledFor(DEBUG):
|
||||
self._log(
|
||||
DEBUG,
|
||||
msg,
|
||||
args,
|
||||
stack_info=kwargs.pop('stack_info', True),
|
||||
stacklevel=kwargs.pop('stacklevel', 1),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@property
|
||||
def debugging(self):
|
||||
return self.isEnabledFor(logging.DEBUG)
|
||||
|
||||
@debugging.setter
|
||||
def debugging(self, value):
|
||||
if value:
|
||||
Handler.LEVELS[logging.DEBUG] = xbmc.LOGNOTICE
|
||||
self.setLevel(logging.DEBUG)
|
||||
root.setLevel(logging.DEBUG)
|
||||
else:
|
||||
Handler.LEVELS[logging.DEBUG] = xbmc.LOGDEBUG
|
||||
self.setLevel(logging.INFO)
|
||||
root.setLevel(logging.INFO)
|
||||
|
||||
@property
|
||||
def stack_info(self):
|
||||
return self._stack_info
|
||||
|
||||
@stack_info.setter
|
||||
def stack_info(self, value):
|
||||
if value:
|
||||
type(self)._stack_info = True
|
||||
Handler.stack_info = True
|
||||
else:
|
||||
type(self)._stack_info = False
|
||||
Handler.stack_info = False
|
||||
|
||||
@property
|
||||
def verbose_logging(self):
|
||||
return self._verbose_logging
|
||||
|
||||
@verbose_logging.setter
|
||||
def verbose_logging(self, value):
|
||||
cls = type(self)
|
||||
if value:
|
||||
cls._verbose_logging = True
|
||||
logging.root = root
|
||||
logging.Logger.root = root
|
||||
logging.Logger.manager = manager
|
||||
logging.Logger.manager.setLoggerClass(KodiLogger)
|
||||
logging.setLoggerClass(KodiLogger)
|
||||
else:
|
||||
if cls._verbose_logging:
|
||||
logging.root = logging.RootLogger(logging.WARNING)
|
||||
logging.Logger.root = logging.root
|
||||
logging.Logger.manager = logging.Manager(logging.root)
|
||||
logging.Logger.manager.setLoggerClass(logging.Logger)
|
||||
logging.setLoggerClass(logging.Logger)
|
||||
cls._verbose_logging = False
|
||||
|
||||
|
||||
class RootLogger(KodiLogger):
|
||||
def __init__(self, level):
|
||||
super(RootLogger, self).__init__('root', level)
|
||||
|
||||
def __reduce__(self):
|
||||
return getLogger, ()
|
||||
|
||||
|
||||
root = RootLogger(logging.INFO)
|
||||
KodiLogger.root = root
|
||||
manager = logging.Manager(root)
|
||||
KodiLogger.manager = manager
|
||||
KodiLogger.manager.setLoggerClass(KodiLogger)
|
||||
|
||||
critical = root.critical
|
||||
error = root.error
|
||||
warning = root.warning
|
||||
info = root.info
|
||||
debug = root.debug
|
||||
log = root.log
|
||||
|
||||
CRITICAL = logging.CRITICAL
|
||||
ERROR = logging.ERROR
|
||||
WARNING = logging.WARNING
|
||||
INFO = logging.INFO
|
||||
DEBUG = logging.DEBUG
|
||||
|
||||
|
||||
def exception(msg, *args, **kwargs):
|
||||
root.error(msg,
|
||||
*args,
|
||||
exc_info=kwargs.pop('exc_info', True),
|
||||
stack_info=kwargs.pop('stack_info', True),
|
||||
stacklevel=kwargs.pop('stacklevel', 1),
|
||||
**kwargs)
|
||||
|
||||
|
||||
def error_trace(msg, *args, **kwargs):
|
||||
root.error(msg,
|
||||
*args,
|
||||
stack_info=kwargs.pop('stack_info', True),
|
||||
stacklevel=kwargs.pop('stacklevel', 1),
|
||||
**kwargs)
|
||||
|
||||
|
||||
def warning_trace(msg, *args, **kwargs):
|
||||
root.warning(msg,
|
||||
*args,
|
||||
stack_info=kwargs.pop('stack_info', True),
|
||||
stacklevel=kwargs.pop('stacklevel', 1),
|
||||
**kwargs)
|
||||
|
||||
|
||||
def debug_trace(msg, *args, **kwargs):
|
||||
root.debug(msg,
|
||||
*args,
|
||||
stack_info=kwargs.pop('stack_info', True),
|
||||
stacklevel=kwargs.pop('stacklevel', 1),
|
||||
**kwargs)
|
||||
|
||||
|
||||
def getLogger(name=None):
|
||||
if not name or isinstance(name, string_type) and name == root.name:
|
||||
return root
|
||||
return KodiLogger.manager.getLogger(name)
|
||||
|
||||
|
||||
_srcfiles = {
|
||||
normpath(getLogger.__code__.co_filename).lower(),
|
||||
normpath(logging.getLogger.__code__.co_filename).lower(),
|
||||
}
|
||||
|
||||
|
||||
def check_frame(frame, stacklevel=None, skip_paths=None):
|
||||
filename = normpath(frame.f_code.co_filename).lower()
|
||||
is_internal = (
|
||||
filename in _srcfiles
|
||||
or ('importlib' in filename and '_bootstrap' in filename)
|
||||
or (skip_paths
|
||||
and any(skip_path in filename for skip_path in skip_paths))
|
||||
)
|
||||
if stacklevel is None:
|
||||
return is_internal
|
||||
|
||||
if (ADDON_ID in filename and filename.endswith((
|
||||
'function_cache.py',
|
||||
'abstract_settings.py',
|
||||
'xbmc_items.py',
|
||||
))):
|
||||
stacklevel += 1
|
||||
return stacklevel, is_internal
|
||||
|
||||
|
||||
__original_module__ = sys.modules[__name__]
|
||||
|
||||
|
||||
class ModuleProperties(__original_module__.__class__, object):
|
||||
__name__ = __original_module__.__name__
|
||||
__file__ = __original_module__.__file__
|
||||
__getattribute__ = __original_module__.__getattribute__
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item == 'debugging':
|
||||
return root.isEnabledFor(logging.DEBUG)
|
||||
raise AttributeError(
|
||||
'module \'{}\' has no attribute \'{}\''.format(__name__, item)
|
||||
)
|
||||
|
||||
|
||||
sys.modules[__name__] = ModuleProperties(__name__, __doc__)
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- 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
|
||||
|
||||
from .player_monitor import PlayerMonitor
|
||||
from .service_monitor import ServiceMonitor
|
||||
|
||||
|
||||
__all__ = (
|
||||
'PlayerMonitor',
|
||||
'ServiceMonitor',
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,465 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2018-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 json
|
||||
import threading
|
||||
|
||||
from .. import logging
|
||||
from ..compatibility import xbmc
|
||||
from ..constants import (
|
||||
BUSY_FLAG,
|
||||
CHANNEL_ID,
|
||||
PATHS,
|
||||
PLAYBACK_STARTED,
|
||||
PLAYBACK_STOPPED,
|
||||
PLAYER_DATA,
|
||||
PLAY_USING,
|
||||
REFRESH_CONTAINER,
|
||||
TRAKT_PAUSE_FLAG,
|
||||
VIDEO_ID,
|
||||
)
|
||||
from ..utils.redact import redact_params
|
||||
|
||||
|
||||
class PlayerMonitorThread(threading.Thread):
|
||||
def __init__(self, player, provider, context, monitor, player_data):
|
||||
self.player_data = player_data
|
||||
video_id = player_data.get(VIDEO_ID)
|
||||
self.video_id = video_id
|
||||
self.channel_id = player_data.get(CHANNEL_ID)
|
||||
self.video_status = player_data.get('video_status')
|
||||
|
||||
self._stopped = threading.Event()
|
||||
self._ended = threading.Event()
|
||||
|
||||
self._player = player
|
||||
self._provider = provider
|
||||
self._context = context
|
||||
self._monitor = monitor
|
||||
|
||||
self.current_time = 0.0
|
||||
self.total_time = 0.0
|
||||
self.progress = 0
|
||||
|
||||
name = '{class_name}[{video_id}]'.format(
|
||||
class_name=self.__class__.__name__,
|
||||
video_id=video_id,
|
||||
)
|
||||
self.log = logging.getLogger(name)
|
||||
|
||||
super(PlayerMonitorThread, self).__init__(name=name)
|
||||
self.daemon = True
|
||||
self.start()
|
||||
|
||||
def abort_now(self):
|
||||
return (not self._player.isPlaying()
|
||||
or self._context.abort_requested()
|
||||
or self.stopped())
|
||||
|
||||
def run(self):
|
||||
video_id = self.video_id
|
||||
playing_file = self.player_data.get('playing_file')
|
||||
play_count = self.player_data.get('play_count', 0)
|
||||
use_remote_history = self.player_data.get('use_remote_history', False)
|
||||
use_local_history = self.player_data.get('use_local_history', False)
|
||||
playback_stats = self.player_data.get('playback_stats', {})
|
||||
refresh_only = self.player_data.get('refresh_only', False)
|
||||
clip = self.player_data.get('clip', False)
|
||||
|
||||
context = self._context
|
||||
log = self.log
|
||||
monitor = self._monitor
|
||||
player = self._player
|
||||
provider = self._provider
|
||||
|
||||
log.debug('Starting')
|
||||
|
||||
timeout_period = 5
|
||||
waited = 0
|
||||
wait_interval = 0.5
|
||||
while not player.isPlaying():
|
||||
if context.abort_requested():
|
||||
break
|
||||
if waited >= timeout_period:
|
||||
self.end()
|
||||
return
|
||||
|
||||
log.debug('Waiting for playback to start')
|
||||
monitor.waitForAbort(wait_interval)
|
||||
waited += wait_interval
|
||||
else:
|
||||
context.send_notification(PLAYBACK_STARTED, {
|
||||
VIDEO_ID: video_id,
|
||||
CHANNEL_ID: self.channel_id,
|
||||
'status': self.video_status,
|
||||
})
|
||||
|
||||
client = provider.get_client(context)
|
||||
logged_in = client.logged_in
|
||||
report_url = use_remote_history and playback_stats.get('playback_url')
|
||||
state = 'playing'
|
||||
|
||||
if report_url:
|
||||
client.update_watch_history(
|
||||
video_id,
|
||||
report_url,
|
||||
)
|
||||
|
||||
access_manager = context.get_access_manager()
|
||||
settings = context.get_settings()
|
||||
playlist_player = context.get_playlist_player()
|
||||
|
||||
video_id_param = 'video_id=%s' % video_id
|
||||
report_url = use_remote_history and playback_stats.get('watchtime_url')
|
||||
|
||||
segment_start = 0.0
|
||||
report_time = -1.0
|
||||
wait_interval = 1
|
||||
report_period = waited = 10
|
||||
while not self.abort_now():
|
||||
try:
|
||||
current_file = player.getPlayingFile()
|
||||
played_time = player.getTime()
|
||||
total_time = player.getTotalTime()
|
||||
if not player.seeking:
|
||||
player.current_time = played_time
|
||||
player.total_time = total_time
|
||||
except RuntimeError:
|
||||
self.stop()
|
||||
break
|
||||
|
||||
if (not current_file.startswith(playing_file) and not (
|
||||
context.is_plugin_path(current_file, PATHS.PLAY)
|
||||
and video_id_param in current_file
|
||||
)) or total_time <= 0:
|
||||
self.stop()
|
||||
break
|
||||
|
||||
_seek_time = player.start_time or player.seek_time
|
||||
if waited and _seek_time and played_time < _seek_time:
|
||||
waited = 0
|
||||
player.seekTime(_seek_time)
|
||||
continue
|
||||
|
||||
if player.end_time and played_time >= player.end_time:
|
||||
if waited and clip and player.start_time:
|
||||
waited = 0
|
||||
player.seekTime(player.start_time)
|
||||
continue
|
||||
if playlist_player.size() > 1:
|
||||
playlist_player.play_playlist_item('next')
|
||||
else:
|
||||
player.stop()
|
||||
|
||||
if waited >= report_period:
|
||||
waited = 0
|
||||
|
||||
last_state = state
|
||||
if played_time == report_time:
|
||||
state = 'paused'
|
||||
else:
|
||||
state = 'playing'
|
||||
report_time = played_time
|
||||
|
||||
if logged_in and report_url:
|
||||
if state == 'playing':
|
||||
segment_end = played_time
|
||||
else:
|
||||
segment_end = segment_start
|
||||
|
||||
if segment_start > segment_end:
|
||||
segment_end = segment_start + report_period
|
||||
|
||||
if segment_end > total_time:
|
||||
segment_end = total_time
|
||||
|
||||
# only report state='paused' once
|
||||
if state == 'playing' or last_state == 'playing':
|
||||
client = provider.get_client(context)
|
||||
logged_in = client.logged_in
|
||||
|
||||
if logged_in:
|
||||
client.update_watch_history(
|
||||
video_id,
|
||||
report_url,
|
||||
status=(
|
||||
played_time,
|
||||
segment_start,
|
||||
segment_end,
|
||||
state,
|
||||
),
|
||||
)
|
||||
|
||||
segment_start = segment_end
|
||||
|
||||
monitor.waitForAbort(wait_interval)
|
||||
waited += wait_interval
|
||||
|
||||
self.current_time = player.current_time
|
||||
self.total_time = player.total_time
|
||||
if self.total_time > 0:
|
||||
self.progress = int(100 * self.current_time / self.total_time)
|
||||
|
||||
if logged_in:
|
||||
client = provider.get_client(context)
|
||||
logged_in = client.logged_in
|
||||
|
||||
if self.video_status.get('live'):
|
||||
play_count += 1
|
||||
segment_end = self.current_time
|
||||
play_data = {
|
||||
'play_count': play_count,
|
||||
'total_time': 0,
|
||||
'played_time': 0,
|
||||
'played_percent': 0,
|
||||
}
|
||||
else:
|
||||
if self.progress >= settings.get_play_count_min_percent():
|
||||
play_count += 1
|
||||
self.current_time = 0
|
||||
segment_end = self.total_time
|
||||
else:
|
||||
segment_end = self.current_time
|
||||
refresh_only = True
|
||||
play_data = {
|
||||
'play_count': play_count,
|
||||
'total_time': self.total_time,
|
||||
'played_time': self.current_time,
|
||||
'played_percent': self.progress,
|
||||
}
|
||||
self.player_data['play_data'] = play_data
|
||||
|
||||
if logged_in and report_url:
|
||||
client.update_watch_history(
|
||||
video_id,
|
||||
report_url,
|
||||
status=(
|
||||
segment_end,
|
||||
segment_end,
|
||||
segment_end,
|
||||
'stopped',
|
||||
),
|
||||
)
|
||||
if use_local_history:
|
||||
context.get_playback_history().set_item(video_id, play_data)
|
||||
|
||||
context.send_notification(PLAYBACK_STOPPED, self.player_data)
|
||||
log.debug('Playback stopped:'
|
||||
' {played_time:.3f} secs of {total_time:.3f}'
|
||||
' @ {played_percent}%,'
|
||||
' played {play_count} time(s)',
|
||||
**play_data)
|
||||
|
||||
if refresh_only:
|
||||
pass
|
||||
elif settings.get_bool(settings.WATCH_LATER_REMOVE, True):
|
||||
watch_later_id = logged_in and access_manager.get_watch_later_id()
|
||||
if not watch_later_id:
|
||||
context.get_watch_later_list().del_item(video_id)
|
||||
elif watch_later_id.lower() == 'wl':
|
||||
provider.on_playlist_x(
|
||||
provider,
|
||||
context,
|
||||
command='remove',
|
||||
category='video',
|
||||
playlist_id=watch_later_id,
|
||||
video_id=video_id,
|
||||
video_name='',
|
||||
confirmed=True,
|
||||
)
|
||||
else:
|
||||
playlist_item_id = client.get_playlist_item_id_of_video_id(
|
||||
playlist_id=watch_later_id,
|
||||
video_id=video_id,
|
||||
do_auth=True,
|
||||
)
|
||||
if playlist_item_id:
|
||||
provider.on_playlist_x(
|
||||
provider,
|
||||
context,
|
||||
command='remove',
|
||||
category='video',
|
||||
playlist_id=watch_later_id,
|
||||
video_id=playlist_item_id,
|
||||
video_name='',
|
||||
confirmed=True,
|
||||
)
|
||||
|
||||
if logged_in and not refresh_only:
|
||||
history_id = access_manager.get_watch_history_id()
|
||||
if history_id and history_id.lower() != 'hl':
|
||||
client.add_video_to_playlist(history_id, video_id)
|
||||
|
||||
# rate video
|
||||
if (settings.get_bool(settings.RATE_VIDEOS) and
|
||||
(settings.get_bool(settings.RATE_PLAYLISTS)
|
||||
or xbmc.PlayList(xbmc.PLAYLIST_VIDEO).size() < 2)):
|
||||
json_data = client.get_video_rating(video_id)
|
||||
if json_data:
|
||||
items = json_data.get('items', [{'rating': 'none'}])
|
||||
rating = items[0].get('rating', 'none')
|
||||
if rating == 'none':
|
||||
provider.on_video_x(
|
||||
provider,
|
||||
context,
|
||||
command='rate',
|
||||
video_id=video_id,
|
||||
current_rating=rating,
|
||||
)
|
||||
|
||||
if settings.get_bool(settings.PLAY_REFRESH):
|
||||
context.send_notification(REFRESH_CONTAINER)
|
||||
|
||||
self.end()
|
||||
|
||||
def stop(self):
|
||||
self.log.debug('Stop event set')
|
||||
self._stopped.set()
|
||||
|
||||
def stopped(self):
|
||||
return self._stopped.is_set()
|
||||
|
||||
def end(self):
|
||||
self.log.debug('End event set')
|
||||
self._ended.set()
|
||||
|
||||
def ended(self):
|
||||
return self._ended.is_set()
|
||||
|
||||
|
||||
class PlayerMonitor(xbmc.Player):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def __init__(self, provider, context, monitor):
|
||||
super(PlayerMonitor, self).__init__()
|
||||
self._provider = provider
|
||||
self._context = context
|
||||
self._monitor = monitor
|
||||
self._ui = self._context.get_ui()
|
||||
self.threads = []
|
||||
self.seeking = False
|
||||
self.seek_time = None
|
||||
self.start_time = None
|
||||
self.end_time = None
|
||||
self.current_time = None
|
||||
self.total_time = None
|
||||
|
||||
def stop_threads(self):
|
||||
for thread in self.threads:
|
||||
if thread.ended():
|
||||
continue
|
||||
|
||||
if not thread.stopped():
|
||||
self.log.debug('Stopping: %s', thread.name)
|
||||
thread.stop()
|
||||
|
||||
for thread in self.threads:
|
||||
if thread.stopped() and not thread.ended():
|
||||
try:
|
||||
thread.join(5)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def cleanup_threads(self, only_ended=True):
|
||||
active_threads = []
|
||||
active_thread_names = []
|
||||
for thread in self.threads:
|
||||
if only_ended and not thread.ended():
|
||||
active_threads.append(thread)
|
||||
active_thread_names.append(thread.name)
|
||||
continue
|
||||
|
||||
if thread.ended():
|
||||
self.log.debug('Clean up: %s', thread.name)
|
||||
else:
|
||||
self.log.debug('Stopping: %s', thread.name)
|
||||
if not thread.stopped():
|
||||
thread.stop()
|
||||
try:
|
||||
thread.join(5)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.log.debug('Active threads: %s', active_thread_names)
|
||||
self.threads = active_threads
|
||||
|
||||
def onPlayBackStarted(self):
|
||||
if not self._ui.busy_dialog_active():
|
||||
self._ui.clear_property(BUSY_FLAG)
|
||||
|
||||
if self._ui.get_property(PLAY_USING):
|
||||
self._context.execute('Action(SwitchPlayer)')
|
||||
self._context.execute('Action(Stop)')
|
||||
return
|
||||
|
||||
def onAVStarted(self):
|
||||
ui = self._ui
|
||||
if ui.get_property(PLAY_USING):
|
||||
return
|
||||
|
||||
if not ui.busy_dialog_active():
|
||||
ui.clear_property(BUSY_FLAG)
|
||||
|
||||
player_data = ui.pop_property(PLAYER_DATA,
|
||||
process=json.loads,
|
||||
log_process=redact_params)
|
||||
if not player_data:
|
||||
return
|
||||
self.cleanup_threads()
|
||||
|
||||
try:
|
||||
self.seek_time = float(player_data.get('seek_time'))
|
||||
self.start_time = float(player_data.get('start_time'))
|
||||
self.end_time = float(player_data.get('end_time'))
|
||||
self.current_time = max(0.0, self.getTime())
|
||||
self.total_time = max(0.0, self.getTotalTime())
|
||||
except (ValueError, TypeError, RuntimeError):
|
||||
self.seek_time = None
|
||||
self.start_time = None
|
||||
self.end_time = None
|
||||
self.current_time = 0.0
|
||||
self.total_time = 0.0
|
||||
|
||||
self.threads.append(PlayerMonitorThread(self,
|
||||
self._provider,
|
||||
self._context,
|
||||
self._monitor,
|
||||
player_data))
|
||||
|
||||
def onPlayBackEnded(self):
|
||||
ui = self._ui
|
||||
if not ui.busy_dialog_active():
|
||||
ui.clear_property(BUSY_FLAG)
|
||||
|
||||
ui.pop_property(PLAY_USING)
|
||||
ui.clear_property(TRAKT_PAUSE_FLAG, raw=True)
|
||||
|
||||
self.stop_threads()
|
||||
self.cleanup_threads()
|
||||
|
||||
def onPlayBackStopped(self):
|
||||
self.onPlayBackEnded()
|
||||
|
||||
def onPlayBackError(self):
|
||||
self.onPlayBackEnded()
|
||||
|
||||
def onPlayBackSeek(self, time, seekOffset):
|
||||
time_s = time / 1000
|
||||
self.seeking = True
|
||||
self.current_time = time_s
|
||||
self.seek_time = None
|
||||
if ((self.end_time and time_s > self.end_time + 1)
|
||||
or (self.start_time and time_s < self.start_time - 1)):
|
||||
self.start_time = None
|
||||
self.end_time = None
|
||||
|
||||
def onAVChange(self):
|
||||
self.seeking = False
|
||||
@@ -0,0 +1,500 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2018-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 json
|
||||
from io import open
|
||||
from threading import Event, Lock, Thread
|
||||
|
||||
from .. import logging
|
||||
from ..compatibility import urlsplit, xbmc, xbmcgui
|
||||
from ..constants import (
|
||||
ACTION,
|
||||
ADDON_ID,
|
||||
CHECK_SETTINGS,
|
||||
CONTAINER_FOCUS,
|
||||
CONTAINER_ID,
|
||||
CONTAINER_POSITION,
|
||||
CURRENT_ITEM,
|
||||
FILE_READ,
|
||||
FILE_WRITE,
|
||||
HAS_PARENT,
|
||||
MARK_AS_LABEL,
|
||||
PATHS,
|
||||
PLAYBACK_STOPPED,
|
||||
PLAYER_VIDEO_ID,
|
||||
PLAY_CANCELLED,
|
||||
PLAY_COUNT,
|
||||
PLAY_FORCED,
|
||||
PLUGIN_WAKEUP,
|
||||
REFRESH_CONTAINER,
|
||||
RELOAD_ACCESS_MANAGER,
|
||||
RESUMABLE,
|
||||
SERVER_WAKEUP,
|
||||
SERVICE_IPC,
|
||||
SYNC_LISTITEM,
|
||||
VIDEO_ID,
|
||||
)
|
||||
from ..network import get_connect_address, get_http_server, httpd_status
|
||||
from ..utils.methods import jsonrpc
|
||||
|
||||
|
||||
class ServiceMonitor(xbmc.Monitor):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_settings_changes = 0
|
||||
_settings_collect = False
|
||||
get_idle_time = xbmc.getGlobalIdleTime
|
||||
|
||||
def __init__(self, context):
|
||||
self._context = context
|
||||
|
||||
self._httpd_address = None
|
||||
self._httpd_port = None
|
||||
self._whitelist = None
|
||||
self._old_httpd_address = None
|
||||
self._old_httpd_port = None
|
||||
self._use_httpd = None
|
||||
self._httpd_error = False
|
||||
|
||||
self.httpd = None
|
||||
self.httpd_thread = None
|
||||
self.httpd_sleep_allowed = True
|
||||
|
||||
self.system_idle = False
|
||||
self.system_sleep = False
|
||||
self.refresh = False
|
||||
self.interrupt = False
|
||||
|
||||
self.file_access = {}
|
||||
|
||||
self.onSettingsChanged(force=True)
|
||||
|
||||
super(ServiceMonitor, self).__init__()
|
||||
|
||||
@staticmethod
|
||||
def send_notification(method,
|
||||
data=True,
|
||||
sender='.'.join((ADDON_ID, 'service'))):
|
||||
jsonrpc(method='JSONRPC.NotifyAll',
|
||||
params={'sender': sender,
|
||||
'message': method,
|
||||
'data': data})
|
||||
|
||||
def set_property(self,
|
||||
property_id,
|
||||
value='true',
|
||||
stacklevel=2,
|
||||
process=None,
|
||||
log_value=None,
|
||||
log_process=None,
|
||||
raw=False):
|
||||
if log_value is None:
|
||||
log_value = value
|
||||
if log_process:
|
||||
log_value = log_process(log_value)
|
||||
self.log.debug_trace('Set property {property_id!r}: {value!r}',
|
||||
property_id=property_id,
|
||||
value=log_value,
|
||||
stacklevel=stacklevel)
|
||||
_property_id = property_id if raw else '-'.join((ADDON_ID, property_id))
|
||||
if process:
|
||||
value = process(value)
|
||||
xbmcgui.Window(10000).setProperty(_property_id, value)
|
||||
return value
|
||||
|
||||
def refresh_container(self, force=False):
|
||||
if force:
|
||||
self.refresh = False
|
||||
refreshed = self._context.get_ui().refresh_container(force=force)
|
||||
if refreshed is None:
|
||||
self.refresh = True
|
||||
|
||||
def onNotification(self, sender, method, data):
|
||||
if sender == 'xbmc':
|
||||
if method == 'System.OnSleep':
|
||||
self.system_idle = True
|
||||
self.system_sleep = True
|
||||
|
||||
elif method in {
|
||||
'GUI.OnScreensaverActivated',
|
||||
'GUI.OnDPMSActivated',
|
||||
}:
|
||||
self.system_idle = True
|
||||
|
||||
elif method in {
|
||||
'GUI.OnScreensaverDeactivated',
|
||||
'GUI.OnDPMSDeactivated',
|
||||
'System.OnWake',
|
||||
}:
|
||||
self.system_idle = False
|
||||
self.system_sleep = False
|
||||
self.interrupt = True
|
||||
|
||||
elif method == 'Player.OnPlay':
|
||||
player = xbmc.Player()
|
||||
try:
|
||||
playing_file = urlsplit(player.getPlayingFile())
|
||||
if playing_file.path in {PATHS.MPD,
|
||||
PATHS.PLAY,
|
||||
PATHS.REDIRECT}:
|
||||
if not self.httpd:
|
||||
self.start_httpd()
|
||||
if self.httpd_sleep_allowed:
|
||||
self.httpd_sleep_allowed = None
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
elif method == 'Playlist.OnAdd':
|
||||
context = self._context
|
||||
|
||||
data = json.loads(data)
|
||||
position = data.get('position', 0)
|
||||
playlist_player = context.get_playlist_player()
|
||||
item_uri = playlist_player.get_item_path(position)
|
||||
|
||||
if context.is_plugin_path(item_uri):
|
||||
path, params = context.parse_uri(item_uri)
|
||||
if path.rstrip('/') != PATHS.PLAY:
|
||||
self.log.warning(('Playlist.OnAdd item is not playable',
|
||||
'Path: {path}',
|
||||
'Params: {params}'),
|
||||
path=path,
|
||||
params=params)
|
||||
self.set_property(PLAY_FORCED)
|
||||
elif params.get(ACTION) == 'list':
|
||||
playlist_player.stop()
|
||||
playlist_player.clear()
|
||||
self.log.warning(('Playlist.OnAdd item is a listing',
|
||||
'Path: {path}',
|
||||
'Params: {params}'),
|
||||
path=path,
|
||||
params=params)
|
||||
self.set_property(PLAY_CANCELLED)
|
||||
|
||||
return
|
||||
|
||||
if sender != ADDON_ID:
|
||||
return
|
||||
|
||||
group, separator, event = method.partition('.')
|
||||
|
||||
if event == SERVICE_IPC:
|
||||
if not isinstance(data, dict):
|
||||
data = json.loads(data)
|
||||
if not data:
|
||||
return
|
||||
|
||||
target = data.get('target')
|
||||
|
||||
if target == PLUGIN_WAKEUP:
|
||||
self.system_idle = False
|
||||
self.system_sleep = False
|
||||
self.interrupt = True
|
||||
response = True
|
||||
|
||||
elif target == SERVER_WAKEUP:
|
||||
if not self.httpd and self.httpd_required():
|
||||
response = self.start_httpd()
|
||||
else:
|
||||
response = bool(self.httpd)
|
||||
if self.httpd_sleep_allowed:
|
||||
self.httpd_sleep_allowed = None
|
||||
|
||||
elif target == CHECK_SETTINGS:
|
||||
state = data.get('state')
|
||||
if state == 'defer':
|
||||
self._settings_collect = True
|
||||
elif state == 'process':
|
||||
self.onSettingsChanged(force=True)
|
||||
elif state == 'ignore':
|
||||
self._settings_collect = -1
|
||||
response = True
|
||||
|
||||
elif target in {FILE_READ, FILE_WRITE}:
|
||||
response = None
|
||||
filepath = data.get('filepath')
|
||||
if filepath:
|
||||
if filepath not in self.file_access:
|
||||
read_access = Event()
|
||||
read_access.set()
|
||||
write_access = Lock()
|
||||
self.file_access[filepath] = (read_access, write_access)
|
||||
else:
|
||||
read_access, write_access = self.file_access[filepath]
|
||||
|
||||
if target == FILE_READ:
|
||||
try:
|
||||
with open(filepath, mode='r',
|
||||
encoding='utf-8') as file:
|
||||
read_access.wait()
|
||||
self.set_property(
|
||||
'-'.join((FILE_READ, filepath)),
|
||||
file.read(),
|
||||
log_value='<redacted>',
|
||||
)
|
||||
response = True
|
||||
except (IOError, OSError):
|
||||
response = False
|
||||
else:
|
||||
with write_access:
|
||||
content = self._context.get_ui().pop_property(
|
||||
'-'.join((FILE_WRITE, filepath)),
|
||||
log_value='<redacted>',
|
||||
)
|
||||
response = None
|
||||
if content:
|
||||
read_access.clear()
|
||||
try:
|
||||
with open(filepath, mode='w',
|
||||
encoding='utf-8') as file:
|
||||
file.write(content)
|
||||
response = True
|
||||
except (IOError, OSError):
|
||||
response = False
|
||||
finally:
|
||||
read_access.set()
|
||||
|
||||
else:
|
||||
return
|
||||
|
||||
if data.get('response_required'):
|
||||
data['response'] = response
|
||||
self.send_notification(SERVICE_IPC, data)
|
||||
|
||||
elif event == REFRESH_CONTAINER:
|
||||
self.refresh_container()
|
||||
|
||||
elif event == CONTAINER_FOCUS:
|
||||
if data:
|
||||
data = json.loads(data)
|
||||
if data:
|
||||
self._context.get_ui().focus_container(
|
||||
container_id=data.get(CONTAINER_ID),
|
||||
position=data.get(CONTAINER_POSITION),
|
||||
)
|
||||
|
||||
elif event == RELOAD_ACCESS_MANAGER:
|
||||
self._context.reload_access_manager()
|
||||
self.refresh_container()
|
||||
|
||||
elif event == PLAYBACK_STOPPED:
|
||||
if data:
|
||||
data = json.loads(data)
|
||||
if not data:
|
||||
return
|
||||
|
||||
if data.get('play_data', {}).get('play_count'):
|
||||
self.set_property(PLAYER_VIDEO_ID, data.get(VIDEO_ID))
|
||||
|
||||
elif event == SYNC_LISTITEM:
|
||||
video_ids = json.loads(data) if data else None
|
||||
if not video_ids:
|
||||
return
|
||||
|
||||
context = self._context
|
||||
ui = context.get_ui()
|
||||
focused_video_id = ui.get_listitem_property(VIDEO_ID)
|
||||
if not focused_video_id:
|
||||
return
|
||||
|
||||
playback_history = context.get_playback_history()
|
||||
for video_id in video_ids:
|
||||
if not video_id or video_id != focused_video_id:
|
||||
continue
|
||||
|
||||
play_count = ui.get_listitem_info(PLAY_COUNT)
|
||||
resumable = ui.get_listitem_bool(RESUMABLE)
|
||||
|
||||
self.set_property(MARK_AS_LABEL,
|
||||
context.localize('history.mark.unwatched')
|
||||
if play_count else
|
||||
context.localize('history.mark.watched'))
|
||||
|
||||
item_history = playback_history.get_item(video_id)
|
||||
if item_history:
|
||||
item_history = dict(
|
||||
item_history,
|
||||
play_count=int(play_count) if play_count else 0,
|
||||
)
|
||||
if not resumable:
|
||||
item_history['played_time'] = 0
|
||||
item_history['played_percent'] = 0
|
||||
playback_history.update_item(video_id, item_history)
|
||||
else:
|
||||
playback_history.set_item(video_id, {
|
||||
'play_count': int(play_count) if play_count else 0,
|
||||
})
|
||||
|
||||
def onSettingsChanged(self, force=False):
|
||||
context = self._context
|
||||
|
||||
if force:
|
||||
self._settings_collect = False
|
||||
self._settings_changes = 0
|
||||
else:
|
||||
self._settings_changes += 1
|
||||
if self._settings_collect:
|
||||
if self._settings_collect == -1:
|
||||
self._settings_collect = False
|
||||
return
|
||||
|
||||
total = self._settings_changes
|
||||
self.waitForAbort(1)
|
||||
if total != self._settings_changes:
|
||||
return
|
||||
|
||||
self.log.debug('onSettingsChanged: %d change(s)', total)
|
||||
self._settings_changes = 0
|
||||
|
||||
settings = context.get_settings(refresh=True)
|
||||
log_level = settings.log_level()
|
||||
if log_level:
|
||||
self.log.debugging = True
|
||||
if log_level & 2:
|
||||
self.log.stack_info = True
|
||||
self.log.verbose_logging = True
|
||||
else:
|
||||
self.log.stack_info = False
|
||||
self.log.verbose_logging = False
|
||||
else:
|
||||
self.log.debugging = False
|
||||
self.log.stack_info = False
|
||||
self.log.verbose_logging = False
|
||||
|
||||
self.set_property(CHECK_SETTINGS)
|
||||
self.refresh_container()
|
||||
|
||||
httpd_started = bool(self.httpd)
|
||||
httpd_restart = False
|
||||
|
||||
address, port = get_connect_address(context)
|
||||
if port != self._httpd_port:
|
||||
self._old_httpd_port = self._httpd_port
|
||||
self._httpd_port = port
|
||||
httpd_restart = httpd_started
|
||||
if address != self._httpd_address:
|
||||
self._old_httpd_address = self._httpd_address
|
||||
self._httpd_address = address
|
||||
httpd_restart = httpd_started
|
||||
|
||||
whitelist = settings.httpd_whitelist()
|
||||
if whitelist != self._whitelist:
|
||||
self._whitelist = whitelist
|
||||
httpd_restart = httpd_started
|
||||
|
||||
sleep_allowed = settings.httpd_sleep_allowed()
|
||||
if sleep_allowed is False:
|
||||
self.httpd_sleep_allowed = False
|
||||
|
||||
if self.httpd_required(settings):
|
||||
if httpd_restart:
|
||||
self.restart_httpd()
|
||||
else:
|
||||
self.start_httpd()
|
||||
elif httpd_started:
|
||||
self.shutdown_httpd(terminate=True)
|
||||
|
||||
def httpd_address_sync(self):
|
||||
self._old_httpd_address = self._httpd_address
|
||||
self._old_httpd_port = self._httpd_port
|
||||
|
||||
def start_httpd(self):
|
||||
if self.httpd:
|
||||
self._httpd_error = False
|
||||
return True
|
||||
|
||||
context = self._context
|
||||
self.log.debug('HTTPServer: Starting {ip}:{port}',
|
||||
ip=self._httpd_address,
|
||||
port=self._httpd_port)
|
||||
self.httpd_address_sync()
|
||||
self.httpd = get_http_server(address=self._httpd_address,
|
||||
port=self._httpd_port,
|
||||
context=context)
|
||||
if not self.httpd:
|
||||
self._httpd_error = True
|
||||
return False
|
||||
|
||||
self.httpd_thread = Thread(target=self.httpd.serve_forever)
|
||||
self.httpd_thread.daemon = True
|
||||
self.httpd_thread.start()
|
||||
|
||||
address = self.httpd.socket.getsockname()
|
||||
self.log.debug('HTTPServer: Listening on {address[0]}:{address[1]}',
|
||||
address=address)
|
||||
self._httpd_error = False
|
||||
return True
|
||||
|
||||
def shutdown_httpd(self, on_idle=False, terminate=False, player=None):
|
||||
if (not self.httpd
|
||||
or (not (terminate or self.system_sleep)
|
||||
and (on_idle or self.system_idle)
|
||||
and self.httpd_required(on_idle=True, player=player))):
|
||||
return
|
||||
self.log.debug('HTTPServer: Shutting down {ip}:{port}',
|
||||
ip=self._old_httpd_address,
|
||||
port=self._old_httpd_port)
|
||||
self.httpd_address_sync()
|
||||
|
||||
shutdown_thread = Thread(target=self.httpd.shutdown)
|
||||
shutdown_thread.daemon = True
|
||||
shutdown_thread.start()
|
||||
|
||||
for thread in (self.httpd_thread, shutdown_thread):
|
||||
if not thread.is_alive():
|
||||
continue
|
||||
try:
|
||||
thread.join(2)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.httpd.server_close()
|
||||
|
||||
self.httpd_thread = None
|
||||
self.httpd = None
|
||||
|
||||
def restart_httpd(self):
|
||||
self.log.debug('HTTPServer: Restarting'
|
||||
' {old_ip}:{old_port} > {ip}:{port}',
|
||||
old_ip=self._old_httpd_address,
|
||||
old_port=self._old_httpd_port,
|
||||
ip=self._httpd_address,
|
||||
port=self._httpd_port)
|
||||
self.shutdown_httpd(terminate=True)
|
||||
self.start_httpd()
|
||||
|
||||
def ping_httpd(self):
|
||||
return self.httpd and httpd_status(self._context)
|
||||
|
||||
def httpd_required(self, settings=None, on_idle=False, player=None):
|
||||
if settings:
|
||||
required = (settings.use_mpd_videos()
|
||||
or settings.api_config_page()
|
||||
or settings.support_alternative_player())
|
||||
self._use_httpd = required
|
||||
|
||||
elif self._httpd_error:
|
||||
required = False
|
||||
|
||||
elif on_idle:
|
||||
settings = self._context.get_settings()
|
||||
|
||||
playing = player.isPlaying() if player else False
|
||||
external = player.isExternalPlayer() if playing else False
|
||||
|
||||
required = ((playing and settings.use_mpd_videos())
|
||||
or settings.api_config_page()
|
||||
or (external and settings.support_alternative_player()))
|
||||
|
||||
else:
|
||||
required = self._use_httpd
|
||||
|
||||
return required
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2023-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
|
||||
|
||||
from .http_server import (
|
||||
get_client_ip_address,
|
||||
get_connect_address,
|
||||
get_http_server,
|
||||
get_listen_addresses,
|
||||
httpd_status,
|
||||
)
|
||||
from .ip_api import Locator
|
||||
from .requests import BaseRequestsClass, InvalidJSONError
|
||||
|
||||
|
||||
__all__ = (
|
||||
'get_client_ip_address',
|
||||
'get_connect_address',
|
||||
'get_http_server',
|
||||
'get_listen_addresses',
|
||||
'httpd_status',
|
||||
'BaseRequestsClass',
|
||||
'InvalidJSONError',
|
||||
'Locator',
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2018-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
|
||||
|
||||
from .requests import BaseRequestsClass
|
||||
from .. import logging
|
||||
|
||||
|
||||
class Locator(BaseRequestsClass):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def __init__(self, context):
|
||||
self._base_url = 'http://ip-api.com'
|
||||
self._response = {}
|
||||
|
||||
super(Locator, self).__init__(context=context)
|
||||
|
||||
def response(self):
|
||||
return self._response
|
||||
|
||||
def locate_requester(self):
|
||||
request_url = '/'.join((self._base_url, 'json'))
|
||||
response = self.request(request_url)
|
||||
if response is None:
|
||||
self._response = {}
|
||||
return
|
||||
with response:
|
||||
self._response = response.json()
|
||||
|
||||
def success(self):
|
||||
response = self.response()
|
||||
successful = response.get('status', 'fail') == 'success'
|
||||
if successful:
|
||||
self.log.debug('Request successful')
|
||||
else:
|
||||
self.log.error(('Request failed', 'Message: %s'),
|
||||
response.get('message', 'Unknown'))
|
||||
return successful
|
||||
|
||||
def coordinates(self):
|
||||
lat = None
|
||||
lon = None
|
||||
if self.success():
|
||||
lat = self._response.get('lat')
|
||||
lon = self._response.get('lon')
|
||||
if lat is None or lon is None:
|
||||
self.log.error('No coordinates returned')
|
||||
return None
|
||||
self.log.debug('Coordinates found')
|
||||
return {'lat': lat, 'lon': lon}
|
||||
@@ -0,0 +1,416 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2023-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 atexit
|
||||
import socket
|
||||
|
||||
from requests import Request, Session
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
from requests.exceptions import InvalidJSONError, RequestException, URLRequired
|
||||
from requests.utils import DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths
|
||||
from urllib3.util.ssl_ import create_urllib3_context
|
||||
|
||||
from .. import logging
|
||||
from ..utils.datetime import imf_fixdate
|
||||
from ..utils.methods import generate_hash
|
||||
|
||||
|
||||
__all__ = (
|
||||
'BaseRequestsClass',
|
||||
'InvalidJSONError'
|
||||
)
|
||||
|
||||
|
||||
class SSLHTTPAdapter(HTTPAdapter):
|
||||
_SOCKET_OPTIONS = (
|
||||
(socket.SOL_SOCKET, getattr(socket, 'SO_KEEPALIVE', None), 1),
|
||||
(socket.IPPROTO_TCP, getattr(socket, 'TCP_NODELAY', None), 1),
|
||||
(socket.IPPROTO_TCP, getattr(socket, 'TCP_KEEPIDLE', None), 300),
|
||||
# TCP_KEEPALIVE equivalent to TCP_KEEPIDLE on iOS/macOS
|
||||
(socket.IPPROTO_TCP, getattr(socket, 'TCP_KEEPALIVE', None), 300),
|
||||
# TCP_KEEPINTVL may not be implemented at app level on iOS/macOS
|
||||
(socket.IPPROTO_TCP, getattr(socket, 'TCP_KEEPINTVL', None), 60),
|
||||
# TCP_KEEPCNT may not be implemented at app level on iOS/macOS
|
||||
(socket.IPPROTO_TCP, getattr(socket, 'TCP_KEEPCNT', None), 5),
|
||||
# TCP_USER_TIMEOUT = TCP_KEEPIDLE + TCP_KEEPINTVL * TCP_KEEPCNT
|
||||
(socket.IPPROTO_TCP, getattr(socket, 'TCP_USER_TIMEOUT', None), 600),
|
||||
)
|
||||
|
||||
_ssl_context = create_urllib3_context()
|
||||
_ssl_context.load_verify_locations(
|
||||
capath=extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
|
||||
)
|
||||
|
||||
def init_poolmanager(self, *args, **kwargs):
|
||||
kwargs['ssl_context'] = self._ssl_context
|
||||
|
||||
kwargs['socket_options'] = [
|
||||
socket_option for socket_option in self._SOCKET_OPTIONS
|
||||
if socket_option[1] is not None
|
||||
]
|
||||
|
||||
return super(SSLHTTPAdapter, self).init_poolmanager(*args, **kwargs)
|
||||
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
self._ssl_context.check_hostname = bool(verify)
|
||||
return super(SSLHTTPAdapter, self).cert_verify(conn, url, verify, cert)
|
||||
|
||||
|
||||
class BaseRequestsClass(object):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_session = Session()
|
||||
_session.trust_env = False
|
||||
_session.mount('https://', SSLHTTPAdapter(
|
||||
pool_maxsize=10,
|
||||
pool_block=True,
|
||||
max_retries=Retry(
|
||||
total=3,
|
||||
backoff_factor=0.1,
|
||||
status_forcelist={500, 502, 503, 504},
|
||||
allowed_methods=None,
|
||||
)
|
||||
))
|
||||
atexit.register(_session.close)
|
||||
|
||||
_context = None
|
||||
_verify = True
|
||||
_timeout = (9.5, 27)
|
||||
_proxy = None
|
||||
_default_exc = (RequestException,)
|
||||
|
||||
METHODS_TO_CACHE = {'GET', 'HEAD'}
|
||||
|
||||
def __init__(self,
|
||||
context=None,
|
||||
verify_ssl=None,
|
||||
timeout=None,
|
||||
proxy_settings=None,
|
||||
exc_type=None,
|
||||
**_kwargs):
|
||||
super(BaseRequestsClass, self).__init__()
|
||||
BaseRequestsClass.init(
|
||||
context=context,
|
||||
verify_ssl=verify_ssl,
|
||||
timeout=timeout,
|
||||
proxy_settings=proxy_settings,
|
||||
)
|
||||
self._default_exc = (
|
||||
(RequestException,) + exc_type
|
||||
if isinstance(exc_type, tuple) else
|
||||
(RequestException, exc_type)
|
||||
if exc_type else
|
||||
(RequestException,)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def init(cls,
|
||||
context=None,
|
||||
verify_ssl=None,
|
||||
timeout=None,
|
||||
proxy_settings=None,
|
||||
**_kwargs):
|
||||
cls._context = (cls._context
|
||||
if context is None else
|
||||
context)
|
||||
if cls._context:
|
||||
settings = cls._context.get_settings()
|
||||
cls._verify = (settings.verify_ssl()
|
||||
if verify_ssl is None else
|
||||
verify_ssl)
|
||||
cls._timeout = (settings.requests_timeout()
|
||||
if timeout is None else
|
||||
timeout)
|
||||
cls._proxy = (settings.proxy_settings()
|
||||
if proxy_settings is None else
|
||||
proxy_settings)
|
||||
|
||||
def reinit(self, **kwargs):
|
||||
self.__init__(**kwargs)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
|
||||
self._session.close()
|
||||
|
||||
@staticmethod
|
||||
def _raise_exception(new_exception, *args, **kwargs):
|
||||
if not new_exception:
|
||||
return
|
||||
if issubclass(new_exception, RequestException):
|
||||
new_exception = new_exception(*args)
|
||||
attrs = new_exception.__dict__
|
||||
for attr, value in kwargs.items():
|
||||
if attr not in attrs:
|
||||
setattr(new_exception, attr, value)
|
||||
raise new_exception
|
||||
else:
|
||||
raise new_exception(*args, **kwargs)
|
||||
|
||||
def _response_hook_json(self, **kwargs):
|
||||
response = kwargs['response']
|
||||
if response is None:
|
||||
return None, None
|
||||
with response:
|
||||
try:
|
||||
json_data = response.json()
|
||||
if 'error' in json_data:
|
||||
kwargs.setdefault('pass_data', True)
|
||||
kwargs.setdefault('json_data', json_data)
|
||||
json_data.setdefault('code', response.status_code)
|
||||
self._raise_exception(
|
||||
kwargs.get('exception', RequestException),
|
||||
'"error" in response JSON data',
|
||||
**kwargs
|
||||
)
|
||||
except ValueError as exc:
|
||||
if kwargs.get('raise_exc') is None:
|
||||
kwargs['raise_exc'] = True
|
||||
self._raise_exception(
|
||||
InvalidJSONError,
|
||||
exc,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
return json_data.get('etag'), json_data
|
||||
|
||||
def _response_hook_text(self, **kwargs):
|
||||
response = kwargs['response']
|
||||
if response is None:
|
||||
return None, None
|
||||
with response:
|
||||
response.raise_for_status()
|
||||
result = response and response.text
|
||||
if not result:
|
||||
self._raise_exception(
|
||||
kwargs.get('exception', RequestException),
|
||||
'Empty response text',
|
||||
**kwargs
|
||||
)
|
||||
|
||||
return None, result
|
||||
|
||||
def request(self, url=None, method='GET',
|
||||
params=None, data=None, headers=None, cookies=None, files=None,
|
||||
auth=None, timeout=None, allow_redirects=None, proxies=None,
|
||||
hooks=None, stream=None, verify=None, cert=None, json=None,
|
||||
prepared_request=None,
|
||||
# Custom event hook implementation
|
||||
# See _response_hook and _error_hook in login_client.py
|
||||
# for example usage
|
||||
response_hook=None,
|
||||
error_hook=None,
|
||||
event_hook_kwargs=None,
|
||||
error_title=None,
|
||||
error_info=None,
|
||||
raise_exc=None,
|
||||
cache=None,
|
||||
**kwargs):
|
||||
if timeout is None:
|
||||
timeout = self._timeout
|
||||
if verify is None:
|
||||
verify = self._verify
|
||||
if proxies is None:
|
||||
proxies = self._proxy
|
||||
if allow_redirects is None:
|
||||
allow_redirects = True
|
||||
stacklevel = kwargs.pop('stacklevel', 2)
|
||||
|
||||
response = None
|
||||
request_id = None
|
||||
cached_response = None
|
||||
etag = None
|
||||
timestamp = None
|
||||
|
||||
if url:
|
||||
prepared_request = self._session.prepare_request(Request(
|
||||
method=method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
files=files,
|
||||
data=data,
|
||||
json=json,
|
||||
params=params,
|
||||
auth=auth,
|
||||
cookies=cookies,
|
||||
hooks=hooks,
|
||||
))
|
||||
|
||||
if cache is not False:
|
||||
if prepared_request:
|
||||
method = prepared_request.method
|
||||
if cache is True or method in self.METHODS_TO_CACHE:
|
||||
headers = prepared_request.headers
|
||||
request_id = generate_hash(
|
||||
method,
|
||||
prepared_request.url,
|
||||
headers,
|
||||
prepared_request.body,
|
||||
)
|
||||
|
||||
if request_id:
|
||||
if cache == 'refresh':
|
||||
cache = self._context.get_requests_cache()
|
||||
cached_request = None
|
||||
else:
|
||||
cache = self._context.get_requests_cache()
|
||||
cached_request = cache.get(request_id)
|
||||
else:
|
||||
cache = False
|
||||
cached_request = None
|
||||
|
||||
if cached_request:
|
||||
etag, cached_response = cached_request['value']
|
||||
if cached_response is not None:
|
||||
if etag:
|
||||
# Etag is meant to be enclosed in double quotes, but the
|
||||
# Google servers don't seem to support this
|
||||
headers['If-None-Match'] = '"{0}", {0}'.format(etag)
|
||||
timestamp = imf_fixdate(cached_request['timestamp'])
|
||||
headers['If-Modified-Since'] = timestamp
|
||||
self.log.debug(('Cached response',
|
||||
'Request ID: {request_id}',
|
||||
'Etag: {etag}',
|
||||
'Modified: {timestamp}'),
|
||||
request_id=request_id,
|
||||
etag=etag,
|
||||
timestamp=timestamp,
|
||||
stacklevel=stacklevel)
|
||||
|
||||
if event_hook_kwargs is None:
|
||||
event_hook_kwargs = {}
|
||||
|
||||
try:
|
||||
if prepared_request:
|
||||
response = self._session.send(
|
||||
request=prepared_request,
|
||||
stream=stream,
|
||||
verify=verify,
|
||||
proxies=proxies,
|
||||
cert=cert,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
else:
|
||||
raise URLRequired()
|
||||
|
||||
status_code = getattr(response, 'status_code', None)
|
||||
if not status_code:
|
||||
raise self._default_exc[0](response=response)
|
||||
|
||||
if cached_response is None or status_code != 304:
|
||||
timestamp = response.headers.get('Date')
|
||||
if response_hook:
|
||||
event_hook_kwargs['exception'] = self._default_exc[-1]
|
||||
event_hook_kwargs['raise_exc'] = raise_exc
|
||||
event_hook_kwargs['response'] = response
|
||||
etag, response = response_hook(**event_hook_kwargs)
|
||||
else:
|
||||
etag = None
|
||||
response.raise_for_status()
|
||||
# Only clear cached response if there was no error response
|
||||
cached_response = None
|
||||
|
||||
except self._default_exc as exc:
|
||||
exc_response = exc.response or response
|
||||
if exc_response:
|
||||
response_text = exc_response.text
|
||||
response_status = exc_response.status_code
|
||||
response_reason = exc_response.reason
|
||||
else:
|
||||
response_text = None
|
||||
response_status = 'Error'
|
||||
response_reason = 'No response'
|
||||
|
||||
log_msg = [
|
||||
'{title}',
|
||||
'URL: {method} {url}',
|
||||
'Status: {response_status} - {response_reason}',
|
||||
'Response: {response_text}',
|
||||
]
|
||||
|
||||
kwargs.update(event_hook_kwargs)
|
||||
kwargs['exc'] = exc
|
||||
kwargs['response'] = exc_response
|
||||
|
||||
if error_hook:
|
||||
error_response = error_hook(**kwargs)
|
||||
_title, _info, _detail, _response, _exc = error_response
|
||||
if _title is not None:
|
||||
error_title = _title
|
||||
if _info:
|
||||
if isinstance(_info, (list, tuple)):
|
||||
log_msg.extend(_info)
|
||||
else:
|
||||
log_msg.append(_info)
|
||||
if _detail is not None:
|
||||
kwargs.update(_detail)
|
||||
if _response is not None:
|
||||
response = _response
|
||||
if response and not response_text:
|
||||
response_text = repr(_response)
|
||||
if _exc is not None:
|
||||
raise_exc = _exc
|
||||
|
||||
if error_info:
|
||||
if isinstance(error_info, (list, tuple)):
|
||||
log_msg.extend(error_info)
|
||||
else:
|
||||
log_msg.append(error_info)
|
||||
|
||||
self.log.exception(log_msg,
|
||||
title=(error_title or 'Failed'),
|
||||
method=method,
|
||||
url=url,
|
||||
response_status=response_status,
|
||||
response_reason=response_reason,
|
||||
response_text=response_text,
|
||||
stacklevel=stacklevel,
|
||||
**kwargs)
|
||||
|
||||
if raise_exc:
|
||||
if not isinstance(raise_exc, BaseException):
|
||||
if not callable(raise_exc):
|
||||
raise_exc = self._default_exc[-1]
|
||||
raise_exc = raise_exc(error_title)
|
||||
|
||||
if isinstance(raise_exc, BaseException):
|
||||
raise_exc.__cause__ = exc
|
||||
raise raise_exc
|
||||
raise exc
|
||||
|
||||
if cache:
|
||||
if cached_response is not None:
|
||||
self.log.debug(('Using cached response',
|
||||
'Request ID: {request_id}',
|
||||
'Etag: {etag}',
|
||||
'Modified: {timestamp}'),
|
||||
request_id=request_id,
|
||||
etag=etag,
|
||||
timestamp=timestamp,
|
||||
stacklevel=stacklevel)
|
||||
cache.set(request_id)
|
||||
response = cached_response
|
||||
else:
|
||||
self.log.debug(('Saving response to cache',
|
||||
'Request ID: {request_id}',
|
||||
'Etag: {etag}',
|
||||
'Modified: {timestamp}'),
|
||||
request_id=request_id,
|
||||
etag=etag,
|
||||
timestamp=timestamp,
|
||||
stacklevel=stacklevel)
|
||||
cache.set(request_id, response, etag)
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2023-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
|
||||
|
||||
from .xbmc.xbmc_playlist_player import XbmcPlaylistPlayer
|
||||
|
||||
|
||||
__all__ = ('XbmcPlaylistPlayer',)
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
|
||||
|
||||
class AbstractPlaylistPlayer(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def clear(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def add(self, base_item):
|
||||
raise NotImplementedError()
|
||||
|
||||
def shuffle(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def unshuffle(self):
|
||||
raise NotImplementedError()
|
||||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user