'
+ pos = 0
+ while True:
+ start = html.find(start_tag, pos)
+ if start == -1:
+ break
+ start += len(start_tag)
+ depth = 0
+ end = None
+ i = start
+ while i < len(html):
+ if html[i:i+5] == '
':
+ if depth == 0:
+ end = i + 6
+ break
+ else:
+ depth -= 1
+ i += 6
+ else:
+ i += 1
+ if end is not None:
+ block_html = html[start:end]
+ if '/vplay/' in block_html:
+ list_blocks.append(block_html)
+ pos = end
+ else:
+ pos = start + 1
+
+ if not list_blocks:
+ blocks = []
+ start_tag2 = '
':
+ if depth == 0:
+ end = i + 6
+ break
+ else:
+ depth -= 1
+ i += 6
+ else:
+ i += 1
+ if end is not None:
+ block_html = html[start:end]
+ if '/vplay/' in block_html:
+ list_blocks.append(block_html)
+ pos = end
+ else:
+ pos = start + 1
+
+ if not list_blocks:
+ simple = re.findall(r'
]*>(.*?)
', html, re.DOTALL)
+ for b in simple:
+ if '/vplay/' in b:
+ list_blocks.append(b)
+
+ # 3. 对齐名称与块数量
+ if len(source_names) > len(list_blocks):
+ source_names = source_names[:len(list_blocks)]
+ while len(source_names) < len(list_blocks):
+ source_names.append(f"源{len(source_names)+1}")
+
+ # 4. 解析每个块的集数
+ for idx, block in enumerate(list_blocks):
+ eps = []
+ for m in re.finditer(r'
]*href="(/vplay/(\d+)-(\d+)-(\d+)\.html)"[^>]*>.*?([^<]*)', block):
+ link = m.group(1)
+ id_ = m.group(2)
+ sid = m.group(3)
+ nid = m.group(4)
+ name = m.group(5).strip()
+ eps.append({"name": name, "link": f"{id_}-{sid}-{nid}"})
+ if eps:
+ sources.append({
+ "source_name": source_names[idx] if idx < len(source_names) else f"源{idx+1}",
+ "episodes": eps
+ })
+
+ if not sources:
+ eps = []
+ for m in re.finditer(r']*href="(/vplay/(\d+)-(\d+)-(\d+)\.html)"[^>]*>.*?([^<]*)', html):
+ link = m.group(1)
+ id_ = m.group(2)
+ sid = m.group(3)
+ nid = m.group(4)
+ name = m.group(5).strip()
+ eps.append({"name": name, "link": f"{id_}-{sid}-{nid}"})
+ if eps:
+ sources.append({"source_name": "默认", "episodes": eps})
+
+ return sources
+
+ # ---------- 播放地址获取 ----------
+ def _get_play_url(self, vod_id, sid, nid):
+ try:
+ play_page = f"{self.BASE_URL}/vplay/{vod_id}-{sid}-{nid}.html"
+ html = self._fetch_html(play_page)
+ if not html:
+ return None
+
+ iframe_match = re.search(r'\s*
', html, re.DOTALL)
+ if not block:
+ return {"list": []}
+ videos = self._parse_video_list(block.group(1))
+ return {"list": videos[:20]}
+ except Exception as e:
+ print(f"[歪比影视] homeVideoContent 异常: {e}")
+ return {"list": []}
+
+ def categoryContent(self, tid, pg, filter, extend):
+ try:
+ pg = int(pg)
+ if tid not in self.CATEGORY_MAP:
+ return {"list": [], "pagecount": 1, "page": pg}
+ if pg == 1:
+ url = f"{self.BASE_URL}/show/{tid}-----------.html"
+ else:
+ url = f"{self.BASE_URL}/show/{tid}--------{pg}---.html"
+ html = self._fetch_html(url)
+ if not html:
+ return {"list": [], "pagecount": 1, "page": pg}
+ videos = self._parse_video_list(html)
+ last = re.search(r'
]*href="/show/\d+--------(\d+)---\.html"[^>]*>尾页', html)
+ pagecount = int(last.group(1)) if last else 1
+ return {"list": videos, "pagecount": pagecount, "page": pg}
+ except Exception as e:
+ print(f"[歪比影视] categoryContent 异常: {e}")
+ return {"list": [], "pagecount": 1, "page": pg}
+
+ def searchContent(self, key, quick, pg='1'):
+ try:
+ pg = int(pg)
+ url = f"{self.BASE_URL}/search/{key}-------------.html"
+ html = self._fetch_html(url)
+ if not html:
+ return {"list": [], "page": pg}
+ # 使用专门解析搜索页的方法
+ videos = self._parse_search_list(html)
+ return {"list": videos, "page": pg}
+ except Exception as e:
+ print(f"[歪比影视] searchContent 异常: {e}")
+ return {"list": [], "page": pg}
+
+ def detailContent(self, ids):
+ try:
+ vod_id = ids[0]
+ url = f"{self.BASE_URL}/detail/{vod_id}.html"
+ html = self._fetch_html(url)
+ if not html:
+ return {"list": []}
+
+ title = re.search(r'
([^<]*)
', html)
+ vod_name = title.group(1).strip() if title else "未知"
+ pic = re.search(r'
![]()
]*data-original="([^"]+)"', html)
+ vod_pic = pic.group(1) if pic else ""
+ desc = re.search(r'
]*>(.*?)
', html, re.DOTALL)
+ vod_content = self._clean_html(desc.group(1)) if desc else ""
+ actor = re.search(r'主演:.*?
(.*?)
', html, re.DOTALL)
+ vod_actor = self._clean_html(actor.group(1)) if actor else ""
+ director = re.search(r'导演:.*?
(.*?)
', html, re.DOTALL)
+ vod_director = self._clean_html(director.group(1)) if director else ""
+ year = re.search(r'
]+>', '', text)
+ text = html_module.unescape(text)
+ text = text.replace('\xa0', ' ')
+ text = ' '.join(text.split())
+ return text.strip()
+
+ def _get(self, url):
+ try:
+ r = self.session.get(url, timeout=15, headers={'Referer': self.site})
+ r.encoding = 'utf-8'
+ return r.text
+ except:
+ return ''
+
+ 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 _extract_list(self, html):
+ videos = []
+ seen = set()
+ for m in re.finditer(r'href="/umo/(\d+)\.html"[^>]*?title="([^"]*)"', html):
+ vid = m.group(1)
+ title = m.group(2).strip()
+ if vid in seen or not title:
+ continue
+ snippet = html[m.start():m.start()+400]
+ pm = re.search(r'data-original="([^"]*)"', snippet)
+ pic = pm.group(1).strip() if pm else ''
+ if vid in seen:
+ continue
+ seen.add(vid)
+ note = ''
+ nm = re.search(r'pic-text text-right">([^<]*)', snippet)
+ if nm:
+ note = nm.group(1).strip()
+ videos.append({
+ 'vod_id': vid,
+ 'vod_name': title,
+ 'vod_pic': pic,
+ 'vod_remarks': note
+ })
+ return videos
+
+ def homeVideoContent(self):
+ result = {'list': [], 'parse': 0, 'jx': 0}
+ html = self._get(self.site)
+ if html:
+ result['list'] = self._extract_list(html)
+ return result
+
+ def categoryContent(self, tid, pg, filter, extend):
+ result = {'list': [], 'parse': 0, 'jx': 0}
+ page = int(pg) if pg else 1
+ url = f'{self.site}/jxk/{tid}.html'
+ html = self._get(url)
+ if html:
+ result['list'] = self._extract_list(html)
+ result['page'] = page
+ result['pagecount'] = page + 1 if result['list'] 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 = ''
+ if isinstance(ids, list):
+ vid = ids[0] if ids else ''
+ elif ids:
+ vid = str(ids)
+ if not vid:
+ return result
+ # 用播放页第一集来获取 player_aaaa 数据
+ html = self._get(f'{self.site}/aey/{vid}/1-1.html')
+ if not html:
+ return result
+
+ # 提取 player_aaaa
+ pd = {}
+ m = re.search(r'var player_aaaa=(\{[^<]+\})', html)
+ if m:
+ try:
+ pd = json.loads(m.group(1))
+ except:
+ pass
+
+ # 从详情页获取更多信息
+ detail_html = self._get(f'{self.site}/umo/{vid}.html')
+
+ # 标题
+ title = pd.get('vod_data', {}).get('vod_name', '')
+ if not title:
+ m2 = re.search(r']*class="title"[^>]*>([^<]*)', detail_html)
+ if m2:
+ title = self._clean(m2.group(1))
+ if not title:
+ m2 = re.search(r'([^<]+)', detail_html)
+ if m2:
+ title = self._clean(re.sub(r'\s*[-–—].*$', '', m2.group(1)))
+
+ # 封面
+ pic = ''
+ m2 = re.search(r'data-original="([^"]+)"[^>]*class="[^"]*cover[^"]*"', detail_html)
+ if not m2:
+ m2 = re.search(r'data-original="([^"]+)"[^>]*rel="nofollow"', detail_html)
+ if m2:
+ pic = m2.group(1).strip()
+
+ # 类型、地区、年份、语言、主演、导演、简介
+ def extract_info(pattern, text):
+ m3 = re.search(pattern, text)
+ if m3:
+ return self._clean(m3.group(1))
+ return ''
+
+ vod_class = extract_info(r'类型:(.*?)(?:
|
(.*?)(?:|(.*?)(?:|(.*?)(?:|(.*?)(?:|)', detail_html)
+ if not actor:
+ actor = pd.get('vod_data', {}).get('vod_actor', '')
+ director = extract_info(r'导演:(.*?)(?:|
)', detail_html)
+ if not director:
+ director = pd.get('vod_data', {}).get('vod_director', '')
+ if director:
+ director = self._m + '、' + director
+ else:
+ director = self._m
+
+ desc = extract_info(r'detail-sketch">(.*?)', detail_html)
+
+ # 播放列表 - 从详情页提取所有线路和集数
+ play_from = []
+ play_url_list = []
+
+ # 提取线路名(在 playlist data-toggle="tab" 里)
+ line_names = re.findall(r'playlist\d+" data-toggle="tab"[^>]*rel="nofollow">([^<]+)<', detail_html)
+ # 如果没找到,尝试提取 pannel__head 里的文字
+ if not line_names:
+ line_names = re.findall(r'pannel__head[^>]*>([^<]*)<', detail_html)
+
+ # 提取每个播放面板的链接
+ link_groups = re.findall(r'tab-pane fade[^>]*>(.*?)', detail_html, re.DOTALL)
+
+ for i, group_html in enumerate(link_groups):
+ line_name = line_names[i] if i < len(line_names) else f'线路{i+1}'
+ line_name = self._clean(line_name)
+ episodes = []
+ for em in re.finditer(r'href="(/aey/\d+/(\d+-\d+)\.html)"[^>]*>([^<]*)<', group_html):
+ ep_href = em.group(1)
+ ep_label = em.group(3).strip()
+ if ep_label and ep_href:
+ episodes.append(f'{ep_label}${ep_href}')
+ if episodes:
+ play_from.append(line_name)
+ play_url_list.append('#'.join(episodes))
+
+ # 把华为云排到第一个(1080p)
+ for i, name in enumerate(play_from):
+ if '华为' in name and i > 0:
+ play_from.insert(0, play_from.pop(i))
+ play_url_list.insert(0, play_url_list.pop(i))
+ break
+
+ # 备用:如果详情页没有找到播放列表,直接用播放页的 URL
+ if not play_from and pd:
+ url = pd.get('url', '')
+ from_flag = pd.get('from', '')
+ if url:
+ play_from.append(from_flag or '线路①')
+ play_url_list.append(f'播放${url}')
+
+ vod = {
+ 'vod_id': vid,
+ 'vod_name': title,
+ 'vod_pic': pic,
+ 'type_name': vod_class,
+ 'vod_year': year,
+ 'vod_area': area,
+ 'vod_lang': lang,
+ 'vod_remarks': '',
+ 'vod_actor': actor,
+ 'vod_director': director,
+ 'vod_content': desc,
+ 'vod_play_from': '$$$'.join(play_from),
+ 'vod_play_url': '$$$'.join(play_url_list)
+ }
+ result['list'].append(vod)
+ return result
+
+ def playerContent(self, flag, id, vipFlags):
+ result = {}
+ try:
+ # id 格式: /aey/119317/1-1.html
+ play_url = id
+ if not play_url.startswith('http'):
+ play_url = self.site + play_url
+
+ html = self._get(play_url)
+ m = re.search(r'var player_aaaa=(\{[^<]+\})', html)
+ if m:
+ pd = json.loads(m.group(1))
+ url = pd.get('url', '')
+ if url:
+ result['parse'] = 0
+ result['url'] = url
+ result['jx'] = 0
+ result['header'] = {
+ 'User-Agent': self.ua,
+ 'Referer': self.site + '/'
+ }
+ return result
+
+ # 备用:嗅探
+ result['parse'] = 1
+ result['url'] = play_url
+ result['jx'] = 0
+ result['header'] = {
+ 'User-Agent': self.ua,
+ 'Referer': self.site + '/'
+ }
+ except Exception as e:
+ print(f'playerContent error: {e}')
+
+ if not result:
+ result = {'parse': 1, 'url': '', 'jx': 0, 'header': {}}
+ return result
+
+ def searchContent(self, key, quick, pg='1'):
+ result = {'list': [], 'parse': 0, 'jx': 0}
+ wd = requests.utils.quote(key)
+ url = f'{self.site}/search/-------------.html?wd={wd}'
+ html = self._get(url)
+ if html:
+ result['list'] = self._extract_list(html)
+ return result
+
+ def localProxy(self, params):
+ return [200, "video/MP2T", {}, ""]
diff --git a/yinshiyuan18/tv/py/熊猫视频.py b/yinshiyuan18/tv/py/熊猫视频.py
new file mode 100644
index 00000000..843aa642
--- /dev/null
+++ b/yinshiyuan18/tv/py/熊猫视频.py
@@ -0,0 +1,253 @@
+# coding=utf-8
+# !/usr/bin/python
+import sys
+import requests
+from bs4 import BeautifulSoup
+import re
+from base.spider import Spider
+import json
+sys.path.append('..')
+xurl = "https://ee55ff.com/video.html"
+headerx = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36'
+}
+class Spider(Spider):
+ global xurl
+ global headerx
+
+ def getName(self):
+ return "首页"
+
+ def init(self, extend):
+ pass
+
+ def isVideoFormat(self, url):
+ pass
+
+ def manualVideoCheck(self):
+ pass
+
+ def homeContent(self, filter):
+ # https://yaselulu.autos/?page_id=9
+
+ data = {"name": "John", "age": 31, "city": "New York"}
+ res = requests.post('https://spiderscloudcn2.51111666.com/getDataInit', headers=headerx, json=data)
+ res.encoding = "utf-8"
+ json_dict = json.loads(res.text)
+ menu0ListMap = json_dict["data"]["menu0ListMap"]
+ result = {}
+ result['class'] = []
+ for item in menu0ListMap:
+ if item['typeName'] == "传媒" or item['typeName'] == "视频" or item['typeName'] == "电影":
+ for item1 in item['menu2List']:
+ result['class'].append({'type_id': item1['typeId2'], 'type_name': item1['typeName2']})
+
+ return result
+
+ def homeVideoContent(self):
+ videos = []
+ try:
+ data = {
+ "command": "WEB_GET_INFO",
+ "pageNumber": 1,
+ "RecordsPage": 20,
+ "typeId": "24",
+ "typeMid": "1",
+ "languageType": "CN",
+ "content": ""
+ }
+ res = requests.post('https://spiderscloudcn2.51111666.com/forward', headers=headerx, json=data)
+ res.encoding = "utf-8"
+ json_dict = json.loads(res.text)
+ menu0ListMap = json_dict["data"]["resultList"]
+ for item in menu0ListMap:
+ name1 = item['vod_name'].replace("yy8ycom", "")
+ pattern = r'(.*?)-(.*?)-\d+\s+'
+ name = re.sub(pattern, '', name1)
+ id = item['id']
+ pic = item['vod_pic']
+ id2 = item['vod_server_id']
+
+ video = {
+ "vod_id": str(id) + '#' + str(id2),
+ "vod_name": name,
+ "vod_pic": pic,
+ "vod_remarks": ''
+ }
+ videos.append(video)
+ result = {'list': videos}
+ return result
+ except:
+ pass
+
+ def categoryContent(self, cid, pg, filter, ext):
+ result = {}
+ videos = []
+ if not pg:
+ pg = 1
+
+ # https://yaselulu.autos/?cat=3754&paged=1
+
+ videos = []
+ try:
+ data = {
+ "command": "WEB_GET_INFO",
+ "pageNumber": pg,
+ "RecordsPage": 20,
+ "typeId": cid,
+ "typeMid": "1",
+ "languageType": "CN",
+ "content": ""
+ }
+ res = requests.post('https://spiderscloudcn2.51111666.com/forward', headers=headerx, json=data)
+ res.encoding = "utf-8"
+ json_dict = json.loads(res.text)
+ menu0ListMap = json_dict["data"]["resultList"]
+ for item in menu0ListMap:
+ name1 = item['vod_name'].replace("yy8ycom", "")
+ pattern = r'(.*?)-(.*?)-\d+\s+'
+ name = re.sub(pattern, '', name1)
+ id = item['id']
+ pic = item['vod_pic']
+ id2 = item['vod_server_id']
+
+ video = {
+ "vod_id": str(id) + '#' + str(id2),
+ "vod_name": name,
+ "vod_pic": pic,
+ "vod_remarks": ''
+ }
+ videos.append(video)
+ except:
+ pass
+
+ result['list'] = videos
+ result['page'] = pg
+ result['pagecount'] = 9999
+ result['limit'] = 90
+ result['total'] = 999999
+ return result
+
+ def detailContent(self, ids):
+ data2 = {"name": "John", "age": 31, "city": "New York"}
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36',
+ 'Content-Type': 'application/json'
+ }
+ res1 = requests.post('https://spiderscloudcn2.51111666.com/getDataInit', headers=headers, json=data2)
+
+ js1 = json.loads(res1.text)
+
+ did = ids[0]
+ cid, svid = did.split("#")
+ videos = []
+ result = {}
+ data = {
+ "command": "WEB_GET_INFO_DETAIL",
+ "type_Mid": "1",
+ "id": cid,
+ "languageType": "CN"
+ }
+ res = requests.post('https://spiderscloudcn2.51111666.com/forward', headers=headerx, json=data)
+ res.encoding = "utf-8"
+
+ json_dict = json.loads(res.text)
+ if svid:
+ purl = js1['data']['macVodLinkMap'][svid]['LINK_2'] + json_dict['data']["result"]["vod_url"]
+ else:
+ purl = json_dict['data']["result"]["vod_url"]
+
+ videos.append({
+ "vod_id": '',
+ "vod_name": '',
+ "vod_pic": "",
+ "type_name": "ぃぅおか🍬 คิดถึง",
+ "vod_year": "",
+ "vod_area": "",
+ "vod_remarks": "",
+ "vod_actor": "",
+ "vod_director": "",
+ "vod_content": "",
+ "vod_play_from": "直链播放",
+ "vod_play_url": purl
+ })
+
+ result['list'] = videos
+ return result
+
+ def playerContent(self, flag, id, vipFlags):
+ result = {}
+ result["parse"] = 0
+ result["playUrl"] = ''
+ result["url"] = id
+ result["header"] = headerx
+ return result
+
+ def searchContentPage(self, key, quick, page):
+ # https://yaselulu.autos/?s=%E6%88%91%E7%9A%84&paged=2
+
+ result = {}
+ videos = []
+ if not page:
+ page = 1
+
+ data = {
+ "command": "WEB_GET_INFO",
+ "pageNumber": page,
+ "RecordsPage": 20,
+ "typeId": "0",
+ "typeMid": "1",
+ "languageType": "CN",
+ "content": key,
+ "type": "1"
+ }
+ res = requests.post('https://spiderscloudcn2.51111666.com/forward', headers=headerx, json=data)
+ res.encoding = "utf-8"
+ json_dict = json.loads(res.text)
+ menu0ListMap = json_dict["data"]["resultList"]
+ for item in menu0ListMap:
+ name = item['vod_name'].replace("yy8ycom", "")
+ id = item['id']
+ pic = item['vod_pic']
+ id2 = item['vod_server_id']
+
+ video = {
+ "vod_id": str(id) + '#' + str(id2),
+ "vod_name": name,
+ "vod_pic": pic,
+ "vod_remarks": ''
+ }
+ videos.append(video)
+
+ result['list'] = videos
+ result['page'] = page
+ result['pagecount'] = 9999
+ result['limit'] = 90
+ result['total'] = 999999
+ return result
+ def searchContent(self, key, quick):
+ return self.searchContentPage(key, quick, '1')
+
+
+
+ def localProxy(self, params):
+ if params['type'] == "m3u8":
+ return self.proxyM3u8(params)
+ elif params['type'] == "media":
+ return self.proxyMedia(params)
+ elif params['type'] == "ts":
+ return self.proxyTs(params)
+ return None
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/yinshiyuan18/tv/py/琉璃神社2.py b/yinshiyuan18/tv/py/琉璃神社2.py
new file mode 100644
index 00000000..8b8270ad
--- /dev/null
+++ b/yinshiyuan18/tv/py/琉璃神社2.py
@@ -0,0 +1,320 @@
+# coding=utf-8
+import re
+import sys
+import time
+import random
+from urllib.parse import quote, urljoin
+
+from base.spider import Spider as BaseSpider
+
+sys.path.append("..")
+
+
+class Spider(BaseSpider):
+ def __init__(self):
+ self.name = "琉璃神社"
+ self.host = "https://www.hacg.icu"
+ self.backend_parse = True
+ self.headers = {
+ "User-Agent": (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/122.0.6261.95 Safari/537.36"
+ ),
+ "Accept-Language": "zh-CN,zh;q=0.9",
+ "Cookie": "existmag=mag; dv=1; age=verified",
+ }
+ # 分类列表(对应顶部导航)
+ self.categories = [
+ {"type_id": "latest", "type_name": "最新更新"},
+ {"type_id": "anime", "type_name": "动画"},
+ {"type_id": "comic", "type_name": "漫画"},
+ {"type_id": "game", "type_name": "游戏"},
+ {"type_id": "other", "type_name": "其他"},
+ {"type_id": "goods", "type_name": "周边"},
+ {"type_id": "op", "type_name": "音乐"},
+ {"type_id": "book", "type_name": "轻小说"},
+ ]
+ # 分类 URL 映射
+ self.category_paths = {
+ "latest": "/wp/",
+ "anime": "/wp/anime.html",
+ "comic": "/wp/comic.html",
+ "game": "/wp/game.html",
+ "other": "/wp/other.html",
+ "goods": "/wp/goods.html",
+ "op": "/wp/op.html",
+ "book": "/wp/book.html",
+ }
+
+ # ---------- 工具方法 ----------
+ @staticmethod
+ def _clean(s):
+ return re.sub(r"\s+", " ", str(s or "")).strip()
+
+ def _build_url(self, path):
+ if not path:
+ return self.host
+ if str(path).startswith("http"):
+ return path
+ return urljoin(self.host.rstrip("/") + "/", str(path).strip().lstrip("/"))
+
+ @staticmethod
+ def _extract_hash(text):
+ if not text:
+ return None
+ match = re.search(r'([a-fA-F0-9]{40})', text, re.I)
+ return match.group(1).lower() if match else None
+
+ @staticmethod
+ def _extract_file_size(text):
+ if not text:
+ return ""
+ match = re.search(r'([\d.]+)\s*(TB|GB|MB|KB)', text, re.I)
+ return f"{match.group(1)}{match.group(2).upper()}" if match else ""
+
+ @staticmethod
+ def _parse_size_bytes(size_str):
+ if not size_str:
+ return 0
+ match = re.search(r'([\d.]+)\s*(TB|GB|MB|KB)', size_str, re.I)
+ if not match:
+ return 0
+ val = float(match.group(1))
+ unit = match.group(2).upper()
+ multipliers = {"KB": 1024, "MB": 1048576, "GB": 1073741824, "TB": 1099511627776}
+ return int(val * multipliers.get(unit, 1))
+
+ # ---------- HTTP 请求 ----------
+ def _request(self, url):
+ target = self._build_url(url)
+ try:
+ rsp = self.fetch(target, headers=self.headers, timeout=15, verify=False, allow_redirects=True)
+ return rsp.text or ""
+ except Exception:
+ return ""
+
+ # ---------- 解析列表(带封面图) ----------
+ def _parse_article_list(self, html):
+ if not html:
+ return []
+ root = self.html(html)
+ if root is None:
+ return []
+
+ items = []
+ seen = set()
+
+ for article in root.xpath("//article"):
+ # 标题 & 链接
+ title_node = article.xpath(".//h1[@class='entry-title']/a | .//h1[contains(@class,'entry-title')]/a")
+ if not title_node:
+ continue
+ link = self._clean("".join(title_node[0].xpath("./@href")))
+ title = self._clean("".join(title_node[0].xpath(".//text()")))
+ if not link or not title:
+ continue
+
+ vid = link.strip("/").split("/")[-1].replace(".html", "")
+ if not vid or vid in seen:
+ continue
+ seen.add(vid)
+
+ # 封面图:从 entry-content 中取第一张图
+ pic = ""
+ img_nodes = article.xpath(".//div[@class='entry-content']//img[1]/@src")
+ if not img_nodes:
+ img_nodes = article.xpath(".//img[1]/@src")
+ if img_nodes:
+ pic = self._build_url(self._clean(img_nodes[0]))
+
+ # 摘要
+ excerpt = ""
+ content_div = article.xpath(".//div[@class='entry-content']")
+ if content_div:
+ excerpt = self._clean("".join(content_div[0].xpath(".//text()")))[:100]
+
+ # 评论数
+ comments = article.xpath(".//div[@class='comments-link']/a/text()")
+ comment_count = self._clean("".join(comments)) if comments else ""
+
+ items.append({
+ "vod_id": vid,
+ "vod_name": title,
+ "vod_pic": pic,
+ "vod_remarks": comment_count,
+ "vod_content": excerpt,
+ })
+
+ return items
+
+ def _has_next_page(self, html):
+ if not html:
+ return False
+ return bool(re.search(r'
]*class="nextpostslink"[^>]*>', html)) or \
+ bool(re.search(r']*rel="next"[^>]*>', html))
+
+ def _get_page_url(self, base_path, page):
+ if page <= 1:
+ return base_path
+ # 处理分页格式
+ if base_path.endswith(".html"):
+ base = base_path[:-5] # 去掉 .html
+ else:
+ base = base_path.rstrip("/")
+ return f"{base}/page/{page}"
+
+ # ---------- 接口方法 ----------
+ def init(self, extend=""):
+ return None
+
+ def getName(self):
+ return self.name
+
+ def danmaku(self):
+ return False
+
+ def homeContent(self, filter):
+ return {"class": self.categories}
+
+ def homeVideoContent(self):
+ try:
+ html = self._request("/wp/")
+ items = self._parse_article_list(html)[:24]
+ except Exception:
+ items = []
+ return {"list": items}
+
+ def categoryContent(self, tid, pg, filter, extend):
+ page = int(pg) or 1
+ base_path = self.category_paths.get(tid, "/wp/")
+ url = self._build_url(self._get_page_url(base_path, page))
+ html = self._request(url)
+ items = self._parse_article_list(html) if html else []
+ has_next = self._has_next_page(html) if html else False
+
+ return {
+ "page": page,
+ "pagecount": page + 1 if has_next else page,
+ "limit": len(items),
+ "total": 9999,
+ "list": items,
+ }
+
+ def searchContent(self, key, quick, pg=1, category=""):
+ keyword = self._clean(key)
+ if not keyword:
+ return {"list": [], "page": 1, "pagecount": 1, "total": 0}
+
+ page = int(pg) or 1
+ if page <= 1:
+ url = self._build_url(f"/wp/?s={quote(keyword)}")
+ else:
+ url = self._build_url(f"/wp/page/{page}?s={quote(keyword)}")
+
+ html = self._request(url)
+ items = self._parse_article_list(html) if html else []
+ has_next = self._has_next_page(html) if html else False
+
+ return {
+ "list": items,
+ "page": page,
+ "pagecount": page + 1 if has_next else page,
+ "total": 9999,
+ }
+
+ def detailContent(self, ids):
+ result = {"list": []}
+
+ for raw_id in ids:
+ vid = str(raw_id or "").strip()
+ if not vid:
+ continue
+
+ if vid.endswith(".html"):
+ detail_url = self._build_url(f"/wp/{vid}")
+ else:
+ detail_url = self._build_url(f"/wp/{vid}.html")
+
+ html = self._request(detail_url)
+ if not html:
+ continue
+
+ root = self.html(html)
+ if root is None:
+ continue
+
+ # 标题
+ title = vid
+ title_nodes = root.xpath("//h1[@class='entry-title']//text()")
+ if title_nodes:
+ title = self._clean("".join(title_nodes))
+
+ # 封面图
+ pic = ""
+ img_nodes = root.xpath("//div[@class='entry-content']//img[1]/@src")
+ if img_nodes:
+ pic = self._build_url(self._clean(img_nodes[0]))
+
+ # 正文内容(用于提取哈希和大小)
+ content = ""
+ content_div = root.xpath("//div[@class='entry-content']")
+ if content_div:
+ content = self._clean(content_div[0].xpath("string(.)"))
+
+ # === 提取磁力链(40位哈希) ===
+ magnets = []
+ seen_hashes = set()
+
+ # 提取所有40位hex
+ all_hashes = re.findall(r'([a-fA-F0-9]{40})', content, re.I)
+ for h in all_hashes:
+ h_lower = h.lower()
+ if h_lower in seen_hashes:
+ continue
+ seen_hashes.add(h_lower)
+ size_str = self._extract_file_size(content)
+ magnet_url = f"magnet:?xt=urn:btih:{h_lower}"
+ magnets.append({
+ "magnet": magnet_url,
+ "hash": h_lower,
+ "size": self._parse_size_bytes(size_str),
+ "size_label": size_str,
+ })
+
+ # 按大小排序(大的在前)
+ magnets.sort(key=lambda x: x["size"], reverse=True)
+
+ # 构建播放列表
+ play_from = []
+ play_url = []
+ for idx, mag in enumerate(magnets[:50]):
+ label = mag["hash"][:8] + "..."
+ if mag["size_label"]:
+ label += f" [{mag['size_label']}]"
+ play_from.append(label)
+ play_url.append(f"{label}${mag['magnet']}")
+
+ if not magnets:
+ play_from = ["无磁力链接"]
+ play_url = ["无磁力链接$"]
+
+ result["list"].append({
+ "vod_id": vid,
+ "vod_name": title,
+ "vod_pic": pic,
+ "type_name": "琉璃神社",
+ "vod_content": content[:500] if content else title,
+ "vod_play_from": "$$$".join(play_from),
+ "vod_play_url": "$$$".join(play_url),
+ })
+
+ return result
+
+ def playerContent(self, flag, id, vipFlags):
+ url = str(id or "").strip()
+ if url.startswith("magnet:?"):
+ return {"parse": 0, "jx": 0, "playUrl": "", "url": url, "header": {}}
+ if url.startswith("http"):
+ return {"parse": 0, "jx": 0, "playUrl": "", "url": url, "header": {}}
+ return {"parse": 0, "jx": 0, "playUrl": "", "url": "", "header": {}}
\ No newline at end of file
diff --git a/yinshiyuan18/tv/py/萝莉AV.py b/yinshiyuan18/tv/py/萝莉AV.py
new file mode 100644
index 00000000..4b018a71
--- /dev/null
+++ b/yinshiyuan18/tv/py/萝莉AV.py
@@ -0,0 +1,350 @@
+# coding=utf-8
+# 文件名: test.py
+# 描述: 萝莉AV爬虫 - 修正版
+
+from base.spider import Spider
+import re
+import json
+import urllib.parse
+
+class Spider(Spider):
+ def getName(self):
+ return "萝莉AV"
+
+ def init(self, extend=""):
+ self.host = "https://202607.4kck.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.host,
+ '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.classes = [
+ {"type_name": "国产精选", "type_id": "1"},
+ {"type_name": "日韩AV", "type_id": "2"},
+ {"type_name": "蓝光超清", "type_id": "3"},
+ {"type_name": "欧美精品", "type_id": "4"},
+ {"type_name": "异族风情", "type_id": "5"},
+ {"type_name": "动漫专区", "type_id": "6"},
+ ]
+
+ def homeContent(self, filter):
+ """返回分类列表"""
+ result = {}
+ result["class"] = self.classes
+ return result
+
+ def homeVideoContent(self):
+ """首页推荐视频"""
+ try:
+ url = f"{self.host}/index.html"
+ rsp = self.fetch(url, headers=self.headers)
+ html = rsp.text
+
+ videos = self._parse_video_list(html)
+ print(f"首页解析到 {len(videos)} 个视频")
+
+ return {"list": videos}
+ except Exception as e:
+ print(f"首页视频解析错误: {e}")
+ return {"list": []}
+
+ def categoryContent(self, tid, pg, filter, extend):
+ """分类页面内容"""
+ try:
+ # 构建分类URL
+ url = f"{self.host}/list.php?cid={tid}&page={pg}"
+ print(f"分类页面URL: {url}")
+
+ rsp = self.fetch(url, headers=self.headers)
+ html = rsp.text
+
+ # 解析视频列表
+ videos = self._parse_video_list(html)
+
+ # 解析分页信息
+ pagecount = int(pg)
+ # 查找分页链接
+ pagination_pattern = r'(\d+)'
+ page_matches = re.findall(pagination_pattern, html)
+ if page_matches:
+ pages = [int(p[1]) for p in page_matches]
+ if pages:
+ pagecount = max(pages)
+ else:
+ # 如果找不到分页,假设有3页
+ pagecount = int(pg) + 2
+
+ print(f"分类解析完成: 第 {pg} 页,共 {pagecount} 页,找到 {len(videos)} 个视频")
+
+ return {
+ "list": videos,
+ "page": int(pg),
+ "pagecount": pagecount,
+ "limit": 30,
+ "total": len(videos) * pagecount
+ }
+ except Exception as e:
+ print(f"分类页面解析错误: {e}")
+ return {
+ "list": [],
+ "page": int(pg),
+ "pagecount": 1,
+ "limit": 30,
+ "total": 0
+ }
+
+ def _parse_video_list(self, html):
+ """解析视频列表的通用方法"""
+ videos = []
+
+ # 找到所有视频项
+ # 模式:
...
+ pattern = r'
]*class="[^"]*group[^"]*item[^"]*"[^>]*>(.*?)
\s*