You-Get

视频下载 acfun 快手 bilibili(已失效) 腾讯(已失效) 优酷(已失效)

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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);

})();