diff --git a/docs/mteam.groovy b/docs/mteam.groovy new file mode 100644 index 0000000..9311da4 --- /dev/null +++ b/docs/mteam.groovy @@ -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()) +}