use crate::{CanvasConfig, Dandan, InputFile};
use anyhow::{anyhow, Result};
use clap::{Parser, ValueEnum};
use std::{collections::HashSet, path::PathBuf};
use super::input_path_to_list;
#[derive(Clone, Debug, ValueEnum, serde::Deserialize)]
pub enum SimplifiedOrTraditional {
#[serde(rename = "simplified")]
Simplified,
#[serde(rename = "traditional")]
Traditional,
#[serde(rename = "original")]
Original,
}
#[derive(Parser, Debug, serde::Deserialize)]
pub struct Args {
#[clap(help = "需要转换的输入,可以是视频、文件夹", default_value = ".")]
pub input: String,
#[clap(long = "width", help = "屏幕宽度", default_value = "1280")]
width: u32,
#[clap(long = "height", help = "屏幕高度", default_value = "720")]
height: u32,
#[clap(
value_enum,
long = "simplified-or-traditional",
help = "换为繁体或简体",
default_value = "simplified"
)]
simplified_or_traditional: SimplifiedOrTraditional,
#[clap(
long = "merge-built-in-interactive",
short = 'm',
help = "与视频内置字幕合并,通过用户选择,需要 ffmpeg 命令"
)]
merge_built_in_interactive: bool,
#[clap(
long = "merge-built-in",
help = "与视频内置字幕合并,值为内置弹幕 stream index, 需要 ffmpeg 命令, 例如: --merge-built-in=0",
default_value = ""
)]
merge_built_in: String,
#[clap(
long = "font",
short = 'f',
help = "弹幕使用字体。单位:像素",
default_value = "黑体"
)]
font: String,
#[clap(long = "font-size", help = "弹幕字体大小", default_value = "35")]
font_size: u32,
#[clap(
long = "lane-size",
short = 'l',
help = "弹幕所占据的高度,即“行高度/行间距”",
default_value = "35"
)]
lane_size: u32,
#[clap(
long = "width-ratio",
help = "计算弹幕宽度的比例,为避免重叠可以调大这个数值",
default_value = "1.2"
)]
width_ratio: f64,
#[clap(
long = "horizontal-gap",
help = "每条弹幕之间的最小水平间距,为避免重叠可以调大这个数值。单位:像素",
default_value = "20.0"
)]
#[serde(default)]
horizontal_gap: f64,
#[clap(
long = "duration",
short = 'd',
help = "弹幕在屏幕上的持续时间,单位为秒,可以有小数",
default_value = "15"
)]
duration: f64,
#[clap(
long = "float-percentage",
short = 'p',
help = "屏幕上滚动弹幕最多高度百分比",
default_value = "0.4"
)]
float_percentage: f64,
#[clap(
long = "alpha",
short = 'a',
help = "弹幕不透明度",
default_value = "0.7"
)]
alpha: f64,
#[clap(
long = "force",
help = "默认会跳过已经存在 json 缓存的文件,此参数会强制更新"
)]
pub force: bool,
#[clap(long = "change-match", help = "修改识别结果")]
pub change_match: bool,
#[clap(
long = "denylist",
help = "黑名单,需要过滤的关键词列表文件,每行一个关键词"
)]
denylist: Option<PathBuf>,
#[clap(long = "pause", help = "在处理完后暂停等待输入")]
pub pause: bool,
#[clap(long = "outline", help = "描边宽度", default_value = "0.8")]
pub outline: f64,
#[clap(long = "bold", help = "加粗")]
#[serde(default)]
pub bold: bool,
#[clap(
long = "time-offset",
help = "时间轴偏移,>0 会让弹幕延后,<0 会让弹幕提前,单位为秒",
default_value = "0.0"
)]
#[serde(default)]
pub time_offset: f64,
}
impl Args {
pub fn check(&mut self) -> Result<()> {
if let Some(f) = self.denylist.as_ref() {
if !f.exists() {
return Err(anyhow!("黑名单文件不存在"));
}
if f.is_dir() {
return Err(anyhow!("黑名单文件不能是目录"));
}
}
if self.float_percentage < 0.0 {
return Err(anyhow!("滚动弹幕最大高度百分比不能小于 0"));
}
if self.float_percentage > 1.0 {
return Err(anyhow!("滚动弹幕最大高度百分比不能大于 1"));
}
Ok(())
}
pub fn canvas_config(&self) -> CanvasConfig {
CanvasConfig {
width: self.width,
height: self.height,
font: self.font.clone(),
font_size: self.font_size,
width_ratio: self.width_ratio,
horizontal_gap: self.horizontal_gap,
duration: self.duration,
lane_size: self.lane_size,
float_percentage: self.float_percentage,
opacity: ((1.0 - self.alpha) * 255.0) as u8,
bottom_percentage: 0.3,
outline: self.outline,
bold: u8::from(self.bold),
time_offset: self.time_offset,
}
}
fn denylist(&self) -> Result<Option<HashSet<String>>> {
match self.denylist.as_ref() {
None => Ok(None),
Some(path) => {
let denylist = std::fs::read_to_string(path)?;
let list: HashSet<String> = denylist
.split('\n')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
info!("黑名单载入 {} 个", list.len());
debug!("黑名单:{:?}", list);
Ok(Some(list))
}
}
}
pub async fn process(&self) -> Result<()> {
let canvas_config = self.canvas_config();
let denylist = self.denylist()?;
let filepaths = input_path_to_list(&self.input)?;
if filepaths.is_empty() {
return Err(anyhow!("没有找到任何文件"));
}
info!("共找到 {} 个文件", filepaths.len());
let t = std::time::Instant::now();
let mut process_file_total = 0;
let mut process_danmu_total = 0;
for filepath in filepaths {
let input_file = InputFile::from(&filepath);
let (file_count, danmu_count) = match Dandan::process_by_path(
&input_file,
self.force,
self.change_match,
self.simplified_or_traditional.clone(),
self.merge_built_in_interactive,
self.merge_built_in.clone(),
canvas_config.clone(),
&denylist,
)
.await
{
Ok(danmu_count) => (1, danmu_count),
Err(e) => {
error!("{} {:?}", input_file.log("文件转换错误"), e);
(0, 0)
}
};
process_file_total += file_count;
process_danmu_total += danmu_count;
}
info!(
"共转换 {} 个文件,共转换 {} 条弹幕,耗时 {:?}",
process_file_total,
process_danmu_total,
t.elapsed()
);
Ok(())
}
}