闲来无事,写了个m3u8视频下载器,分享给各位(好处不多说!都懂!),如果有什么不对的地方,还请指正。另外还有m3u8视频解析器,通过视频播放链接(非商业性网站)解析出m3u8地址,然后再通过m3u8下载器进行下载,如果有需要的小伙伴请私信。
中间可能会有些看上去冗余的代码,主要是为了兼容各种稀奇古怪的m3u8内容。
脚本仅用于技术学习与研究,请勿用于任何非法用途,否则后果自负,本作者不承担任何法律责任。
原创文章,转载请注明出处,谢谢!https://blog.csdn.net/weixin_36381802/article/details/113694338
环境: pip install gevent requests loguru pycryptodome
# -*- coding:utf-8 -*-
"""
NAME: m3u8视频下载器
VERSION: v1.0
DATE: 2021-02-05
TIPS:
1.若部分视频无法播放,建议更改文件名后缀或切换其它播放器(QuickTime、WindowsMedia等)进行尝试;
2.在MacOS或Linux系统上运行前请确认已安装合并视频片段所使用的工具ffmpeg(Windows无视);
3.仅支持下载m3u8类型视频,mp4等链接暂不支持(普通下载器满大街都是);
4.脚本仅用于技术学习与研究,请勿用于任何非法用途,否则后果自负,本作者不承担任何责任。
"""
import argparse
import os
import platform
import re
import shutil
import time
from datetime import datetime
from urllib.parse import urljoin
import gevent
from gevent.pool import Pool
from gevent import monkey; monkey.patch_all()
import requests
import urllib3
from Crypto.Cipher import AES
# 自定义日志显示格式
from os import environ
environ['LOGURU_FORMAT'] = "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level:<5}</level> | <level>{message}</level>"
from loguru import logger
# logger.add('m3u8.log', level='DEBUG', format='{time:YYYY-MM-DD HH:mm:ss} {level:<5} {line} {message}')
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class M3u8VideoDownloader:
headers = {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36',
}
def __init__(self, m3u8_url, download_path=None, video_name=None, is_del_clip=True, test_download_num=0,
retry_count=10, thread_num=30, dec_func=None, m3u8_content_plaintext=None):
"""
:param m3u8_url: m3u8链接
:param download_path: 下载路径
:param video_name: 视频名称(不能出现括号)
:param is_del_clip: 合并视频完成后是否删除原片段
:param test_download_num: 测试下载视频数量
:param retry_count: 单个视频片段下载失败重试次数
:param thread_num: 下载线程数
:param dec_func: m3u8内容解密函数(内容被加密时可传入解密函数,或直接将解密后的明文内容传递给参数m3u8_content_plaintext)
:param m3u8_content_plaintext: 已解密的m3u8明文内容
"""
self.m3u8_url = m3u8_url
self.download_path = download_path
self.cache_path = None # 临时缓存路径
self.video_name = video_name or str(int(time.time()))
self.video_name_suffix = '.mp4' # 文件类型后缀
self.is_del_clip = is_del_clip
self.test_download_num = test_download_num
self.retry_count = retry_count
self.thread_num = min(thread_num, 50)
self.max_merge_num = 500 # 单次合并文件最大数量
self.dec_func = dec_func
self.m3u8_content_plaintext = m3u8_content_plaintext
self.key_url = None
self.key = None
self.iv = None
self.decipher = None
self.video_clip_list = [] # 视频片段名称列表
self.total_duration = 0 # 视频总时间(分钟)
self.total_video_clip_num = 0 # 视频片段数量
self.download_num = 0 # 已下载数量
self.total_download_size = 0 # 总下载大小
self.is_special_link = False # 视频片段链接未带后缀(例`.ts`)时为True,一般出现在m3u8内容被加密的视频网站
def fetch(self, url, binary=False):
resp = requests.get(url, headers=self.headers, timeout=30, verify=False)
status_code = resp.status_code
if status_code != 200:
raise Exception(f'请求失败({status_code}):{url}')
if binary:
return resp.content
return resp.content.decode()
def get_m3u8_content(self):
"""获取m3u8内容"""
logger.info(f'M3U8链接:{self.m3u8_url}')
try:
m3u8_content = self.fetch(self.m3u8_url)
except Exception as e:
raise Exception(f'获取m3u8内容失败({self.m3u8_url}):{repr(e)}')
# 如果内容被加密,需要通过传入的解密函数进行解密
if self.dec_func:
try:
m3u8_content = self.dec_func(m3u8_content)
except Exception as e:
raise Exception(f'解密m3u8内容失败({self.m3u8_url}):{repr(e)}')
if '#EXTM3U' not in m3u8_content:
raise Exception(f'错误的M3U8信息,请确认链接是否正确:{self.m3u8_url}<{m3u8_content}>')
if '#EXT-X-STREAM-INF' in m3u8_content:
m3u8_url_list = [line for line in m3u8_content.split('\n') if line.find('.m3u8') != -1]
if len(m3u8_url_list) > 1:
logger.info(f'发现{len(m3u8_url_list)}个m3u8地址:{m3u8_url_list}')
self.m3u8_url = urljoin(self.m3u8_url, m3u8_url_list[0])
return self.get_m3u8_content()
# logger.info(f'M3U8内容已获取完成:{self.m3u8_url}')
return m3u8_content
def parse_m3u8_info(self, m3u8_content):
"""解析m3u8文件:获取解密key、iv、视频url列表"""
all_lines = m3u8_content