在這篇文章中,咱們將一塊兒學習腳本 網易雲課堂下載助手 的開發。在正式開始以前,先說一下我認爲開發腳本應該遵循的兩個準則:css
網易雲課堂 是一個很是不錯的在線學習網站,上面有不少視頻課程提供給咱們學習。可是有點遺憾的是,官方在 PC 端並無提供視頻的下載功能,而在移動 APP 端能夠下載視頻,可是下載的視頻也只能在軟件內部觀看。因此爲了更加方便在某些網絡不容許的狀況下學習,咱們能夠將視頻資源下載到本地。經過對課程結構的觀察,咱們發現一門課程有可能有不少章,每一章有可能有好幾節,那麼咱們最好既提供單個視頻下載功能,也提供批量下載功能,這樣能知足更多人的需求。官方原版和咱們要實現的最終效果分別以下圖:jquery
在開始編寫代碼以前,須要說明的是,要寫這種資源下載類的腳本,必須確保提早在網頁上查看了各個網絡請求,可以經過接口請求的方式拿到資源的 URL,而且下載下來的資源是有效的,不然只會白忙活一場。就像在這個腳本中,不支持收費視頻的下載,由於收費視頻進行了加密,下載下來也是不能播放的。咱們要將按鈕添加到課程主頁,經過觀察,課程主頁的 URL 形式爲: https://study.163.com/course/courseMain.htm?courseId=xxx
,咱們用 @match
匹配。在腳本編寫過程當中會用到 jQuery,因此咱們使用 @require
引入 jQuery 庫。咱們須要保存用戶設置的一些數據,須要進行網絡請求,須要在新 tab 頁中打開連接,還須要使用當前網頁中的變量,因此須要腳本管理器的 GM_getValue()
、GM_setValue()
、GM_xmlhttpRequest()
、GM_openInTab()
、unsafeWindow
函數,咱們用 @grant
聲明。git
// @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
經過查看網絡請求得知,要獲取視頻的下載地址,須要知道視頻的 id,因此咱們要先拿到課程中全部視頻的基本信息。這些基本信息有時候須要經過接口獲取,有時候能夠經過頁面中的變量獲得,須要你耐心的去尋找。這裏咱們能夠經過頁面中的變量 courseVo
拿到課程的信息。爲了後邊更方便的對每一節課程操做,咱們把全部的課程信息保存在一個 json 類型的變量裏面。最終咱們這個變量保存的課程信息有課程 id,課程名稱,課程價格,課程每一章節的信息。每一章節的信息有章節 id,章節名稱,每一課時的信息。每一課時的信息有課時 id,課時名稱,課時類型。爲了方便後邊下載時命名,咱們還給每一課時加了一個編號。在JavaScript 中,咱們能夠用 forEach()
方法對 Array 數組進行遍歷,能夠用 push()
方法向數組末尾添加一個元素。github
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; //課程價格 var chapter = courseVo.chapterDtos; //課程章節 chapter.forEach(function(val,index){ var chapter = {'chapter_id': val.id,'chapter_name': val.name.replace(/:|\?|\*|"|<|>|\|/g," "),'lesson_info': []}; //保存章節信息的變量 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}; //保存課時信息的變量 chapter.lesson_info.push(lesson); }); course_info.chapter_info.push(chapter); }); if(course_info.course_price > 0){ return false; }else{ return true; } }
拿到課程信息以後,咱們先在頁面中每一節課時上面添加一個下載按鈕,用來下載當前選中的課時。咱們但願咱們添加的 下載
按鈕和當前已有的 開始學習
按鈕的字體大小,字體顏色,背景色都保持一致,因此咱們先經過 getStyle()
方法拿到開始學習按鈕的樣式,而後在建立下載按鈕時賦值給下載按鈕。由於咱們要爲每一課時都添加一個下載按鈕,因此建立元素的代碼應該寫在 for 循環裏面。ajax
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]; } }
下載按鈕添加完成後,咱們須要對每個按鈕進行點擊事件的處理。在 jQuery 中,咱們使用 each()
方法遍歷選擇的多個元素。咱們在後邊進行網絡請求時,須要視頻 id,因此咱們在點擊事件裏面須要拿到被點擊的課時信息。咱們在後面下載視頻時,須要文件保存路徑和文件名,因此咱們在點擊事件裏面將這兩個值拼接好,並傳遞給後面的函數。在進行點擊操做時,要注意事件冒泡和事件捕獲。json
$('.download-button').each(function(){ //下載按鈕點擊事件 $(this).click(function(event){ loadSetting(); if(course_save_path==""){ alert("請到下載助手的設置裏面填寫文件保存位置"); }else if(aria2_url==""){ alert("請到下載助手的設置裏面填寫 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 + '/章節' + (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(); }); });
咱們拿到當前點擊的課時信息後,須要請求接口拿到視頻地址。而且還注意到,課程中除了視頻,還有 PDF 文件,因此咱們根據課時類型分別請求不一樣的接口。在 jQuery 中,咱們可使用 $.ajax()
來進行網絡請求。每一個接口須要的參數都是從網頁中觀察獲得的。因爲視頻可能提供不止一種格式,不止一種清晰度,因此咱們在後面會添加一個設置按鈕讓用戶能夠選擇下載哪一種格式,哪一種清晰度的視頻。api
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 }; //接口須要的數據 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); } } }); }
咱們獲取到文檔和視頻的下載地址後,就能夠進行下載了。腳本管理器提供一個叫作 GM_download()
的方法能夠下載文件,但通過嘗試,體驗不是太好,尤爲是咱們後邊還要進行批量下載,因此就沒有采用。這裏咱們藉助的工具是 Aria2,如何經過 Aria2下載文件能夠看這篇文章: 如何配置 Aria2 來進行文件下載。咱們將獲取到的下載地址和文件名,文件保存路徑都傳給 Aria2,就能夠開始下載了。而後咱們能夠在網站 http://aria2c.com/ 上看到下載進度。數組
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; } } }); }
這樣咱們單個視頻下載的功能就實現了,下面咱們要實現批量下載功能,同時還要提供給用戶一個設置按鈕,讓用戶能夠選擇視頻的格式,清晰度,以及填寫文件保存路徑。咱們在頁面頂部建立一個下載助手按鈕,當鼠標移入下載助手時,顯示一個下拉框,下拉框裏面有批量下載和設置,點擊批量下載,咱們調用批量下載的方法,遍歷全部課時,對每個課時都調用前面獲取視頻地址的方法,而後下載。點擊設置,咱們彈出一個設置頁面,讓用戶能夠進行相應的設置。咱們要使用 GM_setValue()
將設置的內容進行保存,而後在腳本加載的時候使用 GM_getValue()
取出數據,這樣用戶只須要設置一次,之後一直有效,而且腳本更新以後也有效。cookie
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>設置</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("請到下載助手的設置裏面填寫文件保存位置"); }else if(aria2_url==""){ alert("請到下載助手的設置裏面填寫 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 + '/章節' + (index + 1) + '_' + chapter.chapter_name; if(lesson.lesson_type=="3"){ getTextLearnInfo(lesson,file_name,save_path); }else{ getVideoLearnInfo(lesson,file_name,save_path); } }); }); }
至此,咱們就完成了這個腳本的開發,用戶能夠用它來下載單個視頻,也能夠批量下載視頻,而且能夠進行設置,選擇視頻清晰度,視頻格式。至於發佈腳本的流程能夠參考文章 如何開發一個用戶腳本系列(3)——腳本一:百度首頁和搜索頁面添加 Google 搜索框。網絡
本文對腳本 網易雲課堂下載助手 的開發過程進行了介紹,若是還有疑問,能夠留言,下一篇文章將對腳本 視頻跳過廣告和 VIP 視頻解析 的開發過程進行介紹。