Files
CloudSearch/packages/backend/src/cloud/drivers/quark-rename.ts
T

260 lines
10 KiB
TypeScript

import * as crypto from 'crypto';
/**
* 防和谐重命名模块。
* 对文件名/目录名执行谐音替换 + 可读标签保留(集数、画质、语言等)。
*/
// ==================== Homophone Map ====================
const HOMOPHONE_MAP: Record<string, string> = {
// 网盘热门番名 — 谐音替换 (same sound, different char)
'斗':'陡','破':'坡','苍':'仓','穹':'穷',
'完':'玩','美':'每','世':'士','界':'介',
'凡':'烦','人':'仁','修':'休','罗':'络',
'仙':'先','逆':'腻','遮':'折','天':'添',
'吞':'屯','噬':'逝','大':'达','主':'嘱','宰':'崽',
'星':'惺','辰':'晨','变':'便','一':'伊','念':'捻',
'永':'泳','恒':'横','神':'申','墓':'暮','长':'尝','生':'甥',
'剑':'箭','来':'莱','诡':'鬼','秘':'蜜',
'全':'泉','职':'值','盘':'磐','龙':'笼',
'雪':'血','鹰':'莺','莽':'蟒','荒':'慌','纪':'记',
'珠':'株','王':'亡','座':'坐','牧':'木','记':'计',
'沧':'舱','元':'圆','图':'涂','紫':'仔','川':'串',
'百':'白','炼':'恋','成':'程','饶':'绕','命':'冥',
// 通用谐音替换
'的':'得','了':'啦','是':'事','不':'布','我':'窝',
'你':'尼','他':'她','有':'友','和':'合','与':'予',
'上':'尚','下':'夏','中':'忠','第':'弟','集':'级',
'话':'划','季':'际','年':'念','月':'阅','日':'曰',
'新':'心','版':'板','高':'糕','清':'青','原':'源',
'小':'晓','片':'篇','视':'市','频':'贫','道':'到',
'动':'洞','画':'话','声':'升','音':'因','文':'闻',
'明':'名','暗':'黯','光':'广','影':'映','色':'瑟',
'风':'疯','雨':'语','花':'华','国':'果','家':'佳',
'战':'站','争':'挣','士':'仕','兵':'宾',
'皇':'惶','帝':'谛','魔':'磨','鬼':'诡','怪':'乖',
'精':'经','灵':'铃','妖':'夭','武':'舞','侠':'狭',
'杀':'刹','血':'雪','刀':'叨','枪':'呛','炮':'泡',
'时':'石','空':'孔','前':'钱','后':'厚','东':'冬',
'南':'难','西':'夕','北':'备','开':'凯','关':'官',
'出':'初','进':'近','去':'趣',
'短':'短','多':'多','少':'少','真':'贞','假':'价',
'好':'郝','坏':'怀','对':'队','错':'措','以':'已',
'从':'从','被':'被','把':'把','将':'将','在':'在',
'但':'但','就':'就','才':'才','也':'也','很':'狠',
'又':'又','再':'再','更':'更','最':'最','总':'总',
'共':'共','只':'只','各':'各','每':'每','任':'任',
'所':'所','该':'该','本':'本',
};
const NOISE_CJK = '的了在是不有会可对所之也同与及但或如且乃而岂乎焉兮哉亦犹尚乃其若故盖诸焉欤' +
'么个着过把对为从以到说时要就这那和上人家下能出得发来年心开物力些长样吧啊哦嗯嚯哇咯呗哟嘿呵哈';
// ==================== Helpers ====================
/** Convert Chinese text to homophonic (substitute chars with same sound) */
function homophonicText(text: string): string {
let result = '';
for (const ch of text) {
if (/[\u4e00-\u9fff]/.test(ch)) {
const homophone = HOMOPHONE_MAP[ch];
result += homophone || ch;
} else {
result += ch;
}
}
return result;
}
/** Convert Chinese text to pinyin-initial-like string (each char → first pinyin letter or fallback) */
function pinyinLike(text: string): string {
let result = '';
for (const ch of text) {
if (/[\u4e00-\u9fff]/.test(ch)) {
const homophone = HOMOPHONE_MAP[ch];
if (homophone) {
result += pinyinInitial(homophone);
} else {
const code = ch.charCodeAt(0);
result += String.fromCharCode(97 + (code % 26));
}
} else if (/[a-zA-Z0-9]/.test(ch)) {
result += ch;
} else if (/[\s._-]/.test(ch)) {
result += '_';
}
}
return result.replace(/_+/g, '_').replace(/^_|_$/g, '');
}
/** Get pinyin initial (first letter of pinyin) for a Chinese character */
function pinyinInitial(ch: string): string {
const code = ch.charCodeAt(0);
if (code >= 0x4E00 && code <= 0x9FFF) {
const initials = ['b','p','m','f','d','t','n','l','g','k','h','j','q','x','zh','ch','sh','r','z','c','s','y','w'];
const idx = Math.min(Math.floor((code - 0x4E00) / 700), initials.length - 1);
return initials[idx];
}
return ch.toLowerCase();
}
// ==================== Public API ====================
/**
* Anti-harmony rename for directories.
* 80%: light homophonic replacement, 20%: partial pinyin.
*/
export function magicRenameDir(dirName: string): string {
const hash = crypto.createHash('md5').update(dirName + Date.now()).digest('hex').slice(0, 4);
let cleanName = dirName.trim().replace(/\s+/g, ' ');
if (!cleanName) {
return `media_${hash}`;
}
let baseName: string;
if (Math.random() < 0.2) {
// Partial pinyin: 30% of CJK chars → pinyin initial, 70% stay as-is
const chars = [...cleanName];
const result: string[] = [];
for (const ch of chars) {
if (/[\u4e00-\u9fff]/.test(ch) && Math.random() < 0.3) {
result.push(pinyinInitial(ch));
} else {
result.push(ch);
}
}
baseName = result.join('');
} else {
// Light homophonic: replace each CJK char, keep everything else as-is
const chars = [...cleanName];
const result: string[] = [];
for (const ch of chars) {
if (/[\u4e00-\u9fff]/.test(ch)) {
result.push(HOMOPHONE_MAP[ch] || ch);
} else {
result.push(ch);
}
}
baseName = result.join('');
// Optional: insert 0-2 light noise chars (low probability)
const noiseCount = Math.random() < 0.3 ? (Math.random() < 0.5 ? 1 : 2) : 0;
for (let n = 0; n < noiseCount; n++) {
const pos = Math.floor(Math.random() * (baseName.length + 1));
const ink = NOISE_CJK[Math.floor(Math.random() * NOISE_CJK.length)];
baseName = baseName.slice(0, pos) + ink + baseName.slice(pos);
}
}
baseName = baseName.replace(/[^\u4e00-\u9fff\w]/g, '_');
baseName = baseName.replace(/_+/g, '_').replace(/^_|_$/g, '');
if (baseName.length > 30) baseName = baseName.slice(0, 30);
return `${baseName}_${hash}`;
}
/**
* Anti-harmony rename for files.
* KEEPS: episode numbers, quality, language tags, original extension.
* REPLACES: Chinese title with homophonic/pinyin.
*/
export function magicRename(filename: string): string {
const hash = crypto.createHash('md5').update(filename + Date.now()).digest('hex').slice(0, 8);
let ext = '';
const extMatch = filename.match(/\.[a-zA-Z0-9]+$/);
if (extMatch) {
ext = extMatch[0];
filename = filename.slice(0, -ext.length);
}
// Extract and REMEMBER: episode info, quality, language, year
const episodePatterns = [
{ regex: /第\s*(\d+)\s*[集话話話話话回章期]/, format: (m: string) => 'Ep' + m.replace(/[^\d]/g, '') },
{ regex: /Ep\d+|ep\d+/i, format: (m: string) => m.toUpperCase() },
{ regex: /Part\s*\d+/i, format: (m: string) => m.replace(/\s+/g, '') },
{ regex: /E\d{2,}/i, format: (m: string) => m.toUpperCase() },
];
let episodeTag = '';
for (const { regex, format } of episodePatterns) {
const m = filename.match(regex);
if (m) {
episodeTag = format(m[0]);
filename = filename.replace(m[0], '');
break;
}
}
// Extract and REMEMBER: quality tags
const qualityPattern = /\b(4k|1080p|1080P|2160p|720p|HD|BluRay|Blu-ray|HDR|WEB-DL|WEBRip|BDRip|REMUX|DV|Dovi|HEVC|x264|x265|H\.264|H\.265)\b/;
const qualityMatch = filename.match(qualityPattern);
const qualityTag = qualityMatch ? qualityMatch[0] : '';
if (qualityMatch) filename = filename.replace(qualityMatch[0], '');
// Extract and REMEMBER: language tags
const langPattern = /\b(CHS|CHT|JP|EN|BIG5|GB|粤语|国语|日语|英语|中字|日字|英字|繁体中字)\b/;
const langMatch = filename.match(langPattern);
const langTag = langMatch ? langMatch[0] : '';
if (langMatch) filename = filename.replace(langMatch[0], '');
// Extract and REMEMBER: year
const yearMatch = filename.match(/\b(20\d{2})\b/);
const yearTag = yearMatch ? yearMatch[0] : '';
if (yearMatch) filename = filename.replace(yearMatch[0], '');
// Extract and REMEMBER: season info
const seasonMatch = filename.match(/第?\s*(\d+)\s*[季部期]/);
const seasonTag = seasonMatch ? `${seasonMatch[1]}季` : '';
if (seasonMatch) filename = filename.replace(seasonMatch[0], '');
// Now process the remaining name (mostly Chinese title)
filename = filename.replace(/[._\-【】\[\]()()\s]+/g, '_').trim();
const useHomophonic = Math.random() > 0.5;
let titlePart: string;
if (useHomophonic) {
titlePart = homophonicText(filename);
titlePart = titlePart.replace(/[^\u4e00-\u9fff\wa-zA-Z0-9]/g, '_');
titlePart = titlePart.replace(/_+/g, '_').replace(/^_|_$/g, '');
if (titlePart.length > 15) titlePart = titlePart.slice(0, 15);
} else {
titlePart = pinyinLike(filename);
titlePart = titlePart.replace(/[^a-zA-Z0-9]/g, '_');
titlePart = titlePart.replace(/_+/g, '_').replace(/^_|_$/g, '');
if (titlePart.length > 15) titlePart = titlePart.slice(0, 15);
}
// Remove sensitive keywords from title part
const sensitiveWords = /斗破|完美|凡人|仙逆|遮天|吞噬|大主宰|绝世|武动|星辰变|一念永恒|修罗|神墓|长生|剑来|诡秘|全职|斗罗|盘龙|雪鹰|莽荒纪|天珠变|神印王座|牧神记|沧元图|紫川|百炼成神|大王饶命|全球高考/ig;
titlePart = titlePart.replace(sensitiveWords, '');
titlePart = titlePart.replace(/_+/g, '_').replace(/^_|_$/g, '');
// Build preserved tags
const tags: string[] = [];
if (seasonTag) tags.push(seasonTag);
if (episodeTag) tags.push(episodeTag);
if (qualityTag) tags.push(qualityTag.toUpperCase());
if (langTag) tags.push(langTag);
if (yearTag) tags.push(yearTag);
tags.push(hash); // Always add hash for uniqueness
const newExt = ext || '.bin';
const parts = [titlePart, ...tags].filter(Boolean);
let result = parts.join('_');
if (result.length > 80) {
result = result.slice(0, 80);
}
if (result.length < 10) {
const filler = crypto.randomBytes(4).toString('hex');
result = `${filler}_${result}`;
}
return result + newExt;
}