astrXbian/.install/.kodi/addons/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/utils.py

681 lines
28 KiB
Python
Raw Normal View History

2020-12-17 21:52:17 +01:00
# -*- 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 six import PY2
import re
import time
from ... import kodion
from ...kodion import utils
from ...youtube.helper import yt_context_menu
try:
import inputstreamhelper
except ImportError:
inputstreamhelper = None
__RE_SEASON_EPISODE_MATCHES__ = [re.compile(r'Part (?P<episode>\d+)'),
re.compile(r'#(?P<episode>\d+)'),
re.compile(r'Ep.[^\w]?(?P<episode>\d+)'),
re.compile(r'\[(?P<episode>\d+)\]'),
re.compile(r'S(?P<season>\d+)E(?P<episode>\d+)'),
re.compile(r'Season (?P<season>\d+)(.+)Episode (?P<episode>\d+)'),
re.compile(r'Episode (?P<episode>\d+)')]
def extract_urls(text):
result = []
re_url = re.compile(r'(https?://[^\s]+)')
matches = re_url.findall(text)
result = matches or result
return result
def get_thumb_timestamp(minutes=15):
return str(time.mktime(time.gmtime(minutes * 60 * (round(time.time() / (minutes * 60))))))
def make_comment_item(context, provider, snippet, uri, total_replies=0):
author = '[B]{}[/B]'.format(kodion.utils.to_utf8(snippet['authorDisplayName']))
body = kodion.utils.to_utf8(snippet['textOriginal'])
label_props = None
plot_props = None
is_edited = (snippet['publishedAt'] != snippet['updatedAt'])
str_likes = ('%.1fK' % (snippet['likeCount'] / 1000.0)) if snippet['likeCount'] > 1000 else str(snippet['likeCount'])
str_replies = ('%.1fK' % (total_replies / 1000.0)) if total_replies > 1000 else str(total_replies)
if snippet['likeCount'] and total_replies:
label_props = '[COLOR lime][B]+%s[/B][/COLOR]|[COLOR cyan][B]%s[/B][/COLOR]' % (str_likes, str_replies)
plot_props = '[COLOR lime][B]%s %s[/B][/COLOR]|[COLOR cyan][B]%s %s[/B][/COLOR]' % (str_likes,
context.localize(provider.LOCAL_MAP['youtube.video.comments.likes']), str_replies,
context.localize(provider.LOCAL_MAP['youtube.video.comments.replies']))
elif snippet['likeCount']:
label_props = '[COLOR lime][B]+%s[/B][/COLOR]' % str_likes
plot_props = '[COLOR lime][B]%s %s[/B][/COLOR]' % (str_likes,
context.localize(provider.LOCAL_MAP['youtube.video.comments.likes']))
elif total_replies:
label_props = '[COLOR cyan][B]%s[/B][/COLOR]' % str_replies
plot_props = '[COLOR cyan][B]%s %s[/B][/COLOR]' % (str_replies,
context.localize(provider.LOCAL_MAP['youtube.video.comments.replies']))
else:
pass # The comment has no likes or replies.
# Format the label of the comment item.
edited = '[B]*[/B]' if is_edited else ''
if label_props:
label = '{author} ({props}){edited} {body}'.format(author=author, props=label_props, edited=edited,
body=body.replace('\n', ' '))
else:
label = '{author}{edited} {body}'.format(author=author, edited=edited, body=body.replace('\n', ' '))
# Format the plot of the comment item.
edited = ' (%s)' % context.localize(provider.LOCAL_MAP['youtube.video.comments.edited']) if is_edited else ''
if plot_props:
plot = '{author} ({props}){edited}[CR][CR]{body}'.format(author=author, props=plot_props,
edited=edited, body=body)
else:
plot = '{author}{edited}[CR][CR]{body}'.format(author=author, edited=edited, body=body)
comment_item = kodion.items.DirectoryItem(label, uri)
comment_item.set_plot(plot)
comment_item.set_date_from_datetime(utils.datetime_parser.parse(snippet['publishedAt']))
if not uri:
comment_item.set_action(True) # Cosmetic, makes the item not a folder.
return comment_item
def update_channel_infos(provider, context, channel_id_dict, subscription_id_dict=None, channel_items_dict=None):
if subscription_id_dict is None:
subscription_id_dict = {}
channel_ids = list(channel_id_dict.keys())
if len(channel_ids) == 0:
return
resource_manager = provider.get_resource_manager(context)
channel_data = resource_manager.get_channels(channel_ids)
filter_list = []
if context.get_path() == '/subscriptions/list/':
filter_string = context.get_settings().get_string('youtube.filter.my_subscriptions_filtered.list', '')
filter_string = filter_string.replace(', ', ',')
filter_list = filter_string.split(',')
filter_list = [x.lower() for x in filter_list]
thumb_size = context.get_settings().use_thumbnail_size()
for channel_id in list(channel_data.keys()):
yt_item = channel_data[channel_id]
channel_item = channel_id_dict[channel_id]
snippet = yt_item['snippet']
# title
title = snippet['title']
channel_item.set_name(title)
# image
image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
channel_item.set_image(image)
# - update context menu
context_menu = []
# -- unsubscribe from channel
subscription_id = subscription_id_dict.get(channel_id, '')
if subscription_id:
channel_item.set_channel_subscription_id(subscription_id)
yt_context_menu.append_unsubscribe_from_channel(context_menu, provider, context, subscription_id)
# -- subscribe to the channel
if provider.is_logged_in() and context.get_path() != '/subscriptions/list/':
yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id)
if context.get_path() == '/subscriptions/list/':
channel = title.lower()
channel = channel.replace(',', '')
if PY2:
channel = channel.encode('utf-8', 'ignore')
if channel in filter_list:
yt_context_menu.append_remove_my_subscriptions_filter(context_menu, provider, context, title)
else:
yt_context_menu.append_add_my_subscriptions_filter(context_menu, provider, context, title)
channel_item.set_context_menu(context_menu)
fanart = u''
fanart_images = yt_item.get('brandingSettings', {}).get('image', {})
banners = ['bannerTvMediumImageUrl', 'bannerTvLowImageUrl', 'bannerTvImageUrl']
for banner in banners:
fanart = fanart_images.get(banner, u'')
if fanart:
break
channel_item.set_fanart(fanart)
# update channel mapping
if channel_items_dict is not None:
if channel_id not in channel_items_dict:
channel_items_dict[channel_id] = []
channel_items_dict[channel_id].append(channel_item)
def update_playlist_infos(provider, context, playlist_id_dict, channel_items_dict=None):
playlist_ids = list(playlist_id_dict.keys())
if len(playlist_ids) == 0:
return
resource_manager = provider.get_resource_manager(context)
access_manager = context.get_access_manager()
playlist_data = resource_manager.get_playlists(playlist_ids)
custom_watch_later_id = access_manager.get_watch_later_id()
custom_history_id = access_manager.get_watch_history_id()
thumb_size = context.get_settings().use_thumbnail_size()
for playlist_id in list(playlist_data.keys()):
yt_item = playlist_data[playlist_id]
playlist_item = playlist_id_dict[playlist_id]
snippet = yt_item['snippet']
title = snippet['title']
playlist_item.set_name(title)
image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
playlist_item.set_image(image)
channel_id = snippet['channelId']
# if the path directs to a playlist of our own, we correct the channel id to 'mine'
if context.get_path() == '/channel/mine/playlists/':
channel_id = 'mine'
channel_name = snippet.get('channelTitle', '')
context_menu = []
# play all videos of the playlist
yt_context_menu.append_play_all_from_playlist(context_menu, provider, context, playlist_id)
if provider.is_logged_in():
if channel_id != 'mine':
# subscribe to the channel via the playlist item
yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id,
channel_name)
else:
# remove my playlist
yt_context_menu.append_delete_playlist(context_menu, provider, context, playlist_id, title)
# rename playlist
yt_context_menu.append_rename_playlist(context_menu, provider, context, playlist_id, title)
# remove as my custom watch later playlist
if playlist_id == custom_watch_later_id:
yt_context_menu.append_remove_as_watchlater(context_menu, provider, context, playlist_id, title)
# set as my custom watch later playlist
else:
yt_context_menu.append_set_as_watchlater(context_menu, provider, context, playlist_id, title)
# remove as custom history playlist
if playlist_id == custom_history_id:
yt_context_menu.append_remove_as_history(context_menu, provider, context, playlist_id, title)
# set as custom history playlist
else:
yt_context_menu.append_set_as_history(context_menu, provider, context, playlist_id, title)
if len(context_menu) > 0:
playlist_item.set_context_menu(context_menu)
# update channel mapping
if channel_items_dict is not None:
if channel_id not in channel_items_dict:
channel_items_dict[channel_id] = []
channel_items_dict[channel_id].append(playlist_item)
def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict=None, channel_items_dict=None, live_details=False, use_play_data=True):
settings = context.get_settings()
ui = context.get_ui()
video_ids = list(video_id_dict.keys())
if len(video_ids) == 0:
return
if not playlist_item_id_dict:
playlist_item_id_dict = {}
resource_manager = provider.get_resource_manager(context)
video_data = resource_manager.get_videos(video_ids, live_details=live_details,
suppress_errors=True)
thumb_size = settings.use_thumbnail_size()
thumb_stamp = get_thumb_timestamp()
for video_id in list(video_data.keys()):
datetime = None
yt_item = video_data.get(video_id)
video_item = video_id_dict[video_id]
# set mediatype
video_item.set_mediatype('video') # using video
if not yt_item:
continue
snippet = yt_item['snippet'] # crash if not conform
play_data = yt_item['play_data']
video_item.live = snippet.get('liveBroadcastContent') == 'live'
# duration
if not video_item.live and use_play_data and play_data.get('total_time'):
video_item.set_duration_from_seconds(float(play_data.get('total_time')))
else:
duration = yt_item.get('contentDetails', {}).get('duration', '')
if duration:
duration = utils.datetime_parser.parse(duration)
# we subtract 1 seconds because YouTube returns +1 second to much
video_item.set_duration_from_seconds(duration.seconds - 1)
if not video_item.live and use_play_data:
# play count
if play_data.get('play_count'):
video_item.set_play_count(int(play_data.get('play_count')))
if play_data.get('played_percent'):
video_item.set_start_percent(play_data.get('played_percent'))
if play_data.get('played_time'):
video_item.set_start_time(play_data.get('played_time'))
if play_data.get('last_played'):
video_item.set_last_played(play_data.get('last_played'))
elif video_item.live:
video_item.set_play_count(0)
scheduled_start = video_data[video_id].get('liveStreamingDetails', {}).get('scheduledStartTime')
if scheduled_start:
datetime = utils.datetime_parser.parse(scheduled_start)
video_item.set_scheduled_start_utc(datetime)
start_date, start_time = utils.datetime_parser.get_scheduled_start(datetime)
if start_date:
title = u'({live} {date}@{time}) {title}' \
.format(live=context.localize(provider.LOCAL_MAP['youtube.live']), date=start_date, time=start_time, title=snippet['title'])
else:
title = u'({live} @ {time}) {title}' \
.format(live=context.localize(provider.LOCAL_MAP['youtube.live']), date=start_date, time=start_time, title=snippet['title'])
video_item.set_title(title)
else:
# set the title
if not video_item.get_title():
video_item.set_title(snippet['title'])
"""
This is experimental. We try to get the most information out of the title of a video.
This is not based on any language. In some cases this won't work at all.
TODO: via language and settings provide the regex for matching episode and season.
"""
# video_item.set_season(1)
# video_item.set_episode(1)
for regex in __RE_SEASON_EPISODE_MATCHES__:
re_match = regex.search(video_item.get_name())
if re_match:
if 'season' in re_match.groupdict():
video_item.set_season(int(re_match.group('season')))
if 'episode' in re_match.groupdict():
video_item.set_episode(int(re_match.group('episode')))
break
# plot
channel_name = snippet.get('channelTitle', '')
description = kodion.utils.strip_html_from_text(snippet['description'])
if channel_name and settings.get_bool('youtube.view.description.show_channel_name', True):
description = '%s[CR][CR]%s' % (ui.uppercase(ui.bold(channel_name)), description)
video_item.set_studio(channel_name)
# video_item.add_cast(channel_name)
video_item.add_artist(channel_name)
video_item.set_plot(description)
# date time
if not datetime and 'publishedAt' in snippet and snippet['publishedAt']:
datetime = utils.datetime_parser.parse(snippet['publishedAt'])
video_item.set_aired_utc(utils.datetime_parser.strptime(snippet['publishedAt']))
if datetime:
video_item.set_year_from_datetime(datetime)
video_item.set_aired_from_datetime(datetime)
video_item.set_premiered_from_datetime(datetime)
video_item.set_date_from_datetime(datetime)
# try to find a better resolution for the image
image = video_item.get_image()
if not image:
image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
if image.endswith('_live.jpg'):
image = ''.join([image, '?ct=', thumb_stamp])
video_item.set_image(image)
# set fanart
video_item.set_fanart(provider.get_fanart(context))
# update channel mapping
channel_id = snippet.get('channelId', '')
if channel_items_dict is not None:
if channel_id not in channel_items_dict:
channel_items_dict[channel_id] = []
channel_items_dict[channel_id].append(video_item)
context_menu = []
replace_context_menu = False
# Refresh
yt_context_menu.append_refresh(context_menu, provider, context)
# Queue Video
yt_context_menu.append_queue_video(context_menu, provider, context)
"""
Play all videos of the playlist.
/channel/[CHANNEL_ID]/playlist/[PLAYLIST_ID]/
/playlist/[PLAYLIST_ID]/
"""
some_playlist_match = re.match(r'^(/channel/([^/]+))/playlist/(?P<playlist_id>[^/]+)/$', context.get_path())
if some_playlist_match:
replace_context_menu = True
playlist_id = some_playlist_match.group('playlist_id')
yt_context_menu.append_play_all_from_playlist(context_menu, provider, context, playlist_id, video_id)
yt_context_menu.append_play_all_from_playlist(context_menu, provider, context, playlist_id)
# 'play with...' (external player)
if settings.is_support_alternative_player_enabled():
yt_context_menu.append_play_with(context_menu, provider, context)
if provider.is_logged_in():
# add 'Watch Later' only if we are not in my 'Watch Later' list
watch_later_playlist_id = context.get_access_manager().get_watch_later_id()
yt_context_menu.append_watch_later(context_menu, provider, context, watch_later_playlist_id, video_id)
# provide 'remove' for videos in my playlists
if video_id in playlist_item_id_dict:
playlist_match = re.match('^/channel/mine/playlist/(?P<playlist_id>[^/]+)/$', context.get_path())
if playlist_match:
playlist_id = playlist_match.group('playlist_id')
# we support all playlist except 'Watch History'
if playlist_id:
if playlist_id != 'HL' and playlist_id.strip().lower() != 'wl':
playlist_item_id = playlist_item_id_dict[video_id]
video_item.set_playlist_id(playlist_id)
video_item.set_playlist_item_id(playlist_item_id)
context_menu.append((context.localize(provider.LOCAL_MAP['youtube.remove']),
'RunPlugin(%s)' % context.create_uri(
['playlist', 'remove', 'video'],
{'playlist_id': playlist_id, 'video_id': playlist_item_id,
'video_name': video_item.get_name()})))
is_history = re.match('^/special/watch_history_tv/$', context.get_path())
if is_history:
yt_context_menu.append_clear_watch_history(context_menu, provider, context)
# got to [CHANNEL]
if channel_id and channel_name:
# only if we are not directly in the channel provide a jump to the channel
if kodion.utils.create_path('channel', channel_id) != context.get_path():
video_item.set_channel_id(channel_id)
yt_context_menu.append_go_to_channel(context_menu, provider, context, channel_id, channel_name)
if provider.is_logged_in():
# subscribe to the channel of the video
video_item.set_subscription_id(channel_id)
yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id, channel_name)
if not video_item.live and use_play_data:
if play_data.get('play_count') is None or int(play_data.get('play_count')) == 0:
yt_context_menu.append_mark_watched(context_menu, provider, context, video_id)
else:
yt_context_menu.append_mark_unwatched(context_menu, provider, context, video_id)
if int(play_data.get('played_percent', '0')) > 0 or float(play_data.get('played_time', '0.0')) > 0.0:
yt_context_menu.append_reset_resume_point(context_menu, provider, context, video_id)
# more...
refresh_container = \
context.get_path().startswith('/channel/mine/playlist/LL') or \
context.get_path() == '/special/disliked_videos/'
yt_context_menu.append_more_for_video(context_menu, provider, context, video_id,
is_logged_in=provider.is_logged_in(),
refresh_container=refresh_container)
if not video_item.live:
yt_context_menu.append_play_with_subtitles(context_menu, provider, context, video_id)
yt_context_menu.append_play_audio_only(context_menu, provider, context, video_id)
yt_context_menu.append_play_ask_for_quality(context_menu, provider, context, video_id)
if len(context_menu) > 0:
video_item.set_context_menu(context_menu, replace=replace_context_menu)
def update_play_info(provider, context, video_id, video_item, video_stream, use_play_data=True):
settings = context.get_settings()
ui = context.get_ui()
resource_manager = provider.get_resource_manager(context)
video_data = resource_manager.get_videos([video_id], suppress_errors=True)
meta_data = video_stream.get('meta', None)
thumb_size = settings.use_thumbnail_size()
image = None
video_item.video_id = video_id
if meta_data:
video_item.set_subtitles(meta_data.get('subtitles', None))
image = get_thumbnail(thumb_size, meta_data.get('images', {}))
if 'headers' in video_stream:
video_item.set_headers(video_stream['headers'])
# set uses_dash
video_item.set_use_dash(settings.use_dash())
license_info = video_stream.get('license_info', {})
if inputstreamhelper and \
license_info.get('proxy') and \
license_info.get('url') and \
license_info.get('token'):
ishelper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha')
ishelper.check_inputstream()
video_item.set_license_key(license_info.get('proxy'))
ui.set_home_window_property('license_url', license_info.get('url'))
ui.set_home_window_property('license_token', license_info.get('token'))
"""
This is experimental. We try to get the most information out of the title of a video.
This is not based on any language. In some cases this won't work at all.
TODO: via language and settings provide the regex for matching episode and season.
"""
for regex in __RE_SEASON_EPISODE_MATCHES__:
re_match = regex.search(video_item.get_name())
if re_match:
if 'season' in re_match.groupdict():
video_item.set_season(int(re_match.group('season')))
if 'episode' in re_match.groupdict():
video_item.set_episode(int(re_match.group('episode')))
break
if video_item.live:
video_item.set_play_count(0)
if image:
if video_item.live:
image = ''.join([image, '?ct=', get_thumb_timestamp()])
video_item.set_image(image)
# set fanart
video_item.set_fanart(provider.get_fanart(context))
if not video_data:
return video_item
# requires API
# ===============
yt_item = video_data[video_id]
snippet = yt_item['snippet'] # crash if not conform
play_data = yt_item['play_data']
video_item.live = snippet.get('liveBroadcastContent') == 'live'
# set the title
if not video_item.get_title():
video_item.set_title(snippet['title'])
# duration
if not video_item.live and use_play_data and play_data.get('total_time'):
video_item.set_duration_from_seconds(float(play_data.get('total_time')))
else:
duration = yt_item.get('contentDetails', {}).get('duration', '')
if duration:
duration = utils.datetime_parser.parse(duration)
# we subtract 1 seconds because YouTube returns +1 second to much
video_item.set_duration_from_seconds(duration.seconds - 1)
if not video_item.live and use_play_data:
# play count
if play_data.get('play_count'):
video_item.set_play_count(int(play_data.get('play_count')))
if play_data.get('played_percent'):
video_item.set_start_percent(play_data.get('played_percent'))
if play_data.get('played_time'):
video_item.set_start_time(play_data.get('played_time'))
if play_data.get('last_played'):
video_item.set_last_played(play_data.get('last_played'))
# plot
channel_name = snippet.get('channelTitle', '')
description = kodion.utils.strip_html_from_text(snippet['description'])
if channel_name and settings.get_bool('youtube.view.description.show_channel_name', True):
description = '%s[CR][CR]%s' % (ui.uppercase(ui.bold(channel_name)), description)
video_item.set_studio(channel_name)
# video_item.add_cast(channel_name)
video_item.add_artist(channel_name)
video_item.set_plot(description)
# date time
if 'publishedAt' in snippet and snippet['publishedAt']:
date_time = utils.datetime_parser.parse(snippet['publishedAt'])
video_item.set_year_from_datetime(date_time)
video_item.set_aired_from_datetime(date_time)
video_item.set_premiered_from_datetime(date_time)
video_item.set_date_from_datetime(date_time)
if not image:
image = get_thumbnail(thumb_size, snippet.get('thumbnails', {}))
if video_item.live and image:
image = ''.join([image, '?ct=', get_thumb_timestamp()])
video_item.set_image(image)
return video_item
def update_fanarts(provider, context, channel_items_dict):
# at least we need one channel id
channel_ids = list(channel_items_dict.keys())
if len(channel_ids) == 0:
return
fanarts = provider.get_resource_manager(context).get_fanarts(channel_ids)
for channel_id in channel_ids:
channel_items = channel_items_dict[channel_id]
for channel_item in channel_items:
# only set not empty fanarts
fanart = fanarts.get(channel_id, '')
if fanart:
channel_item.set_fanart(fanart)
def get_thumbnail(thumb_size, thumbnails):
if thumb_size == 'high':
thumbnail_sizes = ['high', 'medium', 'default']
else:
thumbnail_sizes = ['medium', 'high', 'default']
image = ''
for thumbnail_size in thumbnail_sizes:
try:
image = thumbnails.get(thumbnail_size, {}).get('url', '')
except AttributeError:
image = thumbnails.get(thumbnail_size, '')
if image:
break
return image
def get_shelf_index_by_title(context, json_data, shelf_title):
shelf_index = None
contents = json_data.get('contents', {}).get('sectionListRenderer', {}).get('contents', [{}])
for idx, shelf in enumerate(contents):
title = shelf.get('shelfRenderer', {}).get('title', {}).get('runs', [{}])[0].get('text', '')
if title.lower() == shelf_title.lower():
shelf_index = idx
context.log_debug('Found shelf index |{index}| for |{title}|'.format(index=str(shelf_index), title=shelf_title))
break
if shelf_index is not None:
if 0 > shelf_index >= len(contents):
context.log_debug('Shelf index |{index}| out of range |0-{content_length}|'.format(index=str(shelf_index), content_length=str(len(contents))))
shelf_index = None
return shelf_index
def add_related_video_to_playlist(provider, context, client, v3, video_id):
playlist = context.get_video_playlist()
if playlist.size() <= 999:
a = 0
add_item = None
page_token = ''
playlist_items = playlist.get_items()
while not add_item and a <= 2:
a += 1
result_items = []
try:
json_data = client.get_related_videos(video_id, page_token=page_token, max_results=17)
result_items = v3.response_to_items(provider, context, json_data, process_next_page=False)
page_token = json_data.get('nextPageToken', '')
except:
context.get_ui().show_notification('Failed to add a suggested video.', time_milliseconds=5000)
if result_items:
add_item = next((
item for item in result_items
if not any((item.get_uri() == pitem.get('file') or
item.get_title() == pitem.get('title'))
for pitem in playlist_items)),
None)
if not add_item and page_token:
continue
if add_item:
playlist.add(add_item)
break
if not page_token:
break