From 2b474b5cc0aefb57a12bbf987ec8125b08ef256e Mon Sep 17 00:00:00 2001 From: mediabot-pt <295750538+mediabot-pt@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:42:50 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=84=9A=E6=9C=AC=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=AF=B9=E9=BD=90mediabot(=E7=9B=B4=E6=8E=A5Jsoup/ctx?= =?UTF-8?q?.auth/capturedUrl)+PluginContext=E6=A1=A9=E7=B1=BB=E8=A1=A5?= =?UTF-8?q?=E5=85=85=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pttime.groovy | 127 ++++++++++-------- .../java/com/mediabot/site/PluginContext.java | 11 +- 2 files changed, 76 insertions(+), 62 deletions(-) diff --git a/docs/pttime.groovy b/docs/pttime.groovy index cb1724d..731d46e 100644 --- a/docs/pttime.groovy +++ b/docs/pttime.groovy @@ -1,12 +1,7 @@ // PTTIME 适配脚本 -// PTTIME 基于 NexusPHP,HTML 结构差异: -// - 搜索页 torrent 表格无 -// - 用户信息在 #info_block 中 -// - 做种/下载数用 ⬆⬇ emoji -// - 认证字段 ctx.site.auth Map parse(String content, String url) { - def doc = jsoup.parse(content) + def doc = Jsoup.parse(content) if (url.contains('/torrents.php')) return parseSearch(doc) if (url.contains('/details.php')) return parseDetail(doc) @@ -17,9 +12,9 @@ Map parse(String content, String url) { } private Map parseIndex(doc) { - def el = utils.selectFirst(doc, '#info_block') - if (!el) return [pageName: '首页', message: '未找到用户信息'] - def t = utils.text(el) + def infoEl = doc.selectFirst('#info_block') + if (!infoEl) return [pageName: '首页', message: '未找到用户信息'] + def t = infoEl.text() def fields = [:] def um = t =~ /上传[::]\s*([0-9.,]+)\s*(TB|GB|MB|KB|B)?/ def dm = t =~ /下载[::]\s*([0-9.,]+)\s*(TB|GB|MB|KB|B)?/ @@ -33,55 +28,66 @@ private Map parseIndex(doc) { if (bm) fields.bonus = bm[0][1] if (sm) fields.seeding = sm[0][1] if (lm) fields.leeching = lm[0][1] - return [pageName: '用户信息', fields: fields] + return [pageName: '用户信息', fields: fields, message: fields.isEmpty() ? '未提取' : '解析成功'] } private Map parseSearch(doc) { def rows = [] doc.select('table.torrents tr:has(td.rowfollow):not(:has(.colhead))').each { row -> - def titleEl = utils.selectFirst(row, 'td.embedded a.torrentname_title') + def titleEl = row.selectFirst('td.embedded a.torrentname_title') if (!titleEl) return - def catEl = utils.selectFirst(row, 'td.rowfollow span.category') - def freeEl = utils.selectFirst(row, 'font.promotion') + def catEl = row.selectFirst('td.rowfollow span.category') + def freeEl = row.selectFirst('font.promotion') def tds = row.select('td.rowfollow') rows << [ - title: utils.text(titleEl), - detailUrl: utils.attr(titleEl, 'href'), - category: utils.attr(catEl, 'title') ?: '', - free: utils.text(freeEl), - size: utils.text(tds.size() > 5 ? tds.get(5) : null), - seeders: utils.text(tds.size() > 6 ? tds.get(6) : null), - leechers: utils.text(tds.size() > 7 ? tds.get(7) : null), - completed: utils.text(tds.size() > 8 ? tds.get(8) : null), - publishDate: utils.text(tds.size() > 4 ? tds.get(4) : null), + title: titleEl.text(), + detailUrl: titleEl.attr('href'), + category: catEl?.attr('title') ?: '', + free: freeEl?.text(), + size: tds.size() > 5 ? tds[5].text() : '', + seeders: tds.size() > 6 ? tds[6].text() : '', + leechers: tds.size() > 7 ? tds[7].text() : '', + completed: tds.size() > 8 ? tds[8].text() : '', + publishDate: tds.size() > 4 ? tds[4].text() : '', ] } return [pageName: '搜索', extracted: rows, message: "共 ${rows.size()} 条"] } private Map parseDetail(doc) { - def fields = [:] - def titleEl = utils.selectFirst(doc, 'a.torrentname_title b, #top h1') - fields.title = utils.text(titleEl) - def descEl = utils.selectFirst(doc, 'td.embedded font[title], #kdescr') - if (descEl) fields.description = utils.text(descEl).take(200) - return [pageName: '种子详情', fields: fields] + def titleEl = doc.selectFirst('a.torrentname_title b, #top h1') + def descEl = doc.selectFirst('td.embedded font[title], #kdescr') + return [pageName: '种子详情', fields: [ + title: titleEl?.text(), + description: descEl?.text()?.take(200) + ]] } private Map parseSign(doc) { - def t = utils.text(doc.body()) + def t = doc.body().text() def matched = t ==~ /.*(签到成功|已签到|重复签到|获得.*魔力值).*/ return [pageName: '签到', fields: [result: t.take(200)], message: matched ? '成功' : '未知'] } -Map sync(PluginContext ctx) { - if (!ctx.site?.auth) return [message: "未配置认证", loginRequired: true] +// ==================== mediabot 运行时 ==================== + +Map sign(PluginContext ctx) { + if (!ctx.auth) return [message: '未配置认证', loginRequired: true] try { - def conn = ctx.httpGet('/index.php') - def doc = jsoup.parse(conn.body()) - def el = utils.selectFirst(doc, '#info_block') - if (!el) return [message: "未找到用户信息"] - def t = utils.text(el) + def body = httpGet(ctx, '/attendance.php') + def matched = body ==~ /.*(签到成功|已签到|重复签到|获得.*魔力值).*/ + return [success: matched, message: matched ? '已签到' : '失败'] + } catch (e) { return [message: "签到异常: ${e.message}"] } +} + +Map sync(PluginContext ctx) { + if (!ctx.auth) return [message: '未配置认证', loginRequired: true] + try { + def body = httpGet(ctx, '/index.php') + def doc = Jsoup.parse(body) + def infoEl = doc.selectFirst('#info_block') + if (!infoEl) { ctx.incomplete = true; return [message: '未找到用户信息'] } + def t = infoEl.text() def result = [:] def um = t =~ /上传[::]\s*([0-9.,]+)\s*(TB|GB|MB|KB|B)?/ def dm = t =~ /下载[::]\s*([0-9.,]+)\s*(TB|GB|MB|KB|B)?/ @@ -90,47 +96,54 @@ Map sync(PluginContext ctx) { def sm = t =~ /⬆\s*(\d+)/ if (um) result.upload = parseTraffic(um[0][1].replace(',','') as double, um[0][2]) if (dm) result.download = parseTraffic(dm[0][1].replace(',','') as double, dm[0][2]) - if (rm) result.shareRate = rm[0][1].replace(',','') as double + if (rm) result.shareRatio = rm[0][1].replace(',','') as double if (bm) result.bonus = bm[0][1].replace(',','') as double if (sm) result.seeding = sm[0][1] as int + if (!result.upload || !result.download) ctx.incomplete = true return result - } catch (e) { return [message: "同步失败: ${e.message}"] } -} - -Map sign(PluginContext ctx) { - if (!ctx.site?.auth) return [message: "未配置认证", loginRequired: true] - try { - def conn = ctx.httpGet('/attendance.php') - def matched = conn.body() ==~ /.*(签到成功|已签到|重复签到|获得.*魔力值).*/ - return [success: matched, message: matched ? '已签到' : '失败'] - } catch (e) { return [message: "签到异常: ${e.message}"] } + } catch (e) { ctx.incomplete = true; return [message: "同步失败: ${e.message}"] } } List search(PluginContext ctx, Map params) { - if (!ctx.site?.auth) return [] + if (!ctx.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 body = httpGet(ctx, "/torrents.php?search=${URLEncoder.encode(keyword,'UTF-8')}&page=${page}") + def doc = Jsoup.parse(body) def rows = [] doc.select('table.torrents tr:has(td.rowfollow):not(:has(.colhead))').each { row -> - def titleEl = utils.selectFirst(row, 'td.embedded a.torrentname_title') + def titleEl = row.selectFirst('td.embedded a.torrentname_title') if (!titleEl) return def tds = row.select('td.rowfollow') rows << [ - title: utils.text(titleEl), - detailUrl: "${ctx.siteUrl}${utils.attr(titleEl,'href')}", - size: tds.size() > 5 ? parseHtmlSize(utils.text(tds.get(5))) : 0, - seeders: tds.size() > 6 ? utils.toInt(utils.text(tds.get(6))) ?: 0 : 0, - leechers: tds.size() > 7 ? utils.toInt(utils.text(tds.get(7))) ?: 0 : 0, - grabs: tds.size() > 8 ? utils.toInt(utils.text(tds.get(8))) ?: 0 : 0, + title: titleEl.text(), + detailUrl: "${ctx.siteUrl}${titleEl.attr('href')}", + size: tds.size() > 5 ? parseHtmlSize(tds[5].text()) : 0, + seeders: tds.size() > 6 ? (tds[6].text() as int ?: 0) : 0, + leechers: tds.size() > 7 ? (tds[7].text() as int ?: 0) : 0, + grabs: tds.size() > 8 ? (tds[8].text() as int ?: 0) : 0, ] } return rows } catch (e) { return [] } } +// ==================== HTTP 工具 ==================== + +private String httpGet(PluginContext ctx, String path) { + def url = ctx.siteUrl.replaceAll('/+\$', '') + '/' + path.replaceAll('^/+', '') + def req = HttpRequest.newBuilder().uri(java.net.URI.create(url)) + .timeout(Duration.ofSeconds(15)).header('User-Agent', 'MediaBot/1.0') + if (ctx.auth) req.header('Cookie', ctx.auth) + req.GET() + def resp = ctx.httpClient.send(req.build(), HttpResponse.BodyHandlers.ofString()) + ctx.capturedUrl = url; ctx.capturedBody = resp.body() + return resp.body() +} + +// ==================== 工具 ==================== + private long parseTraffic(double val, String unit) { def m = [B:1,KB:1024,MB:1048576,GB:1073741824,TB:1099511627776] return (val * (m[unit?.toUpperCase()] ?: 1)) as long diff --git a/par-core/src/main/java/com/mediabot/site/PluginContext.java b/par-core/src/main/java/com/mediabot/site/PluginContext.java index b08ebb0..89eecf5 100644 --- a/par-core/src/main/java/com/mediabot/site/PluginContext.java +++ b/par-core/src/main/java/com/mediabot/site/PluginContext.java @@ -2,18 +2,19 @@ package com.mediabot.site; /** * PluginContext 桩类(mediabot 命名空间),仅供 PAR Groovy 编译通过。 - * 实际运行时由 mediabot 注入真实实现。 + * 字段名和结构参考 mediabo 实际 PluginContext。 */ @SuppressWarnings("unused") public class PluginContext { + public String auth; + public String siteUrl; public Site site; public Object httpClient; - - public Object httpGet(String path) { return null; } - public Object httpPost(String path, String body) { return null; } + public boolean incomplete; + public String capturedUrl; + public String capturedBody; public static class Site { - public String authorization; public String auth; public String siteUrl; }