diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8e0a31b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,679 @@
+
+
+## 💡简介
+[](https://www.python.org/downloads/release/python-3116/)
+[](https://github.com/ihmily/DouyinLiveRecorder)
+[](https://hub.docker.com/r/ihmily/douyin-live-recorder/tags)
+
+[](https://github.com/ihmily/DouyinLiveRecorder/releases/latest)
+[](https://github.com/ihmily/DouyinLiveRecorder/releases/latest)
+
+一款**简易**的可循环值守的直播录制工具,基于FFmpeg实现多平台直播源录制,支持自定义配置录制以及直播状态推送。
+
+
+
+## 😺已支持平台
+
+- [x] 抖音
+- [x] TikTok
+- [x] 快手
+- [x] 虎牙
+- [x] 斗鱼
+- [x] YY
+- [x] B站
+- [x] 小红书
+- [x] bigo
+- [x] blued
+- [x] SOOP(原AfreecaTV)
+- [x] 网易cc
+- [x] 千度热播
+- [x] PandaTV
+- [x] 猫耳FM
+- [x] Look直播
+- [x] WinkTV
+- [x] TTingLive(原Flextv)
+- [x] PopkonTV
+- [x] TwitCasting
+- [x] 百度直播
+- [x] 微博直播
+- [x] 酷狗直播
+- [x] TwitchTV
+- [x] LiveMe
+- [x] 花椒直播
+- [x] 流星直播
+- [x] ShowRoom
+- [x] Acfun
+- [x] 映客直播
+- [x] 音播直播
+- [x] 知乎直播
+- [x] CHZZK
+- [x] 嗨秀直播
+- [x] vv星球直播
+- [x] 17Live
+- [x] 浪Live
+- [x] 畅聊直播
+- [x] 飘飘直播
+- [x] 六间房直播
+- [x] 乐嗨直播
+- [x] 花猫直播
+- [x] Shopee
+- [x] Youtube
+- [x] 淘宝
+- [x] 京东
+- [x] Faceit
+- [x] 咪咕
+- [x] 连接直播
+- [x] 来秀直播
+- [x] Picarto
+- [ ] 更多平台正在更新中
+
+
+
+## 🎈项目结构
+
+```
+.
+└── DouyinLiveRecorder/
+ ├── /config -> (config record)
+ ├── /logs -> (save runing log file)
+ ├── /backup_config -> (backup file)
+ ├── /douyinliverecorder -> (package)
+ ├── initializer.py-> (check and install nodejs)
+ ├── spider.py-> (get live data)
+ ├── stream.py-> (get live stream address)
+ ├── utils.py -> (contains utility functions)
+ ├── logger.py -> (logger handdle)
+ ├── room.py -> (get room info)
+ ├── ab_sign.py-> (generate dy token)
+ ├── /javascript -> (some decrypt code)
+ ├── main.py -> (main file)
+ ├── ffmpeg_install.py -> (ffmpeg install script)
+ ├── demo.py -> (call package test demo)
+ ├── msg_push.py -> (send live status update message)
+ ├── ffmpeg.exe -> (record video)
+ ├── index.html -> (play m3u8 and flv video)
+ ├── requirements.txt -> (library dependencies)
+ ├── docker-compose.yaml -> (Container Orchestration File)
+ ├── Dockerfile -> (Application Build Recipe)
+ ├── StopRecording.vbs -> (stop recording script on Windows)
+ ...
+```
+
+
+
+## 🌱使用说明
+
+- 对于只想使用录制软件的小白用户,进入[Releases](https://github.com/ihmily/DouyinLiveRecorder/releases) 中下载最新发布的 zip压缩包即可,里面有打包好的录制软件。(有些电脑可能会报毒,直接忽略即可,如果下载时被浏览器屏蔽,请更换浏览器下载)
+
+- 压缩包解压后,在 `config` 文件夹内的 `URL_config.ini` 中添加录制直播间地址,一行一个直播间地址。如果要自定义配置录制,可以修改`config.ini` 文件,推荐将录制格式修改为`ts`。
+- 以上步骤都做好后,就可以运行`DouyinLiveRecorder.exe` 程序进行录制了。录制的视频文件保存在同目录下的 `downloads` 文件夹内。
+
+- 另外,如果需要录制TikTok、AfreecaTV等海外平台,请在配置文件中设置开启代理并添加proxy_addr链接 如:`127.0.0.1:7890` (这只是示例地址,具体根据实际填写)。
+
+- 假如`URL_config.ini`文件中添加的直播间地址,有个别直播间暂时不想录制又不想移除链接,可以在对应直播间的链接开头加上`#`,那么将停止该直播间的监测以及录制。
+
+- 软件默认录制清晰度为 `原画` ,如果要单独设置某个直播间的录制画质,可以在添加直播间地址时前面加上画质即可,如`超清,https://live.douyin.com/745964462470` 记得中间要有`,` 分隔。
+
+- 如果要长时间挂着软件循环监测直播,最好循环时间设置长一点(咱也不差没录制到的那几分钟),避免因请求频繁导致被官方封禁IP 。
+
+- 要停止直播录制,Windows平台可执行StopRecording.vbs脚本文件,或者在录制界面使用 `Ctrl+C ` 组合键中断录制,若要停止其中某个直播间的录制,可在`URL_config.ini`文件中的地址前加#,会自动停止对应直播间的录制并正常保存已录制的视频。
+- 最后,欢迎右上角给本项目一个star,同时也非常乐意大家提交pr。
+
+
+
+直播间链接示例:
+
+```
+抖音:
+https://live.douyin.com/745964462470
+https://v.douyin.com/iQFeBnt/
+https://live.douyin.com/yall1102 (链接+抖音号)
+https://v.douyin.com/CeiU5cbX (主播主页地址)
+
+TikTok:
+https://www.tiktok.com/@pearlgaga88/live
+
+快手:
+https://live.kuaishou.com/u/yall1102
+
+虎牙:
+https://www.huya.com/52333
+
+斗鱼:
+https://www.douyu.com/3637778?dyshid=
+https://www.douyu.com/topic/wzDBLS6?rid=4921614&dyshid=
+
+YY:
+https://www.yy.com/22490906/22490906
+
+B站:
+https://live.bilibili.com/320
+
+小红书(直播间分享地址):
+http://xhslink.com/xpJpfM
+
+bigo直播:
+https://www.bigo.tv/cn/716418802
+
+buled直播:
+https://app.blued.cn/live?id=Mp6G2R
+
+SOOP:
+https://play.sooplive.co.kr/sw7love
+
+网易cc:
+https://cc.163.com/583946984
+
+千度热播:
+https://qiandurebo.com/web/video.php?roomnumber=33333
+
+PandaTV:
+https://www.pandalive.co.kr/live/play/bara0109
+
+猫耳FM:
+https://fm.missevan.com/live/868895007
+
+Look直播:
+https://look.163.com/live?id=65108820&position=3
+
+WinkTV:
+https://www.winktv.co.kr/live/play/anjer1004
+
+FlexTV(TTinglive)::
+https://www.flextv.co.kr/channels/593127/live
+
+PopkonTV:
+https://www.popkontv.com/live/view?castId=wjfal007&partnerCode=P-00117
+https://www.popkontv.com/channel/notices?mcid=wjfal007&mcPartnerCode=P-00117
+
+TwitCasting:
+https://twitcasting.tv/c:uonq
+
+百度直播:
+https://live.baidu.com/m/media/pclive/pchome/live.html?room_id=9175031377&tab_category
+
+微博直播:
+https://weibo.com/l/wblive/p/show/1022:2321325026370190442592
+
+酷狗直播:
+https://fanxing2.kugou.com/50428671?refer=2177&sourceFrom=
+
+TwitchTV:
+https://www.twitch.tv/gamerbee
+
+LiveMe:
+https://www.liveme.com/zh/v/17141543493018047815/index.html
+
+花椒直播:
+https://www.huajiao.com/l/345096174
+
+流星直播:
+https://www.7u66.com/100960
+
+ShowRoom:
+https://www.showroom-live.com/room/profile?room_id=480206 (主播主页地址)
+
+Acfun:
+https://live.acfun.cn/live/179922
+
+映客直播:
+https://www.inke.cn/liveroom/index.html?uid=22954469&id=1720860391070904
+
+音播直播:
+https://live.ybw1666.com/800002949
+
+知乎直播:
+https://www.zhihu.com/people/ac3a467005c5d20381a82230101308e9 (主播主页地址)
+
+CHZZK:
+https://chzzk.naver.com/live/458f6ec20b034f49e0fc6d03921646d2
+
+嗨秀直播:
+https://www.haixiutv.com/6095106
+
+VV星球直播:
+https://h5webcdn-pro.vvxqiu.com//activity/videoShare/videoShare.html?h5Server=https://h5p.vvxqiu.com&roomId=LP115924473&platformId=vvstar
+
+17Live:
+https://17.live/en/live/6302408
+
+浪Live:
+https://www.lang.live/en-US/room/3349463
+
+畅聊直播:
+https://live.tlclw.com/106188
+
+飘飘直播:
+https://m.pp.weimipopo.com/live/preview.html?uid=91648673&anchorUid=91625862&app=plpl
+
+六间房直播:
+https://v.6.cn/634435
+
+乐嗨直播:
+https://www.lehaitv.com/8059096
+
+花猫直播:
+https://h.catshow168.com/live/preview.html?uid=19066357&anchorUid=18895331
+
+Shopee:
+https://sg.shp.ee/GmpXeuf?uid=1006401066&session=802458
+
+Youtube:
+https://www.youtube.com/watch?v=cS6zS5hi1w0
+
+淘宝(需cookie):
+https://tbzb.taobao.com/live?liveId=532359023188
+https://m.tb.cn/h.TWp0HTd
+
+京东:
+https://3.cn/28MLBy-E
+
+Faceit:
+https://www.faceit.com/zh/players/Compl1/stream
+
+连接直播:
+https://show.lailianjie.com/10000258
+
+咪咕直播:
+https://www.miguvideo.com/p/live/120000541321
+
+来秀直播:
+https://www.imkktv.com/h5/share/video.html?uid=1845195&roomId=1710496
+
+Picarto:
+https://www.picarto.tv/cuteavalanche
+```
+
+
+
+## 🎃源码运行
+使用源码运行,可参考下面的步骤。
+
+1.首先拉取或手动下载本仓库项目代码
+
+```bash
+git clone https://github.com/ihmily/DouyinLiveRecorder.git
+```
+
+2.进入项目文件夹,安装依赖
+
+```bash
+cd DouyinLiveRecorder
+```
+
+> [!TIP]
+> - 不论你是否已安装 **Python>=3.10** 环境, 都推荐使用 [**uv**](https://github.com/astral-sh/uv) 运行, 因为它可以自动管理虚拟环境和方便地管理 **Python** 版本, **不过这完全是可选的**
+> 使用以下命令安装
+> ```bash
+> # 在 macOS 和 Linux 上安装 uv
+> curl -LsSf https://astral.sh/uv/install.sh | sh
+> ```
+> ```powershell
+> # 在 Windows 上安装 uv
+> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
+> ```
+> - 如果安装依赖速度太慢, 你可以考虑使用国内 pip 镜像源:
+> 在 `pip` 命令使用 `-i` 参数指定, 如 `pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple`
+> 或者在 `uv` 命令 `--index` 选项指定, 如 `uv sync --index https://pypi.tuna.tsinghua.edu.cn/simple`
+
+
+
+ 如果已安装 Python>=3.10 环境
+
+ - :white_check_mark: 在虚拟环境中安装 (推荐)
+
+ 1. 创建虚拟环境
+
+ - 使用系统已安装的 Python, 不使用 uv
+
+ ```bash
+ python -m venv .venv
+ ```
+
+ - 使用 uv, 默认使用系统 Python, 你可以添加 `--python` 选项指定 Python 版本而不使用系统 Python [uv官方文档](https://docs.astral.sh/uv/concepts/python-versions/)
+
+ ```bash
+ uv venv
+ ```
+
+ 2. 在终端激活虚拟环境 (在未安装 uv 或你想要手动激活虚拟环境时执行, 若已安装 uv, 可以跳过这一步, uv 会自动激活并使用虚拟环境)
+
+ **Bash** 中
+ ```bash
+ source .venv/Scripts/activate
+ ```
+
+ **Powershell** 中
+ ```powershell
+ .venv\Scripts\activate.ps1
+ ```
+
+ **Windows CMD** 中
+ ```bat
+ .venv\Scripts\activate.bat
+ ```
+
+ 3. 安装依赖
+
+ ```bash
+ # 使用 pip (若安装太慢或失败, 可使用 `-i` 指定镜像源)
+ pip3 install -U pip && pip3 install -r requirements.txt
+ # 或者使用 uv (可使用 `--index` 指定镜像源)
+ uv sync
+ # 或者
+ uv pip sync requirements.txt
+ ```
+
+ - :x: 在系统 Python 环境中安装 (不推荐)
+
+ ```bash
+ pip3 install -U pip && pip3 install -r requirements.txt
+ ```
+
+
+
+
+
+ 如果未安装 Python>=3.10 环境
+
+ 你可以使用 [**uv**](https://github.com/astral-sh/uv) 安装依赖
+
+ ```bash
+ # uv 将使用 3.10 及以上的最新 python 发行版自动创建并使用虚拟环境, 可使用 --python 选项指定 python 版本, 参见 https://docs.astral.sh/uv/reference/cli/#uv-sync--python 和 https://docs.astral.sh/uv/reference/cli/#uv-pip-sync--python
+ uv sync
+ # 或
+ uv pip sync requirements.txt
+ ```
+
+
+
+3.安装[FFmpeg](https://ffmpeg.org/download.html#build-linux),如果是Windows系统,这一步可跳过。对于Linux系统,执行以下命令安装
+
+CentOS执行
+
+```bash
+yum install epel-release
+yum install ffmpeg
+```
+
+Ubuntu则执行
+
+```bash
+apt update
+apt install ffmpeg
+```
+
+macOS 执行
+
+**如果已经安装 Homebrew 请跳过这一步**
+
+```bash
+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
+```
+
+```bash
+brew install ffmpeg
+```
+
+4.运行程序
+
+```python
+python main.py
+
+```
+或
+
+```bash
+uv run main.py
+```
+
+其中Linux系统请使用`python3 main.py` 运行。
+
+
+## 🐋容器运行
+
+在运行命令之前,请确保您的机器上安装了 [Docker](https://docs.docker.com/get-docker/) 和 [Docker Compose](https://docs.docker.com/compose/install/)
+
+1.快速启动
+
+最简单方法是运行项目中的 [docker-compose.yaml](https://github.com/ihmily/DouyinLiveRecorder/blob/main/docker-compose.yaml) 文件,只需简单执行以下命令:
+
+```bash
+docker-compose up
+```
+
+可选 `-d` 在后台运行。
+
+
+
+2.构建镜像(可选)
+
+如果你只想简单的运行程序,则不需要做这一步。Docker镜像仓库中代码版本可能不是最新的,如果要运行本仓库主分支最新代码,可以本地自定义构建,通过修改 [docker-compose.yaml](https://github.com/ihmily/DouyinLiveRecorder/blob/main/docker-compose.yaml) 文件,如将镜像名修改为 `douyin-live-recorder:latest`,并取消 `# build: .` 注释,然后再执行
+
+```bash
+docker build -t douyin-live-recorder:latest .
+docker-compose up
+```
+
+或者直接使用下面命令进行构建并启动
+
+```bash
+docker-compose -f docker-compose.yaml up
+```
+
+
+
+3.停止容器实例
+
+```bash
+docker-compose stop
+```
+
+
+
+4.注意事项
+
+①在docker容器内运行本程序之前,请先在配置文件中添加要录制的直播间地址。
+
+②在容器内时,如果手动中断容器运行停止录制,会导致正在录制的视频文件损坏!
+
+**无论哪种运行方式,为避免手动中断或者异常中断导致录制的视频文件损坏的情况,推荐使用 `ts` 格式保存**。
+
+
+
+## 🤖相关项目
+
+- StreamCap: https://github.com/ihmily/StreamCap
+- streamget: https://github.com/ihmily/streamget
+
+
+
+## ❤️贡献者
+
+ [](https://github.com/ihmily)
+[](https://github.com/iridescentGray)
+[](https://github.com/annidy)
+[](https://github.com/wwkk2580)
+[](https://github.com/missuo)
+
+
+
+
+[](https://github.com/justdoiting)
+[](https://github.com/dhbxs)
+[](https://github.com/wujiyu115)
+[](https://github.com/zhanghao333)
+
+
+ [](https://github.com/HoratioShaw)
+[](https://github.com/nov30th)
+[](https://github.com/727155455)
+[](https://github.com/nixingshiguang)
+[](https://github.com/1411430556)
+[](https://github.com/Ovear)
+
+
+## ⏳提交日志
+
+- 20251024
+ - 修复抖音风控无法获取数据问题
+
+ - 新增soop.com录制支持
+
+ - 修复bigo录制
+
+- 20250127
+ - 新增淘宝、京东、faceit直播录制
+ - 修复小红书直播流录制以及转码问题
+ - 修复畅聊、VV星球、flexTV直播录制
+ - 修复批量微信直播推送
+ - 新增email发送ssl和port配置
+ - 新增强制转h264配置
+ - 更新ffmpeg版本
+ - 重构包为异步函数!
+
+- 20241130
+ - 新增shopee、youtube直播录制
+ - 新增支持自定义m3u8、flv地址录制
+ - 新增自定义执行脚本,支持python、bat、bash等
+ - 修复YY直播、花椒直播和小红书直播录制
+ - 修复b站标题获取错误
+ - 修复log日志错误
+- 20241030
+ - 新增嗨秀直播、vv星球直播、17Live、浪Live、SOOP、畅聊直播(原时光直播)、飘飘直播、六间房直播、乐嗨直播、花猫直播等10个平台直播录制
+ - 修复小红书直播录制,支持小红书作者主页地址录制直播
+ - 新增支持ntfy消息推送,以及新增支持批量推送多个地址(逗号分隔多个推送地址)
+ - 修复Liveme直播录制、twitch直播录制
+ - 新增Windows平台一键停止录制VB脚本程序
+- 20241005
+ - 新增邮箱和Bark推送
+ - 新增直播注释停止录制
+ - 优化分段录制
+ - 重构部分代码
+- 20240928
+ - 新增知乎直播、CHZZK直播录制
+ - 修复音播直播录制
+- 20240903
+ - 新增抖音双屏录制、音播直播录制
+ - 修复PandaTV、bigo直播录制
+- 20240713
+ - 新增映客直播录制
+- 20240705
+ - 新增时光直播录制
+- 20240701
+ - 修复虎牙直播录制2分钟断流问题
+ - 新增自定义直播推送内容
+- 20240621
+ - 新增Acfun、ShowRoom直播录制
+ - 修复微博录制、新增直播源线路
+ - 修复斗鱼直播60帧录制
+ - 修复酷狗直播录制
+ - 修复TikTok部分无法解析直播源
+ - 修复抖音无法录制连麦直播
+- 20240510
+ - 修复部分虎牙直播间录制错误
+- 20240508
+ - 修复花椒直播录制
+ - 更改文件路径解析方式 [@kaine1973](https://github.com/kaine1973)
+- 20240506
+ - 修复抖音录制画质解析bug
+ - 修复虎牙录制 60帧最高画质问题
+ - 新增流星直播录制
+- 20240427
+ - 新增LiveMe、花椒直播录制
+- 20240425
+ - 新增TwitchTV直播录制
+- 20240424
+ - 新增酷狗直播录制、优化PopkonTV直播录制
+- 20240423
+ - 新增百度直播录制、微博直播录制
+ - 修复斗鱼录制直播回放的问题
+ - 新增直播源地址显示以及输出到日志文件设置
+- 20240311
+ - 修复海外平台录制bug,增加画质选择,增强录制稳定性
+ - 修复虎牙录制bug (虎牙`一起看`频道 有特殊限制,有时无法录制)
+- 20240309
+ - 修复虎牙直播、小红书直播和B站直播录制
+ - 新增5个直播平台录制,包括winktv、flextv、look、popkontv、twitcasting
+ - 新增部分海外平台账号密码配置,实现自动登录并更新配置文件中的cookie
+ - 新增自定义配置需要使用代理录制的平台
+ - 新增只推送开播消息不进行录制设置
+ - 修复了一些bug
+- 20240209
+ - 优化AfreecaTV录制,新增账号密码登录获取cookie以及持久保存
+ - 修复了小红书直播因官方更新直播域名,导致无法录制直播的问题
+ - 修复了更新URL配置文件的bug
+ - 最后,祝大家新年快乐!
+
+点击展开更多提交日志
+
+- 20240129
+ - 新增猫耳FM直播录制
+- 20240127
+ - 新增千度热播直播录制、新增pandaTV(韩国)直播录制
+ - 新增telegram直播状态消息推送,修复了某些bug
+ - 新增自定义设置不同直播间的录制画质(即每个直播间录制画质可不同)
+ - 修改录制视频保存路径为 `downloads` 文件夹,并且分平台进行保存。
+- 20240114
+ - 新增网易cc直播录制,优化ffmpeg参数,修改AfreecaTV输入直播地址格式
+ - 修改日志记录器 @[iridescentGray](https://github.com/iridescentGray)
+- 20240102
+ - 修复Linux上运行,新增docker配置文件
+- 20231210
+ - 修复录制分段bug,修复bigo录制检测bug
+ - 新增自定义修改录制主播名
+ - 新增AfreecaTV直播录制,修复某些可能会发生的bug
+- 20231207
+ - 新增blued直播录制,修复YY直播录制,新增直播结束消息推送
+- 20231206
+ - 新增bigo直播录制
+- 20231203
+ - 新增小红书直播录制(全网首发),目前小红书官方没有切换清晰度功能,因此直播录制也只有默认画质
+ - 小红书录制暂时无法循环监测,每次主播开启直播,都要重新获取一次链接
+ - 获取链接的方式为 将直播间转发到微信,在微信中打开后,复制页面的链接。
+- 20231030
+ - 本次更新只是进行修复,没时间新增功能。
+ - 欢迎各位大佬提pr 帮忙更新维护
+- 20230930
+ - 新增抖音从接口获取直播流,增强稳定性
+ - 修改快手获取直播流的方式,改用从官方接口获取
+ - 祝大家中秋节快乐!
+- 20230919
+ - 修复了快手版本更新后录制出错的问题,增加了其自动获取cookie(~~稳定性未知~~)
+ - 修复了TikTok显示正在直播但不进行录制的问题
+- 20230907
+ - 修复了因抖音官方更新了版本导致的录制出错以及短链接转换出错
+ - 修复B站无法录制原画视频的bug
+ - 修改了配置文件字段,新增各平台自定义设置Cookie
+- 20230903
+ - 修复了TikTok录制时报644无法录制的问题
+ - 新增直播状态推送到钉钉和微信的功能,如有需要请看 [设置推送教程](https://d04vqdiqwr3.feishu.cn/docx/XFPwdDDvfobbzlxhmMYcvouynDh?from=from_copylink)
+ - 最近比较忙,其他问题有时间再更新
+- 20230816
+ - 修复斗鱼直播(官方更新了字段)和快手直播录制出错的问题
+- 20230814
+ - 新增B站直播录制
+ - 写了一个在线播放M3U8和FLV视频的网页源码,打开即可食用
+- 20230812
+ - 新增YY直播录制
+- 20230808
+ - 修复主播重新开播无法再次录制的问题
+- 20230807
+ - 新增了斗鱼直播录制
+ - 修复显示录制完成之后会重新开始录制的问题
+- 20230805
+ - 新增了虎牙直播录制,其暂时只能用flv视频流进行录制
+ - Web API 新增了快手和虎牙这两个平台的直播流解析(TikTok要代理)
+- 20230804
+ - 新增了快手直播录制,优化了部分代码
+ - 上传了一个自动化获取抖音直播间页面Cookie的代码,可以用于录制
+- 20230803
+ - 通宵更新
+ - 新增了国际版抖音TikTok的直播录制,去除冗余 简化了部分代码
+- 20230724
+ - 新增了一个通过抖音直播间地址获取直播视频流链接的API接口,上传即可用
+
+
+
+## 有问题可以提issue, 我会在这里持续添加更多直播平台的录制 欢迎Star
+####
diff --git a/StopRecording.vbs b/StopRecording.vbs
new file mode 100644
index 0000000..f6c47c1
--- /dev/null
+++ b/StopRecording.vbs
@@ -0,0 +1,69 @@
+'********************************************************************************************/
+'* File Name : StopRecording.vbs
+'* Created Date : 2024-10-15 01:50:30
+'* Author : Hmily
+'* GitHub : http://github.com/ihmily
+'* Description : This script is designed to terminate the process of live recording
+'********************************************************************************************/
+
+Dim objWMIService, colProcesses, objProcess
+Dim intResponse
+strComputer = "."
+On Error Resume Next
+intResponse = MsgBox("ȷҪкֱ̨¼ƽ", vbYesNo + vbQuestion, "ȷϽ")
+
+If intResponse = vbYes Then
+ Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
+ If Err.Number <> 0 Then
+ Err.Clear
+ End If
+
+ Set colProcesses = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'ffmpeg.exe'")
+ Set colProcesses2 = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'pythonw.exe'")
+ Set colProcesses3 = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'DouyinLiveRecorder.exe'")
+ If Err.Number <> 0 Then
+ Err.Clear
+ End If
+
+ If Not objWMIService Is Nothing And Not colProcesses Is Nothing And Not colProcesses2 Is Nothing Then
+ If colProcesses2.Count = 0 And colProcesses3.Count = 0 Then
+ MsgBox "ûҵ¼ƳĽ", vbExclamation, "ʾϢ"
+ WScript.Quit(1)
+ Else
+ For Each objProcess in colProcesses
+ objProcess.Terminate()
+ If Err.Number <> 0 Then
+ objShell.Run "taskkill /f /im " & objProcess.Name, 0, True
+ Err.Clear
+ End If
+ Next
+ End If
+ Else
+ objShell.Run "taskkill /f /im " & objProcess.Name, 0, True
+ End If
+ MsgBox "ѳɹ¼ֱḶ̌" & vbCrLf & "رմ˴30Զֹͣ¼Ƴ", vbInformation, "ʾϢ"
+
+ WScript.Sleep 10000
+ If colProcesses3.Count <> 0 Then
+ Set colProcesses_ = colProcesses3
+ Else
+ Set colProcesses_ = colProcesses2
+ End If
+ For Each objProcess in colProcesses_
+ objProcess.Terminate()
+ If Err.Number <> 0 Then
+ objShell.Run "taskkill /f /im " & objProcess.Name, 0, True
+ Err.Clear
+ End If
+ Next
+Else
+ MsgBox "ȡ¼Ʋ", vbExclamation, "ʾϢ"
+End If
+
+On Error GoTo 0
+Set objWMIService = Nothing
+Set colProcesses = Nothing
+Set colProcesses2 = Nothing
+Set colProcesses3 = Nothing
+Set objProcess = Nothing
+Set objShell = Nothing
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..c764715
--- /dev/null
+++ b/main.py
@@ -0,0 +1,2255 @@
+# -*- encoding: utf-8 -*-
+
+"""
+Author: Hmily
+GitHub: https://github.com/ihmily
+Date: 2023-07-17 23:52:05
+Update: 2025-10-23 19:48:05
+Copyright (c) 2023-2025 by Hmily, All Rights Reserved.
+Function: Record live stream video.
+"""
+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.request
+from urllib.error import URLError, HTTPError
+from typing import Any
+import configparser
+import httpx
+from src import spider, stream
+from src.proxy import ProxyDetector
+from src.utils import logger
+from src import utils
+from msg_push import (
+ dingtalk, xizhi, tg_bot, send_email, bark, ntfy, pushplus
+)
+from ffmpeg_install import (
+ check_ffmpeg, ffmpeg_path, current_env_path
+)
+
+
+
+
+version = "v4.0.7"
+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|Picarto")
+
+recording = set()
+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_tuples_list = []
+url_comments = []
+text_no_repeat_url = []
+create_var = locals()
+first_start = True
+exit_recording = False
+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"[\/\\\:\*\??\"\<\>\|.。,, ~!· ]"
+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):
+ sys.exit(0)
+
+
+signal.signal(signal.SIGTERM, signal_handler)
+
+
+def display_info() -> None:
+ global start_display_time
+ time.sleep(5)
+ while True:
+ try:
+ sys.stdout.flush()
+ 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("录制分段开启: 否", end=" | ")
+ if create_time_file:
+ print("是否生成时间文件: 是", 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('\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) -> 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:
+ 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):
+ if system_type == 'nt':
+ 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:
+ try:
+ if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0:
+ ffmpeg_command = [
+ "ffmpeg",
+ "-i", converts_file_path,
+ "-c:v", "copy",
+ "-c:a", "copy",
+ "-map", "0",
+ "-f", "segment",
+ "-segment_time", segment_time,
+ "-segment_format", segment_format,
+ "-reset_timestamps", "1",
+ "-movflags", "+frag_keyframe+empty_moov",
+ segment_save_file_path,
+ ]
+ _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'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:
+ try:
+ if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0:
+ if converts_to_h264:
+ color_obj.print_colored("正在转码为MP4格式并重新编码为h264\n", color_obj.YELLOW)
+ ffmpeg_command = [
+ "ffmpeg", "-i", converts_file_path,
+ "-c:v", "libx264",
+ "-preset", "veryfast",
+ "-crf", "23",
+ "-vf", "format=yuv420p",
+ "-c:a", "copy",
+ "-f", "mp4", converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4",
+ ]
+ else:
+ color_obj.print_colored("正在转码为MP4格式\n", color_obj.YELLOW)
+ ffmpeg_command = [
+ "ffmpeg", "-i", converts_file_path,
+ "-c:v", "copy",
+ "-c:a", "copy",
+ "-f", "mp4", 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'Error occurred during conversion: {e}')
+ except Exception as e:
+ logger.error(f'An unknown error occurred: {e}')
+
+
+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:
+ global max_request, error_count, pre_max_request, error_window
+ preset = max_request
+
+ while True:
+ time.sleep(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:
+ max_request = max(1, max_request - 1)
+ elif error_rate < error_threshold / 2 and max_request < preset:
+ max_request += 1
+ else:
+ pass
+
+ 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:
+ 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
+ ),
+ 'PUSHPLUS': lambda: pushplus(pushplus_token, msg_title, content),
+ }
+
+ 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('脚本无执行权限!, 若是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:
+ 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
+ color_obj.print_colored(f"[{record_name}]已经从录制列表中移除\n", color_obj.YELLOW)
+
+
+def direct_download_stream(source_url: str, save_path: str, record_name: str, live_url: str, platform: str) -> bool:
+ try:
+ with open(save_path, 'wb') as f:
+ client = httpx.Client(timeout=None)
+
+ headers = {}
+ header_params = get_record_headers(platform, live_url)
+ if header_params:
+ key, value = header_params.split(":", 1)
+ headers[key] = value
+
+ with client.stream('GET', source_url, headers=headers, follow_redirects=True) as response:
+ if response.status_code != 200:
+ logger.error(f"请求直播流失败,状态码: {response.status_code}")
+ return False
+
+ downloaded = 0
+ chunk_size = 1024 * 16
+
+ for chunk in response.iter_bytes(chunk_size):
+ if live_url in url_comments or exit_recording:
+ color_obj.print_colored(f"[{record_name}]录制时已被注释或请求停止,下载中断", color_obj.YELLOW)
+ clear_record_info(record_name, live_url)
+ return False
+
+ if chunk:
+ f.write(chunk)
+ downloaded += len(chunk)
+ print()
+ return True
+ except Exception as e:
+ logger.error(f"FLV下载错误: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
+ return False
+
+
+def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, save_type: str,
+ script_command: str | None = None) -> bool:
+ save_file_path = ffmpeg_command[-1]
+ process = subprocess.Popen(
+ ffmpeg_command, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)
+ )
+
+ 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
+ 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:
+ threading.Thread(target=converts_mp4, args=(path, delete_origin_file)).start()
+ else:
+ threading.Thread(target=converts_mp4, args=(save_file_path, delete_origin_file)).start()
+ print(f"\n{record_name} {stop_time} 直播录制完成\n")
+
+ if script_command:
+ logger.debug("开始执行脚本命令!")
+ if "python" in script_command:
+ params = [
+ f'--record_name "{record_name}"',
+ f'--save_file_path "{save_file_path}"',
+ f'--save_type {save_type}',
+ f'--split_video_by_time {split_video_by_time}',
+ f'--converts_to_mp4 {converts_to_mp4}',
+ ]
+ else:
+ params = [
+ f'"{record_name.split(" ", maxsplit=1)[-1]}"',
+ f'"{save_file_path}"',
+ save_type,
+ f'split_video_by_time:{split_video_by_time}',
+ f'converts_to_mp4:{converts_to_mp4}'
+ ]
+ script_command = script_command.strip() + ' ' + ' '.join(params)
+ run_script(script_command)
+ logger.debug("脚本命令执行结束!")
+
+ else:
+ color_obj.print_colored(f"\n{record_name} {stop_time} 直播录制出错,返回码: {return_code}\n", color_obj.RED)
+
+ recording.discard(record_name)
+ return False
+
+
+def clean_name(input_text):
+ cleaned_name = re.sub(rstr, "_", input_text.strip()).strip('_')
+ cleaned_name = cleaned_name.replace("(", "(").replace(")", ")")
+ if clean_emoji:
+ cleaned_name = utils.remove_emojis(cleaned_name, '_').strip('_')
+ return cleaned_name or '空白昵称'
+
+
+def get_quality_code(qn):
+ QUALITY_MAPPING = {
+ "原画": "OD",
+ "蓝光": "BD",
+ "超清": "UHD",
+ "高清": "HD",
+ "标清": "SD",
+ "流畅": "LD"
+ }
+ return QUALITY_MAPPING.get(qn)
+
+
+def get_record_headers(platform, live_url):
+ live_domain = '/'.join(live_url.split('/')[0:3])
+ 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',
+ '17Live': 'referer:https://17.live/en/live/6302408',
+ '浪Live': 'referer:https://www.lang.live',
+ 'shopee': f'origin:{live_domain}',
+ 'Blued直播': 'referer:https://app.blued.cn'
+ }
+ return record_headers.get(platform)
+
+
+def is_flv_preferred_platform(link):
+ return any(i in link for i in ["douyin", "tiktok"])
+
+
+def select_source_url(link, stream_info):
+ if is_flv_preferred_platform(link):
+ codec = utils.get_query_params(stream_info.get('flv_url'), "codec")
+ if codec and codec[0] == 'h265':
+ logger.warning("FLV is not supported for h265 codec, use HLS source instead")
+ else:
+ return stream_info.get('flv_url')
+
+ return stream_info.get('record_url')
+
+
+def start_record(url_data: tuple, count_variable: int = -1) -> None:
+ global error_count
+
+ while True:
+ try:
+ record_finished = False
+ run_once = False
+ start_pushed = False
+ new_record_url = ''
+ count_time = time.time()
+ retry = 0
+ record_quality_zh, record_url, anchor_name = url_data
+ record_quality = get_quality_code(record_quality_zh)
+ proxy_address = proxy_addr
+ platform = '未知平台'
+ live_domain = '/'.join(record_url.split('/')[0:3])
+
+ if proxy_addr:
+ proxy_address = None
+ for platform in enable_proxy_platform_list:
+ if platform and platform.strip() in record_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 record_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 and '/user/' not in record_url:
+ json_data = asyncio.run(spider.get_douyin_web_stream_data(
+ url=record_url,
+ proxy_addr=proxy_address,
+ cookies=dy_cookie))
+ else:
+ json_data = asyncio.run(spider.get_douyin_app_stream_data(
+ url=record_url,
+ proxy_addr=proxy_address,
+ cookies=dy_cookie))
+ port_info = asyncio.run(
+ stream.get_douyin_stream_url(json_data, record_quality, proxy_address))
+
+ elif record_url.find("https://www.tiktok.com/") > -1:
+ platform = 'TikTok直播'
+ with semaphore:
+ if global_proxy or proxy_address:
+ json_data = asyncio.run(spider.get_tiktok_stream_data(
+ url=record_url,
+ proxy_addr=proxy_address,
+ cookies=tiktok_cookie))
+ port_info = asyncio.run(
+ stream.get_tiktok_stream_url(json_data, record_quality, proxy_address))
+ else:
+ logger.error("错误信息: 网络异常,请检查网络是否能正常访问TikTok平台")
+
+ elif record_url.find("https://live.kuaishou.com/") > -1:
+ platform = '快手直播'
+ with semaphore:
+ json_data = asyncio.run(spider.get_kuaishou_stream_data(
+ url=record_url,
+ proxy_addr=proxy_address,
+ cookies=ks_cookie))
+ port_info = asyncio.run(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 ['OD', 'BD', 'UHD']:
+ json_data = asyncio.run(spider.get_huya_stream_data(
+ url=record_url,
+ proxy_addr=proxy_address,
+ cookies=hy_cookie))
+ port_info = asyncio.run(stream.get_huya_stream_url(json_data, record_quality))
+ else:
+ port_info = asyncio.run(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 = asyncio.run(spider.get_douyu_info_data(
+ url=record_url, proxy_addr=proxy_address, cookies=douyu_cookie))
+ port_info = asyncio.run(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 = asyncio.run(spider.get_yy_stream_data(
+ url=record_url, proxy_addr=proxy_address, cookies=yy_cookie))
+ port_info = asyncio.run(stream.get_yy_stream_url(json_data))
+
+ elif record_url.find("https://live.bilibili.com/") > -1:
+ platform = 'B站直播'
+ with semaphore:
+ json_data = asyncio.run(spider.get_bilibili_room_info(
+ url=record_url, proxy_addr=proxy_address, cookies=bili_cookie))
+ port_info = asyncio.run(stream.get_bilibili_stream_url(
+ json_data, video_quality=record_quality, cookies=bili_cookie, proxy_addr=proxy_address))
+
+ elif record_url.find("http://xhslink.com/") > -1 or \
+ record_url.find("https://www.xiaohongshu.com/") > -1:
+ platform = '小红书直播'
+ with semaphore:
+ port_info = asyncio.run(spider.get_xhs_stream_url(
+ record_url, proxy_addr=proxy_address, cookies=xhs_cookie))
+ retry += 1
+
+ elif record_url.find("www.bigo.tv/") > -1 or record_url.find("slink.bigovideo.tv/") > -1:
+ platform = 'Bigo直播'
+ with semaphore:
+ port_info = asyncio.run(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 = asyncio.run(spider.get_blued_stream_url(
+ record_url, proxy_addr=proxy_address, cookies=blued_cookie))
+
+ elif record_url.find("sooplive.co.kr/") > -1 or record_url.find("sooplive.com/") > -1:
+ platform = 'SOOP'
+ with semaphore:
+ if global_proxy or proxy_address:
+ json_data = asyncio.run(spider.get_sooplive_stream_data(
+ url=record_url, proxy_addr=proxy_address,
+ cookies=sooplive_cookie,
+ username=sooplive_username,
+ password=sooplive_password
+ ))
+ if json_data and json_data.get('new_cookies'):
+ utils.update_config(
+ config_file, 'Cookie', 'sooplive_cookie', json_data['new_cookies']
+ )
+ port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
+ else:
+ logger.error("错误信息: 网络异常,请检查本网络是否能正常访问SOOP平台")
+
+ elif record_url.find("cc.163.com/") > -1:
+ platform = '网易CC直播'
+ with semaphore:
+ json_data = asyncio.run(spider.get_netease_stream_data(
+ url=record_url, cookies=netease_cookie))
+ port_info = asyncio.run(stream.get_netease_stream_url(json_data, record_quality))
+
+ elif record_url.find("qiandurebo.com/") > -1:
+ platform = '千度热播'
+ with semaphore:
+ port_info = asyncio.run(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 = asyncio.run(spider.get_pandatv_stream_data(
+ url=record_url,
+ proxy_addr=proxy_address,
+ cookies=pandatv_cookie
+ ))
+ port_info = asyncio.run(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 = asyncio.run(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 = asyncio.run(spider.get_winktv_stream_data(
+ url=record_url,
+ proxy_addr=proxy_address,
+ cookies=winktv_cookie))
+ port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
+ else:
+ logger.error("错误信息: 网络异常,请检查本网络是否能正常访问WinkTV直播平台")
+
+ elif record_url.find("www.flextv.co.kr/") > -1 or record_url.find("www.ttinglive.com/") > -1:
+ platform = 'FlexTV'
+ with semaphore:
+ if global_proxy or proxy_address:
+ json_data = asyncio.run(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'):
+ utils.update_config(
+ config_file, 'Cookie', 'flextv_cookie', json_data['new_cookies']
+ )
+ if 'play_url_list' in json_data:
+ port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
+ else:
+ port_info = json_data
+ else:
+ logger.error("错误信息: 网络异常,请检查本网络是否能正常访问FlexTV直播平台")
+
+ elif record_url.find("look.163.com/") > -1:
+ platform = 'Look直播'
+ with semaphore:
+ port_info = asyncio.run(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 = asyncio.run(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'):
+ utils.update_config(
+ file_path=config_file, section='Authorization', key='popkontv_token',
+ new_value=port_info['new_token']
+ )
+
+ else:
+ logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PopkonTV直播平台")
+
+ elif record_url.find("twitcasting.tv/") > -1:
+ platform = 'TwitCasting'
+ with semaphore:
+ json_data = asyncio.run(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
+ ))
+ port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=False))
+
+ if port_info and port_info.get('new_cookies'):
+ utils.update_config(
+ file_path=config_file, section='Cookie', key='twitcasting_cookie',
+ new_value=port_info['new_cookies']
+ )
+
+ elif record_url.find("live.baidu.com/") > -1:
+ platform = '百度直播'
+ with semaphore:
+ json_data = asyncio.run(spider.get_baidu_stream_data(
+ url=record_url,
+ proxy_addr=proxy_address,
+ cookies=baidu_cookie))
+ port_info = asyncio.run(stream.get_stream_url(json_data, record_quality))
+
+ elif record_url.find("weibo.com/") > -1:
+ platform = '微博直播'
+ with semaphore:
+ json_data = asyncio.run(spider.get_weibo_stream_data(
+ url=record_url, proxy_addr=proxy_address, cookies=weibo_cookie))
+ port_info = asyncio.run(stream.get_stream_url(
+ json_data, record_quality, hls_extra_key='m3u8_url'))
+
+ elif record_url.find("kugou.com/") > -1:
+ platform = '酷狗直播'
+ with semaphore:
+ port_info = asyncio.run(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 = asyncio.run(spider.get_twitchtv_stream_data(
+ url=record_url,
+ proxy_addr=proxy_address,
+ cookies=twitch_cookie
+ ))
+ port_info = asyncio.run(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 = asyncio.run(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 = asyncio.run(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 = asyncio.run(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 = asyncio.run(spider.get_showroom_stream_data(
+ url=record_url, proxy_addr=proxy_address, cookies=showroom_cookie))
+ port_info = asyncio.run(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 = asyncio.run(spider.get_acfun_stream_data(
+ url=record_url, proxy_addr=proxy_address, cookies=acfun_cookie))
+ port_info = asyncio.run(stream.get_stream_url(
+ json_data, record_quality, url_type='flv', flv_extra_key='url'))
+
+ elif record_url.find("live.tlclw.com/") > -1:
+ platform = '畅聊直播'
+ with semaphore:
+ port_info = asyncio.run(spider.get_changliao_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=changliao_cookie))
+
+ elif record_url.find("ybw1666.com/") > -1:
+ platform = '音播直播'
+ with semaphore:
+ port_info = asyncio.run(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 = asyncio.run(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 = asyncio.run(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 = asyncio.run(spider.get_chzzk_stream_data(
+ url=record_url, proxy_addr=proxy_address, cookies=chzzk_cookie))
+ port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
+
+ elif record_url.find("www.haixiutv.com/") > -1:
+ platform = '嗨秀直播'
+ with semaphore:
+ port_info = asyncio.run(spider.get_haixiu_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=haixiu_cookie))
+
+ elif record_url.find("vvxqiu.com/") > -1:
+ platform = 'VV星球'
+ with semaphore:
+ port_info = asyncio.run(spider.get_vvxqiu_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=vvxqiu_cookie))
+
+ elif record_url.find("17.live/") > -1:
+ platform = '17Live'
+ with semaphore:
+ port_info = asyncio.run(spider.get_17live_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=yiqilive_cookie))
+
+ elif record_url.find("www.lang.live/") > -1:
+ platform = '浪Live'
+ with semaphore:
+ port_info = asyncio.run(spider.get_langlive_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=langlive_cookie))
+
+ elif record_url.find("m.pp.weimipopo.com/") > -1:
+ platform = '漂漂直播'
+ with semaphore:
+ port_info = asyncio.run(spider.get_pplive_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=pplive_cookie))
+
+ elif record_url.find(".6.cn/") > -1:
+ platform = '六间房直播'
+ with semaphore:
+ port_info = asyncio.run(spider.get_6room_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=six_room_cookie))
+
+ elif record_url.find("lehaitv.com/") > -1:
+ platform = '乐嗨直播'
+ with semaphore:
+ port_info = asyncio.run(spider.get_haixiu_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=lehaitv_cookie))
+
+ elif record_url.find("h.catshow168.com/") > -1:
+ platform = '花猫直播'
+ with semaphore:
+ port_info = asyncio.run(spider.get_pplive_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=huamao_cookie))
+
+ elif record_url.find("live.shopee") > -1 or record_url.find("shp.ee/") > -1:
+ platform = 'shopee'
+ with semaphore:
+ port_info = asyncio.run(spider.get_shopee_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=shopee_cookie))
+ if port_info.get('uid'):
+ new_record_url = record_url.split('?')[0] + '?' + str(port_info['uid'])
+
+ elif record_url.find("www.youtube.com/") > -1 or record_url.find("youtu.be/") > -1:
+ platform = 'Youtube'
+ with semaphore:
+ json_data = asyncio.run(spider.get_youtube_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=youtube_cookie))
+ port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
+
+ elif record_url.find("tb.cn") > -1:
+ platform = '淘宝直播'
+ with semaphore:
+ json_data = asyncio.run(spider.get_taobao_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=taobao_cookie))
+ port_info = asyncio.run(stream.get_stream_url(
+ json_data, record_quality,
+ url_type='all', hls_extra_key='hlsUrl', flv_extra_key='flvUrl'
+ ))
+
+ elif record_url.find("3.cn") > -1 or record_url.find("m.jd.com") > -1:
+ platform = '京东直播'
+ with semaphore:
+ port_info = asyncio.run(spider.get_jd_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=jd_cookie))
+
+ elif record_url.find("faceit.com/") > -1:
+ platform = 'faceit'
+ with semaphore:
+ if global_proxy or proxy_address:
+ with semaphore:
+ json_data = asyncio.run(spider.get_faceit_stream_data(
+ url=record_url, proxy_addr=proxy_address, cookies=faceit_cookie))
+ port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
+ else:
+ logger.error("错误信息: 网络异常,请检查本网络是否能正常访问faceit直播平台")
+
+ elif record_url.find("www.miguvideo.com") > -1 or record_url.find("m.miguvideo.com") > -1:
+ platform = '咪咕直播'
+ with semaphore:
+ port_info = asyncio.run(spider.get_migu_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=migu_cookie))
+
+ elif record_url.find("show.lailianjie.com") > -1:
+ platform = '连接直播'
+ with semaphore:
+ port_info = asyncio.run(spider.get_lianjie_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=lianjie_cookie))
+
+ elif record_url.find("www.imkktv.com") > -1:
+ platform = '来秀直播'
+ with semaphore:
+ port_info = asyncio.run(spider.get_laixiu_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=laixiu_cookie))
+
+ elif record_url.find("www.picarto.tv") > -1:
+ platform = 'Picarto'
+ with semaphore:
+ port_info = asyncio.run(spider.get_picarto_stream_url(
+ url=record_url, proxy_addr=proxy_address, cookies=picarto_cookie))
+
+ elif record_url.find(".m3u8") > -1 or record_url.find(".flv") > -1:
+ platform = '自定义录制直播'
+ port_info = {
+ "anchor_name": platform + '_' + str(uuid.uuid4())[:8],
+ "is_live": True,
+ "record_url": record_url,
+ }
+ if '.flv' in record_url:
+ port_info['flv_url'] = record_url
+ else:
+ port_info['m3u8_url'] = record_url
+
+ else:
+ logger.error(f'{record_url} {platform}直播地址')
+ return
+
+ if anchor_name:
+ 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}')
+ with max_request_lock:
+ error_count += 1
+ error_window.append(1)
+ else:
+ anchor_name = clean_name(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 not url_data[-1] and run_once is False:
+ if new_record_url:
+ need_update_line_list.append(
+ f'{record_url}|{new_record_url},主播: {anchor_name.strip()}')
+ not_record_list.append(new_record_url)
+ 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))
+ threading.Thread(
+ target=push_message,
+ args=(record_name, record_url, push_content.replace(r'\n', '\n')),
+ daemon=True
+ ).start()
+ 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))
+ threading.Thread(
+ target=push_message,
+ args=(record_name, record_url, push_content.replace(r'\n', '\n')),
+ daemon=True
+ ).start()
+ start_pushed = True
+
+ if disable_record:
+ time.sleep(push_check_seconds)
+ continue
+
+ real_url = select_source_url(record_url, port_info)
+ full_path = f'{default_path}/{platform}'
+ if real_url:
+ now = datetime.datetime.today().strftime("%Y-%m-%d_%H-%M-%S")
+ live_title = port_info.get('title')
+ title_in_name = ''
+ if live_title:
+ live_title = clean_name(live_title)
+ title_in_name = live_title + '_' if filename_by_title else ''
+
+ try:
+ if len(video_save_path) > 0:
+ if not video_save_path.endswith(('/', '\\')):
+ full_path = f'{video_save_path}/{platform}'
+ else:
+ 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 folder_by_title and port_info.get('title'):
+ if folder_by_time:
+ full_path = f'{full_path}/{live_title}_{anchor_name}'
+ else:
+ full_path = f'{full_path}/{now[:10]}_{live_title}'
+ if not os.path.exists(full_path):
+ os.makedirs(full_path)
+ except Exception as e:
+ logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
+
+ if platform != '自定义录制直播':
+ if enable_https_recording and real_url.startswith("http://"):
+ real_url = real_url.replace("http://", "https://")
+
+ http_record_list = ['shopee', "migu"]
+ if platform in http_record_list:
+ real_url = real_url.replace("https://", "http://")
+
+ 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,httpproxy",
+ "-thread_queue_size", "1024",
+ "-analyzeduration", analyzeduration,
+ "-probesize", probesize,
+ "-fflags", "+discardcorrupt",
+ "-re", "-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",
+ "-avoid_negative_ts", "1"
+ ]
+
+ headers = get_record_headers(platform, record_url)
+ 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)
+ # 检查是否已有直播在录制
+ if len(recording) > 0:
+ # 如果已有直播在录制,跳过这次录制
+ return # 或者 continue,如果在循环中
+ recording.add(record_name)
+ start_record_time = datetime.datetime.now()
+ recording_time_list[record_name] = [start_record_time, record_quality_zh]
+ rec_info = f"\r{anchor_name} 准备开始录制视频: {full_path}"
+ if show_url:
+ re_plat = ('WinkTV', 'PandaTV', 'ShowRoom', 'CHZZK', 'Youtube')
+ if platform in re_plat:
+ logger.info(
+ f"{platform} | {anchor_name} | 直播源地址: {port_info.get('m3u8_url')}")
+ else:
+ logger.info(
+ f"{platform} | {anchor_name} | 直播源地址: {real_url}")
+
+ only_flv_record = False
+ only_flv_platform_list = ['shopee', '花椒直播']
+ if platform in only_flv_platform_list:
+ logger.debug(f"提示: {platform} 将强制使用FLV格式录制")
+ only_flv_record = True
+
+ only_audio_record = False
+ only_audio_platform_list = ['猫耳FM直播', 'Look直播']
+ if platform in only_audio_platform_list:
+ only_audio_record = True
+
+ record_save_type = video_save_type
+
+ if is_flv_preferred_platform(record_url) and port_info.get('flv_url'):
+ codec = utils.get_query_params(port_info['flv_url'], "codec")
+ if codec and codec[0] == 'h265':
+ logger.warning("FLV is not supported for h265 codec, use TS format instead")
+ record_save_type = "TS"
+
+ if only_audio_record or any(i in record_save_type for i in ['MP3', 'M4A']):
+ try:
+ now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
+ extension = "mp3" if "m4a" not in record_save_type.lower() else "m4a"
+ name_format = "_%03d" if split_video_by_time else ""
+ save_file_path = (f"{full_path}/{anchor_name}_{title_in_name}{now}"
+ f"{name_format}.{extension}")
+
+ if split_video_by_time:
+ print(f'\r{anchor_name} 准备开始录制音频: {save_file_path}')
+
+ if "MP3" in record_save_type:
+ command = [
+ "-map", "0:a",
+ "-c:a", "libmp3lame",
+ "-ab", "320k",
+ "-f", "segment",
+ "-segment_time", split_time,
+ "-reset_timestamps", "1",
+ save_file_path,
+ ]
+ 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_file_path,
+ ]
+
+ else:
+ if "MP3" in record_save_type:
+ command = [
+ "-map", "0:a",
+ "-c:a", "libmp3lame",
+ "-ab", "320k",
+ save_file_path,
+ ]
+
+ else:
+ command = [
+ "-map", "0:a",
+ "-c:a", "aac",
+ "-bsf:a", "aac_adtstoasc",
+ "-ab", "320k",
+ "-movflags", "+faststart",
+ save_file_path,
+ ]
+
+ ffmpeg_command.extend(command)
+ comment_end = check_subprocess(
+ record_name,
+ record_url,
+ ffmpeg_command,
+ record_save_type,
+ custom_script
+ )
+ if comment_end:
+ return
+
+ except subprocess.CalledProcessError as e:
+ logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
+ with max_request_lock:
+ error_count += 1
+ error_window.append(1)
+
+
+
+ # if only_flv_record:
+ if False:
+
+ logger.info(f"Use Direct Downloader to Download FLV Stream: {record_url}")
+ filename = anchor_name + f'_{title_in_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')
+ if flv_url:
+ recording.add(record_name)
+ start_record_time = datetime.datetime.now()
+ recording_time_list[record_name] = [start_record_time, record_quality_zh]
+
+ download_success = direct_download_stream(
+ flv_url, save_file_path, record_name, record_url, platform
+ )
+
+ if download_success:
+ record_finished = True
+ print(
+ f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制完成\n")
+
+ recording.discard(record_name)
+ else:
+ logger.debug("未找到FLV直播流,跳过录制")
+ except Exception as e:
+ clear_record_info(record_name, record_url)
+ color_obj.print_colored(
+ f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制出错,请检查网络\n",
+ color_obj.RED)
+ logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
+ with max_request_lock:
+ error_count += 1
+ error_window.append(1)
+
+ elif record_save_type == "FLV":
+ filename = anchor_name + f'_{title_in_name}' + now + ".flv"
+ 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}_{title_in_name}{now}_%03d.flv"
+ command = [
+ "-map", "0",
+ "-c:v", "copy",
+ "-c:a", "copy",
+ "-bsf:a", "aac_adtstoasc",
+ "-f", "segment",
+ "-segment_time", split_time,
+ "-segment_format", "flv",
+ "-reset_timestamps", "1",
+ save_file_path
+ ]
+
+ else:
+ command = [
+ "-map", "0",
+ "-c:v", "copy",
+ "-c:a", "copy",
+ "-bsf:a", "aac_adtstoasc",
+ "-f", "flv",
+ "{path}".format(path=save_file_path),
+ ]
+ ffmpeg_command.extend(command)
+
+ comment_end = check_subprocess(
+ record_name,
+ record_url,
+ ffmpeg_command,
+ record_save_type,
+ custom_script
+ )
+ if comment_end:
+ return
+
+ except subprocess.CalledProcessError as e:
+ logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
+ with max_request_lock:
+ error_count += 1
+ error_window.append(1)
+
+ try:
+ if converts_to_mp4:
+ seg_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mp4"
+ if split_video_by_time:
+ segment_video(
+ save_file_path, seg_file_path,
+ segment_format='mp4', segment_time=split_time,
+ is_original_delete=delete_origin_file
+ )
+ else:
+ threading.Thread(
+ target=converts_mp4,
+ args=(save_file_path, delete_origin_file)
+ ).start()
+
+ else:
+ seg_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.flv"
+ if split_video_by_time:
+ segment_video(
+ save_file_path, seg_file_path,
+ segment_format='flv', segment_time=split_time,
+ is_original_delete=delete_origin_file
+ )
+ except Exception as e:
+ logger.error(f"转码失败: {e} ")
+
+ elif record_save_type == "MKV":
+ filename = anchor_name + f'_{title_in_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}_{title_in_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,
+ record_save_type,
+ custom_script
+ )
+ if comment_end:
+ return
+
+ except subprocess.CalledProcessError as e:
+ logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
+ with max_request_lock:
+ error_count += 1
+ error_window.append(1)
+
+ elif record_save_type == "MP4":
+ filename = anchor_name + f'_{title_in_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}_{title_in_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",
+ save_file_path,
+ ]
+
+ ffmpeg_command.extend(command)
+ comment_end = check_subprocess(
+ record_name,
+ record_url,
+ ffmpeg_command,
+ record_save_type,
+ custom_script
+ )
+ if comment_end:
+ return
+
+ except subprocess.CalledProcessError as e:
+ logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
+ with max_request_lock:
+ error_count += 1
+ error_window.append(1)
+
+ else:
+
+
+ import platform
+ # ====================== 抖音→YouTube 转推 2025终极永不死版 ======================
+ try:
+ # 直播标题带时间戳
+ timestamp_str = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
+ title_suffix = f"_{title_in_name}" if title_in_name else ""
+ stream_title = f"{anchor_name}{title_suffix}_{timestamp_str}"
+
+ # YouTube 推流地址(务必换成你自己的!!!)
+ YT_STREAM_KEY = "x04z-564w-aks7-embw-30y4"
+ youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}"
+ print(f"{rec_info} 开始推流到 YouTube: {stream_title}")
+
+ # 自动检测并使用 NVENC(有N卡就硬件加速)
+ codec_v = "libx264"
+ preset_v = "veryfast"
+ tune_param = ["-tune", "zerolatency"]
+ try:
+ if platform.system().lower() in ["windows", "linux"]:
+ check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=8)
+ if check.returncode == 0:
+ enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=8)
+ if "h264_nvenc" in enc.stdout:
+ codec_v = "h264_nvenc"
+ preset_v = "p5"
+ tune_param = ["-tune", "ll", "-rc", "vbr", "-cq", "23"]
+ print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速")
+ except:
+ pass
+
+ # 最强最稳的 FFmpeg 参数(千万不要随便改)
+ ffmpeg_command = [
+ "ffmpeg",
+ "-rw_timeout", "15000000",
+ "-reconnect", "1",
+ "-reconnect_at_eof", "1",
+ "-reconnect_streamed", "1",
+ "-reconnect_delay_max", "10",
+ "-fflags", "+genpts+discardcorrupt+igndts",
+ "-err_detect", "ignore_err",
+ "-thread_queue_size", "4096",
+ "-i", real_url,
+ "-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black",
+ "-c:v", codec_v, "-preset", preset_v, *tune_param,
+ "-b:v", "2500k", "-maxrate", "2500k", "-bufsize", "5000k",
+ "-g", "50", "-r", "25", "-pix_fmt", "yuv420p", "-bf", "0",
+ "-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-ac", "2",
+ "-af", "aresample=async=1:first_pts=0",
+ "-flags", "+low_delay+global_header",
+ "-fflags", "+genpts+nobuffer",
+ "-threads", "8",
+ "-f", "flv", youtube_rtmp
+ ]
+
+ proc = None
+
+ # 主循环:无限重启,直到主播真下播
+ while True:
+ try:
+ # 确保上一次的 ffmpeg 彻底死了
+ if proc and proc.poll() is None:
+ proc.terminate()
+ try: proc.wait(timeout=5)
+ except: proc.kill()
+
+ print(f"[{datetime.datetime.now():%H:%M:%S}] 启动YouTube推流 → {stream_title}")
+ proc = subprocess.Popen(
+ ffmpeg_command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ bufsize=1,
+ universal_newlines=True
+ )
+
+ # 每次启动都重置监控变量
+ proc.last_out_ts = time.time()
+ last_frame_time = time.time()
+ no_frame_restart_count = 0
+
+ # 读取 ffmpeg 输出核心循环
+ while True:
+ line = proc.stderr.readline()
+ if line == '' and proc.poll() is not None:
+ break
+ if not line:
+ time.sleep(0.01)
+ continue
+
+ line = line.strip()
+ if line:
+ proc.last_out_ts = time.time()
+
+ # 收到新帧就清零计数(关键!防止误杀)
+ if "frame=" in line or "fps=" in line:
+ last_frame_time = time.time()
+ no_frame_restart_count = 0
+
+ print(f"FFmpeg: {line}")
+
+ line_lower = line.lower()
+
+ # 致命错误 = 主播真下播(最准)
+ if any(kw in line_lower for kw in [
+ "403 forbidden", "401 unauthorized", "members only",
+ "private video", "live stream ended", "ingestion error"
+ ]):
+ print("【致命错误】检测到主播已下播或被封,停止推流")
+ if proc and proc.poll() is None:
+ proc.kill()
+ raise SystemExit
+
+ # YouTube 救命机制(12秒无新帧就砍)
+ if time.time() - time.time() > 25: # 启动25秒后开始判断
+ if time.time() - last_frame_time > 12:
+ no_frame_restart_count += 1
+ print(f"【YouTube救命】已 {time.time()-last_frame_time:.1f}s 无新帧,连续第 {no_frame_restart_count} 次重启")
+
+ if no_frame_restart_count >= 5: # 连续5次救不回 = 真下播
+ print("【真下播确认】连续5次无新帧失败,主播已下播,停止推流")
+ if proc and proc.poll() is None:
+ proc.kill()
+ raise SystemExit
+
+ if proc and proc.poll() is None:
+ proc.kill()
+ continue # 跳到外层重新启动
+
+ # 20秒完全没日志 = 进程僵死
+ if time.time() - time.time() > 20:
+ if time.time() - proc.last_out_ts > 20:
+ print("【真死流】20秒无任何输出,强制重启")
+ if proc and proc.poll() is None:
+ proc.kill()
+ continue
+
+ except SystemExit:
+ # 主动退出信号:主播真下播
+ break
+ except Exception as e:
+ print(f"推流异常: {e},5秒后重试...")
+ time.sleep(5)
+ continue
+
+ # 最终清理(主播下播后执行)
+ if proc and proc.poll() is None:
+ proc.kill()
+
+ try: recording.discard(record_name)
+ except: pass
+ try: recording_time_list.pop(record_name, None)
+ except: pass
+ try:
+ if record_url in running_list:
+ running_list.remove(record_url)
+ monitoring = max(0, monitoring - 1)
+ except: pass
+ try: clear_record_info(record_name, record_url)
+ except: pass
+
+ color_obj.print_colored(f"[{record_name}] YouTube推流已彻底停止并清理完毕\n", color_obj.GREEN)
+
+ except Exception as e:
+ print(f"最外层异常: {e}")
+ try:
+ if 'proc' in locals() and proc and proc.poll() is None:
+ proc.kill()
+ except: pass
+ color_obj.print_colored(f"[{record_name}] 异常退出但已清理完毕\n", color_obj.RED)
+
+ count_time = time.time()
+
+ except Exception as e:
+ logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
+ with max_request_lock:
+ error_count += 1
+ error_window.append(1)
+
+ num = random.randint(-5, 5) + delay_default
+ if num < 0:
+ num = 0
+ x = num
+
+ if error_count > 20:
+ x = x + 60
+ color_obj.print_colored("\r瞬时错误太多,延迟加60秒", color_obj.YELLOW)
+
+ # 这里是.如果录制结束后,循环时间会暂时变成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}")
+ with max_request_lock:
+ error_count += 1
+ error_window.append(1)
+ time.sleep(2)
+
+
+def backup_file(file_path: str, backup_dir_path: str, limit_counts: int = 6) -> None:
+ 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)
+
+ files = os.listdir(backup_dir_path)
+ _files = [f for f in files if f.startswith(os.path.basename(file_path))]
+ _files.sort(key=lambda x: os.path.getmtime(os.path.join(backup_dir_path, x)))
+
+ while len(_files) > limit_counts:
+ oldest_file = _files[0]
+ os.remove(os.path.join(backup_dir_path, oldest_file))
+ _files = _files[1:]
+
+ except Exception as e:
+ logger.error(f'\r备份配置文件 {file_path} 失败:{str(e)}')
+
+
+def backup_file_start() -> None:
+ config_md5 = ''
+ url_config_md5 = ''
+
+ while True:
+ try:
+ if os.path.exists(config_file):
+ new_config_md5 = utils.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 = utils.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}")
+
+
+def check_ffmpeg_existence() -> bool:
+ try:
+ result = subprocess.run(['ffmpeg', '-version'], check=True, capture_output=True, text=True)
+ if result.returncode == 0:
+ lines = result.stdout.splitlines()
+ version_line = lines[0]
+ built_line = lines[1]
+ print(version_line)
+ print(built_line)
+ except subprocess.CalledProcessError as e:
+ logger.error(e)
+ except FileNotFoundError:
+ pass
+ finally:
+ if check_ffmpeg():
+ time.sleep(1)
+ return True
+ return False
+
+
+# --------------------------初始化程序-------------------------------------
+print("-----------------------------------------------------")
+print("| DouyinLiveRecorder |")
+print("-----------------------------------------------------")
+
+print(f"版本号: {version}")
+print("GitHub: https://github.com/ihmily/DouyinLiveRecorder")
+print(f'支持平台: {platforms}')
+print('.....................................................')
+if not check_ffmpeg_existence():
+ logger.error("缺少ffmpeg无法进行录制,程序退出")
+ sys.exit(1)
+os.makedirs(os.path.dirname(config_file), exist_ok=True)
+t3 = threading.Thread(target=backup_file_start, args=(), daemon=True)
+t3.start()
+utils.remove_duplicate_lines(url_config_file)
+
+
+def read_config_value(config_parser: configparser.RawConfigParser, section: str, option: str, default_value: Any) \
+ -> Any:
+ 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()
+language = read_config_value(config, '录制设置', 'language(zh_cn/en)', "zh_cn")
+skip_proxy_check = options.get(read_config_value(config, '录制设置', '是否跳过代理检测(是/否)', "否"), False)
+if language and 'en' not in language.lower():
+ from i18n import translated_print
+
+ builtins.print = translated_print
+
+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全局/规则网络代理已开启√')
+ pd = ProxyDetector()
+ if pd.is_proxy_enabled():
+ proxy_info = pd.get_proxy_info()
+ print("System Proxy: http://{}:{}".format(proxy_info.ip, proxy_info.port))
+except HTTPError as err:
+ print(f"HTTP error occurred: {err.code} - {err.reason}")
+except URLError:
+ color_obj.print_colored("INFO:未检测到全局/规则网络代理,请检查代理配置(若无需录制海外直播请忽略此条提示)",
+ color_obj.YELLOW)
+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)
+ folder_by_title = options.get(read_config_value(config, '录制设置', '保存文件夹是否以标题区分', "否"), False)
+ filename_by_title = options.get(read_config_value(config, '录制设置', '保存文件名是否包含标题', "否"), False)
+ clean_emoji = options.get(read_config_value(config, '录制设置', '是否去除名称中的表情符号', "是"), True)
+ 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)
+ enable_https_recording = options.get(read_config_value(config, '录制设置', '是否强制启用https录制', "否"), False)
+ disk_space_limit = float(read_config_value(config, '录制设置', '录制空间剩余阈值(gb)', 1.0))
+ split_time = str(read_config_value(config, '录制设置', '视频分段时间(秒)', 1800))
+ converts_to_mp4 = options.get(read_config_value(config, '录制设置', '录制完成后自动转为mp4格式', "否"), False)
+ converts_to_h264 = options.get(read_config_value(config, '录制设置', 'mp4格式重新编码为h264', "否"), False)
+ delete_origin_file = options.get(read_config_value(config, '录制设置', '追加格式后删除原文件', "否"), False)
+ create_time_file = options.get(read_config_value(config, '录制设置', '生成时间字幕文件', "否"), False)
+ is_run_script = options.get(read_config_value(config, '录制设置', '是否录制完成后执行自定义脚本', "否"), False)
+ custom_script = read_config_value(config, '录制设置', '自定义脚本执行命令', "") if is_run_script else None
+ enable_proxy_platform = read_config_value(
+ config, '录制设置', '使用代理录制的平台(逗号分隔)',
+ 'tiktok, soop, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu, faceit'
+ )
+ 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, '推送配置', '直播状态推送渠道', "")
+ 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, '推送配置', '钉钉通知@对象(填手机号)', "")
+ dingtalk_is_atall = options.get(read_config_value(config, '推送配置', '钉钉通知@全体(是/否)', "否"), False)
+ tg_token = read_config_value(config, '推送配置', 'tgapi令牌', "")
+ tg_chat_id = read_config_value(config, '推送配置', 'tg聊天id(个人或者群组id)', "")
+ email_host = read_config_value(config, '推送配置', 'SMTP邮件服务器', "")
+ open_smtp_ssl = options.get(read_config_value(config, '推送配置', '是否使用SMTP服务SSL加密(是/否)', "是"), True)
+ smtp_port = read_config_value(config, '推送配置', 'SMTP邮件服务器端口', "")
+ login_email = read_config_value(config, '推送配置', '邮箱登录账号', "")
+ email_password = read_config_value(config, '推送配置', '发件人密码(授权码)', "")
+ sender_email = read_config_value(config, '推送配置', '发件人邮箱', "")
+ sender_name = read_config_value(config, '推送配置', '发件人显示昵称', "")
+ to_email = read_config_value(config, '推送配置', '收件人邮箱', "")
+ ntfy_api = read_config_value(config, '推送配置', 'ntfy推送地址', "")
+ ntfy_tags = read_config_value(config, '推送配置', 'ntfy推送标签', "tada")
+ ntfy_email = read_config_value(config, '推送配置', 'ntfy推送邮箱', "")
+ pushplus_token = read_config_value(config, '推送配置', 'pushplus推送token', "")
+ push_message_title = 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)
+ sooplive_username = read_config_value(config, '账号密码', 'sooplive账号', '')
+ sooplive_password = read_config_value(config, '账号密码', 'sooplive密码', '')
+ 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', '')
+ sooplive_cookie = read_config_value(config, 'Cookie', 'sooplive_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', '')
+ changliao_cookie = read_config_value(config, 'Cookie', 'changliao_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', '')
+ haixiu_cookie = read_config_value(config, 'Cookie', 'haixiu_cookie', '')
+ vvxqiu_cookie = read_config_value(config, 'Cookie', 'vvxqiu_cookie', '')
+ yiqilive_cookie = read_config_value(config, 'Cookie', '17live_cookie', '')
+ langlive_cookie = read_config_value(config, 'Cookie', 'langlive_cookie', '')
+ pplive_cookie = read_config_value(config, 'Cookie', 'pplive_cookie', '')
+ six_room_cookie = read_config_value(config, 'Cookie', '6room_cookie', '')
+ lehaitv_cookie = read_config_value(config, 'Cookie', 'lehaitv_cookie', '')
+ huamao_cookie = read_config_value(config, 'Cookie', 'huamao_cookie', '')
+ shopee_cookie = read_config_value(config, 'Cookie', 'shopee_cookie', '')
+ youtube_cookie = read_config_value(config, 'Cookie', 'youtube_cookie', '')
+ taobao_cookie = read_config_value(config, 'Cookie', 'taobao_cookie', '')
+ jd_cookie = read_config_value(config, 'Cookie', 'jd_cookie', '')
+ faceit_cookie = read_config_value(config, 'Cookie', 'faceit_cookie', '')
+ migu_cookie = read_config_value(config, 'Cookie', 'migu_cookie', '')
+ lianjie_cookie = read_config_value(config, 'Cookie', 'lianjie_cookie', '')
+ laixiu_cookie = read_config_value(config, 'Cookie', 'laixiu_cookie', '')
+ picarto_cookie = read_config_value(config, 'Cookie', 'picarto_cookie', '')
+
+ video_save_type_list = ("FLV", "MKV", "TS", "MP4", "MP3音频", "M4A音频", "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"
+
+ check_path = video_save_path or default_path
+ if utils.check_disk_capacity(check_path, show=first_run) < disk_space_limit:
+ exit_recording = True
+ if not recording:
+ logger.warning(f"Disk space remaining is below {disk_space_limit} GB. "
+ f"Exiting program due to the disk space limit being reached.")
+ sys.exit(-1)
+
+
+ def contains_url(string: str) -> bool:
+ pattern = r"(https?://)?(www\.)?[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+(:\d+)?(/.*)?"
+ return re.search(pattern, string) is not None
+
+
+ try:
+ url_comments, line_list, url_line_list = [[] for _ in range(3)]
+ with (open(url_config_file, "r", encoding=text_encoding, errors='ignore') as file):
+ for origin_line in file:
+ if origin_line in line_list:
+ delete_line(url_config_file, origin_line)
+ line_list.append(origin_line)
+ line = origin_line.strip()
+ if len(line) < 18:
+ 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 = '原画'
+
+ if url not in url_line_list:
+ url_line_list.append(url)
+ else:
+ delete_line(url_config_file, origin_line)
+
+ 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',
+ 'live.tlclw.com',
+ 'wap.tlclw.com',
+ 'live.ybw1666.com',
+ 'wap.ybw1666.com',
+ 'www.inke.cn',
+ 'www.zhihu.com',
+ 'www.haixiutv.com',
+ "h5webcdnp.vvxqiu.com",
+ "17.live",
+ 'www.lang.live',
+ "m.pp.weimipopo.com",
+ "v.6.cn",
+ "m.6.cn",
+ 'www.lehaitv.com',
+ 'h.catshow168.com',
+ 'e.tb.cn',
+ 'huodong.m.taobao.com',
+ '3.cn',
+ 'eco.m.jd.com',
+ 'www.miguvideo.com',
+ 'm.miguvideo.com',
+ 'show.lailianjie.com',
+ 'www.imkktv.com',
+ 'www.picarto.tv'
+ ]
+ overseas_platform_host = [
+ 'www.tiktok.com',
+ 'play.sooplive.co.kr',
+ 'm.sooplive.co.kr',
+ 'www.sooplive.com',
+ 'm.sooplive.com',
+ 'www.pandalive.co.kr',
+ 'www.winktv.co.kr',
+ 'www.flextv.co.kr',
+ 'www.ttinglive.com',
+ 'www.popkontv.com',
+ 'www.twitch.tv',
+ 'www.liveme.com',
+ 'www.showroom-live.com',
+ 'chzzk.naver.com',
+ 'm.chzzk.naver.com',
+ 'live.shopee.',
+ '.shp.ee',
+ 'www.youtube.com',
+ 'youtu.be',
+ 'www.faceit.com'
+ ]
+
+ platform_host.extend(overseas_platform_host)
+ clean_url_host_list = (
+ "live.douyin.com",
+ "live.bilibili.com",
+ "www.huajiao.com",
+ "www.zhihu.com",
+ "www.huya.com",
+ "chzzk.naver.com",
+ "www.liveme.com",
+ "www.haixiutv.com",
+ "v.6.cn",
+ "m.6.cn",
+ 'www.lehaitv.com'
+ )
+
+ if 'live.shopee.' in url_host or '.shp.ee' in url_host:
+ url_host = 'live.shopee.' if 'live.shopee.' in url_host else '.shp.ee'
+
+ if url_host in platform_host or any(ext in url for ext in (".flv", ".m3u8")):
+ if url_host in clean_url_host_list:
+ url = update_file(url_config_file, old_str=url, new_str=url.split('?')[0])
+
+ if 'xiaohongshu' in url:
+ host_id = re.search('&host_id=(.*?)(?=&|$)', url)
+ if host_id:
+ new_url = url.split('?')[0] + f'?host_id={host_id.group(1)}'
+ url = update_file(url_config_file, old_str=url, new_str=new_url)
+
+ 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:
+ if not origin_line.startswith('#'):
+ color_obj.print_colored(f"\r{origin_line.strip()} 本行包含未知链接.此条跳过", color_obj.YELLOW)
+ update_file(url_config_file, old_str=origin_line, new_str=origin_line, 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, old_str=replace_words[0], new_str=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=adjust_max_request, args=(), daemon=True)
+ t2.start()
+ first_run = False
+
+ time.sleep(3)
\ No newline at end of file
diff --git a/msg_push.py b/msg_push.py
new file mode 100644
index 0000000..c682b20
--- /dev/null
+++ b/msg_push.py
@@ -0,0 +1,295 @@
+# -*- coding: utf-8 -*-
+
+"""
+Author: Hmily
+GitHub: https://github.com/ihmily
+Date: 2023-09-03 19:18:36
+Update: 2025-01-23 17:16:12
+Copyright (c) 2023-2024 by Hmily, All Rights Reserved.
+"""
+from typing import Dict, Any
+import json
+import base64
+import urllib.request
+import urllib.error
+import smtplib
+from email.header import Header
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+no_proxy_handler = urllib.request.ProxyHandler({})
+opener = urllib.request.build_opener(no_proxy_handler)
+headers: Dict[str, str] = {'Content-Type': 'application/json'}
+
+
+def dingtalk(url: str, content: str, number: str = None, is_atall: bool = False) -> Dict[str, Any]:
+ success = []
+ error = []
+ api_list = url.replace(',', ',').split(',') if url.strip() else []
+ for api in api_list:
+ json_data = {
+ 'msgtype': 'text',
+ 'text': {
+ 'content': content,
+ },
+ "at": {
+ "atMobiles": [
+ number
+ ],
+ "isAtAll": is_atall
+ },
+ }
+ try:
+ data = json.dumps(json_data).encode('utf-8')
+ req = urllib.request.Request(api, data=data, headers=headers)
+ response = opener.open(req, timeout=10)
+ json_str = response.read().decode('utf-8')
+ json_data = json.loads(json_str)
+ if json_data['errcode'] == 0:
+ success.append(api)
+ else:
+ error.append(api)
+ print(f'钉钉推送失败, 推送地址:{api}, {json_data["errmsg"]}')
+ except Exception as e:
+ error.append(api)
+ print(f'钉钉推送失败, 推送地址:{api}, 错误信息:{e}')
+ return {"success": success, "error": error}
+
+
+def xizhi(url: str, title: str, content: str) -> Dict[str, Any]:
+ success = []
+ error = []
+ api_list = url.replace(',', ',').split(',') if url.strip() else []
+ for api in api_list:
+ json_data = {
+ 'title': title,
+ 'content': content
+ }
+ try:
+ data = json.dumps(json_data).encode('utf-8')
+ req = urllib.request.Request(api, data=data, headers=headers)
+ response = opener.open(req, timeout=10)
+ json_str = response.read().decode('utf-8')
+ json_data = json.loads(json_str)
+ if json_data['code'] == 200:
+ success.append(api)
+ else:
+ error.append(api)
+ print(f'微信推送失败, 推送地址:{api}, 失败信息:{json_data["msg"]}')
+ except Exception as e:
+ error.append(api)
+ print(f'微信推送失败, 推送地址:{api}, 错误信息:{e}')
+ return {"success": success, "error": error}
+
+
+def send_email(email_host: str, login_email: str, email_pass: str, sender_email: str, sender_name: str,
+ to_email: str, title: str, content: str, smtp_port: str = None, open_ssl: bool = True) -> Dict[str, Any]:
+ receivers = to_email.replace(',', ',').split(',') if to_email.strip() else []
+
+ try:
+ message = MIMEMultipart()
+ send_name = base64.b64encode(sender_name.encode("utf-8")).decode()
+ message['From'] = f'=?UTF-8?B?{send_name}?= <{sender_email}>'
+ message['Subject'] = Header(title, 'utf-8')
+ if len(receivers) == 1:
+ message['To'] = receivers[0]
+
+ t_apart = MIMEText(content, 'plain', 'utf-8')
+ message.attach(t_apart)
+
+ if open_ssl:
+ smtp_port = int(smtp_port) or 465
+ smtp_obj = smtplib.SMTP_SSL(email_host, smtp_port)
+ else:
+ smtp_port = int(smtp_port) or 25
+ smtp_obj = smtplib.SMTP(email_host, smtp_port)
+ smtp_obj.login(login_email, email_pass)
+ smtp_obj.sendmail(sender_email, receivers, message.as_string())
+ return {"success": receivers, "error": []}
+ except smtplib.SMTPException as e:
+ print(f'邮件推送失败, 推送邮箱:{to_email}, 错误信息:{e}')
+ return {"success": [], "error": receivers}
+
+
+def tg_bot(chat_id: int, token: str, content: str) -> Dict[str, Any]:
+ try:
+ json_data = {
+ "chat_id": chat_id,
+ 'text': content
+ }
+ url = f'https://api.telegram.org/bot{token}/sendMessage'
+ data = json.dumps(json_data).encode('utf-8')
+ req = urllib.request.Request(url, data=data, headers=headers)
+ response = urllib.request.urlopen(req, timeout=15)
+ json_str = response.read().decode('utf-8')
+ _json_data = json.loads(json_str)
+ return {"success": [1], "error": []}
+ except Exception as e:
+ print(f'tg推送失败, 聊天ID:{chat_id}, 错误信息:{e}')
+ return {"success": [], "error": [1]}
+
+
+def bark(api: str, title: str = "message", content: str = 'test', level: str = "active",
+ badge: int = 1, auto_copy: int = 1, sound: str = "", icon: str = "", group: str = "",
+ is_archive: int = 1, url: str = "") -> Dict[str, Any]:
+ success = []
+ error = []
+ api_list = api.replace(',', ',').split(',') if api.strip() else []
+ for _api in api_list:
+ json_data = {
+ "title": title,
+ "body": content,
+ "level": level,
+ "badge": badge,
+ "autoCopy": auto_copy,
+ "sound": sound,
+ "icon": icon,
+ "group": group,
+ "isArchive": is_archive,
+ "url": url
+ }
+ try:
+ data = json.dumps(json_data).encode('utf-8')
+ req = urllib.request.Request(_api, data=data, headers=headers)
+ response = opener.open(req, timeout=10)
+ json_str = response.read().decode("utf-8")
+ json_data = json.loads(json_str)
+ if json_data['code'] == 200:
+ success.append(_api)
+ else:
+ error.append(_api)
+ print(f'Bark推送失败, 推送地址:{_api}, 失败信息:{json_data["message"]}')
+ except Exception as e:
+ error.append(api)
+ print(f'Bark推送失败, 推送地址:{_api}, 错误信息:{e}')
+ return {"success": success, "error": error}
+
+
+def ntfy(api: str, title: str = "message", content: str = 'test', tags: str = 'tada', priority: int = 3,
+ action_url: str = "", attach: str = "", filename: str = "", click: str = "", icon: str = "",
+ delay: str = "", email: str = "", call: str = "") -> Dict[str, Any]:
+ success = []
+ error = []
+ api_list = api.replace(',', ',').split(',') if api.strip() else []
+ tags = tags.replace(',', ',').split(',') if tags else ['partying_face']
+ actions = [{"action": "view", "label": "view live", "url": action_url}] if action_url else []
+ for _api in api_list:
+ server, topic = _api.rsplit('/', maxsplit=1)
+ json_data = {
+ "topic": topic,
+ "title": title,
+ "message": content,
+ "tags": tags,
+ "priority": priority,
+ "attach": attach,
+ "filename": filename,
+ "click": click,
+ "actions": actions,
+ "markdown": False,
+ "icon": icon,
+ "delay": delay,
+ "email": email,
+ "call": call
+ }
+
+ try:
+ data = json.dumps(json_data, ensure_ascii=False).encode('utf-8')
+ req = urllib.request.Request(server, data=data, headers=headers)
+ response = opener.open(req, timeout=10)
+ json_str = response.read().decode("utf-8")
+ json_data = json.loads(json_str)
+ if "error" not in json_data:
+ success.append(_api)
+ else:
+ error.append(_api)
+ print(f'ntfy推送失败, 推送地址:{_api}, 失败信息:{json_data["error"]}')
+ except urllib.error.HTTPError as e:
+ error.append(_api)
+ error_msg = e.read().decode("utf-8")
+ print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{json.loads(error_msg)["error"]}')
+ except Exception as e:
+ error.append(api)
+ print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{e}')
+ return {"success": success, "error": error}
+
+
+def pushplus(token: str, title: str, content: str) -> Dict[str, Any]:
+ """
+ PushPlus推送通知
+ API文档: https://www.pushplus.plus/doc/
+ """
+ success = []
+ error = []
+ token_list = token.replace(',', ',').split(',') if token.strip() else []
+
+ for _token in token_list:
+ json_data = {
+ 'token': _token,
+ 'title': title,
+ 'content': content
+ }
+
+ try:
+ url = 'https://www.pushplus.plus/send'
+ data = json.dumps(json_data).encode('utf-8')
+ req = urllib.request.Request(url, data=data, headers=headers)
+ response = opener.open(req, timeout=10)
+ json_str = response.read().decode('utf-8')
+ json_data = json.loads(json_str)
+
+ if json_data.get('code') == 200:
+ success.append(_token)
+ else:
+ error.append(_token)
+ print(f'PushPlus推送失败, Token:{_token}, 失败信息:{json_data.get("msg", "未知错误")}')
+ except Exception as e:
+ error.append(_token)
+ print(f'PushPlus推送失败, Token:{_token}, 错误信息:{e}')
+
+ return {"success": success, "error": error}
+
+
+if __name__ == '__main__':
+ send_title = '直播通知' # 标题
+ send_content = '张三 开播了!' # 推送内容
+
+ # 钉钉推送通知
+ webhook_api = '' # 替换成自己Webhook链接,参考文档:https://open.dingtalk.com/document/robots/custom-robot-access
+ phone_number = '' # 被@用户的手机号码
+ is_atall = '' # 是否@全体
+ # dingtalk(webhook_api, send_content, phone_number)
+
+ # 微信推送通知
+ # 替换成自己的单点推送接口,获取地址:https://xz.qqoq.net/#/admin/one
+ # 当然也可以使用其他平台API 如server酱 使用方法一样
+ xizhi_api = 'https://xizhi.qqoq.net/xxxxxxxxx.send'
+ # xizhi(xizhi_api, send_content)
+
+ # telegram推送通知
+ tg_token = '' # tg搜索"BotFather"获取的token值
+ tg_chat_id = 000000 # tg搜索"userinfobot"获取的chat_id值,即可发送推送消息给你自己,如果下面的是群组id则发送到群
+ # tg_bot(tg_chat_id, tg_token, send_content)
+
+ # email_message(
+ # email_host="smtp.qq.com",
+ # login_email="",
+ # email_pass="",
+ # sender_email="",
+ # sender_name="",
+ # to_email="",
+ # title="",
+ # content="",
+ # )
+
+ bark_url = 'https://xxx.xxx.com/key/'
+ # bark(bark_url, send_title, send_content)
+
+ ntfy(
+ api="https://ntfy.sh/xxxxx",
+ title="直播推送",
+ content="xxx已开播",
+ )
+
+ # PushPlus推送通知
+ pushplus_token = '' # 替换成自己的PushPlus Token,获取地址:https://www.pushplus.plus/
+ # pushplus(pushplus_token, send_title, send_content)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..b2340a6
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,23 @@
+[project]
+name = "DouyinLiveRecorder"
+version = "4.0.7"
+description = "可循环值守和多人录制的直播录制软件, 支持抖音、TikTok、Youtube、快手、虎牙、斗鱼、B站、小红书、pandatv、sooplive、flextv、popkontv、twitcasting、winktv、百度、微博、酷狗、17Live、Twitch、Acfun、CHZZK、shopee等40+平台直播录制"
+readme = "README.md"
+authors = [{name = "Hmily"}]
+license = { text = "MIT" }
+requires-python = ">=3.10"
+dependencies = [
+ "requests>=2.31.0",
+ "loguru>=0.7.3",
+ "pycryptodome>=3.20.0",
+ "distro>=1.9.0",
+ "tqdm>=4.67.1",
+ "httpx[http2]>=0.28.1",
+ "PyExecJS>=1.5.1"
+]
+
+[project.urls]
+"Homepage" = "https://github.com/ihmily/DouyinLiveRecorder"
+"Documentation" = "https://github.com/ihmily/DouyinLiveRecorder"
+"Repository" = "https://github.com/ihmily/DouyinLiveRecorder"
+"Issues" = "https://github.com/ihmily/DouyinLiveRecorder/issues"
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..18f105b
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,7 @@
+requests>=2.31.0
+loguru>=0.7.3
+pycryptodome>=3.20.0
+distro>=1.9.0
+tqdm>=4.67.1
+httpx[http2]>=0.28.1
+PyExecJS>=1.5.1
\ No newline at end of file
diff --git a/robust_relay.py b/robust_relay.py
new file mode 100644
index 0000000..000d753
--- /dev/null
+++ b/robust_relay.py
@@ -0,0 +1,227 @@
+# robust_relay.py
+# 高稳定性 YouTube 转播模块(防误判下播 + 看门狗 + 自动重连 + token刷新)
+# 作者:融合 A+B 代码精华,专为抖音/抖音国际版长期无人值守转播设计
+
+import subprocess
+import time
+import threading
+import os
+import signal
+import datetime
+from typing import List, Optional, Callable
+
+# ==================== 配置常量(可全局覆盖) ====================
+WATCHDOG_SILENCE = 600 # 10分钟无输出 → 软重启
+WATCHDOG_GRACE = 120 # 启动宽限期
+REFRESH_URL_EVERY = 300 # 每5分钟强制刷新 real_url(防token过期)
+OFFLINE_DEBOUNCE = 3 # 连续3次API返回下播才认为真下播
+OFFLINE_WINDOW = 120 # 最近120秒有活跃就忽略下播信号
+SOFT_RESTART_COOLDOWN = 90 # 软重启最小间隔
+
+# ==================== 轻量探活 ====================
+def _probe_alive(url: str, timeout: float = 5.0) -> bool:
+ if not url:
+ return False
+ try:
+ import urllib.request, contextlib
+ opener = urllib.request.build_opener()
+ if hasattr(urllib.request, 'getproxies'):
+ proxy = urllib.request.getproxies().get('http')
+ if proxy:
+ opener.add_handler(urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}, method="HEAD")
+ with contextlib.closing(opener.open(req, timeout=timeout)) as r:
+ if r.code >= 400:
+ return False
+ head = r.read(256)
+ if url.lower().endswith(".m3u8"):
+ return b"#EXTM3U" in head.upper()
+ if url.lower().endswith(".flv"):
+ return head.startswith(b"FLV")
+ return len(head) > 0
+ except:
+ return False
+
+# ==================== 增强版 ffmpeg 启动器 ====================
+def _launch_ffmpeg(
+ input_url: str,
+ youtube_rtmp: str,
+ anchor_name: str,
+ logger: Callable[[str], None],
+ extra_cmd: List[str] = None
+) -> subprocess.Popen:
+
+ cmd = [
+ "ffmpeg", "-nostdin", "-hide_banner", "-stats", "-stats_period", "60", "-loglevel", "info",
+
+ # 输入端超强重连
+ "-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_at_eof", "1",
+ "-reconnect_delay_max", "30", "-rw_timeout", "20000000",
+
+ # 极致低延迟 + 时间戳修复
+ "-fflags", "+genpts+igndts+flush_packets", "-flags", "+global_header",
+ "-thread_queue_size", "4096", "-analyzeduration", "0", "-probesize", "32",
+
+ "-i", input_url,
+
+ # copy 模式(质量无损)
+ "-c:v", "copy", "-c:a", "copy", "-bsf:a", "aac_adtstoasc",
+ "-map", "0:v?", "-map", "0:a?",
+
+ # YouTube 强制参数
+ "-f", "mpegts", "-g", "2", "-rtmp_buffer", "1000", "-rtmp_live", "live",
+ "-max_delay", "0", "-tune", "zerolatency", "-avoid_negative_ts", "make_zero",
+ "-muxdelay", "0", "-muxpreload", "0",
+ "-fflags", "+nobuffer+flush_packets", "-avioflags", "direct",
+ ]
+
+ if extra_cmd:
+ cmd.extend(extra_cmd)
+
+ cmd.append(youtube_rtmp)
+
+ creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
+ proc = subprocess.Popen(
+ cmd,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ bufsize=0,
+ creationflags=creationflags
+ )
+
+ # 用于看门狗
+ proc.last_output_ts = time.time()
+
+ def _reader():
+ try:
+ for line in proc.stdout: # type: ignore
+ if not line:
+ break
+ text = line.decode(errors="ignore").rstrip()
+ proc.last_output_ts = time.time()
+ if any(k in text for k in ["bitrate=", "frame=", "speed=", "fps="]):
+ logger(f"[{anchor_name}] {text}")
+ except:
+ pass
+ threading.Thread(target=_reader, daemon=True).start()
+
+ return proc
+
+# ==================== 主函数:高稳定性转播(替代原 check_subprocess) ====================
+def robust_youtube_relay(
+ anchor_name: str,
+ real_url: str,
+ youtube_rtmp: str,
+ logger: Callable[[str], None],
+ get_latest_stream_url: Callable[[], Optional[str]], # 用于刷新 real_url(如有token过期)
+ is_stream_still_live: Callable[[], bool], # 检查主播是否在播(API)
+ extra_ffmpeg_args: List[str] = None, # 可选:加 -f segment 分段保存
+ heartbeat_callback: Callable[[int], None] = None # 可选:每分钟回调已运行秒数
+) -> bool:
+ """
+ 返回值:
+ True = 主播已下播(正常结束)
+ False = 发生未知错误(建议计数错误次数)
+ """
+ proc: Optional[subprocess.Popen] = None
+ start_time = time.time()
+ last_refresh = 0.0
+ last_soft_restart = 0.0
+ offline_count = 0
+ last_live_ts = time.time()
+
+ try:
+ proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
+ logger(f"[{anchor_name}] 已开始高稳定性转播 → {youtube_rtmp.split('/')[-1]}")
+
+ while True:
+ now = time.time()
+ elapsed = int(now - start_time)
+ silence = now - proc.last_output_ts if proc else 0
+
+ # 1. 进程意外退出 → 重启
+ if proc and proc.poll() is not None:
+ logger(f"[{anchor_name}] ffmpeg 意外退出,3秒后重启...")
+ time.sleep(3)
+ proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
+ start_time = time.time()
+ continue
+
+ # 2. 看门狗:长时间无输出 → 软重启
+ if elapsed > WATCHDOG_GRACE and silence > WATCHDOG_SILENCE:
+ if now - last_soft_restart > SOFT_RESTART_COOLDOWN:
+ logger(f"[{anchor_name}] 检测到卡顿({silence:.0f}s无输出),软重启 ffmpeg...")
+ last_soft_restart = now
+ if proc and proc.stdin:
+ proc.stdin.write(b'q')
+ proc.stdin.flush()
+ time.sleep(4)
+ proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
+ start_time = time.time()
+ continue
+
+ # 3. 定期刷新播放地址(防token过期)
+ if now - last_refresh > REFRESH_URL_EVERY:
+ new_url = get_latest_stream_url()
+ if new_url and new_url != real_url:
+ logger(f"[{anchor_name}] 检测到新的播放地址,切换中...")
+ real_url = new_url
+ if proc and proc.stdin:
+ proc.stdin.write(b'q')
+ time.sleep(3)
+ proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
+ start_time = time.time()
+ last_refresh = now
+
+ # 4. 下播防抖判定(核心防误判)
+ try:
+ still_live = is_stream_still_live()
+ except:
+ still_live = True # 接口异常不判下播
+
+ if not still_live:
+ # 交叉验证:流还能访问 → 忽略API误判
+ if _probe_alive(real_url):
+ logger(f"[{anchor_name}] API判下播但流仍活跃,忽略本次误判")
+ still_live = True
+ last_live_ts = now
+
+ if still_live:
+ offline_count = 0
+ last_live_ts = now
+ else:
+ if now - last_live_ts < OFFLINE_WINDOW:
+ logger(f"[{anchor_name}] 短期掉线,忽略")
+ else:
+ offline_count += 1
+ if offline_count >= OFFLINE_DEBOUNCE:
+ logger(f"[{anchor_name}] 连续{offline_count}次确认下播,结束转播")
+ if proc and proc.stdin:
+ proc.stdin.write(b'q')
+ return True
+ else:
+ logger(f"[{anchor_name}] 疑似下播 {offline_count}/{OFFLINE_DEBOUNCE},继续观察")
+
+ # 5. 心跳日志 & 回调
+ if elapsed % 60 < 2:
+ h, m = divmod(elapsed // 60, 60)
+ logger(f"[{anchor_name}] 正在转播 {h:02d}:{m:02d}")
+ if heartbeat_callback:
+ heartbeat_callback(elapsed)
+
+ time.sleep(8)
+
+ except KeyboardInterrupt:
+ logger(f"[{anchor_name}] 手动终止转播")
+ return True
+ except Exception as e:
+ logger(f"[{anchor_name}] 严重错误: {e}")
+ return False
+ finally:
+ if proc:
+ try:
+ proc.stdin.write(b'q') if proc.stdin else None
+ proc.wait(timeout=8)
+ except:
+ proc.kill()
\ No newline at end of file
diff --git a/wending b/wending
new file mode 100644
index 0000000..d706019
--- /dev/null
+++ b/wending
@@ -0,0 +1,111 @@
+
+ try:
+ if split_video_by_time:
+ now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
+ print(f"{rec_info} 开始推流到 YouTube: {anchor_name}_{title_in_name}_{now}")
+
+ YT_STREAM_KEY = "ue78-1c3e-mr9g-14mz-9r4z"
+ youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}"
+
+ # ------------------------
+ # NVENC 检测
+ # ------------------------
+ codec_v = "libx264"
+ preset_v = "veryfast"
+ tune_param = ["-tune", "zerolatency"]
+
+ try:
+ if platform.system().lower() in ["windows", "linux"]:
+ check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=10)
+ if check.returncode == 0:
+ enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
+ capture_output=True, text=True, timeout=10)
+ if "h264_nvenc" in enc.stdout:
+ codec_v = "h264_nvenc"
+ preset_v = "p5"
+ tune_param = ["-tune", "ll", "-rc", "vbr"]
+ print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速")
+ except:
+ pass # 异常自动降级
+
+ # ------------------------
+ # FFmpeg 命令(稳定推流)
+ # ------------------------
+ ffmpeg_command = [
+ "ffmpeg",
+ "-re",
+ "-i", real_url,
+
+ "-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black",
+
+ "-c:v", codec_v,
+ "-preset", preset_v,
+ *tune_param,
+
+ "-b:v", "2500k",
+ "-maxrate", "2500k",
+ "-bufsize", "5000k",
+ "-g", "50",
+ "-r", "25",
+ "-pix_fmt", "yuv420p",
+ "-bf", "0",
+
+ "-c:a", "aac",
+ "-b:a", "128k",
+ "-ar", "44100",
+ "-ac", "2",
+ "-af", "aresample=async=1:first_pts=0",
+
+ "-flags", "+low_delay",
+ "-fflags", "+genpts",
+ "-thread_queue_size", "512",
+ "-threads", "6",
+
+ "-f", "flv",
+ youtube_rtmp
+ ]
+
+ # ------------------------
+ # 永不断流重连逻辑
+ # ------------------------
+ proc = None
+ while True:
+ try:
+ if proc and proc.poll() is None:
+ proc.terminate()
+ try:
+ proc.wait(timeout=8)
+ except:
+ proc.kill()
+
+ print(f"{anchor_name}_{title_in_name}_{now} → 启动 YouTube 推流...")
+ proc = subprocess.Popen(
+ ffmpeg_command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+
+ # 打印 FFmpeg 错误日志,便于排查
+ while True:
+ line = proc.stderr.readline()
+ if not line:
+ break
+ line = line.strip()
+ if line:
+ print(f"FFmpeg: {line}")
+ if proc.poll() is not None:
+ break
+
+ code = proc.returncode
+ if code != 0:
+ print(f"FFmpeg 退出(code={code}),5秒后重连...")
+ time.sleep(5)
+
+ except Exception as e:
+ print(f"启动 FFmpeg 异常: {e}")
+ time.sleep(5)
+
+ except Exception as e:
+ print(f"外层异常: {e}")
\ No newline at end of file