0733bdcbe2
优化 substore 脚本逻辑,移除 peiqian-rename.js 和 yuetong-rename.js 中对 _subName 的硬编码判断,使其更适合在单条订阅中复用。 同时在 rename.js 中新增流媒体文案清理规则,并同步更新 README.md 文档,说明各脚本在单订阅与整合订阅中的最佳实践建议。
373 lines
12 KiB
JavaScript
373 lines
12 KiB
JavaScript
/**
|
||
* Sub-Store 节点重命名脚本
|
||
* - 统一节点前缀为地区短码,保留旗帜与订阅后缀
|
||
* - 清理常见冗余线路描述,例如 DIP 地区说明、IEPL 专线
|
||
* - 基于归一化后的完整名称做精准去重
|
||
*/
|
||
function operator(proxies) {
|
||
const globalCount = {};
|
||
const currentCount = {};
|
||
|
||
const regionAliases = [
|
||
['United Arab Emirates', 'AE'],
|
||
['United States Los Angeles', 'US LA'],
|
||
['United States San Jose', 'US SJ'],
|
||
['United States Seattle', 'US SEA'],
|
||
['Australia Sydney', 'AU SYD'],
|
||
['Russia St. Petersburg', 'RU LED'],
|
||
['Russia Moscow', 'RU MOW'],
|
||
['USA Los Angeles', 'US LA'],
|
||
['USA San Jose', 'US SJ'],
|
||
['USA Seattle', 'US SEA'],
|
||
['United Kingdom', 'UK'],
|
||
['United States', 'US'],
|
||
['South Africa', 'ZA'],
|
||
['Saudi Arabia', 'SA'],
|
||
['New Zealand', 'NZ'],
|
||
['Hong Kong', 'HK'],
|
||
['Netherlands', 'NL'],
|
||
['Switzerland', 'CH'],
|
||
['Singapore', 'SG'],
|
||
['Indonesia', 'ID'],
|
||
['Philippines', 'PH'],
|
||
['Azerbaijan', 'AZ'],
|
||
['Kazakhstan', 'KZ'],
|
||
['Kyrgyzstan', 'KG'],
|
||
['Argentina', 'AR'],
|
||
['Australia', 'AU'],
|
||
['Macedonia', 'MK'],
|
||
['Lithuania', 'LT'],
|
||
['Colombia', 'CO'],
|
||
['Malaysia', 'MY'],
|
||
['Thailand', 'TH'],
|
||
['Vietnam', 'VN'],
|
||
['Romania', 'RO'],
|
||
['Nigeria', 'NG'],
|
||
['Bulgaria', 'BG'],
|
||
['Germany', 'DE'],
|
||
['Hungary', 'HU'],
|
||
['Austria', 'AT'],
|
||
['Ireland', 'IE'],
|
||
['Turkey', 'TR'],
|
||
['Taiwan', 'TW'],
|
||
['Canada', 'CA'],
|
||
['Brazil', 'BR'],
|
||
['Russia', 'RU'],
|
||
['France', 'FR'],
|
||
['Sweden', 'SE'],
|
||
['Belgium', 'BE'],
|
||
['Denmark', 'DK'],
|
||
['Iceland', 'IS'],
|
||
['Norway', 'NO'],
|
||
['Morocco', 'MA'],
|
||
['Mexico', 'MX'],
|
||
['Israel', 'IL'],
|
||
['Spain', 'ES'],
|
||
['Italy', 'IT'],
|
||
['Czech', 'CZ'],
|
||
['Egypt', 'EG'],
|
||
['Chile', 'CL'],
|
||
['India', 'IN'],
|
||
['Korea', 'KR'],
|
||
['Japan', 'JP'],
|
||
['Macau', 'MO'],
|
||
['Dubai', 'AE DXB'],
|
||
['Sydney', 'AU SYD'],
|
||
['Melbourne', 'AU MEL'],
|
||
['Toronto', 'CA TOR'],
|
||
['Casablanca', 'MA CMN'],
|
||
['USA', 'US'],
|
||
|
||
['美国圣何塞', 'US SJ'],
|
||
['美国洛杉矶', 'US LA'],
|
||
['美国阿什本', 'US IAD'],
|
||
['日本东京', 'JP TYO'],
|
||
['韩国首尔', 'KR SEL'],
|
||
['英国伦敦', 'UK LON'],
|
||
['瑞士苏黎世', 'CH ZRH'],
|
||
['印度尼西亚', 'ID'],
|
||
['所罗门群岛', 'SB'],
|
||
['阿塞拜疆', 'AZ'],
|
||
['哈萨克斯坦', 'KZ'],
|
||
['吉尔吉斯斯坦', 'KG'],
|
||
['卡萨布兰卡', 'MA CMN'],
|
||
['澳大利亚', 'AU'],
|
||
['新西兰', 'NZ'],
|
||
['马来西亚', 'MY'],
|
||
['尼日利亚', 'NG'],
|
||
['摩尔多瓦', 'MD'],
|
||
['巴基斯坦', 'PK'],
|
||
['阿根廷', 'AR'],
|
||
['阿联酋', 'AE'],
|
||
['柬埔寨', 'KH'],
|
||
['东帝汶', 'TL'],
|
||
['梵蒂冈', 'VA'],
|
||
['百慕大', 'BM'],
|
||
['格陵兰', 'GL'],
|
||
['南极洲', 'AQ'],
|
||
['罗马尼亚', 'RO'],
|
||
['哥伦比亚', 'CO'],
|
||
['菲律宾', 'PH'],
|
||
['以色列', 'IL'],
|
||
['比利时', 'BE'],
|
||
['立陶宛', 'LT'],
|
||
['马其顿', 'MK'],
|
||
['俄罗斯', 'RU'],
|
||
['乌克兰', 'UA'],
|
||
['匈牙利', 'HU'],
|
||
['西班牙', 'ES'],
|
||
['土耳其', 'TR'],
|
||
['加拿大', 'CA'],
|
||
['意大利', 'IT'],
|
||
['奥地利', 'AT'],
|
||
['阿富汗', 'AF'],
|
||
['卡塔尔', 'QA'],
|
||
['索马里', 'SO'],
|
||
['新加坡', 'SG'],
|
||
['印度', 'IN'],
|
||
['印尼', 'ID'],
|
||
['日本', 'JP'],
|
||
['香港', 'HK'],
|
||
['台湾', 'TW'],
|
||
['美国', 'US'],
|
||
['韩国', 'KR'],
|
||
['英国', 'UK'],
|
||
['德国', 'DE'],
|
||
['荷兰', 'NL'],
|
||
['法国', 'FR'],
|
||
['巴西', 'BR'],
|
||
['智利', 'CL'],
|
||
['澳门', 'MO'],
|
||
['泰国', 'TH'],
|
||
['越南', 'VN'],
|
||
['瑞典', 'SE'],
|
||
['丹麦', 'DK'],
|
||
['多哥', 'TG'],
|
||
['埃及', 'EG'],
|
||
['冰岛', 'IS'],
|
||
['挪威', 'NO'],
|
||
['南非', 'ZA'],
|
||
['古巴', 'CU'],
|
||
['斐济', 'FJ'],
|
||
['关岛', 'GU'],
|
||
['缅甸', 'MM'],
|
||
['蒙古', 'MN'],
|
||
['老挝', 'LA'],
|
||
['捷克', 'CZ'],
|
||
['沙特', 'SA'],
|
||
['墨西哥', 'MX'],
|
||
['迪拜', 'AE DXB'],
|
||
['悉尼', 'AU SYD'],
|
||
['墨尔本', 'AU MEL'],
|
||
['多伦多', 'CA TOR']
|
||
];
|
||
|
||
const sortedRegionAliases = regionAliases
|
||
.slice()
|
||
.sort((firstAlias, secondAlias) => secondAlias[0].length - firstAlias[0].length);
|
||
|
||
const flagCodeOverrides = {
|
||
'🇬🇧': 'UK',
|
||
'🇼🇸': 'TW'
|
||
};
|
||
|
||
const knownRegionCodes = new Set(
|
||
regionAliases.map(([, replacement]) => replacement.split(/\s+/)[0])
|
||
);
|
||
Object.values(flagCodeOverrides).forEach(code => knownRegionCodes.add(code));
|
||
|
||
const escapeRegex = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
|
||
const flagToRegionCode = flag => {
|
||
if (!flag) return '';
|
||
if (flagCodeOverrides[flag]) return flagCodeOverrides[flag];
|
||
|
||
const codePoints = [...flag].map(char => char.codePointAt(0));
|
||
if (
|
||
codePoints.length !== 2 ||
|
||
codePoints.some(codePoint => codePoint < 0x1F1E6 || codePoint > 0x1F1FF)
|
||
) {
|
||
return '';
|
||
}
|
||
|
||
return codePoints
|
||
.map(codePoint => String.fromCharCode(codePoint - 0x1F1E6 + 65))
|
||
.join('');
|
||
};
|
||
|
||
const stripExistingSuffix = (name, subName) => {
|
||
if (!subName) return name.trim();
|
||
const suffixRegex = new RegExp(`\\s+-\\s*${escapeRegex(subName)}(?:\\s+\\d{2})?\\s*$`, 'i');
|
||
return name.replace(suffixRegex, '').trim();
|
||
};
|
||
|
||
const joinAlias = (replacement, rest) => {
|
||
const cleanRest = rest.trim();
|
||
if (!cleanRest) return replacement;
|
||
if (/^[|/\-]/.test(cleanRest)) {
|
||
return `${replacement}${cleanRest}`;
|
||
}
|
||
return `${replacement} ${cleanRest}`;
|
||
};
|
||
|
||
const normalizeLeadingRegion = name => {
|
||
const cleanName = name.trim();
|
||
|
||
for (const [alias, replacement] of sortedRegionAliases) {
|
||
if (!cleanName.toLowerCase().startsWith(alias.toLowerCase())) continue;
|
||
|
||
const rest = cleanName.slice(alias.length);
|
||
if (/^[A-Za-z]/.test(alias) && rest && !/^[\s\d[|\-.x]/i.test(rest)) {
|
||
continue;
|
||
}
|
||
|
||
return joinAlias(replacement, rest);
|
||
}
|
||
|
||
return cleanName;
|
||
};
|
||
|
||
const leadingCodeOf = name => {
|
||
const match = name.match(/^([A-Z]{2})(?=\b|\s|-|$|\d)/);
|
||
return match ? match[1] : '';
|
||
};
|
||
|
||
const hasKnownRegionCode = name => {
|
||
const leadingCode = leadingCodeOf(name);
|
||
if (knownRegionCodes.has(leadingCode)) return true;
|
||
|
||
const proMatch = name.match(/^Pro-\s*(.+)$/i);
|
||
return proMatch ? knownRegionCodes.has(leadingCodeOf(proMatch[1])) : false;
|
||
};
|
||
|
||
const hasSpecificRegionCode = (name, regionCode) => {
|
||
if (!regionCode) return false;
|
||
if (leadingCodeOf(name) === regionCode) return true;
|
||
|
||
const proMatch = name.match(/^Pro-\s*(.+)$/i);
|
||
return proMatch ? leadingCodeOf(proMatch[1]) === regionCode : false;
|
||
};
|
||
|
||
const moveProRegionToFront = (name, regionCode) => {
|
||
if (!regionCode) return name;
|
||
|
||
const proRegionRegex = new RegExp(`^Pro-\\s*${escapeRegex(regionCode)}(?=\\s|[\\u4e00-\\u9fff]|\\d)(.*)$`, 'i');
|
||
const proRegionMatch = name.match(proRegionRegex);
|
||
if (!proRegionMatch) return name;
|
||
|
||
const rest = proRegionMatch[1].trim();
|
||
return rest ? `${regionCode} Pro-${rest}` : regionCode;
|
||
};
|
||
|
||
const simplifyCoreName = name => {
|
||
return name
|
||
.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\uFE0F]/gu, '')
|
||
.replace(/\u200d/g, '')
|
||
.replace(/\s*\|\s*流媒体/gi, '')
|
||
.replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1x')
|
||
.replace(/(\d+(?:\.\d+)?)倍/gi, '$1x')
|
||
.replace(/\b(\d+(?:\.\d+)?)X\b/g, '$1x')
|
||
.replace(/([A-Z]{2})\s*(专属|家宽|AI|高速|专线)(\d+)/g, '$1 $2 $3')
|
||
.replace(/\b(AI)(\d+)/g, '$1 $2')
|
||
.replace(/\b(Pro-)\s+/gi, '$1')
|
||
.replace(/^((?:Pro-\s*)?)([A-Z]{2})(?=[\u4e00-\u9fff]|\d)/u, '$1$2 ')
|
||
.replace(/\s+\[/g, ' [')
|
||
.replace(/\s{2,}/g, ' ')
|
||
.trim();
|
||
};
|
||
|
||
const normalizeBilingualName = name => {
|
||
const bilingualMatch = name.match(/^(.+?)\s*|\s*(.+)$/);
|
||
if (!bilingualMatch) return name;
|
||
|
||
const leftName = normalizeLeadingRegion(bilingualMatch[1]);
|
||
const rightName = normalizeLeadingRegion(bilingualMatch[2]);
|
||
const leftCode = leadingCodeOf(leftName);
|
||
const rightCode = leadingCodeOf(rightName);
|
||
|
||
if (leftCode && rightCode && leftCode === rightCode) {
|
||
return rightName === rightCode ? leftName : rightName;
|
||
}
|
||
if (rightCode) return rightName;
|
||
if (leftCode) return leftName;
|
||
|
||
return name.replace(/\s*|\s*/g, ' ');
|
||
};
|
||
|
||
const normalizeCoreName = name => {
|
||
const flagPattern = '([\\u{1F1E6}-\\u{1F1FF}]{2}|🏴☠️)';
|
||
const leadingFlagMatch = name.match(new RegExp(`^${flagPattern}\\s*`, 'u'));
|
||
const inlineFlagMatch = leadingFlagMatch
|
||
? null
|
||
: name.match(new RegExp(`^(.{1,24}?)${flagPattern}\\s*(.*)$`, 'u'));
|
||
const flag = leadingFlagMatch ? leadingFlagMatch[1] : inlineFlagMatch ? inlineFlagMatch[2] : '';
|
||
let core = name.trim();
|
||
|
||
if (leadingFlagMatch) {
|
||
core = name.slice(leadingFlagMatch[0].length).trim();
|
||
} else if (inlineFlagMatch) {
|
||
const prefix = inlineFlagMatch[1].trim();
|
||
const rest = inlineFlagMatch[3].trim();
|
||
core = rest ? `${prefix}${prefix.endsWith('-') ? '' : ' '}${rest}` : prefix;
|
||
}
|
||
|
||
core = core
|
||
.replace(/\[DIP[^\]]*\]/gi, '[DIP]');
|
||
|
||
core = normalizeBilingualName(core);
|
||
core = normalizeLeadingRegion(core);
|
||
|
||
if (/^Pro-\s*/i.test(core)) {
|
||
core = core.replace(/^(Pro-\s*)(.+)$/i, (fullMatch, proPrefix, restName) => {
|
||
return `${proPrefix}${normalizeLeadingRegion(restName)}`;
|
||
});
|
||
}
|
||
|
||
core = core
|
||
.replace(/^([A-Z]{2})\s*(实验性|标准|高级)\s+IEPL\s+专线\s*(\d+)(.*)$/i, '$1 $2 $3$4')
|
||
.replace(/搬瓦工荷兰/g, '搬瓦工')
|
||
.replace(/\s{2,}/g, ' ')
|
||
.trim();
|
||
|
||
const flagRegionCode = flagToRegionCode(flag);
|
||
core = moveProRegionToFront(core, flagRegionCode);
|
||
|
||
if (
|
||
flagRegionCode &&
|
||
!hasSpecificRegionCode(core, flagRegionCode) &&
|
||
!hasKnownRegionCode(core)
|
||
) {
|
||
core = joinAlias(flagRegionCode, core);
|
||
}
|
||
|
||
core = simplifyCoreName(core);
|
||
|
||
return flag ? `${flag} ${core}` : core;
|
||
};
|
||
|
||
proxies.forEach(proxy => {
|
||
const proxyName = typeof proxy.name === 'string' ? proxy.name : '';
|
||
const subName = typeof proxy._subName === 'string' ? proxy._subName.trim() : '';
|
||
const coreName = stripExistingSuffix(proxyName, subName);
|
||
const normalizedCoreName = normalizeCoreName(coreName);
|
||
const baseName = subName ? `${normalizedCoreName} - ${subName}` : normalizedCoreName;
|
||
|
||
proxy._tempBaseName = baseName;
|
||
globalCount[baseName] = (globalCount[baseName] || 0) + 1;
|
||
});
|
||
|
||
return proxies.map(proxy => {
|
||
const baseName = proxy._tempBaseName;
|
||
|
||
if (globalCount[baseName] > 1) {
|
||
currentCount[baseName] = (currentCount[baseName] || 0) + 1;
|
||
proxy.name = `${baseName} ${currentCount[baseName].toString().padStart(2, '0')}`;
|
||
} else {
|
||
proxy.name = baseName;
|
||
}
|
||
|
||
delete proxy._tempBaseName;
|
||
return proxy;
|
||
});
|
||
}
|