合并了视频时长清理与VDI清晰度清理功能。支持独立勾选、参数持久化保存、VDI对照表。
// ==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);
})();