diff --git a/docs/mteam.groovy b/docs/mteam.groovy index 8c826d3..e601729 100644 --- a/docs/mteam.groovy +++ b/docs/mteam.groovy @@ -1,63 +1,69 @@ // M-Team 适配脚本 (mteam) -// 站点类型: mtorrent -// API 基地址: https://api.m-team.io (优先) / https://api.m-team.cc (备用) - +// API 基地址(io 优先,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/member/profile')) + return parseProfile(root?.data) - // ── 搜索 ── - if (url.contains('/api/torrent/search')) { - def items = root?.data?.data ?: [] - if (items.isEmpty()) return [pageName: '搜索', message: '无结果'] - return buildSearchResults(items) - } + 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 buildProfile(d) { +private Map parseProfile(d) { + if (!d) return [pageName: '用户信息', message: 'data 为空'] 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(), + 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 buildSearchResults(List items) { +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 [ - 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 ?: '', + 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 != null ? it.hasChineseSubtitle.toString() : '', + standard: it.standard ?: '', ] } return [pageName: '搜索', extracted: rows, message: "共 ${rows.size()} 条"] @@ -68,7 +74,8 @@ private Map buildSearchResults(List items) { Map sync(PluginContext ctx) { if (!ctx.site.authorization) return [message: "未配置 Token", loginRequired: true] try { - def resp = apiGet(ctx, "/api/member/profile") + // 用户信息 + def resp = apiPost(ctx, "/api/member/profile") if (resp.statusCode() == 403 || resp.statusCode() == 401) return [message: "Token 无效", loginRequired: true] if (resp.statusCode() != 200) @@ -77,14 +84,26 @@ Map sync(PluginContext ctx) { 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, + 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}"] } @@ -100,7 +119,7 @@ List search(PluginContext ctx, Map params) { 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 + keyword: keyword, pageNumber: page, pageSize: 100, mode: "normal", visible: 1 ]) def resp = apiPost(ctx, "/api/torrent/search", body) if (resp.statusCode() != 200) return [] @@ -110,14 +129,25 @@ List search(PluginContext ctx, Map params) { 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", + id: item.id?.toString(), + title: item.name ?: "", + subTitle: item.smallDescr ?: "", + size: (item.size ?: 0) as long, + seeders: (st.seeders ?: 0) as int, + leechers: (st.leechers ?: 0) as int, + completed: (st.timesCompleted ?: 0) as int, + publishDate: item.createdDate ?: "", + category: item.category ?: "", + free: st.discount == "FREE" || st.discount == "_2X_FREE", + freeEndTime: st.discountEndTime ?: "", + publisher: item.author ?: "", + tags: item.labelsNew?.join(",") ?: "", + doubanUrl: item.douban ?: "", + doubanRating: item.doubanRating?.toString() ?: "", + imdbUrl: item.imdb ?: "", + cnSub: item.hasChineseSubtitle != null ? item.hasChineseSubtitle.toString() : "", + standard: item.standard ?: "", + detailUrl: "${ctx.siteUrl}detail/${item.id ?: ''}", ] } } catch (e) { @@ -127,28 +157,17 @@ List search(PluginContext ctx, Map params) { // ==================== 工具方法 ==================== -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 = 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") -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(), + 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()) }