192 lines
7.9 KiB
Groovy
192 lines
7.9 KiB
Groovy
// 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 }
|
||
}
|