diff --git a/plugin.video.cbc/addon.xml b/plugin.video.cbc/addon.xml
index 881da7adec..ec462b08fe 100644
--- a/plugin.video.cbc/addon.xml
+++ b/plugin.video.cbc/addon.xml
@@ -1,7 +1,7 @@
@@ -28,6 +28,6 @@
https://forum.kodi.tv/showthread.php?tid=328421
https://watch.cbc.ca/
https://github.com/micahg/plugin.video.cbc
- - Fix live channels and IPTV
+ - Updates for new CBC Gem API
diff --git a/plugin.video.cbc/default.py b/plugin.video.cbc/default.py
index 030580c920..b33af51ea7 100644
--- a/plugin.video.cbc/default.py
+++ b/plugin.video.cbc/default.py
@@ -19,12 +19,6 @@
getString = xbmcaddon.Addon().getLocalizedString
LIVE_CHANNELS = getString(30004)
-GEMS = {
- 'featured': getString(30005),
- 'shows': getString(30006),
- 'documentaries': getString(30024),
- 'kids': getString(30025)
-}
SEARCH = getString(30026)
@@ -56,38 +50,64 @@ def authorize():
return True
-def play(labels, image, url):
+def play(labels, image, data):
"""Play the stream using the configured player."""
- item = xbmcgui.ListItem(labels['title'], path=url)
- if image:
- item.setArt({'thumb': image, 'poster': image})
- item.setInfo(type="Video", infoLabels=labels)
- helper = inputstreamhelper.Helper('hls')
- if not xbmcaddon.Addon().getSettingBool("ffmpeg") and helper.check_inputstream():
- item.setProperty('inputstream', 'inputstream.adaptive')
- item.setProperty('inputstream.adaptive.manifest_type', 'hls')
- xbmcplugin.setResolvedUrl(plugin.handle, True, item)
- if url is None:
+ if not 'url' in data:
xbmcgui.Dialog().ok(getString(30010), getString(30011))
-
+ return
+
+ (lic, tok) = GemV2.get_stream_drm(data)
+ is_helper = None
+ mime = None
+ drm = None
+ if data['type'] == 'hls':
+ is_helper = inputstreamhelper.Helper('hls')
+ elif data['type'] == 'dash':
+ drm = 'com.widevine.alpha'
+ is_helper = inputstreamhelper.Helper('mpd', drm=drm)
+ mime = 'application/dash+xml'
+
+ if is_helper is None:
+ xbmcgui.Dialog().ok(getString(30027), getString(30027))
+ return
+
+ if is_helper.check_inputstream():
+ url = data['url']
+ play_item = xbmcgui.ListItem(path=url)
+ play_item.setInfo(type="Video", infoLabels=labels)
+ if mime:
+ play_item.setMimeType(mime)
+ play_item.setContentLookup(False)
+
+ if int(xbmc.getInfoLabel('System.BuildVersion').split('.')[0]) >= 19:
+ play_item.setProperty('inputstream', is_helper.inputstream_addon)
+ else:
+ play_item.setProperty('inputstreamaddon', is_helper.inputstream_addon)
+
+ if drm:
+ play_item.setProperty('inputstream.adaptive.license_type', drm)
+ license_headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0',
+ 'Content-Type': 'application/octet-stream',
+ 'Origin': 'https://gem.cbc.ca',
+ 'x-dt-auth-token': tok, # string containing "Bearer eyJ...."
+ }
+ license_config = [ lic, urlencode(license_headers), 'R{SSM}', 'R']
+ license_key = '|'.join(license_config)
+ play_item.setProperty('inputstream.adaptive.license_key', license_key)
+ xbmcplugin.setResolvedUrl(plugin.handle, True, play_item)
def add_items(handle, items):
for item in items:
list_item = xbmcgui.ListItem(item['title'])
list_item.setInfo(type="Video", infoLabels=CBC.get_labels(item))
- image = item['image'].replace('(Size)', '224')
+ image = item['image']['url'].replace('(Size)', '224')
list_item.setArt({'thumb': image, 'poster': image})
item_type = item['type']
is_folder = True
- if item_type == 'SHOW':
- url = plugin.url_for(gem_show_menu, item['id'])
- elif item_type == 'ASSET':
- url = plugin.url_for(gem_asset, item['id'])
- list_item.setProperty('IsPlayable', 'true')
- is_folder = False
- elif item_type == 'SEASON':
- # ignore the season and go to the show (its what the web UI does)
- url = plugin.url_for(gem_show_menu, item['id'].split('/')[0])
+ if item_type.lower() == 'show' or item_type.lower() == 'media':
+ p = GemV2.normalized_format_path(item)
+ url = plugin.url_for(layout_menu, p)
else:
log(f'Unable to handle shelf item type "{item_type}".', True)
url = None
@@ -143,9 +163,12 @@ def live_channels_add_only(station):
@plugin.route('/channels/play')
def play_live_channel():
labels = dict(parse_qsl(plugin.args['labels'][0])) if 'labels' in plugin.args else None
- chans = LiveChannels()
- url = chans.get_channel_stream(plugin.args['id'][0])
- return play(labels, plugin.args['image'][0], url)
+ data = GemV2.get_stream(plugin.args['id'][0], plugin.args['app_code'][0])
+ if not 'url' in data:
+ log('Failed to get stream URL, attempting to authorize.')
+ if authorize():
+ data = GemV2.get_stream(plugin.args['id'][0], plugin.args['app_code'][0])
+ return play(labels, plugin.args['image'][0] if 'image' in plugin.args else None, data)
@plugin.route('/channels')
def live_channels_menu():
@@ -169,131 +192,11 @@ def live_channels_menu():
(getString(30017), 'RunPlugin({})'.format(plugin.url_for(live_channels_add_only, callsign))),
])
xbmcplugin.addDirectoryItem(plugin.handle,
- plugin.url_for(play_live_channel, id=channel['idMedia'],
+ plugin.url_for(play_live_channel, id=channel['idMedia'], app_code='medianetlive',
labels=urlencode(labels), image=image), item, False)
xbmcplugin.endOfDirectory(plugin.handle)
-@plugin.route('/gem/show/episode')
-def gem_episode():
- """Play an episode."""
- json_str = plugin.args['query'][0]
- episode = json.loads(json_str)
-
- # get the url, and failing that, attempt authorization, then retry
- resp = GemV2().get_episode(episode['url'])
- url = None if not resp else resp['url'] if 'url' in resp else None
- if not url:
- log('Failed to get stream URL, attempting to authorize.')
- if authorize():
- resp = GemV2().get_episode(episode['url'])
- url = resp['url'] if 'url' in resp else None
-
- labels = episode['labels']
- play(labels, None, url)
-
-
-@plugin.route('/gem/show/season')
-def gem_show_season():
- """Create a menu for a show season."""
- xbmcplugin.setContent(plugin.handle, 'videos')
- json_str = plugin.args['query'][0]
- # remember show['season'] is season details but there is general show info in show as well
- show = json.loads(json_str)
- for episode in show['season']['assets']:
- item = xbmcgui.ListItem(episode['title'])
- image = episode['image'].replace('(Size)', '224')
- item.setArt({'thumb': image, 'poster': image})
- item.setProperty('IsPlayable', 'true')
- labels = GemV2.get_labels(show, episode)
- item.setInfo(type="Video", infoLabels=labels)
- episode_info = {'url': episode['playSession']['url'], 'labels': labels}
- url = plugin.url_for(gem_episode, query=json.dumps(episode_info))
- xbmcplugin.addDirectoryItem(plugin.handle, url, item, False)
- xbmcplugin.endOfDirectory(plugin.handle)
-
-
-@plugin.route('/gem/asset/')
-def gem_asset(asset):
- asset_layout = GemV2.get_asset_by_id(asset)
- resp = GemV2.get_episode(asset_layout['playSession']['url'])
- url = None if not resp else resp['url'] if 'url' in resp else None
- if not url:
- log('Failed to get stream URL, attempting to authorize.')
- if authorize():
- resp = GemV2().get_episode(asset_layout['playSession']['url'])
- url = resp['url'] if 'url' in resp else None
- labels = GemV2.get_labels({'title': asset_layout['series']}, asset_layout)
- image = asset_layout['image']
- play(labels, image, url)
-
-
-def gem_add_film_assets(assets):
- for asset in assets:
- labels = GemV2.get_labels({'title': asset['series']}, asset)
- image = asset['image']
- item = xbmcgui.ListItem(labels['title'])
- item.setInfo(type="Video", infoLabels=labels)
- item.setArt({'thumb': image, 'poster': image})
- item.setProperty('IsPlayable', 'true')
- episode_info = {'url': asset['playSession']['url'], 'labels': labels}
- url = plugin.url_for(gem_episode, query=json.dumps(episode_info))
- xbmcplugin.addDirectoryItem(plugin.handle, url, item, False)
-
-
-@plugin.route('/gem/show/')
-def gem_show_menu(show_id):
- """Create a menu for a shelfs items."""
- xbmcplugin.setContent(plugin.handle, 'videos')
- show_layout = GemV2.get_show_layout_by_id(show_id)
- show = {k: v for (k, v) in show_layout.items() if k not in ['sponsors', 'seasons']}
- for season in show_layout['seasons']:
-
- # films seem to have been shoe-horned (with teeth) into the structure oddly -- compensate
- if season['title'] == 'Film':
- gem_add_film_assets(season['assets'])
- else:
- labels = GemV2.get_labels(season, season)
- item = xbmcgui.ListItem(season['title'])
- item.setInfo(type="Video", infoLabels=labels)
- image = season['image'].replace('(Size)', '224')
- item.setArt({'thumb': image, 'poster': image})
- show['season'] = season
- url = plugin.url_for(gem_show_season, query=json.dumps(show))
- xbmcplugin.addDirectoryItem(plugin.handle, url, item, True)
-
- xbmcplugin.endOfDirectory(plugin.handle)
-
-
-@plugin.route('/gem/shelf')
-def gem_shelf_menu():
- """Create a menu item for each shelf."""
- handle = plugin.handle
- xbmcplugin.setContent(handle, 'videos')
- json_str = plugin.args['query'][0]
- shelf_items = json.loads(json_str)
- add_items(handle, shelf_items)
- xbmcplugin.addSortMethod(plugin.handle, xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE)
- xbmcplugin.endOfDirectory(handle)
-
-
-@plugin.route('/gem/categories/')
-def gem_category_menu(category_id):
- """Populate a menu with categorical content."""
- handle = plugin.handle
- xbmcplugin.setContent(handle, 'videos')
- category = GemV2.get_category(category_id)
- for show in category['items']:
- item = xbmcgui.ListItem(show['title'])
- item.setInfo(type="Video", infoLabels=CBC.get_labels(show))
- image = show['image'].replace('(Size)', '224')
- item.setArt({'thumb': image, 'poster': image})
- url = plugin.url_for(gem_show_menu, show['id'])
- xbmcplugin.addDirectoryItem(handle, url, item, True)
- xbmcplugin.addSortMethod(handle, xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE)
- xbmcplugin.endOfDirectory(handle)
-
-
@plugin.route('/gem/search')
def search():
handle = plugin.handle
@@ -303,23 +206,24 @@ def search():
xbmcplugin.endOfDirectory(handle)
-@plugin.route('/gem/layout/')
+@plugin.route('/gem/layout/')
def layout_menu(layout):
"""Populate the menu with featured items."""
handle = plugin.handle
xbmcplugin.setContent(handle, 'videos')
- layout = GemV2.get_layout(layout)
- if 'categories' in layout:
- for category in layout['categories']:
- item = xbmcgui.ListItem(category['title'])
- url = plugin.url_for(gem_category_menu, category['id'])
- xbmcplugin.addDirectoryItem(handle, url, item, True)
- if 'shelves' in layout:
- for shelf in layout['shelves']:
- item = xbmcgui.ListItem(shelf['title'])
- shelf_items = json.dumps(shelf['items'])
- url = plugin.url_for(gem_shelf_menu, query=shelf_items)
- xbmcplugin.addDirectoryItem(handle, url, item, True)
+ for f in GemV2.get_format(layout):
+ n = GemV2.normalized_format_item(f)
+ p = GemV2.normalized_format_path(f)
+ item = xbmcgui.ListItem(n['label'])
+ if 'art' in n:
+ item.setArt(n['art'])
+ item.setInfo(type="Video", infoLabels=n['info_labels'])
+ if n['playable']:
+ item.setProperty('IsPlayable', 'true')
+ url = plugin.url_for(play_live_channel, id=p, app_code=n['app_code'])
+ else:
+ url = plugin.url_for(layout_menu, p)
+ xbmcplugin.addDirectoryItem(handle, url, item, not n['playable'])
xbmcplugin.endOfDirectory(handle)
@@ -334,8 +238,8 @@ def main_menu():
handle = plugin.handle
xbmcplugin.setContent(handle, 'videos')
- for key, value in GEMS.items():
- xbmcplugin.addDirectoryItem(handle, plugin.url_for(layout_menu, key), xbmcgui.ListItem(value), True)
+ for c in GemV2.get_browse():
+ xbmcplugin.addDirectoryItem(handle, plugin.url_for(layout_menu, c['url']), xbmcgui.ListItem(c['title']), True)
xbmcplugin.addDirectoryItem(handle, plugin.url_for(live_channels_menu), xbmcgui.ListItem(LIVE_CHANNELS), True)
xbmcplugin.addDirectoryItem(handle, plugin.url_for(search), xbmcgui.ListItem(SEARCH), True)
xbmcplugin.endOfDirectory(handle)
diff --git a/plugin.video.cbc/resources/language/resource.language.en_gb/strings.po b/plugin.video.cbc/resources/language/resource.language.en_gb/strings.po
index 91ab86b40b..ecc6711043 100644
--- a/plugin.video.cbc/resources/language/resource.language.en_gb/strings.po
+++ b/plugin.video.cbc/resources/language/resource.language.en_gb/strings.po
@@ -119,3 +119,7 @@ msgstr ""
msgctxt "#30026"
msgid "Search"
msgstr ""
+
+msgctxt "#30027"
+msgid "Unsupported DRM"
+msgstr ""
diff --git a/plugin.video.cbc/resources/lib/cbc.py b/plugin.video.cbc/resources/lib/cbc.py
index a142001459..64b5529be9 100644
--- a/plugin.video.cbc/resources/lib/cbc.py
+++ b/plugin.video.cbc/resources/lib/cbc.py
@@ -325,21 +325,6 @@ def get_labels(item):
return labels
- def parse_smil(self, smil):
- """Parse a SMIL file for the video."""
- resp = self.session.get(smil)
-
- if not resp.status_code == 200:
- log(f'ERROR: {smil} returns status of {resp.status_code}', True)
- return None
- save_cookies(self.session.cookies)
-
- dom = parseString(resp.content)
- seq = dom.getElementsByTagName('seq')[0]
- video = seq.getElementsByTagName('video')[0]
- src = video.attributes['src'].value
- return src
-
@staticmethod
def get_session():
"""Get a requests session object with CBC cookies."""
diff --git a/plugin.video.cbc/resources/lib/gemv2.py b/plugin.video.cbc/resources/lib/gemv2.py
index 57b4faeb91..35d563c454 100644
--- a/plugin.video.cbc/resources/lib/gemv2.py
+++ b/plugin.video.cbc/resources/lib/gemv2.py
@@ -4,41 +4,33 @@
import requests
from resources.lib.cbc import CBC
-from resources.lib.utils import loadAuthorization
-
-LAYOUT_MAP = {
- 'featured': 'https://services.radio-canada.ca/ott/cbc-api/v2/home',
- 'shows': 'https://services.radio-canada.ca/ott/cbc-api/v2/hubs/shows',
- 'documentaries': 'https://services.radio-canada.ca/ott/cbc-api/v2/hubs/documentaries',
- 'kids': 'https://services.radio-canada.ca/ott/cbc-api/v2/hubs/kids'
-}
-SHOW_BY_ID = 'https://services.radio-canada.ca/ott/cbc-api/v2/shows/{}'
-CATEGORY_BY_ID = 'https://services.radio-canada.ca/ott/cbc-api/v2/categories/{}'
-ASSET_BY_ID = 'https://services.radio-canada.ca/ott/cbc-api/v2/assets/{}'
-SEARCH_BY_NAME = 'https://services.radio-canada.ca/ott/cbc-api/v2/search'
+from resources.lib.utils import loadAuthorization, log
+
+
+# api CONFIG IS AT https://services.radio-canada.ca/ott/catalog/v1/gem/settings?device=web
+BROWSE_URI = 'https://services.radio-canada.ca/ott/catalog/v2/gem/browse?device=web'
+FORMAT_BY_ID = 'https://services.radio-canada.ca/ott/catalog/v2/gem/{}?device=web'
+SEARCH_BY_NAME = 'https://services.radio-canada.ca/ott/catalog/v1/gem/search'
+
class GemV2:
"""V2 Gem API class."""
@staticmethod
- def get_layout(name):
- """Get a Gem V2 layout by name."""
- url = LAYOUT_MAP[name]
- resp = CBC.get_session().get(url)
- return json.loads(resp.content)
+ def scrape_json(uri, headers=None, params=None):
+ resp = CBC.get_session().get(uri, headers=headers, params=params)
- @staticmethod
- def get_show_layout_by_id(show_id):
- """Get a Gem V2 show layout by ID."""
- url = SHOW_BY_ID.format(show_id)
- resp = CBC.get_session().get(url)
- return json.loads(resp.content)
+ if resp.status_code != 200:
+ log(f'HTTP {resp.status_code} from {uri}', True)
+ return None
+
+ try:
+ jsObj = json.loads(resp.content)
+ except:
+ log(f'Unable to parse JSON from {uri} (status {resp.status_code})', True)
+ return None
+ return jsObj
- @staticmethod
- def get_asset_by_id(asset_id):
- url = ASSET_BY_ID.format(asset_id)
- resp = CBC.get_session().get(url)
- return json.loads(resp.content)
@staticmethod
def get_episode(url):
@@ -56,15 +48,126 @@ def get_episode(url):
if 'claims' in auth:
headers['x-claims-token'] = auth['claims']
- resp = requests.get(url, headers=headers)
- return json.loads(resp.content)
+ return GemV2.scrape_json(url, headers)
+
+
+ @staticmethod
+ def get_browse(type='formats'):
+ """Get a Gem V2 API V2 browse format"""
+ jsObj = GemV2.scrape_json(BROWSE_URI)
+ if jsObj is not None and type in jsObj:
+ return jsObj[type]
+ log(f'Unable to find key "{type}" in response from {BROWSE_URI}')
+ return None
+
+ @staticmethod
+ def get_format(path):
+ """Get a Gem V2 API V2 browse format"""
+ url = FORMAT_BY_ID.format(path)
+ jsObj = GemV2.scrape_json(url)
+ if jsObj is None or 'content' not in jsObj:
+ log(f'Unable to find key content in response from {url}')
+ return None
+ content = jsObj['content'][0]
+ if 'items' in content and 'results' in content['items']:
+ return content['items']['results']
+
+ if 'requestedType' in jsObj and jsObj['requestedType'].lower() == 'season':
+ # Search for the right season:
+ # - lineup url will be something like 'rosemary-barton-live/s02'
+ # - path will be something like 'show/rosemary-barton-live/s02'
+ for lineup in content['lineups']:
+ if 'url' in lineup and lineup['url'] in path:
+ return lineup['items']
+ return content['lineups'][0]['items']
+ if 'lineups' in content:
+ return content['lineups']
+
+ log(f'Unable to find key content/[0]/items/results in response from {url}')
+ return None
+
+ @staticmethod
+ def get_stream(id, app_code):
+ url = f'https://services.radio-canada.ca/media/meta/v1/index.ashx?appCode={app_code}&idMedia={id}&output=jsonObject'
+ jsObj = GemV2.scrape_json(url)
+ if jsObj['errorMessage'] is not None:
+ log(f'Error fetching {url}: {jsObj["errorMessage"]}')
+ return None
+
+ drm = None
+ for at in jsObj['availableTechs']:
+ if at['name'] == 'dash':
+ drm = at
+ elif at['name'] == 'hls' and drm == None:
+ # only use HLS if dash isn't available -- the new HLS cannot be played
+ drm = at
+ log(drm)
+ tech = drm['name']
+ manifest_versions = drm['manifestVersions']
+ mv = manifest_versions[-1] if manifest_versions is not None else 1
+ url = f'https://services.radio-canada.ca/media/validation/v2/?appCode={app_code}&connectionType=hd&deviceType=multiams&idMedia={id}&multibitrate=true&output=json&tech={tech}&manifestType=desktop&manifestVersion={mv}'
+ retval = GemV2.get_episode(url)
+ retval['type'] = drm['name']
+ return retval
+
+ @staticmethod
+ def get_stream_drm(stream):
+ wv_url = None
+ wv_tok = None
+ for x in stream['params']:
+ if x['name'] == 'widevineLicenseUrl':
+ wv_url = x['value']
+ if x['name'] == 'widevineAuthToken':
+ wv_tok = x['value']
+ return (wv_url, wv_tok)
+
+ @staticmethod
+ def normalized_format_item(item):
+ """
+ Given an object in the list returned by get_format, turn its useful
+ bits into stuff we can display
+ """
+ images = item['images'] if 'images' in item else None
+ retval = {
+ 'label': item['title'],
+ 'playable': 'idMedia' in item,
+ 'info_labels': {
+ 'tvshowtitle': item['title'],
+ 'title': item['title'],
+ }
+ }
+ if 'description' in item:
+ retval['info_labels']['plot'] = item['description']
+ retval['info_labels']['plotoutline'] = item['description']
+ if images:
+ retval['art'] = {
+ 'thumb': images['background']['url'] if 'background' in images else None,
+ 'poster': images['card']['url'],
+ 'clearlogo': images['logo']['url'] if 'logo' in images else None,
+ }
+ if 'metadata' in item:
+ meta = item['metadata']
+ if 'country' in meta:
+ retval['info_labels']['country'] = meta['country']
+ if 'duration' in meta:
+ retval['info_labels']['duration'] = meta['duration']
+ if 'airDate' in meta:
+ retval['info_labels']['aired'] = meta['airDate']
+ if 'credits' in meta:
+ retval['info_labels']['cast'] = meta['credits'][0]['peoples'].split(',')
+ if 'idMedia' in item:
+ retval['app_code'] = 'medianet' if item['mediaType'] == 'LiveToVod' else 'gem'
+ None
+ return retval
@staticmethod
- def get_category(category_id):
- """Get a Gem V2 category by ID."""
- url = CATEGORY_BY_ID.format(category_id)
- resp = CBC.get_session().get(url)
- return json.loads(resp.content)
+ def normalized_format_path(item):
+ if 'idMedia' in item:
+ return item['idMedia']
+ if 'type' in item and item['type'].lower() == 'show':
+ return f'{item["type"]}/{item["url"]}'
+ return f'show/{item["url"]}'
+
@staticmethod
def get_labels(show, episode):
@@ -84,8 +187,15 @@ def get_labels(show, episode):
labels['duration'] = episode['duration']
return labels
+
@staticmethod
def search_by_term(term):
- params = {'term': term}
- resp = CBC.get_session().get(SEARCH_BY_NAME, params=params)
- return json.loads(resp.content)
\ No newline at end of file
+ params = {
+ 'term': term,
+ 'device': 'web',
+ }
+ jsObj = GemV2.scrape_json(SEARCH_BY_NAME, params=params)
+ if jsObj is None or 'result' not in jsObj:
+ log(f'Search by term yields no result: {term}')
+ return None
+ return jsObj['result']
diff --git a/plugin.video.cbc/resources/lib/livechannels.py b/plugin.video.cbc/resources/lib/livechannels.py
index ed2da14f84..68e2403bc6 100644
--- a/plugin.video.cbc/resources/lib/livechannels.py
+++ b/plugin.video.cbc/resources/lib/livechannels.py
@@ -7,6 +7,7 @@
from resources.lib.utils import save_cookies, loadCookies, log, get_iptv_channels_file
from resources.lib.cbc import CBC
+from resources.lib.gemv2 import GemV2
LIST_URL = 'https://services.radio-canada.ca/ott/catalog/v2/gem/home?device=web'
LIST_ELEMENT = '2415871718'
@@ -71,6 +72,7 @@ def get_iptv_channels(self):
# - channel_dict is used by the IPTVManager for the guide and stream is how the IPTV manager calls us back to play something
values = {
'id': callsign,
+ 'app_code': 'medianetlive',
'image': image,
'labels': urlencode(labels)
}
@@ -89,13 +91,7 @@ def get_iptv_channels(self):
return result
def get_channel_stream(self, id):
- url = f'https://services.radio-canada.ca/media/validation/v2/?appCode=medianetlive&connectionType=hd&deviceType=ipad&idMedia={id}&multibitrate=true&output=json&tech=hls&manifestType=desktop'
- resp = self.session.get(url)
- if not resp.status_code == 200:
- log('ERROR: {} returns status of {}'.format(LIST_URL, resp.status_code), True)
- return None
- save_cookies(self.session.cookies)
- return json.loads(resp.content)['url']
+ return GemV2.get_stream(id=id,app_code='medianetlive')
def get_channel_metadata(self, id):
url = f'https://services.radio-canada.ca/media/meta/v1/index.ashx?appCode=medianetlive&idMedia={id}&output=jsonObject'