diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml index 25736ff..d5029a5 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -9,27 +9,26 @@ body: ## 请补全标题并认真填写下方内容 请清晰描述问题,不必严格套用模板。 - + - type: checkboxes id: terms attributes: label: Code of Conduct description: 你必须先同意以下内容 options: - - label: 我清楚知道,可以在 [Release](https://github.com/biliup/biliup/releases) 检阅最新的 Biliup 版本 + - label: 我已在 [Release](https://github.com/biliup/biliup/releases) 检阅最新版本并尝试,且问题依旧 required: true - label: 我清楚知道,反馈非最新 Release 版的错误是无意义的 required: true - - label: 我清楚知道,日志中并不包含 Cookie 此类的信息,可以直接上传完整日志 - required: true - - label: 我已经尝试过最新 Release 版,且问题依旧 + - label: 我清楚知道,可以通过附件功能上传完整文件 required: true - type: input id: system_version attributes: label: 运行环境 - description: 如 Windows Server 2012、CentOS 8.2 等 + description: 包括系统名称、系统版本、系统架构、Python版本 + placeholder: Debian 12.6 x86_64 && Python 3.12.4 或 Docker tag:v0.4.71 validations: required: true @@ -38,6 +37,7 @@ body: attributes: label: biliup 版本 description: 可通过运行 `biliup --version` 查看 + placeholder: v0.4.71 validations: required: true @@ -45,7 +45,8 @@ body: id: url attributes: label: 所录制的主播网址 - description: 可以在配置文件中查看 + description: 因平台内直播分区策略有所不同,请准确填写出现问题的主播网址 + placeholder: https://www.twitch.tv/lck validations: required: true @@ -60,18 +61,18 @@ body: - type: textarea id: config attributes: - label: 配置文件 - description: 请**脱敏**后上传你的配置文件, 或将文件内容复制到下面模板中 + label: 任务平台 + description: 请复制WEBUI的任务平台内容并**脱敏**后放入文件上传, 或直接复制到下面模板中 value: |
- 配置文件 + 任务平台 - + ```bash # 在这里填写你的配置文件 ``` - +
- type: textarea @@ -84,13 +85,13 @@ body: ### 日志 - + ```bash # 在这里填写你的报错日志 ``` - + - + ### 其他补充描述 - + diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 73b5c3a..363b7f0 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -209,7 +209,7 @@ const Dashboard: React.FC = () => { {/* 哔哩哔哩 */} - + {/* CC直播 */} {/* 抖音 */} diff --git a/app/ui/plugins/bilibili.tsx b/app/ui/plugins/bilibili.tsx index 4897f7f..382b063 100644 --- a/app/ui/plugins/bilibili.tsx +++ b/app/ui/plugins/bilibili.tsx @@ -2,7 +2,14 @@ import React from "react"; import { Form, Select, Collapse } from "@douyinfe/semi-ui"; -const Bilibili: React.FC = () => { +type Props = { + entity: any; + list: any; +}; + +const Bilibili: React.FC = (props) => { + const entity = props.entity; + const list = props.list; return ( <> @@ -41,6 +48,46 @@ const Bilibili: React.FC = () => { field="bilibili_danmaku" extraText="录制哔哩哔哩弹幕,目前不支持视频按时长分段下的弹幕文件自动分段。仅限下载插件为非 stream-gears 时生效,默认关闭。" label="录制弹幕(bilibili_danmaku)" + fieldStyle={{ + alignSelf: "stretch", + padding: 0, + }} + /> + + 根据格式填入cookie。推荐使用「 + + biliup-rs + + 」来获取。 +
+ + } + placeholder="SESSDATA=none;bili_jct=none;DedeUserID__ckMd5=none;DedeUserID=none;" + label="哔哩哔哩 Cookie 文本(bili_cookie)" + style={{ width: "100%" }} + fieldStyle={{ + alignSelf: "stretch", + padding: 0, + }} + /> + = (props) => { return ( <> - - 根据格式填入cookie。推荐使用「 - - biliup-rs - - 」来获取。 -
- - } - placeholder="SESSDATA=none;bili_jct=none;DedeUserID__ckMd5=none;DedeUserID=none;" - label="哔哩哔哩 Cookie 文本(bili_cookie)" - style={{ width: "100%" }} - fieldStyle={{ - alignSelf: "stretch", - padding: 0, - }} - /> - - - 如需要录制抖音 www.douyin.com/user/ - 类型链接,或遭到风控,请在此填入 Cookie。 -
- 需要__ac_nonce、__ac_signature、sessionid的值,请不要将所有 - Cookie 填入。 - - } - placeholder="__ac_nonce=none;__ac_signature=none;sessionid=none;" - label="抖音 Cookie(douyin_cookie)" - style={{ width: "100%" }} - fieldStyle={{ - alignSelf: "stretch", - padding: 0, - }} - /> = (props) => { padding: 0, }} /> - - 填入 Cookie 防止游客2分30秒限制。 - 需要 udb_biztoken, udb_uid, yyuid 的值, - 请不要将所有 Cookie 填入。 - - } - placeholder="udb_uid=none;yyuid=none;udb_biztoken=none;" - label="虎牙 Cookie(huya_cookie)" - style={{ width: "100%" }} - fieldStyle={{ - alignSelf: "stretch", - padding: 0, - }} - /> - - 【仅限Turbo会员】如录制Twitch时遇见视频流中广告过多的情况,可尝试在此填入cookie,可以大幅减少视频流中的twitch广告 -
- 该 Cookie - 存在过期风险,Cookie过期后会在日志输出警告,请注意及时更换。 -
- 当 Cookie 失效,录制时将忽略 - Cookie。(经作者个人测试,可保持未过期状态四个月以上) -
- twitch_cookie 获取方式:在浏览器中打开 twitch.tv , - F12 调出控制台,在控制台中执行如下代码: -
- {`document.cookie.split("; ").find(item => item.startsWith("auth-token="))?.split("=")[1]`} -
- twitch_cookie 需要在 downloader= - "ffmpeg" 时候才会生效。 - - } - label="Twitch Cookie(twitch_cookie)" - style={{ width: "100%" }} - fieldStyle={{ - alignSelf: "stretch", - padding: 0, - }} - /> - - 使用Cookies登陆YouTube帐号,可用于下载会限、私享等未登录账号无法访问的内容。 -
- 可以使用 Chrome 插件「Get - cookies.txt」来生成txt文件,请使用 Netscape 格式的 - Cookies 文本路径。 - - } - label="TouTube Cookie(youtube_cookie)" - style={{ width: "100%" }} - fieldStyle={{ - alignSelf: "stretch", - padding: 0, - }} - /> { } label="录制弹幕(douyin_danmaku)" + fieldStyle={{ + alignSelf: "stretch", + padding: 0, + }} + /> + + 如需要录制抖音 www.douyin.com/user/ + 类型链接,或遭到风控,请在此填入 Cookie。 +
+ 需要__ac_nonce、__ac_signature、sessionid的值,请不要将所有 + Cookie 填入。 + + } + placeholder="__ac_nonce=none;__ac_signature=none;sessionid=none;" + label="抖音 Cookie(douyin_cookie)" + style={{ width: "100%" }} + fieldStyle={{ + alignSelf: "stretch", + padding: 0, + }} /> { { showClear={true} > AUTO(自动,默认) + bda bda2 - kodo ws qn bldsa + tx + txa = (props) => { alignSelf: "stretch", padding: 0, }} - rules={[ - { - pattern: /^[A-Z]{2}(?:\d{2})?$/, - message: "请仅输入线路别称" - } - ]} showClear={true} > 直播线路3(AL) @@ -109,11 +103,19 @@ const Huya: React.FC = (props) => { ? entity["huya_imgplus"] : true } + fieldStyle={{ + alignSelf: "stretch", + padding: 0, + }} />
diff --git a/app/ui/plugins/twitcasting.tsx b/app/ui/plugins/twitcasting.tsx index 1a35cb1..9d66844 100644 --- a/app/ui/plugins/twitcasting.tsx +++ b/app/ui/plugins/twitcasting.tsx @@ -11,6 +11,10 @@ const TwitCasting: React.FC = () => { field="twitcasting_danmaku" extraText="录制TwitCasting弹幕,默认关闭" label="录制弹幕(twitcasting_danmaku)" + fieldStyle={{ + alignSelf: "stretch", + padding: 0, + }} /> = (props) => { field="twitch_disable_ads" extraText="去除Twitch广告功能,默认开启 这个功能会导致Twitch录播分段,因为遇到广告就自动断开了,这就是去广告。若需要录播完整一整段可以关闭这个,但是关了之后就会有紫色屏幕的CommercialTime -还有一个不想视频分段的办法是去花钱开一个Turbo会员,能不看广告,然后下面的user里把twitch的cookie填上,也能不看广告,自然就不会分段了" +还有一个办法是去花钱开一个Turbo会员,然后下面的user里把twitch的cookie填上,也能去除广告" label="去除广告(twitch_disable_ads)" + fieldStyle={{ + alignSelf: "stretch", + padding: 0, + }} + /> + + 【仅限Turbo会员】如录制Twitch时遇见视频流中广告过多的情况,可尝试在此填入cookie,可以大幅减少视频流中的twitch广告 +
+ 该 Cookie + 存在过期风险,Cookie过期后会在日志输出警告,请注意及时更换。 +
+ 当 Cookie 失效,录制时将忽略 + Cookie。(经作者个人测试,可保持未过期状态四个月以上) +
+ twitch_cookie 获取方式:在浏览器中打开 twitch.tv , + F12 调出控制台,在控制台中执行如下代码: +
+ {`document.cookie.split("; ").find(item => item.startsWith("auth-token="))?.split("=")[1]`} +
+ twitch_cookie 需要在 downloader= + "ffmpeg" 时候才会生效。 + + } + label="Twitch Cookie(twitch_cookie)" + style={{ width: "100%" }} + fieldStyle={{ + alignSelf: "stretch", + padding: 0, + }} />
diff --git a/app/ui/plugins/youtube.tsx b/app/ui/plugins/youtube.tsx index bfc1ee1..176a7aa 100644 --- a/app/ui/plugins/youtube.tsx +++ b/app/ui/plugins/youtube.tsx @@ -11,6 +11,24 @@ const YouTube: React.FC = (props) => { return ( <> + + 使用Cookies登陆YouTube帐号,可用于下载会限、私享等未登录账号无法访问的内容。 +
+ 可以使用 Chrome 插件「Get + cookies.txt」来生成txt文件,请使用 Netscape 格式的 + Cookies 文本路径。 + + } + label="YouTube Cookie(youtube_cookie)" + style={{ width: "100%" }} + fieldStyle={{ + alignSelf: "stretch", + padding: 0, + }} + /> = (props) => { 例如录制https://www.youtube.com/@NeneAmanoCh/streams,关闭后将忽略正在直播 " label="下载直播(youtube_enable_download_live)" + fieldStyle={{ + alignSelf: "stretch", + padding: 0, + }} /> = (props) => { 例如录制https://www.youtube.com/@NeneAmanoCh/streams,关闭后将忽略直播回放 " label="下载回放(youtube_enable_download_playback)" + fieldStyle={{ + alignSelf: "stretch", + padding: 0, + }} /> {self._auto_os['os']}: {self._auto_os['query']}. time: {self._auto_os.get('cost')}") if self._auto_os['os'] == 'upos': upload = self.upos - elif self._auto_os['os'] == 'cos': - upload = self.cos - elif self._auto_os['os'] == 'cos-internal': - upload = lambda *args, **kwargs: self.cos(*args, **kwargs, internal=True) - elif self._auto_os['os'] == 'kodo': - upload = self.kodo + # elif self._auto_os['os'] == 'cos': + # upload = self.cos + # elif self._auto_os['os'] == 'cos-internal': + # upload = lambda *args, **kwargs: self.cos(*args, **kwargs, internal=True) + # elif self._auto_os['os'] == 'kodo': + # upload = self.kodo else: logger.error(f"NoSearch:{self._auto_os['os']}") raise NotImplementedError(self._auto_os['os']) diff --git a/biliup/plugins/bilibili.py b/biliup/plugins/bilibili.py index ae4c370..65511d0 100644 --- a/biliup/plugins/bilibili.py +++ b/biliup/plugins/bilibili.py @@ -12,8 +12,7 @@ from ..engine.download import DownloadBase OFFICIAL_API = "https://api.live.bilibili.com" -@Plugin.download(regexp=r'(?:https?://)?live\.bilibili\.com') -@Plugin.download(regexp=r'(?:https?://)?b23\.tv') +@Plugin.download(regexp=r'(?:https?://)?(b23\.tv|live\.bilibili\.com)') class Bililive(DownloadBase): def __init__(self, fname, url, suffix='flv'): super().__init__(fname, url, suffix) @@ -22,7 +21,7 @@ class Bililive(DownloadBase): self.fake_headers['referer'] = url if config.get('user', {}).get('bili_cookie'): self.fake_headers['cookie'] = config.get('user', {}).get('bili_cookie') - elif config.get('user', {}).get('bili_cookie_file'): + if config.get('user', {}).get('bili_cookie_file'): cookie_file_name = config.get('user', {}).get('bili_cookie_file') try: with open(cookie_file_name, encoding='utf-8') as stream: @@ -37,10 +36,23 @@ class Bililive(DownloadBase): # logger.warning("No cookie provided. The original quality may not be available.") async def acheck_stream(self, is_check=False): - room_id = match1(self.url, r'/(\d+)') - qualityNumber = int(config.get('bili_qn', 10000)) client.headers.update(self.fake_headers) + + if "b23.tv" in self.url: + try: + url = (await client.get(self.url, follow_redirects=False)).next_request.url + if "live.bilibili" in str(url): + self.url = url + else: + raise + except: + logger.error(f"{self.plugin_msg}: 不支持的链接") + return False + + room_id = match1(self.url, r'bilibili.com/(\d+)') + qualityNumber = int(config.get('bili_qn', 10000)) + # 获取直播状态与房间标题 info_by_room_url = f"{OFFICIAL_API}/xlive/web-room/v1/index/getInfoByRoom?room_id={room_id}" try: diff --git a/biliup/plugins/biliuprs.py b/biliup/plugins/biliuprs.py index 4319acd..7b78fe3 100644 --- a/biliup/plugins/biliuprs.py +++ b/biliup/plugins/biliuprs.py @@ -118,18 +118,18 @@ class BiliWeb(UploadBase): def stream_gears_upload(ex_conn, lines, *args, **kwargs): try: - if lines == 'kodo': - kwargs['line'] = stream_gears.UploadLine.Kodo + if lines == 'bda': + 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 == 'cos': - kwargs['line'] = stream_gears.UploadLine.Cos - elif lines == 'cos-internal': - kwargs['line'] = stream_gears.UploadLine.CosInternal + elif lines == 'tx': + kwargs['line'] = stream_gears.UploadLine.Tx + elif lines == 'txa': + kwargs['line'] = stream_gears.UploadLine.Txa elif lines == 'bldsa': kwargs['line'] = stream_gears.UploadLine.Bldsa diff --git a/biliup/plugins/douyu.py b/biliup/plugins/douyu.py index 9b256d2..0eb7049 100644 --- a/biliup/plugins/douyu.py +++ b/biliup/plugins/douyu.py @@ -1,19 +1,21 @@ import hashlib import time from urllib.parse import parse_qs +from functools import lru_cache from biliup.common.util import client from biliup.config import config from biliup.Danmaku import DanmakuClient from ..engine.decorators import Plugin from ..engine.download import DownloadBase -from ..plugins import logger, match1 +from ..plugins import logger, match1, random_user_agent @Plugin.download(regexp=r'(?:https?://)?(?:(?:www|m)\.)?douyu\.com') class Douyu(DownloadBase): def __init__(self, fname, url, suffix='flv'): super().__init__(fname, url, suffix) + self._room_id = match1(url, r'rid=(\d+)') self.douyu_danmaku = config.get('douyu_danmaku', False) async def acheck_stream(self, is_check=False): @@ -22,24 +24,15 @@ class Douyu(DownloadBase): return False try: - room_id = self.url.split('douyu.com/')[1].split('/')[0].split('?')[0] - # 暂时只判断纯数字的room_id - if not room_id.isdigit() or len(room_id) < 6: - room_id = 0 - if room_id == 0: - resp = await client.get(self.url, headers=self.fake_headers, timeout=5) - room_id = match1(resp.text, r'\$ROOM\.room_id\s*=\s*(\d+)', r'apm_room_id\s*=\s*(\d+)')[0] - if not room_id: - logger.error(f"{self.plugin_msg}: 直播间不存在或已关闭") - return False + if not self._room_id: + self._room_id = _get_real_rid(self.url) except: logger.exception(f"{self.plugin_msg}: 获取房间号错误") return False try: room_info = ( - await client.get(f"https://www.douyu.com/betard/{room_id}", - headers=self.fake_headers, timeout=5) + await client.get(f"https://www.douyu.com/betard/{self._room_id}", headers=self.fake_headers) ).json()['room'] except: logger.exception(f"{self.plugin_msg}: 获取直播间信息错误") @@ -52,8 +45,10 @@ class Douyu(DownloadBase): logger.debug(f"{self.plugin_msg}: 正在放录播") return False if config.get('douyu_disable_interactive_game', False): - gift_info = (await client.get(f"https://www.douyu.com/api/interactive/web/v2/list?rid={room_id}", - headers=self.fake_headers, timeout=5)).json().get('data', {}) + gift_info = ( + await client.get(f"https://www.douyu.com/api/interactive/web/v2/list?rid={self._room_id}", + headers=self.fake_headers) + ).json().get('data', {}) if gift_info: logger.debug(f"{self.plugin_msg}: 正在运行互动游戏") return False @@ -77,18 +72,18 @@ class Douyu(DownloadBase): import jsengine ctx = jsengine.jsengine() js_enc = ( - await client.get(f'https://www.douyu.com/swf_api/homeH5Enc?rids={room_id}', - headers=self.fake_headers, timeout=5) - ).json()['data'][f'room{room_id}'] + await client.get(f'https://www.douyu.com/swf_api/homeH5Enc?rids={self._room_id}', + headers=self.fake_headers) + ).json()['data'][f'room{self._room_id}'] js_enc = js_enc.replace('return eval', 'return [strc, vdwdae325w_64we];') sign_fun, sign_v = ctx.eval(f'{js_enc};ub98484234();') tt = str(int(time.time())) did = hashlib.md5(tt.encode('utf-8')).hexdigest() - rb = hashlib.md5(f"{room_id}{did}{tt}{sign_v}".encode('utf-8')).hexdigest() + rb = hashlib.md5(f"{self._room_id}{did}{tt}{sign_v}".encode('utf-8')).hexdigest() sign_fun = sign_fun.rstrip(';').replace("CryptoJS.MD5(cb).toString()", f'"{rb}"') - sign_fun += f'("{room_id}","{did}","{tt}");' + sign_fun += f'("{self._room_id}","{did}","{tt}");' params = parse_qs(ctx.eval(sign_fun)) except: @@ -100,7 +95,7 @@ class Douyu(DownloadBase): params['rate'] = config.get('douyu_rate', 0) try: - live_data = await self.get_play_info(room_id, params) + live_data = await self.get_play_info(self._room_id, params) self.raw_stream_url = f"{live_data['rtmp_url']}/{live_data['rtmp_live']}" except: logger.exception(f"{self.plugin_msg}: ") @@ -113,17 +108,25 @@ class Douyu(DownloadBase): self.danmaku = DanmakuClient(self.url, self.gen_download_filename()) async def get_play_info(self, room_id, params): - __cdn_check = lambda _name, _list: any(_name in _item['cdn'] for _item in _list) - - live_data = await client.post(f'https://www.douyu.com/lapi/live/getH5Play/{room_id}', - headers=self.fake_headers, params=params, timeout=5) + live_data = await client.post( + f'https://www.douyu.com/lapi/live/getH5Play/{room_id}', headers=self.fake_headers, params=params) if not live_data.is_success: raise RuntimeError(live_data.text) live_data = live_data.json().get('data') if isinstance(live_data, dict): if not live_data['rtmp_cdn'].endswith('h5') or 'akm' in live_data['rtmp_cdn']: - params['cdn'] = 'tct-h5' if __cdn_check('tct-h5', live_data['cdnsWithName']) \ - else live_data['cdnsWithName'][-1]['cdn'] + params['cdn'] = live_data['cdnsWithName'][-1]['cdn'] return await self.get_play_info(room_id, params) return live_data raise RuntimeError(live_data) + +@lru_cache(maxsize=None) +def _get_real_rid(url): + import requests + headers = { + "user-agent": random_user_agent('mobile'), + } + rid = url.split('douyu.com/')[1].split('/')[0].split('?')[0] or match1(url, r'douyu.com/(\d+)') + resp = requests.get(f"https://m.douyu.com/{rid}", headers=headers) + real_rid = match1(resp.text, r'roomInfo":{"rid":(\d+)') + return real_rid \ No newline at end of file diff --git a/biliup/plugins/huya.py b/biliup/plugins/huya.py index 9986981..1c40b66 100644 --- a/biliup/plugins/huya.py +++ b/biliup/plugins/huya.py @@ -4,40 +4,37 @@ import json import random import time from urllib.parse import parse_qs, unquote +from functools import lru_cache from biliup.common.util import client from biliup.config import config from biliup.Danmaku import DanmakuClient from ..engine.decorators import Plugin from ..engine.download import DownloadBase -from ..plugins import logger +from ..plugins import logger, random_user_agent @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.huya_danmaku = config.get('huya_danmaku', False) self.fake_headers['referer'] = url self.fake_headers['cookie'] = config.get('user', {}).get('huya_cookie', '') - self.__room_id = None + self._room_id = url.split('huya.com/')[1].split('?')[0] + self.huya_danmaku = config.get('huya_danmaku', False) + async def acheck_stream(self, is_check=False): try: - await self.__verify_cookie() - self.__room_id = self.url.split('huya.com/')[1] - if not self.__room_id: - raise - if not self.__room_id.isdigit(): - html_info = await self.get_room_profile() - if not html_info: - raise - self.__room_id = html_info['data'][0]['gameLiveInfo']['profileRoom'] + if self.fake_headers.get('cookie'): + await self.verify_cookie() + if not self._room_id.isdigit(): + self._room_id = _get_real_rid(self.url) + room_profile = await self.get_room_profile(use_api=True) except Exception as e: logger.error(f"{self.plugin_msg}: {e}") return False - room_profile = await self.get_room_profile(use_api=True) if room_profile['realLiveStatus'] != 'ON' or room_profile['liveStatus'] != 'ON': ''' ON: 直播 @@ -48,6 +45,11 @@ class Huya(DownloadBase): self.raw_stream_url = None return False + if not room_profile['liveData'].get('bitRateInfo'): + # 主播未推流 + logger.debug(f"{self.plugin_msg} : 未推流") + return False + if is_check: return True @@ -117,6 +119,7 @@ class Huya(DownloadBase): 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()) @@ -124,17 +127,18 @@ class Huya(DownloadBase): async def get_room_profile(self, use_api=False) -> dict: if use_api: - resp = (await client.get(f"https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid={self.__room_id}", \ - headers=self.fake_headers)).json() + resp = (await client.get(f"https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid={self._room_id}", \ + headers=self.fake_headers)).json() if resp['status'] != 200: - raise Exception(f"{self.plugin_msg}: {resp['message']}") + 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 + html = (await client.get(f"https://www.huya.com/{self._room_id}", headers=self.fake_headers)).text if '找不到这个主播' in html: - raise Exception(f"{self.plugin_msg}: 找不到这个主播") + raise Exception(f"找不到这个主播") return json.loads(html.split('stream: ')[1].split('};')[0]) + async def get_stream_urls(self, protocol, use_api=False, allow_imgplus=True) -> dict: ''' 返回指定协议的所有CDN流 @@ -145,25 +149,27 @@ class Huya(DownloadBase): try: stream_info = room_profile['data'][0]['gameStreamInfoList'] except KeyError: - raise Exception(f"{self.plugin_msg}: {room_profile}") + raise Exception(f"{room_profile}") else: stream_info = room_profile['stream']['baseSteamInfoList'] - streams = self.__dict_sorting(json.loads(room_profile['liveData']['mStreamRatioWeb'])) + streams = _dict_sorting(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 = self.__build_query(stream_name, anti_code, ) + anti_code = self.__build_query(stream_name, anti_code, self.fake_headers['cookie']) for stream in stream_info: + stream_url = f"{stream[f's{protocol}Url']}/{stream_name}.{suffix}?{anti_code}" if stream['sCdnType'] in ['HY', 'HUYA', 'HYZJ']: continue - streams[stream['sCdnType']] = f"{stream[f's{protocol}Url']}/{stream_name}.{suffix}?{anti_code}" + streams[stream['sCdnType']] = stream_url return streams - def __build_query(self, stream_name, anti_code) -> str: + @staticmethod + def __build_query(stream_name, anti_code, cookies=None) -> str: url_query = parse_qs(anti_code) platform_id = 100 - uid = self.__get_uid(self.fake_headers['cookie'], stream_name) + uid = _get_uid(cookies, stream_name) convert_uid = (uid << 8 | uid >> (32 - 8)) & 0xFFFFFFFF ws_time = url_query['wsTime'][0] ct = int((int(ws_time, 16) + random.random()) * 1000) @@ -197,7 +203,8 @@ class Huya(DownloadBase): } return '&'.join([f"{k}={v}" for k, v in anti_code.items()]) - async def __verify_cookie(self): + + async def verify_cookie(self): if self.fake_headers['cookie']: resp = (await client.post('https://udblgn.huya.com/web/cookie/verify', \ headers=self.fake_headers, data={'appId': 5002})).json() @@ -205,18 +212,32 @@ class Huya(DownloadBase): logger.error(f"{self.plugin_msg}: {resp.json()['message']}") self.fake_headers['cookie'] = '' - @staticmethod - def __dict_sorting(data: dict) -> dict: +@lru_cache(maxsize=None) +def _get_real_rid(url): + import requests + headers = { + 'user-agent': random_user_agent(), + } + html = requests.get(url, headers=headers).text + if '找不到这个主播' in html: + raise Exception(f"找不到这个主播") + html_obj = json.loads(html.split('stream: ')[1].split('};')[0]) + return html_obj['data'][0]['gameLiveInfo']['profileRoom'] + + +def _dict_sorting(data: 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: x[1], reverse=True)) + return {} - @staticmethod - def __get_uid(cookie: str, stream_name: str) -> int: - if cookie: - cookie_dict = {k.strip(): v for k, v in (item.split('=') for item in cookie.split(';'))} - for key in ['udb_uid', 'yyuid']: - if key in cookie_dict: - return int(cookie_dict[key]) - if stream_name: - return int(stream_name.split('-')[0]) - return random.randint(1400000000000, 1499999999999) \ No newline at end of file + +def _get_uid(cookie: str, stream_name: str) -> int: + if cookie: + cookie_dict = {k.strip(): v for k, v in (item.split('=') for item in cookie.split(';'))} + for key in ['udb_uid', 'yyuid']: + if key in cookie_dict: + return int(cookie_dict[key]) + if stream_name: + return int(stream_name.split('-')[0]) + return random.randint(1400000000000, 1499999999999) diff --git a/biliup/web/aiohttp_basicauth_middleware.py b/biliup/web/aiohttp_basicauth_middleware.py index f52598a..179cbdd 100644 --- a/biliup/web/aiohttp_basicauth_middleware.py +++ b/biliup/web/aiohttp_basicauth_middleware.py @@ -72,6 +72,7 @@ def check_access( if hashed_password != hashed_request_password: return False + # log.debug('%r log in successed', "biliup") return True diff --git a/biliup/web/http_basic_auth.py b/biliup/web/http_basic_auth.py index 649240d..0fda1c1 100644 --- a/biliup/web/http_basic_auth.py +++ b/biliup/web/http_basic_auth.py @@ -3,6 +3,7 @@ HTTP Basic Auth python lib from https://github.com/bugov/http-basic-auth """ import base64 +from typing import Tuple __version__ = '1.2.0' @@ -11,8 +12,7 @@ class BasicAuthException(Exception): """General exception for all http-basic-auth problems """ -# FIXME: 应使用 aes-128-gcm -def parse_token(token: str, coding='utf-8') -> tuple[str, str]: +def parse_token(token: str, coding='utf-8') -> Tuple[str, str]: """Get login + password tuple from Basic Auth token. """ try: @@ -57,7 +57,7 @@ def generate_token(login: str, password: str, coding='utf-8') -> str: return str(b_token, encoding=coding) -def parse_header(header_value: str, coding='utf-8') -> tuple[str, str]: +def parse_header(header_value: str, coding='utf-8') -> Tuple[str, str]: """Get login + password tuple from Basic Auth header value. """ if header_value is None: diff --git a/pyproject.toml b/pyproject.toml index 46352f0..c05682c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "stream-gears >= 0.2.0,<0.3", "psutil >= 5.4.6", "aiofiles >= 22.1.0", - "yt-dlp >= 2024.3.10", + "yt-dlp >= 2024.7.9", "Pillow >= 5.2.0", "aiohttp[speedups]", "aiohttp-cors", @@ -22,13 +22,12 @@ dependencies = [ "httpx[http2] >= 0.27.0", "httpcore > 1.0.3", "PyYAML >= 4.2b1", - "streamlink >= 6.5.0", - "ykdl == 1.8.0; python_version < '3.8'", - "ykdl >= 1.8.0; python_version >= '3.8'", + "streamlink >= 6.8.3", + "ykdl >= 1.8.0", "rsa >= 4.6", - 'importlib-resources; python_version<"3.9"', - 'tomli >= 1.1.0 ; python_version < "3.11"', - 'protobuf >= 4.23.0', + "importlib-resources; python_version < '3.9'", + "tomli >= 1.1.0 ; python_version < '3.11'", + "protobuf >= 4.23.0", "SQLAlchemy>=2.0.0", "alembic>=1.12.1", "m3u8",