Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-29 16:34:09 +00:00
parent bc4c572d85
commit 5c86516fb2
17 changed files with 8160 additions and 7525 deletions
+388
View File
@@ -0,0 +1,388 @@
# coding=utf-8
"""
目标站: 4kvm 首页: https://www.4kvm.net
动态筛选、精准分集、去重列表
"""
import re
import sys
import json
import urllib.parse
from bs4 import BeautifulSoup
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.site_url = "https://www.4kvm.top"
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',
'Referer': self.site_url,
'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'
}
self.categories = [
{"type_id": "1", "type_name": "电影"},
{"type_id": "2", "type_name": "电视剧"},
{"type_id": "3", "type_name": "动漫"}
]
self._filters_cache = None
# ================= 动态筛选解析 =================
def _fetch_filters_for_classify(self, tid):
"""请求 /filter?classify=tid,解析页面筛选区域,返回该分类的筛选列表"""
url = f"{self.site_url}/filter?classify={tid}"
resp = self.fetch(url, headers=self.headers)
if not resp:
return []
soup = BeautifulSoup(resp.text, 'html.parser')
filter_groups = []
containers = soup.select('main div.flex.flex-wrap.items-center.gap-3')
for container in containers:
links = container.select('a[href]')
if len(links) < 2:
continue
first_text = links[0].get_text(strip=True)
if not first_text.startswith('全部'):
continue
group_name = first_text.replace('全部', '', 1).strip()
# 从非全部的链接中提取参数键
param_key = None
for a in links[1:]:
href = a.get('href', '')
parsed = urllib.parse.urlparse(href)
qs = urllib.parse.parse_qs(parsed.query)
for k in qs:
if k not in ('classify', 'page'):
param_key = k
break
if param_key:
break
if not param_key:
continue
if param_key in ('sort_by', 'order'):
continue
options = []
for a in links:
text = a.get_text(strip=True)
href = a.get('href', '')
parsed = urllib.parse.urlparse(href)
qs = urllib.parse.parse_qs(parsed.query)
val = ''
if param_key in qs:
val = qs[param_key][0] if qs[param_key] else ''
if text.startswith('全部'):
val = ''
options.append({"n": text, "v": val})
if options:
filter_groups.append({
"key": param_key,
"name": group_name,
"value": options
})
return filter_groups
def _get_all_filters(self):
if self._filters_cache is not None:
return self._filters_cache
filters = {}
for cat in self.categories:
tid = cat["type_id"]
groups = self._fetch_filters_for_classify(tid)
if groups:
filters[tid] = groups
# 为没有筛选的分类复用电影分类的筛选
if "1" in filters:
if "3" not in filters:
filters["3"] = filters["1"]
if "4" not in filters:
filters["4"] = filters["1"]
self._filters_cache = filters
return filters
# ================= 核心业务方法 =================
def homeContent(self, filter):
url = self.site_url + "/"
resp = self.fetch(url, headers=self.headers)
video_list = []
if resp:
soup = BeautifulSoup(resp.text, 'html.parser')
# 使用唯一卡片容器
cards = soup.select('div[data-vod-id]')
for card in cards[:20]:
a = card.select_one('a.block[href^="/play/"]')
if not a:
continue
vod_id = card.get('data-vod-id', '').strip()
if not vod_id:
href = a.get('href', '')
vod_id = href.replace('/play/', '').strip()
if not vod_id:
continue
title_tag = card.select_one('h3.text-white') or card.select_one('h3')
vod_name = title_tag.get_text(strip=True) if title_tag else ''
if not vod_name:
continue
img = card.select_one('img[data-src]')
vod_pic = ''
if img:
src = img.get('data-src', '')
if src and not src.startswith('data:'):
vod_pic = src if src.startswith('http') else 'https:' + src
remark_tag = card.select_one('.text-green-500, .text-yellow-400, span[class*="px-1.5"]')
vod_remarks = remark_tag.get_text(strip=True) if remark_tag else ''
video_list.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": vod_remarks
})
return {"class": self.categories, "list": video_list, "filters": self._get_all_filters()}
def homeVideoContent(self):
return self.homeContent(False)
def categoryContent(self, tid, pg, filter, extend):
page = int(pg) if pg else 1
params = {"classify": tid}
if extend:
for k, v in extend.items():
if v and k != 'classify':
params[k] = v
if page > 1:
params['page'] = page
query = urllib.parse.urlencode(params)
url = f"{self.site_url}/filter?{query}"
resp = self.fetch(url, headers=self.headers)
if not resp:
return {"list": [], "page": page, "pagecount": 1, "limit": 24, "total": 0}
soup = BeautifulSoup(resp.text, 'html.parser')
video_list = []
cards = soup.select('div[data-vod-id]')
for card in cards:
a = card.select_one('a.block[href^="/play/"]')
if not a:
continue
vod_id = card.get('data-vod-id', '').strip()
if not vod_id:
href = a.get('href', '')
vod_id = href.replace('/play/', '').strip()
if not vod_id:
continue
title_tag = card.select_one('h3.text-white') or card.select_one('h3')
vod_name = title_tag.get_text(strip=True) if title_tag else ''
if not vod_name:
continue
img = card.select_one('img[data-src]')
vod_pic = ''
if img:
src = img.get('data-src', '')
if src and not src.startswith('data:'):
vod_pic = src if src.startswith('http') else 'https:' + src
remark_tag = card.select_one('.text-green-500, .text-yellow-400, span[class*="px-1.5"]')
vod_remarks = remark_tag.get_text(strip=True) if remark_tag else ''
video_list.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": vod_remarks
})
# 分页处理
pagecount = page
page_text = soup.find(string=re.compile(r'\s*\d+\s*页'))
if page_text:
nums = re.findall(r'\d+', page_text)
if nums:
pagecount = int(nums[-1])
else:
page_block = soup.select_one('.flex.justify-center')
if page_block:
page_links = page_block.select('a[href*="page="]')
for a in page_links:
text = a.get_text(strip=True)
if text.isdigit():
pagecount = max(pagecount, int(text))
return {
"list": video_list,
"page": page,
"pagecount": pagecount,
"limit": 24,
"total": len(video_list) * pagecount
}
def detailContent(self, ids):
if not ids:
return {"list": []}
vod_id = ids[0]
url = f"{self.site_url}/play/{vod_id}"
resp = self.fetch(url, headers=self.headers)
if not resp or resp.status_code != 200:
return {"list": []}
soup = BeautifulSoup(resp.text, 'html.parser')
# 标题
title_elem = soup.select_one('h1.text-xl') or soup.select_one('h1') or soup.select_one('h2')
vod_name = title_elem.get_text(strip=True) if title_elem else vod_id
# 图片
vod_pic = ''
img_elem = soup.select_one('img.w-full') or soup.select_one('img[src]')
if img_elem:
src = img_elem.get('src', '') or img_elem.get('data-src', '')
if src and not src.startswith('data:'):
vod_pic = src if src.startswith('http') else 'https:' + src
# 导演、主演、简介
vod_director = ''
vod_actor = ''
vod_content = ''
info_block = soup.select_one('.rounded-lg div.grid') or soup.select_one('div.grid')
if info_block:
text = info_block.get_text(' ', strip=True)
dir_match = re.search(r'导演\s*([^主\n]+)', text)
if dir_match:
vod_director = dir_match.group(1).strip()
act_match = re.search(r'主演\s*([^剧\n]+)', text)
if act_match:
vod_actor = act_match.group(1).strip()
desc_match = re.search(r'剧情简介\s*(.+)', text, re.DOTALL)
if desc_match:
vod_content = desc_match.group(1).strip()
elif re.search(r'简介\s*(.+)', text, re.DOTALL):
vod_content = re.search(r'简介\s*(.+)', text, re.DOTALL).group(1).strip()
# ================= 分集解析 (基于 episodeManager) =================
play_from_list = []
play_url_list = []
episode_manager = soup.select_one('[x-data*="episodeManager"]')
if episode_manager:
xdata = episode_manager.get('x-data', '')
lines_raw = re.findall(r'\{[^}]*lineName\s*:\s*\'([^\']+)\'[^}]*episodeCount\s*:\s*(\d+)[^}]*\}', xdata)
lines_info = [{'lineName': name, 'episodeCount': int(count)} for name, count in lines_raw]
episode_links = episode_manager.select('a[data-episode]')
lines_eps = {}
for a in episode_links:
line = a.get('data-line', '1')
ep = a.get('data-episode', '')
href = a.get('href', '')
if not href or not ep:
continue
full_url = href if href.startswith('http') else self.site_url + href
lines_eps.setdefault(line, []).append((int(ep), full_url))
for line_key in sorted(lines_eps.keys()):
eps = sorted(lines_eps[line_key], key=lambda x: x[0])
line_name = f'线路{line_key}'
for info in lines_info:
line_name = info['lineName']
break # 目前只用第一个线路名
if not eps:
continue
episode_strs = [f"{ep[0]}集${ep[1]}" for ep in eps]
play_from_list.append(line_name)
play_url_list.append('#'.join(episode_strs))
# 回退:无分集则直接播放当前页
if not play_url_list:
play_from_list.append('播放')
play_url_list.append(f"播放${vod_id}")
vod_play_from = '$$$'.join(play_from_list)
vod_play_url = '$$$'.join(play_url_list)
result = [{
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_content": vod_content,
"vod_actor": vod_actor,
"vod_director": vod_director,
"vod_area": "",
"vod_year": "",
"vod_play_from": vod_play_from,
"vod_play_url": vod_play_url
}]
return {"list": result}
def searchContent(self, key, quick, pg="1"):
page = int(pg) if pg else 1
params = {"q": key}
if page > 1:
params['page'] = page
query = urllib.parse.urlencode(params)
url = f"{self.site_url}/search?{query}"
resp = self.fetch(url, headers=self.headers)
if not resp:
return {"list": [], "page": page, "pagecount": 1}
soup = BeautifulSoup(resp.text, 'html.parser')
video_list = []
cards = soup.select('div[data-vod-id]')
if not cards:
# 搜索页可能没有 data-vod-id,降级处理
for a in soup.select('a.block[href^="/play/"]'):
href = a.get('href', '')
vod_id = href.replace('/play/', '').strip()
if not vod_id:
continue
h3 = a.select_one('h3')
vod_name = h3.get_text(strip=True) if h3 else href
if not vod_name:
continue
img = a.select_one('img[data-src]')
vod_pic = ''
if img:
src = img.get('data-src', '')
if src and not src.startswith('data:'):
vod_pic = src if src.startswith('http') else 'https:' + src
video_list.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": ''
})
else:
for card in cards[:30]:
a = card.select_one('a.block[href^="/play/"]')
if not a:
continue
vod_id = card.get('data-vod-id', '').strip()
if not vod_id:
href = a.get('href', '')
vod_id = href.replace('/play/', '').strip()
if not vod_id:
continue
title_tag = card.select_one('h3.text-white') or card.select_one('h3')
vod_name = title_tag.get_text(strip=True) if title_tag else ''
if not vod_name:
continue
img = card.select_one('img[data-src]')
vod_pic = ''
if img:
src = img.get('data-src', '')
if src and not src.startswith('data:'):
vod_pic = src if src.startswith('http') else 'https:' + src
remark_tag = card.select_one('.text-green-500, .text-yellow-400, span[class*="px-1.5"]')
vod_remarks = remark_tag.get_text(strip=True) if remark_tag else ''
video_list.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": vod_remarks
})
return {"list": video_list, "page": page, "pagecount": 1}
def playerContent(self, flag, id, vipFlags):
if not id.startswith('http'):
url = f"{self.site_url}/play/{id}"
else:
url = id
return {"parse": 1, "url": url, "header": self.headers}
+1 -1
View File
@@ -166,4 +166,4 @@ class Spider(Spider):
def destroy(self):
pass
def localProxy(self, param):
pass
pass
+252
View File
@@ -0,0 +1,252 @@
# -*- coding: utf-8 -*-
import json
import sys
from base64 import b64encode, b64decode
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.host = "https://www.dantatv.cc"
pass
def getName(self):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
# ---------- 辅助函数 ----------
def getheader(self, content_type=None):
headers = {
'Unique-Origin': 'B9A378A8C39BDA1277D2D6185FCE2695',
'User-Agent': 'okhttp/4.1.0/luob.app',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip',
}
if content_type:
headers['Content-Type'] = content_type
return headers
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 homeContent(self, filter):
classes = [
{"type_name": "剧集", "type_id": "1"},
{"type_name": "电影", "type_id": "2"},
{"type_name": "动漫", "type_id": "3"},
{"type_name": "短剧", "type_id": "4"},
{"type_name": "综艺", "type_id": "5"},
{"type_name": "体育赛事", "type_id": "29"},
]
return {"class": classes, "filters": {}}
# ---------- 首页推荐视频 ----------
def homeVideoContent(self):
url = f"{self.host}/api/index"
headers = self.getheader()
try:
resp = self.fetch(url, headers=headers)
data = resp.json()
vod_list = []
if data.get('data'):
for item in data['data'][0].get('vodList', []):
vod_list.append(self._vod_to_common(item))
except Exception as e:
print(f"首页推荐请求失败: {e}")
vod_list = []
return {'list': vod_list}
# ---------- 分类页(POST请求改用 self.post ----------
def categoryContent(self, tid, pg, filter, extend=None):
if extend is None:
extend = {}
body = {
"typeId1": int(tid),
"pageNum": int(pg),
"pageSize": 12,
"sortField": "vod_time",
"vodClass": extend.get('class', ''),
"vodArea": extend.get('area', ''),
"vodYear": extend.get('year', '')
}
url = f"{self.host}/api/search/type"
headers = self.getheader(content_type='application/json')
try:
# 使用基类的 post 方法(若基类无 post,可改为 requests.post 自行实现)
resp = self.post(url, headers=headers, data=json.dumps(body).encode('utf-8'))
data = resp.json()
vod_list = [self._vod_to_common(item) for item in data.get('data', [])]
except Exception as e:
print(f"分类页请求失败: {e}")
vod_list = []
return {
'list': vod_list,
'page': pg,
'pagecount': 9999,
'limit': 12,
'total': 999999
}
# ---------- 详情页 ----------
def detailContent(self, ids):
vod_id = ids[0]
url = f"{self.host}/api/vod/play?vodId={vod_id}"
headers = self.getheader()
try:
resp = self.fetch(url, headers=headers)
data = resp.json()
vod = data.get('data', {}).get('dantaVod', {})
except Exception as e:
print(f"详情请求失败: {e}")
return {'list': []}
if not vod:
return {'list': []}
info = {
'vod_id': vod.get('vodId'),
'vod_name': vod.get('vodName'),
'vod_pic': self._fix_pic(vod.get('vodPic')),
'vod_actor': vod.get('vodActor'),
'vod_director': vod.get('vodDirector'),
'vod_blurb': vod.get('vodContent') or vod.get('vodBlurb'),
'vod_area': vod.get('vodArea'),
'vod_year': vod.get('vodYear'),
'vod_remarks': vod.get('vodRemarks'),
'vod_lang': vod.get('vodLang'),
'vod_class': vod.get('vodClass'),
}
sources = vod.get('sources', [])
vod_play_from = []
vod_play_url = []
for idx, src in enumerate(sources):
collect_id = src.get('collectId')
raw_url = src.get('vodPlayUrl', '')
if not raw_url:
continue
items = raw_url.split('#')
encoded_items = []
for part in items:
if '$' not in part:
continue
name, link = part.split('$', 1)
payload = {"collectId": collect_id, "url": link}
enc = self.e64(json.dumps(payload, ensure_ascii=False))
encoded_items.append(f"{name}${enc}")
if encoded_items:
line_name = src.get('collectName') or src.get('vodPlayFrom') or f"线路{idx+1}"
vod_play_from.append(line_name)
vod_play_url.append('#'.join(encoded_items))
info['vod_play_from'] = '$$$'.join(vod_play_from)
info['vod_play_url'] = '$$$'.join(vod_play_url)
return {'list': [info]}
# ---------- 搜索 ----------
def searchContent(self, key, quick, pg="1"):
url = f"{self.host}/api/search/keyword"
params = {"keyword": key, "pageNum": pg, "pageSize": 10}
headers = self.getheader()
try:
resp = self.fetch(url, headers=headers, params=params)
data = resp.json()
vod_list = [self._vod_to_common(item) for item in data.get('data', [])]
except Exception as e:
print(f"搜索失败: {e}")
vod_list = []
return {'list': vod_list, 'page': pg}
# ---------- 播放解析 ----------
def playerContent(self, flag, id, vipFlags):
try:
payload = json.loads(self.d64(id))
collect_id = payload.get('collectId')
raw_url = payload.get('url')
if not collect_id or not raw_url:
raise ValueError("缺少参数")
except:
return {
'parse': 1,
'url': '',
'header': {'User-Agent': 'okhttp/4.1.0/luob.app'}
}
parse_url = f"{self.host}/api/vod/parse"
params = {"collectId": collect_id, "url": raw_url}
headers = self.getheader()
try:
resp = self.fetch(parse_url, headers=headers, params=params)
if resp.status_code == 200:
result = resp.json()
final_url = result.get('data') or result.get('url')
if final_url:
return {
'parse': 0,
'url': final_url,
'header': {'User-Agent': 'okhttp/4.1.0/luob.app'}
}
except Exception as e:
print(f"解析失败: {e}")
return {
'parse': 1,
'url': raw_url,
'header': {'User-Agent': 'okhttp/4.1.0/luob.app'}
}
# ---------- 内部辅助:字段映射 ----------
def _vod_to_common(self, item):
remark = item.get('vodRemarks', '')
color = item.get('vodColor')
if color:
remark = f"[{color}] {remark}" if remark else color
return {
'vod_id': item.get('vodId'),
'vod_name': item.get('vodName'),
'vod_pic': self._fix_pic(item.get('vodPic')),
'vod_remarks': remark,
'vod_year': item.get('vodYear'),
'vod_area': item.get('vodArea'),
'vod_actor': item.get('vodActor'),
'vod_director': item.get('vodDirector'),
'vod_lang': item.get('vodLang'),
'vod_class': item.get('vodClass'),
}
def _fix_pic(self, pic):
"""补全相对路径图片"""
if pic and pic.startswith('/'):
return self.host + pic
return pic