mirror of
https://github.com/acgnhiki/blrec.git
synced 2024-12-28 09:30:13 +08:00
feat: improve customization of base api url
This commit is contained in:
parent
7dd894e256
commit
473fef2acc
@ -1,8 +1,10 @@
|
|||||||
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, Mapping, Optional
|
from typing import Any, Dict, List, Mapping, Optional
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@ -16,15 +18,17 @@ __all__ = 'AppApi', 'WebApi'
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TRACE_API_REQ = bool(os.environ.get('TRACE_API_REQ'))
|
||||||
|
|
||||||
|
|
||||||
class BaseApi(ABC):
|
class BaseApi(ABC):
|
||||||
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__(
|
def __init__(
|
||||||
self, session: aiohttp.ClientSession, headers: Optional[Dict[str, str]] = None
|
self, session: aiohttp.ClientSession, headers: Optional[Dict[str, str]] = None
|
||||||
):
|
):
|
||||||
|
self.base_api_urls: List[str] = ['https://api.bilibili.com']
|
||||||
|
self.base_live_api_urls: List[str] = ['https://api.live.bilibili.com']
|
||||||
|
self.base_play_info_api_urls: List[str] = ['https://api.live.bilibili.com']
|
||||||
|
|
||||||
self._session = session
|
self._session = session
|
||||||
self.headers = headers or {}
|
self.headers = headers or {}
|
||||||
self.timeout = 10
|
self.timeout = 10
|
||||||
@ -45,15 +49,62 @@ class BaseApi(ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@retry(reraise=True, stop=stop_after_delay(5), wait=wait_exponential(0.1))
|
@retry(reraise=True, stop=stop_after_delay(5), wait=wait_exponential(0.1))
|
||||||
async def _get_json(self, *args: Any, **kwds: Any) -> JsonResponse:
|
async def _get_json_res(self, *args: Any, **kwds: Any) -> JsonResponse:
|
||||||
async with self._session.get(
|
kwds = {'timeout': self.timeout, 'headers': self.headers, **kwds}
|
||||||
*args, **kwds, timeout=self.timeout, headers=self.headers
|
async with self._session.get(*args, **kwds) as res:
|
||||||
) as res:
|
if TRACE_API_REQ:
|
||||||
logger.debug(f'real url: {res.real_url}')
|
logger.debug(f'Request info: {res.request_info}')
|
||||||
|
try:
|
||||||
json_res = await res.json()
|
json_res = await res.json()
|
||||||
|
except aiohttp.ContentTypeError:
|
||||||
|
text_res = await res.text()
|
||||||
|
logger.debug(f'Response text: {text_res[:200]}')
|
||||||
|
raise
|
||||||
self._check_response(json_res)
|
self._check_response(json_res)
|
||||||
return json_res
|
return json_res
|
||||||
|
|
||||||
|
async def _get_json(
|
||||||
|
self, base_urls: List[str], path: str, *args: Any, **kwds: Any
|
||||||
|
) -> JsonResponse:
|
||||||
|
if not base_urls:
|
||||||
|
raise ValueError('No base urls')
|
||||||
|
exception = None
|
||||||
|
for base_url in base_urls:
|
||||||
|
url = base_url + path
|
||||||
|
try:
|
||||||
|
return await self._get_json_res(url, *args, **kwds)
|
||||||
|
except Exception as exc:
|
||||||
|
exception = exc
|
||||||
|
if TRACE_API_REQ:
|
||||||
|
logger.debug(f'Failed to get json from {url}', exc_info=exc)
|
||||||
|
else:
|
||||||
|
assert exception is not None
|
||||||
|
raise exception
|
||||||
|
|
||||||
|
async def _get_jsons_concurrently(
|
||||||
|
self, base_urls: List[str], path: str, *args: Any, **kwds: Any
|
||||||
|
) -> List[JsonResponse]:
|
||||||
|
if not base_urls:
|
||||||
|
raise ValueError('No base urls')
|
||||||
|
urls = [base_url + path for base_url in base_urls]
|
||||||
|
aws = (self._get_json_res(url, *args, **kwds) for url in urls)
|
||||||
|
results = await asyncio.gather(*aws, return_exceptions=True)
|
||||||
|
exceptions = []
|
||||||
|
json_responses = []
|
||||||
|
for idx, item in enumerate(results):
|
||||||
|
if isinstance(item, Exception):
|
||||||
|
if TRACE_API_REQ:
|
||||||
|
logger.debug(f'Failed to get json from {urls[idx]}', exc_info=item)
|
||||||
|
exceptions.append(item)
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
json_responses.append(item)
|
||||||
|
else:
|
||||||
|
if TRACE_API_REQ:
|
||||||
|
logger.debug(repr(item))
|
||||||
|
if not json_responses:
|
||||||
|
raise exceptions[0]
|
||||||
|
return json_responses
|
||||||
|
|
||||||
|
|
||||||
class AppApi(BaseApi):
|
class AppApi(BaseApi):
|
||||||
# taken from https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/other/API_sign.md # noqa
|
# taken from https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/other/API_sign.md # noqa
|
||||||
@ -85,15 +136,15 @@ class AppApi(BaseApi):
|
|||||||
params.update(sign=sign)
|
params.update(sign=sign)
|
||||||
return params
|
return params
|
||||||
|
|
||||||
async def get_room_play_info(
|
async def get_room_play_infos(
|
||||||
self,
|
self,
|
||||||
room_id: int,
|
room_id: int,
|
||||||
qn: QualityNumber = 10000,
|
qn: QualityNumber = 10000,
|
||||||
*,
|
*,
|
||||||
only_video: bool = False,
|
only_video: bool = False,
|
||||||
only_audio: bool = False,
|
only_audio: bool = False,
|
||||||
) -> ResponseData:
|
) -> List[ResponseData]:
|
||||||
url = self.base_play_info_api_url + '/xlive/app-room/v2/index/getRoomPlayInfo'
|
path = '/xlive/app-room/v2/index/getRoomPlayInfo'
|
||||||
params = self.signed(
|
params = self.signed(
|
||||||
{
|
{
|
||||||
'actionKey': 'appkey',
|
'actionKey': 'appkey',
|
||||||
@ -121,11 +172,13 @@ class AppApi(BaseApi):
|
|||||||
'ts': int(datetime.utcnow().timestamp()),
|
'ts': int(datetime.utcnow().timestamp()),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
r = await self._get_json(url, params=params, headers=self._headers)
|
json_responses = await self._get_jsons_concurrently(
|
||||||
return r['data']
|
self.base_play_info_api_urls, path, params=params
|
||||||
|
)
|
||||||
|
return [r['data'] for r in json_responses]
|
||||||
|
|
||||||
async def get_info_by_room(self, room_id: int) -> ResponseData:
|
async def get_info_by_room(self, room_id: int) -> ResponseData:
|
||||||
url = self.base_live_api_url + '/xlive/app-room/v1/index/getInfoByRoom'
|
path = '/xlive/app-room/v1/index/getInfoByRoom'
|
||||||
params = self.signed(
|
params = self.signed(
|
||||||
{
|
{
|
||||||
'actionKey': 'appkey',
|
'actionKey': 'appkey',
|
||||||
@ -138,11 +191,12 @@ class AppApi(BaseApi):
|
|||||||
'ts': int(datetime.utcnow().timestamp()),
|
'ts': int(datetime.utcnow().timestamp()),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
r = await self._get_json(url, params=params)
|
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||||
return r['data']
|
return json_res['data']
|
||||||
|
|
||||||
async def get_user_info(self, uid: int) -> ResponseData:
|
async def get_user_info(self, uid: int) -> ResponseData:
|
||||||
url = self.base_api_url + '/x/v2/space'
|
base_api_urls = ['https://app.bilibili.com']
|
||||||
|
path = '/x/v2/space'
|
||||||
params = self.signed(
|
params = self.signed(
|
||||||
{
|
{
|
||||||
'build': '6640400',
|
'build': '6640400',
|
||||||
@ -153,11 +207,11 @@ class AppApi(BaseApi):
|
|||||||
'vmid': uid,
|
'vmid': uid,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
r = await self._get_json(url, params=params)
|
json_res = await self._get_json(base_api_urls, path, params=params)
|
||||||
return r['data']
|
return json_res['data']
|
||||||
|
|
||||||
async def get_danmu_info(self, room_id: int) -> ResponseData:
|
async def get_danmu_info(self, room_id: int) -> ResponseData:
|
||||||
url = self.base_live_api_url + '/xlive/app-room/v1/index/getDanmuInfo'
|
path = '/xlive/app-room/v1/index/getDanmuInfo'
|
||||||
params = self.signed(
|
params = self.signed(
|
||||||
{
|
{
|
||||||
'actionKey': 'appkey',
|
'actionKey': 'appkey',
|
||||||
@ -170,20 +224,21 @@ class AppApi(BaseApi):
|
|||||||
'ts': int(datetime.utcnow().timestamp()),
|
'ts': int(datetime.utcnow().timestamp()),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
r = await self._get_json(url, params=params)
|
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||||
return r['data']
|
return json_res['data']
|
||||||
|
|
||||||
|
|
||||||
class WebApi(BaseApi):
|
class WebApi(BaseApi):
|
||||||
async def room_init(self, room_id: int) -> ResponseData:
|
async def room_init(self, room_id: int) -> ResponseData:
|
||||||
url = self.base_live_api_url + '/room/v1/Room/room_init'
|
path = '/room/v1/Room/room_init'
|
||||||
r = await self._get_json(url, params={'id': room_id})
|
params = {'id': room_id}
|
||||||
return r['data']
|
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||||
|
return json_res['data']
|
||||||
|
|
||||||
async def get_room_play_info(
|
async def get_room_play_infos(
|
||||||
self, room_id: int, qn: QualityNumber = 10000
|
self, room_id: int, qn: QualityNumber = 10000
|
||||||
) -> ResponseData:
|
) -> List[ResponseData]:
|
||||||
url = self.base_play_info_api_url + '/xlive/web-room/v2/index/getRoomPlayInfo'
|
path = '/xlive/web-room/v2/index/getRoomPlayInfo'
|
||||||
params = {
|
params = {
|
||||||
'room_id': room_id,
|
'room_id': room_id,
|
||||||
'protocol': '0,1',
|
'protocol': '0,1',
|
||||||
@ -193,34 +248,37 @@ class WebApi(BaseApi):
|
|||||||
'platform': 'web',
|
'platform': 'web',
|
||||||
'ptype': 8,
|
'ptype': 8,
|
||||||
}
|
}
|
||||||
r = await self._get_json(url, params=params)
|
json_responses = await self._get_jsons_concurrently(
|
||||||
return r['data']
|
self.base_play_info_api_urls, path, params=params
|
||||||
|
)
|
||||||
|
return [r['data'] for r in json_responses]
|
||||||
|
|
||||||
async def get_info_by_room(self, room_id: int) -> ResponseData:
|
async def get_info_by_room(self, room_id: int) -> ResponseData:
|
||||||
url = self.base_live_api_url + '/xlive/web-room/v1/index/getInfoByRoom'
|
path = '/xlive/web-room/v1/index/getInfoByRoom'
|
||||||
params = {'room_id': room_id}
|
params = {'room_id': room_id}
|
||||||
r = await self._get_json(url, params=params)
|
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||||
return r['data']
|
return json_res['data']
|
||||||
|
|
||||||
async def get_info(self, room_id: int) -> ResponseData:
|
async def get_info(self, room_id: int) -> ResponseData:
|
||||||
url = self.base_live_api_url + '/room/v1/Room/get_info'
|
path = '/room/v1/Room/get_info'
|
||||||
params = {'room_id': room_id}
|
params = {'room_id': room_id}
|
||||||
r = await self._get_json(url, params=params)
|
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||||
return r['data']
|
return json_res['data']
|
||||||
|
|
||||||
async def get_timestamp(self) -> int:
|
async def get_timestamp(self) -> int:
|
||||||
url = self.base_live_api_url + '/av/v1/Time/getTimestamp?platform=pc'
|
path = '/av/v1/Time/getTimestamp'
|
||||||
r = await self._get_json(url)
|
params = {'platform': 'pc'}
|
||||||
return r['data']['timestamp']
|
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||||
|
return json_res['data']['timestamp']
|
||||||
|
|
||||||
async def get_user_info(self, uid: int) -> ResponseData:
|
async def get_user_info(self, uid: int) -> ResponseData:
|
||||||
url = self.base_api_url + '/x/space/acc/info'
|
path = '/x/space/acc/info'
|
||||||
params = {'mid': uid}
|
params = {'mid': uid}
|
||||||
r = await self._get_json(url, params=params)
|
json_res = await self._get_json(self.base_api_urls, path, params=params)
|
||||||
return r['data']
|
return json_res['data']
|
||||||
|
|
||||||
async def get_danmu_info(self, room_id: int) -> ResponseData:
|
async def get_danmu_info(self, room_id: int) -> ResponseData:
|
||||||
url = self.base_live_api_url + '/xlive/web-room/v1/index/getDanmuInfo'
|
path = '/xlive/web-room/v1/index/getDanmuInfo'
|
||||||
params = {'id': room_id}
|
params = {'id': room_id}
|
||||||
r = await self._get_json(url, params=params)
|
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||||
return r['data']
|
return json_res['data']
|
||||||
|
@ -3,7 +3,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, cast
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from jsonpath import jsonpath
|
from jsonpath import jsonpath
|
||||||
@ -52,31 +52,31 @@ class Live:
|
|||||||
self._user_info: UserInfo
|
self._user_info: UserInfo
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_api_url(self) -> str:
|
def base_api_urls(self) -> List[str]:
|
||||||
return self._webapi.base_api_url
|
return self._webapi.base_api_urls
|
||||||
|
|
||||||
@base_api_url.setter
|
@base_api_urls.setter
|
||||||
def base_api_url(self, value: str) -> None:
|
def base_api_urls(self, value: List[str]) -> None:
|
||||||
self._webapi.base_api_url = value
|
self._webapi.base_api_urls = value
|
||||||
self._appapi.base_api_url = value
|
self._appapi.base_api_urls = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_live_api_url(self) -> str:
|
def base_live_api_urls(self) -> List[str]:
|
||||||
return self._webapi.base_live_api_url
|
return self._webapi.base_live_api_urls
|
||||||
|
|
||||||
@base_live_api_url.setter
|
@base_live_api_urls.setter
|
||||||
def base_live_api_url(self, value: str) -> None:
|
def base_live_api_urls(self, value: List[str]) -> None:
|
||||||
self._webapi.base_live_api_url = value
|
self._webapi.base_live_api_urls = value
|
||||||
self._appapi.base_live_api_url = value
|
self._appapi.base_live_api_urls = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_play_info_api_url(self) -> str:
|
def base_play_info_api_urls(self) -> List[str]:
|
||||||
return self._webapi.base_play_info_api_url
|
return self._webapi.base_play_info_api_urls
|
||||||
|
|
||||||
@base_play_info_api_url.setter
|
@base_play_info_api_urls.setter
|
||||||
def base_play_info_api_url(self, value: str) -> None:
|
def base_play_info_api_urls(self, value: List[str]) -> None:
|
||||||
self._webapi.base_play_info_api_url = value
|
self._webapi.base_play_info_api_urls = value
|
||||||
self._appapi.base_play_info_api_url = value
|
self._appapi.base_play_info_api_urls = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_agent(self) -> str:
|
def user_agent(self) -> str:
|
||||||
@ -102,7 +102,7 @@ class Live:
|
|||||||
def headers(self) -> Dict[str, str]:
|
def headers(self) -> Dict[str, str]:
|
||||||
return {
|
return {
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en;q=0.3,en-US;q=0.2',
|
'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', # noqa
|
||||||
'Referer': f'https://live.bilibili.com/{self._room_id}',
|
'Referer': f'https://live.bilibili.com/{self._room_id}',
|
||||||
'Origin': 'https://live.bilibili.com',
|
'Origin': 'https://live.bilibili.com',
|
||||||
'Connection': 'keep-alive',
|
'Connection': 'keep-alive',
|
||||||
@ -249,13 +249,14 @@ class Live:
|
|||||||
select_alternative: bool = False,
|
select_alternative: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
if api_platform == 'web':
|
if api_platform == 'web':
|
||||||
info = await self._webapi.get_room_play_info(self._room_id, qn)
|
paly_infos = await self._webapi.get_room_play_infos(self._room_id, qn)
|
||||||
else:
|
else:
|
||||||
info = await self._appapi.get_room_play_info(self._room_id, qn)
|
paly_infos = await self._appapi.get_room_play_infos(self._room_id, qn)
|
||||||
|
|
||||||
|
for info in paly_infos:
|
||||||
self._check_room_play_info(info)
|
self._check_room_play_info(info)
|
||||||
|
|
||||||
streams = jsonpath(info, '$.playurl_info.playurl.stream[*]')
|
streams = jsonpath(paly_infos, '$[*].playurl_info.playurl.stream[*]')
|
||||||
if not streams:
|
if not streams:
|
||||||
raise NoStreamAvailable(stream_format, stream_codec, qn)
|
raise NoStreamAvailable(stream_format, stream_codec, qn)
|
||||||
formats = jsonpath(
|
formats = jsonpath(
|
||||||
@ -266,10 +267,10 @@ class Live:
|
|||||||
codecs = jsonpath(formats, f'$[*].codec[?(@.codec_name == "{stream_codec}")]')
|
codecs = jsonpath(formats, f'$[*].codec[?(@.codec_name == "{stream_codec}")]')
|
||||||
if not codecs:
|
if not codecs:
|
||||||
raise NoStreamCodecAvailable(stream_format, stream_codec, qn)
|
raise NoStreamCodecAvailable(stream_format, stream_codec, qn)
|
||||||
codec = codecs[0]
|
|
||||||
|
|
||||||
accept_qn = cast(List[QualityNumber], codec['accept_qn'])
|
accept_qns = jsonpath(codecs, '$[*].accept_qn[*]')
|
||||||
if qn not in accept_qn or codec['current_qn'] != qn:
|
current_qns = jsonpath(codecs, '$[*].current_qn')
|
||||||
|
if qn not in accept_qns or not all(map(lambda q: q == qn, current_qns)):
|
||||||
raise NoStreamQualityAvailable(stream_format, stream_codec, qn)
|
raise NoStreamQualityAvailable(stream_format, stream_codec, qn)
|
||||||
|
|
||||||
def sort_by_host(info: Any) -> int:
|
def sort_by_host(info: Any) -> int:
|
||||||
@ -282,16 +283,23 @@ class Live:
|
|||||||
return 1
|
return 1
|
||||||
if num == '08':
|
if num == '08':
|
||||||
return 2
|
return 2
|
||||||
|
if num == '05':
|
||||||
|
return 3
|
||||||
|
if num == '07':
|
||||||
|
return 4
|
||||||
return 1000 + int(num)
|
return 1000 + int(num)
|
||||||
elif re.search(r'cn-[a-z]+-[a-z]+', host):
|
|
||||||
return 2000
|
|
||||||
elif 'mcdn' in host:
|
elif 'mcdn' in host:
|
||||||
|
return 2000
|
||||||
|
elif re.search(r'cn-[a-z]+-[a-z]+', host):
|
||||||
return 5000
|
return 5000
|
||||||
else:
|
else:
|
||||||
return 10000
|
return 10000
|
||||||
|
|
||||||
url_info = sorted(codec['url_info'], key=sort_by_host)
|
url_infos = sorted(
|
||||||
urls = [i['host'] + codec['base_url'] + i['extra'] for i in url_info]
|
({**i, 'base_url': c['base_url']} for c in codecs for i in c['url_info']),
|
||||||
|
key=sort_by_host,
|
||||||
|
)
|
||||||
|
urls = [i['host'] + i['base_url'] + i['extra'] for i in url_infos]
|
||||||
|
|
||||||
if not select_alternative:
|
if not select_alternative:
|
||||||
return urls[0]
|
return urls[0]
|
||||||
|
@ -141,7 +141,7 @@ class FLVStreamRecorderImpl(StreamRecorderImpl, SupportDebugMixin):
|
|||||||
self._stream_parser,
|
self._stream_parser,
|
||||||
self._connection_error_handler,
|
self._connection_error_handler,
|
||||||
self._request_exception_handler,
|
self._request_exception_handler,
|
||||||
flv_ops.process(sort_tags=True, trace=self._debug),
|
flv_ops.process(sort_tags=True),
|
||||||
self._cutter,
|
self._cutter,
|
||||||
self._limiter,
|
self._limiter,
|
||||||
self._join_point_extractor,
|
self._join_point_extractor,
|
||||||
|
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/183.ee55fc76717674c3.js
Normal file
1
src/blrec/data/webapp/183.ee55fc76717674c3.js
Normal file
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/205.cf2caa9b46b14212.js
Normal file
1
src/blrec/data/webapp/205.cf2caa9b46b14212.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
1
src/blrec/data/webapp/91.cab8652a2fa56b1a.js
Normal file
1
src/blrec/data/webapp/91.cab8652a2fa56b1a.js
Normal file
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.4ae765ab3bddf383.js" type="module"></script><script src="polyfills.4b08448aee19bb22.js" type="module"></script><script src="main.27d1fff16f7909f2.js" type="module"></script>
|
<script src="runtime.c6818dbcd7b06106.js" type="module"></script><script src="polyfills.4b08448aee19bb22.js" type="module"></script><script src="main.6da8ea192405b948.js" type="module"></script>
|
||||||
|
|
||||||
</body></html>
|
</body></html>
|
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/main.6da8ea192405b948.js
Normal file
1
src/blrec/data/webapp/main.6da8ea192405b948.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"configVersion": 1,
|
"configVersion": 1,
|
||||||
"timestamp": 1661135687643,
|
"timestamp": 1661579095139,
|
||||||
"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",
|
||||||
"/170.d0e14a28ee578d1f.js",
|
"/183.ee55fc76717674c3.js",
|
||||||
"/183.0d3cd9f454be16fb.js",
|
"/205.cf2caa9b46b14212.js",
|
||||||
"/45.c90c3cea2bf1a66e.js",
|
"/45.c90c3cea2bf1a66e.js",
|
||||||
"/91.9ff409a090dace5c.js",
|
"/91.cab8652a2fa56b1a.js",
|
||||||
"/common.858f777e9296e6f2.js",
|
"/common.858f777e9296e6f2.js",
|
||||||
"/index.html",
|
"/index.html",
|
||||||
"/main.27d1fff16f7909f2.js",
|
"/main.6da8ea192405b948.js",
|
||||||
"/manifest.webmanifest",
|
"/manifest.webmanifest",
|
||||||
"/polyfills.4b08448aee19bb22.js",
|
"/polyfills.4b08448aee19bb22.js",
|
||||||
"/runtime.4ae765ab3bddf383.js",
|
"/runtime.c6818dbcd7b06106.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",
|
||||||
"/170.d0e14a28ee578d1f.js": "d6b6208ca442565ed39300b27ab8cbe5501cb46a",
|
"/183.ee55fc76717674c3.js": "2628c996ec80a6c6703d542d34ac95194283bcf8",
|
||||||
"/183.0d3cd9f454be16fb.js": "e7e6ebc715791102fd09edabe2aa47316208b29c",
|
"/205.cf2caa9b46b14212.js": "749df896fbbd279dcf49318963f0ce074c5df87f",
|
||||||
"/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764",
|
"/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764",
|
||||||
"/91.9ff409a090dace5c.js": "d756ffe7cd3f5516e40a7e6d6cf494ea6213a546",
|
"/91.cab8652a2fa56b1a.js": "c11ebf28472c8a75653f7b27b5cffdec477830fe",
|
||||||
"/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": "29167783eb093ffa93369f741a5ce20a534137de",
|
"/index.html": "80797fa46f33b7bcf402788a5d0d0516b77f23b1",
|
||||||
"/main.27d1fff16f7909f2.js": "22e63726601a31af1a96e7901afc0d2bea7fd414",
|
"/main.6da8ea192405b948.js": "b8995c7d8ccd465769b90936db5e0a337a827a58",
|
||||||
"/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586",
|
"/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586",
|
||||||
"/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d",
|
"/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d",
|
||||||
"/runtime.4ae765ab3bddf383.js": "96653fd35d3ad9684e603011436e9d43a1121690",
|
"/runtime.c6818dbcd7b06106.js": "00160f946c5d007a956f5f61293cbd3bed2756dc",
|
||||||
"/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(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))})()})();
|
(()=>{"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,f,o)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,f,o]=e[n],c=!0,l=0;l<t.length;l++)(!1&o||a>=o)&&Object.keys(r.O).every(p=>r.O[p](t[l]))?t.splice(l--,1):(c=!1,o<a&&(a=o));if(c){e.splice(n--,1);var d=f();void 0!==d&&(i=d)}}return i}o=o||0;for(var n=e.length;n>0&&e[n-1][2]>o;n--)e[n]=e[n-1];e[n]=[t,f,o]},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:"cab8652a2fa56b1a",103:"5b5d2a6e5a8a7479",146:"5a8902910bda9e87",183:"ee55fc76717674c3",205:"cf2caa9b46b14212",592:"858f777e9296e6f2"}[e]+".js",r.miniCssF=e=>{},r.o=(e,i)=>Object.prototype.hasOwnProperty.call(e,i),(()=>{var e={},i="blrec:";r.l=(t,f,o,n)=>{if(e[t])e[t].push(f);else{var a,c;if(void 0!==o)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+o){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+o),a.src=r.tu(t)),e[t]=[f];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=(f,o)=>{var n=r.o(e,f)?e[f]:void 0;if(0!==n)if(n)o.push(n[2]);else if(666!=f){var a=new Promise((u,s)=>n=e[f]=[u,s]);o.push(n[2]=a);var c=r.p+r.u(f),l=new Error;r.l(c,u=>{if(r.o(e,f)&&(0!==(n=e[f])&&(e[f]=void 0),n)){var s=u&&("load"===u.type?"missing":u.type),b=u&&u.target&&u.target.src;l.message="Loading chunk "+f+" failed.\n("+s+": "+b+")",l.name="ChunkLoadError",l.type=s,l.request=b,n[1](l)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var i=(f,o)=>{var l,d,[n,a,c]=o,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(f&&f(o);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))})()})();
|
@ -17,15 +17,13 @@ __all__ = ('process',)
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def process(
|
def process(sort_tags: bool = False) -> Callable[[FLVStream], FLVStream]:
|
||||||
sort_tags: bool = False, trace: bool = False
|
|
||||||
) -> Callable[[FLVStream], FLVStream]:
|
|
||||||
def _process(source: FLVStream) -> FLVStream:
|
def _process(source: FLVStream) -> FLVStream:
|
||||||
if sort_tags:
|
if sort_tags:
|
||||||
return source.pipe(
|
return source.pipe(
|
||||||
defragment(),
|
defragment(),
|
||||||
split(),
|
split(),
|
||||||
sort(trace=trace),
|
sort(),
|
||||||
ops.filter(lambda v: not is_avc_end_sequence_tag(v)), # type: ignore
|
ops.filter(lambda v: not is_avc_end_sequence_tag(v)), # type: ignore
|
||||||
correct(),
|
correct(),
|
||||||
fix(),
|
fix(),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from typing import Callable, List, Optional
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
from reactivex import Observable, abc
|
from reactivex import Observable, abc
|
||||||
@ -21,8 +22,10 @@ __all__ = ('sort',)
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TRACE_OP_SORT = bool(os.environ.get('TRACE_OP_SORT'))
|
||||||
|
|
||||||
def sort(trace: bool = False) -> Callable[[FLVStream], FLVStream]:
|
|
||||||
|
def sort() -> Callable[[FLVStream], FLVStream]:
|
||||||
"Sort tags in GOP by timestamp to ensure subsequent operators work as expected."
|
"Sort tags in GOP by timestamp to ensure subsequent operators work as expected."
|
||||||
|
|
||||||
def _sort(source: FLVStream) -> FLVStream:
|
def _sort(source: FLVStream) -> FLVStream:
|
||||||
@ -43,7 +46,7 @@ def sort(trace: bool = False) -> Callable[[FLVStream], FLVStream]:
|
|||||||
if not gop_tags:
|
if not gop_tags:
|
||||||
return
|
return
|
||||||
|
|
||||||
if trace:
|
if TRACE_OP_SORT:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Tags in GOP:\n'
|
'Tags in GOP:\n'
|
||||||
f'Number of tags: {len(gop_tags)}\n'
|
f'Number of tags: {len(gop_tags)}\n'
|
||||||
|
@ -1,26 +1,25 @@
|
|||||||
|
import logging
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any, BinaryIO, Dict, Mapping, TypedDict
|
from typing import Any, BinaryIO, Mapping, TypedDict
|
||||||
|
|
||||||
|
|
||||||
from .amf import AMFReader, AMFWriter
|
from .amf import AMFReader, AMFWriter
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'load',
|
'load',
|
||||||
'loads',
|
'loads',
|
||||||
'dump',
|
'dump',
|
||||||
'dumps',
|
'dumps',
|
||||||
|
|
||||||
'ScriptData',
|
'ScriptData',
|
||||||
|
|
||||||
'ScriptDataParser',
|
'ScriptDataParser',
|
||||||
'ScriptDataDumper',
|
'ScriptDataDumper',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ScriptData(TypedDict):
|
class ScriptData(TypedDict):
|
||||||
name: str
|
name: str
|
||||||
value: Dict[str, Any]
|
value: Any
|
||||||
|
|
||||||
|
|
||||||
class ScriptDataParser:
|
class ScriptDataParser:
|
||||||
@ -29,7 +28,17 @@ class ScriptDataParser:
|
|||||||
|
|
||||||
def parse(self) -> ScriptData:
|
def parse(self) -> ScriptData:
|
||||||
name = self._parse_name()
|
name = self._parse_name()
|
||||||
|
try:
|
||||||
value = self._parse_value()
|
value = self._parse_value()
|
||||||
|
except EOFError:
|
||||||
|
logger.debug(f'No script data: {name}')
|
||||||
|
value = {}
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
if name == 'onMetaData':
|
||||||
|
logger.debug(f'Invalid onMetaData: {value}')
|
||||||
|
value = {}
|
||||||
|
else:
|
||||||
|
logger.debug(f'Unusual script data: {name}, {value}')
|
||||||
return ScriptData(name=name, value=value)
|
return ScriptData(name=name, value=value)
|
||||||
|
|
||||||
def _parse_name(self) -> str:
|
def _parse_name(self) -> str:
|
||||||
@ -37,10 +46,8 @@ class ScriptDataParser:
|
|||||||
assert isinstance(value, str)
|
assert isinstance(value, str)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def _parse_value(self) -> Dict[str, Any]:
|
def _parse_value(self) -> Any:
|
||||||
value = self._reader.read_value()
|
return self._reader.read_value()
|
||||||
assert isinstance(value, dict)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class ScriptDataDumper:
|
class ScriptDataDumper:
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
from .helpers import shadow_settings, update_settings
|
from .helpers import shadow_settings, update_settings
|
||||||
from .models import (
|
from .models import (
|
||||||
DEFAULT_SETTINGS_FILE,
|
DEFAULT_SETTINGS_FILE,
|
||||||
|
BiliApiSettings,
|
||||||
DanmakuOptions,
|
DanmakuOptions,
|
||||||
DanmakuSettings,
|
DanmakuSettings,
|
||||||
EmailMessageTemplateSettings,
|
EmailMessageTemplateSettings,
|
||||||
EmailNotificationSettings,
|
EmailNotificationSettings,
|
||||||
EmailSettings,
|
EmailSettings,
|
||||||
EnvSettings,
|
EnvSettings,
|
||||||
BiliApiOptions,
|
|
||||||
BiliApiSettings,
|
|
||||||
HeaderOptions,
|
HeaderOptions,
|
||||||
HeaderSettings,
|
HeaderSettings,
|
||||||
LoggingSettings,
|
LoggingSettings,
|
||||||
@ -47,7 +46,6 @@ __all__ = (
|
|||||||
'Settings',
|
'Settings',
|
||||||
'SettingsIn',
|
'SettingsIn',
|
||||||
'SettingsOut',
|
'SettingsOut',
|
||||||
'BiliApiOptions',
|
|
||||||
'BiliApiSettings',
|
'BiliApiSettings',
|
||||||
'HeaderOptions',
|
'HeaderOptions',
|
||||||
'HeaderSettings',
|
'HeaderSettings',
|
||||||
|
@ -35,7 +35,6 @@ __all__ = (
|
|||||||
'Settings',
|
'Settings',
|
||||||
'SettingsIn',
|
'SettingsIn',
|
||||||
'SettingsOut',
|
'SettingsOut',
|
||||||
'BiliApiOptions',
|
|
||||||
'BiliApiSettings',
|
'BiliApiSettings',
|
||||||
'HeaderOptions',
|
'HeaderOptions',
|
||||||
'HeaderSettings',
|
'HeaderSettings',
|
||||||
@ -112,16 +111,10 @@ class BaseModel(PydanticBaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BiliApiOptions(BaseModel):
|
class BiliApiSettings(BaseModel):
|
||||||
base_api_url: Optional[str]
|
base_api_urls: List[str] = ['https://api.bilibili.com']
|
||||||
base_live_api_url: Optional[str]
|
base_live_api_urls: List[str] = ['https://api.live.bilibili.com']
|
||||||
base_play_info_api_url: Optional[str]
|
base_play_info_api_urls: List[str] = ['https://api.live.bilibili.com']
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
@ -299,7 +292,6 @@ 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()
|
||||||
@ -309,14 +301,7 @@ 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={
|
include={'output', 'header', 'danmaku', 'recorder', 'postprocessing'}
|
||||||
'output',
|
|
||||||
'bili_api',
|
|
||||||
'header',
|
|
||||||
'danmaku',
|
|
||||||
'recorder',
|
|
||||||
'postprocessing',
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ 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,
|
||||||
@ -211,13 +210,6 @@ 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,
|
self,
|
||||||
room_id: int,
|
room_id: int,
|
||||||
@ -277,8 +269,10 @@ class SettingsManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def apply_bili_api_settings(self) -> None:
|
def apply_bili_api_settings(self) -> None:
|
||||||
for settings in self._settings.tasks:
|
for task_settings in self._settings.tasks:
|
||||||
self.apply_task_bili_api_settings(settings.room_id, settings.bili_api)
|
self._app._task_manager.apply_task_bili_api_settings(
|
||||||
|
task_settings.room_id, self._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:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
@ -51,9 +51,9 @@ class TaskParam:
|
|||||||
filesize_limit: int
|
filesize_limit: int
|
||||||
duration_limit: int
|
duration_limit: int
|
||||||
# BiliApiSettings
|
# BiliApiSettings
|
||||||
base_api_url: str
|
base_api_urls: List[str]
|
||||||
base_live_api_url: str
|
base_live_api_urls: List[str]
|
||||||
base_play_info_api_url: str
|
base_play_info_api_urls: List[str]
|
||||||
# HeaderSettings
|
# HeaderSettings
|
||||||
user_agent: str
|
user_agent: str
|
||||||
cookie: str
|
cookie: str
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
from typing import Iterator, Optional
|
from typing import Iterator, List, Optional
|
||||||
|
|
||||||
from blrec.bili.danmaku_client import DanmakuClient
|
from blrec.bili.danmaku_client import DanmakuClient
|
||||||
from blrec.bili.live import Live
|
from blrec.bili.live import Live
|
||||||
@ -200,28 +200,28 @@ class RecordTask:
|
|||||||
yield DanmakuFileDetail(path=path, size=size, status=status)
|
yield DanmakuFileDetail(path=path, size=size, status=status)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_api_url(self) -> str:
|
def base_api_urls(self) -> List[str]:
|
||||||
return self._live.base_api_url
|
return self._live.base_api_urls
|
||||||
|
|
||||||
@base_api_url.setter
|
@base_api_urls.setter
|
||||||
def base_api_url(self, value: str) -> None:
|
def base_api_urls(self, value: List[str]) -> None:
|
||||||
self._live.base_api_url = value
|
self._live.base_api_urls = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_live_api_url(self) -> str:
|
def base_live_api_urls(self) -> List[str]:
|
||||||
return self._live.base_live_api_url
|
return self._live.base_live_api_urls
|
||||||
|
|
||||||
@base_live_api_url.setter
|
@base_live_api_urls.setter
|
||||||
def base_live_api_url(self, value: str) -> None:
|
def base_live_api_urls(self, value: List[str]) -> None:
|
||||||
self._live.base_live_api_url = value
|
self._live.base_live_api_urls = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_play_info_api_url(self) -> str:
|
def base_play_info_api_urls(self) -> List[str]:
|
||||||
return self._live.base_play_info_api_url
|
return self._live.base_play_info_api_urls
|
||||||
|
|
||||||
@base_play_info_api_url.setter
|
@base_play_info_api_urls.setter
|
||||||
def base_play_info_api_url(self, value: str) -> None:
|
def base_play_info_api_urls(self, value: List[str]) -> None:
|
||||||
self._live.base_play_info_api_url = value
|
self._live.base_play_info_api_urls = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_agent(self) -> str:
|
def user_agent(self) -> str:
|
||||||
|
@ -77,9 +77,9 @@ class RecordTaskManager:
|
|||||||
self._tasks[settings.room_id] = task
|
self._tasks[settings.room_id] = task
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._settings_manager.apply_task_bili_api_settings(
|
bili_api = self._settings_manager.get_settings({'bili_api'}).bili_api
|
||||||
settings.room_id, settings.bili_api
|
assert bili_api is not None
|
||||||
)
|
self.apply_task_bili_api_settings(settings.room_id, bili_api)
|
||||||
await self._settings_manager.apply_task_header_settings(
|
await self._settings_manager.apply_task_header_settings(
|
||||||
settings.room_id, settings.header, restart_danmaku_client=False
|
settings.room_id, settings.header, restart_danmaku_client=False
|
||||||
)
|
)
|
||||||
@ -230,9 +230,9 @@ class RecordTaskManager:
|
|||||||
self, room_id: int, settings: BiliApiSettings
|
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_api_urls = settings.base_api_urls
|
||||||
task.base_live_api_url = settings.base_live_api_url
|
task.base_live_api_urls = settings.base_live_api_urls
|
||||||
task.base_play_info_api_url = settings.base_play_info_api_url
|
task.base_play_info_api_urls = settings.base_play_info_api_urls
|
||||||
|
|
||||||
async def apply_task_header_settings(
|
async def apply_task_header_settings(
|
||||||
self,
|
self,
|
||||||
@ -308,9 +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_api_urls=task.base_api_urls,
|
||||||
base_live_api_url=task.base_live_api_url,
|
base_live_api_urls=task.base_live_api_urls,
|
||||||
base_play_info_api_url=task.base_play_info_api_url,
|
base_play_info_api_urls=task.base_play_info_api_urls,
|
||||||
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,5 +1,5 @@
|
|||||||
<nz-modal
|
<nz-modal
|
||||||
nzTitle="修改 BASE API URL"
|
nzTitle="修改主站 API 主机地址"
|
||||||
nzCentered
|
nzCentered
|
||||||
[(nzVisible)]="visible"
|
[(nzVisible)]="visible"
|
||||||
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
||||||
@ -8,13 +8,19 @@
|
|||||||
<form nz-form [formGroup]="settingsForm">
|
<form nz-form [formGroup]="settingsForm">
|
||||||
<nz-form-item>
|
<nz-form-item>
|
||||||
<nz-form-control [nzErrorTip]="errorTip">
|
<nz-form-control [nzErrorTip]="errorTip">
|
||||||
<input type="text" required nz-input formControlName="baseApiUrl" />
|
<textarea
|
||||||
|
[rows]="5"
|
||||||
|
wrap="soft"
|
||||||
|
nz-input
|
||||||
|
required
|
||||||
|
formControlName="baseApiUrls"
|
||||||
|
></textarea>
|
||||||
<ng-template #errorTip let-control>
|
<ng-template #errorTip let-control>
|
||||||
<ng-container *ngIf="control.hasError('required')">
|
<ng-container *ngIf="control.hasError('required')">
|
||||||
不能为空
|
不能为空
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="control.hasError('pattern')">
|
<ng-container *ngIf="control.hasError('baseUrl')">
|
||||||
输入无效
|
输入无效: {{ control.getError("baseUrl").value | json }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</nz-form-control>
|
</nz-form-control>
|
||||||
|
@ -13,10 +13,8 @@ import {
|
|||||||
FormGroup,
|
FormGroup,
|
||||||
Validators,
|
Validators,
|
||||||
} from '@angular/forms';
|
} from '@angular/forms';
|
||||||
import {
|
import { BASE_API_URL_DEFAULT } from '../../shared/constants/form';
|
||||||
BASE_API_URL_DEFAULT,
|
import { baseUrlValidator } from '../../shared/directives/base-url-validator.directive';
|
||||||
BASE_URL_PATTERN,
|
|
||||||
} from '../../shared/constants/form';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-base-api-url-edit-dialog',
|
selector: 'app-base-api-url-edit-dialog',
|
||||||
@ -25,11 +23,11 @@ import {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BaseApiUrlEditDialogComponent implements OnChanges {
|
export class BaseApiUrlEditDialogComponent implements OnChanges {
|
||||||
@Input() value = '';
|
@Input() value = [];
|
||||||
@Input() visible = false;
|
@Input() visible = false;
|
||||||
@Output() visibleChange = new EventEmitter<boolean>();
|
@Output() visibleChange = new EventEmitter<boolean>();
|
||||||
@Output() cancel = new EventEmitter<undefined>();
|
@Output() cancel = new EventEmitter<undefined>();
|
||||||
@Output() confirm = new EventEmitter<string>();
|
@Output() confirm = new EventEmitter<string[]>();
|
||||||
|
|
||||||
readonly settingsForm: FormGroup;
|
readonly settingsForm: FormGroup;
|
||||||
readonly defaultBaseApiUrl = BASE_API_URL_DEFAULT;
|
readonly defaultBaseApiUrl = BASE_API_URL_DEFAULT;
|
||||||
@ -39,15 +37,12 @@ export class BaseApiUrlEditDialogComponent implements OnChanges {
|
|||||||
private changeDetector: ChangeDetectorRef
|
private changeDetector: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
this.settingsForm = formBuilder.group({
|
this.settingsForm = formBuilder.group({
|
||||||
baseApiUrl: [
|
baseApiUrls: ['', [Validators.required, baseUrlValidator()]],
|
||||||
'',
|
|
||||||
[Validators.required, Validators.pattern(BASE_URL_PATTERN)],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get control() {
|
get control() {
|
||||||
return this.settingsForm.get('baseApiUrl') as FormControl;
|
return this.settingsForm.get('baseApiUrls') as FormControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
@ -70,7 +65,7 @@ export class BaseApiUrlEditDialogComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setValue(): void {
|
setValue(): void {
|
||||||
this.control.setValue(this.value);
|
this.control.setValue(this.value.join('\n'));
|
||||||
this.changeDetector.markForCheck();
|
this.changeDetector.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +75,12 @@ export class BaseApiUrlEditDialogComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleConfirm(): void {
|
handleConfirm(): void {
|
||||||
this.confirm.emit(this.control.value.trim());
|
const value = this.control.value as string;
|
||||||
|
const baseUrls = value
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => !!line);
|
||||||
|
this.confirm.emit(baseUrls);
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<nz-modal
|
<nz-modal
|
||||||
nzTitle="修改 BASE LIVE API URL"
|
nzTitle="修改直播 API 主机地址"
|
||||||
nzCentered
|
nzCentered
|
||||||
[(nzVisible)]="visible"
|
[(nzVisible)]="visible"
|
||||||
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
||||||
@ -8,18 +8,19 @@
|
|||||||
<form nz-form [formGroup]="settingsForm">
|
<form nz-form [formGroup]="settingsForm">
|
||||||
<nz-form-item>
|
<nz-form-item>
|
||||||
<nz-form-control [nzErrorTip]="errorTip">
|
<nz-form-control [nzErrorTip]="errorTip">
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
[rows]="5"
|
||||||
required
|
wrap="soft"
|
||||||
nz-input
|
nz-input
|
||||||
formControlName="baseLiveApiUrl"
|
required
|
||||||
/>
|
formControlName="baseLiveApiUrls"
|
||||||
|
></textarea>
|
||||||
<ng-template #errorTip let-control>
|
<ng-template #errorTip let-control>
|
||||||
<ng-container *ngIf="control.hasError('required')">
|
<ng-container *ngIf="control.hasError('required')">
|
||||||
不能为空
|
不能为空
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="control.hasError('pattern')">
|
<ng-container *ngIf="control.hasError('baseUrl')">
|
||||||
输入无效
|
输入无效: {{ control.getError("baseUrl").value | json }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</nz-form-control>
|
</nz-form-control>
|
||||||
|
@ -13,10 +13,8 @@ import {
|
|||||||
FormGroup,
|
FormGroup,
|
||||||
Validators,
|
Validators,
|
||||||
} from '@angular/forms';
|
} from '@angular/forms';
|
||||||
import {
|
import { BASE_LIVE_API_URL_DEFAULT } from '../../shared/constants/form';
|
||||||
BASE_LIVE_API_URL_DEFAULT,
|
import { baseUrlValidator } from '../../shared/directives/base-url-validator.directive';
|
||||||
BASE_URL_PATTERN,
|
|
||||||
} from '../../shared/constants/form';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-base-live-api-url-edit-dialog',
|
selector: 'app-base-live-api-url-edit-dialog',
|
||||||
@ -25,11 +23,11 @@ import {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BaseLiveApiUrlEditDialogComponent implements OnChanges {
|
export class BaseLiveApiUrlEditDialogComponent implements OnChanges {
|
||||||
@Input() value = '';
|
@Input() value = [];
|
||||||
@Input() visible = false;
|
@Input() visible = false;
|
||||||
@Output() visibleChange = new EventEmitter<boolean>();
|
@Output() visibleChange = new EventEmitter<boolean>();
|
||||||
@Output() cancel = new EventEmitter<undefined>();
|
@Output() cancel = new EventEmitter<undefined>();
|
||||||
@Output() confirm = new EventEmitter<string>();
|
@Output() confirm = new EventEmitter<string[]>();
|
||||||
|
|
||||||
readonly settingsForm: FormGroup;
|
readonly settingsForm: FormGroup;
|
||||||
readonly defaultBaseLiveApiUrl = BASE_LIVE_API_URL_DEFAULT;
|
readonly defaultBaseLiveApiUrl = BASE_LIVE_API_URL_DEFAULT;
|
||||||
@ -39,15 +37,12 @@ export class BaseLiveApiUrlEditDialogComponent implements OnChanges {
|
|||||||
private changeDetector: ChangeDetectorRef
|
private changeDetector: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
this.settingsForm = formBuilder.group({
|
this.settingsForm = formBuilder.group({
|
||||||
baseLiveApiUrl: [
|
baseLiveApiUrls: ['', [Validators.required, baseUrlValidator()]],
|
||||||
'',
|
|
||||||
[Validators.required, Validators.pattern(BASE_URL_PATTERN)],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get control() {
|
get control() {
|
||||||
return this.settingsForm.get('baseLiveApiUrl') as FormControl;
|
return this.settingsForm.get('baseLiveApiUrls') as FormControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
@ -70,7 +65,7 @@ export class BaseLiveApiUrlEditDialogComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setValue(): void {
|
setValue(): void {
|
||||||
this.control.setValue(this.value);
|
this.control.setValue(this.value.join('\n'));
|
||||||
this.changeDetector.markForCheck();
|
this.changeDetector.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +75,12 @@ export class BaseLiveApiUrlEditDialogComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleConfirm(): void {
|
handleConfirm(): void {
|
||||||
this.confirm.emit(this.control.value.trim());
|
const value = this.control.value as string;
|
||||||
|
const baseUrls = value
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => !!line);
|
||||||
|
this.confirm.emit(baseUrls);
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<nz-modal
|
<nz-modal
|
||||||
nzTitle="修改 BASE PLAY INFO API URL"
|
nzTitle="修改直播流 API 主机地址"
|
||||||
nzCentered
|
nzCentered
|
||||||
[(nzVisible)]="visible"
|
[(nzVisible)]="visible"
|
||||||
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
||||||
@ -8,18 +8,19 @@
|
|||||||
<form nz-form [formGroup]="settingsForm">
|
<form nz-form [formGroup]="settingsForm">
|
||||||
<nz-form-item>
|
<nz-form-item>
|
||||||
<nz-form-control [nzErrorTip]="errorTip">
|
<nz-form-control [nzErrorTip]="errorTip">
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
[rows]="5"
|
||||||
required
|
wrap="soft"
|
||||||
nz-input
|
nz-input
|
||||||
formControlName="basePlayInfoApiUrl"
|
required
|
||||||
/>
|
formControlName="basePlayInfoApiUrls"
|
||||||
|
></textarea>
|
||||||
<ng-template #errorTip let-control>
|
<ng-template #errorTip let-control>
|
||||||
<ng-container *ngIf="control.hasError('required')">
|
<ng-container *ngIf="control.hasError('required')">
|
||||||
不能为空
|
不能为空
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="control.hasError('pattern')">
|
<ng-container *ngIf="control.hasError('baseUrl')">
|
||||||
输入无效
|
输入无效: {{ control.getError("baseUrl").value | json }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</nz-form-control>
|
</nz-form-control>
|
||||||
|
@ -13,10 +13,8 @@ import {
|
|||||||
FormGroup,
|
FormGroup,
|
||||||
Validators,
|
Validators,
|
||||||
} from '@angular/forms';
|
} from '@angular/forms';
|
||||||
import {
|
import { BASE_LIVE_API_URL_DEFAULT } from '../../shared/constants/form';
|
||||||
BASE_LIVE_API_URL_DEFAULT,
|
import { baseUrlValidator } from '../../shared/directives/base-url-validator.directive';
|
||||||
BASE_URL_PATTERN,
|
|
||||||
} from '../../shared/constants/form';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-base-play-info-api-url-edit-dialog',
|
selector: 'app-base-play-info-api-url-edit-dialog',
|
||||||
@ -25,11 +23,11 @@ import {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BasePlayInfoApiUrlEditDialogComponent implements OnChanges {
|
export class BasePlayInfoApiUrlEditDialogComponent implements OnChanges {
|
||||||
@Input() value = '';
|
@Input() value = [];
|
||||||
@Input() visible = false;
|
@Input() visible = false;
|
||||||
@Output() visibleChange = new EventEmitter<boolean>();
|
@Output() visibleChange = new EventEmitter<boolean>();
|
||||||
@Output() cancel = new EventEmitter<undefined>();
|
@Output() cancel = new EventEmitter<undefined>();
|
||||||
@Output() confirm = new EventEmitter<string>();
|
@Output() confirm = new EventEmitter<string[]>();
|
||||||
|
|
||||||
readonly settingsForm: FormGroup;
|
readonly settingsForm: FormGroup;
|
||||||
readonly defaultBasePlayInfoApiUrl = BASE_LIVE_API_URL_DEFAULT;
|
readonly defaultBasePlayInfoApiUrl = BASE_LIVE_API_URL_DEFAULT;
|
||||||
@ -39,15 +37,12 @@ export class BasePlayInfoApiUrlEditDialogComponent implements OnChanges {
|
|||||||
private changeDetector: ChangeDetectorRef
|
private changeDetector: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
this.settingsForm = formBuilder.group({
|
this.settingsForm = formBuilder.group({
|
||||||
basePlayInfoApiUrl: [
|
basePlayInfoApiUrls: ['', [Validators.required, baseUrlValidator()]],
|
||||||
'',
|
|
||||||
[Validators.required, Validators.pattern(BASE_URL_PATTERN)],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get control() {
|
get control() {
|
||||||
return this.settingsForm.get('basePlayInfoApiUrl') as FormControl;
|
return this.settingsForm.get('basePlayInfoApiUrls') as FormControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
@ -70,7 +65,7 @@ export class BasePlayInfoApiUrlEditDialogComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setValue(): void {
|
setValue(): void {
|
||||||
this.control.setValue(this.value);
|
this.control.setValue(this.value.join('\n'));
|
||||||
this.changeDetector.markForCheck();
|
this.changeDetector.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +75,12 @@ export class BasePlayInfoApiUrlEditDialogComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleConfirm(): void {
|
handleConfirm(): void {
|
||||||
this.confirm.emit(this.control.value.trim());
|
const value = this.control.value as string;
|
||||||
|
const baseUrls = value
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => !!line);
|
||||||
|
this.confirm.emit(baseUrls);
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,79 +1,100 @@
|
|||||||
<form nz-form [formGroup]="settingsForm">
|
<form nz-form [formGroup]="settingsForm">
|
||||||
<nz-form-item
|
<nz-form-item
|
||||||
class="setting-item actionable"
|
class="setting-item actionable"
|
||||||
(click)="baseApiUrlEditDialog.open()"
|
(click)="baseApiUrlsEditDialog.open()"
|
||||||
>
|
>
|
||||||
<nz-form-label class="setting-label" [nzTooltipTitle]="baseApiUrlTip"
|
<nz-form-label class="setting-label" [nzTooltipTitle]="baseApiUrlsTip"
|
||||||
>BASE API URL</nz-form-label
|
>主站 API 主机地址</nz-form-label
|
||||||
>
|
>
|
||||||
<ng-template #baseApiUrlTip>
|
<ng-template #baseApiUrlsTip>
|
||||||
<p>主站 API 的 BASE URL</p>
|
<p>设置内容:发送主站 API 请求所用的主机的地址,一行一个。</p>
|
||||||
|
<p>请求方式:先用第一个发送请求,出错就用第二个,以此类推。</p>
|
||||||
|
<p>主要目的:缓解请求过多被风控</p>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<nz-form-control
|
<nz-form-control
|
||||||
[nzWarningTip]="syncFailedWarningTip"
|
[nzWarningTip]="syncFailedWarningTip"
|
||||||
[nzValidateStatus]="syncStatus.baseApiUrl ? baseApiUrlControl : 'warning'"
|
[nzValidateStatus]="
|
||||||
|
syncStatus.baseApiUrls ? baseApiUrlsControl : 'warning'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<nz-form-text class="setting-value"
|
<nz-form-text class="setting-value"
|
||||||
>{{ baseApiUrlControl.value }}
|
>{{ baseApiUrlsControl.value }}
|
||||||
</nz-form-text>
|
</nz-form-text>
|
||||||
<app-base-api-url-edit-dialog
|
<app-base-api-url-edit-dialog
|
||||||
#baseApiUrlEditDialog
|
#baseApiUrlsEditDialog
|
||||||
[value]="baseApiUrlControl.value"
|
[value]="baseApiUrlsControl.value"
|
||||||
(confirm)="baseApiUrlControl.setValue($event)"
|
(confirm)="baseApiUrlsControl.setValue($event)"
|
||||||
></app-base-api-url-edit-dialog>
|
></app-base-api-url-edit-dialog>
|
||||||
</nz-form-control>
|
</nz-form-control>
|
||||||
</nz-form-item>
|
</nz-form-item>
|
||||||
<nz-form-item
|
<nz-form-item
|
||||||
class="setting-item actionable"
|
class="setting-item actionable"
|
||||||
(click)="baseLiveApiUrlEditDialog.open()"
|
(click)="baseLiveApiUrlsEditDialog.open()"
|
||||||
>
|
>
|
||||||
<nz-form-label class="setting-label" [nzTooltipTitle]="baseLiveApiUrlTip"
|
<nz-form-label class="setting-label" [nzTooltipTitle]="baseLiveApiUrlsTip"
|
||||||
>BASE LIVE API URL</nz-form-label
|
>直播 API 主机地址</nz-form-label
|
||||||
>
|
>
|
||||||
<ng-template #baseLiveApiUrlTip>
|
<ng-template #baseLiveApiUrlsTip>
|
||||||
<p>直播 API (getRoomPlayInfo 除外) 的 BASE URL</p>
|
<p>
|
||||||
|
设置内容:发送直播 API (直播流 API getRoomPlayInfo 除外)
|
||||||
|
请求所用的主机的地址,一行一个。
|
||||||
|
</p>
|
||||||
|
<p>请求方式:先用第一个发送请求,出错就用第二个,以此类推。</p>
|
||||||
|
<p>主要目的:缓解请求过多被风控</p>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<nz-form-control
|
<nz-form-control
|
||||||
[nzWarningTip]="syncFailedWarningTip"
|
[nzWarningTip]="syncFailedWarningTip"
|
||||||
[nzValidateStatus]="
|
[nzValidateStatus]="
|
||||||
syncStatus.baseLiveApiUrl ? baseLiveApiUrlControl : 'warning'
|
syncStatus.baseLiveApiUrls ? baseLiveApiUrlsControl : 'warning'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<nz-form-text class="setting-value"
|
<nz-form-text class="setting-value"
|
||||||
>{{ baseLiveApiUrlControl.value }}
|
>{{ baseLiveApiUrlsControl.value }}
|
||||||
</nz-form-text>
|
</nz-form-text>
|
||||||
<app-base-live-api-url-edit-dialog
|
<app-base-live-api-url-edit-dialog
|
||||||
#baseLiveApiUrlEditDialog
|
#baseLiveApiUrlsEditDialog
|
||||||
[value]="baseLiveApiUrlControl.value"
|
[value]="baseLiveApiUrlsControl.value"
|
||||||
(confirm)="baseLiveApiUrlControl.setValue($event)"
|
(confirm)="baseLiveApiUrlsControl.setValue($event)"
|
||||||
></app-base-live-api-url-edit-dialog>
|
></app-base-live-api-url-edit-dialog>
|
||||||
</nz-form-control>
|
</nz-form-control>
|
||||||
</nz-form-item>
|
</nz-form-item>
|
||||||
<nz-form-item
|
<nz-form-item
|
||||||
class="setting-item actionable"
|
class="setting-item actionable"
|
||||||
(click)="basePlayInfoApiUrlEditDialog.open()"
|
(click)="basePlayInfoApiUrlsEditDialog.open()"
|
||||||
>
|
>
|
||||||
<nz-form-label
|
<nz-form-label
|
||||||
class="setting-label"
|
class="setting-label"
|
||||||
[nzTooltipTitle]="basePalyInfoApiUrlTip"
|
[nzTooltipTitle]="basePalyInfoApiUrlTip"
|
||||||
>BASE PLAY INFO API URL</nz-form-label
|
>直播流 API 主机地址</nz-form-label
|
||||||
>
|
>
|
||||||
<ng-template #basePalyInfoApiUrlTip>
|
<ng-template #basePalyInfoApiUrlTip>
|
||||||
<p>直播 API getRoomPlayInfo 的 BASE URL</p>
|
<p>
|
||||||
|
设置内容:发送直播流 API (getRoomPlayInfo)
|
||||||
|
请求所用的主机的地址,一行一个。
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
请求方式:同时并发向全部 API
|
||||||
|
主机发送请求(从全部成功的请求结果中提取直播流质量较好的直播流地址)
|
||||||
|
</p>
|
||||||
|
<p>主要目的:改变录制的直播流的 CDN</p>
|
||||||
|
<p>
|
||||||
|
P.S:国外 IP 的请求结果没有 HLS(fmp4) 流,要同时支持 fmp4 和 flv
|
||||||
|
可以混用国内和国外的 API 主机。
|
||||||
|
</p>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<nz-form-control
|
<nz-form-control
|
||||||
[nzWarningTip]="syncFailedWarningTip"
|
[nzWarningTip]="syncFailedWarningTip"
|
||||||
[nzValidateStatus]="
|
[nzValidateStatus]="
|
||||||
syncStatus.basePlayInfoApiUrl ? basePlayInfoApiUrlControl : 'warning'
|
syncStatus.basePlayInfoApiUrls ? basePlayInfoApiUrlsControl : 'warning'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<nz-form-text class="setting-value"
|
<nz-form-text class="setting-value"
|
||||||
>{{ basePlayInfoApiUrlControl.value }}
|
>{{ basePlayInfoApiUrlsControl.value }}
|
||||||
</nz-form-text>
|
</nz-form-text>
|
||||||
<app-base-play-info-api-url-edit-dialog
|
<app-base-play-info-api-url-edit-dialog
|
||||||
#basePlayInfoApiUrlEditDialog
|
#basePlayInfoApiUrlsEditDialog
|
||||||
[value]="basePlayInfoApiUrlControl.value"
|
[value]="basePlayInfoApiUrlsControl.value"
|
||||||
(confirm)="basePlayInfoApiUrlControl.setValue($event)"
|
(confirm)="basePlayInfoApiUrlsControl.setValue($event)"
|
||||||
></app-base-play-info-api-url-edit-dialog>
|
></app-base-play-info-api-url-edit-dialog>
|
||||||
</nz-form-control>
|
</nz-form-control>
|
||||||
</nz-form-item>
|
</nz-form-item>
|
||||||
|
@ -1 +1,6 @@
|
|||||||
@use '../shared/styles/setting';
|
@use "../shared/styles/setting";
|
||||||
|
@use "src/app/shared/styles/text";
|
||||||
|
|
||||||
|
nz-form-control {
|
||||||
|
@include text.elide-text-overflow;
|
||||||
|
}
|
||||||
|
@ -38,22 +38,22 @@ export class BiliApiSettingsComponent implements OnInit, OnChanges {
|
|||||||
private settingsSyncService: SettingsSyncService
|
private settingsSyncService: SettingsSyncService
|
||||||
) {
|
) {
|
||||||
this.settingsForm = formBuilder.group({
|
this.settingsForm = formBuilder.group({
|
||||||
baseApiUrl: [''],
|
baseApiUrls: [[]],
|
||||||
baseLiveApiUrl: [''],
|
baseLiveApiUrls: [[]],
|
||||||
basePlayInfoApiUrl: [''],
|
basePlayInfoApiUrls: [[]],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get baseApiUrlControl() {
|
get baseApiUrlsControl() {
|
||||||
return this.settingsForm.get('baseApiUrl') as FormControl;
|
return this.settingsForm.get('baseApiUrls') as FormControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
get baseLiveApiUrlControl() {
|
get baseLiveApiUrlsControl() {
|
||||||
return this.settingsForm.get('baseLiveApiUrl') as FormControl;
|
return this.settingsForm.get('baseLiveApiUrls') as FormControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
get basePlayInfoApiUrlControl() {
|
get basePlayInfoApiUrlsControl() {
|
||||||
return this.settingsForm.get('basePlayInfoApiUrl') as FormControl;
|
return this.settingsForm.get('basePlayInfoApiUrls') as FormControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
@ -66,7 +66,8 @@ export class BiliApiSettingsComponent implements OnInit, OnChanges {
|
|||||||
.syncSettings(
|
.syncSettings(
|
||||||
'biliApi',
|
'biliApi',
|
||||||
this.settings,
|
this.settings,
|
||||||
this.settingsForm.valueChanges as Observable<BiliApiSettings>
|
this.settingsForm.valueChanges as Observable<BiliApiSettings>,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
.subscribe((detail) => {
|
.subscribe((detail) => {
|
||||||
this.syncStatus = { ...this.syncStatus, ...calcSyncStatus(detail) };
|
this.syncStatus = { ...this.syncStatus, ...calcSyncStatus(detail) };
|
||||||
|
@ -34,6 +34,7 @@ import { WebhookSettingsResolver } from './shared/services/webhook-settings.reso
|
|||||||
import { SettingsRoutingModule } from './settings-routing.module';
|
import { SettingsRoutingModule } from './settings-routing.module';
|
||||||
import { SettingsComponent } from './settings.component';
|
import { SettingsComponent } from './settings.component';
|
||||||
import { SwitchActionableDirective } from './shared/directives/switch-actionable.directive';
|
import { SwitchActionableDirective } from './shared/directives/switch-actionable.directive';
|
||||||
|
import { BaseUrlValidatorDirective } from './shared/directives/base-url-validator.directive';
|
||||||
import { DiskSpaceSettingsComponent } from './disk-space-settings/disk-space-settings.component';
|
import { DiskSpaceSettingsComponent } from './disk-space-settings/disk-space-settings.component';
|
||||||
import { NotificationSettingsComponent } from './notification-settings/notification-settings.component';
|
import { NotificationSettingsComponent } from './notification-settings/notification-settings.component';
|
||||||
import { LoggingSettingsComponent } from './logging-settings/logging-settings.component';
|
import { LoggingSettingsComponent } from './logging-settings/logging-settings.component';
|
||||||
@ -74,6 +75,7 @@ import { BasePlayInfoApiUrlEditDialogComponent } from './bili-api-settings/base-
|
|||||||
declarations: [
|
declarations: [
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
SwitchActionableDirective,
|
SwitchActionableDirective,
|
||||||
|
BaseUrlValidatorDirective,
|
||||||
DiskSpaceSettingsComponent,
|
DiskSpaceSettingsComponent,
|
||||||
NotificationSettingsComponent,
|
NotificationSettingsComponent,
|
||||||
LoggingSettingsComponent,
|
LoggingSettingsComponent,
|
||||||
|
@ -2,7 +2,6 @@ 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_API_URL_DEFAULT = 'https://api.bilibili.com';
|
||||||
export const BASE_LIVE_API_URL_DEFAULT = 'https://api.live.bilibili.com';
|
export const BASE_LIVE_API_URL_DEFAULT = 'https://api.live.bilibili.com';
|
||||||
|
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import { BaseUrlValidatorDirective } from './base-url-validator.directive';
|
||||||
|
|
||||||
|
describe('BaseUrlValidatorDirective', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
const directive = new BaseUrlValidatorDirective();
|
||||||
|
expect(directive).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,40 @@
|
|||||||
|
import { Directive } from '@angular/core';
|
||||||
|
import {
|
||||||
|
AbstractControl,
|
||||||
|
NG_VALIDATORS,
|
||||||
|
ValidationErrors,
|
||||||
|
ValidatorFn,
|
||||||
|
Validators,
|
||||||
|
} from '@angular/forms';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[appBaseUrlValidator]',
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALIDATORS,
|
||||||
|
useExisting: BaseUrlValidatorDirective,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class BaseUrlValidatorDirective implements Validators {
|
||||||
|
validate(control: AbstractControl): ValidationErrors | null {
|
||||||
|
return baseUrlValidator()(control);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function baseUrlValidator(): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
const value = control.value as string;
|
||||||
|
const lines = value
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => !!line);
|
||||||
|
const invalidValues = lines.filter(
|
||||||
|
(line) => !/^https?:\/\/\S+$/.test(line)
|
||||||
|
);
|
||||||
|
return invalidValues.length > 0
|
||||||
|
? { baseUrl: { value: invalidValues } }
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
}
|
@ -53,14 +53,15 @@ export class SettingsSyncService {
|
|||||||
syncSettings<K extends SK, V extends SV>(
|
syncSettings<K extends SK, V extends SV>(
|
||||||
key: K,
|
key: K,
|
||||||
initialValue: V,
|
initialValue: V,
|
||||||
valueChanges: Observable<V>
|
valueChanges: Observable<V>,
|
||||||
|
deepDiff: boolean = true
|
||||||
): Observable<DetailWithResult<V> | DetailWithError<V>> {
|
): Observable<DetailWithResult<V> | DetailWithError<V>> {
|
||||||
return valueChanges.pipe(
|
return valueChanges.pipe(
|
||||||
scan<V, [V, V, Partial<V>]>(
|
scan<V, [V, V, Partial<V>]>(
|
||||||
([, prev], curr) => [
|
([, prev], curr) => [
|
||||||
prev,
|
prev,
|
||||||
curr,
|
curr,
|
||||||
difference(curr!, prev!) as Partial<V>,
|
difference(curr!, prev!, deepDiff) as Partial<V>,
|
||||||
],
|
],
|
||||||
[initialValue, initialValue, {} as Partial<V>]
|
[initialValue, initialValue, {} as Partial<V>]
|
||||||
),
|
),
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import type { Nullable, PartialDeep } from 'src/app/shared/utility-types';
|
import type { Nullable, PartialDeep } from 'src/app/shared/utility-types';
|
||||||
|
|
||||||
export interface BiliApiSettings {
|
export interface BiliApiSettings {
|
||||||
baseApiUrl: string;
|
baseApiUrls: string[];
|
||||||
baseLiveApiUrl: string;
|
baseLiveApiUrls: string[];
|
||||||
basePlayInfoApiUrl: string;
|
basePlayInfoApiUrls: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BiliApiOptions = Nullable<BiliApiSettings>;
|
export type BiliApiOptions = Nullable<BiliApiSettings>;
|
||||||
|
@ -42,7 +42,7 @@ export class InputDurationComponent implements OnInit, ControlValueAccessor {
|
|||||||
this.formGroup = formBuilder.group({
|
this.formGroup = formBuilder.group({
|
||||||
duration: [
|
duration: [
|
||||||
'',
|
'',
|
||||||
[Validators.required, Validators.pattern(/^\d{2}:[0~5]\d:[0~5]\d$/)],
|
[Validators.required, Validators.pattern(/^\d{2}:[0-5]\d:[0-5]\d$/)],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,11 @@ import { transform, isEqual, isObject } from 'lodash-es';
|
|||||||
import * as filesize from 'filesize';
|
import * as filesize from 'filesize';
|
||||||
|
|
||||||
// ref: https://gist.github.com/Yimiprod/7ee176597fef230d1451
|
// ref: https://gist.github.com/Yimiprod/7ee176597fef230d1451
|
||||||
export function difference(object: object, base: object): object {
|
export function difference(
|
||||||
|
object: object,
|
||||||
|
base: object,
|
||||||
|
deep: boolean = true
|
||||||
|
): object {
|
||||||
function diff(object: object, base: object) {
|
function diff(object: object, base: object) {
|
||||||
return transform(object, (result: object, value: any, key: string) => {
|
return transform(object, (result: object, value: any, key: string) => {
|
||||||
const baseValue = Reflect.get(base, key);
|
const baseValue = Reflect.get(base, key);
|
||||||
@ -10,7 +14,7 @@ export function difference(object: object, base: object): object {
|
|||||||
Reflect.set(
|
Reflect.set(
|
||||||
result,
|
result,
|
||||||
key,
|
key,
|
||||||
isObject(value) && isObject(baseValue)
|
deep && isObject(value) && isObject(baseValue)
|
||||||
? diff(value, baseValue)
|
? diff(value, baseValue)
|
||||||
: value
|
: value
|
||||||
);
|
);
|
||||||
|
@ -186,7 +186,6 @@ 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',
|
||||||
|
@ -745,139 +745,6 @@
|
|||||||
</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">
|
||||||
|
@ -29,7 +29,6 @@ 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>;
|
||||||
@ -56,7 +55,6 @@ 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
|
||||||
|
Loading…
Reference in New Issue
Block a user