Last active
September 25, 2024 08:31
-
-
Save bambooGHT/922f5c42c0343ef6bff0962b6a36ba04 to your computer and use it in GitHub Desktop.
筛选文件下载脚本
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name filterDownFile | |
// @namespace https://github.com/bambooGHT | |
// @version 1.3.2 | |
// @description dom改了,改一下代码 | |
// @author bambooGHT | |
// @match https://www.asmr.one/* | |
// @match https://asmr.one/* | |
// @match https://asmr-100.com/* | |
// @match https://www.asmr-100.com/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=asmr.one | |
// @grant GM_xmlhttpRequest | |
// @grant none | |
// @updateURL https://gist.github.com/bambooGHT/922f5c42c0343ef6bff0962b6a36ba04/raw/asmrDownload.user.js | |
// @downloadURL https://gist.github.com/bambooGHT/922f5c42c0343ef6bff0962b6a36ba04/raw/asmrDownload.user.js | |
// ==/UserScript== | |
(() => { | |
const _historyWrap = (type) => { | |
const orig = history[type]; | |
const e = new Event(type); | |
return function () { | |
const rv = orig.apply(this, arguments); | |
e.arguments = arguments; | |
window.dispatchEvent(e); | |
return rv; | |
}; | |
}; | |
history.pushState = _historyWrap('pushState'); | |
let script = document.createElement('script'); | |
script.setAttribute('type', 'text/javascript'); | |
script.src = "https://jimmywarting.github.io/StreamSaver.js/examples/zip-stream.js"; | |
document.documentElement.appendChild(script); | |
let script2 = document.createElement('script'); | |
script2.setAttribute('type', 'text/javascript'); | |
script2.src = "https://cdn.jsdelivr.net/npm/[email protected]/StreamSaver.min.js"; | |
document.documentElement.appendChild(script2); | |
let script3 = document.createElement('script'); | |
script3.setAttribute('type', 'text/javascript'); | |
script3.src = "https://cdn.jsdelivr.net/gh/eligrey/Blob.js/Blob.js"; | |
document.documentElement.appendChild(script3); | |
let script5 = document.createElement('script'); | |
script5.setAttribute('type', 'text/javascript'); | |
script5.src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/ponyfill.min.js"; | |
document.documentElement.appendChild(script5); | |
const darkCss = ` | |
body:not(.body--dark) { | |
.newDOM-dark { | |
border-top: 1px solid #fff !important; | |
} | |
} | |
body.body--dark { | |
.newDOM-dark { | |
background: var(--q-color-dark) !important; | |
color: #fff !important; | |
} | |
.newDOM-dark { | |
border-top: 1px solid hsla(0, 0%, 100%, 0.28) !important; | |
} | |
} | |
`; | |
const style = document.createElement("style"); | |
style.innerHTML = darkCss; | |
document.head.appendChild(style); | |
})(); | |
const newButtom = (innerHTML) => { | |
const name = document.querySelector(".text-weight-regular").innerText.replace(/([\\\/:*?"<>|]|(【.*?】))/g, ' ').trim(); | |
const buttom = document.createElement('button'); | |
buttom.innerHTML = innerHTML; | |
buttom.setAttribute("class", "q-btn q-btn-item non-selectable no-outline q-mt-sm shadow-4 q-mx-xs q-px-sm q-btn--standard q-btn--rectangle bg-cyan text-white q-btn--actionable q-focusable q-hoverable q-btn--wrap q-btn--dense"); | |
buttom.style.lineHeight = '32px'; | |
return { buttom, name }; | |
}; | |
//获取当前rj号数据 | |
const getData = async () => { | |
const RJindex = window.location.pathname.slice(8); | |
const result = await fetch("https://api.asmr.one/api/tracks/" + RJindex); | |
const data = await result.json(); | |
return data; | |
}; | |
//格式化数据 | |
const dataFormat = (data, name, path = "") => { | |
return data.flatMap((value) => { | |
if (value.isDown) { | |
return fileStitch(`${name}/${path}${value.children[0] ? `${value.title}` : ""}`, value.mediaDownloadUrl ? value : value.children); | |
} | |
else { | |
const children = value.children; | |
if (children[0] && isDown(children).some(item => item.isDown === true)) { | |
return dataFormat(children, name, `${path}${value.title}/`); | |
} | |
return []; | |
} | |
}); | |
}; | |
const fileStitch = (name, data) => { | |
return (Array.isArray(data) ? (name += "/", data) : [data]).flatMap(({ children, ...value }) => { | |
const filename = `${name}${value.title}`; | |
return children[0] ? fileStitch(filename, children) : { | |
fileurl: value.mediaDownloadUrl, | |
filename, | |
}; | |
}); | |
}; | |
//打包成压缩包下载 | |
const downloadZip = (name, files) => { | |
const zipFileOutputStream = streamSaver.createWriteStream(name); | |
const progress = updateDownText(files.length); | |
const Files = files.values(); | |
const readableZipStream = new ZIP({ | |
async pull(ctrl) { | |
const data = Files.next(); | |
if (data.done) { | |
ctrl.close(); | |
progress.end(); | |
return; | |
} | |
progress.next(); | |
const { fileurl, filename } = data.value; | |
const process = () => { | |
return new Promise(async (res) => { | |
const downFns = await segmentDown(fileurl, filename); | |
const stream = new ReadableStream({ | |
async start(c) { | |
if (!downFns[0]) { | |
c.close(); | |
res(); | |
return; | |
} | |
const data = await Promise.all(downFns.splice(0, 6).map(p => p())); | |
for (const item of data) { | |
const data = await item.arrayBuffer(); | |
c.enqueue(new Uint8Array(data)); | |
} | |
this.start(c); | |
} | |
}); | |
ctrl.enqueue({ name: filename, stream: () => stream }); | |
}); | |
}; | |
await process(); | |
} | |
}); | |
if (window.WritableStream && readableZipStream.pipeTo) { | |
readableZipStream.pipeTo(zipFileOutputStream); | |
} | |
}; | |
const newDownLoadDOMZIP = () => { | |
const { buttom, name } = newButtom("下载筛选文件(压缩包)"); | |
buttom.onclick = (e) => { | |
const data = dataFormat(Object.values(fileData), name); | |
if (!data.length) return; | |
e.stopPropagation(); | |
downloadZip(name + '.zip', data.flat(Infinity)); | |
}; | |
return buttom; | |
}; | |
const isDown = (arr) => { | |
return arr.flatMap(({ children, ...value }) => { | |
return [value, ...isDown(children)]; | |
}); | |
}; | |
const dataFormat1 = async (dir, arr) => { | |
return (await Promise.all(arr.map(async (value) => { | |
if (value.isDown) return await fileStitch1(dir, value.title, value.mediaDownloadUrl ? value : value.children); | |
else { | |
const children = value.children; | |
if (children[0] && isDown(children).some(item => item.isDown === true)) { | |
const newDir = await getSaveDir(dir, value.title); | |
return await dataFormat1(newDir, children); | |
} | |
return []; | |
} | |
}))).flat(Infinity); | |
}; | |
const fileStitch1 = async (dir, name, arr) => { | |
if (Array.isArray(arr)) { | |
dir = await getSaveDir(dir, name); | |
} else { | |
arr = [arr]; | |
} | |
return await Promise.all(arr.map(async ({ children, ...value }) => { | |
return children[0] ? await fileStitch1(dir, value.title, children) | |
: async () => { | |
await saveFile(dir, value.title, value.mediaDownloadUrl); | |
}; | |
})); | |
}; | |
//获取保存文件的目录api | |
const getSaveDir = async (dir, name) => { | |
const saveDir = await dir.getDirectoryHandle(name, { create: true }); | |
return saveDir; | |
}; | |
//保存文件 | |
const saveFile = async (saveDir, name, url) => { | |
try { | |
await saveDir.getFileHandle(name); | |
return true; | |
} catch (error) { | |
const save = await saveDir.getFileHandle(name, { create: true }); | |
/** @type {WritableStream} */ | |
const writable = await save.createWritable(); | |
const downFns = await segmentDown(url, name); | |
const fn = async () => { | |
const data = await Promise.all(downFns.splice(0, 6).map(p => p())); | |
for (const item of data) { | |
await item.body.pipeTo(writable, { preventClose: true }); | |
} | |
if (downFns[0]) await fn(); | |
}; | |
await fn(); | |
writable.close(); | |
} | |
}; | |
const newDownLoadDOM = () => { | |
const { buttom, name } = newButtom("下载筛选文件"); | |
buttom.onclick = async (e) => { | |
const getDir = await showDirectoryPicker({ mode: "readwrite" }); | |
const saveDir = await getSaveDir(getDir, name); | |
const data = await dataFormat1(saveDir, Object.values(fileData)); | |
if (!data.length) return; | |
const progress = updateDownText(data.length); | |
for (const item of data) { | |
progress.next(); | |
await item(); | |
} | |
progress.end(); | |
}; | |
return buttom; | |
}; | |
const updateDownText = (length) => { | |
let i = 0; | |
let size = 0; | |
const { buttom: progressDOM } = newButtom(""); | |
const filterDown = [...document.querySelectorAll('.q-pa-sm')].at(-1); | |
filterDown.appendChild(progressDOM); | |
updateText = (Size) => { | |
size += Size; | |
progressDOM.innerText = `正在下载 ${i} / ${length} size: ${clacSize(size)}`; | |
}; | |
const next = () => { | |
++i; | |
updateText(0); | |
}; | |
const end = () => { | |
progressDOM.innerText = `筛选的文件已下载完成 ${length} size: ${clacSize(size)}`; | |
updateText = () => { }; | |
setTimeout(() => { | |
filterDown.removeChild(progressDOM); | |
}, 5000); | |
}; | |
return { | |
next, | |
end | |
}; | |
}; | |
// 最大重试次数 | |
const MAX_RETRIES = 6; | |
const SEGMENT_MAX_SIZE = 10 * 1024 * 1024; | |
let updateText = () => { }; | |
const getTotalFileSize = async (url) => { | |
const response = await fetch(url, { method: 'HEAD' }); | |
return parseInt(response.headers.get('Content-Length')); | |
}; | |
const getSpecifyBytes = (start, end) => { | |
return { | |
Range: `bytes=${start}-${end}`, | |
}; | |
}; | |
const segmentDown = async (url, name) => { | |
const totalSize = await getTotalFileSize(url); | |
const segments = []; | |
let startByte = 0; | |
while (startByte < totalSize) { | |
const endByte1 = startByte + SEGMENT_MAX_SIZE; | |
const endByte = Math.min(endByte1, totalSize); | |
const headers = getSpecifyBytes(startByte, endByte); | |
/** @returns {Promise<Response>} */ | |
const downFn = async (retryCount = 0) => { | |
try { | |
const data = await fetch(url, { headers }); | |
const value = endByte1 >= totalSize ? totalSize - (endByte1 - SEGMENT_MAX_SIZE) : SEGMENT_MAX_SIZE; | |
updateText(value); | |
return data; | |
} catch { | |
if (retryCount > MAX_RETRIES) { | |
throw Error("下载失败"); | |
} | |
console.log(`下载失败 正在重试. 文件名:${name}`); | |
return downFn(retryCount + 1); | |
} | |
}; | |
segments.push(downFn); | |
startByte = endByte + 1; | |
} | |
return segments; | |
}; | |
const isdata = () => { | |
setTimeout(async () => { | |
if (fileData.length) { | |
initDOM(); | |
} | |
}, 200); | |
}; | |
const fn = async () => { | |
fileData = await getData(); | |
isdata(); | |
const filterDown = [...document.querySelectorAll('.q-pa-sm')].at(-1); | |
filterDown.appendChild(newDownLoadDOMZIP()); | |
filterDown.appendChild(newDownLoadDOM()); | |
}; | |
window.addEventListener('pushState', async (e) => { | |
const path = window.location.href; | |
if (path.includes('work/RJ') && !path.includes('?')) { | |
await fn(); | |
} | |
}); | |
window.onload = async () => { | |
await fn(); | |
}; | |
const newDOM = (name, info, right) => { | |
return `<div role="listitem" tabindex="0" style="padding-left:${right}px" class="newDOM-dark non-selectable q-card q-item q-item-type row no-wrap q-item--clickable q-link cursor-pointer q-focusable q-hoverable"> | |
<div tabindex="-1" class="q-focus-helper"></div> | |
<div class="q-item__section column q-item__section--avatar q-item__section--side justify-center"> | |
<input type="checkbox"> | |
</div> | |
<div class="q-item__section column q-item__section--main justify-center"> | |
<div class="q-item__label" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"> | |
${name} | |
</div> | |
${info ? | |
`<div class="q-item__label q-item__label--caption text-caption ellipsis"> | |
${info} | |
</div>` | |
: ""} | |
</div> | |
</div>`; | |
}; | |
let fileData = {}; | |
const initDOM = () => { | |
fileData = 遞歸文件(fileData); | |
}; | |
const formatDuration = (duration) => { | |
const hours = Math.floor(duration / 3600); | |
const minutes = Math.floor((duration % 3600) / 60); | |
const seconds = Math.floor(duration % 60); | |
let formattedDuration = ""; | |
if (hours > 0) { | |
formattedDuration += hours.toString().padStart(2, "0") + ":"; | |
} | |
formattedDuration += minutes.toString().padStart(2, "0") + ":" + | |
seconds.toString().padStart(2, "0"); | |
return formattedDuration; | |
}; | |
const 遞歸文件 = (data) => { | |
const DOM = document.getElementById("work-tree"); | |
const domArr = {}; | |
const 遞歸 = (DATA, oldValue = undefined, left = 5) => { | |
for (const item of DATA) { | |
const subName = item.children?.length ? item.children.length + "項目" : (item.duration ? formatDuration(item.duration) : ""); | |
let newElement = document.createElement("div"); | |
newElement.innerHTML = newDOM(item.title, subName, left); | |
newElement = newElement.firstChild; | |
const value = { dom: newElement, title: item.title, parent: oldValue, isDown: false, children: [] }; | |
if (item.mediaDownloadUrl) value.mediaDownloadUrl = item.mediaDownloadUrl; | |
const inputEvent = createDownEvent(value, oldValue); | |
const input = newElement.querySelector("input"); | |
input.onchange = inputEvent; | |
input.onclick = (e) => { | |
e.stopPropagation(); | |
}; | |
newElement.onclick = () => input.click(); | |
if (oldValue) oldValue.children.push(value); | |
else domArr[item.title] = value; | |
if (item.children) { | |
DOM.appendChild(newElement); | |
遞歸(item.children, value, left + 25); | |
} else { | |
DOM.appendChild(newElement); | |
} | |
} | |
}; | |
遞歸(data); | |
return domArr; | |
}; | |
const createDownEvent = (value, parent) => { | |
return (e) => { | |
const open = (Children, boo) => { | |
for (const item of Children) { | |
const inputElement = item.dom.querySelector("input"); | |
inputElement.checked = boo; | |
item.isDown = boo; | |
if (item.children) open(item.children, inputElement.checked); | |
} | |
}; | |
if (value.children) open(value.children, e.target.checked); | |
value.isDown = e.target.checked; | |
isopen(parent); | |
}; | |
}; | |
const isopen = (parent) => { | |
let booArr = []; | |
const fn = (children) => { | |
booArr = []; | |
for (const item of children) { | |
booArr.push(item.isDown); | |
} | |
}; | |
if (parent?.children) { | |
fn(parent.children); | |
do { | |
const is = booArr.every(p => p === true); | |
const parentInput = parent.dom.querySelector("input"); | |
parentInput.checked = is; | |
parent.isDown = is; | |
parent = parent.parent; | |
if (parent?.children) fn(parent.children); | |
} while (parent); | |
} | |
}; | |
const clacSize = (size) => { | |
const aMultiples = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; | |
const bye = 1024; | |
if (size < bye) return size + aMultiples[0]; | |
let i = 0; | |
for (var l = 0; l < 8; l++) { | |
if (size / Math.pow(bye, l) < 1) break; | |
i = l; | |
} | |
return `${(size / Math.pow(bye, i)).toFixed(2)}${aMultiples[i]}`; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment