hs-m3u8 0.1.0a1__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.0a1/.gitignore +25 -0
- hs_m3u8-0.1.0a1/.pre-commit-config.yaml +18 -0
- hs_m3u8-0.1.0a1/PKG-INFO +65 -0
- hs_m3u8-0.1.0a1/README.md +42 -0
- hs_m3u8-0.1.0a1/pyproject.toml +140 -0
- hs_m3u8-0.1.0a1/requirements-dev.lock +172 -0
- hs_m3u8-0.1.0a1/requirements.lock +157 -0
- hs_m3u8-0.1.0a1/res/ffmpeg_win.exe.zip +0 -0
- hs_m3u8-0.1.0a1/src/example/__init__.py +0 -0
- hs_m3u8-0.1.0a1/src/example/jav_1.py +14 -0
- hs_m3u8-0.1.0a1/src/example/jav_2.py +33 -0
- hs_m3u8-0.1.0a1/src/example/jav_3.py +14 -0
- hs_m3u8-0.1.0a1/src/example/movie_1.py +14 -0
- hs_m3u8-0.1.0a1/src/example/movie_2.py +14 -0
- hs_m3u8-0.1.0a1/src/hs_m3u8/__init__.py +3 -0
- hs_m3u8-0.1.0a1/src/hs_m3u8/main.py +340 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# python generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# venv
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# rye
|
|
13
|
+
.python-version
|
|
14
|
+
|
|
15
|
+
# downloiad
|
|
16
|
+
downloads
|
|
17
|
+
|
|
18
|
+
# pycharm
|
|
19
|
+
.idea
|
|
20
|
+
|
|
21
|
+
# res
|
|
22
|
+
res/ffmpeg_win.exe
|
|
23
|
+
|
|
24
|
+
# Mac
|
|
25
|
+
.DS_Store
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
hs_m3u8-0.1.0a1/PKG-INFO
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: hs-m3u8
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: m3u8 下载器
|
|
5
|
+
Author-email: 昊色居士 <xhrtxh@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Operating System :: POSIX :: BSD
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: hssp>=0.4.4
|
|
21
|
+
Requires-Dist: m3u8>=6.0.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# hs-m3u8
|
|
25
|
+
|
|
26
|
+
m3u8 视频下载工具。支持大部分的m3u8视频下载。后续增加UI界面。
|
|
27
|
+
|
|
28
|
+
## 功能
|
|
29
|
+
|
|
30
|
+
- aes解密
|
|
31
|
+
- 自动选择高分辨m3u8
|
|
32
|
+
- 合并MP4
|
|
33
|
+
- 可选择保留ts文件
|
|
34
|
+
- 内置Windows平台ffmpeg可执行文件(由于Linux及Mac下权限问题,需自行安装ffmpeg文件)
|
|
35
|
+
|
|
36
|
+
## 使用
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
url = "https://surrit.com/6d3bb2b2-d707-4b79-adf0-89542cb1383c/playlist.m3u8"
|
|
40
|
+
name = "SDAB-129"
|
|
41
|
+
dl = M3u8Downloader(
|
|
42
|
+
url=url,
|
|
43
|
+
save_path=f"downloads/{name}",
|
|
44
|
+
max_workers=64
|
|
45
|
+
)
|
|
46
|
+
await dl.run(del_hls=False, merge=True)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- del_hls 为True时会删除ts、m3u8、key等文件,否则会经过处理后保留,以便直接使用
|
|
50
|
+
- merge 为True时会自动合并为mp4
|
|
51
|
+
|
|
52
|
+
## 安装
|
|
53
|
+
|
|
54
|
+
### rye 安装
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
rye sync
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### pip 安装
|
|
61
|
+
该`requirements.lock`文件是在Mac环境在生成的,不同系统环境下可能会遇到不同的效果,如果使用请使用`rye`安装
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install -r requirements.lock
|
|
65
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# hs-m3u8
|
|
2
|
+
|
|
3
|
+
m3u8 视频下载工具。支持大部分的m3u8视频下载。后续增加UI界面。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- aes解密
|
|
8
|
+
- 自动选择高分辨m3u8
|
|
9
|
+
- 合并MP4
|
|
10
|
+
- 可选择保留ts文件
|
|
11
|
+
- 内置Windows平台ffmpeg可执行文件(由于Linux及Mac下权限问题,需自行安装ffmpeg文件)
|
|
12
|
+
|
|
13
|
+
## 使用
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
url = "https://surrit.com/6d3bb2b2-d707-4b79-adf0-89542cb1383c/playlist.m3u8"
|
|
17
|
+
name = "SDAB-129"
|
|
18
|
+
dl = M3u8Downloader(
|
|
19
|
+
url=url,
|
|
20
|
+
save_path=f"downloads/{name}",
|
|
21
|
+
max_workers=64
|
|
22
|
+
)
|
|
23
|
+
await dl.run(del_hls=False, merge=True)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- del_hls 为True时会删除ts、m3u8、key等文件,否则会经过处理后保留,以便直接使用
|
|
27
|
+
- merge 为True时会自动合并为mp4
|
|
28
|
+
|
|
29
|
+
## 安装
|
|
30
|
+
|
|
31
|
+
### rye 安装
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
rye sync
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### pip 安装
|
|
38
|
+
该`requirements.lock`文件是在Mac环境在生成的,不同系统环境下可能会遇到不同的效果,如果使用请使用`rye`安装
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -r requirements.lock
|
|
42
|
+
```
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hs-m3u8"
|
|
3
|
+
version = "0.1.0a1"
|
|
4
|
+
description = "m3u8 下载器"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "昊色居士", email = "xhrtxh@gmail.com" }
|
|
7
|
+
]
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Intended Audience :: Developers",
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Operating System :: POSIX :: Linux",
|
|
12
|
+
"Operating System :: MacOS :: MacOS X",
|
|
13
|
+
"Operating System :: POSIX :: BSD",
|
|
14
|
+
"Operating System :: Microsoft :: Windows",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
license = "MIT"
|
|
24
|
+
readme = "README.md"
|
|
25
|
+
requires-python = ">= 3.10"
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"m3u8>=6.0.0",
|
|
29
|
+
"hssp>=0.4.4",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["hatchling"]
|
|
34
|
+
build-backend = "hatchling.build"
|
|
35
|
+
|
|
36
|
+
[tool.rye]
|
|
37
|
+
managed = true
|
|
38
|
+
dev-dependencies = [
|
|
39
|
+
"pre-commit>=4.0.1",
|
|
40
|
+
]
|
|
41
|
+
include = [
|
|
42
|
+
"src/hs_m3u8/"
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.metadata]
|
|
46
|
+
allow-direct-references = true
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/hs_m3u8"]
|
|
50
|
+
|
|
51
|
+
[[tool.rye.sources]]
|
|
52
|
+
name = "default"
|
|
53
|
+
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
54
|
+
|
|
55
|
+
[tool.bandit]
|
|
56
|
+
skips = [
|
|
57
|
+
"B404",
|
|
58
|
+
"B602",
|
|
59
|
+
"B501",
|
|
60
|
+
"B113"
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
# Exclude a variety of commonly ignored directories.
|
|
65
|
+
exclude = [
|
|
66
|
+
".bzr",
|
|
67
|
+
".direnv",
|
|
68
|
+
".eggs",
|
|
69
|
+
".git",
|
|
70
|
+
".git-rewrite",
|
|
71
|
+
".hg",
|
|
72
|
+
".ipynb_checkpoints",
|
|
73
|
+
".mypy_cache",
|
|
74
|
+
".nox",
|
|
75
|
+
".pants.d",
|
|
76
|
+
".pyenv",
|
|
77
|
+
".pytest_cache",
|
|
78
|
+
".pytype",
|
|
79
|
+
".ruff_cache",
|
|
80
|
+
".svn",
|
|
81
|
+
".tox",
|
|
82
|
+
".venv",
|
|
83
|
+
".vscode",
|
|
84
|
+
"__pypackages__",
|
|
85
|
+
"_build",
|
|
86
|
+
"buck-out",
|
|
87
|
+
"build",
|
|
88
|
+
"dist",
|
|
89
|
+
"node_modules",
|
|
90
|
+
"site-packages",
|
|
91
|
+
"venv",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# Same as Black.
|
|
95
|
+
line-length = 120
|
|
96
|
+
indent-width = 4
|
|
97
|
+
|
|
98
|
+
# Assume Python 3.12
|
|
99
|
+
target-version = "py312"
|
|
100
|
+
|
|
101
|
+
[tool.ruff.lint]
|
|
102
|
+
select = [
|
|
103
|
+
# pycodestyle error
|
|
104
|
+
"E",
|
|
105
|
+
# Pyflakes
|
|
106
|
+
"F",
|
|
107
|
+
# pycodestyle warnings
|
|
108
|
+
"W",
|
|
109
|
+
# pyupgrade
|
|
110
|
+
"UP",
|
|
111
|
+
# flake8-comprehensions
|
|
112
|
+
"C",
|
|
113
|
+
# flake8-bugbear
|
|
114
|
+
"B",
|
|
115
|
+
# flake8-simplify
|
|
116
|
+
"SIM",
|
|
117
|
+
# isort
|
|
118
|
+
"I",
|
|
119
|
+
]
|
|
120
|
+
ignore = [
|
|
121
|
+
# do not perform function calls in argument defaults
|
|
122
|
+
"B008",
|
|
123
|
+
# too complex
|
|
124
|
+
"C901"
|
|
125
|
+
]
|
|
126
|
+
fixable = ["ALL"]
|
|
127
|
+
unfixable = []
|
|
128
|
+
|
|
129
|
+
[tool.ruff.lint.per-file-ignores]
|
|
130
|
+
"__init__.py" = ["F401"]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
[tool.rye.scripts]
|
|
134
|
+
publish_testpypi = { cmd = "rye publish --repository testpypi --repository-url https://test.pypi.org/legacy/" }
|
|
135
|
+
publish_pypi = { cmd = "rye publish" }
|
|
136
|
+
sb = { cmd = "rye build --clean" }
|
|
137
|
+
spt = { chain = ["sb", "publish_testpypi"] }
|
|
138
|
+
sp = { chain = ["sb", "publish_pypi"] }
|
|
139
|
+
check_i = { cmd = "rye run pre-commit install" }
|
|
140
|
+
check = { cmd = "rye run pre-commit run --all-files" }
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
downloadkit==2.0.5
|
|
50
|
+
# via drissionpage
|
|
51
|
+
drissionpage==4.1.0.12
|
|
52
|
+
# via hssp
|
|
53
|
+
et-xmlfile==2.0.0
|
|
54
|
+
# via openpyxl
|
|
55
|
+
fake-useragent==1.5.1
|
|
56
|
+
# via hssp
|
|
57
|
+
filelock==3.16.1
|
|
58
|
+
# via tldextract
|
|
59
|
+
# via virtualenv
|
|
60
|
+
frozenlist==1.5.0
|
|
61
|
+
# via aiohttp
|
|
62
|
+
# via aiosignal
|
|
63
|
+
furl==2.1.3
|
|
64
|
+
# via hssp
|
|
65
|
+
h11==0.14.0
|
|
66
|
+
# via httpcore
|
|
67
|
+
h2==4.1.0
|
|
68
|
+
# via httpx
|
|
69
|
+
hpack==4.0.0
|
|
70
|
+
# via h2
|
|
71
|
+
hssp==0.4.7
|
|
72
|
+
# via hs-m3u8
|
|
73
|
+
httpcore==1.0.6
|
|
74
|
+
# via httpx
|
|
75
|
+
httpx==0.27.2
|
|
76
|
+
# via hssp
|
|
77
|
+
hyperframe==6.0.1
|
|
78
|
+
# via h2
|
|
79
|
+
identify==2.6.2
|
|
80
|
+
# via pre-commit
|
|
81
|
+
idna==3.10
|
|
82
|
+
# via anyio
|
|
83
|
+
# via httpx
|
|
84
|
+
# via requests
|
|
85
|
+
# via tldextract
|
|
86
|
+
# via yarl
|
|
87
|
+
jmespath==1.0.1
|
|
88
|
+
# via parsel
|
|
89
|
+
loguru==0.7.2
|
|
90
|
+
# via hssp
|
|
91
|
+
lxml==5.3.0
|
|
92
|
+
# via drissionpage
|
|
93
|
+
# via parsel
|
|
94
|
+
m3u8==6.0.0
|
|
95
|
+
# via hs-m3u8
|
|
96
|
+
multidict==6.1.0
|
|
97
|
+
# via aiohttp
|
|
98
|
+
# via yarl
|
|
99
|
+
nodeenv==1.9.1
|
|
100
|
+
# via pre-commit
|
|
101
|
+
openpyxl==3.1.5
|
|
102
|
+
# via datarecorder
|
|
103
|
+
orderedmultidict==1.0.1
|
|
104
|
+
# via furl
|
|
105
|
+
packaging==24.2
|
|
106
|
+
# via parsel
|
|
107
|
+
parsel==1.9.1
|
|
108
|
+
# via hssp
|
|
109
|
+
platformdirs==4.3.6
|
|
110
|
+
# via virtualenv
|
|
111
|
+
pre-commit==4.0.1
|
|
112
|
+
propcache==0.2.0
|
|
113
|
+
# via yarl
|
|
114
|
+
psutil==6.1.0
|
|
115
|
+
# via drissionpage
|
|
116
|
+
pycparser==2.22
|
|
117
|
+
# via cffi
|
|
118
|
+
pycryptodomex==3.21.0
|
|
119
|
+
# via hssp
|
|
120
|
+
pydantic==2.9.2
|
|
121
|
+
# via hssp
|
|
122
|
+
# via pydantic-settings
|
|
123
|
+
pydantic-core==2.23.4
|
|
124
|
+
# via pydantic
|
|
125
|
+
pydantic-settings==2.6.1
|
|
126
|
+
# via hssp
|
|
127
|
+
python-dotenv==1.0.1
|
|
128
|
+
# via pydantic-settings
|
|
129
|
+
pytz==2024.2
|
|
130
|
+
# via apscheduler
|
|
131
|
+
pyyaml==6.0.2
|
|
132
|
+
# via pre-commit
|
|
133
|
+
# via pydantic-settings
|
|
134
|
+
requests==2.32.3
|
|
135
|
+
# via downloadkit
|
|
136
|
+
# via drissionpage
|
|
137
|
+
# via hssp
|
|
138
|
+
# via requests-file
|
|
139
|
+
# via tldextract
|
|
140
|
+
requests-file==2.1.0
|
|
141
|
+
# via tldextract
|
|
142
|
+
six==1.16.0
|
|
143
|
+
# via apscheduler
|
|
144
|
+
# via furl
|
|
145
|
+
# via orderedmultidict
|
|
146
|
+
sniffio==1.3.1
|
|
147
|
+
# via anyio
|
|
148
|
+
# via httpx
|
|
149
|
+
tenacity==9.0.0
|
|
150
|
+
# via hssp
|
|
151
|
+
tldextract==5.1.3
|
|
152
|
+
# via drissionpage
|
|
153
|
+
tomli==2.1.0
|
|
154
|
+
# via pydantic-settings
|
|
155
|
+
typing-extensions==4.12.2
|
|
156
|
+
# via curl-cffi
|
|
157
|
+
# via pydantic
|
|
158
|
+
# via pydantic-core
|
|
159
|
+
tzlocal==5.2
|
|
160
|
+
# via apscheduler
|
|
161
|
+
urllib3==2.2.3
|
|
162
|
+
# via requests
|
|
163
|
+
uvloop==0.21.0
|
|
164
|
+
# via hssp
|
|
165
|
+
virtualenv==20.27.1
|
|
166
|
+
# via pre-commit
|
|
167
|
+
w3lib==2.2.1
|
|
168
|
+
# via parsel
|
|
169
|
+
websocket-client==1.8.0
|
|
170
|
+
# via drissionpage
|
|
171
|
+
yarl==1.17.1
|
|
172
|
+
# via aiohttp
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
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())
|
|
@@ -0,0 +1,33 @@
|
|
|
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())
|
|
@@ -0,0 +1,14 @@
|
|
|
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())
|
|
@@ -0,0 +1,14 @@
|
|
|
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())
|
|
@@ -0,0 +1,14 @@
|
|
|
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())
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
M3U8 下载器
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import platform
|
|
7
|
+
import posixpath
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from enum import Enum, auto
|
|
12
|
+
from hashlib import md5
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
from urllib.parse import urljoin, urlparse
|
|
16
|
+
from zipfile import ZipFile
|
|
17
|
+
|
|
18
|
+
import m3u8
|
|
19
|
+
from hssp import Net
|
|
20
|
+
from hssp.utils import crypto
|
|
21
|
+
from loguru import logger
|
|
22
|
+
|
|
23
|
+
|
|
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
|
+
res_path = Path(__file__).parent.parent.parent / "res"
|
|
34
|
+
ffmpeg_bin = res_path / "ffmpeg_win.exe"
|
|
35
|
+
|
|
36
|
+
if ffmpeg_bin.exists():
|
|
37
|
+
return str(ffmpeg_bin)
|
|
38
|
+
|
|
39
|
+
# ZIP 文件
|
|
40
|
+
ffmpeg_bin_zip = Path(ffmpeg_bin.parent) / f"{ffmpeg_bin.name}.zip"
|
|
41
|
+
if ffmpeg_bin_zip.exists():
|
|
42
|
+
# 解压缩到同一目录
|
|
43
|
+
with ZipFile(ffmpeg_bin_zip, "r") as zip_ref:
|
|
44
|
+
zip_ref.extractall(ffmpeg_bin.parent)
|
|
45
|
+
|
|
46
|
+
return ffmpeg_bin
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ContentType(Enum):
|
|
50
|
+
"""
|
|
51
|
+
获取URL数据的,类型枚举
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
Text = auto()
|
|
55
|
+
Json = auto()
|
|
56
|
+
Bytes = auto()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class M3u8Key:
|
|
60
|
+
"""
|
|
61
|
+
M3u8key
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, key: bytes, iv: str = None):
|
|
65
|
+
"""
|
|
66
|
+
:param key: 密钥
|
|
67
|
+
:param iv: 偏移
|
|
68
|
+
"""
|
|
69
|
+
self.key = key
|
|
70
|
+
self.iv = iv or key
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class M3u8Downloader:
|
|
74
|
+
"""
|
|
75
|
+
M3u8 异步下载器,并保留hls文件
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
retry_count: int = 0
|
|
79
|
+
retry_max_count: int = 50
|
|
80
|
+
ts_url_list: list = []
|
|
81
|
+
ts_path_list: list = []
|
|
82
|
+
ts_key: M3u8Key = None
|
|
83
|
+
m3u8_md5 = ""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
m3u8_url: str,
|
|
88
|
+
save_path: str,
|
|
89
|
+
decrypt=False,
|
|
90
|
+
max_workers=None,
|
|
91
|
+
headers=None,
|
|
92
|
+
get_m3u8_func: Callable = None,
|
|
93
|
+
):
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
m3u8_url: m3u8 地址
|
|
98
|
+
save_path: 保存路径
|
|
99
|
+
decrypt: 如果ts被加密,是否解密ts
|
|
100
|
+
max_workers: 最大并发数
|
|
101
|
+
headers: 情求头
|
|
102
|
+
get_m3u8_func: 处理m3u8情求的回调函数。适用于m3u8地址不是真正的地址,
|
|
103
|
+
而是包含m3u8内容的情求,会把m3u8_url的响应传递给get_m3u8_func,要求返回真正的m3u8内容
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
sem = asyncio.Semaphore(max_workers) if max_workers else None
|
|
107
|
+
self.headers = headers
|
|
108
|
+
self.net = Net(sem=sem)
|
|
109
|
+
self.decrypt = decrypt
|
|
110
|
+
self.m3u8_url = urlparse(m3u8_url)
|
|
111
|
+
self.get_m3u8_func = get_m3u8_func
|
|
112
|
+
self.save_dir = Path(save_path) / "hls"
|
|
113
|
+
self.save_name = Path(save_path).name
|
|
114
|
+
self.key_path = self.save_dir / "key.key"
|
|
115
|
+
|
|
116
|
+
if not self.save_dir.exists():
|
|
117
|
+
self.save_dir.mkdir(parents=True)
|
|
118
|
+
|
|
119
|
+
logger.add(self.save_dir.parent / f"{self.save_name}.log")
|
|
120
|
+
self.logger = logger
|
|
121
|
+
|
|
122
|
+
async def run(self, merge=True, del_hls=False):
|
|
123
|
+
await self.start(merge, del_hls)
|
|
124
|
+
await self.net.close()
|
|
125
|
+
|
|
126
|
+
async def start(self, merge=True, del_hls=False):
|
|
127
|
+
"""
|
|
128
|
+
下载器启动函数
|
|
129
|
+
:param merge: ts下载完后是否合并,默认合并
|
|
130
|
+
:param del_hls: 是否删除hls系列文件,包括.m3u8文件、*.ts、.key文件
|
|
131
|
+
:return:
|
|
132
|
+
"""
|
|
133
|
+
mp4_path = self.save_dir.parent / f"{self.save_name}.mp4"
|
|
134
|
+
if Path(mp4_path).exists():
|
|
135
|
+
self.logger.info(f"{mp4_path}已存在")
|
|
136
|
+
if del_hls:
|
|
137
|
+
shutil.rmtree(str(self.save_dir))
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
self.logger.info(
|
|
141
|
+
f"开始下载: 合并ts为mp4={merge}, "
|
|
142
|
+
f"删除hls信息={del_hls}, "
|
|
143
|
+
f"下载地址为:{self.m3u8_url.geturl()}. 保存路径为:{self.save_dir}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
await self._download()
|
|
147
|
+
self.logger.info("ts下载完成")
|
|
148
|
+
self.ts_path_list = [ts_path for ts_path in self.ts_path_list if ts_path]
|
|
149
|
+
count_1, count_2 = len(self.ts_url_list), len(self.ts_path_list)
|
|
150
|
+
self.logger.info(f"TS应下载数量为:{count_1}, 实际下载数量为:{count_2}")
|
|
151
|
+
if count_1 == 0 or count_2 == 0:
|
|
152
|
+
self.logger.error("ts数量为0,请检查!!!")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
if count_2 != count_1:
|
|
156
|
+
self.logger.error(f"ts下载数量与实际数量不符合!!!应该下载数量为:{count_1}, 实际下载数量为:{count_2}")
|
|
157
|
+
self.logger.error(self.ts_url_list)
|
|
158
|
+
self.logger.error(self.ts_path_list)
|
|
159
|
+
if self.retry_count < self.retry_max_count:
|
|
160
|
+
self.retry_count += 1
|
|
161
|
+
self.logger.error(f"正在进行重试:{self.retry_count}/{self.retry_max_count}")
|
|
162
|
+
return self.start(merge, del_hls)
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
if not merge:
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
if self.merge():
|
|
169
|
+
self.logger.info("合并成功")
|
|
170
|
+
else:
|
|
171
|
+
self.logger.error(
|
|
172
|
+
f"mp4合并失败. ts应该下载数量为:{count_1}, 实际下载数量为:{count_2}. 保存路径为:{self.save_dir}"
|
|
173
|
+
)
|
|
174
|
+
return False
|
|
175
|
+
if del_hls:
|
|
176
|
+
shutil.rmtree(str(self.save_dir))
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
async def _download(self):
|
|
180
|
+
"""
|
|
181
|
+
下载ts文件、m3u8文件、key文件
|
|
182
|
+
:return:
|
|
183
|
+
"""
|
|
184
|
+
self.ts_url_list = await self.get_ts_list(self.m3u8_url)
|
|
185
|
+
self.ts_path_list = [None] * len(self.ts_url_list)
|
|
186
|
+
await asyncio.gather(*[self._download_ts(url) for url in self.ts_url_list])
|
|
187
|
+
|
|
188
|
+
async def get_url_content(self, url: str, content_type: ContentType) -> bytes | str | Any:
|
|
189
|
+
"""
|
|
190
|
+
按照类型获取url内容
|
|
191
|
+
:param url: 请求地址
|
|
192
|
+
:param content_type: 内容类型
|
|
193
|
+
:return:
|
|
194
|
+
"""
|
|
195
|
+
data = None
|
|
196
|
+
try:
|
|
197
|
+
resp = await self.net.get(url, headers=self.headers)
|
|
198
|
+
if content_type == ContentType.Bytes:
|
|
199
|
+
data = resp.content
|
|
200
|
+
if content_type == ContentType.Text:
|
|
201
|
+
data = resp.text
|
|
202
|
+
if content_type == ContentType.Json:
|
|
203
|
+
data = resp.json
|
|
204
|
+
if resp.status_code != 200:
|
|
205
|
+
self.logger.error(f"请求{url}内容时返回码不正确,类型为:{content_type}, 返回码为:{resp.status_code}")
|
|
206
|
+
return None
|
|
207
|
+
except BaseException as exception:
|
|
208
|
+
self.logger.error(f"请求{url}内容时发生异常,类型为:{content_type}, 异常信息为:{exception}")
|
|
209
|
+
|
|
210
|
+
return data
|
|
211
|
+
|
|
212
|
+
async def get_ts_list(self, url) -> list[dict]:
|
|
213
|
+
"""
|
|
214
|
+
解析m3u8并保存至列表
|
|
215
|
+
:param url:
|
|
216
|
+
:return:
|
|
217
|
+
"""
|
|
218
|
+
resp = await self.net.get(url.geturl(), headers=self.headers)
|
|
219
|
+
m3u8_text = self.get_m3u8_func(resp.text) if self.get_m3u8_func else resp.text
|
|
220
|
+
m3u8_obj = m3u8.loads(m3u8_text)
|
|
221
|
+
prefix = f"{url.scheme}://{url.netloc}"
|
|
222
|
+
base_path = posixpath.normpath(url.path + "/..") + "/"
|
|
223
|
+
m3u8_obj.base_uri = urljoin(prefix, base_path)
|
|
224
|
+
|
|
225
|
+
# 解析多层m3u8, 默认选取比特率最高的
|
|
226
|
+
ts_url_list = []
|
|
227
|
+
if len(m3u8_obj.playlists) > 0:
|
|
228
|
+
bandwidth = 0
|
|
229
|
+
play_url = ""
|
|
230
|
+
self.logger.info("发现多个播放列表")
|
|
231
|
+
for playlist in m3u8_obj.playlists:
|
|
232
|
+
if int(playlist.stream_info.bandwidth) > bandwidth:
|
|
233
|
+
bandwidth = int(playlist.stream_info.bandwidth)
|
|
234
|
+
play_url = playlist.absolute_uri
|
|
235
|
+
self.logger.info(f"选择的播放地址:{play_url},比特率:{bandwidth}")
|
|
236
|
+
return await self.get_ts_list(urlparse(play_url))
|
|
237
|
+
|
|
238
|
+
# 遍历ts文件
|
|
239
|
+
for index, segments in enumerate(m3u8_obj.segments):
|
|
240
|
+
ts_uri = segments.uri if "http" in m3u8_obj.segments[index].uri else segments.absolute_uri
|
|
241
|
+
m3u8_obj.segments[index].uri = f"{index}.ts"
|
|
242
|
+
ts_url_list.append({"uri": ts_uri, "index": index})
|
|
243
|
+
|
|
244
|
+
# 保存解密key
|
|
245
|
+
if len(m3u8_obj.keys) > 0 and m3u8_obj.keys[0]:
|
|
246
|
+
resp = await self.net.get(m3u8_obj.keys[0].absolute_uri, headers=self.headers)
|
|
247
|
+
key_data = resp.content
|
|
248
|
+
self.save_file(key_data, self.key_path)
|
|
249
|
+
self.ts_key = M3u8Key(key=key_data, iv=m3u8_obj.keys[0].iv)
|
|
250
|
+
key = m3u8_obj.segments[0].key
|
|
251
|
+
key.uri = "key.key"
|
|
252
|
+
m3u8_obj.segments[0].key = key
|
|
253
|
+
|
|
254
|
+
# 导出m3u8文件
|
|
255
|
+
m3u8_text = m3u8_obj.dumps()
|
|
256
|
+
self.m3u8_md5 = md5(m3u8_text.encode("utf8"), usedforsecurity=False).hexdigest().lower()
|
|
257
|
+
self.save_file(m3u8_text, self.save_dir / f"{self.m3u8_md5}.m3u8")
|
|
258
|
+
self.logger.info("导出m3u8文件成功")
|
|
259
|
+
|
|
260
|
+
return ts_url_list
|
|
261
|
+
|
|
262
|
+
async def _download_ts(self, ts_item: dict):
|
|
263
|
+
"""
|
|
264
|
+
下载ts
|
|
265
|
+
:param ts_item: ts 数据
|
|
266
|
+
:return:
|
|
267
|
+
"""
|
|
268
|
+
index = ts_item["index"]
|
|
269
|
+
ts_uri = ts_item["uri"]
|
|
270
|
+
ts_path = self.save_dir / f"{index}.ts"
|
|
271
|
+
if Path(ts_path).exists():
|
|
272
|
+
self.ts_path_list[index] = str(ts_path)
|
|
273
|
+
return
|
|
274
|
+
resp = await self.net.get(ts_item["uri"])
|
|
275
|
+
ts_content = resp.content
|
|
276
|
+
if ts_content is None:
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
if self.ts_key and self.decrypt:
|
|
280
|
+
ts_content = crypto.decrypt_aes_256_cbc_pad7(ts_content, self.ts_key.key, self.ts_key.iv)
|
|
281
|
+
|
|
282
|
+
self.save_file(ts_content, ts_path)
|
|
283
|
+
self.logger.info(f"{ts_uri}下载成功")
|
|
284
|
+
self.ts_path_list[index] = str(ts_path)
|
|
285
|
+
|
|
286
|
+
def merge(self):
|
|
287
|
+
"""
|
|
288
|
+
合并ts文件为mp4文件
|
|
289
|
+
:return:
|
|
290
|
+
"""
|
|
291
|
+
self.logger.info("开始合并mp4")
|
|
292
|
+
if len(self.ts_path_list) != len(self.ts_url_list):
|
|
293
|
+
self.logger.error("数量不足拒绝合并!")
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
# 整合后的ts文件路径
|
|
297
|
+
big_ts_path = self.save_dir.parent / f"{self.save_name}.ts"
|
|
298
|
+
if big_ts_path.exists():
|
|
299
|
+
big_ts_path.unlink()
|
|
300
|
+
|
|
301
|
+
# mp4路径
|
|
302
|
+
mp4_path = self.save_dir.parent / f"{self.save_name}.mp4"
|
|
303
|
+
|
|
304
|
+
# 把ts文件整合到一起
|
|
305
|
+
big_ts_file = big_ts_path.open("ab+")
|
|
306
|
+
for path in self.ts_path_list:
|
|
307
|
+
with open(path, "rb") as ts_file:
|
|
308
|
+
data = ts_file.read()
|
|
309
|
+
if self.ts_key:
|
|
310
|
+
data = crypto.decrypt_aes_256_cbc_pad7(data, self.ts_key.key, self.ts_key.iv)
|
|
311
|
+
big_ts_file.write(data)
|
|
312
|
+
big_ts_file.close()
|
|
313
|
+
self.logger.info("ts文件整合完毕")
|
|
314
|
+
|
|
315
|
+
# 把大的ts文件转换成mp4文件
|
|
316
|
+
ffmpeg_bin = get_ffmpeg()
|
|
317
|
+
command = (
|
|
318
|
+
f'{ffmpeg_bin} -i "{big_ts_path}" '
|
|
319
|
+
f'-c copy -map 0:v -map 0:a -bsf:a aac_adtstoasc -threads 32 "{mp4_path}" -y'
|
|
320
|
+
)
|
|
321
|
+
self.logger.info(f"ts整合成功,开始转为mp4。 command:{command}")
|
|
322
|
+
result = subprocess.run(command, shell=True, capture_output=True, text=True)
|
|
323
|
+
if result.returncode != 0:
|
|
324
|
+
logger.error(f"命令执行失败: {result.stderr or result.stdout}")
|
|
325
|
+
|
|
326
|
+
if Path(mp4_path).exists():
|
|
327
|
+
big_ts_path.unlink()
|
|
328
|
+
return Path(mp4_path).exists()
|
|
329
|
+
|
|
330
|
+
@staticmethod
|
|
331
|
+
def save_file(content: bytes | str, filepath):
|
|
332
|
+
"""
|
|
333
|
+
保存内容到文件
|
|
334
|
+
:param content: 内容
|
|
335
|
+
:param filepath: 文件路径
|
|
336
|
+
:return:
|
|
337
|
+
"""
|
|
338
|
+
mode = "wb" if isinstance(content, bytes) else "w"
|
|
339
|
+
with open(file=filepath, mode=mode) as file:
|
|
340
|
+
file.write(content)
|