mirror of
https://github.com/anxdpanic/plugin.video.youtube.git
synced 2025-12-06 02:30:50 -08:00
2176 lines
77 KiB
Python
2176 lines
77 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
|
|
Copyright (C) 2014-2016 bromix (plugin.video.youtube)
|
|
Copyright (C) 2016-2018 plugin.video.youtube
|
|
|
|
SPDX-License-Identifier: GPL-2.0-only
|
|
See LICENSES/GPL-2.0-only for more information.
|
|
"""
|
|
|
|
from __future__ import absolute_import, division, unicode_literals
|
|
|
|
from atexit import register as atexit_register
|
|
from base64 import b64decode
|
|
from functools import partial
|
|
from json import loads as json_loads
|
|
from re import compile as re_compile
|
|
from weakref import proxy
|
|
|
|
from .client import APICheck, YouTube
|
|
from .helper import (
|
|
ResourceManager,
|
|
UrlResolver,
|
|
UrlToItemConverter,
|
|
v3,
|
|
yt_login,
|
|
yt_play,
|
|
yt_playlist,
|
|
yt_setup_wizard,
|
|
yt_specials,
|
|
yt_subscriptions,
|
|
yt_video,
|
|
)
|
|
from .helper.utils import channel_filter_split, update_duplicate_items
|
|
from .youtube_exceptions import InvalidGrant, LoginException
|
|
from ..kodion import AbstractProvider
|
|
from ..kodion.constants import (
|
|
ADDON_ID,
|
|
CHANNEL_ID,
|
|
CONTENT,
|
|
DEVELOPER_CONFIGS,
|
|
PATHS,
|
|
)
|
|
from ..kodion.items import (
|
|
BaseItem,
|
|
DirectoryItem,
|
|
NewSearchItem,
|
|
SearchItem,
|
|
UriItem,
|
|
VideoItem,
|
|
menu_items,
|
|
)
|
|
from ..kodion.utils import strip_html_from_text, to_unicode
|
|
|
|
|
|
class Provider(AbstractProvider):
|
|
def __init__(self):
|
|
super(Provider, self).__init__()
|
|
self._resource_manager = None
|
|
self._client = None
|
|
self._api_check = None
|
|
self._logged_in = False
|
|
|
|
self.on_video_x = self.register_path(
|
|
'^/video/(?P<command>[^/]+)/?$',
|
|
yt_video.process,
|
|
)
|
|
|
|
self.on_playlist_x = self.register_path(
|
|
'^/playlist/(?P<command>[^/]+)/(?P<category>[^/]+)/?$',
|
|
yt_playlist.process,
|
|
)
|
|
|
|
self.register_path(
|
|
'^/play/?$',
|
|
yt_play.process,
|
|
)
|
|
|
|
self.on_specials_x = self.register_path(
|
|
'^/special/(?P<category>[^/]+)(?:/(?P<sub_category>[^/]+))?/?$',
|
|
yt_specials.process,
|
|
)
|
|
|
|
self.register_path(
|
|
'^/subscriptions/(?P<command>[^/]+)/?$',
|
|
yt_subscriptions.process,
|
|
)
|
|
|
|
atexit_register(self.tear_down)
|
|
|
|
@staticmethod
|
|
def get_wizard_steps():
|
|
steps = [
|
|
yt_setup_wizard.process_default_settings,
|
|
yt_setup_wizard.process_performance_settings,
|
|
yt_setup_wizard.process_language,
|
|
yt_setup_wizard.process_subtitles,
|
|
yt_setup_wizard.process_geo_location,
|
|
yt_setup_wizard.process_old_search_db,
|
|
yt_setup_wizard.process_old_history_db,
|
|
yt_setup_wizard.process_list_detail_settings,
|
|
yt_setup_wizard.process_refresh_settings,
|
|
]
|
|
return steps
|
|
|
|
@staticmethod
|
|
def pre_run_wizard_step(provider, context):
|
|
yt_setup_wizard.process_pre_run(context)
|
|
|
|
def is_logged_in(self):
|
|
return self._logged_in
|
|
|
|
@staticmethod
|
|
def get_dev_config(context, addon_id, dev_configs):
|
|
_dev_config = context.get_ui().pop_property(DEVELOPER_CONFIGS)
|
|
|
|
dev_config = {}
|
|
if _dev_config:
|
|
context.log_warning('Using window property for developer keys is'
|
|
' deprecated. Please use the'
|
|
' youtube_registration module instead')
|
|
try:
|
|
dev_config = json_loads(_dev_config)
|
|
except ValueError:
|
|
context.log_error('Error loading developer key: |invalid json|')
|
|
if not dev_config and addon_id and dev_configs:
|
|
dev_config = dev_configs.get(addon_id)
|
|
|
|
if dev_config and not context.get_settings().allow_dev_keys():
|
|
context.log_debug('Developer config ignored')
|
|
return {}
|
|
|
|
if dev_config:
|
|
dev_main = dev_origin = None
|
|
if {'main', 'origin'}.issubset(dev_config):
|
|
dev_main = dev_config['main']
|
|
dev_origin = dev_config['origin']
|
|
|
|
if not {'system', 'key', 'id', 'secret'}.issubset(dev_main):
|
|
dev_main = None
|
|
|
|
if not dev_main:
|
|
context.log_error('Invalid developer config: |{dev_config}|'
|
|
'\n\texpected: |{{'
|
|
' "origin": ADDON_ID,'
|
|
' "main": {{'
|
|
' "system": SYSTEM_NAME,'
|
|
' "key": API_KEY,'
|
|
' "id": CLIENT_ID,'
|
|
' "secret": CLIENT_SECRET'
|
|
'}}}}|'.format(dev_config=dev_config))
|
|
return {}
|
|
|
|
dev_system = dev_main['system']
|
|
if dev_system == 'JSONStore':
|
|
dev_key = b64decode(dev_main['key'])
|
|
dev_id = b64decode(dev_main['id'])
|
|
dev_secret = b64decode(dev_main['secret'])
|
|
else:
|
|
dev_key = dev_main['key']
|
|
dev_id = dev_main['id']
|
|
dev_secret = dev_main['secret']
|
|
context.log_debug('Using developer config: '
|
|
'|origin: {origin}, system: {system}|'
|
|
.format(origin=dev_origin, system=dev_system))
|
|
return {
|
|
'origin': dev_origin,
|
|
'main': {
|
|
'system': dev_system,
|
|
'id': dev_id,
|
|
'secret': dev_secret,
|
|
'key': dev_key,
|
|
}
|
|
}
|
|
|
|
return {}
|
|
|
|
def reset_client(self):
|
|
self._client = None
|
|
self._api_check = None
|
|
|
|
def get_client(self, context):
|
|
access_manager = context.get_access_manager()
|
|
|
|
if not self._api_check:
|
|
self._api_check = APICheck(context)
|
|
configs = self._api_check.get_configs()
|
|
|
|
dev_id = context.get_param('addon_id')
|
|
if not dev_id or dev_id == ADDON_ID:
|
|
dev_id = dev_keys = None
|
|
origin = ADDON_ID
|
|
else:
|
|
dev_config = self.get_dev_config(
|
|
context, dev_id, configs['developer']
|
|
)
|
|
origin = dev_config.get('origin') or dev_id
|
|
dev_keys = dev_config.get('main')
|
|
|
|
api_last_origin = access_manager.get_last_origin()
|
|
if api_last_origin != origin:
|
|
context.log_debug('API key origin changed: |{old}| to |{new}|'
|
|
.format(old=api_last_origin, new=origin))
|
|
access_manager.set_last_origin(origin)
|
|
self.reset_client()
|
|
|
|
access_tokens = access_manager.get_access_token(dev_id)
|
|
if access_manager.is_access_token_expired(dev_id):
|
|
# reset access_token
|
|
access_tokens = [None, None]
|
|
access_manager.update_access_token(dev_id, access_token='')
|
|
elif self._client:
|
|
return self._client
|
|
|
|
if not dev_id:
|
|
context.log_debug('Selecting YouTube config "{0}"'
|
|
.format(configs['main']['system']))
|
|
elif dev_keys:
|
|
context.log_debug('Selecting YouTube developer config "{0}"'
|
|
.format(dev_id))
|
|
configs['main'] = dev_keys
|
|
else:
|
|
dev_keys = configs['main']
|
|
context.log_debug('Selecting YouTube config "{0}"'
|
|
' w/ developer access tokens'
|
|
.format(dev_keys['system']))
|
|
|
|
refresh_tokens = access_manager.get_refresh_token(dev_id)
|
|
if any(refresh_tokens):
|
|
keys_changed = access_manager.dev_keys_changed(
|
|
dev_id, dev_keys['key'], dev_keys['id'], dev_keys['secret']
|
|
) if dev_id else self._api_check.changed
|
|
if keys_changed:
|
|
context.log_warning('API key set changed: Resetting client'
|
|
' and updating access token')
|
|
access_tokens = [None, None]
|
|
refresh_tokens = [None, None]
|
|
access_manager.update_access_token(
|
|
dev_id, access_token='', expiry=-1, refresh_token=''
|
|
)
|
|
self.reset_client()
|
|
|
|
num_access_tokens = len([1 for token in access_tokens if token])
|
|
num_refresh_tokens = len([1 for token in refresh_tokens if token])
|
|
context.log_debug(
|
|
'Access token count: |{0}|, refresh token count: |{1}|'
|
|
.format(num_access_tokens, num_refresh_tokens)
|
|
)
|
|
|
|
settings = context.get_settings()
|
|
client = YouTube(context=context,
|
|
language=settings.get_language(),
|
|
region=settings.get_region(),
|
|
items_per_page=settings.items_per_page(),
|
|
configs=configs)
|
|
|
|
with client:
|
|
# create new access tokens
|
|
if num_refresh_tokens and num_access_tokens != num_refresh_tokens:
|
|
access_tokens = [None, None]
|
|
token_expiry = 0
|
|
try:
|
|
for token_type, value in enumerate(refresh_tokens):
|
|
if not value:
|
|
continue
|
|
|
|
json_data = client.refresh_token(token_type, value)
|
|
if not json_data:
|
|
continue
|
|
|
|
token = json_data.get('access_token')
|
|
expiry = int(json_data.get('expires_in', 3600))
|
|
if token and expiry > 0:
|
|
access_tokens[token_type] = token
|
|
if not token_expiry or expiry < token_expiry:
|
|
token_expiry = expiry
|
|
|
|
if any(access_tokens) and token_expiry:
|
|
access_manager.update_access_token(
|
|
dev_id,
|
|
access_token=access_tokens,
|
|
expiry=token_expiry,
|
|
)
|
|
else:
|
|
raise InvalidGrant('Failed to refresh access token(s)')
|
|
|
|
except (InvalidGrant, LoginException) as exc:
|
|
self.handle_exception(context, exc)
|
|
# reset access token
|
|
# reset refresh token if InvalidGrant otherwise leave as-is
|
|
# to retry later
|
|
if isinstance(exc, InvalidGrant):
|
|
refresh_token = ''
|
|
else:
|
|
refresh_token = None
|
|
access_manager.update_access_token(
|
|
dev_id,
|
|
refresh_token=refresh_token,
|
|
)
|
|
|
|
num_access_tokens = len([1 for token in access_tokens if token])
|
|
|
|
if num_access_tokens and access_tokens[1]:
|
|
self._logged_in = True
|
|
context.log_debug('User is logged in')
|
|
client.set_access_token(
|
|
personal=access_tokens[1],
|
|
tv=access_tokens[0],
|
|
)
|
|
else:
|
|
self._logged_in = False
|
|
context.log_debug('User is not logged in')
|
|
client.set_access_token(personal='', tv='')
|
|
|
|
self._client = client
|
|
return self._client
|
|
|
|
def get_resource_manager(self, context, progress_dialog=None):
|
|
resource_manager = self._resource_manager
|
|
if not resource_manager or resource_manager.context_changed(context):
|
|
new_resource_manager = ResourceManager(proxy(self),
|
|
context,
|
|
progress_dialog)
|
|
if not resource_manager:
|
|
self._resource_manager = new_resource_manager
|
|
return new_resource_manager
|
|
if progress_dialog:
|
|
resource_manager.update_progress_dialog(progress_dialog)
|
|
return resource_manager
|
|
|
|
@AbstractProvider.register_path('^/uri2addon/?$')
|
|
@staticmethod
|
|
def on_uri2addon(provider, context, uri=None, **_kwargs):
|
|
if uri is None:
|
|
uri = context.get_param('uri')
|
|
skip_title = True
|
|
listing = False
|
|
else:
|
|
skip_title = False
|
|
listing = True
|
|
|
|
if not uri:
|
|
return False, None
|
|
|
|
url_resolver = UrlResolver(context)
|
|
resolved_url = url_resolver.resolve(uri)
|
|
if not resolved_url:
|
|
return False, None
|
|
|
|
url_converter = UrlToItemConverter(flatten=True)
|
|
url_converter.add_url(resolved_url, context)
|
|
items = url_converter.get_items(provider=provider,
|
|
context=context,
|
|
skip_title=skip_title)
|
|
if items:
|
|
return (items if listing else items[0]), None
|
|
|
|
return False, None
|
|
|
|
@AbstractProvider.register_path(
|
|
r'^/channel/(?P<channel_id>[^/]+)'
|
|
r'/playlists/?$')
|
|
@staticmethod
|
|
def on_channel_playlists(provider, context, re_match):
|
|
"""
|
|
Lists all playlists of a channel.
|
|
|
|
plugin://plugin.video.youtube/channel/<CHANNEL_ID>/playlists/
|
|
|
|
* CHANNEL_ID: YouTube Channel ID
|
|
"""
|
|
channel_id = re_match.group('channel_id')
|
|
|
|
params = context.get_params()
|
|
page_token = params.get('page_token', '')
|
|
incognito = params.get('incognito')
|
|
addon_id = params.get('addon_id')
|
|
|
|
new_params = {}
|
|
if incognito:
|
|
new_params['incognito'] = incognito
|
|
if addon_id:
|
|
new_params['addon_id'] = addon_id
|
|
|
|
resource_manager = provider.get_resource_manager(context)
|
|
playlists = resource_manager.get_related_playlists(channel_id)
|
|
uploads = playlists.get('uploads') if playlists else None
|
|
if uploads and uploads.startswith('UU'):
|
|
result = [
|
|
{
|
|
'kind': 'youtube#playlist',
|
|
'id': uploads,
|
|
'snippet': {
|
|
'channelId': channel_id,
|
|
'title': context.localize('uploads'),
|
|
'thumbnails': {'default': {
|
|
'url': 'DefaultVideo.png',
|
|
}},
|
|
},
|
|
'_available': True,
|
|
'_partial': True,
|
|
},
|
|
{
|
|
'kind': 'youtube#playlist',
|
|
'id': uploads.replace('UU', 'UUSH', 1),
|
|
'snippet': {
|
|
'channelId': channel_id,
|
|
'title': context.localize('shorts'),
|
|
'thumbnails': {'default': {
|
|
'url': '{media}/shorts.png',
|
|
}},
|
|
},
|
|
'_partial': True,
|
|
},
|
|
{
|
|
'kind': 'youtube#playlist',
|
|
'id': uploads.replace('UU', 'UULV', 1),
|
|
'snippet': {
|
|
'channelId': channel_id,
|
|
'title': context.localize('live'),
|
|
'thumbnails': {'default': {
|
|
'url': '{media}/live.png',
|
|
}},
|
|
},
|
|
'_partial': True,
|
|
},
|
|
]
|
|
else:
|
|
result = False
|
|
|
|
json_data = resource_manager.get_my_playlists(channel_id, page_token)
|
|
if not json_data:
|
|
return False, None
|
|
|
|
if result and 'items' in json_data:
|
|
result.extend(json_data['items'])
|
|
json_data['items'] = result
|
|
result = v3.response_to_items(provider, context, json_data)
|
|
options = {
|
|
provider.CONTENT_TYPE: {
|
|
'content_type': CONTENT.LIST_CONTENT,
|
|
'sub_type': None,
|
|
'category_label': None,
|
|
},
|
|
}
|
|
return result, options
|
|
|
|
@AbstractProvider.register_path(
|
|
r'^/channel/(?P<channel_id>[^/]+)'
|
|
r'/(?:live|playlist/(?P<playlist_id>UULV[^/]+))/?$'
|
|
)
|
|
@staticmethod
|
|
def on_channel_live(provider,
|
|
context,
|
|
re_match=None,
|
|
channel_id=None,
|
|
playlist_id=None):
|
|
"""
|
|
List live streams for a given channel.
|
|
|
|
plugin://plugin.video.youtube/channel/<CHANNEL_ID>/live
|
|
|
|
or
|
|
|
|
plugin://plugin.video.youtube/channel/<CHANNEL_ID>/playlist/<PLAYLIST_ID>
|
|
|
|
* CHANNEL_ID: YouTube channel ID
|
|
* PLAYLIST_ID: YouTube live stream playlist ID beginning with UULV
|
|
"""
|
|
resource_manager = provider.get_resource_manager(context)
|
|
|
|
if re_match:
|
|
channel_id = re_match.group('channel_id')
|
|
playlist_id = re_match.group('playlist_id')
|
|
if not playlist_id:
|
|
playlists = resource_manager.get_related_playlists(channel_id)
|
|
playlist_id = playlists.get('uploads') if playlists else None
|
|
if playlist_id and playlist_id.startswith('UU'):
|
|
playlist_id = playlist_id.replace('UU', 'UULV', 1)
|
|
if not channel_id or not playlist_id:
|
|
return False, None
|
|
|
|
new_params = {
|
|
'channel_id': channel_id,
|
|
'playlist_id': playlist_id,
|
|
}
|
|
context.parse_params(new_params)
|
|
|
|
batch_id = (playlist_id, context.get_param('page_token') or 0)
|
|
json_data = resource_manager.get_playlist_items(batch_id=batch_id)
|
|
if not json_data:
|
|
return False, None
|
|
|
|
live_streams = provider.get_client(context).get_browse_videos(
|
|
channel_id=channel_id,
|
|
route='streams',
|
|
json_path={
|
|
'items': (
|
|
'contents',
|
|
'twoColumnBrowseResultsRenderer',
|
|
'tabs',
|
|
slice(None),
|
|
'tabRenderer',
|
|
lambda x: (
|
|
x['content']
|
|
if x['title'] == 'Live' else
|
|
None
|
|
),
|
|
'richGridRenderer',
|
|
'contents',
|
|
slice(None),
|
|
'richItemRenderer',
|
|
'content',
|
|
'videoRenderer',
|
|
lambda x: (
|
|
x
|
|
if (x[
|
|
'thumbnailOverlays'
|
|
][
|
|
0
|
|
][
|
|
'thumbnailOverlayTimeStatusRenderer'
|
|
][
|
|
'style'
|
|
]) == 'LIVE' else
|
|
None
|
|
),
|
|
),
|
|
'continuation': None,
|
|
},
|
|
)
|
|
if live_streams and 'items' in live_streams and 'items' in json_data:
|
|
live_streams['items'].extend(json_data['items'])
|
|
json_data['items'] = live_streams['items']
|
|
|
|
result = v3.response_to_items(
|
|
provider, context, json_data,
|
|
allow_duplicates=False,
|
|
item_filter={
|
|
'live_folder': True,
|
|
},
|
|
)
|
|
options = {
|
|
provider.CONTENT_TYPE: {
|
|
'content_type': CONTENT.VIDEO_CONTENT,
|
|
'sub_type': None,
|
|
'category_label': None,
|
|
},
|
|
}
|
|
return result, options
|
|
|
|
@AbstractProvider.register_path(
|
|
r'^/channel/(?P<channel_id>[^/]+)'
|
|
r'/(?:shorts|playlist/(?P<playlist_id>UUSH[^/]+))/?$'
|
|
)
|
|
@staticmethod
|
|
def on_channel_shorts(provider,
|
|
context,
|
|
re_match=None,
|
|
channel_id=None,
|
|
playlist_id=None):
|
|
"""
|
|
List shorts for channel.
|
|
|
|
plugin://plugin.video.youtube/channel/<CHANNEL_ID>/shorts
|
|
|
|
or
|
|
|
|
plugin://plugin.video.youtube/channel/<CHANNEL_ID>/playlist/<PLAYLIST_ID>
|
|
|
|
* CHANNEL_ID: YouTube channel ID
|
|
* PLAYLIST_ID: YouTube live stream playlist ID beginning with UUSH
|
|
"""
|
|
resource_manager = provider.get_resource_manager(context)
|
|
|
|
if re_match:
|
|
channel_id = re_match.group('channel_id')
|
|
playlist_id = re_match.group('playlist_id')
|
|
if not playlist_id:
|
|
playlists = resource_manager.get_related_playlists(channel_id)
|
|
playlist_id = playlists.get('uploads') if playlists else None
|
|
if playlist_id and playlist_id.startswith('UU'):
|
|
playlist_id = playlist_id.replace('UU', 'UUSH', 1)
|
|
if not playlist_id:
|
|
return False, None
|
|
|
|
new_params = {
|
|
'playlist_id': playlist_id,
|
|
}
|
|
if channel_id:
|
|
new_params['channel_id'] = channel_id
|
|
context.parse_params(new_params)
|
|
|
|
batch_id = (playlist_id, context.get_param('page_token') or 0)
|
|
json_data = resource_manager.get_playlist_items(batch_id=batch_id)
|
|
if not json_data:
|
|
return False, None
|
|
|
|
result = v3.response_to_items(
|
|
provider, context, json_data,
|
|
item_filter={
|
|
'shorts': True,
|
|
},
|
|
)
|
|
options = {
|
|
provider.CONTENT_TYPE: {
|
|
'content_type': CONTENT.VIDEO_CONTENT,
|
|
'sub_type': None,
|
|
'category_label': None,
|
|
},
|
|
}
|
|
return result, options
|
|
|
|
@AbstractProvider.register_path(
|
|
r'^(?:/channel/(?P<channel_id>[^/]+))?'
|
|
r'/playlist/(?P<playlist_id>[^/]+)/?$'
|
|
)
|
|
@staticmethod
|
|
def on_playlist(provider, context, re_match):
|
|
"""
|
|
Lists the videos of a playlist.
|
|
|
|
plugin://plugin.video.youtube/channel/<CHANNEL_ID>/playlist/<PLAYLIST_ID>
|
|
|
|
or
|
|
|
|
plugin://plugin.video.youtube/playlist/<PLAYLIST_ID>
|
|
|
|
* CHANNEL_ID: ['mine'|YouTube Channel ID]
|
|
* PLAYLIST_ID: YouTube Playlist ID
|
|
"""
|
|
playlist_id = re_match.group('playlist_id')
|
|
new_params = {
|
|
'playlist_id': playlist_id,
|
|
}
|
|
channel_id = re_match.group('channel_id')
|
|
if channel_id:
|
|
new_params['channel_id'] = channel_id
|
|
context.parse_params(new_params)
|
|
|
|
resource_manager = provider.get_resource_manager(context)
|
|
|
|
batch_id = (playlist_id, context.get_param('page_token') or 0)
|
|
json_data = resource_manager.get_playlist_items(batch_id=batch_id)
|
|
if not json_data:
|
|
return False, None
|
|
|
|
result = v3.response_to_items(provider, context, json_data)
|
|
options = {
|
|
provider.CONTENT_TYPE: {
|
|
'content_type': CONTENT.VIDEO_CONTENT,
|
|
'sub_type': None,
|
|
'category_label': None,
|
|
},
|
|
}
|
|
return result, options
|
|
|
|
@AbstractProvider.register_path(
|
|
r'^/(?P<command>(channel|handle|user))'
|
|
r'/(?P<identifier>[^/]+)/?$')
|
|
@staticmethod
|
|
def on_channel(provider, context, re_match):
|
|
"""
|
|
Lists a playlist folder and all uploaded videos of a channel.
|
|
|
|
plugin://plugin.video.youtube/<ID_TYPE>/<ID>
|
|
|
|
* ID_TYPE: channel|handle|user
|
|
* ID: YouTube ID
|
|
"""
|
|
listitem_channel_id = context.get_listitem_property(CHANNEL_ID)
|
|
|
|
client = provider.get_client(context)
|
|
create_uri = context.create_uri
|
|
params = context.get_params()
|
|
|
|
command = re_match.group('command')
|
|
identifier = re_match.group('identifier')
|
|
|
|
if (command == 'channel'
|
|
and identifier
|
|
and identifier.lower() == 'property'
|
|
and listitem_channel_id
|
|
and listitem_channel_id.lower().startswith(('mine', 'uc'))):
|
|
context.execute('ActivateWindow(Videos, {channel}, return)'.format(
|
|
channel=create_uri(
|
|
(PATHS.CHANNEL, listitem_channel_id,),
|
|
)
|
|
))
|
|
|
|
if command == 'channel' and not identifier:
|
|
return False
|
|
|
|
"""
|
|
This is a helper routine that will retrieve the correct channel ID if we
|
|
only have the handle or username of a channel.
|
|
"""
|
|
if identifier == 'mine':
|
|
command = 'mine'
|
|
elif identifier.startswith('@'):
|
|
command = 'handle'
|
|
if command == 'channel':
|
|
channel_id = identifier
|
|
else:
|
|
channel_id = None
|
|
|
|
if not channel_id:
|
|
function_cache = context.get_function_cache()
|
|
channel_id = function_cache.run(
|
|
client.get_channel_by_identifier,
|
|
function_cache.ONE_MONTH,
|
|
_refresh=context.refresh_requested(),
|
|
**{
|
|
command: True,
|
|
'identifier': identifier,
|
|
}
|
|
)
|
|
if not channel_id:
|
|
return False
|
|
|
|
context.parse_params({
|
|
'channel_id': channel_id,
|
|
})
|
|
|
|
resource_manager = provider.get_resource_manager(context)
|
|
result = []
|
|
options = {
|
|
provider.CONTENT_TYPE: {
|
|
'content_type': CONTENT.VIDEO_CONTENT,
|
|
'sub_type': None,
|
|
'category_label': None,
|
|
},
|
|
}
|
|
|
|
playlists = resource_manager.get_related_playlists(channel_id)
|
|
uploads = playlists.get('uploads') if playlists else None
|
|
if uploads and not uploads.startswith('UU'):
|
|
uploads = None
|
|
|
|
if params.get('page', 1) == 1 and not params.get('hide_folders'):
|
|
v3_response = {
|
|
'kind': 'youtube#pluginListResponse',
|
|
'items': [
|
|
{
|
|
'kind': 'youtube#playlistFolder',
|
|
'id': 'playlists',
|
|
'snippet': {
|
|
'channelId': channel_id,
|
|
'title': context.localize('playlists'),
|
|
'thumbnails': {'default': {
|
|
'url': '{media}/playlist.png',
|
|
}},
|
|
},
|
|
'_partial': True,
|
|
} if not params.get('hide_playlists') else None,
|
|
{
|
|
'kind': 'youtube#searchFolder',
|
|
'id': 'search',
|
|
'snippet': {
|
|
'channelId': channel_id,
|
|
'title': context.localize('search'),
|
|
'thumbnails': {'default': {
|
|
'url': '{media}/search.png',
|
|
}},
|
|
},
|
|
'_partial': True,
|
|
} if not params.get('hide_search') else None,
|
|
{
|
|
'kind': 'youtube#playlist',
|
|
'id': uploads.replace('UU', 'UUSH', 1),
|
|
'snippet': {
|
|
'channelId': channel_id,
|
|
'title': context.localize('shorts'),
|
|
'thumbnails': {'default': {
|
|
'url': '{media}/shorts.png',
|
|
}},
|
|
},
|
|
'_partial': True,
|
|
} if uploads and not params.get('hide_shorts') else None,
|
|
{
|
|
'kind': 'youtube#playlist',
|
|
'id': uploads.replace('UU', 'UULV', 1),
|
|
'snippet': {
|
|
'channelId': channel_id,
|
|
'title': context.localize('live'),
|
|
'thumbnails': {'default': {
|
|
'url': '{media}/live.png',
|
|
}},
|
|
},
|
|
'_partial': True,
|
|
} if uploads and not params.get('hide_live') else None,
|
|
],
|
|
}
|
|
result.extend(v3.response_to_items(provider, context, v3_response))
|
|
|
|
if uploads:
|
|
# The "UULF" videos playlist can only be used if videos in a channel
|
|
# are made public. Use "UU" all uploads playlist and filter instead
|
|
# if viewing personal channel.
|
|
if command != 'mine':
|
|
filtered_uploads = uploads.replace('UU', 'UULF', 1)
|
|
else:
|
|
filtered_uploads = None
|
|
while 1:
|
|
page_token = params.get('page_token')
|
|
if filtered_uploads:
|
|
batch_id = (filtered_uploads, page_token or 0)
|
|
else:
|
|
batch_id = (uploads, page_token or 0)
|
|
|
|
json_data = resource_manager.get_playlist_items(
|
|
batch_id=batch_id,
|
|
defer_cache=False,
|
|
)
|
|
if json_data:
|
|
break
|
|
if filtered_uploads:
|
|
filtered_uploads = None
|
|
continue
|
|
return result, options
|
|
|
|
context.parse_params({
|
|
'playlist_id': filtered_uploads or uploads,
|
|
})
|
|
|
|
if not filtered_uploads:
|
|
filler = partial(
|
|
resource_manager.get_playlist_items,
|
|
ids=(uploads,),
|
|
defer_cache=False,
|
|
)
|
|
json_data['_post_filler'] = filler
|
|
|
|
result.extend(v3.response_to_items(
|
|
provider, context, json_data,
|
|
item_filter={
|
|
'shorts': True,
|
|
'live': False,
|
|
'upcoming_live': False,
|
|
},
|
|
))
|
|
return result, options
|
|
|
|
@AbstractProvider.register_path('^/location/mine/?$')
|
|
@staticmethod
|
|
def on_my_location(provider, context, **_kwargs):
|
|
create_uri = context.create_uri
|
|
localize = context.localize
|
|
settings = context.get_settings()
|
|
result = []
|
|
options = {
|
|
provider.CONTENT_TYPE: {
|
|
'content_type': CONTENT.LIST_CONTENT,
|
|
'sub_type': None,
|
|
'category_label': None,
|
|
},
|
|
}
|
|
|
|
# search
|
|
search_item = SearchItem(
|
|
context,
|
|
image='{media}/search.png',
|
|
location=True
|
|
)
|
|
result.append(search_item)
|
|
|
|
# completed live events
|
|
if settings.get_bool('youtube.folder.completed.live.show', True):
|
|
live_events_item = DirectoryItem(
|
|
localize('live.completed'),
|
|
create_uri(
|
|
PATHS.LIVE_VIDEOS_COMPLETED,
|
|
params={'location': True},
|
|
),
|
|
image='{media}/live.png',
|
|
)
|
|
result.append(live_events_item)
|
|
|
|
# upcoming live events
|
|
if settings.get_bool('youtube.folder.upcoming.live.show', True):
|
|
live_events_item = DirectoryItem(
|
|
localize('live.upcoming'),
|
|
create_uri(
|
|
PATHS.LIVE_VIDEOS_UPCOMING,
|
|
params={'location': True},
|
|
),
|
|
image='{media}/live.png',
|
|
)
|
|
result.append(live_events_item)
|
|
|
|
# live events
|
|
live_events_item = DirectoryItem(
|
|
localize('live'),
|
|
create_uri(
|
|
PATHS.LIVE_VIDEOS,
|
|
params={'location': True},
|
|
),
|
|
image='{media}/live.png',
|
|
)
|
|
result.append(live_events_item)
|
|
|
|
return result, options
|
|
|
|
@AbstractProvider.register_path('^/users/(?P<action>[^/]+)/?$')
|
|
@staticmethod
|
|
def on_users(re_match, **_kwargs):
|
|
action = re_match.group('action')
|
|
return UriItem('script://{addon},users/{action}'.format(
|
|
addon=ADDON_ID, action=action
|
|
))
|
|
|
|
@AbstractProvider.register_path('^/sign/(?P<mode>[^/]+)/?$')
|
|
@staticmethod
|
|
def on_sign(provider, context, re_match):
|
|
sign_out_confirmed = context.get_param('confirmed')
|
|
mode = re_match.group('mode')
|
|
if mode == 'in':
|
|
refresh_tokens = context.get_access_manager().get_refresh_token()
|
|
if any(refresh_tokens):
|
|
yt_login.process('out',
|
|
provider,
|
|
context,
|
|
sign_out_refresh=False)
|
|
|
|
if (not sign_out_confirmed and mode == 'out'
|
|
and context.get_ui().on_yes_no_input(
|
|
context.localize('sign.out'),
|
|
context.localize('are_you_sure')
|
|
)):
|
|
sign_out_confirmed = True
|
|
|
|
if mode == 'in' or (mode == 'out' and sign_out_confirmed):
|
|
yt_login.process(mode, provider, context)
|
|
return True
|
|
|
|
def _search_channel_or_playlist(self,
|
|
context,
|
|
identifier,
|
|
channel_re=re_compile(
|
|
r'U[CU][0-9a-zA-Z_\-]{20,24}'
|
|
),
|
|
playlist_re=re_compile(
|
|
r'[OP]L[0-9a-zA-Z_\-]{30,40}'
|
|
)):
|
|
if channel_re.match(identifier):
|
|
json_data = self.get_client(context).get_channels(identifier)
|
|
elif playlist_re.match(identifier):
|
|
json_data = self.get_client(context).get_playlists(identifier)
|
|
else:
|
|
return None
|
|
|
|
if json_data:
|
|
return v3.response_to_items(self, context, json_data)
|
|
return None
|
|
|
|
def on_search_run(self, context, query=None):
|
|
params = context.get_params()
|
|
if query is None:
|
|
query = to_unicode(params.get('q', ''))
|
|
|
|
# Search by url to access unlisted videos
|
|
if query.startswith(('https://', 'http://')):
|
|
return self.on_uri2addon(provider=self, context=context, uri=query)
|
|
if context.is_plugin_path(query):
|
|
return UriItem(query), {
|
|
self.CACHE_TO_DISC: False,
|
|
self.FALLBACK: False,
|
|
}
|
|
|
|
result = self._search_channel_or_playlist(context, query)
|
|
if result: # found a channel or playlist matching search query
|
|
return result, {
|
|
self.CACHE_TO_DISC: False,
|
|
self.FALLBACK: False,
|
|
self.CONTENT_TYPE: {
|
|
'content_type': CONTENT.LIST_CONTENT,
|
|
'sub_type': None,
|
|
'category_label': query,
|
|
},
|
|
}
|
|
result = []
|
|
|
|
channel_id = params.get('channel_id') or params.get('channelId')
|
|
event_type = params.get('event_type') or params.get('eventType')
|
|
location = params.get('location')
|
|
page_token = params.get('page_token') or params.get('pageToken') or ''
|
|
search_type = params.get('search_type', 'video') or params.get('type')
|
|
|
|
options = {
|
|
self.CACHE_TO_DISC: False,
|
|
self.CONTENT_TYPE: {
|
|
'content_type': (
|
|
CONTENT.VIDEO_CONTENT
|
|
if search_type == 'video' else
|
|
CONTENT.LIST_CONTENT
|
|
),
|
|
'sub_type': None,
|
|
'category_label': query,
|
|
},
|
|
}
|
|
|
|
if params.get('page', 1) == 1 and not params.get('hide_folders'):
|
|
if event_type or search_type != 'video':
|
|
video_params = dict(params,
|
|
search_type='video',
|
|
event_type='')
|
|
item_label = context.localize('videos')
|
|
video_item = DirectoryItem(
|
|
context.get_ui().bold(item_label),
|
|
context.create_uri((context.get_path(),), video_params),
|
|
image='DefaultVideo.png',
|
|
category_label=item_label,
|
|
)
|
|
result.append(video_item)
|
|
|
|
if not channel_id and not location and search_type != 'channel':
|
|
channel_params = dict(params,
|
|
search_type='channel',
|
|
event_type='')
|
|
item_label = context.localize('channels')
|
|
channel_item = DirectoryItem(
|
|
context.get_ui().bold(item_label),
|
|
context.create_uri((context.get_path(),), channel_params),
|
|
image='{media}/channels.png',
|
|
category_label=item_label,
|
|
)
|
|
result.append(channel_item)
|
|
|
|
if not location and search_type != 'playlist':
|
|
playlist_params = dict(params,
|
|
search_type='playlist',
|
|
event_type='')
|
|
item_label = context.localize('playlists')
|
|
playlist_item = DirectoryItem(
|
|
context.get_ui().bold(item_label),
|
|
context.create_uri((context.get_path(),), playlist_params),
|
|
image='{media}/playlist.png',
|
|
category_label=item_label,
|
|
)
|
|
result.append(playlist_item)
|
|
|
|
if not channel_id and event_type != 'live':
|
|
live_params = dict(params,
|
|
search_type='video',
|
|
event_type='live')
|
|
item_label = context.localize('live')
|
|
live_item = DirectoryItem(
|
|
context.get_ui().bold(item_label),
|
|
context.create_uri((context.get_path(),), live_params),
|
|
image='{media}/live.png',
|
|
category_label=item_label,
|
|
)
|
|
result.append(live_item)
|
|
|
|
if event_type and event_type != 'upcoming':
|
|
upcoming_params = dict(params,
|
|
search_type='video',
|
|
event_type='upcoming')
|
|
item_label = context.localize('live.upcoming')
|
|
upcoming_item = DirectoryItem(
|
|
context.get_ui().bold(item_label),
|
|
context.create_uri((context.get_path(),), upcoming_params),
|
|
image='{media}/live.png',
|
|
category_label=item_label,
|
|
)
|
|
result.append(upcoming_item)
|
|
|
|
if event_type and event_type != 'completed':
|
|
completed_params = dict(params,
|
|
search_type='video',
|
|
event_type='completed')
|
|
item_label = context.localize('live.completed')
|
|
completed_item = DirectoryItem(
|
|
context.get_ui().bold(item_label),
|
|
context.create_uri((context.get_path(),), completed_params),
|
|
image='{media}/live.png',
|
|
category_label=item_label,
|
|
)
|
|
result.append(completed_item)
|
|
|
|
search_params = {
|
|
'q': query,
|
|
'channelId': channel_id,
|
|
'type': search_type,
|
|
'eventType': event_type,
|
|
'pageToken': page_token,
|
|
'location': location,
|
|
}
|
|
for param in (context.SEARCH_PARAMS
|
|
.intersection(params.keys())
|
|
.difference(search_params.keys())):
|
|
search_params[param] = params[param]
|
|
|
|
function_cache = context.get_function_cache()
|
|
search_params, json_data = function_cache.run(
|
|
self.get_client(context).search_with_params,
|
|
function_cache.ONE_MINUTE * 10,
|
|
_refresh=context.refresh_requested(),
|
|
params=search_params,
|
|
)
|
|
if not json_data:
|
|
return False, None
|
|
|
|
# Store current search query
|
|
if not params.get('incognito') and not params.get('channel_id'):
|
|
context.get_search_history().add_item(search_params)
|
|
|
|
result.extend(v3.response_to_items(
|
|
self, context, json_data,
|
|
item_filter={
|
|
'live_folder': True,
|
|
} if event_type else {
|
|
'live': False,
|
|
},
|
|
))
|
|
return result, options
|
|
|
|
@AbstractProvider.register_path('^/config/(?P<action>[^/]+)/?$')
|
|
@staticmethod
|
|
def on_configure_addon(provider, context, re_match):
|
|
action = re_match.group('action')
|
|
if action == 'setup_wizard':
|
|
provider.run_wizard(context)
|
|
return False, {provider.FALLBACK: False}
|
|
return UriItem('script://{addon},config/{action}'.format(
|
|
addon=ADDON_ID, action=action
|
|
))
|
|
|
|
@AbstractProvider.register_path(
|
|
'^/my_subscriptions/filter'
|
|
'/(?P<command>add|remove)/?$'
|
|
)
|
|
@staticmethod
|
|
def on_manage_my_subscription_filter(context, re_match, **_kwargs):
|
|
settings = context.get_settings()
|
|
ui = context.get_ui()
|
|
|
|
channel_name = context.get_param('item_name')
|
|
command = re_match.group('command')
|
|
if not channel_name or not command:
|
|
return False, None
|
|
|
|
if not settings.subscriptions_filter_enabled():
|
|
return False, None
|
|
|
|
filter_string, filters_set, custom_filters = channel_filter_split(
|
|
settings.subscriptions_filter()
|
|
)
|
|
|
|
if command == 'add':
|
|
num_filters = len(filters_set)
|
|
filters_set.add(channel_name)
|
|
if len(filters_set) == num_filters:
|
|
return False, None
|
|
elif command == 'remove':
|
|
try:
|
|
filters_set.remove(channel_name)
|
|
except KeyError:
|
|
return False, None
|
|
else:
|
|
return False, None
|
|
|
|
filter_list = list(filters_set)
|
|
if custom_filters:
|
|
filter_list.extend([
|
|
''.join([
|
|
'{' + part + '}'
|
|
for part in condition
|
|
])
|
|
for custom_filter in custom_filters
|
|
for condition in custom_filter
|
|
])
|
|
settings.subscriptions_filter(filter_list)
|
|
|
|
ui.show_notification(context.localize(
|
|
'my_subscriptions.filter.added'
|
|
if command == 'add' else
|
|
'my_subscriptions.filter.removed'
|
|
))
|
|
return True, None
|
|
|
|
@AbstractProvider.register_path(
|
|
r'^/maintenance'
|
|
r'/(?P<action>[^/]+)'
|
|
r'/(?P<target>[^/]+)/?$'
|
|
)
|
|
@staticmethod
|
|
def on_maintenance_actions(provider, context, re_match):
|
|
target = re_match.group('target')
|
|
action = re_match.group('action')
|
|
|
|
if action != 'reset':
|
|
return UriItem(
|
|
'script://{addon},maintenance/{action}/?target={target}'.format(
|
|
addon=ADDON_ID, action=action, target=target,
|
|
)
|
|
)
|
|
|
|
ui = context.get_ui()
|
|
localize = context.localize
|
|
|
|
if target == 'access_manager' and ui.on_yes_no_input(
|
|
context.get_name(), localize('reset.access_manager.check')
|
|
):
|
|
addon_id = context.get_param('addon_id', None)
|
|
access_manager = context.get_access_manager()
|
|
client = provider.get_client(context)
|
|
refresh_tokens = access_manager.get_refresh_token()
|
|
success = True
|
|
if any(refresh_tokens):
|
|
for refresh_token in frozenset(refresh_tokens):
|
|
try:
|
|
if refresh_token:
|
|
client.revoke(refresh_token)
|
|
except LoginException:
|
|
success = False
|
|
provider.reset_client()
|
|
access_manager.update_access_token(
|
|
addon_id, access_token='', expiry=-1, refresh_token='',
|
|
)
|
|
ui.refresh_container()
|
|
ui.show_notification(localize('succeeded' if success else 'failed'))
|
|
else:
|
|
success = False
|
|
return success, None
|
|
|
|
@AbstractProvider.register_path('^/api/update/?$')
|
|
@staticmethod
|
|
def on_api_key_update(context, **_kwargs):
|
|
localize = context.localize
|
|
settings = context.get_settings()
|
|
ui = context.get_ui()
|
|
|
|
params = context.get_params()
|
|
api_key = params.get('api_key')
|
|
client_id = params.get('client_id')
|
|
client_secret = params.get('client_secret')
|
|
enable = params.get('enable')
|
|
|
|
updated_list = []
|
|
log_list = []
|
|
|
|
if api_key:
|
|
settings.api_key(api_key)
|
|
updated_list.append(localize('api.key'))
|
|
log_list.append('Key')
|
|
if client_id:
|
|
settings.api_id(client_id)
|
|
updated_list.append(localize('api.id'))
|
|
log_list.append('Id')
|
|
if client_secret:
|
|
settings.api_secret(client_secret)
|
|
updated_list.append(localize('api.secret'))
|
|
log_list.append('Secret')
|
|
if updated_list:
|
|
ui.show_notification(localize('updated_') % ', '.join(updated_list))
|
|
context.log_debug('Updated API keys: %s' % ', '.join(log_list))
|
|
|
|
client_id = settings.api_id()
|
|
client_secret = settings.api_secret()
|
|
api_key = settings.api_key
|
|
missing_list = []
|
|
log_list = []
|
|
|
|
if enable and client_id and client_secret and api_key:
|
|
ui.show_notification(localize('api.personal.enabled'))
|
|
context.log_debug('Personal API keys enabled')
|
|
elif enable:
|
|
if not api_key:
|
|
missing_list.append(localize('api.key'))
|
|
log_list.append('Key')
|
|
if not client_id:
|
|
missing_list.append(localize('api.id'))
|
|
log_list.append('Id')
|
|
if not client_secret:
|
|
missing_list.append(localize('api.secret'))
|
|
log_list.append('Secret')
|
|
ui.show_notification(localize('api.personal.failed')
|
|
% ', '.join(missing_list))
|
|
context.log_error('Failed to enable personal API keys. Missing: %s'
|
|
% ', '.join(log_list))
|
|
|
|
@staticmethod
|
|
def on_playback_history(provider, context, re_match):
|
|
params = context.get_params()
|
|
command = re_match.group('command')
|
|
if not command:
|
|
return False, None
|
|
|
|
localize = context.localize
|
|
playback_history = context.get_playback_history()
|
|
ui = context.get_ui()
|
|
|
|
if command in {'list', 'play'}:
|
|
items = playback_history.get_items()
|
|
if not items:
|
|
return True, None
|
|
|
|
v3_response = {
|
|
'kind': 'youtube#videoListResponse',
|
|
'items': [
|
|
{
|
|
'kind': 'youtube#video',
|
|
'id': video_id,
|
|
'_partial': True,
|
|
'_context_menu': {
|
|
'context_menu': (
|
|
menu_items.history_remove(
|
|
context, video_id
|
|
),
|
|
menu_items.history_clear(
|
|
context
|
|
),
|
|
),
|
|
'position': 0,
|
|
}
|
|
}
|
|
for video_id in items.keys()
|
|
]
|
|
}
|
|
video_items = v3.response_to_items(provider, context, v3_response)
|
|
if command == 'play':
|
|
return yt_play.process_items_for_playlist(
|
|
context,
|
|
video_items,
|
|
action='play',
|
|
play_from='start',
|
|
)
|
|
options = {
|
|
provider.CONTENT_TYPE: {
|
|
'content_type': CONTENT.VIDEO_CONTENT,
|
|
'sub_type': 'history',
|
|
'category_label': None,
|
|
},
|
|
}
|
|
return video_items, options
|
|
|
|
if command == 'clear':
|
|
if not ui.on_yes_no_input(
|
|
localize('history.clear'),
|
|
localize('history.clear.check')
|
|
):
|
|
return False, {provider.FALLBACK: False}
|
|
|
|
playback_history.clear()
|
|
ui.refresh_container()
|
|
|
|
ui.show_notification(
|
|
localize('completed'),
|
|
time_ms=2500,
|
|
audible=False,
|
|
)
|
|
return True
|
|
|
|
video_id = params.get('video_id')
|
|
if not video_id:
|
|
return False
|
|
|
|
if command == 'remove':
|
|
video_name = params.get('item_name') or video_id
|
|
video_name = to_unicode(video_name)
|
|
if not ui.on_yes_no_input(
|
|
localize('content.remove'),
|
|
localize('content.remove.check') % video_name,
|
|
):
|
|
return False, {provider.FALLBACK: False}
|
|
|
|
playback_history.del_item(video_id)
|
|
ui.refresh_container()
|
|
|
|
ui.show_notification(
|
|
localize('removed') % video_name,
|
|
time_ms=2500,
|
|
audible=False,
|
|
)
|
|
return True
|
|
|
|
play_data = playback_history.get_item(video_id)
|
|
if play_data:
|
|
playback_history_method = playback_history.update_item
|
|
else:
|
|
playback_history_method = playback_history.set_item
|
|
play_data = {
|
|
'play_count': 0,
|
|
'total_time': 0,
|
|
'played_time': 0,
|
|
'played_percent': 0
|
|
}
|
|
|
|
if command == 'mark_unwatched':
|
|
if play_data.get('play_count', 0) > 0:
|
|
play_data['play_count'] = 0
|
|
play_data['played_time'] = 0
|
|
play_data['played_percent'] = 0
|
|
|
|
elif command == 'mark_watched':
|
|
if not play_data.get('play_count', 0):
|
|
play_data['play_count'] = 1
|
|
|
|
elif command == 'reset_resume':
|
|
play_data['played_time'] = 0
|
|
play_data['played_percent'] = 0
|
|
|
|
playback_history_method(video_id, play_data)
|
|
ui.refresh_container()
|
|
return True
|
|
|
|
@staticmethod
|
|
def on_root(provider, context, re_match):
|
|
create_uri = context.create_uri
|
|
localize = context.localize
|
|
settings = context.get_settings()
|
|
settings_bool = settings.get_bool
|
|
bold = context.get_ui().bold
|
|
|
|
_ = provider.get_client(context) # required for self.is_logged_in()
|
|
logged_in = provider.is_logged_in()
|
|
# _.get_my_playlists()
|
|
|
|
result = []
|
|
options = {
|
|
provider.CONTENT_TYPE: {
|
|
'category_label': localize('youtube'),
|
|
},
|
|
}
|
|
|
|
# sign in
|
|
if not logged_in and settings_bool('youtube.folder.sign.in.show', True):
|
|
item_label = localize('sign.in')
|
|
sign_in_item = DirectoryItem(
|
|
bold(item_label),
|
|
create_uri(('sign', 'in')),
|
|
image='{media}/sign_in.png',
|
|
action=True,
|
|
category_label=item_label,
|
|
)
|
|
result.append(sign_in_item)
|
|
|
|
if settings_bool('youtube.folder.my_subscriptions.show', True):
|
|
# my subscription
|
|
item_label = localize('my_subscriptions')
|
|
my_subscriptions_item = DirectoryItem(
|
|
bold(item_label),
|
|
create_uri(PATHS.MY_SUBSCRIPTIONS),
|
|
image='{media}/new_uploads.png',
|
|
category_label=item_label,
|
|
)
|
|
result.append(my_subscriptions_item)
|
|
|
|
if settings_bool('youtube.folder.my_subscriptions_filtered.show'):
|
|
# my subscriptions filtered
|
|
my_subscriptions_filtered_item = DirectoryItem(
|
|
localize('my_subscriptions.filtered'),
|
|
create_uri(PATHS.MY_SUBSCRIPTIONS_FILTERED),
|
|
image='{media}/new_uploads.png',
|
|
)
|
|
result.append(my_subscriptions_filtered_item)
|
|
|
|
access_manager = context.get_access_manager()
|
|
watch_later_id = logged_in and access_manager.get_watch_later_id()
|
|
history_id = logged_in and access_manager.get_watch_history_id()
|
|
local_history = settings.use_local_history()
|
|
|
|
# Home / Recommendations
|
|
if (logged_in
|
|
and settings_bool('youtube.folder.recommendations.show', True)):
|
|
recommendations_item = DirectoryItem(
|
|
localize('recommendations'),
|
|
create_uri(PATHS.RECOMMENDATIONS),
|
|
image='{media}/home.png',
|
|
)
|
|
result.append(recommendations_item)
|
|
|
|
# Related
|
|
if settings_bool('youtube.folder.related.show', True):
|
|
if history_id or local_history:
|
|
related_item = DirectoryItem(
|
|
localize('related_videos'),
|
|
create_uri(PATHS.RELATED_VIDEOS),
|
|
image='{media}/related_videos.png',
|
|
)
|
|
result.append(related_item)
|
|
|
|
# Trending
|
|
if settings_bool('youtube.folder.popular_right_now.show', True):
|
|
trending_item = DirectoryItem(
|
|
localize('trending'),
|
|
create_uri(PATHS.TRENDING),
|
|
image='{media}/trending.png',
|
|
)
|
|
result.append(trending_item)
|
|
|
|
# search
|
|
if settings_bool('youtube.folder.search.show', True):
|
|
search_item = SearchItem(
|
|
context,
|
|
)
|
|
result.append(search_item)
|
|
|
|
if settings_bool('youtube.folder.quick_search.show'):
|
|
quick_search_item = NewSearchItem(
|
|
context,
|
|
name=localize('search.quick'),
|
|
image='{media}/quick_search.png',
|
|
)
|
|
result.append(quick_search_item)
|
|
|
|
if settings_bool('youtube.folder.quick_search_incognito.show'):
|
|
quick_search_incognito_item = NewSearchItem(
|
|
context,
|
|
name=localize('search.quick.incognito'),
|
|
image='{media}/incognito_search.png',
|
|
incognito=True,
|
|
)
|
|
result.append(quick_search_incognito_item)
|
|
|
|
# my location
|
|
if (settings_bool('youtube.folder.my_location.show', True)
|
|
and settings.get_location()):
|
|
my_location_item = DirectoryItem(
|
|
localize('my_location'),
|
|
create_uri(('location', 'mine')),
|
|
image='{media}/location.png',
|
|
)
|
|
result.append(my_location_item)
|
|
|
|
# my channel
|
|
if logged_in and settings_bool('youtube.folder.my_channel.show', True):
|
|
my_channel_item = DirectoryItem(
|
|
localize('my_channel'),
|
|
create_uri((PATHS.CHANNEL, 'mine')),
|
|
image='{media}/user.png',
|
|
)
|
|
result.append(my_channel_item)
|
|
|
|
# watch later
|
|
if settings_bool('youtube.folder.watch_later.show', True):
|
|
if watch_later_id:
|
|
watch_later_item = DirectoryItem(
|
|
localize('watch_later'),
|
|
create_uri(
|
|
(PATHS.CHANNEL, 'mine', 'playlist', watch_later_id,),
|
|
),
|
|
image='{media}/watch_later.png',
|
|
)
|
|
context_menu = [
|
|
menu_items.play_playlist(
|
|
context, watch_later_id
|
|
),
|
|
menu_items.play_playlist_recently_added(
|
|
context, watch_later_id
|
|
),
|
|
menu_items.view_playlist(
|
|
context, watch_later_id
|
|
),
|
|
menu_items.shuffle_playlist(
|
|
context, watch_later_id
|
|
),
|
|
]
|
|
watch_later_item.add_context_menu(context_menu)
|
|
result.append(watch_later_item)
|
|
else:
|
|
watch_later_item = DirectoryItem(
|
|
localize('watch_later'),
|
|
create_uri((PATHS.WATCH_LATER, 'list')),
|
|
image='{media}/watch_later.png',
|
|
)
|
|
context_menu = [
|
|
menu_items.watch_later_local_clear(context),
|
|
menu_items.separator(),
|
|
menu_items.play_all_from(
|
|
context,
|
|
path=PATHS.WATCH_LATER,
|
|
),
|
|
menu_items.play_all_from(
|
|
context,
|
|
path=PATHS.WATCH_LATER,
|
|
order='shuffle',
|
|
),
|
|
]
|
|
watch_later_item.add_context_menu(context_menu)
|
|
result.append(watch_later_item)
|
|
|
|
# liked videos
|
|
if (logged_in
|
|
and settings_bool('youtube.folder.liked_videos.show', True)):
|
|
resource_manager = provider.get_resource_manager(context)
|
|
playlists = resource_manager.get_related_playlists('mine')
|
|
if playlists and 'likes' in playlists:
|
|
liked_list_id = playlists['likes'] or 'WL'
|
|
liked_videos_item = DirectoryItem(
|
|
localize('video.liked'),
|
|
create_uri(
|
|
(PATHS.CHANNEL, 'mine', 'playlist', liked_list_id,),
|
|
),
|
|
image='{media}/likes.png',
|
|
)
|
|
context_menu = [
|
|
menu_items.play_playlist(
|
|
context, liked_list_id
|
|
),
|
|
menu_items.play_playlist_recently_added(
|
|
context, liked_list_id
|
|
),
|
|
menu_items.view_playlist(
|
|
context, liked_list_id
|
|
),
|
|
menu_items.shuffle_playlist(
|
|
context, liked_list_id
|
|
),
|
|
]
|
|
liked_videos_item.add_context_menu(context_menu)
|
|
result.append(liked_videos_item)
|
|
|
|
# disliked videos
|
|
if (logged_in
|
|
and settings_bool('youtube.folder.disliked_videos.show', True)):
|
|
disliked_videos_item = DirectoryItem(
|
|
localize('video.disliked'),
|
|
create_uri(PATHS.DISLIKED_VIDEOS),
|
|
image='{media}/dislikes.png',
|
|
)
|
|
result.append(disliked_videos_item)
|
|
|
|
# history
|
|
if settings_bool('youtube.folder.history.show', False):
|
|
if history_id:
|
|
watch_history_item = DirectoryItem(
|
|
localize('history'),
|
|
create_uri(
|
|
(PATHS.CHANNEL, 'mine', 'playlist', history_id,),
|
|
),
|
|
image='{media}/history.png',
|
|
)
|
|
context_menu = [
|
|
menu_items.play_playlist(
|
|
context, history_id
|
|
),
|
|
menu_items.play_playlist_recently_added(
|
|
context, history_id
|
|
),
|
|
menu_items.view_playlist(
|
|
context, history_id
|
|
),
|
|
menu_items.shuffle_playlist(
|
|
context, history_id
|
|
),
|
|
]
|
|
watch_history_item.add_context_menu(context_menu)
|
|
result.append(watch_history_item)
|
|
elif local_history:
|
|
watch_history_item = DirectoryItem(
|
|
localize('history'),
|
|
create_uri((PATHS.HISTORY, 'list')),
|
|
image='{media}/history.png',
|
|
)
|
|
context_menu = [
|
|
menu_items.history_clear(
|
|
context
|
|
),
|
|
menu_items.separator(),
|
|
menu_items.play_all_from(
|
|
context,
|
|
path=PATHS.HISTORY,
|
|
),
|
|
menu_items.play_all_from(
|
|
context,
|
|
path=PATHS.HISTORY,
|
|
order='shuffle',
|
|
),
|
|
]
|
|
watch_history_item.add_context_menu(context_menu)
|
|
result.append(watch_history_item)
|
|
|
|
# (my) playlists
|
|
if logged_in and settings_bool('youtube.folder.playlists.show', True):
|
|
playlists_item = DirectoryItem(
|
|
localize('playlists'),
|
|
create_uri(
|
|
(PATHS.CHANNEL, 'mine', 'playlists',),
|
|
),
|
|
image='{media}/playlist.png',
|
|
)
|
|
result.append(playlists_item)
|
|
|
|
# saved playlists
|
|
# TODO: re-enable once functionality is restored
|
|
# if logged_in and settings_bool('youtube.folder.saved.playlists.show', True):
|
|
# playlists_item = DirectoryItem(
|
|
# localize('saved.playlists'),
|
|
# create_uri(('special', 'saved_playlists')),
|
|
# image='{media}/playlist.png',
|
|
# )
|
|
# result.append(playlists_item)
|
|
|
|
# subscriptions
|
|
if (logged_in
|
|
and settings_bool('youtube.folder.subscriptions.show', True)):
|
|
subscriptions_item = DirectoryItem(
|
|
localize('subscriptions'),
|
|
create_uri(('subscriptions', 'list')),
|
|
image='{media}/channels.png',
|
|
)
|
|
result.append(subscriptions_item)
|
|
|
|
# bookmarks
|
|
if settings_bool('youtube.folder.bookmarks.show', True):
|
|
bookmarks_item = DirectoryItem(
|
|
localize('bookmarks'),
|
|
create_uri((PATHS.BOOKMARKS, 'list')),
|
|
image='{media}/bookmarks.png',
|
|
)
|
|
context_menu = [
|
|
menu_items.bookmarks_clear(
|
|
context
|
|
),
|
|
menu_items.separator(),
|
|
menu_items.play_all_from(
|
|
context,
|
|
path=PATHS.BOOKMARKS,
|
|
),
|
|
menu_items.play_all_from(
|
|
context,
|
|
path=PATHS.BOOKMARKS,
|
|
order='shuffle',
|
|
),
|
|
]
|
|
bookmarks_item.add_context_menu(context_menu)
|
|
result.append(bookmarks_item)
|
|
|
|
# browse channels
|
|
if (logged_in
|
|
and settings_bool('youtube.folder.browse_channels.show', True)):
|
|
browse_channels_item = DirectoryItem(
|
|
localize('browse_channels'),
|
|
create_uri(('special', 'browse_channels')),
|
|
image='{media}/browse_channels.png',
|
|
)
|
|
result.append(browse_channels_item)
|
|
|
|
# completed live events
|
|
if settings_bool('youtube.folder.completed.live.show', True):
|
|
live_events_item = DirectoryItem(
|
|
localize('live.completed'),
|
|
create_uri(PATHS.LIVE_VIDEOS_COMPLETED),
|
|
image='{media}/live.png',
|
|
)
|
|
result.append(live_events_item)
|
|
|
|
# upcoming live events
|
|
if settings_bool('youtube.folder.upcoming.live.show', True):
|
|
live_events_item = DirectoryItem(
|
|
localize('live.upcoming'),
|
|
create_uri(PATHS.LIVE_VIDEOS_UPCOMING),
|
|
image='{media}/live.png',
|
|
)
|
|
result.append(live_events_item)
|
|
|
|
# live events
|
|
if settings_bool('youtube.folder.live.show', True):
|
|
live_events_item = DirectoryItem(
|
|
localize('live'),
|
|
create_uri(PATHS.LIVE_VIDEOS),
|
|
image='{media}/live.png',
|
|
)
|
|
result.append(live_events_item)
|
|
|
|
# switch user
|
|
if settings_bool('youtube.folder.switch.user.show', True):
|
|
switch_user_item = DirectoryItem(
|
|
localize('user.switch'),
|
|
create_uri(('users', 'switch')),
|
|
image='{media}/user.png',
|
|
action=True,
|
|
)
|
|
result.append(switch_user_item)
|
|
|
|
# sign out
|
|
if logged_in and settings_bool('youtube.folder.sign.out.show', True):
|
|
sign_out_item = DirectoryItem(
|
|
localize('sign.out'),
|
|
create_uri(('sign', 'out')),
|
|
image='{media}/sign_out.png',
|
|
action=True,
|
|
)
|
|
result.append(sign_out_item)
|
|
|
|
if settings_bool('youtube.folder.settings.show', True):
|
|
settings_menu_item = DirectoryItem(
|
|
localize('setup_wizard'),
|
|
create_uri(('config', 'setup_wizard')),
|
|
image='{media}/settings.png',
|
|
action=True,
|
|
)
|
|
result.append(settings_menu_item)
|
|
|
|
if settings_bool('youtube.folder.settings.advanced.show'):
|
|
settings_menu_item = DirectoryItem(
|
|
localize('settings'),
|
|
create_uri(('config', 'youtube')),
|
|
image='{media}/settings.png',
|
|
action=True,
|
|
)
|
|
result.append(settings_menu_item)
|
|
|
|
return result, options
|
|
|
|
@staticmethod
|
|
def on_bookmarks(provider, context, re_match):
|
|
params = context.get_params()
|
|
command = re_match.group('command')
|
|
if not command:
|
|
return False, None
|
|
|
|
if command in {'list', 'play'}:
|
|
bookmarks_list = context.get_bookmarks_list()
|
|
items = bookmarks_list.get_items()
|
|
if not items:
|
|
return True, None
|
|
|
|
v3_response = {
|
|
'kind': 'youtube#pluginListResponse',
|
|
'items': []
|
|
}
|
|
|
|
def _update_bookmark(context, _id, old_item):
|
|
def _update(new_item):
|
|
if isinstance(old_item, float):
|
|
bookmark_timestamp = old_item
|
|
elif isinstance(old_item, BaseItem):
|
|
bookmark_timestamp = old_item.get_bookmark_timestamp()
|
|
else:
|
|
return True
|
|
|
|
if new_item.available:
|
|
new_item.bookmark_id = _id
|
|
new_item.set_bookmark_timestamp(bookmark_timestamp)
|
|
new_item.callback = None
|
|
bookmarks_list.update_item(
|
|
_id,
|
|
repr(new_item),
|
|
bookmark_timestamp,
|
|
)
|
|
else:
|
|
update_duplicate_items(old_item, [new_item])
|
|
new_item.bookmark_id = _id
|
|
new_item.set_bookmark_timestamp(bookmark_timestamp)
|
|
new_item.available = False
|
|
new_item.playable = False
|
|
new_item.set_name(context.get_ui().color(
|
|
'AA808080', new_item.get_name()
|
|
))
|
|
return True
|
|
|
|
return _update
|
|
|
|
for item_id, item in items.items():
|
|
callback = _update_bookmark(context, item_id, item)
|
|
if isinstance(item, float):
|
|
kind = 'youtube#channel'
|
|
yt_id = item_id
|
|
item_name = ''
|
|
partial_result = True
|
|
elif isinstance(item, BaseItem):
|
|
partial_result = False
|
|
|
|
if isinstance(item, VideoItem):
|
|
kind = 'youtube#video'
|
|
yt_id = item.video_id
|
|
else:
|
|
yt_id = getattr(item, 'playlist_id', None)
|
|
if yt_id:
|
|
kind = 'youtube#playlist'
|
|
else:
|
|
kind = 'youtube#channel'
|
|
yt_id = getattr(item, 'channel_id', None)
|
|
item_name = item.get_name()
|
|
else:
|
|
kind = None
|
|
yt_id = None
|
|
item_name = ''
|
|
partial_result = False
|
|
|
|
if not yt_id:
|
|
if isinstance(item, BaseItem):
|
|
item_ids = item.parse_item_ids_from_uri()
|
|
to_delete = False
|
|
for kind in ('video', 'playlist', 'channel'):
|
|
yt_id = item_ids.get(kind + '_id')
|
|
if not yt_id:
|
|
continue
|
|
if yt_id == 'None':
|
|
to_delete = True
|
|
continue
|
|
kind = 'youtube#' + kind
|
|
partial_result = True
|
|
break
|
|
else:
|
|
if to_delete:
|
|
bookmarks_list.del_item(item_id)
|
|
continue
|
|
else:
|
|
continue
|
|
|
|
item = {
|
|
'kind': kind,
|
|
'id': yt_id,
|
|
'_partial': partial_result,
|
|
'_context_menu': {
|
|
'context_menu': (
|
|
menu_items.bookmark_remove(
|
|
context, item_id, item_name
|
|
),
|
|
menu_items.bookmarks_clear(
|
|
context
|
|
),
|
|
),
|
|
'position': 0,
|
|
},
|
|
}
|
|
if callback:
|
|
item['_callback'] = callback
|
|
v3_response['items'].append(item)
|
|
|
|
bookmarks = v3.response_to_items(provider, context, v3_response)
|
|
if command == 'play':
|
|
return yt_play.process_items_for_playlist(
|
|
context,
|
|
bookmarks,
|
|
action='play',
|
|
play_from='start',
|
|
)
|
|
options = {
|
|
provider.CONTENT_TYPE: {
|
|
'content_type': CONTENT.VIDEO_CONTENT,
|
|
'sub_type': None,
|
|
'category_label': None,
|
|
},
|
|
}
|
|
return bookmarks, options
|
|
|
|
ui = context.get_ui()
|
|
localize = context.localize
|
|
|
|
if command == 'clear':
|
|
if not ui.on_yes_no_input(
|
|
context.localize('bookmarks.clear'),
|
|
localize('bookmarks.clear.check')
|
|
):
|
|
return False, {provider.FALLBACK: False}
|
|
|
|
context.get_bookmarks_list().clear()
|
|
ui.refresh_container()
|
|
|
|
ui.show_notification(
|
|
localize('completed'),
|
|
time_ms=2500,
|
|
audible=False,
|
|
)
|
|
return True
|
|
|
|
item_id = params.get('item_id')
|
|
if not item_id:
|
|
return False
|
|
|
|
if command == 'add':
|
|
item = params.get('item')
|
|
context.get_bookmarks_list().add_item(item_id, item)
|
|
|
|
ui.show_notification(
|
|
localize('bookmark.created'),
|
|
time_ms=2500,
|
|
audible=False,
|
|
)
|
|
return True
|
|
|
|
if command == 'remove':
|
|
bookmark_name = params.get('item_name') or localize('bookmark')
|
|
bookmark_name = to_unicode(bookmark_name)
|
|
if not ui.on_yes_no_input(
|
|
localize('content.remove'),
|
|
localize('content.remove.check') % bookmark_name,
|
|
):
|
|
return False, {provider.FALLBACK: False}
|
|
|
|
context.get_bookmarks_list().del_item(item_id)
|
|
ui.refresh_container()
|
|
|
|
ui.show_notification(
|
|
localize('removed') % bookmark_name,
|
|
time_ms=2500,
|
|
audible=False,
|
|
)
|
|
return True
|
|
|
|
return False
|
|
|
|
@staticmethod
|
|
def on_watch_later(provider, context, re_match):
|
|
params = context.get_params()
|
|
command = re_match.group('command')
|
|
if not command:
|
|
return False, None
|
|
|
|
localize = context.localize
|
|
ui = context.get_ui()
|
|
|
|
if command in {'list', 'play'}:
|
|
items = context.get_watch_later_list().get_items()
|
|
if not items:
|
|
return True, None
|
|
|
|
v3_response = {
|
|
'kind': 'youtube#videoListResponse',
|
|
'items': [
|
|
{
|
|
'kind': 'youtube#video',
|
|
'id': video_id,
|
|
'_partial': True,
|
|
'_context_menu': {
|
|
'context_menu': (
|
|
menu_items.watch_later_local_remove(
|
|
context, video_id, item.get_name()
|
|
),
|
|
menu_items.watch_later_local_clear(
|
|
context
|
|
),
|
|
),
|
|
'position': 0,
|
|
}
|
|
}
|
|
for video_id, item in items.items()
|
|
]
|
|
}
|
|
video_items = v3.response_to_items(provider, context, v3_response)
|
|
if command == 'play':
|
|
return yt_play.process_items_for_playlist(
|
|
context,
|
|
video_items,
|
|
action='play',
|
|
play_from='start',
|
|
)
|
|
options = {
|
|
provider.CONTENT_TYPE: {
|
|
'content_type': CONTENT.VIDEO_CONTENT,
|
|
'sub_type': 'watch_later',
|
|
'category_label': None,
|
|
},
|
|
}
|
|
return video_items, options
|
|
|
|
if command == 'clear':
|
|
if not ui.on_yes_no_input(
|
|
localize('watch_later.clear'),
|
|
localize('watch_later.clear.check')
|
|
):
|
|
return False, {provider.FALLBACK: False}
|
|
|
|
context.get_watch_later_list().clear()
|
|
ui.refresh_container()
|
|
|
|
ui.show_notification(
|
|
localize('completed'),
|
|
time_ms=2500,
|
|
audible=False,
|
|
)
|
|
return True
|
|
|
|
video_id = params.get('video_id')
|
|
if not video_id:
|
|
return False
|
|
|
|
if command == 'add':
|
|
item = params.get('item')
|
|
if item:
|
|
context.get_watch_later_list().add_item(video_id, item)
|
|
return True
|
|
|
|
if command == 'remove':
|
|
video_name = params.get('item_name') or localize('untitled')
|
|
video_name = to_unicode(video_name)
|
|
if not ui.on_yes_no_input(
|
|
localize('content.remove'),
|
|
localize('content.remove.check') % video_name,
|
|
):
|
|
return False, {provider.FALLBACK: False}
|
|
|
|
context.get_watch_later_list().del_item(video_id)
|
|
ui.refresh_container()
|
|
|
|
ui.show_notification(
|
|
localize('removed') % video_name,
|
|
time_ms=2500,
|
|
audible=False,
|
|
)
|
|
return True
|
|
|
|
return False
|
|
|
|
def handle_exception(self, context, exception_to_handle):
|
|
if not isinstance(exception_to_handle, (InvalidGrant, LoginException)):
|
|
return False
|
|
|
|
ok_dialog = False
|
|
message_timeout = 5000
|
|
|
|
message = exception_to_handle.get_message()
|
|
msg = exception_to_handle.get_message()
|
|
log_message = exception_to_handle.get_message()
|
|
|
|
error = ''
|
|
code = ''
|
|
if isinstance(msg, dict):
|
|
if 'error_description' in msg:
|
|
message = strip_html_from_text(msg['error_description'])
|
|
log_message = strip_html_from_text(msg['error_description'])
|
|
elif 'message' in msg:
|
|
message = strip_html_from_text(msg['message'])
|
|
log_message = strip_html_from_text(msg['message'])
|
|
else:
|
|
message = 'No error message'
|
|
log_message = 'No error message'
|
|
|
|
if 'error' in msg:
|
|
error = msg['error']
|
|
|
|
if 'code' in msg:
|
|
code = msg['code']
|
|
|
|
if error and code:
|
|
title = '%s: [%s] %s' % ('LoginException', code, error)
|
|
elif error:
|
|
title = '%s: %s' % ('LoginException', error)
|
|
else:
|
|
title = 'LoginException'
|
|
|
|
context.log_error('%s: %s' % (title, log_message))
|
|
|
|
if error == 'deleted_client':
|
|
message = context.localize('key.requirement')
|
|
context.get_access_manager().update_access_token(
|
|
context.get_param('addon_id', None),
|
|
access_token='',
|
|
expiry=-1,
|
|
refresh_token='',
|
|
)
|
|
ok_dialog = True
|
|
|
|
if error == 'invalid_client':
|
|
if message == 'The OAuth client was not found.':
|
|
message = context.localize('client.id.incorrect')
|
|
message_timeout = 7000
|
|
elif message == 'Unauthorized':
|
|
message = context.localize('client.secret.incorrect')
|
|
message_timeout = 7000
|
|
|
|
if ok_dialog:
|
|
context.get_ui().on_ok(title, message)
|
|
else:
|
|
context.get_ui().show_notification(message,
|
|
title,
|
|
time_ms=message_timeout)
|
|
return True
|
|
|
|
def tear_down(self):
|
|
attrs = (
|
|
'_resource_manager',
|
|
'_client',
|
|
'_api_check',
|
|
)
|
|
for attr in attrs:
|
|
try:
|
|
delattr(self, attr)
|
|
setattr(self, attr, None)
|
|
except (AttributeError, TypeError):
|
|
pass
|