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

在這篇文章中,咱們將一塊兒學習腳本 網易雲課堂下載助手 的開發。在正式開始以前,先說一下我認爲開發腳本應該遵循的兩個準則: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 視頻解析 的開發過程進行介紹。

相關文章
相關標籤/搜索