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

1355 lines
56 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.
"""
import copy
import json
import re
import threading
import traceback
import requests
from .login_client import LoginClient
from ..helper.video_info import VideoInfo
from ..helper.utils import get_shelf_index_by_title
from ...kodion import constants
from ...kodion import Context
from ...kodion.utils import datetime_parser
_context = Context(plugin_id='plugin.video.youtube')
class YouTube(LoginClient):
def __init__(self, config=None, language='en-US', region='US', items_per_page=50, access_token='', access_token_tv=''):
if config is None:
config = {}
LoginClient.__init__(self, config=config, language=language, region=region, access_token=access_token,
access_token_tv=access_token_tv)
self._max_results = items_per_page
def get_max_results(self):
return self._max_results
def get_language(self):
return self._language
def get_region(self):
return self._region
@staticmethod
def calculate_next_page_token(page, max_result):
page -= 1
low = 'AEIMQUYcgkosw048'
high = 'ABCDEFGHIJKLMNOP'
len_low = len(low)
len_high = len(high)
position = page * max_result
overflow_token = 'Q'
if position >= 128:
overflow_token_iteration = position // 128
overflow_token = '%sE' % high[overflow_token_iteration]
low_iteration = position % len_low
# at this position the iteration starts with 'I' again (after 'P')
if position >= 256:
multiplier = (position // 128) - 1
position -= 128 * multiplier
high_iteration = (position // len_low) % len_high
return 'C%s%s%sAA' % (high[high_iteration], low[low_iteration], overflow_token)
def update_watch_history(self, video_id, url):
headers = {'Host': 'www.youtube.com',
'Connection': 'keep-alive',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.36 Safari/537.36',
'Accept': 'image/webp,*/*;q=0.8',
'DNT': '1',
'Referer': 'https://www.youtube.com/tv',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'en-US,en;q=0.8,de;q=0.6'}
params = {'noflv': '1',
'html5': '1',
'video_id': video_id,
'referrer': '',
'eurl': 'https://www.youtube.com/tv#/watch?v=%s' % video_id,
'skl': 'false',
'ns': 'yt',
'el': 'leanback',
'ps': 'leanback'}
if self._access_token:
params['access_token'] = self._access_token
try:
_ = requests.get(url, params=params, headers=headers, verify=self._verify, allow_redirects=True)
except:
_context.log_error('Failed to update watch history |%s|' % traceback.print_exc())
def get_video_streams(self, context, video_id):
video_info = VideoInfo(context, access_token=self._access_token, language=self._language)
video_streams = video_info.load_stream_infos(video_id)
# update title
for video_stream in video_streams:
title = '%s (%s)' % (context.get_ui().bold(video_stream['title']), video_stream['container'])
if 'audio' in video_stream and 'video' in video_stream:
if video_stream['audio']['bitrate'] > 0 and video_stream['video']['encoding'] and \
video_stream['audio']['encoding']:
title = '%s (%s; %s / %s@%d)' % (context.get_ui().bold(video_stream['title']),
video_stream['container'],
video_stream['video']['encoding'],
video_stream['audio']['encoding'],
video_stream['audio']['bitrate'])
elif video_stream['video']['encoding'] and video_stream['audio']['encoding']:
title = '%s (%s; %s / %s)' % (context.get_ui().bold(video_stream['title']),
video_stream['container'],
video_stream['video']['encoding'],
video_stream['audio']['encoding'])
elif 'audio' in video_stream and 'video' not in video_stream:
if video_stream['audio']['encoding'] and video_stream['audio']['bitrate'] > 0:
title = '%s (%s; %s@%d)' % (context.get_ui().bold(video_stream['title']),
video_stream['container'],
video_stream['audio']['encoding'],
video_stream['audio']['bitrate'])
elif 'audio' in video_stream or 'video' in video_stream:
encoding = video_stream.get('audio', dict()).get('encoding')
if not encoding:
encoding = video_stream.get('video', dict()).get('encoding')
if encoding:
title = '%s (%s; %s)' % (context.get_ui().bold(video_stream['title']),
video_stream['container'],
encoding)
video_stream['title'] = title
return video_streams
def remove_playlist(self, playlist_id):
params = {'id': playlist_id,
'mine': 'true'}
return self.perform_v3_request(method='DELETE', path='playlists', params=params)
def get_supported_languages(self, language=None):
_language = language
if not _language:
_language = self._language
_language = _language.replace('-', '_')
params = {'part': 'snippet',
'hl': _language}
return self.perform_v3_request(method='GET', path='i18nLanguages', params=params)
def get_supported_regions(self, language=None):
_language = language
if not _language:
_language = self._language
_language = _language.replace('-', '_')
params = {'part': 'snippet',
'hl': _language}
return self.perform_v3_request(method='GET', path='i18nRegions', params=params)
def rename_playlist(self, playlist_id, new_title, privacy_status='private'):
params = {'part': 'snippet,id,status'}
post_data = {'kind': 'youtube#playlist',
'id': playlist_id,
'snippet': {'title': new_title},
'status': {'privacyStatus': privacy_status}}
return self.perform_v3_request(method='PUT', path='playlists', params=params, post_data=post_data)
def create_playlist(self, title, privacy_status='private'):
params = {'part': 'snippet,status'}
post_data = {'kind': 'youtube#playlist',
'snippet': {'title': title},
'status': {'privacyStatus': privacy_status}}
return self.perform_v3_request(method='POST', path='playlists', params=params, post_data=post_data)
def get_video_rating(self, video_id):
if isinstance(video_id, list):
video_id = ','.join(video_id)
params = {'id': video_id}
return self.perform_v3_request(method='GET', path='videos/getRating', params=params)
def rate_video(self, video_id, rating='like'):
"""
Rate a video
:param video_id: if of the video
:param rating: [like|dislike|none]
:return:
"""
params = {'id': video_id,
'rating': rating}
return self.perform_v3_request(method='POST', path='videos/rate', params=params)
def add_video_to_playlist(self, playlist_id, video_id):
params = {'part': 'snippet',
'mine': 'true'}
post_data = {'kind': 'youtube#playlistItem',
'snippet': {'playlistId': playlist_id,
'resourceId': {'kind': 'youtube#video',
'videoId': video_id}}}
return self.perform_v3_request(method='POST', path='playlistItems', params=params, post_data=post_data)
# noinspection PyUnusedLocal
def remove_video_from_playlist(self, playlist_id, playlist_item_id):
params = {'id': playlist_item_id}
return self.perform_v3_request(method='DELETE', path='playlistItems', params=params)
def unsubscribe(self, subscription_id):
params = {'id': subscription_id}
return self.perform_v3_request(method='DELETE', path='subscriptions', params=params)
def subscribe(self, channel_id):
params = {'part': 'snippet'}
post_data = {'kind': 'youtube#subscription',
'snippet': {'resourceId': {'kind': 'youtube#channel',
'channelId': channel_id}}}
return self.perform_v3_request(method='POST', path='subscriptions', params=params, post_data=post_data)
def get_subscription(self, channel_id, order='alphabetical', page_token=''):
"""
:param channel_id: [channel-id|'mine']
:param order: ['alphabetical'|'relevance'|'unread']
:param page_token:
:return:
"""
params = {'part': 'snippet',
'maxResults': str(self._max_results),
'order': order}
if channel_id == 'mine':
params['mine'] = 'true'
else:
params['channelId'] = channel_id
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='subscriptions', params=params)
def get_guide_category(self, guide_category_id, page_token=''):
params = {'part': 'snippet,contentDetails,brandingSettings',
'maxResults': str(self._max_results),
'categoryId': guide_category_id,
'regionCode': self._region,
'hl': self._language}
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='channels', params=params)
def get_guide_categories(self, page_token=''):
params = {'part': 'snippet',
'maxResults': str(self._max_results),
'regionCode': self._region,
'hl': self._language}
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='guideCategories', params=params)
def get_popular_videos(self, page_token=''):
params = {'part': 'snippet,status',
'maxResults': str(self._max_results),
'regionCode': self._region,
'hl': self._language,
'chart': 'mostPopular'}
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='videos', params=params)
def get_video_category(self, video_category_id, page_token=''):
params = {'part': 'snippet,contentDetails,status',
'maxResults': str(self._max_results),
'videoCategoryId': video_category_id,
'chart': 'mostPopular',
'regionCode': self._region,
'hl': self._language}
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='videos', params=params)
def get_video_categories(self, page_token=''):
params = {'part': 'snippet',
'maxResults': str(self._max_results),
'regionCode': self._region,
'hl': self._language}
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='videoCategories', params=params)
def _get_recommendations_for_home(self):
# YouTube has deprecated this API, so use history and related items to form
# a recommended set. We cache aggressively because searches incur a high
# quota cost of 100 on the YouTube API.
# Note this is a first stab attempt and can be refined a lot more.
cache = _context.get_data_cache()
# Do we have a cached result?
cache_home_key = 'get-activities-home'
cached = cache.get_item(cache.ONE_HOUR * 4, cache_home_key)
if cache_home_key in cached and cached[cache_home_key].get('items'):
return cached[cache_home_key]
# Fetch existing list of items, if any
items = []
cache_items_key = 'get-activities-home-items'
cached = cache.get_item(cache.ONE_WEEK * 2, cache_items_key)
if cache_items_key in cached:
items = cached[cache_items_key]
# Fetch history and recommended items. Use threads for faster execution.
def helper(video_id, responses):
_context.log_debug(
'Method get_activities: doing expensive API fetch for related'
'items for video %s' % video_id
)
di = self.get_related_videos(video_id, max_results=10)
if 'items' in di:
# Record for which video we fetched the items
for item in di['items']:
item['plugin_fetched_for'] = video_id
responses.extend(di['items'])
history = self.get_watch_history()
result = {
'kind': 'youtube#activityListResponse',
'items': []
}
if not history.get('items'):
return result
threads = []
candidates = []
already_fetched_for_video_ids = [item['plugin_fetched_for'] for item in items]
history_items = [item for item in history['items']
if re.match(r'(?P<video_id>[\w-]{11})', item['id'])]
# TODO:
# It would be nice to make this 8 user configurable
for item in history_items[:8]:
video_id = item['id']
if video_id not in already_fetched_for_video_ids:
thread = threading.Thread(target=helper, args=(video_id, candidates))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
# Prepend new candidates to items
seen = [item['id']['videoId'] for item in items]
for candidate in candidates:
vid = candidate['id']['videoId']
if vid not in seen:
seen.append(vid)
candidate['plugin_created_date'] = datetime_parser.now().strftime('%Y-%m-%dT%H:%M:%SZ')
items.insert(0, candidate)
# Truncate items to keep it manageable, and cache
items = items[:500]
cache.set(cache_items_key, json.dumps(items))
# Build the result set
items.sort(
key=lambda a: datetime_parser.parse(a['plugin_created_date']),
reverse=True
)
sorted_items = []
counter = 0
channel_counts = {}
while items:
counter += 1
# Hard stop on iteration. Good enough for our purposes.
if counter >= 1000:
break
# Reset channel counts on a new page
if counter % 50 == 0:
channel_counts = {}
# Ensure a single channel isn't hogging the page
item = items.pop()
channel_id = item['snippet']['channelId']
channel_counts.setdefault(channel_id, 0)
if channel_counts[channel_id] <= 3:
# Use the item
channel_counts[channel_id] = channel_counts[channel_id] + 1
item["page_number"] = counter // 50
sorted_items.append(item)
else:
# Move the item to the end of the list
items.append(item)
# Finally sort items per page by date for a better distribution
now = datetime_parser.now()
sorted_items.sort(
key=lambda a: (
a['page_number'],
datetime_parser.total_seconds(
now - datetime_parser.parse(a['snippet']['publishedAt'])
)
),
)
# Finalize result
result['items'] = sorted_items
"""
# TODO:
# Enable pagination
result['pageInfo'] = {
'resultsPerPage': 50,
'totalResults': len(sorted_items)
}
"""
# Update cache
cache.set(cache_home_key, json.dumps(result))
# If there are no sorted_items we fall back to default API behaviour
return result
def get_activities(self, channel_id, page_token=''):
params = {'part': 'snippet,contentDetails',
'maxResults': str(self._max_results),
'regionCode': self._region,
'hl': self._language}
if channel_id == 'home':
recommended = self._get_recommendations_for_home()
if 'items' in recommended and recommended.get('items'):
return recommended
if channel_id == 'home':
params['home'] = 'true'
elif channel_id == 'mine':
params['mine'] = 'true'
else:
params['channelId'] = channel_id
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='activities', params=params)
def get_channel_sections(self, channel_id):
params = {'part': 'snippet,contentDetails',
'regionCode': self._region,
'hl': self._language}
if channel_id == 'mine':
params['mine'] = 'true'
else:
params['channelId'] = channel_id
return self.perform_v3_request(method='GET', path='channelSections', params=params)
def get_playlists_of_channel(self, channel_id, page_token=''):
params = {'part': 'snippet',
'maxResults': str(self._max_results)}
if channel_id != 'mine':
params['channelId'] = channel_id
else:
params['mine'] = 'true'
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='playlists', params=params)
def get_playlist_item_id_of_video_id(self, playlist_id, video_id, page_token=''):
old_max_results = self._max_results
self._max_results = 50
json_data = self.get_playlist_items(playlist_id=playlist_id, page_token=page_token)
self._max_results = old_max_results
items = json_data.get('items', [])
for item in items:
playlist_item_id = item['id']
playlist_video_id = item.get('snippet', {}).get('resourceId', {}).get('videoId', '')
if playlist_video_id and playlist_video_id == video_id:
return playlist_item_id
next_page_token = json_data.get('nextPageToken', '')
if next_page_token:
return self.get_playlist_item_id_of_video_id(playlist_id=playlist_id, video_id=video_id,
page_token=next_page_token)
return None
def get_playlist_items(self, playlist_id, page_token='', max_results=None):
# prepare params
max_results = str(self._max_results) if max_results is None else str(max_results)
params = {'part': 'snippet',
'maxResults': max_results,
'playlistId': playlist_id}
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='playlistItems', params=params)
def get_channel_by_username(self, username):
"""
Returns a collection of zero or more channel resources that match the request criteria.
:param username: retrieve channel_id for username
:return:
"""
params = {'part': 'id'}
if username == 'mine':
params.update({'mine': 'true'})
else:
params.update({'forUsername': username})
return self.perform_v3_request(method='GET', path='channels', params=params)
def get_channels(self, channel_id):
"""
Returns a collection of zero or more channel resources that match the request criteria.
:param channel_id: list or comma-separated list of the YouTube channel ID(s)
:return:
"""
if isinstance(channel_id, list):
channel_id = ','.join(channel_id)
params = {'part': 'snippet,contentDetails,brandingSettings'}
if channel_id != 'mine':
params['id'] = channel_id
else:
params['mine'] = 'true'
return self.perform_v3_request(method='GET', path='channels', params=params)
def get_disliked_videos(self, page_token=''):
# prepare page token
if not page_token:
page_token = ''
# prepare params
params = {'part': 'snippet,status',
'myRating': 'dislike',
'maxResults': str(self._max_results)}
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='videos', params=params)
def get_videos(self, video_id, live_details=False):
"""
Returns a list of videos that match the API request parameters
:param video_id: list of video ids
:param live_details: also retrieve liveStreamingDetails
:return:
"""
if isinstance(video_id, list):
video_id = ','.join(video_id)
parts = ['snippet,contentDetails,status']
if live_details:
parts.append(',liveStreamingDetails')
params = {'part': ''.join(parts),
'id': video_id}
return self.perform_v3_request(method='GET', path='videos', params=params)
def get_playlists(self, playlist_id):
if isinstance(playlist_id, list):
playlist_id = ','.join(playlist_id)
params = {'part': 'snippet,contentDetails',
'id': playlist_id}
return self.perform_v3_request(method='GET', path='playlists', params=params)
def get_live_events(self, event_type='live', order='relevance', page_token='', location=False):
"""
:param event_type: one of: 'live', 'completed', 'upcoming'
:param order: one of: 'date', 'rating', 'relevance', 'title', 'videoCount', 'viewCount'
:param page_token:
:param location: bool, use geolocation
:return:
"""
# prepare page token
if not page_token:
page_token = ''
# prepare params
params = {'part': 'snippet',
'type': 'video',
'order': order,
'eventType': event_type,
'regionCode': self._region,
'hl': self._language,
'relevanceLanguage': self._language,
'maxResults': str(self._max_results)}
if location:
location = _context.get_settings().get_location()
if location:
params['location'] = location
params['locationRadius'] = _context.get_settings().get_location_radius()
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='search', params=params)
def get_related_videos(self, video_id, page_token='', max_results=0):
# prepare page token
if not page_token:
page_token = ''
max_results = self._max_results if max_results <= 0 else max_results
# prepare params
params = {'relatedToVideoId': video_id,
'part': 'snippet',
'type': 'video',
'regionCode': self._region,
'hl': self._language,
'maxResults': str(max_results)}
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='search', params=params)
def get_parent_comments(self, video_id, page_token='', max_results=0):
max_results = self._max_results if max_results <= 0 else max_results
# prepare params
params = {'part': 'snippet',
'videoId': video_id,
'order': 'relevance',
'textFormat': 'plainText',
'maxResults': str(max_results)}
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='commentThreads', params=params, no_login=True)
def get_child_comments(self, parent_id, page_token='', max_results=0):
max_results = self._max_results if max_results <= 0 else max_results
# prepare params
params = {'part': 'snippet',
'parentId': parent_id,
'textFormat': 'plainText',
'maxResults': str(max_results)}
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='comments', params=params, no_login=True)
def get_channel_videos(self, channel_id, page_token=''):
"""
Returns a collection of video search results for the specified channel_id
"""
params = {'part': 'snippet',
'hl': self._language,
'maxResults': str(self._max_results),
'type': 'video',
'safeSearch': 'none',
'order': 'date'}
if channel_id == 'mine':
params['forMine'] = 'true'
else:
params['channelId'] = channel_id
if page_token:
params['pageToken'] = page_token
return self.perform_v3_request(method='GET', path='search', params=params)
def search(self, q, search_type=None, event_type='', channel_id='',
order='relevance', safe_search='moderate', page_token='', location=False):
"""
Returns a collection of search results that match the query parameters specified in the API request. By default,
a search result set identifies matching video, channel, and playlist resources, but you can also configure
queries to only retrieve a specific type of resource.
:param q:
:param search_type: acceptable values are: 'video' | 'channel' | 'playlist'
:param event_type: 'live', 'completed', 'upcoming'
:param channel_id: limit search to channel id
:param order: one of: 'date', 'rating', 'relevance', 'title', 'videoCount', 'viewCount'
:param safe_search: one of: 'moderate', 'none', 'strict'
:param page_token: can be ''
:param location: bool, use geolocation
:return:
"""
if search_type is None:
search_type = ['video', 'channel', 'playlist']
# prepare search type
if not search_type:
search_type = ''
if isinstance(search_type, list):
search_type = ','.join(search_type)
# prepare page token
if not page_token:
page_token = ''
# prepare params
params = {'q': q,
'part': 'snippet',
'regionCode': self._region,
'hl': self._language,
'relevanceLanguage': self._language,
'maxResults': str(self._max_results)}
if event_type and event_type in ['live', 'upcoming', 'completed']:
params['eventType'] = event_type
if search_type:
params['type'] = search_type
if channel_id:
params['channelId'] = channel_id
if order:
params['order'] = order
if safe_search:
params['safeSearch'] = safe_search
if page_token:
params['pageToken'] = page_token
video_only_params = ['eventType', 'videoCaption', 'videoCategoryId', 'videoDefinition',
'videoDimension', 'videoDuration', 'videoEmbeddable', 'videoLicense',
'videoSyndicated', 'videoType', 'relatedToVideoId', 'forMine']
for key in video_only_params:
if params.get(key) is not None:
params['type'] = 'video'
break
if params['type'] == 'video' and location:
location = _context.get_settings().get_location()
if location:
params['location'] = location
params['locationRadius'] = _context.get_settings().get_location_radius()
return self.perform_v3_request(method='GET', path='search', params=params)
def get_my_subscriptions(self, page_token=None, offset=0):
if not page_token:
page_token = ''
result = {'items': [],
'next_page_token': page_token,
'offset': offset}
def _perform(_page_token, _offset, _result):
_post_data = {
'context': {
'client': {
'clientName': 'TVHTML5',
'clientVersion': '5.20150304',
'theme': 'CLASSIC',
'acceptRegion': '%s' % self._region,
'acceptLanguage': '%s' % self._language.replace('_', '-')
},
'user': {
'enableSafetyMode': False
}
},
'browseId': 'FEsubscriptions'
}
if _page_token:
_post_data['continuation'] = _page_token
_json_data = self.perform_v1_tv_request(method='POST', path='browse', post_data=_post_data)
_data = _json_data.get('contents', {}).get('sectionListRenderer', {}).get('contents', [{}])[0].get(
'shelfRenderer', {}).get('content', {}).get('horizontalListRenderer', {})
if not _data:
_data = _json_data.get('continuationContents', {}).get('horizontalListContinuation', {})
_items = _data.get('items', [])
if not _result:
_result = {'items': []}
_new_offset = self._max_results - len(_result['items']) + _offset
if _offset > 0:
_items = _items[_offset:]
_result['offset'] = _new_offset
for _item in _items:
_item = _item.get('gridVideoRenderer', {})
if _item:
_video_item = {'id': _item['videoId'],
'title': _item.get('title', {}).get('runs', [{}])[0].get('text', ''),
'channel': _item.get('shortBylineText', {}).get('runs', [{}])[0].get('text', '')}
_result['items'].append(_video_item)
_continuations = _data.get('continuations', [{}])[0].get('nextContinuationData', {}).get('continuation', '')
if _continuations and len(_result['items']) <= self._max_results:
_result['next_page_token'] = _continuations
if len(_result['items']) < self._max_results:
_result = _perform(_page_token=_continuations, _offset=0, _result=_result)
# trim result
if len(_result['items']) > self._max_results:
_items = _result['items']
_items = _items[:self._max_results]
_result['items'] = _items
_result['continue'] = True
if 'offset' in _result and _result['offset'] >= 100:
_result['offset'] -= 100
if len(_result['items']) < self._max_results:
if 'continue' in _result:
del _result['continue']
if 'next_page_token' in _result:
del _result['next_page_token']
if 'offset' in _result:
del _result['offset']
return _result
return _perform(_page_token=page_token, _offset=offset, _result=result)
def get_purchases(self, page_token, offset):
if not page_token:
page_token = ''
shelf_title = 'Purchases'
result = {'items': [],
'next_page_token': page_token,
'offset': offset}
def _perform(_page_token, _offset, _result, _shelf_index=None):
_post_data = {
'context': {
'client': {
'clientName': 'TVHTML5',
'clientVersion': '5.20150304',
'theme': 'CLASSIC',
'acceptRegion': '%s' % self._region,
'acceptLanguage': '%s' % self._language.replace('_', '-')
},
'user': {
'enableSafetyMode': False
}
}
}
if _page_token:
_post_data['continuation'] = _page_token
else:
_post_data['browseId'] = 'FEmy_youtube'
_json_data = self.perform_v1_tv_request(method='POST', path='browse', post_data=_post_data)
_data = {}
if 'continuationContents' in _json_data:
_data = _json_data.get('continuationContents', {}).get('horizontalListContinuation', {})
elif 'contents' in _json_data:
_contents = _json_data.get('contents', {}).get('sectionListRenderer', {}).get('contents', [{}])
if _shelf_index is None:
_shelf_index = get_shelf_index_by_title(_context, _json_data, shelf_title)
if _shelf_index is not None:
_data = _contents[_shelf_index].get('shelfRenderer', {}).get('content', {}).get('horizontalListRenderer', {})
_items = _data.get('items', [])
if not _result:
_result = {'items': []}
_new_offset = self._max_results - len(_result['items']) + _offset
if _offset > 0:
_items = _items[_offset:]
_result['offset'] = _new_offset
for _listItem in _items:
_item = _listItem.get('gridVideoRenderer', {})
if _item:
_video_item = {'id': _item['videoId'],
'title': _item.get('title', {}).get('runs', [{}])[0].get('text', ''),
'channel': _item.get('shortBylineText', {}).get('runs', [{}])[0].get('text', '')}
_result['items'].append(_video_item)
_item = _listItem.get('gridPlaylistRenderer', {})
if _item:
play_next_page_token = ''
while True:
json_playlist_data = self.get_playlist_items(_item['playlistId'], page_token=play_next_page_token)
_playListItems = json_playlist_data.get('items', {})
for _playListItem in _playListItems:
_playListItem = _playListItem.get('snippet', {})
if _playListItem:
_video_item = {'id': _playListItem.get('resourceId', {}).get('videoId', ''),
'title': _playListItem['title'],
'channel': _item.get('shortBylineText', {}).get('runs', [{}])[0].get('text', '')}
_result['items'].append(_video_item)
play_next_page_token = json_playlist_data.get('nextPageToken', '')
if not play_next_page_token or _context.abort_requested():
break
_continuations = _data.get('continuations', [{}])[0].get('nextContinuationData', {}).get('continuation', '')
if _continuations and len(_result['items']) <= self._max_results:
_result['next_page_token'] = _continuations
if len(_result['items']) < self._max_results:
_result = _perform(_page_token=_continuations, _offset=0, _result=_result, _shelf_index=shelf_index)
# trim result
if len(_result['items']) > self._max_results:
_items = _result['items']
_items = _items[:self._max_results]
_result['items'] = _items
_result['continue'] = True
if len(_result['items']) < self._max_results:
if 'continue' in _result:
del _result['continue']
if 'next_page_token' in _result:
del _result['next_page_token']
if 'offset' in _result:
del _result['offset']
return _result
shelf_index = None
if self._language != 'en' and not self._language.startswith('en-') and not page_token:
# shelf index is a moving target, make a request in english first to find the correct index by title
_en_post_data = {
'context': {
'client': {
'clientName': 'TVHTML5',
'clientVersion': '5.20150304',
'theme': 'CLASSIC',
'acceptRegion': 'US',
'acceptLanguage': 'en-US'
},
'user': {
'enableSafetyMode': False
}
},
'browseId': 'FEmy_youtube'
}
json_data = self.perform_v1_tv_request(method='POST', path='browse', post_data=_en_post_data)
shelf_index = get_shelf_index_by_title(_context, json_data, shelf_title)
result = _perform(_page_token=page_token, _offset=offset, _result=result, _shelf_index=shelf_index)
return result
def get_saved_playlists(self, page_token, offset):
if not page_token:
page_token = ''
shelf_title = 'Saved Playlists'
result = {'items': [],
'next_page_token': page_token,
'offset': offset}
def _perform(_page_token, _offset, _result, _shelf_index=None):
_post_data = {
'context': {
'client': {
'clientName': 'TVHTML5',
'clientVersion': '5.20150304',
'theme': 'CLASSIC',
'acceptRegion': '%s' % self._region,
'acceptLanguage': '%s' % self._language.replace('_', '-')
},
'user': {
'enableSafetyMode': False
}
}
}
if _page_token:
_post_data['continuation'] = _page_token
else:
_post_data['browseId'] = 'FEmy_youtube'
_json_data = self.perform_v1_tv_request(method='POST', path='browse', post_data=_post_data)
_data = {}
if 'continuationContents' in _json_data:
_data = _json_data.get('continuationContents', {}).get('horizontalListContinuation', {})
elif 'contents' in _json_data:
_contents = _json_data.get('contents', {}).get('sectionListRenderer', {}).get('contents', [{}])
if _shelf_index is None:
_shelf_index = get_shelf_index_by_title(_context, _json_data, shelf_title)
if _shelf_index is not None:
_data = _contents[_shelf_index].get('shelfRenderer', {}).get('content', {}).get('horizontalListRenderer', {})
_items = _data.get('items', [])
if not _result:
_result = {'items': []}
_new_offset = self._max_results - len(_result['items']) + _offset
if _offset > 0:
_items = _items[_offset:]
_result['offset'] = _new_offset
for _item in _items:
_item = _item.get('gridPlaylistRenderer', {})
if _item:
_video_item = {'id': _item['playlistId'],
'title': _item.get('title', {}).get('runs', [{}])[0].get('text', ''),
'channel': _item.get('shortBylineText', {}).get('runs', [{}])[0].get('text', ''),
'channel_id': _item.get('shortBylineText', {}).get('runs', [{}])[0].get('navigationEndpoint', {}).get('browseEndpoint', {}).get('browseId', ''),
'thumbnails': {'default': {'url': ''}, 'medium': {'url': ''}, 'high': {'url': ''}}}
_thumbs = _item.get('thumbnail', {}).get('thumbnails', [{}])
for _thumb in _thumbs:
_thumb_url = _thumb.get('url', '')
if _thumb_url.startswith('//'):
_thumb_url = ''.join(['https:', _thumb_url])
if _thumb_url.endswith('/default.jpg'):
_video_item['thumbnails']['default']['url'] = _thumb_url
elif _thumb_url.endswith('/mqdefault.jpg'):
_video_item['thumbnails']['medium']['url'] = _thumb_url
elif _thumb_url.endswith('/hqdefault.jpg'):
_video_item['thumbnails']['high']['url'] = _thumb_url
_result['items'].append(_video_item)
_continuations = _data.get('continuations', [{}])[0].get('nextContinuationData', {}).get('continuation', '')
if _continuations and len(_result['items']) <= self._max_results:
_result['next_page_token'] = _continuations
if len(_result['items']) < self._max_results:
_result = _perform(_page_token=_continuations, _offset=0, _result=_result, _shelf_index=_shelf_index)
# trim result
if len(_result['items']) > self._max_results:
_items = _result['items']
_items = _items[:self._max_results]
_result['items'] = _items
_result['continue'] = True
if len(_result['items']) < self._max_results:
if 'continue' in _result:
del _result['continue']
if 'next_page_token' in _result:
del _result['next_page_token']
if 'offset' in _result:
del _result['offset']
return _result
shelf_index = None
if self._language != 'en' and not self._language.startswith('en-') and not page_token:
# shelf index is a moving target, make a request in english first to find the correct index by title
_en_post_data = {
'context': {
'client': {
'clientName': 'TVHTML5',
'clientVersion': '5.20150304',
'theme': 'CLASSIC',
'acceptRegion': 'US',
'acceptLanguage': 'en-US'
},
'user': {
'enableSafetyMode': False
}
},
'browseId': 'FEmy_youtube'
}
json_data = self.perform_v1_tv_request(method='POST', path='browse', post_data=_en_post_data)
shelf_index = get_shelf_index_by_title(_context, json_data, shelf_title)
result = _perform(_page_token=page_token, _offset=offset, _result=result, _shelf_index=shelf_index)
return result
def clear_watch_history(self):
_post_data = {
'context': {
'client': {
'clientName': 'TVHTML5',
'clientVersion': '5.20150304',
'theme': 'CLASSIC',
'acceptRegion': '%s' % self._region,
'acceptLanguage': '%s' % self._language.replace('_', '-')
},
'user': {
'enableSafetyMode': False
}
}
}
_json_data = self.perform_v1_tv_request(method='POST', path='history/clear_watch_history', post_data=_post_data)
return _json_data
def get_watch_history(self, page_token=None, offset=0):
if not page_token:
page_token = ''
result = {'items': [],
'next_page_token': page_token,
'offset': offset}
def _perform(_page_token, _offset, _result):
_post_data = {
'context': {
'client': {
'clientName': 'TVHTML5',
'clientVersion': '5.20150304',
'theme': 'CLASSIC',
'acceptRegion': '%s' % self._region,
'acceptLanguage': '%s' % self._language.replace('_', '-')
},
'user': {
'enableSafetyMode': False
}
},
'browseId': 'FEhistory'
}
if _page_token:
_post_data['continuation'] = _page_token
_json_data = self.perform_v1_tv_request(method='POST', path='browse', post_data=_post_data)
_data = _json_data.get('contents', {}).get('sectionListRenderer', {}).get('contents', [{}])[0].get(
'shelfRenderer', {}).get('content', {}).get('horizontalListRenderer', {})
if not _data:
_data = _json_data.get('continuationContents', {}).get('horizontalListContinuation', {})
_items = _data.get('items', [])
if not _result:
_result = {'items': []}
_new_offset = self._max_results - len(_result['items']) + _offset
if _offset > 0:
_items = _items[_offset:]
_result['offset'] = _new_offset
for _item in _items:
_item = _item.get('gridVideoRenderer', {})
if _item:
_video_item = {'id': _item['videoId'],
'title': _item.get('title', {}).get('runs', [{}])[0].get('text', ''),
'channel': _item.get('shortBylineText', {}).get('runs', [{}])[0].get('text', '')}
_result['items'].append(_video_item)
_continuations = _data.get('continuations', [{}])[0].get('nextContinuationData', {}).get('continuation', '')
if _continuations and len(_result['items']) <= self._max_results:
_result['next_page_token'] = _continuations
if len(_result['items']) < self._max_results:
_result = _perform(_page_token=_continuations, _offset=0, _result=_result)
# trim result
if len(_result['items']) > self._max_results:
_items = _result['items']
_items = _items[:self._max_results]
_result['items'] = _items
_result['continue'] = True
if len(_result['items']) < self._max_results:
if 'continue' in _result:
del _result['continue']
if 'next_page_token' in _result:
del _result['next_page_token']
if 'offset' in _result:
del _result['offset']
return _result
return _perform(_page_token=page_token, _offset=offset, _result=result)
def get_watch_later_id(self):
watch_later_id = ''
def _get_items(_continuation=None):
post_data = {
'context': {
'client': {
'clientName': 'TVHTML5',
'clientVersion': '5.20150304',
'theme': 'CLASSIC',
'acceptRegion': 'US',
'acceptLanguage': 'en-US'
},
'user': {
'enableSafetyMode': False
}
},
'browseId': 'default'
}
if _continuation:
post_data['continuation'] = _continuation
return self.perform_v1_tv_request(method='POST', path='browse', post_data=post_data)
current_page = 1
pages = 30 # 28 seems to be page limit, add a couple page padding, loop will break when there is no next page data
progress_dialog = _context.get_ui().create_progress_dialog(_context.get_name(),
_context.localize(constants.localize.COMMON_PLEASE_WAIT),
background=True)
progress_dialog.set_total(pages)
progress_dialog.update(steps=1, text=_context.localize(constants.localize.WATCH_LATER_RETRIEVAL_PAGE) % str(current_page))
try:
json_data = _get_items()
while current_page < pages:
contents = json_data.get('contents', json_data.get('continuationContents', {}))
section = contents.get('sectionListRenderer', contents.get('sectionListContinuation', {}))
contents = section.get('contents', [{}])
for shelf in contents:
renderer = shelf.get('shelfRenderer', {})
endpoint = renderer.get('endpoint', {})
browse_endpoint = endpoint.get('browseEndpoint', {})
browse_id = browse_endpoint.get('browseId', '')
title = renderer.get('title', {})
title_runs = title.get('runs', [{}])[0]
title_text = title_runs.get('text', '')
if (title_text.lower() == 'watch later') and (browse_id.startswith('VLPL') or browse_id.startswith('PL')):
watch_later_id = browse_id.lstrip('VL')
break
if watch_later_id:
break
continuations = section.get('continuations', [{}])[0]
next_continuation_data = continuations.get('nextContinuationData', {})
continuation = next_continuation_data.get('continuation', '')
if continuation:
current_page += 1
progress_dialog.update(steps=1, text=_context.localize(constants.localize.WATCH_LATER_RETRIEVAL_PAGE) % str(current_page))
json_data = _get_items(continuation)
continue
else:
break
finally:
progress_dialog.close()
return watch_later_id
def perform_v3_request(self, method='GET', headers=None, path=None, post_data=None, params=None,
allow_redirects=True, no_login=False):
yt_config = self._config
if not yt_config.get('key'):
return {
'error':
{
'errors': [{'reason': 'accessNotConfigured'}],
'message': 'No API keys provided'
}
}
# params
if not params:
params = {}
_params = {'key': yt_config['key']}
_params.update(params)
# headers
if not headers:
headers = {}
_headers = {'Host': 'www.googleapis.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.36 Safari/537.36',
'Accept-Encoding': 'gzip, deflate'}
# a config can decide if a token is allowed
if self._access_token and yt_config.get('token-allowed', True) and not no_login:
_headers['Authorization'] = 'Bearer %s' % self._access_token
_headers.update(headers)
# url
_url = 'https://www.googleapis.com/youtube/v3/%s' % path.strip('/')
result = None
log_params = copy.deepcopy(params)
if 'location' in log_params:
log_params['location'] = 'xx.xxxx,xx.xxxx'
_context.log_debug('[data] v3 request: |{0}| path: |{1}| params: |{2}| post_data: |{3}|'.format(method, path, log_params, post_data))
if method == 'GET':
result = requests.get(_url, params=_params, headers=_headers, verify=self._verify, allow_redirects=allow_redirects)
elif method == 'POST':
_headers['content-type'] = 'application/json'
result = requests.post(_url, json=post_data, params=_params, headers=_headers, verify=self._verify,
allow_redirects=allow_redirects)
elif method == 'PUT':
_headers['content-type'] = 'application/json'
result = requests.put(_url, json=post_data, params=_params, headers=_headers, verify=self._verify,
allow_redirects=allow_redirects)
elif method == 'DELETE':
result = requests.delete(_url, params=_params, headers=_headers, verify=self._verify,
allow_redirects=allow_redirects)
_context.log_debug('[data] v3 response: |{0}| headers: |{1}|'.format(result.status_code, result.headers))
if result is None:
return {}
if result.headers.get('content-type', '').startswith('application/json'):
try:
return result.json()
except ValueError:
return {
'status_code': result.status_code,
'payload': result.text
}
return {}
def perform_v1_tv_request(self, method='GET', headers=None, path=None, post_data=None, params=None,
allow_redirects=True):
# params
if not params:
params = {}
_params = {'key': self._config_tv['key']}
_params.update(params)
# headers
if not headers:
headers = {}
_headers = {'Host': 'www.googleapis.com',
'Connection': 'keep-alive',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36',
'Origin': 'https://www.youtube.com',
'Accept': '*/*',
'DNT': '1',
'Referer': 'https://www.youtube.com/tv',
'Accept-Encoding': 'gzip',
'Accept-Language': 'en-US,en;q=0.8,de;q=0.6'}
if self._access_token_tv:
_headers['Authorization'] = 'Bearer %s' % self._access_token_tv
_headers.update(headers)
# url
_url = 'https://www.googleapis.com/youtubei/v1/%s' % path.strip('/')
result = None
_context.log_debug('[i] v1 request: |{0}| path: |{1}| params: |{2}| post_data: |{3}|'.format(method, path, params, post_data))
if method == 'GET':
result = requests.get(_url, params=_params, headers=_headers, verify=self._verify, allow_redirects=allow_redirects)
elif method == 'POST':
_headers['content-type'] = 'application/json'
result = requests.post(_url, json=post_data, params=_params, headers=_headers, verify=self._verify,
allow_redirects=allow_redirects)
elif method == 'PUT':
_headers['content-type'] = 'application/json'
result = requests.put(_url, json=post_data, params=_params, headers=_headers, verify=self._verify,
allow_redirects=allow_redirects)
elif method == 'DELETE':
result = requests.delete(_url, params=_params, headers=_headers, verify=self._verify,
allow_redirects=allow_redirects)
if result is None:
return {}
if result.headers.get('content-type', '').startswith('application/json'):
return result.json()