You-Get

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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

})();