def incompatible_validator(**kwargs): """ Validate the incompatible parameters. Args: kwargs (str) Parameter need to do validate. Returns: None """ given = 0 for name, param in kwargs.items(): if param is not None: given += 1 params = ','.join(kwargs.keys()) if given == 0: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.MISSING_PARAMS, message=f'Specify at least one of {params}')) elif given > 1: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message=f'Incompatible parameters specified for {params}'))
def get_comment_thread_info(self, comment_thread_id=None, return_json=False): """ Retrieve the comment thread info by single id. Refer: https://developers.google.com/youtube/v3/docs/commentThreads/list Args: comment_thread_id (str) The id parameter specifies a comma-separated list of comment thread IDs for the resources that should be retrieved. return_json(bool, optional) The return data type. If you set True JSON data will be returned. False will return pyyoutube.CommentThread. Returns: The list data for you given comment thread. """ part = 'id,snippet,replies' if comment_thread_id is None: raise PyYouTubeException( ErrorMessage(status_code=10005, message='Must Specify the id for the video')) args = {'id': comment_thread_id, 'part': part} resp = self._request(resource='commentThreads', args=args) data = self._parse_response(resp, api=True) if return_json: return data else: return [CommentThread.new_from_json_dict(item) for item in data]
def get_comment_info(self, comment_id=None, return_json=False): """ Args: comment_id (str, optional) Provide a comma-separated list of comment IDs or just a comment id for the resources that are being retrieved return_json(bool, optional) The return data type. If you set True JSON data will be returned. False will return pyyoutube.Comment. Returns: The list data for you given comment id. """ part = 'id,snippet' args = { 'part': part, } if comment_id is not None: args['id'] = comment_id else: raise PyYouTubeException( ErrorMessage(status_code=10007, message='Comment id must specified')) resp = self._request(resource='comments', args=args) data = self._parse_response(resp, api=True) if return_json: return data else: return [Comment.new_from_json_dict(item) for item in data]
def refresh_token(self, refresh_token=None, return_json=False): """ Refresh token by api return refresh token. Args: refresh_token (str) The refresh token which the api returns. If you not provide. will use saved token when do exchange token step retrieve. return_json (bool, optional): If True JSON data will be returned, instead of pyyoutube.AccessToken Return: Retrieved new access token's info, pyyoutube.AccessToken instance. """ if refresh_token is None: refresh_token = self._refresh_token if refresh_token is None: raise PyYouTubeException( ErrorMessage( status_code=10003, message= 'You must provide a refresh token to get a new access token' )) kwargs = { 'refresh_token': refresh_token, 'client_id': self._client_id, 'client_secret': self._client_secret, 'grant_type': 'refresh_token' } return self._fetch_token(kwargs, return_json)
def get_videos_info(self, video_ids=None, return_json=False): """ Retrieve data from YouTube Data Api for video which you point. Args: video_ids (list) The video's ID list you want to get data. return_json(bool, optional) The return data type. If you set True JSON data will be returned. False will return pyyoutube.Video Returns: The data for you given video. """ if video_ids is None or not isinstance(video_ids, (list, tuple)): raise PyYouTubeException( ErrorMessage(status_code=10005, message='Specify the id for the video.')) args = { 'id': ','.join(video_ids), 'part': 'id,snippet,contentDetails,statistics,status' } resp = self._request(resource='videos', method='GET', args=args) data = self._parse_response(resp, api=True) self.calc_quota(resource='videos', parts=args['part'], count=len(video_ids)) if return_json: return data else: return [Video.new_from_json_dict(item) for item in data]
def get_profile(self, access_token=None, return_json=False): """ Get token user info. Args: access_token(str, optional) If you not provide api key, you can do authorization to get an access token. return_json(bool, optional) The return data type. If you set True JSON data will be returned. False will return pyyoutube.UserProfile Returns: The data for you given access token's user info. """ if access_token is None: access_token = self._access_token try: response = self.session.get(self.USER_INFO_URL, params={'access_token': access_token}, timeout=self._timeout, proxies=self.proxies) except requests.HTTPError as e: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message=e.args)) data = self._parse_response(response) if return_json: return data else: return UserProfile.new_from_json_dict(data)
def exchange_code_to_access_token(self, authorization_response, redirect_uri=None, return_json=False): """ Use the google auth response to get access token Args: authorization_response (str) The response url for you give auth permission. redirect_uri (str, optional) The redirect url you have point when do authorization step. If you not provide will use default uri: http://127.0.0.1 return_json (bool, optional): If True JSON data will be returned, instead of pyyoutube.AccessToken Return: Retrieved access token's info, pyyoutube.AccessToken instance. """ query = urlparse(authorization_response).query params = dict(parse_qsl(query)) if 'code' not in params: raise PyYouTubeException( ErrorMessage( status_code=10002, message="Missing code parameter in authorization response." )) if params.get('state', None) != self.DEFAULT_STATE: raise PyYouTubeException( ErrorMessage( status_code=10002, message="Missing state parameter in authorization response." )) if redirect_uri is None: redirect_uri = self.DEFAULT_REDIRECT_URI kwargs = { 'code': params['code'], 'client_id': self._client_id, 'client_secret': self._client_secret, 'redirect_uri': redirect_uri, 'grant_type': 'authorization_code' } return self._fetch_token(kwargs, return_json)
def get_comments_by_parent(self, parent_id=None, limit=20, count=20, return_json=None): """ Retrieve data from YouTube Data Api for top level comment which you point. Refer: https://developers.google.com/youtube/v3/docs/comments/list Args: parent_id (str, optional) Provide the ID of the comment for which replies should be retrieved. Now YouTube currently supports replies only for top-level comments limit (int, optional) Each request retrieve comments from data api. For comments, this should not be more than 100. Default is 20. count (int, optional) The count will retrieve comments data. Default is 20. return_json(bool, optional) The return data type. If you set True JSON data will be returned. False will return pyyoutube.Comment. Returns: The list data for you given comment. """ part = 'id,snippet' args = { 'part': part, 'maxResults': limit, } if parent_id is not None: args['parentId'] = parent_id else: raise PyYouTubeException( ErrorMessage(status_code=10007, message='Parent comment id must specified')) comments = [] next_page_token = None while True: _, next_page_token, data = self.paged_by_page_token( resource='comments', args=args, page_token=next_page_token, ) items = self._parse_data(data) if return_json: comments += items else: comments += [ Comment.new_from_json_dict(item) for item in items ] if len(comments) >= count: break if next_page_token is None: break return comments[:count]
def __init__( self, client_id=None, client_secret=None, api_key=None, access_token=None, timeout=None, proxies=None, quota=None, ): """ This Api provide two method to work. Use api key or use access token. Args: client_id(str, optional) Your google app's ID. client_secret (str, optional) Your google app's secret. api_key(str, optional) The api key which you create from google api console. access_token(str, optional) If you not provide api key, you can do authorization to get an access token. If all api key and access token provided. Use access token first. timeout(int, optional) The request timeout. proxies(dict, optional) If you want use proxy, need point this param. param style like requests lib style. quota(int, optional) if your key has more quota. you can point this. Default is 10000 Returns: YouTube Api instance. """ self._client_id = client_id self._client_secret = client_secret self._api_key = api_key self._access_token = access_token self._timeout = timeout self.session = requests.Session() self.scope = None self.proxies = proxies self.quota = quota if self.quota is None: self.quota = self.DEFAULT_QUOTA self.used_quota = 0 if not ((self._client_id and self._client_secret) or self._api_key or self._access_token): raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message='Must specify either client key info or api key.')) if self._timeout is None: self._timeout = self.DEFAULT_TIMEOUT
def testErrorMessage(self): response = ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message="error") ex = PyYouTubeException(response=response) self.assertEqual(ex.status_code, 10000) self.assertEqual(ex.message, "error") self.assertEqual(ex.error_type, "PyYouTubeException")
def enf_parts(resource: str, value: Optional[Union[str, list, tuple, set]], check=True): """ Check to see if value type belong to correct type, and if resource support the given part. If it is, return api need value, otherwise, raise a PyYouTubeException. Args: resource (str): Name of the resource you want to retrieve. value (str, list, tuple, set, Optional): Value for the part. check (bool, optional): Whether check the resource properties. Returns: Api needed part string """ if value is None: parts = RESOURCE_PARTS_MAPPING[resource] elif isinstance(value, str): parts = set(value.split(",")) elif isinstance(value, (list, tuple, set)): parts = set(value) else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message= f"Parameter (parts) must be single str,comma-separated str,list,tuple or set", )) # check parts whether support. if check: support_parts = RESOURCE_PARTS_MAPPING[resource] if not support_parts.issuperset(parts): not_support_parts = ",".join(parts.difference(support_parts)) raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message= f"Parts {not_support_parts} for resource {resource} not support", )) return ",".join(parts)
def get_video_seconds_duration(self): if not self.duration: return None try: seconds = isodate.parse_duration(self.duration).total_seconds() except ISO8601Error as e: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.INVALID_PARAMS, message=e.args[0])) else: return int(seconds)
def enf_comma_separated( field: str, value: Optional[Union[str, list, tuple, set]], ): """ Check to see if field's value type belong to correct type. If it is, return api need value, otherwise, raise a PyYouTubeException. Args: field (str): Name of the field you want to do check. value (str, list, tuple, set, Optional) Value for the field. Returns: Api needed string """ if value is None: return None try: if isinstance(value, str): return value elif isinstance(value, (list, tuple, set)): if isinstance(value, set): logging.warning(f"Note: The order of the set is unreliable.") return ",".join(value) else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message= f"Parameter ({field}) must be single str,comma-separated str,list,tuple or set", )) except (TypeError, ValueError): raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message= f"Parameter ({field}) must be single str,comma-separated str,list,tuple or set", ))
def get_profile(self, return_json=False): """ """ if self._access_token is None: raise PyYouTubeException( ErrorMessage(status_code=10005, message='Get profile Must need access token.')) try: response = self.session.get( 'https://www.googleapis.com/oauth2/v1/userinfo', params={'access_token': self._access_token}, timeout=self._timeout, proxies=self.proxies) except requests.HTTPError as e: raise PyYouTubeException( ErrorMessage(status_code=10000, message=e.read())) data = self._parse_response(response) if return_json: return data else: return UserProfile.new_from_json_dict(data)
def testResponseError(self) -> None: response = Response() response.status_code = 400 response._content = self.ERROR_DATA ex = PyYouTubeException(response=response) self.assertEqual(ex.status_code, 400) self.assertEqual(ex.message, "Bad Request") self.assertEqual(ex.error_type, "YouTubeException") error_msg = "YouTubeException(status_code=400,message=Bad Request)" self.assertEqual(repr(ex), error_msg) self.assertTrue(str(ex), error_msg)
def _parse_response(self, response, api=False): """ Parse response data and check whether errors exists. Args: response (Response) The response which the request return. Return: response's data """ data = response.json() if 'error' in data: raise PyYouTubeException(response) if api: return self._parse_data(data) return data
def _parse_data(data): """ Parse resp data Args: data (dict) The response data by response.json() Return: response's items """ items = data['items'] if isinstance(items, dict) or len(items) == 0: raise PyYouTubeException( ErrorMessage(status_code=10002, message='Response data not have items.')) else: return items
def string_to_datetime( dt_str: Optional[str]) -> Optional[datetime.datetime]: """ Convert datetime string to datetime instance. original string format is YYYY-MM-DDThh:mm:ss.sZ. :return: """ if not dt_str: return None try: r = isodate.parse_datetime(dt_str) except ISO8601Error as e: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.INVALID_PARAMS, message=e.args[0])) else: return r
def get_channel_info(self, channel_id=None, channel_name=None, return_json=False): """ Retrieve data from YouTube Data API for channel which you given. Args: channel_id (str, optional) The id for youtube channel. Id always likes: UCLA_DiR1FfKNvjuUpBHmylQ channel_name (str, optional) The name for youtube channel. If id and name all given, will use id first. return_json(bool, optional) The return data type. If you set True JSON data will be returned. False will return pyyoutube.Channel Returns: The data for you given channel. """ if channel_name is not None: args = { 'forUsername': channel_name, 'part': 'id,snippet,contentDetails,statistics' } elif channel_id is not None: args = { 'id': channel_id, 'part': 'id,snippet,contentDetails,statistics,status' } else: raise PyYouTubeException( ErrorMessage( status_code=10005, message='Specify at least one of channel id or username')) resp = self._request(resource='channels', method='GET', args=args) data = self._parse_response(resp, api=True) self.calc_quota(resource='channels', parts=args['part']) if return_json: return data else: return Channel.new_from_json_dict(data[0])
def get_authorization_url(self, redirect_uri=None, scope=None, **kwargs): """ Build authorization url to do authorize. Args: redirect_uri(str, optional) The uri you have set on your google app authorized uri. if you not provide, will use default uri: 'http://127.0.0.1' Must this uri in you app's authorized uri list. scope (list, optional) The scope you want give permission. If you not provide, will use default scope. kwargs(dict, optional) Some other params you want provide. Returns: The uri you can open on browser to do authorize. """ if redirect_uri is None: redirect_uri = self.DEFAULT_REDIRECT_URI self.scope = scope if self.scope is None: self.scope = self.DEFAULT_SCOPE try: scope = ' '.join(self.scope) except TypeError: raise PyYouTubeException( ErrorMessage(status_code=10001, message='scope need a list type.')) authorization_kwargs = { 'client_id': self._client_id, 'redirect_uri': redirect_uri, 'scope': scope, 'access_type': 'offline', 'response_type': 'code', 'state': self.DEFAULT_STATE, } if kwargs: authorization_kwargs.update(kwargs) return self.AUTHORIZATION_URL + '?' + urlencode(authorization_kwargs)
def get_video_duration(duration: str) -> int: """ Parse video ISO 8601 duration to seconds. Refer: https://developers.google.com/youtube/v3/docs/videos#contentDetails.duration Args: duration(str) Videos ISO 8601 duration. Like: PT14H23M42S Returns: integer for seconds. """ try: seconds = isodate.parse_duration(duration).total_seconds() return int(seconds) except ISO8601Error as e: raise PyYouTubeException( ErrorMessage( status_code=10001, message= f'Exception in convert video duration: {duration}. errors: {e}' ))
def comma_separated_validator(**kwargs): """ Validate the param layout whether comma-separated string. Args: kwargs (str) Parameter need to do validate. Returns: None """ for name, param in kwargs.items(): if param is not None: try: param.split(',') except AttributeError: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message= f'Parameter {name} must be str or comma-separated list str' ))
def parts_validator(resource: str, parts: str): """ Validate the resource whether support the parts. Args: resource (str) The YouTube resource string. parts (str) Parts need to do validate. Returns: True or False """ if parts is not None: support_parts = RESOURCE_PARTS_MAPPING[resource] parts = set(parts.split(',')) if not support_parts.issuperset(parts): not_support_parts = ','.join(parts.difference(support_parts)) raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message= f'Part {not_support_parts} for resource {resource} not support' ))
def _fetch_token(self, params, return_json=False): """ Use the google auth response to get access token Args: params (dict) The params to get access token. return_json (bool, optional): If True JSON data will be returned, instead of pyyoutube.AccessToken Return: Retrieved access token's info, pyyoutube.AccessToken instance. """ headers = {'Content-Type': 'application/x-www-form-urlencoded'} try: response = self.session.post(self.EXCHANGE_ACCESS_TOKEN_URL, data=params, headers=headers, timeout=self._timeout, proxies=self.proxies) except requests.HTTPError as e: raise PyYouTubeException( ErrorMessage(status_code=10000, message=e.read())) data = self._parse_response(response) access_token = data['access_token'] self._access_token = access_token # once get the refresh token. This token can be use long time. # refer: https://developers.google.com/identity/protocols/OAuth2 refresh_token = data.get('refresh_token') if refresh_token is not None: self._refresh_token = refresh_token if return_json: return data else: return AccessToken.new_from_json_dict(data)
def _request(self, resource, method=None, args=None, post_args=None, enforce_auth=True): """ Main request sender. Args: resource(str) Resource field is which type data you want to retrieve. Such as channels,videos and so on. method(str, optional) The method this request to send request. Default is 'GET' args(dict, optional) The url params for this request. post_args(dict, optional) The Post params for this request. enforce_auth(bool, optional) Whether use google credentials Returns: response """ if method is None: method = 'GET' if args is None: args = dict() if post_args is not None: method = 'POST' key = None access_token = None if self._api_key is not None: key = 'key' access_token = self._api_key if self._access_token is not None: key = 'access_token' access_token = self._access_token if access_token is None and enforce_auth: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.MISSING_PARAMS, message='You must provide your credentials.')) if enforce_auth: if method == 'POST' and key not in post_args: post_args[key] = access_token elif method == 'GET' and key not in args: args[key] = access_token try: response = self.session.request(method=method, url=self.BASE_URL + resource, timeout=self._timeout, params=args, data=post_args, proxies=self.proxies) except requests.HTTPError as e: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message=e.args)) else: return response
def get_playlist_item(self, playlist_id=None, playlist_item_id=None, summary=True, count=5, limit=5, return_json=False): """ Retrieve channel playlistItems info. Provide two methods: by playlist ID, or by playlistItem id (ids) Args: playlist_id (str, optional) If provide channel id, this will return pointed playlist's item info. playlist_item_id (str,list optional) If provide this. will return those playlistItem's info. summary (bool, optional) If True will return playlist item summary of metadata. Notice this depend on your query. count (int, optional) The count will retrieve playlist items data. Default is 5. limit (int, optional) Each request retrieve playlistItems from data api. For playlistItem, this should not be more than 50. Default is 5 return_json(bool, optional) The return data type. If you set True JSON data will be returned. False will return pyyoutube.PlayListItem Returns: return tuple. (playlistItem data, playlistItem summary) """ part = 'id,snippet,contentDetails,status' args = {'part': part, 'maxResults': limit} if playlist_id is not None: args['playlistId'] = playlist_id elif playlist_item_id is not None: if isinstance(playlist_item_id, str): p_id = playlist_item_id elif isinstance(playlist_item_id, (list, tuple)): p_id = ','.join(playlist_item_id) else: raise PyYouTubeException( ErrorMessage( status_code=10007, message='PlaylistItem must be single id or id list.')) args['id'] = p_id else: raise PyYouTubeException( ErrorMessage( status_code=10005, message= 'Specify at least one of channel id or playlist id(id list)' )) playlist_items = [] playlist_items_summary = None next_page_token = None while True: prev_page_token, next_page_token, data = self.paged_by_page_token( resource='playlistItems', args=args, page_token=next_page_token, ) items = self._parse_data(data) if return_json: playlist_items += items else: playlist_items += [ PlaylistItem.new_from_json_dict(item) for item in items ] if summary: playlist_items_summary = data.get('pageInfo', {}) if next_page_token is None: break if len(playlist_items) >= count: break return playlist_items[:count], playlist_items_summary
def get_comment_threads(self, all_to_channel_id=None, channel_id=None, video_id=None, order='time', limit=20, count=20, return_json=False): """ Retrieve the comment thread info by single id. Refer: https://developers.google.com/youtube/v3/docs/commentThreads/list Args: all_to_channel_id (str, optional) If you provide channel id by this parameter. Will return all comment threads associated with the specified channel. The response can include comments about the channel or about the channel's videos. channel_id (str, optional) If you provide channel id by this parameter. Will return comment threads containing comments about the specified channel. But not include comments about the channel's videos. video_id (str, optional) If you provide video id by this parameter. Will return comment threads containing comments about the specified video. order (str, optional) Provide the response order type. Valid value are: time, relevance. Default is time. order by the commented time. limit (int, optional) Each request retrieve comment threads from data api. For comment threads, this should not be more than 100. Default is 20. count (int, optional) The count will retrieve comment threads data. Default is 20. return_json(bool, optional) The return data type. If you set True JSON data will be returned. False will return pyyoutube.CommentThread. Returns: The list data for you given comment thread. """ part = 'id,snippet,replies' args = {'part': part, 'maxResults': limit} if all_to_channel_id is not None: args['allThreadsRelatedToChannelId'] = all_to_channel_id elif channel_id is not None: args['channelId'] = channel_id elif video_id is not None: args['videoId'] = video_id else: raise PyYouTubeException( ErrorMessage( status_code=10007, message= 'Target id must specify. either of all_to_channel_id, channel_id,video_id' )) if order not in ['time', 'relevance']: raise PyYouTubeException( ErrorMessage(status_code=10007, message='Order type must be time or relevance.')) comment_threads = [] next_page_token = None while True: _, next_page_token, data = self.paged_by_page_token( resource='commentThreads', args=args, page_token=next_page_token, ) items = self._parse_data(data) if return_json: comment_threads += items else: comment_threads += [ CommentThread.new_from_json_dict(item) for item in items ] if next_page_token is None: break if len(comment_threads) >= count: break return comment_threads[:count]
def get_comment_threads(self, all_to_channel_id=None, channel_id=None, video_id=None, parts=None, order='time', search_term=None, limit=20, count=20, return_json=False): """ Retrieve the comment thread info by single id. Refer: https://developers.google.com/youtube/v3/docs/commentThreads/list Args: all_to_channel_id (str, optional) If you provide channel id by this parameter. Will return all comment threads associated with the specified channel. The response can include comments about the channel or about the channel's videos. channel_id (str, optional) If you provide channel id by this parameter. Will return comment threads containing comments about the specified channel. But not include comments about the channel's videos. video_id (str, optional) If you provide video id by this parameter. Will return comment threads containing comments about the specified video. parts (str, optional) Comma-separated list of one or more commentThreads resource properties. If not provided. will use default public properties. order (str, optional) Provide the response order type. Valid value are: time, relevance. Default is time. order by the commented time. search_term (str, optional) If you provide this. Only return the comments that contain the search terms. limit (int, optional) Each request retrieve comment threads from data api. For comment threads, this should not be more than 100. Default is 20. count (int, optional) The count will retrieve comment threads data. Default is 20. return_json(bool, optional) The return data type. If you set True JSON data will be returned. False will return pyyoutube.CommentThread. Returns: The list data for you given comment thread. """ comma_separated_validator(parts=parts) incompatible_validator(all_to_channel_id=all_to_channel_id, channel_id=channel_id, video_id=video_id) if parts is None: parts = constants.COMMENT_THREAD_RESOURCE_PROPERTIES parts = ','.join(parts) else: parts_validator('commentThreads', parts=parts) args = {'part': parts, 'maxResults': limit} if all_to_channel_id is not None: args['allThreadsRelatedToChannelId'] = all_to_channel_id elif channel_id is not None: args['channelId'] = channel_id elif video_id is not None: args['videoId'] = video_id if order not in ['time', 'relevance']: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.INVALID_PARAMS, message='Order type must be time or relevance.')) if search_term is not None: args['searchTerms'] = search_term comment_threads = [] next_page_token = None while True: _, next_page_token, data = self.paged_by_page_token( resource='commentThreads', args=args, page_token=next_page_token, ) items = self._parse_data(data) if return_json: comment_threads += items else: comment_threads += [ CommentThread.new_from_json_dict(item) for item in items ] if next_page_token is None: break if len(comment_threads) >= count: break return comment_threads[:count]