小鹅通视频下载

0 阅读3分钟

针对小鹅通已购视频,下载到本地

【主要解决课程过期后无法观看的问题,下载到本地可永久观看】

需自行在浏览器或app抓包,拿到登录和课程信息 本文仅供交流学习使用,侵权即删

from datetime import datetime
import os
import random
import requests
import re
from Crypto.Cipher import AES
import base64
import json


def generate_m3u8_url(video_audio_url_str: str):
    video_audio_url_base64_str = (
        video_audio_url_str.replace("@", "1")
        .replace("#", "2")
        .replace("$", "3")
        .replace("%", "4")
    )
    video_audio_url_base64_str = video_audio_url_base64_str.replace("__ba", "").replace(
        "_", "-"
    )
    decode_str = base64.b64decode(video_audio_url_base64_str).decode("utf-8")
    defeintion = json.loads(decode_str)
    url_str = defeintion[0]["url"]
    return url_str.replace("\\", "")


def list_course_video(app_id, course_id):
    url = f"https://{app_id}.h5.xiaoeknow.com/xe.course.business.avoidlogin.e_course.horizontal.resource_catalog_list.get/1.0.0"
    header = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
        "content-type": "application/x-www-form-urlencoded",
        "priority": "u=1, i",
        "req-uuid": generate_uuid(),
        "retry": "1",
    }
    data = {
        "bizData[app_id]": app_id,
        "bizData[course_id]": course_id,
        "bizData[order]": "asc",
        "bizData[page]": "1",
        "bizData[page_size]": "100",
    }
    response = requests.post(url, headers=header, data=data, verify=False)
    obj = response.json()
    return obj["data"]["list"]


def video_detail(app_id, resource_id, product_id, cookie):
    # 可以不传product_id
    url = f"https://{app_id}.h5.xiaoeknow.com/xe.course.business.video.detail_info.get/2.0.0"
    header = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
        "content-type": "application/x-www-form-urlencoded",
        "priority": "u=1, i",
        "req-uuid": generate_uuid(),
        "retry": "1",
        "Cookie": cookie,
    }

    data = {
        "bizData[resource_id]": resource_id,
        "bizData[product_id]": product_id,
        "bizData[opr_sys]": "MacIntel",
    }
    response = requests.post(url, headers=header, data=data, verify=False)
    obj = response.json()
    return obj["data"]


def generate_uuid():
    now = datetime.now()
    date_time_str = now.strftime("%Y%m%d%H%M%S")
    seq_num = "{:09d}".format(random.randint(0, 999999))
    combined_str = date_time_str + seq_num
    return combined_str


def m3u8(url, file_name, output_dir):
    header = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
    }
    content = requests.get(url, headers=header, verify=False).text
    if "#EXTM3U" not in content:
        print("这不是一个m3u8的视频链接!")
        return False
    if "EXT-X-KEY" not in content:
        print("没有")
        return False

    # 使用re正则得到key和视频地址
    jiami = re.findall("#EXT-X-KEY:(.*)\n", content)
    key = re.findall('URI="(.*)"', jiami[0])
    # 加密向量
    vi = re.findall("IV=(.*)", jiami[0])[0]
    tslist = re.findall("EXTINF:(.*),\n(.*)\n#", content)
    newlist = []
    for i in tslist:
        newlist.append(i[1])

    # 得到key的链接并请求得到加密的key值
    keyurl = key[0]
    keycontent = requests.get(keyurl, headers=header, verify=False).content
    # 得到每一个完整视频的链接地址
    base_url = url.replace(url.split("/")[-1], "")
    # print(base_url)
    tslisturl = []
    for i in newlist:
        tsurl = base_url + i
        tslisturl.append(tsurl)

    print("===========================keycontent", keycontent)
    cryptor = AES.new(keycontent, AES.MODE_CBC, b"0000000000000000")
    
    if not os.path.exists(output_dir):
        print("创建文件夹")
        os.makedirs(output_dir)
    file_path = output_dir + "/" + file_name

    if os.path.exists(file_path):
        try:
            os.remove(file_path)
            print(f"{file_path} 文件已删除")
        except OSError as e:
            print(f"删除文件时出错: {e}")
            return

    try:
        with open(file_path, "w") as file:
            file.write("")
        print(f"{file_path} 创建文件")
    except Exception as e:
        print(f"创建文件时出错: {e}")
        return

    # for循环获取视频文件
    for i in tslisturl:
        print(i)
        res = requests.get(i, header, verify=False)
        # 使用解密方法解密得到的视频文件
        cont = cryptor.decrypt(res.content)
        # 以追加的形式保存
        with open(file_path, "ab+") as f:
            f.write(cont)
    return True


def main(app_id, course_id, cookie, output_dir, start_num, end_num):
    if start_num < 1:
        start_num = 1
    videoList = list_course_video(app_id, course_id)
    total = len(videoList)
    if end_num < start_num | end_num > total:
        end_num = total
    n = 1

    for video in videoList:
        if n < start_num or n > end_num:
            n += 1
            continue
        detail = video_detail(app_id, video["resource_id"], course_id, cookie)
        m3u8_url = generate_m3u8_url(detail["video_urls"])
        video_info = detail["video_info"]
        file_name = video_info["file_name"]
        print(f"====================================开始下载第{n}个视频,{file_name}")
        download_name = "{}_{}".format(n, file_name)
        m3u8(m3u8_url, download_name, output_dir)
        n += 1


app_id = "{{自行从浏览器获取}}"
course_id = "{{自行从浏览器获取}}"
cookie = "{{自行从浏览器获取}}"
output_dir = "{{本地下载目录,注意权限}}"
# 课程视频数超过100 可 全局搜索后修改page和page_size
main(app_id, course_id, cookie, output_dir, 0, 100)