From 7f5b38ed4b61efb67206cbc36bb00a3f25ff5c80 Mon Sep 17 00:00:00 2001
From: XC-Pro <19840+xc-pro@noreply.example.org>
Date: Sat, 15 Aug 2026 10:30:50 +0200
Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?=
=?UTF-8?q?=E3=80=8Cpy=E3=80=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
py/优夏.js | 469 ++++++++++++++++++++++++++++++++++++++++++++++++
py/央库云.py | 476 +++++++++++++++++++++++++++++++++++++++++++++++++
py/央视影视.py | 242 +++++++++++++++++++++++++
py/山楂影视.py | 350 ++++++++++++++++++++++++++++++++++++
py/西瓜影视.py | 336 ++++++++++++++++++++++++++++++++++
5 files changed, 1873 insertions(+)
create mode 100644 py/优夏.js
create mode 100644 py/央库云.py
create mode 100644 py/央视影视.py
create mode 100644 py/山楂影视.py
create mode 100644 py/西瓜影视.py
diff --git a/py/优夏.js b/py/优夏.js
new file mode 100644
index 0000000..16eccdb
--- /dev/null
+++ b/py/优夏.js
@@ -0,0 +1,469 @@
+import cheerio from 'assets://js/lib/cheerio.min.js';
+
+const appConfig = {
+ siteName: "伦理专题影片",
+ siteUrl: "http://yoxayg.com"
+};
+const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
+
+async function init(ext) {
+ console.log("初始化爬虫:", appConfig.siteName);
+}
+
+const classList = [
+ { type_id: "1", type_name: "电影" },
+ { type_id: "2", type_name: "电视剧" },
+ { type_id: "3", type_name: "综艺" },
+ { type_id: "4", type_name: "动漫" },
+ { type_id: "36", type_name: "短剧" },
+ { type_id: "6", type_name: "动作片" },
+ { type_id: "7", type_name: "喜剧片" },
+ { type_id: "8", type_name: "爱情片" },
+ { type_id: "9", type_name: "科幻片" },
+ { type_id: "10", type_name: "恐怖片" },
+ { type_id: "11", type_name: "剧情片" },
+ { type_id: "12", type_name: "战争片" },
+ { type_id: "20", type_name: "记录片" },
+ { type_id: "34", type_name: "伦理片" },
+ { type_id: "13", type_name: "国产剧" },
+ { type_id: "14", type_name: "香港剧" },
+ { type_id: "15", type_name: "韩国剧" },
+ { type_id: "16", type_name: "欧美剧" },
+ { type_id: "21", type_name: "台湾剧" },
+ { type_id: "22", type_name: "日本剧" },
+ { type_id: "23", type_name: "海外剧" },
+ { type_id: "24", type_name: "泰国剧" },
+ { type_id: "25", type_name: "大陆综艺" },
+ { type_id: "26", type_name: "港台综艺" },
+ { type_id: "27", type_name: "日韩综艺" },
+ { type_id: "28", type_name: "欧美综艺" },
+ { type_id: "29", type_name: "国产动漫" },
+ { type_id: "30", type_name: "日韩动漫" },
+ { type_id: "31", type_name: "欧美动漫" },
+ { type_id: "32", type_name: "港台动漫" },
+ { type_id: "33", type_name: "海外动漫" }
+];
+
+const typeSlugMap = {
+ "1": "dianying", "2": "dianshiju", "3": "zongyi", "4": "dongman", "6": "dongzuopian",
+ "7": "xijupian", "8": "aiqingpian", "9": "kehuanpian", "10": "kongbupian", "11": "juqingpian",
+ "12": "zhanzhengpian", "13": "guochanju", "14": "xianggangju", "15": "hanguoju", "16": "oumeiju",
+ "20": "jilupian", "21": "taiwanju", "22": "ribenju", "23": "haiwaiju", "24": "taiguoju",
+ "25": "daluzongyi", "26": "gangtaizongyi", "27": "rihanzongyi", "28": "oumeizongyi",
+ "29": "guochandongman", "30": "rihandongman", "31": "oumeidongman", "32": "gangtaidongman",
+ "33": "haiwaidongman", "34": "lunlipian", "36": "duanju"
+};
+
+const typeParentMap = {
+ "1": 0, "2": 0, "3": 0, "4": 0, "6": 1, "7": 1, "8": 1, "9": 1, "10": 1,
+ "11": 1, "12": 1, "13": 2, "14": 2, "15": 2, "16": 2, "20": 1, "21": 2, "22": 2,
+ "23": 2, "24": 2, "25": 3, "26": 3, "27": 3, "28": 3, "29": 4, "30": 4,
+ "31": 4, "32": 4, "33": 4, "34": 1, "36": 2
+};
+
+function topType(tid) {
+ tid = parseInt(tid || 0, 10);
+ if (tid === 36) return 36;
+ const p = parseInt(typeParentMap[String(tid)] || 0, 10);
+ return p > 0 ? p : tid;
+}
+
+function typeSlug(tid) {
+ return typeSlugMap[String(parseInt(tid || 0, 10))] || ('type' + parseInt(tid || 0, 10));
+}
+
+function getAreaFilter() {
+ return {
+ "key": "area", "name": "地区", "value": [
+ { "n": "全部", "v": "" }, { "n": "大陆", "v": "大陆" }, { "n": "香港", "v": "香港" },
+ { "n": "台湾", "v": "台湾" }, { "n": "美国", "v": "美国" }, { "n": "日本", "v": "日本" },
+ { "n": "韩国", "v": "韩国" }, { "n": "英国", "v": "英国" }, { "n": "法国", "v": "法国" },
+ { "n": "德国", "v": "德国" }, { "n": "泰国", "v": "泰国" }, { "n": "印度", "v": "印度" },
+ { "n": "其他", "v": "其他" }
+ ]
+ };
+}
+
+function getYearFilter() {
+ let years = [{ "n": "全部", "v": "" }];
+ const currentYear = new Date().getFullYear();
+ for (let y = currentYear; y >= 2010; y--) {
+ years.push({ "n": String(y), "v": String(y) });
+ }
+ return { "key": "year", "name": "年份", "value": years };
+}
+
+const commonFilters = [getAreaFilter(), getYearFilter()];
+
+const myFilters = {};
+classList.forEach(item => {
+ myFilters[item.type_id] = commonFilters;
+});
+
+function fixUrl(u) {
+ if (!u) return '';
+ if (u.startsWith('http')) return u;
+ if (u.startsWith('//')) return 'http:' + u;
+ if (u.startsWith('/')) return appConfig.siteUrl + u;
+ return u;
+}
+
+async function home(filter) {
+ let list = [];
+ try {
+ const html = (await req(appConfig.siteUrl, {
+ method: "GET",
+ headers: { "User-Agent": UA }
+ })).content;
+ const $ = cheerio.load(html);
+ let seen = {};
+
+ $(".tpl-card[href]").each(function () {
+ let vod_id = $(this).attr("href");
+ if (!vod_id || seen[vod_id]) return;
+
+ let vod_name = $(this).attr("title") || $(this).find("strong").first().text().trim() || "";
+ vod_name = vod_name.replace(/免费在线观看$/, "");
+ let vod_pic = fixUrl($(this).find("img").first().attr("src") || "");
+ let vod_remarks = $(this).find("em").first().text().trim();
+
+ if (vod_name && vod_id) {
+ seen[vod_id] = true;
+ list.push({ vod_id, vod_name, vod_pic, vod_remarks });
+ }
+ });
+ } catch (e) {
+ console.error("首页推荐获取失败:", e.message);
+ }
+
+ return JSON.stringify({
+ class: classList,
+ filters: myFilters,
+ list: list.slice(0, 30)
+ });
+}
+
+function buildCategoryUrl(tid, pg, extend) {
+ extend = extend || {};
+ let top = topType(tid);
+ let topSlug = typeSlug(top);
+ let childSlug = (tid != top) ? typeSlug(tid) : '';
+
+ let path;
+ if (childSlug && tid != 36) {
+ path = `/${topSlug}/${childSlug}/`;
+ } else {
+ path = `/${topSlug}/`;
+ }
+
+ if (pg > 1) {
+ path += `page-${pg}.html`;
+ }
+
+ let area = extend.area || '';
+ let year = extend.year || '';
+ if (area || year) {
+ let params = [];
+ if (area) params.push('area=' + encodeURIComponent(area));
+ if (year) params.push('year=' + encodeURIComponent(year));
+ return appConfig.siteUrl + `/api_proxy.php?ac=list&t=${tid}&pg=${pg}&${params.join('&')}`;
+ }
+
+ return appConfig.siteUrl + path;
+}
+
+function parseListHtml(html) {
+ const $ = cheerio.load(html);
+ let list = [];
+ let vodIds = {};
+
+ $(".tpl-card.card-poster, .tpl-card.card-wide").each(function () {
+ let $a = $(this);
+ let vod_id = $a.attr("href");
+ if (!vod_id || vodIds[vod_id]) return;
+
+ let vod_name = $a.attr("title") || $a.find("strong").first().text().trim() || "";
+ vod_name = vod_name.replace(/免费在线观看$/, "");
+ let vod_pic = fixUrl($a.find("img").first().attr("src") || "");
+ let vod_remarks = $a.find("em").first().text().trim() || $a.find("small").first().text().trim();
+
+ if (vod_name && vod_id) {
+ vodIds[vod_id] = true;
+ list.push({ vod_id, vod_name, vod_pic, vod_remarks });
+ }
+ });
+
+ if (list.length === 0) {
+ try {
+ let data = JSON.parse(html);
+ if (data.list && Array.isArray(data.list)) {
+ data.list.forEach(item => {
+ let vod_id = `/${typeSlug(topType(item.type_id))}/${(item.vod_en || item.vod_name || '').toLowerCase().replace(/[^a-z0-9]+/g, '')}.html`;
+ let vod_name = item.vod_name || "";
+ let vod_pic = item.vod_pic || "";
+ let vod_remarks = item.vod_remarks || "";
+ if (vod_name) {
+ list.push({ vod_id, vod_name, vod_pic, vod_remarks });
+ }
+ });
+ }
+ } catch (e) {
+ }
+ }
+
+ let pagecount = 1;
+ $(".pagination a").each(function () {
+ let href = $(this).attr("href") || '';
+ let m = href.match(/page-(\d+)/);
+ if (m) {
+ let p = parseInt(m[1]);
+ if (p > pagecount) pagecount = p;
+ }
+ });
+
+ return { list, pagecount };
+}
+
+async function category(tid, pg, filter, extend) {
+ pg = pg || 1;
+ extend = extend || {};
+
+ let url = buildCategoryUrl(tid, pg, extend);
+
+ try {
+ const html = (await req(url, {
+ method: "GET",
+ headers: { "User-Agent": UA, "Referer": appConfig.siteUrl }
+ })).content;
+ const result = parseListHtml(html);
+ return JSON.stringify(result);
+ } catch (e) {
+ console.error("分类列表获取失败:", e.message);
+ return JSON.stringify({ list: [], pagecount: 0 });
+ }
+}
+
+async function search(wd, quick, page) {
+ page = page || 1;
+ try {
+ const url = `${appConfig.siteUrl}/search-${encodeURIComponent(wd)}${page > 1 ? '-page-' + page : ''}.html`;
+ const html = (await req(url, {
+ method: "GET",
+ headers: { "User-Agent": UA, "Referer": appConfig.siteUrl }
+ })).content;
+ const result = parseListHtml(html);
+ return JSON.stringify(result);
+ } catch (e) {
+ console.error("搜索失败:", e.message);
+ return JSON.stringify({ list: [], pagecount: 0 });
+ }
+}
+
+async function detail(id) {
+ try {
+ const html = (await req(appConfig.siteUrl + id, {
+ method: "GET",
+ headers: { "User-Agent": UA, "Referer": appConfig.siteUrl }
+ })).content;
+ const $ = cheerio.load(html);
+
+ let vod_name = $("h1").first().text().trim();
+
+ let vod_pic = fixUrl($(".detail-poster img").first().attr("src") || "");
+
+ let vod_director = "";
+ let vod_actor = "";
+ let vod_area = "";
+ let vod_year = "";
+ let vod_content = "";
+ let vod_class = "";
+ let vod_remarks = "";
+
+ $(".detail-tags span").each(function () {
+ let text = $(this).text().trim();
+ if (/^\d{4}$/.test(text) && !vod_year) vod_year = text;
+ else if (/大陆|香港|台湾|美国|日本|韩国|英国|法国|德国|泰国|印度|其他/.test(text) && !vod_area) vod_area = text;
+ else if (!vod_class && text !== vod_year && text !== vod_area && !/更新至|集|期|TC|HD|正片/.test(text)) vod_class = text;
+ });
+
+ vod_remarks = $(".detail-poster span").first().text().trim();
+
+ $(".detail-meta p").each(function () {
+ let text = $(this).text();
+ if (text.includes("主演:") && !vod_actor) {
+ vod_actor = text.replace("主演:", "").trim();
+ }
+ if (text.includes("导演:") && !vod_director) {
+ vod_director = text.replace("导演:", "").trim();
+ }
+ });
+
+ let $intro = $(".detail-intro");
+ if ($intro.length > 0) {
+ vod_content = $intro.first().text().replace("简介:", "").trim();
+ }
+ if (!vod_content) {
+ $intro = $(".article-text");
+ if ($intro.length > 0) {
+ vod_content = $intro.first().text().trim();
+ }
+ }
+
+ let lines = [];
+ let playlists = [];
+ let epArray = [];
+
+ $(".episode-grid a").each(function () {
+ let name = $(this).text().trim();
+ let href = $(this).attr('href') || '';
+ if (name && href) {
+ epArray.push({ name, href });
+ }
+ });
+
+ epArray.sort((a, b) => {
+ let numA = parseInt(a.name.match(/第(\d+)/)?.[1] || 0);
+ let numB = parseInt(b.name.match(/第(\d+)/)?.[1] || 0);
+ return numA - numB;
+ });
+
+ let episodes = epArray.map(ep => `${ep.name}$${ep.href}`);
+
+ if (episodes.length > 0) {
+ lines.push("默认");
+ playlists.push(episodes);
+ } else {
+ let playHref = $(".detail-actions a.btn-primary").attr("href") || "";
+ if (playHref) {
+ lines.push("默认");
+ playlists.push([`正片$${playHref}`]);
+ }
+ }
+
+ if (lines.length === 0) {
+ lines.push("默认");
+ playlists.push([`暂无播放地址$${id}`]);
+ }
+
+ const { vod_play_from, vod_play_url } = buildVodPlayData(lines, playlists);
+
+ return JSON.stringify({
+ list: [{
+ vod_id: id,
+ vod_name,
+ vod_pic,
+ vod_actor,
+ vod_director,
+ vod_remarks,
+ vod_year,
+ vod_area,
+ vod_content,
+ vod_class,
+ vod_play_from,
+ vod_play_url
+ }]
+ });
+ } catch (error) {
+ console.error(`解析详情页异常 [ID: ${id}]:`, error);
+ return JSON.stringify({ list: [] });
+ }
+}
+
+function buildVodPlayData(lines, playlists) {
+ const processedPlaylists = playlists.map(eps => eps.join('#'));
+ return {
+ vod_play_from: lines.filter(Boolean).join('$$$'),
+ vod_play_url: processedPlaylists.join('$$$')
+ };
+}
+
+async function play(flag, id, flags) {
+ try {
+ if (id.startsWith("http")) {
+ return JSON.stringify({
+ parse: 0,
+ Header: { "User-Agent": UA, "Referer": appConfig.siteUrl },
+ url: id
+ });
+ }
+
+ const html = (await req(`${appConfig.siteUrl}${id}`, {
+ method: "GET",
+ headers: { "User-Agent": UA, "Referer": appConfig.siteUrl }
+ })).content;
+
+ // 方式1:从 noscript 中提取 m3u8 URL
+ let noscriptMatch = html.match(/
', html, re.DOTALL)
+ director = ''
+ if director_match:
+ director = re.sub(r'<[^>]+>', '', director_match.group(1)).strip()
+ actor_match = re.search(r'主演:([^<]+)', html)
+ actor = actor_match.group(1).strip() if actor_match else ''
+ desc_match = re.search(r']*>([^<]+)', html)
+ desc = desc_match.group(1).strip() if desc_match else ''
+ play_from = []
+ play_url = []
+ source_tabs = re.findall(r']*>([^<]+)', html)
+ for idx, source_name in enumerate(source_tabs):
+ source_id = idx + 1
+ episodes_match = re.search(
+ f']*>.*?
',
+ html, re.DOTALL
+ )
+ if episodes_match:
+ episodes = re.findall(r'
([^<]+)', episodes_match.group(1))
+ episode_list = []
+ for ep_url, ep_name in episodes:
+ episode_list.append(f'{ep_name}${self.host}{ep_url}')
+ play_from.append(source_name)
+ play_url.append('#'.join(episode_list))
+ vod_play_from = '$$$'.join(play_from) if play_from else '默认'
+ vod_play_url = '$$$'.join(play_url) if play_url else ''
+ vod = {
+ 'vod_id': vod_id,
+ 'vod_name': title,
+ 'vod_pic': pic,
+ 'vod_year': year,
+ 'vod_area': area,
+ 'vod_remarks': remark,
+ 'vod_director': director,
+ 'vod_actor': actor,
+ 'vod_content': desc,
+ 'vod_play_from': vod_play_from,
+ 'vod_play_url': vod_play_url,
+ }
+ return {"list": [vod]}
+ except Exception as e:
+ return {"list": []}
+
+ def searchContent(self, key, quick, pg="1"):
+ pg = int(pg)
+ url = f'{self.host}/index.php/vod/search/wd/{quote(key)}.html'
+ try:
+ r = self.fetch(url, headers=self.headers, timeout=15, verify=False)
+ html = r.text
+ items = re.findall(
+ r'
]*data-original="([^"]*)"',
+ html
+ )
+ videos = []
+ for href, title, pic in items:
+ vod_id = re.search(r'/id/(\d+)\.html', href)
+ if vod_id:
+ vod_id = vod_id.group(1)
+ else:
+ continue
+ remark_match = re.search(r'([^<]+)',
+ html[html.find(href):html.find(href)+500] if href in html else '')
+ remark = remark_match.group(1) if remark_match else ''
+ if pic.startswith('/'):
+ pic = self.host + pic
+ videos.append({
+ 'vod_id': vod_id,
+ 'vod_name': title,
+ 'vod_pic': pic,
+ 'vod_remarks': remark,
+ })
+ return {
+ "list": videos,
+ "page": pg,
+ "pagecount": 9999,
+ "limit": 90,
+ "total": len(videos),
+ }
+ except Exception as e:
+ return {"list": [], "page": pg, "pagecount": 1, "limit": 90, "total": 0}
+
+ def _clean_vod_name(self, name):
+ import re
+ if not name:
+ return ''
+ cleaned = re.sub(r'第\s*\d+\s*[集話话章部期]', '', name)
+ cleaned = re.sub(r'EP\s*\d+', '', cleaned, flags=re.IGNORECASE)
+ cleaned = re.sub(r'全\d+集', '', cleaned)
+ cleaned = re.sub(r'更新至\d+集', '', cleaned)
+ cleaned = re.sub(r'\d+集全', '', cleaned)
+ cleaned = re.sub(r'[((].*?[))]', '', cleaned)
+ cleaned = re.sub(r'\s*-\s*.*$', '', cleaned)
+ cleaned = re.sub(r'\s+', ' ', cleaned)
+ cleaned = re.sub(r'^[\s\-_,.,。、]+|[\s\-_,.,。、]+$', '', cleaned)
+ return cleaned.strip()
+
+ def _build_danmaku_url(self, vod_name, vod_index=''):
+ import re
+ idx = 0
+ if vod_index:
+ s = str(vod_index).strip()
+ m = re.search(r'第\s*(\d+)\s*[集話话章部期]', s)
+ if m:
+ idx = int(m.group(1))
+ else:
+ m = re.search(r'(\d+)', s)
+ if m:
+ idx = int(m.group(1))
+ cleaned_name = self._clean_vod_name(vod_name)
+ params = []
+ if cleaned_name:
+ params.append(f'vodName={quote(cleaned_name)}')
+ params.append(f'vodIndex={idx}')
+ query = '&'.join(params)
+ return f'http://127.0.0.1:9978/proxy?do=appdanmu&{query}'
+
+ def playerContent(self, flag, id, vipFlags):
+ try:
+ ep_name = ''
+ vod_index = ''
+ if '$' in id:
+ parts = id.split('$', 1)
+ ep_name = parts[0]
+ url = parts[1] if len(parts) > 1 else ''
+ else:
+ url = id if id.startswith('http') else f'{self.host}{id}'
+ # 从 URL 中提取集数 (nid 参数)
+ nid_match = re.search(r'nid/(\d+)\.html', url)
+ if nid_match:
+ vod_index = nid_match.group(1)
+ danmaku_url = self._build_danmaku_url(self._vod_name, vod_index)
+ r = self.fetch(url, headers=self.headers, timeout=15, verify=False)
+ html = r.text
+ iframe_match = re.search(r'