mirror of
https://github.com/Xinrea/bili-shadowreplay.git
synced 2025-11-25 04:22:24 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed0bd88e3b | ||
|
|
b4fb2d058a | ||
|
|
4058b425c8 | ||
|
|
55d872a38c | ||
|
|
1f666d402d |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bili-shadowreplay",
|
||||
"private": true,
|
||||
"version": "2.15.3",
|
||||
"version": "2.15.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -541,7 +541,7 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||
|
||||
[[package]]
|
||||
name = "bili-shadowreplay"
|
||||
version = "2.15.3"
|
||||
version = "2.15.5"
|
||||
dependencies = [
|
||||
"async-ffmpeg-sidecar 0.0.1",
|
||||
"async-std",
|
||||
|
||||
@@ -4,7 +4,7 @@ resolver = "2"
|
||||
|
||||
[package]
|
||||
name = "bili-shadowreplay"
|
||||
version = "2.15.3"
|
||||
version = "2.15.5"
|
||||
description = "BiliBili ShadowReplay"
|
||||
authors = ["Xinrea"]
|
||||
license = ""
|
||||
|
||||
@@ -412,6 +412,7 @@ mod tests {
|
||||
).await.unwrap();
|
||||
assert_eq!(stream.index(), "https://hs.hls.huya.com/huyalive/156976698-156976698-674209784144068608-314076852-10057-A-0-1.m3u8?ratio=2000&wsSecret=7abc7dec8809146f31f92046eb044e3b&wsTime=68fa41ba&fm=RFdxOEJjSjNoNkRKdDZUWV8kMF8kMV8kMl8kMw%3D%3D&ctype=tars_mobile&fs=bgct&t=103");
|
||||
assert_eq!(stream.ts_url("1.ts"), "https://hs.hls.huya.com/huyalive/1.ts?ratio=2000&wsSecret=7abc7dec8809146f31f92046eb044e3b&wsTime=68fa41ba&fm=RFdxOEJjSjNoNkRKdDZUWV8kMF8kMV8kMl8kMw%3D%3D&ctype=tars_mobile&fs=bgct&t=103");
|
||||
assert_eq!(stream.ts_url("1.ts?expires=1760808243"), "https://hs.hls.huya.com/huyalive/1.ts?expires=1760808243&ratio=2000&wsSecret=7abc7dec8809146f31f92046eb044e3b&wsTime=68fa41ba&fm=RFdxOEJjSjNoNkRKdDZUWV8kMF8kMV8kMl8kMw%3D%3D&ctype=tars_mobile&fs=bgct&t=103");
|
||||
assert_eq!(stream.host, "https://hs.hls.huya.com");
|
||||
assert_eq!(
|
||||
stream.base,
|
||||
|
||||
@@ -80,9 +80,18 @@ impl HlsStream {
|
||||
if self.extra.is_empty() {
|
||||
format!("{}{}", self.host, base_url)
|
||||
} else {
|
||||
// Remove the trailing '?' from base_url and add it properly
|
||||
let base_without_query = base_url.trim_end_matches('?');
|
||||
format!("{}{}?{}", self.host, base_without_query, self.extra)
|
||||
// Check if base_url already contains query parameters
|
||||
if base_url.contains('?') {
|
||||
// If seg_name already has query params, append extra with '&'
|
||||
// Remove trailing '?' or '&' before appending
|
||||
let base_trimmed = base_url.trim_end_matches('?').trim_end_matches('&');
|
||||
format!("{}{}&{}", self.host, base_trimmed, self.extra)
|
||||
} else {
|
||||
// If no query params, add them with '?'
|
||||
// Remove trailing '?' from base_url if present
|
||||
let base_without_query = base_url.trim_end_matches('?');
|
||||
format!("{}{}?{}", self.host, base_without_query, self.extra)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ where
|
||||
room_info: Arc<RwLock<RoomInfo>>,
|
||||
/// The user info for the recorder
|
||||
user_info: Arc<RwLock<UserInfo>>,
|
||||
/// The update interval for room status
|
||||
update_interval: Arc<atomic::AtomicU64>,
|
||||
|
||||
/// The platform live id for the current recording
|
||||
platform_live_id: Arc<RwLock<String>>,
|
||||
|
||||
@@ -42,6 +42,7 @@ impl BiliRecorder {
|
||||
account: &Account,
|
||||
cache_dir: PathBuf,
|
||||
event_channel: broadcast::Sender<RecorderEvent>,
|
||||
update_interval: Arc<atomic::AtomicU64>,
|
||||
enabled: bool,
|
||||
) -> Result<Self, crate::errors::RecorderError> {
|
||||
let client = reqwest::Client::new();
|
||||
@@ -59,6 +60,7 @@ impl BiliRecorder {
|
||||
cache_dir,
|
||||
quit: Arc::new(atomic::AtomicBool::new(false)),
|
||||
enabled: Arc::new(atomic::AtomicBool::new(enabled)),
|
||||
update_interval,
|
||||
is_recording: Arc::new(atomic::AtomicBool::new(false)),
|
||||
room_info: Arc::new(RwLock::new(RoomInfo::default())),
|
||||
user_info: Arc::new(RwLock::new(UserInfo::default())),
|
||||
@@ -109,19 +111,22 @@ impl BiliRecorder {
|
||||
room_cover: room_info.room_cover_url.clone(),
|
||||
status: room_info.live_status == 1,
|
||||
};
|
||||
let user_id = room_info.user_id;
|
||||
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(),
|
||||
user_name: user_info.user_name,
|
||||
user_avatar: user_info.user_avatar_url,
|
||||
// Only update user info once
|
||||
if self.user_info.read().await.user_id != room_info.user_id.to_string() {
|
||||
let user_id = room_info.user_id;
|
||||
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(),
|
||||
user_name: user_info.user_name,
|
||||
user_avatar: user_info.user_avatar_url,
|
||||
}
|
||||
} else {
|
||||
self.log_error(&format!(
|
||||
"Failed to get user info: {}",
|
||||
user_info.err().unwrap()
|
||||
));
|
||||
}
|
||||
} else {
|
||||
self.log_error(&format!(
|
||||
"Failed to get user info: {}",
|
||||
user_info.err().unwrap()
|
||||
));
|
||||
}
|
||||
let live_status = room_info.live_status == 1;
|
||||
|
||||
@@ -370,7 +375,10 @@ impl crate::traits::RecorderTrait<BiliExtra> for BiliRecorder {
|
||||
continue;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(15)).await;
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
self_clone.update_interval.load(atomic::Ordering::Relaxed),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ impl DouyinRecorder {
|
||||
account: &Account,
|
||||
cache_dir: PathBuf,
|
||||
channel: broadcast::Sender<RecorderEvent>,
|
||||
update_interval: Arc<atomic::AtomicU64>,
|
||||
enabled: bool,
|
||||
) -> Result<Self, crate::errors::RecorderError> {
|
||||
Ok(Self {
|
||||
@@ -69,6 +70,7 @@ impl DouyinRecorder {
|
||||
last_sequence: Arc::new(atomic::AtomicU64::new(0)),
|
||||
danmu_task: Arc::new(Mutex::new(None)),
|
||||
record_task: Arc::new(Mutex::new(None)),
|
||||
update_interval,
|
||||
total_duration: Arc::new(atomic::AtomicU64::new(0)),
|
||||
total_size: Arc::new(atomic::AtomicU64::new(0)),
|
||||
extra: DouyinExtra {
|
||||
@@ -327,7 +329,10 @@ impl crate::traits::RecorderTrait<DouyinExtra> for DouyinRecorder {
|
||||
continue;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(15)).await;
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
self_clone.update_interval.load(atomic::Ordering::Relaxed),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
log::info!("[{}]Recording thread quit.", self_clone.room_id);
|
||||
}));
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::errors::RecorderError;
|
||||
use crate::utils::user_agent_generator;
|
||||
use deno_core::JsRuntime;
|
||||
use deno_core::RuntimeOptions;
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -332,6 +333,34 @@ pub async fn get_user_info(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_room_owner_sec_uid(
|
||||
client: &Client,
|
||||
room_id: i64,
|
||||
) -> Result<String, RecorderError> {
|
||||
let url = format!("https://live.douyin.com/{room_id}");
|
||||
let mut headers = generate_user_agent_header();
|
||||
headers.insert("Referer", "https://live.douyin.com/".parse().unwrap());
|
||||
let resp = client.get(url).headers(headers).send().await?;
|
||||
let status = resp.status();
|
||||
let text = resp.text().await?;
|
||||
if !status.is_success() {
|
||||
return Err(RecorderError::ApiError {
|
||||
error: format!("Failed to get room owner sec uid: {status} {text}"),
|
||||
});
|
||||
}
|
||||
// match to get sec_uid from text like \"sec_uid\":\"MS4wLjABAAAAdFmmud36bynPjXOvoMjatb42856_zryHsGmlkpIECDA\"
|
||||
let sec_uid = Regex::new(r#"\\"sec_uid\\":\\"(.*?)\\""#)
|
||||
.unwrap()
|
||||
.captures(&text)
|
||||
.and_then(|c| c.get(1))
|
||||
.ok_or_else(|| RecorderError::ApiError {
|
||||
error: "Failed to find sec_uid in room page".to_string(),
|
||||
})?
|
||||
.as_str()
|
||||
.to_string();
|
||||
Ok(sec_uid)
|
||||
}
|
||||
|
||||
/// Download file from url to path
|
||||
pub async fn download_file(client: &Client, url: &str, path: &Path) -> Result<(), RecorderError> {
|
||||
if !path.parent().unwrap().exists() {
|
||||
@@ -344,3 +373,18 @@ pub async fn download_file(client: &Client, url: &str, path: &Path) -> Result<()
|
||||
tokio::io::copy(&mut content, &mut file).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[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();
|
||||
assert_eq!(
|
||||
sec_uid,
|
||||
"MS4wLjABAAAAdFmmud36bynPjXOvoMjatb42856_zryHsGmlkpIECDA"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ impl HuyaRecorder {
|
||||
account: &Account,
|
||||
cache_dir: PathBuf,
|
||||
channel: broadcast::Sender<RecorderEvent>,
|
||||
update_interval: Arc<atomic::AtomicU64>,
|
||||
enabled: bool,
|
||||
) -> Result<Self, crate::errors::RecorderError> {
|
||||
Ok(Self {
|
||||
@@ -55,6 +56,7 @@ impl HuyaRecorder {
|
||||
last_sequence: Arc::new(atomic::AtomicU64::new(0)),
|
||||
danmu_task: Arc::new(Mutex::new(None)),
|
||||
record_task: Arc::new(Mutex::new(None)),
|
||||
update_interval,
|
||||
total_duration: Arc::new(atomic::AtomicU64::new(0)),
|
||||
total_size: Arc::new(atomic::AtomicU64::new(0)),
|
||||
extra: HuyaExtra {
|
||||
@@ -224,7 +226,10 @@ impl crate::traits::RecorderTrait<HuyaExtra> for HuyaRecorder {
|
||||
continue;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(15)).await;
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
self_clone.update_interval.load(atomic::Ordering::Relaxed),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
log::info!("[{}]Recording thread quit.", self_clone.room_id);
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::Local;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{self, AtomicU64};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{danmu2ass::Danmu2AssOptions, recorder_manager::ClipRangeParams};
|
||||
|
||||
@@ -39,6 +40,8 @@ pub struct Config {
|
||||
pub webhook_url: String,
|
||||
#[serde(default = "default_danmu_ass_options")]
|
||||
pub danmu_ass_options: Danmu2AssOptions,
|
||||
#[serde(skip)]
|
||||
pub update_interval: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
@@ -107,6 +110,7 @@ impl Config {
|
||||
if let Ok(content) = std::fs::read_to_string(config_path) {
|
||||
if let Ok(mut config) = toml::from_str::<Config>(&content) {
|
||||
config.config_path = config_path.to_str().unwrap().into();
|
||||
config.update_interval = Arc::new(AtomicU64::new(config.status_check_interval));
|
||||
return Ok(config);
|
||||
}
|
||||
}
|
||||
@@ -137,6 +141,7 @@ impl Config {
|
||||
whisper_language: default_whisper_language(),
|
||||
webhook_url: default_webhook_url(),
|
||||
danmu_ass_options: default_danmu_ass_options(),
|
||||
update_interval: Arc::new(AtomicU64::new(default_status_check_interval())),
|
||||
};
|
||||
|
||||
config.save();
|
||||
@@ -220,4 +225,11 @@ impl Config {
|
||||
|
||||
Path::new(&output).join(&sanitized)
|
||||
}
|
||||
|
||||
pub fn set_status_check_interval(&mut self, interval: u64) {
|
||||
self.status_check_interval = interval;
|
||||
self.update_interval
|
||||
.store(interval, atomic::Ordering::Relaxed);
|
||||
self.save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,8 +251,11 @@ pub async fn update_status_check_interval(
|
||||
interval = 10; // Minimum interval of 10 seconds
|
||||
}
|
||||
log::info!("Updating status check interval to {interval} seconds");
|
||||
state.config.write().await.status_check_interval = interval;
|
||||
state.config.write().await.save();
|
||||
state
|
||||
.config
|
||||
.write()
|
||||
.await
|
||||
.set_status_check_interval(interval);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::webhook::events;
|
||||
use recorder::account::Account;
|
||||
use recorder::danmu::DanmuEntry;
|
||||
use recorder::platforms::bilibili;
|
||||
use recorder::platforms::douyin;
|
||||
use recorder::platforms::PlatformType;
|
||||
use recorder::RecorderInfo;
|
||||
|
||||
@@ -33,7 +34,7 @@ pub async fn add_recorder(
|
||||
state: state_type!(),
|
||||
platform: String,
|
||||
room_id: i64,
|
||||
extra: String,
|
||||
mut extra: String,
|
||||
) -> Result<RecorderRow, String> {
|
||||
log::info!("Add recorder: {platform} {room_id}");
|
||||
let platform = PlatformType::from_str(&platform).unwrap();
|
||||
@@ -47,6 +48,12 @@ 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)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
extra = sec_uid;
|
||||
|
||||
if let Ok(account) = state.db.get_account_by_platform("douyin").await {
|
||||
Ok(account.to_account())
|
||||
} else {
|
||||
|
||||
@@ -497,15 +497,41 @@ impl RecorderManager {
|
||||
let cache_dir = PathBuf::from(&cache_dir);
|
||||
|
||||
let event_tx = self.get_event_sender();
|
||||
let update_interval = self.config.read().await.update_interval.clone();
|
||||
let recorder: RecorderType = match platform {
|
||||
PlatformType::BiliBili => RecorderType::BiliBili(
|
||||
BiliRecorder::new(room_id, account, cache_dir, event_tx, enabled).await?,
|
||||
BiliRecorder::new(
|
||||
room_id,
|
||||
account,
|
||||
cache_dir,
|
||||
event_tx,
|
||||
update_interval,
|
||||
enabled,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
PlatformType::Douyin => RecorderType::Douyin(
|
||||
DouyinRecorder::new(room_id, extra, account, cache_dir, event_tx, enabled).await?,
|
||||
DouyinRecorder::new(
|
||||
room_id,
|
||||
extra,
|
||||
account,
|
||||
cache_dir,
|
||||
event_tx,
|
||||
update_interval,
|
||||
enabled,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
PlatformType::Huya => RecorderType::Huya(
|
||||
HuyaRecorder::new(room_id, account, cache_dir, event_tx, enabled).await?,
|
||||
HuyaRecorder::new(
|
||||
room_id,
|
||||
account,
|
||||
cache_dir,
|
||||
event_tx,
|
||||
update_interval,
|
||||
enabled,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
_ => {
|
||||
return Err(RecorderManagerError::InvalidPlatformType {
|
||||
|
||||
@@ -104,7 +104,6 @@
|
||||
|
||||
let addModal = false;
|
||||
let addRoom = "";
|
||||
let addSecUserId = "";
|
||||
let addValid = false;
|
||||
let addErrorMsg = "";
|
||||
let selectedPlatform = "bilibili";
|
||||
@@ -337,7 +336,6 @@
|
||||
.then(() => {
|
||||
addModal = false;
|
||||
addRoom = "";
|
||||
addSecUserId = "";
|
||||
})
|
||||
.catch(async (e) => {
|
||||
await message(e);
|
||||
@@ -709,27 +707,6 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
{#if selectedPlatform === "douyin"}
|
||||
<label
|
||||
for="sec_user_id"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
主播 SEC_UID (
|
||||
<a
|
||||
href="https://bsr.xinrea.cn/usage/features/room.html#%E6%89%8B%E5%8A%A8%E6%B7%BB%E5%8A%A0%E7%9B%B4%E6%92%AD%E9%97%B4"
|
||||
target="_blank"
|
||||
class="text-blue-500">如何获取</a
|
||||
>
|
||||
)
|
||||
</label>
|
||||
<input
|
||||
id="sec_user_id"
|
||||
type="text"
|
||||
bind:value={addSecUserId}
|
||||
placeholder="请输入主播的 SEC_UID(选填)"
|
||||
class="w-full px-3 py-2 bg-[#f5f5f7] dark:bg-[#1c1c1e] border-0 rounded-lg focus:ring-2 focus:ring-blue-500 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400"
|
||||
/>
|
||||
{/if}
|
||||
<label
|
||||
for="room_id"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
@@ -778,7 +755,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, addSecUserId);
|
||||
addNewRecorder(Number(addRoom), selectedPlatform, "");
|
||||
addModal = false;
|
||||
addRoom = "";
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user