initial services
This commit is contained in:
636
TID/__init__.py
Normal file
636
TID/__init__.py
Normal file
@@ -0,0 +1,636 @@
|
||||
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
|
||||
from http.cookiejar import CookieJar
|
||||
from typing import Optional, Union
|
||||
|
||||
import click
|
||||
|
||||
from unshackle.core.constants import AnyTrack
|
||||
from unshackle.core.credential import Credential
|
||||
from unshackle.core.manifests import DASH
|
||||
from unshackle.core.manifests import HLS
|
||||
from unshackle.core.service import Service
|
||||
from unshackle.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T
|
||||
from unshackle.core.tracks import Chapter, Subtitle, Tracks, Video, Audio
|
||||
|
||||
import requests
|
||||
|
||||
from typing import Callable, Any
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Hash import MD5
|
||||
import base64
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
class TID(Service):
|
||||
"""
|
||||
Service code for TrueID streaming service (https://www.trueid.net/).
|
||||
|
||||
\b
|
||||
Original-Author: [SeFree]
|
||||
Version: 1.0.0
|
||||
Authorization: Cookies
|
||||
Security: FHD@L3, and can be bypass using HLS instead of DASH.
|
||||
|
||||
\b
|
||||
"""
|
||||
|
||||
TITLE_RE = r'https://(?:www|movie)\.trueid\.net/(?:watch/)?(?:[a-z]{2}-[a-z]{2}/)?(?P<type>series|movie)/(?P<id>[a-zA-Z0-9]+)(?:/(?P<season_id>[a-zA-Z0-9]+))?'
|
||||
|
||||
VIDEO_RANGE_MAP = {
|
||||
"SDR": "sdr",
|
||||
"HDR10": "hdr10",
|
||||
"DV": "dolby_vision",
|
||||
}
|
||||
LANGUAGE_MAP = {
|
||||
"en": "English",
|
||||
"th": "Thai",
|
||||
"jp": "Japanese",
|
||||
"ko": "Korean",
|
||||
"zh": "Chinese",
|
||||
}
|
||||
_DRM_MAP={
|
||||
"wv":"WV_FPS",
|
||||
"aes":"AES_128",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@click.command(name="TID", short_help="https://www.trueid.net")
|
||||
@click.argument("title", type=str)
|
||||
@click.option("-SE", "--season", default=None, required=False, type=int,
|
||||
help="TrueID sometime not provide Season in info, so specify it manually.")
|
||||
@click.option("-d", "--drm", default="wv",type=click.Choice(["aes", "wv","None"]), required=False,
|
||||
help="TrueID can be force to use DASH or HLS, this option will force the use of DASH (default: wv).")
|
||||
@click.option("-tl", "--title_lang", default="ja", required=False, type=str,
|
||||
help="If the title is foreigner audio language, specify the foreigner language.")
|
||||
@click.pass_context
|
||||
def cli(ctx, **kwargs):
|
||||
return TID(ctx, **kwargs)
|
||||
|
||||
def extract_id(self,url: str):
|
||||
match = re.compile(self.TITLE_RE).search(url)
|
||||
if not match:
|
||||
return None
|
||||
return match.groupdict()
|
||||
|
||||
def __init__(self, ctx, title,drm, season,title_lang):
|
||||
super().__init__(ctx)
|
||||
|
||||
self.data = self.extract_id(title)
|
||||
self.title =self.data.get("id")
|
||||
self.drm = drm
|
||||
self.type=self.data.get('type') if self.data else "series"
|
||||
self.season = season
|
||||
self.title_lang = title_lang
|
||||
self.license_api = None
|
||||
self.auth_token=None
|
||||
|
||||
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
|
||||
super().authenticate(cookies, credential)
|
||||
"""
|
||||
Refreshes the cookies for the TrueID service.
|
||||
This is necessary to maintain a valid session.
|
||||
"""
|
||||
self.auth_token=self.retry(self.get_auth_token,args=(self.data,))
|
||||
# self.auth_token= self.get_auth_token(self.data)
|
||||
# r=self.session.get(self.title)
|
||||
# data = json.loads(
|
||||
# BeautifulSoup(
|
||||
# r.content, features="lxml"
|
||||
# ).find(
|
||||
# "script",
|
||||
# {
|
||||
# "id": "__NEXT_DATA__"
|
||||
# }).text
|
||||
# )
|
||||
|
||||
params = {
|
||||
'client_id': '893',
|
||||
'browser_id': '*****.*****',
|
||||
'ver': '1.5.0',
|
||||
'scope': 'public_profile,mobile,email,references,identity_account_read,identity_token_bridge',
|
||||
}
|
||||
headers = {
|
||||
'origin': 'https://www.trueid.net',
|
||||
'referer': 'https://www.trueid.net/',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0',
|
||||
}
|
||||
|
||||
# self.session.cookies.clear()
|
||||
# self.session.cookies.update({
|
||||
# "session_tids":self.config['check_sso_params']['session_tids']
|
||||
# })
|
||||
response = self.session.get(self.config['endpoints']['check_sso'], params=params, headers=headers)
|
||||
access_token=response.json().get('access_token')
|
||||
self.log.debug(f"SSO Check Response: {response.json()}")
|
||||
|
||||
if not response.json().get('status') == 'connected':
|
||||
self.log.error("SSO check failed, cannot refresh cookies.")
|
||||
exit()
|
||||
|
||||
# token = base64.b64encode(f"{data['buildId']}:{"b".join(data['buildId'].split("b")[2:])}".encode("utf-8")).decode("utf-8")
|
||||
|
||||
headers = {
|
||||
# 'authorization': f"Basic {token}",
|
||||
'authorization': self.auth_token,
|
||||
}
|
||||
self.session.headers.update(headers)
|
||||
json_data = {
|
||||
'accessToken': access_token,
|
||||
'ssoId': self.session.cookies.get_dict().get("sso_u"),
|
||||
}
|
||||
response = self.session.post(self.config['endpoints']['me'], json=json_data)
|
||||
|
||||
# response = self.session.get(self.config['endpoints']['storelogin'])
|
||||
# print(response.json())
|
||||
# exit()
|
||||
|
||||
# exit()
|
||||
if response.status_code < 300 and response.status_code >=200 and response.json().get('success')== True:
|
||||
data=response.json()['data']
|
||||
json_data = {
|
||||
'USER_PROFILE': {
|
||||
'avatar': data['avatar'],
|
||||
'displayName': data['display_name'],
|
||||
'uid': data['ssoid'],
|
||||
'user_profile_id': data['id'],
|
||||
'trueid': '',
|
||||
},
|
||||
'isLogin': True,
|
||||
'avatar': data['avatar'],
|
||||
'displayName': data['display_name'],
|
||||
'AAA_PROFILE': {
|
||||
'access_token': access_token,
|
||||
'status': 'connected',
|
||||
'income': 'trueid',
|
||||
'hashed': self.config.get('check_sso_params', {}).get('hashed'),
|
||||
'uid': data['ssoid'],
|
||||
},
|
||||
'SUB_RES': [],
|
||||
'uniqueUserId': '*****.*****',
|
||||
'PPID': '',
|
||||
'UID_ads': '',
|
||||
}
|
||||
response = self.session.post(self.config['endpoints']['storelogin'], headers=headers, json=json_data)
|
||||
if response.text == 'Successfully':
|
||||
return True
|
||||
else:
|
||||
self.log.error("Failed to refresh cookies.")
|
||||
exit()
|
||||
# def refresh_cookies(self):
|
||||
# print(self.cookies)
|
||||
|
||||
exit()
|
||||
|
||||
def get_titles(self) -> Titles_T:
|
||||
kind = None
|
||||
drm=None
|
||||
|
||||
if self.type=="movie":
|
||||
headers = {
|
||||
'authorization': self.auth_token,
|
||||
# "referer": self.title,
|
||||
}
|
||||
self.session.headers.update(headers)
|
||||
|
||||
data = {
|
||||
"id": self.title,
|
||||
"lang": "en",
|
||||
# "seasonId": "",
|
||||
}
|
||||
response = self.session.post(url=self.config['endpoints']['getDetailById'], json=data)
|
||||
|
||||
response.raise_for_status()
|
||||
res_json=response.json()
|
||||
|
||||
chapters = self.find_hhmmss_in_dict(res_json)
|
||||
manifest, license=None,None
|
||||
|
||||
if res_json.get("drm")=="WV_FPS":
|
||||
# print("Found Widevine DRM")
|
||||
drm="wv"
|
||||
elif res_json.get("drm")=="AES_128":
|
||||
# print("Found AES_128 DRM")
|
||||
drm="aes"
|
||||
else:
|
||||
drm="aes"
|
||||
|
||||
if self.drm:
|
||||
drm=self.drm
|
||||
|
||||
return Movies(
|
||||
[
|
||||
Movie(
|
||||
id_=res_json.get('id', ''),
|
||||
service=self.__class__,
|
||||
name=res_json.get('title', ''),
|
||||
year=res_json.get('release_year', ''),
|
||||
# original_lang=Language.find(self.LANGUAGE_MAP[res_json['source_country'].lower()]) if res_json.get('source_country') else 'en',
|
||||
|
||||
data={
|
||||
"title":res_json.get('title', ''),
|
||||
# "series_title":res_json.get('title', ''),
|
||||
"url": self.title,
|
||||
"drm":drm,
|
||||
"season_no":res_json.get('season_no', '') ,
|
||||
"episode_no":res_json.get('episode_no', '') ,
|
||||
"audio":res_json.get('audio', ''),
|
||||
"subtitle":res_json.get('subtitle', '') ,
|
||||
"id":res_json.get('id', ''),
|
||||
"year": res_json.get('release_year', ''),
|
||||
'manifest': manifest,
|
||||
'license': license,
|
||||
'chapters': chapters,
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
titles=[]
|
||||
drm=None
|
||||
headers = {
|
||||
'authorization': self.auth_token,
|
||||
"referer": f'https://www.trueid.net/watch/th-th/series/{self.title}/{self.data.get("season_id")}',
|
||||
}
|
||||
# self.session.headers.update(headers)
|
||||
seasonId=self.data.get("season_id")
|
||||
|
||||
params = {
|
||||
"id": self.title,
|
||||
"lang": "en",
|
||||
# "seasonId": seasonId,
|
||||
}
|
||||
# print(self.session.headers)
|
||||
# print(self.session.cookies.get_dict())
|
||||
response =self.session.get(url=self.config['endpoints']['seasonsSerise'],headers=headers, params=params)
|
||||
|
||||
if not response.json():
|
||||
params = {
|
||||
"id": self.title,
|
||||
"lang": "th",
|
||||
# "seasonId": seasonId,
|
||||
}
|
||||
# print(self.session.headers)
|
||||
# print(self.session.cookies.get_dict())
|
||||
response =self.session.get(url=self.config['endpoints']['seasonsSerise'],headers=headers, params=params)
|
||||
|
||||
seasonItems=response.json()['seasonItems']
|
||||
|
||||
|
||||
# original_lang=Language.find(self.LANGUAGE_MAP[res_json['source_country'].lower()]) if res_json.get('source_country') else 'en'
|
||||
|
||||
series_title=response.json().get('seasonShelf', '').get('title', '')
|
||||
# with open("seasonShelf.json", "w") as f:
|
||||
# json.dump(series_title, f, indent=4, ensure_ascii=False)
|
||||
|
||||
season_detail=None
|
||||
url=None
|
||||
previous_season_id=self.data.get('season_id', '')
|
||||
|
||||
# print(previous_season_id)
|
||||
for season in seasonItems:
|
||||
# print(season['id'])
|
||||
headers = {
|
||||
'authorization': self.auth_token,
|
||||
# "referer": f'https://www.trueid.net/watch/th-th/series/{self.title}/{season['id']}',
|
||||
}
|
||||
params = {
|
||||
"id": self.title,
|
||||
"lang": "en",
|
||||
"seasonId": season['id'],
|
||||
}
|
||||
response = self.session.get(url=self.config['endpoints']['seasonsSerise'], params=params).json()
|
||||
if response is None or len(response) == 0:
|
||||
self.log.warning("Season not found")
|
||||
# exit(1)
|
||||
# if len(response) == 0:
|
||||
params = {
|
||||
"id": self.title,
|
||||
"lang": "th",
|
||||
"seasonId": season['id'],
|
||||
}
|
||||
response = self.session.get(url=self.config['endpoints']['seasonsSerise'], params=params).json()
|
||||
|
||||
response = response.get("seasonShelf",[])
|
||||
|
||||
for ss in response.get('season', []):
|
||||
for s in ss.get('episode', []):
|
||||
|
||||
for ep_item in s['subEpisode']:
|
||||
# path=[]
|
||||
# print(ep_item)
|
||||
if ep_item.get("episode_no") is None:
|
||||
continue
|
||||
if ep_item.get('vod_season_id', '') != previous_season_id :
|
||||
self.season= int(self.season)+1 if self.season else ep_item.get('season_no', '')
|
||||
previous_season_id=ep_item.get('vod_season_id', '')
|
||||
# print(self.season)
|
||||
# path.append(ep_item.get('vod_title_id', ''))
|
||||
# path.append(ep_item.get('vod_season_id', ''))
|
||||
# path.append(ep_item.get('vod_episode_id', ''))
|
||||
# path.append(ep_item.get('id', ''))
|
||||
# url= urljoin(self.config['endpoints']['base_series_url'], '/'.join(path))
|
||||
|
||||
# manifest, license = self._get_manifest(url, drm)
|
||||
manifest, license=None,None
|
||||
# manifest, license = self._get_manifest(url, drm)
|
||||
|
||||
chapters = self.find_hhmmss_in_dict(ep_item)
|
||||
# print(ep_item.get('season_no', '') if not self.season else self.season)
|
||||
titles.append({
|
||||
"title":ep_item.get('title', ''),
|
||||
"series_title":series_title,
|
||||
# "url": url,
|
||||
"drm":self._DRM_MAP.get(ep_item.get('drm', 'AES_128')) if self.drm == None else self.drm,
|
||||
"season_no":ep_item.get('season_no', '') if not self.season else self.season,
|
||||
"episode_no":ep_item.get('episode_no', '') ,
|
||||
"sub_ep_no":ep_item.get('sub_ep_no', '') ,
|
||||
"audio":ep_item.get('audio', ''),
|
||||
'sub_ep_included': ep_item.get('sub_ep_included', False),
|
||||
"subtitle":ep_item.get('subtitle', '') ,
|
||||
"id":ep_item.get('id', ''),
|
||||
# "year": res_json.get('release_year', ''),
|
||||
'manifest': manifest,
|
||||
'license': license,
|
||||
'chapters': chapters,
|
||||
|
||||
# 'original_lang': original_lang,
|
||||
})
|
||||
return Series([Episode(
|
||||
id_=x["id"],
|
||||
service=self.__class__,
|
||||
title=x["series_title"],
|
||||
season=x.get("season_no"),
|
||||
number=x.get("episode_no"),
|
||||
name=x.get("title"),
|
||||
data=x,
|
||||
# original_lang=x.get("original_lang", ""),
|
||||
) for x in titles])
|
||||
|
||||
# DASH Example: Service requires separate API calls per codec/range.
|
||||
# Uses _get_tracks_for_variants() which iterates codecs x ranges,
|
||||
# handles HYBRID (HDR10+DV), and best_available fallback.
|
||||
|
||||
def get_tracks(self, title: Title_T) -> Tracks:
|
||||
manifest, title.data["license_api"] = self._get_manifest(title.id, title.data['drm'])
|
||||
res = self.session.get(url=manifest).text
|
||||
tracks=Tracks()
|
||||
if title.data['drm'] == "aes":
|
||||
|
||||
|
||||
tracks.add(HLS.from_text(res, manifest).to_tracks(self.title_lang))
|
||||
|
||||
else:
|
||||
# r = self.session.get(url=manifest)
|
||||
# res = r.text
|
||||
tracks.add(DASH.from_text(
|
||||
text=res,
|
||||
url=manifest
|
||||
).to_tracks(self.title_lang))
|
||||
|
||||
|
||||
return tracks
|
||||
|
||||
# HLS Example: Service returns all codecs/ranges in one master playlist.
|
||||
# No need for _get_tracks_for_variants, dl.py filters by user selection.
|
||||
#
|
||||
# def get_tracks(self, title: Title_T) -> Tracks:
|
||||
# playback = self.session.get(
|
||||
# url=self.config["endpoints"]["playback"].format(title_id=title.id),
|
||||
# params={"token": self.token},
|
||||
# ).json()
|
||||
# return HLS.from_url(
|
||||
# url=playback["manifest_url"],
|
||||
# session=self.session,
|
||||
# ).to_tracks(title.language)
|
||||
|
||||
def get_chapters(self, title: Title_T) -> list[Chapter]:
|
||||
chapters = []
|
||||
|
||||
list_chapter=title.data.get('chapters', [])
|
||||
already_added=[]
|
||||
for i,chapter in enumerate(list_chapter):
|
||||
|
||||
for key, value in chapter.items():
|
||||
if not self.is_valid_nonzero_time(value) or value in already_added:
|
||||
continue
|
||||
chapters.append(Chapter(
|
||||
name=key.replace('_', ' ').replace('skip', '').strip(),
|
||||
timestamp=value,
|
||||
))
|
||||
already_added.append(value)
|
||||
|
||||
|
||||
return chapters
|
||||
|
||||
def get_widevine_service_certificate(self, **_: any) -> str:
|
||||
return self.config.get("certificate")
|
||||
|
||||
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
|
||||
return self.session.post(
|
||||
url=title.data["license_api"],
|
||||
data=challenge # expects bytes
|
||||
).content
|
||||
|
||||
def get_playready_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
|
||||
license_url = self.config["endpoints"].get("playready_license")
|
||||
if not license_url:
|
||||
raise ValueError("PlayReady license endpoint not configured")
|
||||
|
||||
response = self.session.post(
|
||||
url=license_url,
|
||||
data=challenge,
|
||||
headers={
|
||||
"user-agent": self.config["client"][self.device]["license_user_agent"],
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
|
||||
@staticmethod
|
||||
def find_hhmmss_in_dict(data) -> list[dict]:
|
||||
hhmmss_pattern = re.compile(r'^\d{2}:\d{2}:\d{2}$')
|
||||
matches = []
|
||||
|
||||
def search(d):
|
||||
if isinstance(d, dict):
|
||||
for k, v in d.items():
|
||||
if isinstance(v, (dict, list)):
|
||||
search(v)
|
||||
elif isinstance(v, str) and hhmmss_pattern.match(v):
|
||||
matches.append({k: v})
|
||||
elif isinstance(d, list):
|
||||
for item in d:
|
||||
search(item)
|
||||
|
||||
search(data)
|
||||
|
||||
# Sorting by time value
|
||||
def to_seconds(hms):
|
||||
h, m, s = map(int, hms.split(':'))
|
||||
return h * 3600 + m * 60 + s
|
||||
|
||||
matches.sort(key=lambda d: to_seconds(list(d.values())[0]))
|
||||
return matches
|
||||
@staticmethod
|
||||
def is_valid_nonzero_time(value: str) -> bool:
|
||||
# Match HH:MM:SS where each part is two digits
|
||||
if not re.match(r'^\d{2}:\d{2}:\d{2}$', value):
|
||||
return False
|
||||
# Check that it's not 00:00:00
|
||||
return value != "00:00:00"
|
||||
|
||||
def _get_manifest(self, cmsId: str, drm:str = "wv"):
|
||||
|
||||
def get_stream():
|
||||
data = {
|
||||
"drm": drm,
|
||||
"cmsId": cmsId,
|
||||
"contentType": "movie",
|
||||
"lang": "en"
|
||||
}
|
||||
response = self.session.post(self.config['endpoints']['stream'], json=data)
|
||||
retry_lang=["th"]
|
||||
for lang in retry_lang:
|
||||
if not response.status_code==200:
|
||||
self.log.warning(f"Request for English lang fail. Retry with {lang}")
|
||||
data["lang"] = lang
|
||||
response = self.session.post(self.config['endpoints']['stream'], json=data)
|
||||
if response.status_code==200:
|
||||
return response
|
||||
else:
|
||||
|
||||
return response
|
||||
|
||||
response:requests.Response|None = self.retry(get_stream)
|
||||
|
||||
stream = response.json().get("stream")
|
||||
|
||||
manifest=None
|
||||
license=None
|
||||
|
||||
if drm == "wv":
|
||||
manifest = stream.get("streamurl")
|
||||
license = stream.get("license")
|
||||
elif drm == "aes":
|
||||
manifest = stream.get("result")
|
||||
# print(manifest, license)
|
||||
return manifest, license
|
||||
|
||||
def retry(self,func: Callable, args=None, kwargs=None, count: int = 5, delay: float = 5.0) -> Any:
|
||||
args = args or ()
|
||||
kwargs = kwargs or {}
|
||||
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(1, count + 1):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
if attempt < count:
|
||||
time.sleep(delay)
|
||||
else:
|
||||
raise last_exception
|
||||
|
||||
def get_auth_token(self,title_data):
|
||||
authen = None
|
||||
self.log.warning("Getting authorization token")
|
||||
r=self.session.get(self.config["endpoints"]["title"].format(id=title_data.get("id"),season_id=title_data.get("season_id")))
|
||||
data = json.loads(
|
||||
BeautifulSoup(
|
||||
r.content, features="lxml"
|
||||
).find(
|
||||
"script",
|
||||
{
|
||||
"id": "__NEXT_DATA__"
|
||||
}).text
|
||||
)
|
||||
|
||||
if self.type =="movie":
|
||||
url = self.config["endpoints"]["movie_video"].format(id=data.get("id"))
|
||||
else:
|
||||
# sub_season_id = data["props"]["pageProps"]["resultTitleDetail"]["seriesData"]["vod_items"][0]["vod_items"][0]['id']
|
||||
|
||||
def get_id(d):
|
||||
for i in d:
|
||||
for j in i["vod_items"]:
|
||||
sub_season_id = j.get("id")
|
||||
if sub_season_id:
|
||||
for k in j["ep_items"]:
|
||||
ep_id = k.get("id")
|
||||
if ep_id:
|
||||
return sub_season_id, ep_id
|
||||
|
||||
sub_season_id,ep_id=get_id(data["props"]["pageProps"]["resultTitleDetail"]["seriesData"]["vod_items"])
|
||||
# break
|
||||
# break
|
||||
# break
|
||||
# ep_id = data["props"]["pageProps"]["resultTitleDetail"]["seriesData"]["vod_items"][0]["vod_items"][0]['ep_items'][0]['id']
|
||||
url = self.config["endpoints"]["series_video"].format(id=title_data.get("id"),season_id=title_data.get("season_id"),sub_season_id=sub_season_id,ep_id=ep_id)
|
||||
|
||||
r=self.session.get(url)
|
||||
match = re.search(r'__NEXT_DATA__\s*=\s*(\{.*?\});', r.text, re.DOTALL)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
# You can also save it to a file:
|
||||
# with open('next_data.json', 'w', encoding='utf-8') as f:
|
||||
# json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
secret = "ads-top-position"
|
||||
|
||||
username = decrypt_cryptojs(data["runtimeConfig"]["baseAuth"]['username'], secret)
|
||||
password = decrypt_cryptojs(data["runtimeConfig"]["baseAuth"]['password'], secret)
|
||||
data_string=username+":"+password
|
||||
|
||||
authen = "Basic " + base64.b64encode(data_string.encode('utf-8')).decode()
|
||||
self.log.warning(f"Got token : {authen}")
|
||||
except json.JSONDecodeError as e:
|
||||
self.log.error(f"JSON parsing error: {e}", )
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.log.error("HTTP error:", e)
|
||||
|
||||
|
||||
else:
|
||||
self.log.error("Cannot parsing __NEXT_DATA__ from title page.")
|
||||
exit(1)
|
||||
|
||||
|
||||
if not authen:
|
||||
self.log.error("Cannot extract token which require.")
|
||||
exit(1)
|
||||
|
||||
|
||||
return authen
|
||||
|
||||
def evp_bytes_to_key(password, salt, key_len=32, iv_len=16):
|
||||
dtot = b""
|
||||
d = b""
|
||||
|
||||
while len(dtot) < (key_len + iv_len):
|
||||
d = MD5.new(d + password + salt).digest()
|
||||
dtot += d
|
||||
|
||||
return dtot[:key_len], dtot[key_len:key_len+iv_len]
|
||||
|
||||
|
||||
def decrypt_cryptojs(ciphertext_b64, passphrase):
|
||||
data = base64.b64decode(ciphertext_b64)
|
||||
|
||||
assert data[:8] == b"Salted__"
|
||||
salt = data[8:16]
|
||||
ciphertext = data[16:]
|
||||
|
||||
key, iv = evp_bytes_to_key(passphrase.encode(), salt)
|
||||
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
decrypted = cipher.decrypt(ciphertext)
|
||||
|
||||
# remove PKCS7 padding
|
||||
pad_len = decrypted[-1]
|
||||
return decrypted[:-pad_len].decode("utf-8")
|
||||
|
||||
Reference in New Issue
Block a user