// ==UserScript== // @name 115Rename2026 // @namespace https://github.com/liuchanghuaX1/115Rename2026 // @version 1.9.2 // @description 115视频整理:彻底清除@前缀|不碰扩展名|根除番号重复|多站改名+归档+评分+备份 // @author sonarlee // @include https://115.com/* // @icon https://115.com/favicon.ico // @domain javbus.com // @domain avmoo.host // @domain avsox.host // @domain javdb.com // @connect javbus.com // @connect javlibrary.com // @connect xslist.org // @connect javdb.com // @connect webapi.115.com // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_download // @grant GM_setClipboard // @license MIT // @homepageURL https://github.com/liuchanghuaX1/115Rename2026 // @supportURL https://github.com/liuchanghuaX1/115Rename2026/issues // @downloadURL https://raw.githubusercontent.com/liuchanghuaX1/115Rename2026/main/115Rename2026.user.js // @updateURL https://raw.githubusercontent.com/liuchanghuaX1/115Rename2026/main/115Rename2026.user.js // ==/UserScript== (function () { "use strict"; // ========== UI 初始化 ========== const rootInfoId = 'archive-root-info-' + Date.now(); function cleanupExistingRootInfo() { try { document.querySelectorAll('[id^="archive-root-info"]').forEach(el => el.remove()); document.querySelectorAll('iframe').forEach(iframe => { try { if (iframe.contentDocument) iframe.contentDocument.querySelectorAll('[id^="archive-root-info"]').forEach(el => el.remove()); } catch (e) { } }); } catch (e) { } } cleanupExistingRootInfo(); const uiStyle = ``; $('head').append(uiStyle); const ROOT_DIR_CID = "0"; let archiveRootCid = GM_getValue("archiveRootCid", null); let archiveRootName = GM_getValue("archiveRootName", null); const infoCache = {}, actressCache = {}, folderCidCache = {}; // ========== 并发与进度 ========== function runTasksWithLimit(tasks, limit, doneAll) { if (!tasks.length) { doneAll && doneAll(); return; } let index = 0, running = 0; const next = () => { if (index >= tasks.length && running === 0) { doneAll && doneAll(); return; } while (running < limit && index < tasks.length) { const task = tasks[index++]; running++; task(() => { running--; next(); }); } }; next(); } window.progressBox = { init(title, total) { this.total = total || 0; this.current = 0; this.title = title || '任务进度'; let $box = $('#task-progress-box'); if ($box.length === 0) { $('body').append(``); $box = $('#task-progress-box'); } $box.find('.tp-title').text(this.title); this.update(0); $box.show(); }, update(doneCount) { this.current = doneCount; const pct = Math.min(100, Math.round(doneCount * 100 / (this.total || 1))); const $box = $('#task-progress-box'); $box.find('.tp-bar-inner').css('width', pct + '%'); $box.find('.tp-text').text(`${doneCount}/${this.total} (${pct}%)`); }, finish() { this.update(this.total); setTimeout(() => $('#task-progress-box').fadeOut(300), 800); } }; window.showPageNotification = (message, type = 'info', duration = 3000) => { if (duration === 3000) { if (type === 'success') duration = 3000; else if (type === 'error') duration = 5000; } const id = 'cn-' + Date.now(); $('body').append(`
${message}
`); setTimeout(() => $(`#${id}`).addClass('show'), 10); setTimeout(() => { $(`#${id}`).removeClass('show'); setTimeout(() => $(`#${id}`).remove(), 300); }, duration); }; const showArchiveRootInfo = () => { cleanupExistingRootInfo(); let msg = (archiveRootCid && archiveRootName) ? `当前归档根目录: "${archiveRootName}"` : "当前无归档根目录,将使用115网盘根目录"; if (window.self === window.top) $('body').append(`
${msg}
`); }; let rootInfoTimer = null; const initializeRootInfo = () => { if (window.self !== window.top) return; if (rootInfoTimer) clearTimeout(rootInfoTimer); rootInfoTimer = setTimeout(() => { showArchiveRootInfo(); rootInfoTimer = null; }, 2000); }; $(window).on('load', initializeRootInfo); if (document.readyState === 'complete') initializeRootInfo(); // ========== 菜单 ========== const rename_list = `
  • 本地番号加工 改名(多网站轮询) 归档至文件夹 设为归档根目录 获取javdb评分 备份文件名
  • `; let interval = setInterval(buttonInterval, 1000); const javbusBase = "https://www.javbus.com/"; const javbusDirectAccess = javbusBase; const javbusUncensoredBase = javbusBase + "uncensored/"; const javlibSearchBase = "https://www.javlibrary.com/cn/vl_searchbyid.php?keyword="; const javlibBase = "https://www.javlibrary.com/"; const xslistBase = "https://xslist.org/tw/"; const javdbBase = "https://javdb.com"; const javdbSearchBase = javdbBase + "/search?q="; // ========== 域名/广告前缀清理(@ 左侧全部丢弃) ========== const stripDomainPrefix = (filename) => { const idx = filename.lastIndexOf('@'); return idx === -1 ? filename : filename.substring(idx + 1).trim(); }; // ========== 垃圾词与标记 ========== const GARBAGE_WORDS = [ 'WWW', 'FHD', 'HD', 'SD', 'X264', 'X265', 'H264', 'H265', 'HEVC', 'AVC', 'AAC', 'AC3', 'DTS', 'FLAC', 'MP3', 'MP4', 'MKV', 'AVI', 'WMV', 'M4V', 'RMVB', 'ISO', 'TS', 'WATERMARK', 'RARBG', 'WEB-DL', 'WEBRIP', 'BLURAY', 'BDREMUX', '1440P', '1080P', '720P', '480P' ]; const GARBAGE_REGEX = new RegExp('\\b(' + GARBAGE_WORDS.join('|') + ')\\b', 'gi'); const MARKER_PATTERN = /(4K|8K|60fps|120fps|破解|流出|leak(?:ed)?|無修正|无码|uncensored|中字|字幕|chinese|chs|cht|big5|gb|sc|中文字幕|2160p|VR)/gi; const MARKER_MAP = { leak: '流出', leaked: '流出', 流出: '流出', uncensored: '无码', 無修正: '无码', 无码: '无码', chs: '中文字幕', cht: '中文字幕', gb: '中文字幕', big5: '中文字幕', sc: '中文字幕', chinese: '中文字幕', 中字: '中文字幕', 字幕: '中文字幕', 中文: '中文字幕', 中文字幕: '中文字幕', '4k': '4K', '8k': '8K', '60fps': '60fps', '120fps': '120fps', 破解: '破解', '2160p': '4K', vr: 'VR' }; const AD_BADGES = /\[3Q\]|\(原\)|\[BT\]|【广告】|\[廣告\]/gi; const removeMarkers = (str) => { return str.replace(MARKER_PATTERN, (match, p1, offset, full) => { const lower = match.toLowerCase(); if (offset > 0 && /[a-z0-9]/i.test(full[offset - 1])) return match; if (offset + match.length < full.length && /[a-z0-9]/i.test(full[offset + match.length])) return match; return ' '; }); }; // ========== 番号前缀库(长优先) ========== const CODE_PREFIXES = [ 'LEGSJAPAN', 'AYAKISAKI', 'SPERMMANIA', 'FELLATIOJAPAN', 'S2MCR', 'MXVR', 'SIVR', 'T28', 'S2M', '300MAAN', '200GANA', '259LUXU', '277DCV', '230GANA', '261ADA', 'DASS', 'REBD', 'REBDB', 'MIDV', 'SSIS', 'PRED', 'PRTD', 'FSDSS', 'SAMA', 'MIDE', 'MIAD', 'MIAA', 'MIAE', 'MIAS', 'MIGD', 'MIRD', 'MIFD', 'MIID', 'MIZD', 'MDYD', 'MBYD', 'MEYD', 'WANZ', 'NWF', 'BMW', 'JBD', 'RBD', 'ATAD', 'SHKD', 'SSPD', 'ATID', 'ADN', 'IPTD', 'IPZ', 'IPX', 'IPZZ', 'IPIT', 'IPITD', 'IDBD', 'SUPD', 'IPSD', 'DAN', 'AND', 'KAWD', 'KWBD', 'KAPD', 'JUC', 'JUX', 'JUY', 'JUSD', 'JUKD', 'OBA', 'URE', 'JUFE', 'FINH', 'EBOD', 'MKCK', 'EYAN', 'KIRD', 'KIBD', 'BLK', 'KISD', 'ONED', 'SOE', 'SNIS', 'SSNI', 'OFJE', 'SPS', 'SRXV', 'TMSD', 'NEXD', 'PGD', 'PBD', 'PJD', 'TEK', 'PPPD', 'HND', 'TYOD', 'TPPN', 'BF', 'ZUKO', 'BID', 'BBI', 'CJOD', 'CLUB', 'MMND', 'TEAM', 'HHK', 'ALB', 'MUKD', 'MUDR', 'MUM', 'ANND', 'BBAN', 'MOND', 'SPRD', 'VENU', 'VEMA', 'VAGU', 'STARS', 'STAR', 'SACE', 'SDMS', 'SDDE', 'SDMT', 'SDDM', 'SDNM', 'SDAB', 'SDSI', 'SDMU', 'DVDPS', 'DVDES', 'NHDT', 'NHDTA', 'RNHDT', 'IESP', 'IDOL', 'IENE', 'OPEN', 'SVND', 'HBAD', 'HAVD', 'NTR', 'VSPDS', 'VSPDR', 'MV', 'FSET', 'DANDY', 'LADY', 'HUNTA', 'HUNTB', 'HUNT', 'GAR', 'SVDVD', 'RCT', 'RCTD', 'NGKS', 'RD', 'KUF', 'NSS', 'UPSM', 'SERO', 'DVAJ', 'DV', 'XVSR', 'XVSE', 'XV', 'PXV', 'MADA', 'MDS', 'RMLD', 'MILD', 'MDB', 'RMDBB', 'RMDS', 'REAL', 'NATR', 'SCOP', 'SAMA', 'BOKD', 'ABS', 'ABP', 'KBH', 'EZD', 'MAS', 'INU', 'JOB', 'EDD', 'ESK', 'MEK', 'DOM', 'YRZ', 'PPP', 'EVO', 'SAD', 'GYD', 'HYK', 'FST', 'TBL', 'LOO', 'TOR', 'TD', 'RBS', 'MAN', 'ZZR', 'WPC', 'BNDV', 'CRS', 'HODV', 'HRDV', 'YMDD', 'TMD', 'DSD', 'RJMD', 'ALD', 'DBE', 'DOJ', 'OFCD', 'SEND', 'ULJM', 'DSS', 'MOED', 'DER', 'OPD', 'GRYD', 'MSBD', 'SS', 'HD', 'DVH', 'REID', 'GEN', 'DBUD', 'IBW', 'MMO', 'ADZ', 'AKB', 'HITMA', 'RAY', '24ID', 'COSQ', 'GRET', 'GATE', 'GEXP', 'GGFH', 'GGTB', 'GMMD', 'GODS', 'GPTM', 'GSAD', 'GXXD', 'GDGA', 'GOMK', 'GTRL', 'GOMD', 'GDSC', 'TBW', 'TBB', 'TDP', 'TDLN', 'TGGP', 'THP', 'THZ', 'TMS', 'TZZ', 'TRE', 'TSGS', 'TSDL', 'TSWN', 'TSW', 'TTRE', 'ATHB', 'AKBD', 'DMG', 'MGJH', 'ANIX', 'CYCD', 'YNO', 'AZGB', 'SKOT', 'SHP', 'JMSZ', 'JHZD', 'NFDM', 'CGAD', 'CGBD', 'CHSD', 'CUSD', 'CHSH', 'CMV', 'PAED', 'RGI', 'ZARD', 'ZATS', 'ZDAD', 'ZKV', 'COSETT', 'MXGS', 'MX3DS', 'IPBZ', 'FSDSS', 'SVMGM', 'MIDA', 'DSAM', 'RED', 'BT', 'MX', 'SI', 'VOL', 'CR', 'N' ].sort((a, b) => b.length - a.length); const matchCodeByPrefix = str => { if (!str) return null; for (const p of CODE_PREFIXES) { const m = str.match(new RegExp(`\\b${p}[-_ ]?0*(\\d{1,5})\\b`, 'i')); if (m) return `${p}-${(m[1] === '0' ? '0' : m[1]).padStart(3, '0')}`; } for (const p of CODE_PREFIXES) { const m = str.match(new RegExp(`\\b${p}[-_ ]?0*(\\d{1,5})(?![0-9])`, 'i')); if (m) return `${p}-${(m[1] === '0' ? '0' : m[1]).padStart(3, '0')}`; } const loose = str.match(/\b([A-Z]{2,8})\s*0*(\d{2,5})\b/); if (loose) { const prefix = loose[1]; if (!GARBAGE_WORDS.includes(prefix) && prefix.length > 1) { let num = Number(loose[2]).toString(); if (num === '0') num = '0'; return `${prefix}-${num.padStart(3, '0')}`; } } return null; }; // 增强的 FC2 番号提取 const extractFC2Code = (str) => { const patterns = [ /\bFC2[\s_-]*PPV[\s_-]*(\d{5,7})\b/i, /\bFC2PPV[\s_-]*(\d{5,7})\b/i, /\bFC2[\s_-]+(\d{5,7})\b/i, /\bFC2-(\d{5,7})\b/i, /\bFC2(\d{5,7})\b/i, /\bPPV[\s_-]*(\d{5,7})\b/i, /\bF[\s_-]*(\d{5,7})\b(?!\d)/i, ]; for (const regex of patterns) { const m = str.match(regex); if (m && m[1] && !/^(?:HD|FHD|SD|X264|X265|H264|H265|HEVC|AVC|AAC|AC3|DTS|FLAC|MP3|MP4|MKV|AVI|WMV|M4V|RMVB|ISO|TS|WATERMARK|RARBG|WEB-DL|WEBRIP|BLURAY|BDREMUX|1440P|1080P|720P|480P)$/i.test(m[1])) { return 'FC2-PPV-' + m[1]; } } return null; }; // 根除标题中所有番号变体(包括 Tokyo-Hot、FC2 所有粘连形式) const removeAllCodeVariants = (str, baseCode) => { if (!baseCode) return str; // 标准番号 const stdMatch = baseCode.match(/^([A-Za-z]+)[-_\s]?(\d+)$/); if (stdMatch) { const prefix = stdMatch[1]; const rawNum = parseInt(stdMatch[2], 10).toString(); str = str.replace(new RegExp(`\\b${prefix}[-_\\s.]*0*${rawNum}\\b`, 'gi'), ' '); } // FC2 if (/^FC2[-_\s]?PPV[-_\s]?\d+$/i.test(baseCode)) { const num = baseCode.match(/\d+$/)[0]; const rawNum = parseInt(num, 10).toString(); str = str.replace(new RegExp(`\\b(?:FC2[-_\\s.]?(?:PPV[-_\\s.]?)?0*${rawNum}|PPV[-_\\s.]?0*${rawNum})\\b`, 'gi'), ' '); } // Tokyo-Hot const thMatch = baseCode.match(/^Tokyo[-_\s]*Hot[-_\s]*[nN](\d{3,4})$/i); if (thMatch) { const num = thMatch[1].padStart(4, '0'); const rawNum = parseInt(num, 10).toString(); str = str.replace(new RegExp( `\\b(?:Tokyo\\s*[-_\\s]*Hot\\s*[-_\\s]*[nN]?\\s*0*${rawNum}|` + `TokyoHotn?${rawNum}|` + `Hotn?${rawNum})` + `(?:\\s*Tokyo|\\s*Hot|\\s*FHD|\\s*HD)?\\b`, 'gi' ), ' '); } return str.replace(/\s+/g, ' ').trim(); }; // ========== 核心解析(绝不碰扩展名,优先清除广告前缀) ========== const parseVideoInfo = origTitle => { try { if (!origTitle) return null; let raw = String(origTitle); // 1. 最高优先:删除 @ 左侧所有广告字符 raw = stripDomainPrefix(raw); let rawForCode = raw; // 2. 提取独立标记 let markers = []; rawForCode.replace(MARKER_PATTERN, (match, p1, offset, full) => { const lower = match.toLowerCase(); if (offset > 0 && /[a-z0-9]/i.test(full[offset - 1])) return match; if (offset + match.length < full.length && /[a-z0-9]/i.test(full[offset + match.length])) return match; const nm = MARKER_MAP[lower]; if (nm && !markers.includes(nm)) markers.push(nm); return match; }); // 3. 提取日期 let dateStr = ''; const dm = rawForCode.match(/(?:\b|_|^|@|】|\[|【)((?:19|20)\d{2}[-_\/\.\s]+\d{1,2}[-_\/\.\s]+\d{1,2})(?:\b|_|$|(?=[A-Za-z\u4e00-\u9fa5【\[\]】]))/i); if (dm) { const parts = dm[1].trim().split(/[-_\/\.\s]+/); if (parts.length === 3) { const year = parts[0].length === 2 ? '20' + parts[0] : parts[0]; dateStr = `${year}-${parts[1].padStart(2, '0')}-${parts[2].padStart(2, '0')}`; } rawForCode = rawForCode.replace(dm[0], ' '); } // 4. 构建清洁字符串用于番号提取 let t = removeMarkers(rawForCode).toUpperCase(); t = t.replace(/(?:\b|_|^|@|】|\[|【)(?:19|20)\d{2}[-_\/\.\s]+\d{1,2}[-_\/\.\s]+\d{1,2}(?:\b|_|$|(?=[A-Z]))/ig, ' '); t = t.replace(GARBAGE_REGEX, ' ').replace(/[\[\]\{\}()【】]/g, ' ').replace(/[_\.\-\/\\]+/g, ' '); t = t.replace(/\b[01]+(?=[A-Z])/g, '').replace(/\b([A-Z])\s(?=[A-Z]\b)/g, '$1'); // 5. 提取番号 let queryCode = null, displayCode = null; const thMatch = rawForCode.match(/Tokyo[\s_-]*Hot[\s_-]*[nN]?(\d{3,4})/i); if (thMatch) { const num = thMatch[1].padStart(4, '0'); queryCode = `Tokyo-Hot-n${num}`; displayCode = queryCode; } else { const fc2Code = extractFC2Code(rawForCode) || extractFC2Code(t); if (fc2Code) { queryCode = fc2Code; displayCode = fc2Code; } else { const numM = t.match(/\b(\d{4,6})[-_ ](\d{3,4})\b/); if (numM) { queryCode = `${numM[1]}_${numM[2]}`; const lowerRaw = rawForCode.toLowerCase(); if (/1pon/i.test(lowerRaw)) displayCode = `1pondo-${numM[1]}-${numM[2]}`; else if (/carib/i.test(lowerRaw)) displayCode = `Caribbean-${numM[1]}-${numM[2]}`; else if (/paco/i.test(lowerRaw)) displayCode = `Pacopacomama-${numM[1]}-${numM[2]}`; else if (/heydouga/i.test(lowerRaw)) displayCode = `Heydouga-${numM[1]}-${numM[2]}`; else if (/tokyo/i.test(lowerRaw)) displayCode = `TokyoHot-${numM[1]}-${numM[2]}`; else { queryCode = `${numM[1]}-${numM[2]}`; displayCode = queryCode; } } else { queryCode = matchCodeByPrefix(t); if (queryCode) displayCode = queryCode; } } } if (!queryCode) return null; const baseCode = displayCode || queryCode; // 6. 补充标记(中文/无码后缀) const safeB = queryCode.replace(/_/g, '-').replace(/-/g, '[-_ ]?'); if (raw.indexOf("中文") !== -1 || new RegExp(safeB + "[_-](UC|C)\\b", "i").test(raw)) { if (!markers.includes('中文字幕')) markers.push('中文字幕'); } if (raw.indexOf("无码") !== -1 || new RegExp(safeB + "[_-](UC|U)\\b", "i").test(raw)) { if (!markers.includes('无码')) markers.push('无码'); } // 7. 分段提取(支持点、下划线、空格、关键词) let part = ''; const escapedBase = baseCode.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const segmentPattern = new RegExp( `${escapedBase}\\.(\\d{1,3}|[a-dA-D])(?=\\s|$|\\.\\w+$|[^\\d])` + `|${escapedBase}[_\\-](\\d{1,3}|[a-dA-D])(?=\\s|$|\\.\\w+$|[^\\d])` + `|${escapedBase}\\s+` + `(?:part|pt|cd|ep|sp|disc|vol|no|volume)\\s*[.\\-\\s]*(\\d{1,3}|[a-dA-D])` + `|${escapedBase}\\s+(\\d{1,3}|[a-dA-D])(?=\\s|$|\\.\\w+$|[^\\d])`, 'i' ); const segMatch = rawForCode.match(segmentPattern); if (segMatch) { for (let i = 1; i < segMatch.length; i++) { if (segMatch[i]) { part = segMatch[i].toUpperCase(); break; } } rawForCode = rawForCode.replace(segMatch[0], ' ').trim(); } const fullCode = part ? `${baseCode}-${part}` : baseCode; // 8. 标题清洗(根除所有番号变体) let cleanTitle = removeMarkers(rawForCode); cleanTitle = cleanTitle.replace(/(?:\b|_|^|@|】|\[|【)(?:19|20)\d{2}[-_\/\.\s]+\d{1,2}[-_\/\.\s]+\d{1,2}(?:\b|_|$|(?=[A-Z]))/ig, ' '); cleanTitle = cleanTitle.replace(/\[.*?\]|\(.*?\)|【.*?】|\{.*?\}|(.*?)/g, ' '); cleanTitle = cleanTitle.replace(AD_BADGES, ' '); cleanTitle = cleanTitle.replace(GARBAGE_REGEX, ' '); cleanTitle = cleanTitle.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim(); cleanTitle = removeAllCodeVariants(cleanTitle, baseCode); return { queryCode, baseCode, fullCode, markers, date: dateStr, localTitle: cleanTitle }; } catch (e) { console.error('parseVideoInfo error:', e); return null; } }; // ========== 构建新名称(多重去重,确保番号只出现一次) ========== const buildNewName = (vInfo, title, actresses, dateStr, suffix) => { let cleanTitle = removeAllCodeVariants(title, vInfo.baseCode); cleanTitle = cleanTitle.replace(/【[^】]*】/g, '').trim(); let name = vInfo.fullCode; if (cleanTitle) name += ' ' + cleanTitle; if (actresses && actresses.length) { const actressStr = actresses.join('・'); if (!name.includes(actressStr)) name += ' ' + actressStr; } if (vInfo.markers && vInfo.markers.length) { const uniq = [...new Set(vInfo.markers)].filter(Boolean); const existingMarkers = name.match(/【[^】]*】/g) || []; const toAdd = uniq.filter(m => !existingMarkers.includes(`【${m}】`)); if (toAdd.length) name += ' ' + toAdd.map(m => `【${m}】`).join(''); } if (dateStr) name += '_' + dateStr; if (suffix) name += suffix; name = name.replace(/\s+/g, ' ').trim(); name = name.replace(/\s+\./g, '.'); // 最终保险:再清除一次残留番号,并确保番号只出现一次 name = removeAllCodeVariants(name, vInfo.baseCode); const codeRegex = new RegExp(`\\b${vInfo.baseCode.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi'); const matches = name.match(codeRegex); if (matches && matches.length > 1) { let idx = name.indexOf(vInfo.baseCode); if (idx !== -1) { const before = name.substring(0, idx + vInfo.baseCode.length); const after = name.substring(idx + vInfo.baseCode.length).replace(codeRegex, ''); name = before + after; } } name = name.replace(/\s+/g, ' ').trim(); return name.replace(/[\\/:*?"<>|]/g, (c) => ({ '\\': '', '/': ' ', ':': ' ', '?': ' ', '"': ' ', '<': ' ', '>': ' ', '|': '' })[c] || ''); }; let renameCompareList = []; const send_115 = (id, name, fh, origFilename, callback) => { const fn = name.replace(/[\\/:*?"<>|]/g, (c) => ({ '\\': '', '/': ' ', ':': ' ', '?': ' ', '"': ' ', '<': ' ', '>': ' ', '|': '' })[c] || ''); $.post("https://webapi.115.com/files/edit", { fid: id, file_name: fn }, data => { const r = JSON.parse(data); if (!r.state) showPageNotification(`${fh} 修改失败: ${r.error}`, 'error', 3000); else { showPageNotification(`${fh} 修改成功`, 'success', 2000); if (origFilename) renameCompareList.push({ original: origFilename, new: name }); } if (typeof callback === 'function') callback(); }).fail(() => { showPageNotification(`${fh} 请求失败`, 'error', 3000); if (typeof callback === 'function') callback(); }); }; // ========== 多站刮削 ========== const normDate = d => { if (!d) return ''; const m = d.trim().match(/^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/); if (m) return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')}`; const m2 = d.trim().match(/^(\d{2})\/(\d{2})\/(\d{4})$/); if (m2) return `${m2[3]}-${m2[2]}-${m2[1]}`; return d; }; const fetchJavlib = (code, ok, fail) => { GM_xmlhttpRequest({ method: "GET", url: javlibSearchBase + encodeURIComponent(code), onload: x => { try { const $s = $(x.responseText); let link = $s.find("#video_title a").attr("href") || $s.find("div.video a[href*='?v=']").first().attr("href"); if (!link) return fail && fail("JavLibrary 搜索无结果"); if (link.startsWith('/')) link = javlibBase.replace(/\/+$/, '') + link; GM_xmlhttpRequest({ method: "GET", url: link, onload: xx => { try { const $d = $(xx.responseText); let ttl = $d.find("#video_title a").first().text().trim() || $d.find("#video_title").text().trim(); if (ttl.toUpperCase().startsWith(code.toUpperCase())) ttl = ttl.slice(code.length).trim(); const dateText = $d.find("#video_date td.text").text().trim(); const isoDate = normDate(dateText); const actresses = []; $d.find("#video_cast td.text a").each(function () { const n = $(this).text().trim(); if (n) actresses.push(n); }); if (!ttl) return fail && fail("JavLibrary 无标题"); const info = { title: ttl, date: isoDate, actresses }; infoCache[code.toUpperCase()] = info; ok && ok(info); } catch (e) { fail && fail("JavLibrary 解析失败: " + e.message); } }, onerror: () => fail && fail("JavLibrary 详情页请求失败") }); } catch (e) { fail && fail("JavLibrary 搜索解析失败: " + e.message); } }, onerror: () => fail && fail("JavLibrary 搜索请求失败") }); }; const fetchJavbus = (code, ok, fail) => { const tryUrl = u => { GM_xmlhttpRequest({ method: "GET", url: u + code, onload: x => { try { const $r = $(x.responseText); let ttl = null; const h3 = $r.find("h3"); if (h3.length) { ttl = h3.text().trim(); if (ttl.toUpperCase().startsWith(code.toUpperCase())) ttl = ttl.slice(code.length).trim(); } if (!ttl) ttl = $r.find("div.photo-frame img").attr("title"); if (!ttl) { ttl = $r.find("title").text().trim(); if (ttl.includes(" - JavBus")) ttl = ttl.split(" - JavBus")[0].trim(); if (ttl.toUpperCase().startsWith(code.toUpperCase())) ttl = ttl.slice(code.length).trim(); } let isoDate = ''; $r.find("p").each(function () { const t = $(this).text().trim(); if (/發行日期|发行日期/.test(t)) { const m = t.match(/(\d{4}-\d{2}-\d{2})/); if (m) isoDate = normDate(m[1]); } }); if (!isoDate) { const p = $r.find(".info p:contains('發行日期'), .info p:contains('发行日期')"); if (p.length) isoDate = normDate(p.text().replace(/.*?[::]/, '').trim()); } const actresses = []; $r.find("span.genre a[href*='/star/']").each(function () { const n = $(this).text().trim(); if (n) actresses.push(n); }); if (!ttl) { if (u !== javbusUncensoredBase) return tryUrl(javbusUncensoredBase); return fail && fail("JavBus 无标题"); } const info = { title: ttl, date: isoDate, actresses }; infoCache[code.toUpperCase()] = info; ok && ok(info); } catch (e) { fail && fail("JavBus 解析失败: " + e.message); } }, onerror: () => { if (u !== javbusUncensoredBase) return tryUrl(javbusUncensoredBase); fail && fail("JavBus 请求失败"); } }); }; tryUrl(javbusDirectAccess); }; const fetchXslist = (code, ok, fail) => { const parsePage = ($pg, cbOk, cbFail) => { const uc = code.toUpperCase(); let tr = null; $pg.find("#movices tbody tr").each(function () { const c = ($(this).find("td").eq(0).find("strong").text() || '').trim().toUpperCase(); if (c === uc) { tr = $(this); return false; } }); if (!tr) return cbFail && cbFail("xslist 模型页未列出该番号"); const $tds = tr.find("td"); const ttl = $tds.eq(1).text().trim(); const dt = $tds.eq(2).text().trim(); let isoDate = ''; if (dt && !/n\/a/i.test(dt)) isoDate = normDate(dt); const aname = $pg.find("h1 span[itemprop='name']").first().text().trim(); const actresses = aname ? [aname] : []; if (!ttl) return cbFail && cbFail("xslist 无标题"); const info = { title: ttl, date: isoDate, actresses }; infoCache[code.toUpperCase()] = info; cbOk && cbOk(info); }; GM_xmlhttpRequest({ method: "GET", url: xslistBase + "search?query=" + encodeURIComponent(code), onload: x => { try { const $s = $(x.responseText); if ($s.find("#movices").length && $s.find("h1 span[itemprop='name']").length) { return parsePage($s, ok, fail); } let link = $s.find("a[href*='/model/']").first().attr("href"); if (!link) return fail && fail("xslist 搜索无结果"); if (link.startsWith('/')) link = xslistBase.replace(/\/+$/, '') + link; GM_xmlhttpRequest({ method: "GET", url: link, onload: dx => { try { parsePage($(dx.responseText), ok, fail); } catch (e) { fail && fail("xslist 详情解析失败: " + e.message); } }, onerror: () => fail && fail("xslist 详情页请求失败") }); } catch (e) { fail && fail("xslist 搜索解析失败: " + e.message); } }, onerror: () => fail && fail("xslist 搜索请求失败") }); }; const fetchJavdb = (code, ok, fail) => { GM_xmlhttpRequest({ method: "GET", url: `${javdbSearchBase}${encodeURIComponent(code)}&f=all`, onload: x => { try { const $h = $(x.responseText); let link = $h.find('a[href*="/v/"]').first().attr('href') || $h.find('.movie-list .item a').first().attr('href'); if (!link) return fail && fail("JavDB 搜索无结果"); if (link.startsWith('/')) link = javdbBase + link; GM_xmlhttpRequest({ method: "GET", url: link, onload: dx => { try { const $d = $(dx.responseText); let ttl = $d.find('h2.title').text().trim() || $d.find('strong.current-title').text().trim(); if (ttl.toUpperCase().startsWith(code.toUpperCase())) ttl = ttl.slice(code.length).trim(); let dateText = ''; $d.find('.panel-block').each(function () { const t = $(this).text().trim(); if (/日期:|發行日期:|发行日期:/.test(t)) { dateText = t.replace(/.*?[::]/, '').trim(); return false; } }); const isoDate = normDate(dateText); const actresses = []; $d.find('a[href*="/actors/"]').each(function () { const n = $(this).text().trim(); if (n) actresses.push(n); }); if (!ttl) return fail && fail("JavDB 无标题"); const info = { title: ttl, date: isoDate, actresses }; infoCache[code.toUpperCase()] = info; ok && ok(info); } catch (e) { fail && fail("JavDB 详情解析失败: " + e.message); } }, onerror: () => fail && fail("JavDB 详情页请求失败") }); } catch (e) { fail && fail("JavDB 搜索解析失败: " + e.message); } }, onerror: () => fail && fail("JavDB 搜索请求失败") }); }; // ========== 改名主流程 ========== window.rename_multi = (fid, vInfo, suffix, addDate, callback, origFilename) => { const code = vInfo.queryCode; if (/^FC2-PPV-\d{5,7}$/i.test(code)) { showPageNotification('FC2 番号不支持在线信息,使用本地改名', 'info', 2500); local_rename(fid, vInfo, suffix, addDate, callback, origFilename); return; } const key = code.toUpperCase(); if (infoCache[key]) { const info = infoCache[key]; const newName = buildNewName(vInfo, info.title || vInfo.localTitle, info.actresses, (addDate && info.date) ? info.date : (addDate ? vInfo.date : ""), suffix); send_115(fid, newName, vInfo.fullCode, origFilename, callback); return; } const apply = info => { const newName = buildNewName(vInfo, info.title || vInfo.localTitle, info.actresses, (addDate && info.date) ? info.date : (addDate ? vInfo.date : ""), suffix); send_115(fid, newName, vInfo.fullCode, origFilename, callback); }; fetchJavlib(code, apply, () => { fetchJavbus(code, apply, () => { fetchXslist(code, apply, () => { fetchJavdb(code, apply, () => { showPageNotification(`所有信息源未找到 ${code}`, 'error', 4000); if (typeof callback === 'function') callback(); }); }); }); }); }; const local_rename = (fid, vInfo, suffix, addDate, callback, origFilename) => { const newName = buildNewName(vInfo, vInfo.localTitle, [], vInfo.date, suffix); send_115(fid, newName, vInfo.fullCode, origFilename, callback); }; // ========== 批量处理(双确认导出) ========== const rename = (call, addDate) => { const $items = $("iframe[rel='wangpan']").contents().find("li.selected"); const cnt = $items.length; if (!cnt) { showPageNotification("请先选择文件或文件夹", 'info', 3000); return; } const isLocal = (call === local_rename); progressBox.init(isLocal ? '本地番号加工' : '联网改名', cnt); showPageNotification(`开始处理 ${cnt} 个文件...`, 'info', 3000); renameCompareList = []; const tasks = []; $items.each(function () { const $it = $(this); const fn = $it.attr("title"); const ft = $it.attr("file_type"); let fid, suffix = ''; if (ft === "0") fid = $it.attr("cate_id"); else { fid = $it.attr("file_id"); const idx = fn.lastIndexOf('.'); if (idx !== -1) suffix = fn.substring(idx); } if (!fid || !fn) return; const vi = parseVideoInfo(fn); if (!vi) return; tasks.push((done) => { call(fid, vi, suffix, addDate, done, fn); }); }); const concurrency = isLocal ? 5 : 3; let processed = 0; const wrapped = tasks.map(t => done => t(() => { processed++; progressBox.update(processed); done(); })); runTasksWithLimit(wrapped, concurrency, () => { progressBox.finish(); showPageNotification(`所有文件处理完成`, 'success', 5000); if (renameCompareList.length > 0) { if (confirm('改名已完成,是否导出对比?')) { if (confirm('导出为 TXT 文件?\n(确定 = TXT,取消 = 复制到剪贴板)')) { exportCompareToFile(renameCompareList); } else { copyCompareToClipboard(renameCompareList); } } } }); }; function exportCompareToFile(list) { const text = list.map(item => `${item.original}\t${item.new}`).join('\n'); const header = '【旧文件名】\t【新文件名】\n'; downloadTxt('Rename_Compare.txt', header + text); } function copyCompareToClipboard(list) { const text = list.map(item => `${item.original}\t${item.new}`).join('\n'); const header = '【旧文件名】\t【新文件名】\n'; copyToClipboard(header + text); } // ========== 备份文件名(双确认) ========== function backupFileNames() { const $items = $("iframe[rel='wangpan']").contents().find("li.selected"); if ($items.length === 0) { showPageNotification("请先选中要备份的文件", 'info', 3000); return; } const names = []; $items.each(function () { const title = $(this).attr("title"); if (title) names.push(title); }); if (names.length === 0) return; const text = names.join('\n'); if (confirm('导出为 TXT 文件?\n(确定 = TXT,取消 = 复制到剪贴板)')) { downloadTxt('115_File_Backup.txt', text); } else { copyToClipboard(text); } } function downloadTxt(filename, text) { const blob = new Blob([text], { type: 'text/plain' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); showPageNotification('TXT 文件已下载', 'success', 3000); } function copyToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => showPageNotification('已复制到剪贴板', 'success', 3000)) .catch(() => { GM_setClipboard(text); showPageNotification('已复制到剪贴板', 'success', 3000); }); } else { GM_setClipboard(text); showPageNotification('已复制到剪贴板', 'success', 3000); } } // ========== 归档功能 ========== const getSeriesFromCode = code => { const c = (typeof code === 'object' ? code.queryCode : String(code)).toUpperCase(); if (/^FC2-PPV/.test(c) || /^\d{6}_\d{3}$/.test(c) || /^1PONDO[-_]/.test(c) || /^CARIB[-_]/.test(c)) return null; const m = c.match(/^([A-Z]+)-\d+/); return m ? m[1] : null; }; const findOrCreateFolderAndMove = (fid, folderName, successCallback, failCallback) => { const cid = archiveRootCid || ROOT_DIR_CID; const cleanName = folderName.replace(/[\\/:*?"<>|]/g, ' '); if (folderCidCache[cleanName]) { moveFileToFolder(fid, folderCidCache[cleanName], cleanName, successCallback, failCallback); return; } $.get("https://webapi.115.com/files/search", { search_value: cleanName, format: "json", aid: "1", cid: cid, file_type: "0", limit: 1000 }, data => { const result = typeof data === 'string' ? JSON.parse(data) : data; if (result.state && result.data && result.data.count > 0) { const found = result.data.list.find(item => item.name === cleanName && item.file_type === "0"); if (found) { folderCidCache[cleanName] = found.cid; moveFileToFolder(fid, found.cid, cleanName, successCallback, failCallback); return; } } $.post("https://webapi.115.com/files/add", { pid: cid, cname: cleanName }, createData => { const createResult = typeof createData === 'string' ? JSON.parse(createData) : createData; if (createResult.state) { folderCidCache[cleanName] = createResult.cid; moveFileToFolder(fid, createResult.cid, cleanName, successCallback, failCallback); } else { if (createResult.errno === 20004) { $.get("https://webapi.115.com/files/search", { search_value: cleanName, format: "json", aid: "1", cid: cid, file_type: "0", limit: 1000 }, data2 => { const res2 = JSON.parse(data2); const found2 = res2.data && res2.data.list.find(item => item.name === cleanName && item.file_type === "0"); if (found2) { folderCidCache[cleanName] = found2.cid; moveFileToFolder(fid, found2.cid, cleanName, successCallback, failCallback); } else { showPageNotification(`创建文件夹失败,且未找到同名文件夹`, 'error', 3000); if (typeof failCallback === 'function') failCallback('重名冲突'); } }); } else { showPageNotification(`创建文件夹失败: ${createResult.error || '未知错误'}`, 'error', 3000); if (typeof failCallback === 'function') failCallback(createResult.error); } } }).fail(() => { showPageNotification('创建文件夹请求失败', 'error', 3000); if (typeof failCallback === 'function') failCallback('网络错误'); }); }).fail(() => { showPageNotification('搜索文件夹请求失败', 'error', 3000); if (typeof failCallback === 'function') failCallback('网络错误'); }); }; const moveFileToFolder = (fid, targetCid, folderName, successCallback, failCallback) => { $.post("https://webapi.115.com/files/move", { pid: targetCid, fid: fid }, data => { const result = typeof data === 'string' ? JSON.parse(data) : data; if (result.state) { showPageNotification(`已归档到 ${folderName}`, 'success', 2000); if (typeof successCallback === 'function') successCallback(); } else { const errorMsg = result.error || '未知错误'; if (errorMsg.includes('尚未完成') || errorMsg.includes('请稍后再试')) { showPageNotification(`归档到 ${folderName} 暂时失败,请稍后重试`, 'error', 5000); } else { showPageNotification(`归档到 ${folderName} 失败: ${errorMsg}`, 'error', 5000); } if (typeof failCallback === 'function') failCallback(errorMsg); } }).fail(err => { showPageNotification(`移动文件请求失败: ${err.statusText || '网络错误'}`, 'error', 5000); if (typeof failCallback === 'function') failCallback(err.statusText); }); }; const requestActressForArchive = (fid, code, seriesName, archiveMode, doneCallback) => { const key = code.toUpperCase(); if (actressCache[key] && actressCache[key].length) { const folderName = (archiveMode === "2" && seriesName) ? `${actressCache[key][0]} - ${seriesName}` : actressCache[key][0]; findOrCreateFolderAndMove(fid, folderName, doneCallback, err => doneCallback()); return; } GM_xmlhttpRequest({ method: "GET", url: javbusDirectAccess + code, onload: xhr => { const $r = $(xhr.responseText); const actresses = []; $r.find("span.genre a[href*='/star/']").each(function () { const n = $(this).text().trim(); if (n) actresses.push(n); }); if (actresses.length) { actressCache[key] = actresses; const folderName = (archiveMode === "2" && seriesName) ? `${actresses[0]} - ${seriesName}` : actresses[0]; findOrCreateFolderAndMove(fid, folderName, doneCallback, err => doneCallback()); } else { GM_xmlhttpRequest({ method: "GET", url: javbusUncensoredBase + code, onload: xhr2 => { const $r2 = $(xhr2.responseText); const actresses2 = []; $r2.find("span.genre a[href*='/star/']").each(function () { const n = $(this).text().trim(); if (n) actresses2.push(n); }); if (actresses2.length) { actressCache[key] = actresses2; const folderName = (archiveMode === "2" && seriesName) ? `${actresses2[0]} - ${seriesName}` : actresses2[0]; findOrCreateFolderAndMove(fid, folderName, doneCallback, err => doneCallback()); } else { showPageNotification(`未找到 ${code} 的演员信息`, 'error', 3000); doneCallback(); } }, onerror: () => { showPageNotification(`查询演员失败`, 'error', 3000); doneCallback(); } }); } }, onerror: () => { showPageNotification(`查询演员失败`, 'error', 3000); doneCallback(); } }); }; const archiveToActorFolder = () => { const $items = $("iframe[rel='wangpan']").contents().find("li.selected"); const cnt = $items.length; if (!cnt) { showPageNotification("请先选择文件或文件夹", 'info', 3000); return; } if (!archiveRootCid) { showPageNotification("请先设置归档根目录(右键文件夹 → 设为归档根目录)", 'error', 5000); return; } const mode = prompt("选择归档方式:\n1 - 按女优\n2 - 按番号系列\n3 - 按女优+系列"); if (!mode || !['1', '2', '3'].includes(mode)) { showPageNotification("无效选择", 'error', 3000); return; } progressBox.init('归档', cnt); showPageNotification(`开始归档 ${cnt} 个项目...`, 'info', 3000); let processed = 0, success = 0; const tasks = []; $items.each(function () { const $it = $(this); const fn = $it.attr("title"); const ft = $it.attr("file_type"); let fid = (ft === "0") ? $it.attr("cate_id") : $it.attr("file_id"); if (!fid || !fn) return; const vi = parseVideoInfo(fn); if (!vi) { processed++; progressBox.update(processed); return; } const series = getSeriesFromCode(vi); if ((mode === "2" || mode === "3") && !series) { showPageNotification(`无法识别 ${vi.queryCode} 的系列,跳过`, 'error', 2500); processed++; progressBox.update(processed); return; } tasks.push(done => { if (mode === "1") { requestActressForArchive(fid, vi.queryCode, null, "1", () => { processed++; success++; progressBox.update(processed); done(); }); } else if (mode === "2") { findOrCreateFolderAndMove(fid, series, () => { processed++; success++; progressBox.update(processed); done(); }, () => { processed++; progressBox.update(processed); done(); }); } else if (mode === "3") { requestActressForArchive(fid, vi.queryCode, series, "2", () => { processed++; success++; progressBox.update(processed); done(); }); } else { processed++; progressBox.update(processed); done(); } }); }); runTasksWithLimit(tasks, 3, () => { progressBox.finish(); showPageNotification(`归档完成:成功 ${success}/${cnt}`, 'success', 5000); }); }; // ========== JavDB 评分 ========== const getJavdbRating = () => { const $items = $("iframe[rel='wangpan']").contents().find("li.selected"); const cnt = $items.length; if (!cnt) { showPageNotification("请先选择文件或文件夹", 'info', 3000); return; } progressBox.init('获取评分', cnt); showPageNotification(`开始获取 ${cnt} 个项目的评分...`, 'info', 3000); let processed = 0, success = 0; const tasks = []; $items.each(function () { const $it = $(this); const fn = $it.attr("title"); const ft = $it.attr("file_type"); let fid = (ft === "0") ? $it.attr("cate_id") : $it.attr("file_id"); if (!fid || !fn) return; const vi = parseVideoInfo(fn); if (!vi || !vi.queryCode) return; tasks.push(done => { requestJavdbRating(fid, vi.queryCode, fn, ok => { processed++; if (ok) success++; progressBox.update(processed); done(); }); }); }); runTasksWithLimit(tasks, 2, () => { progressBox.finish(); showPageNotification(`评分获取完成:成功 ${success}/${cnt}`, 'success', 5000); }); }; const requestJavdbRating = (fid, fh, fname, callback) => { GM_xmlhttpRequest({ method: "GET", url: `${javdbSearchBase}${encodeURIComponent(fh)}&f=all`, timeout: 10000, onload: xhr => { if (xhr.status !== 200) { callback(false); return; } try { const doc = new DOMParser().parseFromString(xhr.responseText, "text/html"); const item = doc.querySelector('.movie-list .item'); if (item) { let rating = parseFloat(item.getAttribute('score')); if (isNaN(rating)) { const rel = item.querySelector('.score .value'); if (rel) { const m = rel.textContent.trim().match(/(\d+\.\d+)分/); if (m) rating = parseFloat(m[1]); } } if (!isNaN(rating)) { update115Rating(fid, Math.round(rating), fh, fname, callback); return; } const link = item.querySelector('a.box'); if (link) { const href = link.getAttribute('href'); if (href) { const detailUrl = javdbBase + (href.startsWith('/') ? href : '/' + href); GM_xmlhttpRequest({ method: "GET", url: detailUrl, timeout: 10000, onload: dx => { try { const dd = new DOMParser().parseFromString(dx.responseText, "text/html"); const rEl = dd.querySelector('.panel-block .value'); if (rEl) { const rating = parseFloat(rEl.textContent.trim().match(/(\d+\.\d+)/)?.[1]); if (!isNaN(rating)) { update115Rating(fid, Math.round(rating), fh, fname, callback); return; } } callback(false); } catch (e) { callback(false); } }, onerror: () => callback(false), ontimeout: () => callback(false) }); return; } } } callback(false); } catch (e) { callback(false); } }, onerror: () => callback(false), ontimeout: () => callback(false) }); }; const update115Rating = (fid, star, fh, fname, callback) => { star = Math.max(1, Math.min(5, star)); const finish = (ok) => { showPageNotification(`"${fh}"评分${ok ? `更新为 ${star} 星` : '更新失败'}`, ok ? 'success' : 'error', 2000); callback(ok); }; $.ajax({ url: "https://webapi.115.com/files/score", type: "POST", data: { file_id: fid, score: star }, dataType: "json", success: r => { if (r && r.state) finish(true); else backupScore(); }, error: backupScore }); function backupScore() { $.ajax({ url: "https://webapi.115.com/files/edit_property", type: "POST", data: { file_id: fid, property: "score", value: star }, dataType: "json", success: r => finish(r && r.state), error: () => finish(false) }); } }; // ========== 菜单绑定 ========== function buttonInterval() { const $menu = $("div#js_float_content"); if ($menu.length === 0) return; const openDir = $menu.find("li[val='open_dir'], li[data-val='open_dir'], li[menu='open_dir']"); if (openDir.length !== 0 && $("li#rename_list").length === 0) { openDir.before(rename_list); $("a#local_code_process").off("click").on("click", () => rename(local_rename, false)); $("a#rename_all_multi_date").off("click").on("click", () => rename(rename_multi, true)); $("a#archive_to_folder").off("click").on("click", archiveToActorFolder); $("a#set_archive_root").off("click").on("click", setArchiveRoot); $("a#get_javdb_rating").off("click").on("click", getJavdbRating); $("a#backup_file_names").off("click").on("click", backupFileNames); clearInterval(interval); } } function setArchiveRoot() { const sf = $("iframe[rel='wangpan']").contents().find("li.selected"); if (sf.length !== 1) { showPageNotification("请只选择一个文件夹", 'error', 3000); return; } const $it = $(sf[0]); if ($it.attr("file_type") !== "0") { showPageNotification("请选择文件夹类型", 'error', 3000); return; } const cid = $it.attr("cate_id"), name = $it.attr("title"); if (cid) { GM_setValue("archiveRootCid", cid); GM_setValue("archiveRootName", name); archiveRootCid = cid; archiveRootName = name; cleanupExistingRootInfo(); showArchiveRootInfo(); showPageNotification(`归档根目录设置成功: "${name}"`, 'success', 5000); } } })();