From 8845e68bbefbb54a2029ab829df272ac142b0842 Mon Sep 17 00:00:00 2001 From: mediabot-pt <295750538+mediabot-pt@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:26:00 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20PTTIME=E4=B8=93=E7=94=A8Groovy=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=EF=BC=8C=E6=97=A0tbody=E6=90=9C=E7=B4=A2+=E2=AC=86?= =?UTF-8?q?=E2=AC=87=E5=81=9A=E7=A7=8D=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pttime.groovy | 188 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/pttime.groovy diff --git a/docs/pttime.groovy b/docs/pttime.groovy new file mode 100644 index 0000000..7341211 --- /dev/null +++ b/docs/pttime.groovy @@ -0,0 +1,188 @@ +// PTTIME 适配脚本 +// PTTIME 基于 NexusPHP,但 HTML 结构有差异: +// - 搜索页 torrent 表格无 ,数据行直接 下 +// - 用户信息在 #info_block > td.bottom.clearfix 中 +// - 做种/下载数用 ⬆⬇ emoji 表示 + +Map parse(String content, String url) { + def doc = jsoup.parse(content) + + 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('/index.php') && !url.contains('?')) return parseIndex(doc) + + return [pageName: '未知页面', message: "未配置: ${url}"] +} + +// ── 首页 / 用户信息 ── + +private Map parseIndex(doc) { + def el = utils.selectFirst(doc, '#info_block td.bottom.clearfix') + if (!el) return [pageName: '首页', message: '未找到用户信息'] + def t = utils.text(el) + 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)?/ + def rm = t =~ /分享率[::]\s*([\d.,]+)/ + def bm = t =~ /魔力值\([^)]*\)[^0-9]*([\d.,]+)/ + def sm = t =~ /⬆\s*(\d+)/ + def lm = t =~ /⬇\s*(\d+)/ + 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 (sm) fields.seeding = sm[0][1] + if (lm) fields.leeching = lm[0][1] + return [pageName: '用户信息', fields: fields] +} + +// ── 搜索(无 tbody 版本)── + +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') + if (!titleEl) return + def catEl = utils.selectFirst(row, 'td.rowfollow a[href*=cat] img, td.rowfollow span.category') + def freeEl = utils.selectFirst(row, 'font.promotion') + def tds = row.select('td.rowfollow') + // 语义匹配:按内容特征识别字段 + def size = '', seeders = '', leechers = '', completed = '', time = '', publisher = '' + boolean hasSize = false, hasSeed = false, hasLeech = false + for (int i = 0; i < tds.size(); i++) { + def cell = utils.text(tds.get(i)) + if (!cell) continue + if (!hasSize && (cell.contains('GB')||cell.contains('MB')||cell.contains('TB')||cell.contains('KB'))) { + size = cell; hasSize = true + } else if (hasSize && !hasSeed && cell ==~ /^\d+$/) { + seeders = cell; hasSeed = true + } else if (hasSeed && !hasLeech && cell ==~ /^\d+$/) { + leechers = cell; hasLeech = true + } else if (hasSeed && hasLeech && cell ==~ /^\d+$/) { + completed = cell + } else if (cell ==~ /^\d+年|^\d+月|^\d+天|^\d+时/) { + time = cell + } + } + rows << [ + title: utils.text(titleEl), + detailUrl: utils.attr(titleEl, 'href'), + category: utils.attr(catEl, 'alt') ?: utils.attr(catEl, 'title') ?: '', + free: utils.text(freeEl), + size: size, + seeders: seeders, + leechers: leechers, + completed: completed, + publishDate: time, + publisher: publisher, + ] + } + 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] +} + +// ── 签到 ── + +private Map parseSign(doc) { + def t = utils.text(doc.body()) + def matched = t ==~ /.*(签到成功|已签到|重复签到|获得.*魔力值).*/ + return [pageName: '签到', fields: [result: t.take(200)], message: matched ? '成功' : '未知'] +} + +// ==================== mediabot 运行时 ==================== + +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 el = utils.selectFirst(doc, 'td.bottom.clearfix') + if (!el) return [message: "未找到用户信息"] + def t = utils.text(el) + 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)?/ + def rm = t =~ /分享率[::]\s*([\d.,]+)/ + def bm = t =~ /魔力值\([^)]*\)[^0-9]*([\d.,]+)/ + 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 (bm) result.bonus = bm[0][1].replace(',','') as double + if (sm) result.seeding = sm[0][1] as int + 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}"] } +} + +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 tr:has(td.rowfollow):not(:has(.colhead))').each { row -> + def titleEl = utils.selectFirst(row, 'td.embedded a.torrentname_title') + if (!titleEl) return + def tds = row.select('td.rowfollow') + long size = 0; int seeders = 0, leechers = 0, grabs = 0 + boolean hasSize = false, hasSeed = false, hasLeech = false + for (int i = 0; i < tds.size(); i++) { + def cell = utils.text(tds.get(i)) + if (!cell) continue + if (!hasSize && (cell.contains('GB')||cell.contains('MB')||cell.contains('TB')||cell.contains('KB'))) { + size = parseHtmlSize(cell); hasSize = true + } else if (hasSize && !hasSeed && cell ==~ /^\d+$/) { + seeders = cell as int; hasSeed = true + } else if (hasSeed && !hasLeech && cell ==~ /^\d+$/) { + leechers = cell as int; hasLeech = true + } else if (hasSeed && hasLeech && cell ==~ /^\d+$/) { + grabs = cell as int + } + } + rows << [ + title: utils.text(titleEl), + detailUrl: "${ctx.siteUrl}${utils.attr(titleEl,'href')}", + size: size, + seeders: seeders, + leechers: leechers, + grabs: grabs, + ] + } + return rows + } catch (e) { return [] } +} + +// ==================== 工具 ==================== + +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 +} +private long parseHtmlSize(String s) { + def m = s =~ /([\d.,]+)\s*(TB|GB|MB|KB|B)?/ + if (!m) return 0 + return parseTraffic(m[0][1].replace(',','') as double, m[0][2]) +}