Compare commits
479 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92ca2cddad | ||
|
|
3db0d1dfe5 | ||
|
|
57907323e6 | ||
|
|
dbdca44c5f | ||
|
|
fe1dd2201f | ||
|
|
e0ae194cc3 | ||
|
|
6fc5700457 | ||
|
|
c4fdcf86d4 | ||
|
|
3088500c8d | ||
|
|
861f3a3624 | ||
|
|
c55783e4d9 | ||
|
|
955e284d41 | ||
|
|
fc4c47427e | ||
|
|
e2d7563faa | ||
|
|
27d69f7f8d | ||
|
|
a77bb5af44 | ||
|
|
00286261a4 | ||
|
|
0b898dccaa | ||
|
|
a1d9ac4e68 | ||
|
|
4150939e23 | ||
|
|
8f84b7f063 | ||
|
|
04b245ac64 | ||
|
|
12f7e62957 | ||
|
|
9600d310c7 | ||
|
|
dec5a2472a | ||
|
|
13eb7c6ea2 | ||
|
|
2356cfa10a | ||
|
|
3bfaefb3b0 | ||
|
|
78b8c25d96 | ||
|
|
c1d2ff2b96 | ||
|
|
24aee9446a | ||
|
|
2fb094ec31 | ||
|
|
53897c66ee | ||
|
|
ca4e266ae6 | ||
|
|
6612a1e16f | ||
|
|
55ceb65dfb | ||
|
|
6cad3d6afb | ||
|
|
151e1bdb8a | ||
|
|
44a3cfd1ff | ||
|
|
9cbc3028a7 | ||
|
|
8c30730d7b | ||
|
|
acfb870f9d | ||
|
|
3813528f50 | ||
|
|
e3bb014644 | ||
|
|
76a7afde76 | ||
|
|
1184f9f3f5 | ||
|
|
b754f8938f | ||
|
|
6b30ff04b7 | ||
|
|
1c40acca63 | ||
|
|
a5a7a8afaf | ||
|
|
583ac13a37 | ||
|
|
3e58972072 | ||
|
|
f15aa27727 | ||
|
|
2581014dbd | ||
|
|
baaaa1b57e | ||
|
|
160fbb3590 | ||
|
|
6f3253678c | ||
|
|
563ad66243 | ||
|
|
a8d002cc53 | ||
|
|
0615410fa4 | ||
|
|
fc98e065f8 | ||
|
|
66f671ffa0 | ||
|
|
69a35af456 | ||
|
|
e462bd0b4c | ||
|
|
ae6483427f | ||
|
|
ad97677104 | ||
|
|
996d15ef25 | ||
|
|
06de32ffe7 | ||
|
|
dd43074e46 | ||
|
|
93495e13db | ||
|
|
16950edae4 | ||
|
|
4af1203360 | ||
|
|
55b5bd1fd2 | ||
|
|
f0a7cf4ed0 | ||
|
|
62e7412abf | ||
|
|
275bf647d2 | ||
|
|
00af723be9 | ||
|
|
19da577836 | ||
|
|
bf3a2b469b | ||
|
|
bf31bfd099 | ||
|
|
d02fea99f2 | ||
|
|
2404bacb4e | ||
|
|
b6c274c181 | ||
|
|
f9b472aee7 | ||
|
|
45f277741b | ||
|
|
94179f59cd | ||
|
|
c7b550a3e3 | ||
|
|
fd51fd2387 | ||
|
|
23d1798ab6 | ||
|
|
90e81d0d4d | ||
|
|
6a7a19547d | ||
|
|
1550849ee2 | ||
|
|
15116e2197 | ||
|
|
63eda5179b | ||
|
|
d7b1277363 | ||
|
|
337c933b92 | ||
|
|
b01b2cc9c0 | ||
|
|
30069b2f33 | ||
|
|
c5bd57468c | ||
|
|
c050c65675 | ||
|
|
e1bd7e7563 | ||
|
|
cc129f6384 | ||
|
|
e7ea0c0ff0 | ||
|
|
9630d51c4c | ||
|
|
ceb140a4c2 | ||
|
|
fe8410ab98 | ||
|
|
00731cda93 | ||
|
|
c05979cb11 | ||
|
|
6e1a10e45c | ||
|
|
bd74dfdb26 | ||
|
|
b7c2fd3387 | ||
|
|
b65e41ca23 | ||
|
|
ec70eded14 | ||
|
|
dcf9047d82 | ||
|
|
cd85e9f65a | ||
|
|
066fd4fb77 | ||
|
|
9a6bb30e73 | ||
|
|
99d9f27618 | ||
|
|
02ddac6b17 | ||
|
|
017438ee50 | ||
|
|
d938982107 | ||
|
|
bdde1969f7 | ||
|
|
c8eb038190 | ||
|
|
2d90b79f73 | ||
|
|
f39d3baff5 | ||
|
|
84664ee272 | ||
|
|
d603216baf | ||
|
|
522873c7fb | ||
|
|
a6548f9941 | ||
|
|
3843dd88b2 | ||
|
|
baddb4e9d4 | ||
|
|
4aa51b51bd | ||
|
|
725494db7d | ||
|
|
292caa4158 | ||
|
|
29e9656919 | ||
|
|
78f4682efb | ||
|
|
fa090b0b66 | ||
|
|
32b7e9c3c2 | ||
|
|
4d3e069a81 | ||
|
|
3ed658a31c | ||
|
|
efb24798c8 | ||
|
|
e72e9027ef | ||
|
|
17c93fb716 | ||
|
|
a826666ad6 | ||
|
|
c8282cb66f | ||
|
|
592fd3940e | ||
|
|
7e9980b098 | ||
|
|
283ee06034 | ||
|
|
9a00693bb3 | ||
|
|
16906a46cd | ||
|
|
bdf017024a | ||
|
|
58ae1ef426 | ||
|
|
98e6544c25 | ||
|
|
1b57beeea6 | ||
|
|
1625a5f889 | ||
|
|
ae20e7fad7 | ||
|
|
fc594b12e0 | ||
|
|
0d25f32101 | ||
|
|
cfd4522036 | ||
|
|
f638d4aee0 | ||
|
|
b237b78300 | ||
|
|
ed2983c073 | ||
|
|
730227ac45 | ||
|
|
7fb4f41f01 | ||
|
|
d92e013413 | ||
|
|
980fd145d0 | ||
|
|
693734e12a | ||
|
|
cbeae9b40d | ||
|
|
4d0cc2c3b6 | ||
|
|
5dac578389 | ||
|
|
710718bcd6 | ||
|
|
21b4914817 | ||
|
|
77dde29f8d | ||
|
|
20e11c48ff | ||
|
|
dbe47aa223 | ||
|
|
b916553619 | ||
|
|
d752a0260b | ||
|
|
97cf7b1420 | ||
|
|
50e2377efb | ||
|
|
940cfcd9ed | ||
|
|
cfd36cdaf3 | ||
|
|
d3133388d9 | ||
|
|
05bfe68e92 | ||
|
|
d31aa51cf0 | ||
|
|
6d8918f431 | ||
|
|
a5b6856b81 | ||
|
|
68d9d7801a | ||
|
|
f51934d02a | ||
|
|
21a702e716 | ||
|
|
418f799b64 | ||
|
|
185ef743b5 | ||
|
|
54da9b3cf4 | ||
|
|
b5fed447dd | ||
|
|
9b88a21338 | ||
|
|
0c82cd4285 | ||
|
|
22e6c5338a | ||
|
|
8e4e3b5aa9 | ||
|
|
4b57b56189 | ||
|
|
77eab5e80c | ||
|
|
15dac35d31 | ||
|
|
d84c03c93d | ||
|
|
7e417c5bbe | ||
|
|
fe2471553d | ||
|
|
3e2a3cc4fd | ||
|
|
b9830f336e | ||
|
|
782c339c33 | ||
|
|
2c60a7d987 | ||
|
|
0e6f53a343 | ||
|
|
b0a85654ab | ||
|
|
8b6b5d2695 | ||
|
|
1fde087f7b | ||
|
|
82b41c4e81 | ||
|
|
d0f96b6193 | ||
|
|
8a8bbf8bca | ||
|
|
0430988482 | ||
|
|
781ce70210 | ||
|
|
fcca33e908 | ||
|
|
52b74d70c9 | ||
|
|
6d180a671d | ||
|
|
583185911b | ||
|
|
549f79edd0 | ||
|
|
30894ac172 | ||
|
|
62a2c63f9d | ||
|
|
0aef85998f | ||
|
|
3bb215ae94 | ||
|
|
7d9428de14 | ||
|
|
c85c9206f6 | ||
|
|
2b986c16b8 | ||
|
|
a5ae622a30 | ||
|
|
b3a724b22e | ||
|
|
7f96dcd2bf | ||
|
|
534cf1e3c9 | ||
|
|
3ec3f5d23c | ||
|
|
2241b9b936 | ||
|
|
e8fd6a81be | ||
|
|
8b4672b202 | ||
|
|
9734041777 | ||
|
|
e15fe67790 | ||
|
|
937a5996bf | ||
|
|
d6c87e8be7 | ||
|
|
606e29f56b | ||
|
|
45d5ad04eb | ||
|
|
85c4dcb4a5 | ||
|
|
b803857d65 | ||
|
|
8b029410cb | ||
|
|
33cdbc1552 | ||
|
|
67c125faaf | ||
|
|
d21db4b220 | ||
|
|
7bbf0eb566 | ||
|
|
2eeb7a28fe | ||
|
|
6ae39414f4 | ||
|
|
6a22769199 | ||
|
|
8b12886422 | ||
|
|
bde723b9bd | ||
|
|
0f3720296a | ||
|
|
df9c99d596 | ||
|
|
864a7c38b6 | ||
|
|
edadf0b6b5 | ||
|
|
54ba265510 | ||
|
|
dc44327d4c | ||
|
|
309b6449e6 | ||
|
|
7dd5b2fb8d | ||
|
|
f32147658f | ||
|
|
ba511d37ef | ||
|
|
4fa595547f | ||
|
|
72282b180f | ||
|
|
813686d66d | ||
|
|
f03369719d | ||
|
|
c639b012bc | ||
|
|
4144ce96c0 | ||
|
|
38eed8b66e | ||
|
|
0315bfae0c | ||
|
|
305b87265c | ||
|
|
847a790132 | ||
|
|
ab27798283 | ||
|
|
4b521f775c | ||
|
|
0f68ec0677 | ||
|
|
ed4409661f | ||
|
|
789d56b1a4 | ||
|
|
6d6c3ddcb5 | ||
|
|
c4441029f8 | ||
|
|
4285110233 | ||
|
|
4ad33a7ea8 | ||
|
|
b41c800bb0 | ||
|
|
c4592d5ca6 | ||
|
|
5e9dba5d58 | ||
|
|
2cfd140d4a | ||
|
|
13e421bfba | ||
|
|
1933727f89 | ||
|
|
4e16bfcc18 | ||
|
|
398ee831de | ||
|
|
6d01280039 | ||
|
|
8aaa701348 | ||
|
|
733a36571b | ||
|
|
f40ac28781 | ||
|
|
d73e95d2e5 | ||
|
|
770338a68a | ||
|
|
f58dafbde8 | ||
|
|
004712e851 | ||
|
|
4e53ed2cf8 | ||
|
|
92ccad6253 | ||
|
|
74c5e9bb09 | ||
|
|
2724d6b4d3 | ||
|
|
212e144422 | ||
|
|
205a1b82e7 | ||
|
|
44b4604581 | ||
|
|
3d3454b5a4 | ||
|
|
67f1b04b67 | ||
|
|
fd7d299e55 | ||
|
|
ada492f3f0 | ||
|
|
8a4e4fd32b | ||
|
|
86ced2a217 | ||
|
|
c62251dfe9 | ||
|
|
8bf0f5d36e | ||
|
|
a4b6567947 | ||
|
|
6c5c628bbf | ||
|
|
1d6593340d | ||
|
|
0d992d205f | ||
|
|
f82e79efd4 | ||
|
|
5cdb6b6f75 | ||
|
|
7316a022be | ||
|
|
dbd8a29b73 | ||
|
|
d6a5a02d68 | ||
|
|
9eef00b913 | ||
|
|
3b9dd4824b | ||
|
|
902c1ad39e | ||
|
|
b4f6dea97f | ||
|
|
c1c252f54a | ||
|
|
303b5f8847 | ||
|
|
26f55a463b | ||
|
|
0fa2c366dc | ||
|
|
bf1588e414 | ||
|
|
3be0f25dfc | ||
|
|
0e53028922 | ||
|
|
bc458647a3 | ||
|
|
6c5080394a | ||
|
|
30d45ca2c3 | ||
|
|
614aa3184f | ||
|
|
cacd28bd87 | ||
|
|
a3dfe86a04 | ||
|
|
79c65cab63 | ||
|
|
52237b9385 | ||
|
|
b8d22e92ff | ||
|
|
faac0e29b5 | ||
|
|
899afac910 | ||
|
|
7b2cbfefcc | ||
|
|
793e532240 | ||
|
|
68c7bd251e | ||
|
|
656f82df43 | ||
|
|
1571358f13 | ||
|
|
b6326cf36a | ||
|
|
0b7818a9d3 | ||
|
|
b479d6086b | ||
|
|
6d06491ddc | ||
|
|
e15c6d44a1 | ||
|
|
8ab656a097 | ||
|
|
cb8642b9a9 | ||
|
|
1761d398ee | ||
|
|
eb33fd57a8 | ||
|
|
317b0b373a | ||
|
|
563c8d0085 | ||
|
|
80ced70267 | ||
|
|
3a89b43435 | ||
|
|
d117095a5f | ||
|
|
d78c1bf861 | ||
|
|
20da8034a1 | ||
|
|
0d053a3462 | ||
|
|
280e540f4f | ||
|
|
824cfd23ed | ||
|
|
695728df2e | ||
|
|
24deca75d2 | ||
|
|
8a1184f161 | ||
|
|
d61096d1b1 | ||
|
|
3b9d1be002 | ||
|
|
13262f8f10 | ||
|
|
9f05fc4954 | ||
|
|
3fce06ef63 | ||
|
|
3d13f69e5c | ||
|
|
deb19c6223 | ||
|
|
7466127832 | ||
|
|
af982c5fe0 | ||
|
|
b03f0150d8 | ||
|
|
d61ddafb44 | ||
|
|
fd89a197a5 | ||
|
|
31fa29ee62 | ||
|
|
c7e28b2ad6 | ||
|
|
bbc1343079 | ||
|
|
c7d4fb270b | ||
|
|
fcccdee105 | ||
|
|
887072f6c7 | ||
|
|
1932edba21 | ||
|
|
0c15415822 | ||
|
|
b8dc0870b5 | ||
|
|
9d0ad2ae45 | ||
|
|
7278b9f48c | ||
|
|
1aee95492a | ||
|
|
0cff889f4b | ||
|
|
9cd05362ac | ||
|
|
269eccc7ef | ||
|
|
aafd02090b | ||
|
|
e0e43dbfa4 | ||
|
|
37c358a48b | ||
|
|
2b81f7a106 | ||
|
|
cb9b606cb4 | ||
|
|
c1879c6527 | ||
|
|
035c54b2fd | ||
|
|
54b152207c | ||
|
|
034e159442 | ||
|
|
8a2533c182 | ||
|
|
a35cc4cc19 | ||
|
|
75b0cc80e3 | ||
|
|
c39a0bf462 | ||
|
|
10f19366f0 | ||
|
|
36ed9025f8 | ||
|
|
f67acc3fe1 | ||
|
|
dea6418d54 | ||
|
|
adf3f63cfe | ||
|
|
22b1f11577 | ||
|
|
bc9fb48f18 | ||
|
|
0d6e6cc289 | ||
|
|
6d75a50716 | ||
|
|
a8d9760a11 | ||
|
|
2889d3634f | ||
|
|
958e77e240 | ||
|
|
e36a75aa28 | ||
|
|
1d66852134 | ||
|
|
aab9764d33 | ||
|
|
cf1c75c5b3 | ||
|
|
a9c9d743b8 | ||
|
|
0213fda9a4 | ||
|
|
909867e116 | ||
|
|
b75f920201 | ||
|
|
18a2fff07b | ||
|
|
1826dd122d | ||
|
|
2cb19941ee | ||
|
|
00cd4d4b72 | ||
|
|
696cb5f48c | ||
|
|
e13b4f5377 | ||
|
|
4b0bb7f31a | ||
|
|
1f6badb81d | ||
|
|
65acc6aa39 | ||
|
|
44c41ed99f | ||
|
|
301eb4f1dc | ||
|
|
5aa7de8d86 | ||
|
|
28c03918aa | ||
|
|
d14b267305 | ||
|
|
f2f5d36090 | ||
|
|
aec8450c53 | ||
|
|
efc46341ae | ||
|
|
3a69eb4864 | ||
|
|
7044c2bd83 | ||
|
|
74af439deb | ||
|
|
faf7096743 | ||
|
|
6b3f99c8a7 | ||
|
|
23fd528540 | ||
|
|
c701ee002e | ||
|
|
238a755698 | ||
|
|
9cc95fb4c8 | ||
|
|
bc5887eab4 | ||
|
|
01335ad0ca | ||
|
|
18f7bd2bf7 | ||
|
|
d812deccb8 | ||
|
|
c7e5cbf26e | ||
|
|
e230ef0b41 | ||
|
|
5f547e593a | ||
|
|
a1c6caece1 | ||
|
|
0281c2df1c | ||
|
|
88bfc517f7 | ||
|
|
877b6b5764 | ||
|
|
6b73937acd | ||
|
|
95f7790870 | ||
|
|
0a536a518a | ||
|
|
330fa14c8c | ||
|
|
51d4767307 | ||
|
|
e5f12cdacd | ||
|
|
87525419a2 | ||
|
|
d427d32463 | ||
|
|
0881dfdc3f | ||
|
|
a6f369735a |
39
.dockerignore
Normal file
@@ -0,0 +1,39 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnpm-store
|
||||
.npm
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
build
|
||||
target
|
||||
*.log
|
||||
|
||||
# Version control
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE and editor files
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Debug files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Tauri specific
|
||||
src-tauri/target
|
||||
src-tauri/dist
|
||||
41
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# BiliBili-ShadowReplay contribute guide
|
||||
|
||||
## Project Setup
|
||||
|
||||
### MacOS
|
||||
|
||||
项目无需额外配置,直接 `yarn tauri dev` 即可编译运行。
|
||||
|
||||
### Linux
|
||||
|
||||
也无需额外配置。
|
||||
|
||||
### Windows
|
||||
|
||||
Windows 下分为两个版本,分别是 `cpu` 和 `cuda` 版本。区别在于 Whisper 是否使用 GPU 加速。`cpu` 版本使用 CPU 进行推理,`cuda` 版本使用 GPU 进行推理。
|
||||
|
||||
默认运行为 `cpu` 版本,使用 `yarn tauri dev --features cuda` 命令运行 `cuda` 版本。
|
||||
|
||||
在运行前,须要安装以下依赖:
|
||||
|
||||
1. 安装 LLVM 且配置相关环境变量,详情见 [LLVM Windows Setup](https://llvm.org/docs/GettingStarted.html#building-llvm-on-windows);
|
||||
|
||||
2. 安装 CUDA Toolkit,详情见 [CUDA Windows Setup](https://docs.nvidia.com/cuda/cuda-installation-guide-microsoft-windows/index.html);要注意,安装时请勾选 **VisualStudio integration**。
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. error C3688
|
||||
|
||||
构建前配置参数 `/utf-8`:
|
||||
|
||||
```powershell
|
||||
$env:CMAKE_CXX_FLAGS="/utf-8"
|
||||
```
|
||||
|
||||
#### 2. error: 'exists' is unavailable: introduced in macOS 10.15
|
||||
|
||||
配置环境变量 `CMAKE_OSX_DEPLOYMENT_TARGET`,不低于 `13.3`。
|
||||
|
||||
### 3. CUDA arch 错误
|
||||
|
||||
配置环境变量 `CMAKE_CUDA_ARCHITECTURES`,可以参考 Workflows 中的配置。
|
||||
21
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: 提交一个 BUG
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: Xinrea
|
||||
---
|
||||
|
||||
**描述:**
|
||||
简要描述一下这个 BUG 的现象
|
||||
|
||||
**日志和截图:**
|
||||
如果可以的话,请尽量附上相关截图和日志文件(日志是位于安装目录下,名为 bsr.log 的文件)。
|
||||
|
||||
**相关信息:**
|
||||
|
||||
- 程序版本:
|
||||
- 系统类型:
|
||||
|
||||
**其他**
|
||||
任何其他想说的
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: 提交一个新功能的建议
|
||||
title: "[feature]"
|
||||
labels: enhancement
|
||||
assignees: Xinrea
|
||||
|
||||
---
|
||||
|
||||
**遇到的问题:**
|
||||
在使用过程中遇到了什么问题让你想要提出建议
|
||||
|
||||
**想要的功能:**
|
||||
想要怎样的新功能来解决这个问题
|
||||
|
||||
**通过什么方式实现(有思路的话):**
|
||||
如果有相关的实现思路或者是参考,可以在此提供
|
||||
|
||||
**其他:**
|
||||
其他任何想说的话
|
||||
100
.github/workflows/main.yml
vendored
@@ -1,57 +1,103 @@
|
||||
name: Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
- "v*"
|
||||
jobs:
|
||||
release:
|
||||
publish-tauri:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, ubuntu-20.04, windows-latest]
|
||||
include:
|
||||
- platform: "macos-latest" # for Intel based macs.
|
||||
args: "--target x86_64-apple-darwin"
|
||||
- platform: "ubuntu-22.04"
|
||||
args: ""
|
||||
- platform: "windows-latest"
|
||||
args: "--features cuda"
|
||||
features: "cuda"
|
||||
- platform: "windows-latest"
|
||||
args: ""
|
||||
features: "cpu"
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
# You can remove libayatana-appindicator3-dev if you don't use the system tray feature.
|
||||
- name: Set build type
|
||||
id: build_type
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" == *"rc"* ]]; then
|
||||
echo "debug=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "debug=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Rust setup
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: "yarn" # Set this to npm, yarn or pnpm.
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable # Set this to dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
|
||||
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Install CUDA toolkit (Windows CUDA only)
|
||||
if: matrix.platform == 'windows-latest' && matrix.features == 'cuda'
|
||||
uses: Jimver/cuda-toolkit@v0.2.24
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src-tauri -> target'
|
||||
workspaces: "./src-tauri -> target"
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||
- name: Setup ffmpeg
|
||||
if: matrix.platform == 'windows-latest'
|
||||
working-directory: ./
|
||||
shell: pwsh
|
||||
# running script ffmpeg_setup.ps1 to install ffmpeg on windows.
|
||||
# This script is located in the root of the repository.
|
||||
run: ./ffmpeg_setup.ps1
|
||||
|
||||
- name: Install frontend dependencies
|
||||
- name: install frontend dependencies
|
||||
# If you don't have `beforeBuildCommand` configured you may want to build your frontend here too.
|
||||
run: yarn install # Change this to npm, yarn or pnpm.
|
||||
run: yarn install # change this to npm or pnpm depending on which one you use.
|
||||
|
||||
- name: Build the app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
- name: Copy CUDA DLLs (Windows CUDA only)
|
||||
if: matrix.platform == 'windows-latest' && matrix.features == 'cuda'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$cudaPath = "$env:CUDA_PATH\bin"
|
||||
$targetPath = "src-tauri"
|
||||
New-Item -ItemType Directory -Force -Path $targetPath
|
||||
Copy-Item "$cudaPath\cudart64*.dll" -Destination $targetPath
|
||||
Copy-Item "$cudaPath\cublas64*.dll" -Destination $targetPath
|
||||
Copy-Item "$cudaPath\cublasLt64*.dll" -Destination $targetPath
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CMAKE_OSX_DEPLOYMENT_TARGET: "13.3"
|
||||
CMAKE_CUDA_ARCHITECTURES: "75"
|
||||
WHISPER_BACKEND: ${{ matrix.features }}
|
||||
with:
|
||||
tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags.
|
||||
releaseName: 'Bilibili ShadowReplay v__VERSION__' # tauri-action replaces \_\_VERSION\_\_ with the app version.
|
||||
releaseBody: 'See the assets to download and install this version.'
|
||||
tagName: v__VERSION__
|
||||
releaseName: "BiliBili ShadowReplay v__VERSION__"
|
||||
releaseBody: "See the assets to download this version and install."
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
args: ${{ matrix.args }} ${{ matrix.platform == 'windows-latest' && matrix.features == 'cuda' && '--config src-tauri/tauri.windows.cuda.conf.json' || '' }}
|
||||
includeDebug: true
|
||||
|
||||
51
.github/workflows/package.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Docker Build and Push
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,format=long
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
66
.github/workflows/pages.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Deploy VitePress site to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
|
||||
# using the `master` branch as the default branch.
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- docs/**
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Not needed if lastUpdated is not enabled
|
||||
# - uses: pnpm/action-setup@v3 # Uncomment this block if you're using pnpm
|
||||
# with:
|
||||
# version: 9 # Not needed if you've set "packageManager" in package.json
|
||||
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm # or pnpm / yarn
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Install dependencies
|
||||
run: yarn install # or pnpm install / yarn install / bun install
|
||||
- name: Build with VitePress
|
||||
run: yarn run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
14
.gitignore
vendored
@@ -22,3 +22,17 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.zip
|
||||
|
||||
src-tauri/*.exe
|
||||
|
||||
# test files
|
||||
src-tauri/tests/audio/*.srt
|
||||
|
||||
.env
|
||||
|
||||
docs/.vitepress/cache
|
||||
docs/.vitepress/dist
|
||||
|
||||
*.debug.js
|
||||
*.debug.map
|
||||
|
||||
8
.helix/languages.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[[language]]
|
||||
name = "rust"
|
||||
auto-format = true
|
||||
rulers = []
|
||||
|
||||
[[language]]
|
||||
name = "svelte"
|
||||
auto-format = true
|
||||
7
.vscode/extensions.json
vendored
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"svelte.svelte-vscode",
|
||||
"tauri-apps.tauri-vscode",
|
||||
"rust-lang.rust-analyzer"
|
||||
]
|
||||
}
|
||||
86
Dockerfile
Normal file
@@ -0,0 +1,86 @@
|
||||
# Build frontend
|
||||
FROM node:20-bullseye AS frontend-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy package files
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
# Install dependencies with specific flags
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
# Copy source files
|
||||
COPY . .
|
||||
|
||||
# Build frontend
|
||||
RUN yarn build
|
||||
|
||||
# Build Rust backend
|
||||
FROM rust:1.86-slim AS rust-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install required system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
cmake \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
glib-2.0-dev \
|
||||
libclang-dev \
|
||||
g++ \
|
||||
wget \
|
||||
xz-utils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy Rust project files
|
||||
COPY src-tauri/Cargo.toml src-tauri/Cargo.lock ./src-tauri/
|
||||
COPY src-tauri/src ./src-tauri/src
|
||||
COPY src-tauri/crates ./src-tauri/crates
|
||||
|
||||
# Build Rust backend
|
||||
WORKDIR /app/src-tauri
|
||||
RUN rustup component add rustfmt
|
||||
RUN cargo build --no-default-features --features headless --release
|
||||
# Download and install FFmpeg static build
|
||||
RUN wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz \
|
||||
&& tar xf ffmpeg-release-amd64-static.tar.xz \
|
||||
&& mv ffmpeg-*-static/ffmpeg ./ \
|
||||
&& mv ffmpeg-*-static/ffprobe ./ \
|
||||
&& rm -rf ffmpeg-*-static ffmpeg-release-amd64-static.tar.xz
|
||||
|
||||
# Final stage
|
||||
FROM debian:bookworm-slim AS final
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies, SSL certificates and Chinese fonts
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libssl3 \
|
||||
ca-certificates \
|
||||
fonts-wqy-microhei \
|
||||
&& update-ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add /app to PATH
|
||||
ENV PATH="/app:${PATH}"
|
||||
|
||||
# Copy built frontend
|
||||
COPY --from=frontend-builder /app/dist ./dist
|
||||
|
||||
# Copy built Rust binary
|
||||
COPY --from=rust-builder /app/src-tauri/target/release/bili-shadowreplay .
|
||||
COPY --from=rust-builder /app/src-tauri/ffmpeg ./ffmpeg
|
||||
COPY --from=rust-builder /app/src-tauri/ffprobe ./ffprobe
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Run the application
|
||||
CMD ["./bili-shadowreplay"]
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Xinrea
|
||||
|
||||
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.
|
||||
31
README.md
@@ -1,26 +1,27 @@
|
||||
# Bilibili ShadowReplay
|
||||
# BiliBili ShadowReplay
|
||||
|
||||

|
||||

|
||||
|
||||
> 点击关闭后程序仍会在后台运行,请找到托盘区的图标右键退出程序
|
||||

|
||||

|
||||
|
||||
## 介绍
|
||||

|
||||

|
||||
|
||||
Bilibili ShadowReplay 是一个用于缓存B站直播的工具,可以将直播的视频缓存到本地,便于及时保存回放,方便后期剪辑工作。
|
||||
BiliBili ShadowReplay 是一个缓存直播并进行实时编辑投稿的工具。通过划定时间区间,并编辑简单的必需信息,即可完成直播切片以及投稿,将整个流程压缩到分钟级。同时,也支持对缓存的历史直播进行回放,以及相同的切片编辑投稿处理流程。
|
||||
|
||||

|
||||
目前仅支持 B 站和抖音平台的直播。
|
||||
|
||||
除了在界面上手动操作外,还可以通过弹幕触发切片。
|
||||

|
||||
|
||||
> 只有管理员UID设置中的用户才能触发切片
|
||||
## 安装和使用
|
||||
|
||||

|
||||
前往网站查看说明:[BiliBili ShadowReplay](https://bsr.xinrea.cn/)
|
||||
|
||||
## 设置
|
||||
## 参与开发
|
||||
|
||||

|
||||
[Contributing](.github/CONTRIBUTING.md)
|
||||
|
||||
- `缓存时长`:缓存的视频时长,单位为秒
|
||||
- `缓存目录`:缓存的视频存放目录
|
||||
- `切片目录`: 切片的视频存放目录
|
||||
- `管理员UID`:B站的UID,用于判断是否有权限在直播间通过弹幕触发切片;可设置多个,使用英文逗号分隔。
|
||||
## 赞助
|
||||
|
||||

|
||||
|
||||
42
build.ps1
Normal file
@@ -0,0 +1,42 @@
|
||||
# run yarn tauri build
|
||||
|
||||
yarn tauri build
|
||||
yarn tauri build --debug
|
||||
|
||||
# rename the builds, "bili-shadowreplay" to "bili-shadowreplay-cpu"
|
||||
Get-ChildItem -Path ./src-tauri/target/release/bundle/msi/ | ForEach-Object {
|
||||
$newName = $_.Name -replace 'bili-shadowreplay', 'bili-shadowreplay-cpu'
|
||||
Rename-Item -Path $_.FullName -NewName $newName
|
||||
}
|
||||
Get-ChildItem -Path ./src-tauri/target/release/bundle/nsis/ | ForEach-Object {
|
||||
$newName = $_.Name -replace 'bili-shadowreplay', 'bili-shadowreplay-cpu'
|
||||
Rename-Item -Path $_.FullName -NewName $newName
|
||||
}
|
||||
|
||||
# rename the debug builds, "bili-shadowreplay" to "bili-shadowreplay-cpu"
|
||||
Get-ChildItem -Path ./src-tauri/target/debug/bundle/msi/ | ForEach-Object {
|
||||
$newName = $_.Name -replace 'bili-shadowreplay', 'bili-shadowreplay-cpu'
|
||||
Rename-Item -Path $_.FullName -NewName $newName
|
||||
}
|
||||
Get-ChildItem -Path ./src-tauri/target/debug/bundle/nsis/ | ForEach-Object {
|
||||
$newName = $_.Name -replace 'bili-shadowreplay', 'bili-shadowreplay-cpu'
|
||||
Rename-Item -Path $_.FullName -NewName $newName
|
||||
}
|
||||
|
||||
# move the build to the correct location
|
||||
Move-Item ./src-tauri/target/release/bundle/msi/* ./src-tauri/target/
|
||||
Move-Item ./src-tauri/target/release/bundle/nsis/* ./src-tauri/target/
|
||||
|
||||
# rename debug builds to add "-debug" suffix
|
||||
Get-ChildItem -Path ./src-tauri/target/debug/bundle/msi/ | ForEach-Object {
|
||||
$newName = $_.Name -replace '\.msi$', '-debug.msi'
|
||||
Rename-Item -Path $_.FullName -NewName $newName
|
||||
}
|
||||
Get-ChildItem -Path ./src-tauri/target/debug/bundle/nsis/ | ForEach-Object {
|
||||
$newName = $_.Name -replace '\.exe$', '-debug.exe'
|
||||
Rename-Item -Path $_.FullName -NewName $newName
|
||||
}
|
||||
|
||||
# move the debug builds to the correct location
|
||||
Move-Item ./src-tauri/target/debug/bundle/msi/* ./src-tauri/target/
|
||||
Move-Item ./src-tauri/target/debug/bundle/nsis/* ./src-tauri/target/
|
||||
78
cliff.toml
Normal file
@@ -0,0 +1,78 @@
|
||||
# git-cliff ~ default configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
#
|
||||
# Lines starting with "#" are comments.
|
||||
# Configuration options are organized into tables and keys.
|
||||
# See documentation for more information on available options.
|
||||
|
||||
[changelog]
|
||||
# template for the changelog header
|
||||
# header = """"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits %}
|
||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
||||
{% if commit.breaking %}[**breaking**] {% endif %}\
|
||||
{{ commit.message | upper_first }} by @{{ commit.author.name }} - {{ commit.id }}\
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# template for the changelog footer
|
||||
# footer = """"""
|
||||
# remove the leading and trailing s
|
||||
trim = true
|
||||
# postprocessors
|
||||
postprocessors = [
|
||||
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
|
||||
]
|
||||
# render body even when there are no releases to process
|
||||
# render_always = true
|
||||
# output file path
|
||||
# output = "test.md"
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
# Replace issue numbers
|
||||
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
|
||||
# Check spelling of the commit with https://github.com/crate-ci/typos
|
||||
# If the spelling is incorrect, it will be automatically fixed.
|
||||
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(deps.*\\)", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
||||
]
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "oldest"
|
||||
BIN
doc/clip.png
|
Before Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 24 KiB |
BIN
doc/main.png
|
Before Width: | Height: | Size: 127 KiB |
BIN
doc/setting.png
|
Before Width: | Height: | Size: 136 KiB |
43
docs/.vitepress/config.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineConfig } from "vitepress";
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "BiliBili ShadowReplay",
|
||||
description: "直播录制/实时回放/剪辑/投稿工具",
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: "Home", link: "/" },
|
||||
{
|
||||
text: "Releases",
|
||||
link: "https://github.com/Xinrea/bili-shadowreplay/releases",
|
||||
},
|
||||
],
|
||||
|
||||
sidebar: [
|
||||
{
|
||||
text: "开始使用",
|
||||
items: [
|
||||
{ text: "安装准备", link: "/getting-started/installation" },
|
||||
{ text: "配置使用", link: "/getting-started/configuration" },
|
||||
{ text: "FFmpeg 配置", link: "/getting-started/ffmpeg" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "说明文档",
|
||||
items: [
|
||||
{ text: "功能说明", link: "/usage/features" },
|
||||
{ text: "常见问题", link: "/usage/faq" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "开发文档",
|
||||
items: [{ text: "架构设计", link: "/develop/architecture" }],
|
||||
},
|
||||
],
|
||||
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/Xinrea/bili-shadowreplay" },
|
||||
],
|
||||
},
|
||||
});
|
||||
1
docs/develop/architecture.md
Normal file
@@ -0,0 +1 @@
|
||||
# 架构设计
|
||||
57
docs/getting-started/configuration.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 配置使用
|
||||
|
||||
## 账号配置
|
||||
|
||||
要添加直播间,至少需要配置一个同平台的账号。在账号页面,你可以通过添加账号按钮添加一个账号。
|
||||
|
||||
- B 站账号:目前支持扫码登录和 Cookie 手动配置两种方式,推荐使用扫码登录
|
||||
- 抖音账号:目前仅支持 Cookie 手动配置登陆
|
||||
|
||||
### 抖音账号配置
|
||||
|
||||
首先确保已经登录抖音,然后打开[个人主页](https://www.douyin.com/user/self),右键单击网页,在菜单中选择 `检查(Inspect)`,打开开发者工具,切换到 `网络(Network)` 选项卡,然后刷新网页,此时能在列表中找到 `self` 请求(一般是列表中第一个),单击该请求,查看`请求标头`,在 `请求标头` 中找到 `Cookie`,复制该字段的值,粘贴到配置页面的 `Cookie` 输入框中,要注意复制完全。
|
||||
|
||||

|
||||
|
||||
## FFmpeg 配置
|
||||
|
||||
如果想要使用切片生成和压制功能,请确保 FFmpeg 已正确配置;除了 Windows 平台打包自带 FFmpeg 以外,其他平台需要手动安装 FFmpeg,请参考 [FFmpeg 配置](/getting-started/ffmpeg)。
|
||||
|
||||
## Whisper 配置
|
||||
|
||||
要使用 AI 字幕识别功能,需要在设置页面配置 Whisper。目前可以选择使用本地运行 Whisper 模型,或是使用在线的 Whisper 服务(通常需要付费获取 API Key)。
|
||||
|
||||
> [!NOTE]
|
||||
> 其实有许多更好的中文字幕识别解决方案,但是这类服务通常需要将文件上传到对象存储后异步处理,考虑到实现的复杂度,选择了使用本地运行 Whisper 模型或是使用在线的 Whisper 服务,在请求返回时能够直接获取字幕生成结果。
|
||||
|
||||
### 本地运行 Whisper 模型
|
||||
|
||||

|
||||
|
||||
如果需要使用本地运行 Whisper 模型进行字幕生成,需要下载 Whisper.cpp 模型,并在设置中指定模型路径。模型文件可以从网络上下载,例如:
|
||||
|
||||
- [Whisper.cpp(国内镜像,内容较旧)](https://www.modelscope.cn/models/cjc1887415157/whisper.cpp/files)
|
||||
- [Whisper.cpp](https://huggingface.co/ggerganov/whisper.cpp/tree/main)
|
||||
|
||||
可以跟据自己的需求选择不同的模型,要注意带有 `en` 的模型是英文模型,其他模型为多语言模型。
|
||||
|
||||
模型文件的大小通常意味着其在运行时资源占用的大小,因此请根据电脑配置选择合适的模型。此外,GPU 版本与 CPU 版本在字幕生成速度上存在**巨大差异**,因此推荐使用 GPU 版本进行本地处理(目前仅支持 Nvidia GPU)。
|
||||
|
||||
### 使用在线 Whisper 服务
|
||||
|
||||

|
||||
|
||||
如果需要使用在线的 Whisper 服务进行字幕生成,可以在设置中切换为在线 Whisper,并配置好 API Key。提供 Whisper 服务的平台并非只有 OpenAI 一家,许多云服务平台也提供 Whisper 服务。
|
||||
|
||||
### 字幕识别质量的调优
|
||||
|
||||
目前在设置中支持设置 Whisper 语言和 Whisper 提示词,这些设置对于本地和在线的 Whisper 服务都有效。
|
||||
|
||||
通常情况下,`auto` 语言选项能够自动识别语音语言,并生成相应语言的字幕。如果需要生成其他语言的字幕,或是生成的字幕语言不匹配,可以手动配置指定的语言。根据 OpenAI 官方文档中对于 `language` 参数的描述,目前支持的语言包括
|
||||
|
||||
Afrikaans, Arabic, Armenian, Azerbaijani, Belarusian, Bosnian, Bulgarian, Catalan, Chinese, Croatian, Czech, Danish, Dutch, English, Estonian, Finnish, French, Galician, German, Greek, Hebrew, Hindi, Hungarian, Icelandic, Indonesian, Italian, Japanese, Kannada, Kazakh, Korean, Latvian, Lithuanian, Macedonian, Malay, Marathi, Maori, Nepali, Norwegian, Persian, Polish, Portuguese, Romanian, Russian, Serbian, Slovak, Slovenian, Spanish, Swahili, Swedish, Tagalog, Tamil, Thai, Turkish, Ukrainian, Urdu, Vietnamese, and Welsh.
|
||||
|
||||
提示词可以优化生成的字幕的风格(也会一定程度上影响质量),要注意,Whisper 无法理解复杂的提示词,你可以在提示词中使用一些简单的描述,让其在选择词汇时使用偏向于提示词所描述的领域相关的词汇,以避免出现毫不相干领域的词汇;或是让它在标点符号的使用上参照提示词的风格。
|
||||
|
||||
|
||||
|
||||
47
docs/getting-started/ffmpeg.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# FFmpeg 配置
|
||||
|
||||
FFmpeg 是一个开源的音视频处理工具,支持多种格式的音视频编解码、转码、剪辑、合并等操作。
|
||||
在本项目中,FFmpeg 用于切片生成以及字幕和弹幕的硬编码处理,因此需要确保安装了 FFmpeg。
|
||||
|
||||
## MacOS
|
||||
|
||||
在 MacOS 上安装 FFmpeg 非常简单,可以使用 Homebrew 来安装:
|
||||
|
||||
```bash
|
||||
brew install ffmpeg
|
||||
```
|
||||
|
||||
如果没有安装 Homebrew,可以参考 [Homebrew 官网](https://brew.sh/) 进行安装。
|
||||
|
||||
## Linux
|
||||
|
||||
在 Linux 上安装 FFmpeg 可以使用系统自带的包管理器进行安装,例如:
|
||||
|
||||
- Ubuntu/Debian 系统:
|
||||
|
||||
```bash
|
||||
sudo apt install ffmpeg
|
||||
```
|
||||
|
||||
- Fedora 系统:
|
||||
|
||||
```bash
|
||||
sudo dnf install ffmpeg
|
||||
```
|
||||
|
||||
- Arch Linux 系统:
|
||||
|
||||
```bash
|
||||
sudo pacman -S ffmpeg
|
||||
```
|
||||
|
||||
- CentOS 系统:
|
||||
|
||||
```bash
|
||||
sudo yum install epel-release
|
||||
sudo yum install ffmpeg
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
||||
Windows 版本安装后,FFmpeg 已经放置在了程序目录下,因此不需要额外安装。
|
||||
66
docs/getting-started/installation.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# 安装准备
|
||||
|
||||
## 桌面端安装
|
||||
|
||||
桌面端目前提供了 Windows、Linux 和 MacOS 三个平台的安装包。
|
||||
|
||||
安装包分为两个版本,普通版和 debug 版,普通版适合大部分用户使用,debug 版包含了更多的调试信息,适合开发者使用;由于程序会对账号等敏感信息进行管理,请从信任的来源进行下载;所有版本均可在 [GitHub Releases](https://github.com/Xinrea/bili-shadowreplay/releases) 页面下载安装。
|
||||
|
||||
### Windows
|
||||
|
||||
由于程序内置 Whisper 字幕识别模型支持,Windows 版本分为两种:
|
||||
|
||||
- **普通版本**:内置了 Whisper GPU 加速,字幕识别较快,体积较大,只支持 Nvidia 显卡
|
||||
- **CPU 版本**: 使用 CPU 进行字幕识别推理,速度较慢
|
||||
|
||||
请根据自己的显卡情况选择合适的版本进行下载。
|
||||
|
||||
### Linux
|
||||
|
||||
Linux 版本目前仅支持使用 CPU 推理,且测试较少,可能存在一些问题,遇到问题请及时反馈。
|
||||
|
||||
### MacOS
|
||||
|
||||
MacOS 版本内置 Metal GPU 加速;安装后首次运行,会提示无法打开从网络下载的软件,请在设置-隐私与安全性下,选择仍然打开以允许程序运行。
|
||||
|
||||
## Docker 部署
|
||||
|
||||
BiliBili ShadowReplay 提供了服务端部署的能力,提供 Web 控制界面,可以用于在服务器等无图形界面环境下部署使用。
|
||||
|
||||
### 镜像获取
|
||||
|
||||
```bash
|
||||
# 拉取最新版本
|
||||
docker pull ghcr.io/xinrea/bili-shadowreplay:latest
|
||||
# 拉取指定版本
|
||||
docker pull ghcr.io/xinrea/bili-shadowreplay:2.5.0
|
||||
# 速度太慢?从镜像源拉取
|
||||
docker pull ghcr.nju.edu.cn/xinrea/bili-shadowreplay:latest
|
||||
```
|
||||
|
||||
### 镜像使用
|
||||
|
||||
使用方法:
|
||||
|
||||
```bash
|
||||
sudo docker run -it -d\
|
||||
-p 3000:3000 \
|
||||
-v $DATA_DIR:/app/data \
|
||||
-v $CACHE_DIR:/app/cache \
|
||||
-v $OUTPUT_DIR:/app/output \
|
||||
-v $WHISPER_MODEL:/app/whisper_model.bin \
|
||||
--name bili-shadowreplay \
|
||||
ghcr.io/xinrea/bili-shadowreplay:latest
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `$DATA_DIR`:为数据目录,对应于桌面版的数据目录,
|
||||
|
||||
Windows 下位于 `C:\Users\{用户名}\AppData\Roaming\cn.vjoi.bilishadowreplay`;
|
||||
|
||||
MacOS 下位于 `/Users/{user}/Library/Application Support/cn.vjoi.bilishadowreplay`
|
||||
|
||||
- `$CACHE_DIR`:为缓存目录,对应于桌面版的缓存目录;
|
||||
- `$OUTPUT_DIR`:为输出目录,对应于桌面版的输出目录;
|
||||
- `$WHISPER_MODEL`:为 Whisper 模型文件路径,对应于桌面版的 Whisper 模型文件路径。
|
||||
70
docs/index.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "BiliBili ShadowReplay"
|
||||
tagline: "直播录制/实时回放/剪辑/投稿工具"
|
||||
image:
|
||||
src: /images/icon.png
|
||||
alt: BiliBili ShadowReplay
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 开始使用
|
||||
link: /getting-started/installation
|
||||
- theme: alt
|
||||
text: 说明文档
|
||||
link: /usage/features
|
||||
|
||||
features:
|
||||
- icon: 📹
|
||||
title: 直播录制
|
||||
details: 缓存直播流,直播结束自动生成整场录播
|
||||
- icon: 📺
|
||||
title: 实时回放
|
||||
details: 实时回放当前直播,不错过任何内容
|
||||
- icon: ✂️
|
||||
title: 剪辑投稿
|
||||
details: 剪辑切片,封面编辑,一键投稿
|
||||
- icon: 📝
|
||||
title: 字幕生成
|
||||
details: 支持 Wisper 模型生成字幕,编辑与压制
|
||||
- icon: 📄
|
||||
title: 弹幕支持
|
||||
details: 直播间弹幕压制到切片,并支持直播弹幕发送和导出
|
||||
- icon: 🌐
|
||||
title: 多直播平台支持
|
||||
details: 目前支持 B 站和抖音直播
|
||||
- icon: 🔍
|
||||
title: 云端部署
|
||||
details: 支持 Docker 部署,提供 Web 控制界面
|
||||
- icon: 🤖
|
||||
title: AI Agent 支持
|
||||
details: 支持 AI 助手管理录播,分析直播内容,生成切片
|
||||
---
|
||||
|
||||
## 总览
|
||||
|
||||

|
||||
|
||||
## 直播间管理
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 账号管理
|
||||
|
||||

|
||||
|
||||
## 预览窗口
|
||||
|
||||

|
||||
|
||||
## 封面编辑
|
||||
|
||||

|
||||
|
||||
## 设置
|
||||
|
||||

|
||||
BIN
docs/public/images/accounts.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
docs/public/images/ai_agent.png
Normal file
|
After Width: | Height: | Size: 261 KiB |
BIN
docs/public/images/archives.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
BIN
docs/public/images/clip_manage.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
docs/public/images/clip_preview.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
docs/public/images/cover_edit.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/public/images/donate.png
Normal file
|
After Width: | Height: | Size: 474 KiB |
BIN
docs/public/images/douyin_cookie.png
Normal file
|
After Width: | Height: | Size: 548 KiB |
BIN
docs/public/images/header.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
docs/public/images/icon.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/public/images/livewindow.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/public/images/rooms.png
Normal file
|
After Width: | Height: | Size: 949 KiB |
BIN
docs/public/images/settings.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
docs/public/images/summary.png
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
docs/public/images/tasks.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
docs/public/images/whisper_local.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
docs/public/images/whisper_online.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
0
docs/usage/faq.md
Normal file
0
docs/usage/features.md
Normal file
32
ffmpeg_setup.ps1
Normal file
@@ -0,0 +1,32 @@
|
||||
# download ffmpeg from https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip
|
||||
$ffmpegUrl = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
|
||||
$ffmpegPath = "ffmpeg-release-essentials.zip"
|
||||
# download the file if it doesn't exist
|
||||
if (-not (Test-Path $ffmpegPath)) {
|
||||
Invoke-WebRequest -Uri $ffmpegUrl -OutFile $ffmpegPath
|
||||
}
|
||||
# extract the 7z file
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$extractPath = "ffmpeg"
|
||||
# check if the directory exists, if not create it
|
||||
if (-not (Test-Path $extractPath)) {
|
||||
New-Item -ItemType Directory -Path $extractPath
|
||||
}
|
||||
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($ffmpegPath, $extractPath)
|
||||
|
||||
# move the bin directory to the src-tauri directory
|
||||
# ffmpeg/ffmpeg-*-essentials_build/bin to src-tauri
|
||||
$ffmpegDir = Get-ChildItem -Path $extractPath -Directory | Where-Object { $_.Name -match "ffmpeg-.*-essentials_build" }
|
||||
if ($ffmpegDir) {
|
||||
$binPath = Join-Path $ffmpegDir.FullName "bin"
|
||||
} else {
|
||||
Write-Host "No ffmpeg directory found in the extracted files."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$destPath = Join-Path $PSScriptRoot "src-tauri"
|
||||
Copy-Item -Path "$binPath/*" -Destination $destPath -Recurse
|
||||
|
||||
# remove the extracted directory
|
||||
Remove-Item $extractPath -Recurse -Force
|
||||
25
index.html
@@ -1,16 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BiliBili ShadowReplay</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BiliBili ShadowReplay</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
13
index_clip.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>切片窗口</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main_clip.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
63
index_live.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="shaka-player/controls.min.css" />
|
||||
<link rel="stylesheet" href="shaka-player/youtube-theme.css" />
|
||||
<script src="shaka-player/shaka-player.ui.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="src/main_live.ts"></script>
|
||||
<style>
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
width: 12px;
|
||||
/* 设置滑块按钮宽度 */
|
||||
height: 12px;
|
||||
/* 设置滑块按钮高度 */
|
||||
border-radius: 50%;
|
||||
/* 设置为圆形 */
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-face-color: #646464;
|
||||
scrollbar-base-color: #646464;
|
||||
scrollbar-3dlight-color: #646464;
|
||||
scrollbar-highlight-color: #646464;
|
||||
scrollbar-track-color: #000;
|
||||
scrollbar-arrow-color: #000;
|
||||
scrollbar-shadow-color: #646464;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #646464;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
height: 50px;
|
||||
background-color: #666;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: #646464;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
3936
package-lock.json
generated
37
package.json
@@ -1,25 +1,47 @@
|
||||
{
|
||||
"name": "bili-shadowreplay",
|
||||
"private": true,
|
||||
"version": "0.0.3",
|
||||
"version": "2.10.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"tauri": "tauri"
|
||||
"tauri": "tauri",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"bump": "node scripts/bump.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
"@langchain/core": "^0.3.64",
|
||||
"@langchain/deepseek": "^0.1.0",
|
||||
"@langchain/langgraph": "^0.3.10",
|
||||
"@langchain/ollama": "^0.2.3",
|
||||
"@tauri-apps/api": "^2.6.2",
|
||||
"@tauri-apps/plugin-deep-link": "~2",
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-fs": "~2",
|
||||
"@tauri-apps/plugin-http": "~2",
|
||||
"@tauri-apps/plugin-notification": "~2",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-shell": "~2",
|
||||
"@tauri-apps/plugin-sql": "~2",
|
||||
"lucide-svelte": "^0.479.0",
|
||||
"marked": "^16.1.1",
|
||||
"qrcode": "^1.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.0",
|
||||
"@tauri-apps/cli": "^1.2.2",
|
||||
"@tauri-apps/cli": "^2.4.1",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"daisyui": "^2.51.5",
|
||||
"flowbite": "^2.5.1",
|
||||
"flowbite-svelte": "^0.46.16",
|
||||
"flowbite-svelte-icons": "^1.6.1",
|
||||
"postcss": "^8.4.21",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-check": "^3.0.0",
|
||||
@@ -28,6 +50,7 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^4.0.0"
|
||||
"vite": "^4.0.0",
|
||||
"vitepress": "^1.6.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/imgs/donate.png
Normal file
|
After Width: | Height: | Size: 474 KiB |
BIN
public/imgs/douyin.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
53
public/shaka-player/controls.min.css
vendored
Normal file
1865
public/shaka-player/shaka-player.ui.js
Normal file
8
public/shaka-player/shaka-player.ui.map
Normal file
286
public/shaka-player/youtube-theme.css
Normal file
@@ -0,0 +1,286 @@
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Me5Q.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmEU9vAw.ttf) format('truetype');
|
||||
}
|
||||
.youtube-theme {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
.youtube-theme .shaka-bottom-controls {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
padding-bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.youtube-theme .shaka-bottom-controls {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
.youtube-theme .shaka-ad-controls {
|
||||
-webkit-box-ordinal-group: 2;
|
||||
-ms-flex-order: 1;
|
||||
order: 1;
|
||||
}
|
||||
.youtube-theme .shaka-controls-button-panel {
|
||||
-webkit-box-ordinal-group: 3;
|
||||
-ms-flex-order: 2;
|
||||
order: 2;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.youtube-theme .shaka-range-container {
|
||||
margin: 4px 10px 4px 10px;
|
||||
top: 0;
|
||||
}
|
||||
.youtube-theme .shaka-small-play-button {
|
||||
-webkit-box-ordinal-group: -2;
|
||||
-ms-flex-order: -3;
|
||||
order: -3;
|
||||
}
|
||||
.youtube-theme .shaka-mute-button {
|
||||
-webkit-box-ordinal-group: -1;
|
||||
-ms-flex-order: -2;
|
||||
order: -2;
|
||||
}
|
||||
.youtube-theme .shaka-controls-button-panel > * {
|
||||
margin: 0;
|
||||
padding: 3px 8px;
|
||||
color: #EEE;
|
||||
height: 40px;
|
||||
}
|
||||
.youtube-theme .shaka-controls-button-panel > *:focus {
|
||||
outline: none;
|
||||
-webkit-box-shadow: inset 0 0 0 2px rgba(27, 127, 204, 0.8);
|
||||
box-shadow: inset 0 0 0 2px rgba(27, 127, 204, 0.8);
|
||||
color: #FFF;
|
||||
}
|
||||
.youtube-theme .shaka-controls-button-panel > *:hover {
|
||||
color: #FFF;
|
||||
}
|
||||
.youtube-theme .shaka-controls-button-panel .shaka-volume-bar-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
left: -1px;
|
||||
-webkit-box-ordinal-group: 0;
|
||||
-ms-flex-order: -1;
|
||||
order: -1;
|
||||
opacity: 0;
|
||||
width: 0px;
|
||||
-webkit-transition: width 0.2s cubic-bezier(0.4, 0, 1, 1);
|
||||
height: 3px;
|
||||
transition: width 0.2s cubic-bezier(0.4, 0, 1, 1);
|
||||
padding: 0;
|
||||
}
|
||||
.youtube-theme .shaka-controls-button-panel .shaka-volume-bar-container:hover,
|
||||
.youtube-theme .shaka-controls-button-panel .shaka-volume-bar-container:focus {
|
||||
display: block;
|
||||
width: 50px;
|
||||
opacity: 1;
|
||||
padding: 0 6px;
|
||||
}
|
||||
.youtube-theme .shaka-mute-button:hover + div {
|
||||
opacity: 1;
|
||||
width: 50px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
.youtube-theme .shaka-current-time {
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.youtube-theme .shaka-seek-bar-container {
|
||||
height: 3px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.youtube-theme .shaka-seek-bar-container .shaka-range-element {
|
||||
opacity: 0;
|
||||
}
|
||||
.youtube-theme .shaka-seek-bar-container:hover {
|
||||
height: 5px;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.youtube-theme .shaka-seek-bar-container:hover .shaka-range-element {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.youtube-theme .shaka-seek-bar-container input[type=range]::-webkit-slider-thumb {
|
||||
background: #FF0000;
|
||||
cursor: pointer;
|
||||
}
|
||||
.youtube-theme .shaka-seek-bar-container input[type=range]::-moz-range-thumb {
|
||||
background: #FF0000;
|
||||
cursor: pointer;
|
||||
}
|
||||
.youtube-theme .shaka-seek-bar-container input[type=range]::-ms-thumb {
|
||||
background: #FF0000;
|
||||
cursor: pointer;
|
||||
}
|
||||
.youtube-theme .shaka-video-container * {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
.youtube-theme .shaka-video-container .material-icons-round {
|
||||
font-family: 'Material Icons Sharp';
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu,
|
||||
.youtube-theme .shaka-settings-menu {
|
||||
border-radius: 2px;
|
||||
background: rgba(28, 28, 28, 0.9);
|
||||
text-shadow: 0 0 2px rgb(0 0 0%);
|
||||
-webkit-transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1);
|
||||
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1);
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
right: 10px;
|
||||
bottom: 50px;
|
||||
padding: 8px 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
.youtube-theme .shaka-settings-menu {
|
||||
padding: 0 0 8px 0;
|
||||
}
|
||||
.youtube-theme .shaka-settings-menu button {
|
||||
font-size: 12px;
|
||||
}
|
||||
.youtube-theme .shaka-settings-menu button span {
|
||||
margin-left: 33px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.youtube-theme .shaka-settings-menu button[aria-selected="true"] {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
.youtube-theme .shaka-settings-menu button[aria-selected="true"] span {
|
||||
-webkit-box-ordinal-group: 3;
|
||||
-ms-flex-order: 2;
|
||||
order: 2;
|
||||
margin-left: 0;
|
||||
}
|
||||
.youtube-theme .shaka-settings-menu button[aria-selected="true"] i {
|
||||
-webkit-box-ordinal-group: 2;
|
||||
-ms-flex-order: 1;
|
||||
order: 1;
|
||||
font-size: 18px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu button {
|
||||
padding: 0;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu button i {
|
||||
display: none;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu button .shaka-overflow-button-label {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
outline: none;
|
||||
height: 40px;
|
||||
-webkit-box-flex: 0;
|
||||
-ms-flex: 0 0 100%;
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu button .shaka-overflow-button-label span {
|
||||
-ms-flex-negative: initial;
|
||||
flex-shrink: initial;
|
||||
padding-left: 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu span + span {
|
||||
color: #FFF;
|
||||
font-weight: 400 !important;
|
||||
font-size: 12px !important;
|
||||
padding-right: 8px;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu span + span:after {
|
||||
content: "navigate_next";
|
||||
font-family: 'Material Icons Sharp';
|
||||
font-size: 20px;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu .shaka-pip-button span + span {
|
||||
padding-right: 15px !important;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu .shaka-pip-button span + span:after {
|
||||
content: "";
|
||||
}
|
||||
.youtube-theme .shaka-back-to-overflow-button {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
font-size: 12px;
|
||||
color: #eee;
|
||||
height: 40px;
|
||||
}
|
||||
.youtube-theme .shaka-back-to-overflow-button .material-icons-round {
|
||||
font-size: 15px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.youtube-theme .shaka-back-to-overflow-button span {
|
||||
margin-left: 3px !important;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu button:hover,
|
||||
.youtube-theme .shaka-settings-menu button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu button:hover label,
|
||||
.youtube-theme .shaka-settings-menu button:hover label {
|
||||
cursor: pointer;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu button:focus,
|
||||
.youtube-theme .shaka-settings-menu button:focus {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu button,
|
||||
.youtube-theme .shaka-settings-menu button {
|
||||
color: #EEE;
|
||||
}
|
||||
.youtube-theme .shaka-captions-off {
|
||||
color: #BFBFBF;
|
||||
}
|
||||
.youtube-theme .shaka-overflow-menu-button {
|
||||
font-size: 18px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.youtube-theme .shaka-fullscreen-button:hover {
|
||||
font-size: 25px;
|
||||
-webkit-transition: font-size 0.1s cubic-bezier(0, 0, 0.2, 1);
|
||||
transition: font-size 0.1s cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
58
scripts/bump.cjs
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function updatePackageJson(version) {
|
||||
const packageJsonPath = path.join(process.cwd(), "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
packageJson.version = version;
|
||||
fs.writeFileSync(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2) + "\n"
|
||||
);
|
||||
console.log(`✅ Updated package.json version to ${version}`);
|
||||
}
|
||||
|
||||
function updateCargoToml(version) {
|
||||
const cargoTomlPath = path.join(process.cwd(), "src-tauri", "Cargo.toml");
|
||||
let cargoToml = fs.readFileSync(cargoTomlPath, "utf8");
|
||||
|
||||
// Update the version in the [package] section
|
||||
cargoToml = cargoToml.replace(/^version = ".*"$/m, `version = "${version}"`);
|
||||
|
||||
fs.writeFileSync(cargoTomlPath, cargoToml);
|
||||
console.log(`✅ Updated Cargo.toml version to ${version}`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("❌ Please provide a version number");
|
||||
console.error("Usage: yarn bump <version>");
|
||||
console.error("Example: yarn bump 3.1.0");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const version = args[0];
|
||||
|
||||
// Validate version format (simple check)
|
||||
if (!/^\d+\.\d+\.\d+/.test(version)) {
|
||||
console.error(
|
||||
"❌ Invalid version format. Please use semantic versioning (e.g., 3.1.0)"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
updatePackageJson(version);
|
||||
updateCargoToml(version);
|
||||
console.log(`🎉 Successfully bumped version to ${version}`);
|
||||
} catch (error) {
|
||||
console.error("❌ Error updating version:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
4
src-tauri/.gitignore
vendored
@@ -1,5 +1,9 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
cache
|
||||
output
|
||||
tmps
|
||||
clips
|
||||
data
|
||||
config.toml
|
||||
7094
src-tauri/Cargo.lock
generated
@@ -1,7 +1,11 @@
|
||||
[workspace]
|
||||
members = ["crates/danmu_stream"]
|
||||
resolver = "2"
|
||||
|
||||
[package]
|
||||
name = "bili-shadowreplay"
|
||||
version = "0.0.3"
|
||||
description = "A Tauri App"
|
||||
version = "2.10.0"
|
||||
description = "BiliBili ShadowReplay"
|
||||
authors = ["Xinrea"]
|
||||
license = ""
|
||||
repository = ""
|
||||
@@ -9,29 +13,128 @@ edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.2", features = ["dialog-all", "fs-all", "http-all", "protocol-asset", "shell-open", "system-tray"] }
|
||||
danmu_stream = { path = "crates/danmu_stream" }
|
||||
serde_json = "1.0"
|
||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||
reqwest = { version = "0.11", features = ["blocking", "json", "multipart"] }
|
||||
serde_derive = "1.0.158"
|
||||
serde = "1.0.158"
|
||||
sysinfo = "0.32.0"
|
||||
m3u8-rs = "5.0.3"
|
||||
async-std = "1.12.0"
|
||||
futures = "0.3.27"
|
||||
ffmpeg-sidecar = "0.3.3"
|
||||
sqlite = "0.30.4"
|
||||
chrono = "0.4.24"
|
||||
async-ffmpeg-sidecar = "0.0.1"
|
||||
chrono = { version = "0.4.24", features = ["serde"] }
|
||||
toml = "0.7.3"
|
||||
custom_error = "1.9.2"
|
||||
felgens = "0.3.1"
|
||||
regex = "1.7.3"
|
||||
tokio = "1.27.0"
|
||||
tokio = { version = "1.27.0", features = ["process"] }
|
||||
platform-dirs = "0.3.0"
|
||||
pct-str = "1.2.0"
|
||||
md5 = "0.7.0"
|
||||
hyper = { version = "0.14", features = ["full"] }
|
||||
dashmap = "6.1.0"
|
||||
urlencoding = "2.1.3"
|
||||
log = "0.4.22"
|
||||
simplelog = "0.12.2"
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
||||
rand = "0.8.5"
|
||||
base64 = "0.21"
|
||||
mime_guess = "2.0"
|
||||
async-trait = "0.1.87"
|
||||
whisper-rs = "0.14.2"
|
||||
hound = "3.5.1"
|
||||
uuid = { version = "1.4", features = ["v4"] }
|
||||
axum = { version = "0.7", features = ["macros"] }
|
||||
tower-http = { version = "0.5", features = ["cors", "fs"] }
|
||||
futures-core = "0.3"
|
||||
futures = "0.3"
|
||||
tokio-util = { version = "0.7", features = ["io"] }
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
url = "2.5.4"
|
||||
srtparse = "0.2.0"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
cuda = ["whisper-rs/cuda"]
|
||||
headless = []
|
||||
default = ["gui"]
|
||||
gui = [
|
||||
"tauri",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-sql",
|
||||
"tauri-utils",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-deep-link",
|
||||
"fix-path-env",
|
||||
"tauri-build",
|
||||
]
|
||||
|
||||
[dependencies.tauri]
|
||||
version = "2"
|
||||
features = ["protocol-asset", "tray-icon"]
|
||||
optional = true
|
||||
|
||||
[dependencies.tauri-plugin-single-instance]
|
||||
version = "2"
|
||||
optional = true
|
||||
features = ["deep-link"]
|
||||
|
||||
[dependencies.tauri-plugin-dialog]
|
||||
version = "2"
|
||||
optional = true
|
||||
|
||||
[dependencies.tauri-plugin-shell]
|
||||
version = "2"
|
||||
optional = true
|
||||
|
||||
[dependencies.tauri-plugin-fs]
|
||||
version = "2"
|
||||
optional = true
|
||||
|
||||
[dependencies.tauri-plugin-http]
|
||||
version = "2"
|
||||
optional = true
|
||||
|
||||
[dependencies.tauri-plugin-sql]
|
||||
version = "2"
|
||||
optional = true
|
||||
features = ["sqlite"]
|
||||
|
||||
[dependencies.tauri-utils]
|
||||
version = "2"
|
||||
optional = true
|
||||
|
||||
[dependencies.tauri-plugin-os]
|
||||
version = "2"
|
||||
optional = true
|
||||
|
||||
[dependencies.tauri-plugin-notification]
|
||||
version = "2"
|
||||
optional = true
|
||||
|
||||
[dependencies.tauri-plugin-deep-link]
|
||||
version = "2"
|
||||
optional = true
|
||||
|
||||
[dependencies.fix-path-env]
|
||||
git = "https://github.com/tauri-apps/fix-path-env-rs"
|
||||
optional = true
|
||||
|
||||
[build-dependencies.tauri-build]
|
||||
version = "2"
|
||||
features = []
|
||||
optional = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
whisper-rs = { version = "0.14.2", default-features = false }
|
||||
|
||||
[target.'cfg(darwin)'.dependencies.whisper-rs]
|
||||
version = "0.14.2"
|
||||
features = ["metal"]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
#[cfg(feature = "gui")]
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
77
src-tauri/capabilities/migrated.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"identifier": "migrated",
|
||||
"description": "permissions that were migrated from v1",
|
||||
"local": true,
|
||||
"windows": [
|
||||
"main",
|
||||
"Live*",
|
||||
"Clip*"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"fs:allow-read-file",
|
||||
"fs:allow-write-file",
|
||||
"fs:allow-read-dir",
|
||||
"fs:allow-copy-file",
|
||||
"fs:allow-mkdir",
|
||||
"fs:allow-remove",
|
||||
"fs:allow-remove",
|
||||
"fs:allow-rename",
|
||||
"fs:allow-exists",
|
||||
{
|
||||
"identifier": "fs:scope",
|
||||
"allow": [
|
||||
"**"
|
||||
]
|
||||
},
|
||||
"core:window:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-unmaximize",
|
||||
"core:window:allow-set-title",
|
||||
"sql:allow-execute",
|
||||
"shell:allow-open",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"dialog:allow-message",
|
||||
"dialog:allow-ask",
|
||||
"dialog:allow-confirm",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{
|
||||
"url": "https://*.hdslb.com/"
|
||||
},
|
||||
{
|
||||
"url": "https://afdian.com/"
|
||||
},
|
||||
{
|
||||
"url": "https://*.afdiancdn.com/"
|
||||
},
|
||||
{
|
||||
"url": "https://*.douyin.com/"
|
||||
},
|
||||
{
|
||||
"url": "https://*.douyinpic.com/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dialog:default",
|
||||
"shell:default",
|
||||
"fs:default",
|
||||
"http:default",
|
||||
"sql:default",
|
||||
"os:default",
|
||||
"notification:default",
|
||||
"dialog:default",
|
||||
"fs:default",
|
||||
"http:default",
|
||||
"shell:default",
|
||||
"sql:default",
|
||||
"os:default",
|
||||
"dialog:default",
|
||||
"deep-link:default"
|
||||
]
|
||||
}
|
||||
16
src-tauri/config.example.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
cache = "./cache"
|
||||
output = "./output"
|
||||
live_start_notify = true
|
||||
live_end_notify = true
|
||||
clip_notify = true
|
||||
post_notify = true
|
||||
auto_subtitle = false
|
||||
subtitle_generator_type = "whisper_online"
|
||||
whisper_model = "./whisper_model.bin"
|
||||
whisper_prompt = "这是一段中文 你们好"
|
||||
openai_api_key = ""
|
||||
clip_name_format = "[{room_id}][{live_id}][{title}][{created_at}].mp4"
|
||||
|
||||
[auto_generate]
|
||||
enabled = false
|
||||
encode_danmu = false
|
||||
44
src-tauri/crates/danmu_stream/Cargo.toml
Normal file
@@ -0,0 +1,44 @@
|
||||
[package]
|
||||
name = "danmu_stream"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "danmu_stream"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[example]]
|
||||
name = "douyin"
|
||||
path = "examples/douyin.rs"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.20", features = ["native-tls"] }
|
||||
futures-util = "0.3"
|
||||
prost = "0.12"
|
||||
chrono = "0.4"
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
url = "2.4"
|
||||
md5 = "0.7"
|
||||
regex = "1.9"
|
||||
deno_core = "0.242.0"
|
||||
pct-str = "2.0.0"
|
||||
custom_error = "1.9.2"
|
||||
flate2 = "1.0"
|
||||
scroll = "0.13.0"
|
||||
scroll_derive = "0.13.0"
|
||||
brotli = "8.0.1"
|
||||
http = "1.0"
|
||||
rand = "0.9.1"
|
||||
urlencoding = "2.1.3"
|
||||
gzip = "0.1.2"
|
||||
hex = "0.4.3"
|
||||
async-trait = "0.1.88"
|
||||
uuid = "1.17.0"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.10"
|
||||
0
src-tauri/crates/danmu_stream/README.md
Normal file
41
src-tauri/crates/danmu_stream/examples/bilibili.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use danmu_stream::{danmu_stream::DanmuStream, provider::ProviderType, DanmuMessageType};
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize logging
|
||||
env_logger::init();
|
||||
// Replace these with actual values
|
||||
let room_id = 768756;
|
||||
let cookie = "";
|
||||
let stream = Arc::new(DanmuStream::new(ProviderType::BiliBili, cookie, room_id).await?);
|
||||
|
||||
log::info!("Start to receive danmu messages: {}", cookie);
|
||||
|
||||
let stream_clone = stream.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
log::info!("Waitting for message");
|
||||
if let Ok(Some(msg)) = stream_clone.recv().await {
|
||||
match msg {
|
||||
DanmuMessageType::DanmuMessage(danmu) => {
|
||||
log::info!("Received danmu message: {:?}", danmu.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("Channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _ = stream.start().await;
|
||||
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
|
||||
stream.stop().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
40
src-tauri/crates/danmu_stream/examples/douyin.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use danmu_stream::{danmu_stream::DanmuStream, provider::ProviderType, DanmuMessageType};
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize logging
|
||||
env_logger::init();
|
||||
// Replace these with actual values
|
||||
let room_id = 7514298567821937427; // Replace with actual Douyin room_id. When live starts, the room_id will be generated, so it's more like a live_id.
|
||||
let cookie = "your_cookie";
|
||||
let stream = Arc::new(DanmuStream::new(ProviderType::Douyin, cookie, room_id).await?);
|
||||
|
||||
log::info!("Start to receive danmu messages");
|
||||
|
||||
let _ = stream.start().await;
|
||||
|
||||
let stream_clone = stream.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if let Ok(Some(msg)) = stream_clone.recv().await {
|
||||
match msg {
|
||||
DanmuMessageType::DanmuMessage(danmu) => {
|
||||
log::info!("Received danmu message: {:?}", danmu.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("Channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
|
||||
stream.stop().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
51
src-tauri/crates/danmu_stream/src/danmu_stream.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
provider::{new, DanmuProvider, ProviderType},
|
||||
DanmuMessageType, DanmuStreamError,
|
||||
};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DanmuStream {
|
||||
pub provider_type: ProviderType,
|
||||
pub identifier: String,
|
||||
pub room_id: u64,
|
||||
pub provider: Arc<RwLock<Box<dyn DanmuProvider>>>,
|
||||
tx: mpsc::UnboundedSender<DanmuMessageType>,
|
||||
rx: Arc<RwLock<mpsc::UnboundedReceiver<DanmuMessageType>>>,
|
||||
}
|
||||
|
||||
impl DanmuStream {
|
||||
pub async fn new(
|
||||
provider_type: ProviderType,
|
||||
identifier: &str,
|
||||
room_id: u64,
|
||||
) -> Result<Self, DanmuStreamError> {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let provider = new(provider_type, identifier, room_id).await?;
|
||||
Ok(Self {
|
||||
provider_type,
|
||||
identifier: identifier.to_string(),
|
||||
room_id,
|
||||
provider: Arc::new(RwLock::new(provider)),
|
||||
tx,
|
||||
rx: Arc::new(RwLock::new(rx)),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> Result<(), DanmuStreamError> {
|
||||
self.provider.write().await.start(self.tx.clone()).await
|
||||
}
|
||||
|
||||
pub async fn stop(&self) -> Result<(), DanmuStreamError> {
|
||||
self.provider.write().await.stop().await?;
|
||||
// close channel
|
||||
self.rx.write().await.close();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn recv(&self) -> Result<Option<DanmuMessageType>, DanmuStreamError> {
|
||||
Ok(self.rx.write().await.recv().await)
|
||||
}
|
||||
}
|
||||
51
src-tauri/crates/danmu_stream/src/http_client.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::DanmuStreamError;
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
impl From<reqwest::Error> for DanmuStreamError {
|
||||
fn from(value: reqwest::Error) -> Self {
|
||||
Self::HttpError { err: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for DanmuStreamError {
|
||||
fn from(value: url::ParseError) -> Self {
|
||||
Self::ParseError { err: value }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ApiClient {
|
||||
client: reqwest::Client,
|
||||
header: HeaderMap,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
pub fn new(cookies: &str) -> Self {
|
||||
let mut header = HeaderMap::new();
|
||||
header.insert("cookie", cookies.parse().unwrap());
|
||||
|
||||
Self {
|
||||
client: reqwest::Client::new(),
|
||||
header,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
&self,
|
||||
url: &str,
|
||||
query: Option<&[(&str, &str)]>,
|
||||
) -> Result<reqwest::Response, DanmuStreamError> {
|
||||
let resp = self
|
||||
.client
|
||||
.get(url)
|
||||
.query(query.unwrap_or_default())
|
||||
.headers(self.header.clone())
|
||||
.timeout(Duration::from_secs(10))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
31
src-tauri/crates/danmu_stream/src/lib.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
pub mod danmu_stream;
|
||||
mod http_client;
|
||||
pub mod provider;
|
||||
|
||||
use custom_error::custom_error;
|
||||
|
||||
custom_error! {pub DanmuStreamError
|
||||
HttpError {err: reqwest::Error} = "HttpError {err}",
|
||||
ParseError {err: url::ParseError} = "ParseError {err}",
|
||||
WebsocketError {err: String } = "WebsocketError {err}",
|
||||
PackError {err: String} = "PackError {err}",
|
||||
UnsupportProto {proto: u16} = "UnsupportProto {proto}",
|
||||
MessageParseError {err: String} = "MessageParseError {err}",
|
||||
InvalidIdentifier {err: String} = "InvalidIdentifier {err}"
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DanmuMessageType {
|
||||
DanmuMessage(DanmuMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DanmuMessage {
|
||||
pub room_id: u64,
|
||||
pub user_id: u64,
|
||||
pub user_name: String,
|
||||
pub message: String,
|
||||
pub color: u32,
|
||||
/// timestamp in milliseconds
|
||||
pub timestamp: i64,
|
||||
}
|
||||
72
src-tauri/crates/danmu_stream/src/provider.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
mod bilibili;
|
||||
mod douyin;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{
|
||||
provider::bilibili::BiliDanmu, provider::douyin::DouyinDanmu, DanmuMessageType,
|
||||
DanmuStreamError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProviderType {
|
||||
BiliBili,
|
||||
Douyin,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait DanmuProvider: Send + Sync {
|
||||
async fn new(identifier: &str, room_id: u64) -> Result<Self, DanmuStreamError>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
async fn start(
|
||||
&self,
|
||||
tx: mpsc::UnboundedSender<DanmuMessageType>,
|
||||
) -> Result<(), DanmuStreamError>;
|
||||
|
||||
async fn stop(&self) -> Result<(), DanmuStreamError>;
|
||||
}
|
||||
|
||||
/// Creates a new danmu stream provider for the specified platform.
|
||||
///
|
||||
/// This function initializes and starts a danmu stream provider based on the specified platform type.
|
||||
/// The provider will fetch danmu messages and send them through the provided channel.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tx` - An unbounded sender channel that will receive danmu messages
|
||||
/// * `provider_type` - The type of platform to fetch danmu from (BiliBili or Douyin)
|
||||
/// * `identifier` - User validation information (e.g., cookies) required by the platform
|
||||
/// * `room_id` - The unique identifier of the room/channel to fetch danmu from. Notice that douyin room_id is more like a live_id, it changes every time the live starts.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Result<(), DanmmuStreamError>` where:
|
||||
/// * `Ok(())` indicates successful initialization and start of the provider, only return after disconnect
|
||||
/// * `Err(DanmmuStreamError)` indicates an error occurred during initialization or startup
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use tokio::sync::mpsc;
|
||||
/// let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
/// new(tx, ProviderType::BiliBili, "your_cookie", 123456).await?;
|
||||
/// ```
|
||||
pub async fn new(
|
||||
provider_type: ProviderType,
|
||||
identifier: &str,
|
||||
room_id: u64,
|
||||
) -> Result<Box<dyn DanmuProvider>, DanmuStreamError> {
|
||||
match provider_type {
|
||||
ProviderType::BiliBili => {
|
||||
let bili = BiliDanmu::new(identifier, room_id).await?;
|
||||
Ok(Box::new(bili))
|
||||
}
|
||||
ProviderType::Douyin => {
|
||||
let douyin = DouyinDanmu::new(identifier, room_id).await?;
|
||||
Ok(Box::new(douyin))
|
||||
}
|
||||
}
|
||||
}
|
||||
440
src-tauri/crates/danmu_stream/src/provider/bilibili.rs
Normal file
@@ -0,0 +1,440 @@
|
||||
mod dannmu_msg;
|
||||
mod interact_word;
|
||||
mod pack;
|
||||
mod send_gift;
|
||||
mod stream;
|
||||
mod super_chat;
|
||||
|
||||
use std::{sync::Arc, time::SystemTime};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures_util::{SinkExt, StreamExt, TryStreamExt};
|
||||
use log::{error, info};
|
||||
use pct_str::{PctString, URIReserved};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{
|
||||
sync::{mpsc, RwLock},
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
|
||||
use crate::{
|
||||
http_client::ApiClient,
|
||||
provider::{DanmuMessageType, DanmuProvider},
|
||||
DanmuStreamError,
|
||||
};
|
||||
|
||||
type WsReadType = futures_util::stream::SplitStream<
|
||||
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
|
||||
>;
|
||||
|
||||
type WsWriteType = futures_util::stream::SplitSink<
|
||||
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
|
||||
Message,
|
||||
>;
|
||||
|
||||
pub struct BiliDanmu {
|
||||
client: ApiClient,
|
||||
room_id: u64,
|
||||
user_id: u64,
|
||||
stop: Arc<RwLock<bool>>,
|
||||
write: Arc<RwLock<Option<WsWriteType>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DanmuProvider for BiliDanmu {
|
||||
async fn new(cookie: &str, room_id: u64) -> Result<Self, DanmuStreamError> {
|
||||
// find DedeUserID=<user_id> in cookie str
|
||||
let user_id = BiliDanmu::parse_user_id(cookie)?;
|
||||
// add buvid3 to cookie
|
||||
let cookie = format!("{};buvid3={}", cookie, uuid::Uuid::new_v4());
|
||||
let client = ApiClient::new(&cookie);
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
user_id,
|
||||
room_id,
|
||||
stop: Arc::new(RwLock::new(false)),
|
||||
write: Arc::new(RwLock::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn start(
|
||||
&self,
|
||||
tx: mpsc::UnboundedSender<DanmuMessageType>,
|
||||
) -> Result<(), DanmuStreamError> {
|
||||
let mut retry_count = 0;
|
||||
const MAX_RETRIES: u32 = 5;
|
||||
const RETRY_DELAY: Duration = Duration::from_secs(5);
|
||||
info!(
|
||||
"Bilibili WebSocket connection started, room_id: {}",
|
||||
self.room_id
|
||||
);
|
||||
|
||||
loop {
|
||||
if *self.stop.read().await {
|
||||
break;
|
||||
}
|
||||
|
||||
match self.connect_and_handle(tx.clone()).await {
|
||||
Ok(_) => {
|
||||
info!("Bilibili WebSocket connection closed normally");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Bilibili WebSocket connection error: {}", e);
|
||||
retry_count += 1;
|
||||
|
||||
if retry_count >= MAX_RETRIES {
|
||||
return Err(DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to connect after {} retries", MAX_RETRIES),
|
||||
});
|
||||
}
|
||||
|
||||
info!(
|
||||
"Retrying connection in {} seconds... (Attempt {}/{})",
|
||||
RETRY_DELAY.as_secs(),
|
||||
retry_count,
|
||||
MAX_RETRIES
|
||||
);
|
||||
tokio::time::sleep(RETRY_DELAY).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop(&self) -> Result<(), DanmuStreamError> {
|
||||
*self.stop.write().await = true;
|
||||
if let Some(mut write) = self.write.write().await.take() {
|
||||
if let Err(e) = write.close().await {
|
||||
error!("Failed to close WebSocket connection: {}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BiliDanmu {
|
||||
async fn connect_and_handle(
|
||||
&self,
|
||||
tx: mpsc::UnboundedSender<DanmuMessageType>,
|
||||
) -> Result<(), DanmuStreamError> {
|
||||
let wbi_key = self.get_wbi_key().await?;
|
||||
let danmu_info = self.get_danmu_info(&wbi_key, self.room_id).await?;
|
||||
let ws_hosts = danmu_info.data.host_list.clone();
|
||||
let mut conn = None;
|
||||
log::debug!("ws_hosts: {:?}", ws_hosts);
|
||||
// try to connect to ws_hsots, once success, send the token to the tx
|
||||
for i in ws_hosts {
|
||||
let host = format!("wss://{}/sub", i.host);
|
||||
match connect_async(&host).await {
|
||||
Ok((c, _)) => {
|
||||
conn = Some(c);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Connect ws host: {} has error, trying next host ...\n{:?}\n{:?}",
|
||||
host, i, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let conn = conn.ok_or(DanmuStreamError::WebsocketError {
|
||||
err: "Failed to connect to ws host".into(),
|
||||
})?;
|
||||
|
||||
let (write, read) = conn.split();
|
||||
*self.write.write().await = Some(write);
|
||||
|
||||
let json = serde_json::to_string(&WsSend {
|
||||
roomid: self.room_id,
|
||||
key: danmu_info.data.token,
|
||||
uid: self.user_id,
|
||||
protover: 3,
|
||||
platform: "web".to_string(),
|
||||
t: 2,
|
||||
})
|
||||
.map_err(|e| DanmuStreamError::WebsocketError { err: e.to_string() })?;
|
||||
|
||||
let json = pack::encode(&json, 7);
|
||||
if let Some(write) = self.write.write().await.as_mut() {
|
||||
write
|
||||
.send(Message::binary(json))
|
||||
.await
|
||||
.map_err(|e| DanmuStreamError::WebsocketError { err: e.to_string() })?;
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
v = BiliDanmu::send_heartbeat_packets(Arc::clone(&self.write)) => v,
|
||||
v = BiliDanmu::recv(read, tx, Arc::clone(&self.stop)) => v
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_heartbeat_packets(
|
||||
write: Arc<RwLock<Option<WsWriteType>>>,
|
||||
) -> Result<(), DanmuStreamError> {
|
||||
loop {
|
||||
if let Some(write) = write.write().await.as_mut() {
|
||||
write
|
||||
.send(Message::binary(pack::encode("", 2)))
|
||||
.await
|
||||
.map_err(|e| DanmuStreamError::WebsocketError { err: e.to_string() })?;
|
||||
}
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv(
|
||||
mut read: WsReadType,
|
||||
tx: mpsc::UnboundedSender<DanmuMessageType>,
|
||||
stop: Arc<RwLock<bool>>,
|
||||
) -> Result<(), DanmuStreamError> {
|
||||
while let Ok(Some(msg)) = read.try_next().await {
|
||||
if *stop.read().await {
|
||||
log::info!("Stopping bilibili danmu stream");
|
||||
break;
|
||||
}
|
||||
let data = msg.into_data();
|
||||
|
||||
if !data.is_empty() {
|
||||
let s = pack::build_pack(&data);
|
||||
|
||||
if let Ok(msgs) = s {
|
||||
for i in msgs {
|
||||
let ws = stream::WsStreamCtx::new(&i);
|
||||
if let Ok(ws) = ws {
|
||||
match ws.match_msg() {
|
||||
Ok(v) => {
|
||||
log::debug!("Received message: {:?}", v);
|
||||
tx.send(v).map_err(|e| DanmuStreamError::WebsocketError {
|
||||
err: e.to_string(),
|
||||
})?;
|
||||
}
|
||||
Err(e) => {
|
||||
log::trace!(
|
||||
"This message parsing is not yet supported:\nMessage: {i}\nErr: {e:#?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("{}", ws.unwrap_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_danmu_info(
|
||||
&self,
|
||||
wbi_key: &str,
|
||||
room_id: u64,
|
||||
) -> Result<DanmuInfo, DanmuStreamError> {
|
||||
let room_id = self.get_real_room(wbi_key, room_id).await?;
|
||||
let params = self
|
||||
.get_sign(
|
||||
wbi_key,
|
||||
serde_json::json!({
|
||||
"id": room_id,
|
||||
"type": 0,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let resp = self
|
||||
.client
|
||||
.get(
|
||||
&format!(
|
||||
"https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?{}",
|
||||
params
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.json::<DanmuInfo>()
|
||||
.await?;
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
async fn get_real_room(&self, wbi_key: &str, room_id: u64) -> Result<u64, DanmuStreamError> {
|
||||
let params = self
|
||||
.get_sign(
|
||||
wbi_key,
|
||||
serde_json::json!({
|
||||
"id": room_id,
|
||||
"from": "room",
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let resp = self
|
||||
.client
|
||||
.get(
|
||||
&format!(
|
||||
"https://api.live.bilibili.com/room/v1/Room/room_init?{}",
|
||||
params
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.json::<RoomInit>()
|
||||
.await?
|
||||
.data
|
||||
.room_id;
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
fn parse_user_id(cookie: &str) -> Result<u64, DanmuStreamError> {
|
||||
let mut user_id = None;
|
||||
|
||||
// find DedeUserID=<user_id> in cookie str
|
||||
let re = Regex::new(r"DedeUserID=(\d+)").unwrap();
|
||||
if let Some(captures) = re.captures(cookie) {
|
||||
if let Some(user) = captures.get(1) {
|
||||
user_id = Some(user.as_str().parse::<u64>().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(user_id) = user_id {
|
||||
Ok(user_id)
|
||||
} else {
|
||||
Err(DanmuStreamError::InvalidIdentifier {
|
||||
err: format!("Failed to find user_id in cookie: {cookie}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_wbi_key(&self) -> Result<String, DanmuStreamError> {
|
||||
let nav_info: serde_json::Value = self
|
||||
.client
|
||||
.get("https://api.bilibili.com/x/web-interface/nav", None)
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
let re = Regex::new(r"wbi/(.*).png").unwrap();
|
||||
let img = re
|
||||
.captures(nav_info["data"]["wbi_img"]["img_url"].as_str().unwrap())
|
||||
.unwrap()
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
let sub = re
|
||||
.captures(nav_info["data"]["wbi_img"]["sub_url"].as_str().unwrap())
|
||||
.unwrap()
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
let raw_string = format!("{}{}", img, sub);
|
||||
Ok(raw_string)
|
||||
}
|
||||
|
||||
pub async fn get_sign(
|
||||
&self,
|
||||
wbi_key: &str,
|
||||
mut parameters: serde_json::Value,
|
||||
) -> Result<String, DanmuStreamError> {
|
||||
let table = vec![
|
||||
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42,
|
||||
19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60,
|
||||
51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52,
|
||||
];
|
||||
let raw_string = wbi_key;
|
||||
let mut encoded = Vec::new();
|
||||
table.into_iter().for_each(|x| {
|
||||
if x < raw_string.len() {
|
||||
encoded.push(raw_string.as_bytes()[x]);
|
||||
}
|
||||
});
|
||||
// only keep 32 bytes of encoded
|
||||
encoded = encoded[0..32].to_vec();
|
||||
let encoded = String::from_utf8(encoded).unwrap();
|
||||
// Timestamp in seconds
|
||||
let wts = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
parameters
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("wts".to_owned(), serde_json::Value::String(wts.to_string()));
|
||||
// Get all keys from parameters into vec
|
||||
let mut keys = parameters
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.map(|x| x.to_owned())
|
||||
.collect::<Vec<String>>();
|
||||
// sort keys
|
||||
keys.sort();
|
||||
let mut params = String::new();
|
||||
keys.iter().for_each(|x| {
|
||||
params.push_str(x);
|
||||
params.push('=');
|
||||
// Convert value to string based on its type
|
||||
let value = match parameters.get(x).unwrap() {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
serde_json::Value::Number(n) => n.to_string(),
|
||||
serde_json::Value::Bool(b) => b.to_string(),
|
||||
_ => "".to_string(),
|
||||
};
|
||||
// Value filters !'()* characters
|
||||
let value = value.replace(['!', '\'', '(', ')', '*'], "");
|
||||
let value = PctString::encode(value.chars(), URIReserved);
|
||||
params.push_str(value.as_str());
|
||||
// add & if not last
|
||||
if x != keys.last().unwrap() {
|
||||
params.push('&');
|
||||
}
|
||||
});
|
||||
// md5 params+encoded
|
||||
let w_rid = md5::compute(params.to_string() + encoded.as_str());
|
||||
let params = params + format!("&w_rid={:x}", w_rid).as_str();
|
||||
Ok(params)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct WsSend {
|
||||
uid: u64,
|
||||
roomid: u64,
|
||||
key: String,
|
||||
protover: u32,
|
||||
platform: String,
|
||||
#[serde(rename = "type")]
|
||||
t: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct DanmuInfo {
|
||||
pub data: DanmuInfoData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct DanmuInfoData {
|
||||
pub token: String,
|
||||
pub host_list: Vec<WsHost>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct WsHost {
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct RoomInit {
|
||||
data: RoomInitData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct RoomInitData {
|
||||
room_id: u64,
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{provider::bilibili::stream::WsStreamCtx, DanmuStreamError};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct BiliDanmuMessage {
|
||||
pub uid: u64,
|
||||
pub username: String,
|
||||
pub msg: String,
|
||||
pub fan: Option<String>,
|
||||
pub fan_level: Option<u64>,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
impl BiliDanmuMessage {
|
||||
pub fn new_from_ctx(ctx: &WsStreamCtx) -> Result<Self, DanmuStreamError> {
|
||||
let info = ctx
|
||||
.info
|
||||
.as_ref()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "info is None".to_string(),
|
||||
})?;
|
||||
|
||||
let array_2 = info
|
||||
.get(2)
|
||||
.and_then(|x| x.as_array())
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "array_2 is None".to_string(),
|
||||
})?
|
||||
.to_owned();
|
||||
|
||||
let uid = array_2.first().and_then(|x| x.as_u64()).ok_or_else(|| {
|
||||
DanmuStreamError::MessageParseError {
|
||||
err: "uid is None".to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
let username = array_2
|
||||
.get(1)
|
||||
.and_then(|x| x.as_str())
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "username is None".to_string(),
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
let msg = info
|
||||
.get(1)
|
||||
.and_then(|x| x.as_str())
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "msg is None".to_string(),
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
let array_3 = info
|
||||
.get(3)
|
||||
.and_then(|x| x.as_array())
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "array_3 is None".to_string(),
|
||||
})?
|
||||
.to_owned();
|
||||
|
||||
let fan = array_3
|
||||
.get(1)
|
||||
.and_then(|x| x.as_str())
|
||||
.map(|x| x.to_owned());
|
||||
|
||||
let fan_level = array_3.first().and_then(|x| x.as_u64());
|
||||
|
||||
let timestamp = info
|
||||
.first()
|
||||
.and_then(|x| x.as_array())
|
||||
.and_then(|x| x.get(4))
|
||||
.and_then(|x| x.as_i64())
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "timestamp is None".to_string(),
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
uid,
|
||||
username,
|
||||
msg,
|
||||
fan,
|
||||
fan_level,
|
||||
timestamp,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use crate::{provider::bilibili::stream::WsStreamCtx, DanmuStreamError};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct InteractWord {
|
||||
pub uid: u64,
|
||||
pub uname: String,
|
||||
pub fan: Option<String>,
|
||||
pub fan_level: Option<u32>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl InteractWord {
|
||||
pub fn new_from_ctx(ctx: &WsStreamCtx) -> Result<Self, DanmuStreamError> {
|
||||
let data = ctx
|
||||
.data
|
||||
.as_ref()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "data is None".to_string(),
|
||||
})?;
|
||||
|
||||
let uname = data
|
||||
.uname
|
||||
.as_ref()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "uname is None".to_string(),
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
let uid = data
|
||||
.uid
|
||||
.as_ref()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "uid is None".to_string(),
|
||||
})?
|
||||
.as_u64()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "uid is None".to_string(),
|
||||
})?;
|
||||
|
||||
let fan = data
|
||||
.fans_medal
|
||||
.as_ref()
|
||||
.and_then(|x| x.medal_name.to_owned());
|
||||
|
||||
let fan = if fan == Some("".to_string()) {
|
||||
None
|
||||
} else {
|
||||
fan
|
||||
};
|
||||
|
||||
let fan_level = data.fans_medal.as_ref().and_then(|x| x.medal_level);
|
||||
|
||||
let fan_level = if fan_level == Some(0) {
|
||||
None
|
||||
} else {
|
||||
fan_level
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
uid,
|
||||
uname,
|
||||
fan,
|
||||
fan_level,
|
||||
})
|
||||
}
|
||||
}
|
||||
161
src-tauri/crates/danmu_stream/src/provider/bilibili/pack.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
// This file is copied from https://github.com/eatradish/felgens/blob/master/src/pack.rs
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use flate2::read::ZlibDecoder;
|
||||
use scroll::Pread;
|
||||
use scroll_derive::Pread;
|
||||
|
||||
use crate::DanmuStreamError;
|
||||
|
||||
#[derive(Debug, Pread, Clone)]
|
||||
struct BilibiliPackHeader {
|
||||
pack_len: u32,
|
||||
_header_len: u16,
|
||||
ver: u16,
|
||||
_op: u32,
|
||||
_seq: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Pread)]
|
||||
struct PackHotCount {
|
||||
count: u32,
|
||||
}
|
||||
|
||||
type BilibiliPackCtx<'a> = (BilibiliPackHeader, &'a [u8]);
|
||||
|
||||
fn pack(buffer: &[u8]) -> Result<BilibiliPackCtx, DanmuStreamError> {
|
||||
let data = buffer
|
||||
.pread_with(0, scroll::BE)
|
||||
.map_err(|e: scroll::Error| DanmuStreamError::PackError { err: e.to_string() })?;
|
||||
|
||||
let buf = &buffer[16..];
|
||||
|
||||
Ok((data, buf))
|
||||
}
|
||||
|
||||
fn write_int(buffer: &[u8], start: usize, val: u32) -> Vec<u8> {
|
||||
let val_bytes = val.to_be_bytes();
|
||||
|
||||
let mut buf = buffer.to_vec();
|
||||
|
||||
for (i, c) in val_bytes.iter().enumerate() {
|
||||
buf[start + i] = *c;
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn encode(s: &str, op: u8) -> Vec<u8> {
|
||||
let data = s.as_bytes();
|
||||
let packet_len = 16 + data.len();
|
||||
let header = vec![0, 0, 0, 0, 0, 16, 0, 1, 0, 0, 0, op, 0, 0, 0, 1];
|
||||
|
||||
let header = write_int(&header, 0, packet_len as u32);
|
||||
|
||||
[&header, data].concat()
|
||||
}
|
||||
|
||||
pub fn build_pack(buf: &[u8]) -> Result<Vec<String>, DanmuStreamError> {
|
||||
let ctx = pack(buf)?;
|
||||
let msgs = decode(ctx)?;
|
||||
|
||||
Ok(msgs)
|
||||
}
|
||||
|
||||
fn get_hot_count(body: &[u8]) -> Result<u32, DanmuStreamError> {
|
||||
let count = body
|
||||
.pread_with::<PackHotCount>(0, scroll::BE)
|
||||
.map_err(|e| DanmuStreamError::PackError { err: e.to_string() })?
|
||||
.count;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn zlib_decode(body: &[u8]) -> Result<(BilibiliPackHeader, Vec<u8>), DanmuStreamError> {
|
||||
let mut buf = vec![];
|
||||
let mut z = ZlibDecoder::new(body);
|
||||
z.read_to_end(&mut buf)
|
||||
.map_err(|e| DanmuStreamError::PackError { err: e.to_string() })?;
|
||||
|
||||
let ctx = pack(&buf)?;
|
||||
let header = ctx.0;
|
||||
let buf = ctx.1.to_vec();
|
||||
|
||||
Ok((header, buf))
|
||||
}
|
||||
|
||||
fn decode(ctx: BilibiliPackCtx) -> Result<Vec<String>, DanmuStreamError> {
|
||||
let (mut header, body) = ctx;
|
||||
|
||||
let mut buf = body.to_vec();
|
||||
|
||||
loop {
|
||||
(header, buf) = match header.ver {
|
||||
2 => zlib_decode(&buf)?,
|
||||
3 => brotli_decode(&buf)?,
|
||||
0 | 1 => break,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
let msgs = match header.ver {
|
||||
0 => split_msgs(buf, header)?,
|
||||
1 => vec![format!("{{\"count\": {}}}", get_hot_count(&buf)?)],
|
||||
x => return Err(DanmuStreamError::UnsupportProto { proto: x }),
|
||||
};
|
||||
|
||||
Ok(msgs)
|
||||
}
|
||||
|
||||
fn split_msgs(buf: Vec<u8>, header: BilibiliPackHeader) -> Result<Vec<String>, DanmuStreamError> {
|
||||
let mut buf = buf;
|
||||
let mut header = header;
|
||||
let mut msgs = vec![];
|
||||
let mut offset = 0;
|
||||
let buf_len = buf.len();
|
||||
|
||||
msgs.push(
|
||||
std::str::from_utf8(&buf[..(header.pack_len - 16) as usize])
|
||||
.map_err(|e| DanmuStreamError::PackError { err: e.to_string() })?
|
||||
.to_string(),
|
||||
);
|
||||
buf = buf[(header.pack_len - 16) as usize..].to_vec();
|
||||
offset += header.pack_len - 16;
|
||||
|
||||
while offset != buf_len as u32 {
|
||||
let ctx = pack(&buf).map_err(|e| DanmuStreamError::PackError { err: e.to_string() })?;
|
||||
|
||||
header = ctx.0;
|
||||
buf = ctx.1.to_vec();
|
||||
|
||||
msgs.push(
|
||||
std::str::from_utf8(&buf[..(header.pack_len - 16) as usize])
|
||||
.map_err(|e| DanmuStreamError::PackError { err: e.to_string() })?
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
buf = buf[(header.pack_len - 16) as usize..].to_vec();
|
||||
|
||||
offset += header.pack_len;
|
||||
}
|
||||
|
||||
Ok(msgs)
|
||||
}
|
||||
|
||||
fn brotli_decode(body: &[u8]) -> Result<(BilibiliPackHeader, Vec<u8>), DanmuStreamError> {
|
||||
let mut reader = brotli::Decompressor::new(body, 4096);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
|
||||
reader
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| DanmuStreamError::PackError { err: e.to_string() })?;
|
||||
|
||||
let ctx = pack(&buf).map_err(|e| DanmuStreamError::PackError { err: e.to_string() })?;
|
||||
|
||||
let header = ctx.0;
|
||||
let buf = ctx.1.to_vec();
|
||||
|
||||
Ok((header, buf))
|
||||
}
|
||||
115
src-tauri/crates/danmu_stream/src/provider/bilibili/send_gift.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{provider::bilibili::stream::WsStreamCtx, DanmuStreamError};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct SendGift {
|
||||
pub action: String,
|
||||
pub gift_name: String,
|
||||
pub num: u64,
|
||||
pub uname: String,
|
||||
pub uid: u64,
|
||||
pub medal_name: Option<String>,
|
||||
pub medal_level: Option<u32>,
|
||||
pub price: u32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl SendGift {
|
||||
pub fn new_from_ctx(ctx: &WsStreamCtx) -> Result<Self, DanmuStreamError> {
|
||||
let data = ctx
|
||||
.data
|
||||
.as_ref()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "data is None".to_string(),
|
||||
})?;
|
||||
|
||||
let action = data
|
||||
.action
|
||||
.as_ref()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "action is None".to_string(),
|
||||
})?
|
||||
.to_owned();
|
||||
|
||||
let combo_send = data.combo_send.clone();
|
||||
|
||||
let gift_name = if let Some(gift) = data.gift_name.as_ref() {
|
||||
gift.to_owned()
|
||||
} else if let Some(gift) = combo_send.clone().and_then(|x| x.gift_name) {
|
||||
gift
|
||||
} else {
|
||||
return Err(DanmuStreamError::MessageParseError {
|
||||
err: "gift_name is None".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let num = if let Some(num) = combo_send.clone().and_then(|x| x.combo_num) {
|
||||
num
|
||||
} else if let Some(num) = data.num {
|
||||
num
|
||||
} else if let Some(num) = combo_send.and_then(|x| x.gift_num) {
|
||||
num
|
||||
} else {
|
||||
return Err(DanmuStreamError::MessageParseError {
|
||||
err: "num is None".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let uname = data
|
||||
.uname
|
||||
.as_ref()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "uname is None".to_string(),
|
||||
})?
|
||||
.to_owned();
|
||||
|
||||
let uid = data
|
||||
.uid
|
||||
.as_ref()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "uid is None".to_string(),
|
||||
})?
|
||||
.as_u64()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "uid is None".to_string(),
|
||||
})?;
|
||||
|
||||
let medal_name = data
|
||||
.medal_info
|
||||
.as_ref()
|
||||
.and_then(|x| x.medal_name.to_owned());
|
||||
|
||||
let medal_level = data.medal_info.as_ref().and_then(|x| x.medal_level);
|
||||
|
||||
let medal_name = if medal_name == Some("".to_string()) {
|
||||
None
|
||||
} else {
|
||||
medal_name
|
||||
};
|
||||
|
||||
let medal_level = if medal_level == Some(0) {
|
||||
None
|
||||
} else {
|
||||
medal_level
|
||||
};
|
||||
|
||||
let price = data
|
||||
.price
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "price is None".to_string(),
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
action,
|
||||
gift_name,
|
||||
num,
|
||||
uname,
|
||||
uid,
|
||||
medal_name,
|
||||
medal_level,
|
||||
price,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
provider::{bilibili::dannmu_msg::BiliDanmuMessage, DanmuMessageType},
|
||||
DanmuMessage, DanmuStreamError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct WsStreamCtx {
|
||||
pub cmd: Option<String>,
|
||||
pub info: Option<Vec<Value>>,
|
||||
pub data: Option<WsStreamCtxData>,
|
||||
#[serde(flatten)]
|
||||
_v: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct WsStreamCtxData {
|
||||
pub message: Option<String>,
|
||||
pub price: Option<u32>,
|
||||
pub start_time: Option<u64>,
|
||||
pub time: Option<u32>,
|
||||
pub uid: Option<Value>,
|
||||
pub user_info: Option<WsStreamCtxDataUser>,
|
||||
pub medal_info: Option<WsStreamCtxDataMedalInfo>,
|
||||
pub uname: Option<String>,
|
||||
pub fans_medal: Option<WsStreamCtxDataMedalInfo>,
|
||||
pub action: Option<String>,
|
||||
#[serde(rename = "giftName")]
|
||||
pub gift_name: Option<String>,
|
||||
pub num: Option<u64>,
|
||||
pub combo_num: Option<u64>,
|
||||
pub gift_num: Option<u64>,
|
||||
pub combo_send: Box<Option<WsStreamCtxData>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct WsStreamCtxDataMedalInfo {
|
||||
pub medal_name: Option<String>,
|
||||
pub medal_level: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct WsStreamCtxDataUser {
|
||||
pub face: String,
|
||||
pub uname: String,
|
||||
}
|
||||
|
||||
impl WsStreamCtx {
|
||||
pub fn new(s: &str) -> Result<Self, DanmuStreamError> {
|
||||
serde_json::from_str(s).map_err(|_| DanmuStreamError::MessageParseError {
|
||||
err: "Failed to parse message".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn match_msg(&self) -> Result<DanmuMessageType, DanmuStreamError> {
|
||||
let cmd = self.handle_cmd();
|
||||
|
||||
let danmu_msg = match cmd {
|
||||
Some(c) if c.contains("DANMU_MSG") => Some(BiliDanmuMessage::new_from_ctx(self)?),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(danmu_msg) = danmu_msg {
|
||||
Ok(DanmuMessageType::DanmuMessage(DanmuMessage {
|
||||
room_id: 0,
|
||||
user_id: danmu_msg.uid,
|
||||
user_name: danmu_msg.username,
|
||||
message: danmu_msg.msg,
|
||||
color: 0,
|
||||
timestamp: danmu_msg.timestamp,
|
||||
}))
|
||||
} else {
|
||||
Err(DanmuStreamError::MessageParseError {
|
||||
err: "Unknown message".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_cmd(&self) -> Option<&str> {
|
||||
// handle DANMU_MSG:4:0:2:2:2:0
|
||||
let cmd = if let Some(c) = self.cmd.as_deref() {
|
||||
if c.starts_with("DM_INTERACTION") {
|
||||
Some("DANMU_MSG")
|
||||
} else {
|
||||
Some(c)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
cmd
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{provider::bilibili::stream::WsStreamCtx, DanmuStreamError};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct SuperChatMessage {
|
||||
pub uname: String,
|
||||
pub uid: u64,
|
||||
pub face: String,
|
||||
pub price: u32,
|
||||
pub start_time: u64,
|
||||
pub time: u32,
|
||||
pub msg: String,
|
||||
pub medal_name: Option<String>,
|
||||
pub medal_level: Option<u32>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl SuperChatMessage {
|
||||
pub fn new_from_ctx(ctx: &WsStreamCtx) -> Result<Self, DanmuStreamError> {
|
||||
let data = ctx
|
||||
.data
|
||||
.as_ref()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "data is None".to_string(),
|
||||
})?;
|
||||
|
||||
let user_info =
|
||||
data.user_info
|
||||
.as_ref()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "user_info is None".to_string(),
|
||||
})?;
|
||||
|
||||
let uname = user_info.uname.to_owned();
|
||||
|
||||
let uid = data.uid.as_ref().and_then(|x| x.as_u64()).ok_or_else(|| {
|
||||
DanmuStreamError::MessageParseError {
|
||||
err: "uid is None".to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
let face = user_info.face.to_owned();
|
||||
|
||||
let price = data
|
||||
.price
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "price is None".to_string(),
|
||||
})?;
|
||||
|
||||
let start_time = data
|
||||
.start_time
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "start_time is None".to_string(),
|
||||
})?;
|
||||
|
||||
let time = data
|
||||
.time
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "time is None".to_string(),
|
||||
})?;
|
||||
|
||||
let msg = data
|
||||
.message
|
||||
.as_ref()
|
||||
.ok_or_else(|| DanmuStreamError::MessageParseError {
|
||||
err: "message is None".to_string(),
|
||||
})?
|
||||
.to_owned();
|
||||
|
||||
let medal = data
|
||||
.medal_info
|
||||
.as_ref()
|
||||
.map(|x| (x.medal_name.to_owned(), x.medal_level.to_owned()));
|
||||
|
||||
let medal_name = medal.as_ref().and_then(|(name, _)| name.to_owned());
|
||||
|
||||
let medal_level = medal.and_then(|(_, level)| level);
|
||||
|
||||
Ok(Self {
|
||||
uname,
|
||||
uid,
|
||||
face,
|
||||
price,
|
||||
start_time,
|
||||
time,
|
||||
msg,
|
||||
medal_name,
|
||||
medal_level,
|
||||
})
|
||||
}
|
||||
}
|
||||
462
src-tauri/crates/danmu_stream/src/provider/douyin.rs
Normal file
@@ -0,0 +1,462 @@
|
||||
use crate::{provider::DanmuProvider, DanmuMessage, DanmuMessageType, DanmuStreamError};
|
||||
use async_trait::async_trait;
|
||||
use deno_core::v8;
|
||||
use deno_core::JsRuntime;
|
||||
use deno_core::RuntimeOptions;
|
||||
use flate2::read::GzDecoder;
|
||||
use futures_util::{SinkExt, StreamExt, TryStreamExt};
|
||||
use log::debug;
|
||||
use log::{error, info};
|
||||
use prost::bytes::Bytes;
|
||||
use prost::Message;
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_tungstenite::{
|
||||
connect_async, tungstenite::Message as WsMessage, MaybeTlsStream, WebSocketStream,
|
||||
};
|
||||
|
||||
mod messages;
|
||||
use messages::*;
|
||||
|
||||
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||
|
||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(10);
|
||||
|
||||
type WsReadType = futures_util::stream::SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
|
||||
type WsWriteType =
|
||||
futures_util::stream::SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, WsMessage>;
|
||||
|
||||
pub struct DouyinDanmu {
|
||||
room_id: u64,
|
||||
cookie: String,
|
||||
stop: Arc<RwLock<bool>>,
|
||||
write: Arc<RwLock<Option<WsWriteType>>>,
|
||||
}
|
||||
|
||||
impl DouyinDanmu {
|
||||
async fn connect_and_handle(
|
||||
&self,
|
||||
tx: mpsc::UnboundedSender<DanmuMessageType>,
|
||||
) -> Result<(), DanmuStreamError> {
|
||||
let url = self.get_wss_url().await?;
|
||||
|
||||
let request = tokio_tungstenite::tungstenite::http::Request::builder()
|
||||
.uri(url)
|
||||
.header(
|
||||
tokio_tungstenite::tungstenite::http::header::COOKIE,
|
||||
self.cookie.as_str(),
|
||||
)
|
||||
.header(
|
||||
tokio_tungstenite::tungstenite::http::header::REFERER,
|
||||
"https://live.douyin.com/",
|
||||
)
|
||||
.header(
|
||||
tokio_tungstenite::tungstenite::http::header::USER_AGENT,
|
||||
USER_AGENT,
|
||||
)
|
||||
.header(
|
||||
tokio_tungstenite::tungstenite::http::header::HOST,
|
||||
"webcast5-ws-web-hl.douyin.com",
|
||||
)
|
||||
.header(
|
||||
tokio_tungstenite::tungstenite::http::header::UPGRADE,
|
||||
"websocket",
|
||||
)
|
||||
.header(
|
||||
tokio_tungstenite::tungstenite::http::header::CONNECTION,
|
||||
"Upgrade",
|
||||
)
|
||||
.header(
|
||||
tokio_tungstenite::tungstenite::http::header::SEC_WEBSOCKET_VERSION,
|
||||
"13",
|
||||
)
|
||||
.header(
|
||||
tokio_tungstenite::tungstenite::http::header::SEC_WEBSOCKET_EXTENSIONS,
|
||||
"permessage-deflate; client_max_window_bits",
|
||||
)
|
||||
.header(
|
||||
tokio_tungstenite::tungstenite::http::header::SEC_WEBSOCKET_KEY,
|
||||
"V1Yza5x1zcfkembl6u/0Pg==",
|
||||
)
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
let (ws_stream, response) =
|
||||
connect_async(request)
|
||||
.await
|
||||
.map_err(|e| DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to connect to douyin websocket: {}", e),
|
||||
})?;
|
||||
|
||||
// Log the response status for debugging
|
||||
info!("WebSocket connection response: {:?}", response.status());
|
||||
|
||||
let (write, read) = ws_stream.split();
|
||||
*self.write.write().await = Some(write);
|
||||
self.handle_connection(read, tx).await
|
||||
}
|
||||
|
||||
async fn get_wss_url(&self) -> Result<String, DanmuStreamError> {
|
||||
// Create a new V8 runtime
|
||||
let mut runtime = JsRuntime::new(RuntimeOptions::default());
|
||||
|
||||
// Add global CryptoJS object
|
||||
let crypto_js = include_str!("douyin/crypto-js.min.js");
|
||||
runtime
|
||||
.execute_script(
|
||||
"<crypto-js.min.js>",
|
||||
deno_core::FastString::Static(crypto_js),
|
||||
)
|
||||
.map_err(|e| DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to execute crypto-js: {}", e),
|
||||
})?;
|
||||
|
||||
// Load and execute the sign.js file
|
||||
let js_code = include_str!("douyin/webmssdk.js");
|
||||
runtime
|
||||
.execute_script("<sign.js>", deno_core::FastString::Static(js_code))
|
||||
.map_err(|e| DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to execute JavaScript: {}", e),
|
||||
})?;
|
||||
|
||||
// Call the get_wss_url function
|
||||
let sign_call = format!("get_wss_url(\"{}\")", self.room_id);
|
||||
let result = runtime
|
||||
.execute_script(
|
||||
"<sign_call>",
|
||||
deno_core::FastString::Owned(sign_call.into_boxed_str()),
|
||||
)
|
||||
.map_err(|e| DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to execute JavaScript: {}", e),
|
||||
})?;
|
||||
|
||||
// Get the result from the V8 runtime
|
||||
let scope = &mut runtime.handle_scope();
|
||||
let local = v8::Local::new(scope, result);
|
||||
let url = local.to_string(scope).unwrap().to_rust_string_lossy(scope);
|
||||
|
||||
debug!("Douyin wss url: {}", url);
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
&self,
|
||||
mut read: WsReadType,
|
||||
tx: mpsc::UnboundedSender<DanmuMessageType>,
|
||||
) -> Result<(), DanmuStreamError> {
|
||||
// Start heartbeat task with error handling
|
||||
let (tx_write, mut _rx_write) = mpsc::channel(32);
|
||||
let tx_write_clone = tx_write.clone();
|
||||
let stop = Arc::clone(&self.stop);
|
||||
let heartbeat_handle = tokio::spawn(async move {
|
||||
let mut last_heartbeat = SystemTime::now();
|
||||
let mut consecutive_failures = 0;
|
||||
const MAX_FAILURES: u32 = 3;
|
||||
|
||||
loop {
|
||||
if *stop.read().await {
|
||||
log::info!("Stopping douyin danmu stream");
|
||||
break;
|
||||
}
|
||||
|
||||
tokio::time::sleep(HEARTBEAT_INTERVAL).await;
|
||||
|
||||
match Self::send_heartbeat(&tx_write_clone).await {
|
||||
Ok(_) => {
|
||||
last_heartbeat = SystemTime::now();
|
||||
consecutive_failures = 0;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to send heartbeat: {}", e);
|
||||
consecutive_failures += 1;
|
||||
|
||||
if consecutive_failures >= MAX_FAILURES {
|
||||
error!("Too many consecutive heartbeat failures, closing connection");
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we've exceeded the maximum time without a successful heartbeat
|
||||
if let Ok(duration) = last_heartbeat.elapsed() {
|
||||
if duration > HEARTBEAT_INTERVAL * 2 {
|
||||
error!("No successful heartbeat for too long, closing connection");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Main message handling loop
|
||||
let room_id = self.room_id;
|
||||
let stop = Arc::clone(&self.stop);
|
||||
let write = Arc::clone(&self.write);
|
||||
let message_handle = tokio::spawn(async move {
|
||||
while let Some(msg) =
|
||||
read.try_next()
|
||||
.await
|
||||
.map_err(|e| DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to read message: {}", e),
|
||||
})?
|
||||
{
|
||||
if *stop.read().await {
|
||||
log::info!("Stopping douyin danmu stream");
|
||||
break;
|
||||
}
|
||||
|
||||
match msg {
|
||||
WsMessage::Binary(data) => {
|
||||
if let Ok(Some(ack)) = handle_binary_message(&data, &tx, room_id).await {
|
||||
if let Some(write) = write.write().await.as_mut() {
|
||||
if let Err(e) =
|
||||
write.send(WsMessage::Binary(ack.encode_to_vec())).await
|
||||
{
|
||||
error!("Failed to send ack: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WsMessage::Close(_) => {
|
||||
info!("WebSocket connection closed");
|
||||
break;
|
||||
}
|
||||
WsMessage::Ping(data) => {
|
||||
// Respond to ping with pong
|
||||
if let Err(e) = tx_write.send(WsMessage::Pong(data)).await {
|
||||
error!("Failed to send pong: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok::<(), DanmuStreamError>(())
|
||||
});
|
||||
|
||||
// Wait for either the heartbeat or message handling to complete
|
||||
tokio::select! {
|
||||
result = heartbeat_handle => {
|
||||
if let Err(e) = result {
|
||||
error!("Heartbeat task failed: {}", e);
|
||||
}
|
||||
}
|
||||
result = message_handle => {
|
||||
if let Err(e) = result {
|
||||
error!("Message handling task failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_heartbeat(tx: &mpsc::Sender<WsMessage>) -> Result<(), DanmuStreamError> {
|
||||
// heartbeat message: 3A 02 68 62
|
||||
tx.send(WsMessage::Binary(vec![0x3A, 0x02, 0x68, 0x62]))
|
||||
.await
|
||||
.map_err(|e| DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to send heartbeat message: {}", e),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_binary_message(
|
||||
data: &[u8],
|
||||
tx: &mpsc::UnboundedSender<DanmuMessageType>,
|
||||
room_id: u64,
|
||||
) -> Result<Option<PushFrame>, DanmuStreamError> {
|
||||
// First decode the PushFrame
|
||||
let push_frame = PushFrame::decode(Bytes::from(data.to_vec())).map_err(|e| {
|
||||
DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to decode PushFrame: {}", e),
|
||||
}
|
||||
})?;
|
||||
|
||||
// Decompress the payload
|
||||
let mut decoder = GzDecoder::new(push_frame.payload.as_slice());
|
||||
let mut decompressed = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut decompressed)
|
||||
.map_err(|e| DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to decompress payload: {}", e),
|
||||
})?;
|
||||
|
||||
// Decode the Response from decompressed payload
|
||||
let response = Response::decode(Bytes::from(decompressed)).map_err(|e| {
|
||||
DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to decode Response: {}", e),
|
||||
}
|
||||
})?;
|
||||
|
||||
// if payload_package.needAck:
|
||||
// obj = PushFrame()
|
||||
// obj.payloadType = 'ack'
|
||||
// obj.logId = log_id
|
||||
// obj.payloadType = payload_package.internalExt
|
||||
// ack = obj.SerializeToString()
|
||||
let mut ack = None;
|
||||
if response.need_ack {
|
||||
let ack_msg = PushFrame {
|
||||
payload_type: "ack".to_string(),
|
||||
log_id: push_frame.log_id,
|
||||
payload_encoding: "".to_string(),
|
||||
payload: vec![],
|
||||
seq_id: 0,
|
||||
service: 0,
|
||||
method: 0,
|
||||
headers_list: vec![],
|
||||
};
|
||||
|
||||
debug!("Need to respond ack: {:?}", ack_msg);
|
||||
|
||||
ack = Some(ack_msg);
|
||||
}
|
||||
|
||||
for message in response.messages_list {
|
||||
match message.method.as_str() {
|
||||
"WebcastChatMessage" => {
|
||||
let chat_msg =
|
||||
DouyinChatMessage::decode(message.payload.as_slice()).map_err(|e| {
|
||||
DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to decode chat message: {}", e),
|
||||
}
|
||||
})?;
|
||||
if let Some(user) = chat_msg.user {
|
||||
let danmu_msg = DanmuMessage {
|
||||
room_id,
|
||||
user_id: user.id,
|
||||
user_name: user.nick_name,
|
||||
message: chat_msg.content,
|
||||
color: 0xffffff,
|
||||
timestamp: chat_msg.event_time as i64 * 1000,
|
||||
};
|
||||
debug!("Received danmu message: {:?}", danmu_msg);
|
||||
tx.send(DanmuMessageType::DanmuMessage(danmu_msg))
|
||||
.map_err(|e| DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to send message to channel: {}", e),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
"WebcastGiftMessage" => {
|
||||
let gift_msg = GiftMessage::decode(message.payload.as_slice()).map_err(|e| {
|
||||
DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to decode gift message: {}", e),
|
||||
}
|
||||
})?;
|
||||
if let Some(user) = gift_msg.user {
|
||||
if let Some(gift) = gift_msg.gift {
|
||||
log::debug!("Received gift: {} from user: {}", gift.name, user.nick_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
"WebcastLikeMessage" => {
|
||||
let like_msg = LikeMessage::decode(message.payload.as_slice()).map_err(|e| {
|
||||
DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to decode like message: {}", e),
|
||||
}
|
||||
})?;
|
||||
if let Some(user) = like_msg.user {
|
||||
log::debug!(
|
||||
"Received {} likes from user: {}",
|
||||
like_msg.count,
|
||||
user.nick_name
|
||||
);
|
||||
}
|
||||
}
|
||||
"WebcastMemberMessage" => {
|
||||
let member_msg =
|
||||
MemberMessage::decode(message.payload.as_slice()).map_err(|e| {
|
||||
DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to decode member message: {}", e),
|
||||
}
|
||||
})?;
|
||||
if let Some(user) = member_msg.user {
|
||||
log::debug!(
|
||||
"Member joined: {} (Action: {})",
|
||||
user.nick_name,
|
||||
member_msg.action_description
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
debug!("Unknown message: {:?}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ack)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DanmuProvider for DouyinDanmu {
|
||||
async fn new(identifier: &str, room_id: u64) -> Result<Self, DanmuStreamError> {
|
||||
Ok(Self {
|
||||
room_id,
|
||||
cookie: identifier.to_string(),
|
||||
stop: Arc::new(RwLock::new(false)),
|
||||
write: Arc::new(RwLock::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn start(
|
||||
&self,
|
||||
tx: mpsc::UnboundedSender<DanmuMessageType>,
|
||||
) -> Result<(), DanmuStreamError> {
|
||||
let mut retry_count = 0;
|
||||
const MAX_RETRIES: u32 = 5;
|
||||
const RETRY_DELAY: Duration = Duration::from_secs(5);
|
||||
info!(
|
||||
"Douyin WebSocket connection started, room_id: {}",
|
||||
self.room_id
|
||||
);
|
||||
|
||||
loop {
|
||||
if *self.stop.read().await {
|
||||
break;
|
||||
}
|
||||
|
||||
match self.connect_and_handle(tx.clone()).await {
|
||||
Ok(_) => {
|
||||
info!("Douyin WebSocket connection closed normally");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Douyin WebSocket connection error: {}", e);
|
||||
retry_count += 1;
|
||||
|
||||
if retry_count >= MAX_RETRIES {
|
||||
return Err(DanmuStreamError::WebsocketError {
|
||||
err: format!("Failed to connect after {} retries", MAX_RETRIES),
|
||||
});
|
||||
}
|
||||
|
||||
info!(
|
||||
"Retrying connection in {} seconds... (Attempt {}/{})",
|
||||
RETRY_DELAY.as_secs(),
|
||||
retry_count,
|
||||
MAX_RETRIES
|
||||
);
|
||||
tokio::time::sleep(RETRY_DELAY).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop(&self) -> Result<(), DanmuStreamError> {
|
||||
*self.stop.write().await = true;
|
||||
if let Some(mut write) = self.write.write().await.take() {
|
||||
if let Err(e) = write.close().await {
|
||||
error!("Failed to close WebSocket connection: {}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
1
src-tauri/crates/danmu_stream/src/provider/douyin/crypto-js.min.js
vendored
Normal file
861
src-tauri/crates/danmu_stream/src/provider/douyin/messages.rs
Normal file
@@ -0,0 +1,861 @@
|
||||
use prost::Message;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// message Response {
|
||||
// repeated Message messagesList = 1;
|
||||
// string cursor = 2;
|
||||
// uint64 fetchInterval = 3;
|
||||
// uint64 now = 4;
|
||||
// string internalExt = 5;
|
||||
// uint32 fetchType = 6;
|
||||
// map<string, string> routeParams = 7;
|
||||
// uint64 heartbeatDuration = 8;
|
||||
// bool needAck = 9;
|
||||
// string pushServer = 10;
|
||||
// string liveCursor = 11;
|
||||
// bool historyNoMore = 12;
|
||||
// }
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct Response {
|
||||
#[prost(message, repeated, tag = "1")]
|
||||
pub messages_list: Vec<CommonMessage>,
|
||||
#[prost(string, tag = "2")]
|
||||
pub cursor: String,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub fetch_interval: u64,
|
||||
#[prost(uint64, tag = "4")]
|
||||
pub now: u64,
|
||||
#[prost(string, tag = "5")]
|
||||
pub internal_ext: String,
|
||||
#[prost(uint32, tag = "6")]
|
||||
pub fetch_type: u32,
|
||||
#[prost(map = "string, string", tag = "7")]
|
||||
pub route_params: HashMap<String, String>,
|
||||
#[prost(uint64, tag = "8")]
|
||||
pub heartbeat_duration: u64,
|
||||
#[prost(bool, tag = "9")]
|
||||
pub need_ack: bool,
|
||||
#[prost(string, tag = "10")]
|
||||
pub push_server: String,
|
||||
#[prost(string, tag = "11")]
|
||||
pub live_cursor: String,
|
||||
#[prost(bool, tag = "12")]
|
||||
pub history_no_more: bool,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct CommonMessage {
|
||||
#[prost(string, tag = "1")]
|
||||
pub method: String,
|
||||
#[prost(bytes, tag = "2")]
|
||||
pub payload: Vec<u8>,
|
||||
#[prost(int64, tag = "3")]
|
||||
pub msg_id: i64,
|
||||
#[prost(int32, tag = "4")]
|
||||
pub msg_type: i32,
|
||||
#[prost(int64, tag = "5")]
|
||||
pub offset: i64,
|
||||
#[prost(bool, tag = "6")]
|
||||
pub need_wrds_store: bool,
|
||||
#[prost(int64, tag = "7")]
|
||||
pub wrds_version: i64,
|
||||
#[prost(string, tag = "8")]
|
||||
pub wrds_sub_key: String,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct Common {
|
||||
#[prost(string, tag = "1")]
|
||||
pub method: String,
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub msg_id: u64,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub room_id: u64,
|
||||
#[prost(uint64, tag = "4")]
|
||||
pub create_time: u64,
|
||||
#[prost(uint32, tag = "5")]
|
||||
pub monitor: u32,
|
||||
#[prost(bool, tag = "6")]
|
||||
pub is_show_msg: bool,
|
||||
#[prost(string, tag = "7")]
|
||||
pub describe: String,
|
||||
#[prost(uint64, tag = "9")]
|
||||
pub fold_type: u64,
|
||||
#[prost(uint64, tag = "10")]
|
||||
pub anchor_fold_type: u64,
|
||||
#[prost(uint64, tag = "11")]
|
||||
pub priority_score: u64,
|
||||
#[prost(string, tag = "12")]
|
||||
pub log_id: String,
|
||||
#[prost(string, tag = "13")]
|
||||
pub msg_process_filter_k: String,
|
||||
#[prost(string, tag = "14")]
|
||||
pub msg_process_filter_v: String,
|
||||
#[prost(message, optional, tag = "15")]
|
||||
pub user: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct User {
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub id: u64,
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub short_id: u64,
|
||||
#[prost(string, tag = "3")]
|
||||
pub nick_name: String,
|
||||
#[prost(uint32, tag = "4")]
|
||||
pub gender: u32,
|
||||
#[prost(string, tag = "5")]
|
||||
pub signature: String,
|
||||
#[prost(uint32, tag = "6")]
|
||||
pub level: u32,
|
||||
#[prost(uint64, tag = "7")]
|
||||
pub birthday: u64,
|
||||
#[prost(string, tag = "8")]
|
||||
pub telephone: String,
|
||||
#[prost(message, optional, tag = "9")]
|
||||
pub avatar_thumb: Option<Image>,
|
||||
#[prost(message, optional, tag = "10")]
|
||||
pub avatar_medium: Option<Image>,
|
||||
#[prost(message, optional, tag = "11")]
|
||||
pub avatar_large: Option<Image>,
|
||||
#[prost(bool, tag = "12")]
|
||||
pub verified: bool,
|
||||
#[prost(uint32, tag = "13")]
|
||||
pub experience: u32,
|
||||
#[prost(string, tag = "14")]
|
||||
pub city: String,
|
||||
#[prost(int32, tag = "15")]
|
||||
pub status: i32,
|
||||
#[prost(uint64, tag = "16")]
|
||||
pub create_time: u64,
|
||||
#[prost(uint64, tag = "17")]
|
||||
pub modify_time: u64,
|
||||
#[prost(uint32, tag = "18")]
|
||||
pub secret: u32,
|
||||
#[prost(string, tag = "19")]
|
||||
pub share_qrcode_uri: String,
|
||||
#[prost(uint32, tag = "20")]
|
||||
pub income_share_percent: u32,
|
||||
#[prost(message, repeated, tag = "21")]
|
||||
pub badge_image_list: Vec<Image>,
|
||||
#[prost(message, optional, tag = "22")]
|
||||
pub follow_info: Option<FollowInfo>,
|
||||
#[prost(message, optional, tag = "23")]
|
||||
pub pay_grade: Option<PayGrade>,
|
||||
#[prost(message, optional, tag = "24")]
|
||||
pub fans_club: Option<FansClub>,
|
||||
#[prost(string, tag = "26")]
|
||||
pub special_id: String,
|
||||
#[prost(message, optional, tag = "27")]
|
||||
pub avatar_border: Option<Image>,
|
||||
#[prost(message, optional, tag = "28")]
|
||||
pub medal: Option<Image>,
|
||||
#[prost(message, repeated, tag = "29")]
|
||||
pub real_time_icons_list: Vec<Image>,
|
||||
#[prost(string, tag = "38")]
|
||||
pub display_id: String,
|
||||
#[prost(string, tag = "46")]
|
||||
pub sec_uid: String,
|
||||
#[prost(uint64, tag = "1022")]
|
||||
pub fan_ticket_count: u64,
|
||||
#[prost(string, tag = "1028")]
|
||||
pub id_str: String,
|
||||
#[prost(uint32, tag = "1045")]
|
||||
pub age_range: u32,
|
||||
}
|
||||
|
||||
#[derive(Message, PartialEq)]
|
||||
pub struct Image {
|
||||
#[prost(string, repeated, tag = "1")]
|
||||
pub url_list_list: Vec<String>,
|
||||
#[prost(string, tag = "2")]
|
||||
pub uri: String,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub height: u64,
|
||||
#[prost(uint64, tag = "4")]
|
||||
pub width: u64,
|
||||
#[prost(string, tag = "5")]
|
||||
pub avg_color: String,
|
||||
#[prost(uint32, tag = "6")]
|
||||
pub image_type: u32,
|
||||
#[prost(string, tag = "7")]
|
||||
pub open_web_url: String,
|
||||
#[prost(message, optional, tag = "8")]
|
||||
pub content: Option<ImageContent>,
|
||||
#[prost(bool, tag = "9")]
|
||||
pub is_animated: bool,
|
||||
#[prost(message, optional, tag = "10")]
|
||||
pub flex_setting_list: Option<NinePatchSetting>,
|
||||
#[prost(message, optional, tag = "11")]
|
||||
pub text_setting_list: Option<NinePatchSetting>,
|
||||
}
|
||||
|
||||
#[derive(Message, PartialEq)]
|
||||
pub struct ImageContent {
|
||||
#[prost(string, tag = "1")]
|
||||
pub name: String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub font_color: String,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub level: u64,
|
||||
#[prost(string, tag = "4")]
|
||||
pub alternative_text: String,
|
||||
}
|
||||
|
||||
#[derive(Message, PartialEq)]
|
||||
pub struct NinePatchSetting {
|
||||
#[prost(string, repeated, tag = "1")]
|
||||
pub setting_list_list: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct FollowInfo {
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub following_count: u64,
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub follower_count: u64,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub follow_status: u64,
|
||||
#[prost(uint64, tag = "4")]
|
||||
pub push_status: u64,
|
||||
#[prost(string, tag = "5")]
|
||||
pub remark_name: String,
|
||||
#[prost(string, tag = "6")]
|
||||
pub follower_count_str: String,
|
||||
#[prost(string, tag = "7")]
|
||||
pub following_count_str: String,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct PayGrade {
|
||||
#[prost(int64, tag = "1")]
|
||||
pub total_diamond_count: i64,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub diamond_icon: Option<Image>,
|
||||
#[prost(string, tag = "3")]
|
||||
pub name: String,
|
||||
#[prost(message, optional, tag = "4")]
|
||||
pub icon: Option<Image>,
|
||||
#[prost(string, tag = "5")]
|
||||
pub next_name: String,
|
||||
#[prost(int64, tag = "6")]
|
||||
pub level: i64,
|
||||
#[prost(message, optional, tag = "7")]
|
||||
pub next_icon: Option<Image>,
|
||||
#[prost(int64, tag = "8")]
|
||||
pub next_diamond: i64,
|
||||
#[prost(int64, tag = "9")]
|
||||
pub now_diamond: i64,
|
||||
#[prost(int64, tag = "10")]
|
||||
pub this_grade_min_diamond: i64,
|
||||
#[prost(int64, tag = "11")]
|
||||
pub this_grade_max_diamond: i64,
|
||||
#[prost(int64, tag = "12")]
|
||||
pub pay_diamond_bak: i64,
|
||||
#[prost(string, tag = "13")]
|
||||
pub grade_describe: String,
|
||||
#[prost(message, repeated, tag = "14")]
|
||||
pub grade_icon_list: Vec<GradeIcon>,
|
||||
#[prost(int64, tag = "15")]
|
||||
pub screen_chat_type: i64,
|
||||
#[prost(message, optional, tag = "16")]
|
||||
pub im_icon: Option<Image>,
|
||||
#[prost(message, optional, tag = "17")]
|
||||
pub im_icon_with_level: Option<Image>,
|
||||
#[prost(message, optional, tag = "18")]
|
||||
pub live_icon: Option<Image>,
|
||||
#[prost(message, optional, tag = "19")]
|
||||
pub new_im_icon_with_level: Option<Image>,
|
||||
#[prost(message, optional, tag = "20")]
|
||||
pub new_live_icon: Option<Image>,
|
||||
#[prost(int64, tag = "21")]
|
||||
pub upgrade_need_consume: i64,
|
||||
#[prost(string, tag = "22")]
|
||||
pub next_privileges: String,
|
||||
#[prost(message, optional, tag = "23")]
|
||||
pub background: Option<Image>,
|
||||
#[prost(message, optional, tag = "24")]
|
||||
pub background_back: Option<Image>,
|
||||
#[prost(int64, tag = "25")]
|
||||
pub score: i64,
|
||||
#[prost(message, optional, tag = "26")]
|
||||
pub buff_info: Option<GradeBuffInfo>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct GradeIcon {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub icon: Option<Image>,
|
||||
#[prost(int64, tag = "2")]
|
||||
pub icon_diamond: i64,
|
||||
#[prost(int64, tag = "3")]
|
||||
pub level: i64,
|
||||
#[prost(string, tag = "4")]
|
||||
pub level_str: String,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct GradeBuffInfo {}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct FansClub {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub data: Option<FansClubData>,
|
||||
#[prost(map = "int32, message", tag = "2")]
|
||||
pub prefer_data: HashMap<i32, FansClubData>,
|
||||
}
|
||||
|
||||
#[derive(Message, PartialEq)]
|
||||
pub struct FansClubData {
|
||||
#[prost(string, tag = "1")]
|
||||
pub club_name: String,
|
||||
#[prost(int32, tag = "2")]
|
||||
pub level: i32,
|
||||
#[prost(int32, tag = "3")]
|
||||
pub user_fans_club_status: i32,
|
||||
#[prost(message, optional, tag = "4")]
|
||||
pub badge: Option<UserBadge>,
|
||||
#[prost(int64, repeated, tag = "5")]
|
||||
pub available_gift_ids: Vec<i64>,
|
||||
#[prost(int64, tag = "6")]
|
||||
pub anchor_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Message, PartialEq)]
|
||||
pub struct UserBadge {
|
||||
#[prost(map = "int32, message", tag = "1")]
|
||||
pub icons: HashMap<i32, Image>,
|
||||
#[prost(string, tag = "2")]
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct PublicAreaCommon {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub user_label: Option<Image>,
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub user_consume_in_room: u64,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub user_send_gift_cnt_in_room: u64,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct LandscapeAreaCommon {
|
||||
#[prost(bool, tag = "1")]
|
||||
pub show_head: bool,
|
||||
#[prost(bool, tag = "2")]
|
||||
pub show_nickname: bool,
|
||||
#[prost(bool, tag = "3")]
|
||||
pub show_font_color: bool,
|
||||
#[prost(string, repeated, tag = "4")]
|
||||
pub color_value_list: Vec<String>,
|
||||
#[prost(enumeration = "CommentTypeTag", repeated, tag = "5")]
|
||||
pub comment_type_tags_list: Vec<i32>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct Text {
|
||||
#[prost(string, tag = "1")]
|
||||
pub key: String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub default_patter: String,
|
||||
#[prost(message, optional, tag = "3")]
|
||||
pub default_format: Option<TextFormat>,
|
||||
#[prost(message, repeated, tag = "4")]
|
||||
pub pieces_list: Vec<TextPiece>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct TextFormat {
|
||||
#[prost(string, tag = "1")]
|
||||
pub color: String,
|
||||
#[prost(bool, tag = "2")]
|
||||
pub bold: bool,
|
||||
#[prost(bool, tag = "3")]
|
||||
pub italic: bool,
|
||||
#[prost(uint32, tag = "4")]
|
||||
pub weight: u32,
|
||||
#[prost(uint32, tag = "5")]
|
||||
pub italic_angle: u32,
|
||||
#[prost(uint32, tag = "6")]
|
||||
pub font_size: u32,
|
||||
#[prost(bool, tag = "7")]
|
||||
pub use_heigh_light_color: bool,
|
||||
#[prost(bool, tag = "8")]
|
||||
pub use_remote_clor: bool,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct TextPiece {
|
||||
#[prost(bool, tag = "1")]
|
||||
pub r#type: bool,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub format: Option<TextFormat>,
|
||||
#[prost(string, tag = "3")]
|
||||
pub string_value: String,
|
||||
#[prost(message, optional, tag = "4")]
|
||||
pub user_value: Option<TextPieceUser>,
|
||||
#[prost(message, optional, tag = "5")]
|
||||
pub gift_value: Option<TextPieceGift>,
|
||||
#[prost(message, optional, tag = "6")]
|
||||
pub heart_value: Option<TextPieceHeart>,
|
||||
#[prost(message, optional, tag = "7")]
|
||||
pub pattern_ref_value: Option<TextPiecePatternRef>,
|
||||
#[prost(message, optional, tag = "8")]
|
||||
pub image_value: Option<TextPieceImage>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct TextPieceUser {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub user: Option<User>,
|
||||
#[prost(bool, tag = "2")]
|
||||
pub with_colon: bool,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct TextPieceGift {
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub gift_id: u64,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub name_ref: Option<PatternRef>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct PatternRef {
|
||||
#[prost(string, tag = "1")]
|
||||
pub key: String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub default_pattern: String,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct TextPieceHeart {
|
||||
#[prost(string, tag = "1")]
|
||||
pub color: String,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct TextPiecePatternRef {
|
||||
#[prost(string, tag = "1")]
|
||||
pub key: String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub default_pattern: String,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct TextPieceImage {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub image: Option<Image>,
|
||||
#[prost(float, tag = "2")]
|
||||
pub scaling_rate: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum CommentTypeTag {
|
||||
CommentTypeTagUnknown = 0,
|
||||
CommentTypeTagStar = 1,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct DouyinChatMessage {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub common: Option<Common>,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub user: Option<User>,
|
||||
#[prost(string, tag = "3")]
|
||||
pub content: String,
|
||||
#[prost(bool, tag = "4")]
|
||||
pub visible_to_sender: bool,
|
||||
#[prost(message, optional, tag = "5")]
|
||||
pub background_image: Option<Image>,
|
||||
#[prost(string, tag = "6")]
|
||||
pub full_screen_text_color: String,
|
||||
#[prost(message, optional, tag = "7")]
|
||||
pub background_image_v2: Option<Image>,
|
||||
#[prost(message, optional, tag = "9")]
|
||||
pub public_area_common: Option<PublicAreaCommon>,
|
||||
#[prost(message, optional, tag = "10")]
|
||||
pub gift_image: Option<Image>,
|
||||
#[prost(uint64, tag = "11")]
|
||||
pub agree_msg_id: u64,
|
||||
#[prost(uint32, tag = "12")]
|
||||
pub priority_level: u32,
|
||||
#[prost(message, optional, tag = "13")]
|
||||
pub landscape_area_common: Option<LandscapeAreaCommon>,
|
||||
#[prost(uint64, tag = "15")]
|
||||
pub event_time: u64,
|
||||
#[prost(bool, tag = "16")]
|
||||
pub send_review: bool,
|
||||
#[prost(bool, tag = "17")]
|
||||
pub from_intercom: bool,
|
||||
#[prost(bool, tag = "18")]
|
||||
pub intercom_hide_user_card: bool,
|
||||
#[prost(string, tag = "20")]
|
||||
pub chat_by: String,
|
||||
#[prost(uint32, tag = "21")]
|
||||
pub individual_chat_priority: u32,
|
||||
#[prost(message, optional, tag = "22")]
|
||||
pub rtf_content: Option<Text>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct GiftMessage {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub common: Option<Common>,
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub gift_id: u64,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub fan_ticket_count: u64,
|
||||
#[prost(uint64, tag = "4")]
|
||||
pub group_count: u64,
|
||||
#[prost(uint64, tag = "5")]
|
||||
pub repeat_count: u64,
|
||||
#[prost(uint64, tag = "6")]
|
||||
pub combo_count: u64,
|
||||
#[prost(message, optional, tag = "7")]
|
||||
pub user: Option<User>,
|
||||
#[prost(message, optional, tag = "8")]
|
||||
pub to_user: Option<User>,
|
||||
#[prost(uint32, tag = "9")]
|
||||
pub repeat_end: u32,
|
||||
#[prost(message, optional, tag = "10")]
|
||||
pub text_effect: Option<TextEffect>,
|
||||
#[prost(uint64, tag = "11")]
|
||||
pub group_id: u64,
|
||||
#[prost(uint64, tag = "12")]
|
||||
pub income_taskgifts: u64,
|
||||
#[prost(uint64, tag = "13")]
|
||||
pub room_fan_ticket_count: u64,
|
||||
#[prost(message, optional, tag = "14")]
|
||||
pub priority: Option<GiftIMPriority>,
|
||||
#[prost(message, optional, tag = "15")]
|
||||
pub gift: Option<GiftStruct>,
|
||||
#[prost(string, tag = "16")]
|
||||
pub log_id: String,
|
||||
#[prost(uint64, tag = "17")]
|
||||
pub send_type: u64,
|
||||
#[prost(message, optional, tag = "18")]
|
||||
pub public_area_common: Option<PublicAreaCommon>,
|
||||
#[prost(message, optional, tag = "19")]
|
||||
pub tray_display_text: Option<Text>,
|
||||
#[prost(uint64, tag = "20")]
|
||||
pub banned_display_effects: u64,
|
||||
#[prost(bool, tag = "25")]
|
||||
pub display_for_self: bool,
|
||||
#[prost(string, tag = "26")]
|
||||
pub interact_gift_info: String,
|
||||
#[prost(string, tag = "27")]
|
||||
pub diy_item_info: String,
|
||||
#[prost(uint64, repeated, tag = "28")]
|
||||
pub min_asset_set_list: Vec<u64>,
|
||||
#[prost(uint64, tag = "29")]
|
||||
pub total_count: u64,
|
||||
#[prost(uint32, tag = "30")]
|
||||
pub client_gift_source: u32,
|
||||
#[prost(uint64, repeated, tag = "32")]
|
||||
pub to_user_ids_list: Vec<u64>,
|
||||
#[prost(uint64, tag = "33")]
|
||||
pub send_time: u64,
|
||||
#[prost(uint64, tag = "34")]
|
||||
pub force_display_effects: u64,
|
||||
#[prost(string, tag = "35")]
|
||||
pub trace_id: String,
|
||||
#[prost(uint64, tag = "36")]
|
||||
pub effect_display_ts: u64,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct GiftStruct {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub image: Option<Image>,
|
||||
#[prost(string, tag = "2")]
|
||||
pub describe: String,
|
||||
#[prost(bool, tag = "3")]
|
||||
pub notify: bool,
|
||||
#[prost(uint64, tag = "4")]
|
||||
pub duration: u64,
|
||||
#[prost(uint64, tag = "5")]
|
||||
pub id: u64,
|
||||
#[prost(bool, tag = "7")]
|
||||
pub for_linkmic: bool,
|
||||
#[prost(bool, tag = "8")]
|
||||
pub doodle: bool,
|
||||
#[prost(bool, tag = "9")]
|
||||
pub for_fansclub: bool,
|
||||
#[prost(bool, tag = "10")]
|
||||
pub combo: bool,
|
||||
#[prost(uint32, tag = "11")]
|
||||
pub r#type: u32,
|
||||
#[prost(uint32, tag = "12")]
|
||||
pub diamond_count: u32,
|
||||
#[prost(bool, tag = "13")]
|
||||
pub is_displayed_on_panel: bool,
|
||||
#[prost(uint64, tag = "14")]
|
||||
pub primary_effect_id: u64,
|
||||
#[prost(message, optional, tag = "15")]
|
||||
pub gift_label_icon: Option<Image>,
|
||||
#[prost(string, tag = "16")]
|
||||
pub name: String,
|
||||
#[prost(string, tag = "17")]
|
||||
pub region: String,
|
||||
#[prost(string, tag = "18")]
|
||||
pub manual: String,
|
||||
#[prost(bool, tag = "19")]
|
||||
pub for_custom: bool,
|
||||
#[prost(message, optional, tag = "21")]
|
||||
pub icon: Option<Image>,
|
||||
#[prost(uint32, tag = "22")]
|
||||
pub action_type: u32,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct GiftIMPriority {
|
||||
#[prost(uint64, repeated, tag = "1")]
|
||||
pub queue_sizes_list: Vec<u64>,
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub self_queue_priority: u64,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub priority: u64,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct TextEffect {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub portrait: Option<TextEffectDetail>,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub landscape: Option<TextEffectDetail>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct TextEffectDetail {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub text: Option<Text>,
|
||||
#[prost(uint32, tag = "2")]
|
||||
pub text_font_size: u32,
|
||||
#[prost(message, optional, tag = "3")]
|
||||
pub background: Option<Image>,
|
||||
#[prost(uint32, tag = "4")]
|
||||
pub start: u32,
|
||||
#[prost(uint32, tag = "5")]
|
||||
pub duration: u32,
|
||||
#[prost(uint32, tag = "6")]
|
||||
pub x: u32,
|
||||
#[prost(uint32, tag = "7")]
|
||||
pub y: u32,
|
||||
#[prost(uint32, tag = "8")]
|
||||
pub width: u32,
|
||||
#[prost(uint32, tag = "9")]
|
||||
pub height: u32,
|
||||
#[prost(uint32, tag = "10")]
|
||||
pub shadow_dx: u32,
|
||||
#[prost(uint32, tag = "11")]
|
||||
pub shadow_dy: u32,
|
||||
#[prost(uint32, tag = "12")]
|
||||
pub shadow_radius: u32,
|
||||
#[prost(string, tag = "13")]
|
||||
pub shadow_color: String,
|
||||
#[prost(string, tag = "14")]
|
||||
pub stroke_color: String,
|
||||
#[prost(uint32, tag = "15")]
|
||||
pub stroke_width: u32,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct LikeMessage {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub common: Option<Common>,
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub count: u64,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub total: u64,
|
||||
#[prost(uint64, tag = "4")]
|
||||
pub color: u64,
|
||||
#[prost(message, optional, tag = "5")]
|
||||
pub user: Option<User>,
|
||||
#[prost(string, tag = "6")]
|
||||
pub icon: String,
|
||||
#[prost(message, optional, tag = "7")]
|
||||
pub double_like_detail: Option<DoubleLikeDetail>,
|
||||
#[prost(message, optional, tag = "8")]
|
||||
pub display_control_info: Option<DisplayControlInfo>,
|
||||
#[prost(uint64, tag = "9")]
|
||||
pub linkmic_guest_uid: u64,
|
||||
#[prost(string, tag = "10")]
|
||||
pub scene: String,
|
||||
#[prost(message, optional, tag = "11")]
|
||||
pub pico_display_info: Option<PicoDisplayInfo>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct DoubleLikeDetail {
|
||||
#[prost(bool, tag = "1")]
|
||||
pub double_flag: bool,
|
||||
#[prost(uint32, tag = "2")]
|
||||
pub seq_id: u32,
|
||||
#[prost(uint32, tag = "3")]
|
||||
pub renewals_num: u32,
|
||||
#[prost(uint32, tag = "4")]
|
||||
pub triggers_num: u32,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct DisplayControlInfo {
|
||||
#[prost(bool, tag = "1")]
|
||||
pub show_text: bool,
|
||||
#[prost(bool, tag = "2")]
|
||||
pub show_icons: bool,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct PicoDisplayInfo {
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub combo_sum_count: u64,
|
||||
#[prost(string, tag = "2")]
|
||||
pub emoji: String,
|
||||
#[prost(message, optional, tag = "3")]
|
||||
pub emoji_icon: Option<Image>,
|
||||
#[prost(string, tag = "4")]
|
||||
pub emoji_text: String,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct MemberMessage {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub common: Option<Common>,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub user: Option<User>,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub member_count: u64,
|
||||
#[prost(message, optional, tag = "4")]
|
||||
pub operator: Option<User>,
|
||||
#[prost(bool, tag = "5")]
|
||||
pub is_set_to_admin: bool,
|
||||
#[prost(bool, tag = "6")]
|
||||
pub is_top_user: bool,
|
||||
#[prost(uint64, tag = "7")]
|
||||
pub rank_score: u64,
|
||||
#[prost(uint64, tag = "8")]
|
||||
pub top_user_no: u64,
|
||||
#[prost(uint64, tag = "9")]
|
||||
pub enter_type: u64,
|
||||
#[prost(uint64, tag = "10")]
|
||||
pub action: u64,
|
||||
#[prost(string, tag = "11")]
|
||||
pub action_description: String,
|
||||
#[prost(uint64, tag = "12")]
|
||||
pub user_id: u64,
|
||||
#[prost(message, optional, tag = "13")]
|
||||
pub effect_config: Option<EffectConfig>,
|
||||
#[prost(string, tag = "14")]
|
||||
pub pop_str: String,
|
||||
#[prost(message, optional, tag = "15")]
|
||||
pub enter_effect_config: Option<EffectConfig>,
|
||||
#[prost(message, optional, tag = "16")]
|
||||
pub background_image: Option<Image>,
|
||||
#[prost(message, optional, tag = "17")]
|
||||
pub background_image_v2: Option<Image>,
|
||||
#[prost(message, optional, tag = "18")]
|
||||
pub anchor_display_text: Option<Text>,
|
||||
#[prost(message, optional, tag = "19")]
|
||||
pub public_area_common: Option<PublicAreaCommon>,
|
||||
#[prost(uint64, tag = "20")]
|
||||
pub user_enter_tip_type: u64,
|
||||
#[prost(uint64, tag = "21")]
|
||||
pub anchor_enter_tip_type: u64,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct EffectConfig {
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub r#type: u64,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub icon: Option<Image>,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub avatar_pos: u64,
|
||||
#[prost(message, optional, tag = "4")]
|
||||
pub text: Option<Text>,
|
||||
#[prost(message, optional, tag = "5")]
|
||||
pub text_icon: Option<Image>,
|
||||
#[prost(uint32, tag = "6")]
|
||||
pub stay_time: u32,
|
||||
#[prost(uint64, tag = "7")]
|
||||
pub anim_asset_id: u64,
|
||||
#[prost(message, optional, tag = "8")]
|
||||
pub badge: Option<Image>,
|
||||
#[prost(uint64, repeated, tag = "9")]
|
||||
pub flex_setting_array_list: Vec<u64>,
|
||||
#[prost(message, optional, tag = "10")]
|
||||
pub text_icon_overlay: Option<Image>,
|
||||
#[prost(message, optional, tag = "11")]
|
||||
pub animated_badge: Option<Image>,
|
||||
#[prost(bool, tag = "12")]
|
||||
pub has_sweep_light: bool,
|
||||
#[prost(uint64, repeated, tag = "13")]
|
||||
pub text_flex_setting_array_list: Vec<u64>,
|
||||
#[prost(uint64, tag = "14")]
|
||||
pub center_anim_asset_id: u64,
|
||||
#[prost(message, optional, tag = "15")]
|
||||
pub dynamic_image: Option<Image>,
|
||||
#[prost(map = "string, string", tag = "16")]
|
||||
pub extra_map: HashMap<String, String>,
|
||||
#[prost(uint64, tag = "17")]
|
||||
pub mp4_anim_asset_id: u64,
|
||||
#[prost(uint64, tag = "18")]
|
||||
pub priority: u64,
|
||||
#[prost(uint64, tag = "19")]
|
||||
pub max_wait_time: u64,
|
||||
#[prost(string, tag = "20")]
|
||||
pub dress_id: String,
|
||||
#[prost(uint64, tag = "21")]
|
||||
pub alignment: u64,
|
||||
#[prost(uint64, tag = "22")]
|
||||
pub alignment_offset: u64,
|
||||
}
|
||||
|
||||
// message PushFrame {
|
||||
// uint64 seqId = 1;
|
||||
// uint64 logId = 2;
|
||||
// uint64 service = 3;
|
||||
// uint64 method = 4;
|
||||
// repeated HeadersList headersList = 5;
|
||||
// string payloadEncoding = 6;
|
||||
// string payloadType = 7;
|
||||
// bytes payload = 8;
|
||||
// }
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct PushFrame {
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub seq_id: u64,
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub log_id: u64,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub service: u64,
|
||||
#[prost(uint64, tag = "4")]
|
||||
pub method: u64,
|
||||
#[prost(message, repeated, tag = "5")]
|
||||
pub headers_list: Vec<HeadersList>,
|
||||
#[prost(string, tag = "6")]
|
||||
pub payload_encoding: String,
|
||||
#[prost(string, tag = "7")]
|
||||
pub payload_type: String,
|
||||
#[prost(bytes, tag = "8")]
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
// message HeadersList {
|
||||
// string key = 1;
|
||||
// string value = 2;
|
||||
// }
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct HeadersList {
|
||||
#[prost(string, tag = "1")]
|
||||
pub key: String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub value: String,
|
||||
}
|
||||
13167
src-tauri/crates/danmu_stream/src/provider/douyin/webmssdk.js
Normal file
1
src-tauri/gen/schemas/acl-manifests.json
Normal file
1
src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
||||
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main","Live*","Clip*"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists",{"identifier":"fs:scope","allow":["**"]},"core:window:default","core:window:allow-start-dragging","core:window:allow-close","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-set-title","sql:allow-execute","shell:allow-open","dialog:allow-open","dialog:allow-save","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm",{"identifier":"http:default","allow":[{"url":"https://*.hdslb.com/"},{"url":"https://afdian.com/"},{"url":"https://*.afdiancdn.com/"},{"url":"https://*.douyin.com/"},{"url":"https://*.douyinpic.com/"}]},"dialog:default","shell:default","fs:default","http:default","sql:default","os:default","notification:default","dialog:default","fs:default","http:default","shell:default","sql:default","os:default","dialog:default","deep-link:default"]}}
|
||||
6686
src-tauri/gen/schemas/desktop-schema.json
Normal file
6686
src-tauri/gen/schemas/macOS-schema.json
Normal file
5498
src-tauri/gen/schemas/windows-schema.json
Normal file
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 8.8 KiB |