Greasy Fork

Greasy Fork is available in English.

115网盘-全能视频清理(时长+VDI+持久化)

合并了视频时长清理与VDI清晰度清理功能。支持独立勾选、参数持久化保存、VDI对照表。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         115网盘-全能视频清理(时长+VDI+持久化)
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  合并了视频时长清理与VDI清晰度清理功能。支持独立勾选、参数持久化保存、VDI对照表。
// @author       edhnt4551
// @match        https://115.com/*
// @icon         https://115.com/favicon.ico
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    if (window.self !== window.top) return;

    // === 样式配置 ===
    GM_addStyle(`
        #ce-cleaner-btn {
            position: fixed; top: 120px; right: 20px; z-index: 2147483647;
            padding: 8px 15px; background: #2777F8; color: #fff;
            border-radius: 4px; cursor: pointer; font-size: 14px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.3); transition: 0.3s;
            display: flex; align-items: center; gap: 5px; font-weight: 500;
        }
        #ce-cleaner-btn:hover { background: #1C66E6; transform: translateY(-1px); }
        #ce-cleaner-panel {
            position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
            width: 850px; max-height: 85vh; background: #fff; z-index: 2147483647;
            border-radius: 8px; box-shadow: 0 10px 40px rgba(0,0,0,0.4);
            display: none; flex-direction: column; overflow: hidden; font-size: 14px;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            border: 1px solid #ddd;
        }
        .ce-header {
            padding: 15px 20px; border-bottom: 1px solid #eee;
            display: flex; justify-content: space-between; align-items: center;
            background: #f9f9f9;
        }
        .ce-header h3 { margin: 0; font-size: 16px; color: #333; }
        .ce-close { cursor: pointer; font-size: 24px; color: #999; line-height: 1; }
        .ce-close:hover { color: #333; }
        .ce-body { padding: 20px; overflow-y: auto; flex: 1; }

        /* 控制区样式 */
        .ce-controls {
            margin-bottom: 10px; background: #f0f7ff; padding: 15px; border-radius: 6px;
            border: 1px solid #e1ecf9; display: flex; flex-direction: column; gap: 10px;
        }
        .ce-control-row {
            display: flex; align-items: center; gap: 15px; padding-bottom: 10px; border-bottom: 1px dashed #dae8f9;
        }
        .ce-control-row:last-of-type { border-bottom: none; padding-bottom: 0; }

        .ce-group-label { font-weight:bold; color:#2777F8; font-size:13px; width: 80px; display:flex; align-items:center; gap:4px; }

        /* 选项样式 */
        .ce-opt-label { display: flex; align-items: center; gap: 5px; cursor: pointer; user-select: none; color: #444; }
        .ce-opt-label:hover { color: #2777F8; }
        .ce-opt-label input[type="checkbox"] { width: 15px; height: 15px; margin: 0; cursor: pointer; accent-color: #2777F8; }

        .ce-input {
            padding: 4px; border: 1px solid #ddd; border-radius: 4px; width: 50px; text-align: center; font-size: 13px;
        }
        .ce-input:disabled { background: #f0f0f0; color: #bbb; border-color: #eee; cursor: not-allowed; }

        .ce-btn {
            padding: 6px 20px; border: none; border-radius: 4px; cursor: pointer; color: #fff; transition: 0.2s; font-weight:bold;
        }
        .ce-btn-primary { background: #2777F8; }
        .ce-btn-primary:hover { background: #1C66E6; }
        .ce-btn-danger { background: #E64C4C; }
        .ce-btn-danger:hover { background: #D63C3C; }
        .ce-btn-disabled { background: #ccc !important; cursor: not-allowed; }

        /* 提示说明区 */
        .ce-tips {
            margin-bottom: 15px; padding: 10px 15px; background: #fff8e1;
            border: 1px solid #ffe0b2; border-radius: 6px; color: #8d6e63; font-size: 12px;
            line-height: 1.6;
        }
        .ce-tips strong { color: #e65100; font-weight: 600; }
        .ce-vdi-table { margin-top: 5px; display: flex; flex-wrap: wrap; gap: 8px; }
        .ce-vdi-item { background: rgba(255,255,255,0.6); padding: 1px 6px; border-radius: 3px; border: 1px solid #ffe0b2; font-family: monospace; }

        /* 表格样式 */
        .ce-list-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
        .ce-list-table th, .ce-list-table td {
            text-align: left; padding: 10px; border-bottom: 1px solid #eee; font-size: 13px;
        }
        .ce-list-table th { background: #fff; position: sticky; top: 0; z-index: 1; border-bottom: 2px solid #eee; font-weight: 600; }

        .ce-footer { padding: 15px 20px; border-top: 1px solid #eee; text-align: right; background: #fff; }
        .ce-log { color: #666; font-size: 12px; margin-top: 10px; padding-left: 5px; }

        /* 标签样式 */
        .ce-tag { padding: 2px 6px; border-radius: 3px; font-size: 11px; margin-left: 5px; white-space: nowrap; }
        .ce-tag-short { background: #ffebeb; color: #e64c4c; border:1px solid #ffcdd2; }
        .ce-tag-long { background: #e6f3ff; color: #2777f8; border:1px solid #bbdefb; }
        .ce-tag-vdi { background: #fff3e0; color: #ef6c00; border:1px solid #ffe0b2; }
        .ce-vdi-badge { display:inline-block; font-size:10px; background:#eee; color:#555; padding:1px 4px; border-radius:3px; margin-right:4px; }
        .ce-divider { height: 16px; width: 1px; background: #ccc; margin: 0 5px; }
    `);

    // === 设置管理 (持久化) ===
    const settings = {
        save() {
            GM_setValue('cfg_min_on', document.getElementById('ce-chk-min-duration').checked);
            GM_setValue('cfg_min_val', document.getElementById('ce-duration-min').value);

            GM_setValue('cfg_max_on', document.getElementById('ce-chk-max-duration').checked);
            GM_setValue('cfg_max_val', document.getElementById('ce-duration-max').value);

            GM_setValue('cfg_vdi_on', document.getElementById('ce-chk-vdi').checked);
            GM_setValue('cfg_vdi_val', document.getElementById('ce-vdi-val').value);

            GM_setValue('cfg_vdi0_on', document.getElementById('ce-chk-vdi-zero').checked);
        },
        load() {
            // 设置默认值
            const setVal = (id, val) => { if(document.getElementById(id)) document.getElementById(id).value = val; };
            const setChk = (id, val) => {
                const el = document.getElementById(id);
                if(el) {
                    el.checked = val;
                    // 触发change事件以更新关联输入框的disabled状态,或者手动处理
                    const inputId = el.getAttribute('data-target');
                    if(inputId) document.getElementById(inputId).disabled = !val;
                }
            };

            setChk('ce-chk-min-duration', GM_getValue('cfg_min_on', false));
            setVal('ce-duration-min', GM_getValue('cfg_min_val', 8));

            setChk('ce-chk-max-duration', GM_getValue('cfg_max_on', false));
            setVal('ce-duration-max', GM_getValue('cfg_max_val', 60));

            setChk('ce-chk-vdi', GM_getValue('cfg_vdi_on', false));
            setVal('ce-vdi-val', GM_getValue('cfg_vdi_val', 3));

            setChk('ce-chk-vdi-zero', GM_getValue('cfg_vdi0_on', false));
        }
    };

    // === UI 构建 ===
    const ui = {
        btn: null,
        panel: null,
        init() {
            if (document.getElementById('ce-cleaner-btn')) return;

            // 悬浮球
            this.btn = document.createElement('div');
            this.btn.id = 'ce-cleaner-btn';
            this.btn.innerHTML = '🧹 视频清理助手';
            this.btn.title = "点击打开筛选面板";
            this.btn.onclick = (e) => {
                e.stopPropagation();
                this.togglePanel();
            };
            document.body.appendChild(this.btn);

            // 主面板
            this.panel = document.createElement('div');
            this.panel.id = 'ce-cleaner-panel';
            this.panel.innerHTML = `
                <div class="ce-header">
                    <h3>🎥 视频清理助手 (持久化版)</h3>
                    <span class="ce-close" onclick="document.getElementById('ce-cleaner-panel').style.display='none'">×</span>
                </div>
                <div class="ce-body">
                    <div class="ce-controls">
                        <!-- 时长控制行 -->
                        <div class="ce-control-row">
                            <span class="ce-group-label">⏱️ 时长</span>

                            <label class="ce-opt-label">
                                <input type="checkbox" id="ce-chk-min-duration" data-target="ce-duration-min">
                                <span>小于</span>
                                <input type="number" id="ce-duration-min" class="ce-input" placeholder="0" disabled>
                                <span>分钟</span>
                            </label>

                            <div class="ce-divider"></div>

                            <label class="ce-opt-label">
                                <input type="checkbox" id="ce-chk-max-duration" data-target="ce-duration-max">
                                <span>大于</span>
                                <input type="number" id="ce-duration-max" class="ce-input" placeholder="0" disabled>
                                <span>分钟</span>
                            </label>
                        </div>

                        <!-- VDI控制行 -->
                        <div class="ce-control-row">
                            <span class="ce-group-label">📺 清晰度</span>

                            <label class="ce-opt-label">
                                <input type="checkbox" id="ce-chk-vdi" data-target="ce-vdi-val">
                                <span>VDI 低于</span>
                                <input type="number" id="ce-vdi-val" class="ce-input" min="1" max="5" disabled>
                            </label>

                            <div class="ce-divider"></div>

                            <label class="ce-opt-label" title="VDI=0 通常表示转码中或无法识别">
                                <input type="checkbox" id="ce-chk-vdi-zero">
                                <span>删除未知清晰度(VDI=0)</span>
                            </label>
                        </div>

                         <div class="ce-control-row" style="justify-content: flex-end; border:none; padding-top:5px;">
                            <button id="ce-scan-btn" class="ce-btn ce-btn-primary">💾 保存设置并扫描</button>
                        </div>
                    </div>

                    <!-- 使用说明与VDI对照表 -->
                    <div class="ce-tips">
                        <div>💡 <strong>提示:</strong> 您的设置(勾选状态和数值)会在点击“保存设置并扫描”时自动保存,下次无需重新输入。</div>
                        <div style="margin-top:5px;">📊 <strong>VDI 清晰度参考表:</strong></div>
                        <div class="ce-vdi-table">
                            <span class="ce-vdi-item">1 = 低清</span>
                            <span class="ce-vdi-item">2 = 标清</span>
                            <span class="ce-vdi-item">3 = 高清</span>
                            <span class="ce-vdi-item">4 = 1080P</span>
                            <span class="ce-vdi-item">5 = 4K</span>
                            <span class="ce-vdi-item">100 = 原画</span>
                        </div>
                    </div>

                    <div id="ce-status" class="ce-log">请勾选上方条件并点击扫描...</div>

                    <div style="max-height: 400px; overflow-y: auto; border: 1px solid #eee; margin-top:5px; border-radius: 4px;">
                        <table class="ce-list-table">
                            <thead>
                                <tr>
                                    <th width="40"><input type="checkbox" id="ce-select-all" checked></th>
                                    <th>文件名</th>
                                    <th width="80">大小</th>
                                    <th width="100">VDI</th>
                                    <th width="140">删除原因</th>
                                </tr>
                            </thead>
                            <tbody id="ce-file-list"></tbody>
                        </table>
                    </div>
                </div>
                <div class="ce-footer">
                    <span id="ce-summary" style="margin-right: 20px; font-weight: bold; color: #E64C4C;"></span>
                    <button id="ce-delete-btn" class="ce-btn ce-btn-danger ce-btn-disabled" disabled>删除选中文件 (回收站)</button>
                </div>
            `;
            document.body.appendChild(this.panel);

            // 交互逻辑:复选框控制输入框禁用状态
            this.bindCheckbox('ce-chk-min-duration', 'ce-duration-min');
            this.bindCheckbox('ce-chk-max-duration', 'ce-duration-max');
            this.bindCheckbox('ce-chk-vdi', 'ce-vdi-val');

            // 恢复保存的设置
            settings.load();

            // 事件绑定
            document.getElementById('ce-scan-btn').onclick = () => core.scan();
            document.getElementById('ce-delete-btn').onclick = () => core.delete();
            document.getElementById('ce-select-all').onchange = (e) => core.toggleAll(e.target.checked);
        },
        bindCheckbox(chkId, inputId) {
            const chk = document.getElementById(chkId);
            const input = document.getElementById(inputId);
            chk.addEventListener('change', () => {
                input.disabled = !chk.checked;
                if(chk.checked) input.focus();
            });
        },
        togglePanel() {
            this.panel.style.display = this.panel.style.display === 'flex' ? 'none' : 'flex';
        },
        log(msg) {
            const statusEl = document.getElementById('ce-status');
            if(statusEl) statusEl.innerText = msg;
        },
        renderList(files) {
            const tbody = document.getElementById('ce-file-list');
            tbody.innerHTML = '';
            if (files.length === 0) {
                tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; color:#999; padding: 20px;">没有发现符合条件的文件</td></tr>';
                this.updateSummary(0);
                return;
            }

            files.forEach(f => {
                const tr = document.createElement('tr');

                // 构建原因标签
                let reasonHtml = '';
                if (f._reasons.includes('short')) reasonHtml += `<span class="ce-tag ce-tag-short">时长<${f._limit_min}m</span>`;
                if (f._reasons.includes('long')) reasonHtml += `<span class="ce-tag ce-tag-long">时长>${f._limit_max}m</span>`;
                if (f._reasons.includes('low_vdi')) reasonHtml += `<span class="ce-tag ce-tag-vdi">VDI<${f._limit_vdi}</span>`;
                if (f._reasons.includes('zero_vdi')) reasonHtml += `<span class="ce-tag ce-tag-vdi">画质未知</span>`;

                // 时长显示
                const timeStr = core.formatTime(f.play_long);

                tr.innerHTML = `
                    <td><input type="checkbox" class="ce-file-chk" value="${f.fid}" checked></td>
                    <td title="${f.n}">
                        <div style="font-weight:500; word-break:break-all;">${f.n}</div>
                    </td>
                    <td>${core.formatSize(f.s)}</td>
                    <td>
                        <span class="ce-vdi-badge">VDI: ${f.vdi || 0}</span>
                        <div style="font-size:11px; color:#888;">${timeStr}</div>
                    </td>
                    <td>${reasonHtml}</td>
                `;
                tbody.appendChild(tr);
            });
            this.updateSummary(files.length);
        },
        updateSummary(count) {
            const btn = document.getElementById('ce-delete-btn');
            document.getElementById('ce-summary').innerText = `共选中 ${count} 个文件`;
            if (count > 0) {
                btn.removeAttribute('disabled');
                btn.classList.remove('ce-btn-disabled');
            } else {
                btn.setAttribute('disabled', 'true');
                btn.classList.add('ce-btn-disabled');
            }
        }
    };

    // === 核心逻辑 ===
    const core = {
        cid: 0,
        getCid() {
            try {
                if (window.TOP && window.TOP.API && window.TOP.API.aid) return window.TOP.API.cid;
            } catch(e) {}
            const match = location.href.match(/[?&]cid=(\d+)/);
            if (match) return match[1];
            return 0;
        },
        formatTime(seconds) {
            if (!seconds) return '00:00';
            const h = Math.floor(seconds / 3600);
            const m = Math.floor((seconds % 3600) / 60);
            const s = Math.floor(seconds % 60);
            if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
            return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
        },
        formatSize(bytes) {
            if (bytes === 0) return '0 B';
            const k = 1024;
            const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
        },
        async scan() {
            // 保存当前设置
            settings.save();

            this.cid = this.getCid();
            if (!this.cid || this.cid == '0') {
                alert('⚠️ 未获取到文件夹ID。\n\n请进入具体的文件夹后再点击扫描。');
                return;
            }

            // === 获取启用状态 ===
            const useMin = document.getElementById('ce-chk-min-duration').checked;
            const useMax = document.getElementById('ce-chk-max-duration').checked;
            const useVdi = document.getElementById('ce-chk-vdi').checked;
            const useVdiZero = document.getElementById('ce-chk-vdi-zero').checked;

            if (!useMin && !useMax && !useVdi && !useVdiZero) {
                alert('⚠️ 请至少勾选一个筛选条件!');
                return;
            }

            // === 获取数值 ===
            const minVal = parseFloat(document.getElementById('ce-duration-min').value) || 0;
            const maxVal = parseFloat(document.getElementById('ce-duration-max').value) || 0;
            const minSec = minVal * 60;
            const maxSec = maxVal * 60;
            const vdiThreshold = parseInt(document.getElementById('ce-vdi-val').value) || 0;

            ui.log('正在获取文件列表...');
            ui.renderList([]);

            try {
                const files = await this.fetchAllFiles(this.cid);
                const targets = files.filter(f => {
                    if (!f.fid) return false;

                    let reasons = [];
                    const duration = parseFloat(f.play_long || 0);
                    const vdi = isNaN(parseInt(f.vdi)) ? 0 : parseInt(f.vdi);

                    // 1. 时长判断
                    if (duration > 0) {
                        if (useMin && minSec > 0 && duration < minSec) {
                            reasons.push('short');
                            f._limit_min = minVal;
                        }
                        if (useMax && maxSec > 0 && duration > maxSec) {
                            reasons.push('long');
                            f._limit_max = maxVal;
                        }
                    }

                    // 2. VDI(清晰度)判断
                    if (vdi === 0) {
                        if (useVdiZero) reasons.push('zero_vdi');
                    } else {
                        if (useVdi && vdiThreshold > 0 && vdi < vdiThreshold) {
                            reasons.push('low_vdi');
                            f._limit_vdi = vdiThreshold;
                        }
                    }

                    if (reasons.length > 0) {
                        f._reasons = reasons;
                        return true;
                    }
                    return false;
                });

                ui.renderList(targets);
                ui.log(`✅ 扫描完成。共检索 ${files.length} 个视频,命中 ${targets.length} 个。`);

            } catch (e) {
                console.error(e);
                ui.log('❌ 扫描出错: ' + e.message);
            }
        },

        async fetchAllFiles(cid) {
            let allData = [];
            let offset = 0;
            const limit = 1150;
            while (true) {
                ui.log(`正在加载第 ${Math.floor(offset/limit) + 1} 页数据...`);
                const url = `https://webapi.115.com/files?aid=1&cid=${cid}&o=user_ptime&asc=0&offset=${offset}&show_dir=0&limit=${limit}&code=&scid=&snap=0&natsort=1&type=4&format=json`;
                const data = await this.request(url);
                if (!data.state) throw new Error(data.error || 'API请求失败,请检查登录状态');
                if (data.data && data.data.length > 0) allData = allData.concat(data.data);

                if (allData.length >= data.count || data.data.length < limit) break;

                offset += limit;
                await new Promise(r => setTimeout(r, 200));
            }
            return allData;
        },

        async delete() {
            const checkboxes = document.querySelectorAll('.ce-file-chk:checked');
            if (checkboxes.length === 0) return;
            if (!confirm(`⚠️ 高能预警:\n\n即将删除选中的 ${checkboxes.length} 个文件。\n\n文件将移入【回收站】,确定继续吗?`)) return;

            const ids = Array.from(checkboxes).map(cb => cb.value);
            const btn = document.getElementById('ce-delete-btn');
            const originalText = btn.innerText;
            btn.innerText = '正在执行删除...';
            btn.setAttribute('disabled', 'true');

            try {
                const batchSize = 500;
                for (let i = 0; i < ids.length; i += batchSize) {
                    const chunk = ids.slice(i, i + batchSize);
                    await this.deleteBatch(chunk);
                    ui.log(`🗑️ 已删除 ${Math.min(i + batchSize, ids.length)} / ${ids.length} ...`);
                }
                ui.log('✅ 删除完成!请手动刷新网页查看结果。');
                alert('清理完成!文件已移入回收站。');
                ui.togglePanel();
                setTimeout(()=> location.reload(), 1000);
            } catch (e) {
                alert('删除出错:' + e.message);
                btn.innerText = originalText;
                btn.removeAttribute('disabled');
            }
        },

        async deleteBatch(fids) {
            const fd = new FormData();
            fd.append('pid', this.cid);
            fd.append('ignore_warn', '1');
            fids.forEach((fid, index) => { fd.append(`fid[${index}]`, fid); });
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "POST",
                    url: "https://webapi.115.com/rb/delete",
                    data: fd,
                    headers: {
                        "Origin": "https://115.com",
                        "Referer": "https://115.com/",
                    },
                    onload: (res) => {
                        try {
                            const json = JSON.parse(res.responseText);
                            if (json.state) resolve(json); else reject(new Error(json.error));
                        } catch(e) {
                            reject(new Error("返回数据解析失败"));
                        }
                    },
                    onerror: (e) => reject(e)
                });
            });
        },

        request(url) {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: url,
                    headers: { "User-Agent": navigator.userAgent, "Accept": "application/json" },
                    onload: (response) => {
                        if (response.status === 200) {
                            try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(e); }
                        } else { reject(new Error("HTTP " + response.status)); }
                    },
                    onerror: (err) => reject(err)
                });
            });
        },

        toggleAll(checked) {
            document.querySelectorAll('.ce-file-chk').forEach(cb => cb.checked = checked);
        }
    };

    setTimeout(() => { ui.init(); }, 1000);
})();