针对小鹅通已购视频,下载到本地
【主要解决课程过期后无法观看的问题,下载到本地可永久观看】
需自行在浏览器或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)