mirror of
https://github.com/movixcorp/MovixOpenSource.git
synced 2026-07-27 05:02:08 +00:00
445 lines
18 KiB
Python
445 lines
18 KiB
Python
import builtins
|
|
import json
|
|
import os
|
|
import re
|
|
from os.path import join
|
|
from urllib.parse import urlparse, parse_qs
|
|
|
|
import requests
|
|
import xmltodict
|
|
|
|
import utils.tools.common as common_tools
|
|
from utils.constants.macros import USER_ERROR, ERR_MSG
|
|
from utils.main_service import main_service
|
|
from utils.structs import BaseElement, BaseService, CustomException
|
|
from utils.tools.args import check_range
|
|
|
|
|
|
class plus_fifa_com(BaseService):
|
|
DEMO_URLS = [
|
|
"https://www.plus.fifa.com/en/archive?filters=season%3Afifa-club-world-cup-saudi-arabia-2023tm%2Cstage%3Aopening-rounds",
|
|
"https://www.plus.fifa.com/en/showcase/fifa-world-cup-2022/845fdca5-6741-4ab7-a1aa-af01871843cd",
|
|
"https://www.plus.fifa.com/en/showcase/fifa-women-s-world-cup-germany-2011/19e87433-19ba-4f88-b3d3-c6904b7d5a1e",
|
|
"https://www.plus.fifa.com/en/content/when-the-world-watched/3f3a5286-32e7-4587-a094-0edb473d3ed2",
|
|
"https://www.plus.fifa.com/en/content/the-long-walk/627a8255-ec5f-4b0c-a6d0-0fbae421d699",
|
|
"https://www.plus.fifa.com/en/player/580e692e-e8fc-4ad1-a649-5ec0af83f94d?catalogId=5f24e303-ff42-499f-9d7a-b4f8e0eff2ce",
|
|
"https://www.plus.fifa.com/en/player/dd457324-057e-4f17-8457-527f6a8c115d?catalogId=bcf168c3-813c-4882-9878-84a1074fa03a",
|
|
"https://www.plus.fifa.com/en/player/cd472e9a-21ae-49de-91f4-17da7494e56b?catalogId=bb25cc85-30b2-47e8-b721-a419b961ca19",
|
|
"https://www.plus.fifa.com/en/player/d15f15f2-49ce-41b3-a1d8-8fbd8db61cfe?catalogId=3d2612ff-c06f-4a7e-a2d7-ec73504515b5",
|
|
"https://www.plus.fifa.com/en/player/8bfee684-8857-4de9-b530-20358eab1c70?catalogId=7e3e3ac2-0d41-49a0-843a-3029456c6572",
|
|
"https://www.plus.fifa.com/en/player/6cc0c03f-44d9-4e75-8064-31f5ba8ddc70?catalogId=61b1e4dc-0b9c-4018-bb5f-a89d5746f7cb&entryPoint=CTA",
|
|
]
|
|
|
|
DEVICES_URL = 'https://www.plus.fifa.com/gatekeeper/api/v1/devices/'
|
|
CONTENTS_URL = 'https://www.plus.fifa.com/entertainment/api/v1/contents/{catalog_id}/child'
|
|
SHOWCASES_URL = 'https://www.plus.fifa.com/entertainment/api/v1/showcases/{content_id}/child?orderBy=EDITORIAL'
|
|
ASSET_URL = 'https://www.plus.fifa.com/flux-capacitor/api/v1/videoasset?catalog={catalog_id}'
|
|
SESSION_URL = 'https://www.plus.fifa.com/flux-capacitor/api/v1/streaming/session'
|
|
STREAMING_URL = 'https://www.plus.fifa.com/flux-capacitor/api/v1/streaming/urls'
|
|
SEARCH_URL = 'https://www.plus.fifa.com/api/v2/search'
|
|
VIDEO_URL = 'https://www.plus.fifa.com/en/player/{video_id}?catalogId={catalog_id}'
|
|
LICENSE_URL = "https://www.plus.fifa.com/flux-capacitor/api/v1/licensing/widevine/modular?sessionId={session_id}"
|
|
|
|
DEVICE_ID = None
|
|
RES_PRIORITY = {"SDP": 0, "SD": 1, "HD": 2, "HDP": 3}
|
|
PAGE_LIMIT = 50
|
|
LANGUAGE = None
|
|
|
|
@staticmethod
|
|
def get_device_id():
|
|
return json.loads(requests.post(
|
|
plus_fifa_com.DEVICES_URL,
|
|
json={
|
|
'model': 'model', 'manufacturer': 'manufacturer',
|
|
'profile': 'WEB', 'store': 'CHILI'
|
|
}
|
|
).content.decode())["id"]
|
|
|
|
@staticmethod
|
|
def test_service():
|
|
main_service.run_service(plus_fifa_com)
|
|
|
|
@staticmethod
|
|
def get_additional_params(additional):
|
|
return [("MUXER", lambda s: s.format(args="mkv:muxer=mkvmerge"))]
|
|
|
|
@staticmethod
|
|
def credentials_needed():
|
|
return {"NONE_NEEDED": True}
|
|
|
|
@staticmethod
|
|
def initialize_service():
|
|
if builtins.CONFIG.get("BASIC", False) is True:
|
|
plus_fifa_com.LANGUAGE = 'eng'
|
|
plus_fifa_com.LANGUAGE = plus_fifa_com.LANGUAGE.lower()
|
|
|
|
if plus_fifa_com.DEVICE_ID is None:
|
|
plus_fifa_com.DEVICE_ID = plus_fifa_com.get_device_id()
|
|
return plus_fifa_com
|
|
|
|
@staticmethod
|
|
def get_keys(challenge, additional):
|
|
licence = requests.post(plus_fifa_com.LICENSE_URL.format(
|
|
session_id=additional["session_id"]
|
|
), data=challenge)
|
|
licence.raise_for_status()
|
|
return licence.content
|
|
|
|
@staticmethod
|
|
def generate_master_mpd(source_element, manifests):
|
|
manifests_contents = []
|
|
for mpd_url in manifests:
|
|
mpd_content = requests.get(mpd_url).content.decode()
|
|
mpd_content = xmltodict.parse(mpd_content)
|
|
manifests_contents.append((mpd_url, mpd_content))
|
|
|
|
audio_set = []
|
|
index = -1
|
|
for mpd_url, mpd_content in manifests_contents:
|
|
index += 1
|
|
manifest_content = mpd_content["MPD"]["Period"]["AdaptationSet"]
|
|
if type(manifest_content) is not list:
|
|
manifest_content = [manifest_content]
|
|
|
|
for ad_set in manifest_content:
|
|
if index > 0:
|
|
if "audio/" not in ad_set["@mimeType"]:
|
|
continue
|
|
|
|
ad_set["BaseURL"] = mpd_url
|
|
|
|
if index > 0:
|
|
audio_set.append(ad_set)
|
|
|
|
manifests_contents = [manifests_contents[0][1]]
|
|
manifest_content = manifests_contents[0]["MPD"]["Period"]["AdaptationSet"]
|
|
manifest_content += audio_set
|
|
manifests_contents[0]["MPD"]["Period"]["AdaptationSet"] = manifest_content
|
|
manifest_content = manifests_contents[0]
|
|
manifest_content = xmltodict.unparse(manifest_content, pretty=True)
|
|
|
|
output_path = join(source_element.collection, source_element.element)
|
|
if not os.path.exists(output_path):
|
|
os.makedirs(output_path)
|
|
output_path = join(str(output_path), "master.mpd")
|
|
|
|
with open(output_path, "w") as f:
|
|
f.write(manifest_content)
|
|
return output_path
|
|
|
|
@staticmethod
|
|
def get_video_data(source_element):
|
|
if "/player/" in source_element.url:
|
|
catalog_id = parse_qs(urlparse(source_element.url).query)["catalogId"][0]
|
|
video_id = re.search(r"/player/([^/?]*)", source_element.url).group(1)
|
|
elif "/content/" in source_element.url:
|
|
catalog_id = source_element.url.split("/")[-1]
|
|
video_id = None
|
|
assets = json.loads(requests.get(
|
|
plus_fifa_com.ASSET_URL.format(catalog_id=catalog_id),
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID}
|
|
).content.decode())
|
|
for asset in assets:
|
|
if asset['type'].lower() != 'main':
|
|
continue
|
|
|
|
video_id = asset['id']
|
|
break
|
|
|
|
assert video_id is not None
|
|
else:
|
|
raise
|
|
|
|
if source_element.element is None:
|
|
title = common_tools.get_valid_filename(json.loads(requests.get(
|
|
plus_fifa_com.CONTENTS_URL.format(catalog_id=catalog_id).split("/child")[0],
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID}
|
|
).content.decode())['title'])
|
|
if title is None:
|
|
title = f"PlayerId_{video_id}"
|
|
|
|
source_element.element = title
|
|
if source_element.collection is None:
|
|
source_element.collection = join(
|
|
str(builtins.CONFIG["DOWNLOAD_COMMANDS"]["OUTPUT_MEDIA_PATH"]),
|
|
plus_fifa_com.__name__
|
|
)
|
|
|
|
response = json.loads(requests.post(
|
|
plus_fifa_com.SESSION_URL,
|
|
headers={
|
|
'x-chili-device-id': plus_fifa_com.DEVICE_ID,
|
|
'x-chili-avod-compatibility': 'free,free-ads',
|
|
'x-chili-accept-stream': 'mpd/cenc+h264;mpd/clear+h264;mp4/',
|
|
'x-chili-accept-stream-mode': 'multi/codec-compatibility;mono/strict',
|
|
'x-chili-manifest-properties': 'subtitles'
|
|
},
|
|
json={'videoAssetId': video_id}
|
|
).content.decode())
|
|
|
|
if response.get("id", None) is None:
|
|
if response.get("code", "") not in ["", None]:
|
|
if "not_playable" in response["code"].lower():
|
|
raise CustomException(ERR_MSG.format(
|
|
type=USER_ERROR,
|
|
url=source_element.url,
|
|
reason="URL doesn't contain a video",
|
|
solution="Do not attempt to download it"
|
|
))
|
|
|
|
session_id = response["id"]
|
|
manifest = sorted(json.loads(requests.get(
|
|
plus_fifa_com.STREAMING_URL, headers={'x-chili-streaming-session': session_id}
|
|
).content.decode()), key=lambda m: plus_fifa_com.RES_PRIORITY[m["quality"]], reverse=True)
|
|
|
|
manifest = [m for m in manifest if m["quality"] == manifest[0]["quality"]]
|
|
if plus_fifa_com.LANGUAGE is not None:
|
|
temp_man = [
|
|
m for m in manifest
|
|
if m["language"].lower().startswith(plus_fifa_com.LANGUAGE)
|
|
or plus_fifa_com.LANGUAGE.startswith(m["language"].lower())
|
|
]
|
|
if len(temp_man) > 0:
|
|
manifest = [temp_man[0]]
|
|
else:
|
|
manifest = [manifest[0]]
|
|
|
|
manifest = [m["url"] for m in manifest]
|
|
pssh_values = []
|
|
additional = {"PLUS_SERVICE": True}
|
|
|
|
for m in manifest:
|
|
try:
|
|
pssh_value = str(min(
|
|
re.findall(
|
|
r'<cenc:pssh\b[^>]*>(.*?)</cenc:pssh>',
|
|
requests.get(m).content.decode()
|
|
), key=len
|
|
))
|
|
except:
|
|
pssh_value = None
|
|
|
|
if pssh_value is not None:
|
|
if pssh_value not in pssh_values:
|
|
pssh_values.append(pssh_value)
|
|
additional[pssh_value] = {
|
|
"session_id": session_id,
|
|
"PLUS_SERVICE": True
|
|
}
|
|
|
|
if len(manifest) > 1:
|
|
manifest = plus_fifa_com.generate_master_mpd(source_element, manifest)
|
|
else:
|
|
manifest = manifest[0]
|
|
return manifest, pssh_values, additional
|
|
|
|
@staticmethod
|
|
def get_filters_name(filters):
|
|
filters = [f.split(":")[1] for f in filters.split(",")]
|
|
filters = sorted(filters, key=len)
|
|
|
|
filters_name = "Filters_" + str(filters[0])
|
|
filters_name = common_tools.get_valid_filename(filters_name)
|
|
return filters_name
|
|
|
|
@staticmethod
|
|
def get_collection_elements(collection_url):
|
|
if "/player/" in collection_url and "catalogId=" in collection_url:
|
|
return [BaseElement(url=collection_url)]
|
|
|
|
collection = []
|
|
if "/archive" in collection_url and "filters=" in collection_url:
|
|
params_dict = parse_qs(urlparse(collection_url).query)
|
|
filters = params_dict.get("filters", [None])[0]
|
|
if filters is None:
|
|
return None
|
|
|
|
collection_name = plus_fifa_com.get_filters_name(filters)
|
|
collection_name = join(join(
|
|
str(builtins.CONFIG["DOWNLOAD_COMMANDS"]["OUTPUT_MEDIA_PATH"]),
|
|
plus_fifa_com.__name__
|
|
), collection_name)
|
|
|
|
if "fifa-plus:archive-project-fifa-plus" not in filters:
|
|
filters += ",fifa-plus:archive-project-fifa-plus"
|
|
response = requests.get(
|
|
plus_fifa_com.SEARCH_URL,
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID},
|
|
params={
|
|
'filters': filters,
|
|
'limit': plus_fifa_com.PAGE_LIMIT,
|
|
'after': '0',
|
|
'orderBy': 'TIMESTAMP_DESC'
|
|
}
|
|
)
|
|
|
|
index = 0
|
|
while True:
|
|
response = json.loads(response.content.decode())
|
|
|
|
for result in response.get("results", []):
|
|
index += 1
|
|
check = check_range(False, None, index)
|
|
if check is True:
|
|
continue
|
|
elif check is False:
|
|
return collection
|
|
|
|
c_id = result["id"]
|
|
assets = json.loads(requests.get(
|
|
plus_fifa_com.ASSET_URL.format(catalog_id=c_id),
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID}
|
|
).content.decode())
|
|
|
|
video_id = element_title = None
|
|
for asset in assets:
|
|
if asset['type'].lower() != 'main':
|
|
continue
|
|
|
|
video_id = asset['id']
|
|
element_title = asset['title']
|
|
break
|
|
|
|
if video_id is None:
|
|
continue
|
|
|
|
collection.append(BaseElement(
|
|
url=plus_fifa_com.VIDEO_URL.format(video_id=video_id, catalog_id=c_id),
|
|
collection=collection_name,
|
|
element=f'{index}'
|
|
f'_'
|
|
f'{common_tools.get_valid_filename(element_title)}'
|
|
))
|
|
|
|
response = response["pagination"].get("next", None)
|
|
if response is None or type(response) is not dict:
|
|
break
|
|
if response.get("url", None) is None:
|
|
break
|
|
response = requests.get(
|
|
response["url"],
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID}
|
|
)
|
|
|
|
return collection
|
|
|
|
if "/showcase/" in collection_url:
|
|
content_id = re.search(r'/showcase/[^/]+/([^?/]+)', collection_url).group(1)
|
|
content_title = common_tools.get_valid_filename(json.loads(requests.get(
|
|
plus_fifa_com.SHOWCASES_URL.format(content_id=content_id).split("/child?")[0],
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID}
|
|
).content.decode())['title'])
|
|
if content_title is None:
|
|
content_title = f'ShowcaseId_{content_id}'
|
|
|
|
response = json.loads(requests.get(
|
|
plus_fifa_com.SHOWCASES_URL.format(content_id=content_id),
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID}
|
|
).content.decode())
|
|
|
|
index = 0
|
|
for content in response:
|
|
index += 1
|
|
check = check_range(False, None, index)
|
|
if check is True:
|
|
continue
|
|
elif check is False:
|
|
return collection
|
|
|
|
c_id = content["id"]
|
|
assets = json.loads(requests.get(
|
|
plus_fifa_com.ASSET_URL.format(catalog_id=c_id),
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID}
|
|
).content.decode())
|
|
|
|
video_id = element_title = None
|
|
for asset in assets:
|
|
if asset['type'].lower() != 'main':
|
|
continue
|
|
|
|
video_id = asset['id']
|
|
element_title = asset['title']
|
|
break
|
|
|
|
if video_id is None:
|
|
continue
|
|
|
|
collection.append(BaseElement(
|
|
url=plus_fifa_com.VIDEO_URL.format(video_id=video_id, catalog_id=c_id),
|
|
collection=join(join(
|
|
str(builtins.CONFIG["DOWNLOAD_COMMANDS"]["OUTPUT_MEDIA_PATH"]),
|
|
plus_fifa_com.__name__
|
|
), content_title),
|
|
element=f'{index}'
|
|
f'_'
|
|
f'{common_tools.get_valid_filename(element_title)}'
|
|
))
|
|
|
|
return collection
|
|
|
|
if "/content/" in collection_url:
|
|
try:
|
|
content_id = re.search(r'/content/[^/]+/([^/]+)', collection_url).group(1)
|
|
except:
|
|
content_id = re.search(r'/content/([^/]+)', collection_url).group(1)
|
|
|
|
content_title = common_tools.get_valid_filename(json.loads(requests.get(
|
|
plus_fifa_com.CONTENTS_URL.format(catalog_id=content_id).split("/child")[0],
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID}
|
|
).content.decode())['title'])
|
|
if content_title is None:
|
|
content_title = f'ContentId_{content_id}'
|
|
|
|
response = json.loads(requests.get(
|
|
plus_fifa_com.CONTENTS_URL.format(catalog_id=content_id),
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID}
|
|
).content.decode())
|
|
|
|
for content in response:
|
|
check = check_range(True, content["orderIndex"], None)
|
|
if check is True:
|
|
continue
|
|
elif check is False:
|
|
return collection
|
|
|
|
season = json.loads(requests.get(
|
|
plus_fifa_com.CONTENTS_URL.format(catalog_id=content["id"]),
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID}
|
|
).content.decode())
|
|
|
|
for episode in season:
|
|
check = check_range(False, content["orderIndex"], episode['orderIndex'])
|
|
if check is True:
|
|
continue
|
|
elif check is False:
|
|
return collection
|
|
|
|
assets = json.loads(requests.get(
|
|
plus_fifa_com.ASSET_URL.format(catalog_id=episode["id"]),
|
|
headers={'x-chili-device-id': plus_fifa_com.DEVICE_ID}
|
|
).content.decode())
|
|
|
|
video_id = None
|
|
for asset in assets:
|
|
if asset['type'].lower() != 'main':
|
|
continue
|
|
|
|
video_id = asset['id']
|
|
break
|
|
|
|
collection.append(BaseElement(
|
|
url=plus_fifa_com.VIDEO_URL.format(video_id=video_id, catalog_id=episode["id"]),
|
|
collection=join(
|
|
join(
|
|
str(builtins.CONFIG["DOWNLOAD_COMMANDS"]["OUTPUT_MEDIA_PATH"]),
|
|
plus_fifa_com.__name__
|
|
),
|
|
join(content_title, f'Season_{content["orderIndex"]}')
|
|
),
|
|
element=f"Episode_{episode['orderIndex']}"
|
|
f'_'
|
|
f"{common_tools.get_valid_filename(episode['title'])}"
|
|
))
|
|
|
|
return collection
|
|
return None
|