podflow 2025.1.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Podflow/__init__.py +137 -0
- Podflow/basic/__init__.py +2 -0
- Podflow/basic/file_save.py +21 -0
- Podflow/basic/folder_build.py +16 -0
- Podflow/basic/get_duration.py +18 -0
- Podflow/basic/get_file_list.py +57 -0
- Podflow/basic/get_html_dict.py +30 -0
- Podflow/basic/http_client.py +75 -0
- Podflow/basic/list_merge_tidy.py +15 -0
- Podflow/basic/qr_code.py +55 -0
- Podflow/basic/split_dict.py +14 -0
- Podflow/basic/time_format.py +16 -0
- Podflow/basic/time_stamp.py +61 -0
- Podflow/basic/vary_replace.py +11 -0
- Podflow/basic/write_log.py +43 -0
- Podflow/bilibili/__init__.py +2 -0
- Podflow/bilibili/build.py +201 -0
- Podflow/bilibili/get.py +477 -0
- Podflow/bilibili/login.py +307 -0
- Podflow/config/__init__.py +2 -0
- Podflow/config/build_original.py +41 -0
- Podflow/config/channge_icon.py +163 -0
- Podflow/config/correct_channelid.py +230 -0
- Podflow/config/correct_config.py +103 -0
- Podflow/config/get_channelid.py +22 -0
- Podflow/config/get_channelid_id.py +21 -0
- Podflow/config/get_config.py +34 -0
- Podflow/download/__init__.py +2 -0
- Podflow/download/convert_bytes.py +18 -0
- Podflow/download/delete_part.py +20 -0
- Podflow/download/dl_aideo_video.py +307 -0
- Podflow/download/show_progress.py +46 -0
- Podflow/download/wait_animation.py +34 -0
- Podflow/download/youtube_and_bilibili_download.py +30 -0
- Podflow/ffmpeg_judge.py +45 -0
- Podflow/httpfs/__init__.py +2 -0
- Podflow/httpfs/app_bottle.py +212 -0
- Podflow/httpfs/port_judge.py +21 -0
- Podflow/main.py +248 -0
- Podflow/makeup/__init__.py +2 -0
- Podflow/makeup/del_makeup_yt_format_fail.py +19 -0
- Podflow/makeup/make_up_file.py +51 -0
- Podflow/makeup/make_up_file_format_mod.py +96 -0
- Podflow/makeup/make_up_file_mod.py +30 -0
- Podflow/message/__init__.py +2 -0
- Podflow/message/backup_zip_save.py +45 -0
- Podflow/message/create_main_rss.py +44 -0
- Podflow/message/display_qrcode_and_url.py +36 -0
- Podflow/message/fail_message_initialize.py +165 -0
- Podflow/message/format_time.py +27 -0
- Podflow/message/get_original_rss.py +65 -0
- Podflow/message/get_video_format.py +111 -0
- Podflow/message/get_video_format_multithread.py +42 -0
- Podflow/message/get_youtube_and_bilibili_video_format.py +87 -0
- Podflow/message/media_format.py +195 -0
- Podflow/message/original_rss_fail_print.py +15 -0
- Podflow/message/rss_create_hash.py +26 -0
- Podflow/message/title_correction.py +30 -0
- Podflow/message/update_information_display.py +72 -0
- Podflow/message/update_youtube_bilibili_rss.py +116 -0
- Podflow/message/want_retry.py +21 -0
- Podflow/message/xml_item.py +83 -0
- Podflow/message/xml_original_item.py +92 -0
- Podflow/message/xml_rss.py +46 -0
- Podflow/netscape/__init__.py +2 -0
- Podflow/netscape/bulid_netscape.py +44 -0
- Podflow/netscape/get_cookie_dict.py +21 -0
- Podflow/parse_arguments.py +80 -0
- Podflow/remove/__init__.py +2 -0
- Podflow/remove/remove_dir.py +33 -0
- Podflow/remove/remove_file.py +23 -0
- Podflow/youtube/__init__.py +2 -0
- Podflow/youtube/build.py +287 -0
- Podflow/youtube/get.py +376 -0
- Podflow/youtube/login.py +39 -0
- podflow-2025.1.26.dist-info/METADATA +214 -0
- podflow-2025.1.26.dist-info/RECORD +80 -0
- podflow-2025.1.26.dist-info/WHEEL +5 -0
- podflow-2025.1.26.dist-info/entry_points.txt +6 -0
- podflow-2025.1.26.dist-info/top_level.txt +1 -0
@@ -0,0 +1,307 @@
|
|
1
|
+
# Podflow/download/dl_aideo_video.py
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
import os
|
5
|
+
from datetime import datetime
|
6
|
+
import ffmpeg
|
7
|
+
import yt_dlp
|
8
|
+
from Podflow import gVar
|
9
|
+
from Podflow.basic.write_log import write_log
|
10
|
+
from Podflow.basic.get_duration import get_duration
|
11
|
+
from Podflow.download.show_progress import show_progress
|
12
|
+
from Podflow.message.fail_message_initialize import fail_message_initialize
|
13
|
+
|
14
|
+
|
15
|
+
# 下载视频模块
|
16
|
+
def download_video(
|
17
|
+
video_url,
|
18
|
+
output_dir,
|
19
|
+
output_format,
|
20
|
+
format_id,
|
21
|
+
video_website,
|
22
|
+
video_write_log,
|
23
|
+
sesuffix="",
|
24
|
+
cookies=None,
|
25
|
+
playlist_num=None,
|
26
|
+
):
|
27
|
+
class MyLogger:
|
28
|
+
def debug(self, msg):
|
29
|
+
pass
|
30
|
+
|
31
|
+
def warning(self, msg):
|
32
|
+
pass
|
33
|
+
|
34
|
+
def info(self, msg):
|
35
|
+
pass
|
36
|
+
|
37
|
+
def error(self, msg):
|
38
|
+
msg = fail_message_initialize(msg, video_url).ljust(45)
|
39
|
+
print(msg)
|
40
|
+
|
41
|
+
outtmpl = f"channel_audiovisual/{output_dir}/{video_url}{sesuffix}.{output_format}"
|
42
|
+
ydl_opts = {
|
43
|
+
"outtmpl": outtmpl, # 输出文件路径和名称
|
44
|
+
"format": f"{format_id}", # 指定下载的最佳音频和视频格式
|
45
|
+
"noprogress": True,
|
46
|
+
"quiet": True,
|
47
|
+
"progress_hooks": [show_progress],
|
48
|
+
"logger": MyLogger(),
|
49
|
+
"throttled_rate": "70K", # 设置最小下载速率为:字节/秒
|
50
|
+
}
|
51
|
+
if cookies:
|
52
|
+
if "www.bilibili.com" in video_website:
|
53
|
+
ydl_opts["http_headers"] = {
|
54
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
|
55
|
+
"Referer": "https://www.bilibili.com/",
|
56
|
+
}
|
57
|
+
elif "www.youtube.com" in video_website:
|
58
|
+
ydl_opts["http_headers"] = {
|
59
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
|
60
|
+
"Referer": "https://www.youtube.com/",
|
61
|
+
}
|
62
|
+
ydl_opts["cookiefile"] = cookies # cookies 是你的 cookies 文件名
|
63
|
+
if playlist_num: # 播放列表的第n个视频
|
64
|
+
ydl_opts["playliststart"] = playlist_num
|
65
|
+
ydl_opts["playlistend"] = playlist_num
|
66
|
+
try:
|
67
|
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
68
|
+
ydl.download([f"{video_website}"]) # 下载指定视频链接的视频
|
69
|
+
return None, None
|
70
|
+
except Exception as download_video_error:
|
71
|
+
fail_info = fail_message_initialize(download_video_error, video_url).replace("\n","")
|
72
|
+
remove_info = ""
|
73
|
+
if fail_info in [
|
74
|
+
"",
|
75
|
+
"\033[31m请求拒绝\033[0m",
|
76
|
+
"\033[31m数据不完整\033[0m",
|
77
|
+
"\033[31m传输中断\033[0m",
|
78
|
+
"\033[31m请求超时\033[0m",
|
79
|
+
"\033[31m响应超时\033[0m",
|
80
|
+
] and "www.youtube.com" in video_website:
|
81
|
+
if os.path.isfile(outtmpl):
|
82
|
+
os.remove(outtmpl)
|
83
|
+
remove_info = "|已删除失败文件"
|
84
|
+
elif os.path.isfile(outtmpl + ".part"):
|
85
|
+
os.remove(outtmpl + ".part")
|
86
|
+
remove_info = "|已删除部分失败文件"
|
87
|
+
write_log(
|
88
|
+
f"{video_write_log} \033[31m下载失败\033[0m",
|
89
|
+
None,
|
90
|
+
True,
|
91
|
+
True,
|
92
|
+
f"错误信息: {fail_info}{remove_info}",
|
93
|
+
) # 写入下载失败的日志信息
|
94
|
+
return video_url, fail_info
|
95
|
+
|
96
|
+
|
97
|
+
# 视频完整下载模块
|
98
|
+
def dl_full_video(
|
99
|
+
video_url,
|
100
|
+
output_dir,
|
101
|
+
output_format,
|
102
|
+
format_id,
|
103
|
+
id_duration,
|
104
|
+
video_website,
|
105
|
+
video_write_log,
|
106
|
+
sesuffix="",
|
107
|
+
cookies=None,
|
108
|
+
playlist_num=None,
|
109
|
+
):
|
110
|
+
video_id_failed, fail_info = download_video(
|
111
|
+
video_url,
|
112
|
+
output_dir,
|
113
|
+
output_format,
|
114
|
+
format_id,
|
115
|
+
video_website,
|
116
|
+
video_write_log,
|
117
|
+
sesuffix,
|
118
|
+
cookies,
|
119
|
+
playlist_num,
|
120
|
+
)
|
121
|
+
if video_id_failed:
|
122
|
+
return video_url, fail_info
|
123
|
+
duration_video = get_duration(
|
124
|
+
f"channel_audiovisual/{output_dir}/{video_url}{sesuffix}.{output_format}"
|
125
|
+
) # 获取已下载视频的实际时长
|
126
|
+
if abs(id_duration - duration_video) <= 1: # 检查实际时长与预计时长是否一致
|
127
|
+
return None, None
|
128
|
+
if duration_video:
|
129
|
+
fail_info = f"不完整({id_duration}|{duration_video}"
|
130
|
+
write_log(
|
131
|
+
f"{video_write_log} \033[31m下载失败\033[0m\n错误信息: {fail_info})"
|
132
|
+
)
|
133
|
+
os.remove(
|
134
|
+
f"channel_audiovisual/{output_dir}/{video_url}{sesuffix}.{output_format}"
|
135
|
+
) # 删除不完整的视频
|
136
|
+
return video_url, fail_info
|
137
|
+
|
138
|
+
|
139
|
+
# 视频重试下载模块
|
140
|
+
def dl_retry_video(
|
141
|
+
video_url,
|
142
|
+
output_dir,
|
143
|
+
output_format,
|
144
|
+
format_id,
|
145
|
+
id_duration,
|
146
|
+
retry_count,
|
147
|
+
video_website,
|
148
|
+
video_write_log,
|
149
|
+
sesuffix="",
|
150
|
+
cookies=None,
|
151
|
+
playlist_num=None,
|
152
|
+
):
|
153
|
+
video_id_failed, _ = dl_full_video(
|
154
|
+
video_url,
|
155
|
+
output_dir,
|
156
|
+
output_format,
|
157
|
+
format_id,
|
158
|
+
id_duration,
|
159
|
+
video_website,
|
160
|
+
video_write_log,
|
161
|
+
sesuffix,
|
162
|
+
cookies,
|
163
|
+
playlist_num,
|
164
|
+
)
|
165
|
+
# 下载失败后重复尝试下载视频
|
166
|
+
video_id_count = 0
|
167
|
+
while video_id_count < retry_count and video_id_failed:
|
168
|
+
if (
|
169
|
+
cookies is None
|
170
|
+
and "www.youtube.com" in video_website
|
171
|
+
and gVar.youtube_cookie
|
172
|
+
):
|
173
|
+
cookies = "channel_data/yt_dlp_youtube.txt"
|
174
|
+
video_id_count += 1
|
175
|
+
if cookies:
|
176
|
+
write_log(f"{video_write_log} 第\033[34m{video_id_count}\033[0m次重新下载 🍪")
|
177
|
+
else:
|
178
|
+
write_log(f"{video_write_log} 第\033[34m{video_id_count}\033[0m次重新下载")
|
179
|
+
video_id_failed = dl_full_video(
|
180
|
+
video_url,
|
181
|
+
output_dir,
|
182
|
+
output_format,
|
183
|
+
format_id,
|
184
|
+
id_duration,
|
185
|
+
video_website,
|
186
|
+
video_write_log,
|
187
|
+
sesuffix,
|
188
|
+
cookies,
|
189
|
+
playlist_num,
|
190
|
+
)
|
191
|
+
return video_id_failed
|
192
|
+
|
193
|
+
|
194
|
+
# 音视频总下载模块
|
195
|
+
def dl_aideo_video(
|
196
|
+
video_url,
|
197
|
+
output_dir,
|
198
|
+
output_format,
|
199
|
+
video_format,
|
200
|
+
retry_count,
|
201
|
+
video_website,
|
202
|
+
output_dir_name="",
|
203
|
+
cookies=None,
|
204
|
+
playlist_num=None,
|
205
|
+
display_color="\033[95m",
|
206
|
+
):
|
207
|
+
if output_dir_name:
|
208
|
+
video_write_log = f"{display_color}{output_dir_name}\033[0m|{video_url}"
|
209
|
+
else:
|
210
|
+
video_write_log = video_url
|
211
|
+
id_duration = video_format[0]
|
212
|
+
print_message = (
|
213
|
+
"\033[34m开始下载\033[0m 🍪" if cookies else "\033[34m开始下载\033[0m"
|
214
|
+
)
|
215
|
+
print(
|
216
|
+
f"{datetime.now().strftime('%H:%M:%S')}|{video_write_log} {print_message}",
|
217
|
+
end="",
|
218
|
+
)
|
219
|
+
if output_format == "m4a":
|
220
|
+
if video_format[1] in ["140", "30280"]:
|
221
|
+
print("")
|
222
|
+
else:
|
223
|
+
print(f" \033[97m{video_format[1]}\033[0m")
|
224
|
+
video_id_failed = dl_retry_video(
|
225
|
+
video_url,
|
226
|
+
output_dir,
|
227
|
+
"m4a",
|
228
|
+
video_format[1],
|
229
|
+
id_duration,
|
230
|
+
retry_count,
|
231
|
+
video_website,
|
232
|
+
video_write_log,
|
233
|
+
"",
|
234
|
+
cookies,
|
235
|
+
playlist_num,
|
236
|
+
)
|
237
|
+
else:
|
238
|
+
print(
|
239
|
+
f"\n{datetime.now().strftime('%H:%M:%S')}|\033[34m视频部分开始下载\033[0m \033[97m{video_format[2]}\033[0m"
|
240
|
+
)
|
241
|
+
video_id_failed = dl_retry_video(
|
242
|
+
video_url,
|
243
|
+
output_dir,
|
244
|
+
"mp4",
|
245
|
+
video_format[2],
|
246
|
+
id_duration,
|
247
|
+
retry_count,
|
248
|
+
video_website,
|
249
|
+
video_write_log,
|
250
|
+
".part",
|
251
|
+
cookies,
|
252
|
+
playlist_num,
|
253
|
+
)
|
254
|
+
if video_id_failed is None:
|
255
|
+
print(
|
256
|
+
f"{datetime.now().strftime('%H:%M:%S')}|\033[34m音频部分开始下载\033[0m \033[97m{video_format[1]}\033[0m"
|
257
|
+
)
|
258
|
+
video_id_failed = dl_retry_video(
|
259
|
+
video_url,
|
260
|
+
output_dir,
|
261
|
+
"m4a",
|
262
|
+
video_format[1],
|
263
|
+
id_duration,
|
264
|
+
retry_count,
|
265
|
+
video_website,
|
266
|
+
video_write_log,
|
267
|
+
".part",
|
268
|
+
cookies,
|
269
|
+
playlist_num,
|
270
|
+
)
|
271
|
+
if video_id_failed is None:
|
272
|
+
print(
|
273
|
+
f"{datetime.now().strftime('%H:%M:%S')}|\033[34m开始合成...\033[0m",
|
274
|
+
end="",
|
275
|
+
)
|
276
|
+
# 指定视频文件和音频文件的路径
|
277
|
+
video_file = f"channel_audiovisual/{output_dir}/{video_url}.part.mp4"
|
278
|
+
audio_file = f"channel_audiovisual/{output_dir}/{video_url}.part.m4a"
|
279
|
+
output_file = f"channel_audiovisual/{output_dir}/{video_url}.mp4"
|
280
|
+
try:
|
281
|
+
# 使用 ffmpeg-python 合并视频和音频
|
282
|
+
video = ffmpeg.input(video_file)
|
283
|
+
audio = ffmpeg.input(audio_file)
|
284
|
+
stream = ffmpeg.output(
|
285
|
+
audio, video, output_file, vcodec="copy", acodec="copy"
|
286
|
+
)
|
287
|
+
ffmpeg.run(stream, quiet=True)
|
288
|
+
print(" \033[32m合成成功\033[0m")
|
289
|
+
# 删除临时文件
|
290
|
+
os.remove(f"channel_audiovisual/{output_dir}/{video_url}.part.mp4")
|
291
|
+
os.remove(f"channel_audiovisual/{output_dir}/{video_url}.part.m4a")
|
292
|
+
except ffmpeg.Error as dl_aideo_video_error:
|
293
|
+
video_id_failed = video_url
|
294
|
+
write_log(
|
295
|
+
f"\n{video_write_log} \033[31m下载失败\033[0m\n错误信息: 合成失败:{dl_aideo_video_error}"
|
296
|
+
)
|
297
|
+
if video_id_failed is None:
|
298
|
+
if output_format == "m4a":
|
299
|
+
only_log = f" {video_format[1]}"
|
300
|
+
else:
|
301
|
+
only_log = f" {video_format[1]}+{video_format[2]}"
|
302
|
+
if cookies:
|
303
|
+
only_log += " Cookies"
|
304
|
+
write_log(
|
305
|
+
f"{video_write_log} \033[32m下载成功\033[0m", None, True, True, only_log
|
306
|
+
) # 写入下载成功的日志信息
|
307
|
+
return video_id_failed
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Podflow/download/show_progress.py
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
from Podflow.basic.time_format import time_format
|
5
|
+
from Podflow.download.convert_bytes import convert_bytes
|
6
|
+
|
7
|
+
|
8
|
+
# 下载显示模块
|
9
|
+
def show_progress(stream):
|
10
|
+
stream = dict(stream)
|
11
|
+
if "downloaded_bytes" in stream:
|
12
|
+
downloaded_bytes = convert_bytes(stream["downloaded_bytes"]).rjust(9)
|
13
|
+
else:
|
14
|
+
downloaded_bytes = " Unknow B"
|
15
|
+
if "total_bytes" in stream:
|
16
|
+
total_bytes = convert_bytes(stream["total_bytes"])
|
17
|
+
else:
|
18
|
+
total_bytes = "Unknow B"
|
19
|
+
if stream["speed"] is None:
|
20
|
+
speed = " Unknow B"
|
21
|
+
else:
|
22
|
+
speed = convert_bytes(stream["speed"], [" B", "KiB", "MiB", "GiB"], 1000).rjust(
|
23
|
+
9
|
24
|
+
)
|
25
|
+
if stream["status"] in ["downloading", "error"]:
|
26
|
+
if "total_bytes" in stream:
|
27
|
+
percent = stream["downloaded_bytes"] / stream["total_bytes"] * 100
|
28
|
+
else:
|
29
|
+
percent = 0
|
30
|
+
percent = f"{percent:.1f}" if percent == 100 else f"{percent:.2f}"
|
31
|
+
percent = percent.rjust(5)
|
32
|
+
eta = time_format(stream["eta"]).ljust(8)
|
33
|
+
print(
|
34
|
+
(
|
35
|
+
f"\r\033[94m{percent}%\033[0m|{downloaded_bytes}/{total_bytes}|\033[32m{speed}/s\033[0m|\033[93m{eta}\033[0m"
|
36
|
+
),
|
37
|
+
end="",
|
38
|
+
)
|
39
|
+
if stream["status"] == "finished":
|
40
|
+
if "elapsed" in stream:
|
41
|
+
elapsed = time_format(stream["elapsed"]).ljust(8)
|
42
|
+
else:
|
43
|
+
elapsed = "Unknown "
|
44
|
+
print(
|
45
|
+
f"\r100.0%|{downloaded_bytes}/{total_bytes}|\033[32m{speed}/s\033[0m|\033[97m{elapsed}\033[0m"
|
46
|
+
)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Podflow/download/wait_animation.py
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
import time
|
5
|
+
from datetime import datetime
|
6
|
+
|
7
|
+
|
8
|
+
# 等待动画模块
|
9
|
+
def wait_animation(stop_flag, wait_animation_display_info):
|
10
|
+
animation = "."
|
11
|
+
i = 1
|
12
|
+
prepare_youtube_print = datetime.now().strftime("%H:%M:%S")
|
13
|
+
while True:
|
14
|
+
if stop_flag[0] == "keep":
|
15
|
+
print(
|
16
|
+
f"\r{prepare_youtube_print}|{wait_animation_display_info}\033[34m准备中{animation.ljust(5)}\033[0m",
|
17
|
+
end="",
|
18
|
+
)
|
19
|
+
elif stop_flag[0] == "error":
|
20
|
+
print(
|
21
|
+
f"\r{prepare_youtube_print}|{wait_animation_display_info}\033[34m准备中{animation} \033[31m失败:\033[0m"
|
22
|
+
)
|
23
|
+
break
|
24
|
+
elif stop_flag[0] == "end":
|
25
|
+
print(
|
26
|
+
f"\r{prepare_youtube_print}|{wait_animation_display_info}\033[34m准备中{animation} 已完成\033[0m"
|
27
|
+
)
|
28
|
+
break
|
29
|
+
if i % 5 == 0:
|
30
|
+
animation = "."
|
31
|
+
else:
|
32
|
+
animation += "."
|
33
|
+
i += 1
|
34
|
+
time.sleep(0.5)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Podflow/download/youtube_and_bilibili_download.py
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
from Podflow import gVar
|
5
|
+
from Podflow.basic.write_log import write_log
|
6
|
+
from Podflow.download.dl_aideo_video import dl_aideo_video
|
7
|
+
|
8
|
+
|
9
|
+
# 下载YouTube和哔哩哔哩视频
|
10
|
+
def youtube_and_bilibili_download():
|
11
|
+
for video_id, format_value in gVar.video_id_update_format.items():
|
12
|
+
if isinstance(format_value, dict) and format_value["main"] not in gVar.video_id_failed:
|
13
|
+
output_dir_name = format_value["name"]
|
14
|
+
display_color = "\033[35m" if format_value["backward_update"] else "\033[95m"
|
15
|
+
if dl_aideo_video(
|
16
|
+
video_id,
|
17
|
+
format_value["id"],
|
18
|
+
format_value["media"],
|
19
|
+
format_value["format"],
|
20
|
+
gVar.config["retry_count"],
|
21
|
+
format_value["download"]["url"],
|
22
|
+
output_dir_name,
|
23
|
+
format_value["cookie"],
|
24
|
+
format_value["download"]["num"],
|
25
|
+
display_color
|
26
|
+
):
|
27
|
+
gVar.video_id_failed.append(format_value["main"])
|
28
|
+
write_log(
|
29
|
+
f"{display_color}{output_dir_name}\033[0m|{video_id} \033[31m无法下载\033[0m"
|
30
|
+
)
|
Podflow/ffmpeg_judge.py
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# Podflow/ffmpeg_judge.py
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
import sys
|
5
|
+
import subprocess
|
6
|
+
import shutil
|
7
|
+
from Podflow.basic.write_log import write_log
|
8
|
+
|
9
|
+
|
10
|
+
def error_ffmpeg_judge(ffmpeg_worry):
|
11
|
+
write_log("FFmpeg 未安装, 请安装后重试")
|
12
|
+
print(ffmpeg_worry)
|
13
|
+
sys.exit(0)
|
14
|
+
|
15
|
+
|
16
|
+
def ffmpeg_judge():
|
17
|
+
ffmpeg_worry = """\033[0mFFmpeg安装方法:
|
18
|
+
Ubuntu:
|
19
|
+
\033[32msudo apt update
|
20
|
+
sudo apt install ffmpeg\033[0m
|
21
|
+
CentOS:
|
22
|
+
\033[32msudo yum update
|
23
|
+
sudo yum install ffmpeg\033[0m
|
24
|
+
Debian:
|
25
|
+
\033[32msudo apt-get update
|
26
|
+
sudo apt-get install ffmpeg\033[0m
|
27
|
+
Arch Linux、Fedora:
|
28
|
+
\033[32msudo pacman -S ffmpeg
|
29
|
+
sudo dnf install ffmpeg\033[0m
|
30
|
+
检查FFmpeg版本:
|
31
|
+
\033[32mffmpeg -version\033[0m"""
|
32
|
+
|
33
|
+
# 使用 shutil.which 检查 ffmpeg 是否安装
|
34
|
+
if shutil.which("ffmpeg") is None:
|
35
|
+
error_ffmpeg_judge(ffmpeg_worry)
|
36
|
+
|
37
|
+
try:
|
38
|
+
# 执行 ffmpeg 命令获取版本信息
|
39
|
+
result = subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True, check=True)
|
40
|
+
output = result.stdout.lower()
|
41
|
+
# 检查输出中是否包含 ffmpeg 版本信息
|
42
|
+
if "ffmpeg version" not in output:
|
43
|
+
error_ffmpeg_judge(ffmpeg_worry)
|
44
|
+
except FileNotFoundError:
|
45
|
+
error_ffmpeg_judge(ffmpeg_worry)
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# Podflow/httpfs/app_bottle.py
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
import os
|
5
|
+
import hashlib
|
6
|
+
from datetime import datetime
|
7
|
+
import cherrypy
|
8
|
+
from bottle import Bottle, abort, redirect, request, static_file
|
9
|
+
from Podflow import gVar
|
10
|
+
from Podflow.basic.write_log import write_log
|
11
|
+
|
12
|
+
|
13
|
+
class bottle_app:
|
14
|
+
# Bottle和Cherrypy初始化模块
|
15
|
+
def __init__(self):
|
16
|
+
self.app_bottle = Bottle() # 创建 Bottle 应用
|
17
|
+
self.bottle_print = [] # 存储打印日志
|
18
|
+
self.setup_routes()
|
19
|
+
|
20
|
+
def setup_routes(self):
|
21
|
+
self.app_bottle.route("/", callback=self.home)
|
22
|
+
self.app_bottle.route("/shutdown", callback=self.shutdown)
|
23
|
+
self.app_bottle.route("/favicon.ico", callback=self.favicon)
|
24
|
+
self.app_bottle.route("/<filename:path>", callback=self.serve_static)
|
25
|
+
|
26
|
+
# 判断token是否正确的验证模块
|
27
|
+
def token_judgment(self, token, VALID_TOKEN="", filename="", foldername=""):
|
28
|
+
if foldername != "channel_audiovisual/":
|
29
|
+
# 对于其他文件夹, 采用常规的 Token 验证
|
30
|
+
return VALID_TOKEN == "" or token == VALID_TOKEN
|
31
|
+
if (
|
32
|
+
VALID_TOKEN == ""
|
33
|
+
and token == hashlib.sha256(f"{filename}".encode()).hexdigest()
|
34
|
+
): # 如果没有配置 Token, 则使用文件名的哈希值
|
35
|
+
return True
|
36
|
+
elif (
|
37
|
+
token == hashlib.sha256(f"{VALID_TOKEN}/{filename}".encode()).hexdigest()
|
38
|
+
): # 使用验证 Token 和文件名的哈希值
|
39
|
+
return True
|
40
|
+
else:
|
41
|
+
return False
|
42
|
+
|
43
|
+
# 添加至bottle_print模块
|
44
|
+
def add_bottle_print(self, client_ip, filename, status):
|
45
|
+
# 后缀
|
46
|
+
suffixs = [".mp4", ".m4a", ".xml", ".ico"]
|
47
|
+
# 设置状态码对应的颜色
|
48
|
+
status_colors = {
|
49
|
+
200: "\033[32m", # 绿色 (成功)
|
50
|
+
401: "\033[31m", # 红色 (未经授权)
|
51
|
+
404: "\033[35m", # 紫色 (未找到)
|
52
|
+
303: "\033[33m", # 黄色 (重定向)
|
53
|
+
206: "\033[36m", # 青色 (部分内容)
|
54
|
+
}
|
55
|
+
# 默认颜色
|
56
|
+
color = status_colors.get(status, "\033[0m")
|
57
|
+
status = f"{color}{status}\033[0m"
|
58
|
+
now_time = datetime.now().strftime("%H:%M:%S")
|
59
|
+
client_ip = f"\033[34m{client_ip}\033[0m"
|
60
|
+
if gVar.config["httpfs"]:
|
61
|
+
write_log(
|
62
|
+
f"{client_ip} {filename} {status}",
|
63
|
+
None,
|
64
|
+
False,
|
65
|
+
True,
|
66
|
+
None,
|
67
|
+
"httpfs.log",
|
68
|
+
)
|
69
|
+
for suffix in suffixs:
|
70
|
+
filename = filename.replace(suffix, "")
|
71
|
+
self.bottle_print.append(f"{now_time}|{client_ip} {filename} {status}")
|
72
|
+
|
73
|
+
# CherryPy 服务器打印模块
|
74
|
+
def cherry_print(self, flag_judgment=True):
|
75
|
+
if flag_judgment:
|
76
|
+
gVar.server_process_print_flag[0] = "keep"
|
77
|
+
if (
|
78
|
+
gVar.server_process_print_flag[0] == "keep" and self.bottle_print
|
79
|
+
): # 如果设置为保持输出, 则打印日志
|
80
|
+
print("\n".join(self.bottle_print))
|
81
|
+
self.bottle_print.clear()
|
82
|
+
|
83
|
+
# 主路由处理根路径请求
|
84
|
+
def home(self):
|
85
|
+
VALID_TOKEN = gVar.config["token"] # 从配置中读取主验证 Token
|
86
|
+
|
87
|
+
# 输出请求日志的函数
|
88
|
+
def print_out(status):
|
89
|
+
client_ip = request.remote_addr # 获取客户端 IP 地址
|
90
|
+
client_port = request.environ.get("REMOTE_PORT") # 获取客户端端口
|
91
|
+
if client_port:
|
92
|
+
client_ip = f"{client_ip}:{client_port}" # 如果有端口信息, 则包括端口
|
93
|
+
self.add_bottle_print(client_ip, "/", status) # 添加日志信息
|
94
|
+
self.cherry_print(False)
|
95
|
+
|
96
|
+
token = request.query.get("token") # 获取请求中的 Token
|
97
|
+
if self.token_judgment(token, VALID_TOKEN): # 验证 Token
|
98
|
+
print_out(303) # 如果验证成功, 输出 200 状态
|
99
|
+
return redirect("https://github.com/gruel-zxz/podflow") # 返回正常响应
|
100
|
+
else:
|
101
|
+
print_out(401) # 如果验证失败, 输出 401 状态
|
102
|
+
abort(401, "Unauthorized: Invalid Token") # 返回未经授权错误
|
103
|
+
|
104
|
+
# 路由处理关闭服务器的请求
|
105
|
+
def shutdown(self):
|
106
|
+
Shutdown_VALID_TOKEN = "shutdown"
|
107
|
+
Shutdown_VALID_TOKEN += datetime.now().strftime("%Y%m%d%H%M%S")
|
108
|
+
Shutdown_VALID_TOKEN += os.urandom(32).hex()
|
109
|
+
Shutdown_VALID_TOKEN = hashlib.sha256(
|
110
|
+
Shutdown_VALID_TOKEN.encode()
|
111
|
+
).hexdigest() # 用于服务器关闭的验证 Token
|
112
|
+
|
113
|
+
# 输出关闭请求日志的函数
|
114
|
+
def print_out(status):
|
115
|
+
client_ip = request.remote_addr
|
116
|
+
client_port = request.environ.get("REMOTE_PORT")
|
117
|
+
if client_port:
|
118
|
+
client_ip = f"{client_ip}:{client_port}"
|
119
|
+
self.add_bottle_print(client_ip, "shutdown", status)
|
120
|
+
self.cherry_print(False)
|
121
|
+
|
122
|
+
token = request.query.get("token") # 获取请求中的 Token
|
123
|
+
if self.token_judgment(
|
124
|
+
token, Shutdown_VALID_TOKEN
|
125
|
+
): # 验证 Token 是否为关闭用的 Token
|
126
|
+
print_out(200) # 如果验证成功, 输出 200 状态
|
127
|
+
cherrypy.engine.exit() # 使用 CherryPy 提供的停止功能来关闭服务器
|
128
|
+
return "Shutting down..." # 返回关机响应
|
129
|
+
else:
|
130
|
+
print_out(401) # 如果验证失败, 输出 401 状态
|
131
|
+
abort(401, "Unauthorized: Invalid Token") # 返回未经授权错误
|
132
|
+
|
133
|
+
# 路由处理 favicon 请求
|
134
|
+
def favicon(self):
|
135
|
+
client_ip = request.remote_addr
|
136
|
+
if client_port := request.environ.get("REMOTE_PORT"):
|
137
|
+
client_ip = f"{client_ip}:{client_port}"
|
138
|
+
self.add_bottle_print(client_ip, "favicon.ico", 303) # 输出访问 favicon 的日志
|
139
|
+
self.cherry_print(False)
|
140
|
+
return redirect(
|
141
|
+
"https://raw.githubusercontent.com/gruel-zxz/podflow/main/Podflow.png"
|
142
|
+
) # 重定向到图标 URL
|
143
|
+
|
144
|
+
# 路由处理静态文件请求
|
145
|
+
def serve_static(self, filename):
|
146
|
+
VALID_TOKEN = gVar.config["token"] # 从配置中读取主验证 Token
|
147
|
+
# 定义要共享的文件路径
|
148
|
+
bottle_filename = gVar.config["filename"] # 从配置中读取文件名
|
149
|
+
shared_files = {
|
150
|
+
bottle_filename.lower(): f"{bottle_filename}.xml", # 文件路径映射, 支持大小写不敏感的文件名
|
151
|
+
f"{bottle_filename.lower()}.xml": f"{bottle_filename}.xml", # 同上, 支持带 .xml 后缀
|
152
|
+
}
|
153
|
+
bottle_channelid = (
|
154
|
+
gVar.channelid_youtube_ids_original
|
155
|
+
| gVar.channelid_bilibili_ids_original
|
156
|
+
| {"channel_audiovisual/": "", "channel_rss/": ""}
|
157
|
+
) # 合并多个频道 ID
|
158
|
+
token = request.query.get("token") # 获取请求中的 Token
|
159
|
+
|
160
|
+
# 输出文件请求日志的函数
|
161
|
+
def print_out(filename, status):
|
162
|
+
client_ip = request.remote_addr
|
163
|
+
client_port = request.environ.get("REMOTE_PORT")
|
164
|
+
if client_port:
|
165
|
+
client_ip = f"{client_ip}:{client_port}"
|
166
|
+
for (
|
167
|
+
bottle_channelid_key,
|
168
|
+
bottle_channelid_value,
|
169
|
+
) in bottle_channelid.items():
|
170
|
+
filename = filename.replace(
|
171
|
+
bottle_channelid_key, bottle_channelid_value
|
172
|
+
) # 替换频道路径
|
173
|
+
if status == 200 and request.headers.get(
|
174
|
+
"Range"
|
175
|
+
): # 如果是部分请求, 则返回 206 状态
|
176
|
+
status = 206
|
177
|
+
self.add_bottle_print(client_ip, filename, status) # 输出日志
|
178
|
+
self.cherry_print(False)
|
179
|
+
|
180
|
+
# 文件是否存在检查的函数
|
181
|
+
def file_exist(token, VALID_TOKEN, filename, foldername=""):
|
182
|
+
if self.token_judgment(
|
183
|
+
token, VALID_TOKEN, filename, foldername
|
184
|
+
): # 验证 Token
|
185
|
+
if os.path.exists(filename): # 如果文件存在, 返回文件
|
186
|
+
print_out(filename, 200)
|
187
|
+
return static_file(filename, root=".")
|
188
|
+
else: # 如果文件不存在, 返回 404 错误
|
189
|
+
print_out(filename, 404)
|
190
|
+
abort(404, "File not found")
|
191
|
+
else: # 如果 Token 验证失败, 返回 401 错误
|
192
|
+
print_out(filename, 401)
|
193
|
+
abort(401, "Unauthorized: Invalid Token")
|
194
|
+
|
195
|
+
# 处理不同的文件路径
|
196
|
+
if filename in ["channel_audiovisual/", "channel_rss/"]:
|
197
|
+
print_out(filename, 404)
|
198
|
+
abort(404, "File not found")
|
199
|
+
elif filename.startswith("channel_audiovisual/"):
|
200
|
+
return file_exist(token, VALID_TOKEN, filename, "channel_audiovisual/")
|
201
|
+
elif filename.startswith("channel_rss/") and filename.endswith(".xml"):
|
202
|
+
return file_exist(token, VALID_TOKEN, filename)
|
203
|
+
elif filename.startswith("channel_rss/"):
|
204
|
+
return file_exist(token, VALID_TOKEN, f"{filename}.xml")
|
205
|
+
elif filename.lower() in shared_files:
|
206
|
+
return file_exist(token, VALID_TOKEN, shared_files[filename.lower()])
|
207
|
+
else:
|
208
|
+
print_out(filename, 404) # 如果文件路径未匹配, 返回 404 错误
|
209
|
+
abort(404, "File not found")
|
210
|
+
|
211
|
+
|
212
|
+
bottle_app_instance = bottle_app()
|