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