feat: custom BASE API URL
This commit is contained in:
parent
a2fe0bf01b
commit
91ccd8c595
@ -1,47 +1,55 @@
|
||||
from abc import ABC
|
||||
import hashlib
|
||||
from urllib.parse import urlencode
|
||||
import logging
|
||||
from abc import ABC
|
||||
from datetime import datetime
|
||||
from typing import Mapping, Dict, Any, Final
|
||||
from typing import Any, Dict, Mapping, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import aiohttp
|
||||
from tenacity import (
|
||||
retry,
|
||||
wait_exponential,
|
||||
stop_after_delay,
|
||||
)
|
||||
from tenacity import retry, stop_after_delay, wait_exponential
|
||||
|
||||
from .typing import QualityNumber, JsonResponse, ResponseData
|
||||
from .exceptions import ApiRequestError
|
||||
|
||||
from .typing import JsonResponse, QualityNumber, ResponseData
|
||||
|
||||
__all__ = 'AppApi', 'WebApi'
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseApi(ABC):
|
||||
def __init__(self, session: aiohttp.ClientSession):
|
||||
base_api_url: str = 'https://api.bilibili.com'
|
||||
base_live_api_url: str = 'https://api.live.bilibili.com'
|
||||
base_play_info_api_url: str = base_live_api_url
|
||||
|
||||
def __init__(
|
||||
self, session: aiohttp.ClientSession, headers: Optional[Dict[str, str]] = None
|
||||
):
|
||||
self._session = session
|
||||
self.headers = headers or {}
|
||||
self.timeout = 10
|
||||
|
||||
@property
|
||||
def headers(self) -> Dict[str, str]:
|
||||
return self._headers
|
||||
|
||||
@headers.setter
|
||||
def headers(self, value: Dict[str, str]) -> None:
|
||||
self._headers = {**value}
|
||||
|
||||
@staticmethod
|
||||
def _check_response(json_res: JsonResponse) -> None:
|
||||
if json_res['code'] != 0:
|
||||
raise ApiRequestError(
|
||||
json_res['code'],
|
||||
json_res.get('message') or json_res.get('msg') or '',
|
||||
json_res['code'], json_res.get('message') or json_res.get('msg') or ''
|
||||
)
|
||||
|
||||
@retry(
|
||||
reraise=True,
|
||||
stop=stop_after_delay(5),
|
||||
wait=wait_exponential(0.1),
|
||||
)
|
||||
async def _get(self, *args: Any, **kwds: Any) -> JsonResponse:
|
||||
@retry(reraise=True, stop=stop_after_delay(5), wait=wait_exponential(0.1))
|
||||
async def _get_json(self, *args: Any, **kwds: Any) -> JsonResponse:
|
||||
async with self._session.get(
|
||||
*args,
|
||||
**kwds,
|
||||
timeout=self.timeout,
|
||||
*args, **kwds, timeout=self.timeout, headers=self.headers
|
||||
) as res:
|
||||
logger.debug(f'real url: {res.real_url}')
|
||||
json_res = await res.json()
|
||||
self._check_response(json_res)
|
||||
return json_res
|
||||
@ -52,12 +60,20 @@ class AppApi(BaseApi):
|
||||
_appkey = '1d8b6e7d45233436'
|
||||
_appsec = '560c52ccd288fed045859ed18bffd973'
|
||||
|
||||
_headers = {
|
||||
_app_headers = {
|
||||
'User-Agent': 'Mozilla/5.0 BiliDroid/6.64.0 (bbcallen@gmail.com) os/android model/Unknown mobi_app/android build/6640400 channel/bili innerVer/6640400 osVer/6.0.1 network/2', # noqa
|
||||
'Connection': 'Keep-Alive',
|
||||
'Accept-Encoding': 'gzip',
|
||||
}
|
||||
|
||||
@property
|
||||
def headers(self) -> Dict[str, str]:
|
||||
return self._headers
|
||||
|
||||
@headers.setter
|
||||
def headers(self, value: Dict[str, str]) -> None:
|
||||
self._headers = {**value, **self._app_headers}
|
||||
|
||||
@classmethod
|
||||
def signed(cls, params: Mapping[str, Any]) -> Dict[str, Any]:
|
||||
if isinstance(params, Mapping):
|
||||
@ -77,111 +93,97 @@ class AppApi(BaseApi):
|
||||
only_video: bool = False,
|
||||
only_audio: bool = False,
|
||||
) -> ResponseData:
|
||||
url = 'https://api.live.bilibili.com/xlive/app-room/v2/index/getRoomPlayInfo' # noqa
|
||||
|
||||
params = self.signed({
|
||||
'actionKey': 'appkey',
|
||||
'build': '6640400',
|
||||
'channel': 'bili',
|
||||
'codec': '0,1', # 0: avc, 1: hevc
|
||||
'device': 'android',
|
||||
'device_name': 'Unknown',
|
||||
'disable_rcmd': '0',
|
||||
'dolby': '1',
|
||||
'format': '0,1,2', # 0: flv, 1: ts, 2: fmp4
|
||||
'free_type': '0',
|
||||
'http': '1',
|
||||
'mask': '0',
|
||||
'mobi_app': 'android',
|
||||
'need_hdr': '0',
|
||||
'no_playurl': '0',
|
||||
'only_audio': '1' if only_audio else '0',
|
||||
'only_video': '1' if only_video else '0',
|
||||
'platform': 'android',
|
||||
'play_type': '0',
|
||||
'protocol': '0,1',
|
||||
'qn': qn,
|
||||
'room_id': room_id,
|
||||
'ts': int(datetime.utcnow().timestamp()),
|
||||
})
|
||||
|
||||
r = await self._get(url, params=params, headers=self._headers)
|
||||
url = self.base_play_info_api_url + '/xlive/app-room/v2/index/getRoomPlayInfo'
|
||||
params = self.signed(
|
||||
{
|
||||
'actionKey': 'appkey',
|
||||
'build': '6640400',
|
||||
'channel': 'bili',
|
||||
'codec': '0,1', # 0: avc, 1: hevc
|
||||
'device': 'android',
|
||||
'device_name': 'Unknown',
|
||||
'disable_rcmd': '0',
|
||||
'dolby': '1',
|
||||
'format': '0,1,2', # 0: flv, 1: ts, 2: fmp4
|
||||
'free_type': '0',
|
||||
'http': '1',
|
||||
'mask': '0',
|
||||
'mobi_app': 'android',
|
||||
'need_hdr': '0',
|
||||
'no_playurl': '0',
|
||||
'only_audio': '1' if only_audio else '0',
|
||||
'only_video': '1' if only_video else '0',
|
||||
'platform': 'android',
|
||||
'play_type': '0',
|
||||
'protocol': '0,1',
|
||||
'qn': qn,
|
||||
'room_id': room_id,
|
||||
'ts': int(datetime.utcnow().timestamp()),
|
||||
}
|
||||
)
|
||||
r = await self._get_json(url, params=params, headers=self._headers)
|
||||
return r['data']
|
||||
|
||||
async def get_info_by_room(self, room_id: int) -> ResponseData:
|
||||
url = 'https://api.live.bilibili.com/xlive/app-room/v1/index/getInfoByRoom' # noqa
|
||||
|
||||
params = self.signed({
|
||||
'actionKey': 'appkey',
|
||||
'build': '6640400',
|
||||
'channel': 'bili',
|
||||
'device': 'android',
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
'room_id': room_id,
|
||||
'ts': int(datetime.utcnow().timestamp()),
|
||||
})
|
||||
|
||||
r = await self._get(url, params=params)
|
||||
url = self.base_live_api_url + '/xlive/app-room/v1/index/getInfoByRoom'
|
||||
params = self.signed(
|
||||
{
|
||||
'actionKey': 'appkey',
|
||||
'build': '6640400',
|
||||
'channel': 'bili',
|
||||
'device': 'android',
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
'room_id': room_id,
|
||||
'ts': int(datetime.utcnow().timestamp()),
|
||||
}
|
||||
)
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
|
||||
async def get_user_info(self, uid: int) -> ResponseData:
|
||||
url = 'https://app.bilibili.com/x/v2/space'
|
||||
|
||||
params = self.signed({
|
||||
'build': '6640400',
|
||||
'channel': 'bili',
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
'ts': int(datetime.utcnow().timestamp()),
|
||||
'vmid': uid,
|
||||
})
|
||||
|
||||
r = await self._get(url, params=params)
|
||||
url = self.base_api_url + '/x/v2/space'
|
||||
params = self.signed(
|
||||
{
|
||||
'build': '6640400',
|
||||
'channel': 'bili',
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
'ts': int(datetime.utcnow().timestamp()),
|
||||
'vmid': uid,
|
||||
}
|
||||
)
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
|
||||
async def get_danmu_info(self, room_id: int) -> ResponseData:
|
||||
url = 'https://api.live.bilibili.com/xlive/app-room/v1/index/getDanmuInfo' # noqa
|
||||
|
||||
params = self.signed({
|
||||
'actionKey': 'appkey',
|
||||
'build': '6640400',
|
||||
'channel': 'bili',
|
||||
'device': 'android',
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
'room_id': room_id,
|
||||
'ts': int(datetime.utcnow().timestamp()),
|
||||
})
|
||||
|
||||
r = await self._get(url, params=params)
|
||||
url = self.base_live_api_url + '/xlive/app-room/v1/index/getDanmuInfo'
|
||||
params = self.signed(
|
||||
{
|
||||
'actionKey': 'appkey',
|
||||
'build': '6640400',
|
||||
'channel': 'bili',
|
||||
'device': 'android',
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
'room_id': room_id,
|
||||
'ts': int(datetime.utcnow().timestamp()),
|
||||
}
|
||||
)
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
|
||||
|
||||
class WebApi(BaseApi):
|
||||
BASE_API_URL: Final[str] = 'https://api.bilibili.com'
|
||||
BASE_LIVE_API_URL: Final[str] = 'https://api.live.bilibili.com'
|
||||
|
||||
GET_USER_INFO_URL: Final[str] = BASE_API_URL + '/x/space/acc/info'
|
||||
|
||||
GET_DANMU_INFO_URL: Final[str] = BASE_LIVE_API_URL + \
|
||||
'/xlive/web-room/v1/index/getDanmuInfo'
|
||||
ROOM_INIT_URL: Final[str] = BASE_LIVE_API_URL + '/room/v1/Room/room_init'
|
||||
GET_INFO_URL: Final[str] = BASE_LIVE_API_URL + '/room/v1/Room/get_info'
|
||||
GET_INFO_BY_ROOM_URL: Final[str] = BASE_LIVE_API_URL + \
|
||||
'/xlive/web-room/v1/index/getInfoByRoom'
|
||||
GET_ROOM_PLAY_INFO_URL: Final[str] = BASE_LIVE_API_URL + \
|
||||
'/xlive/web-room/v2/index/getRoomPlayInfo'
|
||||
GET_TIMESTAMP_URL: Final[str] = BASE_LIVE_API_URL + \
|
||||
'/av/v1/Time/getTimestamp?platform=pc'
|
||||
|
||||
async def room_init(self, room_id: int) -> ResponseData:
|
||||
r = await self._get(self.ROOM_INIT_URL, params={'id': room_id})
|
||||
url = self.base_live_api_url + '/room/v1/Room/room_init'
|
||||
r = await self._get_json(url, params={'id': room_id})
|
||||
return r['data']
|
||||
|
||||
async def get_room_play_info(
|
||||
self, room_id: int, qn: QualityNumber = 10000
|
||||
) -> ResponseData:
|
||||
url = self.base_play_info_api_url + '/xlive/web-room/v2/index/getRoomPlayInfo'
|
||||
params = {
|
||||
'room_id': room_id,
|
||||
'protocol': '0,1',
|
||||
@ -191,37 +193,34 @@ class WebApi(BaseApi):
|
||||
'platform': 'web',
|
||||
'ptype': 8,
|
||||
}
|
||||
r = await self._get(self.GET_ROOM_PLAY_INFO_URL, params=params)
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
|
||||
async def get_info_by_room(self, room_id: int) -> ResponseData:
|
||||
params = {
|
||||
'room_id': room_id,
|
||||
}
|
||||
r = await self._get(self.GET_INFO_BY_ROOM_URL, params=params)
|
||||
url = self.base_live_api_url + '/xlive/web-room/v1/index/getInfoByRoom'
|
||||
params = {'room_id': room_id}
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
|
||||
async def get_info(self, room_id: int) -> ResponseData:
|
||||
params = {
|
||||
'room_id': room_id,
|
||||
}
|
||||
r = await self._get(self.GET_INFO_URL, params=params)
|
||||
url = self.base_live_api_url + '/room/v1/Room/get_info'
|
||||
params = {'room_id': room_id}
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
|
||||
async def get_timestamp(self) -> int:
|
||||
r = await self._get(self.GET_TIMESTAMP_URL)
|
||||
url = self.base_live_api_url + '/av/v1/Time/getTimestamp?platform=pc'
|
||||
r = await self._get_json(url)
|
||||
return r['data']['timestamp']
|
||||
|
||||
async def get_user_info(self, uid: int) -> ResponseData:
|
||||
params = {
|
||||
'mid': uid,
|
||||
}
|
||||
r = await self._get(self.GET_USER_INFO_URL, params=params)
|
||||
url = self.base_api_url + '/x/space/acc/info'
|
||||
params = {'mid': uid}
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
|
||||
async def get_danmu_info(self, room_id: int) -> ResponseData:
|
||||
params = {
|
||||
'id': room_id,
|
||||
}
|
||||
r = await self._get(self.GET_DANMU_INFO_URL, params=params)
|
||||
url = self.base_live_api_url + '/xlive/web-room/v1/index/getDanmuInfo'
|
||||
params = {'id': room_id}
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
|
@ -53,12 +53,14 @@ class DanmakuClient(EventEmitter[DanmakuListener], AsyncStoppableMixin):
|
||||
room_id: int,
|
||||
*,
|
||||
max_retries: int = 10,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.session = session
|
||||
self.appapi = appapi
|
||||
self.webapi = webapi
|
||||
self._room_id = room_id
|
||||
self.headers = headers or {}
|
||||
|
||||
self._api_platform: ApiPlatform = 'web'
|
||||
self._danmu_info: Dict[str, Any] = COMMON_DANMU_INFO
|
||||
@ -66,6 +68,14 @@ class DanmakuClient(EventEmitter[DanmakuListener], AsyncStoppableMixin):
|
||||
self._retry_delay: int = 0
|
||||
self._MAX_RETRIES: Final[int] = max_retries
|
||||
|
||||
@property
|
||||
def headers(self) -> Dict[str, str]:
|
||||
return self._headers
|
||||
|
||||
@headers.setter
|
||||
def headers(self, value: Dict[str, str]) -> None:
|
||||
self._headers = {**value, 'Connection': 'Upgrade'}
|
||||
|
||||
async def _do_start(self) -> None:
|
||||
await self._update_danmu_info()
|
||||
await self._connect()
|
||||
@ -77,6 +87,12 @@ class DanmakuClient(EventEmitter[DanmakuListener], AsyncStoppableMixin):
|
||||
await self._disconnect()
|
||||
logger.debug('Stopped danmaku client')
|
||||
|
||||
async def restart(self) -> None:
|
||||
logger.debug('Restarting danmaku client...')
|
||||
await self.stop()
|
||||
await self.start()
|
||||
logger.debug('Restarted danmaku client')
|
||||
|
||||
async def reconnect(self) -> None:
|
||||
if self.stopped:
|
||||
return
|
||||
@ -117,7 +133,9 @@ class DanmakuClient(EventEmitter[DanmakuListener], AsyncStoppableMixin):
|
||||
)
|
||||
logger.debug(f'Connecting WebSocket... {url}')
|
||||
try:
|
||||
self._ws = await self.session.ws_connect(url, timeout=5)
|
||||
self._ws = await self.session.ws_connect(
|
||||
url, timeout=5, headers=self.headers
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.debug(f'Failed to connect WebSocket: {repr(exc)}')
|
||||
raise
|
||||
|
@ -40,9 +40,44 @@ class Live:
|
||||
self._cookie = cookie
|
||||
self._html_page_url = f'https://live.bilibili.com/{room_id}'
|
||||
|
||||
self._session = aiohttp.ClientSession(
|
||||
connector=aiohttp.TCPConnector(limit=200),
|
||||
raise_for_status=True,
|
||||
trust_env=True,
|
||||
)
|
||||
self._appapi = AppApi(self._session, self.headers)
|
||||
self._webapi = WebApi(self._session, self.headers)
|
||||
|
||||
self._room_info: RoomInfo
|
||||
self._user_info: UserInfo
|
||||
|
||||
@property
|
||||
def base_api_url(self) -> str:
|
||||
return self._webapi.base_api_url
|
||||
|
||||
@base_api_url.setter
|
||||
def base_api_url(self, value: str) -> None:
|
||||
self._webapi.base_api_url = value
|
||||
self._appapi.base_api_url = value
|
||||
|
||||
@property
|
||||
def base_live_api_url(self) -> str:
|
||||
return self._webapi.base_live_api_url
|
||||
|
||||
@base_live_api_url.setter
|
||||
def base_live_api_url(self, value: str) -> None:
|
||||
self._webapi.base_live_api_url = value
|
||||
self._appapi.base_live_api_url = value
|
||||
|
||||
@property
|
||||
def base_play_info_api_url(self) -> str:
|
||||
return self._webapi.base_play_info_api_url
|
||||
|
||||
@base_play_info_api_url.setter
|
||||
def base_play_info_api_url(self, value: str) -> None:
|
||||
self._webapi.base_play_info_api_url = value
|
||||
self._appapi.base_play_info_api_url = value
|
||||
|
||||
@property
|
||||
def user_agent(self) -> str:
|
||||
return self._user_agent
|
||||
@ -50,6 +85,8 @@ class Live:
|
||||
@user_agent.setter
|
||||
def user_agent(self, value: str) -> None:
|
||||
self._user_agent = value
|
||||
self._webapi.headers = self.headers
|
||||
self._appapi.headers = self.headers
|
||||
|
||||
@property
|
||||
def cookie(self) -> str:
|
||||
@ -58,15 +95,25 @@ class Live:
|
||||
@cookie.setter
|
||||
def cookie(self, value: str) -> None:
|
||||
self._cookie = value
|
||||
self._webapi.headers = self.headers
|
||||
self._appapi.headers = self.headers
|
||||
|
||||
@property
|
||||
def headers(self) -> Dict[str, str]:
|
||||
return {
|
||||
'Referer': 'https://live.bilibili.com/',
|
||||
'Connection': 'Keep-Alive',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en;q=0.3,en-US;q=0.2',
|
||||
'Referer': f'https://live.bilibili.com/{self._room_id}',
|
||||
'Origin': 'https://live.bilibili.com',
|
||||
'Connection': 'keep-alive',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-site',
|
||||
'Pragma': 'no-cache',
|
||||
'Cache-Control': 'no-cache',
|
||||
'User-Agent': self._user_agent,
|
||||
'Cookie': self._cookie,
|
||||
'Accept-Encoding': 'gzip',
|
||||
}
|
||||
|
||||
@property
|
||||
@ -94,15 +141,6 @@ class Live:
|
||||
return self._user_info
|
||||
|
||||
async def init(self) -> None:
|
||||
self._session = aiohttp.ClientSession(
|
||||
connector=aiohttp.TCPConnector(limit=200),
|
||||
headers=self.headers,
|
||||
raise_for_status=True,
|
||||
trust_env=True,
|
||||
)
|
||||
self._appapi = AppApi(self._session)
|
||||
self._webapi = WebApi(self._session)
|
||||
|
||||
self._room_info = await self.get_room_info()
|
||||
self._user_info = await self.get_user_info(self._room_info.uid)
|
||||
|
||||
|
1
src/blrec/data/webapp/170.d0e14a28ee578d1f.js
Normal file
1
src/blrec/data/webapp/170.d0e14a28ee578d1f.js
Normal file
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/183.0d3cd9f454be16fb.js
Normal file
1
src/blrec/data/webapp/183.0d3cd9f454be16fb.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/91.9ff409a090dace5c.js
Normal file
1
src/blrec/data/webapp/91.9ff409a090dace5c.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -10,6 +10,6 @@
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||
<script src="runtime.68e08c4d681726f6.js" type="module"></script><script src="polyfills.4b08448aee19bb22.js" type="module"></script><script src="main.16a8fc7b1f8a870d.js" type="module"></script>
|
||||
<script src="runtime.4ae765ab3bddf383.js" type="module"></script><script src="polyfills.4b08448aee19bb22.js" type="module"></script><script src="main.27d1fff16f7909f2.js" type="module"></script>
|
||||
|
||||
</body></html>
|
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
{
|
||||
"configVersion": 1,
|
||||
"timestamp": 1659243718041,
|
||||
"timestamp": 1661135687643,
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
@ -13,16 +13,16 @@
|
||||
"urls": [
|
||||
"/103.5b5d2a6e5a8a7479.js",
|
||||
"/146.5a8902910bda9e87.js",
|
||||
"/183.2c7c85597ba82f9e.js",
|
||||
"/170.d0e14a28ee578d1f.js",
|
||||
"/183.0d3cd9f454be16fb.js",
|
||||
"/45.c90c3cea2bf1a66e.js",
|
||||
"/500.5d39ab52fb714a12.js",
|
||||
"/91.be3cbd4101dc7500.js",
|
||||
"/91.9ff409a090dace5c.js",
|
||||
"/common.858f777e9296e6f2.js",
|
||||
"/index.html",
|
||||
"/main.16a8fc7b1f8a870d.js",
|
||||
"/main.27d1fff16f7909f2.js",
|
||||
"/manifest.webmanifest",
|
||||
"/polyfills.4b08448aee19bb22.js",
|
||||
"/runtime.68e08c4d681726f6.js",
|
||||
"/runtime.4ae765ab3bddf383.js",
|
||||
"/styles.2e152d608221c2ee.css"
|
||||
],
|
||||
"patterns": []
|
||||
@ -1636,10 +1636,10 @@
|
||||
"hashTable": {
|
||||
"/103.5b5d2a6e5a8a7479.js": "cc0240f217015b6d4ddcc14f31fcc42e1c1c282a",
|
||||
"/146.5a8902910bda9e87.js": "d9c33c7073662699f00f46f3a384ae5b749fdef9",
|
||||
"/183.2c7c85597ba82f9e.js": "22a1524d6399d9bde85334a2eba15670f68ccd96",
|
||||
"/170.d0e14a28ee578d1f.js": "d6b6208ca442565ed39300b27ab8cbe5501cb46a",
|
||||
"/183.0d3cd9f454be16fb.js": "e7e6ebc715791102fd09edabe2aa47316208b29c",
|
||||
"/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764",
|
||||
"/500.5d39ab52fb714a12.js": "646fbfd3af1124519171f1cd9fac4c214b5af60f",
|
||||
"/91.be3cbd4101dc7500.js": "f0fec71455c96f9a60c4fa671d2ccdba07e9a00a",
|
||||
"/91.9ff409a090dace5c.js": "d756ffe7cd3f5516e40a7e6d6cf494ea6213a546",
|
||||
"/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1",
|
||||
"/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1",
|
||||
"/assets/fill/.gitkeep": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||
@ -3234,11 +3234,11 @@
|
||||
"/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01",
|
||||
"/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068",
|
||||
"/common.858f777e9296e6f2.js": "b68ca68e1e214a2537d96935c23410126cc564dd",
|
||||
"/index.html": "3f28dbdfc92c1a0930448a8ff6d5d2ac49648987",
|
||||
"/main.16a8fc7b1f8a870d.js": "9c680888ae14907d6c20e60c026b49a2331768e9",
|
||||
"/index.html": "29167783eb093ffa93369f741a5ce20a534137de",
|
||||
"/main.27d1fff16f7909f2.js": "22e63726601a31af1a96e7901afc0d2bea7fd414",
|
||||
"/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586",
|
||||
"/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d",
|
||||
"/runtime.68e08c4d681726f6.js": "04815a3dd35466f647f3707a295bc2c76c9f0375",
|
||||
"/runtime.4ae765ab3bddf383.js": "96653fd35d3ad9684e603011436e9d43a1121690",
|
||||
"/styles.2e152d608221c2ee.css": "9830389a46daa5b4511e0dd343aad23ca9f9690f"
|
||||
},
|
||||
"navigationUrls": [
|
||||
|
@ -1 +1 @@
|
||||
(()=>{"use strict";var e,v={},m={};function r(e){var i=m[e];if(void 0!==i)return i.exports;var t=m[e]={exports:{}};return v[e].call(t.exports,t,t.exports,r),t.exports}r.m=v,e=[],r.O=(i,t,o,f)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,o,f]=e[n],c=!0,l=0;l<t.length;l++)(!1&f||a>=f)&&Object.keys(r.O).every(b=>r.O[b](t[l]))?t.splice(l--,1):(c=!1,f<a&&(a=f));if(c){e.splice(n--,1);var d=o();void 0!==d&&(i=d)}}return i}f=f||0;for(var n=e.length;n>0&&e[n-1][2]>f;n--)e[n]=e[n-1];e[n]=[t,o,f]},r.n=e=>{var i=e&&e.__esModule?()=>e.default:()=>e;return r.d(i,{a:i}),i},r.d=(e,i)=>{for(var t in i)r.o(i,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:i[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((i,t)=>(r.f[t](e,i),i),[])),r.u=e=>(592===e?"common":e)+"."+{45:"c90c3cea2bf1a66e",91:"be3cbd4101dc7500",103:"5b5d2a6e5a8a7479",146:"5a8902910bda9e87",183:"2c7c85597ba82f9e",500:"5d39ab52fb714a12",592:"858f777e9296e6f2"}[e]+".js",r.miniCssF=e=>{},r.o=(e,i)=>Object.prototype.hasOwnProperty.call(e,i),(()=>{var e={},i="blrec:";r.l=(t,o,f,n)=>{if(e[t])e[t].push(o);else{var a,c;if(void 0!==f)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==i+f){a=u;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",i+f),a.src=r.tu(t)),e[t]=[o];var s=(g,b)=>{a.onerror=a.onload=null,clearTimeout(p);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(b)),g)return g(b)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tu=i=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(i))})(),r.p="",(()=>{var e={666:0};r.f.j=(o,f)=>{var n=r.o(e,o)?e[o]:void 0;if(0!==n)if(n)f.push(n[2]);else if(666!=o){var a=new Promise((u,s)=>n=e[o]=[u,s]);f.push(n[2]=a);var c=r.p+r.u(o),l=new Error;r.l(c,u=>{if(r.o(e,o)&&(0!==(n=e[o])&&(e[o]=void 0),n)){var s=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;l.message="Loading chunk "+o+" failed.\n("+s+": "+p+")",l.name="ChunkLoadError",l.type=s,l.request=p,n[1](l)}},"chunk-"+o,o)}else e[o]=0},r.O.j=o=>0===e[o];var i=(o,f)=>{var l,d,[n,a,c]=f,u=0;if(n.some(p=>0!==e[p])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(c)var s=c(r)}for(o&&o(f);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[n[u]]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(i.bind(null,0)),t.push=i.bind(null,t.push.bind(t))})()})();
|
||||
(()=>{"use strict";var e,v={},m={};function r(e){var i=m[e];if(void 0!==i)return i.exports;var t=m[e]={exports:{}};return v[e].call(t.exports,t,t.exports,r),t.exports}r.m=v,e=[],r.O=(i,t,o,f)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,o,f]=e[n],c=!0,l=0;l<t.length;l++)(!1&f||a>=f)&&Object.keys(r.O).every(p=>r.O[p](t[l]))?t.splice(l--,1):(c=!1,f<a&&(a=f));if(c){e.splice(n--,1);var d=o();void 0!==d&&(i=d)}}return i}f=f||0;for(var n=e.length;n>0&&e[n-1][2]>f;n--)e[n]=e[n-1];e[n]=[t,o,f]},r.n=e=>{var i=e&&e.__esModule?()=>e.default:()=>e;return r.d(i,{a:i}),i},r.d=(e,i)=>{for(var t in i)r.o(i,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:i[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((i,t)=>(r.f[t](e,i),i),[])),r.u=e=>(592===e?"common":e)+"."+{45:"c90c3cea2bf1a66e",91:"9ff409a090dace5c",103:"5b5d2a6e5a8a7479",146:"5a8902910bda9e87",170:"d0e14a28ee578d1f",183:"0d3cd9f454be16fb",592:"858f777e9296e6f2"}[e]+".js",r.miniCssF=e=>{},r.o=(e,i)=>Object.prototype.hasOwnProperty.call(e,i),(()=>{var e={},i="blrec:";r.l=(t,o,f,n)=>{if(e[t])e[t].push(o);else{var a,c;if(void 0!==f)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==i+f){a=u;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",i+f),a.src=r.tu(t)),e[t]=[o];var s=(g,p)=>{a.onerror=a.onload=null,clearTimeout(b);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(p)),g)return g(p)},b=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tu=i=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(i))})(),r.p="",(()=>{var e={666:0};r.f.j=(o,f)=>{var n=r.o(e,o)?e[o]:void 0;if(0!==n)if(n)f.push(n[2]);else if(666!=o){var a=new Promise((u,s)=>n=e[o]=[u,s]);f.push(n[2]=a);var c=r.p+r.u(o),l=new Error;r.l(c,u=>{if(r.o(e,o)&&(0!==(n=e[o])&&(e[o]=void 0),n)){var s=u&&("load"===u.type?"missing":u.type),b=u&&u.target&&u.target.src;l.message="Loading chunk "+o+" failed.\n("+s+": "+b+")",l.name="ChunkLoadError",l.type=s,l.request=b,n[1](l)}},"chunk-"+o,o)}else e[o]=0},r.O.j=o=>0===e[o];var i=(o,f)=>{var l,d,[n,a,c]=f,u=0;if(n.some(b=>0!==e[b])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(c)var s=c(r)}for(o&&o(f);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[n[u]]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(i.bind(null,0)),t.push=i.bind(null,t.push.bind(t))})()})();
|
@ -7,6 +7,8 @@ from .models import (
|
||||
EmailNotificationSettings,
|
||||
EmailSettings,
|
||||
EnvSettings,
|
||||
BiliApiOptions,
|
||||
BiliApiSettings,
|
||||
HeaderOptions,
|
||||
HeaderSettings,
|
||||
LoggingSettings,
|
||||
@ -45,6 +47,8 @@ __all__ = (
|
||||
'Settings',
|
||||
'SettingsIn',
|
||||
'SettingsOut',
|
||||
'BiliApiOptions',
|
||||
'BiliApiSettings',
|
||||
'HeaderOptions',
|
||||
'HeaderSettings',
|
||||
'DanmakuOptions',
|
||||
|
@ -35,6 +35,8 @@ __all__ = (
|
||||
'Settings',
|
||||
'SettingsIn',
|
||||
'SettingsOut',
|
||||
'BiliApiOptions',
|
||||
'BiliApiSettings',
|
||||
'HeaderOptions',
|
||||
'HeaderSettings',
|
||||
'DanmakuOptions',
|
||||
@ -110,6 +112,18 @@ class BaseModel(PydanticBaseModel):
|
||||
)
|
||||
|
||||
|
||||
class BiliApiOptions(BaseModel):
|
||||
base_api_url: Optional[str]
|
||||
base_live_api_url: Optional[str]
|
||||
base_play_info_api_url: Optional[str]
|
||||
|
||||
|
||||
class BiliApiSettings(BiliApiOptions):
|
||||
base_api_url: str = 'https://api.bilibili.com'
|
||||
base_live_api_url: str = 'https://api.live.bilibili.com'
|
||||
base_play_info_api_url: str = base_live_api_url
|
||||
|
||||
|
||||
class HeaderOptions(BaseModel):
|
||||
user_agent: Optional[str]
|
||||
cookie: Optional[str]
|
||||
@ -285,6 +299,7 @@ class OutputSettings(OutputOptions):
|
||||
|
||||
class TaskOptions(BaseModel):
|
||||
output: OutputOptions = OutputOptions()
|
||||
bili_api: BiliApiOptions = BiliApiOptions()
|
||||
header: HeaderOptions = HeaderOptions()
|
||||
danmaku: DanmakuOptions = DanmakuOptions()
|
||||
recorder: RecorderOptions = RecorderOptions()
|
||||
@ -294,7 +309,14 @@ class TaskOptions(BaseModel):
|
||||
def from_settings(cls, settings: TaskSettings) -> TaskOptions:
|
||||
return cls(
|
||||
**settings.dict(
|
||||
include={'output', 'header', 'danmaku', 'recorder', 'postprocessing'}
|
||||
include={
|
||||
'output',
|
||||
'bili_api',
|
||||
'header',
|
||||
'danmaku',
|
||||
'recorder',
|
||||
'postprocessing',
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@ -587,6 +609,7 @@ class Settings(BaseModel):
|
||||
tasks: Annotated[List[TaskSettings], Field(max_items=100)] = []
|
||||
output: OutputSettings = OutputSettings() # type: ignore
|
||||
logging: LoggingSettings = LoggingSettings() # type: ignore
|
||||
bili_api: BiliApiSettings = BiliApiSettings()
|
||||
header: HeaderSettings = HeaderSettings()
|
||||
danmaku: DanmakuSettings = DanmakuSettings()
|
||||
recorder: RecorderSettings = RecorderSettings()
|
||||
@ -636,6 +659,7 @@ class Settings(BaseModel):
|
||||
class SettingsIn(BaseModel):
|
||||
output: Optional[OutputSettings] = None
|
||||
logging: Optional[LoggingSettings] = None
|
||||
bili_api: Optional[BiliApiSettings] = None
|
||||
header: Optional[HeaderSettings] = None
|
||||
danmaku: Optional[DanmakuSettings] = None
|
||||
recorder: Optional[RecorderSettings] = None
|
||||
|
@ -16,6 +16,7 @@ from ..notification import (
|
||||
from ..webhook import WebHook
|
||||
from .helpers import shadow_settings, update_settings
|
||||
from .models import (
|
||||
BiliApiOptions,
|
||||
DanmakuOptions,
|
||||
HeaderOptions,
|
||||
MessageTemplateSettings,
|
||||
@ -210,13 +211,24 @@ class SettingsManager:
|
||||
settings.enable_recorder = False
|
||||
await self.dump_settings()
|
||||
|
||||
def apply_task_bili_api_settings(
|
||||
self, room_id: int, options: BiliApiOptions
|
||||
) -> None:
|
||||
final_settings = self._settings.bili_api.copy()
|
||||
shadow_settings(options, final_settings)
|
||||
self._app._task_manager.apply_task_bili_api_settings(room_id, final_settings)
|
||||
|
||||
async def apply_task_header_settings(
|
||||
self, room_id: int, options: HeaderOptions, *, update_session: bool = True
|
||||
self,
|
||||
room_id: int,
|
||||
options: HeaderOptions,
|
||||
*,
|
||||
restart_danmaku_client: bool = True,
|
||||
) -> None:
|
||||
final_settings = self._settings.header.copy()
|
||||
shadow_settings(options, final_settings)
|
||||
await self._app._task_manager.apply_task_header_settings(
|
||||
room_id, final_settings, update_session=update_session
|
||||
room_id, final_settings, restart_danmaku_client=restart_danmaku_client
|
||||
)
|
||||
|
||||
def apply_task_danmaku_settings(
|
||||
@ -264,6 +276,10 @@ class SettingsManager:
|
||||
backup_count=self._settings.logging.backup_count,
|
||||
)
|
||||
|
||||
def apply_bili_api_settings(self) -> None:
|
||||
for settings in self._settings.tasks:
|
||||
self.apply_task_bili_api_settings(settings.room_id, settings.bili_api)
|
||||
|
||||
async def apply_header_settings(self) -> None:
|
||||
for settings in self._settings.tasks:
|
||||
await self.apply_task_header_settings(settings.room_id, settings.header)
|
||||
|
@ -19,6 +19,7 @@ KeyOfSettings = Literal[
|
||||
'tasks',
|
||||
'output',
|
||||
'logging',
|
||||
'bili_api',
|
||||
'header',
|
||||
'danmaku',
|
||||
'recorder',
|
||||
|
@ -50,6 +50,10 @@ class TaskParam:
|
||||
path_template: str
|
||||
filesize_limit: int
|
||||
duration_limit: int
|
||||
# BiliApiSettings
|
||||
base_api_url: str
|
||||
base_live_api_url: str
|
||||
base_play_info_api_url: str
|
||||
# HeaderSettings
|
||||
user_agent: str
|
||||
cookie: str
|
||||
|
@ -190,6 +190,30 @@ class RecordTask:
|
||||
|
||||
yield DanmakuFileDetail(path=path, size=size, status=status)
|
||||
|
||||
@property
|
||||
def base_api_url(self) -> str:
|
||||
return self._live.base_api_url
|
||||
|
||||
@base_api_url.setter
|
||||
def base_api_url(self, value: str) -> None:
|
||||
self._live.base_api_url = value
|
||||
|
||||
@property
|
||||
def base_live_api_url(self) -> str:
|
||||
return self._live.base_live_api_url
|
||||
|
||||
@base_live_api_url.setter
|
||||
def base_live_api_url(self, value: str) -> None:
|
||||
self._live.base_live_api_url = value
|
||||
|
||||
@property
|
||||
def base_play_info_api_url(self) -> str:
|
||||
return self._live.base_play_info_api_url
|
||||
|
||||
@base_play_info_api_url.setter
|
||||
def base_play_info_api_url(self, value: str) -> None:
|
||||
self._live.base_play_info_api_url = value
|
||||
|
||||
@property
|
||||
def user_agent(self) -> str:
|
||||
return self._live.user_agent
|
||||
@ -460,22 +484,13 @@ class RecordTask:
|
||||
await self._recorder.stop()
|
||||
await self._postprocessor.stop()
|
||||
|
||||
@aio_task_with_room_id
|
||||
async def update_info(self, raise_exception: bool = False) -> bool:
|
||||
return await self._live.update_info(raise_exception=raise_exception)
|
||||
|
||||
@aio_task_with_room_id
|
||||
async def update_session(self) -> None:
|
||||
if self._monitor_enabled:
|
||||
await self._danmaku_client.stop()
|
||||
|
||||
await self._live.deinit()
|
||||
await self._live.init()
|
||||
self._danmaku_client.session = self._live.session
|
||||
self._danmaku_client.appapi = self._live.appapi
|
||||
self._danmaku_client.webapi = self._live.webapi
|
||||
|
||||
if self._monitor_enabled:
|
||||
await self._danmaku_client.start()
|
||||
async def restart_danmaku_client(self) -> None:
|
||||
await self._danmaku_client.restart()
|
||||
|
||||
async def _setup(self) -> None:
|
||||
self._setup_danmaku_client()
|
||||
@ -488,7 +503,11 @@ class RecordTask:
|
||||
|
||||
def _setup_danmaku_client(self) -> None:
|
||||
self._danmaku_client = DanmakuClient(
|
||||
self._live.session, self._live.appapi, self._live.webapi, self._live.room_id
|
||||
self._live.session,
|
||||
self._live.appapi,
|
||||
self._live.webapi,
|
||||
self._live.room_id,
|
||||
headers=self._live.headers,
|
||||
)
|
||||
|
||||
def _setup_live_monitor(self) -> None:
|
||||
|
@ -18,6 +18,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from ..setting import (
|
||||
DanmakuSettings,
|
||||
BiliApiSettings,
|
||||
HeaderSettings,
|
||||
OutputSettings,
|
||||
PostprocessingSettings,
|
||||
@ -76,8 +77,11 @@ class RecordTaskManager:
|
||||
self._tasks[settings.room_id] = task
|
||||
|
||||
try:
|
||||
self._settings_manager.apply_task_bili_api_settings(
|
||||
settings.room_id, settings.bili_api
|
||||
)
|
||||
await self._settings_manager.apply_task_header_settings(
|
||||
settings.room_id, settings.header, update_session=False
|
||||
settings.room_id, settings.header, restart_danmaku_client=False
|
||||
)
|
||||
await task.setup()
|
||||
|
||||
@ -222,21 +226,29 @@ class RecordTaskManager:
|
||||
if coros:
|
||||
await asyncio.wait(coros)
|
||||
|
||||
async def apply_task_header_settings(
|
||||
self, room_id: int, settings: HeaderSettings, *, update_session: bool = True
|
||||
def apply_task_bili_api_settings(
|
||||
self, room_id: int, settings: BiliApiSettings
|
||||
) -> None:
|
||||
task = self._get_task(room_id)
|
||||
task.base_api_url = settings.base_api_url
|
||||
task.base_live_api_url = settings.base_live_api_url
|
||||
task.base_play_info_api_url = settings.base_play_info_api_url
|
||||
|
||||
async def apply_task_header_settings(
|
||||
self,
|
||||
room_id: int,
|
||||
settings: HeaderSettings,
|
||||
*,
|
||||
restart_danmaku_client: bool = True,
|
||||
) -> None:
|
||||
task = self._get_task(room_id)
|
||||
# avoid unnecessary updates that will interrupt connections
|
||||
if task.user_agent == settings.user_agent and task.cookie == settings.cookie:
|
||||
return
|
||||
|
||||
task.user_agent = settings.user_agent
|
||||
task.cookie = settings.cookie
|
||||
|
||||
if update_session:
|
||||
# update task session to take the effect
|
||||
await task.update_session()
|
||||
if restart_danmaku_client:
|
||||
await task.restart_danmaku_client()
|
||||
|
||||
def apply_task_output_settings(
|
||||
self, room_id: int, settings: OutputSettings
|
||||
@ -296,6 +308,9 @@ class RecordTaskManager:
|
||||
path_template=task.path_template,
|
||||
filesize_limit=task.filesize_limit,
|
||||
duration_limit=task.duration_limit,
|
||||
base_api_url=task.base_api_url,
|
||||
base_live_api_url=task.base_live_api_url,
|
||||
base_play_info_api_url=task.base_play_info_api_url,
|
||||
user_agent=task.user_agent,
|
||||
cookie=task.cookie,
|
||||
danmu_uname=task.danmu_uname,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
import os
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Awaitable, TypeVar, final
|
||||
|
||||
from blrec.logging.room_id import aio_task_with_room_id
|
||||
|
||||
|
||||
class SwitchableMixin(ABC):
|
||||
def __init__(self) -> None:
|
||||
@ -127,21 +129,25 @@ class AsyncCooperationMixin(ABC):
|
||||
# call submit_exception in a coroutine
|
||||
# workaround for `RuntimeError: no running event loop`
|
||||
submit_exception(exc)
|
||||
|
||||
self._run_coroutine(wrapper())
|
||||
|
||||
def _run_coroutine(self, coro: Awaitable[_T]) -> _T:
|
||||
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
||||
future = asyncio.run_coroutine_threadsafe(self._with_room_id(coro), self._loop)
|
||||
return future.result()
|
||||
|
||||
@aio_task_with_room_id
|
||||
async def _with_room_id(self, coro: Awaitable[_T]) -> _T:
|
||||
return await coro
|
||||
|
||||
|
||||
class SupportDebugMixin(ABC):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def _init_for_debug(self, room_id: int) -> None:
|
||||
if (
|
||||
(value := os.environ.get('DEBUG')) and
|
||||
(value == '*' or room_id in value.split(','))
|
||||
if (value := os.environ.get('DEBUG')) and (
|
||||
value == '*' or room_id in value.split(',')
|
||||
):
|
||||
self._debug = True
|
||||
self._debug_dir = os.path.expanduser(f'~/.blrec/debug/{room_id}')
|
||||
|
@ -44,6 +44,7 @@ AliasKeyOfSettings = Literal[
|
||||
'tasks',
|
||||
'output',
|
||||
'logging',
|
||||
'biliApi',
|
||||
'header',
|
||||
'danmaku',
|
||||
'recorder',
|
||||
|
@ -0,0 +1,44 @@
|
||||
<nz-modal
|
||||
nzTitle="修改 BASE API URL"
|
||||
nzCentered
|
||||
[(nzVisible)]="visible"
|
||||
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
||||
>
|
||||
<ng-container *nzModalContent>
|
||||
<form nz-form [formGroup]="settingsForm">
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="errorTip">
|
||||
<input type="text" required nz-input formControlName="baseApiUrl" />
|
||||
<ng-template #errorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
</form>
|
||||
</ng-container>
|
||||
<ng-template [nzModalFooter]>
|
||||
<button
|
||||
nz-button
|
||||
nzType="default"
|
||||
(click)="restoreDefault()"
|
||||
[disabled]="control.value.trim() === defaultBaseApiUrl"
|
||||
>
|
||||
恢复默认
|
||||
</button>
|
||||
<button nz-button nzType="default" (click)="handleCancel()">取消</button>
|
||||
<button
|
||||
nz-button
|
||||
nzDanger
|
||||
nzType="default"
|
||||
(click)="handleConfirm()"
|
||||
[disabled]="control.invalid || control.value.trim() === value"
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</ng-template>
|
||||
</nz-modal>
|
@ -0,0 +1 @@
|
||||
@use '../../shared/styles/setting';
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BaseApiUrlEditDialogComponent } from './base-api-url-edit-dialog.component';
|
||||
|
||||
describe('BaseApiUrlEditDialogComponent', () => {
|
||||
let component: BaseApiUrlEditDialogComponent;
|
||||
let fixture: ComponentFixture<BaseApiUrlEditDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ BaseApiUrlEditDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BaseApiUrlEditDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,90 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
Output,
|
||||
ChangeDetectorRef,
|
||||
EventEmitter,
|
||||
OnChanges,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
BASE_API_URL_DEFAULT,
|
||||
BASE_URL_PATTERN,
|
||||
} from '../../shared/constants/form';
|
||||
|
||||
@Component({
|
||||
selector: 'app-base-api-url-edit-dialog',
|
||||
templateUrl: './base-api-url-edit-dialog.component.html',
|
||||
styleUrls: ['./base-api-url-edit-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BaseApiUrlEditDialogComponent implements OnChanges {
|
||||
@Input() value = '';
|
||||
@Input() visible = false;
|
||||
@Output() visibleChange = new EventEmitter<boolean>();
|
||||
@Output() cancel = new EventEmitter<undefined>();
|
||||
@Output() confirm = new EventEmitter<string>();
|
||||
|
||||
readonly settingsForm: FormGroup;
|
||||
readonly defaultBaseApiUrl = BASE_API_URL_DEFAULT;
|
||||
|
||||
constructor(
|
||||
formBuilder: FormBuilder,
|
||||
private changeDetector: ChangeDetectorRef
|
||||
) {
|
||||
this.settingsForm = formBuilder.group({
|
||||
baseApiUrl: [
|
||||
'',
|
||||
[Validators.required, Validators.pattern(BASE_URL_PATTERN)],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
get control() {
|
||||
return this.settingsForm.get('baseApiUrl') as FormControl;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.setValue();
|
||||
}
|
||||
|
||||
open(): void {
|
||||
this.setValue();
|
||||
this.setVisible(true);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.setVisible(false);
|
||||
}
|
||||
|
||||
setVisible(visible: boolean): void {
|
||||
this.visible = visible;
|
||||
this.visibleChange.emit(visible);
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
setValue(): void {
|
||||
this.control.setValue(this.value);
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
handleCancel(): void {
|
||||
this.cancel.emit();
|
||||
this.close();
|
||||
}
|
||||
|
||||
handleConfirm(): void {
|
||||
this.confirm.emit(this.control.value.trim());
|
||||
this.close();
|
||||
}
|
||||
|
||||
restoreDefault(): void {
|
||||
this.control.setValue(this.defaultBaseApiUrl);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<nz-modal
|
||||
nzTitle="修改 BASE LIVE API URL"
|
||||
nzCentered
|
||||
[(nzVisible)]="visible"
|
||||
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
||||
>
|
||||
<ng-container *nzModalContent>
|
||||
<form nz-form [formGroup]="settingsForm">
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="errorTip">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
nz-input
|
||||
formControlName="baseLiveApiUrl"
|
||||
/>
|
||||
<ng-template #errorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
</form>
|
||||
</ng-container>
|
||||
<ng-template [nzModalFooter]>
|
||||
<button
|
||||
nz-button
|
||||
nzType="default"
|
||||
(click)="restoreDefault()"
|
||||
[disabled]="control.value.trim() === defaultBaseLiveApiUrl"
|
||||
>
|
||||
恢复默认
|
||||
</button>
|
||||
<button nz-button nzType="default" (click)="handleCancel()">取消</button>
|
||||
<button
|
||||
nz-button
|
||||
nzDanger
|
||||
nzType="default"
|
||||
(click)="handleConfirm()"
|
||||
[disabled]="control.invalid || control.value.trim() === value"
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</ng-template>
|
||||
</nz-modal>
|
@ -0,0 +1 @@
|
||||
@use "../../shared/styles/setting";
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BaseLiveApiUrlEditDialogComponent } from './base-live-api-url-edit-dialog.component';
|
||||
|
||||
describe('BaseLiveApiUrlEditDialogComponent', () => {
|
||||
let component: BaseLiveApiUrlEditDialogComponent;
|
||||
let fixture: ComponentFixture<BaseLiveApiUrlEditDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ BaseLiveApiUrlEditDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BaseLiveApiUrlEditDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,90 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
Output,
|
||||
ChangeDetectorRef,
|
||||
EventEmitter,
|
||||
OnChanges,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
BASE_LIVE_API_URL_DEFAULT,
|
||||
BASE_URL_PATTERN,
|
||||
} from '../../shared/constants/form';
|
||||
|
||||
@Component({
|
||||
selector: 'app-base-live-api-url-edit-dialog',
|
||||
templateUrl: './base-live-api-url-edit-dialog.component.html',
|
||||
styleUrls: ['./base-live-api-url-edit-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BaseLiveApiUrlEditDialogComponent implements OnChanges {
|
||||
@Input() value = '';
|
||||
@Input() visible = false;
|
||||
@Output() visibleChange = new EventEmitter<boolean>();
|
||||
@Output() cancel = new EventEmitter<undefined>();
|
||||
@Output() confirm = new EventEmitter<string>();
|
||||
|
||||
readonly settingsForm: FormGroup;
|
||||
readonly defaultBaseLiveApiUrl = BASE_LIVE_API_URL_DEFAULT;
|
||||
|
||||
constructor(
|
||||
formBuilder: FormBuilder,
|
||||
private changeDetector: ChangeDetectorRef
|
||||
) {
|
||||
this.settingsForm = formBuilder.group({
|
||||
baseLiveApiUrl: [
|
||||
'',
|
||||
[Validators.required, Validators.pattern(BASE_URL_PATTERN)],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
get control() {
|
||||
return this.settingsForm.get('baseLiveApiUrl') as FormControl;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.setValue();
|
||||
}
|
||||
|
||||
open(): void {
|
||||
this.setValue();
|
||||
this.setVisible(true);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.setVisible(false);
|
||||
}
|
||||
|
||||
setVisible(visible: boolean): void {
|
||||
this.visible = visible;
|
||||
this.visibleChange.emit(visible);
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
setValue(): void {
|
||||
this.control.setValue(this.value);
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
handleCancel(): void {
|
||||
this.cancel.emit();
|
||||
this.close();
|
||||
}
|
||||
|
||||
handleConfirm(): void {
|
||||
this.confirm.emit(this.control.value.trim());
|
||||
this.close();
|
||||
}
|
||||
|
||||
restoreDefault(): void {
|
||||
this.control.setValue(this.defaultBaseLiveApiUrl);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<nz-modal
|
||||
nzTitle="修改 BASE PLAY INFO API URL"
|
||||
nzCentered
|
||||
[(nzVisible)]="visible"
|
||||
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
||||
>
|
||||
<ng-container *nzModalContent>
|
||||
<form nz-form [formGroup]="settingsForm">
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="errorTip">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
nz-input
|
||||
formControlName="basePlayInfoApiUrl"
|
||||
/>
|
||||
<ng-template #errorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
</form>
|
||||
</ng-container>
|
||||
<ng-template [nzModalFooter]>
|
||||
<button
|
||||
nz-button
|
||||
nzType="default"
|
||||
(click)="restoreDefault()"
|
||||
[disabled]="control.value.trim() === defaultBasePlayInfoApiUrl"
|
||||
>
|
||||
恢复默认
|
||||
</button>
|
||||
<button nz-button nzType="default" (click)="handleCancel()">取消</button>
|
||||
<button
|
||||
nz-button
|
||||
nzDanger
|
||||
nzType="default"
|
||||
(click)="handleConfirm()"
|
||||
[disabled]="control.invalid || control.value.trim() === value"
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</ng-template>
|
||||
</nz-modal>
|
@ -0,0 +1 @@
|
||||
@use "../../shared/styles/setting";
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BasePlayInfoApiUrlEditDialogComponent } from './base-play-info-api-url-edit-dialog.component';
|
||||
|
||||
describe('BasePlayInfoApiUrlEditDialogComponent', () => {
|
||||
let component: BasePlayInfoApiUrlEditDialogComponent;
|
||||
let fixture: ComponentFixture<BasePlayInfoApiUrlEditDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ BasePlayInfoApiUrlEditDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BasePlayInfoApiUrlEditDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,90 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
Output,
|
||||
ChangeDetectorRef,
|
||||
EventEmitter,
|
||||
OnChanges,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
BASE_LIVE_API_URL_DEFAULT,
|
||||
BASE_URL_PATTERN,
|
||||
} from '../../shared/constants/form';
|
||||
|
||||
@Component({
|
||||
selector: 'app-base-play-info-api-url-edit-dialog',
|
||||
templateUrl: './base-play-info-api-url-edit-dialog.component.html',
|
||||
styleUrls: ['./base-play-info-api-url-edit-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BasePlayInfoApiUrlEditDialogComponent implements OnChanges {
|
||||
@Input() value = '';
|
||||
@Input() visible = false;
|
||||
@Output() visibleChange = new EventEmitter<boolean>();
|
||||
@Output() cancel = new EventEmitter<undefined>();
|
||||
@Output() confirm = new EventEmitter<string>();
|
||||
|
||||
readonly settingsForm: FormGroup;
|
||||
readonly defaultBasePlayInfoApiUrl = BASE_LIVE_API_URL_DEFAULT;
|
||||
|
||||
constructor(
|
||||
formBuilder: FormBuilder,
|
||||
private changeDetector: ChangeDetectorRef
|
||||
) {
|
||||
this.settingsForm = formBuilder.group({
|
||||
basePlayInfoApiUrl: [
|
||||
'',
|
||||
[Validators.required, Validators.pattern(BASE_URL_PATTERN)],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
get control() {
|
||||
return this.settingsForm.get('basePlayInfoApiUrl') as FormControl;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.setValue();
|
||||
}
|
||||
|
||||
open(): void {
|
||||
this.setValue();
|
||||
this.setVisible(true);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.setVisible(false);
|
||||
}
|
||||
|
||||
setVisible(visible: boolean): void {
|
||||
this.visible = visible;
|
||||
this.visibleChange.emit(visible);
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
setValue(): void {
|
||||
this.control.setValue(this.value);
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
handleCancel(): void {
|
||||
this.cancel.emit();
|
||||
this.close();
|
||||
}
|
||||
|
||||
handleConfirm(): void {
|
||||
this.confirm.emit(this.control.value.trim());
|
||||
this.close();
|
||||
}
|
||||
|
||||
restoreDefault(): void {
|
||||
this.control.setValue(this.defaultBasePlayInfoApiUrl);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
<form nz-form [formGroup]="settingsForm">
|
||||
<nz-form-item
|
||||
class="setting-item actionable"
|
||||
(click)="baseApiUrlEditDialog.open()"
|
||||
>
|
||||
<nz-form-label class="setting-label" [nzTooltipTitle]="baseApiUrlTip"
|
||||
>BASE API URL</nz-form-label
|
||||
>
|
||||
<ng-template #baseApiUrlTip>
|
||||
<p>主站 API 的 BASE URL</p>
|
||||
</ng-template>
|
||||
<nz-form-control
|
||||
[nzWarningTip]="syncFailedWarningTip"
|
||||
[nzValidateStatus]="syncStatus.baseApiUrl ? baseApiUrlControl : 'warning'"
|
||||
>
|
||||
<nz-form-text class="setting-value"
|
||||
>{{ baseApiUrlControl.value }}
|
||||
</nz-form-text>
|
||||
<app-base-api-url-edit-dialog
|
||||
#baseApiUrlEditDialog
|
||||
[value]="baseApiUrlControl.value"
|
||||
(confirm)="baseApiUrlControl.setValue($event)"
|
||||
></app-base-api-url-edit-dialog>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item
|
||||
class="setting-item actionable"
|
||||
(click)="baseLiveApiUrlEditDialog.open()"
|
||||
>
|
||||
<nz-form-label class="setting-label" [nzTooltipTitle]="baseLiveApiUrlTip"
|
||||
>BASE LIVE API URL</nz-form-label
|
||||
>
|
||||
<ng-template #baseLiveApiUrlTip>
|
||||
<p>直播 API (getRoomPlayInfo 除外) 的 BASE URL</p>
|
||||
</ng-template>
|
||||
<nz-form-control
|
||||
[nzWarningTip]="syncFailedWarningTip"
|
||||
[nzValidateStatus]="
|
||||
syncStatus.baseLiveApiUrl ? baseLiveApiUrlControl : 'warning'
|
||||
"
|
||||
>
|
||||
<nz-form-text class="setting-value"
|
||||
>{{ baseLiveApiUrlControl.value }}
|
||||
</nz-form-text>
|
||||
<app-base-live-api-url-edit-dialog
|
||||
#baseLiveApiUrlEditDialog
|
||||
[value]="baseLiveApiUrlControl.value"
|
||||
(confirm)="baseLiveApiUrlControl.setValue($event)"
|
||||
></app-base-live-api-url-edit-dialog>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item
|
||||
class="setting-item actionable"
|
||||
(click)="basePlayInfoApiUrlEditDialog.open()"
|
||||
>
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
[nzTooltipTitle]="basePalyInfoApiUrlTip"
|
||||
>BASE PLAY INFO API URL</nz-form-label
|
||||
>
|
||||
<ng-template #basePalyInfoApiUrlTip>
|
||||
<p>直播 API getRoomPlayInfo 的 BASE URL</p>
|
||||
</ng-template>
|
||||
<nz-form-control
|
||||
[nzWarningTip]="syncFailedWarningTip"
|
||||
[nzValidateStatus]="
|
||||
syncStatus.basePlayInfoApiUrl ? basePlayInfoApiUrlControl : 'warning'
|
||||
"
|
||||
>
|
||||
<nz-form-text class="setting-value"
|
||||
>{{ basePlayInfoApiUrlControl.value }}
|
||||
</nz-form-text>
|
||||
<app-base-play-info-api-url-edit-dialog
|
||||
#basePlayInfoApiUrlEditDialog
|
||||
[value]="basePlayInfoApiUrlControl.value"
|
||||
(confirm)="basePlayInfoApiUrlControl.setValue($event)"
|
||||
></app-base-play-info-api-url-edit-dialog>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
</form>
|
@ -0,0 +1 @@
|
||||
@use '../shared/styles/setting';
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BiliApiSettingsComponent } from './bili-api-settings.component';
|
||||
|
||||
describe('BiliApiSettingsComponent', () => {
|
||||
let component: BiliApiSettingsComponent;
|
||||
let fixture: ComponentFixture<BiliApiSettingsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ BiliApiSettingsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BiliApiSettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,76 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
OnChanges,
|
||||
ChangeDetectorRef,
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import mapValues from 'lodash-es/mapValues';
|
||||
|
||||
import { BiliApiSettings } from '../shared/setting.model';
|
||||
import {
|
||||
SettingsSyncService,
|
||||
SyncStatus,
|
||||
calcSyncStatus,
|
||||
} from '../shared/services/settings-sync.service';
|
||||
import { SYNC_FAILED_WARNING_TIP } from '../shared/constants/form';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bili-api-settings',
|
||||
templateUrl: './bili-api-settings.component.html',
|
||||
styleUrls: ['./bili-api-settings.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BiliApiSettingsComponent implements OnInit, OnChanges {
|
||||
@Input() settings!: BiliApiSettings;
|
||||
syncStatus!: SyncStatus<BiliApiSettings>;
|
||||
|
||||
readonly settingsForm: FormGroup;
|
||||
readonly syncFailedWarningTip = SYNC_FAILED_WARNING_TIP;
|
||||
|
||||
constructor(
|
||||
formBuilder: FormBuilder,
|
||||
private changeDetector: ChangeDetectorRef,
|
||||
private settingsSyncService: SettingsSyncService
|
||||
) {
|
||||
this.settingsForm = formBuilder.group({
|
||||
baseApiUrl: [''],
|
||||
baseLiveApiUrl: [''],
|
||||
basePlayInfoApiUrl: [''],
|
||||
});
|
||||
}
|
||||
|
||||
get baseApiUrlControl() {
|
||||
return this.settingsForm.get('baseApiUrl') as FormControl;
|
||||
}
|
||||
|
||||
get baseLiveApiUrlControl() {
|
||||
return this.settingsForm.get('baseLiveApiUrl') as FormControl;
|
||||
}
|
||||
|
||||
get basePlayInfoApiUrlControl() {
|
||||
return this.settingsForm.get('basePlayInfoApiUrl') as FormControl;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.syncStatus = mapValues(this.settings, () => true);
|
||||
this.settingsForm.setValue(this.settings);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsSyncService
|
||||
.syncSettings(
|
||||
'biliApi',
|
||||
this.settings,
|
||||
this.settingsForm.valueChanges as Observable<BiliApiSettings>
|
||||
)
|
||||
.subscribe((detail) => {
|
||||
this.syncStatus = { ...this.syncStatus, ...calcSyncStatus(detail) };
|
||||
this.changeDetector.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
@ -3,8 +3,6 @@
|
||||
nzCentered
|
||||
[(nzVisible)]="visible"
|
||||
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
||||
(nzOnOk)="handleConfirm()"
|
||||
(nzOnCancel)="handleCancel()"
|
||||
>
|
||||
<ng-container *nzModalContent>
|
||||
<form nz-form [formGroup]="settingsForm">
|
||||
@ -59,7 +57,7 @@
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #modalFooter>
|
||||
<ng-template [nzModalFooter]>
|
||||
<button
|
||||
nz-button
|
||||
nzType="default"
|
||||
|
@ -29,6 +29,12 @@
|
||||
></app-disk-space-settings>
|
||||
</app-page-section>
|
||||
|
||||
<app-page-section name="BILI API">
|
||||
<app-bili-api-settings
|
||||
[settings]="settings.biliApi"
|
||||
></app-bili-api-settings>
|
||||
</app-page-section>
|
||||
|
||||
<app-page-section name="网络请求">
|
||||
<app-header-settings [settings]="settings.header"></app-header-settings>
|
||||
</app-page-section>
|
||||
|
@ -65,6 +65,10 @@ import { LogdirEditDialogComponent } from './logging-settings/logdir-edit-dialog
|
||||
import { PathTemplateEditDialogComponent } from './output-settings/path-template-edit-dialog/path-template-edit-dialog.component';
|
||||
import { MessageTemplateSettingsComponent } from './notification-settings/shared/components/message-template-settings/message-template-settings.component';
|
||||
import { MessageTemplateEditDialogComponent } from './notification-settings/shared/components/message-template-settings/message-template-edit-dialog/message-template-edit-dialog.component';
|
||||
import { BiliApiSettingsComponent } from './bili-api-settings/bili-api-settings.component';
|
||||
import { BaseApiUrlEditDialogComponent } from './bili-api-settings/base-api-url-edit-dialog/base-api-url-edit-dialog.component';
|
||||
import { BaseLiveApiUrlEditDialogComponent } from './bili-api-settings/base-live-api-url-edit-dialog/base-live-api-url-edit-dialog.component';
|
||||
import { BasePlayInfoApiUrlEditDialogComponent } from './bili-api-settings/base-play-info-api-url-edit-dialog/base-play-info-api-url-edit-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -101,6 +105,10 @@ import { MessageTemplateEditDialogComponent } from './notification-settings/shar
|
||||
PathTemplateEditDialogComponent,
|
||||
MessageTemplateSettingsComponent,
|
||||
MessageTemplateEditDialogComponent,
|
||||
BiliApiSettingsComponent,
|
||||
BaseApiUrlEditDialogComponent,
|
||||
BaseLiveApiUrlEditDialogComponent,
|
||||
BasePlayInfoApiUrlEditDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -2,6 +2,10 @@ import { CoverSaveStrategy, DeleteStrategy } from '../setting.model';
|
||||
|
||||
export const SYNC_FAILED_WARNING_TIP = '设置同步失败!';
|
||||
|
||||
export const BASE_URL_PATTERN = /^https?:\/\/.*$/;
|
||||
export const BASE_API_URL_DEFAULT = 'https://api.bilibili.com';
|
||||
export const BASE_LIVE_API_URL_DEFAULT = 'https://api.live.bilibili.com';
|
||||
|
||||
export const PATH_TEMPLATE_PATTERN =
|
||||
/^(?:[^\\\/:*?"<>|\t\n\r\f\v\{\}]*?\{(?:roomid|uname|title|area|parent_area|year|month|day|hour|minute|second)\}[^\\\/:*?"<>|\t\n\r\f\v\{\}]*?)+?(?:\/(?:[^\\\/:*?"<>|\t\n\r\f\v\{\}]*?\{(?:roomid|uname|title|area|parent_area|year|month|day|hour|minute|second)\}[^\\\/:*?"<>|\t\n\r\f\v\{\}]*?)+?)*$/;
|
||||
|
||||
|
@ -19,6 +19,7 @@ type PrimarySettings = Pick<
|
||||
Settings,
|
||||
| 'output'
|
||||
| 'logging'
|
||||
| 'biliApi'
|
||||
| 'header'
|
||||
| 'danmaku'
|
||||
| 'recorder'
|
||||
@ -41,6 +42,7 @@ export class SettingsResolver implements Resolve<PrimarySettings> {
|
||||
.getSettings([
|
||||
'output',
|
||||
'logging',
|
||||
'biliApi',
|
||||
'header',
|
||||
'danmaku',
|
||||
'recorder',
|
||||
|
@ -1,5 +1,13 @@
|
||||
import type { Nullable, PartialDeep } from 'src/app/shared/utility-types';
|
||||
|
||||
export interface BiliApiSettings {
|
||||
baseApiUrl: string;
|
||||
baseLiveApiUrl: string;
|
||||
basePlayInfoApiUrl: string;
|
||||
}
|
||||
|
||||
export type BiliApiOptions = Nullable<BiliApiSettings>;
|
||||
|
||||
export interface HeaderSettings {
|
||||
userAgent: string;
|
||||
cookie: string;
|
||||
@ -65,6 +73,7 @@ export type PostprocessingOptions = Nullable<PostprocessingSettings>;
|
||||
|
||||
export interface TaskOptions {
|
||||
output: OutputOptions;
|
||||
biliApi: BiliApiOptions;
|
||||
header: HeaderOptions;
|
||||
danmaku: DanmakuOptions;
|
||||
recorder: RecorderOptions;
|
||||
@ -81,7 +90,7 @@ export interface TaskSettings extends TaskOptions {
|
||||
|
||||
export type GlobalTaskSettings = Pick<
|
||||
Settings,
|
||||
'output' | 'header' | 'danmaku' | 'recorder' | 'postprocessing'
|
||||
'output' | 'biliApi' | 'header' | 'danmaku' | 'recorder' | 'postprocessing'
|
||||
>;
|
||||
|
||||
export interface OutputSettings {
|
||||
@ -351,6 +360,7 @@ export interface Settings {
|
||||
tasks: TaskSettings[];
|
||||
output: OutputSettings;
|
||||
logging: LoggingSettings;
|
||||
biliApi: BiliApiSettings;
|
||||
header: HeaderSettings;
|
||||
danmaku: DanmakuSettings;
|
||||
recorder: RecorderSettings;
|
||||
|
@ -186,6 +186,7 @@ export class TaskItemComponent implements OnChanges, OnDestroy {
|
||||
this.settingService.getTaskOptions(this.roomId),
|
||||
this.settingService.getSettings([
|
||||
'output',
|
||||
'biliApi',
|
||||
'header',
|
||||
'danmaku',
|
||||
'recorder',
|
||||
|
@ -21,7 +21,7 @@
|
||||
>
|
||||
<nz-form-control
|
||||
class="setting-control input"
|
||||
[nzErrorTip]="errorTip"
|
||||
[nzErrorTip]="pathTemplateErrorTip"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
@ -32,7 +32,7 @@
|
||||
[(ngModel)]="model.output.pathTemplate"
|
||||
[disabled]="options.output.pathTemplate === null"
|
||||
/>
|
||||
<ng-template #errorTip let-control>
|
||||
<ng-template #pathTemplateErrorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
请输入路径模板
|
||||
</ng-container>
|
||||
@ -745,6 +745,139 @@
|
||||
</nz-form-item>
|
||||
</div>
|
||||
|
||||
<div ngModelGroup="biliApi" class="form-group biliapi">
|
||||
<h2>BILI API</h2>
|
||||
<nz-form-item class="setting-item input">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
[nzTooltipTitle]="baseApiUrlTip"
|
||||
>BASE API URL</nz-form-label
|
||||
>
|
||||
<ng-template #baseApiUrlTip>
|
||||
<p>主站 API 的 BASE URL</p>
|
||||
</ng-template>
|
||||
<nz-form-control
|
||||
class="setting-control input"
|
||||
[nzErrorTip]="baseApiUrlErrorTip"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
[pattern]="baseUrlPattern"
|
||||
nz-input
|
||||
name="baseApiUrl"
|
||||
[(ngModel)]="model.biliApi.baseApiUrl"
|
||||
[disabled]="options.biliApi.baseApiUrl === null"
|
||||
/>
|
||||
<ng-template #baseApiUrlErrorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
<label
|
||||
nz-checkbox
|
||||
[nzChecked]="options.biliApi.baseApiUrl !== null"
|
||||
(nzCheckedChange)="
|
||||
options.biliApi.baseApiUrl = $event
|
||||
? globalSettings.biliApi.baseApiUrl
|
||||
: null
|
||||
"
|
||||
>覆盖全局设置</label
|
||||
>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item input">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
[nzTooltipTitle]="baseLiveApiUrlTip"
|
||||
>BASE LIVE API URL</nz-form-label
|
||||
>
|
||||
<ng-template #baseLiveApiUrlTip>
|
||||
<p>直播 API (getRoomPlayInfo 除外) 的 BASE URL</p>
|
||||
</ng-template>
|
||||
<nz-form-control
|
||||
class="setting-control input"
|
||||
[nzErrorTip]="baseLiveApiUrlErrorTip"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
[pattern]="baseUrlPattern"
|
||||
nz-input
|
||||
name="baseLiveApiUrl"
|
||||
[(ngModel)]="model.biliApi.baseLiveApiUrl"
|
||||
[disabled]="options.biliApi.baseLiveApiUrl === null"
|
||||
/>
|
||||
<ng-template #baseLiveApiUrlErrorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
<label
|
||||
nz-checkbox
|
||||
[nzChecked]="options.biliApi.baseLiveApiUrl !== null"
|
||||
(nzCheckedChange)="
|
||||
options.biliApi.baseLiveApiUrl = $event
|
||||
? globalSettings.biliApi.baseLiveApiUrl
|
||||
: null
|
||||
"
|
||||
>覆盖全局设置</label
|
||||
>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item input">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
[nzTooltipTitle]="basePalyInfoApiUrlTip"
|
||||
>BASE PLAY INFO API URL</nz-form-label
|
||||
>
|
||||
<ng-template #basePalyInfoApiUrlTip>
|
||||
<p>直播 API getRoomPlayInfo 的 BASE URL</p>
|
||||
</ng-template>
|
||||
<nz-form-control
|
||||
class="setting-control input"
|
||||
[nzErrorTip]="basePlayInfoApiUrlErrorTip"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
[pattern]="baseUrlPattern"
|
||||
nz-input
|
||||
name="basePlayInfoApiUrl"
|
||||
[(ngModel)]="model.biliApi.basePlayInfoApiUrl"
|
||||
[disabled]="options.biliApi.basePlayInfoApiUrl === null"
|
||||
/>
|
||||
<ng-template #basePlayInfoApiUrlErrorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
<label
|
||||
nz-checkbox
|
||||
[nzChecked]="options.biliApi.basePlayInfoApiUrl !== null"
|
||||
(nzCheckedChange)="
|
||||
options.biliApi.basePlayInfoApiUrl = $event
|
||||
? globalSettings.biliApi.basePlayInfoApiUrl
|
||||
: null
|
||||
"
|
||||
>覆盖全局设置</label
|
||||
>
|
||||
</nz-form-item>
|
||||
</div>
|
||||
|
||||
<div ngModelGroup="header" class="form-group header">
|
||||
<h2>网络请求</h2>
|
||||
<nz-form-item class="setting-item textarea">
|
||||
@ -761,7 +894,7 @@
|
||||
? 'warning'
|
||||
: userAgent
|
||||
"
|
||||
[nzErrorTip]="errorTip"
|
||||
[nzErrorTip]="userAgentErrorTip"
|
||||
>
|
||||
<textarea
|
||||
#userAgent="ngModel"
|
||||
@ -774,7 +907,7 @@
|
||||
[disabled]="options.header.userAgent === null"
|
||||
></textarea>
|
||||
</nz-form-control>
|
||||
<ng-template #errorTip let-control>
|
||||
<ng-template #userAgentErrorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
请输入 User Agent
|
||||
</ng-container>
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
DELETE_STRATEGIES,
|
||||
COVER_SAVE_STRATEGIES,
|
||||
RECORDING_MODE_OPTIONS,
|
||||
BASE_URL_PATTERN,
|
||||
} from '../../settings/shared/constants/form';
|
||||
|
||||
type OptionsModel = NonNullable<TaskOptions>;
|
||||
@ -55,6 +56,7 @@ export class TaskSettingsDialogComponent implements OnChanges {
|
||||
|
||||
readonly warningTip =
|
||||
'需要重启弹幕客户端才能生效,如果任务正在录制可能会丢失弹幕!';
|
||||
readonly baseUrlPattern = BASE_URL_PATTERN;
|
||||
readonly pathTemplatePattern = PATH_TEMPLATE_PATTERN;
|
||||
readonly streamFormatOptions = cloneDeep(STREAM_FORMAT_OPTIONS) as Mutable<
|
||||
typeof STREAM_FORMAT_OPTIONS
|
||||
@ -111,7 +113,6 @@ export class TaskSettingsDialogComponent implements OnChanges {
|
||||
}
|
||||
|
||||
handleConfirm(): void {
|
||||
debugger;
|
||||
this.confirm.emit(difference(this.options, this.taskOptions!));
|
||||
this.close();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user