Compare commits

...

479 Commits

Author SHA1 Message Date
Xinrea
92ca2cddad fix: dependencies 2025-07-29 00:59:21 +08:00
Xinrea
3db0d1dfe5 feat: manual input model name (close #143) 2025-07-29 00:09:06 +08:00
Xinrea
57907323e6 bump version to 2.10.0 2025-07-27 19:52:53 +08:00
Xinrea
dbdca44c5f feat: deep-link support bsr:// 2025-07-27 19:51:58 +08:00
Xinrea
fe1dd2201f fix: prevent list corruption when deleting archived items 2025-07-26 22:52:45 +08:00
Xinrea
e0ae194cc3 bump version to 2.9.5 2025-07-26 22:40:50 +08:00
Xinrea
6fc5700457 ci/cd: add script to bump version 2025-07-26 22:40:49 +08:00
Xinrea
c4fdcf86d4 fix: bilibili stream pathway not update (close #117) 2025-07-26 22:40:46 +08:00
Xinrea
3088500c8d bump version to 2.9.4 2025-07-25 21:10:04 +08:00
Xinrea
861f3a3624 fix: tauri schema not handled by custom plugin for shaka-player 2025-07-25 21:09:41 +08:00
Xinrea
c55783e4d9 chore: update @tauri-apps/api 2025-07-25 20:13:04 +08:00
Xinrea
955e284d41 fix: start a new recording when header changes 2025-07-24 23:03:09 +08:00
Xinrea
fc4c47427e chore: adjust log level 2025-07-24 21:57:04 +08:00
Xinrea
e2d7563faa bump version to 2.9.3 2025-07-24 21:28:35 +08:00
Xinrea
27d69f7f8d fix: clip video cover not loaded 2025-07-24 21:28:10 +08:00
Xinrea
a77bb5af44 bump version to 2.9.2 2025-07-24 00:32:28 +08:00
Xinrea
00286261a4 fix: range offset caused by duration error 2025-07-24 00:23:58 +08:00
Xinrea
0b898dccaa fix: bilibili stream url extraction error caused 404 2025-07-23 22:27:57 +08:00
Xinrea
a1d9ac4e68 chore: remove ai generated docs 2025-07-23 21:56:56 +08:00
Xinrea
4150939e23 Only create records after successful ts download (#141)
* Defer record creation until first successful stream segment download

Co-authored-by: shenwuol <shenwuol@gmail.com>

* Checkpoint before follow-up message

* Improve recording logic with directory management and error handling

Co-authored-by: shenwuol <shenwuol@gmail.com>

* Add recorder flow diagrams for Bilibili and Douyin recorders

Co-authored-by: shenwuol <shenwuol@gmail.com>

* Refactor recorder update_entries to prevent empty records and directories

Co-authored-by: shenwuol <shenwuol@gmail.com>

* Refactor recorder update_entries to prevent empty records and dirs

Co-authored-by: shenwuol <shenwuol@gmail.com>

* Fix panic in non-FMP4 stream recording by safely handling entry store

Co-authored-by: shenwuol <shenwuol@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: shenwuol <shenwuol@gmail.com>
2025-07-23 17:35:00 +08:00
Xinrea
8f84b7f063 fix: import missing in headless build 2025-07-23 00:17:51 +08:00
Xinrea
04b245ac64 bump version to 2.9.1 2025-07-23 00:07:23 +08:00
Xinrea
12f7e62957 chore: remove unused code 2025-07-23 00:01:23 +08:00
Xinrea
9600d310c7 fix: 400-request-error on some douyin stream 2025-07-22 23:58:43 +08:00
Xinrea
dec5a2472a feat: douyin account information fetching (#140)
* Implement Douyin account info retrieval and auto-update

Co-authored-by: shenwuol <shenwuol@gmail.com>

* Refactor Douyin account API to use IM relation endpoint

Co-authored-by: shenwuol <shenwuol@gmail.com>

* Fix Douyin client error handling with correct error variant

Co-authored-by: shenwuol <shenwuol@gmail.com>

* Checkpoint before follow-up message

* Checkpoint before follow-up message

* Add id_str support for cross-platform account ID compatibility

Co-authored-by: shenwuol <shenwuol@gmail.com>

* Fix account update with deref for id_str comparison

Co-authored-by: shenwuol <shenwuol@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: shenwuol <shenwuol@gmail.com>
2025-07-22 23:35:49 +08:00
Xinrea
13eb7c6ea2 docs: update 2025-07-22 01:40:05 +08:00
Xinrea
2356cfa10a bump version to 2.9.0 2025-07-20 22:53:27 +08:00
Xinrea
3bfaefb3b0 doc: update 2025-07-20 22:53:03 +08:00
Xinrea
78b8c25d96 fix: ts entry timestamp 2025-07-20 22:52:54 +08:00
Xinrea
c1d2ff2b96 feat: ai agent (#139)
* feat: ai assistance

* feat: prompt for model configuration

* feat: agent optimize

* feat: archive subtitle

* feat: agent optimize

* fix: live preview callstack error

* chore: update dependencies

* fix: frontend hang with incorrect danmu ts

* fix: platform error handle

* fix: negative clip_offset

* fix: handle fs error
2025-07-20 20:50:26 +08:00
Xinrea
24aee9446a feat: using milisecond timestamp as sequence (#136)
* feat: using milisecond timestamp as sequence

* feat: log for douyin ts timestamp extract-error
2025-07-17 00:52:31 +08:00
Xinrea
2fb094ec31 bump version to 2.8.0 2025-07-15 22:52:46 +08:00
Xinrea
53897c66ee feat: whisper language setting (#134)
* feat: whisper language setting

* fix: set auto as default language setting
2025-07-15 22:38:31 +08:00
Xinrea
ca4e266ae6 feat: audio chunks for whisper (#133)
* feat: split audio into chunks for whisper

* fix: srt time

* fix: remove mp3 cover

* fix: chunks order on local whisper

* fix: improve error handling when reading chunk directory in video subtitle generation

* feat: prevent interruption of processes when using local Whisper

* fix: inconsistency of srt item formatting
2025-07-10 01:58:57 +08:00
Xinrea
6612a1e16f fix: code -352 for user/room api 2025-07-05 18:50:19 +08:00
Xinrea
55ceb65dfb fix: bili danmu (#132)
* fix: danmu message cmd

* bump version to 2.7.5
2025-07-03 01:42:48 +08:00
Xinrea
6cad3d6afb fix: crash with invalid cookie (#131) 2025-07-01 00:24:11 +08:00
Xinrea
151e1bdb8a fix: bilibili api buvid3 check 2025-06-28 21:13:54 +08:00
Xinrea
44a3cfd1ff bump version to 2.7.3 2025-06-26 00:48:16 +08:00
Xinrea
9cbc3028a7 fix: optimize whisper-online file size 2025-06-26 00:48:03 +08:00
Xinrea
8c30730d7b fix: whisper prompt for service 2025-06-26 00:19:23 +08:00
Xinrea
acfb870f9d fix: subtitle response in docker mode 2025-06-26 00:01:16 +08:00
Xinrea
3813528f50 feat: implement virtual scrolling for danmu list 2025-06-24 23:36:15 +08:00
Xinrea
e3bb014644 fix: crashed by loading large amount of cover data 2025-06-24 00:23:56 +08:00
Xinrea
76a7afde76 bump version to 2.7.0 2025-06-22 21:21:09 +08:00
Xinrea
1184f9f3f5 fix: auto close video preview after deleting 2025-06-22 21:20:04 +08:00
Xinrea
b754f8938f chore: clean up code 2025-06-22 21:15:03 +08:00
Xinrea
6b30ff04b7 feat: stt services support (#128)
* feat: add task page

* feat: online whisper service support (close #126)

* fix: blocking in recorder main loop
2025-06-22 21:06:35 +08:00
Xinrea
1c40acca63 feat: clip manage (#127)
* feat: ignore pre-release on version check

* feat: new clip-manage page

* chore: rename custom scrollbar class
2025-06-22 00:32:39 +08:00
Xinrea
a5a7a8afaf fix: douyin danmu 2025-06-20 00:50:37 +08:00
Xinrea
583ac13a37 chore: update dependencies 2025-06-19 23:35:11 +08:00
Xinrea
3e58972072 fix: recorder re-added when removing 2025-06-19 00:31:11 +08:00
Xinrea
f15aa27727 fix: release build with cuda cache error 2025-06-19 00:29:22 +08:00
Xinrea
2581014dbd fix: dockerfile 2025-06-19 00:18:16 +08:00
Xinrea
baaaa1b57e feat: randomly use multiple accounts (close #123) 2025-06-19 00:11:01 +08:00
Xinrea
160fbb3590 feat: collect frontend log to backend (close #122) 2025-06-18 22:49:07 +08:00
Xinrea
6f3253678c feat: readonly mode (#121)
* feat: readonly mode (close #112)

* feat: check free-space when clipping
2025-06-18 01:09:58 +08:00
Xinrea
563ad66243 feat: danmu local offset settings (close #115) (#120) 2025-06-16 23:38:24 +08:00
Xinrea
a8d002cc53 fix: deadlock removing douyin-recorder 2025-06-13 00:29:29 +08:00
Xinrea
0615410fa4 fix: deadlock removing bili-recorder 2025-06-13 00:15:59 +08:00
Xinrea
fc98e065f8 fix: status-check interval not work for bilibili recorder 2025-06-12 23:35:24 +08:00
Xinrea
66f671ffa0 feat: douyin danmu (#119)
* feat: migrate to uniformed interface for danmu stream

* feat: douyin danmu support (close #113)

* chore: fix typo

* fix: loop-decompress body
2025-06-12 01:00:33 +08:00
Xinrea
69a35af456 fix: recorder adding back when removing (close #114)
Add a to_remove-set to filter recorders that are still in removing-stage,
so that monitor-thread wouldn't add them back.
2025-06-08 10:37:04 +08:00
Xinrea
e462bd0b4c feat: simplified room control mechanism (close #111) 2025-06-08 10:22:20 +08:00
Xinrea
ae6483427f bump version to 2.6.0 2025-06-03 21:53:14 +08:00
Xinrea
ad97677104 feat: configuration for status check interval 2025-05-30 00:32:30 +08:00
Xinrea
996d15ef25 chore: adjust ffmpeg log level & clean up code 2025-05-29 01:18:27 +08:00
Xinrea
06de32ffe7 bump version to 2.5.9 2025-05-27 15:13:45 +08:00
Xinrea
dd43074e46 fix: danmu api missing param 2025-05-27 15:13:22 +08:00
Xinrea
93495e13db bump version to 2.5.8 2025-05-27 01:49:02 +08:00
Xinrea
16950edae4 fix: danmu api wbi_sign required 2025-05-27 01:48:38 +08:00
Xinrea
4af1203360 fix: get logfile size on unix and windows 2025-05-24 18:04:51 +08:00
Xinrea
55b5bd1fd2 fix: metadata on windows 2025-05-24 17:00:09 +08:00
Xinrea
f0a7cf4ed0 bump version to 2.5.7 2025-05-24 15:55:02 +08:00
Xinrea
62e7412abf feat: new stream recording control 2025-05-24 15:54:06 +08:00
Xinrea
275bf647d2 feat: add fail count to avoid connection reject 2025-05-24 15:15:28 +08:00
Xinrea
00af723be9 refactor: stream update 2025-05-24 14:05:23 +08:00
Xinrea
19da577836 chore: add logs for recorder control handler 2025-05-24 12:57:24 +08:00
Xinrea
bf3a2b469b feat: implement log rotation on startup 2025-05-20 23:15:37 +08:00
Xinrea
bf31bfd099 feat: add links on release list 2025-05-20 22:26:47 +08:00
Xinrea
d02fea99f2 refactor: sidebar items 2025-05-20 22:17:49 +08:00
Xinrea
2404bacb4e fix: force switching to new stream when error (close #106) 2025-05-15 14:54:01 +08:00
Xinrea
b6c274c181 fix: adjust date-time-adding rule in manifest 2025-05-15 14:43:47 +08:00
Xinrea
f9b472aee7 fix: wrong ts comparison when clip (close #105) 2025-05-15 14:43:31 +08:00
Xinrea
45f277741b fix: entry timestamp to date str 2025-05-15 01:28:00 +08:00
Xinrea
94179f59cd bump version to 2.5.6 2025-05-15 01:07:37 +08:00
Xinrea
c7b550a3e3 fix: stuck when clipping douyin live 2025-05-15 01:07:36 +08:00
Xinrea
fd51fd2387 chore: adjust ffmpeg log level 2025-05-14 16:39:29 +08:00
Xinrea
23d1798ab6 fix: panic on recorder monitor thread 2025-05-14 16:37:47 +08:00
Xinrea
90e81d0d4d bump verstion to 2.5.5 2025-05-13 15:41:09 +08:00
Xinrea
6a7a19547d fix: invoke error message 2025-05-13 14:24:02 +08:00
Xinrea
1550849ee2 fix: ffmpeg path on windows 2025-05-13 13:37:03 +08:00
Xinrea
15116e2197 chore: add debug log for all ffmpeg task 2025-05-13 12:05:04 +08:00
Xinrea
63eda5179b bump version to 2.5.4 2025-05-08 21:06:34 +08:00
Xinrea
d7b1277363 feat: add douyin cookie documentation 2025-05-08 21:05:20 +08:00
Xinrea
337c933b92 chore: add logs for add recorder 2025-05-08 20:45:17 +08:00
Xinrea
b01b2cc9c0 fix: only provide date-time when discontinuity for douyin stream 2025-05-08 20:42:17 +08:00
Xinrea
30069b2f33 bump version to 2.5.3 2025-05-08 01:07:56 +08:00
Xinrea
c5bd57468c fix: failed to encode subtitle after manual-add 2025-05-08 01:06:52 +08:00
Xinrea
c050c65675 fix: danmu encode offset 2025-05-08 00:50:16 +08:00
Xinrea
e1bd7e7563 fix: task progress not updated 2025-05-08 00:32:21 +08:00
Xinrea
cc129f6384 fix: configuration not saved 2025-05-07 23:17:00 +08:00
Xinrea
e7ea0c0ff0 fix: preview on range select 2025-05-07 22:17:07 +08:00
Xinrea
9630d51c4c fix: provide date time on every segment 2025-05-07 22:17:07 +08:00
Xinrea
ceb140a4c2 Update src-tauri/src/recorder/entry.rs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-07 22:17:07 +08:00
Xinrea
fe8410ab98 Update src-tauri/src/recorder/bilibili.rs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-07 22:17:07 +08:00
Xinrea
00731cda93 refactor: manifest handled by entry store 2025-05-07 22:17:07 +08:00
Xinrea
c05979cb11 fix: panic when config path not exists 2025-05-06 20:03:04 +08:00
Xinrea
6e1a10e45c bump version to 2.5.1 2025-05-04 09:56:36 +08:00
Xinrea
bd74dfdb26 fix: preview stall when replacing expired stream (close #100) 2025-05-04 09:56:02 +08:00
Xinrea
b7c2fd3387 fix(web): cannot preview stream in default setting (close #101) 2025-05-04 09:29:36 +08:00
Xinrea
b65e41ca23 fix(web): delete archive 2025-05-04 09:07:00 +08:00
Xinrea
ec70eded14 docs: update 2025-05-03 23:41:51 +08:00
Xinrea
dcf9047d82 ci/cd: build package on tags push 2025-05-03 22:12:15 +08:00
Xinrea
cd85e9f65a feat: update felgens 2025-05-03 19:19:41 +08:00
Xinrea
066fd4fb77 bump version to 2.5.0 2025-05-03 18:33:41 +08:00
Xinrea
9a6bb30e73 fix(bilibili): not fetching real room for danmu 2025-05-03 18:25:28 +08:00
Xinrea
99d9f27618 fix: stream preview in tauri mode 2025-05-03 17:56:59 +08:00
Xinrea
02ddac6b17 docs: update 2025-05-02 17:30:10 +08:00
Xinrea
017438ee50 fix(http_server): using event_id from task creator 2025-05-02 11:16:55 +08:00
Xinrea
d938982107 feat: add clip download button in web mode 2025-05-02 10:44:06 +08:00
Xinrea
bdde1969f7 docs: fix header image 2025-05-02 10:43:48 +08:00
Xinrea
c8eb038190 fix(bilibili): force update for invalid index 2025-05-01 20:29:44 +08:00
Xinrea
2d90b79f73 fix(http_server): body size limit 2025-05-01 20:29:22 +08:00
Xinrea
f39d3baff5 fix(ffmpeg): add font in runtime for danmu encoding 2025-05-01 20:27:17 +08:00
Xinrea
84664ee272 fix(ffmpeg): auto create output folder 2025-05-01 16:49:17 +08:00
Xinrea
d603216baf fix(ci/cd): runtime env PATH 2025-05-01 16:13:10 +08:00
Xinrea
522873c7fb fix(utils): disk info on linux 2025-05-01 15:57:50 +08:00
Xinrea
a6548f9941 fix(ci/cd): runtime certificates 2025-05-01 15:48:49 +08:00
Xinrea
3843dd88b2 fix(utils): disk info from relative cache path 2025-05-01 15:34:04 +08:00
Xinrea
baddb4e9d4 feat: minimal runtime 2025-05-01 03:02:45 +08:00
Xinrea
4aa51b51bd feat: avoid tauri in headless 2025-05-01 01:51:00 +08:00
Xinrea
725494db7d feat: adjust dependencies 2025-05-01 01:24:07 +08:00
Xinrea
292caa4158 fix(utils): cache folder info on linux 2025-05-01 00:39:39 +08:00
Xinrea
29e9656919 fix(http_server): sse message contains invalid characters 2025-04-30 22:03:06 +08:00
Xinrea
78f4682efb fix(bilibili): invalid stream that needs redirect 2025-04-30 22:00:17 +08:00
Xinrea
fa090b0b66 fix: danmu offset 2025-04-30 21:46:29 +08:00
Xinrea
32b7e9c3c2 fix: file export in web 2025-04-30 21:33:09 +08:00
Xinrea
4d3e069a81 ci/cd: static ffmpeg build in runtime 2025-04-30 18:29:33 +08:00
Xinrea
3ed658a31c ci/cd: remove check 2025-04-30 17:23:20 +08:00
Xinrea
efb24798c8 ci/cd: minimal runtime 2025-04-30 17:22:53 +08:00
Xinrea
e72e9027ef feat: download ffmpeg instead installing in runtime 2025-04-30 16:21:14 +08:00
Xinrea
17c93fb716 fix(ci/cd): pages build 2025-04-30 11:26:45 +08:00
Xinrea
a826666ad6 docs: add pages 2025-04-30 11:24:44 +08:00
Xinrea
c8282cb66f fix(ci/cd): add dependencies 2025-04-30 10:33:13 +08:00
Xinrea
592fd3940e ci/cd: add auto clippy check 2025-04-30 10:27:04 +08:00
Xinrea
7e9980b098 refactor: clean up code 2025-04-30 10:22:11 +08:00
Xinrea
283ee06034 fix: url open in web 2025-04-30 09:35:06 +08:00
Xinrea
9a00693bb3 ci/cd: ffmpeg in runtime 2025-04-30 03:42:19 +08:00
Xinrea
16906a46cd doc: update README 2025-04-30 03:36:55 +08:00
Xinrea
bdf017024a fix: runtime dependencies 2025-04-30 03:02:08 +08:00
Xinrea
58ae1ef426 fix: dockerfile 2025-04-30 02:51:07 +08:00
Xinrea
98e6544c25 fix: ci/cd workflow 2025-04-30 02:33:43 +08:00
Xinrea
1b57beeea6 ci/cd: docker package build 2025-04-30 02:30:52 +08:00
Xinrea
1625a5f889 fix: using static shaka-player lib 2025-04-30 02:30:52 +08:00
Xinrea
ae20e7fad7 fix: provide codecs master manifest 2025-04-30 02:30:52 +08:00
Xinrea
fc594b12e0 Revert "feat: switch bilibili stream to TS for compatibility"
This reverts commit 7a22637a7a.
2025-04-30 02:30:52 +08:00
Xinrea
0d25f32101 feat: event listen 2025-04-30 02:30:52 +08:00
Xinrea
cfd4522036 fix: panic when cancel none-existed event 2025-04-30 02:30:52 +08:00
Xinrea
f638d4aee0 fix: hls from endpoint 2025-04-30 02:30:52 +08:00
Xinrea
b237b78300 fix: danmu offset in web 2025-04-30 02:30:52 +08:00
Xinrea
ed2983c073 feat: settings i web env 2025-04-30 02:30:52 +08:00
Xinrea
730227ac45 fix: hls in tauri 2025-04-30 02:30:52 +08:00
Xinrea
7fb4f41f01 feat: switch bilibili stream to TS for compatibility 2025-04-30 02:30:52 +08:00
Xinrea
d92e013413 fix: ranged hls content 2025-04-30 02:30:52 +08:00
Xinrea
980fd145d0 feat: hls server 2025-04-30 02:30:52 +08:00
Xinrea
693734e12a fix: backend fetch 2025-04-30 02:30:52 +08:00
Xinrea
cbeae9b40d fix: handlers in tauri mode 2025-04-30 02:30:52 +08:00
Xinrea
4d0cc2c3b6 refactor: add extra layer for invoke 2025-04-27 23:02:03 +08:00
Xinrea
5dac578389 ci/cd: add rust cache 2025-04-27 19:05:58 +08:00
Xinrea
710718bcd6 feat: add thread to retry creating recorder 2025-04-27 19:02:28 +08:00
Xinrea
21b4914817 fix: shortcut triggered by input (close #97) 2025-04-27 12:07:17 +08:00
Xinrea
77dde29f8d ci/cd: test build cpu version 2025-04-27 10:24:30 +08:00
Xinrea
20e11c48ff bump version to 2.4.2 2025-04-24 19:31:23 +08:00
Xinrea
dbe47aa223 fix: webid error & bili account requirement (close #96) 2025-04-24 19:30:07 +08:00
Xinrea
b916553619 bump version to 2.4.1 2025-04-18 22:47:26 +08:00
Xinrea
d752a0260b fix(bilibili): pause current record not working (close #95) 2025-04-18 21:05:01 +08:00
Xinrea
97cf7b1420 doc: update 2025-04-18 15:38:45 +08:00
Xinrea
50e2377efb feat: update shaka-player to v4.11.21 2025-04-18 15:15:31 +08:00
Xinrea
940cfcd9ed bump version to 2.4.0 2025-04-18 02:31:55 +08:00
Xinrea
cfd36cdaf3 fix(bilibili): update felgen to support none-standard msg cmd (close #84) 2025-04-18 02:31:11 +08:00
Xinrea
d3133388d9 fix: douyin preview stall after interrupt 2025-04-18 02:17:15 +08:00
Xinrea
05bfe68e92 chore: adjust update entries warning log 2025-04-18 01:14:16 +08:00
Xinrea
d31aa51cf0 chore: clean up code 2025-04-17 19:09:34 +08:00
Xinrea
6d8918f431 fix: reset after status change notify 2025-04-17 19:04:11 +08:00
Xinrea
a5b6856b81 feat: tag for live status 2025-04-17 15:57:31 +08:00
Xinrea
68d9d7801a feat: auto generate whole live clip (close #79) 2025-04-17 15:51:34 +08:00
Xinrea
f51934d02a fix: danmu in ranged preview 2025-04-17 02:27:18 +08:00
Xinrea
21a702e716 feat: generate clip by local m3u8 index file 2025-04-17 02:20:36 +08:00
Xinrea
418f799b64 feat: relative path in m3u8 index 2025-04-17 01:51:23 +08:00
Xinrea
185ef743b5 feat: using the same version of shaka-player
related commit:
1fde087f7b
2025-04-17 01:16:39 +08:00
Xinrea
54da9b3cf4 feat: remove hls server (close #93)
Registered a custom shaka-player network plugin
to load data through tauri invoke instead of
maintaining a backend hls server.
2025-04-17 01:12:56 +08:00
Xinrea
b5fed447dd feat: adjust export button icon 2025-04-16 16:49:41 +08:00
Xinrea
9b88a21338 feat: remove setting of primary account (close #94) 2025-04-16 16:34:15 +08:00
Xinrea
0c82cd4285 fix: ignore corrupted segment 2025-04-16 16:04:01 +08:00
Xinrea
22e6c5338a feat: danmu export (close #83) 2025-04-16 15:58:27 +08:00
Xinrea
8e4e3b5aa9 feat: encode danmu when clip (close #80) 2025-04-16 13:25:41 +08:00
Xinrea
4b57b56189 fix: fetch original stream for douyin (close #77, close #91) 2025-04-15 01:13:17 +08:00
Xinrea
77eab5e80c chore: adjust logs 2025-04-14 21:47:38 +08:00
Xinrea
15dac35d31 fix: adjust preview reload interval 2025-04-14 21:44:49 +08:00
Xinrea
d84c03c93d fix: range persistance for live 2025-04-14 21:41:22 +08:00
Xinrea
7e417c5bbe feat: alert for ffmpeg missing (close #86) 2025-04-08 01:20:15 +08:00
Xinrea
fe2471553d feat: persisted range selection (close #87) 2025-04-08 00:40:41 +08:00
Xinrea
3e2a3cc4fd chore: remove vscode configuration 2025-04-07 23:45:41 +08:00
Xinrea
b9830f336e chore: update dependencies (close #88) 2025-04-07 19:25:51 +08:00
Xinrea
782c339c33 fix: wrong live URL for douyin lives (close #81) 2025-04-07 04:20:12 +08:00
Xinrea
2c60a7d987 chore: introduce tiny model for testing 2025-04-03 00:14:50 +08:00
Xinrea
0e6f53a343 fix: panic when error on whisper init 2025-04-02 23:27:16 +08:00
Xinrea
b0a85654ab bump version to 2.3.0 2025-04-01 16:49:40 +08:00
Xinrea
8b6b5d2695 chore: adjust log target and level 2025-04-01 15:59:15 +08:00
Xinrea
1fde087f7b fix: douyin stream preview stall
Using a separate version of shaka-player to avoid stalls on the special stream,
which is mixed with raw AAC data segments.

shaka-player has made some fixes on this:
https://github.com/shaka-project/shaka-player/pull/6360

However, in recent versions, this bug seems to have reappeared,
so we changed to use an older version of shaka-player for the
douyin stream to avoid this.
2025-04-01 14:51:18 +08:00
Xinrea
82b41c4e81 fix: freezed m3u8 for douyin live 2025-04-01 12:27:02 +08:00
Xinrea
d0f96b6193 fix: clip from hls to avoid corrupted output (close #74) 2025-04-01 12:11:10 +08:00
Xinrea
8a8bbf8bca fix: range m3u8 not end when live 2025-04-01 11:59:39 +08:00
Xinrea
0430988482 fix: wait for ffmpeg process 2025-04-01 04:28:02 +08:00
Xinrea
781ce70210 fix: transcode progress 2025-04-01 04:03:32 +08:00
Xinrea
fcca33e908 fix: enable shaka player low-latency-mode 2025-04-01 01:42:36 +08:00
Xinrea
52b74d70c9 feat: preview on range (close #57) 2025-04-01 01:21:39 +08:00
Xinrea
6d180a671d feat: rebuild archives from caches (close #64) 2025-03-31 23:26:51 +08:00
Xinrea
583185911b feat: show archives in time desc order 2025-03-31 21:38:02 +08:00
Xinrea
549f79edd0 refactor: bundle clip params 2025-03-31 21:31:49 +08:00
Xinrea
30894ac172 Create LICENSE 2025-03-31 21:18:19 +08:00
Xinrea
62a2c63f9d feat: play as vod when paused (close #63) 2025-03-31 19:49:16 +08:00
Xinrea
0aef85998f feat: progress feedback from ffmpeg operation 2025-03-31 19:44:19 +08:00
Xinrea
3bb215ae94 ci/cd: update local build script 2025-03-31 01:52:07 +08:00
Xinrea
7d9428de14 feat: bundle ffmpeg for installer on windows 2025-03-30 23:55:05 +08:00
Xinrea
c85c9206f6 doc: update 2025-03-30 23:00:27 +08:00
Xinrea
2b986c16b8 fix: PATH env on macOS (close #76) 2025-03-30 17:40:22 +08:00
Xinrea
a5ae622a30 feat: add error log for audio extraction 2025-03-30 16:33:58 +08:00
Xinrea
b3a724b22e ci/cd: add a script for windows local build 2025-03-30 16:32:09 +08:00
Xinrea
7f96dcd2bf ci/cd: only supply cuda version due to cpu issue 2025-03-30 14:36:28 +08:00
Xinrea
534cf1e3c9 feat: add progress log for whisper 2025-03-30 12:20:25 +08:00
Xinrea
3ec3f5d23c ci/cd: build debug version 2025-03-30 09:50:34 +08:00
Xinrea
2241b9b936 fix: time markers not updated when scale changes 2025-03-30 02:18:11 +08:00
Xinrea
e8fd6a81be fix: ffmpeg subtitles path on windows 2025-03-30 02:11:57 +08:00
Xinrea
8b4672b202 feat: better subtitles shadow preview 2025-03-30 01:30:15 +08:00
Xinrea
9734041777 feat: change default subtitles font size to 18 2025-03-30 01:19:08 +08:00
Xinrea
e15fe67790 fix: timeline marker gap width 2025-03-30 01:18:16 +08:00
Xinrea
937a5996bf fix: duplicated danmu & disable danmu not working 2025-03-30 01:08:42 +08:00
Xinrea
d6c87e8be7 feat: add clear subtitles button 2025-03-30 00:02:33 +08:00
Xinrea
606e29f56b feat: add config for whisper prompt 2025-03-29 23:46:21 +08:00
Xinrea
45d5ad04eb Merge pull request #67 from Xinrea/fix/cicd-workflow
fix/cicd-workflow
2025-03-29 20:34:22 +08:00
Xinrea
85c4dcb4a5 feat: update modal download link 2025-03-29 20:32:28 +08:00
Xinrea
b803857d65 feat: add whisper init prompt to use simplified chinese 2025-03-29 20:13:38 +08:00
Xinrea
8b029410cb feat: add time cost to whisper 2025-03-29 20:01:28 +08:00
Xinrea
33cdbc1552 ci/cd: default build without CUDA 2025-03-29 18:49:05 +08:00
Xinrea
67c125faaf doc: add CONTRIBUTING 2025-03-29 18:44:45 +08:00
Xinrea
d21db4b220 ci/cd: differ cuda installer 2025-03-29 14:49:51 +08:00
Xinrea
7bbf0eb566 ci/cd: debug for rc version 2025-03-29 14:46:41 +08:00
Xinrea
2eeb7a28fe feat: openblas for win 2025-03-29 11:19:01 +08:00
Xinrea
6ae39414f4 ci/cd: fix release workflow 2025-03-29 09:18:16 +08:00
Xinrea
6a22769199 bump version to 2.2.0 2025-03-29 00:23:11 +08:00
Xinrea
8b12886422 Merge pull request #66 from Xinrea/feat/custom-filename
feat/custom-filename
2025-03-29 00:21:06 +08:00
Xinrea
bde723b9bd fix: lastupdate not reset with new stream 2025-03-29 00:19:02 +08:00
Xinrea
0f3720296a fix: clip deletion when preview 2025-03-29 00:18:55 +08:00
Xinrea
df9c99d596 feat: custom filename (close #56) 2025-03-29 00:18:31 +08:00
Xinrea
864a7c38b6 Merge pull request #65 from Xinrea/feat/subtitles-generator
feat/subtitles-generator
2025-03-29 00:15:40 +08:00
Xinrea
edadf0b6b5 feat: subtitle editor and encode 2025-03-29 00:00:06 +08:00
Xinrea
54ba265510 feat: general subtitle generator interface 2025-03-27 10:48:11 +08:00
Xinrea
dc44327d4c bump version to 2.1.2 2025-03-27 00:58:48 +08:00
Xinrea
309b6449e6 Merge pull request #62 from Xinrea/fix/stream-expired
fix: stream update when expire
2025-03-27 00:57:19 +08:00
Xinrea
7dd5b2fb8d fix: stream update when expire 2025-03-27 00:37:08 +08:00
Xinrea
f32147658f feat: replace feedback url to group invite link 2025-03-27 00:27:11 +08:00
Xinrea
ba511d37ef chore: update issue template 2025-03-27 00:18:18 +08:00
Xinrea
4fa595547f Merge pull request #60 from Xinrea/fix/clip-error
fix/clip error
2025-03-27 00:07:21 +08:00
Xinrea
72282b180f bump version to 2.1.1 2025-03-26 20:58:45 +08:00
Xinrea
813686d66d fix: using PathBuf to handle paths 2025-03-26 20:54:54 +08:00
Xinrea
f03369719d refactor: clean some warnings 2025-03-26 20:53:08 +08:00
Xinrea
c639b012bc feat: more log about clip error 2025-03-25 22:13:46 +08:00
Xinrea
4144ce96c0 feat: button for opening log folder 2025-03-25 21:46:42 +08:00
Xinrea
38eed8b66e bump version to 2.1.0 2025-03-25 21:38:47 +08:00
Xinrea
0315bfae0c Merge pull request #55 from Xinrea/feat/progress-display
feat/progress_display
2025-03-25 21:26:33 +08:00
Xinrea
305b87265c feat: cancel upload procedure 2025-03-25 21:23:04 +08:00
Xinrea
847a790132 feat: upload request retry 2025-03-25 21:23:04 +08:00
Xinrea
ab27798283 feat: progress display for clip and post 2025-03-25 21:23:04 +08:00
Xinrea
4b521f775c Merge pull request #54 from Xinrea/feat/new_version_hint
feat: new version hint
2025-03-25 21:22:27 +08:00
Xinrea
0f68ec0677 feat: new version hint 2025-03-25 21:19:24 +08:00
Xinrea
ed4409661f Merge pull request #53 from Xinrea/fix/disk_info
fix: disk info on macOS (close #52)
2025-03-25 19:23:33 +08:00
Xinrea
789d56b1a4 fix: disk info on macOS (close #52) 2025-03-25 19:21:50 +08:00
Xinrea
6d6c3ddcb5 Merge pull request #51 from Xinrea/feat/auto-recording-switch
feat: recording control for rooms (close #42)
2025-03-25 19:19:50 +08:00
Xinrea
c4441029f8 feat: manual start/pause for current live 2025-03-25 10:54:19 +08:00
Xinrea
4285110233 feat: icon for auto-record 2025-03-25 02:41:07 +08:00
Xinrea
4ad33a7ea8 fix: recording control for douyin streams 2025-03-25 02:11:19 +08:00
Xinrea
b41c800bb0 feat: recording control for rooms (close #42) 2025-03-25 01:56:51 +08:00
Xinrea
c4592d5ca6 bump version to 2.0.3 2025-03-24 19:31:53 +08:00
Xinrea
5e9dba5d58 Merge pull request #49 from Xinrea/fix/outdated_stream
fix: outdated bilibili stream (close #44)
2025-03-24 19:06:58 +08:00
Xinrea
2cfd140d4a fix: outdated bilibili stream (close #44)
之前当直播流获取内容出现错误时,会尝试重新获取直播间状态,当检测到直播间关闭则会放弃继续尝试;
而如果直播间快速关播开播,原直播流虽然无新内容会触发Error::FreezedStream,但是由于流未过期,
check_status 不会更新直播流,导致 recorder 无法切换到新的直播流。

更改后,将流过期的判断放在 update_entries 中,如果遇到任何错误,便会尝试重新获取新的流地址。
2025-03-24 19:06:00 +08:00
Xinrea
13e421bfba Merge pull request #47 from Xinrea/fix/macos-crash
fix: macOS crash caused by log and ffmpeg_sidecar (close #45)
2025-03-24 12:47:38 +08:00
Xinrea
1933727f89 fix: macOS crash caused by log and ffmpeg_sidecar (close #45) 2025-03-24 12:42:38 +08:00
Xinrea
4e16bfcc18 Merge pull request #41 from Xinrea/fix/infinite-nested-dirs
fix: infinite nested dirs
2025-03-17 20:52:30 +08:00
Xinrea
398ee831de bump version to 2.0.2 2025-03-17 20:51:21 +08:00
Xinrea
6d01280039 fix: infinite nested dirs when change output/cache folder 2025-03-17 20:50:16 +08:00
Xinrea
8aaa701348 bump version to 2.0.1 2025-03-15 13:11:03 +08:00
Xinrea
733a36571b feat: provide more detailed error information for 'add-room' failures (close #38) 2025-03-15 13:06:38 +08:00
Xinrea
f40ac28781 fix: account removal 2025-03-15 13:00:57 +08:00
Xinrea
d73e95d2e5 feat: cache migration 2025-03-14 11:31:04 +08:00
Xinrea
770338a68a fix: auto-close for donate modal 2025-03-13 11:36:17 +08:00
Xinrea
f58dafbde8 fix: video frame seek on windows 2025-03-13 01:30:35 +08:00
Xinrea
004712e851 feat: fix bilibili clip 2025-03-13 01:22:37 +08:00
Xinrea
4e53ed2cf8 fix: work dir path on windows 2025-03-13 01:12:33 +08:00
Xinrea
92ccad6253 fix: enable window decoration on windows 2025-03-13 01:11:29 +08:00
Xinrea
74c5e9bb09 Merge pull request #36 from Xinrea/feat/new-ui
v2.0.0 版本功能更新
2025-03-13 00:29:37 +08:00
Xinrea
2724d6b4d3 doc: update 2025-03-13 00:15:19 +08:00
Xinrea
212e144422 refactor: adjust player init order 2025-03-13 00:04:56 +08:00
Xinrea
205a1b82e7 bump version to 2.0.0 2025-03-12 23:51:51 +08:00
Xinrea
44b4604581 feat: add checks before deleting archive 2025-03-12 23:48:21 +08:00
Xinrea
3d3454b5a4 feat: advanced cover editor and donate page 2025-03-12 23:36:54 +08:00
Xinrea
67f1b04b67 feat: douyin support 2025-03-12 19:39:32 +08:00
Xinrea
fd7d299e55 refactor: decouple recorder 2025-03-10 19:21:59 +08:00
Xinrea
ada492f3f0 refactor: new ui 2025-03-10 19:21:43 +08:00
Xinrea
8a4e4fd32b fix: using first account when primary missing
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-23 00:52:57 +08:00
Xinrea
86ced2a217 release: bump version to 1.4.2
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-18 02:22:34 +08:00
Xinrea
c62251dfe9 fix(livewindow): adjust titlebar index to avoid blocking buttons
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-18 02:21:42 +08:00
Xinrea
8bf0f5d36e fix(recorder_manager): unable to remove recorder that has no cache folder created yet
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-18 02:10:02 +08:00
Xinrea
a4b6567947 feat(main, recorder): remove unused ffmpeg dependency
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-18 02:10:02 +08:00
Xinrea
6c5c628bbf fix(recorder): reconnect danmu ws after disconnection
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-18 02:10:02 +08:00
Xinrea
1d6593340d fix(account,room): duplicated avatar when adding/removing items
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-03 01:04:08 +08:00
Xinrea
0d992d205f release: bump version to 1.4.1
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-02 00:39:08 +08:00
Xinrea
f82e79efd4 Merge pull request #32 from Xinrea/feat/quick-stream-switch
feat(player): add shortcut button to navigate to other live rooms
2024-12-02 00:36:49 +08:00
Xinrea
5cdb6b6f75 feat(player): add shortcut button to navigate to other live rooms (close #31)
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-02 00:36:22 +08:00
Xinrea
7316a022be feat(player): enable player lowlatency mode
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-02 00:32:31 +08:00
Xinrea
dbd8a29b73 feat(recorder): dynamic entry update interval
Sometimes entry update cost a lot of time (caused by timeout or something),
which makes the actually interval much larger than 1s.

In this commit, entry update interval is ranged from 0ms to 500ms, based on
the time cost by last entry-updating.

Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-02 00:28:40 +08:00
Xinrea
d6a5a02d68 fix(recorder): optimize lock range and download sequence for entries
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-01 21:58:53 +08:00
Xinrea
9eef00b913 fix(bilibili): adjust global timeout to 10s
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-01 21:10:57 +08:00
Xinrea
3b9dd4824b feat: migrate tauri from rc to release
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-01 19:01:41 +08:00
Xinrea
902c1ad39e chore: add feature issue template 2024-12-01 03:28:20 +08:00
Xinrea
b4f6dea97f chore: add bug issue template 2024-12-01 03:20:36 +08:00
Xinrea
c1c252f54a release: bump version to 1.4.0
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-01 01:29:59 +08:00
Xinrea
303b5f8847 Merge pull request #30 from Xinrea/feat/danmu-statistic
feat(player): add danmu statistics graph on top of seek bar (close #17)
2024-12-01 01:28:38 +08:00
Xinrea
26f55a463b feat(player): add danmu statistics graph on top of seek bar (close #17)
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-01 01:15:05 +08:00
Xinrea
0fa2c366dc fix(recorder): add retry for entry downloading to avoid gaps
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-12-01 01:14:03 +08:00
Xinrea
bf1588e414 fix(recorder): potential panic when reopen live window
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-30 21:53:21 +08:00
Xinrea
3be0f25dfc fix(bilibili): add extra info when create index url to avoid 403
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-30 20:49:27 +08:00
Xinrea
0e53028922 fix(recorder_manager): always return error when deleting archive
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-29 02:14:57 +08:00
Xinrea
bc458647a3 release: bump version to 1.3.1
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-26 23:16:42 +08:00
Xinrea
6c5080394a refactor: address clippy warnings
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-26 16:24:57 +08:00
Xinrea
30d45ca2c3 refactor(database): modularize database implementation
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-26 15:58:46 +08:00
Xinrea
614aa3184f fix(player): use 'p' as marker shortcut to avoid conflicts (close #29)
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-26 15:36:28 +08:00
Xinrea
cacd28bd87 release: bump version to 1.3.0
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-26 00:54:26 +08:00
Xinrea
a3dfe86a04 doc: update livewindow description in README
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-26 00:51:23 +08:00
Xinrea
79c65cab63 Merge pull request #28 from Xinrea/feat/timepoint-mark
时间点标记功能
2024-11-26 00:42:47 +08:00
Xinrea
52237b9385 feat(livewindow): add timepoint mark support
By the way, livewindow style change to dark theme.

Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-26 00:38:21 +08:00
Xinrea
b8d22e92ff feat(livewindow): add collapse button for post panel
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-25 14:56:08 +08:00
Xinrea
faac0e29b5 fix(recorder): prevent deletion of current stream archive
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-25 02:11:46 +08:00
Xinrea
899afac910 feat(livewindow): support selection of video post area (close #20)
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-25 02:10:53 +08:00
Xinrea
7b2cbfefcc fix(recorder): safely stop recorder when removing room
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-25 01:57:20 +08:00
Xinrea
793e532240 feat(bilibili): add video type list api
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-24 21:47:10 +08:00
Xinrea
68c7bd251e refactor(bilibili): move response struct into mod response
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-24 20:39:21 +08:00
Xinrea
656f82df43 release: bump version to 1.2.1
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-23 12:49:04 +08:00
Xinrea
1571358f13 fix(recorder): live is mistakenly marked as online when client error (close #27)
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-23 12:45:57 +08:00
Xinrea
b6326cf36a fix(recorder): add error to handle slow stream
BiliBili stream's index content maybe lagging behind real-time content,
potentially caused by cdn issue. We need to switch into a new stream
in this situation.

Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-23 12:35:28 +08:00
Xinrea
0b7818a9d3 fix(recorder): handle freezed stream
Sometimes the stream index content may stop updating after re-pushing stream,
but it is still available. In this situation, we need to fetch a new stream.

Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-23 01:05:26 +08:00
Xinrea
b479d6086b feat(recorder): update current cached stream length with acurrate value
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-23 00:48:16 +08:00
Xinrea
6d06491ddc fix(recorder): manually construct clip instead of using ffmpeg (close #25)
Too many segments will make ffmpeg command spawn with long argument, which might
be exceeded the limit.
Now we manually copy and write segments into one file to generate a clip.
FFMPEG is still remained but not used for now, in the future clip encoding might
need this.

Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-22 22:12:32 +08:00
Xinrea
e15c6d44a1 feat(recorder, bilibili): add more info into error messages
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-22 02:10:36 +08:00
Xinrea
8ab656a097 feat(recorder): add random delay to avoid api limit
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-21 15:57:02 +08:00
Xinrea
cb8642b9a9 feat(main): add message for room deletion
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-19 16:39:54 +08:00
Xinrea
1761d398ee feat(recorder): enhance error message for archive deletion
Signed-off-by: Xinrea <xinreas@outlook.com>
2024-11-19 13:41:54 +08:00
Xinrea
eb33fd57a8 feat(bilibili, recorder): introduce BiliStream to handle stream expiration and extra 2024-11-18 14:33:49 +08:00
Xinrea
317b0b373a fix(recorder): handle error when creating danmu storage 2024-11-18 14:24:51 +08:00
Xinrea
563c8d0085 fix(bilibili): correct the potential issue with retrieving the wrong stream 2024-11-14 18:47:55 +08:00
Xinrea
80ced70267 feat(bilibili): add timeout for requests 2024-11-14 18:46:17 +08:00
Xinrea
3a89b43435 feat(recorder, bilibili): enhance error messages for improved debugging 2024-11-14 18:44:59 +08:00
Xinrea
d117095a5f feat(player): persist live room volume setting 2024-11-12 01:46:09 +08:00
Xinrea
d78c1bf861 fix(recorder): panic when offset not matching sequence 2024-11-09 01:01:31 +08:00
Xinrea
20da8034a1 chore: add git-cliff to generate Changelog
git-cliff configuration is provided. To generate Changelog:
git cliff -l
2024-11-07 12:48:37 +08:00
Xinrea
0d053a3462 release: bump version to 1.2.0 2024-11-07 02:08:07 +08:00
Xinrea
280e540f4f fix(recorder): using "-" instead of "|" to separate offset and sequence
When using "|" in file name, ffmpeg concat is not able to work. So now
we change it to "-".
Here is an example for new format of segment filename:
18b7af7-89947993.m4s
2024-11-07 01:39:40 +08:00
Xinrea
824cfd23ed fix(recorder): using global offset to find clip segments range
The clip range drift problem is finally solved by calculate
date-time for every segment. With date-time, we can ignore
gaps in m3u8 segments. close #5
2024-11-07 01:16:20 +08:00
Xinrea
695728df2e feat(recorder): add DATE-TIME tag for segments 2024-11-06 20:04:49 +08:00
Xinrea
24deca75d2 fix(recorder_manager): handle cors preflight request for hls server 2024-11-06 20:04:13 +08:00
Xinrea
8a1184f161 fix(recorder): clip using accurate segment length 2024-11-06 12:04:02 +08:00
Xinrea
d61096d1b1 refactor(recorder): calculate segment length in entry-creation 2024-11-06 02:14:42 +08:00
Xinrea
3b9d1be002 fix: use accurate segment length to prevent video time drift 2024-11-04 20:00:38 +08:00
Xinrea
13262f8f10 feat: add history danmu replay (close #16) 2024-11-03 21:24:54 +08:00
Xinrea
9f05fc4954 release: bump version to 1.1.0 2024-10-30 21:32:49 +08:00
Xinrea
3fce06ef63 chore: code format 2024-10-30 21:30:40 +08:00
Xinrea
3d13f69e5c feat: log output to file 2024-10-30 21:28:43 +08:00
Xinrea
deb19c6223 feat: change clip encoding to copy for speed 2024-10-30 21:01:58 +08:00
Xinrea
7466127832 feat: add account using cookie str (close #19) 2024-10-30 20:55:35 +08:00
Xinrea
af982c5fe0 fix: project url on about page 2024-10-30 18:38:25 +08:00
Xinrea
b03f0150d8 release: bump version to 1.0.6 2024-10-26 12:43:07 +08:00
Xinrea
d61ddafb44 fix: remove window effects
Tauri currently does not provide an API to retrieve the active window effects, making it impossible to adjust the window style based on that information. Therefore, we have removed the window effects to maintain UI consistency.

close #18
2024-10-26 12:42:41 +08:00
Xinrea
fd89a197a5 release: bump version to 1.0.5 2024-10-25 21:09:36 +08:00
Xinrea
31fa29ee62 fix: shortkey description 2024-10-25 21:08:32 +08:00
Xinrea
c7e28b2ad6 doc: update readme 2024-10-25 21:06:20 +08:00
Xinrea
bbc1343079 chore: fix some clippy warning 2024-10-25 20:19:24 +08:00
Xinrea
c7d4fb270b feat: add text-shadow for danmaku 2024-10-25 01:03:34 +08:00
Xinrea
fcccdee105 release: bump version to 1.0.4 2024-10-24 01:04:36 +08:00
Xinrea
887072f6c7 chore: remove quick-clip for now 2024-10-24 01:04:02 +08:00
Xinrea
1932edba21 feat: remove live-window fullscreen button on windows 2024-10-24 01:01:43 +08:00
Xinrea
0c15415822 feat: delete related cache folder when removing recorder 2024-10-24 01:01:04 +08:00
Xinrea
b8dc0870b5 feat: adjust cover-text style 2024-10-22 02:00:24 +08:00
Xinrea
9d0ad2ae45 feat: hide danmaku when playback is not keeping up with live stream 2024-10-22 01:43:44 +08:00
Xinrea
7278b9f48c fix: remove unused overflow-button 2024-10-22 01:39:22 +08:00
Xinrea
1aee95492a fix: only enable danmaku in live 2024-10-22 01:34:48 +08:00
Xinrea
0cff889f4b release: bump version to 1.0.3 2024-10-21 04:58:03 +08:00
Xinrea
9cd05362ac chore: update dependency ffmpeg-sidecar to 1.2.0 2024-10-21 04:57:24 +08:00
Xinrea
269eccc7ef feat: add playback rate select 2024-10-21 03:43:38 +08:00
Xinrea
aafd02090b feat: live_window title using formatted timestamp to show date 2024-10-21 03:21:53 +08:00
Xinrea
e0e43dbfa4 fix: prevent danmaku overflow from showing scrollbar 2024-10-21 02:39:49 +08:00
Xinrea
37c358a48b fix: video preview modal background 2024-10-21 02:27:07 +08:00
Xinrea
2b81f7a106 release: bump version to 1.0.2 2024-10-20 22:44:39 +08:00
Xinrea
cb9b606cb4 feat: save profile when text changed 2024-10-20 22:43:47 +08:00
Xinrea
c1879c6527 fix: post button 2024-10-20 22:41:15 +08:00
Xinrea
035c54b2fd fix: using player.seekRange() to get video total length 2024-10-20 22:37:23 +08:00
Xinrea
54b152207c fix: video preview window 2024-10-19 14:24:25 +08:00
Xinrea
034e159442 chore: change tray menu text 2024-10-19 14:04:00 +08:00
Xinrea
8a2533c182 fix: missing live_index when releasing 2024-10-19 13:50:14 +08:00
Xinrea
a35cc4cc19 ci/cd: update workflow 2024-10-19 06:05:00 +08:00
Xinrea
75b0cc80e3 feat: add notifications 2024-10-19 04:12:37 +08:00
Xinrea
c39a0bf462 fix: avoid long lock when cache migrating 2024-10-19 04:12:23 +08:00
Xinrea
10f19366f0 feat: add messages for cache's long migration 2024-10-19 03:52:46 +08:00
Xinrea
36ed9025f8 fix: stream not available after cache path changed 2024-10-19 03:50:09 +08:00
Xinrea
f67acc3fe1 fix: config not saved when errors 2024-10-19 03:31:03 +08:00
Xinrea
dea6418d54 fix: config not saving 2024-10-19 03:29:01 +08:00
Xinrea
adf3f63cfe feat: introduce app icon 2024-10-18 20:57:37 +08:00
Xinrea
22b1f11577 feat: simple support for folder migration (output & cache)
close #12
2024-10-18 20:55:38 +08:00
Xinrea
bc9fb48f18 feat: add linux support (untested) 2024-10-18 13:30:27 +08:00
Xinrea
0d6e6cc289 chore: remove unused functions 2024-10-18 01:06:59 +08:00
Xinrea
6d75a50716 feat: instant danmaku display support & danmaku sending
close #14
close #15
2024-10-17 20:11:20 +08:00
Xinrea
a8d9760a11 feat: prompt of empty message list 2024-10-15 04:40:38 +08:00
Xinrea
2889d3634f fix: disk name on windows 2024-10-15 04:31:52 +08:00
Xinrea
958e77e240 fix: message list scrollbar 2024-10-15 04:16:57 +08:00
Xinrea
e36a75aa28 feat: delete video button 2024-10-15 04:15:15 +08:00
Xinrea
1d66852134 feat: add play icon for preview-img-button 2024-10-15 04:01:21 +08:00
Xinrea
aab9764d33 fix: live window scrollbar is over titlebar 2024-10-15 02:10:24 +08:00
Xinrea
cf1c75c5b3 fix: live clip offset
close #11
2024-10-15 01:47:41 +08:00
Xinrea
a9c9d743b8 feat: webid expire-check and update 2024-10-14 19:36:41 +08:00
Xinrea
0213fda9a4 chore: update readme 2024-10-14 18:53:24 +08:00
Xinrea
909867e116 fix: adjust for windows platform 2024-10-14 18:48:21 +08:00
Xinrea
b75f920201 feat: simple disk info display
close #8
2024-10-14 16:57:55 +08:00
Xinrea
18a2fff07b fix: clip preview 2024-10-14 15:53:20 +08:00
Xinrea
1826dd122d fix: avoid code -352 with webid 2024-10-14 13:17:41 +08:00
Xinrea
2cb19941ee feat: add clip video record in db 2024-10-14 13:17:17 +08:00
Xinrea
00cd4d4b72 feat: store profiles in local storage 2024-10-10 12:16:28 +08:00
Xinrea
696cb5f48c feat: archive title 2024-10-09 18:17:35 +08:00
Xinrea
e13b4f5377 fix: disk usage 2024-10-09 17:53:14 +08:00
Xinrea
4b0bb7f31a feat: show version on main window title 2024-10-09 17:28:11 +08:00
Xinrea
1f6badb81d chore: remove automatic page for now 2024-10-09 17:07:02 +08:00
Xinrea
65acc6aa39 feat: show disk usage in summary page 2024-10-09 16:48:22 +08:00
Xinrea
44c41ed99f fix: open archive player window 2024-10-09 16:33:41 +08:00
Xinrea
301eb4f1dc feat: update to tauri 2 release version 2024-10-09 15:00:28 +08:00
Xinrea
5aa7de8d86 feat: cache size statistics 2024-10-09 14:35:25 +08:00
Xinrea
28c03918aa feat: separate clip button 2024-09-28 02:44:56 +08:00
Xinrea
d14b267305 fix: live window on macOS 2024-09-27 12:59:57 +08:00
Xinrea
f2f5d36090 feat: add macOS support 2024-09-27 12:49:48 +08:00
Xinrea
aec8450c53 feat: introduce message system
close #9
2024-09-27 03:07:48 +08:00
Xinrea
efc46341ae feat: multiple accounts support
close #6
2024-09-27 00:46:43 +08:00
Xinrea
3a69eb4864 doc: update readme 2024-09-26 02:42:14 +08:00
Xinrea
7044c2bd83 feat: cover text-adding support
simple cover editing & preview, close #7
2024-09-26 02:30:37 +08:00
Xinrea
74af439deb chore: basic database 2024-09-25 03:44:34 +08:00
Xinrea
faf7096743 feat: support index redirect & using different ways to generate clip 2024-09-25 03:43:53 +08:00
Xinrea
6b3f99c8a7 fix: clip range seek time & hotkey triggered when input 2024-09-25 03:42:44 +08:00
Xinrea
23fd528540 feat: separate clip and post 2024-09-25 03:41:57 +08:00
Xinrea
c701ee002e chore: error handling 2024-09-23 16:26:07 +08:00
Xinrea
238a755698 introduce sqlite 2024-09-22 20:53:36 +08:00
Xinrea
9cc95fb4c8 fix: render error when recorder list empty 2024-09-20 15:01:53 +08:00
Xinrea
bc5887eab4 fix: player slider thumb style 2024-09-20 15:01:10 +08:00
Xinrea
01335ad0ca WIP: update 2024-09-14 18:39:08 +08:00
Xinrea
18f7bd2bf7 WIP: old cache restore 2024-09-13 03:20:46 +08:00
Xinrea
d812deccb8 WIP: update 2024-09-13 01:40:34 +08:00
Xinrea
c7e5cbf26e WIP: using flowbite-svelte 2024-09-09 18:46:52 +08:00
Xinrea
e230ef0b41 WIP: introduce clip_range 2024-09-09 05:03:30 +08:00
Xinrea
5f547e593a WIP: DVR implement 2024-09-09 02:46:42 +08:00
Xinrea
a1c6caece1 ui: adjust styles 2024-09-01 19:53:23 +08:00
Xinrea
0281c2df1c chore: fix app description 2024-09-01 13:04:14 +08:00
Xinrea
88bfc517f7 release v0.1.0 2024-08-31 02:23:01 +08:00
Xinrea
877b6b5764 fix: update felgens to fix danmu stream 2024-08-31 02:23:01 +08:00
Xinrea
6b73937acd fix: deadlock when add/remove room 2024-08-31 02:22:55 +08:00
Xinrea
95f7790870 feat: adjust styles 2024-08-30 18:01:17 +08:00
Xinrea
0a536a518a feat: add qr login & change to async 2024-08-30 17:46:25 +08:00
Xinrea
330fa14c8c chore: change dev port 2024-08-29 15:43:12 +08:00
Xinrea
51d4767307 update dependencies 2024-08-29 15:02:35 +08:00
Xinrea
e5f12cdacd feat: Notification for clip action 2023-05-26 16:15:22 +08:00
Xinrea
87525419a2 release v0.0.5 2023-05-25 17:43:21 +08:00
Xinrea
d427d32463 fix: output path with spaces 2023-05-25 17:16:08 +08:00
Xinrea
0881dfdc3f release v0.0.4 2023-05-25 00:34:51 +08:00
Xinrea
a6f369735a fix: add sign for user info api 2023-05-25 00:32:55 +08:00
199 changed files with 70616 additions and 8119 deletions

39
.dockerignore Normal file
View 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
View 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
View File

@@ -0,0 +1,21 @@
---
name: Bug report
about: 提交一个 BUG
title: "[BUG]"
labels: bug
assignees: Xinrea
---
**描述:**
简要描述一下这个 BUG 的现象
**日志和截图:**
如果可以的话,请尽量附上相关截图和日志文件(日志是位于安装目录下,名为 bsr.log 的文件)。
**相关信息:**
- 程序版本:
- 系统类型:
**其他**
任何其他想说的

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: 提交一个新功能的建议
title: "[feature]"
labels: enhancement
assignees: Xinrea
---
**遇到的问题:**
在使用过程中遇到了什么问题让你想要提出建议
**想要的功能:**
想要怎样的新功能来解决这个问题
**通过什么方式实现(有思路的话):**
如果有相关的实现思路或者是参考,可以在此提供
**其他:**
其他任何想说的话

View File

@@ -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
View 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
View 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
View File

@@ -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
View File

@@ -0,0 +1,8 @@
[[language]]
name = "rust"
auto-format = true
rulers = []
[[language]]
name = "svelte"
auto-format = true

2
.ignore Normal file
View File

@@ -0,0 +1,2 @@
*.png
*.svg

View File

@@ -1,7 +0,0 @@
{
"recommendations": [
"svelte.svelte-vscode",
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer"
]
}

86
Dockerfile Normal file
View 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
View 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.

View File

@@ -1,26 +1,27 @@
# Bilibili ShadowReplay
# BiliBili ShadowReplay
![主界面](doc/main.png)
![icon](docs/public/images/header.png)
> 点击关闭后程序仍会在后台运行,请找到托盘区的图标右键退出程序
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/xinrea/bili-shadowreplay/main.yml?label=Application%20Build)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Xinrea/bili-shadowreplay/package.yml?label=Docker%20Build)
## 介绍
![GitHub Release](https://img.shields.io/github/v/release/xinrea/bili-shadowreplay)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/xinrea/bili-shadowreplay/total)
Bilibili ShadowReplay 是一个用于缓存B站直播的工具可以将直播的视频缓存到本地便于及时保存回放方便后期剪辑工作
BiliBili ShadowReplay 是一个缓存直播并进行实时编辑投稿的工具。通过划定时间区间,并编辑简单的必需信息,即可完成直播切片以及投稿,将整个流程压缩到分钟级。同时,也支持对缓存的历史直播进行回放,以及相同的切片编辑投稿处理流程
![clip](doc/clip.png)
目前仅支持 B 站和抖音平台的直播。
除了在界面上手动操作外,还可以通过弹幕触发切片。
![rooms](docs/public/images/summary.png)
> 只有管理员UID设置中的用户才能触发切片
## 安装和使用
![弹幕](doc/danmu_command.png)
前往网站查看说明:[BiliBili ShadowReplay](https://bsr.xinrea.cn/)
## 设置
## 参与开发
![设置](doc/setting.png)
[Contributing](.github/CONTRIBUTING.md)
- `缓存时长`:缓存的视频时长,单位为秒
- `缓存目录`:缓存的视频存放目录
- `切片目录`: 切片的视频存放目录
- `管理员UID`B站的UID用于判断是否有权限在直播间通过弹幕触发切片可设置多个使用英文逗号分隔。
## 赞助
![donate](docs/public/images/donate.png)

42
build.ps1 Normal file
View 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
View 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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

43
docs/.vitepress/config.ts Normal file
View 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" },
],
},
});

View File

@@ -0,0 +1 @@
# 架构设计

View File

@@ -0,0 +1,57 @@
# 配置使用
## 账号配置
要添加直播间,至少需要配置一个同平台的账号。在账号页面,你可以通过添加账号按钮添加一个账号。
- B 站账号:目前支持扫码登录和 Cookie 手动配置两种方式,推荐使用扫码登录
- 抖音账号:目前仅支持 Cookie 手动配置登陆
### 抖音账号配置
首先确保已经登录抖音,然后打开[个人主页](https://www.douyin.com/user/self),右键单击网页,在菜单中选择 `检查Inspect`,打开开发者工具,切换到 `网络Network` 选项卡,然后刷新网页,此时能在列表中找到 `self` 请求(一般是列表中第一个),单击该请求,查看`请求标头`,在 `请求标头` 中找到 `Cookie`,复制该字段的值,粘贴到配置页面的 `Cookie` 输入框中,要注意复制完全。
![DouyinCookie](/images/douyin_cookie.png)
## FFmpeg 配置
如果想要使用切片生成和压制功能,请确保 FFmpeg 已正确配置;除了 Windows 平台打包自带 FFmpeg 以外,其他平台需要手动安装 FFmpeg请参考 [FFmpeg 配置](/getting-started/ffmpeg)。
## Whisper 配置
要使用 AI 字幕识别功能,需要在设置页面配置 Whisper。目前可以选择使用本地运行 Whisper 模型,或是使用在线的 Whisper 服务(通常需要付费获取 API Key
> [!NOTE]
> 其实有许多更好的中文字幕识别解决方案,但是这类服务通常需要将文件上传到对象存储后异步处理,考虑到实现的复杂度,选择了使用本地运行 Whisper 模型或是使用在线的 Whisper 服务,在请求返回时能够直接获取字幕生成结果。
### 本地运行 Whisper 模型
![WhisperLocal](/images/whisper_local.png)
如果需要使用本地运行 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 服务
![WhisperOnline](/images/whisper_online.png)
如果需要使用在线的 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 无法理解复杂的提示词,你可以在提示词中使用一些简单的描述,让其在选择词汇时使用偏向于提示词所描述的领域相关的词汇,以避免出现毫不相干领域的词汇;或是让它在标点符号的使用上参照提示词的风格。

View 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 已经放置在了程序目录下,因此不需要额外安装。

View 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
View 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 助手管理录播,分析直播内容,生成切片
---
## 总览
![rooms](/images/summary.png)
## 直播间管理
![clip](/images/rooms.png)
![archives](/images/archives.png)
## 账号管理
![accounts](/images/accounts.png)
## 预览窗口
![livewindow](/images/livewindow.png)
## 封面编辑
![cover](/images/cover_edit.png)
## 设置
![settings](/images/settings.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
docs/public/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

0
docs/usage/faq.md Normal file
View File

0
docs/usage/features.md Normal file
View File

32
ffmpeg_setup.ps1 Normal file
View 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

View File

@@ -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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

BIN
public/imgs/douyin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

53
public/shaka-player/controls.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View 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
View 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();

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"]

View File

@@ -1,3 +1,4 @@
fn main() {
tauri_build::build()
#[cfg(feature = "gui")]
tauri_build::build()
}

View 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"
]
}

View 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

View 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"

View File

View 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(())
}

View 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(())
}

View 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)
}
}

View 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)
}
}

View 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,
}

View 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))
}
}
}

View 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,
}

View File

@@ -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,
})
}
}

View File

@@ -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,
})
}
}

View 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))
}

View 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,
})
}
}

View File

@@ -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
}
}

View File

@@ -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,
})
}
}

View 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(())
}
}

File diff suppressed because one or more lines are too long

View 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,
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"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"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

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