's'
This commit is contained in:
454
src/ab_sign.py
Normal file
454
src/ab_sign.py
Normal file
@@ -0,0 +1,454 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
import math
|
||||
import time
|
||||
|
||||
|
||||
def rc4_encrypt(plaintext: str, key: str) -> str:
|
||||
# 初始化状态数组
|
||||
s = list(range(256))
|
||||
|
||||
# 使用密钥对状态数组进行置换
|
||||
j = 0
|
||||
for i in range(256):
|
||||
j = (j + s[i] + ord(key[i % len(key)])) % 256
|
||||
s[i], s[j] = s[j], s[i]
|
||||
|
||||
# 生成密钥流并加密
|
||||
i = j = 0
|
||||
result = []
|
||||
for char in plaintext:
|
||||
i = (i + 1) % 256
|
||||
j = (j + s[i]) % 256
|
||||
s[i], s[j] = s[j], s[i]
|
||||
t = (s[i] + s[j]) % 256
|
||||
result.append(chr(s[t] ^ ord(char)))
|
||||
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def left_rotate(x: int, n: int) -> int:
|
||||
n %= 32
|
||||
return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def get_t_j(j: int) -> int:
|
||||
if 0 <= j < 16:
|
||||
return 2043430169 # 0x79CC4519
|
||||
elif 16 <= j < 64:
|
||||
return 2055708042 # 0x7A879D8A
|
||||
else:
|
||||
raise ValueError("invalid j for constant Tj")
|
||||
|
||||
|
||||
def ff_j(j: int, x: int, y: int, z: int) -> int:
|
||||
if 0 <= j < 16:
|
||||
return (x ^ y ^ z) & 0xFFFFFFFF
|
||||
elif 16 <= j < 64:
|
||||
return ((x & y) | (x & z) | (y & z)) & 0xFFFFFFFF
|
||||
else:
|
||||
raise ValueError("invalid j for bool function FF")
|
||||
|
||||
|
||||
def gg_j(j: int, x: int, y: int, z: int) -> int:
|
||||
if 0 <= j < 16:
|
||||
return (x ^ y ^ z) & 0xFFFFFFFF
|
||||
elif 16 <= j < 64:
|
||||
return ((x & y) | (~x & z)) & 0xFFFFFFFF
|
||||
else:
|
||||
raise ValueError("invalid j for bool function GG")
|
||||
|
||||
|
||||
class SM3:
|
||||
def __init__(self):
|
||||
self.reg = []
|
||||
self.chunk = []
|
||||
self.size = 0
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
# 初始化寄存器值 - 修正为与JS版本相同的值
|
||||
self.reg = [
|
||||
1937774191, 1226093241, 388252375, 3666478592,
|
||||
2842636476, 372324522, 3817729613, 2969243214
|
||||
]
|
||||
self.chunk = []
|
||||
self.size = 0
|
||||
|
||||
def write(self, data):
|
||||
# 将输入转换为字节数组
|
||||
if isinstance(data, str):
|
||||
# 直接转换为UTF-8字节列表
|
||||
a = list(data.encode('utf-8'))
|
||||
else:
|
||||
a = data
|
||||
|
||||
self.size += len(a)
|
||||
f = 64 - len(self.chunk)
|
||||
|
||||
if len(a) < f:
|
||||
# 如果数据长度小于剩余空间,直接添加
|
||||
self.chunk.extend(a)
|
||||
else:
|
||||
# 否则分块处理
|
||||
self.chunk.extend(a[:f])
|
||||
|
||||
while len(self.chunk) >= 64:
|
||||
self._compress(self.chunk)
|
||||
if f < len(a):
|
||||
self.chunk = a[f:min(f + 64, len(a))]
|
||||
else:
|
||||
self.chunk = []
|
||||
f += 64
|
||||
|
||||
def _fill(self):
|
||||
# 计算比特长度
|
||||
bit_length = 8 * self.size
|
||||
|
||||
# 添加填充位
|
||||
padding_pos = len(self.chunk)
|
||||
self.chunk.append(0x80)
|
||||
padding_pos = (padding_pos + 1) % 64
|
||||
|
||||
# 如果剩余空间不足8字节,则填充到下一个块
|
||||
if 64 - padding_pos < 8:
|
||||
padding_pos -= 64
|
||||
|
||||
# 填充0直到剩余8字节用于存储长度
|
||||
while padding_pos < 56:
|
||||
self.chunk.append(0)
|
||||
padding_pos += 1
|
||||
|
||||
# 添加消息长度(高32位)
|
||||
high_bits = bit_length // 4294967296
|
||||
for i in range(4):
|
||||
self.chunk.append((high_bits >> (8 * (3 - i))) & 0xFF)
|
||||
|
||||
# 添加消息长度(低32位)
|
||||
for i in range(4):
|
||||
self.chunk.append((bit_length >> (8 * (3 - i))) & 0xFF)
|
||||
|
||||
def _compress(self, data):
|
||||
if len(data) < 64:
|
||||
raise ValueError("compress error: not enough data")
|
||||
else:
|
||||
# 消息扩展
|
||||
w = [0] * 132
|
||||
|
||||
# 将字节数组转换为字
|
||||
for t in range(16):
|
||||
w[t] = (data[4 * t] << 24) | (data[4 * t + 1] << 16) | (data[4 * t + 2] << 8) | data[4 * t + 3]
|
||||
w[t] &= 0xFFFFFFFF
|
||||
|
||||
# 消息扩展
|
||||
for j in range(16, 68):
|
||||
a = w[j - 16] ^ w[j - 9] ^ left_rotate(w[j - 3], 15)
|
||||
a = a ^ left_rotate(a, 15) ^ left_rotate(a, 23)
|
||||
w[j] = (a ^ left_rotate(w[j - 13], 7) ^ w[j - 6]) & 0xFFFFFFFF
|
||||
|
||||
# 计算w'
|
||||
for j in range(64):
|
||||
w[j + 68] = (w[j] ^ w[j + 4]) & 0xFFFFFFFF
|
||||
|
||||
# 压缩
|
||||
a, b, c, d, e, f, g, h = self.reg
|
||||
|
||||
for j in range(64):
|
||||
ss1 = left_rotate((left_rotate(a, 12) + e + left_rotate(get_t_j(j), j)) & 0xFFFFFFFF, 7)
|
||||
ss2 = ss1 ^ left_rotate(a, 12)
|
||||
tt1 = (ff_j(j, a, b, c) + d + ss2 + w[j + 68]) & 0xFFFFFFFF
|
||||
tt2 = (gg_j(j, e, f, g) + h + ss1 + w[j]) & 0xFFFFFFFF
|
||||
|
||||
d = c
|
||||
c = left_rotate(b, 9)
|
||||
b = a
|
||||
a = tt1
|
||||
h = g
|
||||
g = left_rotate(f, 19)
|
||||
f = e
|
||||
e = (tt2 ^ left_rotate(tt2, 9) ^ left_rotate(tt2, 17)) & 0xFFFFFFFF
|
||||
|
||||
# 更新寄存器
|
||||
self.reg[0] ^= a
|
||||
self.reg[1] ^= b
|
||||
self.reg[2] ^= c
|
||||
self.reg[3] ^= d
|
||||
self.reg[4] ^= e
|
||||
self.reg[5] ^= f
|
||||
self.reg[6] ^= g
|
||||
self.reg[7] ^= h
|
||||
|
||||
def sum(self, data=None, output_format=None):
|
||||
"""
|
||||
计算哈希值
|
||||
"""
|
||||
# 如果提供了输入,则重置并写入
|
||||
if data is not None:
|
||||
self.reset()
|
||||
self.write(data)
|
||||
|
||||
self._fill()
|
||||
|
||||
# 分块压缩
|
||||
for f in range(0, len(self.chunk), 64):
|
||||
self._compress(self.chunk[f:f + 64])
|
||||
|
||||
if output_format == 'hex':
|
||||
# 十六进制输出
|
||||
result = ''.join(f'{val:08x}' for val in self.reg)
|
||||
else:
|
||||
# 字节数组输出
|
||||
result = []
|
||||
for f in range(8):
|
||||
c = self.reg[f]
|
||||
result.append((c >> 24) & 0xFF)
|
||||
result.append((c >> 16) & 0xFF)
|
||||
result.append((c >> 8) & 0xFF)
|
||||
result.append(c & 0xFF)
|
||||
|
||||
self.reset()
|
||||
return result
|
||||
|
||||
|
||||
def result_encrypt(long_str: str, num: str | None = None) -> str:
|
||||
# 魔改base64编码表
|
||||
encoding_tables = {
|
||||
"s0": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
|
||||
"s1": "Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=",
|
||||
"s2": "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=",
|
||||
"s3": "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe",
|
||||
"s4": "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe"
|
||||
}
|
||||
|
||||
# 位移常量
|
||||
masks = [16515072, 258048, 4032, 63] # 对应 0, 1, 2 的掩码,添加63作为第四个掩码
|
||||
shifts = [18, 12, 6, 0] # 对应的位移量
|
||||
|
||||
encoding_table = encoding_tables[num]
|
||||
|
||||
result = ""
|
||||
round_num = 0
|
||||
long_int = get_long_int(round_num, long_str)
|
||||
|
||||
total_chars = math.ceil(len(long_str) / 3 * 4)
|
||||
|
||||
for i in range(total_chars):
|
||||
# 每4个字符处理一组3字节
|
||||
if i // 4 != round_num:
|
||||
round_num += 1
|
||||
long_int = get_long_int(round_num, long_str)
|
||||
|
||||
# 计算当前位置的索引
|
||||
index = i % 4
|
||||
|
||||
# 使用掩码和位移提取6位值
|
||||
char_index = (long_int & masks[index]) >> shifts[index]
|
||||
|
||||
result += encoding_table[char_index]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_long_int(round_num: int, long_str: str) -> int:
|
||||
round_num = round_num * 3
|
||||
|
||||
# 获取字符串中的字符,如果超出范围则使用0
|
||||
char1 = ord(long_str[round_num]) if round_num < len(long_str) else 0
|
||||
char2 = ord(long_str[round_num + 1]) if round_num + 1 < len(long_str) else 0
|
||||
char3 = ord(long_str[round_num + 2]) if round_num + 2 < len(long_str) else 0
|
||||
|
||||
return (char1 << 16) | (char2 << 8) | char3
|
||||
|
||||
|
||||
def gener_random(random_num: int, option: list[int]) -> list[int]:
|
||||
byte1 = random_num & 255
|
||||
byte2 = (random_num >> 8) & 255
|
||||
|
||||
return [
|
||||
(byte1 & 170) | (option[0] & 85), # 偶数位与option[0]的奇数位合并
|
||||
(byte1 & 85) | (option[0] & 170), # 奇数位与option[0]的偶数位合并
|
||||
(byte2 & 170) | (option[1] & 85), # 偶数位与option[1]的奇数位合并
|
||||
(byte2 & 85) | (option[1] & 170), # 奇数位与option[1]的偶数位合并
|
||||
]
|
||||
|
||||
|
||||
def generate_random_str() -> str:
|
||||
"""
|
||||
生成随机字符串
|
||||
|
||||
Returns:
|
||||
随机字符串
|
||||
"""
|
||||
# 使用与JS版本相同的固定随机值
|
||||
random_values = [0.123456789, 0.987654321, 0.555555555]
|
||||
|
||||
# 生成三组随机字节并合并
|
||||
random_bytes = []
|
||||
random_bytes.extend(gener_random(int(random_values[0] * 10000), [3, 45]))
|
||||
random_bytes.extend(gener_random(int(random_values[1] * 10000), [1, 0]))
|
||||
random_bytes.extend(gener_random(int(random_values[2] * 10000), [1, 5]))
|
||||
|
||||
return ''.join(chr(b) for b in random_bytes)
|
||||
|
||||
|
||||
def generate_rc4_bb_str(url_search_params: str, user_agent: str, window_env_str: str,
|
||||
suffix: str = "cus", arguments: list[int] | None = None) -> str:
|
||||
if arguments is None:
|
||||
arguments = [0, 1, 14]
|
||||
|
||||
sm3 = SM3()
|
||||
start_time = int(time.time() * 1000)
|
||||
|
||||
# 三次加密处理
|
||||
# 1: url_search_params两次sm3之的结果
|
||||
url_search_params_list = sm3.sum(sm3.sum(url_search_params + suffix))
|
||||
# 2: 对后缀两次sm3之的结果
|
||||
cus = sm3.sum(sm3.sum(suffix))
|
||||
# 3: 对ua处理之后的结果
|
||||
ua_key = chr(0) + chr(1) + chr(14) # [1/256, 1, 14]
|
||||
ua = sm3.sum(result_encrypt(
|
||||
rc4_encrypt(user_agent, ua_key),
|
||||
"s3"
|
||||
))
|
||||
|
||||
end_time = start_time + 100
|
||||
|
||||
# 构建配置对象
|
||||
b = {
|
||||
8: 3,
|
||||
10: end_time,
|
||||
15: {
|
||||
"aid": 6383,
|
||||
"pageId": 110624,
|
||||
"boe": False,
|
||||
"ddrt": 7,
|
||||
"paths": {
|
||||
"include": [{} for _ in range(7)],
|
||||
"exclude": []
|
||||
},
|
||||
"track": {
|
||||
"mode": 0,
|
||||
"delay": 300,
|
||||
"paths": []
|
||||
},
|
||||
"dump": True,
|
||||
"rpU": "hwj"
|
||||
},
|
||||
16: start_time,
|
||||
18: 44,
|
||||
19: [1, 0, 1, 5],
|
||||
}
|
||||
|
||||
def split_to_bytes(num: int) -> list[int]:
|
||||
return [
|
||||
(num >> 24) & 255,
|
||||
(num >> 16) & 255,
|
||||
(num >> 8) & 255,
|
||||
num & 255
|
||||
]
|
||||
|
||||
# 处理时间戳
|
||||
start_time_bytes = split_to_bytes(b[16])
|
||||
b[20] = start_time_bytes[0]
|
||||
b[21] = start_time_bytes[1]
|
||||
b[22] = start_time_bytes[2]
|
||||
b[23] = start_time_bytes[3]
|
||||
b[24] = int(b[16] / 256 / 256 / 256 / 256) & 255
|
||||
b[25] = int(b[16] / 256 / 256 / 256 / 256 / 256) & 255
|
||||
|
||||
# 处理Arguments参数
|
||||
arg0_bytes = split_to_bytes(arguments[0])
|
||||
b[26] = arg0_bytes[0]
|
||||
b[27] = arg0_bytes[1]
|
||||
b[28] = arg0_bytes[2]
|
||||
b[29] = arg0_bytes[3]
|
||||
|
||||
b[30] = int(arguments[1] / 256) & 255
|
||||
b[31] = (arguments[1] % 256) & 255
|
||||
|
||||
arg1_bytes = split_to_bytes(arguments[1])
|
||||
b[32] = arg1_bytes[0]
|
||||
b[33] = arg1_bytes[1]
|
||||
|
||||
arg2_bytes = split_to_bytes(arguments[2])
|
||||
b[34] = arg2_bytes[0]
|
||||
b[35] = arg2_bytes[1]
|
||||
b[36] = arg2_bytes[2]
|
||||
b[37] = arg2_bytes[3]
|
||||
|
||||
# 处理加密结果
|
||||
b[38] = url_search_params_list[21]
|
||||
b[39] = url_search_params_list[22]
|
||||
b[40] = cus[21]
|
||||
b[41] = cus[22]
|
||||
b[42] = ua[23]
|
||||
b[43] = ua[24]
|
||||
|
||||
# 处理结束时间
|
||||
end_time_bytes = split_to_bytes(b[10])
|
||||
b[44] = end_time_bytes[0]
|
||||
b[45] = end_time_bytes[1]
|
||||
b[46] = end_time_bytes[2]
|
||||
b[47] = end_time_bytes[3]
|
||||
b[48] = b[8]
|
||||
b[49] = int(b[10] / 256 / 256 / 256 / 256) & 255
|
||||
b[50] = int(b[10] / 256 / 256 / 256 / 256 / 256) & 255
|
||||
|
||||
# 处理配置项
|
||||
b[51] = b[15]['pageId']
|
||||
|
||||
page_id_bytes = split_to_bytes(b[15]['pageId'])
|
||||
b[52] = page_id_bytes[0]
|
||||
b[53] = page_id_bytes[1]
|
||||
b[54] = page_id_bytes[2]
|
||||
b[55] = page_id_bytes[3]
|
||||
|
||||
b[56] = b[15]['aid']
|
||||
b[57] = b[15]['aid'] & 255
|
||||
b[58] = (b[15]['aid'] >> 8) & 255
|
||||
b[59] = (b[15]['aid'] >> 16) & 255
|
||||
b[60] = (b[15]['aid'] >> 24) & 255
|
||||
|
||||
# 处理环境信息
|
||||
window_env_list = [ord(char) for char in window_env_str]
|
||||
b[64] = len(window_env_list)
|
||||
b[65] = b[64] & 255
|
||||
b[66] = (b[64] >> 8) & 255
|
||||
|
||||
b[69] = 0
|
||||
b[70] = 0
|
||||
b[71] = 0
|
||||
|
||||
# 计算校验和
|
||||
b[72] = b[18] ^ b[20] ^ b[26] ^ b[30] ^ b[38] ^ b[40] ^ b[42] ^ b[21] ^ b[27] ^ b[31] ^ \
|
||||
b[35] ^ b[39] ^ b[41] ^ b[43] ^ b[22] ^ b[28] ^ b[32] ^ b[36] ^ b[23] ^ b[29] ^ \
|
||||
b[33] ^ b[37] ^ b[44] ^ b[45] ^ b[46] ^ b[47] ^ b[48] ^ b[49] ^ b[50] ^ b[24] ^ \
|
||||
b[25] ^ b[52] ^ b[53] ^ b[54] ^ b[55] ^ b[57] ^ b[58] ^ b[59] ^ b[60] ^ b[65] ^ \
|
||||
b[66] ^ b[70] ^ b[71]
|
||||
|
||||
# 构建最终字节数组
|
||||
bb = [
|
||||
b[18], b[20], b[52], b[26], b[30], b[34], b[58], b[38], b[40], b[53], b[42], b[21],
|
||||
b[27], b[54], b[55], b[31], b[35], b[57], b[39], b[41], b[43], b[22], b[28], b[32],
|
||||
b[60], b[36], b[23], b[29], b[33], b[37], b[44], b[45], b[59], b[46], b[47], b[48],
|
||||
b[49], b[50], b[24], b[25], b[65], b[66], b[70], b[71]
|
||||
]
|
||||
bb.extend(window_env_list)
|
||||
bb.append(b[72])
|
||||
|
||||
return rc4_encrypt(
|
||||
''.join(chr(byte) for byte in bb),
|
||||
chr(121)
|
||||
)
|
||||
|
||||
|
||||
def ab_sign(url_search_params: str, user_agent: str) -> str:
|
||||
window_env_str = "1920|1080|1920|1040|0|30|0|0|1872|92|1920|1040|1857|92|1|24|Win32"
|
||||
|
||||
# 1. 生成随机字符串前缀
|
||||
# 2. 生成RC4加密的主体部分
|
||||
# 3. 对结果进行最终加密并添加等号后缀
|
||||
return result_encrypt(
|
||||
generate_random_str() +
|
||||
generate_rc4_bb_str(url_search_params, user_agent, window_env_str),
|
||||
"s4"
|
||||
) + "="
|
||||
210
src/spider.py
210
src/spider.py
@@ -4,7 +4,7 @@
|
||||
Author: Hmily
|
||||
GitHub: https://github.com/ihmily
|
||||
Date: 2023-07-15 23:15:00
|
||||
Update: 2025-07-19 17:43:00
|
||||
Update: 2025-10-23 18:28:00
|
||||
Copyright (c) 2023-2025 by Hmily, All Rights Reserved.
|
||||
Function: Get live stream data.
|
||||
"""
|
||||
@@ -29,6 +29,7 @@ from .utils import trace_error_decorator, generate_random_string
|
||||
from .logger import script_path
|
||||
from .room import get_sec_user_id, get_unique_id, UnsupportedUrlError
|
||||
from .http_clients.async_http import async_req
|
||||
from .ab_sign import ab_sign
|
||||
|
||||
|
||||
ssl_context = ssl.create_default_context()
|
||||
@@ -64,10 +65,87 @@ async def get_play_url_list(m3u8: str, proxy: OptionalStr = None, header: Option
|
||||
return play_url_list
|
||||
|
||||
|
||||
async def get_douyin_web_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None):
|
||||
headers = {
|
||||
'cookie': 'ttwid=1%7C2iDIYVmjzMcpZ20fcaFde0VghXAA3NaNXE_SLR68IyE%7C1761045455'
|
||||
'%7Cab35197d5cfb21df6cbb2fa7ef1c9262206b062c315b9d04da746d0b37dfbc7d',
|
||||
'referer': 'https://live.douyin.com/335354047186',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/116.0.5845.97 Safari/537.36 Core/1.116.567.400 QQBrowser/19.7.6764.400',
|
||||
}
|
||||
if cookies:
|
||||
headers['cookie'] = cookies
|
||||
|
||||
try:
|
||||
web_rid = url.split('?')[0].split('live.douyin.com/')[-1]
|
||||
params = {
|
||||
"aid": "6383",
|
||||
"app_name": "douyin_web",
|
||||
"live_id": "1",
|
||||
"device_platform": "web",
|
||||
"language": "zh-CN",
|
||||
"browser_language": "zh-CN",
|
||||
"browser_platform": "Win32",
|
||||
"browser_name": "Chrome",
|
||||
"browser_version": "116.0.0.0",
|
||||
"web_rid": web_rid,
|
||||
'msToken': '',
|
||||
}
|
||||
|
||||
api = f'https://live.douyin.com/webcast/room/web/enter/?{urllib.parse.urlencode(params)}'
|
||||
a_bogus = ab_sign(urllib.parse.urlparse(api).query, headers['user-agent'])
|
||||
api += "&a_bogus=" + a_bogus
|
||||
try:
|
||||
json_str = await async_req(url=api, proxy_addr=proxy_addr, headers=headers)
|
||||
if not json_str:
|
||||
raise Exception("it triggered risk control")
|
||||
json_data = json.loads(json_str)['data']
|
||||
if not json_data['data']:
|
||||
raise Exception(f"{url} VR live is not supported")
|
||||
room_data = json_data['data'][0]
|
||||
room_data['anchor_name'] = json_data['user']['nickname']
|
||||
except Exception as e:
|
||||
raise Exception(f"Douyin web data fetch error, because {e}.")
|
||||
|
||||
if room_data['status'] == 2:
|
||||
if 'stream_url' not in room_data:
|
||||
raise RuntimeError(
|
||||
"The live streaming type or gameplay is not supported on the computer side yet, please use the "
|
||||
"app to share the link for recording."
|
||||
)
|
||||
live_core_sdk_data = room_data['stream_url']['live_core_sdk_data']
|
||||
pull_datas = room_data['stream_url']['pull_datas']
|
||||
if live_core_sdk_data:
|
||||
if pull_datas:
|
||||
key = list(pull_datas.keys())[0]
|
||||
json_str = pull_datas[key]['stream_data']
|
||||
else:
|
||||
json_str = live_core_sdk_data['pull_data']['stream_data']
|
||||
json_data = json.loads(json_str)
|
||||
if 'origin' in json_data['data']:
|
||||
stream_data = live_core_sdk_data['pull_data']['stream_data']
|
||||
origin_data = json.loads(stream_data)['data']['origin']['main']
|
||||
sdk_params = json.loads(origin_data['sdk_params'])
|
||||
origin_hls_codec = sdk_params.get('VCodec') or ''
|
||||
|
||||
origin_url_list = json_data['data']['origin']['main']
|
||||
origin_m3u8 = {'ORIGIN': origin_url_list["hls"] + '&codec=' + origin_hls_codec}
|
||||
origin_flv = {'ORIGIN': origin_url_list["flv"] + '&codec=' + origin_hls_codec}
|
||||
hls_pull_url_map = room_data['stream_url']['hls_pull_url_map']
|
||||
flv_pull_url = room_data['stream_url']['flv_pull_url']
|
||||
room_data['stream_url']['hls_pull_url_map'] = {**origin_m3u8, **hls_pull_url_map}
|
||||
room_data['stream_url']['flv_pull_url'] = {**origin_flv, **flv_pull_url}
|
||||
except Exception as e:
|
||||
print(f"Error message: {e} Error line: {e.__traceback__.tb_lineno}")
|
||||
room_data = {'anchor_name': ""}
|
||||
return room_data
|
||||
|
||||
|
||||
@trace_error_decorator
|
||||
async def get_douyin_app_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0',
|
||||
'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',
|
||||
'Referer': 'https://live.douyin.com/',
|
||||
'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'
|
||||
@@ -77,7 +155,7 @@ async def get_douyin_app_stream_data(url: str, proxy_addr: OptionalStr = None, c
|
||||
|
||||
async def get_app_data(room_id: str, sec_uid: str) -> dict:
|
||||
app_params = {
|
||||
"verifyFp": "verify_lxj5zv70_7szNlAB7_pxNY_48Vh_ALKF_GA1Uf3yteoOY",
|
||||
"verifyFp": "verify_hwj52020_7szNlAB7_pxNY_48Vh_ALKF_GA1Uf3yteoOY",
|
||||
"type_id": "0",
|
||||
"live_id": "1",
|
||||
"room_id": room_id,
|
||||
@@ -86,36 +164,25 @@ async def get_douyin_app_stream_data(url: str, proxy_addr: OptionalStr = None, c
|
||||
"app_id": "1128"
|
||||
}
|
||||
api2 = f'https://webcast.amemv.com/webcast/room/reflow/info/?{urllib.parse.urlencode(app_params)}'
|
||||
json_str2 = await async_req(url=api2, proxy_addr=proxy_addr, headers=headers)
|
||||
json_data2 = json.loads(json_str2)['data']
|
||||
room_data2 = json_data2['room']
|
||||
room_data2['anchor_name'] = room_data2['owner']['nickname']
|
||||
return room_data2
|
||||
a_bogus = ab_sign(urllib.parse.urlparse(api2).query, headers['User-Agent'])
|
||||
api2 += "&a_bogus=" + a_bogus
|
||||
try:
|
||||
json_str2 = await async_req(url=api2, proxy_addr=proxy_addr, headers=headers)
|
||||
if not json_str2:
|
||||
raise Exception("it triggered risk control")
|
||||
json_data2 = json.loads(json_str2)['data']
|
||||
if not json_data2.get('room'):
|
||||
raise Exception(f"{url} VR live is not supported")
|
||||
room_data2 = json_data2['room']
|
||||
room_data2['anchor_name'] = room_data2['owner']['nickname']
|
||||
return room_data2
|
||||
except Exception as e:
|
||||
raise Exception(f"Douyin app data fetch error, because {e}.")
|
||||
|
||||
try:
|
||||
web_rid = url.split('?')[0].split('live.douyin.com/')
|
||||
if len(web_rid) > 1:
|
||||
web_rid = web_rid[1]
|
||||
params = {
|
||||
"aid": "6383",
|
||||
"app_name": "douyin_web",
|
||||
"live_id": "1",
|
||||
"device_platform": "web",
|
||||
"language": "zh-CN",
|
||||
"browser_language": "zh-CN",
|
||||
"browser_platform": "Win32",
|
||||
"browser_name": "Chrome",
|
||||
"browser_version": "116.0.0.0",
|
||||
"web_rid": web_rid,
|
||||
'msToken': '',
|
||||
'a_bogus': ''
|
||||
|
||||
}
|
||||
api = f'https://live.douyin.com/webcast/room/web/enter/?{urllib.parse.urlencode(params)}'
|
||||
json_str = await async_req(url=api, proxy_addr=proxy_addr, headers=headers)
|
||||
json_data = json.loads(json_str)['data']
|
||||
room_data = json_data['data'][0]
|
||||
room_data['anchor_name'] = json_data['user']['nickname']
|
||||
return await get_douyin_web_stream_data(url, proxy_addr, cookies)
|
||||
else:
|
||||
try:
|
||||
data = await get_sec_user_id(url, proxy_addr=proxy_addr)
|
||||
@@ -218,18 +285,20 @@ async def get_douyin_stream_data(url: str, proxy_addr: OptionalStr = None, cooki
|
||||
@trace_error_decorator
|
||||
async def get_tiktok_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict | None:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
|
||||
'Cookie': 'ttwid=1%7CM-rF193sJugKuNz2RGNt-rh6pAAR9IMceUSzlDnPCNI%7C1683274418%7Cf726d4947f2fc37fecc7aeb0cdaee52892244d04efde6f8a8edd2bb168263269; tiktok_webapp_theme=light; tt_chain_token=VWkygAWDlm1cFg/k8whmOg==; passport_csrf_token=6e422c5a7991f8cec7033a8082921510; passport_csrf_token_default=6e422c5a7991f8cec7033a8082921510; d_ticket=f8c267d4af4523c97be1ccb355e9991e2ae06; odin_tt=320b5f386cdc23f347be018e588873db7f7aea4ea5d1813681c3fbc018ea025dde957b94f74146dbc0e3612426b865ccb95ec8abe4ee36cca65f15dbffec0deff7b0e69e8ea536d46e0f82a4fc37d211; cmpl_token=AgQQAPNSF-RO0rT04baWtZ0T_jUjl4fVP4PZYM2QPw; uid_tt=319b558dbba684bb1557206c92089cd113a875526a89aee30595925d804b81c7; uid_tt_ss=319b558dbba684bb1557206c92089cd113a875526a89aee30595925d804b81c7; sid_tt=ad5e736f4bedb2f6d42ccd849e706b1d; sessionid=ad5e736f4bedb2f6d42ccd849e706b1d; sessionid_ss=ad5e736f4bedb2f6d42ccd849e706b1d; store-idc=useast5; store-country-code=us; store-country-code-src=uid; tt-target-idc=useast5; tt-target-idc-sign=qXNk0bb1pDQ0FbCNF120Pl9WWMLZg9Edv5PkfyCbS4lIk5ieW5tfLP7XWROnN0mEaSlc5hg6Oji1pF-yz_3ZXnUiNMrA9wNMPvI6D9IFKKVmq555aQzwPIGHv0aQC5dNRgKo5Z5LBkgxUMWEojTKclq2_L8lBciw0IGdhFm_XyVJtbqbBKKgybGDLzK8ZyxF4Jl_cYRXaDlshZjc38JdS6wruDueRSHe7YvNbjxCnApEFUv-OwJANSPU_4rvcqpVhq3JI2VCCfw-cs_4MFIPCDOKisk5EhAo2JlHh3VF7_CLuv80FXg_7ZqQ2pJeMOog294rqxwbbQhl3ATvjQV_JsWyUsMd9zwqecpylrPvtySI2u1qfoggx1owLrrUynee1R48QlanLQnTNW_z1WpmZBgVJqgEGLwFoVOmRzJuFFNj8vIqdjM2nDSdWqX8_wX3wplohkzkPSFPfZgjzGnQX28krhgTytLt7BXYty5dpfGtsdb11WOFHM6MZ9R9uLVB; sid_guard=ad5e736f4bedb2f6d42ccd849e706b1d%7C1690990657%7C15525213%7CMon%2C+29-Jan-2024+08%3A11%3A10+GMT; sid_ucp_v1=1.0.0-KGM3YzgwYjZhODgyYWI1NjIwNTA0NjBmOWUxMGRhMjIzYTI2YjMxNDUKGAiqiJ30keKD5WQQwfCppgYYsws4AkDsBxAEGgd1c2Vhc3Q1IiBhZDVlNzM2ZjRiZWRiMmY2ZDQyY2NkODQ5ZTcwNmIxZA; ssid_ucp_v1=1.0.0-KGM3YzgwYjZhODgyYWI1NjIwNTA0NjBmOWUxMGRhMjIzYTI2YjMxNDUKGAiqiJ30keKD5WQQwfCppgYYsws4AkDsBxAEGgd1c2Vhc3Q1IiBhZDVlNzM2ZjRiZWRiMmY2ZDQyY2NkODQ5ZTcwNmIxZA; tt_csrf_token=dD0EIH8q-pe3qDQsCyyD1jLN6KizJDRjOEyk; __tea_cache_tokens_1988={%22_type_%22:%22default%22%2C%22user_unique_id%22:%227229608516049831425%22%2C%22timestamp%22:1683274422659}; ttwid=1%7CM-rF193sJugKuNz2RGNt-rh6pAAR9IMceUSzlDnPCNI%7C1694002151%7Cd89b77afc809b1a610661a9d1c2784d80ebef9efdd166f06de0d28e27f7e4efe; msToken=KfJAVZ7r9D_QVeQlYAUZzDFbc1Yx-nZz6GF33eOxgd8KlqvTg1lF9bMXW7gFV-qW4MCgUwnBIhbiwU9kdaSpgHJCk-PABsHCtTO5J3qC4oCTsrXQ1_E0XtbqiE4OVLZ_jdF1EYWgKNPT2SnwGkQ=; msToken=KfJAVZ7r9D_QVeQlYAUZzDFbc1Yx-nZz6GF33eOxgd8KlqvTg1lF9bMXW7gFV-qW4MCgUwnBIhbiwU9kdaSpgHJCk-PABsHCtTO5J3qC4oCTsrXQ1_E0XtbqiE4OVLZ_jdF1EYWgKNPT2SnwGkQ='
|
||||
'referer': 'https://www.tiktok.com/',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/141.0.0.0 Safari/537.36',
|
||||
'cookie': cookies or '1%7Cz7FKki38aKyy7i-BC9rEDwcrVvjcLcFEL6QIeqldoy4%7C1761302831%7C6c1461e9f1f980cbe0404c5190'
|
||||
'5177d5d53bbd822e1bf66128887d942c9c3e2f'
|
||||
}
|
||||
if cookies:
|
||||
headers['Cookie'] = cookies
|
||||
|
||||
for i in range(3):
|
||||
html_str = await async_req(url=url, proxy_addr=proxy_addr, headers=headers, abroad=True, http2=False)
|
||||
time.sleep(1)
|
||||
if "We regret to inform you that we have discontinued operating TikTok" in html_str:
|
||||
msg = re.search('<p>\n\\s+(We regret to inform you that we have discontinu.*?)\\.\n\\s+</p>', html_str)
|
||||
raise ConnectionError(
|
||||
f"Your proxy node's regional network is blocked from accessing TikTok; please switch to a node in "
|
||||
"Your proxy node's regional network is blocked from accessing TikTok; please switch to a node in "
|
||||
f"another region to access. {msg.group(1) if msg else ''}"
|
||||
)
|
||||
if 'UNEXPECTED_EOF_WHILE_READING' not in html_str:
|
||||
@@ -935,6 +1004,76 @@ async def get_sooplive_tk(url: str, rtype: str, proxy_addr: OptionalStr = None,
|
||||
return f"{bj_name}-{bj_id}", json_data['CHANNEL']['BNO']
|
||||
|
||||
|
||||
def get_soop_headers(cookies):
|
||||
headers = {
|
||||
'client-id': str(uuid.uuid4()),
|
||||
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, '
|
||||
'like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/141.0.0.0',
|
||||
}
|
||||
if cookies:
|
||||
headers['cookie'] = cookies
|
||||
return headers
|
||||
|
||||
|
||||
async def _get_soop_channel_info_global(bj_id, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> str:
|
||||
headers = get_soop_headers(cookies)
|
||||
api = 'https://api.sooplive.com/v2/channel/info/' + str(bj_id)
|
||||
json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers)
|
||||
json_data = json.loads(json_str)
|
||||
nickname = json_data['data']['streamerChannelInfo']['nickname']
|
||||
channelId = json_data['data']['streamerChannelInfo']['channelId']
|
||||
anchor_name = f"{nickname}-{channelId}"
|
||||
return anchor_name
|
||||
|
||||
|
||||
async def _get_soop_stream_info_global(bj_id, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> tuple:
|
||||
headers = get_soop_headers(cookies)
|
||||
api = 'https://api.sooplive.com/v2/stream/info/' + str(bj_id)
|
||||
json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers)
|
||||
json_data = json.loads(json_str)
|
||||
status = json_data['data']['isStream']
|
||||
title = json_data['data']['title']
|
||||
return status, title
|
||||
|
||||
|
||||
async def _fetch_web_stream_data_global(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict:
|
||||
split_url = url.split('/')
|
||||
bj_id = split_url[3] if len(split_url) < 6 else split_url[5]
|
||||
anchor_name = await _get_soop_channel_info_global(bj_id)
|
||||
result = {"anchor_name": anchor_name or '', "is_live": False, "live_url": url}
|
||||
status, title = await _get_soop_stream_info_global(bj_id)
|
||||
if not status:
|
||||
return result
|
||||
else:
|
||||
async def _get_url_list(m3u8: str) -> list[str]:
|
||||
headers = {
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0',
|
||||
}
|
||||
if cookies:
|
||||
headers['cookie'] = cookies
|
||||
resp = await async_req(url=m3u8, proxy_addr=proxy_addr, headers=headers)
|
||||
play_url_list = []
|
||||
url_prefix = '/'.join(m3u8.split('/')[0:3])
|
||||
for i in resp.split('\n'):
|
||||
if not i.startswith('#') and i.strip():
|
||||
play_url_list.append(url_prefix + i.strip())
|
||||
bandwidth_pattern = re.compile(r'BANDWIDTH=(\d+)')
|
||||
bandwidth_list = bandwidth_pattern.findall(resp)
|
||||
url_to_bandwidth = {purl: int(bandwidth) for bandwidth, purl in zip(bandwidth_list, play_url_list)}
|
||||
play_url_list = sorted(play_url_list, key=lambda purl: url_to_bandwidth[purl], reverse=True)
|
||||
return play_url_list
|
||||
|
||||
m3u8_url = 'https://global-media.sooplive.com/live/' + str(bj_id) + '/master.m3u8'
|
||||
result |= {
|
||||
'is_live': True,
|
||||
'title': title,
|
||||
'm3u8_url': m3u8_url,
|
||||
'play_url_list': await _get_url_list(m3u8_url)
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@trace_error_decorator
|
||||
async def get_sooplive_stream_data(
|
||||
url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None,
|
||||
@@ -949,6 +1088,9 @@ async def get_sooplive_stream_data(
|
||||
if cookies:
|
||||
headers['Cookie'] = cookies
|
||||
|
||||
if "sooplive.com" in url:
|
||||
return await _fetch_web_stream_data_global(url, proxy_addr, cookies)
|
||||
|
||||
split_url = url.split('/')
|
||||
bj_id = split_url[3] if len(split_url) < 6 else split_url[5]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user