Package Version
------------------------- --------------
anyio 4.9.0
argon2-cffi 23.1.0
argon2-cffi-bindings 21.2.0
arrow 1.3.0
asttokens 3.0.0
async-lru 2.0.5
attrs 25.3.0
babel 2.17.0
beautifulsoup4 4.13.3
bleach 6.2.0
certifi 2025.1.31
cffi 1.17.1
charset-normalizer 3.4.1
comm 0.2.2
debugpy 1.8.13
decorator 5.2.1
defusedxml 0.7.1
executing 2.2.0
fastjsonschema 2.21.1
fqdn 1.5.1
h11 0.14.0
httpcore 1.0.7
httpx 0.28.1
idna 3.10
ipykernel 6.29.5
ipython 9.0.2
ipython_pygments_lexers 1.1.1
ipywidgets 8.1.5
isoduration 20.11.0
jedi 0.19.2
Jinja2 3.1.6
json5 0.10.0
jsonpointer 3.0.0
jsonschema 4.23.0
jsonschema-specifications 2024.10.1
jupyter 1.1.1
jupyter_client 8.6.3
jupyter-console 6.6.3
jupyter_core 5.7.2
jupyter-events 0.12.0
jupyter-lsp 2.2.5
jupyter_server 2.15.0
jupyter_server_terminals 0.5.3
jupyterlab 4.3.6
jupyterlab_pygments 0.3.0
jupyterlab_server 2.27.3
jupyterlab_widgets 3.0.13
m3u8 6.0.0
MarkupSafe 3.0.2
matplotlib-inline 0.1.7
mistune 3.1.3
nbclient 0.10.2
nbconvert 7.16.6
nbformat 5.10.4
nest-asyncio 1.6.0
notebook 7.3.3
notebook_shim 0.2.4
overrides 7.7.0
packaging 24.2
pandocfilters 1.5.1
parso 0.8.4
pexpect 4.9.0
pip 23.0.1
platformdirs 4.3.7
prometheus_client 0.21.1
prompt_toolkit 3.0.50
psutil 7.0.0
ptyprocess 0.7.0
pure_eval 0.2.3
pycparser 2.22
Pygments 2.19.1
python-dateutil 2.9.0.post0
python-json-logger 3.3.0
PyYAML 6.0.2
pyzmq 26.3.0
referencing 0.36.2
requests 2.32.3
rfc3339-validator 0.1.4
rfc3986-validator 0.1.1
rpds-py 0.24.0
Send2Trash 1.8.3
setuptools 66.1.1
six 1.17.0
sniffio 1.3.1
soupsieve 2.6
stack-data 0.6.3
terminado 0.18.1
tinycss2 1.4.0
tornado 6.4.2
tqdm 4.67.1
traitlets 5.14.3
types-python-dateutil 2.9.0.20241206
typing_extensions 4.13.0
uri-template 1.3.0
urllib3 2.3.0
wcwidth 0.2.13
webcolors 24.11.1
webencodings 0.5.1
websocket-client 1.8.0
widgetsnbextension 4.0.13
Note: you may need to restart the kernel to use updated packages.
import os
import requests
from m3u8 import M3U8
from urllib.parse import urljoin
import tempfile
import subprocess
from concurrent.futures import ThreadPoolExecutor
from tqdm import tqdm
import threading
def download_ts_video(m3u8_url, output_mp4, headers=None, max_workers=4):
temp_dir = tempfile.mkdtemp()
lock = threading.Lock()
errors = []
try:
if headers is None:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
'Referer': urljoin(m3u8_url, '/')
}
# 获取M3U8目录路径
m3u8_dir = m3u8_url[:m3u8_url.rfind('/') + 1]
# 下载并解析M3U8
response = requests.get(m3u8_url, headers=headers)
response.raise_for_status()
m3u8 = M3U8(response.text, base_uri=m3u8_dir)
ts_urls = [urljoin(m3u8_dir, seg.uri) for seg in m3u8.segments]
if not ts_urls:
raise ValueError("未找到TS片段")
# 多线程下载
print(f"启动 {max_workers} 个线程下载...")
progress = tqdm(total=len(ts_urls), desc="下载进度")
def download_segment(url, index):
try:
ts_file = os.path.join(temp_dir, f'segment_{index:04d}.ts')
if not os.path.exists(ts_file): # 避免重复下载
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
with open(ts_file, 'wb') as f:
f.write(resp.content)
with lock:
progress.update(1)
except Exception as e:
errors.append(f"片段{index}下载失败: {str(e)}")
with lock:
progress.set_description(f"错误数: {len(errors)}")
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = []
for i, url in enumerate(ts_urls):
futures.append(executor.submit(download_segment, url, i))
# 等待所有任务完成
for future in futures:
future.result()
progress.close()
# 检查错误
if errors:
print("\n发生以下错误:")
for error in errors[:3]: # 只显示前3个错误
print(error)
if len(errors) > 3:
print(f"(共 {len(errors)} 个错误)")
if input("是否继续合并?(y/n)").lower() != 'y':
return
# 生成文件列表
ts_files = sorted([os.path.join(temp_dir, f)
for f in os.listdir(temp_dir) if f.endswith('.ts')],
key=lambda x: int(x.split('_')[-1].split('.')[0]))
file_list = os.path.join(temp_dir, 'file_list.txt')
with open(file_list, 'w', encoding='utf-8') as f:
for ts_file in ts_files:
f.write(f"file '{os.path.abspath(ts_file)}'\n")
# 合并转码
print("合并转码中...")
subprocess.run([
'ffmpeg',
'-f', 'concat',
'-safe', '0',
'-i', file_list,
'-c', 'copy',
'-y',
output_mp4
], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print(f"\n视频已保存至:{os.path.abspath(output_mp4)}")
finally:
import shutil
shutil.rmtree(temp_dir)
# 使用示例
#'https://t26.cdn2020.com/video/m3u8/index.m3u8',
if __name__ == '__main__':
url = input("请输入m3u8链接:")
print("您输入的链接名为" + url)
name = './move/{movie_name}.mp4'.format(movie_name=input("请输入影片名:"))
print("您存储影片路径为" +name)
download_ts_video(
m3u8_url= url,
output_mp4= name,
max_workers=16
)
请输入m3u8链接: https://t26.cdn2020.com/video/m3u8/index.m3u8
您输入的链接名为https://t26.cdn2020.com/video/m3u8/index.m3u8
请输入影片名: 0fa806c8
您存储影片路径为./move/0fa806c8.mp4
启动 16 个线程下载...
错误数: 2: 100%|██████████████████████████████████████████████████████████████████████████████████████▊| 1141/1143 [17:43<00:01, 1.07it/s]
发生以下错误:
片段552下载失败: HTTPSConnectionPool(host='t26.cdn2020.com', port=443): Read timed out.
片段608下载失败: HTTPSConnectionPool(host='t26.cdn2020.com', port=443): Read timed out. (read timeout=10)
是否继续合并?(y/n) n