From 7ca5d1d8cbed950e27917828b525327e69323995 Mon Sep 17 00:00:00 2001 From: darodi <4682830+darodi@users.noreply.github.com> Date: Wed, 7 Jun 2023 23:13:07 +0200 Subject: [PATCH 1/8] [FR - MYCANAL] new url and APIs --- resources/lib/channels/fr/mycanal.py | 85 ++++++++++++++++++---------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/resources/lib/channels/fr/mycanal.py b/resources/lib/channels/fr/mycanal.py index ca9a72498..54eeeaa1b 100644 --- a/resources/lib/channels/fr/mycanal.py +++ b/resources/lib/channels/fr/mycanal.py @@ -5,16 +5,20 @@ # This file is part of Catch-up TV & More from __future__ import unicode_literals + +import base64 import json +import math +import pickle +import random import re -import requests import time -import random -import math + +# noinspection PyUnresolvedReferences import inputstreamhelper +import requests import urlquick -import base64 -import pickle +# noinspection PyUnresolvedReferences import xbmcvfs try: @@ -33,7 +37,7 @@ from resources.lib.menu_utils import item_post_treatment # URL : -URL_ROOT_SITE = 'https://www.mycanal.fr' +URL_ROOT_SITE = 'https://www.canalplus.com' # Channel # Replay channel : @@ -44,9 +48,9 @@ LIVE_TOKEN_URL = 'https://secure-browser.canalplus-bo.net/WebPortal/ottlivetv/api/V4/zones/cpfra/devices/3/apps/1/jobs/InitLiveTV' -URL_VIDEO_DATAS = 'https://secure-gen-hapi.canal-plus.com/conso/playset/unit/%s' - -URL_STREAM_DATAS = 'https://secure-gen-hapi.canal-plus.com/conso/view' +SECURE_GEN_HAPI = 'https://secure-gen-hapi.canal-plus.com' +URL_VIDEO_DATAS = SECURE_GEN_HAPI + '/conso/playset/unit/%s' +URL_STREAM_DATAS = SECURE_GEN_HAPI + '/conso/view' URL_JSON = "https://dsh-m013.ora02.live-scy.canalplus-cdn.net/plfiles/v2/metr/dash-ssl/%s-hd.json" @@ -60,7 +64,6 @@ CERTIFICATE_URL = 'https://secure-webtv-static.canal-plus.com/widevine/cert/cert_license_widevine_com.bin' - # The channel need to be on the same order has the website LIVE_MYCANAL = { 'canalplus': 'canalplus', @@ -75,10 +78,14 @@ 'canalplus': 'x5gv6be' } +REACT_QUERY_STATE = re.compile(r'window.REACT_QUERY_STATE\s*=\s*(.*?);\s*document\.documentElement\.classList\.remove') +WINDOW_DATA = re.compile(r'window.__data=(.*?);\s*window.REACT_QUERY_STATE') + def getKeyID(): def rnd(): return str(hex(math.floor((1 + random.random()) * 9007199254740991)))[4:] + ts = int(1000 * time.time()) deviceKeyId = str(ts) + '-' + rnd() @@ -127,30 +134,40 @@ def list_categories(plugin, item_id, **kwargs): """ resp = urlquick.get(URL_REPLAY % item_id) - json_replay = re.compile( - r'window.__data=(.*?); window.app_config').findall(resp.text)[0] - json_parser = json.loads(json_replay) - - for category in json_parser["templates"]["landing"]["strates"]: + # window_data = WINDOW_DATA.findall(resp.text)[0] + # json_window_data = json.loads(window_data) + react_query_state = REACT_QUERY_STATE.findall(resp.text)[0] + json_react_query_state = json.loads(react_query_state) + + # for category in json_window_data["application"]["navigation"]: + # title = category['displayName'] + # item = Listitem() + # item.label = title + # item.set_callback(list_contents, item_id=item_id, key_value='key_value') + # item_post_treatment(item) + # yield item + + for category in json_react_query_state["queries"][0]["state"]["data"]["strates"]: if category["type"] == "carrousel": title = category['context']['context_page_title'] key_value = category['reactKey'] item = Listitem() item.label = title - item.set_callback( - list_contents, item_id=item_id, key_value=key_value) + item.set_callback(list_contents, item_id=item_id, key_value=key_value) item_post_treatment(item) yield item elif category["type"] == "contentRow": if 'title' in category: title = category['title'] + elif 'no title' not in category['context']['context_list_category']: + title = category['context']['context_list_category'] else: - title = json_parser["page"]["displayName"] + title = category['context']['context_list_id'] + continue key_value = category['reactKey'] item = Listitem() item.label = title - item.set_callback( - list_contents, item_id=item_id, key_value=key_value) + item.set_callback(list_contents, item_id=item_id, key_value=key_value) item_post_treatment(item) yield item @@ -158,11 +175,10 @@ def list_categories(plugin, item_id, **kwargs): @Route.register def list_contents(plugin, item_id, key_value, **kwargs): resp = urlquick.get(URL_REPLAY % item_id) - json_replay = re.compile( - r'window.__data=(.*?); window.app_config').findall(resp.text)[0] - json_parser = json.loads(json_replay) + react_query_state = REACT_QUERY_STATE.findall(resp.text)[0] + json_react_query_state = json.loads(react_query_state) - for category in json_parser["templates"]["landing"]["strates"]: + for category in json_react_query_state["queries"][0]["state"]["data"]["strates"]: if category['reactKey'] != key_value: continue @@ -411,8 +427,7 @@ def get_video_url(plugin, # Get Portail Id session_requests = requests.session() resp_app_config = session_requests.get(URL_REPLAY % item_id) - json_app_config = re.compile('window.app_config=(.*?)};').findall( - resp_app_config.text)[0] + json_app_config = re.compile('window.app_config=(.*?)};').findall(resp_app_config.text)[0] json_app_config_parser = json.loads(json_app_config + ('}')) portail_id = json_app_config_parser["api"]["pass"]["portailIdEncrypted"] @@ -462,7 +477,9 @@ def get_video_url(plugin, return False for stream_datas in value_datas_jsonparser["available"]: - if stream_datas['drmType'] == "DRM MKPC Widevine DASH" or stream_datas['drmType'] == "Non protégé": + if stream_datas['drmType'] == "DRM MKPC Widevine DASH" \ + or stream_datas['drmType'] == "Non protégé" \ + or stream_datas['drmType'] == "UNPROTECTED": payload = { 'comMode': stream_datas['comMode'], 'contentId': stream_datas['contentId'], @@ -490,8 +507,8 @@ def get_video_url(plugin, jsonparser_stream_datas = session_requests.put( URL_STREAM_DATAS, data=payload, headers=headers).json() - jsonparser_real_stream_datas = session_requests.get( - jsonparser_stream_datas['@medias'], headers=headers).json() + jsonparser_real_stream_datas = session_requests.get(SECURE_GEN_HAPI + + jsonparser_stream_datas['@medias'], headers=headers).json() certificate_data = base64.b64encode(requests.get(CERTIFICATE_URL).content).decode('utf-8') @@ -499,9 +516,15 @@ def get_video_url(plugin, item = Listitem() stream_data_type = "VM" if 'VM' in jsonparser_real_stream_datas else "VF" - item.path = jsonparser_real_stream_datas[stream_data_type][0]["media"][0]["distribURL"] + if stream_data_type in jsonparser_real_stream_datas: + first_stream = jsonparser_real_stream_datas[stream_data_type][0] + item.path = first_stream["media"][0]["distribURL"] + else: + first_stream = jsonparser_real_stream_datas[0] + item.path = first_stream["files"][0]["distribURL"] + if plugin.setting.get_boolean('active_subtitle'): - for asset in jsonparser_real_stream_datas[stream_data_type][0]["files"]: + for asset in first_stream["files"]: if 'vtt' in asset["mimeType"]: subtitle_url = asset['distribURL'] From 985f436d0cd978aee15ded1964f5d1b23d947ebc Mon Sep 17 00:00:00 2001 From: darodi <4682830+darodi@users.noreply.github.com> Date: Sat, 10 Jun 2023 22:42:09 +0200 Subject: [PATCH 2/8] [FR - MYCANAL] new url and APIs --- resources/lib/channels/fr/mycanal.py | 59 +++++++++++----------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/resources/lib/channels/fr/mycanal.py b/resources/lib/channels/fr/mycanal.py index 54eeeaa1b..feebe03d5 100644 --- a/resources/lib/channels/fr/mycanal.py +++ b/resources/lib/channels/fr/mycanal.py @@ -36,18 +36,12 @@ get_selected_item_info, INPUTSTREAM_PROP from resources.lib.menu_utils import item_post_treatment -# URL : URL_ROOT_SITE = 'https://www.canalplus.com' -# Channel - -# Replay channel : URL_REPLAY = URL_ROOT_SITE + '/chaines/%s' # Channel name URL_TOKEN = 'https://pass-api-v2.canal-plus.com/services/apipublique/createToken' -LIVE_TOKEN_URL = 'https://secure-browser.canalplus-bo.net/WebPortal/ottlivetv/api/V4/zones/cpfra/devices/3/apps/1/jobs/InitLiveTV' - SECURE_GEN_HAPI = 'https://secure-gen-hapi.canal-plus.com' URL_VIDEO_DATAS = SECURE_GEN_HAPI + '/conso/playset/unit/%s' URL_STREAM_DATAS = SECURE_GEN_HAPI + '/conso/view' @@ -60,7 +54,9 @@ 'edge': 'routemeup.canalplus-bo.net' } -LICENSE_URL = 'https://secure-browser.canalplus-bo.net/WebPortal/ottlivetv/api/V4/zones/cpfra/devices/31/apps/1/jobs/GetLicence' +CANALPLUS_BO_NET_API = 'https://secure-browser.canalplus-bo.net/WebPortal/ottlivetv/api/V4' +LIVE_TOKEN_URL = CANALPLUS_BO_NET_API + '/zones/cpfra/devices/3/apps/1/jobs/InitLiveTV' +LICENSE_URL = CANALPLUS_BO_NET_API + '/zones/cpfra/devices/31/apps/1/jobs/GetLicence' CERTIFICATE_URL = 'https://secure-webtv-static.canal-plus.com/widevine/cert/cert_license_widevine_com.bin' @@ -425,10 +421,11 @@ def get_video_url(plugin, deviceKeyId, device_id_first, sessionId = getKeyID() # Get Portail Id + # session_requests = urlquick.Session() session_requests = requests.session() resp_app_config = session_requests.get(URL_REPLAY % item_id) json_app_config = re.compile('window.app_config=(.*?)};').findall(resp_app_config.text)[0] - json_app_config_parser = json.loads(json_app_config + ('}')) + json_app_config_parser = json.loads(json_app_config + '}') portail_id = json_app_config_parser["api"]["pass"]["portailIdEncrypted"] headers = {"User-Agent": web_utils.get_random_ua(), @@ -449,7 +446,7 @@ def get_video_url(plugin, device_id = json_token_parser["response"]["userData"]["deviceId"].split(':')[0] video_id = next_url.split('/')[-1].split('.json')[0] - headers = { + secure_gen_hapi_headers = { 'Accept': 'application/json, text/plain, */*', 'Authorization': 'PASS Token="%s"' % pass_token, 'Content-Type': 'application/json; charset=UTF-8', @@ -459,6 +456,7 @@ def get_video_url(plugin, 'XX-Profile-Id': '0', 'XX-SERVICE': 'mycanal', 'User-Agent': web_utils.get_random_ua(), + 'Origin': 'https://www.mycanal.fr' } # Fix an ssl issue on some device. @@ -469,7 +467,7 @@ def get_video_url(plugin, # no pyopenssl support used / needed / available pass - value_datas_jsonparser = session_requests.get(URL_VIDEO_DATAS % video_id, headers=headers).json() + value_datas_jsonparser = session_requests.get(URL_VIDEO_DATAS % video_id, headers=secure_gen_hapi_headers).json() if 'available' not in value_datas_jsonparser: # Some videos required an account @@ -493,22 +491,13 @@ def get_video_url(plugin, break payload = json.dumps(payload) - headers = {'Accept': 'application/json, text/plain, */*', - 'Authorization': 'PASS Token="%s"' % pass_token, - 'Content-Type': 'application/json; charset=UTF-8', - 'XX-DEVICE': 'pc %s' % device_id, - 'XX-DOMAIN': 'cpfra', - 'XX-OPERATOR': 'pc', - 'XX-Profile-Id': '0', - 'XX-SERVICE': 'mycanal', - 'User-Agent': web_utils.get_random_ua(), - } jsonparser_stream_datas = session_requests.put( - URL_STREAM_DATAS, data=payload, headers=headers).json() + URL_STREAM_DATAS, data=payload, headers=secure_gen_hapi_headers).json() jsonparser_real_stream_datas = session_requests.get(SECURE_GEN_HAPI + - jsonparser_stream_datas['@medias'], headers=headers).json() + jsonparser_stream_datas['@medias'], + headers=secure_gen_hapi_headers).json() certificate_data = base64.b64encode(requests.get(CERTIFICATE_URL).content).decode('utf-8') @@ -521,11 +510,19 @@ def get_video_url(plugin, item.path = first_stream["media"][0]["distribURL"] else: first_stream = jsonparser_real_stream_datas[0] - item.path = first_stream["files"][0]["distribURL"] + files = first_stream["files"] + found_file = False + for file in files: + if file["type"] == 'video': + item.path = file["distribURL"] + found_file = True + break + if not found_file: + return False if plugin.setting.get_boolean('active_subtitle'): for asset in first_stream["files"]: - if 'vtt' in asset["mimeType"]: + if 'vtt' in asset["mimeType"] or asset["type"] == 'subtitle': subtitle_url = asset['distribURL'] item.label = get_selected_item_label() @@ -539,18 +536,8 @@ def get_video_url(plugin, if ".mpd" in item.path: item.property['inputstream.adaptive.manifest_type'] = 'mpd' item.property['inputstream.adaptive.license_type'] = 'com.widevine.alpha' - headers2 = { - 'Accept': 'application/json, text/plain, */*', - 'Authorization': 'PASS Token="%s"' % pass_token, - 'Content-Type': 'text/plain', - 'User-Agent': web_utils.get_random_ua(), - 'Origin': 'https://www.mycanal.fr', - 'XX-DEVICE': 'pc %s' % device_id, - 'XX-DOMAIN': 'cpfra', - 'XX-OPERATOR': 'pc', - 'XX-Profile-Id': '0', - 'XX-SERVICE': 'mycanal', - } + headers2 = secure_gen_hapi_headers.copy() + headers2.update({'Content-Type': 'text/plain'}) with xbmcvfs.File('special://userdata/addon_data/plugin.video.catchuptvandmore/headersCanal', 'wb') as f1: pickle.dump(headers2, f1) From 4b3eb0e84c7d3135c0d567ba3e105058248a04e4 Mon Sep 17 00:00:00 2001 From: darodi <4682830+darodi@users.noreply.github.com> Date: Sat, 29 Jul 2023 10:31:41 +0200 Subject: [PATCH 3/8] [FR - MYCANAL] new url and APIs --- resources/lib/channels/fr/mycanal.py | 40 +++++++++++++++++++--------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/resources/lib/channels/fr/mycanal.py b/resources/lib/channels/fr/mycanal.py index feebe03d5..324019845 100644 --- a/resources/lib/channels/fr/mycanal.py +++ b/resources/lib/channels/fr/mycanal.py @@ -132,8 +132,6 @@ def list_categories(plugin, item_id, **kwargs): resp = urlquick.get(URL_REPLAY % item_id) # window_data = WINDOW_DATA.findall(resp.text)[0] # json_window_data = json.loads(window_data) - react_query_state = REACT_QUERY_STATE.findall(resp.text)[0] - json_react_query_state = json.loads(react_query_state) # for category in json_window_data["application"]["navigation"]: # title = category['displayName'] @@ -143,10 +141,13 @@ def list_categories(plugin, item_id, **kwargs): # item_post_treatment(item) # yield item + react_query_state = REACT_QUERY_STATE.findall(resp.text)[0] + json_react_query_state = json.loads(react_query_state) + for category in json_react_query_state["queries"][0]["state"]["data"]["strates"]: if category["type"] == "carrousel": title = category['context']['context_page_title'] - key_value = category['reactKey'] + key_value = category['reactKey'] if 'reactKey' in category else None item = Listitem() item.label = title item.set_callback(list_contents, item_id=item_id, key_value=key_value) @@ -158,9 +159,9 @@ def list_categories(plugin, item_id, **kwargs): elif 'no title' not in category['context']['context_list_category']: title = category['context']['context_list_category'] else: - title = category['context']['context_list_id'] + # title = category['context']['context_list_id'] continue - key_value = category['reactKey'] + key_value = category['reactKey'] if 'reactKey' in category else None item = Listitem() item.label = title item.set_callback(list_contents, item_id=item_id, key_value=key_value) @@ -175,7 +176,7 @@ def list_contents(plugin, item_id, key_value, **kwargs): json_react_query_state = json.loads(react_query_state) for category in json_react_query_state["queries"][0]["state"]["data"]["strates"]: - if category['reactKey'] != key_value: + if 'reactKey' in category and category['reactKey'] != key_value: continue for content in category["contents"]: @@ -441,7 +442,8 @@ def get_video_url(plugin, 'sessionId': sessionId, } - json_token_parser = session_requests.post(URL_TOKEN, data=payload, headers=headers).json() + resp = session_requests.post(URL_TOKEN, data=payload, headers=headers) + json_token_parser = resp.json() pass_token = json_token_parser["response"]["passToken"] device_id = json_token_parser["response"]["userData"]["deviceId"].split(':')[0] @@ -467,7 +469,8 @@ def get_video_url(plugin, # no pyopenssl support used / needed / available pass - value_datas_jsonparser = session_requests.get(URL_VIDEO_DATAS % video_id, headers=secure_gen_hapi_headers).json() + value_datas_jsonparser = session_requests.get(URL_VIDEO_DATAS % video_id, + headers=secure_gen_hapi_headers).json() if 'available' not in value_datas_jsonparser: # Some videos required an account @@ -475,9 +478,7 @@ def get_video_url(plugin, return False for stream_datas in value_datas_jsonparser["available"]: - if stream_datas['drmType'] == "DRM MKPC Widevine DASH" \ - or stream_datas['drmType'] == "Non protégé" \ - or stream_datas['drmType'] == "UNPROTECTED": + if 'drmType' in stream_datas and is_valid_drm(stream_datas['drmType']): payload = { 'comMode': stream_datas['comMode'], 'contentId': stream_datas['contentId'], @@ -495,6 +496,13 @@ def get_video_url(plugin, jsonparser_stream_datas = session_requests.put( URL_STREAM_DATAS, data=payload, headers=secure_gen_hapi_headers).json() + if ('@medias' not in jsonparser_stream_datas + and 'message' in jsonparser_stream_datas + and 'status' in jsonparser_stream_datas + and jsonparser_stream_datas['status'] != 200): + xbmcgui.Dialog().ok('Info', jsonparser_stream_datas['message']) + return False + jsonparser_real_stream_datas = session_requests.get(SECURE_GEN_HAPI + jsonparser_stream_datas['@medias'], headers=secure_gen_hapi_headers).json() @@ -554,6 +562,14 @@ def get_video_url(plugin, return json_parser["detail"]["informations"]["playsets"]["available"][0]["videoURL"] +def is_valid_drm(drm_type): + return (drm_type == "DRM MKPC Widevine DASH" + or drm_type == "Non protégé" + or drm_type == "UNPROTECTED" + or drm_type == 'DRM_MKPC_WIDEVINE_DASH' + or drm_type == 'DRM_WIDEVINE') + + @Resolver.register def get_live_url(plugin, item_id, **kwargs): if xbmcgui.Dialog().select(Script.localize(30174), ["MyCanal", "Dailymotion"]): @@ -567,7 +583,7 @@ def get_live_url(plugin, item_id, **kwargs): json_app_config = re.compile('window.app_config=(.*?)};').findall( resp_app_config.text)[0] - json_app_config_parser = json.loads(json_app_config + ('}')) + json_app_config_parser = json.loads(json_app_config + '}') portail_id = json_app_config_parser["api"]["pass"]["portailIdEncrypted"] data = { From 7f3fadee51c20d222da797b4faa850979e95fe96 Mon Sep 17 00:00:00 2001 From: darodi <4682830+darodi@users.noreply.github.com> Date: Tue, 1 Aug 2023 22:30:50 +0200 Subject: [PATCH 4/8] [FR - MYCANAL] new url and APIs --- resources/lib/channels/fr/mycanal.py | 469 ++++++++++++++++----------- 1 file changed, 279 insertions(+), 190 deletions(-) diff --git a/resources/lib/channels/fr/mycanal.py b/resources/lib/channels/fr/mycanal.py index 324019845..2780afeb1 100644 --- a/resources/lib/channels/fr/mycanal.py +++ b/resources/lib/channels/fr/mycanal.py @@ -24,6 +24,7 @@ try: from urllib.parse import quote, urlencode except ImportError: + # noinspection PyUnresolvedReferences from urllib import quote, urlencode # noinspection PyUnresolvedReferences from codequick import Listitem, Resolver, Route, Script @@ -36,16 +37,23 @@ get_selected_item_info, INPUTSTREAM_PROP from resources.lib.menu_utils import item_post_treatment -URL_ROOT_SITE = 'https://www.canalplus.com' -URL_REPLAY = URL_ROOT_SITE + '/chaines/%s' +URL_REPLAY_CHANNEL = 'https://www.canalplus.com/chaines/%s' # Channel name + +OFFER_ZONE = "cpfra" +DEVICE_ID = "3" +DRM_ID = "31" +OFFER_LOCATION = 'fr' + URL_TOKEN = 'https://pass-api-v2.canal-plus.com/services/apipublique/createToken' SECURE_GEN_HAPI = 'https://secure-gen-hapi.canal-plus.com' URL_VIDEO_DATAS = SECURE_GEN_HAPI + '/conso/playset/unit/%s' URL_STREAM_DATAS = SECURE_GEN_HAPI + '/conso/view' +# URL_JSON = "https://routemeup.canalplus-bo.net/plfiles/v2/metr/dash-ssl/%s-hd.json?token=%s" +# URL_JSON = "https://routemeup.canalplus-bo.net/plfiles/v2/metr/dash-ssl/%s-hd.json" URL_JSON = "https://dsh-m013.ora02.live-scy.canalplus-cdn.net/plfiles/v2/metr/dash-ssl/%s-hd.json" PARAMS_URL_JSON = { @@ -55,9 +63,11 @@ } CANALPLUS_BO_NET_API = 'https://secure-browser.canalplus-bo.net/WebPortal/ottlivetv/api/V4' -LIVE_TOKEN_URL = CANALPLUS_BO_NET_API + '/zones/cpfra/devices/3/apps/1/jobs/InitLiveTV' LICENSE_URL = CANALPLUS_BO_NET_API + '/zones/cpfra/devices/31/apps/1/jobs/GetLicence' +CANALPLUSTECH_PRO_API = 'https://ltv.slc-app-aka.prod.bo.canal.canalplustech.pro/api/V4' +LIVE_TOKEN_URL = CANALPLUSTECH_PRO_API + '/zones/cpfra/devices/3/apps/1/jobs/InitLiveTV' + CERTIFICATE_URL = 'https://secure-webtv-static.canal-plus.com/widevine/cert/cert_license_widevine_com.bin' # The channel need to be on the same order has the website @@ -78,16 +88,16 @@ WINDOW_DATA = re.compile(r'window.__data=(.*?);\s*window.REACT_QUERY_STATE') -def getKeyID(): +def get_key_id(): def rnd(): return str(hex(math.floor((1 + random.random()) * 9007199254740991)))[4:] ts = int(1000 * time.time()) - deviceKeyId = str(ts) + '-' + rnd() - deviceId = deviceKeyId + ':0:' + str(ts + 2000) + '-' + rnd() - sessionId = str(ts + 3000) + '-' + rnd() - return deviceKeyId, deviceId, sessionId + device_key_id = str(ts) + '-' + rnd() + device_id_full = device_key_id + ':0:' + str(ts + 2000) + '-' + rnd() + session_id = str(ts + 3000) + '-' + rnd() + return device_key_id, device_id_full, session_id @Route.register @@ -114,13 +124,13 @@ def mycanal_root(plugin, **kwargs): item.label = channel_infos[1] item.art["thumb"] = get_item_media_path('channels/fr/' + channel_infos[2]) item.art["fanart"] = get_item_media_path('channels/fr/' + channel_infos[3]) - item.set_callback(list_categories, channel_infos[0]) + item.set_callback(list_channel, channel_infos[0]) item_post_treatment(item) yield item @Route.register -def list_categories(plugin, item_id, **kwargs): +def list_channel(plugin, item_id, **kwargs): """ Build categories listing - Tous les programmes @@ -129,7 +139,7 @@ def list_categories(plugin, item_id, **kwargs): - ... """ - resp = urlquick.get(URL_REPLAY % item_id) + resp = urlquick.get(URL_REPLAY_CHANNEL % item_id) # window_data = WINDOW_DATA.findall(resp.text)[0] # json_window_data = json.loads(window_data) @@ -150,7 +160,7 @@ def list_categories(plugin, item_id, **kwargs): key_value = category['reactKey'] if 'reactKey' in category else None item = Listitem() item.label = title - item.set_callback(list_contents, item_id=item_id, key_value=key_value) + item.set_callback(list_contents, item_id=item_id, key_value=key_value, category=category) item_post_treatment(item) yield item elif category["type"] == "contentRow": @@ -162,48 +172,58 @@ def list_categories(plugin, item_id, **kwargs): # title = category['context']['context_list_id'] continue key_value = category['reactKey'] if 'reactKey' in category else None + if 'strateMode' in category and category['strateMode'] == 'liveTV': + continue item = Listitem() item.label = title - item.set_callback(list_contents, item_id=item_id, key_value=key_value) + item.set_callback(list_contents, item_id=item_id, key_value=key_value, category=category) item_post_treatment(item) yield item @Route.register -def list_contents(plugin, item_id, key_value, **kwargs): - resp = urlquick.get(URL_REPLAY % item_id) - react_query_state = REACT_QUERY_STATE.findall(resp.text)[0] - json_react_query_state = json.loads(react_query_state) +def list_contents(plugin, item_id, key_value, category, **kwargs): - for category in json_react_query_state["queries"][0]["state"]["data"]["strates"]: - if 'reactKey' in category and category['reactKey'] != key_value: - continue + if category is None: + if key_value is None: + return False + category = find_category(item_id, key_value) - for content in category["contents"]: - if content["type"] == 'article': - continue + if category is None: + return False - content_title = content["onClick"]["displayName"] - content_image = '' - if 'URLImageOptimizedRegular' in content_image: - if 'http' in content["URLImageOptimizedRegular"]: - content_image = content["URLImageOptimizedRegular"] - else: - content_image = content["URLImage"] + for content in category["contents"]: + if content["type"] == 'article': + continue + + content_title = content["onClick"]["displayName"] + content_image = '' + if 'URLImageOptimizedRegular' in content_image: + if 'http' in content["URLImageOptimizedRegular"]: + content_image = content["URLImageOptimizedRegular"] else: - if 'URLImage' in content: - content_image = content["URLImage"] - content_url = content["onClick"]["URLPage"] + content_image = content["URLImage"] + else: + if 'URLImage' in content: + content_image = content["URLImage"] + content_url = content["onClick"]["URLPage"] - item = Listitem() - item.label = content_title - item.art['thumb'] = item.art['landscape'] = content_image - item.set_callback( - list_programs, - item_id=item_id, - next_url=content_url) - item_post_treatment(item) - yield item + item = Listitem() + item.label = content_title + item.art['thumb'] = item.art['landscape'] = content_image + item.set_callback(list_programs, item_id=item_id, next_url=content_url) + item_post_treatment(item) + yield item + + +def find_category(item_id, key_value): + resp = urlquick.get(URL_REPLAY_CHANNEL % item_id) + react_query_state = REACT_QUERY_STATE.findall(resp.text)[0] + json_react_query_state = json.loads(react_query_state) + for category in json_react_query_state["queries"][0]["state"]["data"]["strates"]: + if 'reactKey' in category and category['reactKey'] == key_value: + return category + return None @Route.register @@ -221,11 +241,7 @@ def list_programs(plugin, item_id, next_url, **kwargs): item = Listitem() item.label = strate_title - item.set_callback( - list_sub_programs, - item_id=item_id, - next_url=next_url, - strate_title=strate_title) + item.set_callback(list_sub_programs, item_id=item_id, next_url=next_url, strate_title=strate_title) item_post_treatment(item) yield item @@ -236,9 +252,7 @@ def list_programs(plugin, item_id, next_url, **kwargs): for video_datas in json_parser["episodes"]['contents']: video_title = program_title + ' - ' + video_datas['title'] video_image = video_datas['URLImage'] - video_plot = '' - if 'summary' in video_datas: - video_plot = video_datas['summary'] + video_plot = video_datas['summary'] if 'summary' in video_datas else '' if 'contentAvailability' in video_datas: video_url = video_datas["contentAvailability"]["availabilities"]["stream"]["URLMedias"] else: @@ -289,9 +303,7 @@ def list_programs(plugin, item_id, next_url, **kwargs): else: video_title = program_title + ' - ' + video_datas['title'] video_image = video_datas['URLImage'] - video_plot = '' - if 'summary' in video_datas: - video_plot = video_datas['summary'] + video_plot = video_datas['summary'] if 'summary' in video_datas else '' if 'contentAvailability' in video_datas: video_url = video_datas["contentAvailability"]["availabilities"]["stream"]["URLMedias"] else: @@ -375,9 +387,7 @@ def list_videos(plugin, item_id, next_url, **kwargs): for video_datas in json_parser["episodes"]['contents']: video_title = program_title + ' - ' + video_datas['title'] video_image = video_datas['URLImage'] - video_plot = '' - if 'summary' in video_datas: - video_plot = video_datas['summary'] + video_plot = video_datas['summary'] if 'summary' in video_datas else '' if 'contentAvailability' in video_datas: video_url = video_datas["contentAvailability"]["availabilities"]["stream"]["URLMedias"] else: @@ -411,41 +421,34 @@ def get_video_url(plugin, xbmcgui.Dialog().ok('Info', plugin.localize(30602)) return False - is_helper = inputstreamhelper.Helper('mpd', drm='widevine') - if not is_helper.check_inputstream(): - return False if download_mode: xbmcgui.Dialog().ok('Info', plugin.localize(30603)) return False - deviceKeyId, device_id_first, sessionId = getKeyID() + fix_cipher() + + device_key_id, device_id_full, session_id = get_key_id() - # Get Portail Id # session_requests = urlquick.Session() - session_requests = requests.session() - resp_app_config = session_requests.get(URL_REPLAY % item_id) - json_app_config = re.compile('window.app_config=(.*?)};').findall(resp_app_config.text)[0] - json_app_config_parser = json.loads(json_app_config + '}') - portail_id = json_app_config_parser["api"]["pass"]["portailIdEncrypted"] - - headers = {"User-Agent": web_utils.get_random_ua(), - "Origin": "https://www.canalplus.com", - "Referer": "https://www.canalplus.com/", } - - # Get PassToken - payload = { - 'deviceId': device_id_first, - 'vect': 'INTERNET', - 'media': 'web', - 'portailId': portail_id, - 'sessionId': sessionId, + session_requests = requests.sessions.Session() + + certificate_url, license_url, live_init, pass_url, portail_id = get_config(plugin, session_requests) + + data_pass = { + "deviceId": device_id_full, + "media": "web", + "noCache": "false", + "portailId": portail_id, + "sessionId": session_id, + "passIdType": "pass", + "vect": "INTERNET", + "offerZone": OFFER_ZONE, } - resp = session_requests.post(URL_TOKEN, data=payload, headers=headers) - json_token_parser = resp.json() - pass_token = json_token_parser["response"]["passToken"] - device_id = json_token_parser["response"]["userData"]["deviceId"].split(':')[0] + pass_token, device_id = get_pass_token(plugin, data_pass, pass_url, session_requests) + if pass_token is None: + return False video_id = next_url.split('/')[-1].split('.json')[0] secure_gen_hapi_headers = { @@ -453,7 +456,7 @@ def get_video_url(plugin, 'Authorization': 'PASS Token="%s"' % pass_token, 'Content-Type': 'application/json; charset=UTF-8', 'XX-DEVICE': 'pc %s' % device_id, - 'XX-DOMAIN': 'cpfra', + 'XX-DOMAIN': OFFER_ZONE, 'XX-OPERATOR': 'pc', 'XX-Profile-Id': '0', 'XX-SERVICE': 'mycanal', @@ -461,22 +464,16 @@ def get_video_url(plugin, 'Origin': 'https://www.mycanal.fr' } - # Fix an ssl issue on some device. - requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' - try: - requests.packages.urllib3.contrib.pyopenssl.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' - except AttributeError: - # no pyopenssl support used / needed / available - pass - value_datas_jsonparser = session_requests.get(URL_VIDEO_DATAS % video_id, headers=secure_gen_hapi_headers).json() if 'available' not in value_datas_jsonparser: - # Some videos required an account + # Some videos require an account # Get error return False + payload = None + drm_type = None for stream_datas in value_datas_jsonparser["available"]: if 'drmType' in stream_datas and is_valid_drm(stream_datas['drmType']): payload = { @@ -488,39 +485,41 @@ def get_video_url(plugin, 'functionalType': stream_datas['functionalType'], 'hash': stream_datas['hash'], 'idKey': stream_datas['idKey'], - 'quality': stream_datas['quality']} + 'quality': stream_datas['quality'] + } + drm_type = stream_datas['drmType'] break - payload = json.dumps(payload) - - jsonparser_stream_datas = session_requests.put( - URL_STREAM_DATAS, data=payload, headers=secure_gen_hapi_headers).json() + if payload is None: + return False - if ('@medias' not in jsonparser_stream_datas - and 'message' in jsonparser_stream_datas - and 'status' in jsonparser_stream_datas - and jsonparser_stream_datas['status'] != 200): - xbmcgui.Dialog().ok('Info', jsonparser_stream_datas['message']) + payload = json.dumps(payload) + json_stream_data = session_requests.put(URL_STREAM_DATAS, data=payload, + headers=secure_gen_hapi_headers).json() + + if ('@medias' not in json_stream_data + and 'message' in json_stream_data + and 'status' in json_stream_data + and json_stream_data['status'] != 200): + xbmcgui.Dialog().ok('Info', json_stream_data['message']) return False - jsonparser_real_stream_datas = session_requests.get(SECURE_GEN_HAPI + - jsonparser_stream_datas['@medias'], - headers=secure_gen_hapi_headers).json() + json_real_stream_data = session_requests.get(SECURE_GEN_HAPI + + json_stream_data['@medias'], + headers=secure_gen_hapi_headers).json() certificate_data = base64.b64encode(requests.get(CERTIFICATE_URL).content).decode('utf-8') - subtitle_url = '' item = Listitem() - stream_data_type = "VM" if 'VM' in jsonparser_real_stream_datas else "VF" + stream_data_type = "VM" if 'VM' in json_real_stream_data else "VF" - if stream_data_type in jsonparser_real_stream_datas: - first_stream = jsonparser_real_stream_datas[stream_data_type][0] + if stream_data_type in json_real_stream_data: + first_stream = json_real_stream_data[stream_data_type][0] item.path = first_stream["media"][0]["distribURL"] else: - first_stream = jsonparser_real_stream_datas[0] - files = first_stream["files"] + first_stream = json_real_stream_data[0] found_file = False - for file in files: + for file in first_stream["files"]: if file["type"] == 'video': item.path = file["distribURL"] found_file = True @@ -528,40 +527,62 @@ def get_video_url(plugin, if not found_file: return False - if plugin.setting.get_boolean('active_subtitle'): - for asset in first_stream["files"]: - if 'vtt' in asset["mimeType"] or asset["type"] == 'subtitle': - subtitle_url = asset['distribURL'] - item.label = get_selected_item_label() item.art.update(get_selected_item_art()) item.info.update(get_selected_item_info()) item.property[INPUTSTREAM_PROP] = 'inputstream.adaptive' - if 'http' in subtitle_url: - item.subtitles.append(subtitle_url) + set_subtitles(first_stream, item, plugin) + + if ".ism" in item.path: + is_helper = inputstreamhelper.Helper('ism') + if not is_helper.check_inputstream(): + return False + + item.property['inputstream.adaptive.manifest_type'] = 'ism' + # item.property['inputstream.adaptive.server_certificate'] = certificate_data + item.path += "/manifest" + return item if ".mpd" in item.path: - item.property['inputstream.adaptive.manifest_type'] = 'mpd' - item.property['inputstream.adaptive.license_type'] = 'com.widevine.alpha' - headers2 = secure_gen_hapi_headers.copy() - headers2.update({'Content-Type': 'text/plain'}) - - with xbmcvfs.File('special://userdata/addon_data/plugin.video.catchuptvandmore/headersCanal', 'wb') as f1: - pickle.dump(headers2, f1) - - # Return HTTP 200 but the response is not correctly interpreted by inputstream - # (https://github.com/peak3d/inputstream.adaptive/issues/267) - licence = "http://127.0.0.1:5057/license=" + jsonparser_stream_datas['@licence'] - licence += '?drmConfig=mkpl::true|%s|b{SSM}|B' % urlencode(headers2) - item.property['inputstream.adaptive.license_key'] = licence - item.property['inputstream.adaptive.server_certificate'] = certificate_data + + is_helper = inputstreamhelper.Helper('mpd', drm='widevine') + if not is_helper.check_inputstream(): + return False + + create_item_mpd(certificate_data, item, json_stream_data, secure_gen_hapi_headers) + return item json_parser = urlquick.get(next_url, headers={'User-Agent': web_utils.get_random_ua()}, max_age=-1).json() return json_parser["detail"]["informations"]["playsets"]["available"][0]["videoURL"] +def create_item_mpd(certificate_data, item, json_stream_data, secure_gen_hapi_headers): + item.property['inputstream.adaptive.manifest_type'] = 'mpd' + item.property['inputstream.adaptive.license_type'] = 'com.widevine.alpha' + headers = secure_gen_hapi_headers.copy() + headers.update({'Content-Type': 'text/plain'}) + with xbmcvfs.File('special://userdata/addon_data/plugin.video.catchuptvandmore/headersCanal', 'wb') as f1: + pickle.dump(headers, f1) + # Return HTTP 200 but the response is not correctly interpreted by inputstream + # (https://github.com/peak3d/inputstream.adaptive/issues/267) + licence = "http://127.0.0.1:5057/license=" + json_stream_data['@licence'] + licence += '?drmConfig=mkpl::true|%s|b{SSM}|B' % urlencode(headers) + item.property['inputstream.adaptive.license_key'] = licence + item.property['inputstream.adaptive.server_certificate'] = certificate_data + + +def set_subtitles(first_stream, item, plugin): + url = '' + if plugin.setting.get_boolean('active_subtitle'): + for file in first_stream["files"]: + if 'vtt' in file["mimeType"] or file["type"] == 'subtitle': + url = file['distribURL'] + if 'http' in url: + item.subtitles.append(url) + + def is_valid_drm(drm_type): return (drm_type == "DRM MKPC Widevine DASH" or drm_type == "Non protégé" @@ -575,94 +596,162 @@ def get_live_url(plugin, item_id, **kwargs): if xbmcgui.Dialog().select(Script.localize(30174), ["MyCanal", "Dailymotion"]): return resolver_proxy.get_stream_dailymotion(plugin, LIVE_DAILYMOTION[item_id], False) - deviceKeyId, deviceId, sessionId = getKeyID() + fix_cipher() + device_key_id, device_id_full, session_id = get_key_id() - resp_app_config = requests.get("https://www.canalplus.com/chaines/%s" % item_id) - EPGID = re.compile('\"epgidOTT\":\"(.+?)\"').findall( - resp_app_config.text)[0].split(",") + # session_requests = urlquick.Session() + session_requests = requests.sessions.Session() - json_app_config = re.compile('window.app_config=(.*?)};').findall( - resp_app_config.text)[0] - json_app_config_parser = json.loads(json_app_config + '}') - portail_id = json_app_config_parser["api"]["pass"]["portailIdEncrypted"] + certificate_url, license_url, live_init, pass_url, portail_id = get_config(plugin, session_requests) - data = { - "deviceId": deviceId, - "sessionId": sessionId, - "vect": "INTERNET", - "media": "PC", - "portailId": portail_id, - "zone": "cpfra", + resp_app_config = session_requests.get("https://www.canalplus.com/chaines/%s" % item_id) + epg_id = re.compile('\"epgidOTT\":\"(.+?)\"').findall(resp_app_config.text)[0].split(",") + + data_pass = { + "deviceId": device_id_full, + "media": "web", "noCache": "false", - "analytics": "false", - "trackingPub": "false", - "anonymousTracking": "true" + "portailId": portail_id, + "sessionId": session_id, + "passIdType": "pass", + "vect": "INTERNET", + "offerZone": OFFER_ZONE, } - hdr = { - "Content-Type": "application/x-www-form-urlencoded", - "Origin": "https://www.canalplus.com", - "Referer": "https://www.canalplus.com/", - "User-Agent": web_utils.get_random_ua() - } + pass_token, device_id = get_pass_token(plugin, data_pass, pass_url, session_requests) + if pass_token is None: + return False - resp = requests.post(URL_TOKEN, data=data, headers=hdr, timeout=3).json() - passToken = resp['response']['passToken'] - - data = { - "ServiceRequest": { - "InData": { - "DeviceKeyId": deviceKeyId, - "PassData": {"Id": 0, "Token": passToken}, - "PDSData": {"GroupTypes": "1;2;4"}, - "UserKeyId": "_tl1sb683u" - } - } - } - requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += 'HIGH:!DH:!aNULL' - try: - requests.packages.urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST += 'HIGH:!DH:!aNULL' - except AttributeError: - # no pyopenssl support used / needed / available - pass - resp = requests.post(LIVE_TOKEN_URL, json=data, headers=hdr).json() - liveToken = resp['ServiceResponse']['OutData']['LiveToken'] - - indexEPG = [i for i, t in enumerate(LIVE_MYCANAL) if t == item_id][0] + live_token = get_live_token(plugin, device_key_id, live_init, pass_token, session_requests) + if live_token is None: + return False + index_epg = [i for i, t in enumerate(LIVE_MYCANAL) if t == item_id][0] data_drm = quote('''{ "ServiceRequest": { "InData": { "ChallengeInfo": "b{SSM}", - "DeviceKeyId": "''' + deviceKeyId + '''", - "EpgId": ''' + EPGID[indexEPG] + ''', - "LiveToken": "''' + liveToken + '''", + "DeviceKeyId": "''' + device_key_id + '''", + "EpgId": ''' + epg_id[index_epg] + ''', + "LiveToken": "''' + live_token + '''", "Mode": "MKPL", "UserKeyId": "_vf1itm7yv" } } }''') - if item_id == "canalplus": item_id = "canalplusclair" + resp = session_requests.get(URL_JSON % item_id, params=PARAMS_URL_JSON) + if not resp: + plugin.notify(plugin.localize(30600), 'get dash-ssl response: empty') + return None + resp_json = resp.json() + url_stream = resp_json["primary"]["src"] - resp = requests.get(URL_JSON % item_id, params=PARAMS_URL_JSON).json() - url_stream = resp["primary"]["src"] + certificate_data = get_certificate_data(plugin, certificate_url, session_requests) + if certificate_data is None: + return False - certificate_data = base64.b64encode(requests.get(CERTIFICATE_URL).content).decode('utf-8') + return create_item_live(certificate_data, data_drm, license_url, url_stream) + +def create_item_live(certificate_data, data_drm, license_url, url_stream): + headers_licence = { + 'User-Agent': web_utils.get_random_ua(), + 'Content-Type': '' + } item = Listitem() item.label = get_selected_item_label() item.art.update(get_selected_item_art()) item.info.update(get_selected_item_info()) - item.path = url_stream item.property[INPUTSTREAM_PROP] = "inputstream.adaptive" - item.property['inputstream.adaptive.manifest_type'] = 'mpd' item.property['inputstream.adaptive.license_type'] = 'com.widevine.alpha' - item.property['inputstream.adaptive.license_key'] = LICENSE_URL + '||' + data_drm + '|JBLicenseInfo' + item.property['inputstream.adaptive.license_key'] = license_url + '|' + urlencode( + headers_licence) + '|' + data_drm + '|JBLicenseInfo' item.property['inputstream.adaptive.server_certificate'] = certificate_data return item + + +def get_certificate_data(plugin, certificate_url, session_requests): + headers = { + "User-Agent": web_utils.get_random_ua(), + "Accept": "application/json, text/plain, */*", + "referrer": "https://www.canalplus.com/" + } + resp = session_requests.get(certificate_url, headers=headers) + if not resp: + plugin.notify(plugin.localize(30600), 'get_certificate_data response: empty') + return None + return base64.b64encode(resp.content).decode('utf-8') + + +def get_live_token(plugin, device_key_id, live_init, pass_token, session_requests): + data = { + "ServiceRequest": { + "InData": { + "DeviceKeyId": device_key_id, + "PassData": {"Id": 0, "Token": pass_token}, + "PDSData": {"GroupTypes": "1;2;4"}, + "UserKeyId": "_tl1sb683u" + } + } + } + headers = { + "User-Agent": web_utils.get_random_ua(), + "Accept": "application/json, text/plain, */*", + "Content-Type": "application/json", + "referrer": "https://www.canalplus.com/" + } + resp = session_requests.post(live_init, json=data, headers=headers) + if not resp: + plugin.notify(plugin.localize(30600), 'get_pass_token response: empty') + return None + resp_json = resp.json() + return resp_json['ServiceResponse']['OutData']['LiveToken'] + + +def fix_cipher(): + # Fix a ssl issue on some device. + # noinspection PyUnresolvedReferences + requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' + try: + # noinspection PyUnresolvedReferences + requests.packages.urllib3.contrib.pyopenssl.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' + except AttributeError: + # no pyopenssl support used / needed / available + pass + + +def get_pass_token(plugin, data_pass, pass_url, session_requests): + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Origin": "https://www.canalplus.com", + "Referer": "https://www.canalplus.com/", + "User-Agent": web_utils.get_random_ua() + } + resp = session_requests.post(pass_url, data=data_pass, headers=headers, timeout=3) + if not resp: + plugin.notify(plugin.localize(30600), 'get_pass_token response: empty') + return None, None + resp_json = resp.json() + pass_token = resp_json['response']['passToken'] + device_id = resp_json["response"]["userData"]["deviceId"].split(':')[0] + return pass_token, device_id + + +def get_config(plugin, session_requests): + resp = session_requests.get("https://player.canalplus.com/one/configs/v2/10/mycanal/prod.json") + if not resp: + plugin.notify(plugin.localize(30600), 'get_config response: empty') + return None + resp_json = resp.json() + live_init = resp_json['live']['init'].format(offerZone=OFFER_ZONE, deviceId=DEVICE_ID) + pass_url = resp_json['pass']['url'].format(offerLocation=OFFER_LOCATION) + certificate_url = resp_json['drm']['certificates']['widevine'] + portail_id = resp_json['pass']['portailId'] + license_url = resp_json['live']['licence'].format(offerZone=OFFER_ZONE, drmId=DRM_ID) + return certificate_url, license_url, live_init, pass_url, portail_id From 8396b06952778efa3f322f0a87f156d32f387d1e Mon Sep 17 00:00:00 2001 From: darodi <4682830+darodi@users.noreply.github.com> Date: Wed, 2 Aug 2023 20:25:10 +0200 Subject: [PATCH 5/8] [FR - MYCANAL] new url and APIs --- resources/lib/channels/fr/mycanal.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/lib/channels/fr/mycanal.py b/resources/lib/channels/fr/mycanal.py index 2780afeb1..1bd5da4b1 100644 --- a/resources/lib/channels/fr/mycanal.py +++ b/resources/lib/channels/fr/mycanal.py @@ -183,7 +183,6 @@ def list_channel(plugin, item_id, **kwargs): @Route.register def list_contents(plugin, item_id, key_value, category, **kwargs): - if category is None: if key_value is None: return False @@ -421,7 +420,6 @@ def get_video_url(plugin, xbmcgui.Dialog().ok('Info', plugin.localize(30602)) return False - if download_mode: xbmcgui.Dialog().ok('Info', plugin.localize(30603)) return False @@ -539,6 +537,9 @@ def get_video_url(plugin, if not is_helper.check_inputstream(): return False + if drm_type != "UNPROTECTED": + pass # TODO + item.property['inputstream.adaptive.manifest_type'] = 'ism' # item.property['inputstream.adaptive.server_certificate'] = certificate_data item.path += "/manifest" From e804d671fdeae913a1194f3b716f78efa1a08255 Mon Sep 17 00:00:00 2001 From: darodi <4682830+darodi@users.noreply.github.com> Date: Wed, 2 Aug 2023 20:32:39 +0200 Subject: [PATCH 6/8] [FR - MYCANAL] new url and APIs --- resources/lib/channels/fr/mycanal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/channels/fr/mycanal.py b/resources/lib/channels/fr/mycanal.py index 1bd5da4b1..ac7890363 100644 --- a/resources/lib/channels/fr/mycanal.py +++ b/resources/lib/channels/fr/mycanal.py @@ -107,7 +107,8 @@ def mycanal_root(plugin, **kwargs): ('canalplus-en-clair', 'Canal +', 'canalplus.png', 'canalplus_fanart.jpg'), ('c8', 'C8', 'c8.png', 'c8_fanart.jpg'), ('cstar', 'CStar', 'cstar.png', 'cstar_fanart.jpg'), - ('seasons', 'Seasons', 'seasons.png', 'seasons_fanart.jpg'), + ('cnews', 'CNews', 'cnews.png', 'cnews_fanart.jpg'), + # ('seasons', 'Seasons', 'seasons.png', 'seasons_fanart.jpg'), ('comedie', 'Comédie +', 'comedie.png', 'comedie_fanart.jpg'), ('les-chaines-planete', 'Les chaînes planètes +', 'leschainesplanete.png', 'leschainesplanete_fanart.jpg'), ('golfplus', 'Golf +', 'golfplus.png', 'golfplus_fanart.jpg'), From d779a7244d35c0201886dca3b4762eb26c0690f6 Mon Sep 17 00:00:00 2001 From: darodi <4682830+darodi@users.noreply.github.com> Date: Wed, 2 Aug 2023 20:50:04 +0200 Subject: [PATCH 7/8] [FR - MYCANAL] new url and APIs --- resources/lib/channels/fr/mycanal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/channels/fr/mycanal.py b/resources/lib/channels/fr/mycanal.py index ac7890363..5e6306fb1 100644 --- a/resources/lib/channels/fr/mycanal.py +++ b/resources/lib/channels/fr/mycanal.py @@ -469,6 +469,7 @@ def get_video_url(plugin, if 'available' not in value_datas_jsonparser: # Some videos require an account # Get error + plugin.notify('ERROR', plugin.localize(30712)) return False payload = None From b05ff80cedfeccab3f68a508d18cb33e7bbc4731 Mon Sep 17 00:00:00 2001 From: darodi <4682830+darodi@users.noreply.github.com> Date: Fri, 4 Aug 2023 02:40:02 +0200 Subject: [PATCH 8/8] [FR - MYCANAL] new url and APIs - fix drm --- resources/lib/channels/fr/mycanal.py | 18 +++---- resources/lib/resolver_proxy.py | 70 +++++++++++++++++++--------- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/resources/lib/channels/fr/mycanal.py b/resources/lib/channels/fr/mycanal.py index 5e6306fb1..de57d4a13 100644 --- a/resources/lib/channels/fr/mycanal.py +++ b/resources/lib/channels/fr/mycanal.py @@ -37,7 +37,8 @@ get_selected_item_info, INPUTSTREAM_PROP from resources.lib.menu_utils import item_post_treatment -URL_REPLAY_CHANNEL = 'https://www.canalplus.com/chaines/%s' +URL_ROOT = 'https://www.canalplus.com/' +URL_REPLAY_CHANNEL = URL_ROOT + 'chaines/%s' # Channel name @@ -52,8 +53,6 @@ URL_VIDEO_DATAS = SECURE_GEN_HAPI + '/conso/playset/unit/%s' URL_STREAM_DATAS = SECURE_GEN_HAPI + '/conso/view' -# URL_JSON = "https://routemeup.canalplus-bo.net/plfiles/v2/metr/dash-ssl/%s-hd.json?token=%s" -# URL_JSON = "https://routemeup.canalplus-bo.net/plfiles/v2/metr/dash-ssl/%s-hd.json" URL_JSON = "https://dsh-m013.ora02.live-scy.canalplus-cdn.net/plfiles/v2/metr/dash-ssl/%s-hd.json" PARAMS_URL_JSON = { @@ -570,7 +569,7 @@ def create_item_mpd(certificate_data, item, json_stream_data, secure_gen_hapi_he pickle.dump(headers, f1) # Return HTTP 200 but the response is not correctly interpreted by inputstream # (https://github.com/peak3d/inputstream.adaptive/issues/267) - licence = "http://127.0.0.1:5057/license=" + json_stream_data['@licence'] + licence = "http://127.0.0.1:5057/license=" + SECURE_GEN_HAPI + json_stream_data['@licence'] licence += '?drmConfig=mkpl::true|%s|b{SSM}|B' % urlencode(headers) item.property['inputstream.adaptive.license_key'] = licence item.property['inputstream.adaptive.server_certificate'] = certificate_data @@ -607,7 +606,7 @@ def get_live_url(plugin, item_id, **kwargs): certificate_url, license_url, live_init, pass_url, portail_id = get_config(plugin, session_requests) - resp_app_config = session_requests.get("https://www.canalplus.com/chaines/%s" % item_id) + resp_app_config = session_requests.get(URL_REPLAY_CHANNEL % item_id) epg_id = re.compile('\"epgidOTT\":\"(.+?)\"').findall(resp_app_config.text)[0].split(",") data_pass = { @@ -630,6 +629,7 @@ def get_live_url(plugin, item_id, **kwargs): return False index_epg = [i for i, t in enumerate(LIVE_MYCANAL) if t == item_id][0] + # body_data = urllib.parse.quote(json.dumps(dict_data)) data_drm = quote('''{ "ServiceRequest": { @@ -683,7 +683,7 @@ def get_certificate_data(plugin, certificate_url, session_requests): headers = { "User-Agent": web_utils.get_random_ua(), "Accept": "application/json, text/plain, */*", - "referrer": "https://www.canalplus.com/" + "referrer": URL_ROOT } resp = session_requests.get(certificate_url, headers=headers) if not resp: @@ -707,7 +707,7 @@ def get_live_token(plugin, device_key_id, live_init, pass_token, session_request "User-Agent": web_utils.get_random_ua(), "Accept": "application/json, text/plain, */*", "Content-Type": "application/json", - "referrer": "https://www.canalplus.com/" + "referrer": URL_ROOT } resp = session_requests.post(live_init, json=data, headers=headers) if not resp: @@ -732,8 +732,8 @@ def fix_cipher(): def get_pass_token(plugin, data_pass, pass_url, session_requests): headers = { "Content-Type": "application/x-www-form-urlencoded", - "Origin": "https://www.canalplus.com", - "Referer": "https://www.canalplus.com/", + "Origin": URL_ROOT, + "Referer": URL_ROOT, "User-Agent": web_utils.get_random_ua() } resp = session_requests.post(pass_url, data=data_pass, headers=headers, timeout=3) diff --git a/resources/lib/resolver_proxy.py b/resources/lib/resolver_proxy.py index 822e47fd1..2c995ddfc 100644 --- a/resources/lib/resolver_proxy.py +++ b/resources/lib/resolver_proxy.py @@ -175,13 +175,13 @@ def get_stream_with_quality(plugin, if plugin.setting.get_boolean('use_ytdl_stream'): return get_stream_default(plugin, video_url, False) - else: - return __get_non_ia_stream_with_quality(plugin, video_url, - manifest_type=manifest_type, - headers=headers, - map_audio=map_audio, - append_query_string=append_query_string, - verify=verify, subtitles=subtitles) + + return __get_non_ia_stream_with_quality(plugin, video_url, + manifest_type=manifest_type, + headers=headers, + map_audio=map_audio, + append_query_string=append_query_string, + verify=verify, subtitles=subtitles) is_helper = inputstreamhelper.Helper(manifest_type, drm='widevine') if not is_helper.check_inputstream(): @@ -201,19 +201,9 @@ def get_stream_with_quality(plugin, item.property['ResumeTime'] = '43200' # 12 hours buffer, can be changed if not enough item.property['TotalTime'] = workaround - # Useless with Kodi 20 and adaptive stream. - # IA detect the bandwidth and choose the right stream iself - if get_kodi_version() < 20: - # set max bandwidth - stream_bitrate_limit = plugin.setting.get_int('stream_bitrate_limit') - if stream_bitrate_limit > 0: - item.property["inputstream.adaptive.max_bandwidth"] = str(stream_bitrate_limit * 1000) - elif manifest_type == "hls" and Quality['BEST'] != plugin.setting.get_string('quality') and bypass is False: - url, bitrate = M3u8(video_url, headers).get_url_and_bitrate_for_quality() - if url is None and bitrate is None: - return False - if bitrate != 0: - item.property["inputstream.adaptive.max_bandwidth"] = str(bitrate * 1000) + is_ok = __set_ia_quality(plugin, video_url, bypass, headers, item, manifest_type) + if not is_ok: + return False stream_headers = urlencode(headers) item.property['inputstream.adaptive.stream_headers'] = stream_headers @@ -223,7 +213,7 @@ def get_stream_with_quality(plugin, item.property['inputstream.adaptive.license_key'] = license_url if input_stream_properties is not None: - if "manifest_update_parameter" in input_stream_properties: + if "manifest_update_parameter" in input_stream_properties and get_kodi_version() < 21: item.property['inputstream.adaptive.manifest_update_parameter'] = input_stream_properties[ "manifest_update_parameter"] @@ -240,6 +230,44 @@ def get_stream_with_quality(plugin, return item +def __set_ia_quality(plugin, video_url, bypass, headers, item, manifest_type) -> bool: + """ + @param plugin: plugin + @param video_url: video url + @param bypass: use IA to read stream with only one resolution + @param headers: the headers + @param item: the item on which the quality should be set + @param manifest_type: manifest type + + @return: boolean: false when quality is not chosen in dialog box + """ + if get_kodi_version() < 20: + stream_bitrate_limit = plugin.setting.get_int('stream_bitrate_limit') + if stream_bitrate_limit > 0: + item.property["inputstream.adaptive.max_bandwidth"] = str(stream_bitrate_limit * 1000) + elif manifest_type == "hls" and Quality['BEST'] != plugin.setting.get_string('quality') and bypass is False: + url, bitrate = M3u8(video_url, headers).get_url_and_bitrate_for_quality() + if url is None and bitrate is None: + return False + if bitrate != 0: + item.property["inputstream.adaptive.max_bandwidth"] = str(bitrate * 1000) + else: + # see https://github.com/xbmc/inputstream.adaptive/wiki/ \ + # Stream-selection-types-properties#inputstreamadaptivechooser_bandwidth_max + stream_bitrate_limit = plugin.setting.get_int('stream_bitrate_limit') + if stream_bitrate_limit > 0: + item.property['inputstream.adaptive.stream_selection_type'] = 'adaptive' + item.property['inputstream.adaptive.chooser_bandwidth_max'] = str(stream_bitrate_limit * 1000) + elif Quality['DIALOG'] == plugin.setting.get_string('quality'): + item.property['inputstream.adaptive.stream_selection_type'] = 'ask-quality' + elif Quality['WORST'] == plugin.setting.get_string('quality'): + item.property['inputstream.adaptive.stream_selection_type'] = 'fixed-res' + item.property['inputstream.adaptive.chooser_resolution_max'] = '480p' + item.property['inputstream.adaptive.chooser_resolution_secure_max'] = '480p' + + return True + + def get_stream_default(plugin, video_url, download_mode=False):