refactor: plugins.Huya (#1238)

* refactor: plugins.Huya

* chore: standardize header keys

* cleanup

* add huya_codec

* 兼容 #1200

* Update __init__.py

---------

Co-authored-by: ForgQi <34411314+ForgQi@users.noreply.github.com>
This commit is contained in:
XuanRan_NyanandForgQi authored and GitHub committed 2025-04-14 06:36:05 +00:00
1 parent aa4a4e3b68
commit 045e0230eb
10 files changed
+316 -207

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@ import logging
import platform
import sys
__version__ = "0.4.92"
__version__ = "0.4.93"
LOG_CONF = {
'version': 1,
+6 -2
View File
@@ -16,8 +16,12 @@ except ImportError:
else:
_ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
# This setup works very well on my Swedish machine, but who knows about others...
DEFAULT_TIMEOUT = httpx.Timeout(timeout=15.0, connect=10.0)
DEFAULT_TIMEOUT = httpx.Timeout(
connect=15.0,
read=60.0,
write=60.0,
pool=15.0,
)
DEFAULT_MAX_RETRIES = 2
DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=100)
+26 -32
View File
@@ -60,10 +60,10 @@ class DownloadBase(ABC):
self.opt_args = opt_args
self.live_cover_url = None
self.fake_headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Encoding': DEFAULT_ACCEPT_ENCODING,
'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
'User-Agent': random_user_agent(),
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'accept-encoding': DEFAULT_ACCEPT_ENCODING,
'accept-language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
'user-agent': random_user_agent(),
}
self.segment_time = config.get('segment_time', '01:00:00')
self.time_range = config.get('time_range')
@@ -82,7 +82,8 @@ class DownloadBase(ABC):
# 弹幕客户端
self.danmaku: Optional[IDanmakuClient] = None
self.plugin_msg = f"[{self.__class__.__name__}]{self.fname} - {url}"
self.platform = self.__class__.__name__
self.plugin_msg = f"[{self.platform}]{self.fname} - {self.url}"
@abstractmethod
async def acheck_stream(self, is_check=False):
@@ -119,27 +120,26 @@ class DownloadBase(ABC):
if not shutil.which("ffmpeg"):
logger.error("未安装 FFMpeg 或不存在于 PATH 内,本次下载使用 stream-gears")
logger.debug("Current user's PATH is:" + os.getenv("PATH"))
# 同步下载上传器
if self.downloader == 'sync-downloader':
logger.info(f"{self.plugin_msg}: 使用同步下载器")
stream_info = config.get('streamers', {}).get(self.fname, {})
stream_info.update({'name': self.fname})
if not self.file_size:
self.file_size = 2 * 1024 * 1024 * 1024 + (2 * 1024 * 1024)
else:
else:
# 同步下载上传器
if self.downloader == 'sync-downloader':
logger.info(f"{self.plugin_msg}: 使用同步下载器")
stream_info = config.get('streamers', {}).get(self.fname, {})
stream_info.update({'name': self.fname})
min_size = 10 * 1024 * 1024
if not self.file_size:
self.file_size = 2 * 1024 * 1024 * 1024
self.file_size = ((self.file_size + min_size - 1) // min_size) * min_size # 向上取整
sync_download(self.raw_stream_url, self.fake_headers,
max_file_size=int(self.file_size / 1024 / 1024),
output_prefix=self.gen_download_filename(True),
stream_info=stream_info,
file_name_callback=lambda file_name: self.__download_segment_callback(file_name), database_row_id=self.database_row_id)
return True
# streamlink无法处理flv,所以回退到ffmpeg
if self.downloader == 'streamlink' and '.flv' not in parsed_url_path:
return self.ffmpeg_download(use_streamlink=True)
return self.ffmpeg_download()
sync_download(self.raw_stream_url, self.fake_headers,
max_file_size=int(self.file_size / 1024 / 1024),
output_prefix=self.gen_download_filename(True),
stream_info=stream_info,
file_name_callback=lambda file_name: self.__download_segment_callback(file_name), database_row_id=self.database_row_id)
return True
# streamlink无法处理flv,所以回退到ffmpeg
if self.downloader == 'streamlink' and '.flv' not in parsed_url_path:
return self.ffmpeg_download(use_streamlink=True)
return self.ffmpeg_download()
if '.flv' in parsed_url_path:
# 假定flv流
@@ -262,9 +262,6 @@ class DownloadBase(ABC):
f'{fmt_file_name}.{self.suffix}.part']
with subprocess.Popen(args, stdin=subprocess.DEVNULL if not streamlink_proc else streamlink_proc.stdout,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as proc:
# with SessionLocal() as db:
# update_file_list(db, self.database_row_id, fmt_file_name)
# updatedFileList = True
for line in iter(proc.stdout.readline, b''): # b'\n'-separated lines
decode_line = line.rstrip().decode(errors='ignore')
print(decode_line)
@@ -278,10 +275,6 @@ class DownloadBase(ABC):
return True
else:
return False
# except:
# if updatedFileList:
# with SessionLocal() as db:
# delete_file_list(db, self.database_row_id, None)
finally:
try:
if streamlink_proc:
@@ -398,6 +391,7 @@ class DownloadBase(ABC):
'end_time': end_time if end_time else time.localtime(),
'live_cover_path': self.live_cover_path,
'is_download': self.is_download,
'platform': self.platform,
}
return stream_info
@@ -596,7 +590,7 @@ def get_valid_filename(name):
'{self.fname}%Y-%m-%dT%H_%M_%S'
"""
# s = str(name).strip().replace(" ", "_") #因为有些人会在主播名中间加入空格,为了避免和录播完毕自动改名冲突,所以注释掉
s = re.sub(r"(?u)[^-\w.%{}\[\]【】「」()・°\s]", "", str(name))
s = re.sub(r"(?u)[^-\w.%{}\[\]【】「」()・°、。\s]", "", str(name))
if s in {"", ".", ".."}:
raise RuntimeError("Could not derive file name from '%s'" % name)
return s
+20
View File
@@ -2,7 +2,17 @@ import logging
import re
import hashlib
import time
import json
from urllib.parse import urlencode, quote
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Tuple,
Union,
)
logger = logging.getLogger('biliup')
@@ -36,6 +46,16 @@ def random_user_agent(device: str = 'desktop') -> str:
return f'Mozilla/5.0 (Linux; Android {android_version}; {mobile}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{chrome_version}.0.0.0 Mobile Safari/537.36'
return f'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{chrome_version}.0.0.0 Safari/537.36'
def json_loads(text: Union[str, None]) -> Dict[str, Any]:
if not text:
raise ValueError("Invalid JSON: None")
try:
return json.loads(text)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON: {text}") from e
class Wbi:
WTS = "wts"
W_RID = "w_rid"
+4 -1
View File
@@ -336,7 +336,10 @@ class Bililive(DownloadBase):
)
nav_res.raise_for_status()
nav_res = json.loads(nav_res.text)
if nav_res['code'] == 0:
if (
nav_res['code'] == 0 or
(nav_res['code'] == -101 and nav_res['message'] == '账号未登录')
):
return nav_res['data']
logger.error(f"{self.plugin_msg}: 获取 nav 失败-{nav_res}")
except:
+1 -3
View File
@@ -44,7 +44,7 @@ class BiliWeb(UploadBase):
self.user_cookie = user_cookie
self.copyright_source = copyright_source
self.extra_fields = extra_fields
def upload(self, file_list: List[UploadBase.FileInfo]) -> List[UploadBase.FileInfo]:
if self.credits:
desc_v2 = self.creditsToDesc_v2()
@@ -123,8 +123,6 @@ def stream_gears_upload(ex_conn, lines, *args, **kwargs):
kwargs['line'] = stream_gears.UploadLine.Bda
elif lines == 'bda2':
kwargs['line'] = stream_gears.UploadLine.Bda2
elif lines == 'ws':
kwargs['line'] = stream_gears.UploadLine.Ws
elif lines == 'qn':
kwargs['line'] = stream_gears.UploadLine.Qn
elif lines == 'tx':
+6 -6
View File
@@ -18,9 +18,6 @@ class Douyin(DownloadBase):
def __init__(self, fname, url, suffix='flv'):
super().__init__(fname, url, suffix)
self.douyin_danmaku = config.get('douyin_danmaku', False)
self.fake_headers['User-Agent'] = DouyinUtils.DOUYIN_USER_AGENT
self.fake_headers['Referer'] = "https://live.douyin.com/"
self.fake_headers['Cookie'] = config.get('user', {}).get('douyin_cookie', '')
self.douyin_quality = config.get('douyin_quality', 'origin')
self.douyin_protocol = config.get('douyin_protocol', 'flv')
self.douyin_double_screen = config.get('douyin_double_screen', False)
@@ -30,8 +27,11 @@ class Douyin(DownloadBase):
async def acheck_stream(self, is_check=False):
if "ttwid" not in self.fake_headers['Cookie']:
self.fake_headers['Cookie'] = f'ttwid={DouyinUtils.get_ttwid()};{self.fake_headers["Cookie"]}'
self.fake_headers['user-agent'] = DouyinUtils.DOUYIN_USER_AGENT
self.fake_headers['referer'] = "https://live.douyin.com/"
self.fake_headers['cookie'] = config.get('user', {}).get('douyin_cookie', '')
if "ttwid" not in self.fake_headers['cookie']:
self.fake_headers['cookie'] = f'ttwid={DouyinUtils.get_ttwid()};{self.fake_headers["cookie"]}'
if "v.douyin" in self.url:
try:
@@ -210,7 +210,7 @@ class DouyinUtils:
# DOUYIN_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36'
DOUYIN_USER_AGENT = random_user_agent()
DOUYIN_HTTP_HEADERS = {
'User-Agent': DOUYIN_USER_AGENT
'user-agent': DOUYIN_USER_AGENT
}
@staticmethod
+240 -151
View File
@@ -4,25 +4,30 @@ import json
import random
import time
from urllib.parse import parse_qs, unquote
from functools import lru_cache
from async_lru import alru_cache
from typing import (
Any,
Dict,
List,
Union,
)
from biliup.common.util import client
from biliup.config import config
from biliup.Danmaku import DanmakuClient
from ..common.util import client
from ..config import config
from ..Danmaku import DanmakuClient
from ..engine.decorators import Plugin
from ..engine.download import DownloadBase
from ..plugins import logger, match1, random_user_agent
from . import logger, match1, json_loads
HUYA_WEB_BASE_URL = "https://www.huya.com"
HUYA_MOBILE_BASE_URL = "https://m.huya.com"
HUYA_MP_BASE_URL = "https://mp.huya.com"
HUYA_WEB_ROOM_DATA_REGEX = r"var TT_ROOM_DATA = (.*?);"
@Plugin.download(regexp=r'https?://(?:(?:www|m)\.)?huya\.com')
class Huya(DownloadBase):
def __init__(self, fname, url, suffix='flv'):
super().__init__(fname, url, suffix)
self.fake_headers['referer'] = url
# self.fake_headers['cookie'] = config.get('user', {}).get('huya_cookie', '')
self.__room_id = url.split('huya.com/')[1].split('?')[0]
self.huya_danmaku = config.get('huya_danmaku', False)
self.huya_max_ratio = config.get('huya_max_ratio', 0)
@@ -31,32 +36,27 @@ class Huya(DownloadBase):
self.huya_imgplus = config.get('huya_imgplus', True)
self.huya_cdn_fallback = config.get('huya_cdn_fallback', False)
self.huya_mobile_api = config.get('huya_mobile_api', False)
self.huya_codec = config.get('huya_codec', '264')
async def acheck_stream(self, is_check=False):
try:
if not self.__room_id.isdigit():
self.__room_id = _get_real_rid(self.url)
room_profile = await self.get_room_profile(use_api=True)
client.headers.update(self.fake_headers)
self.__room_id = await _get_real_rid(self.url)
logger.debug(f"{self.plugin_msg}: {_get_real_rid.cache_info()}")
self.fake_headers['referer'] = self.url
room_profile = await self.get_room_profile(self.huya_mobile_api)
except Exception as e:
logger.error(f"{self.plugin_msg}: {e}")
logger.error(f"{self.plugin_msg}: {e}", exc_info=True)
return False
if room_profile['realLiveStatus'] != 'ON' or room_profile['liveStatus'] != 'ON':
'''
ON: 直播
REPLAY: 重播
OFF: 未开播
'''
logger.debug(f"{self.plugin_msg}: 未开播")
if not room_profile['live']:
logger.debug(f"{self.plugin_msg}: {room_profile['message']}")
self.raw_stream_url = None
return False
if not room_profile['liveData'].get('bitRateInfo'):
# 主播未推流
logger.debug(f"{self.plugin_msg}: 未推流")
return False
self.room_title = room_profile['liveData']['introduction']
# 兼容 biliup/biliup#1200
self.room_title = room_profile['room_title']
# 虎牙回放
if self.room_title.startswith('【回放】'):
logger.debug(f"{self.plugin_msg}: {self.room_title}")
@@ -65,48 +65,35 @@ class Huya(DownloadBase):
if is_check:
return True
if self.huya_max_ratio:
try:
self.huya_max_ratio = int(self.huya_max_ratio)
# 最大码率(不含hdr)
# max_ratio = html_info['data'][0]['gameLiveInfo']['bitRate']
max_ratio = room_profile['liveData']['bitRate']
# 可选择的码率
live_rate_info = json.loads(room_profile['liveData']['bitRateInfo'])
# 码率信息
ratio_items = [r.get('iBitRate', 0) if r.get('iBitRate', 0) != 0 else max_ratio for r in live_rate_info]
# 符合条件的码率
ratio_in_items = [x for x in ratio_items if x <= self.huya_max_ratio]
# 录制码率
if ratio_in_items:
record_ratio = max(ratio_in_items)
else:
record_ratio = max_ratio
except (json.JSONDecodeError, KeyError) as e:
logger.error(f"{self.plugin_msg}: 在确定码率时发生错误 {e}")
return False
# self.room_title = room_profile['room_title']
is_xingxiu = (room_profile['liveData']['gid'] == 1663)
try:
stream_urls = await self.get_stream_urls(self.huya_protocol, self.huya_mobile_api, self.huya_imgplus, is_xingxiu)
except Exception as e:
logger.error(f"{self.plugin_msg}: 没有可用的链接 {e}")
return False
cdn_name = list(stream_urls.keys())
if not self.huya_cdn or self.huya_cdn not in cdn_name:
self.huya_cdn = cdn_name[0]
is_xingxiu = (room_profile['gid'] == 1663)
stream_urls = self.build_stream_urls(room_profile['streams_info'], is_xingxiu)
cdn_list = list(stream_urls.keys())
if not self.huya_cdn or self.huya_cdn not in cdn_list:
self.huya_cdn = cdn_list[0]
# Thx stream-rec
update_user_agent(self.fake_headers)
self.update_user_agent(self.fake_headers)
try:
self.raw_stream_url = self.add_ratio(
stream_urls[self.huya_cdn],
room_profile['bitrate_info'],
room_profile['max_bitrate']
)
except KeyError as e:
logger.error(f"{self.plugin_msg}: {e}", exc_info=True)
return False
# 虎牙直播流只允许连接一次
if self.huya_cdn_fallback:
_url = await self.acheck_url_healthy(stream_urls[self.huya_cdn])
_url = await self.acheck_url_healthy(self.raw_stream_url)
if _url is None:
logger.info(f"{self.plugin_msg}: cdn_fallback 顺序尝试 {cdn_name}")
for cdn in cdn_name:
logger.info(f"{self.plugin_msg}: cdn_fallback 顺序尝试 {cdn_list}")
for cdn in cdn_list:
if cdn == self.huya_cdn:
continue
logger.info(f"{self.plugin_msg}: cdn_fallback-{cdn}")
if (await self.acheck_url_healthy(stream_urls[cdn])) is None:
continue
@@ -116,19 +103,140 @@ class Huya(DownloadBase):
else:
logger.error(f"{self.plugin_msg}: cdn_fallback 所有链接无法使用")
return False
stream_urls = await self.get_stream_urls(self.huya_protocol, self.huya_mobile_api, self.huya_imgplus, is_xingxiu)
room_profile = await self.get_room_profile(self.huya_mobile_api)
if not room_profile['live']:
logger.debug(f"{self.plugin_msg}: {room_profile['message']}")
return False
stream_urls = self.build_stream_urls(room_profile['streams_info'], is_xingxiu)
self.raw_stream_url = stream_urls[self.huya_cdn]
self.raw_stream_url = self.add_ratio(
stream_urls[self.huya_cdn],
room_profile['bitrate_info'],
room_profile['max_bitrate']
)
if self.huya_max_ratio and record_ratio != max_ratio:
self.raw_stream_url += f"&ratio={record_ratio}"
return True
def danmaku_init(self):
if self.huya_danmaku:
self.danmaku = DanmakuClient(self.url, self.gen_download_filename())
def add_ratio(self, url: str, bitrate_info: Dict[str, Any], max_bitrate: int) -> str:
'''
添加码率
:param url: 流地址
:param bitrate_info: 可选择的码率信息
:param max_bitrate: 最大码率(不含hdr)
:return: 添加码率后的流地址
'''
if self.huya_max_ratio and "&ratio" not in url:
def __get_ratio(info: Dict[str, Any]) -> int:
return info.get('iBitRate', 0) or max_bitrate
try:
selected_ratio = 0
self.huya_max_ratio = int(self.huya_max_ratio)
# 符合条件的码率
allowed_ratio_list = [
__get_ratio(x) \
for x in bitrate_info \
if __get_ratio(x) <= self.huya_max_ratio
]
# 录制码率
if allowed_ratio_list:
selected_ratio = max(allowed_ratio_list)
if selected_ratio:
return f"{url}&ratio={selected_ratio}"
except KeyError as e:
raise KeyError(f"确定码率时发生错误") from e
return url
def get_stream_name(self, stream_name: str) -> str:
if self.huya_imgplus:
return stream_name
return stream_name.replace('-imgplus', '')
def build_stream_urls(
self,
streams_info: List[Dict[str, Any]],
skip_query_build: bool
) -> List[Dict[str, Any]]:
'''
构建流地址
:param streams_info: 流信息
:param skip_query_build: 是否跳过构建anti_code
:return: 流地址
'''
proto = self.huya_protocol
streams = {}
weights = {} # https://cdnweb.huya.com/getUidsDomainList?anchor_uid={anchor_uid}
for stream in streams_info:
# 优先级<0代表不可用
priority = stream['iWebPriorityRate']
if priority < 0:
continue
stream_name = self.get_stream_name(stream['sStreamName'])
cdn = stream['sCdnType']
suffix = stream[f's{proto}UrlSuffix']
anti_code = stream[f's{proto}AntiCode'] + f"&codec={self.huya_codec}"
if not skip_query_build:
anti_code = self.build_query(stream_name, anti_code, self.__get_uid(stream_name))
base_url = stream[f's{proto}Url'].replace('http://', 'https://')
streams[cdn] = f"{base_url}/{stream_name}.{suffix}?{anti_code}"
weights[cdn] = priority
return self.__weight_sorting(streams, weights)
def extract_room_profile(self, data: Union[str, Dict[str, Any]]) -> Dict[str, Any]:
'''
ON: 直播
REPLAY: 重播
OFF: 未开播
'''
if isinstance(data, str):
room_data = json_loads(match1(data, HUYA_WEB_ROOM_DATA_REGEX))
s = data.split('stream: ')[1].split('};')[0]
s_json = json_loads(s)
bitrate_info = s_json.get('vMultiStreamInfo')
if room_data['state'] != 'ON' or not bitrate_info:
return {
'live': False,
'message': '未开播' if room_data['state'] != 'ON' else '未推流',
}
live_info = s_json['data'][0]['gameLiveInfo']
streams_info = s_json['data'][0]['gameStreamInfoList']
elif isinstance(data, dict):
if data['liveStatus'] != 'ON' or not data.get('liveData', {}).get('bitRateInfo'):
return {
'live': False,
'message': '未开播' if data['liveStatus'] != 'ON' else '未推流',
}
live_info = data['liveData']
bitrate_info = live_info['bitRateInfo']
streams_info = live_info['streamsInfo']
return {
'artist': live_info['nick'],
'artist_img': live_info['avatar180'].replace('http://', 'https://'),
'bitrate_info': bitrate_info,
'gid': live_info['gid'],
'live': True,
'live_start_time': live_info['startTime'],
'max_bitrate': live_info['bitRate'],
'room_cover': live_info['screenshot'].replace('http://', 'https://'),
'room_title': live_info['introduction'],
'streams_info': streams_info,
}
async def get_room_profile(self, use_api=False) -> dict:
'''
获取房间信息
:param use_api: 是否使用API
:return: 房间信息
'''
if use_api:
params = {
'm': 'Live',
@@ -136,57 +244,32 @@ class Huya(DownloadBase):
'roomid': self.__room_id,
'showSecret': 1,
}
resp = (await client.get(f"https://mp.huya.com/cache.php",
headers=self.fake_headers, params=params)).json()
resp = await client.get(
f"{HUYA_MP_BASE_URL}/cache.php",
headers=self.fake_headers, params=params)
resp.raise_for_status()
resp = json_loads(resp.text)
if resp['status'] != 200:
raise Exception(f"{resp['message']}")
return resp['data']
else:
html = (await client.get(f"https://www.huya.com/{self.__room_id}", headers=self.fake_headers)).text
if "找不到这个主播" in html:
raise Exception(f"找不到这个主播")
return json.loads(html.split('stream: ')[1].split('};')[0])
resp = await client.get(
f"{HUYA_WEB_BASE_URL}/{self.__room_id}",
headers=self.fake_headers)
resp.raise_for_status()
resp = resp.text
_raise_for_room_block(resp)
return self.extract_room_profile(resp)
async def get_stream_urls(self, protocol, use_api=False, allow_imgplus=True, is_xingxiu=False) -> dict:
'''
返回指定协议的所有CDN流
'''
streams = {}
weights = {} # https://cdnweb.huya.com/getUidsDomainList?anchor_uid={anchor_uid}
room_profile = await self.get_room_profile(use_api=use_api)
if not use_api:
try:
stream_info = room_profile['data'][0]['gameStreamInfoList']
except KeyError:
raise Exception(f"{room_profile}")
else:
stream_info = room_profile['stream']['baseSteamInfoList']
weights = json.loads(room_profile['liveData'].get('mStreamRatioWeb', '{}'))
stream = stream_info[0]
stream_name = stream['sStreamName']
suffix, anti_code = stream[f's{protocol}UrlSuffix'], stream[f's{protocol}AntiCode']
if not allow_imgplus:
stream_name = stream_name.replace('-imgplus', '')
anti_code = anti_code + "&codec=264" \
if is_xingxiu else \
self.__build_query(stream_name, anti_code, _get_uid(self.fake_headers.get('cookie', ''), stream_name))
for stream in stream_info:
# 优先级<0代表不可用
priority = stream['iWebPriorityRate']
if priority < 0:
continue
cdn_name = stream['sCdnType']
secure_url = stream[f's{protocol}Url'].replace('http://', 'https://')
stream_url = f"{secure_url}/{stream_name}.{suffix}?{anti_code}"
streams[cdn_name] = stream_url
if cdn_name not in weights:
weights[cdn_name] = priority
return _weight_sorting(streams, weights)
@staticmethod
def __build_query(stream_name, anti_code, uid: int) -> str:
def build_query(self, stream_name, anti_code, uid: int) -> str:
'''
构建anti_code
:param stream_name: 流名称
:param anti_code: 原始anti_code
:param uid: 主播uid
:return: 构建后的anti_code
'''
url_query = parse_qs(anti_code)
# platform_id = 100
platform_id = url_query.get('t', [100])[0]
ws_time = url_query['wsTime'][0]
convert_uid = (uid << 8 | uid >> (32 - 8)) & 0xFFFFFFFF
@@ -221,56 +304,62 @@ class Huya(DownloadBase):
"u": convert_uid,
"uuid": str(int((ct % 1e10 + random.random()) * 1e3 % 0xffffffff)),
"sdk_sid": str(int(time.time() * 1000)),
"codec": "264",
"codec": self.huya_codec,
}
return '&'.join([f"{k}={v}" for k, v in anti_code.items()])
@staticmethod
def __weight_sorting(
data: Dict[str, Any], weights: Dict[str, Any]
) -> Dict[str, Any]:
if data:
data = {k: v for k, v in data.items() if k not in ['HY', 'HUYA', 'HYZJ']}
return dict(sorted(data.items(), key=lambda x: weights[x[0]], reverse=True))
return {}
@lru_cache(maxsize=None)
def _get_real_rid(url) -> str:
import requests
headers = {
'user-agent': random_user_agent(),
}
resp = requests.get(url, headers=headers)
if '找不到这个主播' in resp.text:
raise Exception(f"找不到这个主播")
hy_config = json.loads(resp.text.split('stream: ')[1].split('};')[0])
return str(hy_config['data'][0]['gameLiveInfo']['profileRoom'])
@staticmethod
def __get_uid(stream_name: str) -> int:
try:
if stream_name:
anchor_uid = int(stream_name.split('-')[0])
if anchor_uid > 0:
return anchor_uid
except IndexError:
pass
return random.randint(1400000000000, 1499999999999)
# udbAnonymousUid = requests.post(
# url='https://udblgn.huya.com/web/anonymousLogin',
# headers={
# 'user-agent': random_user_agent(),
# },
# json={
# "appId": 5002,
# "byPass": 3,
# "context": "",
# "version": "2.4",
# "data": {},
# }
# )['data']['uid']
def _weight_sorting(data: dict, weights: dict) -> dict:
if data:
data = {k: v for k, v in data.items() if k not in ['HY', 'HUYA', 'HYZJ']}
return dict(sorted(data.items(), key=lambda x: weights[x[0]], reverse=True))
return {}
@staticmethod
def update_user_agent(headers: dict):
headers['user-agent'] = f"HYSDK(Windows, {int(time.time())})"
def _get_uid(cookie: str, stream_name: str) -> int:
try:
if cookie and "yyuid=" in cookie:
return int(match1(r'yyuid=(\d+)', cookie))
if stream_name:
anchor_uid = int(stream_name.split('-')[0])
if anchor_uid > 0:
return anchor_uid
except:
pass
return random.randint(1400000000000, 1499999999999)
# udbAnonymousUid = requests.post(
# url='https://udblgn.huya.com/web/anonymousLogin',
# headers={
# 'user-agent': random_user_agent(),
# },
# json={
# "appId": 5002,
# "byPass": 3,
# "context": "",
# "version": "2.4",
# "data": {},
# }
# )['data']['uid']
def _raise_for_room_block(text: str):
for err_key in ("找不到这个主播", "该主播涉嫌违规,正在整改中"):
if err_key in text:
raise Exception(err_key)
def update_user_agent(headers: dict):
headers['User-Agent'] = f"HYSDK(Windows, {int(time.time())})"
@alru_cache(maxsize=None)
async def _get_real_rid(url) -> str:
resp = await client.get(url)
resp.raise_for_status()
_raise_for_room_block(resp.text)
room_data = match1(resp.text, HUYA_WEB_ROOM_DATA_REGEX)
room_data = json_loads(room_data)
if not room_data.get('profileRoom'):
raise Exception("找不到这个主播")
return str(room_data['profileRoom'])
+9 -9
View File
@@ -44,22 +44,22 @@ class BiliWeb(UploadBase):
self.copyright_source = copyright_source
self.extra_fields = extra_fields
def upload(self, file_list) -> List[UploadBase.FileInfo]:
line = None
if self.lines == 'kodo':
line = stream_gears.UploadLine.Kodo
if self.lines == 'bda':
line = stream_gears.UploadLine.Bda
elif self.lines == 'bda2':
line = stream_gears.UploadLine.Bda2
elif self.lines == 'ws':
line = stream_gears.UploadLine.Ws
elif self.lines == 'qn':
line = stream_gears.UploadLine.Qn
elif self.lines == 'cos':
line = stream_gears.UploadLine.Cos
elif self.lines == 'cos-internal':
line = stream_gears.UploadLine.CosInternal
elif self.lines == 'tx':
line = stream_gears.UploadLine.Tx
elif self.lines == 'txa':
line = stream_gears.UploadLine.Txa
elif self.lines == 'bldsa':
line = stream_gears.UploadLine.Bldsa
tag = ','.join(self.tags)
if self.credits:
desc_v2 = self.creditsToDesc_v2()
+3 -2
View File
@@ -14,7 +14,7 @@ dependencies = [
"stream-gears >= 0.2.2,<0.3",
"psutil >= 5.4.6",
"aiofiles >= 22.1.0",
"yt-dlp >= 2025.2.19",
"yt-dlp >= 2025.3.31",
"Pillow >= 5.2.0",
"aiohttp[speedups] < 3.10.0", # aio-libs/aiohttp#10519
"aiohttp_cors >= 0.7.0",
@@ -22,7 +22,7 @@ dependencies = [
"httpx[http2] >= 0.28.1",
"httpcore >= 1.0.7",
"PyYAML >= 4.2b1",
"streamlink >= 7.1.3",
"streamlink >= 7.2.0",
"ykdl >= 1.8.0",
"rsa >= 4.6",
"tomli >= 1.1.0; python_version < '3.11'",
@@ -31,6 +31,7 @@ dependencies = [
"alembic >= 1.12.1",
"m3u8 >= 6.0.0",
"truststore >= 0.10.1; python_version >= '3.10'",
"async-lru >= 2.0.5",
]
dynamic = ["version"]