feat: 四类站点默认脚本+无自定义脚本时按siteType自动加载默认模板
This commit is contained in:
@@ -23,11 +23,40 @@ public class GroovyScriptEngine {
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private static final GroovyClassLoader classLoader = new GroovyClassLoader(
|
||||
Thread.currentThread().getContextClassLoader());
|
||||
/** 类型 → 默认脚本 */
|
||||
private static final Map<String, String> DEFAULT_SCRIPTS = new ConcurrentHashMap<>();
|
||||
/** 脚本前缀:自动导入必要包 + PluginContext 桩 */
|
||||
private static final String SCRIPT_PREFIX =
|
||||
"import groovy.json.*\n" +
|
||||
"import com.par.core.adapter.*\n";
|
||||
|
||||
/**
|
||||
* 根据站点 ID 加载默认脚本(按站点类型匹配)
|
||||
*/
|
||||
public String loadDefaultScript(String siteId) {
|
||||
// 需要从外部注入 siteMapper,这里用静态方法获取 Spring Bean
|
||||
// 实际由 AdapterController 调用时传入
|
||||
return null; // placeholder, 由 AdapterController 实现具体逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 classpath 加载默认脚本文件
|
||||
*/
|
||||
public static String loadDefaultByType(String siteType) {
|
||||
if (siteType == null) return null;
|
||||
String key = siteType.toLowerCase();
|
||||
return DEFAULT_SCRIPTS.computeIfAbsent(key, k -> {
|
||||
String path = "scripts/default-" + k + ".groovy";
|
||||
try (var in = GroovyScriptEngine.class.getClassLoader().getResourceAsStream(path)) {
|
||||
if (in == null) return null;
|
||||
return new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to load default script for type: {}", k, e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> execute(String groovyScript, String content, String url) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
|
||||
111
par-core/src/main/resources/scripts/default-gazelle.groovy
Normal file
111
par-core/src/main/resources/scripts/default-gazelle.groovy
Normal file
@@ -0,0 +1,111 @@
|
||||
// Gazelle 站点默认脚本
|
||||
// 适用: RED / OPS / BHD / PTP 等 Gazelle 架构站点
|
||||
// 覆盖: 搜索 / 种子详情 / 首页
|
||||
|
||||
Map parse(String content, String url) {
|
||||
def doc = jsoup.parse(content)
|
||||
|
||||
if (url.contains('/torrents.php')) return parseSearch(doc)
|
||||
if (url.contains('/torrents.php?id=')) return parseDetail(doc)
|
||||
if (url.contains('/index.php') && !url.contains('?')) return parseUserInfo(doc)
|
||||
|
||||
return [pageName: '未知页面', message: "未配置: ${url}"]
|
||||
}
|
||||
|
||||
private Map parseUserInfo(doc) {
|
||||
def fields = [:]
|
||||
def stats = utils.selectFirst(doc, '#userinfo_stats, .user_stats, ul.stats')
|
||||
if (stats) {
|
||||
def t = utils.text(stats)
|
||||
def um = t =~ /上传[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/i
|
||||
def dm = t =~ /下载[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/i
|
||||
def rm = 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]
|
||||
}
|
||||
return [pageName: '用户信息', fields: fields]
|
||||
}
|
||||
|
||||
private Map parseSearch(doc) {
|
||||
def rows = []
|
||||
doc.select('table#torrent_table > tbody > tr.group, .torrent_table tr.torrent').each { row ->
|
||||
def titleEl = utils.selectFirst(row, 'a[href*=torrents.php?id=]')
|
||||
rows << [
|
||||
title: utils.text(titleEl),
|
||||
detailUrl: utils.attr(titleEl, 'href'),
|
||||
size: utils.text(utils.selectFirst(row, 'td.number_column.nobr, td.size')),
|
||||
seeders: utils.text(utils.selectFirst(row, 'td.seeders')),
|
||||
leechers: utils.text(utils.selectFirst(row, 'td.leechers')),
|
||||
snatched: utils.text(utils.selectFirst(row, 'td.snatched')),
|
||||
uploadTime: utils.text(utils.selectFirst(row, 'td.time, td.uploaded')),
|
||||
]
|
||||
}
|
||||
return [pageName: '搜索', extracted: rows, message: "共 ${rows.size()} 条"]
|
||||
}
|
||||
|
||||
private Map parseDetail(doc) {
|
||||
return [pageName: '种子详情', fields: [
|
||||
title: utils.text(utils.selectFirst(doc, '#content h2, h2.header')),
|
||||
artist: utils.text(utils.selectFirst(doc, '#content h3 a, .artist_name')),
|
||||
format: utils.text(utils.selectFirst(doc, 'td:contains(Format) + td')),
|
||||
bitrate: utils.text(utils.selectFirst(doc, 'td:contains(Bitrate) + td')),
|
||||
size: utils.text(utils.selectFirst(doc, 'td:contains(Size) + td')),
|
||||
seeders: utils.text(utils.selectFirst(doc, 'td:contains(Seeders) + td')),
|
||||
leechers: utils.text(utils.selectFirst(doc, 'td:contains(Leechers) + td')),
|
||||
]]
|
||||
}
|
||||
|
||||
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 stats = utils.selectFirst(doc, '#userinfo_stats, .user_stats, ul.stats')
|
||||
if (!stats) return [message: "未找到用户信息"]
|
||||
def t = utils.text(stats)
|
||||
def result = [:]
|
||||
def um = t =~ /上传[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/i
|
||||
def dm = t =~ /下载[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/i
|
||||
def rm = t =~ /分享率[::]\s*([\d.]+)/
|
||||
if (um) result.upload = parseTraffic(um[0][1] as double, um[0][2])
|
||||
if (dm) result.download = parseTraffic(dm[0][1] as double, dm[0][2])
|
||||
if (rm) result.shareRate = rm[0][1] as double
|
||||
return result
|
||||
} catch (e) { return [message: "同步失败: ${e.message}"] }
|
||||
}
|
||||
|
||||
Map sign(PluginContext ctx) {
|
||||
return [message: "Gazelle 不支持签到"]
|
||||
}
|
||||
|
||||
List search(PluginContext ctx, Map params) {
|
||||
if (!ctx.site.auth) return []
|
||||
try {
|
||||
def keyword = params.keyword ?: ''
|
||||
def conn = ctx.httpGet("/torrents.php?searchstr=${URLEncoder.encode(keyword,'UTF-8')}")
|
||||
def doc = jsoup.parse(conn.body())
|
||||
def rows = []
|
||||
doc.select('table#torrent_table > tbody > tr.group, .torrent_table tr.torrent').each { row ->
|
||||
def titleEl = utils.selectFirst(row, 'a[href*=torrents.php?id=]')
|
||||
rows << [
|
||||
title: utils.text(titleEl),
|
||||
detailUrl: "${ctx.siteUrl}${utils.attr(titleEl,'href')}",
|
||||
size: parseHtmlSize(utils.text(utils.selectFirst(row, 'td.size'))),
|
||||
seeders: utils.toInt(utils.text(utils.selectFirst(row, 'td.seeders'))) ?: 0,
|
||||
leechers: utils.toInt(utils.text(utils.selectFirst(row, 'td.leechers'))) ?: 0,
|
||||
snatched: utils.toInt(utils.text(utils.selectFirst(row, 'td.snatched'))) ?: 0,
|
||||
]
|
||||
}
|
||||
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)?/
|
||||
return m ? parseTraffic(m[0][1] as double, m[0][2]) : 0
|
||||
}
|
||||
125
par-core/src/main/resources/scripts/default-mtorrent.groovy
Normal file
125
par-core/src/main/resources/scripts/default-mtorrent.groovy
Normal file
@@ -0,0 +1,125 @@
|
||||
// MTorrent 站点默认脚本
|
||||
// 适用: M-Team 及其 API 兼容站点
|
||||
// 覆盖: 用户信息 / 做种统计 / 搜索
|
||||
|
||||
final String API_BASE = "https://api.m-team.io"
|
||||
|
||||
Map parse(String content, String url) {
|
||||
Map root
|
||||
try { root = json.readValue(content, Map) }
|
||||
catch (e) { return [pageName: '解析失败', message: 'JSON异常: ' + (e.message ?: e.toString())] }
|
||||
|
||||
if (url.contains('/api/member/profile')) return parseProfile(root?.data)
|
||||
if (url.contains('/api/tracker/myPeerStatistics')) return parsePeerStats(root?.data)
|
||||
if (url.contains('/api/torrent/search')) return parseSearch(root?.data?.data ?: [])
|
||||
|
||||
return [pageName: '未知接口', message: "未配置: ${url}"]
|
||||
}
|
||||
|
||||
private Map parseProfile(d) {
|
||||
if (!d) return [pageName: '用户信息', message: 'data 为空']
|
||||
return [pageName: '用户信息', fields: [
|
||||
uid: d.id?.toString(),
|
||||
username: d.username?.toString(),
|
||||
email: d.email?.toString(),
|
||||
upload: utils.formatSize((d.memberCount?.uploaded ?: 0) as long),
|
||||
download: utils.formatSize((d.memberCount?.downloaded ?: 0) as long),
|
||||
bonus: d.memberCount?.bonus?.toString(),
|
||||
shareRate:d.memberCount?.shareRate?.toString(),
|
||||
invites: d.invites?.toString(),
|
||||
]]
|
||||
}
|
||||
|
||||
private Map parsePeerStats(d) {
|
||||
if (!d) return [pageName: '做种统计', message: 'data 为空']
|
||||
return [pageName: '做种统计', fields: [seeding: d.seederCount?.toString() ?: '0']]
|
||||
}
|
||||
|
||||
private Map parseSearch(List items) {
|
||||
if (items.isEmpty()) return [pageName: '搜索', message: '无结果']
|
||||
def rows = items.collect { it ->
|
||||
def st = it.status ?: [:]
|
||||
return [
|
||||
id: it.id?.toString(),
|
||||
title: it.name ?: '',
|
||||
subTitle: it.smallDescr ?: '',
|
||||
size: utils.formatSize((it.size ?: 0) as long),
|
||||
seeders: (st.seeders ?: 0) as int,
|
||||
leechers: (st.leechers ?: 0) as int,
|
||||
completed: (st.timesCompleted ?: 0) as int,
|
||||
category: it.category ?: '',
|
||||
free: st.discount ?: 'NORMAL',
|
||||
publisher: it.author ?: '',
|
||||
cnSub: it.hasChineseSubtitle?.toString() ?: '',
|
||||
standard: it.standard ?: '',
|
||||
]
|
||||
}
|
||||
return [pageName: '搜索', extracted: rows, message: "共 ${rows.size()} 条"]
|
||||
}
|
||||
|
||||
Map sync(PluginContext ctx) {
|
||||
if (!ctx.site.authorization) return [message: "未配置 Token", loginRequired: true]
|
||||
try {
|
||||
def resp = apiPost(ctx, "/api/member/profile")
|
||||
if (resp.statusCode() in [401,403]) return [message: "Token 无效", loginRequired: true]
|
||||
if (resp.statusCode() != 200) return [message: "HTTP ${resp.statusCode()}", siteUnavailable: true]
|
||||
def json = new groovy.json.JsonSlurper().parseText(resp.body())
|
||||
def d = json.data
|
||||
def mc = d.memberCount ?: [:]
|
||||
def result = [
|
||||
upload: mc.uploaded ? (mc.uploaded as long) * 1073741824 : null,
|
||||
download: mc.downloaded ? (mc.downloaded as long) * 1073741824 : null,
|
||||
bonus: mc.bonus ? mc.bonus as double : null,
|
||||
shareRate: mc.shareRate ? mc.shareRate as double : null,
|
||||
invites: d.invites ? d.invites as int : null,
|
||||
]
|
||||
def seedResp = apiPost(ctx, "/api/tracker/myPeerStatistics")
|
||||
if (seedResp.statusCode() == 200) {
|
||||
def sj = new groovy.json.JsonSlurper().parseText(seedResp.body())
|
||||
if (sj?.data?.seederCount != null) result.seeding = sj.data.seederCount as int
|
||||
}
|
||||
return result
|
||||
} catch (e) { return [message: "同步失败: ${e.message}"] }
|
||||
}
|
||||
|
||||
Map sign(PluginContext ctx) {
|
||||
return [message: "M-Team 不支持签到"]
|
||||
}
|
||||
|
||||
List search(PluginContext ctx, Map params) {
|
||||
if (!ctx.site.authorization) return []
|
||||
try {
|
||||
def body = groovy.json.JsonOutput.toJson([
|
||||
keyword: params.keyword ?: '', pageNumber: (params.page ?: 1) as int,
|
||||
pageSize: 100, mode: "normal", visible: 1
|
||||
])
|
||||
def resp = apiPost(ctx, "/api/torrent/search", body)
|
||||
def list = new groovy.json.JsonSlurper().parseText(resp.body())?.data?.data ?: []
|
||||
return list.collect { item ->
|
||||
def st = item.status ?: [:]
|
||||
return [
|
||||
title: item.name ?: "",
|
||||
detailUrl: "${ctx.siteUrl}detail/${item.id}",
|
||||
size: (item.size ?: 0) as long,
|
||||
seeders: (st.seeders ?: 0) as int,
|
||||
leechers: (st.leechers ?: 0) as int,
|
||||
grabs: (st.timesCompleted ?: 0) as int,
|
||||
category: item.category ?: "",
|
||||
free: st.discount == "FREE" || st.discount == "_2X_FREE",
|
||||
publishDate: item.createdDate ?: "",
|
||||
]
|
||||
}
|
||||
} catch (e) { return [] }
|
||||
}
|
||||
|
||||
private def apiPost(PluginContext ctx, String path, String body = null) {
|
||||
def builder = java.net.http.HttpRequest.newBuilder()
|
||||
.uri(new java.net.URI(API_BASE + path))
|
||||
.timeout(java.time.Duration.ofSeconds(20))
|
||||
.header("User-Agent", "Mozilla/5.0")
|
||||
.header("x-api-key", ctx.site.authorization)
|
||||
.header("Content-Type", "application/json")
|
||||
if (body) builder.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
|
||||
else builder.POST(java.net.http.HttpRequest.BodyPublishers.noBody())
|
||||
return ctx.httpClient.send(builder.build(), java.net.http.HttpResponse.BodyHandlers.ofString())
|
||||
}
|
||||
144
par-core/src/main/resources/scripts/default-nexusphp.groovy
Normal file
144
par-core/src/main/resources/scripts/default-nexusphp.groovy
Normal file
@@ -0,0 +1,144 @@
|
||||
// NexusPHP 站点默认脚本
|
||||
// 适用: PThome / HDSky / OurBits / HDFans / 聆音 等
|
||||
// 覆盖: 搜索 / 详情 / 用户信息 / 签到
|
||||
|
||||
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 parseUserInfo(doc)
|
||||
if (url.contains('/usercp.php')) return parseUserPanel(doc)
|
||||
|
||||
return [pageName: '未知页面', message: "未配置: ${url}"]
|
||||
}
|
||||
|
||||
private Map parseUserInfo(doc) {
|
||||
def fields = [:]
|
||||
def el = utils.selectFirst(doc, '#info_block')
|
||||
if (el) {
|
||||
def t = utils.text(el)
|
||||
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*([\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]
|
||||
}
|
||||
return [pageName: '用户信息', fields: fields]
|
||||
}
|
||||
|
||||
private Map parseSearch(doc) {
|
||||
def rows = []
|
||||
doc.select('table.torrents > tbody > tr:has(td.rowfollow):not(:has(th)):not(:has(.colhead))').each { row ->
|
||||
def titleEl = utils.selectFirst(row, 'td.embedded a[href*=details]')
|
||||
def catEl = utils.selectFirst(row, 'td.rowfollow a[href*=cat] img, td.rowfollow a[href*=torrents] img')
|
||||
def freeEl = utils.selectFirst(row, 'td.rowfollow font.free')
|
||||
def tds = row.select('td.rowfollow')
|
||||
rows << [
|
||||
title: utils.text(titleEl),
|
||||
detailUrl: utils.attr(titleEl, 'href'),
|
||||
category: utils.attr(catEl, 'alt'),
|
||||
free: utils.text(freeEl),
|
||||
size: tds.size() > 4 ? utils.text(tds.get(4)) : '',
|
||||
seeders: utils.text(tds.size() > 5 ? tds.get(5) : null),
|
||||
leechers: utils.text(tds.size() > 6 ? tds.get(6) : null),
|
||||
completed: utils.text(tds.size() > 7 ? tds.get(7) : null),
|
||||
publishDate:utils.text(tds.size() > 3 ? tds.get(3) : null),
|
||||
]
|
||||
}
|
||||
return [pageName: '搜索', extracted: rows, message: "共 ${rows.size()} 条"]
|
||||
}
|
||||
|
||||
private Map parseDetail(doc) {
|
||||
return [pageName: '种子详情', fields: [
|
||||
title: utils.text(utils.selectFirst(doc, '#top h1, h1')),
|
||||
subtitle: utils.text(utils.selectFirst(doc, 'td.embedded b')),
|
||||
size: utils.text(utils.selectFirst(doc, 'td:has(b:contains(大小)) + td, td.rowhead:contains(大小) + td')),
|
||||
seeders: utils.text(utils.selectFirst(doc, 'td:contains(做种者) + td')),
|
||||
leechers: utils.text(utils.selectFirst(doc, 'td:contains(下载者) + td')),
|
||||
]]
|
||||
}
|
||||
|
||||
private Map parseSign(doc) {
|
||||
def t = utils.text(doc.body())
|
||||
def matched = t ==~ /.*(签到成功|已签到|重复签到|获得.*魔力值).*/
|
||||
return [pageName: '签到', fields: [result: t.take(200)], message: matched ? '成功' : '未知']
|
||||
}
|
||||
|
||||
private Map parseUserPanel(doc) {
|
||||
def fields = [:]
|
||||
doc.select('td.text').each { td ->
|
||||
def t = utils.text(td)
|
||||
if (t.contains('分享率')) fields.shareRate = t.replaceAll(/[^0-9.]/, '')
|
||||
if (t.contains('魔力')) fields.bonus = t.replaceAll(/[^0-9.,]/, '')
|
||||
}
|
||||
return [pageName: '控制面板', fields: fields]
|
||||
}
|
||||
|
||||
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, '#info_block')
|
||||
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 =~ /魔力[值::]*\s*([\d,]+)/
|
||||
if (um) result.upload = parseTraffic(um[0][1] as double, um[0][2])
|
||||
if (dm) result.download = parseTraffic(dm[0][1] as double, dm[0][2])
|
||||
if (rm) result.shareRate = rm[0][1] as double
|
||||
if (bm) result.bonus = bm[0][1].replace(',','') as double
|
||||
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 > tbody > tr:has(td.rowfollow):not(:has(th)):not(:has(.colhead))').each { row ->
|
||||
def titleEl = utils.selectFirst(row, 'td.embedded a[href*=details]')
|
||||
def tds = row.select('td.rowfollow')
|
||||
rows << [
|
||||
title: utils.text(titleEl),
|
||||
detailUrl: "${ctx.siteUrl}${utils.attr(titleEl,'href')}",
|
||||
size: tds.size() > 4 ? parseHtmlSize(utils.text(tds.get(4))) : 0,
|
||||
seeders: tds.size() > 5 ? utils.toInt(utils.text(tds.get(5))) ?: 0 : 0,
|
||||
leechers: tds.size() > 6 ? utils.toInt(utils.text(tds.get(6))) ?: 0 : 0,
|
||||
grabs: tds.size() > 7 ? utils.toInt(utils.text(tds.get(7))) ?: 0 : 0,
|
||||
publishDate: tds.size() > 3 ? utils.text(tds.get(3)) : '',
|
||||
]
|
||||
}
|
||||
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)?/
|
||||
return m ? parseTraffic(m[0][1] as double, m[0][2]) : 0
|
||||
}
|
||||
107
par-core/src/main/resources/scripts/default-unit3d.groovy
Normal file
107
par-core/src/main/resources/scripts/default-unit3d.groovy
Normal file
@@ -0,0 +1,107 @@
|
||||
// UNIT3D 站点默认脚本
|
||||
// 适用: BLU / TL / ACM 等 UNIT3D 架构站点(HTML + 可选 API)
|
||||
// 覆盖: 搜索 / 种子详情 / 首页
|
||||
|
||||
Map parse(String content, String url) {
|
||||
def doc = jsoup.parse(content)
|
||||
|
||||
if (url.contains('/torrents') && !url.contains('/torrents/')) return parseSearch(doc)
|
||||
if (url.contains('/torrents/') && url.contains('/')) return parseDetail(doc)
|
||||
|
||||
// 尝试 JSON API
|
||||
if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
|
||||
try {
|
||||
def root = json.readValue(content, Map)
|
||||
if (root?.data) return parseApiData(root?.data, url)
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return [pageName: '未知页面', message: "未配置: ${url}"]
|
||||
}
|
||||
|
||||
private Map parseApiData(d, url) {
|
||||
if (url.contains('/user') || url.contains('/profile'))
|
||||
return [pageName: '用户', fields: [username: d?.username, uploaded: d?.uploaded?.toString(), downloaded: d?.downloaded?.toString()]]
|
||||
return [pageName: 'API数据', fields: d?.subMap(d.keySet().take(10))]
|
||||
}
|
||||
|
||||
private Map parseSearch(doc) {
|
||||
def rows = []
|
||||
doc.select('table.table tbody tr, .torrent-row, div.torrent').each { row ->
|
||||
def titleEl = utils.selectFirst(row, 'a[href*=torrents]')
|
||||
def tds = row.select('td')
|
||||
def size = ''
|
||||
for (int i = 0; i < tds.size(); i++) {
|
||||
def t = utils.text(tds.get(i))
|
||||
if (t =~ /[\d.]+\s*(GB|MB|TB)/) { size = t; break }
|
||||
}
|
||||
rows << [
|
||||
title: utils.text(titleEl),
|
||||
detailUrl: utils.attr(titleEl, 'href'),
|
||||
size: size,
|
||||
seeders: utils.text(utils.selectFirst(row, 'td:nth-child(5), td:has(.text-success)')),
|
||||
leechers: utils.text(utils.selectFirst(row, 'td:nth-child(6), td:has(.text-danger)')),
|
||||
uploadDate: utils.text(utils.selectFirst(row, 'td:nth-child(4), time')),
|
||||
]
|
||||
}
|
||||
return [pageName: '搜索', extracted: rows, message: "共 ${rows.size()} 条"]
|
||||
}
|
||||
|
||||
private Map parseDetail(doc) {
|
||||
return [pageName: '种子详情', fields: [
|
||||
title: utils.text(utils.selectFirst(doc, 'h1, .torrent-title, .meta-title')),
|
||||
size: utils.text(utils.selectFirst(doc, 'td:contains(Size) + td, .torrent-size')),
|
||||
seeders: utils.text(utils.selectFirst(doc, 'td:contains(Seeders) + td, .seeders-count')),
|
||||
leechers: utils.text(utils.selectFirst(doc, 'td:contains(Leechers) + td, .leechers-count')),
|
||||
description:utils.text(utils.selectFirst(doc, '.torrent-description, #description')).take(200),
|
||||
]]
|
||||
}
|
||||
|
||||
Map sync(PluginContext ctx) {
|
||||
if (!ctx.site.auth) return [message: "未配置认证", loginRequired: true]
|
||||
try {
|
||||
def conn = ctx.httpGet('/')
|
||||
def doc = jsoup.parse(conn.body())
|
||||
def upEl = utils.selectFirst(doc, '.user-uploaded, span:contains(Upload), dt:contains(Upload) + dd')
|
||||
def dlEl = utils.selectFirst(doc, '.user-downloaded, span:contains(Download), dt:contains(Download) + dd')
|
||||
def ratioEl = utils.selectFirst(doc, '.user-ratio, span:contains(Ratio), dt:contains(Ratio) + dd')
|
||||
def result = [:]
|
||||
if (upEl) result.upload = parseHtmlSize(utils.text(upEl))
|
||||
if (dlEl) result.download = parseHtmlSize(utils.text(dlEl))
|
||||
if (ratioEl) result.shareRate = (utils.text(ratioEl) =~ /([\d.]+)/)[0][1] as double
|
||||
return result
|
||||
} catch (e) { return [message: "同步失败: ${e.message}"] }
|
||||
}
|
||||
|
||||
Map sign(PluginContext ctx) {
|
||||
return [message: "UNIT3D 不支持签到"]
|
||||
}
|
||||
|
||||
List search(PluginContext ctx, Map params) {
|
||||
if (!ctx.site.auth) return []
|
||||
try {
|
||||
def keyword = params.keyword ?: ''
|
||||
def conn = ctx.httpGet("/torrents?search=${URLEncoder.encode(keyword,'UTF-8')}")
|
||||
def doc = jsoup.parse(conn.body())
|
||||
def rows = []
|
||||
doc.select('table.table tbody tr, .torrent-row').each { row ->
|
||||
def titleEl = utils.selectFirst(row, 'a[href*=torrents]')
|
||||
rows << [
|
||||
title: utils.text(titleEl),
|
||||
detailUrl: "${ctx.siteUrl}${utils.attr(titleEl,'href')}",
|
||||
size: parseHtmlSize(utils.text(utils.selectFirst(row, 'td:nth-child(4)'))) ?: 0,
|
||||
seeders: utils.toInt(utils.text(utils.selectFirst(row, 'td:nth-child(5)'))) ?: 0,
|
||||
leechers: utils.toInt(utils.text(utils.selectFirst(row, 'td:nth-child(6)'))) ?: 0,
|
||||
]
|
||||
}
|
||||
return rows
|
||||
} catch (e) { return [] }
|
||||
}
|
||||
|
||||
private long parseHtmlSize(String s) {
|
||||
if (!s) return 0
|
||||
def m = s =~ /([\d.]+)\s*(TB|GB|MB|KB|B)?/
|
||||
if (!m) return 0
|
||||
def m2 = [B:1,KB:1024,MB:1048576,GB:1073741824,TB:1099511627776]
|
||||
return ((m[0][1] as double) * (m2[m[0][2]?.toUpperCase()] ?: 1)) as long
|
||||
}
|
||||
Reference in New Issue
Block a user