启航教育课程视频下载(开源首发-已打包exe)

全网开源首发!!请帮忙点点赞+关注吧,谢谢!

未经许可禁止转载,禁止任何修改后二次传播。

卖二手课的商家请自行绕道,严禁使用文本提供的开源程序进行任何的盈利行为!


     想把视频下载下来存到云盘,但官网并没有提供外置存储的功能。网上都是些引流要掏馒头帮下载的文章,那就自己来吧,生为大学谋福利! 


零、更新日志

12月22日更新:源代码在第五章,软件在第一章,点赞+关注自取。

12月23日更新:增加了使用账号密码登录,不用去找Authorization参数了

12月25日更新:报错:“正由另一进程使用,因此该进程无法访问此文件。”把maxtreads改到32就好了,可能因为网速上限太大了,多线程下载造成资源抢占了。错误解决方案提供者:里欧布鲁斯

12月26日更新:报错“SyntaxError:缺少标识符、字符串或数字”;这是因为课程包结构不一样,导致报错。已经将字典提取改成了正则匹配,最大化适配课程包结构。(下载链接已替换为新版本链接)

12月29日更新:v1.0系列中调用JS文件的解密也会引起“SyntaxError:缺少标识符、字符串或数字”的错误。目前纯算解密已完成,还加了断点续传功能。v2.0系列正式发布!!在此感谢Endlin Boeingstein的协助测试!

12月30日更新昨天忘记把对m3u8链接加文件存在检测和断点续传了,今天给完善了。


顺带说一句:程序内下载速度没做限制,你带宽多少就能跑到多少。但是下视频的时候如果可以自行做一下限速处理吧,尽量在10MB/s以内,别把启航上传带宽拉太高了。

一、可执行文件

    大学生不懂代码但是只想要下视频的点这个:启航课程视频下载.exe 

    只下这个就行,依赖项全都打包好了,解压出来双击运行,按照运行框的提示操作就行。

二、实现流程

1、通过用户名密码登录

2、获取账号下已有课程

3、选择需要下载的课程

4、下载视频,或者只保留视频解析链接

三、视频链接的解密处理

    启航以前的视频播放采用的是M3U8传输,启航没有对M3U8的链接做加密,所以不需要解密。
    m3u8全称为MP3 Playlist file version 8,被用于HLS(HTTP Live Streaming)流媒体协议中,作为播放列表文件来指引播放器按顺序加载视频片段。

    现在视频播放是传输的MP4文件,HTTP流式传输(没具体分析,应该是)。启航对这种视频链接进行了加密处理,所以在获取视频链接(M3U8文件和MP4文件)时我分别对这两种链接的做了不同的处理。

    在get_mp4_url()方法中有while循环并且校验了解密后链接的合法性是因为我不明确是我解密代码扒得不全还是因为内存污染的原因,导致有时候解密出来的链接有几个字符是乱码,所以做了while循环和合法性校验,如果有乱码重新请求新的加密链接再次解密。

    对于视频的下载,M3U8文件使用的是GitHub上的下载器,没有再编写代码来处理他。MP4链接编写了一个简易的下载代码来完成。

M3U8下载器GitHub主页链接:N_m3u8DL-CLI_v3.0.2

四、免责声明

    本文所提供的信息仅供个人学习和研究之用,不得用于任何商业目的或非法活动。在尝试下载或处理任何在线课程视频时,请确保您有权访问并下载这些内容,并遵守相关的版权法律法规。

    本文不保证所提供的下载方法或工具完全有效或安全,也不对任何因使用本文内容而导致的损失或损害承担责任。在使用本文提供的方法或工具时,请自行承担风险,并确保您的行为符合相关法律法规的规定。

五、源代码

import base64
import json
import re
import requests
import os

os.system('chcp 65001')  # 将cmd的显示字符编码从默认的GBK改为UTF-8


class Download_QiHang_Video:
    def __init__(self, account, password, workDir='./视频存放'):
        self.video_name_vid_list = []
        self.authorization = self.login(account, password)
        self.workDir = workDir
        self.productName = None
        self.productCurriculumId = None
        self.userProductId = None
        self.get_ProductInfoDic()  # 初始化程序

    def login(self, account, password):
        headers = {
            'accept': 'application/json, text/plain, */*',
            'accept-language': 'zh-CN,zh;q=0.9',
            'cache-control': 'no-cache',
            'content-type': 'application/json;charset=UTF-8',
            'origin': 'https://www.iqihang.com',
            'referer': 'https://www.iqihang.com/ark/exam26?code=11',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0',
        }
        json_data = {
            'clientType': 2,
            'address': 'https://www.iqihang.com/ark/exam26',
            'browser': 'chrome',
            'browserVersion': '131.0.0.0',
            'loginAddress': '',
            'loginProduct': '启航教育',
            'loginTerminal': 4,
            'loginType': 2,
            'loginVersion': '1.0.0',
            'network': '',
            'networkType': '',
            'os': 'Win32',
            'osVersion': 'window 10',
            'plant': '',
            'plantType': '',
            'source': '启航教育',
            'sourceType': 17,
            'terminal': 4,
            'version': '1.0.0',
            'account': f'{account}',
            'password': f'{password}',
        }
        response = requests.post('https://www.iqihang.com/api/ark/sso/login', headers=headers, json=json_data)
        dataJson = response.json()
        if dataJson['code'] == 0:
            token = dataJson["data"]["token"]
            print("\033[1;32m登录成功!!\033[0m")
            return f"Bearer {token}"
        else:
            msg = dataJson['msg']
            raise ValueError(f'{msg}')

    # =====获取账户下拥有的课程信息并选择需要下载哪个课程包  程序初始化入口=====
    def get_ProductInfoDic(self):
        ProductInfoDic_list = []
        index = 0
        headers = {
            'authorization': f'{self.authorization}',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0',
        }
        params = {
            'status': '',
            'type': '1',
        }
        response = requests.get('https://www.iqihang.com/api/ark/web/v1/user/course/course-list', params=params, headers=headers)
        dataJson = response.json()
        for data in dataJson["data"]:
            userProductId = data["id"]
            productName = data["productName"]
            productCurriculumId = data["productCurriculumId"]
            ProductInfoDic = {
                "userProductId": userProductId,
                "productName": productName,
                "productCurriculumId": productCurriculumId
            }
            ProductInfoDic_list.append(ProductInfoDic)
            print(f"序号{index}:{productName}")
            index += 1
        index = int(input("\033[1;32m请选择序号(回车确认):\033[0m"))
        print(f"\033[1;32m开始处理“{ProductInfoDic_list[index]['productName']}”\033[0m")
        ProductInfoDic = ProductInfoDic_list[index]
        self.productName = ProductInfoDic['productName']
        self.productCurriculumId = ProductInfoDic['productCurriculumId']
        self.userProductId = ProductInfoDic['userProductId']
        self.get_vid()

    # =====获取产品包含的的视频详细信息,主要需要视频名称和vid=====
    def get_ProductJson(self, productCurriculumId):
        headers = {
            'authorization': f'{self.authorization}',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0',
        }

        response = requests.get(f'https://www.iqihang.com/api/ark/web/v1/course/catalog/{productCurriculumId}', headers=headers)
        return response.json()

    # =====从文件中提取vid=====
    def get_vid(self):
        ProductJson = self.get_ProductJson(self.productCurriculumId)
        try:
            dataJson = ProductJson["data"]["courseNodes"]
            resourceName_list = re.findall(r'\'resourceName\': (.*?),', str(dataJson), re.S)
            vid_list = re.findall(r'\'vid\': (.*?),', str(dataJson), re.S)
            if len(resourceName_list) == len(vid_list):
                for i in range(len(resourceName_list)):
                    name = resourceName_list[i].replace("\'", "")
                    vid = vid_list[i].replace("\'", "")
                    if vid_list[i] != 'None':
                        self.video_name_vid_list.append((name, vid))
            else:
                raise "匹配到的名称和vid数量不一致, 请联系作者"
        except Exception as e:
            raise f"get_vid()报错:{e}"

    # =====获取视频链接======
    def get_video_url(self, name, vid):
        # 非9位vid视频返回m3u8链接
        if len(vid) != 9:
            url = self.get_m3u8_url(vid)
        else:
            # 9位vid的视频直接返回.MP4链接
            url = self.get_mp4_url(vid)
        return url

    # 非9位vid视频返回m3u8链接
    def get_m3u8_url(self, vid):
        headers = {
            'Host': 'p.bokecc.com',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0',
            'referer': 'https://www.iqihang.com/',
            'accept-language': 'zh-CN,zh;q=0.9',
        }
        params = {
            'vid': f'{vid}',
            'siteid': 'A183AC83A2983CCC',  # 固定写死
        }
        response = requests.get('https://p.bokecc.com/servlet/getvideofile', params=params, headers=headers)
        dataJson = json.loads(response.text.replace('null(', '')[:-1])
        try:
            url = dataJson["copies"][1]["playurl"]
            return url
        except:
            return None

    # 9位vid的视频返回MP4链接
    def get_mp4_url(self, vid):
        headers = {
            'accept': 'application/json, text/plain, */*',
            'accept-language': 'zh-CN,zh;q=0.9',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0',
            'authorization': self.authorization,
        }
        session = requests.session()
        session.headers = headers
        while True:
            params = {
                'id': '1',  # 应为resourceId,但实际不影响,但不可为空不可删除
                'userProductId': self.userProductId,  # 用户产品id
                'vid': vid,
                'isAllowAudition': '0',  # 可选,可删除
            }
            response = session.get('https://www.iqihang.com/api/ark/web/v1/baijiayun/player/token', params=params, allow_redirects=False)
            token = response.json()["data"]["token"]
            params = {
                "vid": vid,
                "token": token
            }
            response = session.get('https://www.baijiayun.com/vod/video/getPlayUrl', params=params)
            dataJson = json.loads(response.text)
            enc_url_list = []
            for i in ['1080p', 'superHD']:
                for j in range(0, 3):
                    enc_url_list.append(dataJson["data"]["play_info"][i]["cdn_list"][j]["enc_url"])
            for enc_url in enc_url_list:
                url = self.decodeUrl(enc_url)
                if url != -1:
                    return url

    # =====下载视频=====
    def Download(self, name, url):
        if url is not None:
            if ".m3u8?" in url:
                self.download_m3u8(name, url)
            else:
                self.download_mp4(name, url)

    # M3U8 使用第三方下载器下载,不再编写处理m3u8文件的代码
    def download_m3u8(self, name, url):
        if len(name.split(" ")) > 1:
            workDir = self.workDir + "/" + name.split(" ")[0]
        else:
            workDir = self.workDir
        cmd = f'N_m3u8DL-CLI_v3.0.2.exe "{url}" --workDir "{workDir}" --saveName "{name}" --maxThreads "32" --retryCount "3" --enableDelAfterDone'
        os.system(cmd)

    # mp4 使用requests下载
    def download_mp4(self, file_name, url):
        # 确保文件名以 .mp4 结尾
        if not file_name.endswith(".mp4"):
            file_name += ".mp4"
        # 分章节目录
        if len(file_name.split(" ")) > 1:
            workDir = self.workDir + "/" + file_name.split(" ")[0]
        else:
            workDir = self.workDir
        # 构建完整文件路径
        full_path = os.path.join(workDir, file_name)
        # 创建下载路径目录(如果不存在)
        os.makedirs(workDir, exist_ok=True)
        # 检查文件是否已经存在并完整
        existing_size = os.path.getsize(full_path) if os.path.exists(full_path) else 0
        # 获取远程文件总大小
        try:
            head_response = requests.head(url)
            head_response.raise_for_status()
            total_size = int(head_response.headers.get('content-length', 0))
        except requests.exceptions.RequestException as e:
            print(f"无法获取文件大小:{e}")
            return

        if existing_size >= total_size:
            print(f"文件已存在且完整,跳过下载:{full_path}")
            return

        # 设置 HTTP 头部,指定下载的起始字节位置
        headers = {"Range": f"bytes={existing_size}-"} if existing_size > 0 else {}
        headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"

        try:
            # 发送 HTTP GET 请求下载文件
            with requests.get(url, stream=True, headers=headers) as response:
                # 检查请求状态码
                if response.status_code not in (200, 206):
                    raise requests.exceptions.RequestException(f"Unexpected status code: {response.status_code}")

                # 将文件追加写入本地
                with open(full_path, "ab") as file:
                    for chunk in response.iter_content(chunk_size=8192):  # 每次读取 8KB
                        if chunk:  # 确保 chunk 非空
                            file.write(chunk)
                            existing_size += len(chunk)
                            # 显示进度条
                            print(f"\r正在下载 《{file_name}》: {existing_size}/{total_size} bytes ({(existing_size / total_size) * 100:.2f}%)", end="")

            print(f"\n\033[1;32m下载完成:\033[0m{file_name}")
        except requests.exceptions.RequestException as e:
            print(f"下载失败:{e}  (重新运行如果还有这个报错请联系作者)")

    def decodeUrl(self, enc_url):
        try:
            encoded_str = enc_url.replace("bjcloudvod://", '')
            # 修复填充
            if len(encoded_str) % 4 != 0:
                encoded_str += "=" * (4 - len(encoded_str) % 4)
            # 解码为字节
            decoded_bytes = base64.b64decode(encoded_str)
            # 如果需要将字节转换为字符串
            decoded_str = decoded_bytes.decode("utf-8")
            n = ord(decoded_str[0]) % 8
            decoded_str = decoded_str[1:]
            s = []  # 初始化空列表
            l = 0  # 索引从 0 开始
            # 循环遍历字符串 t
            for l, a in enumerate(decoded_str):
                p = l % 4 * n + l % 3 + 1
                s.append(chr(ord(a) - p))  # 计算新的字符并添加到列表
            # 将处理后的字符列表合并成字符串
            dec_url = ''.join(s)
            return dec_url
        except:
            return -1


if __name__ == '__main__':
    print('\033[1;31m视频将存放在:“.\视频存放”目录下\033[0m')
    account = str(input("\033[1;32m请输入账号(回车确认):\033[0m")).replace("\'", "").replace("\"", "") or False
    password = str(input("\033[1;32m请输入密码(回车确认):\033[0m")).replace("\'", "").replace("\"", "") or False
    if account is not False and password is not False:
        try:
            QiHang = Download_QiHang_Video(account=account, password=password)  # 实例化对象
            for name, vid in QiHang.video_name_vid_list:
                url = QiHang.get_video_url(name, vid)  # 获取url
                QiHang.Download(name, url)  # 下载视频
            print("\033[1;32m下载完成!\033[0m")
        except Exception as e:
            print(f"出现错误:\033[1;31m{e}\033[0m")
    else:
        print("\033[1;31m请输入账号和密码\033[0m")
    input("按任意键退出...")
### 回答1: Java Swing 是一个用于创建图形用户界面(GUI)的 Java 应用程序编程接口(API)。Swing 允许开发人员创建具有不同组件(如按钮、文本框、下拉列表等)的交互式用户界面。Swing 不依赖于平台的外观,因此开发人员可以创建可在多个操作系统上运行的界面。 在使用 Java Swing 进行开发时,需要使用 Swing 组件类和布局管理器。Swing 组件类包括 JFrame、JPanel、JButton、JTextField 等,布局管理器则用于在容器中定位和布置组件。 Java Swing 可以用于创建各种 GUI 应用程序,包括桌面应用程序、工具和游戏等。Swing 还支持多种事件处理机制,使得开发人员可以对用户操作做出响应并采取适当的行动。 总的来说,Java Swing 是一个强大的工具,可用于创建各种类型的图形用户界面。 ### 回答2: Java Swing是一个基于Java语言的GUI工具包,可用于创建桌面应用程序。在Java Swing中,我们可以使用图形用户界面组件来创建可视化应用程序,如窗口、按钮、标签等。 在Swing中,开发人员可以使用编程方式创建GUI组件,也可以使用可视化设计工具来创建和设置GUI组件。这使得开发人员可以更快速地创建GUI应用程序,并且可以更方便地进行设计和布局。 Idea是一个常见的Java集成开发环境,它提供了强大的代码编辑器和开发工具。在Idea中,我们可以使用Swing的可视化设计工具来创建GUI应用程序,这种方法非常直观和易于使用。 使用Idea进行Swing可视化开发,可以实现快速开发和快速迭代。借助Idea的智能提示、自动补全等功能,我们可以高效地编写Java代码,并迅速创建可视化组件。Idea还提供了调试和测试工具,可以帮助我们更好地验证代码和程序逻辑。 总体而言,使用Idea进行Swing可视化开发,可以让开发人员更快速、更高效地创建GUI应用程序,并且可以简化开发过程和降低开发成本。同时,由于Java的跨平台特性,我们可以将这些应用程序部署到不同的操作系统上,并确保其良好的兼容性和稳定性。 ### 回答3: Idea JavaSwing 可视化开发是一种较为常见的 Java 开发方式,其主要特点是使用 Java 的 GUI 编程工具包 Swing 来设计界面,借助集成开发环境 IntelliJ IDEA 以及相关插件进行开发。相比于传统的手动编写 Java GUI 代码,Idea JavaSwing 可视化开发更加直观,便于设计,并且可以节省大量的开发时间。 使用 Idea JavaSwing 可视化开发需要掌握一定的 Java 基础和 GUI 编程知识,同时需要熟悉 IntelliJ IDEA 的操作。在进行可视化开发之前,开发者需要先确定自己的应用程序的需求和界面设计方案,然后使用 IntelliJ IDEA 的 GUI 编辑器设计对应的界面。在编辑器中,开发者可以通过拖拽控件、设置属性、布局等方式设计出界面的样式和交互效果。同时,开发者还可以通过代码生成器快速生成对应的 Java 代码,进一步提高开发效率和减少错误率。 在进行 Idea JavaSwing 可视化开发时,需要注意以下几点: 1. 界面设计要分清层次。界面设计需要分层次,尽量避免 UI 层与业务逻辑层的混淆。同时,需要注意控件的选择和设置方式,保证整个界面的风格一致性。 2. 布局方式要灵活。界面设计需要根据需求灵活选择布局方式,同时要注意布局的稳定性和兼容性,避免在不同屏幕分辨率下出现错位等问题。 3. 控件设计要合理。界面设计需要合理设置控件的样式、属性和事件,保证在用户使用过程中的体验和效果。 总之,Idea JavaSwing 可视化开发是目前 Java GUI 编程中比较流行的一种方式,其使用简单方便,具有高效快捷和易于维护等优点,是 Java 开发人员应该掌握和应用的一种技术。
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MySheep.

赏瓶水钱吧!感谢!!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值