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:
1 parent
aa4a4e3b68
commit
045e0230eb
10 files changed
+316
-207
No files matched your search
+1
-1
@@ -2,7 +2,7 @@ import logging
|
||||
import platform
|
||||
import sys
|
||||
|
||||
__version__ = "0.4.92"
|
||||
__version__ = "0.4.93"
|
||||
|
||||
LOG_CONF = {
|
||||
'version': 1,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
@@ -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'])
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
|
||||
Reference in new issue
Block a user