// NexusPHP 站点通用脚本 // 示例站点: PThome / HDSky / OurBits 等 // ==================== PAR 校验入口 ==================== Map parse(String content, String url) { def doc = jsoup.parse(content) // ── 首页 / 用户信息 ── if (url.contains('/index.php') && !url.contains('?')) { return parseUserInfo(doc) } // ── 搜索页 ── if (url.contains('/torrents.php')) { return parseSearch(doc) } // ── 种子详情 ── if (url.contains('/details.php')) { return parseDetail(doc) } // ── 签到 ── if (url.contains('/attendance.php')) { return parseSign(doc) } // ── 控制面板 ── if (url.contains('/usercp.php')) { return parseUserPanel(doc) } return [pageName: '未知页面', message: "未配置解析: ${url}"] } private Map parseUserInfo(doc) { def fields = [:] def infoEl = utils.selectFirst(doc, '#info_block') if (infoEl) { def text = utils.text(infoEl) // 提取关键信息 def uploadM = text =~ /上传[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/ def downloadM = text =~ /下载[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/ def ratioM = text =~ /分享率[::]\s*([\d.]+)/ def bonusM = text =~ /魔力[值::]*\s*([\d,]+)/ if (uploadM) fields.upload = uploadM[0][1] + (uploadM[0][2] ?: '') if (downloadM) fields.download = downloadM[0][1] + (downloadM[0][2] ?: '') if (ratioM) fields.ratio = ratioM[0][1] if (bonusM) fields.bonus = bonusM[0][1] } return [pageName: '用户信息', fields: fields, message: fields.isEmpty() ? '未提取到数据' : '解析成功'] } private Map parseSearch(doc) { def rows = [] doc.select('table.torrents > tbody > tr:has(td.rowfollow):not(:has(th)):not(:has(.colhead))').each { row -> def titleEl = utils.selectFirst(row, 'td.embedded a[href*=details]') def catEl = utils.selectFirst(row, 'td.rowfollow a[href*=cat] img, td.rowfollow a[href*=torrents] img') def freeEl = utils.selectFirst(row, 'td.rowfollow font.free') def tds = row.select('td.rowfollow') rows << [ title: utils.text(titleEl), detailUrl: utils.attr(titleEl, 'href'), category: utils.attr(catEl, 'alt'), free: utils.text(freeEl), size: tds.size() > 4 ? utils.text(tds.get(4)) : '', seeders: tds.size() > 5 ? utils.text(tds.get(5)) : '', leechers: tds.size() > 6 ? utils.text(tds.get(6)) : '', completed: tds.size() > 7 ? utils.text(tds.get(7)) : '', comments: tds.size() > 8 ? utils.text(tds.get(8)) : '', publishDate: tds.size() > 3 ? utils.text(tds.get(3)) : '', torrentUrl: utils.attr(utils.selectFirst(row, 'td.rowfollow a[href*=download]'), 'href'), ] } return [pageName: '搜索', extracted: rows, message: "共 ${rows.size()} 条"] } private Map parseDetail(doc) { return [ pageName: '种子详情', fields: [ title: utils.text(utils.selectFirst(doc, '#top h1, h1')), subtitle: utils.text(utils.selectFirst(doc, 'td.embedded b, #subtitle')), size: utils.text(utils.selectFirst(doc, 'td.rowhead:contains(大小) + td, b:contains(大小) + span')), seeders: utils.text(utils.selectFirst(doc, 'td:contains(做种者) + td')), leechers: utils.text(utils.selectFirst(doc, 'td:contains(下载者) + td')), publishDate:utils.text(utils.selectFirst(doc, 'td.rowhead:contains(日期) + td')), ] ] } private Map parseSign(doc) { def text = utils.text(doc.body()) def matched = text ==~ /.*(签到成功|已签到|重复签到|获得.*魔力值).*/ return [ pageName: '签到', fields: [result: text.length() > 200 ? text.substring(0, 200) + '...' : text], message: matched ? '签到成功' : '签到状态未知' ] } private Map parseUserPanel(doc) { def fields = [:] doc.select('td.text').each { td -> def t = utils.text(td) if (t.contains('分享率')) fields.shareRate = t.replaceAll(/[^0-9.]/, '') if (t.contains('魔力')) fields.bonus = t.replaceAll(/[^0-9.,]/, '') } return [pageName: '控制面板', fields: fields, message: fields.isEmpty() ? '未提取' : '解析成功'] } // ==================== mediabot 运行时入口 ==================== Map sign(PluginContext ctx) { if (!ctx.site.auth) return [message: "未配置认证", loginRequired: true] try { def conn = ctx.httpGet('/attendance.php') def body = conn.body() def matched = body ==~ /.*(签到成功|已签到|重复签到|获得.*魔力值).*/ return [success: matched, message: matched ? '已签到' : '签到失败'] } catch (e) { return [message: "签到异常: ${e.message}"] } } Map sync(PluginContext ctx) { if (!ctx.site.auth) return [message: "未配置认证", loginRequired: true] try { def conn = ctx.httpGet('/index.php') def doc = jsoup.parse(conn.body()) def infoEl = utils.selectFirst(doc, '#info_block') if (!infoEl) return [message: "未找到用户信息区域"] def text = utils.text(infoEl) def result = [:] def uploadM = text =~ /上传[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/ def downloadM = text =~ /下载[::]\s*([0-9.]+)\s*(TB|GB|MB|KB|B)?/ def ratioM = text =~ /分享率[::]\s*([\d.]+)/ def bonusM = text =~ /魔力[值::]*\s*([\d,]+)/ if (uploadM) result.upload = parseTraffic(uploadM[0][1] as double, uploadM[0][2]) if (downloadM) result.download = parseTraffic(downloadM[0][1] as double, downloadM[0][2]) if (ratioM) result.shareRate = ratioM[0][1] as double if (bonusM) result.bonus = bonusM[0][1].replace(',','') as double return result } catch (e) { return [message: "同步失败: ${e.message}"] } } List search(PluginContext ctx, Map params) { if (!ctx.site.auth) return [] try { def keyword = params.keyword ?: '' def page = (params.page ?: 0) as int def conn = ctx.httpGet("/torrents.php?search=${URLEncoder.encode(keyword,'UTF-8')}&page=${page}") def doc = jsoup.parse(conn.body()) def rows = [] doc.select('table.torrents > tbody > tr:has(td.rowfollow):not(:has(th)):not(:has(.colhead))').each { row -> def titleEl = utils.selectFirst(row, 'td.embedded a[href*=details]') def catEl = utils.selectFirst(row, 'td.rowfollow a[href*=cat] img, td.rowfollow a[href*=torrents] img') def tds = row.select('td.rowfollow') rows << [ title: utils.text(titleEl), detailUrl: "${ctx.siteUrl}${utils.attr(titleEl,'href')}", category: utils.attr(catEl, 'alt'), size: tds.size() > 4 ? parseHtmlSize(utils.text(tds.get(4))) : 0, seeders: tds.size() > 5 ? utils.toInt(utils.text(tds.get(5))) ?: 0 : 0, leechers: tds.size() > 6 ? utils.toInt(utils.text(tds.get(6))) ?: 0 : 0, grabs: tds.size() > 7 ? utils.toInt(utils.text(tds.get(7))) ?: 0 : 0, publishDate: tds.size() > 3 ? utils.text(tds.get(3)) : '', ] } return rows } catch (e) { return [] } } // ==================== 工具方法 ==================== private long parseTraffic(double val, String unit) { if (!unit) return val as long def m = [B:1,KB:1024,MB:1048576,GB:1073741824,TB:1099511627776] return (val * (m[unit.toUpperCase()] ?: 1)) as long } private long parseHtmlSize(String s) { def m = s =~ /([\d.]+)\s*(TB|GB|MB|KB|B)?/ return m ? parseTraffic(m[0][1] as double, m[0][2]) : 0 }