# -*- coding: utf-8 -*- """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) Copyright (C) 2016-2018 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. """ from six.moves import urllib import time import requests from ...youtube.youtube_exceptions import InvalidGrant, LoginException from ...kodion import Context from .__config__ import api, youtube_tv, developer_keys, keys_changed context = Context(plugin_id='plugin.video.youtube') class LoginClient(object): api_keys_changed = keys_changed CONFIGS = { 'youtube-tv': { 'system': 'YouTube TV', 'key': youtube_tv['key'], 'id': youtube_tv['id'], 'secret': youtube_tv['secret'] }, 'main': { 'system': 'All', 'key': api['key'], 'id': api['id'], 'secret': api['secret'] }, 'developer': developer_keys } def __init__(self, config=None, language='en-US', region='', access_token='', access_token_tv=''): self._config = self.CONFIGS['main'] if config is None else config self._config_tv = self.CONFIGS['youtube-tv'] self._verify = context.get_settings().verify_ssl() # the default language is always en_US (like YouTube on the WEB) if not language: language = 'en_US' language = language.replace('-', '_') self._language = language self._region = region self._access_token = access_token self._access_token_tv = access_token_tv self._log_error_callback = None def set_log_error(self, callback): self._log_error_callback = callback def log_error(self, text): if self._log_error_callback: self._log_error_callback(text) else: print(text) def verify(self): return self._verify def set_access_token(self, access_token=''): self._access_token = access_token def set_access_token_tv(self, access_token_tv=''): self._access_token_tv = access_token_tv def revoke(self, refresh_token): # https://developers.google.com/youtube/v3/guides/auth/devices headers = {'Host': 'accounts.google.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} post_data = {'token': refresh_token} # url url = 'https://accounts.google.com/o/oauth2/revoke' result = requests.post(url, data=post_data, headers=headers, verify=self._verify) try: json_data = result.json() if 'error' in json_data: context.log_error('Revoke failed: Code: |%s| JSON: |%s|' % (str(result.status_code), json_data)) json_data.update({'code': str(result.status_code)}) raise LoginException(json_data) except ValueError: json_data = None if result.status_code != requests.codes.ok: response_dump = self._get_response_dump(result, json_data) context.log_error('Revoke failed: Code: |%s| Response dump: |%s|' % (str(result.status_code), response_dump)) raise LoginException('Logout Failed') def refresh_token_tv(self, refresh_token): client_id = str(self.CONFIGS['youtube-tv']['id']) client_secret = str(self.CONFIGS['youtube-tv']['secret']) return self.refresh_token(refresh_token, client_id=client_id, client_secret=client_secret) def refresh_token(self, refresh_token, client_id='', client_secret=''): # https://developers.google.com/youtube/v3/guides/auth/devices headers = {'Host': 'www.googleapis.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} client_id = client_id or self._config['id'] client_secret = client_secret or self._config['secret'] post_data = {'client_id': client_id, 'client_secret': client_secret, 'refresh_token': refresh_token, 'grant_type': 'refresh_token'} # url url = 'https://www.googleapis.com/oauth2/v4/token' config_type = self._get_config_type(client_id, client_secret) context.log_debug('Refresh token: Config: |%s| Client id [:5]: |%s| Client secret [:5]: |%s|' % (config_type, client_id[:5], client_secret[:5])) result = requests.post(url, data=post_data, headers=headers, verify=self._verify) try: json_data = result.json() if 'error' in json_data: context.log_error('Refresh Failed: Code: |%s| JSON: |%s|' % (str(result.status_code), json_data)) json_data.update({'code': str(result.status_code)}) if json_data['error'] == 'invalid_grant' and json_data['code'] == '400': raise InvalidGrant(json_data) raise LoginException(json_data) except ValueError: json_data = None if result.status_code != requests.codes.ok: response_dump = self._get_response_dump(result, json_data) context.log_error('Refresh failed: Config: |%s| Client id [:5]: |%s| Client secret [:5]: |%s| Code: |%s| Response dump |%s|' % (config_type, client_id[:5], client_secret[:5], str(result.status_code), response_dump)) raise LoginException('Login Failed') if result.headers.get('content-type', '').startswith('application/json'): if not json_data: json_data = result.json() access_token = json_data['access_token'] expires_in = time.time() + int(json_data.get('expires_in', 3600)) return access_token, expires_in return '', '' def request_access_token_tv(self, code, client_id='', client_secret=''): client_id = client_id or self.CONFIGS['youtube-tv']['id'] client_secret = client_secret or self.CONFIGS['youtube-tv']['secret'] return self.request_access_token(code, client_id=client_id, client_secret=client_secret) def request_access_token(self, code, client_id='', client_secret=''): # https://developers.google.com/youtube/v3/guides/auth/devices headers = {'Host': 'www.googleapis.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} client_id = client_id or self._config['id'] client_secret = client_secret or self._config['secret'] post_data = {'client_id': client_id, 'client_secret': client_secret, 'code': code, 'grant_type': 'http://oauth.net/grant_type/device/1.0'} # url url = 'https://www.googleapis.com/oauth2/v4/token' config_type = self._get_config_type(client_id, client_secret) context.log_debug('Requesting access token: Config: |%s| Client id [:5]: |%s| Client secret [:5]: |%s|' % (config_type, client_id[:5], client_secret[:5])) result = requests.post(url, data=post_data, headers=headers, verify=self._verify) authorization_pending = False try: json_data = result.json() if 'error' in json_data: if json_data['error'] != u'authorization_pending': context.log_error('Requesting access token: Code: |%s| JSON: |%s|' % (str(result.status_code), json_data)) json_data.update({'code': str(result.status_code)}) raise LoginException(json_data) else: authorization_pending = True except ValueError: json_data = None if (result.status_code != requests.codes.ok) and not authorization_pending: response_dump = self._get_response_dump(result, json_data) context.log_error('Requesting access token: Config: |%s| Client id [:5]: |%s| Client secret [:5]: |%s| Code: |%s| Response dump |%s|' % (config_type, client_id[:5], client_secret[:5], str(result.status_code), response_dump)) raise LoginException('Login Failed: Code %s' % str(result.status_code)) if result.headers.get('content-type', '').startswith('application/json'): if json_data: return json_data else: return result.json() else: response_dump = self._get_response_dump(result, json_data) context.log_error('Requesting access token: Config: |%s| Client id [:5]: |%s| Client secret [:5]: |%s| Code: |%s| Response dump |%s|' % (config_type, client_id[:5], client_secret[:5], str(result.status_code), response_dump)) raise LoginException('Login Failed: Unknown response') def request_device_and_user_code_tv(self): client_id = str(self.CONFIGS['youtube-tv']['id']) return self.request_device_and_user_code(client_id=client_id) def request_device_and_user_code(self, client_id=''): # https://developers.google.com/youtube/v3/guides/auth/devices headers = {'Host': 'accounts.google.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} client_id = client_id or self._config['id'] post_data = {'client_id': client_id, 'scope': 'https://www.googleapis.com/auth/youtube'} # url url = 'https://accounts.google.com/o/oauth2/device/code' config_type = self._get_config_type(client_id) context.log_debug('Requesting device and user code: Config: |%s| Client id [:5]: |%s|' % (config_type, client_id[:5])) result = requests.post(url, data=post_data, headers=headers, verify=self._verify) try: json_data = result.json() if 'error' in json_data: context.log_error('Requesting device and user code failed: Code: |%s| JSON: |%s|' % (str(result.status_code), json_data)) json_data.update({'code': str(result.status_code)}) raise LoginException(json_data) except ValueError: json_data = None if result.status_code != requests.codes.ok: response_dump = self._get_response_dump(result, json_data) context.log_error('Requesting device and user code failed: Config: |%s| Client id [:5]: |%s| Code: |%s| Response dump |%s|' % (config_type, client_id[:5], str(result.status_code), response_dump)) raise LoginException('Login Failed') if result.headers.get('content-type', '').startswith('application/json'): if json_data: return json_data else: return result.json() else: response_dump = self._get_response_dump(result, json_data) context.log_error('Requesting access token: Config: |%s| Client id [:5]: |%s| Code: |%s| Response dump |%s|' % (config_type, client_id[:5], str(result.status_code), response_dump)) raise LoginException('Login Failed: Unknown response') def get_access_token(self): return self._access_token def authenticate(self, username, password): headers = {'device': '38c6ee9a82b8b10a', 'app': 'com.google.android.youtube', 'User-Agent': 'GoogleAuth/1.4 (GT-I9100 KTU84Q)', 'content-type': 'application/x-www-form-urlencoded', 'Host': 'android.clients.google.com', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} post_data = {'device_country': self._region.lower(), 'operatorCountry': self._region.lower(), 'lang': self._language.replace('-', '_'), 'sdk_version': '19', # 'google_play_services_version': '6188034', 'accountType': 'HOSTED_OR_GOOGLE', 'Email': username.encode('utf-8'), 'service': 'oauth2:https://www.googleapis.com/auth/youtube ' 'https://www.googleapis.com/auth/youtube.force-ssl ' 'https://www.googleapis.com/auth/plus.me ' 'https://www.googleapis.com/auth/emeraldsea.mobileapps.doritos.cookie ' 'https://www.googleapis.com/auth/plus.stream.read ' 'https://www.googleapis.com/auth/plus.stream.write ' 'https://www.googleapis.com/auth/plus.pages.manage ' 'https://www.googleapis.com/auth/identity.plus.page.impersonation', 'source': 'android', 'androidId': '38c6ee9a82b8b10a', 'app': 'com.google.android.youtube', # 'client_sig': '24bb24c05e47e0aefa68a58a766179d9b613a600', 'callerPkg': 'com.google.android.youtube', # 'callerSig': '24bb24c05e47e0aefa68a58a766179d9b613a600', 'Passwd': password.encode('utf-8')} # url url = 'https://android.clients.google.com/auth' result = requests.post(url, data=post_data, headers=headers, verify=self._verify) if result.status_code != requests.codes.ok: raise LoginException('Login Failed') lines = result.text.replace('\n', '&') params = dict(urllib.parse.parse_qsl(lines)) token = params.get('Auth', '') expires = int(params.get('Expiry', -1)) if not token or expires == -1: raise LoginException('Failed to get token') return token, expires def _get_config_type(self, client_id, client_secret=None): """used for logging""" if client_secret is None: using_conf_tv = (client_id == self.CONFIGS['youtube-tv'].get('id')) using_conf_main = (client_id == self.CONFIGS['main'].get('id')) else: using_conf_tv = ((client_id == self.CONFIGS['youtube-tv'].get('id')) and (client_secret == self.CONFIGS['youtube-tv'].get('secret'))) using_conf_main = ((client_id == self.CONFIGS['main'].get('id')) and (client_secret == self.CONFIGS['main'].get('secret'))) if not using_conf_main and not using_conf_tv: return 'None' elif using_conf_tv: return 'YouTube-TV' elif using_conf_main: return 'YouTube-Kodi' else: return 'Unknown' @staticmethod def _get_response_dump(response, json_data=None): if json_data: return json_data else: try: return response.json() except ValueError: try: return response.text except: return 'None'