HoneyTian commited on
Commit
80bf15d
·
1 Parent(s): c934e62
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +5 -0
  2. .gitignore +32 -0
  3. Dockerfile +24 -0
  4. README.md +1 -1
  5. data/douyin_live_info_collect.json +9 -0
  6. data/porter_tasks_dev.json +66 -0
  7. data/porter_tasks_prd.json +66 -0
  8. data/porter_tasks_prd2.json +273 -0
  9. install.sh +64 -0
  10. log.py +252 -0
  11. main.py +135 -0
  12. project_settings.py +27 -0
  13. requirements.txt +20 -0
  14. tabs/fs_tab.py +62 -0
  15. tabs/shell_tab.py +28 -0
  16. tabs/youtube_player_tab.py +195 -0
  17. toolbox/__init__.py +6 -0
  18. toolbox/asyncio/__init__.py +6 -0
  19. toolbox/asyncio/cacheout.py +194 -0
  20. toolbox/bilibili/__init__.py +6 -0
  21. toolbox/bilibili/bilibili_client.py +241 -0
  22. toolbox/bilibili/live/__init__.py +6 -0
  23. toolbox/bilibili/live/live_manager.py +301 -0
  24. toolbox/bilibili/video/__init__.py +5 -0
  25. toolbox/bilibili/video/draft_manager.py +279 -0
  26. toolbox/bilibili/video/video_manager.py +332 -0
  27. toolbox/design_patterns/__init__.py +6 -0
  28. toolbox/design_patterns/singleton.py +124 -0
  29. toolbox/douyin/__init__.py +6 -0
  30. toolbox/douyin/douyin_client.py +218 -0
  31. toolbox/douyin/homepage/__init__.py +6 -0
  32. toolbox/douyin/homepage/follow.py +111 -0
  33. toolbox/douyin/live/__init__.py +6 -0
  34. toolbox/douyin/live/live_recording.py +213 -0
  35. toolbox/douyin/video/__init__.py +6 -0
  36. toolbox/douyin/video/download.py +220 -0
  37. toolbox/exception.py +8 -0
  38. toolbox/json/__init__.py +6 -0
  39. toolbox/json/misc.py +63 -0
  40. toolbox/os/__init__.py +6 -0
  41. toolbox/os/command.py +59 -0
  42. toolbox/os/environment.py +114 -0
  43. toolbox/os/other.py +9 -0
  44. toolbox/porter/__init__.py +6 -0
  45. toolbox/porter/common/__init__.py +6 -0
  46. toolbox/porter/common/params.py +197 -0
  47. toolbox/porter/common/registrable.py +58 -0
  48. toolbox/porter/manager.py +67 -0
  49. toolbox/porter/tasks/__init__.py +10 -0
  50. 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