// ==UserScript==
// @name dowload from lurl&myppt automatically
// @name:zh-CN 从lurl&myppt自动下载
// @name:zh-TW 從lurl&myppt自動下載
// @namespace org.jw23.dcardtools
// @version 0.1.2
// @description It will download the media from lurl and myppt automatically
// @description:zh-TW 從lurl和mypppt自動下載媒體
// @description:zh-CN 从lurl和myppt自动下载媒体
// @author jw23
// @match https://lurl.cc/*
// @match https://myppt.cc/*
// @match https://risu.io/*
// @run-at document-start
// @grant GM_download
// @grant GM.xmlHttpRequest
// @grant unsafeWindow
// @connect lurl.cc
// @connect risu.io
// @connect myppt.cc
// @license CC BY-NC
// ==/UserScript==
// type: image? video?
const urlGuards = [
{
guard: (url) => {
return url && /lurl\.cc\/\d+/.test(url)
},
extractFilename: (url) => {
let splices = url.split('/')
let [last] = splices.slice(-1)
return last
}
},
{
guard: (url) => {
return url && /myppt\.cc\/\d+/.test(url)
},
extractFilename: (url) => {
let splices = url.split('/')
let [last] = splices.slice(-1)
return last
}
},
{
guard: (url) => {
return url && url.indexOf('storage') !== -1
},
extractFilename: (url) => {
let splices = url.split('/')
let [last] = splices.slice(-1)
return last
}
},
];
const pwdRules = [
{
urlGuad: url => url.indexOf('lurl.cc') != -1,
cssSelector: 'div.col-sm-12 span.login_span',
extractRegx: /\d{4}-(\d{2})-(\d{2})/,
join: (month, year) => `${month}${year}`,
inputSelector: 'input#password'
},
{
urlGuad: url => url.indexOf('myppt.cc') != -1,
cssSelector: 'div.col-sm-12 span.login_span',
extractRegx: /\d{4}-(\d{2})-(\d{2})/,
join: (month, year) => `${month}${year}`,
inputSelector: 'input#pasahaicsword'
},
];
const videoGuards = [
{
urlGuard: (url) => {
return url.startsWith('https://lurl.cc/') !== -1
},
videoSelector: '.vjs-tech source',
extractFilename: (url) => {
let splices = url.split('/')
let [last] = splices.slice(-1)
return last
}
},
{
urlGuard: (url) => {
return url.startsWith('https://myppt.cc/') !== -1
},
videoSelector: '.vjs-tech source',
extractFilename: (url) => {
let splices = url.split('/')
let [last] = splices.slice(-1)
return last
}
},
]
let oldFetch = unsafeWindow.fetch;
function hookFetch(...args) {
oldFetch(...args).then(resp => {
for (let guard of urlGuards) {
console.log("capture the url ", args[0])
if (guard.guard(args[0])) {
downloadFromUrl(args[0], guard.extractFilename(args[0]))
}
return resp;
}
})
}
function hookDrawImage() {
// 1. 获取 Canvas 2D 上下文的原型
const ctxPrototype = unsafeWindow.CanvasRenderingContext2D.prototype;
// 2. 保存原始的 drawImage 方法
const originalDrawImage = ctxPrototype.drawImage;
// 3. 创建并应用我们的钩子函数
ctxPrototype.drawImage = function (...args) {
// 第一个参数就是被绘制的图像源
const imageSource = args[0];
// 检查源是否是 HTMLImageElement 并且有 src 属性
if (imageSource && imageSource instanceof unsafeWindow.HTMLImageElement && imageSource.src) {
const url = imageSource.src;
// 运行你的守卫逻辑
for (const guard of urlGuards) {
if (guard.guard(url)) {
console.log(`[Canvas 劫持] 捕获到 drawImage URL: ${url}`);
downloadFromUrl(url, guard.extractFilename(url));
break;
}
}
}
// 如果源是另一个 Canvas,你也可以处理
else if (imageSource && imageSource instanceof unsafeWindow.HTMLCanvasElement) {
console.log("[Canvas 劫持] 正在绘制另一个 Canvas,无法直接获取 URL。");
}
// 4. **至关重要**: 调用原始的 drawImage 方法,否则页面无法正常显示
return originalDrawImage.apply(this, args);
};
console.log("Canvas.drawImage 劫持成功!");
}
function extractPwd(...pwdRules) {
for (let pr of pwdRules) {
if (pr.urlGuad(document.URL)) {
console.log("Match the site, and run fiiling pwd")
waitUtil(pr.inputSelector).then(ele => {
console.log("find the input ", ele)
let updatedDataNode = document.querySelector(pr.cssSelector);
let updatedData = updatedDataNode.textContent;
let matches = pr.extractRegx.exec(updatedData)
console.log("find the node contains pwd, mathes", updatedData, matches)
if (matches && matches.length > 2) {
console.log(updatedData)
let pwd = pr.join(...matches.slice(1))
console.log(ele, pwd)
ele.value = pwd
}
}).catch(err => {
console.log("filling pwd failed.", err)
})
return
}
}
}
function waitUtil(cssSelector) {
return new Promise((resolve, reject) => {
const observer = new MutationObserver((mutations, obs) => {
const ele = document.querySelector(cssSelector);
if (ele) {
resolve(ele)
obs.disconnect()
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
});
})
}
function downloadFromUrl(url, filename) {
const headers = {
// 关键修复 #1: 添加 Referer 头,告诉服务器我们是从哪个页面来的
'Referer': window.origin,
// 关键修复 #2: 模仿浏览器的 Accept 头,让自己看起来更像一个真正的浏览器
'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
// 为了保险起见,可以把 User-Agent 也明确加上,尽管它看起来是自动继承的
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0'
};
let updateProgress = showProgress();
GM.xmlHttpRequest({
method: 'GET',
url: url,
headers: headers,
responseType: 'blob',
onprogress: progress => {
if (progress.lengthComputable) {
const percent = Math.round((progress.loaded / progress.total) * 100);
updateProgress(percent / 100)
console.log(`[强制模式] 正在获取数据... ${percent}%`);
}
},
onload: response => {
downloadFromBlob(response.response, filename)
},
onerror: error => console.error(`[强制模式] 获取数据失败:`, error)
});
}
function downloadFromBlob(blob, filname) {
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = filname;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(blobUrl);
}
/**
* 使用闭包创建一个 showProgress 函数.
* 这样可以拥有一个私有的 nextTopPosition 变量, 用于在多次调用之间保持状态.
*/
const showProgress = (() => {
// 这个变量存在于闭包中, 不会污染全局作用域, 并且在多次调用之间保持它的值.
let nextTopPosition = 10; // 第一个进度条的初始top值
const BAR_HEIGHT = 8; // 进度条的高度
const PADDING_BETWEEN_BARS = 6; // 进度条之间的垂直间距
// IIFE返回的这个函数才是我们最终使用的 showProgress 函数
return function () {
// --- 这部分是每次调用时实际执行的代码 ---
const progressContainer = document.createElement('div');
progressContainer.style.position = 'fixed';
// 使用闭包中保存的 nextTopPosition 变量
progressContainer.style.top = `${nextTopPosition}px`;
progressContainer.style.right = '10px';
progressContainer.style.width = '250px';
progressContainer.style.height = `${BAR_HEIGHT}px`;
progressContainer.style.backgroundColor = '#e0e0e0';
progressContainer.style.borderRadius = '5px';
progressContainer.style.overflow = 'hidden';
progressContainer.style.zIndex = '999999';
progressContainer.style.transition = 'opacity 0.5s ease-out';
const progressBar = document.createElement('div');
progressBar.style.width = '0%';
progressBar.style.height = '100%';
progressBar.style.backgroundColor = '#4CAF50';
progressBar.style.borderRadius = '5px';
progressBar.style.transition = 'width 0.2s ease-in-out';
progressContainer.appendChild(progressBar);
document.body.appendChild(progressContainer);
// 为下一次调用 showProgress 更新 top 值
// 这个操作会直接修改闭包中的 nextTopPosition
nextTopPosition += BAR_HEIGHT + PADDING_BETWEEN_BARS;
/**
* 设置进度.
* @param {number} progress - 0 到 1 之间的小数.
*/
const setProgress = (progress) => {
// 注意:你原始代码中的 `initTop += (height + 6)` 被移除了
// 因为位置应该在创建时就固定, 而不是在更新进度时改变
const clampedProgress = Math.max(0, Math.min(1, progress));
progressBar.style.width = `${clampedProgress * 100}%`;
// 当进度完成时,自动淡出并移除
if (clampedProgress >= 1) {
setTimeout(() => {
progressContainer.style.opacity = '0';
setTimeout(() => {
progressContainer.remove();
}, 500); // 匹配 transition 的时间
}, 300); // 延迟一会再消失
}
};
// 返回进度设置函数
return setProgress;
};
})();
(function () {
'use strict';
// drawImage of canvas
hookDrawImage()
// fetch
unsafeWindow.fetch = hookFetch;
document.addEventListener('DOMContentLoaded', () => {
extractPwd(...pwdRules)
// watch video
videoGuards.some(guard => {
if (guard.urlGuard(document.URL)) {
waitUtil(guard.videoSelector).then(ele => {
console.log("find element ", ele)
ele && ele.src && downloadFromUrl(ele.src, guard.extractFilename(ele.src))
})
return true
}
return false
})
})
})();