Files
tvbox_collect/jaychouqq/yingshi/py5/jumi.py
T
2026-06-29 03:41:14 +00:00

488 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
# TVBox爬虫 - Gimy TV 劇迷 (gimy.now)
# by:轻狂书生
import re
import sys
import json
import urllib3
from base.spider import Spider
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
sys.path.append('..')
class Spider(Spider):
# ========== 基础配置 ==========
host = 'https://gimy.now'
parser_base = ''
parser_referer = ''
def_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7',
'Referer': 'https://gimy.now/',
}
# 自营/广告线路过滤(留空不过滤)
self_operated = []
# 搜索 mid 映射: 1=电影, 2=电视剧, 3=综艺, 4=动漫
search_mids = [1, 2, 3, 4]
# ========== 首页分类 ==========
def homeContent(self, filter):
"""首页分类"""
return {'class': [
{'type_id': '13', 'type_name': '陸劇'},
{'type_id': '20', 'type_name': '韓劇'},
{'type_id': '16', 'type_name': '美劇'},
{'type_id': '15', 'type_name': '日劇'},
{'type_id': '14', 'type_name': '台劇'},
{'type_id': '21', 'type_name': '港劇'},
{'type_id': '31', 'type_name': '海外劇'},
{'type_id': '34', 'type_name': '短劇'},
{'type_id': '22', 'type_name': '紀錄片'},
{'type_id': '11', 'type_name': '劇情片'},
{'type_id': '6', 'type_name': '動作片'},
{'type_id': '9', 'type_name': '科幻片'},
{'type_id': '7', 'type_name': '喜劇片'},
{'type_id': '10', 'type_name': '恐怖片'},
{'type_id': '8', 'type_name': '愛情片'},
{'type_id': '12', 'type_name': '戰爭片'},
{'type_id': '24', 'type_name': '動畫電影'},
{'type_id': '29', 'type_name': '綜藝'},
]}
def homeVideoContent(self):
"""首页视频列表"""
return self.categoryContent('13', 1, {}, {})
# ========== 分类内容 ==========
def categoryContent(self, tid, pg, filter, extend):
"""分类内容"""
if int(pg) == 1:
url = f'{self.host}/genre/{tid}.html'
else:
url = f'{self.host}/genre/{tid}-{pg}.html'
try:
response = self.fetch(url, headers=self.def_headers, verify=False)
html = response.text
videos = self._extractList(html)
pagecount = self._getTotalPages(html, tid)
return {'list': videos, 'pagecount': pagecount, 'page': int(pg)}
except Exception:
return {'list': [], 'pagecount': 1, 'page': int(pg)}
# ========== 搜索 ==========
def searchContent(self, key, quick, pg='1'):
"""搜索 - 遍历所有 mid 合并去重"""
result = []
seen_ids = set()
try:
for mid in self.search_mids:
search_url = f'{self.host}/index.php/ajax/suggest'
params = {'mid': str(mid), 'wd': key, 'page': pg, 'limit': '20'}
response = self.fetch(search_url, params=params, headers=self.def_headers, verify=False)
data = json.loads(response.text)
if data.get('code') != 1:
continue
for item in data.get('list', []):
vid = str(item.get('id', ''))
if vid in seen_ids:
continue
seen_ids.add(vid)
result.append({
'vod_id': vid,
'vod_name': item.get('name', ''),
'vod_pic': item.get('pic', '').replace('#err', ''),
'vod_remarks': '',
})
except Exception:
pass
return {'list': result, 'page': int(pg)}
# ========== 详情页 ==========
def detailContent(self, ids):
"""详情页"""
vod_id = ids[0]
try:
url = f'{self.host}/detail/{vod_id}.html'
response = self.fetch(url, headers=self.def_headers, verify=False)
html = response.text
# 标题
vod_name = ''
name_match = re.search(r'<h1[^>]*>(.*?)</h1>', html, re.S)
if name_match:
vod_name = re.sub(r'<[^>]+>', '', name_match.group(1)).strip()
# 图片: a.video-pic 的 data-original 属性
vod_pic = ''
pic_match = re.search(r'<a[^>]*class="video-pic[^"]*"[^>]*data-original="([^"]+)"', html)
if not pic_match:
pic_match = re.search(r'<a[^>]*class="video-pic[^"]*"[^>]*data-src="([^"]+)"', html)
if not pic_match:
pic_match = re.search(r'<img[^>]*src="([^"]+)"[^>]*class="[^"]*(?:pic|img|poster)[^"]*"', html, re.S)
if pic_match:
vod_pic = pic_match.group(1)
# 信息区域: <ul class="info clearfix"> 下的 <li>
vod_year = ''
vod_area = ''
vod_director = ''
vod_actor = ''
vod_remarks = ''
info_match = re.search(r'<ul\s+class="info\s+clearfix">(.*?)</ul>', html, re.S)
if info_match:
info_block = info_match.group(1)
for li_match in re.finditer(r'<li[^>]*>(.*?)</li>', info_block, re.S):
li_text = re.sub(r'<[^>]+>', '', li_match.group(1)).strip()
if li_text.startswith('狀態'):
vod_remarks = re.sub(r'^狀態[:]\s*', '', li_text).strip()
elif li_text.startswith('類別'):
vod_remarks = vod_remarks or re.sub(r'^類別[:]\s*', '', li_text).strip()
elif li_text.startswith('主演'):
vod_actor = re.sub(r'^主演[:]\s*', '', li_text).strip()
elif li_text.startswith('年代'):
ym = re.search(r'(\d{4})', li_text)
if ym:
vod_year = ym.group(1)
elif li_text.startswith('導演'):
vod_director = re.sub(r'^導演[:]\s*', '', li_text).strip()
elif '國家' in li_text or '地區' in li_text:
vod_area = re.sub(r'^.*?[:]\s*', '', li_text).strip()
# 简介: 优先 meta description
vod_content = ''
meta_match = re.search(r'<meta[^>]*name="description"[^>]*content="([^"]+)"', html)
if meta_match:
vod_content = meta_match.group(1).strip()
# 清理标题前缀 (如 "海角遇見愛線上看,")
vod_content = re.sub(r'^.*?線上看[,]?\s*', '', vod_content).strip()
vod_content = re.sub(r'\[.*?\],?', '', vod_content).strip()
# 备选: span class=detail
if not vod_content:
detail_match = re.search(r'<span[^>]*class="[^"]*detail[^"]*"[^>]*>(.*?)</span>', html, re.S)
if detail_match:
vod_content = re.sub(r'<[^>]+>', '', detail_match.group(1)).strip()
vod_content = re.sub(r'\s+', ' ', vod_content).strip()
# 加上简介前缀
if vod_content:
vod_content = '【by:轻狂书生】\n' + vod_content
else:
vod_content = '【by:轻狂书生】'
# ===== 播放源: 用 id 精确匹配 tab 和 playlist =====
# 步骤1: 从 nav-tabs 获取线路名称和 source_id
source_map = [] # [(source_id, source_name), ...]
tabs_match = re.search(r'<ul\s+class="[^"]*nav-tabs[^"]*"[^>]*>(.*?)</ul>', html, re.S)
if tabs_match:
for a_match in re.finditer(r'<a[^>]*href="#con_playlist_(\d+)"[^>]*>(.*?)</a>', tabs_match.group(1), re.S):
source_id = a_match.group(1)
source_name = re.sub(r'<[^>]+>', '', a_match.group(2)).strip()
source_map.append((source_id, source_name))
# 步骤2: 从 id=con_playlist_N 精确提取每个线路的剧集
playlist_data = {} # {source_id: [(ep_name, href), ...]}
for pl_match in re.finditer(r'<ul[^>]*id="con_playlist_(\d+)"[^>]*>(.*?)</ul>', html, re.S):
pl_id = pl_match.group(1)
pl_html = pl_match.group(2)
episodes = []
for a_match in re.finditer(r'<a[^>]*href="(/play/[^"]+)"[^>]*>(.*?)</a>', pl_html, re.S):
href = a_match.group(1)
ep_name = re.sub(r'<[^>]+>', '', a_match.group(2)).strip()
if not ep_name:
ep_match = re.search(r'/play/\d+-\d+-(\d+)\.html', href)
ep_name = f'第{int(ep_match.group(1)):02d}集' if ep_match else href
episodes.append((ep_name, href))
if pl_id not in playlist_data:
playlist_data[pl_id] = episodes
# 步骤3: 按 source_map 顺序组装播放源
play_from = []
play_url = []
for source_id, source_name in source_map:
if self.self_operated and source_name in self.self_operated:
continue
episodes = playlist_data.get(source_id, [])
if not episodes:
continue
ep_list = []
for ep_name, href in episodes:
ep_list.append(f'{ep_name}${self.host}{href}')
play_from.append(source_name.replace(' ', ''))
play_url.append('#'.join(ep_list))
video = {
'vod_id': vod_id,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_year': vod_year,
'vod_area': vod_area,
'vod_director': vod_director,
'vod_actor': vod_actor,
'vod_content': vod_content,
'vod_remarks': vod_remarks,
'vod_play_from': '$$$'.join(play_from) if play_from else '',
'vod_play_url': '$$$'.join(play_url) if play_url else '',
}
return {'list': [video]}
except Exception:
return {'list': []}
# ========== 播放解析 ==========
def playerContent(self, flag, vid, vip_flags):
"""播放解析"""
jx = 0
url = ''
try:
response = self.fetch(vid, headers=self.def_headers, verify=False)
html = response.text
# 1. 优先提取 player_data JSON
video_url = ''
play_from = ''
player_match = re.search(r'player_data\s*=\s*(\{.+?\})\s*;', html, re.S)
if not player_match:
player_match = re.search(r'player_data\s*=\s*(\{.+?\})\s*</script>', html, re.S)
if not player_match:
player_match = re.search(r'player_aaaa\s*=\s*(\{.+?\})\s*</script>', html, re.S)
if not player_match:
player_match = re.search(r'player_aaaa\s*=\s*(\{.+?\})\s*;', html, re.S)
if player_match:
json_str = player_match.group(1).strip()
json_str = re.sub(r'<[^>]+>$', '', json_str).strip()
try:
player_data = json.loads(json_str)
video_url = player_data.get('url', '')
play_from = player_data.get('from', '')
if video_url and '\\u' in video_url:
video_url = video_url.encode('utf-8').decode('unicode_escape')
except json.JSONDecodeError:
url_match = re.search(r'"url"\s*:\s*"([^"]+)"', json_str)
if url_match:
video_url = url_match.group(1)
if '\\u' in video_url:
video_url = video_url.encode('utf-8').decode('unicode_escape')
# 2. 备选: iframe
if not video_url:
iframe_match = re.search(r'<iframe[^>]+src=["\']([^"\']+)["\']', html)
if iframe_match:
video_url = iframe_match.group(1)
if video_url.startswith('//'):
video_url = 'https:' + video_url
# 3. 兜底: 直接 m3u8/mp4
if not video_url:
m3u8_match = re.search(r'(https?://[^\s"\'<>]+\.m3u8[^\s"\'<>]*)', html)
if m3u8_match:
video_url = m3u8_match.group(1)
if not video_url:
mp4_match = re.search(r'(https?://[^\s"\'<>]+\.mp4[^\s"\'<>]*)', html)
if mp4_match:
video_url = mp4_match.group(1)
# 4. 判断是否需要解析
if video_url:
# 直链: m3u8/mp4 直接播放
if '.m3u8' in video_url or '.mp4' in video_url:
url = video_url
jx = 0
# JD- 加密线路: 用 /d/parse.php 解析
elif video_url.startswith('JD-') or 'JD-' in video_url:
parsed = self._parse_jd(video_url)
if parsed:
url = parsed
jx = 0
else:
url = video_url
jx = 1
# VIP 平台链接: 用 /v/parse.php 解析
elif re.search(r'(?:v\.youku|www\.iqiyi|v\.qq|www\.bilibili|www\.mgtv|www\.le\.com|tv\.sohu|www\.pptv)\.com', video_url):
parsed = self._parse_vip(video_url)
if parsed:
url = parsed
jx = 0
else:
url = video_url
jx = 1
# 其他非直链
else:
url = video_url
jx = 1
if not url:
url = vid
jx = 1
except Exception:
url = vid
jx = 1
return {
'jx': jx,
'parse': 0,
'url': url,
'header': {
**self.def_headers,
'Referer': self.host + '/',
}
}
def _parse_jd(self, jd_url):
"""解析 JD- 加密线路 (4K/超清/2K 等)"""
try:
parse_api = f'https://play.gimyai.tw/d/parse.php?url={jd_url}'
parse_headers = {
**self.def_headers,
'Referer': 'https://play.gimyai.tw/',
'X-Requested-With': 'XMLHttpRequest',
}
response = self.fetch(parse_api, headers=parse_headers, verify=False)
data = json.loads(response.text)
if str(data.get('code')) == '200':
return data.get('url', '')
except Exception:
pass
return None
def _parse_vip(self, video_url):
"""解析 VIP 平台链接 (优酷/奇异/腾讯/芒果等)"""
try:
parse_api = f'https://play.gimyai.tw/v/parse.php?url={video_url}'
parse_headers = {
**self.def_headers,
'Referer': 'https://play.gimyai.tw/',
'X-Requested-With': 'XMLHttpRequest',
}
response = self.fetch(parse_api, headers=parse_headers, verify=False)
data = json.loads(response.text)
if str(data.get('code')) == '200':
return data.get('url', '')
except Exception:
pass
return None
# ========== 辅助方法 ==========
def _extractList(self, html):
"""从HTML提取视频列表"""
videos = []
seen_ids = set()
for li_match in re.finditer(r'<li\s+class="col-md-2[^"]*">(.*?)</li>', html, re.S):
li_html = li_match.group(1)
# 图片和链接
pic_a_match = re.search(
r'<a\s+class="video-pic[^"]*"[^>]*data-original="([^"]*)"[^>]*href="(/detail/\d+\.html)"[^>]*title="([^"]*)"',
li_html
)
if not pic_a_match:
pic_a_match = re.search(
r'<a\s+class="video-pic[^"]*"[^>]*href="(/detail/\d+\.html)"[^>]*data-original="([^"]*)"[^>]*title="([^"]*)"',
li_html
)
if pic_a_match:
href = pic_a_match.group(1)
pic = pic_a_match.group(2)
title = pic_a_match.group(3)
else:
continue
else:
pic = pic_a_match.group(1)
href = pic_a_match.group(2)
title = pic_a_match.group(3)
vid_match = re.search(r'/detail/(\d+)\.html', href)
if not vid_match:
continue
vid = vid_match.group(1)
if vid in seen_ids:
continue
seen_ids.add(vid)
# 解码 HTML 实体
pic = pic.replace('&amp;', '&')
# 状态: <span class="note text-bg-r">
remark = ''
note_match = re.search(r'<span\s+class="note[^"]*">(.*?)</span>', li_html, re.S)
if note_match:
remark = re.sub(r'<[^>]+>', '', note_match.group(1)).strip()
# 演员: <div class="subtitle ...">
actor = ''
sub_match = re.search(r'<div\s+class="subtitle[^"]*">(.*?)</div>', li_html, re.S)
if sub_match:
actor = re.sub(r'<[^>]+>', '', sub_match.group(1)).strip()
videos.append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': remark,
'vod_actor': actor,
})
return videos
def _getTotalPages(self, html, tid):
"""从分页区获取总页数"""
tail_match = re.search(
r'<a[^>]*href="/genre/' + re.escape(tid) + r'-(\d+)\.html"[^>]*>尾頁</a>',
html
)
if not tail_match:
tail_match = re.search(
r'<a[^>]*href="/genre/' + re.escape(tid) + r'-(\d+)\.html"[^>]*>末页</a>',
html
)
if tail_match:
return int(tail_match.group(1))
return 1
# ========== 必需方法 ==========
def init(self, extend=''):
pass
def getName(self):
return 'Gimy劇迷'
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
def localProxy(self, param):
pass