// 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) {} } if (url.contains('/torrentrss.php') || url.contains('/rss')) return parseRss() return [pageName: '未知页面', message: "未配置: ${url}"] } private Map parseRss() { return [pageName: 'RSS', message: 'RSS 内容由 Java 层解析'] } 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 }