3232from urllib .parse import urlparse , parse_qs , parse_qsl , urlencode
3333from requests .adapters import HTTPAdapter
3434from urllib3 .util import Retry
35+ import secrets
36+ import base64
3537
3638CHANNEL_IDS = [20875 , 20876 , 192099 , 192100 , 20892 ]
3739CHANNEL_PRESET = {
3840 'DR1' : 1 ,
3941 'DR2' : 2 ,
4042 'DR Ramasjang' : 3 ,
41- 'DRTV ' : 4 ,
43+ 'TVA Live ' : 4 ,
4244 'DRTV Ekstra' : 5
4345}
4446URL = 'https://production.dr-massive.com/api'
47+ URL2 = 'https://prod95.dr-massive.com/api'
48+ CLIENT_ID = "283ba39a2cf31d3b81e922b8"
4549GET_TIMEOUT = 10
4650
4751
@@ -66,68 +70,106 @@ def fix_query(url, remove={}, add={}, remove_keys=[]):
6670 return o ._replace (query = urlencode (qs )).geturl ()
6771
6872
73+ def generate_code_verifier (length : int = 64 ) -> str :
74+ # Generate a secure random string (length between 43 and 128 chars)
75+ return secrets .token_urlsafe (length )[:length ]
76+
77+
78+ def generate_code_challenge (code_verifier : str ) -> str :
79+ # SHA256 hash of the verifier, then base64-url encode without padding
80+ sha256 = hashlib .sha256 (code_verifier .encode ()).digest ()
81+ return base64 .urlsafe_b64encode (sha256 ).decode ().rstrip ('=' )
82+
83+
6984def full_login (user , password ):
7085 ses = requests .Session ()
86+
7187 # start login flow
88+ code_verifier = generate_code_verifier ()
89+ code_challenge = generate_code_challenge (code_verifier )
90+
7291 params = {
73- 'clientRedirectUrl' :'https://www.dr.dk/drtv/callback' ,
74- 'signUp' : 'false' , 'localPath' :'/' , 'optout' :'false' , 'device' :'web_browser' }
75- headers = {
76- 'authority' : 'production.dr-massive.com' ,
77- 'User-Agent' : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0" ,
92+ "client_id" : CLIENT_ID ,
93+ "code_challenge" : code_challenge ,
94+ "code_challenge_method" : "S256" ,
95+ "redirect_uri" : "https://www.dr.dk/drtv/callback" ,
96+ "state" : f'{{"code_verifier":"{ code_verifier } ","logonRedirectPath":"/","optout":false}}' ,
97+ "response_type" : "code" ,
98+ "scope" : "openid roles tracking profile email offline_access"
7899 }
79- url = URL + '/authorization?'
80- res = ses .get (url , params = params , headers = headers , allow_redirects = True )
81-
100+ res = ses .get ('https://login.dr.dk/oidc/authorize' , params = params )
82101 if res .status_code != 200 :
83102 return {'status_code' : res .status_code , 'error' : res .text }
84- client_id = parse_qs (urlparse (res .history [1 ].url ).query )['client_id' ][0 ]
85- referer = res .history [2 ].headers ['Location' ]
86- state = parse_qs (urlparse (referer ).query )['state' ][0 ]
87-
88- # post credentials
89- headers = {
90- 'authority' : 'api.dr.dk' ,
91- 'User-Agent' : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0" ,
92- 'content-type' : 'application/json' ,
93- }
94-
95- url = 'https://api.dr.dk/login/graphql'
96- data = {"operationName" :"InitializeLoginTransaction" ,"variables" :{"input" :{"state" :state ,"clientID" :client_id }}, "query" :"mutation InitializeLoginTransaction($input: InitializeLoginTransactionInput!) {\n initializeLoginTransaction(input: $input) {\n id\n isValid\n isIdentified\n captcha {\n image\n __typename\n }\n proofOfWork {\n salt\n difficulty\n __typename\n }\n __typename\n }\n }" } # noqa: E501
97103
98- u = ses .post (url , data = json .dumps (data ), headers = headers )
99- print (1 , u .status_code )
100- if u .status_code != 200 :
101- return {'status_code' : u .status_code , 'error' : u .text }
102- transaction_id = u .json ()['data' ]['initializeLoginTransaction' ]['id' ]
103-
104- data = {"operationName" :"Identify" ,"variables" :{"input" :{"transaction" :transaction_id ,"email" :user }},"query" :"mutation Identify($input: IdentificationInput\u0021 ) {\n identify(input: $input) {\n id\n isValid\n isIdentified\n captcha {\n image\n __typename\n }\n proofOfWork {\n salt\n difficulty\n __typename\n }\n __typename\n }\n }" } # noqa: E501
105- u2 = ses .post (url , data = json .dumps (data ), headers = headers )
106- print (2 , u2 .status_code )
107- if u2 .status_code != 200 :
108- return {'status_code' : u2 .status_code , 'error' : u2 .text }
104+ trans = urlparse (res .url ).path .split ('/' )[- 1 ]
105+ headers = {'content-type' : 'application/json' }
106+
107+ transaction_fragment = "fragment useTransactionTransactionFragment on Transaction { ... on AuthenticatedAuthenticationTransaction { id email registration href __typename } ... on UnauthenticatedAuthenticationTransaction { id email __typename } ... on UnverifiedAuthenticationTransaction { id email name __typename } ... on UnrecognizedAuthenticationTransaction { id email statisticsConsentDefinition { id type version locale permissions headline summary body __typename } preferencesConsentDefinition { id type version locale permissions headline summary body __typename } newsletterConsentDefinition { id type version locale permissions headline summary body __typename } __typename } ... on UnidentifiedAuthenticationTransaction { id __typename } ... on CompletedEmailVerificationTransaction { id emailVerificationVariant: variant email __typename } ... on PendingEmailVerificationTransaction { id emailVerificationVariant: variant email __typename } ... on CompletedPasswordChangeTransaction { id passwordChangeVariant: variant __typename } ... on PendingPasswordChangeTransaction { id passwordChangeVariant: variant __typename } ... on PendingDeletionConfirmationTransaction { id __typename } ... on CompletedDeletionConfirmationTransaction { id __typename } ... on SettingsTransaction { id identity { id email name roles __typename } statisticsConsentDefinition { id type version locale permissions headline summary body __typename } preferencesConsentDefinition { id type version locale permissions headline summary body __typename } newsletterConsentDefinition { id type version locale permissions headline summary body __typename } statisticsConsentRevision { id status definition createdAt __typename } preferencesConsentRevision { id status definition createdAt __typename } newsletterConsentRevision { id status definition createdAt __typename } referBackUri referBackName sessionState expiresAt __typename } ... on PendingEUPTransaction { id href __typename } ... on CompletedEUPTransaction { id __typename } __typename }" # noqa: E501
108+ trans_query = "query useTransactionTransactionQuery($id: ID!) { transaction(id: $id) { ... on Node { id __typename } ...useTransactionTransactionFragment __typename } }" + transaction_fragment # noqa: E501
109+ identify_query = "mutation useTransactionIdentificationMutation($input: IdentificationInput!) { identify(input: $input) { ... on Node { id __typename } ... on Error { code message __typename } ...useTransactionTransactionFragment __typename } } " + transaction_fragment # noqa: E501
110+ authenticate_query = "mutation useTransactionAuthenticationMutation($input: AuthenticationInput!) { authenticate(input: $input) { ... on Node { id __typename } ... on Error { code message __typename } ...useTransactionTransactionFragment __typename } } " + transaction_fragment # noqa: E501
109111
110- data = {"variables" :{"input" :{"email" :user ,"password" :password ,"state" :state }},"query" :"mutation ($input: LoginInput!) {\n token: login(input: $input)\n }" } # noqa: E501
111- u3 = ses .post (url , data = json .dumps (data ), headers = headers )
112- print (3 , u3 .status_code )
113- if u3 .status_code != 200 :
114- return {'status_code' : u3 .status_code , 'error' : u3 .text }
115- data = u3 .json ()['data' ]
116-
117- # post login to tokens
118- headers = {
119- 'authority' : 'login.dr.dk' ,
120- 'User-Agent' : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0" ,
121- 'content-type' : 'application/x-www-form-urlencoded' ,
112+ trans_data = {
113+ "operationName" : "useTransactionTransactionQuery" ,
114+ "variables" : {"id" : trans }, "query" : trans_query
115+ }
116+ identify_data = {
117+ "operationName" : "useTransactionIdentificationMutation" ,
118+ "variables" : {"input" : {"transaction" : trans , "email" : user }}, "query" : identify_query
119+ }
120+ authenticate_data = {
121+ "operationName" : "useTransactionAuthenticationMutation" ,
122+ "variables" : {"input" : {"transaction" : trans , "password" : password }}, "query" : authenticate_query
123+ }
124+
125+ url = 'https://login.dr.dk/graphql'
126+
127+ u1 = ses .post (url , json = trans_data , headers = headers )
128+ print (u1 .json ())
129+ u2 = ses .post (url , json = identify_data , headers = headers )
130+ print (u2 .json ())
131+
132+ u3 = ses .post (url , json = authenticate_data , headers = headers )
133+ print (u3 .json ())
134+
135+ res2 = ses .get (u3 .json ()['data' ]['authenticate' ]['href' ])
136+ if res2 .status_code != 200 :
137+ return {'status_code' : res2 .status_code , 'error' : res2 .text }
138+ code = parse_qs (urlparse (res2 .url ).query )['code' ][0 ]
139+
140+ data = {
141+ "client_id" : CLIENT_ID ,
142+ "redirect_uri" : "https://www.dr.dk/drtv/callback" ,
143+ "code_verifier" : code_verifier , "code" : code ,
144+ "grant_type" : "authorization_code" ,
145+ }
146+ return oidc_token (data )
147+
148+
149+ def oidc_token (data ):
150+ headers = {"Content-Type" : "application/x-www-form-urlencoded" , "Accept" : "application/json" }
151+ res = requests .post ('https://login.dr.dk/oidc/token' , data = data , headers = headers )
152+ if res .status_code != 200 :
153+ return {'status_code' : res .status_code , 'error' : res .text }
154+ return res .json ()
155+
156+
157+ def refresh_token (refresh_token ):
158+ data = {"client_id" : CLIENT_ID , "refresh_token" : refresh_token , "grant_type" : "refresh_token" }
159+ return oidc_token (data )
160+
161+
162+ def exchange_token (tokens ):
163+ data = {
164+ "accessToken" : tokens ['access_token' ], "identityToken" : tokens ['id_token' ],
165+ "scopes" : ["Catalog" ], "device" : "web_browser" , "optout" : False ,
122166 }
123167
124- url = 'https://login.dr.dk/oidc/interactions/login/continue'
125- res = ses .post (url , data = data , headers = headers , allow_redirects = True )
168+ headers = { "Content-Type" : "application/json" , "Accept" : "application/json" }
169+ res = requests .post (URL + '/authorization/exchange' , json = data , headers = headers )
126170 if res .status_code != 200 :
127171 return {'status_code' : res .status_code , 'error' : res .text }
128- o = parse_qs (urlparse (res .url ).fragment )
129- tokens = [o [label ][0 ] for label in ['RefreshableUserAccount' , 'RefreshableUserProfile' ]]
130- return tokens
172+ return res .json ()
131173
132174
133175def deviceid ():
@@ -161,6 +203,7 @@ def __init__(self, cachePath, getLocalizedString, get_setting):
161203 self .init_sqlite_db ()
162204
163205 self .token_file = Path (f'{ self .cachePath } /token.p' )
206+ self .access_tokens = {}
164207 self ._user_token = None
165208 self .user = get_setting ('drtv_username' )
166209 self .password = get_setting ('drtv_password' )
@@ -193,72 +236,68 @@ def init_sqlite_db(self):
193236 (self .cachePath / 'requests_cleaned' ).write_text (str (datetime .now ()))
194237
195238 def read_tokens (self , tokens ):
196- time_str = tokens [0 ]['expirationDate' ].split ('.' )[0 ]
239+ if 'value' in tokens [0 ]:
240+ #old flow, anonymous
241+ time_str = tokens [0 ]['expirationDate' ].split ('.' )[0 ]
242+ self ._user_token = tokens [0 ]['value' ]
243+ self ._profile_token = tokens [1 ]['value' ]
244+ self ._user_name = 'anonymous'
245+ else :
246+ time_str = tokens [0 ]['Expires' ].split ('.' )[0 ]
247+ self ._user_token = tokens [0 ]['Token' ]
248+ self ._profile_token = tokens [1 ]['Token' ]
249+
197250 try :
198251 self ._token_expire = datetime .strptime (time_str + 'Z' , '%Y-%m-%dT%H:%M:%S%z' )
199252 except Exception :
200253 time_struct = time .strptime (time_str , '%Y-%m-%dT%H:%M:%S' )
201254 self ._token_expire = datetime (* time_struct [0 :6 ], tzinfo = timezone .utc )
202- self ._user_token = tokens [0 ]['value' ]
203- self ._profile_token = tokens [1 ]['value' ]
204- if self .user :
205- tokens [0 ]['name' ] = self .get_profile ()['name' ]
206- else :
207- tokens [0 ]['name' ] = 'anonymous'
208- self ._user_name = tokens [0 ]['name' ]
255+ if self .access_tokens :
256+ self ._user_name = self .get_profile ()['name' ]
209257
210258 def request_tokens (self ):
211259 self ._user_token = None
212260 self ._profile_token = None
213261
214262 if self .user :
215- tokens_pure = full_login (self .user , self .password )
216- if isinstance ( tokens_pure , dict ) :
217- err = tokens_pure ['error' ]
263+ access_tokens = full_login (self .user , self .password )
264+ if 'error' in access_tokens :
265+ err = access_tokens ['error' ]
218266 return err
219- tokens = [ self .refresh_token ( t ) for t in tokens_pure ]
220- self . read_tokens ( tokens )
267+ self .access_tokens = access_tokens
268+ tokens = exchange_token ( access_tokens )
221269 else :
222270 tokens = anonymous_tokens ()
223- self .read_tokens (tokens )
271+ self .read_tokens (tokens )
224272 with self .token_file .open ('wb' ) as fh :
225- pickle .dump (tokens , fh )
273+ pickle .dump ([ tokens , self . access_tokens ] , fh )
226274 return None
227275
228- def refresh_token (self , token ):
229- url = 'https://production.dr-massive.com/api/authorization/refresh'
230- params = {'ff' :'idp,ldp,rpt' , 'supportFallbackToken' : True }
231- headers = {
232- 'Host' : 'production.dr-massive.com' ,
233- 'User-Agent' : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0" ,
234- 'content-type' : 'application/json' ,
235- }
236- data = {
237- 'token' : token , 'optout' : False
238- }
239- res = self .session .post (url , params = params , headers = headers , json = data )
240- if res .status_code != 200 :
241- return {'status_code' : res .status_code , 'error' : res .text }
242- return res .json ()
243-
244276 def refresh_tokens (self ):
245277 if self ._user_token is None :
246278 if self .token_file .exists ():
247279 with self .token_file .open ('rb' ) as fh :
248- self .read_tokens (pickle .load (fh ))
249- else :
250- err = self .request_tokens ()
251- if err :
252- raise ApiException (f'Login failed with: "{ err } "' )
253- return
254- if (self ._token_expire - datetime .now (timezone .utc )).total_seconds () < 120 :
280+ [tokens , self .access_tokens ] = pickle .load (fh )
281+ if isinstance (tokens , list ):
282+ self .read_tokens (tokens )
283+
284+ if self ._user_token is None :
285+ err = self .request_tokens ()
286+ if err :
287+ raise ApiException (f'Login failed with: "{ err } "' )
288+ return
289+
290+ if (self ._token_expire - datetime .now (timezone .utc )) < timedelta (hours = 10 ):
255291 failed_refresh = False
256292 tokens = []
257- for t in [self ._user_token , self ._profile_token ]:
258- newtoken = self .refresh_token (t )
259- tokens .append (newtoken )
260- if 'error' in newtoken :
261- failed_refresh = True
293+ # oidc flow
294+ access_tokens = refresh_token (self .access_tokens ['refresh_token' ])
295+ if 'error' in access_tokens :
296+ failed_refresh = True
297+ else :
298+ tokens = exchange_token (access_tokens )
299+ self .access_tokens = access_tokens
300+
262301 if failed_refresh :
263302 self .request_tokens ()
264303 err = self .request_tokens ()
@@ -267,7 +306,7 @@ def refresh_tokens(self):
267306 else :
268307 self .read_tokens (tokens )
269308 with self .token_file .open ('wb' ) as fh :
270- pickle .dump (tokens , fh )
309+ pickle .dump ([ tokens , self . access_tokens ] , fh )
271310
272311 def user_token (self ):
273312 self .refresh_tokens ()
@@ -381,7 +420,8 @@ def get_continue(self, use_cache=False):
381420 def get_profile (self , use_cache = False ):
382421 url = URL + '/account/profile'
383422 headers = {'X-Authorization' : 'Bearer ' + self .profile_token ()}
384- return self ._request_get (url , headers = headers , use_cache = use_cache )
423+ params = {"ff" : "idp,ldp,rpt" , "lang" : "da" }
424+ return self ._request_get (url , headers = headers , params = params , use_cache = use_cache )
385425
386426 def kids_item (self , item ):
387427 if 'classification' in item :
0 commit comments