mirror of
https://github.com/Xinrea/bili-shadowreplay.git
synced 2025-11-24 20:15:34 +08:00
feat: basic danmu encoding configuration (close #103)
This commit is contained in:
@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
||||
use chrono::Local;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::recorder_manager::ClipRangeParams;
|
||||
use crate::{danmu2ass::Danmu2AssOptions, recorder_manager::ClipRangeParams};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct Config {
|
||||
@@ -37,6 +37,8 @@ pub struct Config {
|
||||
pub whisper_language: String,
|
||||
#[serde(default = "default_webhook_url")]
|
||||
pub webhook_url: String,
|
||||
#[serde(default = "default_danmu_ass_options")]
|
||||
pub danmu_ass_options: Danmu2AssOptions,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
@@ -45,6 +47,10 @@ pub struct AutoGenerateConfig {
|
||||
pub encode_danmu: bool,
|
||||
}
|
||||
|
||||
fn default_danmu_ass_options() -> Danmu2AssOptions {
|
||||
Danmu2AssOptions::default()
|
||||
}
|
||||
|
||||
fn default_auto_subtitle() -> bool {
|
||||
false
|
||||
}
|
||||
@@ -130,6 +136,7 @@ impl Config {
|
||||
config_path: config_path.to_str().unwrap().into(),
|
||||
whisper_language: default_whisper_language(),
|
||||
webhook_url: default_webhook_url(),
|
||||
danmu_ass_options: default_danmu_ass_options(),
|
||||
};
|
||||
|
||||
config.save();
|
||||
@@ -162,6 +169,12 @@ impl Config {
|
||||
self.save();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_danmu_ass_options(&mut self, options: Danmu2AssOptions) {
|
||||
self.danmu_ass_options = options;
|
||||
self.save();
|
||||
}
|
||||
|
||||
pub fn generate_clip_name(&self, params: &ClipRangeParams) -> PathBuf {
|
||||
// get format config
|
||||
// filter special characters from title to make sure file name is valid
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use recorder::danmu::DanmuEntry;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
// code reference: https://github.com/tiansh/us-danmaku/blob/master/bilibili/bilibili_ASS_Danmaku_Downloader.user.js
|
||||
@@ -30,9 +31,32 @@ const BOTTOM_RESERVED: f64 = 50.0;
|
||||
const R2L_TIME: f64 = 8.0;
|
||||
const MAX_DELAY: f64 = 6.0;
|
||||
|
||||
pub fn danmu_to_ass(danmus: Vec<DanmuEntry>) -> String {
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct Danmu2AssOptions {
|
||||
pub font_size: f64,
|
||||
pub opacity: f64, // 透明度,范围 0.0-1.0,0.0为完全透明,1.0为完全不透明
|
||||
}
|
||||
|
||||
impl Default for Danmu2AssOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_size: 36.0,
|
||||
opacity: 0.8, // 默认80%透明度
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn danmu_to_ass(danmus: Vec<DanmuEntry>, options: Danmu2AssOptions) -> String {
|
||||
let font_size = options.font_size; // Default font size
|
||||
let opacity = options.opacity; // 透明度参数
|
||||
|
||||
// 将透明度转换为十六进制Alpha值 (0.0-1.0 -> 0x00-0xFF)
|
||||
let alpha = ((1.0 - opacity) * 255.0) as u8;
|
||||
let alpha_hex = format!("{:02X}", alpha);
|
||||
|
||||
// ASS header
|
||||
let header = r"[Script Info]
|
||||
let header = format!(
|
||||
r"[Script Info]
|
||||
Title: Bilibili Danmaku
|
||||
ScriptType: v4.00+
|
||||
Collisions: Normal
|
||||
@@ -42,14 +66,15 @@ Timer: 10.0000
|
||||
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,微软雅黑,36,&H7fFFFFFF,&H7fFFFFFF,&H7f000000,&H7f000000,0,0,0,0,100,100,0,0,1,1,0,2,20,20,2,0
|
||||
Style: Default,微软雅黑,{},&H{}FFFFFF,&H{}FFFFFF,&H{}000000,&H{}000000,0,0,0,0,100,100,0,0,1,1,0,2,20,20,2,0
|
||||
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
";
|
||||
",
|
||||
font_size, alpha_hex, alpha_hex, alpha_hex, alpha_hex
|
||||
);
|
||||
|
||||
let mut normal = normal_danmaku();
|
||||
let font_size = 36.0; // Default font size
|
||||
|
||||
// Convert danmus to ASS events
|
||||
let events = danmus
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::config::Config;
|
||||
use crate::danmu2ass::Danmu2AssOptions;
|
||||
use crate::state::State;
|
||||
use crate::state_type;
|
||||
|
||||
@@ -280,3 +281,18 @@ pub async fn update_webhook_url(state: state_type!(), webhook_url: String) -> Re
|
||||
state.config.write().await.save();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "gui", tauri::command)]
|
||||
pub async fn update_danmu_ass_options(
|
||||
state: state_type!(),
|
||||
font_size: f64,
|
||||
opacity: f64,
|
||||
) -> Result<(), ()> {
|
||||
log::info!("Updating danmu ass options");
|
||||
state
|
||||
.config
|
||||
.write()
|
||||
.await
|
||||
.set_danmu_ass_options(Danmu2AssOptions { font_size, opacity });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -333,7 +333,10 @@ pub async fn export_danmu(
|
||||
}
|
||||
|
||||
if options.ass {
|
||||
Ok(danmu2ass::danmu_to_ass(danmus))
|
||||
Ok(danmu2ass::danmu_to_ass(
|
||||
danmus,
|
||||
danmu2ass::Danmu2AssOptions::default(),
|
||||
))
|
||||
} else {
|
||||
// map and join entries
|
||||
Ok(danmus
|
||||
|
||||
@@ -14,10 +14,11 @@ use crate::{
|
||||
add_account, get_account_count, get_accounts, get_qr, get_qr_status, remove_account,
|
||||
},
|
||||
config::{
|
||||
get_config, update_auto_generate, update_clip_name_format, update_notify,
|
||||
update_openai_api_endpoint, update_openai_api_key, update_status_check_interval,
|
||||
update_subtitle_generator_type, update_subtitle_setting, update_webhook_url,
|
||||
update_whisper_language, update_whisper_model, update_whisper_prompt,
|
||||
get_config, update_auto_generate, update_clip_name_format, update_danmu_ass_options,
|
||||
update_notify, update_openai_api_endpoint, update_openai_api_key,
|
||||
update_status_check_interval, update_subtitle_generator_type, update_subtitle_setting,
|
||||
update_webhook_url, update_whisper_language, update_whisper_model,
|
||||
update_whisper_prompt,
|
||||
},
|
||||
message::{delete_message, get_messages, read_message},
|
||||
recorder::{
|
||||
@@ -1050,6 +1051,21 @@ async fn handler_get_archives_by_parent_id(
|
||||
Ok(Json(ApiResponse::success(archives)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UpdateDanmuAssOptionsRequest {
|
||||
font_size: f64,
|
||||
opacity: f64,
|
||||
}
|
||||
|
||||
async fn handler_update_danmu_ass_options(
|
||||
state: axum::extract::State<State>,
|
||||
Json(param): Json<UpdateDanmuAssOptionsRequest>,
|
||||
) -> Result<Json<ApiResponse<()>>, ApiError> {
|
||||
update_danmu_ass_options(state.0, param.font_size, param.opacity).await;
|
||||
Ok(Json(ApiResponse::success(())))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BatchImportExternalVideosRequest {
|
||||
|
||||
@@ -430,6 +430,7 @@ fn setup_invoke_handlers(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<
|
||||
crate::handlers::config::update_status_check_interval,
|
||||
crate::handlers::config::update_whisper_language,
|
||||
crate::handlers::config::update_webhook_url,
|
||||
crate::handlers::config::update_danmu_ass_options,
|
||||
crate::handlers::message::get_messages,
|
||||
crate::handlers::message::read_message,
|
||||
crate::handlers::message::delete_message,
|
||||
|
||||
@@ -888,7 +888,8 @@ impl RecorderManager {
|
||||
return Ok(clip_file);
|
||||
}
|
||||
|
||||
let ass_content = danmu2ass::danmu_to_ass(danmus);
|
||||
let ass_content =
|
||||
danmu2ass::danmu_to_ass(danmus, self.config.read().await.danmu_ass_options.clone());
|
||||
// dump ass_content into a temp file
|
||||
let ass_file_path = clip_file.with_extension("ass");
|
||||
if let Err(e) = write(&ass_file_path, ass_content).await {
|
||||
@@ -928,7 +929,8 @@ impl RecorderManager {
|
||||
for d in &mut danmus {
|
||||
d.ts -= first_segment_timestamp_milis;
|
||||
}
|
||||
let ass_content = danmu2ass::danmu_to_ass(danmus);
|
||||
let ass_content =
|
||||
danmu2ass::danmu_to_ass(danmus, self.config.read().await.danmu_ass_options.clone());
|
||||
let work_dir = CachePath::new(
|
||||
self.config.read().await.cache.clone().into(),
|
||||
platform,
|
||||
|
||||
@@ -150,6 +150,12 @@ export interface Config {
|
||||
status_check_interval: number;
|
||||
whisper_language: string;
|
||||
webhook_url: string;
|
||||
danmu_ass_options: Danmu2AssOptions;
|
||||
}
|
||||
|
||||
export interface Danmu2AssOptions {
|
||||
font_size: number;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
export interface AutoGenerateConfig {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
status_check_interval: 30, // 默认30秒
|
||||
whisper_language: "",
|
||||
webhook_url: "",
|
||||
danmu_ass_options: {
|
||||
font_size: 36,
|
||||
opacity: 0.8,
|
||||
},
|
||||
};
|
||||
|
||||
let showModal = false;
|
||||
@@ -148,6 +152,13 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function update_danmu_ass_options() {
|
||||
await invoke("update_danmu_ass_options", {
|
||||
fontSize: setting_model.danmu_ass_options.font_size,
|
||||
opacity: setting_model.danmu_ass_options.opacity,
|
||||
});
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await get_config();
|
||||
});
|
||||
@@ -720,6 +731,73 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Danmu Style Settings -->
|
||||
<div class="space-y-4">
|
||||
<h2
|
||||
class="text-lg font-medium text-gray-900 dark:text-white flex items-center space-x-2"
|
||||
>
|
||||
<Captions class="w-5 h-5 dark:icon-white" />
|
||||
<span>弹幕压制样式</span>
|
||||
</h2>
|
||||
<div
|
||||
class="bg-white dark:bg-[#3c3c3e] rounded-xl border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700"
|
||||
>
|
||||
<!-- Font Size -->
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3
|
||||
class="text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
字体大小
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
设置弹幕字体大小
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 text-gray-900 dark:text-white w-24"
|
||||
bind:value={setting_model.danmu_ass_options.font_size}
|
||||
on:blur={update_danmu_ass_options}
|
||||
min="12"
|
||||
max="72"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Opacity -->
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3
|
||||
class="text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
不透明度
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
设置弹幕不透明度,范围
|
||||
0.0-1.0,0.0为完全透明,1.0为完全不透明
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 text-gray-900 dark:text-white w-24"
|
||||
bind:value={setting_model.danmu_ass_options.opacity}
|
||||
on:blur={update_danmu_ass_options}
|
||||
min="0.0"
|
||||
max="1.0"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto Clip Settings -->
|
||||
<div class="space-y-4">
|
||||
<h2
|
||||
|
||||
Reference in New Issue
Block a user