feat: custom BASE API URL

This commit is contained in:
acgnhik 2022-08-24 11:57:41 +08:00
parent a2fe0bf01b
commit 91ccd8c595
47 changed files with 1177 additions and 197 deletions

View File

@ -1,47 +1,55 @@
from abc import ABC
import hashlib import hashlib
from urllib.parse import urlencode import logging
from abc import ABC
from datetime import datetime 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 import aiohttp
from tenacity import ( from tenacity import retry, stop_after_delay, wait_exponential
retry,
wait_exponential,
stop_after_delay,
)
from .typing import QualityNumber, JsonResponse, ResponseData
from .exceptions import ApiRequestError from .exceptions import ApiRequestError
from .typing import JsonResponse, QualityNumber, ResponseData
__all__ = 'AppApi', 'WebApi' __all__ = 'AppApi', 'WebApi'
logger = logging.getLogger(__name__)
class BaseApi(ABC): 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._session = session
self.headers = headers or {}
self.timeout = 10 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 @staticmethod
def _check_response(json_res: JsonResponse) -> None: def _check_response(json_res: JsonResponse) -> None:
if json_res['code'] != 0: if json_res['code'] != 0:
raise ApiRequestError( raise ApiRequestError(
json_res['code'], json_res['code'], json_res.get('message') or json_res.get('msg') or ''
json_res.get('message') or json_res.get('msg') or '',
) )
@retry( @retry(reraise=True, stop=stop_after_delay(5), wait=wait_exponential(0.1))
reraise=True, async def _get_json(self, *args: Any, **kwds: Any) -> JsonResponse:
stop=stop_after_delay(5),
wait=wait_exponential(0.1),
)
async def _get(self, *args: Any, **kwds: Any) -> JsonResponse:
async with self._session.get( async with self._session.get(
*args, *args, **kwds, timeout=self.timeout, headers=self.headers
**kwds,
timeout=self.timeout,
) as res: ) as res:
logger.debug(f'real url: {res.real_url}')
json_res = await res.json() json_res = await res.json()
self._check_response(json_res) self._check_response(json_res)
return json_res return json_res
@ -52,12 +60,20 @@ class AppApi(BaseApi):
_appkey = '1d8b6e7d45233436' _appkey = '1d8b6e7d45233436'
_appsec = '560c52ccd288fed045859ed18bffd973' _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 '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', 'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip', '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 @classmethod
def signed(cls, params: Mapping[str, Any]) -> Dict[str, Any]: def signed(cls, params: Mapping[str, Any]) -> Dict[str, Any]:
if isinstance(params, Mapping): if isinstance(params, Mapping):
@ -77,9 +93,9 @@ class AppApi(BaseApi):
only_video: bool = False, only_video: bool = False,
only_audio: bool = False, only_audio: bool = False,
) -> ResponseData: ) -> ResponseData:
url = 'https://api.live.bilibili.com/xlive/app-room/v2/index/getRoomPlayInfo' # noqa url = self.base_play_info_api_url + '/xlive/app-room/v2/index/getRoomPlayInfo'
params = self.signed(
params = self.signed({ {
'actionKey': 'appkey', 'actionKey': 'appkey',
'build': '6640400', 'build': '6640400',
'channel': 'bili', 'channel': 'bili',
@ -103,15 +119,15 @@ class AppApi(BaseApi):
'qn': qn, 'qn': qn,
'room_id': room_id, 'room_id': room_id,
'ts': int(datetime.utcnow().timestamp()), 'ts': int(datetime.utcnow().timestamp()),
}) }
)
r = await self._get(url, params=params, headers=self._headers) r = await self._get_json(url, params=params, headers=self._headers)
return r['data'] return r['data']
async def get_info_by_room(self, room_id: int) -> ResponseData: async def get_info_by_room(self, room_id: int) -> ResponseData:
url = 'https://api.live.bilibili.com/xlive/app-room/v1/index/getInfoByRoom' # noqa url = self.base_live_api_url + '/xlive/app-room/v1/index/getInfoByRoom'
params = self.signed(
params = self.signed({ {
'actionKey': 'appkey', 'actionKey': 'appkey',
'build': '6640400', 'build': '6640400',
'channel': 'bili', 'channel': 'bili',
@ -120,30 +136,30 @@ class AppApi(BaseApi):
'platform': 'android', 'platform': 'android',
'room_id': room_id, 'room_id': room_id,
'ts': int(datetime.utcnow().timestamp()), 'ts': int(datetime.utcnow().timestamp()),
}) }
)
r = await self._get(url, params=params) r = await self._get_json(url, params=params)
return r['data'] return r['data']
async def get_user_info(self, uid: int) -> ResponseData: async def get_user_info(self, uid: int) -> ResponseData:
url = 'https://app.bilibili.com/x/v2/space' url = self.base_api_url + '/x/v2/space'
params = self.signed(
params = self.signed({ {
'build': '6640400', 'build': '6640400',
'channel': 'bili', 'channel': 'bili',
'mobi_app': 'android', 'mobi_app': 'android',
'platform': 'android', 'platform': 'android',
'ts': int(datetime.utcnow().timestamp()), 'ts': int(datetime.utcnow().timestamp()),
'vmid': uid, 'vmid': uid,
}) }
)
r = await self._get(url, params=params) r = await self._get_json(url, params=params)
return r['data'] return r['data']
async def get_danmu_info(self, room_id: int) -> ResponseData: async def get_danmu_info(self, room_id: int) -> ResponseData:
url = 'https://api.live.bilibili.com/xlive/app-room/v1/index/getDanmuInfo' # noqa url = self.base_live_api_url + '/xlive/app-room/v1/index/getDanmuInfo'
params = self.signed(
params = self.signed({ {
'actionKey': 'appkey', 'actionKey': 'appkey',
'build': '6640400', 'build': '6640400',
'channel': 'bili', 'channel': 'bili',
@ -152,36 +168,22 @@ class AppApi(BaseApi):
'platform': 'android', 'platform': 'android',
'room_id': room_id, 'room_id': room_id,
'ts': int(datetime.utcnow().timestamp()), 'ts': int(datetime.utcnow().timestamp()),
}) }
)
r = await self._get(url, params=params) r = await self._get_json(url, params=params)
return r['data'] return r['data']
class WebApi(BaseApi): 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: 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'] return r['data']
async def get_room_play_info( async def get_room_play_info(
self, room_id: int, qn: QualityNumber = 10000 self, room_id: int, qn: QualityNumber = 10000
) -> ResponseData: ) -> ResponseData:
url = self.base_play_info_api_url + '/xlive/web-room/v2/index/getRoomPlayInfo'
params = { params = {
'room_id': room_id, 'room_id': room_id,
'protocol': '0,1', 'protocol': '0,1',
@ -191,37 +193,34 @@ class WebApi(BaseApi):
'platform': 'web', 'platform': 'web',
'ptype': 8, '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'] return r['data']
async def get_info_by_room(self, room_id: int) -> ResponseData: async def get_info_by_room(self, room_id: int) -> ResponseData:
params = { url = self.base_live_api_url + '/xlive/web-room/v1/index/getInfoByRoom'
'room_id': room_id, params = {'room_id': room_id}
} r = await self._get_json(url, params=params)
r = await self._get(self.GET_INFO_BY_ROOM_URL, params=params)
return r['data'] return r['data']
async def get_info(self, room_id: int) -> ResponseData: async def get_info(self, room_id: int) -> ResponseData:
params = { url = self.base_live_api_url + '/room/v1/Room/get_info'
'room_id': room_id, params = {'room_id': room_id}
} r = await self._get_json(url, params=params)
r = await self._get(self.GET_INFO_URL, params=params)
return r['data'] return r['data']
async def get_timestamp(self) -> int: 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'] return r['data']['timestamp']
async def get_user_info(self, uid: int) -> ResponseData: async def get_user_info(self, uid: int) -> ResponseData:
params = { url = self.base_api_url + '/x/space/acc/info'
'mid': uid, params = {'mid': uid}
} r = await self._get_json(url, params=params)
r = await self._get(self.GET_USER_INFO_URL, params=params)
return r['data'] return r['data']
async def get_danmu_info(self, room_id: int) -> ResponseData: async def get_danmu_info(self, room_id: int) -> ResponseData:
params = { url = self.base_live_api_url + '/xlive/web-room/v1/index/getDanmuInfo'
'id': room_id, params = {'id': room_id}
} r = await self._get_json(url, params=params)
r = await self._get(self.GET_DANMU_INFO_URL, params=params)
return r['data'] return r['data']

View File

@ -53,12 +53,14 @@ class DanmakuClient(EventEmitter[DanmakuListener], AsyncStoppableMixin):
room_id: int, room_id: int,
*, *,
max_retries: int = 10, max_retries: int = 10,
headers: Optional[Dict[str, str]] = None,
) -> None: ) -> None:
super().__init__() super().__init__()
self.session = session self.session = session
self.appapi = appapi self.appapi = appapi
self.webapi = webapi self.webapi = webapi
self._room_id = room_id self._room_id = room_id
self.headers = headers or {}
self._api_platform: ApiPlatform = 'web' self._api_platform: ApiPlatform = 'web'
self._danmu_info: Dict[str, Any] = COMMON_DANMU_INFO self._danmu_info: Dict[str, Any] = COMMON_DANMU_INFO
@ -66,6 +68,14 @@ class DanmakuClient(EventEmitter[DanmakuListener], AsyncStoppableMixin):
self._retry_delay: int = 0 self._retry_delay: int = 0
self._MAX_RETRIES: Final[int] = max_retries 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: async def _do_start(self) -> None:
await self._update_danmu_info() await self._update_danmu_info()
await self._connect() await self._connect()
@ -77,6 +87,12 @@ class DanmakuClient(EventEmitter[DanmakuListener], AsyncStoppableMixin):
await self._disconnect() await self._disconnect()
logger.debug('Stopped danmaku client') 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: async def reconnect(self) -> None:
if self.stopped: if self.stopped:
return return
@ -117,7 +133,9 @@ class DanmakuClient(EventEmitter[DanmakuListener], AsyncStoppableMixin):
) )
logger.debug(f'Connecting WebSocket... {url}') logger.debug(f'Connecting WebSocket... {url}')
try: 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: except Exception as exc:
logger.debug(f'Failed to connect WebSocket: {repr(exc)}') logger.debug(f'Failed to connect WebSocket: {repr(exc)}')
raise raise

View File

@ -40,9 +40,44 @@ class Live:
self._cookie = cookie self._cookie = cookie
self._html_page_url = f'https://live.bilibili.com/{room_id}' 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._room_info: RoomInfo
self._user_info: UserInfo 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 @property
def user_agent(self) -> str: def user_agent(self) -> str:
return self._user_agent return self._user_agent
@ -50,6 +85,8 @@ class Live:
@user_agent.setter @user_agent.setter
def user_agent(self, value: str) -> None: def user_agent(self, value: str) -> None:
self._user_agent = value self._user_agent = value
self._webapi.headers = self.headers
self._appapi.headers = self.headers
@property @property
def cookie(self) -> str: def cookie(self) -> str:
@ -58,15 +95,25 @@ class Live:
@cookie.setter @cookie.setter
def cookie(self, value: str) -> None: def cookie(self, value: str) -> None:
self._cookie = value self._cookie = value
self._webapi.headers = self.headers
self._appapi.headers = self.headers
@property @property
def headers(self) -> Dict[str, str]: def headers(self) -> Dict[str, str]:
return { return {
'Referer': 'https://live.bilibili.com/', 'Accept': '*/*',
'Connection': 'Keep-Alive', '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',
'Accept-Encoding': 'gzip', '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, 'User-Agent': self._user_agent,
'Cookie': self._cookie, 'Cookie': self._cookie,
'Accept-Encoding': 'gzip',
} }
@property @property
@ -94,15 +141,6 @@ class Live:
return self._user_info return self._user_info
async def init(self) -> None: 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._room_info = await self.get_room_info()
self._user_info = await self.get_user_info(self._room_info.uid) self._user_info = await self.get_user_info(self._room_info.uid)

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

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

View File

@ -10,6 +10,6 @@
<body> <body>
<app-root></app-root> <app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript> <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> </body></html>

View File

@ -1,6 +1,6 @@
{ {
"configVersion": 1, "configVersion": 1,
"timestamp": 1659243718041, "timestamp": 1661135687643,
"index": "/index.html", "index": "/index.html",
"assetGroups": [ "assetGroups": [
{ {
@ -13,16 +13,16 @@
"urls": [ "urls": [
"/103.5b5d2a6e5a8a7479.js", "/103.5b5d2a6e5a8a7479.js",
"/146.5a8902910bda9e87.js", "/146.5a8902910bda9e87.js",
"/183.2c7c85597ba82f9e.js", "/170.d0e14a28ee578d1f.js",
"/183.0d3cd9f454be16fb.js",
"/45.c90c3cea2bf1a66e.js", "/45.c90c3cea2bf1a66e.js",
"/500.5d39ab52fb714a12.js", "/91.9ff409a090dace5c.js",
"/91.be3cbd4101dc7500.js",
"/common.858f777e9296e6f2.js", "/common.858f777e9296e6f2.js",
"/index.html", "/index.html",
"/main.16a8fc7b1f8a870d.js", "/main.27d1fff16f7909f2.js",
"/manifest.webmanifest", "/manifest.webmanifest",
"/polyfills.4b08448aee19bb22.js", "/polyfills.4b08448aee19bb22.js",
"/runtime.68e08c4d681726f6.js", "/runtime.4ae765ab3bddf383.js",
"/styles.2e152d608221c2ee.css" "/styles.2e152d608221c2ee.css"
], ],
"patterns": [] "patterns": []
@ -1636,10 +1636,10 @@
"hashTable": { "hashTable": {
"/103.5b5d2a6e5a8a7479.js": "cc0240f217015b6d4ddcc14f31fcc42e1c1c282a", "/103.5b5d2a6e5a8a7479.js": "cc0240f217015b6d4ddcc14f31fcc42e1c1c282a",
"/146.5a8902910bda9e87.js": "d9c33c7073662699f00f46f3a384ae5b749fdef9", "/146.5a8902910bda9e87.js": "d9c33c7073662699f00f46f3a384ae5b749fdef9",
"/183.2c7c85597ba82f9e.js": "22a1524d6399d9bde85334a2eba15670f68ccd96", "/170.d0e14a28ee578d1f.js": "d6b6208ca442565ed39300b27ab8cbe5501cb46a",
"/183.0d3cd9f454be16fb.js": "e7e6ebc715791102fd09edabe2aa47316208b29c",
"/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764", "/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764",
"/500.5d39ab52fb714a12.js": "646fbfd3af1124519171f1cd9fac4c214b5af60f", "/91.9ff409a090dace5c.js": "d756ffe7cd3f5516e40a7e6d6cf494ea6213a546",
"/91.be3cbd4101dc7500.js": "f0fec71455c96f9a60c4fa671d2ccdba07e9a00a",
"/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1", "/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1",
"/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1", "/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1",
"/assets/fill/.gitkeep": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "/assets/fill/.gitkeep": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
@ -3234,11 +3234,11 @@
"/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01", "/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01",
"/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068", "/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068",
"/common.858f777e9296e6f2.js": "b68ca68e1e214a2537d96935c23410126cc564dd", "/common.858f777e9296e6f2.js": "b68ca68e1e214a2537d96935c23410126cc564dd",
"/index.html": "3f28dbdfc92c1a0930448a8ff6d5d2ac49648987", "/index.html": "29167783eb093ffa93369f741a5ce20a534137de",
"/main.16a8fc7b1f8a870d.js": "9c680888ae14907d6c20e60c026b49a2331768e9", "/main.27d1fff16f7909f2.js": "22e63726601a31af1a96e7901afc0d2bea7fd414",
"/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586", "/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586",
"/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d", "/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d",
"/runtime.68e08c4d681726f6.js": "04815a3dd35466f647f3707a295bc2c76c9f0375", "/runtime.4ae765ab3bddf383.js": "96653fd35d3ad9684e603011436e9d43a1121690",
"/styles.2e152d608221c2ee.css": "9830389a46daa5b4511e0dd343aad23ca9f9690f" "/styles.2e152d608221c2ee.css": "9830389a46daa5b4511e0dd343aad23ca9f9690f"
}, },
"navigationUrls": [ "navigationUrls": [

View File

@ -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))})()})();

View File

@ -7,6 +7,8 @@ from .models import (
EmailNotificationSettings, EmailNotificationSettings,
EmailSettings, EmailSettings,
EnvSettings, EnvSettings,
BiliApiOptions,
BiliApiSettings,
HeaderOptions, HeaderOptions,
HeaderSettings, HeaderSettings,
LoggingSettings, LoggingSettings,
@ -45,6 +47,8 @@ __all__ = (
'Settings', 'Settings',
'SettingsIn', 'SettingsIn',
'SettingsOut', 'SettingsOut',
'BiliApiOptions',
'BiliApiSettings',
'HeaderOptions', 'HeaderOptions',
'HeaderSettings', 'HeaderSettings',
'DanmakuOptions', 'DanmakuOptions',

View File

@ -35,6 +35,8 @@ __all__ = (
'Settings', 'Settings',
'SettingsIn', 'SettingsIn',
'SettingsOut', 'SettingsOut',
'BiliApiOptions',
'BiliApiSettings',
'HeaderOptions', 'HeaderOptions',
'HeaderSettings', 'HeaderSettings',
'DanmakuOptions', '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): class HeaderOptions(BaseModel):
user_agent: Optional[str] user_agent: Optional[str]
cookie: Optional[str] cookie: Optional[str]
@ -285,6 +299,7 @@ class OutputSettings(OutputOptions):
class TaskOptions(BaseModel): class TaskOptions(BaseModel):
output: OutputOptions = OutputOptions() output: OutputOptions = OutputOptions()
bili_api: BiliApiOptions = BiliApiOptions()
header: HeaderOptions = HeaderOptions() header: HeaderOptions = HeaderOptions()
danmaku: DanmakuOptions = DanmakuOptions() danmaku: DanmakuOptions = DanmakuOptions()
recorder: RecorderOptions = RecorderOptions() recorder: RecorderOptions = RecorderOptions()
@ -294,7 +309,14 @@ class TaskOptions(BaseModel):
def from_settings(cls, settings: TaskSettings) -> TaskOptions: def from_settings(cls, settings: TaskSettings) -> TaskOptions:
return cls( return cls(
**settings.dict( **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)] = [] tasks: Annotated[List[TaskSettings], Field(max_items=100)] = []
output: OutputSettings = OutputSettings() # type: ignore output: OutputSettings = OutputSettings() # type: ignore
logging: LoggingSettings = LoggingSettings() # type: ignore logging: LoggingSettings = LoggingSettings() # type: ignore
bili_api: BiliApiSettings = BiliApiSettings()
header: HeaderSettings = HeaderSettings() header: HeaderSettings = HeaderSettings()
danmaku: DanmakuSettings = DanmakuSettings() danmaku: DanmakuSettings = DanmakuSettings()
recorder: RecorderSettings = RecorderSettings() recorder: RecorderSettings = RecorderSettings()
@ -636,6 +659,7 @@ class Settings(BaseModel):
class SettingsIn(BaseModel): class SettingsIn(BaseModel):
output: Optional[OutputSettings] = None output: Optional[OutputSettings] = None
logging: Optional[LoggingSettings] = None logging: Optional[LoggingSettings] = None
bili_api: Optional[BiliApiSettings] = None
header: Optional[HeaderSettings] = None header: Optional[HeaderSettings] = None
danmaku: Optional[DanmakuSettings] = None danmaku: Optional[DanmakuSettings] = None
recorder: Optional[RecorderSettings] = None recorder: Optional[RecorderSettings] = None

View File

@ -16,6 +16,7 @@ from ..notification import (
from ..webhook import WebHook from ..webhook import WebHook
from .helpers import shadow_settings, update_settings from .helpers import shadow_settings, update_settings
from .models import ( from .models import (
BiliApiOptions,
DanmakuOptions, DanmakuOptions,
HeaderOptions, HeaderOptions,
MessageTemplateSettings, MessageTemplateSettings,
@ -210,13 +211,24 @@ class SettingsManager:
settings.enable_recorder = False settings.enable_recorder = False
await self.dump_settings() 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( 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: ) -> None:
final_settings = self._settings.header.copy() final_settings = self._settings.header.copy()
shadow_settings(options, final_settings) shadow_settings(options, final_settings)
await self._app._task_manager.apply_task_header_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( def apply_task_danmaku_settings(
@ -264,6 +276,10 @@ class SettingsManager:
backup_count=self._settings.logging.backup_count, 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: async def apply_header_settings(self) -> None:
for settings in self._settings.tasks: for settings in self._settings.tasks:
await self.apply_task_header_settings(settings.room_id, settings.header) await self.apply_task_header_settings(settings.room_id, settings.header)

View File

@ -19,6 +19,7 @@ KeyOfSettings = Literal[
'tasks', 'tasks',
'output', 'output',
'logging', 'logging',
'bili_api',
'header', 'header',
'danmaku', 'danmaku',
'recorder', 'recorder',

View File

@ -50,6 +50,10 @@ class TaskParam:
path_template: str path_template: str
filesize_limit: int filesize_limit: int
duration_limit: int duration_limit: int
# BiliApiSettings
base_api_url: str
base_live_api_url: str
base_play_info_api_url: str
# HeaderSettings # HeaderSettings
user_agent: str user_agent: str
cookie: str cookie: str

View File

@ -190,6 +190,30 @@ class RecordTask:
yield DanmakuFileDetail(path=path, size=size, status=status) 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 @property
def user_agent(self) -> str: def user_agent(self) -> str:
return self._live.user_agent return self._live.user_agent
@ -460,22 +484,13 @@ class RecordTask:
await self._recorder.stop() await self._recorder.stop()
await self._postprocessor.stop() await self._postprocessor.stop()
@aio_task_with_room_id
async def update_info(self, raise_exception: bool = False) -> bool: async def update_info(self, raise_exception: bool = False) -> bool:
return await self._live.update_info(raise_exception=raise_exception) return await self._live.update_info(raise_exception=raise_exception)
@aio_task_with_room_id @aio_task_with_room_id
async def update_session(self) -> None: async def restart_danmaku_client(self) -> None:
if self._monitor_enabled: await self._danmaku_client.restart()
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 _setup(self) -> None: async def _setup(self) -> None:
self._setup_danmaku_client() self._setup_danmaku_client()
@ -488,7 +503,11 @@ class RecordTask:
def _setup_danmaku_client(self) -> None: def _setup_danmaku_client(self) -> None:
self._danmaku_client = DanmakuClient( 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: def _setup_live_monitor(self) -> None:

View File

@ -18,6 +18,7 @@ if TYPE_CHECKING:
from ..setting import ( from ..setting import (
DanmakuSettings, DanmakuSettings,
BiliApiSettings,
HeaderSettings, HeaderSettings,
OutputSettings, OutputSettings,
PostprocessingSettings, PostprocessingSettings,
@ -76,8 +77,11 @@ class RecordTaskManager:
self._tasks[settings.room_id] = task self._tasks[settings.room_id] = task
try: try:
self._settings_manager.apply_task_bili_api_settings(
settings.room_id, settings.bili_api
)
await self._settings_manager.apply_task_header_settings( 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() await task.setup()
@ -222,21 +226,29 @@ class RecordTaskManager:
if coros: if coros:
await asyncio.wait(coros) await asyncio.wait(coros)
async def apply_task_header_settings( def apply_task_bili_api_settings(
self, room_id: int, settings: HeaderSettings, *, update_session: bool = True self, room_id: int, settings: BiliApiSettings
) -> None: ) -> None:
task = self._get_task(room_id) 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 # avoid unnecessary updates that will interrupt connections
if task.user_agent == settings.user_agent and task.cookie == settings.cookie: if task.user_agent == settings.user_agent and task.cookie == settings.cookie:
return return
task.user_agent = settings.user_agent task.user_agent = settings.user_agent
task.cookie = settings.cookie task.cookie = settings.cookie
if restart_danmaku_client:
if update_session: await task.restart_danmaku_client()
# update task session to take the effect
await task.update_session()
def apply_task_output_settings( def apply_task_output_settings(
self, room_id: int, settings: OutputSettings self, room_id: int, settings: OutputSettings
@ -296,6 +308,9 @@ class RecordTaskManager:
path_template=task.path_template, path_template=task.path_template,
filesize_limit=task.filesize_limit, filesize_limit=task.filesize_limit,
duration_limit=task.duration_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, user_agent=task.user_agent,
cookie=task.cookie, cookie=task.cookie,
danmu_uname=task.danmu_uname, danmu_uname=task.danmu_uname,

View File

@ -1,9 +1,11 @@
import os
from abc import ABC, abstractmethod
import asyncio import asyncio
import os
import threading import threading
from abc import ABC, abstractmethod
from typing import Awaitable, TypeVar, final from typing import Awaitable, TypeVar, final
from blrec.logging.room_id import aio_task_with_room_id
class SwitchableMixin(ABC): class SwitchableMixin(ABC):
def __init__(self) -> None: def __init__(self) -> None:
@ -127,21 +129,25 @@ class AsyncCooperationMixin(ABC):
# call submit_exception in a coroutine # call submit_exception in a coroutine
# workaround for `RuntimeError: no running event loop` # workaround for `RuntimeError: no running event loop`
submit_exception(exc) submit_exception(exc)
self._run_coroutine(wrapper()) self._run_coroutine(wrapper())
def _run_coroutine(self, coro: Awaitable[_T]) -> _T: 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() return future.result()
@aio_task_with_room_id
async def _with_room_id(self, coro: Awaitable[_T]) -> _T:
return await coro
class SupportDebugMixin(ABC): class SupportDebugMixin(ABC):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
def _init_for_debug(self, room_id: int) -> None: def _init_for_debug(self, room_id: int) -> None:
if ( if (value := os.environ.get('DEBUG')) and (
(value := os.environ.get('DEBUG')) and value == '*' or room_id in value.split(',')
(value == '*' or room_id in value.split(','))
): ):
self._debug = True self._debug = True
self._debug_dir = os.path.expanduser(f'~/.blrec/debug/{room_id}') self._debug_dir = os.path.expanduser(f'~/.blrec/debug/{room_id}')

View File

@ -44,6 +44,7 @@ AliasKeyOfSettings = Literal[
'tasks', 'tasks',
'output', 'output',
'logging', 'logging',
'biliApi',
'header', 'header',
'danmaku', 'danmaku',
'recorder', 'recorder',

View File

@ -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>

View File

@ -0,0 +1 @@
@use '../../shared/styles/setting';

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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>

View File

@ -0,0 +1 @@
@use '../shared/styles/setting';

View File

@ -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();
});
});

View File

@ -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();
});
}
}

View File

@ -3,8 +3,6 @@
nzCentered nzCentered
[(nzVisible)]="visible" [(nzVisible)]="visible"
[nzOkDisabled]="control.invalid || control.value.trim() === value" [nzOkDisabled]="control.invalid || control.value.trim() === value"
(nzOnOk)="handleConfirm()"
(nzOnCancel)="handleCancel()"
> >
<ng-container *nzModalContent> <ng-container *nzModalContent>
<form nz-form [formGroup]="settingsForm"> <form nz-form [formGroup]="settingsForm">
@ -59,7 +57,7 @@
</form> </form>
</ng-container> </ng-container>
<ng-template #modalFooter> <ng-template [nzModalFooter]>
<button <button
nz-button nz-button
nzType="default" nzType="default"

View File

@ -29,6 +29,12 @@
></app-disk-space-settings> ></app-disk-space-settings>
</app-page-section> </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-page-section name="网络请求">
<app-header-settings [settings]="settings.header"></app-header-settings> <app-header-settings [settings]="settings.header"></app-header-settings>
</app-page-section> </app-page-section>

View File

@ -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 { 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 { 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 { 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({ @NgModule({
declarations: [ declarations: [
@ -101,6 +105,10 @@ import { MessageTemplateEditDialogComponent } from './notification-settings/shar
PathTemplateEditDialogComponent, PathTemplateEditDialogComponent,
MessageTemplateSettingsComponent, MessageTemplateSettingsComponent,
MessageTemplateEditDialogComponent, MessageTemplateEditDialogComponent,
BiliApiSettingsComponent,
BaseApiUrlEditDialogComponent,
BaseLiveApiUrlEditDialogComponent,
BasePlayInfoApiUrlEditDialogComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@ -2,6 +2,10 @@ import { CoverSaveStrategy, DeleteStrategy } from '../setting.model';
export const SYNC_FAILED_WARNING_TIP = '设置同步失败!'; 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 = 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\{\}]*?)+?)*$/; /^(?:[^\\\/:*?"<>|\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\{\}]*?)+?)*$/;

View File

@ -19,6 +19,7 @@ type PrimarySettings = Pick<
Settings, Settings,
| 'output' | 'output'
| 'logging' | 'logging'
| 'biliApi'
| 'header' | 'header'
| 'danmaku' | 'danmaku'
| 'recorder' | 'recorder'
@ -41,6 +42,7 @@ export class SettingsResolver implements Resolve<PrimarySettings> {
.getSettings([ .getSettings([
'output', 'output',
'logging', 'logging',
'biliApi',
'header', 'header',
'danmaku', 'danmaku',
'recorder', 'recorder',

View File

@ -1,5 +1,13 @@
import type { Nullable, PartialDeep } from 'src/app/shared/utility-types'; 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 { export interface HeaderSettings {
userAgent: string; userAgent: string;
cookie: string; cookie: string;
@ -65,6 +73,7 @@ export type PostprocessingOptions = Nullable<PostprocessingSettings>;
export interface TaskOptions { export interface TaskOptions {
output: OutputOptions; output: OutputOptions;
biliApi: BiliApiOptions;
header: HeaderOptions; header: HeaderOptions;
danmaku: DanmakuOptions; danmaku: DanmakuOptions;
recorder: RecorderOptions; recorder: RecorderOptions;
@ -81,7 +90,7 @@ export interface TaskSettings extends TaskOptions {
export type GlobalTaskSettings = Pick< export type GlobalTaskSettings = Pick<
Settings, Settings,
'output' | 'header' | 'danmaku' | 'recorder' | 'postprocessing' 'output' | 'biliApi' | 'header' | 'danmaku' | 'recorder' | 'postprocessing'
>; >;
export interface OutputSettings { export interface OutputSettings {
@ -351,6 +360,7 @@ export interface Settings {
tasks: TaskSettings[]; tasks: TaskSettings[];
output: OutputSettings; output: OutputSettings;
logging: LoggingSettings; logging: LoggingSettings;
biliApi: BiliApiSettings;
header: HeaderSettings; header: HeaderSettings;
danmaku: DanmakuSettings; danmaku: DanmakuSettings;
recorder: RecorderSettings; recorder: RecorderSettings;

View File

@ -186,6 +186,7 @@ export class TaskItemComponent implements OnChanges, OnDestroy {
this.settingService.getTaskOptions(this.roomId), this.settingService.getTaskOptions(this.roomId),
this.settingService.getSettings([ this.settingService.getSettings([
'output', 'output',
'biliApi',
'header', 'header',
'danmaku', 'danmaku',
'recorder', 'recorder',

View File

@ -21,7 +21,7 @@
> >
<nz-form-control <nz-form-control
class="setting-control input" class="setting-control input"
[nzErrorTip]="errorTip" [nzErrorTip]="pathTemplateErrorTip"
> >
<input <input
type="text" type="text"
@ -32,7 +32,7 @@
[(ngModel)]="model.output.pathTemplate" [(ngModel)]="model.output.pathTemplate"
[disabled]="options.output.pathTemplate === null" [disabled]="options.output.pathTemplate === null"
/> />
<ng-template #errorTip let-control> <ng-template #pathTemplateErrorTip let-control>
<ng-container *ngIf="control.hasError('required')"> <ng-container *ngIf="control.hasError('required')">
请输入路径模板 请输入路径模板
</ng-container> </ng-container>
@ -745,6 +745,139 @@
</nz-form-item> </nz-form-item>
</div> </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"> <div ngModelGroup="header" class="form-group header">
<h2>网络请求</h2> <h2>网络请求</h2>
<nz-form-item class="setting-item textarea"> <nz-form-item class="setting-item textarea">
@ -761,7 +894,7 @@
? 'warning' ? 'warning'
: userAgent : userAgent
" "
[nzErrorTip]="errorTip" [nzErrorTip]="userAgentErrorTip"
> >
<textarea <textarea
#userAgent="ngModel" #userAgent="ngModel"
@ -774,7 +907,7 @@
[disabled]="options.header.userAgent === null" [disabled]="options.header.userAgent === null"
></textarea> ></textarea>
</nz-form-control> </nz-form-control>
<ng-template #errorTip let-control> <ng-template #userAgentErrorTip let-control>
<ng-container *ngIf="control.hasError('required')"> <ng-container *ngIf="control.hasError('required')">
请输入 User Agent 请输入 User Agent
</ng-container> </ng-container>

View File

@ -29,6 +29,7 @@ import {
DELETE_STRATEGIES, DELETE_STRATEGIES,
COVER_SAVE_STRATEGIES, COVER_SAVE_STRATEGIES,
RECORDING_MODE_OPTIONS, RECORDING_MODE_OPTIONS,
BASE_URL_PATTERN,
} from '../../settings/shared/constants/form'; } from '../../settings/shared/constants/form';
type OptionsModel = NonNullable<TaskOptions>; type OptionsModel = NonNullable<TaskOptions>;
@ -55,6 +56,7 @@ export class TaskSettingsDialogComponent implements OnChanges {
readonly warningTip = readonly warningTip =
'需要重启弹幕客户端才能生效,如果任务正在录制可能会丢失弹幕!'; '需要重启弹幕客户端才能生效,如果任务正在录制可能会丢失弹幕!';
readonly baseUrlPattern = BASE_URL_PATTERN;
readonly pathTemplatePattern = PATH_TEMPLATE_PATTERN; readonly pathTemplatePattern = PATH_TEMPLATE_PATTERN;
readonly streamFormatOptions = cloneDeep(STREAM_FORMAT_OPTIONS) as Mutable< readonly streamFormatOptions = cloneDeep(STREAM_FORMAT_OPTIONS) as Mutable<
typeof STREAM_FORMAT_OPTIONS typeof STREAM_FORMAT_OPTIONS
@ -111,7 +113,6 @@ export class TaskSettingsDialogComponent implements OnChanges {
} }
handleConfirm(): void { handleConfirm(): void {
debugger;
this.confirm.emit(difference(this.options, this.taskOptions!)); this.confirm.emit(difference(this.options, this.taskOptions!));
this.close(); this.close();
} }