Updated kodi settings on Lenovo

This commit is contained in:
2026-03-22 22:28:43 +01:00
parent 725dfa7157
commit 32b5a81da6
10925 changed files with 575678 additions and 5511 deletions

View File

@@ -16,17 +16,23 @@ from functools import partial
from itertools import chain, islice
from random import randint
from re import compile as re_compile
from xml.etree.ElementTree import Element as ET_Element, XML as ET_XML
from xml.etree.ElementTree import (
Element as ET_Element,
XML as ET_XML,
XMLParser as ET_XMLParser,
)
from .login_client import YouTubeLoginClient
from ..helper.utils import channel_filter_split
from ..helper.v3 import pre_fill
from ..youtube_exceptions import InvalidJSON, YouTubeException
from ...kodion import logging
from ...kodion.compatibility import available_cpu_count, string_type
from ...kodion.constants import CHANNEL_ID, PLAYLIST_ID
from ...kodion.items import DirectoryItem
from ...kodion.utils.convert_format import strip_html_from_text
from ...kodion.utils.convert_format import (
channel_filter_split,
strip_html_from_text,
)
from ...kodion.utils.datetime import (
since_epoch,
strptime,
@@ -89,23 +95,33 @@ class YouTubeDataClient(YouTubeLoginClient):
'tvSurfaceContentRenderer',
'content',
'sectionListRenderer',
'contents',
0,
'shelfRenderer',
'content',
'horizontalListRenderer',
'continuations',
0,
'nextContinuationData',
(
(
'contents',
slice(None),
None,
'shelfRenderer',
'content',
('horizontalListRenderer', 'verticalListRenderer'),
'continuations',
0,
'nextContinuationData',
),
(
'continuations',
0,
'nextContinuationData'
)
),
),
'continuation_items': (
'continuationContents',
'horizontalListContinuation',
('horizontalListContinuation', 'sectionListContinuation'),
'items',
),
'continuation_continuation': (
'continuationContents',
'horizontalListContinuation',
('horizontalListContinuation', 'sectionListContinuation'),
'continuations',
0,
'nextContinuationData',
@@ -200,7 +216,7 @@ class YouTubeDataClient(YouTubeLoginClient):
slice(None),
'shelfRenderer',
'content',
'horizontalListRenderer',
('horizontalListRenderer', 'verticalListRenderer'),
'items',
),
'item_id': (
@@ -244,23 +260,43 @@ class YouTubeDataClient(YouTubeLoginClient):
'tvSurfaceContentRenderer',
'content',
'sectionListRenderer',
'contents',
0,
'shelfRenderer',
'content',
'horizontalListRenderer',
'continuations',
0,
'nextContinuationData',
(
(
'contents',
slice(None),
None,
'shelfRenderer',
'content',
('horizontalListRenderer', 'verticalListRenderer'),
'continuations',
0,
'nextContinuationData',
),
(
'continuations',
0,
'nextContinuationData'
)
),
),
'continuation_items': (
'continuationContents',
'horizontalListContinuation',
'items',
('horizontalListContinuation', 'sectionListContinuation'),
(
('items',),
(
'contents',
slice(None),
'shelfRenderer',
'content',
('horizontalListRenderer', 'verticalListRenderer'),
'items',
),
),
),
'continuation_continuation': (
'continuationContents',
'horizontalListContinuation',
('horizontalListContinuation', 'sectionListContinuation'),
'continuations',
0,
'nextContinuationData',
@@ -282,7 +318,11 @@ class YouTubeDataClient(YouTubeLoginClient):
('horizontalListRenderer', 'verticalListRenderer'),
'items',
slice(None),
('gridVideoRenderer', 'compactVideoRenderer'),
(
'gridVideoRenderer',
'compactVideoRenderer',
'tileRenderer',
),
# 'videoId',
),
'continuation': (
@@ -307,7 +347,11 @@ class YouTubeDataClient(YouTubeLoginClient):
('horizontalListRenderer', 'verticalListRenderer'),
'items',
slice(None),
('gridVideoRenderer', 'compactVideoRenderer'),
(
'gridVideoRenderer',
'compactVideoRenderer',
'tileRenderer',
),
# 'videoId',
),
'continuation_continuation': (
@@ -521,6 +565,7 @@ class YouTubeDataClient(YouTubeLoginClient):
**kwargs)
if playlist_id_upper == 'WL':
self._context.get_watch_later_list().add_item(video_id)
post_data = {
'playlistId': playlist_id_upper,
'actions': [{
@@ -555,6 +600,7 @@ class YouTubeDataClient(YouTubeLoginClient):
**kwargs)
if playlist_id_upper == 'WL':
self._context.get_watch_later_list().del_item(video_id)
post_data = {
'playlistId': playlist_id_upper,
'actions': [{
@@ -1239,7 +1285,7 @@ class YouTubeDataClient(YouTubeLoginClient):
max_results = self.max_results()
params = {
'part': 'snippet,contentDetails,brandingSettings,statistics',
'maxResults': str(max_results),
'maxResults': max_results,
}
if channel_id == 'mine':
@@ -1274,6 +1320,7 @@ class YouTubeDataClient(YouTubeLoginClient):
video_id,
live_details=False,
max_results=None,
_part='snippet,contentDetails,player,status,statistics',
**kwargs):
"""
Returns a list of videos that match the API request parameters
@@ -1283,12 +1330,9 @@ class YouTubeDataClient(YouTubeLoginClient):
in the result set, from 0 to 50, inclusive
:return:
"""
params = {
'part': (
'snippet,contentDetails,status,statistics,liveStreamingDetails'
if live_details else
'snippet,contentDetails,status,statistics'
),
'part': _part + ',liveStreamingDetails' if live_details else _part,
'id': (
video_id
if isinstance(video_id, string_type) else
@@ -1299,6 +1343,7 @@ class YouTubeDataClient(YouTubeLoginClient):
if max_results is None else
max_results
),
'maxHeight': self._context.get_settings().max_video_height(),
}
return self.api_request(method='GET', path='videos',
params=params,
@@ -1686,7 +1731,7 @@ class YouTubeDataClient(YouTubeLoginClient):
2,
'shelfRenderer',
'content',
'horizontalListRenderer',
('horizontalListRenderer', 'verticalListRenderer'),
'items',
) if retry == 2 else (
'contents',
@@ -2278,27 +2323,39 @@ class YouTubeDataClient(YouTubeLoginClient):
return timestamp
threaded_output = {
'channel_ids': [],
'playlist_ids': [],
'channel_ids': set(),
'playlist_ids': set(),
'feeds': {},
'to_refresh': [],
'to_refresh': set(),
}
bookmarks = context.get_bookmarks_list().get_items()
if bookmarks:
channel_ids = threaded_output['channel_ids']
playlist_ids = threaded_output['playlist_ids']
for item_id, item in bookmarks.items():
if isinstance(item, DirectoryItem):
item_id = getattr(item, PLAYLIST_ID, None)
if item_id:
playlist_ids.append(item_id)
continue
item_id = getattr(item, CHANNEL_ID, None)
elif not isinstance(item, float):
continue
if item_id:
channel_ids.append(item_id)
(use_subscriptions,
use_saved_playlists,
use_bookmarked_channels,
use_bookmarked_playlists) = settings.subscriptions_sources()
if not self.logged_in:
use_subscriptions = False
use_saved_playlists = False
if use_bookmarked_channels or use_bookmarked_playlists:
bookmarks = context.get_bookmarks_list().get_items()
if bookmarks:
channel_ids = threaded_output['channel_ids']
playlist_ids = threaded_output['playlist_ids']
for item_id, item in bookmarks.items():
if isinstance(item, DirectoryItem):
if use_bookmarked_playlists:
item_id = getattr(item, PLAYLIST_ID, None)
if item_id:
playlist_ids.add(item_id)
continue
if use_bookmarked_channels:
item_id = getattr(item, CHANNEL_ID, None)
if item_id:
channel_ids.add(item_id)
continue
elif use_bookmarked_channels and isinstance(item, float):
channel_ids.add(item_id)
headers = {
'Host': 'www.youtube.com',
@@ -2320,11 +2377,12 @@ class YouTubeDataClient(YouTubeLoginClient):
inputs,
item_type,
feed_type=feed_type,
_refresh=refresh,
refresh=refresh,
feed_history=feed_history,
ttl=feed_history.ONE_HOUR):
feeds = output['feeds']
to_refresh = output['to_refresh']
if item_type == 'channel_id':
channel_prefix = (
'UUSH' if feed_type == 'shorts' else
@@ -2333,75 +2391,88 @@ class YouTubeDataClient(YouTubeLoginClient):
)
else:
channel_prefix = False
for item_id in inputs:
if channel_prefix:
channel_id = item_id
item_id = item_id.replace('UC', channel_prefix, 1)
else:
channel_id = None
cached = feed_history.get_item(item_id, seconds=ttl)
cached = feed_history.get_item(item_id)
if cached:
feed_details = cached['value']
feed_details['refresh'] = _refresh
if channel_id:
feed_details.setdefault('channel_id', channel_id)
if _refresh:
to_refresh.append({item_type: channel_id})
elif _refresh:
to_refresh.append({item_type: item_id})
_refresh = refresh or cached['age'] > ttl
feed_details['refresh'] = _refresh
if _refresh:
to_refresh.add(channel_id)
if item_id in feeds:
feeds[item_id].update(feed_details)
else:
feeds[item_id] = feed_details
elif channel_id:
to_refresh.append({item_type: channel_id})
else:
to_refresh.append({item_type: item_id})
del inputs[:]
to_refresh.add(channel_id)
return True, False
def _get_feed(output,
channel_id=None,
playlist_id=None,
input=None,
feed_type=feed_type,
headers=headers):
if channel_id:
item_id = channel_id.replace(
headers=headers,
stream=True,
cache=False):
if not input:
return True, False
if input.startswith('UC'):
channel_id = input
item_id = input.replace(
'UC',
'UUSH' if feed_type == 'shorts' else
'UULV' if feed_type == 'live' else
'UULF',
1,
)
elif playlist_id:
item_id = playlist_id
else:
return True, False
channel_id = None
item_id = input
response = self.request(
''.join((self.BASE_URL,
'/feeds/videos.xml?playlist_id=',
item_id)),
headers=headers,
stream=stream,
cache=cache,
)
if response is None:
return False, True
with response:
if response.status_code == 404:
content = None
elif response.status_code == 429:
status_code = response.status_code
if status_code == 429:
return False, True
if status_code == 404:
content = None
elif stream:
parser = ET_XMLParser(encoding='utf-8')
for chunk in response.iter_content(chunk_size=(8 * 1024)):
if chunk:
parser.feed(chunk)
content = parser.close()
else:
response.encoding = 'utf-8'
content = response.content
content = ET_XML(response.content)
_output = {
'channel_id': channel_id,
'content': content,
'refresh': True,
'refresh': content is not None,
}
feeds = output['feeds']
if item_id in feeds:
feeds[item_id].update(_output)
@@ -2433,6 +2504,7 @@ class YouTubeDataClient(YouTubeLoginClient):
dict_get = {}.get
find = ET_Element.find
findtext = ET_Element.findtext
iterfind = ET_Element.iterfind
all_items = {}
new_cache = {}
@@ -2441,10 +2513,9 @@ class YouTubeDataClient(YouTubeLoginClient):
channel_name = feed.get('channel_name')
cached_items = feed.get('cached_items')
refresh_feed = feed.get('refresh')
content = feed.get('content')
root = feed.get('content')
if refresh_feed and content:
root = ET_XML(content)
if refresh_feed and root is not None:
channel_name = findtext(
root,
'atom:author/atom:name',
@@ -2494,7 +2565,7 @@ class YouTubeDataClient(YouTubeLoginClient):
), 'get', dict_get)('views', 0),
},
'_partial': True,
} for item in root.findall('atom:entry', ns)]
} for item in iterfind(root, 'atom:entry', ns)]
else:
feed_items = []
@@ -2551,6 +2622,7 @@ class YouTubeDataClient(YouTubeLoginClient):
def _threaded_fetch(kwargs,
do_batch,
unpack,
output,
worker,
threads,
@@ -2567,7 +2639,19 @@ class YouTubeDataClient(YouTubeLoginClient):
if kwargs is True:
_kwargs = {}
elif kwargs:
_kwargs = {'inputs': kwargs} if do_batch else kwargs.pop()
if do_batch:
batch = kwargs.copy()
kwargs -= batch
_kwargs = {'inputs': batch}
else:
try:
_kwargs = kwargs.pop()
except KeyError:
if check_inputs:
check_inputs.clear()
break
if unpack:
_kwargs = {'input': _kwargs}
elif check_inputs:
if check_inputs.wait(0.1) and kwargs:
continue
@@ -2611,11 +2695,9 @@ class YouTubeDataClient(YouTubeLoginClient):
'counts': counts,
'active_thread_ids': active_thread_ids,
}
payloads = {}
if self.logged_in:
function_cache = context.get_function_cache()
if use_subscriptions:
channel_params = {
'part': 'snippet,contentDetails',
'maxResults': 50,
@@ -2624,51 +2706,44 @@ class YouTubeDataClient(YouTubeLoginClient):
}
def _get_updated_subscriptions(new_data, old_data):
items = new_data and new_data.get('items')
if not items:
new_items = new_data and new_data.get('items')
if not new_items:
new_data['_abort'] = True
return new_data
_items = old_data and old_data.get('items')
if _items:
_items = {
old_items = old_data and old_data.get('items')
if old_items:
old_items = {
item['snippet']['resourceId']['channelId']:
item['contentDetails']
for item in _items
for item in old_items
}
updated_subscriptions = []
old_subscriptions = []
for item in items:
old_subscriptions = False
updated_subscriptions = set()
for item in new_items:
channel_id = item['snippet']['resourceId']['channelId']
counts = item['contentDetails']
if channel_id in updated_subscriptions:
continue
if (counts['newItemCount']
or counts['totalItemCount']
> _items.get(channel_id, {})['totalItemCount']):
updated_subscriptions.append(
{
'channel_id': channel_id,
}
)
item_counts = item['contentDetails']
if (item_counts['newItemCount']
or (channel_id in old_items
and item_counts['totalItemCount']
> old_items[channel_id]['totalItemCount'])):
updated_subscriptions.add(channel_id)
else:
old_subscriptions.append(channel_id)
old_subscriptions = True
if old_subscriptions:
new_data['nextPageToken'] = None
else:
updated_subscriptions = [
{
'channel_id':
item['snippet']['resourceId']['channelId'],
}
for item in items
]
old_subscriptions = []
updated_subscriptions = {
item['snippet']['resourceId']['channelId']
for item in new_items
}
new_data['_updated_subscriptions'] = updated_subscriptions
new_data['_old_subscriptions'] = old_subscriptions
return new_data
def _get_channels(output,
@@ -2691,11 +2766,14 @@ class YouTubeDataClient(YouTubeLoginClient):
updated_subscriptions = json_data.get('_updated_subscriptions')
if updated_subscriptions:
output['to_refresh'].extend(updated_subscriptions)
output['to_refresh'] |= updated_subscriptions
old_subscriptions = json_data.get('_old_subscriptions')
if old_subscriptions:
output['channel_ids'].extend(old_subscriptions)
all_subscriptions = json_data.get('items')
if all_subscriptions:
output['channel_ids'].update([
item['snippet']['resourceId']['channelId']
for item in all_subscriptions
])
page_token = json_data.get('nextPageToken')
if page_token:
@@ -2705,84 +2783,106 @@ class YouTubeDataClient(YouTubeLoginClient):
del _params['pageToken']
return True, True
# playlist_params = {
# 'part': 'snippet',
# 'maxResults': 50,
# 'order': 'alphabetical',
# 'mine': True,
# }
#
# def _get_playlists(output,
# _params=playlist_params,
# _refresh=refresh,
# _force_cache=force_cache,
# function_cache=function_cache):
# json_data = function_cache.run(
# self.get_saved_playlists,
# function_cache.ONE_HOUR
# if _force_cache or 'pageToken' in _params else
# 5 * function_cache.ONE_MINUTE,
# _refresh=_refresh,
# **kwargs
# )
# if not json_data:
# return False, True
#
# output['playlist_ids'].extend([{
# 'playlist_id': item['snippet']['resourceId']['playlistId']
# } for item in json_data.get('items', [])])
#
# subs_page_token = json_data.get('nextPageToken')
# if subs_page_token:
# _params['pageToken'] = subs_page_token
# return True, False
# return True, True
payloads[1] = {
'worker': _get_channels,
'kwargs': True,
'do_batch': False,
'unpack': False,
'output': threaded_output,
'threads': threads,
'limit': 1,
'check_inputs': False,
'inputs_to_check': None,
}
# payloads[2] = {
# 'worker': _get_playlists,
# 'kwargs': True,
# 'output': threaded_output,
# 'threads': threads,
# 'limit': 1,
# 'check_inputs': False,
# 'inputs_to_check': None,
# }
if use_saved_playlists:
playlist_params = {
'part': 'snippet',
'maxResults': 50,
'order': 'alphabetical',
'mine': True,
}
def _get_playlists(output,
_params=playlist_params,
_refresh=refresh,
_force_cache=force_cache,
function_cache=function_cache):
own_channel = self.channel_id
if own_channel:
own_channel = (own_channel,)
json_data = function_cache.run(
self.get_browse_items,
function_cache.ONE_HOUR
if _force_cache or 'pageToken' in _params else
5 * function_cache.ONE_MINUTE,
_refresh=_refresh,
browse_id='FEplaylist_aggregation',
client='tv',
skip_ids=own_channel,
response_type='playlists',
do_auth=True,
json_path=self.JSON_PATHS['tv_grid'],
**kwargs
)
if not json_data:
return False, True
saved_playlists = json_data.get('items')
if saved_playlists:
output['playlist_ids'].update([
item['id']
for item in saved_playlists
])
subs_page_token = json_data.get('nextPageToken')
if subs_page_token:
_params['pageToken'] = subs_page_token
return True, False
return True, True
payloads[2] = {
'worker': _get_playlists,
'kwargs': True,
'do_batch': False,
'unpack': False,
'output': threaded_output,
'threads': threads,
'limit': 1,
'check_inputs': False,
'inputs_to_check': None,
}
payloads[3] = {
'worker': partial(_get_cached_feed, item_type='channel_id'),
'kwargs': threaded_output['channel_ids'],
'do_batch': True,
'unpack': False,
'output': threaded_output,
'threads': threads,
'limit': None,
'check_inputs': threading.Event(),
'inputs_to_check': {1},
}
payloads[4] = {
'worker': partial(_get_cached_feed, item_type='playlist_id'),
'kwargs': threaded_output['playlist_ids'],
'do_batch': True,
'unpack': False,
'output': threaded_output,
'threads': threads,
'limit': None,
# 'check_inputs': threading.Event(),
# 'inputs_to_check': {2},
'check_inputs': False,
'inputs_to_check': None,
'check_inputs': threading.Event(),
'inputs_to_check': {2},
}
payloads[5] = {
'worker': _get_feed,
'kwargs': threaded_output['to_refresh'],
'do_batch': False,
'unpack': True,
'output': threaded_output,
'threads': threads,
'limit': None,
@@ -2845,6 +2945,7 @@ class YouTubeDataClient(YouTubeLoginClient):
elif available <= 0:
continue
counter.acquire(True)
new_thread = threading.Thread(
target=_threaded_fetch,
kwargs=payload,
@@ -2852,7 +2953,6 @@ class YouTubeDataClient(YouTubeLoginClient):
new_thread.daemon = True
counts[pool_id] += 1
counts['all'] += 1
counter.acquire(True)
new_thread.start()
items = _parse_feeds(
@@ -2901,21 +3001,19 @@ class YouTubeDataClient(YouTubeLoginClient):
return None, None
with response:
headers = response.headers
if kwargs.get('extended_debug'):
self.log.debug(('Request response',
'Status: {response.status_code!r}',
'Headers: {headers!r}',
'Content: {response.text}'),
response=response,
headers=headers._store if headers else None,
stacklevel=4)
if self.log.verbose_logging:
log_msg = ('Request response',
'Status: {response.status_code!r}',
'Headers: {headers!r}',
'Content: {response.text}')
else:
self.log.debug(('Request response',
'Status: {response.status_code!r}',
'Headers: {headers!r}'),
response=response,
headers=headers._store if headers else None,
stacklevel=4)
log_msg = ('Request response',
'Status: {response.status_code!r}',
'Headers: {headers!r}')
self.log.debug(log_msg,
response=response,
headers=headers._store if headers else None,
stacklevel=4)
if response.status_code == 204 and 'no_content' in kwargs:
return None, True
@@ -2956,22 +3054,34 @@ class YouTubeDataClient(YouTubeLoginClient):
message = strip_html_from_text(details.get('message', 'Unknown error'))
if getattr(exc, 'notify', True):
context = self._context
ok_dialog = False
if reason in {'accessNotConfigured', 'forbidden'}:
notification = self._context.localize('key.requirement')
notification = context.localize('key.requirement')
ok_dialog = True
elif reason == 'keyInvalid' and message == 'Bad Request':
notification = self._context.localize('api.key.incorrect')
notification = context.localize('api.key.incorrect')
elif reason in {'quotaExceeded', 'dailyLimitExceeded'}:
notification = message
elif reason == 'authError':
auth_type = kwargs.get('_auth_type')
if auth_type:
if auth_type in self._access_tokens:
self._access_tokens[auth_type] = None
self.set_access_token(self._access_tokens)
context.get_access_manager().update_access_token(
context.get_param('addon_id'),
access_token=self.convert_access_tokens(to_list=True),
)
notification = message
else:
notification = message
title = ': '.join((self._context.get_name(), reason))
title = ': '.join((context.get_name(), reason))
if ok_dialog:
self._context.get_ui().on_ok(title, notification)
context.get_ui().on_ok(title, notification)
else:
self._context.get_ui().show_notification(notification, title)
context.get_ui().show_notification(notification, title)
info = (
'Reason: {error_reason}',
@@ -3041,54 +3151,39 @@ class YouTubeDataClient(YouTubeLoginClient):
if access_token:
access_tokens[config_type] = access_token
client = self.build_client(client, client_data)
if not client:
client = {}
abort = True
_client = self.build_client(client, client_data)
if _client:
client = _client
if clear_data and 'json' in client:
del client['json']
if clear_data and 'json' in client:
del client['json']
params = client.get('params')
if params:
log_params = params.copy()
if 'key' in params:
params = client.get('params')
if params and 'key' in params:
key = params['key']
if key:
abort = False
log_params['key'] = ('...'.join((key[:3], key[-3:]))
if len(key) > 9 else
'...')
elif not client['_has_auth']:
abort = True
if 'location' in params:
log_params['location'] = 'xx.xxxx,xx.xxxx'
else:
log_params = None
headers = client.get('headers')
if headers:
log_headers = headers.copy()
if 'Authorization' in log_headers:
log_headers['Authorization'] = '<redacted>'
else:
log_headers = None
client_data.setdefault('_name', client)
client = client_data
params = client.get('params')
abort = True
context = self._context
self.log.debug(('{request_name} API request',
'method: {method!r}',
'path: {path!r}',
'params: {params!r}',
'post_data: {data!r}',
'headers: {headers!r}'),
'path: {path!u}',
'params: {params!p}',
'post_data: {data!p}',
'headers: {headers!h}'),
request_name=client.get('_name'),
method=method,
path=path,
params=log_params,
path=path or url,
params=params,
data=client.get('json'),
headers=log_headers,
headers=client.get('headers'),
stacklevel=2)
if abort:
if kwargs.get('notify', True):
@@ -3098,8 +3193,6 @@ class YouTubeDataClient(YouTubeLoginClient):
)
self.log.warning('Aborted', stacklevel=2)
return {}
if context.get_settings().log_level() & 2:
kwargs.setdefault('extended_debug', True)
if cache is None and 'no_content' in kwargs:
cache = False
elif cache is not False and self._context.refresh_requested():

View File

@@ -18,7 +18,6 @@ from ...kodion import logging
class YouTubeLoginClient(YouTubeRequestClient):
log = logging.getLogger(__name__)
DOMAIN_SUFFIX = '.apps.googleusercontent.com'
DEVICE_CODE_URL = 'https://accounts.google.com/o/oauth2/device/code'
REVOKE_URL = 'https://accounts.google.com/o/oauth2/revoke'
TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
@@ -72,26 +71,59 @@ class YouTubeLoginClient(YouTubeRequestClient):
def reinit(self, **kwargs):
super(YouTubeLoginClient, self).reinit(**kwargs)
@classmethod
def convert_access_tokens(cls,
access_tokens=None,
to_dict=False,
to_list=False):
if access_tokens is None:
access_tokens = cls._access_tokens
if to_dict or isinstance(access_tokens, (list, tuple)):
access_tokens = {
cls.TOKEN_TYPES[token_idx]: token
for token_idx, token in enumerate(access_tokens)
if token and token_idx in cls.TOKEN_TYPES
}
elif to_list or isinstance(access_tokens, dict):
_access_tokens = [None, None, None, None]
for token_type, token in access_tokens.items():
token_idx = cls.TOKEN_TYPES.get(token_type)
if token_idx is None:
continue
_access_tokens[token_idx] = token
access_tokens = _access_tokens
return access_tokens
def set_access_token(self, access_tokens=None):
existing_access_tokens = type(self)._access_tokens
if access_tokens:
if isinstance(access_tokens, (list, tuple)):
access_tokens = self.convert_access_tokens(
access_tokens,
to_dict=True,
)
token_status = 0
for token_type, token in existing_access_tokens.items():
if token_type in access_tokens:
token = access_tokens[token_type]
existing_access_tokens[token_type] = token
if token or token_type == 'dev':
token_status |= 1
else:
token = None
existing_access_tokens[token_type] = None
if token:
token_status |= 2
elif token_type != 'dev':
token_status |= 1
self.logged_in = (
'partially'
if token_status & 2 else
'fully'
if token_status & 1 else
False
)
if token_status & 1:
if token_status & 2:
self.logged_in = 'partially'
else:
self.logged_in = False
elif token_status & 2:
self.logged_in = 'fully'
else:
self.logged_in = False
self.log.info('User is %s logged in', self.logged_in or 'not')
else:
for token_type in existing_access_tokens:
@@ -171,26 +203,13 @@ class YouTubeLoginClient(YouTubeRequestClient):
'refresh_token': refresh_token,
'grant_type': 'refresh_token'}
client_id.replace(self.DOMAIN_SUFFIX, '')
log_info = ('Login type: {login_type!r}',
'client_id: {client_id!r}',
'client_secret: {client_secret!r}')
log_params = {
'login_type': login_type,
'client_id': '...',
'client_secret': '...',
}
if len(client_id) > 11:
log_params['client_id'] = '...'.join((
client_id[:3],
client_id[-5:],
))
if len(client_secret) > 9:
log_params['client_secret'] = '...'.join((
client_secret[:3],
client_secret[-3:],
))
self.log.debug(('Refresh token:',) + log_info, **log_params)
log_info = ('Refresh token request ({login_type})',
'Params: {log_params!p}',)
self.log.debug(
log_info,
login_type=login_type,
log_params=post_data,
)
json_data = self.request(
self.TOKEN_URL,
@@ -202,7 +221,8 @@ class YouTubeLoginClient(YouTubeRequestClient):
error_title='Login failed - Refresh token grant error',
error_info=log_info,
raise_exc=True,
**log_params
login_type=login_type,
log_params=post_data,
)
return json_data
@@ -229,26 +249,13 @@ class YouTubeLoginClient(YouTubeRequestClient):
'code': code,
'grant_type': 'http://oauth.net/grant_type/device/1.0'}
client_id.replace(self.DOMAIN_SUFFIX, '')
log_info = ('Login type: {login_type!r}',
'client_id: {client_id!r}',
'client_secret: {client_secret!r}')
log_params = {
'login_type': login_type,
'client_id': '...',
'client_secret': '...',
}
if len(client_id) > 11:
log_params['client_id'] = '...'.join((
client_id[:3],
client_id[-5:],
))
if len(client_secret) > 9:
log_params['client_secret'] = '...'.join((
client_secret[:3],
client_secret[-3:],
))
self.log.debug(('Access token request:',) + log_info, **log_params)
log_info = ('Access token request ({login_type})',
'Params: {log_params!p}',)
self.log.debug(
log_info,
login_type=login_type,
log_params=post_data,
)
json_data = self.request(
self.TOKEN_URL,
@@ -260,7 +267,8 @@ class YouTubeLoginClient(YouTubeRequestClient):
error_title='Login failed - Access token request error',
error_info=log_info,
raise_exc=True,
**log_params
login_type=login_type,
log_params=post_data,
)
return json_data
@@ -284,19 +292,13 @@ class YouTubeLoginClient(YouTubeRequestClient):
post_data = {'client_id': client_id,
'scope': 'https://www.googleapis.com/auth/youtube'}
client_id.replace(self.DOMAIN_SUFFIX, '')
log_info = ('Login type: {login_type!r}',
'client_id: {client_id!r}')
log_params = {
'login_type': login_type,
'client_id': '...',
}
if len(client_id) > 11:
log_params['client_id'] = '...'.join((
client_id[:3],
client_id[-5:],
))
self.log.debug(('Device/user code request:',) + log_info, **log_params)
log_info = ('Device/user code request ({login_type})',
'Params: {log_params!p}',)
self.log.debug(
log_info,
login_type=login_type,
log_params=post_data,
)
json_data = self.request(
self.DEVICE_CODE_URL,
@@ -308,6 +310,7 @@ class YouTubeLoginClient(YouTubeRequestClient):
error_title='Login failed - Device/user code request error',
error_info=log_info,
raise_exc=True,
**log_params
login_type=login_type,
log_params=post_data,
)
return json_data

View File

@@ -14,7 +14,7 @@ from base64 import urlsafe_b64encode
from json import dumps as json_dumps, loads as json_loads
from os import path as os_path
from random import choice as random_choice
from re import compile as re_compile
from re import compile as re_compile, sub as re_sub
from .data_client import YouTubeDataClient
from .subtitles import SUBTITLE_SELECTIONS, Subtitles
@@ -39,7 +39,6 @@ from ...kodion.network import get_connect_address
from ...kodion.utils.datetime import fromtimestamp
from ...kodion.utils.file_system import make_dirs
from ...kodion.utils.methods import merge_dicts
from ...kodion.utils.redact import redact_ip_in_uri
class YouTubePlayerClient(YouTubeDataClient):
@@ -790,6 +789,15 @@ class YouTubePlayerClient(YouTubeDataClient):
'-1': ('original', 'main', -6),
}
BAD_STATUSES = frozenset((
'AGE_CHECK_REQUIRED',
'AGE_VERIFICATION_REQUIRED',
'CONTENT_CHECK_REQUIRED',
'LOGIN_REQUIRED',
'CONTENT_NOT_AVAILABLE_IN_THIS_APP',
'ERROR',
'UNPLAYABLE',
))
FAILURE_REASONS = {
'abort': frozenset((
'country',
@@ -804,7 +812,7 @@ class YouTubePlayerClient(YouTubeDataClient):
'inappropriate',
'member',
)),
'retry': frozenset((
'ignore': frozenset((
'try again later',
'unavailable',
'unknown',
@@ -848,11 +856,9 @@ class YouTubePlayerClient(YouTubeDataClient):
INCOGNITO: None,
}
self._visitor_data_key = 'current'
self._auth_client = {}
self._client_groups = (
('custom', clients if clients else ()),
('auth_enabled|initial_request|no_playable_streams', (
'tv_embed',
'tv_unplugged',
'tv',
)),
@@ -888,18 +894,20 @@ class YouTubePlayerClient(YouTubeDataClient):
if not json_data or 'error' not in json_data:
info = (
'video_id: {video_id!r}',
'Client: {client_name!r}',
'Auth: {has_auth!r}',
'video_id: {video_id!r}',
'Client: {client_name!r}',
'Auth: {has_auth!r}',
'Valid visitor: {has_visitor_data}',
)
return None, info, None, data, exception
info = (
'Reason: {error_reason}',
'Message: {error_message}',
'video_id: {video_id!r}',
'Client: {client_name!r}',
'Auth: {has_auth!r}',
'Reason: {error_reason!r}',
'Message: {error_message!r}',
'video_id: {video_id!r}',
'Client: {client_name!r}',
'Auth: {has_auth!r}',
'Valid visitor: {has_visitor_data}',
)
details = json_data['error']
details = {
@@ -1053,14 +1061,14 @@ class YouTubePlayerClient(YouTubeDataClient):
player_config = self._get_player_config()
if not player_config:
return ''
js_url = player_config.get('PLAYER_JS_URL')
if not js_url:
context = player_config.get('WEB_PLAYER_CONTEXT_CONFIGS', {})
for configs in context.values():
if 'jsUrl' in configs:
js_url = configs['jsUrl']
break
js_url = player_config.get('PLAYER_JS_URL')
if not js_url:
context = player_config.get('WEB_PLAYER_CONTEXT_CONFIGS', {})
for configs in context.values():
if 'jsUrl' in configs:
js_url = configs['jsUrl']
break
if not js_url:
return ''
@@ -1086,7 +1094,8 @@ class YouTubePlayerClient(YouTubeDataClient):
error_hook=self._player_error_hook,
video_id=self.video_id,
client_name=client_name,
has_auth=False,
has_auth=client.get('_has_auth'),
has_visitor_data=bool(client.get('_visitor_data')),
cache=False,
)
if not result:
@@ -1130,19 +1139,15 @@ class YouTubePlayerClient(YouTubeDataClient):
if itag in stream_list:
break
url = response['mpd_manifest']
headers = response['client']['headers']
url = self._process_url_params(
response['mpd_manifest'],
mpd_manifest=True,
headers=headers,
)
if not url:
continue
headers = response['client']['headers']
if '?' in url:
url += '&mpd_version=5'
elif url.endswith('/'):
url += 'mpd_version/5'
else:
url += '/mpd_version/5'
stream_list[itag] = self._get_stream_format(
itag=itag,
title='',
@@ -1189,12 +1194,15 @@ class YouTubePlayerClient(YouTubeDataClient):
itags = ('9995', '9996') if is_live else ('9993', '9994')
for client_name, response in responses.items():
url = response['hls_manifest']
client = response['client']
headers = client['headers']
url = self._process_url_params(
response['hls_manifest'],
headers=headers,
)
if not url:
continue
headers = response['client']['headers']
result = self.request(
url,
headers=headers,
@@ -1203,7 +1211,8 @@ class YouTubePlayerClient(YouTubeDataClient):
error_hook=self._player_error_hook,
video_id=self.video_id,
client_name=client_name,
has_auth=False,
has_auth=client.get('_has_auth'),
has_visitor_data=bool(client.get('_visitor_data')),
cache=False,
)
if not result:
@@ -1227,21 +1236,21 @@ class YouTubePlayerClient(YouTubeDataClient):
if itag in stream_list:
continue
url = match.group('url')
yt_format = self._get_stream_format(
itag=itag,
max_height=selected_height,
title='',
url=match.group('url'),
url=url,
meta=meta_info,
headers=headers,
playback_stats=playback_stats,
)
if yt_format is None:
stream_info = redact_ip_in_uri(match.group(1))
self.log.debug(('Unknown itag - {itag}',
'{stream}'),
'{url!u}'),
itag=itag,
stream=stream_info)
url=url)
if (not yt_format
or (yt_format.get('hls/video')
and not yt_format.get('hls/audio'))):
@@ -1310,11 +1319,10 @@ class YouTubePlayerClient(YouTubeDataClient):
else:
new_url = url
new_url = self._process_url_params(new_url,
mpd=False,
headers=headers,
referrer=None,
visitor_data=None)
new_url = self._process_url_params(
new_url,
headers=headers,
)
if not new_url:
continue
@@ -1329,14 +1337,8 @@ class YouTubePlayerClient(YouTubeDataClient):
playback_stats=playback_stats,
)
if yt_format is None:
if url:
stream_map['url'] = redact_ip_in_uri(url)
if conn:
stream_map['conn'] = redact_ip_in_uri(conn)
if stream:
stream_map['stream'] = redact_ip_in_uri(stream)
self.log.debug(('Unknown itag - {itag}',
'{stream}'),
'{stream!p}'),
itag=itag,
stream=stream_map)
if (not yt_format
@@ -1408,11 +1410,12 @@ class YouTubePlayerClient(YouTubeDataClient):
def _process_url_params(self,
url,
mpd=True,
stream_proxy=False,
mpd_manifest=False,
headers=None,
cpn=False,
referrer=False,
visitor_data=False,
referrer=None,
visitor_data=None,
method='POST',
digits_re=re_compile(r'\d+')):
if not url:
@@ -1422,7 +1425,7 @@ class YouTubePlayerClient(YouTubeDataClient):
params = parse_qs(parts.query)
new_params = {}
if 'n' not in params:
if 'n' not in params and '/n/' not in parts.path:
pass
elif not self._calculate_n:
self.log.debug('Decoding of nsig value disabled')
@@ -1465,7 +1468,7 @@ class YouTubePlayerClient(YouTubeDataClient):
or 'https://www.youtube.com/watch?v=%s' % self.video_id,
)
if mpd:
if stream_proxy:
new_params['__id'] = self.video_id
new_params['__method'] = method
new_params['__host'] = [parts.hostname]
@@ -1487,15 +1490,23 @@ class YouTubePlayerClient(YouTubeDataClient):
if cpn is not False:
new_params['cpn'] = cpn or self._generate_cpn()
params.update(new_params)
query_str = urlencode(params, doseq=True)
return parts._replace(
parts = parts._replace(
scheme='http',
netloc=get_connect_address(self._context, as_netloc=True),
path=PATHS.STREAM_PROXY,
query=query_str,
).geturl()
)
elif mpd_manifest:
if 'mpd_version' in params:
new_params['mpd_version'] = ['7']
else:
parts = parts._replace(
path=re_sub(
r'/mpd_version/\d+|/?$',
'/mpd_version/7',
parts.path,
),
)
elif 'ratebypass' not in params and 'range' not in params:
content_length = params.get('clen', [''])[0]
@@ -1504,7 +1515,7 @@ class YouTubePlayerClient(YouTubeDataClient):
if new_params:
params.update(new_params)
query_str = urlencode(params, doseq=True)
return parts._replace(query=query_str).geturl()
parts = parts._replace(query=query_str)
return parts.geturl()
@@ -1541,7 +1552,7 @@ class YouTubePlayerClient(YouTubeDataClient):
'_visitor_data': self._visitor_data[self._visitor_data_key],
}
for client_name in ('tv_embed', 'web'):
for client_name in ('tv_unplugged', 'web'):
client = self.build_client(client_name, client_data)
if not client:
continue
@@ -1552,6 +1563,7 @@ class YouTubePlayerClient(YouTubeDataClient):
video_id=video_id,
client_name=client_name,
has_auth=client.get('_has_auth'),
has_visitor_data=bool(client.get('_visitor_data')),
cache=False,
**client
)
@@ -1645,12 +1657,15 @@ class YouTubePlayerClient(YouTubeDataClient):
_status = None
_reason = None
auth_client = None
visitor_data = self._visitor_data[visitor_data_key]
has_visitor_data = bool(visitor_data)
video_details = {}
microformat = {}
responses = {}
stream_list = {}
bad_statuses = self.BAD_STATUSES
fail = self.FAILURE_REASONS
abort = False
@@ -1722,6 +1737,7 @@ class YouTubePlayerClient(YouTubeDataClient):
video_id=video_id,
client_name=_client_name,
has_auth=_has_auth,
has_visitor_data=has_visitor_data,
cache=False,
pass_data=True,
raise_exc=False,
@@ -1754,6 +1770,8 @@ class YouTubePlayerClient(YouTubeDataClient):
if visitor_data:
client_data['_visitor_data'] = visitor_data
self._visitor_data[visitor_data_key] = visitor_data
has_visitor_data = True
_video_details = _result.get('videoDetails', {})
_microformat = (_result
.get('microformat', {})
@@ -1782,53 +1800,52 @@ class YouTubePlayerClient(YouTubeDataClient):
break
elif _status == 'OK':
break
elif not _playability or _status in {
'AGE_CHECK_REQUIRED',
'AGE_VERIFICATION_REQUIRED',
'CONTENT_CHECK_REQUIRED',
'LOGIN_REQUIRED',
'CONTENT_NOT_AVAILABLE_IN_THIS_APP',
'ERROR',
'UNPLAYABLE',
}:
self.log.warning(('Failed to retrieve video info',
'Status: {status}',
'Reason: {reason}',
'video_id: {video_id!r}',
'Client: {client!r}',
'Auth: {has_auth!r}'),
elif not _playability or _status in bad_statuses:
self.log.warning(('Failed to retrieve stream info',
'Status: {status!r}',
'Reason: {reason!r}',
'video_id: {video_id!r}',
'Client: {client!r}',
'Auth: {has_auth!r}',
'Valid visitor: {has_visitor_data}'),
status=_status,
reason=_reason or 'UNKNOWN',
video_id=video_id,
client=_client_name,
has_auth=_has_auth)
has_auth=_has_auth,
has_visitor_data=has_visitor_data)
fail_reason = _reason.lower()
if any(why in fail_reason for why in fail['auth']):
if _has_auth:
restart = False
elif restart is None and logged_in:
client_data['_auth_requested'] = True
restart = True
else:
continue
break
elif any(why in fail_reason for why in fail['reauth']):
continue
if any(why in fail_reason for why in fail['reauth']):
if _client.get('_auth_required') == 'ignore_fail':
continue
elif client_data.get('_auth_required'):
if client_data.get('_auth_required'):
restart = False
abort = True
elif restart is None and logged_in:
client_data['_auth_required'] = True
restart = True
break
elif any(why in fail_reason for why in fail['abort']):
if any(why in fail_reason for why in fail['abort']):
abort = True
break
elif any(why in fail_reason for why in fail['skip']):
if any(why in fail_reason for why in fail['skip']):
if allow_skip:
break
elif any(why in fail_reason for why in fail['retry']):
continue
if any(why in fail_reason for why in fail['ignore']):
continue
else:
self.log.warning('Unknown playabilityStatus: {status!r}',
@@ -1843,13 +1860,15 @@ class YouTubePlayerClient(YouTubeDataClient):
break
if _status == 'OK':
self.log.debug(('Retrieved video info:',
'video_id: {video_id!r}',
'Client: {client!r}',
'Auth: {has_auth!r}'),
self.log.debug(('Retrieved stream info:',
'video_id: {video_id!r}',
'Client: {client!r}',
'Auth: {has_auth!r}',
'Valid visitor: {has_visitor_data}'),
video_id=video_id,
client=_client_name,
has_auth=_has_auth)
has_auth=_has_auth,
has_visitor_data=has_visitor_data)
video_details = merge_dicts(
_video_details,
@@ -1863,8 +1882,8 @@ class YouTubePlayerClient(YouTubeDataClient):
compare_str=True,
)
if not self._auth_client and _has_auth:
self._auth_client = {
if not auth_client and _has_auth:
auth_client = {
'client': _client.copy(),
'result': _result,
}
@@ -1915,11 +1934,14 @@ class YouTubePlayerClient(YouTubeDataClient):
'duration': 'P' + video_details.get('lengthSeconds', '0') + 'S',
},
'statistics': {
'viewCount': video_details.get('viewCount', ''),
'viewCount': video_details.get('viewCount', '0'),
},
'_partial': True,
}
is_live = video_details.get('isLiveContent') or video_details.get('hasLiveStreamingData')
is_live = (
video_details.get('isLiveContent')
or video_details.get('hasLiveStreamingData')
)
if is_live:
is_live = video_details.get('isLive', False)
live_dvr = video_details.get('isLiveDvrEnabled', False)
@@ -1960,15 +1982,15 @@ class YouTubePlayerClient(YouTubeDataClient):
'subtitles': None,
}
if use_remote_history and self._auth_client:
if use_remote_history and auth_client:
playback_stats = {
'playback_url': 'videostatsPlaybackUrl',
'watchtime_url': 'videostatsWatchtimeUrl',
}
playback_tracking = (self._auth_client
playback_tracking = (auth_client
.get('result', {})
.get('playbackTracking', {}))
cpn = self._auth_client.get('_cpn') or self._generate_cpn()
cpn = auth_client.get('_cpn') or self._generate_cpn()
for key, url_key in playback_stats.items():
url = playback_tracking.get(url_key, {}).get('baseUrl')
@@ -2095,7 +2117,7 @@ class YouTubePlayerClient(YouTubeDataClient):
default_lang_code='und',
codec_re=re_compile(
r'codecs='
r'"((?P<codec>.+?)\.(?P<props>.+))"'
r'"((?P<codec>.+?)(?:\.(?P<props>.+))?)"'
)):
context = self._context
settings = context.get_settings()
@@ -2120,6 +2142,7 @@ class YouTubePlayerClient(YouTubeDataClient):
localize = context.localize
debugging = self.log.debugging
sep = {'__sep__': ' '}
audio_data = {}
video_data = {}
@@ -2178,7 +2201,8 @@ class YouTubePlayerClient(YouTubeDataClient):
if codec.startswith(('vp9', 'vp09')):
codec = 'vp9'
preferred_codec = codec in stream_features
if codec_properties.startswith(('2', '02.')):
if (codec_properties
and codec_properties.startswith(('2', '02.'))):
codec = 'vp9.2'
else:
if codec.startswith('dts'):
@@ -2397,6 +2421,7 @@ class YouTubePlayerClient(YouTubeDataClient):
urls = self._process_url_params(
unquote(url),
stream_proxy=True,
headers=client['headers'],
cpn=client.get('_cpn'),
)
@@ -2441,12 +2466,14 @@ class YouTubePlayerClient(YouTubeDataClient):
mime_group[itag] = quality_group[itag] = details
if log_client:
self.log.debug('{_:{_}^100}', _='=')
self.log.debug('Streams found for %r client:', client_name)
self.log.debug('{_:{_}^100}', _='=', extra=sep)
self.log.debug('Streams found for %r client:',
client_name,
extra=sep)
log_client = False
if log_audio:
if log_audio_header:
self.log.debug('{_:{_}^100}', _='-')
self.log.debug('{_:{_}^100}', _='-', extra=sep)
self.log.debug('{itag:^3}'
' | {container:^4}'
' | {channels:^5}'
@@ -2462,8 +2489,9 @@ class YouTubePlayerClient(YouTubeDataClient):
sample_rate='ASR',
drc='DRC',
codecs='CODECS',
info='INFO')
self.log.debug('{_:{_}^100}', _='-')
info='INFO',
extra=sep)
self.log.debug('{_:{_}^100}', _='-', extra=sep)
log_audio_header = False
self.log.debug('{itag:3}'
' | {container:4}'
@@ -2482,10 +2510,11 @@ class YouTubePlayerClient(YouTubeDataClient):
drc='Y' if is_drc else '-',
codecs='%s (%s)' % (codec, codecs),
language=language,
role_type=role_type)
role_type=role_type,
extra=sep)
elif log_video:
if log_video_header:
self.log.debug('{_:{_}^100}', _='-')
self.log.debug('{_:{_}^100}', _='-', extra=sep)
self.log.debug('{itag:^3}'
' | {container:^4}'
' | {width:>4} x {height:<4}'
@@ -2504,8 +2533,9 @@ class YouTubePlayerClient(YouTubeDataClient):
s3d='3D',
vr='VR',
bitrate='VBR',
codecs='CODECS')
self.log.debug('{_:{_}^100}', _='-')
codecs='CODECS',
extra=sep)
self.log.debug('{_:{_}^100}', _='-', extra=sep)
log_video_header = False
self.log.debug('{itag:3}'
' | {container:4}'
@@ -2525,7 +2555,8 @@ class YouTubePlayerClient(YouTubeDataClient):
s3d='Y' if is_3d else '-',
vr='Y' if is_vr else '-',
bitrate=bitrate // 1000,
codecs='%s (%s)' % (codec, codecs))
codecs='%s (%s)' % (codec, codecs),
extra=sep)
if not video_data and not audio_only:
self.log.debug('No video mime-types found')
@@ -2774,7 +2805,7 @@ class YouTubePlayerClient(YouTubeDataClient):
# + ''.join([''.join([
# '\t\t\t\t<BaseURL>', entity_escape(url), '</BaseURL>\n',
# ]) for url in stream['baseUrl'] if url]) +
'\t\t\t\t<SegmentBase indexRange="{indexRange}">\n'
'\t\t\t\t<SegmentBase indexRange="{indexRange}" timescale="1000">\n'
'\t\t\t\t\t<Initialization range="{initRange}"/>\n'
'\t\t\t\t</SegmentBase>\n'
'\t\t\t</Representation>\n'
@@ -2842,9 +2873,8 @@ class YouTubePlayerClient(YouTubeDataClient):
url = entity_escape(unquote(self._process_url_params(
subtitle['url'],
stream_proxy=True,
headers=headers,
referrer=None,
visitor_data=None,
)))
if not url:
continue

View File

@@ -803,26 +803,21 @@ class YouTubeRequestClient(BaseRequestsClass):
if isinstance(keys, slice):
next_key = path[idx + 1]
parts = result[keys]
if next_key is None:
for part in result[keys]:
new_result = cls.json_traverse(
part,
path[idx + 2:],
default=default,
)
new_path = path[idx + 2:]
for part in parts:
new_result = cls.json_traverse(part, new_path, default)
if not new_result or new_result == default:
continue
return new_result
if isinstance(next_key, range_type):
results_limit = len(next_key)
new_path = path[idx + 2:]
new_results = []
for part in result[keys]:
new_result = cls.json_traverse(
part,
path[idx + 2:],
default=default,
)
for part in parts:
new_result = cls.json_traverse(part, new_path, default)
if not new_result or new_result == default:
continue
new_results.append(new_result)
@@ -831,9 +826,10 @@ class YouTubeRequestClient(BaseRequestsClass):
break
results_limit -= 1
else:
new_path = path[idx + 1:]
new_results = [
cls.json_traverse(part, path[idx + 1:], default=default)
for part in result[keys]
cls.json_traverse(part, new_path, default)
for part in parts
if part
]
return new_results
@@ -843,7 +839,7 @@ class YouTubeRequestClient(BaseRequestsClass):
for key in keys:
if isinstance(key, tuple):
new_result = cls.json_traverse(result, key, default=default)
new_result = cls.json_traverse(result, key, default)
if new_result:
result = new_result
break
@@ -984,13 +980,16 @@ class YouTubeRequestClient(BaseRequestsClass):
return client
def internet_available(self):
def internet_available(self, notify=True):
response = self.request(**self.CLIENTS['generate_204'])
if response is None:
return False
with response:
if response.status_code == 204:
return True
if response is not None:
with response:
if response.status_code == 204:
return True
if notify:
self._context.get_ui().show_notification(
self._context.localize('internet.connection.required')
)
return False
@classmethod

View File

@@ -80,17 +80,20 @@ class Subtitles(YouTubeRequestClient):
}
def __init__(self, context, video_id, use_mpd=None):
super(Subtitles, self).__init__(context=context)
settings = context.get_settings()
super(Subtitles, self).__init__(
context=context,
language=settings.get_language(),
region=settings.get_region(),
)
self.video_id = video_id
self.defaults = None
self.headers = None
self.renderer = None
self.caption_tracks = None
self.translation_langs = None
settings = context.get_settings()
self.pre_download = settings.subtitle_download()
self.sub_selection = settings.get_subtitle_selection()
stream_features = settings.stream_features()
@@ -99,26 +102,33 @@ class Subtitles(YouTubeRequestClient):
use_isa = not self.pre_download and use_mpd
self.use_isa = use_isa
default_format = None
fallback_format = None
if use_isa:
if ('ttml' in stream_features
and context.inputstream_adaptive_capabilities('ttml')):
self.FORMATS['_default'] = 'ttml'
self.FORMATS['_fallback'] = 'ttml'
default_format = 'ttml'
fallback_format = 'ttml'
if context.inputstream_adaptive_capabilities('vtt'):
if 'vtt' in stream_features:
self.FORMATS.setdefault('_default', 'vtt')
self.FORMATS['_fallback'] = 'vtt'
default_format = default_format or 'vtt'
fallback_format = 'vtt'
else:
self.FORMATS.setdefault('_default', 'srt')
self.FORMATS['_fallback'] = 'srt'
else:
default_format = default_format or 'srt'
fallback_format = 'srt'
if not default_format or not use_isa:
if ('vtt' in stream_features
and context.get_system_version().compatible(20)):
self.FORMATS['_default'] = 'vtt'
self.FORMATS['_fallback'] = 'vtt'
default_format = 'vtt'
fallback_format = 'vtt'
else:
self.FORMATS['_default'] = 'srt'
self.FORMATS['_fallback'] = 'srt'
default_format = 'srt'
fallback_format = 'srt'
self.FORMATS['_default'] = default_format
self.FORMATS['_fallback'] = fallback_format
kodi_sub_lang = context.get_subtitle_language()
plugin_lang = settings.get_language()
@@ -146,14 +156,14 @@ class Subtitles(YouTubeRequestClient):
headers.pop('Content-Type', None)
self.headers = headers
self.renderer = captions.get('playerCaptionsTracklistRenderer', {})
self.caption_tracks = self.renderer.get('captionTracks', [])
self.translation_langs = self.renderer.get('translationLanguages', [])
renderer = captions.get('playerCaptionsTracklistRenderer', {})
self.caption_tracks = renderer.get('captionTracks', [])
self.translation_langs = renderer.get('translationLanguages', [])
self.translation_langs.extend(TRANSLATION_LANGUAGES)
try:
default_audio = self.renderer.get('defaultAudioTrackIndex')
default_audio = self.renderer.get('audioTracks')[default_audio]
default_audio = renderer.get('defaultAudioTrackIndex')
default_audio = renderer.get('audioTracks')[default_audio]
except (IndexError, TypeError):
default_audio = None
@@ -167,7 +177,7 @@ class Subtitles(YouTubeRequestClient):
if default_audio is None:
return
default_caption = self.renderer.get(
default_caption = renderer.get(
'defaultTranslationSourceTrackIndices', [None]
)[0]
@@ -447,13 +457,12 @@ class Subtitles(YouTubeRequestClient):
subtitle_url = self._set_query_param(
base_url,
('type', 'track'),
('fmt', sub_format),
('tlang', tlang),
('xosf', None),
)
self.log.debug(('Found new subtitle for: {lang!r}',
'URL: {url}'),
'URL: {url!u}'),
lang=lang,
url=subtitle_url)

View File

@@ -100,13 +100,16 @@ class ResourceManager(object):
result = data_cache.get_items(
ids,
None if forced_cache else data_cache.ONE_DAY,
memory_store=self.new_data,
)
to_update = [id_ for id_ in ids
if id_
and (id_ not in result
or not result[id_]
or result[id_].get('_partial'))]
to_update = (
[]
if forced_cache else
[id_ for id_ in ids
if id_
and (id_ not in result
or not result[id_]
or result[id_].get('_partial'))]
)
if result:
self.log.debugging and self.log.debug(
@@ -149,7 +152,7 @@ class ResourceManager(object):
# Re-sort result to match order of requested IDs
# Will only work in Python v3.7+
if handles or list(result) != ids[:len(result)]:
if result and (handles or list(result) != ids[:len(result)]):
result = {
handles.get(id_, id_): result[id_]
for id_ in ids
@@ -190,13 +193,16 @@ class ResourceManager(object):
result.update(data_cache.get_items(
to_check,
None if forced_cache else data_cache.ONE_MONTH,
memory_store=self.new_data,
))
to_update = [id_ for id_ in ids
if id_
and (id_ not in result
or not result[id_]
or result[id_].get('_partial'))]
to_update = (
[]
if forced_cache else
[id_ for id_ in ids
if id_
and (id_ not in result
or not result[id_]
or result[id_].get('_partial'))]
)
if result:
self.log.debugging and self.log.debug(
@@ -237,6 +243,9 @@ class ResourceManager(object):
result.update(new_data)
self.cache_data(new_data, defer=defer_cache)
if not result:
return result
banners = (
'bannerTvMediumImageUrl',
'bannerTvLowImageUrl',
@@ -297,13 +306,16 @@ class ResourceManager(object):
result = data_cache.get_items(
ids,
None if forced_cache else data_cache.ONE_DAY,
memory_store=self.new_data,
)
to_update = [id_ for id_ in ids
if id_
and (id_ not in result
or not result[id_]
or result[id_].get('_partial'))]
to_update = (
[]
if forced_cache else
[id_ for id_ in ids
if id_
and (id_ not in result
or not result[id_]
or result[id_].get('_partial'))]
)
if result:
self.log.debugging and self.log.debug(
@@ -346,7 +358,7 @@ class ResourceManager(object):
# Re-sort result to match order of requested IDs
# Will only work in Python v3.7+
if list(result) != ids[:len(result)]:
if result and list(result) != ids[:len(result)]:
result = {
id_: result[id_]
for id_ in ids
@@ -410,11 +422,15 @@ class ResourceManager(object):
as_dict=True,
)
if not batch:
to_update.append(batch_id)
if not forced_cache:
to_update.append(batch_id)
break
age = batch.get('age')
batch = batch.get('value')
if forced_cache:
if not batch:
to_update.append(batch_id)
break
elif forced_cache:
result[batch_id] = batch
elif page_token:
if age <= data_cache.ONE_DAY:
@@ -477,6 +493,9 @@ class ResourceManager(object):
for batch_id, batch in new_data.items()
}, defer=defer_cache)
if not result:
return result
# Re-sort result to match order of requested IDs
# Will only work in Python v3.7+
if list(result) != batch_ids[:len(result)]:
@@ -562,16 +581,19 @@ class ResourceManager(object):
result = data_cache.get_items(
ids,
None if forced_cache else data_cache.ONE_MONTH,
memory_store=self.new_data,
)
to_update = [id_ for id_ in ids
if id_
and (id_ not in result
or not result[id_]
or result[id_].get('_partial')
or (yt_items_dict
and yt_items_dict.get(id_)
and result[id_].get('_unavailable')))]
to_update = (
[]
if forced_cache else
[id_ for id_ in ids
if id_
and (id_ not in result
or not result[id_]
or result[id_].get('_partial')
or (yt_items_dict
and yt_items_dict.get(id_)
and result[id_].get('_unavailable')))]
)
if result:
self.log.debugging and self.log.debug(
@@ -615,9 +637,12 @@ class ResourceManager(object):
result.update(new_data)
self.cache_data(new_data, defer=defer_cache)
if not result and not new_data and yt_items_dict:
result = yt_items_dict
self.cache_data(result, defer=defer_cache)
if not result:
if yt_items_dict:
result = yt_items_dict
self.cache_data(result, defer=defer_cache)
else:
return result
# Re-sort result to match order of requested IDs
# Will only work in Python v3.7+
@@ -638,33 +663,25 @@ class ResourceManager(object):
return result
def cache_data(self, data=None, defer=False):
if defer:
if data:
self.new_data.update(data)
return
if not data:
return None
if self.new_data:
flush = True
if data:
self.new_data.update(data)
data = self.new_data
else:
flush = False
if data:
if self._incognito:
self.log.debugging and self.log.debug(
('Incognito mode active - discarded data for {num} item(s)',
'IDs: {ids}'),
num=len(data),
ids=list(data),
)
else:
self.log.debugging and self.log.debug(
('Storing new data to cache for {num} item(s)',
'IDs: {ids}'),
num=len(data),
ids=list(data),
)
self._context.get_data_cache().set_items(data)
if flush:
self.new_data = {}
incognito = self._incognito
if not defer and self.log.debugging:
self.log.debug(
(
'Incognito mode active - discarded data for {num} item(s)',
'IDs: {ids}'
) if incognito else (
'Storing new data to cache for {num} item(s)',
'IDs: {ids}'
),
num=len(data),
ids=list(data)
)
return self._context.get_data_cache().set_items(
data,
defer=defer,
flush=incognito,
)

View File

@@ -43,7 +43,11 @@ from ...kodion.items import (
MediaItem,
menu_items,
)
from ...kodion.utils.convert_format import friendly_number, strip_html_from_text
from ...kodion.utils.convert_format import (
channel_filter_split,
friendly_number,
strip_html_from_text,
)
from ...kodion.utils.datetime import (
get_scheduled_start,
parse_to_dt,
@@ -62,12 +66,6 @@ __RE_SEASON_EPISODE = re_compile(
r'\b(?:Season\s*|S)(\d+)|(?:\b(?:Part|Ep.|Episode)\s*|#|E)(\d+)'
)
__RE_URL = re_compile(r'(https?://\S+)')
def extract_urls(text):
return __RE_URL.findall(text)
def get_thumb_timestamp(minutes=15):
seconds = minutes * 60
@@ -149,6 +147,7 @@ def make_comment_item(context, snippet, uri, reply_count=0):
category_label=' - '.join(
(author, context.format_date_short(local_datetime))
),
special_sort=False,
)
else:
comment_item = CommandItem(
@@ -775,9 +774,10 @@ def update_video_items(provider, context, video_id_dict,
media_item.playable = False
media_item.available = False
is_audio_item = isinstance(media_item, AudioItem)
media_item.set_mediatype(
CONTENT.AUDIO_TYPE
if isinstance(media_item, AudioItem) else
if is_audio_item else
CONTENT.VIDEO_TYPE
)
@@ -808,6 +808,15 @@ def update_video_items(provider, context, video_id_dict,
elif upload_status == 'uploaded' and not duration:
media_item.live = True
if not is_audio_item and 'player' in yt_item:
player = yt_item['player']
height = player.get('embedHeight')
width = player.get('embedWidth')
if height and width:
height = int(height)
width = int(width)
media_item.set_aspect_ratio(width / height)
if 'liveStreamingDetails' in yt_item:
streaming_details = yt_item['liveStreamingDetails']
if 'actualStartTime' in streaming_details:
@@ -890,9 +899,13 @@ def update_video_items(provider, context, video_id_dict,
label_stats = []
stats = []
rating = [0, 0]
likes = 0
views = 0
if 'statistics' in yt_item:
for stat, value in yt_item['statistics'].items():
if not value:
continue
label = context.LOCAL_MAP.get('stats.' + stat)
if not label:
continue
@@ -912,21 +925,23 @@ def update_video_items(provider, context, video_id_dict,
)))))
if stat == 'likeCount':
rating[0] = value
likes = value
elif stat == 'viewCount':
rating[1] = value
media_item.set_count(value)
views = value
media_item.set_count(views)
label_stats = ' | '.join(label_stats)
stats = ' | '.join(stats)
if 0 < rating[0] <= rating[1]:
if rating[0] == rating[1]:
if 0 < likes <= views:
if likes == views:
rating = 10
else:
# This is a completely made up, arbitrary ranking score
rating = (10 * (log10(rating[1]) * log10(rating[0]))
/ (log10(rating[0] + rating[1]) ** 2))
rating = (
10 * (log10(views) * log10(likes))
/ (log10(likes + views) ** 2)
)
media_item.set_rating(rating)
# Used for label2, but is poorly supported in skins
@@ -1518,28 +1533,6 @@ def filter_parse(item,
return criteria_met
def channel_filter_split(filters_string):
custom_filters = []
channel_filters = {
filter_string
for filter_string in filters_string.split(',')
if filter_string and custom_filter_split(filter_string, custom_filters)
}
return filters_string, channel_filters, custom_filters
def custom_filter_split(filter_string,
custom_filters,
criteria_re=re_compile(
r'{?{([^}]+)}{([^}]+)}{([^}]+)}}?'
)):
criteria = criteria_re.findall(filter_string)
if not criteria:
return True
custom_filters.append(criteria)
return False
def update_duplicate_items(updated_item,
items,
channel_id=None,

View File

@@ -86,6 +86,7 @@ def _process_list_response(provider,
channel_items_dict = {}
items = []
position = 0
do_callbacks = False
params = context.get_params()
@@ -119,15 +120,14 @@ def _process_list_response(provider,
item_params = yt_item.get('_params') or {}
item_params.update(new_params)
item_id = None
item_id = yt_item.get('id')
snippet = yt_item.get('snippet', {})
video_id = None
playlist_id = None
channel_id = None
if is_youtube:
item_id = yt_item.get('id')
snippet = yt_item.get('snippet', {})
localised_info = snippet.get('localized') or {}
title = (localised_info.get('title')
or snippet.get('title')
@@ -226,8 +226,8 @@ def _process_list_response(provider,
image=image,
fanart=fanart,
plot=description,
video_id=video_id,
channel_id=channel_id)
channel_id=channel_id,
**item_params)
elif kind_type == 'channel':
channel_id = item_id
@@ -241,7 +241,8 @@ def _process_list_response(provider,
fanart=fanart,
plot=description,
category_label=title,
channel_id=channel_id)
channel_id=channel_id,
**item_params)
elif kind_type == 'guidecategory':
item_params['guide_id'] = item_id
@@ -254,7 +255,8 @@ def _process_list_response(provider,
image=image,
fanart=fanart,
plot=description,
category_label=title)
category_label=title,
**item_params)
elif kind_type == 'subscription':
subscription_id = item_id
@@ -272,7 +274,8 @@ def _process_list_response(provider,
plot=description,
category_label=title,
channel_id=channel_id,
subscription_id=subscription_id)
subscription_id=subscription_id,
**item_params)
elif kind_type == 'searchfolder':
if item_filter and item_filter.get(HIDE_SEARCH):
@@ -360,7 +363,8 @@ def _process_list_response(provider,
plot=description,
category_label=title,
channel_id=channel_id,
playlist_id=playlist_id)
playlist_id=playlist_id,
**item_params)
item.available = yt_item.get('_available', False)
elif kind_type == 'playlistitem':
@@ -383,10 +387,10 @@ def _process_list_response(provider,
image=image,
fanart=fanart,
plot=description,
video_id=video_id,
channel_id=channel_id,
playlist_id=playlist_id,
playlist_item_id=playlist_item_id)
playlist_item_id=playlist_item_id,
**item_params)
# date time
published_at = snippet.get('publishedAt')
@@ -415,7 +419,7 @@ def _process_list_response(provider,
image=image,
fanart=fanart,
plot=description,
video_id=video_id)
**item_params)
elif kind_type.startswith('comment'):
if kind_type == 'commentthread':
@@ -435,8 +439,6 @@ def _process_list_response(provider,
snippet,
uri=item_uri,
reply_count=reply_count)
position = snippet.get('position') or len(items)
item.set_track_number(position + 1)
elif kind_type == 'bookmarkitem':
item = BookmarkItem(**item_params)
@@ -514,14 +516,11 @@ def _process_list_response(provider,
item.callback = yt_item.pop('_callback')
do_callbacks = True
if isinstance(item, MediaItem):
# Set track number from playlist, or set to current list length to
if not item.get_special_sort():
# Set track number from playlist, or set to current list position to
# match "Default" (unsorted) sort order
if kind_type == 'playlistitem':
position = snippet.get('position') or len(items)
else:
position = len(items)
item.set_track_number(position + 1)
item.set_track_number(snippet.get('position', position) + 1)
position += 1
items.append(item)
@@ -776,7 +775,7 @@ def response_to_items(provider,
log.error_trace(('Unknown kind', 'Kind: %r'), kind)
break
pre_filler = json_data.get('_pre_filler')
pre_filler = json_data.pop('_pre_filler', None)
if pre_filler:
if hasattr(pre_filler, '__nowrap__'):
_json_data = pre_filler(
@@ -834,7 +833,7 @@ def response_to_items(provider,
filtered_out),
)
post_filler = json_data.get('_post_filler')
post_filler = json_data.pop('_post_filler', None)
num_items = 0
for item in items:
if post_filler and num_items >= remaining:
@@ -966,7 +965,7 @@ def pre_fill(filler, json_data, max_results, exclude=None):
return None
items = json_data.get('items') or []
post_filler = json_data.get('_post_filler')
post_filler = json_data.pop('_post_filler', None)
all_items = []
if exclude is not None:
@@ -1026,7 +1025,7 @@ def post_fill(filler, json_data):
json_data['_post_filler'] = None
return None
pre_filler = json_data.get('_pre_filler')
pre_filler = json_data.pop('_pre_filler', None)
json_data = filler(
page_token=page_token,

View File

@@ -56,6 +56,7 @@ def _do_login(provider, context, client=None, **kwargs):
access_manager = context.get_access_manager()
addon_id = context.get_param('addon_id', None)
localize = context.localize
function_cache = context.get_function_cache()
ui = context.get_ui()
ui.on_ok(localize('sign.multi.title'), localize('sign.multi.text'))
@@ -83,6 +84,13 @@ def _do_login(provider, context, client=None, **kwargs):
except IndexError:
pass
if not function_cache.run(
client.internet_available,
function_cache.ONE_MINUTE * 5,
_refresh=True,
):
break
new_token = ('', expiry_timestamp, '')
try:
json_data = client.request_device_and_user_code(token_idx)
@@ -124,13 +132,8 @@ def _do_login(provider, context, client=None, **kwargs):
if not json_data:
break
log_data = json_data.copy()
if 'access_token' in log_data:
log_data['access_token'] = '<redacted>'
if 'refresh_token' in log_data:
log_data['refresh_token'] = '<redacted>'
logging.debug('Requesting access token: {data!r}',
data=log_data)
logging.debug('Requesting access token: {data!p}',
data=json_data)
if 'error' not in json_data:
access_token = json_data.get('access_token', '')

View File

@@ -22,6 +22,7 @@ from ...kodion.constants import (
BUSY_FLAG,
CHANNEL_ID,
CONTENT,
CONTEXT_MENU,
FORCE_PLAY_PARAMS,
INCOGNITO,
ORDER,
@@ -54,6 +55,7 @@ def _play_stream(provider, context):
video_id = params.get(VIDEO_ID)
if not video_id:
ui.show_notification(context.localize('error.no_streams_found'))
logging.error('No video_id provided')
return False
client = provider.get_client(context)
@@ -74,10 +76,9 @@ def _play_stream(provider, context):
ask_for_quality = settings.ask_for_video_quality()
if ui.pop_property(PLAY_PROMPT_QUALITY) and not screensaver:
ask_for_quality = True
audio_only = not ask_for_quality and settings.audio_only()
if ui.pop_property(PLAY_FORCE_AUDIO):
audio_only = True
else:
audio_only = settings.audio_only()
use_mpd = ((not is_external or settings.alternative_player_mpd())
and settings.use_mpd_videos()
and context.ipc_exec(SERVER_WAKEUP, timeout=5))
@@ -97,7 +98,7 @@ def _play_stream(provider, context):
if not streams:
ui.show_notification(context.localize('error.no_streams_found'))
logging.debug('No streams found')
logging.error('No streams found')
return False
stream = _select_stream(
@@ -113,6 +114,7 @@ def _play_stream(provider, context):
video_type = stream.get('video')
if video_type and video_type.get('rtmpe'):
ui.show_notification(context.localize('error.rtmpe_not_supported'))
logging.error('RTMPE streams are not supported')
return False
if not screensaver and settings.get_bool(settings.PLAY_SUGGESTED):
@@ -180,7 +182,7 @@ def _play_stream(provider, context):
ui.set_property(PLAYER_DATA,
value=playback_data,
process=json.dumps,
log_process=redact_params)
log_redact=True)
ui.set_property(TRAKT_PAUSE_FLAG, raw=True)
context.send_notification(PLAYBACK_INIT, playback_data)
return media_item
@@ -482,7 +484,7 @@ def process_items_for_playlist(context,
command = playlist_player.play_playlist_item(position,
defer=True)
return UriItem(command)
context.sleep(1)
context.sleep(0.1)
else:
playlist_player.play_playlist_item(position)
return items[position - 1]
@@ -536,8 +538,11 @@ def process(provider, context, **_kwargs):
if context.get_handle() == -1:
# This is required to trigger Kodi resume prompt, along with using
# RunPlugin. Prompt will not be used if using PlayMedia
if force_play_params and not params.get(PLAY_STRM):
# RunPlugin. Prompt will not be used if using PlayMedia, however
# Action(Play) does not work in non-video windows
if ((force_play_params or params.get(CONTEXT_MENU))
and not params.get(PLAY_STRM)
and context.is_plugin_folder(name=True)):
return UriItem('command://Action(Play)')
return UriItem('command://{0}'.format(

View File

@@ -23,6 +23,12 @@ from ...kodion.utils.datetime import since_epoch, strptime
def process_pre_run(context):
context.get_function_cache().clear()
settings = context.get_settings()
if not settings.subscriptions_sources(default=False, raw_values=True):
settings.subscriptions_sources(
settings.subscriptions_sources(raw_values=True)
)
def process_language(context, step, steps, **_kwargs):
localize = context.localize
@@ -111,8 +117,9 @@ def process_default_settings(context, step, steps, **_kwargs):
background=False,
) as progress_dialog:
progress_dialog.update()
if settings.httpd_listen() == '0.0.0.0':
settings.httpd_listen('127.0.0.1')
ip_address = settings.httpd_listen()
if ip_address == '0.0.0.0':
ip_address = settings.httpd_listen('127.0.0.1')
if not httpd_status(context):
port = settings.httpd_port()
addresses = get_listen_addresses()
@@ -120,13 +127,17 @@ def process_default_settings(context, step, steps, **_kwargs):
for address in addresses:
progress_dialog.update()
if httpd_status(context, (address, port)):
settings.httpd_listen(address)
ip_address = settings.httpd_listen(address)
break
context.sleep(5)
context.sleep(3)
else:
ui.show_notification(localize('httpd.connect.failed'),
header=localize('httpd'))
settings.httpd_listen('0.0.0.0')
ip_address = None
if ip_address:
ui.on_ok(context.get_name(),
context.localize('client.ip.is.x', ip_address))
return step
@@ -390,21 +401,23 @@ def process_refresh_settings(context, step, steps, **_kwargs):
def process_migrate_watch_history(context, step, steps, **_kwargs):
localize = context.localize
settings = context.get_settings()
access_manager = context.get_access_manager()
watch_history_id = access_manager.get_watch_history_id().upper()
step += 1
if (watch_history_id != 'HL' and context.get_ui().on_yes_no_input(
'{youtube} - {setup_wizard} ({step}/{steps})'.format(
youtube=localize('youtube'),
setup_wizard=localize('setup_wizard'),
step=step,
steps=steps,
),
localize('setup_wizard.prompt.migrate_watch_history'),
)):
if ((watch_history_id != 'HL' or not settings.use_remote_history())
and context.get_ui().on_yes_no_input(
'{youtube} - {setup_wizard} ({step}/{steps})'.format(
youtube=localize('youtube'),
setup_wizard=localize('setup_wizard'),
step=step,
steps=steps,
),
localize('setup_wizard.prompt.migrate_watch_history'),
)):
access_manager.set_watch_history_id('HL')
context.get_settings().use_remote_history(True)
settings.use_remote_history(True)
return step

View File

@@ -30,7 +30,7 @@ from ...kodion.constants import (
VIDEO_ID,
)
from ...kodion.items import DirectoryItem, UriItem
from ...kodion.utils.convert_format import strip_html_from_text
from ...kodion.utils.convert_format import strip_html_from_text, urls_in_text
def _process_related_videos(provider, context, client):
@@ -291,7 +291,7 @@ def _process_description_links(provider, context):
function_cache = context.get_function_cache()
urls = function_cache.run(
utils.extract_urls,
urls_in_text,
function_cache.ONE_DAY,
_refresh=context.refresh_requested(),
text=description,
@@ -544,6 +544,7 @@ def _process_my_subscriptions(provider,
'name': context.localize('my_subscriptions'),
'uri': context.create_uri(my_subscriptions_path),
'image': '{media}/new_uploads.png',
'special_sort': 'top',
},
},
None
@@ -556,6 +557,7 @@ def _process_my_subscriptions(provider,
(my_subscriptions_path, 'shorts')
),
'image': '{media}/shorts.png',
'special_sort': 'top',
},
},
None
@@ -568,6 +570,7 @@ def _process_my_subscriptions(provider,
(my_subscriptions_path, 'live')
),
'image': '{media}/live.png',
'special_sort': 'top',
},
},
],

View File

@@ -20,13 +20,17 @@ def _process_rate_video(provider,
re_match=None,
video_id=None,
current_rating=None,
new_rating=None,
_ratings=('like', 'dislike', 'none')):
ui = context.get_ui()
li_path = ui.get_listitem_info(URI)
localize = context.localize
rating_param = context.get_param('rating', '')
if new_rating is None:
rating_param = context.get_param('rating', '')
else:
rating_param = new_rating
if rating_param:
rating_param = rating_param.lower()
if rating_param not in _ratings:
@@ -100,7 +104,7 @@ def _process_rate_video(provider,
)
def _process_more_for_video(context):
def _process_more_for_video(provider, context):
params = context.get_params()
video_id = params.get(VIDEO_ID)
@@ -122,8 +126,22 @@ def _process_more_for_video(context):
]
result = context.get_ui().on_select(context.localize('video.more'), items)
if result != -1:
context.execute(result)
if result == -1:
return (
False,
{
provider.FALLBACK: False,
provider.FORCE_RETURN: True,
},
)
return (
True,
{
provider.FALLBACK: result,
provider.FORCE_RETURN: True,
provider.POST_RUN: True,
},
)
def process(provider, context, re_match=None, command=None, **kwargs):
@@ -134,6 +152,6 @@ def process(provider, context, re_match=None, command=None, **kwargs):
return _process_rate_video(provider, context, re_match, **kwargs)
if command == 'more':
return _process_more_for_video(context)
return _process_more_for_video(provider, context)
raise KodionException('Unknown video command: %s' % command)

View File

@@ -29,7 +29,7 @@ from .helper import (
yt_subscriptions,
yt_video,
)
from .helper.utils import channel_filter_split, update_duplicate_items
from .helper.utils import update_duplicate_items
from .youtube_exceptions import InvalidGrant, LoginException
from ..kodion import AbstractProvider, logging
from ..kodion.constants import (
@@ -61,7 +61,11 @@ from ..kodion.items import (
VideoItem,
menu_items,
)
from ..kodion.utils.convert_format import strip_html_from_text, to_unicode
from ..kodion.utils.convert_format import (
channel_filter_split,
strip_html_from_text,
to_unicode,
)
from ..kodion.utils.datetime import now, since_epoch
@@ -135,7 +139,7 @@ class Provider(AbstractProvider):
)
self._client.reinit(**kwargs)
def get_client(self, context):
def get_client(self, context, refresh=False):
access_manager = context.get_access_manager()
api_store = context.get_api_store()
settings = context.get_settings()
@@ -192,15 +196,6 @@ class Provider(AbstractProvider):
user=user,
switch=switch)
if not client:
client = YouTubePlayerClient(
context=context,
language=settings.get_language(),
region=settings.get_region(),
configs=configs,
)
self._client = client
if key_details:
keys_changed = access_manager.keys_changed(
addon_id=dev_id,
@@ -221,14 +216,32 @@ class Provider(AbstractProvider):
self.log.info('API key set changed - Signing out')
yt_login.process(yt_login.SIGN_OUT, self, context)
if api_last_origin != origin:
self.log.info(('API key origin changed - Resetting client',
'Previous: {old!r}',
'Current: {new!r}'),
old=api_last_origin,
new=origin)
access_manager.set_last_origin(origin)
client.initialised = False
(
access_tokens,
num_access_tokens,
_,
) = access_manager.get_access_tokens(dev_id)
if client:
if api_last_origin != origin:
access_manager.set_last_origin(origin)
self.log.info(('API key origin changed - Resetting client',
'Previous: {old!r}',
'Current: {new!r}'),
old=api_last_origin,
new=origin)
client.initialised = False
else:
client = YouTubePlayerClient(
context=context,
language=settings.get_language(),
region=settings.get_region(),
configs=configs,
access_tokens=access_tokens,
)
self._client = client
if api_last_origin != origin:
access_manager.set_last_origin(origin)
if not client.initialised:
self.reset_client(
@@ -237,33 +250,33 @@ class Provider(AbstractProvider):
region=settings.get_region(),
items_per_page=settings.items_per_page(),
configs=configs,
access_tokens=access_tokens,
)
(
access_tokens,
num_access_tokens,
_,
) = access_manager.get_access_tokens(dev_id)
(
refresh_tokens,
num_refresh_tokens,
) = access_manager.get_refresh_tokens(dev_id)
if num_access_tokens and client.logged_in:
self.log.debug('User is %s logged in', client.logged_in)
return client
if num_access_tokens or num_refresh_tokens:
self.log.debug(('# Access tokens: %d',
'# Refresh tokens: %d'),
num_access_tokens,
num_refresh_tokens)
else:
self.log.debug('User is not logged in')
if not num_access_tokens and not num_refresh_tokens:
access_manager.update_access_token(dev_id, access_token='')
return client
if num_access_tokens == num_refresh_tokens and client.logged_in:
return client
self.log.debug(('# Access tokens: %d',
'# Refresh tokens: %d'),
num_access_tokens,
num_refresh_tokens)
# create new access tokens
with client:
# create new access tokens
function_cache = context.get_function_cache()
if not function_cache.run(
client.internet_available,
function_cache.ONE_MINUTE * 5,
_refresh=refresh or context.refresh_requested(),
):
num_refresh_tokens = 0
if num_refresh_tokens and num_access_tokens != num_refresh_tokens:
access_tokens = [None, None, None, None]
token_expiry = 0
@@ -306,14 +319,7 @@ class Provider(AbstractProvider):
access_token='',
refresh_token=refresh_token,
)
client.set_access_token({
client.TOKEN_TYPES[idx]: token
for idx, token in enumerate(access_tokens)
if token
})
client.set_access_token(access_tokens)
return client
def get_resource_manager(self, context, progress_dialog=None):
@@ -400,6 +406,9 @@ class Provider(AbstractProvider):
},
'_available': True,
'_partial': True,
'_params': {
'special_sort': 'top',
},
},
{
'kind': 'youtube#playlistShortsFolder',
@@ -412,6 +421,9 @@ class Provider(AbstractProvider):
}},
},
'_partial': True,
'_params': {
'special_sort': 'top',
},
} if not params.get(HIDE_SHORTS) else None,
{
'kind': 'youtube#playlistLiveFolder',
@@ -424,6 +436,9 @@ class Provider(AbstractProvider):
}},
},
'_partial': True,
'_params': {
'special_sort': 'top',
},
} if not params.get(HIDE_LIVE) else None,
]
else:
@@ -760,6 +775,7 @@ class Provider(AbstractProvider):
'title': context.localize('playlists'),
'image': '{media}/playlist.png',
CHANNEL_ID: channel_id,
'special_sort': 'top',
},
} if not params.get(HIDE_PLAYLISTS) else None,
{
@@ -768,6 +784,7 @@ class Provider(AbstractProvider):
'title': context.localize('search'),
'image': '{media}/search.png',
CHANNEL_ID: channel_id,
'special_sort': 'top',
},
} if not params.get(HIDE_SEARCH) else None,
{
@@ -781,6 +798,9 @@ class Provider(AbstractProvider):
}},
},
'_partial': True,
'_params': {
'special_sort': 'top',
},
} if uploads and not params.get(HIDE_SHORTS) else None,
{
'kind': 'youtube#playlistLiveFolder',
@@ -793,6 +813,9 @@ class Provider(AbstractProvider):
}},
},
'_partial': True,
'_params': {
'special_sort': 'top',
},
} if uploads and not params.get(HIDE_LIVE) else None,
{
'kind': 'youtube#playlistMembersFolder',
@@ -805,6 +828,9 @@ class Provider(AbstractProvider):
}},
},
'_partial': True,
'_params': {
'special_sort': 'top',
},
} if uploads and not params.get(HIDE_MEMBERS) else None,
],
}
@@ -937,7 +963,7 @@ class Provider(AbstractProvider):
re_match.group('mode'),
provider,
context,
client=provider.get_client(context),
client=provider.get_client(context, refresh=True),
)
def _search_channel_or_playlist(self,
@@ -972,6 +998,7 @@ class Provider(AbstractProvider):
return False, {
self.CACHE_TO_DISC: False,
self.FALLBACK: query,
self.POST_RUN: True,
}
result = self._search_channel_or_playlist(context, query)
@@ -1374,6 +1401,7 @@ class Provider(AbstractProvider):
provider.CONTENT_TYPE: {
'category_label': localize('youtube'),
},
provider.CACHE_TO_DISC: False,
}
# sign in
@@ -1747,19 +1775,27 @@ class Provider(AbstractProvider):
if settings_bool(settings.SHOW_SETUP_WIZARD, True):
settings_menu_item = DirectoryItem(
localize('setup_wizard'),
create_uri(('config', 'setup_wizard')),
create_uri(PATHS.SETUP_WIZARD),
image='{media}/settings.png',
action=True,
)
context_menu = [
menu_items.open_settings(context)
]
settings_menu_item.add_context_menu(context_menu)
result.append(settings_menu_item)
if settings_bool(settings.SHOW_SETTINGS):
settings_menu_item = DirectoryItem(
localize('settings'),
create_uri(('config', 'youtube')),
create_uri(PATHS.SETTINGS),
image='{media}/settings.png',
action=True,
)
context_menu = [
menu_items.open_setup_wizard(context)
]
settings_menu_item.add_context_menu(context_menu)
result.append(settings_menu_item)
return result, options
@@ -2035,8 +2071,8 @@ class Provider(AbstractProvider):
return (
True,
{
provider.FORCE_REFRESH: context.get_path().startswith(
PATHS.BOOKMARKS
provider.FORCE_REFRESH: context.is_plugin_folder(
PATHS.BOOKMARKS,
),
},
)