Repository: aiguoli/qcourse_scripts Branch: main Commit: b4383aebe8b3 Files: 9 Total size: 27.2 KB Directory structure: gitextract_74ecof8i/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ └── bug_report.md ├── .gitignore ├── README.md ├── downloader.py ├── downloader_m3u8.py ├── logger.py ├── qcourse.py ├── requirements.txt └── utils.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug提交 about: 提交Bug或者其他你看不懂的报错 title: '' labels: '' assignees: '' --- **简单描述** 例:下载单个视频出错,视频连接https://xxxxx/ **环境** Python: 3.10 浏览器: Chrome 操作系统: Windows 11 **日志** ```shell 在这里粘贴日志(log.log),注意不要把自己的身份信息贴上,避免信息泄露 ``` **Screenshots** 有截图的话,可以贴上 **报错** ```shell 这里写报错信息 ``` **补充** ================================================ FILE: .gitignore ================================================ cookies.json /__pycache__/ /.idea/ /courses/ log.log ================================================ FILE: README.md ================================================ ### 腾讯课堂脚本 要学一些东西,但腾讯课堂不支持自定义变速,播放时有水印,且有些老师的课一遍不够看,于是这个脚本诞生了。 项目中可能还有bug,欢迎斧正。 > 2023.2.15测试可用 ### 使用方法 下载代码并解压,确保你安装了python,版本>=3.5 windows: 首先用`EDGE`浏览器( **不要开无痕** )打开[腾讯课堂](https://ke.qq.com),用任意方式登录,然后依次运行下面的命令(保姆级教程) ``` shell cd qcourse_scripts python -m venv qcourse-venv qcourse-venv\scripts\activate pip install -Ur requirements.txt python qcourse.py ``` linux: - `python` -> `python3` - `pip` -> `pip3` ##### Tips - 若登录失效,删除`cookies.json`再重新运行脚本 - **不可以**下载已经过期的课,不然下载下来的都是未解密的课,密匙文件内容为 `{"msg":"用户未登录","retcode":200}` - **不可以**下载直播课,不管过没过期,在网页上看不了的就下载不了 ### 功能 - 模拟登录,支持QQ / 微信,获取cookies - 下载单个视频 - 按章节下载 - 下载整个课程 - 视频下载后自动转换为`mp4`格式(ffmpeg) ================================================ FILE: downloader.py ================================================ from pathlib import Path import requests import datetime from Crypto.Cipher import AES import httpx from logger import logger from utils import ts2mp4 def add_to_16(value): while len(value) % 16 != 0: value += '\0' return str.encode(value) def decrypt(ciphertext, key): iv = ciphertext[:AES.block_size] cipher = AES.new(key, AES.MODE_CBC, iv) plaintext = cipher.decrypt(ciphertext[AES.block_size :]) return plaintext.rstrip(b'\0') def decrypt_file(filename, key): with open(filename, 'rb') as f: ciphertext = f.read() dec = decrypt(ciphertext, key) with open(filename, 'wb') as f: f.write(dec) def get_key(filename): with open(filename, 'rb') as f: key = f.read() return key def download(file_url, file): res = requests.get(file_url) with open(file, 'wb') as f: f.write(res.content) return 0 def progress(percent, width=30, filename=None): left = int(width * percent // 100) right = width - left print( filename, '\r[', '■' * left, ' ' * right, ']', f' {percent:.0f}%', sep='', end='', flush=True, ) def lg_download(file_url, filename, path, headers=None): # 用来下载大文件,有进度条 file = str(Path(path, filename)) response = requests.get(file_url, stream=True, headers=headers) size = 0 chunk_size = 1024 content_size = int(response.headers['content-length']) if response.status_code == 200: print( '正在下载 {filename},大小: {size:.2f} MB'.format( filename=filename, size=content_size / chunk_size / 1024 ) ) with open(file, 'wb') as f: last_show_time = datetime.datetime.now() delta_time = datetime.timedelta(seconds=1) for data in response.iter_content(chunk_size=chunk_size): f.write(data) size += len(data) if datetime.datetime.now() - last_show_time > delta_time: progress(size / content_size * 100, filename=filename) last_show_time = datetime.datetime.now() async def async_download(url, path: Path, filename): client = httpx.AsyncClient( timeout=httpx.Timeout(10, connect=5, read=5, write=5, pool=5) ) filename_ext = filename + '.ts' base_file = path.joinpath(filename_ext) size = 0 async with client.stream('GET', url) as response: content_size = int(response.headers['content-length']) if base_file.exists(): if base_file.stat().st_size == content_size: return with open(base_file, 'wb') as f: current_size = 0 last_show_time = datetime.datetime.now() delta_time = datetime.timedelta(seconds=1) async for chunk in response.aiter_bytes(chunk_size=1024): f.write(chunk) size += len(chunk) if size > current_size: current_size = max(size, current_size) if datetime.datetime.now() - last_show_time > delta_time: progress(size / content_size * 100, filename=filename) last_show_time = datetime.datetime.now() logger.info('Download '+filename) await client.aclose() def _download(url, path: Path, filename): client = httpx.Client(timeout=httpx.Timeout(10, connect=5, read=5, write=5, pool=5)) filename_ext = filename + '.ts' base_file = path.joinpath(filename_ext) size = 0 with client.stream('GET', url) as response: content_size = int(response.headers['content-length']) if base_file.exists() and base_file.stat().st_size == content_size: return with open(base_file, 'wb') as f: current_size = 0 last_show_time = datetime.datetime.now() delta_time = datetime.timedelta(seconds=1) for chunk in response.iter_bytes(): f.write(chunk) size += len(chunk) current_size = size if datetime.datetime.now() - last_show_time > delta_time: progress(current_size / content_size * 100, filename=filename) last_show_time = datetime.datetime.now() async def download_single(ts_url, key_url, filename, path): print(filename, '开始下载') filename = filename.replace('/', '/').replace('\\', '\') file: Path = path.joinpath(filename) final_video_name = file.name.split('.')[0] + '.mp4' if file.parent.joinpath(final_video_name).exists(): print(final_video_name + '已存在!') return await async_download(ts_url, path, filename) # _download(ts_url, path, filename) download(file_url=key_url, file=file) key = get_key(file) decrypt_file(str(file) + '.ts', key) file.unlink() ts2mp4(str(file) + '.ts') print('\n' + filename + ' 下载完成!') ================================================ FILE: downloader_m3u8.py ================================================ import os import requests from pathlib import Path from downloader import ts2mp4, progress def get_m3u8_body(url): print('read m3u8 file:', url) with requests.Session() as session: adapter = requests.adapters.HTTPAdapter( pool_connections=10, pool_maxsize=10, max_retries=10 ) session.mount('http://', adapter) session.mount('https://', adapter) r = session.get(url, timeout=10) return r.text def get_url_list(host, body): lines = body.split('\n') ts_url_list = [] for line in lines: if not line.startswith('#') and line != '': if line.lower().startswith('http'): ts_url_list.append(line) else: ts_url_list.append('%s/%s' % (host, line)) return ts_url_list def _download_ts_file(ts_url_list, file): i = 0 total = len(ts_url_list) for ts_url in ts_url_list: i += 1 r = requests.get(ts_url) with open(file, 'ab') as f: f.write(r.content) progress(i / total * 100) print(f'{file}下载完成') return file def _check_dir(path): if os.path.exists(path): return os.makedirs(path) def get_download_url_list(host, m3u8_url, url_list=None): if url_list is None: url_list = [] body = get_m3u8_body(m3u8_url) ts_url_list = get_url_list(host, body) for url in ts_url_list: if url.lower().endswith('.m3u8'): url_list = get_download_url_list(host, url, url_list) else: url_list.append(url) return url_list def download_ts(m3u8_url, path: Path, filename, begin=0): _check_dir(path) host = m3u8_url[: m3u8_url.rindex('/')] ts_url_list = get_download_url_list(host, m3u8_url)[begin:] print('Total file count:', len(ts_url_list)) return _download_ts_file(ts_url_list, path.joinpath(filename)) def download_m3u8_raw(m3u8_url, path: Path, filename, trash_first): if trash_first: begin = 2 else: begin = 0 ts2mp4(download_ts(m3u8_url, path, filename, begin)) __all__ = ['download_ts'] ================================================ FILE: logger.py ================================================ import logging class Logger(object): def __init__(self): self.logger = logging.getLogger() self.logger.setLevel(logging.INFO) formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') log_file = './log.log' file_handler = logging.FileHandler(log_file) file_handler.setFormatter(formatter) file_handler.setLevel(logging.DEBUG) self.logger.addHandler(file_handler) def debug(self, msg): self.logger.debug(msg) def info(self, msg): self.logger.info(msg) def error(self, msg): self.logger.error(msg) def warn(self, msg): self.logger.warning(msg) def critical(self, msg): self.logger.critical(msg) logger = Logger() ================================================ FILE: qcourse.py ================================================ import asyncio import json import re from pathlib import Path from uuid import uuid1 import browser_cookie3 from requests.utils import dict_from_cookiejar, cookiejar_from_dict import utils from logger import logger from utils import ( print_menu, get_course_from_api, get_download_url_from_course_url, choose_term, get_download_urls, choose_chapter, get_courses_from_chapter, get_chapters_from_file, ) from downloader import download_single from downloader_m3u8 import download_m3u8_raw as m3u8_down BASE_DIR = Path() COURSE_DIR = BASE_DIR.joinpath('courses') if not COURSE_DIR.exists(): COURSE_DIR.mkdir() class QCourse: def login(self): if not self.is_login(): # Todo: 默认用edge,下版本考虑添加浏览器选择 cj = self.get_cookies() self.save_cookies(dict_from_cookiejar(cj)) print('登陆成功!') logger.info(dict_from_cookiejar(cj)) def get_cookies(self): if not self.is_login(): browsers = { 'Chrome': browser_cookie3.chrome, 'FireFox': browser_cookie3.firefox, 'Opera': browser_cookie3.opera, 'Edge': browser_cookie3.edge, 'Chromium': browser_cookie3.Chromium, 'Brave': browser_cookie3.brave, 'Vivaldi': browser_cookie3.vivaldi, 'Safari': browser_cookie3.safari, } print('登录腾讯课堂(https://ke.qq.com),然后选择你安装的浏览器') browser_menu = list(browsers.keys()) print_menu(browser_menu) chosen = int(input('请选择你的浏览器序号:')) return browsers[browser_menu[chosen]](domain_name='ke.qq.com') @staticmethod def is_login(): return Path('cookies.json').exists() @staticmethod def save_cookies(cookies): with open('cookies.json', 'w') as f: json.dump(cookies, f, indent=4) @staticmethod def load_cookie(): cookies = Path('cookies.json') if cookies.exists(): return cookiejar_from_dict(json.loads(cookies.read_bytes())) @staticmethod def clear_cookies(): cookies = Path('cookies.json') if cookies.exists(): cookies.unlink() async def parse_course_url_and_download(video_url, filename=None, path=None): if not path: path = Path('courses') if not filename: filename = str(uuid1()) urls = get_download_url_from_course_url(video_url, -1) if urls[1]: await download_single(urls[0], urls[1], filename, path) else: m3u8_down(urls[0], path, filename, True) async def download_selected_chapter(term_id, filename, chapter_name, courses, cid): tasks = [] for course in courses: path = Path('courses', filename, chapter_name) course_name = course.get('name') file_id = course.get('resid_list') file_id = re.search(r'(\d+)', file_id).group(1) urls = get_download_urls(term_id, file_id, cid=cid) tasks.append( asyncio.create_task( download_single( ts_url=urls[0], key_url=urls[1], filename=course_name, path=path ) ) ) sem = asyncio.Semaphore(3) async with sem: await asyncio.wait(tasks) def main(): menu = ['下载单个视频', '下载课程指定章节', '下载课程全部视频', '退出登录'] print_menu(menu) chosen = int(input('\n输入需要的功能:')) qq_course = QCourse() qq_course.login() # ========================================= if chosen == 0: course_url = input('输入课程链接:') logger.info('URL: '+course_url) asyncio.run(parse_course_url_and_download(course_url)) elif chosen == 1: cid = str(utils.choose_course()) course_name = get_course_from_api(cid) print('获取课程信息成功') info_file = Path(course_name + '.json') term_index, term_id, term = choose_term(info_file) chapter = choose_chapter(term) chapter_name = chapter.get('name').replace('/', '/').replace('\\', '\') courses = get_courses_from_chapter(chapter) logger.info('cid: {},name: {}, term: {}, chapter: {}'.format(cid, course_name, term_id, chapter_name)) print('即将开始下载章节:' + chapter_name) print('=' * 20) chapter_path = COURSE_DIR.joinpath(course_name, chapter_name) if not chapter_path.exists(): chapter_path.mkdir(parents=True) asyncio.run( download_selected_chapter(term_id, course_name, chapter_name, courses, cid) ) elif chosen == 2: cid = str(utils.choose_course()) course_name = get_course_from_api(cid) term_index, term_id, term = choose_term(course_name + '.json') print('获取课程信息成功,准备下载!') logger.info('cid: '+cid) chapters = get_chapters_from_file(course_name + '.json', term_index) for chapter in chapters: chapter_name = chapter.get('name').replace('/', '/').replace('\\', '\') courses = get_courses_from_chapter(chapter) print('即将开始下载章节:' + chapter_name) print('=' * 20) chapter_path = COURSE_DIR.joinpath(course_name, chapter_name) if not chapter_path.exists(): chapter_path.mkdir(parents=True) asyncio.run( download_selected_chapter( term_id, course_name, chapter_name, courses, cid ) ) elif chosen == 3: QCourse.clear_cookies() else: print('请按要求输入!') if __name__ == '__main__': main() ================================================ FILE: utils.py ================================================ import base64 import json import os import re import subprocess import sys import time from pathlib import Path import requests from requests.utils import dict_from_cookiejar, cookiejar_from_dict import json import subprocess from urllib.parse import urlparse, parse_qs from urllib.request import getproxies class API: ItemsUri = 'https://ke.qq.com/cgi-bin/course/get_terms_detail' TokenUri = 'https://ke.qq.com/cgi-bin/qcloud/get_token' MediaUri = 'https://playvideo.qcloud.com/getplayinfo/v2/1258712167/' InfoUri = 'https://ke.qq.com/cgi-bin/identity/info' BasicInfoUri = 'https://ke.qq.com/cgi-bin/course/basic_info?cid={cid}' MiniAppQrcode = 'https://ke.qq.com/cgi-proxy/get_miniapp_qrcode?' LoginState = 'https://ke.qq.com/cgi-proxy/get_login_state?' A2Login = 'https://ke.qq.com/cgi-proxy/account_login/a2_login?' XLogin = 'https://xui.ptlogin2.qq.com/cgi-bin/xlogin?' PtQrShow = 'https://ssl.ptlogin2.qq.com/ptqrshow?' PtQrLogin = 'https://ssl.ptlogin2.qq.com/ptqrlogin?' Check = 'https://ssl.ptlogin2.qq.com/check?' CourseList = 'https://ke.qq.com/cgi-proxy/user/user_center/get_plan_list' VideoRec = 'https://ke.qq.com/cgi-proxy/rec_video/describe_rec_video' DefaultAccount = 'https://ke.qq.com/cgi-proxy/accbind/get_default_account' DEFAULT_HEADERS = {'referer': 'https://ke.qq.com/webcourse/'} CURRENT_USER = {} PROXIES = getproxies() # 当你使用魔法,避免出现check_hostname requires server_hostname def get_course_from_api(cid): # 获取课程信息 # url = 'https://ke.qq.com/cgi-bin/course/basic_info?cid=' + str(cid) url = API.BasicInfoUri.format(cid=cid) response = requests.get(url, headers=DEFAULT_HEADERS, proxies=PROXIES).json() name = ( response.get('result') .get('course_detail') .get('name') .replace('/', '/') .replace('\\', '\') ) with open(name + '.json', 'w') as f: json.dump(response, f, ensure_ascii=False, indent=4) return name def get_terms_from_api(cid, term_id_list): # term_id_list是一个数组,里面是整数格式的term_id params = {'cid': cid, 'term_id_list': term_id_list} response = requests.get(API.ItemsUri, params=params, headers=DEFAULT_HEADERS, proxies=PROXIES).json() return response def get_terms(filename): # 从json文件内获取学期信息 with open(filename, 'r') as f: course_info = json.loads(f.read()).get('result') if course_info.get('course_detail'): terms = course_info.get('course_detail').get('terms') else: terms = course_info.get('terms') return terms def get_chapters_from_file(filename, term_index): # 从json文件内获取章节信息 with open(filename, 'r') as f: course_info = json.loads(f.read()) chapters = ( course_info.get('result') .get('course_detail') .get('terms')[term_index] .get('chapter_info')[0] .get('sub_info') ) return chapters def get_chapters(term): return term.get('chapter_info')[0].get('sub_info') def get_courses_from_chapter(chapter): return chapter.get('task_info') def get_all_courses(): # 下个版本加入该函数,用于获取用户计划内的课程,在脚本运行时不用输入cid了 # count参数最多为10,返回response中end代表有没有下一页 # 考虑到课程一般不会有几百个,所以不做分页处理 def _load_res(r): if r: for i in r.get('map_list'): for j in i.get('map_courses'): res.append({ 'name': j.get('cname'), 'cid': j.get('cid') }) res = [] page = 1 response = requests.get(API.CourseList, params={'page': page, 'count': '10'}, headers=DEFAULT_HEADERS, cookies=load_json_cookies(), proxies=PROXIES).json().get('result') _load_res(response) while response.get('end') == 0: page += 1 response = requests.get(API.CourseList, params={'page': page, 'count': '10'}, headers=DEFAULT_HEADERS, cookies=load_json_cookies(), proxies=PROXIES).json().get('result') _load_res(response) return res def choose_course(): courses = get_all_courses() print('你的账号里有如下课程:') for course in courses: print(str(courses.index(course)) + '. ' + course.get('name')) cid = courses[int(input('请输入要下载的课程序号(回车结束):'))].get('cid') return cid def get_course_url(course): # 传入课程字典,拼接成课程链接 cid = course.get('cid') term_id = course.get('term_id') course_id = course.get('taid') url = 'https://ke.qq.com/webcourse/{}/{}#taid={}&vid={}'.format( cid, term_id, course_id, course.get('resid_list') ) return url def get_all_urls(filename, term_index): chapters = get_chapters_from_file(filename, term_index) result = {} for chapter in chapters: chapter_name = chapter.get('name') courses = get_courses_from_chapter(chapter) chapter_info = {} for course in courses: # 这里跳过了文件类附件下载 # TODO:添加附件下载支持 chapter_info.update({course.get('name'): get_course_url(course)}) result.update({chapter_name: chapter_info}) return result def print_menu(menu): for item in menu: print(str(menu.index(item)) + '. ' + item) def run_shell(shell, retry=True, retry_times=3): cmd = subprocess.Popen( shell, close_fds=True, shell=True, bufsize=1, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, ) if retry and cmd.returncode != 0: time.sleep(1) if retry_times > 0: return run_shell(shell, retry=True, retry_times=retry_times - 1) print('\nShell出现异常,请自行查看课程文件是否转码成功') return cmd.returncode def ts2mp4(file): file = Path(file) ffmpeg = Path('ffmpeg.exe') basename = file.name.split('.ts')[0] file_dir = file.parent output = file_dir.joinpath(basename) cmd = str(ffmpeg) + ' -i "' + str(file) + '" -c copy "' + str(output) + '".mp4' # 这个命令会报错,但是我不熟悉ffmpeg,而且似乎输出视频没有毛病,所以屏蔽了错误输出 run_shell(cmd, retry_times=False) file.unlink() def choose_term(filename): terms = get_terms(filename) term_index = 0 if len(terms) > 1: print_menu([i.get('name') for i in terms]) term_index = int(input('请选择学期:')) term = terms[term_index] term_id = term.get('term_id') return term_index, term_id, term def choose_chapter(term): chapters = get_chapters(term) chapter_names = [chapter.get('name') for chapter in chapters] print_menu(chapter_names) chapter_index = int(input('请选择章节:')) chapter = chapters[chapter_index] return chapter def load_json_cookies(): cookies = Path('cookies.json') if cookies.exists(): return cookiejar_from_dict(json.loads(cookies.read_bytes())) def parse_video_url(video_url): # 从播放url中提取出获取token要用的file_id和term_id file_id = parse_qs(video_url).get('vid')[0] term_id = urlparse(video_url).path.split('/')[-1] return term_id, file_id def parse_cid_url(video_url): pattern = re.compile('https://ke.qq.com/webcourse/(.*)/') return pattern.findall(video_url)[0] def get_video_token(term_id, file_id): # 获得sign, t, us这三个参数 这三个参数用来获取视频m3u8 params = {'term_id': term_id, 'fileId': file_id} response = requests.get( API.TokenUri, params=params, cookies=load_json_cookies(), proxies=PROXIES ).json() return response.get('result') def get_video_info(file_id, t, sign, us): """ 1258712167这个跟请求的cdn有关 但我发现这东西写死在js里,且不同账户下不同课程都是用这一个cdn 而且这东西没有通过api数据返回,初步判断它是固定的 因此将其作为固定参数 """ url = API.MediaUri + str(file_id) params = {'t': t, 'sign': sign, 'us': us, 'exper': 0} response = requests.get(url, params=params, cookies=load_json_cookies(), proxies=PROXIES).json() return response def get_token_for_key_url(term_id, cid): """ 这个key_url后面要接一个token,研究发现,token是如下结构base64加密后得到的 其中的plskey是要填的,这个东西来自登陆时的token去掉结尾的两个'=',也可以在cookies.json里获取 """ if not CURRENT_USER: cookies = Path('cookies.json') if cookies.exists(): cookies = json.loads(cookies.read_bytes()) uin = get_uin() CURRENT_USER['uin'] = uin if len(CURRENT_USER.get('uin')) > 10: # 微信 CURRENT_USER['ext'] = cookies.get('uid_a2') CURRENT_USER['appid'] = cookies.get('uid_appid') CURRENT_USER['uid_type'] = cookies.get('uid_type') str_token = 'uin={uin};skey=;pskey=;plskey=;ext={uid_a2};uid_appid={appid};' \ 'uid_type={uid_type};uid_origin_uid_type=2;uid_origin_auth_type=2;' \ 'cid={cid};term_id={term_id};vod_type=0;platform=3'\ .format(uin=uin, uid_a2=CURRENT_USER.get('ext'), appid=CURRENT_USER.get('appid'), uid_type=CURRENT_USER.get('uid_type'), cid=cid, term_id=term_id) else: skey = pskey = plskey = None CURRENT_USER['p_lskey'] = cookies.get('p_lskey') CURRENT_USER['skey'] = cookies.get('skey') CURRENT_USER['pskey'] = cookies.get('p_skey') str_token = 'uin={uin};skey={skey};pskey={pskey};plskey={plskey};ext=;uid_type=0;' \ 'uid_origin_uid_type=0;uid_origin_auth_type=0;cid={cid};term_id={term_id};' \ 'vod_type=0'\ .format(uin=uin, skey=CURRENT_USER.get('skey'), pskey=CURRENT_USER.get('pskey'), plskey=CURRENT_USER.get('plskey'), cid=cid, term_id=term_id) CURRENT_USER['token'] = str_token # 直接从CURRENT_USER里读取参数 return base64.b64encode(CURRENT_USER.get('token').encode()).decode()[:-2] def get_video_url(video_info, video_index=-1, cid=None, term_id=None): """ 接收来自get_video_info函数返回的视频信息 根据video_index返回不同清晰度的视频ts下载链接 """ video = video_info.get('videoInfo').get('transcodeList', None) if video: video = video[video_index] video_url = video.get('url').replace('.m3u8', '.ts') key_url = ( get_key_url_from_m3u8(video.get('url')) + '&token=' + get_token_for_key_url(term_id=term_id, cid=cid) ) return video_url, key_url return video_info.get('videoInfo').get('sourceVideo').get('url'), None def get_key_url_from_m3u8(m3u8_url): # 传入带sign, t, us参数的m3u8下载链接 m3u8_text = requests.get(m3u8_url, proxies=PROXIES).text pattern = re.compile(r'(https://ke.qq.com/cgi-bin/qcloud/get_dk.+)"') return pattern.findall(m3u8_text)[0] def get_download_url_from_course_url(video_url, video_index=-1): term_id, file_id = parse_video_url(video_url) cid = parse_cid_url(video_url) tokens = get_video_token(term_id, file_id) video_info = get_video_info( file_id, tokens.get('t'), tokens.get('sign'), tokens.get('us') ) return get_video_url(video_info, video_index, cid=cid, term_id=term_id) def get_download_urls(term_id, file_id, video_index=-1, cid=None): tokens = get_video_token(term_id, file_id) video_info = get_video_info( file_id, tokens.get('t'), tokens.get('sign'), tokens.get('us') ) return get_video_url(video_info, video_index, cid=cid, term_id=term_id) def clear_screen(): if sys.platform.startswith('win'): os.system('cls') else: os.system('clear') def get_video_rec(cid, file_id, term_id, video_index=0): # 腾讯课堂现在强制绑定手机号,看样子是更新了 # 新发现的接口,疑似wechat / qq通用,无需cookie,只要uin # 返回dk( ts文件密匙 ),视频文件链接,时长,生存时间,字幕等信息 # 这里返回的rec_video_info.info里面含有不同清晰度的视频文件,越清晰的排序越靠前 params = { 'course_id': cid, 'file_id': file_id, 'term_id': term_id, 'header': '{{"srv_appid":201,"cli_appid":"ke","uin":"{}","cli_info":{{"cli_platform":3}}}}'.format(get_uin()) } response = requests.get(API.VideoRec, headers=DEFAULT_HEADERS, params=params, proxies=PROXIES).json() if response: info = response.get('result').get('rec_video_info') ts_url = info.get('infos')[video_index].get('url').replace('.m3u8', '.ts') key = info.get('dk') return ts_url, key def get_uin(): response = requests.get(API.DefaultAccount, cookies=load_json_cookies(), headers=DEFAULT_HEADERS, proxies=PROXIES).json() if response.get('retcode') == 0: return response.get('result').get('tiny_id') return input('请输入你的QQ号 / 微信uin(回车结束):')