293 lines
19 KiB
Python
293 lines
19 KiB
Python
import json, re, requests, threading, base64, random
|
|
from Crypto.Cipher import AES
|
|
from Crypto.Util.Padding import pad, unpad
|
|
from urllib.parse import urlparse, quote
|
|
from pyquery import PyQuery as pq
|
|
from base.spider import Spider
|
|
|
|
class Spider(Spider):
|
|
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36', 'sec-ch-ua-platform': '"Windows"', 'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="141", "Google Chrome";v="141"', 'origin': 'https://456movie.net', 'referer': 'https://456movie.net/'}
|
|
|
|
def init(self, extend=""):
|
|
self.site = 'https://456movie.net'
|
|
self.chost, self.token = self.gettoken()
|
|
self.phost = 'https://image.tmdb.org/t/p/w500'
|
|
self.servers = {'vidlink': 'https://vidlink.pro', 'vidfast': 'https://vidfast.pro', '111movies': 'https://111movies.com', 'vidrock': 'https://vidrock.net', 'vidzee': 'https://player.vidzee.wtf'}
|
|
self.server_order = ['vidfast','vidlink', '111movies', 'vidrock', 'vidzee']
|
|
self.headers.update({'origin': self.site, 'referer': f'{self.site}/', 'accept': 'application/json'})
|
|
self._111movies_key = bytes([1, 157, 45, 74, 228, 243, 24, 124, 194, 12, 184, 70, 3, 93, 102, 187, 254, 72, 230, 97, 57, 129, 254, 216, 223, 113, 82, 42, 62, 208, 244, 63])
|
|
self._111movies_iv = bytes([147, 233, 144, 118, 246, 33, 110, 119, 13, 209, 140, 42, 32, 186, 47, 89])
|
|
self._111movies_xkey = bytes([238, 123, 35, 56, 43, 184, 57, 233, 233, 41])
|
|
self.vidzee_key_hex = '6966796f75736372617065796f75726179676179000000000000000000000000'
|
|
self.jx = 'https://111movies.com'
|
|
|
|
def getName(self): return "Movies"
|
|
def isVideoFormat(self, url): return '.m3u8' in url or '.mp4' in url
|
|
def manualVideoCheck(self): return True
|
|
def destroy(self): pass
|
|
|
|
def homeContent(self, filter):
|
|
cate = {"电影": "movie", "剧集": "tv"}
|
|
return {'class': [{'type_name': k, 'type_id': v} for k, v in cate.items()], 'filters': {}}
|
|
|
|
def homeVideoContent(self):
|
|
data = self.fetch(f"{self.chost}/trending/all/week", params={'api_key': self.token, 'language': 'zh-CN', 'page': 1}, headers=self.headers).json()
|
|
return {'list': self.getlist(data['results'])}
|
|
|
|
def categoryContent(self, tid, pg, filter, extend):
|
|
params = {'page': pg, 'api_key': self.token, 'language': 'zh-CN'}
|
|
data = self.fetch(f'{self.chost}/discover/{tid}', params=params, headers=self.headers).json()
|
|
return {'list': self.getlist(data['results'], tid), 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
|
|
|
|
def detailContent(self, ids):
|
|
path = ids[0]
|
|
v = self.fetch(f'{self.chost}{path}', params={'api_key': self.token, 'language': 'zh-CN'}, headers=self.headers).json()
|
|
is_movie = '/movie/' in path
|
|
if is_movie:
|
|
play_str = f"{v.get('title') or v.get('name')}${path}"
|
|
else:
|
|
play_items = []
|
|
for season in v.get('seasons') or []:
|
|
season_number = season.get('season_number')
|
|
if not season_number or season_number < 1: continue
|
|
tv_id = re.findall(r'/tv/(\d+)', path)[0]
|
|
season_data = self.fetch(f"{self.chost}/tv/{tv_id}/season/{season_number}", params={'api_key': self.token, 'language': 'zh-CN'}, headers=self.headers).json()
|
|
for episode in season_data.get('episodes', []):
|
|
episode_number = episode.get('episode_number')
|
|
if episode_number:
|
|
name = episode.get('name') or f'S{season_number:02d}E{episode_number:02d}'
|
|
play_items.append(f"{name}$/tv/{tv_id}/{season_number}/{episode_number}")
|
|
play_str = '#'.join(play_items) if play_items else f"{v.get('name')}${path}/1/1"
|
|
origin_country = v.get('origin_country', [])
|
|
vod_area = ', '.join(origin_country) if origin_country else '未知'
|
|
play_from_list, play_url_list = [], []
|
|
for server_id in self.server_order:
|
|
play_from_list.append(server_id)
|
|
play_url_list.append(play_str)
|
|
play_from, play_url = '$$$'.join(play_from_list), '$$$'.join(play_url_list)
|
|
return {'list': [{'vod_id': path, 'vod_name': v.get('title') or v.get('name'), 'vod_year': (v.get('release_date') or v.get('last_air_date') or '')[:4], 'vod_area': vod_area, 'vod_remarks': v.get('tagline') or '', 'vod_content': v.get('overview') or '', 'vod_play_from': play_from, 'vod_play_url': play_url}]}
|
|
|
|
def searchContent(self, key, quick, pg="1"):
|
|
params = {'query': key, 'page': pg, 'api_key': self.token, 'language': 'zh-CN', 'include_adult': 'false'}
|
|
data = self.fetch(f'{self.chost}/search/multi', params=params, headers=self.headers).json()
|
|
return {'list': self.getlist(data.get('results', [])), 'page': pg}
|
|
|
|
def getlist(self, data, tid=''):
|
|
return [{'vod_id': f"/{media_type}/{i['id']}", 'vod_name': i.get('title') or i.get('name') or '', 'vod_pic': f"{self.phost}{poster}" if (poster := i.get('poster_path') or i.get('backdrop_path')) else '', 'vod_remarks': ''} for i in data or [] if (media_type := tid or i.get('media_type')) in ('movie', 'tv') and i.get('id')]
|
|
|
|
def jxh(self):
|
|
header = self.headers.copy()
|
|
header.update({'referer': f'{self.jx}/', 'origin': self.jx, 'content-type': 'text/plain'})
|
|
header.pop('authorization', None)
|
|
return header
|
|
|
|
def get_server_headers(self, server_id):
|
|
domain = self.servers[server_id]
|
|
header = self.headers.copy()
|
|
header.update({'referer': f'{domain}/', 'origin': domain, 'content-type': 'text/plain'})
|
|
header.pop('authorization', None)
|
|
return header
|
|
|
|
def _parse_play_id(self, id_str):
|
|
m = re.match(r'^/(movie|tv)/(\d+)(?:/(\d+)/(\d+))?$', id_str or '')
|
|
if not m:
|
|
if '/movie/' in id_str: return 'movie', re.findall(r'/movie/(\d+)', id_str)[0], None, None
|
|
elif '/tv/' in id_str:
|
|
parts = re.findall(r'/tv/(\d+)(?:/(\d+)/(\d+))?', id_str)[0]
|
|
return 'tv', parts[0], (parts[1] or '1'), (parts[2] or '1')
|
|
else: raise ValueError('Unrecognized play id')
|
|
media_type, tmdb_id, season, episode = m.groups()
|
|
return media_type, tmdb_id, season, episode
|
|
|
|
def _get_vidrock_url(self, tmdb_id, media_type, season, episode):
|
|
default_domain = 'https://vidrock.net'
|
|
passphrase = "x7k9mPqT2rWvY8zA5bC3nF6hJ2lK4mN9"
|
|
item_id = str(tmdb_id) if media_type == 'movie' else f"{tmdb_id}_{season}_{episode}"
|
|
key, iv = passphrase.encode(), passphrase.encode()[0:16]
|
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
ct = cipher.encrypt(pad(item_id.encode(), AES.block_size))
|
|
encoded = quote(base64.b64encode(ct).decode())
|
|
headers = {"Referer": default_domain, "Origin": default_domain, "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36"}
|
|
response = requests.get(f'{default_domain}/api/{media_type}/{encoded}', headers=headers).json()
|
|
sources = [src['url'] for src in response.values() if src.get('url') and ('.m3u8' in src['url'] or '.mp4' in src['url'])]
|
|
return random.choice(sorted(sources, key=lambda x: '.m3u8' not in x)) if sources else None
|
|
|
|
def _111movies_encrypt_data(self, data_str):
|
|
cipher = AES.new(self._111movies_key, AES.MODE_CBC, self._111movies_iv)
|
|
padded_data = pad(data_str.encode(), AES.block_size)
|
|
encrypted_data = cipher.encrypt(padded_data)
|
|
hex_data = encrypted_data.hex()
|
|
result = "".join(chr(ord(hex_data[i]) ^ self._111movies_xkey[i % len(self._111movies_xkey)]) for i in range(len(hex_data)))
|
|
base64_result = base64.b64encode(result.encode('utf-8')).decode('ascii').replace('+', '-').replace('/', '_').replace('=', '')
|
|
source_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
|
|
target_chars = "PpowE6rtqQ9OxFNzg_vLTJmHKi07j45fXubCVecGURsaS1ny8lBWdAD2ZkM3-YhI"
|
|
char_map = {source_chars[i]: target_chars[i] for i in range(len(source_chars))}
|
|
return ''.join([char_map.get(c, c) for c in base64_result])
|
|
|
|
def _get_111movies_url(self, encrypted_txt):
|
|
rsp = self.post(f"{self.jx}/rijevra/{encrypted_txt}/sr", headers=self.jxh(), timeout=10)
|
|
data = rsp.json()
|
|
urls = []
|
|
for i in data:
|
|
if name := i.get('name'):
|
|
if dat := i.get('data'):
|
|
urls.extend([name, f"{self.getProxyUrl()}&dddd={dat}"])
|
|
return urls
|
|
|
|
def _get_vidzee_url(self, tmdb_id, media_type, season, episode):
|
|
user_agent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"
|
|
default_domain = 'https://player.vidzee.wtf'
|
|
headers = {'Referer': default_domain, 'Origin': default_domain, 'User-Agent': user_agent}
|
|
server = 1
|
|
api_url = f'{default_domain}/api/server?id={tmdb_id}&sr={server}' if media_type == 'movie' else f'{default_domain}/api/server?id={tmdb_id}&sr={server}&ss={season}&ep={episode}'
|
|
response = requests.get(api_url, headers=headers, timeout=10).json()
|
|
if encrypted_url := response.get('url', [{}])[0].get('link'):
|
|
iv_b64, ciphertext_b64 = base64.b64decode(encrypted_url).decode().split(':', 1)
|
|
iv, ciphertext = base64.b64decode(iv_b64), base64.b64decode(ciphertext_b64)
|
|
key = bytes.fromhex(self.vidzee_key_hex)
|
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
decrypted_data = cipher.decrypt(ciphertext)
|
|
plaintext_bytes = unpad(decrypted_data, AES.block_size)
|
|
return plaintext_bytes.decode('utf-8')
|
|
return None
|
|
|
|
def _vf_custom_encode(self, input_bytes):
|
|
source_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
|
|
target_chars = "4stjqN6BT05-L8rQe_HxWmAVv9icYKaCDzIP1fZ7kwXRyFhd2GEng3SMJlUubOop"
|
|
translation_table = str.maketrans(source_chars, target_chars)
|
|
encoded = base64.urlsafe_b64encode(input_bytes).decode().rstrip('=')
|
|
return encoded.translate(translation_table)
|
|
|
|
def _get_vidfast_streams(self, tmdb_id, media_type, season, episode):
|
|
base_url = f"https://vidfast.pro/movie/{tmdb_id}" if media_type == 'movie' else f"https://vidfast.pro/tv/{tmdb_id}/{season}/{episode}"
|
|
ua = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36"
|
|
default_domain = '{uri.scheme}://{uri.netloc}/'.format(uri=urlparse(base_url))
|
|
headers = {"Accept": "*/*", "Referer": default_domain, "User-Agent": ua, "X-Csrf-Token": "iwwuf3C7tleIfqxlgG5NUxOrOROfn5d9", "X-Requested-With": "XMLHttpRequest"}
|
|
sess = requests.Session()
|
|
requests.packages.urllib3.disable_warnings()
|
|
def _get(url, hdrs):
|
|
last = None
|
|
for _ in range(3):
|
|
r = sess.get(url, headers=hdrs, timeout=10, verify=False)
|
|
last = r
|
|
if r.status_code == 200: return r
|
|
return last
|
|
html_resp = _get(base_url, {"User-Agent": ua, "Referer": default_domain})
|
|
if not html_resp or html_resp.status_code != 200: return []
|
|
if m := re.search(r'\\"en\\":\\"(.*?)\\"', html_resp.text):
|
|
raw_data = m.group(1)
|
|
key_hex, iv_hex = '1f9b96f4e6604062c39f69f4c2edd92210d44d185434b0d569b077a72975bf08', '70ed610a03c6a59c7967abf77db57f71'
|
|
aes_key, aes_iv = bytes.fromhex(key_hex), bytes.fromhex(iv_hex)
|
|
cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
|
|
padded = pad(raw_data.encode(), AES.block_size)
|
|
aes_encrypted = cipher.encrypt(padded)
|
|
xor_key = bytes.fromhex("d6f87ef72c")
|
|
xor_bytes = bytes(b ^ xor_key[i % len(xor_key)] for i, b in enumerate(aes_encrypted))
|
|
encoded_final = self._vf_custom_encode(xor_bytes)
|
|
static_path = "hezushon/ira/2264ec23bfa5e4891e26d563e5daac61bcb05688/b544e02b"
|
|
api_servers = f"https://vidfast.pro/{static_path}/wfPFjh__qQ/{encoded_final}"
|
|
resp = _get(api_servers, headers)
|
|
if not resp or resp.status_code != 200: return []
|
|
servers_list = resp.json()
|
|
wanted = {"Oscar", "Alpha", "vFast"}
|
|
results = []
|
|
for item in servers_list or []:
|
|
if (name := item.get('name')) and (data := item.get('data')) and name in wanted:
|
|
api_stream = f"https://vidfast.pro/{static_path}/AddlBFe5/{data}"
|
|
r2 = _get(api_stream, headers)
|
|
if r2 and r2.status_code == 200 and (url := r2.json().get('url')) and ('.m3u8' in url or '.mp4' in url):
|
|
results.append((name, url))
|
|
return results
|
|
return []
|
|
|
|
def playerContent(self, flag, id, vipFlags):
|
|
subs = []
|
|
media_type, tmdb_id, season, episode = self._parse_play_id(id)
|
|
s, e = season or '1', episode or '1'
|
|
server_id = flag if flag in self.servers else self.server_order[0]
|
|
domain = self.servers.get(server_id)
|
|
lang_map = {'english': 'en', 'chinese': 'zh', 'zh': 'zh', '简体': 'zh-CN', '繁體': 'zh-TW', 'japanese': 'ja', 'korean': 'ko'}
|
|
def _map_lang(label):
|
|
name = (label or '').lower()
|
|
if name in lang_map: return lang_map[name]
|
|
for k, v in lang_map.items():
|
|
if name.startswith(k) or k in name: return v
|
|
return ''
|
|
hdr = self.jxh().copy()
|
|
hdr.update({'referer': 'https://vidrock.net/'})
|
|
sub_api = f"https://s.vdrk.site/subfetch.php?id={tmdb_id}" + (f'&s={s}&e={e}' if media_type == 'tv' else '')
|
|
resp = self.fetch(sub_api, headers=hdr, timeout=7)
|
|
if resp and resp.status_code == 200:
|
|
items = resp.json() if hasattr(resp, 'json') else json.loads(resp.text or '[]')
|
|
if not items and media_type == 'tv':
|
|
resp2 = self.fetch(f"https://s.vdrk.site/subfetch.php?id={tmdb_id}", headers=hdr, timeout=7)
|
|
if resp2 and resp2.status_code == 200:
|
|
items = resp2.json() if hasattr(resp2, 'json') else json.loads(resp2.text or '[]')
|
|
for it in items or []:
|
|
if u := it.get('file') or it.get('url') or it.get('src'):
|
|
fmt = 'application/x-subrip' if 'srt' in u.lower() else 'text/vtt'
|
|
subs.append({'url': u, 'name': it.get('label') or it.get('name') or 'Subtitle (vdrk)', 'lang': _map_lang(it.get('label')), 'format': fmt})
|
|
if server_id == 'vidrock':
|
|
if video_url := self._get_vidrock_url(tmdb_id, media_type, s, e):
|
|
return {'parse': 0, 'url': video_url, 'header': self.get_server_headers(server_id), 'subs': subs}
|
|
embed = f"{domain}/movie/{tmdb_id}" if media_type == 'movie' else f"{domain}/tv/{tmdb_id}/{s}/{e}?autoplay=true&autonext=true"
|
|
return {'parse': 1, 'url': embed, 'header': self.get_server_headers(server_id), 'subs': subs}
|
|
elif server_id == '111movies':
|
|
html = self.fetch(f'{self.jx}{id}', headers=self.get_server_headers(server_id)).text
|
|
next_data = pq(html)('#__NEXT_DATA__').text()
|
|
jstr = json.loads(next_data)
|
|
data_token = (jstr.get('props', {}).get('pageProps', {}) or {}).get('data')
|
|
encrypted_txt = self._111movies_encrypt_data(data_token)
|
|
if video_urls := self._get_111movies_url(encrypted_txt):
|
|
return {'parse': 0, 'url': video_urls, 'header': self.get_server_headers(server_id), 'subs': subs}
|
|
return {'parse': 1, 'url': f'{self.jx}{id}', 'header': self.get_server_headers(server_id), 'subs': subs}
|
|
elif server_id == 'vidzee':
|
|
if video_url := self._get_vidzee_url(tmdb_id, media_type, s, e):
|
|
vidzee_headers = {'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36', 'Referer': 'https://player.vidzee.wtf/', 'Origin': 'https://player.vidzee.wtf', 'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="141", "Google Chrome";v="141"', 'sec-ch-ua-mobile': '?1', 'sec-ch-ua-platform': '"Android"'}
|
|
return {'parse': 0, 'url': video_url, 'header': vidzee_headers, 'subs': subs}
|
|
embed = f"{domain}/embed/{media_type}/{tmdb_id}" + (f"/{s}/{e}" if media_type == 'tv' else "")
|
|
return {'parse': 1, 'url': embed, 'header': self.get_server_headers(server_id), 'subs': subs}
|
|
elif server_id == 'vidfast':
|
|
if pairs := self._get_vidfast_streams(tmdb_id, media_type, s, e):
|
|
url_list = []
|
|
for name, u in pairs: url_list.extend([name, u])
|
|
vf_headers = {'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36', 'Referer': 'https://vidfast.pro/', 'Origin': 'https://vidfast.pro'}
|
|
return {'parse': 0, 'url': url_list, 'header': vf_headers, 'subs': subs}
|
|
embed = f"{domain}/movie/{tmdb_id}" if media_type == 'movie' else f"{domain}/tv/{tmdb_id}/{s}/{e}?autoNext=true&nextButton=false&title=true&poster=true&autoPlay=true"
|
|
return {'parse': 1, 'url': embed, 'header': self.get_server_headers(server_id), 'subs': subs}
|
|
else:
|
|
if media_type == 'movie':
|
|
embed = f"{domain}/movie/{tmdb_id}"
|
|
else:
|
|
if server_id == 'vidfast':
|
|
embed = f"{domain}/tv/{tmdb_id}/{s}/{e}?autoNext=true&nextButton=false&title=true&poster=true&autoPlay=true"
|
|
elif server_id == 'vidlink':
|
|
embed = f"{domain}/tv/{tmdb_id}/{s}/{e}?primaryColor=63b8bc&secondaryColor=a2a2a2&iconColor=eefdec&icons=default&player=default&title=true&poster=true&autoplay=true&nextbutton=true"
|
|
else:
|
|
embed = f"{domain}/embed/{'movie' if media_type=='movie' else 'tv'}/{tmdb_id}{'' if media_type=='movie' else f'/{s}/{e}'}"
|
|
return {'parse': 1, 'url': embed, 'header': self.get_server_headers(server_id), 'subs': subs}
|
|
|
|
def localProxy(self, param):
|
|
if dddd := param.get('dddd', ''):
|
|
headers = self.jxh()
|
|
data = self.post(f"{self.jx}/rijevra/{dddd}", headers=headers).json()
|
|
return [302, 'application/vnd.apple.mpegurl', None, {'Location': data['url']}]
|
|
return ''
|
|
|
|
def gettoken(self):
|
|
hosts, paths = [self.site], ['/', '/movies', '/tv-shows']
|
|
key_pattern = re.compile(r'TMDB_API_KEY\s*[:=]\s*[\"\']([A-Za-z0-9]+)[\"\']')
|
|
for host in hosts:
|
|
for path in paths:
|
|
hdr = self.headers.copy()
|
|
hdr.update({'origin': host, 'referer': f'{host}/'})
|
|
html = self.fetch(f'{host}{path}', headers=hdr, timeout=10).text
|
|
if mod := pq(html)('script[type="module"]').attr('src'):
|
|
murl = mod if mod.startswith('http') else f'{host}{mod}'
|
|
mjs = self.fetch(murl, headers=hdr, timeout=10).text
|
|
if m := key_pattern.search(mjs): return 'https://api.themoviedb.org/3', m.group(1)
|
|
if mw := re.search(r'player-watch-([\w-]+)\.js', mjs):
|
|
pjs = self.fetch(f"{host}/assets/player-watch-{mw.group(1)}.js", headers=hdr, timeout=10).text
|
|
if m2 := key_pattern.search(pjs): return 'https://api.themoviedb.org/3', m2.group(1)
|
|
return 'https://api.themoviedb.org/3', '524c16f6e2a0a13c49ff7b99d27b5efb'
|