diff --git a/common/common/googleapis.py b/common/common/googleapis.py new file mode 100644 index 0000000..b2b898d --- /dev/null +++ b/common/common/googleapis.py @@ -0,0 +1,62 @@ + +import time + +import gevent +import requests + + +class GoogleAPIClient(object): + """Manages access to google apis and maintains an active access token. + Make calls using client.request(), which is a wrapper for requests.request(). + """ + + ACCESS_TOKEN_ERROR_RETRY_INTERVAL = 10 + # Refresh token 10min before it expires (it normally lasts an hour) + ACCESS_TOKEN_REFRESH_TIME_BEFORE_EXPIRY = 600 + + def __init__(self, client_id, client_secret, refresh_token): + self.client_id = client_id + self.client_secret = client_secret + self.refresh_token = refresh_token + + self._first_get_access_token = gevent.spawn(self.get_access_token) + + @property + def access_token(self): + """Blocks if access token unavailable yet""" + self._first_get_access_token.join() + return self._access_token + + def get_access_token(self): + """Authenticates against google's API and retrieves a token we will use in + subsequent requests. + This function gets called automatically when needed, there should be no need to call it + yourself.""" + while True: + try: + start_time = time.time() + resp = requests.post('https://www.googleapis.com/oauth2/v4/token', data={ + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'refresh_token': self.refresh_token, + 'grant_type': 'refresh_token', + }) + resp.raise_for_status() + data = resp.json() + self._access_token = data['access_token'] + expires_in = (start_time + data['expires_in']) - time.time() + if expires_in < self.ACCESS_TOKEN_REFRESH_TIME_BEFORE_EXPIRY: + self.logger.warning("Access token expires in {}s, less than normal leeway time of {}s".format( + expires_in, self.ACCESS_TOKEN_REFRESH_TIME_BEFORE_EXPIRY, + )) + gevent.spawn_later(expires_in - self.ACCESS_TOKEN_REFRESH_TIME_BEFORE_EXPIRY, self.get_access_token) + except Exception: + self.logger.exception("Failed to fetch access token, retrying") + self.wait(self.ACCESS_TOKEN_ERROR_RETRY_INTERVAL) + else: + break + + def request(self, method, url, headers={}, **kwargs): + # merge in auth header + headers = dict(headers, Authorization='Bearer {}'.format(self.access_token)) + return requests.request(method, url, headers=headers, **kwargs) diff --git a/cutter/cutter/youtube.py b/cutter/cutter/youtube.py index 278822c..2b2a23e 100644 --- a/cutter/cutter/youtube.py +++ b/cutter/cutter/youtube.py @@ -1,63 +1,15 @@ import logging -import time -import gevent -import requests +from common.googleapis import GoogleAPIClient class Youtube(object): - """Manages access to youtube and maintains an active access token""" - - ACCESS_TOKEN_ERROR_RETRY_INTERVAL = 10 - # Refresh token 10min before it expires (it normally lasts an hour) - ACCESS_TOKEN_REFRESH_TIME_BEFORE_EXPIRY = 600 + """Manages youtube API operations""" def __init__(self, client_id, client_secret, refresh_token): self.logger = logging.getLogger(type(self).__name__) - self.client_id = client_id - self.client_secret = client_secret - self.refresh_token = refresh_token - - self._first_get_access_token = gevent.spawn(self.get_access_token) - - @property - def access_token(self): - """Blocks if access token unavailable yet""" - self._first_get_access_token.join() - return self._access_token - - def get_access_token(self): - """Authenticates against the youtube API and retrieves a token we will use in - subsequent requests. - This function gets called automatically when needed, there should be no need to call it - yourself.""" - while True: - try: - start_time = time.time() - resp = requests.post('https://www.googleapis.com/oauth2/v4/token', data={ - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'refresh_token': self.refresh_token, - 'grant_type': 'refresh_token', - }) - resp.raise_for_status() - data = resp.json() - self._access_token = data['access_token'] - expires_in = (start_time + data['expires_in']) - time.time() - if expires_in < self.ACCESS_TOKEN_REFRESH_TIME_BEFORE_EXPIRY: - self.logger.warning("Access token expires in {}s, less than normal leeway time of {}s".format( - expires_in, self.ACCESS_TOKEN_REFRESH_TIME_BEFORE_EXPIRY, - )) - gevent.spawn_later(expires_in - self.ACCESS_TOKEN_REFRESH_TIME_BEFORE_EXPIRY, self.get_access_token) - except Exception: - self.logger.exception("Failed to fetch access token, retrying") - self.wait(self.ACCESS_TOKEN_ERROR_RETRY_INTERVAL) - else: - break - - def auth_headers(self): - return {'Authorization': 'Bearer {}'.format(self.access_token)} + self.client = GoogleAPIClient(client_id, client_secret, refresh_token) def upload_video(self, title, description, tags, data, hidden=False): """Data may be a string, file-like object or iterator. Returns id.""" @@ -72,7 +24,7 @@ class Youtube(object): json['status'] = { 'privacyStatus': 'unlisted', } - resp = requests.post( + resp = self.client.request('POST', 'https://www.googleapis.com/upload/youtube/v3/videos', headers=self.auth_headers(), params={ @@ -83,7 +35,7 @@ class Youtube(object): ) resp.raise_for_status() upload_url = resp.headers['Location'] - resp = requests.post(upload_url, headers=self.auth_headers(), data=data) + resp = self.client.request('POST', upload_url, headers=self.auth_headers(), data=data) resp.raise_for_status() return resp.json()['id'] @@ -97,7 +49,7 @@ class Youtube(object): # Break up into groups of 10 videos. I'm not sure what the limit is so this is reasonable. for i in range(0, len(ids), 10): group = ids[i:i+10] - resp = requests.get( + resp = self.client.request('GET', 'https://www.googleapis.com/youtube/v3/videos', headers=self.auth_headers(), params={