Spaces:
Running
Running
update
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +5 -0
- .gitignore +32 -0
- Dockerfile +24 -0
- README.md +1 -1
- data/douyin_live_info_collect.json +9 -0
- data/porter_tasks_dev.json +66 -0
- data/porter_tasks_prd.json +66 -0
- data/porter_tasks_prd2.json +273 -0
- install.sh +64 -0
- log.py +252 -0
- main.py +135 -0
- project_settings.py +27 -0
- requirements.txt +20 -0
- tabs/fs_tab.py +62 -0
- tabs/shell_tab.py +28 -0
- tabs/youtube_player_tab.py +195 -0
- toolbox/__init__.py +6 -0
- toolbox/asyncio/__init__.py +6 -0
- toolbox/asyncio/cacheout.py +194 -0
- toolbox/bilibili/__init__.py +6 -0
- toolbox/bilibili/bilibili_client.py +241 -0
- toolbox/bilibili/live/__init__.py +6 -0
- toolbox/bilibili/live/live_manager.py +301 -0
- toolbox/bilibili/video/__init__.py +5 -0
- toolbox/bilibili/video/draft_manager.py +279 -0
- toolbox/bilibili/video/video_manager.py +332 -0
- toolbox/design_patterns/__init__.py +6 -0
- toolbox/design_patterns/singleton.py +124 -0
- toolbox/douyin/__init__.py +6 -0
- toolbox/douyin/douyin_client.py +218 -0
- toolbox/douyin/homepage/__init__.py +6 -0
- toolbox/douyin/homepage/follow.py +111 -0
- toolbox/douyin/live/__init__.py +6 -0
- toolbox/douyin/live/live_recording.py +213 -0
- toolbox/douyin/video/__init__.py +6 -0
- toolbox/douyin/video/download.py +220 -0
- toolbox/exception.py +8 -0
- toolbox/json/__init__.py +6 -0
- toolbox/json/misc.py +63 -0
- toolbox/os/__init__.py +6 -0
- toolbox/os/command.py +59 -0
- toolbox/os/environment.py +114 -0
- toolbox/os/other.py +9 -0
- toolbox/porter/__init__.py +6 -0
- toolbox/porter/common/__init__.py +6 -0
- toolbox/porter/common/params.py +197 -0
- toolbox/porter/common/registrable.py +58 -0
- toolbox/porter/manager.py +67 -0
- toolbox/porter/tasks/__init__.py +10 -0
- toolbox/porter/tasks/base_task.py +37 -0
.dockerignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
.git/
|
| 3 |
+
.idea/
|
| 4 |
+
|
| 5 |
+
/examples/
|
.gitignore
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
.gradio/
|
| 3 |
+
.git/
|
| 4 |
+
.idea/
|
| 5 |
+
|
| 6 |
+
**/flagged/
|
| 7 |
+
**/logs/
|
| 8 |
+
**/__pycache__/
|
| 9 |
+
|
| 10 |
+
#/data/
|
| 11 |
+
/docs/
|
| 12 |
+
/dotenv/
|
| 13 |
+
/hub_datasets/
|
| 14 |
+
/script/
|
| 15 |
+
/thirdparty/
|
| 16 |
+
/trained_models/
|
| 17 |
+
/temp/
|
| 18 |
+
|
| 19 |
+
/data/live_info
|
| 20 |
+
/data/live_records
|
| 21 |
+
/data/video
|
| 22 |
+
|
| 23 |
+
/data/.DS_Store
|
| 24 |
+
|
| 25 |
+
/examples
|
| 26 |
+
|
| 27 |
+
**/*.flv
|
| 28 |
+
**/*.mp4
|
| 29 |
+
**/*.wav
|
| 30 |
+
**/*.xlsx
|
| 31 |
+
|
| 32 |
+
**/.DS_store
|
Dockerfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12
|
| 2 |
+
|
| 3 |
+
WORKDIR /code
|
| 4 |
+
|
| 5 |
+
COPY . /code
|
| 6 |
+
|
| 7 |
+
RUN apt-get update
|
| 8 |
+
RUN apt-get install -y ffmpeg build-essential
|
| 9 |
+
|
| 10 |
+
RUN pip install --upgrade pip
|
| 11 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 12 |
+
|
| 13 |
+
RUN useradd -m -u 1000 user
|
| 14 |
+
|
| 15 |
+
USER user
|
| 16 |
+
|
| 17 |
+
ENV HOME=/home/user \
|
| 18 |
+
PATH=/home/user/.local/bin:$PATH
|
| 19 |
+
|
| 20 |
+
WORKDIR $HOME/app
|
| 21 |
+
|
| 22 |
+
COPY --chown=user . $HOME/app
|
| 23 |
+
|
| 24 |
+
CMD ["python3", "main.py"]
|
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
---
|
| 2 |
title: Video Platform
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: pink
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
|
|
|
| 1 |
---
|
| 2 |
title: Video Platform
|
| 3 |
+
emoji: 🎬
|
| 4 |
colorFrom: pink
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
data/douyin_live_info_collect.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"572033528289": {
|
| 3 |
+
"nickname": "🪭嘉玉(卷卷)书法",
|
| 4 |
+
"sec_uid": "MS4wLjABAAAAb0mqEsXmBehDdg2Q9mMA2T6YEWPGbEtYofSzX_bDnz4",
|
| 5 |
+
"room_id": "572033528289",
|
| 6 |
+
"status": 2,
|
| 7 |
+
"title": "🪭嘉玉写字✍️…正在直播"
|
| 8 |
+
}
|
| 9 |
+
}
|
data/porter_tasks_dev.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"enable": true,
|
| 4 |
+
"type": "douyin_live_to_bilibili",
|
| 5 |
+
"room_name": "老陈的退路",
|
| 6 |
+
"room_id": "330025930592",
|
| 7 |
+
"check_interval": 10,
|
| 8 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"enable": true,
|
| 12 |
+
"type": "douyin_live_to_bilibili",
|
| 13 |
+
"room_name": "老陈come_back",
|
| 14 |
+
"room_id": "78835697536",
|
| 15 |
+
"check_interval": 10,
|
| 16 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"enable": true,
|
| 20 |
+
"type": "douyin_live_to_bilibili",
|
| 21 |
+
"room_name": "清源第一帅",
|
| 22 |
+
"room_id": "654177813521",
|
| 23 |
+
"check_interval": 10,
|
| 24 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"enable": true,
|
| 28 |
+
"type": "douyin_live_to_bilibili",
|
| 29 |
+
"room_name": "清源人工智能研究院",
|
| 30 |
+
"room_id": "81728900292",
|
| 31 |
+
"check_interval": 10,
|
| 32 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"enable": true,
|
| 36 |
+
"type": "douyin_live_to_bilibili",
|
| 37 |
+
"room_name": "小熊Bella与老爸",
|
| 38 |
+
"room_id": "139751520143",
|
| 39 |
+
"check_interval": 10,
|
| 40 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"enable": true,
|
| 44 |
+
"type": "douyin_live_to_bilibili",
|
| 45 |
+
"room_name": "清源之虎",
|
| 46 |
+
"room_id": "998621457719",
|
| 47 |
+
"check_interval": 10,
|
| 48 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"enable": true,
|
| 52 |
+
"type": "douyin_live_to_bilibili",
|
| 53 |
+
"room_name": "老陈真是好人",
|
| 54 |
+
"room_id": "599130203190",
|
| 55 |
+
"check_interval": 10,
|
| 56 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"enable": true,
|
| 60 |
+
"type": "douyin_live_to_bilibili",
|
| 61 |
+
"room_name": "老陈小帮手",
|
| 62 |
+
"room_id": "738682070097",
|
| 63 |
+
"check_interval": 10,
|
| 64 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 65 |
+
}
|
| 66 |
+
]
|
data/porter_tasks_prd.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"enable": true,
|
| 4 |
+
"type": "douyin_live_to_bilibili",
|
| 5 |
+
"room_name": "老陈的退路",
|
| 6 |
+
"room_id": "330025930592",
|
| 7 |
+
"check_interval": 10,
|
| 8 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"enable": true,
|
| 12 |
+
"type": "douyin_live_to_bilibili",
|
| 13 |
+
"room_name": "老陈come_back",
|
| 14 |
+
"room_id": "78835697536",
|
| 15 |
+
"check_interval": 10,
|
| 16 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"enable": true,
|
| 20 |
+
"type": "douyin_live_to_bilibili",
|
| 21 |
+
"room_name": "清源第一帅",
|
| 22 |
+
"room_id": "654177813521",
|
| 23 |
+
"check_interval": 10,
|
| 24 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"enable": true,
|
| 28 |
+
"type": "douyin_live_to_bilibili",
|
| 29 |
+
"room_name": "清源人工智能研究院",
|
| 30 |
+
"room_id": "81728900292",
|
| 31 |
+
"check_interval": 10,
|
| 32 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"enable": true,
|
| 36 |
+
"type": "douyin_live_to_bilibili",
|
| 37 |
+
"room_name": "小熊Bella与老爸",
|
| 38 |
+
"room_id": "139751520143",
|
| 39 |
+
"check_interval": 10,
|
| 40 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"enable": true,
|
| 44 |
+
"type": "douyin_live_to_bilibili",
|
| 45 |
+
"room_name": "清源之虎",
|
| 46 |
+
"room_id": "998621457719",
|
| 47 |
+
"check_interval": 10,
|
| 48 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"enable": true,
|
| 52 |
+
"type": "douyin_live_to_bilibili",
|
| 53 |
+
"room_name": "老陈真是好人",
|
| 54 |
+
"room_id": "599130203190",
|
| 55 |
+
"check_interval": 10,
|
| 56 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials",
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"enable": true,
|
| 60 |
+
"type": "douyin_live_to_bilibili",
|
| 61 |
+
"room_name": "老陈小帮手",
|
| 62 |
+
"room_id": "738682070097",
|
| 63 |
+
"check_interval": 10,
|
| 64 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials",
|
| 65 |
+
}
|
| 66 |
+
]
|
data/porter_tasks_prd2.json
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"enable": false,
|
| 4 |
+
"type": "douyin_live_info_collect",
|
| 5 |
+
"check_interval": 10,
|
| 6 |
+
"key_of_credentials": "douyin_wentao_credentials",
|
| 7 |
+
"output_file": "data/douyin_live_info_collect.json"
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"enable": true,
|
| 11 |
+
"type": "douyin_video_download",
|
| 12 |
+
"user_name": "肖峰说新能源",
|
| 13 |
+
"sec_user_id": "MS4wLjABAAAAQinRMLyQNYA45OYXoCDrwszhRGaDVirRE1fTNSaGGkc",
|
| 14 |
+
"check_interval": 900,
|
| 15 |
+
"key_of_credentials": "douyin_wentao_credentials",
|
| 16 |
+
"min_date2": "2025-08-06 00:00:00",
|
| 17 |
+
"output_video_dir": "data/video/douyin/肖峰说新能源",
|
| 18 |
+
"output_video_info_file": "data/video/douyin/肖峰说新能源/file_info.json"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"enable": true,
|
| 22 |
+
"type": "douyin_video_download",
|
| 23 |
+
"user_name": "陈杰森 资本NewBoombap",
|
| 24 |
+
"sec_user_id": "MS4wLjABAAAATGoBrO7yiJ3q9go4fxq9JXjrnP1bFpdkgKckC1IpfXA_vrjSmL9ZtjmTju8ApwbT",
|
| 25 |
+
"check_interval": 900,
|
| 26 |
+
"min_date2": "2025-09-06 00:00:00",
|
| 27 |
+
"output_video_dir": "data/video/douyin/陈杰森",
|
| 28 |
+
"output_video_info_file": "data/video/douyin/陈杰森/file_info.json"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"enable": true,
|
| 32 |
+
"type": "douyin_video_download",
|
| 33 |
+
"user_name": "小熊Bella与老爸",
|
| 34 |
+
"sec_user_id": "MS4wLjABAAAA49QFP6YhorLIIX9M-FiZeKxmqhqXlttluSsZeaxvxzU",
|
| 35 |
+
"check_interval": 900,
|
| 36 |
+
"min_date2": "2025-09-06 00:00:00",
|
| 37 |
+
"output_video_dir": "data/video/douyin/陈杰森",
|
| 38 |
+
"output_video_info_file": "data/video/douyin/陈杰森/file_info.json"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"enable": true,
|
| 42 |
+
"type": "douyin_video_download",
|
| 43 |
+
"user_name": "清华陈晶聊商业",
|
| 44 |
+
"sec_user_id": "MS4wLjABAAAAV5oVsV-RjxHKrcCuqQotWtHvT8_Y7z_aQnTvT61slic",
|
| 45 |
+
"check_interval": 900,
|
| 46 |
+
"min_date2": "2025-09-06 00:00:00",
|
| 47 |
+
"output_video_dir": "data/video/douyin/清华陈晶",
|
| 48 |
+
"output_video_info_file": "data/video/douyin/清华陈晶/file_info.json"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"enable": true,
|
| 52 |
+
"type": "douyin_video_download",
|
| 53 |
+
"user_name": "清华陈晶聊直播",
|
| 54 |
+
"sec_user_id": "MS4wLjABAAAARKzB1ApIHOuHlrl8Hqg_0RxIp-2Dz-AW3ipYfCCLr6wX5Y7ewfRce-QQ_19w7R34",
|
| 55 |
+
"check_interval": 900,
|
| 56 |
+
"min_date2": "2025-09-06 00:00:00",
|
| 57 |
+
"output_video_dir": "data/video/douyin/清华陈晶",
|
| 58 |
+
"output_video_info_file": "data/video/douyin/清华陈晶/file_info.json"
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"enable": true,
|
| 62 |
+
"type": "douyin_video_download",
|
| 63 |
+
"user_name": "清华陈晶聊创业",
|
| 64 |
+
"sec_user_id": "MS4wLjABAAAAwRmjwKuBA0K6VSrBYevRHrG6-c7UFppdICgKqcYhVDWlza3_Xj8f4R8H252e8tiF",
|
| 65 |
+
"check_interval": 900,
|
| 66 |
+
"min_date2": "2025-09-06 00:00:00",
|
| 67 |
+
"output_video_dir": "data/video/douyin/清华陈晶",
|
| 68 |
+
"output_video_info_file": "data/video/douyin/清华陈晶/file_info.json"
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"enable": true,
|
| 72 |
+
"type": "douyin_video_download",
|
| 73 |
+
"user_name": "吕晓彤",
|
| 74 |
+
"sec_user_id": "MS4wLjABAAAAqejZxZKopDBDEzxcQp-_1b019FfM05C0NzjQNpc5ylU",
|
| 75 |
+
"check_interval": 900,
|
| 76 |
+
"min_date2": "2025-09-06 00:00:00",
|
| 77 |
+
"output_video_dir": "data/video/douyin/吕晓彤",
|
| 78 |
+
"output_video_info_file": "data/video/douyin/吕晓彤/file_info.json"
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"enable": true,
|
| 82 |
+
"type": "douyin_video_download",
|
| 83 |
+
"user_name": "吕晓彤视野",
|
| 84 |
+
"sec_user_id": "MS4wLjABAAAAenYfLf-t_uRZhgQmEFOLN1iY3l2FTo4ToyC6wGEQVAH3i763q4-QgtUEleMB8n-m",
|
| 85 |
+
"check_interval": 900,
|
| 86 |
+
"min_date2": "2025-09-06 00:00:00",
|
| 87 |
+
"output_video_dir": "data/video/douyin/吕晓彤",
|
| 88 |
+
"output_video_info_file": "data/video/douyin/吕晓彤/file_info.json"
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"enable": true,
|
| 92 |
+
"type": "douyin_video_download",
|
| 93 |
+
"user_name": "吕晓彤说",
|
| 94 |
+
"sec_user_id": "MS4wLjABAAAAkwn_ZhJXBB5f4qNT4mn_uk5hOmaCFS503C3tSHiSzSQADUpFTPuPHx4ZVScKQ1Yl",
|
| 95 |
+
"check_interval": 900,
|
| 96 |
+
"min_date2": "2025-09-06 00:00:00",
|
| 97 |
+
"output_video_dir": "data/video/douyin/吕晓彤",
|
| 98 |
+
"output_video_info_file": "data/video/douyin/吕晓彤/file_info.json"
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"enable": true,
|
| 102 |
+
"type": "douyin_video_download",
|
| 103 |
+
"user_name": "吕晓彤的诗与远方",
|
| 104 |
+
"sec_user_id": "MS4wLjABAAAAnDI9XdGvKm9azWhg0qOviLt9xKTVT0E2fu7xOMDiMq_KJp2TdYPXvuhG8leGj-p6",
|
| 105 |
+
"check_interval": 900,
|
| 106 |
+
"min_date2": "2025-09-06 00:00:00",
|
| 107 |
+
"output_video_dir": "data/video/douyin/吕晓彤",
|
| 108 |
+
"output_video_info_file": "data/video/douyin/吕晓彤/file_info.json"
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"enable": true,
|
| 112 |
+
"type": "douyin_live_record",
|
| 113 |
+
"room_name": "老陌",
|
| 114 |
+
"room_id": "770758107267",
|
| 115 |
+
"check_interval": 10,
|
| 116 |
+
"output_video_dir": "data/live_records/douyin/老陌Live",
|
| 117 |
+
"output_video_info_file": "data/live_records/douyin/老陌Live/file_info.json"
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"enable": true,
|
| 121 |
+
"type": "douyin_live_record",
|
| 122 |
+
"room_name": "老陈的退路",
|
| 123 |
+
"room_id": "330025930592",
|
| 124 |
+
"check_interval": 10,
|
| 125 |
+
"output_video_dir": "data/live_records/douyin/陈杰森Live",
|
| 126 |
+
"output_video_info_file": "data/live_records/douyin/陈杰森Live/file_info.json"
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"enable": true,
|
| 130 |
+
"type": "douyin_live_record",
|
| 131 |
+
"room_name": "老陈come_back",
|
| 132 |
+
"room_id": "78835697536",
|
| 133 |
+
"check_interval": 10,
|
| 134 |
+
"output_video_dir": "data/live_records/douyin/陈杰森Live",
|
| 135 |
+
"output_video_info_file": "data/live_records/douyin/陈杰森Live/file_info.json"
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"enable": true,
|
| 139 |
+
"type": "douyin_live_record",
|
| 140 |
+
"room_name": "清源第一帅",
|
| 141 |
+
"room_id": "654177813521",
|
| 142 |
+
"check_interval": 10,
|
| 143 |
+
"output_video_dir": "data/live_records/douyin/陈杰森Live",
|
| 144 |
+
"output_video_info_file": "data/live_records/douyin/陈杰森Live/file_info.json"
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"enable": true,
|
| 148 |
+
"type": "douyin_live_record",
|
| 149 |
+
"room_name": "清源人工智能研究院",
|
| 150 |
+
"room_id": "81728900292",
|
| 151 |
+
"check_interval": 10,
|
| 152 |
+
"output_video_dir": "data/live_records/douyin/陈杰森Live",
|
| 153 |
+
"output_video_info_file": "data/live_records/douyin/陈杰森Live/file_info.json"
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"enable": true,
|
| 157 |
+
"type": "douyin_live_record",
|
| 158 |
+
"room_name": "小熊Bella与老爸",
|
| 159 |
+
"room_id": "139751520143",
|
| 160 |
+
"check_interval": 10,
|
| 161 |
+
"output_video_dir": "data/live_records/douyin/陈杰森Live",
|
| 162 |
+
"output_video_info_file": "data/live_records/douyin/陈杰森Live/file_info.json"
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"enable": true,
|
| 166 |
+
"type": "douyin_live_record",
|
| 167 |
+
"room_name": "清源之虎",
|
| 168 |
+
"room_id": "998621457719",
|
| 169 |
+
"check_interval": 10,
|
| 170 |
+
"output_video_dir": "data/live_records/douyin/陈杰森Live",
|
| 171 |
+
"output_video_info_file": "data/live_records/douyin/陈杰森Live/file_info.json"
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"enable": true,
|
| 175 |
+
"type": "douyin_live_record",
|
| 176 |
+
"room_name": "老陈真是好人",
|
| 177 |
+
"room_id": "599130203190",
|
| 178 |
+
"check_interval": 10,
|
| 179 |
+
"output_video_dir": "data/live_records/douyin/陈杰森Live",
|
| 180 |
+
"output_video_info_file": "data/live_records/douyin/陈杰森Live/file_info.json"
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"enable": true,
|
| 184 |
+
"type": "douyin_live_record",
|
| 185 |
+
"room_name": "老陈小帮手",
|
| 186 |
+
"room_id": "738682070097",
|
| 187 |
+
"check_interval": 10,
|
| 188 |
+
"output_video_dir": "data/live_records/douyin/陈杰森Live",
|
| 189 |
+
"output_video_info_file": "data/live_records/douyin/陈杰森Live/file_info.json"
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"enable": true,
|
| 193 |
+
"type": "douyin_live_record",
|
| 194 |
+
"room_name": "吕晓彤",
|
| 195 |
+
"room_id": "25132757833",
|
| 196 |
+
"check_interval": 10,
|
| 197 |
+
"output_video_dir": "data/live_records/douyin/吕晓彤Live",
|
| 198 |
+
"output_video_info_file": "data/live_records/douyin/吕晓彤Live/file_info.json"
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
"enable": true,
|
| 202 |
+
"type": "douyin_live_record",
|
| 203 |
+
"room_name": "文韬武略",
|
| 204 |
+
"room_id": "1293783051",
|
| 205 |
+
"check_interval": 10,
|
| 206 |
+
"output_video_dir": "data/live_records/douyin/文韬武略Live",
|
| 207 |
+
"output_video_info_file": "data/live_records/douyin/文韬武略Live/file_info.json"
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"enable": true,
|
| 211 |
+
"type": "douyin_live_to_bilibili",
|
| 212 |
+
"room_name": "老陈的退路",
|
| 213 |
+
"room_id": "330025930592",
|
| 214 |
+
"check_interval": 10,
|
| 215 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"enable": true,
|
| 219 |
+
"type": "douyin_live_to_bilibili",
|
| 220 |
+
"room_name": "老陈come_back",
|
| 221 |
+
"room_id": "78835697536",
|
| 222 |
+
"check_interval": 10,
|
| 223 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"enable": true,
|
| 227 |
+
"type": "douyin_live_to_bilibili",
|
| 228 |
+
"room_name": "清源第一帅",
|
| 229 |
+
"room_id": "654177813521",
|
| 230 |
+
"check_interval": 10,
|
| 231 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"enable": true,
|
| 235 |
+
"type": "douyin_live_to_bilibili",
|
| 236 |
+
"room_name": "清源人工智能研究院",
|
| 237 |
+
"room_id": "81728900292",
|
| 238 |
+
"check_interval": 10,
|
| 239 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 240 |
+
},
|
| 241 |
+
{
|
| 242 |
+
"enable": true,
|
| 243 |
+
"type": "douyin_live_to_bilibili",
|
| 244 |
+
"room_name": "小熊Bella与老爸",
|
| 245 |
+
"room_id": "139751520143",
|
| 246 |
+
"check_interval": 10,
|
| 247 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"enable": true,
|
| 251 |
+
"type": "douyin_live_to_bilibili",
|
| 252 |
+
"room_name": "清源之虎",
|
| 253 |
+
"room_id": "998621457719",
|
| 254 |
+
"check_interval": 10,
|
| 255 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials"
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"enable": true,
|
| 259 |
+
"type": "douyin_live_to_bilibili",
|
| 260 |
+
"room_name": "老陈真是好人",
|
| 261 |
+
"room_id": "599130203190",
|
| 262 |
+
"check_interval": 10,
|
| 263 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials",
|
| 264 |
+
},
|
| 265 |
+
{
|
| 266 |
+
"enable": true,
|
| 267 |
+
"type": "douyin_live_to_bilibili",
|
| 268 |
+
"room_name": "老陈小帮手",
|
| 269 |
+
"room_id": "738682070097",
|
| 270 |
+
"check_interval": 10,
|
| 271 |
+
"key_of_credentials": "bilibili_chenjiesen_credentials",
|
| 272 |
+
}
|
| 273 |
+
]
|
install.sh
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
|
| 3 |
+
# bash install.sh --stage 2 --stop_stage 2 --system_version centos
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
python_version=3.12.1
|
| 7 |
+
system_version="centos";
|
| 8 |
+
|
| 9 |
+
verbose=true;
|
| 10 |
+
stage=-1
|
| 11 |
+
stop_stage=0
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# parse options
|
| 15 |
+
while true; do
|
| 16 |
+
[ -z "${1:-}" ] && break; # break if there are no arguments
|
| 17 |
+
case "$1" in
|
| 18 |
+
--*) name=$(echo "$1" | sed s/^--// | sed s/-/_/g);
|
| 19 |
+
eval '[ -z "${'"$name"'+xxx}" ]' && echo "$0: invalid option $1" 1>&2 && exit 1;
|
| 20 |
+
old_value="(eval echo \\$$name)";
|
| 21 |
+
if [ "${old_value}" == "true" ] || [ "${old_value}" == "false" ]; then
|
| 22 |
+
was_bool=true;
|
| 23 |
+
else
|
| 24 |
+
was_bool=false;
|
| 25 |
+
fi
|
| 26 |
+
|
| 27 |
+
# Set the variable to the right value-- the escaped quotes make it work if
|
| 28 |
+
# the option had spaces, like --cmd "queue.pl -sync y"
|
| 29 |
+
eval "${name}=\"$2\"";
|
| 30 |
+
|
| 31 |
+
# Check that Boolean-valued arguments are really Boolean.
|
| 32 |
+
if $was_bool && [[ "$2" != "true" && "$2" != "false" ]]; then
|
| 33 |
+
echo "$0: expected \"true\" or \"false\": $1 $2" 1>&2
|
| 34 |
+
exit 1;
|
| 35 |
+
fi
|
| 36 |
+
shift 2;
|
| 37 |
+
;;
|
| 38 |
+
|
| 39 |
+
*) break;
|
| 40 |
+
esac
|
| 41 |
+
done
|
| 42 |
+
|
| 43 |
+
work_dir="$(pwd)"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then
|
| 47 |
+
$verbose && echo "stage 1: install python"
|
| 48 |
+
cd "${work_dir}" || exit 1;
|
| 49 |
+
|
| 50 |
+
sh ./script/install_python.sh --python_version "${python_version}" --system_version "${system_version}"
|
| 51 |
+
fi
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then
|
| 55 |
+
$verbose && echo "stage 2: create virtualenv"
|
| 56 |
+
|
| 57 |
+
# /usr/local/python-3.9.9/bin/virtualenv LiveRecorder
|
| 58 |
+
# source /data/local/bin/LiveRecorder/bin/activate
|
| 59 |
+
/usr/local/python-${python_version}/bin/pip3 install virtualenv
|
| 60 |
+
mkdir -p /data/local/bin
|
| 61 |
+
cd /data/local/bin || exit 1;
|
| 62 |
+
/usr/local/python-${python_version}/bin/virtualenv nx_denoise
|
| 63 |
+
|
| 64 |
+
fi
|
log.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import logging
|
| 5 |
+
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
| 6 |
+
import os
|
| 7 |
+
from zoneinfo import ZoneInfo # Python 3.9+ 自带,无需安装
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def get_converter(tz_info: str = "Asia/Shanghai"):
|
| 11 |
+
def converter(timestamp):
|
| 12 |
+
dt = datetime.fromtimestamp(timestamp, ZoneInfo(tz_info))
|
| 13 |
+
result = dt.timetuple()
|
| 14 |
+
return result
|
| 15 |
+
return converter
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def setup_size_rotating(log_directory: str, tz_info: str = "Asia/Shanghai"):
|
| 19 |
+
fmt = "%(asctime)s|%(name)s|%(levelname)s|%(filename)s|%(lineno)d|%(message)s"
|
| 20 |
+
|
| 21 |
+
formatter = logging.Formatter(
|
| 22 |
+
fmt=fmt,
|
| 23 |
+
datefmt="%Y-%m-%d %H:%M:%S %z"
|
| 24 |
+
)
|
| 25 |
+
formatter.converter = get_converter(tz_info)
|
| 26 |
+
|
| 27 |
+
stream_handler = logging.StreamHandler()
|
| 28 |
+
stream_handler.setLevel(logging.INFO)
|
| 29 |
+
stream_handler.setFormatter(formatter)
|
| 30 |
+
|
| 31 |
+
# main
|
| 32 |
+
main_logger = logging.getLogger("main")
|
| 33 |
+
main_logger.addHandler(stream_handler)
|
| 34 |
+
main_info_file_handler = RotatingFileHandler(
|
| 35 |
+
filename=os.path.join(log_directory, "main.log"),
|
| 36 |
+
maxBytes=100*1024*1024, # 100MB
|
| 37 |
+
encoding="utf-8",
|
| 38 |
+
backupCount=2,
|
| 39 |
+
)
|
| 40 |
+
main_info_file_handler.setLevel(logging.INFO)
|
| 41 |
+
main_info_file_handler.setFormatter(formatter)
|
| 42 |
+
main_logger.addHandler(main_info_file_handler)
|
| 43 |
+
|
| 44 |
+
# http
|
| 45 |
+
http_logger = logging.getLogger("http")
|
| 46 |
+
http_file_handler = RotatingFileHandler(
|
| 47 |
+
filename=os.path.join(log_directory, "http.log"),
|
| 48 |
+
maxBytes=100*1024*1024, # 100MB
|
| 49 |
+
encoding="utf-8",
|
| 50 |
+
backupCount=2,
|
| 51 |
+
)
|
| 52 |
+
http_file_handler.setLevel(logging.DEBUG)
|
| 53 |
+
http_file_handler.setFormatter(formatter)
|
| 54 |
+
http_logger.addHandler(http_file_handler)
|
| 55 |
+
|
| 56 |
+
# api
|
| 57 |
+
api_logger = logging.getLogger("api")
|
| 58 |
+
api_file_handler = RotatingFileHandler(
|
| 59 |
+
filename=os.path.join(log_directory, "api.log"),
|
| 60 |
+
maxBytes=10*1024*1024, # 10MB
|
| 61 |
+
encoding="utf-8",
|
| 62 |
+
backupCount=2,
|
| 63 |
+
)
|
| 64 |
+
api_file_handler.setLevel(logging.DEBUG)
|
| 65 |
+
api_file_handler.setFormatter(formatter)
|
| 66 |
+
api_logger.addHandler(api_file_handler)
|
| 67 |
+
|
| 68 |
+
# toolbox
|
| 69 |
+
toolbox_logger = logging.getLogger("toolbox")
|
| 70 |
+
toolbox_logger.addHandler(stream_handler)
|
| 71 |
+
toolbox_file_handler = RotatingFileHandler(
|
| 72 |
+
filename=os.path.join(log_directory, "toolbox.log"),
|
| 73 |
+
maxBytes=10*1024*1024, # 10MB
|
| 74 |
+
encoding="utf-8",
|
| 75 |
+
backupCount=2,
|
| 76 |
+
)
|
| 77 |
+
toolbox_file_handler.setLevel(logging.DEBUG)
|
| 78 |
+
toolbox_file_handler.setFormatter(formatter)
|
| 79 |
+
toolbox_logger.addHandler(toolbox_file_handler)
|
| 80 |
+
|
| 81 |
+
# alarm
|
| 82 |
+
alarm_logger = logging.getLogger("alarm")
|
| 83 |
+
alarm_file_handler = RotatingFileHandler(
|
| 84 |
+
filename=os.path.join(log_directory, "alarm.log"),
|
| 85 |
+
maxBytes=1*1024*1024, # 1MB
|
| 86 |
+
encoding="utf-8",
|
| 87 |
+
backupCount=2,
|
| 88 |
+
)
|
| 89 |
+
alarm_file_handler.setLevel(logging.DEBUG)
|
| 90 |
+
alarm_file_handler.setFormatter(formatter)
|
| 91 |
+
alarm_logger.addHandler(alarm_file_handler)
|
| 92 |
+
|
| 93 |
+
debug_file_handler = RotatingFileHandler(
|
| 94 |
+
filename=os.path.join(log_directory, "debug.log"),
|
| 95 |
+
maxBytes=1*1024*1024, # 1MB
|
| 96 |
+
encoding="utf-8",
|
| 97 |
+
backupCount=2,
|
| 98 |
+
)
|
| 99 |
+
debug_file_handler.setLevel(logging.DEBUG)
|
| 100 |
+
debug_file_handler.setFormatter(formatter)
|
| 101 |
+
|
| 102 |
+
info_file_handler = RotatingFileHandler(
|
| 103 |
+
filename=os.path.join(log_directory, "info.log"),
|
| 104 |
+
maxBytes=1*1024*1024, # 1MB
|
| 105 |
+
encoding="utf-8",
|
| 106 |
+
backupCount=2,
|
| 107 |
+
)
|
| 108 |
+
info_file_handler.setLevel(logging.INFO)
|
| 109 |
+
info_file_handler.setFormatter(formatter)
|
| 110 |
+
|
| 111 |
+
error_file_handler = RotatingFileHandler(
|
| 112 |
+
filename=os.path.join(log_directory, "error.log"),
|
| 113 |
+
maxBytes=1*1024*1024, # 1MB
|
| 114 |
+
encoding="utf-8",
|
| 115 |
+
backupCount=2,
|
| 116 |
+
)
|
| 117 |
+
error_file_handler.setLevel(logging.ERROR)
|
| 118 |
+
error_file_handler.setFormatter(formatter)
|
| 119 |
+
|
| 120 |
+
logging.basicConfig(
|
| 121 |
+
level=logging.DEBUG,
|
| 122 |
+
datefmt="%a, %d %b %Y %H:%M:%S",
|
| 123 |
+
handlers=[
|
| 124 |
+
debug_file_handler,
|
| 125 |
+
info_file_handler,
|
| 126 |
+
error_file_handler,
|
| 127 |
+
]
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def setup_time_rotating(log_directory: str, tz_info: str = "Asia/Shanghai"):
|
| 132 |
+
fmt = "%(asctime)s|%(name)s|%(levelname)s|%(filename)s|%(lineno)d|%(message)s"
|
| 133 |
+
|
| 134 |
+
formatter = logging.Formatter(
|
| 135 |
+
fmt=fmt,
|
| 136 |
+
datefmt="%Y-%m-%d %H:%M:%S %z"
|
| 137 |
+
)
|
| 138 |
+
formatter.converter = get_converter(tz_info)
|
| 139 |
+
|
| 140 |
+
stream_handler = logging.StreamHandler()
|
| 141 |
+
stream_handler.setLevel(logging.INFO)
|
| 142 |
+
stream_handler.setFormatter(formatter)
|
| 143 |
+
|
| 144 |
+
# main
|
| 145 |
+
main_logger = logging.getLogger("main")
|
| 146 |
+
main_logger.addHandler(stream_handler)
|
| 147 |
+
main_info_file_handler = TimedRotatingFileHandler(
|
| 148 |
+
filename=os.path.join(log_directory, "main.log"),
|
| 149 |
+
encoding="utf-8",
|
| 150 |
+
when="midnight",
|
| 151 |
+
interval=1,
|
| 152 |
+
backupCount=7
|
| 153 |
+
)
|
| 154 |
+
main_info_file_handler.setLevel(logging.INFO)
|
| 155 |
+
main_info_file_handler.setFormatter(formatter)
|
| 156 |
+
main_logger.addHandler(main_info_file_handler)
|
| 157 |
+
|
| 158 |
+
# http
|
| 159 |
+
http_logger = logging.getLogger("http")
|
| 160 |
+
http_file_handler = TimedRotatingFileHandler(
|
| 161 |
+
filename=os.path.join(log_directory, "http.log"),
|
| 162 |
+
encoding='utf-8',
|
| 163 |
+
when="midnight",
|
| 164 |
+
interval=1,
|
| 165 |
+
backupCount=7
|
| 166 |
+
)
|
| 167 |
+
http_file_handler.setLevel(logging.DEBUG)
|
| 168 |
+
http_file_handler.setFormatter(formatter)
|
| 169 |
+
http_logger.addHandler(http_file_handler)
|
| 170 |
+
|
| 171 |
+
# api
|
| 172 |
+
api_logger = logging.getLogger("api")
|
| 173 |
+
api_file_handler = TimedRotatingFileHandler(
|
| 174 |
+
filename=os.path.join(log_directory, "api.log"),
|
| 175 |
+
encoding='utf-8',
|
| 176 |
+
when="midnight",
|
| 177 |
+
interval=1,
|
| 178 |
+
backupCount=7
|
| 179 |
+
)
|
| 180 |
+
api_file_handler.setLevel(logging.DEBUG)
|
| 181 |
+
api_file_handler.setFormatter(formatter)
|
| 182 |
+
api_logger.addHandler(api_file_handler)
|
| 183 |
+
|
| 184 |
+
# toolbox
|
| 185 |
+
toolbox_logger = logging.getLogger("toolbox")
|
| 186 |
+
toolbox_logger.addHandler(stream_handler)
|
| 187 |
+
toolbox_file_handler = RotatingFileHandler(
|
| 188 |
+
filename=os.path.join(log_directory, "toolbox.log"),
|
| 189 |
+
maxBytes=10*1024*1024, # 10MB
|
| 190 |
+
encoding="utf-8",
|
| 191 |
+
backupCount=2,
|
| 192 |
+
)
|
| 193 |
+
toolbox_file_handler.setLevel(logging.DEBUG)
|
| 194 |
+
toolbox_file_handler.setFormatter(formatter)
|
| 195 |
+
toolbox_logger.addHandler(toolbox_file_handler)
|
| 196 |
+
|
| 197 |
+
# alarm
|
| 198 |
+
alarm_logger = logging.getLogger("alarm")
|
| 199 |
+
alarm_file_handler = TimedRotatingFileHandler(
|
| 200 |
+
filename=os.path.join(log_directory, "alarm.log"),
|
| 201 |
+
encoding="utf-8",
|
| 202 |
+
when="midnight",
|
| 203 |
+
interval=1,
|
| 204 |
+
backupCount=7
|
| 205 |
+
)
|
| 206 |
+
alarm_file_handler.setLevel(logging.DEBUG)
|
| 207 |
+
alarm_file_handler.setFormatter(formatter)
|
| 208 |
+
alarm_logger.addHandler(alarm_file_handler)
|
| 209 |
+
|
| 210 |
+
debug_file_handler = TimedRotatingFileHandler(
|
| 211 |
+
filename=os.path.join(log_directory, "debug.log"),
|
| 212 |
+
encoding="utf-8",
|
| 213 |
+
when="D",
|
| 214 |
+
interval=1,
|
| 215 |
+
backupCount=7
|
| 216 |
+
)
|
| 217 |
+
debug_file_handler.setLevel(logging.DEBUG)
|
| 218 |
+
debug_file_handler.setFormatter(formatter)
|
| 219 |
+
|
| 220 |
+
info_file_handler = TimedRotatingFileHandler(
|
| 221 |
+
filename=os.path.join(log_directory, "info.log"),
|
| 222 |
+
encoding="utf-8",
|
| 223 |
+
when="D",
|
| 224 |
+
interval=1,
|
| 225 |
+
backupCount=7
|
| 226 |
+
)
|
| 227 |
+
info_file_handler.setLevel(logging.INFO)
|
| 228 |
+
info_file_handler.setFormatter(formatter)
|
| 229 |
+
|
| 230 |
+
error_file_handler = TimedRotatingFileHandler(
|
| 231 |
+
filename=os.path.join(log_directory, "error.log"),
|
| 232 |
+
encoding="utf-8",
|
| 233 |
+
when="D",
|
| 234 |
+
interval=1,
|
| 235 |
+
backupCount=7
|
| 236 |
+
)
|
| 237 |
+
error_file_handler.setLevel(logging.ERROR)
|
| 238 |
+
error_file_handler.setFormatter(formatter)
|
| 239 |
+
|
| 240 |
+
logging.basicConfig(
|
| 241 |
+
level=logging.DEBUG,
|
| 242 |
+
datefmt="%a, %d %b %Y %H:%M:%S",
|
| 243 |
+
handlers=[
|
| 244 |
+
debug_file_handler,
|
| 245 |
+
info_file_handler,
|
| 246 |
+
error_file_handler,
|
| 247 |
+
]
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
if __name__ == "__main__":
|
| 252 |
+
pass
|
main.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
docker build -t video_platform:v20250819_2052 .
|
| 5 |
+
docker stop video_platform_7871 && docker rm video_platform_7871
|
| 6 |
+
docker run -itd \
|
| 7 |
+
--name video_platform_7871 \
|
| 8 |
+
--restart=always \
|
| 9 |
+
--network host \
|
| 10 |
+
-e server_port=7870 \
|
| 11 |
+
video_platform:v20250819_2052 /bin/bash
|
| 12 |
+
|
| 13 |
+
docker run -itd \
|
| 14 |
+
--name video_platform_7871 \
|
| 15 |
+
--restart=always \
|
| 16 |
+
--network host \
|
| 17 |
+
-e server_port=7871 \
|
| 18 |
+
-v /data/tianxing/PycharmProjects/video_platform:/data/tianxing/PycharmProjects/video_platform \
|
| 19 |
+
python:3.12 /bin/bash
|
| 20 |
+
|
| 21 |
+
nohup python3 main.py --server_port 7871 &
|
| 22 |
+
"""
|
| 23 |
+
import argparse
|
| 24 |
+
import asyncio
|
| 25 |
+
import logging
|
| 26 |
+
import platform
|
| 27 |
+
import time
|
| 28 |
+
import threading
|
| 29 |
+
|
| 30 |
+
import gradio as gr
|
| 31 |
+
|
| 32 |
+
import log
|
| 33 |
+
from project_settings import environment, project_path, log_directory, time_zone_info
|
| 34 |
+
|
| 35 |
+
log.setup_size_rotating(log_directory=log_directory, tz_info=time_zone_info)
|
| 36 |
+
|
| 37 |
+
from toolbox.os.command import Command
|
| 38 |
+
from toolbox.porter.manager import PorterManager
|
| 39 |
+
from tabs.fs_tab import get_fs_tab
|
| 40 |
+
from tabs.shell_tab import get_shell_tab
|
| 41 |
+
from tabs.youtube_player_tab import get_youtube_player_tab
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
logger = logging.getLogger("main")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def get_args():
|
| 48 |
+
parser = argparse.ArgumentParser()
|
| 49 |
+
parser.add_argument(
|
| 50 |
+
"--live_info_collect_tasks_file",
|
| 51 |
+
default=(project_path / "data/live_info_collect_tasks.json").as_posix(),
|
| 52 |
+
type=str
|
| 53 |
+
)
|
| 54 |
+
parser.add_argument(
|
| 55 |
+
"--live_recorder_tasks_file",
|
| 56 |
+
default=(project_path / "data/live_recorder_tasks.json").as_posix(),
|
| 57 |
+
type=str
|
| 58 |
+
)
|
| 59 |
+
parser.add_argument(
|
| 60 |
+
"--video_download_tasks_file",
|
| 61 |
+
default=(project_path / "data/video_download_tasks.json").as_posix(),
|
| 62 |
+
type=str
|
| 63 |
+
)
|
| 64 |
+
parser.add_argument(
|
| 65 |
+
"--youtube_video_upload_tasks_file",
|
| 66 |
+
default=(project_path / "data/youtube_video_upload_tasks.json").as_posix(),
|
| 67 |
+
type=str
|
| 68 |
+
)
|
| 69 |
+
parser.add_argument(
|
| 70 |
+
"--bilibili_video_upload_tasks_file",
|
| 71 |
+
default=(project_path / "data/bilibili_video_upload_tasks.json").as_posix(),
|
| 72 |
+
type=str
|
| 73 |
+
)
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--live_records_dir",
|
| 76 |
+
default=(project_path / "data/live_records").as_posix(),
|
| 77 |
+
type=str
|
| 78 |
+
)
|
| 79 |
+
parser.add_argument(
|
| 80 |
+
"--server_port",
|
| 81 |
+
default=environment.get("server_port", 7860),
|
| 82 |
+
type=int
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
args = parser.parse_args()
|
| 86 |
+
return args
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def shell(cmd: str):
|
| 90 |
+
return Command.popen(cmd)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def async_thread_wrapper(coro_task):
|
| 94 |
+
logger.info("background_task start")
|
| 95 |
+
|
| 96 |
+
loop = asyncio.new_event_loop()
|
| 97 |
+
asyncio.set_event_loop(loop)
|
| 98 |
+
loop.run_until_complete(coro_task)
|
| 99 |
+
return
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def main():
|
| 103 |
+
args = get_args()
|
| 104 |
+
|
| 105 |
+
porter_task_file = environment.get("porter_tasks_file")
|
| 106 |
+
|
| 107 |
+
# porter manager
|
| 108 |
+
video_download_manager = PorterManager(porter_task_file)
|
| 109 |
+
video_download_thread = threading.Thread(target=async_thread_wrapper,
|
| 110 |
+
args=(video_download_manager.run(),),
|
| 111 |
+
daemon=True)
|
| 112 |
+
video_download_thread.start()
|
| 113 |
+
time.sleep(5)
|
| 114 |
+
|
| 115 |
+
# ui
|
| 116 |
+
with gr.Blocks() as blocks:
|
| 117 |
+
gr.Markdown(value="live recording.")
|
| 118 |
+
with gr.Tabs():
|
| 119 |
+
_ = get_fs_tab()
|
| 120 |
+
_ = get_shell_tab()
|
| 121 |
+
_ = get_youtube_player_tab()
|
| 122 |
+
|
| 123 |
+
# http://127.0.0.1:7870/
|
| 124 |
+
# http://10.75.27.247:7870/
|
| 125 |
+
blocks.queue().launch(
|
| 126 |
+
# share=True,
|
| 127 |
+
share=False if platform.system() == "Windows" else False,
|
| 128 |
+
server_name="127.0.0.1" if platform.system() == "Windows" else "0.0.0.0",
|
| 129 |
+
server_port=args.server_port
|
| 130 |
+
)
|
| 131 |
+
return
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
main()
|
project_settings.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
from toolbox.os.environment import EnvironmentManager
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
project_path = os.path.abspath(os.path.dirname(__file__))
|
| 10 |
+
project_path = Path(project_path)
|
| 11 |
+
|
| 12 |
+
time_zone_info = "Asia/Shanghai"
|
| 13 |
+
|
| 14 |
+
log_directory = project_path / "logs"
|
| 15 |
+
log_directory.mkdir(parents=True, exist_ok=True)
|
| 16 |
+
|
| 17 |
+
temp_directory = project_path / "temp"
|
| 18 |
+
temp_directory.mkdir(parents=True, exist_ok=True)
|
| 19 |
+
|
| 20 |
+
environment = EnvironmentManager(
|
| 21 |
+
path=os.path.join(project_path, "dotenv"),
|
| 22 |
+
env=os.environ.get("environment", "dev"),
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
if __name__ == '__main__':
|
| 27 |
+
pass
|
requirements.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==5.33.0
|
| 2 |
+
python-dotenv==1.0.1
|
| 3 |
+
pandas==2.2.3
|
| 4 |
+
openpyxl==3.1.5
|
| 5 |
+
streamlink>=6.8.2
|
| 6 |
+
httpx[http2]>=0.27.0
|
| 7 |
+
ffmpeg-python>=0.2.0
|
| 8 |
+
loguru>=0.7.2
|
| 9 |
+
jsonpath-ng>=1.6.1
|
| 10 |
+
jsengine>=1.0.7.post1
|
| 11 |
+
quickjs>=1.19.4
|
| 12 |
+
httpx-socks[asyncio]>=0.9.1
|
| 13 |
+
google-api-python-client
|
| 14 |
+
google-auth-oauthlib
|
| 15 |
+
google-auth-httplib2
|
| 16 |
+
beautifulsoup4==4.13.4
|
| 17 |
+
aiocache==0.12.3
|
| 18 |
+
biliupload
|
| 19 |
+
cacheout
|
| 20 |
+
tenacity
|
tabs/fs_tab.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import gradio as gr
|
| 4 |
+
|
| 5 |
+
from project_settings import project_path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def get_fs_tab():
|
| 9 |
+
with gr.TabItem("fs"):
|
| 10 |
+
with gr.Row():
|
| 11 |
+
with gr.Column(scale=3):
|
| 12 |
+
fs_filename = gr.Textbox(label="filename", max_lines=10)
|
| 13 |
+
fs_file = gr.File(label="file")
|
| 14 |
+
fs_file_dir = gr.Textbox(value="data", label="file_dir")
|
| 15 |
+
fs_query = gr.Button("query", variant="primary")
|
| 16 |
+
with gr.Column(scale=7):
|
| 17 |
+
fs_filelist_dataset_state = gr.State(value=[])
|
| 18 |
+
fs_filelist_dataset = gr.Dataset(
|
| 19 |
+
components=[fs_filename, fs_file],
|
| 20 |
+
samples=fs_filelist_dataset_state.value,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def when_click_query_files(file_dir: str = "data"):
|
| 24 |
+
file_dir = project_path / file_dir
|
| 25 |
+
dataset_state = list()
|
| 26 |
+
for filename in file_dir.glob("**/*.*"):
|
| 27 |
+
if filename.is_dir():
|
| 28 |
+
continue
|
| 29 |
+
if filename.stem.startswith("."):
|
| 30 |
+
continue
|
| 31 |
+
if filename.name.endswith(".py"):
|
| 32 |
+
continue
|
| 33 |
+
dataset_state.append((
|
| 34 |
+
filename.relative_to(file_dir).as_posix(),
|
| 35 |
+
filename.as_posix(),
|
| 36 |
+
))
|
| 37 |
+
|
| 38 |
+
dataset = gr.Dataset(
|
| 39 |
+
components=[fs_filename, fs_file],
|
| 40 |
+
samples=dataset_state,
|
| 41 |
+
)
|
| 42 |
+
return dataset_state, dataset
|
| 43 |
+
|
| 44 |
+
fs_filelist_dataset.click(
|
| 45 |
+
fn=lambda x: (
|
| 46 |
+
x[1], x[1]
|
| 47 |
+
),
|
| 48 |
+
inputs=[fs_filelist_dataset],
|
| 49 |
+
outputs=[fs_filename, fs_file]
|
| 50 |
+
)
|
| 51 |
+
fs_query.click(
|
| 52 |
+
fn=when_click_query_files,
|
| 53 |
+
inputs=[fs_file_dir],
|
| 54 |
+
outputs=[fs_filelist_dataset_state, fs_filelist_dataset]
|
| 55 |
+
)
|
| 56 |
+
return locals()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
with gr.Blocks() as block:
|
| 61 |
+
fs_components = get_fs_tab()
|
| 62 |
+
block.launch()
|
tabs/shell_tab.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import gradio as gr
|
| 4 |
+
|
| 5 |
+
from toolbox.os.command import Command
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def shell(cmd: str):
|
| 9 |
+
return Command.popen(cmd)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_shell_tab():
|
| 13 |
+
with gr.TabItem("shell"):
|
| 14 |
+
shell_text = gr.Textbox(label="cmd")
|
| 15 |
+
shell_button = gr.Button("run")
|
| 16 |
+
shell_output = gr.Textbox(label="output", max_lines=100)
|
| 17 |
+
|
| 18 |
+
shell_button.click(
|
| 19 |
+
shell,
|
| 20 |
+
inputs=[shell_text, ],
|
| 21 |
+
outputs=[shell_output],
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
return locals()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
if __name__ == "__main__":
|
| 28 |
+
pass
|
tabs/youtube_player_tab.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
from jinja2 import Template
|
| 8 |
+
import tempfile
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
html_content = """
|
| 12 |
+
<!DOCTYPE html>
|
| 13 |
+
<html lang="en">
|
| 14 |
+
<head>
|
| 15 |
+
<meta charset="UTF-8">
|
| 16 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 17 |
+
<title>YouTube视频连续播放器</title>
|
| 18 |
+
<style>
|
| 19 |
+
body {
|
| 20 |
+
font-family: Arial, sans-serif;
|
| 21 |
+
max-width: 1600px;
|
| 22 |
+
margin: 0 auto;
|
| 23 |
+
padding: 20px;
|
| 24 |
+
text-align: center;
|
| 25 |
+
}
|
| 26 |
+
#player {
|
| 27 |
+
width: 1200px;
|
| 28 |
+
height: 675px;
|
| 29 |
+
# aspect-ratio: 16/9;
|
| 30 |
+
}
|
| 31 |
+
#playlist {
|
| 32 |
+
text-align: left;
|
| 33 |
+
margin: 20px 0;
|
| 34 |
+
}
|
| 35 |
+
.current {
|
| 36 |
+
font-weight: bold;
|
| 37 |
+
color: #ff0000;
|
| 38 |
+
}
|
| 39 |
+
</style>
|
| 40 |
+
</head>
|
| 41 |
+
<body>
|
| 42 |
+
<h1>YouTube视频连续播放器</h1>
|
| 43 |
+
<div id="player"></div>
|
| 44 |
+
<div id="playlist"></div>
|
| 45 |
+
<div id="status">准备播放...</div>
|
| 46 |
+
|
| 47 |
+
<script>
|
| 48 |
+
|
| 49 |
+
// 获取所有视频ID
|
| 50 |
+
const videoIds = {{ video_ids }};
|
| 51 |
+
|
| 52 |
+
let currentVideoIndex = 0;
|
| 53 |
+
let player;
|
| 54 |
+
|
| 55 |
+
// 加载YouTube IFrame API
|
| 56 |
+
function onYouTubeIframeAPIReady() {
|
| 57 |
+
player = new YT.Player('player', {
|
| 58 |
+
height: '390',
|
| 59 |
+
width: '640',
|
| 60 |
+
events: {
|
| 61 |
+
'onReady': onPlayerReady,
|
| 62 |
+
'onStateChange': onPlayerStateChange
|
| 63 |
+
}
|
| 64 |
+
});
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// 播放器准备就绪
|
| 68 |
+
function onPlayerReady(event) {
|
| 69 |
+
loadAndPlayVideo(currentVideoIndex);
|
| 70 |
+
updatePlaylist();
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// 播放器状态变化
|
| 74 |
+
function onPlayerStateChange(event) {
|
| 75 |
+
if (event.data === YT.PlayerState.ENDED) {
|
| 76 |
+
// 当前视频结束,播放下一个
|
| 77 |
+
currentVideoIndex++;
|
| 78 |
+
if (currentVideoIndex < videoIds.length) {
|
| 79 |
+
loadAndPlayVideo(currentVideoIndex);
|
| 80 |
+
updatePlaylist();
|
| 81 |
+
} else {
|
| 82 |
+
document.getElementById('status').textContent = '所有视频播放完毕';
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// 加载并播放指定索引的视频
|
| 88 |
+
function loadAndPlayVideo(index) {
|
| 89 |
+
if (index >= 0 && index < videoIds.length) {
|
| 90 |
+
player.loadVideoById(videoIds[index]);
|
| 91 |
+
document.getElementById('status').textContent = `正在播放: 视频 ${index + 1}/${videoIds.length}`;
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// 更新播放列表显示
|
| 96 |
+
function updatePlaylist() {
|
| 97 |
+
const playlistElement = document.getElementById('playlist');
|
| 98 |
+
playlistElement.innerHTML = '<h3>播放列表:</h3>';
|
| 99 |
+
|
| 100 |
+
videoIds.forEach((id, index) => {
|
| 101 |
+
const item = document.createElement('div');
|
| 102 |
+
item.textContent = `视频 ${index + 1}: https://youtu.be/${id}`;
|
| 103 |
+
if (index === currentVideoIndex) {
|
| 104 |
+
item.className = 'current';
|
| 105 |
+
}
|
| 106 |
+
playlistElement.appendChild(item);
|
| 107 |
+
});
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// 动态加载YouTube IFrame API
|
| 111 |
+
const tag = document.createElement('script');
|
| 112 |
+
tag.src = "https://www.youtube.com/iframe_api";
|
| 113 |
+
const firstScriptTag = document.getElementsByTagName('script')[0];
|
| 114 |
+
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
| 115 |
+
</script>
|
| 116 |
+
</body>
|
| 117 |
+
</html>
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def make_file(content: str):
|
| 122 |
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.html') as tmp:
|
| 123 |
+
tmp.write(content)
|
| 124 |
+
tmp_path = tmp.name
|
| 125 |
+
|
| 126 |
+
return tmp_path
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def get_video_id(url: str):
|
| 130 |
+
"""
|
| 131 |
+
从各种YouTube URL格式中提取视频ID
|
| 132 |
+
支持的格式包括:
|
| 133 |
+
- 普通URL: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
| 134 |
+
- 短链接: https://youtu.be/dQw4w9WgXcQ
|
| 135 |
+
- 嵌入链接: https://www.youtube.com/embed/dQw4w9WgXcQ
|
| 136 |
+
- 带时间戳: https://youtu.be/dQw4w9WgXcQ?t=120
|
| 137 |
+
- 带其他参数: https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=share
|
| 138 |
+
- 移动端链接: https://m.youtube.com/watch?v=dQw4w9WgXcQ
|
| 139 |
+
- 无协议链接: youtube.com/watch?v=dQw4w9WgXcQ
|
| 140 |
+
- 仅视频ID: dQw4w9WgXcQ
|
| 141 |
+
"""
|
| 142 |
+
pattern = r'(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([^"&?\/\s]{11})'
|
| 143 |
+
match = re.search(pattern, url)
|
| 144 |
+
|
| 145 |
+
if match is None:
|
| 146 |
+
return None
|
| 147 |
+
video_id = match.group(1)
|
| 148 |
+
return video_id
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def when_click_make_html_button(playlist: str):
|
| 152 |
+
urls = [url.strip() for url in playlist.split("\n") if url.strip()]
|
| 153 |
+
video_ids = [get_video_id(url) for url in urls]
|
| 154 |
+
video_ids = [video_id for video_id in video_ids if video_id is not None]
|
| 155 |
+
|
| 156 |
+
video_ids_ = json.dumps(video_ids, ensure_ascii=False)
|
| 157 |
+
|
| 158 |
+
template = Template(html_content)
|
| 159 |
+
variables = {
|
| 160 |
+
"video_ids": video_ids_
|
| 161 |
+
}
|
| 162 |
+
html_content_ = template.render(variables)
|
| 163 |
+
|
| 164 |
+
filename = make_file(html_content_)
|
| 165 |
+
return filename
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def get_youtube_player_tab():
|
| 169 |
+
with gr.TabItem("youtube_player"):
|
| 170 |
+
with gr.Row():
|
| 171 |
+
with gr.Column(scale=5):
|
| 172 |
+
yp_playlist = gr.Textbox(
|
| 173 |
+
label="playlist",
|
| 174 |
+
value="https://www.youtube.com/watch?v=tvRNE-ULe94\nhttps://www.youtube.com/watch?v=VQmaDgmyIBc\nhttps://www.youtube.com/watch?v=e_sFLaQUN3k\nhttps://www.youtube.com/watch?v=-VDYnk0jM8Q",
|
| 175 |
+
placeholder="每行输入一个YouTube视频链接\n例如:\nhttps://www.youtube.com/watch?v=dQw4w9WgXcQ\nhttps://youtu.be/9bZkp7q19f0",
|
| 176 |
+
lines=10,
|
| 177 |
+
)
|
| 178 |
+
yp_make_html_button = gr.Button("make_html")
|
| 179 |
+
|
| 180 |
+
with gr.Column(scale=5):
|
| 181 |
+
yp_video_output = gr.File(label="play_html_file")
|
| 182 |
+
|
| 183 |
+
yp_make_html_button.click(
|
| 184 |
+
fn=when_click_make_html_button,
|
| 185 |
+
inputs=[yp_playlist],
|
| 186 |
+
outputs=[yp_video_output]
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
return locals()
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
if __name__ == "__main__":
|
| 193 |
+
with gr.Blocks() as block:
|
| 194 |
+
fs_components = get_youtube_player_tab()
|
| 195 |
+
block.launch()
|
toolbox/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
pass
|
toolbox/asyncio/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
pass
|
toolbox/asyncio/cacheout.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import asyncio
|
| 4 |
+
import time
|
| 5 |
+
from functools import wraps
|
| 6 |
+
|
| 7 |
+
import time
|
| 8 |
+
import hashlib
|
| 9 |
+
import pickle
|
| 10 |
+
import asyncio
|
| 11 |
+
from functools import wraps
|
| 12 |
+
from typing import Any, Callable, Dict, Optional, Tuple
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
from collections import OrderedDict
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class CacheEntry:
|
| 19 |
+
expire_time: float
|
| 20 |
+
result: Any
|
| 21 |
+
access_time: float
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def async_cache_decorator(
|
| 25 |
+
max_age: int = 10,
|
| 26 |
+
max_size: Optional[int] = 100,
|
| 27 |
+
ignore_kwargs: Optional[list] = None
|
| 28 |
+
):
|
| 29 |
+
"""
|
| 30 |
+
高级缓存装饰器
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
max_age: 缓存最大年龄(秒)
|
| 34 |
+
max_size: 缓存最大大小(None表示无限制)
|
| 35 |
+
ignore_kwargs: 忽略的kwargs参数名列表
|
| 36 |
+
"""
|
| 37 |
+
ignore_kwargs = ignore_kwargs or []
|
| 38 |
+
|
| 39 |
+
def decorator(func: Callable):
|
| 40 |
+
cache: Dict[str, CacheEntry] = {}
|
| 41 |
+
access_order = OrderedDict() # 用于LRU淘汰
|
| 42 |
+
|
| 43 |
+
@wraps(func)
|
| 44 |
+
async def wrapper(*args, **kwargs):
|
| 45 |
+
# 清理过期的缓存
|
| 46 |
+
_clean_expired_cache()
|
| 47 |
+
|
| 48 |
+
# 生成缓存键(忽略指定的kwargs)
|
| 49 |
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k not in ignore_kwargs}
|
| 50 |
+
cache_key = _generate_cache_key(args, filtered_kwargs)
|
| 51 |
+
current_time = time.time()
|
| 52 |
+
|
| 53 |
+
# 检查缓存
|
| 54 |
+
if cache_key in cache:
|
| 55 |
+
entry = cache[cache_key]
|
| 56 |
+
if current_time < entry.expire_time:
|
| 57 |
+
# 更新访问时间和LRU顺序
|
| 58 |
+
entry.access_time = current_time
|
| 59 |
+
access_order.move_to_end(cache_key)
|
| 60 |
+
return entry.result
|
| 61 |
+
else:
|
| 62 |
+
# 缓存过期
|
| 63 |
+
_remove_from_cache(cache_key)
|
| 64 |
+
|
| 65 |
+
# 调用原函数
|
| 66 |
+
result = await func(*args, **kwargs)
|
| 67 |
+
|
| 68 |
+
# 检查缓存大小并可能淘汰
|
| 69 |
+
if max_size and len(cache) >= max_size:
|
| 70 |
+
# 移除最久未使用的缓存
|
| 71 |
+
oldest_key = next(iter(access_order))
|
| 72 |
+
_remove_from_cache(oldest_key)
|
| 73 |
+
|
| 74 |
+
# 添加新缓存
|
| 75 |
+
cache[cache_key] = CacheEntry(
|
| 76 |
+
expire_time=current_time + max_age,
|
| 77 |
+
result=result,
|
| 78 |
+
access_time=current_time
|
| 79 |
+
)
|
| 80 |
+
access_order[cache_key] = True
|
| 81 |
+
|
| 82 |
+
return result
|
| 83 |
+
|
| 84 |
+
def _clean_expired_cache():
|
| 85 |
+
"""清理过期缓存"""
|
| 86 |
+
current_time = time.time()
|
| 87 |
+
expired_keys = [
|
| 88 |
+
key for key, entry in cache.items()
|
| 89 |
+
if current_time >= entry.expire_time
|
| 90 |
+
]
|
| 91 |
+
for key in expired_keys:
|
| 92 |
+
_remove_from_cache(key)
|
| 93 |
+
|
| 94 |
+
def _remove_from_cache(cache_key: str):
|
| 95 |
+
"""从缓存中移除指定键"""
|
| 96 |
+
if cache_key in cache:
|
| 97 |
+
del cache[cache_key]
|
| 98 |
+
if cache_key in access_order:
|
| 99 |
+
del access_order[cache_key]
|
| 100 |
+
|
| 101 |
+
# 缓存管理方法
|
| 102 |
+
def clear_cache():
|
| 103 |
+
"""清空所有缓存"""
|
| 104 |
+
cache.clear()
|
| 105 |
+
access_order.clear()
|
| 106 |
+
|
| 107 |
+
def remove_from_cache(*args, **kwargs):
|
| 108 |
+
"""移除特定参数的缓存"""
|
| 109 |
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k not in ignore_kwargs}
|
| 110 |
+
cache_key = _generate_cache_key(args, filtered_kwargs)
|
| 111 |
+
if cache_key in cache:
|
| 112 |
+
_remove_from_cache(cache_key)
|
| 113 |
+
return True
|
| 114 |
+
return False
|
| 115 |
+
|
| 116 |
+
def get_cache_info() -> Dict[str, Any]:
|
| 117 |
+
"""获取缓存统计信息"""
|
| 118 |
+
current_time = time.time()
|
| 119 |
+
valid_entries = sum(
|
| 120 |
+
1 for entry in cache.values()
|
| 121 |
+
if current_time < entry.expire_time
|
| 122 |
+
)
|
| 123 |
+
return {
|
| 124 |
+
'total_entries': len(cache),
|
| 125 |
+
'valid_entries': valid_entries,
|
| 126 |
+
'max_size': max_size,
|
| 127 |
+
'max_age': max_age,
|
| 128 |
+
'cache_keys': list(cache.keys())
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
def get_cached_result(*args, **kwargs):
|
| 132 |
+
"""获取缓存结果(如果存在且未过期)"""
|
| 133 |
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k not in ignore_kwargs}
|
| 134 |
+
cache_key = _generate_cache_key(args, filtered_kwargs)
|
| 135 |
+
current_time = time.time()
|
| 136 |
+
|
| 137 |
+
if cache_key in cache:
|
| 138 |
+
entry = cache[cache_key]
|
| 139 |
+
if current_time < entry.expire_time:
|
| 140 |
+
return entry.result
|
| 141 |
+
return None
|
| 142 |
+
|
| 143 |
+
# 附加方法
|
| 144 |
+
wrapper.clear_cache = clear_cache
|
| 145 |
+
wrapper.remove_from_cache = remove_from_cache
|
| 146 |
+
wrapper.get_cache_info = get_cache_info
|
| 147 |
+
wrapper.get_cached_result = get_cached_result
|
| 148 |
+
|
| 149 |
+
return wrapper
|
| 150 |
+
|
| 151 |
+
return decorator
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _generate_cache_key(args: tuple, kwargs: dict) -> str:
|
| 155 |
+
"""生成缓存键"""
|
| 156 |
+
try:
|
| 157 |
+
# 尝��使用更高效的序列化
|
| 158 |
+
key_data = pickle.dumps((args, sorted(kwargs.items())))
|
| 159 |
+
return hashlib.sha256(key_data).hexdigest()
|
| 160 |
+
except (TypeError, pickle.PickleError):
|
| 161 |
+
# 如果无法序列化,使用字符串表示
|
| 162 |
+
args_str = str(args)
|
| 163 |
+
kwargs_str = str(sorted(kwargs.items()))
|
| 164 |
+
return hashlib.sha256(f"{args_str}:{kwargs_str}".encode()).hexdigest()
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
@async_cache_decorator(10)
|
| 168 |
+
async def call_api():
|
| 169 |
+
await asyncio.sleep(1)
|
| 170 |
+
return {"data": f"API响应时间: {time.time()}", "status": "success"}
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
async def main():
|
| 174 |
+
# 第一次调用 - 实际调用API
|
| 175 |
+
result1 = await call_api()
|
| 176 |
+
print("第一次结果:", result1)
|
| 177 |
+
|
| 178 |
+
# 立即再次调用 - 返回缓存结果
|
| 179 |
+
result2 = await call_api()
|
| 180 |
+
print("第二次结果:", result2)
|
| 181 |
+
|
| 182 |
+
# 等待5秒后调用 - 返回缓存结果
|
| 183 |
+
await asyncio.sleep(5)
|
| 184 |
+
result3 = await call_api()
|
| 185 |
+
print("第三次结果:", result3)
|
| 186 |
+
|
| 187 |
+
# 等待11秒后调用 - 重新调用API
|
| 188 |
+
await asyncio.sleep(6) # 总共等待11秒
|
| 189 |
+
result4 = await call_api()
|
| 190 |
+
print("第四次结果:", result4)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
if __name__ == "__main__":
|
| 194 |
+
asyncio.run(main())
|
toolbox/bilibili/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
pass
|
toolbox/bilibili/bilibili_client.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
https://pypi.org/project/biliupload/
|
| 5 |
+
|
| 6 |
+
https://github.com/SocialSisterYi/bilibili-API-collect
|
| 7 |
+
|
| 8 |
+
"""
|
| 9 |
+
import argparse
|
| 10 |
+
import hashlib
|
| 11 |
+
import json
|
| 12 |
+
import logging
|
| 13 |
+
import subprocess
|
| 14 |
+
import time
|
| 15 |
+
from urllib.parse import urlencode, urlparse
|
| 16 |
+
|
| 17 |
+
import qrcode
|
| 18 |
+
import requests
|
| 19 |
+
import requests.utils
|
| 20 |
+
|
| 21 |
+
from project_settings import project_path
|
| 22 |
+
from toolbox.design_patterns.singleton import ParamsSingleton
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger("toolbox")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class BilibiliUtils(object):
|
| 29 |
+
app_key = "4409e2ce8ffd12b8"
|
| 30 |
+
app_sec = "59b43e04ad6965f34319062b478f83dd"
|
| 31 |
+
|
| 32 |
+
@classmethod
|
| 33 |
+
def signature(cls, params):
|
| 34 |
+
params["appkey"] = cls.app_key
|
| 35 |
+
keys = sorted(params.keys())
|
| 36 |
+
query = "&".join(f"{k}={params[k]}" for k in keys)
|
| 37 |
+
query += cls.app_sec
|
| 38 |
+
md5_hash = hashlib.md5(query.encode("utf-8")).hexdigest()
|
| 39 |
+
params["sign"] = md5_hash
|
| 40 |
+
|
| 41 |
+
@staticmethod
|
| 42 |
+
def map_to_string(params):
|
| 43 |
+
return urlencode(params)
|
| 44 |
+
|
| 45 |
+
@classmethod
|
| 46 |
+
def execute_curl_command(cls, api, data):
|
| 47 |
+
data_string = cls.map_to_string(data)
|
| 48 |
+
headers = "Content-Type: application/x-www-form-urlencoded"
|
| 49 |
+
curl_command = f"curl -X POST -H \"{headers}\" -d \"{data_string}\" {api}"
|
| 50 |
+
result = subprocess.run(
|
| 51 |
+
curl_command, shell=True, capture_output=True, text=True, encoding="utf-8"
|
| 52 |
+
)
|
| 53 |
+
if result.returncode != 0:
|
| 54 |
+
raise Exception(f"curl command failed: {result.stderr}")
|
| 55 |
+
return json.loads(result.stdout)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class BilibiliClient(BilibiliUtils, ParamsSingleton):
|
| 59 |
+
headers = {
|
| 60 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
def __init__(self):
|
| 64 |
+
if not self._initialized:
|
| 65 |
+
self.credentials = None
|
| 66 |
+
self.cookies = None
|
| 67 |
+
|
| 68 |
+
self._session = requests.Session()
|
| 69 |
+
|
| 70 |
+
self._initialized = True
|
| 71 |
+
|
| 72 |
+
@property
|
| 73 |
+
def session(self):
|
| 74 |
+
if not self._session.cookies:
|
| 75 |
+
self._session.headers = self.headers
|
| 76 |
+
self._session.cookies = requests.utils.cookiejar_from_dict(self.cookies)
|
| 77 |
+
return self._session
|
| 78 |
+
|
| 79 |
+
@classmethod
|
| 80 |
+
def get_tv_qrcode_url_and_auth_code(cls):
|
| 81 |
+
api = "https://passport.bilibili.com/x/passport-tv-login/qrcode/auth_code"
|
| 82 |
+
data = {
|
| 83 |
+
"local_id": "0",
|
| 84 |
+
"ts": str(int(time.time()))
|
| 85 |
+
}
|
| 86 |
+
cls.signature(data)
|
| 87 |
+
body = cls.execute_curl_command(api, data)
|
| 88 |
+
if body["code"] == 0:
|
| 89 |
+
qrcode_url = body["data"]["url"]
|
| 90 |
+
auth_code = body["data"]["auth_code"]
|
| 91 |
+
return qrcode_url, auth_code
|
| 92 |
+
else:
|
| 93 |
+
raise Exception("get_tv_qrcode_url_and_auth_code error")
|
| 94 |
+
|
| 95 |
+
@classmethod
|
| 96 |
+
def verify_login(cls, auth_code):
|
| 97 |
+
api = "https://passport.bilibili.com/x/passport-tv-login/qrcode/poll"
|
| 98 |
+
data = {
|
| 99 |
+
"auth_code": auth_code,
|
| 100 |
+
"local_id": "0",
|
| 101 |
+
"ts": str(int(time.time()))
|
| 102 |
+
}
|
| 103 |
+
cls.signature(data)
|
| 104 |
+
while True:
|
| 105 |
+
body = cls.execute_curl_command(api, data)
|
| 106 |
+
if body["code"] == 0:
|
| 107 |
+
print("Login success!")
|
| 108 |
+
return body
|
| 109 |
+
else:
|
| 110 |
+
time.sleep(3)
|
| 111 |
+
|
| 112 |
+
def set_cookies(self, credentials: dict):
|
| 113 |
+
access_token = credentials["data"]["access_token"]
|
| 114 |
+
sessdata_value = credentials["data"]["cookie_info"]["cookies"][0]["value"]
|
| 115 |
+
bili_jct_value = credentials["data"]["cookie_info"]["cookies"][1]["value"]
|
| 116 |
+
dede_user_id_value = credentials["data"]["cookie_info"]["cookies"][2]["value"]
|
| 117 |
+
dede_user_id_ckmd5_value = credentials["data"]["cookie_info"]["cookies"][3]["value"]
|
| 118 |
+
sid_value = credentials["data"]["cookie_info"]["cookies"][4]["value"]
|
| 119 |
+
cookies = {
|
| 120 |
+
# "access_token": access_token,
|
| 121 |
+
"SESSDATA": sessdata_value,
|
| 122 |
+
"bili_jct": bili_jct_value,
|
| 123 |
+
"DedeUserID": dede_user_id_value,
|
| 124 |
+
"DedeUserID__ckMd5": dede_user_id_ckmd5_value,
|
| 125 |
+
# "sid": sid_value,
|
| 126 |
+
}
|
| 127 |
+
self.cookies = cookies
|
| 128 |
+
return self.cookies
|
| 129 |
+
|
| 130 |
+
def login_with_qrcode_url(self):
|
| 131 |
+
input("Please maximize the window to ensure the QR code is fully displayed, press Enter to continue: ")
|
| 132 |
+
login_url, auth_code = self.get_tv_qrcode_url_and_auth_code()
|
| 133 |
+
qr = qrcode.QRCode()
|
| 134 |
+
qr.add_data(login_url)
|
| 135 |
+
qr.print_ascii()
|
| 136 |
+
print("Or copy this link to your phone Bilibili:", login_url)
|
| 137 |
+
credentials = self.verify_login(auth_code)
|
| 138 |
+
self.credentials = credentials
|
| 139 |
+
self.set_cookies(credentials)
|
| 140 |
+
return True
|
| 141 |
+
|
| 142 |
+
def login_with_credentials_file(self, credentials_file: str):
|
| 143 |
+
with open(credentials_file, "r", encoding="utf-8") as f:
|
| 144 |
+
credentials = json.load(f)
|
| 145 |
+
self.credentials = credentials
|
| 146 |
+
self.set_cookies(credentials)
|
| 147 |
+
return True
|
| 148 |
+
|
| 149 |
+
def login_with_credentials_info(self, credentials_info: dict):
|
| 150 |
+
self.credentials = credentials_info
|
| 151 |
+
self.set_cookies(credentials_info)
|
| 152 |
+
return True
|
| 153 |
+
|
| 154 |
+
def check_login(self):
|
| 155 |
+
url = "https://api.bilibili.com/x/web-interface/nav"
|
| 156 |
+
with requests.Session() as session:
|
| 157 |
+
session.headers = self.headers
|
| 158 |
+
session.cookies = requests.utils.cookiejar_from_dict(self.cookies)
|
| 159 |
+
response = session.get(url)
|
| 160 |
+
if response.status_code == 200:
|
| 161 |
+
response_data = json.loads(response.text)
|
| 162 |
+
if response_data["data"]["isLogin"] is True:
|
| 163 |
+
return True
|
| 164 |
+
else:
|
| 165 |
+
return False
|
| 166 |
+
else:
|
| 167 |
+
logger.error(f"Check failed, please check the info; status_code: {response.status_code}, text: {response.text}")
|
| 168 |
+
return False
|
| 169 |
+
|
| 170 |
+
def get_now(self):
|
| 171 |
+
url = "https://api.bilibili.com/x/report/click/now"
|
| 172 |
+
|
| 173 |
+
response = self.session.get(
|
| 174 |
+
url,
|
| 175 |
+
headers=self.headers,
|
| 176 |
+
)
|
| 177 |
+
if response.status_code != 200:
|
| 178 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 179 |
+
js = response.json()
|
| 180 |
+
return js
|
| 181 |
+
|
| 182 |
+
def get_version(self):
|
| 183 |
+
url = "https://api.live.bilibili.com/xlive/app-blink/v1/liveVersionInfo/getHomePageLiveVersion"
|
| 184 |
+
|
| 185 |
+
js = self.get_now()
|
| 186 |
+
ts = js["data"]["now"]
|
| 187 |
+
|
| 188 |
+
params = {
|
| 189 |
+
"system_version": 2,
|
| 190 |
+
"ts": ts,
|
| 191 |
+
}
|
| 192 |
+
self.signature(params)
|
| 193 |
+
|
| 194 |
+
response = self.session.get(
|
| 195 |
+
url,
|
| 196 |
+
headers=self.headers,
|
| 197 |
+
params=params,
|
| 198 |
+
)
|
| 199 |
+
if response.status_code != 200:
|
| 200 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 201 |
+
js = response.json()
|
| 202 |
+
return js
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def get_args():
|
| 206 |
+
parser = argparse.ArgumentParser()
|
| 207 |
+
parser.add_argument(
|
| 208 |
+
"--credentials_file",
|
| 209 |
+
default=(project_path / "dotenv/bilibili_chenjiesen_credentials.json").as_posix(),
|
| 210 |
+
type=str
|
| 211 |
+
)
|
| 212 |
+
args = parser.parse_args()
|
| 213 |
+
return args
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def main():
|
| 217 |
+
import log
|
| 218 |
+
from project_settings import project_path, log_directory
|
| 219 |
+
|
| 220 |
+
log.setup_size_rotating(log_directory=log_directory)
|
| 221 |
+
|
| 222 |
+
args = get_args()
|
| 223 |
+
|
| 224 |
+
client = BilibiliClient()
|
| 225 |
+
|
| 226 |
+
flag = client.check_login()
|
| 227 |
+
print(f"flag: {flag}")
|
| 228 |
+
client.login_with_credentials_file(args.credentials_file)
|
| 229 |
+
# client.login_with_qrcode_url()
|
| 230 |
+
flag = client.check_login()
|
| 231 |
+
print(f"flag: {flag}")
|
| 232 |
+
|
| 233 |
+
# result = client.get_room_id()
|
| 234 |
+
# result = client.get_now()
|
| 235 |
+
result = client.get_version()
|
| 236 |
+
print(result)
|
| 237 |
+
return
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
if __name__ == "__main__":
|
| 241 |
+
main()
|
toolbox/bilibili/live/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
pass
|
toolbox/bilibili/live/live_manager.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
https://github.com/lateautumn233/Bilibili-Live-Stream/blob/main/bilibili-live.py
|
| 5 |
+
|
| 6 |
+
https://github.com/ChaceQC/bilibili_live_stream_code/blob/master/main/bilibili_live_stream_code.py
|
| 7 |
+
|
| 8 |
+
"""
|
| 9 |
+
import argparse
|
| 10 |
+
import logging
|
| 11 |
+
import math
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
import re
|
| 14 |
+
import json
|
| 15 |
+
import urllib
|
| 16 |
+
import urllib.parse
|
| 17 |
+
import hashlib
|
| 18 |
+
|
| 19 |
+
import requests
|
| 20 |
+
import requests.utils
|
| 21 |
+
from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
| 22 |
+
|
| 23 |
+
logger = logging.getLogger("toolbox")
|
| 24 |
+
|
| 25 |
+
from project_settings import project_path
|
| 26 |
+
from toolbox.bilibili.bilibili_client import BilibiliClient
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def appsign(params, appkey, appsec):
|
| 30 |
+
"""
|
| 31 |
+
为请求参数进行app签名
|
| 32 |
+
:param params: 原参数
|
| 33 |
+
:param appkey: key
|
| 34 |
+
:param appsec: key对应的secret
|
| 35 |
+
:return:
|
| 36 |
+
"""
|
| 37 |
+
params.update({'appkey': appkey})
|
| 38 |
+
params = dict(sorted(params.items())) # 按照 key 重排参数
|
| 39 |
+
query = urllib.parse.urlencode(params) # 序列化参数
|
| 40 |
+
sign = hashlib.md5((query + appsec).encode()).hexdigest() # 计算 api 签名
|
| 41 |
+
params.update({'sign': sign})
|
| 42 |
+
return params
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class RtmpPublishCMD(object):
|
| 46 |
+
def __init__(self, cmd_list: list, cmd_str: str):
|
| 47 |
+
self.cmd_list = cmd_list
|
| 48 |
+
self.cmd_str = cmd_str
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class BilibiliLiveManager(BilibiliClient):
|
| 52 |
+
def __init__(self):
|
| 53 |
+
super().__init__()
|
| 54 |
+
|
| 55 |
+
def set_live_title(self, title: str):
|
| 56 |
+
"""
|
| 57 |
+
:return
|
| 58 |
+
{
|
| 59 |
+
"code": 0,
|
| 60 |
+
"msg": "ok",
|
| 61 |
+
"message": "ok",
|
| 62 |
+
"data": {
|
| 63 |
+
"sub_session_key": "",
|
| 64 |
+
"audit_info": {
|
| 65 |
+
"audit_title_reason": "进入审核",
|
| 66 |
+
"update_title": "",
|
| 67 |
+
"audit_title_status": 0,
|
| 68 |
+
"audit_title": "设置直播标题"
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
"""
|
| 73 |
+
url = "https://api.live.bilibili.com/room/v1/Room/update"
|
| 74 |
+
|
| 75 |
+
js = self.get_room_id_by_uid()
|
| 76 |
+
room_id = js["data"]["room_id"]
|
| 77 |
+
|
| 78 |
+
bili_jct = self.cookies["bili_jct"]
|
| 79 |
+
|
| 80 |
+
data = {
|
| 81 |
+
"room_id": room_id,
|
| 82 |
+
"platform": "pc_link",
|
| 83 |
+
"title": title,
|
| 84 |
+
"csrf_token": bili_jct,
|
| 85 |
+
"csrf": bili_jct,
|
| 86 |
+
}
|
| 87 |
+
response = self.session.post(
|
| 88 |
+
url,
|
| 89 |
+
headers=self.headers,
|
| 90 |
+
data=data,
|
| 91 |
+
)
|
| 92 |
+
if response.status_code != 200:
|
| 93 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 94 |
+
js = response.json()
|
| 95 |
+
# print(json.dumps(js, ensure_ascii=False, indent=4))
|
| 96 |
+
return js
|
| 97 |
+
|
| 98 |
+
def get_room_id_by_uid(self):
|
| 99 |
+
dede_user_id = self.cookies.get("DedeUserID")
|
| 100 |
+
|
| 101 |
+
url = f"https://api.live.bilibili.com/room/v2/Room/room_id_by_uid?uid={dede_user_id}"
|
| 102 |
+
response = self.session.get(
|
| 103 |
+
url,
|
| 104 |
+
headers=self.headers
|
| 105 |
+
)
|
| 106 |
+
if response.status_code != 200:
|
| 107 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 108 |
+
js = response.json()
|
| 109 |
+
return js
|
| 110 |
+
|
| 111 |
+
def get_rtmp_publish_cmd_by_flv_file(self, input_source: str):
|
| 112 |
+
js = self.get_rtmp_info_by_room_id()
|
| 113 |
+
# print(json.dumps(js, ensure_ascii=False, indent=4))
|
| 114 |
+
rtmp_code = js["data"]["rtmp"]["code"]
|
| 115 |
+
rtmp_addr = js["data"]["rtmp"]["addr"]
|
| 116 |
+
|
| 117 |
+
rtmp_url = f"{rtmp_addr}/{rtmp_code}"
|
| 118 |
+
|
| 119 |
+
# cmd = f'ffmpeg -re -i "{input_source}" -c copy -f flv -flvflags no_duration_filesize "{rtmp_url}"'
|
| 120 |
+
|
| 121 |
+
# cmd_list = [
|
| 122 |
+
# 'ffmpeg',
|
| 123 |
+
# '-i', input_source,
|
| 124 |
+
# '-c', 'copy',
|
| 125 |
+
# '-f', 'flv',
|
| 126 |
+
# rtmp_url
|
| 127 |
+
# ]
|
| 128 |
+
cmd_list = [
|
| 129 |
+
"ffmpeg",
|
| 130 |
+
"-i", input_source,
|
| 131 |
+
"-c", "copy",
|
| 132 |
+
"-f", "flv",
|
| 133 |
+
rtmp_url
|
| 134 |
+
]
|
| 135 |
+
cmd_str = [
|
| 136 |
+
"ffmpeg",
|
| 137 |
+
'-i', f'"{input_source}"',
|
| 138 |
+
"-c", "copy",
|
| 139 |
+
"-f", "flv",
|
| 140 |
+
f'"{rtmp_url}"'
|
| 141 |
+
]
|
| 142 |
+
cmd_str = " ".join(cmd_str)
|
| 143 |
+
cmd = RtmpPublishCMD(
|
| 144 |
+
cmd_list=cmd_list,
|
| 145 |
+
cmd_str=cmd_str
|
| 146 |
+
)
|
| 147 |
+
return cmd
|
| 148 |
+
|
| 149 |
+
def get_rtmp_info_by_room_id(self):
|
| 150 |
+
"""
|
| 151 |
+
:return:
|
| 152 |
+
{
|
| 153 |
+
"code": 0,
|
| 154 |
+
"data": {
|
| 155 |
+
"change": 1,
|
| 156 |
+
"status": "LIVE",
|
| 157 |
+
"try_time": "0000-00-00 00:00:00",
|
| 158 |
+
"room_type": 0,
|
| 159 |
+
"live_key": "630309426298156613",
|
| 160 |
+
"sub_session_key": "630309426298156613sub_time:1757133948",
|
| 161 |
+
"rtmp": {
|
| 162 |
+
"type": 1,
|
| 163 |
+
"addr": "rtmp://txy2.live-push.bilivideo.com/live-bvc/",
|
| 164 |
+
"code": "?streamname=live_442286660_79175069&key=fd263bd4cad752a05c2e284adda16302&schedule=rtmp&pflag=2",
|
| 165 |
+
"new_link": "",
|
| 166 |
+
"provider": "txy2"
|
| 167 |
+
},
|
| 168 |
+
"protocols": [
|
| 169 |
+
{
|
| 170 |
+
"protocol": "rtmp",
|
| 171 |
+
"addr": "rtmp://txy2.live-push.bilivideo.com/live-bvc/",
|
| 172 |
+
"code": "?streamname=live_442286660_79175069&key=fd263bd4cad752a05c2e284adda16302&schedule=rtmp&pflag=2",
|
| 173 |
+
"new_link": "",
|
| 174 |
+
"provider": "txy"
|
| 175 |
+
}
|
| 176 |
+
],
|
| 177 |
+
"notice": {
|
| 178 |
+
"type": 1,
|
| 179 |
+
"status": 0,
|
| 180 |
+
"title": "",
|
| 181 |
+
"msg": "",
|
| 182 |
+
"button_text": "",
|
| 183 |
+
"button_url": ""
|
| 184 |
+
},
|
| 185 |
+
"qr": "",
|
| 186 |
+
"need_face_auth": false,
|
| 187 |
+
"service_source": "live-streaming",
|
| 188 |
+
"rtmp_backup": null,
|
| 189 |
+
"up_stream_extra": {
|
| 190 |
+
"isp": "小运营商"
|
| 191 |
+
},
|
| 192 |
+
"collaboration_live_extra": null
|
| 193 |
+
},
|
| 194 |
+
"message": "",
|
| 195 |
+
"msg": ""
|
| 196 |
+
}
|
| 197 |
+
"""
|
| 198 |
+
url = "https://api.live.bilibili.com/room/v1/Room/startLive"
|
| 199 |
+
|
| 200 |
+
js = self.get_room_id_by_uid()
|
| 201 |
+
room_id = js["data"]["room_id"]
|
| 202 |
+
|
| 203 |
+
js = self.get_version()
|
| 204 |
+
build = js["data"]["build"]
|
| 205 |
+
curr_version = js["data"]["curr_version"]
|
| 206 |
+
|
| 207 |
+
js = self.get_now()
|
| 208 |
+
ts = js["data"]["now"]
|
| 209 |
+
|
| 210 |
+
bili_jct = self.cookies["bili_jct"]
|
| 211 |
+
data = {
|
| 212 |
+
"room_id": room_id,
|
| 213 |
+
"platform": "pc_link",
|
| 214 |
+
"area_v2": "624",
|
| 215 |
+
"backup_stream": "0",
|
| 216 |
+
"csrf_token": bili_jct,
|
| 217 |
+
"csrf": bili_jct,
|
| 218 |
+
"build": build,
|
| 219 |
+
"version": curr_version,
|
| 220 |
+
"ts": ts,
|
| 221 |
+
}
|
| 222 |
+
self.signature(data)
|
| 223 |
+
|
| 224 |
+
response = self.session.post(
|
| 225 |
+
url,
|
| 226 |
+
headers=self.headers,
|
| 227 |
+
data=data,
|
| 228 |
+
)
|
| 229 |
+
if response.status_code != 200:
|
| 230 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 231 |
+
js = response.json()
|
| 232 |
+
return js
|
| 233 |
+
|
| 234 |
+
@retry(
|
| 235 |
+
wait=wait_fixed(10),
|
| 236 |
+
stop=stop_after_attempt(3),
|
| 237 |
+
before_sleep=before_sleep_log(logger, logging.ERROR),
|
| 238 |
+
)
|
| 239 |
+
def stop_live(self):
|
| 240 |
+
url = "https://api.live.bilibili.com/room/v1/Room/stopLive"
|
| 241 |
+
|
| 242 |
+
js = self.get_room_id_by_uid()
|
| 243 |
+
room_id = js["data"]["room_id"]
|
| 244 |
+
|
| 245 |
+
bili_jct = self.cookies["bili_jct"]
|
| 246 |
+
|
| 247 |
+
data = {
|
| 248 |
+
"room_id": room_id,
|
| 249 |
+
"platform": "pc_link",
|
| 250 |
+
"csrf_token": bili_jct,
|
| 251 |
+
"csrf": bili_jct,
|
| 252 |
+
}
|
| 253 |
+
response = self.session.post(
|
| 254 |
+
url,
|
| 255 |
+
headers=self.headers,
|
| 256 |
+
data=data,
|
| 257 |
+
)
|
| 258 |
+
if response.status_code != 200:
|
| 259 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 260 |
+
js = response.json()
|
| 261 |
+
return js
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def get_args():
|
| 265 |
+
parser = argparse.ArgumentParser()
|
| 266 |
+
parser.add_argument(
|
| 267 |
+
"--credentials_file",
|
| 268 |
+
default=(project_path / "dotenv/bilibili_chenjiesen_credentials.json").as_posix(),
|
| 269 |
+
type=str
|
| 270 |
+
)
|
| 271 |
+
args = parser.parse_args()
|
| 272 |
+
return args
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def main():
|
| 276 |
+
import log
|
| 277 |
+
from project_settings import project_path, log_directory
|
| 278 |
+
|
| 279 |
+
log.setup_size_rotating(log_directory=log_directory)
|
| 280 |
+
|
| 281 |
+
args = get_args()
|
| 282 |
+
|
| 283 |
+
client = BilibiliLiveManager()
|
| 284 |
+
|
| 285 |
+
flag = client.check_login()
|
| 286 |
+
print(f"flag: {flag}")
|
| 287 |
+
client.login_with_credentials_file(args.credentials_file)
|
| 288 |
+
# client.login_with_qrcode_url()
|
| 289 |
+
flag = client.check_login()
|
| 290 |
+
print(f"flag: {flag}")
|
| 291 |
+
|
| 292 |
+
# result = client.get_room_id()
|
| 293 |
+
# result = client.stop_live()
|
| 294 |
+
result = client.start_live_by_flv_file(args.room_id)
|
| 295 |
+
# result = client.set_live_title("设置直播标题")
|
| 296 |
+
print(result)
|
| 297 |
+
return
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
if __name__ == "__main__":
|
| 301 |
+
main()
|
toolbox/bilibili/video/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
if __name__ == "__main__":
|
| 5 |
+
pass
|
toolbox/bilibili/video/draft_manager.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
https://pypi.org/project/biliupload/
|
| 5 |
+
|
| 6 |
+
https://github.com/SocialSisterYi/bilibili-API-collect
|
| 7 |
+
|
| 8 |
+
"""
|
| 9 |
+
import argparse
|
| 10 |
+
import logging
|
| 11 |
+
|
| 12 |
+
from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger("toolbox")
|
| 15 |
+
|
| 16 |
+
from project_settings import project_path
|
| 17 |
+
from toolbox.bilibili.video.video_manager import BilibiliVideoUploader
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class BilibiliVideoDraftUploader(BilibiliVideoUploader):
|
| 21 |
+
def __init__(self):
|
| 22 |
+
super().__init__()
|
| 23 |
+
|
| 24 |
+
@retry(
|
| 25 |
+
wait=wait_fixed(10),
|
| 26 |
+
stop=stop_after_attempt(3),
|
| 27 |
+
before_sleep=before_sleep_log(logger, logging.ERROR),
|
| 28 |
+
)
|
| 29 |
+
def list_draft(self):
|
| 30 |
+
url = f"https://member.bilibili.com/x/vupre/web/draft/list"
|
| 31 |
+
|
| 32 |
+
response = self.session.get(url)
|
| 33 |
+
if response.status_code != 200:
|
| 34 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 35 |
+
js = response.json()
|
| 36 |
+
return js
|
| 37 |
+
|
| 38 |
+
@retry(
|
| 39 |
+
wait=wait_fixed(10),
|
| 40 |
+
stop=stop_after_attempt(3),
|
| 41 |
+
before_sleep=before_sleep_log(logger, logging.ERROR),
|
| 42 |
+
)
|
| 43 |
+
def add_draft(self, biz_id: int, bilibili_filename, metadata: dict):
|
| 44 |
+
csrf = self.cookies["bili_jct"]
|
| 45 |
+
url = "https://member.bilibili.com/x/vupre/web/draft/add"
|
| 46 |
+
|
| 47 |
+
params = {
|
| 48 |
+
"csrf": csrf,
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
data = {
|
| 52 |
+
"copyright": metadata["copyright"],
|
| 53 |
+
"videos": [
|
| 54 |
+
{
|
| 55 |
+
"filename": bilibili_filename,
|
| 56 |
+
"title": metadata["title"],
|
| 57 |
+
"desc": metadata["desc"],
|
| 58 |
+
"cid": biz_id,
|
| 59 |
+
}
|
| 60 |
+
],
|
| 61 |
+
|
| 62 |
+
"source": metadata["source"],
|
| 63 |
+
"tid": metadata["tid"],
|
| 64 |
+
"title": metadata["title"],
|
| 65 |
+
"cover": metadata["cover"],
|
| 66 |
+
"tag": metadata["tag"],
|
| 67 |
+
"desc_format_id": 0,
|
| 68 |
+
"desc": metadata["desc"],
|
| 69 |
+
"dynamic": metadata["dynamic"],
|
| 70 |
+
"subtitle": {"open": 0, "lan": ""},
|
| 71 |
+
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
response = self.session.post(
|
| 75 |
+
url,
|
| 76 |
+
params=params,
|
| 77 |
+
json=data,
|
| 78 |
+
headers=self.headers,
|
| 79 |
+
)
|
| 80 |
+
if response.status_code != 200:
|
| 81 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 82 |
+
js = response.json()
|
| 83 |
+
return js
|
| 84 |
+
|
| 85 |
+
@retry(
|
| 86 |
+
wait=wait_fixed(10),
|
| 87 |
+
stop=stop_after_attempt(3),
|
| 88 |
+
before_sleep=before_sleep_log(logger, logging.ERROR),
|
| 89 |
+
)
|
| 90 |
+
def update_draft(self, draft_id: int, biz_id: int, bilibili_filename, metadata: dict):
|
| 91 |
+
csrf = self.cookies["bili_jct"]
|
| 92 |
+
url = "https://member.bilibili.com/x/vupre/web/draft/update"
|
| 93 |
+
|
| 94 |
+
params = {
|
| 95 |
+
"csrf": csrf,
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
data = {
|
| 99 |
+
"id": draft_id,
|
| 100 |
+
|
| 101 |
+
"copyright": metadata["copyright"],
|
| 102 |
+
"videos": [
|
| 103 |
+
{
|
| 104 |
+
"filename": bilibili_filename,
|
| 105 |
+
"title": metadata["title"],
|
| 106 |
+
"desc": metadata["desc"],
|
| 107 |
+
"cid": biz_id,
|
| 108 |
+
}
|
| 109 |
+
],
|
| 110 |
+
|
| 111 |
+
"source": metadata["source"],
|
| 112 |
+
"tid": metadata["tid"],
|
| 113 |
+
"title": metadata["title"],
|
| 114 |
+
"cover": metadata["cover"],
|
| 115 |
+
"tag": metadata["tag"],
|
| 116 |
+
"desc_format_id": 0,
|
| 117 |
+
"desc": metadata["desc"],
|
| 118 |
+
"dynamic": metadata["dynamic"],
|
| 119 |
+
"subtitle": {"open": 0, "lan": ""},
|
| 120 |
+
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
response = self.session.post(
|
| 124 |
+
url,
|
| 125 |
+
params=params,
|
| 126 |
+
json=data,
|
| 127 |
+
headers=self.headers,
|
| 128 |
+
)
|
| 129 |
+
if response.status_code != 200:
|
| 130 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 131 |
+
js = response.json()
|
| 132 |
+
return js
|
| 133 |
+
|
| 134 |
+
@retry(
|
| 135 |
+
wait=wait_fixed(10),
|
| 136 |
+
stop=stop_after_attempt(3),
|
| 137 |
+
before_sleep=before_sleep_log(logger, logging.ERROR),
|
| 138 |
+
)
|
| 139 |
+
def publish_draft(self, draft_id: int, bilibili_filename: str, metadata: dict):
|
| 140 |
+
csrf = self.cookies["bili_jct"]
|
| 141 |
+
url = "https://member.bilibili.com/x/vu/web/add/v3"
|
| 142 |
+
|
| 143 |
+
params = {
|
| 144 |
+
"csrf": csrf,
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
data = {
|
| 148 |
+
"draft_id": draft_id,
|
| 149 |
+
"copyright": metadata["copyright"],
|
| 150 |
+
"videos":[
|
| 151 |
+
{
|
| 152 |
+
"filename": bilibili_filename,
|
| 153 |
+
"title": metadata["title"],
|
| 154 |
+
"desc": metadata["desc"],
|
| 155 |
+
}
|
| 156 |
+
],
|
| 157 |
+
"source": metadata["source"],
|
| 158 |
+
"tid": metadata["tid"],
|
| 159 |
+
"title": metadata["title"],
|
| 160 |
+
"cover": metadata["cover"],
|
| 161 |
+
"tag": metadata["tag"],
|
| 162 |
+
"desc_format_id": 0,
|
| 163 |
+
"desc": metadata["desc"],
|
| 164 |
+
"dynamic": metadata["dynamic"],
|
| 165 |
+
"subtitle": {"open": 0, "lan": ""},
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
if metadata["copyright"] != 2:
|
| 169 |
+
del data["source"]
|
| 170 |
+
# copyright: 1 original 2 reprint
|
| 171 |
+
data["copyright"] = 1
|
| 172 |
+
|
| 173 |
+
response = self.session.post(
|
| 174 |
+
url,
|
| 175 |
+
params=params,
|
| 176 |
+
json=data,
|
| 177 |
+
headers=self.headers,
|
| 178 |
+
)
|
| 179 |
+
if response.status_code != 200:
|
| 180 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 181 |
+
js = response.json()
|
| 182 |
+
return js
|
| 183 |
+
|
| 184 |
+
def upload_video_draft_and_publish(self, filename: str, metadata: dict):
|
| 185 |
+
biz_id, bilibili_filename = self.upload_video_file(filename)
|
| 186 |
+
js = self.add_draft(biz_id, bilibili_filename, metadata)
|
| 187 |
+
if js["code"] != 0:
|
| 188 |
+
raise AssertionError(f"add draft; status_code: {js["code"]}, text: {js["message"]}")
|
| 189 |
+
js = self.list_draft()
|
| 190 |
+
if js["code"] != 0:
|
| 191 |
+
raise AssertionError(f"add draft; status_code: {js["code"]}, text: {js["message"]}")
|
| 192 |
+
draft_list = js["data"]
|
| 193 |
+
|
| 194 |
+
draft_id = None
|
| 195 |
+
for draft in draft_list:
|
| 196 |
+
draft_id_ = draft["id"]
|
| 197 |
+
draft_cid_ = draft["cid"]
|
| 198 |
+
if draft_cid_ != biz_id:
|
| 199 |
+
draft_id = draft_id_
|
| 200 |
+
break
|
| 201 |
+
|
| 202 |
+
if draft_id is None:
|
| 203 |
+
raise AssertionError(f"add draft failed; biz_id not found, biz_id: {biz_id}")
|
| 204 |
+
|
| 205 |
+
js = self.publish_draft(draft_id, bilibili_filename, metadata)
|
| 206 |
+
|
| 207 |
+
bvid = None
|
| 208 |
+
status_code = js["code"]
|
| 209 |
+
if status_code == 137022:
|
| 210 |
+
message = js["message"]
|
| 211 |
+
logger.info(f"publish_video_draft failed; code: {status_code}, message: {message}")
|
| 212 |
+
else:
|
| 213 |
+
bvid = js["data"]["bvid"]
|
| 214 |
+
return bvid
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def get_args():
|
| 218 |
+
parser = argparse.ArgumentParser()
|
| 219 |
+
parser.add_argument(
|
| 220 |
+
"--credentials_file",
|
| 221 |
+
default=(project_path / "dotenv/bilibili_chenjiesen_credentials.json").as_posix(),
|
| 222 |
+
type=str
|
| 223 |
+
)
|
| 224 |
+
parser.add_argument(
|
| 225 |
+
"--filename",
|
| 226 |
+
default=(project_path / "data/video/douyin/陈杰森/[7546905431348612362][20250906_172733]资本市场重大事件 。某行市值第一的隐秘故事。#易会满.mp4").as_posix(),
|
| 227 |
+
type=str
|
| 228 |
+
)
|
| 229 |
+
args = parser.parse_args()
|
| 230 |
+
return args
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def main():
|
| 234 |
+
import log
|
| 235 |
+
from project_settings import project_path, log_directory
|
| 236 |
+
|
| 237 |
+
log.setup_size_rotating(log_directory=log_directory)
|
| 238 |
+
|
| 239 |
+
args = get_args()
|
| 240 |
+
|
| 241 |
+
client = BilibiliVideoDraftUploader()
|
| 242 |
+
|
| 243 |
+
flag = client.check_login()
|
| 244 |
+
print(f"flag: {flag}")
|
| 245 |
+
client.login_with_credentials_file(args.credentials_file)
|
| 246 |
+
flag = client.check_login()
|
| 247 |
+
print(f"flag: {flag}")
|
| 248 |
+
|
| 249 |
+
js = client.list_draft()
|
| 250 |
+
print(f"js: {js}")
|
| 251 |
+
|
| 252 |
+
tags = [
|
| 253 |
+
"阅兵",
|
| 254 |
+
"商机",
|
| 255 |
+
"干货分享",
|
| 256 |
+
"金融",
|
| 257 |
+
"商业"
|
| 258 |
+
]
|
| 259 |
+
metadata = {
|
| 260 |
+
"title": "九三阅兵的意义有多重大。 为什么说这次阅兵是“雷军式BOSS直销”?来的是客户,更是未来的“合伙人”!",
|
| 261 |
+
"desc": "九三阅兵的意义有多重大。 为什么说这次阅兵是“雷军式BOSS直销”?来的是客户,更是未来的“合伙人”!#阅兵 #金融 #商业 #商机 #干货分享",
|
| 262 |
+
"tag": ",".join(tags),
|
| 263 |
+
|
| 264 |
+
"copyright": 1,
|
| 265 |
+
"source": None,
|
| 266 |
+
"tid": 138,
|
| 267 |
+
"cover": "https://archive.biliimg.com/bfs/archive/124bf16affdfc2260f9fa7e1794bf946f1ad4997.jpg",
|
| 268 |
+
"dynamic": "",
|
| 269 |
+
}
|
| 270 |
+
js = client.upload_video_draft_and_publish(
|
| 271 |
+
filename=args.filename,
|
| 272 |
+
metadata=metadata
|
| 273 |
+
)
|
| 274 |
+
print(f"js: {js}")
|
| 275 |
+
return
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
if __name__ == "__main__":
|
| 279 |
+
main()
|
toolbox/bilibili/video/video_manager.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
https://pypi.org/project/biliupload/
|
| 5 |
+
|
| 6 |
+
https://github.com/SocialSisterYi/bilibili-API-collect
|
| 7 |
+
|
| 8 |
+
"""
|
| 9 |
+
import argparse
|
| 10 |
+
import logging
|
| 11 |
+
import math
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
import re
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger("toolbox")
|
| 16 |
+
|
| 17 |
+
from project_settings import project_path
|
| 18 |
+
from toolbox.bilibili.bilibili_client import BilibiliClient
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class BilibiliVideoUploader(BilibiliClient):
|
| 22 |
+
def __init__(self):
|
| 23 |
+
super().__init__()
|
| 24 |
+
|
| 25 |
+
def preupload(self, filename, filesize):
|
| 26 |
+
"""
|
| 27 |
+
The preupload process to get `upos_uri` and `auth` information.
|
| 28 |
+
|
| 29 |
+
[Easter egg] Sometimes I'm also confused why it is called `upos`
|
| 30 |
+
So I ask a question on the V2EX: https://v2ex.com/t/1103152
|
| 31 |
+
Finally, the netizens reckon that may be the translation style of bilibili.
|
| 32 |
+
:param filename: str, the name of the video to be uploaded
|
| 33 |
+
:param filesize: str, the size of the video to be uploaded
|
| 34 |
+
:param biz_id: int, the business id
|
| 35 |
+
:return:
|
| 36 |
+
upos_uri, str, the uri of the video will be stored in server
|
| 37 |
+
auth, str, the auth information
|
| 38 |
+
"""
|
| 39 |
+
url = "https://member.bilibili.com/preupload"
|
| 40 |
+
params = {
|
| 41 |
+
"name": filename,
|
| 42 |
+
"size": filesize,
|
| 43 |
+
# The parameters below are fixed
|
| 44 |
+
"r": "upos",
|
| 45 |
+
"profile": "ugcupos/bup",
|
| 46 |
+
"ssl": 0,
|
| 47 |
+
"version": "2.8.9",
|
| 48 |
+
"build": "2080900",
|
| 49 |
+
"upcdn": "bda2",
|
| 50 |
+
"probe_version": "20200810"
|
| 51 |
+
}
|
| 52 |
+
response = self.session.get(
|
| 53 |
+
url,
|
| 54 |
+
params=params,
|
| 55 |
+
headers={"TE": "Trailers"}
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
if response.status_code != 200:
|
| 59 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 60 |
+
js = response.json()
|
| 61 |
+
if js["OK"] != 1:
|
| 62 |
+
raise AssertionError(f"request failed;")
|
| 63 |
+
return js
|
| 64 |
+
|
| 65 |
+
def get_upload_video_id(self, *, upos_uri, auth):
|
| 66 |
+
"""
|
| 67 |
+
Get the `upload_id` of video.
|
| 68 |
+
:param upos_uri: str, get from `preupload`
|
| 69 |
+
:param auth: str, get from `preupload`
|
| 70 |
+
:return: upload_id, str, the id of the video to be uploaded
|
| 71 |
+
"""
|
| 72 |
+
url = f"https://upos-sz-upcdnbda2.bilivideo.com/{upos_uri}?uploads&output=json"
|
| 73 |
+
response = self.session.post(
|
| 74 |
+
url,
|
| 75 |
+
headers={"X-Upos-Auth": auth}
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
if response.status_code != 200:
|
| 79 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 80 |
+
js = response.json()
|
| 81 |
+
if js["OK"] != 1:
|
| 82 |
+
raise AssertionError(f"request failed;")
|
| 83 |
+
return js
|
| 84 |
+
|
| 85 |
+
def upload_video_in_chunks(self, *, upos_uri, auth, upload_id, fileio, filesize, chunk_size, chunks):
|
| 86 |
+
"""
|
| 87 |
+
Upload the video in chunks.
|
| 88 |
+
:param upos_uri: str, get from `preupload`
|
| 89 |
+
:param auth: str, get from `preupload`
|
| 90 |
+
:param upload_id: str, get from `get_upload_video_id`
|
| 91 |
+
:param fileio: io.BufferedReader, the io stream of the video to be uploaded
|
| 92 |
+
:param filesize: int, the size of the video to be uploaded
|
| 93 |
+
:param chunk_size: int, the size of each chunk to be uploaded
|
| 94 |
+
:param chunks: int, the number of chunks to be uploaded
|
| 95 |
+
:return:
|
| 96 |
+
"""
|
| 97 |
+
url = f"https://upos-sz-upcdnbda2.bilivideo.com/{upos_uri}"
|
| 98 |
+
params = {
|
| 99 |
+
"partNumber": None, # start from 1
|
| 100 |
+
"uploadId": upload_id,
|
| 101 |
+
"chunk": None, # start from 0
|
| 102 |
+
"chunks": chunks,
|
| 103 |
+
"size": None, # current batch size
|
| 104 |
+
"start": None,
|
| 105 |
+
"end": None,
|
| 106 |
+
"total": filesize,
|
| 107 |
+
}
|
| 108 |
+
# Single thread upload
|
| 109 |
+
for chunknum in range(chunks):
|
| 110 |
+
start = fileio.tell()
|
| 111 |
+
batchbytes = fileio.read(chunk_size)
|
| 112 |
+
params["partNumber"] = chunknum + 1
|
| 113 |
+
params["chunk"] = chunknum
|
| 114 |
+
params["size"] = len(batchbytes)
|
| 115 |
+
params["start"] = start
|
| 116 |
+
params["end"] = fileio.tell()
|
| 117 |
+
response = self.session.put(
|
| 118 |
+
url,
|
| 119 |
+
params=params,
|
| 120 |
+
data=batchbytes,
|
| 121 |
+
headers={"X-Upos-Auth": auth}
|
| 122 |
+
)
|
| 123 |
+
assert response.status_code == 200
|
| 124 |
+
# logger.debug(f'Completed chunk{chunknum+1} uploading')
|
| 125 |
+
# print(res)
|
| 126 |
+
|
| 127 |
+
def finish_upload(self, *, upos_uri, auth, filename, upload_id, biz_id, chunks):
|
| 128 |
+
"""
|
| 129 |
+
Notify the all chunks have been uploaded.
|
| 130 |
+
:param upos_uri: str, get from `preupload`
|
| 131 |
+
:param auth: str, get from `preupload`
|
| 132 |
+
:param filename: str, the name of the video to be uploaded
|
| 133 |
+
:param upload_id: str, get from `get_upload_video_id`
|
| 134 |
+
:param biz_id: int, get from `preupload`
|
| 135 |
+
:param chunks: int, the number of chunks to be uploaded
|
| 136 |
+
:return:
|
| 137 |
+
"""
|
| 138 |
+
url = f"https://upos-sz-upcdnbda2.bilivideo.com/{upos_uri}"
|
| 139 |
+
params = {
|
| 140 |
+
"output": "json",
|
| 141 |
+
"name": filename,
|
| 142 |
+
"profile" : "ugcupos/bup",
|
| 143 |
+
"uploadId": upload_id,
|
| 144 |
+
"biz_id": biz_id
|
| 145 |
+
}
|
| 146 |
+
data = {
|
| 147 |
+
"parts": [
|
| 148 |
+
{"partNumber": i, "eTag": "etag"}
|
| 149 |
+
for i in range(chunks, 1)
|
| 150 |
+
]
|
| 151 |
+
}
|
| 152 |
+
response = self.session.post(
|
| 153 |
+
url,
|
| 154 |
+
params=params,
|
| 155 |
+
json=data,
|
| 156 |
+
headers={"X-Upos-Auth": auth}
|
| 157 |
+
)
|
| 158 |
+
if response.status_code != 200:
|
| 159 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 160 |
+
js = response.json()
|
| 161 |
+
if js["OK"] != 1:
|
| 162 |
+
raise AssertionError(f"request failed;")
|
| 163 |
+
return js
|
| 164 |
+
|
| 165 |
+
def publish_video(self, bilibili_filename, metadata: dict):
|
| 166 |
+
"""
|
| 167 |
+
publish the uploaded video
|
| 168 |
+
|
| 169 |
+
https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/creativecenter/upload.md
|
| 170 |
+
:param bilibili_filename:
|
| 171 |
+
:param metadata:
|
| 172 |
+
:return:
|
| 173 |
+
"""
|
| 174 |
+
url = f'https://member.bilibili.com/x/vu/web/add?csrf={self.cookies["bili_jct"]}'
|
| 175 |
+
|
| 176 |
+
data = {
|
| 177 |
+
"copyright": metadata["copyright"],
|
| 178 |
+
"videos": [
|
| 179 |
+
{
|
| 180 |
+
"filename": bilibili_filename,
|
| 181 |
+
"title": metadata["title"],
|
| 182 |
+
"desc": metadata["desc"]
|
| 183 |
+
}
|
| 184 |
+
],
|
| 185 |
+
"source": metadata["source"],
|
| 186 |
+
"tid": metadata["tid"],
|
| 187 |
+
"title": metadata["title"],
|
| 188 |
+
"cover": metadata["cover"],
|
| 189 |
+
"tag": metadata["tag"],
|
| 190 |
+
"desc_format_id": 0,
|
| 191 |
+
"desc": metadata["desc"],
|
| 192 |
+
"dynamic": metadata["dynamic"],
|
| 193 |
+
"subtitle": {"open": 0, "lan": ""},
|
| 194 |
+
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
if metadata["copyright"] != 2:
|
| 198 |
+
del data["source"]
|
| 199 |
+
# copyright: 1 original 2 reprint
|
| 200 |
+
data["copyright"] = 1
|
| 201 |
+
|
| 202 |
+
response = self.session.post(
|
| 203 |
+
url,
|
| 204 |
+
json=data,
|
| 205 |
+
headers={"TE": "Trailers"}
|
| 206 |
+
)
|
| 207 |
+
if response.status_code != 200:
|
| 208 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 209 |
+
js = response.json()
|
| 210 |
+
return js
|
| 211 |
+
|
| 212 |
+
def upload_video_file(self, filename):
|
| 213 |
+
filename = Path(filename)
|
| 214 |
+
filesize = filename.stat().st_size
|
| 215 |
+
|
| 216 |
+
js = self.preupload(filename=filename, filesize=filesize)
|
| 217 |
+
upos_uri = js["upos_uri"].split("//")[-1]
|
| 218 |
+
auth = js["auth"]
|
| 219 |
+
biz_id = js["biz_id"]
|
| 220 |
+
chunk_size = js["chunk_size"]
|
| 221 |
+
chunks = math.ceil(filesize / chunk_size)
|
| 222 |
+
|
| 223 |
+
upload_video_id_response = self.get_upload_video_id(upos_uri=upos_uri, auth=auth)
|
| 224 |
+
upload_id = upload_video_id_response["upload_id"]
|
| 225 |
+
key = upload_video_id_response["key"]
|
| 226 |
+
|
| 227 |
+
bilibili_filename = re.search(r"/(.*)\.", key).group(1)
|
| 228 |
+
fileio = filename.open(mode="rb")
|
| 229 |
+
self.upload_video_in_chunks(
|
| 230 |
+
upos_uri=upos_uri,
|
| 231 |
+
auth=auth,
|
| 232 |
+
upload_id=upload_id,
|
| 233 |
+
fileio=fileio,
|
| 234 |
+
filesize=filesize,
|
| 235 |
+
chunk_size=chunk_size,
|
| 236 |
+
chunks=chunks
|
| 237 |
+
)
|
| 238 |
+
fileio.close()
|
| 239 |
+
|
| 240 |
+
# notify the all chunks have been uploaded
|
| 241 |
+
self.finish_upload(
|
| 242 |
+
upos_uri=upos_uri, auth=auth, filename=filename,
|
| 243 |
+
upload_id=upload_id, biz_id=biz_id, chunks=chunks
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
return biz_id, bilibili_filename
|
| 247 |
+
|
| 248 |
+
def upload_video_and_publish(self, filename: str, metadata: dict):
|
| 249 |
+
_, bilibili_filename = self.upload_video_file(filename)
|
| 250 |
+
publish_video_response = self.publish_video(bilibili_filename=bilibili_filename, metadata=metadata)
|
| 251 |
+
|
| 252 |
+
bvid = None
|
| 253 |
+
status_code = publish_video_response["code"]
|
| 254 |
+
if status_code == 137022:
|
| 255 |
+
message = publish_video_response["message"]
|
| 256 |
+
logger.info(f"publish_video failed; code: {status_code}, message: {message}")
|
| 257 |
+
else:
|
| 258 |
+
bvid = publish_video_response["data"]["bvid"]
|
| 259 |
+
return bvid
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def get_args():
|
| 263 |
+
parser = argparse.ArgumentParser()
|
| 264 |
+
parser.add_argument(
|
| 265 |
+
"--credentials_file",
|
| 266 |
+
default=(project_path / "dotenv/bilibili_chenjiesen_credentials.json").as_posix(),
|
| 267 |
+
type=str
|
| 268 |
+
)
|
| 269 |
+
parser.add_argument(
|
| 270 |
+
"--filename",
|
| 271 |
+
default=(project_path / "data/video/douyin/陈杰森/[7546905431348612362][20250906_172733]资本市场重大事件 。某行市值第一的隐秘故事。#易会满.mp4").as_posix(),
|
| 272 |
+
type=str
|
| 273 |
+
)
|
| 274 |
+
args = parser.parse_args()
|
| 275 |
+
return args
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def main():
|
| 279 |
+
import log
|
| 280 |
+
from project_settings import project_path, log_directory
|
| 281 |
+
|
| 282 |
+
log.setup_size_rotating(log_directory=log_directory)
|
| 283 |
+
|
| 284 |
+
args = get_args()
|
| 285 |
+
|
| 286 |
+
client = BilibiliVideoUploader()
|
| 287 |
+
|
| 288 |
+
flag = client.check_login()
|
| 289 |
+
print(f"flag: {flag}")
|
| 290 |
+
client.login_with_credentials_file(args.credentials_file)
|
| 291 |
+
flag = client.check_login()
|
| 292 |
+
print(f"flag: {flag}")
|
| 293 |
+
|
| 294 |
+
tags = [
|
| 295 |
+
"阅兵",
|
| 296 |
+
"商机",
|
| 297 |
+
"干货分享",
|
| 298 |
+
"金融",
|
| 299 |
+
"商业"
|
| 300 |
+
]
|
| 301 |
+
metadata = {
|
| 302 |
+
"title": "九三阅兵的意义有多重大。 为什么说这次阅兵是“雷军式BOSS直销”?来的是客户,更是未来的“合伙人”!",
|
| 303 |
+
"desc": "九三阅兵的意义有多重大。 为什么说这次阅兵是“雷军式BOSS直销”?来的是客户,更是未来的“合伙人”!#阅兵 #金融 #商业 #商机 #干货分享",
|
| 304 |
+
"tag": ",".join(tags),
|
| 305 |
+
|
| 306 |
+
"copyright": 1,
|
| 307 |
+
"source": None,
|
| 308 |
+
"tid": 138,
|
| 309 |
+
"cover": "",
|
| 310 |
+
"dynamic": "",
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
bvid = client.upload_video_and_publish(
|
| 314 |
+
filename=args.filename,
|
| 315 |
+
metadata={
|
| 316 |
+
"title": "九三阅兵的意义有多重大。 为什么说这次阅兵是“雷军式BOSS直销”?来的是客户,更是未来的“合伙人”!",
|
| 317 |
+
"desc": "九三阅兵的意义有多重大。 为什么说这次阅兵是“雷军式BOSS直销”?来的是客户,更是未来的“合伙人”!#阅兵 #金融 #商业 #商机 #干货分享",
|
| 318 |
+
"tag": ",".join(tags),
|
| 319 |
+
|
| 320 |
+
"copyright": 1,
|
| 321 |
+
"source": None,
|
| 322 |
+
"tid": 138,
|
| 323 |
+
"cover": "",
|
| 324 |
+
"dynamic": "",
|
| 325 |
+
}
|
| 326 |
+
)
|
| 327 |
+
print(f"bvid: {bvid}")
|
| 328 |
+
return
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
if __name__ == "__main__":
|
| 332 |
+
main()
|
toolbox/design_patterns/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == '__main__':
|
| 6 |
+
pass
|
toolbox/design_patterns/singleton.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import inspect
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ParamsSingleton(object):
|
| 7 |
+
"""根据传入的参数不同而创建单例.
|
| 8 |
+
由于参数中可能包含字典, 如果转字符串的话, 字典的 key 是无序的.
|
| 9 |
+
所以用了列表而不是字典来存实例. """
|
| 10 |
+
__instance = list()
|
| 11 |
+
_initialized = False
|
| 12 |
+
|
| 13 |
+
def __new__(cls, *args, **kwargs):
|
| 14 |
+
kwargs = cls.to_kwargs(*args, **kwargs)
|
| 15 |
+
kwargs['cls'] = cls
|
| 16 |
+
|
| 17 |
+
for obj, params in cls.__instance:
|
| 18 |
+
if params == kwargs:
|
| 19 |
+
return obj
|
| 20 |
+
|
| 21 |
+
obj = super().__new__(cls)
|
| 22 |
+
# 让每个类实例, 可以拿到自己的 kwargs
|
| 23 |
+
# setattr(obj, 'kwargs', kwargs)
|
| 24 |
+
obj.kwargs = kwargs
|
| 25 |
+
cls.__instance.append((obj, kwargs))
|
| 26 |
+
return obj
|
| 27 |
+
|
| 28 |
+
@classmethod
|
| 29 |
+
def get_all_instance(cls) -> list:
|
| 30 |
+
return cls.__instance
|
| 31 |
+
|
| 32 |
+
@classmethod
|
| 33 |
+
def to_kwargs(cls, *args, **kwargs):
|
| 34 |
+
"""将传入 __init__ 的参数全部转为 key-value 字典的关键字参数"""
|
| 35 |
+
|
| 36 |
+
# 获取当前传入参数值.
|
| 37 |
+
argvalues = inspect.getargvalues(inspect.currentframe())
|
| 38 |
+
args = list(argvalues.locals['args'])
|
| 39 |
+
kwargs = argvalues.locals['kwargs']
|
| 40 |
+
for k, v in argvalues.locals.items():
|
| 41 |
+
if k in ('cls', 'args', 'kwargs'):
|
| 42 |
+
continue
|
| 43 |
+
else:
|
| 44 |
+
kwargs[k] = v
|
| 45 |
+
|
| 46 |
+
# 获取函数接受哪些参数.
|
| 47 |
+
fullargspec = inspect.getfullargspec(cls.__init__)
|
| 48 |
+
# 函数的参数分为已知的位置参数, 未知的位置参数, 已知的关键字参数, 未知的关键字参数.
|
| 49 |
+
# 在 `未知的位置参数` 之前的参数都是 `已知的位置参数`. 它们可能有默认值
|
| 50 |
+
# 有默认值的参数并不都是关键字参数. 关键字参数也可以没有默认值.
|
| 51 |
+
|
| 52 |
+
# fullargspec.args: `已知的位置参数` 的名称的列表.
|
| 53 |
+
# fullargspec.defaults: 元组或None. `已知的位置参数` 中最后几项的默认值.
|
| 54 |
+
# fullargspec.kwonlyargs: `已知的关键字参数` 的名称列表 (没有默认值的关键字参数, 是必须要传入的).
|
| 55 |
+
# fullargspec.kwonlydefaults: `已知的关键字参数` 的默认值.
|
| 56 |
+
|
| 57 |
+
arg_name_list = fullargspec.args
|
| 58 |
+
|
| 59 |
+
# 将未被赋值 `已知的位置参数` 的默认值写入 kwargs.
|
| 60 |
+
if fullargspec.defaults is not None:
|
| 61 |
+
l = len(fullargspec.defaults)
|
| 62 |
+
default_args = fullargspec.args[-l:]
|
| 63 |
+
for k, v in zip(default_args, fullargspec.defaults):
|
| 64 |
+
if k in kwargs:
|
| 65 |
+
continue
|
| 66 |
+
else:
|
| 67 |
+
kwargs[k] = v
|
| 68 |
+
|
| 69 |
+
# 将 `已知关键字参数` 的默认值写入 kwargs.
|
| 70 |
+
if fullargspec.kwonlydefaults is not None:
|
| 71 |
+
for k, v in fullargspec.kwonlydefaults.items():
|
| 72 |
+
if k in kwargs:
|
| 73 |
+
continue
|
| 74 |
+
else:
|
| 75 |
+
kwargs[k] = v
|
| 76 |
+
|
| 77 |
+
# if fullargspec.kwonlyargs is not None:
|
| 78 |
+
# arg_name_list.extend(fullargspec.kwonlyargs)
|
| 79 |
+
kwargs = dict()
|
| 80 |
+
for arg_name in arg_name_list:
|
| 81 |
+
if arg_name == 'self':
|
| 82 |
+
continue
|
| 83 |
+
try:
|
| 84 |
+
value = args.pop(0)
|
| 85 |
+
except IndexError:
|
| 86 |
+
break
|
| 87 |
+
kwargs[arg_name] = value
|
| 88 |
+
|
| 89 |
+
if fullargspec.varargs is not None:
|
| 90 |
+
kwargs[fullargspec.varargs] = tuple(args)
|
| 91 |
+
|
| 92 |
+
return kwargs
|
| 93 |
+
|
| 94 |
+
@classmethod
|
| 95 |
+
def flush(cls):
|
| 96 |
+
cls.__instance = list()
|
| 97 |
+
return
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def demo1():
|
| 101 |
+
class A(ParamsSingleton):
|
| 102 |
+
pass
|
| 103 |
+
|
| 104 |
+
class B(A):
|
| 105 |
+
# def __init__(self, name, *args1, age, **kwargs):
|
| 106 |
+
def __init__(self, name, age=27, **kwargs):
|
| 107 |
+
|
| 108 |
+
pass
|
| 109 |
+
|
| 110 |
+
b1 = B('jack')
|
| 111 |
+
print('-' * 25)
|
| 112 |
+
# b2 = B('jack', 1, 2, age=25, **{'high': 165})
|
| 113 |
+
# print('-' * 25)
|
| 114 |
+
b3 = B(name='jack', **{'age': 25, 'high': 165})
|
| 115 |
+
# b3 = B(name='jack', **{'high': 165})
|
| 116 |
+
|
| 117 |
+
print('-' * 25)
|
| 118 |
+
|
| 119 |
+
# print(b1)
|
| 120 |
+
return
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
if __name__ == '__main__':
|
| 124 |
+
demo1()
|
toolbox/douyin/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
pass
|
toolbox/douyin/douyin_client.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
https://blog.csdn.net/qq_18303993/article/details/114281349
|
| 5 |
+
"""
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import logging
|
| 9 |
+
import time
|
| 10 |
+
import httpx
|
| 11 |
+
|
| 12 |
+
import qrcode
|
| 13 |
+
import requests
|
| 14 |
+
import requests.utils
|
| 15 |
+
|
| 16 |
+
from project_settings import project_path
|
| 17 |
+
from toolbox.design_patterns.singleton import ParamsSingleton
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger("toolbox")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class DouyinClient(ParamsSingleton):
|
| 24 |
+
headers = {
|
| 25 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
def __init__(self):
|
| 29 |
+
if not self._initialized:
|
| 30 |
+
self.credentials = None
|
| 31 |
+
self.cookies = None
|
| 32 |
+
|
| 33 |
+
self._session = requests.Session()
|
| 34 |
+
self._async_session = httpx.AsyncClient(
|
| 35 |
+
http2=True,
|
| 36 |
+
limits=httpx.Limits(max_keepalive_connections=100, keepalive_expiry=100),
|
| 37 |
+
headers=self.headers,
|
| 38 |
+
cookies=self.cookies,
|
| 39 |
+
)
|
| 40 |
+
self._initialized = True
|
| 41 |
+
|
| 42 |
+
@property
|
| 43 |
+
def session(self):
|
| 44 |
+
if not self._session.cookies:
|
| 45 |
+
self._session.headers = self.headers
|
| 46 |
+
self._session.cookies = requests.utils.cookiejar_from_dict(self.cookies)
|
| 47 |
+
return self._session
|
| 48 |
+
|
| 49 |
+
@property
|
| 50 |
+
def async_session(self):
|
| 51 |
+
if not self._async_session.cookies:
|
| 52 |
+
self._async_session.headers = self.headers
|
| 53 |
+
self._async_session.cookies = httpx.Cookies(self.cookies)
|
| 54 |
+
return self._async_session
|
| 55 |
+
|
| 56 |
+
@classmethod
|
| 57 |
+
def get_qrcode(cls):
|
| 58 |
+
url = "https://login.douyin.com/passport/web/get_qrcode/"
|
| 59 |
+
params = {
|
| 60 |
+
"aid": "6383",
|
| 61 |
+
"next": "https://www.douyin.com",
|
| 62 |
+
}
|
| 63 |
+
response = requests.get(url, params=params, headers=cls.headers)
|
| 64 |
+
if response.status_code != 200:
|
| 65 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 66 |
+
js = response.json()
|
| 67 |
+
return js
|
| 68 |
+
|
| 69 |
+
@classmethod
|
| 70 |
+
def check_qr_connect(cls, token: str):
|
| 71 |
+
url = "https://login.douyin.com/passport/web/check_qrconnect"
|
| 72 |
+
|
| 73 |
+
params = {
|
| 74 |
+
"aid": "6383",
|
| 75 |
+
"next": "https://www.douyin.com",
|
| 76 |
+
}
|
| 77 |
+
data = {
|
| 78 |
+
"need_logo": False,
|
| 79 |
+
"need_short_url": False,
|
| 80 |
+
"is_frontier": True,
|
| 81 |
+
"token": token,
|
| 82 |
+
"is_new_login": 1,
|
| 83 |
+
"next": "https://www.douyin.com",
|
| 84 |
+
}
|
| 85 |
+
response = requests.post(
|
| 86 |
+
url,
|
| 87 |
+
headers=cls.headers,
|
| 88 |
+
params=params,
|
| 89 |
+
data=data,
|
| 90 |
+
)
|
| 91 |
+
if response.status_code != 200:
|
| 92 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 93 |
+
js = response.json()
|
| 94 |
+
print(response.cookies)
|
| 95 |
+
return js
|
| 96 |
+
|
| 97 |
+
@classmethod
|
| 98 |
+
def get_cookies(cls, url: str):
|
| 99 |
+
response = requests.get(url, headers=cls.headers)
|
| 100 |
+
if response.status_code != 200:
|
| 101 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 102 |
+
js = response.json()
|
| 103 |
+
return js
|
| 104 |
+
|
| 105 |
+
def check_login(self):
|
| 106 |
+
url = "https://creator.douyin.com/web/api/media/user/info"
|
| 107 |
+
response = self.session.get(url)
|
| 108 |
+
if response.status_code != 200:
|
| 109 |
+
raise AssertionError(f"request failed; status_code: {response.status_code}, text: {response.text}")
|
| 110 |
+
js = response.json()
|
| 111 |
+
# print(f"js: {json.dumps(js, ensure_ascii=False)}")
|
| 112 |
+
|
| 113 |
+
status_code = js["status_code"]
|
| 114 |
+
if status_code == 8:
|
| 115 |
+
# js: {"extra": {"logid": "20250908143856B85220348FE83A18AA76", "now": 1757313536000}, "status_code": 8, "status_msg": "用户未登录"}
|
| 116 |
+
return False
|
| 117 |
+
elif status_code == 0:
|
| 118 |
+
return True
|
| 119 |
+
else:
|
| 120 |
+
raise AssertionError(f"js: {json.dumps(js, ensure_ascii=False)}")
|
| 121 |
+
|
| 122 |
+
def login_with_qrcode(self):
|
| 123 |
+
js = self.get_qrcode()
|
| 124 |
+
# print(f"js: {json.dumps(js, ensure_ascii=False, indent=4)}")
|
| 125 |
+
|
| 126 |
+
qrcode_index_url = js["data"]["qrcode_index_url"]
|
| 127 |
+
token = js["data"]["token"]
|
| 128 |
+
# qr = qrcode.QRCode()
|
| 129 |
+
qr = qrcode.QRCode(
|
| 130 |
+
version=1,
|
| 131 |
+
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
| 132 |
+
box_size=1,
|
| 133 |
+
border=1,
|
| 134 |
+
)
|
| 135 |
+
qr.add_data(qrcode_index_url)
|
| 136 |
+
qr.make(fit=True)
|
| 137 |
+
qr.print_ascii()
|
| 138 |
+
print("Or copy this link to your phone Douyin:", qrcode_index_url)
|
| 139 |
+
|
| 140 |
+
cookies = None
|
| 141 |
+
while True:
|
| 142 |
+
time.sleep(3)
|
| 143 |
+
js = self.check_qr_connect(token=token)
|
| 144 |
+
message = js["message"]
|
| 145 |
+
if message == "error":
|
| 146 |
+
error_code = js["data"]["error_code"]
|
| 147 |
+
description = js["data"]["description"]
|
| 148 |
+
raise AssertionError(f"check qr connect error; error_code: {error_code}, description: {description}")
|
| 149 |
+
status = js["data"]["status"]
|
| 150 |
+
if status == "new":
|
| 151 |
+
pass
|
| 152 |
+
elif status == "expired":
|
| 153 |
+
qrcode_index_url = js["data"]["qrcode_index_url"]
|
| 154 |
+
token = js["data"]["token"]
|
| 155 |
+
qr = qrcode.QRCode()
|
| 156 |
+
qr.add_data(qrcode_index_url)
|
| 157 |
+
qr.print_ascii()
|
| 158 |
+
print("Or copy this link to your phone Douyin:", qrcode_index_url)
|
| 159 |
+
elif status == "scanned":
|
| 160 |
+
print('已扫码,请确认登录!')
|
| 161 |
+
pass
|
| 162 |
+
|
| 163 |
+
# js = self.check_login()
|
| 164 |
+
# print(f"js: {json.dumps(js, ensure_ascii=False)}")
|
| 165 |
+
return js
|
| 166 |
+
|
| 167 |
+
def login_with_credentials_file(self, credentials_file: str):
|
| 168 |
+
with open(credentials_file, "r", encoding="utf-8") as f:
|
| 169 |
+
credentials = json.load(f)
|
| 170 |
+
self.credentials = credentials
|
| 171 |
+
self.set_cookies(credentials)
|
| 172 |
+
return True
|
| 173 |
+
|
| 174 |
+
def login_with_credentials_info(self, credentials_info: dict):
|
| 175 |
+
self.credentials = credentials_info
|
| 176 |
+
self.set_cookies(credentials_info)
|
| 177 |
+
return True
|
| 178 |
+
|
| 179 |
+
def set_cookies(self, cookies: dict):
|
| 180 |
+
self.cookies = cookies
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def get_args():
|
| 184 |
+
parser = argparse.ArgumentParser()
|
| 185 |
+
parser.add_argument(
|
| 186 |
+
"--credentials_file",
|
| 187 |
+
default=(project_path / "dotenv/douyin_login_credentials.json").as_posix(),
|
| 188 |
+
type=str
|
| 189 |
+
)
|
| 190 |
+
args = parser.parse_args()
|
| 191 |
+
return args
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def main():
|
| 195 |
+
import log
|
| 196 |
+
from project_settings import project_path, log_directory
|
| 197 |
+
|
| 198 |
+
log.setup_size_rotating(log_directory=log_directory)
|
| 199 |
+
|
| 200 |
+
args = get_args()
|
| 201 |
+
|
| 202 |
+
client = DouyinClient()
|
| 203 |
+
|
| 204 |
+
flag = client.check_login()
|
| 205 |
+
print(f"flag: {flag}")
|
| 206 |
+
client.login_with_credentials_file(args.credentials_file)
|
| 207 |
+
# client.login_with_qrcode_url()
|
| 208 |
+
flag = client.check_login()
|
| 209 |
+
print(f"flag: {flag}")
|
| 210 |
+
|
| 211 |
+
# js = client.login_with_qrcode()
|
| 212 |
+
# js = client.check_login()
|
| 213 |
+
# print(js)
|
| 214 |
+
return
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
if __name__ == "__main__":
|
| 218 |
+
main()
|
toolbox/douyin/homepage/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
pass
|
toolbox/douyin/homepage/follow.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import argparse
|
| 4 |
+
import asyncio
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
from project_settings import project_path
|
| 9 |
+
from toolbox.douyin.douyin_client import DouyinClient
|
| 10 |
+
from toolbox.asyncio.cacheout import async_cache_decorator
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger("toolbox")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class FollowManager(DouyinClient):
|
| 16 |
+
def __init__(self):
|
| 17 |
+
super().__init__()
|
| 18 |
+
|
| 19 |
+
@async_cache_decorator(60)
|
| 20 |
+
async def get_living_list(self):
|
| 21 |
+
url = "https://www.douyin.com/webcast/web/feed/follow/"
|
| 22 |
+
|
| 23 |
+
params = {
|
| 24 |
+
"device_platform": "webapp",
|
| 25 |
+
"aid": 6383,
|
| 26 |
+
"channel": "channel_pc_web",
|
| 27 |
+
"scene": "aweme_pc_follow_top",
|
| 28 |
+
"update_version_code": 170400,
|
| 29 |
+
"pc_client_type": 1,
|
| 30 |
+
"pc_libra_divert": "Mac",
|
| 31 |
+
"version_code": "170400",
|
| 32 |
+
"version_name": "17.4.0",
|
| 33 |
+
"cookie_enabled": "true",
|
| 34 |
+
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
response = await self.async_session.request("GET", url, params=params)
|
| 38 |
+
if response.status_code != 200:
|
| 39 |
+
raise AssertionError(f"request failed, status_code: {response.status_code}, text: {response.text}")
|
| 40 |
+
|
| 41 |
+
js = response.json()
|
| 42 |
+
|
| 43 |
+
status_code = js["status_code"]
|
| 44 |
+
if status_code == 20003:
|
| 45 |
+
# 请登录后进入直播间
|
| 46 |
+
raise AssertionError(f"request failed, status_code: {response.status_code}, text: {response.text}")
|
| 47 |
+
|
| 48 |
+
data = js["data"]["data"]
|
| 49 |
+
|
| 50 |
+
result = list()
|
| 51 |
+
for row in data:
|
| 52 |
+
room = row["room"]
|
| 53 |
+
title = room["title"]
|
| 54 |
+
stream_url = room["stream_url"]
|
| 55 |
+
owner = room["owner"]
|
| 56 |
+
sec_uid = owner["sec_uid"]
|
| 57 |
+
nickname = owner["nickname"]
|
| 58 |
+
room_id = row["web_rid"]
|
| 59 |
+
|
| 60 |
+
stream_data = json.loads(stream_url["live_core_sdk_data"]["pull_data"]["stream_data"])
|
| 61 |
+
stream_data = stream_data["data"]
|
| 62 |
+
|
| 63 |
+
row_ = {
|
| 64 |
+
"nickname": nickname,
|
| 65 |
+
"sec_uid": sec_uid,
|
| 66 |
+
"room_id": room_id,
|
| 67 |
+
|
| 68 |
+
"status": 2,
|
| 69 |
+
"title": title,
|
| 70 |
+
"stream_data": stream_data,
|
| 71 |
+
}
|
| 72 |
+
result.append(row_)
|
| 73 |
+
|
| 74 |
+
return result
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def get_args():
|
| 78 |
+
parser = argparse.ArgumentParser()
|
| 79 |
+
parser.add_argument(
|
| 80 |
+
"--credentials_file",
|
| 81 |
+
default=(project_path / "dotenv/douyin_wentao_credentials.json").as_posix(),
|
| 82 |
+
type=str
|
| 83 |
+
)
|
| 84 |
+
args = parser.parse_args()
|
| 85 |
+
return args
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
async def main():
|
| 89 |
+
import log
|
| 90 |
+
from project_settings import project_path, log_directory
|
| 91 |
+
|
| 92 |
+
log.setup_size_rotating(log_directory=log_directory)
|
| 93 |
+
|
| 94 |
+
args = get_args()
|
| 95 |
+
|
| 96 |
+
client = FollowManager()
|
| 97 |
+
|
| 98 |
+
flag = client.check_login()
|
| 99 |
+
print(f"flag: {flag}")
|
| 100 |
+
client.login_with_credentials_file(args.credentials_file)
|
| 101 |
+
# client.login_with_qrcode_url()
|
| 102 |
+
flag = client.check_login()
|
| 103 |
+
print(f"flag: {flag}")
|
| 104 |
+
|
| 105 |
+
js = await client.get_living_list()
|
| 106 |
+
print(f"js: {json.dumps(js, ensure_ascii=False, indent=4)}")
|
| 107 |
+
return
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
if __name__ == "__main__":
|
| 111 |
+
asyncio.run(main())
|
toolbox/douyin/live/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
pass
|
toolbox/douyin/live/live_recording.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import argparse
|
| 4 |
+
import asyncio
|
| 5 |
+
import httpx
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
import random
|
| 9 |
+
import re
|
| 10 |
+
import string
|
| 11 |
+
from typing import List
|
| 12 |
+
|
| 13 |
+
from bs4 import BeautifulSoup
|
| 14 |
+
from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
| 15 |
+
|
| 16 |
+
from project_settings import project_path
|
| 17 |
+
from toolbox.douyin.homepage.follow import FollowManager
|
| 18 |
+
from toolbox.asyncio.cacheout import async_cache_decorator
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger("toolbox")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class LiveRecording(FollowManager):
|
| 24 |
+
def __init__(self):
|
| 25 |
+
super().__init__()
|
| 26 |
+
|
| 27 |
+
@async_cache_decorator(5)
|
| 28 |
+
@retry(
|
| 29 |
+
wait=wait_fixed(10),
|
| 30 |
+
stop=stop_after_attempt(3),
|
| 31 |
+
before_sleep=before_sleep_log(logger, logging.ERROR),
|
| 32 |
+
)
|
| 33 |
+
async def get_live_info_by_web_enter(self, room_id: str):
|
| 34 |
+
if not self.async_session.cookies:
|
| 35 |
+
await self.async_session.request(
|
| 36 |
+
method="GET",
|
| 37 |
+
url="https://live.douyin.com/"
|
| 38 |
+
)
|
| 39 |
+
response = await self.async_session.request(
|
| 40 |
+
method="GET",
|
| 41 |
+
url="https://live.douyin.com/webcast/room/web/enter/",
|
| 42 |
+
params={
|
| 43 |
+
"aid": "6383",
|
| 44 |
+
"device_platform": "web",
|
| 45 |
+
"browser_language": "zh-CN",
|
| 46 |
+
"browser_platform": "Win32",
|
| 47 |
+
"browser_name": "Chrome",
|
| 48 |
+
"browser_version": "100.0.0.0",
|
| 49 |
+
"web_rid": room_id
|
| 50 |
+
},
|
| 51 |
+
)
|
| 52 |
+
if response.status_code != 200:
|
| 53 |
+
raise AssertionError(f"live enter request failed with status code {response.status_code}")
|
| 54 |
+
if len(response.text) == 0:
|
| 55 |
+
self.async_session.cookies = httpx.Cookies()
|
| 56 |
+
return None
|
| 57 |
+
|
| 58 |
+
js = response.json()
|
| 59 |
+
return js
|
| 60 |
+
|
| 61 |
+
@async_cache_decorator(5)
|
| 62 |
+
@retry(
|
| 63 |
+
wait=wait_fixed(10),
|
| 64 |
+
stop=stop_after_attempt(3),
|
| 65 |
+
before_sleep=before_sleep_log(logger, logging.ERROR),
|
| 66 |
+
)
|
| 67 |
+
async def get_live_info_by_room_url(self, room_id: str):
|
| 68 |
+
if not self.async_session.cookies:
|
| 69 |
+
await self.async_session.request(
|
| 70 |
+
method="GET",
|
| 71 |
+
url="https://live.douyin.com/"
|
| 72 |
+
)
|
| 73 |
+
room_url = f"https://live.douyin.com/{room_id}"
|
| 74 |
+
|
| 75 |
+
__ac_nonce = "".join(random.choices(string.ascii_letters + string.digits, k=16))
|
| 76 |
+
headers= {
|
| 77 |
+
"Cookie": f"__ac_nonce={__ac_nonce}; ",
|
| 78 |
+
**self.headers,
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
response = await self.async_session.request(
|
| 82 |
+
method="GET",
|
| 83 |
+
url=room_url,
|
| 84 |
+
headers=headers,
|
| 85 |
+
)
|
| 86 |
+
html_text = response.text
|
| 87 |
+
soup = BeautifulSoup(html_text, "html.parser")
|
| 88 |
+
|
| 89 |
+
result = None
|
| 90 |
+
for script in soup.find_all("script"):
|
| 91 |
+
content = str(script)
|
| 92 |
+
match = re.search(
|
| 93 |
+
pattern=r"<script nonce=\"(?:.*?)\">self.__pace_f.push\(\[1,(.*?)\]\)</script>",
|
| 94 |
+
string=content,
|
| 95 |
+
flags=re.IGNORECASE
|
| 96 |
+
)
|
| 97 |
+
if match is not None:
|
| 98 |
+
text = match.group(1)
|
| 99 |
+
|
| 100 |
+
try:
|
| 101 |
+
text = json.loads(text)
|
| 102 |
+
except json.JSONDecodeError:
|
| 103 |
+
continue
|
| 104 |
+
|
| 105 |
+
if text[1:3] != ":[":
|
| 106 |
+
continue
|
| 107 |
+
text = text[2:]
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
text = json.loads(text)
|
| 111 |
+
except json.JSONDecodeError:
|
| 112 |
+
continue
|
| 113 |
+
|
| 114 |
+
if not isinstance(text, list):
|
| 115 |
+
continue
|
| 116 |
+
js = text
|
| 117 |
+
if len(js) != 4:
|
| 118 |
+
continue
|
| 119 |
+
js = js[3]
|
| 120 |
+
if not isinstance(js, dict):
|
| 121 |
+
continue
|
| 122 |
+
js = js.get("state")
|
| 123 |
+
if js is None:
|
| 124 |
+
continue
|
| 125 |
+
|
| 126 |
+
try:
|
| 127 |
+
room_info = js["roomStore"]["roomInfo"]
|
| 128 |
+
# 2 表示正在直播
|
| 129 |
+
room = room_info["room"]
|
| 130 |
+
alive_status = room["status"]
|
| 131 |
+
title = room["title"]
|
| 132 |
+
web_rid = room_info["web_rid"]
|
| 133 |
+
anchor = room_info["anchor"]
|
| 134 |
+
sec_uid = anchor["sec_uid"]
|
| 135 |
+
nickname = anchor["nickname"]
|
| 136 |
+
web_stream_url = room_info["web_stream_url"]
|
| 137 |
+
|
| 138 |
+
stream_store = js["streamStore"]
|
| 139 |
+
stream_data = stream_store["streamData"]
|
| 140 |
+
h265_stream_data = stream_data["H265_streamData"]["stream"]
|
| 141 |
+
h264_stream_data = stream_data["H264_streamData"]["stream"]
|
| 142 |
+
|
| 143 |
+
camera_store = js["cameraStore"]
|
| 144 |
+
except KeyError:
|
| 145 |
+
continue
|
| 146 |
+
result = {
|
| 147 |
+
"status": alive_status,
|
| 148 |
+
"title": title,
|
| 149 |
+
"web_rid": web_rid,
|
| 150 |
+
"sec_uid": sec_uid,
|
| 151 |
+
"nickname": nickname,
|
| 152 |
+
# "web_stream_url": web_stream_url,
|
| 153 |
+
# "h265_stream_data": h265_stream_data,
|
| 154 |
+
# "h264_stream_data": h264_stream_data,
|
| 155 |
+
"stream_data": h264_stream_data,
|
| 156 |
+
# "camera_store": camera_store,
|
| 157 |
+
}
|
| 158 |
+
break
|
| 159 |
+
return result
|
| 160 |
+
|
| 161 |
+
@async_cache_decorator(60)
|
| 162 |
+
async def get_live_info_by_follow(self, room_id: str):
|
| 163 |
+
result = None
|
| 164 |
+
live_info_list: List[dict] = await self.get_living_list()
|
| 165 |
+
|
| 166 |
+
if live_info_list is None:
|
| 167 |
+
return None
|
| 168 |
+
for live_info in live_info_list:
|
| 169 |
+
room_id_ = live_info["room_id"]
|
| 170 |
+
if room_id_ == room_id:
|
| 171 |
+
result = live_info
|
| 172 |
+
break
|
| 173 |
+
return result
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def get_args():
|
| 177 |
+
parser = argparse.ArgumentParser()
|
| 178 |
+
parser.add_argument(
|
| 179 |
+
"--credentials_file",
|
| 180 |
+
default=(project_path / "dotenv/douyin_login_credentials.json").as_posix(),
|
| 181 |
+
type=str
|
| 182 |
+
)
|
| 183 |
+
args = parser.parse_args()
|
| 184 |
+
return args
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
async def main():
|
| 188 |
+
import log
|
| 189 |
+
from project_settings import project_path, log_directory
|
| 190 |
+
|
| 191 |
+
log.setup_size_rotating(log_directory=log_directory)
|
| 192 |
+
|
| 193 |
+
args = get_args()
|
| 194 |
+
|
| 195 |
+
client = LiveRecording()
|
| 196 |
+
|
| 197 |
+
flag = client.check_login()
|
| 198 |
+
print(f"flag: {flag}")
|
| 199 |
+
client.login_with_credentials_file(args.credentials_file)
|
| 200 |
+
# client.login_with_qrcode_url()
|
| 201 |
+
flag = client.check_login()
|
| 202 |
+
print(f"flag: {flag}")
|
| 203 |
+
|
| 204 |
+
# js = await client.get_live_info_by_web_enter(room_id="770758107267")
|
| 205 |
+
js = await client.get_live_info_by_room_url(room_id="572033528289")
|
| 206 |
+
# js = await client.get_live_info_by_follow(room_id="572033528289")
|
| 207 |
+
# js = await client.get_live_info_by_follow(room_id="572033528289")
|
| 208 |
+
print(f"js: {json.dumps(js, ensure_ascii=False, indent=4)}")
|
| 209 |
+
return
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
if __name__ == "__main__":
|
| 213 |
+
asyncio.run(main())
|
toolbox/douyin/video/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
pass
|
toolbox/douyin/video/download.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import argparse
|
| 4 |
+
import asyncio
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
| 11 |
+
|
| 12 |
+
from project_settings import project_path
|
| 13 |
+
from toolbox.douyin.douyin_client import DouyinClient
|
| 14 |
+
from toolbox.exception import ExpectedError
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger("toolbox")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class VideoDownload(DouyinClient):
|
| 20 |
+
def __init__(self):
|
| 21 |
+
super().__init__()
|
| 22 |
+
|
| 23 |
+
@retry(
|
| 24 |
+
wait=wait_fixed(10),
|
| 25 |
+
stop=stop_after_attempt(3),
|
| 26 |
+
before_sleep=before_sleep_log(logger, logging.ERROR),
|
| 27 |
+
)
|
| 28 |
+
async def download_video_by_url(self, filename: Path, url: str):
|
| 29 |
+
filename = Path(filename)
|
| 30 |
+
filename.parent.mkdir(parents=True, exist_ok=True)
|
| 31 |
+
|
| 32 |
+
response = await self.async_session.request(
|
| 33 |
+
method="GET",
|
| 34 |
+
url=url,
|
| 35 |
+
headers={
|
| 36 |
+
**self.headers,
|
| 37 |
+
"referer": "https://www.douyin.com/",
|
| 38 |
+
},
|
| 39 |
+
)
|
| 40 |
+
# 302 重定向
|
| 41 |
+
if response.status_code == 302:
|
| 42 |
+
url = response.headers["Location"]
|
| 43 |
+
return await self.download_video_by_url(filename, url)
|
| 44 |
+
elif response.status_code == 200:
|
| 45 |
+
with open(filename, "wb") as f:
|
| 46 |
+
f.write(response.content)
|
| 47 |
+
return filename
|
| 48 |
+
else:
|
| 49 |
+
raise AssertionError(f"Got status code {response.status_code}")
|
| 50 |
+
|
| 51 |
+
# @retry(
|
| 52 |
+
# wait=wait_fixed(10),
|
| 53 |
+
# stop=stop_after_attempt(3),
|
| 54 |
+
# before_sleep=before_sleep_log(logger, logging.ERROR),
|
| 55 |
+
# )
|
| 56 |
+
async def get_video_list_by_user_id(self, sec_user_id: str, max_cursor: int = 0, count: int = 18):
|
| 57 |
+
url = "https://www.douyin.com/aweme/v1/web/aweme/post/"
|
| 58 |
+
|
| 59 |
+
params = {
|
| 60 |
+
"device_platform": "webapp",
|
| 61 |
+
"aid": "6383",
|
| 62 |
+
"channel": "channel_pc_web",
|
| 63 |
+
"sec_user_id": sec_user_id,
|
| 64 |
+
"max_cursor": max_cursor,
|
| 65 |
+
"count": count,
|
| 66 |
+
"publish_video_strategy_type": "2",
|
| 67 |
+
"version_code": "290100",
|
| 68 |
+
"version_name": "29.1.0",
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
response = await self.async_session.request(
|
| 72 |
+
method="GET",
|
| 73 |
+
url=url,
|
| 74 |
+
headers={
|
| 75 |
+
**self.headers,
|
| 76 |
+
"referer": "https://www.douyin.com/",
|
| 77 |
+
},
|
| 78 |
+
params=params,
|
| 79 |
+
)
|
| 80 |
+
if response.status_code == 444:
|
| 81 |
+
# Access Denied
|
| 82 |
+
raise ExpectedError(status_code=60444, message=f"request failed, status_code: {response.status_code}, text: {response.text}")
|
| 83 |
+
elif response.status_code == 200 and len(response.text) == 0:
|
| 84 |
+
# Maybe Access Denied
|
| 85 |
+
raise ExpectedError(status_code=60444, message=f"request failed, status_code: {response.status_code}, text: {response.text}")
|
| 86 |
+
elif response.status_code != 200:
|
| 87 |
+
raise AssertionError(f"request failed, status_code: {response.status_code}, text: {response.text}")
|
| 88 |
+
elif response.text == "blocked":
|
| 89 |
+
raise AssertionError(f"request failed, status_code: {response.status_code}, text: {response.text}")
|
| 90 |
+
js = response.json()
|
| 91 |
+
aweme_list = js["aweme_list"]
|
| 92 |
+
|
| 93 |
+
result = list()
|
| 94 |
+
for aweme in aweme_list:
|
| 95 |
+
# aweme_ = json.dumps(aweme, ensure_ascii=False, indent=4)
|
| 96 |
+
# print(aweme_)
|
| 97 |
+
|
| 98 |
+
aweme_id = aweme["aweme_id"]
|
| 99 |
+
desc = aweme["desc"]
|
| 100 |
+
create_time = aweme["create_time"]
|
| 101 |
+
create_time_ = datetime.fromtimestamp(create_time)
|
| 102 |
+
create_time_str = create_time_.strftime("%Y%m%dT%H%M%S")
|
| 103 |
+
|
| 104 |
+
# video
|
| 105 |
+
video = aweme["video"]
|
| 106 |
+
video_url_list = video["play_addr"]["url_list"]
|
| 107 |
+
|
| 108 |
+
# tags
|
| 109 |
+
text_extra = aweme["text_extra"]
|
| 110 |
+
tags = set()
|
| 111 |
+
for t in text_extra:
|
| 112 |
+
tag = t.get("hashtag_name")
|
| 113 |
+
if tag is None:
|
| 114 |
+
tag = t.get("search_text")
|
| 115 |
+
if tag is None:
|
| 116 |
+
# print(t)
|
| 117 |
+
continue
|
| 118 |
+
tags.add(tag)
|
| 119 |
+
tags = list(tags)
|
| 120 |
+
|
| 121 |
+
# title
|
| 122 |
+
title: str = desc
|
| 123 |
+
for tag in tags:
|
| 124 |
+
title = title.replace(f"#{tag}", "")
|
| 125 |
+
# title = title.replace(f"# {tag}", "")
|
| 126 |
+
title = title.strip()
|
| 127 |
+
|
| 128 |
+
row = {
|
| 129 |
+
"aweme_id": aweme_id,
|
| 130 |
+
"create_time": create_time,
|
| 131 |
+
"create_time_str": create_time_str,
|
| 132 |
+
"title": title,
|
| 133 |
+
"desc": desc,
|
| 134 |
+
"video_url_list": video_url_list,
|
| 135 |
+
"tags": tags,
|
| 136 |
+
}
|
| 137 |
+
result.append(row)
|
| 138 |
+
return result
|
| 139 |
+
|
| 140 |
+
async def get_video_list_by_min_date(self, sec_user_id: str, min_date: str = "2025-06-10 00:00:00"):
|
| 141 |
+
min_date_ = datetime.strptime(min_date, "%Y-%m-%d %H:%M:%S")
|
| 142 |
+
|
| 143 |
+
result = list()
|
| 144 |
+
|
| 145 |
+
stop_flag = False
|
| 146 |
+
max_cursor = 0
|
| 147 |
+
for i in range(100):
|
| 148 |
+
if stop_flag:
|
| 149 |
+
break
|
| 150 |
+
rows = await self.get_video_list_by_user_id(sec_user_id=sec_user_id, max_cursor=max_cursor, count=18)
|
| 151 |
+
this_min_date_ = [datetime.fromtimestamp(row["create_time"]) < min_date_ for row in rows]
|
| 152 |
+
if all(this_min_date_):
|
| 153 |
+
break
|
| 154 |
+
for row in rows:
|
| 155 |
+
create_time = row["create_time"]
|
| 156 |
+
aweme_id = row["aweme_id"]
|
| 157 |
+
create_time_str = row["create_time_str"]
|
| 158 |
+
title = row["title"]
|
| 159 |
+
desc = row["desc"]
|
| 160 |
+
video_url_list = row["video_url_list"]
|
| 161 |
+
tags = row["tags"]
|
| 162 |
+
|
| 163 |
+
max_cursor_ = int(create_time * 1000)
|
| 164 |
+
if max_cursor == 0 or max_cursor_ < max_cursor:
|
| 165 |
+
max_cursor = max_cursor_
|
| 166 |
+
|
| 167 |
+
create_time_ = datetime.fromtimestamp(create_time)
|
| 168 |
+
|
| 169 |
+
if create_time_ > min_date_:
|
| 170 |
+
task = {
|
| 171 |
+
"aweme_id": aweme_id,
|
| 172 |
+
"create_time_str": create_time_str,
|
| 173 |
+
"title": title,
|
| 174 |
+
"desc": desc,
|
| 175 |
+
"video_url_list": video_url_list,
|
| 176 |
+
"tags": tags,
|
| 177 |
+
}
|
| 178 |
+
result.append(task)
|
| 179 |
+
return result
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def get_args():
|
| 183 |
+
parser = argparse.ArgumentParser()
|
| 184 |
+
parser.add_argument(
|
| 185 |
+
"--credentials_file",
|
| 186 |
+
default=(project_path / "dotenv/douyin_wentao_credentials.json").as_posix(),
|
| 187 |
+
type=str
|
| 188 |
+
)
|
| 189 |
+
args = parser.parse_args()
|
| 190 |
+
return args
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
async def main():
|
| 194 |
+
import log
|
| 195 |
+
from project_settings import project_path, log_directory
|
| 196 |
+
|
| 197 |
+
log.setup_size_rotating(log_directory=log_directory)
|
| 198 |
+
|
| 199 |
+
args = get_args()
|
| 200 |
+
|
| 201 |
+
client = VideoDownload()
|
| 202 |
+
|
| 203 |
+
flag = client.check_login()
|
| 204 |
+
print(f"flag: {flag}")
|
| 205 |
+
client.login_with_credentials_file(args.credentials_file)
|
| 206 |
+
# client.login_with_qrcode_url()
|
| 207 |
+
flag = client.check_login()
|
| 208 |
+
print(f"flag: {flag}")
|
| 209 |
+
|
| 210 |
+
js = await client.get_video_list_by_min_date(
|
| 211 |
+
# sec_user_id="MS4wLjABAAAAb0mqEsXmBehDdg2Q9mMA2T6YEWPGbEtYofSzX_bDnz4",
|
| 212 |
+
sec_user_id="MS4wLjABAAAAQinRMLyQNYA45OYXoCDrwszhRGaDVirRE1fTNSaGGkc",
|
| 213 |
+
min_date="2025-08-06 00:00:00"
|
| 214 |
+
)
|
| 215 |
+
print(f"js: {json.dumps(js, ensure_ascii=False, indent=4)}")
|
| 216 |
+
return
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
if __name__ == "__main__":
|
| 220 |
+
asyncio.run(main())
|
toolbox/exception.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
class ExpectedError(Exception):
|
| 4 |
+
def __init__(self, status_code, message, traceback="", detail=""):
|
| 5 |
+
self.status_code = status_code
|
| 6 |
+
self.message = message
|
| 7 |
+
self.traceback = traceback
|
| 8 |
+
self.detail = detail
|
toolbox/json/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == '__main__':
|
| 6 |
+
pass
|
toolbox/json/misc.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
from typing import Callable
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def traverse(js, callback: Callable, *args, **kwargs):
|
| 7 |
+
if isinstance(js, list):
|
| 8 |
+
result = list()
|
| 9 |
+
for l in js:
|
| 10 |
+
l = traverse(l, callback, *args, **kwargs)
|
| 11 |
+
result.append(l)
|
| 12 |
+
return result
|
| 13 |
+
elif isinstance(js, tuple):
|
| 14 |
+
result = list()
|
| 15 |
+
for l in js:
|
| 16 |
+
l = traverse(l, callback, *args, **kwargs)
|
| 17 |
+
result.append(l)
|
| 18 |
+
return tuple(result)
|
| 19 |
+
elif isinstance(js, dict):
|
| 20 |
+
result = dict()
|
| 21 |
+
for k, v in js.items():
|
| 22 |
+
k = traverse(k, callback, *args, **kwargs)
|
| 23 |
+
v = traverse(v, callback, *args, **kwargs)
|
| 24 |
+
result[k] = v
|
| 25 |
+
return result
|
| 26 |
+
elif isinstance(js, int):
|
| 27 |
+
return callback(js, *args, **kwargs)
|
| 28 |
+
elif isinstance(js, str):
|
| 29 |
+
return callback(js, *args, **kwargs)
|
| 30 |
+
else:
|
| 31 |
+
return js
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def demo1():
|
| 35 |
+
d = {
|
| 36 |
+
"env": "ppe",
|
| 37 |
+
"mysql_connect": {
|
| 38 |
+
"host": "$mysql_connect_host",
|
| 39 |
+
"port": 3306,
|
| 40 |
+
"user": "callbot",
|
| 41 |
+
"password": "NxcloudAI2021!",
|
| 42 |
+
"database": "callbot_ppe",
|
| 43 |
+
"charset": "utf8"
|
| 44 |
+
},
|
| 45 |
+
"es_connect": {
|
| 46 |
+
"hosts": ["10.20.251.8"],
|
| 47 |
+
"http_auth": ["elastic", "ElasticAI2021!"],
|
| 48 |
+
"port": 9200
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
def callback(s):
|
| 53 |
+
if isinstance(s, str) and s.startswith('$'):
|
| 54 |
+
return s[1:]
|
| 55 |
+
return s
|
| 56 |
+
|
| 57 |
+
result = traverse(d, callback=callback)
|
| 58 |
+
print(result)
|
| 59 |
+
return
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if __name__ == '__main__':
|
| 63 |
+
demo1()
|
toolbox/os/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == '__main__':
|
| 6 |
+
pass
|
toolbox/os/command.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Command(object):
|
| 7 |
+
custom_command = [
|
| 8 |
+
"cd"
|
| 9 |
+
]
|
| 10 |
+
|
| 11 |
+
@staticmethod
|
| 12 |
+
def _get_cmd(command):
|
| 13 |
+
command = str(command).strip()
|
| 14 |
+
if command == "":
|
| 15 |
+
return None
|
| 16 |
+
cmd_and_args = command.split(sep=" ")
|
| 17 |
+
cmd = cmd_and_args[0]
|
| 18 |
+
args = " ".join(cmd_and_args[1:])
|
| 19 |
+
return cmd, args
|
| 20 |
+
|
| 21 |
+
@classmethod
|
| 22 |
+
def popen(cls, command):
|
| 23 |
+
cmd, args = cls._get_cmd(command)
|
| 24 |
+
if cmd in cls.custom_command:
|
| 25 |
+
method = getattr(cls, cmd)
|
| 26 |
+
return method(args)
|
| 27 |
+
else:
|
| 28 |
+
resp = os.popen(command)
|
| 29 |
+
result = resp.read()
|
| 30 |
+
resp.close()
|
| 31 |
+
return result
|
| 32 |
+
|
| 33 |
+
@classmethod
|
| 34 |
+
def cd(cls, args):
|
| 35 |
+
if args.startswith("/"):
|
| 36 |
+
os.chdir(args)
|
| 37 |
+
else:
|
| 38 |
+
pwd = os.getcwd()
|
| 39 |
+
path = os.path.join(pwd, args)
|
| 40 |
+
os.chdir(path)
|
| 41 |
+
|
| 42 |
+
@classmethod
|
| 43 |
+
def system(cls, command):
|
| 44 |
+
return os.system(command)
|
| 45 |
+
|
| 46 |
+
def __init__(self):
|
| 47 |
+
pass
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def ps_ef_grep(keyword: str):
|
| 51 |
+
cmd = "ps -ef | grep {}".format(keyword)
|
| 52 |
+
rows = Command.popen(cmd)
|
| 53 |
+
rows = str(rows).split("\n")
|
| 54 |
+
rows = [row for row in rows if row.__contains__(keyword) and not row.__contains__("grep")]
|
| 55 |
+
return rows
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
pass
|
toolbox/os/environment.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
from dotenv.main import DotEnv
|
| 8 |
+
|
| 9 |
+
from toolbox.json.misc import traverse
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class EnvironmentManager(object):
|
| 13 |
+
def __init__(self, path, env, override=False):
|
| 14 |
+
filename = os.path.join(path, '{}.env'.format(env))
|
| 15 |
+
self.filename = filename
|
| 16 |
+
|
| 17 |
+
load_dotenv(
|
| 18 |
+
dotenv_path=filename,
|
| 19 |
+
override=override
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
self._environ = dict()
|
| 23 |
+
|
| 24 |
+
def open_dotenv(self, filename: str = None):
|
| 25 |
+
filename = filename or self.filename
|
| 26 |
+
dotenv = DotEnv(
|
| 27 |
+
dotenv_path=filename,
|
| 28 |
+
stream=None,
|
| 29 |
+
verbose=False,
|
| 30 |
+
interpolate=False,
|
| 31 |
+
override=False,
|
| 32 |
+
encoding="utf-8",
|
| 33 |
+
)
|
| 34 |
+
result = dotenv.dict()
|
| 35 |
+
return result
|
| 36 |
+
|
| 37 |
+
def get(self, key, default=None, dtype=str):
|
| 38 |
+
result = os.environ.get(key)
|
| 39 |
+
if result is None:
|
| 40 |
+
if default is None:
|
| 41 |
+
result = None
|
| 42 |
+
else:
|
| 43 |
+
result = default
|
| 44 |
+
else:
|
| 45 |
+
result = dtype(result)
|
| 46 |
+
self._environ[key] = result
|
| 47 |
+
return result
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
_DEFAULT_DTYPE_MAP = {
|
| 51 |
+
'int': int,
|
| 52 |
+
'float': float,
|
| 53 |
+
'str': str,
|
| 54 |
+
'json.loads': json.loads
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class JsonConfig(object):
|
| 59 |
+
"""
|
| 60 |
+
将 json 中, 形如 `$float:threshold` 的值, 处理为:
|
| 61 |
+
从环境变量中查到 threshold, 再将其转换为 float 类型.
|
| 62 |
+
"""
|
| 63 |
+
def __init__(self, dtype_map: dict = None, environment: EnvironmentManager = None):
|
| 64 |
+
self.dtype_map = dtype_map or _DEFAULT_DTYPE_MAP
|
| 65 |
+
self.environment = environment or os.environ
|
| 66 |
+
|
| 67 |
+
def sanitize_by_filename(self, filename: str):
|
| 68 |
+
with open(filename, 'r', encoding='utf-8') as f:
|
| 69 |
+
js = json.load(f)
|
| 70 |
+
|
| 71 |
+
return self.sanitize_by_json(js)
|
| 72 |
+
|
| 73 |
+
def sanitize_by_json(self, js):
|
| 74 |
+
js = traverse(
|
| 75 |
+
js,
|
| 76 |
+
callback=self.sanitize,
|
| 77 |
+
environment=self.environment
|
| 78 |
+
)
|
| 79 |
+
return js
|
| 80 |
+
|
| 81 |
+
def sanitize(self, string, environment):
|
| 82 |
+
"""支持 $ 符开始的, 环境变量配置"""
|
| 83 |
+
if isinstance(string, str) and string.startswith('$'):
|
| 84 |
+
dtype, key = string[1:].split(':')
|
| 85 |
+
dtype = self.dtype_map[dtype]
|
| 86 |
+
|
| 87 |
+
value = environment.get(key)
|
| 88 |
+
if value is None:
|
| 89 |
+
raise AssertionError('environment not exist. key: {}'.format(key))
|
| 90 |
+
|
| 91 |
+
value = dtype(value)
|
| 92 |
+
result = value
|
| 93 |
+
else:
|
| 94 |
+
result = string
|
| 95 |
+
return result
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def demo1():
|
| 99 |
+
import json
|
| 100 |
+
|
| 101 |
+
from project_settings import project_path
|
| 102 |
+
|
| 103 |
+
environment = EnvironmentManager(
|
| 104 |
+
path=os.path.join(project_path, 'server/callbot_server/dotenv'),
|
| 105 |
+
env='dev',
|
| 106 |
+
)
|
| 107 |
+
init_scenes = environment.get(key='init_scenes', dtype=json.loads)
|
| 108 |
+
print(init_scenes)
|
| 109 |
+
print(environment._environ)
|
| 110 |
+
return
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
if __name__ == '__main__':
|
| 114 |
+
demo1()
|
toolbox/os/other.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import inspect
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def pwd():
|
| 6 |
+
"""你在哪个文件调用此函数, 它就会返回那个文件所在的 dir 目标"""
|
| 7 |
+
frame = inspect.stack()[1]
|
| 8 |
+
module = inspect.getmodule(frame[0])
|
| 9 |
+
return os.path.dirname(os.path.abspath(module.__file__))
|
toolbox/porter/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
pass
|
toolbox/porter/common/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == '__main__':
|
| 6 |
+
pass
|
toolbox/porter/common/params.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import logging
|
| 4 |
+
import inspect
|
| 5 |
+
import typing
|
| 6 |
+
|
| 7 |
+
from toolbox.porter.common.registrable import Registrable
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger("toolbox")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Params(Registrable):
|
| 13 |
+
"""
|
| 14 |
+
仿照 AllenNLP 框架, Params 与 Registrable 配合使用, 通过子类的 Annotation 类型标注使得子类可以通过
|
| 15 |
+
|
| 16 |
+
.from_json({
|
| 17 |
+
**parameters
|
| 18 |
+
})
|
| 19 |
+
|
| 20 |
+
方式实例化.
|
| 21 |
+
|
| 22 |
+
注意事项:
|
| 23 |
+
1. 子类除了 self, 每个参数都必须有类型标注.
|
| 24 |
+
2. 当子类没有 __init__ 方法时, 会调用到基类的该方法 (基类的默认实现没有类型标注),
|
| 25 |
+
因此, 都应实现 __init__ 方法.
|
| 26 |
+
|
| 27 |
+
"""
|
| 28 |
+
# def __init__(self):
|
| 29 |
+
# # Subclasses should override this method, even if it is def __init__(self): pass
|
| 30 |
+
# pass
|
| 31 |
+
|
| 32 |
+
@classmethod
|
| 33 |
+
def from_json(cls, params: dict = None, global_params: dict = None):
|
| 34 |
+
"""
|
| 35 |
+
:param params:
|
| 36 |
+
:param global_params: 当缺少某参数时, 尝试从 global_params 中查找.
|
| 37 |
+
:return:
|
| 38 |
+
"""
|
| 39 |
+
if params is None:
|
| 40 |
+
params = dict()
|
| 41 |
+
if global_params is None:
|
| 42 |
+
global_params = dict()
|
| 43 |
+
|
| 44 |
+
if "type" in params:
|
| 45 |
+
cls = cls.by_name(params["type"])
|
| 46 |
+
|
| 47 |
+
signature = inspect.signature(cls.__init__)
|
| 48 |
+
|
| 49 |
+
kwargs = dict()
|
| 50 |
+
for k, v in signature.parameters.items():
|
| 51 |
+
if k in ("self",):
|
| 52 |
+
continue
|
| 53 |
+
if k in ("args", "kwargs"):
|
| 54 |
+
msg = (
|
| 55 |
+
f"parameter: args or kwargs is not expected. "
|
| 56 |
+
f"you may need to override the __init__ method of cls: {cls.__name__}."
|
| 57 |
+
)
|
| 58 |
+
logger.warning(msg)
|
| 59 |
+
print(msg)
|
| 60 |
+
continue
|
| 61 |
+
|
| 62 |
+
if v.annotation is inspect._empty:
|
| 63 |
+
raise NotImplementedError(
|
| 64 |
+
"all parameter should have a annotation. "
|
| 65 |
+
"parameter `{}` of {} have not annotation".format(k, cls)
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
if v.name in params:
|
| 69 |
+
sub_params = params[v.name]
|
| 70 |
+
elif v.name in global_params:
|
| 71 |
+
sub_params = global_params[v.name]
|
| 72 |
+
else:
|
| 73 |
+
continue
|
| 74 |
+
|
| 75 |
+
if isinstance(v.annotation, str):
|
| 76 |
+
raise NotImplementedError("string annotation not supported.")
|
| 77 |
+
|
| 78 |
+
if hasattr(v.annotation, "_subs_tree"):
|
| 79 |
+
# typing 标注类型.
|
| 80 |
+
subs_tree = v.annotation._subs_tree()
|
| 81 |
+
kwargs[v.name] = cls.from_annotation(sub_params, global_params, subs_tree)
|
| 82 |
+
elif isinstance(v.annotation, typing._GenericAlias):
|
| 83 |
+
# typing 标注类型.
|
| 84 |
+
subs_tree = (v.annotation.__origin__, *v.annotation.__args__)
|
| 85 |
+
kwargs[v.name] = cls.from_annotation(sub_params, global_params, subs_tree)
|
| 86 |
+
elif issubclass(v.annotation, Params):
|
| 87 |
+
# Params 子类.
|
| 88 |
+
kwargs[v.name] = v.annotation.from_json(
|
| 89 |
+
sub_params, global_params
|
| 90 |
+
)
|
| 91 |
+
elif isinstance(sub_params, v.annotation):
|
| 92 |
+
# 传入的是已实例化好的值.
|
| 93 |
+
kwargs[v.name] = sub_params
|
| 94 |
+
else:
|
| 95 |
+
# str, int, list, dict 等基本类型.
|
| 96 |
+
value = sub_params
|
| 97 |
+
if isinstance(value, dict):
|
| 98 |
+
value = v.annotation(**value)
|
| 99 |
+
else:
|
| 100 |
+
value = v.annotation(value)
|
| 101 |
+
|
| 102 |
+
kwargs[v.name] = value
|
| 103 |
+
|
| 104 |
+
obj = cls.__new__(cls, **kwargs)
|
| 105 |
+
try:
|
| 106 |
+
obj.__init__(**kwargs)
|
| 107 |
+
except TypeError as e:
|
| 108 |
+
print(e)
|
| 109 |
+
print("cls: {}, obj: {}, kwargs: {}".format(cls, obj, kwargs))
|
| 110 |
+
logger.error(e)
|
| 111 |
+
logger.error("cls: {}, obj: {}, kwargs: {}".format(cls, obj, kwargs))
|
| 112 |
+
raise e
|
| 113 |
+
|
| 114 |
+
return obj
|
| 115 |
+
|
| 116 |
+
@classmethod
|
| 117 |
+
def from_annotation(cls, params, global_params: dict, subs_tree=None):
|
| 118 |
+
if params is None:
|
| 119 |
+
return params
|
| 120 |
+
if subs_tree is None:
|
| 121 |
+
return params
|
| 122 |
+
|
| 123 |
+
if isinstance(subs_tree, tuple) and len(subs_tree) > 1:
|
| 124 |
+
# such as: (Dict, str, int) in List[Dict[str, int]]
|
| 125 |
+
args_type = subs_tree[0]
|
| 126 |
+
annotation = subs_tree[1:]
|
| 127 |
+
elif isinstance(subs_tree, tuple) and len(subs_tree) == 1:
|
| 128 |
+
args_type = subs_tree[0]
|
| 129 |
+
annotation = None
|
| 130 |
+
else:
|
| 131 |
+
args_type = subs_tree
|
| 132 |
+
annotation = None
|
| 133 |
+
|
| 134 |
+
if args_type is typing.List or args_type is list:
|
| 135 |
+
result = list()
|
| 136 |
+
for param in params:
|
| 137 |
+
result.append(cls.from_annotation(param, global_params, annotation))
|
| 138 |
+
return result
|
| 139 |
+
elif args_type is typing.Dict or args_type is list:
|
| 140 |
+
result = dict()
|
| 141 |
+
for k, v in params.items():
|
| 142 |
+
key = cls.from_annotation(k, global_params, annotation[0])
|
| 143 |
+
value = cls.from_annotation(v, global_params, annotation[1])
|
| 144 |
+
result[key] = value
|
| 145 |
+
return result
|
| 146 |
+
elif args_type is typing.Tuple or args_type is tuple:
|
| 147 |
+
if len(annotation) != len(params):
|
| 148 |
+
raise AssertionError(
|
| 149 |
+
"number of params not match the annotation. "
|
| 150 |
+
"{}, annotation: {}, params: {}".format(cls, annotation, params)
|
| 151 |
+
)
|
| 152 |
+
result = list()
|
| 153 |
+
for param, sub_annotation in zip(params, annotation):
|
| 154 |
+
result.append(cls.from_annotation(param, global_params, sub_annotation))
|
| 155 |
+
return tuple(result)
|
| 156 |
+
elif args_type is typing.Union:
|
| 157 |
+
for option in annotation:
|
| 158 |
+
try:
|
| 159 |
+
result = cls.from_annotation(params, global_params, option)
|
| 160 |
+
break
|
| 161 |
+
except Exception:
|
| 162 |
+
continue
|
| 163 |
+
else:
|
| 164 |
+
raise ValueError("no type of Union match the params {}".format(params))
|
| 165 |
+
return result
|
| 166 |
+
elif args_type is typing.Any:
|
| 167 |
+
result = params
|
| 168 |
+
return result
|
| 169 |
+
|
| 170 |
+
if hasattr(typing, "GenericMeta"):
|
| 171 |
+
built_in_type = typing.GenericMeta
|
| 172 |
+
elif hasattr(typing, "GenericAlias"):
|
| 173 |
+
built_in_type = typing.GenericAlias
|
| 174 |
+
else:
|
| 175 |
+
raise NotImplementedError
|
| 176 |
+
|
| 177 |
+
if not isinstance(args_type, built_in_type):
|
| 178 |
+
if hasattr(args_type, "from_json"):
|
| 179 |
+
result = args_type.from_json(params, global_params)
|
| 180 |
+
elif isinstance(args_type, tuple) and len(args_type) > 0 and isinstance(args_type[0], built_in_type):
|
| 181 |
+
# List[Dict[str, List[str]]]
|
| 182 |
+
result = cls.from_annotation(params, global_params, args_type)
|
| 183 |
+
else:
|
| 184 |
+
if isinstance(params, dict):
|
| 185 |
+
result = args_type(**params)
|
| 186 |
+
else:
|
| 187 |
+
result = args_type(params)
|
| 188 |
+
|
| 189 |
+
return result
|
| 190 |
+
|
| 191 |
+
raise NotImplementedError(
|
| 192 |
+
"{}, params: {}, subs_tree: {}".format(cls, params, subs_tree)
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
if __name__ == "__main__":
|
| 197 |
+
pass
|
toolbox/porter/common/registrable.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections import defaultdict
|
| 2 |
+
from typing import TypeVar, Type, Dict, List
|
| 3 |
+
import importlib
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger("toolbox")
|
| 7 |
+
|
| 8 |
+
T = TypeVar("T")
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Registrable(object):
|
| 12 |
+
_registry: Dict[Type, Dict[str, Type]] = defaultdict(dict)
|
| 13 |
+
default_implementation: str = None
|
| 14 |
+
register_name: str = "unknown"
|
| 15 |
+
|
| 16 |
+
@classmethod
|
| 17 |
+
def register(cls: Type[T], name: str, exist_ok=False):
|
| 18 |
+
registry = Registrable._registry[cls]
|
| 19 |
+
def add_subclass_to_registry(subclass: Type[T]):
|
| 20 |
+
# set a name on the subclass
|
| 21 |
+
setattr(subclass, "register_name", name)
|
| 22 |
+
if name in registry:
|
| 23 |
+
if exist_ok:
|
| 24 |
+
message = (f"{name} has already been registered as {registry[name].__name__}, but "
|
| 25 |
+
f"exist_ok=True, so overwriting with {cls.__name__}")
|
| 26 |
+
# logger.info(message)
|
| 27 |
+
else:
|
| 28 |
+
message = (f"Cannot register {name} as {cls.__name__}; "
|
| 29 |
+
f"name already in use for {registry[name].__name__}")
|
| 30 |
+
raise ValueError(message)
|
| 31 |
+
registry[name] = subclass
|
| 32 |
+
return subclass
|
| 33 |
+
return add_subclass_to_registry
|
| 34 |
+
|
| 35 |
+
@classmethod
|
| 36 |
+
def by_name(cls: Type[T], name: str) -> Type[T]:
|
| 37 |
+
# logger.info(f"instantiating registered subclass {name} of {cls}")
|
| 38 |
+
if name in Registrable._registry[cls]:
|
| 39 |
+
return Registrable._registry[cls].get(name)
|
| 40 |
+
else:
|
| 41 |
+
raise ValueError(
|
| 42 |
+
f"{name} is not a registered name for {cls.__name__}. "
|
| 43 |
+
f"the available is: [{Registrable._registry[cls].keys()}]"
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@classmethod
|
| 48 |
+
def list_available(cls) -> List[str]:
|
| 49 |
+
keys = list(Registrable._registry[cls].keys())
|
| 50 |
+
default = cls.default_implementation
|
| 51 |
+
|
| 52 |
+
if default is None:
|
| 53 |
+
return keys
|
| 54 |
+
elif default not in keys:
|
| 55 |
+
message = "Default implementation %s is not registered" % default
|
| 56 |
+
raise ValueError(message)
|
| 57 |
+
else:
|
| 58 |
+
return [default] + [k for k in keys if k != default]
|
toolbox/porter/manager.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import asyncio
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
from toolbox.porter.tasks.base_task import BaseTask
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class PorterManager(object):
|
| 10 |
+
def __init__(self,
|
| 11 |
+
tasks_file: str,
|
| 12 |
+
):
|
| 13 |
+
self.tasks_file = tasks_file
|
| 14 |
+
|
| 15 |
+
# state
|
| 16 |
+
self.coro_task_set = set()
|
| 17 |
+
|
| 18 |
+
def get_init_tasks(self):
|
| 19 |
+
with open(self.tasks_file, "r", encoding="utf-8") as f:
|
| 20 |
+
tasks = json.load(f)
|
| 21 |
+
|
| 22 |
+
for task in tasks:
|
| 23 |
+
enable = task.pop("enable")
|
| 24 |
+
task_type = task.pop("type")
|
| 25 |
+
|
| 26 |
+
if not enable:
|
| 27 |
+
continue
|
| 28 |
+
task_cls = BaseTask.by_name(task_type)
|
| 29 |
+
task_obj = task_cls(**task)
|
| 30 |
+
|
| 31 |
+
self.coro_task_set.add(task_obj.start())
|
| 32 |
+
return self.coro_task_set
|
| 33 |
+
|
| 34 |
+
async def run(self):
|
| 35 |
+
coro_task_set = self.get_init_tasks()
|
| 36 |
+
|
| 37 |
+
future_tasks = list()
|
| 38 |
+
for task in coro_task_set:
|
| 39 |
+
task = asyncio.ensure_future(task)
|
| 40 |
+
# task = asyncio.create_task(task)
|
| 41 |
+
future_tasks.append(task)
|
| 42 |
+
await asyncio.sleep(3)
|
| 43 |
+
|
| 44 |
+
await asyncio.wait(future_tasks)
|
| 45 |
+
|
| 46 |
+
async def run2(self):
|
| 47 |
+
future_tasks = self.get_init_tasks()
|
| 48 |
+
await asyncio.wait(future_tasks)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
async def main():
|
| 52 |
+
import log
|
| 53 |
+
from project_settings import environment, project_path, log_directory, time_zone_info
|
| 54 |
+
|
| 55 |
+
log.setup_size_rotating(log_directory=log_directory, tz_info=time_zone_info)
|
| 56 |
+
|
| 57 |
+
porter_task_file = environment.get("porter_tasks_file")
|
| 58 |
+
|
| 59 |
+
youtube_video_upload_tasks_file = project_path / porter_task_file
|
| 60 |
+
|
| 61 |
+
manager = PorterManager(youtube_video_upload_tasks_file)
|
| 62 |
+
await manager.run()
|
| 63 |
+
return
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
asyncio.run(main())
|
toolbox/porter/tasks/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
from .douyin_live_info_collect_task import DouyinLiveInfoCollectTask
|
| 4 |
+
from .douyin_video_download_task import DouyinVideoDownloadTask
|
| 5 |
+
from .douyin_live_record_task import DouyinLiveRecordTask
|
| 6 |
+
from .douyin_live_to_bilibili_task import DouyinLiveToBilibiliTask
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
if __name__ == "__main__":
|
| 10 |
+
pass
|
toolbox/porter/tasks/base_task.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
import traceback
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger("toolbox")
|
| 8 |
+
|
| 9 |
+
from toolbox.porter.common.params import Params
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class BaseTask(Params):
|
| 13 |
+
def __init__(self,
|
| 14 |
+
flag: str,
|
| 15 |
+
check_interval: int,
|
| 16 |
+
):
|
| 17 |
+
super().__init__()
|
| 18 |
+
self.flag = flag
|
| 19 |
+
self.check_interval = check_interval
|
| 20 |
+
|
| 21 |
+
async def do_task(self):
|
| 22 |
+
raise NotImplementedError
|
| 23 |
+
|
| 24 |
+
async def start(self):
|
| 25 |
+
while True:
|
| 26 |
+
try:
|
| 27 |
+
await self.do_task()
|
| 28 |
+
logger.info(f"{self.flag}任务检测... 刷新间隔 {self.check_interval}s")
|
| 29 |
+
await asyncio.sleep(self.check_interval)
|
| 30 |
+
except Exception as error:
|
| 31 |
+
logger.error(f"{self.flag}任务检测出错\nerror type: {type(error)}, error text: {error}, traceback: {traceback.format_exc()}")
|
| 32 |
+
await asyncio.sleep(self.check_interval)
|
| 33 |
+
continue
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
if __name__ == "__main__":
|
| 37 |
+
pass
|