// 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') 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 = [] // PTTIME 列表列序: 类别|标题|PTR(dn)|评论(dn)|时间|大小|做种|下载|完成|进度|发布者|行为 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 span.category') def freeEl = utils.selectFirst(row, '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), ] } 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, '#info_block') 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') 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, ] } 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]) }