// 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: utils.formatSize(d.uploaded as long), downloaded: utils.formatSize(d.downloaded as long), bonus: d.bonus?.toString(), seeding: d.seeding?.toString(), seedingSize: utils.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: utils.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()} 条"] } // ==================== 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()) }