hs-m3u8 0.1.6__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hs_m3u8-0.1.6 → hs_m3u8-0.1.8}/PKG-INFO +2 -2
- {hs_m3u8-0.1.6 → hs_m3u8-0.1.8}/pyproject.toml +11 -31
- {hs_m3u8-0.1.6 → hs_m3u8-0.1.8}/src/hs_m3u8/main.py +168 -98
- hs_m3u8-0.1.6/.pre-commit-config.yaml +0 -18
- hs_m3u8-0.1.6/requirements-dev.lock +0 -209
- hs_m3u8-0.1.6/requirements.lock +0 -157
- hs_m3u8-0.1.6/src/example/__init__.py +0 -0
- hs_m3u8-0.1.6/src/example/jav_1.py +0 -14
- hs_m3u8-0.1.6/src/example/jav_2.py +0 -33
- hs_m3u8-0.1.6/src/example/jav_3.py +0 -14
- hs_m3u8-0.1.6/src/example/kyt.py +0 -43
- hs_m3u8-0.1.6/src/example/movie_1.py +0 -14
- hs_m3u8-0.1.6/src/example/movie_2.py +0 -14
- hs_m3u8-0.1.6/src/example/siyuanren.py +0 -14
- hs_m3u8-0.1.6/src/example/x.py +0 -16
- hs_m3u8-0.1.6/src/hs_m3u8/res/ffmpeg_win.exe.zip +0 -0
- {hs_m3u8-0.1.6 → hs_m3u8-0.1.8}/.gitignore +0 -0
- {hs_m3u8-0.1.6 → hs_m3u8-0.1.8}/README.md +0 -0
- {hs_m3u8-0.1.6 → hs_m3u8-0.1.8}/src/hs_m3u8/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hs-m3u8
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: m3u8 下载器
|
|
5
5
|
Project-URL: homepage, https://github.com/x-haose/hs-m3u8
|
|
6
6
|
Project-URL: repository, https://github.com/x-haose/hs-m3u8
|
|
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
|
-
Requires-Dist: hssp>=0.4.
|
|
23
|
+
Requires-Dist: hssp>=0.4.18
|
|
24
24
|
Requires-Dist: m3u8>=6.0.0
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hs-m3u8"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.8"
|
|
4
4
|
description = "m3u8 下载器"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "昊色居士", email = "xhrtxh@gmail.com" }
|
|
@@ -26,7 +26,13 @@ requires-python = ">= 3.10"
|
|
|
26
26
|
|
|
27
27
|
dependencies = [
|
|
28
28
|
"m3u8>=6.0.0",
|
|
29
|
-
"hssp>=0.4.
|
|
29
|
+
"hssp>=0.4.18",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"pre-commit>=4.2.0",
|
|
35
|
+
"twine>=6.1.0",
|
|
30
36
|
]
|
|
31
37
|
|
|
32
38
|
[project.urls]
|
|
@@ -38,32 +44,16 @@ documentation = "https://github.com/x-haose/hs-m3u8"
|
|
|
38
44
|
requires = ["hatchling"]
|
|
39
45
|
build-backend = "hatchling.build"
|
|
40
46
|
|
|
41
|
-
[tool.
|
|
42
|
-
managed = true
|
|
43
|
-
dev-dependencies = [
|
|
44
|
-
"pre-commit>=4.0.1",
|
|
45
|
-
"twine>=6.0.1",
|
|
46
|
-
]
|
|
47
|
+
[tool.hatch.build.targets.sdist]
|
|
47
48
|
include = [
|
|
48
|
-
"src/hs_m3u8
|
|
49
|
+
"src/hs_m3u8",
|
|
49
50
|
]
|
|
50
51
|
|
|
51
|
-
[tool.hatch.metadata]
|
|
52
|
-
allow-direct-references = true
|
|
53
|
-
|
|
54
52
|
[tool.hatch.build.targets.wheel]
|
|
55
53
|
packages = ["src/hs_m3u8"]
|
|
56
54
|
|
|
57
|
-
[[tool.rye.sources]]
|
|
58
|
-
name = "default"
|
|
59
|
-
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
60
|
-
|
|
61
55
|
[tool.bandit]
|
|
62
56
|
skips = [
|
|
63
|
-
"B404",
|
|
64
|
-
"B602",
|
|
65
|
-
"B501",
|
|
66
|
-
"B113"
|
|
67
57
|
]
|
|
68
58
|
|
|
69
59
|
[tool.ruff]
|
|
@@ -127,20 +117,10 @@ ignore = [
|
|
|
127
117
|
# do not perform function calls in argument defaults
|
|
128
118
|
"B008",
|
|
129
119
|
# too complex
|
|
130
|
-
"C901"
|
|
120
|
+
"C901",
|
|
131
121
|
]
|
|
132
122
|
fixable = ["ALL"]
|
|
133
123
|
unfixable = []
|
|
134
124
|
|
|
135
125
|
[tool.ruff.lint.per-file-ignores]
|
|
136
126
|
"__init__.py" = ["F401"]
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
[tool.rye.scripts]
|
|
140
|
-
publish_testpypi = { cmd = "rye run twine upload -r testpypi dist/*" }
|
|
141
|
-
publish_pypi = { cmd = "rye run twine upload dist/*" }
|
|
142
|
-
sb = { cmd = "rye build --clean" }
|
|
143
|
-
spt = { chain = ["sb", "publish_testpypi"] }
|
|
144
|
-
sp = { chain = ["sb", "publish_pypi"] }
|
|
145
|
-
check_i = { cmd = "rye run pre-commit install" }
|
|
146
|
-
check = { cmd = "rye run pre-commit run --all-files" }
|
|
@@ -3,58 +3,45 @@ M3U8 下载器
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
import platform
|
|
7
6
|
import posixpath
|
|
8
7
|
import shutil
|
|
9
|
-
import subprocess
|
|
10
8
|
from collections.abc import Callable
|
|
11
9
|
from hashlib import md5
|
|
12
|
-
from importlib import resources
|
|
13
10
|
from pathlib import Path
|
|
14
|
-
from
|
|
15
|
-
from
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import ParseResult, urljoin, urlparse
|
|
16
13
|
|
|
14
|
+
import av
|
|
17
15
|
import m3u8
|
|
18
16
|
from hssp import Net
|
|
19
17
|
from hssp.models.net import RequestModel
|
|
18
|
+
from hssp.network.response import Response
|
|
20
19
|
from hssp.utils import crypto
|
|
21
20
|
from loguru import logger
|
|
22
21
|
|
|
23
22
|
|
|
24
|
-
def get_ffmpeg():
|
|
25
|
-
"""
|
|
26
|
-
根据平台不同获取不同的ffmpeg可执行文件
|
|
27
|
-
:return: FFmpeg 的可执行文件路径
|
|
28
|
-
"""
|
|
29
|
-
current_os = platform.system()
|
|
30
|
-
if current_os != "Windows":
|
|
31
|
-
return "ffmpeg"
|
|
32
|
-
|
|
33
|
-
with resources.as_file(resources.files("hs_m3u8.res")) as package_dir:
|
|
34
|
-
ffmpeg_bin = package_dir / "ffmpeg_win.exe"
|
|
35
|
-
if ffmpeg_bin.exists():
|
|
36
|
-
return str(ffmpeg_bin)
|
|
37
|
-
|
|
38
|
-
# ZIP 文件
|
|
39
|
-
ffmpeg_bin_zip = package_dir / "ffmpeg_win.exe.zip"
|
|
40
|
-
if ffmpeg_bin_zip.exists():
|
|
41
|
-
with ZipFile(ffmpeg_bin_zip, "r") as zip_ref:
|
|
42
|
-
zip_ref.extractall(ffmpeg_bin.parent)
|
|
43
|
-
|
|
44
|
-
return ffmpeg_bin
|
|
45
|
-
|
|
46
|
-
|
|
47
23
|
class M3u8Key:
|
|
48
24
|
"""
|
|
49
25
|
M3u8key
|
|
50
26
|
"""
|
|
51
27
|
|
|
52
|
-
def __init__(self, key: bytes, iv: str = None):
|
|
28
|
+
def __init__(self, key: bytes, iv: str | bytes | None = None):
|
|
53
29
|
"""
|
|
54
|
-
:
|
|
55
|
-
|
|
30
|
+
Args:
|
|
31
|
+
key: 密钥
|
|
32
|
+
iv: 偏移,可以是十六进制字符串(0x开头)、bytes或None
|
|
56
33
|
"""
|
|
57
|
-
|
|
34
|
+
try:
|
|
35
|
+
if isinstance(iv, str) and iv.startswith("0x"):
|
|
36
|
+
iv = bytes.fromhex(iv[2:])
|
|
37
|
+
elif isinstance(iv, str):
|
|
38
|
+
iv = bytes.fromhex(iv)
|
|
39
|
+
except ValueError as e:
|
|
40
|
+
raise ValueError(f"iv {iv!r} 值不对: {e}") from e
|
|
41
|
+
|
|
42
|
+
if iv and len(iv) != 16:
|
|
43
|
+
raise ValueError(f"iv {iv} 长度不等于16")
|
|
44
|
+
|
|
58
45
|
self.key = key
|
|
59
46
|
self.iv = iv
|
|
60
47
|
|
|
@@ -64,40 +51,38 @@ class M3u8Downloader:
|
|
|
64
51
|
M3u8 异步下载器,并保留hls文件
|
|
65
52
|
"""
|
|
66
53
|
|
|
67
|
-
retry_count: int = 0
|
|
68
|
-
retry_max_count: int = 50
|
|
69
|
-
ts_url_list: list = []
|
|
70
|
-
ts_path_list: list = []
|
|
71
|
-
ts_key: M3u8Key = None
|
|
72
|
-
mp4_head_hrl: str = None
|
|
73
|
-
m3u8_md5 = ""
|
|
74
|
-
|
|
75
54
|
def __init__(
|
|
76
55
|
self,
|
|
77
56
|
m3u8_url: str,
|
|
78
57
|
save_path: str,
|
|
79
|
-
|
|
80
|
-
max_workers=None,
|
|
81
|
-
headers=None,
|
|
82
|
-
key: M3u8Key = None,
|
|
83
|
-
get_m3u8_func: Callable = None,
|
|
84
|
-
m3u8_request_before: Callable[[RequestModel], RequestModel] = None,
|
|
85
|
-
|
|
86
|
-
|
|
58
|
+
is_decrypt: bool = False,
|
|
59
|
+
max_workers: int | None = None,
|
|
60
|
+
headers: dict[str, Any] | None = None,
|
|
61
|
+
key: M3u8Key | None = None,
|
|
62
|
+
get_m3u8_func: Callable | None = None,
|
|
63
|
+
m3u8_request_before: Callable[[RequestModel], RequestModel] | None = None,
|
|
64
|
+
m3u8_response_after: Callable[[Response], Response] | None = None,
|
|
65
|
+
key_request_before: Callable[[RequestModel], RequestModel] | None = None,
|
|
66
|
+
key_response_after: Callable[[Response], Response] | None = None,
|
|
67
|
+
ts_request_before: Callable[[RequestModel], RequestModel] | None = None,
|
|
68
|
+
ts_response_after: Callable[[Response], Response] | None = None,
|
|
87
69
|
):
|
|
88
70
|
"""
|
|
89
71
|
|
|
90
72
|
Args:
|
|
91
73
|
m3u8_url: m3u8 地址
|
|
92
74
|
save_path: 保存路径
|
|
93
|
-
|
|
75
|
+
is_decrypt: 如果ts被加密,是否解密ts
|
|
94
76
|
max_workers: 最大并发数
|
|
95
77
|
headers: 情求头
|
|
96
78
|
get_m3u8_func: 处理m3u8情求的回调函数。适用于m3u8地址不是真正的地址,
|
|
97
79
|
而是包含m3u8内容的情求,会把m3u8_url的响应传递给get_m3u8_func,要求返回真正的m3u8内容
|
|
98
80
|
m3u8_request_before: m3u8请求前的回调函数
|
|
81
|
+
m3u8_response_after: m3u8响应后的回调函数
|
|
99
82
|
key_request_before: key请求前的回调函数
|
|
100
|
-
|
|
83
|
+
key_response_after: key响应后的回调函数
|
|
84
|
+
ts_request_before: ts请求前的回调函数
|
|
85
|
+
ts_response_after: ts响应后的回调函数
|
|
101
86
|
"""
|
|
102
87
|
|
|
103
88
|
sem = asyncio.Semaphore(max_workers) if max_workers else None
|
|
@@ -107,18 +92,24 @@ class M3u8Downloader:
|
|
|
107
92
|
self.m3u8_net = Net(sem=sem)
|
|
108
93
|
if m3u8_request_before:
|
|
109
94
|
self.m3u8_net.request_before_signal.connect(m3u8_request_before)
|
|
95
|
+
if m3u8_response_after:
|
|
96
|
+
self.m3u8_net.response_after_signal.connect(m3u8_response_after)
|
|
110
97
|
|
|
111
98
|
# 加密key的请求器
|
|
112
99
|
self.key_net = Net()
|
|
113
100
|
if key_request_before:
|
|
114
101
|
self.key_net.request_before_signal.connect(key_request_before)
|
|
102
|
+
if key_response_after:
|
|
103
|
+
self.key_net.response_after_signal.connect(key_response_after)
|
|
115
104
|
|
|
116
105
|
# ts内容的请求器
|
|
117
106
|
self.ts_net = Net()
|
|
118
107
|
if ts_request_before:
|
|
119
108
|
self.ts_net.request_before_signal.connect(ts_request_before)
|
|
109
|
+
if ts_response_after:
|
|
110
|
+
self.ts_net.response_after_signal.connect(ts_response_after)
|
|
120
111
|
|
|
121
|
-
self.
|
|
112
|
+
self.is_decrypt = is_decrypt
|
|
122
113
|
self.m3u8_url = urlparse(m3u8_url)
|
|
123
114
|
self.get_m3u8_func = get_m3u8_func
|
|
124
115
|
self.save_dir = Path(save_path) / "hls"
|
|
@@ -126,6 +117,15 @@ class M3u8Downloader:
|
|
|
126
117
|
self.key_path = self.save_dir / "key.key"
|
|
127
118
|
self.custom_key = key
|
|
128
119
|
|
|
120
|
+
# 实例变量初始化
|
|
121
|
+
self.retry_count: int = 0
|
|
122
|
+
self.retry_max_count: int = 50
|
|
123
|
+
self.ts_url_list: list = []
|
|
124
|
+
self.ts_path_list: list = []
|
|
125
|
+
self.ts_key: M3u8Key | None = None
|
|
126
|
+
self.mp4_head_url: str | None = None
|
|
127
|
+
self.m3u8_md5: str = ""
|
|
128
|
+
|
|
129
129
|
if not self.save_dir.exists():
|
|
130
130
|
self.save_dir.mkdir(parents=True)
|
|
131
131
|
|
|
@@ -141,21 +141,25 @@ class M3u8Downloader:
|
|
|
141
141
|
async def start(self, merge=True, del_hls=False):
|
|
142
142
|
"""
|
|
143
143
|
下载器启动函数
|
|
144
|
-
:
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
Args:
|
|
145
|
+
merge: ts下载完后是否合并,默认合并
|
|
146
|
+
del_hls: 是否删除hls系列文件,包括.m3u8文件、*.ts、.key文件
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
|
|
147
150
|
"""
|
|
148
151
|
mp4_path = self.save_dir.parent / f"{self.save_name}.mp4"
|
|
149
|
-
|
|
152
|
+
mp4_path = mp4_path.absolute()
|
|
153
|
+
if mp4_path.exists():
|
|
150
154
|
self.logger.info(f"{mp4_path}已存在")
|
|
151
|
-
if del_hls:
|
|
155
|
+
if del_hls and self.save_dir.exists():
|
|
152
156
|
shutil.rmtree(str(self.save_dir))
|
|
153
157
|
return True
|
|
154
158
|
|
|
155
159
|
self.logger.info(
|
|
156
160
|
f"开始下载: 合并ts为mp4={merge}, "
|
|
157
161
|
f"删除hls信息={del_hls}, "
|
|
158
|
-
f"下载地址为:{self.m3u8_url.geturl()}. 保存路径为:{self.save_dir}"
|
|
162
|
+
f"下载地址为:{self.m3u8_url.geturl()}. 保存路径为:{self.save_dir.absolute()}"
|
|
159
163
|
)
|
|
160
164
|
|
|
161
165
|
await self._download()
|
|
@@ -165,7 +169,7 @@ class M3u8Downloader:
|
|
|
165
169
|
self.logger.info(f"TS应下载数量为:{count_1}, 实际下载数量为:{count_2}")
|
|
166
170
|
if count_1 == 0 or count_2 == 0:
|
|
167
171
|
self.logger.error("ts数量为0,请检查!!!")
|
|
168
|
-
return
|
|
172
|
+
return None
|
|
169
173
|
|
|
170
174
|
if count_2 != count_1:
|
|
171
175
|
self.logger.error(f"ts下载数量与实际数量不符合!!!应该下载数量为:{count_1}, 实际下载数量为:{count_2}")
|
|
@@ -174,7 +178,7 @@ class M3u8Downloader:
|
|
|
174
178
|
if self.retry_count < self.retry_max_count:
|
|
175
179
|
self.retry_count += 1
|
|
176
180
|
self.logger.error(f"正在进行重试:{self.retry_count}/{self.retry_max_count}")
|
|
177
|
-
return self.start(merge, del_hls)
|
|
181
|
+
return await self.start(merge, del_hls)
|
|
178
182
|
return False
|
|
179
183
|
|
|
180
184
|
if not merge:
|
|
@@ -187,24 +191,30 @@ class M3u8Downloader:
|
|
|
187
191
|
f"mp4合并失败. ts应该下载数量为:{count_1}, 实际下载数量为:{count_2}. 保存路径为:{self.save_dir}"
|
|
188
192
|
)
|
|
189
193
|
return False
|
|
194
|
+
|
|
190
195
|
if del_hls:
|
|
191
196
|
shutil.rmtree(str(self.save_dir))
|
|
197
|
+
|
|
192
198
|
return True
|
|
193
199
|
|
|
194
200
|
async def _download(self):
|
|
195
201
|
"""
|
|
196
202
|
下载ts文件、m3u8文件、key文件
|
|
197
|
-
:
|
|
203
|
+
Returns:
|
|
204
|
+
|
|
198
205
|
"""
|
|
199
206
|
self.ts_url_list = await self.get_ts_list(self.m3u8_url)
|
|
200
207
|
self.ts_path_list = [None] * len(self.ts_url_list)
|
|
201
208
|
await asyncio.gather(*[self._download_ts(url) for url in self.ts_url_list])
|
|
202
209
|
|
|
203
|
-
async def get_ts_list(self, url) -> list[dict]:
|
|
210
|
+
async def get_ts_list(self, url: ParseResult) -> list[dict]:
|
|
204
211
|
"""
|
|
205
212
|
解析m3u8并保存至列表
|
|
206
|
-
:
|
|
207
|
-
|
|
213
|
+
Args:
|
|
214
|
+
url: m3u8地址
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
|
|
208
218
|
"""
|
|
209
219
|
resp = await self.m3u8_net.get(url.geturl(), headers=self.headers)
|
|
210
220
|
m3u8_text = self.get_m3u8_func(resp.text) if self.get_m3u8_func else resp.text
|
|
@@ -231,7 +241,7 @@ class M3u8Downloader:
|
|
|
231
241
|
if segment_map_count > 0:
|
|
232
242
|
if segment_map_count > 1:
|
|
233
243
|
raise ValueError("暂不支持segment_map有多个的情况,请提交issues,并告知m3u8的地址,方便做适配")
|
|
234
|
-
self.
|
|
244
|
+
self.mp4_head_url = prefix + m3u8_obj.segment_map[0].uri
|
|
235
245
|
m3u8_obj.segment_map[0].uri = "head.mp4"
|
|
236
246
|
|
|
237
247
|
# 遍历ts文件
|
|
@@ -267,32 +277,40 @@ class M3u8Downloader:
|
|
|
267
277
|
async def _download_ts(self, ts_item: dict):
|
|
268
278
|
"""
|
|
269
279
|
下载ts
|
|
270
|
-
:
|
|
271
|
-
|
|
280
|
+
Args:
|
|
281
|
+
ts_item: ts数据
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
|
|
272
285
|
"""
|
|
273
286
|
index = ts_item["index"]
|
|
274
287
|
ts_uri = ts_item["uri"]
|
|
275
288
|
ts_path = self.save_dir / f"{index}.ts"
|
|
276
|
-
|
|
289
|
+
|
|
290
|
+
if ts_path.exists():
|
|
277
291
|
self.ts_path_list[index] = str(ts_path)
|
|
278
292
|
return
|
|
293
|
+
|
|
279
294
|
resp = await self.ts_net.get(ts_item["uri"], self.headers)
|
|
280
295
|
ts_content = resp.content
|
|
281
296
|
if ts_content is None:
|
|
282
297
|
return
|
|
283
298
|
|
|
284
|
-
if self.ts_key and self.
|
|
285
|
-
ts_content = crypto.
|
|
299
|
+
if self.ts_key and self.is_decrypt:
|
|
300
|
+
ts_content = crypto.decrypt_aes_256_cbc(ts_content, self.ts_key.key, self.ts_key.iv)
|
|
286
301
|
|
|
287
302
|
self.save_file(ts_content, ts_path)
|
|
288
303
|
self.logger.info(f"{ts_uri}下载成功")
|
|
289
304
|
self.ts_path_list[index] = str(ts_path)
|
|
290
305
|
|
|
291
|
-
async def merge(self):
|
|
306
|
+
async def merge(self) -> bool:
|
|
292
307
|
"""
|
|
293
308
|
合并ts文件为mp4文件
|
|
294
|
-
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
返回是否合并成功
|
|
295
312
|
"""
|
|
313
|
+
|
|
296
314
|
self.logger.info("开始合并mp4")
|
|
297
315
|
if len(self.ts_path_list) != len(self.ts_url_list):
|
|
298
316
|
self.logger.error("数量不足拒绝合并!")
|
|
@@ -308,47 +326,99 @@ class M3u8Downloader:
|
|
|
308
326
|
|
|
309
327
|
# 如果有mp4的头,则把ts放到后面
|
|
310
328
|
mp4_head_data = b""
|
|
311
|
-
if self.
|
|
312
|
-
resp = await self.ts_net.get(self.
|
|
329
|
+
if self.mp4_head_url:
|
|
330
|
+
resp = await self.ts_net.get(self.mp4_head_url)
|
|
313
331
|
mp4_head_data = resp.content
|
|
314
332
|
mp4_head_file = self.save_dir / "head.mp4"
|
|
315
333
|
mp4_head_file.write_bytes(mp4_head_data)
|
|
316
334
|
|
|
317
335
|
# 把ts文件整合到一起
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
big_ts_file.close()
|
|
336
|
+
with big_ts_path.open("ab+") as big_ts_file:
|
|
337
|
+
big_ts_file.write(mp4_head_data)
|
|
338
|
+
for path in self.ts_path_list:
|
|
339
|
+
with open(path, "rb") as ts_file:
|
|
340
|
+
data = ts_file.read()
|
|
341
|
+
if self.ts_key:
|
|
342
|
+
data = crypto.decrypt_aes_256_cbc(data, self.ts_key.key, self.ts_key.iv)
|
|
343
|
+
big_ts_file.write(data)
|
|
327
344
|
self.logger.info("ts文件整合完毕")
|
|
328
345
|
|
|
329
346
|
# 把大的ts文件转换成mp4文件
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
self.logger.info(f"ts整合成功,开始转为mp4。 command:{command}")
|
|
336
|
-
result = subprocess.run(command, shell=True, capture_output=True, text=True)
|
|
337
|
-
if result.returncode != 0:
|
|
338
|
-
logger.error(f"命令执行失败: {result.stderr or result.stdout}")
|
|
347
|
+
self.logger.info(f"ts整合成功,开始转为mp4。 ts路径:{big_ts_path} mp4路径:{mp4_path}")
|
|
348
|
+
result = self.ts_to_mp4(big_ts_path, mp4_path)
|
|
349
|
+
if not result:
|
|
350
|
+
self.logger.error("ts转mp4失败!")
|
|
351
|
+
return False
|
|
339
352
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
return
|
|
353
|
+
# 删除完整ts文件
|
|
354
|
+
big_ts_path.unlink()
|
|
355
|
+
return True
|
|
343
356
|
|
|
344
357
|
@staticmethod
|
|
345
|
-
def save_file(content: bytes | str, filepath):
|
|
358
|
+
def save_file(content: bytes | str, filepath: Path | str):
|
|
346
359
|
"""
|
|
347
360
|
保存内容到文件
|
|
348
|
-
:
|
|
349
|
-
|
|
350
|
-
|
|
361
|
+
Args:
|
|
362
|
+
content: 内容
|
|
363
|
+
filepath: 文件路径
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
|
|
351
367
|
"""
|
|
352
368
|
mode = "wb" if isinstance(content, bytes) else "w"
|
|
353
369
|
with open(file=filepath, mode=mode) as file:
|
|
354
370
|
file.write(content)
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def ts_to_mp4(ts_path: Path, mp4_path: Path) -> bool:
|
|
374
|
+
"""
|
|
375
|
+
将 TS 转为 MP4 (stream copy,不重编码)
|
|
376
|
+
Args:
|
|
377
|
+
ts_path: ts 视频文件路径
|
|
378
|
+
mp4_path: mp4 视频文件路径
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
返回是否转换成功:mp4路径存在并且是一个文件并且大小大于0
|
|
382
|
+
"""
|
|
383
|
+
if not ts_path.exists():
|
|
384
|
+
raise FileNotFoundError("ts文件不存在")
|
|
385
|
+
|
|
386
|
+
if not mp4_path.parent.exists():
|
|
387
|
+
mp4_path.parent.mkdir(parents=True)
|
|
388
|
+
|
|
389
|
+
with av.open(str(ts_path)) as input_container, av.open(str(mp4_path), "w") as output_container:
|
|
390
|
+
# 映射视频流
|
|
391
|
+
out_stream = None
|
|
392
|
+
if input_container.streams.video:
|
|
393
|
+
in_stream = input_container.streams.video[0]
|
|
394
|
+
out_stream = output_container.add_stream(in_stream.codec_context.name)
|
|
395
|
+
out_stream.width = in_stream.codec_context.width
|
|
396
|
+
out_stream.height = in_stream.codec_context.height
|
|
397
|
+
out_stream.pix_fmt = in_stream.codec_context.pix_fmt
|
|
398
|
+
if in_stream.average_rate:
|
|
399
|
+
out_stream.rate = in_stream.average_rate
|
|
400
|
+
|
|
401
|
+
# 映射音频流 (如果存在)
|
|
402
|
+
out_audio = None
|
|
403
|
+
if input_container.streams.audio:
|
|
404
|
+
in_audio = input_container.streams.audio[0]
|
|
405
|
+
out_audio = output_container.add_stream(in_audio.codec_context.name)
|
|
406
|
+
out_audio.rate = in_audio.codec_context.sample_rate # type: ignore
|
|
407
|
+
if in_audio.codec_context.layout:
|
|
408
|
+
out_audio.layout = in_audio.codec_context.layout # type: ignore
|
|
409
|
+
if in_audio.codec_context.format:
|
|
410
|
+
out_audio.format = in_audio.codec_context.format # type: ignore
|
|
411
|
+
|
|
412
|
+
# Stream copy - 直接复制数据包,不重编码
|
|
413
|
+
for packet in input_container.demux():
|
|
414
|
+
if packet.dts is None:
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
if packet.stream.type == "video" and out_stream:
|
|
418
|
+
packet.stream = out_stream
|
|
419
|
+
output_container.mux(packet)
|
|
420
|
+
elif packet.stream.type == "audio" and out_audio:
|
|
421
|
+
packet.stream = out_audio
|
|
422
|
+
output_container.mux(packet)
|
|
423
|
+
|
|
424
|
+
return mp4_path.exists() and mp4_path.is_file() and mp4_path.stat().st_size > 0
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# See https://pre-commit.com for more information
|
|
2
|
-
# See https://pre-commit.com/hooks.html for more hooks
|
|
3
|
-
repos:
|
|
4
|
-
# bandit
|
|
5
|
-
- repo: https://github.com/PyCQA/bandit
|
|
6
|
-
rev: 1.7.10
|
|
7
|
-
hooks:
|
|
8
|
-
- id: bandit
|
|
9
|
-
args: [ "-c", "pyproject.toml" ]
|
|
10
|
-
additional_dependencies: [ "bandit[toml]" ]
|
|
11
|
-
|
|
12
|
-
# ruff
|
|
13
|
-
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
14
|
-
rev: 'v0.6.9'
|
|
15
|
-
hooks:
|
|
16
|
-
- id: ruff
|
|
17
|
-
args: [ --fix, --exit-non-zero-on-fix, --show-fixes ]
|
|
18
|
-
- id: ruff-format
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
# generated by rye
|
|
2
|
-
# use `rye lock` or `rye sync` to update this lockfile
|
|
3
|
-
#
|
|
4
|
-
# last locked with the following flags:
|
|
5
|
-
# pre: false
|
|
6
|
-
# features: []
|
|
7
|
-
# all-features: false
|
|
8
|
-
# with-sources: false
|
|
9
|
-
# generate-hashes: false
|
|
10
|
-
# universal: false
|
|
11
|
-
|
|
12
|
-
-e file:.
|
|
13
|
-
aiohttp==3.9.5
|
|
14
|
-
# via hssp
|
|
15
|
-
aiosignal==1.3.1
|
|
16
|
-
# via aiohttp
|
|
17
|
-
annotated-types==0.7.0
|
|
18
|
-
# via pydantic
|
|
19
|
-
anyio==4.6.2.post1
|
|
20
|
-
# via httpx
|
|
21
|
-
apscheduler==3.10.4
|
|
22
|
-
# via hssp
|
|
23
|
-
attrs==24.2.0
|
|
24
|
-
# via aiohttp
|
|
25
|
-
blinker==1.9.0
|
|
26
|
-
# via hssp
|
|
27
|
-
certifi==2024.8.30
|
|
28
|
-
# via curl-cffi
|
|
29
|
-
# via httpcore
|
|
30
|
-
# via httpx
|
|
31
|
-
# via requests
|
|
32
|
-
cffi==1.17.1
|
|
33
|
-
# via curl-cffi
|
|
34
|
-
cfgv==3.4.0
|
|
35
|
-
# via pre-commit
|
|
36
|
-
charset-normalizer==3.4.0
|
|
37
|
-
# via requests
|
|
38
|
-
click==8.1.7
|
|
39
|
-
# via drissionpage
|
|
40
|
-
cssselect==1.2.0
|
|
41
|
-
# via drissionpage
|
|
42
|
-
# via parsel
|
|
43
|
-
curl-cffi==0.7.3
|
|
44
|
-
# via hssp
|
|
45
|
-
datarecorder==3.6.2
|
|
46
|
-
# via downloadkit
|
|
47
|
-
distlib==0.3.9
|
|
48
|
-
# via virtualenv
|
|
49
|
-
docutils==0.21.2
|
|
50
|
-
# via readme-renderer
|
|
51
|
-
downloadkit==2.0.5
|
|
52
|
-
# via drissionpage
|
|
53
|
-
drissionpage==4.1.0.12
|
|
54
|
-
# via hssp
|
|
55
|
-
et-xmlfile==2.0.0
|
|
56
|
-
# via openpyxl
|
|
57
|
-
fake-useragent==1.5.1
|
|
58
|
-
# via hssp
|
|
59
|
-
filelock==3.16.1
|
|
60
|
-
# via tldextract
|
|
61
|
-
# via virtualenv
|
|
62
|
-
frozenlist==1.5.0
|
|
63
|
-
# via aiohttp
|
|
64
|
-
# via aiosignal
|
|
65
|
-
furl==2.1.3
|
|
66
|
-
# via hssp
|
|
67
|
-
h11==0.14.0
|
|
68
|
-
# via httpcore
|
|
69
|
-
h2==4.1.0
|
|
70
|
-
# via httpx
|
|
71
|
-
hpack==4.0.0
|
|
72
|
-
# via h2
|
|
73
|
-
hssp==0.4.7
|
|
74
|
-
# via hs-m3u8
|
|
75
|
-
httpcore==1.0.6
|
|
76
|
-
# via httpx
|
|
77
|
-
httpx==0.27.2
|
|
78
|
-
# via hssp
|
|
79
|
-
hyperframe==6.0.1
|
|
80
|
-
# via h2
|
|
81
|
-
identify==2.6.2
|
|
82
|
-
# via pre-commit
|
|
83
|
-
idna==3.10
|
|
84
|
-
# via anyio
|
|
85
|
-
# via httpx
|
|
86
|
-
# via requests
|
|
87
|
-
# via tldextract
|
|
88
|
-
# via yarl
|
|
89
|
-
jaraco-classes==3.4.0
|
|
90
|
-
# via keyring
|
|
91
|
-
jaraco-context==6.0.1
|
|
92
|
-
# via keyring
|
|
93
|
-
jaraco-functools==4.1.0
|
|
94
|
-
# via keyring
|
|
95
|
-
jmespath==1.0.1
|
|
96
|
-
# via parsel
|
|
97
|
-
keyring==25.6.0
|
|
98
|
-
# via twine
|
|
99
|
-
loguru==0.7.2
|
|
100
|
-
# via hssp
|
|
101
|
-
lxml==5.3.0
|
|
102
|
-
# via drissionpage
|
|
103
|
-
# via parsel
|
|
104
|
-
m3u8==6.0.0
|
|
105
|
-
# via hs-m3u8
|
|
106
|
-
markdown-it-py==3.0.0
|
|
107
|
-
# via rich
|
|
108
|
-
mdurl==0.1.2
|
|
109
|
-
# via markdown-it-py
|
|
110
|
-
more-itertools==10.6.0
|
|
111
|
-
# via jaraco-classes
|
|
112
|
-
# via jaraco-functools
|
|
113
|
-
multidict==6.1.0
|
|
114
|
-
# via aiohttp
|
|
115
|
-
# via yarl
|
|
116
|
-
nh3==0.2.20
|
|
117
|
-
# via readme-renderer
|
|
118
|
-
nodeenv==1.9.1
|
|
119
|
-
# via pre-commit
|
|
120
|
-
openpyxl==3.1.5
|
|
121
|
-
# via datarecorder
|
|
122
|
-
orderedmultidict==1.0.1
|
|
123
|
-
# via furl
|
|
124
|
-
packaging==24.2
|
|
125
|
-
# via parsel
|
|
126
|
-
# via twine
|
|
127
|
-
parsel==1.9.1
|
|
128
|
-
# via hssp
|
|
129
|
-
pkginfo==1.12.0
|
|
130
|
-
# via twine
|
|
131
|
-
platformdirs==4.3.6
|
|
132
|
-
# via virtualenv
|
|
133
|
-
pre-commit==4.0.1
|
|
134
|
-
propcache==0.2.0
|
|
135
|
-
# via yarl
|
|
136
|
-
psutil==6.1.0
|
|
137
|
-
# via drissionpage
|
|
138
|
-
pycparser==2.22
|
|
139
|
-
# via cffi
|
|
140
|
-
pycryptodomex==3.21.0
|
|
141
|
-
# via hssp
|
|
142
|
-
pydantic==2.9.2
|
|
143
|
-
# via hssp
|
|
144
|
-
# via pydantic-settings
|
|
145
|
-
pydantic-core==2.23.4
|
|
146
|
-
# via pydantic
|
|
147
|
-
pydantic-settings==2.6.1
|
|
148
|
-
# via hssp
|
|
149
|
-
pygments==2.19.1
|
|
150
|
-
# via readme-renderer
|
|
151
|
-
# via rich
|
|
152
|
-
python-dotenv==1.0.1
|
|
153
|
-
# via pydantic-settings
|
|
154
|
-
pytz==2024.2
|
|
155
|
-
# via apscheduler
|
|
156
|
-
pyyaml==6.0.2
|
|
157
|
-
# via pre-commit
|
|
158
|
-
# via pydantic-settings
|
|
159
|
-
readme-renderer==44.0
|
|
160
|
-
# via twine
|
|
161
|
-
requests==2.32.3
|
|
162
|
-
# via downloadkit
|
|
163
|
-
# via drissionpage
|
|
164
|
-
# via hssp
|
|
165
|
-
# via requests-file
|
|
166
|
-
# via requests-toolbelt
|
|
167
|
-
# via tldextract
|
|
168
|
-
# via twine
|
|
169
|
-
requests-file==2.1.0
|
|
170
|
-
# via tldextract
|
|
171
|
-
requests-toolbelt==1.0.0
|
|
172
|
-
# via twine
|
|
173
|
-
rfc3986==2.0.0
|
|
174
|
-
# via twine
|
|
175
|
-
rich==13.9.4
|
|
176
|
-
# via twine
|
|
177
|
-
six==1.16.0
|
|
178
|
-
# via apscheduler
|
|
179
|
-
# via furl
|
|
180
|
-
# via orderedmultidict
|
|
181
|
-
sniffio==1.3.1
|
|
182
|
-
# via anyio
|
|
183
|
-
# via httpx
|
|
184
|
-
tenacity==9.0.0
|
|
185
|
-
# via hssp
|
|
186
|
-
tldextract==5.1.3
|
|
187
|
-
# via drissionpage
|
|
188
|
-
tomli==2.1.0
|
|
189
|
-
# via pydantic-settings
|
|
190
|
-
twine==6.0.1
|
|
191
|
-
typing-extensions==4.12.2
|
|
192
|
-
# via curl-cffi
|
|
193
|
-
# via pydantic
|
|
194
|
-
# via pydantic-core
|
|
195
|
-
tzlocal==5.2
|
|
196
|
-
# via apscheduler
|
|
197
|
-
urllib3==2.2.3
|
|
198
|
-
# via requests
|
|
199
|
-
# via twine
|
|
200
|
-
uvloop==0.21.0
|
|
201
|
-
# via hssp
|
|
202
|
-
virtualenv==20.27.1
|
|
203
|
-
# via pre-commit
|
|
204
|
-
w3lib==2.2.1
|
|
205
|
-
# via parsel
|
|
206
|
-
websocket-client==1.8.0
|
|
207
|
-
# via drissionpage
|
|
208
|
-
yarl==1.17.1
|
|
209
|
-
# via aiohttp
|
hs_m3u8-0.1.6/requirements.lock
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
# generated by rye
|
|
2
|
-
# use `rye lock` or `rye sync` to update this lockfile
|
|
3
|
-
#
|
|
4
|
-
# last locked with the following flags:
|
|
5
|
-
# pre: false
|
|
6
|
-
# features: []
|
|
7
|
-
# all-features: false
|
|
8
|
-
# with-sources: false
|
|
9
|
-
# generate-hashes: false
|
|
10
|
-
# universal: false
|
|
11
|
-
|
|
12
|
-
-e file:.
|
|
13
|
-
aiohttp==3.9.5
|
|
14
|
-
# via hssp
|
|
15
|
-
aiosignal==1.3.1
|
|
16
|
-
# via aiohttp
|
|
17
|
-
annotated-types==0.7.0
|
|
18
|
-
# via pydantic
|
|
19
|
-
anyio==4.6.2.post1
|
|
20
|
-
# via httpx
|
|
21
|
-
apscheduler==3.10.4
|
|
22
|
-
# via hssp
|
|
23
|
-
attrs==24.2.0
|
|
24
|
-
# via aiohttp
|
|
25
|
-
blinker==1.9.0
|
|
26
|
-
# via hssp
|
|
27
|
-
certifi==2024.8.30
|
|
28
|
-
# via curl-cffi
|
|
29
|
-
# via httpcore
|
|
30
|
-
# via httpx
|
|
31
|
-
# via requests
|
|
32
|
-
cffi==1.17.1
|
|
33
|
-
# via curl-cffi
|
|
34
|
-
charset-normalizer==3.4.0
|
|
35
|
-
# via requests
|
|
36
|
-
click==8.1.7
|
|
37
|
-
# via drissionpage
|
|
38
|
-
cssselect==1.2.0
|
|
39
|
-
# via drissionpage
|
|
40
|
-
# via parsel
|
|
41
|
-
curl-cffi==0.7.3
|
|
42
|
-
# via hssp
|
|
43
|
-
datarecorder==3.6.2
|
|
44
|
-
# via downloadkit
|
|
45
|
-
downloadkit==2.0.5
|
|
46
|
-
# via drissionpage
|
|
47
|
-
drissionpage==4.1.0.12
|
|
48
|
-
# via hssp
|
|
49
|
-
et-xmlfile==2.0.0
|
|
50
|
-
# via openpyxl
|
|
51
|
-
fake-useragent==1.5.1
|
|
52
|
-
# via hssp
|
|
53
|
-
filelock==3.16.1
|
|
54
|
-
# via tldextract
|
|
55
|
-
frozenlist==1.5.0
|
|
56
|
-
# via aiohttp
|
|
57
|
-
# via aiosignal
|
|
58
|
-
furl==2.1.3
|
|
59
|
-
# via hssp
|
|
60
|
-
h11==0.14.0
|
|
61
|
-
# via httpcore
|
|
62
|
-
h2==4.1.0
|
|
63
|
-
# via httpx
|
|
64
|
-
hpack==4.0.0
|
|
65
|
-
# via h2
|
|
66
|
-
hssp==0.4.7
|
|
67
|
-
# via hs-m3u8
|
|
68
|
-
httpcore==1.0.6
|
|
69
|
-
# via httpx
|
|
70
|
-
httpx==0.27.2
|
|
71
|
-
# via hssp
|
|
72
|
-
hyperframe==6.0.1
|
|
73
|
-
# via h2
|
|
74
|
-
idna==3.10
|
|
75
|
-
# via anyio
|
|
76
|
-
# via httpx
|
|
77
|
-
# via requests
|
|
78
|
-
# via tldextract
|
|
79
|
-
# via yarl
|
|
80
|
-
jmespath==1.0.1
|
|
81
|
-
# via parsel
|
|
82
|
-
loguru==0.7.2
|
|
83
|
-
# via hssp
|
|
84
|
-
lxml==5.3.0
|
|
85
|
-
# via drissionpage
|
|
86
|
-
# via parsel
|
|
87
|
-
m3u8==6.0.0
|
|
88
|
-
# via hs-m3u8
|
|
89
|
-
multidict==6.1.0
|
|
90
|
-
# via aiohttp
|
|
91
|
-
# via yarl
|
|
92
|
-
openpyxl==3.1.5
|
|
93
|
-
# via datarecorder
|
|
94
|
-
orderedmultidict==1.0.1
|
|
95
|
-
# via furl
|
|
96
|
-
packaging==24.2
|
|
97
|
-
# via parsel
|
|
98
|
-
parsel==1.9.1
|
|
99
|
-
# via hssp
|
|
100
|
-
propcache==0.2.0
|
|
101
|
-
# via yarl
|
|
102
|
-
psutil==6.1.0
|
|
103
|
-
# via drissionpage
|
|
104
|
-
pycparser==2.22
|
|
105
|
-
# via cffi
|
|
106
|
-
pycryptodomex==3.21.0
|
|
107
|
-
# via hssp
|
|
108
|
-
pydantic==2.9.2
|
|
109
|
-
# via hssp
|
|
110
|
-
# via pydantic-settings
|
|
111
|
-
pydantic-core==2.23.4
|
|
112
|
-
# via pydantic
|
|
113
|
-
pydantic-settings==2.6.1
|
|
114
|
-
# via hssp
|
|
115
|
-
python-dotenv==1.0.1
|
|
116
|
-
# via pydantic-settings
|
|
117
|
-
pytz==2024.2
|
|
118
|
-
# via apscheduler
|
|
119
|
-
pyyaml==6.0.2
|
|
120
|
-
# via pydantic-settings
|
|
121
|
-
requests==2.32.3
|
|
122
|
-
# via downloadkit
|
|
123
|
-
# via drissionpage
|
|
124
|
-
# via hssp
|
|
125
|
-
# via requests-file
|
|
126
|
-
# via tldextract
|
|
127
|
-
requests-file==2.1.0
|
|
128
|
-
# via tldextract
|
|
129
|
-
six==1.16.0
|
|
130
|
-
# via apscheduler
|
|
131
|
-
# via furl
|
|
132
|
-
# via orderedmultidict
|
|
133
|
-
sniffio==1.3.1
|
|
134
|
-
# via anyio
|
|
135
|
-
# via httpx
|
|
136
|
-
tenacity==9.0.0
|
|
137
|
-
# via hssp
|
|
138
|
-
tldextract==5.1.3
|
|
139
|
-
# via drissionpage
|
|
140
|
-
tomli==2.1.0
|
|
141
|
-
# via pydantic-settings
|
|
142
|
-
typing-extensions==4.12.2
|
|
143
|
-
# via curl-cffi
|
|
144
|
-
# via pydantic
|
|
145
|
-
# via pydantic-core
|
|
146
|
-
tzlocal==5.2
|
|
147
|
-
# via apscheduler
|
|
148
|
-
urllib3==2.2.3
|
|
149
|
-
# via requests
|
|
150
|
-
uvloop==0.21.0
|
|
151
|
-
# via hssp
|
|
152
|
-
w3lib==2.2.1
|
|
153
|
-
# via parsel
|
|
154
|
-
websocket-client==1.8.0
|
|
155
|
-
# via drissionpage
|
|
156
|
-
yarl==1.17.1
|
|
157
|
-
# via aiohttp
|
|
File without changes
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
from hs_m3u8 import M3u8Downloader
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async def main():
|
|
7
|
-
url = "https://surrit.com/6d3bb2b2-d707-4b79-adf0-89542cb1383c/playlist.m3u8"
|
|
8
|
-
name = "SDAB-129"
|
|
9
|
-
dl = M3u8Downloader(m3u8_url=url, save_path=f"../../downloads/{name}", max_workers=64)
|
|
10
|
-
await dl.run(del_hls=False, merge=True)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if __name__ == "__main__":
|
|
14
|
-
asyncio.run(main())
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
from hs_m3u8 import M3u8Downloader
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def get_m3u8(resp_text: str):
|
|
7
|
-
"""
|
|
8
|
-
获取m3u8真实文本
|
|
9
|
-
Args:
|
|
10
|
-
resp_text: m3u8_url 获取到的响应文本
|
|
11
|
-
|
|
12
|
-
Returns:
|
|
13
|
-
返回真正的m3u8文本
|
|
14
|
-
|
|
15
|
-
"""
|
|
16
|
-
return resp_text
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
async def main():
|
|
20
|
-
url = "https://surrit.com/6d3bb2b2-d707-4b79-adf0-89542cb1383c/playlist.m3u8"
|
|
21
|
-
name = "SDAB-129"
|
|
22
|
-
headers = {
|
|
23
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
24
|
-
"(KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
|
|
25
|
-
}
|
|
26
|
-
dl = M3u8Downloader(
|
|
27
|
-
m3u8_url=url, save_path=f"../../downloads/{name}", headers=headers, max_workers=64, get_m3u8_func=get_m3u8
|
|
28
|
-
)
|
|
29
|
-
await dl.run(del_hls=False, merge=True)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if __name__ == "__main__":
|
|
33
|
-
asyncio.run(main())
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
from hs_m3u8 import M3u8Downloader
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async def main():
|
|
7
|
-
url = "https://surrit.com/85f671be-4ebc-4cad-961e-a8d339483cc6/playlist.m3u8"
|
|
8
|
-
name = "CUS-2413"
|
|
9
|
-
dl = M3u8Downloader(m3u8_url=url, save_path=f"../../downloads/{name}", max_workers=64)
|
|
10
|
-
await dl.run(del_hls=False, merge=True)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if __name__ == "__main__":
|
|
14
|
-
asyncio.run(main())
|
hs_m3u8-0.1.6/src/example/kyt.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# pip install hs-m3u8==0.1.4
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
|
|
5
|
-
from hs_m3u8 import M3u8Downloader, M3u8Key
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
async def main():
|
|
9
|
-
url = "https://r1-ndr-private.ykt.cbern.com.cn/edu_product/esp/assets/68b6bed7-d093-7c8c-9133-95cf8205d21d.t/zh-CN/1712805650284/transcode/videos/68b6bed7-d093-7c8c-9133-95cf8205d21d-1920x1080-true-47fe81c5c8d91daf25e9fffd7082f934-eed6db5a85074b6dbbe5fa71f1243b26.m3u8"
|
|
10
|
-
name = "ykt"
|
|
11
|
-
headers = {
|
|
12
|
-
"Accept": "*/*",
|
|
13
|
-
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
14
|
-
"Cache-Control": "no-cache",
|
|
15
|
-
"Connection": "keep-alive",
|
|
16
|
-
"Origin": "https://basic.smartedu.cn",
|
|
17
|
-
"Pragma": "no-cache",
|
|
18
|
-
"Referer": "https://basic.smartedu.cn/",
|
|
19
|
-
"Sec-Fetch-Dest": "empty",
|
|
20
|
-
"Sec-Fetch-Mode": "cors",
|
|
21
|
-
"Sec-Fetch-Site": "cross-site",
|
|
22
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
23
|
-
"Chrome/126.0.0.0 Safari/537.36",
|
|
24
|
-
"X-ND-AUTH": 'MAC id="7F938B205F876FC3C7550081F114A1A4028222C3BFB978FD9B439192D004CB8EEB65E66BB'
|
|
25
|
-
'C63E66FED6DD51F34F99411A6039E623E9A9D05",nonce="1742462574752:Z4IGAAV6"'
|
|
26
|
-
',mac="EUr56dXrCO1YGd3Ub1fj9MyJY9NxQPi7ZI14N/GFwpQ="',
|
|
27
|
-
"sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
|
|
28
|
-
"sec-ch-ua-mobile": "?0",
|
|
29
|
-
"sec-ch-ua-platform": '"Windows"',
|
|
30
|
-
}
|
|
31
|
-
key = M3u8Key(key=bytes.fromhex("34623235336163353939353834643437"))
|
|
32
|
-
dl = M3u8Downloader(
|
|
33
|
-
m3u8_url=url,
|
|
34
|
-
save_path=f"../../downloads/{name}",
|
|
35
|
-
max_workers=64,
|
|
36
|
-
headers=headers,
|
|
37
|
-
key=key,
|
|
38
|
-
)
|
|
39
|
-
await dl.run(del_hls=False, merge=True)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if __name__ == "__main__":
|
|
43
|
-
asyncio.run(main())
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
from hs_m3u8 import M3u8Downloader
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async def main():
|
|
7
|
-
url = "https://v3.dious.cc/20220422/EZWdBGuQ/index.m3u8"
|
|
8
|
-
name = "日日是好日"
|
|
9
|
-
dl = M3u8Downloader(m3u8_url=url, save_path=f"../../downloads/{name}", max_workers=64)
|
|
10
|
-
await dl.run(del_hls=False, merge=True)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if __name__ == "__main__":
|
|
14
|
-
asyncio.run(main())
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
from hs_m3u8 import M3u8Downloader
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async def main():
|
|
7
|
-
url = "https://v4.qrssv.com/202412/05/CCu6EzN8tR20/video/index.m3u8"
|
|
8
|
-
name = "毒液:最后一舞 HD-索尼"
|
|
9
|
-
dl = M3u8Downloader(m3u8_url=url, save_path=f"../../downloads/{name}", max_workers=64)
|
|
10
|
-
await dl.run(del_hls=False, merge=True)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if __name__ == "__main__":
|
|
14
|
-
asyncio.run(main())
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
from hs_m3u8 import M3u8Downloader
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async def main():
|
|
7
|
-
url = "http://1251107588.vod2.myqcloud.com/40f34e4dvodtransgzp1251107588/722ec94f387702303749471522/voddrm.token.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9~eyJ0eXBlIjoiRHJtVG9rZW4iLCJhcHBJZCI6MTI1MTEwNzU4OCwiZmlsZUlkIjoiMzg3NzAyMzAzNzQ5NDcxNTIyIiwiY3VycmVudFRpbWVTdGFtcCI6MCwiZXhwaXJlVGltZVN0YW1wIjoyMTQ3NDgzNjQ3LCJyYW5kb20iOjAsIm92ZXJsYXlLZXkiOiIiLCJvdmVybGF5SXYiOiIiLCJjaXBoZXJlZE92ZXJsYXlLZXkiOiI0NTQ2NzliYjY5Zjg1N2M3NGZjY2YxOGM1ZTk4ZjQyMjk4YTMxMzhkYjgyMTNkZThmYjA4ZDM2MzY0MjQ0NjQ3OWFhYmZlYjk0MmYzZTE1MWNkZjM2OGQ5NGEwMzhlNjI4YjlmMTM1OGM4ODkwMjlkMzcwMjZiYzQ3MTY0MGViMWI5ODExYTg5MWU1ZmYxODk3MjliNGIyOWU4ZWExZjNkZWNkZWJmYTVjZmY0N2U4YzBjYjU4OGE2MWUxZmMzNzNmZWQxYWZhOGU5MmJmMGQ4YjFmNjIwMjM4YWJjMzYyM2FlYWVjYjlkNDI3MmI2ZmMzMmRmNjBlN2VmYjc1NjkzIiwiY2lwaGVyZWRPdmVybGF5SXYiOiIwMTYxOTE2YmI5NWRjOWJlZDgyNmI5NmE0MTY1OThkM2IyZmYyYzJmM2JhZDVmNzg2NDEyMWFlYjJiMWVmNjQwNzg0NmNmZmI5YjkyN2E1YzFlMTFhMGE0MDNlMzg5MWE4Y2VkMTJhMTYyNWRlZDFlYWYwMmJkNTI2ZGFjZWE2OGRjNjc1NTE5ZmE3MjBhYTcxNGU3MGE2MjdhM2IwM2I0YzFjMWJjNjJmODYxMDAyN2M3ZjhlYzc4MTg1MDhlNDc0YTJiZmM3NTZjOTVmMWY3NGMxYWQ5NTkyNjBhZTM0NDczNDA4OWZlMGY5YWIzZmYwYzQxZTFlNGNjYWE4YjcxIiwia2V5SWQiOjEsInN0cmljdE1vZGUiOjAsInBlcnNpc3RlbnQiOiIiLCJyZW50YWxEdXJhdGlvbiI6MCwiZm9yY2VMMVRyYWNrVHlwZXMiOm51bGx9~OvCFF2x41XbwQK_C1fUE6Orr77BeCttlt6-H5Uj8izg.video_1407500_1.m3u8?encdomain=cmv1&sign=b574e853145eeb93fcec3b26d28b9665&t=7fffffff"
|
|
8
|
-
name = "siyuanren"
|
|
9
|
-
dl = M3u8Downloader(m3u8_url=url, save_path=f"../../downloads/{name}", max_workers=64)
|
|
10
|
-
await dl.run(del_hls=False, merge=True)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if __name__ == "__main__":
|
|
14
|
-
asyncio.run(main())
|
hs_m3u8-0.1.6/src/example/x.py
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
from hs_m3u8 import M3u8Downloader
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async def main():
|
|
7
|
-
url = (
|
|
8
|
-
"https://video.twimg.com/ext_tw_video/1879556885663342592/pu/pl/Vcvv0UK9lOhezJt1.m3u8?variant_version=1&tag=12"
|
|
9
|
-
)
|
|
10
|
-
name = "x"
|
|
11
|
-
dl = M3u8Downloader(m3u8_url=url, save_path=f"../../downloads/{name}", max_workers=64)
|
|
12
|
-
await dl.run(del_hls=False, merge=True)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if __name__ == "__main__":
|
|
16
|
-
asyncio.run(main())
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|