From d60b3070dee6c89c461c536c8cf1538777dec4c1 Mon Sep 17 00:00:00 2001 From: mediabot-pt <295750538+mediabot-pt@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:22:19 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20NexusPHP=E7=AB=99=E7=82=B9=E9=80=9A?= =?UTF-8?q?=E7=94=A8Groovy=E8=84=9A=E6=9C=AC=EF=BC=8CHTML/CSS=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=99=A8=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/nexusphp.groovy | 191 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/nexusphp.groovy diff --git a/docs/nexusphp.groovy b/docs/nexusphp.groovy new file mode 100644 index 0000000..f7f70ab --- /dev/null +++ b/docs/nexusphp.groovy @@ -0,0 +1,191 @@ +// NexusPHP 站点通用脚本 +// 示例站点: PThome / HDSky / OurBits 等 + +// ==================== PAR 校验入口 ==================== + +Map parse(String content, String url) { + def doc = jsoup.parse(content) + + // ── 首页 / 用户信息 ── + if (url.contains('/index.php') && !url.contains('?')) { + return parseUserInfo(doc) + } + + // ── 搜索页 ── + if (url.contains('/torrents.php')) { + return parseSearch(doc) + } + + // ── 种子详情 ── + if (url.contains('/details.php')) { + return parseDetail(doc) + } + + // ── 签到 ── + if (url.contains('/attendance.php')) { + return parseSign(doc) + } + + // ── 控制面板 ── + if (url.contains('/usercp.php')) { + return parseUserPanel(doc) + } + + return [pageName: '未知页面', message: "未配置解析: ${url}"] +} + +private Map parseUserInfo(doc) { + def fields = [:] + def infoEl = utils.selectFirst(doc, '#info_block') + if (infoEl) { + def text = utils.text(infoEl) + // 提取关键信息 + def uploadM = text =~ /上传[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/ + def downloadM = text =~ /下载[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/ + def ratioM = text =~ /分享率[::]\s*([\d.]+)/ + def bonusM = text =~ /魔力[值::]*\s*([\d,]+)/ + if (uploadM) fields.upload = uploadM[0][1] + (uploadM[0][2] ?: '') + if (downloadM) fields.download = downloadM[0][1] + (downloadM[0][2] ?: '') + if (ratioM) fields.ratio = ratioM[0][1] + if (bonusM) fields.bonus = bonusM[0][1] + } + return [pageName: '用户信息', fields: fields, message: fields.isEmpty() ? '未提取到数据' : '解析成功'] +} + +private Map parseSearch(doc) { + def rows = [] + doc.select('table.torrents > tbody > tr:has(td.rowfollow):not(:has(th)):not(:has(.colhead))').each { row -> + def titleEl = utils.selectFirst(row, 'td.embedded a[href*=details]') + def catEl = utils.selectFirst(row, 'td.rowfollow a[href*=cat] img, td.rowfollow a[href*=torrents] img') + def freeEl = utils.selectFirst(row, 'td.rowfollow font.free') + def tds = row.select('td.rowfollow') + + rows << [ + title: utils.text(titleEl), + detailUrl: utils.attr(titleEl, 'href'), + category: utils.attr(catEl, 'alt'), + free: utils.text(freeEl), + size: tds.size() > 4 ? utils.text(tds.get(4)) : '', + seeders: tds.size() > 5 ? utils.text(tds.get(5)) : '', + leechers: tds.size() > 6 ? utils.text(tds.get(6)) : '', + completed: tds.size() > 7 ? utils.text(tds.get(7)) : '', + comments: tds.size() > 8 ? utils.text(tds.get(8)) : '', + publishDate: tds.size() > 3 ? utils.text(tds.get(3)) : '', + torrentUrl: utils.attr(utils.selectFirst(row, 'td.rowfollow a[href*=download]'), 'href'), + ] + } + return [pageName: '搜索', extracted: rows, message: "共 ${rows.size()} 条"] +} + +private Map parseDetail(doc) { + return [ + pageName: '种子详情', + fields: [ + title: utils.text(utils.selectFirst(doc, '#top h1, h1')), + subtitle: utils.text(utils.selectFirst(doc, 'td.embedded b, #subtitle')), + size: utils.text(utils.selectFirst(doc, 'td.rowhead:contains(大小) + td, b:contains(大小) + span')), + seeders: utils.text(utils.selectFirst(doc, 'td:contains(做种者) + td')), + leechers: utils.text(utils.selectFirst(doc, 'td:contains(下载者) + td')), + publishDate:utils.text(utils.selectFirst(doc, 'td.rowhead:contains(日期) + td')), + ] + ] +} + +private Map parseSign(doc) { + def text = utils.text(doc.body()) + def matched = text ==~ /.*(签到成功|已签到|重复签到|获得.*魔力值).*/ + return [ + pageName: '签到', + fields: [result: text.length() > 200 ? text.substring(0, 200) + '...' : text], + message: matched ? '签到成功' : '签到状态未知' + ] +} + +private Map parseUserPanel(doc) { + def fields = [:] + doc.select('td.text').each { td -> + def t = utils.text(td) + if (t.contains('分享率')) fields.shareRate = t.replaceAll(/[^0-9.]/, '') + if (t.contains('魔力')) fields.bonus = t.replaceAll(/[^0-9.,]/, '') + } + return [pageName: '控制面板', fields: fields, message: fields.isEmpty() ? '未提取' : '解析成功'] +} + +// ==================== mediabot 运行时入口 ==================== + +Map sign(PluginContext ctx) { + if (!ctx.site.auth) return [message: "未配置认证", loginRequired: true] + try { + def conn = ctx.httpGet('/attendance.php') + def body = conn.body() + def matched = body ==~ /.*(签到成功|已签到|重复签到|获得.*魔力值).*/ + return [success: matched, message: matched ? '已签到' : '签到失败'] + } catch (e) { + return [message: "签到异常: ${e.message}"] + } +} + +Map sync(PluginContext ctx) { + if (!ctx.site.auth) return [message: "未配置认证", loginRequired: true] + try { + def conn = ctx.httpGet('/index.php') + def doc = jsoup.parse(conn.body()) + def infoEl = utils.selectFirst(doc, '#info_block') + if (!infoEl) return [message: "未找到用户信息区域"] + def text = utils.text(infoEl) + def result = [:] + def uploadM = text =~ /上传[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/ + def downloadM = text =~ /下载[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/ + def ratioM = text =~ /分享率[::]\s*([\d.]+)/ + def bonusM = text =~ /魔力[值::]*\s*([\d,]+)/ + if (uploadM) result.upload = parseTraffic(uploadM[0][1] as double, uploadM[0][2]) + if (downloadM) result.download = parseTraffic(downloadM[0][1] as double, downloadM[0][2]) + if (ratioM) result.shareRate = ratioM[0][1] as double + if (bonusM) result.bonus = bonusM[0][1].replace(',','') as double + return result + } catch (e) { + return [message: "同步失败: ${e.message}"] + } +} + +List search(PluginContext ctx, Map params) { + if (!ctx.site.auth) return [] + try { + def keyword = params.keyword ?: '' + def page = (params.page ?: 0) as int + def conn = ctx.httpGet("/torrents.php?search=${URLEncoder.encode(keyword,'UTF-8')}&page=${page}") + def doc = jsoup.parse(conn.body()) + def rows = [] + doc.select('table.torrents > tbody > tr:has(td.rowfollow):not(:has(th)):not(:has(.colhead))').each { row -> + def titleEl = utils.selectFirst(row, 'td.embedded a[href*=details]') + def catEl = utils.selectFirst(row, 'td.rowfollow a[href*=cat] img, td.rowfollow a[href*=torrents] img') + def tds = row.select('td.rowfollow') + rows << [ + title: utils.text(titleEl), + detailUrl: "${ctx.siteUrl}${utils.attr(titleEl,'href')}", + category: utils.attr(catEl, 'alt'), + size: tds.size() > 4 ? parseHtmlSize(utils.text(tds.get(4))) : 0, + seeders: tds.size() > 5 ? utils.toInt(utils.text(tds.get(5))) ?: 0 : 0, + leechers: tds.size() > 6 ? utils.toInt(utils.text(tds.get(6))) ?: 0 : 0, + grabs: tds.size() > 7 ? utils.toInt(utils.text(tds.get(7))) ?: 0 : 0, + publishDate: tds.size() > 3 ? utils.text(tds.get(3)) : '', + ] + } + return rows + } catch (e) { + return [] + } +} + +// ==================== 工具方法 ==================== + +private long parseTraffic(double val, String unit) { + if (!unit) return val as long + def m = [B:1,KB:1024,MB:1048576,GB:1073741824,TB:1099511627776] + return (val * (m[unit.toUpperCase()] ?: 1)) as long +} + +private long parseHtmlSize(String s) { + def m = s =~ /([\d.]+)\s*(TB|GB|MB|KB|B)?/ + return m ? parseTraffic(m[0][1] as double, m[0][2]) : 0 +}