成人交流 · text · 0 分 · 评论 0 · 阅读 1

分享一个自写115番号重命名油猴脚本

大家好,种草个自己拿 AI 写的省事小脚本,适合整理番号视频文件。用法超简单,举个例子:
步骤示例:在 115 重命名弹窗里输入番号 WAAA-580 → 点“加载” → 脚本会自动从抓信息并翻译标题 → 自动填好文件名 → 点击“确定”完成。
主要功能(简单版):自动抓取番号/演员/标题。自动把标题翻译并拼成格式化文件名(默认格式为:番号+演员+翻译结果)适配旧版本和新版本115 页面如果有自己的命名规则可以丢给 AI 修改,非常方便

注意:需要自己自行替换腾讯翻译的 SECRET_ID/SECRET_KEY// ==UserScript==
// [url=home.php?mod=space&uid=264574]@name[/url]         115网盘重命名辅助
// @namespace    http://tampermonkey.net/
// [url=home.php?mod=space&uid=361048]@Version[/url]      4.4
// @description  兼容旧版 .file-rename 与新版 data-dialog="true" 弹窗;优化新版 modal 注入位置与布局,避免遮挡标题/输入区。保留 Javbus 爬取与腾讯云翻译功能。
// @author       You
// @match        https://*.115.com/*
// @grant        GM_xmlhttpRequest
// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// @connect      tmt.tencentcloudapi.com
// @connect      javbus.com
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';
    try {
        console.info('[gm-115-rename] start v4.4');

        /********** 配置区:请注意不要在公共场合泄露 SECRET_ID/SECRET_KEY **********/
        const TENCENT_TRANSLATE_API = "https://tmt.tencentcloudapi.com";
        const SECRET_ID = "AK****t3E";
        const SECRET_KEY = "4****z";
        /*****************************************************************************/

        const INJECT_ATTR = 'data-gm-115-rename-helper';

        /***** 强健写入函数(支持受控 React/Vue 输入) *****/
        const setValueAndNotify = (el, value) => {
            if (!el) return;
            try {
                const proto = Object.getPrototypeOf(el);
                const desc = Object.getOwnPropertyDescriptor(proto, 'value')
                    || Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')
                    || Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value');

                if (desc && desc.set) {
                    desc.set.call(el, value);
                } else {
                    el.value = value;
                }

                try { el.setAttribute('value', value); } catch (e) { /* ignore */ }

                const ie = new InputEvent('input', { bubbles: true, cancelable: true, composed: true, data: value });
                el.dispatchEvent(ie);
                el.dispatchEvent(new Event('change', { bubbles: true }));

                try { el.blur && el.blur(); } catch (e) { /* ignore */ }

                console.debug('[gm-115-rename] wrote value to', el, value);
            } catch (e) {
                console.warn('[gm-115-rename] setValueAndNotify error', e);
            }
        };

        /***** 腾讯云翻译(TC3 HMAC-SHA256 签名) *****/
        const translateText = (text, source = "auto", target = "zh") => {
            const now = new Date();
            const date = now.toISOString().slice(0, 10);
            const timestamp = Math.floor(now.getTime() / 1000);

            const requestPayload = { SourceText: text, Source: source, Target: target, ProjectId: 0 };
            const httpRequestMethod = "POST";
            const canonicalHeaders = `content-type:application/json\nhost:tmt.tencentcloudapi.com\n`;
            const signedHeaders = "content-type;host";
            const hashedRequestPayload = CryptoJS.SHA256(JSON.stringify(requestPayload)).toString();
            const canonicalRequest = `${httpRequestMethod}\n/\n\n${canonicalHeaders}\n${signedHeaders}\n${hashedRequestPayload}`;
            const hashedCanonicalRequest = CryptoJS.SHA256(canonicalRequest).toString();
            const stringToSign = `TC3-HMAC-SHA256\n${timestamp}\n${date}/tmt/tc3_request\n${hashedCanonicalRequest}`;

            const kDate = CryptoJS.HmacSHA256(date, `TC3${SECRET_KEY}`);
            const kService = CryptoJS.HmacSHA256("tmt", kDate);
            const kSigning = CryptoJS.HmacSHA256("tc3_request", kService);
            const signature = CryptoJS.HmacSHA256(stringToSign, kSigning).toString();

            const authorization = `TC3-HMAC-SHA256 Credential=${SECRET_ID}/${date}/tmt/tc3_request, SignedHeaders=${signedHeaders}, Signature=${signature}`;

            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "POST",
                    url: TENCENT_TRANSLATE_API,
                    headers: {
                        "Content-Type": "application/json",
                        "Authorization": authorization,
                        "X-TC-Action": "TextTranslate",
                        "X-TC-Timestamp": timestamp.toString(),
                        "X-TC-Version": "2018-03-21",
                        "X-TC-Region": "ap-beijing",
                    },
                    data: JSON.stringify(requestPayload),
                    onload: (response) => {
                        try {
                            const result = JSON.parse(response.responseText || '{}');
                            if (result.Response && result.Response.TargetText != null) {
                                resolve(result.Response.TargetText);
                            } else {
                                reject('翻译返回格式异常: ' + (response.responseText || 'empty'));
                            }
                        } catch (e) {
                            reject('翻译解析失败: ' + e);
                        }
                    },
                    onerror: () => reject('翻译请求失败(网络)'),
                });
            });
        };

        /***** 爬取 Javbus 页面(容错多种 DOM 结构) *****/
        const fetchJavbusContent = (fileCode) => {
            const url = `https://www.javbus.com/${encodeURIComponent(fileCode)}`;
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url,
                    headers: { "User-Agent": "Mozilla/5.0" },
                    onload: (response) => {
                        try {
                            const parser = new DOMParser();
                            const doc = parser.parseFromString(response.responseText, "text/html");

                            const codeEl = doc.querySelector("div.row.movie p:first-of-type span:nth-of-type(2)")
                                || doc.querySelector(".header .identifier")
                                || doc.querySelector("span#aka")
                                || null;
                            const titleEl = doc.querySelector("body > div.container > h3")
                                || doc.querySelector("h3.movie-title")
                                || doc.querySelector('meta[property="og:title"]')
                                || null;
                            const actorEl = doc.querySelector("div.row.movie p:last-of-type")
                                || doc.querySelector(".star-name")
                                || null;

                            const code = (codeEl && (codeEl.textContent || codeEl.getAttribute && codeEl.getAttribute('content')) && (codeEl.textContent || codeEl.getAttribute('content')).trim()) || fileCode;
                            const title = (titleEl && (titleEl.textContent || titleEl.getAttribute && titleEl.getAttribute('content')) && (titleEl.textContent || titleEl.getAttribute('content')).trim()) || "标题未找到";
                            const actorName = (actorEl && actorEl.textContent && actorEl.textContent.trim()) || "演员未找到";

                            resolve({ code, title, actorName });
                        } catch (e) {
                            reject('页面解析失败: ' + e);
                        }
                    },
                    onerror: () => reject('爬取失败(网络)'),
                });
            });
        };

        /***** 旧版注入(.file-rename) *****/
        const addCustomElementsOld = (targetElement) => {
            if (!targetElement) return;
            if (targetElement.getAttribute && targetElement.getAttribute(INJECT_ATTR)) {
                console.debug('[gm-115-rename] old already injected');
                return;
            }
            try { targetElement.setAttribute(INJECT_ATTR, '1'); } catch (e) {}
            console.debug('[gm-115-rename] injecting into old target', targetElement);

            const container = document.createElement('div');
            container.style.cssText = 'display:flex;align-items:center;margin-top:10px;gap:8px;';

            const inputBox = document.createElement('input');
            inputBox.type = 'text';
            inputBox.placeholder = '输入番号(例如:MIDV-654)';
            inputBox.style.cssText = 'flex:1;height:36px;padding:0 10px;border:1px solid #dcdcdc;border-radius:4px;font-size:14px;color:#333;';

            const loadButton = document.createElement('button');
            loadButton.textContent = '加载';
            loadButton.style.cssText = 'height:36px;padding:0 14px;background:#108ee9;color:#fff;border:none;border-radius:4px;cursor:pointer;';

            loadButton.addEventListener('click', async () => {
                const fileCode = inputBox.value.trim();
                if (!fileCode) return alert('请输入有效的番号!');
                loadButton.disabled = true;
                const original = loadButton.textContent;
                loadButton.textContent = '加载中...';
                try {
                    const { code, title, actorName } = await fetchJavbusContent(fileCode);
                    const titleForTranslation = title.replace(new RegExp(code, 'gi'), '').trim();
                    const translatedTitle = await translateText(titleForTranslation);
                    const formattedName = `${code} ${actorName} ${translatedTitle}`;
                    const nameCover = targetElement.querySelector && targetElement.querySelector("p[rel='name_cover']");
                    if (nameCover) {
                        nameCover.textContent = formattedName;
                        const textArea = targetElement.querySelector && targetElement.querySelector("textarea[rel='txt']");
                        if (textArea) setValueAndNotify(textArea, formattedName);
                    } else {
                        try { navigator.clipboard && navigator.clipboard.writeText(formattedName); } catch (e) {}
                        alert('未找到旧版 name_cover,已复制到剪贴板:\n' + formattedName);
                    }
                } catch (err) {
                    alert(err);
                    console.warn('[gm-115-rename] old load error', err);
                } finally {
                    loadButton.textContent = original;
                    loadButton.disabled = false;
                }
            });

            container.appendChild(inputBox);
            container.appendChild(loadButton);

            try {
                targetElement.appendChild(container);
            } catch (e) {
                try { targetElement.insertAdjacentElement('afterend', container); } catch (e2) { console.error(e2); }
            }
        };

        /***** 新版 modal 注入(插入到 modal body,避免 header 遮挡) *****/
        const addCustomElementsForModal = (modalRoot) => {
            if (!modalRoot) return;
            if (modalRoot.getAttribute && modalRoot.getAttribute(INJECT_ATTR)) {
                console.debug('[gm-115-rename] modal already injected');
                return;
            }
            try { modalRoot.setAttribute(INJECT_ATTR, '1'); } catch (e) {}
            console.debug('[gm-115-rename] injecting into modal (body insertion)');

            // 优先定位 modal body(避免 header)
            const bodySelectors = [
                '.px-6.py-6',                      // exact body in your example
                'div.flex-1.overflow-y-auto',      // common body container
                'div[class*="overflow-y-auto"]',   // fallback
                '.modal-body',                     // generic
            ];
            let contentBlock = null;
            for (const sel of bodySelectors) {
                try {
                    const found = modalRoot.querySelector(sel);
                    if (found) { contentBlock = found; break; }
                } catch (e) { /* ignore invalid selectors */ }
            }
            // 最后回退,但尽量避免 header:选取 .px-6.py-4(header)只在无其它选择时使用
            if (!contentBlock) {
                const px6 = modalRoot.querySelector('.px-6');
                const headerCheck = px6 && px6.classList && px6.classList.contains('py-4'); // likely header
                if (px6 && !headerCheck) contentBlock = px6;
                else contentBlock = modalRoot; // 最后回退到 modalRoot
            }

            // 找到真实用于填写文件名的输入(优先匹配你提到的 class)
            const filenameInput = modalRoot.querySelector('input.flex-1.outline-none.text-sm')
                || modalRoot.querySelector('input[class*="flex-1"][class*="outline-none"][class*="text-sm"]')
                || modalRoot.querySelector('input[placeholder*="请输入文件名"]')
                || modalRoot.querySelector('input[type="text"]');

            // 构造控件
            const outer = document.createElement('div');
            outer.className = 'gm-rename-modal-row';
            outer.style.cssText = [
                'display:flex',
                'gap:12px',
                'align-items:center',
                'margin:12px 0',
                'padding:10px',
                'border-radius:8px',
                'background:linear-gradient(180deg, rgba(255,255,255,0.9), rgba(250,250,250,0.95))',
                'border:1px solid rgba(0,0,0,0.06)',
                'box-shadow:0 2px 8px rgba(15,23,42,0.04)',
                'width:100%',
                'box-sizing:border-box'
            ].join(';');

            const left = document.createElement('div');
            left.style.cssText = 'flex:1; display:flex; gap:8px; align-items:center;';

            const label = document.createElement('div');
            label.textContent = '番号:';
            label.style.cssText = 'min-width:56px; color:#111827; font-size:14px; font-weight:500;';

            const numberInput = document.createElement('input');
            numberInput.type = 'text';
            numberInput.placeholder = '例如:MIDV-654';
            numberInput.style.cssText = [
                'flex:1',
                'height:44px',
                'padding:0 12px',
                'border-radius:6px',
                'border:1px solid #e6eef8',
                'box-sizing:border-box',
                'font-size:14px',
                'outline:none',
                'background:#fff'
            ].join(';');

            const right = document.createElement('div');
            right.style.cssText = 'display:flex; gap:8px; align-items:center;';

            const helper = document.createElement('button');
            helper.type = 'button';
            helper.title = '清除';
            helper.innerHTML = '✕';
            helper.style.cssText = [
                'height:38px',
                'width:38px',
                'display:inline-flex',
                'align-items:center',
                'justify-content:center',
                'border-radius:6px',
                'border:1px solid #e6eef8',
                'background:#fff',
                'cursor:pointer',
                'color:#6b7280'
            ].join(';');

            const loadButton = document.createElement('button');
            loadButton.type = 'button';
            loadButton.textContent = '加载';
            loadButton.style.cssText = [
                'height:44px',
                'min-width:90px',
                'padding:0 16px',
                'border-radius:6px',
                'border:none',
                'cursor:pointer',
                'background:#0b7ef0',
                'color:#fff',
                'font-weight:600'
            ].join(';');

            // spinner
            const spinnerSVG = '<svg width="16" height="16" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><circle cx="25" cy="25" r="20" stroke="#fff" stroke-width="5" fill="none" stroke-linecap="round" stroke-dasharray="31.4 31.4" transform="rotate(-90 25 25)"><animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="0.9s" repeatCount="indefinite"/></circle></svg>';

            left.appendChild(label);
            left.appendChild(numberInput);
            right.appendChild(helper);
            right.appendChild(loadButton);
            outer.appendChild(left);
            outer.appendChild(right);

            // 插入:优先放在 body 的开头(afterbegin),避免插到 header 内
            try {
                if (contentBlock && contentBlock.insertAdjacentElement) {
                    contentBlock.insertAdjacentElement('afterbegin', outer);
                } else {
                    modalRoot.appendChild(outer);
                }
            } catch (e) {
                try { modalRoot.appendChild(outer); } catch (e2) { console.error(e2); }
            }

            helper.addEventListener('click', () => { numberInput.value = ''; numberInput.focus(); });

            loadButton.addEventListener('click', async () => {
                const fileCode = numberInput.value.trim();
                if (!fileCode) return alert('请输入番号(例如:MIDV-654)');
                const orig = loadButton.innerHTML;
                loadButton.innerHTML = spinnerSVG;
                loadButton.disabled = true;
                helper.disabled = true;
                try {
                    const { code, title, actorName } = await fetchJavbusContent(fileCode);
                    const titleForTranslation = title.replace(new RegExp(code, 'gi'), '').trim();
                    const translatedTitle = await translateText(titleForTranslation);
                    const formattedName = `${code} ${actorName} ${translatedTitle}`;

                    if (filenameInput) {
                        setValueAndNotify(filenameInput, formattedName);
                    } else {
                        const oldTarget = document.querySelector('.file-rename');
                        if (oldTarget) {
                            const nameCover = oldTarget.querySelector && oldTarget.querySelector("p[rel='name_cover']");
                            if (nameCover) {
                                nameCover.textContent = formattedName;
                                const ta = oldTarget.querySelector && oldTarget.querySelector("textarea[rel='txt']");
                                if (ta) setValueAndNotify(ta, formattedName);
                            }
                        } else {
                            try { navigator.clipboard && navigator.clipboard.writeText(formattedName); } catch (e) {}
                            alert('生成的重命名已复制到剪贴板:\n' + formattedName);
                        }
                    }
                } catch (err) {
                    alert(err);
                    console.warn('[gm-115-rename] modal load error', err);
                } finally {
                    loadButton.innerHTML = orig;
                    loadButton.disabled = false;
                    helper.disabled = false;
                }
            });

            numberInput.addEventListener('keydown', (ev) => {
                if (ev.key === 'Enter') {
                    ev.preventDefault();
                    loadButton.click();
                }
            });

            // 如果 modal 被移除,断开任何观察器(小优化)
            const mo = new MutationObserver(() => {
                if (!document.contains(modalRoot)) {
                    try { mo.disconnect(); } catch (e) {}
                }
            });
            mo.observe(document.body, { childList: true, subtree: true });
        };

        /***** 扫描已有目标并注入 *****/
        const scanExisting = () => {
            try {
                document.querySelectorAll && document.querySelectorAll('.file-rename').forEach(el => {
                    try { addCustomElementsOld(el); } catch (e) { console.warn(e); }
                });
                document.querySelectorAll && document.querySelectorAll('[data-dialog="true"]').forEach(el => {
                    try { addCustomElementsForModal(el); } catch (e) { console.warn(e); }
                });
                Array.from(document.querySelectorAll('div[role="dialog"], div[class*="fixed"]'))
                    .filter(el => el.querySelector && el.querySelector('input[placeholder*="请输入文件名"], input[type="text"]'))
                    .forEach(el => {
                        try { addCustomElementsForModal(el); } catch (e) { console.warn(e); }
                    });
            } catch (e) {
                console.warn('[gm-115-rename] scanExisting error', e);
            }
        };

        /***** MutationObserver 监听动态插入 *****/
        const observeDynamicElements = () => {
            const observer = new MutationObserver((mutations) => {
                for (const mutation of mutations) {
                    if (mutation.addedNodes && mutation.addedNodes.length) {
                        mutation.addedNodes.forEach(node => {
                            if (node.nodeType !== 1) return;
                            const el = node;
                            if (el.classList && el.classList.contains('file-rename')) {
                                try { addCustomElementsOld(el); } catch (e) { console.warn(e); }
                            }
                            if (el.getAttribute && el.getAttribute('data-dialog') === 'true') {
                                try { addCustomElementsForModal(el); } catch (e) { console.warn(e); }
                            }
                            try {
                                el.querySelectorAll && el.querySelectorAll('.file-rename').forEach(sub => addCustomElementsOld(sub));
                                el.querySelectorAll && el.querySelectorAll('[data-dialog="true"]').forEach(sub => addCustomElementsForModal(sub));
                            } catch (e) { /* ignore */ }
                        });
                    }
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
            console.debug('[gm-115-rename] MutationObserver started');
        };

        /***** 启动:先扫描再监听;前 10s 内短轮询提高命中率 *****/
        const initialize = () => {
            scanExisting();
            observeDynamicElements();

            let tries = 0;
            const maxTries = 20;
            const intervalId = setInterval(() => {
                tries++;
                scanExisting();
                if (tries >= maxTries) {
                    clearInterval(intervalId);
                    console.debug('[gm-115-rename] initial scanning finished');
                }
            }, 500);
        };

        if (document.readyState === 'loading') {
            window.addEventListener('DOMContentLoaded', initialize);
        } else {
            initialize();
        }

        console.info('[gm-115-rename] initialized v4.4');
    } catch (e) {
        console.error('[gm-115-rename] fatal error', e);
    }
})();複製代碼

评论 (0)

登录 后参与讨论。

还没有评论,来抢沙发吧。