Files
par/docs/nexusphp-default.groovy

192 lines
7.9 KiB
Groovy
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// NexusPHP 默认脚本
// 适用: carpt / cbg / ptfans / ptskit / hdfans / cyanbug / qingwa 等
// medibo 负责 HTTP,脚本只做解析 + 状态码判断
// ==================== medibo 入口 ====================
// medibo 根据 pageKey 调用对应方法,如 parseSync(context)
// 约定方法名: parse + pageKey 首字母大写
// medibo 直接调用的入口方法
Map parseSync(context) {
if (context.status != 200) return [matched: false, error: true, status: context.status]
def doc = Jsoup.parse(context.content)
def profile = parseUserProfile(doc)
// 如果需要详情,通过 context.fetch 子请求
if (profile.matched && false /* 需要详情 */) {
def resp = context.fetch("detail", [id: "xxx"])
if (resp.status == 200) profile.detail = parseDetailPage(Jsoup.parse(resp.content))
}
return profile
}
Map parseSearch(context) { return checkStatus(context) ?: parseSearchPage(Jsoup.parse(context.content)) }
Map parseSignIn(context) { return checkStatus(context) ?: parseSignPage(Jsoup.parse(context.content)) }
Map parseDownload(context) { return checkStatus(context) ?: parseDownloadPage(context) }
/** 通用状态码检查,非 200 直接返回错误 */
private Map checkStatus(context) {
if (context.status == 200) return null
return switch(context.status) {
case 302 -> [matched: false, redirect: context.headers?.Location ?: '']
case 403 -> [matched: false, banned: true, message: 'IP被禁']
case 429 -> [matched: false, rateLimited: true]
default -> [matched: false, error: true, status: context.status, message: "HTTP ${context.status}"]
}
}
private Map parseUserProfile(doc) {
def fields = extractUserInfo(doc)
if (fields.isEmpty()) return [pageName: '用户信息', matched: false, message: '未提取到数据']
def result = [pageName: '用户信息', matched: true]
if (fields.upload) result.upload = parseTraffic(fields.upload)
if (fields.download) result.download = parseTraffic(fields.download)
if (fields.ratio) result.shareRatio = fields.ratio == '无限' ? 999.0 : parseDouble(fields.ratio)
if (fields.bonus) result.bonus = parseDouble(fields.bonus)
if (fields.seeding) result.seeding = fields.seeding as int
if (fields.leeching) result.leeching = fields.leeching as int
if (fields.invites) result.invites = fields.invites as int
if (fields.hnr) result.hnr = fields.hnr as int
return result
}
// ==================== 搜索页 ====================
private Map parseSearchPage(doc) {
def items = []
doc.select('table.torrents tr:has(td.rowfollow):not(:has(.colhead))').each { row ->
def titleEl = row.selectFirst('td.embedded a[href*=details]')
if (!titleEl) return
def freeEl = row.selectFirst('font.promotion')
def tds = row.select('td.rowfollow')
def size = '', seeders = '', leechers = '', completed = '', time = ''
for (int i = 0; i < tds.size(); i++) {
def cell = tds[i].text()
if (!cell) continue
if (cell.contains('GB')||cell.contains('MB')||cell.contains('TB')||cell.contains('KB')) size = cell
else if (cell ==~ /^\d+$/ && !seeders) seeders = cell
else if (cell ==~ /^\d+$/ && seeders && !leechers) leechers = cell
else if (cell ==~ /^\d+$/ && leechers) completed = cell
else if (cell ==~ /^\d+年|^\d+月|^\d+天|^\d+时/) time = cell
}
items << [
title: titleEl.text(),
detailUrl: titleEl.attr('href'),
free: freeEl?.text(),
size: parseSize(size),
seeders: seeders as int,
leechers: leechers as int,
grabs: completed as int,
publishDate: time,
]
}
return [pageName: '搜索', matched: !items.isEmpty(), items: items]
}
// ==================== 种子详情 ====================
private Map parseDetailPage(doc) {
def title = doc.selectFirst('#top h1, h1')?.text()
def subtitle = doc.selectFirst('td.embedded b')?.text()
def size = doc.selectFirst('td:has(b:contains(大小)) + td, td.rowhead:contains(大小) + td')?.text()
def seeders = doc.selectFirst('td:contains(做种者) + td')?.text()
def leechers = doc.selectFirst('td:contains(下载者) + td')?.text()
// 提取下载链接
def dlUrl = doc.selectFirst('a[href*=download]:not([href*=details])')?.attr('href')
return [pageName: '种子详情', matched: title != null,
title: title, subtitle: subtitle, size: parseSize(size),
seeders: seeders as int, leechers: leechers as int,
downloadUrl: dlUrl
]
}
// ==================== 签到 ====================
private Map parseSignPage(doc) {
def t = doc.body().text()
def matched = t ==~ /.*(签到成功|已签到|重复签到|今日已签|已经签到|获得.*).*/
return [pageName: '签到', matched: matched, sign: true, message: matched ? '成功' : '结果未匹配']
}
// ==================== 下载 ====================
private Map parseDownloadPage(context) {
// 下载页通常是重定向到种子文件
if (context.status == 302 || context.status == 301) {
return [pageName: '下载', matched: false, redirect: context.headers?.Location ?: '']
}
if (context.headers?.'Content-Type'?.contains('octet-stream')) {
return [pageName: '下载', matched: true, downloaded: true, size: context.content.length()]
}
return [pageName: '下载', matched: false, error: true, status: context.status, message: "非种子文件 response"]
}
// ==================== 工具方法 ====================
/** 从 #info_block 提取原始字段 */
private Map extractUserInfo(doc) {
def fields = [:]
def el = doc.selectFirst('#info_block')
if (!el) return fields
def t = el.text()
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 =~ /(?:魔力[值]?|时魔|蝌蚪)\s*[^0-9]*[::]\s*([\d.,]+)/
def im = t =~ /邀请[^0-9]*[::]\s*(\d+)/
def seed = null, leech = null
def sm = t =~ /⬆\s*(\d+)/; if (sm) seed = sm[0][1]
def lm = t =~ /⬇\s*(\d+)/; if (lm) leech = lm[0][1]
if (seed == null || leech == null) {
def am = t =~ /当前活动[^0-9]*[::]\s*(\d+)\s+(\d+)/
if (am) { seed = am[0][1]; leech = am[0][2] }
}
if (seed == null) { sm = t =~ /当前做种[^0-9]*(\d+)/; if (sm) seed = sm[0][1] }
if (leech == null) { lm = t =~ /当前下载[^0-9]*(\d+)/; if (lm) leech = lm[0][1] }
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 (seed != null) fields.seeding = seed
if (leech != null) fields.leeching = leech
if (im) fields.invites = im[0][1]
def hrm = t =~ /H&R[::]\s*\[\d+\/\d+\/(\d+)\]/
if (hrm) fields.hnr = hrm[0][1]
else {
def hrEl = doc.selectFirst('#info_block font[class=fcr], #info_block font[color=red], #info_block .red')
if (hrEl) {
def hrm2 = hrEl.text() =~ /(\d+)/
if (hrm2) fields.hnr = hrm2[0][1]
}
}
return fields
}
private double parseDouble(s) {
try { return (s as String).replace(',','') as double } catch (e) { return 0 }
}
private long parseSize(s) {
if (!s) return 0
try {
def m = s =~ /([0-9.,]+)\s*(GB|MB|TB|KB|B)?/i
if (!m) return 0
def v = parseDouble(m[0][1])
def u = (m[0][2] ?: 'B').toUpperCase()
return (v * [B:1, KB:1024, MB:1024*1024, GB:1024L*1024*1024, TB:1024L*1024*1024*1024][u]) as long
} catch (e) { return 0 }
}
private long parseTraffic(s) {
return parseSize(s)
}
private String encode(String s) {
try { return URLEncoder.encode(s, 'UTF-8') } catch (e) { return s }
}