// PTTIME 适配脚本 // 内置: content, url, jsoup, json, utils, http // ==================== PAR 校验 ==================== 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 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)?/ def rm = t =~ /分享率[::]\s*([\d.,]+)/ def bm = t =~ /魔力值\([^)]*\)[^0-9]*([\d.,]+)/ def sm = t =~ /⬆\s*(\d+)/ def lm = t =~ /⬇\s*(\d+)/ def im = 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] if (im) fields.invites = im[0][1] // H&R:只取红色数字(未达标数),用 CSS 选取 red 元素 def hrEl = doc.selectFirst('#info_block font[class=fcr], #info_block font[color=red], #info_block .red') if (hrEl) { def hrText = hrEl.text() def hrm = hrText ==~ /(\d+)/ ? (hrText =~ /(\d+)/) : null if (hrm) fields.hnr = hrm[0][1] } 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 = row.selectFirst('td.embedded a.torrentname_title') if (!titleEl) return def catEl = row.selectFirst('td.rowfollow span.category') def freeEl = row.selectFirst('font.promotion') def tds = row.select('td.rowfollow') rows << [ 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 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 = doc.body().text() def matched = t ==~ /.*(签到成功|已签到|重复签到|获得.*魔力值).*/ return [pageName: '签到', fields: [result: t.take(200)], message: matched ? '成功' : '未知'] } // ==================== mediabot 运行时(统一用 http.get/post + utils) ==================== Map sign(PluginContext ctx) { if (!ctx.auth) return [message: '未配置认证', loginRequired: true] try { def url = ctx.siteUrl.replaceAll('/+\$', '') + '/attendance.php' def body = http.get(url, ctx.auth) 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 url = ctx.siteUrl.replaceAll('/+\$', '') + '/index.php' def body = http.get(url, ctx.auth) 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)?/ def rm = t =~ /分享率[::]\s*([\d.,]+)/ def bm = t =~ /魔力值\([^)]*\)[^0-9]*([\d.,]+)/ def sm = t =~ /⬆\s*(\d+)/ def im = t =~ /邀请[::]\s*(\d+)/ if (um) result.upload = utils.parseTraffic(um[0][1] + (um[0][2] ?: '')) if (dm) result.download = utils.parseTraffic(dm[0][1] + (dm[0][2] ?: '')) 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 (im) result.invites = im[0][1] as int // H&R 红色数字(未达标数) def hrEl = doc.selectFirst('#info_block font[class=fcr], #info_block font[color=red], #info_block .red') if (hrEl) { def hrm = hrEl.text() =~ /(\d+)/ if (hrm) result.hnr = hrm[0][1] as int } if (!result.upload || !result.download) ctx.incomplete = true return result } catch (e) { ctx.incomplete = true; return [message: "同步失败: ${e.message}"] } } List search(PluginContext ctx, Map params) { if (!ctx.auth) return [] try { def keyword = params.keyword ?: '' def page = (params.page ?: 0) as int def url = "${ctx.siteUrl.replaceAll('/+\$', '')}/torrents.php?search=${URLEncoder.encode(keyword,'UTF-8')}&page=${page}" def body = http.get(url, ctx.auth) def doc = Jsoup.parse(body) def rows = [] doc.select('table.torrents tr:has(td.rowfollow):not(:has(.colhead))').each { row -> def titleEl = row.selectFirst('td.embedded a.torrentname_title') if (!titleEl) return def tds = row.select('td.rowfollow') rows << [ title: titleEl.text(), detailUrl: "${ctx.siteUrl}${titleEl.attr('href')}", size: tds.size() > 5 ? utils.parseHtmlSize(tds[5].text()) : 0, seeders: tds.size() > 6 ? utils.toInt(tds[6].text()) ?: 0 : 0, leechers: tds.size() > 7 ? utils.toInt(tds[7].text()) ?: 0 : 0, grabs: tds.size() > 8 ? utils.toInt(tds[8].text()) ?: 0 : 0, ] } return rows } catch (e) { return [] } }