// ==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();
})();