115 Rename for CN

免VPN 番号重命名 free version(Query and modify filenames based on existing filename "番号”, includes detailed notification feature)

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name                115 Rename for CN
// @namespace           http://tampermonkey.net/
// @version             2.1
// @description         免VPN 番号重命名 free version(Query and modify filenames based on existing filename "番号”, includes detailed notification feature)
// @author              no_one
// @include             https://115.com/*
// @grant               GM_xmlhttpRequest
// @grant               GM_notification
// @license             MIT
// ==/UserScript==

(function () {
    'use strict';

    // HTML for the rename buttons, only keeping javhoo related buttons
    const renameListHTML = `
        <li id="rename_list">
            <a id="rename_all_javhoo" class="mark" href="javascript:;">Rename</a>
            <a id="rename_all_javhoo_date" class="mark" href="javascript:;">Rename with Date</a>
        </li>
    `;

    // Base URLs for javhoo
    const JAVHOO_BASE = "https://www.javhoo.top/";
    const JAVHOO_SEARCH = `${JAVHOO_BASE}av/`;
    const JAVHOO_UNCENSORED_SEARCH = `${JAVHOO_BASE}uncensored/av/`;

    // Timer ID
    let intervalId = null;

    // Constants
    const MAX_RETRIES = 3;
    const MAX_CONCURRENT_REQUESTS = 5;
    const NOTIFICATION_DISPLAY_TIME = 4000; // 4 seconds

    // State variables
    let activeRequests = 0;
    const requestQueue = [];

    const progressData = {
        total: 0,
        processed: 0,
        success: 0,
        failed: 0
    };

    const notificationQueue = [];
    let isNotificationShowing = false;

    /**
     * Initialize the script
     */
    function init() {
        intervalId = setInterval(addRenameButtons, 1000);
        injectProgressBar();
        requestNotificationPermission();
    }

    /**
     * Request notification permission and test
     */
    function requestNotificationPermission() {
        if (Notification.permission === "default") {
            Notification.requestPermission().then(permission => {
                if (permission === "granted") {
                    new Notification("115Rename", { body: "Notification permission granted." });
                } else {
                    console.log("Notification permission denied.");
                }
            });
        } else if (Notification.permission === "granted") {
            new Notification("115Rename", { body: "Script loaded." });
        }
    }

    /**
     * Periodically check and add rename buttons
     */
    function addRenameButtons() {
        const openDir = $("div#js_float_content li[val='open_dir']");
        if (openDir.length !== 0 && $("li#rename_list").length === 0) {
            openDir.before(renameListHTML);
            bindButtonEvents();
            console.log("Rename buttons added");
            clearInterval(intervalId);
        }
    }

    /**
     * Bind click events to buttons
     */
    function bindButtonEvents() {
        $("#rename_all_javhoo").on("click", () => {
            rename(rename_javhoo, false);
        });
        $("#rename_all_javhoo_date").on("click", () => {
            rename(rename_javhoo, true);
        });
    }

    /**
     * Execute rename operation
     * @param {Function} call Callback function
     * @param {Boolean} addDate Whether to add date
     */
    function rename(call, addDate) {
        const selectedItems = $("iframe[rel='wangpan']")
            .contents()
            .find("li.selected");

        if (selectedItems.length === 0) {
            enqueueNotification("Please select files to rename.", "error");
            return;
        }

        // Reset progress data
        progressData.total = selectedItems.length;
        progressData.processed = 0;
        progressData.success = 0;
        progressData.failed = 0;
        updateProgressBar();

        // Show start notification
        enqueueNotification(`Starting to rename ${progressData.total} files...`, "info");

        selectedItems.each(function () {
            const $item = $(this);
            const fileName = $item.attr("title");
            const fileType = $item.attr("file_type");
            let fid, suffix;

            if (fileType === "0") {
                fid = $item.attr("cate_id");
            } else {
                fid = $item.attr("file_id");
                const lastDot = fileName.lastIndexOf('.');
                if (lastDot !== -1) {
                    suffix = fileName.substring(lastDot);
                }
            }

            if (fid && fileName) {
                const fh = getVideoCode(fileName);
                if (fh) {
                    const chineseCaptions = checkChineseCaptions(fh, fileName);
                    enqueueRequest(() => call(fid, fh, suffix, chineseCaptions, addDate, $item));
                } else {
                    progressData.processed++;
                    progressData.failed++;
                    updateProgressBar();
                    enqueueNotification(`${fileName}: No code extracted`, "error");
                }
            }
        });

        processQueue();
    }

    /**
     * Add request to queue and process
     * @param {Function} requestFn Request function
     */
    function enqueueRequest(requestFn) {
        requestQueue.push(requestFn);
        console.log(`Request added to queue. Current queue length: ${requestQueue.length}`);
    }

    /**
     * Process the request queue with concurrency limit
     */
    function processQueue() {
        while (activeRequests < MAX_CONCURRENT_REQUESTS && requestQueue.length > 0) {
            const requestFn = requestQueue.shift();
            activeRequests++;
            console.log(`Processing request from queue. Active requests: ${activeRequests}`);
            requestFn().finally(() => {
                activeRequests--;
                console.log(`Request completed. Active requests: ${activeRequests}`);
                processQueue();
            });
        }
    }

    /**
     * Query javhoo and rename
     * @param {String} fid File ID
     * @param {String} fh Code
     * @param {String} suffix File suffix
     * @param {Boolean} chineseCaptions Whether it has Chinese captions
     * @param {Boolean} addDate Whether to add date
     * @param {jQuery} $item jQuery object of the file
     * @returns {Promise}
     */
    function rename_javhoo(fid, fh, suffix, chineseCaptions, addDate, $item) {
        const url = JAVHOO_SEARCH;
        return requestJavhoo(fid, fh, suffix, chineseCaptions, addDate, url, 0, $item);
    }

    /**
     * Request javhoo and handle rename
     * @param {String} fid File ID
     * @param {String} fh Code
     * @param {String} suffix File suffix
     * @param {Boolean} chineseCaptions Whether it has Chinese captions
     * @param {Boolean} addDate Whether to add date
     * @param {String} url Request URL
     * @param {Number} retryCount Current retry count
     * @param {jQuery} $item jQuery object of the file
     * @returns {Promise}
     */
    function requestJavhoo(fid, fh, suffix, chineseCaptions, addDate, url, retryCount, $item) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `${url}${fh}`,
                headers: {
                    "User-Agent": navigator.userAgent,
                    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
                },
                onload: (xhr) => {
                    if (xhr.status !== 200) {
                        console.warn(`Request failed, status code: ${xhr.status}`);
                        handleRetryOrFail(`HTTP ${xhr.status}`, fid, fh, suffix, chineseCaptions, addDate, url, retryCount, $item, resolve, reject);
                        return;
                    }

                    const parser = new DOMParser();
                    const doc = parser.parseFromString(xhr.responseText, "text/html");

                    const titleElement = doc.querySelector("header.article-header h1.article-title");
                    const title = titleElement ? titleElement.textContent.trim() : null;

                    console.log(`Extracted title: "${title}"`);

                    if (title) {
                        let newName = buildNewName(fh, suffix, chineseCaptions, title);

                        if (addDate) {
                            const currentDate = new Date().toISOString().split('T')[0];
                            newName = `${currentDate}_${newName}`;
                        }

                        if (newName) {
                            send_115(fid, newName, fh)
                                .then(() => {
                                    progressData.processed++;
                                    progressData.success++;
                                    updateProgressBar();
                                    enqueueNotification(`${fh}: Rename successful`, "success");
                                    updateDOM($item, newName);
                                    resolve();
                                })
                                .catch((error) => {
                                    progressData.processed++;
                                    progressData.failed++;
                                    updateProgressBar();
                                    enqueueNotification(`${fh}: Rename failed - ${error}`, "error");
                                    reject(error);
                                });
                        } else {
                            progressData.processed++;
                            progressData.failed++;
                            updateProgressBar();
                            enqueueNotification(`${fh}: Failed to generate new name`, "error");
                            reject("Failed to generate new name");
                        }
                    } else if (url !== JAVHOO_UNCENSORED_SEARCH && retryCount < MAX_RETRIES) {
                        console.warn(`Attempting uncensored search: ${JAVHOO_UNCENSORED_SEARCH}${fh}`);
                        requestJavhoo(fid, fh, suffix, chineseCaptions, addDate, JAVHOO_UNCENSORED_SEARCH, retryCount + 1, $item)
                            .then(resolve)
                            .catch(reject);
                    } else {
                        progressData.processed++;
                        progressData.failed++;
                        updateProgressBar();
                        enqueueNotification(`${fh}: Title not found or error occurred`, "error");
                        reject("Title not found or error occurred");
                    }
                },
                onerror: () => {
                    console.warn(`Request failed: ${url}${fh}`);
                    handleRetryOrFail("Network error", fid, fh, suffix, chineseCaptions, addDate, url, retryCount, $item, resolve, reject);
                }
            });
        });
    }

    /**
     * Handle retry or fail
     * @param {String} errorMsg Error message
     * @param {String} fid File ID
     * @param {String} fh Code
     * @param {String} suffix File suffix
     * @param {Boolean} chineseCaptions Whether it has Chinese captions
     * @param {Boolean} addDate Whether to add date
     * @param {String} url Request URL
     * @param {Number} retryCount Current retry count
     * @param {jQuery} $item jQuery object of the file
     * @param {Function} resolve Promise resolve function
     * @param {Function} reject Promise reject function
     */
    function handleRetryOrFail(errorMsg, fid, fh, suffix, chineseCaptions, addDate, url, retryCount, $item, resolve, reject) {
        if (retryCount < MAX_RETRIES) {
            console.warn(`Request failed (${errorMsg}), retrying (${retryCount + 1}/${MAX_RETRIES}): ${url}${fh}`);
            const newUrl = url === JAVHOO_SEARCH ? JAVHOO_UNCENSORED_SEARCH : url;
            requestJavhoo(fid, fh, suffix, chineseCaptions, addDate, newUrl, retryCount + 1, $item)
                .then(resolve)
                .catch(reject);
        } else {
            progressData.processed++;
            progressData.failed++;
            updateProgressBar();
            enqueueNotification(`${fh}: ${errorMsg}`, "error");
            reject(errorMsg);
        }
    }

    /**
     * Build new file name
     * @param {String} fh Code
     * @param {String} suffix File suffix
     * @param {Boolean} chineseCaptions Whether it has Chinese captions
     * @param {String} title Title
     * @returns {String} New name
     */
    function buildNewName(fh, suffix, chineseCaptions, title) {
        let newName = '';

        if (title.startsWith(fh)) {
            newName = title;
        } else {
            newName = `${fh}`;
            if (chineseCaptions) {
                newName += "【中文字幕】";
            }
            newName += ` ${title}`;
        }

        if (suffix) {
            newName += suffix;
        }

        return newName;
    }

    /**
     * Send rename request to 115.com
     * @param {String} fid File ID
     * @param {String} name New file name
     * @param {String} fh Code
     * @returns {Promise} Request promise
     */
    function send_115(fid, name, fh) {
        return new Promise((resolve, reject) => {
            const standardizedName = stringStandard(name);
            $.post("https://webapi.115.com/files/edit", {
                fid: fid,
                file_name: standardizedName
            })
            .done((data) => {
                console.log("send_115 response data:", data);
                try {
                    const result = typeof data === "string" ? JSON.parse(data) : data;
                    if (result.state) {
                        resolve();
                    } else {
                        const errorMsg = unescape(result.error.replace(/\\(u[0-9a-fA-F]{4})/gm, '%$1'));
                        reject(errorMsg);
                    }
                } catch (e) {
                    console.error("Failed to parse response:", e);
                    reject("Failed to parse response");
                }
            })
            .fail(() => {
                reject("Network error");
            });
        });
    }

    /**
     * Add notification to queue and process
     * @param {String} message Notification message
     * @param {String} type Notification type ('success', 'error', 'info')
     */
    function enqueueNotification(message, type = "info") {
        notificationQueue.push({ message, type });
        console.log(`Notification added to queue: ${message} Type: ${type}`);
        processNotificationQueue();
    }

    /**
     * Process notification queue to ensure one notification at a time
     */
    function processNotificationQueue() {
        if (isNotificationShowing || notificationQueue.length === 0) {
            return;
        }

        isNotificationShowing = true;
        const { message, type } = notificationQueue.shift();
        console.log(`Displaying notification: ${message} Type: ${type}`);

        // Use browser's Notification API
        if (Notification.permission === "granted") {
            let notification = new Notification("115Rename", { body: message });

            notification.onclose = () => {
                isNotificationShowing = false;
                processNotificationQueue();
            };
        } else if (Notification.permission !== "denied") {
            Notification.requestPermission().then(permission => {
                if (permission === "granted") {
                    let notification = new Notification("115Rename", { body: message });

                    notification.onclose = () => {
                        isNotificationShowing = false;
                        processNotificationQueue();
                    };
                } else {
                    // Fallback to alert if permission denied
                    alert(message);
                    isNotificationShowing = false;
                    processNotificationQueue();
                }
            });
        } else {
            // Fallback to alert if permission denied
            alert(message);
            isNotificationShowing = false;
            processNotificationQueue();
        }
    }

    /**
     * Standardize file name by removing or replacing invalid characters
     * @param {String} name Original file name
     * @returns {String} Standardized file name
     */
    function stringStandard(name) {
        return name.replace(/\\/g, "")
                   .replace(/\//g, " ")
                   .replace(/:/g, " ")
                   .replace(/\?/g, " ")
                   .replace(/"/g, " ")
                   .replace(/</g, " ")
                   .replace(/>/g, " ")
                   .replace(/\|/g, "")
                   .replace(/\*/g, " ");
    }

    /**
     * Check if file name contains Chinese captions
     * @param {String} fh Code
     * @param {String} title File name
     * @returns {Boolean} Whether it contains Chinese captions
     */
    function checkChineseCaptions(fh, title) {
        if (title.includes("中文字幕")) {
            return true;
        }
        const regex = new RegExp(`${fh}-C`, 'i');
        return regex.test(title);
    }

    /**
     * Extract code from file name
     * @param {String} title File name
     * @returns {String|null} Extracted code or null
     */
    function getVideoCode(title) {
        title = title.toUpperCase()
                     .replace("SIS001", "")
                     .replace("1080P", "")
                     .replace("720P", "")
                     .trim();

        const patterns = [
            /FC2-PPV-\d+/,
            /1PONDO-\d{6}-\d{2,4}/,
            /HEYZO-?\d{4}/,
            /CARIB-\d{6}-\d{3}/,
            /N-\d{4}/,
            /JUKUJO-\d{4}/,
            /[A-Z]{2,5}-\d{3,5}/,
            /\d{6}-\d{2,4}/,
            /[A-Z]+\d{3,5}/,
            /[A-Za-z]+-?\d+/,
            /\d+-?\d+/
        ];

        for (let pattern of patterns) {
            let match = title.match(pattern);
            if (match) {
                let code = match[0];
                console.log(`Found code: ${code}`);
                return code;
            }
        }

        console.warn("Code not found:", title);
        return null; // Return null if not found
    }

    /**
     * Inject progress bar into the page
     */
    function injectProgressBar() {
        const progressBarContainer = document.createElement('div');
        progressBarContainer.id = 'renameProgressBarContainer';
        Object.assign(progressBarContainer.style, {
            position: 'fixed',
            top: '100px',
            right: '10px',
            width: '320px',
            padding: '10px',
            backgroundColor: 'rgba(0, 0, 0, 0.8)',
            color: '#fff',
            borderRadius: '5px',
            zIndex: '9999',
            display: 'none'
        });

        const title = document.createElement('div');
        title.innerText = '115Rename Progress';
        Object.assign(title.style, {
            marginBottom: '5px',
            fontWeight: 'bold'
        });

        const progress = document.createElement('div');
        progress.id = 'renameProgressBar';
        Object.assign(progress.style, {
            width: '100%',
            backgroundColor: '#555',
            borderRadius: '3px',
            overflow: 'hidden',
            position: 'relative'
        });

        const progressFill = document.createElement('div');
        progressFill.id = 'renameProgressFill';
        Object.assign(progressFill.style, {
            width: '0%',
            height: '10px',
            backgroundColor: '#4caf50'
        });

        const progressText = document.createElement('div');
        progressText.id = 'renameProgressText';
        Object.assign(progressText.style, {
            position: 'absolute',
            top: '0',
            left: '50%',
            transform: 'translateX(-50%)',
            fontSize: '12px',
            lineHeight: '10px',
            color: '#fff',
            pointerEvents: 'none'
        });

        progress.appendChild(progressFill);
        progress.appendChild(progressText);
        progressBarContainer.appendChild(title);
        progressBarContainer.appendChild(progress);

        document.body.appendChild(progressBarContainer);
    }

    /**
     * Update progress bar
     */
    function updateProgressBar() {
        const container = $('#renameProgressBarContainer');
        const fill = $('#renameProgressFill');
        const text = $('#renameProgressText');

        if (progressData.processed === 0 && requestQueue.length === 0 && activeRequests === 0) {
            container.hide();
            return;
        }

        container.show();

        const percent = ((progressData.processed / progressData.total) * 100).toFixed(2);
        fill.css('width', `${percent}%`);
        text.text(`${progressData.processed} / ${progressData.total}`);

        if (progressData.processed >= progressData.total) {
            container.hide();
            enqueueNotification(`Rename completed: Success ${progressData.success}, Failed ${progressData.failed}.`, "info");
            console.log(`Rename completed: Success ${progressData.success}, Failed ${progressData.failed}.`);
        }
    }

    /**
     * Update file name displayed on the page
     * @param {jQuery} $item jQuery object of the file
     * @param {String} newName New file name
     */
    function updateDOM($item, newName) {
        $item.attr("title", newName);

        const fileNameElement = $item.find(".file_name");
        if (fileNameElement.length > 0) {
            fileNameElement.text(newName);
        } else {
            $item.text(newName);
        }
    }

    // Initialize the script
    init();

})();