podflow 20250417.2__py3-none-any.whl → 20250427__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 +1 -0
- podflow/download/dl_aideo_video.py +12 -0
- podflow/download/show_progress.py +20 -5
- podflow/download/youtube_and_bilibili_download.py +3 -2
- podflow/httpfs/app_bottle.py +73 -12
- podflow/httpfs/download_bar.py +35 -0
- podflow/main_podcast.py +12 -7
- podflow/templates/css/index.css +391 -0
- podflow/templates/index.html +9 -654
- podflow/templates/js/index.js +481 -0
- podflow/templates/js/qrcode.min.js +1 -0
- {podflow-20250417.2.dist-info → podflow-20250427.dist-info}/METADATA +1 -1
- {podflow-20250417.2.dist-info → podflow-20250427.dist-info}/RECORD +16 -12
- {podflow-20250417.2.dist-info → podflow-20250427.dist-info}/WHEEL +0 -0
- {podflow-20250417.2.dist-info → podflow-20250427.dist-info}/entry_points.txt +0 -0
- {podflow-20250417.2.dist-info → podflow-20250427.dist-info}/top_level.txt +0 -0
podflow/__init__.py
CHANGED
@@ -9,6 +9,7 @@ from podflow import gVar
|
|
9
9
|
from podflow.basic.write_log import write_log
|
10
10
|
from podflow.basic.time_print import time_print
|
11
11
|
from podflow.basic.get_duration import get_duration
|
12
|
+
from podflow.httpfs.download_bar import download_bar
|
12
13
|
from podflow.download.show_progress import show_progress
|
13
14
|
from podflow.message.fail_message_initialize import fail_message_initialize
|
14
15
|
|
@@ -94,6 +95,7 @@ def download_video(
|
|
94
95
|
True,
|
95
96
|
f"错误信息: {fail_info}{remove_info}",
|
96
97
|
) # 写入下载失败的日志信息
|
98
|
+
download_bar(mod=2, status="下载失败")
|
97
99
|
return video_url, fail_info
|
98
100
|
|
99
101
|
|
@@ -133,6 +135,7 @@ def dl_full_video(
|
|
133
135
|
if duration_video:
|
134
136
|
fail_info = f"不完整({id_duration}|{duration_video}"
|
135
137
|
write_log(f"{video_write_log} \033[31m下载失败\033[0m\n错误信息: {fail_info})")
|
138
|
+
download_bar(mod=2, status="下载失败")
|
136
139
|
os.remove(
|
137
140
|
f"channel_audiovisual/{output_dir}/{video_url}{sesuffix}.{output_format}"
|
138
141
|
) # 删除不完整的视频
|
@@ -208,6 +211,7 @@ def dl_aideo_video(
|
|
208
211
|
cookies=None,
|
209
212
|
playlist_num=None,
|
210
213
|
display_color="\033[95m",
|
214
|
+
title_name="",
|
211
215
|
):
|
212
216
|
if output_dir_name:
|
213
217
|
video_write_log = f"{display_color}{output_dir_name}\033[0m|{video_url}"
|
@@ -221,6 +225,13 @@ def dl_aideo_video(
|
|
221
225
|
f"{video_write_log} {print_message}",
|
222
226
|
NoEnter=True,
|
223
227
|
)
|
228
|
+
download_bar(
|
229
|
+
mod=0,
|
230
|
+
per=0,
|
231
|
+
idname=output_dir_name,
|
232
|
+
nametext=title_name,
|
233
|
+
file=f"{video_url}.{output_format}",
|
234
|
+
)
|
224
235
|
if output_format == "m4a":
|
225
236
|
if video_format[1] in ["140", "30280"]:
|
226
237
|
time_print(
|
@@ -323,4 +334,5 @@ def dl_aideo_video(
|
|
323
334
|
write_log(
|
324
335
|
f"{video_write_log} \033[32m下载成功\033[0m", None, True, True, only_log
|
325
336
|
) # 写入下载成功的日志信息
|
337
|
+
download_bar(mod=2, status="下载成功")
|
326
338
|
return video_id_failed
|
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
from podflow.basic.time_print import time_print
|
5
5
|
from podflow.basic.time_format import time_format
|
6
|
+
from podflow.httpfs.download_bar import download_bar
|
6
7
|
from podflow.download.convert_bytes import convert_bytes
|
7
8
|
|
8
9
|
|
@@ -30,20 +31,34 @@ def show_progress(stream):
|
|
30
31
|
percent = 0
|
31
32
|
percent = f"{percent:.1f}" if percent == 100 else f"{percent:.2f}"
|
32
33
|
percent = percent.rjust(5)
|
33
|
-
eta = time_format(stream["eta"])
|
34
|
+
eta = time_format(stream["eta"])
|
34
35
|
time_print(
|
35
|
-
f"\033[94m{percent}%\033[0m|{downloaded_bytes}/{total_bytes}|\033[32m{speed}/s\033[0m|\033[93m{eta}\033[0m",
|
36
|
+
f"\033[94m{percent}%\033[0m|{downloaded_bytes}/{total_bytes}|\033[32m{speed}/s\033[0m|\033[93m{eta.ljust(8)}\033[0m",
|
36
37
|
Top=True,
|
37
38
|
NoEnter=True,
|
38
39
|
Time=False,
|
39
40
|
)
|
41
|
+
download_bar(
|
42
|
+
mod=1,
|
43
|
+
per=stream["downloaded_bytes"] / stream["total_bytes"],
|
44
|
+
retime=eta,
|
45
|
+
speed=f"{speed}/s\t{downloaded_bytes}/{total_bytes}",
|
46
|
+
status="下载中",
|
47
|
+
)
|
40
48
|
if stream["status"] == "finished":
|
41
49
|
if "elapsed" in stream:
|
42
|
-
elapsed = time_format(stream["elapsed"])
|
50
|
+
elapsed = time_format(stream["elapsed"])
|
43
51
|
else:
|
44
|
-
elapsed = "Unknown
|
52
|
+
elapsed = "Unknown"
|
45
53
|
time_print(
|
46
|
-
f"100.0%|{downloaded_bytes}/{total_bytes}|\033[32m{speed}/s\033[0m|\033[97m{elapsed}\033[0m",
|
54
|
+
f"100.0%|{downloaded_bytes}/{total_bytes}|\033[32m{speed}/s\033[0m|\033[97m{elapsed.ljust(8)}\033[0m",
|
47
55
|
Top=True,
|
48
56
|
Time=False,
|
49
57
|
)
|
58
|
+
download_bar(
|
59
|
+
mod=1,
|
60
|
+
per=1,
|
61
|
+
retime=elapsed,
|
62
|
+
speed=f"{speed}/s\t{downloaded_bytes}/{total_bytes}",
|
63
|
+
status="下载完成",
|
64
|
+
)
|
@@ -22,10 +22,11 @@ def youtube_and_bilibili_download():
|
|
22
22
|
output_dir_name,
|
23
23
|
format_value["cookie"],
|
24
24
|
format_value["download"]["num"],
|
25
|
-
display_color
|
25
|
+
display_color,
|
26
|
+
format_value["title"],
|
26
27
|
):
|
27
28
|
gVar.video_id_failed.append(format_value["main"])
|
28
29
|
write_log(
|
29
30
|
f"{display_color}{output_dir_name}\033[0m|{video_id} \033[31m无法下载\033[0m"
|
30
31
|
)
|
31
|
-
gVar.video_id_update_format[video_id]["finish"] = True
|
32
|
+
gVar.video_id_update_format[video_id]["finish"] = True
|
podflow/httpfs/app_bottle.py
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
# coding: utf-8
|
3
3
|
|
4
4
|
import os
|
5
|
+
import json
|
6
|
+
import time
|
5
7
|
import hashlib
|
6
8
|
import mimetypes
|
7
9
|
import pkg_resources
|
@@ -26,6 +28,7 @@ class bottle_app:
|
|
26
28
|
self.setup_routes() # 设置路由
|
27
29
|
self.logname = "httpfs.log" # 默认日志文件名
|
28
30
|
self.http_fs = False
|
31
|
+
self._last_message_state = {} # 用于 SSE,跟踪消息状态
|
29
32
|
|
30
33
|
def setup_routes(self, upload=False):
|
31
34
|
# 设置/favicon.ico路由,回调函数为favicon
|
@@ -41,8 +44,11 @@ class bottle_app:
|
|
41
44
|
else:
|
42
45
|
self.app_bottle.route("/index", callback=self.index)
|
43
46
|
self.app_bottle.route("/getid", method="POST", callback=self.getid)
|
47
|
+
self.app_bottle.route(
|
48
|
+
"/templates/<filepath:path>", callback=self.serve_template_file
|
49
|
+
)
|
44
50
|
self.app_bottle.route("/<filename:path>", callback=self.serve_static)
|
45
|
-
self.app_bottle.route("/
|
51
|
+
self.app_bottle.route("/stream", callback=self.stream)
|
46
52
|
|
47
53
|
# 设置日志文件名及写入判断
|
48
54
|
def set_logname(self, logname="httpfs.log", http_fs=False):
|
@@ -68,7 +74,7 @@ class bottle_app:
|
|
68
74
|
return False
|
69
75
|
|
70
76
|
# 添加至bottle_print模块
|
71
|
-
def add_bottle_print(self, client_ip, filename, status):
|
77
|
+
def add_bottle_print(self, client_ip, filename, status=""):
|
72
78
|
# 后缀
|
73
79
|
suffixs = [".mp4", ".m4a", ".xml", ".ico"]
|
74
80
|
# 设置状态码对应的颜色
|
@@ -80,8 +86,9 @@ class bottle_app:
|
|
80
86
|
206: "\033[36m", # 青色 (部分内容)
|
81
87
|
}
|
82
88
|
# 默认颜色
|
83
|
-
|
84
|
-
|
89
|
+
if status:
|
90
|
+
color = status_colors.get(status, "\033[0m")
|
91
|
+
status = f"{color}{status}\033[0m"
|
85
92
|
now_time = datetime.now().strftime("%H:%M:%S")
|
86
93
|
client_ip = f"\033[34m{client_ip}\033[0m"
|
87
94
|
if self.http_fs:
|
@@ -115,7 +122,7 @@ class bottle_app:
|
|
115
122
|
self.bottle_print.clear()
|
116
123
|
|
117
124
|
# 输出请求日志的函数
|
118
|
-
def print_out(self, filename, status):
|
125
|
+
def print_out(self, filename, status=""):
|
119
126
|
client_ip = request.remote_addr
|
120
127
|
if client_port := request.environ.get("REMOTE_PORT"):
|
121
128
|
client_ip = f"{client_ip}:{client_port}"
|
@@ -395,10 +402,16 @@ class bottle_app:
|
|
395
402
|
},
|
396
403
|
}
|
397
404
|
|
405
|
+
def serve_template_file(self, filepath):
|
406
|
+
template_dir = pkg_resources.resource_filename("podflow", "templates")
|
407
|
+
return static_file(filepath, root=template_dir)
|
408
|
+
|
398
409
|
# 使用pkg_resources获取模板文件路径
|
399
410
|
def index(self):
|
400
|
-
template_path = pkg_resources.resource_filename(
|
401
|
-
|
411
|
+
template_path = pkg_resources.resource_filename(
|
412
|
+
"podflow", "templates/index.html"
|
413
|
+
)
|
414
|
+
with open(template_path, "r", encoding="UTF-8") as f:
|
402
415
|
html_content = f.read()
|
403
416
|
self.print_out("index", 200)
|
404
417
|
return html_content
|
@@ -412,13 +425,61 @@ class bottle_app:
|
|
412
425
|
response_message = get_channelid(content)
|
413
426
|
self.print_out("channelid", 200)
|
414
427
|
# 设置响应头为 application/json
|
415
|
-
response.content_type =
|
428
|
+
response.content_type = "application/json"
|
416
429
|
return {"response": response_message}
|
417
430
|
|
418
|
-
#
|
419
|
-
def
|
420
|
-
response.content_type =
|
421
|
-
|
431
|
+
# --- 新增 SSE 流处理路由 ---
|
432
|
+
def stream(self):
|
433
|
+
response.content_type = "text/event-stream"
|
434
|
+
response.set_header("Cache-Control", "no-cache")
|
435
|
+
response.set_header("Connection", "keep-alive")
|
436
|
+
response.set_header(
|
437
|
+
"Access-Control-Allow-Origin", "*"
|
438
|
+
) # 如果前端在不同源,需要设置 CORS
|
439
|
+
try:
|
440
|
+
while True:
|
441
|
+
# 获取当前消息状态
|
442
|
+
# 确保 gVar.index_message 存在且结构完整
|
443
|
+
if not hasattr(gVar, "index_message"):
|
444
|
+
current_state = {
|
445
|
+
"http": [],
|
446
|
+
"podflow": [],
|
447
|
+
"schedule": {},
|
448
|
+
"download": [],
|
449
|
+
}
|
450
|
+
else:
|
451
|
+
current_state = gVar.index_message
|
452
|
+
# 确保所有预期的键都存在
|
453
|
+
for key in ["http", "podflow", "schedule", "download"]:
|
454
|
+
if key not in current_state:
|
455
|
+
current_state[key] = (
|
456
|
+
[] if key in ["http", "podflow", "download"] else {}
|
457
|
+
)
|
458
|
+
# 简单实现:总是发送当前状态
|
459
|
+
# 优化:可以比较 current_state 和 last_state_sent,仅在有变化时发送
|
460
|
+
# if current_state != last_state_sent:
|
461
|
+
try:
|
462
|
+
# 使用 json.dumps 将 Python 字典转换为 JSON 字符串
|
463
|
+
message_json = json.dumps(current_state)
|
464
|
+
sse_data = f"data: {message_json}\n\n"
|
465
|
+
yield sse_data.encode("utf-8") # 发送编码后的数据
|
466
|
+
except TypeError as e:
|
467
|
+
# 如果序列化失败,记录错误,可以发送一个错误事件
|
468
|
+
self.print_out(f"Error serializing message data for SSE: {e}")
|
469
|
+
error_message = json.dumps({"error": "Failed to serialize data"})
|
470
|
+
yield f"event: error\ndata: {error_message}\n\n".encode("utf-8")
|
471
|
+
# 等待一段时间再检查/发送
|
472
|
+
time.sleep(0.25) # 每秒发送一次更新(或检查更新)
|
473
|
+
except (GeneratorExit, BrokenPipeError, ConnectionResetError):
|
474
|
+
# 客户端断开连接时会触发这些异常
|
475
|
+
self.print_out("SSE client disconnected.")
|
476
|
+
# 这里可以进行一些清理工作(如果需要)
|
477
|
+
except Exception as e:
|
478
|
+
# 捕获其他潜在错误
|
479
|
+
self.print_out(f"Error in SSE stream: {e}")
|
480
|
+
finally:
|
481
|
+
# 确保循环退出时会执行一些操作(如果需要)
|
482
|
+
self.print_out("SSE stream loop finished.")
|
422
483
|
|
423
484
|
|
424
485
|
bottle_app_instance = bottle_app()
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# podflow/httpfs/download_bar.py
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
from podflow import gVar
|
5
|
+
|
6
|
+
|
7
|
+
def download_bar(
|
8
|
+
mod=0,
|
9
|
+
per=0,
|
10
|
+
retime="00:00",
|
11
|
+
speed=" 0.00 B",
|
12
|
+
status="准备中",
|
13
|
+
idname="",
|
14
|
+
nametext="",
|
15
|
+
file="",
|
16
|
+
):
|
17
|
+
if mod == 0:
|
18
|
+
gVar.index_message["download"].append(
|
19
|
+
[
|
20
|
+
per,
|
21
|
+
retime,
|
22
|
+
speed,
|
23
|
+
status,
|
24
|
+
idname,
|
25
|
+
nametext,
|
26
|
+
file,
|
27
|
+
]
|
28
|
+
)
|
29
|
+
elif mod == 1 and gVar.index_message["download"]:
|
30
|
+
gVar.index_message["download"][-1][0] = per
|
31
|
+
gVar.index_message["download"][-1][1] = retime
|
32
|
+
gVar.index_message["download"][-1][2] = speed
|
33
|
+
gVar.index_message["download"][-1][3] = status
|
34
|
+
elif mod == 2 and gVar.index_message["download"]:
|
35
|
+
gVar.index_message["download"][-1][3] = status
|
podflow/main_podcast.py
CHANGED
@@ -69,16 +69,19 @@ def main_podcast():
|
|
69
69
|
# 初始化
|
70
70
|
build_original()
|
71
71
|
# http共享
|
72
|
-
port = gVar.config
|
72
|
+
port = gVar.config.get("port", 8080) # 使用 .get 获取端口
|
73
73
|
hostip = "0.0.0.0"
|
74
|
-
|
75
|
-
|
76
|
-
|
74
|
+
|
75
|
+
if port_judge(hostip, port): # 假设 port_judge 存在
|
76
|
+
# 设置路由 (确保此时 gVar.config 等已就绪)
|
77
|
+
bottle_app_instance.setup_routes(upload=False) # 或者根据需要设置为 True
|
78
|
+
|
77
79
|
# 设置logname
|
78
80
|
bottle_app_instance.set_logname(
|
79
81
|
logname="httpfs.log",
|
80
|
-
http_fs=gVar.config
|
82
|
+
http_fs=gVar.config.get("httpfs", False), # 使用 .get
|
81
83
|
)
|
84
|
+
|
82
85
|
# 启动 CherryPy 服务器
|
83
86
|
cherrypy.tree.graft(
|
84
87
|
bottle_app_instance.app_bottle
|
@@ -92,13 +95,15 @@ def main_podcast():
|
|
92
95
|
"log.screen": False, # 禁用屏幕日志输出
|
93
96
|
"log.access_file": "", # 关闭访问日志
|
94
97
|
"log.error_file": "", # 关闭错误日志
|
98
|
+
# 添加线程池配置,对于长连接 (SSE) 可能有帮助
|
99
|
+
'server.thread_pool': 30 # 示例值,根据需要调整
|
95
100
|
}
|
96
101
|
}
|
97
102
|
)
|
98
103
|
cherrypy.engine.start() # 启动 CherryPy 服务器
|
99
104
|
time_print(f"HTTP服务器启动, 端口: \033[32m{port}\033[0m")
|
100
|
-
if parse.index:
|
101
|
-
open_url(f"{gVar.config['address']}/index")
|
105
|
+
if parse.index: # 假设 parse 已定义
|
106
|
+
open_url(f"{gVar.config['address']}/index") # 假设 open_url 已定义
|
102
107
|
if parse.httpfs: # HttpFS参数判断, 是否继续运行
|
103
108
|
cherrypy.engine.block() # 阻止程序退出, 保持HTTP服务运行
|
104
109
|
sys.exit(0)
|