视频下载 acfun 快手 bilibili(已失效) 腾讯(已失效) 优酷(已失效)
// ==UserScript==
// @name You-Get
// @description 视频下载 acfun 快手 bilibili(已失效) 腾讯(已失效) 优酷(已失效)
// @namespace https://greasyfork.org/zh-CN/users/863179
// @version 1.0.0
// @author You (Fixed by AI)
// @match https://www.kuaishou.com/*
// @match https://www.acfun.cn/v/*
// @license MIT
// @grant none
// @run-at document-end
// ==/UserScript==
(() => {
'use strict';
/* 失效匹配项
//@match https://v.qq.com/*
//@match https://www.bilibili.com/video/*
//@match https://v.youku.com/*
*/
// --- UI Manager (进度管理) ---
const UIManager = {
container: null,
logEl: null,
progressEl: null,
init() {
if (this.container) return;
const container = document.createElement('div');
container.style = `
position: fixed; top: 10vh; right: 2vw; z-index: 99999;
background: rgba(28, 28, 30, 0.95); color: #fff;
padding: 15px; border-radius: 12px;
font-family: 'Segoe UI', sans-serif; font-size: 14px;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.1);
min-width: 260px; max-width: 300px;
backdrop-filter: blur(10px);
`;
const title = document.createElement('div');
title.textContent = '📥 You-Get (顺序模式)';
title.style = 'font-weight: bold; margin-bottom: 10px; font-size: 16px; color: #89FF89;';
const status = document.createElement('div');
status.id = 'you-get-status';
status.style = 'margin-bottom: 8px; color: #aaa; min-height: 20px;';
status.textContent = '准备就绪';
const progressContainer = document.createElement('div');
progressContainer.style = 'width: 100%; background: #444; height: 6px; border-radius: 3px; overflow: hidden; margin-bottom: 8px;';
const progress = document.createElement('div');
progress.id = 'you-get-progress';
progress.style = 'width: 0%; height: 100%; background: linear-gradient(90deg, #89FF89, #56CCF2); transition: width 0.1s;';
progressContainer.appendChild(progress);
const log = document.createElement('div');
log.id = 'you-get-log';
log.style = 'max-height: 150px; overflow-y: auto; font-size: 12px; color: #ccc; line-height: 1.4;';
const startBtn = document.createElement('button');
startBtn.textContent = '🚀 开始下载';
startBtn.style = `
width: 100%; margin-top: 10px; padding: 8px; border: none;
background: #89FF89; color: #000; border-radius: 6px;
font-weight: bold; cursor: pointer; transition: transform 0.1s;
`;
startBtn.onclick = () => startDownload();
container.appendChild(title);
container.appendChild(status);
container.appendChild(progressContainer);
container.appendChild(log);
container.appendChild(startBtn);
document.body.appendChild(container);
this.container = container;
this.logEl = log;
this.statusEl = status;
this.progressEl = progress;
},
setStatus(text) {
if (this.statusEl) this.statusEl.textContent = text;
},
setProgress(percent) {
if (this.progressEl) {
const p = Math.min(100, Math.max(0, percent));
this.progressEl.style.width = p + '%';
}
},
log(msg, type = 'info') {
if (!this.logEl) return;
const div = document.createElement('div');
div.style.marginTop = '4px';
if (type === 'error') div.style.color = '#ff6b6b';
if (type === 'success') div.style.color = '#89FF89';
div.textContent = `> ${msg}`;
this.logEl.appendChild(div);
this.logEl.scrollTop = this.logEl.scrollHeight;
}
};
// --- Utils ---
const download = (blob, fileName) => {
const link = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = link;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(link);
};
const safetyParse = (str) => {
try { return JSON.parse(str); } catch (err) { console.log(err);return null; }
};
// 核心下载函数:返回 Uint8Array
const getFile = async (url) => {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const reader = res.body.getReader();
let chunks = [];
while(true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
// 合并 Uint8Arrays
let totalLength = chunks.reduce((acc, val) => acc + val.length, 0);
let result = new Uint8Array(totalLength);
let position = 0;
for (let chunk of chunks) {
result.set(chunk, position);
position += chunk.length;
}
return result;
};
const getUrlsByM3u8 = async (url) => {
const urlObj = new URL(url);
const base = urlObj.origin + urlObj.pathname.substring(0, urlObj.pathname.lastIndexOf('/'));
const res = await fetch(url);
const data = await res.text();
return data.split('\n')
.filter(i => i && !i.startsWith('#'))
.map(i => i.startsWith('http') ? i : (i.startsWith('/') ? urlObj.origin + i : `${base}/${i}`));
};
// --- Site Modules ---
// Acfun
const acfunModule = {
name: 'Acfun',
match: () => location.host.includes('acfun.cn'),
getVideoInfo: async () => {
let retry = 0;
while (!window.pageInfo?.currentVideoInfo?.ksPlayJson && retry < 20) {
await new Promise(r => setTimeout(r, 500));
retry++;
}
const playJson = window.pageInfo?.currentVideoInfo?.ksPlayJson;
if (!playJson) throw new Error('未找到视频信息,请刷新页面');
const m3u8Url = safetyParse(playJson)?.adaptationSet?.[0]?.representation?.[0]?.url;
if (!m3u8Url) throw new Error('解析 m3u8 失败');
const title = document.title || location.pathname.split("/").pop();
return { type: 'm3u8', url: m3u8Url, fileName: title+'.ts' };
}
};
// Bilibili
const bilibiliModule = {
name: 'Bilibili',
match: () => location.host.includes('bilibili.com'),
getVideoInfo: async () => {
const res = await fetch(location.href);
const str = await res.text();
const match = str.match(/window.__playinfo__=([\d\D]+?)<\/script>/);
if (!match || !match[1]) throw new Error('未找到 Bilibili playinfo');
const data = safetyParse(match[1]);
const dash = data?.data?.dash;
if (!dash) throw new Error('Dash 信息解析失败');
const video = dash.video?.sort((a, b) => b.width - a.width)[0];
const audio = dash.audio?.[0];
const files = [];
if (video) files.push({ url: video.baseUrl, fileName: `bilibili_video.${video.mimeType?.split('/')[1] || 'm4v'}` });
if (audio) files.push({ url: audio.baseUrl, fileName: `bilibili_audio.${audio.mimeType?.split('/')[1] || 'm4a'}` });
return { type: 'separate', files };
}
};
// Kuaishou
const kuaishouModule = {
name: 'Kuaishou',
match: () => location.host.includes('kuaishou.com'),
getVideoInfo: async () => {
let retry = 0;
let src = null;
while(retry < 20) {
const v = document.querySelector('video');
if (v && v.src) { src = v.src; break; }
await new Promise(r => setTimeout(r, 500));
retry++;
}
if (!src) throw new Error('未找到快手视频源');
return { type: 'single', url: src, fileName: 'kuaishou.mp4' };
}
};
// QQ Video
const qqModule = {
name: 'QQ',
match: () => location.host.includes('qq.com'),
proxyBody: null,
init() {
try {
const waitPlayer = setInterval(() => {
if (window.__PLAYER__?.pluginMsg) {
clearInterval(waitPlayer);
const originalEmit = window.__PLAYER__.pluginMsg.emit;
window.__PLAYER__.pluginMsg.emit = (...args) => {
if (args[2]?.[0] === "PROXY_HTTP_START" && args[2]?.[1]?.vinfoparam) {
this.proxyBody = JSON.stringify(args[2][1]);
}
return originalEmit.apply(window.__PLAYER__.pluginMsg, args);
};
}
}, 500);
} catch (e) { console.error('QQ Hook Error:', e); }
},
async getVideoInfo() {
if (!this.proxyBody) throw new Error('未捕获到视频参数,请刷新页面并播放视频');
const res = await fetch("https://vd6.l.qq.com/proxyhttp", {
method: "POST",
body: this.proxyBody
});
const json = await res.json();
if (json?.errCode !== 0 || !json.vinfo) throw new Error('获取 vinfo 失败');
const data = safetyParse(json.vinfo);
const m3u8Url = data?.vl?.vi?.sort((a, b) => b.vw - a.vw)?.[0]?.ul?.ui?.[0]?.url;
if (!m3u8Url) throw new Error('解析 m3u8 URL 失败');
return { type: 'm3u8', url: m3u8Url, fileName: 'qq_video.mp4' };
}
};
// Youku
const youkuModule = {
name: 'Youku',
match: () => location.host.includes('youku.com'),
async getVideoInfo() {
const data = window.videoPlayer?.context?.mediaData?.mediaResource?._model;
if (!data) throw new Error('优酷视频信息未加载,请稍候重试');
const stream = data.streamList.sort((a, b) => b.width - a.width)[0];
const m3u8Url = stream?.uri?.HLS;
if (!m3u8Url) throw new Error('未找到 HLS 链接');
return { type: 'm3u8', url: m3u8Url, fileName: `${data.video?.title || 'youku_video'}.ts` };
}
};
// --- Main Controller ---
const modules = [acfunModule, bilibiliModule, kuaishouModule, qqModule, youkuModule];
async function startDownload() {
UIManager.init();
UIManager.setStatus('正在解析...');
UIManager.log('开始获取视频信息...', 'info');
UIManager.setProgress(0);
try {
const module = modules.find(m => m.match());
if (!module) throw new Error('当前网站不支持');
if (module.init) module.init();
const info = await module.getVideoInfo();
UIManager.log(`解析成功: ${info.fileName || info.files?.length + ' files'}`, 'success');
if (info.type === 'single') {
// 单文件下载
UIManager.setStatus('下载中...');
const blob = await getFile(info.url);
download(new Blob([blob]), info.fileName);
} else if (info.type === 'm3u8') {
// M3U8 列表顺序下载
UIManager.setStatus('获取列表...');
const urls = await getUrlsByM3u8(info.url);
const total = urls.length;
UIManager.log(`找到 ${total} 个片段,开始顺序下载...`, 'info');
const chunks = [];
// 顺序循环
for (let i = 0; i < urls.length; i++) {
const url = urls[i];
UIManager.setStatus(`下载分片 ${i + 1}/${total}`);
try {
const chunk = await getFile(url);
chunks.push(chunk);
// 更新进度条
UIManager.setProgress(((i + 1) / total) * 100);
} catch (err) {
// 如果某一片段下载失败,记录错误,填充空数据以防止视频错位,或者直接终止
UIManager.log(`分片 ${i+1} 下载失败: ${err.message}`, 'error');
// 这里选择跳过并填充空数据(可选),或者 throw 终止
// 为了避免视频花屏,通常建议终止或重试。此处简单处理为跳过并记录
chunks.push(new Uint8Array(0));
}
}
UIManager.setStatus('正在合并文件...');
// 顺序合并
const finalBlob = new Blob(chunks);
download(finalBlob, info.fileName);
} else if (info.type === 'separate') {
// Bilibili 分离文件顺序下载
for (const file of info.files) {
UIManager.log(`开始下载: ${file.fileName}`, 'info');
const blob = await getFile(file.url);
download(new Blob([blob]), file.fileName);
}
}
UIManager.setStatus('下载完成');
UIManager.log('所有任务完成!', 'success');
} catch (err) {
console.error(err);
UIManager.setStatus('出错了');
UIManager.log(err.message, 'error');
}
}
window.youget = startDownload;
const btn = document.createElement('div');
btn.innerHTML = '📥';
btn.style = `
position: fixed; top: 10vh; right: 20px; z-index: 99998;
width: 40px; height: 40px; background: #89FF89; color: #000;
border-radius: 50%; display: flex; align-items: center; justify-content: center;
font-size: 20px; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.3);
`;
btn.onclick = () => UIManager.init();
document.body.appendChild(btn);
})();