diff --git a/config/config.ini b/config/config.ini index c353071..29acdc2 100644 --- a/config/config.ini +++ b/config/config.ini @@ -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密码 = \ No newline at end of file diff --git a/demo.py b/demo.py index 47fd5e2..590b121 100644 --- a/demo.py +++ b/demo.py @@ -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) + \ No newline at end of file diff --git a/main.py b/main.py index 0392c34..6dbe8c6 100644 --- a/main.py +++ b/main.py @@ -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) \ No newline at end of file diff --git a/src/javascript/migu.js b/src/javascript/migu.js new file mode 100644 index 0000000..3ac86d3 --- /dev/null +++ b/src/javascript/migu.js @@ -0,0 +1,143 @@ +/** + * Function to get the ddCalcu parameter value + * @param {string} inputUrl - The original URL before encryption + * @returns {Promise} - 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); +}); \ No newline at end of file diff --git a/src/spider.py b/src/spider.py index 6eb180c..f6cb5a7 100644 --- a/src/spider.py +++ b/src/spider.py @@ -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 \ No newline at end of file + 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