427 lines
15 KiB
Python
427 lines
15 KiB
Python
#coding=utf-8
|
|
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
TVBox / 影视仓 Python源脚本
|
|
站点: 可可影视 (103.51.147.112:51120)
|
|
"""
|
|
|
|
import sys
|
|
import re
|
|
import json
|
|
import requests
|
|
import urllib3
|
|
from urllib.parse import quote
|
|
from pyquery import PyQuery as pq
|
|
sys.path.append('..')
|
|
from base.spider import Spider
|
|
|
|
# 禁用SSL警告
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
class Spider(Spider):
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.site = 'https://103.51.147.112:51120'
|
|
self.session = requests.Session()
|
|
self.session.headers.update({
|
|
'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': 'https://103.51.147.112:51120/'
|
|
})
|
|
self.cateManual = {
|
|
'电影': '1',
|
|
'连续剧': '2',
|
|
'动漫': '3',
|
|
'综艺纪录': '4',
|
|
'短剧': '6'
|
|
}
|
|
# 隐晦的站点标识
|
|
self._mark = chr(26143) + chr(27827)
|
|
|
|
def init(self, extend=""):
|
|
pass
|
|
|
|
def getName(self):
|
|
return "可可影视"
|
|
|
|
def isVideoFormat(self, url):
|
|
pass
|
|
|
|
def manualVideoCheck(self):
|
|
pass
|
|
|
|
def homeContent(self, filter):
|
|
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
|
|
for k, v in self.cateManual.items():
|
|
result['class'].append({
|
|
'type_id': str(v),
|
|
'type_name': k
|
|
})
|
|
return result
|
|
|
|
def homeVideoContent(self):
|
|
videos = []
|
|
try:
|
|
url = f'{self.site}/channel/1.html'
|
|
r = self.session.get(url, timeout=15, verify=False)
|
|
r.encoding = 'utf-8'
|
|
doc = pq(r.text)
|
|
items = doc('.module-item')
|
|
seen = set()
|
|
for item in items.items():
|
|
a = item.find('.v-item')
|
|
href = a.attr('href') or ''
|
|
vid = self.getVid(href)
|
|
if not vid or vid in seen:
|
|
continue
|
|
seen.add(vid)
|
|
|
|
# 标题
|
|
titles = item.find('.v-item-title')
|
|
title = ''
|
|
for j in range(len(titles)):
|
|
t = titles.eq(j).text().strip()
|
|
if t and t != '可可影视-kekys.com':
|
|
title = t
|
|
break
|
|
|
|
# 图片
|
|
pic = ''
|
|
imgs = item.find('img')
|
|
for j in range(len(imgs)):
|
|
img = imgs.eq(j)
|
|
src = img.attr('data-original') or ''
|
|
if src and 'placeholder' not in src and 'logo_placeholder' not in src:
|
|
pic = src
|
|
break
|
|
if pic and pic.startswith('/'):
|
|
pic = 'https://vres.zyxpedu.com' + pic
|
|
|
|
# 备注
|
|
note = ''
|
|
bottom = item.find('.v-item-bottom span')
|
|
if bottom.length:
|
|
note = bottom.text().strip()
|
|
|
|
if title:
|
|
videos.append({
|
|
'vod_id': vid,
|
|
'vod_name': title,
|
|
'vod_pic': pic,
|
|
'vod_remarks': note
|
|
})
|
|
except Exception as e:
|
|
print(f'homeVideoContent error: {e}')
|
|
return {'list': videos[:24], 'parse': 0, 'jx': 0}
|
|
|
|
def categoryContent(self, tid, pg, filter, extend):
|
|
result = {'list': [], 'parse': 0, 'jx': 0}
|
|
page = int(pg) if pg else 1
|
|
try:
|
|
url = f'{self.site}/channel/{tid}.html?page={page}'
|
|
r = self.session.get(url, timeout=15, verify=False)
|
|
r.encoding = 'utf-8'
|
|
doc = pq(r.text)
|
|
items = doc('.module-item')
|
|
for item in items.items():
|
|
a = item.find('.v-item')
|
|
href = a.attr('href') or ''
|
|
vid = self.getVid(href)
|
|
if not vid:
|
|
continue
|
|
|
|
# 标题
|
|
titles = item.find('.v-item-title')
|
|
title = ''
|
|
for j in range(len(titles)):
|
|
t = titles.eq(j).text().strip()
|
|
if t and t != '可可影视-kekys.com':
|
|
title = t
|
|
break
|
|
|
|
# 图片
|
|
pic = ''
|
|
imgs = item.find('img')
|
|
for j in range(len(imgs)):
|
|
img = imgs.eq(j)
|
|
src = img.attr('data-original') or ''
|
|
if src and 'placeholder' not in src and 'logo_placeholder' not in src:
|
|
pic = src
|
|
break
|
|
if pic and pic.startswith('/'):
|
|
pic = 'https://vres.zyxpedu.com' + pic
|
|
|
|
# 备注
|
|
note = ''
|
|
bottom = item.find('.v-item-bottom span')
|
|
if bottom.length:
|
|
note = bottom.text().strip()
|
|
|
|
if title:
|
|
result['list'].append({
|
|
'vod_id': vid,
|
|
'vod_name': title,
|
|
'vod_pic': pic,
|
|
'vod_remarks': note
|
|
})
|
|
except Exception as e:
|
|
print(f'categoryContent error: {e}')
|
|
|
|
result['page'] = page
|
|
# 修复:满页24条说明还有下一页,否则已到尾页
|
|
result['pagecount'] = page + 1 if len(result['list']) >= 24 else page
|
|
result['limit'] = len(result['list'])
|
|
result['total'] = len(result['list'])
|
|
return result
|
|
|
|
def detailContent(self, ids):
|
|
result = {'list': [], 'parse': 0, 'jx': 0}
|
|
vid = ids[0] if ids else ''
|
|
if not vid:
|
|
return result
|
|
try:
|
|
url = f'{self.site}/detail/{vid}.html'
|
|
r = self.session.get(url, timeout=15, verify=False)
|
|
r.encoding = 'utf-8'
|
|
html = r.text
|
|
|
|
# 标题:从title标签提取,最可靠
|
|
title = ''
|
|
title_match = re.search(r'<title>(.+?)</title>', html)
|
|
if title_match:
|
|
title = title_match.group(1).split('-')[0].strip()
|
|
# 去掉特殊字符水印
|
|
title = re.sub(r'[𝕜𝕜𝕪𝕤𝟘𝟙𝕔𝕠𝕞.\s]+', ' ', title).strip()
|
|
title = re.sub(r'\s+', ' ', title).strip()
|
|
|
|
# 图片:从og:image提取
|
|
pic = ''
|
|
og_img = re.search(r'<meta\s+property="og:image"\s+content="([^"]+)"', html)
|
|
if og_img:
|
|
pic = og_img.group(1)
|
|
if pic.startswith('/'):
|
|
pic = 'https://vres.zyxpedu.com' + pic
|
|
|
|
# 简介:从meta description提取
|
|
desc = ''
|
|
desc_match = re.search(r'<meta\s+name="description"\s+content="([^"]+)"', html)
|
|
if desc_match:
|
|
desc = desc_match.group(1).strip()
|
|
|
|
# 播放线路和集数
|
|
play_from = []
|
|
play_url = []
|
|
episodes_by_sid = {}
|
|
sids_in_order = []
|
|
seen_sids = set()
|
|
|
|
# 纯正则提取所有播放链接
|
|
all_play = re.findall(r'<a[^>]+href="(/play/\d+-(\d+)-(\d+)\.html)"[^>]+class="episode-item"[^>]*>(.*?)</a>', html, re.DOTALL)
|
|
# 如果没匹配到,试试class在href前面的情况
|
|
if not all_play:
|
|
all_play = re.findall(r'<a[^>]+class="episode-item"[^>]+href="(/play/\d+-(\d+)-(\d+)\.html)"[^>]*>(.*?)</a>', html, re.DOTALL)
|
|
for href, sid, nid, link_html in all_play:
|
|
text = re.sub(r'<[^>]+>', '', link_html).strip()
|
|
if not text:
|
|
continue
|
|
if sid not in episodes_by_sid:
|
|
episodes_by_sid[sid] = []
|
|
episodes_by_sid[sid].append(f'{text}${href}')
|
|
if sid not in seen_sids:
|
|
seen_sids.add(sid)
|
|
sids_in_order.append(sid)
|
|
|
|
# 线路名称
|
|
source_labels = []
|
|
all_labels = re.findall(r'class="source-item-label"[^>]*>([^<]+)</', html)
|
|
for label in all_labels:
|
|
label = label.strip()
|
|
if label:
|
|
source_labels.append(label)
|
|
|
|
for i, sid in enumerate(sids_in_order):
|
|
if sid in episodes_by_sid and episodes_by_sid[sid]:
|
|
if i < len(source_labels):
|
|
line_name = source_labels[i]
|
|
else:
|
|
line_name = f'线路{sid}'
|
|
# 跳过4K线路(只有APP端能用)
|
|
if line_name == '4K':
|
|
continue
|
|
play_from.append(line_name)
|
|
play_url.append('#'.join(episodes_by_sid[sid]))
|
|
|
|
vod = {
|
|
'vod_id': vid,
|
|
'vod_name': title,
|
|
'vod_pic': pic,
|
|
'type_name': '',
|
|
'vod_year': '',
|
|
'vod_area': '',
|
|
'vod_remarks': '',
|
|
'vod_actor': '',
|
|
'vod_director': self._mark,
|
|
'vod_content': desc,
|
|
'vod_play_from': '$$$'.join(play_from) if play_from else '',
|
|
'vod_play_url': '$$$'.join(play_url) if play_url else ''
|
|
}
|
|
result['list'].append(vod)
|
|
except Exception as e:
|
|
print(f'detailContent error: {e}')
|
|
return result
|
|
|
|
def playerContent(self, flag, id, vipFlags):
|
|
result = {}
|
|
try:
|
|
play_url = id
|
|
if id and not id.startswith('http'):
|
|
play_url = self.site + id
|
|
|
|
r = self.session.get(play_url, timeout=15, verify=False)
|
|
r.encoding = 'utf-8'
|
|
|
|
video_url = ''
|
|
|
|
patterns = [
|
|
r"src:\s*['\"]([^'\"]+\.(m3u8|mp4)[^'\"]*)['\"]",
|
|
r'"url"\s*:\s*"([^"]+\.(m3u8|mp4)[^"]*)"',
|
|
r"url\s*:\s*'([^']+\.(m3u8|mp4)[^']*)'",
|
|
]
|
|
|
|
for pat in patterns:
|
|
m = re.search(pat, r.text, re.DOTALL)
|
|
if m:
|
|
video_url = m.group(1)
|
|
break
|
|
|
|
# 修复:使用非捕获组 (?:m3u8|mp4),避免 re.findall 只返回捕获组内容
|
|
if not video_url:
|
|
all_urls = re.findall(r'https?://[^\s"\'<>]+\.(?:m3u8|mp4)[^\s"\'<>]*', r.text)
|
|
if all_urls:
|
|
for u in all_urls:
|
|
if 'index.m3u8' in u or 'video.m3u8' in u or '.mp4' in u:
|
|
video_url = u
|
|
break
|
|
if not video_url:
|
|
video_url = all_urls[0]
|
|
|
|
# 修复:处理相对路径视频URL
|
|
if video_url and not video_url.startswith('http'):
|
|
if video_url.startswith('/'):
|
|
video_url = self.site + video_url
|
|
else:
|
|
video_url = self.site + '/' + video_url
|
|
|
|
if video_url:
|
|
result['parse'] = 0
|
|
result['url'] = video_url
|
|
result['jx'] = 0
|
|
result['header'] = {
|
|
'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 + '/'
|
|
}
|
|
else:
|
|
result['parse'] = 1
|
|
result['url'] = play_url
|
|
result['jx'] = 0
|
|
result['header'] = {
|
|
'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 + '/'
|
|
}
|
|
except Exception as e:
|
|
print(f'playerContent error: {e}')
|
|
result['parse'] = 1
|
|
result['url'] = id if id.startswith('http') else self.site + id
|
|
result['jx'] = 0
|
|
result['header'] = {
|
|
'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 + '/'
|
|
}
|
|
return result
|
|
|
|
def searchContent(self, key, quick, pg='1'):
|
|
result = {'list': [], 'parse': 0, 'jx': 0}
|
|
page = int(pg) if pg else 1
|
|
try:
|
|
# 先访问搜索页获取token
|
|
search_url = f'{self.site}/search?k={quote(key)}'
|
|
r = self.session.get(search_url, timeout=15, verify=False)
|
|
r.encoding = 'utf-8'
|
|
t_match = re.search(r'name="t" value="([^"]+)"', r.text)
|
|
t = t_match.group(1) if t_match else ''
|
|
|
|
if t:
|
|
url = f'{self.site}/search?k={quote(key)}&t={quote(t)}'
|
|
if page > 1:
|
|
url += f'&page={page}'
|
|
r = self.session.get(url, timeout=15, verify=False)
|
|
r.encoding = 'utf-8'
|
|
|
|
doc = pq(r.text)
|
|
items = doc('.search-result-item')
|
|
for item in items.items():
|
|
# 修复:兼容 href 在元素本身或子级 <a> 标签上的情况
|
|
href = item.attr('href') or ''
|
|
if not href:
|
|
a_tag = item.find('a')
|
|
if a_tag.length:
|
|
href = a_tag.attr('href') or ''
|
|
|
|
vid = self.getVid(href)
|
|
if not vid:
|
|
continue
|
|
|
|
# 标题
|
|
title = ''
|
|
title_elem = item.find('.title')
|
|
if title_elem.length:
|
|
title = title_elem.text().strip()
|
|
if not title:
|
|
img = item.find('img')
|
|
if img.length:
|
|
title = img.attr('alt') or img.attr('title') or ''
|
|
title = title.strip()
|
|
|
|
# 图片
|
|
pic = ''
|
|
imgs = item.find('img')
|
|
for j in range(len(imgs)):
|
|
img = imgs.eq(j)
|
|
src = img.attr('data-original') or img.attr('src') or ''
|
|
if src and 'placeholder' not in src and 'logo_placeholder' not in src:
|
|
pic = src
|
|
break
|
|
if pic and pic.startswith('/'):
|
|
pic = 'https://vres.zyxpedu.com' + pic
|
|
|
|
if title:
|
|
result['list'].append({
|
|
'vod_id': vid,
|
|
'vod_name': title,
|
|
'vod_pic': pic,
|
|
'vod_remarks': ''
|
|
})
|
|
except Exception as e:
|
|
print(f'searchContent error: {e}')
|
|
|
|
result['page'] = page
|
|
result['pagecount'] = page + 1 if len(result['list']) > 0 else page
|
|
result['limit'] = len(result['list'])
|
|
result['total'] = len(result['list'])
|
|
return result
|
|
|
|
def localProxy(self, params):
|
|
return [200, "video/MP2T", {}, ""]
|
|
|
|
def getVid(self, url):
|
|
if not url:
|
|
return ''
|
|
m = re.search(r'/detail/(\d+)\.html', url)
|
|
if m:
|
|
return m.group(1)
|
|
m = re.search(r'/play/(\d+)-', url)
|
|
if m:
|
|
return m.group(1)
|
|
return ''
|