mirror of
https://github.com/acgnhiki/blrec.git
synced 2024-12-27 17:10:18 +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 logging
|
||||
import os
|
||||
from abc import ABC
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Mapping, Optional
|
||||
from typing import Any, Dict, List, Mapping, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import aiohttp
|
||||
@ -16,15 +18,17 @@ __all__ = 'AppApi', 'WebApi'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TRACE_API_REQ = bool(os.environ.get('TRACE_API_REQ'))
|
||||
|
||||
|
||||
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__(
|
||||
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.headers = headers or {}
|
||||
self.timeout = 10
|
||||
@ -45,15 +49,62 @@ class BaseApi(ABC):
|
||||
)
|
||||
|
||||
@retry(reraise=True, stop=stop_after_delay(5), wait=wait_exponential(0.1))
|
||||
async def _get_json(self, *args: Any, **kwds: Any) -> JsonResponse:
|
||||
async with self._session.get(
|
||||
*args, **kwds, timeout=self.timeout, headers=self.headers
|
||||
) as res:
|
||||
logger.debug(f'real url: {res.real_url}')
|
||||
async def _get_json_res(self, *args: Any, **kwds: Any) -> JsonResponse:
|
||||
kwds = {'timeout': self.timeout, 'headers': self.headers, **kwds}
|
||||
async with self._session.get(*args, **kwds) as res:
|
||||
if TRACE_API_REQ:
|
||||
logger.debug(f'Request info: {res.request_info}')
|
||||
try:
|
||||
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)
|
||||
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):
|
||||
# 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)
|
||||
return params
|
||||
|
||||
async def get_room_play_info(
|
||||
async def get_room_play_infos(
|
||||
self,
|
||||
room_id: int,
|
||||
qn: QualityNumber = 10000,
|
||||
*,
|
||||
only_video: bool = False,
|
||||
only_audio: bool = False,
|
||||
) -> ResponseData:
|
||||
url = self.base_play_info_api_url + '/xlive/app-room/v2/index/getRoomPlayInfo'
|
||||
) -> List[ResponseData]:
|
||||
path = '/xlive/app-room/v2/index/getRoomPlayInfo'
|
||||
params = self.signed(
|
||||
{
|
||||
'actionKey': 'appkey',
|
||||
@ -121,11 +172,13 @@ class AppApi(BaseApi):
|
||||
'ts': int(datetime.utcnow().timestamp()),
|
||||
}
|
||||
)
|
||||
r = await self._get_json(url, params=params, headers=self._headers)
|
||||
return r['data']
|
||||
json_responses = await self._get_jsons_concurrently(
|
||||
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:
|
||||
url = self.base_live_api_url + '/xlive/app-room/v1/index/getInfoByRoom'
|
||||
path = '/xlive/app-room/v1/index/getInfoByRoom'
|
||||
params = self.signed(
|
||||
{
|
||||
'actionKey': 'appkey',
|
||||
@ -138,11 +191,12 @@ class AppApi(BaseApi):
|
||||
'ts': int(datetime.utcnow().timestamp()),
|
||||
}
|
||||
)
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||
return json_res['data']
|
||||
|
||||
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(
|
||||
{
|
||||
'build': '6640400',
|
||||
@ -153,11 +207,11 @@ class AppApi(BaseApi):
|
||||
'vmid': uid,
|
||||
}
|
||||
)
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
json_res = await self._get_json(base_api_urls, path, params=params)
|
||||
return json_res['data']
|
||||
|
||||
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(
|
||||
{
|
||||
'actionKey': 'appkey',
|
||||
@ -170,20 +224,21 @@ class AppApi(BaseApi):
|
||||
'ts': int(datetime.utcnow().timestamp()),
|
||||
}
|
||||
)
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||
return json_res['data']
|
||||
|
||||
|
||||
class WebApi(BaseApi):
|
||||
async def room_init(self, room_id: int) -> ResponseData:
|
||||
url = self.base_live_api_url + '/room/v1/Room/room_init'
|
||||
r = await self._get_json(url, params={'id': room_id})
|
||||
return r['data']
|
||||
path = '/room/v1/Room/room_init'
|
||||
params = {'id': room_id}
|
||||
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
|
||||
) -> ResponseData:
|
||||
url = self.base_play_info_api_url + '/xlive/web-room/v2/index/getRoomPlayInfo'
|
||||
) -> List[ResponseData]:
|
||||
path = '/xlive/web-room/v2/index/getRoomPlayInfo'
|
||||
params = {
|
||||
'room_id': room_id,
|
||||
'protocol': '0,1',
|
||||
@ -193,34 +248,37 @@ class WebApi(BaseApi):
|
||||
'platform': 'web',
|
||||
'ptype': 8,
|
||||
}
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
json_responses = await self._get_jsons_concurrently(
|
||||
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:
|
||||
url = self.base_live_api_url + '/xlive/web-room/v1/index/getInfoByRoom'
|
||||
path = '/xlive/web-room/v1/index/getInfoByRoom'
|
||||
params = {'room_id': room_id}
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||
return json_res['data']
|
||||
|
||||
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}
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||
return json_res['data']
|
||||
|
||||
async def get_timestamp(self) -> int:
|
||||
url = self.base_live_api_url + '/av/v1/Time/getTimestamp?platform=pc'
|
||||
r = await self._get_json(url)
|
||||
return r['data']['timestamp']
|
||||
path = '/av/v1/Time/getTimestamp'
|
||||
params = {'platform': 'pc'}
|
||||
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:
|
||||
url = self.base_api_url + '/x/space/acc/info'
|
||||
path = '/x/space/acc/info'
|
||||
params = {'mid': uid}
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
json_res = await self._get_json(self.base_api_urls, path, params=params)
|
||||
return json_res['data']
|
||||
|
||||
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}
|
||||
r = await self._get_json(url, params=params)
|
||||
return r['data']
|
||||
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||
return json_res['data']
|
||||
|
@ -3,7 +3,7 @@ import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Dict, List, cast
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import aiohttp
|
||||
from jsonpath import jsonpath
|
||||
@ -52,31 +52,31 @@ class Live:
|
||||
self._user_info: UserInfo
|
||||
|
||||
@property
|
||||
def base_api_url(self) -> str:
|
||||
return self._webapi.base_api_url
|
||||
def base_api_urls(self) -> List[str]:
|
||||
return self._webapi.base_api_urls
|
||||
|
||||
@base_api_url.setter
|
||||
def base_api_url(self, value: str) -> None:
|
||||
self._webapi.base_api_url = value
|
||||
self._appapi.base_api_url = value
|
||||
@base_api_urls.setter
|
||||
def base_api_urls(self, value: List[str]) -> None:
|
||||
self._webapi.base_api_urls = value
|
||||
self._appapi.base_api_urls = value
|
||||
|
||||
@property
|
||||
def base_live_api_url(self) -> str:
|
||||
return self._webapi.base_live_api_url
|
||||
def base_live_api_urls(self) -> List[str]:
|
||||
return self._webapi.base_live_api_urls
|
||||
|
||||
@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
|
||||
@base_live_api_urls.setter
|
||||
def base_live_api_urls(self, value: List[str]) -> None:
|
||||
self._webapi.base_live_api_urls = value
|
||||
self._appapi.base_live_api_urls = value
|
||||
|
||||
@property
|
||||
def base_play_info_api_url(self) -> str:
|
||||
return self._webapi.base_play_info_api_url
|
||||
def base_play_info_api_urls(self) -> List[str]:
|
||||
return self._webapi.base_play_info_api_urls
|
||||
|
||||
@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
|
||||
@base_play_info_api_urls.setter
|
||||
def base_play_info_api_urls(self, value: List[str]) -> None:
|
||||
self._webapi.base_play_info_api_urls = value
|
||||
self._appapi.base_play_info_api_urls = value
|
||||
|
||||
@property
|
||||
def user_agent(self) -> str:
|
||||
@ -102,7 +102,7 @@ class Live:
|
||||
def headers(self) -> Dict[str, str]:
|
||||
return {
|
||||
'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}',
|
||||
'Origin': 'https://live.bilibili.com',
|
||||
'Connection': 'keep-alive',
|
||||
@ -249,13 +249,14 @@ class Live:
|
||||
select_alternative: bool = False,
|
||||
) -> str:
|
||||
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:
|
||||
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)
|
||||
|
||||
streams = jsonpath(info, '$.playurl_info.playurl.stream[*]')
|
||||
streams = jsonpath(paly_infos, '$[*].playurl_info.playurl.stream[*]')
|
||||
if not streams:
|
||||
raise NoStreamAvailable(stream_format, stream_codec, qn)
|
||||
formats = jsonpath(
|
||||
@ -266,10 +267,10 @@ class Live:
|
||||
codecs = jsonpath(formats, f'$[*].codec[?(@.codec_name == "{stream_codec}")]')
|
||||
if not codecs:
|
||||
raise NoStreamCodecAvailable(stream_format, stream_codec, qn)
|
||||
codec = codecs[0]
|
||||
|
||||
accept_qn = cast(List[QualityNumber], codec['accept_qn'])
|
||||
if qn not in accept_qn or codec['current_qn'] != qn:
|
||||
accept_qns = jsonpath(codecs, '$[*].accept_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)
|
||||
|
||||
def sort_by_host(info: Any) -> int:
|
||||
@ -282,16 +283,23 @@ class Live:
|
||||
return 1
|
||||
if num == '08':
|
||||
return 2
|
||||
if num == '05':
|
||||
return 3
|
||||
if num == '07':
|
||||
return 4
|
||||
return 1000 + int(num)
|
||||
elif re.search(r'cn-[a-z]+-[a-z]+', host):
|
||||
return 2000
|
||||
elif 'mcdn' in host:
|
||||
return 2000
|
||||
elif re.search(r'cn-[a-z]+-[a-z]+', host):
|
||||
return 5000
|
||||
else:
|
||||
return 10000
|
||||
|
||||
url_info = sorted(codec['url_info'], key=sort_by_host)
|
||||
urls = [i['host'] + codec['base_url'] + i['extra'] for i in url_info]
|
||||
url_infos = sorted(
|
||||
({**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:
|
||||
return urls[0]
|
||||
|
@ -141,7 +141,7 @@ class FLVStreamRecorderImpl(StreamRecorderImpl, SupportDebugMixin):
|
||||
self._stream_parser,
|
||||
self._connection_error_handler,
|
||||
self._request_exception_handler,
|
||||
flv_ops.process(sort_tags=True, trace=self._debug),
|
||||
flv_ops.process(sort_tags=True),
|
||||
self._cutter,
|
||||
self._limiter,
|
||||
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>
|
||||
<app-root></app-root>
|
||||
<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>
|
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,
|
||||
"timestamp": 1661135687643,
|
||||
"timestamp": 1661579095139,
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
@ -13,16 +13,16 @@
|
||||
"urls": [
|
||||
"/103.5b5d2a6e5a8a7479.js",
|
||||
"/146.5a8902910bda9e87.js",
|
||||
"/170.d0e14a28ee578d1f.js",
|
||||
"/183.0d3cd9f454be16fb.js",
|
||||
"/183.ee55fc76717674c3.js",
|
||||
"/205.cf2caa9b46b14212.js",
|
||||
"/45.c90c3cea2bf1a66e.js",
|
||||
"/91.9ff409a090dace5c.js",
|
||||
"/91.cab8652a2fa56b1a.js",
|
||||
"/common.858f777e9296e6f2.js",
|
||||
"/index.html",
|
||||
"/main.27d1fff16f7909f2.js",
|
||||
"/main.6da8ea192405b948.js",
|
||||
"/manifest.webmanifest",
|
||||
"/polyfills.4b08448aee19bb22.js",
|
||||
"/runtime.4ae765ab3bddf383.js",
|
||||
"/runtime.c6818dbcd7b06106.js",
|
||||
"/styles.2e152d608221c2ee.css"
|
||||
],
|
||||
"patterns": []
|
||||
@ -1636,10 +1636,10 @@
|
||||
"hashTable": {
|
||||
"/103.5b5d2a6e5a8a7479.js": "cc0240f217015b6d4ddcc14f31fcc42e1c1c282a",
|
||||
"/146.5a8902910bda9e87.js": "d9c33c7073662699f00f46f3a384ae5b749fdef9",
|
||||
"/170.d0e14a28ee578d1f.js": "d6b6208ca442565ed39300b27ab8cbe5501cb46a",
|
||||
"/183.0d3cd9f454be16fb.js": "e7e6ebc715791102fd09edabe2aa47316208b29c",
|
||||
"/183.ee55fc76717674c3.js": "2628c996ec80a6c6703d542d34ac95194283bcf8",
|
||||
"/205.cf2caa9b46b14212.js": "749df896fbbd279dcf49318963f0ce074c5df87f",
|
||||
"/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764",
|
||||
"/91.9ff409a090dace5c.js": "d756ffe7cd3f5516e40a7e6d6cf494ea6213a546",
|
||||
"/91.cab8652a2fa56b1a.js": "c11ebf28472c8a75653f7b27b5cffdec477830fe",
|
||||
"/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1",
|
||||
"/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1",
|
||||
"/assets/fill/.gitkeep": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||
@ -3234,11 +3234,11 @@
|
||||
"/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01",
|
||||
"/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068",
|
||||
"/common.858f777e9296e6f2.js": "b68ca68e1e214a2537d96935c23410126cc564dd",
|
||||
"/index.html": "29167783eb093ffa93369f741a5ce20a534137de",
|
||||
"/main.27d1fff16f7909f2.js": "22e63726601a31af1a96e7901afc0d2bea7fd414",
|
||||
"/index.html": "80797fa46f33b7bcf402788a5d0d0516b77f23b1",
|
||||
"/main.6da8ea192405b948.js": "b8995c7d8ccd465769b90936db5e0a337a827a58",
|
||||
"/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586",
|
||||
"/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d",
|
||||
"/runtime.4ae765ab3bddf383.js": "96653fd35d3ad9684e603011436e9d43a1121690",
|
||||
"/runtime.c6818dbcd7b06106.js": "00160f946c5d007a956f5f61293cbd3bed2756dc",
|
||||
"/styles.2e152d608221c2ee.css": "9830389a46daa5b4511e0dd343aad23ca9f9690f"
|
||||
},
|
||||
"navigationUrls": [
|
||||
|
@ -1 +1 @@
|
||||
(()=>{"use strict";var e,v={},m={};function r(e){var i=m[e];if(void 0!==i)return i.exports;var t=m[e]={exports:{}};return v[e].call(t.exports,t,t.exports,r),t.exports}r.m=v,e=[],r.O=(i,t,o,f)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,o,f]=e[n],c=!0,l=0;l<t.length;l++)(!1&f||a>=f)&&Object.keys(r.O).every(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__)
|
||||
|
||||
|
||||
def process(
|
||||
sort_tags: bool = False, trace: bool = False
|
||||
) -> Callable[[FLVStream], FLVStream]:
|
||||
def process(sort_tags: bool = False) -> Callable[[FLVStream], FLVStream]:
|
||||
def _process(source: FLVStream) -> FLVStream:
|
||||
if sort_tags:
|
||||
return source.pipe(
|
||||
defragment(),
|
||||
split(),
|
||||
sort(trace=trace),
|
||||
sort(),
|
||||
ops.filter(lambda v: not is_avc_end_sequence_tag(v)), # type: ignore
|
||||
correct(),
|
||||
fix(),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from reactivex import Observable, abc
|
||||
@ -21,8 +22,10 @@ __all__ = ('sort',)
|
||||
|
||||
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."
|
||||
|
||||
def _sort(source: FLVStream) -> FLVStream:
|
||||
@ -43,7 +46,7 @@ def sort(trace: bool = False) -> Callable[[FLVStream], FLVStream]:
|
||||
if not gop_tags:
|
||||
return
|
||||
|
||||
if trace:
|
||||
if TRACE_OP_SORT:
|
||||
logger.debug(
|
||||
'Tags in GOP:\n'
|
||||
f'Number of tags: {len(gop_tags)}\n'
|
||||
|
@ -1,26 +1,25 @@
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Any, BinaryIO, Dict, Mapping, TypedDict
|
||||
|
||||
from typing import Any, BinaryIO, Mapping, TypedDict
|
||||
|
||||
from .amf import AMFReader, AMFWriter
|
||||
|
||||
|
||||
__all__ = (
|
||||
'load',
|
||||
'loads',
|
||||
'dump',
|
||||
'dumps',
|
||||
|
||||
'ScriptData',
|
||||
|
||||
'ScriptDataParser',
|
||||
'ScriptDataDumper',
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ScriptData(TypedDict):
|
||||
name: str
|
||||
value: Dict[str, Any]
|
||||
value: Any
|
||||
|
||||
|
||||
class ScriptDataParser:
|
||||
@ -29,7 +28,17 @@ class ScriptDataParser:
|
||||
|
||||
def parse(self) -> ScriptData:
|
||||
name = self._parse_name()
|
||||
try:
|
||||
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)
|
||||
|
||||
def _parse_name(self) -> str:
|
||||
@ -37,10 +46,8 @@ class ScriptDataParser:
|
||||
assert isinstance(value, str)
|
||||
return value
|
||||
|
||||
def _parse_value(self) -> Dict[str, Any]:
|
||||
value = self._reader.read_value()
|
||||
assert isinstance(value, dict)
|
||||
return value
|
||||
def _parse_value(self) -> Any:
|
||||
return self._reader.read_value()
|
||||
|
||||
|
||||
class ScriptDataDumper:
|
||||
|
@ -1,14 +1,13 @@
|
||||
from .helpers import shadow_settings, update_settings
|
||||
from .models import (
|
||||
DEFAULT_SETTINGS_FILE,
|
||||
BiliApiSettings,
|
||||
DanmakuOptions,
|
||||
DanmakuSettings,
|
||||
EmailMessageTemplateSettings,
|
||||
EmailNotificationSettings,
|
||||
EmailSettings,
|
||||
EnvSettings,
|
||||
BiliApiOptions,
|
||||
BiliApiSettings,
|
||||
HeaderOptions,
|
||||
HeaderSettings,
|
||||
LoggingSettings,
|
||||
@ -47,7 +46,6 @@ __all__ = (
|
||||
'Settings',
|
||||
'SettingsIn',
|
||||
'SettingsOut',
|
||||
'BiliApiOptions',
|
||||
'BiliApiSettings',
|
||||
'HeaderOptions',
|
||||
'HeaderSettings',
|
||||
|
@ -35,7 +35,6 @@ __all__ = (
|
||||
'Settings',
|
||||
'SettingsIn',
|
||||
'SettingsOut',
|
||||
'BiliApiOptions',
|
||||
'BiliApiSettings',
|
||||
'HeaderOptions',
|
||||
'HeaderSettings',
|
||||
@ -112,16 +111,10 @@ 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 BiliApiSettings(BaseModel):
|
||||
base_api_urls: List[str] = ['https://api.bilibili.com']
|
||||
base_live_api_urls: List[str] = ['https://api.live.bilibili.com']
|
||||
base_play_info_api_urls: List[str] = ['https://api.live.bilibili.com']
|
||||
|
||||
|
||||
class HeaderOptions(BaseModel):
|
||||
@ -299,7 +292,6 @@ class OutputSettings(OutputOptions):
|
||||
|
||||
class TaskOptions(BaseModel):
|
||||
output: OutputOptions = OutputOptions()
|
||||
bili_api: BiliApiOptions = BiliApiOptions()
|
||||
header: HeaderOptions = HeaderOptions()
|
||||
danmaku: DanmakuOptions = DanmakuOptions()
|
||||
recorder: RecorderOptions = RecorderOptions()
|
||||
@ -309,14 +301,7 @@ class TaskOptions(BaseModel):
|
||||
def from_settings(cls, settings: TaskSettings) -> TaskOptions:
|
||||
return cls(
|
||||
**settings.dict(
|
||||
include={
|
||||
'output',
|
||||
'bili_api',
|
||||
'header',
|
||||
'danmaku',
|
||||
'recorder',
|
||||
'postprocessing',
|
||||
}
|
||||
include={'output', 'header', 'danmaku', 'recorder', 'postprocessing'}
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -16,7 +16,6 @@ from ..notification import (
|
||||
from ..webhook import WebHook
|
||||
from .helpers import shadow_settings, update_settings
|
||||
from .models import (
|
||||
BiliApiOptions,
|
||||
DanmakuOptions,
|
||||
HeaderOptions,
|
||||
MessageTemplateSettings,
|
||||
@ -211,13 +210,6 @@ class SettingsManager:
|
||||
settings.enable_recorder = False
|
||||
await self.dump_settings()
|
||||
|
||||
def apply_task_bili_api_settings(
|
||||
self, room_id: int, options: BiliApiOptions
|
||||
) -> None:
|
||||
final_settings = self._settings.bili_api.copy()
|
||||
shadow_settings(options, final_settings)
|
||||
self._app._task_manager.apply_task_bili_api_settings(room_id, final_settings)
|
||||
|
||||
async def apply_task_header_settings(
|
||||
self,
|
||||
room_id: int,
|
||||
@ -277,8 +269,10 @@ class SettingsManager:
|
||||
)
|
||||
|
||||
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)
|
||||
for task_settings in self._settings.tasks:
|
||||
self._app._task_manager.apply_task_bili_api_settings(
|
||||
task_settings.room_id, self._settings.bili_api
|
||||
)
|
||||
|
||||
async def apply_header_settings(self) -> None:
|
||||
for settings in self._settings.tasks:
|
||||
|
@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
import attr
|
||||
|
||||
@ -51,9 +51,9 @@ class TaskParam:
|
||||
filesize_limit: int
|
||||
duration_limit: int
|
||||
# BiliApiSettings
|
||||
base_api_url: str
|
||||
base_live_api_url: str
|
||||
base_play_info_api_url: str
|
||||
base_api_urls: List[str]
|
||||
base_live_api_urls: List[str]
|
||||
base_play_info_api_urls: List[str]
|
||||
# HeaderSettings
|
||||
user_agent: str
|
||||
cookie: str
|
||||
|
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
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.live import Live
|
||||
@ -200,28 +200,28 @@ class RecordTask:
|
||||
yield DanmakuFileDetail(path=path, size=size, status=status)
|
||||
|
||||
@property
|
||||
def base_api_url(self) -> str:
|
||||
return self._live.base_api_url
|
||||
def base_api_urls(self) -> List[str]:
|
||||
return self._live.base_api_urls
|
||||
|
||||
@base_api_url.setter
|
||||
def base_api_url(self, value: str) -> None:
|
||||
self._live.base_api_url = value
|
||||
@base_api_urls.setter
|
||||
def base_api_urls(self, value: List[str]) -> None:
|
||||
self._live.base_api_urls = value
|
||||
|
||||
@property
|
||||
def base_live_api_url(self) -> str:
|
||||
return self._live.base_live_api_url
|
||||
def base_live_api_urls(self) -> List[str]:
|
||||
return self._live.base_live_api_urls
|
||||
|
||||
@base_live_api_url.setter
|
||||
def base_live_api_url(self, value: str) -> None:
|
||||
self._live.base_live_api_url = value
|
||||
@base_live_api_urls.setter
|
||||
def base_live_api_urls(self, value: List[str]) -> None:
|
||||
self._live.base_live_api_urls = value
|
||||
|
||||
@property
|
||||
def base_play_info_api_url(self) -> str:
|
||||
return self._live.base_play_info_api_url
|
||||
def base_play_info_api_urls(self) -> List[str]:
|
||||
return self._live.base_play_info_api_urls
|
||||
|
||||
@base_play_info_api_url.setter
|
||||
def base_play_info_api_url(self, value: str) -> None:
|
||||
self._live.base_play_info_api_url = value
|
||||
@base_play_info_api_urls.setter
|
||||
def base_play_info_api_urls(self, value: List[str]) -> None:
|
||||
self._live.base_play_info_api_urls = value
|
||||
|
||||
@property
|
||||
def user_agent(self) -> str:
|
||||
|
@ -77,9 +77,9 @@ class RecordTaskManager:
|
||||
self._tasks[settings.room_id] = task
|
||||
|
||||
try:
|
||||
self._settings_manager.apply_task_bili_api_settings(
|
||||
settings.room_id, settings.bili_api
|
||||
)
|
||||
bili_api = self._settings_manager.get_settings({'bili_api'}).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(
|
||||
settings.room_id, settings.header, restart_danmaku_client=False
|
||||
)
|
||||
@ -230,9 +230,9 @@ class RecordTaskManager:
|
||||
self, room_id: int, settings: BiliApiSettings
|
||||
) -> None:
|
||||
task = self._get_task(room_id)
|
||||
task.base_api_url = settings.base_api_url
|
||||
task.base_live_api_url = settings.base_live_api_url
|
||||
task.base_play_info_api_url = settings.base_play_info_api_url
|
||||
task.base_api_urls = settings.base_api_urls
|
||||
task.base_live_api_urls = settings.base_live_api_urls
|
||||
task.base_play_info_api_urls = settings.base_play_info_api_urls
|
||||
|
||||
async def apply_task_header_settings(
|
||||
self,
|
||||
@ -308,9 +308,9 @@ class RecordTaskManager:
|
||||
path_template=task.path_template,
|
||||
filesize_limit=task.filesize_limit,
|
||||
duration_limit=task.duration_limit,
|
||||
base_api_url=task.base_api_url,
|
||||
base_live_api_url=task.base_live_api_url,
|
||||
base_play_info_api_url=task.base_play_info_api_url,
|
||||
base_api_urls=task.base_api_urls,
|
||||
base_live_api_urls=task.base_live_api_urls,
|
||||
base_play_info_api_urls=task.base_play_info_api_urls,
|
||||
user_agent=task.user_agent,
|
||||
cookie=task.cookie,
|
||||
danmu_uname=task.danmu_uname,
|
||||
|
@ -1,5 +1,5 @@
|
||||
<nz-modal
|
||||
nzTitle="修改 BASE API URL"
|
||||
nzTitle="修改主站 API 主机地址"
|
||||
nzCentered
|
||||
[(nzVisible)]="visible"
|
||||
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
||||
@ -8,13 +8,19 @@
|
||||
<form nz-form [formGroup]="settingsForm">
|
||||
<nz-form-item>
|
||||
<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-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
<ng-container *ngIf="control.hasError('baseUrl')">
|
||||
输入无效: {{ control.getError("baseUrl").value | json }}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
|
@ -13,10 +13,8 @@ import {
|
||||
FormGroup,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
BASE_API_URL_DEFAULT,
|
||||
BASE_URL_PATTERN,
|
||||
} from '../../shared/constants/form';
|
||||
import { BASE_API_URL_DEFAULT } from '../../shared/constants/form';
|
||||
import { baseUrlValidator } from '../../shared/directives/base-url-validator.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-base-api-url-edit-dialog',
|
||||
@ -25,11 +23,11 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BaseApiUrlEditDialogComponent implements OnChanges {
|
||||
@Input() value = '';
|
||||
@Input() value = [];
|
||||
@Input() visible = false;
|
||||
@Output() visibleChange = new EventEmitter<boolean>();
|
||||
@Output() cancel = new EventEmitter<undefined>();
|
||||
@Output() confirm = new EventEmitter<string>();
|
||||
@Output() confirm = new EventEmitter<string[]>();
|
||||
|
||||
readonly settingsForm: FormGroup;
|
||||
readonly defaultBaseApiUrl = BASE_API_URL_DEFAULT;
|
||||
@ -39,15 +37,12 @@ export class BaseApiUrlEditDialogComponent implements OnChanges {
|
||||
private changeDetector: ChangeDetectorRef
|
||||
) {
|
||||
this.settingsForm = formBuilder.group({
|
||||
baseApiUrl: [
|
||||
'',
|
||||
[Validators.required, Validators.pattern(BASE_URL_PATTERN)],
|
||||
],
|
||||
baseApiUrls: ['', [Validators.required, baseUrlValidator()]],
|
||||
});
|
||||
}
|
||||
|
||||
get control() {
|
||||
return this.settingsForm.get('baseApiUrl') as FormControl;
|
||||
return this.settingsForm.get('baseApiUrls') as FormControl;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
@ -70,7 +65,7 @@ export class BaseApiUrlEditDialogComponent implements OnChanges {
|
||||
}
|
||||
|
||||
setValue(): void {
|
||||
this.control.setValue(this.value);
|
||||
this.control.setValue(this.value.join('\n'));
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
@ -80,7 +75,12 @@ export class BaseApiUrlEditDialogComponent implements OnChanges {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<nz-modal
|
||||
nzTitle="修改 BASE LIVE API URL"
|
||||
nzTitle="修改直播 API 主机地址"
|
||||
nzCentered
|
||||
[(nzVisible)]="visible"
|
||||
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
||||
@ -8,18 +8,19 @@
|
||||
<form nz-form [formGroup]="settingsForm">
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="errorTip">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
<textarea
|
||||
[rows]="5"
|
||||
wrap="soft"
|
||||
nz-input
|
||||
formControlName="baseLiveApiUrl"
|
||||
/>
|
||||
required
|
||||
formControlName="baseLiveApiUrls"
|
||||
></textarea>
|
||||
<ng-template #errorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
<ng-container *ngIf="control.hasError('baseUrl')">
|
||||
输入无效: {{ control.getError("baseUrl").value | json }}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
|
@ -13,10 +13,8 @@ import {
|
||||
FormGroup,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
BASE_LIVE_API_URL_DEFAULT,
|
||||
BASE_URL_PATTERN,
|
||||
} from '../../shared/constants/form';
|
||||
import { BASE_LIVE_API_URL_DEFAULT } from '../../shared/constants/form';
|
||||
import { baseUrlValidator } from '../../shared/directives/base-url-validator.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-base-live-api-url-edit-dialog',
|
||||
@ -25,11 +23,11 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BaseLiveApiUrlEditDialogComponent implements OnChanges {
|
||||
@Input() value = '';
|
||||
@Input() value = [];
|
||||
@Input() visible = false;
|
||||
@Output() visibleChange = new EventEmitter<boolean>();
|
||||
@Output() cancel = new EventEmitter<undefined>();
|
||||
@Output() confirm = new EventEmitter<string>();
|
||||
@Output() confirm = new EventEmitter<string[]>();
|
||||
|
||||
readonly settingsForm: FormGroup;
|
||||
readonly defaultBaseLiveApiUrl = BASE_LIVE_API_URL_DEFAULT;
|
||||
@ -39,15 +37,12 @@ export class BaseLiveApiUrlEditDialogComponent implements OnChanges {
|
||||
private changeDetector: ChangeDetectorRef
|
||||
) {
|
||||
this.settingsForm = formBuilder.group({
|
||||
baseLiveApiUrl: [
|
||||
'',
|
||||
[Validators.required, Validators.pattern(BASE_URL_PATTERN)],
|
||||
],
|
||||
baseLiveApiUrls: ['', [Validators.required, baseUrlValidator()]],
|
||||
});
|
||||
}
|
||||
|
||||
get control() {
|
||||
return this.settingsForm.get('baseLiveApiUrl') as FormControl;
|
||||
return this.settingsForm.get('baseLiveApiUrls') as FormControl;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
@ -70,7 +65,7 @@ export class BaseLiveApiUrlEditDialogComponent implements OnChanges {
|
||||
}
|
||||
|
||||
setValue(): void {
|
||||
this.control.setValue(this.value);
|
||||
this.control.setValue(this.value.join('\n'));
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
@ -80,7 +75,12 @@ export class BaseLiveApiUrlEditDialogComponent implements OnChanges {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<nz-modal
|
||||
nzTitle="修改 BASE PLAY INFO API URL"
|
||||
nzTitle="修改直播流 API 主机地址"
|
||||
nzCentered
|
||||
[(nzVisible)]="visible"
|
||||
[nzOkDisabled]="control.invalid || control.value.trim() === value"
|
||||
@ -8,18 +8,19 @@
|
||||
<form nz-form [formGroup]="settingsForm">
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="errorTip">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
<textarea
|
||||
[rows]="5"
|
||||
wrap="soft"
|
||||
nz-input
|
||||
formControlName="basePlayInfoApiUrl"
|
||||
/>
|
||||
required
|
||||
formControlName="basePlayInfoApiUrls"
|
||||
></textarea>
|
||||
<ng-template #errorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
<ng-container *ngIf="control.hasError('baseUrl')">
|
||||
输入无效: {{ control.getError("baseUrl").value | json }}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
|
@ -13,10 +13,8 @@ import {
|
||||
FormGroup,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
BASE_LIVE_API_URL_DEFAULT,
|
||||
BASE_URL_PATTERN,
|
||||
} from '../../shared/constants/form';
|
||||
import { BASE_LIVE_API_URL_DEFAULT } from '../../shared/constants/form';
|
||||
import { baseUrlValidator } from '../../shared/directives/base-url-validator.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-base-play-info-api-url-edit-dialog',
|
||||
@ -25,11 +23,11 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BasePlayInfoApiUrlEditDialogComponent implements OnChanges {
|
||||
@Input() value = '';
|
||||
@Input() value = [];
|
||||
@Input() visible = false;
|
||||
@Output() visibleChange = new EventEmitter<boolean>();
|
||||
@Output() cancel = new EventEmitter<undefined>();
|
||||
@Output() confirm = new EventEmitter<string>();
|
||||
@Output() confirm = new EventEmitter<string[]>();
|
||||
|
||||
readonly settingsForm: FormGroup;
|
||||
readonly defaultBasePlayInfoApiUrl = BASE_LIVE_API_URL_DEFAULT;
|
||||
@ -39,15 +37,12 @@ export class BasePlayInfoApiUrlEditDialogComponent implements OnChanges {
|
||||
private changeDetector: ChangeDetectorRef
|
||||
) {
|
||||
this.settingsForm = formBuilder.group({
|
||||
basePlayInfoApiUrl: [
|
||||
'',
|
||||
[Validators.required, Validators.pattern(BASE_URL_PATTERN)],
|
||||
],
|
||||
basePlayInfoApiUrls: ['', [Validators.required, baseUrlValidator()]],
|
||||
});
|
||||
}
|
||||
|
||||
get control() {
|
||||
return this.settingsForm.get('basePlayInfoApiUrl') as FormControl;
|
||||
return this.settingsForm.get('basePlayInfoApiUrls') as FormControl;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
@ -70,7 +65,7 @@ export class BasePlayInfoApiUrlEditDialogComponent implements OnChanges {
|
||||
}
|
||||
|
||||
setValue(): void {
|
||||
this.control.setValue(this.value);
|
||||
this.control.setValue(this.value.join('\n'));
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
@ -80,7 +75,12 @@ export class BasePlayInfoApiUrlEditDialogComponent implements OnChanges {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -1,79 +1,100 @@
|
||||
<form nz-form [formGroup]="settingsForm">
|
||||
<nz-form-item
|
||||
class="setting-item actionable"
|
||||
(click)="baseApiUrlEditDialog.open()"
|
||||
(click)="baseApiUrlsEditDialog.open()"
|
||||
>
|
||||
<nz-form-label class="setting-label" [nzTooltipTitle]="baseApiUrlTip"
|
||||
>BASE API URL</nz-form-label
|
||||
<nz-form-label class="setting-label" [nzTooltipTitle]="baseApiUrlsTip"
|
||||
>主站 API 主机地址</nz-form-label
|
||||
>
|
||||
<ng-template #baseApiUrlTip>
|
||||
<p>主站 API 的 BASE URL</p>
|
||||
<ng-template #baseApiUrlsTip>
|
||||
<p>设置内容:发送主站 API 请求所用的主机的地址,一行一个。</p>
|
||||
<p>请求方式:先用第一个发送请求,出错就用第二个,以此类推。</p>
|
||||
<p>主要目的:缓解请求过多被风控</p>
|
||||
</ng-template>
|
||||
<nz-form-control
|
||||
[nzWarningTip]="syncFailedWarningTip"
|
||||
[nzValidateStatus]="syncStatus.baseApiUrl ? baseApiUrlControl : 'warning'"
|
||||
[nzValidateStatus]="
|
||||
syncStatus.baseApiUrls ? baseApiUrlsControl : 'warning'
|
||||
"
|
||||
>
|
||||
<nz-form-text class="setting-value"
|
||||
>{{ baseApiUrlControl.value }}
|
||||
>{{ baseApiUrlsControl.value }}
|
||||
</nz-form-text>
|
||||
<app-base-api-url-edit-dialog
|
||||
#baseApiUrlEditDialog
|
||||
[value]="baseApiUrlControl.value"
|
||||
(confirm)="baseApiUrlControl.setValue($event)"
|
||||
#baseApiUrlsEditDialog
|
||||
[value]="baseApiUrlsControl.value"
|
||||
(confirm)="baseApiUrlsControl.setValue($event)"
|
||||
></app-base-api-url-edit-dialog>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item
|
||||
class="setting-item actionable"
|
||||
(click)="baseLiveApiUrlEditDialog.open()"
|
||||
(click)="baseLiveApiUrlsEditDialog.open()"
|
||||
>
|
||||
<nz-form-label class="setting-label" [nzTooltipTitle]="baseLiveApiUrlTip"
|
||||
>BASE LIVE API URL</nz-form-label
|
||||
<nz-form-label class="setting-label" [nzTooltipTitle]="baseLiveApiUrlsTip"
|
||||
>直播 API 主机地址</nz-form-label
|
||||
>
|
||||
<ng-template #baseLiveApiUrlTip>
|
||||
<p>直播 API (getRoomPlayInfo 除外) 的 BASE URL</p>
|
||||
<ng-template #baseLiveApiUrlsTip>
|
||||
<p>
|
||||
设置内容:发送直播 API (直播流 API getRoomPlayInfo 除外)
|
||||
请求所用的主机的地址,一行一个。
|
||||
</p>
|
||||
<p>请求方式:先用第一个发送请求,出错就用第二个,以此类推。</p>
|
||||
<p>主要目的:缓解请求过多被风控</p>
|
||||
</ng-template>
|
||||
<nz-form-control
|
||||
[nzWarningTip]="syncFailedWarningTip"
|
||||
[nzValidateStatus]="
|
||||
syncStatus.baseLiveApiUrl ? baseLiveApiUrlControl : 'warning'
|
||||
syncStatus.baseLiveApiUrls ? baseLiveApiUrlsControl : 'warning'
|
||||
"
|
||||
>
|
||||
<nz-form-text class="setting-value"
|
||||
>{{ baseLiveApiUrlControl.value }}
|
||||
>{{ baseLiveApiUrlsControl.value }}
|
||||
</nz-form-text>
|
||||
<app-base-live-api-url-edit-dialog
|
||||
#baseLiveApiUrlEditDialog
|
||||
[value]="baseLiveApiUrlControl.value"
|
||||
(confirm)="baseLiveApiUrlControl.setValue($event)"
|
||||
#baseLiveApiUrlsEditDialog
|
||||
[value]="baseLiveApiUrlsControl.value"
|
||||
(confirm)="baseLiveApiUrlsControl.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()"
|
||||
(click)="basePlayInfoApiUrlsEditDialog.open()"
|
||||
>
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
[nzTooltipTitle]="basePalyInfoApiUrlTip"
|
||||
>BASE PLAY INFO API URL</nz-form-label
|
||||
>直播流 API 主机地址</nz-form-label
|
||||
>
|
||||
<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>
|
||||
<nz-form-control
|
||||
[nzWarningTip]="syncFailedWarningTip"
|
||||
[nzValidateStatus]="
|
||||
syncStatus.basePlayInfoApiUrl ? basePlayInfoApiUrlControl : 'warning'
|
||||
syncStatus.basePlayInfoApiUrls ? basePlayInfoApiUrlsControl : 'warning'
|
||||
"
|
||||
>
|
||||
<nz-form-text class="setting-value"
|
||||
>{{ basePlayInfoApiUrlControl.value }}
|
||||
>{{ basePlayInfoApiUrlsControl.value }}
|
||||
</nz-form-text>
|
||||
<app-base-play-info-api-url-edit-dialog
|
||||
#basePlayInfoApiUrlEditDialog
|
||||
[value]="basePlayInfoApiUrlControl.value"
|
||||
(confirm)="basePlayInfoApiUrlControl.setValue($event)"
|
||||
#basePlayInfoApiUrlsEditDialog
|
||||
[value]="basePlayInfoApiUrlsControl.value"
|
||||
(confirm)="basePlayInfoApiUrlsControl.setValue($event)"
|
||||
></app-base-play-info-api-url-edit-dialog>
|
||||
</nz-form-control>
|
||||
</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
|
||||
) {
|
||||
this.settingsForm = formBuilder.group({
|
||||
baseApiUrl: [''],
|
||||
baseLiveApiUrl: [''],
|
||||
basePlayInfoApiUrl: [''],
|
||||
baseApiUrls: [[]],
|
||||
baseLiveApiUrls: [[]],
|
||||
basePlayInfoApiUrls: [[]],
|
||||
});
|
||||
}
|
||||
|
||||
get baseApiUrlControl() {
|
||||
return this.settingsForm.get('baseApiUrl') as FormControl;
|
||||
get baseApiUrlsControl() {
|
||||
return this.settingsForm.get('baseApiUrls') as FormControl;
|
||||
}
|
||||
|
||||
get baseLiveApiUrlControl() {
|
||||
return this.settingsForm.get('baseLiveApiUrl') as FormControl;
|
||||
get baseLiveApiUrlsControl() {
|
||||
return this.settingsForm.get('baseLiveApiUrls') as FormControl;
|
||||
}
|
||||
|
||||
get basePlayInfoApiUrlControl() {
|
||||
return this.settingsForm.get('basePlayInfoApiUrl') as FormControl;
|
||||
get basePlayInfoApiUrlsControl() {
|
||||
return this.settingsForm.get('basePlayInfoApiUrls') as FormControl;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
@ -66,7 +66,8 @@ export class BiliApiSettingsComponent implements OnInit, OnChanges {
|
||||
.syncSettings(
|
||||
'biliApi',
|
||||
this.settings,
|
||||
this.settingsForm.valueChanges as Observable<BiliApiSettings>
|
||||
this.settingsForm.valueChanges as Observable<BiliApiSettings>,
|
||||
false
|
||||
)
|
||||
.subscribe((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 { SettingsComponent } from './settings.component';
|
||||
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 { NotificationSettingsComponent } from './notification-settings/notification-settings.component';
|
||||
import { LoggingSettingsComponent } from './logging-settings/logging-settings.component';
|
||||
@ -74,6 +75,7 @@ import { BasePlayInfoApiUrlEditDialogComponent } from './bili-api-settings/base-
|
||||
declarations: [
|
||||
SettingsComponent,
|
||||
SwitchActionableDirective,
|
||||
BaseUrlValidatorDirective,
|
||||
DiskSpaceSettingsComponent,
|
||||
NotificationSettingsComponent,
|
||||
LoggingSettingsComponent,
|
||||
|
@ -2,7 +2,6 @@ import { CoverSaveStrategy, DeleteStrategy } from '../setting.model';
|
||||
|
||||
export const SYNC_FAILED_WARNING_TIP = '设置同步失败!';
|
||||
|
||||
export const BASE_URL_PATTERN = /^https?:\/\/.*$/;
|
||||
export const BASE_API_URL_DEFAULT = 'https://api.bilibili.com';
|
||||
export const BASE_LIVE_API_URL_DEFAULT = 'https://api.live.bilibili.com';
|
||||
|
||||
|
@ -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>(
|
||||
key: K,
|
||||
initialValue: V,
|
||||
valueChanges: Observable<V>
|
||||
valueChanges: Observable<V>,
|
||||
deepDiff: boolean = true
|
||||
): Observable<DetailWithResult<V> | DetailWithError<V>> {
|
||||
return valueChanges.pipe(
|
||||
scan<V, [V, V, Partial<V>]>(
|
||||
([, prev], curr) => [
|
||||
prev,
|
||||
curr,
|
||||
difference(curr!, prev!) as Partial<V>,
|
||||
difference(curr!, prev!, deepDiff) as Partial<V>,
|
||||
],
|
||||
[initialValue, initialValue, {} as Partial<V>]
|
||||
),
|
||||
|
@ -1,9 +1,9 @@
|
||||
import type { Nullable, PartialDeep } from 'src/app/shared/utility-types';
|
||||
|
||||
export interface BiliApiSettings {
|
||||
baseApiUrl: string;
|
||||
baseLiveApiUrl: string;
|
||||
basePlayInfoApiUrl: string;
|
||||
baseApiUrls: string[];
|
||||
baseLiveApiUrls: string[];
|
||||
basePlayInfoApiUrls: string[];
|
||||
}
|
||||
|
||||
export type BiliApiOptions = Nullable<BiliApiSettings>;
|
||||
|
@ -42,7 +42,7 @@ export class InputDurationComponent implements OnInit, ControlValueAccessor {
|
||||
this.formGroup = formBuilder.group({
|
||||
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';
|
||||
|
||||
// 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) {
|
||||
return transform(object, (result: object, value: any, key: string) => {
|
||||
const baseValue = Reflect.get(base, key);
|
||||
@ -10,7 +14,7 @@ export function difference(object: object, base: object): object {
|
||||
Reflect.set(
|
||||
result,
|
||||
key,
|
||||
isObject(value) && isObject(baseValue)
|
||||
deep && isObject(value) && isObject(baseValue)
|
||||
? diff(value, baseValue)
|
||||
: value
|
||||
);
|
||||
|
@ -186,7 +186,6 @@ export class TaskItemComponent implements OnChanges, OnDestroy {
|
||||
this.settingService.getTaskOptions(this.roomId),
|
||||
this.settingService.getSettings([
|
||||
'output',
|
||||
'biliApi',
|
||||
'header',
|
||||
'danmaku',
|
||||
'recorder',
|
||||
|
@ -745,139 +745,6 @@
|
||||
</nz-form-item>
|
||||
</div>
|
||||
|
||||
<div ngModelGroup="biliApi" class="form-group biliapi">
|
||||
<h2>BILI API</h2>
|
||||
<nz-form-item class="setting-item input">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
[nzTooltipTitle]="baseApiUrlTip"
|
||||
>BASE API URL</nz-form-label
|
||||
>
|
||||
<ng-template #baseApiUrlTip>
|
||||
<p>主站 API 的 BASE URL</p>
|
||||
</ng-template>
|
||||
<nz-form-control
|
||||
class="setting-control input"
|
||||
[nzErrorTip]="baseApiUrlErrorTip"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
[pattern]="baseUrlPattern"
|
||||
nz-input
|
||||
name="baseApiUrl"
|
||||
[(ngModel)]="model.biliApi.baseApiUrl"
|
||||
[disabled]="options.biliApi.baseApiUrl === null"
|
||||
/>
|
||||
<ng-template #baseApiUrlErrorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
<label
|
||||
nz-checkbox
|
||||
[nzChecked]="options.biliApi.baseApiUrl !== null"
|
||||
(nzCheckedChange)="
|
||||
options.biliApi.baseApiUrl = $event
|
||||
? globalSettings.biliApi.baseApiUrl
|
||||
: null
|
||||
"
|
||||
>覆盖全局设置</label
|
||||
>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item input">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
[nzTooltipTitle]="baseLiveApiUrlTip"
|
||||
>BASE LIVE API URL</nz-form-label
|
||||
>
|
||||
<ng-template #baseLiveApiUrlTip>
|
||||
<p>直播 API (getRoomPlayInfo 除外) 的 BASE URL</p>
|
||||
</ng-template>
|
||||
<nz-form-control
|
||||
class="setting-control input"
|
||||
[nzErrorTip]="baseLiveApiUrlErrorTip"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
[pattern]="baseUrlPattern"
|
||||
nz-input
|
||||
name="baseLiveApiUrl"
|
||||
[(ngModel)]="model.biliApi.baseLiveApiUrl"
|
||||
[disabled]="options.biliApi.baseLiveApiUrl === null"
|
||||
/>
|
||||
<ng-template #baseLiveApiUrlErrorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
<label
|
||||
nz-checkbox
|
||||
[nzChecked]="options.biliApi.baseLiveApiUrl !== null"
|
||||
(nzCheckedChange)="
|
||||
options.biliApi.baseLiveApiUrl = $event
|
||||
? globalSettings.biliApi.baseLiveApiUrl
|
||||
: null
|
||||
"
|
||||
>覆盖全局设置</label
|
||||
>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item input">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
[nzTooltipTitle]="basePalyInfoApiUrlTip"
|
||||
>BASE PLAY INFO API URL</nz-form-label
|
||||
>
|
||||
<ng-template #basePalyInfoApiUrlTip>
|
||||
<p>直播 API getRoomPlayInfo 的 BASE URL</p>
|
||||
</ng-template>
|
||||
<nz-form-control
|
||||
class="setting-control input"
|
||||
[nzErrorTip]="basePlayInfoApiUrlErrorTip"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
[pattern]="baseUrlPattern"
|
||||
nz-input
|
||||
name="basePlayInfoApiUrl"
|
||||
[(ngModel)]="model.biliApi.basePlayInfoApiUrl"
|
||||
[disabled]="options.biliApi.basePlayInfoApiUrl === null"
|
||||
/>
|
||||
<ng-template #basePlayInfoApiUrlErrorTip let-control>
|
||||
<ng-container *ngIf="control.hasError('required')">
|
||||
不能为空
|
||||
</ng-container>
|
||||
<ng-container *ngIf="control.hasError('pattern')">
|
||||
输入无效
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
<label
|
||||
nz-checkbox
|
||||
[nzChecked]="options.biliApi.basePlayInfoApiUrl !== null"
|
||||
(nzCheckedChange)="
|
||||
options.biliApi.basePlayInfoApiUrl = $event
|
||||
? globalSettings.biliApi.basePlayInfoApiUrl
|
||||
: null
|
||||
"
|
||||
>覆盖全局设置</label
|
||||
>
|
||||
</nz-form-item>
|
||||
</div>
|
||||
|
||||
<div ngModelGroup="header" class="form-group header">
|
||||
<h2>网络请求</h2>
|
||||
<nz-form-item class="setting-item textarea">
|
||||
|
@ -29,7 +29,6 @@ import {
|
||||
DELETE_STRATEGIES,
|
||||
COVER_SAVE_STRATEGIES,
|
||||
RECORDING_MODE_OPTIONS,
|
||||
BASE_URL_PATTERN,
|
||||
} from '../../settings/shared/constants/form';
|
||||
|
||||
type OptionsModel = NonNullable<TaskOptions>;
|
||||
@ -56,7 +55,6 @@ export class TaskSettingsDialogComponent implements OnChanges {
|
||||
|
||||
readonly warningTip =
|
||||
'需要重启弹幕客户端才能生效,如果任务正在录制可能会丢失弹幕!';
|
||||
readonly baseUrlPattern = BASE_URL_PATTERN;
|
||||
readonly pathTemplatePattern = PATH_TEMPLATE_PATTERN;
|
||||
readonly streamFormatOptions = cloneDeep(STREAM_FORMAT_OPTIONS) as Mutable<
|
||||
typeof STREAM_FORMAT_OPTIONS
|
||||
|
Loading…
Reference in New Issue
Block a user