在這篇文章中,我們將一起學(xué)習(xí)腳本 網(wǎng)易云課堂下載助手 的開發(fā)。在正式開始之前,先說(shuō)一下我認(rèn)為開發(fā)腳本應(yīng)該遵循的兩個(gè)準(zhǔn)則:
- 功能實(shí)現(xiàn)。當(dāng)你決定要開發(fā)一個(gè)腳本的時(shí)候,你肯定清楚你的腳本要實(shí)現(xiàn)什么功能,只有你的腳本實(shí)現(xiàn)了你所描述的功能,才會(huì)有更多的人安裝使用,才會(huì)有更多的人給你好評(píng);
- 樣式實(shí)現(xiàn)。什么叫樣式實(shí)現(xiàn)?就是你在目標(biāo)網(wǎng)站中添加的元素,要盡量與原網(wǎng)站的配色,樣式相一致。這一項(xiàng)是非必須的,但我認(rèn)為是非常重要的。你想想,如果原網(wǎng)站整體是藍(lán)色,而你添加的按鈕是紅色,那該有多突兀,有多丑,雖然你的按鈕確實(shí)突出了,但別人一看就是山寨,看著會(huì)很不舒服。而如果你的按鈕也用它網(wǎng)站的顏色,這樣就會(huì)跟原網(wǎng)站已有的元素契合,整體特別自然,做到以假亂真的效果。你的腳本讓別人用的舒服,別人才更愿意給你好評(píng)。
需求分析
網(wǎng)易云課堂 是一個(gè)非常不錯(cuò)的在線學(xué)習(xí)網(wǎng)站,上面有很多視頻課程提供給我們學(xué)習(xí)。但是有點(diǎn)遺憾的是,官方在 PC 端并沒(méi)有提供視頻的下載功能,而在移動(dòng) APP 端可以下載視頻,但是下載的視頻也只能在軟件內(nèi)部觀看。所以為了更加方便在某些網(wǎng)絡(luò)不允許的情況下學(xué)習(xí),我們可以將視頻資源下載到本地。通過(guò)對(duì)課程結(jié)構(gòu)的觀察,我們發(fā)現(xiàn)一門課程有可能有很多章,每一章有可能有好幾節(jié),那么我們最好既提供單個(gè)視頻下載功能,也提供批量下載功能,這樣能滿足更多人的需求。官方原版和我們要實(shí)現(xiàn)的最終效果分別如下圖:


功能實(shí)現(xiàn)
在開始編寫代碼之前,需要說(shuō)明的是,要寫這種資源下載類的腳本,必須確保提前在網(wǎng)頁(yè)上查看了各個(gè)網(wǎng)絡(luò)請(qǐng)求,能夠通過(guò)接口請(qǐng)求的方式拿到資源的 URL,并且下載下來(lái)的資源是有效的,否則只會(huì)白忙活一場(chǎng)。就像在這個(gè)腳本中,不支持收費(fèi)視頻的下載,因?yàn)槭召M(fèi)視頻進(jìn)行了加密,下載下來(lái)也是不能播放的。我們要將按鈕添加到課程主頁(yè),通過(guò)觀察,課程主頁(yè)的 URL 形式為: https://study.163.com/course/courseMain.htm?courseId=xxx,我們用 @match 匹配。在腳本編寫過(guò)程中會(huì)用到 jQuery,所以我們使用 @require 引入 jQuery 庫(kù)。我們需要保存用戶設(shè)置的一些數(shù)據(jù),需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求,需要在新 tab 頁(yè)中打開鏈接,還需要使用當(dāng)前網(wǎng)頁(yè)中的變量,所以需要腳本管理器的 GM_getValue()、GM_setValue()、GM_xmlhttpRequest()、GM_openInTab()、unsafeWindow 函數(shù),我們用 @grant 聲明。
// @require https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js
// @match *://study.163.com/course/courseMain.htm?courseId=*
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
通過(guò)查看網(wǎng)絡(luò)請(qǐng)求得知,要獲取視頻的下載地址,需要知道視頻的 id,所以我們要先拿到課程中所有視頻的基本信息。這些基本信息有時(shí)候需要通過(guò)接口獲取,有時(shí)候可以通過(guò)頁(yè)面中的變量得到,需要你耐心的去尋找。這里我們可以通過(guò)頁(yè)面中的變量 courseVo 拿到課程的信息。為了后邊更方便的對(duì)每一節(jié)課程操作,我們把所有的課程信息保存在一個(gè) json 類型的變量里面。最終我們這個(gè)變量保存的課程信息有課程 id,課程名稱,課程價(jià)格,課程每一章節(jié)的信息。每一章節(jié)的信息有章節(jié) id,章節(jié)名稱,每一課時(shí)的信息。每一課時(shí)的信息有課時(shí) id,課時(shí)名稱,課時(shí)類型。為了方便后邊下載時(shí)命名,我們還給每一課時(shí)加了一個(gè)編號(hào)。在JavaScript 中,我們可以用 forEach() 方法對(duì) Array 數(shù)組進(jìn)行遍歷,可以用 push() 方法向數(shù)組末尾添加一個(gè)元素。
var course_info = {'course_id': {},'course_name': {},'chapter_info': [],'course_price': {}}; //保存課程信息的變量
function getCourseInfo(){ //獲取課程信息
var courseVo = unsafeWindow.courseVo;
course_info.course_id = courseVo.id; //課程 id
course_info.course_name = courseVo.name.replace(/:|\?|\*|"|<|>|\|/g," "); //課程名稱
course_info.course_price = courseVo.price; //課程價(jià)格
var chapter = courseVo.chapterDtos; //課程章節(jié)
chapter.forEach(function(val,index){
var chapter = {'chapter_id': val.id,'chapter_name': val.name.replace(/:|\?|\*|"|<|>|\|/g," "),'lesson_info': []}; //保存章節(jié)信息的變量
var lessonDtos = val.lessonDtos;
lessonDtos.forEach(function(val,index){
var lesson = {'keshi':val.ksstr,'lesson_id':val.id,'lesson_name':val.lessonName.replace(/:|\?|\*|"|<|>|\|/g," "),'lesson_type':val.lessonType}; //保存課時(shí)信息的變量
chapter.lesson_info.push(lesson);
});
course_info.chapter_info.push(chapter);
});
if(course_info.course_price > 0){
return false;
}else{
return true;
}
}
拿到課程信息之后,我們先在頁(yè)面中每一節(jié)課時(shí)上面添加一個(gè)下載按鈕,用來(lái)下載當(dāng)前選中的課時(shí)。我們希望我們添加的 下載 按鈕和當(dāng)前已有的 開始學(xué)習(xí) 按鈕的字體大小,字體顏色,背景色都保持一致,所以我們先通過(guò) getStyle() 方法拿到開始學(xué)習(xí)按鈕的樣式,然后在創(chuàng)建下載按鈕時(shí)賦值給下載按鈕。因?yàn)槲覀円獮槊恳徽n時(shí)都添加一個(gè)下載按鈕,所以創(chuàng)建元素的代碼應(yīng)該寫在 for 循環(huán)里面。
var ksbtn = document.getElementsByClassName('ksbtn')[0];
var ksbtn_style = 'display:' + getStyle(ksbtn,'display') + ';width:' + getStyle(ksbtn,'width') + ';background-position:' + getStyle(ksbtn,'background-position') + ';margin-top:' + getStyle(ksbtn,'margin-top') + ';';
var ksbtn_span = ksbtn.firstChild;
var ksbtn_span_style = 'display:' + getStyle(ksbtn_span,'display') + ';text-align:' + getStyle(ksbtn_span,'text-align') + ';background:' + getStyle(ksbtn_span,'background') +
';width:' + getStyle(ksbtn_span,'width') + ';font-size:' + getStyle(ksbtn_span,'font-size') + ';height:' + getStyle(ksbtn_span,'height') + ';line-height:' +
getStyle(ksbtn_span,'line-height') + ';color:' + getStyle(ksbtn_span,'color') + ';background-position:' + getStyle(ksbtn_span,'background-position') + ';';
var allNodes = document.getElementsByClassName("section");
for (var i = 0;i < allNodes.length;i ++) {
var download_button = document.createElement("a");
var style = 'display:block;text-align:center;padding-left:10px;width:58px;font-size:12px;height:34px;line-height:33px;color:#fff;background-position:-40px 0px;';
download_button.innerHTML = "<span>下載</span>";
download_button.className = "f-fr j-hovershow download-button";
download_button.style = ksbtn_style;
download_button.lastChild.style = ksbtn_span_style;
allNodes[i].appendChild(download_button);
}
function getStyle(element,cssPropertyName){ //獲取元素樣式
if(window.getComputedStyle){ //如果支持getComputedStyle屬性(IE9及以上,ie9以下不兼容)
return window.getComputedStyle(element)[cssPropertyName];
} else { //如果支持currentStyle(IE9以下使用),返回
return element.currentStyle[cssPropertyName];
}
}
下載按鈕添加完成后,我們需要對(duì)每一個(gè)按鈕進(jìn)行點(diǎn)擊事件的處理。在 jQuery 中,我們使用 each() 方法遍歷選擇的多個(gè)元素。我們?cè)诤筮呥M(jìn)行網(wǎng)絡(luò)請(qǐng)求時(shí),需要視頻 id,所以我們?cè)邳c(diǎn)擊事件里面需要拿到被點(diǎn)擊的課時(shí)信息。我們?cè)诤竺嫦螺d視頻時(shí),需要文件保存路徑和文件名,所以我們?cè)邳c(diǎn)擊事件里面將這兩個(gè)值拼接好,并傳遞給后面的函數(shù)。在進(jìn)行點(diǎn)擊操作時(shí),要注意事件冒泡和事件捕獲。
$('.download-button').each(function(){ //下載按鈕點(diǎn)擊事件
$(this).click(function(event){
loadSetting();
if(course_save_path==""){
alert("請(qǐng)到下載助手的設(shè)置里面填寫文件保存位置");
}else if(aria2_url==""){
alert("請(qǐng)到下載助手的設(shè)置里面填寫 Aria2 地址");
}else{
var data_chapter = event.target.parentNode.parentNode.getAttribute("data-chapter");
var data_lesson = event.target.parentNode.parentNode.getAttribute("data-lesson");
var index = Number(data_lesson);
for(var i = 0;i < Number(data_chapter); i ++){
index = index - course_info.chapter_info[i].lesson_info.length;
}
var lesson = course_info.chapter_info[data_chapter].lesson_info[index];
mylog("選擇的課為【lesson_name: " + lesson.lesson_name + ",lesson_id: " + lesson.lesson_id + ",lesson_type: " + lesson.lesson_type + '】');
var file_name = lesson.keshi + '_' + lesson.lesson_name;
var save_path = course_save_path.replace(/\\/g,'\/') + '/' + course_info.course_name + '/章節(jié)' + (Number(data_chapter) + 1) + '_' + course_info.chapter_info[data_chapter].chapter_name;
if(lesson.lesson_type=="3"){
getTextLearnInfo(lesson,file_name,save_path);
}else{
getVideoLearnInfo(lesson,file_name,save_path);
}
}
event.stopPropagation();
});
});
我們拿到當(dāng)前點(diǎn)擊的課時(shí)信息后,需要請(qǐng)求接口拿到視頻地址。并且還注意到,課程中除了視頻,還有 PDF 文件,所以我們根據(jù)課時(shí)類型分別請(qǐng)求不同的接口。在 jQuery 中,我們可以使用 $.ajax() 來(lái)進(jìn)行網(wǎng)絡(luò)請(qǐng)求。每個(gè)接口需要的參數(shù)都是從網(wǎng)頁(yè)中觀察得到的。由于視頻可能提供不止一種格式,不止一種清晰度,所以我們?cè)诤竺鏁?huì)添加一個(gè)設(shè)置按鈕讓用戶可以選擇下載哪種格式,哪種清晰度的視頻。
function getTextLearnInfo(lesson,file_name,save_path){ // 獲取文檔下載地址
var timestamp = new Date().getTime();
var params = {
"callCount":"1",
"scriptSessionId":"${scriptSessionId}190",
"httpSessionId":match_cookie,
"c0-scriptName":"LessonLearnBean",
"c0-methodName":"getTextLearnInfo",
"c0-id":"0",
"c0-param0":"string:" + lesson.lesson_id,
"c0-param1":"string:" + course_info.course_id,
"batchId":timestamp
}; //接口需要的數(shù)據(jù)
var url = "https://study.163.com/dwr/call/plaincall/LessonLearnBean.getTextLearnInfo.dwr?" + timestamp;
$.ajax({
url:url,
method:'POST',
async: true,
data: params,
success: function (response){
var pdfUrl = response.match(/pdfUrl:"(.*?)"/)[1];
sendDownloadTaskToAria2(pdfUrl,file_name + ".pdf",save_path);
}
});
}
function getVideoUrl(videoId,signature,file_name,save_path){ // 獲取視頻下載地址
var params = {
'videoId':videoId,
'signature':signature,
'clientType':'1'
};
$.ajax({
url:"https://vod.study.163.com/eds/api/v1/vod/video",
method:'POST',
async:true,
data:params,
success:function(response){
var videoUrls = response.result.videos;
var video_url_list = [];
videoUrls.forEach(function(video){
if(video.format == video_format) {
video_url_list.push({'video_format': video.format,'video_quality': video.quality,'video_url': video.videoUrl});
}
});
if(video_url_list.length != 0){
if(video_quality=="2"){
video_download_url = video_url_list[video_url_list.length-1].video_url;
}else{
video_download_url = video_url_list[0].video_url;
}
}
if(video_download_url != ""){
//mylog(video_download_url);
sendDownloadTaskToAria2(video_download_url,file_name + '.' + video_format,save_path);
}
}
});
}
我們獲取到文檔和視頻的下載地址后,就可以進(jìn)行下載了。腳本管理器提供一個(gè)叫做 GM_download() 的方法可以下載文件,但經(jīng)過(guò)嘗試,體驗(yàn)不是太好,尤其是我們后邊還要進(jìn)行批量下載,所以就沒(méi)有采用。這里我們借助的工具是 Aria2,如何通過(guò) Aria2下載文件可以看這篇文章: 如何配置 Aria2 來(lái)進(jìn)行文件下載。我們將獲取到的下載地址和文件名,文件保存路徑都傳給 Aria2,就可以開始下載了。然后我們可以在網(wǎng)站 http://aria2c.com/ 上看到下載進(jìn)度。
function sendDownloadTaskToAria2(download_url,file_name,save_path){
var json_rpc = {
id:'',
jsonrpc:'2.0',
method:'aria2.addUri',
params:[
[download_url],
{
dir:save_path,
out:file_name
}
]
};
GM_xmlhttpRequest({
url:aria2_url,
method:'POST',
data:JSON.stringify(json_rpc),
onerror:function(response){
mylog(response);
},
onload:function(response){
mylog(response);
if (!hasOpenAriac2Tab){
GM_openInTab('http://aria2c.com/',{active:true});
hasOpenAriac2Tab = true;
}
}
});
}
這樣我們單個(gè)視頻下載的功能就實(shí)現(xiàn)了,下面我們要實(shí)現(xiàn)批量下載功能,同時(shí)還要提供給用戶一個(gè)設(shè)置按鈕,讓用戶可以選擇視頻的格式,清晰度,以及填寫文件保存路徑。我們?cè)陧?yè)面頂部創(chuàng)建一個(gè)下載助手按鈕,當(dāng)鼠標(biāo)移入下載助手時(shí),顯示一個(gè)下拉框,下拉框里面有批量下載和設(shè)置,點(diǎn)擊批量下載,我們調(diào)用批量下載的方法,遍歷所有課時(shí),對(duì)每一個(gè)課時(shí)都調(diào)用前面獲取視頻地址的方法,然后下載。點(diǎn)擊設(shè)置,我們彈出一個(gè)設(shè)置頁(yè)面,讓用戶可以進(jìn)行相應(yīng)的設(shè)置。我們要使用 GM_setValue() 將設(shè)置的內(nèi)容進(jìn)行保存,然后在腳本加載的時(shí)候使用 GM_getValue() 取出數(shù)據(jù),這樣用戶只需要設(shè)置一次,以后一直有效,并且腳本更新之后也有效。
function addDownloadAssistant(){ // 添加下載助手按鈕
$(".u-navsearchUI").css("width","224px");
var download_assistant_div = $("<div class='m-nav_item'></div>");
var download_assistant = $("<span>下載助手</span>");
var assistant_div = $("<div class='f-pa' style='line-height:40px;display:none;left:0px;top:60px;width:auto;height:auto;background-color:#fff;color:#666;border:1px solid #ddd;padding:5px 10px;text-align:center;'><div class='arrr f-pa' style='background:url(//s.stu.126.net/res/images/ui/ui_new_yktnav_sprite.png) 9999px 9999px no-repeat;top:-9px;left:40px;width:14px;height:9px;background-position:-187px 0;'></div></div>");
var batch_download = $("<a>批量下載</a>");
var assistant_setting = $("<a>設(shè)置</a>");
assistant_div.append(batch_download).append(assistant_setting);
download_assistant_div.append(download_assistant).append(assistant_div);
$('.m-nav').append(download_assistant_div);
download_assistant_div.mouseover(function(){
assistant_div.show();
});
download_assistant_div.mouseout(function(){
assistant_div.hide();
});
batch_download.click(function(){
assistant_div.hide();
loadSetting();
if(course_save_path==""){
alert("請(qǐng)到下載助手的設(shè)置里面填寫文件保存位置");
}else if(aria2_url==""){
alert("請(qǐng)到下載助手的設(shè)置里面填寫 Aria2 地址");
}else{
batchDownload();
}
});
assistant_setting.click(function(){
assistant_div.hide();
showSetting();
});
}
function batchDownload(){ // 批量下載
course_info.chapter_info.forEach(function(chapter,index){
chapter.lesson_info.forEach(function(lesson){
var file_name = lesson.keshi + '_' + lesson.lesson_name;
var save_path = course_save_path.replace(/\\/g,'\/') + '/' + course_info.course_name + '/章節(jié)' + (index + 1) + '_' + chapter.chapter_name;
if(lesson.lesson_type=="3"){
getTextLearnInfo(lesson,file_name,save_path);
}else{
getVideoLearnInfo(lesson,file_name,save_path);
}
});
});
}
至此,我們就完成了這個(gè)腳本的開發(fā),用戶可以用它來(lái)下載單個(gè)視頻,也可以批量下載視頻,并且可以進(jìn)行設(shè)置,選擇視頻清晰度,視頻格式。至于發(fā)布腳本的流程可以參考文章 如何開發(fā)一個(gè)用戶腳本系列(3)——腳本一:百度首頁(yè)和搜索頁(yè)面添加 Google 搜索框。
總結(jié)
本文對(duì)腳本 網(wǎng)易云課堂下載助手 的開發(fā)過(guò)程進(jìn)行了介紹,如果還有疑問(wèn),可以留言,下一篇文章將對(duì)腳本 視頻跳過(guò)廣告和 VIP 視頻解析 的開發(fā)過(guò)程進(jìn)行介紹。