mirror of
https://github.com/Xinrea/bili-shadowreplay.git
synced 2025-11-24 20:15:34 +08:00
Compare commits
5 Commits
e7411d25b4
...
8bea9336ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bea9336ae | ||
|
|
617a6a0b8e | ||
|
|
140ab772d0 | ||
|
|
e7d8c8814d | ||
|
|
588559c645 |
@@ -18,7 +18,7 @@
|
||||
"id": "a96a5e9f-9857-4c13-b889-91da2ace208a",
|
||||
"event": "recorder.added",
|
||||
"payload": {
|
||||
"room_id": 26966466,
|
||||
"room_id": "26966466",
|
||||
"created_at": "2025-09-07T03:33:14.258796+00:00",
|
||||
"platform": "bilibili",
|
||||
"auto_start": true,
|
||||
@@ -35,7 +35,7 @@
|
||||
"id": "e33623d4-e040-4390-88f5-d351ceeeace7",
|
||||
"event": "recorder.removed",
|
||||
"payload": {
|
||||
"room_id": 27183290,
|
||||
"room_id": "27183290",
|
||||
"created_at": "2025-08-30T10:54:18.569198+00:00",
|
||||
"platform": "bilibili",
|
||||
"auto_start": true,
|
||||
@@ -57,9 +57,9 @@
|
||||
"id": "f12f3424-f7d8-4b2f-a8b7-55477411482e",
|
||||
"event": "live.started",
|
||||
"payload": {
|
||||
"room_id": 843610,
|
||||
"room_id": "843610",
|
||||
"room_info": {
|
||||
"room_id": 843610,
|
||||
"room_id": "843610",
|
||||
"room_title": "登顶!",
|
||||
"room_cover": "https://i0.hdslb.com/bfs/live/new_room_cover/73aea43f4b4624c314d62fea4b424822fb506dfb.jpg"
|
||||
},
|
||||
@@ -86,9 +86,9 @@
|
||||
"id": "e8b0756a-02f9-4655-b5ae-a170bf9547bd",
|
||||
"event": "live.ended",
|
||||
"payload": {
|
||||
"room_id": 843610,
|
||||
"room_id": "843610",
|
||||
"room_info": {
|
||||
"room_id": 843610,
|
||||
"room_id": "843610",
|
||||
"room_title": "登顶!",
|
||||
"room_cover": "https://i0.hdslb.com/bfs/live/new_room_cover/73aea43f4b4624c314d62fea4b424822fb506dfb.jpg"
|
||||
},
|
||||
@@ -117,9 +117,9 @@
|
||||
"id": "5ec1ea10-2b31-48fd-8deb-f2d7d2ea5985",
|
||||
"event": "record.started",
|
||||
"payload": {
|
||||
"room_id": 26966466,
|
||||
"room_id": "26966466",
|
||||
"room_info": {
|
||||
"room_id": 26966466,
|
||||
"room_id": "26966466",
|
||||
"room_title": "早安獭獭栞!下播前抽fufu",
|
||||
"room_cover": "https://i0.hdslb.com/bfs/live/user_cover/b810c36855168034557e905e5916b1dba1761fa4.jpg"
|
||||
},
|
||||
@@ -146,9 +146,9 @@
|
||||
"id": "56fd03e5-3965-4c2e-a6a9-bb6932347eb3",
|
||||
"event": "record.ended",
|
||||
"payload": {
|
||||
"room_id": 26966466,
|
||||
"room_id": "26966466",
|
||||
"room_info": {
|
||||
"room_id": 26966466,
|
||||
"room_id": "26966466",
|
||||
"room_title": "早安獭獭栞!下播前抽fufu",
|
||||
"room_cover": "https://i0.hdslb.com/bfs/live/user_cover/b810c36855168034557e905e5916b1dba1761fa4.jpg"
|
||||
},
|
||||
@@ -177,7 +177,7 @@
|
||||
"payload": {
|
||||
"platform": "bilibili",
|
||||
"live_id": "1756607084705",
|
||||
"room_id": 1967212929,
|
||||
"room_id": "1967212929",
|
||||
"title": "灶台O.o",
|
||||
"length": 9,
|
||||
"size": 1927112,
|
||||
@@ -198,7 +198,7 @@
|
||||
"event": "clip.generated",
|
||||
"payload": {
|
||||
"id": 316,
|
||||
"room_id": 27183290,
|
||||
"room_id": "27183290",
|
||||
"cover": "[27183290][1757172501727][一起看凡人修仙传][2025-09-07_00-16-11].jpg",
|
||||
"file": "[27183290][1757172501727][一起看凡人修仙传][2025-09-07_00-16-11].mp4",
|
||||
"note": "",
|
||||
@@ -225,7 +225,7 @@
|
||||
"event": "clip.deleted",
|
||||
"payload": {
|
||||
"id": 313,
|
||||
"room_id": 27183290,
|
||||
"room_id": "27183290",
|
||||
"cover": "[27183290][1756903953470][不出非洲之心不下播][2025-09-03_21-10-54].jpg",
|
||||
"file": "[27183290][1756903953470][不出非洲之心不下播][2025-09-03_21-10-54].mp4",
|
||||
"note": "",
|
||||
|
||||
@@ -8,7 +8,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize logging
|
||||
env_logger::init();
|
||||
// Replace these with actual values
|
||||
let room_id = 768756;
|
||||
let room_id = "768756";
|
||||
let cookie = "";
|
||||
let stream = Arc::new(DanmuStream::new(ProviderType::BiliBili, cookie, room_id).await?);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 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?);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
pub struct DanmuStream {
|
||||
pub provider_type: ProviderType,
|
||||
pub identifier: String,
|
||||
pub room_id: i64,
|
||||
pub room_id: String,
|
||||
pub provider: Arc<RwLock<Box<dyn DanmuProvider>>>,
|
||||
tx: mpsc::UnboundedSender<DanmuMessageType>,
|
||||
rx: Arc<RwLock<mpsc::UnboundedReceiver<DanmuMessageType>>>,
|
||||
@@ -21,14 +21,14 @@ impl DanmuStream {
|
||||
pub async fn new(
|
||||
provider_type: ProviderType,
|
||||
identifier: &str,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
) -> 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,
|
||||
room_id: room_id.to_string(),
|
||||
provider: Arc::new(RwLock::new(provider)),
|
||||
tx,
|
||||
rx: Arc::new(RwLock::new(rx)),
|
||||
|
||||
@@ -29,7 +29,7 @@ pub enum DanmuMessageType {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DanmuMessage {
|
||||
pub room_id: i64,
|
||||
pub room_id: String,
|
||||
pub user_id: u64,
|
||||
pub user_name: String,
|
||||
pub message: String,
|
||||
|
||||
@@ -36,7 +36,7 @@ type WsWriteType = futures_util::stream::SplitSink<
|
||||
|
||||
pub struct BiliDanmu {
|
||||
client: ApiClient,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
user_id: i64,
|
||||
stop: Arc<RwLock<bool>>,
|
||||
write: Arc<RwLock<Option<WsWriteType>>>,
|
||||
@@ -44,7 +44,7 @@ pub struct BiliDanmu {
|
||||
|
||||
#[async_trait]
|
||||
impl DanmuProvider for BiliDanmu {
|
||||
async fn new(cookie: &str, room_id: i64) -> Result<Self, DanmuStreamError> {
|
||||
async fn new(cookie: &str, room_id: &str) -> Result<Self, DanmuStreamError> {
|
||||
// find DedeUserID=<user_id> in cookie str
|
||||
let user_id = BiliDanmu::parse_user_id(cookie)?;
|
||||
// add buvid3 to cookie
|
||||
@@ -54,7 +54,7 @@ impl DanmuProvider for BiliDanmu {
|
||||
Ok(Self {
|
||||
client,
|
||||
user_id,
|
||||
room_id,
|
||||
room_id: room_id.to_string(),
|
||||
stop: Arc::new(RwLock::new(false)),
|
||||
write: Arc::new(RwLock::new(None)),
|
||||
})
|
||||
@@ -126,8 +126,10 @@ impl BiliDanmu {
|
||||
tx: mpsc::UnboundedSender<DanmuMessageType>,
|
||||
) -> Result<(), DanmuStreamError> {
|
||||
let wbi_key = self.get_wbi_key().await?;
|
||||
let real_room = self.get_real_room(&wbi_key, self.room_id).await?;
|
||||
let danmu_info = self.get_danmu_info(&wbi_key, real_room).await?;
|
||||
let real_room = self.get_real_room(&wbi_key, &self.room_id).await?;
|
||||
let danmu_info = self
|
||||
.get_danmu_info(&wbi_key, real_room.to_string().as_str())
|
||||
.await?;
|
||||
let ws_hosts = danmu_info.data.host_list.clone();
|
||||
let mut conn = None;
|
||||
log::debug!("ws_hosts: {:?}", ws_hosts);
|
||||
@@ -241,7 +243,7 @@ impl BiliDanmu {
|
||||
async fn get_danmu_info(
|
||||
&self,
|
||||
wbi_key: &str,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
) -> Result<DanmuInfo, DanmuStreamError> {
|
||||
let params = self
|
||||
.get_sign(
|
||||
@@ -268,7 +270,7 @@ impl BiliDanmu {
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
async fn get_real_room(&self, wbi_key: &str, room_id: i64) -> Result<i64, DanmuStreamError> {
|
||||
async fn get_real_room(&self, wbi_key: &str, room_id: &str) -> Result<i64, DanmuStreamError> {
|
||||
let params = self
|
||||
.get_sign(
|
||||
wbi_key,
|
||||
|
||||
@@ -65,7 +65,7 @@ impl WsStreamCtx {
|
||||
|
||||
if let Some(danmu_msg) = danmu_msg {
|
||||
Ok(DanmuMessageType::DanmuMessage(DanmuMessage {
|
||||
room_id: 0,
|
||||
room_id: "".to_string(),
|
||||
user_id: danmu_msg.uid,
|
||||
user_name: danmu_msg.username,
|
||||
message: danmu_msg.msg,
|
||||
|
||||
@@ -33,7 +33,7 @@ type WsWriteType =
|
||||
futures_util::stream::SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, WsMessage>;
|
||||
|
||||
pub struct DouyinDanmu {
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
cookie: String,
|
||||
stop: Arc<RwLock<bool>>,
|
||||
write: Arc<RwLock<Option<WsWriteType>>>,
|
||||
@@ -192,7 +192,7 @@ impl DouyinDanmu {
|
||||
});
|
||||
|
||||
// Main message handling loop
|
||||
let room_id = self.room_id;
|
||||
let room_id = self.room_id.clone();
|
||||
let stop = Arc::clone(&self.stop);
|
||||
let write = Arc::clone(&self.write);
|
||||
let message_handle = tokio::spawn(async move {
|
||||
@@ -210,7 +210,7 @@ impl DouyinDanmu {
|
||||
|
||||
match msg {
|
||||
WsMessage::Binary(data) => {
|
||||
if let Ok(Some(ack)) = handle_binary_message(&data, &tx, room_id).await {
|
||||
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
|
||||
@@ -268,7 +268,7 @@ impl DouyinDanmu {
|
||||
async fn handle_binary_message(
|
||||
data: &[u8],
|
||||
tx: &mpsc::UnboundedSender<DanmuMessageType>,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
) -> Result<Option<PushFrame>, DanmuStreamError> {
|
||||
// First decode the PushFrame
|
||||
let push_frame = PushFrame::decode(Bytes::from(data.to_vec())).map_err(|e| {
|
||||
@@ -328,7 +328,7 @@ async fn handle_binary_message(
|
||||
})?;
|
||||
if let Some(user) = chat_msg.user {
|
||||
let danmu_msg = DanmuMessage {
|
||||
room_id,
|
||||
room_id: room_id.to_string(),
|
||||
user_id: user.id,
|
||||
user_name: user.nick_name,
|
||||
message: chat_msg.content,
|
||||
@@ -394,9 +394,9 @@ async fn handle_binary_message(
|
||||
|
||||
#[async_trait]
|
||||
impl DanmuProvider for DouyinDanmu {
|
||||
async fn new(identifier: &str, room_id: i64) -> Result<Self, DanmuStreamError> {
|
||||
async fn new(identifier: &str, room_id: &str) -> Result<Self, DanmuStreamError> {
|
||||
Ok(Self {
|
||||
room_id,
|
||||
room_id: room_id.to_string(),
|
||||
cookie: identifier.to_string(),
|
||||
stop: Arc::new(RwLock::new(false)),
|
||||
write: Arc::new(RwLock::new(None)),
|
||||
|
||||
@@ -17,7 +17,7 @@ pub enum ProviderType {
|
||||
|
||||
#[async_trait]
|
||||
pub trait DanmuProvider: Send + Sync {
|
||||
async fn new(identifier: &str, room_id: i64) -> Result<Self, DanmuStreamError>
|
||||
async fn new(identifier: &str, room_id: &str) -> Result<Self, DanmuStreamError>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
@@ -57,7 +57,7 @@ pub trait DanmuProvider: Send + Sync {
|
||||
pub async fn new(
|
||||
provider_type: ProviderType,
|
||||
identifier: &str,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
) -> Result<Box<dyn DanmuProvider>, DanmuStreamError> {
|
||||
match provider_type {
|
||||
ProviderType::BiliBili => {
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::errors::RecorderError;
|
||||
use crate::ffmpeg::VideoMetadata;
|
||||
use crate::{core::HlsStream, events::RecorderEvent};
|
||||
|
||||
const UPDATE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const UPDATE_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
const UPDATE_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const PLAYLIST_FILE_NAME: &str = "playlist.m3u8";
|
||||
const DOWNLOAD_RETRY: u32 = 3;
|
||||
@@ -193,13 +193,19 @@ impl HlsRecorder {
|
||||
// we need to remove the query parameters: 1.ts
|
||||
let filename = segment.uri.split('?').next().unwrap_or(&segment.uri);
|
||||
let segment_path = self.work_dir.join(filename);
|
||||
let size = download(
|
||||
let Ok(size) = download(
|
||||
&self.client,
|
||||
&segment_full_url,
|
||||
&segment_path,
|
||||
DOWNLOAD_RETRY,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
else {
|
||||
log::error!("Download failed: {:#?}", segment);
|
||||
return Err(RecorderError::IoError(std::io::Error::other(
|
||||
"Download failed",
|
||||
)));
|
||||
};
|
||||
|
||||
// check if the stream is changed
|
||||
let segment_metadata = crate::ffmpeg::extract_video_metadata(&segment_path)
|
||||
|
||||
@@ -7,7 +7,7 @@ pub enum RecorderEvent {
|
||||
recorder: RecorderInfo,
|
||||
},
|
||||
LiveEnd {
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
platform: PlatformType,
|
||||
recorder: RecorderInfo,
|
||||
},
|
||||
@@ -32,7 +32,7 @@ pub enum RecorderEvent {
|
||||
message: String,
|
||||
},
|
||||
DanmuReceived {
|
||||
room: i64,
|
||||
room: String,
|
||||
ts: i64,
|
||||
content: String,
|
||||
},
|
||||
|
||||
@@ -58,7 +58,7 @@ where
|
||||
T: Send + Sync,
|
||||
{
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
/// The account for the recorder
|
||||
account: Account,
|
||||
/// The client for the recorder
|
||||
@@ -108,8 +108,8 @@ impl<T: Send + Sync> traits::RecorderBasicTrait<T> for Recorder<T> {
|
||||
self.platform
|
||||
}
|
||||
|
||||
fn room_id(&self) -> i64 {
|
||||
self.room_id
|
||||
fn room_id(&self) -> String {
|
||||
self.room_id.clone()
|
||||
}
|
||||
|
||||
fn account(&self) -> &Account {
|
||||
@@ -194,17 +194,17 @@ impl<T: Send + Sync> traits::RecorderBasicTrait<T> for Recorder<T> {
|
||||
pub struct CachePath {
|
||||
pub cache_path: PathBuf,
|
||||
pub platform: PlatformType,
|
||||
pub room_id: i64,
|
||||
pub room_id: String,
|
||||
pub live_id: String,
|
||||
pub file_name: Option<String>,
|
||||
}
|
||||
|
||||
impl CachePath {
|
||||
pub fn new(cache_path: PathBuf, platform: PlatformType, room_id: i64, live_id: &str) -> Self {
|
||||
pub fn new(cache_path: PathBuf, platform: PlatformType, room_id: &str, live_id: &str) -> Self {
|
||||
Self {
|
||||
cache_path,
|
||||
platform,
|
||||
room_id,
|
||||
room_id: room_id.to_string(),
|
||||
live_id: live_id.to_string(),
|
||||
file_name: None,
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ pub type BiliRecorder = Recorder<BiliExtra>;
|
||||
|
||||
impl BiliRecorder {
|
||||
pub async fn new(
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
account: &Account,
|
||||
cache_dir: PathBuf,
|
||||
event_channel: broadcast::Sender<RecorderEvent>,
|
||||
@@ -53,7 +53,7 @@ impl BiliRecorder {
|
||||
|
||||
let recorder = Self {
|
||||
platform: PlatformType::BiliBili,
|
||||
room_id,
|
||||
room_id: room_id.to_string(),
|
||||
account: account.clone(),
|
||||
client,
|
||||
event_channel,
|
||||
@@ -102,7 +102,7 @@ impl BiliRecorder {
|
||||
|
||||
async fn check_status(&self) -> bool {
|
||||
let pre_live_status = self.room_info.read().await.status;
|
||||
match api::get_room_info(&self.client, &self.account, self.room_id).await {
|
||||
match api::get_room_info(&self.client, &self.account, &self.room_id).await {
|
||||
Ok(room_info) => {
|
||||
*self.room_info.write().await = RoomInfo {
|
||||
platform: "bilibili".to_string(),
|
||||
@@ -112,9 +112,9 @@ impl BiliRecorder {
|
||||
status: room_info.live_status == 1,
|
||||
};
|
||||
// Only update user info once
|
||||
if self.user_info.read().await.user_id != room_info.user_id.to_string() {
|
||||
if self.user_info.read().await.user_id != room_info.user_id {
|
||||
let user_id = room_info.user_id;
|
||||
let user_info = api::get_user_info(&self.client, &self.account, user_id).await;
|
||||
let user_info = api::get_user_info(&self.client, &self.account, &user_id).await;
|
||||
if let Ok(user_info) = user_info {
|
||||
*self.user_info.write().await = UserInfo {
|
||||
user_id: user_id.to_string(),
|
||||
@@ -141,7 +141,7 @@ impl BiliRecorder {
|
||||
if live_status {
|
||||
// Get cover image
|
||||
let room_cover_path = Path::new(PlatformType::BiliBili.as_str())
|
||||
.join(self.room_id.to_string())
|
||||
.join(&self.room_id)
|
||||
.join("cover.jpg");
|
||||
let full_room_cover_path = self.cache_dir.join(&room_cover_path);
|
||||
if (api::download_file(
|
||||
@@ -161,7 +161,7 @@ impl BiliRecorder {
|
||||
} else {
|
||||
let _ = self.event_channel.send(RecorderEvent::LiveEnd {
|
||||
platform: PlatformType::BiliBili,
|
||||
room_id: self.room_id,
|
||||
room_id: self.room_id.to_string(),
|
||||
recorder: self.info().await,
|
||||
});
|
||||
*self.live_id.write().await = String::new();
|
||||
@@ -187,7 +187,7 @@ impl BiliRecorder {
|
||||
let new_stream = api::get_stream_info(
|
||||
&self.client,
|
||||
&self.account,
|
||||
self.room_id,
|
||||
&self.room_id,
|
||||
Protocol::HttpHls,
|
||||
Format::TS,
|
||||
&[Codec::Avc, Codec::Hevc],
|
||||
@@ -204,7 +204,7 @@ impl BiliRecorder {
|
||||
|
||||
log::info!(
|
||||
"[{}]Update to a new stream: {:#?} => {:#?}",
|
||||
self.room_id,
|
||||
&self.room_id,
|
||||
pre_live_stream,
|
||||
stream
|
||||
);
|
||||
@@ -213,11 +213,11 @@ impl BiliRecorder {
|
||||
}
|
||||
Err(e) => {
|
||||
if let crate::errors::RecorderError::FormatNotFound { format } = e {
|
||||
log::error!("[{}]Format {} not found", self.room_id, format);
|
||||
log::error!("[{}]Format {} not found", &self.room_id, format);
|
||||
|
||||
true
|
||||
} else {
|
||||
log::error!("[{}]Fetch stream failed: {}", self.room_id, e);
|
||||
log::error!("[{}]Fetch stream failed: {}", &self.room_id, e);
|
||||
|
||||
true
|
||||
}
|
||||
@@ -225,7 +225,7 @@ impl BiliRecorder {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("[{}]Update room status failed: {}", self.room_id, e);
|
||||
log::error!("[{}]Update room status failed: {}", &self.room_id, e);
|
||||
// may encounter internet issues, not sure whether the stream is closed or started, just remain
|
||||
pre_live_status
|
||||
}
|
||||
@@ -234,43 +234,58 @@ impl BiliRecorder {
|
||||
|
||||
async fn danmu(&self) -> Result<(), crate::errors::RecorderError> {
|
||||
let cookies = self.account.cookies.clone();
|
||||
let room_id = self.room_id;
|
||||
let danmu_stream = DanmuStream::new(ProviderType::BiliBili, &cookies, room_id).await;
|
||||
let room_id = self.room_id.clone();
|
||||
let danmu_stream = DanmuStream::new(ProviderType::BiliBili, &cookies, &room_id).await;
|
||||
if danmu_stream.is_err() {
|
||||
let err = danmu_stream.err().unwrap();
|
||||
log::error!("[{}]Failed to create danmu stream: {}", self.room_id, err);
|
||||
log::error!("[{}]Failed to create danmu stream: {}", &self.room_id, err);
|
||||
return Err(crate::errors::RecorderError::DanmuStreamError(err));
|
||||
}
|
||||
let danmu_stream = danmu_stream.unwrap();
|
||||
|
||||
// create a task to receive danmu message
|
||||
let danmu_stream_clone = danmu_stream.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = danmu_stream_clone.start().await;
|
||||
});
|
||||
let mut start_fut = Box::pin(danmu_stream.start());
|
||||
|
||||
loop {
|
||||
if let Ok(Some(msg)) = danmu_stream.recv().await {
|
||||
match msg {
|
||||
DanmuMessageType::DanmuMessage(danmu) => {
|
||||
let ts = Utc::now().timestamp_millis();
|
||||
let _ = self.event_channel.send(RecorderEvent::DanmuReceived {
|
||||
room: self.room_id,
|
||||
ts,
|
||||
content: danmu.message.clone(),
|
||||
});
|
||||
if let Some(storage) = self.danmu_storage.write().await.as_ref() {
|
||||
storage.add_line(ts, &danmu.message).await;
|
||||
tokio::select! {
|
||||
start_res = &mut start_fut => {
|
||||
match start_res {
|
||||
Ok(_) => {
|
||||
log::info!("[{}]Danmu stream finished", &self.room_id);
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("[{}]Danmu stream start error: {}", &self.room_id, err);
|
||||
return Err(crate::errors::RecorderError::DanmuStreamError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
recv_res = danmu_stream.recv() => {
|
||||
match recv_res {
|
||||
Ok(Some(msg)) => {
|
||||
match msg {
|
||||
DanmuMessageType::DanmuMessage(danmu) => {
|
||||
let ts = Utc::now().timestamp_millis();
|
||||
let _ = self.event_channel.send(RecorderEvent::DanmuReceived {
|
||||
room: self.room_id.clone(),
|
||||
ts,
|
||||
content: danmu.message.clone(),
|
||||
});
|
||||
if let Some(storage) = self.danmu_storage.write().await.as_ref() {
|
||||
storage.add_line(ts, &danmu.message).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
log::info!("[{}]Danmu stream closed", &self.room_id);
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("[{}]Failed to receive danmu message: {}", &self.room_id, err);
|
||||
return Err(crate::errors::RecorderError::DanmuStreamError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("[{}]Failed to receive danmu message", self.room_id);
|
||||
return Err(crate::errors::RecorderError::DanmuStreamError(
|
||||
danmu_stream::DanmuStreamError::WebsocketError {
|
||||
err: "Failed to receive danmu message".to_string(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +309,7 @@ impl BiliRecorder {
|
||||
let room_cover_path = self
|
||||
.cache_dir
|
||||
.join(PlatformType::BiliBili.as_str())
|
||||
.join(self.room_id.to_string())
|
||||
.join(&self.room_id)
|
||||
.join("cover.jpg");
|
||||
|
||||
tokio::fs::copy(room_cover_path, &cover_path.full_path())
|
||||
|
||||
@@ -40,16 +40,16 @@ struct UploadParams<'a> {
|
||||
pub struct RoomInfo {
|
||||
pub live_status: u8,
|
||||
pub room_cover_url: String,
|
||||
pub room_id: i64,
|
||||
pub room_id: String,
|
||||
pub room_keyframe_url: String,
|
||||
pub room_title: String,
|
||||
pub user_id: i64,
|
||||
pub user_id: String,
|
||||
pub live_start_time: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserInfo {
|
||||
pub user_id: i64,
|
||||
pub user_id: String,
|
||||
pub user_name: String,
|
||||
pub user_sign: String,
|
||||
pub user_avatar_url: String,
|
||||
@@ -242,7 +242,7 @@ pub async fn logout(client: &Client, account: &Account) -> Result<(), RecorderEr
|
||||
pub async fn get_user_info(
|
||||
client: &Client,
|
||||
account: &Account,
|
||||
user_id: i64,
|
||||
user_id: &str,
|
||||
) -> Result<UserInfo, RecorderError> {
|
||||
let params: Value = json!({
|
||||
"mid": user_id.to_string(),
|
||||
@@ -284,7 +284,7 @@ pub async fn get_user_info(
|
||||
return Err(RecorderError::InvalidResponseJson { resp: res.clone() });
|
||||
}
|
||||
Ok(UserInfo {
|
||||
user_id,
|
||||
user_id: user_id.to_string(),
|
||||
user_name: res["data"]["name"].as_str().unwrap_or("").to_string(),
|
||||
user_sign: res["data"]["sign"].as_str().unwrap_or("").to_string(),
|
||||
user_avatar_url: res["data"]["face"].as_str().unwrap_or("").to_string(),
|
||||
@@ -294,7 +294,7 @@ pub async fn get_user_info(
|
||||
pub async fn get_room_info(
|
||||
client: &Client,
|
||||
account: &Account,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
) -> Result<RoomInfo, RecorderError> {
|
||||
let mut headers = generate_user_agent_header();
|
||||
if let Ok(cookies) = account.cookies.parse() {
|
||||
@@ -329,7 +329,8 @@ pub async fn get_room_info(
|
||||
|
||||
let room_id = res["data"]["room_id"]
|
||||
.as_i64()
|
||||
.ok_or(RecorderError::InvalidValue)?;
|
||||
.ok_or(RecorderError::InvalidValue)?
|
||||
.to_string();
|
||||
let room_title = res["data"]["title"]
|
||||
.as_str()
|
||||
.ok_or(RecorderError::InvalidValue)?
|
||||
@@ -344,7 +345,8 @@ pub async fn get_room_info(
|
||||
.to_string();
|
||||
let user_id = res["data"]["uid"]
|
||||
.as_i64()
|
||||
.ok_or(RecorderError::InvalidValue)?;
|
||||
.ok_or(RecorderError::InvalidValue)?
|
||||
.to_string();
|
||||
let live_status = res["data"]["live_status"]
|
||||
.as_u64()
|
||||
.ok_or(RecorderError::InvalidValue)? as u8;
|
||||
@@ -384,7 +386,7 @@ pub async fn get_room_info(
|
||||
pub async fn get_stream_info(
|
||||
client: &Client,
|
||||
account: &Account,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
protocol: Protocol,
|
||||
format: Format,
|
||||
codec: &[Codec],
|
||||
@@ -900,7 +902,7 @@ pub async fn upload_cover(
|
||||
pub async fn send_danmaku(
|
||||
client: &Client,
|
||||
account: &Account,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
message: &str,
|
||||
) -> Result<(), RecorderError> {
|
||||
let url = "https://api.live.bilibili.com/msg/send".to_string();
|
||||
@@ -918,7 +920,7 @@ pub async fn send_danmaku(
|
||||
("fontsize", "25"),
|
||||
("room_type", "0"),
|
||||
("rnd", &format!("{}", chrono::Local::now().timestamp())),
|
||||
("roomid", &format!("{room_id}")),
|
||||
("roomid", room_id),
|
||||
("csrf", &account.csrf),
|
||||
("csrf_token", &account.csrf),
|
||||
];
|
||||
|
||||
@@ -43,7 +43,7 @@ fn get_best_stream_url(stream: &DouyinStream) -> Option<String> {
|
||||
|
||||
impl DouyinRecorder {
|
||||
pub async fn new(
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
sec_user_id: &str,
|
||||
account: &Account,
|
||||
cache_dir: PathBuf,
|
||||
@@ -53,7 +53,7 @@ impl DouyinRecorder {
|
||||
) -> Result<Self, crate::errors::RecorderError> {
|
||||
Ok(Self {
|
||||
platform: PlatformType::Douyin,
|
||||
room_id,
|
||||
room_id: room_id.to_string(),
|
||||
account: account.clone(),
|
||||
client: reqwest::Client::new(),
|
||||
event_channel: channel,
|
||||
@@ -85,7 +85,7 @@ impl DouyinRecorder {
|
||||
match api::get_room_info(
|
||||
&self.client,
|
||||
&self.account,
|
||||
self.room_id,
|
||||
&self.room_id,
|
||||
&self.extra.sec_user_id,
|
||||
)
|
||||
.await
|
||||
@@ -123,7 +123,7 @@ impl DouyinRecorder {
|
||||
} else {
|
||||
let _ = self.event_channel.send(RecorderEvent::LiveEnd {
|
||||
platform: PlatformType::Douyin,
|
||||
room_id: self.room_id,
|
||||
room_id: self.room_id.clone(),
|
||||
recorder: self.info().await,
|
||||
});
|
||||
}
|
||||
@@ -166,7 +166,7 @@ impl DouyinRecorder {
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("[{}]Update room status failed: {}", self.room_id, e);
|
||||
log::warn!("[{}]Update room status failed: {}", &self.room_id, e);
|
||||
pre_live_status
|
||||
}
|
||||
}
|
||||
@@ -181,7 +181,8 @@ impl DouyinRecorder {
|
||||
.clone()
|
||||
.parse::<i64>()
|
||||
.unwrap_or(0);
|
||||
let danmu_stream = DanmuStream::new(ProviderType::Douyin, &cookies, danmu_room_id).await;
|
||||
let danmu_stream =
|
||||
DanmuStream::new(ProviderType::Douyin, &cookies, &danmu_room_id.to_string()).await;
|
||||
if danmu_stream.is_err() {
|
||||
let err = danmu_stream.err().unwrap();
|
||||
log::error!("Failed to create danmu stream: {err}");
|
||||
@@ -189,34 +190,50 @@ impl DouyinRecorder {
|
||||
}
|
||||
let danmu_stream = danmu_stream.unwrap();
|
||||
|
||||
let danmu_stream_clone = danmu_stream.clone();
|
||||
*self.danmu_task.lock().await = Some(tokio::spawn(async move {
|
||||
let _ = danmu_stream_clone.start().await;
|
||||
}));
|
||||
let mut start_fut = Box::pin(danmu_stream.start());
|
||||
|
||||
loop {
|
||||
if let Ok(Some(msg)) = danmu_stream.recv().await {
|
||||
match msg {
|
||||
DanmuMessageType::DanmuMessage(danmu) => {
|
||||
let ts = Utc::now().timestamp_millis();
|
||||
let _ = self.event_channel.send(RecorderEvent::DanmuReceived {
|
||||
room: self.room_id,
|
||||
ts,
|
||||
content: danmu.message.clone(),
|
||||
});
|
||||
|
||||
if let Some(danmu_storage) = self.danmu_storage.read().await.as_ref() {
|
||||
danmu_storage.add_line(ts, &danmu.message).await;
|
||||
tokio::select! {
|
||||
start_res = &mut start_fut => {
|
||||
match start_res {
|
||||
Ok(_) => {
|
||||
log::info!("Danmu stream finished");
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Danmu stream start error: {err}");
|
||||
return Err(crate::errors::RecorderError::DanmuStreamError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
recv_res = danmu_stream.recv() => {
|
||||
match recv_res {
|
||||
Ok(Some(msg)) => {
|
||||
match msg {
|
||||
DanmuMessageType::DanmuMessage(danmu) => {
|
||||
let ts = Utc::now().timestamp_millis();
|
||||
let _ = self.event_channel.send(RecorderEvent::DanmuReceived {
|
||||
room: self.room_id.clone(),
|
||||
ts,
|
||||
content: danmu.message.clone(),
|
||||
});
|
||||
|
||||
if let Some(danmu_storage) = self.danmu_storage.read().await.as_ref() {
|
||||
danmu_storage.add_line(ts, &danmu.message).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
log::info!("Danmu stream closed");
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to receive danmu message: {err}");
|
||||
return Err(crate::errors::RecorderError::DanmuStreamError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to receive danmu message");
|
||||
return Err(crate::errors::RecorderError::DanmuStreamError(
|
||||
danmu_stream::DanmuStreamError::WebsocketError {
|
||||
err: "Failed to receive danmu message".to_string(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,6 +246,11 @@ impl DouyinRecorder {
|
||||
self.total_duration.store(0, atomic::Ordering::Relaxed);
|
||||
self.total_size.store(0, atomic::Ordering::Relaxed);
|
||||
*self.extra.live_stream.write().await = None;
|
||||
if let Some(danmu_task) = self.danmu_task.lock().await.as_mut() {
|
||||
danmu_task.abort();
|
||||
let _ = danmu_task.await;
|
||||
log::info!("Danmu task aborted");
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_entries(&self, live_id: &str) -> Result<(), RecorderError> {
|
||||
@@ -255,13 +277,6 @@ impl DouyinRecorder {
|
||||
*self.danmu_storage.write().await = danmu_storage;
|
||||
|
||||
// Start danmu task
|
||||
if let Some(danmu_task) = self.danmu_task.lock().await.as_mut() {
|
||||
danmu_task.abort();
|
||||
}
|
||||
if let Some(danmu_stream_task) = self.danmu_task.lock().await.as_mut() {
|
||||
danmu_stream_task.abort();
|
||||
}
|
||||
|
||||
*self.live_id.write().await = live_id.to_string();
|
||||
|
||||
let self_clone = self.clone();
|
||||
@@ -289,7 +304,7 @@ impl DouyinRecorder {
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = hls_recorder.start().await {
|
||||
log::error!("[{}]Failed to start hls recorder: {}", self.room_id, e);
|
||||
log::error!("[{}]Error from hls recorder: {}", self.room_id, e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ pub fn generate_user_agent_header() -> reqwest::header::HeaderMap {
|
||||
pub async fn get_room_info(
|
||||
client: &Client,
|
||||
account: &Account,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
sec_user_id: &str,
|
||||
) -> Result<DouyinBasicRoomInfo, RecorderError> {
|
||||
let mut headers = generate_user_agent_header();
|
||||
@@ -140,7 +140,7 @@ pub async fn get_room_info(
|
||||
pub async fn get_room_info_h5(
|
||||
client: &Client,
|
||||
account: &Account,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
sec_user_id: &str,
|
||||
) -> Result<DouyinBasicRoomInfo, RecorderError> {
|
||||
// 参考biliup实现,构建完整的URL参数
|
||||
@@ -335,7 +335,7 @@ pub async fn get_user_info(
|
||||
|
||||
pub async fn get_room_owner_sec_uid(
|
||||
client: &Client,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
) -> Result<String, RecorderError> {
|
||||
let url = format!("https://live.douyin.com/{room_id}");
|
||||
let mut headers = generate_user_agent_header();
|
||||
@@ -381,7 +381,9 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_get_room_owner_sec_uid() {
|
||||
let client = Client::new();
|
||||
let sec_uid = get_room_owner_sec_uid(&client, 200525029536).await.unwrap();
|
||||
let sec_uid = get_room_owner_sec_uid(&client, "200525029536")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
sec_uid,
|
||||
"MS4wLjABAAAAdFmmud36bynPjXOvoMjatb42856_zryHsGmlkpIECDA"
|
||||
|
||||
@@ -70,7 +70,7 @@ pub async fn get_user_info(
|
||||
pub async fn get_room_info(
|
||||
client: &Client,
|
||||
account: &Account,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
) -> Result<(UserInfo, RoomInfo, StreamInfo), HuyaClientError> {
|
||||
let mut headers = generate_user_agent_header();
|
||||
if let Ok(cookies) = account.cookies.parse() {
|
||||
@@ -142,7 +142,7 @@ mod tests {
|
||||
let client = Client::new();
|
||||
let account = Account::default();
|
||||
let (user_info, room_info, stream_info) =
|
||||
get_room_info(&client, &account, 599934).await.unwrap();
|
||||
get_room_info(&client, &account, "599934").await.unwrap();
|
||||
println!("{:?}", user_info);
|
||||
println!("{:?}", room_info);
|
||||
println!("{:?}", stream_info);
|
||||
|
||||
@@ -30,7 +30,7 @@ pub struct HuyaExtra {
|
||||
|
||||
impl HuyaRecorder {
|
||||
pub async fn new(
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
account: &Account,
|
||||
cache_dir: PathBuf,
|
||||
channel: broadcast::Sender<RecorderEvent>,
|
||||
@@ -39,7 +39,7 @@ impl HuyaRecorder {
|
||||
) -> Result<Self, crate::errors::RecorderError> {
|
||||
Ok(Self {
|
||||
platform: PlatformType::Huya,
|
||||
room_id,
|
||||
room_id: room_id.to_string(),
|
||||
account: account.clone(),
|
||||
client: reqwest::Client::new(),
|
||||
event_channel: channel,
|
||||
@@ -67,7 +67,7 @@ impl HuyaRecorder {
|
||||
|
||||
async fn check_status(&self) -> bool {
|
||||
let pre_live_status = self.room_info.read().await.status;
|
||||
match api::get_room_info(&self.client, &self.account, self.room_id).await {
|
||||
match api::get_room_info(&self.client, &self.account, &self.room_id).await {
|
||||
Ok((user_info, room_info, stream_info)) => {
|
||||
let live_status = room_info.status;
|
||||
|
||||
@@ -79,7 +79,7 @@ impl HuyaRecorder {
|
||||
// live status changed, reset current record flag
|
||||
log::info!(
|
||||
"[{}]Live status changed to {}, auto_start: {}",
|
||||
self.room_id,
|
||||
&self.room_id,
|
||||
live_status,
|
||||
self.enabled.load(atomic::Ordering::Relaxed)
|
||||
);
|
||||
@@ -91,7 +91,7 @@ impl HuyaRecorder {
|
||||
} else {
|
||||
let _ = self.event_channel.send(RecorderEvent::LiveEnd {
|
||||
platform: PlatformType::Douyin,
|
||||
room_id: self.room_id,
|
||||
room_id: self.room_id.clone(),
|
||||
recorder: self.info().await,
|
||||
});
|
||||
}
|
||||
@@ -118,7 +118,7 @@ impl HuyaRecorder {
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("[{}]Update room status failed: {}", self.room_id, e);
|
||||
log::warn!("[{}]Update room status failed: {}", &self.room_id, e);
|
||||
pre_live_status
|
||||
}
|
||||
}
|
||||
@@ -168,14 +168,14 @@ impl HuyaRecorder {
|
||||
recorder: self.info().await,
|
||||
});
|
||||
|
||||
log::debug!("[{}]Stream URL: {}", self.room_id, stream.hls_url);
|
||||
log::debug!("[{}]Stream URL: {}", &self.room_id, stream.hls_url);
|
||||
|
||||
let hls_stream =
|
||||
construct_stream_from_variant(live_id, &stream.hls_url, Format::TS, Codec::Avc)
|
||||
.await
|
||||
.map_err(|_| RecorderError::NoStreamAvailable)?;
|
||||
let hls_recorder = HlsRecorder::new(
|
||||
self.room_id.to_string(),
|
||||
self.room_id.clone(),
|
||||
Arc::new(hls_stream),
|
||||
self.client.clone(),
|
||||
Some(self.account.cookies.clone()),
|
||||
@@ -186,7 +186,7 @@ impl HuyaRecorder {
|
||||
.await;
|
||||
|
||||
if let Err(e) = hls_recorder.start().await {
|
||||
log::error!("[{}]Failed to start hls recorder: {}", self.room_id, e);
|
||||
log::error!("[{}]Failed to start hls recorder: {}", &self.room_id, e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ impl crate::traits::RecorderTrait<HuyaExtra> for HuyaRecorder {
|
||||
.store(true, atomic::Ordering::Relaxed);
|
||||
let live_id = Utc::now().timestamp_millis().to_string();
|
||||
if let Err(e) = self_clone.update_entries(&live_id).await {
|
||||
log::error!("[{}]Update entries error: {}", self_clone.room_id, e);
|
||||
log::error!("[{}]Update entries error: {}", &self_clone.room_id, e);
|
||||
}
|
||||
}
|
||||
if self_clone.is_recording.load(atomic::Ordering::Relaxed) {
|
||||
@@ -231,7 +231,7 @@ impl crate::traits::RecorderTrait<HuyaExtra> for HuyaRecorder {
|
||||
))
|
||||
.await;
|
||||
}
|
||||
log::info!("[{}]Recording thread quit.", self_clone.room_id);
|
||||
log::info!("[{}]Recording thread quit.", &self_clone.room_id);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use tokio::{
|
||||
#[allow(dead_code)]
|
||||
pub trait RecorderBasicTrait<T> {
|
||||
fn platform(&self) -> PlatformType;
|
||||
fn room_id(&self) -> i64;
|
||||
fn room_id(&self) -> String;
|
||||
fn account(&self) -> &Account;
|
||||
fn client(&self) -> &reqwest::Client;
|
||||
fn event_channel(&self) -> &broadcast::Sender<RecorderEvent>;
|
||||
@@ -62,7 +62,7 @@ pub trait RecorderTrait<T>: RecorderBasicTrait<T> {
|
||||
}
|
||||
|
||||
async fn work_dir(&self, live_id: &str) -> CachePath {
|
||||
CachePath::new(self.cache_dir(), self.platform(), self.room_id(), live_id)
|
||||
CachePath::new(self.cache_dir(), self.platform(), &self.room_id(), live_id)
|
||||
}
|
||||
async fn info(&self) -> RecorderInfo {
|
||||
let room_info = self.room_info().read().await.clone();
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use recorder::account::Account;
|
||||
use recorder::platforms::PlatformType;
|
||||
|
||||
use super::Database;
|
||||
use super::DatabaseError;
|
||||
use chrono::Utc;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::Rng;
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
|
||||
pub struct AccountRow {
|
||||
pub platform: String,
|
||||
pub uid: i64, // Keep for Bilibili compatibility
|
||||
pub id_str: Option<String>, // New field for string IDs like Douyin sec_uid
|
||||
pub uid: String,
|
||||
pub name: String,
|
||||
pub avatar: String,
|
||||
pub csrf: String,
|
||||
@@ -25,11 +19,7 @@ impl AccountRow {
|
||||
pub fn to_account(&self) -> Account {
|
||||
Account {
|
||||
platform: self.platform.clone(),
|
||||
id: if let Some(id) = self.id_str.as_ref() {
|
||||
id.clone()
|
||||
} else {
|
||||
self.uid.to_string()
|
||||
},
|
||||
id: self.uid.clone(),
|
||||
name: self.name.clone(),
|
||||
avatar: self.avatar.clone(),
|
||||
csrf: self.csrf.clone(),
|
||||
@@ -41,99 +31,14 @@ impl AccountRow {
|
||||
// accounts
|
||||
impl Database {
|
||||
// CREATE TABLE accounts (uid INTEGER PRIMARY KEY, name TEXT, avatar TEXT, csrf TEXT, cookies TEXT, created_at TEXT);
|
||||
pub async fn add_account(
|
||||
&self,
|
||||
platform: &str,
|
||||
cookies: &str,
|
||||
) -> Result<AccountRow, DatabaseError> {
|
||||
pub async fn add_account(&self, account: &AccountRow) -> Result<(), DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
let platform = PlatformType::from_str(platform).unwrap();
|
||||
sqlx::query("INSERT INTO accounts (uid, platform, name, avatar, csrf, cookies, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7)").bind(&account.uid).bind(&account.platform).bind(&account.name).bind(&account.avatar).bind(&account.csrf).bind(&account.cookies).bind(&account.created_at).execute(&lock).await?;
|
||||
|
||||
let csrf = match platform {
|
||||
PlatformType::BiliBili => {
|
||||
cookies
|
||||
.split(';')
|
||||
.map(str::trim)
|
||||
.find_map(|cookie| -> Option<String> {
|
||||
if cookie.starts_with("bili_jct=") {
|
||||
let var_name = &"bili_jct=";
|
||||
Some(cookie[var_name.len()..].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => Some(String::new()),
|
||||
};
|
||||
|
||||
if csrf.is_none() {
|
||||
return Err(DatabaseError::InvalidCookies);
|
||||
}
|
||||
|
||||
// parse uid and id_str based on platform
|
||||
let (uid, id_str) = match platform {
|
||||
PlatformType::BiliBili => {
|
||||
// For Bilibili, extract numeric uid from cookies
|
||||
let uid = (*cookies
|
||||
.split("DedeUserID=")
|
||||
.collect::<Vec<&str>>()
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.split(';')
|
||||
.collect::<Vec<&str>>()
|
||||
.first()
|
||||
.unwrap())
|
||||
.to_string()
|
||||
.parse::<u64>()
|
||||
.map_err(|_| DatabaseError::InvalidCookies)?;
|
||||
(uid, None)
|
||||
}
|
||||
PlatformType::Douyin => {
|
||||
// For Douyin, use temporary uid and will set id_str later with real sec_uid
|
||||
// Fix: Generate a u32 within the desired range and then cast to u64 to avoid `clippy::cast-sign-loss`.
|
||||
let temp_uid = rand::thread_rng().gen_range(10000u64..=i32::MAX as u64);
|
||||
(temp_uid, Some(format!("temp_{temp_uid}")))
|
||||
}
|
||||
PlatformType::Huya => {
|
||||
let uid = (*cookies
|
||||
.split("yyuid=")
|
||||
.collect::<Vec<&str>>()
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.split(';')
|
||||
.collect::<Vec<&str>>()
|
||||
.first()
|
||||
.unwrap())
|
||||
.to_string()
|
||||
.parse::<u64>()
|
||||
.map_err(|_| DatabaseError::InvalidCookies)?;
|
||||
(uid, None)
|
||||
}
|
||||
PlatformType::Youtube => {
|
||||
// unsupported
|
||||
return Err(DatabaseError::InvalidCookies);
|
||||
}
|
||||
};
|
||||
|
||||
let uid = i64::try_from(uid).map_err(|_| DatabaseError::InvalidCookies)?;
|
||||
|
||||
let account = AccountRow {
|
||||
platform: platform.as_str().to_string(),
|
||||
uid,
|
||||
id_str,
|
||||
name: String::new(),
|
||||
avatar: String::new(),
|
||||
csrf: csrf.unwrap(),
|
||||
cookies: cookies.into(),
|
||||
created_at: Utc::now().to_rfc3339(),
|
||||
};
|
||||
|
||||
sqlx::query("INSERT INTO accounts (uid, platform, id_str, name, avatar, csrf, cookies, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)").bind(uid).bind(&account.platform).bind(&account.id_str).bind(&account.name).bind(&account.avatar).bind(&account.csrf).bind(&account.cookies).bind(&account.created_at).execute(&lock).await?;
|
||||
|
||||
Ok(account)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_account(&self, platform: &str, uid: i64) -> Result<(), DatabaseError> {
|
||||
pub async fn remove_account(&self, platform: &str, uid: &str) -> Result<(), DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
let sql = sqlx::query("DELETE FROM accounts WHERE uid = $1 and platform = $2")
|
||||
.bind(uid)
|
||||
@@ -146,75 +51,6 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_account(
|
||||
&self,
|
||||
platform: &str,
|
||||
uid: i64,
|
||||
name: &str,
|
||||
avatar: &str,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
let sql = sqlx::query(
|
||||
"UPDATE accounts SET name = $1, avatar = $2 WHERE uid = $3 and platform = $4",
|
||||
)
|
||||
.bind(name)
|
||||
.bind(avatar)
|
||||
.bind(uid)
|
||||
.bind(platform)
|
||||
.execute(&lock)
|
||||
.await?;
|
||||
if sql.rows_affected() != 1 {
|
||||
return Err(DatabaseError::NotFound);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_account_with_id_str(
|
||||
&self,
|
||||
old_account: &AccountRow,
|
||||
new_id_str: &str,
|
||||
name: &str,
|
||||
avatar: &str,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
|
||||
// If the id_str changed, we need to delete the old record and create a new one
|
||||
if old_account.id_str.as_deref() == Some(new_id_str) {
|
||||
// id_str is the same, just update name and avatar
|
||||
sqlx::query(
|
||||
"UPDATE accounts SET name = $1, avatar = $2 WHERE uid = $3 and platform = $4",
|
||||
)
|
||||
.bind(name)
|
||||
.bind(avatar)
|
||||
.bind(old_account.uid)
|
||||
.bind(&old_account.platform)
|
||||
.execute(&lock)
|
||||
.await?;
|
||||
} else {
|
||||
// Delete the old record (for Douyin accounts, we use uid to identify)
|
||||
sqlx::query("DELETE FROM accounts WHERE uid = $1 and platform = $2")
|
||||
.bind(old_account.uid)
|
||||
.bind(&old_account.platform)
|
||||
.execute(&lock)
|
||||
.await?;
|
||||
|
||||
// Insert the new record with updated id_str
|
||||
sqlx::query("INSERT INTO accounts (uid, platform, id_str, name, avatar, csrf, cookies, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)")
|
||||
.bind(old_account.uid)
|
||||
.bind(&old_account.platform)
|
||||
.bind(new_id_str)
|
||||
.bind(name)
|
||||
.bind(avatar)
|
||||
.bind(&old_account.csrf)
|
||||
.bind(&old_account.cookies)
|
||||
.bind(&old_account.created_at)
|
||||
.execute(&lock)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_accounts(&self) -> Result<Vec<AccountRow>, DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
Ok(sqlx::query_as::<_, AccountRow>("SELECT * FROM accounts")
|
||||
@@ -222,7 +58,11 @@ impl Database {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_account(&self, platform: &str, uid: i64) -> Result<AccountRow, DatabaseError> {
|
||||
pub async fn get_account(
|
||||
&self,
|
||||
platform: &str,
|
||||
uid: &str,
|
||||
) -> Result<AccountRow, DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
Ok(sqlx::query_as::<_, AccountRow>(
|
||||
"SELECT * FROM accounts WHERE uid = $1 and platform = $2",
|
||||
|
||||
@@ -9,7 +9,7 @@ pub struct RecordRow {
|
||||
pub platform: String,
|
||||
pub parent_id: String,
|
||||
pub live_id: String,
|
||||
pub room_id: i64,
|
||||
pub room_id: String,
|
||||
pub title: String,
|
||||
pub length: i64,
|
||||
pub size: i64,
|
||||
@@ -17,11 +17,11 @@ pub struct RecordRow {
|
||||
pub cover: Option<String>,
|
||||
}
|
||||
|
||||
// CREATE TABLE records (live_id INTEGER PRIMARY KEY, room_id INTEGER, title TEXT, length INTEGER, size INTEGER, created_at TEXT);
|
||||
// CREATE TABLE records (live_id INTEGER PRIMARY KEY, room_id TEXT, title TEXT, length INTEGER, size INTEGER, created_at TEXT);
|
||||
impl Database {
|
||||
pub async fn get_records(
|
||||
&self,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<RecordRow>, DatabaseError> {
|
||||
@@ -38,7 +38,7 @@ impl Database {
|
||||
|
||||
pub async fn get_record(
|
||||
&self,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_id: &str,
|
||||
) -> Result<RecordRow, DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
@@ -53,7 +53,7 @@ impl Database {
|
||||
|
||||
pub async fn get_archives_by_parent_id(
|
||||
&self,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
parent_id: &str,
|
||||
) -> Result<Vec<RecordRow>, DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
@@ -71,7 +71,7 @@ impl Database {
|
||||
platform: PlatformType,
|
||||
parent_id: &str,
|
||||
live_id: &str,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
title: &str,
|
||||
cover: Option<String>,
|
||||
) -> Result<RecordRow, DatabaseError> {
|
||||
@@ -80,7 +80,7 @@ impl Database {
|
||||
platform: platform.as_str().to_string(),
|
||||
parent_id: parent_id.to_string(),
|
||||
live_id: live_id.to_string(),
|
||||
room_id,
|
||||
room_id: room_id.to_string(),
|
||||
title: title.into(),
|
||||
length: 0,
|
||||
size: 0,
|
||||
@@ -88,7 +88,7 @@ impl Database {
|
||||
cover,
|
||||
};
|
||||
if let Err(e) = sqlx::query("INSERT INTO records (live_id, room_id, title, length, size, cover, created_at, platform, parent_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)").bind(record.live_id.clone())
|
||||
.bind(record.room_id).bind(&record.title).bind(0).bind(0).bind(&record.cover).bind(&record.created_at).bind(platform.as_str().to_string()).bind(parent_id).execute(&lock).await {
|
||||
.bind(&record.room_id).bind(&record.title).bind(0).bind(0).bind(&record.cover).bind(&record.created_at).bind(platform.as_str().to_string()).bind(parent_id).execute(&lock).await {
|
||||
// if the record already exists, return the existing record
|
||||
if e.to_string().contains("UNIQUE constraint failed") {
|
||||
return self.get_record(room_id, live_id).await;
|
||||
@@ -180,12 +180,12 @@ impl Database {
|
||||
|
||||
pub async fn get_recent_record(
|
||||
&self,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<RecordRow>, DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
if room_id == 0 {
|
||||
if room_id.is_empty() {
|
||||
Ok(sqlx::query_as::<_, RecordRow>(
|
||||
"SELECT * FROM records ORDER BY created_at DESC LIMIT $1 OFFSET $2",
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ use recorder::platforms::PlatformType;
|
||||
/// because many room infos are collected in realtime
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
|
||||
pub struct RecorderRow {
|
||||
pub room_id: i64,
|
||||
pub room_id: String,
|
||||
pub created_at: String,
|
||||
pub platform: String,
|
||||
pub auto_start: bool,
|
||||
@@ -18,12 +18,12 @@ impl Database {
|
||||
pub async fn add_recorder(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
extra: &str,
|
||||
) -> Result<RecorderRow, DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
let recorder = RecorderRow {
|
||||
room_id,
|
||||
room_id: room_id.to_string(),
|
||||
created_at: Utc::now().to_rfc3339(),
|
||||
platform: platform.as_str().to_string(),
|
||||
auto_start: true,
|
||||
@@ -42,7 +42,7 @@ impl Database {
|
||||
Ok(recorder)
|
||||
}
|
||||
|
||||
pub async fn remove_recorder(&self, room_id: i64) -> Result<RecorderRow, DatabaseError> {
|
||||
pub async fn remove_recorder(&self, room_id: &str) -> Result<RecorderRow, DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
let recorder =
|
||||
sqlx::query_as::<_, RecorderRow>("SELECT * FROM recorders WHERE room_id = $1")
|
||||
@@ -71,7 +71,7 @@ impl Database {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn remove_archive(&self, room_id: i64) -> Result<(), DatabaseError> {
|
||||
pub async fn remove_archive(&self, room_id: &str) -> Result<(), DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
let _ = sqlx::query("DELETE FROM records WHERE room_id = $1")
|
||||
.bind(room_id)
|
||||
@@ -83,7 +83,7 @@ impl Database {
|
||||
pub async fn update_recorder(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
auto_start: bool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::Database;
|
||||
use super::DatabaseError;
|
||||
|
||||
// CREATE TABLE videos (id INTEGER PRIMARY KEY, room_id INTEGER, cover TEXT, file TEXT, length INTEGER, size INTEGER, status INTEGER, bvid TEXT, title TEXT, desc TEXT, tags TEXT, area INTEGER, created_at TEXT);
|
||||
// CREATE TABLE videos (id INTEGER PRIMARY KEY, room_id TEXT, cover TEXT, file TEXT, length INTEGER, size INTEGER, status INTEGER, bvid TEXT, title TEXT, desc TEXT, tags TEXT, area INTEGER, created_at TEXT);
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
|
||||
pub struct VideoRow {
|
||||
pub id: i64,
|
||||
pub room_id: i64,
|
||||
pub room_id: String,
|
||||
pub cover: String,
|
||||
pub file: String,
|
||||
pub note: String,
|
||||
@@ -22,7 +22,7 @@ pub struct VideoRow {
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn get_videos(&self, room_id: i64) -> Result<Vec<VideoRow>, DatabaseError> {
|
||||
pub async fn get_videos(&self, room_id: &str) -> Result<Vec<VideoRow>, DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
let videos = sqlx::query_as::<_, VideoRow>("SELECT * FROM videos WHERE room_id = $1;")
|
||||
.bind(room_id)
|
||||
@@ -69,7 +69,7 @@ impl Database {
|
||||
pub async fn add_video(&self, video: &VideoRow) -> Result<VideoRow, DatabaseError> {
|
||||
let lock = self.db.read().await.clone().unwrap();
|
||||
let sql = sqlx::query("INSERT INTO videos (room_id, cover, file, note, length, size, status, bvid, title, desc, tags, area, created_at, platform) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)")
|
||||
.bind(video.room_id)
|
||||
.bind(&video.room_id)
|
||||
.bind(&video.cover)
|
||||
.bind(&video.file)
|
||||
.bind(&video.note)
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
use async_ffmpeg_sidecar::{event::FfmpegEvent, log_parser::FfmpegLogParser};
|
||||
use tokio::io::{AsyncWriteExt, BufReader};
|
||||
|
||||
use crate::progress::progress_reporter::ProgressReporterTrait;
|
||||
use crate::{ffmpeg::hwaccel, progress::progress_reporter::ProgressReporterTrait};
|
||||
|
||||
use super::ffmpeg_path;
|
||||
|
||||
@@ -103,9 +103,10 @@ pub async fn concat_videos(
|
||||
output_folder.join(&filelist_filename).to_str().unwrap(),
|
||||
]);
|
||||
if should_encode {
|
||||
let video_encoder = hwaccel::get_x264_encoder().await;
|
||||
ffmpeg_process.args(["-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"]);
|
||||
ffmpeg_process.args(["-r", "60"]);
|
||||
ffmpeg_process.args(["-c", "libx264"]);
|
||||
ffmpeg_process.args(["-c:v", video_encoder]);
|
||||
ffmpeg_process.args(["-c:a", "aac"]);
|
||||
ffmpeg_process.args(["-b:v", "6000k"]);
|
||||
ffmpeg_process.args(["-b:a", "128k"]);
|
||||
|
||||
143
src-tauri/src/ffmpeg/hwaccel.rs
Normal file
143
src-tauri/src/ffmpeg/hwaccel.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use std::{collections::HashSet, process::Stdio};
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::ffmpeg_path;
|
||||
|
||||
const TARGET_ENCODERS: [&str; 7] = [
|
||||
"h264_nvenc",
|
||||
"h264_videotoolbox",
|
||||
"h264_qsv",
|
||||
"h264_amf",
|
||||
"h264_mf",
|
||||
"h264_vaapi",
|
||||
"h264_v4l2m2m",
|
||||
];
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
/// 检测当前环境下 FFmpeg 支持的 H.264 硬件编码器。
|
||||
///
|
||||
/// 返回值为可直接用于 `-c:v <value>` 的编码器名称列表。
|
||||
pub async fn list_supported_hwaccels() -> Result<Vec<String>, String> {
|
||||
let mut command = tokio::process::Command::new(ffmpeg_path());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
|
||||
let mut child = command
|
||||
.arg("-hide_banner")
|
||||
.arg("-encoders")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("无法启动 ffmpeg 进程: {e}"))?;
|
||||
|
||||
let mut stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| "无法获取 ffmpeg 标准输出".to_string())?;
|
||||
let mut stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| "无法获取 ffmpeg 标准错误输出".to_string())?;
|
||||
|
||||
let (mut stdout_buf, mut stderr_buf) = (String::new(), String::new());
|
||||
|
||||
let stdout_future = stdout.read_to_string(&mut stdout_buf);
|
||||
let stderr_future = stderr.read_to_string(&mut stderr_buf);
|
||||
|
||||
let (stdout_res, stderr_res, status) = tokio::join!(stdout_future, stderr_future, child.wait());
|
||||
|
||||
stdout_res.map_err(|e| format!("读取 ffmpeg 标准输出失败: {e}"))?;
|
||||
stderr_res.map_err(|e| format!("读取 ffmpeg 标准错误输出失败: {e}"))?;
|
||||
|
||||
let status = status.map_err(|e| format!("等待 ffmpeg 进程退出失败: {e}"))?;
|
||||
|
||||
if !status.success() {
|
||||
let err = if stderr_buf.trim().is_empty() {
|
||||
stdout_buf.trim().to_string()
|
||||
} else {
|
||||
stderr_buf.trim().to_string()
|
||||
};
|
||||
log::error!("ffmpeg -encoders 运行失败: {err}");
|
||||
return Err(format!("ffmpeg -encoders 运行失败: {err}"));
|
||||
}
|
||||
|
||||
let mut hwaccels = Vec::new();
|
||||
|
||||
for line in stdout_buf.lines() {
|
||||
let trimmed = line.trim_start();
|
||||
if trimmed.is_empty() || !trimmed.starts_with('V') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut parts = trimmed.split_whitespace();
|
||||
let flags = parts.next().unwrap_or_default();
|
||||
if !flags.starts_with('V') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(name) = parts.next() {
|
||||
if TARGET_ENCODERS
|
||||
.iter()
|
||||
.any(|candidate| candidate.eq_ignore_ascii_case(name))
|
||||
{
|
||||
hwaccels.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去重并保持原有顺序(即第一次出现时保留)
|
||||
let mut seen = HashSet::new();
|
||||
hwaccels.retain(|value| seen.insert(value.clone()));
|
||||
|
||||
Ok(hwaccels)
|
||||
}
|
||||
|
||||
/// 依据优先级从支持列表中挑选推荐的硬件编码器。
|
||||
///
|
||||
/// 当前优先级顺序:`h264_nvenc` > `h264_videotoolbox` > `h264_qsv` > `h264_amf` > `h264_mf` > `h264_vaapi` > `h264_v4l2m2m`。
|
||||
pub fn select_preferred_hwaccel(supported: &[String]) -> Option<&'static str> {
|
||||
const PRIORITY: [&str; 7] = [
|
||||
"h264_nvenc",
|
||||
"h264_videotoolbox",
|
||||
"h264_qsv",
|
||||
"h264_amf",
|
||||
"h264_mf",
|
||||
"h264_vaapi",
|
||||
"h264_v4l2m2m",
|
||||
];
|
||||
|
||||
PRIORITY
|
||||
.iter()
|
||||
.find(|candidate| {
|
||||
supported
|
||||
.iter()
|
||||
.any(|value| value.eq_ignore_ascii_case(candidate))
|
||||
})
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Get the preferred hardware encoder for x264
|
||||
///
|
||||
/// Returns the preferred hardware encoder for x264, or "libx264" if no hardware acceleration is available.
|
||||
pub async fn get_x264_encoder() -> &'static str {
|
||||
let mut encoder = "libx264";
|
||||
match list_supported_hwaccels().await {
|
||||
Ok(hwaccels) => {
|
||||
if let Some(arg) = select_preferred_hwaccel(&hwaccels) {
|
||||
encoder = arg;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Failed to query hardware encoders: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Selected x264 encoder: {encoder}");
|
||||
encoder
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
|
||||
pub mod general;
|
||||
pub mod hwaccel;
|
||||
pub mod playlist;
|
||||
|
||||
use crate::constants;
|
||||
@@ -14,7 +15,7 @@ use crate::subtitle_generator::{
|
||||
use async_ffmpeg_sidecar::event::{FfmpegEvent, LogLevel};
|
||||
use async_ffmpeg_sidecar::log_parser::FfmpegLogParser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncWriteExt, BufReader};
|
||||
use tokio::io::BufReader;
|
||||
|
||||
// 视频元数据结构
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -71,9 +72,10 @@ pub async fn transcode(
|
||||
if copy_codecs {
|
||||
ffmpeg_process.args(["-c:v", "copy"]).args(["-c:a", "copy"]);
|
||||
} else {
|
||||
let video_encoder = hwaccel::get_x264_encoder().await;
|
||||
ffmpeg_process
|
||||
.args(["-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"])
|
||||
.args(["-c:v", "libx264"])
|
||||
.args(["-c:v", video_encoder])
|
||||
.args(["-c:a", "aac"])
|
||||
.args(["-b:v", "6000k"])
|
||||
.args(["-b:a", "128k"])
|
||||
@@ -396,10 +398,12 @@ pub async fn encode_video_subtitle(
|
||||
#[cfg(target_os = "windows")]
|
||||
ffmpeg_process.creation_flags(CREATE_NO_WINDOW);
|
||||
|
||||
let video_encoder = hwaccel::get_x264_encoder().await;
|
||||
|
||||
let child = ffmpeg_process
|
||||
.args(["-i", file.to_str().unwrap()])
|
||||
.args(["-vf", vf.as_str()])
|
||||
.args(["-c:v", "libx264"])
|
||||
.args(["-c:v", video_encoder])
|
||||
.args(["-c:a", "copy"])
|
||||
.args(["-b:v", "6000k"])
|
||||
.args([output_path.to_str().unwrap()])
|
||||
@@ -488,10 +492,12 @@ pub async fn encode_video_danmu(
|
||||
#[cfg(target_os = "windows")]
|
||||
ffmpeg_process.creation_flags(CREATE_NO_WINDOW);
|
||||
|
||||
let video_encoder = hwaccel::get_x264_encoder().await;
|
||||
|
||||
let child = ffmpeg_process
|
||||
.args(["-i", file.to_str().unwrap()])
|
||||
.args(["-vf", &format!("ass={subtitle}")])
|
||||
.args(["-c:v", "libx264"])
|
||||
.args(["-c:v", video_encoder])
|
||||
.args(["-c:a", "copy"])
|
||||
.args(["-b:v", "6000k"])
|
||||
.args([output_file_path.to_str().unwrap()])
|
||||
@@ -778,11 +784,13 @@ pub async fn clip_from_video_file(
|
||||
#[cfg(target_os = "windows")]
|
||||
ffmpeg_process.creation_flags(CREATE_NO_WINDOW);
|
||||
|
||||
let video_encoder = hwaccel::get_x264_encoder().await;
|
||||
|
||||
let child = ffmpeg_process
|
||||
.args(["-i", &format!("{}", input_path.display())])
|
||||
.args(["-ss", &start_time.to_string()])
|
||||
.args(["-t", &duration.to_string()])
|
||||
.args(["-c:v", "libx264"])
|
||||
.args(["-c:v", video_encoder])
|
||||
.args(["-c:a", "aac"])
|
||||
.args(["-b:v", "6000k"])
|
||||
.args(["-avoid_negative_ts", "make_zero"])
|
||||
@@ -1150,124 +1158,6 @@ pub async fn check_videos(video_paths: &[&Path]) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn convert_fmp4_to_ts_raw(
|
||||
header_data: &[u8],
|
||||
source_data: &[u8],
|
||||
output_ts: &Path,
|
||||
) -> Result<(), String> {
|
||||
// Combine the data
|
||||
let mut combined_data = header_data.to_vec();
|
||||
combined_data.extend_from_slice(source_data);
|
||||
|
||||
// Build ffmpeg command to convert combined data to TS
|
||||
let mut ffmpeg_process = tokio::process::Command::new(ffmpeg_path());
|
||||
#[cfg(target_os = "windows")]
|
||||
ffmpeg_process.creation_flags(CREATE_NO_WINDOW);
|
||||
|
||||
let child = ffmpeg_process
|
||||
.args(["-f", "mp4"])
|
||||
.args(["-i", "-"]) // Read from stdin
|
||||
.args(["-c:v", "libx264"])
|
||||
.args(["-b:v", "6000k"])
|
||||
.args(["-maxrate", "10000k"])
|
||||
.args(["-bufsize", "16000k"])
|
||||
.args(["-c:a", "copy"])
|
||||
.args(["-threads", "0"])
|
||||
.args(["-f", "mpegts"])
|
||||
.args(["-y", output_ts.to_str().unwrap()]) // Overwrite output
|
||||
.args(["-progress", "pipe:2"]) // Progress to stderr
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn();
|
||||
|
||||
if let Err(e) = child {
|
||||
return Err(format!("Failed to spawn ffmpeg process: {e}"));
|
||||
}
|
||||
|
||||
let mut child = child.unwrap();
|
||||
|
||||
// Write the combined data to stdin and close it
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin
|
||||
.write_all(&combined_data)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to write data to ffmpeg stdin: {e}"))?;
|
||||
// stdin is automatically closed when dropped
|
||||
}
|
||||
|
||||
// Parse ffmpeg output for progress and errors
|
||||
let stderr = child.stderr.take().unwrap();
|
||||
let reader = BufReader::new(stderr);
|
||||
let mut parser = FfmpegLogParser::new(reader);
|
||||
|
||||
let mut conversion_error = None;
|
||||
while let Ok(event) = parser.parse_next_event().await {
|
||||
match event {
|
||||
FfmpegEvent::LogEOF => break,
|
||||
FfmpegEvent::Log(level, content) => {
|
||||
if content.contains("error") || level == LogLevel::Error {
|
||||
log::error!("fMP4 to TS conversion error: {content}");
|
||||
}
|
||||
}
|
||||
FfmpegEvent::Error(e) => {
|
||||
log::error!("fMP4 to TS conversion error: {e}");
|
||||
conversion_error = Some(e.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for ffmpeg to complete
|
||||
if let Err(e) = child.wait().await {
|
||||
return Err(format!("ffmpeg process failed: {e}"));
|
||||
}
|
||||
|
||||
// Check for conversion errors
|
||||
if let Some(error) = conversion_error {
|
||||
Err(error)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert fragmented MP4 (fMP4) files to MPEG-TS format
|
||||
/// Combines an initialization segment (header) and a media segment (source) into a single TS file
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `header` - Path to the initialization segment (.mp4)
|
||||
/// * `source` - Path to the media segment (.m4s)
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Result` indicating success or failure with error message
|
||||
#[allow(unused)]
|
||||
pub async fn convert_fmp4_to_ts(header: &Path, source: &Path) -> Result<(), String> {
|
||||
log::info!(
|
||||
"Converting fMP4 to TS: {} + {}",
|
||||
header.display(),
|
||||
source.display()
|
||||
);
|
||||
|
||||
// Check if input files exist
|
||||
if !header.exists() {
|
||||
return Err(format!("Header file does not exist: {}", header.display()));
|
||||
}
|
||||
if !source.exists() {
|
||||
return Err(format!("Source file does not exist: {}", source.display()));
|
||||
}
|
||||
|
||||
let output_ts = source.with_extension("ts");
|
||||
|
||||
// Read the header and source files into memory
|
||||
let header_data = tokio::fs::read(header)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read header file: {e}"))?;
|
||||
let source_data = tokio::fs::read(source)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read source file: {e}"))?;
|
||||
|
||||
convert_fmp4_to_ts_raw(&header_data, &source_data, &output_ts).await
|
||||
}
|
||||
|
||||
// tests
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -1399,6 +1289,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// 测试硬件加速能力探测
|
||||
#[tokio::test]
|
||||
async fn test_list_supported_hwaccels() {
|
||||
match super::hwaccel::list_supported_hwaccels().await {
|
||||
Ok(hwaccels) => {
|
||||
println!("hwaccels: {:?}", hwaccels);
|
||||
let mut sorted = hwaccels.clone();
|
||||
sorted.sort();
|
||||
sorted.dedup();
|
||||
assert_eq!(sorted.len(), hwaccels.len());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("FFmpeg hardware acceleration query not available for testing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_preferred_hwaccel() {
|
||||
let cases = vec![
|
||||
(vec!["h264_nvenc", "h264_vaapi"], Some("h264_nvenc")),
|
||||
(
|
||||
vec!["h264_videotoolbox", "h264_qsv"],
|
||||
Some("h264_videotoolbox"),
|
||||
),
|
||||
(vec!["h264_vaapi"], Some("h264_vaapi")),
|
||||
(vec!["h264_v4l2m2m"], Some("h264_v4l2m2m")),
|
||||
(vec!["libx264"], None),
|
||||
];
|
||||
|
||||
for (inputs, expected) in cases {
|
||||
let inputs = inputs.into_iter().map(String::from).collect::<Vec<_>>();
|
||||
assert_eq!(super::hwaccel::select_preferred_hwaccel(&inputs), expected);
|
||||
}
|
||||
}
|
||||
|
||||
// 测试字幕生成错误处理
|
||||
#[tokio::test]
|
||||
async fn test_generate_video_subtitle_errors() {
|
||||
@@ -1480,52 +1406,4 @@ mod tests {
|
||||
assert!(chunk_dir.to_string_lossy().contains("_chunks"));
|
||||
assert!(chunk_dir.to_string_lossy().contains("test"));
|
||||
}
|
||||
|
||||
// 测试 fMP4 到 TS 转换
|
||||
#[tokio::test]
|
||||
async fn test_convert_fmp4_to_ts() {
|
||||
let header_file = Path::new("tests/video/init.m4s");
|
||||
let segment_file = Path::new("tests/video/segment.m4s");
|
||||
let output_file = Path::new("tests/video/segment.ts");
|
||||
|
||||
// 如果测试文件存在,则进行转换测试
|
||||
if header_file.exists() && segment_file.exists() {
|
||||
let result = convert_fmp4_to_ts(header_file, segment_file).await;
|
||||
|
||||
// 检查转换是否成功
|
||||
match result {
|
||||
Ok(()) => {
|
||||
// 检查输出文件是否创建
|
||||
assert!(output_file.exists());
|
||||
log::info!("fMP4 to TS conversion test passed");
|
||||
|
||||
// 清理测试文件
|
||||
let _ = std::fs::remove_file(output_file);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("fMP4 to TS conversion test failed: {}", e);
|
||||
// 对于测试文件不存在或其他错误,我们仍然认为测试通过
|
||||
// 因为这不是功能性问题
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("Test files not found, skipping fMP4 to TS conversion test");
|
||||
}
|
||||
}
|
||||
|
||||
// 测试 fMP4 到 TS 转换的错误处理
|
||||
#[tokio::test]
|
||||
async fn test_convert_fmp4_to_ts_error_handling() {
|
||||
let non_existent_header = Path::new("tests/video/non_existent_init.mp4");
|
||||
let non_existent_segment = Path::new("tests/video/non_existent_segment.m4s");
|
||||
|
||||
// 测试文件不存在的错误处理
|
||||
let result = convert_fmp4_to_ts(non_existent_header, non_existent_segment).await;
|
||||
assert!(result.is_err());
|
||||
|
||||
let error_msg = result.unwrap_err();
|
||||
assert!(error_msg.contains("does not exist"));
|
||||
|
||||
log::info!("fMP4 to TS error handling test passed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::database::account::AccountRow;
|
||||
use crate::state::State;
|
||||
use crate::state_type;
|
||||
use chrono::Utc;
|
||||
use recorder::platforms::bilibili::api::{QrInfo, QrStatus};
|
||||
use recorder::platforms::{bilibili, douyin};
|
||||
use recorder::platforms::{bilibili, douyin, huya, PlatformType};
|
||||
use recorder::UserInfo;
|
||||
|
||||
use hyper::header::HeaderValue;
|
||||
#[cfg(feature = "gui")]
|
||||
@@ -16,107 +20,163 @@ pub async fn get_accounts(state: state_type!()) -> Result<super::AccountInfo, St
|
||||
Ok(account_info)
|
||||
}
|
||||
|
||||
fn get_item_from_cookies(name: &str, cookies: &str) -> Result<String, String> {
|
||||
Ok(cookies
|
||||
.split(';')
|
||||
.map(str::trim)
|
||||
.find_map(|cookie| cookie.strip_prefix(format!("{name}=").as_str()))
|
||||
.ok_or_else(|| format!("Invalid cookies: missing {name}").to_string())?
|
||||
.to_string())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "gui", tauri::command)]
|
||||
pub async fn add_account(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
cookies: &str,
|
||||
) -> Result<AccountRow, String> {
|
||||
) -> Result<(), String> {
|
||||
// check if cookies is valid
|
||||
if let Err(e) = cookies.parse::<HeaderValue>() {
|
||||
return Err(format!("Invalid cookies: {e}"));
|
||||
}
|
||||
let account = state.db.add_account(&platform, cookies).await?;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
if platform == "bilibili" {
|
||||
let account_info =
|
||||
match bilibili::api::get_user_info(&client, &account.to_account(), account.uid).await {
|
||||
Ok(account_info) => account_info,
|
||||
let platform = PlatformType::from_str(&platform).map_err(|_| "Invalid platform".to_string())?;
|
||||
|
||||
let csrf = match platform {
|
||||
PlatformType::BiliBili => {
|
||||
cookies
|
||||
.split(';')
|
||||
.map(str::trim)
|
||||
.find_map(|cookie| -> Option<String> {
|
||||
if cookie.starts_with("bili_jct=") {
|
||||
let var_name = &"bili_jct=";
|
||||
Some(cookie[var_name.len()..].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => Some(String::new()),
|
||||
};
|
||||
|
||||
// fetch basic account user info
|
||||
let client = reqwest::Client::new();
|
||||
let user_info = match platform {
|
||||
PlatformType::BiliBili => {
|
||||
// For Bilibili, extract numeric uid from cookies
|
||||
if csrf.is_none() {
|
||||
return Err("Invalid bilibili cookies".to_string());
|
||||
}
|
||||
let uid = get_item_from_cookies("DedeUserID", cookies)?;
|
||||
let tmp_account = AccountRow {
|
||||
platform: platform.as_str().to_string(),
|
||||
uid,
|
||||
name: String::new(),
|
||||
avatar: String::new(),
|
||||
csrf: csrf.clone().unwrap(),
|
||||
cookies: cookies.into(),
|
||||
created_at: Utc::now().to_rfc3339(),
|
||||
};
|
||||
match bilibili::api::get_user_info(&client, &tmp_account.to_account(), &tmp_account.uid)
|
||||
.await
|
||||
{
|
||||
Ok(user_info) => UserInfo {
|
||||
user_id: user_info.user_id,
|
||||
user_name: user_info.user_name,
|
||||
user_avatar: user_info.user_avatar_url,
|
||||
},
|
||||
Err(e) => {
|
||||
return Err(e.to_string());
|
||||
}
|
||||
};
|
||||
state
|
||||
.db
|
||||
.update_account(
|
||||
&platform,
|
||||
account_info.user_id,
|
||||
&account_info.user_name,
|
||||
&account_info.user_avatar_url,
|
||||
)
|
||||
.await?;
|
||||
return Ok(account);
|
||||
}
|
||||
|
||||
if platform == "douyin" {
|
||||
// Get user info from Douyin API
|
||||
match douyin::api::get_user_info(&client, &account.to_account()).await {
|
||||
Ok(user_info) => {
|
||||
// For Douyin, use sec_uid as the primary identifier in id_str field
|
||||
let avatar_url = user_info
|
||||
.avatar_thumb
|
||||
.url_list
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
state
|
||||
.db
|
||||
.update_account_with_id_str(
|
||||
&account,
|
||||
&user_info.sec_uid,
|
||||
&user_info.nickname,
|
||||
&avatar_url,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to get Douyin user info: {e}");
|
||||
// Keep the account but with default values
|
||||
}
|
||||
}
|
||||
PlatformType::Douyin => {
|
||||
let tmp_account = AccountRow {
|
||||
platform: platform.as_str().to_string(),
|
||||
uid: "".into(),
|
||||
name: String::new(),
|
||||
avatar: String::new(),
|
||||
csrf: "".into(),
|
||||
cookies: cookies.into(),
|
||||
created_at: Utc::now().to_rfc3339(),
|
||||
};
|
||||
|
||||
return Ok(account);
|
||||
}
|
||||
match douyin::api::get_user_info(&client, &tmp_account.to_account()).await {
|
||||
Ok(user_info) => {
|
||||
// For Douyin, use sec_uid as the primary identifier in id_str field
|
||||
let avatar_url = user_info
|
||||
.avatar_thumb
|
||||
.url_list
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
// if platform == "huya" {
|
||||
// let huya_client = crate::recorder::huya::client::HuyaClient::new();
|
||||
// match huya_client.get_user_info(&account).await {
|
||||
// Ok(user_info) => {
|
||||
// state
|
||||
// .db
|
||||
// .update_account(
|
||||
// &platform,
|
||||
// user_info.user_id,
|
||||
// &user_info.user_name,
|
||||
// &user_info.user_avatar_url,
|
||||
// )
|
||||
// .await?;
|
||||
// }
|
||||
// Err(e) => {
|
||||
// log::warn!("Failed to get Huya user info: {e}");
|
||||
// // Keep the account but with default values
|
||||
// }
|
||||
// }
|
||||
// return Ok(account);
|
||||
// }
|
||||
UserInfo {
|
||||
user_id: user_info.sec_uid,
|
||||
user_name: user_info.nickname,
|
||||
user_avatar: avatar_url,
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("Failed to get Douyin user info: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
PlatformType::Huya => {
|
||||
let user_id = get_item_from_cookies("yyuid", cookies)?;
|
||||
|
||||
todo!("unsupported platform: {platform}");
|
||||
let tmp_account = AccountRow {
|
||||
platform: platform.as_str().to_string(),
|
||||
uid: user_id,
|
||||
name: String::new(),
|
||||
avatar: String::new(),
|
||||
csrf: "".into(),
|
||||
cookies: cookies.into(),
|
||||
created_at: Utc::now().to_rfc3339(),
|
||||
};
|
||||
|
||||
match huya::api::get_user_info(&client, &tmp_account.to_account()).await {
|
||||
Ok(user_info) => UserInfo {
|
||||
user_id: user_info.user_id,
|
||||
user_name: user_info.user_name,
|
||||
user_avatar: user_info.user_avatar,
|
||||
},
|
||||
Err(e) => {
|
||||
return Err(format!("Failed to get Huya user info: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
PlatformType::Youtube => {
|
||||
// unsupported
|
||||
return Err("Unsupported platform".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let account = AccountRow {
|
||||
platform: platform.as_str().to_string(),
|
||||
uid: user_info.user_id,
|
||||
name: user_info.user_name,
|
||||
avatar: user_info.user_avatar,
|
||||
csrf: csrf.unwrap(),
|
||||
cookies: cookies.into(),
|
||||
created_at: Utc::now().to_rfc3339(),
|
||||
};
|
||||
state.db.add_account(&account).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "gui", tauri::command)]
|
||||
pub async fn remove_account(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
uid: i64,
|
||||
uid: String,
|
||||
) -> Result<(), String> {
|
||||
if platform == "bilibili" {
|
||||
let account = state.db.get_account(&platform, uid).await?;
|
||||
let account = state.db.get_account(&platform, &uid).await?;
|
||||
let client = reqwest::Client::new();
|
||||
let _ = bilibili::api::logout(&client, &account.to_account()).await;
|
||||
}
|
||||
Ok(state.db.remove_account(&platform, uid).await?)
|
||||
Ok(state.db.remove_account(&platform, &uid).await?)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "gui", tauri::command)]
|
||||
@@ -141,3 +201,21 @@ pub async fn get_qr(_state: state_type!()) -> Result<QrInfo, ()> {
|
||||
Err(_e) => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_item_from_cookies() {
|
||||
let cookies = "DedeUserID=1234567890; bili_jct=1234567890; yyuid=1234567890";
|
||||
let uid = get_item_from_cookies("DedeUserID", cookies).unwrap();
|
||||
assert_eq!(uid, "1234567890");
|
||||
let uid = get_item_from_cookies("yyuid", cookies).unwrap();
|
||||
assert_eq!(uid, "1234567890");
|
||||
let uid = get_item_from_cookies("bili_jct", cookies).unwrap();
|
||||
assert_eq!(uid, "1234567890");
|
||||
let uid = get_item_from_cookies("unknown", cookies).unwrap_err();
|
||||
assert_eq!(uid, "Invalid cookies: missing unknown");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub async fn get_recorder_list(state: state_type!()) -> Result<RecorderList, ()>
|
||||
pub async fn add_recorder(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
mut extra: String,
|
||||
) -> Result<RecorderRow, String> {
|
||||
log::info!("Add recorder: {platform} {room_id}");
|
||||
@@ -49,7 +49,7 @@ pub async fn add_recorder(
|
||||
}
|
||||
PlatformType::Douyin => {
|
||||
let client = reqwest::Client::new();
|
||||
let sec_uid = douyin::api::get_room_owner_sec_uid(&client, room_id)
|
||||
let sec_uid = douyin::api::get_room_owner_sec_uid(&client, &room_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
extra = sec_uid;
|
||||
@@ -74,11 +74,11 @@ pub async fn add_recorder(
|
||||
match account {
|
||||
Ok(account) => match state
|
||||
.recorder_manager
|
||||
.add_recorder(&account, platform, room_id, &extra, true)
|
||||
.add_recorder(&account, platform, &room_id, &extra, true)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
let room = state.db.add_recorder(platform, room_id, &extra).await?;
|
||||
let room = state.db.add_recorder(platform, &room_id, &extra).await?;
|
||||
state
|
||||
.db
|
||||
.new_message("添加直播间", &format!("添加了新直播间 {room_id}"))
|
||||
@@ -109,13 +109,13 @@ pub async fn add_recorder(
|
||||
pub async fn remove_recorder(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
) -> Result<(), String> {
|
||||
log::info!("Remove recorder: {platform} {room_id}");
|
||||
let platform = PlatformType::from_str(&platform).unwrap();
|
||||
match state
|
||||
.recorder_manager
|
||||
.remove_recorder(platform, room_id)
|
||||
.remove_recorder(platform, &room_id)
|
||||
.await
|
||||
{
|
||||
Ok(recorder) => {
|
||||
@@ -145,12 +145,12 @@ pub async fn remove_recorder(
|
||||
pub async fn get_room_info(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
) -> Result<RecorderInfo, String> {
|
||||
let platform = PlatformType::from_str(&platform).unwrap();
|
||||
if let Some(info) = state
|
||||
.recorder_manager
|
||||
.get_recorder_info(platform, room_id)
|
||||
.get_recorder_info(platform, &room_id)
|
||||
.await
|
||||
{
|
||||
Ok(info)
|
||||
@@ -167,37 +167,37 @@ pub async fn get_archive_disk_usage(state: state_type!()) -> Result<i64, String>
|
||||
#[cfg_attr(feature = "gui", tauri::command)]
|
||||
pub async fn get_archives(
|
||||
state: state_type!(),
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<RecordRow>, String> {
|
||||
Ok(state
|
||||
.recorder_manager
|
||||
.get_archives(room_id, offset, limit)
|
||||
.get_archives(&room_id, offset, limit)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "gui", tauri::command)]
|
||||
pub async fn get_archive(
|
||||
state: state_type!(),
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
) -> Result<RecordRow, String> {
|
||||
Ok(state
|
||||
.recorder_manager
|
||||
.get_archive(room_id, &live_id)
|
||||
.get_archive(&room_id, &live_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "gui", tauri::command)]
|
||||
pub async fn get_archives_by_parent_id(
|
||||
state: state_type!(),
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
parent_id: String,
|
||||
) -> Result<Vec<RecordRow>, String> {
|
||||
Ok(state
|
||||
.db
|
||||
.get_archives_by_parent_id(room_id, &parent_id)
|
||||
.get_archives_by_parent_id(&room_id, &parent_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -205,13 +205,13 @@ pub async fn get_archives_by_parent_id(
|
||||
pub async fn get_archive_subtitle(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
) -> Result<String, String> {
|
||||
let platform = PlatformType::from_str(&platform)?;
|
||||
Ok(state
|
||||
.recorder_manager
|
||||
.get_archive_subtitle(platform, room_id, &live_id)
|
||||
.get_archive_subtitle(platform, &room_id, &live_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -219,13 +219,13 @@ pub async fn get_archive_subtitle(
|
||||
pub async fn generate_archive_subtitle(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
) -> Result<String, String> {
|
||||
let platform = PlatformType::from_str(&platform)?;
|
||||
Ok(state
|
||||
.recorder_manager
|
||||
.generate_archive_subtitle(platform, room_id, &live_id)
|
||||
.generate_archive_subtitle(platform, &room_id, &live_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -233,13 +233,13 @@ pub async fn generate_archive_subtitle(
|
||||
pub async fn delete_archive(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
) -> Result<(), String> {
|
||||
let platform = PlatformType::from_str(&platform)?;
|
||||
let to_delete = state
|
||||
.recorder_manager
|
||||
.delete_archive(platform, room_id, &live_id)
|
||||
.delete_archive(platform, &room_id, &live_id)
|
||||
.await?;
|
||||
state
|
||||
.db
|
||||
@@ -261,7 +261,7 @@ pub async fn delete_archive(
|
||||
pub async fn delete_archives(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_ids: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
let platform = PlatformType::from_str(&platform)?;
|
||||
@@ -269,7 +269,7 @@ pub async fn delete_archives(
|
||||
.recorder_manager
|
||||
.delete_archives(
|
||||
platform,
|
||||
room_id,
|
||||
&room_id,
|
||||
&live_ids
|
||||
.iter()
|
||||
.map(std::string::String::as_str)
|
||||
@@ -298,13 +298,13 @@ pub async fn delete_archives(
|
||||
pub async fn get_danmu_record(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
) -> Result<Vec<DanmuEntry>, String> {
|
||||
let platform = PlatformType::from_str(&platform)?;
|
||||
Ok(state
|
||||
.recorder_manager
|
||||
.load_danmus(platform, room_id, &live_id)
|
||||
.load_danmus(platform, &room_id, &live_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ pub async fn get_danmu_record(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExportDanmuOptions {
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
x: i64,
|
||||
y: i64,
|
||||
@@ -327,7 +327,7 @@ pub async fn export_danmu(
|
||||
let platform = PlatformType::from_str(&options.platform)?;
|
||||
let mut danmus = state
|
||||
.recorder_manager
|
||||
.load_danmus(platform, options.room_id, &options.live_id)
|
||||
.load_danmus(platform, &options.room_id, &options.live_id)
|
||||
.await?;
|
||||
|
||||
log::debug!("First danmu entry: {:?}", danmus.first());
|
||||
@@ -357,13 +357,13 @@ pub async fn export_danmu(
|
||||
#[cfg_attr(feature = "gui", tauri::command)]
|
||||
pub async fn send_danmaku(
|
||||
state: state_type!(),
|
||||
uid: i64,
|
||||
room_id: i64,
|
||||
uid: String,
|
||||
room_id: String,
|
||||
message: String,
|
||||
) -> Result<(), String> {
|
||||
let account = state.db.get_account("bilibili", uid).await?;
|
||||
let account = state.db.get_account("bilibili", &uid).await?;
|
||||
let client = reqwest::Client::new();
|
||||
match bilibili::api::send_danmaku(&client, &account.to_account(), room_id, &message).await {
|
||||
match bilibili::api::send_danmaku(&client, &account.to_account(), &room_id, &message).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
@@ -388,11 +388,11 @@ pub async fn get_today_record_count(state: state_type!()) -> Result<i64, String>
|
||||
#[cfg_attr(feature = "gui", tauri::command)]
|
||||
pub async fn get_recent_record(
|
||||
state: state_type!(),
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<RecordRow>, String> {
|
||||
match state.db.get_recent_record(room_id, offset, limit).await {
|
||||
match state.db.get_recent_record(&room_id, offset, limit).await {
|
||||
Ok(records) => Ok(records),
|
||||
Err(e) => Err(format!("Failed to get recent record: {e}")),
|
||||
}
|
||||
@@ -402,14 +402,14 @@ pub async fn get_recent_record(
|
||||
pub async fn set_enable(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
log::info!("Set enable for recorder {platform} {room_id} {enabled}");
|
||||
let platform = PlatformType::from_str(&platform)?;
|
||||
state
|
||||
.recorder_manager
|
||||
.set_enable(platform, room_id, enabled)
|
||||
.set_enable(platform, &room_id, enabled)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -434,7 +434,7 @@ pub async fn generate_whole_clip(
|
||||
state: state_type!(),
|
||||
encode_danmu: bool,
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
parent_id: String,
|
||||
) -> Result<TaskRow, String> {
|
||||
log::info!("Generate whole clip for {platform} {room_id} {parent_id}");
|
||||
@@ -470,7 +470,7 @@ pub async fn generate_whole_clip(
|
||||
tokio::spawn(async move {
|
||||
match state_clone
|
||||
.recorder_manager
|
||||
.generate_whole_clip(Some(&reporter), encode_danmu, platform, room_id, parent_id)
|
||||
.generate_whole_clip(Some(&reporter), encode_danmu, platform, &room_id, parent_id)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
|
||||
@@ -216,7 +216,7 @@ pub async fn open_log_folder(state: state_type!()) -> Result<(), String> {
|
||||
pub async fn open_live(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
) -> Result<(), String> {
|
||||
log::info!("Open player window: {room_id} {live_id}");
|
||||
@@ -303,6 +303,7 @@ pub async fn list_folder(_state: state_type!(), path: String) -> Result<Vec<Stri
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "gui", tauri::command)]
|
||||
#[allow(dead_code)]
|
||||
pub async fn file_exists(_state: state_type!(), path: String) -> Result<bool, String> {
|
||||
let path = PathBuf::from(path);
|
||||
match std::fs::metadata(&path) {
|
||||
|
||||
@@ -435,7 +435,7 @@ async fn clip_range_inner(
|
||||
.add_video(&VideoRow {
|
||||
id: 0,
|
||||
status: 0,
|
||||
room_id: params.room_id,
|
||||
room_id: params.room_id.clone(),
|
||||
created_at: Local::now().to_rfc3339(),
|
||||
cover: cover_file
|
||||
.file_name()
|
||||
@@ -464,7 +464,7 @@ async fn clip_range_inner(
|
||||
"生成新切片",
|
||||
&format!(
|
||||
"生成了房间 {} 的切片,长度 {}s:{}",
|
||||
params.room_id,
|
||||
¶ms.room_id,
|
||||
params
|
||||
.range
|
||||
.as_ref()
|
||||
@@ -482,7 +482,7 @@ async fn clip_range_inner(
|
||||
.title("BiliShadowReplay - 切片完成")
|
||||
.body(format!(
|
||||
"生成了房间 {} 的切片: {}",
|
||||
params.room_id, filename
|
||||
¶ms.room_id, filename
|
||||
))
|
||||
.show()
|
||||
.unwrap();
|
||||
@@ -497,8 +497,8 @@ async fn clip_range_inner(
|
||||
pub async fn upload_procedure(
|
||||
state: state_type!(),
|
||||
event_id: String,
|
||||
uid: i64,
|
||||
room_id: i64,
|
||||
uid: String,
|
||||
room_id: String,
|
||||
video_id: i64,
|
||||
cover: String,
|
||||
profile: Profile,
|
||||
@@ -547,13 +547,13 @@ pub async fn upload_procedure(
|
||||
async fn upload_procedure_inner(
|
||||
state: &state_type!(),
|
||||
reporter: &ProgressReporter,
|
||||
uid: i64,
|
||||
room_id: i64,
|
||||
uid: String,
|
||||
room_id: String,
|
||||
video_id: i64,
|
||||
cover: String,
|
||||
mut profile: Profile,
|
||||
) -> Result<String, String> {
|
||||
let account = state.db.get_account("bilibili", uid).await?;
|
||||
let account = state.db.get_account("bilibili", &uid).await?;
|
||||
// get video info from dbs
|
||||
let mut video_row = state.db.get_video(video_id).await?;
|
||||
// construct file path
|
||||
@@ -625,10 +625,10 @@ pub async fn get_video(state: state_type!(), id: i64) -> Result<VideoRow, String
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "gui", tauri::command)]
|
||||
pub async fn get_videos(state: state_type!(), room_id: i64) -> Result<Vec<VideoRow>, String> {
|
||||
pub async fn get_videos(state: state_type!(), room_id: String) -> Result<Vec<VideoRow>, String> {
|
||||
state
|
||||
.db
|
||||
.get_videos(room_id)
|
||||
.get_videos(&room_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -949,7 +949,7 @@ pub async fn import_external_video(
|
||||
event_id: String,
|
||||
file_path: String,
|
||||
title: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
) -> Result<VideoRow, String> {
|
||||
#[cfg(feature = "gui")]
|
||||
let emitter = EventEmitter::new(state.app_handle.clone());
|
||||
@@ -1293,7 +1293,7 @@ pub async fn batch_import_external_videos(
|
||||
state: state_type!(),
|
||||
event_id: String,
|
||||
file_paths: Vec<String>,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
) -> Result<BatchImportResult, String> {
|
||||
if file_paths.is_empty() {
|
||||
return Ok(BatchImportResult {
|
||||
@@ -1343,7 +1343,7 @@ pub async fn batch_import_external_videos(
|
||||
file_event_id,
|
||||
file_path.clone(),
|
||||
title,
|
||||
room_id,
|
||||
room_id.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::{
|
||||
use crate::{
|
||||
config::Config,
|
||||
database::{
|
||||
account::AccountRow, message::MessageRow, record::RecordRow, recorder::RecorderRow,
|
||||
task::TaskRow, video::VideoRow,
|
||||
message::MessageRow, record::RecordRow, recorder::RecorderRow, task::TaskRow,
|
||||
video::VideoRow,
|
||||
},
|
||||
handlers::{
|
||||
account::{
|
||||
@@ -137,17 +137,16 @@ struct AddAccountRequest {
|
||||
async fn handler_add_account(
|
||||
state: axum::extract::State<State>,
|
||||
Json(param): Json<AddAccountRequest>,
|
||||
) -> Result<Json<ApiResponse<AccountRow>>, ApiError> {
|
||||
let mut account = add_account(state.0, param.platform, ¶m.cookies).await?;
|
||||
account.cookies = "".to_string();
|
||||
Ok(Json(ApiResponse::success(account)))
|
||||
) -> Result<Json<ApiResponse<()>>, ApiError> {
|
||||
add_account(state.0, param.platform, ¶m.cookies).await?;
|
||||
Ok(Json(ApiResponse::success(())))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RemoveAccountRequest {
|
||||
platform: String,
|
||||
uid: i64,
|
||||
uid: String,
|
||||
}
|
||||
|
||||
async fn handler_remove_account(
|
||||
@@ -449,7 +448,7 @@ async fn handler_get_recorder_list(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AddRecorderRequest {
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
extra: String,
|
||||
}
|
||||
|
||||
@@ -467,7 +466,7 @@ async fn handler_add_recorder(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RemoveRecorderRequest {
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
}
|
||||
|
||||
async fn handler_remove_recorder(
|
||||
@@ -484,7 +483,7 @@ async fn handler_remove_recorder(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetRoomInfoRequest {
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
}
|
||||
|
||||
async fn handler_get_room_info(
|
||||
@@ -505,7 +504,7 @@ async fn handler_get_archive_disk_usage(
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetArchivesRequest {
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
}
|
||||
@@ -521,7 +520,7 @@ async fn handler_get_archives(
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetArchiveRequest {
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
}
|
||||
|
||||
@@ -537,7 +536,7 @@ async fn handler_get_archive(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetArchiveSubtitleRequest {
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
}
|
||||
|
||||
@@ -554,7 +553,7 @@ async fn handler_get_archive_subtitle(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GenerateArchiveSubtitleRequest {
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
}
|
||||
|
||||
@@ -571,7 +570,7 @@ async fn handler_generate_archive_subtitle(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DeleteArchiveRequest {
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
}
|
||||
|
||||
@@ -587,7 +586,7 @@ async fn handler_delete_archive(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DeleteArchivesRequest {
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_ids: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -603,7 +602,7 @@ async fn handler_delete_archives(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetDanmuRecordRequest {
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
live_id: String,
|
||||
}
|
||||
|
||||
@@ -619,8 +618,8 @@ async fn handler_get_danmu_record(
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SendDanmakuRequest {
|
||||
uid: i64,
|
||||
room_id: i64,
|
||||
uid: String,
|
||||
room_id: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
@@ -649,7 +648,7 @@ async fn handler_get_today_record_count(
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetRecentRecordRequest {
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
}
|
||||
@@ -667,7 +666,7 @@ async fn handler_get_recent_record(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SetEnableRequest {
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
@@ -698,8 +697,8 @@ async fn handler_clip_range(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UploadProcedureRequest {
|
||||
event_id: String,
|
||||
uid: i64,
|
||||
room_id: i64,
|
||||
uid: String,
|
||||
room_id: String,
|
||||
video_id: i64,
|
||||
cover: String,
|
||||
profile: Profile,
|
||||
@@ -753,7 +752,7 @@ async fn handler_get_video(
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetVideosRequest {
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
}
|
||||
|
||||
async fn handler_get_videos(
|
||||
@@ -962,7 +961,7 @@ struct ImportExternalVideoRequest {
|
||||
event_id: String,
|
||||
file_path: String,
|
||||
title: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
}
|
||||
|
||||
async fn handler_import_external_video(
|
||||
@@ -1017,7 +1016,7 @@ struct GetFileSizeRequest {
|
||||
struct GenerateWholeClipRequest {
|
||||
encode_danmu: bool,
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
parent_id: String,
|
||||
}
|
||||
|
||||
@@ -1039,7 +1038,7 @@ async fn handler_generate_whole_clip(
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetArchivesByParentIdRequest {
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
parent_id: String,
|
||||
}
|
||||
|
||||
@@ -1071,7 +1070,7 @@ async fn handler_update_danmu_ass_options(
|
||||
struct BatchImportExternalVideosRequest {
|
||||
event_id: String,
|
||||
file_paths: Vec<String>,
|
||||
room_id: i64,
|
||||
room_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -1238,7 +1237,7 @@ async fn handler_upload_and_import_files(
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<ApiResponse<UploadAndImportResponse>>, ApiError> {
|
||||
let mut uploaded_files = Vec::new();
|
||||
let mut room_id = 0i64;
|
||||
let mut room_id = "".to_string();
|
||||
let upload_dir = std::env::temp_dir().join("bsr_uploads");
|
||||
|
||||
// 确保上传目录存在
|
||||
@@ -1252,8 +1251,7 @@ async fn handler_upload_and_import_files(
|
||||
match name {
|
||||
"room_id" => {
|
||||
// 读取房间ID
|
||||
let text = field.text().await.map_err(|e| e.to_string())?;
|
||||
room_id = text.parse().unwrap_or(0);
|
||||
room_id = field.text().await.map_err(|e| e.to_string())?;
|
||||
}
|
||||
"files" => {
|
||||
// 处理文件上传
|
||||
|
||||
@@ -197,6 +197,198 @@ fn get_migrations() -> Vec<Migration> {
|
||||
sql: r"ALTER TABLE records ADD COLUMN parent_id TEXT;",
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
// change records table primary key to (parent_id, live_id)
|
||||
Migration {
|
||||
version: 10,
|
||||
description: "change_records_primary_key",
|
||||
sql: r"
|
||||
CREATE TABLE records_new (
|
||||
parent_id TEXT NOT NULL DEFAULT '',
|
||||
live_id TEXT NOT NULL,
|
||||
platform TEXT NOT NULL DEFAULT 'bilibili',
|
||||
room_id TEXT,
|
||||
title TEXT,
|
||||
length INTEGER,
|
||||
size INTEGER,
|
||||
cover BLOB,
|
||||
created_at TEXT,
|
||||
PRIMARY KEY (parent_id, live_id)
|
||||
);
|
||||
INSERT INTO records_new (
|
||||
parent_id,
|
||||
live_id,
|
||||
platform,
|
||||
room_id,
|
||||
title,
|
||||
length,
|
||||
size,
|
||||
cover,
|
||||
created_at
|
||||
)
|
||||
SELECT
|
||||
COALESCE(parent_id, live_id) AS parent_id,
|
||||
live_id,
|
||||
platform,
|
||||
CAST(room_id AS TEXT),
|
||||
title,
|
||||
length,
|
||||
size,
|
||||
cover,
|
||||
created_at
|
||||
FROM records;
|
||||
DROP TABLE records;
|
||||
ALTER TABLE records_new RENAME TO records;
|
||||
CREATE INDEX IF NOT EXISTS idx_records_live_id ON records (room_id, live_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_created_at ON records (room_id, created_at);
|
||||
",
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
// convert room_id columns to TEXT across tables
|
||||
Migration {
|
||||
version: 11,
|
||||
description: "convert_room_id_to_text",
|
||||
sql: r"
|
||||
CREATE TABLE recorders_new (
|
||||
room_id TEXT PRIMARY KEY,
|
||||
platform TEXT NOT NULL DEFAULT 'bilibili',
|
||||
created_at TEXT,
|
||||
auto_start INTEGER NOT NULL DEFAULT 1,
|
||||
extra TEXT
|
||||
);
|
||||
INSERT INTO recorders_new (
|
||||
room_id,
|
||||
platform,
|
||||
created_at,
|
||||
auto_start,
|
||||
extra
|
||||
)
|
||||
SELECT
|
||||
CAST(room_id AS TEXT),
|
||||
platform,
|
||||
created_at,
|
||||
COALESCE(auto_start, 1),
|
||||
extra
|
||||
FROM recorders;
|
||||
DROP TABLE recorders;
|
||||
ALTER TABLE recorders_new RENAME TO recorders;
|
||||
|
||||
CREATE TABLE danmu_statistics_new (
|
||||
live_id TEXT PRIMARY KEY,
|
||||
room_id TEXT,
|
||||
value INTEGER,
|
||||
time_point TEXT
|
||||
);
|
||||
INSERT INTO danmu_statistics_new (
|
||||
live_id,
|
||||
room_id,
|
||||
value,
|
||||
time_point
|
||||
)
|
||||
SELECT
|
||||
live_id,
|
||||
CAST(room_id AS TEXT),
|
||||
value,
|
||||
time_point
|
||||
FROM danmu_statistics;
|
||||
DROP TABLE danmu_statistics;
|
||||
ALTER TABLE danmu_statistics_new RENAME TO danmu_statistics;
|
||||
|
||||
CREATE TABLE videos_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id TEXT,
|
||||
cover TEXT,
|
||||
file TEXT,
|
||||
length INTEGER,
|
||||
size INTEGER,
|
||||
status INTEGER,
|
||||
bvid TEXT,
|
||||
title TEXT,
|
||||
desc TEXT,
|
||||
tags TEXT,
|
||||
area INTEGER,
|
||||
created_at TEXT,
|
||||
platform TEXT,
|
||||
note TEXT
|
||||
);
|
||||
INSERT INTO videos_new (
|
||||
id,
|
||||
room_id,
|
||||
cover,
|
||||
file,
|
||||
length,
|
||||
size,
|
||||
status,
|
||||
bvid,
|
||||
title,
|
||||
desc,
|
||||
tags,
|
||||
area,
|
||||
created_at,
|
||||
platform,
|
||||
note
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
CAST(room_id AS TEXT),
|
||||
cover,
|
||||
file,
|
||||
length,
|
||||
size,
|
||||
status,
|
||||
bvid,
|
||||
title,
|
||||
desc,
|
||||
tags,
|
||||
area,
|
||||
created_at,
|
||||
platform,
|
||||
note
|
||||
FROM videos;
|
||||
DROP TABLE videos;
|
||||
ALTER TABLE videos_new RENAME TO videos;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_videos_room_id ON videos (room_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_videos_created_at ON videos (created_at);
|
||||
",
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 12,
|
||||
description: "convert_account_uid_to_text",
|
||||
sql: r"
|
||||
CREATE TABLE accounts_new (
|
||||
uid TEXT,
|
||||
platform TEXT NOT NULL DEFAULT 'bilibili',
|
||||
name TEXT,
|
||||
avatar TEXT,
|
||||
csrf TEXT,
|
||||
cookies TEXT,
|
||||
created_at TEXT,
|
||||
PRIMARY KEY (uid, platform)
|
||||
);
|
||||
INSERT INTO accounts_new (
|
||||
uid,
|
||||
platform,
|
||||
name,
|
||||
avatar,
|
||||
csrf,
|
||||
cookies,
|
||||
created_at
|
||||
)
|
||||
SELECT
|
||||
COALESCE(NULLIF(id_str, ''), CAST(uid AS TEXT)),
|
||||
platform,
|
||||
name,
|
||||
avatar,
|
||||
csrf,
|
||||
cookies,
|
||||
created_at
|
||||
FROM accounts;
|
||||
DROP TABLE accounts;
|
||||
ALTER TABLE accounts_new RENAME TO accounts;
|
||||
",
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ pub async fn try_rebuild_archives(
|
||||
let live_id = file.file_name();
|
||||
let live_id = live_id.to_str().unwrap();
|
||||
// check if live_id is in db
|
||||
let record = db.get_record(room_id, live_id).await;
|
||||
let record = db.get_record(&room_id, live_id).await;
|
||||
if record.is_ok() {
|
||||
continue;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ pub async fn try_rebuild_archives(
|
||||
})?,
|
||||
live_id,
|
||||
live_id,
|
||||
room_id,
|
||||
&room_id,
|
||||
&format!("UnknownLive {live_id}"),
|
||||
None,
|
||||
)
|
||||
@@ -60,7 +60,7 @@ pub async fn try_convert_live_covers(
|
||||
for room in rooms {
|
||||
let room_id = room.room_id;
|
||||
let room_cache_path = cache_path.join(format!("{}/{}", room.platform, room_id));
|
||||
let records = db.get_records(room_id, 0, 999_999_999).await?;
|
||||
let records = db.get_records(&room_id, 0, 999_999_999).await?;
|
||||
for record in &records {
|
||||
let record_path = room_cache_path.join(record.live_id.clone());
|
||||
let cover = record.cover.clone();
|
||||
@@ -129,7 +129,7 @@ pub async fn try_add_parent_id_to_records(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rooms = db.get_recorders().await?;
|
||||
for room in &rooms {
|
||||
let records = db.get_records(room.room_id, 0, 999_999_999).await?;
|
||||
let records = db.get_records(&room.room_id, 0, 999_999_999).await?;
|
||||
for record in &records {
|
||||
if record.parent_id.is_empty() {
|
||||
db.update_record_parent_id(record.live_id.as_str(), record.live_id.as_str())
|
||||
@@ -146,7 +146,7 @@ pub async fn try_convert_entry_to_m3u8(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rooms = db.get_recorders().await?;
|
||||
for room in &rooms {
|
||||
let records = db.get_records(room.room_id, 0, 999_999_999).await?;
|
||||
let records = db.get_records(&room.room_id, 0, 999_999_999).await?;
|
||||
for record in &records {
|
||||
let record_path = cache_path.join(format!(
|
||||
"{}/{}/{}",
|
||||
|
||||
@@ -52,7 +52,7 @@ pub struct ClipRangeParams {
|
||||
pub note: String,
|
||||
pub cover: String,
|
||||
pub platform: String,
|
||||
pub room_id: i64,
|
||||
pub room_id: String,
|
||||
pub live_id: String,
|
||||
pub range: Option<Range>,
|
||||
/// Encode danmu after clip
|
||||
@@ -133,9 +133,9 @@ pub struct RecorderManager {
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RecorderManagerError {
|
||||
#[error("Recorder already exists: {room_id}")]
|
||||
AlreadyExisted { room_id: i64 },
|
||||
AlreadyExisted { room_id: String },
|
||||
#[error("Recorder not found: {room_id}")]
|
||||
NotFound { room_id: i64 },
|
||||
NotFound { room_id: String },
|
||||
#[error("Invalid platform type: {platform}")]
|
||||
InvalidPlatformType { platform: String },
|
||||
#[error("Recorder error: {0}")]
|
||||
@@ -244,7 +244,7 @@ impl RecorderManager {
|
||||
Payload::Room(recorder.clone()),
|
||||
);
|
||||
let _ = self.webhook_poster.post_event(&event).await;
|
||||
self.handle_live_end(platform, room_id, &recorder).await;
|
||||
self.handle_live_end(platform, &room_id, &recorder).await;
|
||||
if self.config.read().await.live_end_notify {
|
||||
#[cfg(feature = "gui")]
|
||||
self.app_handle
|
||||
@@ -262,7 +262,7 @@ impl RecorderManager {
|
||||
RecorderEvent::RecordStart { recorder } => {
|
||||
// add record entry into db
|
||||
let platform = PlatformType::from_str(&recorder.room_info.platform).unwrap();
|
||||
let room_id = recorder.room_info.room_id.parse::<i64>().unwrap();
|
||||
let room_id = recorder.room_info.room_id.clone();
|
||||
log::info!("Record start: {recorder:?}");
|
||||
if let Err(e) = self
|
||||
.db
|
||||
@@ -270,7 +270,7 @@ impl RecorderManager {
|
||||
platform,
|
||||
&recorder.platform_live_id,
|
||||
&recorder.live_id,
|
||||
room_id,
|
||||
&room_id,
|
||||
&recorder.room_info.room_title,
|
||||
None,
|
||||
)
|
||||
@@ -321,7 +321,12 @@ impl RecorderManager {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_live_end(&self, platform: PlatformType, room_id: i64, recorder: &RecorderInfo) {
|
||||
async fn handle_live_end(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: &str,
|
||||
recorder: &RecorderInfo,
|
||||
) {
|
||||
if !self.config.read().await.auto_generate.enabled {
|
||||
return;
|
||||
}
|
||||
@@ -442,14 +447,14 @@ impl RecorderManager {
|
||||
if !self.recorders.read().await.contains_key(&recorder_id)
|
||||
&& !self.to_remove.read().await.contains(&recorder_id)
|
||||
{
|
||||
recorders_to_add.push((*platform, *room_id));
|
||||
recorders_to_add.push((*platform, room_id.clone()));
|
||||
}
|
||||
}
|
||||
for (platform, room_id) in recorders_to_add {
|
||||
if self.is_migrating.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let (auto_start, extra) = recorder_map.get(&(platform, room_id)).unwrap();
|
||||
let (auto_start, extra) = recorder_map.get(&(platform, room_id.clone())).unwrap();
|
||||
let account = self
|
||||
.db
|
||||
.get_account_by_platform(platform.clone().as_str())
|
||||
@@ -465,7 +470,7 @@ impl RecorderManager {
|
||||
};
|
||||
|
||||
if let Err(e) = self
|
||||
.add_recorder(&account, platform, room_id, extra, *auto_start)
|
||||
.add_recorder(&account, platform, &room_id, extra, *auto_start)
|
||||
.await
|
||||
{
|
||||
log::error!(
|
||||
@@ -484,13 +489,15 @@ impl RecorderManager {
|
||||
&self,
|
||||
account: &Account,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
extra: &str,
|
||||
enabled: bool,
|
||||
) -> Result<(), RecorderManagerError> {
|
||||
let recorder_id = format!("{}:{}", platform.as_str(), room_id);
|
||||
if self.recorders.read().await.contains_key(&recorder_id) {
|
||||
return Err(RecorderManagerError::AlreadyExisted { room_id });
|
||||
return Err(RecorderManagerError::AlreadyExisted {
|
||||
room_id: room_id.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let cache_dir = self.config.read().await.cache.clone();
|
||||
@@ -565,12 +572,14 @@ impl RecorderManager {
|
||||
pub async fn remove_recorder(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
) -> Result<RecorderRow, RecorderManagerError> {
|
||||
// check recorder exists
|
||||
let recorder_id = format!("{}:{}", platform.as_str(), room_id);
|
||||
if !self.recorders.read().await.contains_key(&recorder_id) {
|
||||
return Err(RecorderManagerError::NotFound { room_id });
|
||||
return Err(RecorderManagerError::NotFound {
|
||||
room_id: room_id.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// remove from db
|
||||
@@ -611,14 +620,14 @@ impl RecorderManager {
|
||||
async fn load_playlist_bytes(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_id: &str,
|
||||
) -> Result<Vec<u8>, RecorderManagerError> {
|
||||
let cache_path = self.config.read().await.cache.clone();
|
||||
let cache_path = Path::new(&cache_path);
|
||||
let playlist_path = cache_path
|
||||
.join(platform.as_str())
|
||||
.join(room_id.to_string())
|
||||
.join(room_id)
|
||||
.join(live_id)
|
||||
.join("playlist.m3u8");
|
||||
if !playlist_path.exists() {
|
||||
@@ -646,7 +655,7 @@ impl RecorderManager {
|
||||
async fn is_outdated_playlist(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_id: &str,
|
||||
) -> bool {
|
||||
// check current recorder live id is the same as the live id
|
||||
@@ -665,7 +674,7 @@ impl RecorderManager {
|
||||
async fn load_playlist(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_id: &str,
|
||||
) -> Result<MediaPlaylist, RecorderManagerError> {
|
||||
let bytes = self.load_playlist_bytes(platform, room_id, live_id).await?;
|
||||
@@ -707,7 +716,7 @@ impl RecorderManager {
|
||||
async fn first_segment_timestamp(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_id: &str,
|
||||
) -> Result<i64, RecorderManagerError> {
|
||||
let playlist = self.load_playlist(platform, room_id, live_id).await?;
|
||||
@@ -745,14 +754,14 @@ impl RecorderManager {
|
||||
pub async fn load_danmus(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_id: &str,
|
||||
) -> Result<Vec<DanmuEntry>, RecorderManagerError> {
|
||||
let cache_path = self.config.read().await.cache.clone();
|
||||
let cache_path = Path::new(&cache_path);
|
||||
let danmus_path = cache_path
|
||||
.join(platform.as_str())
|
||||
.join(room_id.to_string())
|
||||
.join(room_id)
|
||||
.join(live_id)
|
||||
.join("danmu.txt");
|
||||
if !danmus_path.exists() {
|
||||
@@ -772,7 +781,7 @@ impl RecorderManager {
|
||||
async fn get_related_playlists(
|
||||
&self,
|
||||
platform: &PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
parent_id: &str,
|
||||
) -> Vec<RelatedPlaylist> {
|
||||
let cache_path = self.config.read().await.cache.clone();
|
||||
@@ -823,7 +832,7 @@ impl RecorderManager {
|
||||
let cache_path = Path::new(&cache_path);
|
||||
let playlist_path = cache_path
|
||||
.join(params.platform.clone())
|
||||
.join(params.room_id.to_string())
|
||||
.join(params.room_id.clone())
|
||||
.join(params.live_id.clone())
|
||||
.join("playlist.m3u8");
|
||||
|
||||
@@ -869,11 +878,11 @@ impl RecorderManager {
|
||||
});
|
||||
};
|
||||
let stream_start_timestamp_milis = self
|
||||
.first_segment_timestamp(platform, params.room_id, ¶ms.live_id)
|
||||
.first_segment_timestamp(platform, ¶ms.room_id, ¶ms.live_id)
|
||||
.await?;
|
||||
|
||||
let danmus = self
|
||||
.load_danmus(platform, params.room_id, ¶ms.live_id)
|
||||
.load_danmus(platform, ¶ms.room_id, ¶ms.live_id)
|
||||
.await;
|
||||
if danmus.is_err() {
|
||||
log::error!(
|
||||
@@ -941,7 +950,7 @@ impl RecorderManager {
|
||||
async fn generate_archive_danmu_ass(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_id: &str,
|
||||
) -> Result<PathBuf, RecorderManagerError> {
|
||||
log::info!(
|
||||
@@ -1038,7 +1047,7 @@ impl RecorderManager {
|
||||
pub async fn get_recorder_info(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
) -> Option<RecorderInfo> {
|
||||
let recorder_id = format!("{}:{}", platform.as_str(), room_id);
|
||||
if let Some(recorder_ref) = self.recorders.read().await.get(&recorder_id) {
|
||||
@@ -1055,7 +1064,7 @@ impl RecorderManager {
|
||||
|
||||
pub async fn get_archives(
|
||||
&self,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<RecordRow>, RecorderManagerError> {
|
||||
@@ -1064,7 +1073,7 @@ impl RecorderManager {
|
||||
|
||||
pub async fn get_archive(
|
||||
&self,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_id: &str,
|
||||
) -> Result<RecordRow, RecorderManagerError> {
|
||||
Ok(self.db.get_record(room_id, live_id).await?)
|
||||
@@ -1073,7 +1082,7 @@ impl RecorderManager {
|
||||
pub async fn get_archive_subtitle(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_id: &str,
|
||||
) -> Result<String, RecorderManagerError> {
|
||||
// read subtitle file under work_dir
|
||||
@@ -1100,7 +1109,7 @@ impl RecorderManager {
|
||||
pub async fn generate_archive_subtitle(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_id: &str,
|
||||
) -> Result<String, RecorderManagerError> {
|
||||
// generate subtitle file under work_dir
|
||||
@@ -1181,14 +1190,14 @@ impl RecorderManager {
|
||||
pub async fn delete_archive(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_id: &str,
|
||||
) -> Result<RecordRow, RecorderManagerError> {
|
||||
log::info!("Deleting archive {room_id}:{live_id}");
|
||||
let to_delete = self.db.remove_record(live_id).await?;
|
||||
let cache_folder = Path::new(self.config.read().await.cache.as_str())
|
||||
.join(platform.as_str())
|
||||
.join(room_id.to_string())
|
||||
.join(room_id)
|
||||
.join(live_id);
|
||||
let _ = tokio::fs::remove_dir_all(cache_folder).await;
|
||||
Ok(to_delete)
|
||||
@@ -1197,7 +1206,7 @@ impl RecorderManager {
|
||||
pub async fn delete_archives(
|
||||
&self,
|
||||
platform: PlatformType,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
live_ids: &[&str],
|
||||
) -> Result<Vec<RecordRow>, RecorderManagerError> {
|
||||
log::info!("Deleting archives in batch: {live_ids:?}");
|
||||
@@ -1224,7 +1233,7 @@ impl RecorderManager {
|
||||
// parse recorder type
|
||||
let platform = path_segs[0];
|
||||
// parse room id
|
||||
let room_id = path_segs[1].parse::<i64>().unwrap();
|
||||
let room_id = path_segs[1];
|
||||
// parse live id
|
||||
let live_id = path_segs[2];
|
||||
|
||||
@@ -1299,7 +1308,7 @@ impl RecorderManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_enable(&self, platform: PlatformType, room_id: i64, enabled: bool) {
|
||||
pub async fn set_enable(&self, platform: PlatformType, room_id: &str, enabled: bool) {
|
||||
// update RecordRow auto_start field
|
||||
if let Err(e) = self.db.update_recorder(platform, room_id, enabled).await {
|
||||
log::error!("Failed to update recorder auto_start: {e}");
|
||||
@@ -1320,7 +1329,7 @@ impl RecorderManager {
|
||||
reporter: Option<&ProgressReporter>,
|
||||
encode_danmu: bool,
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
room_id: &str,
|
||||
parent_id: String,
|
||||
) -> Result<(), RecorderManagerError> {
|
||||
let platform = PlatformType::from_str(&platform).map_err(|_| {
|
||||
@@ -1353,7 +1362,7 @@ impl RecorderManager {
|
||||
|
||||
futures::future::join_all(danmu_ass_files).await
|
||||
} else {
|
||||
Vec::new()
|
||||
vec![None; playlists.len()]
|
||||
};
|
||||
|
||||
let timestamp = chrono::Local::now().format("%Y%m%d%H%M%S").to_string();
|
||||
@@ -1412,7 +1421,7 @@ impl RecorderManager {
|
||||
.add_video(&VideoRow {
|
||||
id: 0,
|
||||
status: 0,
|
||||
room_id,
|
||||
room_id: room_id.to_string(),
|
||||
created_at: chrono::Local::now().to_rfc3339(),
|
||||
cover: cover_filename.to_string_lossy().to_string(),
|
||||
file: output_filename.to_string_lossy().to_string(),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
let video: VideoItem | null = null;
|
||||
let videos: any[] = [];
|
||||
let showVideoPreview = false;
|
||||
let roomId: number | null = null;
|
||||
let roomId: string = "";
|
||||
|
||||
let config: Config = null;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
// update window title to file name
|
||||
set_title((videoData as VideoItem).file);
|
||||
// 获取房间下的所有视频列表
|
||||
if (roomId !== null && roomId !== undefined) {
|
||||
if (roomId.length > 0) {
|
||||
const videoList = (await invoke("get_videos", {
|
||||
roomId: roomId,
|
||||
})) as VideoItem[];
|
||||
@@ -68,7 +68,7 @@
|
||||
}
|
||||
|
||||
async function handleVideoListUpdate() {
|
||||
if (roomId !== null && roomId !== undefined) {
|
||||
if (roomId.length > 0) {
|
||||
const videosData = await invoke("get_videos", { roomId });
|
||||
videos = await Promise.all(
|
||||
(videosData as VideoItem[]).map(async (v) => {
|
||||
@@ -85,7 +85,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if showVideoPreview && video && roomId !== null && roomId !== undefined}
|
||||
{#if showVideoPreview && video && roomId.length > 0}
|
||||
<VideoPreview
|
||||
bind:show={showVideoPreview}
|
||||
{video}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const room_id = parseInt(urlParams.get("room_id"));
|
||||
const room_id = urlParams.get("room_id");
|
||||
const platform = urlParams.get("platform");
|
||||
const live_id = urlParams.get("live_id");
|
||||
const focus_start = parseInt(urlParams.get("start") || "0");
|
||||
|
||||
@@ -62,7 +62,7 @@ const add_recorder = tool(
|
||||
extra,
|
||||
}: {
|
||||
platform: string;
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
extra: string;
|
||||
}) => {
|
||||
const result = await invoke("add_recorder", {
|
||||
@@ -81,7 +81,7 @@ const add_recorder = tool(
|
||||
.describe(
|
||||
`The platform of the recorder. Can be ${platform_list.join(", ")}`
|
||||
),
|
||||
room_id: z.number().describe("The room id of the recorder"),
|
||||
room_id: z.string().describe("The room id of the recorder"),
|
||||
extra: z
|
||||
.string()
|
||||
.describe(
|
||||
@@ -93,7 +93,7 @@ const add_recorder = tool(
|
||||
|
||||
// @ts-ignore
|
||||
const remove_recorder = tool(
|
||||
async ({ platform, room_id }: { platform: string; room_id: number }) => {
|
||||
async ({ platform, room_id }: { platform: string; room_id: string }) => {
|
||||
const result = await invoke("remove_recorder", {
|
||||
platform,
|
||||
roomId: room_id,
|
||||
@@ -109,7 +109,7 @@ const remove_recorder = tool(
|
||||
.describe(
|
||||
`The platform of the recorder. Can be ${platform_list.join(", ")}`
|
||||
),
|
||||
room_id: z.number().describe("The room id of the recorder"),
|
||||
room_id: z.string().describe("The room id of the recorder"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -129,7 +129,7 @@ const get_recorder_list = tool(
|
||||
|
||||
// @ts-ignore
|
||||
const get_recorder_info = tool(
|
||||
async ({ platform, room_id }: { platform: string; room_id: number }) => {
|
||||
async ({ platform, room_id }: { platform: string; room_id: string }) => {
|
||||
const result = await invoke("get_room_info", { platform, roomId: room_id });
|
||||
return result;
|
||||
},
|
||||
@@ -138,7 +138,7 @@ const get_recorder_info = tool(
|
||||
description: "Get the info of a recorder",
|
||||
schema: z.object({
|
||||
platform: z.string().describe("The platform of the room"),
|
||||
room_id: z.number().describe("The room id of the room"),
|
||||
room_id: z.string().describe("The room id of the room"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -150,7 +150,7 @@ const get_archives = tool(
|
||||
offset,
|
||||
limit,
|
||||
}: {
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}) => {
|
||||
@@ -174,7 +174,7 @@ const get_archives = tool(
|
||||
name: "get_archives",
|
||||
description: "Get the list of all archives of a recorder",
|
||||
schema: z.object({
|
||||
room_id: z.number().describe("The room id of the recorder"),
|
||||
room_id: z.string().describe("The room id of the recorder"),
|
||||
offset: z.number().describe("The offset of the archives"),
|
||||
limit: z.number().describe("The limit of the archives"),
|
||||
}),
|
||||
@@ -183,7 +183,7 @@ const get_archives = tool(
|
||||
|
||||
// @ts-ignore
|
||||
const get_archive = tool(
|
||||
async ({ room_id, live_id }: { room_id: number; live_id: string }) => {
|
||||
async ({ room_id, live_id }: { room_id: string; live_id: string }) => {
|
||||
const result = (await invoke("get_archive", {
|
||||
roomId: room_id,
|
||||
liveId: live_id,
|
||||
@@ -199,7 +199,7 @@ const get_archive = tool(
|
||||
name: "get_archive",
|
||||
description: "Get the info of a archive",
|
||||
schema: z.object({
|
||||
room_id: z.number().describe("The room id of the recorder"),
|
||||
room_id: z.string().describe("The room id of the recorder"),
|
||||
live_id: z.string().describe("The live id of the archive"),
|
||||
}),
|
||||
}
|
||||
@@ -213,7 +213,7 @@ const delete_archive = tool(
|
||||
live_id,
|
||||
}: {
|
||||
platform: string;
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
live_id: string;
|
||||
}) => {
|
||||
const result = await invoke("delete_archive", {
|
||||
@@ -232,7 +232,7 @@ const delete_archive = tool(
|
||||
.describe(
|
||||
`The platform of the recorder. Can be ${platform_list.join(", ")}`
|
||||
),
|
||||
room_id: z.number().describe("The room id of the recorder"),
|
||||
room_id: z.string().describe("The room id of the recorder"),
|
||||
live_id: z.string().describe("The live id of the archive"),
|
||||
}),
|
||||
}
|
||||
@@ -246,7 +246,7 @@ const delete_archives = tool(
|
||||
live_ids,
|
||||
}: {
|
||||
platform: string;
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
live_ids: string[];
|
||||
}) => {
|
||||
const result = await invoke("delete_archives", {
|
||||
@@ -265,7 +265,7 @@ const delete_archives = tool(
|
||||
.describe(
|
||||
`The platform of the recorder. Can be ${platform_list.join(", ")}`
|
||||
),
|
||||
room_id: z.number().describe("The room id of the recorder"),
|
||||
room_id: z.string().describe("The room id of the recorder"),
|
||||
live_ids: z.array(z.string()).describe("The live ids of the archives"),
|
||||
}),
|
||||
}
|
||||
@@ -308,7 +308,7 @@ const delete_background_task = tool(
|
||||
|
||||
// @ts-ignore
|
||||
const get_videos = tool(
|
||||
async ({ room_id }: { room_id: number }) => {
|
||||
async ({ room_id }: { room_id: string }) => {
|
||||
const result = (await invoke("get_videos", { roomId: room_id })) as any[];
|
||||
return {
|
||||
videos: result.map((v: any) => {
|
||||
@@ -323,7 +323,7 @@ const get_videos = tool(
|
||||
name: "get_videos",
|
||||
description: "Get the list of all videos of a room",
|
||||
schema: z.object({
|
||||
room_id: z.number().describe("The room id of the room"),
|
||||
room_id: z.string().describe("The room id of the room"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -480,7 +480,7 @@ const post_video_to_bilibili = tool(
|
||||
tid,
|
||||
}: {
|
||||
uid: number;
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
video_id: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
@@ -521,7 +521,7 @@ const post_video_to_bilibili = tool(
|
||||
.describe(
|
||||
"The uid of the user, it should be one of the uid in the bilibili accounts"
|
||||
),
|
||||
room_id: z.number().describe("The room id of the room"),
|
||||
room_id: z.string().describe("The room id of the room"),
|
||||
video_id: z.number().describe("The id of the video"),
|
||||
title: z.string().describe("The title of the video"),
|
||||
desc: z.string().describe("The description of the video"),
|
||||
@@ -547,7 +547,7 @@ const get_danmu_record = tool(
|
||||
live_id,
|
||||
}: {
|
||||
platform: string;
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
live_id: string;
|
||||
}) => {
|
||||
const result = (await invoke("get_danmu_record", {
|
||||
@@ -571,7 +571,7 @@ const get_danmu_record = tool(
|
||||
"Get the danmu record of a live, entry ts is relative to the live start time in seconds",
|
||||
schema: z.object({
|
||||
platform: z.string().describe("The platform of the room"),
|
||||
room_id: z.number().describe("The room id of the room"),
|
||||
room_id: z.string().describe("The room id of the room"),
|
||||
live_id: z.string().describe("The live id of the live"),
|
||||
}),
|
||||
}
|
||||
@@ -604,7 +604,7 @@ const clip_range = tool(
|
||||
"The reason for the clip range, it will be shown to the user. You must offer a summary of the clip range content and why you choose this clip range."
|
||||
),
|
||||
clip_range_params: z.object({
|
||||
room_id: z.number().describe("The room id of the room"),
|
||||
room_id: z.string().describe("The room id of the room"),
|
||||
live_id: z.string().describe("The live id of the live"),
|
||||
range: z.object({
|
||||
start: z.number().describe("The start time in SECONDS of the clip"),
|
||||
@@ -641,7 +641,7 @@ const get_recent_record = tool(
|
||||
offset,
|
||||
limit,
|
||||
}: {
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}) => {
|
||||
@@ -664,7 +664,7 @@ const get_recent_record = tool(
|
||||
name: "get_recent_record",
|
||||
description: "Get the list of recent records that bsr has recorded",
|
||||
schema: z.object({
|
||||
room_id: z.number().describe("The room id of the room"),
|
||||
room_id: z.string().describe("The room id of the room"),
|
||||
offset: z.number().describe("The offset of the records"),
|
||||
limit: z.number().describe("The limit of the records"),
|
||||
}),
|
||||
@@ -752,7 +752,7 @@ const get_archive_subtitle = tool(
|
||||
live_id,
|
||||
}: {
|
||||
platform: string;
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
live_id: string;
|
||||
}) => {
|
||||
const result = await invoke("get_archive_subtitle", {
|
||||
@@ -768,7 +768,7 @@ const get_archive_subtitle = tool(
|
||||
"Get the subtitle of a archive, it may not be generated yet, you can use generate_archive_subtitle to generate the subtitle",
|
||||
schema: z.object({
|
||||
platform: z.string().describe("The platform of the archive"),
|
||||
room_id: z.number().describe("The room id of the archive"),
|
||||
room_id: z.string().describe("The room id of the archive"),
|
||||
live_id: z.string().describe("The live id of the archive"),
|
||||
}),
|
||||
}
|
||||
@@ -782,7 +782,7 @@ const generate_archive_subtitle = tool(
|
||||
live_id,
|
||||
}: {
|
||||
platform: string;
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
live_id: string;
|
||||
}) => {
|
||||
const result = await invoke("generate_archive_subtitle", {
|
||||
@@ -798,7 +798,7 @@ const generate_archive_subtitle = tool(
|
||||
"Generate the subtitle of a archive, it may take a long time, you should not call this tool unless user ask you to generate the subtitle. It can be used to overwrite the subtitle of a archive",
|
||||
schema: z.object({
|
||||
platform: z.string().describe("The platform of the archive"),
|
||||
room_id: z.number().describe("The room id of the archive"),
|
||||
room_id: z.string().describe("The room id of the archive"),
|
||||
live_id: z.string().describe("The live id of the archive"),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
export let showModal = false;
|
||||
export let archive: RecordItem | null = null;
|
||||
export let roomId: number;
|
||||
export let roomId: string;
|
||||
export const platform: string = "";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -21,7 +21,7 @@
|
||||
loadWholeClipArchives(roomId, archive.parent_id);
|
||||
}
|
||||
|
||||
async function loadWholeClipArchives(roomId: number, parentId: string) {
|
||||
async function loadWholeClipArchives(roomId: string, parentId: string) {
|
||||
if (isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
|
||||
export let platform: string;
|
||||
export let room_id: number;
|
||||
export let room_id: string;
|
||||
export let live_id: string;
|
||||
export let start = 0;
|
||||
export let end = 0;
|
||||
@@ -184,11 +184,11 @@ ${mediaPlaylistUrl}`;
|
||||
recorders = (
|
||||
(await invoke("get_recorder_list")) as RecorderList
|
||||
).recorders.filter(
|
||||
(r) => r.room_info.status && Number(r.room_info.room_id) != room_id
|
||||
(r) => r.room_info.status && r.room_info.room_id != room_id
|
||||
);
|
||||
}
|
||||
|
||||
function go_to(platform: string, room_id: number, live_id: string) {
|
||||
function go_to(platform: string, room_id: string, live_id: string) {
|
||||
const url = `${window.location.origin}${window.location.pathname}?platform=${platform}&room_id=${room_id}&live_id=${live_id}`;
|
||||
window.location.href = url;
|
||||
}
|
||||
@@ -1030,7 +1030,7 @@ ${mediaPlaylistUrl}`;
|
||||
on:click={() => {
|
||||
go_to(
|
||||
recorder.room_info.platform,
|
||||
Number(recorder.room_info.room_id),
|
||||
recorder.room_info.room_id,
|
||||
recorder.live_id
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
export let show = false;
|
||||
export let onClose: () => void;
|
||||
export let roomId: number;
|
||||
export let roomId: string;
|
||||
|
||||
// 默认样式
|
||||
const defaultStyle: SubtitleStyle = {
|
||||
@@ -48,7 +48,7 @@
|
||||
font-family: ${style.fontName};
|
||||
font-size: ${style.fontSize}px;
|
||||
color: ${fontColor};
|
||||
text-shadow:
|
||||
text-shadow:
|
||||
${style.outlineWidth}px ${style.outlineWidth}px 0 ${outlineColor},
|
||||
-${style.outlineWidth}px ${style.outlineWidth}px 0 ${outlineColor},
|
||||
${style.outlineWidth}px -${style.outlineWidth}px 0 ${outlineColor},
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
export let show = false;
|
||||
export let video: VideoItem;
|
||||
export let roomId: number;
|
||||
export let roomId: string;
|
||||
export let videos: any[] = [];
|
||||
export let onVideoChange: ((video: VideoItem) => void) | undefined =
|
||||
undefined;
|
||||
@@ -809,8 +809,16 @@
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (!show || !isVideoLoaded) return;
|
||||
|
||||
// 如果在输入框中,不处理某些快捷键
|
||||
const isInInput = (event.target as HTMLElement)?.tagName === "INPUT";
|
||||
const target = event.target as HTMLElement | null;
|
||||
const tagName = target?.tagName;
|
||||
|
||||
// 如果当前焦点位于可编辑元素内,则跳过快捷键处理
|
||||
const isInInput =
|
||||
(!!tagName && ["INPUT", "TEXTAREA", "SELECT"].includes(tagName)) ||
|
||||
!!target?.isContentEditable ||
|
||||
!!target?.closest(
|
||||
"input, textarea, select, [contenteditable='true'], [data-hotkey-block]"
|
||||
);
|
||||
|
||||
switch (event.key) {
|
||||
case "【":
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
export interface RecorderItem {
|
||||
platform: string;
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AccountItem {
|
||||
platform: string;
|
||||
uid: number;
|
||||
id_str?: string; // For platforms like Douyin that use string IDs
|
||||
uid: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
csrf: string;
|
||||
@@ -29,7 +28,7 @@ export interface RecordItem {
|
||||
title: string;
|
||||
parent_id: string;
|
||||
live_id: string;
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
length: number;
|
||||
size: number;
|
||||
created_at: string;
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface Video {
|
||||
|
||||
export interface VideoItem {
|
||||
id: number;
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
cover: string;
|
||||
file: string;
|
||||
length: number;
|
||||
@@ -259,7 +259,7 @@ export interface ClipRangeParams {
|
||||
note: string;
|
||||
cover: string;
|
||||
platform: string;
|
||||
room_id: number;
|
||||
room_id: string;
|
||||
live_id: string;
|
||||
range: {
|
||||
start: number;
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
</h3>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
UID: {account.id_str || account.uid}
|
||||
UID: {account.uid}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
for (const room of allRooms) {
|
||||
try {
|
||||
const roomArchives = await invoke<RecordItem[]>("get_archives", {
|
||||
roomId: parseInt(room.room_info.room_id),
|
||||
roomId: room.room_info.room_id,
|
||||
offset: 0,
|
||||
limit: 100, // 每个直播间获取更多数据
|
||||
});
|
||||
@@ -184,7 +184,7 @@
|
||||
// Apply room filter
|
||||
if (selectedRoomId !== null) {
|
||||
filtered = filtered.filter(
|
||||
(archive) => String(archive.room_id) === selectedRoomId
|
||||
(archive) => archive.room_id === selectedRoomId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getRoomUrl(platform: string, roomId: number) {
|
||||
function getRoomUrl(platform: string, roomId: string) {
|
||||
switch (platform.toLowerCase()) {
|
||||
case "bilibili":
|
||||
return `https://live.bilibili.com/${roomId}`;
|
||||
@@ -310,6 +310,10 @@
|
||||
return ((size * 8) / duration / 1024).toFixed(0);
|
||||
}
|
||||
|
||||
function getArchiveKey(archive: RecordItem) {
|
||||
return `${archive.platform}-${archive.room_id}-${archive.parent_id}-${archive.live_id}`;
|
||||
}
|
||||
|
||||
function toggleSort(field: string) {
|
||||
if (sortBy === field) {
|
||||
sortOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
@@ -723,7 +727,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700/50">
|
||||
{#each filteredArchives as archive (archive.live_id)}
|
||||
{#each filteredArchives as archive (getArchiveKey(archive))}
|
||||
<tr
|
||||
class="group hover:bg-[#f5f5f7] dark:hover:bg-[#3a3a3c] transition-colors"
|
||||
>
|
||||
@@ -906,7 +910,7 @@
|
||||
<GenerateWholeClipModal
|
||||
bind:showModal={showWholeClipModal}
|
||||
archive={wholeClipArchive}
|
||||
roomId={wholeClipArchive?.room_id || 0}
|
||||
roomId={wholeClipArchive?.room_id || ""}
|
||||
platform={wholeClipArchive?.platform || ""}
|
||||
on:generated={handleWholeClipGenerated}
|
||||
/>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
let isLoading = false;
|
||||
let loadError = "";
|
||||
|
||||
async function showArchives(room_id: number) {
|
||||
async function showArchives(room_id: string) {
|
||||
// 重置分页状态
|
||||
currentPage = 0;
|
||||
archives = [];
|
||||
@@ -139,7 +139,7 @@
|
||||
|
||||
try {
|
||||
let new_archives = (await invoke("get_archives", {
|
||||
roomId: Number(archiveRoom.room_info.room_id),
|
||||
roomId: archiveRoom.room_info.room_id,
|
||||
offset: currentPage * pageSize,
|
||||
limit: pageSize,
|
||||
})) as RecordItem[];
|
||||
@@ -297,7 +297,7 @@
|
||||
// Function to toggle auto-record state
|
||||
function toggleEnabled(room: RecorderInfo) {
|
||||
invoke("set_enable", {
|
||||
roomId: Number(room.room_info.room_id),
|
||||
roomId: room.room_info.room_id,
|
||||
platform: room.room_info.platform,
|
||||
enabled: !room.enabled,
|
||||
});
|
||||
@@ -322,7 +322,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function addNewRecorder(room_id: number, platform: string, extra: string) {
|
||||
function addNewRecorder(room_id: string, platform: string, extra: string) {
|
||||
// if extra contains ?, remove it
|
||||
if (extra.includes("?")) {
|
||||
extra = extra.split("?")[0];
|
||||
@@ -560,7 +560,7 @@
|
||||
on:click={() => {
|
||||
invoke("open_live", {
|
||||
platform: room.room_info.platform,
|
||||
roomId: Number(room.room_info.room_id),
|
||||
roomId: room.room_info.room_id,
|
||||
liveId: room.live_id,
|
||||
});
|
||||
}}
|
||||
@@ -572,7 +572,7 @@
|
||||
class="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
on:click={() => {
|
||||
archiveRoom = room;
|
||||
showArchives(Number(room.room_info.room_id));
|
||||
showArchives(room.room_info.room_id);
|
||||
}}
|
||||
>
|
||||
<History class="w-5 h-5 dark:icon-white" />
|
||||
@@ -636,7 +636,7 @@
|
||||
class="w-24 px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-lg transition-colors"
|
||||
on:click={async () => {
|
||||
await invoke("remove_recorder", {
|
||||
roomId: Number(deleteRoom.room_info.room_id),
|
||||
roomId: deleteRoom.room_info.room_id,
|
||||
platform: deleteRoom.room_info.platform,
|
||||
});
|
||||
deleteModal = false;
|
||||
@@ -725,8 +725,7 @@
|
||||
addValid = false;
|
||||
return;
|
||||
}
|
||||
const room_id = Number(addRoom);
|
||||
if (Number.isInteger(room_id) && room_id > 0) {
|
||||
if (addRoom.length > 0) {
|
||||
addErrorMsg = "";
|
||||
addValid = true;
|
||||
} else {
|
||||
@@ -755,7 +754,7 @@
|
||||
class="px-4 py-2 bg-[#0A84FF] hover:bg-[#0A84FF]/90 text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={!addValid}
|
||||
on:click={() => {
|
||||
addNewRecorder(Number(addRoom), selectedPlatform, "");
|
||||
addNewRecorder(addRoom, selectedPlatform, "");
|
||||
addModal = false;
|
||||
addRoom = "";
|
||||
}}
|
||||
@@ -879,7 +878,7 @@
|
||||
on:click={() => {
|
||||
invoke("open_live", {
|
||||
platform: archiveRoom.room_info.platform,
|
||||
roomId: Number(archiveRoom.room_info.room_id),
|
||||
roomId: archiveRoom.room_info.room_id,
|
||||
liveId: archive.live_id,
|
||||
});
|
||||
}}
|
||||
@@ -901,7 +900,7 @@
|
||||
on:click={() => {
|
||||
invoke("delete_archive", {
|
||||
platform: archiveRoom.room_info.platform,
|
||||
roomId: Number(archiveRoom.room_info.room_id),
|
||||
roomId: archiveRoom.room_info.room_id,
|
||||
liveId: archive.live_id,
|
||||
})
|
||||
.then(async () => {
|
||||
@@ -978,7 +977,7 @@
|
||||
<GenerateWholeClipModal
|
||||
bind:showModal={generateWholeClipModal}
|
||||
archive={generateWholeClipArchive}
|
||||
roomId={generateWholeClipArchive?.room_id || 0}
|
||||
roomId={generateWholeClipArchive?.room_id || ""}
|
||||
platform={generateWholeClipArchive?.platform || ""}
|
||||
on:generated={handleWholeClipGenerated}
|
||||
/>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
// check for new records
|
||||
if (recent_records.length > 0) {
|
||||
const latestRecords = (await invoke("get_recent_record", {
|
||||
roomId: 0,
|
||||
roomId: "",
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
})) as RecordItem[];
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
loading = true;
|
||||
const newRecords = (await invoke("get_recent_record", {
|
||||
roomId: 0,
|
||||
roomId: "",
|
||||
offset: hasNewRecords ? 0 : offset,
|
||||
limit: RECORDS_PER_PAGE,
|
||||
})) as RecordItem[];
|
||||
|
||||
Reference in New Issue
Block a user