// ==UserScript==
// @name MissAv批量备份收藏视频
// @namespace https://github.com/10086100886
// @version 1.2.0.0
// @description 从当前missav页面获取图片文件和视频信息,并合并结果后提供下载生成的网页文件
// @license MIT
// @author 人民的勤务员 <toniaiwanowskiskr47@gmail.com> & ChatGPT
// @match https://missav.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_openInTab
// @icon https://pic.616pic.com/ys_bnew_img/00/35/79/Gv93yQh7v6.jpg
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require https://update.greasyfork.org/scripts/498124/1396763/video.js
// @require https://update.greasyfork.org/scripts/498149/1395619/%E4%BF%A1%E6%81%AF%E6%9F%A5%E7%9C%8B%E5%99%A8.js
// ==/UserScript==
(function() {
'use strict';
var controlButton = createButton('备份', '10px', '10px');
var buttonA = createButton('备份当前', '70px', '10px');
var buttonB = createButton('备份片单', '130px', '10px');
var buttonC = createButton('设置选项', '190px', '10px');
var webdavbutton = createButton('WebDav', '250px', '10px');
// 设置按钮的背景颜色和样式
controlButton.style.backgroundColor = 'blue'; // 控制按钮改为蓝色背景
buttonA.style.backgroundColor = 'green';
buttonB.style.backgroundColor = 'blue';
buttonC.style.backgroundColor = 'red';
webdavbutton.style.backgroundColor = 'blue';
// 隐藏初始的三个按钮
buttonA.style.display = 'none';
buttonB.style.display = 'none';
buttonC.style.display = 'none';
webdavbutton.style.display = 'none';
// 添加按钮到页面
document.body.appendChild(controlButton);
document.body.appendChild(buttonA);
document.body.appendChild(buttonB);
document.body.appendChild(buttonC);
document.body.appendChild(webdavbutton);
// 控制按钮的点击事件
controlButton.addEventListener('click', function() {
if (buttonA.style.display === 'none') {
// 显示三个按钮
buttonA.style.display = 'block';
buttonB.style.display = 'block';
buttonC.style.display = 'block';
webdavbutton.style.display = 'block';
controlButton.innerHTML = '隐藏';
} else {
// 隐藏三个按钮
buttonA.style.display = 'none';
buttonB.style.display = 'none';
buttonC.style.display = 'none';
webdavbutton.style.display = 'none';
controlButton.innerHTML = '备份';
}
});
webdavbutton.addEventListener('click', function() {
// 点击按钮时执行的操作
WebDAVManager.listFilesAndFolders(webdavfold);
});
// 按钮A的点击事件
buttonA.addEventListener('click', function() {
resetGlobalVariables();
singleFileDownload =true;
window.showLogContainer();
var currentDate = new Date();
var currentTime = currentDate.getFullYear() + '-' + (currentDate.getMonth() + 1) + '-' + currentDate.getDate() + '_' +currentDate.getHours() + '-' + currentDate.getMinutes() + '-' + currentDate.getSeconds();
if (useDefaultTitle) {
name = document.querySelector('meta[name="twitter:title"]').content;
} else {
const twitterTitleContent = document.querySelector('meta[name="twitter:title"]').content;
name = prompt("请输入自定义名称:", twitterTitleContent);
if (name === null) {
name = twitterTitleContent;
}
}
inurl = window.location.href;
const defaultPages = getTotalPagesd();
const totalPages = setTotalPage(defaultPages);
allpages=totalPages
//const delay = settime();
if (totalPages ) {
start(totalPages);
}
});
// 按钮B的点击事件
buttonB.addEventListener('click', function() {
// 点击按钮时执行的操作
resetGlobalVariables();
fetchJsonData();
});
// 按钮C的点击事件
buttonC.addEventListener('click', function() {
createSettingsUI();
// 这里可以添加按钮C点击后的具体操作,比如打开链接或执行其他动作
});
// 创建按钮的辅助函数
function createButton(text, top, left) {
var button = document.createElement('button');
button.innerHTML = text;
button.style.position = 'fixed';
button.style.bottom = top;
button.style.right = left;
button.style.zIndex = '1000';
button.style.padding = '10px';
button.style.border = 'none';
button.style.cursor = 'pointer';
button.style.color = '#fff';
button.style.fontSize = '14px';
button.style.fontWeight = 'bold';
button.style.textAlign = 'center';
button.style.width = '100px'; // 调整按钮宽度
return button;
}
// 全局变量
var allResults = []; // 存储所有的结果数据
var zip = new JSZip(); // 创建一个压缩文件实例
var allzip = new JSZip(); // 另一个可能的压缩文件实例
var imgFolder = zip.folder("img"); // 在 zip 中创建一个名为 "img" 的文件夹,用于存储图片文件
var allimgFolder = allzip.folder("img"); // 在 allzip 中创建一个名为 "img" 的文件夹,可能用于另一个压缩文件的图片存储
var ALLfiledown = false; // 标识是否所有文件已下载完毕的布尔变量
var videos = []; // 存储视频文件或相关信息的数组
var finalData = []; // 存储最终处理数据的数组
var inurl = ""; // 当前下载地址的变量
var pendingRequests = 0; // 当前待处理的请求数量
var delayTime; // 延迟时间,以毫秒为单位,用于控制异步操作的时间间隔
var currentPage = 1; // 当前处理的页数,可能用于分页处理或其他进度跟踪
var currentUrlIndex = 0; // 当前处理的 urls 数组中的索引位置
var name = ""; // 当前下载的名称
var urls = []; // 存储需要处理的网址数组
var a = -1; // 循环中的计数或索引,初始值为 -1
var allZipContents = []; // 存储所有压缩文件内容的数组
var singleFileDownload = false; // 标识是否为单个文件下载模式的布尔变量
var names = []; // 存储下载名称列表的数组
var allpages = 0; // 存储总页数或其他页面处理相关信息的变量
var modalContainer = null; // 存储模态窗口容器的全局变量,用于显示下载进度或其他信息
var shouldReplace = false; // 控制是否在下载大图时进行替换操作的布尔变量
var temporaryData = [];
var saveJson= false ;
var useDefaultTitle= true;
var pageCount= true;
var saveVideoInfo= false;
var saveImage= false;
var downloadLog = {};
var errorLogs = {};
var downloadLogFileA = false; // 这里设置为 true 时载日志
var webdavfold="missavsave";
//var webdavfold="1111";
var savetowebdav= false;
var webdavUrl =''
var webdavUsername =''
var webdavPassword =''
var deleteSelected=false;
ini();//读取配置
function resetGlobalVariables() {
zip = new JSZip(); // 重置为一个新的 JSZip 实例,用于创建新的压缩文件
allzip = new JSZip(); // 可能是另一个新的 JSZip 实例,用于其他用途的压缩文件
if (saveImage) {
imgFolder = zip.folder("img"); // 在 zip 中创建一个名为 "img" 的文件夹,用于存储图片文件
allimgFolder = allzip.folder("img"); // 在 allzip 中创建一个名为 "img" 的文件夹,可能用于另一个压缩文件的图片存储
}
downloadLog = {};
errorLogs = {};
ALLfiledown = false; // 重置为 false,表示所有文件未下载完毕
videos = []; // 清空存储视频文件或相关信息的数组
finalData = []; // 清空存储最终处理数据的数组
inurl = ""; // 重置当前下载地址为空字符串
pendingRequests = 0; // 重置待处理的请求数量为 0
currentPage = 1; // 重置当前处理的页数为 1
currentUrlIndex = 0; // 重置当前处理的 urls 数组索引为 0
name = ""; // 重置当前下载的名称为空字符串
urls = []; // 清空存储需要处理的网址数组
a = -1; // 重置循环中的计数或索引为 -1
allZipContents = []; // 清空存储所有压缩文件内容的数组
singleFileDownload = false; // 重置为 false,表示不是单个文件下载模式
names = []; // 清空存储下载名称列表的数组
allpages = 0; // 重置存储总页数或其他页面处理相关信息的变量为 0
temporaryData = [];
}
async function processUrls() {
//delayTime = 20;
let completedTasks = 0; // 计数已完成的任务数量
for (const url of urls) {
a = a + 1; // 每次循环递增 a
inurl = url;
console.log("正在处理网址:", url, names[a]);
window.addToLog("处理:"+url+names[a], 'info');
name = names[a];
try {
const totalPages = await getTotalPages(url); // 等待 getTotalPages 返回结果
console.log("Total pages for", url, ":", totalPages); // 显示总页数
window.addToLog(name+" 总页数:"+url+ totalPages, 'info');
allpages = totalPages;
start(totalPages); // 启动处理流程
// 等待当前页面的请求完成
while (pendingRequests > 0) {
await new Promise(resolve => setTimeout(resolve, 100)); // 每隔 100 毫秒检查一次是否所有请求都已完成
}
completedTasks++; // 标记当前任务已完成
} catch (error) {
console.error("Error processing URL:", url, error); // 处理错误信息
allpages = 1;
start(1); // 启动处理流程
while (pendingRequests > 0) {
await new Promise(resolve => setTimeout(resolve, 100)); // 每隔 100 毫秒检查一次是否所有请求都已完成
}
completedTasks++; // 标记当前任务已完成
}
}
// 如果所有任务都已完成且 urls 数组不为空,则调用下载函数
if (completedTasks === urls.length && urls.length !== 0) {
downloadAllZips();
}
}
function getAllCookies() {
return document.cookie;
}
// 获取指定 JSON 数据的函数
function fetchJsonData() {
const cookies = getAllCookies();
//alert(cookies);
console.log('Current page cookies:', cookies);
// 构建 API URL
const apiUrl = 'https://missav.com/api/playlists/dfe-057';
// 发送带有 cookies 的请求
GM_xmlhttpRequest({
method: 'GET',
url: apiUrl,
headers: {
'Cookie': cookies
},
onload: function(response) {
if (response.status === 200) {
try {
const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse && Array.isArray(jsonResponse.data)) {
createReportUI(jsonResponse.data, 500); // 假设每页显示 10 个项目
// 调用 processUrls 函数处理 URLs
} else {
console.error('JSON 格式无效');
showModal("JSON 格式无效",2000);
}
} catch (error) {
console.error('Error parsing JSON:', error);
showModal("解析错误"+error,2000);
}
} else {
console.error('Request failed with status:', response.status);
showModal("解析错误",2000);
}
},
onerror: function(error) {
showModal("解析错误"+error,2000);
}
});
}
function processUrl(url) {
// 检查是否包含 `page=` 参数
var pageIndex = url.indexOf('page=');
if (pageIndex !== -1) {
// 找到 `page=` 参数并删除它及其后的所有内容
var baseUrl = url.substring(0, pageIndex + 5); // +5 to include `page=`
return baseUrl;
} else {
// 检查是否已有其他参数
if (url.includes('?')) {
// 有其他参数,添加 `&page=`
return url + '&page=';
} else {
// 没有其他参数,添加 `?page=`
return url + '?page=';
}
}
}
function settime() {
// 让用户输入延时时间
delayTime = prompt("请输入每页请求的延时时间(毫秒):", "1000");
// 检查用户是否取消输入
if (delayTime === null) {
alert("输入取消");
return;
}
delayTime = parseInt(delayTime);
// 检查输入的延时时间是否有效
if (isNaN(delayTime) || delayTime <= 0) {
alert("请输入有效的延时时间(正整数)!");
return;
}
// 返回有效的延时时间
return delayTime;
}
function getTotalPagesd() { // 获取总页数
var totalPagesElement = document.querySelector('#price-currency');
var totalPagesText = totalPagesElement ? totalPagesElement.innerText : '';
var totalPages = parseInt(totalPagesText.replace('/', '').trim(), 10);
// 如果获取总页数失败,则返回 1
if (isNaN(totalPages) || totalPages <= 0) {
totalPages = 1;
}
return totalPages;
}
function getTotalPages(url) {
return new Promise((resolve, reject) => {
// 如果没有提供 URL,则使用当前页面的 URL
if (!url) {
url = window.location.href;
}
// 发起 GM_xmlhttpRequest 请求获取页面内容
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: { "Cookie": document.cookie },
onload: function(response) {
// 处理响应
if (response.status === 200) {
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, 'text/html');
const totalPagesElement = doc.querySelector('#price-currency'); // 替换为实际选择器
if (totalPagesElement) {
const totalPagesText = totalPagesElement.innerText;
const totalPages = parseInt(totalPagesText.replace('/', '').trim(), 10);
resolve(totalPages); // 成功时返回总页数
} else {
window.addToLog('页面中没有找到总页数,默认为1页', 'warning');
reject('Total pages element not found'); // 页面中没有找到总页数元素
}
} else {
window.addToLog('请求失败', 'warning');
reject(`Request failed with status ${response.status}`); // 请求失败
}
},
onerror: function() {
window.addToLog('请求出错', 'warning');
reject('Request failed'); // 请求出错
}
});
});
}
// 设置总页数
function setTotalPage(defaultPages) {
if (!pageCount) {
return defaultPages;
}
const inputPages = parseInt(prompt(`当前 ${name} 总页数为 ${defaultPages}。请输入你想要抓取的页数(不输入抓取全部):`, defaultPages), 10);
if (isNaN(inputPages) || inputPages <= 0) {
return defaultPages;
}
return inputPages;
}
// 开始处理页面抓取
function start(totalPages, callback) {
const pages = Array.from({ length: totalPages }, (_, i) => i + 1);
pendingRequests = pages.length;
fetchPage(pages.shift(), pages, callback);
}
// 点击按钮时执行操作
// 异步获取页面内容
function fetchPage(pageNum, pages, callback) {
const pageUrl = `${processUrl(inurl)}${pageNum}`;
console.log(`正在获取第 ${pageNum} 页的内容...`);
//showModal(`正在获取${name} 第 ${pageNum} / ${allpages}页 `);
if (a !== -1) {
showModal(`${a + 1}/${names.length} 正在获取 ${name} 第 ${pageNum} / ${allpages} 页`);
} else {
showModal(`正在获取 ${name} 第 ${pageNum} / ${allpages} 页`);
}
GM_xmlhttpRequest({
method: "GET",
url: pageUrl,
headers: { "Cookie": document.cookie },
onload: function(response) {
if (response.status === 200) {
processPageContent(response.responseText, pageNum, pages, callback);
} else {
pendingRequests--;
checkIfComplete(callback);
if (pages.length > 0) {
setTimeout(() => fetchPage(pages.shift(), pages, callback), delayTime);
}
}
}
});
}
//获取视频信息
function extractInformation(htmlContent) {
let data = {}; // 创建一个对象来存储提取的数据
let xhr = new XMLHttpRequest();
xhr.open('GET', htmlContent, false); // 同步方式打开请求
xhr.send();
// 创建一个虚拟的 <div> 元素来加载 HTML 内容
let tempDiv = document.createElement('div');
tempDiv.innerHTML = xhr.responseText;
// 获取所有包含信息的父元素列表
let parentElements = tempDiv.querySelectorAll('div.space-y-2 > div');
if (parentElements.length > 0) {
let allInfo = {}; // 初始化一个空对象来存储所有信息
// 遍历每个包含信息的 <div> 元素
parentElements.forEach(div => {
let span = div.querySelector('span'); // 获取第一个 <span> 元素
if (span) {
let category = span.textContent.trim(); // 获取主分类名称
if (!allInfo[category]) {
allInfo[category] = []; // 初始化一个空数组来存储该分类下的所有信息
}
// 查找所有的 <a> 元素和 <time> 元素
div.querySelectorAll('a, time').forEach(element => {
let info = {};
// 判断元素是否是 <a> 元素
if (element.tagName === 'A') {
info['name'] = element.textContent.trim(); // 获取名称
info['link'] = element.href.trim(); // 获取链接
} else if (element.tagName === 'TIME') {
info = element.textContent.trim(); // 如果是 <time> 元素,则直接保存其文本内容
}
// 添加信息对象到相应的主分类数组中
allInfo[category].push(info);
});
// 如果没有找到 <a> 元素,则尝试获取 <span> 标签内的文本内容
if (div.querySelectorAll('a').length === 0) {
let spanText = div.querySelector('span.font-medium');
if (spanText) {
let info = spanText.textContent.trim();
allInfo[category].push(info);
}
}
}
});
// 提取 class="mb-1 text-secondary break-all line-clamp-2" 的内容
let descriptionElement = tempDiv.querySelector('.mb-1.text-secondary.break-all.line-clamp-2');
let descriptionContent = descriptionElement ? descriptionElement.textContent.trim() : '';
allInfo['简介'] = descriptionContent;
// 将所有信息存储到 data 对象中
data['videosinfo'] = allInfo;
// 查找包含 x-cloak 和 x-show="currentTab === 'magnets'" 的第二个元素
let secondElement = tempDiv.querySelector('div[x-cloak][x-show="currentTab === \'magnets\'"]');
if (secondElement) {
let linksAndInfo = [];
// 遍历第二个元素内的 <a> 元素
secondElement.querySelectorAll('a[rel="nofollow"]').forEach(a => {
let linkInfo = {
name: a.textContent.trim(),
link: a.href.trim()
};
// 查找相邻的 <td> 元素,获取大小和日期信息
let sizeTd = a.closest('td').nextElementSibling;
if (sizeTd && sizeTd.classList.contains('font-mono')) {
linkInfo['size'] = sizeTd.textContent.trim(); // 获取大小信息
}
let dateTd = sizeTd ? sizeTd.nextElementSibling : null;
if (dateTd && dateTd.classList.contains('hidden')) {
linkInfo['date'] = dateTd.textContent.trim(); // 获取日期信息
}
let nextSibling = a.nextElementSibling;
// 循环处理所有紧邻的<span>元素
while (nextSibling && nextSibling.tagName === 'SPAN') {
let spanText = nextSibling.textContent.trim();
linkInfo['name'] += ' ' + spanText // 将<span>元素的文本内容追加到name中
nextSibling = nextSibling.nextElementSibling; // 继续查找下一个兄弟元素
}
linksAndInfo.push(linkInfo);
});
// 将第二个元素的链接和信息添加到 data 中
data['secondElementLinksInfo'] = linksAndInfo;
} else {
console.error('未找到包含 x-cloak 和 x-show="currentTab === \'magnets\'" 的第二个元素。');
}
return data; // 返回结构化的数据
} else {
console.error('未找到匹配的父元素 div.space-y-2');
return null; // 如果未找到匹配的父元素,返回 null
}
} ///大
// 使用XMLHttpRequest获取页面内容
function fetchPageforinfo(url) {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
extractInformation(xhr.responseText); // 将获取的页面内容传递给提取信息的函数
} else {
console.error('请求失败:' + xhr.status);
}
}
};
xhr.open('GET', url, true);
xhr.send();
}
// 处理获取到的页面内容
function processPageContent(htmlContent, pageNum, pages, callback) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const divElements = doc.querySelectorAll('div.relative.aspect-w-16.aspect-h-9.rounded.overflow-hidden.shadow-lg');
const logEntry = {
url: `${processUrl(inurl)}${pageNum}`,
elementsFetched: divElements.length
};
// 如果当前名称的日志组不存在,则创建一个新数组
if (!downloadLog[name]) {
downloadLog[name] = [];
}
// 将日志条目添加到日志数组中
downloadLog[name].push(logEntry);
if (divElements.length === 0) {
const logEntry = {
url: `${processUrl(inurl)}${pageNum}`,
elementsFetched: 0, // 这里可以根据实际需求设置其他信息
errorMessage: `获取第 ${pageNum} 页失败。`
};
if (!errorLogs[name]) {
errorLogs[name] = [];
}
errorLogs[name].push(logEntry);
console.log(`获取第 ${pageNum} 页失败。`);
window.addToLog(`${name}${processUrl(inurl)}${pageNum}+获取失败 数量:`+divElements.length, 'error');
}
divElements.forEach(div => {
var imgUrl = div.querySelector('img').getAttribute('data-src');
if (shouldReplace) {
imgUrl = imgUrl.replace('cover-t.jpg', 'cover-n.jpg');
}
const video = {
fileName: div.querySelector('a').getAttribute('alt'),
imgUrl: imgUrl,
videoUrl: div.querySelector('video').getAttribute('data-src'),
markContent: Array.from(div.querySelectorAll('span')).map(mark => mark.textContent).join(' '),
altText: div.querySelector('img').getAttribute('alt'),
jumpUrl: div.querySelector('a').getAttribute('href'),
};
if(saveVideoInfo){
video.info=extractInformation(video.jumpUrl);
//showBanner(`正在获取 ${video.fileName} 信息`);
window.addToLog(`正在获取 ${video.fileName} 信息`, 'info');
console.log()
};
if (video.imgUrl && video.altText) {
videos.push(video);
if (saveImage) {
window.addToLog(`保存`+video.imgUrl, 'info');
pendingRequests++;
GM_xmlhttpRequest({
method: 'GET',
url: video.imgUrl,
responseType: 'blob',
onload: function(response) {
if (response.status === 200) {
if (saveImage) {
if (singleFileDownload) {
console.log("这是单个文件下载");
imgFolder.file(`${video.fileName}.jpg`, response.response, { binary: true });
} else {
console.log("这是批量文件下载");
allimgFolder.file(`${video.fileName}.jpg`, response.response, { binary: true });
}
}
pendingRequests--;
checkIfComplete(callback);
} else {
pendingRequests--;
checkIfComplete(callback);
}
}
});
}
}else {
pendingRequests--;
checkIfComplete(callback);
}
});
showModal(`获取第 ${pageNum} 页的内容完成,等待 ${delayTime} 毫秒加载第 ${pageNum + 1} 页。`);
pendingRequests--;
checkIfComplete(callback);
if (pages.length > 0) {
setTimeout(() => fetchPage(pages.shift(), pages, callback), delayTime);
} else {
}
}
closeModal();
function downloadLogFile() {
if (!downloadLogFileA) {
console.log('日志下载已被跳过');
return;
}
if (Object.keys(errorLogs).length === 0) {
// 如果错误日志为空,直接下载正常日志文件
const logBlob = new Blob([JSON.stringify(downloadLog, null, 4)], { type: 'application/json' });
const logUrl = URL.createObjectURL(logBlob);
const logLink = document.createElement('a');
logLink.href = logUrl;
logLink.download = 'download_log.json';
logLink.click();
URL.revokeObjectURL(logUrl);
} else {
// 创建一个JSZip实例
const zip = new JSZip();
// 添加正常日志文件到压缩包
const logBlob = new Blob([JSON.stringify(downloadLog, null, 4)], { type: 'application/json' });
zip.file('download_log.json', logBlob);
// 添加错误日志文件到压缩包
const errorLogBlob = new Blob([JSON.stringify(errorLogs, null, 4)], { type: 'application/json' });
zip.file('error_log.json', errorLogBlob);
// 生成压缩包并触发下载
zip.generateAsync({ type: 'blob' }).then(function(content) {
const zipUrl = URL.createObjectURL(content);
const link = document.createElement('a');
link.href = zipUrl;
link.download = 'logs.zip';
link.click();
URL.revokeObjectURL(zipUrl);
});
}
}
function sanitizeFileName(name) {
return name.replace(/[\\\/:*?"<>|]/g, '_');
}
function checkIfComplete(callback) {
if (pendingRequests === 0) {
const additionalInfo = {
timestamp: new Date().toISOString(),
inurl: inurl
};
if (singleFileDownload) {
showModal("获取完毕,正在生成单个文件...");
finalData = {
info: additionalInfo,
video: videos
};
if (saveJson) {
zip.file("data.json", JSON.stringify(finalData, null, 4));
}
if (savetowebdav){
WebDAVManager.uploadFile(webdavfold, `${sanitizeFileName(name)}.json`, JSON.stringify(finalData, null, 4));
}
const jsonIndexContent = generateJsonIndexContent(finalData);
const numFiles = Object.keys(zip.files).length; // 获取压缩包中文件的数量
if (numFiles === 0) {
const htmlContent = jsonIndexContent; // 替换为实际的HTML内容
const htmlBlob = new Blob([htmlContent], { type: 'text/html' });
const htmlUrl = URL.createObjectURL(htmlBlob);
const a = document.createElement('a');
a.href = htmlUrl;
a.download = `${sanitizeFileName(name)}.html`;
a.click();
closeModal();
downloadLogFile();
if (callback) callback();
}else{
zip.file(`${sanitizeFileName(name)}.html`, jsonIndexContent);
// 生成并下载单个文件
zip.generateAsync({ type: "blob" }, function updateCallback(metadata) {
const progress = metadata.percent.toFixed(2);
showModal(`压缩进度: ${progress}%`);
}).then(content => {
const zipUrl = URL.createObjectURL(content);
const a = document.createElement('a');
a.href = zipUrl;
a.download = `${name}.zip`;
a.click();
URL.revokeObjectURL(zipUrl);
closeModal();
downloadLogFile();
if (callback) callback();
});
}
} else {
finalData = {
info: additionalInfo,
video: videos
};
if (saveJson) {
allzip.file(`${sanitizeFileName(name)}.json`, JSON.stringify(finalData, null, 4));
}
if (savetowebdav){
WebDAVManager.uploadFile(webdavfold, `${sanitizeFileName(name)}.json`, JSON.stringify(finalData, null, 4));
}
const jsonIndexContent = generateJsonIndexContent(finalData);;
allzip.file(`${sanitizeFileName(name)}.html`, jsonIndexContent);
finalData = [];
videos= [];
if (callback) callback();
}
}
}
function downloadAllZips() {
if (singleFileDownload === false) {
showModal("获取完毕,正在生成压缩文件...");
const numFiles = Object.keys(allzip.files).length; // 获取压缩包中文件的数量
if (numFiles === 1) {
// 如果压缩包中只有一个文件,直接处理该文件
const fileName = Object.keys(allzip.files)[0]; // 获取唯一的文件名
const file = allzip.files[fileName];
// 根据文件类型获取文件内容
file.async('blob').then(content => {
// 创建一个Blob对象,并下载
const blob = new Blob([content]);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a); // 添加到文档中以确保点击有效
a.click();
document.body.removeChild(a); // 下载完成后移除元素
URL.revokeObjectURL(url);
closeModal();
}).catch(error => {
console.error('Error fetching file content:', error);
closeModal();
});
downloadLogFile();
return; // 结束函数执行,不生成压缩包
}
allzip.generateAsync({ type: "blob" }, function updateCallback(metadata) {
const progress = metadata.percent.toFixed(2);
showModal(`压缩进度: ${progress}%`);
}).then(content => {
const zipUrl = URL.createObjectURL(content);
const a = document.createElement('a');
a.href = zipUrl;
a.download = `批量备份${urls.length}个片单.zip`;
a.click();
URL.revokeObjectURL(zipUrl);
closeModal();
downloadLogFile();
if (callback) callback();
});
// 如果 singleFileDownload 等于假,则执行这里的代码
}
}
function showBanner(text) {
// 查找现有的横幅元素
var existingBanner = document.querySelector('.banner');
if (existingBanner) {
// 如果横幅已经存在,直接更新文本内容
existingBanner.textContent = text;
} else {
// 如果横幅不存在,创建一个新的横幅
var banner = document.createElement('div');
banner.className = 'banner'; // 添加一个类名以便识别
banner.style.position = 'fixed';
banner.style.bottom = '20px'; // 距离底部的距离
banner.style.left = '20px'; // 距离左侧的距离
banner.style.width = 'auto'; // 根据文本自动调整宽度
banner.style.backgroundColor = 'rgba(255, 255, 255, 0.9)';
banner.style.color = '#000'; // 黑色文本
banner.style.textAlign = 'center';
banner.style.padding = '20px';
banner.style.borderRadius = '8px';
banner.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
banner.style.zIndex = '9999';
banner.textContent = text; // 将传入的文本设置为横幅内容
document.body.appendChild(banner); // 将横幅添加到文档的末尾
// 3秒后移除横幅提示
setTimeout(function() {
banner.remove();
}, 3000);
}
}
// 创建或更新模态窗口
function showModal(message, autoCloseDelay = 0) {
// 如果模态窗口不存在,则创建新的模态窗口
if (!modalContainer) {
modalContainer = document.createElement('div');
modalContainer.className = 'modal-container';
modalContainer.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.9);
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 9999;
padding: 20px;
`;
document.body.appendChild(modalContainer);
}
// 更新模态窗口的内容
modalContainer.textContent = message;
// 自动关闭模态窗口
if (autoCloseDelay > 0) {
setTimeout(closeModal, autoCloseDelay);
}
}
// 关闭模态窗口
function closeModal() {
// 如果模态窗口存在,则从 DOM 中移除
if (modalContainer) {
document.body.removeChild(modalContainer);
modalContainer = null; // 将变量重置为 null,以便下次创建新的模态窗口
}
}
// 创建JSONindex
function createReportUI(data, itemsPerPage) {
temporaryData = data;
// 创建全屏遮罩层
const overlay = document.createElement('div');
overlay.className = 'overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 1); /* 全黑不透明背景 */
z-index: 9999; /* 确保遮罩层位于所有内容之上 */
`;
// document.body.appendChild(overlay);
const modalContainer = document.createElement('div');
modalContainer.className = 'modal-container';
modalContainer.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 1);
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 10000; /* 确保弹出框位于遮罩层之上 */
padding: 20px;
width: 80%;
max-width: 800px;
`;
const title = document.createElement('h2');
title.textContent =`当前共有片单数量: ${temporaryData.length}`;
title.style.textAlign = 'center';
modalContainer.appendChild(title);
const closeButton = document.createElement('button');
closeButton.textContent = '×';
closeButton.style.position = 'absolute';
closeButton.style.top = '10px';
closeButton.style.right = '10px';
closeButton.style.backgroundColor = 'transparent';
closeButton.style.border = 'none';
closeButton.style.fontSize = '24px';
closeButton.style.cursor = 'pointer';
modalContainer.appendChild(closeButton);
closeButton.addEventListener('click', () => {
// document.body.removeChild(overlay); // 移除遮罩层
document.body.removeChild(modalContainer); // 移除模态框
});
const tableContainer = document.createElement('div');
tableContainer.style.cssText = `
max-height: 60vh;
overflow-y: auto;
`;
modalContainer.appendChild(tableContainer);
const table = document.createElement('table');
table.style.width = '100%';
table.style.borderCollapse = 'collapse';
table.style.fontSize = '16px';
tableContainer.appendChild(table);
const thead = document.createElement('thead');
table.appendChild(thead);
const headerRow = document.createElement('tr');
thead.appendChild(headerRow);
const checkboxHeader = document.createElement('th');
checkboxHeader.textContent = '选择';
checkboxHeader.style.textAlign = 'center';
checkboxHeader.style.padding = '10px';
headerRow.appendChild(checkboxHeader);
const nameHeader = document.createElement('th');
nameHeader.textContent = '片单';
nameHeader.style.padding = '10px';
nameHeader.style.width = '40%';
headerRow.appendChild(nameHeader);
const urlHeader = document.createElement('th');
urlHeader.textContent = '地址';
urlHeader.style.padding = '10px';
urlHeader.style.width = '40%';
headerRow.appendChild(urlHeader);
const tbody = document.createElement('tbody');
table.appendChild(tbody);
let currentPage = 1;
const totalItems = data.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
function generateTableData(page) {
tbody.innerHTML = '';
const startIndex = (page - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
for (let i = startIndex; i < endIndex && i < data.length; i++) {
const row = document.createElement('tr');
tbody.appendChild(row);
// 序号列
const indexCell = document.createElement('td');
indexCell.textContent = i + 1; // 显示序号,从1开始
indexCell.style.textAlign = 'center';
indexCell.style.padding = '5px';
row.appendChild(indexCell);
const checkboxCell = document.createElement('td');
checkboxCell.style.textAlign = 'center';
checkboxCell.style.padding = '5px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `checkbox_${i}`;
checkbox.value = i;
checkboxCell.appendChild(checkbox);
row.appendChild(checkboxCell);
const nameCell = document.createElement('td');
nameCell.textContent = data[i].name;
nameCell.style.padding = '10px';
nameCell.style.borderBottom = '1px solid #ddd';
row.appendChild(nameCell);
const urlCell = document.createElement('td');
const fullUrl = "https://missav.com/playlists/" + data[i].key;
const link = document.createElement('a');
link.textContent = fullUrl;
link.href = fullUrl;
link.target = '_blank'; // 在新标签页中打开链接
urlCell.appendChild(link);
urlCell.style.padding = '10px';
urlCell.style.borderBottom = '1px solid #ddd';
row.appendChild(urlCell);
}
}
generateTableData(currentPage);
const paginationContainer = document.createElement('div');
paginationContainer.style.marginTop = '20px';
paginationContainer.style.textAlign = 'center';
modalContainer.appendChild(paginationContainer);
const prevButton = document.createElement('button');
prevButton.textContent = '上一页';
prevButton.style.marginRight = '10px';
prevButton.disabled = true;
const pageIndicator = document.createElement('span');
pageIndicator.style.marginRight = '10px';
updatePageIndicator();
paginationContainer.appendChild(pageIndicator);
const nextButton = document.createElement('button');
nextButton.textContent = '下一页';
nextButton.style.marginLeft = '10px';
if (totalPages <= 1) {
nextButton.disabled = true;
}
prevButton.addEventListener('click', () => {
currentPage--;
generateTableData(currentPage);
updatePaginationButtons();
updatePageIndicator();
});
nextButton.addEventListener('click', () => {
currentPage++;
generateTableData(currentPage);
updatePaginationButtons();
updatePageIndicator();
});
function updatePaginationButtons() {
prevButton.disabled = currentPage === 1;
nextButton.disabled = currentPage === totalPages;
}
function updatePageIndicator() {
pageIndicator.textContent = `第 ${currentPage}/${totalPages} 页`;
}
const selectAllButton = document.createElement('button');
selectAllButton.textContent = '全部选择';
selectAllButton.style.marginRight = '10px';
selectAllButton.style.marginTop = '20px';
selectAllButton.style.padding = '10px 20px';
selectAllButton.style.fontSize = '16px';
selectAllButton.style.backgroundColor = '#007bff';
selectAllButton.style.color = '#fff';
selectAllButton.style.border = 'none';
selectAllButton.style.borderRadius = '5px';
selectAllButton.style.cursor = 'pointer';
selectAllButton.style.float = 'left';
modalContainer.appendChild(selectAllButton);
let selectAll = true;
selectAllButton.addEventListener('click', () => {
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAll;
});
if (selectAll) {
selectAllButton.textContent = '取消选择';
} else {
selectAllButton.textContent = '全部选择';
}
selectAll = !selectAll;
});
//
const confirmButton = document.createElement('button');
confirmButton.textContent = '确认选择';
confirmButton.style.marginTop = '20px';
confirmButton.style.padding = '10px 20px';
confirmButton.style.fontSize = '16px';
confirmButton.style.backgroundColor = '#007bff';
confirmButton.style.color = '#fff';
confirmButton.style.border = 'none';
confirmButton.style.borderRadius = '5px';
confirmButton.style.cursor = 'pointer';
confirmButton.style.float = 'right';
modalContainer.appendChild(confirmButton);
document.body.appendChild(modalContainer);
confirmButton.addEventListener('click', () => {
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
let anyCheckboxChecked = false;
checkboxes.forEach(checkbox => {
if (checkbox.checked) {
const index = parseInt(checkbox.value, 10);
if (index >= 0 && index < temporaryData.length) {
// 检查 temporaryData[index] 是否为 undefined 或 null
if (temporaryData[index]) {
// 将选中的名称和URL推送到全局变量
names.push(temporaryData[index].name);
urls.push("https://missav.com/playlists/"+temporaryData[index].key);
anyCheckboxChecked = true;
} else {
console.error(`temporaryData[${index}] is undefined or null.`);
}
} else {
console.error(`Index ${index} is out of bounds for temporaryData.`);
}
}
});
document.body.removeChild(modalContainer);
// document.body.removeChild(overlay);
if (anyCheckboxChecked) {
processUrls();
window.showLogContainer();
}
});
}
function ini() {
delayTime= GM_getValue('delayTime', 1000); // 从GM存储中读取延迟时间
shouldReplace=GM_getValue('shouldReplace', false); // 从GM存储中读取状态
saveJson=GM_getValue('saveJson', false); // 从GM存储中读取状态
useDefaultTitle = GM_getValue('useDefaultTitle', true);
pageCount= GM_getValue('pageCount', true);
saveVideoInfo= GM_getValue('saveVideoInfo', false);
saveImage= GM_getValue('saveImage', true);
downloadLogFileA= GM_getValue('downloadLogFileA', false);
savetowebdav= GM_getValue('savetowebdav', false);
webdavUrl = GM_getValue('webdavUrl', '');
webdavUsername = GM_getValue('webdavUsername', '');
webdavPassword = GM_getValue('webdavPassword', '');
}
// 创建设置界面
function createControl(tagName, attributes = {}, styles = {}, parent = document.body) {
const element = document.createElement(tagName);
// 设置属性
for (const key in attributes) {
element[key] = attributes[key];
}
// 设置样式
Object.assign(element.style, styles);
// 添加到父元素
if (parent) {
parent.appendChild(element);
}
return element;
}
function createSettingsUI() {
const modalContainer = createControl('div', {
className: 'settings-modal'
}, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'rgba(255, 255, 255, 1)',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2)',
zIndex: '9999',
padding: '20px',
width: '400px',
maxWidth: '80%'
});
const title = createControl('h2', {
textContent: '设置'
}, {
textAlign: 'center'
});
modalContainer.appendChild(title);
// 创建控件并添加到模态框
const controls = [
{
type: 'checkbox',
id: 'saveImageCheckbox',
label: '下载图片',
checked: GM_getValue('saveImage', true),
onchange: function() { GM_setValue('saveImage', this.checked); }
},
{
type: 'checkbox',
id: 'saveJsonCheckbox',
label: '下载JSON',
checked: GM_getValue('saveJson', false),
onchange: function() { GM_setValue('saveJson', this.checked); }
},
{
type: 'checkbox',
id: 'hdImageCheckbox',
label: '下载高清大图',
checked: GM_getValue('shouldReplace', false),
onchange: function() { GM_setValue('shouldReplace', this.checked); }
},
{
type: 'checkbox',
id: 'defaultTitleCheckbox',
label: '使用网页标题名保存',
checked: GM_getValue('useDefaultTitle', true),
onchange: function() { GM_setValue('useDefaultTitle', this.checked); }
},
{
type: 'checkbox',
id: 'saveVideoInfoCheckbox',
label: '下载视频信息',
checked: GM_getValue('saveVideoInfo', false),
onchange: function() { GM_setValue('saveVideoInfo', this.checked); }
},
{
type: 'checkbox',
id: 'pageCountCheckbox',
label: '自定义抓取页数',
checked: GM_getValue('pageCount', true),
onchange: function() { GM_setValue('pageCount', this.checked); }
},
{
type: 'checkbox',
id: 'downloadLogFileA',
label: '保存下载日志',
checked: GM_getValue('downloadLogFileA', false),
onchange: function() { GM_setValue('downloadLogFileA', this.checked); }
},
{
type: 'checkbox',
id: 'savetowebdav',
label: '上传JSON到WebDav',
checked: GM_getValue('savetowebdav', false),
onchange: function() { GM_setValue('savetowebdav', this.checked); }
},
{
type: 'number',
id: 'delayInput',
label: '设置延迟(毫秒)',
value: GM_getValue('delayTime', 1000),
placeholder: '设置延迟(毫秒)',
onchange: function() { GM_setValue('delayTime', this.value); },
style: {
width: '1px' // 设置输入框宽度为100%
}
},
{
type: 'text',
id: 'webdavUrlInput',
label: 'WebDAV 网址',
value: GM_getValue('webdavUrl', ''),
placeholder: '输入WebDAV网址',
onchange: function() { GM_setValue('webdavUrl', this.value); }
},
{
type: 'text',
id: 'webdavUsernameInput',
label: 'WebDAV 账号',
value: GM_getValue('webdavUsername', ''),
placeholder: '输入WebDAV账号',
onchange: function() { GM_setValue('webdavUsername', this.value); }
},
{
type: 'text',
id: 'webdavPasswordInput',
label: 'WebDAV 密码',
value: GM_getValue('webdavPassword', ''),
placeholder: '输入WebDAV密码',
onchange: function() { GM_setValue('webdavPassword', this.value); }
}
];
controls.forEach(control => {
const input = createControl('input', {
type: control.type,
id: control.id,
checked: control.checked,
value: control.value,
placeholder: control.placeholder,
onchange: control.onchange
}, {
marginRight: '10px'
});
const label = createControl('label', {
textContent: control.label,
htmlFor: control.id
}, {
fontSize: '14px',
marginLeft: '5px'
});
modalContainer.appendChild(input);
modalContainer.appendChild(label);
modalContainer.appendChild(createControl('br'));
});
// 关闭按钮
const closeButton = createControl('button', {
textContent: '关闭',
onclick: () => {
ini();
WebDAVManager.updateConfig(webdavUrl, webdavUsername, webdavPassword);
document.body.removeChild(modalContainer);
}
}, {
marginTop: '10px',
padding: '10px 20px',
fontSize: '16px',
backgroundColor: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
float: 'right'
});
modalContainer.appendChild(closeButton);
// 将模态框添加到页面
document.body.appendChild(modalContainer);
}
// 调用示例
const WebDAVManager = (function() {
// WebDAV 配置
let url = webdavUrl;
let username = webdavUsername;
let password =webdavPassword;
// 通用 GM_xmlhttpRequest 封装函数
function GM_xhr({ path = '/', method, success, fail, headers = {}, data, ...config }) {
return new Promise(resolve => {
GM_xmlhttpRequest({
url: url + path,
method,
...config,
headers: {
'Connection': 'Keep-Alive', // 保持连接
'User-Agent': 'Mozilla/5.0', // 用户代理
'Authorization': 'Basic ' + btoa(username + ':' + password), // 基本认证
...headers
},
data,
onload: xhr => {
if (xhr.status >= 200 && xhr.status < 300) {
if (success) success(xhr);
} else {
if (fail) fail(xhr);
}
resolve(xhr);
},
onerror: xhr => {
if (fail) fail(xhr);
resolve(xhr);
}
});
});
}
//登录
async function login() {
let retryCount = 2; // 设置重试次数为2次
while (retryCount > 0) {
// 构建登录请求
const LOGIN = {
method: 'PROPFIND', // 使用 PROPFIND 方法检查根目录
path: retryCount === 2 ? '/' : '', // 根据重试次数设置 path
headers: {
'Depth': '1',
'Authorization': 'Basic ' + btoa(username + ':' + password),
'Connection': 'Keep-Alive', // 保持连接
'User-Agent': 'Mozilla/5.0' // 用户代理
}
};
// 发起登录请求
const loginResponse = await GM_xhr(LOGIN);
// 判断登录结果
if (loginResponse.status === 207) {
console.log('登录成功');
// 登录成功后,可以执行其他操作
showModal("Webdav登录成功!",1000);
return true; // 返回登录成功标志
} else {
console.error('登录失败:', loginResponse.status);
if (retryCount === 1) {
showModal("Webdav登录失败!" + loginResponse.status,1000);
return false; // 返回登录失败标志
} else {
retryCount--; // 减少重试次数
}
}
}
// 如果重试次数用尽仍未登录成功,执行其他操作(可根据实际情况添加代码)
console.error('重试次数用尽,登录失败');
showModal("Webdav登录失败!重试次数用尽",1000);
return false;
}
//刷新
function updateConfig(newUrl, newUsername, newPassword) {
if (newUrl && newUsername && newPassword) {
url = newUrl;
username = newUsername;
password = newPassword;
// 在这里调用登录函数
login();
} else {
console.error('WebDAV 配置信息不完整');
}
}
// 获取 WebDAV 中指定路径下的所有文件和文件夹
async function listFilesAndFolders(folderName) {
const path = folderName.endsWith('/') ? folderName : folderName + '/';
const PROPFIND = {
method: 'PROPFIND',
headers: {
'Depth': '1'
},
success: (xhr) => {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xhr.responseText, 'text/xml');
const responses = xmlDoc.getElementsByTagNameNS('DAV:', 'response');
let fileCount = 0;
//let fileList = `<div class="custom-hr"></div>`;
let fileList = "";
for (let i = 0; i < responses.length; i++) {
const href = decodeURIComponent(responses[i].getElementsByTagNameNS('DAV:', 'href')[0].textContent.trim());
const displayName = href.substring(href.lastIndexOf('/') + 1);
const isCollection = responses[i].getElementsByTagNameNS('DAV:', 'collection').length > 0;
if (!isCollection) {
fileList += `
<li class="file-item">
<input type="checkbox" name="fileCheckbox" class="file-checkbox">
<span>${fileCount + 1}.</span> <!-- 文件序号 -->
<a href="#" data-display-name="${displayName}" onclick="WebDAVManager.downloadAndDisplayFile('${folderName}', '${displayName}')" class="file-link">${displayName}</a> <!-- 文件名链接 -->
<div class="file-actions">
<a href="#" onclick="WebDAVManager.renameFile('${folderName}', '${displayName}')" class="file-action">更名</a>
<a href="#" onclick="WebDAVManager.deleteFile('${folderName}', '${displayName}')" class="file-action">删除</a>
<a href="#" onclick="WebDAVManager.downloadFile('${folderName}', '${displayName}')" class="file-action">下载</a>
</div>
<div style="clear: both;"></div> <!-- 清除浮动 -->
</li>
`;
fileCount++;
}
}
/*fileList += `
<div class="custom-hr"></div>
<p>文件数量: ${fileCount}</p>
</ul>
`;*/
fileList += '</ul>';
showDialog(`
<span style="font-size: 24px;">文件夹 ${folderName}</span><br>
<span style="color: green;"><span id="fileCountLabel">文件数量:</span> <span class="file-count">${fileCount}</span></span>
<div class="custom-hr"></div>
`, fileList);
},
fail: (xhr) => {
alert('获取文件列表失败:' + xhr.status);
}
};
await GM_xhr({ ...PROPFIND, path });
}
// 下载文件函数
// 在 WebDAV 中创建新文件夹
async function createFolder(folderName) {
const MKCOL = {
method: 'MKCOL',
path: folderName.endsWith('/') ? folderName : folderName + '/',
success: () => {
alert('文件夹创建成功');
},
fail: (xhr) => {
if (xhr.status === 409) {
// alert('冲突:文件夹可能已经存在或路径不正确');
} else {
//alert('创建文件夹失败:' + xhr.status);
}
}
};
await GM_xhr({ ...MKCOL });
}
// 上传文件到 WebDAV
async function uploadFile(folderName, fileName, fileContent) {
// 检查文件是否已存在
const HEAD = {
method: 'HEAD',
path: folderName + '/' + fileName,
success: () => {
//alert('文件已存在,无需重复上传');
window.addToLog(fileName+'文件存在', 'info');
// 可以在这里执行文件已存在时的逻辑,比如显示提示信息或执行其他操作
},
fail: async (xhr) => {
if (xhr.status === 404) {
// 文件不存在,执行上传操作
const PUT = {
method: 'PUT',
path: folderName + '/' + fileName,
data: fileContent,
success: () => {
window.addToLog(fileName+'文件上传成功', 'info');
},
fail: (xhr) => {
window.addToLog(fileName+'文件上传失败', 'error');
}
};
await GM_xhr({ ...PUT });
} else {
window.addToLog(fileName+'文件检查上传失败', 'error');
}
}
};
await GM_xhr({ ...HEAD });
}
// 重命名文件
async function renameFile(folderName, oldFileName) {
// 弹出对话框让用户输入新文件名
const newFileName = prompt(`请输入 ${oldFileName} 的新文件名:`);
if (!newFileName) {
alert('未输入新文件名,操作已取消。');
return;
}
const encodedOldFileName = encodeURIComponent(oldFileName); // 对旧文件名进行编码
const encodedNewFileName = encodeURIComponent(newFileName+ '.json'); // 对新文件名进行编码
const sourcePath = folderName + '/' + encodedOldFileName;
const destinationPath = folderName + '/' + encodedNewFileName;
const MOVE = {
method: 'MOVE',
path: sourcePath,
headers: {
'Destination': url + destinationPath,
'Overwrite': 'T' // 允许覆盖目标文件
},
success: () => {
alert(`文件 ${oldFileName} 已成功重命名为 ${newFileName}`);
listFilesAndFolders(folderName); // 刷新文件列表
},
fail: (xhr) => {
if (xhr.status === 409) {
alert('冲突:文件名可能已存在或路径不正确');
} else {
alert('重命名文件失败:' + xhr.status);
}
}
};
await GM_xhr({ ...MOVE });
}
// 删除文件
async function deleteFile(folderName, fileName) {
let confirmdelete=false;
if(deleteSelected){
confirmdelete =true;
}else{
confirmdelete = confirm(`确定要删除文件 ${fileName} 吗?`);
}
if (confirmdelete) {
try {
await deleteFileFromServer(folderName, fileName);
const fileItem = document.querySelector(`a[onclick*="${fileName}"]`).closest('li');
const checkbox = fileItem.querySelector('input[type="checkbox"]');
if (checkbox && checkbox.checked) {
checkbox.checked = false; // 清除选中状态
checkbox.disabled = true; // 禁止操作
}
markItemDeleted(fileItem);
} catch (error) {
alert('删除文件失败:' + error.message);
}
}
}
function markItemDeleted(fileItem) {
// 将文件名链接标记为已删除
const fileNameLink = fileItem.querySelector('a');
fileNameLink.style.textDecoration = 'line-through';
fileNameLink.removeAttribute('onclick');
fileNameLink.style.pointerEvents = 'none'; // 不可点击
fileNameLink.style.color = 'gray'; // 设置为灰色
// 将后续按钮标记为已删除
const actionButtons = fileItem.querySelectorAll('div > a');
actionButtons.forEach(button => {
button.style.textDecoration = 'line-through';
button.removeAttribute('onclick');
button.style.pointerEvents = 'none'; // 不可点击
button.style.color = 'gray'; // 设置为灰色
});
}
// 实际删除文件的函数
async function deleteFileFromServer(folderName, fileName) {
const DELETE = {
method: 'DELETE',
path: folderName + '/' + fileName,
};
return new Promise((resolve, reject) => {
GM_xhr({ ...DELETE, success: resolve, fail: reject });
});
}
// 检查文件夹是否存在
async function checkFolderExists(folderName) {
const HEAD = {
method: 'HEAD',
path: folderName.endsWith('/') ? folderName : folderName + '/',
success: (xhr) => {
if (xhr.status === 200) {
// alert('文件夹存在');
} else if (xhr.status === 404) {
alert('文件夹不存在');
} else {
alert('检查文件夹状态失败:' + xhr.status);
}
},
fail: (xhr) => {
alert('检查文件夹失败:' + xhr.status);
}
};
await GM_xhr({ ...HEAD });
}
// 下载并展示文件
async function downloadFile(folderName, fileName, zip = null) {
return new Promise((resolve, reject) => {
// 发起 GET 请求下载文件内容
const GET = {
method: 'GET',
path: folderName + '/' + fileName,
success: (xhr) => {
const fileContent = xhr.responseText;
const jsonData = JSON.parse(fileContent);
const content = generateJsonIndexContent(jsonData);
if (zip) {
// 如果传入了压缩包实例,则将文件内容添加到压缩包中
const sanitizedFileName = sanitizeFileName(fileName.replace('.json', '')) + '.html';
zip.file(sanitizedFileName, content);
resolve();
} else {
// 否则直接下载文件
const blob = new Blob([content], { type: 'text/html' });
const htmlUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = htmlUrl;
a.download = sanitizeFileName(fileName.replace('.json', '')) + '.html';
a.click();
resolve();
}
},
fail: (xhr) => {
reject(new Error('下载文件失败:' + xhr.status));
}
};
GM_xhr({ ...GET });
});
}
async function downloadAndDisplayFile(folderName, fileName) {
const GET = {
method: 'GET',
path: folderName + '/' + fileName,
success: (xhr) => {
try {
const fileContent = xhr.responseText;
const jsonData = JSON.parse(fileContent);
const content = generateJsonIndexContent(jsonData);
// 打开一个新的浏览器标签页显示内容
const dataUrl = 'data:text/html;charset=utf-8,' + encodeURIComponent(content);
GM_openInTab(dataUrl);
} catch (e) {
alert('解析 JSON 文件失败:' + e.message);
}
},
fail: (xhr) => {
alert('下载文件失败:' + xhr.status);
}
};
await GM_xhr({ ...GET });
}
//我是你爹啊
// 显示对话框
function showDialog(title, fileList, folderName) {
// 添加CSS样式
const style = document.createElement('style');
style.textContent = `
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9998;
}
.close-x-button {
position: absolute;
top: 10px;
right: 10px;
font-size: 24px;
background: none;
border: none;
cursor: pointer;
color: red;
}
.dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
padding: 20px;
border-radius: 10px;
width: 80%;
max-height: 80%;
overflow-y: auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
z-index: 9999;
}
.dialog-title {
margin-bottom: 20px;
}
.dialog-button {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
margin-bottom: 20px;
}
.select-button {
background-color: #007BFF;
color: #fff;
}
.delete-button {
background-color: #dc3545;
color: #fff;
margin-left: 10px;
}
.select-all-button {
background-color: #28a745;
color: #fff;
margin-left: 10px;
}
.download-button {
background-color: #007BFF;
color: #fff;
margin-left: 10px;
}
.search-button {
background-color: #6c757d;
color: #fff;
margin-left: 10px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.search-button:hover {
background-color: #495057;
}
.close-button {
background-color: #007BFF;
color: #fff;
margin-top: 20px;
}
.file-list {
list-style-type: none;
padding: 0;
}
.file-action {
margin-right: 10px;
float: left;
color: blue;
cursor: pointer;
}
.file-actions-container {
float: right;
}
.clear-float {
clear: both;
}
.file-item {
border-bottom: 1px solid #ddd; /* 添加下划线 */
padding: 10px 0; /* 添加上下内边距,使下划线与内容保持一定距离 */
display: flex;
align-items: center;
}
.file-checkbox {
margin-right: 10px;
}
.file-link {
margin-right: 10px;
color: blue;
flex-grow: 1; /* 使链接占据剩余空间 */
}
.file-actions {
display: flex;
gap: 10px; /* 按钮之间的间距 */
}
.file-action {
color: #007BFF;
cursor: pointer;
}
.custom-hr {
height: 1px;
background-color: #007BFF; /* 设置分割线的颜色为蓝色 */
margin: 20px 0; /* 设置分割线的上下间距 */
}
.file-action:hover {
text-decoration: underline;
}
@media (max-width: 600px) {
.dialog-button {
padding: 5px 10px;
font-size: 12px;
margin-bottom: 10px;
}
.delete-button,
.download-button,
.select-all-button,
.search-button {
margin-left: 0;
}
.button-container {
right: 5px;
}
}
`;
document.head.appendChild(style);
// 创建对话框
const dialog = document.createElement('div');
dialog.className = 'dialog';
// 创建遮罩
const overlay = document.createElement('div');
overlay.className = 'overlay';
overlay.onclick = function() {
document.body.removeChild(dialog);
document.body.removeChild(overlay);
};
// 创建右上角的关闭 "X" 按钮
const closeXButton = document.createElement('button');
closeXButton.textContent = '✖';
closeXButton.className = 'close-x-button';
closeXButton.onclick = function() {
document.body.removeChild(dialog);
document.body.removeChild(overlay);
};
dialog.appendChild(closeXButton);
document.addEventListener('click', function(event) {
// 检查点击事件发生的目标元素是否在对话框内部
if (!dialog.contains(event.target) && !overlay.contains(event.target)) {
// 如果点击位置不在对话框内部,则移除对话框和覆盖层
document.body.removeChild(dialog);
document.body.removeChild(overlay);
}
});
// 标题
const titleElement = document.createElement('h1');
titleElement.innerHTML =title;
titleElement.className = 'dialog-title';
dialog.appendChild(titleElement);
const buttonContainer = document.createElement('div');
// 创建“选择”按钮
const selectButton = document.createElement('button');
selectButton.textContent = '选择列表';
selectButton.className = 'dialog-button select-button';
selectButton.onclick = function() {
toggleSelection();
};
buttonContainer.appendChild(selectButton);
// 创建“搜索”按钮
const searchButton = document.createElement('button');
searchButton.textContent = '搜索列表'; // 按钮显示的文本内容
searchButton.className = 'dialog-button search-button'; // 按钮的CSS类名
searchButton.onclick = function() {
if (searchButton.textContent === '搜索列表') {
// 如果当前是搜索状态,则进行搜索
const searchTerm = prompt('请输入搜索内容:'); // 弹出输入框等待用户输入搜索内容
if (searchTerm !== null) { // 用户点击了确定按钮
filterFiles(searchTerm); // 调用过滤文件函数,传入搜索关键词
searchButton.textContent = '回到列表'; // 将搜索按钮文本改为"Back"
}
} else {
// 如果当前是Back状态,则恢复初始文件列表
resetFileList();
}
};
function filterFiles(searchTerm) {
const files = Array.from(fileListContainer.children); // 获取文件列表的所有子元素(文件项)
let matchCount = 0; // 初始化匹配数量为 0
// 遍历文件列表,根据搜索关键词过滤显示符合条件的文件项
files.forEach(file => {
const fileName = file.querySelector('.file-link').textContent.toLowerCase(); // 获取文件名并转换为小写
if (fileName.includes(searchTerm.toLowerCase())) {
file.style.display = ''; // 匹配到的文件项显示
matchCount++; // 匹配数量加一
} else {
file.style.display = 'none'; // 不匹配的文件项隐藏
}
});
// 更新文件数量标签内容
const fileCountLabel = document.getElementById('fileCountLabel');
if (fileCountLabel) {
fileCountLabel.textContent = `搜索数量: `;
}
// 更新包裹文件数量的元素内容
const fileCountElement = document.querySelector('.file-count');
if (fileCountElement) {
fileCountElement.textContent = matchCount;
}
}
// 恢复初始文件列表函数
function resetFileList() {
const files = Array.from(fileListContainer.children); // 获取文件列表的所有子元素(文件项)
files.forEach(file => {
file.style.display = ''; // 显示所有文件项
});
searchButton.textContent = '搜索列表'; // 恢复搜索按钮文本为"搜索"
// 更新文件数量标签内容为 "文件数量:"
const fileCountLabel = document.getElementById('fileCountLabel');
if (fileCountLabel) {
fileCountLabel.textContent = '文件数量:';
}
// 更新包裹文件数量的元素内容为当前文件数量
const fileCountElement = document.querySelector('.file-count');
if (fileCountElement) {
const currentFileCount = files.length; // 获取当前文件数量
fileCountElement.textContent = currentFileCount;
}
}
buttonContainer.appendChild(searchButton);
// 创建“全部”按钮
const selectAllButton = document.createElement('button');
selectAllButton.textContent = '全部选中';
selectAllButton.className = 'dialog-button select-all-button';
selectAllButton.style.display = 'none'; // 初始隐藏
selectAllButton.onclick = function() {
toggleSelectAll(selectAllButton);
};
buttonContainer.appendChild(selectAllButton);
// 创建“删除选中”按钮
const deleteSelectedButton = document.createElement('button');
deleteSelectedButton.textContent = '删除选中';
deleteSelectedButton.className = 'dialog-button delete-button';
deleteSelectedButton.style.display = 'none'; // 初始隐藏
deleteSelectedButton.onclick = function() {
deleteSelectedFiles();
};
buttonContainer.appendChild(deleteSelectedButton);
// 创建“下载选中”按钮
const downloadSelectedButton = document.createElement('button');
downloadSelectedButton.textContent = '下载选中';
downloadSelectedButton.className = 'dialog-button download-button';
downloadSelectedButton.style.display = 'none'; // 初始隐藏
downloadSelectedButton.onclick = function() {
downloadSelectedFiles();
};
buttonContainer.appendChild(downloadSelectedButton);
// 将按钮容器添加到对话框中
dialog.appendChild(buttonContainer);
// 文件列表
const fileListContainer = document.createElement('ul');
fileListContainer.className = 'file-list';
fileListContainer.innerHTML = fileList;
dialog.appendChild(fileListContainer);
// 添加到body
document.body.appendChild(overlay);
document.body.appendChild(dialog);
let selectButtonInitialTop = 0;
// 记录选择按钮的初始位置
if (selectButton) {
selectButtonInitialTop = selectButton.offsetTop;
}
// 监听对话框的滚动事件
dialog.addEventListener('scroll', function() {
const buttons = [
{ button: selectButton, offsetHeight: true },
{ button: selectAllButton, offsetHeight: true },
{ button: deleteSelectedButton, offsetHeight: true },
{ button: downloadSelectedButton, offsetHeight: true },
{ button: searchButton, offsetHeight: true }
// 添加更多按钮对象,如果有的话
];
if (buttons.every(buttonObj => buttonObj.button)) {
const dialogRect = dialog.getBoundingClientRect();
const fileListRect = fileListContainer.getBoundingClientRect();
const newButtonTopBase = Math.max(fileListRect.top, dialog.scrollTop);
let newButtonTop = newButtonTopBase;
for (const { button, offsetHeight } of buttons) {
if (button) {
button.style.position = dialog.scrollTop === 0 ? 'static' : 'absolute';
button.style.top = `${newButtonTop}px`;
button.style.right = '10px';
if (offsetHeight) {
newButtonTop += button.offsetHeight;
}
}
}
}
});
function toggleSelectAll(selectAllButton) {
const checkboxes = document.querySelectorAll('input[name="fileCheckbox"]');
const allChecked = Array.from(checkboxes).every(checkbox => checkbox.checked);
checkboxes.forEach(checkbox => {
checkbox.checked = !allChecked;
});
// 切换按钮文本和背景颜色
if (allChecked) {
selectAllButton.textContent = '全部选中';
selectAllButton.style.backgroundColor = ''; // 恢复默认背景颜色
} else {
selectAllButton.textContent = '全部取消选中';
selectAllButton.style.backgroundColor = 'red'; // 改为红色背景
}
}
// 获取所有复选框
const checkboxes = fileListContainer.querySelectorAll('input[type="checkbox"]');
// 初始隐藏复选框
checkboxes.forEach(checkbox => {
checkbox.style.display = 'none';
});
// 复选框处理函数
function toggleSelection() {
checkboxes.forEach(checkbox => {
checkbox.style.display = checkbox.style.display === 'none' ? 'inline-block' : 'none';
checkbox.checked = false; // 取消复选框的选中状态
});
// 切换显示“删除选中”和“下载选中”按钮
deleteSelectedButton.style.display = deleteSelectedButton.style.display === 'none' ? 'inline-block' : 'none';
downloadSelectedButton.style.display = downloadSelectedButton.style.display === 'none' ? 'inline-block' : 'none';
selectAllButton.style.display = selectAllButton.style.display === 'none' ? 'inline-block' : 'none';
selectButton.textContent = downloadSelectedButton.style.display === 'none' ? '选择列表' : '取消选择';
}
}
async function downloadSelectedFiles() {
// 获取所有选中的复选框
const checkboxes = document.querySelectorAll('input[name="fileCheckbox"]:checked');
if (checkboxes.length === 0) {
showModal('没有选中的文件', 2000);
return;
}
// 创建一个 JSZip 实例
const zip = new JSZip();
try {
// 遍历所有选中的复选框
for (const checkbox of checkboxes) {
// 获取文件项信息
const fileItem = checkbox.closest('li');
const fileName = fileItem.querySelector('a[data-display-name]').dataset.displayName;
// 调用 downloadFile 函数,将文件添加到压缩包中
await downloadFile(webdavfold, fileName, zip);
}
// 生成 ZIP 文件
const zipBlob = await zip.generateAsync({ type: 'blob' });
// 创建下载链接并触发下载
const zipUrl = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = zipUrl;
a.download = 'selected_files.zip';
a.click();
console.log('压缩包下载完成');
showModal('下载选中完成:',2000);
} catch (error) {
console.error('下载选中文件失败:', error);
showModal('下载选中文件失败: ' + error.message,2000);
}
}
function deleteSelectedFiles() {
// 获取所有选中的复选框
const selectedCheckboxes = document.querySelectorAll('input[name="fileCheckbox"]:checked');
if (selectedCheckboxes.length === 0) {
alert('没有选中文件');
return;
}
// 弹出确认删除的提示框
const confirmDelete = confirm('确定要删除选中的文件吗?');
if (confirmDelete) {
deleteSelected=true;
selectedCheckboxes.forEach(checkbox => {
const fileItem = checkbox.closest('li');
const deleteButton = fileItem.querySelector('a[onclick*="deleteFile"]');
// 调试日志
console.log('fileItem:', fileItem);
console.log('deleteButton:', deleteButton);
// 获取文件名
const fileName = fileItem.querySelector('a[data-display-name]').dataset.displayName;
// 添加调试日志
console.log('Deleting file:', fileName);
// 触发删除按钮的点击事件
deleteButton.click();
//deleteFile(webdavfold,fileName);
// 将文件项标记为已删除
// markItemDeleted(fileItem);
});
deleteSelected=false;
console.log('删除选中文件');
}
}
// 将 API 方法公开
return {
listFilesAndFolders,
updateConfig,
createFolder,
uploadFile,
renameFile,
deleteFile,
downloadFile,
downloadAndDisplayFile,
checkFolderExists
};
})();
// 将库暴露到全局作用域
unsafeWindow.WebDAVManager = WebDAVManager;
// 示例用法:列出文件和文件夹,创建新文件夹,上传文件,删除文件
if (!url || !username || !password) {
console.error('WebDAV 配置更新失败:缺少 URL、用户名或密码');
return;
}
WebDAVManager.updateConfig(webdavUrl, webdavUsername, webdavPassword);
WebDAVManager.createFolder(webdavfold);
WebDAVManager.checkFolderExists(webdavfold);
//WebDAVManager.uploadFile(webdavfold, 'nidie.txt', '这是 example.txt 的内容');
// WebDAVManager.deleteFile('Missav保存', 'example.txt');
})();