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

1568 lines
54 KiB
Python

# -*- coding: utf-8 -*-
"""
Copyright (C) 2014-2016 bromix (plugin.video.youtube)
Copyright (C) 2016-2025 plugin.video.youtube
SPDX-License-Identifier: GPL-2.0-only
See LICENSES/GPL-2.0-only for more information.
"""
from __future__ import absolute_import, division, unicode_literals
import time
from datetime import date as dt_date, datetime as dt_datetime
from math import log10
from operator import (
contains as op_contains,
eq as op_eq,
ge as op_ge,
gt as op_gt,
le as op_le,
lt as op_lt,
)
from re import (
compile as re_compile,
error as re_error,
search as re_search,
)
from ...kodion import logging
from ...kodion.compatibility import string_type, unquote, urlsplit
from ...kodion.constants import (
CHANNEL_ID,
CONTENT,
FANART_TYPE,
PATHS,
PLAYLIST_ID,
)
from ...kodion.items import (
AudioItem,
CommandItem,
DirectoryItem,
MediaItem,
menu_items,
)
from ...kodion.utils.convert_format import friendly_number, strip_html_from_text
from ...kodion.utils.datetime import (
get_scheduled_start,
parse_to_dt,
utc_to_local,
)
# RegExp used to match plugin playlist paths of the form:
# /channel/[CHANNEL_ID]/playlist/[PLAYLIST_ID]/
# /playlist/[PLAYLIST_ID]/
__RE_PLAYLIST = re_compile(
r'^(/channel/(?P<channel_id>[^/]+))/playlist/(?P<playlist_id>[^/]+)/?$'
)
__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
return str(time.mktime(time.gmtime(
seconds * (round(time.time() / seconds))
)))
def make_comment_item(context, snippet, uri, reply_count=0):
localize = context.localize
settings = context.get_settings()
ui = context.get_ui()
author = snippet.get('authorDisplayName')
if not author:
author = urlsplit(snippet.get('authorChannelUrl', ''))
author = unquote(author.path.rstrip('/').split('/')[-1])
author_id = snippet.get('authorChannelId', {}).get('value', '')
author_image = snippet.get('authorProfileImageUrl')
if author_image:
author_image = author_image.replace('=s48', '=s160')
else:
author_image = None
body = strip_html_from_text(snippet['textOriginal'])
label_props = []
plot_props = []
like_count = snippet['likeCount']
if like_count:
like_count, likes_value = friendly_number(like_count, as_str=False)
color = settings.get_label_color('likeCount')
label_likes = ui.color(color, ui.bold(like_count))
plot_likes = ui.color(color, ui.bold(' '.join((
like_count, localize('video.comments.likes')
))))
label_props.append(label_likes)
plot_props.append(plot_likes)
if reply_count:
reply_count, replies_value = friendly_number(reply_count, as_str=False)
color = settings.get_label_color('commentCount')
label_replies = ui.color(color, ui.bold(reply_count))
plot_replies = ui.color(color, ui.bold(' '.join((
reply_count, localize('video.comments.replies')
))))
label_props.append(label_replies)
plot_props.append(plot_replies)
else:
replies_value = 0
published_at = snippet['publishedAt']
updated_at = snippet['updatedAt']
edited = published_at != updated_at
if edited:
label_props.append('*')
plot_props.append(localize('video.comments.edited'))
label = body.replace('\n', ' ')[:140]
label_stats = ' | '.join(label_props)
plot_stats = ' | '.join(plot_props)
# Format the plot of the comment item.
plot = ''.join((
ui.bold(author, cr_after=1),
ui.new_line(plot_stats, cr_after=1) if plot_stats else '',
ui.new_line(body, cr_after=1) if body else ''
))
datetime = parse_to_dt(published_at)
local_datetime = utc_to_local(datetime)
if uri:
comment_item = DirectoryItem(
label,
uri,
image=author_image,
plot=plot,
category_label=' - '.join(
(author, context.format_date_short(local_datetime))
),
)
else:
comment_item = CommandItem(
label,
'Action(Info)',
context,
image=author_image,
plot=plot,
)
comment_item.set_count(replies_value)
comment_item.set_short_details(label_stats)
comment_item.set_production_code(label_stats)
comment_item.channel_id = author_id
comment_item.add_artist(ui.bold(author))
comment_item.add_cast(author,
role=localize('author'),
thumbnail=author_image)
comment_item.set_added_utc(datetime)
comment_item.set_dateadded_from_datetime(local_datetime)
if edited:
datetime = parse_to_dt(updated_at)
local_datetime = utc_to_local(datetime)
comment_item.set_date_from_datetime(local_datetime)
return comment_item
def update_channel_items(provider, context, channel_id_dict,
subscription_id_dict=None,
channel_items_dict=None,
data=None):
if not channel_id_dict and not data and not channel_items_dict:
return
channel_ids = list(channel_id_dict)
if channel_ids and not data:
resource_manager = provider.get_resource_manager(context)
data = resource_manager.get_channels(channel_ids)
if not data:
if channel_items_dict:
update_channel_info(provider,
context,
channel_items_dict=channel_items_dict)
return
if subscription_id_dict is None:
subscription_id_dict = {}
client = provider.get_client(context)
logged_in = client.logged_in
settings = context.get_settings()
show_details = settings.show_detailed_description()
localize = context.localize
untitled = localize('untitled')
path = context.get_path()
ui = context.get_ui()
if path.startswith(PATHS.SUBSCRIPTIONS):
in_bookmarks_list = False
in_subscription_list = True
elif path.startswith(PATHS.BOOKMARKS):
in_bookmarks_list = True
in_subscription_list = False
else:
in_bookmarks_list = False
in_subscription_list = False
filters_set = None
if in_bookmarks_list or in_subscription_list:
if settings.subscriptions_filter_enabled():
filter_string, filters_set, custom_filters = channel_filter_split(
settings.subscriptions_filter()
)
fanart_type = context.get_param(FANART_TYPE)
if fanart_type is None:
fanart_type = settings.fanart_selection()
thumb_size = settings.get_thumbnail_size()
thumb_fanart = (
settings.get_thumbnail_size(settings.THUMB_SIZE_BEST)
if fanart_type == settings.FANART_THUMBNAIL else
False
)
cxm_unsubscribe_from_channel = menu_items.channel_unsubscribe_from(
context,
subscription_id=menu_items.SUBSCRIPTION_ID_INFOLABEL,
)
cxm_subscribe_to_channel = (
menu_items.channel_subscribe_to(context)
if logged_in and not in_subscription_list else
None
)
cxm_filter_remove = menu_items.my_subscriptions_filter_remove(context)
cxm_filter_add = menu_items.my_subscriptions_filter_add(context)
cxm_bookmark_channel = (
None
if in_bookmarks_list else
menu_items.bookmark_add_channel(context)
)
for channel_id, yt_item in data.items():
if not yt_item or 'snippet' not in yt_item:
continue
channel_items = channel_id_dict.get(channel_id)
if channel_items:
channel_item = channel_items[-1]
else:
continue
snippet = yt_item['snippet']
label_stats = []
stats = []
if 'statistics' in yt_item:
for stat, value in yt_item['statistics'].items():
label = context.LOCAL_MAP.get('stats.' + stat)
if not label:
continue
str_value, value = friendly_number(value, as_str=False)
if not value:
continue
color = settings.get_label_color(stat)
label = localize(label)
if value == 1:
label = label.rstrip('s')
label_stats.append(ui.color(color, str_value))
stats.append(ui.color(color, ui.bold(' '.join((
str_value, label
)))))
label_stats = ' | '.join(label_stats)
stats = ' | '.join(stats)
# Used for label2, but is poorly supported in skins
channel_item.set_short_details(label_stats)
# Hack to force a custom label mask containing production code,
# activated on sort order selection, to display details
# Refer XbmcContext.apply_content for usage
channel_item.set_production_code(label_stats)
# channel name and title
localised_info = snippet.get('localized') or {}
channel_handle = snippet.get('customUrl')
channel_name = (localised_info.get('title')
or snippet.get('title')
or untitled)
channel_item.set_name(channel_name)
channel_item.add_artist(channel_handle or channel_name)
# plot
description = strip_html_from_text(localised_info.get('description')
or snippet.get('description')
or '')
if show_details:
description = ''.join((
ui.bold(channel_name, cr_after=1),
ui.new_line(stats, cr_after=1) if stats else '',
ui.new_line(description, cr_after=1) if description else '',
ui.new_line('--------', cr_after=1),
'https://www.youtube.com/',
channel_handle if channel_handle else
channel_id if channel_id.startswith('@') else
'channel/' + channel_id,
))
channel_item.set_plot(description)
# date time
published_at = snippet.get('publishedAt')
if published_at:
datetime = parse_to_dt(published_at)
channel_item.set_added_utc(datetime)
local_datetime = utc_to_local(datetime)
channel_item.set_date_from_datetime(local_datetime)
# try to find a better resolution for the image
image = get_thumbnail(thumb_size, snippet.get('thumbnails'))
channel_item.set_image(image)
# try to find a better resolution for the fanart
if thumb_fanart:
fanart = get_thumbnail(thumb_fanart, snippet.get('thumbnails'))
channel_item.set_fanart(fanart)
subscription_id = subscription_id_dict.get(channel_id, '')
if subscription_id:
channel_item.subscription_id = subscription_id
context_menu = [
cxm_unsubscribe_from_channel,
cxm_bookmark_channel,
]
else:
context_menu = [
cxm_subscribe_to_channel,
cxm_bookmark_channel,
]
# add/remove from filter list
if filters_set is not None:
context_menu.append(
cxm_filter_remove
if client.channel_match(channel_id, filters_set) else
cxm_filter_add
)
update_duplicate_items(channel_item,
channel_items,
channel_id,
channel_items_dict,
context_menu)
if channel_items_dict:
update_channel_info(provider,
context,
channel_items_dict=channel_items_dict,
channel_data=data)
def update_playlist_items(provider, context, playlist_id_dict,
channel_items_dict=None,
data=None):
if not playlist_id_dict and not data:
return
playlist_ids = list(playlist_id_dict)
if playlist_ids and not data:
resource_manager = provider.get_resource_manager(context)
data = resource_manager.get_playlists(playlist_ids)
if not data:
return
access_manager = context.get_access_manager()
logged_in = provider.get_client(context).logged_in
if logged_in:
history_id = access_manager.get_watch_history_id()
watch_later_id = access_manager.get_watch_later_id()
else:
history_id = ''
watch_later_id = ''
settings = context.get_settings()
show_details = settings.show_detailed_description()
item_count_color = settings.get_label_color('itemCount')
params = context.get_params()
fanart_type = params.get(FANART_TYPE)
if fanart_type is None:
fanart_type = settings.fanart_selection()
thumb_size = settings.get_thumbnail_size()
thumb_fanart = (
settings.get_thumbnail_size(settings.THUMB_SIZE_BEST)
if fanart_type == settings.FANART_THUMBNAIL else
False
)
localize = context.localize
episode_count_label = localize('stats.itemCount')
video_count_label = localize('stats.videoCount')
podcast_label = context.localize('playlist.podcast')
untitled = localize('untitled')
path = context.get_path()
ui = context.get_ui()
in_bookmarks_list = False
in_my_playlists = False
in_saved_playlists = False
# if the path directs to a playlist of our own, set channel id to 'mine'
if path.startswith(PATHS.MY_PLAYLISTS):
in_my_playlists = True
elif path.startswith(PATHS.BOOKMARKS):
in_bookmarks_list = True
elif path.startswith(PATHS.SAVED_PLAYLISTS):
in_saved_playlists = True
cxm_playlist_delete = menu_items.playlist_delete(context)
cxm_playlist_rename = menu_items.playlist_rename(context)
cxm_watch_later_unassign = menu_items.watch_later_list_unassign(context)
cxm_watch_later_assign = menu_items.watch_later_list_assign(context)
cxm_history_list_unassign = menu_items.history_list_unassign(context)
cxm_history_list_assign = menu_items.history_list_assign(context)
cxm_separator = menu_items.separator()
cxm_play_playlist = menu_items.playlist_play(context)
cxm_play_recently_added = menu_items.playlist_play_recently_added(context)
cxm_view_playlist = menu_items.playlist_view(context)
cxm_play_shuffled_playlist = menu_items.playlist_shuffle(context)
cxm_refresh_listing = menu_items.refresh_listing(context, path, params)
cxm_remove_saved_playlist = menu_items.playlist_remove_from_library(context)
cxm_save_playlist = (
menu_items.playlist_save_to_library(context)
if logged_in and not (in_my_playlists or in_saved_playlists) else
None
)
cxm_go_to_channel = (
menu_items.channel_go_to(context)
if not in_my_playlists else
None
)
cxm_subscribe_to_channel = (
menu_items.channel_subscribe_to(context)
if logged_in and not in_my_playlists else
None
)
cxm_bookmark_channel = (
menu_items.bookmark_add_channel(context)
if not in_my_playlists else
None
)
for playlist_id, yt_item in data.items():
if not yt_item or 'snippet' not in yt_item:
continue
playlist_items = playlist_id_dict.get(playlist_id)
if playlist_items:
playlist_item = playlist_items[-1]
else:
continue
item_count_str, item_count = friendly_number(
yt_item.get('contentDetails', {}).get('itemCount', 0),
as_str=False,
)
if not item_count and playlist_id.startswith('UU'):
continue
snippet = yt_item['snippet']
playlist_item.available = True
is_podcast = yt_item.get('status', {}).get('podcastStatus') == 'enabled'
count_label = episode_count_label if is_podcast else video_count_label
label_details = ' | '.join([item for item in (
ui.bold('((○))') if is_podcast else '',
ui.color(item_count_color, item_count_str),
) if item])
# Used for label2, but is poorly supported in skins
playlist_item.set_short_details(label_details)
# Hack to force a custom label mask containing production code,
# activated on sort order selection, to display details
# Refer XbmcContext.apply_content for usage
playlist_item.set_production_code(label_details)
# title
localised_info = snippet.get('localized') or {}
title = localised_info.get('title') or snippet.get('title') or untitled
playlist_item.set_name(title)
# channel name
channel_name = snippet.get('channelTitle') or untitled
playlist_item.add_artist(channel_name)
# plot with channel name, podcast status and item count
description = strip_html_from_text(localised_info.get('description')
or snippet.get('description')
or '')
if show_details:
description = ''.join((
ui.bold(channel_name, cr_after=1),
ui.bold(podcast_label) if is_podcast else '',
' | ' if is_podcast else '',
ui.color(
item_count_color,
ui.bold(' '.join((item_count_str,
count_label.rstrip('s')
if item_count == 1 else
count_label))),
cr_after=1,
),
ui.new_line(description, cr_after=1) if description else '',
ui.new_line('--------', cr_after=1),
'https://youtube.com/playlist?list=' + playlist_id,
))
playlist_item.set_plot(description)
# date time
published_at = snippet.get('publishedAt')
if published_at:
datetime = parse_to_dt(published_at)
playlist_item.set_added_utc(datetime)
local_datetime = utc_to_local(datetime)
playlist_item.set_date_from_datetime(local_datetime)
# try to find a better resolution for the image
image = get_thumbnail(thumb_size, snippet.get('thumbnails'))
playlist_item.set_image(image)
# try to find a better resolution for the fanart
if thumb_fanart:
fanart = get_thumbnail(thumb_fanart, snippet.get('thumbnails'))
playlist_item.set_fanart(fanart)
# update channel mapping
channel_id = snippet.get('channelId', '')
playlist_item.channel_id = channel_id
if in_my_playlists:
context_menu = [
# remove my playlist
cxm_playlist_delete,
# rename playlist
cxm_playlist_rename,
# remove as my custom watch later playlist
cxm_watch_later_unassign
if playlist_id == watch_later_id else
# set as my custom watch later playlist
cxm_watch_later_assign,
# remove as custom history playlist
cxm_history_list_unassign
if playlist_id == history_id else
# set as custom history playlist
cxm_history_list_assign,
cxm_separator,
]
elif in_saved_playlists:
context_menu = [
cxm_remove_saved_playlist,
cxm_separator,
]
else:
context_menu = []
context_menu.extend((
# play all videos of the playlist
cxm_play_playlist,
cxm_play_recently_added,
cxm_view_playlist,
cxm_play_shuffled_playlist,
cxm_refresh_listing,
cxm_separator,
cxm_save_playlist,
menu_items.bookmark_add(
context, playlist_item
)
if not (in_my_playlists or in_bookmarks_list) else
None,
cxm_go_to_channel,
# subscribe to the channel via the playlist item
cxm_subscribe_to_channel,
# bookmark channel of the playlist
cxm_bookmark_channel,
))
update_duplicate_items(playlist_item,
playlist_items,
channel_id,
channel_items_dict,
context_menu)
def update_video_items(provider, context, video_id_dict,
channel_items_dict=None,
live_details=True,
item_filter=None,
data=None,
yt_items_dict=None):
if not video_id_dict and not data:
return
video_ids = list(video_id_dict)
if video_ids and not data:
resource_manager = provider.get_resource_manager(context)
data = resource_manager.get_videos(video_ids,
live_details=live_details,
suppress_errors=True,
yt_items_dict=yt_items_dict)
if not data:
return
logged_in = provider.get_client(context).logged_in
if logged_in:
watch_later_id = context.get_access_manager().get_watch_later_id()
else:
watch_later_id = ''
settings = context.get_settings()
alternate_player = settings.support_alternative_player()
default_web_urls = settings.default_player_web_urls()
ask_quality = not default_web_urls and settings.ask_for_video_quality()
audio_only = settings.audio_only()
show_details = settings.show_detailed_description()
shorts_duration = settings.shorts_duration()
subtitles_prompt = settings.get_subtitle_selection() == 1
use_play_data = settings.use_local_history()
params = context.get_params()
fanart_type = params.get(FANART_TYPE)
if fanart_type is None:
fanart_type = settings.fanart_selection()
thumb_size = settings.get_thumbnail_size()
get_better_thumbs = (settings.get_int(settings.THUMB_SIZE)
== settings.THUMB_SIZE_BEST)
thumb_fanart = (
settings.get_thumbnail_size(settings.THUMB_SIZE_BEST)
if fanart_type == settings.FANART_THUMBNAIL else
False
)
thumb_stamp = get_thumb_timestamp()
localize = context.localize
untitled = localize('untitled')
path = context.get_path()
ui = context.get_ui()
playlist_id = None
playlist_channel_id = None
in_bookmarks_list = False
in_my_subscriptions_list = False
in_watch_history_list = False
in_watch_later_list = False
if path.startswith(PATHS.MY_SUBSCRIPTIONS):
in_my_subscriptions_list = True
elif path.startswith(PATHS.WATCH_LATER):
in_watch_later_list = True
elif path.startswith(PATHS.BOOKMARKS):
in_bookmarks_list = True
elif path.startswith(PATHS.VIRTUAL_PLAYLIST):
playlist_id = params.get(PLAYLIST_ID)
playlist_channel_id = 'mine'
if playlist_id:
playlist_id_upper = playlist_id.upper()
if playlist_id_upper == 'WL':
in_watch_later_list = True
elif playlist_id_upper == 'HL':
in_watch_history_list = True
else:
playlist_match = __RE_PLAYLIST.match(path)
if playlist_match:
playlist_id = playlist_match.group(PLAYLIST_ID)
playlist_channel_id = playlist_match.group(CHANNEL_ID)
cxm_remove_from_playlist = menu_items.playlist_remove_from(
context,
playlist_id=playlist_id,
)
cxm_separator = menu_items.separator()
cxm_play = menu_items.media_play(context)
cxm_play_with_subtitles = (
None
if subtitles_prompt else
menu_items.media_play_with_subtitles(context)
)
cxm_play_audio_only = (
None
if audio_only else
menu_items.media_play_audio_only(context)
)
cxm_play_ask_for_quality = (
None
if ask_quality else
menu_items.media_play_ask_for_quality(context)
)
cxm_play_timeshift = menu_items.media_play_timeshift(context)
cxm_play_using = (
menu_items.media_play_using(context)
if alternate_player else
None
)
cxm_play_from = menu_items.playlist_play_from(context, playlist_id)
cxm_queue = menu_items.media_queue(context)
cxm_watch_later = menu_items.playlist_add_to(
context,
watch_later_id,
'watch_later',
)
cxm_go_to_channel = menu_items.channel_go_to(context)
cxm_unsubscribe_from_channel = menu_items.channel_unsubscribe_from(
context,
channel_id=menu_items.CHANNEL_ID_INFOLABEL,
)
cxm_subscribe_to_channel = menu_items.channel_subscribe_to(context)
cxm_remove_bookmarked_channel = menu_items.bookmark_remove(
context,
menu_items.CHANNEL_ID_INFOLABEL,
menu_items.ARTIST_INFOLABEL,
)
cxm_bookmark_channel = menu_items.bookmark_add_channel(context)
cxm_mark_as = menu_items.history_local_mark_as(context)
cxm_reset_resume = menu_items.history_local_reset_resume(context)
cxm_refresh_listing = menu_items.refresh_listing(context)
cxm_more = menu_items.video_more_for(
context,
logged_in=logged_in,
refresh=path.startswith((PATHS.LIKED_VIDEOS, PATHS.DISLIKED_VIDEOS)),
)
for video_id, yt_item in data.items():
if not yt_item:
continue
media_items = video_id_dict.get(video_id)
if media_items:
media_item = media_items[-1]
else:
continue
available = True
if 'snippet' in yt_item:
snippet = yt_item['snippet']
else:
snippet = {}
if yt_item.get('_unavailable'):
available = False
media_item.playable = False
media_item.available = False
media_item.set_mediatype(
CONTENT.AUDIO_TYPE
if isinstance(media_item, AudioItem) else
CONTENT.VIDEO_TYPE
)
play_data = use_play_data and yt_item.get('play_data')
if play_data and 'total_time' in play_data:
duration = play_data['total_time']
else:
duration = yt_item.get('contentDetails', {}).get('duration')
if duration:
duration = parse_to_dt(duration)
if duration.seconds:
# subtract 1s because YouTube duration is +1s too long
duration = duration.seconds - 1
else:
duration = 0
if duration:
media_item.set_duration_from_seconds(duration)
if duration <= shorts_duration:
media_item.short = True
broadcast_type = snippet.get('liveBroadcastContent')
media_item.live = broadcast_type == 'live'
media_item.upcoming = broadcast_type == 'upcoming'
upload_status = yt_item.get('status', {}).get('uploadStatus')
if upload_status == 'processed' and duration:
media_item.live = False
elif upload_status == 'uploaded' and not duration:
media_item.live = True
if 'liveStreamingDetails' in yt_item:
streaming_details = yt_item['liveStreamingDetails']
if 'actualStartTime' in streaming_details:
start_at = streaming_details['actualStartTime']
media_item.upcoming = False
if 'actualEndTime' in streaming_details:
media_item.completed = True
else:
start_at = streaming_details.get('scheduledStartTime')
media_item.upcoming = True
else:
media_item.completed = False
media_item.live = False
media_item.upcoming = False
media_item.vod = True
start_at = None
if item_filter and (
(not item_filter['completed']
and media_item.completed)
or (not item_filter['live']
and media_item.live and not media_item.upcoming)
or (not item_filter['upcoming']
and media_item.upcoming)
or (not item_filter['premieres']
and media_item.upcoming and not media_item.live)
or (not item_filter['upcoming_live']
and media_item.upcoming and media_item.live)
or (not item_filter['vod']
and media_item.vod)
or (not item_filter['shorts']
and media_item.short)
):
continue
if play_data:
if media_item.live:
if 'play_count' in play_data:
media_item.set_play_count(play_data['play_count'])
if 'last_played' in play_data:
media_item.set_last_played(play_data['last_played'])
media_item.set_start_percent(0)
media_item.set_start_time(0)
else:
if 'play_count' in play_data:
media_item.set_play_count(play_data['play_count'])
if 'last_played' in play_data:
media_item.set_last_played(play_data['last_played'])
if 'played_percent' in play_data:
media_item.set_start_percent(play_data['played_percent'])
if 'played_time' in play_data:
media_item.set_start_time(play_data['played_time'])
if start_at:
datetime = parse_to_dt(start_at)
media_item.set_scheduled_start_utc(datetime)
local_datetime = utc_to_local(datetime)
media_item.set_year_from_datetime(local_datetime)
media_item.set_aired_from_datetime(local_datetime)
media_item.set_premiered_from_datetime(local_datetime)
media_item.set_date_from_datetime(local_datetime)
if media_item.upcoming:
if media_item.live:
type_label = localize('live.upcoming')
else:
type_label = localize('upcoming')
elif media_item.live:
type_label = localize('live')
else:
type_label = localize('start')
start_at = ' '.join((
type_label,
get_scheduled_start(context, local_datetime),
))
label_stats = []
stats = []
rating = [0, 0]
if 'statistics' in yt_item:
for stat, value in yt_item['statistics'].items():
label = context.LOCAL_MAP.get('stats.' + stat)
if not label:
continue
str_value, value = friendly_number(value, as_str=False)
if not value:
continue
color = settings.get_label_color(stat)
label = localize(label)
if value == 1:
label = label.rstrip('s')
label_stats.append(ui.color(color, str_value))
stats.append(ui.color(color, ui.bold(' '.join((
str_value, label
)))))
if stat == 'likeCount':
rating[0] = value
elif stat == 'viewCount':
rating[1] = value
media_item.set_count(value)
label_stats = ' | '.join(label_stats)
stats = ' | '.join(stats)
if 0 < rating[0] <= rating[1]:
if rating[0] == rating[1]:
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))
media_item.set_rating(rating)
# Used for label2, but is poorly supported in skins
media_item.set_short_details(label_stats)
# Hack to force a custom label mask containing production code,
# activated on sort order selection, to display details
# Refer XbmcContext.apply_content for usage
media_item.set_production_code(label_stats)
# update and set the title
localised_info = snippet.get('localized') or {}
title = media_item.get_name()
if not title or title == untitled or media_item.bookmark_id:
title = (localised_info.get('title')
or snippet.get('title')
or untitled)
media_item.set_name(ui.italic(title) if media_item.upcoming else 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.
"""
season = episode = None
for season_episode in __RE_SEASON_EPISODE.findall(title):
if not season:
value = season_episode[0]
if value:
value = int(value)
if value < 2 ** 31:
season = value
media_item.set_season(season)
if not episode:
value = season_episode[1]
if value:
value = int(value)
if value < 2 ** 31:
episode = value
media_item.set_episode(episode)
if season and episode:
break
# channel name
channel_name = snippet.get('channelTitle', '') or untitled
media_item.add_artist(channel_name)
# plot
description = strip_html_from_text(localised_info.get('description')
or snippet.get('description')
or '')
if show_details:
description = ''.join((
ui.bold(channel_name, cr_after=1),
ui.new_line(stats, cr_after=1) if stats else '',
(ui.italic(start_at, cr_after=1) if media_item.upcoming
else ui.new_line(start_at, cr_after=1)) if start_at else '',
ui.new_line(description, cr_after=1) if description else '',
ui.new_line('--------', cr_after=1),
'https://youtu.be/' + video_id,
))
media_item.set_plot(description)
# date time
published_at = snippet.get('publishedAt')
if not published_at:
datetime = None
elif isinstance(published_at, string_type):
datetime = parse_to_dt(published_at)
else:
datetime = published_at
if datetime:
media_item.set_added_utc(datetime)
local_datetime = utc_to_local(datetime)
# If item is in a playlist, then use date added to playlist rather
# than date that item was published to YouTube
if not media_item.get_dateadded():
media_item.set_dateadded_from_datetime(local_datetime)
if not start_at:
media_item.set_year_from_datetime(local_datetime)
media_item.set_aired_from_datetime(local_datetime)
media_item.set_premiered_from_datetime(local_datetime)
media_item.set_date_from_datetime(local_datetime)
# try to find a better resolution for the image
image = media_item.get_image()
if (not image
or get_better_thumbs
or image.startswith(('Default', 'special://'))):
image = get_thumbnail(thumb_size, snippet.get('thumbnails'))
if image and media_item.live:
if '?' in image:
image = ''.join((image, '&ct=', thumb_stamp))
elif image.endswith(('_live.jpg', '_live.webp')):
image = ''.join((image, '?ct=', thumb_stamp))
media_item.set_image(image)
# try to find a better resolution for the fanart
if thumb_fanart:
fanart = get_thumbnail(thumb_fanart, snippet.get('thumbnails'))
if fanart and media_item.live:
if '?' in fanart:
fanart = ''.join((fanart, '&ct=', thumb_stamp))
elif image.endswith(('_live.jpg', '_live.webp')):
fanart = ''.join((fanart, '?ct=', thumb_stamp))
media_item.set_fanart(fanart)
# update channel mapping
channel_id = snippet.get('channelId') or playlist_channel_id
media_item.channel_id = channel_id
item_from_playlist = playlist_id or media_item.playlist_id
# Provide 'remove' in own playlists or virtual lists, except the
# YouTube Watch History list as that does not support direct edits
if (not in_watch_history_list
and item_from_playlist
and logged_in
and playlist_channel_id == 'mine'):
context_menu = [
cxm_remove_from_playlist,
cxm_separator,
]
else:
context_menu = []
if available:
context_menu.extend((
cxm_play,
cxm_play_with_subtitles,
cxm_play_audio_only,
cxm_play_ask_for_quality,
cxm_play_timeshift if media_item.live else None,
cxm_play_using,
cxm_play_from if item_from_playlist else None,
cxm_queue,
))
# add 'Watch Later' only if we are not in my 'Watch Later' list
if not available or in_watch_later_list:
pass
elif watch_later_id:
context_menu.append(cxm_watch_later)
else:
context_menu.append(
menu_items.watch_later_local_add(
context, media_item
)
)
if not in_bookmarks_list:
context_menu.append(
menu_items.bookmark_add(
context, media_item
)
)
if channel_id:
# got to [CHANNEL] only if we are not directly in the channel
if context.create_path(PATHS.CHANNEL, channel_id) != path:
media_item.channel_id = channel_id
context_menu.append(cxm_go_to_channel)
if logged_in:
context_menu.append(
# unsubscribe from the channel of the video
cxm_unsubscribe_from_channel
if in_my_subscriptions_list else
# subscribe to the channel of the video
cxm_subscribe_to_channel
)
context_menu.append(
# remove bookmarked channel of the video
cxm_remove_bookmarked_channel
if in_my_subscriptions_list else
# bookmark channel of the video
cxm_bookmark_channel
)
if use_play_data:
context_menu.append(cxm_mark_as)
if play_data and (play_data.get('played_percent', 0) > 0
or play_data.get('played_time', 0) > 0):
context_menu.append(cxm_reset_resume)
# more...
context_menu.extend((
cxm_refresh_listing,
cxm_more,
))
update_duplicate_items(media_item,
media_items,
channel_id,
channel_items_dict,
context_menu)
def update_play_info(provider,
context,
video_id,
media_item,
video_stream,
yt_item=None):
update_video_items(
provider,
context,
{video_id: [media_item]},
yt_items_dict={video_id: yt_item},
)
settings = context.get_settings()
meta_data = video_stream.get('meta')
if meta_data:
media_item.live = meta_data.get('status', {}).get('live', False)
media_item.set_subtitles(meta_data.get('subtitles', None))
image = get_thumbnail(settings.get_thumbnail_size(),
meta_data.get('thumbnails'))
if image:
if media_item.live:
if '?' in image:
image = ''.join((image, '&ct=', get_thumb_timestamp()))
elif image.endswith(('_live.jpg', '_live.webp')):
image = ''.join((image, '?ct=', get_thumb_timestamp()))
media_item.set_image(image)
if 'headers' in video_stream:
media_item.set_headers(video_stream['headers'])
# set _uses_isa
if video_stream.get('adaptive'):
if media_item.live:
use_isa = settings.use_isa_live_streams()
else:
use_isa = settings.use_isa()
else:
use_isa = False
media_item.set_isa(use_isa)
if use_isa:
drm_details = video_stream.get('drm_details')
if drm_details:
drm_type = drm_details.get('widevine')
if drm_type:
try:
from inputstreamhelper import Helper
except ImportError:
Helper = None
if Helper:
is_helper = Helper(
'mpd' if media_item.use_mpd() else 'hls',
drm=drm_type['license_type'],
)
if is_helper and is_helper.check_inputstream():
media_item.set_license_key('|'.join((
drm_type['proxy_url'],
drm_type['headers'],
drm_type['post_format'],
drm_type['response_format'],
)))
def update_channel_info(provider,
context,
channel_items_dict,
data=None,
channel_data=None):
# at least we need one channel id
if not channel_items_dict and not (data or channel_data):
return
channel_ids = list(channel_items_dict)
if channel_ids and not data:
resource_manager = provider.get_resource_manager(context)
data = resource_manager.get_channel_info(channel_ids,
channel_data=channel_data,
suppress_errors=True)
if not data:
return
settings = context.get_settings()
channel_name_aliases = settings.get_channel_name_aliases()
fanart_type = context.get_param(FANART_TYPE)
if fanart_type is None:
fanart_type = settings.fanart_selection()
use_channel_fanart = fanart_type == settings.FANART_CHANNEL
use_thumb_fanart = fanart_type == settings.FANART_THUMBNAIL
channel_role = context.localize('channel')
for channel_id, channel_items in channel_items_dict.items():
channel_info = data.get(channel_id)
if not channel_info:
continue
for item in channel_items:
if (use_channel_fanart
or use_thumb_fanart and not item.get_fanart(default=False)):
item.set_fanart(channel_info.get('fanart'))
channel_name = channel_info.get('name')
if channel_name:
if 'cast' in channel_name_aliases:
item.add_cast(channel_name,
role=channel_role,
thumbnail=channel_info.get('image'))
if 'studio' in channel_name_aliases:
item.add_studio(channel_name)
PREFER_WEBP_THUMBS = False
if PREFER_WEBP_THUMBS:
THUMB_URL = 'https://i.ytimg.com/vi_webp/{0}/{1}{2}.webp'
else:
THUMB_URL = 'https://i.ytimg.com/vi/{0}/{1}{2}.jpg'
RE_CUSTOM_THUMB = re_compile(r'_custom_[0-9]')
THUMB_TYPES = {
'default': {
'name': 'default',
'width': 120,
'height': 90,
'size': 120 * 90,
'ratio': 120 / 90, # 4:3
},
'medium': {
'name': 'mqdefault',
'width': 320,
'height': 180,
'size': 320 * 180,
'ratio': 320 / 180, # 16:9
},
'high': {
'name': 'hqdefault',
'width': 480,
'height': 360,
'size': 480 * 360,
'ratio': 480 / 360, # 4:3
},
'standard': {
'name': 'sddefault',
'width': 640,
'height': 480,
'size': 640 * 480,
'ratio': 640 / 480, # 4:3
},
'720': {
'name': 'hq720',
'width': 1280,
'height': 720,
'size': 1280 * 720,
'ratio': 1280 / 720, # 16:9
},
'oar': {
'name': 'oardefault',
'size': 0,
'ratio': 0,
},
'maxres': {
'name': 'maxresdefault',
'size': 0,
'ratio': 0,
},
}
def get_thumbnail(thumb_size, thumbnails, default_thumb=None):
if not thumbnails:
return default_thumb
is_dict = isinstance(thumbnails, dict)
size_limit = thumb_size['size']
ratio_limit = thumb_size['ratio']
def _sort_ratio_size(thumb):
if is_dict:
thumb_type, thumb = thumb
else:
thumb_type = None
if 'size' in thumb:
size = thumb['size']
ratio = thumb['ratio']
elif 'width' in thumb:
width = thumb['width']
height = thumb['height']
size = width * height
ratio = width / height
elif thumb_type in THUMB_TYPES:
thumb = THUMB_TYPES[thumb_type]
size = thumb['size']
ratio = thumb['ratio']
else:
return False, False, False
return (
ratio_limit and ratio_limit * 0.9 <= ratio <= ratio_limit * 1.1,
not thumb.get('unverified', False),
size <= size_limit and size if size_limit else size,
)
thumbnail = sorted(thumbnails.items() if is_dict else thumbnails,
key=_sort_ratio_size,
reverse=True)[0]
url = (thumbnail[1] if is_dict else thumbnail).get('url')
if not url:
return default_thumb
if url.startswith('//'):
url = 'https:' + url
if '?' in url:
url = urlsplit(url)
url = url._replace(
netloc='i.ytimg.com',
path=RE_CUSTOM_THUMB.sub('', url.path),
query=None,
).geturl()
elif PREFER_WEBP_THUMBS and '/vi_webp/' not in url:
url = url.replace('/vi/', '/vi_webp/', 1).replace('.jpg', '.webp', 1)
return url
def add_related_video_to_playlist(provider, context, client, v3, video_id):
playlist_player = context.get_playlist_player()
if playlist_player.size() > 999:
return
playlist_items = playlist_player.get_items()
next_item = None
page_token = ''
for _ in range(2):
json_data = client.get_related_videos(
video_id,
page_token=page_token,
)
if not json_data:
break
result_items = v3.response_to_items(
provider,
context,
json_data,
process_next_page=False,
)
try:
next_item = next((
item for item in result_items
if (item
and isinstance(item, MediaItem)
and not any((
item.get_uri() == playlist_item.get('file')
or item.get_name() == playlist_item.get('title')
for playlist_item in playlist_items
)))
))
except StopIteration:
page_token = json_data.get('nextPageToken')
if not page_token:
break
if next_item:
playlist_player.add(next_item)
else:
context.get_ui().show_notification(
context.localize('error.no_videos_found'),
header=context.localize('after_watch.play_suggested'),
time_ms=5000,
)
def filter_videos(items,
exclude=None,
shorts=True,
live=True,
upcoming_live=True,
premieres=True,
upcoming=True,
completed=True,
vod=True,
custom=None,
callback=None,
**_kwargs):
accepted = []
rejected = []
for item in items:
rejected_reason = None
if item.callback and not item.callback():
rejected_reason = 'Item callback'
elif callback and not callback(item):
rejected_reason = 'Collection callback'
elif custom and not filter_parse(item, custom):
rejected_reason = 'Custom filter'
elif item.playable:
if exclude and item.video_id in exclude:
rejected_reason = 'Is excluded'
elif not completed and item.completed:
rejected_reason = 'Is completed'
elif not live and item.live and not item.upcoming:
rejected_reason = 'Is live'
elif not upcoming and item.upcoming:
rejected_reason = 'Is upcoming'
elif not premieres and item.upcoming and not item.live:
rejected_reason = 'Is premiere'
elif not upcoming_live and item.upcoming and item.live:
rejected_reason = 'Is upcoming live'
elif not vod and item.vod:
rejected_reason = 'Is VOD'
elif not shorts and item.short:
rejected_reason = 'Is short'
if rejected_reason:
item.set_filter_reason(rejected_reason)
rejected.append(item)
else:
accepted.append(item)
return accepted, rejected
def filter_parse(item,
all_criteria,
criteria_re=re_compile(
r'{?{([^}]+)}{([^}]+)}{([^}]+)}}?'
),
op_map={
'=': op_eq,
'==': op_eq,
'>': op_gt,
'>=': op_ge,
'<': op_lt,
'<=': op_le,
'contains': op_contains,
'endswith': str.endswith,
'startswith': str.startswith,
'search': re_search,
},
_none=lambda: None):
criteria_met = False
for idx, criteria in enumerate(all_criteria):
if isinstance(criteria, string_type):
criteria = criteria_re.findall(criteria)
all_criteria[idx] = criteria
for input_1, op_str, input_2 in criteria:
try:
if input_1.startswith('.'):
input_1 = getattr(item, input_1[1:], None)
else:
input_1 = getattr(item, 'get_{0}'.format(input_1), _none)()
if input_2.startswith('"'):
input_2 = unquote(input_2[1:-1])
if input_1 is None:
input_1 = ''
elif isinstance(input_1, (dt_date, dt_datetime)):
input_2 = parse_to_dt(input_2)
else:
input_2 = float(input_2)
if input_1 is None:
input_1 = -1
_, negate, op_str = op_str.rpartition('!')
op = op_map.get(op_str)
if not op:
break
if op_str == 'search':
input_1, input_2 = input_2, input_1
result = op(input_1, input_2)
if negate:
result = not result
if not result:
break
except (AttributeError, TypeError, ValueError, re_error):
logging.exception(('Error',
'Criteria: {criteria!r}',
'input_1: {input_1!r}',
'op: {op_str!r}',
'input_2: {input_2!r}'),
criteria=criteria,
input_1=input_1,
op_str=op_str,
input_2=input_2)
break
else:
criteria_met = True
break
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,
channel_items_dict=None,
context_menu=None,
skip_keys=frozenset(('_bookmark_id',
'_bookmark_timestamp',
'_callback',
'_context_menu',
'_track_number',
'_uri')),
skip_vals=(None, '', -1)):
updates = {
key: val
for key, val in updated_item.__dict__.items()
if key not in skip_keys and val not in skip_vals
}
for item in items:
if item != updated_item:
item.__dict__.update(updates)
if context_menu:
item.add_context_menu(context_menu)
if channel_id and channel_items_dict is not None:
channel_items = channel_items_dict.setdefault(channel_id, [])
channel_items.extend(items)