release: v1.0.0

This commit is contained in:
acgnhiki 2021-08-11 13:33:21 +08:00
parent ca93684a70
commit 01b11768bd
2008 changed files with 50705 additions and 0 deletions

7
.flake8 Normal file
View File

@ -0,0 +1,7 @@
[flake8]
ignore = D203, W504
exclude =
__*,
.*,
build,
dist,

8
MANIFEST.in Normal file
View File

@ -0,0 +1,8 @@
# Include the README
include README.md
# Include the license file
include LICENSE
# Include all package data
graft src/blrec/data

195
README.md Normal file
View File

@ -0,0 +1,195 @@
# Bilibili Live Streaming Recorder (blrec)
这是一个前后端分离的 B 站直播录制工具。前端使用了响应式设计,可适应不同的屏幕尺寸;后端是用 Python 写的,可以跨平台运行。
这个工具是自动化的,会自动完成直播的录制, 在出现未处理异常时会发送通知,空间不足能够自动回收空间,还有详细日志记录,因此可以长期无人值守运行在服务器上。
## 屏幕截图
![webapp](https://user-images.githubusercontent.com/33854576/128959800-451d03e7-c9f9-4732-ac90-97fdb6b88972.png)
![terminal](https://user-images.githubusercontent.com/33854576/128959819-70d72937-65da-4c15-b61c-d2da65bf42be.png)
## 功能
- 自动完成直播录制
- 同步保存弹幕
- 自动修复时间戳问题:跳变、反跳等。
- 直播流参数改变自动分割文件,避免出现花屏等问题。
- 流中断自动拼接且支持 **无缝** 拼接,不会因网络中断而使录播文件片段化。
- `flv` 文件注入关键帧等元数据,定位播放和拖进度条不会卡顿。
- 可选录制的画质
- 可自定义文件保存路径和文件名
- 支持按文件大小或时长分割文件
- 支持转换 `flv``mp4` 格式(需要安装 `ffmpeg`
- 硬盘空间检测并支持空间不足自动删除旧录播文件。
- 事件通知(支持邮箱、`ServerChan`、`pushplus`
- `Webhook`(可配合 `REST API` 实现录制控制,录制完成后压制、上传等自定义需求)
## 先决条件
Python 3.8+
ffmpeg (如果需要转换 flv 为 mp4)
## 安装
pip install blrec
用到的一些库需要 C 编译器Windows 没 C 编译器会安装出错,使用以下方法安装已编译好的库。
pip install -r windows-requirements.txt
## 使用方法
### 使用默认设置文件和保存位置
在命令行终端里敲入 `blrec` 并回车运行,然后在浏览器访问 `http://localhost:2233`
设置文件为 `toml` 文件,默认位置在 `~/.blrec/settings.toml`。默认录播文件保存位置为当前工作目录 `.`
### 指定设置文件和保存位置
`blrec -c path/to/settings.toml -o dirpath/to/save/files`
如果指定的设置文件不存在会自动创建。通过命令行参数指定保存位置会覆盖掉设置文件的设置。
### 绑定主机和端口
`blrec --host 0.0.0.0 --port 8000`
默认主机 `127.0.0.1`,默认端口 `2233`.
### 安全保障
`blrec --key-file path/to/key-file --cert-file path/to/cert-file --api-key ********`
可以使用 `api key` 防止被恶意访问和泄漏设置里的敏感信息
## 作为 ASGI 应用运行
uvicorn blrec.web:app
或者
hypercorn blrec.web:app
作为 ASGI 应用运行,参数通过环境变量指定。
- `config` 指定设置文件
- `out_dir` 指定保存位置
- `api_key` 指定 `api key`
### bash
config=path/to/settings.toml out_dir=path/to/dir api_key=******** uvicorn blrec.web:app --host 0.0.0.0 --port 8000
### cmd
set config=D:\\path\\to\\config.toml & set out_dir=D:\\path\\to\\dir & set api_key=******** uvicorn blrec.web:app --host 0.0.0.0 --port 8000
## Webhook
程序在运行过程中会触发一些事件,如果是支持 `webhook` 的事件,就会给所设置的 `webhook` 网络地址发送 POST 请求。
关于支持的事件和 `POST` 请求所发送的数据,详见 wiki。
## REST API
后端 `web` 框架用的是 `FastApi` , 要查看自动生成的交互式 `API` 文档,访问 `http://localhost:2233/docs` (默认主机和端口绑定)。
## Progressive Web AppPWA
前端其实是一个渐进式网络应用,可以通过地址栏右侧的图标安装,然后像原生应用一样从桌面启动运行。
**注意PWA 要在本地访问或者在 `https` 下才支持。**
---
## 常见问题
### 如何终止程序?
`ctrl + c`
### 如何更新程序?
`pip install blrec --upgrade`
### 设置修改后什么时候生效?
设置修改后基本上是即时生效的,但是对于正在录制的任务有些例外。
对保存位置或路径模板的修改,不影响当前正在录制的文件,要下一次创建文件才会生效。
对画质的修改,要下一次获取直播流 `url` 才会生效。如果想立即生效,停止录制再重新开始录制就可以。
### 什么是流拼接和无缝拼接?
流拼接就是在录制的过程中出现网络中断时从中断的位置接续上去,以避免录播文件片段化。
而无缝拼接就是把中断处前后重复的数据丢掉然后再接续上,就好像没中断过一样,不会丢失数据。
**注意:只有网络中断后在短时间内重新连接上,中断处前后有重复数据才有可能进行无缝拼接。**
### 怎么知道是否出现了流中断?
如果流出现了中断,不但日志里有记录,程序还会把流中断的详细信息写入到视频文件的元数据里。
可以用 `ffprob` 查看视频文件元数据的 `Comment` 字段。
有些播放器也可以查看。`VLC` 可以按 `ctrl + i` 后查看注释;`PotPlayer` 可以按两次 tab 键查看评论mp4 没问题,但是 flv 会因为内容过长而不显示~)。
mp4 还在非无缝拼接的位置添加了章节标记,支持章节标记的播放器应该可以看到。
### 如何避免流中断、漏录?
cpu 使用率过高、网络带宽不足或不稳定、硬盘读写慢都会导致流中断,如果不能及时恢复就无法进行无缝拼接进而导致漏录了。所以,只要运行程序的机器配置足够高且网络带宽充足稳定就可以有效避免录制端导致的流中断、漏录。
主播网络或 B 站服务器问题导致的流中断、漏录是无法避免的~
### 为什么录播文件的时长小于直播持续时间?
- 开播后推流有延迟
- 录制过程出现流中断并且未能无缝拼接,漏录了~
- 主播网络不稳定,流中出现了时间戳跳变、反跳,经过修正后时长可能会变短。
### 为什么录制的画质和所设置的画质不符合?
所设置的画质在开播后不一定存在,如果不存在就会以原画代替。
### 为什么按文件大小或时长分割的文件比设置的值小或短?
分割位置必须在关键帧处才不会丢帧。为了确保文件不会超过指定的限制,会在关键帧将超过所设置的值的前一个关键帧处进行分割,所以文件的大小或时长要比所设置的值小一些。
### 为什么没有设置分割文件还是出现了多个录播文件?
为了避免录播文件出现花屏等问题,在直播流参数改变(主播修改分辨率、码率等)时就会自动分割文件。
有时则是主播网络很不稳定造成多次下播上播导致的。
### 为什么设置了转换为 `mp4` 格式,但却没有进行转换?
请确保正确安装了 `ffmpeg`,可以在终端里正常使用 `ffmpeg`
### 怎样才算是旧录播文件?
创建时间超过 24 小时才会被当成旧录播文件在空间不足时被删除。
### 空间不足时是怎样删除旧录播文件的?
删除文件是按创建时间的先后进行的,最早创建的最先被删除,直到可用空间不少于所设置的阈值为止。
### 支持录制付费直播吗?
没试过~ 可以尝试在网络请求设置里填写已付费账号登录后的 Cookie
### 为什么要重复造轮子?
因为现有工具大多都是直接下载流或调用 ffmpeg 进行录制,不能很好地解决漏录、数据损坏、录播文件片段化等录播问题。尤其录制的直播很不稳定,结果很不尽如人意。
---
## 赞助 & 支持
如果觉得这个工具好用,对你有所帮助,可以投喂支持亿下哦~
投喂赞助 ☞ <https://afdian.net/@acgnhiki>

19
mypy.ini Normal file
View File

@ -0,0 +1,19 @@
[mypy]
plugins = pydantic.mypy
follow_imports = silent
show_column_numbers = True
ignore_missing_imports = True
warn_unused_ignores = True
warn_redundant_casts = True
check_untyped_defs = True
disallow_any_generics = True
# for strict mypy: (this is the tricky one :-))
disallow_untyped_defs = True
[pydantic-mypy]
init_forbid_extra = True
init_typed = True
warn_required_dynamic_aliases = True
warn_untyped_fields = True

6
pyproject.toml Normal file
View File

@ -0,0 +1,6 @@
[build-system]
requires = [
"setuptools >= 57.0.0, < 58.0.0",
"wheel >= 0.37, < 0.38.0",
]
build-backend = "setuptools.build_meta"

79
setup.cfg Normal file
View File

@ -0,0 +1,79 @@
[metadata]
name = blrec
version = attr: blrec.__version__
description = Bilibili Live Streaming Recorder
long_description = file: README.md
long_description_content_type = text/markdown
keywords = bilibili, live, danmaku, recorder
license = GPLv3
license_file = LICENSE
author = acgnhiki
author_email = acgnhiki@outlook.com
url = https://github.com/acgnhiki/blrec
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Console
Environment :: Web Environment
Intended Audience :: Developers
Intended Audience :: End Users/Desktop
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Topic :: Internet
Topic :: Multimedia :: Video
Operating System :: OS Independent
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: Implementation :: CPython
Typing :: Typed
[options]
packages = find:
package_dir =
=src
include_package_data = True
python_requires = >= 3.8
install_requires =
typing-extensions >= 3.10.0.0
fastapi >= 0.65.2, < 0.66.0
email_validator >=1.1.2, < 2.0.0
typer >= 0.3.2, < 0.4.0
aiohttp >= 3.6.2, < 3.7.0
requests >= 2.24.0, < 2.25.0
aiofiles >= 0.5.0, < 0.6.0
tenacity >= 6.2.0, < 6.3.0
colorama >= 0.4.3, < 0.5.0
humanize >= 3.2.0, < 3.3.0
tqdm >= 4.56.2, < 4.57.0
attrs >= 21.2.0, < 21.3.0
lxml >= 4.6.2, < 4.7.0
toml >= 0.10.2, < 0.11.0
psutil >= 5.8.0, < 5.9.0
rx >= 3.1.1, < 3.2.0
bitarray >= 2.2.5, < 2.3.0
uvicorn[standard] >=0.12.0, < 0.15.0
[options.extras_require]
dev =
flake8 >= 3.9.2, < 4.0.0
mypy >= 0.910, < 1.000
setuptools >= 57.0.0, < 58.0.0
wheel >= 0.37, < 0.38.0
build >= 0.5.1, < 0.6.0
# missing stub packages
types-requests >= 2.25.1, < 2.26.0
types-aiofiles >= 0.1.7, < 0.2.0
types-toml >= 0.1.3, < 0.2.0
types-setuptools >= 57.0.0, < 58.0.0
speedups = aiohttp[speedups] >= 3.6.2, < 3.7.0
[options.packages.find]
where = src
[options.entry_points]
console_scripts =
blrec = blrec.cli.main:main

2
setup.py Normal file
View File

@ -0,0 +1,2 @@
import setuptools
setuptools.setup()

3
src/blrec/__init__.py Normal file
View File

@ -0,0 +1,3 @@
__version__ = '1.0.0'
__prog__ = 'blrec'

5
src/blrec/__main__.py Normal file
View File

@ -0,0 +1,5 @@
import sys
from .cli.main import main
sys.exit(main())

298
src/blrec/application.py Normal file
View File

@ -0,0 +1,298 @@
import os
import logging
import asyncio
from typing import Iterator, List, Optional
import attr
import psutil
from . import __prog__, __version__
from .disk_space import SpaceMonitor, SpaceReclaimer
from .bili.helpers import ensure_room_id
from .task import (
RecordTaskManager,
TaskParam,
TaskData,
FileDetail,
)
from .exception import ExistsError, ExceptionHandler
from .event.event_submitters import SpaceEventSubmitter
from .setting import (
SettingsManager,
Settings,
SettingsIn,
SettingsOut,
KeySetOfSettings,
TaskOptions,
)
from .notification import (
EmailNotifier,
ServerchanNotifier,
PushplusNotifier,
)
from .webhook import WebHookEmitter
logger = logging.getLogger(__name__)
@attr.s(auto_attribs=True, slots=True, frozen=True)
class AppInfo:
name: str
version: str
pid: int
ppid: int
create_time: float
cwd: str
exe: str
cmdline: List[str]
@attr.s(auto_attribs=True, slots=True, frozen=True)
class AppStatus:
cpu_percent: float
memory_percent: float
num_threads: int
class Application:
def __init__(self, settings: Settings) -> None:
self._out_dir = settings.output.out_dir
self._settings_manager = SettingsManager(self, settings)
self._task_manager = RecordTaskManager(self._settings_manager)
@property
def info(self) -> AppInfo:
p = psutil.Process(os.getpid())
with p.oneshot():
return AppInfo(
name=__prog__,
version=__version__,
pid=p.pid,
ppid=p.ppid(),
create_time=p.create_time(),
cwd=p.cwd(),
exe=p.exe(),
cmdline=p.cmdline(),
)
@property
def status(self) -> AppStatus:
p = psutil.Process(os.getpid())
with p.oneshot():
return AppStatus(
cpu_percent=p.cpu_percent(),
memory_percent=p.memory_percent(),
num_threads=p.num_threads(),
)
def run(self) -> None:
asyncio.run(self._run())
async def _run(self) -> None:
self._loop = asyncio.get_running_loop()
await self.launch()
try:
self._interrupt_event = asyncio.Event()
await self._interrupt_event.wait()
finally:
await self.exit()
async def launch(self) -> None:
self._setup()
await self._task_manager.load_all_tasks()
logger.info('Launched Application')
async def exit(self) -> None:
await self._exit()
logger.info('Exited Application')
async def abort(self) -> None:
await self._exit(force=True)
logger.info('Aborted Application')
async def _exit(self, force: bool = False) -> None:
await self._task_manager.stop_all_tasks(force=force)
await self._task_manager.destroy_all_tasks()
self._destroy()
async def restart(self) -> None:
logger.info('Restarting Application...')
await self.exit()
await self.launch()
def has_task(self, room_id: int) -> bool:
return self._task_manager.has_task(room_id)
async def add_task(self, room_id: int) -> int:
room_id = await ensure_room_id(room_id)
if self._settings_manager.has_task_settings(room_id):
raise ExistsError(
f"a task for the room {room_id} is already existed"
)
settings = await self._settings_manager.add_task_settings(room_id)
await self._task_manager.add_task(settings)
return room_id
async def remove_task(self, room_id: int) -> None:
await self._task_manager.remove_task(room_id)
await self._settings_manager.remove_task_settings(room_id)
async def remove_all_tasks(self) -> None:
await self._task_manager.remove_all_tasks()
await self._settings_manager.remove_all_task_settings()
async def start_task(self, room_id: int) -> None:
await self._task_manager.start_task(room_id)
await self._settings_manager.mark_task_enabled(room_id)
async def stop_task(self, room_id: int, force: bool = False) -> None:
await self._task_manager.stop_task(room_id, force)
await self._settings_manager.mark_task_disabled(room_id)
async def start_all_tasks(self) -> None:
await self._task_manager.start_all_tasks()
await self._settings_manager.mark_all_tasks_enabled()
async def stop_all_tasks(self, force: bool = False) -> None:
await self._task_manager.stop_all_tasks(force)
await self._settings_manager.mark_all_tasks_disabled()
async def enable_task_recorder(self, room_id: int) -> None:
await self._task_manager.enable_task_recorder(room_id)
await self._settings_manager.mark_task_recorder_enabled(room_id)
async def disable_task_recorder(
self, room_id: int, force: bool = False
) -> None:
await self._task_manager.disable_task_recorder(room_id, force)
await self._settings_manager.mark_task_recorder_disabled(room_id)
async def enable_all_task_recorders(self) -> None:
await self._task_manager.enable_all_task_recorders()
await self._settings_manager.mark_all_task_recorders_enabled()
async def disable_all_task_recorders(self, force: bool = False) -> None:
await self._task_manager.disable_all_task_recorders(force)
await self._settings_manager.mark_all_task_recorders_disabled()
def get_task_data(self, room_id: int) -> TaskData:
return self._task_manager.get_task_data(room_id)
def get_all_task_data(self) -> Iterator[TaskData]:
yield from self._task_manager.get_all_task_data()
def get_task_param(self, room_id: int) -> TaskParam:
return self._task_manager.get_task_param(room_id)
def get_task_file_details(self, room_id: int) -> Iterator[FileDetail]:
yield from self._task_manager.get_task_file_details(room_id)
async def update_task_info(self, room_id: int) -> None:
await self._task_manager.update_task_info(room_id)
async def update_all_task_infos(self) -> None:
await self._task_manager.update_all_task_infos()
def get_settings(
self,
include: Optional[KeySetOfSettings] = None,
exclude: Optional[KeySetOfSettings] = None,
) -> SettingsOut:
return self._settings_manager.get_settings(include, exclude)
async def change_settings(self, settings: SettingsIn) -> SettingsOut:
return await self._settings_manager.change_settings(settings)
def get_task_options(self, room_id: int) -> TaskOptions:
return self._settings_manager.get_task_options(room_id)
async def change_task_options(
self, room_id: int, options: TaskOptions
) -> TaskOptions:
return await self._settings_manager.change_task_options(
room_id, options
)
def _setup(self) -> None:
self._setup_logger()
self._setup_exception_handler()
self._setup_space_monitor()
self._setup_space_event_submitter()
self._setup_space_reclaimer()
self._setup_notifiers()
self._setup_webhooks()
def _setup_logger(self) -> None:
self._settings_manager.apply_logging_settings()
def _setup_exception_handler(self) -> None:
self._exception_handler = ExceptionHandler()
self._exception_handler.enable()
def _setup_space_monitor(self) -> None:
self._space_monitor = SpaceMonitor(self._out_dir)
self._settings_manager.apply_space_monitor_settings()
self._space_monitor.enable()
def _setup_space_event_submitter(self) -> None:
self._space_event_submitter = SpaceEventSubmitter(self._space_monitor)
def _setup_space_reclaimer(self) -> None:
self._space_reclaimer = SpaceReclaimer(
self._space_monitor, self._out_dir,
)
self._settings_manager.apply_space_reclaimer_settings()
self._space_reclaimer.enable()
def _setup_notifiers(self) -> None:
self._email_notifier = EmailNotifier()
self._serverchan_notifier = ServerchanNotifier()
self._pushplus_notifier = PushplusNotifier()
self._settings_manager.apply_email_notification_settings()
self._settings_manager.apply_serverchan_notification_settings()
self._settings_manager.apply_pushplus_notification_settings()
def _setup_webhooks(self) -> None:
self._webhook_emitter = WebHookEmitter()
self._settings_manager.apply_webhooks_settings()
self._webhook_emitter.enable()
def _destroy(self) -> None:
self._destroy_space_reclaimer()
self._destroy_space_event_submitter()
self._destroy_space_monitor()
self._destroy_notifiers()
self._destroy_webhooks()
self._destroy_exception_handler()
def _destroy_space_monitor(self) -> None:
self._space_monitor.disable()
del self._space_monitor
def _destroy_space_event_submitter(self) -> None:
del self._space_event_submitter
def _destroy_space_reclaimer(self) -> None:
self._space_reclaimer.disable()
del self._space_reclaimer
def _destroy_notifiers(self) -> None:
self._email_notifier.disable()
self._serverchan_notifier.disable()
self._pushplus_notifier.disable()
del self._email_notifier
del self._serverchan_notifier
del self._pushplus_notifier
def _destroy_webhooks(self) -> None:
self._webhook_emitter.disable()
del self._webhook_emitter
def _destroy_exception_handler(self) -> None:
self._exception_handler.disable()
del self._exception_handler

View File

101
src/blrec/bili/api.py Normal file
View File

@ -0,0 +1,101 @@
from typing import Any, Final
import aiohttp
from tenacity import (
retry,
wait_exponential,
stop_after_delay,
)
from .typing import QualityNumber, JsonResponse, ResponseData
from .exceptions import ApiRequestError
__all__ = 'WebApi',
class WebApi:
BASE_API_URL: Final[str] = 'https://api.bilibili.com'
BASE_LIVE_API_URL: Final[str] = 'https://api.live.bilibili.com'
GET_USER_INFO_URL: Final[str] = BASE_API_URL + '/x/space/acc/info'
ROOM_INIT_URL: Final[str] = BASE_LIVE_API_URL + '/room/v1/Room/room_init'
GET_INFO_URL: Final[str] = BASE_LIVE_API_URL + '/room/v1/Room/get_info'
GET_INFO_BY_ROOM_URL: Final[str] = BASE_LIVE_API_URL + \
'/xlive/web-room/v1/index/getInfoByRoom'
GET_ROOM_PLAY_INFO_URL: Final[str] = BASE_LIVE_API_URL + \
'/xlive/web-room/v2/index/getRoomPlayInfo'
GET_TIMESTAMP_URL: Final[str] = BASE_LIVE_API_URL + \
'/av/v1/Time/getTimestamp?platform=pc'
def __init__(self, session: aiohttp.ClientSession):
self._session = session
self.timeout = 10
@staticmethod
def _check_response(json_res: JsonResponse) -> None:
if json_res['code'] != 0:
raise ApiRequestError(
json_res['code'],
json_res.get('message') or json_res.get('msg') or '',
)
@retry(
reraise=True,
stop=stop_after_delay(5),
wait=wait_exponential(0.1),
)
async def _get(self, *args: Any, **kwds: Any) -> JsonResponse:
async with self._session.get(
*args,
**kwds,
timeout=self.timeout,
) as res:
json_res = await res.json()
self._check_response(json_res)
return json_res
async def room_init(self, room_id: int) -> ResponseData:
r = await self._get(self.ROOM_INIT_URL, params={'id': room_id})
return r['data']
async def get_room_play_info(
self, room_id: int, qn: QualityNumber = 10000
) -> ResponseData:
params = {
'room_id': room_id,
'protocol': '0,1',
'format': '0,2',
'codec': '0,1',
'qn': qn,
'platform': 'web',
'ptype': 16,
}
r = await self._get(self.GET_ROOM_PLAY_INFO_URL, params=params)
return r['data']
async def get_info_by_room(self, room_id: int) -> ResponseData:
params = {
'room_id': room_id,
}
r = await self._get(self.GET_INFO_BY_ROOM_URL, params=params)
return r['data']
async def get_info(self, room_id: int) -> ResponseData:
params = {
'room_id': room_id,
}
r = await self._get(self.GET_INFO_URL, params=params)
return r['data']
async def get_timestamp(self) -> int:
r = await self._get(self.GET_TIMESTAMP_URL)
return r['data']['timestamp']
async def get_user_info(self, uid: int) -> ResponseData:
params = {
'mid': uid,
}
r = await self._get(self.GET_USER_INFO_URL, params=params)
return r['data']

View File

@ -0,0 +1,543 @@
#!/usr/bin/env python3
import json
import struct
import zlib
import asyncio
import logging
from enum import IntEnum, Enum
from contextlib import suppress
from typing import Any, Dict, Final, Tuple, List, Union, cast, Optional
import aiohttp
from aiohttp import ClientSession
from tenacity import (
retry,
wait_exponential,
retry_if_exception_type,
)
from .typing import Danmaku
from ..event.event_emitter import EventListener, EventEmitter
from ..exception import exception_callback
from ..utils.mixins import AsyncStoppableMixin
from ..logging.room_id import aio_task_with_room_id
__all__ = 'DanmakuClient', 'DanmakuListener', 'Danmaku', 'DanmakuCommand'
logger = logging.getLogger(__name__)
class DanmakuListener(EventListener):
async def on_client_connected(self) -> None:
...
async def on_client_disconnected(self) -> None:
...
async def on_client_reconnected(self) -> None:
...
async def on_danmaku_received(self, danmu: Danmaku) -> None:
...
async def on_error_occurred(self, error: Exception) -> None:
...
class DanmakuClient(EventEmitter[DanmakuListener], AsyncStoppableMixin):
_URL: Final[str] = 'wss://broadcastlv.chat.bilibili.com:443/sub'
_HEARTBEAT_INTERVAL: Final[int] = 30
def __init__(
self,
session: ClientSession,
room_id: int,
*,
max_retries: int = 10,
) -> None:
super().__init__()
self.session = session
self._room_id = room_id
self._retry_delay: int = 0
self._MAX_RETRIES: Final[int] = max_retries
async def _do_start(self) -> None:
await self._connect()
await self._create_message_loop()
logger.debug('Started danmaku client')
async def _do_stop(self) -> None:
await self._terminate_message_loop()
await self._disconnect()
logger.debug('Stopped danmaku client')
async def reconnect(self) -> None:
if self.stopped:
return
logger.debug('Reconnecting...')
await self._disconnect()
await self._connect()
await self._emit('client_reconnected')
@retry(
wait=wait_exponential(multiplier=0.1, max=60),
retry=retry_if_exception_type((
asyncio.TimeoutError, aiohttp.ClientError,
)),
)
async def _connect(self) -> None:
logger.debug('Connecting to server...')
await self._connect_websocket()
await self._send_auth()
reply = await self._recieve_auth_reply()
self._handle_auth_reply(reply)
logger.debug('Connected to server')
await self._emit('client_connected')
async def _connect_websocket(self) -> None:
self._ws = await self.session.ws_connect(self._URL, timeout=5)
logger.debug('Established WebSocket connection')
async def _send_auth(self) -> None:
auth_msg = json.dumps({
'uid': 0,
'roomid': self._room_id, # must not be the short id!
# 'protover': WS.BODY_PROTOCOL_VERSION_NORMAL,
'protover': WS.BODY_PROTOCOL_VERSION_DEFLATE,
'platform': 'web',
'clientver': '1.1.031',
'type': 2
})
data = Frame.encode(WS.OP_USER_AUTHENTICATION, auth_msg)
await self._ws.send_bytes(data)
logger.debug('Sent user authentication')
async def _recieve_auth_reply(self) -> aiohttp.WSMessage:
msg = await self._ws.receive(timeout=5)
if msg.type != aiohttp.WSMsgType.BINARY:
raise aiohttp.ClientError(msg)
logger.debug('Recieved reply')
return msg
def _handle_auth_reply(self, reply: aiohttp.WSMessage) -> None:
op, msg = Frame.decode(reply.data)
assert op == WS.OP_CONNECT_SUCCESS
msg = cast(str, msg)
code = cast(int, json.loads(msg)['code'])
if code == WS.AUTH_OK:
logger.debug('Auth OK')
self._create_heartbeat_task()
elif code == WS.AUTH_TOKEN_ERROR:
logger.debug('Auth Token Error')
raise ValueError(f'Auth Token Error: {code}')
else:
raise ValueError(f'Unexpected code: {code}')
async def _disconnect(self) -> None:
await self._cancel_heartbeat_task()
await self._close_websocket()
logger.debug('Disconnected from server')
await self._emit('client_disconnected')
async def _close_websocket(self) -> None:
with suppress(BaseException):
await self._ws.close()
def _create_heartbeat_task(self) -> None:
self._heartbeat_task = asyncio.create_task(self._send_heartbeat())
self._heartbeat_task.add_done_callback(exception_callback)
async def _cancel_heartbeat_task(self) -> None:
self._heartbeat_task.cancel()
with suppress(asyncio.CancelledError):
await self._heartbeat_task
@aio_task_with_room_id
async def _send_heartbeat(self) -> None:
data = Frame.encode(WS.OP_HEARTBEAT, '')
while True:
await self._ws.send_bytes(data)
await asyncio.sleep(self._HEARTBEAT_INTERVAL)
async def _create_message_loop(self) -> None:
self._message_loop_task = asyncio.create_task(self._message_loop())
self._message_loop_task.add_done_callback(exception_callback)
logger.debug('Created message loop')
async def _terminate_message_loop(self) -> None:
self._message_loop_task.cancel()
with suppress(asyncio.CancelledError):
await self._message_loop_task
logger.debug('Terminated message loop')
@aio_task_with_room_id
async def _message_loop(self) -> None:
while True:
for msg in await self._receive():
await self._dispatch_message(msg)
async def _dispatch_message(self, msg: Dict[str, Any]) -> None:
await self._emit('danmaku_received', msg)
@retry(retry=retry_if_exception_type((asyncio.TimeoutError,)))
async def _receive(self) -> List[Dict[str, Any]]:
self._retry_count = 0
self._retry_delay = 0
while True:
wsmsg = await self._ws.receive(timeout=self._HEARTBEAT_INTERVAL)
if wsmsg.type == aiohttp.WSMsgType.BINARY:
if (result := await self._handle_data(wsmsg.data)):
return result
elif wsmsg.type == aiohttp.WSMsgType.ERROR:
await self._handle_error(cast(Exception, wsmsg.data))
elif wsmsg.type == aiohttp.WSMsgType.CLOSED:
msg = 'WebSocket Closed'
exc = aiohttp.WebSocketError(self._ws.close_code or 1006, msg)
await self._handle_error(exc)
else:
raise ValueError(wsmsg)
@staticmethod
async def _handle_data(data: bytes) -> Optional[List[Dict[str, Any]]]:
loop = asyncio.get_running_loop()
op, msg = await loop.run_in_executor(None, Frame.decode, data)
if op == WS.OP_MESSAGE:
msg = cast(List[str], msg)
return [json.loads(m) for m in msg]
elif op == WS.OP_HEARTBEAT_REPLY:
return None
else:
return None
async def _handle_error(self, exc: Exception) -> None:
logger.debug(f'Failed to receive message due to: {repr(exc)}')
await self._emit('error_occurred', exc)
await self._retry()
async def _retry(self) -> None:
if self._retry_count < self._MAX_RETRIES:
if self._retry_delay > 0:
logger.debug('Retry after {} second{}'.format(
self._retry_delay,
's' if self._retry_delay > 1 else '',
))
await asyncio.sleep(self._retry_delay)
await self.reconnect()
self._retry_count += 1
self._retry_delay += 1
else:
raise aiohttp.WebSocketError(1006, 'Over the maximum of retries')
class Frame:
HEADER_FORMAT = '>IHHII'
@staticmethod
def encode(op: int, msg: str) -> bytes:
body = msg.encode()
header_length = WS.PACKAGE_HEADER_TOTAL_LENGTH
packet_length = header_length + len(body)
ver = WS.HEADER_DEFAULT_VERSION
seq = WS.HEADER_DEFAULT_SEQUENCE
header = struct.pack(
Frame.HEADER_FORMAT,
packet_length,
header_length,
ver, # protocal version
op, # operation
seq, # sequence id
)
return header + body
@staticmethod
def decode(data: bytes) -> Tuple[int, Union[int, str, List[str]]]:
plen, hlen, ver, op, _ = struct.unpack_from(
Frame.HEADER_FORMAT, data, 0
)
body = data[hlen:]
if op == WS.OP_MESSAGE:
if ver == WS.BODY_PROTOCOL_VERSION_DEFLATE:
data = zlib.decompress(body)
msg_list = []
offset = 0
while offset < len(data):
plen, hlen, ver, op, _ = struct.unpack_from(
Frame.HEADER_FORMAT, data, offset
)
body = data[hlen + offset:plen + offset]
msg = body.decode('utf8')
msg_list.append(msg)
offset += plen
return op, msg_list
elif op == WS.OP_HEARTBEAT_REPLY:
online_count = cast(int, struct.unpack('>I', body)[0])
return op, online_count
elif op == WS.OP_CONNECT_SUCCESS:
auth_result = body.decode()
return op, auth_result
else:
raise ValueError(f'Unexpected Operation: {op}')
class WS(IntEnum):
AUTH_OK = 0
AUTH_TOKEN_ERROR = -101
BODY_PROTOCOL_VERSION_DEFLATE = 2
BODY_PROTOCOL_VERSION_NORMAL = 0
HEADER_DEFAULT_OPERATION = 1
HEADER_DEFAULT_SEQUENCE = 1
HEADER_DEFAULT_VERSION = 1
HEADER_OFFSET = 4
OP_CONNECT_SUCCESS = 8
OP_HEARTBEAT = 2
OP_HEARTBEAT_REPLY = 3
OP_MESSAGE = 5
OP_USER_AUTHENTICATION = 7
OPERATION_OFFSET = 8
PACKAGE_HEADER_TOTAL_LENGTH = 16
PACKAGE_OFFSET = 0
SEQUENCE_OFFSET = 12
VERSION_OFFSET = 6
class DanmakuCommand(Enum):
ACTIVITY_MATCH_GIFT = 'ACTIVITY_MATCH_GIFT'
ANCHOR_LOT_AWARD = 'ANCHOR_LOT_AWARD'
ANCHOR_LOT_CHECKSTATUS = 'ANCHOR_LOT_CHECKSTATUS'
ANCHOR_LOT_END = 'ANCHOR_LOT_END'
ANCHOR_LOT_START = 'ANCHOR_LOT_START'
ANIMATION = 'ANIMATION'
BOX_ACTIVITY_START = 'BOX_ACTIVITY_START'
CHANGE_ROOM_INFO = 'CHANGE_ROOM_INFO'
CHASE_FRAME_SWITCH = 'CHASE_FRAME_SWITCH'
COMBO_SEND = 'COMBO_SEND'
CUT_OFF = 'CUT_OFF'
DANMU_GIFT_LOTTERY_AWARD = 'DANMU_GIFT_LOTTERY_AWARD'
DANMU_GIFT_LOTTERY_END = 'DANMU_GIFT_LOTTERY_END'
DANMU_GIFT_LOTTERY_START = 'DANMU_GIFT_LOTTERY_START'
DANMU_MSG = 'DANMU_MSG'
ENTRY_EFFECT = 'ENTRY_EFFECT'
GUARD_ACHIEVEMENT_ROOM = 'GUARD_ACHIEVEMENT_ROOM'
GUARD_LOTTERY_START = 'GUARD_LOTTERY_START'
HOUR_RANK_AWARDS = 'HOUR_RANK_AWARDS'
LITTLE_TIPS = 'LITTLE_TIPS'
LIVE = 'LIVE'
LOL_ACTIVITY = 'LOL_ACTIVITY'
LUCK_GIFT_AWARD_USER = 'LUCK_GIFT_AWARD_USER'
MATCH_TEAM_GIFT_RANK = 'MATCH_TEAM_GIFT_RANK'
MESSAGEBOX_USER_GAIN_MEDAL = 'MESSAGEBOX_USER_GAIN_MEDAL'
NOTICE_MSG = 'NOTICE_MSG'
PK_AGAIN = 'PK_AGAIN'
PK_BATTLE_CRIT = 'PK_BATTLE_CRIT'
PK_BATTLE_END = 'PK_BATTLE_END'
PK_BATTLE_GIFT = 'PK_BATTLE_GIFT'
PK_BATTLE_PRE = 'PK_BATTLE_PRE'
PK_BATTLE_PRO_TYPE = 'PK_BATTLE_PRO_TYPE'
PK_BATTLE_PROCESS = 'PK_BATTLE_PROCESS'
PK_BATTLE_RANK_CHANGE = 'PK_BATTLE_RANK_CHANGE'
PK_BATTLE_SETTLE_USER = 'PK_BATTLE_SETTLE_USER'
PK_BATTLE_SPECIAL_GIFT = 'PK_BATTLE_SPECIAL_GIFT'
PK_BATTLE_START = 'PK_BATTLE_START'
PK_BATTLE_VOTES_ADD = 'PK_BATTLE_VOTES_ADD'
PK_END = 'PK_END'
PK_LOTTERY_START = 'PK_LOTTERY_START'
PK_MATCH = 'PK_MATCH'
PK_MIC_END = 'PK_MIC_END'
PK_PRE = 'PK_PRE'
PK_PROCESS = 'PK_PROCESS'
PK_SETTLE = 'PK_SETTLE'
PK_START = 'PK_START'
PREPARING = 'PREPARING'
RAFFLE_END = 'RAFFLE_END'
RAFFLE_START = 'RAFFLE_START'
ROOM_BLOCK_INTO = 'ROOM_BLOCK_INTO'
ROOM_BLOCK_MSG = 'ROOM_BLOCK_MSG'
ROOM_BOX_USER = 'ROOM_BOX_USER'
ROOM_CHANGE = 'ROOM_CHANGE'
ROOM_KICKOUT = 'ROOM_KICKOUT'
ROOM_LIMIT = 'ROOM_LIMIT'
ROOM_LOCK = 'ROOM_LOCK'
ROOM_RANK = 'ROOM_RANK'
ROOM_REAL_TIME_MESSAGE_UPDATE = 'ROOM_REAL_TIME_MESSAGE_UPDATE'
ROOM_REFRESH = 'ROOM_REFRESH'
ROOM_SILENT_OFF = 'ROOM_SILENT_OFF'
ROOM_SILENT_ON = 'ROOM_SILENT_ON'
ROOM_SKIN_MSG = 'ROOM_SKIN_MSG'
SCORE_CARD = 'SCORE_CARD'
SEND_GIFT = 'SEND_GIFT'
SEND_TOP = 'SEND_TOP'
SPECIAL_GIFT = 'SPECIAL_GIFT'
SUPER_CHAT_ENTRANCE = 'SUPER_CHAT_ENTRANCE'
SUPER_CHAT_MESSAGE = 'SUPER_CHAT_MESSAGE'
SUPER_CHAT_MESSAGE_DELETE = 'SUPER_CHAT_MESSAGE_DELETE'
TV_END = 'TV_END'
TV_START = 'TV_START'
USER_TOAST_MSG = 'USER_TOAST_MSG'
VOICE_JOIN_STATUS = 'VOICE_JOIN_STATUS'
WARNING = 'WARNING'
WATCH_LPL_EXPIRED = 'WATCH_LPL_EXPIRED'
WEEK_STAR_CLOCK = 'WEEK_STAR_CLOCK'
WELCOME = 'WELCOME'
WELCOME_GUARD = 'WELCOME_GUARD'
WIN_ACTIVITY = 'WIN_ACTIVITY'
WIN_ACTIVITY_USER = 'WIN_ACTIVITY_USER'
WISH_BOTTLE = 'WISH_BOTTLE'
# ...
if __name__ == '__main__':
def add_task_name(record: logging.LogRecord) -> bool:
try:
task = asyncio.current_task()
assert task is not None
except Exception:
name = ''
else:
name = task.get_name()
if '::' in name:
record.roomid = '[' + name.split('::')[-1] + '] ' # type: ignore
else:
record.roomid = '' # type: ignore
return True
def configure_logger() -> None:
# config root logger
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# config formatter
fmt = '[%(asctime)s] [%(levelname)s] %(roomid)s%(message)s'
formatter = logging.Formatter(fmt)
# logging to console
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(formatter)
console_handler.addFilter(add_task_name)
logger.addHandler(console_handler)
async def get_room_list(
session: ClientSession, count: int = 99
) -> List[Dict[str, Any]]:
url = 'https://api.live.bilibili.com/room/v3/area/getRoomList'
params = {
'platform': 'web',
'parent_area_id': 1,
'cate_id': 0,
'area_id': 0,
'sort_type': 'online',
'page': 1,
'page_size': count,
'tag_version': 1,
}
async with session.get(url, params=params) as response:
j = await response.json()
return j['data']['list']
class DanmakuPrinter(DanmakuListener):
def __init__(
self, danmaku_client: DanmakuClient, room_id: int
) -> None:
self._danmaku_client = danmaku_client
self._room_id = room_id
async def enable(self) -> None:
self._danmaku_client.add_listener(self)
async def disable(self) -> None:
self._danmaku_client.remove_listener(self)
async def on_danmaku_received(self, danmu: Danmaku) -> None:
json_string = json.dumps(danmu, ensure_ascii=False)
logger.info(f'{json_string}')
class DanmakuDumper(DanmakuListener):
def __init__(
self, danmaku_client: DanmakuClient, room_id: int
) -> None:
self._danmaku_client = danmaku_client
from datetime import datetime
data_time_string = datetime.now().strftime("%Y-%m-%d_%H%M%S")
self._filename = f'blive_cmd_{room_id}_{data_time_string}.jsonl'
async def start(self) -> None:
import aiofiles
self._file = await aiofiles.open(
self._filename, mode='wt', encoding='utf8'
)
logger.debug(f'Opened file: {self._filename}')
self._danmaku_client.add_listener(self)
async def stop(self) -> None:
self._danmaku_client.remove_listener(self)
await self._file.close()
logger.debug(f'Closed file: {self._filename}')
async def on_danmaku_received(self, danmu: Danmaku) -> None:
json_string = json.dumps(danmu, ensure_ascii=False)
await self._file.write(json_string + '\n')
async def test_room(session: ClientSession, room_id: int) -> None:
client = DanmakuClient(session, room_id)
printer = DanmakuPrinter(client, room_id)
dumper = DanmakuDumper(client, room_id)
await printer.enable()
await dumper.start()
await client.start()
keyboard_interrupt_event = asyncio.Event()
try:
await keyboard_interrupt_event.wait()
finally:
await client.stop()
await dumper.stop()
await printer.disable()
async def test() -> None:
import sys
try:
# the room id must not be the short id!
room_ids = list(map(int, sys.argv[1:]))
except ValueError:
print('Usage: room_id, ...')
sys.exit(0)
configure_logger()
async with ClientSession() as session:
tasks = []
if not room_ids:
room_list = await get_room_list(session)
room_ids = [room['roomid'] for room in room_list]
logger.debug(f'room count: {len(room_ids)}')
for room_id in room_ids:
task = asyncio.create_task(
test_room(session, room_id), name=f'test_room::{room_id}',
)
tasks.append(task)
await asyncio.wait(tasks)
try:
asyncio.run(test())
except KeyboardInterrupt:
pass

View File

@ -0,0 +1,24 @@
import attr
@attr.s(auto_attribs=True, frozen=True, slots=True)
class ApiRequestError(Exception):
code: int
message: str
class LiveRoomHidden(Exception):
pass
class LiveRoomLocked(Exception):
pass
class LiveRoomEncrypted(Exception):
pass
class NoStreamUrlAvailable(Exception):
pass

29
src/blrec/bili/helpers.py Normal file
View File

@ -0,0 +1,29 @@
import aiohttp
from .api import WebApi
from .typing import ResponseData
from .exceptions import ApiRequestError
from ..exception import NotFoundError
__all__ = 'room_init', 'ensure_room_id'
async def room_init(room_id: int) -> ResponseData:
async with aiohttp.ClientSession(raise_for_status=True) as session:
api = WebApi(session)
return await api.room_init(room_id)
async def ensure_room_id(room_id: int) -> int:
"""Ensure room id is valid and is the real room id"""
try:
result = await room_init(room_id)
except ApiRequestError as e:
if e.code == 60004:
raise NotFoundError(f'the room {room_id} not existed')
else:
raise
else:
return result['room_id']

221
src/blrec/bili/live.py Normal file
View File

@ -0,0 +1,221 @@
import asyncio
import re
import json
from typing import Dict, List, Optional, cast
import aiohttp
from .api import WebApi
from .models import LiveStatus, RoomInfo, UserInfo
from .typing import QualityNumber, StreamFormat, ResponseData
from .exceptions import (
LiveRoomHidden, LiveRoomLocked, LiveRoomEncrypted, NoStreamUrlAvailable
)
__all__ = 'Live',
_INFO_PATTERN = re.compile(
rb'<script>\s*window\.__NEPTUNE_IS_MY_WAIFU__\s*=\s*(\{.*?\})\s*</script>'
)
_LIVE_STATUS_PATTERN = re.compile(rb'"live_status"\s*:\s*(\d)')
class Live:
def __init__(
self, room_id: int, user_agent: str = '', cookie: str = ''
) -> None:
self._room_id = room_id
self._user_agent = user_agent
self._cookie = cookie
self._html_page_url = f'https://live.bilibili.com/{room_id}'
@property
def user_agent(self) -> str:
return self._user_agent
@user_agent.setter
def user_agent(self, value: str) -> None:
self._user_agent = value
@property
def cookie(self) -> str:
return self._cookie
@cookie.setter
def cookie(self, value: str) -> None:
self._cookie = value
@property
def headers(self) -> Dict[str, str]:
return {
'Referer': 'https://live.bilibili.com/',
'Connection': 'Keep-Alive',
'User-Agent': self._user_agent,
'cookie': self._cookie,
}
@property
def session(self) -> aiohttp.ClientSession:
return self._session
@property
def room_id(self) -> int:
return self._room_id
@property
def room_info(self) -> RoomInfo:
return self._room_info
@property
def user_info(self) -> UserInfo:
return self._user_info
async def init(self) -> None:
self._session = aiohttp.ClientSession(
headers=self.headers,
raise_for_status=True,
trust_env=True,
)
self._api = WebApi(self._session)
self._room_info = await self.get_room_info()
self._user_info = await self.get_user_info(self._room_info.uid)
async def deinit(self) -> None:
await self._session.close()
async def get_live_status(self) -> LiveStatus:
try:
# frequent requests will be intercepted by the server's firewall!
live_status = await self._get_live_status_via_api()
except Exception:
# more cpu consumption
live_status = await self._get_live_status_via_html_page()
return LiveStatus(live_status)
def is_living(self) -> bool:
return self._room_info.live_status == LiveStatus.LIVE
async def update_info(self) -> None:
await asyncio.wait([self.update_user_info(), self.update_room_info()])
async def update_user_info(self) -> None:
self._user_info = await self.get_user_info(self._room_info.uid)
async def update_room_info(self) -> None:
self._room_info = await self.get_room_info()
async def get_room_info(self) -> RoomInfo:
try:
# frequent requests will be intercepted by the server's firewall!
room_info_data = await self._get_room_info_via_api()
except Exception:
# more cpu consumption
room_info_data = await self._get_room_info_via_html_page()
return RoomInfo.from_data(room_info_data)
async def get_user_info(self, uid: int) -> UserInfo:
user_info_data = await self._api.get_user_info(uid)
return UserInfo.from_data(user_info_data)
async def get_server_timestamp(self) -> int:
# the timestamp on the server at the moment in seconds
return await self._api.get_timestamp()
async def get_live_stream_url(
self, qn: QualityNumber = 10000, format: StreamFormat = 'flv'
) -> Optional[str]:
try:
data = await self._api.get_room_play_info(self._room_id, qn)
except Exception:
# fallback to the html page global info
data = await self._get_room_play_info_via_html_page()
self._check_room_play_info(data)
stream = data['playurl_info']['playurl']['stream']
if stream[0]['format'][0]['codec'][0]['current_qn'] != qn:
raise
else:
self._check_room_play_info(data)
streams = list(filter(
lambda s: s['format'][0]['format_name'] == format,
data['playurl_info']['playurl']['stream']
))
codec = streams[0]['format'][0]['codec'][0]
accept_qn = cast(List[QualityNumber], codec['accept_qn'])
if qn not in accept_qn:
return None
assert codec['current_qn'] == qn
url_info = codec['url_info'][0]
return url_info['host'] + codec['base_url'] + url_info['extra']
def _check_room_play_info(self, data: ResponseData) -> None:
if data['is_hidden']:
raise LiveRoomHidden()
if data['is_locked']:
raise LiveRoomLocked()
if data['encrypted'] and not data['pwd_verified']:
raise LiveRoomEncrypted()
try:
data['playurl_info']['playurl']['stream'][0]
except Exception:
raise NoStreamUrlAvailable()
async def _get_live_status_via_api(self) -> int:
room_info_data = await self._get_room_info_via_api()
return int(room_info_data['live_status'])
async def _get_room_info_via_api(self) -> ResponseData:
try:
room_info_data = await self._api.get_info(self._room_id)
except Exception:
info_data = await self._api.get_info_by_room(self._room_id)
room_info_data = info_data['room_info']
return room_info_data
async def _get_live_status_via_html_page(self) -> int:
async with self._session.get(self._html_page_url) as response:
data = await response.read()
m = _LIVE_STATUS_PATTERN.search(data)
assert m is not None, data
return int(m.group(1))
async def _get_room_info_via_html_page(self) -> ResponseData:
info_res = await self._get_room_info_res_via_html_page()
return info_res['room_info']
async def _get_room_play_info_via_html_page(self) -> ResponseData:
return await self._get_room_init_res_via_html_page()
async def _get_room_info_res_via_html_page(self) -> ResponseData:
info = await self._get_info_via_html_page()
if info['roomInfoRes']['code'] != 0:
raise ValueError(f"Invaild roomInfoRes: {info['roomInfoRes']}")
return info['roomInfoRes']['data']
async def _get_room_init_res_via_html_page(self) -> ResponseData:
info = await self._get_info_via_html_page()
if info['roomInitRes']['code'] != 0:
raise ValueError(f"Invaild roomInitRes: {info['roomInitRes']}")
return info['roomInitRes']['data']
async def _get_info_via_html_page(self) -> ResponseData:
async with self._session.get(self._html_page_url) as response:
data = await response.read()
m = _INFO_PATTERN.search(data)
assert m is not None, data
string = m.group(1).decode(encoding='utf8')
return json.loads(string)

View File

@ -0,0 +1,134 @@
import logging
from .danmaku_client import DanmakuClient, DanmakuListener, DanmakuCommand
from .live import Live
from .typing import Danmaku
from .models import LiveStatus, RoomInfo
from ..event.event_emitter import EventListener, EventEmitter
from ..utils.mixins import SwitchableMixin
__all__ = 'LiveMonitor', 'LiveEventListener'
logger = logging.getLogger(__name__)
class LiveEventListener(EventListener):
async def on_live_status_changed(
self, current_status: LiveStatus, previous_status: LiveStatus
) -> None:
...
async def on_live_began(self, live: Live) -> None:
...
async def on_live_ended(self, live: Live) -> None:
...
async def on_live_stream_available(self, live: Live) -> None:
...
async def on_live_stream_reset(self, live: Live) -> None:
...
async def on_room_changed(self, room_info: RoomInfo) -> None:
...
class LiveMonitor(
EventEmitter[LiveEventListener], DanmakuListener, SwitchableMixin
):
def __init__(self, danmaku_client: DanmakuClient, live: Live) -> None:
super().__init__()
self._danmaku_client = danmaku_client
self._live = live
def _init_status(self) -> None:
self._previous_status = self._live.room_info.live_status
if self._live.is_living():
self._status_count = 2
else:
self._status_count = 0
def _do_enable(self) -> None:
self._init_status()
self._danmaku_client.add_listener(self)
logger.debug('Enabled live monitor')
def _do_disable(self) -> None:
self._danmaku_client.remove_listener(self)
logger.debug('Disabled live monitor')
async def on_client_reconnected(self) -> None:
# check the live status after the client reconnected and simulate
# events if necessary.
# make sure the recorder works well continuously after interruptions
# such as an operating system hibernation.
logger.warning('The Danmaku Client Reconnected')
await self._live.update_info()
current_status = self._live.room_info.live_status
if current_status == self._previous_status:
return
if current_status == LiveStatus.LIVE:
logger.debug('Simulating live began event')
await self._handle_status_change(current_status)
logger.debug('Simulating live stream available event')
await self._handle_status_change(current_status)
else:
logger.debug('Simulating live ended event')
await self._handle_status_change(current_status)
async def on_danmaku_received(self, danmu: Danmaku) -> None:
danmu_cmd = danmu['cmd']
if danmu_cmd == DanmakuCommand.LIVE.value:
await self._handle_status_change(LiveStatus.LIVE)
elif danmu_cmd == DanmakuCommand.PREPARING.value:
if danmu.get('round', None) == 1:
await self._handle_status_change(LiveStatus.ROUND)
else:
await self._handle_status_change(LiveStatus.PREPARING)
elif danmu_cmd == DanmakuCommand.ROOM_CHANGE.value:
await self._live.update_room_info()
await self._emit('room_changed', self._live.room_info)
async def _handle_status_change(self, current_status: LiveStatus) -> None:
logger.debug('Live status changed from {} to {}'.format(
self._previous_status.name, current_status.name
))
await self._live.update_info()
assert self._live.room_info.live_status == current_status
await self._emit(
'live_status_changed', current_status, self._previous_status
)
if current_status != LiveStatus.LIVE:
self._status_count = 0
await self._emit('live_ended', self._live)
else:
self._status_count += 1
if self._status_count == 1:
assert self._previous_status != LiveStatus.LIVE
await self._emit('live_began', self._live)
elif self._status_count == 2:
assert self._previous_status == LiveStatus.LIVE
await self._emit('live_stream_available', self._live)
elif self._status_count > 2:
assert self._previous_status == LiveStatus.LIVE
await self._emit('live_stream_reset', self._live)
else:
pass
logger.debug('Number of sequential LIVE status: {}'.format(
self._status_count
))
self._previous_status = current_status

85
src/blrec/bili/models.py Normal file
View File

@ -0,0 +1,85 @@
from enum import IntEnum
from datetime import datetime
from typing import cast
import attr
from .typing import ResponseData
__all__ = 'LiveStatus', 'RoomInfo', 'UserInfo'
class LiveStatus(IntEnum):
PREPARING = 0
LIVE = 1
ROUND = 2
@attr.s(auto_attribs=True, frozen=True, slots=True)
class RoomInfo:
uid: int
room_id: int
short_room_id: int
area_id: int
area_name: str
parent_area_id: int
parent_area_name: str
live_status: LiveStatus
live_start_time: int # a integer in seconds
online: int
title: str
cover: str
tags: str
description: str
@staticmethod
def from_data(data: ResponseData) -> 'RoomInfo':
if (timestamp := data.get('live_start_time', None)) is not None:
live_start_time = cast(int, timestamp)
elif (time_string := data.get('live_time', None)) is not None:
if time_string == '0000-00-00 00:00:00':
live_start_time = 0
else:
dt = datetime.fromisoformat(time_string)
live_start_time = int(dt.timestamp())
else:
raise ValueError(f'Failed to init live_start_time: {data}')
return RoomInfo(
uid=data['uid'],
room_id=int(data['room_id']),
short_room_id=int(data['short_id']),
area_id=data['area_id'],
area_name=data['area_name'],
parent_area_id=data['parent_area_id'],
parent_area_name=data['parent_area_name'],
live_status=LiveStatus(data['live_status']),
live_start_time=live_start_time,
online=int(data['online']),
title=data['title'],
cover=data.get('cover', None) or data.get('user_cover', None),
tags=data['tags'],
description=data['description'],
)
@attr.s(auto_attribs=True, frozen=True, slots=True)
class UserInfo:
name: str
gender: str
face: str
uid: int
level: int
sign: str
@staticmethod
def from_data(data: ResponseData) -> 'UserInfo':
return UserInfo(
name=data['name'],
gender=data['sex'],
face=data['face'],
uid=data['mid'],
level=data['level'],
sign=data['sign'],
)

22
src/blrec/bili/typing.py Normal file
View File

@ -0,0 +1,22 @@
from typing import Any, Dict, Literal, Mapping
Danmaku = Mapping[str, Any]
QualityNumber = Literal[
20000, # 4K
10000, # 原画
401, # 蓝光(杜比)
400, # 蓝光
250, # 超清
150, # 高清
80, # 流畅
]
StreamFormat = Literal[
'flv',
'fmp4',
]
JsonResponse = Dict[str, Any]
ResponseData = Dict[str, Any]

View File

98
src/blrec/cli/main.py Normal file
View File

@ -0,0 +1,98 @@
import os
import logging
from copy import deepcopy
from typing import Optional
import uvicorn
from uvicorn.config import LOGGING_CONFIG
import typer
from .. import __prog__, __version__
from ..logging import TqdmOutputStream
from ..setting import DEFAULT_SETTINGS_PATH
logger = logging.getLogger(__name__)
cli = typer.Typer()
def version_callback(value: bool) -> None:
if value:
typer.echo(f'Bilibili live streaming recorder {__version__}')
raise typer.Exit()
@cli.command()
def cli_main(
version: Optional[bool] = typer.Option(
None,
'--version',
callback=version_callback,
is_eager=True,
help=f"show {__prog__}'s version and exit",
),
config: str = typer.Option(
'~/.blrec/settings.toml',
'--config',
'-c',
help='path of setting file',
),
out_dir: Optional[str] = typer.Option(
None,
'--out-dir',
'-o',
help='path of directory to save files (overwrite setting)'
),
host: str = typer.Option('127.0.0.1', help='webapp host bind'),
port: int = typer.Option(2233, help='webapp port bind'),
open: bool = typer.Option(False, help='open webapp in default browser'),
key_file: Optional[str] = typer.Option(None, help='SSL key file'),
cert_file: Optional[str] = typer.Option(None, help='SSL certificate file'),
api_key: Optional[str] = typer.Option(None, help='web api key'),
) -> None:
"""Bilibili live streaming recorder"""
if config != DEFAULT_SETTINGS_PATH:
os.environ['config'] = config
if api_key is not None:
os.environ['api_key'] = api_key
if out_dir is not None:
os.environ['out_dir'] = out_dir
if open:
typer.launch(f'http://{host}:{port}')
logging_config = deepcopy(LOGGING_CONFIG)
logging_config['handlers']['default']['stream'] = TqdmOutputStream
logging_config['handlers']['access']['stream'] = TqdmOutputStream
uvicorn.run(
'blrec.web:app',
host=host,
port=port,
ssl_keyfile=key_file,
ssl_certfile=cert_file,
log_config=logging_config,
log_level='info',
access_log=False,
)
def main() -> int:
try:
cli()
except KeyboardInterrupt:
return 1
except SystemExit:
return 1
except BaseException as e:
logger.exception(e)
return 2
else:
return 0
finally:
logger.info('Exit')
if __name__ == '__main__':
main()

View File

@ -0,0 +1,7 @@
from .recorder import Recorder, RecorderEventListener
__all__ = (
'Recorder',
'RecorderEventListener',
)

View File

@ -0,0 +1,156 @@
import html
import asyncio
import logging
from contextlib import suppress
from typing import Iterator, List
from .. import __version__, __prog__
from .danmaku_receiver import DanmakuReceiver, DanmuMsg
from .stream_recorder import StreamRecorder, StreamRecorderEventListener
from .statistics import StatisticsCalculator
from ..bili.live import Live
from ..exception import exception_callback
from ..path import danmaku_path
from ..danmaku.models import Metadata, Danmu
from ..danmaku.io import DanmakuWriter
from ..utils.mixins import SwitchableMixin
from ..logging.room_id import aio_task_with_room_id
__all__ = 'DanmakuDumper',
logger = logging.getLogger(__name__)
class DanmakuDumper(StreamRecorderEventListener, SwitchableMixin):
def __init__(
self,
live: Live,
stream_recorder: StreamRecorder,
danmaku_receiver: DanmakuReceiver,
*,
danmu_uname: bool = False,
) -> None:
super().__init__()
self._live = live
self._stream_recorder = stream_recorder
self._receiver = danmaku_receiver
self.danmu_uname = danmu_uname
self._files: List[str] = []
self._calculator = StatisticsCalculator(interval=60)
@property
def danmu_count(self) -> int:
return self._calculator.count
@property
def danmu_rate(self) -> float:
return self._calculator.rate
@property
def elapsed(self) -> float:
return self._calculator.elapsed
def _do_enable(self) -> None:
self._stream_recorder.add_listener(self)
logger.debug('Enabled danmaku dumper')
def _do_disable(self) -> None:
self._stream_recorder.remove_listener(self)
logger.debug('Disabled danmaku dumper')
def set_live_start_time(self, time: int) -> None:
self._live_start_time = time
def has_file(self) -> bool:
return bool(self._files)
def get_files(self) -> Iterator[str]:
for file in self._files:
yield file
def clear_files(self) -> None:
self._files.clear()
async def on_video_file_created(
self, video_path: str, record_start_time: int
) -> None:
self._path = danmaku_path(video_path)
self._record_start_time = record_start_time
self._files.append(self._path)
self._start_dumping()
async def on_video_file_completed(self, video_path: str) -> None:
await self._stop_dumping()
def _start_dumping(self) -> None:
self._create_dump_task()
async def _stop_dumping(self) -> None:
await self._cancel_dump_task()
def _create_dump_task(self) -> None:
self._dump_task = asyncio.create_task(self._dump())
self._dump_task.add_done_callback(exception_callback)
async def _cancel_dump_task(self) -> None:
self._dump_task.cancel()
with suppress(asyncio.CancelledError):
await self._dump_task
@aio_task_with_room_id
async def _dump(self) -> None:
logger.debug('Started dumping danmaku')
self._calculator.reset()
try:
async with DanmakuWriter(self._path) as writer:
logger.info(f"Danmaku file created: '{self._path}'")
await writer.write_metadata(self._make_metadata())
while True:
msg = await self._receiver.get_message()
await writer.write_danmu(self._make_danmu(msg))
self._calculator.submit(1)
finally:
logger.info(f"Danmaku file completed: '{self._path}'")
logger.debug('Stopped dumping danmaku')
self._calculator.freeze()
def _make_metadata(self) -> Metadata:
return Metadata(
user_name=self._live.user_info.name,
room_id=self._live.room_info.room_id,
room_title=self._live.room_info.title,
area=self._live.room_info.area_name,
parent_area=self._live.room_info.parent_area_name,
live_start_time=self._live.room_info.live_start_time,
record_start_time=self._record_start_time,
recorder=f'{__prog__} {__version__}',
)
def _make_danmu(self, msg: DanmuMsg) -> Danmu:
stime = max((msg.date - self._record_start_time * 1000), 0) / 1000
if self.danmu_uname:
text = f'{msg.uname}: {msg.text}'
else:
text = msg.text
text = html.escape(text)
return Danmu(
stime=stime,
mode=msg.mode,
size=msg.size,
color=msg.color,
date=msg.date,
pool=msg.pool,
uid=msg.uid,
dmid=msg.dmid,
text=text,
)

View File

@ -0,0 +1,52 @@
import logging
from asyncio import Queue, QueueFull
from typing import Final
from .models import DanmuMsg
from ..bili.danmaku_client import (
DanmakuClient, DanmakuListener, DanmakuCommand
)
from ..bili.typing import Danmaku
from ..utils.mixins import StoppableMixin
__all__ = 'DanmakuReceiver',
logger = logging.getLogger(__name__)
class DanmakuReceiver(DanmakuListener, StoppableMixin):
_MAX_QUEUE_SIZE: Final[int] = 2000
def __init__(self, danmaku_client: DanmakuClient) -> None:
super().__init__()
self._danmaku_client = danmaku_client
self._queue: Queue[DanmuMsg] = Queue(maxsize=self._MAX_QUEUE_SIZE)
def _do_start(self) -> None:
self._danmaku_client.add_listener(self)
logger.debug('Started danmaku receiver')
def _do_stop(self) -> None:
self._danmaku_client.remove_listener(self)
self._clear_queue()
logger.debug('Stopped danmaku receiver')
async def get_message(self) -> DanmuMsg:
return await self._queue.get()
async def on_danmaku_received(self, danmu: Danmaku) -> None:
if danmu['cmd'] != DanmakuCommand.DANMU_MSG.value:
return
danmu_msg = DanmuMsg.from_cmd(danmu)
try:
self._queue.put_nowait(danmu_msg)
except QueueFull:
self._queue.get_nowait() # discard the first item
self._queue.put_nowait(danmu_msg)
def _clear_queue(self) -> None:
self._queue = Queue(maxsize=self._MAX_QUEUE_SIZE)

36
src/blrec/core/models.py Normal file
View File

@ -0,0 +1,36 @@
import logging
import attr
from ..bili.typing import Danmaku
logger = logging.getLogger(__name__)
@attr.s(auto_attribs=True, frozen=True, slots=True)
class DanmuMsg:
mode: int
size: int # font size
color: int
date: int # a timestamp in miliseconds
dmid: int
pool: int
uid: str # midHash
text: str
uname: str # sender name
@staticmethod
def from_cmd(danmu: Danmaku) -> 'DanmuMsg':
info = danmu['info']
return DanmuMsg(
mode=int(info[0][1]),
size=int(info[0][2]),
color=int(info[0][3]),
date=int(info[0][4]),
dmid=int(info[0][5]),
pool=int(info[0][6]),
uid=info[0][7],
text=info[1],
uname=info[2][1],
)

307
src/blrec/core/recorder.py Normal file
View File

@ -0,0 +1,307 @@
from __future__ import annotations
import re
import html
import logging
from datetime import datetime
from typing import Iterator, Optional
import humanize
from .danmaku_receiver import DanmakuReceiver
from .danmaku_dumper import DanmakuDumper
from .stream_recorder import StreamRecorder
from ..event.event_emitter import EventListener, EventEmitter
from ..bili.live import Live
from ..bili.models import RoomInfo
from ..bili.danmaku_client import DanmakuClient
from ..bili.live_monitor import LiveMonitor, LiveEventListener
from ..bili.typing import QualityNumber
from ..utils.mixins import AsyncStoppableMixin
__all__ = 'RecorderEventListener', 'Recorder'
logger = logging.getLogger(__name__)
class RecorderEventListener(EventListener):
async def on_recording_started(self, recorder: Recorder) -> None:
...
async def on_recording_finished(self, recorder: Recorder) -> None:
...
async def on_recording_cancelled(self, recorder: Recorder) -> None:
...
class Recorder(
EventEmitter[RecorderEventListener], LiveEventListener, AsyncStoppableMixin
):
def __init__(
self,
live: Live,
danmaku_client: DanmakuClient,
live_monitor: LiveMonitor,
out_dir: str,
path_template: str,
*,
buffer_size: Optional[int] = None,
read_timeout: Optional[int] = None,
danmu_uname: bool = False,
filesize_limit: int = 0,
duration_limit: int = 0,
) -> None:
super().__init__()
self._live = live
self._danmaku_client = danmaku_client
self._live_monitor = live_monitor
self._recording: bool = False
self._stream_recorder = StreamRecorder(
self._live,
out_dir=out_dir,
path_template=path_template,
buffer_size=buffer_size,
read_timeout=read_timeout,
filesize_limit=filesize_limit,
duration_limit=duration_limit,
)
self._danmaku_receiver = DanmakuReceiver(danmaku_client)
self._danmaku_dumper = DanmakuDumper(
self._live,
self._stream_recorder,
self._danmaku_receiver,
danmu_uname=danmu_uname,
)
@property
def recording(self) -> bool:
return self._recording
@property
def quality_number(self) -> QualityNumber:
return self._stream_recorder.quality_number
@quality_number.setter
def quality_number(self, value: QualityNumber) -> None:
self._stream_recorder.quality_number = value
@property
def real_quality_number(self) -> QualityNumber:
return self._stream_recorder.real_quality_number
@property
def buffer_size(self) -> int:
return self._stream_recorder.buffer_size
@buffer_size.setter
def buffer_size(self, value: int) -> None:
self._stream_recorder.buffer_size = value
@property
def read_timeout(self) -> int:
return self._stream_recorder.read_timeout
@read_timeout.setter
def read_timeout(self, value: int) -> None:
self._stream_recorder.read_timeout = value
@property
def danmu_uname(self) -> bool:
return self._danmaku_dumper.danmu_uname
@danmu_uname.setter
def danmu_uname(self, value: bool) -> None:
self._danmaku_dumper.danmu_uname = value
@property
def elapsed(self) -> float:
return self._stream_recorder.elapsed
@property
def data_count(self) -> int:
return self._stream_recorder.data_count
@property
def data_rate(self) -> float:
return self._stream_recorder.data_rate
@property
def danmu_count(self) -> int:
return self._danmaku_dumper.danmu_count
@property
def danmu_rate(self) -> float:
return self._danmaku_dumper.danmu_rate
@property
def out_dir(self) -> str:
return self._stream_recorder.out_dir
@out_dir.setter
def out_dir(self, value: str) -> None:
self._stream_recorder.out_dir = value
@property
def path_template(self) -> str:
return self._stream_recorder.path_template
@path_template.setter
def path_template(self, value: str) -> None:
self._stream_recorder.path_template = value
@property
def filesize_limit(self) -> int:
return self._stream_recorder.filesize_limit
@filesize_limit.setter
def filesize_limit(self, value: int) -> None:
self._stream_recorder.filesize_limit = value
@property
def duration_limit(self) -> int:
return self._stream_recorder.duration_limit
@duration_limit.setter
def duration_limit(self, value: int) -> None:
self._stream_recorder.duration_limit = value
async def _do_start(self) -> None:
self._live_monitor.add_listener(self)
logger.debug('Started recorder')
self._print_live_info()
if self._live.is_living():
await self._start_recording(stream_available=True)
else:
self._print_waiting_message()
async def _do_stop(self) -> None:
await self._stop_recording()
self._live_monitor.remove_listener(self)
logger.debug('Stopped recorder')
def get_video_files(self) -> Iterator[str]:
yield from self._stream_recorder.get_files()
def get_danmaku_files(self) -> Iterator[str]:
yield from self._danmaku_dumper.get_files()
async def on_live_began(self, live: Live) -> None:
logger.info('The live has began')
self._print_live_info()
await self._start_recording()
async def on_live_ended(self, live: Live) -> None:
logger.info('The live has ended')
await self._stop_recording()
self._print_waiting_message()
async def on_live_stream_available(self, live: Live) -> None:
logger.debug('The live stream becomes available')
await self._stream_recorder.start()
async def on_live_stream_reset(self, live: Live) -> None:
logger.warning('The live stream has been reset')
async def on_room_changed(self, room_info: RoomInfo) -> None:
self._print_changed_room_info(room_info)
self._stream_recorder.update_progress_bar_info()
async def _start_recording(self, stream_available: bool = False) -> None:
if self._recording:
return
self._recording = True
self._danmaku_dumper.enable()
self._danmaku_receiver.start()
await self._prepare()
if stream_available:
await self._stream_recorder.start()
logger.info('Started recording')
await self._emit('recording_started', self)
async def _stop_recording(self) -> None:
if not self._recording:
return
self._recording = False
await self._stream_recorder.stop()
self._danmaku_dumper.disable()
self._danmaku_receiver.stop()
if self._stopped:
logger.info('Recording Cancelled')
await self._emit('recording_cancelled', self)
else:
logger.info('Recording Finished')
await self._emit('recording_finished', self)
async def _prepare(self) -> None:
live_start_time = self._live.room_info.live_start_time
self._danmaku_dumper.set_live_start_time(live_start_time)
self._danmaku_dumper.clear_files()
self._stream_recorder.clear_files()
def _print_waiting_message(self) -> None:
logger.info('Waiting... until the live starts')
def _print_live_info(self) -> None:
room_info = self._live.room_info
user_info = self._live.user_info
if room_info.live_start_time > 0:
live_start_time = str(
datetime.fromtimestamp(room_info.live_start_time)
)
else:
live_start_time = 'NULL'
desc = re.sub(
r'</?[a-zA-Z][a-zA-Z\d]*[^<>]*>',
'',
re.sub(r'<br\s*/?>', '\n', html.unescape(room_info.description))
).strip()
msg = f"""
================================== User Info ==================================
user name : {user_info.name}
gender : {user_info.gender}
sign : {user_info.sign}
uid : {user_info.uid}
level : {user_info.level}
---------------------------------- Room Info ----------------------------------
title : {room_info.title}
cover : {room_info.cover}
online : {humanize.intcomma(room_info.online)}
live status : {room_info.live_status.name}
live start time : {live_start_time}
room id : {room_info.room_id}
short room id : {room_info.short_room_id or 'NULL'}
area id : {room_info.area_id}
area name : {room_info.area_name}
parent area id : {room_info.parent_area_id}
parent area name : {room_info.parent_area_name}
tags : {room_info.tags}
description :
{desc}
===============================================================================
"""
logger.info(msg)
def _print_changed_room_info(self, room_info: RoomInfo) -> None:
msg = f"""
================================= Room Change =================================
title : {room_info.title}
area id {room_info.area_id}
area name : {room_info.area_name}
parent area id : {room_info.parent_area_id}
parent area name : {room_info.parent_area_name}
===============================================================================
"""
logger.info(msg)

62
src/blrec/core/retry.py Normal file
View File

@ -0,0 +1,62 @@
import logging
from typing import Any, Callable, Optional, Type, cast
from tenacity import wait_exponential, RetryCallState
from tenacity import _utils
from tenacity import compat as _compat
class wait_exponential_for_same_exceptions(wait_exponential):
"""Wait strategy that applies exponential backoff only for same
continuing exceptions.
"""
def __init__(
self,
multiplier: float = 1,
max: float = _utils.MAX_WAIT,
exp_base: int = 2,
min: float = 0,
continuing_criteria: float = 5.0,
) -> None:
super().__init__(multiplier, max, exp_base, min)
self._continuing_criteria = continuing_criteria
self._prev_exc_type: Optional[Type[BaseException]] = None
self._prev_exc_ts: Optional[float] = None
self._last_wait_time: float = 0
@_compat.wait_dunder_call_accept_old_params
def __call__(self, retry_state: RetryCallState) -> float:
if (
retry_state.outcome is not None and
(exc := retry_state.outcome.exception())
):
curr_exc_type = type(exc)
curr_exc_ts = cast(float, retry_state.outcome_timestamp)
if (
curr_exc_type is not self._prev_exc_type or
not self._is_continuing(curr_exc_ts)
):
retry_state.attempt_number = 1
self._prev_exc_type = curr_exc_type
self._prev_exc_ts = curr_exc_ts
self._last_wait_time = wait_time = super().__call__(retry_state)
return wait_time
def _is_continuing(self, curr_exc_ts: float) -> bool:
assert self._prev_exc_ts is not None
return (
curr_exc_ts - (self._prev_exc_ts + self._last_wait_time) <
self._continuing_criteria
)
def before_sleep_log(
logger: logging.Logger, log_level: int, name: str = ''
) -> Callable[[RetryCallState], Any]:
def log_it(retry_state: RetryCallState) -> None:
seconds = cast(float, getattr(retry_state.next_action, 'sleep'))
logger.log(log_level, 'Retry %s after %s seconds', name, seconds)
return log_it

View File

@ -0,0 +1,59 @@
import time
__all__ = 'StatisticsCalculator',
class StatisticsCalculator:
def __init__(self, interval: float = 1.0) -> None:
self._interval = interval
self._frozen = True
self._count: int = 0
self._rate: float = 0.0
self._start_time: float = 0.0
self._last_time: float = 0.0
self._last_count: int = 0
self._elapsed: float = 0.0
@property
def count(self) -> int:
return self._count
@property
def rate(self) -> float:
if self._frozen:
return self._rate
curr_time = time.monotonic()
time_delta = curr_time - self._last_time
if time_delta >= self._interval:
count_delta = self._count - self._last_count
self._rate = count_delta / time_delta
self._last_time = curr_time
self._last_count = self._count
return self._rate
@property
def elapsed(self) -> float:
if not self._frozen:
self._elapsed = time.monotonic() - self._start_time
return self._elapsed
def freeze(self) -> None:
self._frozen = True
def reset(self) -> None:
self._count = self._last_count = 0
self._start_time = self._last_time = time.monotonic()
self._rate = 0.0
self._elapsed = 0.0
self._frozen = False
def submit(self, count: int) -> None:
if not self._frozen:
self._count += count

View File

@ -0,0 +1,506 @@
import io
import os
import time
import errno
import asyncio
import logging
from threading import Thread
from datetime import datetime, timezone, timedelta
from collections import OrderedDict
from typing import Any, BinaryIO, Dict, Iterator, Optional, Tuple
import aiohttp
import requests
import urllib3
from tqdm import tqdm
from rx.subject import Subject
from rx.core import Observable
from tenacity import (
retry,
wait_none,
wait_fixed,
wait_chain,
wait_exponential,
stop_after_delay,
stop_after_attempt,
retry_if_result,
retry_if_exception,
retry_if_exception_type,
Retrying,
TryAgain,
)
from .. import __version__, __prog__
from .retry import wait_exponential_for_same_exceptions, before_sleep_log
from .statistics import StatisticsCalculator
from ..event.event_emitter import EventListener, EventEmitter
from ..bili.live import Live
from ..bili.typing import QualityNumber
from ..flv.stream_processor import StreamProcessor, BaseOutputFileManager
from ..utils.mixins import AsyncCooperationMix, AsyncStoppableMixin
from ..flv.exceptions import FlvStreamCorruptedError
from ..bili.exceptions import (
LiveRoomHidden, LiveRoomLocked, LiveRoomEncrypted, NoStreamUrlAvailable
)
__all__ = 'StreamRecorderEventListener', 'StreamRecorder'
logger = logging.getLogger(__name__)
logging.getLogger(urllib3.__name__).setLevel(logging.WARNING)
class StreamRecorderEventListener(EventListener):
async def on_video_file_created(
self, path: str, record_start_time: int
) -> None:
...
async def on_video_file_completed(self, path: str) -> None:
...
class StreamRecorder(
EventEmitter[StreamRecorderEventListener],
AsyncCooperationMix,
AsyncStoppableMixin,
):
def __init__(
self,
live: Live,
out_dir: str,
path_template: str,
*,
quality_number: QualityNumber = 10000,
buffer_size: Optional[int] = None,
read_timeout: Optional[int] = None,
filesize_limit: int = 0,
duration_limit: int = 0,
) -> None:
super().__init__()
self._live = live
self._progress_bar: Optional[tqdm] = None
self._stream_processor: Optional[StreamProcessor] = None
self._calculator = StatisticsCalculator()
self._file_manager = OutputFileManager(
live, out_dir, path_template, buffer_size
)
self._quality_number = quality_number
self._real_quality_number: Optional[QualityNumber] = None
self.buffer_size = buffer_size or io.DEFAULT_BUFFER_SIZE # bytes
self.read_timeout = read_timeout or 3 # seconds
self._filesize_limit = filesize_limit or 0
self._duration_limit = duration_limit or 0
def on_file_created(args: Tuple[str, int]) -> None:
logger.info(f"Video file created: '{args[0]}'")
self._emit_event('video_file_created', *args)
def on_file_closed(path: str) -> None:
logger.info(f"Video file completed: '{path}'")
self._emit_event('video_file_completed', path)
self._file_manager.file_creates.subscribe(on_file_created)
self._file_manager.file_closes.subscribe(on_file_closed)
@property
def data_count(self) -> int:
return self._calculator.count
@property
def data_rate(self) -> float:
return self._calculator.rate
@property
def elapsed(self) -> float:
return self._calculator.elapsed
@property
def out_dir(self) -> str:
return self._file_manager.out_dir
@out_dir.setter
def out_dir(self, value: str) -> None:
self._file_manager.out_dir = value
@property
def path_template(self) -> str:
return self._file_manager.path_template
@path_template.setter
def path_template(self, value: str) -> None:
self._file_manager.path_template = value
@property
def quality_number(self) -> QualityNumber:
return self._quality_number
@quality_number.setter
def quality_number(self, value: QualityNumber) -> None:
self._quality_number = value
self._real_quality_number = None
@property
def real_quality_number(self) -> QualityNumber:
return self._real_quality_number or 10000
@property
def filesize_limit(self) -> int:
if self._stream_processor is not None:
return self._stream_processor.filesize_limit
else:
return self._filesize_limit
@filesize_limit.setter
def filesize_limit(self, value: int) -> None:
self._filesize_limit = value
if self._stream_processor is not None:
self._stream_processor.filesize_limit = value
@property
def duration_limit(self) -> int:
if self._stream_processor is not None:
return self._stream_processor.duration_limit
else:
return self._duration_limit
@duration_limit.setter
def duration_limit(self, value: int) -> None:
self._duration_limit = value
if self._stream_processor is not None:
self._stream_processor.duration_limit = value
def has_file(self) -> bool:
return self._file_manager.has_file()
def get_files(self) -> Iterator[str]:
yield from self._file_manager.get_files()
def clear_files(self) -> None:
self._file_manager.clear_files()
def update_progress_bar_info(self) -> None:
if self._progress_bar is not None:
self._progress_bar.set_postfix_str(self._make_pbar_postfix())
async def _do_start(self) -> None:
self._thread = Thread(
target=self._run, name=f'StreamRecorder::{self._live.room_id}'
)
self._thread.start()
logger.debug('Started stream recorder')
async def _do_stop(self) -> None:
logger.debug('Stopping stream recorder...')
if self._stream_processor is not None:
self._stream_processor.cancel()
await self._loop.run_in_executor(None, self._thread.join)
logger.debug('Stopped stream recorder')
def _run(self) -> None:
self._calculator.reset()
try:
with tqdm(
desc='Recording',
unit='B',
unit_scale=True,
unit_divisor=1024,
postfix=self._make_pbar_postfix(),
) as progress_bar:
self._progress_bar = progress_bar
def update_size(size: int) -> None:
progress_bar.update(size)
self._calculator.submit(size)
self._stream_processor = StreamProcessor(
self._file_manager,
filesize_limit=self._filesize_limit,
duration_limit=self._duration_limit,
metadata=self._make_metadata(),
analyse_data=True,
dedup_join=True,
save_extra_metadata=True,
)
self._stream_processor.size_updates.subscribe(update_size)
with requests.Session() as self._session:
self._main_loop()
except Exception as e:
self._handle_exception(e)
finally:
if self._stream_processor is not None:
self._stream_processor.finalize()
self._stream_processor = None
self._progress_bar = None
self._calculator.freeze()
def _main_loop(self) -> None:
for attempt in Retrying(
reraise=True,
retry=(
retry_if_result(lambda r: not self._stopped) |
retry_if_exception(lambda e: not isinstance(e, OSError))
),
wait=wait_exponential_for_same_exceptions(max=60),
stop=stop_after_delay(1800),
before_sleep=before_sleep_log(logger, logging.DEBUG, 'main_loop'),
):
with attempt:
try:
self._streaming_loop()
except NoStreamUrlAvailable:
logger.debug('No stream url available')
if not self._stopped:
raise TryAgain
except OSError as e:
if e.errno == errno.ENOSPC:
# OSError(28, 'No space left on device')
raise
logger.critical(repr(e), exc_info=e)
raise TryAgain
except LiveRoomHidden:
logger.error('The live room has been hidden!')
self._stopped = True
except LiveRoomLocked:
logger.error('The live room has been locked!')
self._stopped = True
except LiveRoomEncrypted:
logger.error('The live room has been encrypted!')
self._stopped = True
except Exception as e:
logger.exception(e)
raise
def _streaming_loop(self) -> None:
url = self._get_live_stream_url()
while not self._stopped:
try:
self._streaming(url)
except requests.exceptions.HTTPError as e:
# frequently occurred when the live just started or ended.
logger.debug(repr(e))
self._defer_retry(1, 'streaming_loop')
# the url may has been forbidden or expired
# when the status code is 404 or 403
if e.response.status_code in (403, 404):
url = self._get_live_stream_url()
except requests.exceptions.Timeout as e:
logger.warning(repr(e))
except urllib3.exceptions.TimeoutError as e:
logger.warning(repr(e))
except urllib3.exceptions.ProtocolError as e:
# ProtocolError('Connection broken: IncompleteRead(
logger.warning(repr(e))
except requests.exceptions.ConnectionError as e:
logger.warning(repr(e))
except FlvStreamCorruptedError as e:
logger.warning(repr(e))
def _streaming(self, url: str) -> None:
logger.debug('Getting the live stream...')
with self._session.get(
url,
headers=self._live.headers,
stream=True,
timeout=self.read_timeout,
) as response:
logger.debug('Response received')
response.raise_for_status()
assert self._stream_processor is not None
self._stream_processor.process_stream(
io.BufferedReader(
ResponseProxy(response.raw), buffer_size=8192
)
)
@retry(
reraise=True,
retry=retry_if_exception_type((
asyncio.TimeoutError, aiohttp.ClientError,
)),
wait=wait_chain(wait_none(), wait_fixed(1)),
stop=stop_after_attempt(300),
)
def _get_live_stream_url(self) -> str:
qn = self._real_quality_number or self.quality_number
url = self._run_coroutine(self._live.get_live_stream_url(qn, 'flv'))
if self._real_quality_number is None:
if url is None:
logger.info(
f'The specified video quality ({qn}) is not available, '
'using the original video quality (10000) instead.'
)
self._real_quality_number = 10000
raise TryAgain
else:
logger.info(f'The specified video quality ({qn}) is available')
self._real_quality_number = self.quality_number
assert url is not None
logger.debug(f"Got live stream url: '{url}'")
return url
def _defer_retry(self, seconds: float, name: str = '') -> None:
if seconds <= 0:
return
logger.debug(f'Retry {name} after {seconds} seconds')
time.sleep(seconds)
def _make_pbar_postfix(self) -> str:
return '{room_id} - {user_name}: {room_title}'.format(
room_id=self._live.room_info.room_id,
user_name=self._live.user_info.name,
room_title=self._live.room_info.title,
)
def _make_metadata(self) -> Dict[str, Any]:
github_url = 'https://github.com/acgnhiki/blrec'
live_start_time = datetime.fromtimestamp(
self._live.room_info.live_start_time, timezone(timedelta(hours=8))
)
return {
'Title': self._live.room_info.title,
'Artist': self._live.user_info.name,
'Date': str(live_start_time),
'Comment': f'''\
B站直播录像
主播{self._live.user_info.name}
标题{self._live.room_info.title}
分区{self._live.room_info.parent_area_name} - {self._live.room_info.area_name}
房间号{self._live.room_info.room_id}
开播时间{live_start_time}
录制程序{__prog__} v{__version__} {github_url}''',
'description': OrderedDict({
'UserId': str(self._live.user_info.uid),
'UserName': self._live.user_info.name,
'RoomId': str(self._live.room_info.room_id),
'RoomTitle': self._live.room_info.title,
'Area': self._live.room_info.area_name,
'ParentArea': self._live.room_info.parent_area_name,
'LiveStartTime': str(live_start_time),
'Recorder': f'{__prog__} v{__version__} {github_url}',
})
}
def _emit_event(self, name: str, *args: Any, **kwds: Any) -> None:
self._run_coroutine(super()._emit(name, *args, **kwds))
class ResponseProxy(io.RawIOBase):
def __init__(self, response: urllib3.HTTPResponse) -> None:
self._response = response
@property
def closed(self) -> bool:
# always return False to avoid that `ValueError: read of closed file`,
# raised from `CHECK_CLOSED(self, "read of closed file")`,
# result in losing data those remaining in the buffer.
# ref: `https://gihub.com/python/cpython/blob/63298930fb531ba2bb4f23bc3b915dbf1e17e9e1/Modules/_io/bufferedio.c#L882` # noqa
return False
def readable(self) -> bool:
return True
def read(self, size: int = -1) -> bytes:
return self._response.read(size)
def tell(self) -> int:
return self._response.tell()
def readinto(self, b: Any) -> int:
return self._response.readinto(b)
def close(self) -> None:
self._response.close()
class OutputFileManager(BaseOutputFileManager, AsyncCooperationMix):
def __init__(
self,
live: Live,
out_dir: str,
path_template: str,
buffer_size: Optional[int] = None,
) -> None:
super().__init__(buffer_size)
self._live = live
self.out_dir = out_dir
self.path_template = path_template
self._file_creates = Subject()
self._file_closes = Subject()
@property
def file_creates(self) -> Observable:
return self._file_creates
@property
def file_closes(self) -> Observable:
return self._file_closes
def create_file(self) -> BinaryIO:
self._start_time = self._get_timestamp()
file = super().create_file()
self._file_creates.on_next((self._curr_path, self._start_time))
return file
def close_file(self) -> None:
path = self._curr_path
super().close_file()
self._file_closes.on_next(path)
def _get_timestamp(self) -> int:
try:
return self._get_server_timestamp()
except Exception as e:
logger.warning(f'Failed to get server timestamp: {repr(e)}')
return self._get_local_timestamp()
def _get_local_timestamp(self) -> int:
return int(time.time())
@retry(
reraise=True,
retry=retry_if_exception_type((
asyncio.TimeoutError, aiohttp.ClientError,
)),
wait=wait_exponential(multiplier=0.1, max=1),
stop=stop_after_delay(3),
)
def _get_server_timestamp(self) -> int:
return self._run_coroutine(self._live.get_server_timestamp())
def _make_path(self) -> str:
date_time = datetime.fromtimestamp(self._start_time)
relpath = self.path_template.format(
roomid=self._live.room_id,
uname=self._live.user_info.name,
title=self._live.room_info.title,
area=self._live.room_info.area_name,
parent_area=self._live.room_info.parent_area_name,
year=date_time.year,
month=str(date_time.month).rjust(2, '0'),
day=str(date_time.day).rjust(2, '0'),
hour=str(date_time.hour).rjust(2, '0'),
minute=str(date_time.minute).rjust(2, '0'),
second=str(date_time.second).rjust(2, '0'),
)
full_pathname = os.path.abspath(
os.path.expanduser(os.path.join(self.out_dir, relpath) + '.flv')
)
os.makedirs(os.path.dirname(full_pathname), exist_ok=True)
return full_pathname

View File

View File

@ -0,0 +1,44 @@
import logging
from typing import Iterable
from .io import DanmakuReader, DanmakuWriter
from .common import copy_damus
from .typing import TimebaseType
__all__ = 'TimebaseType', 'DanmakuCombinator'
logger = logging.getLogger(__name__)
class DanmakuCombinator:
def __init__(
self,
in_paths: Iterable[str],
out_path: str,
timebase_type: TimebaseType = TimebaseType.RECORD,
) -> None:
self._in_paths = list(in_paths)
self._out_path = out_path
self._timebase_type = timebase_type
async def combine(self) -> None:
async with DanmakuWriter(self._out_path) as writer:
writer = writer
async with DanmakuReader(self._in_paths[0]) as reader:
metadata = await reader.read_metadata()
await writer.write_metadata(metadata)
if self._timebase_type == TimebaseType.LIVE:
timebase = metadata.live_start_time * 1000
else:
timebase = metadata.record_start_time * 1000
await copy_damus(reader, writer, timebase=timebase)
for path in self._in_paths[1:]:
async with DanmakuReader(path) as reader:
await copy_damus(reader, writer, timebase=timebase)

View File

@ -0,0 +1,22 @@
from typing import Optional
import attr
from .io import DanmakuReader, DanmakuWriter
async def copy_damus(
reader: DanmakuReader,
writer: DanmakuWriter,
*,
timebase: Optional[int] = None, # milliseconds
delta: int = 0, # milliseconds
) -> None:
async for danmu in reader.read_danmus():
if timebase is None:
stime = max(0, danmu.stime * 1000 + delta) / 1000
else:
stime = max(0, danmu.date - timebase + delta) / 1000
new_danmu = attr.evolve(danmu, stime=stime)
await writer.write_danmu(new_danmu)

View File

@ -0,0 +1,33 @@
import logging
from typing import Iterable
from .io import DanmakuReader, DanmakuWriter
from .common import copy_damus
logger = logging.getLogger(__name__)
__all__ = 'DanmakuConcatenator'
class DanmakuConcatenator:
def __init__(
self, in_paths: Iterable[str], deltas: Iterable[int], out_path: str
) -> None:
self._in_paths = list(in_paths)
self._deltas = list(deltas)
self._out_path = out_path
assert len(self._in_paths) == len(self._deltas)
async def concat(self) -> None:
async with DanmakuWriter(self._out_path) as writer:
async with DanmakuReader(self._in_paths[0]) as reader:
metadata = await reader.read_metadata()
await writer.write_metadata(metadata)
await copy_damus(reader, writer, delta=self._deltas[0])
for path, delta in zip(self._in_paths[1:], self._deltas[1:]):
async with DanmakuReader(path) as reader:
await copy_damus(reader, writer, delta=delta)

View File

@ -0,0 +1,51 @@
import os
from typing import Iterable
from .io import DanmakuReader, DanmakuWriter
from .combination import DanmakuCombinator, TimebaseType
from .concatenation import DanmakuConcatenator
async def has_danmu(path: str) -> bool:
async with DanmakuReader(path) as reader:
async for _ in reader.read_danmus():
return True
return False
async def clear_danmu(path: str) -> None:
async with DanmakuReader(path) as reader:
async with DanmakuWriter(path) as writer:
metadata = await reader.read_metadata()
await writer.write_metadata(metadata)
async def concat_danmaku(
in_paths: Iterable[str], deltas: Iterable[int], out_path: str
) -> None:
concatenator = DanmakuConcatenator(in_paths, deltas, out_path)
await concatenator.concat()
async def combine_danmaku(
in_paths: Iterable[str],
out_path: str,
timebase_type: TimebaseType = TimebaseType.RECORD,
) -> None:
combiator = DanmakuCombinator(in_paths, out_path, timebase_type)
await combiator.combine()
async def merge_danmaku(src: str, dst: str, insert: bool = False) -> None:
"""Merge all danmus in the src into the dst.
insert: if true, src insert before dst otherwise src append after dst
"""
out = dst + '.tmp'
if insert:
await combine_danmaku([src, dst], out)
else:
await combine_danmaku([dst, src], out)
await clear_danmu(src)
os.replace(out, dst)

130
src/blrec/danmaku/io.py Normal file
View File

@ -0,0 +1,130 @@
from __future__ import annotations
import html
import asyncio
from typing import AsyncIterator, Final, List
from lxml import etree
import aiofiles
from .models import Metadata, Danmu
from .typing import Element
__all__ = 'DanmakuReader', 'DanmakuWriter'
class DanmakuReader:
def __init__(self, path: str) -> None:
self._path = path
async def __aenter__(self) -> DanmakuReader:
await self.init()
return self
async def __aexit__(self, exc_type, exc, tb): # type: ignore
pass
async def init(self) -> None:
loop = asyncio.get_running_loop()
self._tree = await loop.run_in_executor(None, etree.parse, self._path)
async def read_metadata(self) -> Metadata:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, self._read_metadata)
async def read_danmus(self) -> AsyncIterator[Danmu]:
for elem in await self._get_danmu_elems():
yield self._make_danmu(elem)
def _read_metadata(self) -> Metadata:
return Metadata(
user_name=self._tree.xpath('/i/metadata/user_name')[0].text,
room_id=int(self._tree.xpath('/i/metadata/room_id')[0].text),
room_title=self._tree.xpath('/i/metadata/room_title')[0].text,
area=self._tree.xpath('/i/metadata/area')[0].text,
parent_area=self._tree.xpath('/i/metadata/parent_area')[0].text,
live_start_time=int(
self._tree.xpath('/i/metadata/live_start_time')[0].text
),
record_start_time=int(
self._tree.xpath('/i/metadata/record_start_time')[0].text
),
recorder=self._tree.xpath('/i/metadata/recorder')[0].text,
)
async def _get_danmu_elems(self) -> List[Element]:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, self._tree.xpath, '/i/d')
def _make_danmu(self, elem: Element) -> Danmu:
params = elem.get('p').split(',')
return Danmu(
stime=float(params[0]),
mode=int(params[1]),
size=int(params[2]),
color=int(params[3]),
date=int(params[4]),
pool=int(params[5]),
uid=params[6],
dmid=int(params[7]),
text=elem.text,
)
class DanmakuWriter:
_XML_HEAD: Final[str] = """\
<?xml version="1.0" encoding="UTF-8"?>
<i>
<chatserver>chat.bilibili.com</chatserver>
<chatid>0</chatid>
<mission>0</mission>
<maxlimit>0</maxlimit>
<state>0</state>
<real_name>0</real_name>
<source>e-r</source>
"""
def __init__(self, path: str):
self._path = path
async def __aenter__(self) -> DanmakuWriter:
await self.init()
return self
async def __aexit__(self, exc_type, exc, tb): # type: ignore
await self.complete()
async def init(self) -> None:
self._file = await aiofiles.open(self._path, 'wt', encoding='utf8')
await self._file.write(self._XML_HEAD)
async def write_metadata(self, metadata: Metadata) -> None:
await self._file.write(self._serialize_metadata(metadata))
async def write_danmu(self, danmu: Danmu) -> None:
await self._file.write(self._serialize_danmu(danmu))
async def complete(self) -> None:
await self._file.write('</i>')
await self._file.close()
def _serialize_metadata(self, metadata: Metadata) -> str:
return f"""\
<metadata>
<user_name>{html.escape(metadata.user_name)}</user_name>
<room_id>{metadata.room_id}</room_id>
<room_title>{html.escape(metadata.room_title)}</room_title>
<area>{html.escape(metadata.area)}</area>
<parent_area>{html.escape(metadata.parent_area)}</parent_area>
<live_start_time>{metadata.live_start_time}</live_start_time>
<record_start_time>{metadata.record_start_time}</record_start_time>
<recorder>{metadata.recorder}</recorder>
</metadata>
"""
def _serialize_danmu(self, dm: Danmu) -> str:
return (
f' <d p="{dm.stime:.5f},{dm.mode},{dm.size},{dm.color},'
f'{dm.date},{dm.pool},{dm.uid},{dm.dmid}">'
f'{html.escape(dm.text)}</d>\n'
)

View File

@ -0,0 +1,28 @@
import attr
@attr.s(auto_attribs=True, slots=True, frozen=True)
class Metadata:
user_name: str
room_id: int
room_title: str
area: str
parent_area: str
live_start_time: int # seconds
record_start_time: int # seconds
recorder: str
@attr.s(auto_attribs=True, slots=True, frozen=True)
class Danmu:
stime: float
mode: int
size: int
color: int
date: int # milliseconds
pool: int
uid: str # uid hash
dmid: int
text: str

View File

@ -0,0 +1,10 @@
from enum import Enum
from typing import Any
class TimebaseType(Enum):
LIVE = 'live'
RECORD = 'record'
Element = Any

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,478 @@
@angular/animations
MIT
@angular/cdk
MIT
The MIT License
Copyright (c) 2021 Google LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@angular/common
MIT
@angular/core
MIT
@angular/forms
MIT
@angular/platform-browser
MIT
@angular/router
MIT
@angular/service-worker
MIT
@ant-design/colors
MIT
MIT LICENSE
Copyright (c) 2018-present Ant UED, https://xtech.antfin.com/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@ant-design/icons-angular
MIT
@babel/runtime
MIT
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@ctrl/tinycolor
MIT
Copyright (c) Scott Cooper <scttcper@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
filesize
BSD-3-Clause
Copyright (c) 2021, Jason Mulligan
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of filesize nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
lodash-es
MIT
Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
Based on Underscore.js, copyright Jeremy Ashkenas,
DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
This software consists of voluntary contributions made by many
individuals. For exact contribution history, see the revision history
available at https://github.com/lodash/lodash
The following license applies to all parts of this software except as
documented below:
====
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
====
Copyright and related rights for sample code are waived via CC0. Sample
code is defined as all source code displayed within the prose of the
documentation.
CC0: http://creativecommons.org/publicdomain/zero/1.0/
====
Files located in the node_modules and vendor directories are externally
maintained libraries used by this software which have their own
licenses; we recommend you read them, as their terms may differ from the
terms above.
ng-zorro-antd
MIT
ngx-logger
MIT
The MIT License
Copyright (c) 2018 David Fannin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
rxjs
Apache-2.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
tslib
0BSD
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
vlq
MIT
Copyright (c) 2017 [these people](https://github.com/Rich-Harris/vlq/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
zone.js
MIT
The MIT License
Copyright (c) 2010-2020 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
(function() {
__ant_icon_load({
name: 'animal:panda',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M99.096 315.634s-82.58-64.032-82.58-132.13c0-66.064 33.032-165.162 148.646-148.646 83.37 11.91 99.096 165.162 99.096 165.162l-165.162 115.614zM924.906 315.634s82.58-64.032 82.58-132.13c0-66.064-33.032-165.162-148.646-148.646-83.37 11.91-99.096 165.162-99.096 165.162l165.162 115.614z" fill="#6B676E" p-id="1143" />
<path d="M1024 561.548c0 264.526-229.23 429.42-512.002 429.42S0 826.076 0 561.548 283.96 66.064 512.002 66.064 1024 297.022 1024 561.548z" fill="#FFEBD2" p-id="1144" />
<path d="M330.324 842.126c0 82.096 81.34 148.646 181.678 148.646s181.678-66.55 181.678-148.646H330.324z" fill="#E9D7C3" p-id="1145" />
<path d="M644.13 611.098C594.582 528.516 561.55 512 512.002 512c-49.548 0-82.58 16.516-132.13 99.096-42.488 70.814-78.73 211.264-49.548 247.742 66.064 82.58 165.162 33.032 181.678 33.032 16.516 0 115.614 49.548 181.678-33.032 29.18-36.476-7.064-176.93-49.55-247.74z" fill="#FFFFFF" p-id="1146" />
<path d="M611.098 495.484c0-45.608 36.974-82.58 82.58-82.58 49.548 0 198.194 99.098 198.194 165.162s-79.934 144.904-148.646 99.096c-49.548-33.032-132.128-148.646-132.128-181.678zM412.904 495.484c0-45.608-36.974-82.58-82.58-82.58-49.548 0-198.194 99.098-198.194 165.162s79.934 144.904 148.646 99.096c49.548-33.032 132.128-148.646 132.128-181.678z" fill="#6B676E" p-id="1147" />
<path d="M512.002 726.622c-30.06 0-115.614 5.668-115.614 33.032 0 49.638 105.484 85.24 115.614 82.58 10.128 2.66 115.614-32.944 115.614-82.58-0.002-27.366-85.556-33.032-115.614-33.032z" fill="#464655" p-id="1148" />
<path d="M330.324 495.484m-33.032 0a33.032 33.032 0 1 0 66.064 0 33.032 33.032 0 1 0-66.064 0Z" fill="#464655" p-id="1149" />
<path d="M693.678 495.484m-33.032 0a33.032 33.032 0 1 0 66.064 0 33.032 33.032 0 1 0-66.064 0Z" fill="#464655" p-id="1150" />
</svg>
`
});
})();

View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M99.096 315.634s-82.58-64.032-82.58-132.13c0-66.064 33.032-165.162 148.646-148.646 83.37 11.91 99.096 165.162 99.096 165.162l-165.162 115.614zM924.906 315.634s82.58-64.032 82.58-132.13c0-66.064-33.032-165.162-148.646-148.646-83.37 11.91-99.096 165.162-99.096 165.162l165.162 115.614z" fill="#6B676E" p-id="1143" />
<path d="M1024 561.548c0 264.526-229.23 429.42-512.002 429.42S0 826.076 0 561.548 283.96 66.064 512.002 66.064 1024 297.022 1024 561.548z" fill="#FFEBD2" p-id="1144" />
<path d="M330.324 842.126c0 82.096 81.34 148.646 181.678 148.646s181.678-66.55 181.678-148.646H330.324z" fill="#E9D7C3" p-id="1145" />
<path d="M644.13 611.098C594.582 528.516 561.55 512 512.002 512c-49.548 0-82.58 16.516-132.13 99.096-42.488 70.814-78.73 211.264-49.548 247.742 66.064 82.58 165.162 33.032 181.678 33.032 16.516 0 115.614 49.548 181.678-33.032 29.18-36.476-7.064-176.93-49.55-247.74z" fill="#FFFFFF" p-id="1146" />
<path d="M611.098 495.484c0-45.608 36.974-82.58 82.58-82.58 49.548 0 198.194 99.098 198.194 165.162s-79.934 144.904-148.646 99.096c-49.548-33.032-132.128-148.646-132.128-181.678zM412.904 495.484c0-45.608-36.974-82.58-82.58-82.58-49.548 0-198.194 99.098-198.194 165.162s79.934 144.904 148.646 99.096c49.548-33.032 132.128-148.646 132.128-181.678z" fill="#6B676E" p-id="1147" />
<path d="M512.002 726.622c-30.06 0-115.614 5.668-115.614 33.032 0 49.638 105.484 85.24 115.614 82.58 10.128 2.66 115.614-32.944 115.614-82.58-0.002-27.366-85.556-33.032-115.614-33.032z" fill="#464655" p-id="1148" />
<path d="M330.324 495.484m-33.032 0a33.032 33.032 0 1 0 66.064 0 33.032 33.032 0 1 0-66.064 0Z" fill="#464655" p-id="1149" />
<path d="M693.678 495.484m-33.032 0a33.032 33.032 0 1 0 66.064 0 33.032 33.032 0 1 0-66.064 0Z" fill="#464655" p-id="1150" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'account-book',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zM648.3 426.8l-87.7 161.1h45.7c5.5 0 10 4.5 10 10v21.3c0 5.5-4.5 10-10 10h-63.4v29.7h63.4c5.5 0 10 4.5 10 10v21.3c0 5.5-4.5 10-10 10h-63.4V752c0 5.5-4.5 10-10 10h-41.3c-5.5 0-10-4.5-10-10v-51.8h-63.1c-5.5 0-10-4.5-10-10v-21.3c0-5.5 4.5-10 10-10h63.1v-29.7h-63.1c-5.5 0-10-4.5-10-10v-21.3c0-5.5 4.5-10 10-10h45.2l-88-161.1c-2.6-4.8-.9-10.9 4-13.6 1.5-.8 3.1-1.2 4.8-1.2h46c3.8 0 7.2 2.1 8.9 5.5l72.9 144.3 73.2-144.3a10 10 0 018.9-5.5h45c5.5 0 10 4.5 10 10 .1 1.7-.3 3.3-1.1 4.8z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zM648.3 426.8l-87.7 161.1h45.7c5.5 0 10 4.5 10 10v21.3c0 5.5-4.5 10-10 10h-63.4v29.7h63.4c5.5 0 10 4.5 10 10v21.3c0 5.5-4.5 10-10 10h-63.4V752c0 5.5-4.5 10-10 10h-41.3c-5.5 0-10-4.5-10-10v-51.8h-63.1c-5.5 0-10-4.5-10-10v-21.3c0-5.5 4.5-10 10-10h63.1v-29.7h-63.1c-5.5 0-10-4.5-10-10v-21.3c0-5.5 4.5-10 10-10h45.2l-88-161.1c-2.6-4.8-.9-10.9 4-13.6 1.5-.8 3.1-1.2 4.8-1.2h46c3.8 0 7.2 2.1 8.9 5.5l72.9 144.3 73.2-144.3a10 10 0 018.9-5.5h45c5.5 0 10 4.5 10 10 .1 1.7-.3 3.3-1.1 4.8z" /></svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'alert',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M512 244c176.18 0 319 142.82 319 319v233a32 32 0 01-32 32H225a32 32 0 01-32-32V563c0-176.18 142.82-319 319-319zM484 68h56a8 8 0 018 8v96a8 8 0 01-8 8h-56a8 8 0 01-8-8V76a8 8 0 018-8zM177.25 191.66a8 8 0 0111.32 0l67.88 67.88a8 8 0 010 11.31l-39.6 39.6a8 8 0 01-11.31 0l-67.88-67.88a8 8 0 010-11.31l39.6-39.6zm669.6 0l39.6 39.6a8 8 0 010 11.3l-67.88 67.9a8 8 0 01-11.32 0l-39.6-39.6a8 8 0 010-11.32l67.89-67.88a8 8 0 0111.31 0zM192 892h640a32 32 0 0132 32v24a8 8 0 01-8 8H168a8 8 0 01-8-8v-24a32 32 0 0132-32zm148-317v253h64V575h-64z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M512 244c176.18 0 319 142.82 319 319v233a32 32 0 01-32 32H225a32 32 0 01-32-32V563c0-176.18 142.82-319 319-319zM484 68h56a8 8 0 018 8v96a8 8 0 01-8 8h-56a8 8 0 01-8-8V76a8 8 0 018-8zM177.25 191.66a8 8 0 0111.32 0l67.88 67.88a8 8 0 010 11.31l-39.6 39.6a8 8 0 01-11.31 0l-67.88-67.88a8 8 0 010-11.31l39.6-39.6zm669.6 0l39.6 39.6a8 8 0 010 11.3l-67.88 67.9a8 8 0 01-11.32 0l-39.6-39.6a8 8 0 010-11.32l67.89-67.88a8 8 0 0111.31 0zM192 892h640a32 32 0 0132 32v24a8 8 0 01-8 8H168a8 8 0 01-8-8v-24a32 32 0 0132-32zm148-317v253h64V575h-64z" /></svg>

After

Width:  |  Height:  |  Size: 598 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'alipay-circle',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M308.6 545.7c-19.8 2-57.1 10.7-77.4 28.6-61 53-24.5 150 99 150 71.8 0 143.5-45.7 199.8-119-80.2-38.9-148.1-66.8-221.4-59.6zm460.5 67c100.1 33.4 154.7 43 166.7 44.8A445.9 445.9 0 00960 512c0-247.4-200.6-448-448-448S64 264.6 64 512s200.6 448 448 448c155.9 0 293.2-79.7 373.5-200.5-75.6-29.8-213.6-85-286.8-120.1-69.9 85.7-160.1 137.8-253.7 137.8-158.4 0-212.1-138.1-137.2-229 16.3-19.8 44.2-38.7 87.3-49.4 67.5-16.5 175 10.3 275.7 43.4 18.1-33.3 33.4-69.9 44.7-108.9H305.1V402h160v-56.2H271.3v-31.3h193.8v-80.1s0-13.5 13.7-13.5H557v93.6h191.7v31.3H557.1V402h156.4c-15 61.1-37.7 117.4-66.2 166.8 47.5 17.1 90.1 33.3 121.8 43.9z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M308.6 545.7c-19.8 2-57.1 10.7-77.4 28.6-61 53-24.5 150 99 150 71.8 0 143.5-45.7 199.8-119-80.2-38.9-148.1-66.8-221.4-59.6zm460.5 67c100.1 33.4 154.7 43 166.7 44.8A445.9 445.9 0 00960 512c0-247.4-200.6-448-448-448S64 264.6 64 512s200.6 448 448 448c155.9 0 293.2-79.7 373.5-200.5-75.6-29.8-213.6-85-286.8-120.1-69.9 85.7-160.1 137.8-253.7 137.8-158.4 0-212.1-138.1-137.2-229 16.3-19.8 44.2-38.7 87.3-49.4 67.5-16.5 175 10.3 275.7 43.4 18.1-33.3 33.4-69.9 44.7-108.9H305.1V402h160v-56.2H271.3v-31.3h193.8v-80.1s0-13.5 13.7-13.5H557v93.6h191.7v31.3H557.1V402h156.4c-15 61.1-37.7 117.4-66.2 166.8 47.5 17.1 90.1 33.3 121.8 43.9z" /></svg>

After

Width:  |  Height:  |  Size: 690 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'alipay-square',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M308.6 545.7c-19.8 2-57.1 10.7-77.4 28.6-61 53-24.5 150 99 150 71.8 0 143.5-45.7 199.8-119-80.2-38.9-148.1-66.8-221.4-59.6zM880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm29.4 663.2S703 689.4 598.7 639.5C528.8 725.2 438.6 777.3 345 777.3c-158.4 0-212.1-138.1-137.2-229 16.3-19.8 44.2-38.7 87.3-49.4 67.5-16.5 175 10.3 275.7 43.4 18.1-33.3 33.4-69.9 44.7-108.9H305.1V402h160v-56.2H271.3v-31.3h193.8v-80.1s0-13.5 13.7-13.5H557v93.6h191.7v31.3H557.1V402h156.4c-15 61.1-37.7 117.4-66.2 166.8 47.5 17.1 90.1 33.3 121.8 43.9 114.3 38.2 140.2 40.2 140.2 40.2v122.3z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M308.6 545.7c-19.8 2-57.1 10.7-77.4 28.6-61 53-24.5 150 99 150 71.8 0 143.5-45.7 199.8-119-80.2-38.9-148.1-66.8-221.4-59.6zM880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm29.4 663.2S703 689.4 598.7 639.5C528.8 725.2 438.6 777.3 345 777.3c-158.4 0-212.1-138.1-137.2-229 16.3-19.8 44.2-38.7 87.3-49.4 67.5-16.5 175 10.3 275.7 43.4 18.1-33.3 33.4-69.9 44.7-108.9H305.1V402h160v-56.2H271.3v-31.3h193.8v-80.1s0-13.5 13.7-13.5H557v93.6h191.7v31.3H557.1V402h156.4c-15 61.1-37.7 117.4-66.2 166.8 47.5 17.1 90.1 33.3 121.8 43.9 114.3 38.2 140.2 40.2 140.2 40.2v122.3z" /></svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'aliwangwang',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M868.2 377.4c-18.9-45.1-46.3-85.6-81.2-120.6a377.26 377.26 0 00-120.5-81.2A375.65 375.65 0 00519 145.8c-41.9 0-82.9 6.7-121.9 20C306 123.3 200.8 120 170.6 120c-2.2 0-7.4 0-9.4.2-11.9.4-22.8 6.5-29.2 16.4-6.5 9.9-7.7 22.4-3.4 33.5l64.3 161.6a378.59 378.59 0 00-52.8 193.2c0 51.4 10 101 29.8 147.6 18.9 45 46.2 85.6 81.2 120.5 34.7 34.8 75.4 62.1 120.5 81.2C418.3 894 467.9 904 519 904c51.3 0 100.9-10 147.7-29.8 44.9-18.9 85.5-46.3 120.4-81.2 34.7-34.8 62.1-75.4 81.2-120.6a376.5 376.5 0 0029.8-147.6c-.2-51.2-10.1-100.8-29.9-147.4zm-325.2 79c0 20.4-16.6 37.1-37.1 37.1-20.4 0-37.1-16.7-37.1-37.1v-55.1c0-20.4 16.6-37.1 37.1-37.1 20.4 0 37.1 16.6 37.1 37.1v55.1zm175.2 0c0 20.4-16.6 37.1-37.1 37.1S644 476.8 644 456.4v-55.1c0-20.4 16.7-37.1 37.1-37.1 20.4 0 37.1 16.6 37.1 37.1v55.1z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M868.2 377.4c-18.9-45.1-46.3-85.6-81.2-120.6a377.26 377.26 0 00-120.5-81.2A375.65 375.65 0 00519 145.8c-41.9 0-82.9 6.7-121.9 20C306 123.3 200.8 120 170.6 120c-2.2 0-7.4 0-9.4.2-11.9.4-22.8 6.5-29.2 16.4-6.5 9.9-7.7 22.4-3.4 33.5l64.3 161.6a378.59 378.59 0 00-52.8 193.2c0 51.4 10 101 29.8 147.6 18.9 45 46.2 85.6 81.2 120.5 34.7 34.8 75.4 62.1 120.5 81.2C418.3 894 467.9 904 519 904c51.3 0 100.9-10 147.7-29.8 44.9-18.9 85.5-46.3 120.4-81.2 34.7-34.8 62.1-75.4 81.2-120.6a376.5 376.5 0 0029.8-147.6c-.2-51.2-10.1-100.8-29.9-147.4zm-325.2 79c0 20.4-16.6 37.1-37.1 37.1-20.4 0-37.1-16.7-37.1-37.1v-55.1c0-20.4 16.6-37.1 37.1-37.1 20.4 0 37.1 16.6 37.1 37.1v55.1zm175.2 0c0 20.4-16.6 37.1-37.1 37.1S644 476.8 644 456.4v-55.1c0-20.4 16.7-37.1 37.1-37.1 20.4 0 37.1 16.6 37.1 37.1v55.1z" /></svg>

After

Width:  |  Height:  |  Size: 848 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'amazon-circle',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M485 467.5c-11.6 4.9-20.9 12.2-27.8 22-6.9 9.8-10.4 21.6-10.4 35.5 0 17.8 7.5 31.5 22.4 41.2 14.1 9.1 28.9 11.4 44.4 6.8 17.9-5.2 30-17.9 36.4-38.1 3-9.3 4.5-19.7 4.5-31.3v-50.2c-12.6.4-24.4 1.6-35.5 3.7-11.1 2.1-22.4 5.6-34 10.4zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm35.8 262.7c-7.2-10.9-20.1-16.4-38.7-16.4-1.3 0-3 .1-5.3.3-2.2.2-6.6 1.5-12.9 3.7a79.4 79.4 0 00-17.9 9.1c-5.5 3.8-11.5 10-18 18.4-6.4 8.5-11.5 18.4-15.3 29.8l-94-8.4c0-12.4 2.4-24.7 7-36.9 4.7-12.2 11.8-23.9 21.4-35 9.6-11.2 21.1-21 34.5-29.4 13.4-8.5 29.6-15.2 48.4-20.3 18.9-5.1 39.1-7.6 60.9-7.6 21.3 0 40.6 2.6 57.8 7.7 17.2 5.2 31.1 11.5 41.4 19.1a117 117 0 0125.9 25.7c6.9 9.6 11.7 18.5 14.4 26.7 2.7 8.2 4 15.7 4 22.8v182.5c0 6.4 1.4 13 4.3 19.8 2.9 6.8 6.3 12.8 10.2 18 3.9 5.2 7.9 9.9 12 14.3 4.1 4.3 7.6 7.7 10.6 9.9l4.1 3.4-72.5 69.4c-8.5-7.7-16.9-15.4-25.2-23.4-8.3-8-14.5-14-18.5-18.1l-6.1-6.2c-2.4-2.3-5-5.7-8-10.2-8.1 12.2-18.5 22.8-31.1 31.8-12.7 9-26.3 15.6-40.7 19.7-14.5 4.1-29.4 6.5-44.7 7.1-15.3.6-30-1.5-43.9-6.5-13.9-5-26.5-11.7-37.6-20.3-11.1-8.6-19.9-20.2-26.5-35-6.6-14.8-9.9-31.5-9.9-50.4 0-17.4 3-33.3 8.9-47.7 6-14.5 13.6-26.5 23-36.1 9.4-9.6 20.7-18.2 34-25.7s26.4-13.4 39.2-17.7c12.8-4.2 26.6-7.8 41.5-10.7 14.9-2.9 27.6-4.8 38.2-5.7 10.6-.9 21.2-1.6 31.8-2v-39.4c0-13.5-2.3-23.5-6.7-30.1zm180.5 379.6c-2.8 3.3-7.5 7.8-14.1 13.5s-16.8 12.7-30.5 21.1c-13.7 8.4-28.8 16-45 22.9-16.3 6.9-36.3 12.9-60.1 18-23.7 5.1-48.2 7.6-73.3 7.6-25.4 0-50.7-3.2-76.1-9.6-25.4-6.4-47.6-14.3-66.8-23.7-19.1-9.4-37.6-20.2-55.1-32.2-17.6-12.1-31.7-22.9-42.4-32.5-10.6-9.6-19.6-18.7-26.8-27.1-1.7-1.9-2.8-3.6-3.2-5.1-.4-1.5-.3-2.8.3-3.7.6-.9 1.5-1.6 2.6-2.2a7.42 7.42 0 017.4.8c40.9 24.2 72.9 41.3 95.9 51.4 82.9 36.4 168 45.7 255.3 27.9 40.5-8.3 82.1-22.2 124.9-41.8 3.2-1.2 6-1.5 8.3-.9 2.3.6 3.5 2.4 3.5 5.4 0 2.8-1.6 6.3-4.8 10.2zm59.9-29c-1.8 11.1-4.9 21.6-9.1 31.8-7.2 17.1-16.3 30-27.1 38.4-3.6 2.9-6.4 3.8-8.3 2.8-1.9-1-1.9-3.5 0-7.4 4.5-9.3 9.2-21.8 14.2-37.7 5-15.8 5.7-26 2.1-30.5-1.1-1.5-2.7-2.6-5-3.6-2.2-.9-5.1-1.5-8.6-1.9s-6.7-.6-9.4-.8c-2.8-.2-6.5-.2-11.2 0-4.7.2-8 .4-10.1.6a874.4 874.4 0 01-17.1 1.5c-1.3.2-2.7.4-4.1.5-1.5.1-2.7.2-3.5.3l-2.7.3c-1 .1-1.7.2-2.2.2h-3.2l-1-.2-.6-.5-.5-.9c-1.3-3.3 3.7-7.4 15-12.4s22.3-8.1 32.9-9.3c9.8-1.5 21.3-1.5 34.5-.3s21.3 3.7 24.3 7.4c2.3 3.5 2.5 10.7.7 21.7z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M485 467.5c-11.6 4.9-20.9 12.2-27.8 22-6.9 9.8-10.4 21.6-10.4 35.5 0 17.8 7.5 31.5 22.4 41.2 14.1 9.1 28.9 11.4 44.4 6.8 17.9-5.2 30-17.9 36.4-38.1 3-9.3 4.5-19.7 4.5-31.3v-50.2c-12.6.4-24.4 1.6-35.5 3.7-11.1 2.1-22.4 5.6-34 10.4zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm35.8 262.7c-7.2-10.9-20.1-16.4-38.7-16.4-1.3 0-3 .1-5.3.3-2.2.2-6.6 1.5-12.9 3.7a79.4 79.4 0 00-17.9 9.1c-5.5 3.8-11.5 10-18 18.4-6.4 8.5-11.5 18.4-15.3 29.8l-94-8.4c0-12.4 2.4-24.7 7-36.9 4.7-12.2 11.8-23.9 21.4-35 9.6-11.2 21.1-21 34.5-29.4 13.4-8.5 29.6-15.2 48.4-20.3 18.9-5.1 39.1-7.6 60.9-7.6 21.3 0 40.6 2.6 57.8 7.7 17.2 5.2 31.1 11.5 41.4 19.1a117 117 0 0125.9 25.7c6.9 9.6 11.7 18.5 14.4 26.7 2.7 8.2 4 15.7 4 22.8v182.5c0 6.4 1.4 13 4.3 19.8 2.9 6.8 6.3 12.8 10.2 18 3.9 5.2 7.9 9.9 12 14.3 4.1 4.3 7.6 7.7 10.6 9.9l4.1 3.4-72.5 69.4c-8.5-7.7-16.9-15.4-25.2-23.4-8.3-8-14.5-14-18.5-18.1l-6.1-6.2c-2.4-2.3-5-5.7-8-10.2-8.1 12.2-18.5 22.8-31.1 31.8-12.7 9-26.3 15.6-40.7 19.7-14.5 4.1-29.4 6.5-44.7 7.1-15.3.6-30-1.5-43.9-6.5-13.9-5-26.5-11.7-37.6-20.3-11.1-8.6-19.9-20.2-26.5-35-6.6-14.8-9.9-31.5-9.9-50.4 0-17.4 3-33.3 8.9-47.7 6-14.5 13.6-26.5 23-36.1 9.4-9.6 20.7-18.2 34-25.7s26.4-13.4 39.2-17.7c12.8-4.2 26.6-7.8 41.5-10.7 14.9-2.9 27.6-4.8 38.2-5.7 10.6-.9 21.2-1.6 31.8-2v-39.4c0-13.5-2.3-23.5-6.7-30.1zm180.5 379.6c-2.8 3.3-7.5 7.8-14.1 13.5s-16.8 12.7-30.5 21.1c-13.7 8.4-28.8 16-45 22.9-16.3 6.9-36.3 12.9-60.1 18-23.7 5.1-48.2 7.6-73.3 7.6-25.4 0-50.7-3.2-76.1-9.6-25.4-6.4-47.6-14.3-66.8-23.7-19.1-9.4-37.6-20.2-55.1-32.2-17.6-12.1-31.7-22.9-42.4-32.5-10.6-9.6-19.6-18.7-26.8-27.1-1.7-1.9-2.8-3.6-3.2-5.1-.4-1.5-.3-2.8.3-3.7.6-.9 1.5-1.6 2.6-2.2a7.42 7.42 0 017.4.8c40.9 24.2 72.9 41.3 95.9 51.4 82.9 36.4 168 45.7 255.3 27.9 40.5-8.3 82.1-22.2 124.9-41.8 3.2-1.2 6-1.5 8.3-.9 2.3.6 3.5 2.4 3.5 5.4 0 2.8-1.6 6.3-4.8 10.2zm59.9-29c-1.8 11.1-4.9 21.6-9.1 31.8-7.2 17.1-16.3 30-27.1 38.4-3.6 2.9-6.4 3.8-8.3 2.8-1.9-1-1.9-3.5 0-7.4 4.5-9.3 9.2-21.8 14.2-37.7 5-15.8 5.7-26 2.1-30.5-1.1-1.5-2.7-2.6-5-3.6-2.2-.9-5.1-1.5-8.6-1.9s-6.7-.6-9.4-.8c-2.8-.2-6.5-.2-11.2 0-4.7.2-8 .4-10.1.6a874.4 874.4 0 01-17.1 1.5c-1.3.2-2.7.4-4.1.5-1.5.1-2.7.2-3.5.3l-2.7.3c-1 .1-1.7.2-2.2.2h-3.2l-1-.2-.6-.5-.5-.9c-1.3-3.3 3.7-7.4 15-12.4s22.3-8.1 32.9-9.3c9.8-1.5 21.3-1.5 34.5-.3s21.3 3.7 24.3 7.4c2.3 3.5 2.5 10.7.7 21.7z" /></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'amazon-square',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zM547.8 326.7c-7.2-10.9-20.1-16.4-38.7-16.4-1.3 0-3 .1-5.3.3-2.2.2-6.6 1.5-12.9 3.7a79.4 79.4 0 00-17.9 9.1c-5.5 3.8-11.5 10-18 18.4-6.4 8.5-11.5 18.4-15.3 29.8l-94-8.4c0-12.4 2.4-24.7 7-36.9s11.8-23.9 21.4-35c9.6-11.2 21.1-21 34.5-29.4 13.4-8.5 29.6-15.2 48.4-20.3 18.9-5.1 39.1-7.6 60.9-7.6 21.3 0 40.6 2.6 57.8 7.7 17.2 5.2 31.1 11.5 41.4 19.1a117 117 0 0125.9 25.7c6.9 9.6 11.7 18.5 14.4 26.7 2.7 8.2 4 15.7 4 22.8v182.5c0 6.4 1.4 13 4.3 19.8 2.9 6.8 6.3 12.8 10.2 18 3.9 5.2 7.9 9.9 12 14.3 4.1 4.3 7.6 7.7 10.6 9.9l4.1 3.4-72.5 69.4c-8.5-7.7-16.9-15.4-25.2-23.4-8.3-8-14.5-14-18.5-18.1l-6.1-6.2c-2.4-2.3-5-5.7-8-10.2-8.1 12.2-18.5 22.8-31.1 31.8-12.7 9-26.3 15.6-40.7 19.7-14.5 4.1-29.4 6.5-44.7 7.1-15.3.6-30-1.5-43.9-6.5-13.9-5-26.5-11.7-37.6-20.3-11.1-8.6-19.9-20.2-26.5-35-6.6-14.8-9.9-31.5-9.9-50.4 0-17.4 3-33.3 8.9-47.7 6-14.5 13.6-26.5 23-36.1 9.4-9.6 20.7-18.2 34-25.7s26.4-13.4 39.2-17.7c12.8-4.2 26.6-7.8 41.5-10.7 14.9-2.9 27.6-4.8 38.2-5.7 10.6-.9 21.2-1.6 31.8-2v-39.4c0-13.5-2.3-23.5-6.7-30.1zm180.5 379.6c-2.8 3.3-7.5 7.8-14.1 13.5s-16.8 12.7-30.5 21.1c-13.7 8.4-28.8 16-45 22.9-16.3 6.9-36.3 12.9-60.1 18-23.7 5.1-48.2 7.6-73.3 7.6-25.4 0-50.7-3.2-76.1-9.6-25.4-6.4-47.6-14.3-66.8-23.7-19.1-9.4-37.6-20.2-55.1-32.2-17.6-12.1-31.7-22.9-42.4-32.5-10.6-9.6-19.6-18.7-26.8-27.1-1.7-1.9-2.8-3.6-3.2-5.1-.4-1.5-.3-2.8.3-3.7.6-.9 1.5-1.6 2.6-2.2a7.42 7.42 0 017.4.8c40.9 24.2 72.9 41.3 95.9 51.4 82.9 36.4 168 45.7 255.3 27.9 40.5-8.3 82.1-22.2 124.9-41.8 3.2-1.2 6-1.5 8.3-.9 2.3.6 3.5 2.4 3.5 5.4 0 2.8-1.6 6.3-4.8 10.2zm59.9-29c-1.8 11.1-4.9 21.6-9.1 31.8-7.2 17.1-16.3 30-27.1 38.4-3.6 2.9-6.4 3.8-8.3 2.8-1.9-1-1.9-3.5 0-7.4 4.5-9.3 9.2-21.8 14.2-37.7 5-15.8 5.7-26 2.1-30.5-1.1-1.5-2.7-2.6-5-3.6-2.2-.9-5.1-1.5-8.6-1.9s-6.7-.6-9.4-.8c-2.8-.2-6.5-.2-11.2 0-4.7.2-8 .4-10.1.6a874.4 874.4 0 01-17.1 1.5c-1.3.2-2.7.4-4.1.5-1.5.1-2.7.2-3.5.3l-2.7.3c-1 .1-1.7.2-2.2.2h-3.2l-1-.2-.6-.5-.5-.9c-1.3-3.3 3.7-7.4 15-12.4s22.3-8.1 32.9-9.3c9.8-1.5 21.3-1.5 34.5-.3s21.3 3.7 24.3 7.4c2.3 3.5 2.5 10.7.7 21.7zM485 467.5c-11.6 4.9-20.9 12.2-27.8 22-6.9 9.8-10.4 21.6-10.4 35.5 0 17.8 7.5 31.5 22.4 41.2 14.1 9.1 28.9 11.4 44.4 6.8 17.9-5.2 30-17.9 36.4-38.1 3-9.3 4.5-19.7 4.5-31.3v-50.2c-12.6.4-24.4 1.6-35.5 3.7-11.1 2.1-22.4 5.6-34 10.4z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zM547.8 326.7c-7.2-10.9-20.1-16.4-38.7-16.4-1.3 0-3 .1-5.3.3-2.2.2-6.6 1.5-12.9 3.7a79.4 79.4 0 00-17.9 9.1c-5.5 3.8-11.5 10-18 18.4-6.4 8.5-11.5 18.4-15.3 29.8l-94-8.4c0-12.4 2.4-24.7 7-36.9s11.8-23.9 21.4-35c9.6-11.2 21.1-21 34.5-29.4 13.4-8.5 29.6-15.2 48.4-20.3 18.9-5.1 39.1-7.6 60.9-7.6 21.3 0 40.6 2.6 57.8 7.7 17.2 5.2 31.1 11.5 41.4 19.1a117 117 0 0125.9 25.7c6.9 9.6 11.7 18.5 14.4 26.7 2.7 8.2 4 15.7 4 22.8v182.5c0 6.4 1.4 13 4.3 19.8 2.9 6.8 6.3 12.8 10.2 18 3.9 5.2 7.9 9.9 12 14.3 4.1 4.3 7.6 7.7 10.6 9.9l4.1 3.4-72.5 69.4c-8.5-7.7-16.9-15.4-25.2-23.4-8.3-8-14.5-14-18.5-18.1l-6.1-6.2c-2.4-2.3-5-5.7-8-10.2-8.1 12.2-18.5 22.8-31.1 31.8-12.7 9-26.3 15.6-40.7 19.7-14.5 4.1-29.4 6.5-44.7 7.1-15.3.6-30-1.5-43.9-6.5-13.9-5-26.5-11.7-37.6-20.3-11.1-8.6-19.9-20.2-26.5-35-6.6-14.8-9.9-31.5-9.9-50.4 0-17.4 3-33.3 8.9-47.7 6-14.5 13.6-26.5 23-36.1 9.4-9.6 20.7-18.2 34-25.7s26.4-13.4 39.2-17.7c12.8-4.2 26.6-7.8 41.5-10.7 14.9-2.9 27.6-4.8 38.2-5.7 10.6-.9 21.2-1.6 31.8-2v-39.4c0-13.5-2.3-23.5-6.7-30.1zm180.5 379.6c-2.8 3.3-7.5 7.8-14.1 13.5s-16.8 12.7-30.5 21.1c-13.7 8.4-28.8 16-45 22.9-16.3 6.9-36.3 12.9-60.1 18-23.7 5.1-48.2 7.6-73.3 7.6-25.4 0-50.7-3.2-76.1-9.6-25.4-6.4-47.6-14.3-66.8-23.7-19.1-9.4-37.6-20.2-55.1-32.2-17.6-12.1-31.7-22.9-42.4-32.5-10.6-9.6-19.6-18.7-26.8-27.1-1.7-1.9-2.8-3.6-3.2-5.1-.4-1.5-.3-2.8.3-3.7.6-.9 1.5-1.6 2.6-2.2a7.42 7.42 0 017.4.8c40.9 24.2 72.9 41.3 95.9 51.4 82.9 36.4 168 45.7 255.3 27.9 40.5-8.3 82.1-22.2 124.9-41.8 3.2-1.2 6-1.5 8.3-.9 2.3.6 3.5 2.4 3.5 5.4 0 2.8-1.6 6.3-4.8 10.2zm59.9-29c-1.8 11.1-4.9 21.6-9.1 31.8-7.2 17.1-16.3 30-27.1 38.4-3.6 2.9-6.4 3.8-8.3 2.8-1.9-1-1.9-3.5 0-7.4 4.5-9.3 9.2-21.8 14.2-37.7 5-15.8 5.7-26 2.1-30.5-1.1-1.5-2.7-2.6-5-3.6-2.2-.9-5.1-1.5-8.6-1.9s-6.7-.6-9.4-.8c-2.8-.2-6.5-.2-11.2 0-4.7.2-8 .4-10.1.6a874.4 874.4 0 01-17.1 1.5c-1.3.2-2.7.4-4.1.5-1.5.1-2.7.2-3.5.3l-2.7.3c-1 .1-1.7.2-2.2.2h-3.2l-1-.2-.6-.5-.5-.9c-1.3-3.3 3.7-7.4 15-12.4s22.3-8.1 32.9-9.3c9.8-1.5 21.3-1.5 34.5-.3s21.3 3.7 24.3 7.4c2.3 3.5 2.5 10.7.7 21.7zM485 467.5c-11.6 4.9-20.9 12.2-27.8 22-6.9 9.8-10.4 21.6-10.4 35.5 0 17.8 7.5 31.5 22.4 41.2 14.1 9.1 28.9 11.4 44.4 6.8 17.9-5.2 30-17.9 36.4-38.1 3-9.3 4.5-19.7 4.5-31.3v-50.2c-12.6.4-24.4 1.6-35.5 3.7-11.1 2.1-22.4 5.6-34 10.4z" /></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'android',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M270.1 741.7c0 23.4 19.1 42.5 42.6 42.5h48.7v120.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V784.1h85v120.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V784.1h48.7c23.5 0 42.6-19.1 42.6-42.5V346.4h-486v395.3zm357.1-600.1l44.9-65c2.6-3.8 2-8.9-1.5-11.4-3.5-2.4-8.5-1.2-11.1 2.6l-46.6 67.6c-30.7-12.1-64.9-18.8-100.8-18.8-35.9 0-70.1 6.7-100.8 18.8l-46.6-67.5c-2.6-3.8-7.6-5.1-11.1-2.6-3.5 2.4-4.1 7.4-1.5 11.4l44.9 65c-71.4 33.2-121.4 96.1-127.8 169.6h486c-6.6-73.6-56.7-136.5-128-169.7zM409.5 244.1a26.9 26.9 0 1126.9-26.9 26.97 26.97 0 01-26.9 26.9zm208.4 0a26.9 26.9 0 1126.9-26.9 26.97 26.97 0 01-26.9 26.9zm223.4 100.7c-30.2 0-54.6 24.8-54.6 55.4v216.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V400.1c.1-30.6-24.3-55.3-54.6-55.3zm-658.6 0c-30.2 0-54.6 24.8-54.6 55.4v216.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V400.1c0-30.6-24.5-55.3-54.6-55.3z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M270.1 741.7c0 23.4 19.1 42.5 42.6 42.5h48.7v120.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V784.1h85v120.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V784.1h48.7c23.5 0 42.6-19.1 42.6-42.5V346.4h-486v395.3zm357.1-600.1l44.9-65c2.6-3.8 2-8.9-1.5-11.4-3.5-2.4-8.5-1.2-11.1 2.6l-46.6 67.6c-30.7-12.1-64.9-18.8-100.8-18.8-35.9 0-70.1 6.7-100.8 18.8l-46.6-67.5c-2.6-3.8-7.6-5.1-11.1-2.6-3.5 2.4-4.1 7.4-1.5 11.4l44.9 65c-71.4 33.2-121.4 96.1-127.8 169.6h486c-6.6-73.6-56.7-136.5-128-169.7zM409.5 244.1a26.9 26.9 0 1126.9-26.9 26.97 26.97 0 01-26.9 26.9zm208.4 0a26.9 26.9 0 1126.9-26.9 26.97 26.97 0 01-26.9 26.9zm223.4 100.7c-30.2 0-54.6 24.8-54.6 55.4v216.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V400.1c.1-30.6-24.3-55.3-54.6-55.3zm-658.6 0c-30.2 0-54.6 24.8-54.6 55.4v216.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V400.1c0-30.6-24.5-55.3-54.6-55.3z" /></svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'api',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M917.7 148.8l-42.4-42.4c-1.6-1.6-3.6-2.3-5.7-2.3s-4.1.8-5.7 2.3l-76.1 76.1a199.27 199.27 0 00-112.1-34.3c-51.2 0-102.4 19.5-141.5 58.6L432.3 308.7a8.03 8.03 0 000 11.3L704 591.7c1.6 1.6 3.6 2.3 5.7 2.3 2 0 4.1-.8 5.7-2.3l101.9-101.9c68.9-69 77-175.7 24.3-253.5l76.1-76.1c3.1-3.2 3.1-8.3 0-11.4zM578.9 546.7a8.03 8.03 0 00-11.3 0L501 613.3 410.7 523l66.7-66.7c3.1-3.1 3.1-8.2 0-11.3L441 408.6a8.03 8.03 0 00-11.3 0L363 475.3l-43-43a7.85 7.85 0 00-5.7-2.3c-2 0-4.1.8-5.7 2.3L206.8 534.2c-68.9 68.9-77 175.7-24.3 253.5l-76.1 76.1a8.03 8.03 0 000 11.3l42.4 42.4c1.6 1.6 3.6 2.3 5.7 2.3s4.1-.8 5.7-2.3l76.1-76.1c33.7 22.9 72.9 34.3 112.1 34.3 51.2 0 102.4-19.5 141.5-58.6l101.9-101.9c3.1-3.1 3.1-8.2 0-11.3l-43-43 66.7-66.7c3.1-3.1 3.1-8.2 0-11.3l-36.6-36.2z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M917.7 148.8l-42.4-42.4c-1.6-1.6-3.6-2.3-5.7-2.3s-4.1.8-5.7 2.3l-76.1 76.1a199.27 199.27 0 00-112.1-34.3c-51.2 0-102.4 19.5-141.5 58.6L432.3 308.7a8.03 8.03 0 000 11.3L704 591.7c1.6 1.6 3.6 2.3 5.7 2.3 2 0 4.1-.8 5.7-2.3l101.9-101.9c68.9-69 77-175.7 24.3-253.5l76.1-76.1c3.1-3.2 3.1-8.3 0-11.4zM578.9 546.7a8.03 8.03 0 00-11.3 0L501 613.3 410.7 523l66.7-66.7c3.1-3.1 3.1-8.2 0-11.3L441 408.6a8.03 8.03 0 00-11.3 0L363 475.3l-43-43a7.85 7.85 0 00-5.7-2.3c-2 0-4.1.8-5.7 2.3L206.8 534.2c-68.9 68.9-77 175.7-24.3 253.5l-76.1 76.1a8.03 8.03 0 000 11.3l42.4 42.4c1.6 1.6 3.6 2.3 5.7 2.3s4.1-.8 5.7-2.3l76.1-76.1c33.7 22.9 72.9 34.3 112.1 34.3 51.2 0 102.4-19.5 141.5-58.6l101.9-101.9c3.1-3.1 3.1-8.2 0-11.3l-43-43 66.7-66.7c3.1-3.1 3.1-8.2 0-11.3l-36.6-36.2z" /></svg>

After

Width:  |  Height:  |  Size: 819 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'apple',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M747.4 535.7c-.4-68.2 30.5-119.6 92.9-157.5-34.9-50-87.7-77.5-157.3-82.8-65.9-5.2-138 38.4-164.4 38.4-27.9 0-91.7-36.6-141.9-36.6C273.1 298.8 163 379.8 163 544.6c0 48.7 8.9 99 26.7 150.8 23.8 68.2 109.6 235.3 199.1 232.6 46.8-1.1 79.9-33.2 140.8-33.2 59.1 0 89.7 33.2 141.9 33.2 90.3-1.3 167.9-153.2 190.5-221.6-121.1-57.1-114.6-167.2-114.6-170.7zm-105.1-305c50.7-60.2 46.1-115 44.6-134.7-44.8 2.6-96.6 30.5-126.1 64.8-32.5 36.8-51.6 82.3-47.5 133.6 48.4 3.7 92.6-21.2 129-63.7z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M747.4 535.7c-.4-68.2 30.5-119.6 92.9-157.5-34.9-50-87.7-77.5-157.3-82.8-65.9-5.2-138 38.4-164.4 38.4-27.9 0-91.7-36.6-141.9-36.6C273.1 298.8 163 379.8 163 544.6c0 48.7 8.9 99 26.7 150.8 23.8 68.2 109.6 235.3 199.1 232.6 46.8-1.1 79.9-33.2 140.8-33.2 59.1 0 89.7 33.2 141.9 33.2 90.3-1.3 167.9-153.2 190.5-221.6-121.1-57.1-114.6-167.2-114.6-170.7zm-105.1-305c50.7-60.2 46.1-115 44.6-134.7-44.8 2.6-96.6 30.5-126.1 64.8-32.5 36.8-51.6 82.3-47.5 133.6 48.4 3.7 92.6-21.2 129-63.7z" /></svg>

After

Width:  |  Height:  |  Size: 544 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'appstore',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M864 144H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm0 400H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zM464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm0 400H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M864 144H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm0 400H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zM464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm0 400H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16z" /></svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'audio',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M512 624c93.9 0 170-75.2 170-168V232c0-92.8-76.1-168-170-168s-170 75.2-170 168v224c0 92.8 76.1 168 170 168zm330-170c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 140.3-113.7 254-254 254S258 594.3 258 454c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 168.7 126.6 307.9 290 327.6V884H326.7c-13.7 0-24.7 14.3-24.7 32v36c0 4.4 2.8 8 6.2 8h407.6c3.4 0 6.2-3.6 6.2-8v-36c0-17.7-11-32-24.7-32H548V782.1c165.3-18 294-158 294-328.1z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M512 624c93.9 0 170-75.2 170-168V232c0-92.8-76.1-168-170-168s-170 75.2-170 168v224c0 92.8 76.1 168 170 168zm330-170c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 140.3-113.7 254-254 254S258 594.3 258 454c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 168.7 126.6 307.9 290 327.6V884H326.7c-13.7 0-24.7 14.3-24.7 32v36c0 4.4 2.8 8 6.2 8h407.6c3.4 0 6.2-3.6 6.2-8v-36c0-17.7-11-32-24.7-32H548V782.1c165.3-18 294-158 294-328.1z" /></svg>

After

Width:  |  Height:  |  Size: 475 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'backward',
theme: 'fill',
icon: '<svg viewBox="0 0 1024 1024" focusable="false"><path d="M485.6 249.9L198.2 498c-8.3 7.1-8.3 20.8 0 27.9l287.4 248.2c10.7 9.2 26.4.9 26.4-14V263.8c0-14.8-15.7-23.2-26.4-13.9zm320 0L518.2 498a18.6 18.6 0 00-6.2 14c0 5.2 2.1 10.4 6.2 14l287.4 248.2c10.7 9.2 26.4.9 26.4-14V263.8c0-14.8-15.7-23.2-26.4-13.9z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" focusable="false"><path d="M485.6 249.9L198.2 498c-8.3 7.1-8.3 20.8 0 27.9l287.4 248.2c10.7 9.2 26.4.9 26.4-14V263.8c0-14.8-15.7-23.2-26.4-13.9zm320 0L518.2 498a18.6 18.6 0 00-6.2 14c0 5.2 2.1 10.4 6.2 14l287.4 248.2c10.7 9.2 26.4.9 26.4-14V263.8c0-14.8-15.7-23.2-26.4-13.9z" /></svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'bank',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M894 462c30.9 0 43.8-39.7 18.7-58L530.8 126.2a31.81 31.81 0 00-37.6 0L111.3 404c-25.1 18.2-12.2 58 18.8 58H192v374h-72c-4.4 0-8 3.6-8 8v52c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-52c0-4.4-3.6-8-8-8h-72V462h62zM381 836H264V462h117v374zm189 0H453V462h117v374zm190 0H642V462h118v374z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M894 462c30.9 0 43.8-39.7 18.7-58L530.8 126.2a31.81 31.81 0 00-37.6 0L111.3 404c-25.1 18.2-12.2 58 18.8 58H192v374h-72c-4.4 0-8 3.6-8 8v52c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-52c0-4.4-3.6-8-8-8h-72V462h62zM381 836H264V462h117v374zm189 0H453V462h117v374zm190 0H642V462h118v374z" /></svg>

After

Width:  |  Height:  |  Size: 343 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'behance-circle',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M420.3 470.3c8.7-6.3 12.9-16.7 12.9-31 .3-6.8-1.1-13.5-4.1-19.6-2.7-4.9-6.7-9-11.6-11.9a44.8 44.8 0 00-16.6-6c-6.4-1.2-12.9-1.8-19.3-1.7h-70.3v79.7h76.1c13.1.1 24.2-3.1 32.9-9.5zm11.8 72c-9.8-7.5-22.9-11.2-39.2-11.2h-81.8v94h80.2c7.5 0 14.4-.7 21.1-2.1a50.5 50.5 0 0017.8-7.2c5.1-3.3 9.2-7.8 12.3-13.6 3-5.8 4.5-13.2 4.5-22.1 0-17.7-5-30.2-14.9-37.8zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm86.5 286.9h138.4v33.7H598.5v-33.7zM512 628.8a89.52 89.52 0 01-27 31c-11.8 8.2-24.9 14.2-38.8 17.7a167.4 167.4 0 01-44.6 5.7H236V342.1h161c16.3 0 31.1 1.5 44.6 4.3 13.4 2.8 24.8 7.6 34.4 14.1 9.5 6.5 17 15.2 22.3 26 5.2 10.7 7.9 24.1 7.9 40 0 17.2-3.9 31.4-11.7 42.9-7.9 11.5-19.3 20.8-34.8 28.1 21.1 6 36.6 16.7 46.8 31.7 10.4 15.2 15.5 33.4 15.5 54.8 0 17.4-3.3 32.3-10 44.8zM790.8 576H612.4c0 19.4 6.7 38 16.8 48 10.2 9.9 24.8 14.9 43.9 14.9 13.8 0 25.5-3.5 35.5-10.4 9.9-6.9 15.9-14.2 18.1-21.8h59.8c-9.6 29.7-24.2 50.9-44 63.7-19.6 12.8-43.6 19.2-71.5 19.2-19.5 0-37-3.2-52.7-9.3-15.1-5.9-28.7-14.9-39.9-26.5a121.2 121.2 0 01-25.1-41.2c-6.1-16.9-9.1-34.7-8.9-52.6 0-18.5 3.1-35.7 9.1-51.7 11.5-31.1 35.4-56 65.9-68.9 16.3-6.8 33.8-10.2 51.5-10 21 0 39.2 4 55 12.2a111.6 111.6 0 0138.6 32.8c10.1 13.7 17.2 29.3 21.7 46.9 4.3 17.3 5.8 35.5 4.6 54.7zm-122-95.6c-10.8 0-19.9 1.9-26.9 5.6-7 3.7-12.8 8.3-17.2 13.6a48.4 48.4 0 00-9.1 17.4c-1.6 5.3-2.7 10.7-3.1 16.2H723c-1.6-17.3-7.6-30.1-15.6-39.1-8.4-8.9-21.9-13.7-38.6-13.7z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M420.3 470.3c8.7-6.3 12.9-16.7 12.9-31 .3-6.8-1.1-13.5-4.1-19.6-2.7-4.9-6.7-9-11.6-11.9a44.8 44.8 0 00-16.6-6c-6.4-1.2-12.9-1.8-19.3-1.7h-70.3v79.7h76.1c13.1.1 24.2-3.1 32.9-9.5zm11.8 72c-9.8-7.5-22.9-11.2-39.2-11.2h-81.8v94h80.2c7.5 0 14.4-.7 21.1-2.1a50.5 50.5 0 0017.8-7.2c5.1-3.3 9.2-7.8 12.3-13.6 3-5.8 4.5-13.2 4.5-22.1 0-17.7-5-30.2-14.9-37.8zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm86.5 286.9h138.4v33.7H598.5v-33.7zM512 628.8a89.52 89.52 0 01-27 31c-11.8 8.2-24.9 14.2-38.8 17.7a167.4 167.4 0 01-44.6 5.7H236V342.1h161c16.3 0 31.1 1.5 44.6 4.3 13.4 2.8 24.8 7.6 34.4 14.1 9.5 6.5 17 15.2 22.3 26 5.2 10.7 7.9 24.1 7.9 40 0 17.2-3.9 31.4-11.7 42.9-7.9 11.5-19.3 20.8-34.8 28.1 21.1 6 36.6 16.7 46.8 31.7 10.4 15.2 15.5 33.4 15.5 54.8 0 17.4-3.3 32.3-10 44.8zM790.8 576H612.4c0 19.4 6.7 38 16.8 48 10.2 9.9 24.8 14.9 43.9 14.9 13.8 0 25.5-3.5 35.5-10.4 9.9-6.9 15.9-14.2 18.1-21.8h59.8c-9.6 29.7-24.2 50.9-44 63.7-19.6 12.8-43.6 19.2-71.5 19.2-19.5 0-37-3.2-52.7-9.3-15.1-5.9-28.7-14.9-39.9-26.5a121.2 121.2 0 01-25.1-41.2c-6.1-16.9-9.1-34.7-8.9-52.6 0-18.5 3.1-35.7 9.1-51.7 11.5-31.1 35.4-56 65.9-68.9 16.3-6.8 33.8-10.2 51.5-10 21 0 39.2 4 55 12.2a111.6 111.6 0 0138.6 32.8c10.1 13.7 17.2 29.3 21.7 46.9 4.3 17.3 5.8 35.5 4.6 54.7zm-122-95.6c-10.8 0-19.9 1.9-26.9 5.6-7 3.7-12.8 8.3-17.2 13.6a48.4 48.4 0 00-9.1 17.4c-1.6 5.3-2.7 10.7-3.1 16.2H723c-1.6-17.3-7.6-30.1-15.6-39.1-8.4-8.9-21.9-13.7-38.6-13.7z" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'behance-square',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zM598.5 350.9h138.4v33.7H598.5v-33.7zM512 628.8a89.52 89.52 0 01-27 31c-11.8 8.2-24.9 14.2-38.8 17.7a167.4 167.4 0 01-44.6 5.7H236V342.1h161c16.3 0 31.1 1.5 44.6 4.3 13.4 2.8 24.8 7.6 34.4 14.1 9.5 6.5 17 15.2 22.3 26 5.2 10.7 7.9 24.1 7.9 40 0 17.2-3.9 31.4-11.7 42.9-7.9 11.5-19.3 20.8-34.8 28.1 21.1 6 36.6 16.7 46.8 31.7 10.4 15.2 15.5 33.4 15.5 54.8 0 17.4-3.3 32.3-10 44.8zM790.8 576H612.4c0 19.4 6.7 38 16.8 48 10.2 9.9 24.8 14.9 43.9 14.9 13.8 0 25.5-3.5 35.5-10.4 9.9-6.9 15.9-14.2 18.1-21.8h59.8c-9.6 29.7-24.2 50.9-44 63.7-19.6 12.8-43.6 19.2-71.5 19.2-19.5 0-37-3.2-52.7-9.3-15.1-5.9-28.7-14.9-39.9-26.5a121.2 121.2 0 01-25.1-41.2c-6.1-16.9-9.1-34.7-8.9-52.6 0-18.5 3.1-35.7 9.1-51.7 11.5-31.1 35.4-56 65.9-68.9 16.3-6.8 33.8-10.2 51.5-10 21 0 39.2 4 55 12.2a111.6 111.6 0 0138.6 32.8c10.1 13.7 17.2 29.3 21.7 46.9 4.3 17.3 5.8 35.5 4.6 54.7zm-122-95.6c-10.8 0-19.9 1.9-26.9 5.6-7 3.7-12.8 8.3-17.2 13.6a48.4 48.4 0 00-9.1 17.4c-1.6 5.3-2.7 10.7-3.1 16.2H723c-1.6-17.3-7.6-30.1-15.6-39.1-8.4-8.9-21.9-13.7-38.6-13.7zm-248.5-10.1c8.7-6.3 12.9-16.7 12.9-31 .3-6.8-1.1-13.5-4.1-19.6-2.7-4.9-6.7-9-11.6-11.9a44.8 44.8 0 00-16.6-6c-6.4-1.2-12.9-1.8-19.3-1.7h-70.3v79.7h76.1c13.1.1 24.2-3.1 32.9-9.5zm11.8 72c-9.8-7.5-22.9-11.2-39.2-11.2h-81.8v94h80.2c7.5 0 14.4-.7 21.1-2.1s12.7-3.8 17.8-7.2c5.1-3.3 9.2-7.8 12.3-13.6 3-5.8 4.5-13.2 4.5-22.1 0-17.7-5-30.2-14.9-37.8z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zM598.5 350.9h138.4v33.7H598.5v-33.7zM512 628.8a89.52 89.52 0 01-27 31c-11.8 8.2-24.9 14.2-38.8 17.7a167.4 167.4 0 01-44.6 5.7H236V342.1h161c16.3 0 31.1 1.5 44.6 4.3 13.4 2.8 24.8 7.6 34.4 14.1 9.5 6.5 17 15.2 22.3 26 5.2 10.7 7.9 24.1 7.9 40 0 17.2-3.9 31.4-11.7 42.9-7.9 11.5-19.3 20.8-34.8 28.1 21.1 6 36.6 16.7 46.8 31.7 10.4 15.2 15.5 33.4 15.5 54.8 0 17.4-3.3 32.3-10 44.8zM790.8 576H612.4c0 19.4 6.7 38 16.8 48 10.2 9.9 24.8 14.9 43.9 14.9 13.8 0 25.5-3.5 35.5-10.4 9.9-6.9 15.9-14.2 18.1-21.8h59.8c-9.6 29.7-24.2 50.9-44 63.7-19.6 12.8-43.6 19.2-71.5 19.2-19.5 0-37-3.2-52.7-9.3-15.1-5.9-28.7-14.9-39.9-26.5a121.2 121.2 0 01-25.1-41.2c-6.1-16.9-9.1-34.7-8.9-52.6 0-18.5 3.1-35.7 9.1-51.7 11.5-31.1 35.4-56 65.9-68.9 16.3-6.8 33.8-10.2 51.5-10 21 0 39.2 4 55 12.2a111.6 111.6 0 0138.6 32.8c10.1 13.7 17.2 29.3 21.7 46.9 4.3 17.3 5.8 35.5 4.6 54.7zm-122-95.6c-10.8 0-19.9 1.9-26.9 5.6-7 3.7-12.8 8.3-17.2 13.6a48.4 48.4 0 00-9.1 17.4c-1.6 5.3-2.7 10.7-3.1 16.2H723c-1.6-17.3-7.6-30.1-15.6-39.1-8.4-8.9-21.9-13.7-38.6-13.7zm-248.5-10.1c8.7-6.3 12.9-16.7 12.9-31 .3-6.8-1.1-13.5-4.1-19.6-2.7-4.9-6.7-9-11.6-11.9a44.8 44.8 0 00-16.6-6c-6.4-1.2-12.9-1.8-19.3-1.7h-70.3v79.7h76.1c13.1.1 24.2-3.1 32.9-9.5zm11.8 72c-9.8-7.5-22.9-11.2-39.2-11.2h-81.8v94h80.2c7.5 0 14.4-.7 21.1-2.1s12.7-3.8 17.8-7.2c5.1-3.3 9.2-7.8 12.3-13.6 3-5.8 4.5-13.2 4.5-22.1 0-17.7-5-30.2-14.9-37.8z" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'bell',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M816 768h-24V428c0-141.1-104.3-257.8-240-277.2V112c0-22.1-17.9-40-40-40s-40 17.9-40 40v38.8C336.3 170.2 232 286.9 232 428v340h-24c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h216c0 61.8 50.2 112 112 112s112-50.2 112-112h216c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM512 888c-26.5 0-48-21.5-48-48h96c0 26.5-21.5 48-48 48z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M816 768h-24V428c0-141.1-104.3-257.8-240-277.2V112c0-22.1-17.9-40-40-40s-40 17.9-40 40v38.8C336.3 170.2 232 286.9 232 428v340h-24c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h216c0 61.8 50.2 112 112 112s112-50.2 112-112h216c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM512 888c-26.5 0-48-21.5-48-48h96c0 26.5-21.5 48-48 48z" /></svg>

After

Width:  |  Height:  |  Size: 382 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'book',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zM668 345.9L621.5 312 572 347.4V124h96v221.9z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zM668 345.9L621.5 312 572 347.4V124h96v221.9z" /></svg>

After

Width:  |  Height:  |  Size: 218 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'box-plot',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M952 224h-52c-4.4 0-8 3.6-8 8v248h-92V304c0-4.4-3.6-8-8-8H448v432h344c4.4 0 8-3.6 8-8V548h92v244c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8V232c0-4.4-3.6-8-8-8zm-728 80v176h-92V232c0-4.4-3.6-8-8-8H72c-4.4 0-8 3.6-8 8v560c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8V548h92v172c0 4.4 3.6 8 8 8h152V296H232c-4.4 0-8 3.6-8 8z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M952 224h-52c-4.4 0-8 3.6-8 8v248h-92V304c0-4.4-3.6-8-8-8H448v432h344c4.4 0 8-3.6 8-8V548h92v244c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8V232c0-4.4-3.6-8-8-8zm-728 80v176h-92V232c0-4.4-3.6-8-8-8H72c-4.4 0-8 3.6-8 8v560c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8V548h92v172c0 4.4 3.6 8 8 8h152V296H232c-4.4 0-8 3.6-8 8z" /></svg>

After

Width:  |  Height:  |  Size: 370 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'bug',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M304 280h416c4.4 0 8-3.6 8-8 0-40-8.8-76.7-25.9-108.1a184.31 184.31 0 00-74-74C596.7 72.8 560 64 520 64h-16c-40 0-76.7 8.8-108.1 25.9a184.31 184.31 0 00-74 74C304.8 195.3 296 232 296 272c0 4.4 3.6 8 8 8z" /><path d="M940 512H792V412c76.8 0 139-62.2 139-139 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8a63 63 0 01-63 63H232a63 63 0 01-63-63c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 76.8 62.2 139 139 139v100H84c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h148v96c0 6.5.2 13 .7 19.3C164.1 728.6 116 796.7 116 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-44.2 23.9-82.9 59.6-103.7a273 273 0 0022.7 49c24.3 41.5 59 76.2 100.5 100.5 28.9 16.9 61 28.8 95.3 34.5 4.4 0 8-3.6 8-8V484c0-4.4 3.6-8 8-8h60c4.4 0 8 3.6 8 8v464.2c0 4.4 3.6 8 8 8 34.3-5.7 66.4-17.6 95.3-34.5a281.38 281.38 0 00123.2-149.5A120.4 120.4 0 01836 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-79.3-48.1-147.4-116.7-176.7.4-6.4.7-12.8.7-19.3v-96h148c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M304 280h416c4.4 0 8-3.6 8-8 0-40-8.8-76.7-25.9-108.1a184.31 184.31 0 00-74-74C596.7 72.8 560 64 520 64h-16c-40 0-76.7 8.8-108.1 25.9a184.31 184.31 0 00-74 74C304.8 195.3 296 232 296 272c0 4.4 3.6 8 8 8z" /><path d="M940 512H792V412c76.8 0 139-62.2 139-139 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8a63 63 0 01-63 63H232a63 63 0 01-63-63c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 76.8 62.2 139 139 139v100H84c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h148v96c0 6.5.2 13 .7 19.3C164.1 728.6 116 796.7 116 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-44.2 23.9-82.9 59.6-103.7a273 273 0 0022.7 49c24.3 41.5 59 76.2 100.5 100.5 28.9 16.9 61 28.8 95.3 34.5 4.4 0 8-3.6 8-8V484c0-4.4 3.6-8 8-8h60c4.4 0 8 3.6 8 8v464.2c0 4.4 3.6 8 8 8 34.3-5.7 66.4-17.6 95.3-34.5a281.38 281.38 0 00123.2-149.5A120.4 120.4 0 01836 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-79.3-48.1-147.4-116.7-176.7.4-6.4.7-12.8.7-19.3v-96h148c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z" /></svg>

After

Width:  |  Height:  |  Size: 988 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'build',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M916 210H376c-17.7 0-32 14.3-32 32v236H108c-17.7 0-32 14.3-32 32v272c0 17.7 14.3 32 32 32h540c17.7 0 32-14.3 32-32V546h236c17.7 0 32-14.3 32-32V242c0-17.7-14.3-32-32-32zM612 746H412V546h200v200zm268-268H680V278h200v200z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M916 210H376c-17.7 0-32 14.3-32 32v236H108c-17.7 0-32 14.3-32 32v272c0 17.7 14.3 32 32 32h540c17.7 0 32-14.3 32-32V546h236c17.7 0 32-14.3 32-32V242c0-17.7-14.3-32-32-32zM612 746H412V546h200v200zm268-268H680V278h200v200z" /></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'bulb',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M348 676.1C250 619.4 184 513.4 184 392c0-181.1 146.9-328 328-328s328 146.9 328 328c0 121.4-66 227.4-164 284.1V792c0 17.7-14.3 32-32 32H380c-17.7 0-32-14.3-32-32V676.1zM392 888h240c4.4 0 8 3.6 8 8v32c0 17.7-14.3 32-32 32H416c-17.7 0-32-14.3-32-32v-32c0-4.4 3.6-8 8-8z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M348 676.1C250 619.4 184 513.4 184 392c0-181.1 146.9-328 328-328s328 146.9 328 328c0 121.4-66 227.4-164 284.1V792c0 17.7-14.3 32-32 32H380c-17.7 0-32-14.3-32-32V676.1zM392 888h240c4.4 0 8 3.6 8 8v32c0 17.7-14.3 32-32 32H416c-17.7 0-32-14.3-32-32v-32c0-4.4 3.6-8 8-8z" /></svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'calculator',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zM440.2 765h-50.8c-2.2 0-4.5-1.1-5.9-2.9L348 718.6l-35.5 43.5a7.38 7.38 0 01-5.9 2.9h-50.8c-6.6 0-10.2-7.9-5.8-13.1l62.7-76.8-61.2-74.9c-4.3-5.2-.7-13.1 5.9-13.1h50.9c2.2 0 4.5 1.1 5.9 2.9l34 41.6 34-41.6c1.5-1.9 3.6-2.9 5.9-2.9h50.8c6.6 0 10.2 7.9 5.9 13.1L383.5 675l62.7 76.8c4.2 5.3.6 13.2-6 13.2zm7.8-382c0 2.2-1.4 4-3.2 4H376v68.7c0 1.9-1.8 3.3-4 3.3h-48c-2.2 0-4-1.4-4-3.2V387h-68.8c-1.8 0-3.2-1.8-3.2-4v-48c0-2.2 1.4-4 3.2-4H320v-68.8c0-1.8 1.8-3.2 4-3.2h48c2.2 0 4 1.4 4 3.2V331h68.7c1.9 0 3.3 1.8 3.3 4v48zm328 369c0 2.2-1.4 4-3.2 4H579.2c-1.8 0-3.2-1.8-3.2-4v-48c0-2.2 1.4-4 3.2-4h193.5c1.9 0 3.3 1.8 3.3 4v48zm0-104c0 2.2-1.4 4-3.2 4H579.2c-1.8 0-3.2-1.8-3.2-4v-48c0-2.2 1.4-4 3.2-4h193.5c1.9 0 3.3 1.8 3.3 4v48zm0-265c0 2.2-1.4 4-3.2 4H579.2c-1.8 0-3.2-1.8-3.2-4v-48c0-2.2 1.4-4 3.2-4h193.5c1.9 0 3.3 1.8 3.3 4v48z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zM440.2 765h-50.8c-2.2 0-4.5-1.1-5.9-2.9L348 718.6l-35.5 43.5a7.38 7.38 0 01-5.9 2.9h-50.8c-6.6 0-10.2-7.9-5.8-13.1l62.7-76.8-61.2-74.9c-4.3-5.2-.7-13.1 5.9-13.1h50.9c2.2 0 4.5 1.1 5.9 2.9l34 41.6 34-41.6c1.5-1.9 3.6-2.9 5.9-2.9h50.8c6.6 0 10.2 7.9 5.9 13.1L383.5 675l62.7 76.8c4.2 5.3.6 13.2-6 13.2zm7.8-382c0 2.2-1.4 4-3.2 4H376v68.7c0 1.9-1.8 3.3-4 3.3h-48c-2.2 0-4-1.4-4-3.2V387h-68.8c-1.8 0-3.2-1.8-3.2-4v-48c0-2.2 1.4-4 3.2-4H320v-68.8c0-1.8 1.8-3.2 4-3.2h48c2.2 0 4 1.4 4 3.2V331h68.7c1.9 0 3.3 1.8 3.3 4v48zm328 369c0 2.2-1.4 4-3.2 4H579.2c-1.8 0-3.2-1.8-3.2-4v-48c0-2.2 1.4-4 3.2-4h193.5c1.9 0 3.3 1.8 3.3 4v48zm0-104c0 2.2-1.4 4-3.2 4H579.2c-1.8 0-3.2-1.8-3.2-4v-48c0-2.2 1.4-4 3.2-4h193.5c1.9 0 3.3 1.8 3.3 4v48zm0-265c0 2.2-1.4 4-3.2 4H579.2c-1.8 0-3.2-1.8-3.2-4v-48c0-2.2 1.4-4 3.2-4h193.5c1.9 0 3.3 1.8 3.3 4v48z" /></svg>

After

Width:  |  Height:  |  Size: 1001 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'calendar',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M112 880c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V460H112v420zm768-696H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v176h800V216c0-17.7-14.3-32-32-32z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M112 880c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V460H112v420zm768-696H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v176h800V216c0-17.7-14.3-32-32-32z" /></svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'camera',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M864 260H728l-32.4-90.8a32.07 32.07 0 00-30.2-21.2H358.6c-13.5 0-25.6 8.5-30.1 21.2L296 260H160c-44.2 0-80 35.8-80 80v456c0 44.2 35.8 80 80 80h704c44.2 0 80-35.8 80-80V340c0-44.2-35.8-80-80-80zM512 716c-88.4 0-160-71.6-160-160s71.6-160 160-160 160 71.6 160 160-71.6 160-160 160zm-96-160a96 96 0 10192 0 96 96 0 10-192 0z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M864 260H728l-32.4-90.8a32.07 32.07 0 00-30.2-21.2H358.6c-13.5 0-25.6 8.5-30.1 21.2L296 260H160c-44.2 0-80 35.8-80 80v456c0 44.2 35.8 80 80 80h704c44.2 0 80-35.8 80-80V340c0-44.2-35.8-80-80-80zM512 716c-88.4 0-160-71.6-160-160s71.6-160 160-160 160 71.6 160 160-71.6 160-160 160zm-96-160a96 96 0 10192 0 96 96 0 10-192 0z" /></svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'car',
theme: 'fill',
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M959 413.4L935.3 372a8 8 0 00-10.9-2.9l-50.7 29.6-78.3-216.2a63.9 63.9 0 00-60.9-44.4H301.2c-34.7 0-65.5 22.4-76.2 55.5l-74.6 205.2-50.8-29.6a8 8 0 00-10.9 2.9L65 413.4c-2.2 3.8-.9 8.6 2.9 10.8l60.4 35.2-14.5 40c-1.2 3.2-1.8 6.6-1.8 10v348.2c0 15.7 11.8 28.4 26.3 28.4h67.6c12.3 0 23-9.3 25.6-22.3l7.7-37.7h545.6l7.7 37.7c2.7 13 13.3 22.3 25.6 22.3h67.6c14.5 0 26.3-12.7 26.3-28.4V509.4c0-3.4-.6-6.8-1.8-10l-14.5-40 60.3-35.2a8 8 0 003-10.8zM264 621c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40zm388 75c0 4.4-3.6 8-8 8H380c-4.4 0-8-3.6-8-8v-84c0-4.4 3.6-8 8-8h40c4.4 0 8 3.6 8 8v36h168v-36c0-4.4 3.6-8 8-8h40c4.4 0 8 3.6 8 8v84zm108-75c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40zM220 418l72.7-199.9.5-1.3.4-1.3c1.1-3.3 4.1-5.5 7.6-5.5h427.6l75.4 208H220z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="64 64 896 896" focusable="false"><path d="M959 413.4L935.3 372a8 8 0 00-10.9-2.9l-50.7 29.6-78.3-216.2a63.9 63.9 0 00-60.9-44.4H301.2c-34.7 0-65.5 22.4-76.2 55.5l-74.6 205.2-50.8-29.6a8 8 0 00-10.9 2.9L65 413.4c-2.2 3.8-.9 8.6 2.9 10.8l60.4 35.2-14.5 40c-1.2 3.2-1.8 6.6-1.8 10v348.2c0 15.7 11.8 28.4 26.3 28.4h67.6c12.3 0 23-9.3 25.6-22.3l7.7-37.7h545.6l7.7 37.7c2.7 13 13.3 22.3 25.6 22.3h67.6c14.5 0 26.3-12.7 26.3-28.4V509.4c0-3.4-.6-6.8-1.8-10l-14.5-40 60.3-35.2a8 8 0 003-10.8zM264 621c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40zm388 75c0 4.4-3.6 8-8 8H380c-4.4 0-8-3.6-8-8v-84c0-4.4 3.6-8 8-8h40c4.4 0 8 3.6 8 8v36h168v-36c0-4.4 3.6-8 8-8h40c4.4 0 8 3.6 8 8v84zm108-75c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40zM220 418l72.7-199.9.5-1.3.4-1.3c1.1-3.3 4.1-5.5 7.6-5.5h427.6l75.4 208H220z" /></svg>

After

Width:  |  Height:  |  Size: 860 B

View File

@ -0,0 +1,7 @@
(function() {
__ant_icon_load({
name: 'caret-down',
theme: 'fill',
icon: '<svg viewBox="0 0 1024 1024" focusable="false"><path d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z" /></svg>'
});
})()

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" focusable="false"><path d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z" /></svg>

After

Width:  |  Height:  |  Size: 180 B

Some files were not shown because too many files have changed in this diff Show More