This commit is contained in:
eric
2025-09-20 21:17:35 +08:00
parent d7fb6cf578
commit bcaeb96d1f
36 changed files with 10082 additions and 0 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.github/workflows/build-image.yml
.git
.gitignore
.dockerignore
README.md
LICENSE

167
.gitignore vendored Normal file
View File

@@ -0,0 +1,167 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# DouyinLiveRecord
backup_config/
logs/
node/
node-v*.zip
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
backup_config/

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM python:3.11-slim
WORKDIR /app
COPY . /app
RUN apt-get update && \
apt-get install -y curl gnupg && \
curl -sL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs
RUN pip install --no-cache-dir -r requirements.txt
RUN apt-get update && \
apt-get install -y ffmpeg tzdata && \
ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata
CMD ["python", "main.py"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Hmily
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

69
StopRecording.vbs Normal file
View File

@@ -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

38
config/URL_config.ini Normal file
View File

@@ -0,0 +1,38 @@
https://live.douyin.com/210443559964,主播: 小小诺
#https://live.douyin.com/525514386431,主播: 阿闯烤肉夹馍
https://live.douyin.com/8687122573,主播: 小彤炒粉
https://live.douyin.com/642534242822,主播: (小王_)煎饼果子
https://live.douyin.com/589037028237,主播: 娜姐辉哥在奋斗
https://live.douyin.com/743565594721,主播: 汪汪汪小妞
https://live.douyin.com/249578288248,主播: 思思努力版
https://live.douyin.com/153648759493,主播: 晨晨_烤冷面
https://live.douyin.com/472140253414,主播: 戴盘龙
https://live.douyin.com/700846653732,主播: 三哥大锅菜_炒鸡(国基路店)官方号
https://live.douyin.com/81849868631,主播: 纯儿²¹
https://live.douyin.com/690114366322,主播: 卤味鲜(东大)
https://live.douyin.com/469980190666,主播: 茜茜柠檬茶
https://live.douyin.com/388066418744,主播: 加速姐的摆摊日记
https://live.douyin.com/846711047399,主播: 小高的车摊日记(摆摊创业中)
https://live.douyin.com/782322393954,主播: 小葵(摆摊版)
https://live.douyin.com/436862336055,主播: 彡哥户外
https://live.douyin.com/270513395244,主播: 小喻的摆摊日记
https://live.douyin.com/145957089252,主播: 餐小雪摆摊生活
https://live.douyin.com/500150269677,主播: 是丁槑呀(摆摊版)
https://live.douyin.com/97282309368,主播: 会跳舞的饭
https://live.douyin.com/667615017083,主播: 一个臭摆摊的(23届勇哥学员)
https://live.douyin.com/111807658723,主播: 小买(摆摊版)
https://live.douyin.com/830441278598
https://live.douyin.com/442018876873
https://live.douyin.com/749528491328
https://live.douyin.com/81002009942
https://live.douyin.com/157898434218
https://live.douyin.com/216655810662
https://live.douyin.com/306503782554
https://live.douyin.com/204107598826
https://live.douyin.com/146835808133
https://live.douyin.com/136996589868
https://live.douyin.com/558384797228
https://live.douyin.com/709515122824
https://live.douyin.com/318386221997
https://live.douyin.com/331356315060
https://live.douyin.com/165844997990

131
config/config.ini Normal file
View File

@@ -0,0 +1,131 @@
[录制设置]
language(zh_cn/en) = zh_cn
是否跳过代理检测(是/否) =
直播保存路径(不填则默认) =
保存文件夹是否以作者区分 =
保存文件夹是否以时间区分 =
保存文件夹是否以标题区分 =
保存文件名是否包含标题 =
是否去除名称中的表情符号 =
视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频 = mp4
原画|超清|高清|标清|流畅 = 原画
是否使用代理ip(是/否) =
代理地址 =
同一时间访问网络的线程数 = 20
循环时间(秒) = 300
排队读取网址时间(秒) = 0
是否显示循环秒数 =
是否显示直播源地址 =
分段录制是否开启 =
是否强制启用https录制 =
录制空间剩余阈值(gb) = 1.0
视频分段时间(秒) = 600
录制完成后自动转为mp4格式 =
mp4格式重新编码为h264 =
追加格式后删除原文件 =
生成时间字幕文件 =
是否录制完成后执行自定义脚本 =
自定义脚本执行命令 =
使用代理录制的平台(逗号分隔) = tiktok, sooplive, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu
额外使用代理录制的平台(逗号分隔) =
[推送配置]
# 可选微信|钉钉|tg|邮箱|bark|ntfy|pushplus 可填多个
直播状态推送渠道 =
钉钉推送接口链接 =
微信推送接口链接 =
bark推送接口链接 =
bark推送中断级别 = active
bark推送铃声 =
钉钉通知@对象(填手机号) =
钉钉通知@全体(是/否) =
tgapi令牌 =
tg聊天id(个人或者群组id) =
smtp邮件服务器 =
是否使用SMTP服务SSL加密(是/否) =
SMTP邮件服务器端口 =
邮箱登录账号 =
发件人密码(授权码) =
发件人邮箱 =
发件人显示昵称 =
收件人邮箱 =
ntfy推送地址 = https://ntfy.sh/xxxx
ntfy推送标签 = tada
ntfy推送邮箱 =
pushplus推送token =
自定义推送标题 =
自定义开播推送内容 =
自定义关播推送内容 =
只推送通知不录制(是/否) =
直播推送检测频率(秒) = 1800
开播推送开启(是/否) =
关播推送开启(是/否)=
[Cookie]
# 录制抖音必填
抖音cookie = ttwid=1%7CB1qls3GdnZhUov9o2NxOMxxYS2ff6OSvEWbv0ytbES4%7C1680522049%7C280d802d6d478e3e78d0c807f7c487e7ffec0ae4e5fdd6a0fe74c3c6af149511; my_rd=1; passport_csrf_token=3ab34460fa656183fccfb904b16ff742; passport_csrf_token_default=3ab34460fa656183fccfb904b16ff742; d_ticket=9f562383ac0547d0b561904513229d76c9c21; n_mh=hvnJEQ4Q5eiH74-84kTFUyv4VK8xtSrpRZG1AhCeFNI; store-region=cn-fj; store-region-src=uid; LOGIN_STATUS=1; __security_server_data_status=1; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; pwa2=%223%7C0%7C3%7C0%22; download_guide=%223%2F20230729%2F0%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.6%7D; strategyABtestKey=%221690824679.923%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1536%2C%5C%22screen_height%5C%22%3A864%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A8%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A150%7D%22; VIDEO_FILTER_MEMO_SELECT=%7B%22expireTime%22%3A1691443863751%2C%22type%22%3Anull%7D; home_can_add_dy_2_desktop=%221%22; __live_version__=%221.1.1.2169%22; device_web_cpu_core=8; device_web_memory_size=8; xgplayer_user_id=346045893336; csrf_session_id=2e00356b5cd8544d17a0e66484946f28; odin_tt=724eb4dd23bc6ffaed9a1571ac4c757ef597768a70c75fef695b95845b7ffcd8b1524278c2ac31c2587996d058e03414595f0a4e856c53bd0d5e5f56dc6d82e24004dc77773e6b83ced6f80f1bb70627; __ac_nonce=064caded4009deafd8b89; __ac_signature=_02B4Z6wo00f01HLUuwwAAIDBh6tRkVLvBQBy9L-AAHiHf7; ttcid=2e9619ebbb8449eaa3d5a42d8ce88ec835; webcast_leading_last_show_time=1691016922379; webcast_leading_total_show_times=1; webcast_local_quality=sd; live_can_add_dy_2_desktop=%221%22; msToken=1JDHnVPw_9yTvzIrwb7cQj8dCMNOoesXbA_IooV8cezcOdpe4pzusZE7NB7tZn9TBXPr0ylxmv-KMs5rqbNUBHP4P7VBFUu0ZAht_BEylqrLpzgt3y5ne_38hXDOX8o=; msToken=jV_yeN1IQKUd9PlNtpL7k5vthGKcHo0dEh_QPUQhr8G3cuYv-Jbb4NnIxGDmhVOkZOCSihNpA2kvYtHiTW25XNNX_yrsv5FN8O6zm3qmCIXcEe0LywLn7oBO2gITEeg=; tt_scid=mYfqpfbDjqXrIGJuQ7q-DlQJfUSG51qG.KUdzztuGP83OjuVLXnQHjsz-BRHRJu4e986
快手cookie =
tiktok_cookie =
虎牙cookie =
斗鱼cookie =
yy_cookie =
b站cookie =
小红书cookie =
bigo_cookie =
blued_cookie =
sooplive_cookie =
netease_cookie =
千度热播_cookie =
pandatv_cookie =
猫耳fm_cookie =
winktv_cookie =
flextv_cookie =
look_cookie =
twitcasting_cookie =
baidu_cookie =
weibo_cookie =
kugou_cookie =
twitch_cookie =
liveme_cookie =
huajiao_cookie =
liuxing_cookie =
showroom_cookie =
acfun_cookie =
changliao_cookie =
yinbo_cookie =
yingke_cookie =
zhihu_cookie =
chzzk_cookie =
haixiu_cookie =
vvxqiu_cookie =
17live_cookie =
langlive_cookie =
pplive_cookie =
6room_cookie =
lehaitv_cookie =
huamao_cookie =
shopee_cookie =
youtube_cookie =
taobao_cookie =
jd_cookie =
faceit_cookie =
migu_cookie =
lianjie_cookie =
laixiu_cookie =
picarto_cookie =
[Authorization]
popkontv_token =
[账号密码]
sooplive账号 =
sooplive密码 =
flextv账号 =
flextv密码 =
popkontv账号 =
partner_code = P-00001
popkontv密码 =
twitcasting账号类型 = normal
twitcasting账号 =
twitcasting密码 =

227
demo.py Normal file
View File

@@ -0,0 +1,227 @@
# -*- coding: utf-8 -*-
import asyncio
from src.logger import logger
from src import spider
# 以下示例直播间链接不保证时效性,请自行查看链接是否能正常访问
# Please note that the following example live room links may not be up-to-date
LIVE_STREAM_CONFIG = {
"douyin": {
"url": "https://live.douyin.com/745964462470",
"func": spider.get_douyin_app_stream_data,
},
"tiktok": {
"url": "https://www.tiktok.com/@pearlgaga88/live",
"func": spider.get_tiktok_stream_data,
},
"kuaishou": {
"url": "https://live.kuaishou.com/u/yall1102",
"func": spider.get_kuaishou_stream_data,
},
"huya": {
"url": "https://www.huya.com/116",
"func": spider.get_huya_app_stream_url,
},
"douyu": {
"url": "https://www.douyu.com/topic/wzDBLS6?rid=4921614&dyshid=",
"func": spider.get_douyu_info_data,
},
"yy": {
"url": "https://www.yy.com/22490906/22490906",
"func": spider.get_yy_stream_data,
},
"bilibili": {
"url": "https://live.bilibili.com/21593109",
"func": spider.get_bilibili_stream_data,
},
"xhs": {
"url": "https://www.xiaohongshu.com/user/profile/6330049c000000002303c7ed?appuid=5f3f478a00000000010005b3",
"func": spider.get_xhs_stream_url,
},
"bigo": {
"url": "https://www.bigo.tv/cn/716418802",
"func": spider.get_bigo_stream_url,
},
"blued": {
"url": "https://app.blued.cn/live?id=Mp6G2R",
"func": spider.get_blued_stream_url,
},
"sooplive": {
"url": "https://play.sooplive.co.kr/sw7love",
"func": spider.get_sooplive_stream_data,
},
"netease": {
"url": "https://cc.163.com/583946984",
"func": spider.get_netease_stream_data,
},
"qiandurebo": {
"url": "https://qiandurebo.com/web/video.php?roomnumber=33333",
"func": spider.get_qiandurebo_stream_data,
},
"pandatv": {
"url": "https://www.pandalive.co.kr/live/play/bara0109",
"func": spider.get_pandatv_stream_data,
},
"maoerfm": {
"url": "https://fm.missevan.com/live/868895007",
"func": spider.get_maoerfm_stream_url,
},
"winktv": {
"url": "https://www.winktv.co.kr/live/play/anjer1004",
"func": spider.get_winktv_stream_data,
},
"flextv": {
"url": "https://www.ttinglive.com/channels/685479/live",
"func": spider.get_flextv_stream_data,
},
"looklive": {
"url": "https://look.163.com/live?id=65108820&position=3",
"func": spider.get_looklive_stream_url,
},
"popkontv": {
"url": "https://www.popkontv.com/live/view?castId=wjfal007&partnerCode=P-00117",
"func": spider.get_popkontv_stream_url,
},
"twitcasting": {
"url": "https://twitcasting.tv/c:uonq",
"func": spider.get_twitcasting_stream_url,
},
"baidu": {
"url": "https://live.baidu.com/m/media/pclive/pchome/live.html?room_id=9175031377&tab_category",
"func": spider.get_baidu_stream_data,
},
"weibo": {
"url": "https://weibo.com/u/7849520225",
"func": spider.get_weibo_stream_data,
},
"kugou": {
"url": "https://fanxing2.kugou.com/50428671?refer=2177&sourceFrom=",
"func": spider.get_kugou_stream_url,
},
"twitchtv": {
"url": "https://www.twitch.tv/gamerbee",
"func": spider.get_twitchtv_stream_data,
},
"liveme": {
"url": "https://www.liveme.com/zh/v/17141937295821012854/index.html",
"func": spider.get_liveme_stream_url,
},
"huajiao": {
"url": "https://www.huajiao.com/user/207446325",
"func": spider.get_huajiao_stream_url,
},
"showroom": {
"url": "https://www.showroom-live.com/room/profile?room_id=511033",
"func": spider.get_showroom_stream_data,
},
"acfun": {
"url": "https://live.acfun.cn/live/17912421",
"func": spider.get_acfun_stream_data,
},
"changliao": {
"url": "https://www.tlclw.com/801044397",
"func": spider.get_changliao_stream_url,
},
"yingke": {
"url": "https://www.inke.cn/liveroom/index.html?uid=710032101&id=1720857535354099",
"func": spider.get_yingke_stream_url,
},
"yinbo": {
"url": "https://live.ybw1666.com/800008687",
"func": spider.get_yinbo_stream_url,
},
"zhihu": {
"url": "https://www.zhihu.com/people/ac3a467005c5d20381a82230101308e9",
"func": spider.get_zhihu_stream_url,
},
"chzzk": {
"url": "https://chzzk.naver.com/live/458f6ec20b034f49e0fc6d03921646d2",
"func": spider.get_chzzk_stream_data,
},
"haixiu": {
"url": "https://www.haixiutv.com/6095106",
"func": spider.get_haixiu_stream_url,
},
"vvxqiu": {
"url": "https://h5webcdnp.vvxqiu.com//activity/videoShare/videoShare.html?h5Server=https://h5p.vvxqiu.com&"
"roomId=LP115664695&platformId=vvstar",
"func": spider.get_vvxqiu_stream_url,
},
"17live": {
"url": "https://17.live/en/live/6302408",
"func": spider.get_17live_stream_url,
},
"langlive": {
"url": "https://www.lang.live/en-US/room/3349463",
"func": spider.get_langlive_stream_url,
},
"pplive": {
"url": "https://m.pp.weimipopo.com/live/preview.html?uid=91648673&anchorUid=91625862&app=plpl",
"func": spider.get_pplive_stream_url,
},
"6room": {
"url": "https://v.6.cn/634435",
"func": spider.get_6room_stream_url,
},
"lehai": {
"url": "https://www.lehaitv.com/8059096",
"func": spider.get_haixiu_stream_url,
},
"huamao": {
"url": "https://h.catshow168.com/live/preview.html?uid=19066357&anchorUid=18895331",
"func": spider.get_pplive_stream_url,
},
"shopee": {
"url": "https://sg.shp.ee/GmpXeuf?uid=1006401066&session=802458",
"func": spider.get_shopee_stream_url,
},
"youtube": {
"url": "https://www.youtube.com/watch?v=cS6zS5hi1w0",
"func": spider.get_youtube_stream_url,
},
"taobao": {
"url": "https://m.tb.cn/h.TWp0HTd",
"func": spider.get_taobao_stream_url,
},
"jd": {
"url": "https://3.cn/28MLBy-E",
"func": spider.get_jd_stream_url,
},
"faceit": {
"url": "https://www.faceit.com/zh/players/Compl1/stream",
"func": spider.get_faceit_stream_data,
},
"lianjie": {
"url": "https://show.lailianjie.com/10000258",
"func": spider.get_lianjie_stream_url,
},
"migu": {
"url": "https://www.miguvideo.com/p/live/120000541321",
"func": spider.get_migu_stream_url,
},
"laixiu": {
"url": "https://www.imkktv.com/h5/share/video.html?uid=1845195&roomId=1710496",
"func": spider.get_laixiu_stream_url,
},
"picarto": {
"url": "https://www.picarto.tv/cuteavalanche",
"func": spider.get_picarto_stream_url,
}
}
def test_live_stream(platform_name: str, proxy_addr=None, cookies=None) -> None:
if platform_name in LIVE_STREAM_CONFIG:
config = LIVE_STREAM_CONFIG[platform_name]
try:
stream_data = asyncio.run(config['func'](config['url'], proxy_addr=proxy_addr, cookies=cookies))
logger.debug(f"Stream data for {platform_name}: {stream_data}")
except Exception as e:
logger.error(f"Error fetching stream data for {platform_name}: {e}")
else:
logger.warning(f"No configuration found for platform: {platform_name}")
if __name__ == "__main__":
platform = "douyin"
test_live_stream(platform)

16
docker-compose.yaml Normal file
View File

@@ -0,0 +1,16 @@
version: '3.8'
services:
app:
image: ihmily/douyin-live-recorder:latest
environment:
- TERM=xterm-256color
tty: true
stdin_open: true
#build: .
volumes:
- ./config:/app/config
- ./logs:/app/logs
- ./backup_config:/app/backup_config
- ./downloads:/app/downloads
restart: always

221
ffmpeg_install.py Normal file
View File

@@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
"""
Author: Hmily
GitHub: https://github.com/ihmily
Copyright (c) 2024 by Hmily, All Rights Reserved.
"""
import os
import re
import subprocess
import sys
import platform
import zipfile
from pathlib import Path
import requests
from tqdm import tqdm
from src.logger import logger
current_platform = platform.system()
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
current_env_path = os.environ.get('PATH')
ffmpeg_path = os.path.join(execute_dir, 'ffmpeg')
def unzip_file(zip_path: str | Path, extract_to: str | Path, delete: bool = True) -> None:
if not os.path.exists(extract_to):
os.makedirs(extract_to)
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_to)
if delete and os.path.exists(zip_path):
os.remove(zip_path)
def get_lanzou_download_link(url: str, password: str | None = None) -> str | None:
try:
headers = {
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Origin': 'https://wweb.lanzouv.com',
'Referer': 'https://wweb.lanzouv.com/iXncv0dly6mh',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0',
}
response = requests.get(url, headers=headers)
sign = re.search("var skdklds = '(.*?)';", response.text).group(1)
data = {
'action': 'downprocess',
'sign': sign,
'p': password,
'kd': '1',
}
response = requests.post('https://wweb.lanzouv.com/ajaxm.php', headers=headers, data=data)
json_data = response.json()
download_url = json_data['dom'] + "/file/" + json_data['url']
response = requests.get(download_url, headers=headers)
return response.url
except Exception as e:
logger.error(f"Failed to obtain ffmpeg download address. {e}")
def install_ffmpeg_windows():
try:
logger.warning("ffmpeg is not installed.")
logger.debug("Installing the latest version of ffmpeg for Windows...")
ffmpeg_url = get_lanzou_download_link('https://wweb.lanzouv.com/iHAc22ly3r3g', 'eots')
if ffmpeg_url:
full_file_name = 'ffmpeg_latest_build_20250124.zip'
version = 'v20250124'
zip_file_path = Path(execute_dir) / full_file_name
if Path(zip_file_path).exists():
logger.debug("ffmpeg installation file already exists, start install...")
else:
response = requests.get(ffmpeg_url, stream=True)
total_size = int(response.headers.get('Content-Length', 0))
block_size = 1024
with tqdm(total=total_size, unit="B", unit_scale=True,
ncols=100, desc=f'Downloading ffmpeg ({version})') as t:
with open(zip_file_path, 'wb') as f:
for data in response.iter_content(block_size):
t.update(len(data))
f.write(data)
unzip_file(zip_file_path, execute_dir)
os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path
result = subprocess.run(["ffmpeg", "-version"], capture_output=True)
if result.returncode == 0:
logger.debug('ffmpeg installation was successful')
else:
logger.error('ffmpeg installation failed. Please manually install ffmpeg by yourself')
return True
else:
logger.error("Please manually install ffmpeg by yourself")
except Exception as e:
logger.error(f"type: {type(e).__name__}, ffmpeg installation failed {e}")
def install_ffmpeg_mac():
logger.warning("ffmpeg is not installed.")
logger.debug("Installing the stable version of ffmpeg for macOS...")
try:
result = subprocess.run(["brew", "install", "ffmpeg"], capture_output=True)
if result.returncode == 0:
logger.debug('ffmpeg installation was successful. Restart for changes to take effect.')
return True
else:
logger.error("ffmpeg installation failed")
except subprocess.CalledProcessError as e:
logger.error(f"Failed to install ffmpeg using Homebrew. {e}")
logger.error("Please install ffmpeg manually or check your Homebrew installation.")
except Exception as e:
logger.error(f"An unexpected error occurred: {e}")
def install_ffmpeg_linux():
is_RHS = True
try:
logger.warning("ffmpeg is not installed.")
logger.debug("Trying to install the stable version of ffmpeg")
result = subprocess.run(['yum', '-y', 'update'], capture_output=True)
if result.returncode != 0:
logger.error("Failed to update package lists using yum.")
return False
result = subprocess.run(['yum', 'install', '-y', 'ffmpeg'], capture_output=True)
if result.returncode == 0:
logger.debug("ffmpeg installation was successful using yum. Restart for changes to take effect.")
return True
logger.error(result.stderr.decode('utf-8').strip())
except FileNotFoundError:
logger.debug("yum command not found, trying to install using apt...")
is_RHS = False
except Exception as e:
logger.error(f"An error occurred while trying to install ffmpeg using yum: {e}")
if not is_RHS:
try:
logger.debug("Trying to install the stable version of ffmpeg for Linux using apt...")
result = subprocess.run(['apt', 'update'], capture_output=True)
if result.returncode != 0:
logger.error("Failed to update package lists using apt")
return False
result = subprocess.run(['apt', 'install', '-y', 'ffmpeg'], capture_output=True)
if result.returncode == 0:
logger.debug("ffmpeg installation was successful using apt. Restart for changes to take effect.")
return True
else:
logger.error(result.stderr.decode('utf-8').strip())
except FileNotFoundError:
logger.error("apt command not found, unable to install ffmpeg. Please manually install ffmpeg by yourself")
except Exception as e:
logger.error(f"An error occurred while trying to install ffmpeg using apt: {e}")
logger.error("Manual installation of ffmpeg is required. Please manually install ffmpeg by yourself.")
return False
def install_ffmpeg() -> bool:
if current_platform == "Windows":
return install_ffmpeg_windows()
elif current_platform == "Linux":
return install_ffmpeg_linux()
elif current_platform == "Darwin":
return install_ffmpeg_mac()
else:
logger.debug(f"ffmpeg auto installation is not supported on this platform: {current_platform}. "
f"Please install ffmpeg manually.")
return False
def ensure_ffmpeg_installed(func):
def wrapper(*args, **kwargs):
try:
result = subprocess.run(['ffmpeg', '-version'], capture_output=True)
version = result.stdout.strip()
if result.returncode == 0 and version:
return func(*args, **kwargs)
except FileNotFoundError:
pass
return False
def wrapped_func(*args, **kwargs):
if sys.version_info >= (3, 7):
res = wrapper(*args, **kwargs)
else:
res = wrapper(*args, **kwargs)
if not res:
install_ffmpeg()
res = wrapper(*args, **kwargs)
if not res:
raise RuntimeError("ffmpeg is not installed.")
return func(*args, **kwargs)
return wrapped_func
def check_ffmpeg_installed() -> bool:
try:
result = subprocess.run(['ffmpeg', '-version'], capture_output=True)
version = result.stdout.strip()
if result.returncode == 0 and version:
return True
except FileNotFoundError:
pass
except OSError as e:
print(f"OSError occurred: {e}. ffmpeg may not be installed correctly or is not available in the system PATH.")
print("Please delete the ffmpeg and try to download and install again.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
return False
def check_ffmpeg() -> bool:
if not check_ffmpeg_installed():
return install_ffmpeg()
return True

32
i18n.py Normal file
View File

@@ -0,0 +1,32 @@
import os
import sys
import gettext
import inspect
import builtins
from pathlib import Path
def init_gettext(locale_dir, locale_name):
gettext.bindtextdomain('zh_CN', locale_dir)
gettext.textdomain('zh_CN')
os.environ['LANG'] = f'{locale_name}.utf8'
return gettext.gettext
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
if os.path.exists(Path(execute_dir) / '_internal/i18n'):
locale_path = Path(execute_dir) / '_internal/i18n'
else:
locale_path = Path(execute_dir) / 'i18n'
_tr = init_gettext(locale_path, 'zh_CN')
original_print = builtins.print
package_name = 'src'
def translated_print(*args, **kwargs):
for arg in args:
if package_name in inspect.stack()[1].filename:
translated_arg = _tr(str(arg))
else:
translated_arg = str(arg)
original_print(translated_arg, **kwargs)

View File

Binary file not shown.

View File

@@ -0,0 +1,85 @@
# DouyinLiveRecorder.
# Copyright (C) 2024 Hmily
# This file is distributed under the same license as the DouyinLiveRecorder package.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: 4.0.1\n"
"POT-Creation-Date: 2024-10-20 00:00+0800\n"
"PO-Revision-Date: 2024-11-09 03:05+0800\n"
"Last-Translator: Hmily <EMAIL@ADDRESS>\n"
"Language-Team: Chinese\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_CN\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: douyinliverecorder/spider.py
msgid "IP banned. Please change device or network."
msgstr "IP被禁止 请更换设备或网络"
msgid "The anchor did not start broadcasting."
msgstr "主播并未开播"
msgid "sooplive platform login successful! Starting to fetch live streaming data..."
msgstr "sooplive平台登录成功开始获取直播数据..."
msgid "sooplive live stream failed to retrieve, the live stream just ended."
msgstr "sooplive直播获取失败,该直播间刚结束直播"
msgid "sooplive live stream retrieval failed, the live needs 19+, you are not logged in."
msgstr "soop直播获取失败该直播间需要年龄19+观看,您尚未登录"
msgid "Attempting to log in to the sooplive live streaming platform with your account and password, please ensure it is configured."
msgstr "正在尝试使用您的账号和密码登录soop直播平台请确保已在config配置文件中配置"
msgid "error messagePlease check if the input sooplive live room address is correct."
msgstr "错误信息请检查输入的sooplive直播间地址是否正确"
msgid "Please check if the FlexTV account and password in the configuration file are correct."
msgstr "请检查配置文件中的FlexTV账号和密码是否正确"
msgid "FlexTV live stream retrieval failed [not logged in]: 19+ live streams are only available for logged-in adults."
msgstr "FlexTV直播获取失败[未登录]: 19+直播需要登录后是成人才可观看"
msgid "Attempting to log in to the FlexTV live streaming platform, please ensure your account and password are correctly filled in the configuration file."
msgstr "正在尝试登录FlexTV直播平台请确保已在配置文件中填写好您的账号和密码"
msgid "Logging into FlexTV platform..."
msgstr "FlexTV平台登录中..."
msgid "Logged into FlexTV platform successfully! Starting to fetch live streaming data..."
msgstr "FlexTV平台登录成功开始获取直播数据..."
msgid "Look live currently only supports audio live streaming, not video live streaming!"
msgstr "Look直播暂时只支持音频直播不支持Look视频直播!"
msgid "Failed to retrieve popkontv live stream [token does not exist or has expired]: Please log in to watch."
msgstr "popkontv直播获取失败[token不存在或者已过期]: 请登录后观看"
msgid "Attempting to log in to the popkontv live streaming platform, please ensure your account and password are correctly filled in the configuration file."
msgstr "正在尝试登录popkontv直播平台请确保已在配置文件中填写好您的账号和密码"
msgid "Logging into popkontv platform..."
msgstr "popkontv平台登录中..."
msgid "Logged into popkontv platform successfully! Starting to fetch live streaming data..."
msgstr "popkontv平台登录成功开始获取直播数据..."
msgid "Attempting to log in to TwitCasting..."
msgstr "TwitCasting正在尝试登录..."
msgid "TwitCasting login successful! Starting to fetch data..."
msgstr "TwitCasting 登录成功!开始获取数据..."
msgid "Failed to retrieve TwitCasting data, attempting to log in..."
msgstr "获取TwitCasting数据失败正在尝试登录..."
msgid "Failed to retrieve live room data, the Huajiao live room address is not fixed, please manually change the address for recording."
msgstr "获取直播间数据失败,花椒直播间地址是非固定的,请手动更换地址进行录制"
msgid "Fetch shopee live data failed, please update the address of the live broadcast room and try again."
msgstr "获取shopee直播间数据失败请手动更换直播录制地址后重试"

215
index.html Normal file
View File

@@ -0,0 +1,215 @@
<!--
Project: DouyinLiveRecorder
Author: Hmily
Build: 2023.08.14 - 20:24:05
GitHub Project URL: https://github.com/ihmily/DouyinLiveRecorder
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="never">
<title>M3U8 视频播放器</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&amp;display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flv.js@1.6.2/dist/flv.min.js"></script>
<style>
body {
font-family: 'Roboto', Arial, sans-serif;
background-color: #1a237e;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
padding: 0;
color: #ffffff;
background-image: linear-gradient(120deg, #1a237e 0%, #283593 50%, #4a148c 100%);
}
.container {
max-width: 640px;
width: 80%;
padding: 20px;
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.15);
}
#videoPlayer {
width: 100%;
height: 0;
padding-bottom: 56.25%;
position: relative;
background-color: #000;
border-radius: 5px;
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
display: none;
}
video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#videoUrlInput{
display: block;
width: 100%;
margin: 10px 0;
padding: 8px;
border-radius: 5px;
border: 1px solid #ccc;
box-sizing: border-box;
}
#playButton {
display: block;
width: 100%;
padding: 10px;
background-color: #283593;
color: white;
font-weight: bold;
border-radius: 5px;
border: none;
cursor: pointer;
transition: background-color 0.3s;
margin: 0 0 10px 0;
box-shadow: 0px 2px 4px 0px rgba(0,0,0,0.15);
}
#playButton:hover {
background-color: #1a237e;
}
.description {
margin-top: 20px;
line-height: 1.4;
font-size: 14px;
text-align: left;
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.15);
display: block;
}
.footer {
margin-top: 30px;
text-align: center;
font-size: 14px;
color: white;
}
p{
color: black;
}
a.no_style {
color: inherit;
text-decoration: none;
}
@media screen and (max-width: 768px) {
.container {
width: 90%;
border-radius: 0;
box-shadow: none;
margin-top:30px;
}
body {
overflow-y: scroll;
}
#videoUrlInput{
margin-top: 30px;
margin-bottom: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<input type="text" id="videoUrlInput" placeholder="请输入 M3U8或者FLV 视频链接">
<button id="playButton">播放视频</button>
<div id="videoPlayer">
<video controls></video>
</div>
<div class="description">
<p><strong>说明</strong><p>
<p>M3U8文件格式</p>
<p>M3U8文件是采用UTF-8编码格式的M3U文件。M3U文件本身是一个纯文本索引文件其核心功能是记录多媒体文件链接。当用户打开此类文件时播放软件会根据索引查找相应的音视频文件网络地址然后进行在线播放。</p>
<p>M3U最初设计用于播放音频文件例如MP3。但随着时间推移更多的播放器和软件开始使用M3U来播放视频文件列表同时也支持在线流媒体音频源的指定。目前许多播放器和软件都兼容M3U文件格式。</p>
<p>FLV文件格式Flash Video Format是Adobe公司开发的一种专门用于网页视频播放的文件格式。FLV格式的视频文件通常用于播放短视频和在线流媒体可以嵌入到网页中供用户观看。FLV视频通常由Adobe Flash Player播放器播放而其他第三方播放器也支持此格式。</p>
</div>
<div class="footer">
<p>&copy; 2023 <a href='https://github.com/ihmily/DouyinLiveRecorder' class="no_style" target="_blank">Hmily</a>. All rights reserved.</p>
</div>
<script>
function httpToHttps(url) {
if (url.startsWith("http://")) {
return url.replace("http://", "https://");
}
return url;
}
function playVideo() {
let videoUrl = document.getElementById('videoUrlInput').value;
const video = document.querySelector('#videoPlayer video');
const description = document.querySelector('.description');
if (videoUrl == ''){
alert('请输入视频链接');
return;
}
videoUrl = httpToHttps(videoUrl);
if (videoUrl.includes('.m3u8')) {
videoPlayer.style.display = 'block';
description.style.display = 'none';
if (Hls.isSupported()) {
const hls = new Hls();
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
hls.loadSource(videoUrl);
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = videoUrl;
} else {
alert('M3U8 格式不受您的浏览器支持。');
console.error('M3U8 格式不受您的浏览器支持。');
return;
}
} else if (videoUrl.includes('.flv')) {
if (flvjs.isSupported()) {
const flvPlayer = flvjs.createPlayer({
type: 'flv',
url: videoUrl
});
flvPlayer.attachMediaElement(video);
flvPlayer.load();
flvPlayer.play();
} else {
alert('FLV 格式不受您的浏览器支持。');
console.error('FLV 格式不受您的浏览器支持。');
return;
}
videoPlayer.style.display = 'block';
description.style.display = 'none';
} else {
console.error('不支持播放该视频格式。');
alert('不支持播放该视频格式。');
}
}
document.getElementById('playButton').addEventListener('click', playVideo);
</script>
</div>
</body>
</html>

2179
main.py Normal file

File diff suppressed because it is too large Load Diff

295
msg_push.py Normal file
View File

@@ -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)

7
requirements.txt Normal file
View File

@@ -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

14
src/__init__.py Normal file
View File

@@ -0,0 +1,14 @@
import os
import sys
from pathlib import Path
from .initializer import check_node
current_file_path = Path(__file__).resolve()
current_dir = current_file_path.parent
JS_SCRIPT_PATH = current_dir / 'javascript'
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
node_execute_dir = Path(execute_dir) / 'node'
current_env_path = os.environ.get('PATH')
os.environ['PATH'] = str(node_execute_dir) + os.pathsep + current_env_path
check_node()

View File

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
import httpx
from typing import Dict, Any
from .. import utils
OptionalStr = str | None
OptionalDict = Dict[str, Any] | None
async def async_req(
url: str,
proxy_addr: OptionalStr = None,
headers: OptionalDict = None,
data: dict | bytes | None = None,
json_data: dict | list | None = None,
timeout: int = 20,
redirect_url: bool = False,
return_cookies: bool = False,
include_cookies: bool = False,
abroad: bool = False,
content_conding: str = 'utf-8',
verify: bool = False,
http2: bool = True
) -> OptionalDict | OptionalStr | tuple:
if headers is None:
headers = {}
try:
proxy_addr = utils.handle_proxy_addr(proxy_addr)
if data or json_data:
async with httpx.AsyncClient(proxy=proxy_addr, timeout=timeout, verify=verify, http2=http2) as client:
response = await client.post(url, data=data, json=json_data, headers=headers)
else:
async with httpx.AsyncClient(proxy=proxy_addr, timeout=timeout, verify=verify, http2=http2) as client:
response = await client.get(url, headers=headers, follow_redirects=True)
if redirect_url:
return str(response.url)
elif return_cookies:
cookies_dict = {name: value for name, value in response.cookies.items()}
return (response.text, cookies_dict) if include_cookies else cookies_dict
else:
resp_str = response.text
except Exception as e:
resp_str = str(e)
return resp_str
async def get_response_status(url: str, proxy_addr: OptionalStr = None, headers: OptionalDict = None,
timeout: int = 10, abroad: bool = False, verify: bool = False, http2=False) -> bool:
try:
proxy_addr = utils.handle_proxy_addr(proxy_addr)
async with httpx.AsyncClient(proxy=proxy_addr, timeout=timeout, verify=verify) as client:
response = await client.head(url, headers=headers, follow_redirects=True)
return response.status_code == 200
except Exception as e:
print(e)
return False

View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
import gzip
import urllib.parse
import urllib.error
import requests
import ssl
import json
import urllib.request
no_proxy_handler = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(no_proxy_handler)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
OptionalStr = str | None
OptionalDict = dict | None
def sync_req(
url: str,
proxy_addr: OptionalStr = None,
headers: OptionalDict = None,
data: dict | bytes | None = None,
json_data: dict | list | None = None,
timeout: int = 20,
redirect_url: bool = False,
abroad: bool = False,
content_conding: str = 'utf-8'
) -> str:
if headers is None:
headers = {}
try:
if proxy_addr:
proxies = {
'http': proxy_addr,
'https': proxy_addr
}
if data or json_data:
response = requests.post(
url, data=data, json=json_data, headers=headers, proxies=proxies, timeout=timeout
)
else:
response = requests.get(url, headers=headers, proxies=proxies, timeout=timeout)
if redirect_url:
return response.url
resp_str = response.text
else:
if data and not isinstance(data, bytes):
data = urllib.parse.urlencode(data).encode(content_conding)
if json_data and isinstance(json_data, (dict, list)):
data = json.dumps(json_data).encode(content_conding)
req = urllib.request.Request(url, data=data, headers=headers)
try:
if abroad:
response = urllib.request.urlopen(req, timeout=timeout)
else:
response = opener.open(req, timeout=timeout)
if redirect_url:
return response.url
content_encoding = response.info().get('Content-Encoding')
try:
if content_encoding == 'gzip':
with gzip.open(response, 'rt', encoding=content_conding) as gzipped:
resp_str = gzipped.read()
else:
resp_str = response.read().decode(content_conding)
finally:
response.close()
except urllib.error.HTTPError as e:
if e.code == 400:
resp_str = e.read().decode(content_conding)
else:
raise
except urllib.error.URLError as e:
print(f"URL Error: {e}")
raise
except Exception as e:
print(f"An error occurred: {e}")
raise
except Exception as e:
resp_str = str(e)
return resp_str

220
src/initializer.py Normal file
View File

@@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
"""
Author: Hmily
GitHub:https://github.com/ihmily
Copyright (c) 2024 by Hmily, All Rights Reserved.
"""
import os
import subprocess
import sys
import platform
import zipfile
from pathlib import Path
import requests
import re
import distro
from tqdm import tqdm
from .logger import logger
current_platform = platform.system()
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
current_env_path = os.environ.get('PATH')
def unzip_file(zip_path: str | Path, extract_to: str | Path, delete: bool = True) -> None:
if not os.path.exists(extract_to):
os.makedirs(extract_to)
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_to)
if delete and os.path.exists(zip_path):
os.remove(zip_path)
def install_nodejs_windows():
try:
logger.warning("Node.js is not installed.")
logger.debug("Installing the stable version of Node.js for Windows...")
response = requests.get('https://nodejs.cn/download/')
if response.status_code == 200:
match = re.search('https://npmmirror.com/mirrors/node/(v.*?)/node-(v.*?)-x64.msi',
response.text)
if match:
version = match.group(1)
system_bit = 'x64' if '32' not in platform.machine() else 'x86'
url = f'https://npmmirror.com/mirrors/node/{version}/node-{version}-win-{system_bit}.zip'
else:
logger.error("Failed to retrieve the download URL for the latest version of Node.js...")
return
full_file_name = url.rsplit('/', maxsplit=1)[-1]
zip_file_path = Path(execute_dir) / full_file_name
if Path(zip_file_path).exists():
logger.debug("Node.js installation file already exists, start install...")
else:
response = requests.get(url, stream=True)
total_size = int(response.headers.get('Content-Length', 0))
block_size = 1024
with tqdm(total=total_size, unit="B", unit_scale=True,
ncols=100, desc=f'Downloading Node.js ({version})') as t:
with open(zip_file_path, 'wb') as f:
for data in response.iter_content(block_size):
t.update(len(data))
f.write(data)
unzip_file(zip_file_path, execute_dir)
extract_dir_path = str(zip_file_path).rsplit('.', maxsplit=1)[0]
f_path, f_name = os.path.splitext(zip_file_path)
new_extract_dir_path = Path(f_path).parent / 'node'
if Path(extract_dir_path).exists() and not Path(new_extract_dir_path).exists():
os.rename(extract_dir_path, new_extract_dir_path)
os.environ['PATH'] = execute_dir + '/node' + os.pathsep + current_env_path
result = subprocess.run(["node", "-v"], capture_output=True)
if result.returncode == 0:
logger.debug('Node.js installation was successful. Restart for changes to take effect')
else:
logger.debug('Node.js installation failed')
return True
else:
logger.error("Failed to retrieve the Node.js version page")
except Exception as e:
logger.error(f"type: {type(e).__name__}, Node.js installation failed {e}")
def install_nodejs_centos():
try:
logger.warning("Node.js is not installed.")
logger.debug("Installing the latest version of Node.js for CentOS...")
result = subprocess.run('curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/nodesource/rpm/setup_lts.x | '
'bash -', shell=True, capture_output=True)
if result.returncode != 0:
logger.error("Failed to run NodeSource installation script")
return
result = subprocess.run(['yum', 'install', '-y', 'epel-release'], capture_output=True)
if result.returncode != 0:
logger.error("Failed to install EPEL repository")
return
result = subprocess.run(['yum', 'install', '-y', 'nodejs'], capture_output=True)
if result.returncode == 0:
logger.debug('Node.js installation was successful. Restart for changes to take effect.')
return True
else:
logger.error("Node.js installation failed")
except Exception as e:
logger.error(f"type: {type(e).__name__}, Node.js installation failed {e}")
def install_nodejs_ubuntu():
try:
logger.warning("Node.js is not installed.")
logger.debug("Installing the latest version of Node.js for Ubuntu...")
install_script = 'curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -'
result = subprocess.run(install_script, shell=True, capture_output=True)
if result.returncode != 0:
logger.error("Failed to run NodeSource installation script")
return
install_command = ['apt', 'install', '-y', 'nodejs']
result = subprocess.run(install_command, capture_output=True)
if result.returncode == 0:
logger.debug('Node.js installation was successful. Restart for changes to take effect.')
return True
else:
logger.error("Node.js installation failed")
except Exception as e:
logger.error(f"type: {type(e).__name__}, Node.js installation failed, {e}")
def install_nodejs_mac():
logger.warning("Node.js is not installed.")
logger.debug("Installing the latest version of Node.js for macOS...")
try:
result = subprocess.run(["brew", "install", "node"], capture_output=True)
if result.returncode == 0:
logger.debug('Node.js installation was successful. Restart for changes to take effect.')
return True
else:
logger.error("Node.js installation failed")
except subprocess.CalledProcessError as e:
logger.error(f"Failed to install Node.js using Homebrew. {e}")
logger.error("Please install Node.js manually or check your Homebrew installation.")
except Exception as e:
logger.error(f"An unexpected error occurred: {e}")
def get_package_manager():
dist_id = distro.id()
if dist_id in ["centos", "fedora", "rhel", "amzn", "oracle", "scientific", "opencloudos", "alinux"]:
return "RHS"
else:
return "DBS"
def install_nodejs() -> bool:
if current_platform == "Windows":
return install_nodejs_windows()
elif current_platform == "Linux":
os_type = get_package_manager()
if os_type == "RHS":
return install_nodejs_centos()
else:
return install_nodejs_ubuntu()
elif current_platform == "Darwin":
return install_nodejs_mac()
else:
logger.debug(f"Node.js auto installation is not supported on this platform: {current_platform}. "
f"Please install Node.js manually.")
return False
def ensure_nodejs_installed(func):
def wrapper(*args, **kwargs):
try:
result = subprocess.run(['node', '-v'], capture_output=True)
version = result.stdout.strip()
if result.returncode == 0 and version:
return func(*args, **kwargs)
except FileNotFoundError:
pass
return False
def wrapped_func(*args, **kwargs):
if sys.version_info >= (3, 7):
res = wrapper(*args, **kwargs)
else:
res = wrapper(*args, **kwargs)
if not res:
install_nodejs()
res = wrapper(*args, **kwargs)
if not res:
raise RuntimeError("Node.js is not installed.")
return func(*args, **kwargs)
return wrapped_func
def check_nodejs_installed() -> bool:
try:
result = subprocess.run(['node', '-v'], capture_output=True)
version = result.stdout.strip()
if result.returncode == 0 and version:
return True
except FileNotFoundError:
pass
return False
def check_node() -> bool:
if not check_nodejs_installed():
return install_nodejs()

1
src/javascript/crypto-js.min.js vendored Normal file

File diff suppressed because one or more lines are too long

539
src/javascript/haixiu.js Normal file
View File

@@ -0,0 +1,539 @@
var closeGeetest = !1, _a123 = "haija1c7", _b2x = "xiuhc2a6", _c3y = "anchc3a5", _dx34 = "famic7a2", _hf_constants1 = "sowh1e", _hf_constants2 = "1000ha", _hf_constants3 = "butr12", _hf_constants4 = "2000h5", _gf_constants1 = "lehaaj", _gf_constants2 = "1000ax", _gf_constants3 = "lehaData"
let CryptoJS = null;
function EnmoliParamter() {
this._a123 = eval("_hf_constants1"),
this._b2x = eval("_hf_constants2"),
this._c3y = eval("_hf_constants3"),
this._dx34 = eval("_hf_constants4"),
this.getA123 = function() {
return this._a123
}
,
this.getB2X = function() {
return this._b2x
}
,
this.getC3Y = function() {
return this._c3y
}
,
this.getDX34 = function() {
return this._dx34
}
}
EnmoliParamter.prototype = {
aa: function(e, t) {
if (e === t)
return e;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e;
if (e.arity === t.arity && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ac: function(e, t) {
if (e === t)
return e;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e;
if (e.id === t.id && (e = CryptoJS.MD5(e) + ""),
e.arity1 === t.arity && e.string2 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ad: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e;
if (e.arity2 === t.arity && e.string3 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ae: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity3 === t.arity && e.string4 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
af: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity4 === t.arity && e.string5 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ah: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e;
if (e.arity6 === t.arity && e.string9 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ai: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e;
if (e.arity2 === t.arity5 && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
aj: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity44 === t.arity42 && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ak: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity21 === t.arity322 && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
ax: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity22 === t.arity32 && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
az: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity42 === t.arity57 && e.string2 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return e
},
are_similar: function(e, t) {
if (e === t)
return !0;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return !0;
return !0
}
return e
}
if (Array.isArray(t))
return e;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity === t.arity && e.string === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third);
case "function":
case "regexp":
return e;
default:
return !0
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e.second.string === t.second.string && "(string)" === t.second.id;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return e.second.string === t.second.string && "(string)" === e.second.id
}
return !1
},
ayz: function(e, t) {
if (e === t)
return e;
if (Array.isArray(e)) {
if (Array.isArray(t) && e.length === t.length) {
var i;
for (i = 0; i < e.length; i += 1)
if (!this.are_similar(e[i], t[i]))
return e;
return e
}
return e
}
if (Array.isArray(t))
return t;
if ("(number)" === e.id && "(number)" === t.id)
return e.number === t.number;
if (e.arity42 === t.arity57 && e.string2 === t.string)
switch (e.arity) {
case "prefix":
case "suffix":
case "infix":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second);
case "ternary":
return this.are_similar(e.first, t.first) && this.are_similar(e.second, t.second) && this.are_similar(e.third, t.third)
}
else {
if ("." === e.id && "[" === t.id && "infix" === t.arity)
return e;
if ("[" === e.id && "infix" === e.arity && "." === t.id)
return t
}
return this.getA123().substring(4) + this.getB2X().substring(4) + this.getC3Y().substring(4) + this.getDX34().substring(4)
}
}
function EnmoliSubmiter() {}
EnmoliSubmiter.prototype = {
bsq: function(e) {
var t = this.pf(e)
, i = this.as(t);
return this.brm(i)
},
pf: function(e) {
var t = {};
for (var i in e)
"" !== e[i] && (t[i] = e[i]);
return t
},
as: function(e) {
for (var t = {}, i = Object.keys(e).sort(), o = 0; o < i.length; o++) {
var n = i[o];
t[n] = e[n]
}
return t
},
brm: function(e) {
var t = this.cls(e)
, i = new EnmoliParamter;
return this.pt(t, i.ayz(t, "showselfAnchorVisitorParameters"))
},
cls: function(e) {
var t = "";
for (var i in e)
t = t + i + "=" + e[i] + "&";
return t = t.substring(0, t.length - 1)
},
pt: function(e, t) {
var i = new EnmoliParamter;
return e += t,
i.az(i.ax(i.ak(i.aj(i.ai(i.ah(i.af(i.ae(i.ad(i.ac(i.aa(e, e + "01" + t), e + "escape" + t), e + "same"), e + "visitor"), "anchor"), e + "person"), e + "ax" + t), "ae" + t), e + "ax" + t), e + "inspect" + t), "af" + t)
},
bnu: function(e, t) {
for (var i = e.split("&"), o = 0; o < i.length; o++) {
var n = i[o].split("=");
2 == n.length && (t[n[0]] = encodeURIComponent($.trim(n[1])).toString())
}
},
bn: function(e, t) {
for (var i in e)
"object" == typeof e[i] ? t[i] = encodeURIComponent($.trim(JSON.stringify(e[i]))).toString() : t[i] = encodeURIComponent($.trim(e[i])).toString()
}
}
var enmoliSubmiter = new EnmoliSubmiter();
function sign(options, cryptoJSPath){
CryptoJS = require(cryptoJSPath);
return enmoliSubmiter.bsq(options);
}
module.exports = {
sign
};
// const options = {
// "accessToken": "pLXSC%252FXJ0asc1I21tVL5FYZhNJn2Zg6d7m94umCnpgL%252BuVm31GQvyw%253D%253D",
// "tku": "3000006",
// "c": "10138100100000",
// "_st1": "1728621076958"
// }
// const cryptoJSPath = './crypto-js.min.js'
// console.log(sign(options, cryptoJSPath))

33
src/javascript/laixiu.js Normal file
View File

@@ -0,0 +1,33 @@
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function calculateSign() {
const a = new Date().getTime();
const s = generateUUID().replace(/-/g, "");
const u = 'kk792f28d6ff1f34ec702c08626d454b39pro';
const input = "web" + s + a + u;
const hash = CryptoJS.MD5(input).toString();
return {
timestamp: a,
imei: s,
requestId: hash,
inputString: input
};
}
function sign(cryptoJSPath) {
CryptoJS = require(cryptoJSPath);
return calculateSign();
}
module.exports = {
sign
};

425
src/javascript/liveme.js Normal file
View File

@@ -0,0 +1,425 @@
/**
* @author Hmily
* @createTime 2024-10-10
*/
const id = 1;
const r = `${new Date().getTime()}${id}`
const Am = "LM6000101139961122666757";
const rl = "undefined"
function createRandom(length = 32) {
let result = "";
const characters = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
for (let i = 0; i < length; ++i) {
const randomIndex = Math.floor(Math.random() * characters.length);
result += characters.charAt(randomIndex);
}
return result;
}
function createSignature(input = "4l4m5") {
let signature = "";
let number = 0;
for (let i = 0; i < input.length; ++i) {
const charCode = input.charCodeAt(i);
if (charCode >= 48 && charCode <= 57) {
number = number * 10 + (charCode - 48);
} else {
if (number !== 0) {
signature += createRandom(number);
number = 0;
}
signature += String.fromCharCode(charCode);
}
}
if (number !== 0) {
signature += createRandom(number);
}
return signature;
}
function oC(e) {
return e && e.__esModule && Object.prototype.hasOwnProperty.call(e, "default") ? e.default : e
}
var Tm = {
exports: {}
}
, Sm = {
exports: {}
};
(function() {
var e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
, t = {
rotl: function(n, r) {
return n << r | n >>> 32 - r
},
rotr: function(n, r) {
return n << 32 - r | n >>> r
},
endian: function(n) {
if (n.constructor == Number)
return t.rotl(n, 8) & 16711935 | t.rotl(n, 24) & 4278255360;
for (var r = 0; r < n.length; r++)
n[r] = t.endian(n[r]);
return n
},
randomBytes: function(n) {
for (var r = []; n > 0; n--)
r.push(Math.floor(Math.random() * 256));
return r
},
bytesToWords: function(n) {
for (var r = [], s = 0, o = 0; s < n.length; s++,
o += 8)
r[o >>> 5] |= n[s] << 24 - o % 32;
return r
},
wordsToBytes: function(n) {
for (var r = [], s = 0; s < n.length * 32; s += 8)
r.push(n[s >>> 5] >>> 24 - s % 32 & 255);
return r
},
bytesToHex: function(n) {
for (var r = [], s = 0; s < n.length; s++)
r.push((n[s] >>> 4).toString(16)),
r.push((n[s] & 15).toString(16));
return r.join("")
},
hexToBytes: function(n) {
for (var r = [], s = 0; s < n.length; s += 2)
r.push(parseInt(n.substr(s, 2), 16));
return r
},
bytesToBase64: function(n) {
for (var r = [], s = 0; s < n.length; s += 3)
for (var o = n[s] << 16 | n[s + 1] << 8 | n[s + 2], i = 0; i < 4; i++)
s * 8 + i * 6 <= n.length * 8 ? r.push(e.charAt(o >>> 6 * (3 - i) & 63)) : r.push("=");
return r.join("")
},
base64ToBytes: function(n) {
n = n.replace(/[^A-Z0-9+\/]/ig, "");
for (var r = [], s = 0, o = 0; s < n.length; o = ++s % 4)
o != 0 && r.push((e.indexOf(n.charAt(s - 1)) & Math.pow(2, -2 * o + 8) - 1) << o * 2 | e.indexOf(n.charAt(s)) >>> 6 - o * 2);
return r
}
};
Sm.exports = t
}
)();
var iC = Sm.exports
, nl = {
utf8: {
stringToBytes: function(e) {
return nl.bin.stringToBytes(unescape(encodeURIComponent(e)))
},
bytesToString: function(e) {
return decodeURIComponent(escape(nl.bin.bytesToString(e)))
}
},
bin: {
stringToBytes: function(e) {
for (var t = [], n = 0; n < e.length; n++)
t.push(e.charCodeAt(n) & 255);
return t
},
bytesToString: function(e) {
for (var t = [], n = 0; n < e.length; n++)
t.push(String.fromCharCode(e[n]));
return t.join("")
}
}
}, sd = nl;
var aC = function(e) {
return e != null && (Cm(e) || lC(e) || !!e._isBuffer)
};
function Cm(e) {
return !!e.constructor && typeof e.constructor.isBuffer == "function" && e.constructor.isBuffer(e)
}
function lC(e) {
return typeof e.readFloatLE == "function" && typeof e.slice == "function" && Cm(e.slice(0, 0))
}
(function() {
var e = iC
, t = sd.utf8
, n = aC
, r = sd.bin
, s = function(o, i) {
o.constructor == String ? i && i.encoding === "binary" ? o = r.stringToBytes(o) : o = t.stringToBytes(o) : n(o) ? o = Array.prototype.slice.call(o, 0) : !Array.isArray(o) && o.constructor !== Uint8Array && (o = o.toString());
for (var a = e.bytesToWords(o), l = o.length * 8, c = 1732584193, u = -271733879, f = -1732584194, d = 271733878, m = 0; m < a.length; m++)
a[m] = (a[m] << 8 | a[m] >>> 24) & 16711935 | (a[m] << 24 | a[m] >>> 8) & 4278255360;
a[l >>> 5] |= 128 << l % 32,
a[(l + 64 >>> 9 << 4) + 14] = l;
for (var v = s._ff, w = s._gg, R = s._hh, y = s._ii, m = 0; m < a.length; m += 16) {
var b = c
, _ = u
, g = f
, C = d;
c = v(c, u, f, d, a[m + 0], 7, -680876936),
d = v(d, c, u, f, a[m + 1], 12, -389564586),
f = v(f, d, c, u, a[m + 2], 17, 606105819),
u = v(u, f, d, c, a[m + 3], 22, -1044525330),
c = v(c, u, f, d, a[m + 4], 7, -176418897),
d = v(d, c, u, f, a[m + 5], 12, 1200080426),
f = v(f, d, c, u, a[m + 6], 17, -1473231341),
u = v(u, f, d, c, a[m + 7], 22, -45705983),
c = v(c, u, f, d, a[m + 8], 7, 1770035416),
d = v(d, c, u, f, a[m + 9], 12, -1958414417),
f = v(f, d, c, u, a[m + 10], 17, -42063),
u = v(u, f, d, c, a[m + 11], 22, -1990404162),
c = v(c, u, f, d, a[m + 12], 7, 1804603682),
d = v(d, c, u, f, a[m + 13], 12, -40341101),
f = v(f, d, c, u, a[m + 14], 17, -1502002290),
u = v(u, f, d, c, a[m + 15], 22, 1236535329),
c = w(c, u, f, d, a[m + 1], 5, -165796510),
d = w(d, c, u, f, a[m + 6], 9, -1069501632),
f = w(f, d, c, u, a[m + 11], 14, 643717713),
u = w(u, f, d, c, a[m + 0], 20, -373897302),
c = w(c, u, f, d, a[m + 5], 5, -701558691),
d = w(d, c, u, f, a[m + 10], 9, 38016083),
f = w(f, d, c, u, a[m + 15], 14, -660478335),
u = w(u, f, d, c, a[m + 4], 20, -405537848),
c = w(c, u, f, d, a[m + 9], 5, 568446438),
d = w(d, c, u, f, a[m + 14], 9, -1019803690),
f = w(f, d, c, u, a[m + 3], 14, -187363961),
u = w(u, f, d, c, a[m + 8], 20, 1163531501),
c = w(c, u, f, d, a[m + 13], 5, -1444681467),
d = w(d, c, u, f, a[m + 2], 9, -51403784),
f = w(f, d, c, u, a[m + 7], 14, 1735328473),
u = w(u, f, d, c, a[m + 12], 20, -1926607734),
c = R(c, u, f, d, a[m + 5], 4, -378558),
d = R(d, c, u, f, a[m + 8], 11, -2022574463),
f = R(f, d, c, u, a[m + 11], 16, 1839030562),
u = R(u, f, d, c, a[m + 14], 23, -35309556),
c = R(c, u, f, d, a[m + 1], 4, -1530992060),
d = R(d, c, u, f, a[m + 4], 11, 1272893353),
f = R(f, d, c, u, a[m + 7], 16, -155497632),
u = R(u, f, d, c, a[m + 10], 23, -1094730640),
c = R(c, u, f, d, a[m + 13], 4, 681279174),
d = R(d, c, u, f, a[m + 0], 11, -358537222),
f = R(f, d, c, u, a[m + 3], 16, -722521979),
u = R(u, f, d, c, a[m + 6], 23, 76029189),
c = R(c, u, f, d, a[m + 9], 4, -640364487),
d = R(d, c, u, f, a[m + 12], 11, -421815835),
f = R(f, d, c, u, a[m + 15], 16, 530742520),
u = R(u, f, d, c, a[m + 2], 23, -995338651),
c = y(c, u, f, d, a[m + 0], 6, -198630844),
d = y(d, c, u, f, a[m + 7], 10, 1126891415),
f = y(f, d, c, u, a[m + 14], 15, -1416354905),
u = y(u, f, d, c, a[m + 5], 21, -57434055),
c = y(c, u, f, d, a[m + 12], 6, 1700485571),
d = y(d, c, u, f, a[m + 3], 10, -1894986606),
f = y(f, d, c, u, a[m + 10], 15, -1051523),
u = y(u, f, d, c, a[m + 1], 21, -2054922799),
c = y(c, u, f, d, a[m + 8], 6, 1873313359),
d = y(d, c, u, f, a[m + 15], 10, -30611744),
f = y(f, d, c, u, a[m + 6], 15, -1560198380),
u = y(u, f, d, c, a[m + 13], 21, 1309151649),
c = y(c, u, f, d, a[m + 4], 6, -145523070),
d = y(d, c, u, f, a[m + 11], 10, -1120210379),
f = y(f, d, c, u, a[m + 2], 15, 718787259),
u = y(u, f, d, c, a[m + 9], 21, -343485551),
c = c + b >>> 0,
u = u + _ >>> 0,
f = f + g >>> 0,
d = d + C >>> 0
}
return e.endian([c, u, f, d])
};
s._ff = function(o, i, a, l, c, u, f) {
var d = o + (i & a | ~i & l) + (c >>> 0) + f;
return (d << u | d >>> 32 - u) + i
}
,
s._gg = function(o, i, a, l, c, u, f) {
var d = o + (i & l | a & ~l) + (c >>> 0) + f;
return (d << u | d >>> 32 - u) + i
}
,
s._hh = function(o, i, a, l, c, u, f) {
var d = o + (i ^ a ^ l) + (c >>> 0) + f;
return (d << u | d >>> 32 - u) + i
}
,
s._ii = function(o, i, a, l, c, u, f) {
var d = o + (a ^ (i | ~l)) + (c >>> 0) + f;
return (d << u | d >>> 32 - u) + i
}
,
s._blocksize = 16,
s._digestsize = 16,
Tm.exports = function(o, i) {
if (o == null)
throw new Error("Illegal argument " + o);
var a = e.wordsToBytes(s(o, i));
return i && i.asBytes ? a : i && i.asString ? r.bytesToString(a) : e.bytesToHex(a)
}
}
)();
var cC = Tm.exports;
var t = {
utf8: {
stringToBytes: function(e) {
return nl.bin.stringToBytes(unescape(encodeURIComponent(e)))
},
bytesToString: function(e) {
return decodeURIComponent(escape(nl.bin.bytesToString(e)))
}
},
bin: {
stringToBytes: function(e) {
for (var t = [], n = 0; n < e.length; n++)
t.push(e.charCodeAt(n) & 255);
return t
},
bytesToString: function(e) {
for (var t = [], n = 0; n < e.length; n++)
t.push(String.fromCharCode(e[n]));
return t.join("")
}
}
};
const hC = (e, t, n=!1) => {
if (t.params) {
const o = {};
Object.keys(t.params).forEach(i => {
t.params[i] !== void 0 && t.params[i] !== null && (o[i] = t.params[i])
}
),
t.params = o
}
let r = {};
const s = t.method.toLowerCase();
if (s === "get")
t.params = Object.assign({}, e, t.params || {});
else if (s === "post")
if (typeof t.data == "string") {
let o;
t.data.split("&").forEach(i => {
o = i.split("="),
r[o[0]] = o[1]
}
),
r = Object.assign({}, e, r),
t.data = Object.keys(r).map(i => `${i}=${r[i]}`).join("&")
} else
r = Object.assign(r, e, t.data || {}),
t.data = r;
return n ? t : (r = Object.assign({}, t.params, r),
r)
}
const Rm = oC(cC);
const s = Rm(r);
pC = e => {
let t = Object.keys(e).sort().map(n => {
function r(s) {
return Array.isArray(s) ? s.join(",") : typeof s === "object" ? JSON.stringify(s) : s
}
return n + r(e[n])
}
).join("");
return t += Am + e.lm_s_ts + rl,
Rm(t)
}
// final encryption function
let CryptoJS = null;
lm_s_key = atob('ZGQ0NmRiYjQ0MmI2ZTRiYTgxN2Q2MzQ3ZDJkZGY0OTM=');
function requestSign(signParams, cryptoJSPath) {
let sKey = Object.keys(signParams).sort().map(key => {
function getValue(val) {
if (Array.isArray(val)) {
return val.join(',');
}
if (typeof val === 'object') {
return JSON.stringify(val);
}
return val;
}
return key + getValue(signParams[key]);
}).join('');
sKey += signParams.lm_s_id + signParams.lm_s_ts + lm_s_key;
console.log(`sKey: ${sKey}`);
CryptoJS = require(cryptoJSPath);
return CryptoJS.MD5(sKey).toString();
}
function sign(videoid, cryptoJSPath, platform='web'){
const vali = createSignature();
const data_e = {
lm_s_id: Am,
lm_s_ts: r,
lm_s_str: s,
lm_s_ver: 1,
h5: 1
};
/* data_e example value
const data_e = {
lm_s_id: Am,
lm_s_ts: "17284909009151",
lm_s_str: "88f9777231dc2d6ac462a1d7ebf5f54e",
lm_s_ver: 1,
h5: 1
};
*/
console.log("data_e:",data_e);
data_i = {
...data_e,
_time: new Date().valueOf(),
thirdchannel: 6,
videoid: videoid,
area: 'zh',
vali: vali
}
console.log("data_i:",data_i);
// fake lm_s_sign param value
let lm_s_sign = pC(data_i);
console.log(`fake lm_s_sign: ${lm_s_sign}`);
//finnal request params
/*
signParams = {
"alias": "liveme",
"tongdun_black_box": "iWPU21728483558afruvSVo6x0",
"os": "android",
"lm_s_id": "LM6000101139961122666757",
"lm_s_ts": "17284909009151",
"lm_s_str": "88f9777231dc2d6ac462a1d7ebf5f54e",
"lm_s_ver": 1,
"h5": 1,
"_time": 1728490664651,
"thirdchannel": 6,
"videoid": "17284844223282059697",
"area": "zh",
"vali": "zH8SlBwnCm4AZWp"
}#
//result: 4eaf71a1ec19b49b7267e4d16e007105
*/
signParams = {
"alias": "liveme",
"tongdun_black_box": "",
"os": platform,
...data_i
}
console.log("signParams: ", signParams);
lm_s_sign = requestSign(signParams, cryptoJSPath);
console.log(`\x1b[32mfinal lm_s_sign: \x1b[0m${lm_s_sign}\n`);
data = {
...signParams,
lm_s_sign
}
return data;
}
module.exports = {
sign
};

143
src/javascript/migu.js Normal file
View File

@@ -0,0 +1,143 @@
/**
* Function to get the ddCalcu parameter value
* @param {string} inputUrl - The original URL before encryption
* @returns {Promise<string>} - Returns the calculated ddCalcu value
*/
async function getDdCalcu(inputUrl) {
let wasmInstance = null;
let memory_p = null; // Uint8Array view
let memory_h = null; // Uint32Array view
// Fixed parameter
const f = 'PBTxuWiTEbUPPFcpyxs0ww==';
// Utility function: Convert string to UTF-8 in memory
function stringToUTF8(string, offset) {
const encoder = new TextEncoder();
const encoded = encoder.encode(string);
for (let i = 0; i < encoded.length; i++) {
memory_p[offset + i] = encoded[i];
}
memory_p[offset + encoded.length] = 0; // Null-terminate
}
// Utility function: Read UTF-8 string from memory address
function UTF8ToString(offset) {
let s = '';
let i = 0;
while (memory_p[offset + i]) {
s += String.fromCharCode(memory_p[offset + i]);
i++;
}
return s;
}
// WASM import function stubs
function a(e, t, r, n) {
let s = 0;
for (let i = 0; i < r; i++) {
const d = memory_h[t + 4 >> 2];
t += 8;
s += d;
}
memory_h[n >> 2] = s;
return 0;
}
function b() {}
function c() {}
// Step 1: Retrieve playerVersion
const settingsResp = await fetch('https://app-sc.miguvideo.com/common/v1/settings/H5_DetailPage');
const settingsData = await settingsResp.json();
const playerVersion = JSON.parse(settingsData.body.paramValue).playerVersion;
// Step 2: Load WASM module
const wasmUrl = `https://www.miguvideo.com/mgs/player/prd/${playerVersion}/dist/mgprtcl.wasm`;
const wasmResp = await fetch(wasmUrl);
if (!wasmResp.ok) throw new Error("Failed to download WASM");
const wasmBuffer = await wasmResp.arrayBuffer();
const importObject = {
a: { a, b, c }
};
const { instance } = await WebAssembly.instantiate(wasmBuffer, importObject);
wasmInstance = instance;
const memory = wasmInstance.exports.d;
memory_p = new Uint8Array(memory.buffer);
memory_h = new Uint32Array(memory.buffer);
const exports = {
CallInterface1: wasmInstance.exports.h,
CallInterface2: wasmInstance.exports.i,
CallInterface3: wasmInstance.exports.j,
CallInterface4: wasmInstance.exports.k,
CallInterface6: wasmInstance.exports.m,
CallInterface7: wasmInstance.exports.n,
CallInterface8: wasmInstance.exports.o,
CallInterface9: wasmInstance.exports.p,
CallInterface10: wasmInstance.exports.q,
CallInterface11: wasmInstance.exports.r,
CallInterface14: wasmInstance.exports.t,
malloc: wasmInstance.exports.u,
};
const parsedUrl = new URL(inputUrl);
const query = Object.fromEntries(parsedUrl.searchParams);
const o = query.userid || '';
const a_val = query.timestamp || '';
const s = query.ProgramID || '';
const u = query.Channel_ID || '';
const v = query.puData || '';
// Allocate memory
const d = exports.malloc(o.length + 1);
const h = exports.malloc(a_val.length + 1);
const y = exports.malloc(s.length + 1);
const m = exports.malloc(u.length + 1);
const g = exports.malloc(v.length + 1);
const b_val = exports.malloc(f.length + 1);
const E = exports.malloc(128);
const T = exports.malloc(128);
// Write data to memory
stringToUTF8(o, d);
stringToUTF8(a_val, h);
stringToUTF8(s, y);
stringToUTF8(u, m);
stringToUTF8(v, g);
stringToUTF8(f, b_val);
// Call interface functions
const S = exports.CallInterface6(); // Create context
exports.CallInterface1(S, y, s.length);
exports.CallInterface10(S, h, a_val.length);
exports.CallInterface9(S, d, o.length);
exports.CallInterface3(S, 0, 0);
exports.CallInterface11(S, 0, 0);
exports.CallInterface8(S, g, v.length);
exports.CallInterface2(S, m, u.length);
exports.CallInterface14(S, b_val, f.length, T, 128);
const w = UTF8ToString(T);
const I = exports.malloc(w.length + 1);
stringToUTF8(w, I);
exports.CallInterface7(S, I, w.length);
exports.CallInterface4(S, E, 128);
return UTF8ToString(E);
}
const url = process.argv[2];
getDdCalcu(url).then(result => {
console.log(result);
}).catch(err => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,78 @@
function sign(e) {
function t(e, t) {
return e << t | e >>> 32 - t
}
function o(e, t) {
var o, n, r, i, a;
return r = 2147483648 & e,
i = 2147483648 & t,
a = (1073741823 & e) + (1073741823 & t),
(o = 1073741824 & e) & (n = 1073741824 & t) ? 2147483648 ^ a ^ r ^ i : o | n ? 1073741824 & a ? 3221225472 ^ a ^ r ^ i : 1073741824 ^ a ^ r ^ i : a ^ r ^ i
}
function n(e, n, r, i, a, s, u) {
return o(t(e = o(e, o(o(function(e, t, o) {
return e & t | ~e & o
}(n, r, i), a), u)), s), n)
}
function r(e, n, r, i, a, s, u) {
return o(t(e = o(e, o(o(function(e, t, o) {
return e & o | t & ~o
}(n, r, i), a), u)), s), n)
}
function i(e, n, r, i, a, s, u) {
return o(t(e = o(e, o(o(function(e, t, o) {
return e ^ t ^ o
}(n, r, i), a), u)), s), n)
}
function a(e, n, r, i, a, s, u) {
return o(t(e = o(e, o(o(function(e, t, o) {
return t ^ (e | ~o)
}(n, r, i), a), u)), s), n)
}
function s(e) {
var t, o = "", n = "";
for (t = 0; 3 >= t; t++)
o += (n = "0" + (e >>> 8 * t & 255).toString(16)).substr(n.length - 2, 2);
return o
}
var u, l, d, c, p, f, h, m, y, g;
for (g = function(e) {
for (var t = e.length, o = t + 8, n = 16 * ((o - o % 64) / 64 + 1), r = Array(n - 1), i = 0, a = 0; t > a; )
i = a % 4 * 8,
r[(a - a % 4) / 4] |= e.charCodeAt(a) << i,
a++;
return i = a % 4 * 8,
r[(a - a % 4) / 4] |= 128 << i,
r[n - 2] = t << 3,
r[n - 1] = t >>> 29,
r
}(e = function(e) {
var t = String.fromCharCode;
e = e.replace(/\r\n/g, "\n");
for (var o, n = "", r = 0; r < e.length; r++)
128 > (o = e.charCodeAt(r)) ? n += t(o) : o > 127 && 2048 > o ? (n += t(o >> 6 | 192),
n += t(63 & o | 128)) : (n += t(o >> 12 | 224),
n += t(o >> 6 & 63 | 128),
n += t(63 & o | 128));
return n
}(e)),
f = 1732584193,
h = 4023233417,
m = 2562383102,
y = 271733878,
u = 0; u < g.length; u += 16)
l = f,
d = h,
c = m,
p = y,
h = a(h = a(h = a(h = a(h = i(h = i(h = i(h = i(h = r(h = r(h = r(h = r(h = n(h = n(h = n(h = n(h, m = n(m, y = n(y, f = n(f, h, m, y, g[u + 0], 7, 3614090360), h, m, g[u + 1], 12, 3905402710), f, h, g[u + 2], 17, 606105819), y, f, g[u + 3], 22, 3250441966), m = n(m, y = n(y, f = n(f, h, m, y, g[u + 4], 7, 4118548399), h, m, g[u + 5], 12, 1200080426), f, h, g[u + 6], 17, 2821735955), y, f, g[u + 7], 22, 4249261313), m = n(m, y = n(y, f = n(f, h, m, y, g[u + 8], 7, 1770035416), h, m, g[u + 9], 12, 2336552879), f, h, g[u + 10], 17, 4294925233), y, f, g[u + 11], 22, 2304563134), m = n(m, y = n(y, f = n(f, h, m, y, g[u + 12], 7, 1804603682), h, m, g[u + 13], 12, 4254626195), f, h, g[u + 14], 17, 2792965006), y, f, g[u + 15], 22, 1236535329), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 1], 5, 4129170786), h, m, g[u + 6], 9, 3225465664), f, h, g[u + 11], 14, 643717713), y, f, g[u + 0], 20, 3921069994), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 5], 5, 3593408605), h, m, g[u + 10], 9, 38016083), f, h, g[u + 15], 14, 3634488961), y, f, g[u + 4], 20, 3889429448), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 9], 5, 568446438), h, m, g[u + 14], 9, 3275163606), f, h, g[u + 3], 14, 4107603335), y, f, g[u + 8], 20, 1163531501), m = r(m, y = r(y, f = r(f, h, m, y, g[u + 13], 5, 2850285829), h, m, g[u + 2], 9, 4243563512), f, h, g[u + 7], 14, 1735328473), y, f, g[u + 12], 20, 2368359562), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 5], 4, 4294588738), h, m, g[u + 8], 11, 2272392833), f, h, g[u + 11], 16, 1839030562), y, f, g[u + 14], 23, 4259657740), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 1], 4, 2763975236), h, m, g[u + 4], 11, 1272893353), f, h, g[u + 7], 16, 4139469664), y, f, g[u + 10], 23, 3200236656), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 13], 4, 681279174), h, m, g[u + 0], 11, 3936430074), f, h, g[u + 3], 16, 3572445317), y, f, g[u + 6], 23, 76029189), m = i(m, y = i(y, f = i(f, h, m, y, g[u + 9], 4, 3654602809), h, m, g[u + 12], 11, 3873151461), f, h, g[u + 15], 16, 530742520), y, f, g[u + 2], 23, 3299628645), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 0], 6, 4096336452), h, m, g[u + 7], 10, 1126891415), f, h, g[u + 14], 15, 2878612391), y, f, g[u + 5], 21, 4237533241), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 12], 6, 1700485571), h, m, g[u + 3], 10, 2399980690), f, h, g[u + 10], 15, 4293915773), y, f, g[u + 1], 21, 2240044497), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 8], 6, 1873313359), h, m, g[u + 15], 10, 4264355552), f, h, g[u + 6], 15, 2734768916), y, f, g[u + 13], 21, 1309151649), m = a(m, y = a(y, f = a(f, h, m, y, g[u + 4], 6, 4149444226), h, m, g[u + 11], 10, 3174756917), f, h, g[u + 2], 15, 718787259), y, f, g[u + 9], 21, 3951481745),
f = o(f, l),
h = o(h, d),
m = o(m, c),
y = o(y, p);
return (s(f) + s(h) + s(m) + s(y)).toLowerCase()
}
// 正确sign值05748e8359cd3e6deaab02d15caafc11
// var sg =sign('5655b7041ca049730330701082886efd&1719411639403&12574478&{"componentKey":"wp_pc_shop_basic_info","params":"{\\"memberId\\":\\"b2b-22133374292418351a\\"}"}')
// console.log(sg)

564
src/javascript/x-bogus.js Normal file

File diff suppressed because one or more lines are too long

43
src/logger.py Normal file
View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
import os
import sys
from loguru import logger
logger.remove()
custom_format = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> - <level>{message}</level>"
logger.add(
sink=sys.stderr,
format=custom_format,
level="DEBUG",
colorize=True,
enqueue=True
)
script_path = os.path.split(os.path.realpath(sys.argv[0]))[0]
logger.add(
f"{script_path}/logs/streamget.log",
level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}",
filter=lambda i: i["level"].name != "INFO",
serialize=False,
enqueue=True,
retention=1,
rotation="300 KB",
encoding='utf-8'
)
logger.add(
f"{script_path}/logs/PlayURL.log",
level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {message}",
filter=lambda i: i["level"].name == "INFO",
serialize=False,
enqueue=True,
retention=1,
rotation="300 KB",
encoding='utf-8'
)

92
src/proxy.py Normal file
View File

@@ -0,0 +1,92 @@
import os
import sys
from enum import Enum, auto
from dataclasses import dataclass, field
from .utils import logger
class ProxyType(Enum):
HTTP = auto()
HTTPS = auto()
SOCKS = auto()
@dataclass(frozen=True)
class ProxyInfo:
ip: str = field(default="", repr=True)
port: str = field(default="", repr=True)
def __post_init__(self):
if (self.ip and not self.port) or (not self.ip and self.port):
raise ValueError("IP or port cannot be empty")
if (self.ip and self.port) and (not self.port.isdigit() or not (1 <= int(self.port) <= 65535)):
raise ValueError("Port must be a digit between 1 and 65535")
class ProxyDetector:
def __init__(self):
if sys.platform.startswith('win'):
import winreg
self.winreg = winreg
self.__path = r'Software\Microsoft\Windows\CurrentVersion\Internet Settings'
with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as key_user:
self.__INTERNET_SETTINGS = winreg.OpenKeyEx(key_user, self.__path, 0, winreg.KEY_ALL_ACCESS)
else:
self.__is_windows = False
def get_proxy_info(self) -> ProxyInfo:
if sys.platform.startswith('win'):
ip, port = self._get_proxy_info_windows()
else:
ip, port = self._get_proxy_info_linux()
return ProxyInfo(ip, port)
def is_proxy_enabled(self) -> bool:
if sys.platform.startswith('win'):
return self._is_proxy_enabled_windows()
else:
return self._is_proxy_enabled_linux()
def _get_proxy_info_windows(self) -> tuple[str, str]:
ip, port = "", ""
if self._is_proxy_enabled_windows():
try:
ip_port = self.winreg.QueryValueEx(self.__INTERNET_SETTINGS, "ProxyServer")[0]
if ip_port:
ip, port = ip_port.split(":")
except FileNotFoundError as err:
logger.warning("No proxy information found: " + str(err))
except Exception as err:
logger.error("An error occurred: " + str(err))
else:
logger.debug("No proxy is enabled on the system")
return ip, port
def _is_proxy_enabled_windows(self) -> bool:
try:
if self.winreg.QueryValueEx(self.__INTERNET_SETTINGS, "ProxyEnable")[0] == 1:
return True
except FileNotFoundError as err:
print("No proxy information found: " + str(err))
except Exception as err:
print("An error occurred: " + str(err))
return False
@staticmethod
def _get_proxy_info_linux() -> tuple[str, str]:
proxies = {
'http': os.getenv('http_proxy'),
'https': os.getenv('https_proxy'),
'ftp': os.getenv('ftp_proxy')
}
ip = port = ""
for proto, proxy in proxies.items():
if proxy:
ip, port = proxy.split(':')
break
return ip, port
def _is_proxy_enabled_linux(self) -> bool:
proxies = self._get_proxy_info_linux()
return any(proxy != '' for proxy in proxies)

150
src/room.py Normal file
View File

@@ -0,0 +1,150 @@
# -*- encoding: utf-8 -*-
"""
Author: Hmily
GitHub:https://github.com/ihmily
Date: 2023-07-17 23:52:05
Update: 2025-02-04 04:57:00
Copyright (c) 2023 by Hmily, All Rights Reserved.
"""
import re
import urllib.parse
import execjs
import httpx
import urllib.request
from . import JS_SCRIPT_PATH, utils
no_proxy_handler = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(no_proxy_handler)
class UnsupportedUrlError(Exception):
pass
HEADERS = {
'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',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Cookie': 's_v_web_id=verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf'
}
HEADERS_PC = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0',
'Cookie': 'sessionid=7494ae59ae06784454373ce25761e864; __ac_nonce=0670497840077ee4c9eb2; '
'__ac_signature=_02B4Z6wo00f012DZczQAAIDCJJBb3EjnINdg-XeAAL8-db; '
's_v_web_id=verify_m1ztgtjj_vuHnMLZD_iwZ9_4YO4_BdN1_7wLP3pyqXsf2; '
}
# X-bogus算法
async def get_xbogus(url: str, headers: dict | None = None) -> str:
if not headers or 'user-agent' not in (k.lower() for k in headers):
headers = HEADERS
query = urllib.parse.urlparse(url).query
xbogus = execjs.compile(open(f'{JS_SCRIPT_PATH}/x-bogus.js').read()).call(
'sign', query, headers.get("User-Agent", "user-agent"))
return xbogus
# 获取房间ID和用户secID
async def get_sec_user_id(url: str, proxy_addr: str | None = None, headers: dict | None = None) -> tuple | None:
if not headers or all(k.lower() not in ['user-agent', 'cookie'] for k in headers):
headers = HEADERS
try:
proxy_addr = utils.handle_proxy_addr(proxy_addr)
async with httpx.AsyncClient(proxy=proxy_addr, timeout=15) as client:
response = await client.get(url, headers=headers, follow_redirects=True)
redirect_url = response.url
if 'reflow/' in str(redirect_url):
match = re.search(r'sec_user_id=([\w_\-]+)&', str(redirect_url))
if match:
sec_user_id = match.group(1)
room_id = str(redirect_url).split('?')[0].rsplit('/', maxsplit=1)[1]
return room_id, sec_user_id
else:
raise RuntimeError("Could not find sec_user_id in the URL.")
else:
raise UnsupportedUrlError("The redirect URL does not contain 'reflow/'.")
except UnsupportedUrlError as e:
raise e
except Exception as e:
raise RuntimeError(f"An error occurred: {e}")
# 获取抖音号
async def get_unique_id(url: str, proxy_addr: str | None = None, headers: dict | None = None) -> str | None:
if not headers or all(k.lower() not in ['user-agent', 'cookie'] for k in headers):
headers = HEADERS
try:
proxy_addr = utils.handle_proxy_addr(proxy_addr)
async with httpx.AsyncClient(proxy=proxy_addr, timeout=15) as client:
response = await client.get(url, headers=headers, follow_redirects=True)
redirect_url = str(response.url)
if 'reflow/' in str(redirect_url):
raise UnsupportedUrlError("Unsupported URL")
sec_user_id = redirect_url.split('?')[0].rsplit('/', maxsplit=1)[1]
headers['Cookie'] = ('ttwid=1%7C4ejCkU2bKY76IySQENJwvGhg1IQZrgGEupSyTKKfuyk%7C1740470403%7Cbc9a'
'd2ee341f1a162f9e27f4641778030d1ae91e31f9df6553a8f2efa3bdb7b4; __ac_nonce=06'
'83e59f3009cc48fbab0; __ac_signature=_02B4Z6wo00f01mG6waQAAIDB9JUCzFb6.TZhmsU'
'AAPBf34; __ac_referer=__ac_blank')
user_page_response = await client.get(f'https://www.iesdouyin.com/share/user/{sec_user_id}',
headers=headers, follow_redirects=True)
matches = re.findall(r'unique_id":"(.*?)","verification_type', user_page_response.text)
if matches:
unique_id = matches[-1]
return unique_id
else:
raise RuntimeError("Could not find unique_id in the response.")
except UnsupportedUrlError as e:
raise e
except Exception as e:
raise RuntimeError(f"An error occurred: {e}")
# 获取直播间webID
async def get_live_room_id(room_id: str, sec_user_id: str, proxy_addr: str | None = None, params: dict | None = None,
headers: dict | None = None) -> str:
if not headers or all(k.lower() not in ['user-agent', 'cookie'] for k in headers):
headers = HEADERS
if not params:
params = {
"verifyFp": "verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf",
"type_id": "0",
"live_id": "1",
"room_id": room_id,
"sec_user_id": sec_user_id,
"app_id": "1128",
"msToken": "wrqzbEaTlsxt52-vxyZo_mIoL0RjNi1ZdDe7gzEGMUTVh_HvmbLLkQrA_1HKVOa2C6gkxb6IiY6TY2z8enAkPEwGq--gM"
"-me3Yudck2ailla5Q4osnYIHxd9dI4WtQ==",
}
api = f'https://webcast.amemv.com/webcast/room/reflow/info/?{urllib.parse.urlencode(params)}'
xbogus = await get_xbogus(api)
api = api + "&X-Bogus=" + xbogus
try:
proxy_addr = utils.handle_proxy_addr(proxy_addr)
async with httpx.AsyncClient(proxy=proxy_addr,
timeout=15) as client:
response = await client.get(api, headers=headers)
response.raise_for_status()
json_data = response.json()
return json_data['data']['room']['owner']['web_rid']
except httpx.HTTPStatusError as e:
print(f"HTTP status error occurred: {e.response.status_code}")
raise
except Exception as e:
print(f"An exception occurred during get_live_room_id: {e}")
raise
if __name__ == '__main__':
room_url = "https://v.douyin.com/iQLgKSj/"
_room_id, sec_uid = get_sec_user_id(room_url)
web_rid = get_live_room_id(_room_id, sec_uid)
print("return web_rid:", web_rid)

3253
src/spider.py Normal file

File diff suppressed because it is too large Load Diff

446
src/stream.py Normal file
View File

@@ -0,0 +1,446 @@
# -*- encoding: utf-8 -*-
"""
Author: Hmily
GitHub: https://github.com/ihmily
Date: 2023-07-15 23:15:00
Update: 2025-02-06 02:28:00
Copyright (c) 2023-2025 by Hmily, All Rights Reserved.
Function: Get live stream data.
"""
import base64
import hashlib
import json
import time
import random
import re
from operator import itemgetter
import urllib.parse
import urllib.request
from .utils import trace_error_decorator
from .spider import (
get_douyu_stream_data, get_bilibili_stream_data
)
from .http_clients.async_http import get_response_status
QUALITY_MAPPING = {"OD": 0, "BD": 0, "UHD": 1, "HD": 2, "SD": 3, "LD": 4}
def get_quality_index(quality) -> tuple:
if not quality:
return list(QUALITY_MAPPING.items())[0]
quality_str = str(quality).upper()
if quality_str.isdigit():
quality_int = int(quality_str[0])
quality_str = list(QUALITY_MAPPING.keys())[quality_int]
return quality_str, QUALITY_MAPPING.get(quality_str, 0)
@trace_error_decorator
async def get_douyin_stream_url(json_data: dict, video_quality: str, proxy_addr: str) -> dict:
anchor_name = json_data.get('anchor_name')
result = {
"anchor_name": anchor_name,
"is_live": False,
}
status = json_data.get("status", 4)
if status == 2:
stream_url = json_data['stream_url']
flv_url_dict = stream_url['flv_pull_url']
flv_url_list: list = list(flv_url_dict.values())
m3u8_url_dict = stream_url['hls_pull_url_map']
m3u8_url_list: list = list(m3u8_url_dict.values())
while len(flv_url_list) < 5:
flv_url_list.append(flv_url_list[-1])
m3u8_url_list.append(m3u8_url_list[-1])
video_quality, quality_index = get_quality_index(video_quality)
m3u8_url = m3u8_url_list[quality_index]
flv_url = flv_url_list[quality_index]
ok = await get_response_status(url=m3u8_url, proxy_addr=proxy_addr)
if not ok:
index = quality_index + 1 if quality_index < 4 else quality_index - 1
m3u8_url = m3u8_url_list[index]
flv_url = flv_url_list[index]
result |= {
'is_live': True,
'title': json_data['title'],
'quality': video_quality,
'm3u8_url': m3u8_url,
'flv_url': flv_url,
'record_url': m3u8_url or flv_url,
}
return result
@trace_error_decorator
async def get_tiktok_stream_url(json_data: dict, video_quality: str, proxy_addr: str) -> dict:
if not json_data:
return {"anchor_name": None, "is_live": False}
def get_video_quality_url(stream, q_key) -> list:
play_list = []
for key in stream:
url_info = stream[key]['main']
sdk_params = url_info['sdk_params']
sdk_params = json.loads(sdk_params)
vbitrate = int(sdk_params['vbitrate'])
v_codec = sdk_params.get('VCodec', '')
play_url = ''
if url_info.get(q_key):
if url_info[q_key].endswith(".flv") or url_info[q_key].endswith(".m3u8"):
play_url = url_info[q_key] + '?codec=' + v_codec
else:
play_url = url_info[q_key] + '&codec=' + v_codec
resolution = sdk_params['resolution']
if vbitrate != 0 and resolution:
width, height = map(int, resolution.split('x'))
play_list.append({'url': play_url, 'vbitrate': vbitrate, 'resolution': (width, height)})
play_list.sort(key=itemgetter('vbitrate'), reverse=True)
play_list.sort(key=lambda x: (-x['vbitrate'], -x['resolution'][0], -x['resolution'][1]))
return play_list
live_room = json_data['LiveRoom']['liveRoomUserInfo']
user = live_room['user']
anchor_name = f"{user['nickname']}-{user['uniqueId']}"
status = user.get("status", 4)
result = {
"anchor_name": anchor_name,
"is_live": False,
}
if status == 2:
stream_data = live_room['liveRoom']['streamData']['pull_data']['stream_data']
stream_data = json.loads(stream_data).get('data', {})
flv_url_list = get_video_quality_url(stream_data, 'flv')
m3u8_url_list = get_video_quality_url(stream_data, 'hls')
while len(flv_url_list) < 5:
flv_url_list.append(flv_url_list[-1])
while len(m3u8_url_list) < 5:
m3u8_url_list.append(m3u8_url_list[-1])
video_quality, quality_index = get_quality_index(video_quality)
flv_dict: dict = flv_url_list[quality_index]
m3u8_dict: dict = m3u8_url_list[quality_index]
check_url = m3u8_dict.get('url') or flv_dict.get('url')
ok = await get_response_status(url=check_url, proxy_addr=proxy_addr, http2=False)
if not ok:
index = quality_index + 1 if quality_index < 4 else quality_index - 1
flv_dict: dict = flv_url_list[index]
m3u8_dict: dict = m3u8_url_list[index]
flv_url = flv_dict['url']
m3u8_url = m3u8_dict['url']
result |= {
'is_live': True,
'title': live_room['liveRoom']['title'],
'quality': video_quality,
'm3u8_url': m3u8_url,
'flv_url': flv_url,
'record_url': m3u8_url or flv_url,
}
return result
@trace_error_decorator
async def get_kuaishou_stream_url(json_data: dict, video_quality: str) -> dict:
if json_data['type'] == 1 and not json_data["is_live"]:
return json_data
live_status = json_data['is_live']
result = {
"type": 2,
"anchor_name": json_data['anchor_name'],
"is_live": live_status,
}
if live_status:
quality_mapping_bit = {'OD': 99999, 'BD': 4000, 'UHD': 2000, 'HD': 1000, 'SD': 800, 'LD': 600}
if video_quality in QUALITY_MAPPING:
quality, quality_index = get_quality_index(video_quality)
if 'm3u8_url_list' in json_data:
m3u8_url_list = json_data['m3u8_url_list'][::-1]
while len(m3u8_url_list) < 5:
m3u8_url_list.append(m3u8_url_list[-1])
m3u8_url = m3u8_url_list[quality_index]['url']
result['m3u8_url'] = m3u8_url
if 'flv_url_list' in json_data:
if 'bitrate' in json_data['flv_url_list'][0]:
flv_url_list = json_data['flv_url_list']
flv_url_list = sorted(flv_url_list, key=lambda x: x['bitrate'], reverse=True)
quality_str = str(video_quality).upper()
if quality_str.isdigit():
video_quality, quality_index_bitrate_value = list(quality_mapping_bit.items())[int(quality_str)]
else:
quality_index_bitrate_value = quality_mapping_bit.get(quality_str, 99999)
video_quality = quality_str
quality_index = next(
(i for i, x in enumerate(flv_url_list) if x['bitrate'] <= quality_index_bitrate_value), None)
if quality_index is None:
quality_index = len(flv_url_list) - 1
flv_url = flv_url_list[quality_index]['url']
result['flv_url'] = flv_url
result['record_url'] = flv_url
else:
flv_url_list = json_data['flv_url_list'][::-1]
while len(flv_url_list) < 5:
flv_url_list.append(flv_url_list[-1])
flv_url = flv_url_list[quality_index]['url']
result |= {'flv_url': flv_url, 'record_url': flv_url}
result['is_live'] = True
result['quality'] = video_quality
return result
@trace_error_decorator
async def get_huya_stream_url(json_data: dict, video_quality: str) -> dict:
game_live_info = json_data['data'][0]['gameLiveInfo']
live_title = game_live_info['introduction']
stream_info_list = json_data['data'][0]['gameStreamInfoList']
anchor_name = game_live_info.get('nick', '')
result = {
"anchor_name": anchor_name,
"is_live": False,
}
if stream_info_list:
select_cdn = stream_info_list[0]
flv_url = select_cdn.get('sFlvUrl')
stream_name = select_cdn.get('sStreamName')
flv_url_suffix = select_cdn.get('sFlvUrlSuffix')
hls_url = select_cdn.get('sHlsUrl')
hls_url_suffix = select_cdn.get('sHlsUrlSuffix')
flv_anti_code = select_cdn.get('sFlvAntiCode')
def get_anti_code(old_anti_code: str) -> str:
# js地址https://hd.huya.com/cdn_libs/mobile/hysdk-m-202402211431.js
params_t = 100
sdk_version = 2403051612
# sdk_id是13位数毫秒级时间戳
t13 = int(time.time()) * 1000
sdk_sid = t13
# 计算uuid和uid参数值
init_uuid = (int(t13 % 10 ** 10 * 1000) + int(1000 * random.random())) % 4294967295 # 直接初始化
uid = random.randint(1400000000000, 1400009999999) # 经过测试uid也可以使用init_uuid代替
seq_id = uid + sdk_sid # 移动端请求的直播流地址中包含seqId参数
# 计算ws_time参数值(16进制) 可以是当前毫秒时间戳当然也可以直接使用url_query['wsTime'][0]
# 原始最大误差不得慢240000毫秒
target_unix_time = (t13 + 110624) // 1000
ws_time = f"{target_unix_time:x}".lower()
# fm参数值是经过url编码然后base64编码得到的解码结果类似 DWq8BcJ3h6DJt6TY_$0_$1_$2_$3
# 具体细节在上面js中查看大概在32657行代码开始有base64混淆代码请自行替换
url_query = urllib.parse.parse_qs(old_anti_code)
ws_secret_pf = base64.b64decode(urllib.parse.unquote(url_query['fm'][0]).encode()).decode().split("_")[0]
ws_secret_hash = hashlib.md5(f'{seq_id}|{url_query["ctype"][0]}|{params_t}'.encode()).hexdigest()
ws_secret = f'{ws_secret_pf}_{uid}_{stream_name}_{ws_secret_hash}_{ws_time}'
ws_secret_md5 = hashlib.md5(ws_secret.encode()).hexdigest()
anti_code = (
f'wsSecret={ws_secret_md5}&wsTime={ws_time}&seqid={seq_id}&ctype={url_query["ctype"][0]}&ver=1'
f'&fs={url_query["fs"][0]}&uuid={init_uuid}&u={uid}&t={params_t}&sv={sdk_version}'
f'&sdk_sid={sdk_sid}&codec=264'
)
return anti_code
new_anti_code = get_anti_code(flv_anti_code)
flv_url = f'{flv_url}/{stream_name}.{flv_url_suffix}?{new_anti_code}&ratio='
m3u8_url = f'{hls_url}/{stream_name}.{hls_url_suffix}?{new_anti_code}&ratio='
quality_list = flv_anti_code.split('&exsphd=')
if len(quality_list) > 1 and video_quality not in ["OD", "BD"]:
pattern = r"(?<=264_)\d+"
quality_list = list(re.findall(pattern, quality_list[1]))[::-1]
while len(quality_list) < 5:
quality_list.append(quality_list[-1])
video_quality_options = {
"UHD": quality_list[0],
"HD": quality_list[1],
"SD": quality_list[2],
"LD": quality_list[3]
}
if video_quality not in video_quality_options:
raise ValueError(
f"Invalid video quality. Available options are: {', '.join(video_quality_options.keys())}")
flv_url = flv_url + str(video_quality_options[video_quality])
m3u8_url = m3u8_url + str(video_quality_options[video_quality])
result |= {
'is_live': True,
'title': live_title,
'quality': video_quality,
'm3u8_url': m3u8_url,
'flv_url': flv_url,
'record_url': flv_url or m3u8_url
}
return result
@trace_error_decorator
async def get_douyu_stream_url(json_data: dict, video_quality: str, cookies: str, proxy_addr: str) -> dict:
if not json_data["is_live"]:
return json_data
video_quality_options = {
"OD": '0',
"BD": '0',
"UHD": '3',
"HD": '2',
"SD": '1',
"LD": '1'
}
rid = str(json_data["room_id"])
json_data.pop("room_id")
rate = video_quality_options.get(video_quality, '0')
flv_data = await get_douyu_stream_data(rid, rate, cookies=cookies, proxy_addr=proxy_addr)
rtmp_url = flv_data['data'].get('rtmp_url')
rtmp_live = flv_data['data'].get('rtmp_live')
if rtmp_live:
flv_url = f'{rtmp_url}/{rtmp_live}'
json_data |= {'quality': video_quality, 'flv_url': flv_url, 'record_url': flv_url}
return json_data
@trace_error_decorator
async def get_yy_stream_url(json_data: dict) -> dict:
anchor_name = json_data.get('anchor_name', '')
result = {
"anchor_name": anchor_name,
"is_live": False,
}
if 'avp_info_res' in json_data:
stream_line_addr = json_data['avp_info_res']['stream_line_addr']
cdn_info = list(stream_line_addr.values())[0]
flv_url = cdn_info['cdn_info']['url']
result |= {
'is_live': True,
'title': json_data['title'],
'quality': 'OD',
'flv_url': flv_url,
'record_url': flv_url
}
return result
@trace_error_decorator
async def get_bilibili_stream_url(json_data: dict, video_quality: str, proxy_addr: str, cookies: str) -> dict:
anchor_name = json_data["anchor_name"]
if not json_data["live_status"]:
return {
"anchor_name": anchor_name,
"is_live": False
}
room_url = json_data['room_url']
video_quality_options = {
"OD": '10000',
"BD": '400',
"UHD": '250',
"HD": '150',
"SD": '80',
"LD": '80'
}
select_quality = video_quality_options[video_quality]
play_url = await get_bilibili_stream_data(
room_url, qn=select_quality, platform='web', proxy_addr=proxy_addr, cookies=cookies)
return {
'anchor_name': json_data['anchor_name'],
'is_live': True,
'title': json_data['title'],
'quality': video_quality,
'record_url': play_url
}
@trace_error_decorator
async def get_netease_stream_url(json_data: dict, video_quality: str) -> dict:
if not json_data['is_live']:
return json_data
m3u8_url = json_data['m3u8_url']
flv_url = None
if json_data.get('stream_list'):
stream_list = json_data['stream_list']['resolution']
order = ['blueray', 'ultra', 'high', 'standard']
sorted_keys = [key for key in order if key in stream_list]
while len(sorted_keys) < 5:
sorted_keys.append(sorted_keys[-1])
video_quality, quality_index = get_quality_index(video_quality)
selected_quality = sorted_keys[quality_index]
flv_url_list = stream_list[selected_quality]['cdn']
selected_cdn = list(flv_url_list.keys())[0]
flv_url = flv_url_list[selected_cdn]
return {
"is_live": True,
"anchor_name": json_data['anchor_name'],
"title": json_data['title'],
'quality': video_quality,
"m3u8_url": m3u8_url,
"flv_url": flv_url,
"record_url": flv_url or m3u8_url
}
async def get_stream_url(json_data: dict, video_quality: str, url_type: str = 'm3u8', spec: bool = False,
hls_extra_key: str | int = None, flv_extra_key: str | int = None) -> dict:
if not json_data['is_live']:
return json_data
play_url_list = json_data['play_url_list']
while len(play_url_list) < 5:
play_url_list.append(play_url_list[-1])
video_quality, selected_quality = get_quality_index(video_quality)
data = {
"anchor_name": json_data['anchor_name'],
"is_live": True
}
def get_url(key):
play_url = play_url_list[selected_quality]
return play_url[key] if key else play_url
if url_type == 'all':
m3u8_url = get_url(hls_extra_key)
flv_url = get_url(flv_extra_key)
data |= {
"m3u8_url": json_data['m3u8_url'] if spec else m3u8_url,
"flv_url": json_data['flv_url'] if spec else flv_url,
"record_url": m3u8_url
}
elif url_type == 'm3u8':
m3u8_url = get_url(hls_extra_key)
data |= {"m3u8_url": json_data['m3u8_url'] if spec else m3u8_url, "record_url": m3u8_url}
else:
flv_url = get_url(flv_extra_key)
data |= {"flv_url": flv_url, "record_url": flv_url}
data['title'] = json_data.get('title')
data['quality'] = video_quality
return data

206
src/utils.py Normal file
View File

@@ -0,0 +1,206 @@
# -*- coding: utf-8 -*-
import json
import os
import random
import shutil
import string
from pathlib import Path
import functools
import hashlib
import re
import traceback
from typing import Any
from urllib.parse import parse_qs, urlparse
from collections import OrderedDict
import execjs
from .logger import logger
import configparser
OptionalStr = str | None
OptionalDict = dict | None
class Color:
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
RESET = "\033[0m"
@staticmethod
def print_colored(text, color):
print(f"{color}{text}{Color.RESET}")
def trace_error_decorator(func: callable) -> callable:
@functools.wraps(func)
def wrapper(*args: list, **kwargs: dict) -> Any:
try:
return func(*args, **kwargs)
except execjs.ProgramError:
logger.warning('Failed to execute JS code. Please check if the Node.js environment')
except Exception as e:
error_line = traceback.extract_tb(e.__traceback__)[-1].lineno
error_info = f"message: type: {type(e).__name__}, {str(e)} in function {func.__name__} at line: {error_line}"
logger.error(error_info)
return []
return wrapper
def check_md5(file_path: str | Path) -> str:
with open(file_path, 'rb') as fp:
file_md5 = hashlib.md5(fp.read()).hexdigest()
return file_md5
def dict_to_cookie_str(cookies_dict: dict) -> str:
cookie_str = '; '.join([f"{key}={value}" for key, value in cookies_dict.items()])
return cookie_str
def read_config_value(file_path: str | Path, section: str, key: str) -> str | None:
config = configparser.ConfigParser()
try:
config.read(file_path, encoding='utf-8-sig')
except Exception as e:
print(f"Error occurred while reading the configuration file: {e}")
return None
if section in config:
if key in config[section]:
return config[section][key]
else:
print(f"Key [{key}] does not exist in section [{section}].")
else:
print(f"Section [{section}] does not exist in the file.")
return None
def update_config(file_path: str | Path, section: str, key: str, new_value: str) -> None:
config = configparser.ConfigParser()
try:
config.read(file_path, encoding='utf-8-sig')
except Exception as e:
print(f"An error occurred while reading the configuration file: {e}")
return
if section not in config:
print(f"Section [{section}] does not exist in the file.")
return
# 转义%字符
escaped_value = new_value.replace('%', '%%')
config[section][key] = escaped_value
try:
with open(file_path, 'w', encoding='utf-8-sig') as configfile:
config.write(configfile)
print(f"The value of {key} under [{section}] in the configuration file has been updated.")
except Exception as e:
print(f"Error occurred while writing to the configuration file: {e}")
def get_file_paths(directory: str) -> list:
file_paths = []
for root, dirs, files in os.walk(directory):
for file in files:
file_paths.append(os.path.join(root, file))
return file_paths
def remove_emojis(text: str, replace_text: str = '') -> str:
emoji_pattern = re.compile(
"["
"\U0001F1E0-\U0001F1FF" # flags (iOS)
"\U0001F300-\U0001F5FF" # symbols & pictographs
"\U0001F600-\U0001F64F" # emoticons
"\U0001F680-\U0001F6FF" # transport & map symbols
"\U0001F700-\U0001F77F" # alchemical symbols
"\U0001F780-\U0001F7FF" # Geometric Shapes Extended
"\U0001F800-\U0001F8FF" # Supplemental Arrows-C
"\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs
"\U0001FA00-\U0001FA6F" # Chess Symbols
"\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A
"\U00002702-\U000027B0" # Dingbats
"]+",
flags=re.UNICODE
)
return emoji_pattern.sub(replace_text, text)
def remove_duplicate_lines(file_path: str | Path) -> None:
unique_lines = OrderedDict()
text_encoding = 'utf-8-sig'
with open(file_path, 'r', encoding=text_encoding) as input_file:
for line in input_file:
unique_lines[line.strip()] = None
with open(file_path, 'w', encoding=text_encoding) as output_file:
for line in unique_lines:
output_file.write(line + '\n')
def check_disk_capacity(file_path: str | Path, show: bool = False) -> float:
absolute_path = os.path.abspath(file_path)
directory = os.path.dirname(absolute_path)
disk_usage = shutil.disk_usage(directory)
disk_root = Path(directory).anchor
free_space_gb = disk_usage.free / (1024 ** 3)
if show:
print(f"{disk_root} Total: {disk_usage.total / (1024 ** 3):.2f} GB "
f"Used: {disk_usage.used / (1024 ** 3):.2f} GB "
f"Free: {free_space_gb:.2f} GB\n")
return free_space_gb
def handle_proxy_addr(proxy_addr):
if proxy_addr:
if not proxy_addr.startswith('http'):
proxy_addr = 'http://' + proxy_addr
else:
proxy_addr = None
return proxy_addr
def generate_random_string(length: int) -> str:
characters = string.ascii_uppercase + string.digits
random_string = ''.join(random.choices(characters, k=length))
return random_string
def jsonp_to_json(jsonp_str: str) -> OptionalDict:
pattern = r'(\w+)\((.*)\);?$'
match = re.search(pattern, jsonp_str)
if match:
_, json_str = match.groups()
json_obj = json.loads(json_str)
return json_obj
else:
raise Exception("No JSON data found in JSONP response.")
def replace_url(file_path: str | Path, old: str, new: str) -> None:
with open(file_path, 'r', encoding='utf-8-sig') as f:
content = f.read()
if old in content:
with open(file_path, 'w', encoding='utf-8-sig') as f:
f.write(content.replace(old, new))
def get_query_params(url: str, param_name: OptionalStr) -> dict | list[str]:
parsed_url = urlparse(url)
query_params = parse_qs(parsed_url.query)
if param_name is None:
return query_params
else:
values = query_params.get(param_name, [])
return values