115文件完整性批量检查重命名

检查115网盘中"全xx集"文件夹的完整性,用绿色圆点标注完整的,红色圆点标注不完整的。支持点击单个绿标或批量重命名为"影视名称 (年份)"格式

// ==UserScript==
// @name         115文件完整性批量检查重命名
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  检查115网盘中"全xx集"文件夹的完整性,用绿色圆点标注完整的,红色圆点标注不完整的。支持点击单个绿标或批量重命名为"影视名称 (年份)"格式
// @author       zscc
// @match        https://115.com/*
// @match        https://*.115.com/*
// @grant        none
// @updateURL
// @downloadURL
// @supportURL
// @homepageURL
// @license MIT
// ==/UserScript==

/*
 * 115网盘文件完整性检查器增强版
 *
 * 功能说明:
 * 1. 自动检测"全xx集"格式的文件夹完整性
 * 2. 用绿色圆点标注完整的文件夹,红色圆点标注不完整的
 * 3. 点击绿色圆点可一键重命名为"影视名称 (年份)"格式
 * 4. 支持批量重命名所有绿标(完整)文件
 * 5. 智能提取影视剧名称和年份信息
 * 6. 支持多种115网盘页面布局
 * 7. 智能文件数量检测,支持多种115网盘布局
 * 8. 可配置无法确定文件数量时的处理方式(显示为红色/隐藏标注)
 * 9. 严格模式:启用更多文件数量检测方法
 * 10. 优化API调用:参考115pan_aria2脚本的文件获取逻辑
 * 11. 智能文件夹ID提取:支持多种115网盘页面布局和属性
 * 12. 纯API检测:仅使用官方API获取准确的文件数量
 *
 * 重命名规则:
 * • 优先从第一个"】"和"["符号之间提取影视名称
 * • 示例:"【高清剧集网发布 www.PTHDTV.com】成家[全36集]..." → "成家 (2025)"
 * • 自动提取年份信息(2000-2039年范围)
 * • 如果没有年份,则只使用影视名称
 *
 * 使用方法:
 * 1. 安装脚本后访问115网盘
 * 2. 脚本会自动在文件夹图标上添加彩色圆点
 * 3. 绿色圆点表示文件完整,可点击进行单个重命名
 * 4. 红色圆点表示文件不完整
 * 5. 点击页面右上角的"批量重命名绿标文件"按钮进行批量操作
 * 6. 重命名前会显示提取信息供确认
 *
 * 支持的文件名格式:
 * • 【发布组】影视名称[集数信息]技术参数
 * • 影视名称.技术参数.年份.格式信息
 * • 其他包含】和[符号的格式
 *
 * 自定义配置:
 * 可以修改下方CONFIG对象中的设置来自定义脚本行为
 *
 * 新增配置选项:
 * • STRICT_FILE_COUNT_CHECK: 启用严格的文件数量检查模式
 * • SHOW_UNKNOWN_AS_INCOMPLETE: 无法确定文件数量时显示为不完整(红色)
 * • HIDE_UNKNOWN_FILES: 隐藏无法确定文件数量的文件(不显示任何标注)
 *
 * 问题修复:
 * • 修复了绿标文件误判问题:当无法获取实际文件数量时,不再错误地使用文件名中的集数
 * • 改进了115网盘文件数量检测算法,支持更多页面布局
 * • 增加了多种文件数量提取方法,提高检测准确性
 *
 * 最新优化(v2.4):
 * • 修复API调用问题:使用GM.xmlHttpRequest替代fetch,解决CORS跨域限制
 * • 参考115pan_aria2脚本的API调用方法,使用官方API获取准确的文件数量
 * • 智能文件夹ID提取:支持从元素属性、父元素、链接URL、onclick事件等多种方式获取
 * • API调用优化:使用正确的请求头、认证信息和错误处理机制
 * • 文件类型过滤:准确区分文件和文件夹,只统计实际文件数量
 * • 纯API检测:移除所有DOM解析备用方法,确保数据准确性
 * • 增强错误处理:针对权限错误、网络错误等不同情况采用不同的重试策略
 */

(function() {
    'use strict';

    // 脚本配置 - 用户可以根据需要修改这些设置
    const CONFIG = {
        // 调试模式:开启后会在控制台输出详细的调试信息
        // 如果遇到问题,建议开启此选项以便排查
        DEBUG_MODE: true,

        // 自动处理延迟(毫秒):页面加载后等待多久开始处理文件
        // 如果网络较慢,可以适当增加这个值
        PROCESS_DELAY: 3000,

        // 页面变化检测延迟(毫秒):检测到页面变化后等待多久重新处理
        // 避免频繁触发,提高性能
        MUTATION_DELAY: 1000,

        // 重命名功能开关:是否启用点击绿色圆点进行重命名的功能
        // 如果只想要完整性检查功能,可以设置为false
        ENABLE_RENAME: true,

        // 文件数量检测超时(毫秒):获取文件数量的最大等待时间
        FILE_COUNT_TIMEOUT: 5000,

        // 圆点样式配置
        DOT_SIZE: '12px',           // 圆点大小
        DOT_BORDER_RADIUS: '50%',   // 圆点圆角
        DOT_OPACITY: '0.8',         // 圆点透明度

        // 颜色配置
        COMPLETE_COLOR: '#4CAF50',   // 完整文件的圆点颜色(绿色)
        INCOMPLETE_COLOR: '#F44336', // 不完整文件的圆点颜色(红色)

        // 重命名配置
        AUTO_REFRESH_AFTER_RENAME: true,  // 重命名成功后是否自动刷新页面
        SHOW_RENAME_CONFIRMATION: true,   // 是否显示重命名确认对话框
        COPY_TO_CLIPBOARD_ON_ERROR: true, // 重命名失败时是否复制建议名称到剪贴板

        // 批量重命名配置
        ENABLE_BATCH_RENAME: true,        // 是否启用批量重命名功能
        BATCH_RENAME_DELAY: 1000,         // 批量重命名时每个文件之间的延迟(毫秒)
        BATCH_SHOW_PROGRESS: true,        // 是否显示批量重命名进度
        BATCH_CONFIRM_BEFORE_START: true, // 批量重命名前是否显示确认对话框
        BATCH_STOP_ON_ERROR: false,      // 遇到错误时是否停止批量操作

        // 文件数量检测配置
        STRICT_FILE_COUNT_CHECK: true,    // 是否启用严格的文件数量检查
        SHOW_UNKNOWN_AS_INCOMPLETE: true, // 无法确定文件数量时是否显示为不完整(红色)
        HIDE_UNKNOWN_FILES: false        // 是否隐藏无法确定文件数量的文件(不显示任何标注)
    };

    // 调试日志函数
    function debugLog(...args) {
        if (CONFIG.DEBUG_MODE) {
            console.log('[115文件检查器]', ...args);
        }
    }

    debugLog('115网盘文件完整性检查器增强版已加载');
    debugLog('配置:', CONFIG);

    // 测试提取函数(仅在调试模式下运行)
    if (CONFIG.DEBUG_MODE) {
        // 测试用户提供的示例
        const testFileName = '【高清剧集网发布 www.PTHDTV.com】成家[全36集][国语配音+中文字幕].Home.About.Us.S01.2025.2160p.WEB-DL.DDP2.0.H265-ZeroTV星标';
        const testResult = extractTitleAndYear(testFileName);
        debugLog('测试提取结果:', testResult);
        debugLog('期望结果: {title: "成家", year: "2025"}');

        // 测试其他常见格式
        const testCases = [
            '【字幕组】电影名称[1080p][中文字幕].Movie.Name.2023.1080p.BluRay.x264',
            '影视剧名称.TV.Show.S01.2024.WEB-DL.1080p.H264',
            '】测试影片[全24集][国语].Test.Movie.2022.720p.WEB-DL'
        ];

        testCases.forEach((testCase, index) => {
            const result = extractTitleAndYear(testCase);
            debugLog(`测试案例${index + 1}: "${testCase}" => `, result);
        });
    }

    // 等待页面加载完成
    function waitForElement(selector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const startTime = Date.now();
            const checkElement = () => {
                const element = document.querySelector(selector);
                if (element) {
                    resolve(element);
                } else if (Date.now() - startTime > timeout) {
                    reject(new Error(`Element ${selector} not found within ${timeout}ms`));
                } else {
                    setTimeout(checkElement, 100);
                }
            };
            checkElement();
        });
    }

    // 创建标注圆点
    function createDot(color, count, episodeNumber, fileName, fileElement) {
        const dot = document.createElement('span');
        dot.style.cssText = `
            display: inline-block;
            width: ${CONFIG.DOT_SIZE};
            height: ${CONFIG.DOT_SIZE};
            border-radius: ${CONFIG.DOT_BORDER_RADIUS};
            background-color: ${color};
            position: absolute;
            top: 50%;
            left: 0;
            transform: translateY(-50%);
            border: 2px solid white;
            box-shadow: 0 2px 6px rgba(0,0,0,0.4);
            z-index: 1000;
            cursor: pointer;
            opacity: ${CONFIG.DOT_OPACITY};
            transition: all 0.2s ease;
        `;

        // 添加悬停效果
        dot.addEventListener('mouseenter', function() {
            this.style.opacity = '1';
            this.style.transform = 'translateY(-50%) scale(1.1)';
        });

        dot.addEventListener('mouseleave', function() {
            this.style.opacity = CONFIG.DOT_OPACITY;
            this.style.transform = 'translateY(-50%) scale(1)';
        });

        // 添加文件数量提示
        if (count !== undefined && episodeNumber !== undefined) {
            const status = count >= episodeNumber ? '完整' : '不完整';
            let tooltip = `文件${status}: ${count}/${episodeNumber}集`;
            if (color === CONFIG.COMPLETE_COLOR && CONFIG.ENABLE_RENAME) {
                tooltip += '\n\n点击进行一键重命名';
            }
            dot.title = tooltip;
        } else if (count !== undefined) {
            dot.title = `文件数量: ${count}`;
        }

        // 如果是绿色圆点(完整)且启用了重命名功能,添加点击事件进行重命名
        if (color === CONFIG.COMPLETE_COLOR && CONFIG.ENABLE_RENAME) {
            dot.addEventListener('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                renameFolder(fileName, fileElement);
            });

            // 添加特殊样式表示可点击
            dot.style.cursor = 'pointer';
            dot.style.boxShadow = '0 2px 6px rgba(76, 175, 80, 0.4)';
        }

        return dot;
    }

    // 提取"全xx集"中的数字
    function extractEpisodeNumber(text) {
        const match = text.match(/全(\d+)集/);
        return match ? parseInt(match[1]) : null;
    }

    // 从文件名中提取清晰度信息
    function extractQuality(fileName) {
        debugLog('开始提取清晰度信息:', fileName);

        // 清晰度匹配模式,按优先级排序
        const qualityPatterns = [
            /\b(2160p|4K)\b/i,
            /\b(1080p)\b/i,
            /\b(720p)\b/i,
            /\b(480p)\b/i,
            /\b(UHD)\b/i,
            /\b(HDR)\b/i
        ];

        for (const pattern of qualityPatterns) {
            const match = fileName.match(pattern);
            if (match) {
                const quality = match[1].toLowerCase();
                debugLog('提取到清晰度:', quality);
                return quality === '4k' ? '2160p' : quality; // 统一4K为2160p
            }
        }

        debugLog('未找到清晰度信息');
        return null;
    }

    // 从复杂文件名中提取影视剧名称和年份
    function extractTitleAndYear(fileName) {
        debugLog('开始提取影视剧名称和年份:', fileName);

        let title = '';
        let year = null;

        // 新的提取规则:从第一个"】"和"["符号之间提取影视名称
        // 示例:"【高清剧集网发布 www.PTHDTV.com】成家[全36集][国语配音+中文字幕]..."
        // 提取:"成家"
        const titleMatch = fileName.match(/】([^\[]+)\[/);
        if (titleMatch && titleMatch[1]) {
            title = titleMatch[1].trim();
            debugLog('从】和[之间提取到标题:', title);
        } else {
            // 备用方案:如果没有找到】和[的组合,尝试其他方式
            debugLog('未找到】和[的组合,尝试备用提取方案');

            // 方案1:移除【】内容后,取第一个[前的内容
            let cleanName = fileName.replace(/【[^】]*】/g, '').trim();
            const beforeBracket = cleanName.match(/^([^\[]+)/);
            if (beforeBracket && beforeBracket[1]) {
                title = beforeBracket[1].trim();
                debugLog('备用方案1提取到标题:', title);
            } else {
                // 方案2:传统清理方式
                title = fileName
                    .replace(/【[^】]*】/g, '') // 移除【】内容
                    .replace(/\[[^\]]*\]/g, '') // 移除[]内容
                    .replace(/\([^)]*\)/g, '') // 移除()内容
                    .replace(/\{[^}]*\}/g, '') // 移除{}内容
                    .replace(/www\.[^\s]+/gi, '') // 移除网址
                    .replace(/[\u4e00-\u9fff]+网/g, '') // 移除中文网站名
                    .replace(/发布|字幕组|压制|制作/g, '')
                    .replace(/\b(1080p|720p|480p|2160p|4K|UHD|HDR|WEB-DL|BluRay|BDRip|DVDRip|HDTV|WEBRip)\b/gi, '')
                    .replace(/\b(x264|x265|H264|H265|HEVC|AVC|AAC|AC3|DTS|DD5\.1|DDP2\.0|DDP5\.1)\b/gi, '')
                    .replace(/\b(mkv|mp4|avi|rmvb|flv|wmv|mov)\b/gi, '')
                    .replace(/\b(S\d{2}|Season\s*\d+|第[一二三四五六七八九十\d]+季)\b/gi, '')
                    .replace(/\b(E\d{2}|Episode\s*\d+|第[一二三四五六七八九十\d]+集)\b/gi, '')
                    .replace(/\b(全\d+集|共\d+集|\d+集全)\b/gi, '')
                    .replace(/[\-_\.]+/g, ' ') // 替换连字符、下划线、点为空格
                    .replace(/\s+/g, ' ') // 合并多个空格
                    .trim();
                debugLog('备用方案2提取到标题:', title);
            }
        }

        // 进一步清理标题中的技术参数
        title = title
            .replace(/\b(1080p|720p|480p|2160p|4K|UHD|HDR|WEB-DL|BluRay|BDRip|DVDRip|HDTV|WEBRip)\b/gi, '')
            .replace(/\b(x264|x265|H264|H265|HEVC|AVC|AAC|AC3|DTS|DD5\.1|DDP2\.0|DDP5\.1)\b/gi, '')
            .replace(/\b(S\d{2}|Season\s*\d+)\b/gi, '')
            .replace(/[\-_\.]+/g, ' ')
            .replace(/\s+/g, ' ')
            .trim();

        // 提取年份 (四位数字,通常在2000-2030之间)
        const yearMatch = fileName.match(/(20[0-3]\d)/g);
        if (yearMatch && yearMatch.length > 0) {
            // 如果有多个年份,选择最后一个(通常是发布年份)
            year = yearMatch[yearMatch.length - 1];
        }

        // 从标题中移除年份(如果存在)
        if (year) {
            title = title.replace(new RegExp(year, 'g'), '').trim();
        }

        // 最终清理标题
        title = title
            .replace(/[\s\-_\.]+/g, ' ')
            .replace(/\s+/g, ' ')
            .trim();

        debugLog('最终提取的标题:', title, '年份:', year);

        return {
            title: title || '未知影视剧',
            year: year
        };
    }

    // 检查当前页面是否存在重复的文件名
    function checkDuplicateName(proposedName, currentElement) {
        debugLog('检查重复文件名:', proposedName);

        // 获取当前页面所有文件/文件夹元素
        const allFileElements = document.querySelectorAll('tr[nid], tr[data-id], .file-item, .list-item');

        for (const element of allFileElements) {
            // 跳过当前正在重命名的元素
            if (element === currentElement) continue;

            // 获取元素的文件名
            let existingName = '';
            if (element.title) {
                existingName = element.title.trim();
            } else {
                const nameCell = element.querySelector('td:nth-child(2), .file-name, .name');
                if (nameCell) {
                    existingName = nameCell.textContent.trim();
                }
            }

            // 比较文件名(忽略大小写)
            if (existingName.toLowerCase() === proposedName.toLowerCase()) {
                debugLog('发现重复文件名:', existingName);
                return true;
            }
        }

        debugLog('未发现重复文件名');
        return false;
    }

    // 重命名文件夹
    function renameFolder(fileName, fileElement) {
        debugLog('开始重命名文件夹:', fileName);
        debugLog('文件元素:', fileElement);

        // 提取影视剧名称和年份
        const extracted = extractTitleAndYear(fileName);

        if (!extracted.title) {
            console.log('无法提取有效的影视剧名称');
            alert('无法从文件名中提取有效的影视剧名称\n\n请确保文件名包含有效的影视剧信息');
            return;
        }

        // 验证提取的标题是否有效
        if (!extracted.title || extracted.title === '未知影视剧' || extracted.title.length < 2) {
            alert(`无法从文件名中提取有效的影视剧名称\n\n原文件名: "${fileName}"\n提取结果: "${extracted.title}"\n\n请检查文件名格式是否正确,或手动重命名。`);
            return;
        }

        // 构建基础新文件名:"影视名称 (年份)"
        let baseName;
        if (extracted.year) {
            baseName = `${extracted.title} (${extracted.year})`;
        } else {
            // 如果没有年份,只使用影视名称
            baseName = extracted.title;
        }

        let newName = baseName;

        // 检查是否存在重复名称
        const isDuplicate = checkDuplicateName(newName, fileElement.closest('tr') || fileElement);

        if (isDuplicate) {
            // 如果存在重复,尝试添加清晰度信息
            const quality = extractQuality(fileName);
            if (quality) {
                newName = `${baseName} ${quality}`;
                debugLog('检测到重复名称,添加清晰度后缀:', newName);

                // 再次检查添加清晰度后是否还有重复
                const stillDuplicate = checkDuplicateName(newName, fileElement.closest('tr') || fileElement);
                if (stillDuplicate) {
                    // 如果还是重复,添加时间戳
                    const timestamp = new Date().getTime().toString().slice(-4);
                    newName = `${baseName} ${quality} (${timestamp})`;
                    debugLog('添加清晰度后仍重复,添加时间戳:', newName);
                }
            } else {
                // 如果没有清晰度信息,直接添加时间戳
                const timestamp = new Date().getTime().toString().slice(-4);
                newName = `${baseName} (${timestamp})`;
                debugLog('未找到清晰度信息,添加时间戳:', newName);
            }
        }

        debugLog('最终的新文件名:', newName);

        // 弹出确认对话框(如果启用)
        let userConfirmed = true;
        if (CONFIG.SHOW_RENAME_CONFIRMATION) {
            let confirmMessage = `是否将文件夹重命名为:\n\n"${newName}"\n\n原文件名:\n"${fileName}"\n\n提取信息:\n• 影视名称: ${extracted.title}\n• 年份: ${extracted.year || '未检测到'}`;

            const quality = extractQuality(fileName);
            if (quality) {
                confirmMessage += `\n• 清晰度: ${quality}`;
            }

            if (isDuplicate) {
                confirmMessage += `\n\n⚠️ 检测到重复名称,已自动添加区分信息`;
            }

            confirmMessage += `\n\n点击"确定"进行重命名,点击"取消"放弃操作。`;
            userConfirmed = confirm(confirmMessage);
        }

        if (userConfirmed) {
            // 获取文件夹ID
            const folderId = getFolderId(fileElement);
            if (folderId) {
                performRename(folderId, newName, fileName);
            } else {
                console.error('无法获取文件夹ID,元素信息:', {
                    element: fileElement,
                    tagName: fileElement.tagName,
                    className: fileElement.className,
                    id: fileElement.id,
                    attributes: Array.from(fileElement.attributes).map(attr => `${attr.name}="${attr.value}"`),
                    parentElement: fileElement.parentElement,
                    closestTr: fileElement.closest('tr')
                });

                // 提供更详细的错误信息和可能的解决方案
                const errorMessage = `无法获取文件夹ID,重命名失败\n\n可能的原因:\n1. 当前页面结构与脚本不兼容\n2. 115网盘更新了页面结构\n3. 文件夹不是标准的115网盘文件夹\n\n建议:\n- 刷新页面后重试\n- 确保在115网盘的文件列表页面使用\n- 联系脚本开发者更新兼容性`;
                alert(errorMessage);

                // 尝试手动重命名提示
                if (CONFIG.COPY_TO_CLIPBOARD_ON_ERROR) {
                    const manualRename = confirm(`是否要复制建议的文件名到剪贴板?\n您可以手动重命名文件夹\n\n建议文件名:"${newName}"`);
                    if (manualRename) {
                        try {
                            navigator.clipboard.writeText(newName).then(() => {
                                alert('文件名已复制到剪贴板!\n您可以手动重命名文件夹');
                            }).catch(() => {
                                // 降级方案:显示文件名让用户手动复制
                                prompt('请复制以下文件名进行手动重命名:', newName);
                            });
                        } catch (error) {
                            // 降级方案:显示文件名让用户手动复制
                            prompt('请复制以下文件名进行手动重命名:', newName);
                        }
                    }
                }
            }
        }
    }

    // 获取文件夹ID
    function getFolderId(fileElement) {
        console.log('开始获取文件夹ID,元素:', fileElement);

        // 尝试从不同的属性中获取文件夹ID
        const row = fileElement.closest('tr');
        console.log('找到的行元素:', row);

        let folderId = null;

        if (row) {
            // 115网盘的文件夹ID通常存储在这些属性中
            const possibleAttributes = ['cate_id', 'data-id', 'nid', 'file_id', 'fid', 'id', 'data-fid', 'data-file-id'];

            for (const attr of possibleAttributes) {
                const value = row.getAttribute(attr);
                if (value) {
                    debugLog(`从行元素的${attr}属性获取到ID:`, value);
                    folderId = value;
                    break;
                }
            }

            // 如果还没找到,尝试从行内的链接或按钮中获取
            if (!folderId) {
                const links = row.querySelectorAll('a[href*="cid="], a[href*="fid="]');
                for (const link of links) {
                    const href = link.getAttribute('href');
                    const cidMatch = href.match(/cid=([^&]+)/);
                    const fidMatch = href.match(/fid=([^&]+)/);
                    if (cidMatch) {
                        folderId = cidMatch[1];
                        debugLog('从链接href中获取到cid:', folderId);
                        break;
                    } else if (fidMatch) {
                        folderId = fidMatch[1];
                        debugLog('从链接href中获取到fid:', folderId);
                        break;
                    }
                }
            }

            // 尝试从onclick事件中获取ID
            if (!folderId) {
                const clickableElements = row.querySelectorAll('[onclick]');
                for (const element of clickableElements) {
                    const onclick = element.getAttribute('onclick');
                    const idMatch = onclick.match(/['"]([a-zA-Z0-9]+)['"]/);
                    if (idMatch && idMatch[1].length > 5) { // 假设ID长度大于5
                        folderId = idMatch[1];
                        debugLog('从onclick事件中获取到ID:', folderId);
                        break;
                    }
                }
            }
        }

        // 尝试从元素本身获取
        if (!folderId) {
            const possibleAttributes = ['cate_id', 'data-id', 'nid', 'file_id', 'fid', 'id', 'data-fid', 'data-file-id'];

            for (const attr of possibleAttributes) {
                const value = fileElement.getAttribute(attr);
                if (value) {
                    debugLog(`从文件元素的${attr}属性获取到ID:`, value);
                    folderId = value;
                    break;
                }
            }
        }

        // 如果还是没找到,尝试从父元素中查找
        if (!folderId) {
            let parent = fileElement.parentElement;
            let depth = 0;
            while (parent && depth < 5) {
                const possibleAttributes = ['cate_id', 'data-id', 'nid', 'file_id', 'fid', 'id', 'data-fid', 'data-file-id'];

                for (const attr of possibleAttributes) {
                    const value = parent.getAttribute(attr);
                    if (value && value.length > 3) { // 基本的ID长度检查
                        debugLog(`从父元素的${attr}属性获取到ID:`, value);
                        folderId = value;
                        break;
                    }
                }

                if (folderId) break;
                parent = parent.parentElement;
                depth++;
            }
        }

        debugLog('最终获取到的文件夹ID:', folderId);
        return folderId;
    }

    // 批量重命名相关变量
    let batchRenameInProgress = false;
    let batchRenameResults = [];

    // 创建批量重命名按钮
    function createBatchRenameButton() {
        if (!CONFIG.ENABLE_BATCH_RENAME) {
            return;
        }

        // 检查是否有绿标文件
        const completeFiles = collectCompleteFiles();
        const existingButton = document.getElementById('batch-rename-btn');

        if (completeFiles.length === 0) {
            // 如果没有绿标文件,移除按钮(如果存在)
            if (existingButton) {
                existingButton.remove();
                debugLog('没有绿标文件,已移除批量重命名按钮');
            }
            return;
        }

        // 如果按钮已存在且有绿标文件,不重复创建
        if (existingButton) {
            return;
        }

        const button = document.createElement('button');
        button.id = 'batch-rename-btn';
        button.innerHTML = `🔄 批量重命名绿标文件 (${completeFiles.length})`;
        button.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 10000;
            padding: 10px 15px;
            background: linear-gradient(135deg, #4CAF50, #45a049);
            color: white;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
            font-weight: bold;
            box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
            transition: all 0.3s ease;
            min-width: 160px;
        `;

        // 悬停效果
        button.addEventListener('mouseenter', function() {
            this.style.transform = 'translateY(-2px)';
            this.style.boxShadow = '0 6px 16px rgba(76, 175, 80, 0.4)';
        });

        button.addEventListener('mouseleave', function() {
            this.style.transform = 'translateY(0)';
            this.style.boxShadow = '0 4px 12px rgba(76, 175, 80, 0.3)';
        });

        button.addEventListener('click', startBatchRename);

        document.body.appendChild(button);
        debugLog(`批量重命名按钮已创建,检测到 ${completeFiles.length} 个绿标文件`);
    }

    // 收集所有绿标(完整)文件
    function collectCompleteFiles() {
        const completeFiles = [];
        const greenDots = document.querySelectorAll('.file-status-dot');

        for (const dot of greenDots) {
            // 检查是否是绿色圆点(完整文件)
            const bgColor = window.getComputedStyle(dot).backgroundColor;
            const isGreen = bgColor.includes('76, 175, 80') || bgColor.includes('rgb(76, 175, 80)') ||
                           dot.style.backgroundColor === CONFIG.COMPLETE_COLOR;

            if (isGreen) {
                const fileElement = dot.closest('tr, li, .file-item, .list-item');
                if (fileElement) {
                    let fileName = '';
                    if (fileElement.title) {
                        fileName = fileElement.title;
                    } else if (fileElement.textContent) {
                        fileName = fileElement.textContent.trim();
                    } else if (fileElement.innerText) {
                        fileName = fileElement.innerText.trim();
                    }

                    if (fileName && fileName.includes('集')) {
                        completeFiles.push({
                            element: fileElement,
                            fileName: fileName,
                            dot: dot
                        });
                    }
                }
            }
        }

        debugLog(`收集到 ${completeFiles.length} 个完整文件`);
        return completeFiles;
    }

    // 开始批量重命名
    async function startBatchRename() {
        if (batchRenameInProgress) {
            alert('批量重命名正在进行中,请稍候...');
            return;
        }

        const completeFiles = collectCompleteFiles();

        if (completeFiles.length === 0) {
            alert('未找到可重命名的完整文件(绿标文件)\n\n请确保:\n1. 页面已完全加载\n2. 存在标记为完整的文件夹\n3. 文件夹名称包含"集"字符');
            return;
        }

        // 预处理:检查哪些文件可以成功提取标题,并处理重复名称
        const validFiles = [];
        const invalidFiles = [];
        const usedNames = new Set(); // 用于跟踪已使用的文件名

        for (const file of completeFiles) {
            const extracted = extractTitleAndYear(file.fileName);
            if (extracted.title && extracted.title !== '未知影视剧' && extracted.title.length >= 2) {
                // 构建基础新文件名
                let baseName = extracted.year ? `${extracted.title} (${extracted.year})` : extracted.title;
                let finalName = baseName;

                // 检查是否与已处理的文件名重复
                if (usedNames.has(finalName.toLowerCase())) {
                    // 如果重复,尝试添加清晰度信息
                    const quality = extractQuality(file.fileName);
                    if (quality) {
                        finalName = `${baseName} ${quality}`;
                        debugLog('批量重命名检测到重复名称,添加清晰度后缀:', finalName);

                        // 再次检查添加清晰度后是否还有重复
                        if (usedNames.has(finalName.toLowerCase())) {
                            // 如果还是重复,添加时间戳
                            const timestamp = new Date().getTime().toString().slice(-4);
                            finalName = `${baseName} ${quality} (${timestamp})`;
                            debugLog('批量重命名添加清晰度后仍重复,添加时间戳:', finalName);
                        }
                    } else {
                        // 如果没有清晰度信息,直接添加时间戳
                        const timestamp = new Date().getTime().toString().slice(-4);
                        finalName = `${baseName} (${timestamp})`;
                        debugLog('批量重命名未找到清晰度信息,添加时间戳:', finalName);
                    }
                }

                // 再次检查是否与页面现有文件名重复
                const currentElement = file.element.closest('tr') || file.element;
                if (checkDuplicateName(finalName, currentElement)) {
                    const quality = extractQuality(file.fileName);
                    if (quality && !finalName.includes(quality)) {
                        finalName = `${baseName} ${quality}`;
                        if (checkDuplicateName(finalName, currentElement)) {
                            const timestamp = new Date().getTime().toString().slice(-4);
                            finalName = `${baseName} ${quality} (${timestamp})`;
                        }
                    } else {
                        const timestamp = new Date().getTime().toString().slice(-4);
                        finalName = `${baseName} (${timestamp})`;
                    }
                }

                // 记录已使用的文件名
                usedNames.add(finalName.toLowerCase());

                validFiles.push({
                    ...file,
                    extracted: extracted,
                    newName: finalName,
                    quality: extractQuality(file.fileName)
                });
            } else {
                invalidFiles.push(file);
            }
        }

        let confirmMessage = `准备批量重命名 ${completeFiles.length} 个完整文件\n\n`;
        confirmMessage += `✅ 可重命名: ${validFiles.length} 个\n`;
        if (invalidFiles.length > 0) {
            confirmMessage += `❌ 无法提取标题: ${invalidFiles.length} 个\n\n`;
            confirmMessage += '无法提取标题的文件:\n';
            invalidFiles.slice(0, 3).forEach(file => {
                confirmMessage += `• ${file.fileName.substring(0, 50)}...\n`;
            });
            if (invalidFiles.length > 3) {
                confirmMessage += `• 还有 ${invalidFiles.length - 3} 个文件...\n`;
            }
            confirmMessage += '\n';
        }

        if (validFiles.length > 0) {
            confirmMessage += '预览重命名结果(前3个):\n';
            validFiles.slice(0, 3).forEach(file => {
                let preview = `• "${file.fileName.substring(0, 30)}..." → "${file.newName}"`;
                if (file.quality) {
                    preview += ` [${file.quality}]`;
                }
                confirmMessage += preview + '\n';
            });
            if (validFiles.length > 3) {
                confirmMessage += `• 还有 ${validFiles.length - 3} 个文件...\n`;
            }

            // 统计清晰度信息
            const qualityCount = validFiles.filter(f => f.quality).length;
            if (qualityCount > 0) {
                confirmMessage += `\n📺 检测到清晰度信息: ${qualityCount} 个文件\n`;
            }
        }

        confirmMessage += '\n是否继续批量重命名?';

        if (CONFIG.BATCH_CONFIRM_BEFORE_START && !confirm(confirmMessage)) {
            return;
        }

        if (validFiles.length === 0) {
            alert('没有可以重命名的文件,操作取消。');
            return;
        }

        // 开始批量重命名
        await performBatchRename(validFiles);
    }

    // 执行批量重命名
    async function performBatchRename(files) {
        batchRenameInProgress = true;
        batchRenameResults = [];

        const button = document.getElementById('batch-rename-btn');
        const originalText = button.innerHTML;

        let successCount = 0;
        let failCount = 0;
        const usedNames = new Set(); // 跟踪已使用的文件名

        try {
            for (let i = 0; i < files.length; i++) {
                const file = files[i];

                // 更新按钮状态
                if (CONFIG.BATCH_SHOW_PROGRESS) {
                    button.innerHTML = `🔄 重命名中... (${i + 1}/${files.length})`;
                    button.style.background = 'linear-gradient(135deg, #FF9800, #F57C00)';
                }

                try {
                    debugLog(`批量重命名 ${i + 1}/${files.length}: ${file.fileName} → ${file.newName}`);

                    const folderId = getFolderId(file.element);
                    if (folderId) {
                        const result = await performRenameAsync(folderId, file.newName, file.fileName, usedNames);
                        if (result.success) {
                            successCount++;
                            batchRenameResults.push({
                                fileName: file.fileName,
                                newName: result.finalName,
                                status: 'success'
                            });
                        } else {
                            failCount++;
                            batchRenameResults.push({
                                fileName: file.fileName,
                                newName: file.newName,
                                status: 'failed',
                                error: result.error || '重命名API调用失败'
                            });

                            if (CONFIG.BATCH_STOP_ON_ERROR) {
                                break;
                            }
                        }
                    } else {
                        failCount++;
                        batchRenameResults.push({
                            fileName: file.fileName,
                            newName: file.newName,
                            status: 'failed',
                            error: '无法获取文件夹ID'
                        });

                        if (CONFIG.BATCH_STOP_ON_ERROR) {
                            break;
                        }
                    }
                } catch (error) {
                    failCount++;
                    batchRenameResults.push({
                        fileName: file.fileName,
                        newName: file.newName,
                        status: 'failed',
                        error: error.message
                    });

                    debugLog(`批量重命名失败: ${file.fileName}`, error);

                    if (CONFIG.BATCH_STOP_ON_ERROR) {
                        break;
                    }
                }

                // 延迟以避免请求过于频繁
                if (i < files.length - 1) {
                    await new Promise(resolve => setTimeout(resolve, CONFIG.BATCH_RENAME_DELAY));
                }
            }
        } finally {
            batchRenameInProgress = false;

            // 恢复按钮状态
            button.innerHTML = originalText;
            button.style.background = 'linear-gradient(135deg, #4CAF50, #45a049)';

            // 显示结果
            showBatchRenameResults(successCount, failCount);

            // 如果有成功的重命名且启用了自动刷新
            if (successCount > 0 && CONFIG.AUTO_REFRESH_AFTER_RENAME) {
                setTimeout(() => {
                    location.reload();
                }, 2000);
            }
        }
    }

    // 异步版本的重命名函数
    function performRenameAsync(folderId, newName, originalName, usedNames = new Set()) {
        return new Promise((resolve) => {
            debugLog('执行异步重命名操作:', { folderId, newName, originalName });

            // 验证文件夹ID
            if (!folderId || folderId.length < 3) {
                debugLog('无效的文件夹ID:', folderId);
                resolve({ success: false, error: '无效的文件夹ID' });
                return;
            }

            // 清理文件名
            const cleanName = newName
                .replace(/\\/g, "")
                .replace(/\//g, " ")
                .replace(/:/g, " ")
                .replace(/\?/g, " ")
                .replace(/"/g, " ")
                .replace(/</g, " ")
                .replace(/>/g, " ")
                .replace(/\|/g, "")
                .replace(/\*/g, " ")
                .trim();

            // 准备API请求参数
            const requestBody = new URLSearchParams({
                fid: folderId,
                file_name: cleanName
            });

            // 调用115的重命名API
            fetch('https://webapi.115.com/files/edit', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'X-Requested-With': 'XMLHttpRequest'
                },
                body: requestBody,
                credentials: 'include'
            })
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP错误: ${response.status}`);
                }
                return response.json();
            })
            .then(data => {
                if (data.state === true || data.state === 1) {
                    debugLog('异步重命名成功:', cleanName);
                    usedNames.add(cleanName.toLowerCase());
                    resolve({ success: true, finalName: cleanName });
                } else {
                    debugLog('异步重命名失败,API返回:', data);
                    const errorMsg = data.error || data.msg || data.message || '未知错误';

                    // 检查是否是重复名称错误,如果是则尝试添加清晰度参数
                    if (errorMsg.includes('已存在') || errorMsg.includes('重复') || errorMsg.includes('duplicate')) {
                        debugLog('检测到重复名称错误,尝试添加清晰度参数重新重命名');

                        // 提取清晰度信息
                        const quality = extractQuality(originalName);
                        if (quality && !cleanName.includes(quality)) {
                            const newNameWithQuality = `${cleanName} ${quality}`;
                            if (!usedNames.has(newNameWithQuality.toLowerCase())) {
                                debugLog('尝试使用带清晰度的文件名重新重命名:', newNameWithQuality);
                                // 递归调用,使用带清晰度的文件名
                                performRenameAsync(folderId, newNameWithQuality, originalName, usedNames)
                                    .then(result => resolve(result));
                                return;
                            }
                        }

                        // 如果没有清晰度信息或已包含清晰度,尝试添加时间戳
                        const timestamp = new Date().getTime().toString().slice(-4);
                        const newNameWithTimestamp = `${cleanName} (${timestamp})`;
                        if (!usedNames.has(newNameWithTimestamp.toLowerCase())) {
                            debugLog('尝试添加时间戳:', newNameWithTimestamp);
                            // 递归调用,使用带时间戳的文件名
                            performRenameAsync(folderId, newNameWithTimestamp, originalName, usedNames)
                                .then(result => resolve(result));
                            return;
                        }
                    }

                    resolve({ success: false, error: errorMsg });
                }
            })
            .catch(error => {
                debugLog('异步重命名请求失败:', error);
                resolve({ success: false, error: error.message });
            });
        });
    }

    // 显示批量重命名结果
    function showBatchRenameResults(successCount, failCount) {
        let message = `批量重命名完成!\n\n`;
        message += `✅ 成功: ${successCount} 个\n`;
        message += `❌ 失败: ${failCount} 个\n\n`;

        if (failCount > 0) {
            message += '失败的文件:\n';
            const failedResults = batchRenameResults.filter(r => r.status === 'failed');
            failedResults.slice(0, 5).forEach(result => {
                message += `• ${result.fileName.substring(0, 30)}... (${result.error})\n`;
            });
            if (failedResults.length > 5) {
                message += `• 还有 ${failedResults.length - 5} 个失败的文件...\n`;
            }
            message += '\n';
        }

        if (successCount > 0) {
            message += '成功重命名的文件:\n';
            const successResults = batchRenameResults.filter(r => r.status === 'success');

            // 统计清晰度信息
            const qualityResults = successResults.filter(r => {
                const quality = extractQuality(r.fileName);
                return quality && r.newName.includes(quality);
            });

            if (qualityResults.length > 0) {
                message += `📺 包含清晰度信息: ${qualityResults.length} 个\n\n`;
            }

            successResults.slice(0, 3).forEach(result => {
                const quality = extractQuality(result.fileName);
                let displayText = `• "${result.fileName.substring(0, 20)}..." → "${result.newName}"`;
                if (quality && result.newName.includes(quality)) {
                    displayText += ` [${quality}]`;
                }
                message += displayText + '\n';
            });
            if (successResults.length > 3) {
                message += `• 还有 ${successResults.length - 3} 个成功的文件...\n`;
            }
        }

        if (successCount > 0 && CONFIG.AUTO_REFRESH_AFTER_RENAME) {
            message += '\n页面将在2秒后自动刷新以显示更改。';
        }

        alert(message);
    }

    // 执行重命名操作
    function performRename(folderId, newName, originalName) {
        debugLog('执行重命名操作:', { folderId, newName, originalName });

        // 验证文件夹ID
        if (!folderId || folderId.length < 3) {
            console.error('无效的文件夹ID:', folderId);
            alert('文件夹ID无效,无法执行重命名操作');
            return;
        }

        // 清理文件名,移除115不支持的字符
        const cleanName = newName
            .replace(/\\/g, "")
            .replace(/\//g, " ")
            .replace(/:/g, " ")
            .replace(/\?/g, " ")
            .replace(/"/g, " ")
            .replace(/</g, " ")
            .replace(/>/g, " ")
            .replace(/\|/g, "")
            .replace(/\*/g, " ")
            .trim();

        debugLog('清理后的文件名:', cleanName);

        // 显示进度提示
        const progressMessage = `正在重命名文件夹...\n\n原文件名: "${originalName}"\n新文件名: "${cleanName}"`;
        console.log(progressMessage);

        // 准备API请求参数
        const requestBody = new URLSearchParams({
            fid: folderId,
            file_name: cleanName
        });

        debugLog('API请求参数:', {
            url: 'https://webapi.115.com/files/edit',
            method: 'POST',
            fid: folderId,
            file_name: cleanName
        });

        // 调用115的重命名API
        fetch('https://webapi.115.com/files/edit', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'X-Requested-With': 'XMLHttpRequest'
            },
            body: requestBody,
            credentials: 'include' // 包含cookies
        })
        .then(response => {
            debugLog('API响应状态:', response.status, response.statusText);
            if (!response.ok) {
                throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
            }
            return response.json();
        })
        .then(data => {
            debugLog('API响应数据:', data);

            if (data.state === true || data.state === 1) {
                debugLog('重命名成功:', cleanName);
                alert(`重命名成功!\n\n原文件名: "${originalName}"\n新文件名: "${cleanName}"${CONFIG.AUTO_REFRESH_AFTER_RENAME ? '\n\n页面将在1秒后刷新以显示更改' : ''}`);
                // 刷新页面以显示更改(如果启用)
                if (CONFIG.AUTO_REFRESH_AFTER_RENAME) {
                    setTimeout(() => {
                        location.reload();
                    }, 1000);
                }
            } else {
                debugLog('重命名失败,API返回:', data);
                const errorMsg = data.error || data.msg || data.message || '未知错误';

                // 检查是否是重复名称错误,如果是则尝试添加清晰度参数
                if (errorMsg.includes('已存在') || errorMsg.includes('重复') || errorMsg.includes('duplicate')) {
                    debugLog('检测到重复名称错误,尝试添加清晰度参数重新重命名');

                    // 提取清晰度信息
                    const quality = extractQuality(originalName);
                    if (quality && !cleanName.includes(quality)) {
                        const newNameWithQuality = `${cleanName} ${quality}`;
                        debugLog('尝试使用带清晰度的文件名重新重命名:', newNameWithQuality);

                        // 确认是否要使用带清晰度的文件名重新尝试
                        const retryConfirm = confirm(`重命名失败:${errorMsg}\n\n检测到文件清晰度信息:${quality}\n\n是否尝试使用以下文件名重新重命名?\n"${newNameWithQuality}"\n\n点击"确定"重新尝试,点击"取消"放弃操作。`);

                        if (retryConfirm) {
                            // 递归调用重命名,使用带清晰度的文件名
                            performRename(folderId, newNameWithQuality, originalName);
                            return; // 避免显示原始错误消息
                        }
                    } else {
                        // 如果没有清晰度信息或已包含清晰度,尝试添加时间戳
                        const timestamp = new Date().getTime().toString().slice(-4);
                        const newNameWithTimestamp = `${cleanName} (${timestamp})`;
                        debugLog('没有清晰度信息或已包含,尝试添加时间戳:', newNameWithTimestamp);

                        const retryConfirm = confirm(`重命名失败:${errorMsg}\n\n是否尝试使用以下文件名重新重命名?\n"${newNameWithTimestamp}"\n\n点击"确定"重新尝试,点击"取消"放弃操作。`);

                        if (retryConfirm) {
                            // 递归调用重命名,使用带时间戳的文件名
                            performRename(folderId, newNameWithTimestamp, originalName);
                            return; // 避免显示原始错误消息
                        }
                    }
                }

                // 显示原始错误消息
                alert(`重命名失败\n\n错误信息: ${errorMsg}\n\n可能的原因:\n1. 没有重命名权限\n2. 文件名包含不支持的字符\n3. 文件夹正在被使用\n4. 网络连接问题\n5. 目录名称已存在`);
            }
        })
        .catch(error => {
            debugLog('重命名请求失败:', error);

            let errorMessage = `重命名请求失败\n\n错误详情: ${error.message}`;

            if (error.message.includes('HTTP错误')) {
                errorMessage += `\n\n可能的原因:\n1. 未登录(不可用)115网盘\n2. 会话已过期\n3. 网络连接问题\n4. 115服务器暂时不可用`;
            } else if (error.message.includes('Failed to fetch')) {
                errorMessage += `\n\n可能的原因:\n1. 网络连接中断\n2. 115网盘服务器无响应\n3. 浏览器阻止了请求`;
            }

            alert(errorMessage);
        });
    }

    // 查找文件夹图标元素
    function findFolderIcon(fileElement) {
        // 115网盘特定的文件夹图标选择器
        const iconSelectors = [
            // 115网盘文件夹图标的常见选择器
            '.list-thumb',
            '.file-icon',
            '.folder-icon',
            'img[src*="folder"]',
            'img[src*="dir"]',
            '.icon',
            'i[class*="folder"]',
            'span[class*="icon"]',
            // 通用图标选择器
            '.fa-folder',
            '.glyphicon-folder'
        ];

        // 首先在当前行元素中查找图标
        const rowElement = fileElement.closest('tr') || fileElement;

        for (const selector of iconSelectors) {
            const icon = rowElement.querySelector(selector);
            if (icon) {
                return icon;
            }
        }

        // 如果没找到,尝试查找第一个td元素(通常包含图标)
        const firstCell = rowElement.querySelector('td:first-child');
        if (firstCell) {
            // 在第一个单元格中查找任何图片或图标元素
            const imgIcon = firstCell.querySelector('img, i, span[class*="icon"], .icon');
            if (imgIcon) {
                return imgIcon;
            }
            // 如果还是没找到,返回第一个单元格本身作为图标容器
            return firstCell;
        }

        return null;
    }

    // 获取文件夹内文件数量
    // 文件数量获取函数 - 使用115官方API
    async function getFileCount(fileElement, retries = 3, delay = 1000) {
        const cid = extractFolderId(fileElement);
        if (!cid) {
            debugLog('无法提取文件夹ID,返回null');
            return null;
        }

        debugLog('使用115官方API获取文件数量,文件夹ID:', cid);
        const apiCount = await getFileCountFromAPI(cid, retries, delay);

        if (apiCount !== null) {
            debugLog('API成功获取文件数量:', apiCount);
            return apiCount;
        }

        debugLog('API获取文件数量失败');
        return null;
    }

    // 提取文件夹ID的函数 - 支持多种115网盘页面布局
    function extractFolderId(fileElement) {
        // 尝试多种方式获取文件夹ID
        let cid = null;

        // 方式1: 从元素属性中获取
        cid = fileElement.getAttribute('cate_id') ||
              fileElement.getAttribute('data-id') ||
              fileElement.getAttribute('data-cid') ||
              fileElement.getAttribute('fid');

        if (cid) {
            debugLog('从元素属性获取到文件夹ID:', cid);
            return cid;
        }

        // 方式2: 从父元素或行元素中获取
        const row = fileElement.closest('tr, li, .file-item, .list-item');
        if (row) {
            cid = row.getAttribute('cate_id') ||
                  row.getAttribute('data-id') ||
                  row.getAttribute('data-cid') ||
                  row.getAttribute('nid') ||
                  row.getAttribute('fid');

            if (cid) {
                debugLog('从行元素获取到文件夹ID:', cid);
                return cid;
            }
        }

        // 方式3: 从文件夹链接中提取
        const folderLink = fileElement.querySelector('a[href*="cid="]') ||
                          fileElement.closest('tr, li')?.querySelector('a[href*="cid="]');

        if (folderLink) {
            const href = folderLink.getAttribute('href');
            const cidMatch = href.match(/cid=([^&]+)/);
            if (cidMatch) {
                cid = cidMatch[1];
                debugLog('从链接URL提取到文件夹ID:', cid);
                return cid;
            }
        }

        // 方式4: 从onclick事件中提取
        const clickableElement = fileElement.querySelector('[onclick*="cid"]') ||
                                fileElement.closest('tr, li')?.querySelector('[onclick*="cid"]');

        if (clickableElement) {
            const onclick = clickableElement.getAttribute('onclick');
            const cidMatch = onclick.match(/cid['"]?\s*[:=]\s*['"]?([^'"\s,)]+)/);
            if (cidMatch) {
                cid = cidMatch[1];
                debugLog('从onclick事件提取到文件夹ID:', cid);
                return cid;
            }
        }

        debugLog('无法提取文件夹ID');
        return null;
    }

    // 使用115 API获取文件数量 - 参考115pan_aria2的实现
    async function getFileCountFromAPI(cid, retries = 3, delay = 1000) {
        if (!cid || cid.length < 3) {
            debugLog('无效的文件夹ID:', cid);
            return null;
        }

        // 使用与115pan_aria2相同的API端点和参数
        const limit = 1000; // 使用较大的limit以获取完整的文件数量
        const url = `https://webapi.115.com/files?aid=1&limit=${limit}&offset=0&show_dir=1&cid=${cid}`;

        for (let i = 0; i < retries; i++) {
            try {
                debugLog(`API调用尝试 ${i + 1}/${retries}:`, url);

                const data = await new Promise((resolve, reject) => {
                    const gmRequest = GM.xmlHttpRequest || GM_xmlhttpRequest;
                    if (!gmRequest) {
                        reject(new Error('GM.xmlHttpRequest 不可用'));
                        return;
                    }

                    gmRequest({
                        method: 'GET',
                        url: url,
                        headers: {
                            'User-Agent': navigator.userAgent,
                            'Referer': 'https://115.com/',
                            'X-Requested-With': 'XMLHttpRequest'
                        },
                        onload: function(response) {
                            if (response.status >= 200 && response.status < 300) {
                                try {
                                    const data = JSON.parse(response.responseText);
                                    resolve(data);
                                } catch (parseError) {
                                    reject(new Error(`JSON解析错误: ${parseError.message}`));
                                }
                            } else {
                                reject(new Error(`HTTP错误: ${response.status} ${response.statusText}`));
                            }
                        },
                        onerror: function(error) {
                            reject(new Error(`网络错误: ${error.message || '未知错误'}`));
                        },
                        ontimeout: function() {
                            reject(new Error('请求超时'));
                        },
                        timeout: 10000
                    });
                });
                debugLog('API响应数据:', data);

                if (data.state === true || data.state === 1) {
                    // 计算实际文件数量(排除文件夹)
                    let fileCount = 0;
                    if (data.data && Array.isArray(data.data)) {
                        // 统计非文件夹项目(有sha值的是文件,没有sha值的是文件夹)
                        fileCount = data.data.filter(item => item.sha && item.sha.length > 0).length;
                        debugLog(`API返回总项目数: ${data.data.length}, 文件数: ${fileCount}`);
                    } else if (typeof data.count === 'number') {
                        // 如果没有详细数据,使用总数作为近似值
                        fileCount = data.count;
                        debugLog(`API返回总数: ${fileCount}`);
                    }

                    return fileCount;
                } else {
                    const errorMsg = data.error || data.msg || data.message || '未知API错误';
                    debugLog('API返回错误状态:', errorMsg);

                    // 如果是权限错误,不重试
                    if (errorMsg.includes('权限') || errorMsg.includes('登录(不可用)') || errorMsg.includes('认证')) {
                        debugLog('检测到权限错误,停止重试');
                        return null;
                    }
                }
            } catch (error) {
                debugLog(`API调用失败 (尝试 ${i + 1}/${retries}):`, error.message);

                // 如果是网络错误或超时,可以重试
                if (i < retries - 1 && (error.message.includes('网络错误') || error.message.includes('请求超时') || error.message.includes('HTTP错误'))) {
                    debugLog(`等待 ${delay}ms 后重试...`);
                    await new Promise(resolve => setTimeout(resolve, delay));
                    continue;
                }
            }

            // 如果不是最后一次尝试,等待后重试
            if (i < retries - 1) {
                await new Promise(resolve => setTimeout(resolve, delay));
            }
        }

        debugLog('所有API调用尝试均失败');
        return null;
    }




    // 处理iframe内容
    function processIframes() {
        const iframes = document.querySelectorAll('iframe');
        for (const iframe of iframes) {
            try {
                if (iframe.contentDocument && iframe.contentDocument.body) {
                    debugLog('找到可访问的iframe,开始处理其内容...');
                    processFileItemsInDocument(iframe.contentDocument);
                }
            } catch (error) {
                debugLog('无法访问iframe内容(可能是跨域限制):', error.message);
            }
        }
    }

    // 在指定文档中处理文件列表项
    async function processFileItemsInDocument(doc = document) {
        debugLog('开始处理文件列表项...');

        // 查找文件列表容器的多种可能选择器
        const possibleSelectors = [
            // 115网盘特定选择器 - 针对文件夹行
            'tr[data-id]',
            '.list-contents tr',
            '.file-opr-wraper',
            '.list-item',
            // 通用选择器
            '.file-item',
            '.file-name',
            '[data-file-name]',
            'a[title]',
            '.filename'
        ];

        let fileItems = [];

        for (const selector of possibleSelectors) {
            const elements = doc.querySelectorAll(selector);
            if (elements.length > 0) {
                fileItems = Array.from(elements);
                debugLog(`找到 ${fileItems.length} 个文件项,使用选择器: ${selector}`);
                break;
            }
        }

        if (fileItems.length === 0) {
            debugLog('未找到文件列表项,尝试查找所有可能的文本元素...');
            // 如果没有找到特定的文件列表,尝试查找所有包含文本的元素
            const allElements = doc.querySelectorAll('*');
            fileItems = Array.from(allElements).filter(el => {
                const text = el.textContent || el.innerText || '';
                return text.includes('集') && el.children.length === 0; // 只选择叶子节点
            });
            debugLog(`通过文本搜索找到 ${fileItems.length} 个可能的文件项`);
        }

        for (const item of fileItems) {
            try {
                // 获取文件名
                let fileName = '';
                if (item.title) {
                    fileName = item.title;
                } else if (item.textContent) {
                    fileName = item.textContent.trim();
                } else if (item.innerText) {
                    fileName = item.innerText.trim();
                }

                if (!fileName) continue;

                debugLog(`处理文件: ${fileName}`);

                // 检查是否已经添加过标注
                if (item.querySelector('.file-status-dot')) {
                    continue;
                }

                // 检查是否包含"全xx集"模式
                const episodeNumber = extractEpisodeNumber(fileName);

                if (episodeNumber) {
                    // 获取文件夹内文件数量
                    let fileCount = await getFileCount(item);
                    if (fileCount === null) {
                        debugLog('API方法获取文件数量失败');
                        fileCount = -1; // 设置为未知
                    }
                    debugLog(`${fileName}: 提取的集数=${episodeNumber}, 获取的文件数量=${fileCount}`);

                    // 创建标注圆点
                    let dot;
                    if (fileCount === -1) {
                        // 无法获取文件数量的处理
                        if (CONFIG.HIDE_UNKNOWN_FILES) {
                            // 隐藏无法确定文件数量的文件,不显示任何标注
                            debugLog(`${fileName}: 无法确定文件数量,已隐藏标注`);
                            continue;
                        } else if (CONFIG.SHOW_UNKNOWN_AS_INCOMPLETE) {
                            // 显示为不完整(红色)
                            dot = createDot(CONFIG.INCOMPLETE_COLOR, '?', episodeNumber, fileName, item);
                            debugLog(`${fileName}: 未知文件数量,标记为不完整 (?/${episodeNumber})`);
                        } else {
                            // 不显示标注
                            debugLog(`${fileName}: 无法确定文件数量,跳过标注`);
                            continue;
                        }
                    } else if (fileCount >= episodeNumber) {
                        dot = createDot(CONFIG.COMPLETE_COLOR, fileCount, episodeNumber, fileName, item); // 完整
                        debugLog(`${fileName}: 完整 (${fileCount}/${episodeNumber})`);
                    } else {
                        dot = createDot(CONFIG.INCOMPLETE_COLOR, fileCount, episodeNumber, fileName, item); // 不完整
                        debugLog(`${fileName}: 不完整 (${fileCount}/${episodeNumber})`);
                    }

                    dot.className = 'file-status-dot';

                    // 找到文件夹图标并添加圆点
                     const iconElement = findFolderIcon(item);
                     if (iconElement) {
                         // 确保图标容器有相对定位
                         const computedStyle = window.getComputedStyle(iconElement);
                         if (computedStyle.position === 'static') {
                             iconElement.style.position = 'relative';
                         }

                         // 确保图标容器有足够的尺寸来显示圆点
                         if (iconElement.offsetWidth < 20 || iconElement.offsetHeight < 20) {
                             iconElement.style.minWidth = '20px';
                             iconElement.style.minHeight = '20px';
                         }

                         iconElement.appendChild(dot);
                         debugLog(`${fileName}: 圆点已添加到图标元素`, iconElement);
                     } else {
                         // 备用方案:添加到文件名前面
                         if (item.firstChild) {
                             item.insertBefore(dot, item.firstChild);
                         } else {
                             item.appendChild(dot);
                         }
                         debugLog(`${fileName}: 未找到图标元素,使用备用方案`);
                     }
                }
            } catch (error) {
                debugLog('处理文件项时出错:', error);
            }
        }
    }

    // 处理文件列表项(包装函数)
    async function processFileItems() {
        // 处理主文档
        await processFileItemsInDocument(document);

        // 处理iframe内容
        processIframes();
    }

    // 监听页面变化
    function observePageChanges() {
        const observer = new MutationObserver((mutations) => {
            let shouldProcess = false;

            mutations.forEach((mutation) => {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    // 检查是否有新的文件列表项添加
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.classList && (node.classList.contains('file-item') ||
                                node.classList.contains('list-item') ||
                                node.querySelector && node.querySelector('.file-name'))) {
                                shouldProcess = true;
                                break;
                            }
                        }
                    }
                }
            });

            if (shouldProcess) {
                debugLog('检测到页面变化,重新处理文件列表...');
                setTimeout(() => {
                    processFileItems().then(() => {
                        // 处理完文件后,更新批量重命名按钮状态
                        createBatchRenameButton();
                    });
                }, CONFIG.MUTATION_DELAY);
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        debugLog('页面变化监听器已启动');
    }

    // 主函数
    async function main() {
        debugLog('115网盘文件完整性检查器已启动');

        try {
            // 等待页面加载
            await new Promise(resolve => {
                if (document.readyState === 'loading') {
                    document.addEventListener('DOMContentLoaded', resolve);
                } else {
                    resolve();
                }
            });

            // 等待一段时间让页面完全加载
            await new Promise(resolve => setTimeout(resolve, CONFIG.PROCESS_DELAY));

            // 处理当前页面的文件列表
            await processFileItems();

            // 处理完文件后,创建批量重命名按钮(仅在有绿标文件时显示)
            createBatchRenameButton();

            // 开始监听页面变化
            observePageChanges();

            debugLog('文件完整性检查器初始化完成');

        } catch (error) {
            debugLog('初始化失败:', error);
        }
    }

    // 启动脚本
    main();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址