Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-11 04:45:19 +00:00
parent ee2c468427
commit 3ad48c717c
9487 changed files with 7075550 additions and 6121 deletions
+292
View File
@@ -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'
+130
View File
@@ -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'))
+768
View File
@@ -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}'}
+169
View File
@@ -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&timestamp={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'))
+190
View File
@@ -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)
+372
View File
@@ -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
+365
View File
@@ -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')
+109
View File
@@ -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
+315
View File
@@ -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()
+796
View File
@@ -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
+280
View File
@@ -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&timestamp={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 ""
+128
View File
@@ -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
+174
View File
@@ -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&timestamp={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"))
+169
View File
@@ -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&timestamp={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'))
+223
View File
@@ -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 ""
+557
View File
@@ -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
+197
View File
@@ -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&timestamp={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()
+380
View File
@@ -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
+436
View File
@@ -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('')
+563
View File
@@ -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('')
+51
View File
@@ -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>
+151
View File
@@ -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
+279
View File
@@ -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)
+127
View File
@@ -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
+147
View File
@@ -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')
+153
View File
@@ -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
+51
View File
@@ -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>
+210
View File
@@ -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]
+51
View File
@@ -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>
+374
View File
@@ -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"
}
+460
View File
@@ -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
+218
View File
@@ -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