在这篇文章中,咱们将一块儿学习脚本 网易云课堂下载助手 的开发。在正式开始以前,先说一下我认为开发脚本应该遵循的两个准则: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 视频解析 的开发过程进行介绍。