mirror of
https://github.com/ihmily/DouyinLiveRecorder.git
synced 2025-12-26 05:48:32 +08:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
add187f8d8 | ||
|
|
0333cb4a01 | ||
|
|
73857755a7 | ||
|
|
fec734ae74 | ||
|
|
853d03ea14 | ||
|
|
2fb7f7afd7 | ||
|
|
200e5b5b58 | ||
|
|
abb204e6e9 | ||
|
|
271a53621d | ||
|
|
d77760f3c9 | ||
|
|
9c913e23cc | ||
|
|
af37bf28f0 | ||
|
|
d4796409c7 | ||
|
|
93a12ab41d | ||
|
|
525b720627 | ||
|
|
e9f2a55ceb | ||
|
|
3965487746 | ||
|
|
e80f1e653a | ||
|
|
63787f1743 | ||
|
|
a328c6a1c5 | ||
|
|
be2c3a393f | ||
|
|
c7e3cf47ce | ||
|
|
199186fb09 | ||
|
|
5778ebc4b3 | ||
|
|
d9f985303a | ||
|
|
110d5bded4 | ||
|
|
e478d72e62 | ||
|
|
bcfc268c1c | ||
|
|
8e4e9b098f | ||
|
|
9f499f3fa6 | ||
|
|
ae8200e01c | ||
|
|
952eeb9b7c | ||
|
|
ba8979e4ee | ||
|
|
c157c08e5a | ||
|
|
effcfcc76f | ||
|
|
d787838ed2 | ||
|
|
86a822f0db | ||
|
|
51f79d70f8 | ||
|
|
d37d9f25ad | ||
|
|
c40a10235d | ||
|
|
d860023808 | ||
|
|
57739410f8 | ||
|
|
9bc629c1b0 | ||
|
|
2dd9c42767 | ||
|
|
fd06bc89da | ||
|
|
019b30203e | ||
|
|
0bd2a3a360 | ||
|
|
4fa3fbb773 | ||
|
|
ba046660fc | ||
|
|
f73ce7b68e | ||
|
|
9e494f4377 | ||
|
|
151a6a45c9 | ||
|
|
807f4b758e | ||
|
|
bf7381bf6c | ||
|
|
d055602e81 | ||
|
|
ef97e01dba | ||
|
|
e189640d3a | ||
|
|
7bb778a875 | ||
|
|
99ea143c78 | ||
|
|
52c0287150 | ||
|
|
246632de78 | ||
|
|
71fbf93ffe | ||
|
|
a61bdc852e | ||
|
|
a5d567092d | ||
|
|
272e2dd28e | ||
|
|
c9b2310fa4 | ||
|
|
46842c3b48 | ||
|
|
736943cbca | ||
|
|
5d3c295c7a | ||
|
|
8c30787353 | ||
|
|
73d9ee1334 | ||
|
|
2fa830307f | ||
|
|
bbb0c5ebaa | ||
|
|
fa92a4196f | ||
|
|
29a16ba00b | ||
|
|
d5de8bd77b | ||
|
|
0751107ae1 | ||
|
|
c637e5617c | ||
|
|
a00305cafb | ||
|
|
c9f26b116d | ||
|
|
4181e9745a | ||
|
|
1a8f2fe9bd | ||
|
|
0c97d0612f | ||
|
|
80f88f6420 | ||
|
|
9835374174 | ||
|
|
2ca2fdf627 | ||
|
|
35bfca0a7a | ||
|
|
4b692c0f77 | ||
|
|
7a07a5282d | ||
|
|
2729fcec46 | ||
|
|
eeeed2fc5f | ||
|
|
b963d8ac6f | ||
|
|
7cbd995591 | ||
|
|
b3449093bb | ||
|
|
14552afcdf | ||
|
|
645f2ea782 | ||
|
|
38b423504d | ||
|
|
3db010315f | ||
|
|
ad0b6c72dd | ||
|
|
463fa44756 | ||
|
|
6657374db3 | ||
|
|
9d292cf865 | ||
|
|
310390b3c2 | ||
|
|
d2d34ceae7 | ||
|
|
4de6bae1fa | ||
|
|
0bf1550884 | ||
|
|
461489290a | ||
|
|
92f5fbf4b8 | ||
|
|
b3f110e3ec | ||
|
|
2e6e699fdb | ||
|
|
a95e0a8849 | ||
|
|
240f0e2400 | ||
|
|
37f7ad0048 | ||
|
|
3e2772a3d7 | ||
|
|
826a3a37e7 | ||
|
|
ce6285c1b9 | ||
|
|
4946b49e95 | ||
|
|
f03f39ca85 | ||
|
|
14e4c530a2 | ||
|
|
19fb6a2b20 | ||
|
|
f56559b93b | ||
|
|
48c261ac5f | ||
|
|
13bb9efcf9 | ||
|
|
56add9e5b8 | ||
|
|
f0007db68e | ||
|
|
da09583de8 | ||
|
|
0b7d2508ba | ||
|
|
c6a2500096 | ||
|
|
4959379c30 | ||
|
|
2066d725d6 | ||
|
|
249c61c6c3 | ||
|
|
6d087aa648 | ||
|
|
e1cce5c0f1 | ||
|
|
a9b95cc491 | ||
|
|
34e038784f | ||
|
|
76e56f3673 | ||
|
|
6508c0549c | ||
|
|
b886f68646 | ||
|
|
23e4eb23e4 | ||
|
|
d3143108c1 | ||
|
|
cb1316b4aa | ||
|
|
d2ad9b8884 | ||
|
|
9e5ad30841 | ||
|
|
5a89ff3e6d | ||
|
|
c2e79e125d | ||
|
|
5ffce76060 | ||
|
|
c75b5e7111 | ||
|
|
f11787d515 | ||
|
|
05da54cebe | ||
|
|
bc3c425d13 | ||
|
|
f387b82360 | ||
|
|
560f294819 | ||
|
|
634bd9f41f | ||
|
|
9a8dfc6a45 | ||
|
|
44af6912d4 | ||
|
|
f0700e8655 |
28
.github/ISSUE_TEMPLATE/bug.yml
vendored
28
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -1,12 +1,13 @@
|
|||||||
name: 🐛 Bug report
|
name: 🐛 Bug report
|
||||||
description: 项目运行中遇到的Bug或问题。
|
description: 创建Bug报告以帮助项目改进。
|
||||||
labels: ["status: issue-bug"]
|
title: 🐛[BUG] 请输入标题
|
||||||
|
labels: bug
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
📝 **请在上方的`title`中填写一个简洁明了的标题**,格式建议为:[Bug] 简短描述。
|
📝 **请在上方的`title`中填写一个简洁明了的标题**,格式建议为:🐛[Bug] 简短描述。
|
||||||
例如:[Bug] B站某些直播间无法录制。
|
例如:🐛[Bug] B站某些直播间无法录制。
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: ⚠️ 确认是否已存在类似问题
|
label: ⚠️ 确认是否已存在类似问题
|
||||||
@ -23,6 +24,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- 直接运行的exe文件
|
- 直接运行的exe文件
|
||||||
- 使用源代码运行
|
- 使用源代码运行
|
||||||
|
- 使用docker运行
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@ -30,14 +32,28 @@ body:
|
|||||||
label: 🐍 如果是使用源代码运行,请选择你的Python环境版本
|
label: 🐍 如果是使用源代码运行,请选择你的Python环境版本
|
||||||
description: 请选择你运行程序的Python版本。
|
description: 请选择你运行程序的Python版本。
|
||||||
options:
|
options:
|
||||||
- Python 3.8
|
|
||||||
- Python 3.9
|
|
||||||
- Python 3.10
|
- Python 3.10
|
||||||
- Python 3.11
|
- Python 3.11
|
||||||
- Python 3.12
|
- Python 3.12
|
||||||
|
- Python 3.13
|
||||||
- Other (请在问题中说明)
|
- Other (请在问题中说明)
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 💻 请选择你的系统环境
|
||||||
|
description: 请选择你运行程序的具体系统版本。
|
||||||
|
options:
|
||||||
|
- Windows 10
|
||||||
|
- Windows 11
|
||||||
|
- macOS
|
||||||
|
- Ubuntu
|
||||||
|
- CentOS
|
||||||
|
- Fedora
|
||||||
|
- Debian
|
||||||
|
- Other (请在问题中说明)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: ⚠️ 确认是否已经重试多次
|
label: ⚠️ 确认是否已经重试多次
|
||||||
|
|||||||
91
.github/ISSUE_TEMPLATE/bug_en.yml
vendored
Normal file
91
.github/ISSUE_TEMPLATE/bug_en.yml
vendored
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
name: 🐛 (English)Bug report
|
||||||
|
description: Create a bug report to help improve the project.
|
||||||
|
title: 🐛[BUG] Please enter a title
|
||||||
|
labels: bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
📝 **Please fill in a concise and clear title in the `title` above**, the format is suggested as: 🐛[Bug] Short description.
|
||||||
|
For example: 🐛[Bug] Unable to record certain TikTok live rooms.
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: ⚠️ Confirm if similar issues exist
|
||||||
|
description: >
|
||||||
|
🔍 [Click here to search historical issues](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue)
|
||||||
|
Please make sure your issue hasn't been reported before.
|
||||||
|
options:
|
||||||
|
- label: I have searched the issues and found no similar problems
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 🔧 How did you run the program?
|
||||||
|
description: Please select how you ran the program.
|
||||||
|
options:
|
||||||
|
- Directly running the exe file
|
||||||
|
- Running with source code
|
||||||
|
- Running with docker
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 🐍 If running with source code, please select your Python environment version
|
||||||
|
description: Please select the Python version you used to run the program.
|
||||||
|
options:
|
||||||
|
- Python 3.10
|
||||||
|
- Python 3.11
|
||||||
|
- Python 3.12
|
||||||
|
- Python 3.13
|
||||||
|
- Other (please specify in the issue)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 💻 Please select your system environment
|
||||||
|
description: Please select the specific system version you are running the program on.
|
||||||
|
options:
|
||||||
|
- Windows 10
|
||||||
|
- Windows 11
|
||||||
|
- macOS
|
||||||
|
- Ubuntu
|
||||||
|
- CentOS
|
||||||
|
- Fedora
|
||||||
|
- Debian
|
||||||
|
- Other (please specify in the issue)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: ⚠️ Confirm if you have retried multiple times
|
||||||
|
description: >
|
||||||
|
Sometimes it might be due to your device or network issues.
|
||||||
|
options:
|
||||||
|
- label: I have tried multiple times and still encounter the problem
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 🕹 Reproduction steps
|
||||||
|
description: |
|
||||||
|
**⚠️ Issues that cannot be reproduced will be closed.**
|
||||||
|
Please fill in according to the following format:
|
||||||
|
1. The live room address I tried to record is...
|
||||||
|
2. The recording format I used is...
|
||||||
|
3. ...
|
||||||
|
placeholder: |
|
||||||
|
1. ...
|
||||||
|
2. ...
|
||||||
|
3. ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 😯 Problem description
|
||||||
|
description: Describe the problem in detail or provide relevant screenshots.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 📜 Error information
|
||||||
|
description: If available, please paste the relevant log error information or screenshots.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
16
.github/ISSUE_TEMPLATE/feature.yml
vendored
16
.github/ISSUE_TEMPLATE/feature.yml
vendored
@ -1,17 +1,18 @@
|
|||||||
name: 🚀 Feature request
|
name: 🚀 Feature request
|
||||||
description: 提出你对项目的新想法或建议。
|
description: 提出你对项目的新想法或建议。
|
||||||
labels: ["status: issue-feature"]
|
title: 🚀[Feature] 请输入标题
|
||||||
|
labels: enhancement
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
📝 **请在上方的`title`中填写一个简洁明了的标题**,格式建议为:[Feature] 简短描述。
|
📝 **请在上方的`title`中填写一个简洁明了的标题**,格式建议为:🚀[Feature] 简短描述。
|
||||||
例如:[Feature] 添加xx直播录制。
|
例如:🚀[Feature] 添加xx直播录制。
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: ⚠️ 搜索是否存在类似issue
|
label: ⚠️ 搜索是否存在类似issue
|
||||||
description: >
|
description: >
|
||||||
🔍 请在 [历史issue](https://github.com/ihmily/DouyinLiveRecorder/issues) 中清空输入框,使用关键词搜索,确保没有重复的issue。
|
🔍 [点击这里搜索历史issue](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) 使用关键词搜索,确保没有重复的issue。
|
||||||
options:
|
options:
|
||||||
- label: 我已经搜索过issues,没有发现相似issue
|
- label: 我已经搜索过issues,没有发现相似issue
|
||||||
required: true
|
required: true
|
||||||
@ -34,9 +35,4 @@ body:
|
|||||||
label: 💡 动机
|
label: 💡 动机
|
||||||
description: 描述你提出该feature的动机,以及没有这项feature对你的使用造成了怎样的影响。
|
description: 描述你提出该feature的动机,以及没有这项feature对你的使用造成了怎样的影响。
|
||||||
placeholder: |
|
placeholder: |
|
||||||
我需要这个功能是因为...
|
我需要这个功能是因为...
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 📚 附加信息
|
|
||||||
description: 如果有其他相关信息,如屏幕截图、错误日志或相关文档,请在这里提供。
|
|
||||||
37
.github/ISSUE_TEMPLATE/feature_en.yml
vendored
Normal file
37
.github/ISSUE_TEMPLATE/feature_en.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: 🚀 (English)Feature request
|
||||||
|
description: Propose new ideas or suggestions for the project.
|
||||||
|
title: 🚀[Feature] Please enter a title
|
||||||
|
labels: enhancement
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
📝 **Please fill in a concise and clear title in the `title` above**, the format is suggested as: 🚀[Feature] Short description.
|
||||||
|
For example: 🚀[Feature] Add xx live recording.
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: ⚠️ Search for similar issues
|
||||||
|
description: >
|
||||||
|
🔍 [Click here to search historical issues](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) using keywords to ensure there are no duplicate issues.
|
||||||
|
options:
|
||||||
|
- label: I have searched the issues and found no similar issues
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 📜 Feature description
|
||||||
|
description: Please describe in detail the feature you would like to add, including how it should work and what its expected outcomes are.
|
||||||
|
placeholder: |
|
||||||
|
Feature description:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 🌐 Example (Optional)
|
||||||
|
description: If possible, provide examples, screenshots, or related URLs related to the feature.
|
||||||
|
placeholder: |
|
||||||
|
Live room example URL:
|
||||||
|
`https://www.example.com/live/xxxx`
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 💡 Motivation
|
||||||
|
description: Describe the motivation behind your feature request and how not having this feature impacts your use of the project.
|
||||||
|
placeholder: |
|
||||||
|
I need this feature because...
|
||||||
63
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
63
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
name: ❓ Question
|
||||||
|
description: 对程序使用有疑问?在这里提出你的问题。
|
||||||
|
title: ❓[Question] 请输入标题
|
||||||
|
labels: question
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
📝 **请在上方的`title`中填写一个简洁明了的问题标题**。这将帮助其他人快速理解你的问题。
|
||||||
|
例如:❓[Question] 如果设置单个直播间的录制清晰度。
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: ⚠️ 搜索是否存在类似问题
|
||||||
|
description: >
|
||||||
|
🔍 [点击这里搜索历史issue](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) 使用关键词搜索,看看是否已经有人问过类似的问题。
|
||||||
|
options:
|
||||||
|
- label: 我已经搜索过issues,没有找到相似的问题
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 🔧 运行方式
|
||||||
|
description: 请选择你是如何运行程序的。
|
||||||
|
options:
|
||||||
|
- 直接运行的exe文件
|
||||||
|
- 使用源代码运行
|
||||||
|
- 使用docker运行
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 🐍 如果是使用源代码运行,请选择你的Python环境版本
|
||||||
|
description: 请选择你运行程序的Python版本。
|
||||||
|
options:
|
||||||
|
- Python 3.10
|
||||||
|
- Python 3.11
|
||||||
|
- Python 3.12
|
||||||
|
- Python 3.13
|
||||||
|
- Other (请在问题中说明)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 💻 请选择你的系统环境
|
||||||
|
description: 请选择你运行程序的具体系统版本。
|
||||||
|
options:
|
||||||
|
- Windows 10
|
||||||
|
- Windows 11
|
||||||
|
- macOS
|
||||||
|
- Ubuntu
|
||||||
|
- CentOS
|
||||||
|
- Fedora
|
||||||
|
- Debian
|
||||||
|
- Other (请在问题中说明)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 🤔 问题详情
|
||||||
|
description: 请提供与你的问题相关的所有详细信息。
|
||||||
|
placeholder: |
|
||||||
|
你的问题具体是关于什么?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
63
.github/ISSUE_TEMPLATE/question_en.yml
vendored
Normal file
63
.github/ISSUE_TEMPLATE/question_en.yml
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
name: ❓ (English)Question
|
||||||
|
description: Have questions about using the program? Ask them here.
|
||||||
|
title: ❓[Question] Please enter a title
|
||||||
|
labels: question
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
📝 **Please fill in a concise and clear question title in the `title` above**. This will help others quickly understand your question.
|
||||||
|
For example: ❓[Question] How to set the recording quality for a single live room.
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: ⚠️ Search for similar issues
|
||||||
|
description: >
|
||||||
|
🔍 [Click here to search historical issues](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) see if your question has already been asked.
|
||||||
|
options:
|
||||||
|
- label: I have searched the issues and found no similar questions
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 🔧 How did you run the program?
|
||||||
|
description: Please select how you ran the program.
|
||||||
|
options:
|
||||||
|
- Executable file run directly
|
||||||
|
- Running with source code
|
||||||
|
- Running with docker
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 🐍 If running with source code, please select your Python environment version
|
||||||
|
description: Please select the Python version you used to run the program.
|
||||||
|
options:
|
||||||
|
- Python 3.10
|
||||||
|
- Python 3.11
|
||||||
|
- Python 3.12
|
||||||
|
- Python 3.13
|
||||||
|
- Other (please specify in the question)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 💻 Please select your system environment
|
||||||
|
description: Please select the specific system version you are running the program on.
|
||||||
|
options:
|
||||||
|
- Windows 10
|
||||||
|
- Windows 11
|
||||||
|
- macOS
|
||||||
|
- Ubuntu
|
||||||
|
- CentOS
|
||||||
|
- Fedora
|
||||||
|
- Debian
|
||||||
|
- Other (please specify in the question)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 🤔 Question details
|
||||||
|
description: Please provide all the details relevant to your question.
|
||||||
|
placeholder: |
|
||||||
|
What is your question about?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
68
.github/workflows/build-image.yml
vendored
68
.github/workflows/build-image.yml
vendored
@ -4,45 +4,51 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: 'Tag name for the Docker image'
|
||||||
|
required: false
|
||||||
|
default: 'latest'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_push:
|
build_and_push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Cache Docker layers
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-buildx-
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
ihmily/douyin-live-recorder:${{ github.ref_name }}
|
ihmily/douyin-live-recorder:${{ github.event.inputs.tag_name || github.ref_name }}
|
||||||
ihmily/douyin-live-recorder:latest
|
ihmily/douyin-live-recorder:latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -52,7 +52,6 @@ coverage.xml
|
|||||||
cover/
|
cover/
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
|
||||||
*.pot
|
*.pot
|
||||||
|
|
||||||
# Django stuff:
|
# Django stuff:
|
||||||
@ -82,10 +81,16 @@ target/
|
|||||||
profile_default/
|
profile_default/
|
||||||
ipython_config.py
|
ipython_config.py
|
||||||
|
|
||||||
|
# DouyinLiveRecord
|
||||||
|
backup_config/
|
||||||
|
logs/
|
||||||
|
node/
|
||||||
|
node-v*.zip
|
||||||
|
|
||||||
# pyenv
|
# pyenv
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
# .python-version
|
.python-version
|
||||||
|
|
||||||
# pipenv
|
# pipenv
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 Hmily
|
Copyright (c) 2025 Hmily
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
326
README.md
326
README.md
@ -24,14 +24,14 @@
|
|||||||
- [x] 小红书
|
- [x] 小红书
|
||||||
- [x] bigo
|
- [x] bigo
|
||||||
- [x] blued
|
- [x] blued
|
||||||
- [x] AfreecaTV
|
- [x] SOOP(原AfreecaTV)
|
||||||
- [x] 网易cc
|
- [x] 网易cc
|
||||||
- [x] 千度热播
|
- [x] 千度热播
|
||||||
- [x] PandaTV
|
- [x] PandaTV
|
||||||
- [x] 猫耳FM
|
- [x] 猫耳FM
|
||||||
- [x] Look直播
|
- [x] Look直播
|
||||||
- [x] WinkTV
|
- [x] WinkTV
|
||||||
- [x] FlexTV
|
- [x] TTingLive(原Flextv)
|
||||||
- [x] PopkonTV
|
- [x] PopkonTV
|
||||||
- [x] TwitCasting
|
- [x] TwitCasting
|
||||||
- [x] 百度直播
|
- [x] 百度直播
|
||||||
@ -43,11 +43,28 @@
|
|||||||
- [x] 流星直播
|
- [x] 流星直播
|
||||||
- [x] ShowRoom
|
- [x] ShowRoom
|
||||||
- [x] Acfun
|
- [x] Acfun
|
||||||
- [x] 时光直播
|
|
||||||
- [x] 映客直播
|
- [x] 映客直播
|
||||||
- [x] 音播直播
|
- [x] 音播直播
|
||||||
- [x] 知乎直播
|
- [x] 知乎直播
|
||||||
- [x] CHZZK
|
- [x] CHZZK
|
||||||
|
- [x] 嗨秀直播
|
||||||
|
- [x] vv星球直播
|
||||||
|
- [x] 17Live
|
||||||
|
- [x] 浪Live
|
||||||
|
- [x] 畅聊直播
|
||||||
|
- [x] 飘飘直播
|
||||||
|
- [x] 六间房直播
|
||||||
|
- [x] 乐嗨直播
|
||||||
|
- [x] 花猫直播
|
||||||
|
- [x] Shopee
|
||||||
|
- [x] Youtube
|
||||||
|
- [x] 淘宝
|
||||||
|
- [x] 京东
|
||||||
|
- [x] Faceit
|
||||||
|
- [x] 咪咕
|
||||||
|
- [x] 连接直播
|
||||||
|
- [x] 来秀直播
|
||||||
|
- [x] Picarto
|
||||||
- [ ] 更多平台正在更新中
|
- [ ] 更多平台正在更新中
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -60,22 +77,25 @@
|
|||||||
├── /config -> (config record)
|
├── /config -> (config record)
|
||||||
├── /logs -> (save runing log file)
|
├── /logs -> (save runing log file)
|
||||||
├── /backup_config -> (backup file)
|
├── /backup_config -> (backup file)
|
||||||
├── /libs -> (dll file)
|
|
||||||
├── /douyinliverecorder -> (package)
|
├── /douyinliverecorder -> (package)
|
||||||
|
├── initializer.py-> (check and install nodejs)
|
||||||
├── spider.py-> (get live data)
|
├── spider.py-> (get live data)
|
||||||
├── stream.py-> (get live stream address)
|
├── stream.py-> (get live stream address)
|
||||||
├── utils.py -> (contains utility functions)
|
├── utils.py -> (contains utility functions)
|
||||||
├── logger.py -> (logger handdle)
|
├── logger.py -> (logger handdle)
|
||||||
├── web_rid.py -> (get web_rid)
|
├── room.py -> (get room info)
|
||||||
├── msg_push.py -> (send live status update message)
|
├── ab_sign.py-> (generate dy token)
|
||||||
├── x-bogus.js -> (get douyin xbogus token)
|
├── /javascript -> (some decrypt code)
|
||||||
├── main.py -> (main file)
|
├── main.py -> (main file)
|
||||||
|
├── ffmpeg_install.py -> (ffmpeg install script)
|
||||||
├── demo.py -> (call package test demo)
|
├── demo.py -> (call package test demo)
|
||||||
|
├── msg_push.py -> (send live status update message)
|
||||||
├── ffmpeg.exe -> (record video)
|
├── ffmpeg.exe -> (record video)
|
||||||
├── index.html -> (play m3u8 and flv video)
|
├── index.html -> (play m3u8 and flv video)
|
||||||
├── requirements.txt -> (library dependencies)
|
├── requirements.txt -> (library dependencies)
|
||||||
├── docker-compose.yaml -> (Container Orchestration File)
|
├── docker-compose.yaml -> (Container Orchestration File)
|
||||||
├── Dockerfile -> (Application Build Recipe)
|
├── Dockerfile -> (Application Build Recipe)
|
||||||
|
├── StopRecording.vbs -> (stop recording script on Windows)
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -96,7 +116,7 @@
|
|||||||
|
|
||||||
- 如果要长时间挂着软件循环监测直播,最好循环时间设置长一点(咱也不差没录制到的那几分钟),避免因请求频繁导致被官方封禁IP 。
|
- 如果要长时间挂着软件循环监测直播,最好循环时间设置长一点(咱也不差没录制到的那几分钟),避免因请求频繁导致被官方封禁IP 。
|
||||||
|
|
||||||
- 要停止直播录制,在录制界面使用 `Ctrl+C ` 组合键中断录制,若要停止其中某个直播间的录制,可在`URL_config.ini`文件中的地址前加#,会自动停止对应直播间的录制并正常保存视频。
|
- 要停止直播录制,Windows平台可执行StopRecording.vbs脚本文件,或者在录制界面使用 `Ctrl+C ` 组合键中断录制,若要停止其中某个直播间的录制,可在`URL_config.ini`文件中的地址前加#,会自动停止对应直播间的录制并正常保存已录制的视频。
|
||||||
- 最后,欢迎右上角给本项目一个star,同时也非常乐意大家提交pr。
|
- 最后,欢迎右上角给本项目一个star,同时也非常乐意大家提交pr。
|
||||||
|
|
||||||
 
|
 
|
||||||
@ -104,53 +124,53 @@
|
|||||||
直播间链接示例:
|
直播间链接示例:
|
||||||
|
|
||||||
```
|
```
|
||||||
抖音:
|
抖音:
|
||||||
https://live.douyin.com/745964462470
|
https://live.douyin.com/745964462470
|
||||||
https://v.douyin.com/iQFeBnt/
|
https://v.douyin.com/iQFeBnt/
|
||||||
https://live.douyin.com/yall1102
|
https://live.douyin.com/yall1102 (链接+抖音号)
|
||||||
|
https://v.douyin.com/CeiU5cbX (主播主页地址)
|
||||||
|
|
||||||
TikTok:
|
TikTok:
|
||||||
https://www.tiktok.com/@pearlgaga88/live
|
https://www.tiktok.com/@pearlgaga88/live
|
||||||
|
|
||||||
快手:
|
快手:
|
||||||
https://live.kuaishou.com/u/yall1102
|
https://live.kuaishou.com/u/yall1102
|
||||||
|
|
||||||
虎牙:
|
虎牙:
|
||||||
https://www.huya.com/52333
|
https://www.huya.com/52333
|
||||||
|
|
||||||
斗鱼:
|
斗鱼:
|
||||||
https://www.douyu.com/3637778?dyshid=
|
https://www.douyu.com/3637778?dyshid=
|
||||||
https://www.douyu.com/topic/wzDBLS6?rid=4921614&dyshid=
|
https://www.douyu.com/topic/wzDBLS6?rid=4921614&dyshid=
|
||||||
|
|
||||||
YY:
|
YY:
|
||||||
https://www.yy.com/22490906/22490906
|
https://www.yy.com/22490906/22490906
|
||||||
|
|
||||||
B站:
|
B站:
|
||||||
https://live.bilibili.com/320
|
https://live.bilibili.com/320
|
||||||
|
|
||||||
小红书:
|
小红书(直播间分享地址):
|
||||||
http://xhslink.com/xpJpfM
|
http://xhslink.com/xpJpfM
|
||||||
https://www.xiaohongshu.com/hina/livestream/569077534207413574/1707413727088?appuid=5f3f478a00000000010005b3&
|
|
||||||
|
|
||||||
bigo直播:
|
bigo直播:
|
||||||
https://www.bigo.tv/cn/716418802
|
https://www.bigo.tv/cn/716418802
|
||||||
|
|
||||||
buled直播:
|
buled直播:
|
||||||
https://app.blued.cn/live?id=Mp6G2R
|
https://app.blued.cn/live?id=Mp6G2R
|
||||||
|
|
||||||
AfreecaTV:
|
SOOP:
|
||||||
https://play.afreecatv.com/sw7love
|
https://play.sooplive.co.kr/sw7love
|
||||||
|
|
||||||
网易cc:
|
网易cc:
|
||||||
https://cc.163.com/583946984
|
https://cc.163.com/583946984
|
||||||
|
|
||||||
千度热播:
|
千度热播:
|
||||||
https://qiandurebo.com/web/video.php?roomnumber=33333
|
https://qiandurebo.com/web/video.php?roomnumber=33333
|
||||||
|
|
||||||
PandaTV:
|
PandaTV:
|
||||||
https://www.pandalive.co.kr/live/play/bara0109
|
https://www.pandalive.co.kr/live/play/bara0109
|
||||||
|
|
||||||
猫耳FM:
|
猫耳FM:
|
||||||
https://fm.missevan.com/live/868895007
|
https://fm.missevan.com/live/868895007
|
||||||
|
|
||||||
Look直播:
|
Look直播:
|
||||||
@ -159,7 +179,7 @@ https://look.163.com/live?id=65108820&position=3
|
|||||||
WinkTV:
|
WinkTV:
|
||||||
https://www.winktv.co.kr/live/play/anjer1004
|
https://www.winktv.co.kr/live/play/anjer1004
|
||||||
|
|
||||||
FlexTV:
|
FlexTV(TTinglive)::
|
||||||
https://www.flextv.co.kr/channels/593127/live
|
https://www.flextv.co.kr/channels/593127/live
|
||||||
|
|
||||||
PopkonTV:
|
PopkonTV:
|
||||||
@ -173,7 +193,6 @@ https://twitcasting.tv/c:uonq
|
|||||||
https://live.baidu.com/m/media/pclive/pchome/live.html?room_id=9175031377&tab_category
|
https://live.baidu.com/m/media/pclive/pchome/live.html?room_id=9175031377&tab_category
|
||||||
|
|
||||||
微博直播:
|
微博直播:
|
||||||
https://weibo.com/u/7676267963 (主页地址)
|
|
||||||
https://weibo.com/l/wblive/p/show/1022:2321325026370190442592
|
https://weibo.com/l/wblive/p/show/1022:2321325026370190442592
|
||||||
|
|
||||||
酷狗直播:
|
酷狗直播:
|
||||||
@ -186,41 +205,89 @@ LiveMe:
|
|||||||
https://www.liveme.com/zh/v/17141543493018047815/index.html
|
https://www.liveme.com/zh/v/17141543493018047815/index.html
|
||||||
|
|
||||||
花椒直播:
|
花椒直播:
|
||||||
https://www.huajiao.com/user/223184650 (主页地址)
|
https://www.huajiao.com/l/345096174
|
||||||
|
|
||||||
流星直播:
|
流星直播:
|
||||||
https://www.7u66.com/100960
|
https://www.7u66.com/100960
|
||||||
|
|
||||||
ShowRoom:
|
ShowRoom:
|
||||||
https://www.showroom-live.com/room/profile?room_id=480206 (主页地址)
|
https://www.showroom-live.com/room/profile?room_id=480206 (主播主页地址)
|
||||||
|
|
||||||
Acfun:
|
Acfun:
|
||||||
https://live.acfun.cn/live/179922
|
https://live.acfun.cn/live/179922
|
||||||
|
|
||||||
时光直播:
|
映客直播:
|
||||||
https://www.rengzu.com/180778
|
|
||||||
|
|
||||||
映客直播:
|
|
||||||
https://www.inke.cn/liveroom/index.html?uid=22954469&id=1720860391070904
|
https://www.inke.cn/liveroom/index.html?uid=22954469&id=1720860391070904
|
||||||
|
|
||||||
音播直播:
|
音播直播:
|
||||||
https://live.ybw1666.com/800002949
|
https://live.ybw1666.com/800002949
|
||||||
|
|
||||||
知乎直播:
|
知乎直播:
|
||||||
https://www.zhihu.com/theater/114453
|
https://www.zhihu.com/people/ac3a467005c5d20381a82230101308e9 (主播主页地址)
|
||||||
|
|
||||||
CHZZK:
|
CHZZK:
|
||||||
https://chzzk.naver.com/live/458f6ec20b034f49e0fc6d03921646d2
|
https://chzzk.naver.com/live/458f6ec20b034f49e0fc6d03921646d2
|
||||||
|
|
||||||
|
嗨秀直播:
|
||||||
|
https://www.haixiutv.com/6095106
|
||||||
|
|
||||||
|
VV星球直播:
|
||||||
|
https://h5webcdn-pro.vvxqiu.com//activity/videoShare/videoShare.html?h5Server=https://h5p.vvxqiu.com&roomId=LP115924473&platformId=vvstar
|
||||||
|
|
||||||
|
17Live:
|
||||||
|
https://17.live/en/live/6302408
|
||||||
|
|
||||||
|
浪Live:
|
||||||
|
https://www.lang.live/en-US/room/3349463
|
||||||
|
|
||||||
|
畅聊直播:
|
||||||
|
https://live.tlclw.com/106188
|
||||||
|
|
||||||
|
飘飘直播:
|
||||||
|
https://m.pp.weimipopo.com/live/preview.html?uid=91648673&anchorUid=91625862&app=plpl
|
||||||
|
|
||||||
|
六间房直播:
|
||||||
|
https://v.6.cn/634435
|
||||||
|
|
||||||
|
乐嗨直播:
|
||||||
|
https://www.lehaitv.com/8059096
|
||||||
|
|
||||||
|
花猫直播:
|
||||||
|
https://h.catshow168.com/live/preview.html?uid=19066357&anchorUid=18895331
|
||||||
|
|
||||||
|
Shopee:
|
||||||
|
https://sg.shp.ee/GmpXeuf?uid=1006401066&session=802458
|
||||||
|
|
||||||
|
Youtube:
|
||||||
|
https://www.youtube.com/watch?v=cS6zS5hi1w0
|
||||||
|
|
||||||
|
淘宝(需cookie):
|
||||||
|
https://tbzb.taobao.com/live?liveId=532359023188
|
||||||
|
https://m.tb.cn/h.TWp0HTd
|
||||||
|
|
||||||
|
京东:
|
||||||
|
https://3.cn/28MLBy-E
|
||||||
|
|
||||||
|
Faceit:
|
||||||
|
https://www.faceit.com/zh/players/Compl1/stream
|
||||||
|
|
||||||
|
连接直播:
|
||||||
|
https://show.lailianjie.com/10000258
|
||||||
|
|
||||||
|
咪咕直播:
|
||||||
|
https://www.miguvideo.com/p/live/120000541321
|
||||||
|
|
||||||
|
来秀直播:
|
||||||
|
https://www.imkktv.com/h5/share/video.html?uid=1845195&roomId=1710496
|
||||||
|
|
||||||
|
Picarto:
|
||||||
|
https://www.picarto.tv/cuteavalanche
|
||||||
```
|
```
|
||||||
|
|
||||||
 
|
 
|
||||||
|
|
||||||
在线播放m3u8和flv视频网站:[M3U8 在线视频播放器 ](https://jx.hmily.vip/play/)
|
|
||||||
|
|
||||||
 
|
|
||||||
|
|
||||||
## 🎃源码运行
|
## 🎃源码运行
|
||||||
使用源码运行,前提要有Python环境,如果没有请先安装Python,再执行下面步骤。
|
使用源码运行,可参考下面的步骤。
|
||||||
|
|
||||||
1.首先拉取或手动下载本仓库项目代码
|
1.首先拉取或手动下载本仓库项目代码
|
||||||
|
|
||||||
@ -232,9 +299,94 @@ git clone https://github.com/ihmily/DouyinLiveRecorder.git
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd DouyinLiveRecorder
|
cd DouyinLiveRecorder
|
||||||
pip3 install -r requirements.txt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> - 不论你是否已安装 **Python>=3.10** 环境, 都推荐使用 [**uv**](https://github.com/astral-sh/uv) 运行, 因为它可以自动管理虚拟环境和方便地管理 **Python** 版本, **不过这完全是可选的**<br />
|
||||||
|
> 使用以下命令安装
|
||||||
|
> ```bash
|
||||||
|
> # 在 macOS 和 Linux 上安装 uv
|
||||||
|
> curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
> ```
|
||||||
|
> ```powershell
|
||||||
|
> # 在 Windows 上安装 uv
|
||||||
|
> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||||
|
> ```
|
||||||
|
> - 如果安装依赖速度太慢, 你可以考虑使用国内 pip 镜像源:<br />
|
||||||
|
> 在 `pip` 命令使用 `-i` 参数指定, 如 `pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple`<br />
|
||||||
|
> 或者在 `uv` 命令 `--index` 选项指定, 如 `uv sync --index https://pypi.tuna.tsinghua.edu.cn/simple`
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>如果已安装 <b>Python>=3.10</b> 环境</summary>
|
||||||
|
|
||||||
|
- :white_check_mark: 在虚拟环境中安装 (推荐)
|
||||||
|
|
||||||
|
1. 创建虚拟环境
|
||||||
|
|
||||||
|
- 使用系统已安装的 Python, 不使用 uv
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
- 使用 uv, 默认使用系统 Python, 你可以添加 `--python` 选项指定 Python 版本而不使用系统 Python [uv官方文档](https://docs.astral.sh/uv/concepts/python-versions/)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv venv
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 在终端激活虚拟环境 (在未安装 uv 或你想要手动激活虚拟环境时执行, 若已安装 uv, 可以跳过这一步, uv 会自动激活并使用虚拟环境)
|
||||||
|
|
||||||
|
**Bash** 中
|
||||||
|
```bash
|
||||||
|
source .venv/Scripts/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Powershell** 中
|
||||||
|
```powershell
|
||||||
|
.venv\Scripts\activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows CMD** 中
|
||||||
|
```bat
|
||||||
|
.venv\Scripts\activate.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用 pip (若安装太慢或失败, 可使用 `-i` 指定镜像源)
|
||||||
|
pip3 install -U pip && pip3 install -r requirements.txt
|
||||||
|
# 或者使用 uv (可使用 `--index` 指定镜像源)
|
||||||
|
uv sync
|
||||||
|
# 或者
|
||||||
|
uv pip sync requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
- :x: 在系统 Python 环境中安装 (不推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip3 install -U pip && pip3 install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>如果未安装 <b>Python>=3.10</b> 环境</summary>
|
||||||
|
|
||||||
|
你可以使用 [**uv**](https://github.com/astral-sh/uv) 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# uv 将使用 3.10 及以上的最新 python 发行版自动创建并使用虚拟环境, 可使用 --python 选项指定 python 版本, 参见 https://docs.astral.sh/uv/reference/cli/#uv-sync--python 和 https://docs.astral.sh/uv/reference/cli/#uv-pip-sync--python
|
||||||
|
uv sync
|
||||||
|
# 或
|
||||||
|
uv pip sync requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
3.安装[FFmpeg](https://ffmpeg.org/download.html#build-linux),如果是Windows系统,这一步可跳过。对于Linux系统,执行以下命令安装
|
3.安装[FFmpeg](https://ffmpeg.org/download.html#build-linux),如果是Windows系统,这一步可跳过。对于Linux系统,执行以下命令安装
|
||||||
|
|
||||||
CentOS执行
|
CentOS执行
|
||||||
@ -267,6 +419,12 @@ brew install ffmpeg
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
python main.py
|
python main.py
|
||||||
|
|
||||||
|
```
|
||||||
|
或
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
其中Linux系统请使用`python3 main.py` 运行。
|
其中Linux系统请使用`python3 main.py` 运行。
|
||||||
@ -290,7 +448,7 @@ docker-compose up
|
|||||||
|
|
||||||
2.构建镜像(可选)
|
2.构建镜像(可选)
|
||||||
|
|
||||||
如果你只想简单的运行程序,则不需要做这一步。要自定义本地构建,可以修改 [docker-compose.yaml](https://github.com/ihmily/DouyinLiveRecorder/blob/main/docker-compose.yaml) 文件,如将镜像名修改为 `douyin-live-recorder:latest`,并取消 `# build: .` 注释,然后再执行
|
如果你只想简单的运行程序,则不需要做这一步。Docker镜像仓库中代码版本可能不是最新的,如果要运行本仓库主分支最新代码,可以本地自定义构建,通过修改 [docker-compose.yaml](https://github.com/ihmily/DouyinLiveRecorder/blob/main/docker-compose.yaml) 文件,如将镜像名修改为 `douyin-live-recorder:latest`,并取消 `# build: .` 注释,然后再执行
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t douyin-live-recorder:latest .
|
docker build -t douyin-live-recorder:latest .
|
||||||
@ -323,6 +481,13 @@ docker-compose stop
|
|||||||
|
|
||||||
 
|
 
|
||||||
|
|
||||||
|
## 🤖相关项目
|
||||||
|
|
||||||
|
- StreamCap: https://github.com/ihmily/StreamCap
|
||||||
|
- streamget: https://github.com/ihmily/streamget
|
||||||
|
|
||||||
|
 
|
||||||
|
|
||||||
## ❤️贡献者
|
## ❤️贡献者
|
||||||
|
|
||||||
   [](https://github.com/ihmily)
|
   [](https://github.com/ihmily)
|
||||||
@ -337,16 +502,54 @@ docker-compose stop
|
|||||||
[](https://github.com/justdoiting)
|
[](https://github.com/justdoiting)
|
||||||
[](https://github.com/dhbxs)
|
[](https://github.com/dhbxs)
|
||||||
[](https://github.com/wujiyu115)
|
[](https://github.com/wujiyu115)
|
||||||
|
[](https://github.com/zhanghao333)
|
||||||
|
<a href="https://github.com/gyc0123" target="_blank"><img src="https://github.com/gyc0123.png?size=50" alt="gyc0123" style="width:53px; height:51px;" /></a>
|
||||||
|
|
||||||
|
   [](https://github.com/HoratioShaw)
|
||||||
|
[](https://github.com/nov30th)
|
||||||
|
[](https://github.com/727155455)
|
||||||
|
[](https://github.com/nixingshiguang)
|
||||||
|
[](https://github.com/1411430556)
|
||||||
|
[](https://github.com/Ovear)
|
||||||
 
|
 
|
||||||
|
|
||||||
## ⏳提交日志
|
## ⏳提交日志
|
||||||
|
|
||||||
|
- 20251024
|
||||||
|
- 修复抖音风控无法获取数据问题
|
||||||
|
|
||||||
|
- 新增soop.com录制支持
|
||||||
|
|
||||||
|
- 修复bigo录制
|
||||||
|
|
||||||
|
- 20250127
|
||||||
|
- 新增淘宝、京东、faceit直播录制
|
||||||
|
- 修复小红书直播流录制以及转码问题
|
||||||
|
- 修复畅聊、VV星球、flexTV直播录制
|
||||||
|
- 修复批量微信直播推送
|
||||||
|
- 新增email发送ssl和port配置
|
||||||
|
- 新增强制转h264配置
|
||||||
|
- 更新ffmpeg版本
|
||||||
|
- 重构包为异步函数!
|
||||||
|
|
||||||
|
- 20241130
|
||||||
|
- 新增shopee、youtube直播录制
|
||||||
|
- 新增支持自定义m3u8、flv地址录制
|
||||||
|
- 新增自定义执行脚本,支持python、bat、bash等
|
||||||
|
- 修复YY直播、花椒直播和小红书直播录制
|
||||||
|
- 修复b站标题获取错误
|
||||||
|
- 修复log日志错误
|
||||||
|
- 20241030
|
||||||
|
- 新增嗨秀直播、vv星球直播、17Live、浪Live、SOOP、畅聊直播(原时光直播)、飘飘直播、六间房直播、乐嗨直播、花猫直播等10个平台直播录制
|
||||||
|
- 修复小红书直播录制,支持小红书作者主页地址录制直播
|
||||||
|
- 新增支持ntfy消息推送,以及新增支持批量推送多个地址(逗号分隔多个推送地址)
|
||||||
|
- 修复Liveme直播录制、twitch直播录制
|
||||||
|
- 新增Windows平台一键停止录制VB脚本程序
|
||||||
- 20241005
|
- 20241005
|
||||||
- 新增邮箱和Bark推送
|
- 新增邮箱和Bark推送
|
||||||
- 新增直播注释停止录制
|
- 新增直播注释停止录制
|
||||||
- 优化分段录制
|
- 优化分段录制
|
||||||
- 重构部分代码
|
- 重构部分代码
|
||||||
|
|
||||||
- 20240928
|
- 20240928
|
||||||
- 新增知乎直播、CHZZK直播录制
|
- 新增知乎直播、CHZZK直播录制
|
||||||
- 修复音播直播录制
|
- 修复音播直播录制
|
||||||
@ -359,7 +562,6 @@ docker-compose stop
|
|||||||
- 新增时光直播录制
|
- 新增时光直播录制
|
||||||
- 20240701
|
- 20240701
|
||||||
- 修复虎牙直播录制2分钟断流问题
|
- 修复虎牙直播录制2分钟断流问题
|
||||||
|
|
||||||
- 新增自定义直播推送内容
|
- 新增自定义直播推送内容
|
||||||
- 20240621
|
- 20240621
|
||||||
- 新增Acfun、ShowRoom直播录制
|
- 新增Acfun、ShowRoom直播录制
|
||||||
@ -372,13 +574,10 @@ docker-compose stop
|
|||||||
- 修复部分虎牙直播间录制错误
|
- 修复部分虎牙直播间录制错误
|
||||||
- 20240508
|
- 20240508
|
||||||
- 修复花椒直播录制
|
- 修复花椒直播录制
|
||||||
|
|
||||||
- 更改文件路径解析方式 [@kaine1973](https://github.com/kaine1973)
|
- 更改文件路径解析方式 [@kaine1973](https://github.com/kaine1973)
|
||||||
- 20240506
|
- 20240506
|
||||||
- 修复抖音录制画质解析bug
|
- 修复抖音录制画质解析bug
|
||||||
|
|
||||||
- 修复虎牙录制 60帧最高画质问题
|
- 修复虎牙录制 60帧最高画质问题
|
||||||
|
|
||||||
- 新增流星直播录制
|
- 新增流星直播录制
|
||||||
- 20240427
|
- 20240427
|
||||||
- 新增LiveMe、花椒直播录制
|
- 新增LiveMe、花椒直播录制
|
||||||
@ -388,13 +587,10 @@ docker-compose stop
|
|||||||
- 新增酷狗直播录制、优化PopkonTV直播录制
|
- 新增酷狗直播录制、优化PopkonTV直播录制
|
||||||
- 20240423
|
- 20240423
|
||||||
- 新增百度直播录制、微博直播录制
|
- 新增百度直播录制、微博直播录制
|
||||||
|
|
||||||
- 修复斗鱼录制直播回放的问题
|
- 修复斗鱼录制直播回放的问题
|
||||||
|
|
||||||
- 新增直播源地址显示以及输出到日志文件设置
|
- 新增直播源地址显示以及输出到日志文件设置
|
||||||
- 20240311
|
- 20240311
|
||||||
- 修复海外平台录制bug,增加画质选择,增强录制稳定性
|
- 修复海外平台录制bug,增加画质选择,增强录制稳定性
|
||||||
|
|
||||||
- 修复虎牙录制bug (虎牙`一起看`频道 有特殊限制,有时无法录制)
|
- 修复虎牙录制bug (虎牙`一起看`频道 有特殊限制,有时无法录制)
|
||||||
- 20240309
|
- 20240309
|
||||||
- 修复虎牙直播、小红书直播和B站直播录制
|
- 修复虎牙直播、小红书直播和B站直播录制
|
||||||
@ -408,37 +604,29 @@ docker-compose stop
|
|||||||
- 修复了小红书直播因官方更新直播域名,导致无法录制直播的问题
|
- 修复了小红书直播因官方更新直播域名,导致无法录制直播的问题
|
||||||
- 修复了更新URL配置文件的bug
|
- 修复了更新URL配置文件的bug
|
||||||
- 最后,祝大家新年快乐!
|
- 最后,祝大家新年快乐!
|
||||||
|
|
||||||
|
<details><summary>点击展开更多提交日志</summary>
|
||||||
|
|
||||||
- 20240129
|
- 20240129
|
||||||
- 新增猫耳FM直播录制
|
- 新增猫耳FM直播录制
|
||||||
- 20240127
|
- 20240127
|
||||||
- 新增千度热播直播录制、新增pandaTV(韩国)直播录制
|
- 新增千度热播直播录制、新增pandaTV(韩国)直播录制
|
||||||
|
|
||||||
- 新增telegram直播状态消息推送,修复了某些bug
|
- 新增telegram直播状态消息推送,修复了某些bug
|
||||||
|
|
||||||
- 新增自定义设置不同直播间的录制画质(即每个直播间录制画质可不同)
|
- 新增自定义设置不同直播间的录制画质(即每个直播间录制画质可不同)
|
||||||
|
|
||||||
- 修改录制视频保存路径为 `downloads` 文件夹,并且分平台进行保存。
|
- 修改录制视频保存路径为 `downloads` 文件夹,并且分平台进行保存。
|
||||||
- 20240114
|
- 20240114
|
||||||
- 新增网易cc直播录制,优化ffmpeg参数,修改AfreecaTV输入直播地址格式
|
- 新增网易cc直播录制,优化ffmpeg参数,修改AfreecaTV输入直播地址格式
|
||||||
|
|
||||||
- 修改日志记录器 @[iridescentGray](https://github.com/iridescentGray)
|
- 修改日志记录器 @[iridescentGray](https://github.com/iridescentGray)
|
||||||
- 20240102
|
- 20240102
|
||||||
- 修复Linux上运行,新增docker配置文件
|
- 修复Linux上运行,新增docker配置文件
|
||||||
- 20231210
|
- 20231210
|
||||||
|
|
||||||
- 修复录制分段bug,修复bigo录制检测bug
|
- 修复录制分段bug,修复bigo录制检测bug
|
||||||
|
|
||||||
- 新增自定义修改录制主播名
|
- 新增自定义修改录制主播名
|
||||||
|
|
||||||
|
|
||||||
- 新增AfreecaTV直播录制,修复某些可能会发生的bug
|
- 新增AfreecaTV直播录制,修复某些可能会发生的bug
|
||||||
|
|
||||||
- 20231207
|
- 20231207
|
||||||
- 新增blued直播录制,修复YY直播录制,新增直播结束消息推送
|
- 新增blued直播录制,修复YY直播录制,新增直播结束消息推送
|
||||||
|
|
||||||
- 20231206
|
- 20231206
|
||||||
- 新增bigo直播录制
|
- 新增bigo直播录制
|
||||||
|
|
||||||
- 20231203
|
- 20231203
|
||||||
- 新增小红书直播录制(全网首发),目前小红书官方没有切换清晰度功能,因此直播录制也只有默认画质
|
- 新增小红书直播录制(全网首发),目前小红书官方没有切换清晰度功能,因此直播录制也只有默认画质
|
||||||
- 小红书录制暂时无法循环监测,每次主播开启直播,都要重新获取一次链接
|
- 小红书录制暂时无法循环监测,每次主播开启直播,都要重新获取一次链接
|
||||||
@ -448,18 +636,14 @@ docker-compose stop
|
|||||||
- 欢迎各位大佬提pr 帮忙更新维护
|
- 欢迎各位大佬提pr 帮忙更新维护
|
||||||
- 20230930
|
- 20230930
|
||||||
- 新增抖音从接口获取直播流,增强稳定性
|
- 新增抖音从接口获取直播流,增强稳定性
|
||||||
|
|
||||||
- 修改快手获取直播流的方式,改用从官方接口获取
|
- 修改快手获取直播流的方式,改用从官方接口获取
|
||||||
|
|
||||||
- 祝大家中秋节快乐!
|
- 祝大家中秋节快乐!
|
||||||
- 20230919
|
- 20230919
|
||||||
- 修复了快手版本更新后录制出错的问题,增加了其自动获取cookie(~~稳定性未知~~)
|
- 修复了快手版本更新后录制出错的问题,增加了其自动获取cookie(~~稳定性未知~~)
|
||||||
- 修复了TikTok显示正在直播但不进行录制的问题
|
- 修复了TikTok显示正在直播但不进行录制的问题
|
||||||
- 20230907
|
- 20230907
|
||||||
- 修复了因抖音官方更新了版本导致的录制出错以及短链接转换出错
|
- 修复了因抖音官方更新了版本导致的录制出错以及短链接转换出错
|
||||||
|
|
||||||
- 修复B站无法录制原画视频的bug
|
- 修复B站无法录制原画视频的bug
|
||||||
|
|
||||||
- 修改了配置文件字段,新增各平台自定义设置Cookie
|
- 修改了配置文件字段,新增各平台自定义设置Cookie
|
||||||
- 20230903
|
- 20230903
|
||||||
- 修复了TikTok录制时报644无法录制的问题
|
- 修复了TikTok录制时报644无法录制的问题
|
||||||
@ -476,11 +660,9 @@ docker-compose stop
|
|||||||
- 修复主播重新开播无法再次录制的问题
|
- 修复主播重新开播无法再次录制的问题
|
||||||
- 20230807
|
- 20230807
|
||||||
- 新增了斗鱼直播录制
|
- 新增了斗鱼直播录制
|
||||||
|
|
||||||
- 修复显示录制完成之后会重新开始录制的问题
|
- 修复显示录制完成之后会重新开始录制的问题
|
||||||
- 20230805
|
- 20230805
|
||||||
- 新增了虎牙直播录制,其暂时只能用flv视频流进行录制
|
- 新增了虎牙直播录制,其暂时只能用flv视频流进行录制
|
||||||
|
|
||||||
- Web API 新增了快手和虎牙这两个平台的直播流解析(TikTok要代理)
|
- Web API 新增了快手和虎牙这两个平台的直播流解析(TikTok要代理)
|
||||||
- 20230804
|
- 20230804
|
||||||
- 新增了快手直播录制,优化了部分代码
|
- 新增了快手直播录制,优化了部分代码
|
||||||
@ -490,10 +672,8 @@ docker-compose stop
|
|||||||
- 新增了国际版抖音TikTok的直播录制,去除冗余 简化了部分代码
|
- 新增了国际版抖音TikTok的直播录制,去除冗余 简化了部分代码
|
||||||
- 20230724
|
- 20230724
|
||||||
- 新增了一个通过抖音直播间地址获取直播视频流链接的API接口,上传即可用
|
- 新增了一个通过抖音直播间地址获取直播视频流链接的API接口,上传即可用
|
||||||
|
</details>
|
||||||
|
 
|
||||||
|
|
||||||
|
## 有问题可以提issue, 我会在这里持续添加更多直播平台的录制 欢迎Star
|
||||||
 
|
|
||||||
|
|
||||||
## 有问题可以提issue ,后续我会在这里不断更新其他直播平台的录制 欢迎Star
|
|
||||||
|
|
||||||
####
|
####
|
||||||
|
|||||||
69
StopRecording.vbs
Normal file
69
StopRecording.vbs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
'********************************************************************************************/
|
||||||
|
'* File Name : StopRecording.vbs
|
||||||
|
'* Created Date : 2024-10-15 01:50:30
|
||||||
|
'* Author : Hmily
|
||||||
|
'* GitHub : http://github.com/ihmily
|
||||||
|
'* Description : This script is designed to terminate the process of live recording
|
||||||
|
'********************************************************************************************/
|
||||||
|
|
||||||
|
Dim objWMIService, colProcesses, objProcess
|
||||||
|
Dim intResponse
|
||||||
|
strComputer = "."
|
||||||
|
On Error Resume Next
|
||||||
|
intResponse = MsgBox("确定要结束所有后台直播录制进程吗?", vbYesNo + vbQuestion, "确认结束进程")
|
||||||
|
|
||||||
|
If intResponse = vbYes Then
|
||||||
|
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
|
||||||
|
If Err.Number <> 0 Then
|
||||||
|
Err.Clear
|
||||||
|
End If
|
||||||
|
|
||||||
|
Set colProcesses = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'ffmpeg.exe'")
|
||||||
|
Set colProcesses2 = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'pythonw.exe'")
|
||||||
|
Set colProcesses3 = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'DouyinLiveRecorder.exe'")
|
||||||
|
If Err.Number <> 0 Then
|
||||||
|
Err.Clear
|
||||||
|
End If
|
||||||
|
|
||||||
|
If Not objWMIService Is Nothing And Not colProcesses Is Nothing And Not colProcesses2 Is Nothing Then
|
||||||
|
If colProcesses2.Count = 0 And colProcesses3.Count = 0 Then
|
||||||
|
MsgBox "没有找到录制程序的进程", vbExclamation, "提示信息"
|
||||||
|
WScript.Quit(1)
|
||||||
|
Else
|
||||||
|
For Each objProcess in colProcesses
|
||||||
|
objProcess.Terminate()
|
||||||
|
If Err.Number <> 0 Then
|
||||||
|
objShell.Run "taskkill /f /im " & objProcess.Name, 0, True
|
||||||
|
Err.Clear
|
||||||
|
End If
|
||||||
|
Next
|
||||||
|
End If
|
||||||
|
Else
|
||||||
|
objShell.Run "taskkill /f /im " & objProcess.Name, 0, True
|
||||||
|
End If
|
||||||
|
MsgBox "已成功结束正在录制直播的进程!" & vbCrLf & "关闭此窗口30秒后自动停止录制程序", vbInformation, "提示信息"
|
||||||
|
|
||||||
|
WScript.Sleep 10000
|
||||||
|
If colProcesses3.Count <> 0 Then
|
||||||
|
Set colProcesses_ = colProcesses3
|
||||||
|
Else
|
||||||
|
Set colProcesses_ = colProcesses2
|
||||||
|
End If
|
||||||
|
For Each objProcess in colProcesses_
|
||||||
|
objProcess.Terminate()
|
||||||
|
If Err.Number <> 0 Then
|
||||||
|
objShell.Run "taskkill /f /im " & objProcess.Name, 0, True
|
||||||
|
Err.Clear
|
||||||
|
End If
|
||||||
|
Next
|
||||||
|
Else
|
||||||
|
MsgBox "已取消结束录制操作", vbExclamation, "提示信息"
|
||||||
|
End If
|
||||||
|
|
||||||
|
On Error GoTo 0
|
||||||
|
Set objWMIService = Nothing
|
||||||
|
Set colProcesses = Nothing
|
||||||
|
Set colProcesses2 = Nothing
|
||||||
|
Set colProcesses3 = Nothing
|
||||||
|
Set objProcess = Nothing
|
||||||
|
Set objShell = Nothing
|
||||||
121
api/convert.php
121
api/convert.php
@ -1,121 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Author: Hmily
|
|
||||||
* Github:https://github.com/ihmily
|
|
||||||
* Date: 2023-07-20 21:06:20
|
|
||||||
* Update: 2023-09-07 22:34:57
|
|
||||||
* Copyright (c) 2023 by Hmily, All Rights Reserved.
|
|
||||||
* Function:convert short url to long url
|
|
||||||
* Address:https://github.com/ihmily/DouyinLiveRecorder
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// 该代码主要是用来转换地址,将短app端直播分享链接转为PC网页端长链接
|
|
||||||
header('Content-type: application/json; charset=utf-8');
|
|
||||||
|
|
||||||
|
|
||||||
if(empty($_GET['url'])){
|
|
||||||
exit(json_encode(['code'=>-2,'msg'=>'请输入app端直播间分享地址'],448));
|
|
||||||
}
|
|
||||||
$share_url=$_GET['url'];
|
|
||||||
|
|
||||||
$get_id=get_redirect_url($share_url);
|
|
||||||
preg_match('/reflow\/(.*?)\?/', $get_id, $room_id);
|
|
||||||
preg_match('/sec_user_id=([\w\d_\-]+)&/', $get_id, $sec_user_id);
|
|
||||||
$room_data=get_live_web_rid($room_id[1],$sec_user_id[1]);
|
|
||||||
$title=$room_data[0];
|
|
||||||
$web_rid=$room_data[1];
|
|
||||||
|
|
||||||
|
|
||||||
if(empty($web_rid)){
|
|
||||||
exit(json_encode(['code'=>-1,'status'=>'解析失败','msg'=>'请检测链接是否正确,多次失败请联系作者修复!https://github.com/ihmily/DouyinLiveRecorder'],448));
|
|
||||||
}
|
|
||||||
$long_url="https://live.douyin.com/".$web_rid;
|
|
||||||
$return=
|
|
||||||
[
|
|
||||||
'code'=>0,
|
|
||||||
'status'=>'解析成功',
|
|
||||||
'title'=>$title,
|
|
||||||
'room_id'=>$room_id[1],
|
|
||||||
'share_url'=>$share_url,
|
|
||||||
'long_url'=>$long_url,
|
|
||||||
'source'=>'源码地址:https://github.com/ihmily/DouyinLiveRecorder'
|
|
||||||
];
|
|
||||||
exit(json_encode($return,448));
|
|
||||||
|
|
||||||
|
|
||||||
// 抖音X-bogus算法,直接调用我封装的接口
|
|
||||||
function get_xbogus($url) {
|
|
||||||
$query = parse_url($url, PHP_URL_QUERY);
|
|
||||||
$url = "http://43.138.133.177:8890/xbogus";
|
|
||||||
$data = array(
|
|
||||||
'url' => $query,
|
|
||||||
'ua' => "Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36",
|
|
||||||
);
|
|
||||||
$params = http_build_query($data);
|
|
||||||
$url = $url . '?' . $params;
|
|
||||||
$response = file_get_contents($url);
|
|
||||||
$response_json = json_decode($response, true);
|
|
||||||
$xbogus = $response_json['result'];
|
|
||||||
// echo "生成的X-Bogus签名为: " . $xbogus;
|
|
||||||
return $xbogus;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function get_live_web_rid($room_id, $sec_user_id) {
|
|
||||||
|
|
||||||
$url = 'https://webcast.amemv.com/webcast/room/reflow/info/?verifyFp=verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf&type_id=0&live_id=1&room_id='.$room_id.'&sec_user_id='.$sec_user_id.'&app_id=1128&msToken=wrqzbEaTlsxt52-vxyZo_mIoL0RjNi1ZdDe7gzEGMUTVh_HvmbLLkQrA_1HKVOa2C6gkxb6IiY6TY2z8enAkPEwGq--gM-me3Yudck2ailla5Q4osnYIHxd9dI4WtQ==';
|
|
||||||
$xbogus = get_xbogus($url); // 获取X-Bogus算法
|
|
||||||
|
|
||||||
$url = $url . "&X-Bogus=" . $xbogus;
|
|
||||||
$headers = array(
|
|
||||||
'User-Agent: Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36',
|
|
||||||
'Accept:application/json, text/plain, */*',
|
|
||||||
'Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
|
|
||||||
);
|
|
||||||
$cookies='s_v_web_id=verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf';
|
|
||||||
$json_data = get_curl($url,$headers,$cookies);
|
|
||||||
$json_data = json_decode($json_data, true);
|
|
||||||
$web_rid = $json_data['data']['room']['owner']['web_rid'];
|
|
||||||
$title=$json_data["data"]['room']['title'];
|
|
||||||
return [$title,$web_rid];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 封装的CURL函数
|
|
||||||
function get_curl($url,$headers=array(),$cookies=''){
|
|
||||||
$curl=curl_init((string)$url);
|
|
||||||
curl_setopt($curl,CURLOPT_HEADER,false);
|
|
||||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
|
||||||
curl_setopt($curl,CURLOPT_SSL_VERIFYPEER,false);
|
|
||||||
curl_setopt($curl, CURLOPT_ENCODING, "");
|
|
||||||
curl_setopt($curl,CURLOPT_RETURNTRANSFER,true);
|
|
||||||
curl_setopt($curl,CURLOPT_HTTPHEADER,$headers);
|
|
||||||
curl_setopt($curl, CURLOPT_COOKIE, $cookies);
|
|
||||||
curl_setopt($curl,CURLOPT_TIMEOUT,20);
|
|
||||||
$data = curl_exec($curl);
|
|
||||||
// var_dump($data);
|
|
||||||
curl_close($curl);
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function get_redirect_url($url) {
|
|
||||||
$curl = curl_init();
|
|
||||||
curl_setopt($curl, CURLOPT_URL, $url);
|
|
||||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
|
|
||||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
|
|
||||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array( "User-Agent:Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"));
|
|
||||||
curl_setopt($curl, CURLOPT_HEADER, true);
|
|
||||||
curl_setopt($curl, CURLOPT_NOBODY, 1);
|
|
||||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
|
|
||||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
|
||||||
curl_setopt($curl,CURLOPT_TIMEOUT,20);
|
|
||||||
$ret = curl_exec($curl);
|
|
||||||
curl_close($curl);
|
|
||||||
preg_match("/Location: (.*?)\r\n/iU",$ret,$location);
|
|
||||||
return $location[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
252
api/index.php
252
api/index.php
@ -1,252 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Author: Hmily
|
|
||||||
* Github:https://github.com/ihmily
|
|
||||||
* Date: 2023-07-20 21:06:20
|
|
||||||
* Update: 2023-09-17 20:23:00
|
|
||||||
* Copyright (c) 2023 by Hmily, All Rights Reserved.
|
|
||||||
* Function:Spider the live stream url
|
|
||||||
* Address:https://github.com/ihmily/DouyinLiveRecorder
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// 本API代码只有解析抖音、快手和虎牙的,有需要其他的可自己根据源码增加
|
|
||||||
// 注意:抖音和快手的 要添加上自己的cookie才能用
|
|
||||||
header('Content-type: application/json; charset=utf-8');
|
|
||||||
|
|
||||||
if(empty($_GET['url'])){
|
|
||||||
exit(json_encode(['code'=>-2,'msg'=>'请输入抖音/快手/虎牙等平台的直播间地址'],448));
|
|
||||||
}
|
|
||||||
$live_url=$_GET['url'];
|
|
||||||
|
|
||||||
if (strpos($live_url, 'douyin') !== false) {
|
|
||||||
$pattern = "/^https:\/\/v\.douyin\.com\/\w+\/$/";
|
|
||||||
if (preg_match($pattern, $live_url)) {
|
|
||||||
// 判断是否是app端分享链接,如果是则转为PC网页端地址,否则无法解析
|
|
||||||
// 示例链接:
|
|
||||||
// $live_url="https://live.douyin.com/187615265444";
|
|
||||||
$json_str=get_curl("https://hmily.vip/api/jx/live/convert.php?url=".$live_url);
|
|
||||||
$json_data=json_decode($json_str,true);
|
|
||||||
$live_url = $json_data['long_url'];
|
|
||||||
}
|
|
||||||
$json_data2=get_douyin_json_data($live_url);
|
|
||||||
$return=get_douyin_stream_url2($json_data2,$live_url); // 选用第二种方式
|
|
||||||
|
|
||||||
} else if(strpos($live_url, 'kuaishou') !== false) {
|
|
||||||
$return=get_kuaishou_stream_url($live_url); // 选用第二种方式
|
|
||||||
}else if(strpos($live_url, 'huya') !== false) {
|
|
||||||
$return=get_huya_stream_url($live_url); // 选用第二种方式
|
|
||||||
}else{
|
|
||||||
$return=['code'=>-1,'msg'=>'暂不支持该平台,请检查链接是否正确'];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
exit(json_encode($return,448));
|
|
||||||
|
|
||||||
|
|
||||||
function get_douyin_json_data($url) {
|
|
||||||
$headers = array(
|
|
||||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
|
||||||
'Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
|
|
||||||
'Referer: https://live.douyin.com/',
|
|
||||||
);
|
|
||||||
|
|
||||||
$cookies='your cookie'; # 任意抖音直播间页面的Cookie
|
|
||||||
|
|
||||||
$html_str = get_curl($url,$headers,$cookies);
|
|
||||||
$pattern = '/\{\\\"state(.*?)\"\]\)\<\/script\>\<div hidden/';
|
|
||||||
preg_match($pattern, $html_str, $matches);
|
|
||||||
$json_string = '{\"state' . explode(']\n', $matches[1])[0];
|
|
||||||
$cleaned_string = str_replace("\\", "", $json_string);
|
|
||||||
$cleaned_string = preg_replace('/bdp_log=(.*?)&bdpsum=/', '', $cleaned_string);
|
|
||||||
$replacements = array(
|
|
||||||
'"[' => '[',
|
|
||||||
']"' => ']',
|
|
||||||
'"{' => '{',
|
|
||||||
'}"' => '}',
|
|
||||||
'u0026' => '&'
|
|
||||||
);
|
|
||||||
$cleaned_string = strtr($cleaned_string, $replacements);
|
|
||||||
$json_data = json_decode($cleaned_string, true);
|
|
||||||
return $json_data;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 第一种数据
|
|
||||||
function get_douyin_stream_url($json_data) {
|
|
||||||
$initialState = $json_data['state'];
|
|
||||||
$streamStore = $initialState['streamStore'];
|
|
||||||
$roomStore = $initialState['roomStore'];
|
|
||||||
$streamData = $streamStore['streamData']['H265_streamData']['options'];
|
|
||||||
$stream = $streamStore['streamData']['H265_streamData']['stream'];
|
|
||||||
$stream2 = $streamStore['streamData']['H264_streamData']['stream'];
|
|
||||||
$anchor_name = $roomStore['roomInfo']['anchor']['nickname'];
|
|
||||||
$data=array();
|
|
||||||
if ($stream === null) {
|
|
||||||
$data=["live_status"=>'主播未开播或者直播已经结束!'];
|
|
||||||
} else {
|
|
||||||
|
|
||||||
$m3u8_url = $stream['origin']['main']['hls'];
|
|
||||||
$m3u8_url2 = $stream2['origin']['main']['hls'];
|
|
||||||
$data=["title"=>$live_title,'stream'=>['m3u8_url_265'=>$m3u8_url,'m3u8_url_264'=>$m3u8_url2]];
|
|
||||||
}
|
|
||||||
$return=[
|
|
||||||
'code'=>0,
|
|
||||||
'status'=>'解析成功',
|
|
||||||
'anchor_name'=>$anchor_name,
|
|
||||||
'live_url'=>$live_url,
|
|
||||||
'data'=>$data,
|
|
||||||
'source'=>'源码地址:https://github.com/ihmily/DouyinLiveRecorder'
|
|
||||||
];
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 第二种数据(更好)
|
|
||||||
function get_douyin_stream_url2($json_data,$live_url) {
|
|
||||||
$roomStore = $json_data['state']['roomStore'];
|
|
||||||
$roomInfo = $roomStore['roomInfo'];
|
|
||||||
$anchor_name = $roomInfo['anchor']['nickname'];
|
|
||||||
$live_title = $roomInfo['room']['title'] ;
|
|
||||||
// 获取直播间状态
|
|
||||||
$status = $roomInfo["room"]["status"]; // 直播状态2是正在直播.4是未开播
|
|
||||||
$data=array();
|
|
||||||
if ($status == 4) {
|
|
||||||
$data=["live_status"=>'主播未开播或者直播已经结束!'];
|
|
||||||
} else {
|
|
||||||
$stream_url = $roomInfo['room']['stream_url'];
|
|
||||||
// flv视频流链接
|
|
||||||
$flv_url_list = $stream_url['flv_pull_url'];
|
|
||||||
// m3u8视频流链接
|
|
||||||
$m3u8_url_list = $stream_url['hls_pull_url_map'];
|
|
||||||
$data=["title"=>$live_title,'stream'=>['flv_url_list'=>$flv_url_list,'m3u8_url_list'=>$m3u8_url_list]];
|
|
||||||
|
|
||||||
}
|
|
||||||
$return=[
|
|
||||||
'code'=>0,
|
|
||||||
'status'=>'解析成功',
|
|
||||||
'platform'=>'抖音直播',
|
|
||||||
'anchor_name'=>$anchor_name,
|
|
||||||
'live_url'=>$live_url,
|
|
||||||
'data'=>$data,
|
|
||||||
'source'=>'源码地址:https://github.com/ihmily/DouyinLiveRecorder'
|
|
||||||
];
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function get_kuaishou_stream_url($live_url){
|
|
||||||
$headers = array(
|
|
||||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
|
||||||
'Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
|
|
||||||
|
|
||||||
);
|
|
||||||
|
|
||||||
$cookies = 'your cookie'; # 任意快手直播间页面的cookie
|
|
||||||
|
|
||||||
$html_str = get_curl($live_url,$headers,$cookies);
|
|
||||||
preg_match('/__INITIAL_STATE__=(.*?);\(function/', $html_str, $matches);
|
|
||||||
$json_data = json_decode($matches[1], true);
|
|
||||||
|
|
||||||
$play_list= $json_data['liveroom']['playList'][0];
|
|
||||||
$live_title=$play_list['liveStream']['caption'];
|
|
||||||
$anchor_name = $play_list['author']['name'];
|
|
||||||
# 获取直播间状态
|
|
||||||
$status = $play_list['isLiving']; # 直播状态True是正在直播.False是未开播
|
|
||||||
if (!$status) {
|
|
||||||
$data=["live_status"=>'主播未开播或者直播已经结束!'];
|
|
||||||
}else{
|
|
||||||
$stream_data=$play_list['liveStream']['playUrls'][0]['adaptationSet']['representation'];
|
|
||||||
$data=["title"=>$live_title,'stream'=>$stream_data];
|
|
||||||
}
|
|
||||||
|
|
||||||
$return=[
|
|
||||||
'code'=>0,
|
|
||||||
'platform'=>'快手直播',
|
|
||||||
'status'=>'解析成功',
|
|
||||||
'anchor_name'=>$anchor_name,
|
|
||||||
'live_url'=>$live_url,
|
|
||||||
'data'=>$data,
|
|
||||||
'source'=>'源码地址:https://github.com/ihmily/DouyinLiveRecorder'
|
|
||||||
];
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function get_huya_stream_url($live_url){
|
|
||||||
$headers = array(
|
|
||||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
|
||||||
'Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2'
|
|
||||||
);
|
|
||||||
$html_str = get_curl($live_url,$headers);
|
|
||||||
|
|
||||||
preg_match('/stream: (\{"data".*?),"iWebDefaultBitRate"/', $html_str, $matches);
|
|
||||||
$json_data = json_decode($matches[1].'}', true);
|
|
||||||
$gameLiveInfo = $json_data['data'][0]['gameLiveInfo'];
|
|
||||||
$live_title=$gameLiveInfo['introduction'];
|
|
||||||
$gameStreamInfoList = $json_data['data'][0]['gameStreamInfoList'];
|
|
||||||
|
|
||||||
$anchor_name = $gameLiveInfo['nick'];
|
|
||||||
if (count($gameStreamInfoList)==0) {
|
|
||||||
$data=["live_status"=>'主播未开播或者直播已经结束!'];
|
|
||||||
}else{
|
|
||||||
# gameStreamInfoList 索引从小到大 分别是'al', 'tx', 'hw', 'hs'四种cdn线路
|
|
||||||
# 默认使用第二种 即host链接开头为tx的cdn
|
|
||||||
$sFlvUrl = $gameStreamInfoList[1]['sFlvUrl'];
|
|
||||||
$sStreamName = $gameStreamInfoList[1]['sStreamName'];
|
|
||||||
$sFlvUrlSuffix = $gameStreamInfoList[1]['sFlvUrlSuffix'];
|
|
||||||
$sHlsUrl = $gameStreamInfoList[1]['sHlsUrl'];
|
|
||||||
$sHlsUrlSuffix = $gameStreamInfoList[1]['sHlsUrlSuffix'];
|
|
||||||
$sFlvAntiCode = $gameStreamInfoList[1]['sFlvAntiCode'];
|
|
||||||
$quality_list = explode('&exsphd=', $sFlvAntiCode)[1];
|
|
||||||
$pattern = "/(?<=264_)\d+/";
|
|
||||||
$matches = [];
|
|
||||||
preg_match_all($pattern, $quality_list, $matches);
|
|
||||||
$quality_list = $matches[0];
|
|
||||||
$quality_list = array_reverse($quality_list);
|
|
||||||
$m3u8_list=[];
|
|
||||||
$flv_list=[];
|
|
||||||
foreach ($quality_list as $quality){
|
|
||||||
$flv_url = "{$sFlvUrl}/{$sStreamName}.{$sFlvUrlSuffix}?{$sFlvAntiCode}&ratio={$quality}";
|
|
||||||
$m3u8_url = "{$sHlsUrl}/{$sStreamName}.{$sHlsUrlSuffix}?{$sFlvAntiCode}&ratio={$quality}";
|
|
||||||
array_push($m3u8_list,$m3u8_url);
|
|
||||||
array_push($flv_list,$flv_url);
|
|
||||||
}
|
|
||||||
$data=["title"=>$live_title,'stream'=>['flv_url'=>$flv_list,'m3u8_url'=>$m3u8_list]];
|
|
||||||
}
|
|
||||||
$return=[
|
|
||||||
'code'=>0,
|
|
||||||
'platform'=>'虎牙直播',
|
|
||||||
'status'=>'解析成功',
|
|
||||||
'anchor_name'=>$anchor_name,
|
|
||||||
'live_url'=>$live_url,
|
|
||||||
'data'=>$data,
|
|
||||||
'source'=>'源码地址:https://github.com/ihmily/DouyinLiveRecorder'
|
|
||||||
];
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 封装的CURL函数
|
|
||||||
function get_curl($url,$headers=array(),$cookies=''){
|
|
||||||
$curl=curl_init((string)$url);
|
|
||||||
curl_setopt($curl,CURLOPT_HEADER,false);
|
|
||||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
|
||||||
curl_setopt($curl,CURLOPT_SSL_VERIFYPEER,false);
|
|
||||||
curl_setopt($curl, CURLOPT_ENCODING, "");
|
|
||||||
curl_setopt($curl,CURLOPT_RETURNTRANSFER,true);
|
|
||||||
curl_setopt($curl,CURLOPT_HTTPHEADER,$headers);
|
|
||||||
curl_setopt($curl, CURLOPT_COOKIE, $cookies);
|
|
||||||
curl_setopt($curl,CURLOPT_TIMEOUT,20);
|
|
||||||
$data = curl_exec($curl);
|
|
||||||
// var_dump($data);
|
|
||||||
curl_close($curl);
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,11 +1,15 @@
|
|||||||
[录制设置]
|
[录制设置]
|
||||||
是否跳过代理检测(是/否) = 否
|
language(zh_cn/en) = zh_cn
|
||||||
直播保存路径(不填则默认) =
|
是否跳过代理检测(是/否) = 否
|
||||||
|
直播保存路径(不填则默认) =
|
||||||
保存文件夹是否以作者区分 = 是
|
保存文件夹是否以作者区分 = 是
|
||||||
保存文件夹是否以时间区分 = 否
|
保存文件夹是否以时间区分 = 否
|
||||||
|
保存文件夹是否以标题区分 = 否
|
||||||
|
保存文件名是否包含标题 = 否
|
||||||
|
是否去除名称中的表情符号 = 是
|
||||||
视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频 = ts
|
视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频 = ts
|
||||||
原画|超清|高清|标清|流畅 = 原画
|
原画|超清|高清|标清|流畅 = 原画
|
||||||
是否使用代理ip(是/否) = 是
|
是否使用代理ip(是/否) = 是
|
||||||
代理地址 =
|
代理地址 =
|
||||||
同一时间访问网络的线程数 = 3
|
同一时间访问网络的线程数 = 3
|
||||||
循环时间(秒) = 300
|
循环时间(秒) = 300
|
||||||
@ -13,82 +17,115 @@
|
|||||||
是否显示循环秒数 = 否
|
是否显示循环秒数 = 否
|
||||||
是否显示直播源地址 = 否
|
是否显示直播源地址 = 否
|
||||||
分段录制是否开启 = 是
|
分段录制是否开启 = 是
|
||||||
|
是否强制启用https录制 = 否
|
||||||
|
录制空间剩余阈值(gb) = 1.0
|
||||||
视频分段时间(秒) = 1800
|
视频分段时间(秒) = 1800
|
||||||
ts录制完成后自动转为mp4格式 = 是
|
录制完成后自动转为mp4格式 = 是
|
||||||
|
mp4格式重新编码为h264 = 否
|
||||||
追加格式后删除原文件 = 是
|
追加格式后删除原文件 = 是
|
||||||
生成时间字幕文件 = 否
|
生成时间字幕文件 = 否
|
||||||
是否录制完成后执行bash脚本 = 否
|
是否录制完成后执行自定义脚本 = 否
|
||||||
bash脚本路径 =
|
自定义脚本执行命令 =
|
||||||
使用代理录制的平台(逗号分隔) = tiktok, afreecatv, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk
|
使用代理录制的平台(逗号分隔) = tiktok, sooplive, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu
|
||||||
额外使用代理录制的平台(逗号分隔) =
|
额外使用代理录制的平台(逗号分隔) =
|
||||||
|
|
||||||
[推送配置]
|
[推送配置]
|
||||||
直播状态通知(可选微信|钉钉|tg|邮箱|bark或者都填) =
|
# 可选微信|钉钉|tg|邮箱|bark|ntfy|pushplus 可填多个
|
||||||
钉钉推送接口链接 =
|
直播状态推送渠道 =
|
||||||
微信推送接口链接 =
|
钉钉推送接口链接 =
|
||||||
bark推送接口链接 =
|
微信推送接口链接 =
|
||||||
|
bark推送接口链接 =
|
||||||
bark推送中断级别 = active
|
bark推送中断级别 = active
|
||||||
bark推送铃声 =
|
bark推送铃声 =
|
||||||
钉钉通知@对象(填手机号) =
|
钉钉通知@对象(填手机号) =
|
||||||
tgapi令牌 =
|
钉钉通知@全体(是/否) = 否
|
||||||
tg聊天id(个人或者群组id) =
|
tgapi令牌 =
|
||||||
smtp邮件服务器 =
|
tg聊天id(个人或者群组id) =
|
||||||
发件人邮箱 =
|
smtp邮件服务器 =
|
||||||
发件人密码(授权码) =
|
是否使用SMTP服务SSL加密(是/否) =
|
||||||
收件人邮箱 =
|
SMTP邮件服务器端口 =
|
||||||
自定义开播推送内容 =
|
邮箱登录账号 =
|
||||||
自定义关播推送内容 =
|
发件人密码(授权码) =
|
||||||
只推送通知不录制(是/否) = 否
|
发件人邮箱 =
|
||||||
直播推送检测频率(秒) = 1800
|
发件人显示昵称 =
|
||||||
开播推送开启(是/否)= 是
|
收件人邮箱 =
|
||||||
关播推送开启(是/否)= 否
|
ntfy推送地址 = https://ntfy.sh/xxxx
|
||||||
|
ntfy推送标签 = tada
|
||||||
|
ntfy推送邮箱 =
|
||||||
|
pushplus推送token =
|
||||||
|
自定义推送标题 =
|
||||||
|
自定义开播推送内容 =
|
||||||
|
自定义关播推送内容 =
|
||||||
|
只推送通知不录制(是/否) = 否
|
||||||
|
直播推送检测频率(秒) = 1800
|
||||||
|
开播推送开启(是/否) = 是
|
||||||
|
关播推送开启(是/否)= 否
|
||||||
|
|
||||||
[Cookie]
|
[Cookie]
|
||||||
抖音cookie(录制抖音必须要有) = ttwid=1%7CB1qls3GdnZhUov9o2NxOMxxYS2ff6OSvEWbv0ytbES4%7C1680522049%7C280d802d6d478e3e78d0c807f7c487e7ffec0ae4e5fdd6a0fe74c3c6af149511; my_rd=1; passport_csrf_token=3ab34460fa656183fccfb904b16ff742; passport_csrf_token_default=3ab34460fa656183fccfb904b16ff742; d_ticket=9f562383ac0547d0b561904513229d76c9c21; n_mh=hvnJEQ4Q5eiH74-84kTFUyv4VK8xtSrpRZG1AhCeFNI; store-region=cn-fj; store-region-src=uid; LOGIN_STATUS=1; __security_server_data_status=1; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; pwa2=%223%7C0%7C3%7C0%22; download_guide=%223%2F20230729%2F0%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.6%7D; strategyABtestKey=%221690824679.923%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1536%2C%5C%22screen_height%5C%22%3A864%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A8%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A150%7D%22; VIDEO_FILTER_MEMO_SELECT=%7B%22expireTime%22%3A1691443863751%2C%22type%22%3Anull%7D; home_can_add_dy_2_desktop=%221%22; __live_version__=%221.1.1.2169%22; device_web_cpu_core=8; device_web_memory_size=8; xgplayer_user_id=346045893336; csrf_session_id=2e00356b5cd8544d17a0e66484946f28; odin_tt=724eb4dd23bc6ffaed9a1571ac4c757ef597768a70c75fef695b95845b7ffcd8b1524278c2ac31c2587996d058e03414595f0a4e856c53bd0d5e5f56dc6d82e24004dc77773e6b83ced6f80f1bb70627; __ac_nonce=064caded4009deafd8b89; __ac_signature=_02B4Z6wo00f01HLUuwwAAIDBh6tRkVLvBQBy9L-AAHiHf7; ttcid=2e9619ebbb8449eaa3d5a42d8ce88ec835; webcast_leading_last_show_time=1691016922379; webcast_leading_total_show_times=1; webcast_local_quality=sd; live_can_add_dy_2_desktop=%221%22; msToken=1JDHnVPw_9yTvzIrwb7cQj8dCMNOoesXbA_IooV8cezcOdpe4pzusZE7NB7tZn9TBXPr0ylxmv-KMs5rqbNUBHP4P7VBFUu0ZAht_BEylqrLpzgt3y5ne_38hXDOX8o=; msToken=jV_yeN1IQKUd9PlNtpL7k5vthGKcHo0dEh_QPUQhr8G3cuYv-Jbb4NnIxGDmhVOkZOCSihNpA2kvYtHiTW25XNNX_yrsv5FN8O6zm3qmCIXcEe0LywLn7oBO2gITEeg=; tt_scid=mYfqpfbDjqXrIGJuQ7q-DlQJfUSG51qG.KUdzztuGP83OjuVLXnQHjsz-BRHRJu4e986
|
# 录制抖音必填
|
||||||
快手cookie =
|
抖音cookie = ttwid=1%7CB1qls3GdnZhUov9o2NxOMxxYS2ff6OSvEWbv0ytbES4%7C1680522049%7C280d802d6d478e3e78d0c807f7c487e7ffec0ae4e5fdd6a0fe74c3c6af149511; my_rd=1; passport_csrf_token=3ab34460fa656183fccfb904b16ff742; passport_csrf_token_default=3ab34460fa656183fccfb904b16ff742; d_ticket=9f562383ac0547d0b561904513229d76c9c21; n_mh=hvnJEQ4Q5eiH74-84kTFUyv4VK8xtSrpRZG1AhCeFNI; store-region=cn-fj; store-region-src=uid; LOGIN_STATUS=1; __security_server_data_status=1; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; pwa2=%223%7C0%7C3%7C0%22; download_guide=%223%2F20230729%2F0%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.6%7D; strategyABtestKey=%221690824679.923%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1536%2C%5C%22screen_height%5C%22%3A864%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A8%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A150%7D%22; VIDEO_FILTER_MEMO_SELECT=%7B%22expireTime%22%3A1691443863751%2C%22type%22%3Anull%7D; home_can_add_dy_2_desktop=%221%22; __live_version__=%221.1.1.2169%22; device_web_cpu_core=8; device_web_memory_size=8; xgplayer_user_id=346045893336; csrf_session_id=2e00356b5cd8544d17a0e66484946f28; odin_tt=724eb4dd23bc6ffaed9a1571ac4c757ef597768a70c75fef695b95845b7ffcd8b1524278c2ac31c2587996d058e03414595f0a4e856c53bd0d5e5f56dc6d82e24004dc77773e6b83ced6f80f1bb70627; __ac_nonce=064caded4009deafd8b89; __ac_signature=_02B4Z6wo00f01HLUuwwAAIDBh6tRkVLvBQBy9L-AAHiHf7; ttcid=2e9619ebbb8449eaa3d5a42d8ce88ec835; webcast_leading_last_show_time=1691016922379; webcast_leading_total_show_times=1; webcast_local_quality=sd; live_can_add_dy_2_desktop=%221%22; msToken=1JDHnVPw_9yTvzIrwb7cQj8dCMNOoesXbA_IooV8cezcOdpe4pzusZE7NB7tZn9TBXPr0ylxmv-KMs5rqbNUBHP4P7VBFUu0ZAht_BEylqrLpzgt3y5ne_38hXDOX8o=; msToken=jV_yeN1IQKUd9PlNtpL7k5vthGKcHo0dEh_QPUQhr8G3cuYv-Jbb4NnIxGDmhVOkZOCSihNpA2kvYtHiTW25XNNX_yrsv5FN8O6zm3qmCIXcEe0LywLn7oBO2gITEeg=; tt_scid=mYfqpfbDjqXrIGJuQ7q-DlQJfUSG51qG.KUdzztuGP83OjuVLXnQHjsz-BRHRJu4e986
|
||||||
tiktok_cookie =
|
快手cookie =
|
||||||
虎牙cookie =
|
tiktok_cookie =
|
||||||
斗鱼cookie =
|
虎牙cookie =
|
||||||
yy_cookie =
|
斗鱼cookie =
|
||||||
b站cookie =
|
yy_cookie =
|
||||||
小红书cookie =
|
b站cookie =
|
||||||
bigo_cookie =
|
小红书cookie =
|
||||||
blued_cookie =
|
bigo_cookie =
|
||||||
afreecatv_cookie =
|
blued_cookie =
|
||||||
netease_cookie =
|
sooplive_cookie =
|
||||||
千度热播_cookie =
|
netease_cookie =
|
||||||
pandatv_cookie =
|
千度热播_cookie =
|
||||||
猫耳fm_cookie =
|
pandatv_cookie =
|
||||||
winktv_cookie =
|
猫耳fm_cookie =
|
||||||
flextv_cookie =
|
winktv_cookie =
|
||||||
look_cookie =
|
flextv_cookie =
|
||||||
twitcasting_cookie =
|
look_cookie =
|
||||||
baidu_cookie =
|
twitcasting_cookie =
|
||||||
weibo_cookie =
|
baidu_cookie =
|
||||||
kugou_cookie =
|
weibo_cookie =
|
||||||
twitch_cookie =
|
kugou_cookie =
|
||||||
liveme_cookie =
|
twitch_cookie =
|
||||||
huajiao_cookie =
|
liveme_cookie =
|
||||||
liuxing_cookie =
|
huajiao_cookie =
|
||||||
showroom_cookie =
|
liuxing_cookie =
|
||||||
acfun_cookie =
|
showroom_cookie =
|
||||||
shiguang_cookie =
|
acfun_cookie =
|
||||||
|
changliao_cookie =
|
||||||
yinbo_cookie =
|
yinbo_cookie =
|
||||||
yingke_cookie =
|
yingke_cookie =
|
||||||
zhihu_cookie =
|
zhihu_cookie =
|
||||||
chzzk_cookie =
|
chzzk_cookie =
|
||||||
|
haixiu_cookie =
|
||||||
|
vvxqiu_cookie =
|
||||||
|
17live_cookie =
|
||||||
|
langlive_cookie =
|
||||||
|
pplive_cookie =
|
||||||
|
6room_cookie =
|
||||||
|
lehaitv_cookie =
|
||||||
|
huamao_cookie =
|
||||||
|
shopee_cookie =
|
||||||
|
youtube_cookie =
|
||||||
|
taobao_cookie =
|
||||||
|
jd_cookie =
|
||||||
|
faceit_cookie =
|
||||||
|
migu_cookie =
|
||||||
|
lianjie_cookie =
|
||||||
|
laixiu_cookie =
|
||||||
|
picarto_cookie =
|
||||||
|
|
||||||
|
|
||||||
[Authorization]
|
[Authorization]
|
||||||
popkontv_token =
|
popkontv_token =
|
||||||
|
|
||||||
[账号密码]
|
[账号密码]
|
||||||
afreecatv账号 =
|
sooplive账号 =
|
||||||
afreecatv密码 =
|
sooplive密码 =
|
||||||
flextv账号 =
|
flextv账号 =
|
||||||
flextv密码 =
|
flextv密码 =
|
||||||
popkontv账号 =
|
popkontv账号 =
|
||||||
partner_code = P-00001
|
partner_code = P-00001
|
||||||
popkontv密码 =
|
popkontv密码 =
|
||||||
twitcasting账号类型 = normal
|
twitcasting账号类型 = normal
|
||||||
twitcasting账号 =
|
twitcasting账号 =
|
||||||
twitcasting密码 =
|
twitcasting密码 =
|
||||||
111
demo.py
111
demo.py
@ -1,12 +1,14 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from loguru import logger
|
import asyncio
|
||||||
from douyinliverecorder import spider
|
from src.logger import logger
|
||||||
|
from src import spider
|
||||||
|
|
||||||
# 以下示例直播间链接不保证时效性,请自行查看链接是否能正常访问
|
# 以下示例直播间链接不保证时效性,请自行查看链接是否能正常访问
|
||||||
|
# Please note that the following example live room links may not be up-to-date
|
||||||
LIVE_STREAM_CONFIG = {
|
LIVE_STREAM_CONFIG = {
|
||||||
"douyin": {
|
"douyin": {
|
||||||
"url": "https://live.douyin.com/745964462470",
|
"url": "https://live.douyin.com/745964462470",
|
||||||
"func": spider.get_douyin_stream_data,
|
"func": spider.get_douyin_app_stream_data,
|
||||||
},
|
},
|
||||||
"tiktok": {
|
"tiktok": {
|
||||||
"url": "https://www.tiktok.com/@pearlgaga88/live",
|
"url": "https://www.tiktok.com/@pearlgaga88/live",
|
||||||
@ -14,11 +16,11 @@ LIVE_STREAM_CONFIG = {
|
|||||||
},
|
},
|
||||||
"kuaishou": {
|
"kuaishou": {
|
||||||
"url": "https://live.kuaishou.com/u/yall1102",
|
"url": "https://live.kuaishou.com/u/yall1102",
|
||||||
"func": spider.get_kuaishou_stream_data2,
|
"func": spider.get_kuaishou_stream_data,
|
||||||
},
|
},
|
||||||
"huya": {
|
"huya": {
|
||||||
"url": "https://www.huya.com/116",
|
"url": "https://www.huya.com/116",
|
||||||
"func": spider.get_huya_stream_data,
|
"func": spider.get_huya_app_stream_url,
|
||||||
},
|
},
|
||||||
"douyu": {
|
"douyu": {
|
||||||
"url": "https://www.douyu.com/topic/wzDBLS6?rid=4921614&dyshid=",
|
"url": "https://www.douyu.com/topic/wzDBLS6?rid=4921614&dyshid=",
|
||||||
@ -33,7 +35,7 @@ LIVE_STREAM_CONFIG = {
|
|||||||
"func": spider.get_bilibili_stream_data,
|
"func": spider.get_bilibili_stream_data,
|
||||||
},
|
},
|
||||||
"xhs": {
|
"xhs": {
|
||||||
"url": "http://xhslink.com/O9f9fM",
|
"url": "https://www.xiaohongshu.com/user/profile/6330049c000000002303c7ed?appuid=5f3f478a00000000010005b3",
|
||||||
"func": spider.get_xhs_stream_url,
|
"func": spider.get_xhs_stream_url,
|
||||||
},
|
},
|
||||||
"bigo": {
|
"bigo": {
|
||||||
@ -44,9 +46,9 @@ LIVE_STREAM_CONFIG = {
|
|||||||
"url": "https://app.blued.cn/live?id=Mp6G2R",
|
"url": "https://app.blued.cn/live?id=Mp6G2R",
|
||||||
"func": spider.get_blued_stream_url,
|
"func": spider.get_blued_stream_url,
|
||||||
},
|
},
|
||||||
"afreecatv": {
|
"sooplive": {
|
||||||
"url": "https://play.afreecatv.com/sw7love",
|
"url": "https://play.sooplive.co.kr/sw7love",
|
||||||
"func": spider.get_afreecatv_stream_data,
|
"func": spider.get_sooplive_stream_data,
|
||||||
},
|
},
|
||||||
"netease": {
|
"netease": {
|
||||||
"url": "https://cc.163.com/583946984",
|
"url": "https://cc.163.com/583946984",
|
||||||
@ -69,7 +71,7 @@ LIVE_STREAM_CONFIG = {
|
|||||||
"func": spider.get_winktv_stream_data,
|
"func": spider.get_winktv_stream_data,
|
||||||
},
|
},
|
||||||
"flextv": {
|
"flextv": {
|
||||||
"url": "https://www.flextv.co.kr/channels/593127/live",
|
"url": "https://www.ttinglive.com/channels/685479/live",
|
||||||
"func": spider.get_flextv_stream_data,
|
"func": spider.get_flextv_stream_data,
|
||||||
},
|
},
|
||||||
"looklive": {
|
"looklive": {
|
||||||
@ -105,13 +107,9 @@ LIVE_STREAM_CONFIG = {
|
|||||||
"func": spider.get_liveme_stream_url,
|
"func": spider.get_liveme_stream_url,
|
||||||
},
|
},
|
||||||
"huajiao": {
|
"huajiao": {
|
||||||
"url": "https://www.huajiao.com/user/223184650",
|
"url": "https://www.huajiao.com/user/207446325",
|
||||||
"func": spider.get_huajiao_stream_url,
|
"func": spider.get_huajiao_stream_url,
|
||||||
},
|
},
|
||||||
"liuxing": {
|
|
||||||
"url": "https://www.7u66.com/100960",
|
|
||||||
"func": spider.get_liuxing_stream_url,
|
|
||||||
},
|
|
||||||
"showroom": {
|
"showroom": {
|
||||||
"url": "https://www.showroom-live.com/room/profile?room_id=511033",
|
"url": "https://www.showroom-live.com/room/profile?room_id=511033",
|
||||||
"func": spider.get_showroom_stream_data,
|
"func": spider.get_showroom_stream_data,
|
||||||
@ -120,34 +118,103 @@ LIVE_STREAM_CONFIG = {
|
|||||||
"url": "https://live.acfun.cn/live/17912421",
|
"url": "https://live.acfun.cn/live/17912421",
|
||||||
"func": spider.get_acfun_stream_data,
|
"func": spider.get_acfun_stream_data,
|
||||||
},
|
},
|
||||||
"shiguang": {
|
"changliao": {
|
||||||
"url": "https://www.rengzu.com/180778",
|
"url": "https://www.tlclw.com/801044397",
|
||||||
"func": spider.get_shiguang_stream_url,
|
"func": spider.get_changliao_stream_url,
|
||||||
},
|
},
|
||||||
"yingke": {
|
"yingke": {
|
||||||
"url": "https://www.inke.cn/liveroom/index.html?uid=710032101&id=1720857535354099",
|
"url": "https://www.inke.cn/liveroom/index.html?uid=710032101&id=1720857535354099",
|
||||||
"func": spider.get_yingke_stream_url,
|
"func": spider.get_yingke_stream_url,
|
||||||
},
|
},
|
||||||
"yinbo": {
|
"yinbo": {
|
||||||
"url": "https://live.ybw1666.com/800002949",
|
"url": "https://live.ybw1666.com/800008687",
|
||||||
"func": spider.get_yinbo_stream_url,
|
"func": spider.get_yinbo_stream_url,
|
||||||
},
|
},
|
||||||
"zhihu": {
|
"zhihu": {
|
||||||
"url": "https://www.zhihu.com/theater/114453",
|
"url": "https://www.zhihu.com/people/ac3a467005c5d20381a82230101308e9",
|
||||||
"func": spider.get_zhihu_stream_url,
|
"func": spider.get_zhihu_stream_url,
|
||||||
},
|
},
|
||||||
"chzzk": {
|
"chzzk": {
|
||||||
"url": "https://chzzk.naver.com/live/458f6ec20b034f49e0fc6d03921646d2",
|
"url": "https://chzzk.naver.com/live/458f6ec20b034f49e0fc6d03921646d2",
|
||||||
"func": spider.get_chzzk_stream_data,
|
"func": spider.get_chzzk_stream_data,
|
||||||
|
},
|
||||||
|
"haixiu": {
|
||||||
|
"url": "https://www.haixiutv.com/6095106",
|
||||||
|
"func": spider.get_haixiu_stream_url,
|
||||||
|
},
|
||||||
|
"vvxqiu": {
|
||||||
|
"url": "https://h5webcdnp.vvxqiu.com//activity/videoShare/videoShare.html?h5Server=https://h5p.vvxqiu.com&"
|
||||||
|
"roomId=LP115664695&platformId=vvstar",
|
||||||
|
"func": spider.get_vvxqiu_stream_url,
|
||||||
|
},
|
||||||
|
"17live": {
|
||||||
|
"url": "https://17.live/en/live/6302408",
|
||||||
|
"func": spider.get_17live_stream_url,
|
||||||
|
},
|
||||||
|
"langlive": {
|
||||||
|
"url": "https://www.lang.live/en-US/room/3349463",
|
||||||
|
"func": spider.get_langlive_stream_url,
|
||||||
|
},
|
||||||
|
"pplive": {
|
||||||
|
"url": "https://m.pp.weimipopo.com/live/preview.html?uid=91648673&anchorUid=91625862&app=plpl",
|
||||||
|
"func": spider.get_pplive_stream_url,
|
||||||
|
},
|
||||||
|
"6room": {
|
||||||
|
"url": "https://v.6.cn/634435",
|
||||||
|
"func": spider.get_6room_stream_url,
|
||||||
|
},
|
||||||
|
"lehai": {
|
||||||
|
"url": "https://www.lehaitv.com/8059096",
|
||||||
|
"func": spider.get_haixiu_stream_url,
|
||||||
|
},
|
||||||
|
"huamao": {
|
||||||
|
"url": "https://h.catshow168.com/live/preview.html?uid=19066357&anchorUid=18895331",
|
||||||
|
"func": spider.get_pplive_stream_url,
|
||||||
|
},
|
||||||
|
"shopee": {
|
||||||
|
"url": "https://sg.shp.ee/GmpXeuf?uid=1006401066&session=802458",
|
||||||
|
"func": spider.get_shopee_stream_url,
|
||||||
|
},
|
||||||
|
"youtube": {
|
||||||
|
"url": "https://www.youtube.com/watch?v=cS6zS5hi1w0",
|
||||||
|
"func": spider.get_youtube_stream_url,
|
||||||
|
},
|
||||||
|
"taobao": {
|
||||||
|
"url": "https://m.tb.cn/h.TWp0HTd",
|
||||||
|
"func": spider.get_taobao_stream_url,
|
||||||
|
},
|
||||||
|
"jd": {
|
||||||
|
"url": "https://3.cn/28MLBy-E",
|
||||||
|
"func": spider.get_jd_stream_url,
|
||||||
|
},
|
||||||
|
"faceit": {
|
||||||
|
"url": "https://www.faceit.com/zh/players/Compl1/stream",
|
||||||
|
"func": spider.get_faceit_stream_data,
|
||||||
|
},
|
||||||
|
"lianjie": {
|
||||||
|
"url": "https://show.lailianjie.com/10000258",
|
||||||
|
"func": spider.get_lianjie_stream_url,
|
||||||
|
},
|
||||||
|
"migu": {
|
||||||
|
"url": "https://www.miguvideo.com/p/live/120000541321",
|
||||||
|
"func": spider.get_migu_stream_url,
|
||||||
|
},
|
||||||
|
"laixiu": {
|
||||||
|
"url": "https://www.imkktv.com/h5/share/video.html?uid=1845195&roomId=1710496",
|
||||||
|
"func": spider.get_laixiu_stream_url,
|
||||||
|
},
|
||||||
|
"picarto": {
|
||||||
|
"url": "https://www.picarto.tv/cuteavalanche",
|
||||||
|
"func": spider.get_picarto_stream_url,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_live_stream(platform_name: str) -> None:
|
def test_live_stream(platform_name: str, proxy_addr=None, cookies=None) -> None:
|
||||||
if platform_name in LIVE_STREAM_CONFIG:
|
if platform_name in LIVE_STREAM_CONFIG:
|
||||||
config = LIVE_STREAM_CONFIG[platform_name]
|
config = LIVE_STREAM_CONFIG[platform_name]
|
||||||
try:
|
try:
|
||||||
stream_data = config['func'](config['url'], proxy_addr='')
|
stream_data = asyncio.run(config['func'](config['url'], proxy_addr=proxy_addr, cookies=cookies))
|
||||||
logger.debug(f"Stream data for {platform_name}: {stream_data}")
|
logger.debug(f"Stream data for {platform_name}: {stream_data}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching stream data for {platform_name}: {e}")
|
logger.error(f"Error fetching stream data for {platform_name}: {e}")
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
script_path = os.path.split(os.path.realpath(sys.argv[0]))[0]
|
|
||||||
logger.add(
|
|
||||||
f"{script_path}/logs/PlayURL.log",
|
|
||||||
level="INFO",
|
|
||||||
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {message}",
|
|
||||||
filter=lambda i: i["level"].name == "INFO",
|
|
||||||
serialize=False,
|
|
||||||
enqueue=True,
|
|
||||||
retention=1,
|
|
||||||
rotation="300 KB",
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.add(
|
|
||||||
f"{script_path}/logs/DouyinLiveRecorder.log",
|
|
||||||
level="WARNING",
|
|
||||||
serialize=False,
|
|
||||||
enqueue=True,
|
|
||||||
retention=1,
|
|
||||||
rotation="100 KB",
|
|
||||||
)
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
Author: Hmily
|
|
||||||
GitHub: https://github.com/ihmily
|
|
||||||
Date: 2023-09-03 19:18:36
|
|
||||||
Update: 2024-09-24 20:43:12
|
|
||||||
Copyright (c) 2023-2024 by Hmily, All Rights Reserved.
|
|
||||||
"""
|
|
||||||
from typing import Dict, Any, Optional
|
|
||||||
import json
|
|
||||||
import urllib.request
|
|
||||||
from .utils import trace_error_decorator
|
|
||||||
import smtplib
|
|
||||||
from email.header import Header
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
|
|
||||||
no_proxy_handler = urllib.request.ProxyHandler({})
|
|
||||||
opener = urllib.request.build_opener(no_proxy_handler)
|
|
||||||
headers: Dict[str, str] = {'Content-Type': 'application/json'}
|
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
|
||||||
def dingtalk(url: str, content: str, number: Optional[str] = '') -> Dict[str, Any]:
|
|
||||||
json_data = {
|
|
||||||
'msgtype': 'text',
|
|
||||||
'text': {
|
|
||||||
'content': content,
|
|
||||||
},
|
|
||||||
"at": {
|
|
||||||
"atMobiles": [
|
|
||||||
number # 添加这个手机号,可以被@通知(必须要在群里)
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
data = json.dumps(json_data).encode('utf-8')
|
|
||||||
req = urllib.request.Request(url, data=data, headers=headers)
|
|
||||||
response = opener.open(req, timeout=10)
|
|
||||||
json_str = response.read().decode('utf-8')
|
|
||||||
json_data = json.loads(json_str)
|
|
||||||
return json_data
|
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
|
||||||
def xizhi(url: str, content: str, title: str = '直播间状态更新') -> Dict[str, Any]:
|
|
||||||
json_data = {
|
|
||||||
'title': title,
|
|
||||||
'content': content
|
|
||||||
}
|
|
||||||
data = json.dumps(json_data).encode('utf-8')
|
|
||||||
req = urllib.request.Request(url, data=data, headers=headers)
|
|
||||||
response = opener.open(req, timeout=10)
|
|
||||||
json_str = response.read().decode('utf-8')
|
|
||||||
json_data = json.loads(json_str)
|
|
||||||
return json_data
|
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
|
||||||
def email_message(mail_host: str, mail_pass: str, from_email: str, to_email: str, title: str, content: str) -> Dict[
|
|
||||||
str, Any]:
|
|
||||||
message = MIMEMultipart()
|
|
||||||
message['From'] = "{}".format(from_email)
|
|
||||||
message['Subject'] = Header(title, 'utf-8')
|
|
||||||
receivers = to_email.replace(',', ',').split(',')
|
|
||||||
if len(receivers) == 1:
|
|
||||||
message['To'] = receivers[0]
|
|
||||||
|
|
||||||
t_apart = MIMEText(content, 'plain', 'utf-8')
|
|
||||||
message.attach(t_apart)
|
|
||||||
|
|
||||||
try:
|
|
||||||
smtp_obj = smtplib.SMTP_SSL(mail_host, 465)
|
|
||||||
smtp_obj.login(from_email, mail_pass)
|
|
||||||
smtp_obj.sendmail(from_email, receivers, message.as_string())
|
|
||||||
data = {'code': 200, 'msg': '邮件发送成功'}
|
|
||||||
return data
|
|
||||||
except smtplib.SMTPException as e:
|
|
||||||
raise smtplib.SMTPException(f'邮件发送失败 {e}')
|
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
|
||||||
def tg_bot(chat_id: int, token: str, content: str) -> Dict[str, Any]:
|
|
||||||
json_data = {
|
|
||||||
"chat_id": chat_id,
|
|
||||||
'text': content
|
|
||||||
}
|
|
||||||
url = f'https://api.telegram.org/bot{token}/sendMessage'
|
|
||||||
data = json.dumps(json_data).encode('utf-8')
|
|
||||||
req = urllib.request.Request(url, data=data, headers=headers)
|
|
||||||
response = urllib.request.urlopen(req, timeout=15)
|
|
||||||
json_str = response.read().decode('utf-8')
|
|
||||||
json_data = json.loads(json_str)
|
|
||||||
return json_data
|
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
|
||||||
def bark(api: str, title: str = "message", content: str = 'test', level: str = "active",
|
|
||||||
badge: int = 1, auto_copy: int = 1, sound: str = "", icon: str = "", group: str = "",
|
|
||||||
is_archive: int = 1, url: str = "") -> Dict[str, Any]:
|
|
||||||
json_data = {
|
|
||||||
"title": title,
|
|
||||||
"body": content,
|
|
||||||
"level": level,
|
|
||||||
"badge": badge,
|
|
||||||
"autoCopy": auto_copy,
|
|
||||||
"sound": sound,
|
|
||||||
"icon": icon,
|
|
||||||
"group": group,
|
|
||||||
"isArchive": is_archive,
|
|
||||||
"url": url
|
|
||||||
}
|
|
||||||
|
|
||||||
data = json.dumps(json_data).encode('utf-8')
|
|
||||||
req = urllib.request.Request(api, data=data, headers=headers)
|
|
||||||
response = opener.open(req, timeout=10)
|
|
||||||
json_str = response.read().decode("utf-8")
|
|
||||||
json_data = json.loads(json_str)
|
|
||||||
return json_data
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
send_title = '直播通知' # 标题
|
|
||||||
send_content = '张三 开播了!' # 推送内容
|
|
||||||
|
|
||||||
# 钉钉推送通知
|
|
||||||
webhook_api = '' # 替换成自己Webhook链接,参考文档:https://open.dingtalk.com/document/robots/custom-robot-access
|
|
||||||
phone_number = '' # 被@用户的手机号码
|
|
||||||
# dingtalk(webhook_api, send_content, phone_number)
|
|
||||||
|
|
||||||
# 微信推送通知
|
|
||||||
# 替换成自己的单点推送接口,获取地址:https://xz.qqoq.net/#/admin/one
|
|
||||||
# 当然也可以使用其他平台API 如server酱 使用方法一样
|
|
||||||
xizhi_api = 'https://xizhi.qqoq.net/xxxxxxxxx.send'
|
|
||||||
# xizhi(xizhi_api, send_content)
|
|
||||||
|
|
||||||
# telegram推送通知
|
|
||||||
tg_token = '' # tg搜索"BotFather"获取的token值
|
|
||||||
tg_chat_id = 000000 # tg搜索"userinfobot"获取的chat_id值,即可发送推送消息给你自己,如果下面的是群组id则发送到群
|
|
||||||
# tg_bot(tg_chat_id, tg_token, send_content)
|
|
||||||
|
|
||||||
# email_message("", "", "", "", "", "")
|
|
||||||
|
|
||||||
bark_url = 'https://xxx.xxx.com/key/'
|
|
||||||
# bark(bark_url, send_title, send_content)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,110 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import functools
|
|
||||||
import hashlib
|
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
from typing import Union, Any
|
|
||||||
|
|
||||||
from .logger import logger
|
|
||||||
import configparser
|
|
||||||
|
|
||||||
|
|
||||||
def trace_error_decorator(func: callable) -> callable:
|
|
||||||
@functools.wraps(func)
|
|
||||||
def wrapper(*args: list, **kwargs: dict) -> Any:
|
|
||||||
try:
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
error_line = traceback.extract_tb(e.__traceback__)[-1].lineno
|
|
||||||
error_info = f"错误信息: type: {type(e).__name__}, {str(e)} in function {func.__name__} at line: {error_line}"
|
|
||||||
logger.error(error_info)
|
|
||||||
return []
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def check_md5(file_path: str) -> str:
|
|
||||||
with open(file_path, 'rb') as fp:
|
|
||||||
file_md5 = hashlib.md5(fp.read()).hexdigest()
|
|
||||||
return file_md5
|
|
||||||
|
|
||||||
|
|
||||||
def dict_to_cookie_str(cookies_dict) -> str:
|
|
||||||
cookie_str = '; '.join([f"{key}={value}" for key, value in cookies_dict.items()])
|
|
||||||
return cookie_str
|
|
||||||
|
|
||||||
|
|
||||||
def read_config_value(file_path, section, key) -> Union[str, None]:
|
|
||||||
config = configparser.ConfigParser()
|
|
||||||
|
|
||||||
try:
|
|
||||||
config.read(file_path, encoding='utf-8-sig')
|
|
||||||
except Exception as e:
|
|
||||||
print(f"读取配置文件时出错: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if section in config:
|
|
||||||
if key in config[section]:
|
|
||||||
return config[section][key]
|
|
||||||
else:
|
|
||||||
print(f"键[{key}]不存在于部分[{section}]中。")
|
|
||||||
else:
|
|
||||||
print(f"部分[{section}]不存在于文件中。")
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def update_config(file_path, section, key, new_value) -> None:
|
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
|
||||||
|
|
||||||
try:
|
|
||||||
config.read(file_path, encoding='utf-8-sig')
|
|
||||||
except Exception as e:
|
|
||||||
print(f"读取配置文件时出错: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if section not in config:
|
|
||||||
print(f"部分[{section}]不存在于文件中。")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 转义%字符
|
|
||||||
escaped_value = new_value.replace('%', '%%')
|
|
||||||
config[section][key] = escaped_value
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(file_path, 'w', encoding='utf-8-sig') as configfile:
|
|
||||||
config.write(configfile)
|
|
||||||
print(f"配置文件中[{section}]下的{key}的值已更新")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"写入配置文件时出错: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_file_paths(directory) -> list:
|
|
||||||
file_paths = []
|
|
||||||
for root, dirs, files in os.walk(directory):
|
|
||||||
for file in files:
|
|
||||||
file_paths.append(os.path.join(root, file))
|
|
||||||
return file_paths
|
|
||||||
|
|
||||||
|
|
||||||
def remove_emojis(text, replace_text=r''):
|
|
||||||
emoji_pattern = re.compile(
|
|
||||||
"["
|
|
||||||
"\U0001F1E0-\U0001F1FF" # flags (iOS)
|
|
||||||
"\U0001F300-\U0001F5FF" # symbols & pictographs
|
|
||||||
"\U0001F600-\U0001F64F" # emoticons
|
|
||||||
"\U0001F680-\U0001F6FF" # transport & map symbols
|
|
||||||
"\U0001F700-\U0001F77F" # alchemical symbols
|
|
||||||
"\U0001F780-\U0001F7FF" # Geometric Shapes Extended
|
|
||||||
"\U0001F800-\U0001F8FF" # Supplemental Arrows-C
|
|
||||||
"\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs
|
|
||||||
"\U0001FA00-\U0001FA6F" # Chess Symbols
|
|
||||||
"\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A
|
|
||||||
"\U00002702-\U000027B0" # Dingbats
|
|
||||||
"]+",
|
|
||||||
flags=re.UNICODE
|
|
||||||
)
|
|
||||||
return emoji_pattern.sub(replace_text, text)
|
|
||||||
@ -1,100 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
Author: Hmily
|
|
||||||
Github:https://github.com/ihmily
|
|
||||||
Date: 2023-07-17 23:52:05
|
|
||||||
Update: 2024-03-06 23:35:00
|
|
||||||
Copyright (c) 2023 by Hmily, All Rights Reserved.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import urllib.parse
|
|
||||||
from typing import Union
|
|
||||||
import execjs
|
|
||||||
import requests
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
no_proxy_handler = urllib.request.ProxyHandler({})
|
|
||||||
opener = urllib.request.build_opener(no_proxy_handler)
|
|
||||||
|
|
||||||
HEADERS = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) '
|
|
||||||
'SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36',
|
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
|
|
||||||
'Cookie': 's_v_web_id=verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# X-bogus算法
|
|
||||||
def get_xbogus(url: str, headers: Union[dict, None] = None) -> str:
|
|
||||||
if not headers or "User-Agent" not in headers and "user-agent" not in headers:
|
|
||||||
headers = HEADERS
|
|
||||||
query = urllib.parse.urlparse(url).query
|
|
||||||
xbogus = execjs.compile(open('./x-bogus.js').read()).call('sign', query, headers["User-Agent"])
|
|
||||||
# print(xbogus)
|
|
||||||
return xbogus
|
|
||||||
|
|
||||||
|
|
||||||
# 获取房间ID和用户secID
|
|
||||||
def get_sec_user_id(url: str, proxy_addr: Union[str, None] = None, headers: Union[dict, None] = None):
|
|
||||||
if not headers or "User-Agent" not in headers and "user-agent" not in headers:
|
|
||||||
headers = HEADERS
|
|
||||||
|
|
||||||
if proxy_addr:
|
|
||||||
proxies = {
|
|
||||||
'http': proxy_addr,
|
|
||||||
'https': proxy_addr
|
|
||||||
}
|
|
||||||
response = requests.get(url, headers=headers, proxies=proxies, timeout=15)
|
|
||||||
else:
|
|
||||||
response = opener.open(url, timeout=15)
|
|
||||||
redirect_url = response.url
|
|
||||||
sec_user_id = re.search(r'sec_user_id=([\w_\-]+)&', redirect_url).group(1)
|
|
||||||
room_id = redirect_url.split('?')[0].rsplit('/', maxsplit=1)[1]
|
|
||||||
return room_id, sec_user_id
|
|
||||||
|
|
||||||
|
|
||||||
# 获取直播间webID
|
|
||||||
def get_live_room_id(room_id: str, sec_user_id: str, proxy_addr: Union[str, None] = None,
|
|
||||||
params: Union[dict, None] = None, headers: Union[dict, None] = None) -> str:
|
|
||||||
if not headers or "User-Agent" not in headers and "user-agent" not in headers:
|
|
||||||
headers = HEADERS
|
|
||||||
|
|
||||||
if not params:
|
|
||||||
params = {
|
|
||||||
"verifyFp": "verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf",
|
|
||||||
"type_id": "0",
|
|
||||||
"live_id": "1",
|
|
||||||
"room_id": room_id,
|
|
||||||
"sec_user_id": sec_user_id,
|
|
||||||
"app_id": "1128",
|
|
||||||
"msToken": "wrqzbEaTlsxt52-vxyZo_mIoL0RjNi1ZdDe7gzEGMUTVh_HvmbLLkQrA_1HKVOa2C6gkxb6IiY6TY2z8enAkPEwGq--gM"
|
|
||||||
"-me3Yudck2ailla5Q4osnYIHxd9dI4WtQ==",
|
|
||||||
}
|
|
||||||
api = f'https://webcast.amemv.com/webcast/room/reflow/info/?{urllib.parse.urlencode(params)}'
|
|
||||||
xbogus = get_xbogus(api)
|
|
||||||
api = api + "&X-Bogus=" + xbogus
|
|
||||||
|
|
||||||
if proxy_addr:
|
|
||||||
proxies = {
|
|
||||||
'http': proxy_addr,
|
|
||||||
'https': proxy_addr
|
|
||||||
}
|
|
||||||
response = requests.get(api, headers=headers, proxies=proxies, timeout=15)
|
|
||||||
json_str = response.text
|
|
||||||
else:
|
|
||||||
req = urllib.request.Request(api, headers=headers)
|
|
||||||
response = opener.open(req, timeout=15)
|
|
||||||
json_str = response.read().decode('utf-8')
|
|
||||||
json_data = json.loads(json_str)
|
|
||||||
return json_data['data']['room']['owner']['web_rid']
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
room_url = "https://v.douyin.com/iQLgKSj/"
|
|
||||||
# url="https://v.douyin.com/iQFeBnt/"
|
|
||||||
# url="https://v.douyin.com/iehvKttp/"
|
|
||||||
_room_id, sec_uid = get_sec_user_id(room_url)
|
|
||||||
web_rid = get_live_room_id(_room_id, sec_uid)
|
|
||||||
print("return web_rid:", web_rid)
|
|
||||||
BIN
ffmpeg.exe
BIN
ffmpeg.exe
Binary file not shown.
221
ffmpeg_install.py
Normal file
221
ffmpeg_install.py
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Author: Hmily
|
||||||
|
GitHub: https://github.com/ihmily
|
||||||
|
Copyright (c) 2024 by Hmily, All Rights Reserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import platform
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
import requests
|
||||||
|
from tqdm import tqdm
|
||||||
|
from src.logger import logger
|
||||||
|
|
||||||
|
current_platform = platform.system()
|
||||||
|
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
|
||||||
|
current_env_path = os.environ.get('PATH')
|
||||||
|
ffmpeg_path = os.path.join(execute_dir, 'ffmpeg')
|
||||||
|
|
||||||
|
|
||||||
|
def unzip_file(zip_path: str | Path, extract_to: str | Path, delete: bool = True) -> None:
|
||||||
|
if not os.path.exists(extract_to):
|
||||||
|
os.makedirs(extract_to)
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(extract_to)
|
||||||
|
|
||||||
|
if delete and os.path.exists(zip_path):
|
||||||
|
os.remove(zip_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_lanzou_download_link(url: str, password: str | None = None) -> str | None:
|
||||||
|
try:
|
||||||
|
headers = {
|
||||||
|
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
||||||
|
'Origin': 'https://wweb.lanzouv.com',
|
||||||
|
'Referer': 'https://wweb.lanzouv.com/iXncv0dly6mh',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||||
|
'Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0',
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
sign = re.search("var skdklds = '(.*?)';", response.text).group(1)
|
||||||
|
data = {
|
||||||
|
'action': 'downprocess',
|
||||||
|
'sign': sign,
|
||||||
|
'p': password,
|
||||||
|
'kd': '1',
|
||||||
|
}
|
||||||
|
response = requests.post('https://wweb.lanzouv.com/ajaxm.php', headers=headers, data=data)
|
||||||
|
json_data = response.json()
|
||||||
|
download_url = json_data['dom'] + "/file/" + json_data['url']
|
||||||
|
response = requests.get(download_url, headers=headers)
|
||||||
|
return response.url
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to obtain ffmpeg download address. {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def install_ffmpeg_windows():
|
||||||
|
try:
|
||||||
|
logger.warning("ffmpeg is not installed.")
|
||||||
|
logger.debug("Installing the latest version of ffmpeg for Windows...")
|
||||||
|
ffmpeg_url = get_lanzou_download_link('https://wweb.lanzouv.com/iHAc22ly3r3g', 'eots')
|
||||||
|
if ffmpeg_url:
|
||||||
|
full_file_name = 'ffmpeg_latest_build_20250124.zip'
|
||||||
|
version = 'v20250124'
|
||||||
|
zip_file_path = Path(execute_dir) / full_file_name
|
||||||
|
if Path(zip_file_path).exists():
|
||||||
|
logger.debug("ffmpeg installation file already exists, start install...")
|
||||||
|
else:
|
||||||
|
response = requests.get(ffmpeg_url, stream=True)
|
||||||
|
total_size = int(response.headers.get('Content-Length', 0))
|
||||||
|
block_size = 1024
|
||||||
|
|
||||||
|
with tqdm(total=total_size, unit="B", unit_scale=True,
|
||||||
|
ncols=100, desc=f'Downloading ffmpeg ({version})') as t:
|
||||||
|
with open(zip_file_path, 'wb') as f:
|
||||||
|
for data in response.iter_content(block_size):
|
||||||
|
t.update(len(data))
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
unzip_file(zip_file_path, execute_dir)
|
||||||
|
os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path
|
||||||
|
result = subprocess.run(["ffmpeg", "-version"], capture_output=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.debug('ffmpeg installation was successful')
|
||||||
|
else:
|
||||||
|
logger.error('ffmpeg installation failed. Please manually install ffmpeg by yourself')
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Please manually install ffmpeg by yourself")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"type: {type(e).__name__}, ffmpeg installation failed {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def install_ffmpeg_mac():
|
||||||
|
logger.warning("ffmpeg is not installed.")
|
||||||
|
logger.debug("Installing the stable version of ffmpeg for macOS...")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["brew", "install", "ffmpeg"], capture_output=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.debug('ffmpeg installation was successful. Restart for changes to take effect.')
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("ffmpeg installation failed")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"Failed to install ffmpeg using Homebrew. {e}")
|
||||||
|
logger.error("Please install ffmpeg manually or check your Homebrew installation.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An unexpected error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def install_ffmpeg_linux():
|
||||||
|
is_RHS = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.warning("ffmpeg is not installed.")
|
||||||
|
logger.debug("Trying to install the stable version of ffmpeg")
|
||||||
|
result = subprocess.run(['yum', '-y', 'update'], capture_output=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error("Failed to update package lists using yum.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = subprocess.run(['yum', 'install', '-y', 'ffmpeg'], capture_output=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.debug("ffmpeg installation was successful using yum. Restart for changes to take effect.")
|
||||||
|
return True
|
||||||
|
logger.error(result.stderr.decode('utf-8').strip())
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.debug("yum command not found, trying to install using apt...")
|
||||||
|
is_RHS = False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An error occurred while trying to install ffmpeg using yum: {e}")
|
||||||
|
|
||||||
|
if not is_RHS:
|
||||||
|
try:
|
||||||
|
logger.debug("Trying to install the stable version of ffmpeg for Linux using apt...")
|
||||||
|
result = subprocess.run(['apt', 'update'], capture_output=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error("Failed to update package lists using apt")
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = subprocess.run(['apt', 'install', '-y', 'ffmpeg'], capture_output=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.debug("ffmpeg installation was successful using apt. Restart for changes to take effect.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(result.stderr.decode('utf-8').strip())
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error("apt command not found, unable to install ffmpeg. Please manually install ffmpeg by yourself")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An error occurred while trying to install ffmpeg using apt: {e}")
|
||||||
|
logger.error("Manual installation of ffmpeg is required. Please manually install ffmpeg by yourself.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def install_ffmpeg() -> bool:
|
||||||
|
if current_platform == "Windows":
|
||||||
|
return install_ffmpeg_windows()
|
||||||
|
elif current_platform == "Linux":
|
||||||
|
return install_ffmpeg_linux()
|
||||||
|
elif current_platform == "Darwin":
|
||||||
|
return install_ffmpeg_mac()
|
||||||
|
else:
|
||||||
|
logger.debug(f"ffmpeg auto installation is not supported on this platform: {current_platform}. "
|
||||||
|
f"Please install ffmpeg manually.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_ffmpeg_installed(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['ffmpeg', '-version'], capture_output=True)
|
||||||
|
version = result.stdout.strip()
|
||||||
|
if result.returncode == 0 and version:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wrapped_func(*args, **kwargs):
|
||||||
|
if sys.version_info >= (3, 7):
|
||||||
|
res = wrapper(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
res = wrapper(*args, **kwargs)
|
||||||
|
if not res:
|
||||||
|
install_ffmpeg()
|
||||||
|
res = wrapper(*args, **kwargs)
|
||||||
|
|
||||||
|
if not res:
|
||||||
|
raise RuntimeError("ffmpeg is not installed.")
|
||||||
|
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapped_func
|
||||||
|
|
||||||
|
|
||||||
|
def check_ffmpeg_installed() -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['ffmpeg', '-version'], capture_output=True)
|
||||||
|
version = result.stdout.strip()
|
||||||
|
if result.returncode == 0 and version:
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except OSError as e:
|
||||||
|
print(f"OSError occurred: {e}. ffmpeg may not be installed correctly or is not available in the system PATH.")
|
||||||
|
print("Please delete the ffmpeg and try to download and install again.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_ffmpeg() -> bool:
|
||||||
|
if not check_ffmpeg_installed():
|
||||||
|
return install_ffmpeg()
|
||||||
|
return True
|
||||||
32
i18n.py
Normal file
32
i18n.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import gettext
|
||||||
|
import inspect
|
||||||
|
import builtins
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def init_gettext(locale_dir, locale_name):
|
||||||
|
gettext.bindtextdomain('zh_CN', locale_dir)
|
||||||
|
gettext.textdomain('zh_CN')
|
||||||
|
os.environ['LANG'] = f'{locale_name}.utf8'
|
||||||
|
return gettext.gettext
|
||||||
|
|
||||||
|
|
||||||
|
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
|
||||||
|
if os.path.exists(Path(execute_dir) / '_internal/i18n'):
|
||||||
|
locale_path = Path(execute_dir) / '_internal/i18n'
|
||||||
|
else:
|
||||||
|
locale_path = Path(execute_dir) / 'i18n'
|
||||||
|
_tr = init_gettext(locale_path, 'zh_CN')
|
||||||
|
original_print = builtins.print
|
||||||
|
package_name = 'src'
|
||||||
|
|
||||||
|
|
||||||
|
def translated_print(*args, **kwargs):
|
||||||
|
for arg in args:
|
||||||
|
if package_name in inspect.stack()[1].filename:
|
||||||
|
translated_arg = _tr(str(arg))
|
||||||
|
else:
|
||||||
|
translated_arg = str(arg)
|
||||||
|
original_print(translated_arg, **kwargs)
|
||||||
BIN
i18n/zh_CN/LC_MESSAGES/zh_CN.mo
Normal file
BIN
i18n/zh_CN/LC_MESSAGES/zh_CN.mo
Normal file
Binary file not shown.
85
i18n/zh_CN/LC_MESSAGES/zh_CN.po
Normal file
85
i18n/zh_CN/LC_MESSAGES/zh_CN.po
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# DouyinLiveRecorder.
|
||||||
|
# Copyright (C) 2024 Hmily
|
||||||
|
# This file is distributed under the same license as the DouyinLiveRecorder package.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: 4.0.1\n"
|
||||||
|
"POT-Creation-Date: 2024-10-20 00:00+0800\n"
|
||||||
|
"PO-Revision-Date: 2024-11-09 03:05+0800\n"
|
||||||
|
"Last-Translator: Hmily <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: Chinese\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Language: zh_CN\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
|
||||||
|
#: douyinliverecorder/spider.py
|
||||||
|
msgid "IP banned. Please change device or network."
|
||||||
|
msgstr "IP被禁止 请更换设备或网络"
|
||||||
|
|
||||||
|
msgid "The anchor did not start broadcasting."
|
||||||
|
msgstr "主播并未开播"
|
||||||
|
|
||||||
|
msgid "sooplive platform login successful! Starting to fetch live streaming data..."
|
||||||
|
msgstr "sooplive平台登录成功!开始获取直播数据..."
|
||||||
|
|
||||||
|
msgid "sooplive live stream failed to retrieve, the live stream just ended."
|
||||||
|
msgstr "sooplive直播获取失败,该直播间刚结束直播"
|
||||||
|
|
||||||
|
msgid "sooplive live stream retrieval failed, the live needs 19+, you are not logged in."
|
||||||
|
msgstr "soop直播获取失败,该直播间需要年龄19+观看,您尚未登录"
|
||||||
|
|
||||||
|
msgid "Attempting to log in to the sooplive live streaming platform with your account and password, please ensure it is configured."
|
||||||
|
msgstr "正在尝试使用您的账号和密码登录soop直播平台,请确保已在config配置文件中配置"
|
||||||
|
|
||||||
|
msgid "error message:Please check if the input sooplive live room address is correct."
|
||||||
|
msgstr "错误信息:请检查输入的sooplive直播间地址是否正确"
|
||||||
|
|
||||||
|
msgid "Please check if the FlexTV account and password in the configuration file are correct."
|
||||||
|
msgstr "请检查配置文件中的FlexTV账号和密码是否正确"
|
||||||
|
|
||||||
|
msgid "FlexTV live stream retrieval failed [not logged in]: 19+ live streams are only available for logged-in adults."
|
||||||
|
msgstr "FlexTV直播获取失败[未登录]: 19+直播需要登录后是成人才可观看"
|
||||||
|
|
||||||
|
msgid "Attempting to log in to the FlexTV live streaming platform, please ensure your account and password are correctly filled in the configuration file."
|
||||||
|
msgstr "正在尝试登录FlexTV直播平台,请确保已在配置文件中填写好您的账号和密码"
|
||||||
|
|
||||||
|
msgid "Logging into FlexTV platform..."
|
||||||
|
msgstr "FlexTV平台登录中..."
|
||||||
|
|
||||||
|
msgid "Logged into FlexTV platform successfully! Starting to fetch live streaming data..."
|
||||||
|
msgstr "FlexTV平台登录成功!开始获取直播数据..."
|
||||||
|
|
||||||
|
msgid "Look live currently only supports audio live streaming, not video live streaming!"
|
||||||
|
msgstr "Look直播暂时只支持音频直播,不支持Look视频直播!"
|
||||||
|
|
||||||
|
msgid "Failed to retrieve popkontv live stream [token does not exist or has expired]: Please log in to watch."
|
||||||
|
msgstr "popkontv直播获取失败[token不存在或者已过期]: 请登录后观看"
|
||||||
|
|
||||||
|
msgid "Attempting to log in to the popkontv live streaming platform, please ensure your account and password are correctly filled in the configuration file."
|
||||||
|
msgstr "正在尝试登录popkontv直播平台,请确保已在配置文件中填写好您的账号和密码"
|
||||||
|
|
||||||
|
msgid "Logging into popkontv platform..."
|
||||||
|
msgstr "popkontv平台登录中..."
|
||||||
|
|
||||||
|
msgid "Logged into popkontv platform successfully! Starting to fetch live streaming data..."
|
||||||
|
msgstr "popkontv平台登录成功!开始获取直播数据..."
|
||||||
|
|
||||||
|
msgid "Attempting to log in to TwitCasting..."
|
||||||
|
msgstr "TwitCasting正在尝试登录..."
|
||||||
|
|
||||||
|
msgid "TwitCasting login successful! Starting to fetch data..."
|
||||||
|
msgstr "TwitCasting 登录成功!开始获取数据..."
|
||||||
|
|
||||||
|
msgid "Failed to retrieve TwitCasting data, attempting to log in..."
|
||||||
|
msgstr "获取TwitCasting数据失败,正在尝试登录..."
|
||||||
|
|
||||||
|
msgid "Failed to retrieve live room data, the Huajiao live room address is not fixed, please manually change the address for recording."
|
||||||
|
msgstr "获取直播间数据失败,花椒直播间地址是非固定的,请手动更换地址进行录制"
|
||||||
|
|
||||||
|
msgid "Fetch shopee live data failed, please update the address of the live broadcast room and try again."
|
||||||
|
msgstr "获取shopee直播间数据失败,请手动更换直播录制地址后重试"
|
||||||
|
|
||||||
Binary file not shown.
295
msg_push.py
Normal file
295
msg_push.py
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Author: Hmily
|
||||||
|
GitHub: https://github.com/ihmily
|
||||||
|
Date: 2023-09-03 19:18:36
|
||||||
|
Update: 2025-01-23 17:16:12
|
||||||
|
Copyright (c) 2023-2024 by Hmily, All Rights Reserved.
|
||||||
|
"""
|
||||||
|
from typing import Dict, Any
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import smtplib
|
||||||
|
from email.header import Header
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
no_proxy_handler = urllib.request.ProxyHandler({})
|
||||||
|
opener = urllib.request.build_opener(no_proxy_handler)
|
||||||
|
headers: Dict[str, str] = {'Content-Type': 'application/json'}
|
||||||
|
|
||||||
|
|
||||||
|
def dingtalk(url: str, content: str, number: str = None, is_atall: bool = False) -> Dict[str, Any]:
|
||||||
|
success = []
|
||||||
|
error = []
|
||||||
|
api_list = url.replace(',', ',').split(',') if url.strip() else []
|
||||||
|
for api in api_list:
|
||||||
|
json_data = {
|
||||||
|
'msgtype': 'text',
|
||||||
|
'text': {
|
||||||
|
'content': content,
|
||||||
|
},
|
||||||
|
"at": {
|
||||||
|
"atMobiles": [
|
||||||
|
number
|
||||||
|
],
|
||||||
|
"isAtAll": is_atall
|
||||||
|
},
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
data = json.dumps(json_data).encode('utf-8')
|
||||||
|
req = urllib.request.Request(api, data=data, headers=headers)
|
||||||
|
response = opener.open(req, timeout=10)
|
||||||
|
json_str = response.read().decode('utf-8')
|
||||||
|
json_data = json.loads(json_str)
|
||||||
|
if json_data['errcode'] == 0:
|
||||||
|
success.append(api)
|
||||||
|
else:
|
||||||
|
error.append(api)
|
||||||
|
print(f'钉钉推送失败, 推送地址:{api}, {json_data["errmsg"]}')
|
||||||
|
except Exception as e:
|
||||||
|
error.append(api)
|
||||||
|
print(f'钉钉推送失败, 推送地址:{api}, 错误信息:{e}')
|
||||||
|
return {"success": success, "error": error}
|
||||||
|
|
||||||
|
|
||||||
|
def xizhi(url: str, title: str, content: str) -> Dict[str, Any]:
|
||||||
|
success = []
|
||||||
|
error = []
|
||||||
|
api_list = url.replace(',', ',').split(',') if url.strip() else []
|
||||||
|
for api in api_list:
|
||||||
|
json_data = {
|
||||||
|
'title': title,
|
||||||
|
'content': content
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
data = json.dumps(json_data).encode('utf-8')
|
||||||
|
req = urllib.request.Request(api, data=data, headers=headers)
|
||||||
|
response = opener.open(req, timeout=10)
|
||||||
|
json_str = response.read().decode('utf-8')
|
||||||
|
json_data = json.loads(json_str)
|
||||||
|
if json_data['code'] == 200:
|
||||||
|
success.append(api)
|
||||||
|
else:
|
||||||
|
error.append(api)
|
||||||
|
print(f'微信推送失败, 推送地址:{api}, 失败信息:{json_data["msg"]}')
|
||||||
|
except Exception as e:
|
||||||
|
error.append(api)
|
||||||
|
print(f'微信推送失败, 推送地址:{api}, 错误信息:{e}')
|
||||||
|
return {"success": success, "error": error}
|
||||||
|
|
||||||
|
|
||||||
|
def send_email(email_host: str, login_email: str, email_pass: str, sender_email: str, sender_name: str,
|
||||||
|
to_email: str, title: str, content: str, smtp_port: str = None, open_ssl: bool = True) -> Dict[str, Any]:
|
||||||
|
receivers = to_email.replace(',', ',').split(',') if to_email.strip() else []
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = MIMEMultipart()
|
||||||
|
send_name = base64.b64encode(sender_name.encode("utf-8")).decode()
|
||||||
|
message['From'] = f'=?UTF-8?B?{send_name}?= <{sender_email}>'
|
||||||
|
message['Subject'] = Header(title, 'utf-8')
|
||||||
|
if len(receivers) == 1:
|
||||||
|
message['To'] = receivers[0]
|
||||||
|
|
||||||
|
t_apart = MIMEText(content, 'plain', 'utf-8')
|
||||||
|
message.attach(t_apart)
|
||||||
|
|
||||||
|
if open_ssl:
|
||||||
|
smtp_port = int(smtp_port) or 465
|
||||||
|
smtp_obj = smtplib.SMTP_SSL(email_host, smtp_port)
|
||||||
|
else:
|
||||||
|
smtp_port = int(smtp_port) or 25
|
||||||
|
smtp_obj = smtplib.SMTP(email_host, smtp_port)
|
||||||
|
smtp_obj.login(login_email, email_pass)
|
||||||
|
smtp_obj.sendmail(sender_email, receivers, message.as_string())
|
||||||
|
return {"success": receivers, "error": []}
|
||||||
|
except smtplib.SMTPException as e:
|
||||||
|
print(f'邮件推送失败, 推送邮箱:{to_email}, 错误信息:{e}')
|
||||||
|
return {"success": [], "error": receivers}
|
||||||
|
|
||||||
|
|
||||||
|
def tg_bot(chat_id: int, token: str, content: str) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
json_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
'text': content
|
||||||
|
}
|
||||||
|
url = f'https://api.telegram.org/bot{token}/sendMessage'
|
||||||
|
data = json.dumps(json_data).encode('utf-8')
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers)
|
||||||
|
response = urllib.request.urlopen(req, timeout=15)
|
||||||
|
json_str = response.read().decode('utf-8')
|
||||||
|
_json_data = json.loads(json_str)
|
||||||
|
return {"success": [1], "error": []}
|
||||||
|
except Exception as e:
|
||||||
|
print(f'tg推送失败, 聊天ID:{chat_id}, 错误信息:{e}')
|
||||||
|
return {"success": [], "error": [1]}
|
||||||
|
|
||||||
|
|
||||||
|
def bark(api: str, title: str = "message", content: str = 'test', level: str = "active",
|
||||||
|
badge: int = 1, auto_copy: int = 1, sound: str = "", icon: str = "", group: str = "",
|
||||||
|
is_archive: int = 1, url: str = "") -> Dict[str, Any]:
|
||||||
|
success = []
|
||||||
|
error = []
|
||||||
|
api_list = api.replace(',', ',').split(',') if api.strip() else []
|
||||||
|
for _api in api_list:
|
||||||
|
json_data = {
|
||||||
|
"title": title,
|
||||||
|
"body": content,
|
||||||
|
"level": level,
|
||||||
|
"badge": badge,
|
||||||
|
"autoCopy": auto_copy,
|
||||||
|
"sound": sound,
|
||||||
|
"icon": icon,
|
||||||
|
"group": group,
|
||||||
|
"isArchive": is_archive,
|
||||||
|
"url": url
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
data = json.dumps(json_data).encode('utf-8')
|
||||||
|
req = urllib.request.Request(_api, data=data, headers=headers)
|
||||||
|
response = opener.open(req, timeout=10)
|
||||||
|
json_str = response.read().decode("utf-8")
|
||||||
|
json_data = json.loads(json_str)
|
||||||
|
if json_data['code'] == 200:
|
||||||
|
success.append(_api)
|
||||||
|
else:
|
||||||
|
error.append(_api)
|
||||||
|
print(f'Bark推送失败, 推送地址:{_api}, 失败信息:{json_data["message"]}')
|
||||||
|
except Exception as e:
|
||||||
|
error.append(api)
|
||||||
|
print(f'Bark推送失败, 推送地址:{_api}, 错误信息:{e}')
|
||||||
|
return {"success": success, "error": error}
|
||||||
|
|
||||||
|
|
||||||
|
def ntfy(api: str, title: str = "message", content: str = 'test', tags: str = 'tada', priority: int = 3,
|
||||||
|
action_url: str = "", attach: str = "", filename: str = "", click: str = "", icon: str = "",
|
||||||
|
delay: str = "", email: str = "", call: str = "") -> Dict[str, Any]:
|
||||||
|
success = []
|
||||||
|
error = []
|
||||||
|
api_list = api.replace(',', ',').split(',') if api.strip() else []
|
||||||
|
tags = tags.replace(',', ',').split(',') if tags else ['partying_face']
|
||||||
|
actions = [{"action": "view", "label": "view live", "url": action_url}] if action_url else []
|
||||||
|
for _api in api_list:
|
||||||
|
server, topic = _api.rsplit('/', maxsplit=1)
|
||||||
|
json_data = {
|
||||||
|
"topic": topic,
|
||||||
|
"title": title,
|
||||||
|
"message": content,
|
||||||
|
"tags": tags,
|
||||||
|
"priority": priority,
|
||||||
|
"attach": attach,
|
||||||
|
"filename": filename,
|
||||||
|
"click": click,
|
||||||
|
"actions": actions,
|
||||||
|
"markdown": False,
|
||||||
|
"icon": icon,
|
||||||
|
"delay": delay,
|
||||||
|
"email": email,
|
||||||
|
"call": call
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.dumps(json_data, ensure_ascii=False).encode('utf-8')
|
||||||
|
req = urllib.request.Request(server, data=data, headers=headers)
|
||||||
|
response = opener.open(req, timeout=10)
|
||||||
|
json_str = response.read().decode("utf-8")
|
||||||
|
json_data = json.loads(json_str)
|
||||||
|
if "error" not in json_data:
|
||||||
|
success.append(_api)
|
||||||
|
else:
|
||||||
|
error.append(_api)
|
||||||
|
print(f'ntfy推送失败, 推送地址:{_api}, 失败信息:{json_data["error"]}')
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
error.append(_api)
|
||||||
|
error_msg = e.read().decode("utf-8")
|
||||||
|
print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{json.loads(error_msg)["error"]}')
|
||||||
|
except Exception as e:
|
||||||
|
error.append(api)
|
||||||
|
print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{e}')
|
||||||
|
return {"success": success, "error": error}
|
||||||
|
|
||||||
|
|
||||||
|
def pushplus(token: str, title: str, content: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
PushPlus推送通知
|
||||||
|
API文档: https://www.pushplus.plus/doc/
|
||||||
|
"""
|
||||||
|
success = []
|
||||||
|
error = []
|
||||||
|
token_list = token.replace(',', ',').split(',') if token.strip() else []
|
||||||
|
|
||||||
|
for _token in token_list:
|
||||||
|
json_data = {
|
||||||
|
'token': _token,
|
||||||
|
'title': title,
|
||||||
|
'content': content
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = 'https://www.pushplus.plus/send'
|
||||||
|
data = json.dumps(json_data).encode('utf-8')
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers)
|
||||||
|
response = opener.open(req, timeout=10)
|
||||||
|
json_str = response.read().decode('utf-8')
|
||||||
|
json_data = json.loads(json_str)
|
||||||
|
|
||||||
|
if json_data.get('code') == 200:
|
||||||
|
success.append(_token)
|
||||||
|
else:
|
||||||
|
error.append(_token)
|
||||||
|
print(f'PushPlus推送失败, Token:{_token}, 失败信息:{json_data.get("msg", "未知错误")}')
|
||||||
|
except Exception as e:
|
||||||
|
error.append(_token)
|
||||||
|
print(f'PushPlus推送失败, Token:{_token}, 错误信息:{e}')
|
||||||
|
|
||||||
|
return {"success": success, "error": error}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
send_title = '直播通知' # 标题
|
||||||
|
send_content = '张三 开播了!' # 推送内容
|
||||||
|
|
||||||
|
# 钉钉推送通知
|
||||||
|
webhook_api = '' # 替换成自己Webhook链接,参考文档:https://open.dingtalk.com/document/robots/custom-robot-access
|
||||||
|
phone_number = '' # 被@用户的手机号码
|
||||||
|
is_atall = '' # 是否@全体
|
||||||
|
# dingtalk(webhook_api, send_content, phone_number)
|
||||||
|
|
||||||
|
# 微信推送通知
|
||||||
|
# 替换成自己的单点推送接口,获取地址:https://xz.qqoq.net/#/admin/one
|
||||||
|
# 当然也可以使用其他平台API 如server酱 使用方法一样
|
||||||
|
xizhi_api = 'https://xizhi.qqoq.net/xxxxxxxxx.send'
|
||||||
|
# xizhi(xizhi_api, send_content)
|
||||||
|
|
||||||
|
# telegram推送通知
|
||||||
|
tg_token = '' # tg搜索"BotFather"获取的token值
|
||||||
|
tg_chat_id = 000000 # tg搜索"userinfobot"获取的chat_id值,即可发送推送消息给你自己,如果下面的是群组id则发送到群
|
||||||
|
# tg_bot(tg_chat_id, tg_token, send_content)
|
||||||
|
|
||||||
|
# email_message(
|
||||||
|
# email_host="smtp.qq.com",
|
||||||
|
# login_email="",
|
||||||
|
# email_pass="",
|
||||||
|
# sender_email="",
|
||||||
|
# sender_name="",
|
||||||
|
# to_email="",
|
||||||
|
# title="",
|
||||||
|
# content="",
|
||||||
|
# )
|
||||||
|
|
||||||
|
bark_url = 'https://xxx.xxx.com/key/'
|
||||||
|
# bark(bark_url, send_title, send_content)
|
||||||
|
|
||||||
|
ntfy(
|
||||||
|
api="https://ntfy.sh/xxxxx",
|
||||||
|
title="直播推送",
|
||||||
|
content="xxx已开播",
|
||||||
|
)
|
||||||
|
|
||||||
|
# PushPlus推送通知
|
||||||
|
pushplus_token = '' # 替换成自己的PushPlus Token,获取地址:https://www.pushplus.plus/
|
||||||
|
# pushplus(pushplus_token, send_title, send_content)
|
||||||
@ -1,4 +0,0 @@
|
|||||||
[virtualenvs]
|
|
||||||
in-project = true
|
|
||||||
create = true
|
|
||||||
prefer-active-python = true
|
|
||||||
@ -1,24 +1,23 @@
|
|||||||
[project]
|
[project]
|
||||||
requires-python = ">=3.8,<3.13"
|
name = "DouyinLiveRecorder"
|
||||||
|
version = "4.0.7"
|
||||||
[build-system]
|
description = "可循环值守和多人录制的直播录制软件, 支持抖音、TikTok、Youtube、快手、虎牙、斗鱼、B站、小红书、pandatv、sooplive、flextv、popkontv、twitcasting、winktv、百度、微博、酷狗、17Live、Twitch、Acfun、CHZZK、shopee等40+平台直播录制"
|
||||||
requires = ["poetry-core>=1.0.0"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
|
|
||||||
[tool.poetry]
|
|
||||||
name = "douyinliverecorder"
|
|
||||||
version = "3.0.8"
|
|
||||||
description = "An easy tool for recording live streams"
|
|
||||||
authors = ["Hmily"]
|
|
||||||
license = "MIT"
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "https://github.com/ihmily/DouyinLiveRecorder"
|
authors = [{name = "Hmily"}]
|
||||||
repository = "https://github.com/ihmily/DouyinLiveRecorder"
|
license = { text = "MIT" }
|
||||||
keywords = ["douyin", "live", "recorder"]
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"requests>=2.31.0",
|
||||||
|
"loguru>=0.7.3",
|
||||||
|
"pycryptodome>=3.20.0",
|
||||||
|
"distro>=1.9.0",
|
||||||
|
"tqdm>=4.67.1",
|
||||||
|
"httpx[http2]>=0.28.1",
|
||||||
|
"PyExecJS>=1.5.1"
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[project.urls]
|
||||||
python = "^3.8"
|
"Homepage" = "https://github.com/ihmily/DouyinLiveRecorder"
|
||||||
requests = "^2.25.1"
|
"Documentation" = "https://github.com/ihmily/DouyinLiveRecorder"
|
||||||
PyExecJS = "^1.5.1"
|
"Repository" = "https://github.com/ihmily/DouyinLiveRecorder"
|
||||||
loguru = "^0.5.3"
|
"Issues" = "https://github.com/ihmily/DouyinLiveRecorder/issues"
|
||||||
pycryptodome = "^3.10.1"
|
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
requests
|
requests>=2.31.0
|
||||||
PyExecJS
|
loguru>=0.7.3
|
||||||
loguru==0.7.2
|
pycryptodome>=3.20.0
|
||||||
pycryptodome==3.20.0
|
distro>=1.9.0
|
||||||
|
tqdm>=4.67.1
|
||||||
|
httpx[http2]>=0.28.1
|
||||||
|
PyExecJS>=1.5.1
|
||||||
30
setup.py
30
setup.py
@ -1,30 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from setuptools import setup, find_packages
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='douyinliverecorder',
|
|
||||||
version='3.0.8',
|
|
||||||
author='Hmily',
|
|
||||||
description='An easy tool for recording live streams',
|
|
||||||
long_description=open('README.md', encoding='utf-8').read(),
|
|
||||||
long_description_content_type='text/markdown',
|
|
||||||
url='https://github.com/ihmily/DouyinLiveRecorder',
|
|
||||||
packages=find_packages(),
|
|
||||||
install_requires=[
|
|
||||||
'requests',
|
|
||||||
'PyExecJS',
|
|
||||||
'loguru',
|
|
||||||
'pycryptodome'
|
|
||||||
],
|
|
||||||
classifiers=[
|
|
||||||
'Development Status :: 3 - Alpha',
|
|
||||||
'Intended Audience :: Developers',
|
|
||||||
'Programming Language :: Python :: 3',
|
|
||||||
'Programming Language :: Python :: 3 :: Only',
|
|
||||||
'Programming Language :: Python :: 3.8',
|
|
||||||
'Programming Language :: Python :: 3.9',
|
|
||||||
'Programming Language :: Python :: 3.10',
|
|
||||||
'Programming Language :: Python :: 3.11',
|
|
||||||
'Programming Language :: Python :: 3.12',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
14
src/__init__.py
Normal file
14
src/__init__.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from .initializer import check_node
|
||||||
|
|
||||||
|
current_file_path = Path(__file__).resolve()
|
||||||
|
current_dir = current_file_path.parent
|
||||||
|
JS_SCRIPT_PATH = current_dir / 'javascript'
|
||||||
|
|
||||||
|
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
|
||||||
|
node_execute_dir = Path(execute_dir) / 'node'
|
||||||
|
current_env_path = os.environ.get('PATH')
|
||||||
|
os.environ['PATH'] = str(node_execute_dir) + os.pathsep + current_env_path
|
||||||
|
check_node()
|
||||||
454
src/ab_sign.py
Normal file
454
src/ab_sign.py
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def rc4_encrypt(plaintext: str, key: str) -> str:
|
||||||
|
# 初始化状态数组
|
||||||
|
s = list(range(256))
|
||||||
|
|
||||||
|
# 使用密钥对状态数组进行置换
|
||||||
|
j = 0
|
||||||
|
for i in range(256):
|
||||||
|
j = (j + s[i] + ord(key[i % len(key)])) % 256
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
|
||||||
|
# 生成密钥流并加密
|
||||||
|
i = j = 0
|
||||||
|
result = []
|
||||||
|
for char in plaintext:
|
||||||
|
i = (i + 1) % 256
|
||||||
|
j = (j + s[i]) % 256
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
t = (s[i] + s[j]) % 256
|
||||||
|
result.append(chr(s[t] ^ ord(char)))
|
||||||
|
|
||||||
|
return ''.join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def left_rotate(x: int, n: int) -> int:
|
||||||
|
n %= 32
|
||||||
|
return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def get_t_j(j: int) -> int:
|
||||||
|
if 0 <= j < 16:
|
||||||
|
return 2043430169 # 0x79CC4519
|
||||||
|
elif 16 <= j < 64:
|
||||||
|
return 2055708042 # 0x7A879D8A
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid j for constant Tj")
|
||||||
|
|
||||||
|
|
||||||
|
def ff_j(j: int, x: int, y: int, z: int) -> int:
|
||||||
|
if 0 <= j < 16:
|
||||||
|
return (x ^ y ^ z) & 0xFFFFFFFF
|
||||||
|
elif 16 <= j < 64:
|
||||||
|
return ((x & y) | (x & z) | (y & z)) & 0xFFFFFFFF
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid j for bool function FF")
|
||||||
|
|
||||||
|
|
||||||
|
def gg_j(j: int, x: int, y: int, z: int) -> int:
|
||||||
|
if 0 <= j < 16:
|
||||||
|
return (x ^ y ^ z) & 0xFFFFFFFF
|
||||||
|
elif 16 <= j < 64:
|
||||||
|
return ((x & y) | (~x & z)) & 0xFFFFFFFF
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid j for bool function GG")
|
||||||
|
|
||||||
|
|
||||||
|
class SM3:
|
||||||
|
def __init__(self):
|
||||||
|
self.reg = []
|
||||||
|
self.chunk = []
|
||||||
|
self.size = 0
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
# 初始化寄存器值 - 修正为与JS版本相同的值
|
||||||
|
self.reg = [
|
||||||
|
1937774191, 1226093241, 388252375, 3666478592,
|
||||||
|
2842636476, 372324522, 3817729613, 2969243214
|
||||||
|
]
|
||||||
|
self.chunk = []
|
||||||
|
self.size = 0
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
# 将输入转换为字节数组
|
||||||
|
if isinstance(data, str):
|
||||||
|
# 直接转换为UTF-8字节列表
|
||||||
|
a = list(data.encode('utf-8'))
|
||||||
|
else:
|
||||||
|
a = data
|
||||||
|
|
||||||
|
self.size += len(a)
|
||||||
|
f = 64 - len(self.chunk)
|
||||||
|
|
||||||
|
if len(a) < f:
|
||||||
|
# 如果数据长度小于剩余空间,直接添加
|
||||||
|
self.chunk.extend(a)
|
||||||
|
else:
|
||||||
|
# 否则分块处理
|
||||||
|
self.chunk.extend(a[:f])
|
||||||
|
|
||||||
|
while len(self.chunk) >= 64:
|
||||||
|
self._compress(self.chunk)
|
||||||
|
if f < len(a):
|
||||||
|
self.chunk = a[f:min(f + 64, len(a))]
|
||||||
|
else:
|
||||||
|
self.chunk = []
|
||||||
|
f += 64
|
||||||
|
|
||||||
|
def _fill(self):
|
||||||
|
# 计算比特长度
|
||||||
|
bit_length = 8 * self.size
|
||||||
|
|
||||||
|
# 添加填充位
|
||||||
|
padding_pos = len(self.chunk)
|
||||||
|
self.chunk.append(0x80)
|
||||||
|
padding_pos = (padding_pos + 1) % 64
|
||||||
|
|
||||||
|
# 如果剩余空间不足8字节,则填充到下一个块
|
||||||
|
if 64 - padding_pos < 8:
|
||||||
|
padding_pos -= 64
|
||||||
|
|
||||||
|
# 填充0直到剩余8字节用于存储长度
|
||||||
|
while padding_pos < 56:
|
||||||
|
self.chunk.append(0)
|
||||||
|
padding_pos += 1
|
||||||
|
|
||||||
|
# 添加消息长度(高32位)
|
||||||
|
high_bits = bit_length // 4294967296
|
||||||
|
for i in range(4):
|
||||||
|
self.chunk.append((high_bits >> (8 * (3 - i))) & 0xFF)
|
||||||
|
|
||||||
|
# 添加消息长度(低32位)
|
||||||
|
for i in range(4):
|
||||||
|
self.chunk.append((bit_length >> (8 * (3 - i))) & 0xFF)
|
||||||
|
|
||||||
|
def _compress(self, data):
|
||||||
|
if len(data) < 64:
|
||||||
|
raise ValueError("compress error: not enough data")
|
||||||
|
else:
|
||||||
|
# 消息扩展
|
||||||
|
w = [0] * 132
|
||||||
|
|
||||||
|
# 将字节数组转换为字
|
||||||
|
for t in range(16):
|
||||||
|
w[t] = (data[4 * t] << 24) | (data[4 * t + 1] << 16) | (data[4 * t + 2] << 8) | data[4 * t + 3]
|
||||||
|
w[t] &= 0xFFFFFFFF
|
||||||
|
|
||||||
|
# 消息扩展
|
||||||
|
for j in range(16, 68):
|
||||||
|
a = w[j - 16] ^ w[j - 9] ^ left_rotate(w[j - 3], 15)
|
||||||
|
a = a ^ left_rotate(a, 15) ^ left_rotate(a, 23)
|
||||||
|
w[j] = (a ^ left_rotate(w[j - 13], 7) ^ w[j - 6]) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
# 计算w'
|
||||||
|
for j in range(64):
|
||||||
|
w[j + 68] = (w[j] ^ w[j + 4]) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
# 压缩
|
||||||
|
a, b, c, d, e, f, g, h = self.reg
|
||||||
|
|
||||||
|
for j in range(64):
|
||||||
|
ss1 = left_rotate((left_rotate(a, 12) + e + left_rotate(get_t_j(j), j)) & 0xFFFFFFFF, 7)
|
||||||
|
ss2 = ss1 ^ left_rotate(a, 12)
|
||||||
|
tt1 = (ff_j(j, a, b, c) + d + ss2 + w[j + 68]) & 0xFFFFFFFF
|
||||||
|
tt2 = (gg_j(j, e, f, g) + h + ss1 + w[j]) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
d = c
|
||||||
|
c = left_rotate(b, 9)
|
||||||
|
b = a
|
||||||
|
a = tt1
|
||||||
|
h = g
|
||||||
|
g = left_rotate(f, 19)
|
||||||
|
f = e
|
||||||
|
e = (tt2 ^ left_rotate(tt2, 9) ^ left_rotate(tt2, 17)) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
# 更新寄存器
|
||||||
|
self.reg[0] ^= a
|
||||||
|
self.reg[1] ^= b
|
||||||
|
self.reg[2] ^= c
|
||||||
|
self.reg[3] ^= d
|
||||||
|
self.reg[4] ^= e
|
||||||
|
self.reg[5] ^= f
|
||||||
|
self.reg[6] ^= g
|
||||||
|
self.reg[7] ^= h
|
||||||
|
|
||||||
|
def sum(self, data=None, output_format=None):
|
||||||
|
"""
|
||||||
|
计算哈希值
|
||||||
|
"""
|
||||||
|
# 如果提供了输入,则重置并写入
|
||||||
|
if data is not None:
|
||||||
|
self.reset()
|
||||||
|
self.write(data)
|
||||||
|
|
||||||
|
self._fill()
|
||||||
|
|
||||||
|
# 分块压缩
|
||||||
|
for f in range(0, len(self.chunk), 64):
|
||||||
|
self._compress(self.chunk[f:f + 64])
|
||||||
|
|
||||||
|
if output_format == 'hex':
|
||||||
|
# 十六进制输出
|
||||||
|
result = ''.join(f'{val:08x}' for val in self.reg)
|
||||||
|
else:
|
||||||
|
# 字节数组输出
|
||||||
|
result = []
|
||||||
|
for f in range(8):
|
||||||
|
c = self.reg[f]
|
||||||
|
result.append((c >> 24) & 0xFF)
|
||||||
|
result.append((c >> 16) & 0xFF)
|
||||||
|
result.append((c >> 8) & 0xFF)
|
||||||
|
result.append(c & 0xFF)
|
||||||
|
|
||||||
|
self.reset()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def result_encrypt(long_str: str, num: str | None = None) -> str:
|
||||||
|
# 魔改base64编码表
|
||||||
|
encoding_tables = {
|
||||||
|
"s0": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
|
||||||
|
"s1": "Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=",
|
||||||
|
"s2": "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=",
|
||||||
|
"s3": "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe",
|
||||||
|
"s4": "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 位移常量
|
||||||
|
masks = [16515072, 258048, 4032, 63] # 对应 0, 1, 2 的掩码,添加63作为第四个掩码
|
||||||
|
shifts = [18, 12, 6, 0] # 对应的位移量
|
||||||
|
|
||||||
|
encoding_table = encoding_tables[num]
|
||||||
|
|
||||||
|
result = ""
|
||||||
|
round_num = 0
|
||||||
|
long_int = get_long_int(round_num, long_str)
|
||||||
|
|
||||||
|
total_chars = math.ceil(len(long_str) / 3 * 4)
|
||||||
|
|
||||||
|
for i in range(total_chars):
|
||||||
|
# 每4个字符处理一组3字节
|
||||||
|
if i // 4 != round_num:
|
||||||
|
round_num += 1
|
||||||
|
long_int = get_long_int(round_num, long_str)
|
||||||
|
|
||||||
|
# 计算当前位置的索引
|
||||||
|
index = i % 4
|
||||||
|
|
||||||
|
# 使用掩码和位移提取6位值
|
||||||
|
char_index = (long_int & masks[index]) >> shifts[index]
|
||||||
|
|
||||||
|
result += encoding_table[char_index]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_long_int(round_num: int, long_str: str) -> int:
|
||||||
|
round_num = round_num * 3
|
||||||
|
|
||||||
|
# 获取字符串中的字符,如果超出范围则使用0
|
||||||
|
char1 = ord(long_str[round_num]) if round_num < len(long_str) else 0
|
||||||
|
char2 = ord(long_str[round_num + 1]) if round_num + 1 < len(long_str) else 0
|
||||||
|
char3 = ord(long_str[round_num + 2]) if round_num + 2 < len(long_str) else 0
|
||||||
|
|
||||||
|
return (char1 << 16) | (char2 << 8) | char3
|
||||||
|
|
||||||
|
|
||||||
|
def gener_random(random_num: int, option: list[int]) -> list[int]:
|
||||||
|
byte1 = random_num & 255
|
||||||
|
byte2 = (random_num >> 8) & 255
|
||||||
|
|
||||||
|
return [
|
||||||
|
(byte1 & 170) | (option[0] & 85), # 偶数位与option[0]的奇数位合并
|
||||||
|
(byte1 & 85) | (option[0] & 170), # 奇数位与option[0]的偶数位合并
|
||||||
|
(byte2 & 170) | (option[1] & 85), # 偶数位与option[1]的奇数位合并
|
||||||
|
(byte2 & 85) | (option[1] & 170), # 奇数位与option[1]的偶数位合并
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_random_str() -> str:
|
||||||
|
"""
|
||||||
|
生成随机字符串
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
随机字符串
|
||||||
|
"""
|
||||||
|
# 使用与JS版本相同的固定随机值
|
||||||
|
random_values = [0.123456789, 0.987654321, 0.555555555]
|
||||||
|
|
||||||
|
# 生成三组随机字节并合并
|
||||||
|
random_bytes = []
|
||||||
|
random_bytes.extend(gener_random(int(random_values[0] * 10000), [3, 45]))
|
||||||
|
random_bytes.extend(gener_random(int(random_values[1] * 10000), [1, 0]))
|
||||||
|
random_bytes.extend(gener_random(int(random_values[2] * 10000), [1, 5]))
|
||||||
|
|
||||||
|
return ''.join(chr(b) for b in random_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_rc4_bb_str(url_search_params: str, user_agent: str, window_env_str: str,
|
||||||
|
suffix: str = "cus", arguments: list[int] | None = None) -> str:
|
||||||
|
if arguments is None:
|
||||||
|
arguments = [0, 1, 14]
|
||||||
|
|
||||||
|
sm3 = SM3()
|
||||||
|
start_time = int(time.time() * 1000)
|
||||||
|
|
||||||
|
# 三次加密处理
|
||||||
|
# 1: url_search_params两次sm3之的结果
|
||||||
|
url_search_params_list = sm3.sum(sm3.sum(url_search_params + suffix))
|
||||||
|
# 2: 对后缀两次sm3之的结果
|
||||||
|
cus = sm3.sum(sm3.sum(suffix))
|
||||||
|
# 3: 对ua处理之后的结果
|
||||||
|
ua_key = chr(0) + chr(1) + chr(14) # [1/256, 1, 14]
|
||||||
|
ua = sm3.sum(result_encrypt(
|
||||||
|
rc4_encrypt(user_agent, ua_key),
|
||||||
|
"s3"
|
||||||
|
))
|
||||||
|
|
||||||
|
end_time = start_time + 100
|
||||||
|
|
||||||
|
# 构建配置对象
|
||||||
|
b = {
|
||||||
|
8: 3,
|
||||||
|
10: end_time,
|
||||||
|
15: {
|
||||||
|
"aid": 6383,
|
||||||
|
"pageId": 110624,
|
||||||
|
"boe": False,
|
||||||
|
"ddrt": 7,
|
||||||
|
"paths": {
|
||||||
|
"include": [{} for _ in range(7)],
|
||||||
|
"exclude": []
|
||||||
|
},
|
||||||
|
"track": {
|
||||||
|
"mode": 0,
|
||||||
|
"delay": 300,
|
||||||
|
"paths": []
|
||||||
|
},
|
||||||
|
"dump": True,
|
||||||
|
"rpU": "hwj"
|
||||||
|
},
|
||||||
|
16: start_time,
|
||||||
|
18: 44,
|
||||||
|
19: [1, 0, 1, 5],
|
||||||
|
}
|
||||||
|
|
||||||
|
def split_to_bytes(num: int) -> list[int]:
|
||||||
|
return [
|
||||||
|
(num >> 24) & 255,
|
||||||
|
(num >> 16) & 255,
|
||||||
|
(num >> 8) & 255,
|
||||||
|
num & 255
|
||||||
|
]
|
||||||
|
|
||||||
|
# 处理时间戳
|
||||||
|
start_time_bytes = split_to_bytes(b[16])
|
||||||
|
b[20] = start_time_bytes[0]
|
||||||
|
b[21] = start_time_bytes[1]
|
||||||
|
b[22] = start_time_bytes[2]
|
||||||
|
b[23] = start_time_bytes[3]
|
||||||
|
b[24] = int(b[16] / 256 / 256 / 256 / 256) & 255
|
||||||
|
b[25] = int(b[16] / 256 / 256 / 256 / 256 / 256) & 255
|
||||||
|
|
||||||
|
# 处理Arguments参数
|
||||||
|
arg0_bytes = split_to_bytes(arguments[0])
|
||||||
|
b[26] = arg0_bytes[0]
|
||||||
|
b[27] = arg0_bytes[1]
|
||||||
|
b[28] = arg0_bytes[2]
|
||||||
|
b[29] = arg0_bytes[3]
|
||||||
|
|
||||||
|
b[30] = int(arguments[1] / 256) & 255
|
||||||
|
b[31] = (arguments[1] % 256) & 255
|
||||||
|
|
||||||
|
arg1_bytes = split_to_bytes(arguments[1])
|
||||||
|
b[32] = arg1_bytes[0]
|
||||||
|
b[33] = arg1_bytes[1]
|
||||||
|
|
||||||
|
arg2_bytes = split_to_bytes(arguments[2])
|
||||||
|
b[34] = arg2_bytes[0]
|
||||||
|
b[35] = arg2_bytes[1]
|
||||||
|
b[36] = arg2_bytes[2]
|
||||||
|
b[37] = arg2_bytes[3]
|
||||||
|
|
||||||
|
# 处理加密结果
|
||||||
|
b[38] = url_search_params_list[21]
|
||||||
|
b[39] = url_search_params_list[22]
|
||||||
|
b[40] = cus[21]
|
||||||
|
b[41] = cus[22]
|
||||||
|
b[42] = ua[23]
|
||||||
|
b[43] = ua[24]
|
||||||
|
|
||||||
|
# 处理结束时间
|
||||||
|
end_time_bytes = split_to_bytes(b[10])
|
||||||
|
b[44] = end_time_bytes[0]
|
||||||
|
b[45] = end_time_bytes[1]
|
||||||
|
b[46] = end_time_bytes[2]
|
||||||
|
b[47] = end_time_bytes[3]
|
||||||
|
b[48] = b[8]
|
||||||
|
b[49] = int(b[10] / 256 / 256 / 256 / 256) & 255
|
||||||
|
b[50] = int(b[10] / 256 / 256 / 256 / 256 / 256) & 255
|
||||||
|
|
||||||
|
# 处理配置项
|
||||||
|
b[51] = b[15]['pageId']
|
||||||
|
|
||||||
|
page_id_bytes = split_to_bytes(b[15]['pageId'])
|
||||||
|
b[52] = page_id_bytes[0]
|
||||||
|
b[53] = page_id_bytes[1]
|
||||||
|
b[54] = page_id_bytes[2]
|
||||||
|
b[55] = page_id_bytes[3]
|
||||||
|
|
||||||
|
b[56] = b[15]['aid']
|
||||||
|
b[57] = b[15]['aid'] & 255
|
||||||
|
b[58] = (b[15]['aid'] >> 8) & 255
|
||||||
|
b[59] = (b[15]['aid'] >> 16) & 255
|
||||||
|
b[60] = (b[15]['aid'] >> 24) & 255
|
||||||
|
|
||||||
|
# 处理环境信息
|
||||||
|
window_env_list = [ord(char) for char in window_env_str]
|
||||||
|
b[64] = len(window_env_list)
|
||||||
|
b[65] = b[64] & 255
|
||||||
|
b[66] = (b[64] >> 8) & 255
|
||||||
|
|
||||||
|
b[69] = 0
|
||||||
|
b[70] = 0
|
||||||
|
b[71] = 0
|
||||||
|
|
||||||
|
# 计算校验和
|
||||||
|
b[72] = b[18] ^ b[20] ^ b[26] ^ b[30] ^ b[38] ^ b[40] ^ b[42] ^ b[21] ^ b[27] ^ b[31] ^ \
|
||||||
|
b[35] ^ b[39] ^ b[41] ^ b[43] ^ b[22] ^ b[28] ^ b[32] ^ b[36] ^ b[23] ^ b[29] ^ \
|
||||||
|
b[33] ^ b[37] ^ b[44] ^ b[45] ^ b[46] ^ b[47] ^ b[48] ^ b[49] ^ b[50] ^ b[24] ^ \
|
||||||
|
b[25] ^ b[52] ^ b[53] ^ b[54] ^ b[55] ^ b[57] ^ b[58] ^ b[59] ^ b[60] ^ b[65] ^ \
|
||||||
|
b[66] ^ b[70] ^ b[71]
|
||||||
|
|
||||||
|
# 构建最终字节数组
|
||||||
|
bb = [
|
||||||
|
b[18], b[20], b[52], b[26], b[30], b[34], b[58], b[38], b[40], b[53], b[42], b[21],
|
||||||
|
b[27], b[54], b[55], b[31], b[35], b[57], b[39], b[41], b[43], b[22], b[28], b[32],
|
||||||
|
b[60], b[36], b[23], b[29], b[33], b[37], b[44], b[45], b[59], b[46], b[47], b[48],
|
||||||
|
b[49], b[50], b[24], b[25], b[65], b[66], b[70], b[71]
|
||||||
|
]
|
||||||
|
bb.extend(window_env_list)
|
||||||
|
bb.append(b[72])
|
||||||
|
|
||||||
|
return rc4_encrypt(
|
||||||
|
''.join(chr(byte) for byte in bb),
|
||||||
|
chr(121)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ab_sign(url_search_params: str, user_agent: str) -> str:
|
||||||
|
window_env_str = "1920|1080|1920|1040|0|30|0|0|1872|92|1920|1040|1857|92|1|24|Win32"
|
||||||
|
|
||||||
|
# 1. 生成随机字符串前缀
|
||||||
|
# 2. 生成RC4加密的主体部分
|
||||||
|
# 3. 对结果进行最终加密并添加等号后缀
|
||||||
|
return result_encrypt(
|
||||||
|
generate_random_str() +
|
||||||
|
generate_rc4_bb_str(url_search_params, user_agent, window_env_str),
|
||||||
|
"s4"
|
||||||
|
) + "="
|
||||||
0
src/http_clients/__init__.py
Normal file
0
src/http_clients/__init__.py
Normal file
59
src/http_clients/async_http.py
Normal file
59
src/http_clients/async_http.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import httpx
|
||||||
|
from typing import Dict, Any
|
||||||
|
from .. import utils
|
||||||
|
|
||||||
|
OptionalStr = str | None
|
||||||
|
OptionalDict = Dict[str, Any] | None
|
||||||
|
|
||||||
|
|
||||||
|
async def async_req(
|
||||||
|
url: str,
|
||||||
|
proxy_addr: OptionalStr = None,
|
||||||
|
headers: OptionalDict = None,
|
||||||
|
data: dict | bytes | None = None,
|
||||||
|
json_data: dict | list | None = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
redirect_url: bool = False,
|
||||||
|
return_cookies: bool = False,
|
||||||
|
include_cookies: bool = False,
|
||||||
|
abroad: bool = False,
|
||||||
|
content_conding: str = 'utf-8',
|
||||||
|
verify: bool = False,
|
||||||
|
http2: bool = True
|
||||||
|
) -> OptionalDict | OptionalStr | tuple:
|
||||||
|
if headers is None:
|
||||||
|
headers = {}
|
||||||
|
try:
|
||||||
|
proxy_addr = utils.handle_proxy_addr(proxy_addr)
|
||||||
|
if data or json_data:
|
||||||
|
async with httpx.AsyncClient(proxy=proxy_addr, timeout=timeout, verify=verify, http2=http2) as client:
|
||||||
|
response = await client.post(url, data=data, json=json_data, headers=headers)
|
||||||
|
else:
|
||||||
|
async with httpx.AsyncClient(proxy=proxy_addr, timeout=timeout, verify=verify, http2=http2) as client:
|
||||||
|
response = await client.get(url, headers=headers, follow_redirects=True)
|
||||||
|
|
||||||
|
if redirect_url:
|
||||||
|
return str(response.url)
|
||||||
|
elif return_cookies:
|
||||||
|
cookies_dict = {name: value for name, value in response.cookies.items()}
|
||||||
|
return (response.text, cookies_dict) if include_cookies else cookies_dict
|
||||||
|
else:
|
||||||
|
resp_str = response.text
|
||||||
|
except Exception as e:
|
||||||
|
resp_str = str(e)
|
||||||
|
|
||||||
|
return resp_str
|
||||||
|
|
||||||
|
|
||||||
|
async def get_response_status(url: str, proxy_addr: OptionalStr = None, headers: OptionalDict = None,
|
||||||
|
timeout: int = 10, abroad: bool = False, verify: bool = False, http2=False) -> bool:
|
||||||
|
|
||||||
|
try:
|
||||||
|
proxy_addr = utils.handle_proxy_addr(proxy_addr)
|
||||||
|
async with httpx.AsyncClient(proxy=proxy_addr, timeout=timeout, verify=verify) as client:
|
||||||
|
response = await client.head(url, headers=headers, follow_redirects=True)
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
88
src/http_clients/sync_http.py
Normal file
88
src/http_clients/sync_http.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import gzip
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.error
|
||||||
|
import requests
|
||||||
|
import ssl
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
no_proxy_handler = urllib.request.ProxyHandler({})
|
||||||
|
opener = urllib.request.build_opener(no_proxy_handler)
|
||||||
|
|
||||||
|
ssl_context = ssl.create_default_context()
|
||||||
|
ssl_context.check_hostname = False
|
||||||
|
ssl_context.verify_mode = ssl.CERT_NONE
|
||||||
|
OptionalStr = str | None
|
||||||
|
OptionalDict = dict | None
|
||||||
|
|
||||||
|
|
||||||
|
def sync_req(
|
||||||
|
url: str,
|
||||||
|
proxy_addr: OptionalStr = None,
|
||||||
|
headers: OptionalDict = None,
|
||||||
|
data: dict | bytes | None = None,
|
||||||
|
json_data: dict | list | None = None,
|
||||||
|
timeout: int = 20,
|
||||||
|
redirect_url: bool = False,
|
||||||
|
abroad: bool = False,
|
||||||
|
content_conding: str = 'utf-8'
|
||||||
|
) -> str:
|
||||||
|
if headers is None:
|
||||||
|
headers = {}
|
||||||
|
try:
|
||||||
|
if proxy_addr:
|
||||||
|
proxies = {
|
||||||
|
'http': proxy_addr,
|
||||||
|
'https': proxy_addr
|
||||||
|
}
|
||||||
|
if data or json_data:
|
||||||
|
response = requests.post(
|
||||||
|
url, data=data, json=json_data, headers=headers, proxies=proxies, timeout=timeout
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response = requests.get(url, headers=headers, proxies=proxies, timeout=timeout)
|
||||||
|
if redirect_url:
|
||||||
|
return response.url
|
||||||
|
resp_str = response.text
|
||||||
|
else:
|
||||||
|
if data and not isinstance(data, bytes):
|
||||||
|
data = urllib.parse.urlencode(data).encode(content_conding)
|
||||||
|
if json_data and isinstance(json_data, (dict, list)):
|
||||||
|
data = json.dumps(json_data).encode(content_conding)
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if abroad:
|
||||||
|
response = urllib.request.urlopen(req, timeout=timeout)
|
||||||
|
else:
|
||||||
|
response = opener.open(req, timeout=timeout)
|
||||||
|
if redirect_url:
|
||||||
|
return response.url
|
||||||
|
content_encoding = response.info().get('Content-Encoding')
|
||||||
|
try:
|
||||||
|
if content_encoding == 'gzip':
|
||||||
|
with gzip.open(response, 'rt', encoding=content_conding) as gzipped:
|
||||||
|
resp_str = gzipped.read()
|
||||||
|
else:
|
||||||
|
resp_str = response.read().decode(content_conding)
|
||||||
|
finally:
|
||||||
|
response.close()
|
||||||
|
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code == 400:
|
||||||
|
resp_str = e.read().decode(content_conding)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
print(f"URL Error: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
resp_str = str(e)
|
||||||
|
|
||||||
|
return resp_str
|
||||||
220
src/initializer.py
Normal file
220
src/initializer.py
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Author: Hmily
|
||||||
|
GitHub:https://github.com/ihmily
|
||||||
|
Copyright (c) 2024 by Hmily, All Rights Reserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import platform
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
import requests
|
||||||
|
import re
|
||||||
|
import distro
|
||||||
|
from tqdm import tqdm
|
||||||
|
from .logger import logger
|
||||||
|
|
||||||
|
current_platform = platform.system()
|
||||||
|
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
|
||||||
|
current_env_path = os.environ.get('PATH')
|
||||||
|
|
||||||
|
|
||||||
|
def unzip_file(zip_path: str | Path, extract_to: str | Path, delete: bool = True) -> None:
|
||||||
|
if not os.path.exists(extract_to):
|
||||||
|
os.makedirs(extract_to)
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(extract_to)
|
||||||
|
|
||||||
|
if delete and os.path.exists(zip_path):
|
||||||
|
os.remove(zip_path)
|
||||||
|
|
||||||
|
|
||||||
|
def install_nodejs_windows():
|
||||||
|
try:
|
||||||
|
logger.warning("Node.js is not installed.")
|
||||||
|
logger.debug("Installing the stable version of Node.js for Windows...")
|
||||||
|
response = requests.get('https://nodejs.cn/download/')
|
||||||
|
if response.status_code == 200:
|
||||||
|
match = re.search('https://npmmirror.com/mirrors/node/(v.*?)/node-(v.*?)-x64.msi',
|
||||||
|
response.text)
|
||||||
|
if match:
|
||||||
|
version = match.group(1)
|
||||||
|
system_bit = 'x64' if '32' not in platform.machine() else 'x86'
|
||||||
|
url = f'https://npmmirror.com/mirrors/node/{version}/node-{version}-win-{system_bit}.zip'
|
||||||
|
else:
|
||||||
|
logger.error("Failed to retrieve the download URL for the latest version of Node.js...")
|
||||||
|
return
|
||||||
|
|
||||||
|
full_file_name = url.rsplit('/', maxsplit=1)[-1]
|
||||||
|
zip_file_path = Path(execute_dir) / full_file_name
|
||||||
|
|
||||||
|
if Path(zip_file_path).exists():
|
||||||
|
logger.debug("Node.js installation file already exists, start install...")
|
||||||
|
else:
|
||||||
|
response = requests.get(url, stream=True)
|
||||||
|
total_size = int(response.headers.get('Content-Length', 0))
|
||||||
|
block_size = 1024
|
||||||
|
|
||||||
|
with tqdm(total=total_size, unit="B", unit_scale=True,
|
||||||
|
ncols=100, desc=f'Downloading Node.js ({version})') as t:
|
||||||
|
with open(zip_file_path, 'wb') as f:
|
||||||
|
for data in response.iter_content(block_size):
|
||||||
|
t.update(len(data))
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
unzip_file(zip_file_path, execute_dir)
|
||||||
|
extract_dir_path = str(zip_file_path).rsplit('.', maxsplit=1)[0]
|
||||||
|
f_path, f_name = os.path.splitext(zip_file_path)
|
||||||
|
new_extract_dir_path = Path(f_path).parent / 'node'
|
||||||
|
if Path(extract_dir_path).exists() and not Path(new_extract_dir_path).exists():
|
||||||
|
os.rename(extract_dir_path, new_extract_dir_path)
|
||||||
|
os.environ['PATH'] = execute_dir + '/node' + os.pathsep + current_env_path
|
||||||
|
result = subprocess.run(["node", "-v"], capture_output=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.debug('Node.js installation was successful. Restart for changes to take effect')
|
||||||
|
else:
|
||||||
|
logger.debug('Node.js installation failed')
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Failed to retrieve the Node.js version page")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"type: {type(e).__name__}, Node.js installation failed {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def install_nodejs_centos():
|
||||||
|
try:
|
||||||
|
logger.warning("Node.js is not installed.")
|
||||||
|
logger.debug("Installing the latest version of Node.js for CentOS...")
|
||||||
|
result = subprocess.run('curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/nodesource/rpm/setup_lts.x | '
|
||||||
|
'bash -', shell=True, capture_output=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error("Failed to run NodeSource installation script")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = subprocess.run(['yum', 'install', '-y', 'epel-release'], capture_output=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error("Failed to install EPEL repository")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = subprocess.run(['yum', 'install', '-y', 'nodejs'], capture_output=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.debug('Node.js installation was successful. Restart for changes to take effect.')
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Node.js installation failed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"type: {type(e).__name__}, Node.js installation failed {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def install_nodejs_ubuntu():
|
||||||
|
try:
|
||||||
|
logger.warning("Node.js is not installed.")
|
||||||
|
logger.debug("Installing the latest version of Node.js for Ubuntu...")
|
||||||
|
install_script = 'curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -'
|
||||||
|
result = subprocess.run(install_script, shell=True, capture_output=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error("Failed to run NodeSource installation script")
|
||||||
|
return
|
||||||
|
|
||||||
|
install_command = ['apt', 'install', '-y', 'nodejs']
|
||||||
|
result = subprocess.run(install_command, capture_output=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.debug('Node.js installation was successful. Restart for changes to take effect.')
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Node.js installation failed")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"type: {type(e).__name__}, Node.js installation failed, {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def install_nodejs_mac():
|
||||||
|
logger.warning("Node.js is not installed.")
|
||||||
|
logger.debug("Installing the latest version of Node.js for macOS...")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["brew", "install", "node"], capture_output=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.debug('Node.js installation was successful. Restart for changes to take effect.')
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Node.js installation failed")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"Failed to install Node.js using Homebrew. {e}")
|
||||||
|
logger.error("Please install Node.js manually or check your Homebrew installation.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An unexpected error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_manager():
|
||||||
|
dist_id = distro.id()
|
||||||
|
if dist_id in ["centos", "fedora", "rhel", "amzn", "oracle", "scientific", "opencloudos", "alinux"]:
|
||||||
|
return "RHS"
|
||||||
|
else:
|
||||||
|
return "DBS"
|
||||||
|
|
||||||
|
|
||||||
|
def install_nodejs() -> bool:
|
||||||
|
if current_platform == "Windows":
|
||||||
|
return install_nodejs_windows()
|
||||||
|
elif current_platform == "Linux":
|
||||||
|
os_type = get_package_manager()
|
||||||
|
if os_type == "RHS":
|
||||||
|
return install_nodejs_centos()
|
||||||
|
else:
|
||||||
|
return install_nodejs_ubuntu()
|
||||||
|
elif current_platform == "Darwin":
|
||||||
|
return install_nodejs_mac()
|
||||||
|
else:
|
||||||
|
logger.debug(f"Node.js auto installation is not supported on this platform: {current_platform}. "
|
||||||
|
f"Please install Node.js manually.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_nodejs_installed(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['node', '-v'], capture_output=True)
|
||||||
|
version = result.stdout.strip()
|
||||||
|
if result.returncode == 0 and version:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wrapped_func(*args, **kwargs):
|
||||||
|
if sys.version_info >= (3, 7):
|
||||||
|
res = wrapper(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
res = wrapper(*args, **kwargs)
|
||||||
|
if not res:
|
||||||
|
install_nodejs()
|
||||||
|
res = wrapper(*args, **kwargs)
|
||||||
|
|
||||||
|
if not res:
|
||||||
|
raise RuntimeError("Node.js is not installed.")
|
||||||
|
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapped_func
|
||||||
|
|
||||||
|
|
||||||
|
def check_nodejs_installed() -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['node', '-v'], capture_output=True)
|
||||||
|
version = result.stdout.strip()
|
||||||
|
if result.returncode == 0 and version:
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_node() -> bool:
|
||||||
|
if not check_nodejs_installed():
|
||||||
|
return install_nodejs()
|
||||||
1
src/javascript/crypto-js.min.js
vendored
Normal file
1
src/javascript/crypto-js.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
539
src/javascript/haixiu.js
Normal file
539
src/javascript/haixiu.js
Normal file
@ -0,0 +1,539 @@
|
|||||||
|
var closeGeetest = !1, _a123 = "haija1c7", _b2x = "xiuhc2a6", _c3y = "anchc3a5", _dx34 = "famic7a2", _hf_constants1 = "sowh1e", _hf_constants2 = "1000ha", _hf_constants3 = "butr12", _hf_constants4 = "2000h5", _gf_constants1 = "lehaaj", _gf_constants2 = "1000ax", _gf_constants3 = "lehaData"
|
||||||
|
let CryptoJS = null;
|
||||||
|
function EnmoliParamter() {
|
||||||
|
|
||||||
|
this._a123 = eval("_hf_constants1"),
|
||||||
|
this._b2x = eval("_hf_constants2"),
|
||||||
|
this._c3y = eval("_hf_constants3"),
|
||||||
|
this._dx34 = eval("_hf_constants4"),
|
||||||
|
this.getA123 = function() {
|
||||||
|
return this._a123
|
||||||
|
}
|
||||||
|
,
|
||||||
|
this.getB2X = function() {
|
||||||
|
return this._b2x
|
||||||
|
}
|
||||||
|
,
|
||||||
|
this.getC3Y = function() {
|
||||||
|
return this._c3y
|
||||||
|
}
|
||||||
|
,
|
||||||
|
this.getDX34 = function() {
|
||||||
|
return this._dx34
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EnmoliParamter.prototype = {
|
||||||
|
aa: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return e;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e;
|
||||||
|
if (e.arity === t.arity && e.string === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
ac: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return e;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e;
|
||||||
|
if (e.id === t.id && (e = CryptoJS.MD5(e) + ""),
|
||||||
|
e.arity1 === t.arity && e.string2 === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
ad: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return !0;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e;
|
||||||
|
if (e.arity2 === t.arity && e.string3 === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
ae: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return !0;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e.number === t.number;
|
||||||
|
if (e.arity3 === t.arity && e.string4 === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
af: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return !0;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e.number === t.number;
|
||||||
|
if (e.arity4 === t.arity && e.string5 === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
ah: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return !0;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e;
|
||||||
|
if (e.arity6 === t.arity && e.string9 === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
ai: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return !0;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e;
|
||||||
|
if (e.arity2 === t.arity5 && e.string === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
aj: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return !0;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e.number === t.number;
|
||||||
|
if (e.arity44 === t.arity42 && e.string === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
ak: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return !0;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e.number === t.number;
|
||||||
|
if (e.arity21 === t.arity322 && e.string === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
ax: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return !0;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e.number === t.number;
|
||||||
|
if (e.arity22 === t.arity32 && e.string === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
az: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return !0;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e.number === t.number;
|
||||||
|
if (e.arity42 === t.arity57 && e.string2 === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
are_similar: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return !0;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return !0;
|
||||||
|
return !0
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return e;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e.number === t.number;
|
||||||
|
if (e.arity === t.arity && e.string === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third);
|
||||||
|
case "function":
|
||||||
|
case "regexp":
|
||||||
|
return e;
|
||||||
|
default:
|
||||||
|
return !0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e.second.string === t.second.string && "(string)" === t.second.id;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return e.second.string === t.second.string && "(string)" === e.second.id
|
||||||
|
}
|
||||||
|
return !1
|
||||||
|
},
|
||||||
|
ayz: function(e, t) {
|
||||||
|
if (e === t)
|
||||||
|
return e;
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (Array.isArray(t) && e.length === t.length) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < e.length; i += 1)
|
||||||
|
if (!this.are_similar(e[i], t[i]))
|
||||||
|
return e;
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if (Array.isArray(t))
|
||||||
|
return t;
|
||||||
|
if ("(number)" === e.id && "(number)" === t.id)
|
||||||
|
return e.number === t.number;
|
||||||
|
if (e.arity42 === t.arity57 && e.string2 === t.string)
|
||||||
|
switch (e.arity) {
|
||||||
|
case "prefix":
|
||||||
|
case "suffix":
|
||||||
|
case "infix":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
|
||||||
|
case "ternary":
|
||||||
|
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ("." === e.id && "[" === t.id && "infix" === t.arity)
|
||||||
|
return e;
|
||||||
|
if ("[" === e.id && "infix" === e.arity && "." === t.id)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return this.getA123().substring(4) + this.getB2X().substring(4) + this.getC3Y().substring(4) + this.getDX34().substring(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function EnmoliSubmiter() {}
|
||||||
|
EnmoliSubmiter.prototype = {
|
||||||
|
bsq: function(e) {
|
||||||
|
var t = this.pf(e)
|
||||||
|
, i = this.as(t);
|
||||||
|
return this.brm(i)
|
||||||
|
},
|
||||||
|
pf: function(e) {
|
||||||
|
var t = {};
|
||||||
|
for (var i in e)
|
||||||
|
"" !== e[i] && (t[i] = e[i]);
|
||||||
|
return t
|
||||||
|
},
|
||||||
|
as: function(e) {
|
||||||
|
for (var t = {}, i = Object.keys(e).sort(), o = 0; o < i.length; o++) {
|
||||||
|
var n = i[o];
|
||||||
|
t[n] = e[n]
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
},
|
||||||
|
brm: function(e) {
|
||||||
|
var t = this.cls(e)
|
||||||
|
, i = new EnmoliParamter;
|
||||||
|
return this.pt(t, i.ayz(t, "showselfAnchorVisitorParameters"))
|
||||||
|
},
|
||||||
|
cls: function(e) {
|
||||||
|
var t = "";
|
||||||
|
for (var i in e)
|
||||||
|
t = t + i + "=" + e[i] + "&";
|
||||||
|
return t = t.substring(0, t.length - 1)
|
||||||
|
},
|
||||||
|
pt: function(e, t) {
|
||||||
|
var i = new EnmoliParamter;
|
||||||
|
return e += t,
|
||||||
|
i.az(i.ax(i.ak(i.aj(i.ai(i.ah(i.af(i.ae(i.ad(i.ac(i.aa(e, e + "01" + t), e + "escape" + t), e + "same"), e + "visitor"), "anchor"), e + "person"), e + "ax" + t), "ae" + t), e + "ax" + t), e + "inspect" + t), "af" + t)
|
||||||
|
},
|
||||||
|
bnu: function(e, t) {
|
||||||
|
for (var i = e.split("&"), o = 0; o < i.length; o++) {
|
||||||
|
var n = i[o].split("=");
|
||||||
|
2 == n.length && (t[n[0]] = encodeURIComponent($.trim(n[1])).toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bn: function(e, t) {
|
||||||
|
for (var i in e)
|
||||||
|
"object" == typeof e[i] ? t[i] = encodeURIComponent($.trim(JSON.stringify(e[i]))).toString() : t[i] = encodeURIComponent($.trim(e[i])).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var enmoliSubmiter = new EnmoliSubmiter();
|
||||||
|
|
||||||
|
function sign(options, cryptoJSPath){
|
||||||
|
CryptoJS = require(cryptoJSPath);
|
||||||
|
return enmoliSubmiter.bsq(options);
|
||||||
|
}
|
||||||
|
module.exports = {
|
||||||
|
sign
|
||||||
|
};
|
||||||
|
|
||||||
|
// const options = {
|
||||||
|
// "accessToken": "pLXSC%252FXJ0asc1I21tVL5FYZhNJn2Zg6d7m94umCnpgL%252BuVm31GQvyw%253D%253D",
|
||||||
|
// "tku": "3000006",
|
||||||
|
// "c": "10138100100000",
|
||||||
|
// "_st1": "1728621076958"
|
||||||
|
// }
|
||||||
|
// const cryptoJSPath = './crypto-js.min.js'
|
||||||
|
// console.log(sign(options, cryptoJSPath))
|
||||||
33
src/javascript/laixiu.js
Normal file
33
src/javascript/laixiu.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
function generateUUID() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateSign() {
|
||||||
|
const a = new Date().getTime();
|
||||||
|
const s = generateUUID().replace(/-/g, "");
|
||||||
|
const u = 'kk792f28d6ff1f34ec702c08626d454b39pro';
|
||||||
|
|
||||||
|
const input = "web" + s + a + u;
|
||||||
|
|
||||||
|
const hash = CryptoJS.MD5(input).toString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: a,
|
||||||
|
imei: s,
|
||||||
|
requestId: hash,
|
||||||
|
inputString: input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sign(cryptoJSPath) {
|
||||||
|
CryptoJS = require(cryptoJSPath);
|
||||||
|
return calculateSign();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sign
|
||||||
|
};
|
||||||
425
src/javascript/liveme.js
Normal file
425
src/javascript/liveme.js
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
/**
|
||||||
|
* @author Hmily
|
||||||
|
* @createTime 2024-10-10
|
||||||
|
*/
|
||||||
|
|
||||||
|
const id = 1;
|
||||||
|
const r = `${new Date().getTime()}${id}`
|
||||||
|
const Am = "LM6000101139961122666757";
|
||||||
|
const rl = "undefined"
|
||||||
|
|
||||||
|
function createRandom(length = 32) {
|
||||||
|
let result = "";
|
||||||
|
const characters = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
|
||||||
|
for (let i = 0; i < length; ++i) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||||
|
result += characters.charAt(randomIndex);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSignature(input = "4l4m5") {
|
||||||
|
let signature = "";
|
||||||
|
let number = 0;
|
||||||
|
for (let i = 0; i < input.length; ++i) {
|
||||||
|
const charCode = input.charCodeAt(i);
|
||||||
|
if (charCode >= 48 && charCode <= 57) {
|
||||||
|
number = number * 10 + (charCode - 48);
|
||||||
|
} else {
|
||||||
|
if (number !== 0) {
|
||||||
|
signature += createRandom(number);
|
||||||
|
number = 0;
|
||||||
|
}
|
||||||
|
signature += String.fromCharCode(charCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (number !== 0) {
|
||||||
|
signature += createRandom(number);
|
||||||
|
}
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
function oC(e) {
|
||||||
|
return e && e.__esModule && Object.prototype.hasOwnProperty.call(e, "default") ? e.default : e
|
||||||
|
}
|
||||||
|
var Tm = {
|
||||||
|
exports: {}
|
||||||
|
}
|
||||||
|
, Sm = {
|
||||||
|
exports: {}
|
||||||
|
};
|
||||||
|
(function() {
|
||||||
|
var e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
, t = {
|
||||||
|
rotl: function(n, r) {
|
||||||
|
return n << r | n >>> 32 - r
|
||||||
|
},
|
||||||
|
rotr: function(n, r) {
|
||||||
|
return n << 32 - r | n >>> r
|
||||||
|
},
|
||||||
|
endian: function(n) {
|
||||||
|
if (n.constructor == Number)
|
||||||
|
return t.rotl(n, 8) & 16711935 | t.rotl(n, 24) & 4278255360;
|
||||||
|
for (var r = 0; r < n.length; r++)
|
||||||
|
n[r] = t.endian(n[r]);
|
||||||
|
return n
|
||||||
|
},
|
||||||
|
randomBytes: function(n) {
|
||||||
|
for (var r = []; n > 0; n--)
|
||||||
|
r.push(Math.floor(Math.random() * 256));
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
bytesToWords: function(n) {
|
||||||
|
for (var r = [], s = 0, o = 0; s < n.length; s++,
|
||||||
|
o += 8)
|
||||||
|
r[o >>> 5] |= n[s] << 24 - o % 32;
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
wordsToBytes: function(n) {
|
||||||
|
for (var r = [], s = 0; s < n.length * 32; s += 8)
|
||||||
|
r.push(n[s >>> 5] >>> 24 - s % 32 & 255);
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
bytesToHex: function(n) {
|
||||||
|
for (var r = [], s = 0; s < n.length; s++)
|
||||||
|
r.push((n[s] >>> 4).toString(16)),
|
||||||
|
r.push((n[s] & 15).toString(16));
|
||||||
|
return r.join("")
|
||||||
|
},
|
||||||
|
hexToBytes: function(n) {
|
||||||
|
for (var r = [], s = 0; s < n.length; s += 2)
|
||||||
|
r.push(parseInt(n.substr(s, 2), 16));
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
bytesToBase64: function(n) {
|
||||||
|
for (var r = [], s = 0; s < n.length; s += 3)
|
||||||
|
for (var o = n[s] << 16 | n[s + 1] << 8 | n[s + 2], i = 0; i < 4; i++)
|
||||||
|
s * 8 + i * 6 <= n.length * 8 ? r.push(e.charAt(o >>> 6 * (3 - i) & 63)) : r.push("=");
|
||||||
|
return r.join("")
|
||||||
|
},
|
||||||
|
base64ToBytes: function(n) {
|
||||||
|
n = n.replace(/[^A-Z0-9+\/]/ig, "");
|
||||||
|
for (var r = [], s = 0, o = 0; s < n.length; o = ++s % 4)
|
||||||
|
o != 0 && r.push((e.indexOf(n.charAt(s - 1)) & Math.pow(2, -2 * o + 8) - 1) << o * 2 | e.indexOf(n.charAt(s)) >>> 6 - o * 2);
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Sm.exports = t
|
||||||
|
}
|
||||||
|
)();
|
||||||
|
var iC = Sm.exports
|
||||||
|
, nl = {
|
||||||
|
utf8: {
|
||||||
|
stringToBytes: function(e) {
|
||||||
|
return nl.bin.stringToBytes(unescape(encodeURIComponent(e)))
|
||||||
|
},
|
||||||
|
bytesToString: function(e) {
|
||||||
|
return decodeURIComponent(escape(nl.bin.bytesToString(e)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bin: {
|
||||||
|
stringToBytes: function(e) {
|
||||||
|
for (var t = [], n = 0; n < e.length; n++)
|
||||||
|
t.push(e.charCodeAt(n) & 255);
|
||||||
|
return t
|
||||||
|
},
|
||||||
|
bytesToString: function(e) {
|
||||||
|
for (var t = [], n = 0; n < e.length; n++)
|
||||||
|
t.push(String.fromCharCode(e[n]));
|
||||||
|
return t.join("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, sd = nl;
|
||||||
|
|
||||||
|
var aC = function(e) {
|
||||||
|
return e != null && (Cm(e) || lC(e) || !!e._isBuffer)
|
||||||
|
};
|
||||||
|
function Cm(e) {
|
||||||
|
return !!e.constructor && typeof e.constructor.isBuffer == "function" && e.constructor.isBuffer(e)
|
||||||
|
}
|
||||||
|
function lC(e) {
|
||||||
|
return typeof e.readFloatLE == "function" && typeof e.slice == "function" && Cm(e.slice(0, 0))
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
var e = iC
|
||||||
|
, t = sd.utf8
|
||||||
|
, n = aC
|
||||||
|
, r = sd.bin
|
||||||
|
, s = function(o, i) {
|
||||||
|
o.constructor == String ? i && i.encoding === "binary" ? o = r.stringToBytes(o) : o = t.stringToBytes(o) : n(o) ? o = Array.prototype.slice.call(o, 0) : !Array.isArray(o) && o.constructor !== Uint8Array && (o = o.toString());
|
||||||
|
for (var a = e.bytesToWords(o), l = o.length * 8, c = 1732584193, u = -271733879, f = -1732584194, d = 271733878, m = 0; m < a.length; m++)
|
||||||
|
a[m] = (a[m] << 8 | a[m] >>> 24) & 16711935 | (a[m] << 24 | a[m] >>> 8) & 4278255360;
|
||||||
|
a[l >>> 5] |= 128 << l % 32,
|
||||||
|
a[(l + 64 >>> 9 << 4) + 14] = l;
|
||||||
|
for (var v = s._ff, w = s._gg, R = s._hh, y = s._ii, m = 0; m < a.length; m += 16) {
|
||||||
|
var b = c
|
||||||
|
, _ = u
|
||||||
|
, g = f
|
||||||
|
, C = d;
|
||||||
|
c = v(c, u, f, d, a[m + 0], 7, -680876936),
|
||||||
|
d = v(d, c, u, f, a[m + 1], 12, -389564586),
|
||||||
|
f = v(f, d, c, u, a[m + 2], 17, 606105819),
|
||||||
|
u = v(u, f, d, c, a[m + 3], 22, -1044525330),
|
||||||
|
c = v(c, u, f, d, a[m + 4], 7, -176418897),
|
||||||
|
d = v(d, c, u, f, a[m + 5], 12, 1200080426),
|
||||||
|
f = v(f, d, c, u, a[m + 6], 17, -1473231341),
|
||||||
|
u = v(u, f, d, c, a[m + 7], 22, -45705983),
|
||||||
|
c = v(c, u, f, d, a[m + 8], 7, 1770035416),
|
||||||
|
d = v(d, c, u, f, a[m + 9], 12, -1958414417),
|
||||||
|
f = v(f, d, c, u, a[m + 10], 17, -42063),
|
||||||
|
u = v(u, f, d, c, a[m + 11], 22, -1990404162),
|
||||||
|
c = v(c, u, f, d, a[m + 12], 7, 1804603682),
|
||||||
|
d = v(d, c, u, f, a[m + 13], 12, -40341101),
|
||||||
|
f = v(f, d, c, u, a[m + 14], 17, -1502002290),
|
||||||
|
u = v(u, f, d, c, a[m + 15], 22, 1236535329),
|
||||||
|
c = w(c, u, f, d, a[m + 1], 5, -165796510),
|
||||||
|
d = w(d, c, u, f, a[m + 6], 9, -1069501632),
|
||||||
|
f = w(f, d, c, u, a[m + 11], 14, 643717713),
|
||||||
|
u = w(u, f, d, c, a[m + 0], 20, -373897302),
|
||||||
|
c = w(c, u, f, d, a[m + 5], 5, -701558691),
|
||||||
|
d = w(d, c, u, f, a[m + 10], 9, 38016083),
|
||||||
|
f = w(f, d, c, u, a[m + 15], 14, -660478335),
|
||||||
|
u = w(u, f, d, c, a[m + 4], 20, -405537848),
|
||||||
|
c = w(c, u, f, d, a[m + 9], 5, 568446438),
|
||||||
|
d = w(d, c, u, f, a[m + 14], 9, -1019803690),
|
||||||
|
f = w(f, d, c, u, a[m + 3], 14, -187363961),
|
||||||
|
u = w(u, f, d, c, a[m + 8], 20, 1163531501),
|
||||||
|
c = w(c, u, f, d, a[m + 13], 5, -1444681467),
|
||||||
|
d = w(d, c, u, f, a[m + 2], 9, -51403784),
|
||||||
|
f = w(f, d, c, u, a[m + 7], 14, 1735328473),
|
||||||
|
u = w(u, f, d, c, a[m + 12], 20, -1926607734),
|
||||||
|
c = R(c, u, f, d, a[m + 5], 4, -378558),
|
||||||
|
d = R(d, c, u, f, a[m + 8], 11, -2022574463),
|
||||||
|
f = R(f, d, c, u, a[m + 11], 16, 1839030562),
|
||||||
|
u = R(u, f, d, c, a[m + 14], 23, -35309556),
|
||||||
|
c = R(c, u, f, d, a[m + 1], 4, -1530992060),
|
||||||
|
d = R(d, c, u, f, a[m + 4], 11, 1272893353),
|
||||||
|
f = R(f, d, c, u, a[m + 7], 16, -155497632),
|
||||||
|
u = R(u, f, d, c, a[m + 10], 23, -1094730640),
|
||||||
|
c = R(c, u, f, d, a[m + 13], 4, 681279174),
|
||||||
|
d = R(d, c, u, f, a[m + 0], 11, -358537222),
|
||||||
|
f = R(f, d, c, u, a[m + 3], 16, -722521979),
|
||||||
|
u = R(u, f, d, c, a[m + 6], 23, 76029189),
|
||||||
|
c = R(c, u, f, d, a[m + 9], 4, -640364487),
|
||||||
|
d = R(d, c, u, f, a[m + 12], 11, -421815835),
|
||||||
|
f = R(f, d, c, u, a[m + 15], 16, 530742520),
|
||||||
|
u = R(u, f, d, c, a[m + 2], 23, -995338651),
|
||||||
|
c = y(c, u, f, d, a[m + 0], 6, -198630844),
|
||||||
|
d = y(d, c, u, f, a[m + 7], 10, 1126891415),
|
||||||
|
f = y(f, d, c, u, a[m + 14], 15, -1416354905),
|
||||||
|
u = y(u, f, d, c, a[m + 5], 21, -57434055),
|
||||||
|
c = y(c, u, f, d, a[m + 12], 6, 1700485571),
|
||||||
|
d = y(d, c, u, f, a[m + 3], 10, -1894986606),
|
||||||
|
f = y(f, d, c, u, a[m + 10], 15, -1051523),
|
||||||
|
u = y(u, f, d, c, a[m + 1], 21, -2054922799),
|
||||||
|
c = y(c, u, f, d, a[m + 8], 6, 1873313359),
|
||||||
|
d = y(d, c, u, f, a[m + 15], 10, -30611744),
|
||||||
|
f = y(f, d, c, u, a[m + 6], 15, -1560198380),
|
||||||
|
u = y(u, f, d, c, a[m + 13], 21, 1309151649),
|
||||||
|
c = y(c, u, f, d, a[m + 4], 6, -145523070),
|
||||||
|
d = y(d, c, u, f, a[m + 11], 10, -1120210379),
|
||||||
|
f = y(f, d, c, u, a[m + 2], 15, 718787259),
|
||||||
|
u = y(u, f, d, c, a[m + 9], 21, -343485551),
|
||||||
|
c = c + b >>> 0,
|
||||||
|
u = u + _ >>> 0,
|
||||||
|
f = f + g >>> 0,
|
||||||
|
d = d + C >>> 0
|
||||||
|
}
|
||||||
|
return e.endian([c, u, f, d])
|
||||||
|
};
|
||||||
|
s._ff = function(o, i, a, l, c, u, f) {
|
||||||
|
var d = o + (i & a | ~i & l) + (c >>> 0) + f;
|
||||||
|
return (d << u | d >>> 32 - u) + i
|
||||||
|
}
|
||||||
|
,
|
||||||
|
s._gg = function(o, i, a, l, c, u, f) {
|
||||||
|
var d = o + (i & l | a & ~l) + (c >>> 0) + f;
|
||||||
|
return (d << u | d >>> 32 - u) + i
|
||||||
|
}
|
||||||
|
,
|
||||||
|
s._hh = function(o, i, a, l, c, u, f) {
|
||||||
|
var d = o + (i ^ a ^ l) + (c >>> 0) + f;
|
||||||
|
return (d << u | d >>> 32 - u) + i
|
||||||
|
}
|
||||||
|
,
|
||||||
|
s._ii = function(o, i, a, l, c, u, f) {
|
||||||
|
var d = o + (a ^ (i | ~l)) + (c >>> 0) + f;
|
||||||
|
return (d << u | d >>> 32 - u) + i
|
||||||
|
}
|
||||||
|
,
|
||||||
|
s._blocksize = 16,
|
||||||
|
s._digestsize = 16,
|
||||||
|
Tm.exports = function(o, i) {
|
||||||
|
if (o == null)
|
||||||
|
throw new Error("Illegal argument " + o);
|
||||||
|
var a = e.wordsToBytes(s(o, i));
|
||||||
|
return i && i.asBytes ? a : i && i.asString ? r.bytesToString(a) : e.bytesToHex(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)();
|
||||||
|
var cC = Tm.exports;
|
||||||
|
var t = {
|
||||||
|
utf8: {
|
||||||
|
stringToBytes: function(e) {
|
||||||
|
return nl.bin.stringToBytes(unescape(encodeURIComponent(e)))
|
||||||
|
},
|
||||||
|
bytesToString: function(e) {
|
||||||
|
return decodeURIComponent(escape(nl.bin.bytesToString(e)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bin: {
|
||||||
|
stringToBytes: function(e) {
|
||||||
|
for (var t = [], n = 0; n < e.length; n++)
|
||||||
|
t.push(e.charCodeAt(n) & 255);
|
||||||
|
return t
|
||||||
|
},
|
||||||
|
bytesToString: function(e) {
|
||||||
|
for (var t = [], n = 0; n < e.length; n++)
|
||||||
|
t.push(String.fromCharCode(e[n]));
|
||||||
|
return t.join("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hC = (e, t, n=!1) => {
|
||||||
|
if (t.params) {
|
||||||
|
const o = {};
|
||||||
|
Object.keys(t.params).forEach(i => {
|
||||||
|
t.params[i] !== void 0 && t.params[i] !== null && (o[i] = t.params[i])
|
||||||
|
}
|
||||||
|
),
|
||||||
|
t.params = o
|
||||||
|
}
|
||||||
|
let r = {};
|
||||||
|
const s = t.method.toLowerCase();
|
||||||
|
if (s === "get")
|
||||||
|
t.params = Object.assign({}, e, t.params || {});
|
||||||
|
else if (s === "post")
|
||||||
|
if (typeof t.data == "string") {
|
||||||
|
let o;
|
||||||
|
t.data.split("&").forEach(i => {
|
||||||
|
o = i.split("="),
|
||||||
|
r[o[0]] = o[1]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
r = Object.assign({}, e, r),
|
||||||
|
t.data = Object.keys(r).map(i => `${i}=${r[i]}`).join("&")
|
||||||
|
} else
|
||||||
|
r = Object.assign(r, e, t.data || {}),
|
||||||
|
t.data = r;
|
||||||
|
return n ? t : (r = Object.assign({}, t.params, r),
|
||||||
|
r)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Rm = oC(cC);
|
||||||
|
const s = Rm(r);
|
||||||
|
|
||||||
|
pC = e => {
|
||||||
|
let t = Object.keys(e).sort().map(n => {
|
||||||
|
function r(s) {
|
||||||
|
return Array.isArray(s) ? s.join(",") : typeof s === "object" ? JSON.stringify(s) : s
|
||||||
|
}
|
||||||
|
return n + r(e[n])
|
||||||
|
}
|
||||||
|
).join("");
|
||||||
|
return t += Am + e.lm_s_ts + rl,
|
||||||
|
Rm(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// final encryption function
|
||||||
|
let CryptoJS = null;
|
||||||
|
lm_s_key = atob('ZGQ0NmRiYjQ0MmI2ZTRiYTgxN2Q2MzQ3ZDJkZGY0OTM=');
|
||||||
|
function requestSign(signParams, cryptoJSPath) {
|
||||||
|
let sKey = Object.keys(signParams).sort().map(key => {
|
||||||
|
function getValue(val) {
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
return val.join(',');
|
||||||
|
}
|
||||||
|
if (typeof val === 'object') {
|
||||||
|
return JSON.stringify(val);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
return key + getValue(signParams[key]);
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
sKey += signParams.lm_s_id + signParams.lm_s_ts + lm_s_key;
|
||||||
|
console.log(`sKey: ${sKey}`);
|
||||||
|
CryptoJS = require(cryptoJSPath);
|
||||||
|
return CryptoJS.MD5(sKey).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sign(videoid, cryptoJSPath, platform='web'){
|
||||||
|
const vali = createSignature();
|
||||||
|
const data_e = {
|
||||||
|
lm_s_id: Am,
|
||||||
|
lm_s_ts: r,
|
||||||
|
lm_s_str: s,
|
||||||
|
lm_s_ver: 1,
|
||||||
|
h5: 1
|
||||||
|
};
|
||||||
|
/* data_e example value
|
||||||
|
const data_e = {
|
||||||
|
lm_s_id: Am,
|
||||||
|
lm_s_ts: "17284909009151",
|
||||||
|
lm_s_str: "88f9777231dc2d6ac462a1d7ebf5f54e",
|
||||||
|
lm_s_ver: 1,
|
||||||
|
h5: 1
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
console.log("data_e:",data_e);
|
||||||
|
|
||||||
|
data_i = {
|
||||||
|
...data_e,
|
||||||
|
_time: new Date().valueOf(),
|
||||||
|
thirdchannel: 6,
|
||||||
|
videoid: videoid,
|
||||||
|
area: 'zh',
|
||||||
|
vali: vali
|
||||||
|
}
|
||||||
|
console.log("data_i:",data_i);
|
||||||
|
|
||||||
|
// fake lm_s_sign param value
|
||||||
|
let lm_s_sign = pC(data_i);
|
||||||
|
console.log(`fake lm_s_sign: ${lm_s_sign}`);
|
||||||
|
|
||||||
|
//finnal request params
|
||||||
|
/*
|
||||||
|
signParams = {
|
||||||
|
"alias": "liveme",
|
||||||
|
"tongdun_black_box": "iWPU21728483558afruvSVo6x0",
|
||||||
|
"os": "android",
|
||||||
|
"lm_s_id": "LM6000101139961122666757",
|
||||||
|
"lm_s_ts": "17284909009151",
|
||||||
|
"lm_s_str": "88f9777231dc2d6ac462a1d7ebf5f54e",
|
||||||
|
"lm_s_ver": 1,
|
||||||
|
"h5": 1,
|
||||||
|
"_time": 1728490664651,
|
||||||
|
"thirdchannel": 6,
|
||||||
|
"videoid": "17284844223282059697",
|
||||||
|
"area": "zh",
|
||||||
|
"vali": "zH8SlBwnCm4AZWp"
|
||||||
|
}#
|
||||||
|
//result: 4eaf71a1ec19b49b7267e4d16e007105
|
||||||
|
*/
|
||||||
|
signParams = {
|
||||||
|
"alias": "liveme",
|
||||||
|
"tongdun_black_box": "",
|
||||||
|
"os": platform,
|
||||||
|
...data_i
|
||||||
|
}
|
||||||
|
console.log("signParams: ", signParams);
|
||||||
|
lm_s_sign = requestSign(signParams, cryptoJSPath);
|
||||||
|
console.log(`\x1b[32mfinal lm_s_sign: \x1b[0m${lm_s_sign}\n`);
|
||||||
|
data = {
|
||||||
|
...signParams,
|
||||||
|
lm_s_sign
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sign
|
||||||
|
};
|
||||||
143
src/javascript/migu.js
Normal file
143
src/javascript/migu.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* Function to get the ddCalcu parameter value
|
||||||
|
* @param {string} inputUrl - The original URL before encryption
|
||||||
|
* @returns {Promise<string>} - Returns the calculated ddCalcu value
|
||||||
|
*/
|
||||||
|
async function getDdCalcu(inputUrl) {
|
||||||
|
let wasmInstance = null;
|
||||||
|
let memory_p = null; // Uint8Array view
|
||||||
|
let memory_h = null; // Uint32Array view
|
||||||
|
|
||||||
|
// Fixed parameter
|
||||||
|
const f = 'PBTxuWiTEbUPPFcpyxs0ww==';
|
||||||
|
|
||||||
|
// Utility function: Convert string to UTF-8 in memory
|
||||||
|
function stringToUTF8(string, offset) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const encoded = encoder.encode(string);
|
||||||
|
for (let i = 0; i < encoded.length; i++) {
|
||||||
|
memory_p[offset + i] = encoded[i];
|
||||||
|
}
|
||||||
|
memory_p[offset + encoded.length] = 0; // Null-terminate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility function: Read UTF-8 string from memory address
|
||||||
|
function UTF8ToString(offset) {
|
||||||
|
let s = '';
|
||||||
|
let i = 0;
|
||||||
|
while (memory_p[offset + i]) {
|
||||||
|
s += String.fromCharCode(memory_p[offset + i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WASM import function stubs
|
||||||
|
function a(e, t, r, n) {
|
||||||
|
let s = 0;
|
||||||
|
for (let i = 0; i < r; i++) {
|
||||||
|
const d = memory_h[t + 4 >> 2];
|
||||||
|
t += 8;
|
||||||
|
s += d;
|
||||||
|
}
|
||||||
|
memory_h[n >> 2] = s;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function b() {}
|
||||||
|
|
||||||
|
function c() {}
|
||||||
|
|
||||||
|
// Step 1: Retrieve playerVersion
|
||||||
|
const settingsResp = await fetch('https://app-sc.miguvideo.com/common/v1/settings/H5_DetailPage');
|
||||||
|
const settingsData = await settingsResp.json();
|
||||||
|
const playerVersion = JSON.parse(settingsData.body.paramValue).playerVersion;
|
||||||
|
|
||||||
|
// Step 2: Load WASM module
|
||||||
|
const wasmUrl = `https://www.miguvideo.com/mgs/player/prd/${playerVersion}/dist/mgprtcl.wasm`;
|
||||||
|
const wasmResp = await fetch(wasmUrl);
|
||||||
|
if (!wasmResp.ok) throw new Error("Failed to download WASM");
|
||||||
|
const wasmBuffer = await wasmResp.arrayBuffer();
|
||||||
|
|
||||||
|
const importObject = {
|
||||||
|
a: { a, b, c }
|
||||||
|
};
|
||||||
|
|
||||||
|
const { instance } = await WebAssembly.instantiate(wasmBuffer, importObject);
|
||||||
|
wasmInstance = instance;
|
||||||
|
|
||||||
|
const memory = wasmInstance.exports.d;
|
||||||
|
memory_p = new Uint8Array(memory.buffer);
|
||||||
|
memory_h = new Uint32Array(memory.buffer);
|
||||||
|
|
||||||
|
const exports = {
|
||||||
|
CallInterface1: wasmInstance.exports.h,
|
||||||
|
CallInterface2: wasmInstance.exports.i,
|
||||||
|
CallInterface3: wasmInstance.exports.j,
|
||||||
|
CallInterface4: wasmInstance.exports.k,
|
||||||
|
CallInterface6: wasmInstance.exports.m,
|
||||||
|
CallInterface7: wasmInstance.exports.n,
|
||||||
|
CallInterface8: wasmInstance.exports.o,
|
||||||
|
CallInterface9: wasmInstance.exports.p,
|
||||||
|
CallInterface10: wasmInstance.exports.q,
|
||||||
|
CallInterface11: wasmInstance.exports.r,
|
||||||
|
CallInterface14: wasmInstance.exports.t,
|
||||||
|
malloc: wasmInstance.exports.u,
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsedUrl = new URL(inputUrl);
|
||||||
|
const query = Object.fromEntries(parsedUrl.searchParams);
|
||||||
|
|
||||||
|
const o = query.userid || '';
|
||||||
|
const a_val = query.timestamp || '';
|
||||||
|
const s = query.ProgramID || '';
|
||||||
|
const u = query.Channel_ID || '';
|
||||||
|
const v = query.puData || '';
|
||||||
|
|
||||||
|
// Allocate memory
|
||||||
|
const d = exports.malloc(o.length + 1);
|
||||||
|
const h = exports.malloc(a_val.length + 1);
|
||||||
|
const y = exports.malloc(s.length + 1);
|
||||||
|
const m = exports.malloc(u.length + 1);
|
||||||
|
const g = exports.malloc(v.length + 1);
|
||||||
|
const b_val = exports.malloc(f.length + 1);
|
||||||
|
const E = exports.malloc(128);
|
||||||
|
const T = exports.malloc(128);
|
||||||
|
|
||||||
|
// Write data to memory
|
||||||
|
stringToUTF8(o, d);
|
||||||
|
stringToUTF8(a_val, h);
|
||||||
|
stringToUTF8(s, y);
|
||||||
|
stringToUTF8(u, m);
|
||||||
|
stringToUTF8(v, g);
|
||||||
|
stringToUTF8(f, b_val);
|
||||||
|
|
||||||
|
// Call interface functions
|
||||||
|
const S = exports.CallInterface6(); // Create context
|
||||||
|
exports.CallInterface1(S, y, s.length);
|
||||||
|
exports.CallInterface10(S, h, a_val.length);
|
||||||
|
exports.CallInterface9(S, d, o.length);
|
||||||
|
exports.CallInterface3(S, 0, 0);
|
||||||
|
exports.CallInterface11(S, 0, 0);
|
||||||
|
exports.CallInterface8(S, g, v.length);
|
||||||
|
exports.CallInterface2(S, m, u.length);
|
||||||
|
exports.CallInterface14(S, b_val, f.length, T, 128);
|
||||||
|
|
||||||
|
const w = UTF8ToString(T);
|
||||||
|
const I = exports.malloc(w.length + 1);
|
||||||
|
stringToUTF8(w, I);
|
||||||
|
|
||||||
|
exports.CallInterface7(S, I, w.length);
|
||||||
|
exports.CallInterface4(S, E, 128);
|
||||||
|
|
||||||
|
return UTF8ToString(E);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = process.argv[2];
|
||||||
|
|
||||||
|
getDdCalcu(url).then(result => {
|
||||||
|
console.log(result);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
78
src/javascript/taobao-sign.js
Normal file
78
src/javascript/taobao-sign.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
function sign(e) {
|
||||||
|
function t(e, t) {
|
||||||
|
return e << t | e >>> 32 - t
|
||||||
|
}
|
||||||
|
function o(e, t) {
|
||||||
|
var o, n, r, i, a;
|
||||||
|
return r = 2147483648 & e,
|
||||||
|
i = 2147483648 & t,
|
||||||
|
a = (1073741823 & e) + (1073741823 & t),
|
||||||
|
(o = 1073741824 & e) & (n = 1073741824 & t) ? 2147483648 ^ a ^ r ^ i : o | n ? 1073741824 & a ? 3221225472 ^ a ^ r ^ i : 1073741824 ^ a ^ r ^ i : a ^ r ^ i
|
||||||
|
}
|
||||||
|
function n(e, n, r, i, a, s, u) {
|
||||||
|
return o(t(e = o(e, o(o(function(e, t, o) {
|
||||||
|
return e & t | ~e & o
|
||||||
|
}(n, r, i), a), u)), s), n)
|
||||||
|
}
|
||||||
|
function r(e, n, r, i, a, s, u) {
|
||||||
|
return o(t(e = o(e, o(o(function(e, t, o) {
|
||||||
|
return e & o | t & ~o
|
||||||
|
}(n, r, i), a), u)), s), n)
|
||||||
|
}
|
||||||
|
function i(e, n, r, i, a, s, u) {
|
||||||
|
return o(t(e = o(e, o(o(function(e, t, o) {
|
||||||
|
return e ^ t ^ o
|
||||||
|
}(n, r, i), a), u)), s), n)
|
||||||
|
}
|
||||||
|
function a(e, n, r, i, a, s, u) {
|
||||||
|
return o(t(e = o(e, o(o(function(e, t, o) {
|
||||||
|
return t ^ (e | ~o)
|
||||||
|
}(n, r, i), a), u)), s), n)
|
||||||
|
}
|
||||||
|
function s(e) {
|
||||||
|
var t, o = "", n = "";
|
||||||
|
for (t = 0; 3 >= t; t++)
|
||||||
|
o += (n = "0" + (e >>> 8 * t & 255).toString(16)).substr(n.length - 2, 2);
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
var u, l, d, c, p, f, h, m, y, g;
|
||||||
|
for (g = function(e) {
|
||||||
|
for (var t = e.length, o = t + 8, n = 16 * ((o - o % 64) / 64 + 1), r = Array(n - 1), i = 0, a = 0; t > a; )
|
||||||
|
i = a % 4 * 8,
|
||||||
|
r[(a - a % 4) / 4] |= e.charCodeAt(a) << i,
|
||||||
|
a++;
|
||||||
|
return i = a % 4 * 8,
|
||||||
|
r[(a - a % 4) / 4] |= 128 << i,
|
||||||
|
r[n - 2] = t << 3,
|
||||||
|
r[n - 1] = t >>> 29,
|
||||||
|
r
|
||||||
|
}(e = function(e) {
|
||||||
|
var t = String.fromCharCode;
|
||||||
|
e = e.replace(/\r\n/g, "\n");
|
||||||
|
for (var o, n = "", r = 0; r < e.length; r++)
|
||||||
|
128 > (o = e.charCodeAt(r)) ? n += t(o) : o > 127 && 2048 > o ? (n += t(o >> 6 | 192),
|
||||||
|
n += t(63 & o | 128)) : (n += t(o >> 12 | 224),
|
||||||
|
n += t(o >> 6 & 63 | 128),
|
||||||
|
n += t(63 & o | 128));
|
||||||
|
return n
|
||||||
|
}(e)),
|
||||||
|
f = 1732584193,
|
||||||
|
h = 4023233417,
|
||||||
|
m = 2562383102,
|
||||||
|
y = 271733878,
|
||||||
|
u = 0; u < g.length; u += 16)
|
||||||
|
l = f,
|
||||||
|
d = h,
|
||||||
|
c = m,
|
||||||
|
p = y,
|
||||||
|
h = a(h = a(h = a(h = a(h = i(h = i(h = i(h = i(h = r(h = r(h = r(h = r(h = n(h = n(h = n(h = n(h, m = n(m, y = n(y, f = n(f, h, m, y, g[u + 0], 7, 3614090360), h, m, g[u + 1], 12, 3905402710), f, h, g[u + 2], 17, 606105819), y, f, g[u + 3], 22, 3250441966), m = n(m, y = n(y, f = n(f, h, m, y, g[u + 4], 7, 4118548399), h, m, g[u + 5], 12, 1200080426), f, h, g[u + 6], 17, 2821735955), y, f, g[u + 7], 22, 4249261313), m = n(m, y = n(y, f = n(f, h, m, y, g[u + 8], 7, 1770035416), h, m, g[u + 9], 12, 2336552879), f, h, g[u + 10], 17, 4294925233), y, f, g[u + 11], 22, 2304563134), m = n(m, y = n(y, f = n(f, h, m, y, g[u + 12], 7, 1804603682), h, m, g[u + 13], 12, 4254626195), f, h, g[u + 14], 17, 2792965006), y, f, g[u + 15], 22, 1236535329), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 1], 5, 4129170786), h, m, g[u + 6], 9, 3225465664), f, h, g[u + 11], 14, 643717713), y, f, g[u + 0], 20, 3921069994), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 5], 5, 3593408605), h, m, g[u + 10], 9, 38016083), f, h, g[u + 15], 14, 3634488961), y, f, g[u + 4], 20, 3889429448), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 9], 5, 568446438), h, m, g[u + 14], 9, 3275163606), f, h, g[u + 3], 14, 4107603335), y, f, g[u + 8], 20, 1163531501), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 13], 5, 2850285829), h, m, g[u + 2], 9, 4243563512), f, h, g[u + 7], 14, 1735328473), y, f, g[u + 12], 20, 2368359562), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 5], 4, 4294588738), h, m, g[u + 8], 11, 2272392833), f, h, g[u + 11], 16, 1839030562), y, f, g[u + 14], 23, 4259657740), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 1], 4, 2763975236), h, m, g[u + 4], 11, 1272893353), f, h, g[u + 7], 16, 4139469664), y, f, g[u + 10], 23, 3200236656), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 13], 4, 681279174), h, m, g[u + 0], 11, 3936430074), f, h, g[u + 3], 16, 3572445317), y, f, g[u + 6], 23, 76029189), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 9], 4, 3654602809), h, m, g[u + 12], 11, 3873151461), f, h, g[u + 15], 16, 530742520), y, f, g[u + 2], 23, 3299628645), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 0], 6, 4096336452), h, m, g[u + 7], 10, 1126891415), f, h, g[u + 14], 15, 2878612391), y, f, g[u + 5], 21, 4237533241), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 12], 6, 1700485571), h, m, g[u + 3], 10, 2399980690), f, h, g[u + 10], 15, 4293915773), y, f, g[u + 1], 21, 2240044497), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 8], 6, 1873313359), h, m, g[u + 15], 10, 4264355552), f, h, g[u + 6], 15, 2734768916), y, f, g[u + 13], 21, 1309151649), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 4], 6, 4149444226), h, m, g[u + 11], 10, 3174756917), f, h, g[u + 2], 15, 718787259), y, f, g[u + 9], 21, 3951481745),
|
||||||
|
f = o(f, l),
|
||||||
|
h = o(h, d),
|
||||||
|
m = o(m, c),
|
||||||
|
y = o(y, p);
|
||||||
|
return (s(f) + s(h) + s(m) + s(y)).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正确sign值:05748e8359cd3e6deaab02d15caafc11
|
||||||
|
// var sg =sign('5655b7041ca049730330701082886efd&1719411639403&12574478&{"componentKey":"wp_pc_shop_basic_info","params":"{\\"memberId\\":\\"b2b-22133374292418351a\\"}"}')
|
||||||
|
// console.log(sg)
|
||||||
43
src/logger.py
Normal file
43
src/logger.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
logger.remove()
|
||||||
|
|
||||||
|
custom_format = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> - <level>{message}</level>"
|
||||||
|
|
||||||
|
logger.add(
|
||||||
|
sink=sys.stderr,
|
||||||
|
format=custom_format,
|
||||||
|
level="DEBUG",
|
||||||
|
colorize=True,
|
||||||
|
enqueue=True
|
||||||
|
)
|
||||||
|
|
||||||
|
script_path = os.path.split(os.path.realpath(sys.argv[0]))[0]
|
||||||
|
|
||||||
|
logger.add(
|
||||||
|
f"{script_path}/logs/streamget.log",
|
||||||
|
level="DEBUG",
|
||||||
|
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}",
|
||||||
|
filter=lambda i: i["level"].name != "INFO",
|
||||||
|
serialize=False,
|
||||||
|
enqueue=True,
|
||||||
|
retention=1,
|
||||||
|
rotation="300 KB",
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.add(
|
||||||
|
f"{script_path}/logs/PlayURL.log",
|
||||||
|
level="INFO",
|
||||||
|
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {message}",
|
||||||
|
filter=lambda i: i["level"].name == "INFO",
|
||||||
|
serialize=False,
|
||||||
|
enqueue=True,
|
||||||
|
retention=1,
|
||||||
|
rotation="300 KB",
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
92
src/proxy.py
Normal file
92
src/proxy.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from enum import Enum, auto
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from .utils import logger
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyType(Enum):
|
||||||
|
HTTP = auto()
|
||||||
|
HTTPS = auto()
|
||||||
|
SOCKS = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ProxyInfo:
|
||||||
|
ip: str = field(default="", repr=True)
|
||||||
|
port: str = field(default="", repr=True)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if (self.ip and not self.port) or (not self.ip and self.port):
|
||||||
|
raise ValueError("IP or port cannot be empty")
|
||||||
|
|
||||||
|
if (self.ip and self.port) and (not self.port.isdigit() or not (1 <= int(self.port) <= 65535)):
|
||||||
|
raise ValueError("Port must be a digit between 1 and 65535")
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyDetector:
|
||||||
|
def __init__(self):
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
import winreg
|
||||||
|
self.winreg = winreg
|
||||||
|
self.__path = r'Software\Microsoft\Windows\CurrentVersion\Internet Settings'
|
||||||
|
with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as key_user:
|
||||||
|
self.__INTERNET_SETTINGS = winreg.OpenKeyEx(key_user, self.__path, 0, winreg.KEY_ALL_ACCESS)
|
||||||
|
else:
|
||||||
|
self.__is_windows = False
|
||||||
|
|
||||||
|
def get_proxy_info(self) -> ProxyInfo:
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
ip, port = self._get_proxy_info_windows()
|
||||||
|
else:
|
||||||
|
ip, port = self._get_proxy_info_linux()
|
||||||
|
return ProxyInfo(ip, port)
|
||||||
|
|
||||||
|
def is_proxy_enabled(self) -> bool:
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
return self._is_proxy_enabled_windows()
|
||||||
|
else:
|
||||||
|
return self._is_proxy_enabled_linux()
|
||||||
|
|
||||||
|
def _get_proxy_info_windows(self) -> tuple[str, str]:
|
||||||
|
ip, port = "", ""
|
||||||
|
if self._is_proxy_enabled_windows():
|
||||||
|
try:
|
||||||
|
ip_port = self.winreg.QueryValueEx(self.__INTERNET_SETTINGS, "ProxyServer")[0]
|
||||||
|
if ip_port:
|
||||||
|
ip, port = ip_port.split(":")
|
||||||
|
except FileNotFoundError as err:
|
||||||
|
logger.warning("No proxy information found: " + str(err))
|
||||||
|
except Exception as err:
|
||||||
|
logger.error("An error occurred: " + str(err))
|
||||||
|
else:
|
||||||
|
logger.debug("No proxy is enabled on the system")
|
||||||
|
return ip, port
|
||||||
|
|
||||||
|
def _is_proxy_enabled_windows(self) -> bool:
|
||||||
|
try:
|
||||||
|
if self.winreg.QueryValueEx(self.__INTERNET_SETTINGS, "ProxyEnable")[0] == 1:
|
||||||
|
return True
|
||||||
|
except FileNotFoundError as err:
|
||||||
|
print("No proxy information found: " + str(err))
|
||||||
|
except Exception as err:
|
||||||
|
print("An error occurred: " + str(err))
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_proxy_info_linux() -> tuple[str, str]:
|
||||||
|
proxies = {
|
||||||
|
'http': os.getenv('http_proxy'),
|
||||||
|
'https': os.getenv('https_proxy'),
|
||||||
|
'ftp': os.getenv('ftp_proxy')
|
||||||
|
}
|
||||||
|
ip = port = ""
|
||||||
|
for proto, proxy in proxies.items():
|
||||||
|
if proxy:
|
||||||
|
ip, port = proxy.split(':')
|
||||||
|
break
|
||||||
|
return ip, port
|
||||||
|
|
||||||
|
def _is_proxy_enabled_linux(self) -> bool:
|
||||||
|
proxies = self._get_proxy_info_linux()
|
||||||
|
return any(proxy != '' for proxy in proxies)
|
||||||
150
src/room.py
Normal file
150
src/room.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Author: Hmily
|
||||||
|
GitHub:https://github.com/ihmily
|
||||||
|
Date: 2023-07-17 23:52:05
|
||||||
|
Update: 2025-02-04 04:57:00
|
||||||
|
Copyright (c) 2023 by Hmily, All Rights Reserved.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import urllib.parse
|
||||||
|
import execjs
|
||||||
|
import httpx
|
||||||
|
import urllib.request
|
||||||
|
from . import JS_SCRIPT_PATH, utils
|
||||||
|
|
||||||
|
no_proxy_handler = urllib.request.ProxyHandler({})
|
||||||
|
opener = urllib.request.build_opener(no_proxy_handler)
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedUrlError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||||
|
'SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
|
||||||
|
'Cookie': 's_v_web_id=verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf'
|
||||||
|
}
|
||||||
|
|
||||||
|
HEADERS_PC = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||||
|
'Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0',
|
||||||
|
'Cookie': 'sessionid=7494ae59ae06784454373ce25761e864; __ac_nonce=0670497840077ee4c9eb2; '
|
||||||
|
'__ac_signature=_02B4Z6wo00f012DZczQAAIDCJJBb3EjnINdg-XeAAL8-db; '
|
||||||
|
's_v_web_id=verify_m1ztgtjj_vuHnMLZD_iwZ9_4YO4_BdN1_7wLP3pyqXsf2; '
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# X-bogus算法
|
||||||
|
async def get_xbogus(url: str, headers: dict | None = None) -> str:
|
||||||
|
if not headers or 'user-agent' not in (k.lower() for k in headers):
|
||||||
|
headers = HEADERS
|
||||||
|
query = urllib.parse.urlparse(url).query
|
||||||
|
xbogus = execjs.compile(open(f'{JS_SCRIPT_PATH}/x-bogus.js').read()).call(
|
||||||
|
'sign', query, headers.get("User-Agent", "user-agent"))
|
||||||
|
return xbogus
|
||||||
|
|
||||||
|
|
||||||
|
# 获取房间ID和用户secID
|
||||||
|
async def get_sec_user_id(url: str, proxy_addr: str | None = None, headers: dict | None = None) -> tuple | None:
|
||||||
|
if not headers or all(k.lower() not in ['user-agent', 'cookie'] for k in headers):
|
||||||
|
headers = HEADERS
|
||||||
|
|
||||||
|
try:
|
||||||
|
proxy_addr = utils.handle_proxy_addr(proxy_addr)
|
||||||
|
async with httpx.AsyncClient(proxy=proxy_addr, timeout=15) as client:
|
||||||
|
response = await client.get(url, headers=headers, follow_redirects=True)
|
||||||
|
redirect_url = response.url
|
||||||
|
if 'reflow/' in str(redirect_url):
|
||||||
|
match = re.search(r'sec_user_id=([\w_\-]+)&', str(redirect_url))
|
||||||
|
if match:
|
||||||
|
sec_user_id = match.group(1)
|
||||||
|
room_id = str(redirect_url).split('?')[0].rsplit('/', maxsplit=1)[1]
|
||||||
|
return room_id, sec_user_id
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Could not find sec_user_id in the URL.")
|
||||||
|
else:
|
||||||
|
raise UnsupportedUrlError("The redirect URL does not contain 'reflow/'.")
|
||||||
|
except UnsupportedUrlError as e:
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"An error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# 获取抖音号
|
||||||
|
async def get_unique_id(url: str, proxy_addr: str | None = None, headers: dict | None = None) -> str | None:
|
||||||
|
if not headers or all(k.lower() not in ['user-agent', 'cookie'] for k in headers):
|
||||||
|
headers = HEADERS
|
||||||
|
|
||||||
|
try:
|
||||||
|
proxy_addr = utils.handle_proxy_addr(proxy_addr)
|
||||||
|
async with httpx.AsyncClient(proxy=proxy_addr, timeout=15) as client:
|
||||||
|
response = await client.get(url, headers=headers, follow_redirects=True)
|
||||||
|
redirect_url = str(response.url)
|
||||||
|
if 'reflow/' in str(redirect_url):
|
||||||
|
raise UnsupportedUrlError("Unsupported URL")
|
||||||
|
sec_user_id = redirect_url.split('?')[0].rsplit('/', maxsplit=1)[1]
|
||||||
|
headers['Cookie'] = ('ttwid=1%7C4ejCkU2bKY76IySQENJwvGhg1IQZrgGEupSyTKKfuyk%7C1740470403%7Cbc9a'
|
||||||
|
'd2ee341f1a162f9e27f4641778030d1ae91e31f9df6553a8f2efa3bdb7b4; __ac_nonce=06'
|
||||||
|
'83e59f3009cc48fbab0; __ac_signature=_02B4Z6wo00f01mG6waQAAIDB9JUCzFb6.TZhmsU'
|
||||||
|
'AAPBf34; __ac_referer=__ac_blank')
|
||||||
|
user_page_response = await client.get(f'https://www.iesdouyin.com/share/user/{sec_user_id}',
|
||||||
|
headers=headers, follow_redirects=True)
|
||||||
|
matches = re.findall(r'unique_id":"(.*?)","verification_type', user_page_response.text)
|
||||||
|
if matches:
|
||||||
|
unique_id = matches[-1]
|
||||||
|
return unique_id
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Could not find unique_id in the response.")
|
||||||
|
except UnsupportedUrlError as e:
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"An error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# 获取直播间webID
|
||||||
|
async def get_live_room_id(room_id: str, sec_user_id: str, proxy_addr: str | None = None, params: dict | None = None,
|
||||||
|
headers: dict | None = None) -> str:
|
||||||
|
if not headers or all(k.lower() not in ['user-agent', 'cookie'] for k in headers):
|
||||||
|
headers = HEADERS
|
||||||
|
|
||||||
|
if not params:
|
||||||
|
params = {
|
||||||
|
"verifyFp": "verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf",
|
||||||
|
"type_id": "0",
|
||||||
|
"live_id": "1",
|
||||||
|
"room_id": room_id,
|
||||||
|
"sec_user_id": sec_user_id,
|
||||||
|
"app_id": "1128",
|
||||||
|
"msToken": "wrqzbEaTlsxt52-vxyZo_mIoL0RjNi1ZdDe7gzEGMUTVh_HvmbLLkQrA_1HKVOa2C6gkxb6IiY6TY2z8enAkPEwGq--gM"
|
||||||
|
"-me3Yudck2ailla5Q4osnYIHxd9dI4WtQ==",
|
||||||
|
}
|
||||||
|
|
||||||
|
api = f'https://webcast.amemv.com/webcast/room/reflow/info/?{urllib.parse.urlencode(params)}'
|
||||||
|
xbogus = await get_xbogus(api)
|
||||||
|
api = api + "&X-Bogus=" + xbogus
|
||||||
|
|
||||||
|
try:
|
||||||
|
proxy_addr = utils.handle_proxy_addr(proxy_addr)
|
||||||
|
async with httpx.AsyncClient(proxy=proxy_addr,
|
||||||
|
timeout=15) as client:
|
||||||
|
response = await client.get(api, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
json_data = response.json()
|
||||||
|
return json_data['data']['room']['owner']['web_rid']
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
print(f"HTTP status error occurred: {e.response.status_code}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An exception occurred during get_live_room_id: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
room_url = "https://v.douyin.com/iQLgKSj/"
|
||||||
|
_room_id, sec_uid = get_sec_user_id(room_url)
|
||||||
|
web_rid = get_live_room_id(_room_id, sec_uid)
|
||||||
|
print("return web_rid:", web_rid)
|
||||||
3395
src/spider.py
Normal file
3395
src/spider.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,11 +4,10 @@
|
|||||||
Author: Hmily
|
Author: Hmily
|
||||||
GitHub: https://github.com/ihmily
|
GitHub: https://github.com/ihmily
|
||||||
Date: 2023-07-15 23:15:00
|
Date: 2023-07-15 23:15:00
|
||||||
Update: 2024-10-02 04:36:12
|
Update: 2025-02-06 02:28:00
|
||||||
Copyright (c) 2023-2024 by Hmily, All Rights Reserved.
|
Copyright (c) 2023-2025 by Hmily, All Rights Reserved.
|
||||||
Function: Get live stream data.
|
Function: Get live stream data.
|
||||||
"""
|
"""
|
||||||
from typing import Dict, Any, Union
|
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
@ -22,54 +21,84 @@ from .utils import trace_error_decorator
|
|||||||
from .spider import (
|
from .spider import (
|
||||||
get_douyu_stream_data, get_bilibili_stream_data
|
get_douyu_stream_data, get_bilibili_stream_data
|
||||||
)
|
)
|
||||||
|
from .http_clients.async_http import get_response_status
|
||||||
|
|
||||||
|
QUALITY_MAPPING = {"OD": 0, "BD": 0, "UHD": 1, "HD": 2, "SD": 3, "LD": 4}
|
||||||
|
|
||||||
|
|
||||||
|
def get_quality_index(quality) -> tuple:
|
||||||
|
if not quality:
|
||||||
|
return list(QUALITY_MAPPING.items())[0]
|
||||||
|
|
||||||
|
quality_str = str(quality).upper()
|
||||||
|
if quality_str.isdigit():
|
||||||
|
quality_int = int(quality_str[0])
|
||||||
|
quality_str = list(QUALITY_MAPPING.keys())[quality_int]
|
||||||
|
return quality_str, QUALITY_MAPPING.get(quality_str, 0)
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
@trace_error_decorator
|
||||||
def get_douyin_stream_url(json_data: dict, video_quality: str) -> Dict[str, Any]:
|
async def get_douyin_stream_url(json_data: dict, video_quality: str, proxy_addr: str) -> dict:
|
||||||
anchor_name = json_data.get('anchor_name', None)
|
anchor_name = json_data.get('anchor_name')
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"anchor_name": anchor_name,
|
"anchor_name": anchor_name,
|
||||||
"is_live": False,
|
"is_live": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
status = json_data.get("status", 4) # 直播状态 2 是正在直播、4 是未开播
|
status = json_data.get("status", 4)
|
||||||
|
|
||||||
if status == 2:
|
if status == 2:
|
||||||
stream_url = json_data['stream_url']
|
stream_url = json_data['stream_url']
|
||||||
flv_url_dict = stream_url['flv_pull_url']
|
flv_url_dict = stream_url['flv_pull_url']
|
||||||
flv_url_list = list(flv_url_dict.values())
|
flv_url_list: list = list(flv_url_dict.values())
|
||||||
m3u8_url_dict = stream_url['hls_pull_url_map']
|
m3u8_url_dict = stream_url['hls_pull_url_map']
|
||||||
m3u8_url_list = list(m3u8_url_dict.values())
|
m3u8_url_list: list = list(m3u8_url_dict.values())
|
||||||
|
|
||||||
while len(flv_url_list) < 5:
|
while len(flv_url_list) < 5:
|
||||||
flv_url_list.append(flv_url_list[-1])
|
flv_url_list.append(flv_url_list[-1])
|
||||||
m3u8_url_list.append(m3u8_url_list[-1])
|
m3u8_url_list.append(m3u8_url_list[-1])
|
||||||
|
|
||||||
video_qualities = {"原画": 0, "蓝光": 0, "超清": 1, "高清": 2, "标清": 3, "流畅": 4}
|
video_quality, quality_index = get_quality_index(video_quality)
|
||||||
quality_index = video_qualities.get(video_quality)
|
|
||||||
m3u8_url = m3u8_url_list[quality_index]
|
m3u8_url = m3u8_url_list[quality_index]
|
||||||
flv_url = flv_url_list[quality_index]
|
flv_url = flv_url_list[quality_index]
|
||||||
result['m3u8_url'] = m3u8_url
|
ok = await get_response_status(url=m3u8_url, proxy_addr=proxy_addr)
|
||||||
result['flv_url'] = flv_url
|
if not ok:
|
||||||
result['is_live'] = True
|
index = quality_index + 1 if quality_index < 4 else quality_index - 1
|
||||||
result['record_url'] = m3u8_url
|
m3u8_url = m3u8_url_list[index]
|
||||||
|
flv_url = flv_url_list[index]
|
||||||
|
result |= {
|
||||||
|
'is_live': True,
|
||||||
|
'title': json_data['title'],
|
||||||
|
'quality': video_quality,
|
||||||
|
'm3u8_url': m3u8_url,
|
||||||
|
'flv_url': flv_url,
|
||||||
|
'record_url': m3u8_url or flv_url,
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
@trace_error_decorator
|
||||||
def get_tiktok_stream_url(json_data: dict, video_quality: str) -> Dict[str, Any]:
|
async def get_tiktok_stream_url(json_data: dict, video_quality: str, proxy_addr: str) -> dict:
|
||||||
if not json_data:
|
if not json_data:
|
||||||
return {"anchor_name": None, "is_live": False}
|
return {"anchor_name": None, "is_live": False}
|
||||||
|
|
||||||
def get_video_quality_url(stream, q_key) -> list[dict[str, int | Any]]:
|
def get_video_quality_url(stream, q_key) -> list:
|
||||||
play_list = []
|
play_list = []
|
||||||
for key in stream:
|
for key in stream:
|
||||||
url_info = stream[key]['main']
|
url_info = stream[key]['main']
|
||||||
play_url = url_info[q_key]
|
|
||||||
sdk_params = url_info['sdk_params']
|
sdk_params = url_info['sdk_params']
|
||||||
sdk_params = json.loads(sdk_params)
|
sdk_params = json.loads(sdk_params)
|
||||||
vbitrate = int(sdk_params['vbitrate'])
|
vbitrate = int(sdk_params['vbitrate'])
|
||||||
|
v_codec = sdk_params.get('VCodec', '')
|
||||||
|
|
||||||
|
play_url = ''
|
||||||
|
if url_info.get(q_key):
|
||||||
|
if url_info[q_key].endswith(".flv") or url_info[q_key].endswith(".m3u8"):
|
||||||
|
play_url = url_info[q_key] + '?codec=' + v_codec
|
||||||
|
else:
|
||||||
|
play_url = url_info[q_key] + '&codec=' + v_codec
|
||||||
|
|
||||||
resolution = sdk_params['resolution']
|
resolution = sdk_params['resolution']
|
||||||
if vbitrate != 0 and resolution:
|
if vbitrate != 0 and resolution:
|
||||||
width, height = map(int, resolution.split('x'))
|
width, height = map(int, resolution.split('x'))
|
||||||
@ -99,17 +128,33 @@ def get_tiktok_stream_url(json_data: dict, video_quality: str) -> Dict[str, Any]
|
|||||||
flv_url_list.append(flv_url_list[-1])
|
flv_url_list.append(flv_url_list[-1])
|
||||||
while len(m3u8_url_list) < 5:
|
while len(m3u8_url_list) < 5:
|
||||||
m3u8_url_list.append(m3u8_url_list[-1])
|
m3u8_url_list.append(m3u8_url_list[-1])
|
||||||
video_qualities = {"原画": 0, "蓝光": 0, "超清": 1, "高清": 2, "标清": 3, '流畅': 4}
|
video_quality, quality_index = get_quality_index(video_quality)
|
||||||
quality_index = video_qualities.get(video_quality)
|
flv_dict: dict = flv_url_list[quality_index]
|
||||||
result['flv_url'] = flv_url_list[quality_index]['url']
|
m3u8_dict: dict = m3u8_url_list[quality_index]
|
||||||
result['m3u8_url'] = m3u8_url_list[quality_index]['url']
|
|
||||||
result['is_live'] = True
|
check_url = m3u8_dict.get('url') or flv_dict.get('url')
|
||||||
result['record_url'] = flv_url_list[quality_index]['url'].replace("https://", "http://")
|
ok = await get_response_status(url=check_url, proxy_addr=proxy_addr, http2=False)
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
index = quality_index + 1 if quality_index < 4 else quality_index - 1
|
||||||
|
flv_dict: dict = flv_url_list[index]
|
||||||
|
m3u8_dict: dict = m3u8_url_list[index]
|
||||||
|
|
||||||
|
flv_url = flv_dict['url']
|
||||||
|
m3u8_url = m3u8_dict['url']
|
||||||
|
result |= {
|
||||||
|
'is_live': True,
|
||||||
|
'title': live_room['liveRoom']['title'],
|
||||||
|
'quality': video_quality,
|
||||||
|
'm3u8_url': m3u8_url,
|
||||||
|
'flv_url': flv_url,
|
||||||
|
'record_url': m3u8_url or flv_url,
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
@trace_error_decorator
|
||||||
def get_kuaishou_stream_url(json_data: dict, video_quality: str) -> Dict[str, Any]:
|
async def get_kuaishou_stream_url(json_data: dict, video_quality: str) -> dict:
|
||||||
if json_data['type'] == 1 and not json_data["is_live"]:
|
if json_data['type'] == 1 and not json_data["is_live"]:
|
||||||
return json_data
|
return json_data
|
||||||
live_status = json_data['is_live']
|
live_status = json_data['is_live']
|
||||||
@ -121,11 +166,10 @@ def get_kuaishou_stream_url(json_data: dict, video_quality: str) -> Dict[str, An
|
|||||||
}
|
}
|
||||||
|
|
||||||
if live_status:
|
if live_status:
|
||||||
quality_mapping = {'原画': 0, '蓝光': 0, '超清': 1, '高清': 2, '标清': 3, '流畅': 4}
|
quality_mapping_bit = {'OD': 99999, 'BD': 4000, 'UHD': 2000, 'HD': 1000, 'SD': 800, 'LD': 600}
|
||||||
|
if video_quality in QUALITY_MAPPING:
|
||||||
|
|
||||||
if video_quality in quality_mapping:
|
quality, quality_index = get_quality_index(video_quality)
|
||||||
|
|
||||||
quality_index = quality_mapping[video_quality]
|
|
||||||
if 'm3u8_url_list' in json_data:
|
if 'm3u8_url_list' in json_data:
|
||||||
m3u8_url_list = json_data['m3u8_url_list'][::-1]
|
m3u8_url_list = json_data['m3u8_url_list'][::-1]
|
||||||
while len(m3u8_url_list) < 5:
|
while len(m3u8_url_list) < 5:
|
||||||
@ -134,20 +178,38 @@ def get_kuaishou_stream_url(json_data: dict, video_quality: str) -> Dict[str, An
|
|||||||
result['m3u8_url'] = m3u8_url
|
result['m3u8_url'] = m3u8_url
|
||||||
|
|
||||||
if 'flv_url_list' in json_data:
|
if 'flv_url_list' in json_data:
|
||||||
flv_url_list = json_data['flv_url_list'][::-1]
|
if 'bitrate' in json_data['flv_url_list'][0]:
|
||||||
while len(flv_url_list) < 5:
|
flv_url_list = json_data['flv_url_list']
|
||||||
flv_url_list.append(flv_url_list[-1])
|
flv_url_list = sorted(flv_url_list, key=lambda x: x['bitrate'], reverse=True)
|
||||||
flv_url = flv_url_list[quality_index]['url']
|
quality_str = str(video_quality).upper()
|
||||||
result['flv_url'] = flv_url
|
if quality_str.isdigit():
|
||||||
result['record_url'] = flv_url
|
video_quality, quality_index_bitrate_value = list(quality_mapping_bit.items())[int(quality_str)]
|
||||||
|
else:
|
||||||
|
quality_index_bitrate_value = quality_mapping_bit.get(quality_str, 99999)
|
||||||
|
video_quality = quality_str
|
||||||
|
quality_index = next(
|
||||||
|
(i for i, x in enumerate(flv_url_list) if x['bitrate'] <= quality_index_bitrate_value), None)
|
||||||
|
if quality_index is None:
|
||||||
|
quality_index = len(flv_url_list) - 1
|
||||||
|
flv_url = flv_url_list[quality_index]['url']
|
||||||
|
|
||||||
|
result['flv_url'] = flv_url
|
||||||
|
result['record_url'] = flv_url
|
||||||
|
else:
|
||||||
|
flv_url_list = json_data['flv_url_list'][::-1]
|
||||||
|
while len(flv_url_list) < 5:
|
||||||
|
flv_url_list.append(flv_url_list[-1])
|
||||||
|
flv_url = flv_url_list[quality_index]['url']
|
||||||
|
result |= {'flv_url': flv_url, 'record_url': flv_url}
|
||||||
result['is_live'] = True
|
result['is_live'] = True
|
||||||
|
result['quality'] = video_quality
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
@trace_error_decorator
|
||||||
def get_huya_stream_url(json_data: dict, video_quality: str) -> Dict[str, Any]:
|
async def get_huya_stream_url(json_data: dict, video_quality: str) -> dict:
|
||||||
game_live_info = json_data['data'][0]['gameLiveInfo']
|
game_live_info = json_data['data'][0]['gameLiveInfo']
|
||||||
|
live_title = game_live_info['introduction']
|
||||||
stream_info_list = json_data['data'][0]['gameStreamInfoList']
|
stream_info_list = json_data['data'][0]['gameStreamInfoList']
|
||||||
anchor_name = game_live_info.get('nick', '')
|
anchor_name = game_live_info.get('nick', '')
|
||||||
|
|
||||||
@ -206,17 +268,17 @@ def get_huya_stream_url(json_data: dict, video_quality: str) -> Dict[str, Any]:
|
|||||||
m3u8_url = f'{hls_url}/{stream_name}.{hls_url_suffix}?{new_anti_code}&ratio='
|
m3u8_url = f'{hls_url}/{stream_name}.{hls_url_suffix}?{new_anti_code}&ratio='
|
||||||
|
|
||||||
quality_list = flv_anti_code.split('&exsphd=')
|
quality_list = flv_anti_code.split('&exsphd=')
|
||||||
if len(quality_list) > 1 and video_quality not in ["原画", "蓝光"]:
|
if len(quality_list) > 1 and video_quality not in ["OD", "BD"]:
|
||||||
pattern = r"(?<=264_)\d+"
|
pattern = r"(?<=264_)\d+"
|
||||||
quality_list = list(re.findall(pattern, quality_list[1]))[::-1]
|
quality_list = list(re.findall(pattern, quality_list[1]))[::-1]
|
||||||
while len(quality_list) < 5:
|
while len(quality_list) < 5:
|
||||||
quality_list.append(quality_list[-1])
|
quality_list.append(quality_list[-1])
|
||||||
|
|
||||||
video_quality_options = {
|
video_quality_options = {
|
||||||
"超清": quality_list[0],
|
"UHD": quality_list[0],
|
||||||
"高清": quality_list[1],
|
"HD": quality_list[1],
|
||||||
"标清": quality_list[2],
|
"SD": quality_list[2],
|
||||||
"流畅": quality_list[3]
|
"LD": quality_list[3]
|
||||||
}
|
}
|
||||||
|
|
||||||
if video_quality not in video_quality_options:
|
if video_quality not in video_quality_options:
|
||||||
@ -226,42 +288,45 @@ def get_huya_stream_url(json_data: dict, video_quality: str) -> Dict[str, Any]:
|
|||||||
flv_url = flv_url + str(video_quality_options[video_quality])
|
flv_url = flv_url + str(video_quality_options[video_quality])
|
||||||
m3u8_url = m3u8_url + str(video_quality_options[video_quality])
|
m3u8_url = m3u8_url + str(video_quality_options[video_quality])
|
||||||
|
|
||||||
result['flv_url'] = flv_url
|
result |= {
|
||||||
result['m3u8_url'] = m3u8_url
|
'is_live': True,
|
||||||
result['is_live'] = True
|
'title': live_title,
|
||||||
result['record_url'] = flv_url
|
'quality': video_quality,
|
||||||
|
'm3u8_url': m3u8_url,
|
||||||
|
'flv_url': flv_url,
|
||||||
|
'record_url': flv_url or m3u8_url
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
@trace_error_decorator
|
||||||
def get_douyu_stream_url(json_data: dict, video_quality: str, cookies: str, proxy_addr: str) -> Dict[str, Any]:
|
async def get_douyu_stream_url(json_data: dict, video_quality: str, cookies: str, proxy_addr: str) -> dict:
|
||||||
if not json_data["is_live"]:
|
if not json_data["is_live"]:
|
||||||
return json_data
|
return json_data
|
||||||
|
|
||||||
video_quality_options = {
|
video_quality_options = {
|
||||||
"原画": '0',
|
"OD": '0',
|
||||||
"蓝光": '0',
|
"BD": '0',
|
||||||
"超清": '3',
|
"UHD": '3',
|
||||||
"高清": '2',
|
"HD": '2',
|
||||||
"标清": '1',
|
"SD": '1',
|
||||||
"流畅": '1'
|
"LD": '1'
|
||||||
}
|
}
|
||||||
|
|
||||||
rid = str(json_data["room_id"])
|
rid = str(json_data["room_id"])
|
||||||
json_data.pop("room_id", None)
|
json_data.pop("room_id")
|
||||||
rate = video_quality_options.get(video_quality, '0')
|
rate = video_quality_options.get(video_quality, '0')
|
||||||
flv_data = get_douyu_stream_data(rid, rate, cookies=cookies, proxy_addr=proxy_addr)
|
flv_data = await get_douyu_stream_data(rid, rate, cookies=cookies, proxy_addr=proxy_addr)
|
||||||
rtmp_url = flv_data['data'].get('rtmp_url', None)
|
rtmp_url = flv_data['data'].get('rtmp_url')
|
||||||
rtmp_live = flv_data['data'].get('rtmp_live', None)
|
rtmp_live = flv_data['data'].get('rtmp_live')
|
||||||
if rtmp_live:
|
if rtmp_live:
|
||||||
flv_url = f'{rtmp_url}/{rtmp_live}'
|
flv_url = f'{rtmp_url}/{rtmp_live}'
|
||||||
json_data['flv_url'] = flv_url
|
json_data |= {'quality': video_quality, 'flv_url': flv_url, 'record_url': flv_url}
|
||||||
json_data['record_url'] = flv_url
|
|
||||||
return json_data
|
return json_data
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
@trace_error_decorator
|
||||||
def get_yy_stream_url(json_data: dict) -> Dict[str, Any]:
|
async def get_yy_stream_url(json_data: dict) -> dict:
|
||||||
anchor_name = json_data.get('anchor_name', '')
|
anchor_name = json_data.get('anchor_name', '')
|
||||||
result = {
|
result = {
|
||||||
"anchor_name": anchor_name,
|
"anchor_name": anchor_name,
|
||||||
@ -271,14 +336,18 @@ def get_yy_stream_url(json_data: dict) -> Dict[str, Any]:
|
|||||||
stream_line_addr = json_data['avp_info_res']['stream_line_addr']
|
stream_line_addr = json_data['avp_info_res']['stream_line_addr']
|
||||||
cdn_info = list(stream_line_addr.values())[0]
|
cdn_info = list(stream_line_addr.values())[0]
|
||||||
flv_url = cdn_info['cdn_info']['url']
|
flv_url = cdn_info['cdn_info']['url']
|
||||||
result['flv_url'] = flv_url
|
result |= {
|
||||||
result['is_live'] = True
|
'is_live': True,
|
||||||
result['record_url'] = flv_url
|
'title': json_data['title'],
|
||||||
|
'quality': 'OD',
|
||||||
|
'flv_url': flv_url,
|
||||||
|
'record_url': flv_url
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
@trace_error_decorator
|
||||||
def get_bilibili_stream_url(json_data: dict, video_quality: str, proxy_addr: str, cookies: str) -> Dict[str, Any]:
|
async def get_bilibili_stream_url(json_data: dict, video_quality: str, proxy_addr: str, cookies: str) -> dict:
|
||||||
anchor_name = json_data["anchor_name"]
|
anchor_name = json_data["anchor_name"]
|
||||||
if not json_data["live_status"]:
|
if not json_data["live_status"]:
|
||||||
return {
|
return {
|
||||||
@ -289,67 +358,89 @@ def get_bilibili_stream_url(json_data: dict, video_quality: str, proxy_addr: str
|
|||||||
room_url = json_data['room_url']
|
room_url = json_data['room_url']
|
||||||
|
|
||||||
video_quality_options = {
|
video_quality_options = {
|
||||||
"原画": '10000',
|
"OD": '10000',
|
||||||
"蓝光": '400',
|
"BD": '400',
|
||||||
"超清": '250',
|
"UHD": '250',
|
||||||
"高清": '150',
|
"HD": '150',
|
||||||
"标清": '80',
|
"SD": '80',
|
||||||
"流畅": '80'
|
"LD": '80'
|
||||||
}
|
}
|
||||||
|
|
||||||
select_quality = video_quality_options[video_quality]
|
select_quality = video_quality_options[video_quality]
|
||||||
play_url = get_bilibili_stream_data(
|
play_url = await get_bilibili_stream_data(
|
||||||
room_url, qn=select_quality, platform='web', proxy_addr=proxy_addr, cookies=cookies)
|
room_url, qn=select_quality, platform='web', proxy_addr=proxy_addr, cookies=cookies)
|
||||||
return {
|
return {
|
||||||
'anchor_name': json_data['anchor_name'],
|
'anchor_name': json_data['anchor_name'],
|
||||||
'is_live': True,
|
'is_live': True,
|
||||||
|
'title': json_data['title'],
|
||||||
|
'quality': video_quality,
|
||||||
'record_url': play_url
|
'record_url': play_url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@trace_error_decorator
|
@trace_error_decorator
|
||||||
def get_netease_stream_url(json_data: dict, video_quality: str) -> Dict[str, Any]:
|
async def get_netease_stream_url(json_data: dict, video_quality: str) -> dict:
|
||||||
if not json_data['is_live']:
|
if not json_data['is_live']:
|
||||||
return json_data
|
return json_data
|
||||||
stream_list = json_data['stream_list']['resolution']
|
|
||||||
order = ['blueray', 'ultra', 'high', 'standard']
|
m3u8_url = json_data['m3u8_url']
|
||||||
sorted_keys = [key for key in order if key in stream_list]
|
flv_url = None
|
||||||
while len(sorted_keys) < 5:
|
if json_data.get('stream_list'):
|
||||||
sorted_keys.append(sorted_keys[-1])
|
stream_list = json_data['stream_list']['resolution']
|
||||||
quality_list = {'原画': 0, '蓝光': 0, '超清': 1, '高清': 2, '标清': 3, '流畅': 4}
|
order = ['blueray', 'ultra', 'high', 'standard']
|
||||||
selected_quality = sorted_keys[quality_list[video_quality]]
|
sorted_keys = [key for key in order if key in stream_list]
|
||||||
flv_url_list = stream_list[selected_quality]['cdn']
|
while len(sorted_keys) < 5:
|
||||||
selected_cdn = list(flv_url_list.keys())[0]
|
sorted_keys.append(sorted_keys[-1])
|
||||||
flv_url = flv_url_list[selected_cdn]
|
video_quality, quality_index = get_quality_index(video_quality)
|
||||||
|
selected_quality = sorted_keys[quality_index]
|
||||||
|
flv_url_list = stream_list[selected_quality]['cdn']
|
||||||
|
selected_cdn = list(flv_url_list.keys())[0]
|
||||||
|
flv_url = flv_url_list[selected_cdn]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"is_live": True,
|
"is_live": True,
|
||||||
"anchor_name": json_data['anchor_name'],
|
"anchor_name": json_data['anchor_name'],
|
||||||
|
"title": json_data['title'],
|
||||||
|
'quality': video_quality,
|
||||||
|
"m3u8_url": m3u8_url,
|
||||||
"flv_url": flv_url,
|
"flv_url": flv_url,
|
||||||
"record_url": flv_url
|
"record_url": flv_url or m3u8_url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_stream_url(json_data: dict, video_quality: str, url_type: str = 'm3u8', spec: bool = False,
|
async def get_stream_url(json_data: dict, video_quality: str, url_type: str = 'm3u8', spec: bool = False,
|
||||||
extra_key: Union[str, int] = None) -> Dict[str, Any]:
|
hls_extra_key: str | int = None, flv_extra_key: str | int = None) -> dict:
|
||||||
if not json_data['is_live']:
|
if not json_data['is_live']:
|
||||||
return json_data
|
return json_data
|
||||||
|
|
||||||
play_url_list = json_data['play_url_list']
|
play_url_list = json_data['play_url_list']
|
||||||
quality_list = {'原画': 0, '蓝光': 0, '超清': 1, '高清': 2, '标清': 3, '流畅': 4}
|
|
||||||
while len(play_url_list) < 5:
|
while len(play_url_list) < 5:
|
||||||
play_url_list.append(play_url_list[-1])
|
play_url_list.append(play_url_list[-1])
|
||||||
|
|
||||||
selected_quality = quality_list[video_quality]
|
video_quality, selected_quality = get_quality_index(video_quality)
|
||||||
data = {
|
data = {
|
||||||
"anchor_name": json_data['anchor_name'],
|
"anchor_name": json_data['anchor_name'],
|
||||||
"is_live": True
|
"is_live": True
|
||||||
}
|
}
|
||||||
if url_type == 'm3u8':
|
|
||||||
m3u8_url = play_url_list[selected_quality][extra_key] if extra_key else play_url_list[selected_quality]
|
def get_url(key):
|
||||||
data["m3u8_url"] = json_data['m3u8_url'] if spec else m3u8_url
|
play_url = play_url_list[selected_quality]
|
||||||
data["record_url"] = m3u8_url
|
return play_url[key] if key else play_url
|
||||||
|
|
||||||
|
if url_type == 'all':
|
||||||
|
m3u8_url = get_url(hls_extra_key)
|
||||||
|
flv_url = get_url(flv_extra_key)
|
||||||
|
data |= {
|
||||||
|
"m3u8_url": json_data['m3u8_url'] if spec else m3u8_url,
|
||||||
|
"flv_url": json_data['flv_url'] if spec else flv_url,
|
||||||
|
"record_url": m3u8_url
|
||||||
|
}
|
||||||
|
elif url_type == 'm3u8':
|
||||||
|
m3u8_url = get_url(hls_extra_key)
|
||||||
|
data |= {"m3u8_url": json_data['m3u8_url'] if spec else m3u8_url, "record_url": m3u8_url}
|
||||||
else:
|
else:
|
||||||
flv = play_url_list[selected_quality][extra_key] if extra_key else play_url_list[selected_quality]
|
flv_url = get_url(flv_extra_key)
|
||||||
data["flv_url"] = flv
|
data |= {"flv_url": flv_url, "record_url": flv_url}
|
||||||
data["record_url"] = flv
|
data['title'] = json_data.get('title')
|
||||||
return data
|
data['quality'] = video_quality
|
||||||
|
return data
|
||||||
206
src/utils.py
Normal file
206
src/utils.py
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
import string
|
||||||
|
from pathlib import Path
|
||||||
|
import functools
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
from collections import OrderedDict
|
||||||
|
import execjs
|
||||||
|
from .logger import logger
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
OptionalStr = str | None
|
||||||
|
OptionalDict = dict | None
|
||||||
|
|
||||||
|
|
||||||
|
class Color:
|
||||||
|
RED = "\033[31m"
|
||||||
|
GREEN = "\033[32m"
|
||||||
|
YELLOW = "\033[33m"
|
||||||
|
BLUE = "\033[34m"
|
||||||
|
MAGENTA = "\033[35m"
|
||||||
|
CYAN = "\033[36m"
|
||||||
|
WHITE = "\033[37m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_colored(text, color):
|
||||||
|
print(f"{color}{text}{Color.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def trace_error_decorator(func: callable) -> callable:
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args: list, **kwargs: dict) -> Any:
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except execjs.ProgramError:
|
||||||
|
logger.warning('Failed to execute JS code. Please check if the Node.js environment')
|
||||||
|
except Exception as e:
|
||||||
|
error_line = traceback.extract_tb(e.__traceback__)[-1].lineno
|
||||||
|
error_info = f"message: type: {type(e).__name__}, {str(e)} in function {func.__name__} at line: {error_line}"
|
||||||
|
logger.error(error_info)
|
||||||
|
return []
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def check_md5(file_path: str | Path) -> str:
|
||||||
|
with open(file_path, 'rb') as fp:
|
||||||
|
file_md5 = hashlib.md5(fp.read()).hexdigest()
|
||||||
|
return file_md5
|
||||||
|
|
||||||
|
|
||||||
|
def dict_to_cookie_str(cookies_dict: dict) -> str:
|
||||||
|
cookie_str = '; '.join([f"{key}={value}" for key, value in cookies_dict.items()])
|
||||||
|
return cookie_str
|
||||||
|
|
||||||
|
|
||||||
|
def read_config_value(file_path: str | Path, section: str, key: str) -> str | None:
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
|
||||||
|
try:
|
||||||
|
config.read(file_path, encoding='utf-8-sig')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error occurred while reading the configuration file: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if section in config:
|
||||||
|
if key in config[section]:
|
||||||
|
return config[section][key]
|
||||||
|
else:
|
||||||
|
print(f"Key [{key}] does not exist in section [{section}].")
|
||||||
|
else:
|
||||||
|
print(f"Section [{section}] does not exist in the file.")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def update_config(file_path: str | Path, section: str, key: str, new_value: str) -> None:
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
|
||||||
|
try:
|
||||||
|
config.read(file_path, encoding='utf-8-sig')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred while reading the configuration file: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if section not in config:
|
||||||
|
print(f"Section [{section}] does not exist in the file.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 转义%字符
|
||||||
|
escaped_value = new_value.replace('%', '%%')
|
||||||
|
config[section][key] = escaped_value
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'w', encoding='utf-8-sig') as configfile:
|
||||||
|
config.write(configfile)
|
||||||
|
print(f"The value of {key} under [{section}] in the configuration file has been updated.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error occurred while writing to the configuration file: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_paths(directory: str) -> list:
|
||||||
|
file_paths = []
|
||||||
|
for root, dirs, files in os.walk(directory):
|
||||||
|
for file in files:
|
||||||
|
file_paths.append(os.path.join(root, file))
|
||||||
|
return file_paths
|
||||||
|
|
||||||
|
|
||||||
|
def remove_emojis(text: str, replace_text: str = '') -> str:
|
||||||
|
emoji_pattern = re.compile(
|
||||||
|
"["
|
||||||
|
"\U0001F1E0-\U0001F1FF" # flags (iOS)
|
||||||
|
"\U0001F300-\U0001F5FF" # symbols & pictographs
|
||||||
|
"\U0001F600-\U0001F64F" # emoticons
|
||||||
|
"\U0001F680-\U0001F6FF" # transport & map symbols
|
||||||
|
"\U0001F700-\U0001F77F" # alchemical symbols
|
||||||
|
"\U0001F780-\U0001F7FF" # Geometric Shapes Extended
|
||||||
|
"\U0001F800-\U0001F8FF" # Supplemental Arrows-C
|
||||||
|
"\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs
|
||||||
|
"\U0001FA00-\U0001FA6F" # Chess Symbols
|
||||||
|
"\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A
|
||||||
|
"\U00002702-\U000027B0" # Dingbats
|
||||||
|
"]+",
|
||||||
|
flags=re.UNICODE
|
||||||
|
)
|
||||||
|
return emoji_pattern.sub(replace_text, text)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_duplicate_lines(file_path: str | Path) -> None:
|
||||||
|
unique_lines = OrderedDict()
|
||||||
|
text_encoding = 'utf-8-sig'
|
||||||
|
with open(file_path, 'r', encoding=text_encoding) as input_file:
|
||||||
|
for line in input_file:
|
||||||
|
unique_lines[line.strip()] = None
|
||||||
|
with open(file_path, 'w', encoding=text_encoding) as output_file:
|
||||||
|
for line in unique_lines:
|
||||||
|
output_file.write(line + '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def check_disk_capacity(file_path: str | Path, show: bool = False) -> float:
|
||||||
|
absolute_path = os.path.abspath(file_path)
|
||||||
|
directory = os.path.dirname(absolute_path)
|
||||||
|
disk_usage = shutil.disk_usage(directory)
|
||||||
|
disk_root = Path(directory).anchor
|
||||||
|
free_space_gb = disk_usage.free / (1024 ** 3)
|
||||||
|
if show:
|
||||||
|
print(f"{disk_root} Total: {disk_usage.total / (1024 ** 3):.2f} GB "
|
||||||
|
f"Used: {disk_usage.used / (1024 ** 3):.2f} GB "
|
||||||
|
f"Free: {free_space_gb:.2f} GB\n")
|
||||||
|
return free_space_gb
|
||||||
|
|
||||||
|
|
||||||
|
def handle_proxy_addr(proxy_addr):
|
||||||
|
if proxy_addr:
|
||||||
|
if not proxy_addr.startswith('http'):
|
||||||
|
proxy_addr = 'http://' + proxy_addr
|
||||||
|
else:
|
||||||
|
proxy_addr = None
|
||||||
|
return proxy_addr
|
||||||
|
|
||||||
|
|
||||||
|
def generate_random_string(length: int) -> str:
|
||||||
|
characters = string.ascii_uppercase + string.digits
|
||||||
|
random_string = ''.join(random.choices(characters, k=length))
|
||||||
|
return random_string
|
||||||
|
|
||||||
|
|
||||||
|
def jsonp_to_json(jsonp_str: str) -> OptionalDict:
|
||||||
|
pattern = r'(\w+)\((.*)\);?$'
|
||||||
|
match = re.search(pattern, jsonp_str)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
_, json_str = match.groups()
|
||||||
|
json_obj = json.loads(json_str)
|
||||||
|
return json_obj
|
||||||
|
else:
|
||||||
|
raise Exception("No JSON data found in JSONP response.")
|
||||||
|
|
||||||
|
|
||||||
|
def replace_url(file_path: str | Path, old: str, new: str) -> None:
|
||||||
|
with open(file_path, 'r', encoding='utf-8-sig') as f:
|
||||||
|
content = f.read()
|
||||||
|
if old in content:
|
||||||
|
with open(file_path, 'w', encoding='utf-8-sig') as f:
|
||||||
|
f.write(content.replace(old, new))
|
||||||
|
|
||||||
|
|
||||||
|
def get_query_params(url: str, param_name: OptionalStr) -> dict | list[str]:
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
query_params = parse_qs(parsed_url.query)
|
||||||
|
|
||||||
|
if param_name is None:
|
||||||
|
return query_params
|
||||||
|
else:
|
||||||
|
values = query_params.get(param_name, [])
|
||||||
|
return values
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user