-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
2752 lines (2380 loc) · 139 KB
/
Copy pathmain.py
File metadata and controls
2752 lines (2380 loc) · 139 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- encoding: utf-8 -*-
"""
直播錄製程式 - 主程式模組 (優化版)
Author: SAOJSM
GitHub: https://github.com/SAOJSM
Date: 2025-03-18 05:40:00
Update: 2025-08-15 12:00:00
Copyright (c) 2025-2025 by SAOJSM, All Rights Reserved.
Function: Record live stream video.
主要功能:
1. 多平台直播監控與錄製
2. 自動檔案命名與重複處理
3. 視頻格式轉換與分段
4. 推送通知功能
5. 代理支援與錯誤處理
v4.1.0 更新內容:
- 重構程式碼結構,添加詳細註解和文檔
- 優化檔案命名邏輯,統一控制重複檔案處理
- 改進錯誤處理和日誌記錄
- 增強視頻處理功能的穩定性
- 優化推送通知機制
- 提升程式碼可讀性和維護性
"""
# ==================== 標準庫導入 ====================
import asyncio
import os
import sys
import builtins
import subprocess
import signal
import threading
import time
import datetime
import re
import shutil
import random
import uuid
from pathlib import Path
import urllib.parse
import urllib.request
from urllib.error import URLError, HTTPError
from typing import Any
import configparser
# ==================== 第三方庫導入 ====================
from streamget import spider, stream
from streamget.proxy import ProxyDetector
from streamget.utils import logger
from streamget import utils
from msg_push import (
dingtalk, xizhi, tg_bot, send_email, bark, ntfy
)
from ffmpeg_install import (
check_ffmpeg, ffmpeg_path, current_env_path
)
# ==================== 程式版本與平台資訊 ====================
version = "v4.1.0"
platforms = ("\n國內站點:抖音|快手|虎牙|鬥魚|YY|B站|小紅書|bigo|blued|網易CC|千度熱播|貓耳FM|Look|TwitCasting|百度|微博|"
"酷狗|花椒|流星|Acfun|暢聊|映客|音播|知乎|嗨秀|VV星球|17Live|浪Live|漂漂|六間房|樂嗨|花貓|淘寶|京東"
"\n海外站點:TikTok|SOOP|PandaTV|WinkTV|FlexTV|PopkonTV|TwitchTV|LiveMe|ShowRoom|CHZZK|Shopee|"
"Youtube|Faceit")
# ==================== 全域變數初始化 ====================
# 錄製狀態管理
recording = set() # 正在錄製的直播間集合
recording_time_list = {} # 錄製時間記錄字典
# 錯誤處理相關
error_count = 0 # 瞬時錯誤計數
pre_max_request = 10 # 前一次的最大請求數
max_request_lock = threading.Lock() # 最大請求數鎖
error_window = [] # 錯誤窗口列表
error_window_size = 10 # 錯誤窗口大小
error_threshold = 5 # 錯誤閾值
# 監控狀態管理
monitoring = 0 # 監控中的直播間數量
running_list = [] # 正在運行的URL列表
url_tuples_list = [] # URL元組列表
url_comments = [] # 被註釋的URL列表
text_no_repeat_url = [] # 去重後的URL列表
need_update_line_list = [] # 需要更新的行列表
not_record_list = [] # 不錄製的URL列表
# 程式狀態標誌
create_var = locals() # 動態變數容器
first_start = True # 首次啟動標誌
exit_recording = False # 退出錄製標誌
first_run = True # 首次運行標誌
global_proxy = False # 全域代理標誌
start_display_time = datetime.datetime.now() # 顯示開始時間
# ==================== 路徑與配置 ====================
script_path = os.path.split(os.path.realpath(sys.argv[0]))[0] # 腳本路徑
config_file = f'{script_path}/config/config.ini' # 配置檔案路徑
url_config_file = f'{script_path}/config/URL_config.ini' # URL配置檔案路徑
backup_dir = f'{script_path}/backup_config' # 備份目錄路徑
text_encoding = 'utf-8-sig'
rstr = r"[\/\\\:\*\??\"\<\>\|&#.。,, ~!· ]"
default_path = f'{script_path}/downloads'
os.makedirs(default_path, exist_ok=True)
file_update_lock = threading.Lock()
os_type = os.name
clear_command = "cls" if os_type == 'nt' else "clear"
color_obj = utils.Color()
os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path
# ==================== 信號處理 ====================
def signal_handler(_signal, _frame):
"""
信號處理函數,用於優雅地退出程式
參數:
_signal: 接收到的信號
_frame: 當前執行框架
"""
sys.exit(0)
signal.signal(signal.SIGTERM, signal_handler)
# ==================== 檔案命名相關函數 ====================
def get_formatted_filename(anchor_name: str, title_in_name: str = "") -> str:
"""
根據記憶中的要求生成檔案名稱:
主播名稱 + 空格 + 日期(YYYYMMDD),如有重複則加上-1,-2等後綴
如果時間在早上6點以前,使用前一天的日期
這個函數實現了統一的檔案命名規則,確保檔案名稱的一致性
參數:
anchor_name (str): 主播名稱
title_in_name (str): 直播標題(此版本中不使用,保留參數以維持相容性)
返回:
str: 格式化的檔名(不含副檔名)
範例:
get_formatted_filename("主播名") -> "主播名 20250815"
"""
# 取得目前時間
current_time = datetime.datetime.now()
# 如果時間在早上6點以前,則使用前一天的日期
# 這樣可以確保深夜直播的檔案使用正確的日期
if current_time.hour < 6:
current_time = current_time - datetime.timedelta(days=1)
# 格式化日期為 YYYYMMDD 格式
date_str = current_time.strftime('%Y%m%d')
# 基本檔名: 主播名稱 + 空格 + 日期
base_filename = f"{anchor_name} {date_str}"
return base_filename
def get_segment_base_filename(base_path: str, anchor_name: str, title_in_name: str, extension: str) -> tuple[str, int]:
"""
為分段錄製獲取基本檔名,確保分段檔案使用連續編號
這個函數專門用於分段錄製,它會找到下一個可用的編號,
確保分段檔案按照 -1, -2, -3, -4... 的順序連續編號
參數:
base_path (str): 檔案所在的目錄路徑
anchor_name (str): 主播名稱
title_in_name (str): 直播標題
extension (str): 副檔名
返回:
tuple[str, int]: (基本檔名, 起始編號)
"""
base_filename = get_formatted_filename(anchor_name, title_in_name)
# 找到下一個可用的編號
counter = 1
while True:
test_filename = f"{base_filename}-{counter}.{extension}"
if not os.path.exists(f"{base_path}/{test_filename}"):
# 找到第一個不存在的編號,從這個編號開始
break
counter += 1
# 返回基本檔名和起始編號
return base_filename, counter
def get_non_duplicate_filename(base_path: str, base_filename: str, extension: str) -> str:
"""
生成不重複的檔案名稱,如果檔案已存在則自動添加遞增編號
這個函數是檔案命名的核心控制點,確保不會覆蓋現有檔案
根據記憶中的要求,重複檔案應該按照 -1, -2, -3... 的順序編號
參數:
base_path (str): 檔案所在的目錄路徑
base_filename (str): 基本檔名(不含副檔名)
extension (str): 副檔名(如 'ts', 'mp4' 等,不含點)
返回:
str: 完整的檔案名稱(不含路徑,但包含副檔名)
範例:
get_non_duplicate_filename("/path", "主播名 20250815", "mp4")
-> "主播名 20250815.mp4" (如果不存在)
-> "主播名 20250815-1.mp4" (如果原檔案存在)
"""
# 檢查基本檔名是否已經包含編號,如果有則移除
# 這確保我們總是從原始檔名開始計算編號
if "-" in base_filename:
parts = base_filename.split("-")
if len(parts) > 1 and parts[-1].isdigit():
# 如果最後一部分是數字,則認為這是一個已經有編號的檔名
# 我們需要去掉這個編號,使用原始的基本檔名
base_filename = "-".join(parts[:-1])
# 先檢查基本檔名是否可用
filename = f"{base_filename}.{extension}"
full_path = f"{base_path}/{filename}"
# 如果檔案不存在,直接返回基本檔名
if not os.path.exists(full_path):
return filename
# 如果檔案存在,則嘗試添加遞增編號,從1開始
counter = 1
# 持續檢查帶編號的檔案名,直到找到不存在的檔名
while True:
# 生成帶編號的檔名格式: "基本檔名-編號.副檔名"
filename = f"{base_filename}-{counter}.{extension}"
full_path = f"{base_path}/{filename}"
# 如果這個檔名不存在,就使用它
if not os.path.exists(full_path):
return filename
# 否則繼續嘗試下一個編號
counter += 1
# ==================== 顯示資訊相關函數 ====================
def display_info() -> None:
"""
顯示程式運行狀態資訊的函數
這個函數在後台執行緒中運行,定期更新並顯示:
- 監測的直播間數量
- 網路執行緒數
- 代理設定狀態
- 錄製參數配置
- 正在錄製的直播列表
- 錄製時長統計
"""
global start_display_time
# 初始等待,讓其他組件先啟動
time.sleep(5)
while True:
try:
time.sleep(5)
# 如果不是後台運行模式,清屏顯示最新資訊
if Path(sys.executable).name != 'pythonw.exe':
os.system(clear_command)
# 顯示基本監控資訊
print(f"\r共監測{monitoring}個直播中", end=" | ")
print(f"同一時間訪問網路的執行緒數: {max_request}", end=" | ")
print(f"是否開啟代理錄製: {'是' if use_proxy else '否'}", end=" | ")
# 顯示分段錄製設定
if split_video_by_time:
print(f"錄製分段開啟: {split_time}秒", end=" | ")
else:
print(f"錄製分段開啟: 否", end=" | ")
# 顯示時間檔案生成設定
if create_time_file:
print(f"是否產生時間檔案: 是", end=" | ")
# 顯示錄製參數
print(f"錄製視訊質量為: {video_record_quality}", end=" | ")
print(f"錄製視訊格式為: {video_save_type}", end=" | ")
print(f"目前瞬時錯誤數為: {error_count}", end=" | ")
# 顯示當前時間
now = time.strftime("%H:%M:%S", time.localtime())
print(f"目前時間: {now}")
# 根據錄製狀態顯示不同資訊
if len(recording) == 0:
time.sleep(5)
if monitoring == 0:
print("\r沒有正在監測和錄製的直播")
else:
print(f"\r沒有正在錄製的直播 循環監測間隔時間:{delay_default}秒")
else:
# 顯示正在錄製的直播詳細資訊
now_time = datetime.datetime.now()
print("x" * 60)
# 去重並顯示錄製列表
no_repeat_recording = list(set(recording))
print(f"正在錄製{len(no_repeat_recording)}個直播: ")
# 顯示每個正在錄製的直播的詳細資訊
for recording_live in no_repeat_recording:
rt, qa = recording_time_list[recording_live]
have_record_time = now_time - rt
print(f"{recording_live}[{qa}] 正在錄製中 {str(have_record_time).split('.')[0]}")
print("x" * 60)
start_display_time = now_time
except Exception as e:
logger.error(f"顯示資訊錯誤: {e} 發生錯誤的行數: {e.__traceback__.tb_lineno}")
# ==================== 檔案操作相關函數 ====================
def update_file(file_path: str, old_str: str, new_str: str, start_str: str = None) -> str | None:
"""
更新檔案中的指定內容
這個函數用於安全地更新配置檔案,支援字符串替換和行前綴添加
使用檔案鎖確保多執行緒環境下的檔案操作安全性
參數:
file_path (str): 要更新的檔案路徑
old_str (str): 要替換的舊字符串
new_str (str): 替換後的新字符串
start_str (str, optional): 要添加到行首的前綴字符串
返回:
str | None: 返回新字符串,如果操作失敗則返回舊字符串
"""
# 如果新舊字符串相同且沒有前綴要添加,直接返回
if old_str == new_str and start_str is None:
return old_str
# 使用檔案鎖確保操作的原子性
with file_update_lock:
file_data = []
# 讀取檔案內容並進行替換
with open(file_path, "r", encoding=text_encoding) as f:
try:
for text_line in f:
# 如果找到要替換的字符串
if old_str in text_line:
text_line = text_line.replace(old_str, new_str)
# 如果需要添加前綴
if start_str:
text_line = f'{start_str}{text_line}'
# 避免重複行
if text_line not in file_data:
file_data.append(text_line)
except RuntimeError as e:
logger.error(f"檔案更新錯誤: {e} 發生錯誤的行數: {e.__traceback__.tb_lineno}")
# 如果有備份內容,嘗試恢復
if ini_URL_content:
with open(file_path, "w", encoding=text_encoding) as f2:
f2.write(ini_URL_content)
return old_str
# 將更新後的內容寫回檔案
if file_data:
with open(file_path, "w", encoding=text_encoding) as f:
f.write(''.join(file_data))
return new_str
def delete_line(file_path: str, del_line: str, delete_all: bool = False) -> None:
"""
從檔案中刪除包含指定字符串的行
這個函數用於清理配置檔案中的重複或無效行
使用檔案鎖確保操作的安全性
參數:
file_path (str): 要操作的檔案路徑
del_line (str): 要刪除的行中包含的字符串
delete_all (bool): 是否刪除所有匹配的行,預設為False(只刪除第一個匹配的行)
"""
with file_update_lock:
with open(file_path, 'r+', encoding=text_encoding) as f:
lines = f.readlines()
f.seek(0)
f.truncate()
skip_line = False
for txt_line in lines:
# 檢查是否包含要刪除的字符串
if del_line in txt_line:
if delete_all or not skip_line:
skip_line = True
continue # 跳過這一行(即刪除)
else:
skip_line = False
# 保留這一行
f.write(txt_line)
# ==================== 系統相關函數 ====================
def get_startup_info(system_type: str):
"""
根據作業系統類型獲取subprocess的啟動資訊
在Windows系統中,設定不顯示命令行視窗
在其他系統中,使用預設設定
參數:
system_type (str): 作業系統類型 ('nt' 表示Windows)
返回:
subprocess.STARTUPINFO | None: Windows系統返回配置好的STARTUPINFO,其他系統返回None
"""
if system_type == 'nt':
# Windows系統:隱藏命令行視窗
startup_info = subprocess.STARTUPINFO()
startup_info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
else:
# 其他系統:使用預設設定
startup_info = None
return startup_info
# ==================== 視頻處理相關函數 ====================
def segment_video(converts_file_path: str, segment_save_file_path: str, segment_format: str, segment_time: str,
is_original_delete: bool = True) -> None:
"""
將視頻檔案分段處理
這個函數將大型視頻檔案分割成多個較小的段落,便於管理和播放
使用統一的檔案命名規則,確保分段檔案不會覆蓋現有檔案
參數:
converts_file_path (str): 要分段的原始視頻檔案路徑
segment_save_file_path (str): 分段檔案的保存路徑模板
segment_format (str): 分段檔案的格式(如 'mp4', 'flv')
segment_time (str): 每段的時長(秒)
is_original_delete (bool): 是否刪除原始檔案,預設為True
功能:
- 自動檢測視頻總時長
- 按指定時間分割視頻
- 使用統一命名規則避免檔案覆蓋
- 支援MP4格式的faststart優化
"""
try:
# 檢查原始檔案是否存在且不為空
if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0:
# 解析檔案路徑和檔名
dir_path = os.path.dirname(segment_save_file_path)
base_filename = os.path.basename(segment_save_file_path)
# 處理模板檔名中的 "-%d" 佔位符
if "-%d" in base_filename:
base_filename = base_filename.replace("-%d", "")
# 分離檔名和副檔名
extension = os.path.splitext(base_filename)[1][1:] # 獲取副檔名(不含點)
base_name = os.path.splitext(base_filename)[0] # 獲取基本檔名(不含副檔名)
# 使用統一的檔案命名函數獲取不重複的檔名
first_filename = get_non_duplicate_filename(dir_path, base_name, extension)
first_file_path = f"{dir_path}/{first_filename}"
# 構建第一個分段的FFmpeg命令
first_command = [
"ffmpeg",
"-i", converts_file_path,
"-c:v", "copy", # 視頻流複製(不重新編碼)
"-c:a", "copy", # 音頻流複製(不重新編碼)
"-map", "0", # 映射所有流
"-t", segment_time, # 設定分段時長
"-f", segment_format, # 輸出格式
first_file_path,
]
# 執行第一個分段命令
_first_output = subprocess.check_output(
first_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)
)
# 如果視頻長度超過分段時間,則創建後續分段
# 獲取視頻總時長
duration_command = [
"ffprobe",
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
converts_file_path
]
try:
duration_output = subprocess.check_output(
duration_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)
)
duration = float(duration_output.decode('utf-8').strip())
# 如果視頻總時長超過分段時間,則需要創建後續分段
if duration > float(segment_time):
# 獲取第二個檔案的名稱
second_filename = get_non_duplicate_filename(dir_path, base_name, extension)
# 創建第二個檔案
second_command = [
"ffmpeg",
"-i", converts_file_path,
"-c:v", "copy",
"-c:a", "copy",
"-map", "0",
"-ss", segment_time, # 從第一個分段結束的時間開始
"-f", segment_format,
f"{dir_path}/{second_filename}",
]
# 執行第二個命令
_second_output = subprocess.check_output(
second_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)
)
# 如果視頻總時長超過兩個分段時間,則需要創建更多分段
segment_count = 2
current_time = float(segment_time) * 2
while current_time < duration:
# 獲取下一個檔案的名稱
next_filename = get_non_duplicate_filename(dir_path, base_name, extension)
# 創建下一個檔案
next_command = [
"ffmpeg",
"-i", converts_file_path,
"-c:v", "copy",
"-c:a", "copy",
"-map", "0",
"-ss", str(current_time), # 從當前時間開始
"-t", segment_time, # 只取一個分段的時間
"-f", segment_format,
f"{dir_path}/{next_filename}",
]
# 執行命令
_next_output = subprocess.check_output(
next_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)
)
segment_count += 1
current_time += float(segment_time)
except Exception as e:
logger.error(f'Error getting video duration or creating segments: {e}')
# 如果是MP4分段,需要優化每個分段檔案以確保快速開啟
if segment_format == 'mp4':
# 優化第一個檔案
if os.path.exists(first_file_path):
threading.Thread(target=optimize_mp4, args=(first_file_path,)).start()
# 優化其他分段檔案
if duration > float(segment_time):
segment_count = 2
current_time = float(segment_time) * 2
while current_time < duration:
# 構建分段檔案路徑
segment_filename = get_non_duplicate_filename(dir_path, base_name, extension)
segment_file_path = f"{dir_path}/{segment_filename}"
if os.path.exists(segment_file_path):
threading.Thread(target=optimize_mp4, args=(segment_file_path,)).start()
segment_count += 1
current_time += float(segment_time)
if is_original_delete:
time.sleep(1)
if os.path.exists(converts_file_path):
os.remove(converts_file_path)
except subprocess.CalledProcessError as e:
logger.error(f'Error occurred during conversion: {e}')
except Exception as e:
logger.error(f'An unknown error occurred: {e}')
def converts_mp4(converts_file_path: str, is_original_delete: bool = True) -> None:
"""
將視頻檔案轉換為MP4格式
這個函數支援兩種轉換模式:
1. 直接容器轉換(不重新編碼,速度快)
2. H.264重新編碼(相容性好,檔案較小)
轉換後的檔案會自動添加faststart標誌,確保網路播放時的快速啟動
參數:
converts_file_path (str): 要轉換的原始檔案路徑
is_original_delete (bool): 轉換完成後是否刪除原始檔案,預設為True
功能:
- 支援H.264重新編碼或直接容器轉換
- 自動添加faststart優化
- 轉換完成後可選擇刪除原檔案
- 完整的錯誤處理和日誌記錄
"""
try:
# 檢查原始檔案是否存在且不為空
if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0:
# 根據配置選擇轉換模式
if converts_to_h264:
# H.264重新編碼模式:提供更好的相容性和壓縮率
color_obj.print_colored(f"正在轉碼為MP4格式並重新編碼為h264\n", color_obj.YELLOW)
ffmpeg_command = [
"ffmpeg", "-i", converts_file_path,
"-c:v", "libx264", # 使用H.264編碼器
"-preset", "veryfast", # 編碼速度預設(平衡速度和質量)
"-crf", "23", # 恆定質量因子(23是較好的平衡點)
"-vf", "format=yuv420p", # 確保色彩格式相容性
"-c:a", "copy", # 音頻直接複製
"-f", "mp4", # 輸出格式
"-movflags", "+faststart", # 添加faststart標誌以確保快速開啟
converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4",
]
else:
# 直接容器轉換模式:速度快,不重新編碼
color_obj.print_colored(f"正在轉碼為MP4格式\n", color_obj.YELLOW)
ffmpeg_command = [
"ffmpeg", "-i", converts_file_path,
"-c:v", "copy", # 視頻流直接複製
"-c:a", "copy", # 音頻流直接複製
"-f", "mp4", # 輸出格式
"-movflags", "+faststart", # 添加faststart標誌以確保快速開啟
converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4",
]
# 執行轉換命令
_output = subprocess.check_output(
ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)
)
# 根據設定決定是否刪除原檔案
if is_original_delete:
time.sleep(1) # 短暫等待確保檔案寫入完成
if os.path.exists(converts_file_path):
os.remove(converts_file_path)
except subprocess.CalledProcessError as e:
logger.error(f'視頻轉換過程中發生錯誤: {e}')
except Exception as e:
logger.error(f'視頻轉換時發生未知錯誤: {e}')
def fix_h264_stream_errors(ffmpeg_command: list) -> list:
"""
為FFmpeg命令添加H.264流錯誤修復參數
參數:
ffmpeg_command: 原始FFmpeg命令列表
返回:
修正後的FFmpeg命令列表
"""
# 簡化的H.264錯誤修復 - 只添加最關鍵的參數
# 這些參數將被插入到基礎命令中,而不是在輸入後
return ffmpeg_command # 暫時返回原命令,錯誤處理已在基礎命令中設定
def optimize_mp4(mp4_file_path: str) -> None:
"""
優化MP4檔案,添加faststart標誌以確保快速開啟
這個函數將MP4檔案的metadata移動到檔案開頭,
使得檔案可以在網路環境中邊下載邊播放,提升用戶體驗
參數:
mp4_file_path (str): 要優化的MP4檔案路徑
功能:
- 添加faststart標誌優化檔案結構
- 保持原始視頻和音頻品質(不重新編碼)
- 使用臨時檔案確保操作安全性
- 完整的錯誤處理和清理機制
"""
try:
# 檢查檔案是否存在且不為空
if os.path.exists(mp4_file_path) and os.path.getsize(mp4_file_path) > 0:
temp_file_path = mp4_file_path + ".temp"
color_obj.print_colored(f"正在優化MP4檔案以確保快速開啟\n", color_obj.YELLOW)
# 構建FFmpeg優化命令
ffmpeg_command = [
"ffmpeg", "-i", mp4_file_path,
"-c:v", "copy", # 視頻流直接複製(不重新編碼)
"-c:a", "copy", # 音頻流直接複製(不重新編碼)
"-f", "mp4", # 輸出格式
"-movflags", "+faststart", # 添加faststart標誌進行優化
temp_file_path,
]
# 執行優化命令
_output = subprocess.check_output(
ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)
)
# 安全地替換原檔案
if os.path.exists(temp_file_path):
os.remove(mp4_file_path) # 刪除原檔案
os.rename(temp_file_path, mp4_file_path) # 重命名臨時檔案
except subprocess.CalledProcessError as e:
logger.error(f'MP4優化過程中發生錯誤: {e}')
# 清理臨時檔案
temp_file = mp4_file_path + ".temp"
if os.path.exists(temp_file):
os.remove(temp_file)
except Exception as e:
logger.error(f'MP4優化時發生未知錯誤: {e}')
# 清理臨時檔案
temp_file = mp4_file_path + ".temp"
if os.path.exists(temp_file):
os.remove(temp_file)
def converts_m4a(converts_file_path: str, is_original_delete: bool = True) -> None:
try:
if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0:
_output = subprocess.check_output([
"ffmpeg", "-i", converts_file_path,
"-n", "-vn",
"-c:a", "aac", "-bsf:a", "aac_adtstoasc", "-ab", "320k",
converts_file_path.rsplit('.', maxsplit=1)[0] + ".m4a",
], stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type))
if is_original_delete:
time.sleep(1)
if os.path.exists(converts_file_path):
os.remove(converts_file_path)
except subprocess.CalledProcessError as e:
logger.error(f'Error occurred during conversion: {e}')
except Exception as e:
logger.error(f'An unknown error occurred: {e}')
def generate_subtitles(record_name: str, ass_filename: str, sub_format: str = 'srt') -> None:
index_time = 0
today = datetime.datetime.now()
re_datatime = today.strftime('%Y-%m-%d %H:%M:%S')
def transform_int_to_time(seconds: int) -> str:
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
return f"{h:02d}:{m:02d}:{s:02d}"
while True:
index_time += 1
txt = str(index_time) + "\n" + transform_int_to_time(index_time) + ',000 --> ' + transform_int_to_time(
index_time + 1) + ',000' + "\n" + str(re_datatime) + "\n\n"
with open(f"{ass_filename}.{sub_format.lower()}", 'a', encoding=text_encoding) as f:
f.write(txt)
if record_name not in recording:
return
time.sleep(1)
today = datetime.datetime.now()
re_datatime = today.strftime('%Y-%m-%d %H:%M:%S')
# ==================== 錯誤處理與監控函數 ====================
def adjust_max_request() -> None:
"""
動態調整最大網路請求執行緒數
這個函數根據錯誤率自動調整併發請求數量,以平衡效能和穩定性:
- 錯誤率高時減少併發數,提高穩定性
- 錯誤率低時增加併發數,提高效率
- 使用滑動窗口計算錯誤率,避免短期波動影響
全域變數:
max_request: 當前最大請求數
error_count: 當前錯誤計數
pre_max_request: 前一次的最大請求數
error_window: 錯誤窗口列表
調整策略:
- 錯誤率 > 閾值:減少併發數(最少保持1個)
- 錯誤率 < 閾值/2:增加併發數(不超過預設值)
- 其他情況:保持當前併發數
"""
global max_request, error_count, pre_max_request, error_window
preset = max_request # 保存初始設定值
while True:
time.sleep(5) # 每5秒檢查一次
with max_request_lock:
# 計算當前錯誤率
if error_window:
error_rate = sum(error_window) / len(error_window)
else:
error_rate = 0
# 根據錯誤率調整併發數
if error_rate > error_threshold:
# 錯誤率過高,減少併發數(最少保持1個)
max_request = max(1, max_request - 1)
elif error_rate < error_threshold / 2 and max_request < preset:
# 錯誤率較低且未達到預設值,增加併發數
max_request += 1
# 其他情況保持不變
# 如果併發數發生變化,通知用戶
if pre_max_request != max_request:
pre_max_request = max_request
print(f"\r同一時間訪問網路的執行緒數動態改為 {max_request}")
# 更新錯誤窗口(滑動窗口機制)
error_window.append(error_count)
if len(error_window) > error_window_size:
error_window.pop(0) # 移除最舊的錯誤記錄
error_count = 0 # 重置當前週期的錯誤計數
# ==================== 推送通知相關函數 ====================
def push_message(record_name: str, live_url: str, content: str) -> None:
"""
發送直播狀態推送通知到各個平台
支援多種推送平台:微信、釘釘、郵箱、Telegram、BARK、NTFY
根據配置自動選擇啟用的推送平台進行通知
參數:
record_name (str): 錄製名稱(主播名稱)
live_url (str): 直播間URL
content (str): 推送內容
功能:
- 支援多平台同時推送
- 自動錯誤處理和重試
- 詳細的推送結果統計
- 靈活的推送內容自定義
"""
# 設定推送標題,使用自定義標題或預設標題
msg_title = push_message_title.strip() or "直播間狀態更新通知"
# 定義各平台的推送函數
push_functions = {
'微信': lambda: xizhi(xizhi_api_url, msg_title, content),
'釘釘': lambda: dingtalk(dingtalk_api_url, content, dingtalk_phone_num, dingtalk_is_atall),
'郵箱': lambda: send_email(
email_host, login_email, email_password, sender_email, sender_name,
to_email, msg_title, content, smtp_port, open_smtp_ssl
),
'TG': lambda: tg_bot(tg_chat_id, tg_token, content),
'BARK': lambda: bark(
bark_msg_api, title=msg_title, content=content, level=bark_msg_level, sound=bark_msg_ring
),
'NTFY': lambda: ntfy(
ntfy_api, title=msg_title, content=content, tags=ntfy_tags, action_url=live_url, email=ntfy_email
),
}
# 遍歷所有推送平台,根據配置進行推送
for platform, func in push_functions.items():
if platform in live_status_push.upper():
try:
# 執行推送
result = func()
# 顯示推送結果統計
print(f'提示資訊:已經將[{record_name}]直播狀態訊息推送至你的{platform},'
f' 成功{len(result["success"])}, 失敗{len(result["error"])}')
except Exception as e:
# 推送失敗時的錯誤處理
color_obj.print_colored(f"直播訊息推送到{platform}失敗: {e}", color_obj.RED)
def run_script(command: str) -> None:
try:
process = subprocess.Popen(
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=get_startup_info(os_type)
)
stdout, stderr = process.communicate()
stdout_decoded = stdout.decode('utf-8')
stderr_decoded = stderr.decode('utf-8')
if stdout_decoded.strip():
print(stdout_decoded)
if stderr_decoded.strip():
print(stderr_decoded)
except PermissionError as e:
logger.error(e)
logger.error(f'指令碼無執行許可權!, 若是Linux環境, 請先執行:chmod +x your_script.sh 授予指令碼可執行許可權')
except OSError as e:
logger.error(e)
logger.error('Please add `#!/bin/bash` at the beginning of your bash script file.')
def clear_record_info(record_name: str, record_url: str) -> None:
"""
清理錄製資訊,從各個列表中移除指定的錄製項目
這個函數負責清理錄製結束或被取消的直播間相關資訊,
確保程式狀態的一致性和記憶體的有效使用
參數:
record_name (str): 錄製名稱
record_url (str): 直播間URL
功能:
- 從錄製集合中移除項目
- 更新監控計數
- 清理運行列表
- 提供用戶反饋
"""
global monitoring
# 從正在錄製的集合中移除
recording.discard(record_name)
# 如果URL在註釋列表和運行列表中,進行清理
if record_url in url_comments and record_url in running_list:
running_list.remove(record_url)
monitoring -= 1
color_obj.print_colored(f"[{record_name}]已經從錄製列表中移除\n", color_obj.YELLOW)
def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, save_type: str,
script_command: str | None = None, platform: str = "", proxy_address: str = None) -> bool:
save_file_path = ffmpeg_command[-1]
# 修改為捕獲stderr以便監控錯誤訊息
process = subprocess.Popen(
ffmpeg_command, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE,
startupinfo=get_startup_info(os_type), text=True, bufsize=1
)
subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0]
subs_thread_name = f'subs_{Path(subs_file_path).name}'
if create_time_file and not split_video_by_time and '音訊' not in save_type:
create_var[subs_thread_name] = threading.Thread(
target=generate_subtitles, args=(record_name, subs_file_path)
)
create_var[subs_thread_name].daemon = True
create_var[subs_thread_name].start()
while process.poll() is None:
if record_url in url_comments or exit_recording:
color_obj.print_colored(f"[{record_name}]錄製時已被註釋,本條執行緒將會退出", color_obj.YELLOW)
clear_record_info(record_name, record_url)
# process.terminate()
if os.name == 'nt':
if process.stdin:
process.stdin.write(b'q')
process.stdin.close()
else:
process.send_signal(signal.SIGINT)
process.wait()
return True # 只有被手動註釋時才真正退出執行緒
# 簡化的封包監控(預設啟用,不記錄)
if enable_packet_monitoring:
try:
if process.stderr and process.stderr.readable():
# 使用非阻塞讀取檢查是否有錯誤訊息
import select
if hasattr(select, 'select'): # Unix系統
ready, _, _ = select.select([process.stderr], [], [], 0)
if ready:
error_line = process.stderr.readline()
# 靜默監控損壞封包,不輸出任何記錄
if error_line and 'corrupt' in error_line.lower():
pass # 檢測到損壞封包,但不記錄
except Exception as e:
pass # 靜默處理監控錯誤
time.sleep(1)
return_code = process.returncode
stop_time = time.strftime('%Y-%m-%d %H:%M:%S')
if return_code == 0:
if converts_to_mp4 and save_type == 'TS':
if split_video_by_time:
file_paths = utils.get_file_paths(os.path.dirname(save_file_path))
prefix = os.path.basename(save_file_path).rsplit('_', maxsplit=1)[0]
for path in file_paths:
if prefix in path: