feat: add migu live record

This commit is contained in:
ihmily 2025-07-04 17:28:44 +08:00
parent 952eeb9b7c
commit ae8200e01c
5 changed files with 314 additions and 81 deletions

View File

@ -31,30 +31,30 @@ mp4格式重新编码为h264 = 否
[推送配置]
# 可选微信|钉钉|tg|邮箱|bark|ntfy 可填多个
直播状态推送渠道 =
钉钉推送接口链接 =
微信推送接口链接 =
bark推送接口链接 =
直播状态推送渠道 =
钉钉推送接口链接 =
微信推送接口链接 =
bark推送接口链接 =
bark推送中断级别 = active
bark推送铃声 =
钉钉通知@对象(填手机号) =
bark推送铃声 =
钉钉通知@对象(填手机号) =
钉钉通知@全体(是/否) =
tgapi令牌 =
tg聊天id(个人或者群组id) =
smtp邮件服务器 =
是否使用SMTP服务SSL加密(是/否) =
SMTP邮件服务器端口 =
邮箱登录账号 =
发件人密码(授权码) =
发件人邮箱 =
发件人显示昵称 =
收件人邮箱 =
tgapi令牌 =
tg聊天id(个人或者群组id) =
smtp邮件服务器 =
是否使用SMTP服务SSL加密(是/否) =
SMTP邮件服务器端口 =
邮箱登录账号 =
发件人密码(授权码) =
发件人邮箱 =
发件人显示昵称 =
收件人邮箱 =
ntfy推送地址 = https://ntfy.sh/xxxx
ntfy推送标签 = tada
ntfy推送邮箱 =
自定义推送标题 =
自定义开播推送内容 =
自定义关播推送内容 =
ntfy推送邮箱 =
自定义推送标题 =
自定义开播推送内容 =
自定义关播推送内容 =
只推送通知不录制(是/否) =
直播推送检测频率(秒) = 1800
开播推送开启(是/否) =
@ -63,63 +63,64 @@ ntfy推送邮箱 =
[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 =
快手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 =
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 =
[Authorization]
popkontv_token =
popkontv_token =
[账号密码]
sooplive账号 =
sooplive密码 =
flextv账号 =
flextv密码 =
popkontv账号 =
sooplive账号 =
sooplive密码 =
flextv账号 =
flextv密码 =
popkontv账号 =
partner_code = P-00001
popkontv密码 =
popkontv密码 =
twitcasting账号类型 = normal
twitcasting账号 =
twitcasting密码 =
twitcasting账号 =
twitcasting密码 =

View File

@ -190,6 +190,10 @@ LIVE_STREAM_CONFIG = {
"faceit": {
"url": "https://www.faceit.com/zh/players/Compl1/stream",
"func": spider.get_faceit_stream_data,
},
"migu": {
"url": "https://www.miguvideo.com/p/live/120000541321",
"func": spider.get_migu_stream_url,
}
}
@ -209,3 +213,4 @@ def test_live_stream(platform_name: str, proxy_addr=None, cookies=None) -> None:
if __name__ == "__main__":
platform = "douyin"
test_live_stream(platform)

21
main.py
View File

@ -4,7 +4,7 @@
Author: Hmily
GitHub: https://github.com/ihmily
Date: 2023-07-17 23:52:05
Update: 2025-06-14 12:19:00
Update: 2025-07-04 17:23:00
Copyright (c) 2023-2025 by Hmily, All Rights Reserved.
Function: Record live stream video.
"""
@ -38,9 +38,9 @@ from ffmpeg_install import (
check_ffmpeg, ffmpeg_path, current_env_path
)
version = "v4.0.3"
version = "v4.0.5"
platforms = ("\n国内站点:抖音|快手|虎牙|斗鱼|YY|B站|小红书|bigo|blued|网易CC|千度热播|猫耳FM|Look|TwitCasting|百度|微博|"
"酷狗|花椒|流星|Acfun|畅聊|映客|音播|知乎|嗨秀|VV星球|17Live|浪Live|漂漂|六间房|乐嗨|花猫|淘宝|京东"
"酷狗|花椒|流星|Acfun|畅聊|映客|音播|知乎|嗨秀|VV星球|17Live|浪Live|漂漂|六间房|乐嗨|花猫|淘宝|京东|咪咕"
"\n海外站点TikTok|SOOP|PandaTV|WinkTV|FlexTV|PopkonTV|TwitchTV|LiveMe|ShowRoom|CHZZK|Shopee|"
"Youtube|Faceit")
@ -92,7 +92,7 @@ def display_info() -> None:
time.sleep(5)
while True:
try:
sys.stdout.flush() # 强制刷新输出缓冲区
sys.stdout.flush()
time.sleep(5)
if Path(sys.executable).name != 'pythonw.exe':
os.system(clear_command)
@ -925,6 +925,12 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
else:
logger.error("错误信息: 网络异常请检查本网络是否能正常访问faceit直播平台")
elif record_url.find("www.miguvideo.com") > -1 or record_url.find("m.miguvideo.com") > -1:
platform = '咪咕直播'
with semaphore:
port_info = asyncio.run(spider.get_migu_stream_url(
url=record_url, proxy_addr=proxy_address, cookies=migu_cookie))
elif record_url.find(".m3u8") > -1 or record_url.find(".flv") > -1:
platform = '自定义录制直播'
port_info = {
@ -1765,6 +1771,7 @@ while True:
taobao_cookie = read_config_value(config, 'Cookie', 'taobao_cookie', '')
jd_cookie = read_config_value(config, 'Cookie', 'jd_cookie', '')
faceit_cookie = read_config_value(config, 'Cookie', 'faceit_cookie', '')
migu_cookie = read_config_value(config, 'Cookie', 'migu_cookie', '')
video_save_type_list = ("FLV", "MKV", "TS", "MP4", "MP3音频", "M4A音频")
if video_save_type and video_save_type.upper() in video_save_type_list:
@ -1882,7 +1889,9 @@ while True:
'e.tb.cn',
'huodong.m.taobao.com',
'3.cn',
'eco.m.jd.com'
'eco.m.jd.com',
'www.miguvideo.com',
'm.miguvideo.com'
]
overseas_platform_host = [
'www.tiktok.com',
@ -1986,4 +1995,4 @@ while True:
t2.start()
first_run = False
time.sleep(3)
time.sleep(3)

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

@ -4,14 +4,16 @@
Author: Hmily
GitHub: https://github.com/ihmily
Date: 2023-07-15 23:15:00
Update: 2025-06-14 12:19:00
Update: 2025-07-04 17:23:00
Copyright (c) 2023-2025 by Hmily, All Rights Reserved.
Function: Get live stream data.
"""
import hashlib
import random
import subprocess
import time
import uuid
from operator import itemgetter
import urllib.parse
import urllib.error
@ -3016,4 +3018,77 @@ async def get_faceit_stream_data(url: str, proxy_addr: OptionalStr = None, cooki
result['anchor_name'] = anchor_name
else:
result = {'anchor_name': anchor_name, 'is_live': False}
return result
return result
@trace_error_decorator
async def get_migu_stream_url(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict:
headers = {
'origin': 'https://www.miguvideo.com',
'referer': 'https://www.miguvideo.com/',
'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',
'appCode': 'miguvideo_default_www',
'appId': 'miguvideo',
'channel': 'H5',
}
if cookies:
headers['Cookie'] = cookies
web_id = url.split('?')[0].rsplit('/')[-1]
api = f'https://vms-sc.miguvideo.com/vms-match/v6/staticcache/basic/basic-data/{web_id}/miguvideo'
json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers)
json_data = json.loads(json_str)
room_id = json_data['body']['pId']
anchor_name = json_data['body']['title']
live_title = json_data['body'].get('title') + '-' + json_data['body'].get('detailPageTitle', '')
result = {"anchor_name": anchor_name, "is_live": False}
if not room_id:
raise RuntimeError("Room ID fetch error")
params = {
'contId': room_id,
'rateType': '3',
'clientId': str(uuid.uuid4()),
'timestamp': int(time.time() * 1000),
'flvEnable': 'true',
'xh265': 'false',
'chip': 'mgwww',
'channelId': '',
}
api = f'https://webapi.miguvideo.com/gateway/playurl/v3/play/playurl?{urllib.parse.urlencode(params)}'
json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers)
json_data = json.loads(json_str)
live_status = json_data['body']['content']['currentLive']
if live_status != '1':
return result
else:
result['title'] = live_title
source_url = json_data['body']['urlInfo']['url']
async def _get_dd_calcu(url):
try:
result = subprocess.run(
["node", f"{JS_SCRIPT_PATH}/migu.js", url],
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except execjs.ProgramError:
raise execjs.ProgramError('Failed to execute JS code. Please check if the Node.js environment')
ddCalcu = await _get_dd_calcu(source_url)
real_source_url = f'{source_url}&ddCalcu={ddCalcu}&sv=10010'
if '.m3u8' in real_source_url:
m3u8_url = await async_req(
real_source_url, proxy_addr=proxy_addr, headers=headers, redirect_url=True)
result['m3u8_url'] = m3u8_url
result['record_url'] = m3u8_url
else:
result['flv_url'] = real_source_url
result['record_url'] = real_source_url
result['is_live'] = True
return result