Updated kodi settings on Lenovo
This commit is contained in:
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.
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
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.
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user