Sync all projects
This commit is contained in:
@@ -5,10 +5,10 @@
|
||||
"warningText": "注意:如果别人倒卖海豚影视接口收费的都是骗子,没有qq群微信群,只有tg官方交流群 TG:@hshsjk",
|
||||
"sites": [
|
||||
{
|
||||
"key": "智能接口[工具]",
|
||||
"name": "🐬智能接口[工具].py",
|
||||
"key": "自动加载",
|
||||
"name": "🐬自动加载.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/智能接口[工具].py"
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/自动加载.py"
|
||||
},
|
||||
{
|
||||
"key": "hanime1",
|
||||
|
||||
@@ -807,6 +807,12 @@
|
||||
"name": "🐬hanime1动漫.py🔞[成人动漫]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/hanime1.py"
|
||||
},
|
||||
{
|
||||
"key": "亚色影库",
|
||||
"name": "🐬亚色影库.py🔞[成人]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/亚色影库.py"
|
||||
},
|
||||
{
|
||||
"key": "MoTV",
|
||||
|
||||
@@ -550,6 +550,12 @@
|
||||
"name": "🐬hanime1动漫.py🔞[成人动漫]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/hanime1.py"
|
||||
},
|
||||
{
|
||||
"key": "亚色影库",
|
||||
"name": "🐬亚色影库.py🔞[成人]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/亚色影库.py"
|
||||
},
|
||||
{
|
||||
"key": "MoTV",
|
||||
|
||||
+1369
@@ -0,0 +1,1369 @@
|
||||
function rouOption(title, value) {
|
||||
return { title: title, value: value || title };
|
||||
}
|
||||
|
||||
var ROU_SORT_OPTIONS = [
|
||||
rouOption("最新发布", "createdAt"),
|
||||
rouOption("最多播放", "viewCount"),
|
||||
rouOption("最多点赞", "likeCount"),
|
||||
rouOption("最长时长", "duration"),
|
||||
rouOption("最近更新", "updatedAt")
|
||||
];
|
||||
|
||||
var ROU_CNAV_OPTIONS = [
|
||||
rouOption("全部 国产AV", "國產AV"),
|
||||
rouOption("糖心Vlog"),
|
||||
rouOption("蜜桃影像傳媒"),
|
||||
rouOption("香蕉視頻傳媒"),
|
||||
rouOption("星空無限傳媒"),
|
||||
rouOption("天美傳媒"),
|
||||
rouOption("精東影業"),
|
||||
rouOption("杏吧傳媒"),
|
||||
rouOption("91製片廠"),
|
||||
rouOption("皇家華人"),
|
||||
rouOption("起點傳媒"),
|
||||
rouOption("大象傳媒"),
|
||||
rouOption("果凍傳媒"),
|
||||
rouOption("蘿莉社"),
|
||||
rouOption("ED Mosaic"),
|
||||
rouOption("兔子先生"),
|
||||
rouOption("扣扣傳媒"),
|
||||
rouOption("SA國際傳媒"),
|
||||
rouOption("愛神傳媒"),
|
||||
rouOption("性視界傳媒"),
|
||||
rouOption("PsychopornTW"),
|
||||
rouOption("拍攝花絮"),
|
||||
rouOption("抖陰"),
|
||||
rouOption("91茄子"),
|
||||
rouOption("絕對領域傳媒"),
|
||||
rouOption("烏托邦傳媒"),
|
||||
rouOption("紅斯燈影像"),
|
||||
rouOption("草莓視頻"),
|
||||
rouOption("渡邊傳媒"),
|
||||
rouOption("葫蘆影業"),
|
||||
rouOption("樂播傳媒"),
|
||||
rouOption("Pussy Hunter"),
|
||||
rouOption("麻麻傳媒"),
|
||||
rouOption("三只狼傳媒"),
|
||||
rouOption("萝莉原创"),
|
||||
rouOption("辣椒原創"),
|
||||
rouOption("MisAV"),
|
||||
rouOption("SWAG@daisybaby"),
|
||||
rouOption("冠希傳媒"),
|
||||
rouOption("微密圈傳媒"),
|
||||
rouOption("愛妃傳媒"),
|
||||
rouOption("天美影院"),
|
||||
rouOption("西瓜影視"),
|
||||
rouOption("肉肉傳媒"),
|
||||
rouOption("烏鴉傳媒"),
|
||||
rouOption("日出文化"),
|
||||
rouOption("鯨魚傳媒"),
|
||||
rouOption("國產AV劇情"),
|
||||
rouOption("SWAG@cartiernn"),
|
||||
rouOption("TWAV"),
|
||||
rouOption("Mini傳媒"),
|
||||
rouOption("桃花源"),
|
||||
rouOption("叮叮映畫"),
|
||||
rouOption("蜜桃視頻"),
|
||||
rouOption("O-STAR"),
|
||||
rouOption("開心鬼傳媒"),
|
||||
rouOption("葵心娛樂"),
|
||||
rouOption("愛污傳媒")
|
||||
];
|
||||
|
||||
var ROU_MADOU_OPTIONS = [
|
||||
rouOption("全部 麻豆传媒", "麻豆傳媒"),
|
||||
rouOption("愛豆傳媒"),
|
||||
rouOption("MD"),
|
||||
rouOption("MDX"),
|
||||
rouOption("麻豆US"),
|
||||
rouOption("MSD"),
|
||||
rouOption("MCY"),
|
||||
rouOption("MKY"),
|
||||
rouOption("MPG"),
|
||||
rouOption("FLIXKO"),
|
||||
rouOption("貓爪影像"),
|
||||
rouOption("國產麻豆AV節目"),
|
||||
rouOption("麻豆女神微愛視頻"),
|
||||
rouOption("麻豆番外"),
|
||||
rouOption("麻豆三十天特別企劃"),
|
||||
rouOption("麻豆導演系列"),
|
||||
rouOption("情趣K歌房"),
|
||||
rouOption("MDWP"),
|
||||
rouOption("突襲女優家"),
|
||||
rouOption("麻豆女優"),
|
||||
rouOption("麻豆達人秀"),
|
||||
rouOption("澀會"),
|
||||
rouOption("MDS"),
|
||||
rouOption("MDSR"),
|
||||
rouOption("麻豆女神微愛影片"),
|
||||
rouOption("MDL"),
|
||||
rouOption("MAN"),
|
||||
rouOption("MSM"),
|
||||
rouOption("MDHT"),
|
||||
rouOption("MDAG"),
|
||||
rouOption("MS"),
|
||||
rouOption("MSG"),
|
||||
rouOption("MDJ"),
|
||||
rouOption("MDM"),
|
||||
rouOption("MXJ"),
|
||||
rouOption("MDD"),
|
||||
rouOption("MLT")
|
||||
];
|
||||
|
||||
var ROU_ONLYFANS_OPTIONS = [
|
||||
rouOption("全部 OnlyFans", "OnlyFans"),
|
||||
rouOption("fansly"),
|
||||
rouOption("tangbo_hu"),
|
||||
rouOption("HongKongDoll"),
|
||||
rouOption("BunnyMiffy"),
|
||||
rouOption("Nana_Taipei"),
|
||||
rouOption("qiobnxingcai"),
|
||||
rouOption("suchanghub"),
|
||||
rouOption("ssrpeach"),
|
||||
rouOption("nicolove.cc"),
|
||||
rouOption("Miuzxc"),
|
||||
rouOption("kitty2002102"),
|
||||
rouOption("kittyxkum"),
|
||||
rouOption("yui_xin_tw"),
|
||||
rouOption("juneliu"),
|
||||
rouOption("YuZuKitty"),
|
||||
rouOption("jeenzen"),
|
||||
rouOption("monmon_tw"),
|
||||
rouOption("applecptv"),
|
||||
rouOption("andmlove"),
|
||||
rouOption("Loliiiiipop99"),
|
||||
rouOption("daintywilder"),
|
||||
rouOption("ZZZ666"),
|
||||
rouOption("aixiaixi"),
|
||||
rouOption("ChiChibae"),
|
||||
rouOption("blazeconjure3"),
|
||||
rouOption("moremore618"),
|
||||
rouOption("bdollairi"),
|
||||
rouOption("olive_emmm"),
|
||||
rouOption("chocoletmilkk"),
|
||||
rouOption("SLRabbit"),
|
||||
rouOption("Xreindeers"),
|
||||
rouOption("Carla Grace")
|
||||
];
|
||||
|
||||
var ROU_TANHUA_OPTIONS = [
|
||||
rouOption("全部 探花", "探花"),
|
||||
rouOption("91沈先生"),
|
||||
rouOption("探花精選400"),
|
||||
rouOption("小寶尋花"),
|
||||
rouOption("91lisa"),
|
||||
rouOption("調教小景甜"),
|
||||
rouOption("午夜尋花"),
|
||||
rouOption("91鳳鳴鳥唱"),
|
||||
rouOption("大神精選"),
|
||||
rouOption("AVOVE直播"),
|
||||
rouOption("91貓先生"),
|
||||
rouOption("千人斬探花"),
|
||||
rouOption("全國探花"),
|
||||
rouOption("91Fans"),
|
||||
rouOption("七天探花"),
|
||||
rouOption("9總全國探花"),
|
||||
rouOption("91大神@LovELolita7"),
|
||||
rouOption("18歲母狗無限高潮"),
|
||||
rouOption("鴨哥探花"),
|
||||
rouOption("錘子探花"),
|
||||
rouOption("探花合集"),
|
||||
rouOption("91不見星空"),
|
||||
rouOption("早期東莞ISO桑拿系列"),
|
||||
rouOption("91康先生"),
|
||||
rouOption("肉オナホ"),
|
||||
rouOption("91大神唐伯虎"),
|
||||
rouOption("韋小寶"),
|
||||
rouOption("91風流哥全集"),
|
||||
rouOption("91蜜桃的合集"),
|
||||
rouOption("換妻探花"),
|
||||
rouOption("小陳頭星選"),
|
||||
rouOption("91大神括約肌大叔"),
|
||||
rouOption("情侶自拍"),
|
||||
rouOption("探花精選"),
|
||||
rouOption("91呆哥"),
|
||||
rouOption("mmmn753"),
|
||||
rouOption("楊導撩妹"),
|
||||
rouOption("歌廳探花陳先生"),
|
||||
rouOption("91美女涵菱"),
|
||||
rouOption("太子探花"),
|
||||
rouOption("小馬尋花"),
|
||||
rouOption("91唐哥"),
|
||||
rouOption("jimmybiiig"),
|
||||
rouOption("91天堂原創"),
|
||||
rouOption("小飛探花"),
|
||||
rouOption("王子哥專啪學生妹"),
|
||||
rouOption("文軒探花"),
|
||||
rouOption("偉哥尋歡"),
|
||||
rouOption("大草莓寶貝"),
|
||||
rouOption("探花女下海直播"),
|
||||
rouOption("91天堂系列"),
|
||||
rouOption("91大神胖Kyo"),
|
||||
rouOption("攝影師果哥出品"),
|
||||
rouOption("莞式選妃"),
|
||||
rouOption("catman"),
|
||||
rouOption("90w粉"),
|
||||
rouOption("探花大神"),
|
||||
rouOption("91原創達人@多乙丶"),
|
||||
rouOption("91大黃鴨"),
|
||||
rouOption("小東全國尋妹"),
|
||||
rouOption("91Dr哥"),
|
||||
rouOption("大熊探花"),
|
||||
rouOption("91約妹達人"),
|
||||
rouOption("91大神揚風"),
|
||||
rouOption("91愛絲小仙女思妍"),
|
||||
rouOption("探花郎李尋歡"),
|
||||
rouOption("91新晉大神sweattt"),
|
||||
rouOption("91新人GD超模(現改名69DD)"),
|
||||
rouOption("91大神jinx"),
|
||||
rouOption("91sex哥"),
|
||||
rouOption("175車模"),
|
||||
rouOption("東莞探花"),
|
||||
rouOption("嫖嫖sex探花"),
|
||||
rouOption("秀人網模特")
|
||||
];
|
||||
|
||||
var ROU_SELFIE_OPTIONS = [
|
||||
rouOption("全部 自拍流出", "自拍流出"),
|
||||
rouOption("中國"),
|
||||
rouOption("台灣"),
|
||||
rouOption("twitter"),
|
||||
rouOption("主播"),
|
||||
rouOption("韓國"),
|
||||
rouOption("絲襪"),
|
||||
rouOption("多P"),
|
||||
rouOption("素人"),
|
||||
rouOption("偷拍"),
|
||||
rouOption("情侶自拍"),
|
||||
rouOption("91Fans"),
|
||||
rouOption("91天堂原創"),
|
||||
rouOption("大神精選"),
|
||||
rouOption("糖心Vlog")
|
||||
];
|
||||
|
||||
var WidgetMetadata = {
|
||||
id: "rou_video",
|
||||
title: "肉视频",
|
||||
description: "rou.video 分类、排序、搜索与播放解析组件",
|
||||
author: "Evil",
|
||||
version: "1.2.5",
|
||||
site: "https://rou.video",
|
||||
icon: "https://rou.video/favicon.ico",
|
||||
modules: [
|
||||
// {
|
||||
// id: "search",
|
||||
// title: "站内搜索",
|
||||
// type: "media_list",
|
||||
// functionName: "searchVideos",
|
||||
// cacheDuration: 300,
|
||||
// timeoutSeconds: 30,
|
||||
// params: [
|
||||
// {
|
||||
// name: "keyword",
|
||||
// title: "搜索关键词",
|
||||
// type: "input",
|
||||
// description: "输入搜索关键词(标题、标签、演员/创作者等)",
|
||||
// value: ""
|
||||
// },
|
||||
// { name: "page", title: "页码", type: "page", description: "页码", value: "1" }
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
id: "home_sections",
|
||||
title: "首页精选",
|
||||
type: "media_list",
|
||||
functionName: "loadSectionVideos",
|
||||
cacheDuration: 1800,
|
||||
timeoutSeconds: 30,
|
||||
params: [
|
||||
{
|
||||
name: "section",
|
||||
title: "分区",
|
||||
type: "enum",
|
||||
value: "latestVideos",
|
||||
enumOptions: [
|
||||
{ title: "最新更新", value: "latestVideos" },
|
||||
{ title: "热门 91", value: "hot91" },
|
||||
{ title: "热门 自拍流出", value: "hotSelfie" },
|
||||
{ title: "热门 国产AV视频", value: "hotCNAV" },
|
||||
{ title: "日榜 91", value: "dailyHot91" },
|
||||
{ title: "日榜 自拍流出", value: "dailyHotSelfie" },
|
||||
{ title: "日榜 国产AV视频", value: "dailyHotCNAV" },
|
||||
{ title: "日榜 OnlyFans", value: "dailyOnlyFans" },
|
||||
{ title: "日榜 日本区", value: "dailyJV" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "cnav",
|
||||
title: "国产AV",
|
||||
type: "media_list",
|
||||
functionName: "loadTagVideos",
|
||||
cacheDuration: 1800,
|
||||
timeoutSeconds: 30,
|
||||
params: [
|
||||
{ name: "tag", title: "子模块", type: "enum", value: "國產AV", enumOptions: ROU_CNAV_OPTIONS },
|
||||
{ name: "order", title: "排序", type: "enum", value: "createdAt", enumOptions: ROU_SORT_OPTIONS },
|
||||
{ name: "page", title: "页码", type: "page", value: "1" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "madou",
|
||||
title: "麻豆传媒",
|
||||
type: "media_list",
|
||||
functionName: "loadTagVideos",
|
||||
cacheDuration: 1800,
|
||||
timeoutSeconds: 30,
|
||||
params: [
|
||||
{ name: "tag", title: "子模块", type: "enum", value: "麻豆傳媒", enumOptions: ROU_MADOU_OPTIONS },
|
||||
{ name: "order", title: "排序", type: "enum", value: "createdAt", enumOptions: ROU_SORT_OPTIONS },
|
||||
{ name: "page", title: "页码", type: "page", value: "1" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "onlyfans",
|
||||
title: "OnlyFans",
|
||||
type: "media_list",
|
||||
functionName: "loadTagVideos",
|
||||
cacheDuration: 1800,
|
||||
timeoutSeconds: 30,
|
||||
params: [
|
||||
{ name: "tag", title: "子模块", type: "enum", value: "OnlyFans", enumOptions: ROU_ONLYFANS_OPTIONS },
|
||||
{ name: "order", title: "排序", type: "enum", value: "createdAt", enumOptions: ROU_SORT_OPTIONS },
|
||||
{ name: "page", title: "页码", type: "page", value: "1" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "tanhua",
|
||||
title: "探花",
|
||||
type: "media_list",
|
||||
functionName: "loadTagVideos",
|
||||
cacheDuration: 1800,
|
||||
timeoutSeconds: 30,
|
||||
params: [
|
||||
{ name: "tag", title: "子模块", type: "enum", value: "探花", enumOptions: ROU_TANHUA_OPTIONS },
|
||||
{ name: "order", title: "排序", type: "enum", value: "createdAt", enumOptions: ROU_SORT_OPTIONS },
|
||||
{ name: "page", title: "页码", type: "page", value: "1" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "selfie",
|
||||
title: "自拍流出",
|
||||
type: "media_list",
|
||||
functionName: "loadTagVideos",
|
||||
cacheDuration: 1800,
|
||||
timeoutSeconds: 30,
|
||||
params: [
|
||||
{ name: "tag", title: "子模块", type: "enum", value: "自拍流出", enumOptions: ROU_SELFIE_OPTIONS },
|
||||
{ name: "order", title: "排序", type: "enum", value: "createdAt", enumOptions: ROU_SORT_OPTIONS },
|
||||
{ name: "page", title: "页码", type: "page", value: "1" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "loadResource",
|
||||
title: "肉视频播放源",
|
||||
description: "根据当前影片返回 MDK 标准化与 MPV 原始播放线路",
|
||||
functionName: "loadResource",
|
||||
type: "stream",
|
||||
cacheDuration: 0,
|
||||
timeoutSeconds: 45,
|
||||
retryCount: 0,
|
||||
params: []
|
||||
}
|
||||
],
|
||||
search: {
|
||||
title: "搜索",
|
||||
functionName: "searchVideos",
|
||||
params: [
|
||||
{
|
||||
name: "keyword",
|
||||
title: "搜索关键词",
|
||||
type: "input",
|
||||
description: "输入搜索关键词(标题、标签、演员/创作者等)",
|
||||
value: ""
|
||||
},
|
||||
{ name: "page", title: "页码", type: "page", description: "页码", value: "1" }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var ROU_SITE = "https://rou.video";
|
||||
var ROU_IMAGE_PROXY = "https://external-content.duckduckgo.com/iu/";
|
||||
var ROU_RESOURCE_LINKS = {};
|
||||
var ROU_HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"Referer": ROU_SITE + "/"
|
||||
};
|
||||
|
||||
function ensureArray(v) {
|
||||
return Array.isArray(v) ? v : [];
|
||||
}
|
||||
|
||||
function text(v) {
|
||||
return String(v == null ? "" : v).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function stripRouLineSuffix(value) {
|
||||
return text(value).replace(/\s*·\s*(?:MDK\s*标准化线路|MPV\s*原始线路)\s*$/i, "").trim();
|
||||
}
|
||||
|
||||
function normalizeRouMatchTitle(value) {
|
||||
return stripRouLineSuffix(value)
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "")
|
||||
.replace(/[·•::,,。!!??"'“”‘’()()\[\]【】{}<>《》_\-.\/\\|~~]/g, "");
|
||||
}
|
||||
|
||||
function rouTitleHash(value) {
|
||||
value = String(value || "");
|
||||
var hash = 2166136261;
|
||||
for (var i = 0; i < value.length; i++) {
|
||||
hash ^= value.charCodeAt(i);
|
||||
hash = Math.imul ? Math.imul(hash, 16777619) : ((hash * 16777619) | 0);
|
||||
}
|
||||
return (hash >>> 0).toString(16);
|
||||
}
|
||||
|
||||
function rouResourceStorageKey(title) {
|
||||
var normalized = normalizeRouMatchTitle(title);
|
||||
return normalized ? ("rou.resource.link." + rouTitleHash(normalized) + "." + normalized.length) : "";
|
||||
}
|
||||
|
||||
function rememberRouResourceLink(title, link) {
|
||||
var key = rouResourceStorageKey(title);
|
||||
link = text(link);
|
||||
if (!key || !/^https?:\/\//i.test(link)) return;
|
||||
|
||||
ROU_RESOURCE_LINKS[key] = link;
|
||||
try {
|
||||
if (Widget.storage && typeof Widget.storage.set === "function") {
|
||||
Widget.storage.set(key, link);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[rou.video] resource link cache write failed: " + (e && e.message ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
function getRememberedRouResourceLink(title) {
|
||||
var key = rouResourceStorageKey(title);
|
||||
if (!key) return "";
|
||||
if (ROU_RESOURCE_LINKS[key]) return ROU_RESOURCE_LINKS[key];
|
||||
|
||||
try {
|
||||
if (Widget.storage && typeof Widget.storage.get === "function") {
|
||||
var stored = text(Widget.storage.get(key, ""));
|
||||
if (/^https?:\/\//i.test(stored)) {
|
||||
ROU_RESOURCE_LINKS[key] = stored;
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[rou.video] resource link cache read failed: " + (e && e.message ? e.message : e));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function utf8BytesToString(bytes) {
|
||||
var result = "";
|
||||
var i = 0;
|
||||
|
||||
while (i < bytes.length) {
|
||||
var first = bytes[i++] & 255;
|
||||
var codePoint = first;
|
||||
|
||||
if (first >= 240 && i + 2 < bytes.length) {
|
||||
codePoint = ((first & 7) << 18) |
|
||||
((bytes[i++] & 63) << 12) |
|
||||
((bytes[i++] & 63) << 6) |
|
||||
(bytes[i++] & 63);
|
||||
} else if (first >= 224 && i + 1 < bytes.length) {
|
||||
codePoint = ((first & 15) << 12) |
|
||||
((bytes[i++] & 63) << 6) |
|
||||
(bytes[i++] & 63);
|
||||
} else if (first >= 192 && i < bytes.length) {
|
||||
codePoint = ((first & 31) << 6) | (bytes[i++] & 63);
|
||||
}
|
||||
|
||||
if (codePoint > 65535) {
|
||||
codePoint -= 65536;
|
||||
result += String.fromCharCode(55296 + (codePoint >> 10));
|
||||
result += String.fromCharCode(56320 + (codePoint & 1023));
|
||||
} else {
|
||||
result += String.fromCharCode(codePoint);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function responseBodyToString(data) {
|
||||
if (typeof data === "string") return data;
|
||||
if (data == null) return "";
|
||||
|
||||
var bytes = null;
|
||||
if (Array.isArray(data)) {
|
||||
bytes = data;
|
||||
} else if (typeof Uint8Array !== "undefined" && data instanceof Uint8Array) {
|
||||
bytes = data;
|
||||
} else if (typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer) {
|
||||
bytes = new Uint8Array(data);
|
||||
} else if (data && data.buffer && typeof Uint8Array !== "undefined") {
|
||||
try {
|
||||
bytes = new Uint8Array(data.buffer, data.byteOffset || 0, data.byteLength || data.length);
|
||||
} catch (e) {
|
||||
bytes = null;
|
||||
}
|
||||
}
|
||||
|
||||
return bytes ? utf8BytesToString(bytes) : String(data);
|
||||
}
|
||||
|
||||
function proxyCoverUrl(url) {
|
||||
url = absoluteUrl(url);
|
||||
if (!/^https?:\/\//i.test(url)) return url;
|
||||
if (url.indexOf(ROU_IMAGE_PROXY) === 0) return url;
|
||||
return ROU_IMAGE_PROXY + "?u=" + encodeURIComponent(url) + "&f=1&nofb=1";
|
||||
}
|
||||
|
||||
function absoluteUrl(url) {
|
||||
url = text(url);
|
||||
if (!url) return "";
|
||||
if (url.indexOf("//") === 0) return "https:" + url;
|
||||
if (/^https?:\/\//i.test(url)) return url;
|
||||
if (url.charAt(0) === "/") return ROU_SITE + url;
|
||||
return ROU_SITE + "/" + url;
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
seconds = Number(seconds || 0);
|
||||
if (!isFinite(seconds) || seconds <= 0) return "";
|
||||
seconds = Math.round(seconds);
|
||||
var h = Math.floor(seconds / 3600);
|
||||
var m = Math.floor((seconds % 3600) / 60);
|
||||
var s = seconds % 60;
|
||||
function pad(n) { return n < 10 ? "0" + n : String(n); }
|
||||
return h > 0 ? (h + ":" + pad(m) + ":" + pad(s)) : (pad(m) + ":" + pad(s));
|
||||
}
|
||||
|
||||
function formatCount(value) {
|
||||
var n = Number(value || 0);
|
||||
if (!isFinite(n) || n <= 0) return "";
|
||||
if (n >= 100000000) return (n / 100000000).toFixed(1).replace(/\.0$/, "") + "亿";
|
||||
if (n >= 10000) return (n / 10000).toFixed(1).replace(/\.0$/, "") + "万";
|
||||
return String(Math.round(n));
|
||||
}
|
||||
|
||||
function createPlaceholderItem(message) {
|
||||
return {
|
||||
id: "placeholder",
|
||||
type: "placeholder",
|
||||
title: "提示",
|
||||
description: text(message || "暂无内容"),
|
||||
posterPath: "",
|
||||
backdropPath: "",
|
||||
mediaType: "movie",
|
||||
duration: 0,
|
||||
durationText: "",
|
||||
previewUrl: "",
|
||||
videoUrl: "",
|
||||
url: "",
|
||||
playUrl: "",
|
||||
link: "",
|
||||
playerType: "none"
|
||||
};
|
||||
}
|
||||
|
||||
function base64ToBytes(input) {
|
||||
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
var clean = String(input || "").replace(/[^A-Za-z0-9+/=]/g, "");
|
||||
var bytes = [];
|
||||
var buffer = 0;
|
||||
var bits = 0;
|
||||
|
||||
for (var i = 0; i < clean.length; i++) {
|
||||
var c = clean.charAt(i);
|
||||
if (c === "=") break;
|
||||
var value = chars.indexOf(c);
|
||||
if (value < 0) continue;
|
||||
buffer = (buffer << 6) | value;
|
||||
bits += 6;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
bytes.push((buffer >> bits) & 255);
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function decodeRouVideoPayload(ev) {
|
||||
if (!ev || !ev.d) return {};
|
||||
var key = Number(ev.k || 0);
|
||||
var bytes = base64ToBytes(ev.d);
|
||||
var text = "";
|
||||
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
text += String.fromCharCode((bytes[i] - key + 256) & 255);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function extractNextData(html) {
|
||||
var match = String(html || "").match(/<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/i);
|
||||
if (!match) return {};
|
||||
try {
|
||||
var raw = JSON.parse(match[1]);
|
||||
return (raw && raw.props && raw.props.pageProps) ? raw.props.pageProps : {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function requestTextWithRetry(url, headers, timeout, validator, label, maxAttempts) {
|
||||
var lastError = null;
|
||||
var attempts = Math.max(1, parseInt(maxAttempts, 10) || 2);
|
||||
|
||||
for (var attempt = 1; attempt <= attempts; attempt++) {
|
||||
try {
|
||||
var resp = await Widget.http.get(url, {
|
||||
headers: headers || {},
|
||||
allow_redirects: true,
|
||||
timeout: timeout
|
||||
});
|
||||
var status = Number(resp && resp.status || 0);
|
||||
var body = responseBodyToString(resp && resp.data);
|
||||
|
||||
if (!resp || resp.ok === false || status >= 400) {
|
||||
throw new Error((label || "request") + " HTTP " + status);
|
||||
}
|
||||
if (!body || (validator && !validator(body))) {
|
||||
throw new Error((label || "request") + " response validation failed");
|
||||
}
|
||||
return body;
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
console.log("[rou.video] " + (label || "request") + " attempt " + attempt + " failed: " + (e && e.message ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error((label || "request") + " failed");
|
||||
}
|
||||
|
||||
function fetchHtmlWithOptions(url, timeout, attempts, label) {
|
||||
return requestTextWithRetry(url, ROU_HEADERS, timeout || 8000, function (body) {
|
||||
return body.indexOf("__NEXT_DATA__") >= 0;
|
||||
}, label || "html", attempts || 2);
|
||||
}
|
||||
|
||||
function fetchHtml(url) {
|
||||
return fetchHtmlWithOptions(url, 8000, 2, "html");
|
||||
}
|
||||
|
||||
function getPlayHeaders(referer) {
|
||||
return {
|
||||
"User-Agent": ROU_HEADERS["User-Agent"],
|
||||
"Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,*/*",
|
||||
"Referer": referer || ROU_SITE + "/",
|
||||
"Origin": ROU_SITE
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRouVideoUrl(url) {
|
||||
url = text(url);
|
||||
if (!url) return "";
|
||||
return url.replace(/\/index\.jpg(?=([?#]|$))/i, "/index.m3u8");
|
||||
}
|
||||
|
||||
function parseRouHlsAttributeList(attributeText) {
|
||||
var attributes = {};
|
||||
var source = String(attributeText || "");
|
||||
var token = "";
|
||||
var quoted = false;
|
||||
|
||||
for (var i = 0; i <= source.length; i++) {
|
||||
var ch = i < source.length ? source.charAt(i) : ",";
|
||||
if (ch === '"' && source.charAt(i - 1) !== "\\") {
|
||||
quoted = !quoted;
|
||||
token += ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === "," && !quoted) {
|
||||
var part = token.trim();
|
||||
token = "";
|
||||
if (!part) continue;
|
||||
var equalsIndex = part.indexOf("=");
|
||||
if (equalsIndex <= 0) continue;
|
||||
var key = part.slice(0, equalsIndex).trim().toUpperCase();
|
||||
var value = part.slice(equalsIndex + 1).trim();
|
||||
if (value.length >= 2 && value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') {
|
||||
value = value.slice(1, -1).replace(/\\"/g, '"');
|
||||
}
|
||||
attributes[key] = value;
|
||||
continue;
|
||||
}
|
||||
token += ch;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
function positiveRouNumber(value) {
|
||||
var result = parseInt(String(value || ""), 10);
|
||||
return isFinite(result) && result > 0 ? result : 0;
|
||||
}
|
||||
|
||||
function parseRouHlsResolution(value, uri) {
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
var declared = String(value || "").match(/(\d{2,5})\s*x\s*(\d{2,5})/i);
|
||||
if (declared) {
|
||||
width = positiveRouNumber(declared[1]);
|
||||
height = positiveRouNumber(declared[2]);
|
||||
}
|
||||
|
||||
var pathText = String(uri || "");
|
||||
if (!width || !height) {
|
||||
var pathResolution = pathText.match(/(?:^|[\/_\-.])(\d{2,5})x(\d{2,5})(?:[\/_\-.]|$)/i);
|
||||
if (pathResolution) {
|
||||
width = positiveRouNumber(pathResolution[1]);
|
||||
height = positiveRouNumber(pathResolution[2]);
|
||||
}
|
||||
}
|
||||
if (!height) {
|
||||
var heightOnly = pathText.match(/(?:^|[\/_\-.])(\d{3,4})p(?:[\/_\-.]|$)/i);
|
||||
if (heightOnly) {
|
||||
height = positiveRouNumber(heightOnly[1]);
|
||||
width = height ? Math.round(height * 16 / 9) : 0;
|
||||
}
|
||||
}
|
||||
return { width: width, height: height, pixels: width && height ? width * height : 0 };
|
||||
}
|
||||
|
||||
function resolveRouHlsUrl(uri, playlistUrl) {
|
||||
var value = text(uri);
|
||||
var base = text(playlistUrl);
|
||||
if (!value) return "";
|
||||
if (/^https?:\/\//i.test(value)) return normalizeRouVideoUrl(value);
|
||||
if (/^\/\//.test(value)) return normalizeRouVideoUrl("https:" + value);
|
||||
try {
|
||||
if (typeof URL !== "undefined") return normalizeRouVideoUrl(new URL(value, base).toString());
|
||||
} catch (e) {}
|
||||
|
||||
var cleanBase = base.replace(/[?#].*$/, "");
|
||||
var originMatch = cleanBase.match(/^(https?:\/\/[^/]+)/i);
|
||||
if (value.charAt(0) === "/" && originMatch) return normalizeRouVideoUrl(originMatch[1] + value);
|
||||
var slashIndex = cleanBase.lastIndexOf("/");
|
||||
var directory = slashIndex >= 0 ? cleanBase.slice(0, slashIndex + 1) : cleanBase + "/";
|
||||
return normalizeRouVideoUrl(directory + value.replace(/^\.\//, ""));
|
||||
}
|
||||
|
||||
function parseRouMasterVariants(masterText, playlistUrl) {
|
||||
var lines = String(masterText || "").split(/\r?\n/);
|
||||
var variants = [];
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = String(lines[i] || "").trim();
|
||||
if (!/^#EXT-X-STREAM-INF\s*:/i.test(line)) continue;
|
||||
var attrs = parseRouHlsAttributeList(line.slice(line.indexOf(":") + 1));
|
||||
var uri = "";
|
||||
var uriIndex = i + 1;
|
||||
while (uriIndex < lines.length) {
|
||||
var candidate = String(lines[uriIndex] || "").trim();
|
||||
if (!candidate || candidate.charAt(0) === "#") {
|
||||
uriIndex++;
|
||||
continue;
|
||||
}
|
||||
uri = candidate;
|
||||
break;
|
||||
}
|
||||
if (!uri) continue;
|
||||
|
||||
var resolution = parseRouHlsResolution(attrs.RESOLUTION, uri);
|
||||
var averageBandwidth = positiveRouNumber(attrs["AVERAGE-BANDWIDTH"]);
|
||||
var peakBandwidth = positiveRouNumber(attrs.BANDWIDTH);
|
||||
variants.push({
|
||||
url: resolveRouHlsUrl(uri, playlistUrl),
|
||||
uri: uri,
|
||||
codecs: String(attrs.CODECS || ""),
|
||||
resolution: String(attrs.RESOLUTION || ""),
|
||||
width: resolution.width,
|
||||
height: resolution.height,
|
||||
pixels: resolution.pixels,
|
||||
bandwidth: averageBandwidth || peakBandwidth,
|
||||
peakBandwidth: peakBandwidth,
|
||||
frameRate: parseFloat(String(attrs["FRAME-RATE"] || "0")) || 0,
|
||||
order: variants.length
|
||||
});
|
||||
i = uriIndex;
|
||||
}
|
||||
return variants;
|
||||
}
|
||||
|
||||
function selectRouHighestQualityVariant(masterText, playlistUrl) {
|
||||
var variants = parseRouMasterVariants(masterText, playlistUrl);
|
||||
if (!variants.length) return null;
|
||||
variants.sort(function (left, right) {
|
||||
var fields = ["pixels", "height", "width", "bandwidth", "peakBandwidth", "frameRate", "order"];
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var difference = Number(right[fields[i]] || 0) - Number(left[fields[i]] || 0);
|
||||
if (difference) return difference;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return variants[0];
|
||||
}
|
||||
|
||||
function classifyRouCodecString(codecs) {
|
||||
var value = String(codecs || "").toLowerCase();
|
||||
if (/(^|[,\s])(hvc1|hev1|hevc|h265|av01|av1|vp09|vp9|dvhe|dvh1)(?:\.|[,\s]|$)/i.test(value)) return "modern";
|
||||
if (/(^|[,\s])(avc1|avc3|h264)(?:\.|[,\s]|$)/i.test(value)) return "avc";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function firstRouMediaSegmentUrl(playlist, playlistUrl) {
|
||||
var lines = String(playlist || "").split(/\r?\n/);
|
||||
var expectSegment = false;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = String(lines[i] || "").trim();
|
||||
if (/^#EXTINF:/i.test(line)) {
|
||||
expectSegment = true;
|
||||
continue;
|
||||
}
|
||||
if (!line || line.charAt(0) === "#") continue;
|
||||
if (expectSegment) return resolveRouHlsUrl(line, playlistUrl);
|
||||
expectSegment = false;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function responseBodyToBytes(data) {
|
||||
if (data == null) return [];
|
||||
if (Array.isArray(data)) return data;
|
||||
if (typeof Uint8Array !== "undefined" && data instanceof Uint8Array) return data;
|
||||
if (typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer) return new Uint8Array(data);
|
||||
if (data && data.buffer && typeof Uint8Array !== "undefined") {
|
||||
try {
|
||||
return new Uint8Array(data.buffer, data.byteOffset || 0, data.byteLength || data.length);
|
||||
} catch (e) {}
|
||||
}
|
||||
var source = String(data || "");
|
||||
var bytes = [];
|
||||
var limit = Math.min(source.length, 131072);
|
||||
for (var i = 0; i < limit; i++) bytes.push(source.charCodeAt(i) & 255);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function inspectRouTsCodec(bytes) {
|
||||
bytes = responseBodyToBytes(bytes);
|
||||
var syncOffset = -1;
|
||||
for (var offset = 0; offset < 188 && offset + 376 < bytes.length; offset++) {
|
||||
if (bytes[offset] === 71 && bytes[offset + 188] === 71 && bytes[offset + 376] === 71) {
|
||||
syncOffset = offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (syncOffset < 0) return { family: "unknown", streamTypes: [] };
|
||||
|
||||
var pmtPid = -1;
|
||||
var streamTypes = [];
|
||||
for (var packet = syncOffset; packet + 188 <= bytes.length; packet += 188) {
|
||||
if (bytes[packet] !== 71) continue;
|
||||
var payloadStart = (bytes[packet + 1] & 64) !== 0;
|
||||
var pid = ((bytes[packet + 1] & 31) << 8) | bytes[packet + 2];
|
||||
var adaptation = (bytes[packet + 3] >> 4) & 3;
|
||||
if (adaptation === 0 || adaptation === 2) continue;
|
||||
var cursor = packet + 4;
|
||||
if (adaptation === 3) cursor += 1 + bytes[cursor];
|
||||
if (cursor >= packet + 188) continue;
|
||||
if (payloadStart) cursor += 1 + bytes[cursor];
|
||||
if (cursor + 12 >= packet + 188) continue;
|
||||
|
||||
if (pid === 0 && bytes[cursor] === 0) {
|
||||
var patLength = ((bytes[cursor + 1] & 15) << 8) | bytes[cursor + 2];
|
||||
var patEnd = Math.min(cursor + 3 + patLength - 4, packet + 188);
|
||||
for (var pat = cursor + 8; pat + 3 < patEnd; pat += 4) {
|
||||
var program = (bytes[pat] << 8) | bytes[pat + 1];
|
||||
if (program) {
|
||||
pmtPid = ((bytes[pat + 2] & 31) << 8) | bytes[pat + 3];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (pid === pmtPid && bytes[cursor] === 2) {
|
||||
var pmtLength = ((bytes[cursor + 1] & 15) << 8) | bytes[cursor + 2];
|
||||
var programInfoLength = ((bytes[cursor + 10] & 15) << 8) | bytes[cursor + 11];
|
||||
var pmtEnd = Math.min(cursor + 3 + pmtLength - 4, packet + 188);
|
||||
for (var stream = cursor + 12 + programInfoLength; stream + 4 < pmtEnd;) {
|
||||
var streamType = bytes[stream];
|
||||
var infoLength = ((bytes[stream + 3] & 15) << 8) | bytes[stream + 4];
|
||||
if (streamTypes.indexOf(streamType) < 0) streamTypes.push(streamType);
|
||||
stream += 5 + infoLength;
|
||||
}
|
||||
if (streamTypes.length) break;
|
||||
}
|
||||
}
|
||||
if (streamTypes.indexOf(36) >= 0) return { family: "modern", streamTypes: streamTypes };
|
||||
if (streamTypes.indexOf(27) >= 0) return { family: "avc", streamTypes: streamTypes };
|
||||
return { family: "unknown", streamTypes: streamTypes };
|
||||
}
|
||||
|
||||
async function probeRouSegmentCodec(segmentUrl, referer, timeout) {
|
||||
var headers = getPlayHeaders(referer);
|
||||
headers.Range = "bytes=0-65535";
|
||||
var response = await Widget.http.get(segmentUrl, {
|
||||
headers: headers,
|
||||
allow_redirects: true,
|
||||
timeout: Math.max(2000, Math.min(Number(timeout || 3500), 4500))
|
||||
});
|
||||
var status = Number(response && response.status || 0);
|
||||
if (!response || response.ok === false || status >= 400) throw new Error("segment probe HTTP " + status);
|
||||
return inspectRouTsCodec(response.data);
|
||||
}
|
||||
function stringToUtf8Bytes(input) {
|
||||
input = String(input || "");
|
||||
var bytes = [];
|
||||
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
var codePoint = input.charCodeAt(i);
|
||||
if (codePoint >= 55296 && codePoint <= 56319 && i + 1 < input.length) {
|
||||
var next = input.charCodeAt(i + 1);
|
||||
if (next >= 56320 && next <= 57343) {
|
||||
codePoint = 65536 + ((codePoint - 55296) << 10) + (next - 56320);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (codePoint <= 127) {
|
||||
bytes.push(codePoint);
|
||||
} else if (codePoint <= 2047) {
|
||||
bytes.push(192 | (codePoint >> 6));
|
||||
bytes.push(128 | (codePoint & 63));
|
||||
} else if (codePoint <= 65535) {
|
||||
bytes.push(224 | (codePoint >> 12));
|
||||
bytes.push(128 | ((codePoint >> 6) & 63));
|
||||
bytes.push(128 | (codePoint & 63));
|
||||
} else {
|
||||
bytes.push(240 | (codePoint >> 18));
|
||||
bytes.push(128 | ((codePoint >> 12) & 63));
|
||||
bytes.push(128 | ((codePoint >> 6) & 63));
|
||||
bytes.push(128 | (codePoint & 63));
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
var result = "";
|
||||
|
||||
for (var i = 0; i < bytes.length; i += 3) {
|
||||
var a = bytes[i] & 255;
|
||||
var hasB = i + 1 < bytes.length;
|
||||
var hasC = i + 2 < bytes.length;
|
||||
var b = hasB ? (bytes[i + 1] & 255) : 0;
|
||||
var c = hasC ? (bytes[i + 2] & 255) : 0;
|
||||
var value = (a << 16) | (b << 8) | c;
|
||||
|
||||
result += chars.charAt((value >> 18) & 63);
|
||||
result += chars.charAt((value >> 12) & 63);
|
||||
result += hasB ? chars.charAt((value >> 6) & 63) : "=";
|
||||
result += hasC ? chars.charAt(value & 63) : "=";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function rewriteRouPlaylistForMdk(playlist) {
|
||||
var lines = String(playlist || "").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").split("\n");
|
||||
var expectSegment = false;
|
||||
var rewritten = 0;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var trimmed = lines[i].trim();
|
||||
if (/^#EXTINF:/i.test(trimmed)) {
|
||||
expectSegment = true;
|
||||
continue;
|
||||
}
|
||||
if (!trimmed || trimmed.charAt(0) === "#") continue;
|
||||
if (expectSegment && /^https?:\/\//i.test(trimmed) && /\.jpg(?=([?#]|$))/i.test(trimmed)) {
|
||||
lines[i] = lines[i].replace(/\.jpg(?=([?#]|$))/i, ".ts");
|
||||
rewritten++;
|
||||
}
|
||||
expectSegment = false;
|
||||
}
|
||||
if (!/^\s*#EXTM3U/i.test(lines.join("\n")) || rewritten < 1) return null;
|
||||
return { body: lines.join("\n"), segmentCount: rewritten };
|
||||
}
|
||||
|
||||
function createMdkPlaylistDataUrl(playlist) {
|
||||
var normalized = rewriteRouPlaylistForMdk(playlist);
|
||||
if (!normalized) return null;
|
||||
if (normalized.body.length > 800000) throw new Error("playlist is too large for inline transport");
|
||||
return {
|
||||
url: "data:application/vnd.apple.mpegurl;base64," + bytesToBase64(stringToUtf8Bytes(normalized.body)),
|
||||
segmentCount: normalized.segmentCount
|
||||
};
|
||||
}
|
||||
|
||||
async function buildMdkPlaylistDataUrl(videoUrl, referer, timeout, attempts) {
|
||||
var playlist = await requestTextWithRetry(videoUrl, getPlayHeaders(referer), timeout || 6000, function (body) {
|
||||
return /^\s*#EXTM3U/i.test(body);
|
||||
}, "playlist", attempts || 2);
|
||||
var result = createMdkPlaylistDataUrl(playlist);
|
||||
if (!result) throw new Error("playlist has no disguised MPEG-TS segments");
|
||||
return result;
|
||||
}
|
||||
|
||||
async function resolveRouPlaybackPlan(videoUrl, referer, timeout, attempts) {
|
||||
var originalUrl = normalizeRouVideoUrl(videoUrl);
|
||||
var playlistTimeout = Math.max(3000, Math.min(Number(timeout || 5000), 5500));
|
||||
var playlistAttempts = Math.max(1, Math.min(parseInt(attempts, 10) || 1, 2));
|
||||
var initialPlaylist = await requestTextWithRetry(originalUrl, getPlayHeaders(referer), playlistTimeout, function (body) {
|
||||
return /^\s*#EXTM3U/i.test(body);
|
||||
}, "playlist-inspection", playlistAttempts);
|
||||
var selected = selectRouHighestQualityVariant(initialPlaylist, originalUrl);
|
||||
var rawUrl = selected && selected.url ? selected.url : originalUrl;
|
||||
var mediaPlaylist = selected ? "" : initialPlaylist;
|
||||
|
||||
if (selected) {
|
||||
try {
|
||||
mediaPlaylist = await requestTextWithRetry(rawUrl, getPlayHeaders(referer), playlistTimeout, function (body) {
|
||||
return /^\s*#EXTM3U/i.test(body) && !/#EXT-X-STREAM-INF/i.test(body);
|
||||
}, "selected-playlist", 1);
|
||||
} catch (selectedError) {
|
||||
console.log("[rou.video] selected playlist inspection failed: " + (selectedError && selectedError.message ? selectedError.message : selectedError));
|
||||
}
|
||||
}
|
||||
|
||||
var codecFamily = classifyRouCodecString(selected && selected.codecs);
|
||||
var streamTypes = [];
|
||||
var segmentUrl = mediaPlaylist ? firstRouMediaSegmentUrl(mediaPlaylist, rawUrl) : "";
|
||||
if (codecFamily === "unknown" && segmentUrl) {
|
||||
try {
|
||||
var codecProbe = await probeRouSegmentCodec(segmentUrl, referer, Math.min(4000, playlistTimeout));
|
||||
codecFamily = codecProbe.family;
|
||||
streamTypes = codecProbe.streamTypes;
|
||||
} catch (probeError) {
|
||||
console.log("[rou.video] segment codec probe failed: " + (probeError && probeError.message ? probeError.message : probeError));
|
||||
}
|
||||
}
|
||||
|
||||
var mdkPlaylist = codecFamily === "modern" || !mediaPlaylist ? null : createMdkPlaylistDataUrl(mediaPlaylist);
|
||||
var rawPlayerType = codecFamily === "avc" && mediaPlaylist && !mdkPlaylist ? "ijk" : "system";
|
||||
var resolution = selected && (selected.resolution || (selected.width && selected.height ? selected.width + "x" + selected.height : ""));
|
||||
console.log("[rou.video] selected quality=" + (resolution || "single") + " codec=" + codecFamily + " engine=" + (mdkPlaylist && codecFamily === "avc" ? "MDK" : rawPlayerType));
|
||||
return {
|
||||
rawUrl: rawUrl,
|
||||
rawPlayerType: rawPlayerType,
|
||||
codecFamily: codecFamily,
|
||||
codecs: selected ? selected.codecs : "",
|
||||
resolution: resolution || "",
|
||||
bandwidth: selected ? selected.bandwidth : 0,
|
||||
streamTypes: streamTypes,
|
||||
mdkPlaylist: mdkPlaylist
|
||||
};
|
||||
}
|
||||
function buildVideoItem(item) {
|
||||
item = item || {};
|
||||
var id = text(item.id || item.vid || item.nameZh || item.name);
|
||||
var title = text(item.nameZh || item.name || item.title || id);
|
||||
var cover = proxyCoverUrl(item.coverImageUrl || "");
|
||||
var tags = ensureArray(item.tagsZh && item.tagsZh.length ? item.tagsZh : item.tags);
|
||||
var durationText = formatDuration(item.duration);
|
||||
var detailUrl = item.id ? (ROU_SITE + "/v/" + encodeURIComponent(item.id)) : absoluteUrl(item.ref || "");
|
||||
var desc = [];
|
||||
|
||||
if (item.vid) desc.push("编号: " + text(item.vid));
|
||||
if (durationText) desc.push("时长: " + durationText);
|
||||
if (item.viewCount != null) desc.push("播放: " + formatCount(item.viewCount));
|
||||
if (tags.length) desc.push("标签: " + tags.slice(0, 4).join(" / "));
|
||||
if (item.ref) desc.push("来源: " + text(item.ref));
|
||||
|
||||
return {
|
||||
id: id || detailUrl || title,
|
||||
type: "movie",
|
||||
title: title || id || "肉视频",
|
||||
description: desc.join("\n"),
|
||||
posterPath: cover,
|
||||
backdropPath: cover,
|
||||
mediaType: "movie",
|
||||
duration: Math.round(Number(item.duration || 0)) || 0,
|
||||
durationText: durationText,
|
||||
previewUrl: "",
|
||||
videoUrl: "",
|
||||
url: "",
|
||||
playUrl: "",
|
||||
link: detailUrl,
|
||||
detailUrl: detailUrl,
|
||||
playerType: "none"
|
||||
};
|
||||
}
|
||||
|
||||
function mapList(items) {
|
||||
return ensureArray(items).map(buildVideoItem);
|
||||
}
|
||||
|
||||
async function loadHomeData() {
|
||||
var html = await fetchHtml(ROU_SITE + "/home");
|
||||
return extractNextData(html);
|
||||
}
|
||||
|
||||
async function loadSectionVideos(params) {
|
||||
params = params || {};
|
||||
var section = text(params.section || "latestVideos");
|
||||
var data = await loadHomeData();
|
||||
var list = ensureArray(data[section]);
|
||||
|
||||
if (!list.length) {
|
||||
return [createPlaceholderItem("未找到该分区内容")];
|
||||
}
|
||||
|
||||
return mapList(list);
|
||||
}
|
||||
|
||||
function buildTagUrl(tag, order, page) {
|
||||
tag = text(tag || "自拍流出");
|
||||
order = text(order || "createdAt");
|
||||
page = parseInt(page, 10) || 1;
|
||||
var url = ROU_SITE + "/t/" + encodeURIComponent(tag) + "?order=" + encodeURIComponent(order);
|
||||
url += "&page=" + encodeURIComponent(page);
|
||||
return url;
|
||||
}
|
||||
|
||||
async function loadTagVideos(params) {
|
||||
params = params || {};
|
||||
var tag = text(params.tag || params.category || "自拍流出");
|
||||
var order = text(params.order || params.sort || "createdAt");
|
||||
var page = parseInt(params.page, 10) || 1;
|
||||
var html = await fetchHtml(buildTagUrl(tag, order, page));
|
||||
var data = extractNextData(html);
|
||||
var list = ensureArray(data.videos);
|
||||
|
||||
if (!list.length) {
|
||||
return [createPlaceholderItem("未找到「" + tag + "」相关内容")];
|
||||
}
|
||||
|
||||
return mapList(list);
|
||||
}
|
||||
|
||||
async function searchVideos(params) {
|
||||
params = params || {};
|
||||
var keyword = text(params.keyword || params.query || params.q || "");
|
||||
var page = parseInt(params.page, 10) || 1;
|
||||
if (!keyword) return [createPlaceholderItem("请输入搜索关键词")];
|
||||
|
||||
var url = ROU_SITE + "/search?q=" + encodeURIComponent(keyword) + "&t=&sort=&page=" + encodeURIComponent(page);
|
||||
|
||||
var html = await fetchHtml(url);
|
||||
var data = extractNextData(html);
|
||||
var list = ensureArray(data.videos);
|
||||
|
||||
if (!list.length) {
|
||||
return [createPlaceholderItem("未找到与「" + keyword + "」相关的内容")];
|
||||
}
|
||||
|
||||
return mapList(list);
|
||||
}
|
||||
|
||||
async function loadDetail(link) {
|
||||
try {
|
||||
var requestProfile = arguments.length > 1 && arguments[1] ? arguments[1] : {};
|
||||
var detailLink = link;
|
||||
if (detailLink && typeof detailLink === "object") {
|
||||
detailLink = detailLink.detailUrl || detailLink.link || detailLink.id || "";
|
||||
}
|
||||
detailLink = text(detailLink);
|
||||
if (!detailLink) return createPlaceholderItem("\u7f3a\u5c11\u8be6\u60c5\u94fe\u63a5");
|
||||
|
||||
var html = await fetchHtmlWithOptions(
|
||||
detailLink,
|
||||
Number(requestProfile.htmlTimeout || 8000),
|
||||
Number(requestProfile.htmlAttempts || 2),
|
||||
"detail"
|
||||
);
|
||||
var data = extractNextData(html);
|
||||
var video = data.video || {};
|
||||
var item = buildVideoItem(video);
|
||||
rememberRouResourceLink(item.title, detailLink);
|
||||
var payload = decodeRouVideoPayload(data.ev);
|
||||
var videoUrl = normalizeRouVideoUrl(payload.videoUrl || "");
|
||||
if (!videoUrl) throw new Error("video URL not found");
|
||||
|
||||
var detailBase = {
|
||||
id: item.id,
|
||||
type: "detail",
|
||||
title: item.title,
|
||||
description: item.description || "\u6682\u65e0\u7b80\u4ecb",
|
||||
posterPath: item.posterPath,
|
||||
backdropPath: item.backdropPath,
|
||||
mediaType: "movie",
|
||||
duration: item.duration,
|
||||
durationText: item.durationText,
|
||||
previewUrl: text(payload.thumbVTTUrl || ""),
|
||||
link: "",
|
||||
detailUrl: ""
|
||||
};
|
||||
|
||||
try {
|
||||
var playback = await resolveRouPlaybackPlan(
|
||||
videoUrl,
|
||||
detailLink,
|
||||
Number(requestProfile.playlistTimeout || 6000),
|
||||
Number(requestProfile.playlistAttempts || 2)
|
||||
);
|
||||
videoUrl = playback.rawUrl || videoUrl;
|
||||
var playHeaders = getPlayHeaders(detailLink);
|
||||
var rawLine = Object.assign({}, detailBase, {
|
||||
id: item.id + ":raw",
|
||||
title: item.title,
|
||||
description: (item.description || "\u6682\u65e0\u7b80\u4ecb") + "\n\u7ebf\u8def: Auto/MPV \u539f\u59cb HLS",
|
||||
videoUrl: videoUrl,
|
||||
url: videoUrl,
|
||||
playUrl: videoUrl,
|
||||
playerType: playback.rawPlayerType || "system",
|
||||
customHeaders: playHeaders,
|
||||
headers: playHeaders
|
||||
});
|
||||
var mdkLine = playback.mdkPlaylist ? Object.assign({}, detailBase, {
|
||||
id: item.id + ":mdk",
|
||||
title: item.title,
|
||||
description: (item.description || "\u6682\u65e0\u7b80\u4ecb") + "\n\u7ebf\u8def: MDK \u6807\u51c6\u5316\uff08" + playback.mdkPlaylist.segmentCount + " \u4e2a TS \u5206\u7247\uff09",
|
||||
videoUrl: playback.mdkPlaylist.url,
|
||||
url: playback.mdkPlaylist.url,
|
||||
playUrl: playback.mdkPlaylist.url,
|
||||
playerType: "ijk",
|
||||
customHeaders: playHeaders,
|
||||
headers: playHeaders
|
||||
}) : null;
|
||||
|
||||
if (mdkLine && playback.codecFamily === "avc") return [mdkLine, rawLine];
|
||||
if (mdkLine && playback.codecFamily === "unknown") return [rawLine, mdkLine];
|
||||
return [rawLine];
|
||||
} catch (playlistError) {
|
||||
console.log("[rou.video] playback inspection failed: " + (playlistError && playlistError.message ? playlistError.message : playlistError));
|
||||
var fallbackHeaders = getPlayHeaders(detailLink);
|
||||
return Object.assign({}, detailBase, {
|
||||
videoUrl: videoUrl,
|
||||
url: videoUrl,
|
||||
playUrl: videoUrl,
|
||||
playerType: "system",
|
||||
customHeaders: fallbackHeaders,
|
||||
headers: fallbackHeaders
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[rou.video] loadDetail failed", e && e.message ? e.message : e);
|
||||
return {
|
||||
id: text(link) || "detail",
|
||||
type: "detail",
|
||||
title: "\u8089\u89c6\u9891",
|
||||
description: "\u8be6\u60c5\u9875\u89e3\u6790\u5931\u8d25",
|
||||
posterPath: "",
|
||||
backdropPath: "",
|
||||
mediaType: "movie",
|
||||
duration: 0,
|
||||
durationText: "",
|
||||
previewUrl: "",
|
||||
videoUrl: "",
|
||||
url: "",
|
||||
playUrl: "",
|
||||
playerType: "none",
|
||||
link: ""
|
||||
};
|
||||
}
|
||||
}
|
||||
function scoreRouResourceCandidate(item, targetTitle) {
|
||||
var candidate = normalizeRouMatchTitle(item && item.title);
|
||||
var target = normalizeRouMatchTitle(targetTitle);
|
||||
if (!candidate || !target) return 0;
|
||||
if (candidate === target) return 100000;
|
||||
if (candidate.indexOf(target) >= 0 || target.indexOf(candidate) >= 0) {
|
||||
return 50000 - Math.abs(candidate.length - target.length);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function findRouResourceItem(title) {
|
||||
title = stripRouLineSuffix(title);
|
||||
if (!title) return null;
|
||||
|
||||
var remembered = getRememberedRouResourceLink(title);
|
||||
if (remembered) {
|
||||
return { title: title, link: remembered, detailUrl: remembered };
|
||||
}
|
||||
|
||||
var url = ROU_SITE + "/search?q=" + encodeURIComponent(title) + "&t=&sort=&page=1";
|
||||
var html = await requestTextWithRetry(url, ROU_HEADERS, 6000, function (body) {
|
||||
return body.indexOf("__NEXT_DATA__") >= 0;
|
||||
}, "resource-search", 1);
|
||||
var data = extractNextData(html);
|
||||
var candidates = mapList(ensureArray(data.videos)).filter(function (item) {
|
||||
return item && item.link;
|
||||
});
|
||||
var best = null;
|
||||
var bestScore = 0;
|
||||
|
||||
for (var i = 0; i < candidates.length; i++) {
|
||||
var score = scoreRouResourceCandidate(candidates[i], title);
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
best = candidates[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (bestScore >= 50000 && best) {
|
||||
rememberRouResourceLink(best.title || title, best.link);
|
||||
return best;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadResource(params) {
|
||||
params = params || {};
|
||||
var rawTitle = params.seriesName || params.title || params.name || params.episodeName || "";
|
||||
var title = stripRouLineSuffix(rawTitle);
|
||||
if (!title) return [];
|
||||
|
||||
try {
|
||||
var matched = await findRouResourceItem(title);
|
||||
if (!matched || !matched.link) return [];
|
||||
|
||||
var detail = await loadDetail(matched.link, {
|
||||
htmlTimeout: 6000,
|
||||
htmlAttempts: 1,
|
||||
playlistTimeout: 5000,
|
||||
playlistAttempts: 2
|
||||
});
|
||||
var lines = Array.isArray(detail)
|
||||
? detail
|
||||
: (detail && Array.isArray(detail.playSources) && detail.playSources.length
|
||||
? detail.playSources
|
||||
: [detail]);
|
||||
var resources = [];
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i] || {};
|
||||
var url = String(line.videoUrl || line.url || "");
|
||||
if (!url) continue;
|
||||
var isMdk = /:mdk$/i.test(String(line.id || "")) || /MDK\s*标准化线路/i.test(String(line.title || ""));
|
||||
var name = isMdk ? "肉视频 MDK 标准化" : "肉视频 MPV 原始";
|
||||
resources.push({
|
||||
name: name,
|
||||
description: "匹配: " + (matched.title || title) + "\n线路: " + (isMdk ? "标准 TS 清单" : "原始伪装 HLS"),
|
||||
url: url
|
||||
});
|
||||
}
|
||||
|
||||
return resources;
|
||||
} catch (e) {
|
||||
console.error("[rou.video] loadResource failed", e && e.message ? e.message : e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -742,6 +742,12 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/蜜桃视频.py"
|
||||
},
|
||||
{
|
||||
"key": "亚色影库",
|
||||
"name": "🐬亚色影库.py🔞",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/亚色影库.py"
|
||||
},
|
||||
{
|
||||
"key": "hanime1",
|
||||
"name": "🐬hanime1动漫.py🔞",
|
||||
"type": 3,
|
||||
|
||||
@@ -575,6 +575,12 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/蜜桃视频.py"
|
||||
},
|
||||
{
|
||||
"key": "亚色影库",
|
||||
"name": "🐬亚色影库.py🔞",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/亚色影库.py"
|
||||
},
|
||||
{
|
||||
"key": "hanime1",
|
||||
"name": "🐬hanime1动漫.py🔞",
|
||||
"type": 3,
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import requests
|
||||
from urllib.parse import quote, urljoin
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
except Exception:
|
||||
BeautifulSoup = None
|
||||
try:
|
||||
from base.spider import Spider as BaseSpider
|
||||
except Exception:
|
||||
BaseSpider = object
|
||||
|
||||
class Spider(BaseSpider):
|
||||
BASE_URL = 'https://www.yasetube.com'
|
||||
HEADERS = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36','Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8','Accept-Language':'zh-CN,zh;q=0.9,en;q=0.8','Referer':'https://www.yasetube.com/'}
|
||||
CATS = {'nvce':'女厕偷拍','fc2-ppv':'FC2 PPV','me':'Mesubuta系列','milf':'MILF人妻无码','dalu':'自拍偷拍','madou':'品牌传媒'}
|
||||
|
||||
def __init__(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.HEADERS)
|
||||
|
||||
def getName(self):
|
||||
return '亚色影库'
|
||||
|
||||
def init(self, extend=''):
|
||||
return None
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return any(x in url.lower() for x in ['.m3u8','.mp4','.flv','.mkv','.avi','.ts'])
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return True
|
||||
|
||||
def destroy(self):
|
||||
return None
|
||||
|
||||
def _get(self, url):
|
||||
url = url if str(url).startswith('http') else urljoin(self.BASE_URL, url)
|
||||
try:
|
||||
r = self.session.get(url, timeout=12, verify=False, allow_redirects=True)
|
||||
if not r.encoding or r.encoding.lower() == 'iso-8859-1':
|
||||
r.encoding = 'utf-8'
|
||||
return r.text
|
||||
except requests.RequestException:
|
||||
return ''
|
||||
|
||||
def _soup(self, html):
|
||||
return BeautifulSoup(html, 'html.parser') if BeautifulSoup else None
|
||||
|
||||
def _txt(self, s):
|
||||
return re.sub(r'\s+', ' ', s or '').strip()
|
||||
|
||||
def _abs(self, u):
|
||||
if not u:
|
||||
return ''
|
||||
if u.startswith('//'):
|
||||
return 'https:' + u
|
||||
return urljoin(self.BASE_URL, u)
|
||||
|
||||
def _meta(self, html, name):
|
||||
m = re.search(r'<meta[^>]+(?:property|name)=["\']%s["\'][^>]+content=["\']([^"\']+)' % re.escape(name), html, re.I)
|
||||
return self._txt(m.group(1)) if m else ''
|
||||
|
||||
def _id(self, url):
|
||||
url = self._abs(url).split('?')[0].rstrip('/')
|
||||
return url.replace(self.BASE_URL + '/', '')
|
||||
|
||||
def _parse_list(self, html):
|
||||
arr = []
|
||||
if BeautifulSoup and html:
|
||||
soup = self._soup(html)
|
||||
for a in soup.select('article.loop-video.thumb-block a[href]'):
|
||||
href = self._abs(a.get('href'))
|
||||
if '/video/' not in href:
|
||||
continue
|
||||
img = a.select_one('img')
|
||||
title = self._txt(a.get('title') or (img.get('alt') if img else '') or (a.select_one('header.entry-header span').get_text(' ', strip=True) if a.select_one('header.entry-header span') else ''))
|
||||
pic = self._abs((img.get('data-src') or img.get('src') or img.get('data-original')) if img else '')
|
||||
remark = self._txt(' '.join([x.get_text(' ', strip=True) for x in a.select('span.hd-video,span.views,span.duration')]))
|
||||
if title and href:
|
||||
arr.append({'vod_id':self._id(href),'vod_name':title,'vod_pic':pic,'vod_remarks':remark})
|
||||
if not arr:
|
||||
for it in re.findall(r'<article[^>]+loop-video[\s\S]*?</article>', html, re.I):
|
||||
h = re.search(r'<a[^>]+href=["\']([^"\']+)["\'][^>]*title=["\']([^"\']+)', it, re.I)
|
||||
p = re.search(r'<img[^>]+(?:data-src|src)=["\']([^"\']+)', it, re.I)
|
||||
if h:
|
||||
arr.append({'vod_id':self._id(h.group(1)),'vod_name':self._txt(h.group(2)),'vod_pic':self._abs(p.group(1)) if p else '','vod_remarks':'HD' if 'hd-video' in it else ''})
|
||||
seen, out = set(), []
|
||||
for v in arr:
|
||||
if v['vod_id'] not in seen:
|
||||
seen.add(v['vod_id'])
|
||||
out.append(v)
|
||||
return out
|
||||
|
||||
def _cats(self):
|
||||
html = self._get('/categories')
|
||||
classes = []
|
||||
if BeautifulSoup and html:
|
||||
soup = self._soup(html)
|
||||
for a in soup.select('a[href*="/video/category/"]'):
|
||||
href = a.get('href') or ''
|
||||
slug = href.rstrip('/').split('/')[-1]
|
||||
name = self._txt(a.get('title') or a.get_text(' ', strip=True))
|
||||
if slug and name and slug not in [x['type_id'] for x in classes]:
|
||||
classes.append({'type_id':slug,'type_name':name})
|
||||
if not classes:
|
||||
classes = [{'type_id':k,'type_name':v} for k,v in self.CATS.items()]
|
||||
return classes[:30]
|
||||
|
||||
def homeContent(self, filter=False):
|
||||
return {'class':self._cats(),'filters':{},'list':self.homeVideoContent()['list']}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {'list':self._parse_list(self._get('/'))[:20]}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, ext):
|
||||
pg = str(pg or '1')
|
||||
if tid in ['latest','home','']:
|
||||
url = '/' if pg == '1' else '/page/%s/' % pg
|
||||
else:
|
||||
url = '/video/category/%s/' % tid if pg == '1' else '/video/category/%s/page/%s/' % (tid, pg)
|
||||
return {'page':int(pg),'pagecount':999,'limit':20,'total':999,'list':self._parse_list(self._get(url))}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0] if isinstance(ids, list) else ids
|
||||
url = vid if str(vid).startswith('http') else self.BASE_URL + '/' + str(vid).lstrip('/')
|
||||
html = self._get(url)
|
||||
name = self._meta(html, 'og:title')
|
||||
pic = self._meta(html, 'og:image')
|
||||
desc = self._meta(html, 'og:description')
|
||||
if BeautifulSoup and html:
|
||||
soup = self._soup(html)
|
||||
h = soup.select_one('h1.entry-title')
|
||||
if h:
|
||||
name = self._txt(h.get_text(' ', strip=True)) or name
|
||||
im = soup.select_one('meta[itemprop="thumbnailUrl"]')
|
||||
if im and im.get('content'):
|
||||
pic = im.get('content') or pic
|
||||
ds = soup.select_one('meta[itemprop="description"]')
|
||||
if ds and ds.get('content'):
|
||||
desc = ds.get('content') or desc
|
||||
play = self._find_play(html) or url
|
||||
vod = {'vod_id':vid,'vod_name':name or str(vid),'vod_pic':self._abs(pic),'type_name':'','vod_year':'','vod_area':'','vod_actor':'','vod_director':'','vod_content':desc or name or '','vod_play_from':'嗅探','vod_play_url':'正片$%s' % play}
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
pg = str(pg or '1')
|
||||
kw = quote(key)
|
||||
url = '/?s=%s' % kw if pg == '1' else '/page/%s/?s=%s' % (pg, kw)
|
||||
return {'page':int(pg),'pagecount':999,'limit':20,'total':999,'list':self._parse_list(self._get(url))}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
u = self._abs(id)
|
||||
if self.isVideoFormat(u):
|
||||
return {'parse':0,'playUrl':'','url':u,'header':self.HEADERS}
|
||||
html = self._get(u)
|
||||
play = self._find_play(html)
|
||||
if play and self.isVideoFormat(play):
|
||||
return {'parse':0,'playUrl':'','url':play,'header':self.HEADERS}
|
||||
return {'parse':1,'playUrl':'','url':u,'header':self.HEADERS}
|
||||
|
||||
def _find_play(self, html):
|
||||
if not html:
|
||||
return ''
|
||||
pats = [r'<source[^>]+src=["\']([^"\']+)',r'<video[^>]+src=["\']([^"\']+)',r'(https?:\\?/\\?/[^"\'<>]+?\.(?:m3u8|mp4)(?:\?[^"\'<>]*)?)',r'file["\']?\s*[:=]\s*["\']([^"\']+)',r'url["\']?\s*[:=]\s*["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)']
|
||||
for p in pats:
|
||||
m = re.search(p, html, re.I)
|
||||
if m:
|
||||
return self._abs(m.group(1).replace('\\/','/'))
|
||||
return ''
|
||||
@@ -1,557 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
TVBox 本地 Py/Js 爬虫聚合源(精简版)
|
||||
========================================
|
||||
仅保留扫描与配置生成功能。
|
||||
包含:子文件夹后缀、增量合并、锁定站点、分页浏览。
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
import hashlib
|
||||
import time
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
# ==========================================================================
|
||||
# 📂 【配置区】
|
||||
# ==========================================================================
|
||||
PY_DIR = "/storage/emulated/0/peekpro/py"
|
||||
JS_DIR = "/storage/emulated/0/peekpro/js"
|
||||
JAR_DIR = "/storage/emulated/0/peekpro/jar"
|
||||
SAVE_PATH = "/storage/emulated/0/peekpro/智能接口.json"
|
||||
LOGO_PATH = "/storage/emulated/0/peekpro/jar/头像.gif"
|
||||
|
||||
_LOCKED_SITES = [
|
||||
|
||||
]
|
||||
_LOCKED_KEYS = {""}
|
||||
|
||||
GENERATED_KEY_PREFIX = "local_auto_"
|
||||
PAGE_SIZE = 60
|
||||
# ==========================================================================
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.inited = False
|
||||
|
||||
self.cache = {
|
||||
"categories": [],
|
||||
"file_index": {},
|
||||
"sources": [],
|
||||
"source_index": {},
|
||||
"type_counts": {},
|
||||
}
|
||||
|
||||
self.status = {
|
||||
"scan_time": "-",
|
||||
"included": 0,
|
||||
"manual_sites": 0,
|
||||
"generated_sites": 0,
|
||||
"added": 0,
|
||||
"removed": 0,
|
||||
"unchanged": 0,
|
||||
"write_state": "尚未扫描",
|
||||
"written": False,
|
||||
}
|
||||
|
||||
def getName(self):
|
||||
return "本地Py/Js聚合源(精简版)"
|
||||
|
||||
def init(self, extend):
|
||||
if self.inited:
|
||||
return
|
||||
self._scan_all()
|
||||
self._save_config_json()
|
||||
self.inited = True
|
||||
|
||||
# ==========================================================================
|
||||
# 🔍 【扫描核心】手动递归
|
||||
# ==========================================================================
|
||||
def _scan_dir(self, base_dir, ext_list):
|
||||
results = []
|
||||
if not base_dir:
|
||||
return results
|
||||
if not os.path.exists(base_dir):
|
||||
try:
|
||||
os.makedirs(base_dir, exist_ok=True)
|
||||
except Exception:
|
||||
return results
|
||||
if not os.path.isdir(base_dir):
|
||||
return results
|
||||
try:
|
||||
entries = os.listdir(base_dir)
|
||||
except Exception:
|
||||
return results
|
||||
for entry in sorted(entries):
|
||||
full_path = os.path.join(base_dir, entry)
|
||||
if entry.startswith("."):
|
||||
continue
|
||||
if os.path.isdir(full_path):
|
||||
results.extend(self._scan_dir(full_path, ext_list))
|
||||
elif os.path.isfile(full_path):
|
||||
lower_name = entry.lower()
|
||||
for ext in ext_list:
|
||||
if lower_name.endswith(ext):
|
||||
name_no_ext = entry[: -len(ext)]
|
||||
results.append((full_path, name_no_ext, ext))
|
||||
break
|
||||
return results
|
||||
|
||||
def _get_sub_sfx(self, full_path, base_dir):
|
||||
try:
|
||||
rel = os.path.relpath(full_path, base_dir)
|
||||
rel_parts = rel.split(os.sep)
|
||||
subfolder = rel_parts[0] if len(rel_parts) > 1 else ""
|
||||
except (ValueError, IndexError):
|
||||
subfolder = ""
|
||||
if not subfolder:
|
||||
return ""
|
||||
if subfolder.startswith("[") and subfolder.endswith("]"):
|
||||
return subfolder
|
||||
return f"[{subfolder}]"
|
||||
|
||||
def _scan_all(self):
|
||||
sources = []
|
||||
self_path = os.path.abspath(__file__) if hasattr(__file__, '__file__') else ""
|
||||
|
||||
scan_specs = [
|
||||
(self.PY_DIR, [".py"], "PY", 0),
|
||||
(self.JS_DIR, [".js"], "JS", 1),
|
||||
]
|
||||
|
||||
for dir_path, ext_list, type_tag, order in scan_specs:
|
||||
files = self._scan_dir(dir_path, ext_list)
|
||||
for full_path, name, ext in files:
|
||||
if self_path and os.path.abspath(full_path) == self_path:
|
||||
continue
|
||||
|
||||
identity = type_tag + "|" + full_path
|
||||
tid = base64.b64encode(identity.encode("utf-8")).decode("utf-8")
|
||||
sub_sfx = self._get_sub_sfx(full_path, dir_path)
|
||||
display_name = f"【{type_tag}】{name}{sub_sfx}"
|
||||
|
||||
source = {
|
||||
"type_id": tid,
|
||||
"type_name": display_name,
|
||||
"identity": identity,
|
||||
"_path": full_path,
|
||||
"_ext": ext.lstrip("."),
|
||||
"_dir": dir_path,
|
||||
"_type_tag": type_tag,
|
||||
"_sk": (order, name),
|
||||
"_sub_sfx": sub_sfx,
|
||||
}
|
||||
sources.append(source)
|
||||
|
||||
self.cache["file_index"][tid] = {
|
||||
"path": full_path,
|
||||
"ext": source["_ext"],
|
||||
"dir": dir_path,
|
||||
"type_tag": type_tag,
|
||||
"sub_sfx": sub_sfx,
|
||||
}
|
||||
|
||||
sources.sort(key=lambda x: x["_sk"])
|
||||
|
||||
self.cache["sources"] = sources
|
||||
self.cache["source_index"] = {}
|
||||
self.cache["type_counts"] = {}
|
||||
for s in sources:
|
||||
self.cache["source_index"][s["type_id"]] = s
|
||||
tag = s["_type_tag"]
|
||||
self.cache["type_counts"][tag] = self.cache["type_counts"].get(tag, 0) + 1
|
||||
|
||||
self.cache["categories"] = [
|
||||
{"type_id": s["type_id"], "type_name": s["type_name"]}
|
||||
for s in sources
|
||||
]
|
||||
self.status["included"] = len(sources)
|
||||
|
||||
# ==========================================================================
|
||||
# 【增量合并配置生成】
|
||||
# ==========================================================================
|
||||
def _build_api(self, file_info):
|
||||
f_path = file_info["path"]
|
||||
base_dir = file_info["dir"]
|
||||
try:
|
||||
rel = os.path.relpath(f_path, base_dir)
|
||||
except ValueError:
|
||||
rel = os.path.basename(f_path)
|
||||
dir_name = os.path.basename(base_dir)
|
||||
return "./" + dir_name + "/" + rel
|
||||
|
||||
def _build_spider_value(self):
|
||||
jar_dir = self.JAR_DIR
|
||||
if not jar_dir or not os.path.isdir(jar_dir):
|
||||
return ""
|
||||
jar_files = []
|
||||
save_dir = os.path.dirname(self.SAVE_PATH)
|
||||
try:
|
||||
entries = sorted(os.listdir(jar_dir))
|
||||
except Exception:
|
||||
return ""
|
||||
for entry in entries:
|
||||
if entry.startswith("."):
|
||||
continue
|
||||
if entry.lower().endswith(".jar") and os.path.isfile(os.path.join(jar_dir, entry)):
|
||||
abs_jar = os.path.join(jar_dir, entry)
|
||||
try:
|
||||
rel = os.path.relpath(abs_jar, save_dir)
|
||||
except ValueError:
|
||||
rel = "jar/" + entry
|
||||
rel = "./" + rel.replace("\\", "/")
|
||||
if not rel.startswith("./"):
|
||||
rel = "./" + rel.lstrip("./")
|
||||
jar_files.append(rel)
|
||||
return ";".join(jar_files)
|
||||
|
||||
def _get_locked_api_set(self):
|
||||
locked = set()
|
||||
for site in self._LOCKED_SITES:
|
||||
for field in ("api", "homePage", "ext"):
|
||||
val = str(site.get(field, "")).strip()
|
||||
if val.startswith("./"):
|
||||
locked.add(val)
|
||||
return locked
|
||||
|
||||
def _is_generated_key(self, key):
|
||||
return str(key).startswith(self.GENERATED_KEY_PREFIX)
|
||||
|
||||
def _load_existing_config(self):
|
||||
if not os.path.isfile(self.SAVE_PATH):
|
||||
return None
|
||||
try:
|
||||
with open(self.SAVE_PATH, "r", encoding="utf-8") as fp:
|
||||
data = json.load(fp)
|
||||
return data if isinstance(data, dict) else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _generate_auto_sites(self):
|
||||
locked_paths = self._get_locked_api_set()
|
||||
sites = []
|
||||
for source in self.cache["sources"]:
|
||||
file_info = self.cache["file_index"].get(source["type_id"])
|
||||
if not file_info:
|
||||
continue
|
||||
f_path = file_info["path"]
|
||||
type_tag = file_info.get("type_tag", "PY")
|
||||
f_base = os.path.basename(f_path)
|
||||
if "." in f_base:
|
||||
f_base = f_base.rsplit(".", 1)[0]
|
||||
|
||||
api_path = self._build_api(file_info)
|
||||
sub_sfx = file_info.get("sub_sfx", "")
|
||||
|
||||
if api_path in locked_paths:
|
||||
continue
|
||||
|
||||
if not os.path.isfile(f_path):
|
||||
continue
|
||||
|
||||
key = self.GENERATED_KEY_PREFIX + type_tag.lower() + "_" + hashlib.sha256(
|
||||
(type_tag + "|" + f_path).encode("utf-8")
|
||||
).hexdigest()[:14]
|
||||
|
||||
sites.append({
|
||||
"key": key,
|
||||
"name": f"{f_base}{sub_sfx}",
|
||||
"type": 3,
|
||||
"searchable": 1,
|
||||
"quickSearch": 1,
|
||||
"filterable": 1,
|
||||
"api": api_path,
|
||||
})
|
||||
return sites
|
||||
|
||||
def _save_config_json(self):
|
||||
self.status["scan_time"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
new_auto_sites = self._generate_auto_sites()
|
||||
|
||||
existing = self._load_existing_config()
|
||||
manual_sites = []
|
||||
old_auto_keys = set()
|
||||
if existing and isinstance(existing.get("sites"), list):
|
||||
for site in existing["sites"]:
|
||||
if not isinstance(site, dict):
|
||||
continue
|
||||
k = site.get("key", "")
|
||||
if k in self._LOCKED_KEYS:
|
||||
continue
|
||||
if self._is_generated_key(k):
|
||||
old_auto_keys.add(k)
|
||||
else:
|
||||
manual_sites.append(site)
|
||||
|
||||
new_auto_keys = {s.get("key") for s in new_auto_sites}
|
||||
self.status["added"] = len(new_auto_keys - old_auto_keys)
|
||||
self.status["removed"] = len(old_auto_keys - new_auto_keys)
|
||||
self.status["unchanged"] = len(old_auto_keys & new_auto_keys)
|
||||
self.status["manual_sites"] = len(manual_sites)
|
||||
self.status["generated_sites"] = len(new_auto_sites)
|
||||
|
||||
config = {
|
||||
"logo": self.LOGO_PATH,
|
||||
"spider": self._build_spider_value(),
|
||||
"sites": list(self._LOCKED_SITES) + manual_sites + new_auto_sites,
|
||||
}
|
||||
|
||||
new_content = json.dumps(config, ensure_ascii=False, indent=2)
|
||||
if existing:
|
||||
old_content = json.dumps(existing, ensure_ascii=False, indent=2)
|
||||
if new_content == old_content:
|
||||
self.status["write_state"] = "配置未变化"
|
||||
self.status["written"] = True
|
||||
return
|
||||
|
||||
save_dir = os.path.dirname(self.SAVE_PATH)
|
||||
if save_dir and not os.path.exists(save_dir):
|
||||
try:
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
tmp = self.SAVE_PATH + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as fp:
|
||||
fp.write(new_content)
|
||||
fp.flush()
|
||||
os.fsync(fp.fileno())
|
||||
os.replace(tmp, self.SAVE_PATH)
|
||||
self.status["write_state"] = "已写入配置"
|
||||
self.status["written"] = True
|
||||
except Exception as e:
|
||||
self.status["write_state"] = "写入失败: {}".format(e)
|
||||
self.status["written"] = False
|
||||
|
||||
# ==========================================================================
|
||||
# 🔧 辅助方法
|
||||
# ==========================================================================
|
||||
def _get_file_info(self, tid):
|
||||
return self.cache["file_index"].get(tid)
|
||||
|
||||
def _count_str(self):
|
||||
c = self.cache["type_counts"]
|
||||
return f"共扫描到 {c.get('PY', 0)} 个PY文件, {c.get('JS', 0)} 个JS文件"
|
||||
|
||||
def _count_jar_str(self):
|
||||
if not os.path.isdir(self.JAR_DIR):
|
||||
return "jar 目录不存在"
|
||||
count = sum(
|
||||
1 for f in os.listdir(self.JAR_DIR)
|
||||
if f.lower().endswith(".jar") and os.path.isfile(os.path.join(self.JAR_DIR, f))
|
||||
)
|
||||
return f"共扫描到 {count} 个JAR文件"
|
||||
|
||||
def _page_number(self, value):
|
||||
try:
|
||||
return max(1, int(value))
|
||||
except Exception:
|
||||
return 1
|
||||
|
||||
def _paged_result(self, items, page, make_vod):
|
||||
total = len(items)
|
||||
page_size = max(1, self.PAGE_SIZE)
|
||||
page_count = max(1, (total + page_size - 1) // page_size)
|
||||
page = max(1, min(page, page_count))
|
||||
start = (page - 1) * page_size
|
||||
page_items = items[start: start + page_size]
|
||||
return {
|
||||
"page": page,
|
||||
"pagecount": page_count,
|
||||
"limit": page_size,
|
||||
"total": total,
|
||||
"list": [make_vod(item) for item in page_items],
|
||||
}
|
||||
|
||||
def _source_to_vod(self, source):
|
||||
return {
|
||||
"vod_id": source["type_id"],
|
||||
"vod_name": source["type_name"],
|
||||
"vod_pic": "",
|
||||
"vod_remarks": source["_type_tag"],
|
||||
}
|
||||
|
||||
# ==========================================================================
|
||||
# 📺 【TVBox 标准接口】
|
||||
# ==========================================================================
|
||||
def homeContent(self, filter):
|
||||
classes = [
|
||||
{"type_id": "all", "type_name": f"全部 ({len(self.cache['sources'])})"}
|
||||
]
|
||||
for tag in ("PY", "JS"):
|
||||
count = self.cache["type_counts"].get(tag, 0)
|
||||
if count:
|
||||
classes.append({"type_id": "type:" + tag, "type_name": f"{tag} ({count})"})
|
||||
return {"class": classes, "list": []}
|
||||
|
||||
def homeVod(self):
|
||||
info = self._count_str() + " | " + self._count_jar_str()
|
||||
return {"list": [{
|
||||
"vod_id": "__debug__",
|
||||
"vod_name": info,
|
||||
"vod_pic": "",
|
||||
"vod_remarks": "统计",
|
||||
}]}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, ext):
|
||||
page = self._page_number(pg)
|
||||
|
||||
if tid == "all":
|
||||
return self._paged_result(self.cache["sources"], page, self._source_to_vod)
|
||||
|
||||
if str(tid).startswith("type:"):
|
||||
source_type = str(tid).split(":", 1)[1].upper()
|
||||
items = [s for s in self.cache["sources"] if s["_type_tag"] == source_type]
|
||||
return self._paged_result(items, page, self._source_to_vod)
|
||||
|
||||
return self._category_content_single(tid)
|
||||
|
||||
def _category_content_single(self, tid):
|
||||
file_info = self._get_file_info(tid)
|
||||
if not file_info:
|
||||
return {"list": []}
|
||||
f_path = file_info["path"]
|
||||
if not os.path.exists(f_path):
|
||||
return {"list": []}
|
||||
|
||||
f_base = os.path.basename(f_path)
|
||||
if "." in f_base:
|
||||
f_base = f_base.rsplit(".", 1)[0]
|
||||
ext_name = file_info["ext"]
|
||||
type_tag = file_info.get("type_tag", "PY")
|
||||
sub_sfx = file_info.get("sub_sfx", "")
|
||||
|
||||
v_id = base64.b64encode(
|
||||
(type_tag + "|" + f_path).encode("utf-8")
|
||||
).decode("utf-8")
|
||||
|
||||
vod_name = f"{f_base}{sub_sfx}"
|
||||
vod_remarks = "[" + ext_name.upper() + "]"
|
||||
|
||||
return {
|
||||
"page": 1, "pagecount": 1, "limit": 1, "total": 1,
|
||||
"list": [{
|
||||
"vod_id": v_id,
|
||||
"vod_name": vod_name,
|
||||
"vod_pic": "",
|
||||
"vod_remarks": vod_remarks,
|
||||
}]
|
||||
}
|
||||
|
||||
def detailContent(self, array):
|
||||
try:
|
||||
v_id_raw = str(array[0]) if isinstance(array, (list, tuple)) and array else str(array or "")
|
||||
|
||||
if v_id_raw == "__debug__":
|
||||
return {"list": [self._status_detail()]}
|
||||
|
||||
v_id_padded = v_id_raw + "=" * ((4 - len(v_id_raw) % 4) % 4)
|
||||
raw = base64.b64decode(v_id_padded).decode("utf-8", errors="ignore")
|
||||
|
||||
if "|" in raw:
|
||||
type_tag, f_path = raw.split("|", 1)
|
||||
else:
|
||||
type_tag, f_path = "PY", raw
|
||||
|
||||
if not os.path.exists(f_path):
|
||||
return {"list": [{"vod_name": "文件不存在", "vod_content": "路径: " + f_path}]}
|
||||
|
||||
f_base = os.path.basename(f_path)
|
||||
if "." in f_base:
|
||||
f_base = f_base.rsplit(".", 1)[0]
|
||||
ext_name = f_path.rsplit(".", 1)[-1] if "." in f_path else "unknown"
|
||||
|
||||
file_info = self.cache["file_index"].get(v_id_raw)
|
||||
api_path = self._build_api(file_info) if file_info else f_path
|
||||
sub_sfx = file_info.get("sub_sfx", "") if file_info else ""
|
||||
|
||||
site_info = {
|
||||
"key": f_base + "_" + ext_name,
|
||||
"name": f"{f_base}{sub_sfx}",
|
||||
"type": 3,
|
||||
"searchable": 1,
|
||||
"quickSearch": 1,
|
||||
"filterable": 1,
|
||||
"api": api_path,
|
||||
}
|
||||
info_text = json.dumps(site_info, ensure_ascii=False, indent=2)
|
||||
|
||||
return {"list": [{
|
||||
"vod_name": "[" + ext_name.upper() + "] " + f_base + sub_sfx,
|
||||
"vod_pic": "",
|
||||
"vod_play_from": "配置信息",
|
||||
"vod_play_url": "查看配置$" + f_path,
|
||||
"vod_content": (
|
||||
"配置文件: " + self.SAVE_PATH + "\n\n"
|
||||
"站点类型: " + type_tag + " | 后缀: ." + ext_name + "\n\n"
|
||||
"站点配置:\n" + info_text + "\n\n"
|
||||
"文件路径: " + f_path
|
||||
),
|
||||
}]}
|
||||
except Exception as e:
|
||||
return {"list": [{"vod_name": "解析错误", "vod_content": str(e)}]}
|
||||
|
||||
def _status_detail(self):
|
||||
c = self.cache["type_counts"]
|
||||
content = (
|
||||
"扫描时间: {scan_time}\n"
|
||||
"有效源: {included}\n"
|
||||
"分类统计: PY={py} JS={js}\n\n"
|
||||
"保留手工站点: {manual}\n"
|
||||
"自动注入站点: {generated}\n"
|
||||
"变更预览: +{added} -{removed} ={unchanged}\n"
|
||||
"写入状态: {state}\n\n"
|
||||
"{py_info}\n"
|
||||
"{jar_info}\n\n"
|
||||
"配置文件: {save}\n\n"
|
||||
"已扫描文件列表:\n"
|
||||
"{file_list}"
|
||||
).format(
|
||||
scan_time=self.status["scan_time"],
|
||||
included=self.status["included"],
|
||||
py=c.get("PY", 0), js=c.get("JS", 0),
|
||||
manual=self.status["manual_sites"],
|
||||
generated=self.status["generated_sites"],
|
||||
added=self.status["added"],
|
||||
removed=self.status["removed"],
|
||||
unchanged=self.status["unchanged"],
|
||||
state=self.status["write_state"],
|
||||
py_info=self._count_str(),
|
||||
jar_info=self._count_jar_str(),
|
||||
save=self.SAVE_PATH,
|
||||
file_list="\n".join(
|
||||
f" [{fin.get('type_tag', fin['ext'].upper())}] {fin['path']}"
|
||||
for fin in self.cache["file_index"].values()
|
||||
) or " 无",
|
||||
)
|
||||
return {
|
||||
"vod_id": "__debug__",
|
||||
"vod_name": "扫描状态详情",
|
||||
"vod_pic": "",
|
||||
"vod_remarks": self.status["write_state"],
|
||||
"vod_content": content,
|
||||
}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
page = self._page_number(pg)
|
||||
keyword = str(key or "").strip().lower()
|
||||
if not keyword:
|
||||
return {"list": []}
|
||||
items = [
|
||||
s for s in self.cache["sources"]
|
||||
if keyword in s["type_name"].lower()
|
||||
or keyword in s["_type_tag"].lower()
|
||||
or keyword in os.path.basename(s["_path"]).lower()
|
||||
]
|
||||
return self._paged_result(items, page, self._source_to_vod)
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = id.split("$")[-1] if "$" in id else id
|
||||
return {"url": url, "header": {}, "parse": 0}
|
||||
|
||||
def destroy(self):
|
||||
return "destroy"
|
||||
+26
-22
@@ -14,33 +14,37 @@ class Spider(Spider):
|
||||
# ==========================================================================
|
||||
# 📂 【配置区】
|
||||
# ==========================================================================
|
||||
PY_DIR = "/storage/emulated/0/TV/小百合/py"
|
||||
JS_DIR = "/storage/emulated/0/TV/小百合/js"
|
||||
JAR_DIR = "/storage/emulated/0/TV/小百合/jar"
|
||||
SAVE_PATH = "/storage/emulated/0/TV/小百合/自动接口.json"
|
||||
LOGO_PATH = "/storage/emulated/0/TV/小百合/jar/头像.gif"
|
||||
PY_DIR = "/storage/emulated/0/TV/海豚/py"
|
||||
JS_DIR = "/storage/emulated/0/TV/海豚/js"
|
||||
JAR_DIR = "/storage/emulated/0/TV/海豚/jar"
|
||||
SAVE_PATH = "/storage/emulated/0/TV/海豚/自动接口.json"
|
||||
LOGO_PATH = "/https://img.freepik.com/free-vector/cute-dolphin-swimming-cartoon-vector-icon-illustration-animal-nature-icon-isolated-flat-vector_138676-12582.jpg?semt=ais_hybrid&w=740&q=80"
|
||||
|
||||
# 🔒 锁定在 sites 第 0、1 位的配置,无论扫描结果如何始终存在
|
||||
_LOCKED_SITES = [
|
||||
{
|
||||
"key": "FishConfig",
|
||||
"name": "🍼┆设置┆中心[工具]",
|
||||
"key": "自动加载",
|
||||
"name": "自动加载",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/自动加载.py"
|
||||
},
|
||||
{
|
||||
"name": "弹幕",
|
||||
"key": "弹幕豆瓣",
|
||||
"type": 3,
|
||||
"api": "csp_FishConfig"
|
||||
},
|
||||
{
|
||||
"key": "Local",
|
||||
"name": "📁┆文件┆浏览[工具]",
|
||||
"type": 3,
|
||||
"api": "csp_Local",
|
||||
"searchable": 0,
|
||||
"changeable": 0,
|
||||
"indexs": 0,
|
||||
"style": {
|
||||
"type": "list"
|
||||
},
|
||||
"ext": "https://6800.kstore.vip/share.json"
|
||||
}
|
||||
"api": "csp_SecureDanmu",
|
||||
"searchable": 1,
|
||||
"jar": "https://ghfast.top/https://raw.githubusercontent.com/goodcommunication/mydm/main/danmu-spider-native.jar",
|
||||
"ext": {
|
||||
"apiUrls": [
|
||||
"https://danmu.iyo.us.ci/theft-dastardly-prognosis-hula-agenda2-dropkick|公益源",
|
||||
"https://logo.saodu.work:8888/87654321|公益源1",
|
||||
"https://dm.ljiaovm.com/luosen|公益源2"
|
||||
],
|
||||
"titleMappingsUrl": "https://ghfast.top/https://raw.githubusercontent.com/goodcommunication/mydm/main/yins.json",
|
||||
"filter": "./lib/douban.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
_LOCKED_KEYS = {"FishConfig", "Local"}
|
||||
# ==========================================================================
|
||||
|
||||
Reference in New Issue
Block a user