189 lines
7.9 KiB
Groovy
189 lines
7.9 KiB
Groovy
// PTTIME 适配脚本
|
||
// PTTIME 基于 NexusPHP,但 HTML 结构有差异:
|
||
// - 搜索页 torrent 表格无 <tbody>,数据行直接 <tr> 下
|
||
// - 用户信息在 #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 td.bottom.clearfix')
|
||
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 = []
|
||
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 a[href*=cat] img, td.rowfollow span.category')
|
||
def freeEl = utils.selectFirst(row, 'font.promotion')
|
||
def tds = row.select('td.rowfollow')
|
||
// 语义匹配:按内容特征识别字段
|
||
def size = '', seeders = '', leechers = '', completed = '', time = '', publisher = ''
|
||
boolean hasSize = false, hasSeed = false, hasLeech = false
|
||
for (int i = 0; i < tds.size(); i++) {
|
||
def cell = utils.text(tds.get(i))
|
||
if (!cell) continue
|
||
if (!hasSize && (cell.contains('GB')||cell.contains('MB')||cell.contains('TB')||cell.contains('KB'))) {
|
||
size = cell; hasSize = true
|
||
} else if (hasSize && !hasSeed && cell ==~ /^\d+$/) {
|
||
seeders = cell; hasSeed = true
|
||
} else if (hasSeed && !hasLeech && cell ==~ /^\d+$/) {
|
||
leechers = cell; hasLeech = true
|
||
} else if (hasSeed && hasLeech && cell ==~ /^\d+$/) {
|
||
completed = cell
|
||
} else if (cell ==~ /^\d+年|^\d+月|^\d+天|^\d+时/) {
|
||
time = cell
|
||
}
|
||
}
|
||
rows << [
|
||
title: utils.text(titleEl),
|
||
detailUrl: utils.attr(titleEl, 'href'),
|
||
category: utils.attr(catEl, 'alt') ?: utils.attr(catEl, 'title') ?: '',
|
||
free: utils.text(freeEl),
|
||
size: size,
|
||
seeders: seeders,
|
||
leechers: leechers,
|
||
completed: completed,
|
||
publishDate: time,
|
||
publisher: publisher,
|
||
]
|
||
}
|
||
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, 'td.bottom.clearfix')
|
||
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')
|
||
long size = 0; int seeders = 0, leechers = 0, grabs = 0
|
||
boolean hasSize = false, hasSeed = false, hasLeech = false
|
||
for (int i = 0; i < tds.size(); i++) {
|
||
def cell = utils.text(tds.get(i))
|
||
if (!cell) continue
|
||
if (!hasSize && (cell.contains('GB')||cell.contains('MB')||cell.contains('TB')||cell.contains('KB'))) {
|
||
size = parseHtmlSize(cell); hasSize = true
|
||
} else if (hasSize && !hasSeed && cell ==~ /^\d+$/) {
|
||
seeders = cell as int; hasSeed = true
|
||
} else if (hasSeed && !hasLeech && cell ==~ /^\d+$/) {
|
||
leechers = cell as int; hasLeech = true
|
||
} else if (hasSeed && hasLeech && cell ==~ /^\d+$/) {
|
||
grabs = cell as int
|
||
}
|
||
}
|
||
rows << [
|
||
title: utils.text(titleEl),
|
||
detailUrl: "${ctx.siteUrl}${utils.attr(titleEl,'href')}",
|
||
size: size,
|
||
seeders: seeders,
|
||
leechers: leechers,
|
||
grabs: grabs,
|
||
]
|
||
}
|
||
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])
|
||
}
|