1. 程式人生 > >如何開發一個使用者腳本系列(5)——指令碼三:網易雲課堂下載助手

如何開發一個使用者腳本系列(5)——指令碼三:網易雲課堂下載助手

在這篇文章中,我們將一起學習指令碼 網易雲課堂下載助手 的開發。在正式開始之前,先說一下我認為開發指令碼應該遵循的兩個準則:

  • 功能實現。當你決定要開發一個指令碼的時候,你肯定清楚你的指令碼要實現什麼功能,只有你的指令碼實現了你所描述的功能,才會有更多的人安裝使用,才會有更多的人給你好評;
  • 樣式實現。什麼叫樣式實現?就是你在目標網站中新增的元素,要儘量與原網站的配色,樣式相一致。這一項是非必須的,但我認為是非常重要的。你想想,如果原網站整體是藍色,而你新增的按鈕是紅色,那該有多突兀,有多醜,雖然你的按鈕確實突出了,但別人一看就是山寨,看著會很不舒服。而如果你的按鈕也用它網站的顏色,這樣就會跟原網站已有的元素契合,整體特別自然,做到以假亂真的效果。你的指令碼讓別人用的舒服,別人才更願意給你好評。

需求分析

網易雲課堂 是一個非常不錯的線上學習網站,上面有很多視訊課程提供給我們學習。但是有點遺憾的是,官方在 PC 端並沒有提供視訊的下載功能,而在移動 APP 端可以下載視訊,但是下載的視訊也只能在軟體內部觀看。所以為了更加方便在某些網路不允許的情況下學習,我們可以將視訊資源下載到本地。通過對課程結構的觀察,我們發現一門課程有可能有很多章,每一章有可能有好幾節,那麼我們最好既提供單個視訊下載功能,也提供批量下載功能,這樣能滿足更多人的需求。官方原版和我們要實現的最終效果分別如下圖:

功能實現

在開始編寫程式碼之前,需要說明的是,要寫這種資源下載類的指令碼,必須確保提前在網頁上查看了各個網路請求,能夠通過介面請求的方式拿到資源的 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 宣告。

// @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() 方法向陣列末尾新增一個元素。

    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 迴圈裡面。

        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,所以我們在點選事件裡面需要拿到被點選的課時資訊。我們在後面下載視訊時,需要檔案儲存路徑和檔名,所以我們在點選事件裡面將這兩個值拼接好,並傳遞給後面的函式。在進行點選操作時,要注意事件冒泡和事件捕獲。

    $('.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() 來進行網路請求。每個介面需要的引數都是從網頁中觀察得到的。由於視訊可能提供不止一種格式,不止一種清晰度,所以我們在後面會新增一個設定按鈕讓使用者可以選擇下載哪種格式,哪種清晰度的視訊。

    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() 取出資料,這樣使用者只需要設定一次,以後一直有效,並且指令碼更新之後也有效。

    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 視訊解析 的開發過程進行介紹。