Files
par/docs/nexusphp-default.groovy

261 lines
11 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) }
Map parseRss(context) { return checkStatus(context) ?: parseRssPage(context) }
Map parseDetail(context) { return checkStatus(context) ?: parseDetailPage(Jsoup.parse(context.content)) }
/** 获取资源元数据:搜索 → 详情 → 提取豆瓣链接(Douban 解析由 Java 层负责) */
Map parseMetadata(context) {
if (context.status != 200) return [matched: false, error: true]
// 1. 搜索
String title = context.params?.title ?: ''
def searchResp = context.fetch("search", [keyword: title])
if (searchResp.status != 200) return [matched: false, message: '搜索失败']
def searchItems = parseSearchPage(Jsoup.parse(searchResp.content)).items
if (!searchItems || searchItems.isEmpty()) return [matched: false, message: '未搜到']
// 2. 取第一个结果 → 访问详情(复用 parseDetailPage 统一解析)
def first = searchItems[0]
def detailResp = context.fetch("detail", [id: first.detailUrl])
if (detailResp.status != 200) return [matched: false, message: '详情页访问失败']
def detail = parseDetailPage(Jsoup.parse(detailResp.content))
return detail.doubanUrl
? [matched: true, doubanUrl: detail.doubanUrl, title: first.title, seedTitle: first.title]
: [matched: false, message: '未找到豆瓣链接', seedTitle: first.title]
}
/** 通用状态码检查,非 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')
// 提取促销信息:font.promotion、img.promotion、alt 属性等
def freeEl = doc.selectFirst('font.promotion, img.promotion, img[alt*=Free], img[alt*=free]')
def free = freeEl?.text() ?: freeEl?.attr('alt')
// 提取豆瓣链接
def dbLink = doc.selectFirst('a[href*=movie.douban.com], a[href*=douban.com/subject]')?.attr('href')
if (!dbLink) dbLink = doc.selectFirst('.rowhead:contains(简介) + td a[href*=douban], #kdescr a[href*=douban]')?.attr('href')
return [pageName: '种子详情', matched: title != null,
title: title, subtitle: subtitle, size: parseSize(size),
seeders: seeders as int, leechers: leechers as int,
downloadUrl: dlUrl, free: free, doubanUrl: dbLink
]
}
// ==================== 签到 ====================
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"]
}
// ==================== RSS ====================
/** RSS 页面解析:解析 NexusPHP 标准 RSS XML,返回 items 列表 */
private Map parseRssPage(context) {
def xml = context.content
if (!xml) return [pageName: 'RSS', matched: false, message: '无内容']
// 解析 RSS XML
def rss = new XmlSlurper().parseText(xml)
def items = []
rss.channel.item.each { item ->
// 下载链接优先从 enclosure url 获取
def dlUrl = ''
def size = 0
def enclosure = item.enclosure
if (enclosure && enclosure.@url) {
dlUrl = enclosure.@url.text()
try { size = enclosure.@length.toLong() } catch (e) {}
}
if (!dlUrl) dlUrl = item.link.text()
def title = item.title.text()
items << [
title: title,
detailUrl: item.link.text(),
downloadUrl: dlUrl,
torrentUrl: dlUrl,
size: size,
publishDate: item.pubDate.text(),
category: item.category.text(),
author: item.author.text(),
]
}
return [pageName: 'RSS', matched: !items.isEmpty(), items: items]
}
// ==================== 工具方法 ====================
/** 从 #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 }
}