// ==UserScript==
// @name:zh-CN Udemy 字幕下载 v3
// @name Udemy Subtitle Downloader v3
// @version 3
// @description:zh-CN 下载 Udemy 的字幕为 vtt 文件
// @description 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);
})();