diff --git a/biliup/__init__.py b/biliup/__init__.py index 5c88421..1638869 100644 --- a/biliup/__init__.py +++ b/biliup/__init__.py @@ -2,7 +2,7 @@ import logging import platform import sys -__version__ = "0.4.92" +__version__ = "0.4.93" LOG_CONF = { 'version': 1, diff --git a/biliup/common/util.py b/biliup/common/util.py index 95166cb..50bff66 100644 --- a/biliup/common/util.py +++ b/biliup/common/util.py @@ -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) diff --git a/biliup/engine/download.py b/biliup/engine/download.py index 6337d5d..de46e6c 100644 --- a/biliup/engine/download.py +++ b/biliup/engine/download.py @@ -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 diff --git a/biliup/plugins/__init__.py b/biliup/plugins/__init__.py index 758c4a3..e7eb9a7 100644 --- a/biliup/plugins/__init__.py +++ b/biliup/plugins/__init__.py @@ -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" diff --git a/biliup/plugins/bilibili.py b/biliup/plugins/bilibili.py index aa1aa27..79c449e 100644 --- a/biliup/plugins/bilibili.py +++ b/biliup/plugins/bilibili.py @@ -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: diff --git a/biliup/plugins/biliuprs.py b/biliup/plugins/biliuprs.py index 3be0760..6ab7de1 100644 --- a/biliup/plugins/biliuprs.py +++ b/biliup/plugins/biliuprs.py @@ -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': diff --git a/biliup/plugins/douyin.py b/biliup/plugins/douyin.py index 64008cb..988aa7f 100644 --- a/biliup/plugins/douyin.py +++ b/biliup/plugins/douyin.py @@ -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 diff --git a/biliup/plugins/huya.py b/biliup/plugins/huya.py index fb29ed0..d23e584 100644 --- a/biliup/plugins/huya.py +++ b/biliup/plugins/huya.py @@ -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']) \ No newline at end of file diff --git a/biliup/plugins/stream_gears.py b/biliup/plugins/stream_gears.py index 037d97e..c6a1139 100644 --- a/biliup/plugins/stream_gears.py +++ b/biliup/plugins/stream_gears.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml index 32f16e0..154a3fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"]