mirror of
https://github.com/movixcorp/MovixOpenSource.git
synced 2026-07-29 11:02:45 +00:00
375 lines
14 KiB
Python
375 lines
14 KiB
Python
import builtins
|
|
import json
|
|
import os
|
|
import re
|
|
from os.path import join
|
|
|
|
import requests
|
|
|
|
from utils.constants.macros import ERR_MSG, USER_ERROR, APP_ERROR
|
|
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, has_only_roman_chars
|
|
|
|
|
|
class tv5mondeplus_com(BaseService):
|
|
DEMO_URLS = [
|
|
"https://www.tv5mondeplus.com/en/series-et-films-tv/comedie/la-maison-bleue-s-1-e2-le-projet-de-societe/play",
|
|
"https://www.tv5mondeplus.com/en/podcast/subcategory/dingue-14884767_74079A/play",
|
|
"https://www.tv5mondeplus.com/en/cinema/policier-et-suspense/mille-milliards-de-dollars",
|
|
"https://www.tv5mondeplus.com/en/series-et-films-tv/comedie/la-maison-bleue",
|
|
"https://www.tv5mondeplus.com/en/podcast/subcategory/micro-sciences",
|
|
"https://www.tv5mondeplus.com/en/series-et-films-tv/policier-et-suspense/avocats-associes",
|
|
]
|
|
|
|
AUTH_URL = 'https://api.tv5mondeplus.com/v2/customer/TV5MONDE/businessunit/TV5MONDEplus/auth/anonymous'
|
|
GRAPHQL_URL = 'https://www.tv5mondeplus.com/api/graphql/v1/'
|
|
PLAY_URL = 'https://api.tv5mondeplus.com/v2/customer/TV5MONDE/businessunit/TV5MONDEplus/entitlement/{play_id}/play'
|
|
ASSET_URL = 'https://api.tv5mondeplus.com/v1/customer/TV5MONDE/businessunit/TV5MONDEplus/content/asset/{asset_id}'
|
|
|
|
BEARER_TOKEN = None
|
|
|
|
@staticmethod
|
|
def test_service():
|
|
main_service.run_service(tv5mondeplus_com)
|
|
|
|
@staticmethod
|
|
def credentials_needed():
|
|
return {"NONE_NEEDED": True}
|
|
|
|
@staticmethod
|
|
def get_bearer_token():
|
|
return json.loads(requests.post(
|
|
tv5mondeplus_com.AUTH_URL,
|
|
json={'device': {}, 'deviceId': 'deviceId'}
|
|
).content.decode())["sessionToken"]
|
|
|
|
@staticmethod
|
|
def initialize_service():
|
|
if tv5mondeplus_com.BEARER_TOKEN is None:
|
|
tv5mondeplus_com.BEARER_TOKEN = tv5mondeplus_com.get_bearer_token()
|
|
|
|
return tv5mondeplus_com
|
|
|
|
@staticmethod
|
|
def get_keys(challenge, additional):
|
|
licence = requests.post(
|
|
additional["license_url"],
|
|
data=challenge
|
|
)
|
|
licence.raise_for_status()
|
|
return licence.content
|
|
|
|
@staticmethod
|
|
def generate_segment_m3u8(output_path, manifest, content_info):
|
|
m3u8_content = "#EXTM3U\n#EXT-X-VERSION:3\n\n"
|
|
m3u8_content += f'#EXTINF:{content_info.get("durationInMilliseconds", 1)},\n'
|
|
m3u8_content += f'{manifest}\n'
|
|
|
|
m3u8_content += "#EXT-X-ENDLIST\n"
|
|
with open(output_path, "w") as f:
|
|
f.write(m3u8_content)
|
|
|
|
@staticmethod
|
|
def generate_master_m3u8(source_element, manifest, extension, content_info):
|
|
output_path = str(join(source_element.collection, source_element.element))
|
|
if not os.path.exists(output_path):
|
|
os.makedirs(output_path)
|
|
m3u8_content = "#EXTM3U\n#EXT-X-VERSION:3\n\n"
|
|
|
|
title = f'audio.m3u8'
|
|
tv5mondeplus_com.generate_segment_m3u8(join(output_path, title), manifest, content_info)
|
|
m3u8_content += f"#EXT-X-STREAM-INF:BANDWIDTH=1000,TYPE=AUDIO,MIME-TYPE=\"audio/{extension}\"\n"
|
|
m3u8_content += f'{title}\n'
|
|
|
|
output_path = join(output_path, "master.m3u8")
|
|
with open(output_path, "w") as f:
|
|
f.write(m3u8_content)
|
|
return output_path
|
|
|
|
@staticmethod
|
|
def get_video_data(source_element):
|
|
video_slug = source_element.url.split("/play")[0].split("/")[-1]
|
|
language = source_element.url.split("/")[3]
|
|
|
|
response = requests.post(
|
|
tv5mondeplus_com.GRAPHQL_URL,
|
|
json={
|
|
'operationName': 'VODContentDetails',
|
|
'variables': {'contentId': 'redbee:' + video_slug + ':' + language},
|
|
'extensions': {'persistedQuery': {
|
|
'version': 1,
|
|
'sha256Hash': 'e396131572f170605ea7a9f13139568323ab9e398a350741952e690be38efb30'
|
|
}}
|
|
}
|
|
)
|
|
|
|
if response.status_code == 400:
|
|
raise CustomException(ERR_MSG.format(
|
|
type=f'{USER_ERROR}',
|
|
url=source_element.url,
|
|
reason="Unknown language format",
|
|
solution="Use a valid content URL"
|
|
))
|
|
if response.status_code == 404:
|
|
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"
|
|
))
|
|
response = json.loads(response.content.decode())
|
|
|
|
if source_element.element is None:
|
|
video_title = response["data"]["lookupContent"]["title"]
|
|
episode_index = response["data"]["lookupContent"].get("episodeNumber", None)
|
|
if episode_index is not None:
|
|
video_title = f'{video_title}_{episode_index}'
|
|
|
|
source_element.element = get_valid_filename(video_title)
|
|
if source_element.collection is None:
|
|
source_element.collection = join(
|
|
str(builtins.CONFIG["DOWNLOAD_COMMANDS"]["OUTPUT_MEDIA_PATH"]),
|
|
tv5mondeplus_com.__name__
|
|
)
|
|
|
|
play_id = response["data"]["lookupContent"]["id"].split(":")[0]
|
|
response = json.loads(requests.get(
|
|
tv5mondeplus_com.PLAY_URL.format(play_id=play_id),
|
|
headers={'Authorization': f'Bearer {tv5mondeplus_com.BEARER_TOKEN}'},
|
|
params={
|
|
'supportedFormats': 'dash,hls,mss,mp3',
|
|
'supportedDrms': 'widevine'
|
|
}
|
|
).content.decode())
|
|
|
|
formats = response["formats"]
|
|
drm_free = list(filter(lambda f: f.get("drm", None) in ["", None, {}], formats))
|
|
license_url = None
|
|
|
|
if len(drm_free) == 0:
|
|
drm_widevine = list(filter(lambda f: f.get("drm", None) not in ["", None, {}], formats))
|
|
drm_widevine = list(filter(
|
|
lambda f: len([w for w in f["drm"].keys() if "widevine" in w.lower()]) > 0,
|
|
drm_widevine
|
|
))
|
|
|
|
if len(drm_widevine) == 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"
|
|
))
|
|
drm_widevine = drm_widevine[0]
|
|
|
|
for k, v in drm_widevine["drm"].items():
|
|
if "widevine" not in k.lower():
|
|
continue
|
|
license_url = v["licenseServerUrl"]
|
|
break
|
|
|
|
selected_drm = drm_widevine
|
|
else:
|
|
selected_drm = drm_free[0]
|
|
|
|
manifest = selected_drm["mediaLocator"]
|
|
manifest_format = selected_drm["format"].lower()
|
|
|
|
pssh_value = None
|
|
if license_url is not None:
|
|
try:
|
|
if manifest_format in ["mp3"]:
|
|
raise
|
|
|
|
pssh_value = str(min(re.findall(
|
|
r'<[^<>]*cenc:pssh[^<>]*>(.*?)</[^<>]*cenc:pssh[^<>]*>',
|
|
requests.get(manifest).content.decode()
|
|
), key=len))
|
|
except:
|
|
pass
|
|
|
|
if pssh_value is None:
|
|
raise CustomException(ERR_MSG.format(
|
|
type=APP_ERROR,
|
|
url=source_element.url,
|
|
reason=f"Manifest format not supported: {manifest}",
|
|
solution=f"Extend the {tv5mondeplus_com.__name__} service"
|
|
))
|
|
|
|
if manifest_format in ["mp3"]:
|
|
manifest = tv5mondeplus_com.generate_master_m3u8(source_element, manifest, manifest_format, response)
|
|
return manifest, pssh_value, {"license_url": license_url}
|
|
|
|
@staticmethod
|
|
def get_locale_name(content, language):
|
|
content_name = None
|
|
for i in range(0, 2):
|
|
for local in content.get("localized", []):
|
|
if i == 0:
|
|
valid_title = local.get("locale", "").lower() in ["en", "eng", language]
|
|
if not valid_title:
|
|
continue
|
|
|
|
valid_title = has_only_roman_chars(local.get("title", None))
|
|
if valid_title:
|
|
content_name = local["title"]
|
|
else:
|
|
valid_title = has_only_roman_chars(local.get("sortingTitle", None))
|
|
if valid_title:
|
|
content_name = local["sortingTitle"]
|
|
if content_name is not None and len(content_name.strip()) > 0:
|
|
break
|
|
|
|
if content_name is not None:
|
|
break
|
|
return content_name
|
|
|
|
@staticmethod
|
|
def get_collection_elements(collection_url):
|
|
collection_url = collection_url.split("?")[0].split("#")[0].rstrip("/")
|
|
if collection_url.endswith("/play"):
|
|
return [BaseElement(url=collection_url)]
|
|
|
|
series_slug = collection_url.split("/")[-1]
|
|
language = collection_url.split("/")[3]
|
|
response = requests.get(
|
|
tv5mondeplus_com.ASSET_URL.format(asset_id=series_slug),
|
|
params={
|
|
'fieldSet': 'ALL',
|
|
'types': 'MOVIE,TV_SHOW,PODCAST',
|
|
'onlyPublished': 'true',
|
|
'includeEpisodes': 'true',
|
|
'client': 'json'
|
|
}
|
|
)
|
|
if response.status_code == 404:
|
|
raise CustomException(ERR_MSG.format(
|
|
type=f'{USER_ERROR}',
|
|
url=collection_url,
|
|
reason="The content isn't available",
|
|
solution="Do not attempt to download it"
|
|
))
|
|
|
|
collection = []
|
|
response = json.loads(response.content.decode())
|
|
collection_name = tv5mondeplus_com.get_locale_name(response, language)
|
|
|
|
if collection_name is None:
|
|
collection_name = series_slug
|
|
collection_name = join(
|
|
join(
|
|
str(builtins.CONFIG["DOWNLOAD_COMMANDS"]["OUTPUT_MEDIA_PATH"]),
|
|
tv5mondeplus_com.__name__
|
|
),
|
|
get_valid_filename(collection_name)
|
|
)
|
|
|
|
seasons = []
|
|
for season in response.get("seasons", []):
|
|
season_index = season.get("season", 1)
|
|
try:
|
|
season_index = int(season_index)
|
|
except:
|
|
season_index = 1
|
|
seasons.append((season_index, season))
|
|
seasons = sorted(seasons, key=lambda s: s[0])
|
|
|
|
for season_index, season in seasons:
|
|
check = check_range(True, season_index, None)
|
|
if check is True:
|
|
continue
|
|
elif check is False:
|
|
return collection
|
|
|
|
for episode in season.get("episodes", []):
|
|
episode_index = int(episode.get("episode", 1))
|
|
check = check_range(False, season_index, episode_index)
|
|
if check in [True, False]:
|
|
continue
|
|
|
|
episode_name = tv5mondeplus_com.get_locale_name(episode, language)
|
|
episode_url = collection_url.split("/")
|
|
try:
|
|
episode_url[-1] = episode["slugs"][0]
|
|
except:
|
|
episode_url[-1] = episode["assetId"]
|
|
|
|
if episode_name is None:
|
|
episode_name = episode_url[-1]
|
|
episode_url.append("play")
|
|
episode_url = "/".join(episode_url)
|
|
|
|
collection.append(BaseElement(
|
|
url=episode_url,
|
|
collection=join(collection_name, f'Season_{season_index}'),
|
|
element=f'Episode_{episode_index}'
|
|
f'_'
|
|
f'{get_valid_filename(episode_name)}'
|
|
))
|
|
|
|
if len(collection) > 0:
|
|
return collection
|
|
|
|
response = requests.post(
|
|
tv5mondeplus_com.GRAPHQL_URL,
|
|
json={
|
|
'operationName': 'VODContentDetails',
|
|
'variables': {'contentId': f'redbee:{series_slug}:{language}'},
|
|
'extensions': {'persistedQuery': {
|
|
'version': 1,
|
|
'sha256Hash': 'e396131572f170605ea7a9f13139568323ab9e398a350741952e690be38efb30'
|
|
}}
|
|
}
|
|
)
|
|
|
|
response = json.loads(response.content.decode())
|
|
content_id = response["data"]["lookupContent"]["id"].split(":")[0]
|
|
|
|
response = requests.post(
|
|
tv5mondeplus_com.GRAPHQL_URL,
|
|
json={
|
|
'operationName': 'VODContentEpisodes',
|
|
'variables': {'contentId': f'PODCAST:{content_id}:{language}'},
|
|
'extensions': {'persistedQuery': {
|
|
'version': 1,
|
|
'sha256Hash': '3ef37000cba42e64e4f2505fa4fa48d42f84e4335039c4e82f5ca24c11db0676'
|
|
}}
|
|
}
|
|
)
|
|
response = json.loads(response.content.decode())
|
|
|
|
try:
|
|
episodes = response["data"]["lookupContent"]["episodes"]["items"]
|
|
assert len(episodes) > 0
|
|
except:
|
|
return [BaseElement(url=collection_url)]
|
|
|
|
episode_index = 0
|
|
for episode in episodes:
|
|
episode_index += 1
|
|
check = check_range(False, None, episode_index)
|
|
if check is True:
|
|
continue
|
|
elif check is False:
|
|
return collection
|
|
|
|
episode_url = collection_url.split("/")
|
|
episode_url[-1] = episode["id"].split(":")[0]
|
|
episode_url.append("play")
|
|
episode_url = "/".join(episode_url)
|
|
|
|
if has_only_roman_chars(episode.get("title", None)):
|
|
episode_name = episode["title"]
|
|
else:
|
|
episode_name = episode["id"]
|
|
|
|
collection.append(BaseElement(
|
|
url=episode_url,
|
|
collection=collection_name,
|
|
element=f'Podcast_{episode_index}'
|
|
f'_'
|
|
f'{get_valid_filename(episode_name)}'
|
|
))
|
|
return collection
|