MovixOpenSource/API/proxiesembed/drmproxy/services/canalplus_com.py
Movix 1401dc20f3 tird release
mon compte github a été détecté comme du spam je crois
2026-04-28 17:55:00 +02:00

928 lines
36 KiB
Python

import base64
import builtins
import json
import re
import threading
from os.path import join
from urllib.parse import parse_qs, urlparse
import requests
import xmltodict
from curl_cffi import requests as requests_cf
from utils.constants.macros import WIDEVINE_SCHEME_ID, ERR_MSG, USER_ERROR, PLAYREADY_SCHEME_ID, CACHE_DIR
from utils.main_service import main_service
from utils.structs import BaseElement, BaseService, CustomException
from utils.tools.args import check_range
from utils.tools.common import get_valid_filename, update_url_params, dict_to_file, file_to_dict
class canalplus_com(BaseService):
DEMO_URLS = [
"https://www.canalplus.com/divertissement/hot-ones-jerome-niel-vrille-completement/h/21582462_50001",
"https://www.canalplus.com/divertissement/pierre-niney-veritable-justicier-masque-dans-le-comte-de-monte-christo-canal/h/25788986_50001",
"https://www.canalplus.com/divertissement/hot-ones/h/18582832_50001",
"https://www.canalplus.com/series/bref/h/4603293_50001",
"https://www.canalplus.com/series/d-argent-et-de-sang/h/22860111_50001/bonus/",
"https://www.canalplus.com/live/?channel=450",
"https://www.canalplus.com/pl/seria/the-office-pl/h/11800604_70026/explorer/bonus/",
"https://www.canalplus.com/pl/seriale/za-kulisami-serialu-powrot-belfra/h/14153942_70033",
"https://www.canalplus.com/pl/seriale/powrot-sezon-1/h/12397026_70033",
]
CONFIG_URL = 'https://player.canalplus.com/one/configs/v2/11'
MAP_URL = CONFIG_URL + '/mapping/mapping.json'
PROD_URL = CONFIG_URL + '/{zone_value}/prod.json'
ENDPOINT_URLS = None
BASE_URL = "https://www.canalplus.com"
VIDEO_URL = BASE_URL + "{language}/h/{video_id}"
DIST_MODES = 'catchup,live,svod,tvod,posttvod'
DEVICE_ID = 3
DRM_ID = 31
ECK = "pc"
DEVICE = "pc"
DEFAULT_GROUP_TYPE = "1"
DEFAULT_LOCATION = "fr"
LIVE_MODE = 'MKPL'
PROFILE_ID = "undefined"
QUALITY_PRIORITY = {"hd": 1, "sd": 0}
DRM_PRIORITY = {
"unprotected": 3,
"drm_mkpc_widevine_dash": 2,
"drm_mkpc_widevine_dash_download": 1,
"drm_widevine": 0
}
IMPERSONATOR = "chrome110"
LOCK = None
CACHE_FILE = None
USER_AGENT = None
@staticmethod
def test_service():
main_service.run_service(canalplus_com)
@staticmethod
def is_content_livestream(content, additional):
return additional["IS_LIVE"]
@staticmethod
def credentials_needed():
return {"NONE_NEEDED": True}
@staticmethod
def get_endpoints():
try:
endpoints_dict = file_to_dict(canalplus_com.CACHE_FILE)
assert len(endpoints_dict.keys()) > 0
return endpoints_dict
except:
endpoints_dict = {}
response = requests.get(canalplus_com.MAP_URL)
response = json.loads(response.content.decode())
for zone, zone_value in response["mycanal"].items():
if zone.lower() in ["default"]:
continue
response = requests.get(canalplus_com.PROD_URL.format(zone_value=zone_value))
response = json.loads(response.content.decode())
license_base_url = response["hapi"]["licenceBaseUrl"]
if license_base_url.startswith("//"):
license_base_url = f'https:{license_base_url}'
view_url = response["hapi"]["view"]
if view_url.startswith("//"):
view_url = f'https:{view_url}'
playset_url = response["hapi"]["playset"]
if playset_url.startswith("//"):
playset_url = f'https:{playset_url}'
spyro_url = response["hapi"]["spyro"]
if spyro_url.startswith("//"):
spyro_url = f'https:{spyro_url}'
service = response["hapi"]["service"]
spyro_version = response["hapi"]["spyroVersion"]
try:
group_types = response["live"]["liveTvGroupType"][zone]
except:
group_types = canalplus_com.DEFAULT_GROUP_TYPE
init_live_url = response["live"]["init"]
license_live_url = response["live"]["licence"]
pass_url = response["pass"]["url"]
pass_media = response["pass"]["media"]
pass_portail_id = response["pass"]["portailId"]
pass_vect = response["pass"]["vect"]
endpoints_dict[zone] = {
"LICENSE_BASE_URL": license_base_url,
"VIEW_URL": view_url,
"PLAYSET_URL": playset_url,
"SPYRO_URL": spyro_url,
"SERVICE": service,
"SPYRO_VERSION": spyro_version,
"GROUP_TYPES": group_types,
"INIT_LIVE_URL": init_live_url,
"LICENSE_LIVE_URL": license_live_url,
"PASS_URL": pass_url,
"PASS_MEDIA": pass_media,
"PASS_PORTAIL_ID": pass_portail_id,
"PASS_VECT": pass_vect
}
for k, u in endpoints_dict[zone].items():
u = u.replace("{offerZone}", zone)
u = u.replace("{drmId}", str(canalplus_com.DRM_ID))
u = u.replace("{deviceId}", str(canalplus_com.DEVICE_ID))
endpoints_dict[zone][k] = u
dict_to_file(canalplus_com.CACHE_FILE, endpoints_dict)
return endpoints_dict
@staticmethod
def initialize_service():
if canalplus_com.USER_AGENT is None:
canalplus_com.USER_AGENT = builtins.CONFIG["USER_AGENT"]
if canalplus_com.CACHE_FILE is None:
canalplus_com.CACHE_FILE = join(CACHE_DIR, f'{canalplus_com.__name__}.json')
if builtins.CONFIG.get("FRESH", False) is True:
dict_to_file(canalplus_com.CACHE_FILE, {})
if canalplus_com.ENDPOINT_URLS is None:
canalplus_com.ENDPOINT_URLS = canalplus_com.get_endpoints()
assert len(canalplus_com.ENDPOINT_URLS.keys()) > 0
if canalplus_com.LOCK is None:
canalplus_com.LOCK = threading.Lock()
return canalplus_com
@staticmethod
def get_best_decryptable_video(manifest, keys):
mpd_content_raw = requests.get(manifest).content.decode()
mpd_content = xmltodict.parse(mpd_content_raw)
if mpd_content.get("MPD", None) is None:
if WIDEVINE_SCHEME_ID not in mpd_content_raw and PLAYREADY_SCHEME_ID in mpd_content_raw:
return None
raise
mpd_content = mpd_content["MPD"]["Period"]
if type(mpd_content["AdaptationSet"]) is not list:
mpd_content["AdaptationSet"] = [mpd_content["AdaptationSet"]]
best_content = None
best_height = None
best_width = None
for content in mpd_content["AdaptationSet"]:
if content["@contentType"] != "video":
continue
default_kid = None
for protection in content["ContentProtection"]:
if protection.get("@cenc:default_KID", None) is None:
continue
default_kid = protection["@cenc:default_KID"].lower().replace("-", "")
if default_kid is None:
continue
is_decryptable = False
for k in keys:
k1 = k.split(":")[1]
if k1 == "0" * len(k1):
continue
if default_kid == k.split(":")[0]:
is_decryptable = True
break
if not is_decryptable:
continue
for representation in content["Representation"]:
update_content = False
height = int(representation.get("@height", content.get("@height", None)))
if height is None:
continue
height = int(height)
width = int(representation.get("@width", content.get("@width", None)))
if width is None:
continue
width = int(width)
if best_content is None:
update_content = True
else:
if height > best_height:
update_content = True
if update_content:
best_content = content
best_height = height
best_width = width
assert best_content is not None
return best_width, best_height
@staticmethod
def get_keys(challenge, additional):
if not additional["IS_LIVE"]:
pass_token = additional["pass_token"]
licence = requests.post(
additional["license_url"],
headers={
'Content-Type': 'text/plain',
'XX-OZ': additional["offer_zone"],
'XX-SERVICE': additional["service"],
'XX-OPERATOR': canalplus_com.ECK,
'Authorization': f'PASS Token="{pass_token}"',
'XX-Profile-Id': canalplus_com.PROFILE_ID,
'XX-DEVICE': f'{canalplus_com.DEVICE} device-device'
},
data=base64.b64encode(challenge).decode()
)
licence.raise_for_status()
licence = xmltodict.parse(licence.content.decode())
return licence["licenseresponse"]["clientresponse"]["license"]["#text"]
else:
licence = requests.post(
additional["license_url"],
json={
'ServiceRequest': {
'InData': {
'EpgId': additional["epg_id"],
'LiveToken': additional["live_token"],
'ChallengeInfo': base64.b64encode(challenge).decode(),
'Mode': canalplus_com.LIVE_MODE
}
}
}
)
licence.raise_for_status()
return json.loads(licence.content.decode())["ServiceResponse"]["OutData"]["LicenseInfo"]
@staticmethod
def sort_drm_quality(obj):
quality_score = canalplus_com.QUALITY_PRIORITY[obj["quality"].lower()]
drm_score = canalplus_com.DRM_PRIORITY[obj["drmType"].lower()]
return -quality_score, -drm_score
@staticmethod
def get_pssh_from_manifest(manifest_content):
psshs = re.findall(
fr'<ContentProtection[^<>"]*"[^<>"]*{WIDEVINE_SCHEME_ID}[^<>"]*"[^<>]*>(.*?)</ContentProtection>',
manifest_content,
re.DOTALL | re.IGNORECASE
)
pssh = []
for p in psshs:
try:
pssh.append(str(min(re.findall(
r'<[^<>]*cenc:pssh[^<>]*>(.*?)</[^<>]*cenc:pssh[^<>]*>', p
), key=len)))
except:
pass
if len(psshs) > 0:
return str(min(pssh, key=len))
if PLAYREADY_SCHEME_ID not in manifest_content:
return None
psshs = re.findall(
fr'<ProtectionHeader[^<>"]*"[^<>"]*{PLAYREADY_SCHEME_ID}[^<>"]*"[^<>]*>(.*?)</ProtectionHeader>',
manifest_content,
re.DOTALL | re.IGNORECASE
)
return str(min(psshs, key=len))
@staticmethod
def get_video_data(source_element):
response = requests.get(source_element.url).content.decode()
offer_zone = re.search(
r'"offerZone":"(.+?)"',
response
).group(1)
page_json = re.findall(
r'__data\s*=\s*({.+?})\s*;',
response
)
if len(page_json) == 0:
raise
page_json = page_json[0].replace("undefined", "null")
page_json = json.loads(page_json)
offer_location = page_json.get("application", {}).get("zoneInfo", {}).get(
"offerLocation", canalplus_com.DEFAULT_LOCATION
)
pass_token = canalplus_com.ENDPOINT_URLS[offer_zone].get("PASS_TOKEN", None)
if pass_token is None:
with canalplus_com.LOCK:
pass_token = canalplus_com.ENDPOINT_URLS[offer_zone].get("PASS_TOKEN", None)
if pass_token is None:
response = requests_cf.post(
canalplus_com.ENDPOINT_URLS[offer_zone]["PASS_URL"].format(
offerLocation=offer_location
),
headers={'User-Agent': canalplus_com.USER_AGENT},
data='&'.join(f'{k}={v}' for k, v in {
'media': canalplus_com.ENDPOINT_URLS[offer_zone]["PASS_MEDIA"],
'portailId': canalplus_com.ENDPOINT_URLS[offer_zone]["PASS_PORTAIL_ID"],
'vect': canalplus_com.ENDPOINT_URLS[offer_zone]["PASS_VECT"]
}.items()),
impersonate=canalplus_com.IMPERSONATOR
)
response = json.loads(response.content.decode())
pass_token = response["response"]["passToken"]
canalplus_com.ENDPOINT_URLS[offer_zone]["PASS_TOKEN"] = pass_token
epg_id = None
if "/h/" in source_element.url:
content_id = re.search(r"/h/([^/?]*)", source_element.url).group(1)
else:
content_id = None
params_dict = parse_qs(urlparse(source_element.url).query)
epg_id = params_dict["channel"][0]
if content_id is not None:
response = requests.get(
canalplus_com.ENDPOINT_URLS[offer_zone]["PLAYSET_URL"].format(id=content_id),
headers={
'XX-DISTMODES': canalplus_com.DIST_MODES,
'XX-OZ': offer_zone,
'XX-SERVICE': canalplus_com.ENDPOINT_URLS[offer_zone]["SERVICE"],
'XX-OPERATOR': canalplus_com.ECK,
'Authorization': f'PASS Token="{pass_token}"',
'XX-Profile-Id': canalplus_com.PROFILE_ID,
'XX-DEVICE': f'{canalplus_com.DEVICE} device-device'
}
)
status_code = response.status_code
response = json.loads(response.content.decode())
if status_code < 200 or status_code >= 300:
page_json = re.findall(
r'REACT_QUERY_STATE\s*=\s*({.+?})\s*;',
requests.get(source_element.url).content.decode()
)
if len(page_json) > 0:
page_json = page_json[0].replace("undefined", "null")
page_json = json.loads(page_json)
for q in page_json.get("queries", []):
d = q.get("state", {}).get("data", {})
p = d.get("currentPage", {}).get("path", None)
if p is None:
continue
if not source_element.url.endswith(p.rstrip("/")):
continue
for a in d.get("actionLayout", {}).get("primaryActions", []):
if a.get("type", "").lower() not in ["play"]:
continue
epg_id = a.get("onClick", {}).get("epgID", None)
if epg_id is None:
for o in a.get("onClick", {}).get("options", []):
epg_id = o.get("onClick", {}).get("epgID", None)
if epg_id is not None:
break
if epg_id is not None:
break
if epg_id is not None:
break
else:
status_code = 500
response = {}
if epg_id is None:
if status_code == 404 or response.get("code", "").lower() == "conso-404-0":
raise CustomException(ERR_MSG.format(
type=f'{USER_ERROR}',
url=source_element.url,
reason="The content isn't available",
solution="Do not attempt to download it"
))
if status_code in [500, 403] or response.get("code", "").lower() in ["tec-500-1", "conso-403-3"]:
raise CustomException(ERR_MSG.format(
type=USER_ERROR,
url=source_element.url,
reason="Can't download paid content",
solution='Do not attempt to download it'
))
live_token = None
if epg_id is not None:
has_drm = True
response = requests.post(
canalplus_com.ENDPOINT_URLS[offer_zone]["INIT_LIVE_URL"].replace("{offerLocation}", offer_location),
json={
'ServiceRequest': {'InData': {
'PassData': {'Token': pass_token},
'PDSData': {'GroupTypes': canalplus_com.ENDPOINT_URLS[offer_zone]["GROUP_TYPES"]}
}}
}
)
response = json.loads(response.content.decode())
response = response["ServiceResponse"]["OutData"]
live_token = response["LiveToken"]
manifest = None
for group in response.get("PDS", {}).get("ChannelsGroups", {}).get("ChannelsGroup", []):
for channel in group.get("Channels", []):
if str(epg_id) != str(channel.get("EpgId", None)):
continue
manifest = channel.get("WSXUrl", None)
if channel.get("NoEncrypt", None) not in ["", None]:
has_drm = str(channel["NoEncrypt"]).lower() != "true"
if manifest is not None:
break
if manifest is not None:
break
manifest = requests.get(manifest).content.decode()
manifest = json.loads(manifest)
manifest = manifest["primary"]["src"]
license_url = canalplus_com.ENDPOINT_URLS[offer_zone]["LICENSE_LIVE_URL"].format(
offerZone=offer_zone,
drmId=canalplus_com.DRM_ID
)
else:
response = response.get("available", [])
if len(response) == 0:
raise CustomException(ERR_MSG.format(
type=USER_ERROR,
url=source_element.url,
reason="Can't download paid content",
solution='Do not attempt to download it'
))
response = list(filter(
lambda r: "widevine" in r.get("drmType", "").lower() or
"unprotected" in r.get("drmType", "").lower(),
response
))
response = sorted(response, key=canalplus_com.sort_drm_quality)
if len(response) == 0:
raise CustomException(ERR_MSG.format(
type=USER_ERROR,
url=source_element.url,
reason=f"DRM not supported. Widevine wasn't found",
solution="Do not attempt to download it"
))
response = response[0]
has_drm = "widevine" in response["drmType"].lower()
response = response["hash"]
response = requests.put(
canalplus_com.ENDPOINT_URLS[offer_zone]["VIEW_URL"],
headers={
'XX-DISTMODES': canalplus_com.DIST_MODES,
'XX-OZ': offer_zone,
'XX-SERVICE': canalplus_com.ENDPOINT_URLS[offer_zone]["SERVICE"],
'XX-OPERATOR': canalplus_com.ECK,
'Authorization': f'PASS Token="{pass_token}"',
'XX-Profile-Id': canalplus_com.PROFILE_ID,
'XX-DEVICE': f'{canalplus_com.DEVICE} device-device'
},
params={"include": "medias"},
json={'hash': response}
)
response = json.loads(response.content.decode())
licence_path = None
if has_drm:
licence_path = response["@licence"]
manifest = None
license_url = None
for media in response.get("medias", []):
try:
if has_drm:
if licence_path not in media["@licence"] and media["@licence"] not in licence_path:
continue
for file in media.get("files", []):
if "video" != file.get("type", "").lower():
continue
manifest = file.get("distribURL", None)
try:
redirect = requests.get(manifest, allow_redirects=False).headers
location = dict(redirect)["Location"]
assert len(location) > 0
manifest = location
except:
pass
if manifest is None:
try:
manifest = file["routemeup"]
manifest = requests.get(manifest)
manifest = json.loads(manifest.content.decode())
manifest = manifest["primary"]["src"]
except:
manifest = None
if manifest is not None:
if has_drm:
license_url = f'{canalplus_com.ENDPOINT_URLS[offer_zone]["LICENSE_BASE_URL"]}{media["@licence"]}'
break
if manifest is not None:
break
except:
manifest = None
if manifest is None:
raise
if manifest.endswith(".ism"):
manifest += "/manifest"
if has_drm and "drmConfig=" not in license_url:
license_url = update_url_params(license_url, {
"drmConfig": "mkpl::true|persistent::false"
})
manifest_content = requests.get(manifest)
if manifest_content.status_code < 200 or 300 <= manifest_content.status_code:
raise CustomException(ERR_MSG.format(
type=USER_ERROR,
url=source_element.url,
reason="Can't download paid content or the VPN was detected",
solution='Do not attempt to download it or use a better VPN'
))
try:
if not has_drm:
raise
pssh_value = canalplus_com.get_pssh_from_manifest(manifest_content.content.decode())
except:
pssh_value = None
if has_drm and pssh_value is None:
raise CustomException(ERR_MSG.format(
type=USER_ERROR,
url=source_element.url,
reason=f"DRM not supported. Widevine wasn't found",
solution="Do not attempt to download it"
))
if source_element.element is None:
if content_id is not None:
response = requests.post(
canalplus_com.ENDPOINT_URLS[offer_zone]["SPYRO_URL"],
headers={
'XX-OZ': offer_zone,
'XX-SERVICE': canalplus_com.ENDPOINT_URLS[offer_zone]["SERVICE"],
'XX-OPERATOR': canalplus_com.ECK,
'Authorization': f'PASS Token="{pass_token}"',
'XX-Profile-Id': canalplus_com.PROFILE_ID,
'XX-DEVICE': f'{canalplus_com.DEVICE} device-device',
'XX-SPYRO-VERSION': canalplus_com.ENDPOINT_URLS[offer_zone]["SPYRO_VERSION"]
},
json={
'operationName': 'content',
'variables': {'id': content_id},
'query': '''
query content($id: String!) {
catalog { unit(id: $id) {
id titles {
title originalTitle
subtitle
}
}}
}
'''
}
)
response = json.loads(response.content.decode())
response = response["data"]["catalog"]["unit"].get("titles", {})
title = response.get("title", response.get("originalTitle", ""))
if title is None:
title = ""
if len(title) > 0 and response.get("subtitle", "") not in ["", None]:
title = title + "_" + response["subtitle"]
if len(title) == 0:
title = content_id
else:
if epg_id is not None:
title = f"Live_{epg_id}"
else:
title = source_element.url.split(canalplus_com.BASE_URL)[1]
source_element.element = get_valid_filename(title)
if source_element.collection is None:
source_element.collection = join(
str(builtins.CONFIG["DOWNLOAD_COMMANDS"]["OUTPUT_MEDIA_PATH"]),
canalplus_com.__name__
)
return manifest, pssh_value, {
"license_url": license_url,
"live_token": live_token,
"epg_id": epg_id,
"IS_LIVE": epg_id is not None,
"USE_SHAKA": False,
"pass_token": pass_token,
"offer_zone": offer_zone,
"service": canalplus_com.ENDPOINT_URLS[offer_zone]["SERVICE"],
}
@staticmethod
def extra_sections_handler(collection_url, collection_page_content, extra_section, language):
page_json = re.findall(
r'REACT_QUERY_STATE\s*=\s*({.+?})\s*;',
collection_page_content
)
if len(page_json) == 0:
return None
page_json = page_json[0].replace("undefined", "null")
page_json = json.loads(page_json)
content_id = re.search(r"/h/([^/?]*)", collection_url).group(1)
collection_title = re.findall(r"/([^/]*)/h/", collection_url)
if len(collection_title) == 0:
collection_title = content_id
else:
collection_title = collection_title[0]
collection_title = join(
join(
str(builtins.CONFIG["DOWNLOAD_COMMANDS"]["OUTPUT_MEDIA_PATH"]),
canalplus_com.__name__
),
get_valid_filename(collection_title + "_" + extra_section)
)
tabs = []
for query in page_json.get("queries", []):
query = query.get("state", {}).get("data", {})
for k, v in query.items():
if k not in ["pages", "tabs"]:
continue
if type(v) is not list:
continue
if k == "tabs":
for tab in v:
if tab.get("path", "") in [None, ""]:
continue
if collection_url.endswith(tab["path"].rstrip("/")):
tabs.append(tab["URLPage"])
else:
pages = []
for page in v:
page_path = page.get("currentPage", {}).get("path", "")
if page_path in [None, ""]:
continue
if collection_url.endswith(page_path.rstrip("/")):
pages.append(page)
tabs.append({"strates": pages})
rail_index = 0
collection = []
for tab in tabs:
if type(tab) is not dict:
response = requests.get(tab).content.decode()
response = json.loads(response)
is_tab = True
else:
response = tab
is_tab = False
for rail in response.get("strates", []):
if type(rail) is not dict:
continue
rail_index += 1
check = check_range(True, rail_index, None)
if check is True:
continue
elif check is False:
return collection
list_title = None
if is_tab:
list_title = rail.get("title", rail.get("context", {}).get("context_list_title", None))
rail_title = f"Rail_{rail_index}"
if list_title is not None:
rail_title = f'{rail_title}_{list_title}'
rail_title = get_valid_filename(rail_title)
video_index = 0
while True:
for content in rail.get("contents", []):
video_index += 1
if content.get("type", "").lower() not in ["vod", "detailpage"]:
continue
check = check_range(False, rail_index, video_index)
if check is True:
continue
elif check is False:
return collection
video_title = f"Video_{video_index}"
for f in ["title", "subtitle"]:
if content.get(f, None) not in ["", None]:
video_title = video_title + "_" + content[f]
collection.append(BaseElement(
url=canalplus_com.VIDEO_URL.format(
video_id=content["contentID"],
language=language
),
collection=join(collection_title, rail_title),
element=get_valid_filename(video_title)
))
paging = rail.get("paging", {})
if paging is None:
break
if paging.get("hasNextPage", False) is False:
break
rail = requests.get(paging["URLPage"])
rail = json.loads(rail.content.decode())
return collection
@staticmethod
def get_collection_elements(collection_url):
if "/live" in collection_url and "channel=" in collection_url:
return [BaseElement(url=collection_url)]
collection_url = collection_url.split("#")[0].split("?")[0].rstrip("/")
if "/h/" not in collection_url:
return None
extra_section = re.findall(r"/h/[^/]*/(.+)", collection_url)
if len(extra_section) == 0:
extra_section = None
else:
extra_section = extra_section[0]
if extra_section in ["saisons", "sezony"]:
return None
collection_page_content = requests.get(collection_url)
if collection_page_content.status_code == 403:
raise CustomException(ERR_MSG.format(
type=f'{USER_ERROR}',
url=collection_url,
reason="The VPN was detected",
solution="Use a better VPN"
))
collection_page_content = collection_page_content.content.decode()
language = re.findall(fr'{canalplus_com.BASE_URL}/([^/?]*)/', collection_url)
if len(language) == 0:
language = ""
else:
language = language[0]
if len(language) != 2:
language = ""
else:
language = f"/{language}"
if extra_section is not None:
return canalplus_com.extra_sections_handler(
collection_url, collection_page_content,
extra_section, language
)
content_id = re.search(r"/h/([^/?]*)", collection_url).group(1)
page_json = re.findall(
r'REACT_QUERY_STATE\s*=\s*({.+?})\s*;',
collection_page_content
)
if len(page_json) == 0:
return None
page_json = page_json[0].replace("undefined", "null")
page_json = json.loads(page_json)
episodes_url = None
for query in page_json.get("queries", []):
query = query.get("state", {}).get("data", {})
for k, v in query.items():
if k not in ["tabs"]:
continue
if type(v) is not list:
continue
for tab in v:
if tab.get("displayTemplate", "").lower() not in ["episodeslist"]:
continue
if tab.get("path", "") in [None, ""]:
continue
episodes_url = tab["URLPage"]
break
if episodes_url is not None:
break
if episodes_url is not None:
break
if episodes_url is None:
return [BaseElement(url=collection_url)]
response = json.loads(requests.get(episodes_url).content.decode())
collection_title = response.get("meta", {}).get("title", None)
if collection_title is None:
collection_title = re.findall(r"/([^/]*)/h/", collection_url)
if len(collection_title) == 0:
collection_title = None
if collection_title is None:
collection_title = content_id
collection_title = join(
join(
str(builtins.CONFIG["DOWNLOAD_COMMANDS"]["OUTPUT_MEDIA_PATH"]),
canalplus_com.__name__
),
get_valid_filename(collection_title)
)
seasons = []
for season in response.get("selector", []):
if season.get("seasonNumber", None) is None:
continue
seasons.append((season["seasonNumber"], season["contentID"]))
seasons = sorted(seasons, key=lambda s: s[0])
collection = []
for season_index, season_id in seasons:
check = check_range(True, season_index, None)
if check is True:
continue
elif check is False:
return collection
response = requests.get(update_url_params(
episodes_url, {'seasonID': season_id}
))
response = json.loads(response.content.decode())
has_episode_index = None
episode_counter = 0
while True:
episodes = response.get("episodes", {})
for episode in episodes.get("contents", []):
if has_episode_index is None:
has_episode_index = episode.get("episodeNumber", None) is not None
episode_counter += 1
if not has_episode_index:
episode_index = episode_counter
else:
episode_index = episode["episodeNumber"]
check = check_range(False, season_index, episode_index)
if check in [True, False]:
continue
video_title = f"E{episode_index}"
for f in ["title", "editorialTitle"]:
if episode.get(f, None) not in ["", None]:
video_title = video_title + "_" + episode[f]
collection.append(BaseElement(
url=canalplus_com.VIDEO_URL.format(
language=language,
video_id=episode["contentID"]
),
collection=join(collection_title, f'Season_{season_index}'),
element=get_valid_filename(video_title)
))
paging = episodes.get("paging", {})
if paging is None:
break
if paging.get("hasNextPage", False) is False:
break
response = requests.get(paging["URLPage"])
response = json.loads(response.content.decode())
return collection