-
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
Copyright (C) 2017-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 re
|
||||
|
||||
from youtube_plugin.youtube.provider import Provider
|
||||
from youtube_plugin.kodion.context import XbmcContext
|
||||
|
||||
|
||||
def __get_core_components(addon_id=None):
|
||||
"""
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:return: addon provider, context and client
|
||||
"""
|
||||
provider = Provider()
|
||||
if addon_id is not None:
|
||||
context = XbmcContext(params={'addon_id': addon_id})
|
||||
else:
|
||||
context = XbmcContext()
|
||||
client = provider.get_client(context=context)
|
||||
|
||||
return provider, context, client
|
||||
|
||||
|
||||
def v3_request(method='GET', headers=None, path=None, post_data=None, params=None, addon_id=None):
|
||||
"""
|
||||
https://developers.google.com/youtube/v3/docs/
|
||||
:param method:
|
||||
:param headers:
|
||||
:param path:
|
||||
:param post_data:
|
||||
:param params:
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:type addon_id: str
|
||||
"""
|
||||
provider, context, client = __get_core_components(addon_id)
|
||||
return client.perform_v3_request(method=method,
|
||||
headers=headers,
|
||||
path=path,
|
||||
post_data=post_data,
|
||||
params=params,
|
||||
notify=False,
|
||||
pass_data=True,
|
||||
raise_exc=False)
|
||||
|
||||
|
||||
def _append_missing_page_token(items):
|
||||
if items and isinstance(items, list) and 'nextPageToken' not in items[-1]:
|
||||
items.append({'nextPageToken': ''})
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def get_videos(video_id, addon_id=None):
|
||||
"""
|
||||
|
||||
:param video_id: video id(s)
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:type video_id: str | list
|
||||
:type addon_id: str
|
||||
:return: list of <kind: youtube#video> <parts: ['snippet', 'contentDetails']> for the given video id(s)
|
||||
see also https://developers.google.com/youtube/v3/docs/videos#resource
|
||||
:rtype: list of dict
|
||||
"""
|
||||
provider, context, client = __get_core_components(addon_id)
|
||||
|
||||
json_data = client.get_videos(video_id,
|
||||
notify=False,
|
||||
pass_data=True,
|
||||
raise_exc=False)
|
||||
if not json_data or 'error' in json_data:
|
||||
return [json_data]
|
||||
|
||||
return json_data.get('items', [{}])
|
||||
|
||||
|
||||
def get_activities(channel_id, page_token='', all_pages=False, addon_id=None):
|
||||
"""
|
||||
:param channel_id: channel id
|
||||
:param page_token: nextPageToken for starting page
|
||||
:param all_pages: return all pages(starting at page_token) or single page
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:type channel_id: str
|
||||
:type page_token: str
|
||||
:type all_pages: bool
|
||||
:type addon_id: str
|
||||
:return: list of <kind: youtube#activity> <parts: ['snippet', 'contentDetails']> for the given channel id
|
||||
see also https://developers.google.com/youtube/v3/docs/activities#resource
|
||||
last item contains nextPageToken
|
||||
:rtype: list of dict
|
||||
"""
|
||||
provider, context, client = __get_core_components(addon_id)
|
||||
|
||||
items = []
|
||||
|
||||
def get_items(_page_token=''):
|
||||
json_data = client.get_activities(channel_id,
|
||||
page_token=_page_token,
|
||||
notify=False,
|
||||
pass_data=True,
|
||||
raise_exc=False)
|
||||
if not json_data or 'error' in json_data:
|
||||
return [json_data]
|
||||
|
||||
items.extend(json_data.get('items', [{}]))
|
||||
error = False
|
||||
|
||||
next_page_token = json_data.get('nextPageToken')
|
||||
if not next_page_token:
|
||||
return error
|
||||
if all_pages:
|
||||
error = get_items(_page_token=next_page_token)
|
||||
else:
|
||||
items.append({'nextPageToken': next_page_token})
|
||||
return error
|
||||
|
||||
error = get_items(_page_token=page_token)
|
||||
if error:
|
||||
return error
|
||||
|
||||
items = _append_missing_page_token(items)
|
||||
return items
|
||||
|
||||
|
||||
def get_playlist_items(playlist_id, page_token='', all_pages=False, addon_id=None):
|
||||
"""
|
||||
|
||||
:param playlist_id: playlist id
|
||||
:param page_token: nextPageToken for starting page
|
||||
:param all_pages: return all pages(starting at page_token) or single page
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:type playlist_id: str
|
||||
:type page_token: str
|
||||
:type all_pages: bool
|
||||
:type addon_id: str
|
||||
:return: list of <kind: youtube#playlistItem> <parts: ['snippet', 'contentDetails']> for the given playlist id
|
||||
see also https://developers.google.com/youtube/v3/docs/playlistItems#resource
|
||||
last item contains nextPageToken
|
||||
:rtype: list of dict
|
||||
"""
|
||||
provider, context, client = __get_core_components(addon_id)
|
||||
|
||||
items = []
|
||||
|
||||
def get_items(_page_token=''):
|
||||
json_data = client.get_playlist_items(playlist_id,
|
||||
page_token=_page_token,
|
||||
notify=False,
|
||||
pass_data=True,
|
||||
raise_exc=False)
|
||||
if not json_data or 'error' in json_data:
|
||||
return [json_data]
|
||||
|
||||
items.extend(json_data.get('items', [{}]))
|
||||
error = False
|
||||
|
||||
next_page_token = json_data.get('nextPageToken')
|
||||
if not next_page_token:
|
||||
return error
|
||||
if all_pages:
|
||||
error = get_items(_page_token=next_page_token)
|
||||
else:
|
||||
items.append({'nextPageToken': next_page_token})
|
||||
return error
|
||||
|
||||
error = get_items(_page_token=page_token)
|
||||
if error:
|
||||
return error
|
||||
|
||||
items = _append_missing_page_token(items)
|
||||
return items
|
||||
|
||||
|
||||
def get_channel_id(identifier,
|
||||
mine=False,
|
||||
handle=False,
|
||||
addon_id=None):
|
||||
"""
|
||||
:param str identifier: channel username to retrieve channel ID for
|
||||
:param bool mine: treat identifier as request for authenticated user
|
||||
:param bool handle: treat identifier as request for handle
|
||||
:param str addon_id: addon id associated with developer keys to use for requests
|
||||
:return: list of <kind: youtube#channel> <parts: ['id']> for the given channel name
|
||||
see also https://developers.google.com/youtube/v3/docs/channels#resource
|
||||
:rtype: list of dict
|
||||
"""
|
||||
provider, context, client = __get_core_components(addon_id)
|
||||
|
||||
json_data = client.get_channel_by_identifier(identifier=identifier,
|
||||
mine=mine,
|
||||
handle=handle,
|
||||
as_json=True,
|
||||
notify=False,
|
||||
pass_data=True,
|
||||
raise_exc=False)
|
||||
if not json_data or 'error' in json_data:
|
||||
return [json_data]
|
||||
|
||||
return json_data.get('items', [{}])
|
||||
|
||||
|
||||
def get_channels(channel_id, addon_id=None):
|
||||
"""
|
||||
|
||||
:param channel_id: channel id(s)
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:type channel_id: str | list
|
||||
:type addon_id: str
|
||||
:return: list of <kind: youtube#channel> <parts: ['snippet', 'contentDetails', 'brandingSettings']> for the given channel id(s)
|
||||
see also https://developers.google.com/youtube/v3/docs/channels#resource
|
||||
:rtype: list of dict
|
||||
"""
|
||||
provider, context, client = __get_core_components(addon_id)
|
||||
|
||||
json_data = client.get_channels(channel_id,
|
||||
notify=False,
|
||||
pass_data=True,
|
||||
raise_exc=False)
|
||||
if not json_data or 'error' in json_data:
|
||||
return [json_data]
|
||||
|
||||
return json_data.get('items', [{}])
|
||||
|
||||
|
||||
def get_channel_sections(channel_id, addon_id=None):
|
||||
"""
|
||||
|
||||
:param channel_id: channel id
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:type channel_id: str
|
||||
:type addon_id: str
|
||||
:return: list of <kind: youtube#channelSections> <parts: ['snippet', 'contentDetails']> for the given channel id
|
||||
see also https://developers.google.com/youtube/v3/docs/channelSections#resource
|
||||
:rtype: list of dict
|
||||
"""
|
||||
provider, context, client = __get_core_components(addon_id)
|
||||
|
||||
json_data = client.get_channel_sections(channel_id,
|
||||
notify=False,
|
||||
pass_data=True,
|
||||
raise_exc=False)
|
||||
if not json_data or 'error' in json_data:
|
||||
return [json_data]
|
||||
|
||||
return json_data.get('items', [{}])
|
||||
|
||||
|
||||
def get_playlists_of_channel(channel_id, page_token='', all_pages=False, addon_id=None):
|
||||
"""
|
||||
|
||||
:param channel_id: channel id
|
||||
:param page_token: nextPageToken for starting page
|
||||
:param all_pages: return all pages(starting at page_token) or single page
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:type channel_id: str
|
||||
:type page_token: str
|
||||
:type all_pages: bool
|
||||
:type addon_id: str
|
||||
:return: list of <kind: youtube#playlists> <parts: ['snippet']> for the given channel id
|
||||
see also https://developers.google.com/youtube/v3/docs/playlists#resource
|
||||
last item contains nextPageToken
|
||||
:rtype: list of dict
|
||||
"""
|
||||
provider, context, client = __get_core_components(addon_id)
|
||||
|
||||
items = []
|
||||
|
||||
def get_items(_page_token=''):
|
||||
json_data = client.get_playlists_of_channel(channel_id,
|
||||
page_token=_page_token,
|
||||
notify=False,
|
||||
pass_data=True,
|
||||
raise_exc=False)
|
||||
if not json_data or 'error' in json_data:
|
||||
return [json_data]
|
||||
|
||||
items.extend(json_data.get('items', [{}]))
|
||||
error = False
|
||||
|
||||
next_page_token = json_data.get('nextPageToken')
|
||||
if not next_page_token:
|
||||
return error
|
||||
if all_pages:
|
||||
error = get_items(_page_token=next_page_token)
|
||||
else:
|
||||
items.append({'nextPageToken': next_page_token})
|
||||
return error
|
||||
|
||||
error = get_items(_page_token=page_token)
|
||||
if error:
|
||||
return error
|
||||
|
||||
items = _append_missing_page_token(items)
|
||||
return items
|
||||
|
||||
|
||||
def get_playlists(playlist_id, addon_id=None):
|
||||
"""
|
||||
|
||||
:param playlist_id: playlist id(s)
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:type playlist_id: str | list
|
||||
:type addon_id: str
|
||||
:return: list of <kind: youtube#playlists> <parts: ['snippet', 'contentDetails']> for the given playlist id(s)
|
||||
see also https://developers.google.com/youtube/v3/docs/playlists#resource
|
||||
:rtype: list of dict
|
||||
"""
|
||||
provider, context, client = __get_core_components(addon_id)
|
||||
|
||||
json_data = client.get_playlists(playlist_id,
|
||||
notify=False,
|
||||
pass_data=True,
|
||||
raise_exc=False)
|
||||
if not json_data or 'error' in json_data:
|
||||
return [json_data]
|
||||
|
||||
return json_data.get('items', [{}])
|
||||
|
||||
|
||||
def get_related_videos(video_id, page_token='', addon_id=None):
|
||||
"""
|
||||
|
||||
:param video_id: video id
|
||||
:param page_token: nextPageToken for page
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:type video_id: str
|
||||
:type page_token: str
|
||||
:type addon_id: str
|
||||
:return: list of <kind: youtube#searchResult> <parts: ['snippet']> for the given video id
|
||||
see also https://developers.google.com/youtube/v3/docs/search#resource
|
||||
last item contains nextPageToken
|
||||
:rtype: list of dict
|
||||
:note: this is a search api request with high cost
|
||||
"""
|
||||
provider, context, client = __get_core_components(addon_id)
|
||||
|
||||
items = []
|
||||
|
||||
def get_items(_page_token=''):
|
||||
json_data = client.get_related_videos(video_id,
|
||||
page_token=_page_token,
|
||||
notify=False,
|
||||
pass_data=True,
|
||||
raise_exc=False)
|
||||
if not json_data or 'error' in json_data:
|
||||
return [json_data]
|
||||
|
||||
items.extend([item for item in json_data.get('items', [{}])
|
||||
if 'snippet' in item])
|
||||
error = False
|
||||
|
||||
next_page_token = json_data.get('nextPageToken')
|
||||
if next_page_token:
|
||||
items.append({'nextPageToken': next_page_token})
|
||||
return error
|
||||
|
||||
error = get_items(_page_token=page_token)
|
||||
if error:
|
||||
return error
|
||||
|
||||
items = _append_missing_page_token(items)
|
||||
return items
|
||||
|
||||
|
||||
def get_search(q, search_type='', event_type='', channel_id='', order='relevance', safe_search='moderate', page_token='', addon_id=None):
|
||||
"""
|
||||
|
||||
:param q: search query
|
||||
:param search_type: acceptable values are: 'video' | 'channel' | 'playlist', defaults to ['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: nextPageToken for page
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:type q: str
|
||||
:type search_type: str | list
|
||||
:type event_type: str
|
||||
:type channel_id: str
|
||||
:type order: str
|
||||
:type safe_search: str
|
||||
:type page_token: str
|
||||
:type addon_id: str
|
||||
:return: list of <kind: youtube#searchResult> <parts: ['snippet']> for the given parameters,
|
||||
see also https://developers.google.com/youtube/v3/docs/search#resource
|
||||
last item contains nextPageToken
|
||||
:rtype: list of dict
|
||||
:note: this is a search api request with high cost
|
||||
"""
|
||||
search_type = search_type or ['video', 'channel', 'playlist']
|
||||
provider, context, client = __get_core_components(addon_id)
|
||||
|
||||
items = []
|
||||
|
||||
def get_items(_page_token=''):
|
||||
json_data = client.search(q,
|
||||
search_type=search_type,
|
||||
event_type=event_type,
|
||||
channel_id=channel_id,
|
||||
order=order,
|
||||
safe_search=safe_search,
|
||||
page_token=_page_token,
|
||||
notify=False,
|
||||
pass_data=True,
|
||||
raise_exc=False)
|
||||
if not json_data or 'error' in json_data:
|
||||
return [json_data]
|
||||
|
||||
items.extend(json_data.get('items', [{}]))
|
||||
error = False
|
||||
|
||||
next_page_token = json_data.get('nextPageToken')
|
||||
if next_page_token:
|
||||
items.append({'nextPageToken': next_page_token})
|
||||
return error
|
||||
|
||||
error = get_items(_page_token=page_token)
|
||||
if error:
|
||||
return error
|
||||
|
||||
items = _append_missing_page_token(items)
|
||||
return items
|
||||
|
||||
|
||||
def get_live(channel_id=None, user=None, url=None, addon_id=None):
|
||||
"""
|
||||
|
||||
:param channel_id: a channel id
|
||||
One of channel_id, user, or url required
|
||||
ex. UCLA_DiR1FfKNvjuUpBHmylQ
|
||||
:param user: a channel username
|
||||
One of channel_id, user, or url required
|
||||
ex. NASAtelevision
|
||||
:param url: a channel url
|
||||
One of channel_id, channel_id, or url required
|
||||
ex.
|
||||
https://www.youtube.com/channel/UCLA_DiR1FfKNvjuUpBHmylQ
|
||||
https://www.youtube.com/channel/UCLA_DiR1FfKNvjuUpBHmylQ/live
|
||||
https://www.youtube.com/user/NASAtelevision
|
||||
https://www.youtube.com/user/NASAtelevision/live
|
||||
:param addon_id: addon id associated with developer keys to use for requests
|
||||
:type channel_id: str, optional
|
||||
:type user: str, optional
|
||||
:type url: str, optional
|
||||
:type addon_id: str, optional
|
||||
:return: all live stream items for the given channel
|
||||
:rtype: list of dicts, or None
|
||||
"""
|
||||
|
||||
if not channel_id and not user and not url:
|
||||
return None
|
||||
|
||||
matched_id = None
|
||||
matched_type = None
|
||||
live_content = []
|
||||
|
||||
if channel_id:
|
||||
matched_id = channel_id
|
||||
matched_type = 'channel'
|
||||
|
||||
elif user:
|
||||
matched_id = user
|
||||
matched_type = 'user'
|
||||
|
||||
elif url:
|
||||
patterns = [r'^(?:http)*s*:*[/]{0,2}(?:w{3}\.|m\.)*youtu(?:\.be|be\.com)/'
|
||||
r'(?P<type>channel|user)/(?P<channel_id>[^/]+)(?:/live)*$']
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, url)
|
||||
if match:
|
||||
matched_id = match.group(CHANNEL_ID)
|
||||
matched_type = match.group('type')
|
||||
break
|
||||
|
||||
if not matched_id or not matched_type:
|
||||
return None
|
||||
|
||||
if matched_type == 'user':
|
||||
items = get_channel_id(matched_id, addon_id=addon_id)
|
||||
if not items or not isinstance(items, list) or 'id' not in items[0]:
|
||||
return None
|
||||
|
||||
matched_id = items[0]['id']
|
||||
|
||||
search_results = get_search(q='', search_type='video', event_type='live',
|
||||
channel_id=matched_id, safe_search='none', addon_id=addon_id)
|
||||
|
||||
if not search_results:
|
||||
return None
|
||||
|
||||
for search_result in search_results:
|
||||
if 'id' in search_result and 'videoId' in search_result['id'] and 'snippet' in search_result:
|
||||
search_result['snippet']['videoId'] = search_result['id']['videoId']
|
||||
live_content.append(search_result['snippet'])
|
||||
|
||||
return live_content
|
||||
Reference in New Issue
Block a user