Sync all projects
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
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'
|
||||
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import sys
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
headers = {
|
||||
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'sec-fetch-dest': 'document',
|
||||
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.8 Mobile/15E148 Safari/604.1'
|
||||
}
|
||||
|
||||
host = "https://4k-av.com"
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=self.getpq()
|
||||
result = {}
|
||||
classes = []
|
||||
for k in list(data('#category ul li').items())[:-1]:
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': k('a').attr('href')
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list'] = self.getlist(data('#MainContent_scrollul ul li'),'.poster span')
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data=self.getpq(f"{tid}page-{pg}.html")
|
||||
result = {}
|
||||
result['list'] = self.getlist(data('#MainContent_newestlist .virow .NTMitem'))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.getpq(ids[0])
|
||||
v = data('#videoinfo')
|
||||
vod = {
|
||||
'vod_name': data('#tophead h1').text().split(' ')[0],
|
||||
'type_name': v('#MainContent_tags.tags a').text(),
|
||||
'vod_year': v('#MainContent_videodetail.videodetail a').text(),
|
||||
'vod_remarks': v('#MainContent_titleh12 h2').text(),
|
||||
'vod_content': v('p.cnline').text(),
|
||||
'vod_play_from': '4KAV',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
vlist = data('#rtlist li')
|
||||
jn = f"{vod['vod_name']}_" if 'EP0' in vlist.eq(0)('span').text() else ''
|
||||
if vlist:
|
||||
c = [f"{jn}{i('span').text()}${i('a').attr('href')}" for i in list(vlist.items())[1:]]
|
||||
c.insert(0, f"{jn}{vlist.eq(0)('span').text()}${ids[0]}")
|
||||
vod['vod_play_url'] = '#'.join(c)
|
||||
else:
|
||||
vod['vod_play_url'] = f"{vod['vod_name']}${ids[0]}"
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=self.getpq(f"/s?k={key}")
|
||||
return {'list':self.getlist(data('#MainContent_newestlist .virow.search .NTMitem.Main'))}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
data=self.getpq(id)
|
||||
p,url=0,data('#MainContent_videowindow source').attr('src')
|
||||
if not url:raise Exception("未找到播放地址")
|
||||
except Exception as e:
|
||||
p,url=1,f"{self.host}{id}"
|
||||
headers = {
|
||||
'origin': self.host,
|
||||
'referer': f'{self.host}/',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.8 Mobile/15E148 Safari/604.1',
|
||||
}
|
||||
return {'parse': p, 'url': url, 'header': headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def getlist(self,data,y='.resyear label[title="分辨率"]'):
|
||||
videos = []
|
||||
for i in data.items():
|
||||
ns = i('.title h2').text().split(' ')
|
||||
videos.append({
|
||||
'vod_id': i('.title a').attr('href'),
|
||||
'vod_name': ns[0],
|
||||
'vod_pic': i('.poster img').attr('src'),
|
||||
'vod_remarks': ns[-1] if len(ns) > 1 else '',
|
||||
'vod_year': i(y).text()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getpq(self, path=''):
|
||||
url=f"{self.host}{path}"
|
||||
data=self.fetch(url,headers=self.headers).text
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
@@ -0,0 +1,768 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from base64 import b64decode, b64encode
|
||||
from urllib.parse import parse_qs
|
||||
import requests
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
tid = 'douyin'
|
||||
headers = self.gethr(0, tid)
|
||||
response = requests.head(self.hosts[tid], headers=headers)
|
||||
ttwid = response.cookies.get('ttwid')
|
||||
headers.update({
|
||||
'authority': self.hosts[tid].split('//')[-1],
|
||||
'cookie': f'ttwid={ttwid}' if ttwid else ''
|
||||
})
|
||||
self.dyheaders = headers
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
headers = [
|
||||
{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0"
|
||||
},
|
||||
{
|
||||
"User-Agent": "Dart/3.4 (dart:io)"
|
||||
}
|
||||
]
|
||||
|
||||
excepturl = 'https://www.baidu.com'
|
||||
|
||||
hosts = {
|
||||
"huya": ["https://www.huya.com","https://mp.huya.com"],
|
||||
"douyin": "https://live.douyin.com",
|
||||
"douyu": "https://www.douyu.com",
|
||||
"wangyi": "https://cc.163.com",
|
||||
"bili": ["https://api.live.bilibili.com", "https://api.bilibili.com"]
|
||||
}
|
||||
|
||||
referers = {
|
||||
"huya": "https://live.cdn.huya.com",
|
||||
"douyin": "https://live.douyin.com",
|
||||
"douyu": "https://m.douyu.com",
|
||||
"bili": "https://live.bilibili.com"
|
||||
}
|
||||
|
||||
playheaders = {
|
||||
"wangyi": {
|
||||
"User-Agent": "ExoPlayer",
|
||||
"Connection": "Keep-Alive",
|
||||
"Icy-MetaData": "1"
|
||||
},
|
||||
"bili": {
|
||||
'Accept': '*/*',
|
||||
'Icy-MetaData': '1',
|
||||
'referer': referers['bili'],
|
||||
'user-agent': headers[0]['User-Agent']
|
||||
},
|
||||
'douyin': {
|
||||
'User-Agent': 'libmpv',
|
||||
'Icy-MetaData': '1'
|
||||
},
|
||||
'huya': {
|
||||
'User-Agent': 'ExoPlayer',
|
||||
'Connection': 'Keep-Alive',
|
||||
'Icy-MetaData': '1'
|
||||
},
|
||||
'douyu': {
|
||||
'User-Agent': 'libmpv',
|
||||
'Icy-MetaData': '1'
|
||||
}
|
||||
}
|
||||
|
||||
def process_bili(self):
|
||||
try:
|
||||
self.blfdata = self.fetch(
|
||||
f'{self.hosts["bili"][0]}/room/v1/Area/getList?need_entrance=1&parent_id=0',
|
||||
headers=self.gethr(0, 'bili')
|
||||
).json()
|
||||
return ('bili', [{'key': 'cate', 'name': '分类',
|
||||
'value': [{'n': i['name'], 'v': str(i['id'])}
|
||||
for i in self.blfdata['data']]}])
|
||||
except Exception as e:
|
||||
print(f"bili处理错误: {e}")
|
||||
return 'bili', None
|
||||
|
||||
def process_douyin(self):
|
||||
try:
|
||||
data = self.getpq(self.hosts['douyin'], headers=self.dyheaders)('script')
|
||||
for i in data.items():
|
||||
if 'categoryData' in i.text():
|
||||
content = i.text()
|
||||
start = content.find('{')
|
||||
end = content.rfind('}') + 1
|
||||
if start != -1 and end != -1:
|
||||
json_str = content[start:end]
|
||||
json_str = json_str.replace('\\"', '"')
|
||||
try:
|
||||
self.dyifdata = json.loads(json_str)
|
||||
return ('douyin', [{'key': 'cate', 'name': '分类',
|
||||
'value': [{'n': i['partition']['title'],
|
||||
'v': f"{i['partition']['id_str']}@@{i['partition']['title']}"}
|
||||
for i in self.dyifdata['categoryData']]}])
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"douyin解析错误: {e}")
|
||||
return 'douyin', None
|
||||
except Exception as e:
|
||||
print(f"douyin请求或处理错误: {e}")
|
||||
return 'douyin', None
|
||||
|
||||
def process_douyu(self):
|
||||
try:
|
||||
self.dyufdata = self.fetch(
|
||||
f'{self.referers["douyu"]}/api/cate/list',
|
||||
headers=self.headers[1]
|
||||
).json()
|
||||
return ('douyu', [{'key': 'cate', 'name': '分类',
|
||||
'value': [{'n': i['cate1Name'], 'v': str(i['cate1Id'])}
|
||||
for i in self.dyufdata['data']['cate1Info']]}])
|
||||
except Exception as e:
|
||||
print(f"douyu错误: {e}")
|
||||
return 'douyu', None
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"虎牙": "huya",
|
||||
"哔哩": "bili",
|
||||
"抖音": "douyin",
|
||||
"斗鱼": "douyu",
|
||||
"网易": "wangyi"
|
||||
}
|
||||
classes = []
|
||||
filters = {
|
||||
'huya': [{'key': 'cate', 'name': '分类',
|
||||
'value': [{'n': '网游', 'v': '1'}, {'n': '单机', 'v': '2'},
|
||||
{'n': '娱乐', 'v': '8'}, {'n': '手游', 'v': '3'}]}]
|
||||
}
|
||||
|
||||
with ThreadPoolExecutor(max_workers=3) as executor:
|
||||
futures = {
|
||||
executor.submit(self.process_bili): 'bili',
|
||||
executor.submit(self.process_douyin): 'douyin',
|
||||
executor.submit(self.process_douyu): 'douyu'
|
||||
}
|
||||
|
||||
for future in futures:
|
||||
platform, filter_data = future.result()
|
||||
if filter_data:
|
||||
filters[platform] = filter_data
|
||||
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
vdata = []
|
||||
result = {}
|
||||
pagecount = 9999
|
||||
result['page'] = pg
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
if tid == 'wangyi':
|
||||
vdata, pagecount = self.wyccContent(tid, pg, filter, extend, vdata)
|
||||
elif 'bili' in tid:
|
||||
vdata, pagecount = self.biliContent(tid, pg, filter, extend, vdata)
|
||||
elif 'huya' in tid:
|
||||
vdata, pagecount = self.huyaContent(tid, pg, filter, extend, vdata)
|
||||
elif 'douyin' in tid:
|
||||
vdata, pagecount = self.douyinContent(tid, pg, filter, extend, vdata)
|
||||
elif 'douyu' in tid:
|
||||
vdata, pagecount = self.douyuContent(tid, pg, filter, extend, vdata)
|
||||
result['list'] = vdata
|
||||
result['pagecount'] = pagecount
|
||||
return result
|
||||
|
||||
def wyccContent(self, tid, pg, filter, extend, vdata):
|
||||
params = {
|
||||
'format': 'json',
|
||||
'start': (int(pg) - 1) * 20,
|
||||
'size': '20',
|
||||
}
|
||||
response = self.fetch(f'{self.hosts[tid]}/api/category/live/', params=params, headers=self.headers[0]).json()
|
||||
for i in response['lives']:
|
||||
if i.get('cuteid'):
|
||||
bvdata = self.buildvod(
|
||||
vod_id=f"{tid}@@{i['cuteid']}",
|
||||
vod_name=i.get('title'),
|
||||
vod_pic=i.get('cover'),
|
||||
vod_remarks=i.get('nickname'),
|
||||
style={"type": "rect", "ratio": 1.33}
|
||||
)
|
||||
vdata.append(bvdata)
|
||||
return vdata, 9999
|
||||
|
||||
def biliContent(self, tid, pg, filter, extend, vdata):
|
||||
if extend.get('cate') and pg == '1' and 'click' not in tid:
|
||||
for i in self.blfdata['data']:
|
||||
if str(i['id']) == extend['cate']:
|
||||
for j in i['list']:
|
||||
v = self.buildvod(
|
||||
vod_id=f"click_{tid}@@{i['id']}@@{j['id']}",
|
||||
vod_name=j.get('name'),
|
||||
vod_pic=j.get('pic'),
|
||||
vod_tag=1,
|
||||
style={"type": "oval", "ratio": 1}
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 1
|
||||
else:
|
||||
path = f'/xlive/web-interface/v1/second/getListByArea?platform=web&sort=online&page_size=30&page={pg}'
|
||||
if 'click' in tid:
|
||||
ids = tid.split('_')[1].split('@@')
|
||||
tid = ids[0]
|
||||
path = f'/xlive/web-interface/v1/second/getList?platform=web&parent_area_id={ids[1]}&area_id={ids[-1]}&sort_type=&page={pg}'
|
||||
data = self.fetch(f'{self.hosts[tid][0]}{path}', headers=self.gethr(0, tid)).json()
|
||||
for i in data['data']['list']:
|
||||
if i.get('roomid'):
|
||||
data = self.buildvod(
|
||||
f"{tid}@@{i['roomid']}",
|
||||
i.get('title'),
|
||||
i.get('cover'),
|
||||
i.get('watched_show', {}).get('text_large'),
|
||||
0,
|
||||
i.get('uname'),
|
||||
style={"type": "rect", "ratio": 1.33}
|
||||
)
|
||||
vdata.append(data)
|
||||
return vdata, 9999
|
||||
|
||||
def huyaContent(self, tid, pg, filter, extend, vdata):
|
||||
if extend.get('cate') and pg == '1' and 'click' not in tid:
|
||||
id = extend.get('cate')
|
||||
data = self.fetch(f'{self.referers[tid]}/liveconfig/game/bussLive?bussType={id}',
|
||||
headers=self.headers[1]).json()
|
||||
for i in data['data']:
|
||||
v = self.buildvod(
|
||||
vod_id=f"click_{tid}@@{int(i['gid'])}",
|
||||
vod_name=i.get('gameFullName'),
|
||||
vod_pic=f'https://huyaimg.msstatic.com/cdnimage/game/{int(i["gid"])}-MS.jpg',
|
||||
vod_tag=1,
|
||||
style={"type": "oval", "ratio": 1}
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 1
|
||||
else:
|
||||
gid = ''
|
||||
if 'click' in tid:
|
||||
ids = tid.split('_')[1].split('@@')
|
||||
tid = ids[0]
|
||||
gid = f'&gameId={ids[1]}'
|
||||
data = self.fetch(f'{self.hosts[tid][0]}/cache.php?m=LiveList&do=getLiveListByPage&tagAll=0{gid}&page={pg}',
|
||||
headers=self.headers[1]).json()
|
||||
for i in data['data']['datas']:
|
||||
if i.get('profileRoom'):
|
||||
v = self.buildvod(
|
||||
f"{tid}@@{i['profileRoom']}",
|
||||
i.get('introduction'),
|
||||
i.get('screenshot'),
|
||||
str(int(i.get('totalCount', '1')) / 10000) + '万',
|
||||
0,
|
||||
i.get('nick'),
|
||||
style={"type": "rect", "ratio": 1.33}
|
||||
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 9999
|
||||
|
||||
def douyinContent(self, tid, pg, filter, extend, vdata):
|
||||
if extend.get('cate') and pg == '1' and 'click' not in tid:
|
||||
ids = extend.get('cate').split('@@')
|
||||
for i in self.dyifdata['categoryData']:
|
||||
c = i['partition']
|
||||
if c['id_str'] == ids[0] and c['title'] == ids[1]:
|
||||
vlist = i['sub_partition'].copy()
|
||||
vlist.insert(0, {'partition': c})
|
||||
for j in vlist:
|
||||
j = j['partition']
|
||||
v = self.buildvod(
|
||||
vod_id=f"click_{tid}@@{j['id_str']}@@{j['type']}",
|
||||
vod_name=j.get('title'),
|
||||
vod_pic='https://p3-pc-weboff.byteimg.com/tos-cn-i-9r5gewecjs/pwa_v3/512x512-1.png',
|
||||
vod_tag=1,
|
||||
style={"type": "oval", "ratio": 1}
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 1
|
||||
else:
|
||||
path = f'/webcast/web/partition/detail/room/?aid=6383&app_name=douyin_web&live_id=1&device_platform=web&count=15&offset={(int(pg) - 1) * 15}&partition=720&partition_type=1'
|
||||
if 'click' in tid:
|
||||
ids = tid.split('_')[1].split('@@')
|
||||
tid = ids[0]
|
||||
path = f'/webcast/web/partition/detail/room/?aid=6383&app_name=douyin_web&live_id=1&device_platform=web&count=15&offset={(int(pg) - 1) * 15}&partition={ids[1]}&partition_type={ids[-1]}&req_from=2'
|
||||
data = self.fetch(f'{self.hosts[tid]}{path}', headers=self.dyheaders).json()
|
||||
for i in data['data']['data']:
|
||||
v = self.buildvod(
|
||||
vod_id=f"{tid}@@{i['web_rid']}",
|
||||
vod_name=i['room'].get('title'),
|
||||
vod_pic=i['room']['cover'].get('url_list')[0],
|
||||
vod_year=i.get('user_count_str'),
|
||||
vod_remarks=i['room']['owner'].get('nickname'),
|
||||
style={"type": "rect", "ratio": 1.33}
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 9999
|
||||
|
||||
def douyuContent(self, tid, pg, filter, extend, vdata):
|
||||
if extend.get('cate') and pg == '1' and 'click' not in tid:
|
||||
for i in self.dyufdata['data']['cate2Info']:
|
||||
if str(i['cate1Id']) == extend['cate']:
|
||||
v = self.buildvod(
|
||||
vod_id=f"click_{tid}@@{i['cate2Id']}",
|
||||
vod_name=i.get('cate2Name'),
|
||||
vod_pic=i.get('icon'),
|
||||
vod_remarks=i.get('count'),
|
||||
vod_tag=1,
|
||||
style={"type": "oval", "ratio": 1}
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 1
|
||||
else:
|
||||
path = f'/japi/weblist/apinc/allpage/6/{pg}'
|
||||
if 'click' in tid:
|
||||
ids = tid.split('_')[1].split('@@')
|
||||
tid = ids[0]
|
||||
path = f'/gapi/rkc/directory/mixList/2_{ids[1]}/{pg}'
|
||||
url = f'{self.hosts[tid]}{path}'
|
||||
data = self.fetch(url, headers=self.headers[1]).json()
|
||||
for i in data['data']['rl']:
|
||||
v = self.buildvod(
|
||||
vod_id=f"{tid}@@{i['rid']}",
|
||||
vod_name=i.get('rn'),
|
||||
vod_pic=i.get('rs16'),
|
||||
vod_year=str(int(i.get('ol', 1)) / 10000) + '万',
|
||||
vod_remarks=i.get('nn'),
|
||||
style={"type": "rect", "ratio": 1.33}
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 9999
|
||||
|
||||
def detailContent(self, ids):
|
||||
ids = ids[0].split('@@')
|
||||
if ids[0] == 'wangyi':
|
||||
vod = self.wyccDetail(ids)
|
||||
elif ids[0] == 'bili':
|
||||
vod = self.biliDetail(ids)
|
||||
elif ids[0] == 'huya':
|
||||
vod = self.huyaDetail(ids)
|
||||
elif ids[0] == 'douyin':
|
||||
vod = self.douyinDetail(ids)
|
||||
elif ids[0] == 'douyu':
|
||||
vod = self.douyuDetail(ids)
|
||||
return {'list': [vod]}
|
||||
|
||||
def wyccDetail(self, ids):
|
||||
try:
|
||||
vdata = self.getpq(f'{self.hosts[ids[0]]}/{ids[1]}', self.headers[0])('script').eq(-1).text()
|
||||
|
||||
def get_quality_name(vbr):
|
||||
if vbr <= 600:
|
||||
return "标清"
|
||||
elif vbr <= 1000:
|
||||
return "高清"
|
||||
elif vbr <= 2000:
|
||||
return "超清"
|
||||
else:
|
||||
return "蓝光"
|
||||
|
||||
data = json.loads(vdata)['props']['pageProps']['roomInfoInitData']
|
||||
name = data['live'].get('title', ids[0])
|
||||
vod = self.buildvod(vod_name=data.get('keywords_suffix'), vod_remarks=data['live'].get('title'),
|
||||
vod_content=data.get('description_suffix'))
|
||||
resolution_data = data['live']['quickplay']['resolution']
|
||||
all_streams = {}
|
||||
sorted_qualities = sorted(resolution_data.items(),
|
||||
key=lambda x: x[1]['vbr'],
|
||||
reverse=True)
|
||||
for quality, data in sorted_qualities:
|
||||
vbr = data['vbr']
|
||||
quality_name = get_quality_name(vbr)
|
||||
for cdn_name, url in data['cdn'].items():
|
||||
if cdn_name not in all_streams and type(url) == str and url.startswith('http'):
|
||||
all_streams[cdn_name] = []
|
||||
if isinstance(url, str) and url.startswith('http'):
|
||||
all_streams[cdn_name].extend([quality_name, url])
|
||||
plists = []
|
||||
names = []
|
||||
for i, (cdn_name, stream_list) in enumerate(all_streams.items(), 1):
|
||||
names.append(f'线路{i}')
|
||||
pstr = f"{name}${ids[0]}@@{self.e64(json.dumps(stream_list))}"
|
||||
plists.append(pstr)
|
||||
vod['vod_play_from'] = "$$$".join(names)
|
||||
vod['vod_play_url'] = "$$$".join(plists)
|
||||
return vod
|
||||
except Exception as e:
|
||||
return self.handle_exception(e)
|
||||
|
||||
def biliDetail(self, ids):
|
||||
try:
|
||||
vdata = self.fetch(
|
||||
f'{self.hosts[ids[0]][0]}/xlive/web-room/v1/index/getInfoByRoom?room_id={ids[1]}&wts={int(time.time())}',
|
||||
headers=self.gethr(0, ids[0])).json()
|
||||
v = vdata['data']['room_info']
|
||||
vod = self.buildvod(
|
||||
vod_name=v.get('title'),
|
||||
type_name=v.get('parent_area_name') + '/' + v.get('area_name'),
|
||||
vod_remarks=v.get('tags'),
|
||||
vod_play_from=v.get('title'),
|
||||
)
|
||||
data = self.fetch(
|
||||
f'{self.hosts[ids[0]][0]}/xlive/web-room/v2/index/getRoomPlayInfo?room_id={ids[1]}&protocol=0%2C1&format=0%2C1%2C2&codec=0%2C1&platform=web',
|
||||
headers=self.gethr(0, ids[0])).json()
|
||||
vdnams = data['data']['playurl_info']['playurl']['g_qn_desc']
|
||||
all_accept_qns = []
|
||||
streams = data['data']['playurl_info']['playurl']['stream']
|
||||
for stream in streams:
|
||||
for format_item in stream['format']:
|
||||
for codec in format_item['codec']:
|
||||
if 'accept_qn' in codec:
|
||||
all_accept_qns.append(codec['accept_qn'])
|
||||
max_accept_qn = max(all_accept_qns, key=len) if all_accept_qns else []
|
||||
quality_map = {
|
||||
item['qn']: item['desc']
|
||||
for item in vdnams
|
||||
}
|
||||
quality_names = [f"{quality_map.get(qn)}${ids[0]}@@{ids[1]}@@{qn}" for qn in max_accept_qn]
|
||||
vod['vod_play_url'] = "#".join(quality_names)
|
||||
return vod
|
||||
except Exception as e:
|
||||
return self.handle_exception(e)
|
||||
|
||||
def huyaDetail(self, ids):
|
||||
try:
|
||||
vdata = self.fetch(f'{self.hosts[ids[0]][1]}/cache.php?m=Live&do=profileRoom&roomid={ids[1]}',
|
||||
headers=self.headers[0]).json()
|
||||
v = vdata['data']['liveData']
|
||||
vod = self.buildvod(
|
||||
vod_name=v.get('introduction'),
|
||||
type_name=v.get('gameFullName'),
|
||||
vod_director=v.get('nick'),
|
||||
vod_remarks=v.get('contentIntro'),
|
||||
)
|
||||
data = dict(reversed(list(vdata['data']['stream'].items())))
|
||||
names = []
|
||||
plist = []
|
||||
|
||||
for stream_type, stream_data in data.items():
|
||||
if isinstance(stream_data, dict) and 'multiLine' in stream_data and 'rateArray' in stream_data:
|
||||
names.append(f"线路{len(names) + 1}")
|
||||
qualities = sorted(
|
||||
stream_data['rateArray'],
|
||||
key=lambda x: (x['iBitRate'], x['sDisplayName']),
|
||||
reverse=True
|
||||
)
|
||||
cdn_urls = []
|
||||
for cdn in stream_data['multiLine']:
|
||||
quality_urls = []
|
||||
for quality in qualities:
|
||||
quality_name = quality['sDisplayName']
|
||||
bit_rate = quality['iBitRate']
|
||||
base_url = cdn['url']
|
||||
if bit_rate > 0:
|
||||
if '.m3u8' in base_url:
|
||||
new_url = base_url.replace(
|
||||
'ratio=2000',
|
||||
f'ratio={bit_rate}'
|
||||
)
|
||||
else:
|
||||
new_url = base_url.replace(
|
||||
'imgplus.flv',
|
||||
f'imgplus_{bit_rate}.flv'
|
||||
)
|
||||
else:
|
||||
new_url = base_url
|
||||
quality_urls.extend([quality_name, new_url])
|
||||
encoded_urls = self.e64(json.dumps(quality_urls))
|
||||
cdn_urls.append(f"{cdn['cdnType']}${ids[0]}@@{encoded_urls}")
|
||||
|
||||
if cdn_urls:
|
||||
plist.append('#'.join(cdn_urls))
|
||||
vod['vod_play_from'] = "$$$".join(names)
|
||||
vod['vod_play_url'] = "$$$".join(plist)
|
||||
return vod
|
||||
except Exception as e:
|
||||
return self.handle_exception(e)
|
||||
|
||||
def douyinDetail(self, ids):
|
||||
url = f'{self.hosts[ids[0]]}/webcast/room/web/enter/?aid=6383&app_name=douyin_web&live_id=1&device_platform=web&enter_from=web_live&web_rid={ids[1]}&room_id_str=&enter_source=&Room-Enter-User-Login-Ab=0&is_need_double_stream=false&cookie_enabled=true&screen_width=1980&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=125.0.0.0'
|
||||
data = self.fetch(url, headers=self.dyheaders).json()
|
||||
try:
|
||||
vdata = data['data']['data'][0]
|
||||
vod = self.buildvod(
|
||||
vod_name=vdata['title'],
|
||||
vod_remarks=vdata['user_count_str'],
|
||||
)
|
||||
resolution_data = vdata['stream_url']['live_core_sdk_data']['pull_data']['options']['qualities']
|
||||
stream_json = vdata['stream_url']['live_core_sdk_data']['pull_data']['stream_data']
|
||||
stream_json = json.loads(stream_json)
|
||||
available_types = []
|
||||
if any(sdk_key in stream_json['data'] and 'main' in stream_json['data'][sdk_key] for sdk_key in
|
||||
stream_json['data']):
|
||||
available_types.append('main')
|
||||
if any(sdk_key in stream_json['data'] and 'backup' in stream_json['data'][sdk_key] for sdk_key in
|
||||
stream_json['data']):
|
||||
available_types.append('backup')
|
||||
plist = []
|
||||
for line_type in available_types:
|
||||
format_arrays = {'flv': [], 'hls': [], 'lls': []}
|
||||
qualities = sorted(resolution_data, key=lambda x: x['level'], reverse=True)
|
||||
for quality in qualities:
|
||||
sdk_key = quality['sdk_key']
|
||||
if sdk_key in stream_json['data'] and line_type in stream_json['data'][sdk_key]:
|
||||
stream_info = stream_json['data'][sdk_key][line_type]
|
||||
if stream_info.get('flv'):
|
||||
format_arrays['flv'].extend([quality['name'], stream_info['flv']])
|
||||
if stream_info.get('hls'):
|
||||
format_arrays['hls'].extend([quality['name'], stream_info['hls']])
|
||||
if stream_info.get('lls'):
|
||||
format_arrays['lls'].extend([quality['name'], stream_info['lls']])
|
||||
format_urls = []
|
||||
for format_name, url_array in format_arrays.items():
|
||||
if url_array:
|
||||
encoded_urls = self.e64(json.dumps(url_array))
|
||||
format_urls.append(f"{format_name}${ids[0]}@@{encoded_urls}")
|
||||
|
||||
if format_urls:
|
||||
plist.append('#'.join(format_urls))
|
||||
|
||||
names = ['线路1', '线路2'][:len(plist)]
|
||||
vod['vod_play_from'] = "$$$".join(names)
|
||||
vod['vod_play_url'] = "$$$".join(plist)
|
||||
return vod
|
||||
|
||||
except Exception as e:
|
||||
return self.handle_exception(e)
|
||||
|
||||
def douyuDetail(self, ids):
|
||||
headers = self.gethr(0, zr=f'{self.hosts[ids[0]]}/{ids[1]}')
|
||||
try:
|
||||
data = self.fetch(f'{self.hosts[ids[0]]}/betard/{ids[1]}', headers=headers).json()
|
||||
vname = data['room']['room_name']
|
||||
vod = self.buildvod(
|
||||
vod_name=vname,
|
||||
vod_remarks=data['room'].get('second_lvl_name'),
|
||||
vod_director=data['room'].get('nickname'),
|
||||
)
|
||||
vdata = self.fetch(f'{self.hosts[ids[0]]}/swf_api/homeH5Enc?rids={ids[1]}', headers=headers).json()
|
||||
json_body = vdata['data']
|
||||
json_body = {"html": self.douyu_text(json_body[f'room{ids[1]}']), "rid": ids[1]}
|
||||
sign = self.post('http://alive.nsapps.cn/api/AllLive/DouyuSign', json=json_body, headers=self.headers[1]).json()['data']
|
||||
body = f'{sign}&cdn=&rate=-1&ver=Douyu_223061205&iar=1&ive=1&hevc=0&fa=0'
|
||||
body=self.params_to_json(body)
|
||||
nubdata = self.post(f'{self.hosts[ids[0]]}/lapi/live/getH5Play/{ids[1]}', data=body, headers=headers).json()
|
||||
plist = []
|
||||
names = []
|
||||
for i,x in enumerate(nubdata['data']['cdnsWithName']):
|
||||
names.append(f'线路{i+1}')
|
||||
d = {'sign': sign, 'cdn': x['cdn'], 'id': ids[1]}
|
||||
plist.append(
|
||||
f'{vname}${ids[0]}@@{self.e64(json.dumps(d))}@@{self.e64(json.dumps(nubdata["data"]["multirates"]))}')
|
||||
vod['vod_play_from'] = "$$$".join(names)
|
||||
vod['vod_play_url'] = "$$$".join(plist)
|
||||
return vod
|
||||
except Exception as e:
|
||||
return self.handle_exception(e)
|
||||
|
||||
def douyu_text(self, text):
|
||||
function_positions = [m.start() for m in re.finditer('function', text)]
|
||||
total_functions = len(function_positions)
|
||||
if total_functions % 2 == 0:
|
||||
target_index = total_functions // 2 + 1
|
||||
else:
|
||||
target_index = (total_functions - 1) // 2 + 1
|
||||
if total_functions >= target_index:
|
||||
cut_position = function_positions[target_index - 1]
|
||||
ctext = text[4:cut_position]
|
||||
return re.sub(r'eval\(strc\)\([\w\d,]+\)', 'strc', ctext)
|
||||
return text
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
pass
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
ids = id.split('@@')
|
||||
p = 1
|
||||
if ids[0] in ['wangyi', 'douyin','huya']:
|
||||
p, url = 0, json.loads(self.d64(ids[1]))
|
||||
elif ids[0] == 'bili':
|
||||
p, url = self.biliplay(ids)
|
||||
elif ids[0] == 'huya':
|
||||
p, url = 0, json.loads(self.d64(ids[1]))
|
||||
elif ids[0] == 'douyu':
|
||||
p, url = self.douyuplay(ids)
|
||||
return {'parse': p, 'url': url, 'header': self.playheaders[ids[0]]}
|
||||
except Exception as e:
|
||||
return {'parse': 1, 'url': self.excepturl, 'header': self.headers[0]}
|
||||
|
||||
def biliplay(self, ids):
|
||||
try:
|
||||
data = self.fetch(
|
||||
f'{self.hosts[ids[0]][0]}/xlive/web-room/v2/index/getRoomPlayInfo?room_id={ids[1]}&protocol=0,1&format=0,2&codec=0&platform=web&qn={ids[2]}',
|
||||
headers=self.gethr(0, ids[0])).json()
|
||||
urls = []
|
||||
line_index = 1
|
||||
for stream in data['data']['playurl_info']['playurl']['stream']:
|
||||
for format_item in stream['format']:
|
||||
for codec in format_item['codec']:
|
||||
for url_info in codec['url_info']:
|
||||
full_url = f"{url_info['host']}/{codec['base_url'].lstrip('/')}{url_info['extra']}"
|
||||
urls.extend([f"线路{line_index}", full_url])
|
||||
line_index += 1
|
||||
return 0, urls
|
||||
except Exception as e:
|
||||
return 1, self.excepturl
|
||||
|
||||
def douyuplay(self, ids):
|
||||
try:
|
||||
sdata = json.loads(self.d64(ids[1]))
|
||||
headers = self.gethr(0, zr=f'{self.hosts[ids[0]]}/{sdata["id"]}')
|
||||
ldata = json.loads(self.d64(ids[2]))
|
||||
result_obj = {}
|
||||
with ThreadPoolExecutor(max_workers=len(ldata)) as executor:
|
||||
futures = [
|
||||
executor.submit(
|
||||
self.douyufp,
|
||||
sdata,
|
||||
quality,
|
||||
headers,
|
||||
self.hosts[ids[0]],
|
||||
result_obj
|
||||
) for quality in ldata
|
||||
]
|
||||
for future in futures:
|
||||
future.result()
|
||||
|
||||
result = []
|
||||
for bit in sorted(result_obj.keys(), reverse=True):
|
||||
result.extend(result_obj[bit])
|
||||
|
||||
if result:
|
||||
return 0, result
|
||||
return 1, self.excepturl
|
||||
|
||||
except Exception as e:
|
||||
return 1, self.excepturl
|
||||
|
||||
def douyufp(self, sdata, quality, headers, host, result_obj):
|
||||
try:
|
||||
body = f'{sdata["sign"]}&cdn={sdata["cdn"]}&rate={quality["rate"]}'
|
||||
body=self.params_to_json(body)
|
||||
data = self.post(f'{host}/lapi/live/getH5Play/{sdata["id"]}',
|
||||
data=body, headers=headers).json()
|
||||
if data.get('data'):
|
||||
play_url = data['data']['rtmp_url'] + '/' + data['data']['rtmp_live']
|
||||
bit = quality.get('bit', 0)
|
||||
if bit not in result_obj:
|
||||
result_obj[bit] = []
|
||||
result_obj[bit].extend([quality['name'], play_url])
|
||||
except Exception as e:
|
||||
print(f"Error fetching {quality['name']}: {str(e)}")
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def josn_to_params(self, params, skip_empty=False):
|
||||
query = []
|
||||
for k, v in params.items():
|
||||
if skip_empty and not v:
|
||||
continue
|
||||
query.append(f"{k}={v}")
|
||||
return "&".join(query)
|
||||
|
||||
def params_to_json(self, query_string):
|
||||
parsed_data = parse_qs(query_string)
|
||||
result = {key: value[0] for key, value in parsed_data.items()}
|
||||
return result
|
||||
|
||||
def buildvod(self, vod_id='', vod_name='', vod_pic='', vod_year='', vod_tag='', vod_remarks='', style='',
|
||||
type_name='', vod_area='', vod_actor='', vod_director='',
|
||||
vod_content='', vod_play_from='', vod_play_url=''):
|
||||
vod = {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': vod_name,
|
||||
'vod_pic': vod_pic,
|
||||
'vod_year': vod_year,
|
||||
'vod_tag': 'folder' if vod_tag else '',
|
||||
'vod_remarks': vod_remarks,
|
||||
'style': style,
|
||||
'type_name': type_name,
|
||||
'vod_area': vod_area,
|
||||
'vod_actor': vod_actor,
|
||||
'vod_director': vod_director,
|
||||
'vod_content': vod_content,
|
||||
'vod_play_from': vod_play_from,
|
||||
'vod_play_url': vod_play_url
|
||||
}
|
||||
vod = {key: value for key, value in vod.items() if value}
|
||||
return vod
|
||||
|
||||
def getpq(self, url, headers=None, cookies=None):
|
||||
data = self.fetch(url, headers=headers, cookies=cookies).text
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"解析页面错误: {str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
|
||||
def gethr(self, index, rf='', zr=''):
|
||||
headers = self.headers[index]
|
||||
if zr:
|
||||
headers['referer'] = zr
|
||||
else:
|
||||
headers['referer'] = f"{self.referers[rf]}/"
|
||||
return headers
|
||||
|
||||
def handle_exception(self, e):
|
||||
print(f"报错: {str(e)}")
|
||||
return {'vod_play_from': '哎呀翻车啦', 'vod_play_url': f'翻车啦${self.excepturl}'}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import re
|
||||
import sys
|
||||
from Crypto.Hash import MD5
|
||||
sys.path.append("..")
|
||||
import json
|
||||
import time
|
||||
from pyquery import PyQuery as pq
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = 'https://www.lreeok.vip'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'Origin': host,
|
||||
'Referer': f"{host}/",
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self.getpq(self.fetch(self.host, headers=self.headers).text)
|
||||
result = {}
|
||||
classes = []
|
||||
for k in data('.head-more.box a').items():
|
||||
i = k.attr('href')
|
||||
if i and '/vod' in i:
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': re.search(r'\d+', i).group(0)
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list'] = self.getlist(data('.border-box.diy-center .public-list-div'))
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {'type': tid, 'class': '', 'area': '', 'lang': '', 'version': '', 'state': '', 'letter': '', 'page': pg}
|
||||
data = self.post(f"{self.host}/index.php/api/vod", headers=self.headers, data=self.getbody(body)).json()
|
||||
result = {}
|
||||
result['list'] = data['list']
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.getpq(self.fetch(f"{self.host}/voddetail/{ids[0]}.html", headers=self.headers).text)
|
||||
v = data('.detail-info.lightSpeedIn .slide-info')
|
||||
vod = {
|
||||
'vod_year': v.eq(-1).text(),
|
||||
'vod_remarks': v.eq(0).text(),
|
||||
'vod_actor': v.eq(3).text(),
|
||||
'vod_director': v.eq(2).text(),
|
||||
'vod_content': data('.switch-box #height_limit').text()
|
||||
}
|
||||
np = data('.anthology.wow.fadeInUp')
|
||||
ndata = np('.anthology-tab .swiper-wrapper .swiper-slide')
|
||||
pdata = np('.anthology-list .anthology-list-box ul')
|
||||
play, names = [], []
|
||||
for i in range(len(ndata)):
|
||||
n = ndata.eq(i)('a')
|
||||
n('span').remove()
|
||||
names.append(n.text())
|
||||
vs = []
|
||||
for v in pdata.eq(i)('li').items():
|
||||
vs.append(f"{v.text()}${v('a').attr('href')}")
|
||||
play.append('#'.join(vs))
|
||||
vod["vod_play_from"] = "$$$".join(names)
|
||||
vod["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
# data = self.getpq(self.fetch(f"{self.host}/vodsearch/{key}----------{pg}---.html", headers=self.headers).text)
|
||||
# return {'list': self.getlist(data('.row-right .search-box .public-list-bj')), 'page': pg}
|
||||
data = self.fetch(
|
||||
f"{self.host}/index.php/ajax/suggest?mid={pg}&wd={key}&limit=999×tamp={int(time.time() * 1000)}",
|
||||
headers=self.headers).json()
|
||||
videos = []
|
||||
for i in data['list']:
|
||||
videos.append({
|
||||
'vod_id': i['id'],
|
||||
'vod_name': i['name'],
|
||||
'vod_pic': i['pic']
|
||||
})
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
h, p = {"User-Agent": "okhttp/3.14.9"}, 1
|
||||
url = f"{self.host}{id}"
|
||||
data = self.getpq(self.fetch(url, headers=self.headers).text)
|
||||
try:
|
||||
jstr = data('.player .player-left script').eq(0).text()
|
||||
jsdata = json.loads(jstr.split('aaa=')[-1])
|
||||
body = {'url': jsdata['url']}
|
||||
if not re.search(r'\.m3u8|\.mp4', body['url']):
|
||||
data = self.post(f"{self.host}/okplay/api_config.php", headers=self.headers,
|
||||
data=self.getbody(body)).json()
|
||||
url = data.get('url') or data.get('data', {}).get('url')
|
||||
p = 0
|
||||
except Exception as e:
|
||||
print('错误信息:', e)
|
||||
pass
|
||||
result = {}
|
||||
result["parse"] = p
|
||||
result["url"] = url
|
||||
result["header"] = h
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def getbody(self, params):
|
||||
t = int(time.time())
|
||||
h = MD5.new()
|
||||
h.update(f"DS{t}DCC147D11943AF75".encode('utf-8'))
|
||||
key = h.hexdigest()
|
||||
params.update({'time': t, 'key': key})
|
||||
return params
|
||||
|
||||
def getlist(self, data):
|
||||
videos = []
|
||||
for i in data.items():
|
||||
id = i('a').attr('href')
|
||||
if id:
|
||||
id = re.search(r'\d+', id).group(0)
|
||||
img = i('img').attr('data-src')
|
||||
if img and 'url=' in img: img = f'{self.host}{img}'
|
||||
videos.append({
|
||||
'vod_id': id,
|
||||
'vod_name': i('img').attr('alt'),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': i('.public-prt').text() or i('.public-list-prb').text()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getpq(self, data):
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
@@ -0,0 +1,190 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pprint import pprint
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.chost,self.token=self.gettoken()
|
||||
self.phost='https://wsrv.nl?url=https://image.tmdb.org/t/p/w500'
|
||||
# self.chost,self.token= 'https://api.themoviedb.org/3','eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJhZDFhYmJjMmM4YjhkY2I2NzJiYzI1Y2M3ZDcxYzVhOCIsIm5iZiI6MTczNzUxMTI4MC4wOCwic3ViIjoiNjc5MDUxNzA1NTBlNmZjM2NkZGZlOThiIiwic2NvcGVzIjpbImFwaV9yZWFkIl0sInZlcnNpb24iOjF9.fmGzxmyxA-r74R_1_wo-sPHtfOn3zyGQqzPxr3NUIII'
|
||||
# print(self.chost,self.token)
|
||||
self.headers.update({'authorization': f"Bearer {self.token}"})
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
headers ={
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.7103.48 Safari/537.36',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="136", "Google Chrome";v="136"',
|
||||
'origin': 'https://nunflix.org',
|
||||
'referer': 'https://nunflix.org/',
|
||||
}
|
||||
|
||||
jx='https://111movies.com'
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cate = {
|
||||
"电影": "movie",
|
||||
"剧集": "tv"
|
||||
}
|
||||
classes = []
|
||||
filters = {}
|
||||
for k, j in cate.items():
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': j
|
||||
})
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data=self.fetch(f"{self.chost}/trending/all/week",headers=self.headers).json()
|
||||
return {'list':self.getlist(data['results'])}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
params = {'page':pg}
|
||||
data=self.fetch(f'{self.chost}/discover/{tid}',params=params,headers=self.headers).json()
|
||||
result = {}
|
||||
result['list'] = self.getlist(data['results'],tid)
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
v=self.fetch(f'{self.chost}{ids[0]}',headers=self.headers).json()
|
||||
if 'movie' in ids[0]:
|
||||
p=f"{v.get('title') or v.get('name')}${ids[0]}"
|
||||
else:
|
||||
p='#'.join([f"{i.get('name')}${ids[0]}/{i.get('season_number')}/1" for i in v.get('seasons')])
|
||||
vod = {
|
||||
'vod_year': v.get('release_date') or v.get('last_air_date'),
|
||||
'vod_area': v.get('original_language'),
|
||||
'vod_remarks': v.get('tagline'),
|
||||
'vod_content': v.get('overview'),
|
||||
'vod_play_from': '默认',
|
||||
'vod_play_url': p
|
||||
}
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=self.fetch(f'{self.chost}/search/multi',params={'query':key,'page':pg},headers=self.headers).json()
|
||||
return {'list':self.getlist(data['results']),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
data=self.fetch(f'{self.jx}{id}',headers=self.headers).text
|
||||
jstr=json.loads(pq(data)('#__NEXT_DATA__').text())
|
||||
url=self.encrypt_data(jstr['props']['pageProps'].get('data'))
|
||||
return {'parse': 0, 'url': url, 'header': self.jxh()}
|
||||
|
||||
def getlist(self,data,tid=''):
|
||||
videos = []
|
||||
for i in data:
|
||||
videos.append({
|
||||
'vod_id': f"/{tid or i.get('media_type')}/{i.get('id')}",
|
||||
'vod_name': i.get('title') or i.get('name'),
|
||||
'vod_pic': f"{self.phost}{i.get('backdrop_path')}",
|
||||
'vod_remarks': f"{i.get('popularity', 0):.2f}",
|
||||
})
|
||||
return videos
|
||||
|
||||
def encrypt_data(self,data_str):
|
||||
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])
|
||||
iv = bytes([147, 233, 144, 118, 246, 33, 110, 119, 13, 209, 140, 42, 32, 186, 47, 89])
|
||||
xkey = bytes([238, 123, 35, 56, 43, 184, 57, 233, 233, 41])
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
padded_data = pad(data_str.encode(), AES.block_size)
|
||||
encrypted_data = cipher.encrypt(padded_data)
|
||||
hex_data = encrypted_data.hex()
|
||||
result = ""
|
||||
for i in range(len(hex_data)):
|
||||
char_code = ord(hex_data[i])
|
||||
xor_value = xkey[i % len(xkey)]
|
||||
result += chr(char_code ^ xor_value)
|
||||
|
||||
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))}
|
||||
final_result = ''.join([char_map.get(c, c) for c in base64_result])
|
||||
return self.geturl(final_result)
|
||||
|
||||
def geturl(self,txt):
|
||||
data=self.post(f"{self.jx}/rijevra/{txt}/sr",headers=self.jxh()).json()
|
||||
urls=[]
|
||||
for i in data:
|
||||
urls.extend([i['name'],f"{self.getProxyUrl()}&dddd={i['data']}"])
|
||||
return urls
|
||||
|
||||
def localProxy(self, param):
|
||||
try:
|
||||
data=self.post(f"{self.jx}/rijevra/{param.get('dddd')}",headers=self.jxh()).json()
|
||||
return [302,'application/vnd.apple.mpegurl',None,{'Location':data['url']}]
|
||||
except Exception as e:
|
||||
self.log(e)
|
||||
return ''
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
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 gettoken(self):
|
||||
host='https://nunflix.org'
|
||||
data=self.fetch(f'{host}/explore/movie',headers=self.headers).text
|
||||
mod=pq(data)('script[type="module"]').attr('src')
|
||||
murl= mod if mod.startswith('http') else f'{host}{mod}'
|
||||
print(murl)
|
||||
mdd=self.fetch(murl,headers=self.headers).text
|
||||
ane_match = re.search(r'Ane\s*=\s*"([^"]+)"', mdd)
|
||||
ane_value = ane_match.group(1) if ane_match else ''
|
||||
xne_match = re.search(r'xne\s*=\s*"([^"]+)"', mdd)
|
||||
xne_value = xne_match.group(1) if xne_match else ''
|
||||
if ane_value and xne_value:
|
||||
return ane_value.strip(),xne_value.strip()
|
||||
|
||||
if __name__ == "__main__":
|
||||
sp = Spider()
|
||||
formatJo = sp.init()
|
||||
# formatJo = sp.homeContent(False) # 主页,等于真表示启用筛选
|
||||
formatJo = sp.homeVideoContent() # 主页视频
|
||||
# formatJo = sp.searchContent("斗罗",False,'1') # 搜索{"area":"大陆","by":"hits","class":"国产","lg":"国语"}
|
||||
# formatJo = sp.categoryContent('movie', '1', False, {}) # 分类
|
||||
# formatJo = sp.detailContent(['/tv/93405']) # 详情
|
||||
# formatJo = sp.playerContent("","/tv/93405/1/1",{}) # 播放
|
||||
# formatJo = sp.localProxy({"dddd":""}) # 播放
|
||||
pprint(formatJo)
|
||||
@@ -0,0 +1,372 @@
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from urllib.parse import urlparse, urlencode
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class CloudSearchSpider(Spider):
|
||||
"""网盘资源搜索爬虫"""
|
||||
|
||||
DEFAULT_BASE_URL = "https://so.252035.xyz"
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
SEARCH_PAGE_SIZE = 300
|
||||
REQUEST_TIMEOUT = 30
|
||||
MAX_RETRIES = 3
|
||||
BACKOFF_FACTOR = 0.5
|
||||
|
||||
PAN_CONFIG = {
|
||||
'ali': {
|
||||
'api_type': 'aliyun',
|
||||
'name': '阿里',
|
||||
'keywords': ['alipan.com', 'aliyundrive.com'],
|
||||
'icon': 'ali.png'
|
||||
},
|
||||
'quark': {
|
||||
'api_type': 'quark',
|
||||
'name': '夸克',
|
||||
'keywords': ['pan.quark.cn'],
|
||||
'icon': 'quark.png'
|
||||
},
|
||||
'uc': {
|
||||
'api_type': 'uc',
|
||||
'name': 'UC',
|
||||
'keywords': ['drive.uc.cn'],
|
||||
'icon': 'uc.png'
|
||||
},
|
||||
'xunlei': {
|
||||
'api_type': 'xunlei',
|
||||
'name': '迅雷',
|
||||
'keywords': ['xunlei', 'thunder'],
|
||||
'icon': 'xunlei.png'
|
||||
},
|
||||
'a123': {
|
||||
'api_type': '123',
|
||||
'name': '123',
|
||||
'keywords': ['123684.com', '123685.com', '123912.com', '123pan.com', '123pan.cn', '123592.com'],
|
||||
'icon': '123.png'
|
||||
},
|
||||
'a189': {
|
||||
'api_type': 'tianyi',
|
||||
'name': '天翼',
|
||||
'keywords': ['cloud.189.cn'],
|
||||
'icon': '189.png'
|
||||
},
|
||||
'a139': {
|
||||
'api_type': 'mobile',
|
||||
'name': '移动',
|
||||
'keywords': ['caiyun.139.com'],
|
||||
'icon': '139.png'
|
||||
},
|
||||
'a115': {
|
||||
'api_type': '115',
|
||||
'name': '115',
|
||||
'keywords': ['115cdn.com', '115.com', 'anxia.com'],
|
||||
'icon': '115.png'
|
||||
},
|
||||
'baidu': {
|
||||
'api_type': 'baidu',
|
||||
'name': '百度',
|
||||
'keywords': ['baidu'],
|
||||
'icon': 'baidu.png'
|
||||
},
|
||||
'pikpak': {
|
||||
'api_type': 'pikpak',
|
||||
'name': 'PikPak',
|
||||
'keywords': ['pikpak'],
|
||||
'icon': 'pikpak.png'
|
||||
},
|
||||
'magnet': {
|
||||
'api_type': 'magnet',
|
||||
'name': '磁力',
|
||||
'keywords': ['magnet'],
|
||||
'icon': 'cili.png'
|
||||
},
|
||||
'ed2k': {
|
||||
'api_type': 'ed2k',
|
||||
'name': '电驴',
|
||||
'keywords': ['ed2k'],
|
||||
'icon': ''
|
||||
}
|
||||
}
|
||||
|
||||
REVERSE_PAN_MAP = {v['api_type']: k for k, v in PAN_CONFIG.items()}
|
||||
# 优化:从 PAN_CONFIG 自动派生网盘简称,消除数据冗余
|
||||
PAN_SHORT_NAMES = {k: v['name'] for k, v in PAN_CONFIG.items()}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.base_url = self.DEFAULT_BASE_URL
|
||||
self.proxy = ''
|
||||
self.pan_priority = ''
|
||||
self.pan_order = ''
|
||||
self.channels = ''
|
||||
self.plugins = ''
|
||||
self.cloud_types = ''
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.HEADERS)
|
||||
|
||||
# 增强重试策略
|
||||
retries = Retry(
|
||||
total=self.MAX_RETRIES,
|
||||
backoff_factor=self.BACKOFF_FACTOR,
|
||||
status_forcelist=[429, 500, 502, 503, 504],
|
||||
raise_on_status=False
|
||||
)
|
||||
self.session.mount('http://', HTTPAdapter(max_retries=retries))
|
||||
self.session.mount('https://', HTTPAdapter(max_retries=retries))
|
||||
|
||||
def init(self, extend):
|
||||
try:
|
||||
extend_dict = json.loads(extend) if extend else {}
|
||||
self.base_url = extend_dict.get('server', self.DEFAULT_BASE_URL).rstrip('/')
|
||||
self.proxy = extend_dict.get('proxy', '')
|
||||
self.pan_priority = extend_dict.get('pan_priority', '')
|
||||
self.pan_order = extend_dict.get('pan_order', '')
|
||||
# 新增三个搜索过滤参数
|
||||
self.channels = extend_dict.get('channels', '')
|
||||
self.plugins = extend_dict.get('plugins', '')
|
||||
self.cloud_types = extend_dict.get('cloud_types', '')
|
||||
except json.JSONDecodeError:
|
||||
self._reset_to_defaults()
|
||||
|
||||
# 代理协议分离
|
||||
if self.proxy:
|
||||
self.session.proxies = {
|
||||
"http": self.proxy,
|
||||
"https": self.proxy
|
||||
}
|
||||
|
||||
def _reset_to_defaults(self):
|
||||
self.base_url = self.DEFAULT_BASE_URL
|
||||
self.proxy = ''
|
||||
self.pan_priority = ''
|
||||
self.pan_order = ''
|
||||
self.channels = ''
|
||||
self.plugins = ''
|
||||
self.cloud_types = ''
|
||||
|
||||
def getName(self):
|
||||
return "盘搜"
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {'class': [{"type_id": "1", "type_name": "盘搜|聚合搜索"}], 'list': []}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {}
|
||||
|
||||
def categoryContent(self, cid, page, filter, ext):
|
||||
return {
|
||||
'list': [{"vod_id": "1", "vod_name": "请在搜索框中输入关键词搜索", "vod_pic": "", "vod_remarks": "盘搜"}],
|
||||
'page': 1, 'pagecount': 1, 'limit': 1, 'total': 1
|
||||
}
|
||||
|
||||
def detailContent(self, did):
|
||||
result = {'list': []}
|
||||
if not did or not did[0]:
|
||||
return result
|
||||
|
||||
resource_url = did[0]
|
||||
pan_type = self._extract_pan_type_from_url(resource_url)
|
||||
pan_config = self.PAN_CONFIG.get(pan_type, {})
|
||||
pan_name = pan_config.get('name', '网盘资源')
|
||||
pan_icon = self._get_icon_url(pan_config.get('icon', ''))
|
||||
|
||||
result['list'].append({
|
||||
"vod_id": resource_url,
|
||||
"vod_name": f"{pan_name}资源",
|
||||
"vod_pic": pan_icon,
|
||||
"vod_play_from": "盘搜",
|
||||
"vod_play_url": f"盘搜${resource_url}",
|
||||
"vod_content": f"网盘类型: {pan_name}\n资源链接: {resource_url}"
|
||||
})
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
return self._perform_search(key, pg)
|
||||
|
||||
def searchContentPage(self, key, quick, page):
|
||||
return self._perform_search(key, page)
|
||||
|
||||
def _perform_search(self, keywords, page_str):
|
||||
try:
|
||||
page = int(page_str)
|
||||
except (ValueError, TypeError):
|
||||
page = 1
|
||||
|
||||
result = {'list': [], 'page': page, 'pagecount': 1, 'limit': self.SEARCH_PAGE_SIZE, 'total': 0}
|
||||
if not keywords:
|
||||
return result
|
||||
|
||||
try:
|
||||
# 构建查询参数
|
||||
params = {'kw': keywords}
|
||||
if self.channels:
|
||||
params['channels'] = self.channels
|
||||
if self.plugins:
|
||||
params['plugins'] = self.plugins
|
||||
if self.cloud_types:
|
||||
params['cloud_types'] = self.cloud_types
|
||||
|
||||
url = f"{self.base_url}/api/search"
|
||||
search_response = self.session.get(
|
||||
url,
|
||||
params=params,
|
||||
timeout=self.REQUEST_TIMEOUT
|
||||
)
|
||||
search_response.raise_for_status()
|
||||
|
||||
search_data = search_response.json()
|
||||
if search_data.get('code') != 0:
|
||||
return result
|
||||
|
||||
all_results = self._parse_and_sort_results(search_data, keywords)
|
||||
total_count = len(all_results)
|
||||
start_index = (page - 1) * self.SEARCH_PAGE_SIZE
|
||||
paged_results = all_results[start_index:start_index + self.SEARCH_PAGE_SIZE]
|
||||
|
||||
result.update({
|
||||
'list': paged_results,
|
||||
'total': total_count,
|
||||
'pagecount': max(1, (total_count + self.SEARCH_PAGE_SIZE - 1) // self.SEARCH_PAGE_SIZE)
|
||||
})
|
||||
|
||||
except requests.RequestException:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
def _parse_and_sort_results(self, data, keywords):
|
||||
# 优化:移除此处的硬编码字典,它已在类级别定义
|
||||
|
||||
if self.pan_order:
|
||||
enabled_pan_types = [pan.strip() for pan in self.pan_order.split(',') if pan.strip()]
|
||||
use_pan_order = True
|
||||
elif self.pan_priority:
|
||||
enabled_pan_types = [pan.strip() for pan in self.pan_priority.split(',') if pan.strip()]
|
||||
use_pan_order = False
|
||||
else:
|
||||
enabled_pan_types = []
|
||||
use_pan_order = False
|
||||
|
||||
all_items = []
|
||||
all_images = []
|
||||
|
||||
# 收集所有图片用于兜底随机
|
||||
for items in data.get('data', {}).get('merged_by_type', {}).values():
|
||||
for item in items:
|
||||
if item.get('images'):
|
||||
all_images.extend(item['images'])
|
||||
|
||||
# 设置随机种子,确保同关键词结果一致
|
||||
random.seed(hash(keywords))
|
||||
|
||||
for cloud_type, items in data.get('data', {}).get('merged_by_type', {}).items():
|
||||
pan_type = self.REVERSE_PAN_MAP.get(cloud_type, cloud_type)
|
||||
if enabled_pan_types and pan_type not in enabled_pan_types:
|
||||
continue
|
||||
|
||||
for item in items:
|
||||
url = item.get('url')
|
||||
if not url:
|
||||
continue
|
||||
|
||||
pan_config = self.PAN_CONFIG.get(pan_type, {})
|
||||
pan_icon = self._get_icon_url(pan_config.get('icon', ''))
|
||||
# 优化:使用 self.PAN_SHORT_NAMES 替代本地字典
|
||||
pan_short = self.PAN_SHORT_NAMES.get(pan_type, '网盘')
|
||||
|
||||
# 简化图片逻辑:优先自带 → 全局随机 → 图标
|
||||
vod_pic = self._get_best_image(item, all_images, pan_icon)
|
||||
|
||||
dt_obj = self._to_datetime(item.get('datetime'))
|
||||
time_str = dt_obj.strftime("%m-%d") if dt_obj else ""
|
||||
|
||||
source = item.get('source', '盘搜')
|
||||
remarks = f"{pan_short}:{time_str}|{source}" if time_str else f"{pan_short}|{source}"
|
||||
|
||||
all_items.append({
|
||||
"vod_id": url,
|
||||
"vod_name": item.get('note', ''),
|
||||
"vod_pic": vod_pic,
|
||||
"vod_remarks": remarks,
|
||||
"_timestamp": dt_obj.timestamp() if dt_obj else 0,
|
||||
"_pan_type": pan_type
|
||||
})
|
||||
|
||||
# 排序逻辑
|
||||
if use_pan_order:
|
||||
all_items.sort(key=lambda x: -x['_timestamp'])
|
||||
elif enabled_pan_types:
|
||||
pan_priority_order = {pan: idx for idx, pan in enumerate(enabled_pan_types)}
|
||||
all_items.sort(key=lambda x: (pan_priority_order.get(x['_pan_type'], len(enabled_pan_types)), -x['_timestamp']))
|
||||
else:
|
||||
all_items.sort(key=lambda x: -x['_timestamp'])
|
||||
|
||||
# 清理临时字段
|
||||
for item in all_items:
|
||||
item.pop('_timestamp', None)
|
||||
item.pop('_pan_type', None)
|
||||
|
||||
return all_items
|
||||
|
||||
def _get_best_image(self, item, all_images, pan_icon):
|
||||
if item.get('images') and item['images']:
|
||||
return item['images'][0]
|
||||
if all_images:
|
||||
return random.choice(all_images)
|
||||
return pan_icon
|
||||
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
result = {"parse": 0, "header": self.HEADERS, "url": ""}
|
||||
if not pid:
|
||||
return result
|
||||
|
||||
url = pid.strip()
|
||||
if not url.startswith('push:'):
|
||||
if not url.startswith(('http://', 'https://')):
|
||||
url = f'https://{url}'
|
||||
result['url'] = f"push:{url}"
|
||||
else:
|
||||
result['url'] = url
|
||||
|
||||
return result
|
||||
|
||||
def _to_datetime(self, time_str):
|
||||
if not time_str or time_str == "0001-01-01T00:00:00Z":
|
||||
return None
|
||||
try:
|
||||
time_str_clean = time_str.replace('Z', '+00:00')
|
||||
return datetime.fromisoformat(time_str_clean)
|
||||
except (ValueError, TypeError):
|
||||
try:
|
||||
return datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S')
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def _extract_pan_type_from_url(self, url):
|
||||
if not url:
|
||||
return "unknown"
|
||||
url_lower = urlparse(url).netloc.lower() or url.lower()
|
||||
for pan_type, config in self.PAN_CONFIG.items():
|
||||
if any(keyword in url_lower for keyword in config['keywords']):
|
||||
return pan_type
|
||||
return "unknown"
|
||||
|
||||
def _get_icon_url(self, icon_name):
|
||||
if not icon_name:
|
||||
return ""
|
||||
return f"http://127.0.0.1:9978/file/Download/lib/icon/{icon_name}"
|
||||
|
||||
Spider = CloudSearchSpider
|
||||
@@ -0,0 +1,365 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import re
|
||||
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/136.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="136", "Google Chrome";v="136"',
|
||||
'origin': 'https://redflix.co',
|
||||
'referer': 'https://redflix.co/',
|
||||
}
|
||||
|
||||
def init(self, extend=""):
|
||||
self.site = 'https://redflix.co'
|
||||
self.chost, self.token = self.gettoken()
|
||||
self.phost = 'https://image.tmdb.org/t/p/w500'
|
||||
|
||||
self.translate_enabled = True
|
||||
|
||||
self.translate_urls = [
|
||||
'https://api.mymemory.translated.net/get',
|
||||
'https://translate.argosopentech.com/translate'
|
||||
]
|
||||
|
||||
self.servers = {
|
||||
'vidfast': 'https://vidfast.pro',
|
||||
'vidrock': 'https://vidrock.net',
|
||||
'vidlink': 'https://vidlink.pro',
|
||||
'videasy': 'https://player.videasy.net',
|
||||
}
|
||||
self.server_order = ['vidfast', 'vidrock', 'vidlink', 'videasy']
|
||||
|
||||
self.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="136", "Google Chrome";v="136"',
|
||||
'origin': self.site,
|
||||
'referer': f'{self.site}/',
|
||||
'accept': 'application/json'
|
||||
})
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "Redflix"
|
||||
|
||||
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):
|
||||
result = {}
|
||||
cate = {
|
||||
"电影": "movie",
|
||||
"剧集": "tv"
|
||||
}
|
||||
classes = []
|
||||
filters = {}
|
||||
for k, j in cate.items():
|
||||
classes.append({'type_name': k, 'type_id': j})
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self.fetch(
|
||||
f"{self.chost}/trending/all/day",
|
||||
params={'api_key': self.token, 'language': 'en-US', 'page': 1},
|
||||
headers=self.headers
|
||||
).json()
|
||||
return {'list': self.getlist(data.get('results', []))}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
params = {'page': pg, 'api_key': self.token, 'language': 'en-US'}
|
||||
data = self.fetch(f'{self.chost}/discover/{tid}', params=params, headers=self.headers).json()
|
||||
result = {
|
||||
'list': self.getlist(data.get('results', []), tid),
|
||||
'page': pg,
|
||||
'pagecount': 9999,
|
||||
'limit': 90,
|
||||
'total': 999999
|
||||
}
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
path = ids[0]
|
||||
v = self.fetch(
|
||||
f'{self.chost}{path}',
|
||||
params={'api_key': self.token, 'language': 'en-US', 'append_to_response': 'videos'},
|
||||
headers=self.headers
|
||||
).json()
|
||||
is_movie = '/movie/' in path
|
||||
if is_movie:
|
||||
play_str = f"{v.get('title') or v.get('name')}${path}"
|
||||
else:
|
||||
seasons = v.get('seasons') or []
|
||||
play_items = [
|
||||
f"{i.get('name')}${path}/{i.get('season_number')}/1" for i in seasons if i.get('season_number')
|
||||
]
|
||||
play_str = '#'.join(play_items) if play_items else f"{v.get('name')}${path}/1/1"
|
||||
|
||||
title = v.get('title') or v.get('name') or ''
|
||||
overview = v.get('overview') or ''
|
||||
tagline = v.get('tagline') or ''
|
||||
|
||||
if self.translate_enabled:
|
||||
try:
|
||||
if title:
|
||||
translated_title = self.simple_translate(title)
|
||||
if translated_title and translated_title != title:
|
||||
title = f"{title}\n{translated_title}"
|
||||
|
||||
if overview:
|
||||
translated_overview = self.simple_translate(overview)
|
||||
if translated_overview and translated_overview != overview:
|
||||
overview = f"{overview}\n\n{translated_overview}"
|
||||
|
||||
if tagline:
|
||||
translated_tagline = self.simple_translate(tagline)
|
||||
if translated_tagline and translated_tagline != tagline:
|
||||
tagline = f"{tagline}\n{translated_tagline}"
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
vod = {
|
||||
'vod_name': title,
|
||||
'vod_year': (v.get('release_date') or v.get('last_air_date') or '')[:4],
|
||||
'vod_area': v.get('original_language') or '',
|
||||
'vod_remarks': tagline,
|
||||
'vod_content': overview,
|
||||
'vod_play_from': 'Redflix',
|
||||
'vod_play_url': play_str
|
||||
}
|
||||
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.fetch(
|
||||
f'{self.chost}/search/multi',
|
||||
params={'query': key, 'page': pg, 'api_key': self.token, 'language': 'en-US', 'include_adult': 'false'},
|
||||
headers=self.headers
|
||||
).json()
|
||||
return {'list': self.getlist(data.get('results', [])), 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
media_type, tmdb_id, season, episode = self._parse_play_id(id)
|
||||
|
||||
s = season or '1'
|
||||
e = episode or '1'
|
||||
|
||||
subs = []
|
||||
|
||||
def _map_lang(label: str) -> str:
|
||||
name = (label or '').lower()
|
||||
table = {
|
||||
'english': 'en', 'arabic': 'ar', 'chinese': 'zh', 'zh': 'zh', '简体': 'zh-CN', '繁體': 'zh-TW',
|
||||
'croatian': 'hr', 'czech': 'cs', 'danish': 'da', 'dutch': 'nl', 'finnish': 'fi', 'french': 'fr',
|
||||
'german': 'de', 'greek': 'el', 'hungarian': 'hu', 'indonesian': 'id', 'italian': 'it',
|
||||
'japanese': 'ja', 'korean': 'ko', 'norwegian': 'no', 'persian': 'fa', 'polish': 'pl',
|
||||
'portuguese (br)': 'pt-BR', 'portuguese': 'pt', 'romanian': 'ro', 'russian': 'ru',
|
||||
'serbian': 'sr', 'spanish': 'es', 'swedish': 'sv', 'turkish': 'tr', 'thai': 'th', 'vietnamese': 'vi'
|
||||
}
|
||||
if name in table:
|
||||
return table[name]
|
||||
for k, v in table.items():
|
||||
if name.startswith(k) or k in name:
|
||||
return v
|
||||
return ''
|
||||
|
||||
try:
|
||||
if media_type == 'tv':
|
||||
sub_api = f"https://s.vdrk.site/subfetch.php?id={tmdb_id}&s={s}&e={e}"
|
||||
else:
|
||||
sub_api = f"https://s.vdrk.site/subfetch.php?id={tmdb_id}"
|
||||
hdr = self.jxh().copy()
|
||||
hdr.update({'referer': 'https://vidrock.net/'})
|
||||
resp = self.fetch(sub_api, headers=hdr, timeout=10)
|
||||
if resp is not None and resp.status_code == 200:
|
||||
try:
|
||||
items = resp.json()
|
||||
except Exception:
|
||||
items = json.loads(resp.text or '[]')
|
||||
if (not items) and media_type == 'tv':
|
||||
try:
|
||||
resp2 = self.fetch(f"https://s.vdrk.site/subfetch.php?id={tmdb_id}", headers=hdr, timeout=10)
|
||||
if resp2 is not None and resp2.status_code == 200:
|
||||
try:
|
||||
items = resp2.json()
|
||||
except Exception:
|
||||
items = json.loads(resp2.text or '[]')
|
||||
except Exception:
|
||||
pass
|
||||
for it in items or []:
|
||||
u = it.get('file') or it.get('url') or it.get('src')
|
||||
name = it.get('label') or it.get('name') or 'Subtitle'
|
||||
if not u:
|
||||
continue
|
||||
low = u.lower()
|
||||
fmt = 'application/x-subrip' if ('srt' in low) else 'text/vtt'
|
||||
subs.append({'url': u, 'name': name, 'lang': _map_lang(name), 'format': fmt})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for sid in self.server_order:
|
||||
domain = self.servers.get(sid)
|
||||
if not domain:
|
||||
continue
|
||||
if media_type == 'movie':
|
||||
embed = f"{domain}/movie/{tmdb_id}"
|
||||
else:
|
||||
if sid == 'vidfast':
|
||||
embed = f"{domain}/tv/{tmdb_id}/{s}/{e}?autoNext=true&nextButton=false&title=true&poster=true&autoPlay=true"
|
||||
elif sid == 'vidrock':
|
||||
embed = f"{domain}/tv/{tmdb_id}/{s}/{e}?autoplay=true&autonext=true"
|
||||
elif sid == 'vidlink':
|
||||
params = "primaryColor=63b8bc&secondaryColor=a2a2a2&iconColor=eefdec&icons=default&player=default&title=true&poster=true&autoplay=true&nextbutton=true"
|
||||
embed = f"{domain}/tv/{tmdb_id}/{s}/{e}?{params}"
|
||||
elif sid == 'videasy':
|
||||
embed = f"{domain}/tv/{tmdb_id}/{s}/{e}?nextEpisode=true&autoplayNextEpisode=true&episodeSelector=true&color=8B5CF6"
|
||||
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.jxh(), 'subs': subs}
|
||||
fallback = f"{self.site}/{media_type}/{tmdb_id}/watch"
|
||||
return {'parse': 1, 'url': fallback, 'header': self.jxh(), 'subs': subs}
|
||||
except Exception:
|
||||
return {'parse': 1, 'url': f"{self.site}{id if id.startswith('/') else '/' + id}", 'header': self.jxh()}
|
||||
|
||||
def getlist(self, data, tid=''):
|
||||
videos = []
|
||||
for i in data or []:
|
||||
media_type = tid or i.get('media_type')
|
||||
if media_type not in ('movie', 'tv'):
|
||||
continue
|
||||
vid = i.get('id')
|
||||
if not vid:
|
||||
continue
|
||||
name = i.get('title') or i.get('name') or ''
|
||||
poster = i.get('backdrop_path') or i.get('poster_path') or ''
|
||||
videos.append({
|
||||
'vod_id': f"/{media_type}/{vid}",
|
||||
'vod_name': name,
|
||||
'vod_pic': f"{self.phost}{poster}",
|
||||
'vod_remarks': ''
|
||||
})
|
||||
return videos
|
||||
|
||||
def jxh(self):
|
||||
header = self.headers.copy()
|
||||
header.update({'referer': f'{self.site}/', 'origin': self.site})
|
||||
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') if len(parts) > 1 else '1', (parts[2] or '1') if len(parts) > 2 else '1'
|
||||
else:
|
||||
raise ValueError('Unrecognized play id')
|
||||
media_type, tmdb_id, season, episode = m.groups()
|
||||
return media_type, tmdb_id, season, episode
|
||||
|
||||
def gettoken(self):
|
||||
hosts = [self.site]
|
||||
paths = ['/', '/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:
|
||||
try:
|
||||
hdr = self.headers.copy()
|
||||
hdr.update({'origin': host, 'referer': f'{host}/'})
|
||||
html = self.fetch(f'{host}{path}', headers=hdr, timeout=10).text
|
||||
mod = pq(html)('script[type="module"]').attr('src') or ''
|
||||
if not mod:
|
||||
continue
|
||||
murl = mod if mod.startswith('http') else f'{host}{mod}'
|
||||
mjs = self.fetch(murl, headers=hdr, timeout=10).text
|
||||
m = key_pattern.search(mjs)
|
||||
if m:
|
||||
return 'https://api.themoviedb.org/3', m.group(1)
|
||||
mw = re.search(r'player-watch-([\w-]+)\.js', mjs)
|
||||
if mw:
|
||||
pw = f"{host}/assets/player-watch-{mw.group(1)}.js"
|
||||
pjs = self.fetch(pw, headers=hdr, timeout=10).text
|
||||
m2 = key_pattern.search(pjs)
|
||||
if m2:
|
||||
return 'https://api.themoviedb.org/3', m2.group(1)
|
||||
except Exception:
|
||||
continue
|
||||
return 'https://api.themoviedb.org/3', '524c16f6e2a0a13c49ff7b99d27b5efb'
|
||||
|
||||
def simple_translate(self, text, target_lang='zh'):
|
||||
"""
|
||||
"""
|
||||
if not text or len(text.strip()) == 0:
|
||||
return text
|
||||
|
||||
text = text.strip()
|
||||
if len(text) > 1000:
|
||||
text = text[:1000] + "..."
|
||||
|
||||
for url in self.translate_urls:
|
||||
try:
|
||||
if 'mymemory.translated.net' in url:
|
||||
params = {
|
||||
'q': text,
|
||||
'langpair': f'en|{target_lang}',
|
||||
'de': '[email protected]'
|
||||
}
|
||||
response = self.fetch(f"{url}?{self.urlencode(params)}", timeout=8)
|
||||
if response and response.status_code == 200:
|
||||
data = response.json()
|
||||
translated = data.get('responseData', {}).get('translatedText')
|
||||
if translated and translated != text:
|
||||
return translated
|
||||
|
||||
else:
|
||||
payload = {
|
||||
'q': text,
|
||||
'source': 'en',
|
||||
'target': target_lang,
|
||||
'format': 'text'
|
||||
}
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
response = self.fetch(
|
||||
url,
|
||||
method='POST',
|
||||
data=json.dumps(payload),
|
||||
headers=headers,
|
||||
timeout=8
|
||||
)
|
||||
if response and response.status_code == 200:
|
||||
data = response.json()
|
||||
translated = data.get('translatedText')
|
||||
if translated and translated != text:
|
||||
return translated
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return text
|
||||
|
||||
def urlencode(self, params):
|
||||
|
||||
return '&'.join([f"{k}={self.quote(str(v))}" for k, v in params.items()])
|
||||
|
||||
def quote(self, text):
|
||||
|
||||
return text.replace(' ', '%20').replace('&', '%26')
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import sys
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='http://www.toule.top'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
||||
'Referer':f'{host}/',
|
||||
'Origin':host
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=self.getpq()
|
||||
result = {}
|
||||
classes = []
|
||||
for k in data('.swiper-wrapper .swiper-slide').items():
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': k.text()
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list'] = self.getlist(data('.container.items ul li'))
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data=self.getpq(f"/index.php/vod/show/class/{tid}/id/1/page/{pg}.html")
|
||||
result = {}
|
||||
result['list'] = self.getlist(data('.container.items ul li'))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data=self.getpq(ids[0])
|
||||
v=data('.container.detail-content')
|
||||
vod = {
|
||||
'vod_remarks': v('.items-tags a').text(),
|
||||
'vod_content': v('.text-content .detail').text(),
|
||||
'vod_play_from': '嗷呜爱看短剧',
|
||||
'vod_play_url': '#'.join([f"{i.text()}${i('a').attr('href')}" for i in data('.swiper-wrapper .swiper-slide').items()])
|
||||
}
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=self.getpq(f"/index.php/vod/search/page/{pg}/wd/{key}.html")
|
||||
return {'list':self.getlist(data('.container.items ul li')),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
data=self.getpq(id)
|
||||
try:
|
||||
jstr=data('.player-content script').eq(0).text()
|
||||
jt=json.loads(jstr.split('=',1)[-1])
|
||||
p,url=0,jt['url']
|
||||
except Exception as e:
|
||||
print(f"获取播放地址失败: {e}")
|
||||
p,url=1,f'{self.host}{id}'
|
||||
return {'parse': p, 'url': url, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def getpq(self, path=''):
|
||||
data=self.fetch(f"{self.host}{path}",headers=self.headers).text
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
|
||||
def getlist(self,data):
|
||||
videos = []
|
||||
for i in data.items():
|
||||
videos.append({
|
||||
'vod_id': i('.image-line').attr('href'),
|
||||
'vod_name': i('img').attr('alt'),
|
||||
'vod_pic': i('img').attr('src'),
|
||||
'vod_remarks': i('.remarks.light').text()
|
||||
})
|
||||
return videos
|
||||
@@ -0,0 +1,315 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from base64 import b64decode, b64encode
|
||||
import concurrent.futures
|
||||
import requests
|
||||
from Crypto.Hash import MD5
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host=self.gethost()
|
||||
self.headers.update({
|
||||
'referer': f'{self.host}/',
|
||||
'origin': self.host,
|
||||
})
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.headers)
|
||||
self.session.get(self.host)
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'sec-fetch-mode': 'navigate',
|
||||
'sec-fetch-user': '?1',
|
||||
'sec-fetch-dest': 'document',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
}
|
||||
|
||||
config={
|
||||
"1":[{"key":"class","name":"剧情","value":[{"n":"全部","v":""},{"n":"喜剧","v":"喜剧"},{"n":"爱情","v":"爱情"},{"n":"恐怖","v":"恐怖"},{"n":"动作","v":"动作"},{"n":"科幻","v":"科幻"},{"n":"剧情","v":"剧情"},{"n":"战争","v":"战争"},{"n":"警匪","v":"警匪"},{"n":"犯罪","v":"犯罪"},{"n":"动画","v":"动画"},{"n":"奇幻","v":"奇幻"},{"n":"武侠","v":"武侠"},{"n":"冒险","v":"冒险"},{"n":"枪战","v":"枪战"},{"n":"悬疑","v":"悬疑"},{"n":"惊悚","v":"惊悚"},{"n":"经典","v":"经典"},{"n":"青春","v":"青春"},{"n":"伦理","v":"伦理"},{"n":"文艺","v":"文艺"},{"n":"微电影","v":"微电影"},{"n":"古装","v":"古装"},{"n":"历史","v":"历史"},{"n":"运动","v":"运动"},{"n":"农村","v":"农村"},{"n":"儿童","v":"儿童"},{"n":"网络电影","v":"网络电影"}]},{"key":"area","name":"地区","value":[{"n":"全部","v":""},{"n":"大陆","v":"大陆"},{"n":"香港","v":"香港"},{"n":"台湾","v":"台湾"},{"n":"美国","v":"美国"},{"n":"法国","v":"法国"},{"n":"英国","v":"英国"},{"n":"日本","v":"日本"},{"n":"韩国","v":"韩国"},{"n":"德国","v":"德国"},{"n":"泰国","v":"泰国"},{"n":"印度","v":"印度"},{"n":"意大利","v":"意大利"},{"n":"西班牙","v":"西班牙"},{"n":"加拿大","v":"加拿大"},{"n":"其他","v":"其他"}]},{"key":"year","name":"年份","value":[{"n":"全部","v":""},{"n":"2025","v":"2025"},{"n":"2024","v":"2024"},{"n":"2023","v":"2023"},{"n":"2022","v":"2022"},{"n":"2021","v":"2021"},{"n":"2020","v":"2020"},{"n":"2019","v":"2019"},{"n":"2018","v":"2018"},{"n":"2017","v":"2017"},{"n":"2016","v":"2016"},{"n":"2015","v":"2015"},{"n":"2014","v":"2014"},{"n":"2013","v":"2013"},{"n":"2012","v":"2012"},{"n":"2011","v":"2011"},{"n":"2010","v":"2010"},{"n":"2009","v":"2009"},{"n":"2008","v":"2008"},{"n":"2007","v":"2007"},{"n":"2006","v":"2006"},{"n":"2005","v":"2005"},{"n":"2004","v":"2004"},{"n":"2003","v":"2003"},{"n":"2002","v":"2002"},{"n":"2001","v":"2001"},{"n":"2000","v":"2000"}]},{"key":"by","name":"排序","value":[{"n":"时间","v":"time"},{"n":"人气","v":"hits"},{"n":"评分","v":"score"}]}],
|
||||
"2":[{"key":"class","name":"剧情","value":[{"n":"全部","v":""},{"n":"古装","v":"古装"},{"n":"战争","v":"战争"},{"n":"青春偶像","v":"青春偶像"},{"n":"喜剧","v":"喜剧"},{"n":"家庭","v":"家庭"},{"n":"犯罪","v":"犯罪"},{"n":"动作","v":"动作"},{"n":"奇幻","v":"奇幻"},{"n":"剧情","v":"剧情"},{"n":"历史","v":"历史"},{"n":"经典","v":"经典"},{"n":"乡村","v":"乡村"},{"n":"情景","v":"情景"},{"n":"商战","v":"商战"},{"n":"网剧","v":"网剧"},{"n":"其他","v":"其他"}]},{"key":"area","name":"地区","value":[{"n":"全部","v":""},{"n":"内地","v":"内地"},{"n":"香港","v":"香港"},{"n":"台湾","v":"台湾"},{"n":"美国","v":"美国"},{"n":"法国","v":"法国"},{"n":"英国","v":"英国"},{"n":"日本","v":"日本"},{"n":"韩国","v":"韩国"},{"n":"德国","v":"德国"},{"n":"泰国","v":"泰国"},{"n":"印度","v":"印度"},{"n":"意大利","v":"意大利"},{"n":"西班牙","v":"西班牙"},{"n":"加拿大","v":"加拿大"},{"n":"其他","v":"其他"}]},{"key":"year","name":"年份","value":[{"n":"全部","v":""},{"n":"2025","v":"2025"},{"n":"2024","v":"2024"},{"n":"2023","v":"2023"},{"n":"2022","v":"2022"},{"n":"2021","v":"2021"},{"n":"2020","v":"2020"},{"n":"2019","v":"2019"},{"n":"2018","v":"2018"},{"n":"2017","v":"2017"},{"n":"2016","v":"2016"},{"n":"2015","v":"2015"},{"n":"2014","v":"2014"},{"n":"2013","v":"2013"},{"n":"2012","v":"2012"},{"n":"2011","v":"2011"},{"n":"2010","v":"2010"},{"n":"2009","v":"2009"},{"n":"2008","v":"2008"},{"n":"2007","v":"2007"},{"n":"2006","v":"2006"},{"n":"2005","v":"2005"},{"n":"2004","v":"2004"},{"n":"2003","v":"2003"},{"n":"2002","v":"2002"},{"n":"2001","v":"2001"},{"n":"2000","v":"2000"}]},{"key":"by","name":"排序","value":[{"n":"时间","v":"time"},{"n":"人气","v":"hits"},{"n":"评分","v":"score"}]}],
|
||||
"3":[{"key":"class","name":"剧情","value":[{"n":"全部","v":""},{"n":"选秀","v":"选秀"},{"n":"情感","v":"情感"},{"n":"访谈","v":"访谈"},{"n":"播报","v":"播报"},{"n":"旅游","v":"旅游"},{"n":"音乐","v":"音乐"},{"n":"美食","v":"美食"},{"n":"纪实","v":"纪实"},{"n":"曲艺","v":"曲艺"},{"n":"生活","v":"生活"},{"n":"游戏互动","v":"游戏互动"},{"n":"财经","v":"财经"},{"n":"求职","v":"求职"}]},{"key":"area","name":"地区","value":[{"n":"全部","v":""},{"n":"内地","v":"内地"},{"n":"港台","v":"港台"},{"n":"欧美","v":"欧美"},{"n":"日韩","v":"日韩"},{"n":"其他","v":"其他"}]},{"key":"year","name":"年份","value":[{"n":"全部","v":""},{"n":"2025","v":"2025"},{"n":"2024","v":"2024"},{"n":"2023","v":"2023"},{"n":"2022","v":"2022"},{"n":"2021","v":"2021"},{"n":"2020","v":"2020"},{"n":"2019","v":"2019"},{"n":"2018","v":"2018"},{"n":"2017","v":"2017"},{"n":"2016","v":"2016"},{"n":"2015","v":"2015"},{"n":"2014","v":"2014"},{"n":"2013","v":"2013"},{"n":"2012","v":"2012"},{"n":"2011","v":"2011"},{"n":"2010","v":"2010"},{"n":"2009","v":"2009"},{"n":"2008","v":"2008"},{"n":"2007","v":"2007"},{"n":"2006","v":"2006"},{"n":"2005","v":"2005"},{"n":"2004","v":"2004"},{"n":"2003","v":"2003"},{"n":"2002","v":"2002"},{"n":"2001","v":"2001"},{"n":"2000","v":"2000"}]},{"key":"by","name":"排序","value":[{"n":"时间","v":"time"},{"n":"人气","v":"hits"},{"n":"评分","v":"score"}]}],
|
||||
"4":[{"key":"class","name":"剧情","value":[{"n":"全部","v":""},{"n":"情感","v":"情感"},{"n":"科幻","v":"科幻"},{"n":"热血","v":"热血"},{"n":"推理","v":"推理"},{"n":"搞笑","v":"搞笑"},{"n":"冒险","v":"冒险"},{"n":"萝莉","v":"萝莉"},{"n":"校园","v":"校园"},{"n":"动作","v":"动作"},{"n":"机战","v":"机战"},{"n":"运动","v":"运动"},{"n":"战争","v":"战争"},{"n":"少年","v":"少年"},{"n":"少女","v":"少女"},{"n":"社会","v":"社会"},{"n":"原创","v":"原创"},{"n":"亲子","v":"亲子"},{"n":"益智","v":"益智"},{"n":"励志","v":"励志"},{"n":"其他","v":"其他"}]},{"key":"area","name":"地区","value":[{"n":"全部","v":""},{"n":"国产","v":"国产"},{"n":"欧美","v":"欧美"},{"n":"日本","v":"日本"},{"n":"其他","v":"其他"}]},{"key":"year","name":"年份","value":[{"n":"全部","v":""},{"n":"2025","v":"2025"},{"n":"2024","v":"2024"},{"n":"2023","v":"2023"},{"n":"2022","v":"2022"},{"n":"2021","v":"2021"},{"n":"2020","v":"2020"},{"n":"2019","v":"2019"},{"n":"2018","v":"2018"},{"n":"2017","v":"2017"},{"n":"2016","v":"2016"},{"n":"2015","v":"2015"},{"n":"2014","v":"2014"},{"n":"2013","v":"2013"},{"n":"2012","v":"2012"},{"n":"2011","v":"2011"},{"n":"2010","v":"2010"},{"n":"2009","v":"2009"},{"n":"2008","v":"2008"},{"n":"2007","v":"2007"},{"n":"2006","v":"2006"},{"n":"2005","v":"2005"},{"n":"2004","v":"2004"},{"n":"2003","v":"2003"},{"n":"2002","v":"2002"},{"n":"2001","v":"2001"},{"n":"2000","v":"2000"}]},{"key":"by","name":"排序","value":[{"n":"时间","v":"time"},{"n":"人气","v":"hits"},{"n":"评分","v":"score"}]}],
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=self.getpq()
|
||||
result = {}
|
||||
classes = []
|
||||
for k in data('ul.swiper-wrapper').eq(0)('li').items():
|
||||
i=k('a').attr('href')
|
||||
if i and 'type' in i:
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': re.findall(r'\d+', i)[0],
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list'] = self.getlist(data('.tab-content.ewave-pannel_bd li'))
|
||||
result['filters'] = self.config
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
path=f"/vodshow/{tid}-{extend.get('area','')}-{extend.get('by','')}-{extend.get('class','')}-----{pg}---{extend.get('year','')}.html"
|
||||
data=self.getpq(path)
|
||||
result = {}
|
||||
result['list'] = self.getlist(data('ul.ewave-vodlist.clearfix li'))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data=self.getpq(f"/voddetail/{ids[0]}.html")
|
||||
v=data('.ewave-content__detail')
|
||||
c=data('p')
|
||||
vod = {
|
||||
'type_name':c.eq(0)('a').text(),
|
||||
'vod_year': v('.data.hidden-sm').text(),
|
||||
'vod_remarks': v('h1').text(),
|
||||
'vod_actor': c.eq(1)('a').text(),
|
||||
'vod_director': c.eq(2)('a').text(),
|
||||
'vod_content': c.eq(-1).text(),
|
||||
'vod_play_from': '',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
nd=list(data('ul.nav-tabs.swiper-wrapper li').items())
|
||||
pd=list(data('ul.ewave-content__playlist').items())
|
||||
n,p=[],[]
|
||||
for i,x in enumerate(nd):
|
||||
n.append(x.text())
|
||||
p.append('#'.join([f"{j.text()}${j('a').attr('href')}" for j in pd[i]('li').items()]))
|
||||
vod['vod_play_url']='$$$'.join(p)
|
||||
vod['vod_play_from']='$$$'.join(n)
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
if pg=="1":
|
||||
p=f"-------------.html?wd={key}"
|
||||
else:
|
||||
p=f"{key}----------{pg}---.html"
|
||||
data=self.getpq(f"/vodsearch/{p}")
|
||||
return {'list':self.getlist(data('ul.ewave-vodlist__media.clearfix li')),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
data=self.getpq(id)
|
||||
jstr = json.loads(data('.ewave-player__video script').eq(0).text().split('=', 1)[-1])
|
||||
jxpath='/bbplayer/api.php'
|
||||
data=self.session.post(f"{self.host}{jxpath}",data={'vid':jstr['url']}).json()['data']
|
||||
if re.search(r'\.m3u8|\.mp4',data['url']):
|
||||
url=data['url']
|
||||
elif data['urlmode'] == 1:
|
||||
url=self.decode1(data['url'])
|
||||
elif data['urlmode'] == 2:
|
||||
url=self.decode2(data['url'])
|
||||
elif re.search(r'\.m3u8|\.mp4',jstr['url']):
|
||||
url=jstr['url']
|
||||
else:
|
||||
url=None
|
||||
if not url:raise Exception('未找到播放地址')
|
||||
p,c=0,''
|
||||
except Exception as e:
|
||||
self.log(f"解析失败: {e}")
|
||||
p,url,c=1,f"{self.host}{id}",'document.querySelector("#playleft iframe").contentWindow.document.querySelector("#start").click()'
|
||||
return {'parse': p, 'url': url, 'header': {'User-Agent':'okhttp/3.12.1'},'click': c}
|
||||
|
||||
def localProxy(self, param):
|
||||
wdict=json.loads(self.d64(param['wdict']))
|
||||
url=f"{wdict['jx']}{wdict['id']}"
|
||||
data=pq(self.fetch(url,headers=self.headers).text)
|
||||
html=data('script').eq(-1).text()
|
||||
url = re.search(r'src="(.*?)"', html).group(1)
|
||||
return [302,'text/html',None,{'Location':url}]
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def gethost(self):
|
||||
data=pq(self.fetch('https://www.jubaba.vip',headers=self.headers).text)
|
||||
hlist=list(data('.content-top ul li').items())[:2]
|
||||
hsots=[j('a').attr('href') for i in hlist for j in i('a').items()]
|
||||
return self.host_late(hsots)
|
||||
|
||||
def host_late(self, urls):
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future_to_url = {
|
||||
executor.submit(self.test_host, url): url
|
||||
for url in urls
|
||||
}
|
||||
results = {}
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
results[url] = future.result()
|
||||
except Exception as e:
|
||||
results[url] = float('inf')
|
||||
min_url = min(results.items(), key=lambda x: x[1])[0] if results else None
|
||||
if all(delay == float('inf') for delay in results.values()) or not min_url:
|
||||
return urls[0]
|
||||
return min_url
|
||||
|
||||
def test_host(self, url):
|
||||
try:
|
||||
start_time = time.monotonic()
|
||||
response = requests.head(
|
||||
url,
|
||||
timeout=1.0,
|
||||
allow_redirects=False,
|
||||
headers=self.headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return (time.monotonic() - start_time) * 1000
|
||||
except Exception as e:
|
||||
print(f"测试{url}失败: {str(e)}")
|
||||
return float('inf')
|
||||
|
||||
def getpq(self, path='',min=0,max=3):
|
||||
data = self.session.get(f"{self.host}{path}")
|
||||
data=data.text
|
||||
try:
|
||||
if '人机验证' in data:
|
||||
print(f"第{min}次尝试人机验证")
|
||||
jstr=pq(data)('script').eq(-1).html()
|
||||
token,tpath,stt=self.extract(jstr)
|
||||
body={'value':self.encrypt(self.host,stt),'token':self.encrypt(token,stt)}
|
||||
cd=self.session.post(f"{self.host}{tpath}",data=body)
|
||||
if min>max:raise Exception('人机验证失败')
|
||||
return self.getpq(path,min+1,max)
|
||||
return pq(data)
|
||||
except:
|
||||
return pq(data.encode('utf-8'))
|
||||
|
||||
def encrypt(self, input_str,staticchars):
|
||||
encodechars = ""
|
||||
for char in input_str:
|
||||
num0 = staticchars.find(char)
|
||||
if num0 == -1:
|
||||
code = char
|
||||
else:
|
||||
code = staticchars[(num0 + 3) % 62]
|
||||
num1 = random.randint(0, 61)
|
||||
num2 = random.randint(0, 61)
|
||||
encodechars += staticchars[num1] + code + staticchars[num2]
|
||||
return self.e64(encodechars)
|
||||
|
||||
def extract(self, js_code):
|
||||
token_match = re.search(r'var token = encrypt\("([^"]+)"\);', js_code)
|
||||
token_value = token_match.group(1) if token_match else None
|
||||
url_match = re.search(r'var url = \'([^\']+)\';', js_code)
|
||||
url_value = url_match.group(1) if url_match else None
|
||||
staticchars_match = re.search(r'var\s+staticchars\s*=\s*["\']([^"\']+)["\'];', js_code)
|
||||
staticchars = staticchars_match.group(1) if staticchars_match else None
|
||||
return token_value, url_value,staticchars
|
||||
|
||||
def decode1(self, val):
|
||||
url = self._custom_str_decode(val)
|
||||
parts = url.split("/")
|
||||
result = "/".join(parts[2:])
|
||||
key1 = json.loads(self.d64(parts[1]))
|
||||
key2 = json.loads(self.d64(parts[0]))
|
||||
decoded = self.d64(result)
|
||||
return self._de_string(key1, key2, decoded)
|
||||
|
||||
def _custom_str_decode(self, val):
|
||||
decoded = self.d64(val)
|
||||
key = self.md5("test")
|
||||
result = ""
|
||||
for i in range(len(decoded)):
|
||||
result += chr(ord(decoded[i]) ^ ord(key[i % len(key)]))
|
||||
return self.d64(result)
|
||||
|
||||
def _de_string(self, key_array, value_array, input_str):
|
||||
result = ""
|
||||
for char in input_str:
|
||||
if re.match(r'^[a-zA-Z]$', char):
|
||||
if char in key_array:
|
||||
index = key_array.index(char)
|
||||
result += value_array[index]
|
||||
continue
|
||||
result += char
|
||||
return result
|
||||
|
||||
def decode2(self, url):
|
||||
key = "PXhw7UT1B0a9kQDKZsjIASmOezxYG4CHo5Jyfg2b8FLpEvRr3WtVnlqMidu6cN"
|
||||
url=self.d64(url)
|
||||
result = ""
|
||||
i = 1
|
||||
while i < len(url):
|
||||
try:
|
||||
index = key.find(url[i])
|
||||
if index == -1:
|
||||
char = url[i]
|
||||
else:
|
||||
char = key[(index + 59) % 62]
|
||||
result += char
|
||||
except IndexError:
|
||||
break
|
||||
i += 3
|
||||
return result
|
||||
|
||||
def getlist(self, data):
|
||||
videos = []
|
||||
for k in data.items():
|
||||
j = k('.ewave-vodlist__thumb')
|
||||
h=k('.text-overflow a')
|
||||
if not h.attr('href'):h=j
|
||||
videos.append({
|
||||
'vod_id': re.findall(r'\d+', h.attr('href'))[0],
|
||||
'vod_name': j.attr('title'),
|
||||
'vod_pic': j.attr('data-original'),
|
||||
'vod_remarks': k('.pic-text').text(),
|
||||
})
|
||||
return videos
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self,encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def md5(self, text):
|
||||
h = MD5.new()
|
||||
h.update(text.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
@@ -0,0 +1,796 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
import json
|
||||
import time
|
||||
import urllib.parse
|
||||
import re
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def getName(self):
|
||||
return "厂长资源"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://www.cz233.com"
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': self.host
|
||||
}
|
||||
self.log(f"厂长资源爬虫初始化完成,主站: {self.host}")
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""获取首页内容和分类 - 修复版"""
|
||||
result = {}
|
||||
|
||||
# 定义分类 - 基于实际网站结构(移除不存在的电视剧分类)
|
||||
classes = [
|
||||
{'type_id': 'movie_bt', 'type_name': '全部影片'},
|
||||
{'type_id': 'dyy', 'type_name': '电影'},
|
||||
{'type_id': 'guochanju', 'type_name': '国产剧'},
|
||||
{'type_id': 'mj', 'type_name': '美剧'},
|
||||
{'type_id': 'hj', 'type_name': '韩剧'},
|
||||
{'type_id': 'rj', 'type_name': '日剧'},
|
||||
{'type_id': 'hwj', 'type_name': '海外剧'},
|
||||
{'type_id': 'fjj', 'type_name': '番剧'},
|
||||
{'type_id': 'zuixindianying', 'type_name': '最新电影'},
|
||||
{'type_id': 'dbtop250', 'type_name': '豆瓣Top250'},
|
||||
{'type_id': 'dongmanjuchangban', 'type_name': '动漫剧场版'}
|
||||
]
|
||||
result['class'] = classes
|
||||
|
||||
# 添加筛选配置
|
||||
result['filters'] = self._get_filters()
|
||||
|
||||
# 获取首页推荐内容
|
||||
try:
|
||||
rsp = self.fetch(self.host, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
videos = self._get_videos(doc, limit=50)
|
||||
result['list'] = videos
|
||||
except Exception as e:
|
||||
self.log(f"首页获取出错: {str(e)}")
|
||||
result['list'] = []
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""分类定义 - 兼容性方法"""
|
||||
return {
|
||||
'class': [
|
||||
{'type_id': 'movie_bt', 'type_name': '全部影片'},
|
||||
{'type_id': 'dyy', 'type_name': '电影'},
|
||||
{'type_id': 'guochanju', 'type_name': '国产剧'},
|
||||
{'type_id': 'mj', 'type_name': '美剧'},
|
||||
{'type_id': 'hj', 'type_name': '韩剧'},
|
||||
{'type_id': 'rj', 'type_name': '日剧'},
|
||||
{'type_id': 'hwj', 'type_name': '海外剧'},
|
||||
{'type_id': 'fjj', 'type_name': '番剧'},
|
||||
{'type_id': 'zuixindianying', 'type_name': '最新电影'},
|
||||
{'type_id': 'dbtop250', 'type_name': '豆瓣Top250'},
|
||||
{'type_id': 'dongmanjuchangban', 'type_name': '动漫剧场版'}
|
||||
],
|
||||
'filters': self._get_filters()
|
||||
}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""分类内容 - 支持筛选功能"""
|
||||
try:
|
||||
# 处理筛选参数 - 将filter参数合并到extend中
|
||||
if filter and isinstance(filter, dict):
|
||||
if not extend:
|
||||
extend = {}
|
||||
extend.update(filter)
|
||||
|
||||
self.log(f"分类请求: tid={tid}, pg={pg}, extend={extend}")
|
||||
|
||||
url = self._build_url(tid, pg, extend)
|
||||
if not url:
|
||||
return {'list': []}
|
||||
|
||||
self.log(f"访问分类URL: {url}")
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
videos = self._get_videos(doc, limit=20)
|
||||
|
||||
return {
|
||||
'list': videos,
|
||||
'page': int(pg),
|
||||
'pagecount': 999,
|
||||
'limit': 20,
|
||||
'total': 19980
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"分类内容获取出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
"""搜索功能 - 智能过滤版"""
|
||||
try:
|
||||
search_url = f"{self.host}/xsss1O1?q={urllib.parse.quote(key)}"
|
||||
self.log(f"搜索URL: {search_url}")
|
||||
rsp = self.fetch(search_url, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
|
||||
videos = []
|
||||
seen_ids = set()
|
||||
elements = doc.xpath('//li[contains(@class,"") and .//a[contains(@href,"/movie/")]]')
|
||||
self.log(f"找到 {len(elements)} 个搜索结果元素")
|
||||
|
||||
for elem in elements:
|
||||
video = self._extract_video(elem)
|
||||
if video and video['vod_id'] not in seen_ids:
|
||||
if self._is_relevant_search_result(video['vod_name'], key):
|
||||
videos.append(video)
|
||||
seen_ids.add(video['vod_id'])
|
||||
self.log(f"✅ 相关视频: {video['vod_name']} (ID: {video['vod_id']})")
|
||||
else:
|
||||
self.log(f"❌ 过滤无关: {video['vod_name']} (搜索: {key})")
|
||||
|
||||
self.log(f"最终搜索结果: {len(videos)} 个视频")
|
||||
return {'list': videos}
|
||||
except Exception as e:
|
||||
self.log(f"搜索出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""详情页面"""
|
||||
try:
|
||||
vid = ids[0]
|
||||
detail_url = f"{self.host}/movie/{vid}.html"
|
||||
rsp = self.fetch(detail_url, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
|
||||
video_info = self._get_detail(doc, vid)
|
||||
return {'list': [video_info]} if video_info else {'list': []}
|
||||
except Exception as e:
|
||||
self.log(f"详情获取出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""播放链接 - 包含解密功能"""
|
||||
try:
|
||||
self.log(f"获取播放链接: flag={flag}, id={id}")
|
||||
start_time = time.time()
|
||||
|
||||
play_url = f"{self.host}/v_play/{id}.html"
|
||||
play_headers = self.headers.copy()
|
||||
play_headers.update({
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
})
|
||||
|
||||
rsp = self.fetch(play_url, headers=play_headers)
|
||||
|
||||
# 智能解码页面内容
|
||||
try:
|
||||
html = rsp.text if rsp.encoding else rsp.content.decode('utf-8', errors='ignore')
|
||||
except:
|
||||
try:
|
||||
html = rsp.content.decode('gbk', errors='ignore')
|
||||
except:
|
||||
html = rsp.content.decode('latin-1', errors='ignore')
|
||||
|
||||
# 提取真实播放链接
|
||||
real_url = self._extract_real_video_url(html, play_url)
|
||||
|
||||
end_time = time.time()
|
||||
self.log(f"播放链接获取完成,耗时{end_time-start_time:.2f}秒")
|
||||
|
||||
# 根据获取结果返回不同的解析方式
|
||||
if real_url and any(ext in real_url.lower() for ext in ['.m3u8', '.mp4', '.flv', '.avi']):
|
||||
return {'parse': 0, 'playUrl': '', 'url': real_url}
|
||||
elif real_url and real_url.startswith('http'):
|
||||
return {'parse': 1, 'playUrl': '', 'url': real_url}
|
||||
else:
|
||||
return {'parse': 1, 'playUrl': '', 'url': play_url}
|
||||
except Exception as e:
|
||||
self.log(f"播放链接获取出错: {str(e)}")
|
||||
return {'parse': 1, 'playUrl': '', 'url': f"{self.host}/v_play/{id}.html"}
|
||||
|
||||
# ========== 核心辅助方法 ==========
|
||||
|
||||
def _is_relevant_search_result(self, title, search_key):
|
||||
"""智能搜索相关性检查"""
|
||||
if not title or not search_key:
|
||||
return False
|
||||
|
||||
title_lower = title.lower()
|
||||
search_key_lower = search_key.lower()
|
||||
|
||||
# 直接包含搜索关键词
|
||||
if search_key_lower in title_lower:
|
||||
return True
|
||||
|
||||
# 特定作品严格匹配
|
||||
specific_works = {
|
||||
'碧蓝之海': ['碧蓝之海', '碧蓝', 'grand blue'],
|
||||
'海贼王': ['海贼王', '海贼', '航海王', 'one piece'],
|
||||
'火影忍者': ['火影忍者', '火影', '忍者', 'naruto'],
|
||||
'死神': ['死神', 'bleach'],
|
||||
'进击的巨人': ['进击的巨人', '进击', '巨人'],
|
||||
'鬼灭之刃': ['鬼灭之刃', '鬼灭', '炭治郎'],
|
||||
'龙珠': ['龙珠', '悟空', 'dragon ball'],
|
||||
'蜘蛛侠': ['蜘蛛侠', 'spider-man', 'spiderman'],
|
||||
'钢铁侠': ['钢铁侠', 'iron man'],
|
||||
'复仇者联盟': ['复仇者联盟', '复仇者', 'avengers'],
|
||||
'变形金刚': ['变形金刚', 'transformers'],
|
||||
'哈利波特': ['哈利波特', 'harry potter'],
|
||||
'指环王': ['指环王', '魔戒', 'lord of the rings'],
|
||||
'星球大战': ['星球大战', '星战', 'star wars']
|
||||
}
|
||||
|
||||
for work, keywords in specific_works.items():
|
||||
if work in search_key_lower:
|
||||
return any(keyword in title_lower for keyword in keywords)
|
||||
|
||||
# 过滤明显不相关的内容
|
||||
irrelevant_patterns = [
|
||||
(r'海', ['盒中之海', '寂静之海', '永生之海', '石之海']),
|
||||
(r'王', ['霸王', '君王', '王者', '国王', '女王']),
|
||||
(r'龙', ['追龙', '卧虎藏龙', '龙门', '龙虎'])
|
||||
]
|
||||
|
||||
for pattern, irrelevant_list in irrelevant_patterns:
|
||||
if pattern in search_key_lower:
|
||||
for irrelevant in irrelevant_list:
|
||||
if irrelevant in title_lower and search_key_lower not in title_lower:
|
||||
return False
|
||||
|
||||
# 字符匹配(高阈值)
|
||||
search_chars = set(search_key_lower.replace(' ', ''))
|
||||
title_chars = set(title_lower.replace(' ', ''))
|
||||
|
||||
if len(search_chars) > 0:
|
||||
match_ratio = len(search_chars & title_chars) / len(search_chars)
|
||||
if match_ratio >= 0.8:
|
||||
return True
|
||||
|
||||
# 短搜索词要求严格匹配
|
||||
if len(search_key_lower) <= 2:
|
||||
return search_key_lower in title_lower
|
||||
|
||||
return False
|
||||
|
||||
def _get_filters(self):
|
||||
"""筛选配置 - TVBox兼容版"""
|
||||
# 基础筛选配置
|
||||
base_filters = [
|
||||
{
|
||||
'key': 'area',
|
||||
'name': '地区',
|
||||
'value': [
|
||||
{'n': '全部', 'v': ''},
|
||||
{'n': '中国大陆', 'v': '中国大陆'},
|
||||
{'n': '中国香港', 'v': '中国香港'},
|
||||
{'n': '中国台湾', 'v': '中国台湾'},
|
||||
{'n': '美国', 'v': '美国'},
|
||||
{'n': '韩国', 'v': '韩国'},
|
||||
{'n': '日本', 'v': '日本'},
|
||||
{'n': '英国', 'v': '英国'},
|
||||
{'n': '法国', 'v': '法国'},
|
||||
{'n': '德国', 'v': '德国'},
|
||||
{'n': '其他', 'v': '其他'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'key': 'year',
|
||||
'name': '年份',
|
||||
'value': [
|
||||
{'n': '全部', 'v': ''},
|
||||
{'n': '2025', 'v': '2025'},
|
||||
{'n': '2024', 'v': '2024'},
|
||||
{'n': '2023', 'v': '2023'},
|
||||
{'n': '2022', 'v': '2022'},
|
||||
{'n': '2021', 'v': '2021'},
|
||||
{'n': '2020', 'v': '2020'},
|
||||
{'n': '2019', 'v': '2019'},
|
||||
{'n': '2018', 'v': '2018'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'key': 'type',
|
||||
'name': '类型',
|
||||
'value': [
|
||||
{'n': '全部', 'v': ''},
|
||||
{'n': '动作', 'v': '动作'},
|
||||
{'n': '喜剧', 'v': '喜剧'},
|
||||
{'n': '爱情', 'v': '爱情'},
|
||||
{'n': '科幻', 'v': '科幻'},
|
||||
{'n': '恐怖', 'v': '恐怖'},
|
||||
{'n': '剧情', 'v': '剧情'},
|
||||
{'n': '悬疑', 'v': '悬疑'},
|
||||
{'n': '惊悚', 'v': '惊悚'},
|
||||
{'n': '战争', 'v': '战争'},
|
||||
{'n': '犯罪', 'v': '犯罪'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'key': 'tag',
|
||||
'name': '标签',
|
||||
'value': [
|
||||
{'n': '全部', 'v': ''},
|
||||
{'n': '1080P', 'v': '1080P'},
|
||||
{'n': '4K', 'v': '4K'},
|
||||
{'n': '720P', 'v': '720P'},
|
||||
{'n': 'HD', 'v': 'HD'},
|
||||
{'n': '剧场版', 'v': '剧场版'},
|
||||
{'n': '国产剧', 'v': '国产剧'},
|
||||
{'n': '韩剧', 'v': '韩剧'},
|
||||
{'n': '美剧', 'v': '美剧'},
|
||||
{'n': '日剧', 'v': '日剧'},
|
||||
{'n': '番剧', 'v': '番剧'}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# 为每个分类提供筛选配置
|
||||
filters = {}
|
||||
category_ids = ['movie_bt', 'dyy', 'guochanju', 'mj', 'hj', 'rj', 'hwj', 'fjj', 'zuixindianying', 'dbtop250', 'dongmanjuchangban']
|
||||
|
||||
for category_id in category_ids:
|
||||
if category_id == 'movie_bt':
|
||||
# 全部影片分类提供完整筛选
|
||||
filters[category_id] = base_filters
|
||||
elif category_id in ['dyy', 'zuixindianying', 'dbtop250', 'dongmanjuchangban']:
|
||||
# 电影相关分类提供地区、年份、类型筛选
|
||||
filters[category_id] = [
|
||||
base_filters[0], # 地区
|
||||
base_filters[1], # 年份
|
||||
base_filters[2] # 类型
|
||||
]
|
||||
else:
|
||||
# 剧集分类提供地区、年份筛选
|
||||
filters[category_id] = [
|
||||
base_filters[0], # 地区
|
||||
base_filters[1] # 年份
|
||||
]
|
||||
|
||||
return filters
|
||||
|
||||
def _build_url(self, tid, pg, extend):
|
||||
"""构建URL - 修复版(基于实际网站结构)"""
|
||||
try:
|
||||
self.log(f"构建URL: tid={tid}, pg={pg}, extend={extend}")
|
||||
|
||||
# 基础分类映射
|
||||
category_map = {
|
||||
'movie_bt': '/movie_bt',
|
||||
'zuixindianying': '/zuixindianying',
|
||||
'dbtop250': '/dbtop250',
|
||||
'fanju': '/fanju',
|
||||
'hanjutv': '/hanjutv',
|
||||
'meijutt': '/meijutt',
|
||||
'gcj': '/gcj',
|
||||
'dongmanjuchangban': '/dongmanjuchangban',
|
||||
'riju': '/riju',
|
||||
'haiwaijuqita': '/haiwaijuqita'
|
||||
}
|
||||
|
||||
# 特殊分类映射(基于实际网站结构)
|
||||
special_categories = {
|
||||
'dyy': '/movie_bt_series/dyy', # 电影
|
||||
'guochanju': '/movie_bt_series/guochanju', # 国产剧
|
||||
'mj': '/movie_bt_series/mj', # 美剧
|
||||
'hj': '/movie_bt_series/hj', # 韩剧
|
||||
'rj': '/movie_bt_series/rj', # 日剧
|
||||
'hwj': '/movie_bt_series/hwj', # 海外剧
|
||||
'fjj': '/movie_bt_view_cat/fjj' # 番剧
|
||||
}
|
||||
|
||||
# 处理筛选参数 - 基于实际网站结构
|
||||
if extend and tid == 'movie_bt':
|
||||
# 根据筛选条件选择合适的分类
|
||||
if extend.get('tag'):
|
||||
tag = extend['tag']
|
||||
if tag == '国产剧':
|
||||
url = f"{self.host}/movie_bt_series/guochanju"
|
||||
elif tag == '美剧':
|
||||
url = f"{self.host}/movie_bt_series/mj"
|
||||
elif tag == '韩剧':
|
||||
url = f"{self.host}/movie_bt_series/hj"
|
||||
elif tag == '日剧':
|
||||
url = f"{self.host}/movie_bt_series/rj"
|
||||
elif tag == '番剧':
|
||||
url = f"{self.host}/movie_bt_view_cat/fjj"
|
||||
else:
|
||||
# 其他标签使用基础分类
|
||||
url = f"{self.host}/movie_bt"
|
||||
elif extend.get('type'):
|
||||
# 类型筛选暂时使用基础分类
|
||||
url = f"{self.host}/movie_bt"
|
||||
elif extend.get('area'):
|
||||
# 地区筛选暂时使用基础分类
|
||||
url = f"{self.host}/movie_bt"
|
||||
elif extend.get('year'):
|
||||
# 年份筛选暂时使用基础分类
|
||||
url = f"{self.host}/movie_bt"
|
||||
else:
|
||||
url = f"{self.host}/movie_bt"
|
||||
else:
|
||||
# 检查是否为特殊分类
|
||||
if tid in special_categories:
|
||||
url = f"{self.host}{special_categories[tid]}"
|
||||
else:
|
||||
# 普通分类
|
||||
base_url = category_map.get(tid)
|
||||
if not base_url:
|
||||
self.log(f"未知的分类ID: {tid}")
|
||||
return None
|
||||
url = f"{self.host}{base_url}"
|
||||
|
||||
# 添加分页
|
||||
if pg and pg != '1':
|
||||
url = f"{url}/page/{pg}"
|
||||
|
||||
self.log(f"构建的URL: {url}")
|
||||
return url
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"构建URL出错: {str(e)}")
|
||||
return f"{self.host}/movie_bt"
|
||||
|
||||
def _get_videos(self, doc, limit=None):
|
||||
"""获取视频列表"""
|
||||
try:
|
||||
videos = []
|
||||
seen_ids = set()
|
||||
|
||||
# 查找视频元素
|
||||
selectors = [
|
||||
'//div[contains(@class,"bt_img")]//li',
|
||||
'//ul[contains(@class,"bt_img")]//li'
|
||||
]
|
||||
|
||||
for selector in selectors:
|
||||
elements = doc.xpath(selector)
|
||||
if elements:
|
||||
for elem in elements:
|
||||
video = self._extract_video(elem)
|
||||
if video and video['vod_id'] not in seen_ids:
|
||||
videos.append(video)
|
||||
seen_ids.add(video['vod_id'])
|
||||
break
|
||||
|
||||
return videos[:limit] if limit and videos else videos
|
||||
except Exception as e:
|
||||
self.log(f"获取视频列表出错: {str(e)}")
|
||||
return []
|
||||
|
||||
def _extract_video(self, element):
|
||||
"""提取视频信息"""
|
||||
try:
|
||||
# 提取链接和ID
|
||||
links = element.xpath('.//a[contains(@href,"/movie/")]/@href')
|
||||
if not links:
|
||||
return None
|
||||
|
||||
link = links[0]
|
||||
if link.startswith('/'):
|
||||
link = self.host + link
|
||||
|
||||
vod_id = self.regStr(r'/movie/(\d+)\.html', link)
|
||||
if not vod_id:
|
||||
return None
|
||||
|
||||
# 提取标题
|
||||
title_selectors = ['.//h3/a/text()', './/h3/text()', './/a/@title', './/a/text()']
|
||||
title = ''
|
||||
for selector in title_selectors:
|
||||
titles = element.xpath(selector)
|
||||
for t in titles:
|
||||
if t and t.strip() and len(t.strip()) > 1:
|
||||
title = t.strip()
|
||||
break
|
||||
if title:
|
||||
break
|
||||
|
||||
if not title:
|
||||
return None
|
||||
|
||||
# 提取图片
|
||||
pic_selectors = ['.//img/@data-src', './/img/@src', './/img/@data-original']
|
||||
pic = ''
|
||||
for selector in pic_selectors:
|
||||
pics = element.xpath(selector)
|
||||
for p in pics:
|
||||
if (p and not p.endswith('blank.gif') and
|
||||
not p.startswith('data:image/') and 'base64' not in p):
|
||||
if p.startswith('//'):
|
||||
pic = 'https:' + p
|
||||
elif p.startswith('/'):
|
||||
pic = self.host + p
|
||||
elif p.startswith('http'):
|
||||
pic = p
|
||||
break
|
||||
if pic:
|
||||
break
|
||||
|
||||
# 提取备注
|
||||
remarks_selectors = [
|
||||
'.//span[contains(@class,"rating")]/text()',
|
||||
'.//div[contains(@class,"rating")]/text()',
|
||||
'.//span[contains(@class,"status")]/text()'
|
||||
]
|
||||
remarks = ''
|
||||
for selector in remarks_selectors:
|
||||
remarks_list = element.xpath(selector)
|
||||
for r in remarks_list:
|
||||
if r and r.strip():
|
||||
remarks = r.strip()
|
||||
break
|
||||
if remarks:
|
||||
break
|
||||
|
||||
return {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': remarks,
|
||||
'vod_year': ''
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"提取视频信息出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def _get_detail(self, doc, vid):
|
||||
"""获取详情信息"""
|
||||
try:
|
||||
# 基本信息
|
||||
title = self._get_text(doc, ['//h1/text()', '//title/text()'])
|
||||
pic = self._get_text(doc, ['//div[@class="dyimg"]//img/@src', '//img[@class="poster"]/@src'])
|
||||
|
||||
if pic and pic.startswith('/'):
|
||||
pic = self.host + pic
|
||||
|
||||
# 描述信息
|
||||
desc = self._get_text(doc, ['//div[@class="yp_context"]/text()', '//div[@class="introduction"]//text()'])
|
||||
|
||||
# 演员和导演
|
||||
actor = self._get_text(doc, ['//span[contains(text(),"主演")]/following-sibling::*/text()'])
|
||||
director = self._get_text(doc, ['//span[contains(text(),"导演")]/following-sibling::*/text()'])
|
||||
|
||||
# 播放源
|
||||
play_from = []
|
||||
play_urls = []
|
||||
|
||||
# 查找播放源
|
||||
source_elements = doc.xpath('//div[@class="mi_paly_box"]')
|
||||
for i, source_elem in enumerate(source_elements):
|
||||
source_name = f"播放源{i+1}"
|
||||
|
||||
# 获取播放链接
|
||||
episodes = []
|
||||
episode_links = source_elem.xpath('.//a')
|
||||
|
||||
for ep_link in episode_links:
|
||||
ep_title = ep_link.xpath('./text()')
|
||||
ep_href = ep_link.xpath('./@href')
|
||||
|
||||
if ep_title and ep_href:
|
||||
ep_title = ep_title[0].strip()
|
||||
ep_href = ep_href[0]
|
||||
|
||||
# 提取播放ID
|
||||
play_id = self.regStr(r'/v_play/([^/]+)\.html', ep_href)
|
||||
if play_id:
|
||||
episodes.append(f"{ep_title}${play_id}")
|
||||
|
||||
if episodes:
|
||||
play_from.append(source_name)
|
||||
play_urls.append('#'.join(episodes))
|
||||
|
||||
return {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': '',
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': actor,
|
||||
'vod_director': director,
|
||||
'vod_content': desc,
|
||||
'vod_play_from': '$$$'.join(play_from),
|
||||
'vod_play_url': '$$$'.join(play_urls)
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"获取详情出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def _get_text(self, doc, selectors):
|
||||
"""通用文本提取"""
|
||||
for selector in selectors:
|
||||
texts = doc.xpath(selector)
|
||||
for text in texts:
|
||||
if text and text.strip():
|
||||
return text.strip()
|
||||
return ''
|
||||
|
||||
def _extract_real_video_url(self, html, play_page_url):
|
||||
"""提取真实视频播放链接 - 精简版"""
|
||||
try:
|
||||
self.log("开始分析播放页面,提取真实视频链接...")
|
||||
|
||||
# 1. 查找iframe的src属性
|
||||
iframe_pattern = r'<iframe[^>]+src="([^"]+)"'
|
||||
iframe_matches = re.findall(iframe_pattern, html, re.IGNORECASE)
|
||||
|
||||
for iframe_src in iframe_matches:
|
||||
self.log(f"找到iframe src: {iframe_src}")
|
||||
|
||||
try:
|
||||
iframe_headers = self.headers.copy()
|
||||
iframe_headers['Referer'] = play_page_url
|
||||
|
||||
if iframe_src.startswith('./'):
|
||||
base_url = '/'.join(play_page_url.split('/')[:-1])
|
||||
iframe_url = f"{base_url}/{iframe_src[2:]}"
|
||||
elif iframe_src.startswith('/'):
|
||||
iframe_url = f"{self.host}{iframe_src}"
|
||||
else:
|
||||
iframe_url = iframe_src
|
||||
|
||||
self.log(f"获取iframe内容: {iframe_url}")
|
||||
iframe_rsp = self.fetch(iframe_url, headers=iframe_headers)
|
||||
iframe_html = iframe_rsp.text
|
||||
|
||||
# 在iframe内容中查找真实视频链接
|
||||
video_url = self._extract_from_iframe_content(iframe_html, iframe_url)
|
||||
if video_url:
|
||||
return video_url
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取iframe内容失败: {str(e)}")
|
||||
continue
|
||||
|
||||
# 2. 直接查找播放器URL
|
||||
player_url_patterns = [
|
||||
r'https://[^"\'\\s]+\.php\?[^"\'\\s]*url=([^"\'\\s&]+)',
|
||||
r'url=(https://[^"\'\\s&]+\.(?:m3u8|mp4|flv)[^"\'\\s]*)',
|
||||
r'url=([^"\'\\s&]+\.(?:m3u8|mp4|flv)[^"\'\\s]*)'
|
||||
]
|
||||
|
||||
for pattern in player_url_patterns:
|
||||
matches = re.findall(pattern, html, re.IGNORECASE)
|
||||
if matches:
|
||||
for match in matches:
|
||||
decoded_url = urllib.parse.unquote(match)
|
||||
if (decoded_url.startswith('http') and
|
||||
any(ext in decoded_url.lower() for ext in ['.m3u8', '.mp4', '.flv'])):
|
||||
self.log(f"从播放器URL提取到真实视频链接: {decoded_url}")
|
||||
return decoded_url
|
||||
|
||||
# 3. 查找129服务器的视频链接
|
||||
direct_129_patterns = [
|
||||
r'https://129\.[^"\'\\s]+\.(?:m3u8|mp4|flv)[^"\'\\s]*',
|
||||
r'https://129[^"\'\\s]+/[^"\'\\s]*\.(?:m3u8|mp4|flv)[^"\'\\s]*'
|
||||
]
|
||||
|
||||
for pattern in direct_129_patterns:
|
||||
matches = re.findall(pattern, html, re.IGNORECASE)
|
||||
if matches:
|
||||
for match in matches:
|
||||
self.log(f"找到129服务器直接视频链接: {match}")
|
||||
return match
|
||||
|
||||
# 4. 查找任何视频文件链接
|
||||
video_file_patterns = [
|
||||
r'https?://[^"\'\\s]+\.m3u8[^"\'\\s]*',
|
||||
r'https?://[^"\'\\s]+\.mp4[^"\'\\s]*',
|
||||
r'https?://[^"\'\\s]+\.flv[^"\'\\s]*'
|
||||
]
|
||||
|
||||
for pattern in video_file_patterns:
|
||||
matches = re.findall(pattern, html, re.IGNORECASE)
|
||||
if matches:
|
||||
for match in matches:
|
||||
if not any(skip in match.lower() for skip in ['blank.gif', 'logo', 'thumb']):
|
||||
self.log(f"找到视频文件链接: {match}")
|
||||
return match
|
||||
|
||||
self.log("未找到任何有效的视频播放链接")
|
||||
return ''
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"提取真实视频链接出错: {str(e)}")
|
||||
return ''
|
||||
|
||||
def _extract_from_iframe_content(self, iframe_html, iframe_url):
|
||||
"""从iframe内容中提取真实视频链接 - 精简版"""
|
||||
try:
|
||||
# 1. 查找JavaScript中的视频URL变量
|
||||
js_video_patterns = [
|
||||
r'const\s+mysvg\s*=\s*["\']([^"\']+)["\']',
|
||||
r'var\s+mysvg\s*=\s*["\']([^"\']+)["\']',
|
||||
r'art\.url\s*=\s*["\']([^"\']+)["\']',
|
||||
r'video\.src\s*=\s*["\']([^"\']+)["\']'
|
||||
]
|
||||
|
||||
for pattern in js_video_patterns:
|
||||
matches = re.findall(pattern, iframe_html, re.IGNORECASE)
|
||||
if matches:
|
||||
for match in matches:
|
||||
if (match.startswith('http') and
|
||||
any(ext in match.lower() for ext in ['.m3u8', '.mp4', '.flv'])):
|
||||
self.log(f"从JavaScript变量找到视频链接: {match}")
|
||||
return match
|
||||
|
||||
# 2. 查找HTML注释中的原始URL
|
||||
comment_pattern = r'<!-- saved from url=\([^)]+\)([^>]+) -->'
|
||||
comment_match = re.search(comment_pattern, iframe_html)
|
||||
if comment_match:
|
||||
saved_url = comment_match.group(1)
|
||||
self.log(f"从HTML注释找到保存的URL: {saved_url}")
|
||||
|
||||
# 从保存的URL中提取url参数
|
||||
url_match = re.search(r'url=([^&\s]+)', saved_url)
|
||||
if url_match:
|
||||
encoded_url = url_match.group(1)
|
||||
decoded_url = urllib.parse.unquote(encoded_url)
|
||||
if any(ext in decoded_url.lower() for ext in ['.m3u8', '.mp4', '.flv']):
|
||||
self.log(f"从HTML注释成功提取视频链接: {decoded_url}")
|
||||
return decoded_url
|
||||
|
||||
# 3. 查找iframe URL本身的url参数
|
||||
if 'url=' in iframe_url:
|
||||
url_match = re.search(r'url=([^&\s]+)', iframe_url)
|
||||
if url_match:
|
||||
encoded_url = url_match.group(1)
|
||||
decoded_url = urllib.parse.unquote(encoded_url)
|
||||
|
||||
# 如果是明文视频链接,直接返回
|
||||
if any(ext in decoded_url.lower() for ext in ['.m3u8', '.mp4', '.flv']):
|
||||
self.log(f"从iframe URL参数提取视频链接: {decoded_url}")
|
||||
return decoded_url
|
||||
|
||||
# 如果是加密字符串,尝试解密
|
||||
elif decoded_url.startswith('videos') and len(decoded_url) > 50:
|
||||
self.log(f"发现加密的视频URL参数: {decoded_url[:50]}...")
|
||||
# 尝试简单的字符替换解密
|
||||
decrypt_attempts = [
|
||||
decoded_url.replace('videos', 'https://129.211.209.237:9091/hls3/hls/'),
|
||||
decoded_url.replace('videos', 'https://129.211.209.237/hls/'),
|
||||
decoded_url.replace('videos', 'https://129.211.209.237:9091/')
|
||||
]
|
||||
|
||||
for attempt in decrypt_attempts:
|
||||
if any(ext in attempt.lower() for ext in ['.m3u8', '.mp4', '.flv']):
|
||||
self.log(f"尝试字符替换解密: {attempt}")
|
||||
return attempt
|
||||
|
||||
# 4. 查找iframe内容中的129服务器链接
|
||||
server_129_patterns = [
|
||||
r'https://129[^"\'\\s]+\.(?:m3u8|mp4|flv)[^"\'\\s]*',
|
||||
r'https://129[^"\'\\s]+/[^"\'\\s]*\.(?:m3u8|mp4|flv)',
|
||||
r'129\.[^"\'\\s]+\.(?:m3u8|mp4|flv)[^"\'\\s]*'
|
||||
]
|
||||
|
||||
for pattern in server_129_patterns:
|
||||
matches = re.findall(pattern, iframe_html, re.IGNORECASE)
|
||||
if matches:
|
||||
for match in matches:
|
||||
if not match.startswith('http'):
|
||||
match = f"https://{match}"
|
||||
self.log(f"从iframe内容找到129服务器链接: {match}")
|
||||
return match
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"从iframe内容提取视频链接出错: {str(e)}")
|
||||
return None
|
||||
@@ -0,0 +1,280 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import colorsys
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
from base64 import b64decode, b64encode
|
||||
from email.utils import unquote
|
||||
from Crypto.Hash import MD5
|
||||
sys.path.append("..")
|
||||
import json
|
||||
import time
|
||||
from pyquery import PyQuery as pq
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='https://www.aowu.tv'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'pragma': 'no-cache',
|
||||
'cache-control': 'no-cache',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'dnt': '1',
|
||||
'upgrade-insecure-requests': '1',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'sec-fetch-mode': 'navigate',
|
||||
'sec-fetch-user': '?1',
|
||||
'sec-fetch-dest': 'document',
|
||||
'referer': f'{host}/',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'priority': 'u=0, i',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=self.getpq(self.fetch(self.host,headers=self.headers).text)
|
||||
result = {}
|
||||
classes = []
|
||||
ldata=data('.wrap.border-box.public-r .public-list-box')
|
||||
cd={"新番":"32","番剧":"20","剧场":"33"}
|
||||
for k,r in cd.items():
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': r,
|
||||
})
|
||||
videos=[]
|
||||
for i in ldata.items():
|
||||
j = i('.public-list-exp')
|
||||
k=i('.public-list-button')
|
||||
videos.append({
|
||||
'vod_id': j.attr('href').split('/')[-1].split('-')[0],
|
||||
'vod_name': k('.time-title').text(),
|
||||
'vod_pic': j('img').attr('data-src'),
|
||||
'vod_year': f"·{j('.public-list-prb').text()}",
|
||||
'vod_remarks': k('.public-list-subtitle').text(),
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list']=videos
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {'type':tid,'class':'','area':'','lang':'','version':'','state':'','letter':'','page':pg}
|
||||
data = self.post(f"{self.host}/index.php/api/vod", headers=self.headers, data=self.getbody(body)).json()
|
||||
result = {}
|
||||
result['list'] = data['list']
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.getpq(self.fetch(f"{self.host}/play/{ids[0]}-1-1.html", headers=self.headers).text)
|
||||
v=data('.player-info-text .this-text')
|
||||
vod = {
|
||||
'type_name': v.eq(-1)('a').text(),
|
||||
'vod_year': v.eq(1)('a').text(),
|
||||
'vod_remarks': v.eq(0).text(),
|
||||
'vod_actor': v.eq(2)('a').text(),
|
||||
'vod_content': data('.player-content').text()
|
||||
}
|
||||
ns=data('.swiper-wrapper .vod-playerUrl')
|
||||
ps=data('.player-list-box .anthology-list-box ul')
|
||||
play,names=[],[]
|
||||
for i in range(len(ns)):
|
||||
n=ns.eq(i)('a')
|
||||
n('span').remove()
|
||||
names.append(re.sub(r"[\ue679\xa0]", "", n.text()))
|
||||
play.append('#'.join([f"{v.text()}${v('a').attr('href')}" for v in ps.eq(i)('li').items()]))
|
||||
vod["vod_play_from"] = "$$$".join(names)
|
||||
vod["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.fetch(f"{self.host}/index.php/ajax/suggest?mid=1&wd={key}&limit=9999×tamp={int(time.time()*1000)}", headers=self.headers).json()
|
||||
videos=[]
|
||||
for i in data['list']:
|
||||
videos.append({
|
||||
'vod_id': i['id'],
|
||||
'vod_name': i['name'],
|
||||
'vod_pic': i['pic']
|
||||
})
|
||||
return {'list':videos,'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
p,url1= 1,''
|
||||
yurl=f"{self.host}{id}"
|
||||
data = self.getpq(self.fetch(yurl, headers=self.headers).text)
|
||||
dmhtm=data('.ds-log-set')
|
||||
dmdata={'vod_id':dmhtm.attr('data-id'),'vod_ep':dmhtm.attr('data-nid')}
|
||||
try:
|
||||
jstr = data('.player-top.box.radius script').eq(0).text()
|
||||
jsdata = json.loads(jstr.split('=',1)[-1])
|
||||
url1= jsdata['url']
|
||||
data = self.fetch(f"{self.host}/player/?url={unquote(self.d64(jsdata['url']))}", headers=self.headers).text
|
||||
data=self.p_qjs(self.getjstr(data))
|
||||
url=data['qualities'] if len(data['qualities']) else data['url']
|
||||
p = 0
|
||||
if not url:raise Exception("未找到播放地址")
|
||||
except Exception as e:
|
||||
self.log(e)
|
||||
url = yurl
|
||||
if re.search(r'\.m3u8|\.mp4',url1):url=url1
|
||||
dmurl = f"{self.getProxyUrl()}&data={self.e64(json.dumps(dmdata))}&type=dm.xml"
|
||||
return {"parse": p, "url": url, "header": {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36'},'danmaku':dmurl}
|
||||
|
||||
def localProxy(self, param):
|
||||
try:
|
||||
data = json.loads(self.d64(param['data']))
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'origin': self.host,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
params = {'vod_id': data['vod_id'], 'vod_ep': data['vod_ep']}
|
||||
res = self.post(f"https://app.wuyaoy.cn/danmu/api.php/getDanmu", headers=headers, data=params).json()
|
||||
danmustr = f'<?xml version="1.0" encoding="UTF-8"?>\n<i>\n\t<chatserver>chat.aowudm.com</chatserver>\n\t<chatid>88888888</chatid>\n\t<mission>0</mission>\n\t<maxlimit>99999</maxlimit>\n\t<state>0</state>\n\t<real_name>0</real_name>\n\t<source>k-v</source>\n'
|
||||
my_list = ['1', '4', '5', '6']
|
||||
for i in sorted(res['data'], key=lambda x: x['time']):
|
||||
dms = [str(i.get('time',1)), random.choice(my_list), '25', self.get_color(), '0']
|
||||
dmtxt = re.sub(r'[<>&\u0000\b]', '', self.cleanText(i.get('text', '')))
|
||||
tempdata = f'\t<d p="{",".join(dms)}">{dmtxt}</d>\n'
|
||||
danmustr += tempdata
|
||||
danmustr += '</i>'
|
||||
return [200,'text/xml',danmustr]
|
||||
except Exception as e:
|
||||
print(f"获取弹幕失败:{str(e)}")
|
||||
return ""
|
||||
|
||||
def getbody(self, params):
|
||||
t=int(time.time())
|
||||
h = MD5.new()
|
||||
h.update(f"DS{t}DCC147D11943AF75".encode('utf-8'))
|
||||
key=h.hexdigest()
|
||||
params.update({'time':t,'key':key})
|
||||
return params
|
||||
|
||||
def getpq(self, data):
|
||||
data=self.cleanText(data)
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
|
||||
def get_color(self):
|
||||
h = random.random()
|
||||
s = random.uniform(0.7, 1.0)
|
||||
v = random.uniform(0.8, 1.0)
|
||||
r, g, b = colorsys.hsv_to_rgb(h, s, v)
|
||||
r = int(r * 255)
|
||||
g = int(g * 255)
|
||||
b = int(b * 255)
|
||||
decimal_color = (r << 16) + (g << 8) + b
|
||||
return str(decimal_color)
|
||||
|
||||
def getjstr(self, data):
|
||||
pattern = r'new\s+Artplayer\s*\((\{[\s\S]*?\})\);'
|
||||
match = re.search(pattern, data)
|
||||
config_str = match.group(1) if match else '{}'
|
||||
|
||||
replacements = [
|
||||
(r'contextmenu\s*:\s*\[[\s\S]*?\{[\s\S]*?\}[\s\S]*?\],', 'contextmenu: [],'),
|
||||
(r'customType\s*:\s*\{[\s\S]*?\},', 'customType: {},'),
|
||||
(r'plugins\s*:\s*\[\s*artplayerPluginDanmuku\(\{[\s\S]*?lockTime:\s*\d+,?\s*\}\)\,?\s*\]', 'plugins: []')
|
||||
]
|
||||
for pattern, replacement in replacements:
|
||||
config_str = re.sub(pattern, replacement, config_str)
|
||||
return config_str
|
||||
|
||||
def p_qjs(self, config_str):
|
||||
try:
|
||||
from com.whl.quickjs.wrapper import QuickJSContext
|
||||
ctx = QuickJSContext.create()
|
||||
js_code = f"""
|
||||
function extractVideoInfo() {{
|
||||
try {{
|
||||
const config = {config_str};
|
||||
const result = {{
|
||||
url: "",
|
||||
qualities: []
|
||||
}};
|
||||
if (config.url) {{
|
||||
result.url = config.url;
|
||||
}}
|
||||
if (config.quality && Array.isArray(config.quality)) {{
|
||||
config.quality.forEach(function(q) {{
|
||||
if (q && q.url) {{
|
||||
result.qualities.push(q.html || "嗷呜");
|
||||
result.qualities.push(q.url);
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
return JSON.stringify(result);
|
||||
}} catch (e) {{
|
||||
return JSON.stringify({{
|
||||
error: "解析错误: " + e.message,
|
||||
url: "",
|
||||
qualities: []
|
||||
}});
|
||||
}}
|
||||
}}
|
||||
extractVideoInfo();
|
||||
"""
|
||||
result_json = ctx.evaluate(js_code)
|
||||
ctx.destroy()
|
||||
return json.loads(result_json)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"执行失败: {e}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"url": "",
|
||||
"qualities": []
|
||||
}
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
return ""
|
||||
|
||||
def d64(self,encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
return ""
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='https://www.nhsyy.com'
|
||||
|
||||
headers = {
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'DNT': '1',
|
||||
'Origin': host,
|
||||
'Pragma': 'no-cache',
|
||||
'Referer': f'{host}/',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="130", "Google Chrome";v="130"',
|
||||
'sec-ch-ua-mobile': '?1',
|
||||
'sec-ch-ua-platform': '"Android"',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = pq(self.fetch(self.host, headers=self.headers).text)
|
||||
result = {}
|
||||
classes = []
|
||||
for i in data('.drop-content-items li').items():
|
||||
j = i('a').attr('href')
|
||||
if j and 'type' in j:
|
||||
id = j.split('/')[-1].split('.')[0]
|
||||
classes.append({
|
||||
'type_name': i('a').text(),
|
||||
'type_id': id
|
||||
})
|
||||
hlist = self.getlist(data('.module-lines-list .module-item'))
|
||||
result['class'] = classes
|
||||
result['list'] = hlist
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data = self.fetch(f'{self.host}/vodshwo/{tid}--------{pg}---.html', headers=self.headers).text
|
||||
vlist = self.getlist(pq(data)('.module-list .module-item'))
|
||||
return {"list": vlist, "page": pg, "pagecount": 9999, "limit": 90, "total": 999999}
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = pq(self.fetch(f"{self.host}{ids[0]}", headers=self.headers).text)
|
||||
udata = data('.scroll-box-y .scroll-content a')
|
||||
vdata = data('.video-info-main .video-info-item')
|
||||
vod = {
|
||||
'vod_year': vdata.eq(2)('div').text(),
|
||||
'vod_remarks': vdata.eq(3)('div').text(),
|
||||
'vod_actor': vdata.eq(1)('a').text(),
|
||||
'vod_director': vdata.eq(0)('a').text(),
|
||||
'typt_name': data('.video-info-aux a').eq(0).attr('title'),
|
||||
'vod_content': vdata.eq(4)('p').eq(-1).text(),
|
||||
'vod_play_from': '嗷呜爱看短剧',
|
||||
'vod_play_url': '#'.join([f"{i.text()}${i.attr('href')}" for i in udata.items()]),
|
||||
}
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
dlist = self.fetch(f'{self.host}/vodsearch/{key}----------{pg}---.html', headers=self.headers).text
|
||||
ldata = pq(dlist)('.module-list .module-search-item')
|
||||
vlist = []
|
||||
for i in ldata.items():
|
||||
img = i('.module-item-pic')
|
||||
vlist.append({
|
||||
'vod_id': i('.video-serial').attr('href'),
|
||||
'vod_name': img('img').attr('alt'),
|
||||
'vod_pic': img('img').attr('data-src'),
|
||||
'vod_year': i('.tag-link a').eq(0).text(),
|
||||
'vod_remarks': i('.video-serial').text()
|
||||
})
|
||||
result = {"list": vlist, "page": pg}
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
data=self.fetch(f"{self.host}{id}", headers=self.headers).text
|
||||
jstr = pq(data)('.player-wrapper script').eq(0).text()
|
||||
try:
|
||||
jdata = json.loads(jstr.split('=', 1)[-1])
|
||||
url = jdata.get('url') or jdata.get('next_url')
|
||||
p=0
|
||||
except:
|
||||
url,p = f"{self.host}{id}",1
|
||||
return {'parse': p, 'url': url, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def getlist(self, data):
|
||||
vlist = []
|
||||
for i in data.items():
|
||||
img = i('.module-item-pic')
|
||||
vlist.append({
|
||||
'vod_id': img('a').attr('href'),
|
||||
'vod_name': img('img').attr('alt'),
|
||||
'vod_pic': img('img').attr('data-src'),
|
||||
'vod_remarks': i('.module-item-text').text()
|
||||
})
|
||||
return vlist
|
||||
@@ -0,0 +1,174 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import re
|
||||
import sys
|
||||
from base64 import b64decode
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Hash import MD5
|
||||
from Crypto.Util.Padding import unpad
|
||||
sys.path.append("..")
|
||||
import json
|
||||
import time
|
||||
from pyquery import PyQuery as pq
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='https://www.xiaohys.com'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'Origin': host,
|
||||
'Referer': f"{host}/",
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=self.getpq(self.fetch(self.host,headers=self.headers).text)
|
||||
result = {}
|
||||
classes = []
|
||||
for k in data('.head-more.box a').items():
|
||||
i=k.attr('href')
|
||||
if i and '/show' in i:
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': i.split('/')[-1]
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list']=self.getlist(data('.border-box.diy-center .public-list-div'))
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {'type':tid,'class':'','area':'','lang':'','version':'','state':'','letter':'','page':pg}
|
||||
data = self.post(f"{self.host}/index.php/api/vod", headers=self.headers, data=self.getbody(body)).json()
|
||||
result = {}
|
||||
result['list'] = data['list']
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.getpq(self.fetch(f"{self.host}/detail/{ids[0]}/", headers=self.headers).text)
|
||||
v=data('.detail-info.lightSpeedIn .slide-info')
|
||||
vod = {
|
||||
'vod_year': v.eq(-1).text(),
|
||||
'vod_remarks': v.eq(0).text(),
|
||||
'vod_actor': v.eq(3).text(),
|
||||
'vod_director': v.eq(2).text(),
|
||||
'vod_content': data('.switch-box #height_limit').text()
|
||||
}
|
||||
np=data('.anthology.wow.fadeInUp')
|
||||
ndata=np('.anthology-tab .swiper-wrapper .swiper-slide')
|
||||
pdata=np('.anthology-list .anthology-list-box ul')
|
||||
play,names=[],[]
|
||||
for i in range(len(ndata)):
|
||||
n=ndata.eq(i)('a')
|
||||
n('span').remove()
|
||||
names.append(n.text())
|
||||
vs=[]
|
||||
for v in pdata.eq(i)('li').items():
|
||||
vs.append(f"{v.text()}${v('a').attr('href')}")
|
||||
play.append('#'.join(vs))
|
||||
vod["vod_play_from"] = "$$$".join(names)
|
||||
vod["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.fetch(f"{self.host}/index.php/ajax/suggest?mid=1&wd={key}&limit=9999×tamp={int(time.time()*1000)}", headers=self.headers).json()
|
||||
videos=[]
|
||||
for i in data['list']:
|
||||
videos.append({
|
||||
'vod_id': i['id'],
|
||||
'vod_name': i['name'],
|
||||
'vod_pic': i['pic']
|
||||
})
|
||||
return {'list':videos,'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
h,p,url1= {"User-Agent": "okhttp/3.14.9"},1,''
|
||||
url=f"{self.host}{id}"
|
||||
data = self.getpq(self.fetch(url, headers=self.headers).text)
|
||||
try:
|
||||
jstr = data('.player .player-left script').eq(0).text()
|
||||
jsdata = json.loads(jstr.split('=',1)[-1])
|
||||
body, url1= {'url': jsdata['url'],'referer':url},jsdata['url']
|
||||
data = self.post(f"{self.host}/static/player/artplayer/api.php?ac=getdate", headers=self.headers, data=body).json()
|
||||
l=self.aes(data['data'],data['iv'])
|
||||
url=l.get('url') or l['data'].get('url')
|
||||
p = 0
|
||||
if not url:raise Exception('未找到播放地址')
|
||||
except Exception as e:
|
||||
print('错误信息:',e)
|
||||
if re.search(r'\.m3u8|\.mp4',url1):url=url1
|
||||
result = {}
|
||||
result["parse"] = p
|
||||
result["url"] = url
|
||||
result["header"] = h
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def getbody(self, params):
|
||||
t=int(time.time())
|
||||
h = MD5.new()
|
||||
h.update(f"DS{t}DCC147D11943AF75".encode('utf-8'))
|
||||
key=h.hexdigest()
|
||||
params.update({'time':t,'key':key})
|
||||
return params
|
||||
|
||||
def getlist(self,data):
|
||||
videos=[]
|
||||
for i in data.items():
|
||||
id = i('a').attr('href')
|
||||
if id:
|
||||
id = re.search(r'\d+', id).group(0)
|
||||
img = i('img').attr('data-src')
|
||||
if img and 'url=' in img and 'http' not in img: img = f'{self.host}{img}'
|
||||
videos.append({
|
||||
'vod_id': id,
|
||||
'vod_name': i('img').attr('alt'),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': i('.public-prt').text() or i('.public-list-prb').text()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getpq(self, data):
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
|
||||
def aes(self, text,iv):
|
||||
key = b"d978a93ffb4d3a00"
|
||||
iv = iv.encode("utf-8")
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size)
|
||||
return json.loads(pt.decode("utf-8"))
|
||||
@@ -0,0 +1,169 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import re
|
||||
import sys
|
||||
from Crypto.Hash import MD5
|
||||
sys.path.append("..")
|
||||
import json
|
||||
import time
|
||||
from pyquery import PyQuery as pq
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = 'https://www.lreeok.vip'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'Origin': host,
|
||||
'Referer': f"{host}/",
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self.getpq(self.fetch(self.host, headers=self.headers).text)
|
||||
result = {}
|
||||
classes = []
|
||||
for k in data('.head-more.box a').items():
|
||||
i = k.attr('href')
|
||||
if i and '/vod' in i:
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': re.search(r'\d+', i).group(0)
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list'] = self.getlist(data('.border-box.diy-center .public-list-div'))
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {'type': tid, 'class': '', 'area': '', 'lang': '', 'version': '', 'state': '', 'letter': '', 'page': pg}
|
||||
data = self.post(f"{self.host}/index.php/api/vod", headers=self.headers, data=self.getbody(body)).json()
|
||||
result = {}
|
||||
result['list'] = data['list']
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.getpq(self.fetch(f"{self.host}/voddetail/{ids[0]}.html", headers=self.headers).text)
|
||||
v = data('.detail-info.lightSpeedIn .slide-info')
|
||||
vod = {
|
||||
'vod_year': v.eq(-1).text(),
|
||||
'vod_remarks': v.eq(0).text(),
|
||||
'vod_actor': v.eq(3).text(),
|
||||
'vod_director': v.eq(2).text(),
|
||||
'vod_content': data('.switch-box #height_limit').text()
|
||||
}
|
||||
np = data('.anthology.wow.fadeInUp')
|
||||
ndata = np('.anthology-tab .swiper-wrapper .swiper-slide')
|
||||
pdata = np('.anthology-list .anthology-list-box ul')
|
||||
play, names = [], []
|
||||
for i in range(len(ndata)):
|
||||
n = ndata.eq(i)('a')
|
||||
n('span').remove()
|
||||
names.append(n.text())
|
||||
vs = []
|
||||
for v in pdata.eq(i)('li').items():
|
||||
vs.append(f"{v.text()}${v('a').attr('href')}")
|
||||
play.append('#'.join(vs))
|
||||
vod["vod_play_from"] = "$$$".join(names)
|
||||
vod["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
# data = self.getpq(self.fetch(f"{self.host}/vodsearch/{key}----------{pg}---.html", headers=self.headers).text)
|
||||
# return {'list': self.getlist(data('.row-right .search-box .public-list-bj')), 'page': pg}
|
||||
data = self.fetch(
|
||||
f"{self.host}/index.php/ajax/suggest?mid={pg}&wd={key}&limit=999×tamp={int(time.time() * 1000)}",
|
||||
headers=self.headers).json()
|
||||
videos = []
|
||||
for i in data['list']:
|
||||
videos.append({
|
||||
'vod_id': i['id'],
|
||||
'vod_name': i['name'],
|
||||
'vod_pic': i['pic']
|
||||
})
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
h, p = {"User-Agent": "okhttp/3.14.9"}, 1
|
||||
url = f"{self.host}{id}"
|
||||
data = self.getpq(self.fetch(url, headers=self.headers).text)
|
||||
try:
|
||||
jstr = data('.player .player-left script').eq(0).text()
|
||||
jsdata = json.loads(jstr.split('aaa=')[-1])
|
||||
body = {'url': jsdata['url']}
|
||||
if not re.search(r'\.m3u8|\.mp4', body['url']):
|
||||
data = self.post(f"{self.host}/okplay/api_config.php", headers=self.headers,
|
||||
data=self.getbody(body)).json()
|
||||
url = data.get('url') or data.get('data', {}).get('url')
|
||||
p = 0
|
||||
except Exception as e:
|
||||
print('错误信息:', e)
|
||||
pass
|
||||
result = {}
|
||||
result["parse"] = p
|
||||
result["url"] = url
|
||||
result["header"] = h
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def getbody(self, params):
|
||||
t = int(time.time())
|
||||
h = MD5.new()
|
||||
h.update(f"DS{t}DCC147D11943AF75".encode('utf-8'))
|
||||
key = h.hexdigest()
|
||||
params.update({'time': t, 'key': key})
|
||||
return params
|
||||
|
||||
def getlist(self, data):
|
||||
videos = []
|
||||
for i in data.items():
|
||||
id = i('a').attr('href')
|
||||
if id:
|
||||
id = re.search(r'\d+', id).group(0)
|
||||
img = i('img').attr('data-src')
|
||||
if img and 'url=' in img: img = f'{self.host}{img}'
|
||||
videos.append({
|
||||
'vod_id': id,
|
||||
'vod_name': i('img').attr('alt'),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': i('.public-prt').text() or i('.public-list-prb').text()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getpq(self, data):
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
@@ -0,0 +1,223 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import concurrent.futures
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from base64 import b64decode, b64encode
|
||||
import requests
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = self.gethost()
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
||||
'Pragma': 'no-cache',
|
||||
'Cache-Control': 'no-cache',
|
||||
'sec-ch-ua-platform': '"Android"',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="130", "Google Chrome";v="130"',
|
||||
'DNT': '1',
|
||||
'sec-ch-ua-mobile': '?1',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Dest': 'video',
|
||||
'Sec-Fetch-Storage-Access': 'active',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
||||
}
|
||||
|
||||
config ={"1": [{"key": "cateId","name": "类型","value": [{"n": "全部","v": "1"},{"n": "动作片","v": "5"},{"n": "喜剧片","v": "6"},{"n": "爱情片","v": "7"},{"n": "科幻片","v": "8"},{"n": "恐怖片","v": "9"},{"n": "剧情片","v": "10"},{"n": "战争片","v": "11"},{"n": "惊悚片","v": "16"},{"n": "奇幻片","v": "17"}]},{"key": "area","name": "地区","value": [{"n": "全部","v": ""},{"n": "大陆","v": "大陆"},{"n": "香港","v": "香港"},{"n": "台湾","v": "台湾"},{"n": "美国","v": "美国"},{"n": "韩国","v": "韩国"},{"n": "日本","v": "日本"},{"n": "泰国","v": "泰国"},{"n": "新加坡","v": "新加坡"},{"n": "马来西亚","v": "马来西亚"},{"n": "印度","v": "印度"},{"n": "英国","v": "英国"},{"n": "法国","v": "法国"},{"n": "加拿大","v": "加拿大"},{"n": "西班牙","v": "西班牙"},{"n": "俄罗斯","v": "俄罗斯"},{"n": "其它","v": "其它"}]},{"key": "year","name": "时间","value": [{"n": "全部","v": ""},{"n": "2024","v": "2024"},{"n": "2023","v": "2023"},{"n": "2022","v": "2022"},{"n": "2021","v": "2021"},{"n": "2020","v": "2020"},{"n": "2019","v": "2019"},{"n": "2018","v": "2018"},{"n": "2017","v": "2017"},{"n": "2016","v": "2016"},{"n": "2015","v": "2015"},{"n": "2014","v": "2014"},{"n": "2013","v": "2013"},{"n": "2012","v": "2012"},{"n": "2011","v": "2011"},{"n": "2010","v": "2010"},{"n": "2009","v": "2009"},{"n": "2008","v": "2008"},{"n": "2007","v": "2007"},{"n": "2006","v": "2006"},{"n": "2005","v": "2005"},{"n": "2004","v": "2004"},{"n": "2003","v": "2003"},{"n": "2002","v": "2002"},{"n": "2001","v": "2001"},{"n": "2000","v": "2000"},{"n": "1999","v": "1999"},{"n": "1998","v": "1998"},{"n": "1997","v": "1997"},{"n": "1996","v": "1996"},{"n": "1995","v": "1995"},{"n": "1994","v": "1994"},{"n": "1993","v": "1993"},{"n": "1992","v": "1992"},{"n": "1991","v": "1991"},{"n": "1990","v": "1990"},{"n": "1989","v": "1989"},{"n": "1988","v": "1988"},{"n": "1987","v": "1987"},{"n": "1986","v": "1986"},{"n": "1985","v": "1985"},{"n": "1984","v": "1984"},{"n": "1983","v": "1983"},{"n": "1982","v": "1982"},{"n": "1981","v": "1981"},{"n": "1980","v": "1980"},{"n": "1979","v": "1979"},{"n": "1978","v": "1978"},{"n": "1977","v": "1977"},{"n": "1976","v": "1976"},{"n": "1975","v": "1975"},{"n": "1974","v": "1974"},{"n": "1973","v": "1973"},{"n": "1972","v": "1972"},{"n": "1971","v": "1971"},{"n": "1970","v": "1970"},{"n": "1969","v": "1969"},{"n": "1968","v": "1968"},{"n": "1967","v": "1967"},{"n": "1966","v": "1966"},{"n": "1965","v": "1965"},{"n": "1964","v": "1964"},{"n": "1963","v": "1963"},{"n": "1962","v": "1962"},{"n": "1961","v": "1961"},{"n": "1960","v": "1960"},{"n": "1959","v": "1959"},{"n": "1958","v": "1958"},{"n": "1957","v": "1957"},{"n": "1956","v": "1956"},{"n": "1955","v": "1955"},{"n": "1954","v": "1954"},{"n": "1953","v": "1953"},{"n": "1952","v": "1952"},{"n": "1951","v": "1951"},{"n": "1950","v": "1950"},{"n": "1949","v": "1949"},{"n": "1948","v": "1948"},{"n": "1947","v": "1947"},{"n": "1946","v": "1946"},{"n": "1945","v": "1945"},{"n": "1944","v": "1944"},{"n": "1943","v": "1943"},{"n": "1942","v": "1942"},{"n": "1941","v": "1941"},{"n": "1940","v": "1940"},{"n": "1939","v": "1939"},{"n": "1938","v": "1938"},{"n": "1937","v": "1937"},{"n": "1936","v": "1936"},{"n": "1935","v": "1935"},{"n": "1934","v": "1934"},{"n": "1933","v": "1933"},{"n": "1932","v": "1932"},{"n": "1931","v": "1931"},{"n": "1930","v": "1930"},{"n": "1929","v": "1929"},{"n": "1928","v": "1928"},{"n": "1927","v": "1927"},{"n": "1926","v": "1926"},{"n": "1925","v": "1925"},{"n": "1924","v": "1924"},{"n": "1923","v": "1923"},{"n": "1922","v": "1922"},{"n": "1921","v": "1921"},{"n": "1920","v": "1920"},{"n": "1919","v": "1919"},{"n": "1918","v": "1918"},{"n": "1917","v": "1917"},{"n": "1916","v": "1916"},{"n": "1915","v": "1915"},{"n": "1914","v": "1914"}]},{"key": "letter","name": "字母","value": [{"n": "全部","v": ""},{"n": "A","v": "A"},{"n": "B","v": "B"},{"n": "C","v": "C"},{"n": "D","v": "D"},{"n": "E","v": "E"},{"n": "F","v": "F"},{"n": "G","v": "G"},{"n": "H","v": "H"},{"n": "I","v": "I"},{"n": "J","v": "J"},{"n": "K","v": "K"},{"n": "L","v": "L"},{"n": "M","v": "M"},{"n": "N","v": "N"},{"n": "O","v": "O"},{"n": "P","v": "P"},{"n": "Q","v": "Q"},{"n": "R","v": "R"},{"n": "S","v": "S"},{"n": "T","v": "T"},{"n": "U","v": "U"},{"n": "V","v": "V"},{"n": "W","v": "W"},{"n": "X","v": "X"},{"n": "Y","v": "Y"},{"n": "Z","v": "Z"},{"n": "0-9","v": "0-9"}]},{"key": "by","name": "排序","value": [{"n": "全部","v": ""},{"n": "时间","v": "time"},{"n": "人气","v": "hits"},{"n": "评分","v": "score"}]}],"2": [{"key": "cateId","name": "类型","value": [{"n": "全部","v": "2"},{"n": "国产剧","v": "12"},{"n": "港台泰","v": "13"},{"n": "日韩剧","v": "14"},{"n": "欧美剧","v": "15"}]},{"key": "area","name": "地区","value": [{"n": "全部","v": ""},{"n": "大陆","v": "大陆"},{"n": "香港","v": "香港"},{"n": "台湾","v": "台湾"},{"n": "美国","v": "美国"},{"n": "韩国","v": "韩国"},{"n": "日本","v": "日本"},{"n": "泰国","v": "泰国"},{"n": "新加坡","v": "新加坡"},{"n": "马来西亚","v": "马来西亚"},{"n": "印度","v": "印度"},{"n": "英国","v": "英国"},{"n": "法国","v": "法国"},{"n": "加拿大","v": "加拿大"},{"n": "西班牙","v": "西班牙"},{"n": "俄罗斯","v": "俄罗斯"},{"n": "其它","v": "其它"}]},{"key": "year","name": "时间","value": [{"n": "全部","v": ""},{"n": "2024","v": "2024"},{"n": "2023","v": "2023"},{"n": "2022","v": "2022"},{"n": "2021","v": "2021"},{"n": "2020","v": "2020"},{"n": "2019","v": "2019"},{"n": "2018","v": "2018"},{"n": "2017","v": "2017"},{"n": "2016","v": "2016"},{"n": "2015","v": "2015"},{"n": "2014","v": "2014"},{"n": "2013","v": "2013"},{"n": "2012","v": "2012"},{"n": "2011","v": "2011"},{"n": "2010","v": "2010"},{"n": "2009","v": "2009"},{"n": "2008","v": "2008"},{"n": "2007","v": "2007"},{"n": "2006","v": "2006"},{"n": "2005","v": "2005"},{"n": "2004","v": "2004"},{"n": "2003","v": "2003"},{"n": "2002","v": "2002"},{"n": "2001","v": "2001"},{"n": "2000","v": "2000"},{"n": "1999","v": "1999"},{"n": "1998","v": "1998"},{"n": "1997","v": "1997"},{"n": "1996","v": "1996"},{"n": "1995","v": "1995"},{"n": "1994","v": "1994"},{"n": "1993","v": "1993"},{"n": "1992","v": "1992"},{"n": "1991","v": "1991"},{"n": "1990","v": "1990"},{"n": "1989","v": "1989"},{"n": "1988","v": "1988"},{"n": "1987","v": "1987"},{"n": "1986","v": "1986"},{"n": "1985","v": "1985"},{"n": "1984","v": "1984"},{"n": "1983","v": "1983"},{"n": "1982","v": "1982"},{"n": "1981","v": "1981"},{"n": "1980","v": "1980"},{"n": "1979","v": "1979"},{"n": "1978","v": "1978"},{"n": "1977","v": "1977"},{"n": "1976","v": "1976"},{"n": "1975","v": "1975"},{"n": "1974","v": "1974"},{"n": "1973","v": "1973"},{"n": "1972","v": "1972"},{"n": "1971","v": "1971"},{"n": "1970","v": "1970"},{"n": "1969","v": "1969"},{"n": "1968","v": "1968"},{"n": "1967","v": "1967"},{"n": "1966","v": "1966"},{"n": "1965","v": "1965"},{"n": "1964","v": "1964"},{"n": "1963","v": "1963"},{"n": "1962","v": "1962"},{"n": "1961","v": "1961"},{"n": "1960","v": "1960"}]},{"key": "letter","name": "字母","value": [{"n": "全部","v": ""},{"n": "A","v": "A"},{"n": "B","v": "B"},{"n": "C","v": "C"},{"n": "D","v": "D"},{"n": "E","v": "E"},{"n": "F","v": "F"},{"n": "G","v": "G"},{"n": "H","v": "H"},{"n": "I","v": "I"},{"n": "J","v": "J"},{"n": "K","v": "K"},{"n": "L","v": "L"},{"n": "M","v": "M"},{"n": "N","v": "N"},{"n": "O","v": "O"},{"n": "P","v": "P"},{"n": "Q","v": "Q"},{"n": "R","v": "R"},{"n": "S","v": "S"},{"n": "T","v": "T"},{"n": "U","v": "U"},{"n": "V","v": "V"},{"n": "W","v": "W"},{"n": "X","v": "X"},{"n": "Y","v": "Y"},{"n": "Z","v": "Z"},{"n": "0-9","v": "0-9"}]},{"key": "by","name": "排序","value": [{"n": "全部","v": ""},{"n": "时间","v": "time"},{"n": "人气","v": "hits"},{"n": "评分","v": "score"}]}],"3": [{"key": "area","name": "地区","value": [{"n": "全部","v": ""},{"n": "大陆","v": "大陆"},{"n": "香港","v": "香港"},{"n": "台湾","v": "台湾"},{"n": "美国","v": "美国"},{"n": "韩国","v": "韩国"},{"n": "日本","v": "日本"},{"n": "泰国","v": "泰国"},{"n": "新加坡","v": "新加坡"},{"n": "马来西亚","v": "马来西亚"},{"n": "印度","v": "印度"},{"n": "英国","v": "英国"},{"n": "法国","v": "法国"},{"n": "加拿大","v": "加拿大"},{"n": "西班牙","v": "西班牙"},{"n": "俄罗斯","v": "俄罗斯"},{"n": "其它","v": "其它"}]},{"key": "year","name": "时间","value": [{"n": "全部","v": ""},{"n": "2024","v": "2024"},{"n": "2023","v": "2023"},{"n": "2022","v": "2022"},{"n": "2021","v": "2021"},{"n": "2020","v": "2020"},{"n": "2019","v": "2019"},{"n": "2018","v": "2018"},{"n": "2017","v": "2017"},{"n": "2016","v": "2016"},{"n": "2015","v": "2015"},{"n": "2014","v": "2014"},{"n": "2013","v": "2013"},{"n": "2012","v": "2012"},{"n": "2011","v": "2011"},{"n": "2010","v": "2010"},{"n": "2009","v": "2009"},{"n": "2008","v": "2008"},{"n": "2007","v": "2007"},{"n": "2006","v": "2006"},{"n": "2005","v": "2005"},{"n": "2004","v": "2004"},{"n": "2003","v": "2003"},{"n": "2002","v": "2002"},{"n": "2001","v": "2001"},{"n": "2000","v": "2000"},{"n": "1999","v": "1999"},{"n": "1998","v": "1998"},{"n": "1997","v": "1997"},{"n": "1996","v": "1996"},{"n": "1995","v": "1995"},{"n": "1994","v": "1994"},{"n": "1993","v": "1993"},{"n": "1992","v": "1992"},{"n": "1991","v": "1991"},{"n": "1990","v": "1990"},{"n": "1989","v": "1989"},{"n": "1988","v": "1988"},{"n": "1987","v": "1987"},{"n": "1986","v": "1986"},{"n": "1985","v": "1985"},{"n": "1984","v": "1984"},{"n": "1983","v": "1983"}]},{"key": "letter","name": "字母","value": [{"n": "全部","v": ""},{"n": "A","v": "A"},{"n": "B","v": "B"},{"n": "C","v": "C"},{"n": "D","v": "D"},{"n": "E","v": "E"},{"n": "F","v": "F"},{"n": "G","v": "G"},{"n": "H","v": "H"},{"n": "I","v": "I"},{"n": "J","v": "J"},{"n": "K","v": "K"},{"n": "L","v": "L"},{"n": "M","v": "M"},{"n": "N","v": "N"},{"n": "O","v": "O"},{"n": "P","v": "P"},{"n": "Q","v": "Q"},{"n": "R","v": "R"},{"n": "S","v": "S"},{"n": "T","v": "T"},{"n": "U","v": "U"},{"n": "V","v": "V"},{"n": "W","v": "W"},{"n": "X","v": "X"},{"n": "Y","v": "Y"},{"n": "Z","v": "Z"},{"n": "0-9","v": "0-9"}]},{"key": "by","name": "排序","value": [{"n": "全部","v": ""},{"n": "时间","v": "time"},{"n": "人气","v": "hits"},{"n": "评分","v": "score"}]}],"4": [{"key": "cateId","name": "类型","value": [{"n": "全部","v": "4"},{"n": "动漫剧","v": "18"},{"n": "动漫片","v": "19"}]},{"key": "area","name": "地区","value": [{"n": "全部","v": ""},{"n": "大陆","v": "大陆"},{"n": "香港","v": "香港"},{"n": "台湾","v": "台湾"},{"n": "美国","v": "美国"},{"n": "韩国","v": "韩国"},{"n": "日本","v": "日本"},{"n": "泰国","v": "泰国"},{"n": "新加坡","v": "新加坡"},{"n": "马来西亚","v": "马来西亚"},{"n": "印度","v": "印度"},{"n": "英国","v": "英国"},{"n": "法国","v": "法国"},{"n": "加拿大","v": "加拿大"},{"n": "西班牙","v": "西班牙"},{"n": "俄罗斯","v": "俄罗斯"},{"n": "其它","v": "其它"}]},{"key": "year","name": "时间","value": [{"n": "全部","v": ""},{"n": "2024","v": "2024"},{"n": "2023","v": "2023"},{"n": "2022","v": "2022"},{"n": "2021","v": "2021"},{"n": "2020","v": "2020"},{"n": "2019","v": "2019"},{"n": "2018","v": "2018"},{"n": "2017","v": "2017"},{"n": "2016","v": "2016"},{"n": "2015","v": "2015"},{"n": "2014","v": "2014"},{"n": "2013","v": "2013"},{"n": "2012","v": "2012"},{"n": "2011","v": "2011"},{"n": "2010","v": "2010"},{"n": "2009","v": "2009"},{"n": "2008","v": "2008"},{"n": "2007","v": "2007"},{"n": "2006","v": "2006"},{"n": "2005","v": "2005"},{"n": "2004","v": "2004"},{"n": "2003","v": "2003"},{"n": "2002","v": "2002"},{"n": "2001","v": "2001"},{"n": "2000","v": "2000"},{"n": "1999","v": "1999"},{"n": "1998","v": "1998"},{"n": "1997","v": "1997"},{"n": "1996","v": "1996"},{"n": "1995","v": "1995"},{"n": "1994","v": "1994"},{"n": "1993","v": "1993"},{"n": "1992","v": "1992"},{"n": "1991","v": "1991"},{"n": "1990","v": "1990"},{"n": "1989","v": "1989"},{"n": "1988","v": "1988"},{"n": "1987","v": "1987"},{"n": "1986","v": "1986"},{"n": "1985","v": "1985"},{"n": "1984","v": "1984"},{"n": "1983","v": "1983"},{"n": "1982","v": "1982"},{"n": "1981","v": "1981"},{"n": "1980","v": "1980"},{"n": "1979","v": "1979"},{"n": "1978","v": "1978"},{"n": "1977","v": "1977"},{"n": "1976","v": "1976"},{"n": "1975","v": "1975"},{"n": "1974","v": "1974"},{"n": "1973","v": "1973"},{"n": "1972","v": "1972"},{"n": "1971","v": "1971"},{"n": "1970","v": "1970"},{"n": "1969","v": "1969"},{"n": "1968","v": "1968"},{"n": "1967","v": "1967"}]},{"key": "letter","name": "字母","value": [{"n": "全部","v": ""},{"n": "A","v": "A"},{"n": "B","v": "B"},{"n": "C","v": "C"},{"n": "D","v": "D"},{"n": "E","v": "E"},{"n": "F","v": "F"},{"n": "G","v": "G"},{"n": "H","v": "H"},{"n": "I","v": "I"},{"n": "J","v": "J"},{"n": "K","v": "K"},{"n": "L","v": "L"},{"n": "M","v": "M"},{"n": "N","v": "N"},{"n": "O","v": "O"},{"n": "P","v": "P"},{"n": "Q","v": "Q"},{"n": "R","v": "R"},{"n": "S","v": "S"},{"n": "T","v": "T"},{"n": "U","v": "U"},{"n": "V","v": "V"},{"n": "W","v": "W"},{"n": "X","v": "X"},{"n": "Y","v": "Y"},{"n": "Z","v": "Z"},{"n": "0-9","v": "0-9"}]},{"key": "by","name": "排序","value": [{"n": "全部","v": ""},{"n": "时间","v": "time"},{"n": "人气","v": "hits"},{"n": "评分","v": "score"}]}],"26": [{"key": "area","name": "地区","value": [{"n": "全部","v": ""},{"n": "大陆","v": "大陆"},{"n": "香港","v": "香港"},{"n": "台湾","v": "台湾"},{"n": "美国","v": "美国"},{"n": "韩国","v": "韩国"},{"n": "日本","v": "日本"},{"n": "泰国","v": "泰国"},{"n": "新加坡","v": "新加坡"},{"n": "马来西亚","v": "马来西亚"},{"n": "印度","v": "印度"},{"n": "英国","v": "英国"},{"n": "法国","v": "法国"},{"n": "加拿大","v": "加拿大"},{"n": "西班牙","v": "西班牙"},{"n": "俄罗斯","v": "俄罗斯"},{"n": "其它","v": "其它"}]},{"key": "year","name": "时间","value": [{"n": "全部","v": ""},{"n": "2024","v": "2024"},{"n": "2023","v": "2023"},{"n": "2022","v": "2022"},{"n": "2021","v": "2021"},{"n": "2020","v": "2020"}]},{"key": "letter","name": "字母","value": [{"n": "全部","v": ""},{"n": "A","v": "A"},{"n": "B","v": "B"},{"n": "C","v": "C"},{"n": "D","v": "D"},{"n": "E","v": "E"},{"n": "F","v": "F"},{"n": "G","v": "G"},{"n": "H","v": "H"},{"n": "I","v": "I"},{"n": "J","v": "J"},{"n": "K","v": "K"},{"n": "L","v": "L"},{"n": "M","v": "M"},{"n": "N","v": "N"},{"n": "O","v": "O"},{"n": "P","v": "P"},{"n": "Q","v": "Q"},{"n": "R","v": "R"},{"n": "S","v": "S"},{"n": "T","v": "T"},{"n": "U","v": "U"},{"n": "V","v": "V"},{"n": "W","v": "W"},{"n": "X","v": "X"},{"n": "Y","v": "Y"},{"n": "Z","v": "Z"},{"n": "0-9","v": "0-9"}]},{"key": "by","name": "排序","value": [{"n": "全部","v": ""},{"n": "时间","v": "time"},{"n": "人气","v": "hits"},{"n": "评分","v": "score"}]}]}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self.getpq()
|
||||
cdata = data('#topnav .swiper-wrapper li')
|
||||
result = {}
|
||||
classes = []
|
||||
videos = []
|
||||
for k in cdata.items():
|
||||
i = k('a').attr('href')
|
||||
if i and 'type' in i and '音乐' not in k.text():
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': i.split('-')[-3],
|
||||
})
|
||||
for i in list(data('.globalPicList').items())[1:]:
|
||||
videos.extend(self.getlist(i('ul li')))
|
||||
result['class'] = classes
|
||||
result['filters'] = self.config
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data = self.getpq(
|
||||
f"/vod-list-id-{extend.get('cateId', tid)}-pg-{pg}-order--by-{extend.get('by', 'time')}-class-0-year-{extend.get('year', '')}-letter-{extend.get('letter', '')}-area-{extend.get('area', '')}-lang-.html")
|
||||
result = {}
|
||||
result['list'] = self.getlist(data('.globalPicList .resize_list li'))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.getpq(ids[0])
|
||||
v = data('.numList ul li').eq(0)('a').attr('href')
|
||||
html = self.getpq(v)
|
||||
d = html('.detailPosterIntro script').eq(0).text()
|
||||
mac_from = re.search(r"mac_from='(.*?)'", d)
|
||||
mac_url = re.search(r"mac_url='(.*?)'", d).group(1)
|
||||
z = data('.page-bd')
|
||||
c = z('.desc_item')
|
||||
vod = {
|
||||
'vod_name': z('h1 a').text(),
|
||||
'vod_year': c.eq(3)('a').text(),
|
||||
'vod_remarks': c.eq(0)('font').text(),
|
||||
'vod_actor': c.eq(1)('a').text(),
|
||||
'vod_director': c.eq(2)('a').text(),
|
||||
'vod_content': data('.detail-con p').text().split(':')[-1],
|
||||
'vod_play_from': mac_from.group(1) if mac_from else '呜呜呜',
|
||||
'vod_play_url': mac_url
|
||||
}
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = pq(self.post(f"{self.host}/index.php?m=vod-search", data={'wd': key}, headers=self.headers).text)
|
||||
video = []
|
||||
for k in data('#data_list li').items():
|
||||
video.append({
|
||||
'vod_id': k('.pic a').attr('href'),
|
||||
'vod_name': k('.sTit').text(),
|
||||
'vod_pic': k('.pic img').attr('src'),
|
||||
'vod_year': k('.sStyle').text(),
|
||||
'vod_remarks': k('.sDes').eq(-1).text()
|
||||
})
|
||||
return {'list': video, 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
if flag == '呜呜呜': raise Exception('未找到播放地址')
|
||||
jxdata = self.getpq(f"/player/{flag}.js").html()
|
||||
jxurl = re.search(r'http.*?url=', jxdata).group()
|
||||
data = self.fetch(f"{jxurl}{id}", headers=self.headers).text
|
||||
matches = re.findall(r'http.*?url=', data)
|
||||
if len(matches):
|
||||
url = []
|
||||
for i, x in enumerate(matches):
|
||||
js = {'jx': x, 'id': id}
|
||||
purl = f"{self.getProxyUrl()}&wdict={self.e64(json.dumps(js))}"
|
||||
url.extend([f'线路{i + 1}', purl])
|
||||
else:
|
||||
url = re.search(r"url='(.*?)'", data).group(1)
|
||||
if not url: raise Exception('未找到播放地址')
|
||||
p = 0
|
||||
except:
|
||||
p, url = 1, id
|
||||
return {'parse': p, 'url': url, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
wdict = json.loads(self.d64(param['wdict']))
|
||||
url = f"{wdict['jx']}{wdict['id']}"
|
||||
data = pq(self.fetch(url, headers=self.headers).text)
|
||||
html = data('script').eq(-1).text()
|
||||
url = re.search(r'src="(.*?)"', html).group(1)
|
||||
return [302, 'text/html', None, {'Location': url}]
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def gethost(self):
|
||||
data = pq(self.fetch('https://www.nmdvd.com', headers=self.headers).text)
|
||||
hlist = data('a[rel="nofollow"] b').text().split(' ')
|
||||
return self.host_late(hlist)
|
||||
|
||||
def host_late(self, urls):
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future_to_url = {
|
||||
executor.submit(self.test_host, f"https://{url}"): f"https://{url}"
|
||||
for url in urls
|
||||
}
|
||||
results = {}
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
results[url] = future.result()
|
||||
except Exception as e:
|
||||
results[url] = float('inf')
|
||||
min_url = min(results.items(), key=lambda x: x[1])[0] if results else None
|
||||
if all(delay == float('inf') for delay in results.values()) or not min_url:
|
||||
return f"https://{urls[0]}"
|
||||
return min_url
|
||||
|
||||
def test_host(self, url):
|
||||
try:
|
||||
start_time = time.monotonic()
|
||||
response = requests.head(
|
||||
url,
|
||||
timeout=1.0,
|
||||
allow_redirects=False,
|
||||
headers=self.headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return (time.monotonic() - start_time) * 1000
|
||||
except Exception as e:
|
||||
print(f"测试{url}失败: {str(e)}")
|
||||
return float('inf')
|
||||
|
||||
def getpq(self, path=''):
|
||||
data = self.fetch(f"{self.host}{path}", headers=self.headers).text
|
||||
return pq(data)
|
||||
|
||||
def getlist(self, data):
|
||||
videos = []
|
||||
for k in data.items():
|
||||
i = k('.sBottom')
|
||||
j = i('em').text()
|
||||
i.remove('em')
|
||||
videos.append({
|
||||
'vod_id': k('a').attr('href'),
|
||||
'vod_name': k('.sTit').text(),
|
||||
'vod_pic': k('.pic img').attr('src'),
|
||||
'vod_year': j,
|
||||
'vod_remarks': i.text(),
|
||||
})
|
||||
return videos
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
@@ -0,0 +1,557 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# cnotv.com (明月影院) 爬虫插件
|
||||
# 开发者: Augment Agent
|
||||
# 网站: https://cnotv.com/
|
||||
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import urllib.parse
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append("..")
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.extend = extend
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "明月影院"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
# 网站基本配置
|
||||
host = 'https://cnotv.com'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'identity', # 禁用压缩
|
||||
'Referer': 'https://cnotv.com/',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""获取首页内容和分类列表"""
|
||||
try:
|
||||
# 获取首页内容
|
||||
response = self.fetch(self.host, headers=self.headers)
|
||||
doc = pq(response.text)
|
||||
|
||||
result = {}
|
||||
|
||||
# 提取分类列表 - 基于实际HTML结构
|
||||
classes = []
|
||||
# 查找导航链接
|
||||
nav_links = doc('ul li a')
|
||||
for item in nav_links.items():
|
||||
href = item.attr('href')
|
||||
text = item.text().strip()
|
||||
if href and '/vodtype/' in href and text:
|
||||
type_id = re.search(r'/vodtype/(\d+)/', href)
|
||||
if type_id:
|
||||
classes.append({
|
||||
'type_name': text,
|
||||
'type_id': type_id.group(1)
|
||||
})
|
||||
|
||||
result['class'] = classes
|
||||
|
||||
# 提取首页推荐视频列表 - 使用.module-item容器
|
||||
videos = []
|
||||
video_containers = doc('.module-item')
|
||||
|
||||
for container in video_containers.items():
|
||||
video_info = self.extract_video_info_from_container(container)
|
||||
if video_info:
|
||||
videos.append(video_info)
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"homeContent error: {str(e)}")
|
||||
return {'class': [], 'list': []}
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""获取分类页面内容"""
|
||||
try:
|
||||
# 构建分类页面URL
|
||||
if pg == '1' or pg == 1:
|
||||
url = f"{self.host}/vodtype/{tid}/"
|
||||
else:
|
||||
url = f"{self.host}/vodtype/{tid}/page/{pg}/"
|
||||
|
||||
response = self.fetch(url, headers=self.headers)
|
||||
doc = pq(response.text)
|
||||
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
# 提取视频列表 - 使用.module-item容器
|
||||
video_containers = doc('.module-item')
|
||||
if len(video_containers):
|
||||
# 分类页面使用容器结构
|
||||
for container in video_containers.items():
|
||||
video_info = self.extract_video_info_from_container(container)
|
||||
if video_info:
|
||||
videos.append(video_info)
|
||||
else:
|
||||
# 备用方案:直接查找链接
|
||||
video_links = doc('a[href*="/voddetail/"]')
|
||||
for link in video_links.items():
|
||||
video_info = self.extract_video_info_from_link(link)
|
||||
if video_info:
|
||||
videos.append(video_info)
|
||||
|
||||
result['list'] = videos
|
||||
result['page'] = int(pg)
|
||||
result['pagecount'] = 9999 # 设置较大值,实际翻页时会自动调整
|
||||
result['limit'] = 20
|
||||
result['total'] = 999999
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"categoryContent error: {str(e)}")
|
||||
return {'list': [], 'page': int(pg), 'pagecount': 1, 'limit': 20, 'total': 0}
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""获取视频详情"""
|
||||
try:
|
||||
video_id = ids[0]
|
||||
url = f"{self.host}/voddetail/{video_id}/"
|
||||
|
||||
response = self.fetch(url, headers=self.headers)
|
||||
doc = pq(response.text)
|
||||
|
||||
# 提取视频详细信息
|
||||
vod = {}
|
||||
|
||||
# 基本信息
|
||||
vod['vod_id'] = video_id
|
||||
vod['vod_name'] = doc('h1').text().strip() or doc('.detail-title').text().strip()
|
||||
|
||||
# 图片
|
||||
pic_elem = doc('.detail-pic img, .module-item-pic img').eq(0)
|
||||
vod['vod_pic'] = pic_elem.attr('src') or pic_elem.attr('data-src') or ''
|
||||
if vod['vod_pic'] and not vod['vod_pic'].startswith('http'):
|
||||
vod['vod_pic'] = self.host + vod['vod_pic']
|
||||
|
||||
# 提取详情信息
|
||||
info_items = doc('.detail-info p, .module-info-item')
|
||||
for item in info_items.items():
|
||||
text = item.text().strip()
|
||||
if '导演' in text:
|
||||
vod['vod_director'] = text.replace('导演:', '').replace('导演', '').strip()
|
||||
elif '主演' in text:
|
||||
vod['vod_actor'] = text.replace('主演:', '').replace('主演', '').strip()
|
||||
elif '年份' in text or '上映' in text:
|
||||
year_match = re.search(r'(\d{4})', text)
|
||||
if year_match:
|
||||
vod['vod_year'] = year_match.group(1)
|
||||
elif '地区' in text:
|
||||
vod['vod_area'] = text.replace('地区:', '').replace('地区', '').strip()
|
||||
elif '类型' in text:
|
||||
vod['vod_type'] = text.replace('类型:', '').replace('类型', '').strip()
|
||||
|
||||
# 剧情简介
|
||||
content_elem = doc('.detail-content, .module-info-introduction')
|
||||
vod['vod_content'] = content_elem.text().strip()
|
||||
|
||||
# 备注信息
|
||||
remarks_elem = doc('.detail-remarks, .module-item-note')
|
||||
vod['vod_remarks'] = remarks_elem.text().strip()
|
||||
|
||||
# 提取播放源和播放列表
|
||||
play_sources = []
|
||||
play_urls = []
|
||||
|
||||
# 查找播放源标签
|
||||
source_tabs = doc('.play-source-tab a, .module-tab-item')
|
||||
if not len(source_tabs):
|
||||
# 如果没有找到播放源标签,设置默认播放源
|
||||
play_sources.append('1080P8')
|
||||
else:
|
||||
for tab in source_tabs.items():
|
||||
source_name = tab.text().strip()
|
||||
if source_name:
|
||||
play_sources.append(source_name)
|
||||
|
||||
# 查找播放链接 - 使用实际找到的选择器
|
||||
play_links = doc('a[href*="/vodplay/"]')
|
||||
episodes = []
|
||||
|
||||
for link in play_links.items():
|
||||
ep_name = link.text().strip()
|
||||
ep_url = link.attr('href')
|
||||
|
||||
# 跳过空的或重复的"立即播放"链接
|
||||
if ep_url and ep_name and ep_name != '立即播放':
|
||||
episodes.append(f"{ep_name}${ep_url}")
|
||||
|
||||
# 如果没有找到有效的剧集,但有播放链接,使用第一个
|
||||
if not episodes and len(play_links):
|
||||
first_link = play_links.eq(0)
|
||||
ep_url = first_link.attr('href')
|
||||
if ep_url:
|
||||
episodes.append(f"播放${ep_url}")
|
||||
|
||||
if episodes:
|
||||
play_urls.append('#'.join(episodes))
|
||||
|
||||
vod['vod_play_from'] = '$$$'.join(play_sources) if play_sources else '默认播放源'
|
||||
vod['vod_play_url'] = '$$$'.join(play_urls) if play_urls else ''
|
||||
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"detailContent error: {str(e)}")
|
||||
return {"list": []}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
"""搜索功能"""
|
||||
try:
|
||||
# URL编码关键词
|
||||
encoded_key = urllib.parse.quote(key)
|
||||
url = f"{self.host}/vodsearch/{encoded_key}-------------/"
|
||||
|
||||
response = self.fetch(url, headers=self.headers)
|
||||
doc = pq(response.text)
|
||||
|
||||
videos = []
|
||||
seen_ids = set() # 用于去重
|
||||
|
||||
# 基于实际HTML结构查找搜索结果
|
||||
# 优先查找.module-search-item容器
|
||||
search_containers = doc('.module-search-item')
|
||||
if len(search_containers):
|
||||
for container in search_containers.items():
|
||||
video_info = self.extract_video_info_from_search_container(container)
|
||||
if video_info and video_info['vod_id'] not in seen_ids:
|
||||
videos.append(video_info)
|
||||
seen_ids.add(video_info['vod_id'])
|
||||
else:
|
||||
# 备用方案:直接查找链接并去重
|
||||
video_links = doc('a[href*="/voddetail/"]')
|
||||
for link in video_links.items():
|
||||
video_info = self.extract_video_info_from_link(link)
|
||||
if video_info and video_info['vod_id'] not in seen_ids:
|
||||
videos.append(video_info)
|
||||
seen_ids.add(video_info['vod_id'])
|
||||
|
||||
return {'list': videos, 'page': int(pg)}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"searchContent error: {str(e)}")
|
||||
return {'list': [], 'page': int(pg)}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""获取播放地址"""
|
||||
try:
|
||||
url = f"{self.host}{id}" if id.startswith('/') else f"{self.host}/vodplay/{id}/"
|
||||
|
||||
response = self.fetch(url, headers=self.headers)
|
||||
doc = pq(response.text)
|
||||
|
||||
# 查找播放器配置
|
||||
script_texts = doc('script').text()
|
||||
|
||||
# 尝试提取播放地址
|
||||
play_url = ""
|
||||
|
||||
# 方法1: 查找直接的视频URL
|
||||
url_patterns = [
|
||||
r'"url"\s*:\s*"([^"]+\.m3u8[^"]*)"',
|
||||
r'"url"\s*:\s*"([^"]+\.mp4[^"]*)"',
|
||||
r'player_aaaa\s*=\s*{[^}]*"url"\s*:\s*"([^"]+)"',
|
||||
r'var\s+player\s*=\s*{[^}]*"url"\s*:\s*"([^"]+)"'
|
||||
]
|
||||
|
||||
for pattern in url_patterns:
|
||||
match = re.search(pattern, script_texts)
|
||||
if match:
|
||||
play_url = match.group(1)
|
||||
break
|
||||
|
||||
# 如果没有找到直接URL,尝试查找iframe
|
||||
if not play_url:
|
||||
iframe = doc('iframe').attr('src')
|
||||
if iframe:
|
||||
play_url = iframe
|
||||
|
||||
result = {
|
||||
"parse": 1 if not play_url.endswith(('.m3u8', '.mp4')) else 0,
|
||||
"url": play_url,
|
||||
"header": self.headers
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"playerContent error: {str(e)}")
|
||||
return {"parse": 1, "url": "", "header": {}}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def extract_video_info_from_link(self, link):
|
||||
"""从链接元素提取视频信息"""
|
||||
try:
|
||||
href = link.attr('href')
|
||||
if not href or '/voddetail/' not in href:
|
||||
return None
|
||||
|
||||
# 提取视频ID
|
||||
id_match = re.search(r'/voddetail/(\d+)/', href)
|
||||
if not id_match:
|
||||
return None
|
||||
|
||||
video_id = id_match.group(1)
|
||||
|
||||
# 提取标题 - 优化处理
|
||||
title = link.attr('title') or ''
|
||||
if not title:
|
||||
# 从链接文本中提取,去除多余信息
|
||||
link_text = link.text().strip()
|
||||
# 尝试提取视频标题(通常在第二行或包含中文的部分)
|
||||
lines = [line.strip() for line in link_text.split('\n') if line.strip()]
|
||||
for line in lines:
|
||||
# 跳过分类信息(如"国产剧"、"爱情片"等)
|
||||
if line not in ['国产剧', '爱情片', '动作片', '喜剧片', '剧情片', '科幻片', '恐怖片', '战争片', '国产综艺', '日本动漫', '欧美动漫']:
|
||||
# 如果包含演员信息,只取标题部分
|
||||
if ',' in line and len(line) > 20:
|
||||
# 可能是"标题 演员1, 演员2"的格式
|
||||
parts = line.split(',')
|
||||
if len(parts) > 1:
|
||||
title = parts[0].strip()
|
||||
break
|
||||
else:
|
||||
title = line
|
||||
break
|
||||
|
||||
# 查找相关的图片
|
||||
pic = ''
|
||||
# 尝试在同一父元素中查找图片
|
||||
parent = link.parent()
|
||||
img_elem = parent.find('img').eq(0)
|
||||
if img_elem.length:
|
||||
# 优先使用data-src(真实图片),fallback到src(占位图片)
|
||||
pic = img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
|
||||
# 如果没找到图片,尝试在链接内部查找
|
||||
if not pic:
|
||||
img_elem = link.find('img').eq(0)
|
||||
if img_elem.length:
|
||||
# 优先使用data-src(真实图片),fallback到src(占位图片)
|
||||
pic = img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic
|
||||
|
||||
# 提取备注信息 - 查找相关文本
|
||||
remarks = ''
|
||||
# 尝试从父元素中查找备注
|
||||
parent_text = parent.text()
|
||||
if '更新至' in parent_text:
|
||||
remarks_match = re.search(r'更新至【([^】]+)】', parent_text)
|
||||
if remarks_match:
|
||||
remarks = f"更新至{remarks_match.group(1)}"
|
||||
else:
|
||||
# 尝试其他格式
|
||||
remarks_match = re.search(r'更新至(\d+)', parent_text)
|
||||
if remarks_match:
|
||||
remarks = f"更新至{remarks_match.group(1)}"
|
||||
elif '第' in parent_text and '集' in parent_text:
|
||||
remarks_match = re.search(r'第(\d+)集', parent_text)
|
||||
if remarks_match:
|
||||
remarks = f"第{remarks_match.group(1)}集"
|
||||
|
||||
if not title:
|
||||
return None
|
||||
|
||||
return {
|
||||
'vod_id': video_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': remarks
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"extract_video_info_from_link error: {str(e)}")
|
||||
return None
|
||||
|
||||
def extract_video_info_from_container(self, container):
|
||||
"""从.module-item容器提取视频信息"""
|
||||
try:
|
||||
# 查找容器内的视频链接
|
||||
video_links = container.find('a[href*="/voddetail/"]')
|
||||
if not len(video_links):
|
||||
return None
|
||||
|
||||
# 获取第一个视频链接(通常是主链接)
|
||||
main_link = video_links.eq(0)
|
||||
href = main_link.attr('href')
|
||||
if not href:
|
||||
return None
|
||||
|
||||
# 提取视频ID
|
||||
id_match = re.search(r'/voddetail/(\d+)/', href)
|
||||
if not id_match:
|
||||
return None
|
||||
|
||||
video_id = id_match.group(1)
|
||||
|
||||
# 查找容器内的图片
|
||||
img_elem = container.find('img').eq(0)
|
||||
pic = ''
|
||||
title_from_img = ''
|
||||
|
||||
if img_elem.length:
|
||||
# 优先使用data-src(真实图片),fallback到src(占位图片)
|
||||
pic = img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
title_from_img = img_elem.attr('alt') or ''
|
||||
|
||||
# 确保图片URL是完整的HTTP链接
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic
|
||||
|
||||
# 提取标题 - 优先使用图片alt,然后是链接文本
|
||||
title = title_from_img
|
||||
if not title:
|
||||
# 查找标题链接(通常是h3或.title内的链接)
|
||||
title_links = container.find('h3 a, .title a, .module-item-titlebox a')
|
||||
if len(title_links):
|
||||
title = title_links.eq(0).text().strip()
|
||||
|
||||
if not title:
|
||||
# 使用主链接的文本,但需要清理
|
||||
link_text = main_link.text().strip()
|
||||
lines = [line.strip() for line in link_text.split('\n') if line.strip()]
|
||||
for line in lines:
|
||||
if line not in ['国产剧', '爱情片', '动作片', '喜剧片', '剧情片', '科幻片', '恐怖片', '战争片', '国产综艺', '日本动漫', '欧美动漫']:
|
||||
if ',' in line and len(line) > 20:
|
||||
title = line.split(',')[0].strip()
|
||||
break
|
||||
else:
|
||||
title = line
|
||||
break
|
||||
|
||||
# 提取备注信息
|
||||
remarks = ''
|
||||
remarks_elem = container.find('.module-item-note, .note, .remarks')
|
||||
if len(remarks_elem):
|
||||
remarks = remarks_elem.eq(0).text().strip()
|
||||
|
||||
if not title:
|
||||
return None
|
||||
|
||||
return {
|
||||
'vod_id': video_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': remarks
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"extract_video_info_from_container error: {str(e)}")
|
||||
return None
|
||||
|
||||
def extract_video_info_from_search_container(self, container):
|
||||
"""从搜索结果容器提取视频信息"""
|
||||
try:
|
||||
# 在搜索容器中查找标题链接(避免剧集链接)
|
||||
title_links = container.find('h3 a[href*="/voddetail/"]')
|
||||
if not len(title_links):
|
||||
# 备用:查找所有视频链接,选择文本最长的(通常是标题)
|
||||
all_links = container.find('a[href*="/voddetail/"]')
|
||||
if not len(all_links):
|
||||
return None
|
||||
|
||||
# 选择文本最长的链接作为标题链接
|
||||
best_link = None
|
||||
max_length = 0
|
||||
for link in all_links.items():
|
||||
text = link.text().strip()
|
||||
if len(text) > max_length and '第' not in text and '集' not in text:
|
||||
max_length = len(text)
|
||||
best_link = link
|
||||
|
||||
if not best_link:
|
||||
best_link = all_links.eq(0)
|
||||
|
||||
title_links = best_link
|
||||
else:
|
||||
title_links = title_links.eq(0)
|
||||
|
||||
href = title_links.attr('href')
|
||||
if not href:
|
||||
return None
|
||||
|
||||
# 提取视频ID
|
||||
id_match = re.search(r'/voddetail/(\d+)/', href)
|
||||
if not id_match:
|
||||
return None
|
||||
|
||||
video_id = id_match.group(1)
|
||||
|
||||
# 提取标题
|
||||
title = title_links.text().strip()
|
||||
|
||||
# 查找图片
|
||||
img_elem = container.find('img').eq(0)
|
||||
pic = ''
|
||||
if img_elem.length:
|
||||
# 优先使用data-src(真实图片),fallback到src(占位图片)
|
||||
pic = img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic
|
||||
|
||||
# 提取备注
|
||||
remarks = ''
|
||||
remarks_elem = container.find('.note, .remarks, .video-serial')
|
||||
if len(remarks_elem):
|
||||
remarks = remarks_elem.eq(0).text().strip()
|
||||
|
||||
if not title:
|
||||
return None
|
||||
|
||||
return {
|
||||
'vod_id': video_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': remarks
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"extract_video_info_from_search_container error: {str(e)}")
|
||||
return None
|
||||
|
||||
def extract_video_info(self, item):
|
||||
"""提取视频信息的通用方法(保持兼容性)"""
|
||||
try:
|
||||
# 查找链接
|
||||
link_elem = item.find('a[href*="/voddetail/"]').eq(0)
|
||||
if link_elem.length:
|
||||
return self.extract_video_info_from_link(link_elem)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"extract_video_info error: {str(e)}")
|
||||
return None
|
||||
@@ -0,0 +1,197 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import quote, urlparse
|
||||
from Crypto.Hash import SHA256
|
||||
sys.path.append("..")
|
||||
import json
|
||||
import time
|
||||
from pyquery import PyQuery as pq
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='https://www.knvod.com'
|
||||
|
||||
headers = {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'Origin': host,
|
||||
'Referer': f"{host}/",
|
||||
'Cookie':'X-Robots-Tag=CDN-VERIFY'
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=self.getpq(self.fetch(self.host,headers=self.headers).text)
|
||||
result = {}
|
||||
classes = []
|
||||
for k in data('.head-more.box a').items():
|
||||
i=k.attr('href')
|
||||
if i and '/show' in i:
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': re.findall(r'\d+', i)[0]
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list']=self.getlist(data('.border-box.public-r .public-list-div'))
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data=self.getpq(self.fetch(f"{self.host}/show/{tid}--------{pg}---/",headers=self.headers).text)
|
||||
result = {}
|
||||
result['list'] = self.getlist(data('.border-box.public-r .public-list-div'))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.getpq(self.fetch(f"{self.host}/list/{ids[0]}/", headers=self.headers).text)
|
||||
v=data('.detail-info.lightSpeedIn .slide-info')
|
||||
vod = {
|
||||
'vod_year': v.eq(-1).text().split(':',1)[-1],
|
||||
'vod_remarks': v.eq(0),
|
||||
'vod_actor': v.eq(3).text().split(':',1)[-1],
|
||||
'vod_director': v.eq(2).text().split(':',1)[-1],
|
||||
'vod_content': data('.switch-box #height_limit').text()
|
||||
}
|
||||
np=data('.anthology.wow.fadeInUp')
|
||||
ndata=np('.anthology-tab .swiper-wrapper .swiper-slide')
|
||||
pdata=np('.anthology-list .anthology-list-box ul')
|
||||
play,names=[],[]
|
||||
for i in range(len(ndata)):
|
||||
n=ndata.eq(i)('a')
|
||||
n('span').remove()
|
||||
names.append(n.text())
|
||||
vs=[]
|
||||
for v in pdata.eq(i)('li').items():
|
||||
vs.append(f"{v.text()}${v('a').attr('href')}")
|
||||
play.append('#'.join(vs))
|
||||
vod["vod_play_from"] = "$$$".join(names)
|
||||
vod["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.fetch(f"{self.host}/index.php/ajax/suggest?mid=1&wd={key}&limit=9999×tamp={int(time.time()*1000)}", headers=self.headers).json()
|
||||
videos=[]
|
||||
for i in data['list']:
|
||||
videos.append({
|
||||
'vod_id': i['id'],
|
||||
'vod_name': i['name'],
|
||||
'vod_pic': i['pic']
|
||||
})
|
||||
return {'list':videos,'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
h={
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.8 Mobile/15E148 Safari/604.1',
|
||||
'Origin': self.host
|
||||
}
|
||||
data = self.getpq(self.fetch(f"{self.host}{id}", headers=self.headers).text)
|
||||
try:
|
||||
jstr = data('.player-box .player-left script').eq(1).text()
|
||||
jsdata = json.loads(jstr.split('=',1)[-1])
|
||||
url = jsdata.get('url')
|
||||
if not re.search(r'\.m3u8|\.mp4',jsdata['url']):
|
||||
jxd=self.fetch(f"{self.host}/static/player/{jsdata['from']}.js", headers=self.headers).text
|
||||
jx=re.search(r'http.*?url=', jxd)
|
||||
if not jx:raise Exception('未找到jx')
|
||||
parsed_url = urlparse(jx.group())
|
||||
jxhost = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
title=data('head title').eq(0).text().split('-')[0]
|
||||
next=f"{self.host.split('//')[-1]}{jsdata['link_next']}" if jsdata.get('link_next') else ''
|
||||
cd=self.fetch(f"{jx.group()}{jsdata['url']}&next=//{next}&title={quote(title)}", headers=self.headers).text
|
||||
match = re.search(r'var\s+config\s*=\s*(\{[\s\S]*?\})', cd)
|
||||
if not match:raise Exception('未找到config')
|
||||
cm=re.sub(r',\s*}(?=\s*$)', '}', match.group(1))
|
||||
config=json.loads(cm)
|
||||
config.update({'key':self.sha256(f"{self.gettime()}knvod")})
|
||||
config.pop('next',None)
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.8 Mobile/15E148 Safari/604.1',
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'DNT': '1',
|
||||
'Origin': jxhost,
|
||||
'Pragma': 'no-cache',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
'Sec-Fetch-Storage-Access': 'active',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
}
|
||||
h['Origin']=jxhost
|
||||
jd=self.post(f"{jxhost}/post.php", headers=headers, data=json.dumps(config))
|
||||
data=json.loads(jd.content.decode('utf-8-sig'))
|
||||
url=data.get('knvod')
|
||||
p = 0
|
||||
if not url:raise Exception('未找到播放地址')
|
||||
except Exception as e:
|
||||
print('错误信息:',e)
|
||||
p,url=1,f"{self.host}{id}"
|
||||
return {"parse": p, "url": url, "header": h}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def getlist(self,data):
|
||||
videos=[]
|
||||
for i in data.items():
|
||||
id = i('a').attr('href')
|
||||
if id:
|
||||
id = re.search(r'\d+', id).group(0)
|
||||
img = i('img').attr('data-src')
|
||||
if img and 'url=' in img and 'http' not in img: img = f'{self.host}{img}'
|
||||
videos.append({
|
||||
'vod_id': id,
|
||||
'vod_name': i('a').attr('title'),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': i('.public-prt').text() or i('.public-list-prb').text()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getpq(self, data):
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
|
||||
def gettime(self):
|
||||
current_time = int(time.time())
|
||||
hourly_timestamp = current_time - (current_time % 3600)
|
||||
return hourly_timestamp
|
||||
|
||||
def sha256(self, text):
|
||||
sha = SHA256.new()
|
||||
sha.update(text.encode())
|
||||
return sha.hexdigest()
|
||||
@@ -0,0 +1,380 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import requests
|
||||
import re
|
||||
import json
|
||||
import traceback
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
|
||||
sys.path.append('../../')
|
||||
try:
|
||||
from base.spider import Spider
|
||||
except ImportError:
|
||||
# 定义一个基础接口类,用于本地测试
|
||||
class Spider:
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
self.siteUrl = "https://www.kuaikaw.cn"
|
||||
self.cateManual = {
|
||||
"甜宠": "462",
|
||||
"古装仙侠": "1102",
|
||||
"现代言情": "1145",
|
||||
"青春": "1170",
|
||||
"豪门恩怨": "585",
|
||||
"逆袭": "417-464",
|
||||
"重生": "439-465",
|
||||
"系统": "1159",
|
||||
"总裁": "1147",
|
||||
"职场商战": "943"
|
||||
}
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
|
||||
"Referer": self.siteUrl,
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
|
||||
}
|
||||
|
||||
def getName(self):
|
||||
return "河马短剧"
|
||||
|
||||
def init(self, extend=""):
|
||||
return
|
||||
|
||||
def fetch(self, url, headers=None, retry=2):
|
||||
"""统一的网络请求接口"""
|
||||
if headers is None:
|
||||
headers = self.headers
|
||||
|
||||
for i in range(retry + 1):
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=10, allow_redirects=True)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
except Exception as e:
|
||||
if i == retry:
|
||||
print(f"请求异常: {url}, 错误: {str(e)}")
|
||||
return None
|
||||
continue
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
video_formats = ['.mp4', '.mkv', '.avi', '.wmv', '.m3u8', '.flv', '.rmvb']
|
||||
return any(format in url.lower() for format in video_formats)
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
classes = [{'type_name': k, 'type_id': v} for k, v in self.cateManual.items()]
|
||||
result['class'] = classes
|
||||
|
||||
try:
|
||||
result['list'] = self.homeVideoContent()['list']
|
||||
except:
|
||||
result['list'] = []
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
response = self.fetch(self.siteUrl)
|
||||
if not response:
|
||||
return {'list': []}
|
||||
|
||||
html_content = response.text
|
||||
next_data_pattern = r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>'
|
||||
next_data_match = re.search(next_data_pattern, html_content, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return {'list': []}
|
||||
|
||||
next_data_json = json.loads(next_data_match.group(1))
|
||||
page_props = next_data_json.get("props", {}).get("pageProps", {})
|
||||
|
||||
# 处理轮播图数据
|
||||
if "bannerList" in page_props:
|
||||
for banner in page_props["bannerList"]:
|
||||
if banner.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{banner['bookId']}",
|
||||
"vod_name": banner.get("bookName", ""),
|
||||
"vod_pic": banner.get("coverWap", ""),
|
||||
"vod_remarks": f"{banner.get('statusDesc', '')} {banner.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
# 处理SEO分类推荐
|
||||
if "seoColumnVos" in page_props:
|
||||
for column in page_props["seoColumnVos"]:
|
||||
for book in column.get("bookInfos", []):
|
||||
if book.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{book['bookId']}",
|
||||
"vod_name": book.get("bookName", ""),
|
||||
"vod_pic": book.get("coverWap", ""),
|
||||
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
# 去重处理
|
||||
seen = set()
|
||||
unique_videos = []
|
||||
for video in videos:
|
||||
key = (video["vod_id"], video["vod_name"])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique_videos.append(video)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取首页推荐内容出错: {e}")
|
||||
unique_videos = []
|
||||
|
||||
return {'list': unique_videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'page': pg, 'pagecount': 1, 'limit': 20, 'total': 0}
|
||||
url = f"{self.siteUrl}/browse/{tid}/{pg}"
|
||||
|
||||
response = self.fetch(url)
|
||||
if not response:
|
||||
return result
|
||||
|
||||
html_content = response.text
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html_content, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return result
|
||||
|
||||
try:
|
||||
next_data_json = json.loads(next_data_match.group(1))
|
||||
page_props = next_data_json.get("props", {}).get("pageProps", {})
|
||||
|
||||
current_page = page_props.get("page", 1)
|
||||
total_pages = page_props.get("pages", 1)
|
||||
book_list = page_props.get("bookList", [])
|
||||
|
||||
videos = []
|
||||
for book in book_list:
|
||||
if book.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{book['bookId']}",
|
||||
"vod_name": book.get("bookName", ""),
|
||||
"vod_pic": book.get("coverWap", ""),
|
||||
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
result.update({
|
||||
'list': videos,
|
||||
'page': int(current_page),
|
||||
'pagecount': total_pages,
|
||||
'limit': len(videos),
|
||||
'total': len(videos) * total_pages if videos else 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"分类内容获取出错: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
return self.searchContentPage(key, quick, pg)
|
||||
|
||||
def searchContentPage(self, key, quick, pg=1):
|
||||
result = {'list': [], 'page': pg, 'pagecount': 1, 'limit': 20, 'total': 0}
|
||||
search_url = f"{self.siteUrl}/search?searchValue={quote(key)}&page={pg}"
|
||||
|
||||
response = self.fetch(search_url)
|
||||
if not response:
|
||||
return result
|
||||
|
||||
html_content = response.text
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html_content, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return result
|
||||
|
||||
try:
|
||||
next_data_json = json.loads(next_data_match.group(1))
|
||||
page_props = next_data_json.get("props", {}).get("pageProps", {})
|
||||
|
||||
total_pages = page_props.get("pages", 1)
|
||||
book_list = page_props.get("bookList", [])
|
||||
|
||||
videos = []
|
||||
for book in book_list:
|
||||
if book.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{book['bookId']}",
|
||||
"vod_name": book.get("bookName", ""),
|
||||
"vod_pic": book.get("coverWap", ""),
|
||||
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
result.update({
|
||||
'list': videos,
|
||||
'pagecount': total_pages,
|
||||
'total': len(videos) * total_pages if videos else 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"搜索内容出错: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': []}
|
||||
if not ids:
|
||||
return result
|
||||
|
||||
vod_id = ids[0]
|
||||
if not vod_id.startswith('/drama/'):
|
||||
vod_id = f'/drama/{vod_id}'
|
||||
|
||||
drama_url = f"{self.siteUrl}{vod_id}"
|
||||
response = self.fetch(drama_url)
|
||||
if not response:
|
||||
return result
|
||||
|
||||
html = response.text
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return result
|
||||
|
||||
try:
|
||||
next_data = json.loads(next_data_match.group(1))
|
||||
page_props = next_data.get("props", {}).get("pageProps", {})
|
||||
book_info = page_props.get("bookInfoVo", {})
|
||||
chapter_list = page_props.get("chapterList", [])
|
||||
|
||||
if not book_info.get("bookId"):
|
||||
return result
|
||||
|
||||
# 基本信息
|
||||
categories = [c.get("name", "") for c in book_info.get("categoryList", [])]
|
||||
performers = [p.get("name", "") for p in book_info.get("performerList", [])]
|
||||
|
||||
vod = {
|
||||
"vod_id": vod_id,
|
||||
"vod_name": book_info.get("title", ""),
|
||||
"vod_pic": book_info.get("coverWap", ""),
|
||||
"type_name": ",".join(categories),
|
||||
"vod_year": "",
|
||||
"vod_area": book_info.get("countryName", ""),
|
||||
"vod_remarks": f"{book_info.get('statusDesc', '')} {book_info.get('totalChapterNum', '')}集".strip(),
|
||||
"vod_actor": ", ".join(performers),
|
||||
"vod_director": "",
|
||||
"vod_content": book_info.get("introduction", "")
|
||||
}
|
||||
|
||||
# 处理剧集
|
||||
play_urls = self.processEpisodes(vod_id, chapter_list)
|
||||
if play_urls:
|
||||
vod['vod_play_from'] = '河马剧场'
|
||||
vod['vod_play_url'] = '$$$'.join(play_urls)
|
||||
|
||||
result['list'] = [vod]
|
||||
|
||||
except Exception as e:
|
||||
print(f"详情页解析出错: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
return result
|
||||
|
||||
def processEpisodes(self, vod_id, chapter_list):
|
||||
play_urls = []
|
||||
episodes = []
|
||||
|
||||
for chapter in chapter_list:
|
||||
chapter_id = chapter.get("chapterId", "")
|
||||
chapter_name = chapter.get("chapterName", "")
|
||||
|
||||
if not chapter_id or not chapter_name:
|
||||
continue
|
||||
|
||||
# 尝试获取直接视频链接
|
||||
video_url = self.getDirectVideoUrl(chapter)
|
||||
if video_url:
|
||||
episodes.append(f"{chapter_name}${video_url}")
|
||||
continue
|
||||
|
||||
# 回退方案
|
||||
episodes.append(f"{chapter_name}${vod_id}${chapter_id}${chapter_name}")
|
||||
|
||||
if episodes:
|
||||
play_urls.append("#".join(episodes))
|
||||
|
||||
return play_urls
|
||||
|
||||
def getDirectVideoUrl(self, chapter):
|
||||
if "chapterVideoVo" not in chapter or not chapter["chapterVideoVo"]:
|
||||
return None
|
||||
|
||||
video_info = chapter["chapterVideoVo"]
|
||||
for key in ["mp4", "mp4720p", "vodMp4Url"]:
|
||||
if key in video_info and video_info[key] and ".mp4" in video_info[key].lower():
|
||||
return video_info[key]
|
||||
return None
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {
|
||||
"parse": 0,
|
||||
"url": id,
|
||||
"header": json.dumps(self.headers)
|
||||
}
|
||||
|
||||
# 如果已经是视频链接直接返回
|
||||
if 'http' in id and ('.mp4' in id or '.m3u8' in id):
|
||||
return result
|
||||
|
||||
# 解析参数
|
||||
parts = id.split('$')
|
||||
if len(parts) < 2:
|
||||
return result
|
||||
|
||||
drama_id = parts[0].replace('/drama/', '')
|
||||
chapter_id = parts[1]
|
||||
|
||||
# 尝试获取视频链接
|
||||
video_url = self.getEpisodeVideoUrl(drama_id, chapter_id)
|
||||
if video_url:
|
||||
result["url"] = video_url
|
||||
|
||||
return result
|
||||
|
||||
def getEpisodeVideoUrl(self, drama_id, chapter_id):
|
||||
episode_url = f"{self.siteUrl}/episode/{drama_id}/{chapter_id}"
|
||||
response = self.fetch(episode_url)
|
||||
if not response:
|
||||
return None
|
||||
|
||||
html = response.text
|
||||
|
||||
# 方法1: 从NEXT_DATA提取
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__".*?>(.*?)</script>', html, re.DOTALL)
|
||||
if next_data_match:
|
||||
try:
|
||||
next_data = json.loads(next_data_match.group(1))
|
||||
page_props = next_data.get("props", {}).get("pageProps", {})
|
||||
chapter_info = page_props.get("chapterInfo", {})
|
||||
|
||||
if chapter_info and "chapterVideoVo" in chapter_info:
|
||||
video_info = chapter_info["chapterVideoVo"]
|
||||
for key in ["mp4", "mp4720p", "vodMp4Url"]:
|
||||
if key in video_info and video_info[key] and ".mp4" in video_info[key].lower():
|
||||
return video_info[key]
|
||||
except:
|
||||
pass
|
||||
|
||||
# 方法2: 直接从HTML提取
|
||||
mp4_matches = re.findall(r'(https?://[^"\']+\.mp4)', html)
|
||||
if mp4_matches:
|
||||
for url in mp4_matches:
|
||||
if chapter_id in url or drama_id in url:
|
||||
return url
|
||||
return mp4_matches[0]
|
||||
|
||||
return None
|
||||
|
||||
def localProxy(self, param):
|
||||
return [200, "video/MP2T", {}, param]
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
@@ -0,0 +1,436 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 泥视频 - https://www.nivod.vip/
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
from urllib.parse import quote, unquote
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "泥视频"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = 'https://www.nivod.vip'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'DNT': '1',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""获取首页内容和分类"""
|
||||
try:
|
||||
response = self.fetch_with_encoding(self.host, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
result = {}
|
||||
classes = []
|
||||
|
||||
# 获取分类导航
|
||||
nav_items = doc('.navbar a')
|
||||
for item in nav_items.items():
|
||||
text = item.text().strip()
|
||||
href = item.attr('href')
|
||||
if text and href and href != '/' and '/t/' in href:
|
||||
# 提取分类ID
|
||||
type_id = href.split('/t/')[-1].rstrip('/')
|
||||
if type_id.isdigit():
|
||||
classes.append({
|
||||
'type_name': text,
|
||||
'type_id': type_id
|
||||
})
|
||||
|
||||
# 获取首页视频列表
|
||||
videos = []
|
||||
video_items = doc('.module-item')
|
||||
for item in video_items.items():
|
||||
try:
|
||||
title = item.attr('title') or ''
|
||||
href = item.attr('href') or ''
|
||||
|
||||
if title and href:
|
||||
# 提取视频ID
|
||||
vod_id = href.split('/nivod/')[-1].rstrip('/')
|
||||
|
||||
# 获取图片 - 优先获取data-original(真实图片),避免懒加载占位图
|
||||
img_elem = item.find('img')
|
||||
pic = ''
|
||||
if img_elem:
|
||||
# 优先获取data-original(真实图片URL),然后是data-src,最后是src
|
||||
pic = img_elem.attr('data-original') or img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic if pic.startswith('/') else ''
|
||||
|
||||
# 获取备注信息
|
||||
note_elem = item.find('.module-item-note')
|
||||
remarks = note_elem.text() if note_elem else ''
|
||||
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_year': '',
|
||||
'vod_remarks': remarks
|
||||
})
|
||||
except Exception as e:
|
||||
self.log(f"解析视频项时出错: {e}")
|
||||
continue
|
||||
|
||||
result['class'] = classes
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取首页内容时出错: {e}")
|
||||
return {'class': [], 'list': []}
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""获取推荐视频"""
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""获取分类内容"""
|
||||
try:
|
||||
# 构建分类URL
|
||||
url = f"{self.host}/t/{tid}/"
|
||||
if int(pg) > 1:
|
||||
url = f"{self.host}/t/{tid}/page/{pg}/"
|
||||
|
||||
response = self.fetch_with_encoding(url, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 获取视频列表
|
||||
videos = []
|
||||
video_items = doc('.module-item')
|
||||
for item in video_items.items():
|
||||
try:
|
||||
title = item.attr('title') or ''
|
||||
href = item.attr('href') or ''
|
||||
|
||||
if title and href:
|
||||
# 提取视频ID
|
||||
vod_id = href.split('/nivod/')[-1].rstrip('/')
|
||||
|
||||
# 获取图片 - 优先获取data-original(真实图片),避免懒加载占位图
|
||||
img_elem = item.find('img')
|
||||
pic = ''
|
||||
if img_elem:
|
||||
# 优先获取data-original(真实图片URL),然后是data-src,最后是src
|
||||
pic = img_elem.attr('data-original') or img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic if pic.startswith('/') else ''
|
||||
|
||||
# 获取备注信息
|
||||
note_elem = item.find('.module-item-note')
|
||||
remarks = note_elem.text() if note_elem else ''
|
||||
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_year': '',
|
||||
'vod_remarks': remarks
|
||||
})
|
||||
except Exception as e:
|
||||
self.log(f"解析分类视频项时出错: {e}")
|
||||
continue
|
||||
|
||||
result = {
|
||||
'list': videos,
|
||||
'page': pg,
|
||||
'pagecount': 9999, # 设置一个较大的值
|
||||
'limit': 80,
|
||||
'total': 999999
|
||||
}
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取分类内容时出错: {e}")
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 80, 'total': 0}
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""获取视频详情"""
|
||||
try:
|
||||
vod_id = ids[0]
|
||||
url = f"{self.host}/nivod/{vod_id}/"
|
||||
|
||||
response = self.fetch_with_encoding(url, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 获取标题
|
||||
title_elem = doc('h1')
|
||||
title = self.fix_encoding(title_elem.text()) if title_elem else ''
|
||||
|
||||
# 获取视频信息
|
||||
info_elem = doc('.module-info')
|
||||
content = self.fix_encoding(info_elem.text()) if info_elem else ''
|
||||
|
||||
# 获取播放源和播放列表
|
||||
play_from = []
|
||||
play_url = []
|
||||
|
||||
# 获取播放源标签
|
||||
tab_items = doc('.module-tab-item')
|
||||
play_lists = doc('.module-play-list')
|
||||
|
||||
for i, tab in enumerate(tab_items.items()):
|
||||
# 分别提取播放源名称和集数
|
||||
span_elem = tab.find('span')
|
||||
small_elem = tab.find('small')
|
||||
|
||||
source_name = ''
|
||||
if span_elem:
|
||||
source_name = self.fix_encoding(span_elem.text().strip())
|
||||
# 如果有集数信息,添加到播放源名称后
|
||||
if small_elem:
|
||||
episode_count = self.fix_encoding(small_elem.text().strip())
|
||||
source_name = f"{source_name}{episode_count}"
|
||||
else:
|
||||
# 如果没有span元素,使用整个文本
|
||||
source_name = self.fix_encoding(tab.text().strip())
|
||||
|
||||
if source_name:
|
||||
play_from.append(source_name)
|
||||
|
||||
# 获取对应的播放列表
|
||||
episodes = []
|
||||
if i < len(play_lists):
|
||||
episode_items = play_lists.eq(i).find('a')
|
||||
for ep in episode_items.items():
|
||||
ep_title = self.fix_encoding(ep.text().strip())
|
||||
ep_href = ep.attr('href')
|
||||
if ep_title and ep_href:
|
||||
episodes.append(f"{ep_title}${ep_href}")
|
||||
|
||||
play_url.append('#'.join(episodes))
|
||||
|
||||
vod = {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': '',
|
||||
'vod_year': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': '',
|
||||
'vod_director': '',
|
||||
'vod_content': content,
|
||||
'vod_play_from': '$$$'.join(play_from),
|
||||
'vod_play_url': '$$$'.join(play_url)
|
||||
}
|
||||
|
||||
return {'list': [vod]}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取视频详情时出错: {e}")
|
||||
return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
"""搜索内容"""
|
||||
try:
|
||||
# 使用正确的搜索URL格式
|
||||
search_url = f"{self.host}/s/-------------/"
|
||||
params = {'wd': key}
|
||||
|
||||
response = self.fetch_with_encoding(search_url, params=params, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 获取搜索结果
|
||||
videos = []
|
||||
video_items = doc('.module-item')
|
||||
for item in video_items.items():
|
||||
try:
|
||||
# 搜索页面的结构不同,需要从内部链接获取信息
|
||||
# 查找详情链接(通常是第一个或标题链接)
|
||||
detail_links = item.find('a[href*="/nivod/"]')
|
||||
if not detail_links:
|
||||
continue
|
||||
|
||||
# 获取第一个详情链接
|
||||
detail_link = detail_links.eq(0)
|
||||
href = detail_link.attr('href')
|
||||
|
||||
if not href:
|
||||
continue
|
||||
|
||||
# 提取视频ID
|
||||
vod_id = href.split('/nivod/')[-1].rstrip('/')
|
||||
|
||||
# 获取标题 - 尝试多种方式
|
||||
title = ''
|
||||
# 方法1: 从链接的strong标签获取
|
||||
strong_elem = detail_link.find('strong')
|
||||
if strong_elem:
|
||||
title = self.fix_encoding(strong_elem.text().strip())
|
||||
|
||||
# 方法2: 从图片的alt属性获取
|
||||
if not title:
|
||||
img_elem = item.find('img')
|
||||
if img_elem:
|
||||
title = self.fix_encoding(img_elem.attr('alt') or '')
|
||||
|
||||
# 方法3: 从链接文本获取
|
||||
if not title:
|
||||
title = self.fix_encoding(detail_link.text().strip())
|
||||
|
||||
if not title:
|
||||
continue
|
||||
|
||||
# 获取图片 - 优先获取data-original(真实图片),避免懒加载占位图
|
||||
img_elem = item.find('img')
|
||||
pic = ''
|
||||
if img_elem:
|
||||
# 优先获取data-original(真实图片URL),然后是data-src,最后是src
|
||||
pic = img_elem.attr('data-original') or img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic if pic.startswith('/') else ''
|
||||
|
||||
# 获取备注信息
|
||||
note_elem = item.find('.module-item-note')
|
||||
remarks = self.fix_encoding(note_elem.text()) if note_elem else ''
|
||||
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_year': '',
|
||||
'vod_remarks': remarks
|
||||
})
|
||||
except Exception as e:
|
||||
self.log(f"解析搜索结果时出错: {e}")
|
||||
continue
|
||||
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"搜索时出错: {e}")
|
||||
return {'list': [], 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""获取播放地址"""
|
||||
try:
|
||||
# 播放页面URL
|
||||
play_url = f"{self.host}{id}"
|
||||
|
||||
response = self.fetch_with_encoding(play_url, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 查找播放器配置
|
||||
scripts = doc('script')
|
||||
for script in scripts.items():
|
||||
script_text = script.text()
|
||||
if 'player' in script_text and ('url' in script_text):
|
||||
# 尝试提取播放地址
|
||||
url_match = re.search(r'"url"\s*:\s*"([^"]+)"', script_text)
|
||||
if url_match:
|
||||
video_url = url_match.group(1)
|
||||
return {
|
||||
'parse': 0,
|
||||
'url': video_url,
|
||||
'header': self.headers
|
||||
}
|
||||
|
||||
# 如果没有找到直接播放地址,返回播放页面让系统解析
|
||||
return {
|
||||
'parse': 1,
|
||||
'url': play_url,
|
||||
'header': self.headers
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取播放地址时出错: {e}")
|
||||
return {
|
||||
'parse': 1,
|
||||
'url': f"{self.host}{id}",
|
||||
'header': self.headers
|
||||
}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def fix_encoding(self, text):
|
||||
"""修复UTF-8编码问题"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
try:
|
||||
# 检查是否包含乱码特征(常见的UTF-8乱码模式)
|
||||
garbled_patterns = [
|
||||
'\u00e4\u00b8', '\u00e5', '\u00e6', '\u00e7', '\u00e8', '\u00e9', # 常见乱码前缀
|
||||
'\u00c3\u00a4', '\u00c3\u00a5', '\u00c3\u00a6', # UTF-8被误解为Latin1
|
||||
'\u00ef\u00bc', '\u00e2\u0080' # 标点符号乱码
|
||||
]
|
||||
|
||||
has_garbled = any(pattern in text for pattern in garbled_patterns)
|
||||
|
||||
if has_garbled:
|
||||
self.log("检测到编码问题,尝试修复...")
|
||||
|
||||
# 方法1: 尝试Latin1->UTF-8转换
|
||||
try:
|
||||
fixed = text.encode('latin1').decode('utf-8')
|
||||
# 检查是否修复成功(包含中文字符)
|
||||
if re.search(r'[\u4e00-\u9fff]', fixed):
|
||||
self.log("使用Latin1->UTF-8修复成功")
|
||||
return fixed
|
||||
except Exception as e:
|
||||
self.log(f"Latin1->UTF-8修复失败: {e}")
|
||||
|
||||
# 方法2: 尝试其他编码转换
|
||||
encodings = ['cp1252', 'iso-8859-1']
|
||||
for encoding in encodings:
|
||||
try:
|
||||
fixed = text.encode(encoding).decode('utf-8')
|
||||
if re.search(r'[\u4e00-\u9fff]', fixed):
|
||||
self.log(f"使用{encoding}->UTF-8修复成功")
|
||||
return fixed
|
||||
except:
|
||||
continue
|
||||
|
||||
self.log("编码修复失败,返回原文本")
|
||||
|
||||
return text
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"编码修复异常: {e}")
|
||||
return text
|
||||
|
||||
def fetch_with_encoding(self, url, **kwargs):
|
||||
"""带编码处理的请求方法"""
|
||||
try:
|
||||
response = self.fetch(url, **kwargs)
|
||||
# 确保使用UTF-8编码
|
||||
response.encoding = 'utf-8'
|
||||
return response
|
||||
except Exception as e:
|
||||
self.log(f"请求失败: {e}")
|
||||
raise
|
||||
|
||||
def getpq(self, text):
|
||||
"""安全的pyquery解析"""
|
||||
try:
|
||||
return pq(text)
|
||||
except Exception as e:
|
||||
self.log(f"pyquery解析出错: {e}")
|
||||
try:
|
||||
return pq(text.encode('utf-8'))
|
||||
except:
|
||||
return pq('')
|
||||
@@ -0,0 +1,563 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 爱壹帆 - https://www.iyf.lv/
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
from urllib.parse import quote, unquote
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "爱壹帆"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = 'https://www.iyf.lv'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'DNT': '1',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""获取首页内容和分类"""
|
||||
try:
|
||||
response = self.fetch_with_encoding(self.host, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
result = {}
|
||||
classes = []
|
||||
|
||||
# 获取分类导航 - 基于浏览器分析,分类链接包含/t/
|
||||
nav_items = doc('a[href*="/t/"]')
|
||||
for item in nav_items.items():
|
||||
text = item.text().strip()
|
||||
href = item.attr('href')
|
||||
if text and href and '/t/' in href:
|
||||
# 提取分类ID
|
||||
type_id = href.split('/t/')[-1].rstrip('/')
|
||||
if type_id.isdigit():
|
||||
classes.append({
|
||||
'type_name': text,
|
||||
'type_id': type_id
|
||||
})
|
||||
|
||||
# 获取首页视频列表 - 优先查找包含图片的视频链接
|
||||
videos = []
|
||||
seen_ids = set() # 用于去重
|
||||
|
||||
# 优先查找包含图片的视频链接(主要的视频项)
|
||||
video_links = doc('a[href*="/iyftv/"]').filter(lambda _, e: pq(e).find('img').length > 0)
|
||||
|
||||
for link in video_links.items():
|
||||
try:
|
||||
href = link.attr('href') or ''
|
||||
if not href:
|
||||
continue
|
||||
|
||||
# 提取视频ID
|
||||
vod_id = href.split('/iyftv/')[-1].rstrip('/')
|
||||
if not vod_id or vod_id in seen_ids:
|
||||
continue
|
||||
|
||||
seen_ids.add(vod_id) # 添加到已见集合
|
||||
|
||||
# 获取标题 - 优先从图片alt属性获取(最准确)
|
||||
title = ''
|
||||
img_elem = link.find('img')
|
||||
if img_elem:
|
||||
title = img_elem.attr('alt') or ''
|
||||
|
||||
# 如果图片alt为空,尝试其他方式
|
||||
if not title:
|
||||
title = link.attr('title') or ''
|
||||
if not title:
|
||||
# 从链接文本获取,但要过滤掉无关文本
|
||||
link_text = link.text().strip()
|
||||
if link_text and link_text not in ['正片', '详情', '播放', '观看']:
|
||||
title = link_text
|
||||
|
||||
if not title:
|
||||
continue
|
||||
|
||||
# 获取图片 - 优先获取data-original(真实图片),避免懒加载占位图
|
||||
pic = ''
|
||||
if img_elem:
|
||||
pic = img_elem.attr('data-original') or img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic if pic.startswith('/') else ''
|
||||
|
||||
# 获取备注信息 - 查找可能的备注元素
|
||||
remarks = ''
|
||||
# 查找父容器中的备注信息
|
||||
parent = link.parent()
|
||||
if parent:
|
||||
# 查找集数信息
|
||||
episode_elem = parent.find('.episode, .status, .note')
|
||||
if episode_elem:
|
||||
remarks = episode_elem.text().strip()
|
||||
else:
|
||||
# 查找包含"第"、"集"、"期"等关键字的文本
|
||||
parent_text = parent.text()
|
||||
import re
|
||||
episode_match = re.search(r'第\d+[集期]|更新至|完结|正片', parent_text)
|
||||
if episode_match:
|
||||
remarks = episode_match.group()
|
||||
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': self.fix_encoding(title),
|
||||
'vod_pic': pic,
|
||||
'vod_year': '',
|
||||
'vod_remarks': self.fix_encoding(remarks)
|
||||
})
|
||||
except Exception as e:
|
||||
self.log(f"解析视频项时出错: {e}")
|
||||
continue
|
||||
|
||||
result['class'] = classes
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取首页内容时出错: {e}")
|
||||
return {'class': [], 'list': []}
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""获取推荐视频"""
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""获取分类内容"""
|
||||
try:
|
||||
# 构建分类URL
|
||||
url = f"{self.host}/t/{tid}/"
|
||||
if int(pg) > 1:
|
||||
url = f"{self.host}/t/{tid}/page/{pg}/"
|
||||
|
||||
response = self.fetch_with_encoding(url, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 获取视频列表 - 优先查找包含图片的视频链接
|
||||
videos = []
|
||||
seen_ids = set() # 用于去重
|
||||
|
||||
# 优先查找包含图片的视频链接(主要的视频项)
|
||||
video_links = doc('a[href*="/iyftv/"]').filter(lambda _, e: pq(e).find('img').length > 0)
|
||||
|
||||
for link in video_links.items():
|
||||
try:
|
||||
href = link.attr('href') or ''
|
||||
if not href:
|
||||
continue
|
||||
|
||||
# 提取视频ID
|
||||
vod_id = href.split('/iyftv/')[-1].rstrip('/')
|
||||
if not vod_id or vod_id in seen_ids:
|
||||
continue
|
||||
|
||||
seen_ids.add(vod_id) # 添加到已见集合
|
||||
|
||||
# 获取标题 - 优先从图片alt属性获取(最准确)
|
||||
title = ''
|
||||
img_elem = link.find('img')
|
||||
if img_elem:
|
||||
title = img_elem.attr('alt') or ''
|
||||
|
||||
# 如果图片alt为空,尝试其他方式
|
||||
if not title:
|
||||
title = link.attr('title') or ''
|
||||
if not title:
|
||||
# 从链接文本获取,但要过滤掉无关文本
|
||||
link_text = link.text().strip()
|
||||
if link_text and link_text not in ['正片', '详情', '播放', '观看']:
|
||||
title = link_text
|
||||
|
||||
if not title:
|
||||
continue
|
||||
|
||||
# 获取图片 - 优先获取data-original(真实图片),避免懒加载占位图
|
||||
pic = ''
|
||||
if img_elem:
|
||||
pic = img_elem.attr('data-original') or img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic if pic.startswith('/') else ''
|
||||
|
||||
# 获取备注信息 - 查找集数或状态信息
|
||||
remarks = ''
|
||||
parent = link.parent()
|
||||
if parent:
|
||||
# 查找集数信息
|
||||
episode_elem = parent.find('.episode, .status, .note')
|
||||
if episode_elem:
|
||||
remarks = episode_elem.text().strip()
|
||||
else:
|
||||
# 查找包含"第"、"集"、"期"等关键字的文本
|
||||
parent_text = parent.text()
|
||||
import re
|
||||
episode_match = re.search(r'第\d+[集期]|更新至|完结|正片', parent_text)
|
||||
if episode_match:
|
||||
remarks = episode_match.group()
|
||||
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': self.fix_encoding(title),
|
||||
'vod_pic': pic,
|
||||
'vod_year': '',
|
||||
'vod_remarks': self.fix_encoding(remarks)
|
||||
})
|
||||
except Exception as e:
|
||||
self.log(f"解析分类视频项时出错: {e}")
|
||||
continue
|
||||
|
||||
result = {
|
||||
'list': videos,
|
||||
'page': pg,
|
||||
'pagecount': 9999, # 设置一个较大的值
|
||||
'limit': 80,
|
||||
'total': 999999
|
||||
}
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取分类内容时出错: {e}")
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 80, 'total': 0}
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""获取视频详情"""
|
||||
try:
|
||||
vod_id = ids[0]
|
||||
url = f"{self.host}/iyftv/{vod_id}/"
|
||||
|
||||
response = self.fetch_with_encoding(url, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 获取标题
|
||||
title_elem = doc('h1')
|
||||
title = self.fix_encoding(title_elem.text()) if title_elem else ''
|
||||
|
||||
# 获取视频信息 - 查找可能的简介元素
|
||||
content = ''
|
||||
info_selectors = ['.module-info', '.video-info', '.content', '.description', '.intro']
|
||||
for selector in info_selectors:
|
||||
info_elem = doc(selector)
|
||||
if info_elem:
|
||||
content = self.fix_encoding(info_elem.text())
|
||||
break
|
||||
|
||||
# 获取播放源和播放列表
|
||||
play_from = []
|
||||
play_url = []
|
||||
|
||||
# 查找播放源标签 - 基于浏览器分析,可能是.module-tab-item
|
||||
tab_selectors = ['.module-tab-item', '.tab-item', '.play-source', '.source-tab']
|
||||
playlist_selectors = ['.module-play-list', '.play-list', '.episode-list']
|
||||
|
||||
tabs = None
|
||||
playlists = None
|
||||
|
||||
for selector in tab_selectors:
|
||||
tabs = doc(selector)
|
||||
if tabs:
|
||||
break
|
||||
|
||||
for selector in playlist_selectors:
|
||||
playlists = doc(selector)
|
||||
if playlists:
|
||||
break
|
||||
|
||||
if tabs and playlists:
|
||||
for i, tab in enumerate(tabs.items()):
|
||||
# 获取播放源名称
|
||||
source_name = self.fix_encoding(tab.text().strip())
|
||||
|
||||
if source_name:
|
||||
play_from.append(source_name)
|
||||
|
||||
# 获取对应的播放列表
|
||||
episodes = []
|
||||
if i < len(playlists):
|
||||
episode_items = playlists.eq(i).find('a')
|
||||
for ep in episode_items.items():
|
||||
ep_title = self.fix_encoding(ep.text().strip())
|
||||
ep_href = ep.attr('href')
|
||||
if ep_title and ep_href:
|
||||
episodes.append(f"{ep_title}${ep_href}")
|
||||
|
||||
play_url.append('#'.join(episodes))
|
||||
|
||||
vod = {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': '',
|
||||
'vod_year': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': '',
|
||||
'vod_director': '',
|
||||
'vod_content': content,
|
||||
'vod_play_from': '$$$'.join(play_from),
|
||||
'vod_play_url': '$$$'.join(play_url)
|
||||
}
|
||||
|
||||
return {'list': [vod]}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取视频详情时出错: {e}")
|
||||
return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
"""搜索内容"""
|
||||
try:
|
||||
# 使用正确的搜索URL格式
|
||||
search_url = f"{self.host}/s/-------------/"
|
||||
params = {'wd': key}
|
||||
|
||||
response = self.fetch_with_encoding(search_url, params=params, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 获取搜索结果 - 基于搜索页面的实际结构
|
||||
videos = []
|
||||
seen_ids = set() # 用于去重
|
||||
|
||||
# 搜索页面的结构:每个视频在一个容器中,包含图片链接和标题链接
|
||||
# 优先查找包含图片的视频链接(主要的视频项)
|
||||
video_containers = doc('a[href*="/iyftv/"]').filter(lambda _, e: pq(e).find('img').length > 0)
|
||||
|
||||
for link in video_containers.items():
|
||||
try:
|
||||
href = link.attr('href') or ''
|
||||
if not href:
|
||||
continue
|
||||
|
||||
# 提取视频ID
|
||||
vod_id = href.split('/iyftv/')[-1].rstrip('/')
|
||||
if not vod_id or vod_id in seen_ids:
|
||||
continue
|
||||
|
||||
seen_ids.add(vod_id) # 添加到已见集合
|
||||
|
||||
# 获取标题 - 优先从图片alt属性获取
|
||||
title = ''
|
||||
img_elem = link.find('img')
|
||||
if img_elem:
|
||||
title = img_elem.attr('alt') or ''
|
||||
|
||||
# 如果图片alt为空,查找同级或父级的标题链接
|
||||
if not title:
|
||||
# 查找父容器中的标题链接
|
||||
parent_container = link.parent()
|
||||
if parent_container:
|
||||
title_link = parent_container.find(f'a[href="/iyftv/{vod_id}/"] strong')
|
||||
if title_link:
|
||||
title = title_link.text().strip()
|
||||
else:
|
||||
# 查找其他可能的标题元素
|
||||
title_elem = parent_container.find(f'a[href="/iyftv/{vod_id}/"]').not_(link)
|
||||
if title_elem:
|
||||
title = title_elem.text().strip()
|
||||
|
||||
if not title:
|
||||
continue
|
||||
|
||||
# 获取图片 - 优先获取data-original(真实图片),避免懒加载占位图
|
||||
pic = ''
|
||||
if img_elem:
|
||||
pic = img_elem.attr('data-original') or img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic if pic.startswith('/') else ''
|
||||
|
||||
# 获取备注信息 - 查找集数或状态信息
|
||||
remarks = ''
|
||||
parent_container = link.parent()
|
||||
if parent_container:
|
||||
# 查找集数信息(通常在图片上方的标签中)
|
||||
episode_elem = parent_container.find('.episode, .status, .note')
|
||||
if episode_elem:
|
||||
remarks = episode_elem.text().strip()
|
||||
else:
|
||||
# 查找包含"第"、"集"、"期"等关键字的文本
|
||||
parent_text = parent_container.text()
|
||||
import re
|
||||
episode_match = re.search(r'第\d+[集期]|更新至|完结|正片', parent_text)
|
||||
if episode_match:
|
||||
remarks = episode_match.group()
|
||||
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': self.fix_encoding(title),
|
||||
'vod_pic': pic,
|
||||
'vod_year': '',
|
||||
'vod_remarks': self.fix_encoding(remarks)
|
||||
})
|
||||
except Exception as e:
|
||||
self.log(f"解析搜索结果时出错: {e}")
|
||||
continue
|
||||
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"搜索时出错: {e}")
|
||||
return {'list': [], 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""获取播放地址"""
|
||||
try:
|
||||
# 播放页面URL
|
||||
play_url = f"{self.host}{id}"
|
||||
|
||||
response = self.fetch_with_encoding(play_url, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 查找播放器配置
|
||||
scripts = doc('script')
|
||||
for script in scripts.items():
|
||||
script_text = script.text()
|
||||
if 'player' in script_text and ('url' in script_text):
|
||||
# 尝试提取播放地址
|
||||
url_match = re.search(r'"url"\s*:\s*"([^"]+)"', script_text)
|
||||
if url_match:
|
||||
video_url = url_match.group(1)
|
||||
return {
|
||||
'parse': 0,
|
||||
'url': video_url,
|
||||
'header': self.headers
|
||||
}
|
||||
|
||||
# 如果没有找到直接播放地址,返回播放页面让系统解析
|
||||
return {
|
||||
'parse': 1,
|
||||
'url': play_url,
|
||||
'header': self.headers
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取播放地址时出错: {e}")
|
||||
return {
|
||||
'parse': 1,
|
||||
'url': f"{self.host}{id}",
|
||||
'header': self.headers
|
||||
}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def fix_encoding(self, text):
|
||||
"""修复UTF-8编码问题 - 加强版"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
try:
|
||||
# 扩展的乱码特征检测
|
||||
garbled_patterns = [
|
||||
# 常见的UTF-8乱码模式
|
||||
'\u00e4\u00b8', '\u00e5', '\u00e6', '\u00e7', '\u00e8', '\u00e9',
|
||||
'\u00c3\u00a4', '\u00c3\u00a5', '\u00c3\u00a6', '\u00c3\u00a7',
|
||||
'\u00ef\u00bc', '\u00e2\u0080', '\u00e2\u0084',
|
||||
# 更多乱码模式
|
||||
'\u00c2\u00a0', '\u00c2\u00b7', '\u00c2\u00bb',
|
||||
'\u00e2\u0082', '\u00e2\u0086', '\u00e2\u0088',
|
||||
# 特殊字符乱码
|
||||
'\u00c3\u0097', '\u00c3\u00b7', '\u00c2\u00b1'
|
||||
]
|
||||
|
||||
has_garbled = any(pattern in text for pattern in garbled_patterns)
|
||||
|
||||
# 额外检查:如果文本包含大量非ASCII字符但没有中文,可能是乱码
|
||||
if not has_garbled:
|
||||
non_ascii_count = sum(1 for c in text if ord(c) > 127)
|
||||
chinese_count = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
|
||||
if non_ascii_count > 0 and chinese_count == 0 and non_ascii_count > len(text) * 0.3:
|
||||
has_garbled = True
|
||||
|
||||
if has_garbled:
|
||||
self.log(f"检测到编码问题,尝试修复: {text[:50]}...")
|
||||
|
||||
# 方法1: 尝试Latin1->UTF-8转换
|
||||
try:
|
||||
fixed = text.encode('latin1').decode('utf-8')
|
||||
# 检查是否修复成功(包含中文字符且减少了乱码字符)
|
||||
if re.search(r'[\u4e00-\u9fff]', fixed):
|
||||
self.log("使用Latin1->UTF-8修复成功")
|
||||
return fixed
|
||||
except Exception as e:
|
||||
self.log(f"Latin1->UTF-8修复失败: {e}")
|
||||
|
||||
# 方法2: 尝试其他编码转换
|
||||
encodings = ['cp1252', 'iso-8859-1', 'windows-1252']
|
||||
for encoding in encodings:
|
||||
try:
|
||||
fixed = text.encode(encoding).decode('utf-8')
|
||||
if re.search(r'[\u4e00-\u9fff]', fixed):
|
||||
self.log(f"使用{encoding}->UTF-8修复成功")
|
||||
return fixed
|
||||
except:
|
||||
continue
|
||||
|
||||
# 方法3: 尝试直接处理常见的乱码替换
|
||||
try:
|
||||
# 常见乱码字符替换表
|
||||
replacements = {
|
||||
'\u00e4\u00b8\u00ad': '中',
|
||||
'\u00e6\u0096\u0087': '文',
|
||||
'\u00e5\u00bd\u00b1': '影',
|
||||
'\u00e8\u00a7\u0086': '视',
|
||||
'\u00e9\u00a2\u0091': '频',
|
||||
}
|
||||
|
||||
fixed = text
|
||||
for garbled, correct in replacements.items():
|
||||
fixed = fixed.replace(garbled, correct)
|
||||
|
||||
if fixed != text and re.search(r'[\u4e00-\u9fff]', fixed):
|
||||
self.log("使用字符替换修复成功")
|
||||
return fixed
|
||||
except:
|
||||
pass
|
||||
|
||||
self.log("编码修复失败,返回原文本")
|
||||
|
||||
return text
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"编码修复异常: {e}")
|
||||
return text
|
||||
|
||||
def fetch_with_encoding(self, url, **kwargs):
|
||||
"""带编码处理的请求方法"""
|
||||
try:
|
||||
response = self.fetch(url, **kwargs)
|
||||
# 确保使用UTF-8编码
|
||||
response.encoding = 'utf-8'
|
||||
return response
|
||||
except Exception as e:
|
||||
self.log(f"请求失败: {e}")
|
||||
raise
|
||||
|
||||
def getpq(self, text):
|
||||
"""安全的pyquery解析"""
|
||||
try:
|
||||
return pq(text)
|
||||
except Exception as e:
|
||||
self.log(f"pyquery解析出错: {e}")
|
||||
try:
|
||||
return pq(text.encode('utf-8'))
|
||||
except:
|
||||
return pq('')
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Catvod | 主页</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="description" content="Catvod 提供简洁高效的 Tvbox 接口、GitHub 文件加速服务与美图壁纸服务。" />
|
||||
<meta name="keywords" content="Catvod, TVbox, GitHub 加速, 高清壁纸, 壁纸 API, 文件加速" />
|
||||
<meta name="robots" content="index,follow" />
|
||||
<meta name="theme-color" content="#0F7D00" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<link rel="stylesheet" href="/css/all.min.css" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" href="/icons/icon-192.png" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/all.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/css/main.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<main id="main" role="main">
|
||||
<header>
|
||||
<div class="avatar">
|
||||
<img src="/image/avatar.jpg" alt="Catvod 头像" loading="lazy" />
|
||||
</div>
|
||||
<h1>Catvod.com</h1>
|
||||
<p>简简单单!</p>
|
||||
</header>
|
||||
<footer>
|
||||
<ul class="icons" role="list">
|
||||
<li><a href="https://tvbox.catvod.com/" title="Tvbox接口" aria-label="Tvbox接口" target="_blank" rel="noopener noreferrer"><i class="fas fa-tv"></i></a></li>
|
||||
<li><a href="https://github.catvod.com/" title="GitHub 文件加速" aria-label="GitHub 文件加速" target="_blank" rel="noopener noreferrer"><i class="fab fa-github"></i></a></li>
|
||||
<li><a href="https://imgs.catvod.com/" title="随机精美壁纸" aria-label="随机精美壁纸" target="_blank" rel="noopener noreferrer"><i class="fas fa-image"></i></a></li>
|
||||
<li><a href="https://img.catvod.com/" title="4K随机背景图片" aria-label="4K随机背景图片" target="_blank" rel="noopener noreferrer"><i class="fas fa-photo-video"></i></a></li>
|
||||
<li><a href="https://lives.catvod.com/" title="TXT/M3U 转换工具" aria-label="TXT/M3U 转换工具" target="_blank" rel="noopener noreferrer"><i class="fas fa-cogs"></i></a></li>
|
||||
<li><a href="https://www.catvod.com/jiaoliu" title="联系我们" aria-label="联系我们" target="_blank" rel="noopener noreferrer"><i class="fas fa-comments"></i></a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
<footer id="footer" role="contentinfo">
|
||||
<p id="copyright"></p>
|
||||
</footer>
|
||||
<script src="/js/main.js"></script>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js')
|
||||
.catch(err => console.warn('Service Worker 注册失败:', err));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,151 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
ahost='https://api.cenguigui.cn'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'DNT': '1',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Dest': 'video',
|
||||
'Sec-Fetch-Storage-Access': 'active',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [{'type_id': '推荐榜', 'type_name': '🔥 推荐榜'},
|
||||
{'type_id': '新剧', 'type_name': '🎬 新剧'},
|
||||
{'type_id': '逆袭', 'type_name': '🎬 逆袭'},
|
||||
{'type_id': '霸总', 'type_name': '🎬 霸总'},
|
||||
{'type_id': '现代言情', 'type_name': '🎬 现代言情'},
|
||||
{'type_id': '打脸虐渣', 'type_name': '🎬 打脸虐渣'},
|
||||
{'type_id': '豪门恩怨', 'type_name': '🎬 豪门恩怨'},
|
||||
{'type_id': '神豪', 'type_name': '🎬 神豪'},
|
||||
{'type_id': '马甲', 'type_name': '🎬 马甲'},
|
||||
{'type_id': '都市日常', 'type_name': '🎬 都市日常'},
|
||||
{'type_id': '战神归来', 'type_name': '🎬 战神归来'},
|
||||
{'type_id': '小人物', 'type_name': '🎬 小人物'},
|
||||
{'type_id': '女性成长', 'type_name': '🎬 女性成长'},
|
||||
{'type_id': '大女主', 'type_name': '🎬 大女主'},
|
||||
{'type_id': '穿越', 'type_name': '🎬 穿越'},
|
||||
{'type_id': '都市修仙', 'type_name': '🎬 都市修仙'},
|
||||
{'type_id': '强者回归', 'type_name': '🎬 强者回归'},
|
||||
{'type_id': '亲情', 'type_name': '🎬 亲情'},
|
||||
{'type_id': '古装', 'type_name': '🎬 古装'},
|
||||
{'type_id': '重生', 'type_name': '🎬 重生'},
|
||||
{'type_id': '闪婚', 'type_name': '🎬 闪婚'},
|
||||
{'type_id': '赘婿逆袭', 'type_name': '🎬 赘婿逆袭'},
|
||||
{'type_id': '虐恋', 'type_name': '🎬 虐恋'},
|
||||
{'type_id': '追妻', 'type_name': '🎬 追妻'},
|
||||
{'type_id': '天下无敌', 'type_name': '🎬 天下无敌'},
|
||||
{'type_id': '家庭伦理', 'type_name': '🎬 家庭伦理'},
|
||||
{'type_id': '萌宝', 'type_name': '🎬 萌宝'},
|
||||
{'type_id': '古风权谋', 'type_name': '🎬 古风权谋'},
|
||||
{'type_id': '职场', 'type_name': '🎬 职场'},
|
||||
{'type_id': '奇幻脑洞', 'type_name': '🎬 奇幻脑洞'},
|
||||
{'type_id': '异能', 'type_name': '🎬 异能'},
|
||||
{'type_id': '无敌神医', 'type_name': '🎬 无敌神医'},
|
||||
{'type_id': '古风言情', 'type_name': '🎬 古风言情'},
|
||||
{'type_id': '传承觉醒', 'type_name': '🎬 传承觉醒'},
|
||||
{'type_id': '现言甜宠', 'type_name': '🎬 现言甜宠'},
|
||||
{'type_id': '奇幻爱情', 'type_name': '🎬 奇幻爱情'},
|
||||
{'type_id': '乡村', 'type_name': '🎬 乡村'},
|
||||
{'type_id': '历史古代', 'type_name': '🎬 历史古代'},
|
||||
{'type_id': '王妃', 'type_name': '🎬 王妃'},
|
||||
{'type_id': '高手下山', 'type_name': '🎬 高手下山'},
|
||||
{'type_id': '娱乐圈', 'type_name': '🎬 娱乐圈'},
|
||||
{'type_id': '强强联合', 'type_name': '🎬 强强联合'},
|
||||
{'type_id': '破镜重圆', 'type_name': '🎬 破镜重圆'},
|
||||
{'type_id': '暗恋成真', 'type_name': '🎬 暗恋成真'},
|
||||
{'type_id': '民国', 'type_name': '🎬 民国'},
|
||||
{'type_id': '欢喜冤家', 'type_name': '🎬 欢喜冤家'},
|
||||
{'type_id': '系统', 'type_name': '🎬 系统'},
|
||||
{'type_id': '真假千金', 'type_name': '🎬 真假千金'},
|
||||
{'type_id': '龙王', 'type_name': '🎬 龙王'},
|
||||
{'type_id': '校园', 'type_name': '🎬 校园'},
|
||||
{'type_id': '穿书', 'type_name': '🎬 穿书'},
|
||||
{'type_id': '女帝', 'type_name': '🎬 女帝'},
|
||||
{'type_id': '团宠', 'type_name': '🎬 团宠'},
|
||||
{'type_id': '年代爱情', 'type_name': '🎬 年代爱情'},
|
||||
{'type_id': '玄幻仙侠', 'type_name': '🎬 玄幻仙侠'},
|
||||
{'type_id': '青梅竹马', 'type_name': '🎬 青梅竹马'},
|
||||
{'type_id': '悬疑推理', 'type_name': '🎬 悬疑推理'},
|
||||
{'type_id': '皇后', 'type_name': '🎬 皇后'},
|
||||
{'type_id': '替身', 'type_name': '🎬 替身'},
|
||||
{'type_id': '大叔', 'type_name': '🎬 大叔'},
|
||||
{'type_id': '喜剧', 'type_name': '🎬 喜剧'},
|
||||
{'type_id': '剧情', 'type_name': '🎬 剧情'}]}
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
params = {
|
||||
'classname': tid,
|
||||
'offset': str((int(pg) - 1)),
|
||||
}
|
||||
data = self.fetch(f'{self.ahost}/api/duanju/api.php', params=params, headers=self.headers).json()
|
||||
videos = []
|
||||
for k in data['data']:
|
||||
videos.append({
|
||||
'vod_id': k.get('book_id'),
|
||||
'vod_name': k.get('title'),
|
||||
'vod_pic': k.get('cover'),
|
||||
'vod_year': k.get('score'),
|
||||
'vod_remarks': f"{k.get('sub_title')}|{k.get('episode_cnt')}"
|
||||
})
|
||||
result = {}
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
v=self.fetch(f'{self.ahost}/api/duanju/api.php', params={'book_id': ids[0]}, headers=self.headers).json()
|
||||
vod = {
|
||||
'type_name': v.get('category'),
|
||||
'vod_year': v.get('time'),
|
||||
'vod_remarks': v.get('duration'),
|
||||
'vod_content': v.get('desc'),
|
||||
'vod_play_from': '嗷呜爱看短剧',
|
||||
'vod_play_url': '#'.join([f"{i['title']}${i['video_id']}" for i in v['data']])
|
||||
}
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
return self.categoryContent(key, pg, True, {})
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
data=self.fetch(f'{self.ahost}/api/duanju/api.php', params={'video_id': id}, headers=self.headers).json()
|
||||
return {'parse': 0, 'url': data['data']['url'], 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
@@ -0,0 +1,279 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
# by嗷呜(finally)
|
||||
import sys
|
||||
import os
|
||||
sys.path.append("..")
|
||||
import re
|
||||
import hashlib
|
||||
import hmac
|
||||
import random
|
||||
import string
|
||||
from Crypto.Util.Padding import unpad
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5, AES
|
||||
from base64 import b64encode, b64decode
|
||||
import json
|
||||
import time
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def getName(self):
|
||||
return "电影猎手"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.device = self.device_id()
|
||||
self.host = self.gethost()
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
t = str(int(time.time()))
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
filters = {}
|
||||
classes = []
|
||||
bba = self.url()
|
||||
data = self.fetch(f"{self.host}/api/v1/app/config?pack={bba[0]}&signature={bba[1]}", headers=self.header()).text
|
||||
data1 = self.aes(data)
|
||||
dy = {"class":"类型","area":"地区","lang":"语言","year":"年份","letter":"字母","by":"排序","sort":"排序"}
|
||||
data1['data']['movie_screen']['sort'].pop(0)
|
||||
for item in data1['data']['movie_screen']['sort']:
|
||||
item['n'] = item.pop('name')
|
||||
item['v'] = item.pop('value')
|
||||
for item in data1['data']['movie_screen']['filter']:
|
||||
has_non_empty_field = False
|
||||
classes.append({"type_name": item["name"], "type_id": str(item["id"])})
|
||||
for key in dy:
|
||||
if key in item and item[key]:
|
||||
has_non_empty_field = True
|
||||
break
|
||||
if has_non_empty_field:
|
||||
filters[str(item["id"])] = []
|
||||
filters[str(item["id"])].append(
|
||||
{"key": 'sort', "name": '排序', "value": data1['data']['movie_screen']['sort']})
|
||||
for dkey in item:
|
||||
if dkey in dy and item[dkey]:
|
||||
item[dkey].pop(0)
|
||||
value_array = [
|
||||
{"n": value.strip(), "v": value.strip()}
|
||||
for value in item[dkey]
|
||||
if value.strip() != ""
|
||||
]
|
||||
filters[str(item["id"])].append(
|
||||
{"key": dkey, "name": dy[dkey], "value": value_array}
|
||||
)
|
||||
result["class"] = classes
|
||||
result["filters"] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
bba = self.url()
|
||||
url = f'{self.host}/api/v1/movie/index_recommend?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()
|
||||
videos = []
|
||||
for item in data['data']:
|
||||
if len(item['list']) > 0:
|
||||
for it in item['list']:
|
||||
try:
|
||||
videos.append(self.voides(it))
|
||||
except Exception as e:
|
||||
continue
|
||||
result = {"list": videos}
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {"type_id": tid, "sort": extend.get("sort", "by_default"), "class": extend.get("class", "类型"),
|
||||
"area": extend.get("area", "地区"), "year": extend.get("year", "年份"), "page": str(pg),
|
||||
"pageSize": "21"}
|
||||
result = {}
|
||||
list = []
|
||||
bba = self.url(body)
|
||||
url = f"{self.host}/api/v1/movie/screen/list?pack={bba[0]}&signature={bba[1]}"
|
||||
data = self.fetch(url, headers=self.header()).json()['data']['list']
|
||||
for item in data:
|
||||
list.append(self.voides(item))
|
||||
result["list"] = list
|
||||
result["page"] = pg
|
||||
result["pagecount"] = 9999
|
||||
result["limit"] = 90
|
||||
result["total"] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
body = {"id": ids[0]}
|
||||
bba = self.url(body)
|
||||
url = f'{self.host}/api/v1/movie/detail?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()['data']
|
||||
video = {'vod_name': data.get('name'),'type_name': data.get('type_name'),'vod_year': data.get('year'),'vod_area': data.get('area'),'vod_remarks': data.get('dynami'),'vod_content': data.get('content')}
|
||||
play = []
|
||||
names = []
|
||||
tasks = []
|
||||
for itt in data["play_from"]:
|
||||
name = itt["name"]
|
||||
a = []
|
||||
if len(itt["list"]) > 0:
|
||||
names.append(name)
|
||||
play.append(self.playeach(itt['list']))
|
||||
else:
|
||||
tasks.append({"movie_id": ids[0], "from_code": itt["code"]})
|
||||
names.append(name)
|
||||
if tasks:
|
||||
with ThreadPoolExecutor(max_workers=len(tasks)) as executor:
|
||||
results = executor.map(self.playlist, tasks)
|
||||
for result in results:
|
||||
if result:
|
||||
play.append(result)
|
||||
else:
|
||||
play.append("")
|
||||
video["vod_play_from"] = "$$$".join(names)
|
||||
video["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [video]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
body = {"keyword": key, "sort": "", "type_id": "0", "page": str(pg), "pageSize": "10",
|
||||
"res_type": "by_movie_name"}
|
||||
bba = self.url(body)
|
||||
url = f"{self.host}/api/v1/movie/search?pack={bba[0]}&signature={bba[1]}"
|
||||
data = self.fetch(url, headers=self.header()).json()['data'].get('list')
|
||||
videos = []
|
||||
for it in data:
|
||||
try:
|
||||
videos.append(self.voides(it))
|
||||
except Exception as e:
|
||||
continue
|
||||
result = {"list": videos, "page": pg}
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = id
|
||||
if "m3u8" not in url and "mp4" not in url:
|
||||
try:
|
||||
add = id.split('|||')
|
||||
data = {"from_code": add[0], "play_url": add[1], "episode_id": add[2], "type": "play"}
|
||||
bba = self.url(data)
|
||||
data2 = self.fetch(f"{self.host}/api/v1/movie_addr/parse_url?pack={bba[0]}&signature={bba[1]}",
|
||||
headers=self.header()).json()['data']
|
||||
url = data2.get('play_url') or data2.get('download_url')
|
||||
try:
|
||||
url1 = self.fetch(url, headers=self.header(), allow_redirects=False).headers['Location']
|
||||
if url1 and "http" in url1:
|
||||
url = url1
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
pass
|
||||
if '.jpg' in url or '.jpeg' in url or '.png' in url:
|
||||
url = self.getProxyUrl() + "&url=" + b64encode(url.encode('utf-8')).decode('utf-8') + "&type=m3u8"
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["url"] = url
|
||||
result["header"] = {'user-agent': 'okhttp/4.9.2'}
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
url = b64decode(param["url"]).decode('utf-8')
|
||||
durl = url[:url.rfind('/')]
|
||||
data = self.fetch(url, headers=self.header()).content.decode("utf-8")
|
||||
lines = data.strip().split('\n')
|
||||
for index, string in enumerate(lines):
|
||||
# if 'URI="' in string and 'http' not in string:
|
||||
# lines[index] = index
|
||||
# 暂时预留,貌似用不到
|
||||
if '#EXT' not in string and 'http' not in string:
|
||||
lines[index] = durl + ('' if string.startswith('/') else '/') + string
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def device_id(self):
|
||||
characters = string.ascii_lowercase + string.digits
|
||||
random_string = ''.join(random.choices(characters, k=32))
|
||||
return random_string
|
||||
|
||||
def gethost(self):
|
||||
headers = {
|
||||
'User-Agent': 'okhttp/4.9.2',
|
||||
'Connection': 'Keep-Alive',
|
||||
}
|
||||
response = self.fetch('https://app-site.ecoliving168.com/domain_v5.json', headers=headers).json()
|
||||
url = response['api_service'].replace('/api/', '')
|
||||
return url
|
||||
|
||||
def header(self):
|
||||
headers = {
|
||||
'User-Agent': 'Android',
|
||||
'Accept': 'application/prs.55App.v2+json',
|
||||
'timestamp': self.t,
|
||||
'x-client-setting': '{"pure-mode":1}',
|
||||
'x-client-uuid': '{"device_id":' + self.device + '}, "type":1,"brand":"Redmi", "model":"M2012K10C", "system_version":30, "sdk_version":"3.1.0.7"}',
|
||||
'x-client-version': '3096 '
|
||||
}
|
||||
return headers
|
||||
|
||||
def url(self, id=None):
|
||||
if not id:
|
||||
id = {}
|
||||
id["timestamp"] = self.t
|
||||
public_key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA02F/kPg5A2NX4qZ5JSns+bjhVMCC6JbTiTKpbgNgiXU+Kkorg6Dj76gS68gB8llhbUKCXjIdygnHPrxVHWfzmzisq9P9awmXBkCk74Skglx2LKHa/mNz9ivg6YzQ5pQFUEWS0DfomGBXVtqvBlOXMCRxp69oWaMsnfjnBV+0J7vHbXzUIkqBLdXSNfM9Ag5qdRDrJC3CqB65EJ3ARWVzZTTcXSdMW9i3qzEZPawPNPe5yPYbMZIoXLcrqvEZnRK1oak67/ihf7iwPJqdc+68ZYEmmdqwunOvRdjq89fQMVelmqcRD9RYe08v+xDxG9Co9z7hcXGTsUquMxkh29uNawIDAQAB'
|
||||
encrypted_text = json.dumps(id)
|
||||
public_key = RSA.import_key(b64decode(public_key))
|
||||
cipher = PKCS1_v1_5.new(public_key)
|
||||
encrypted_message = cipher.encrypt(encrypted_text.encode('utf-8'))
|
||||
encrypted_message_base64 = b64encode(encrypted_message).decode('utf-8')
|
||||
result = encrypted_message_base64.replace('+', '-').replace('/', '_').replace('=', '')
|
||||
key = '635a580fcb5dc6e60caa39c31a7bde48'
|
||||
sign = hmac.new(key.encode(), result.encode(), hashlib.md5).hexdigest()
|
||||
return result, sign
|
||||
|
||||
def playlist(self, body):
|
||||
try:
|
||||
bba = self.url(body)
|
||||
url = f'{self.host}/api/v1/movie_addr/list?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()['data']
|
||||
return self.playeach(data)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def playeach(self,data):
|
||||
play_urls = []
|
||||
for it in data:
|
||||
if re.search(r"mp4|m3u8", it["play_url"]):
|
||||
play_urls.append(f"{it['episode_name']}${it['play_url']}")
|
||||
else:
|
||||
play_urls.append(
|
||||
f"{it['episode_name']}${it['from_code']}|||{it['play_url']}|||{it['episode_id']}"
|
||||
)
|
||||
return '#'.join(play_urls)
|
||||
|
||||
def voides(self, item):
|
||||
if item['name'] or item['title']:
|
||||
voide = {
|
||||
"vod_id": item.get('id') or item.get('click'),
|
||||
'vod_name': item.get('name') or item.get('title'),
|
||||
'vod_pic': item.get('cover') or item.get('image'),
|
||||
'vod_year': item.get('year') or item.get('label'),
|
||||
'vod_remarks': item.get('dynamic') or item.get('sub_title')
|
||||
}
|
||||
return voide
|
||||
|
||||
def aes(self, text):
|
||||
text = text.replace('-', '+').replace('_', '/') + '=='
|
||||
key = b"e6d5de5fcc51f53d"
|
||||
iv = b"2f13eef7dfc6c613"
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size).decode("utf-8")
|
||||
return json.loads(pt)
|
||||
@@ -0,0 +1,127 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import re
|
||||
import sys
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='https://www.hongguodj.cc'
|
||||
|
||||
headers = {
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'DNT': '1',
|
||||
'Origin': host,
|
||||
'Pragma': 'no-cache',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
classes = []
|
||||
vlist = []
|
||||
data = pq(self.fetch(self.host, headers=self.headers).text)
|
||||
for i in list(data('.slip li').items())[1:]:
|
||||
classes.append({
|
||||
'type_name': i.text(),
|
||||
'type_id': re.findall(r'\d+', i('a').attr('href'))[0]
|
||||
})
|
||||
for i in data('.wrap .rows').items():
|
||||
vlist.extend(self.getlist(i('li')))
|
||||
result['class'] = classes
|
||||
result['list'] = vlist
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data=pq(self.fetch(f'{self.host}/type/{tid}-{pg}.html', headers=self.headers).text)
|
||||
result = {}
|
||||
result['list'] = self.getlist(data('.list ul li'))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data=pq(self.fetch(f'{self.host}{ids[0]}', headers=self.headers).text)
|
||||
v=data('.info')
|
||||
p=v('p')
|
||||
vod = {
|
||||
'vod_name': v('h1').text(),
|
||||
'type_name': p.eq(2).text(),
|
||||
'vod_year': p.eq(3).text(),
|
||||
'vod_area': p.eq(4).text(),
|
||||
'vod_remarks': v('em').text(),
|
||||
'vod_actor': p.eq(0).text(),
|
||||
'vod_director': p.eq(1).text(),
|
||||
'vod_content': data('#desc .text').text(),
|
||||
'vod_play_from': '',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
names = [i.text() for i in data('.title.slip a').items()]
|
||||
plist=[]
|
||||
for i in data('.play-list ul').items():
|
||||
plist.append('#'.join([f'{j("a").text()}${j("a").attr("href")}' for j in i('li').items()]))
|
||||
vod['vod_play_from'] = '$$$'.join(names)
|
||||
vod['vod_play_url'] = '$$$'.join(plist)
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=pq(self.fetch(f'{self.host}/search/{key}----------{pg}---.html', headers=self.headers).text)
|
||||
return {'list': self.getlist(data('.show.rows li')),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
p=0
|
||||
uid=f'{self.host}{id}'
|
||||
data=pq(self.fetch(uid, headers=self.headers).text)
|
||||
url=data('.video.ratio').attr('data-play')
|
||||
if not url:
|
||||
url = uid
|
||||
p = 1
|
||||
return {'parse': p, 'url': url, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def getlist(self,data):
|
||||
vlist = []
|
||||
for j in data.items():
|
||||
vlist.append({
|
||||
'vod_id': j('a').attr('href'),
|
||||
'vod_name': j('img').attr('alt'),
|
||||
'vod_pic': self.host + j('img').attr('data-src'),
|
||||
'vod_year': j('.bg').text(),
|
||||
'vod_remarks': j('p').text()
|
||||
})
|
||||
return vlist
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import base64
|
||||
import re
|
||||
import sys
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = 'https://www.jdys.art'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'dnt': '1',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'origin': host,
|
||||
'sec-fetch-site': 'cross-site',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'referer': f'{host}/',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'priority': 'u=1, i',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self.getpq(self.fetch(self.host, headers=self.headers).text)
|
||||
result = {}
|
||||
classes = []
|
||||
for k in list(data('.navtop .navlist li').items())[:9]:
|
||||
classes.append({
|
||||
'type_name': k('a').text(),
|
||||
'type_id': k('a').attr('href'),
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list'] = self.getlist(data('.mi_btcon .bt_img ul li'))
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data = self.getpq(self.fetch(f"{tid}{'' if pg == '1' else f'page/{pg}/'}", headers=self.headers).text)
|
||||
result = {}
|
||||
result['list'] = self.getlist(data('.mi_cont .bt_img ul li'))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.getpq(self.fetch(ids[0], headers=self.headers).text)
|
||||
data2 = data('.moviedteail_list li')
|
||||
vod = {
|
||||
'vod_name': data('.dytext h1').text(),
|
||||
'type_name': data2.eq(0).text(),
|
||||
'vod_year': data2.eq(2).text(),
|
||||
'vod_area': data2.eq(1).text(),
|
||||
'vod_remarks': data2.eq(4).text(),
|
||||
'vod_actor': data2.eq(7).text(),
|
||||
'vod_director': data2.eq(5).text(),
|
||||
'vod_content': data('.yp_context').text().strip()
|
||||
}
|
||||
vdata = data('.paly_list_btn a')
|
||||
play = []
|
||||
for i in vdata.items():
|
||||
a = i.text() + "$" + i.attr.href
|
||||
play.append(a)
|
||||
vod["vod_play_from"] = "在线播放"
|
||||
vod["vod_play_url"] = "#".join(play)
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.getpq(self.fetch(f"{self.host}/page/{pg}/?s={key}", headers=self.headers).text)
|
||||
return {'list': self.getlist(data('.mi_cont .bt_img ul li')), 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
data = self.getpq(self.fetch(id, headers=self.headers).text)
|
||||
try:
|
||||
sc = data('.videoplay script').eq(-1).text()
|
||||
strd = re.findall(r'var\s+[^=]*=\s*"([^"]*)";', sc)
|
||||
kdata = re.findall(r'parse\((.*?)\);', sc)
|
||||
jm = self.aes(strd[0], kdata[0].replace('"', ''), kdata[1].replace('"', ''))
|
||||
url = re.search(r'url: "(.*?)"', jm).group(1)
|
||||
p = 0
|
||||
except:
|
||||
p = 1
|
||||
url = id
|
||||
result = {}
|
||||
result["parse"] = p
|
||||
result["url"] = url
|
||||
result["header"] = self.headers
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def getpq(self, text):
|
||||
try:
|
||||
return pq(text)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(text.encode('utf-8'))
|
||||
|
||||
def getlist(self, data):
|
||||
videos = []
|
||||
for i in data.items():
|
||||
videos.append({
|
||||
'vod_id': i('a').attr('href'),
|
||||
'vod_name': i('a img').attr('alt'),
|
||||
'vod_pic': i('a img').attr('src'),
|
||||
'vod_remarks': i('.dycategory').text(),
|
||||
'vod_year': i('.dyplayinfo').text() or i('.rating').text(),
|
||||
})
|
||||
return videos
|
||||
|
||||
def aes(self, word, key, iv):
|
||||
key = key.encode('utf-8')
|
||||
iv = iv.encode('utf-8')
|
||||
encrypted_data = base64.b64decode(word)
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
decrypted_data = cipher.decrypt(encrypted_data)
|
||||
decrypted_data = unpad(decrypted_data, AES.block_size)
|
||||
return decrypted_data.decode('utf-8')
|
||||
@@ -0,0 +1,153 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import concurrent.futures
|
||||
import json
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.ihost=self.imgsite()
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
#host='https://api.ubj83.com'
|
||||
#host='https://4icnx7.qyjzlh.com'
|
||||
#host='https://ij1men.slsw6.com'
|
||||
host='https://ev5356.970xw.com'
|
||||
|
||||
headers={
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 11; M2012K10C Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.141 Mobile Safari/537.36;webank/h5face;webank/1.0;netType:NETWORK_WIFI;appVersion:416;packageName:com.jp3.xg3',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'x-requested-with': 'com.jp3.xg3',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||
}
|
||||
|
||||
def imgsite(self):
|
||||
data=self.fetch(f"{self.host}/api/appAuthConfig",headers=self.headers).json()
|
||||
host=data['data']['imgDomain']
|
||||
return host if host.startswith('http') else f"https://{host}"
|
||||
|
||||
def getfts(self,id):
|
||||
data=self.fetch(f"{self.host}/api/crumb/filterOptions",params={'fcate_pid':id},headers=self.headers).json()
|
||||
fts=[{
|
||||
'key': i['key'],
|
||||
'name':i['key'],
|
||||
'value': [{
|
||||
'n': j['name'],
|
||||
'v': j['id']
|
||||
} for j in i['data']]
|
||||
} for i in data['data']]
|
||||
return id,fts
|
||||
|
||||
def build_cl(self,data,tid=''):
|
||||
videos=[]
|
||||
for i in data:
|
||||
text=json.dumps(i.get('res_categories',[]))
|
||||
videos.append({
|
||||
'vod_id': f"{i.get('id')}@{'67' if json.dumps('短剧') in text and '67' in text else tid}",
|
||||
'vod_name': i.get('title'),
|
||||
'vod_pic': f"{self.ihost}{i.get('path') or i.get('cover_image') or i.get('thumbnail')}",
|
||||
'vod_remarks': i.get('mask'),
|
||||
'vod_year': i.get('score'),
|
||||
})
|
||||
return videos
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cdata=self.fetch(f"{self.host}/api/term/home_fenlei",headers=self.headers).json()
|
||||
hdata=self.fetch(f"{self.host}/api/dyTag/hand_data",params={'category_id':cdata['data'][0]['id']},headers=self.headers).json()
|
||||
classes = []
|
||||
filters = {}
|
||||
for k in cdata['data']:
|
||||
if 'abbr' in k:
|
||||
classes.append({
|
||||
'type_name': k['name'],
|
||||
'type_id': k['id']
|
||||
})
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(classes)) as executor:
|
||||
future_to_aid = {
|
||||
executor.submit(self.getfts, aid['type_id']): aid['type_id']
|
||||
for aid in classes
|
||||
}
|
||||
for future in concurrent.futures.as_completed(future_to_aid):
|
||||
aid = future_to_aid[future]
|
||||
try:
|
||||
aid_id, fts = future.result()
|
||||
filters[aid_id] = fts
|
||||
except Exception as e:
|
||||
print(f"Error processing aid {aid}: {e}")
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
result['list'] = [item for i in hdata['data'].values() for item in self.build_cl(i)]
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
|
||||
params={**{'category_id': tid, 'page': pg}, **extend}
|
||||
path= '/api/crumb/shortList' if tid=='67' else '/api/crumb/list'
|
||||
#data=self.fetch(f"{self.host}{path}",params=params,headers=self.headers).json()
|
||||
data=self.fetch(f"{self.host}{path}"+'&area=0&year=0&type=0&sort=0',params=params,headers=self.headers).json()
|
||||
result = {}
|
||||
result['list'] = self.build_cl(data['data'],tid)
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
ids=ids[0].split('@')
|
||||
path, ikey = ('/api/detail', 'vid') if ids[-1] == '67' else ('/api/video/detailv2', 'id')
|
||||
data=self.fetch(f"{self.host}{path}",params={ikey:ids[0]},headers=self.headers).json()
|
||||
v=data['data']
|
||||
if ids[-1]=='67':
|
||||
pdata=v.get('playlist',[])
|
||||
n,p=[pdata[0].get('source_config_name')],['#'.join([f"{i.get('title')}${i['url']}" for i in pdata])]
|
||||
else:
|
||||
n,p=[],[]
|
||||
for i in v.get('source_list_source',[]):
|
||||
n.append(i.get('name'))
|
||||
p.append('#'.join([f"{j.get('source_name') or j.get('weight')}${j['url']}" for j in i.get('source_list',[])]))
|
||||
|
||||
vod = {
|
||||
'type_name': '/'.join([i.get('name') for i in v.get('types',[])]),
|
||||
'vod_year': v.get('year'),
|
||||
'vod_area': v.get('area'),
|
||||
'vod_remarks': v.get('update_cycle'),
|
||||
'vod_actor': '/'.join([i.get('name') for i in v.get('actors',[])]),
|
||||
'vod_content': v.get('description'),
|
||||
'vod_play_from': '$$$'.join(n),
|
||||
'vod_play_url': '$$$'.join(p)
|
||||
}
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=self.fetch(f"{self.host}/api/v2/search/videoV2",params={'key':key,'page':pg,'pageSize':20},headers=self.headers).json()
|
||||
return {'list':self.build_cl(data['data']),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
return {'parse': 0, 'url': id, 'header': {'User-Agent':self.headers['User-Agent']}}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Catvod | 主页</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="description" content="Catvod 提供简洁高效的 Tvbox 接口、GitHub 文件加速服务与美图壁纸服务。" />
|
||||
<meta name="keywords" content="Catvod, TVbox, GitHub 加速, 高清壁纸, 壁纸 API, 文件加速" />
|
||||
<meta name="robots" content="index,follow" />
|
||||
<meta name="theme-color" content="#0F7D00" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<link rel="stylesheet" href="/css/all.min.css" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" href="/icons/icon-192.png" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/all.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/css/main.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<main id="main" role="main">
|
||||
<header>
|
||||
<div class="avatar">
|
||||
<img src="/image/avatar.jpg" alt="Catvod 头像" loading="lazy" />
|
||||
</div>
|
||||
<h1>Catvod.com</h1>
|
||||
<p>简简单单!</p>
|
||||
</header>
|
||||
<footer>
|
||||
<ul class="icons" role="list">
|
||||
<li><a href="https://tvbox.catvod.com/" title="Tvbox接口" aria-label="Tvbox接口" target="_blank" rel="noopener noreferrer"><i class="fas fa-tv"></i></a></li>
|
||||
<li><a href="https://github.catvod.com/" title="GitHub 文件加速" aria-label="GitHub 文件加速" target="_blank" rel="noopener noreferrer"><i class="fab fa-github"></i></a></li>
|
||||
<li><a href="https://imgs.catvod.com/" title="随机精美壁纸" aria-label="随机精美壁纸" target="_blank" rel="noopener noreferrer"><i class="fas fa-image"></i></a></li>
|
||||
<li><a href="https://img.catvod.com/" title="4K随机背景图片" aria-label="4K随机背景图片" target="_blank" rel="noopener noreferrer"><i class="fas fa-photo-video"></i></a></li>
|
||||
<li><a href="https://lives.catvod.com/" title="TXT/M3U 转换工具" aria-label="TXT/M3U 转换工具" target="_blank" rel="noopener noreferrer"><i class="fas fa-cogs"></i></a></li>
|
||||
<li><a href="https://www.catvod.com/jiaoliu" title="联系我们" aria-label="联系我们" target="_blank" rel="noopener noreferrer"><i class="fas fa-comments"></i></a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
<footer id="footer" role="contentinfo">
|
||||
<p id="copyright"></p>
|
||||
</footer>
|
||||
<script src="/js/main.js"></script>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js')
|
||||
.catch(err => console.warn('Service Worker 注册失败:', err));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,210 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import uuid
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
import time
|
||||
from Crypto.Hash import MD5, SHA1
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
if extend:
|
||||
hosts=json.loads(extend)['site']
|
||||
self.host = self.host_late(hosts)
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
cdata = self.fetch(f"{self.host}/api/mw-movie/anonymous/get/filer/type", headers=self.getheaders()).json()
|
||||
fdata = self.fetch(f"{self.host}/api/mw-movie/anonymous/v1/get/filer/list", headers=self.getheaders()).json()
|
||||
result = {}
|
||||
classes = []
|
||||
filters={}
|
||||
for k in cdata['data']:
|
||||
classes.append({
|
||||
'type_name': k['typeName'],
|
||||
'type_id': str(k['typeId']),
|
||||
})
|
||||
sort_values = [{"n": "最近更新", "v": "2"},{"n": "人气高低", "v": "3"}, {"n": "评分高低", "v": "4"}]
|
||||
for tid, d in fdata['data'].items():
|
||||
current_sort_values = sort_values.copy()
|
||||
if tid == '1':
|
||||
del current_sort_values[0]
|
||||
filters[tid] = [
|
||||
{"key": "type", "name": "类型",
|
||||
"value": [{"n": i["itemText"], "v": i["itemValue"]} for i in d["typeList"]]},
|
||||
|
||||
*([] if not d["plotList"] else [{"key": "v_class", "name": "剧情",
|
||||
"value": [{"n": i["itemText"], "v": i["itemText"]}
|
||||
for i in d["plotList"]]}]),
|
||||
|
||||
{"key": "area", "name": "地区",
|
||||
"value": [{"n": i["itemText"], "v": i["itemText"]} for i in d["districtList"]]},
|
||||
|
||||
{"key": "year", "name": "年份",
|
||||
"value": [{"n": i["itemText"], "v": i["itemText"]} for i in d["yearList"]]},
|
||||
|
||||
{"key": "lang", "name": "语言",
|
||||
"value": [{"n": i["itemText"], "v": i["itemText"]} for i in d["languageList"]]},
|
||||
|
||||
{"key": "sort", "name": "排序", "value": current_sort_values}
|
||||
]
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data1 = self.fetch(f"{self.host}/api/mw-movie/anonymous/v1/home/all/list", headers=self.getheaders()).json()
|
||||
data2=self.fetch(f"{self.host}/api/mw-movie/anonymous/home/hotSearch",headers=self.getheaders()).json()
|
||||
data=[]
|
||||
for i in data1['data'].values():
|
||||
data.extend(i['list'])
|
||||
data.extend(data2['data'])
|
||||
vods=self.getvod(data)
|
||||
return {'list':vods}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
|
||||
params = {
|
||||
"area": extend.get('area', ''),
|
||||
"filterStatus": "1",
|
||||
"lang": extend.get('lang', ''),
|
||||
"pageNum": pg,
|
||||
"pageSize": "30",
|
||||
"sort": extend.get('sort', '1'),
|
||||
"sortBy": "1",
|
||||
"type": extend.get('type', ''),
|
||||
"type1": tid,
|
||||
"v_class": extend.get('v_class', ''),
|
||||
"year": extend.get('year', '')
|
||||
}
|
||||
data = self.fetch(f"{self.host}/api/mw-movie/anonymous/video/list?{self.js(params)}", headers=self.getheaders(params)).json()
|
||||
result = {}
|
||||
result['list'] = self.getvod(data['data']['list'])
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data=self.fetch(f"{self.host}/api/mw-movie/anonymous/video/detail?id={ids[0]}",headers=self.getheaders({'id':ids[0]})).json()
|
||||
vod=self.getvod([data['data']])[0]
|
||||
vod['vod_play_from']='雷蒙影视'
|
||||
vod['vod_play_url'] = '#'.join(
|
||||
f"{i['name'] if len(vod['episodelist']) > 1 else vod['vod_name']}${ids[0]}@@{i['nid']}" for i in
|
||||
vod['episodelist'])
|
||||
vod.pop('episodelist', None)
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
params = {
|
||||
"keyword": key,
|
||||
"pageNum": pg,
|
||||
"pageSize": "8",
|
||||
"sourceCode": "1"
|
||||
}
|
||||
data=self.fetch(f"{self.host}/api/mw-movie/anonymous/video/searchByWord?{self.js(params)}",headers=self.getheaders(params)).json()
|
||||
vods=self.getvod(data['data']['result']['list'])
|
||||
return {'list':vods,'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
self.header = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.61 Chrome/126.0.6478.61 Not/A)Brand/8 Safari/537.36',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'DNT': '1',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'Origin': self.host,
|
||||
'Referer': f'{self.host}/'
|
||||
}
|
||||
ids=id.split('@@')
|
||||
pdata = self.fetch(f"{self.host}/api/mw-movie/anonymous/v2/video/episode/url?clientType=1&id={ids[0]}&nid={ids[1]}",headers=self.getheaders({'clientType':'1','id': ids[0], 'nid': ids[1]})).json()
|
||||
vlist=[]
|
||||
for i in pdata['data']['list']:vlist.extend([i['resolutionName'],i['url']])
|
||||
return {'parse':0,'url':vlist,'header':self.header}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def host_late(self, url_list):
|
||||
if isinstance(url_list, str):
|
||||
urls = [u.strip() for u in url_list.split(',')]
|
||||
else:
|
||||
urls = url_list
|
||||
if len(urls) <= 1:
|
||||
return urls[0] if urls else ''
|
||||
|
||||
results = {}
|
||||
threads = []
|
||||
|
||||
def test_host(url):
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = requests.head(url, timeout=1.0, allow_redirects=False)
|
||||
delay = (time.time() - start_time) * 1000
|
||||
results[url] = delay
|
||||
except Exception as e:
|
||||
results[url] = float('inf')
|
||||
for url in urls:
|
||||
t = threading.Thread(target=test_host, args=(url,))
|
||||
threads.append(t)
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
return min(results.items(), key=lambda x: x[1])[0]
|
||||
|
||||
def md5(self, sign_key):
|
||||
md5_hash = MD5.new()
|
||||
md5_hash.update(sign_key.encode('utf-8'))
|
||||
md5_result = md5_hash.hexdigest()
|
||||
return md5_result
|
||||
|
||||
def js(self, param):
|
||||
return '&'.join(f"{k}={v}" for k, v in param.items())
|
||||
|
||||
def getheaders(self, param=None):
|
||||
if param is None:param = {}
|
||||
t=str(int(time.time()*1000))
|
||||
param['key']='cb808529bae6b6be45ecfab29a4889bc'
|
||||
param['t']=t
|
||||
sha1_hash = SHA1.new()
|
||||
sha1_hash.update(self.md5(self.js(param)).encode('utf-8'))
|
||||
sign = sha1_hash.hexdigest()
|
||||
deviceid = str(uuid.uuid4())
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.61 Chrome/126.0.6478.61 Not/A)Brand/8 Safari/537.36',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'sign': sign,
|
||||
't': t,
|
||||
'deviceid':deviceid
|
||||
}
|
||||
return headers
|
||||
|
||||
def convert_field_name(self, field):
|
||||
field = field.lower()
|
||||
if field.startswith('vod') and len(field) > 3:
|
||||
field = field.replace('vod', 'vod_')
|
||||
if field.startswith('type') and len(field) > 4:
|
||||
field = field.replace('type', 'type_')
|
||||
return field
|
||||
|
||||
def getvod(self, array):
|
||||
return [{self.convert_field_name(k): v for k, v in item.items()} for item in array]
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Catvod | 主页</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="description" content="Catvod 提供简洁高效的 Tvbox 接口、GitHub 文件加速服务与美图壁纸服务。" />
|
||||
<meta name="keywords" content="Catvod, TVbox, GitHub 加速, 高清壁纸, 壁纸 API, 文件加速" />
|
||||
<meta name="robots" content="index,follow" />
|
||||
<meta name="theme-color" content="#0F7D00" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<link rel="stylesheet" href="/css/all.min.css" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" href="/icons/icon-192.png" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/all.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/css/main.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<main id="main" role="main">
|
||||
<header>
|
||||
<div class="avatar">
|
||||
<img src="/image/avatar.jpg" alt="Catvod 头像" loading="lazy" />
|
||||
</div>
|
||||
<h1>Catvod.com</h1>
|
||||
<p>简简单单!</p>
|
||||
</header>
|
||||
<footer>
|
||||
<ul class="icons" role="list">
|
||||
<li><a href="https://tvbox.catvod.com/" title="Tvbox接口" aria-label="Tvbox接口" target="_blank" rel="noopener noreferrer"><i class="fas fa-tv"></i></a></li>
|
||||
<li><a href="https://github.catvod.com/" title="GitHub 文件加速" aria-label="GitHub 文件加速" target="_blank" rel="noopener noreferrer"><i class="fab fa-github"></i></a></li>
|
||||
<li><a href="https://imgs.catvod.com/" title="随机精美壁纸" aria-label="随机精美壁纸" target="_blank" rel="noopener noreferrer"><i class="fas fa-image"></i></a></li>
|
||||
<li><a href="https://img.catvod.com/" title="4K随机背景图片" aria-label="4K随机背景图片" target="_blank" rel="noopener noreferrer"><i class="fas fa-photo-video"></i></a></li>
|
||||
<li><a href="https://lives.catvod.com/" title="TXT/M3U 转换工具" aria-label="TXT/M3U 转换工具" target="_blank" rel="noopener noreferrer"><i class="fas fa-cogs"></i></a></li>
|
||||
<li><a href="https://www.catvod.com/jiaoliu" title="联系我们" aria-label="联系我们" target="_blank" rel="noopener noreferrer"><i class="fas fa-comments"></i></a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
<footer id="footer" role="contentinfo">
|
||||
<p id="copyright"></p>
|
||||
</footer>
|
||||
<script src="/js/main.js"></script>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js')
|
||||
.catch(err => console.warn('Service Worker 注册失败:', err));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
@header({
|
||||
searchable: 1,
|
||||
filterable: 1,
|
||||
quickSearch: 1,
|
||||
title: '飞快',
|
||||
lang: 'hipy'
|
||||
})
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import base64
|
||||
import datetime
|
||||
from urllib.parse import quote_plus, unquote
|
||||
from lxml import etree
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "飞快"
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"电影": "1",
|
||||
"剧集": "2",
|
||||
"综艺": "3",
|
||||
"动漫": "4"
|
||||
}
|
||||
classes = [{'type_name': k, 'type_id': v} for k, v in cateManual.items()]
|
||||
result['class'] = classes
|
||||
|
||||
return result
|
||||
|
||||
def _parse_video_item(self, a_element):
|
||||
"""解析单个视频项的公共方法"""
|
||||
try:
|
||||
href = a_element.xpath('./@href')[0] if a_element.xpath('./@href') else ''
|
||||
m = re.search(r'/voddetail/(\d+)\.html', href)
|
||||
if not m:
|
||||
return None
|
||||
sid = m.group(1)
|
||||
|
||||
title_nodes = (a_element.xpath('.//div[contains(@class, "module-poster-item-title")]//text()') or
|
||||
a_element.xpath('.//div[contains(@class, "module-card-item-title")]/a//text()') or
|
||||
a_element.xpath('./@title') or a_element.xpath('.//img/@alt'))
|
||||
name = title_nodes[0].strip() if title_nodes else f"视频_{sid}"
|
||||
|
||||
img = self._parse_image_url(a_element)
|
||||
|
||||
remark_nodes = a_element.xpath('.//div[contains(@class, "module-item-note")]//text()')
|
||||
remark = ''.join([x.strip() for x in remark_nodes if x.strip()]) if remark_nodes else ""
|
||||
|
||||
return {
|
||||
"vod_id": sid,
|
||||
"vod_name": name,
|
||||
"vod_pic": img,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _parse_image_url(self, element):
|
||||
"""解析图片URL的公共方法"""
|
||||
img_nodes = (element.xpath('.//img[contains(@class, "lazy")]/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img[contains(@class, "lazy")]/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]'))
|
||||
|
||||
if img_nodes:
|
||||
img = img_nodes[0]
|
||||
if img.startswith('/'):
|
||||
img = 'https://feikuai.tv' + img
|
||||
return img
|
||||
return ''
|
||||
|
||||
def homeVideoContent(self):
|
||||
recommend_list = []
|
||||
try:
|
||||
url = "https://feikuai.tv/"
|
||||
rsp = self.fetch(url, headers=self.header)
|
||||
if not rsp or not rsp.text:
|
||||
return {'list': recommend_list}
|
||||
|
||||
root = self._parse_html(rsp)
|
||||
if not root:
|
||||
return {'list': recommend_list}
|
||||
|
||||
recommend_links = root.xpath(
|
||||
'//div[contains(@class, "module-focus")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-hot")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-recommend")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]'
|
||||
)
|
||||
|
||||
seen = set()
|
||||
for a in recommend_links:
|
||||
video_item = self._parse_video_item(a)
|
||||
if video_item and video_item["vod_id"] not in seen:
|
||||
seen.add(video_item["vod_id"])
|
||||
recommend_list.append(video_item)
|
||||
|
||||
recommend_list = recommend_list[:30]
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': recommend_list}
|
||||
|
||||
def _parse_html(self, rsp):
|
||||
try:
|
||||
parser = etree.HTMLParser(encoding='utf-8', recover=True, remove_blank_text=True)
|
||||
if hasattr(rsp, 'content'):
|
||||
return etree.HTML(rsp.content, parser=parser)
|
||||
return etree.HTML(rsp.text.encode('utf-8', errors='ignore'), parser=parser)
|
||||
except Exception:
|
||||
|
||||
return None
|
||||
|
||||
def _build_vodshow_url(self, tid, pg, ext):
|
||||
area = (ext.get('area') or ext.get('1') or '').strip()
|
||||
cate = (ext.get('class') or ext.get('3') or '').strip()
|
||||
year = (ext.get('year') or ext.get('11') or '').strip()
|
||||
|
||||
enc_area = quote_plus(area) if area else ''
|
||||
enc_cate = quote_plus(cate) if cate else ''
|
||||
pg_str = '' if str(pg) in ('', '1') else str(pg)
|
||||
return f'https://feikuai.tv/vodshow/{tid}-{enc_area}--{enc_cate}-----{pg_str}---{year}.html'
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if tid == '0':
|
||||
return self.homeVideoContent()
|
||||
|
||||
result = {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
|
||||
try:
|
||||
ext = json.loads(extend) if extend and isinstance(extend, str) else {}
|
||||
url = self._build_vodshow_url(tid, pg, ext)
|
||||
except Exception:
|
||||
url = f'https://feikuai.tv/vodshow/{tid}-----------.html'
|
||||
|
||||
headers = self.header.copy()
|
||||
|
||||
headers['Referer'] = f'https://feikuai.tv/vodtype/{tid}.html'
|
||||
|
||||
rsp = self.fetch(url, headers=headers)
|
||||
if not rsp or not rsp.text:
|
||||
return result
|
||||
|
||||
root = self._parse_html(rsp)
|
||||
videos = []
|
||||
seen = set()
|
||||
try:
|
||||
links = root.xpath('//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
root.xpath('//a[contains(@class, "module-poster-item") and contains(@href, "/voddetail/")]') or \
|
||||
root.xpath('//div[contains(@class, "module-card-items")]//a[contains(@href, "/voddetail/")]')
|
||||
|
||||
for a in links:
|
||||
video_item = self._parse_video_item(a)
|
||||
if video_item and video_item["vod_id"] not in seen:
|
||||
seen.add(video_item["vod_id"])
|
||||
videos.append(video_item)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def _decode_url_field(self, raw, encrypt):
|
||||
try:
|
||||
if not raw:
|
||||
return ''
|
||||
enc = str(encrypt or '0').strip()
|
||||
if enc == '1':
|
||||
txt = unquote(raw)
|
||||
elif enc == '2':
|
||||
try:
|
||||
b = base64.b64decode(raw + '===')
|
||||
txt = unquote(b.decode('utf-8', errors='ignore'))
|
||||
except Exception:
|
||||
txt = unquote(raw)
|
||||
else:
|
||||
txt = raw
|
||||
|
||||
txt = re.sub(r'%u([0-9a-fA-F]{4})',
|
||||
lambda m: chr(int(m.group(1), 16)), txt)
|
||||
return txt
|
||||
except Exception:
|
||||
return raw
|
||||
|
||||
def detailContent(self, ids):
|
||||
if not ids:
|
||||
return {'list': []}
|
||||
tid = str(ids[0]).strip()
|
||||
url = f'https://feikuai.tv/voddetail/{tid}.html'
|
||||
try:
|
||||
rsp = self.fetch(url, headers=self.header)
|
||||
if not rsp or not rsp.text:
|
||||
return {'list': []}
|
||||
root = self._parse_html(rsp)
|
||||
if not root:
|
||||
return {'list': []}
|
||||
except Exception:
|
||||
return {'list': []}
|
||||
|
||||
title = root.xpath('//h1/text() | //div[contains(@class, "module-info-heading")]//h1/text()')
|
||||
title = title[0].strip() if title else ''
|
||||
pic = ''
|
||||
try:
|
||||
pnodes = root.xpath('//div[contains(@class, "module-info-poster")]//img/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]')
|
||||
if not pnodes:
|
||||
pnodes = root.xpath('//div[contains(@class, "module-info-poster")]//img/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]')
|
||||
if not pnodes:
|
||||
pnodes = root.xpath('//img[contains(@class, "lazy")]/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]')
|
||||
if not pnodes:
|
||||
pnodes = root.xpath('//img[contains(@class, "lazy")]/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]')
|
||||
if pnodes:
|
||||
pic = pnodes[0]
|
||||
if pic.startswith('/'):
|
||||
pic = 'https://feikuai.tv' + pic
|
||||
except Exception:
|
||||
pic = ''
|
||||
|
||||
detail = ''
|
||||
try:
|
||||
dnodes = root.xpath('//div[contains(@class, "module-info-introduction-content")]//text()')
|
||||
detail = '\n'.join([x.strip() for x in dnodes if x.strip()]) if dnodes else ''
|
||||
except Exception:
|
||||
detail = ''
|
||||
|
||||
vod = {
|
||||
"vod_id": tid,
|
||||
"vod_name": title,
|
||||
"vod_pic": pic,
|
||||
"type_name": "",
|
||||
"vod_year": "",
|
||||
"vod_area": "",
|
||||
"vod_remarks": "",
|
||||
"vod_actor": "",
|
||||
"vod_director": "",
|
||||
"vod_content": detail
|
||||
}
|
||||
|
||||
playFrom = []
|
||||
playList = []
|
||||
try:
|
||||
ep_links = root.xpath('//div[contains(@class, "module-play-list")]//a[contains(@href, "/vodplay/")] | //ul[contains(@class, "module-play-list")]//a[contains(@href, "/vodplay/")]')
|
||||
groups = {}
|
||||
for a in ep_links:
|
||||
try:
|
||||
href = a.xpath('./@href')[0] if a.xpath('./@href') else ''
|
||||
m = re.search(r'/vodplay/(\d+)-(\d+)-(\d+)\.html', href)
|
||||
if not m:
|
||||
continue
|
||||
vid, sid, epid = m.groups()
|
||||
if vid != tid:
|
||||
continue
|
||||
name = ''.join(a.xpath('string(.)')).strip()
|
||||
if not name or name in ('立即播放', '收藏', '追更', '分享', '报错', '下载'):
|
||||
continue
|
||||
if sid not in groups:
|
||||
groups[sid] = []
|
||||
groups[sid].append(f"{name}${vid}-{sid}-{epid}")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
ordered_sids = []
|
||||
for blk in root.xpath('//div[contains(@class, "his-tab-list")]'):
|
||||
try:
|
||||
first = blk.xpath('.//a[contains(@href, "/vodplay/")][1]/@href')
|
||||
if first:
|
||||
mm = re.search(r'/vodplay/(\d+)-(\d+)-(\d+)\.html', first[0])
|
||||
if mm:
|
||||
sid = mm.group(2)
|
||||
if sid not in ordered_sids:
|
||||
ordered_sids.append(sid)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
labels = [x.strip() for x in root.xpath('//div[contains(@class, "module-tab-items-box")]//div[contains(@class, "module-tab-item")]//span/text()') if x.strip()]
|
||||
|
||||
for idx, sid in enumerate(ordered_sids):
|
||||
sname = labels[idx] if idx < len(labels) else f"线路{sid}"
|
||||
playFrom.append(sname)
|
||||
playList.append('#'.join(groups.get(sid, [])))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
vod['vod_play_from'] = '$$$'.join(playFrom) if playFrom else ""
|
||||
vod['vod_play_url'] = '$$$'.join(playList)
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
videos = []
|
||||
try:
|
||||
pg = str(pg)
|
||||
if pg in ('', '1'):
|
||||
url = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
headers = self.header.copy()
|
||||
headers['Referer'] = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
else:
|
||||
url = f'https://feikuai.tv/label/search_ajax.html?wd={quote_plus(key)}&by=time&order=desc&page={pg}'
|
||||
headers = self.header.copy()
|
||||
headers['X-Requested-With'] = 'XMLHttpRequest'
|
||||
headers['Accept'] = '*/*'
|
||||
headers['Referer'] = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
|
||||
rsp = self.fetch(url, headers=headers)
|
||||
if not rsp or not rsp.text:
|
||||
return {'list': []}
|
||||
|
||||
root = self._parse_html(rsp)
|
||||
items = root.xpath('//div[@id="resultList"]//a[contains(@href, "/voddetail/")]') or \
|
||||
root.xpath('//div[contains(@class, "module-card-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
root.xpath('//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
root.xpath('//a[contains(@href, "/voddetail/")]')
|
||||
|
||||
seen = set()
|
||||
for a in items:
|
||||
video_item = self._parse_video_item(a)
|
||||
if video_item and video_item["vod_id"] not in seen:
|
||||
seen.add(video_item["vod_id"])
|
||||
videos.append(video_item)
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': videos}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
play_url = f'https://feikuai.tv/vodplay/{id}.html'
|
||||
vurl = play_url
|
||||
try:
|
||||
rsp = self.fetch(play_url, headers=self.header, timeout=45)
|
||||
if not rsp or not rsp.text:
|
||||
return {"parse": 0, "url": vurl, "header": self.header}
|
||||
pattern = r'(?:var\s+)?player_[a-zA-Z0-9_]+\s*=\s*(\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})(?=\s*</script>)'
|
||||
m = re.search(pattern, rsp.text, re.S)
|
||||
if m:
|
||||
data = json.loads(m.group(1))
|
||||
vurl = data.get('url') or ''
|
||||
vurl = self._decode_url_field(vurl, str(data.get('encrypt', '0')))
|
||||
if vurl.startswith('//'):
|
||||
vurl = 'https:' + vurl
|
||||
if vurl.endswith('.m3u8'):
|
||||
return {"parse": 1, "url": vurl, "header": self.header}
|
||||
except Exception:
|
||||
pass
|
||||
return {"parse": 0, "url": vurl, "header": self.header}
|
||||
|
||||
def localProxy(self, params):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return url
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return []
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def getProxyUrl(self, local=True):
|
||||
return 'http://127.0.0.1:9978/proxy?do=py'
|
||||
|
||||
header = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"Connection": "keep-alive"
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
"""
|
||||
@header({
|
||||
searchable: 1,
|
||||
filterable: 1,
|
||||
quickSearch: 1,
|
||||
title: '飞快',
|
||||
author: 'EylinSir修复版-网盘推送播放',
|
||||
lang: 'hipy'
|
||||
})
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import base64
|
||||
import datetime
|
||||
from urllib.parse import quote_plus, unquote
|
||||
from lxml import etree
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "飞快"
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
_r1 = {}
|
||||
_c1 = {
|
||||
"电影": "1",
|
||||
"剧集": "2",
|
||||
"综艺": "3",
|
||||
"动漫": "4"
|
||||
}
|
||||
_c2 = [{'type_name': _k1, 'type_id': _v1} for _k1, _v1 in _c1.items()]
|
||||
_r1['class'] = _c2
|
||||
return _r1
|
||||
|
||||
def homeVideoContent(self):
|
||||
_l1 = []
|
||||
try:
|
||||
_u1 = "".join(['h', 't', 't', 'p', 's', ':', '/', '/', 'f', 'e', 'i', 'k', 'u', 'a', 'i', '.', 't', 'v', '/'])
|
||||
_r2 = self.fetch(_u1, headers=self._get_header())
|
||||
if not _r2 or not _r2.text:
|
||||
return {'list': _l1}
|
||||
|
||||
_h1 = self._parse_dom(_r2)
|
||||
if not _h1:
|
||||
return {'list': _l1}
|
||||
|
||||
_x1 = _h1.xpath(
|
||||
'//div[contains(@class, "module-focus")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-hot")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-recommend")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]'
|
||||
)
|
||||
|
||||
_s1 = set()
|
||||
for _a1 in _x1:
|
||||
_v1 = self._parse_item(_a1)
|
||||
if _v1 and _v1["vod_id"] not in _s1:
|
||||
_s1.add(_v1["vod_id"])
|
||||
_l1.append(_v1)
|
||||
|
||||
_l1 = _l1[:30]
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': _l1}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if tid == '0':
|
||||
return self.homeVideoContent()
|
||||
|
||||
_r3 = {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
|
||||
try:
|
||||
_e1 = json.loads(extend) if extend and isinstance(extend, str) else {}
|
||||
_u2 = self._build_url(tid, pg, _e1)
|
||||
except Exception:
|
||||
_u2 = f'{"".join(["h","t","t","p","s",":","/","/","f","e","i","k","u","a","i",".","t","v","/","v","o","d","s","h","o","w","/",tid,"-","-","-","-","-","-","-","-","-",".","h","t","m","l"])}'
|
||||
|
||||
_h2 = self._get_header().copy()
|
||||
_h2['Referer'] = f'https://feikuai.tv/vodtype/{tid}.html'
|
||||
|
||||
_r4 = self.fetch(_u2, headers=_h2)
|
||||
if not _r4 or not _r4.text:
|
||||
return _r3
|
||||
|
||||
_h3 = self._parse_dom(_r4)
|
||||
_v2 = []
|
||||
_s2 = set()
|
||||
try:
|
||||
_l2 = _h3.xpath('//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
_h3.xpath('//a[contains(@class, "module-poster-item") and contains(@href, "/voddetail/")]') or \
|
||||
_h3.xpath('//div[contains(@class, "module-card-items")]//a[contains(@href, "/voddetail/")]')
|
||||
|
||||
for _a2 in _l2:
|
||||
_v3 = self._parse_item(_a2)
|
||||
if _v3 and _v3["vod_id"] not in _s2:
|
||||
_s2.add(_v3["vod_id"])
|
||||
_v2.append(_v3)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_r3['list'] = _v2
|
||||
return _r3
|
||||
|
||||
def detailContent(self, ids):
|
||||
if not ids:
|
||||
return {'list': []}
|
||||
_t1 = str(ids[0]).strip()
|
||||
_u3 = f'https://feikuai.tv/voddetail/{_t1}.html'
|
||||
try:
|
||||
_r5 = self.fetch(_u3, headers=self._get_header())
|
||||
if not _r5 or not _r5.text:
|
||||
return {'list': []}
|
||||
_h4 = self._parse_dom(_r5)
|
||||
if not _h4:
|
||||
return {'list': []}
|
||||
except Exception:
|
||||
return {'list': []}
|
||||
|
||||
_t2 = _h4.xpath('//h1/text() | //div[contains(@class, "module-info-heading")]//h1/text()')
|
||||
_t3 = _t2[0].strip() if _t2 else ''
|
||||
_p1 = self._get_img(_h4)
|
||||
_d1 = self._get_desc(_h4)
|
||||
|
||||
_v4 = {
|
||||
"vod_id": _t1,
|
||||
"vod_name": _t3,
|
||||
"vod_pic": _p1,
|
||||
"type_name": "",
|
||||
"vod_year": "",
|
||||
"vod_area": "",
|
||||
"vod_remarks": "",
|
||||
"vod_actor": "",
|
||||
"vod_director": "",
|
||||
"vod_content": _d1
|
||||
}
|
||||
|
||||
_f1, _l3 = self._get_sources(_h4, _t1)
|
||||
|
||||
if not _f1 or not _l3:
|
||||
_f1 = ['飞快']
|
||||
_l3 = [f'{"".join(["暂","无","播","放","源"])}${"".join(["h","t","t","p","s",":","/","/","f","e","i","k","u","a","i",".","t","v","/","v","o","d","d","e","t","a","i","l","/",_t1,".","h","t","m","l"])}']
|
||||
|
||||
_v4['vod_play_from'] = '$$$'.join(_f1) if _f1 else ""
|
||||
_v4['vod_play_url'] = '$$$'.join(_l3)
|
||||
return {'list': [_v4]}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
_v5 = []
|
||||
try:
|
||||
_p2 = str(pg)
|
||||
if _p2 in ('', '1'):
|
||||
_u4 = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
_h5 = self._get_header().copy()
|
||||
_h5['Referer'] = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
else:
|
||||
_u4 = f'https://feikuai.tv/label/search_ajax.html?wd={quote_plus(key)}&by=time&order=desc&page={_p2}'
|
||||
_h5 = self._get_header().copy()
|
||||
_h5['X-Requested-With'] = 'XMLHttpRequest'
|
||||
_h5['Accept'] = '*/*'
|
||||
_h5['Referer'] = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
|
||||
_r6 = self.fetch(_u4, headers=_h5)
|
||||
if not _r6 or not _r6.text:
|
||||
return {'list': []}
|
||||
|
||||
_h6 = self._parse_dom(_r6)
|
||||
_i1 = _h6.xpath('//div[@id="resultList"]//a[contains(@href, "/voddetail/")]') or \
|
||||
_h6.xpath('//div[contains(@class, "module-card-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
_h6.xpath('//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
_h6.xpath('//a[contains(@href, "/voddetail/")]')
|
||||
|
||||
_s3 = set()
|
||||
for _a3 in _i1:
|
||||
_v6 = self._parse_item(_a3)
|
||||
if _v6 and _v6["vod_id"] not in _s3:
|
||||
_s3.add(_v6["vod_id"])
|
||||
_v5.append(_v6)
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': _v5}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
|
||||
if isinstance(id, str) and id.startswith(''.join(['p','u','s','h',':','/','/'])):
|
||||
return {"parse": 0, "url": id}
|
||||
|
||||
|
||||
_u5 = f'https://feikuai.tv/vodplay/{id}.html'
|
||||
_v7 = _u5
|
||||
try:
|
||||
_r7 = self.fetch(_u5, headers=self._get_header(), timeout=45)
|
||||
if not _r7 or not _r7.text:
|
||||
return {"parse": 0, "url": _v7, "header": self._get_header()}
|
||||
|
||||
_p3 = r'(?:var\s+)?player_[a-zA-Z0-9_]+\s*=\s*(\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})(?=\s*</script>)'
|
||||
_m1 = re.search(_p3, _r7.text, re.S)
|
||||
if _m1:
|
||||
_d2 = json.loads(_m1.group(1))
|
||||
_v7 = _d2.get('url') or ''
|
||||
_v7 = self._decode_str(_v7, str(_d2.get('encrypt', '0')))
|
||||
if _v7.startswith('//'):
|
||||
_v7 = 'https:' + _v7
|
||||
if _v7.endswith('.m3u8'):
|
||||
return {"parse": 1, "url": _v7, "header": self._get_header()}
|
||||
except Exception:
|
||||
pass
|
||||
return {"parse": 0, "url": _v7, "header": self._get_header()}
|
||||
|
||||
def _parse_item(self, a_element):
|
||||
try:
|
||||
_h7 = a_element.xpath('./@href')[0] if a_element.xpath('./@href') else ''
|
||||
_m2 = re.search(r'/voddetail/(\d+)\.html', _h7)
|
||||
if not _m2:
|
||||
return None
|
||||
_s4 = _m2.group(1)
|
||||
|
||||
_t4 = (a_element.xpath('.//div[contains(@class, "module-poster-item-title")]//text()') or
|
||||
a_element.xpath('.//div[contains(@class, "module-card-item-title")]/a//text()') or
|
||||
a_element.xpath('./@title') or a_element.xpath('.//img/@alt'))
|
||||
_n1 = _t4[0].strip() if _t4 else f"视频_{_s4}"
|
||||
|
||||
_i2 = self._get_img(a_element)
|
||||
|
||||
_r8 = a_element.xpath('.//div[contains(@class, "module-item-note")]//text()')
|
||||
_r9 = ''.join([x.strip() for x in _r8 if x.strip()]) if _r8 else ""
|
||||
|
||||
return {
|
||||
"vod_id": _s4,
|
||||
"vod_name": _n1,
|
||||
"vod_pic": _i2,
|
||||
"vod_remarks": _r9
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _get_img(self, element):
|
||||
_i3 = (element.xpath('.//img[contains(@class, "lazy")]/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img[contains(@class, "lazy")]/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]'))
|
||||
|
||||
if _i3:
|
||||
_i4 = _i3[0]
|
||||
if _i4.startswith('/'):
|
||||
_i4 = 'https://feikuai.tv' + _i4
|
||||
return _i4
|
||||
return ''
|
||||
|
||||
def _get_desc(self, root):
|
||||
try:
|
||||
_d3 = root.xpath('//div[contains(@class, "module-info-introduction-content")]//text()')
|
||||
_d4 = '\n'.join([x.strip() for x in _d3 if x.strip()]) if _d3 else ''
|
||||
return _d4
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
def _parse_dom(self, rsp):
|
||||
try:
|
||||
_p4 = etree.HTMLParser(encoding='utf-8', recover=True, remove_blank_text=True)
|
||||
if hasattr(rsp, 'content'):
|
||||
return etree.HTML(rsp.content, parser=_p4)
|
||||
return etree.HTML(rsp.text.encode('utf-8', errors='ignore'), parser=_p4)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _build_url(self, tid, pg, ext):
|
||||
_a1 = (ext.get('area') or ext.get('1') or '').strip()
|
||||
_c3 = (ext.get('class') or ext.get('3') or '').strip()
|
||||
_y1 = (ext.get('year') or ext.get('11') or '').strip()
|
||||
|
||||
_e2 = quote_plus(_a1) if _a1 else ''
|
||||
_e3 = quote_plus(_c3) if _c3 else ''
|
||||
_p5 = '' if str(pg) in ('', '1') else str(pg)
|
||||
return f'https://feikuai.tv/vodshow/{tid}-{_e2}--{_e3}-----{_p5}---{_y1}.html'
|
||||
|
||||
def _get_sources(self, root, tid):
|
||||
_f2 = []
|
||||
_l4 = []
|
||||
|
||||
self._get_normal_sources(root, tid, _f2, _l4)
|
||||
|
||||
self._get_pan_sources(root, tid, _f2, _l4)
|
||||
|
||||
return _f2, _l4
|
||||
|
||||
def _get_normal_sources(self, root, tid, playFrom, playList):
|
||||
try:
|
||||
_e4 = root.xpath('//div[contains(@class, "module-play-list")]//a[contains(@href, "/vodplay/")] | //ul[contains(@class, "module-play-list")]//a[contains(@href, "/vodplay/")]')
|
||||
_g1 = {}
|
||||
for _a4 in _e4:
|
||||
try:
|
||||
_h8 = _a4.xpath('./@href')[0] if _a4.xpath('./@href') else ''
|
||||
_m3 = re.search(r'/vodplay/(\d+)-(\d+)-(\d+)\.html', _h8)
|
||||
if not _m3:
|
||||
continue
|
||||
_v8, _s5, _e5 = _m3.groups()
|
||||
if _v8 != tid:
|
||||
continue
|
||||
_n2 = ''.join(_a4.xpath('string(.)')).strip()
|
||||
if not _n2 or _n2 in ('立即播放', '收藏', '追更', '分享', '报错', '下载'):
|
||||
continue
|
||||
if _s5 not in _g1:
|
||||
_g1[_s5] = []
|
||||
_g1[_s5].append(f"{_n2}${_v8}-{_s5}-{_e5}")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
_o1 = []
|
||||
for _b1 in root.xpath('//div[contains(@class, "his-tab-list")]'):
|
||||
try:
|
||||
_f3 = _b1.xpath('.//a[contains(@href, "/vodplay/")][1]/@href')
|
||||
if _f3:
|
||||
_m4 = re.search(r'/vodplay/(\d+)-(\d+)-(\d+)\.html', _f3[0])
|
||||
if _m4:
|
||||
_s6 = _m4.group(2)
|
||||
if _s6 not in _o1:
|
||||
_o1.append(_s6)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
_l5 = [x.strip() for x in root.xpath('//div[contains(@class, "module-tab-items-box")]//div[contains(@class, "module-tab-item")]//span/text()') if x.strip()]
|
||||
|
||||
for _i5, _s7 in enumerate(_o1):
|
||||
_s8 = _l5[_i5] if _i5 < len(_l5) else f"线路{_s7}"
|
||||
playFrom.append(_s8)
|
||||
playList.append('#'.join(_g1.get(_s7, [])))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _get_pan_sources(self, root, tid, playFrom, playList):
|
||||
try:
|
||||
_d5 = root.xpath('//div[@id="download-list"]')
|
||||
if not _d5:
|
||||
return
|
||||
|
||||
_t5 = root.xpath('//div[@id="y-downList"]//div[contains(@class, "module-tab-item")]')
|
||||
|
||||
_p6 = {
|
||||
'百度网盘': '百度网盘',
|
||||
'夸克网盘': '夸克网盘',
|
||||
'迅雷云盘': '迅雷云盘',
|
||||
'阿里云盘': '阿里云盘',
|
||||
'天翼云盘': '天翼云盘',
|
||||
'UC网盘': 'UC网盘',
|
||||
'115网盘': '115网盘',
|
||||
'移动云盘': '移动云盘'
|
||||
}
|
||||
|
||||
for _t6 in _t5:
|
||||
try:
|
||||
_s9 = ''.join(_t6.xpath('.//span/text()')).strip()
|
||||
if not _s9 or _s9 == '磁力链接':
|
||||
continue
|
||||
|
||||
_s10 = _p6.get(_s9, _s9)
|
||||
|
||||
_t7 = _t6.xpath('./@data-index')
|
||||
if not _t7:
|
||||
continue
|
||||
_t8 = _t7[0]
|
||||
|
||||
_c4 = root.xpath(f'//div[@id="tab-content-{_t8}"]//div[@class="module-row-info"]//a')
|
||||
|
||||
_e6 = []
|
||||
for _i6, _l6 in enumerate(_c4, 1):
|
||||
try:
|
||||
_u6 = _l6.xpath('./@href')
|
||||
if not _u6:
|
||||
continue
|
||||
_u7 = _u6[0].strip()
|
||||
|
||||
_h9 = _l6.xpath('.//h4/text()')
|
||||
if _h9:
|
||||
_e7 = _h9[0].strip()
|
||||
_e7 = re.sub(r'@一键搜片-\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$', '', _e7).strip()
|
||||
else:
|
||||
_e7 = f"资源{_i6}"
|
||||
|
||||
_u8 = self._process_url(_u7)
|
||||
if _u8:
|
||||
_e6.append(f"{_e7}${_u8}")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if _e6:
|
||||
playFrom.append(_s10)
|
||||
playList.append('#'.join(_e6))
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _process_url(self, raw_url):
|
||||
try:
|
||||
if not raw_url:
|
||||
return None
|
||||
|
||||
if not raw_url.startswith(('http://', 'https://')):
|
||||
if raw_url.startswith('//'):
|
||||
_u9 = 'https:' + raw_url
|
||||
elif raw_url.startswith('/'):
|
||||
_u9 = 'https://feikuai.tv' + raw_url
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
_u9 = raw_url
|
||||
|
||||
return f"push://{_u9}"
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _decode_str(self, raw, encrypt):
|
||||
try:
|
||||
if not raw:
|
||||
return ''
|
||||
_e8 = str(encrypt or '0').strip()
|
||||
if _e8 == '1':
|
||||
_t9 = unquote(raw)
|
||||
elif _e8 == '2':
|
||||
try:
|
||||
_b2 = base64.b64decode(raw + '===')
|
||||
_t9 = unquote(_b2.decode('utf-8', errors='ignore'))
|
||||
except Exception:
|
||||
_t9 = unquote(raw)
|
||||
else:
|
||||
_t9 = raw
|
||||
|
||||
_t9 = re.sub(r'%u([0-9a-fA-F]{4})',
|
||||
lambda _m5: chr(int(_m5.group(1), 16)), _t9)
|
||||
return _t9
|
||||
except Exception:
|
||||
return raw
|
||||
|
||||
def _get_header(self):
|
||||
return {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"Connection": "keep-alive"
|
||||
}
|
||||
|
||||
def localProxy(self, params):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return url
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return []
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
@@ -0,0 +1,218 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
import base64
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host=self.gethost()
|
||||
self.headers.update({'referer': f'{self.host}/'})
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
headers = {
|
||||
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="130", "Google Chrome";v="130"',
|
||||
'sec-ch-ua-platform': '"Android"',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=self.getpq()
|
||||
result = {}
|
||||
classes = []
|
||||
filters = {"1": {"name": "类型","key": "tid","value": [{"n": "喜剧","v": 6},{"n": "爱情","v": 7},{"n": "恐怖","v": 8},{"n": "动作","v": 9},{"n": "科幻","v": 10},{"n": "战争","v": 11},{"n": "犯罪","v": 12},{"n": "动画","v": 13},{"n": "奇幻","v": 14},{"n": "剧情","v": 15},{"n": "冒险","v": 16},{"n": "悬疑","v": 17},{"n": "惊悚","v": 18},{"n": "其它","v": 19}]},"2": {"name": "类型","key": "tid","value": [{"n": "大陆剧","v": 20},{"n": "港剧","v": 21},{"n": "韩剧","v": 22},{"n": "美剧","v": 23},{"n": "日剧","v": 24},{"n": "英剧","v": 25},{"n": "台剧","v": 26},{"n": "其它","v": 27}]}}
|
||||
for k in data('.top_bar.clearfix a').items():
|
||||
j = k.attr('href')
|
||||
if j and 'list' in j:
|
||||
id = re.search(r'\d+', j).group(0)
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': id
|
||||
})
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
result['list'] = self.getlist(data('.grid_box ul li'))
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data=self.getpq(f"/list/{extend.get('tid',tid)}-{pg}.html")
|
||||
result = {}
|
||||
result['list'] = self.getlist(data('.grid_box ul li'))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data=self.getpq(ids[0])
|
||||
vod = {
|
||||
'vod_remarks': data('.grid_box.v_info_box p').text(),
|
||||
'vod_content': data('.p_txt.show_part').text().split('\n')[0],
|
||||
}
|
||||
n=list(data('.play_from ul li').items())
|
||||
p=list(data('ul.play_list li').items())
|
||||
ns,ps=[],[]
|
||||
for i,j in enumerate(n):
|
||||
ns.append(j.text())
|
||||
ps.append('#'.join([f"{k.text()}${k.attr('href')}" for k in list(p[i]('a').items())[::-1]]))
|
||||
vod['vod_play_from']='$$$'.join(ns)
|
||||
vod['vod_play_url']='$$$'.join(ps)
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
pass
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
data=self.getpq(id)
|
||||
try:
|
||||
surl=data('section[style*="padding-top"] iframe').eq(0).attr('src')
|
||||
sd=pq(self.fetch(surl,headers=self.headers).text)('body script').html()
|
||||
jdata=self.extract_values(sd)
|
||||
jdata['key']=self.hhh(jdata['key'])
|
||||
parsed_url = urlparse(surl)
|
||||
durl = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
headers = {
|
||||
'accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'cache-control': 'no-cache',
|
||||
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'dnt': '1',
|
||||
'origin': durl,
|
||||
'pragma': 'no-cache',
|
||||
'priority': 'u=1, i',
|
||||
'referer': f'{surl}',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="130", "Google Chrome";v="130"',
|
||||
'sec-ch-ua-mobile': '?1',
|
||||
'sec-ch-ua-platform': '"Android"',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'sec-fetch-storage-access': 'active',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
}
|
||||
jjb=self.post(f"{durl}/api.php",headers=headers,data=jdata).json()
|
||||
url,p=jjb['url'],0
|
||||
except Exception as e:
|
||||
self.log(f"失败: {e}")
|
||||
url,p=f'{self.host}{id}',1
|
||||
phd={
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua-platform': '"Android"',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="130", "Google Chrome";v="130"',
|
||||
'sec-fetch-dest': 'video',
|
||||
'referer': f'{self.host}/',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
}
|
||||
return {'parse': p, 'url': url, 'header': phd}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def gethost(self):
|
||||
data=pq(self.fetch("http://shapp.us",headers=self.headers).text)
|
||||
for i in data('.content-top ul li').items():
|
||||
h=i('a').attr('href')
|
||||
if h:
|
||||
data = self.fetch(h, headers=self.headers, timeout=5)
|
||||
if data.status_code == 200:
|
||||
return h
|
||||
|
||||
def extract_values(self, text):
|
||||
url_match = re.search(r'var url = "([^"]+)"', text)
|
||||
url = url_match.group(1) if url_match else None
|
||||
t_match = re.search(r'var t = "([^"]+)"', text)
|
||||
t = t_match.group(1) if t_match else None
|
||||
key_match = re.search(r'var key = hhh\("([^"]+)"\)', text)
|
||||
key_param = key_match.group(1) if key_match else None
|
||||
act_match = re.search(r'var act = "([^"]+)"', text)
|
||||
act = act_match.group(1) if act_match else None
|
||||
play_match = re.search(r'var play = "([^"]+)"', text)
|
||||
play = play_match.group(1) if play_match else None
|
||||
return {
|
||||
"url": url,
|
||||
"t": t,
|
||||
"key": key_param,
|
||||
"act": act,
|
||||
"play": play
|
||||
}
|
||||
|
||||
def getlist(self,data):
|
||||
videos = []
|
||||
for i in data.items():
|
||||
videos.append({
|
||||
'vod_id': i('a').attr('href'),
|
||||
'vod_name': i('a').attr('title'),
|
||||
'vod_pic': i('a img').attr('data-original'),
|
||||
'vod_remarks': i('.v_note').text()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getpq(self, path=''):
|
||||
data=self.fetch(f"{self.host}{path}",headers=self.headers).text
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
|
||||
def hhh(self, t):
|
||||
ee = {
|
||||
"0Oo0o0O0": "a", "1O0bO001": "b", "2OoCcO2": "c", "3O0dO0O3": "d",
|
||||
"4OoEeO4": "e", "5O0fO0O5": "f", "6OoGgO6": "g", "7O0hO0O7": "h",
|
||||
"8OoIiO8": "i", "9O0jO0O9": "j", "0OoKkO0": "k", "1O0lO0O1": "l",
|
||||
"2OoMmO2": "m", "3O0nO0O3": "n", "4OoOoO4": "o", "5O0pO0O5": "p",
|
||||
"6OoQqO6": "q", "7O0rO0O7": "r", "8OoSsO8": "s", "9O0tO0O9": "t",
|
||||
"0OoUuO0": "u", "1O0vO0O1": "v", "2OoWwO2": "w", "3O0xO0O3": "x",
|
||||
"4OoYyO4": "y", "5O0zO0O5": "z", "0OoAAO0": "A", "1O0BBO1": "B",
|
||||
"2OoCCO2": "C", "3O0DDO3": "D", "4OoEEO4": "E", "5O0FFO5": "F",
|
||||
"6OoGGO6": "G", "7O0HHO7": "H", "8OoIIO8": "I", "9O0JJO9": "J",
|
||||
"0OoKKO0": "K", "1O0LLO1": "L", "2OoMMO2": "M", "3O0NNO3": "N",
|
||||
"4OoOOO4": "O", "5O0PPO5": "P", "6OoQQO6": "Q", "7O0RRO7": "R",
|
||||
"8OoSSO8": "S", "9O0TTO9": "T", "0OoUO0": "U", "1O0VVO1": "V",
|
||||
"2OoWWO2": "W", "3O0XXO3": "X", "4OoYYO4": "Y", "5O0ZZO5": "Z"
|
||||
}
|
||||
n = ""
|
||||
o = base64.b64decode(t).decode('utf-8', errors='replace')
|
||||
i = 0
|
||||
while i < len(o):
|
||||
l = o[i]
|
||||
found = False
|
||||
for key, value in ee.items():
|
||||
if o[i:i + len(key)] == key:
|
||||
l = value
|
||||
i += len(key) - 1
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
pass
|
||||
n += l
|
||||
i += 1
|
||||
return n
|
||||
Reference in New Issue
Block a user