Files
2026-08-01 10:20:01 +00:00

226 lines
9.6 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
import re, json, base64, requests, urllib.parse
from lxml import etree
from base.spider import Spider
class Spider(Spider):
def getName(self):
return "番茄动漫"
def init(self, extend=""):
self.host = "https://www.fqdm.cc"
self.ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
self.headers = {"User-Agent": self.ua, "Referer": self.host + "/"}
self.playHeaders = {"User-Agent": self.ua, "Referer": self.host + "/", "Accept": "*/*"}
self.categories = [
{"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": "动漫综合"},
]
self.pageCache = {}
def _get(self, url):
try:
r = requests.get(url, headers=self.headers, timeout=15)
r.encoding = "utf-8"
return r.text
except:
return None
def _fix(self, u):
if not u:
return ""
if u.startswith("//"):
return "https:" + u
return self.host + u if u.startswith("/") else u
def _first(self, node, xpaths):
for xp in xpaths:
for v in node.xpath(xp):
v = v.strip() if isinstance(v, str) else v
if v and "load.gif" not in v and "errorpic" not in v:
return v
return ""
def _text(self, node):
return re.sub(r"\s+", "", "".join(node.xpath(".//text()[not(parent::small)]")))
def _cls(self, name):
return f'contains(concat(" ", normalize-space(@class), " "), " {name} ")'
def _parseList(self, html):
if not html:
return []
tree = etree.HTML(html)
items, seen = [], set()
for a in tree.xpath('//a[contains(@href,"/vod/detail/id/")]'):
m = re.search(r"/detail/id/(\d+)", a.get("href", ""))
if not m or m.group(1) in seen:
continue
txt = re.sub(r"\s+", " ", "".join(a.xpath(".//text()"))).strip()
name = (a.get("title") or "").strip()
if not name:
name = self._first(a, [
'.//strong//text()',
'following-sibling::*//*[contains(@class,"module-card-item-title")]//text()',
])
if not name:
name = txt
if not name or a.xpath("ancestor::h1|ancestor::h2"):
continue
seen.add(m.group(1))
pic = self._first(a, [
'.//img/@data-original', './/img/@data-src', './/img/@src',
'ancestor::*[position()<=2][not(self::body or self::html)]//img/@data-original',
'ancestor::*[position()<=2][not(self::body or self::html)]//img/@data-src',
'ancestor::*[position()<=2][not(self::body or self::html)]//img/@src',
])
remark = self._first(a, ['.//div[contains(@class,"module-item-note")]/text()'])
if not remark:
remark = re.sub(r"豆瓣:[\d.]+分|^\d+\s*", "", txt.replace(name, "")).strip()
items.append({"vod_id": m.group(1), "vod_name": name, "vod_pic": self._fix(pic), "vod_remarks": remark})
return items
def homeContent(self, filter):
return {"class": self.categories, "list": self._parseList(self._get(self.host + "/")), "filters": {}}
def homeVideoContent(self):
return {"list": self._parseList(self._get(self.host + "/"))}
def categoryContent(self, tid, pg, filter, extend):
pg = int(pg) if str(pg).isdigit() else 1
url = f"{self.host}/index.php/vod/type/id/{tid}.html" if pg == 1 else f"{self.host}/index.php/vod/type/id/{tid}/page/{pg}.html"
vodList = self._parseList(self._get(url))
sign = ",".join(v["vod_id"] for v in vodList)
if pg == 1:
self.pageCache[str(tid)] = sign
elif sign and sign == self.pageCache.get(str(tid)):
vodList = []
return {"page": pg, "pagecount": pg + 1 if vodList else pg, "limit": len(vodList) or 20, "total": 999, "list": vodList}
def _episodes(self, tree):
nodes = tree.xpath(f'//a[{self._cls("module-play-list-link")}]')
if not nodes:
nodes = [a for a in tree.xpath('//a[contains(@href,"/vod/play/id/")]')
if not re.search(r"tab-item|swiper-slide", a.get("class") or "")]
sidOrder, playMap, seen = [], {}, set()
for a in nodes:
m = re.search(r"/play/id/\d+/sid/(\d+)/nid/(\d+)", a.get("href", ""))
if not m:
continue
sid, nid = m.group(1), m.group(2)
if (sid, nid) in seen:
continue
title = re.sub(r"\s+", "", "".join(a.xpath(".//span/text()"))) or self._text(a)
if not title or "立即播放" in title or "立刻播放" in title:
continue
seen.add((sid, nid))
if sid not in playMap:
playMap[sid] = []
sidOrder.append(sid)
playMap[sid].append(f"{title}${self._fix(a.get('href', ''))}")
return sidOrder, playMap
def _sourceNames(self, tree, sidOrder):
tabs = tree.xpath(f'//*[{self._cls("module-tab-item")} and {self._cls("tab-item")}]') or tree.xpath(f'//*[{self._cls("tab-item")}]')
nameMap, noHref = {}, []
for t in tabs:
txt = self._text(t)
if not txt:
continue
m = re.search(r"/sid/(\d+)/", t.get("href", "") or "")
if m:
nameMap.setdefault(m.group(1), txt)
elif txt not in noHref:
noHref.append(txt)
rest = [s for s in sidOrder if s not in nameMap]
for i, s in enumerate(rest):
if i < len(noHref):
nameMap[s] = noHref[i]
return [nameMap.get(s) or f"线路{s}" for s in sidOrder]
def detailContent(self, ids):
vid = str(ids[0]).split("/")[-1].replace(".html", "")
html = self._get(f"{self.host}/index.php/vod/detail/id/{vid}.html")
if not html:
return {"list": []}
tree = etree.HTML(html)
name = re.sub(r"\s+", " ", "".join(tree.xpath("//h1//text()"))).strip()
pic = (re.search(r"vod_pic\s*=\s*'([^']+)'", html) or re.search(r"vod_image\s*=\s*'([^']+)'", html))
pic = pic.group(1) if pic else self._first(tree, [
'//div[contains(@class,"module-item-pic")]//img/@data-original',
'//img[contains(@class,"lazyload")]/@data-original',
'//meta[@property="og:image"]/@content',
])
desc = ""
m = re.search(r"vod_content\s*=\s*'([^']+)'", html)
if m:
try:
desc = base64.b64decode(m.group(1)).decode("utf-8", "ignore").strip()
except:
desc = ""
if not desc:
desc = self._first(tree, ['//meta[@name="description"]/@content'])
year = self._first(tree, ['//div[contains(@class,"module-info-tag-link")]/a[contains(@href,"/year/")]/text()'])
area = self._first(tree, ['//div[contains(@class,"module-info-tag-link")]/a[contains(@href,"/area/")]/text()'])
sidOrder, playMap = self._episodes(tree)
froms = self._sourceNames(tree, sidOrder)
urls = ["#".join(playMap[s]) for s in sidOrder]
return {"list": [{
"vod_id": vid,
"vod_name": name,
"vod_pic": self._fix(pic),
"vod_year": year,
"vod_area": area,
"vod_content": desc,
"vod_play_from": "$$$".join(froms),
"vod_play_url": "$$$".join(urls),
}]}
def searchContent(self, key, quick, pg="1"):
pg = int(pg) if str(pg).isdigit() else 1
url = f"{self.host}/index.php/vod/search/page/{pg}/wd/{urllib.parse.quote(key)}.html"
return {"list": self._parseList(self._get(url)), "page": pg}
def _play_headers(self, u):
hdrs = dict(self.playHeaders)
try:
if str(u).startswith("http"):
p = urllib.parse.urlparse(u)
if p.netloc:
hdrs["Referer"] = f"{p.scheme}://{p.netloc}/"
except Exception:
pass
return hdrs
def playerContent(self, flag, id, vipFlags):
url = id if str(id).startswith("http") else self.host + str(id)
html = self._get(url) or ""
idx = html.find("player_aaaa")
seg = html[idx:idx + 4000] if idx >= 0 else ""
playUrl, enc = "", "0"
m = re.search(r"player_aaaa\s*=\s*(\{.*?\})\s*</script>", seg, re.S) or re.search(r"player_aaaa\s*=\s*(\{.*\})", seg)
if m:
try:
cfg = json.loads(m.group(1))
playUrl, enc = cfg.get("url", ""), str(cfg.get("encrypt", "0"))
except:
playUrl = ""
if not playUrl:
m2 = re.search(r'"url"\s*:\s*"(.*?)"', seg)
playUrl = m2.group(1).replace("\\/", "/") if m2 else ""
try:
if enc == "1":
playUrl = urllib.parse.unquote(playUrl)
elif enc == "2":
playUrl = urllib.parse.unquote(base64.b64decode(playUrl).decode("utf-8"))
except:
pass
if playUrl and re.search(r"\.(m3u8|mp4|flv|mkv|ts)(\?|$)", playUrl.split("#")[0], re.I):
return {"parse": 0, "url": playUrl, "header": self._play_headers(playUrl)}
return {"parse": 1, "url": playUrl if str(playUrl).startswith("http") else url, "header": self._play_headers(playUrl)}