YoutubeAuth: use HBBTV_USER_AGENT
[enigma2-plugins.git] / tubelib / src / youtube / YoutubeAuth.py
1 from enigma import HBBTV_USER_AGENT
2
3 from oauth2client.client import OAuth2Credentials
4 from time import time
5 from twisted.internet import reactor
6 from twisted.internet.defer import succeed
7 from twisted.internet.ssl import ClientContextFactory
8 from twisted.web.client import Agent, readBody, PartialDownloadError
9 from twisted.web.http_headers import Headers
10 from twisted.web.iweb import IBodyProducer
11 from zope.interface import implements
12
13 import json
14
15 class StringProducer(object):
16         implements(IBodyProducer)
17
18         def __init__(self, body):
19                 self.body = body
20                 self.length = len(body)
21
22         def startProducing(self, consumer):
23                 consumer.write(self.body)
24                 return succeed(None)
25
26         def pauseProducing(self):
27                 pass
28
29         def stopProducing(self):
30                 pass
31
32 class GoogleUserCode(object):
33         def __init__(self, data):
34                 self._data = data
35                 self.device_code = self._data['device_code']
36                 self.interval = int(self._data['interval'])
37                 self.user_code = self._data['user_code']
38                 self.verification_url = data['verification_url']
39                 self._expires_at = time() + int(self._data['expires_in'])
40
41         def expired(self):
42                 print "Expires in %s" % (self._expires_at - time())
43                 return time() >= self._expires_at
44
45         def __str__(self):
46                 return "[GoogleUserCode] %s" % (self._data,)
47
48 class WebClientContextFactory(ClientContextFactory):
49         def getContext(self, hostname, port):
50                 return ClientContextFactory.getContext(self)
51
52 class YoutubeAuth(object):
53         AUTH_SCOPE_YT = "https://www.googleapis.com/auth/youtube"
54         AUTH_SCOPE_YT_RO = "https://www.googleapis.com/auth/youtube.readonly"
55         AUTH_SCOPE_YT_FORCE_SSL = "https://www.googleapis.com/auth/youtube.force-ssl"
56
57         AUTH_REQUEST_URI = "https://accounts.google.com/o/oauth2/device/code"
58         AUTH_RESPONSE_URI = "https://accounts.google.com/o/oauth2/token"
59         GRANT_TYPE_DEVICE_AUTH = "http://oauth.net/grant_type/device/1.0"
60         GRANT_TYPE_REFRESH_CREDENTIALS = "refresh_token"
61
62         CLIENT_ID = "59698529433-7n49jvm74m8mfkpjp2i1s2l18vsj35fu.apps.googleusercontent.com"
63         CLIENT_SECRET = "QaAAkmuTmYDo35vZt8aQ093N"
64
65         ERROR_AUTH_REQUEST = 0
66         ERROR_AUTH_REQUEST_PARSE = 1
67         ERROR_CREDENTIALS_REQUEST = 2
68         ERROR_CREDENTIALS_REQUEST_PARSE = 3
69         ERROR_CREDENTIALS_REQUEST_EXPIRED = 4
70         ERROR_CREDENTIALS_UNKOWN = 5
71
72         USER_AGENT = HBBTV_USER_AGENT
73
74         def __init__(self, scope=AUTH_SCOPE_YT_RO):
75                 self._auth_scope = scope
76                 self._agent = Agent(reactor, WebClientContextFactory())
77                 self.onUserCodeReady = []
78                 self.onCredentialsReady = []
79                 self.onError = []
80                 self._user_code = None
81                 self._requestDeferred = None
82                 self._responseDeferred = None
83
84         def cleanup(self):
85                 if self._requestDeferred:
86                         self._requestDeferred.cancel()
87                 if self._responseDeferred:
88                         self._responseDeferred.cancel()
89
90         def startAuthFlow(self):
91                 d = self._agent.request(
92                         'POST',
93                         self.AUTH_REQUEST_URI,
94                         Headers({
95                                 'User-Agent' : [self.USER_AGENT],
96                                 'Content-Type' : ["application/x-www-form-urlencoded"],
97                         }),
98                         StringProducer("client_id=%s&scope=%s" % (self.CLIENT_ID, self._auth_scope))
99                 )
100                 d.addCallbacks(self._onRequestResponse, self._onRequestError)
101                 self._requestDeferred = d
102                 return d;
103
104         def _onError(self, type, errorText=""):
105                 print "ERROR! %s, %s" % (type, errorText)
106                 self._requestDeferred = None
107                 for fnc in self.onError:
108                         fnc(type, errorText)
109
110         def _onRequestResponse(self, response):
111                 self._requestDeferred = None
112                 readBody(response).addCallback(self._onRequestBodyReady).addErrback(self._onRequestBodyError)
113
114         def _onRequestError(self, error):
115                 self._requestDeferred = None
116                 self._onError(self.ERROR_AUTH_REQUEST, str(error))
117
118         def _onRequestBodyReady(self, body):
119                 self._parseAuthRequestResponse(body)
120                 self._pollForResult()
121
122         def _onRequestBodyError(self, failure):
123                 if isinstance(failure.value, PartialDownloadError):
124                         self._onRequestBodyReady(failure.value.response)
125                 else:
126                         self._onError(self.ERROR_AUTH_REQUEST, str(failure))
127
128         def _parseAuthRequestResponse(self, body):
129                 response = json.loads(body)
130                 self._user_code = GoogleUserCode(response)
131                 for fnc in self.onUserCodeReady:
132                         fnc(self._user_code)
133
134         def _pollForResult(self):
135                 if self._user_code.expired():
136                         self._onError(self.ERROR_CREDENTIALS_REQUEST_EXPIRED)
137                         return
138                 d = self._agent.request(
139                         'POST',
140                         self.AUTH_RESPONSE_URI,
141                         Headers({
142                                 'User-Agent' : [self.USER_AGENT],
143                                 'Content-Type' : ["application/x-www-form-urlencoded"],
144                         }),
145                         StringProducer("client_id=%s&client_secret=%s&code=%s&grant_type=%s" % (self.CLIENT_ID, self.CLIENT_SECRET, str(self._user_code.device_code), self.GRANT_TYPE_DEVICE_AUTH))
146                 )
147                 d.addCallbacks(self._onCredentialsPollResponse, self._onCredentialsPollError)
148                 self._responseDeferred = d
149                 return d;
150
151         def _onCredentialsPollResponse(self, response):
152                 self._responseDeferred = None
153                 readBody(response).addCallback(self._onCredentialsPollBodyReady).addErrback(self._onCredentialsPollBodyError)
154
155         def _onCredentialsPollError(self, error):
156                 self._responseDeferred = None
157                 self._onError(self.ERROR_CREDENTIALS_REQUEST, str(error))
158
159         def _onCredentialsPollBodyReady(self, body):
160                 print body
161                 result = json.loads(body)
162                 error = result.get("error", None)
163                 if error:
164                         if error == "authorization_pending":
165                                 print "not ready, yet"
166                                 reactor.callLater(self._user_code.interval, self._pollForResult)
167                         elif error == "slow_down":
168                                 print "too fast, slowing down"
169                                 self._device_code.interval = self._device_code.interval * 2
170                         elif error == "expired_token":
171                                 self._onError(self.ERROR_CREDENTIALS_REQUEST_EXPIRED)
172                         else:
173                                 print result
174                                 self._onError(self.ERROR_CREDENTIALS_REQUEST_PARSE, error)
175                 elif "access_token" in result:
176                         access_token = result.get("access_token")
177                         refresh_token = result.get("refresh_token")
178                         token_expiry = str( int(time()) + int(result.get("expires_in")) )
179                         self._credentials = OAuth2Credentials(access_token, self.CLIENT_ID, self.CLIENT_SECRET, refresh_token, token_expiry, self.AUTH_REQUEST_URI, self.USER_AGENT)
180                         for fnc in self.onCredentialsReady:
181                                 fnc(self._credentials)
182                 else:
183                         self._onError(self.ERROR_CREDENTIALS_REQUEST_PARSE, error)
184
185         def _onCredentialsPollBodyError(self, failure):
186                 if isinstance(failure.value, PartialDownloadError):
187                         self._onCredentialsPollBodyReady(failure.value.response)
188                 else:
189                         self._onError(self.ERROR_CREDENTIALS_REQUEST_PARSE, str(failure))
190
191 if __name__ == "__main__":
192         def userCodeReady(userCode):
193                 print "%s => %s" % (userCode.verification_url, userCode.user_code)
194
195         def credentialsReady(credentials):
196                 print "CREDENTIALS READY: %s" % (credentials.to_json(),)
197
198         yta = YoutubeAuth()
199         yta.onUserCodeReady.append(userCodeReady)
200         yta.onCredentialsReady.append(credentialsReady)
201         yta.startAuthFlow()
202         reactor.run()