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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hs-m3u8
3
- Version: 0.1.6
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.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.6"
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.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.rye]
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 urllib.parse import urljoin, urlparse
15
- from zipfile import ZipFile
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
- :param key: 密钥
55
- :param iv: 偏移
30
+ Args:
31
+ key: 密钥
32
+ iv: 偏移,可以是十六进制字符串(0x开头)、bytes或None
56
33
  """
57
- iv = bytes.fromhex(iv[2:]) if iv and iv.startswith("0x") else iv
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
- decrypt=False,
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
- key_request_before: Callable[[RequestModel], RequestModel] = None,
86
- ts_request_before: Callable[[RequestModel], RequestModel] = None,
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
- decrypt: 如果ts被加密,是否解密ts
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
- ts_request_before: ts 请求前的回调函数
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.decrypt = decrypt
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
- :param merge: ts下载完后是否合并,默认合并
145
- :param del_hls: 是否删除hls系列文件,包括.m3u8文件、*.ts、.key文件
146
- :return:
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
- if Path(mp4_path).exists():
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
- :return:
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
- :param url:
207
- :return:
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.mp4_head_hrl = prefix + m3u8_obj.segment_map[0].uri
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
- :param ts_item: ts 数据
271
- :return:
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
- if Path(ts_path).exists():
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.decrypt:
285
- ts_content = crypto.decrypt_aes_256_cbc_pad7(ts_content, self.ts_key.key, self.ts_key.iv)
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
- :return:
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.mp4_head_hrl:
312
- resp = await self.ts_net.get(self.mp4_head_hrl)
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
- big_ts_file = big_ts_path.open("ab+")
319
- big_ts_file.write(mp4_head_data)
320
- for path in self.ts_path_list:
321
- with open(path, "rb") as ts_file:
322
- data = ts_file.read()
323
- if self.ts_key:
324
- data = crypto.decrypt_aes_256_cbc_pad7(data, self.ts_key.key, self.ts_key.iv)
325
- big_ts_file.write(data)
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
- ffmpeg_bin = get_ffmpeg()
331
- command = (
332
- f'{ffmpeg_bin} -i "{big_ts_path}" '
333
- f'-c copy -map 0:v -map 0:a? -bsf:a aac_adtstoasc -threads 32 "{mp4_path}" -y'
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
- if Path(mp4_path).exists():
341
- big_ts_path.unlink()
342
- return Path(mp4_path).exists()
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
- :param content: 内容
349
- :param filepath: 文件路径
350
- :return:
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
@@ -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())
@@ -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())
@@ -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())
File without changes
File without changes
File without changes