[ie/gem.cbc.ca] Fix login support (#12414)

Closes #12406
Authored by: bashonly
pull/12462/head
bashonly 2 months ago committed by GitHub
parent 6933f5670c
commit eb1417786a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,17 +1,17 @@
import base64
import functools import functools
import json
import re import re
import time import time
import urllib.parse import urllib.parse
from .common import InfoExtractor from .common import InfoExtractor
from ..networking import HEADRequest from ..networking import HEADRequest
from ..networking.exceptions import HTTPError
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
float_or_none, float_or_none,
int_or_none, int_or_none,
js_to_json, js_to_json,
jwt_decode_hs256,
mimetype2ext, mimetype2ext,
orderedSet, orderedSet,
parse_age_limit, parse_age_limit,
@ -24,6 +24,7 @@ from ..utils import (
update_url, update_url,
url_basename, url_basename,
url_or_none, url_or_none,
urlencode_postdata,
) )
from ..utils.traversal import require, traverse_obj, trim_str from ..utils.traversal import require, traverse_obj, trim_str
@ -608,66 +609,82 @@ class CBCGemIE(CBCGemBaseIE):
'only_matching': True, 'only_matching': True,
}] }]
_TOKEN_API_KEY = '3f4beddd-2061-49b0-ae80-6f1f2ed65b37' _CLIENT_ID = 'fc05b0ee-3865-4400-a3cc-3da82c330c23'
_refresh_token = None
_access_token = None
_claims_token = None _claims_token = None
def _new_claims_token(self, email, password): @functools.cached_property
data = json.dumps({ def _ropc_settings(self):
'email': email, return self._download_json(
'password': password, 'https://services.radio-canada.ca/ott/catalog/v1/gem/settings', None,
}).encode() 'Downloading site settings', query={'device': 'web'})['identityManagement']['ropc']
headers = {'content-type': 'application/json'}
query = {'apikey': self._TOKEN_API_KEY} def _is_jwt_expired(self, token):
resp = self._download_json('https://api.loginradius.com/identity/v2/auth/login', return jwt_decode_hs256(token)['exp'] - time.time() < 300
None, data=data, headers=headers, query=query)
access_token = resp['access_token'] def _call_oauth_api(self, oauth_data, note='Refreshing access token'):
response = self._download_json(
query = { self._ropc_settings['url'], None, note, data=urlencode_postdata({
'access_token': access_token, 'client_id': self._CLIENT_ID,
'apikey': self._TOKEN_API_KEY, **oauth_data,
'jwtapp': 'jwt', 'scope': self._ropc_settings['scopes'],
} }))
resp = self._download_json('https://cloud-api.loginradius.com/sso/jwt/api/token', self._refresh_token = response['refresh_token']
None, headers=headers, query=query) self._access_token = response['access_token']
sig = resp['signature'] self.cache.store(self._NETRC_MACHINE, 'token_data', [self._refresh_token, self._access_token])
data = json.dumps({'jwt': sig}).encode() def _perform_login(self, username, password):
headers = {'content-type': 'application/json', 'ott-device-type': 'web'} if not self._refresh_token:
resp = self._download_json('https://services.radio-canada.ca/ott/cbc-api/v2/token', self._refresh_token, self._access_token = self.cache.load(
None, data=data, headers=headers, expected_status=426) self._NETRC_MACHINE, 'token_data', default=[None, None])
cbc_access_token = resp['accessToken']
if self._refresh_token and self._access_token:
headers = {'content-type': 'application/json', 'ott-device-type': 'web', 'ott-access-token': cbc_access_token} self.write_debug('Using cached refresh token')
resp = self._download_json('https://services.radio-canada.ca/ott/cbc-api/v2/profile', if not self._claims_token:
None, headers=headers, expected_status=426) self._claims_token = self.cache.load(self._NETRC_MACHINE, 'claims_token')
return resp['claimsToken'] return
def _get_claims_token_expiry(self): try:
# Token is a JWT self._call_oauth_api({
# JWT is decoded here and 'exp' field is extracted 'grant_type': 'password',
# It is a Unix timestamp for when the token expires 'username': username,
b64_data = self._claims_token.split('.')[1] 'password': password,
data = base64.urlsafe_b64decode(b64_data + '==') }, note='Logging in')
return json.loads(data)['exp'] except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
def claims_token_expired(self): raise ExtractorError('Invalid username and/or password', expected=True)
exp = self._get_claims_token_expiry() raise
# It will expire in less than 10 seconds, or has already expired
return exp - time.time() < 10 def _fetch_access_token(self):
if self._is_jwt_expired(self._access_token):
def claims_token_valid(self): try:
return self._claims_token is not None and not self.claims_token_expired() self._call_oauth_api({
'grant_type': 'refresh_token',
def _get_claims_token(self, email, password): 'refresh_token': self._refresh_token,
if not self.claims_token_valid(): })
self._claims_token = self._new_claims_token(email, password) except ExtractorError:
self._refresh_token, self._access_token = None, None
self.cache.store(self._NETRC_MACHINE, 'token_data', [None, None])
self.report_warning('Refresh token has been invalidated; retrying with credentials')
self._perform_login(*self._get_login_info())
return self._access_token
def _fetch_claims_token(self):
if not self._get_login_info()[0]:
return None
if not self._claims_token or self._is_jwt_expired(self._claims_token):
self._claims_token = self._download_json(
'https://services.radio-canada.ca/ott/subscription/v2/gem/Subscriber/profile',
None, 'Downloading claims token', query={'device': 'web'},
headers={'Authorization': f'Bearer {self._fetch_access_token()}'})['claimsToken']
self.cache.store(self._NETRC_MACHINE, 'claims_token', self._claims_token) self.cache.store(self._NETRC_MACHINE, 'claims_token', self._claims_token)
return self._claims_token else:
self.write_debug('Using cached claims token')
def _real_initialize(self): return self._claims_token
if self.claims_token_valid():
return
self._claims_token = self.cache.load(self._NETRC_MACHINE, 'claims_token')
def _real_extract(self, url): def _real_extract(self, url):
video_id, season_number = self._match_valid_url(url).group('id', 'season') video_id, season_number = self._match_valid_url(url).group('id', 'season')
@ -675,14 +692,10 @@ class CBCGemIE(CBCGemBaseIE):
item_info = traverse_obj(video_info, ( item_info = traverse_obj(video_info, (
'content', ..., 'lineups', ..., 'items', 'content', ..., 'lineups', ..., 'items',
lambda _, v: v['url'] == video_id, any, {require('item info')})) lambda _, v: v['url'] == video_id, any, {require('item info')}))
media_id = item_info['idMedia']
email, password = self._get_login_info() headers = {}
if email and password: if claims_token := self._fetch_claims_token():
claims_token = self._get_claims_token(email, password) headers['x-claims-token'] = claims_token
headers = {'x-claims-token': claims_token}
else:
headers = {}
m3u8_info = self._download_json( m3u8_info = self._download_json(
'https://services.radio-canada.ca/media/validation/v2/', 'https://services.radio-canada.ca/media/validation/v2/',
@ -695,7 +708,7 @@ class CBCGemIE(CBCGemBaseIE):
'tech': 'hls', 'tech': 'hls',
'manifestVersion': '2', 'manifestVersion': '2',
'manifestType': 'desktop', 'manifestType': 'desktop',
'idMedia': media_id, 'idMedia': item_info['idMedia'],
}) })
if m3u8_info.get('errorCode') == 1: if m3u8_info.get('errorCode') == 1:

Loading…
Cancel
Save