├── README.md ├── common.py └── main.py /README.md: -------------------------------------------------------------------------------- 1 | # Open163-Downloader 2 | 网易公开课下载器,支持多线程,可分别下载视频及字幕 3 | 4 | ## TODO 5 | - [ ] 修复bugs 6 | - [ ] 打包成linux版本使用 7 | - [ ] 添加视频名称模式选择 8 | - [ ] 上传pypi包,重构项目为open163包 9 | 10 | 欢迎在issue里提出反馈和提议,有时间我会修改的。 11 | 12 | ## 下载 13 | [Github发布页](https://github.com/JamesHoi/Open163-Downloader/releases) 14 | [微云下载](https://share.weiyun.com/oYfwIX8F) 15 | 16 | ## 软件使用教程 17 | 输入视频页面链接,范例:https://open.163.com/newview/movie/free?pid=MA32VG4SA&mid=MA35IIC76 18 | ![image](https://user-images.githubusercontent.com/33508232/110079073-9a8da180-7dc3-11eb-9a90-996a2b87d4e7.png) 19 | 20 | 视频页面 21 | [![6eOfmR.png](https://s3.ax1x.com/2021/03/05/6eOfmR.png)](https://imgtu.com/i/6eOfmR) 22 | 23 | 目前测试可达到满速 24 | [![6eOJfS.png](https://s3.ax1x.com/2021/03/05/6eOJfS.png)](https://imgtu.com/i/6eOJfS) 25 | 26 | 27 | ## 功能 28 | 1. 可选择多少个视频同时下载(线程个数) 29 | 2. 可选择字幕和影片是否分开 30 | 3. 可选择视频画质(如果原视频可以选择) 31 | 4. 可分集下载,用逗号隔开 32 | 例如一共有11集,不需要下载第2集和第6集,填1,3-5,7-11 33 | 下载全部填all,下载当前视频填current 34 | 若填写课程链接默认下载全部,视频链接默认下载当前视频 35 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import time 3 | 4 | 5 | def download_file(url, path, name): 6 | r = requests.get(url, stream=True,verify=False) 7 | length = float(r.headers['content-length']) 8 | f = open(path+name, 'wb') 9 | count = 0 10 | count_tmp = 0 11 | time1 = time.time() 12 | for chunk in r.iter_content(chunk_size=512): 13 | if chunk: 14 | f.write(chunk) 15 | count += len(chunk) 16 | if time.time() - time1 > 2: 17 | p = count / length * 100 18 | speed = (count - count_tmp) / 1024 / 1024 / 2 19 | count_tmp = count 20 | print(name + ': ' + format_float(p) + '%' + ' Speed: ' + format_float(speed) + 'M/S') 21 | time1 = time.time() 22 | f.close() 23 | 24 | 25 | def format_float(num): 26 | return '{:.2f}'.format(num) 27 | 28 | 29 | _MAPPING = (u'零', u'一', u'二', u'三', u'四', u'五', u'六', u'七', u'八', u'九', u'十', u'十一', u'十二', u'十三', u'十四', u'十五', u'十六', u'十七',u'十八', u'十九') 30 | _P0 = (u'', u'十', u'百', u'千',) 31 | _S4 = 10 ** 4 32 | def _to_chinese4(num): 33 | assert (0 <= num and num < _S4) 34 | if num < 20: 35 | return _MAPPING[num] 36 | else: 37 | lst = [] 38 | while num >= 10: 39 | lst.append(num % 10) 40 | num = num / 10 41 | lst.append(num) 42 | c = len(lst) # 位数 43 | result = u'' 44 | 45 | for idx, val in enumerate(lst): 46 | val = int(val) 47 | if val != 0: 48 | result += _P0[idx] + _MAPPING[val] 49 | if idx < c - 1 and lst[idx + 1] == 0: 50 | result += u'零' 51 | return result[::-1] -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import sys 4 | import os 5 | import threadpool 6 | from urllib import parse 7 | from common import download_file, _to_chinese4 8 | 9 | def input_default(statement,default): 10 | text = input(statement) 11 | if text == "": text = str(default) 12 | return text 13 | 14 | 15 | def get_url_query(url): 16 | result = parse.urlparse(url) 17 | query = parse.parse_qs(result.query) 18 | if "pid" not in query or "mid" not in query or result.netloc != 'open.163.com': return {} 19 | query["pid"] = query["pid"][0] 20 | query["mid"] = query["mid"][0] 21 | return query 22 | 23 | 24 | def get_current_episode(data,pid): 25 | for index, video in enumerate(data["videoList"]): 26 | if video["plid"] == pid: 27 | return index 28 | 29 | 30 | def download_video(video,path,quality,key,index,no_head): 31 | dic = {"Sd":"标清","Hd":"高清","Shd":"超清","Share":"标清(包含字幕)"} 32 | head = _to_chinese4(index+1)+"、" if not no_head else "" 33 | download_file(video[key],path,head+video["title"]+" - "+dic[quality]+".mp4") 34 | for sub in video["subList"]: 35 | filename = head+video["title"]+" - "+sub["subName"]+"字幕.srt" 36 | download_file(sub["subUrl"],path,filename) 37 | 38 | 39 | def download_videos(data, episode, quality, path, max_workers): 40 | base_key = "mp4"+quality+"Url" 41 | if data["videoList"][episode[0]][base_key] == "": base_key += "Orign" 42 | pool = threadpool.ThreadPool(max_workers) 43 | 44 | # 新建线程池 45 | func_var = [] 46 | for inx, video in enumerate(data["videoList"]): 47 | if inx in episode:func_var.append(([video, path, quality, base_key, inx, len(episode)==1], None)) 48 | reqs = threadpool.makeRequests(download_video, func_var) 49 | [pool.putRequest(req) for req in reqs] 50 | pool.wait() 51 | print("已成功下载全部视频及字幕") 52 | 53 | 54 | def main(): 55 | # 获取输入 56 | print("软件介绍:") 57 | print("注意:软件为网易公开课下载器,并非网易云课堂!!") 58 | print("软件支持多线程,可分别下载视频及字幕,可选择画质(倘若原视频有)。支持分集下载") 59 | print("作者: JamesHoi") 60 | print("Github项目: https://github.com/JamesHoi/Open163-Downloader") 61 | print("") 62 | print("请输入视频页面链接下载") 63 | print("链接范例一:https://open.163.com/newview/movie/free?pid=MF750DHJV&mid=MF751IN70") 64 | print("链接范例二:https://open.163.com/newview/movie/free?pid=MA32VG4SA&mid=MA35IIC76") 65 | while True: 66 | input_url = input("请输入网易公开课视频链接或课程列表:") 67 | query = get_url_query(input_url) 68 | if "pid" in query: 69 | print("[正在获取数据,请等待]") 70 | print("") 71 | r = requests.get("https://c.open.163.com/open/mob/movie/list.do?plid=" + query["pid"]) 72 | content = json.loads(r.content) 73 | data = content["data"] 74 | if content["code"] == 200: break 75 | print("请输入正确的链接,参考范例链接 https://open.163.com/newview/movie/free?pid=MA32VG4SA&mid=MA35IIC76") 76 | 77 | # 初始化下载选择 78 | course_name = data["title"] 79 | print("检测到视频为:"+ course_name) 80 | video_num = len(data["videoList"]) 81 | if video_num != 1: print("检测到一共有%s集" %video_num) 82 | print("[按Enter自动选择默认]") 83 | if video_num != 1: print("[支持分集下载,用逗号隔开,范例:1,3-9,12-15]") 84 | current_episode = get_current_episode(data, query["pid"]) 85 | episode = input_default("下载第几集?(全部下载填all,当前视频填current) (默认:current):","current") if video_num != 1 else [0] 86 | if episode == "all": episode = range(video_num) 87 | elif episode == "current": episode = [current_episode] 88 | elif type(episode) == str: 89 | parts = episode.split(","); episode = [] 90 | for part in parts: 91 | heads = part.split("-") 92 | if len(heads) ==2: episode.extend(list(range(int(heads[0])-1,int(heads[1])))) 93 | else: episode.append(int(heads[0])-1) 94 | max_worker = int(input_default("最多同时多少个视频一起下载(默认:5):",5)) if (video_num != 1 and episode != [current_episode]) else 1 95 | video_type = input_default("请选择影片格式 1.字幕与影片分开(某些影片不能分开字幕) 2.获取已合成字幕影片(只有标清) (默认:1):",1) 96 | if int(video_type) == 2: quality = "Share" 97 | else: 98 | notice = "检测到有画质 "; qualities = []; video = data["videoList"][current_episode-1] 99 | dic = {"Sd":"标清","Hd":"高清","Shd":"超清"} 100 | for key in dic: 101 | if video["mp4"+key+"Url"] != "" or video["mp4"+key+"UrlOrign"] != "": 102 | qualities.append(key) 103 | for index, quality in enumerate(qualities): 104 | notice += "%s.%s "%(index+1,dic[quality]) 105 | index = input_default(notice+"请选择(默认:最高画质):",len(qualities)) 106 | quality = qualities[int(index)-1] 107 | 108 | # 开始下载 109 | if episode == [0]: path = "" 110 | else: 111 | if not os.path.exists(course_name): 112 | os.mkdir(course_name) 113 | path = course_name + "/" 114 | with open(path + "课程介绍.txt", "wb+") as f: 115 | f.write(data["description"].encode()) 116 | print("") 117 | print("[开始下载视频]") 118 | print("[若下载速度太快,可能会看不到下载信息]") 119 | download_videos(data, episode, quality, path, max_worker) 120 | os.system("pause") 121 | 122 | if __name__ == '__main__': 123 | sys.exit(main()) --------------------------------------------------------------------------------