From 972d6ecc75547478bc602799fed090f234882fb3 Mon Sep 17 00:00:00 2001 From: mediabot-pt <295750538+mediabot-pt@users.noreply.github.com> Date: Mon, 6 Jul 2026 01:47:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20NexusPHP=E9=BB=98=E8=AE=A4=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E6=96=B0=E5=86=99=E6=B3=95(mediabot=E8=B4=9F=E8=B4=A3?= =?UTF-8?q?HTTP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/nexusphp-default.groovy | 183 +++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 docs/nexusphp-default.groovy diff --git a/docs/nexusphp-default.groovy b/docs/nexusphp-default.groovy new file mode 100644 index 0000000..2794870 --- /dev/null +++ b/docs/nexusphp-default.groovy @@ -0,0 +1,183 @@ +// NexusPHP 默认脚本 +// 适用: carpt / cbg / ptfans / ptskit / hdfans / cyanbug / qingwa 等 +// medibo 负责 HTTP,脚本只做解析 + 状态码判断 + +// ==================== medibo 入口 (统一分发) ==================== + +Map parse(context) { + if (context.status != 200) { + return switch(context.status) { + case 302 -> [redirect: context.headers?.Location ?: ''] + case 403 -> [banned: true, message: 'IP被禁'] + case 429 -> [rateLimited: true] + default -> [error: true, status: context.status, message: "HTTP ${context.status}"] + } + } + return switch(context.pageKey) { + case 'sync' -> parseUserProfile(Jsoup.parse(context.content)) + case 'search' -> parseSearchPage(Jsoup.parse(context.content)) + case 'detail' -> parseDetailPage(Jsoup.parse(context.content)) + case 'signIn' -> parseSignPage(Jsoup.parse(context.content)) + case 'download'-> parseDownload(context) + default -> [pageName: context.pageKey, message: '未实现'] + } +} + +// ==================== 用户信息 / 同步 ==================== + +private Map parseUserProfile(doc) { + def fields = extractUserInfo(doc) + if (fields.isEmpty()) return [pageName: '用户信息', matched: false, message: '未提取到数据'] + + def result = [pageName: '用户信息', matched: true] + if (fields.upload) result.upload = parseTraffic(fields.upload) + if (fields.download) result.download = parseTraffic(fields.download) + if (fields.ratio) result.shareRatio = fields.ratio == '无限' ? 999.0 : parseDouble(fields.ratio) + if (fields.bonus) result.bonus = parseDouble(fields.bonus) + if (fields.seeding) result.seeding = fields.seeding as int + if (fields.leeching) result.leeching = fields.leeching as int + if (fields.invites) result.invites = fields.invites as int + if (fields.hnr) result.hnr = fields.hnr as int + return result +} + +// ==================== 搜索页 ==================== + +private Map parseSearchPage(doc) { + def items = [] + doc.select('table.torrents tr:has(td.rowfollow):not(:has(.colhead))').each { row -> + def titleEl = row.selectFirst('td.embedded a[href*=details]') + if (!titleEl) return + def freeEl = row.selectFirst('font.promotion') + def tds = row.select('td.rowfollow') + def size = '', seeders = '', leechers = '', completed = '', time = '' + for (int i = 0; i < tds.size(); i++) { + def cell = tds[i].text() + if (!cell) continue + if (cell.contains('GB')||cell.contains('MB')||cell.contains('TB')||cell.contains('KB')) size = cell + else if (cell ==~ /^\d+$/ && !seeders) seeders = cell + else if (cell ==~ /^\d+$/ && seeders && !leechers) leechers = cell + else if (cell ==~ /^\d+$/ && leechers) completed = cell + else if (cell ==~ /^\d+年|^\d+月|^\d+天|^\d+时/) time = cell + } + items << [ + title: titleEl.text(), + detailUrl: titleEl.attr('href'), + free: freeEl?.text(), + size: parseSize(size), + seeders: seeders as int, + leechers: leechers as int, + grabs: completed as int, + publishDate: time, + ] + } + return [pageName: '搜索', items: items] +} + +// ==================== 种子详情 ==================== + +private Map parseDetailPage(doc) { + def title = doc.selectFirst('#top h1, h1')?.text() + def subtitle = doc.selectFirst('td.embedded b')?.text() + def size = doc.selectFirst('td:has(b:contains(大小)) + td, td.rowhead:contains(大小) + td')?.text() + def seeders = doc.selectFirst('td:contains(做种者) + td')?.text() + def leechers = doc.selectFirst('td:contains(下载者) + td')?.text() + + // 提取下载链接 + def dlUrl = doc.selectFirst('a[href*=download]:not([href*=details])')?.attr('href') + + return [pageName: '种子详情', matched: title != null, + title: title, subtitle: subtitle, size: parseSize(size), + seeders: seeders as int, leechers: leechers as int, + downloadUrl: dlUrl + ] +} + +// ==================== 签到 ==================== + +private Map parseSignPage(doc) { + def t = doc.body().text() + def matched = t ==~ /.*(签到成功|已签到|重复签到|今日已签|已经签到|获得.*).*/ + return [pageName: '签到', sign: true, message: matched ? '成功' : '结果未匹配'] +} + +// ==================== 下载 ==================== + +private Map parseDownload(context) { + // 下载页通常是重定向到种子文件 + if (context.status == 302 || context.status == 301) { + return [pageName: '下载', redirect: context.headers?.Location ?: ''] + } + if (context.headers?.'Content-Type'?.contains('octet-stream')) { + return [pageName: '下载', downloaded: true, size: context.content.length()] + } + return [pageName: '下载', error: true, status: context.status, message: "非种子文件 response"] +} + +// ==================== 工具方法 ==================== + +/** 从 #info_block 提取原始字段 */ +private Map extractUserInfo(doc) { + def fields = [:] + def el = doc.selectFirst('#info_block') + if (!el) return fields + def t = el.text() + + def um = t =~ /上传[量]?[::]\s*([0-9.,]+)\s*(TB|GB|MB|KB|B)?/ + def dm = t =~ /下载[量]?[::]\s*([0-9.,]+)\s*(TB|GB|MB|KB|B)?/ + def rm = t =~ /分享率[::]\s*([\d.,]+|无限)/ + def bm = t =~ /(?:魔力[值]?|时魔|蝌蚪)\s*[^0-9]*[::]\s*([\d.,]+)/ + def im = t =~ /邀请[^0-9]*[::]\s*(\d+)/ + + def seed = null, leech = null + def sm = t =~ /⬆\s*(\d+)/; if (sm) seed = sm[0][1] + def lm = t =~ /⬇\s*(\d+)/; if (lm) leech = lm[0][1] + if (seed == null || leech == null) { + def am = t =~ /当前活动[^0-9]*[::]\s*(\d+)\s+(\d+)/ + if (am) { seed = am[0][1]; leech = am[0][2] } + } + if (seed == null) { sm = t =~ /当前做种[^0-9]*(\d+)/; if (sm) seed = sm[0][1] } + if (leech == null) { lm = t =~ /当前下载[^0-9]*(\d+)/; if (lm) leech = lm[0][1] } + + if (um) fields.upload = um[0][1] + (um[0][2] ?: '') + if (dm) fields.download = dm[0][1] + (dm[0][2] ?: '') + if (rm) fields.ratio = rm[0][1] + if (bm) fields.bonus = bm[0][1] + if (seed != null) fields.seeding = seed + if (leech != null) fields.leeching = leech + if (im) fields.invites = im[0][1] + + def hrm = t =~ /H&R[::]\s*\[\d+\/\d+\/(\d+)\]/ + if (hrm) fields.hnr = hrm[0][1] + else { + def hrEl = doc.selectFirst('#info_block font[class=fcr], #info_block font[color=red], #info_block .red') + if (hrEl) { + def hrm2 = hrEl.text() =~ /(\d+)/ + if (hrm2) fields.hnr = hrm2[0][1] + } + } + return fields +} + +private double parseDouble(s) { + try { return (s as String).replace(',','') as double } catch (e) { return 0 } +} + +private long parseSize(s) { + if (!s) return 0 + try { + def m = s =~ /([0-9.,]+)\s*(GB|MB|TB|KB|B)?/i + if (!m) return 0 + def v = parseDouble(m[0][1]) + def u = (m[0][2] ?: 'B').toUpperCase() + return (v * [B:1, KB:1024, MB:1024*1024, GB:1024L*1024*1024, TB:1024L*1024*1024*1024][u]) as long + } catch (e) { return 0 } +} + +private long parseTraffic(s) { + return parseSize(s) +} + +private String encode(String s) { + try { return URLEncoder.encode(s, 'UTF-8') } catch (e) { return s } +}