docs: M-Team 完整 Groovy 脚本参考,含 parse/sync/search/verify 入口
This commit is contained in:
212
docs/mteam.groovy
Normal file
212
docs/mteam.groovy
Normal file
@@ -0,0 +1,212 @@
|
||||
// M-Team 适配脚本 (mteam)
|
||||
// 站点类型: mtorrent
|
||||
// API 基地址: https://api.m-team.io (优先) / https://api.m-team.cc (备用)
|
||||
|
||||
final String API_BASE = "https://api.m-team.io"
|
||||
|
||||
// ==================== PAR 校验入口 ====================
|
||||
// content: 访问结果页面内容(JSON 字符串)
|
||||
// url: 访问结果的 URL
|
||||
// 返回: [pageName:"xx", fields:[k:v,...]] 或 [pageName:"xx", extracted:[[...]], message:"..."]
|
||||
|
||||
Map parse(String content, String url) {
|
||||
def slurp = new groovy.json.JsonSlurper()
|
||||
def root = slurp.parseText(content)
|
||||
|
||||
// ── 用户信息 ──
|
||||
if (url.contains('/api/member/profile')) {
|
||||
def d = root?.data
|
||||
if (!d) return [pageName: '用户信息', message: 'data 为空']
|
||||
return buildProfile(d)
|
||||
}
|
||||
|
||||
// ── 搜索 ──
|
||||
if (url.contains('/api/torrent/search')) {
|
||||
def items = root?.data?.data ?: []
|
||||
if (items.isEmpty()) return [pageName: '搜索', message: '无结果']
|
||||
return buildSearchResults(items)
|
||||
}
|
||||
|
||||
// ── 未匹配 ──
|
||||
return [pageName: '未知接口', message: "未配置解析: ${url}"]
|
||||
}
|
||||
|
||||
private Map buildProfile(d) {
|
||||
return [
|
||||
pageName: '用户信息',
|
||||
fields: [
|
||||
id: d.id?.toString(),
|
||||
username: d.username?.toString(),
|
||||
uploaded: formatSize(d.uploaded as long),
|
||||
downloaded: formatSize(d.downloaded as long),
|
||||
bonus: d.bonus?.toString(),
|
||||
seeding: d.seeding?.toString(),
|
||||
seedingSize: formatSize(d.seedingSize as long),
|
||||
invite: d.memberCount?.invite?.toString(),
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
private Map buildSearchResults(List items) {
|
||||
def rows = items.collect { it ->
|
||||
def st = it.status ?: [:]
|
||||
return [
|
||||
title: it.name ?: '',
|
||||
size: formatSize((it.size ?: 0) as long),
|
||||
seeders: st.seeders ?: 0,
|
||||
leechers: st.leechers ?: 0,
|
||||
grabs: st.timesCompleted ?: 0,
|
||||
discount: st.discount ?: 'NORMAL',
|
||||
category: it.category ?: '',
|
||||
]
|
||||
}
|
||||
return [pageName: '搜索', extracted: rows, message: "共 ${rows.size()} 条"]
|
||||
}
|
||||
|
||||
private String formatSize(long bytes) {
|
||||
if (bytes == 0) return '0'
|
||||
def units = ['B','KB','MB','GB','TB']
|
||||
double v = bytes
|
||||
int i = 0
|
||||
while (v >= 1024 && i < units.size() - 1) { v /= 1024; i++ }
|
||||
return String.format('%.2f %s', v, units[i])
|
||||
}
|
||||
|
||||
// ==================== mediabot 运行时入口 ====================
|
||||
|
||||
Map sync(PluginContext ctx) {
|
||||
if (!ctx.site.authorization) return [message: "未配置 Token", loginRequired: true]
|
||||
try {
|
||||
def resp = apiGet(ctx, "/api/member/profile")
|
||||
if (resp.statusCode() == 403 || resp.statusCode() == 401)
|
||||
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())
|
||||
if (json.message != "SUCCESS" || !json.data)
|
||||
return [message: "响应格式异常"]
|
||||
def d = json.data
|
||||
return [
|
||||
upload: d.uploaded ? (d.uploaded as long) * 1073741824 : null,
|
||||
download: d.downloaded ? (d.downloaded as long) * 1073741824 : null,
|
||||
bonus: d.bonus ? d.bonus as double : null,
|
||||
seeding: d.seeding ? d.seeding as int : null,
|
||||
seedingSize: d.seedingSize? (d.seedingSize as long) * 1073741824: null,
|
||||
]
|
||||
} catch (e) {
|
||||
return [message: "同步失败: ${e.message}"]
|
||||
}
|
||||
}
|
||||
|
||||
Map verify(PluginContext ctx) {
|
||||
if (!ctx.site.authorization) return [available: false, reason: "未配置 Token"]
|
||||
try {
|
||||
def resp = apiGet(ctx, "/api/member/profile")
|
||||
if (resp.statusCode() == 200) {
|
||||
def json = new groovy.json.JsonSlurper().parseText(resp.body())
|
||||
return json.message == "SUCCESS"
|
||||
? [available: true, reason: "Token 验证通过"]
|
||||
: [available: false, reason: "Token 无效"]
|
||||
}
|
||||
return [available: false, reason: "HTTP ${resp.statusCode()}"]
|
||||
} catch (e) {
|
||||
return [available: false, reason: "连接失败: ${e.message}"]
|
||||
}
|
||||
}
|
||||
|
||||
List search(PluginContext ctx, Map params) {
|
||||
if (!ctx.site.authorization) return []
|
||||
try {
|
||||
def keyword = params.keyword ?: ""
|
||||
def page = (params.page ?: 1) as int
|
||||
def body = groovy.json.JsonOutput.toJson([
|
||||
keyword: keyword, pageNumber: page, pageSize: 50, visible: 1
|
||||
])
|
||||
def resp = apiPost(ctx, "/api/torrent/search", body)
|
||||
if (resp.statusCode() != 200) return []
|
||||
|
||||
def json = new groovy.json.JsonSlurper().parseText(resp.body())
|
||||
def list = json?.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",
|
||||
]
|
||||
}
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
Map detectAnnounce(PluginContext ctx) {
|
||||
if (!ctx.site.authorization) return [error: "未配置 Token"]
|
||||
try {
|
||||
// 搜索任意种子获取 ID
|
||||
def body = groovy.json.JsonOutput.toJson([keyword: "", pageNumber: 1, pageSize: 1, visible: 1])
|
||||
def resp = apiPost(ctx, "/api/torrent/search", body)
|
||||
def json = new groovy.json.JsonSlurper().parseText(resp.body())
|
||||
def item = json?.data?.data?.get(0)
|
||||
if (!item?.id) return [error: "未找到种子"]
|
||||
|
||||
// 生成下载 Token
|
||||
def dlBody = groovy.json.JsonOutput.toJson([id: item.id.toString()])
|
||||
def dlResp = apiPost(ctx, "/api/torrent/genDlToken", dlBody)
|
||||
def dlJson = new groovy.json.JsonSlurper().parseText(dlResp.body())
|
||||
def token = dlJson?.data
|
||||
if (!token) return [error: "获取下载 Token 失败"]
|
||||
|
||||
// 下载种子文件
|
||||
def torResp = ctx.httpClient.send(
|
||||
java.net.http.HttpRequest.newBuilder()
|
||||
.uri(new java.net.URI("${ctx.siteUrl}download/${item.id}/${token}"))
|
||||
.timeout(java.time.Duration.ofSeconds(30))
|
||||
.header("User-Agent", "Mozilla/5.0")
|
||||
.GET().build(),
|
||||
java.net.http.HttpResponse.BodyHandlers.ofByteArray())
|
||||
|
||||
if (torResp.statusCode() != 200 || !torResp.body()) return [error: "种子下载失败"]
|
||||
def info = com.mediabot.util.Bencode.decode(torResp.body())
|
||||
def announce = info?.announce
|
||||
return announce && announce.toString().startsWith("http")
|
||||
? [announceUrl: announce.toString()]
|
||||
: [error: "解析失败"]
|
||||
} catch (e) {
|
||||
return [error: e.message]
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
private def apiGet(PluginContext ctx, String path) {
|
||||
return ctx.httpClient.send(
|
||||
java.net.http.HttpRequest.newBuilder()
|
||||
.uri(new java.net.URI(API_BASE + path))
|
||||
.timeout(java.time.Duration.ofSeconds(path.contains('profile') ? 10 : 20))
|
||||
.header("User-Agent", "Mozilla/5.0")
|
||||
.header("x-api-key", ctx.site.authorization)
|
||||
.header("Origin", ctx.siteUrl)
|
||||
.POST(java.net.http.HttpRequest.BodyPublishers.noBody())
|
||||
.build(),
|
||||
java.net.http.HttpResponse.BodyHandlers.ofString())
|
||||
}
|
||||
|
||||
private def apiPost(PluginContext ctx, String path, String body) {
|
||||
return ctx.httpClient.send(
|
||||
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")
|
||||
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
|
||||
.build(),
|
||||
java.net.http.HttpResponse.BodyHandlers.ofString())
|
||||
}
|
||||
Reference in New Issue
Block a user