feat: custom BASE API URL
This commit is contained in:
parent
a2fe0bf01b
commit
91ccd8c595
@ -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,111 +93,97 @@ 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',
|
||||||
'codec': '0,1', # 0: avc, 1: hevc
|
'codec': '0,1', # 0: avc, 1: hevc
|
||||||
'device': 'android',
|
'device': 'android',
|
||||||
'device_name': 'Unknown',
|
'device_name': 'Unknown',
|
||||||
'disable_rcmd': '0',
|
'disable_rcmd': '0',
|
||||||
'dolby': '1',
|
'dolby': '1',
|
||||||
'format': '0,1,2', # 0: flv, 1: ts, 2: fmp4
|
'format': '0,1,2', # 0: flv, 1: ts, 2: fmp4
|
||||||
'free_type': '0',
|
'free_type': '0',
|
||||||
'http': '1',
|
'http': '1',
|
||||||
'mask': '0',
|
'mask': '0',
|
||||||
'mobi_app': 'android',
|
'mobi_app': 'android',
|
||||||
'need_hdr': '0',
|
'need_hdr': '0',
|
||||||
'no_playurl': '0',
|
'no_playurl': '0',
|
||||||
'only_audio': '1' if only_audio else '0',
|
'only_audio': '1' if only_audio else '0',
|
||||||
'only_video': '1' if only_video else '0',
|
'only_video': '1' if only_video else '0',
|
||||||
'platform': 'android',
|
'platform': 'android',
|
||||||
'play_type': '0',
|
'play_type': '0',
|
||||||
'protocol': '0,1',
|
'protocol': '0,1',
|
||||||
'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',
|
||||||
'device': 'android',
|
'device': 'android',
|
||||||
'mobi_app': 'android',
|
'mobi_app': 'android',
|
||||||
'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',
|
||||||
'device': 'android',
|
'device': 'android',
|
||||||
'mobi_app': 'android',
|
'mobi_app': 'android',
|
||||||
'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']
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
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>
|
<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>
|
File diff suppressed because one or more lines are too long
@ -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": [
|
||||||
|
@ -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,
|
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',
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -19,6 +19,7 @@ KeyOfSettings = Literal[
|
|||||||
'tasks',
|
'tasks',
|
||||||
'output',
|
'output',
|
||||||
'logging',
|
'logging',
|
||||||
|
'bili_api',
|
||||||
'header',
|
'header',
|
||||||
'danmaku',
|
'danmaku',
|
||||||
'recorder',
|
'recorder',
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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,
|
||||||
|
@ -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}')
|
||||||
|
@ -44,6 +44,7 @@ AliasKeyOfSettings = Literal[
|
|||||||
'tasks',
|
'tasks',
|
||||||
'output',
|
'output',
|
||||||
'logging',
|
'logging',
|
||||||
|
'biliApi',
|
||||||
'header',
|
'header',
|
||||||
'danmaku',
|
'danmaku',
|
||||||
'recorder',
|
'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
|
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"
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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\{\}]*?)+?)*$/;
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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;
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user