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

Closes #12406
Authored by: bashonly
pull/12462/head
bashonly 3 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(
'https://services.radio-canada.ca/ott/catalog/v1/gem/settings', None,
'Downloading site settings', query={'device': 'web'})['identityManagement']['ropc']
def _is_jwt_expired(self, token):
return jwt_decode_hs256(token)['exp'] - time.time() < 300
def _call_oauth_api(self, oauth_data, note='Refreshing access token'):
response = self._download_json(
self._ropc_settings['url'], None, note, data=urlencode_postdata({
'client_id': self._CLIENT_ID,
**oauth_data,
'scope': self._ropc_settings['scopes'],
}))
self._refresh_token = response['refresh_token']
self._access_token = response['access_token']
self.cache.store(self._NETRC_MACHINE, 'token_data', [self._refresh_token, self._access_token])
def _perform_login(self, username, password):
if not self._refresh_token:
self._refresh_token, self._access_token = self.cache.load(
self._NETRC_MACHINE, 'token_data', default=[None, None])
if self._refresh_token and self._access_token:
self.write_debug('Using cached refresh token')
if not self._claims_token:
self._claims_token = self.cache.load(self._NETRC_MACHINE, 'claims_token')
return
try:
self._call_oauth_api({
'grant_type': 'password',
'username': username,
'password': password, 'password': password,
}).encode() }, note='Logging in')
headers = {'content-type': 'application/json'} except ExtractorError as e:
query = {'apikey': self._TOKEN_API_KEY} if isinstance(e.cause, HTTPError) and e.cause.status == 400:
resp = self._download_json('https://api.loginradius.com/identity/v2/auth/login', raise ExtractorError('Invalid username and/or password', expected=True)
None, data=data, headers=headers, query=query) raise
access_token = resp['access_token']
def _fetch_access_token(self):
query = { if self._is_jwt_expired(self._access_token):
'access_token': access_token, try:
'apikey': self._TOKEN_API_KEY, self._call_oauth_api({
'jwtapp': 'jwt', 'grant_type': 'refresh_token',
} 'refresh_token': self._refresh_token,
resp = self._download_json('https://cloud-api.loginradius.com/sso/jwt/api/token', })
None, headers=headers, query=query) except ExtractorError:
sig = resp['signature'] self._refresh_token, self._access_token = None, None
self.cache.store(self._NETRC_MACHINE, 'token_data', [None, None])
data = json.dumps({'jwt': sig}).encode() self.report_warning('Refresh token has been invalidated; retrying with credentials')
headers = {'content-type': 'application/json', 'ott-device-type': 'web'} self._perform_login(*self._get_login_info())
resp = self._download_json('https://services.radio-canada.ca/ott/cbc-api/v2/token',
None, data=data, headers=headers, expected_status=426) return self._access_token
cbc_access_token = resp['accessToken']
def _fetch_claims_token(self):
headers = {'content-type': 'application/json', 'ott-device-type': 'web', 'ott-access-token': cbc_access_token} if not self._get_login_info()[0]:
resp = self._download_json('https://services.radio-canada.ca/ott/cbc-api/v2/profile', return None
None, headers=headers, expected_status=426)
return resp['claimsToken'] if not self._claims_token or self._is_jwt_expired(self._claims_token):
self._claims_token = self._download_json(
def _get_claims_token_expiry(self): 'https://services.radio-canada.ca/ott/subscription/v2/gem/Subscriber/profile',
# Token is a JWT None, 'Downloading claims token', query={'device': 'web'},
# JWT is decoded here and 'exp' field is extracted headers={'Authorization': f'Bearer {self._fetch_access_token()}'})['claimsToken']
# It is a Unix timestamp for when the token expires
b64_data = self._claims_token.split('.')[1]
data = base64.urlsafe_b64decode(b64_data + '==')
return json.loads(data)['exp']
def claims_token_expired(self):
exp = self._get_claims_token_expiry()
# It will expire in less than 10 seconds, or has already expired
return exp - time.time() < 10
def claims_token_valid(self):
return self._claims_token is not None and not self.claims_token_expired()
def _get_claims_token(self, email, password):
if not self.claims_token_valid():
self._claims_token = self._new_claims_token(email, password)
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()
if email and password:
claims_token = self._get_claims_token(email, password)
headers = {'x-claims-token': claims_token}
else:
headers = {} headers = {}
if claims_token := self._fetch_claims_token():
headers['x-claims-token'] = claims_token
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