/** * Loon Resource Parser * * Author: mc2u * Repository: https://github.com/mc2u/Loon * * 用于 Loon 节点订阅的资源解析脚本,可在订阅加载时自动整理节点名称。 * * 功能: * - 添加节点名前缀 * - 添加节点名后缀 * - 去除节点名中的 emoji * - 普通文本替换 * - 使用 Shadowrocket User-Agent 重新拉取订阅 * - 按订阅 URL 保存独立配置 * - 重置当前订阅配置 * * 支持参数: * - pre= 给节点名添加前缀,例如 pre=🍑 * - suf= 给节点名添加后缀,例如 suf=🍓 * - emoji 去除节点名中的 emoji * - rename= 普通文本替换,例如 rename=香港:HK,日本:JP * - ua 使用 Shadowrocket User-Agent 重新拉取订阅 * - reset 清空当前订阅已保存的配置 * * 参数说明: * - pre=🍑 * 给所有节点名称前面添加 🍑 * - suf=🍓 * 给所有节点名称后面添加 🍓 * - emoji * 去除节点名称中的 emoji * - rename=香港:HK,日本:JP,新加坡:SG * 将香港、日本、新加坡分别替换为 HK、JP、SG * - rename=0.1倍: * 删除节点名中的 0.1倍 * - ua * 使用 Shadowrocket User-Agent 重新拉取当前订阅 * - reset * 清空当前订阅已保存的参数配置 * * 组合示例: * - pre=🍑&emoji * 去除 emoji 后添加前缀 * - pre=🍑&rename=0.1倍: * 删除倍率后添加前缀 * - pre=🍑&suf=🍓&emoji * 同时添加前缀、后缀并去除 emoji * - ua&pre=🍑 * 使用 Shadowrocket UA 并添加前缀 * - ua&emoji * 使用 Shadowrocket UA 并去除 emoji * - ua&pre=🍑&emoji&rename=0.1倍: * 使用 Shadowrocket UA,去 emoji,删除倍率并添加前缀 * * 处理顺序: * - ua -> emoji -> rename -> pre -> suf * * 配置说明: * - 参数按订阅 URL 单独保存 * - 不同订阅互不影响 * - 不重新填写参数时,会优先使用已保存配置 * - 如果订阅编辑界面的参数输入框内出现 xxx = https...,直接点 x 删除掉后重新填写参数即可 * - 修改参数后需要手动更新订阅才会生效 */ var type = $resourceType; var result = ""; var pre = ""; var suf = ""; var emoji = false; var rename = ""; var ua = false; var HAS_SUPPORTED_PARAM = false; function getStorageKey() { var url = "default"; if (typeof $resourceUrl !== 'undefined' && $resourceUrl) url = String($resourceUrl); return "loon_parser_config_" + encodeURIComponent(url); } function isSupportedParamKey(key) { return key === 'pre' || key === 'suf' || key === 'emoji' || key === 'rename' || key === 'ua' || key === 'reset'; } function cleanEmoji(text) { return String(text || '').replace(/[\u{1F1E0}-\u{1F1FF}]|[\u{1F300}-\u{1F9FF}]|[\u{1F600}-\u{1F64F}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1FA00}-\u{1FAFF}]|[\u{1FB00}-\u{1FBFF}]|[\u{1F900}-\u{1F9FF}]/gu, '').replace(/\s+/g, ' ').trim(); } function normalizeText(s) { s = String(s || ''); if (s && s.charCodeAt(0) === 0xFEFF) s = s.slice(1); return s.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); } function parseRename(raw) { var pairs = []; if (!raw) return pairs; var items = String(raw).split(','); for (var i = 0; i < items.length; i++) { var item = items[i]; if (!item) continue; var idx = item.indexOf(':'); if (idx === -1) continue; var from = item.slice(0, idx).trim(); var to = item.slice(idx + 1).trim(); if (from === '') continue; pairs.push([from, to]); } return pairs; } function applyRename(name) { var n = String(name || ''); var pairs = parseRename(rename); for (var i = 0; i < pairs.length; i++) { n = n.split(pairs[i][0]).join(pairs[i][1]); } return n; } function modifyName(name) { var n = String(name || ''); if (emoji) n = cleanEmoji(n); if (rename) n = applyRename(n); if (pre) n = pre + n; if (suf) n = n + suf; return n; } function base64DecodeUnicode(str) { try { if (typeof atob !== 'undefined') { var binary = atob(str); var bytes = []; for (var i = 0; i < binary.length; i++) bytes.push('%' + ('00' + binary.charCodeAt(i).toString(16)).slice(-2)); return decodeURIComponent(bytes.join('')); } } catch (e) {} return null; } function base64EncodeUnicode(str) { try { if (typeof btoa !== 'undefined') { var enc = encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { return String.fromCharCode('0x' + p1); }); return btoa(enc); } } catch (e) {} return null; } function looksLikeBase64(text) { var s = String(text || '').replace(/\s+/g, ''); if (!s || s.length < 16) return false; if (s.length % 4 !== 0) return false; return /^[A-Za-z0-9+/=]+$/.test(s); } function renameLoonStyleText(text) { var raw = normalizeText(text).trim(); if (!raw) return ""; var lines = raw.split('\n'); var output = []; var count = 0; for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (!line || line.charAt(0) === '#') { output.push(line); continue; } var eqPos = line.indexOf('='); if (eqPos > 0) { var name = line.substring(0, eqPos).trim(); var value = line.substring(eqPos + 1).trim(); line = modifyName(name) + '=' + value; count++; } output.push(line); } console.log('[解析器] 已修改节点数: ' + count); return output.join('\n'); } function renameBase64UriList(text) { var compact = String(text || '').replace(/\s+/g, ''); var decoded = base64DecodeUnicode(compact); if (!decoded) return null; var lines = normalizeText(decoded).split('\n'); var changed = 0; for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (!line) continue; var hashPos = line.lastIndexOf('#'); if (hashPos > -1 && hashPos < line.length - 1) { var left = line.slice(0, hashPos + 1); var frag = line.slice(hashPos + 1); var oldName = ''; try { oldName = decodeURIComponent(frag); } catch (e) { oldName = frag; } var newName = modifyName(oldName); lines[i] = left + encodeURIComponent(newName); changed++; } } var encoded = base64EncodeUnicode(lines.join('\n')); if (!encoded) return null; console.log('[解析器] 已修改节点数: ' + changed); return encoded; } function processResourceContent(content) { var raw = normalizeText(content); var configParts = []; if (pre) configParts.push('pre=' + pre); if (suf) configParts.push('suf=' + suf); if (emoji) configParts.push('emoji'); if (rename) configParts.push('rename=' + rename); if (ua) configParts.push('ua'); console.log('[解析器] 当前配置: ' + (configParts.length ? configParts.join(', ') : '无')); console.log('[解析器] 订阅内容长度: ' + raw.length); var trimmed = raw.trim(); if (!trimmed) return ""; if (looksLikeBase64(trimmed)) { var base64Result = renameBase64UriList(trimmed); if (base64Result !== null) return base64Result; } return renameLoonStyleText(trimmed); } function finishWithContent(content) { if (type == 1) result = processResourceContent(content); else result = String(content || ""); console.log('[解析器] 处理完成'); $done(result); } var STORAGE_KEY = getStorageKey(); var savedConfig = $persistentStore.read(STORAGE_KEY); if (savedConfig) { try { var c = JSON.parse(savedConfig); pre = c.pre || ""; suf = c.suf || ""; emoji = c.emoji || false; rename = c.rename || ""; ua = c.ua === true; console.log('[解析器] 已读取本地配置'); } catch (e) { console.log('[解析器] 本地配置解析失败'); } } else { console.log('[解析器] 未找到本地配置,使用默认值'); } var argStr = ""; if (typeof $argument !== 'undefined' && $argument) { argStr = $argument.toString(); } else if (typeof $args !== 'undefined' && $args) { argStr = $args.toString(); } else if (typeof $parameter !== 'undefined' && $parameter) { argStr = $parameter.toString(); } var params = []; var canUpdateConfig = false; var doReset = false; if (argStr) { params = argStr.split('&'); for (var i = 0; i < params.length; i++) { var kv0 = params[i].split('='); var key0 = (kv0[0] || "").trim().toLowerCase(); var value0 = kv0.length > 1 ? decodeURIComponent(kv0.slice(1).join('=').trim()) : ''; if (isSupportedParamKey(key0)) HAS_SUPPORTED_PARAM = true; if (key0 === 'reset' && value0 === '') doReset = true; } } if (argStr && !argStr.match(/^https?:\/\//) && HAS_SUPPORTED_PARAM && (argStr.indexOf('=') > 0 || doReset)) { canUpdateConfig = true; } if (canUpdateConfig) { if (doReset) { pre = ""; suf = ""; emoji = false; rename = ""; ua = false; } for (var j = 0; j < params.length; j++) { var kv = params[j].split('='); var key = (kv[0] || "").trim().toLowerCase(); var value = kv.length > 1 ? decodeURIComponent(kv.slice(1).join('=').trim()) : ''; if (key === 'pre') pre = value; else if (key === 'suf') suf = value; else if (key === 'emoji') emoji = value === ''; else if (key === 'rename') rename = value; else if (key === 'ua') ua = value === ''; else if (key === 'reset') {} } var config = { pre: pre, suf: suf, emoji: emoji, rename: rename, ua: ua }; $persistentStore.write(JSON.stringify(config), STORAGE_KEY); if (doReset) console.log('[解析器] 已重置配置'); else console.log('[解析器] 已更新配置'); } else { console.log('[解析器] 未更新配置'); } function refetchWithShadowrocketUA() { if (typeof $httpClient === 'undefined' || !$httpClient) { console.log('[解析器] 当前环境不支持自定义 UA 拉取,已回退默认内容'); finishWithContent($resource || ""); return; } if (typeof $resourceUrl === 'undefined' || !$resourceUrl) { console.log('[解析器] 缺少资源地址,已回退默认内容'); finishWithContent($resource || ""); return; } var req = { url: String($resourceUrl), headers: { 'User-Agent': 'Shadowrocket/3082' } }; console.log('[解析器] 已启用 Shadowrocket UA 拉取'); $httpClient.get(req, function(error, response, data) { if (error || !data) { console.log('[解析器] Shadowrocket UA 拉取失败,已回退默认内容'); finishWithContent($resource || ""); return; } console.log('[解析器] Shadowrocket UA 拉取成功'); finishWithContent(data); }); } if (ua && type == 1) refetchWithShadowrocketUA(); else finishWithContent($resource || "");