Udemy 字幕下载 v3 | Udemy Subtitle Downloader v3

下载 Udemy 的字幕为 vtt 文件 | Download Udemy Subtitle as .vtt file

当前为 2021-03-03 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name:zh      Udemy 字幕下载 v3
// @name         Udemy 字幕下载 v3 | Udemy Subtitle Downloader v3
// @version      3
// @description:zh  下载 Udemy 的字幕为 vtt 文件
// @description     下载 Udemy 的字幕为 vtt 文件 | Download Udemy Subtitle as .vtt file
// @author       Zheng Cheng
// @match        https://www.udemy.com/course/*
// @run-at       document-end
// @grant        unsafeWindow
// @namespace    https://greasyfork.org/users/5711
// ==/UserScript==

// 写于2021-3-2
// [优点]
// 1. 使用门槛比 udemy-dl 低 (不需要用命令行)
// 2. 方便,点击就下载

// [备注]
// 本脚本依赖于 Udemy 的 API,如果哪天 Udemy 进行了改动,那么本程序不能用了是很正常的,修复一下即可。
// 作者邮箱 [email protected]
// 测试/开发环境: 
// macOS Big Sur 11.2.1
// Chrome 版本 88.0.4324.192(正式版本) (x86_64)
// Tampermonkey v4.11
// 不保证其他浏览器可用

// [实现原理]
// 数据从 API 拿, 发请求时带上一个 token 就行,放到请求头里,这个 token 去 Cookie 里面拿 access_token 就行。
// 这是基本概念,具体作法参考下方的代码即可。

(function () {
  'use strict';

  // 全局变量
  var div = document.createElement('div'); // 所有元素都放这里面
  var button1 = document.createElement('button'); // 下载本集的字幕(1个 .vtt 文件)
  var button2 = document.createElement('button'); // 下载整门课程的字幕 (多个 .vtt 文件)
  var button3 = document.createElement('button'); // 下载本集视频
  var title_element = null; // 页面左上角的标题

  // 用法 await sleep(1000) 毫秒
  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 在某节点后面插入新节点
  function insertAfter(newNode, referenceNode) {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  }

  // copy from: https://gist.github.com/danallison/3ec9d5314788b337b682
  // Example downloadString(srt, "text/plain", filename);
  function downloadString(text, fileType, fileName) {
    var blob = new Blob([text], {
      type: fileType
    });
    var a = document.createElement('a');
    a.download = fileName;
    a.href = URL.createObjectURL(blob);
    a.dataset.downloadurl = [fileType, a.download, a.href].join(':');
    a.style.display = "none";
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    setTimeout(function () {
      URL.revokeObjectURL(a.href);
    }, 11500);
  }

  // 获得参数
  function get_args() {
    var ud_app_loader = document.querySelector('.ud-app-loader')
    var args = ud_app_loader.dataset.moduleArgs
    var json = JSON.parse(args)
    return json
  }

  // 获得课程 id
  function get_args_course_id() {
    var json = get_args()
    return json.courseId
  }

  // 获得这一节的 id
  function get_args_lecture_id() {
    var json = get_args()
    return json.initialCurriculumItemId
  }

  // 返回 Cookie 里指定名字的值
  // https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript
  function getCookie(name) {
    return (document.cookie.match('(?:^|;)\\s*' + name.trim() + '\\s*=\\s*([^;]*?)\\s*(?:;|$)') || [])[1];
  }

  // 单个视频的数据 URL
  // 可以传参数也可以不传,不传就当做取当前视频的
  function get_lecture_data_url(param_course_id = null, param_lecture_id = null) {
    // var course_id = '3681012'
    // var lecture_id = '23665120'
    // var example_url = `https://www.udemy.com/api-2.0/users/me/subscribed-courses/3681012/lectures/23665120/?fields[lecture]=asset,description,download_url,is_free,last_watched_second&fields[asset]=asset_type,length,media_license_token,media_sources,captions,thumbnail_sprite,slides,slide_urls,download_urls`
    var course_id = param_course_id || get_args_course_id()
    var lecture_id = param_lecture_id || get_args_lecture_id()
    var url = `https://www.udemy.com/api-2.0/users/me/subscribed-courses/${course_id}/lectures/${lecture_id}/?fields[lecture]=asset,description,download_url,is_free,last_watched_second&fields[asset]=asset_type,length,media_license_token,media_sources,captions,thumbnail_sprite,slides,slide_urls,download_urls`
    return url
  }


  // 一整门课的数据 URL
  function get_course_data_url() {
    var course_id = get_args_course_id()
    // var example_url = "https://www.udemy.com/api-2.0/courses/3681012/subscriber-curriculum-items/?page_size=1400&fields[lecture]=title,object_index,is_published,sort_order,created,asset,supplementary_assets,is_free&fields[quiz]=title,object_index,is_published,sort_order,type&fields[practice]=title,object_index,is_published,sort_order&fields[chapter]=title,object_index,is_published,sort_order&fields[asset]=title,filename,asset_type,status,time_estimation,is_external&caching_intent=True"
    var url = `https://www.udemy.com/api-2.0/courses/${course_id}/subscriber-curriculum-items/?page_size=1400&fields[lecture]=title,object_index,is_published,sort_order,created,asset,supplementary_assets,is_free&fields[quiz]=title,object_index,is_published,sort_order,type&fields[practice]=title,object_index,is_published,sort_order&fields[chapter]=title,object_index,is_published,sort_order&fields[asset]=title,filename,asset_type,status,time_estimation,is_external&caching_intent=True`
    return url
  }

  // 获得一节的数据
  function get_lecture_data(course_id = null, lecture_id = null) {
    return new Promise((resolve, reject) => {
      var access_token = getCookie("access_token")
      var bearer_token = `Bearer ${access_token}`
      fetch(get_lecture_data_url(course_id, lecture_id), {
          headers: {
            'x-udemy-authorization': bearer_token,
            'authorization': bearer_token,
          }
        })
        .then(response => response.json())
        .then(data => {
          resolve(data);
        }).catch(e => {
          reject(e);
        })
    })
  }

  // 获得一整门课的数据
  function get_course_data() {
    return new Promise((resolve, reject) => {
      var access_token = getCookie("access_token")
      var bearer_token = `Bearer ${access_token}`
      fetch(get_course_data_url(), {
          headers: {
            'x-udemy-authorization': bearer_token,
            'authorization': bearer_token,
          }
        })
        .then(response => response.json())
        .then(data => {
          // console.log(data);
          // var captions_array = data.asset.captions;
          // console.log(cations_array);
          resolve(data);
        }).catch(e => {
          reject(e);
        })
    })
  }

  // 转换成安全的文件名
  function safe_filename(string) {
    var s = string
    s = s.replace(':', '-')
    s = s.replace('\'', ' ')
    return s
  }

  // 输入 id
  // 返回那节课的标题
  // await get_lecture_title_by_id(id)
  async function get_lecture_title_by_id(id) {
    var data = await get_course_data()
    var array = data.results;
    for (let i = 0; i < array.length; i++) {
      const r = array[i];
      if (r._class == 'lecture' && r.id == id) {
        var name = `${r.object_index}. ${r.title}`
        return name;
      }
    }
  }

  // 下载当前这一节视频的字幕
  // 如何调用: await parse_lecture_data();
  // 会下载得到一个 .vtt 字幕
  async function parse_lecture_data(course_id = null, lecture_id = null) {
    var data = await get_lecture_data(course_id, lecture_id) // 获得当前这一节的数据
    var lecture_id = data.id; // 获得这一节的 id
    var lecture_title = await get_lecture_title_by_id(lecture_id) // 根据 id 找到标题

    // 遍历数组
    var array = data.asset.captions
    for (let i = 0; i < array.length; i++) {
      const caption = array[i];
      var url = caption.url // vtt 字幕的 URL
      // var locale_id = caption.locale_id // locale_id: "en_US"
      // var label = caption.video_label
      // var filename = `${label}_${safe_filename(lecture_title)}.vtt` // 构造文件名
      var filename = `${safe_filename(lecture_title)}.vtt` // 构造文件名
      save_vtt(url, filename); // 直接保存
    }
  }

  // 保存 vtt
  // 参数: url 是 vtt 文件的 url,访问 url 应该得到文件内容
  // filename 是要保存的文件名
  function save_vtt(url, filename) {
    fetch(url, {})
      .then(response => response.text())
      .then(data => {
        downloadString(data, "text/plain", filename);
      }).catch(e => {
        console.log(e);
      })
  }

  // 把 UI 元素放到页面上
  async function inject_our_script() {
    title_element = document.querySelector('a[data-purpose="course-header-title"]')

    var button1_css = `
      font-size: 14px;
      padding: 1px 12px;
      border-radius: 4px;
      border: none;
      color: black;
    `;

    var button2_css = `
      font-size: 14px;
      padding: 1px 12px;
      border-radius: 4px;
      border: none;
      color: black;
      margin-left: 8px;
    `;

    var div_css = `
      margin-bottom: 10px;
    `;

    button1.setAttribute('style', button1_css);
    button1.textContent = "下载本集字幕"
    button1.addEventListener('click', download_lecture_subtitle);

    button2.setAttribute('style', button2_css);
    var num = await get_course_lecture_number()
    button2.textContent = `下载整门课程的字幕(${num}个文件)`
    button2.addEventListener('click', download_course_subtitle);

    button3.setAttribute('style', button2_css);
    button3.textContent = "下载本集视频"
    button3.addEventListener('click', download_lecture_video);

    div.setAttribute('style', div_css);
    div.appendChild(button1);
    div.appendChild(button2);
    div.appendChild(button3);

    insertAfter(div, title_element);
  }

  // 下载本集字幕
  async function download_lecture_subtitle() {
    await parse_lecture_data();
  }

  // 下载课程全部字幕
  async function download_course_subtitle() {
    var course_id = get_args_course_id();
    var data = await get_course_data()
    var array = data.results;
    for (let i = 0; i < array.length; i++) {
      const result = array[i];
      if (result._class == 'lecture') {
        var lecture_id = result.id;
        await parse_lecture_data(course_id, lecture_id)
        await sleep(800);
      }
    }
  }

  // 下载本集视频
  async function download_lecture_video() {
    button3.textContent = "下载本集视频 (开始下载)"
    var data = await get_lecture_data() // 获得当前这一节的数据
    var lecture_id = data.id; // 获得这一节的 id
    var lecture_title = await get_lecture_title_by_id(lecture_id) // 根据 id 找到标题

    var r = data.asset.media_sources[0]
    // var example = {
    //   "type": "video/mp4",
    //   "src": "https://mp4-a.udemycdn.com/2020-12-04_12-48-10-150cfde997c5ba9f05e5e7d86c813db3/1/WebHD_720p.mp4?lKL6M-V-HXBl9MVKyHqfbP9nVBBFDd6lLLXl7USDCVB63OhpUk722Vt6EW1NlopbdZmF9J_9YZCTOhMrhxj26O1uGmgUqUL4F8e79BxKUeKCnxjTKPo3vA6eRzNAINw4k174S8MaD7ND9b37F_TOs4mxC9BLcUyPTxrSMhDLbjQuWl_P",
    //   "label": "720"
    // }

    var url = r.src // "https://mp4-a.udemycdn.com/2020-12-04_12-48-10-150cfde997c5ba9f05e5e7d86c813db3/1/WebHD_720p.mp4?XquxJGAXiyTc17qxb6iyah_9GXvjHC43UK98UHC3LUkZk7q9yPPll-BJ-5RKz--T9ucjtKOES68m_rZ6vzDZkyEROWwuaoHGFsr3DDuN0AWwk3RpjEo-JNfp98iIaEd_0Vfk0te375rNGtvtCnXibgcZmxDOx4tI5jqFKkl5hVDnwVE7"
    var resolution = r.label // 720 or 1080
    var filename = `${safe_filename(lecture_title)}_${resolution}p.mp4` // 构造文件名
    var type = r.type

    fetch(url)
      .then(res => res.blob())
      .then(blob => {
        downloadString(blob, type, filename);
        button3.textContent = "下载本集视频 (下载完成)"
      });
  }

  // 返回一个整数,代表有多少个视频
  async function get_course_lecture_number() {
    var data = await get_course_data()
    var array = data.results;
    var num = 0
    for (let i = 0; i < array.length; i++) {
      const r = array[i];
      if (r._class == 'lecture') {
        num += 1;
      }
    }
    return num
  }

  // 主入口
  async function main() {
    inject_our_script()
  }

  // 延迟执行,保险一点
  setTimeout(main, 2500);
})();