mirror of
https://github.com/ihmily/DouyinLiveRecorder.git
synced 2025-12-26 05:48:32 +08:00
1589 lines
80 KiB
Python
1589 lines
80 KiB
Python
# -*- encoding: utf-8 -*-
|
||
|
||
"""
|
||
Author: Hmily
|
||
GitHub: https://github.com/ihmily
|
||
Date: 2023-07-17 23:52:05
|
||
Update: 2024-10-05 00:57:00
|
||
Copyright (c) 2023-2024 by Hmily, All Rights Reserved.
|
||
Function: Record live stream video.
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import subprocess
|
||
import signal
|
||
import threading
|
||
import time
|
||
import datetime
|
||
import re
|
||
import shutil
|
||
import random
|
||
from pathlib import Path
|
||
import urllib.parse
|
||
import urllib.request
|
||
from urllib.error import URLError, HTTPError
|
||
from typing import Any, Union
|
||
import configparser
|
||
from douyinliverecorder import spider
|
||
from douyinliverecorder import stream
|
||
from douyinliverecorder.utils import (
|
||
logger, check_md5, update_config,
|
||
get_file_paths, remove_emojis
|
||
)
|
||
from douyinliverecorder.msg_push import (
|
||
dingtalk, xizhi, tg_bot, email_message, bark
|
||
)
|
||
|
||
version = "v3.0.8"
|
||
platforms = ("\n国内站点:抖音|快手|虎牙|斗鱼|YY|B站|小红书|bigo|blued|网易CC|千度热播|猫耳FM|Look|TwitCasting|百度|微博|"
|
||
"酷狗|花椒|流星|Acfun|时光|映客|音播|知乎"
|
||
"\n海外站点:TikTok|AfreecaTV|PandaTV|WinkTV|FlexTV|PopkonTV|TwitchTV|LiveMe|ShowRoom|CHZZK")
|
||
|
||
recording = set()
|
||
warning_count = 0
|
||
max_request = 0
|
||
pre_max_request = 0
|
||
monitoring = 0
|
||
running_list = []
|
||
url_tuples_list = []
|
||
url_comments = []
|
||
text_no_repeat_url = []
|
||
create_var = locals()
|
||
first_start = True
|
||
need_update_line_list = []
|
||
first_run = True
|
||
not_record_list = []
|
||
start_display_time = datetime.datetime.now()
|
||
global_proxy = False
|
||
recording_time_list = {}
|
||
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'
|
||
backup_dir = f'{script_path}/backup_config'
|
||
text_encoding = 'utf-8-sig'
|
||
rstr = r"[\/\\\:\*\?\"\<\>\|&#.。,, ]"
|
||
ffmpeg_path = f"{script_path}/ffmpeg.exe"
|
||
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"
|
||
|
||
|
||
def signal_handler(_signal, _frame):
|
||
sys.exit(0)
|
||
|
||
|
||
signal.signal(signal.SIGTERM, signal_handler)
|
||
|
||
|
||
def display_info():
|
||
global start_display_time
|
||
time.sleep(5)
|
||
while True:
|
||
try:
|
||
time.sleep(5)
|
||
os.system(clear_command)
|
||
print(f"\r共监测{monitoring}个直播中", end=" | ")
|
||
print(f"同一时间访问网络的线程数: {max_request}", end=" | ")
|
||
if len(video_save_path) > 0:
|
||
if not os.path.exists(video_save_path):
|
||
print("配置文件里,直播保存路径并不存在,请重新输入一个正确的路径.或留空表示当前目录,按回车退出")
|
||
input("程序结束")
|
||
sys.exit(0)
|
||
|
||
print(f"是否开启代理录制: {'是' if use_proxy else '否'}", end=" | ")
|
||
if split_video_by_time:
|
||
print(f"录制分段开启: {split_time}秒", end=" | ")
|
||
print(f"是否生成时间文件: {'是' if create_time_file else '否'}", end=" | ")
|
||
print(f"录制视频质量为: {video_record_quality}", end=" | ")
|
||
print(f"录制视频格式为: {video_save_type}", end=" | ")
|
||
print(f"目前瞬时错误数为: {warning_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('\n本软件已运行:'+str(now_time - start_display_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) -> Union[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}'
|
||
file_data += 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(file_data)
|
||
return new_str
|
||
|
||
|
||
def delete_line(file_path: str, del_line: str):
|
||
with file_update_lock:
|
||
with open(file_path, 'r+', encoding=text_encoding) as f:
|
||
lines = f.readlines()
|
||
f.seek(0)
|
||
f.truncate()
|
||
for txt_line in lines:
|
||
if del_line not in txt_line:
|
||
f.write(txt_line)
|
||
|
||
|
||
def converts_mp4(address: str, is_original_delete: bool = True):
|
||
_output = subprocess.check_output([
|
||
"ffmpeg", "-i", address,
|
||
"-c:v", "copy",
|
||
"-c:a", "copy",
|
||
"-f", "mp4", address.rsplit('.', maxsplit=1)[0] + ".mp4",
|
||
], stderr=subprocess.STDOUT)
|
||
if is_original_delete:
|
||
time.sleep(1)
|
||
if os.path.exists(address):
|
||
os.remove(address)
|
||
|
||
|
||
def converts_m4a(address: str, is_original_delete: bool = True):
|
||
_output = subprocess.check_output([
|
||
"ffmpeg", "-i", address,
|
||
"-n", "-vn",
|
||
"-c:a", "aac", "-bsf:a", "aac_adtstoasc", "-ab", "320k",
|
||
address.rsplit('.', maxsplit=1)[0] + ".m4a",
|
||
], stderr=subprocess.STDOUT)
|
||
if is_original_delete:
|
||
time.sleep(1)
|
||
if os.path.exists(address):
|
||
os.remove(address)
|
||
|
||
|
||
def generate_subtitles(record_name: str, ass_filename: str, sub_format: str = 'srt'):
|
||
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 change_max_connect():
|
||
global max_request, warning_count, pre_max_request
|
||
preset = max_request
|
||
start_time = time.time()
|
||
pre_max_request = max_request
|
||
|
||
while True:
|
||
time.sleep(5)
|
||
if 10 <= warning_count <= 20:
|
||
if preset > 5:
|
||
max_request = 5
|
||
else:
|
||
max_request //= 2
|
||
if max_request > 0:
|
||
max_request = preset
|
||
else:
|
||
preset = 1
|
||
time.sleep(5)
|
||
|
||
elif 20 < warning_count:
|
||
max_request = 1
|
||
time.sleep(10)
|
||
|
||
elif warning_count < 10 and time.time() - start_time > 60:
|
||
max_request = preset
|
||
start_time = time.time()
|
||
|
||
warning_count = 0
|
||
if pre_max_request != max_request:
|
||
pre_max_request = max_request
|
||
print("同一时间访问网络的线程数动态改为", max_request)
|
||
|
||
|
||
def push_message(content: str) -> Union[str, list]:
|
||
push_pts = []
|
||
if '微信' in live_status_push:
|
||
push_pts.append('微信')
|
||
xizhi(xizhi_api_url, content)
|
||
if '钉钉' in live_status_push:
|
||
push_pts.append('钉钉')
|
||
dingtalk(dingtalk_api_url, content, dingtalk_phone_num)
|
||
if '邮箱' in live_status_push:
|
||
push_pts.append('邮箱')
|
||
email_message(mail_host, mail_password, from_email, to_email, "直播间状态更新通知", content)
|
||
if 'TG' in live_status_push.upper():
|
||
push_pts.append('TG')
|
||
tg_bot(tg_chat_id, tg_token, content)
|
||
if 'BARK' in live_status_push.upper():
|
||
push_pts.append('BARK')
|
||
bark(bark_msg_api, title="直播录制通知", content=content, level=bark_msg_level, sound=bark_msg_ring)
|
||
push_pts = '、'.join(push_pts) if len(push_pts) > 0 else []
|
||
return push_pts
|
||
|
||
|
||
def run_bash(command):
|
||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||
stdout, stderr = process.communicate()
|
||
stdout_decoded = stdout.decode('utf-8')
|
||
stderr_decoded = stderr.decode('utf-8')
|
||
print(stdout_decoded)
|
||
print(stderr_decoded)
|
||
|
||
|
||
def clear_record_info(record_name, record_url):
|
||
global monitoring
|
||
recording.discard(record_name)
|
||
if record_url in url_comments and record_url in running_list:
|
||
running_list.remove(record_url)
|
||
monitoring -= 1
|
||
print(f"[{record_name}]已经从录制列表中移除")
|
||
|
||
|
||
def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, save_type: str,
|
||
bash_file_path: Union[str, None] = None) -> bool:
|
||
|
||
save_path_name = ffmpeg_command[-1]
|
||
process = subprocess.Popen(
|
||
ffmpeg_command, stderr=subprocess.STDOUT
|
||
)
|
||
|
||
subs_file_path = save_path_name.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:
|
||
print(f"[{record_name}]录制时已被注释,本条线程将会退出")
|
||
clear_record_info(record_name, record_url)
|
||
process.terminate()
|
||
process.wait()
|
||
return True
|
||
time.sleep(1)
|
||
|
||
return_code = process.returncode
|
||
stop_time = time.strftime('%Y-%m-%d %H:%M:%S')
|
||
if return_code == 0:
|
||
if ts_to_mp4 and save_type == 'TS':
|
||
if split_video_by_time:
|
||
file_paths = get_file_paths(os.path.dirname(save_path_name))
|
||
prefix = os.path.basename(save_path_name).rsplit('_', maxsplit=1)[0]
|
||
for path in file_paths:
|
||
if prefix in path:
|
||
threading.Thread(target=converts_mp4, args=(path, delete_origin_file)).start()
|
||
else:
|
||
threading.Thread(target=converts_mp4, args=(save_path_name, delete_origin_file)).start()
|
||
print(f"\n{record_name} {stop_time} 直播录制完成\n")
|
||
|
||
if bash_file_path:
|
||
if os_type != 'nt':
|
||
print(f'准备执行自定义Bash脚本,请确认脚本是否有执行权限! 路径:{bash_file_path}')
|
||
bash_command = [bash_file_path, record_name.split(' ', maxsplit=1)[-1], save_path_name, save_type,
|
||
split_video_by_time, ts_to_mp4]
|
||
run_bash(bash_command)
|
||
else:
|
||
print('只支持在Linux环境下执行Bash脚本')
|
||
|
||
else:
|
||
print(f"\n{record_name} {stop_time} 直播录制出错,返回码: {return_code}\n")
|
||
|
||
recording.discard(record_name)
|
||
return False
|
||
|
||
|
||
def start_record(url_data: tuple, count_variable: int = -1):
|
||
global warning_count, video_save_path
|
||
start_pushed = False
|
||
|
||
while True:
|
||
try:
|
||
record_finished = False
|
||
run_once = False
|
||
is_long_url = False
|
||
new_record_url = ''
|
||
count_time = time.time()
|
||
retry = 0
|
||
record_quality, record_url, anchor_name = url_data
|
||
proxy_address = proxy_addr
|
||
platform = '未知平台'
|
||
|
||
if proxy_addr:
|
||
proxy_address = None
|
||
for platform in enable_proxy_platform_list:
|
||
if platform and platform.strip() in url:
|
||
proxy_address = proxy_addr
|
||
break
|
||
|
||
if not proxy_address:
|
||
if extra_enable_proxy_platform_list:
|
||
for pt in extra_enable_proxy_platform_list:
|
||
if pt and pt.strip() in url:
|
||
proxy_address = proxy_addr_bak or None
|
||
|
||
# print(f'\r代理地址:{proxy_address}')
|
||
# print(f'\r全局代理:{global_proxy}')
|
||
while True:
|
||
try:
|
||
port_info = []
|
||
if record_url.find("douyin.com/") > -1:
|
||
platform = '抖音直播'
|
||
with semaphore:
|
||
if 'v.douyin.com' not in record_url:
|
||
json_data = spider.get_douyin_stream_data(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=dy_cookie)
|
||
else:
|
||
json_data = spider.get_douyin_app_stream_data(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=dy_cookie)
|
||
port_info = stream.get_douyin_stream_url(json_data, record_quality)
|
||
|
||
elif record_url.find("https://www.tiktok.com/") > -1:
|
||
platform = 'TikTok直播'
|
||
with semaphore:
|
||
if global_proxy or proxy_address:
|
||
json_data = spider.get_tiktok_stream_data(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=tiktok_cookie)
|
||
port_info = stream.get_tiktok_stream_url(json_data, record_quality)
|
||
else:
|
||
logger.error("错误信息: 网络异常,请检查网络是否能正常访问TikTok平台")
|
||
|
||
elif record_url.find("https://live.kuaishou.com/") > -1:
|
||
platform = '快手直播'
|
||
with semaphore:
|
||
json_data = spider.get_kuaishou_stream_data(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=ks_cookie)
|
||
port_info = stream.get_kuaishou_stream_url(json_data, record_quality)
|
||
|
||
elif record_url.find("https://www.huya.com/") > -1:
|
||
platform = '虎牙直播'
|
||
with semaphore:
|
||
if record_quality not in ['原画', '蓝光', '超清']:
|
||
json_data = spider.get_huya_stream_data(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=hy_cookie)
|
||
port_info = stream.get_huya_stream_url(json_data, record_quality)
|
||
else:
|
||
port_info = spider.get_huya_app_stream_url(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=hy_cookie
|
||
)
|
||
|
||
elif record_url.find("https://www.douyu.com/") > -1:
|
||
platform = '斗鱼直播'
|
||
with semaphore:
|
||
json_data = spider.get_douyu_info_data(
|
||
url=record_url, proxy_addr=proxy_address, cookies=douyu_cookie)
|
||
port_info = stream.get_douyu_stream_url(
|
||
json_data, video_quality=record_quality, cookies=douyu_cookie, proxy_addr=proxy_address
|
||
)
|
||
|
||
elif record_url.find("https://www.yy.com/") > -1:
|
||
platform = 'YY直播'
|
||
with semaphore:
|
||
json_data = spider.get_yy_stream_data(
|
||
url=record_url, proxy_addr=proxy_address, cookies=yy_cookie)
|
||
port_info = stream.get_yy_stream_url(json_data)
|
||
|
||
elif record_url.find("https://live.bilibili.com/") > -1:
|
||
platform = 'B站直播'
|
||
with semaphore:
|
||
json_data = spider.get_bilibili_room_info(
|
||
url=record_url, proxy_addr=proxy_address, cookies=bili_cookie)
|
||
port_info = stream.get_bilibili_stream_url(
|
||
json_data, video_quality=record_quality, cookies=bili_cookie, proxy_addr=proxy_address)
|
||
|
||
elif record_url.find("https://www.redelight.cn/") > -1 or \
|
||
record_url.find("https://www.xiaohongshu.com/") > -1 or \
|
||
record_url.find("http://xhslink.com/") > -1:
|
||
platform = '小红书直播'
|
||
if retry > 1:
|
||
delete_line(url_config_file, record_url)
|
||
if record_url in running_list:
|
||
running_list.remove(record_url)
|
||
not_record_list.append(record_url)
|
||
logger.info(f'{record_url} 小红书直播已结束,停止录制')
|
||
return
|
||
with semaphore:
|
||
port_info = spider.get_xhs_stream_url(
|
||
record_url, proxy_addr=proxy_address, cookies=xhs_cookie)
|
||
retry += 1
|
||
|
||
elif record_url.find("https://www.bigo.tv/") > -1 or record_url.find("slink.bigovideo.tv/") > -1:
|
||
platform = 'Bigo直播'
|
||
with semaphore:
|
||
port_info = spider.get_bigo_stream_url(
|
||
record_url, proxy_addr=proxy_address, cookies=bigo_cookie)
|
||
|
||
elif record_url.find("https://app.blued.cn/") > -1:
|
||
platform = 'Blued直播'
|
||
with semaphore:
|
||
port_info = spider.get_blued_stream_url(
|
||
record_url, proxy_addr=proxy_address, cookies=blued_cookie)
|
||
|
||
elif record_url.find("afreecatv.com/") > -1:
|
||
platform = 'AfreecaTV'
|
||
with semaphore:
|
||
if global_proxy or proxy_address:
|
||
json_data = spider.get_afreecatv_stream_data(
|
||
url=record_url, proxy_addr=proxy_address,
|
||
cookies=afreecatv_cookie,
|
||
username=afreecatv_username,
|
||
password=afreecatv_password
|
||
)
|
||
if json_data and json_data.get('new_cookies', None):
|
||
update_config(config_file, 'Cookie', 'afreecatv_cookie', json_data['new_cookies'])
|
||
port_info = stream.get_stream_url(json_data, record_quality, spec=True)
|
||
else:
|
||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问AfreecaTV平台")
|
||
|
||
elif record_url.find("cc.163.com/") > -1:
|
||
platform = '网易CC直播'
|
||
with semaphore:
|
||
json_data = spider.get_netease_stream_data(url=record_url, cookies=netease_cookie)
|
||
port_info = stream.get_netease_stream_url(json_data, record_quality)
|
||
|
||
elif record_url.find("qiandurebo.com/") > -1:
|
||
platform = '千度热播'
|
||
with semaphore:
|
||
port_info = spider.get_qiandurebo_stream_data(
|
||
url=record_url, proxy_addr=proxy_address, cookies=qiandurebo_cookie)
|
||
|
||
elif record_url.find("www.pandalive.co.kr/") > -1:
|
||
platform = 'PandaTV'
|
||
with semaphore:
|
||
if global_proxy or proxy_address:
|
||
json_data = spider.get_pandatv_stream_data(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=pandatv_cookie
|
||
)
|
||
port_info = stream.get_stream_url(json_data, record_quality, spec=True)
|
||
else:
|
||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PandaTV直播平台")
|
||
|
||
elif record_url.find("fm.missevan.com/") > -1:
|
||
platform = '猫耳FM直播'
|
||
with semaphore:
|
||
port_info = spider.get_maoerfm_stream_url(
|
||
url=record_url, proxy_addr=proxy_address, cookies=maoerfm_cookie)
|
||
|
||
elif record_url.find("www.winktv.co.kr/") > -1:
|
||
platform = 'WinkTV'
|
||
with semaphore:
|
||
if global_proxy or proxy_address:
|
||
json_data = spider.get_winktv_stream_data(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=winktv_cookie)
|
||
port_info = stream.get_stream_url(json_data, record_quality, spec=True)
|
||
else:
|
||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问WinkTV直播平台")
|
||
|
||
elif record_url.find("www.flextv.co.kr/") > -1:
|
||
platform = 'FlexTV'
|
||
with semaphore:
|
||
if global_proxy or proxy_address:
|
||
json_data = spider.get_flextv_stream_data(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=flextv_cookie,
|
||
username=flextv_username,
|
||
password=flextv_password
|
||
)
|
||
if json_data and json_data.get('new_cookies', None):
|
||
update_config(config_file, 'Cookie', 'flextv_cookie', json_data['new_cookies'])
|
||
port_info = stream.get_stream_url(json_data, record_quality, spec=True)
|
||
else:
|
||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问FlexTV直播平台")
|
||
|
||
elif record_url.find("look.163.com/") > -1:
|
||
platform = 'Look直播'
|
||
with semaphore:
|
||
port_info = spider.get_looklive_stream_url(
|
||
url=record_url, proxy_addr=proxy_address, cookies=look_cookie
|
||
)
|
||
|
||
elif record_url.find("www.popkontv.com/") > -1:
|
||
platform = 'PopkonTV'
|
||
with semaphore:
|
||
if global_proxy or proxy_address:
|
||
port_info = spider.get_popkontv_stream_url(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
access_token=popkontv_access_token,
|
||
username=popkontv_username,
|
||
password=popkontv_password,
|
||
partner_code=popkontv_partner_code
|
||
)
|
||
if port_info and port_info.get('new_token'):
|
||
update_config(config_file, 'Authorization',
|
||
'popkontv_token', port_info['new_token'])
|
||
|
||
else:
|
||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PopkonTV直播平台")
|
||
|
||
elif record_url.find("twitcasting.tv/") > -1:
|
||
platform = 'TwitCasting'
|
||
with semaphore:
|
||
port_info = spider.get_twitcasting_stream_url(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=twitcasting_cookie,
|
||
account_type=twitcasting_account_type,
|
||
username=twitcasting_username,
|
||
password=twitcasting_password
|
||
)
|
||
if port_info and port_info.get('new_cookies'):
|
||
update_config(config_file, 'Cookie', 'twitcasting_cookie', port_info['new_cookies'])
|
||
|
||
elif record_url.find("live.baidu.com/") > -1:
|
||
platform = '百度直播'
|
||
with semaphore:
|
||
json_data = spider.get_baidu_stream_data(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=baidu_cookie)
|
||
port_info = stream.get_stream_url(json_data, record_quality)
|
||
|
||
elif record_url.find("weibo.com/") > -1:
|
||
platform = '微博直播'
|
||
with semaphore:
|
||
json_data = spider.get_weibo_stream_data(
|
||
url=record_url, proxy_addr=proxy_address, cookies=weibo_cookie)
|
||
port_info = stream.get_stream_url(json_data, record_quality, extra_key='m3u8_url')
|
||
|
||
elif record_url.find("kugou.com/") > -1:
|
||
platform = '酷狗直播'
|
||
with semaphore:
|
||
port_info = spider.get_kugou_stream_url(
|
||
url=record_url, proxy_addr=proxy_address, cookies=kugou_cookie)
|
||
|
||
elif record_url.find("www.twitch.tv/") > -1:
|
||
platform = 'TwitchTV'
|
||
with semaphore:
|
||
if global_proxy or proxy_address:
|
||
json_data = spider.get_twitchtv_stream_data(
|
||
url=record_url,
|
||
proxy_addr=proxy_address,
|
||
cookies=twitch_cookie
|
||
)
|
||
port_info = stream.get_stream_url(json_data, record_quality, spec=True)
|
||
else:
|
||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问TwitchTV直播平台")
|
||
|
||
elif record_url.find("www.liveme.com/") > -1:
|
||
if global_proxy or proxy_address:
|
||
platform = 'LiveMe'
|
||
with semaphore:
|
||
port_info = spider.get_liveme_stream_url(
|
||
url=record_url, proxy_addr=proxy_address, cookies=liveme_cookie)
|
||
else:
|
||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问LiveMe直播平台")
|
||
|
||
elif record_url.find("www.huajiao.com/") > -1:
|
||
platform = '花椒直播'
|
||
with semaphore:
|
||
port_info = spider.get_huajiao_stream_url(
|
||
url=record_url, proxy_addr=proxy_address, cookies=huajiao_cookie)
|
||
|
||
elif record_url.find("7u66.com/") > -1:
|
||
platform = '流星直播'
|
||
with semaphore:
|
||
port_info = spider.get_liuxing_stream_url(
|
||
url=record_url, proxy_addr=proxy_address, cookies=liuxing_cookie)
|
||
|
||
elif record_url.find("showroom-live.com/") > -1:
|
||
platform = 'ShowRoom'
|
||
with semaphore:
|
||
json_data = spider.get_showroom_stream_data(
|
||
url=record_url, proxy_addr=proxy_address, cookies=showroom_cookie)
|
||
port_info = stream.get_stream_url(json_data, record_quality, spec=True)
|
||
|
||
elif record_url.find("live.acfun.cn/") > -1 or record_url.find("m.acfun.cn/") > -1:
|
||
platform = 'Acfun'
|
||
with semaphore:
|
||
json_data = spider.get_acfun_stream_data(
|
||
url=record_url, proxy_addr=proxy_address, cookies=acfun_cookie)
|
||
port_info = stream.get_stream_url(
|
||
json_data, record_quality, url_type='flv', extra_key='url')
|
||
|
||
elif record_url.find("rengzu.com/") > -1:
|
||
platform = '时光直播'
|
||
with semaphore:
|
||
port_info = spider.get_shiguang_stream_url(
|
||
url=record_url, proxy_addr=proxy_address, cookies=shiguang_cookie)
|
||
|
||
elif record_url.find("ybw1666.com/") > -1:
|
||
platform = '音播直播'
|
||
with semaphore:
|
||
port_info = spider.get_yinbo_stream_url(
|
||
url=record_url, proxy_addr=proxy_address, cookies=yinbo_cookie)
|
||
|
||
elif record_url.find("www.inke.cn/") > -1:
|
||
platform = '映客直播'
|
||
with semaphore:
|
||
port_info = spider.get_yingke_stream_url(
|
||
url=record_url, proxy_addr=proxy_address, cookies=yingke_cookie)
|
||
|
||
elif record_url.find("www.zhihu.com/") > -1:
|
||
platform = '知乎直播'
|
||
with semaphore:
|
||
port_info = spider.get_zhihu_stream_url(
|
||
url=record_url, proxy_addr=proxy_address, cookies=zhihu_cookie)
|
||
|
||
elif record_url.find("chzzk.naver.com/") > -1:
|
||
platform = 'CHZZK'
|
||
with semaphore:
|
||
json_data = spider.get_chzzk_stream_data(
|
||
url=record_url, proxy_addr=proxy_address, cookies=chzzk_cookie)
|
||
port_info = stream.get_stream_url(json_data, record_quality, spec=True)
|
||
|
||
else:
|
||
logger.error(f'{record_url} {platform}直播地址')
|
||
return
|
||
|
||
if anchor_name:
|
||
# 第一次从config中读取,带有'主播:',去除'主播:'
|
||
# 之后的线程循环,已经是处理后的结果,不需要去处理
|
||
if '主播:' in anchor_name:
|
||
anchor_split: list = anchor_name.split('主播:')
|
||
if len(anchor_split) > 1 and anchor_split[1].strip():
|
||
anchor_name = anchor_split[1].strip()
|
||
else:
|
||
anchor_name = port_info.get("anchor_name", '')
|
||
else:
|
||
anchor_name = port_info.get("anchor_name", '')
|
||
|
||
if not port_info.get("anchor_name", ''):
|
||
print(f'序号{count_variable} 网址内容获取失败,进行重试中...获取失败的地址是:{url_data}')
|
||
warning_count += 1
|
||
else:
|
||
anchor_name = re.sub(rstr, "_", anchor_name)
|
||
anchor_name = remove_emojis(anchor_name, '_')
|
||
|
||
record_name = f'序号{count_variable} {anchor_name}'
|
||
|
||
if record_url in url_comments:
|
||
print(f"[{anchor_name}]已被注释,本条线程将会退出")
|
||
clear_record_info(record_name, record_url)
|
||
return
|
||
|
||
if record_name in recording:
|
||
print(f"新增的地址: {anchor_name} 已经存在,本条线程将会退出")
|
||
need_update_line_list.append(f'{record_url}|#{record_url}')
|
||
return
|
||
|
||
if url_data[-1] == "" and run_once is False:
|
||
if is_long_url:
|
||
need_update_line_list.append(
|
||
f'{record_url}|{new_record_url},主播: {anchor_name.strip()}')
|
||
else:
|
||
need_update_line_list.append(f'{record_url}|{record_url},主播: {anchor_name.strip()}')
|
||
run_once = True
|
||
|
||
push_at = datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S')
|
||
if port_info['is_live'] is False:
|
||
print(f"\r{record_name} 等待直播... ")
|
||
|
||
if start_pushed:
|
||
if over_show_push:
|
||
push_content = "直播间状态更新:[直播间名称] 直播已结束!时间:[时间]"
|
||
if over_push_message_text:
|
||
push_content = over_push_message_text
|
||
|
||
push_content = (push_content.replace('[直播间名称]', record_name).
|
||
replace('[时间]', push_at))
|
||
push_pts = push_message(push_content.replace(r'\n', '\n'))
|
||
if push_pts:
|
||
print(f'提示信息:已经将[{record_name}]直播状态消息推送至你的{push_pts}')
|
||
start_pushed = False
|
||
else:
|
||
content = f"\r{record_name} 正在直播中..."
|
||
print(content)
|
||
|
||
if live_status_push and not start_pushed:
|
||
if begin_show_push:
|
||
push_content = "直播间状态更新:[直播间名称] 正在直播中,时间:[时间]"
|
||
if begin_push_message_text:
|
||
push_content = begin_push_message_text
|
||
|
||
push_content = (push_content.replace('[直播间名称]', record_name).
|
||
replace('[时间]', push_at))
|
||
push_pts = push_message(push_content.replace(r'\n', '\n'))
|
||
if push_pts:
|
||
print(f'提示信息:已经将[{record_name}]直播状态消息推送至你的{push_pts}')
|
||
start_pushed = True
|
||
|
||
if disable_record:
|
||
time.sleep(push_check_seconds)
|
||
continue
|
||
|
||
real_url = port_info.get('record_url', None)
|
||
full_path = f'{default_path}/{platform}'
|
||
if len(real_url) > 0:
|
||
now = datetime.datetime.today().strftime("%Y-%m-%d_%H-%M-%S")
|
||
|
||
try:
|
||
if len(video_save_path) > 0:
|
||
if video_save_path[-1] not in ["/", "\\"]:
|
||
video_save_path = video_save_path + "/"
|
||
full_path = f'{video_save_path}{platform}'
|
||
|
||
full_path = full_path.replace("\\", '/')
|
||
if folder_by_author:
|
||
full_path = f'{full_path}/{anchor_name}'
|
||
if folder_by_time:
|
||
full_path = f'{full_path}/{now[:10]}'
|
||
if not os.path.exists(full_path):
|
||
os.makedirs(full_path)
|
||
except Exception as e:
|
||
logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||
|
||
user_agent = ("Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 ("
|
||
"KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile "
|
||
"Safari/537.36")
|
||
|
||
rw_timeout = "15000000"
|
||
analyzeduration = "20000000"
|
||
probesize = "10000000"
|
||
bufsize = "8000k"
|
||
max_muxing_queue_size = "1024"
|
||
for pt_host in overseas_platform_host:
|
||
if pt_host in record_url:
|
||
rw_timeout = "50000000"
|
||
analyzeduration = "40000000"
|
||
probesize = "20000000"
|
||
bufsize = "15000k"
|
||
max_muxing_queue_size = "2048"
|
||
break
|
||
|
||
ffmpeg_command = [
|
||
'ffmpeg', "-y",
|
||
"-v", "verbose",
|
||
"-rw_timeout", rw_timeout,
|
||
"-loglevel", "error",
|
||
"-hide_banner",
|
||
"-user_agent", user_agent,
|
||
"-protocol_whitelist", "rtmp,crypto,file,http,https,tcp,tls,udp,rtp",
|
||
"-thread_queue_size", "1024",
|
||
"-analyzeduration", analyzeduration,
|
||
"-probesize", probesize,
|
||
"-fflags", "+discardcorrupt",
|
||
"-i", real_url,
|
||
"-bufsize", bufsize,
|
||
"-sn", "-dn",
|
||
"-reconnect_delay_max", "60",
|
||
"-reconnect_streamed", "-reconnect_at_eof",
|
||
"-max_muxing_queue_size", max_muxing_queue_size,
|
||
"-correct_ts_overflow", "1",
|
||
]
|
||
|
||
record_headers = {
|
||
'PandaTV': 'origin:https://www.pandalive.co.kr',
|
||
'WinkTV': 'origin:https://www.winktv.co.kr',
|
||
'PopkonTV': 'origin:https://www.popkontv.com',
|
||
'FlexTV': 'origin:https://www.flextv.co.kr',
|
||
'千度热播': 'referer:https://qiandurebo.com',
|
||
}
|
||
headers = record_headers.get(platform, '')
|
||
if headers:
|
||
ffmpeg_command.insert(11, "-headers")
|
||
ffmpeg_command.insert(12, headers)
|
||
|
||
if proxy_address:
|
||
ffmpeg_command.insert(1, "-http_proxy")
|
||
ffmpeg_command.insert(2, proxy_address)
|
||
|
||
recording.add(record_name)
|
||
start_record_time = datetime.datetime.now()
|
||
recording_time_list[record_name] = [start_record_time, record_quality]
|
||
rec_info = f"\r{anchor_name} 准备开始录制视频: {full_path}"
|
||
if show_url:
|
||
re_plat = ('WinkTV', 'PandaTV', 'ShowRoom', 'CHZZK')
|
||
if platform in re_plat:
|
||
logger.info(f"{platform} | {anchor_name} | 直播源地址: {port_info['m3u8_url']}")
|
||
else:
|
||
logger.info(
|
||
f"{platform} | {anchor_name} | 直播源地址: {port_info['record_url']}")
|
||
|
||
if video_save_type == "FLV":
|
||
filename = anchor_name + '_' + now + '.flv'
|
||
save_file_path = f'{full_path}/{filename}'
|
||
print(f'{rec_info}/{filename}')
|
||
|
||
subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0]
|
||
subs_thread_name = f'subs_{Path(subs_file_path).name}'
|
||
if create_time_file:
|
||
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()
|
||
|
||
try:
|
||
flv_url = port_info.get('flv_url', None)
|
||
if flv_url:
|
||
_filepath, _ = urllib.request.urlretrieve(real_url, save_file_path)
|
||
record_finished = True
|
||
recording.discard(record_name)
|
||
print(f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制完成\n")
|
||
except Exception as e:
|
||
print(f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制出错,请检查网络\n")
|
||
logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||
warning_count += 1
|
||
|
||
elif video_save_type == "MKV":
|
||
filename = anchor_name + '_' + now + ".mkv"
|
||
print(f'{rec_info}/{filename}')
|
||
save_file_path = full_path + '/' + filename
|
||
|
||
try:
|
||
if split_video_by_time:
|
||
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
|
||
save_file_path = f"{full_path}/{anchor_name}_{now}_%03d.mkv"
|
||
command = [
|
||
"-flags", "global_header",
|
||
"-c:v", "copy",
|
||
"-c:a", "aac",
|
||
"-map", "0",
|
||
"-f", "segment",
|
||
"-segment_time", split_time,
|
||
"-segment_format", "matroska",
|
||
"-reset_timestamps", "1",
|
||
save_file_path,
|
||
]
|
||
|
||
else:
|
||
command = [
|
||
"-flags", "global_header",
|
||
"-map", "0",
|
||
"-c:v", "copy",
|
||
"-c:a", "copy",
|
||
"-f", "matroska",
|
||
"{path}".format(path=save_file_path),
|
||
]
|
||
ffmpeg_command.extend(command)
|
||
|
||
comment_end = check_subprocess(
|
||
record_name,
|
||
record_url,
|
||
ffmpeg_command,
|
||
video_save_type,
|
||
bash_path
|
||
)
|
||
if comment_end:
|
||
return
|
||
|
||
except subprocess.CalledProcessError as e:
|
||
logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||
warning_count += 1
|
||
|
||
elif video_save_type == "MP4":
|
||
filename = anchor_name + '_' + now + ".mp4"
|
||
print(f'{rec_info}/{filename}')
|
||
save_file_path = full_path + '/' + filename
|
||
|
||
try:
|
||
if split_video_by_time:
|
||
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
|
||
save_file_path = f"{full_path}/{anchor_name}_{now}_%03d.mp4"
|
||
command = [
|
||
"-c:v", "copy",
|
||
"-c:a", "aac",
|
||
"-map", "0",
|
||
"-f", "segment",
|
||
"-segment_time", split_time,
|
||
"-segment_format", "mp4",
|
||
"-reset_timestamps", "1",
|
||
"-movflags", "+frag_keyframe+empty_moov",
|
||
save_file_path,
|
||
]
|
||
|
||
else:
|
||
command = [
|
||
"-map", "0",
|
||
"-c:v", "copy",
|
||
"-c:a", "copy",
|
||
"-f", "mp4",
|
||
"{path}".format(path=save_file_path),
|
||
]
|
||
|
||
ffmpeg_command.extend(command)
|
||
comment_end = check_subprocess(
|
||
record_name,
|
||
record_url,
|
||
ffmpeg_command,
|
||
video_save_type,
|
||
bash_path
|
||
)
|
||
if comment_end:
|
||
return
|
||
|
||
except subprocess.CalledProcessError as e:
|
||
logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||
warning_count += 1
|
||
|
||
elif "音频" in video_save_type:
|
||
try:
|
||
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
|
||
extension = "mp3" if "MP3" in video_save_type else "m4a"
|
||
name_format = "_%03d" if split_video_by_time else ""
|
||
save_path_name = f"{full_path}/{anchor_name}_{now}{name_format}.{extension}"
|
||
|
||
if split_video_by_time:
|
||
print(f'\r{anchor_name} 准备开始录制音频: {save_path_name}')
|
||
|
||
if "MP3" in video_save_type:
|
||
command = [
|
||
"-map", "0:a",
|
||
"-c:a", "libmp3lame",
|
||
"-ab", "320k",
|
||
"-f", "segment",
|
||
"-segment_time", split_time,
|
||
"-reset_timestamps", "1",
|
||
save_path_name,
|
||
]
|
||
else:
|
||
command = [
|
||
"-map", "0:a",
|
||
"-c:a", "aac",
|
||
"-bsf:a", "aac_adtstoasc",
|
||
"-ab", "320k",
|
||
"-f", "segment",
|
||
"-segment_time", split_time,
|
||
"-segment_format", 'mpegts',
|
||
"-reset_timestamps", "1",
|
||
save_path_name,
|
||
]
|
||
|
||
else:
|
||
if "MP3" in video_save_type:
|
||
command = [
|
||
"-map", "0:a",
|
||
"-c:a", "libmp3lame",
|
||
"-ab", "320k",
|
||
save_path_name,
|
||
]
|
||
|
||
else:
|
||
command = [
|
||
"-map", "0:a",
|
||
"-c:a", "aac",
|
||
"-bsf:a", "aac_adtstoasc",
|
||
"-ab", "320k",
|
||
"-movflags", "+faststart",
|
||
save_path_name,
|
||
]
|
||
|
||
ffmpeg_command.extend(command)
|
||
comment_end = check_subprocess(
|
||
record_name,
|
||
record_url,
|
||
ffmpeg_command,
|
||
video_save_type,
|
||
bash_path
|
||
)
|
||
if comment_end:
|
||
return
|
||
|
||
except subprocess.CalledProcessError as e:
|
||
logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||
warning_count += 1
|
||
|
||
else:
|
||
if split_video_by_time:
|
||
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
|
||
filename = anchor_name + '_' + now + ".ts"
|
||
print(f'{rec_info}/{filename}')
|
||
|
||
try:
|
||
save_path_name = f"{full_path}/{anchor_name}_{now}_%03d.ts"
|
||
command = [
|
||
"-c:v", "copy",
|
||
"-c:a", "copy",
|
||
"-map", "0",
|
||
"-f", "segment",
|
||
"-segment_time", split_time,
|
||
"-segment_format", 'mpegts',
|
||
"-reset_timestamps", "1",
|
||
save_path_name,
|
||
]
|
||
|
||
ffmpeg_command.extend(command)
|
||
comment_end = check_subprocess(
|
||
record_name,
|
||
record_url,
|
||
ffmpeg_command,
|
||
video_save_type,
|
||
bash_path
|
||
)
|
||
if comment_end:
|
||
if ts_to_mp4:
|
||
file_paths = get_file_paths(os.path.dirname(save_path_name))
|
||
prefix = os.path.basename(save_path_name).rsplit('_', maxsplit=1)[0]
|
||
for path in file_paths:
|
||
if prefix in path:
|
||
threading.Thread(
|
||
target=converts_mp4,
|
||
args=(path, delete_origin_file)
|
||
).start()
|
||
return
|
||
|
||
except subprocess.CalledProcessError as e:
|
||
logger.error(
|
||
f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||
warning_count += 1
|
||
|
||
else:
|
||
filename = anchor_name + '_' + now + ".ts"
|
||
print(f'{rec_info}/{filename}')
|
||
save_file_path = full_path + '/' + filename
|
||
|
||
try:
|
||
command = [
|
||
"-c:v", "copy",
|
||
"-c:a", "copy",
|
||
"-map", "0",
|
||
"-f", "mpegts",
|
||
"{path}".format(path=save_file_path),
|
||
]
|
||
|
||
ffmpeg_command.extend(command)
|
||
comment_end = check_subprocess(
|
||
record_name,
|
||
record_url,
|
||
ffmpeg_command,
|
||
video_save_type,
|
||
bash_path
|
||
)
|
||
if comment_end:
|
||
return
|
||
|
||
except subprocess.CalledProcessError as e:
|
||
logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||
warning_count += 1
|
||
|
||
count_time = time.time()
|
||
|
||
except Exception as e:
|
||
logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||
warning_count += 1
|
||
|
||
num = random.randint(-5, 5) + delay_default
|
||
if num < 0:
|
||
num = 0
|
||
x = num
|
||
|
||
if warning_count > 20:
|
||
x = x + 60
|
||
print("瞬时错误太多,延迟加60秒")
|
||
|
||
# 这里是.如果录制结束后,循环时间会暂时变成30s后检测一遍. 这样一定程度上防止主播卡顿造成少录
|
||
# 当30秒过后检测一遍后. 会回归正常设置的循环秒数
|
||
if record_finished:
|
||
count_time_end = time.time() - count_time
|
||
if count_time_end < 60:
|
||
x = 30
|
||
record_finished = False
|
||
|
||
else:
|
||
x = num
|
||
|
||
# 这里是正常循环
|
||
while x:
|
||
x = x - 1
|
||
if loop_time:
|
||
print(f'\r{anchor_name}循环等待{x}秒 ', end="")
|
||
time.sleep(1)
|
||
if loop_time:
|
||
print('\r检测直播间中...', end="")
|
||
except Exception as e:
|
||
logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
|
||
warning_count += 1
|
||
time.sleep(2)
|
||
|
||
|
||
def backup_file(file_path: str, backup_dir_path: str):
|
||
try:
|
||
if not os.path.exists(backup_dir_path):
|
||
os.makedirs(backup_dir_path)
|
||
|
||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
|
||
backup_file_name = os.path.basename(file_path) + '_' + timestamp
|
||
|
||
backup_file_path = os.path.join(backup_dir_path, backup_file_name).replace("\\", "/")
|
||
shutil.copy2(file_path, backup_file_path)
|
||
# print(f'\r已备份配置文件 {file_path} 到 {backup_file_path}')
|
||
|
||
files = os.listdir(backup_dir_path)
|
||
url_files = [f for f in files if f.startswith(os.path.basename(url_config_file))]
|
||
config_files = [f for f in files if f.startswith(os.path.basename(config_file))]
|
||
|
||
url_files.sort(key=lambda x: os.path.getmtime(os.path.join(backup_dir_path, x)))
|
||
config_files.sort(key=lambda x: os.path.getmtime(os.path.join(backup_dir_path, x)))
|
||
|
||
while len(url_files) > 6:
|
||
oldest_file = url_files[0]
|
||
os.remove(os.path.join(backup_dir_path, oldest_file))
|
||
url_files = url_files[1:]
|
||
|
||
while len(config_files) > 6:
|
||
oldest_file = config_files[0]
|
||
os.remove(os.path.join(backup_dir_path, oldest_file))
|
||
config_files = config_files[1:]
|
||
|
||
except Exception as e:
|
||
logger.error(f'\r备份配置文件 {file_path} 失败:{str(e)}')
|
||
|
||
|
||
def backup_file_start():
|
||
config_md5 = ''
|
||
url_config_md5 = ''
|
||
|
||
while True:
|
||
try:
|
||
if os.path.exists(config_file):
|
||
new_config_md5 = check_md5(config_file)
|
||
if new_config_md5 != config_md5:
|
||
backup_file(config_file, backup_dir)
|
||
config_md5 = new_config_md5
|
||
|
||
if os.path.exists(url_config_file):
|
||
new_url_config_md5 = check_md5(url_config_file)
|
||
if new_url_config_md5 != url_config_md5:
|
||
backup_file(url_config_file, backup_dir)
|
||
url_config_md5 = new_url_config_md5
|
||
time.sleep(600)
|
||
except Exception as e:
|
||
logger.error(f"备份配置文件失败, 错误信息: {e}")
|
||
|
||
|
||
# --------------------------检查是否存在ffmpeg-------------------------------------
|
||
def check_ffmpeg_existence():
|
||
dev_null = open(os.devnull, 'wb')
|
||
try:
|
||
subprocess.run(['ffmpeg', '--help'], stdout=dev_null, stderr=dev_null, check=True)
|
||
except subprocess.CalledProcessError as e:
|
||
logger.error(e)
|
||
return False
|
||
except FileNotFoundError:
|
||
ffmpeg_file_check = subprocess.getoutput(ffmpeg_path)
|
||
if ffmpeg_file_check.find("run") > -1 and os.path.isfile(ffmpeg_path):
|
||
os.environ['PATH'] += os.pathsep + os.path.dirname(os.path.abspath(ffmpeg_path))
|
||
# print(f"已将ffmpeg路径添加到环境变量:{ffmpeg_path}")
|
||
return True
|
||
else:
|
||
logger.error("检测到ffmpeg不存在,请将ffmpeg.exe放到同目录,或者设置为环境变量,没有ffmpeg将无法录制")
|
||
sys.exit(0)
|
||
finally:
|
||
dev_null.close()
|
||
return True
|
||
|
||
|
||
if not check_ffmpeg_existence():
|
||
logger.error("ffmpeg检查失败,程序将退出。")
|
||
sys.exit(1)
|
||
|
||
# --------------------------初始化程序-------------------------------------
|
||
print("-----------------------------------------------------")
|
||
print("| DouyinLiveRecorder |")
|
||
print("-----------------------------------------------------")
|
||
|
||
print(f"版本号: {version}")
|
||
print("GitHub: https://github.com/ihmily/DouyinLiveRecorder")
|
||
print(f'支持平台: {platforms}')
|
||
print('.....................................................')
|
||
|
||
os.makedirs(os.path.dirname(config_file), exist_ok=True)
|
||
t3 = threading.Thread(target=backup_file_start, args=(), daemon=True)
|
||
t3.start()
|
||
|
||
|
||
def read_config_value(config_parser: configparser.RawConfigParser, section: str, option: str, default_value: Any) \
|
||
-> Union[str, int, bool]:
|
||
try:
|
||
|
||
config_parser.read(config_file, encoding=text_encoding)
|
||
if '录制设置' not in config_parser.sections():
|
||
config_parser.add_section('录制设置')
|
||
if '推送配置' not in config_parser.sections():
|
||
config_parser.add_section('推送配置')
|
||
if 'Cookie' not in config_parser.sections():
|
||
config_parser.add_section('Cookie')
|
||
if 'Authorization' not in config_parser.sections():
|
||
config_parser.add_section('Authorization')
|
||
if '账号密码' not in config_parser.sections():
|
||
config_parser.add_section('账号密码')
|
||
return config_parser.get(section, option)
|
||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||
config_parser.set(section, option, str(default_value))
|
||
with open(config_file, 'w', encoding=text_encoding) as f:
|
||
config_parser.write(f)
|
||
return default_value
|
||
|
||
|
||
options = {"是": True, "否": False}
|
||
|
||
config = configparser.RawConfigParser()
|
||
skip_proxy_check = options.get(read_config_value(config, '录制设置', '是否跳过代理检测(是/否)', "否"), False)
|
||
|
||
try:
|
||
if skip_proxy_check:
|
||
global_proxy = True
|
||
else:
|
||
print('系统代理检测中,请耐心等待...')
|
||
response_g = urllib.request.urlopen("https://www.google.com/", timeout=15)
|
||
global_proxy = True
|
||
print('\r全局/规则网络代理已开启√')
|
||
except HTTPError as err:
|
||
print(f"HTTP error occurred: {err.code} - {err.reason}")
|
||
except URLError as err:
|
||
# print("URLError:", err.reason)
|
||
print('INFO:未检测到全局/规则网络代理,请检查代理配置(若无需录制海外直播请忽略此条提示)')
|
||
except Exception as err:
|
||
print("An unexpected error occurred:", err)
|
||
|
||
while True:
|
||
|
||
try:
|
||
if not os.path.isfile(config_file):
|
||
with open(config_file, 'w', encoding=text_encoding) as file:
|
||
pass
|
||
|
||
ini_URL_content = ''
|
||
if os.path.isfile(url_config_file):
|
||
with open(url_config_file, 'r', encoding=text_encoding) as file:
|
||
ini_URL_content = file.read().strip()
|
||
|
||
if not ini_URL_content.strip():
|
||
input_url = input('请输入要录制的主播直播间网址(尽量使用PC网页端的直播间地址):\n')
|
||
with open(url_config_file, 'w', encoding=text_encoding) as file:
|
||
file.write(input_url)
|
||
except OSError as err:
|
||
logger.error(f"发生 I/O 错误: {err}")
|
||
|
||
video_save_path = read_config_value(config, '录制设置', '直播保存路径(不填则默认)', "")
|
||
folder_by_author = options.get(read_config_value(config, '录制设置', '保存文件夹是否以作者区分', "是"), False)
|
||
folder_by_time = options.get(read_config_value(config, '录制设置', '保存文件夹是否以时间区分', "否"), False)
|
||
video_save_type = read_config_value(config, '录制设置', '视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频', "ts")
|
||
video_record_quality = read_config_value(config, '录制设置', '原画|超清|高清|标清|流畅', "原画")
|
||
use_proxy = options.get(read_config_value(config, '录制设置', '是否使用代理ip(是/否)', "是"), False)
|
||
proxy_addr_bak = read_config_value(config, '录制设置', '代理地址', "")
|
||
proxy_addr = None if not use_proxy else proxy_addr_bak
|
||
max_request = int(read_config_value(config, '录制设置', '同一时间访问网络的线程数', 3))
|
||
semaphore = threading.Semaphore(max_request)
|
||
delay_default = int(read_config_value(config, '录制设置', '循环时间(秒)', 120))
|
||
local_delay_default = int(read_config_value(config, '录制设置', '排队读取网址时间(秒)', 0))
|
||
loop_time = options.get(read_config_value(config, '录制设置', '是否显示循环秒数', "否"), False)
|
||
show_url = options.get(read_config_value(config, '录制设置', '是否显示直播源地址', "否"), False)
|
||
split_video_by_time = options.get(read_config_value(config, '录制设置', '分段录制是否开启', "否"), False)
|
||
split_time = str(read_config_value(config, '录制设置', '视频分段时间(秒)', 1800))
|
||
ts_to_mp4 = options.get(read_config_value(config, '录制设置', 'ts录制完成后自动转为mp4格式', "否"),
|
||
False)
|
||
delete_origin_file = options.get(read_config_value(config, '录制设置', '追加格式后删除原文件', "否"), False)
|
||
create_time_file = options.get(read_config_value(config, '录制设置', '生成时间字幕文件', "否"), False)
|
||
is_run_bash = options.get(read_config_value(config, '录制设置', '是否录制完成后执行bash脚本', "否"), False)
|
||
bash_path = read_config_value(config, '录制设置', 'bash脚本路径', "") if is_run_bash else None
|
||
enable_proxy_platform = read_config_value(
|
||
config, '录制设置', '使用代理录制的平台(逗号分隔)',
|
||
'tiktok, afreecatv, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk')
|
||
enable_proxy_platform_list = enable_proxy_platform.replace(',', ',').split(',') if enable_proxy_platform else None
|
||
extra_enable_proxy = read_config_value(config, '录制设置', '额外使用代理录制的平台(逗号分隔)', '')
|
||
extra_enable_proxy_platform_list = extra_enable_proxy.replace(',', ',').split(',') if extra_enable_proxy else None
|
||
live_status_push = read_config_value(config, '推送配置', '直播状态通知(可选微信|钉钉|tg|邮箱|bark或者都填)', "")
|
||
dingtalk_api_url = read_config_value(config, '推送配置', '钉钉推送接口链接', "")
|
||
xizhi_api_url = read_config_value(config, '推送配置', '微信推送接口链接', "")
|
||
bark_msg_api = read_config_value(config, '推送配置', 'bark推送接口链接', "")
|
||
bark_msg_level = read_config_value(config, '推送配置', 'bark推送中断级别', "active")
|
||
bark_msg_ring = read_config_value(config, '推送配置', 'bark推送铃声', "bell")
|
||
dingtalk_phone_num = read_config_value(config, '推送配置', '钉钉通知@对象(填手机号)', "")
|
||
tg_token = read_config_value(config, '推送配置', 'tgapi令牌', "")
|
||
tg_chat_id = read_config_value(config, '推送配置', 'tg聊天id(个人或者群组id)', "")
|
||
mail_host = read_config_value(config, '推送配置', 'SMTP邮件服务器', "")
|
||
from_email = read_config_value(config, '推送配置', '发件人邮箱', "")
|
||
mail_password = read_config_value(config, '推送配置', '发件人密码(授权码)', "")
|
||
to_email = read_config_value(config, '推送配置', '收件人邮箱', "")
|
||
begin_push_message_text = read_config_value(config, '推送配置', '自定义开播推送内容', "")
|
||
over_push_message_text = read_config_value(config, '推送配置', '自定义关播推送内容', "")
|
||
disable_record = options.get(read_config_value(config, '推送配置', '只推送通知不录制(是/否)', "否"), False)
|
||
push_check_seconds = int(read_config_value(config, '推送配置', '直播推送检测频率(秒)', 1800))
|
||
begin_show_push = options.get(read_config_value(config, '推送配置', '开播推送开启(是/否)', "是"), True)
|
||
over_show_push = options.get(read_config_value(config, '推送配置', '关播推送开启(是/否)', "否"), False)
|
||
afreecatv_username = read_config_value(config, '账号密码', 'afreecatv账号', '')
|
||
afreecatv_password = read_config_value(config, '账号密码', 'afreecatv密码', '')
|
||
flextv_username = read_config_value(config, '账号密码', 'flextv账号', '')
|
||
flextv_password = read_config_value(config, '账号密码', 'flextv密码', '')
|
||
popkontv_username = read_config_value(config, '账号密码', 'popkontv账号', '')
|
||
popkontv_partner_code = read_config_value(config, '账号密码', 'partner_code', 'P-00001')
|
||
popkontv_password = read_config_value(config, '账号密码', 'popkontv密码', '')
|
||
twitcasting_account_type = read_config_value(config, '账号密码', 'twitcasting账号类型', 'normal')
|
||
twitcasting_username = read_config_value(config, '账号密码', 'twitcasting账号', '')
|
||
twitcasting_password = read_config_value(config, '账号密码', 'twitcasting密码', '')
|
||
popkontv_access_token = read_config_value(config, 'Authorization', 'popkontv_token', '')
|
||
dy_cookie = read_config_value(config, 'Cookie', '抖音cookie(录制抖音必须要有)', '')
|
||
ks_cookie = read_config_value(config, 'Cookie', '快手cookie', '')
|
||
tiktok_cookie = read_config_value(config, 'Cookie', 'tiktok_cookie', '')
|
||
hy_cookie = read_config_value(config, 'Cookie', '虎牙cookie', '')
|
||
douyu_cookie = read_config_value(config, 'Cookie', '斗鱼cookie', '')
|
||
yy_cookie = read_config_value(config, 'Cookie', 'yy_cookie', '')
|
||
bili_cookie = read_config_value(config, 'Cookie', 'B站cookie', '')
|
||
xhs_cookie = read_config_value(config, 'Cookie', '小红书cookie', '')
|
||
bigo_cookie = read_config_value(config, 'Cookie', 'bigo_cookie', '')
|
||
blued_cookie = read_config_value(config, 'Cookie', 'blued_cookie', '')
|
||
afreecatv_cookie = read_config_value(config, 'Cookie', 'afreecatv_cookie', '')
|
||
netease_cookie = read_config_value(config, 'Cookie', 'netease_cookie', '')
|
||
qiandurebo_cookie = read_config_value(config, 'Cookie', '千度热播_cookie', '')
|
||
pandatv_cookie = read_config_value(config, 'Cookie', 'pandatv_cookie', '')
|
||
maoerfm_cookie = read_config_value(config, 'Cookie', '猫耳fm_cookie', '')
|
||
winktv_cookie = read_config_value(config, 'Cookie', 'winktv_cookie', '')
|
||
flextv_cookie = read_config_value(config, 'Cookie', 'flextv_cookie', '')
|
||
look_cookie = read_config_value(config, 'Cookie', 'look_cookie', '')
|
||
twitcasting_cookie = read_config_value(config, 'Cookie', 'twitcasting_cookie', '')
|
||
baidu_cookie = read_config_value(config, 'Cookie', 'baidu_cookie', '')
|
||
weibo_cookie = read_config_value(config, 'Cookie', 'weibo_cookie', '')
|
||
kugou_cookie = read_config_value(config, 'Cookie', 'kugou_cookie', '')
|
||
twitch_cookie = read_config_value(config, 'Cookie', 'twitch_cookie', '')
|
||
liveme_cookie = read_config_value(config, 'Cookie', 'liveme_cookie', '')
|
||
huajiao_cookie = read_config_value(config, 'Cookie', 'huajiao_cookie', '')
|
||
liuxing_cookie = read_config_value(config, 'Cookie', 'liuxing_cookie', '')
|
||
showroom_cookie = read_config_value(config, 'Cookie', 'showroom_cookie', '')
|
||
acfun_cookie = read_config_value(config, 'Cookie', 'acfun_cookie', '')
|
||
shiguang_cookie = read_config_value(config, 'Cookie', 'shiguang_cookie', '')
|
||
yinbo_cookie = read_config_value(config, 'Cookie', 'yinbo_cookie', '')
|
||
yingke_cookie = read_config_value(config, 'Cookie', 'yingke_cookie', '')
|
||
zhihu_cookie = read_config_value(config, 'Cookie', 'zhihu_cookie', '')
|
||
chzzk_cookie = read_config_value(config, 'Cookie', 'chzzk_cookie', '')
|
||
|
||
video_save_type_list = ("FLV", "MKV", "TS", "MP4", "MP3音频", "M4A音频")
|
||
if video_save_type and video_save_type.upper() in video_save_type_list:
|
||
video_save_type = video_save_type.upper()
|
||
else:
|
||
video_save_type = "TS"
|
||
|
||
|
||
def contains_url(string: str) -> bool:
|
||
pattern = (r"(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-zA-Z0-9][a-zA-Z0-9\-]+(\.["
|
||
r"a-zA-Z0-9\-]+)*\.[a-zA-Z]{2,10}(:[0-9]{1,5})?(\/.*)?$")
|
||
return re.search(pattern, string) is not None
|
||
|
||
try:
|
||
url_comments = []
|
||
with (open(url_config_file, "r", encoding=text_encoding, errors='ignore') as file):
|
||
for origin_line in file:
|
||
line = origin_line.strip()
|
||
if len(line) < 20:
|
||
continue
|
||
|
||
line_spilt = line.split('主播: ')
|
||
if len(line_spilt) > 2:
|
||
line = update_file(url_config_file, line, f'{line_spilt[0]}主播: {line_spilt[-1]}')
|
||
|
||
is_comment_line = line.startswith("#")
|
||
if is_comment_line:
|
||
line = line.lstrip('#')
|
||
|
||
if re.search('[,,]', line):
|
||
split_line = re.split('[,,]', line)
|
||
else:
|
||
split_line = [line, '']
|
||
|
||
if len(split_line) == 1:
|
||
url = split_line[0]
|
||
quality, name = [video_record_quality, '']
|
||
elif len(split_line) == 2:
|
||
if contains_url(split_line[0]):
|
||
quality = video_record_quality
|
||
url, name = split_line
|
||
else:
|
||
quality, url = split_line
|
||
name = ''
|
||
else:
|
||
quality, url, name = split_line
|
||
|
||
if quality not in ("原画", "蓝光", "超清", "高清", "标清", "流畅"):
|
||
quality = '原画'
|
||
|
||
url = 'https://' + url if '://' not in url else url
|
||
|
||
url_host = url.split('/')[2]
|
||
platform_host = [
|
||
'live.douyin.com',
|
||
'v.douyin.com',
|
||
'www.douyin.com',
|
||
'live.kuaishou.com',
|
||
'www.huya.com',
|
||
'www.douyu.com',
|
||
'www.yy.com',
|
||
'live.bilibili.com',
|
||
'www.redelight.cn',
|
||
'www.xiaohongshu.com',
|
||
'xhslink.com',
|
||
'www.bigo.tv',
|
||
'slink.bigovideo.tv',
|
||
'app.blued.cn',
|
||
'cc.163.com',
|
||
'qiandurebo.com',
|
||
'fm.missevan.com',
|
||
'look.163.com',
|
||
'twitcasting.tv',
|
||
'live.baidu.com',
|
||
'weibo.com',
|
||
'fanxing.kugou.com',
|
||
'fanxing2.kugou.com',
|
||
'mfanxing.kugou.com',
|
||
'www.huajiao.com',
|
||
'www.7u66.com',
|
||
'wap.7u66.com',
|
||
'live.acfun.cn',
|
||
'm.acfun.cn',
|
||
'www.rengzu.com',
|
||
'wap.rengzu.com',
|
||
'live.ybw1666.com',
|
||
'wap.ybw1666.com',
|
||
'www.inke.cn',
|
||
'www.zhihu.com'
|
||
]
|
||
overseas_platform_host = [
|
||
'www.tiktok.com',
|
||
'play.afreecatv.com',
|
||
'm.afreecatv.com',
|
||
'www.pandalive.co.kr',
|
||
'www.winktv.co.kr',
|
||
'www.flextv.co.kr',
|
||
'www.popkontv.com',
|
||
'www.twitch.tv',
|
||
'www.liveme.com',
|
||
'www.showroom-live.com',
|
||
'chzzk.naver.com',
|
||
'm.chzzk.naver.com',
|
||
]
|
||
|
||
platform_host.extend(overseas_platform_host)
|
||
clean_url_host_list = (
|
||
"live.douyin.com",
|
||
"live.bilibili.com",
|
||
"www.huajiao.com",
|
||
"www.zhihu.com",
|
||
"www.xiaohongshu.com",
|
||
"www.redelight.cn",
|
||
"www.huya.com",
|
||
"chzzk.naver.com"
|
||
)
|
||
|
||
if url_host in platform_host:
|
||
if url_host in clean_url_host_list:
|
||
url = update_file(url_config_file, url, url.split('?')[0])
|
||
|
||
url_comments = [i for i in url_comments if url not in i]
|
||
if is_comment_line:
|
||
url_comments.append(url)
|
||
else:
|
||
new_line = (quality, url, name)
|
||
url_tuples_list.append(new_line)
|
||
else:
|
||
print(f"\r{origin_line} 本行包含未知链接.此条跳过")
|
||
update_file(url_config_file, url, url, start_str='#')
|
||
|
||
while len(need_update_line_list):
|
||
a = need_update_line_list.pop()
|
||
replace_words = a.split('|')
|
||
if replace_words[0] != replace_words[1]:
|
||
if replace_words[1].startswith("#"):
|
||
start_with = '#'
|
||
new_word = replace_words[1][1:]
|
||
else:
|
||
start_with = None
|
||
new_word = replace_words[1]
|
||
update_file(url_config_file, replace_words[0], new_word, start_str=start_with)
|
||
|
||
text_no_repeat_url = list(set(url_tuples_list))
|
||
|
||
if len(text_no_repeat_url) > 0:
|
||
for url_tuple in text_no_repeat_url:
|
||
monitoring = len(running_list)
|
||
|
||
if url_tuple[1] in not_record_list:
|
||
continue
|
||
|
||
if url_tuple[1] not in running_list:
|
||
print(f"\r{'新增' if not first_start else '传入'}地址: {url_tuple[1]}")
|
||
monitoring += 1
|
||
args = [url_tuple, monitoring]
|
||
create_var[f'thread_{monitoring}'] = threading.Thread(target=start_record, args=args)
|
||
create_var[f'thread_{monitoring}'].daemon = True
|
||
create_var[f'thread_{monitoring}'].start()
|
||
running_list.append(url_tuple[1])
|
||
time.sleep(local_delay_default)
|
||
url_tuples_list = []
|
||
first_start = False
|
||
|
||
except Exception as err:
|
||
logger.error(f"错误信息: {err} 发生错误的行数: {err.__traceback__.tb_lineno}")
|
||
|
||
if first_run:
|
||
t = threading.Thread(target=display_info, args=(), daemon=True)
|
||
t.start()
|
||
t2 = threading.Thread(target=change_max_connect, args=(), daemon=True)
|
||
t2.start()
|
||
first_run = False
|
||
|
||
time.sleep(3) |