上传文件至「py」
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
#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 ''
|
||||
Reference in New Issue
Block a user