// ==UserScript==
// @name MissAv批量备份收藏视频
// @namespace https://github.com/100816100886
// @version 1.2.3.52
// @description 从当前missav页面获取图片文件和视频信息,并合并结果后提供下载生成的网页文件
// @license MIT
// @author 人民的勤务员 <[email protected]> & 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
// @supportURL https://github.com/ChinaGodMan/UserScripts/issues
// @homepageURL https://github.com/ChinaGodMan/UserScripts
// ==/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 mediaQuery = window.matchMedia('(max-width: 600px)')
if (mediaQuery.matches) {
modalContainer.style.width = '90%'
modalContainer.style.maxWidth = '90%'
modalContainer.style.padding = '10px'
}
}
// 在页面加载时调用设置界面创建函数
// 调用示例
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;
}
.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;
}
.close-x-button {
background-color: #6c757d;
color: red;
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,
.close-x-button,
.search-button {
margin-left: 0;
}
.button-container {
right: 5px;
}
}
`
document.head.appendChild(style)
// 创建对话框
const dialog = document.createElement('div')
dialog.className = 'dialog'
// 创建遮罩
function close() {
document.body.removeChild(dialog)
}
// 标题
const titleElement = document.createElement('h1')
titleElement.innerHTML = title
titleElement.className = 'dialog-title'
dialog.appendChild(titleElement)
const buttonContainer = document.createElement('div')
const buttonConfigs = [
{ text: '选择列表', className: 'dialog-button select-button', onclick: toggleSelection },
{ text: '搜索列表', className: 'dialog-button search-button', onclick: toggleSearch },
{ text: '关闭窗口', className: 'dialog-button close-x-button', onclick: close },
{ text: '全部选中', className: 'dialog-button select-all-button', onclick: toggleSelectAll },
{ text: '删除选中', className: 'dialog-button delete-button', onclick: deleteSelectedFiles },
{ text: '下载选中', className: 'dialog-button download-button', onclick: downloadSelectedFiles }
// 可以添加更多按钮配置
]
let count = 0 // 计数器,用于控制显示状态
buttonConfigs.forEach(config => {
const button = document.createElement('button')
button.textContent = config.text
button.className = config.className
button.onclick = config.onclick
if (count < 3) {
button.style.display = 'inline-block' // 前两个按钮初始可见
} else {
button.style.display = 'none' // 后面的按钮初始隐藏
}
buttonContainer.appendChild(button)
count++ // 每创建一个按钮,计数器加一
})
// 将按钮容器添加到对话框中
dialog.appendChild(buttonContainer)
// 文件列表
const fileListContainer = document.createElement('ul')
fileListContainer.className = 'file-list'
fileListContainer.innerHTML = fileList
dialog.appendChild(fileListContainer)
// 添加到body
document.body.appendChild(dialog)
let selectButtonInitialTop = 0
let selectAllButton = document.querySelector('.select-all-button')
let searchButton = document.querySelector('.search-button')
let downloadSelectedButton = document.querySelector('.download-button')
let deleteSelectedButton = document.querySelector('.delete-button')
let selectButton = document.querySelector('.select-button')
let closeXButton = document.querySelector('.close-x-button')
let scrollTimeout = 0 // 定义全局变量存储定时器
// 记录选择按钮的初始位置
if (selectButton) {
selectButtonInitialTop = selectButton.offsetTop
}
// 监听对话框的滚动事件
dialog.addEventListener('scroll', function () {
clearTimeout(scrollTimeout)
const buttons = [
{ button: selectButton, offsetHeight: true },
{ button: selectAllButton, offsetHeight: true },
{ button: deleteSelectedButton, offsetHeight: true },
{ button: downloadSelectedButton, offsetHeight: true },
{ button: searchButton, offsetHeight: true },
{ button: closeXButton, offsetHeight: true },
// 添加更多按钮对象,如果有的话
]
scrollTimeout = setTimeout(() => {
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
}
}
}
}
}, 300) // 设置 300 毫秒的超时
})
// 获取所有复选框
const checkboxes = fileListContainer.querySelectorAll('input[type="checkbox"]')
// 初始隐藏复选框
checkboxes.forEach(checkbox => {
checkbox.style.display = 'none'
})
//选中全部
function toggleSelectAll() {
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' // 改为红色背景
}
}
//搜索
//---------------
function toggleSearch() {
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
}
}
//搜索结束
//---------------
// 选择列表
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');
})();