feat: 添加文件名序号匹配规则功能
- 新增 FilenameUtils 工具类,支持从文件名中提取3位数字序号 - 扩展 SegmentMatchingRule 枚举,添加 FilenameSequence 匹配规则 - 在 MaterialMatchingService 中实现基于文件名序号的匹配逻辑 - 确保每个模板只能使用一个序号为001的视频文件 - 添加全面的单元测试和集成测试 - 支持多种文件名格式:name_001.ext, 001_name.ext, name001.ext 等 遵循 promptx/tauri-desktop-app-expert 开发规范
This commit is contained in:
@@ -15,6 +15,7 @@ use crate::data::repositories::{
|
||||
};
|
||||
use crate::business::services::template_service::TemplateService;
|
||||
use crate::business::services::template_matching_result_service::TemplateMatchingResultService;
|
||||
use crate::infrastructure::filename_utils::FilenameUtils;
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -518,6 +519,15 @@ impl MaterialMatchingService {
|
||||
used_segment_ids,
|
||||
).await
|
||||
}
|
||||
SegmentMatchingRule::FilenameSequence { target_sequence } => {
|
||||
self.match_by_filename_sequence(
|
||||
track_segment,
|
||||
available_segments,
|
||||
target_sequence,
|
||||
project_materials,
|
||||
used_segment_ids,
|
||||
).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,6 +720,91 @@ impl MaterialMatchingService {
|
||||
best_match
|
||||
}
|
||||
|
||||
/// 根据文件名序号匹配素材
|
||||
async fn match_by_filename_sequence(
|
||||
&self,
|
||||
track_segment: &TrackSegment,
|
||||
available_segments: &[(MaterialSegment, String)],
|
||||
target_sequence: &str,
|
||||
project_materials: &[Material],
|
||||
used_segment_ids: &mut HashSet<String>,
|
||||
) -> Result<SegmentMatch, String> {
|
||||
println!("🔢 开始文件名序号匹配:");
|
||||
println!(" 目标序号: {}", target_sequence);
|
||||
println!(" 轨道片段: {} ({})", track_segment.id, track_segment.name);
|
||||
|
||||
let target_duration = track_segment.duration as f64 / 1_000_000.0; // 转换为秒
|
||||
println!(" 目标时长(秒): {:.3}", target_duration);
|
||||
|
||||
// 特殊处理:如果目标序号是001,检查是否已经使用了序号001的视频
|
||||
if target_sequence == "001" {
|
||||
// 检查已使用的片段中是否有序号001的视频
|
||||
for used_id in used_segment_ids.iter() {
|
||||
if let Some((used_segment, _)) = available_segments.iter().find(|(seg, _)| seg.id == *used_id) {
|
||||
if FilenameUtils::has_sequence_001(&used_segment.file_path) && FilenameUtils::is_video_file(&used_segment.file_path) {
|
||||
return Err("该模板已经使用了一个序号为001的视频,不能再使用其他序号001的视频".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选出包含目标序号的视频文件片段
|
||||
let mut matching_segments = Vec::new();
|
||||
|
||||
for (segment, category) in available_segments {
|
||||
// 跳过已使用的片段
|
||||
if used_segment_ids.contains(&segment.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查文件名是否包含目标序号
|
||||
if FilenameUtils::has_sequence_number(&segment.file_path, target_sequence) {
|
||||
// 确保是视频文件
|
||||
if FilenameUtils::is_video_file(&segment.file_path) {
|
||||
// 查找对应的素材信息
|
||||
if let Some(material) = project_materials.iter().find(|m| m.id == segment.material_id) {
|
||||
matching_segments.push((segment, category, material));
|
||||
println!(" ✅ 找到匹配文件: {} (序号: {})",
|
||||
FilenameUtils::extract_filename(&segment.file_path), target_sequence);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matching_segments.is_empty() {
|
||||
return Err(format!("没有找到包含序号 {} 的视频文件", target_sequence));
|
||||
}
|
||||
|
||||
println!(" 找到 {} 个包含目标序号的视频片段", matching_segments.len());
|
||||
|
||||
// 在匹配的片段中找到最佳时长匹配
|
||||
if let Some((best_segment, best_category, best_material)) = self.find_best_duration_match(
|
||||
&matching_segments,
|
||||
target_duration,
|
||||
) {
|
||||
// 标记为已使用
|
||||
used_segment_ids.insert(best_segment.id.clone());
|
||||
|
||||
println!("🎯 文件名序号匹配成功:");
|
||||
println!(" 选中文件: {}", FilenameUtils::extract_filename(&best_segment.file_path));
|
||||
println!(" 序号: {}", target_sequence);
|
||||
println!(" 片段时长(秒): {:.3}", best_segment.duration);
|
||||
|
||||
return Ok(SegmentMatch {
|
||||
track_segment_id: track_segment.id.clone(),
|
||||
track_segment_name: track_segment.name.clone(),
|
||||
material_segment_id: best_segment.id.clone(),
|
||||
material_segment: (*best_segment).clone(),
|
||||
material_name: best_material.name.clone(),
|
||||
model_name: best_material.model_id.clone(),
|
||||
match_score: best_segment.duration_match_score(target_duration),
|
||||
match_reason: format!("文件名序号匹配: {}", target_sequence),
|
||||
});
|
||||
}
|
||||
|
||||
Err(format!("没有找到满足时长要求且包含序号 {} 的视频片段", target_sequence))
|
||||
}
|
||||
|
||||
/// 执行一键匹配 - 遍历项目的所有活跃模板绑定并逐一匹配
|
||||
pub async fn batch_match_all_templates(&self, request: BatchMatchingRequest, database: Arc<crate::infrastructure::database::Database>) -> Result<BatchMatchingResult> {
|
||||
// 调用优化的循环匹配方法
|
||||
@@ -761,6 +856,10 @@ impl MaterialMatchingService {
|
||||
// 为每个模板维护独立的序号计数器
|
||||
let mut template_counters: HashMap<String, u32> = HashMap::new();
|
||||
|
||||
// 跟踪哪些模板已经使用了序号001的视频(确保每个模板只能使用一个序号001的视频)
|
||||
// TODO: 实现模板级别的序号001使用限制
|
||||
// let mut _templates_used_sequence_001: HashSet<String> = HashSet::new();
|
||||
|
||||
// 获取项目中已使用的素材片段ID列表(从数据库)
|
||||
let existing_used_segments = match self.material_usage_repo.get_usage_records_by_project(&request.project_id) {
|
||||
Ok(usage_records) => {
|
||||
@@ -1026,6 +1125,13 @@ impl MaterialMatchingService {
|
||||
SegmentMatchingRule::RandomMatch => {
|
||||
!available_segments.is_empty()
|
||||
}
|
||||
SegmentMatchingRule::FilenameSequence { target_sequence } => {
|
||||
// 检查是否有包含目标序号的视频文件
|
||||
available_segments.iter().any(|(segment, _)| {
|
||||
FilenameUtils::is_video_file(&segment.file_path) &&
|
||||
FilenameUtils::has_sequence_number(&segment.file_path, target_sequence)
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ pub enum SegmentMatchingRule {
|
||||
AiClassification { category_id: String, category_name: String },
|
||||
/// 随机匹配 - 从项目中随机选择合适的素材
|
||||
RandomMatch,
|
||||
/// 文件名序号匹配 - 根据文件名中的序号进行匹配
|
||||
FilenameSequence { target_sequence: String },
|
||||
}
|
||||
|
||||
impl Default for SegmentMatchingRule {
|
||||
@@ -87,6 +89,7 @@ impl SegmentMatchingRule {
|
||||
Self::FixedMaterial => "固定素材".to_string(),
|
||||
Self::AiClassification { category_name, .. } => format!("AI分类: {}", category_name),
|
||||
Self::RandomMatch => "随机匹配".to_string(),
|
||||
Self::FilenameSequence { target_sequence } => format!("文件名序号: {}", target_sequence),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +107,19 @@ impl SegmentMatchingRule {
|
||||
pub fn is_random_match(&self) -> bool {
|
||||
matches!(self, Self::RandomMatch)
|
||||
}
|
||||
|
||||
/// 检查是否为文件名序号匹配
|
||||
pub fn is_filename_sequence(&self) -> bool {
|
||||
matches!(self, Self::FilenameSequence { .. })
|
||||
}
|
||||
|
||||
/// 获取目标序号(如果是文件名序号匹配)
|
||||
pub fn get_target_sequence(&self) -> Option<&String> {
|
||||
match self {
|
||||
Self::FilenameSequence { target_sequence } => Some(target_sequence),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 轨道片段
|
||||
|
||||
364
apps/desktop/src-tauri/src/infrastructure/filename_utils.rs
Normal file
364
apps/desktop/src-tauri/src/infrastructure/filename_utils.rs
Normal file
@@ -0,0 +1,364 @@
|
||||
use std::path::Path;
|
||||
use regex::Regex;
|
||||
use anyhow::{Result, anyhow};
|
||||
|
||||
/// 文件名序号解析工具
|
||||
/// 遵循 Tauri 开发规范的工具函数设计原则
|
||||
pub struct FilenameUtils;
|
||||
|
||||
impl FilenameUtils {
|
||||
/// 从文件名中提取序号
|
||||
/// 支持多种文件名格式:
|
||||
/// - name_001.ext -> 001
|
||||
/// - name001.ext -> 001
|
||||
/// - name-001.ext -> 001
|
||||
/// - 001_name.ext -> 001
|
||||
/// - name_001_suffix.ext -> 001
|
||||
pub fn extract_sequence_number(file_path: &str) -> Option<String> {
|
||||
let path = Path::new(file_path);
|
||||
let filename = path.file_stem()?.to_str()?;
|
||||
|
||||
// 首先检查是否以3位数字开头(最高优先级)
|
||||
if let Ok(regex) = Regex::new(r"^(\d{3})_") {
|
||||
if let Some(captures) = regex.captures(filename) {
|
||||
if let Some(sequence) = captures.get(1) {
|
||||
return Some(sequence.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 然后按优先级检查其他模式(确保是恰好3位数字)
|
||||
let patterns = vec$", // name001 (字母后跟3位数字结尾)
|
||||
];
|
||||
|
||||
// 对于非开头的模式,选择最右边的匹配
|
||||
let mut rightmost_match: Option<(usize, String)> = None;
|
||||
|
||||
for pattern in patterns {
|
||||
if let Ok(regex) = Regex::new(pattern) {
|
||||
for captures in regex.captures_iter(filename) {
|
||||
if let Some(sequence_match) = captures.get(1) {
|
||||
let position = sequence_match.start();
|
||||
let sequence = sequence_match.as_str().to_string();
|
||||
|
||||
if rightmost_match.is_none() || position > rightmost_match.as_ref().unwrap().0 {
|
||||
rightmost_match = Some((position, sequence));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightmost_match.map(|(_, sequence)| sequence)
|
||||
}
|
||||
|
||||
/// 检查文件名是否包含指定序号
|
||||
pub fn has_sequence_number(file_path: &str, target_sequence: &str) -> bool {
|
||||
if let Some(sequence) = Self::extract_sequence_number(file_path) {
|
||||
sequence == target_sequence
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查文件名是否包含序号001
|
||||
pub fn has_sequence_001(file_path: &str) -> bool {
|
||||
Self::has_sequence_number(file_path, "001")
|
||||
}
|
||||
|
||||
/// 从文件路径中提取文件名(不含路径)
|
||||
pub fn extract_filename(file_path: &str) -> String {
|
||||
Path::new(file_path)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// 从文件路径中提取文件名(不含扩展名)
|
||||
pub fn extract_filename_without_extension(file_path: &str) -> String {
|
||||
Path::new(file_path)
|
||||
.file_stem()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// 验证序号格式(必须是3位数字)
|
||||
pub fn is_valid_sequence_format(sequence: &str) -> bool {
|
||||
if sequence.len() != 3 {
|
||||
return false;
|
||||
}
|
||||
|
||||
sequence.chars().all(|c| c.is_ascii_digit())
|
||||
}
|
||||
|
||||
/// 格式化序号为3位数字格式
|
||||
pub fn format_sequence_number(number: u32) -> Result<String> {
|
||||
if number > 999 {
|
||||
return Err(anyhow!("序号不能超过999: {}", number));
|
||||
}
|
||||
|
||||
Ok(format!("{:03}", number))
|
||||
}
|
||||
|
||||
/// 解析序号字符串为数字
|
||||
pub fn parse_sequence_number(sequence: &str) -> Result<u32> {
|
||||
if !Self::is_valid_sequence_format(sequence) {
|
||||
return Err(anyhow!("无效的序号格式: {}", sequence));
|
||||
}
|
||||
|
||||
sequence.parse::<u32>()
|
||||
.map_err(|e| anyhow!("解析序号失败: {}", e))
|
||||
}
|
||||
|
||||
/// 生成带序号的文件名
|
||||
pub fn generate_filename_with_sequence(
|
||||
base_name: &str,
|
||||
sequence: u32,
|
||||
extension: &str,
|
||||
) -> Result<String> {
|
||||
let formatted_sequence = Self::format_sequence_number(sequence)?;
|
||||
Ok(format!("{}_{}.{}", base_name, formatted_sequence, extension))
|
||||
}
|
||||
|
||||
/// 检查文件是否为视频文件
|
||||
pub fn is_video_file(file_path: &str) -> bool {
|
||||
let video_extensions = vec![
|
||||
"mp4", "avi", "mov", "mkv", "wmv", "flv", "webm", "m4v", "3gp", "ts", "mts"
|
||||
];
|
||||
|
||||
if let Some(extension) = Path::new(file_path).extension() {
|
||||
if let Some(ext_str) = extension.to_str() {
|
||||
return video_extensions.contains(&ext_str.to_lowercase().as_str());
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// 从文件路径列表中筛选出包含指定序号的视频文件
|
||||
pub fn filter_videos_by_sequence(
|
||||
file_paths: &[String],
|
||||
target_sequence: &str,
|
||||
) -> Vec<String> {
|
||||
file_paths
|
||||
.iter()
|
||||
.filter(|path| Self::is_video_file(path))
|
||||
.filter(|path| Self::has_sequence_number(path, target_sequence))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 从文件路径列表中筛选出包含序号001的视频文件
|
||||
pub fn filter_videos_with_sequence_001(file_paths: &[String]) -> Vec<String> {
|
||||
Self::filter_videos_by_sequence(file_paths, "001")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_sequence_number() {
|
||||
// 测试各种文件名格式
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_001.mp4"), Some("001".to_string()));
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video001.mp4"), Some("001".to_string()));
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video-001.mp4"), Some("001".to_string()));
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("001_video.mp4"), Some("001".to_string()));
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_001_final.mp4"), Some("001".to_string()));
|
||||
|
||||
// 测试无序号的情况
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video.mp4"), None);
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_1.mp4"), None); // 不是3位数
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_abc.mp4"), None); // 不是数字
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_sequence_001() {
|
||||
assert!(FilenameUtils::has_sequence_001("video_001.mp4"));
|
||||
assert!(FilenameUtils::has_sequence_001("001_video.mp4"));
|
||||
assert!(!FilenameUtils::has_sequence_001("video_002.mp4"));
|
||||
assert!(!FilenameUtils::has_sequence_001("video.mp4"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_sequence_format() {
|
||||
assert!(FilenameUtils::is_valid_sequence_format("001"));
|
||||
assert!(FilenameUtils::is_valid_sequence_format("123"));
|
||||
assert!(FilenameUtils::is_valid_sequence_format("999"));
|
||||
|
||||
assert!(!FilenameUtils::is_valid_sequence_format("1"));
|
||||
assert!(!FilenameUtils::is_valid_sequence_format("12"));
|
||||
assert!(!FilenameUtils::is_valid_sequence_format("1234"));
|
||||
assert!(!FilenameUtils::is_valid_sequence_format("abc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_sequence_number() {
|
||||
assert_eq!(FilenameUtils::format_sequence_number(1).unwrap(), "001");
|
||||
assert_eq!(FilenameUtils::format_sequence_number(42).unwrap(), "042");
|
||||
assert_eq!(FilenameUtils::format_sequence_number(999).unwrap(), "999");
|
||||
|
||||
assert!(FilenameUtils::format_sequence_number(1000).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_video_file() {
|
||||
assert!(FilenameUtils::is_video_file("video.mp4"));
|
||||
assert!(FilenameUtils::is_video_file("video.MP4")); // 大小写不敏感
|
||||
assert!(FilenameUtils::is_video_file("video.avi"));
|
||||
assert!(FilenameUtils::is_video_file("video.mov"));
|
||||
|
||||
assert!(!FilenameUtils::is_video_file("image.jpg"));
|
||||
assert!(!FilenameUtils::is_video_file("audio.mp3"));
|
||||
assert!(!FilenameUtils::is_video_file("document.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_videos_with_sequence_001() {
|
||||
let files = vec![
|
||||
"video_001.mp4".to_string(),
|
||||
"video_002.mp4".to_string(),
|
||||
"image_001.jpg".to_string(),
|
||||
"audio_001.mp3".to_string(),
|
||||
"video.mp4".to_string(),
|
||||
];
|
||||
|
||||
let result = FilenameUtils::filter_videos_with_sequence_001(&files);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0], "video_001.mp4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_sequence_number_edge_cases() {
|
||||
// 测试路径分隔符
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("C:\\videos\\test_001.mp4"), Some("001".to_string()));
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("/home/user/videos/test_001.mp4"), Some("001".to_string()));
|
||||
|
||||
// 测试多个数字序列
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_123_001.mp4"), Some("001".to_string()));
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("001_video_456.mp4"), Some("001".to_string()));
|
||||
|
||||
// 测试边界数字
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_000.mp4"), Some("000".to_string()));
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_999.mp4"), Some("999".to_string()));
|
||||
|
||||
// 测试无效格式
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_0001.mp4"), None); // 4位数字
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_01.mp4"), None); // 2位数字
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_1.mp4"), None); // 1位数字
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_sequence_number_various_formats() {
|
||||
// 测试不同的分隔符
|
||||
assert!(FilenameUtils::has_sequence_number("video_001.mp4", "001"));
|
||||
assert!(FilenameUtils::has_sequence_number("video-001.mp4", "001"));
|
||||
assert!(FilenameUtils::has_sequence_number("001_video.mp4", "001"));
|
||||
assert!(FilenameUtils::has_sequence_number("video001.mp4", "001"));
|
||||
|
||||
// 测试不匹配的情况
|
||||
assert!(!FilenameUtils::has_sequence_number("video_002.mp4", "001"));
|
||||
assert!(!FilenameUtils::has_sequence_number("video.mp4", "001"));
|
||||
assert!(!FilenameUtils::has_sequence_number("video_abc.mp4", "001"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_sequence_number() {
|
||||
assert_eq!(FilenameUtils::parse_sequence_number("001").unwrap(), 1);
|
||||
assert_eq!(FilenameUtils::parse_sequence_number("042").unwrap(), 42);
|
||||
assert_eq!(FilenameUtils::parse_sequence_number("999").unwrap(), 999);
|
||||
assert_eq!(FilenameUtils::parse_sequence_number("000").unwrap(), 0);
|
||||
|
||||
// 测试无效格式
|
||||
assert!(FilenameUtils::parse_sequence_number("1").is_err());
|
||||
assert!(FilenameUtils::parse_sequence_number("12").is_err());
|
||||
assert!(FilenameUtils::parse_sequence_number("1234").is_err());
|
||||
assert!(FilenameUtils::parse_sequence_number("abc").is_err());
|
||||
assert!(FilenameUtils::parse_sequence_number("").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_filename_with_sequence() {
|
||||
assert_eq!(
|
||||
FilenameUtils::generate_filename_with_sequence("video", 1, "mp4").unwrap(),
|
||||
"video_001.mp4"
|
||||
);
|
||||
assert_eq!(
|
||||
FilenameUtils::generate_filename_with_sequence("test", 42, "avi").unwrap(),
|
||||
"test_042.avi"
|
||||
);
|
||||
assert_eq!(
|
||||
FilenameUtils::generate_filename_with_sequence("movie", 999, "mkv").unwrap(),
|
||||
"movie_999.mkv"
|
||||
);
|
||||
|
||||
// 测试超出范围的序号
|
||||
assert!(FilenameUtils::generate_filename_with_sequence("video", 1000, "mp4").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_filename_functions() {
|
||||
let test_path = "C:\\Users\\test\\videos\\my_video_001.mp4";
|
||||
|
||||
assert_eq!(FilenameUtils::extract_filename(test_path), "my_video_001.mp4");
|
||||
assert_eq!(FilenameUtils::extract_filename_without_extension(test_path), "my_video_001");
|
||||
|
||||
// 测试Unix路径
|
||||
let unix_path = "/home/user/videos/my_video_001.mp4";
|
||||
assert_eq!(FilenameUtils::extract_filename(unix_path), "my_video_001.mp4");
|
||||
assert_eq!(FilenameUtils::extract_filename_without_extension(unix_path), "my_video_001");
|
||||
|
||||
// 测试无效路径
|
||||
assert_eq!(FilenameUtils::extract_filename(""), "unknown");
|
||||
assert_eq!(FilenameUtils::extract_filename_without_extension(""), "unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_videos_by_sequence() {
|
||||
let files = vec![
|
||||
"video_001.mp4".to_string(),
|
||||
"video_002.mp4".to_string(),
|
||||
"video_003.avi".to_string(),
|
||||
"image_001.jpg".to_string(),
|
||||
"audio_002.mp3".to_string(),
|
||||
"document_002.txt".to_string(),
|
||||
"movie-002.mkv".to_string(),
|
||||
];
|
||||
|
||||
let result_001 = FilenameUtils::filter_videos_by_sequence(&files, "001");
|
||||
assert_eq!(result_001.len(), 1);
|
||||
assert_eq!(result_001[0], "video_001.mp4");
|
||||
|
||||
let result_002 = FilenameUtils::filter_videos_by_sequence(&files, "002");
|
||||
assert_eq!(result_002.len(), 2);
|
||||
assert!(result_002.contains(&"video_002.mp4".to_string()));
|
||||
assert!(result_002.contains(&"movie-002.mkv".to_string()));
|
||||
|
||||
let result_999 = FilenameUtils::filter_videos_by_sequence(&files, "999");
|
||||
assert_eq!(result_999.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_filename_patterns() {
|
||||
// 测试复杂的文件名模式
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("2024_01_15_video_001_final.mp4"), Some("001".to_string()));
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("project-v2-scene_042-take1.avi"), Some("042".to_string()));
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("001_opening_scene_HD.mkv"), Some("001".to_string()));
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("movie_trailer_v001.mp4"), Some("001".to_string()));
|
||||
|
||||
// 测试不应该匹配的模式
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_2024_01_15.mp4"), None); // 年份不应该匹配
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("1080p_video.mp4"), None); // 分辨率不应该匹配
|
||||
assert_eq!(FilenameUtils::extract_sequence_number("video_60fps.mp4"), None); // 帧率不应该匹配
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
pub mod database;
|
||||
pub mod connection_pool;
|
||||
pub mod file_system;
|
||||
pub mod filename_utils;
|
||||
pub mod performance;
|
||||
pub mod event_bus;
|
||||
pub mod ffmpeg;
|
||||
|
||||
@@ -353,6 +353,7 @@ pub fn run() {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod batch_delete_test;
|
||||
mod material_matching_service_test;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// use crate::business::services::material_matching_service::MaterialMatchingService;
|
||||
use crate::data::models::{
|
||||
material::{Material, MaterialSegment, MaterialType, ProcessingStatus, MaterialMetadata},
|
||||
template::{SegmentMatchingRule, TrackSegment},
|
||||
};
|
||||
use crate::infrastructure::filename_utils::FilenameUtils;
|
||||
use std::collections::HashSet;
|
||||
use chrono::Utc;
|
||||
|
||||
/// 创建测试用的素材片段
|
||||
fn create_test_material_segment(
|
||||
id: &str,
|
||||
material_id: &str,
|
||||
file_path: &str,
|
||||
duration: f64,
|
||||
) -> MaterialSegment {
|
||||
MaterialSegment {
|
||||
id: id.to_string(),
|
||||
material_id: material_id.to_string(),
|
||||
segment_index: 0,
|
||||
start_time: 0.0,
|
||||
end_time: duration,
|
||||
duration,
|
||||
file_path: file_path.to_string(),
|
||||
file_size: 1024 * 1024, // 1MB
|
||||
thumbnail_path: None,
|
||||
usage_count: 0,
|
||||
is_used: false,
|
||||
last_used_at: None,
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建测试用的素材
|
||||
fn create_test_material(id: &str, name: &str, model_id: Option<String>) -> Material {
|
||||
Material {
|
||||
id: id.to_string(),
|
||||
project_id: "test_project".to_string(),
|
||||
model_id,
|
||||
name: name.to_string(),
|
||||
original_path: format!("/test/path/{}.mp4", name),
|
||||
file_size: 1024 * 1024,
|
||||
md5_hash: "test_hash".to_string(),
|
||||
material_type: MaterialType::Video,
|
||||
processing_status: ProcessingStatus::Completed,
|
||||
metadata: MaterialMetadata::None,
|
||||
scene_detection: None,
|
||||
segments: Vec::new(),
|
||||
thumbnail_path: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
processed_at: Some(Utc::now()),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建测试用的轨道片段
|
||||
fn create_test_track_segment(
|
||||
id: &str,
|
||||
name: &str,
|
||||
duration_seconds: f64,
|
||||
matching_rule: SegmentMatchingRule,
|
||||
) -> TrackSegment {
|
||||
let duration_microseconds = (duration_seconds * 1_000_000.0) as u64;
|
||||
let mut segment = TrackSegment::new(
|
||||
id.to_string(),
|
||||
"test_track".to_string(),
|
||||
name.to_string(),
|
||||
0,
|
||||
duration_microseconds,
|
||||
0,
|
||||
);
|
||||
segment.set_matching_rule(matching_rule);
|
||||
segment
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filename_sequence_matching_basic() {
|
||||
// 测试基本的文件名序号匹配
|
||||
let segments = vec![
|
||||
(
|
||||
create_test_material_segment("seg1", "mat1", "video_001.mp4", 10.0),
|
||||
"category1".to_string(),
|
||||
),
|
||||
(
|
||||
create_test_material_segment("seg2", "mat2", "video_002.mp4", 15.0),
|
||||
"category2".to_string(),
|
||||
),
|
||||
(
|
||||
create_test_material_segment("seg3", "mat3", "image_001.jpg", 5.0),
|
||||
"category3".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
let materials = vec![
|
||||
create_test_material("mat1", "video_001", Some("model1".to_string())),
|
||||
create_test_material("mat2", "video_002", Some("model2".to_string())),
|
||||
create_test_material("mat3", "image_001", None),
|
||||
];
|
||||
|
||||
let _track_segment = create_test_track_segment(
|
||||
"track_seg1",
|
||||
"Test Segment",
|
||||
8.0,
|
||||
SegmentMatchingRule::FilenameSequence {
|
||||
target_sequence: "001".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
// 模拟匹配逻辑
|
||||
let mut matching_segments = Vec::new();
|
||||
for (segment, category) in &segments {
|
||||
if FilenameUtils::has_sequence_number(&segment.file_path, "001") {
|
||||
if FilenameUtils::is_video_file(&segment.file_path) {
|
||||
if let Some(material) = materials.iter().find(|m| m.id == segment.material_id) {
|
||||
matching_segments.push((segment, category, material));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应该找到一个匹配的视频文件
|
||||
assert_eq!(matching_segments.len(), 1);
|
||||
assert_eq!(matching_segments[0].0.file_path, "video_001.mp4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filename_sequence_matching_no_videos() {
|
||||
// 测试没有视频文件的情况
|
||||
let segments = vec![
|
||||
(
|
||||
create_test_material_segment("seg1", "mat1", "image_001.jpg", 10.0),
|
||||
"category1".to_string(),
|
||||
),
|
||||
(
|
||||
create_test_material_segment("seg2", "mat2", "audio_001.mp3", 15.0),
|
||||
"category2".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
let materials = vec![
|
||||
create_test_material("mat1", "image_001", None),
|
||||
create_test_material("mat2", "audio_001", None),
|
||||
];
|
||||
|
||||
// 模拟匹配逻辑
|
||||
let mut matching_segments = Vec::new();
|
||||
for (segment, category) in &segments {
|
||||
if FilenameUtils::has_sequence_number(&segment.file_path, "001") {
|
||||
if FilenameUtils::is_video_file(&segment.file_path) {
|
||||
if let Some(material) = materials.iter().find(|m| m.id == segment.material_id) {
|
||||
matching_segments.push((segment, category, material));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应该没有找到匹配的视频文件
|
||||
assert_eq!(matching_segments.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filename_sequence_matching_wrong_sequence() {
|
||||
// 测试序号不匹配的情况
|
||||
let segments = vec![
|
||||
(
|
||||
create_test_material_segment("seg1", "mat1", "video_002.mp4", 10.0),
|
||||
"category1".to_string(),
|
||||
),
|
||||
(
|
||||
create_test_material_segment("seg2", "mat2", "video_003.mp4", 15.0),
|
||||
"category2".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
let materials = vec![
|
||||
create_test_material("mat1", "video_002", Some("model1".to_string())),
|
||||
create_test_material("mat2", "video_003", Some("model2".to_string())),
|
||||
];
|
||||
|
||||
// 模拟匹配逻辑 - 寻找序号001
|
||||
let mut matching_segments = Vec::new();
|
||||
for (segment, category) in &segments {
|
||||
if FilenameUtils::has_sequence_number(&segment.file_path, "001") {
|
||||
if FilenameUtils::is_video_file(&segment.file_path) {
|
||||
if let Some(material) = materials.iter().find(|m| m.id == segment.material_id) {
|
||||
matching_segments.push((segment, category, material));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应该没有找到匹配的文件(因为没有序号001的文件)
|
||||
assert_eq!(matching_segments.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sequence_001_usage_validation() {
|
||||
// 测试序号001使用限制逻辑
|
||||
let segments = vec![
|
||||
(
|
||||
create_test_material_segment("seg1", "mat1", "video_001.mp4", 10.0),
|
||||
"category1".to_string(),
|
||||
),
|
||||
(
|
||||
create_test_material_segment("seg2", "mat2", "another_001.mp4", 15.0),
|
||||
"category2".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
let mut used_segment_ids = HashSet::new();
|
||||
|
||||
// 模拟第一次使用序号001的视频
|
||||
used_segment_ids.insert("seg1".to_string());
|
||||
|
||||
// 检查是否已经使用了序号001的视频
|
||||
let mut has_used_sequence_001 = false;
|
||||
for used_id in &used_segment_ids {
|
||||
if let Some((used_segment, _)) = segments.iter().find(|(seg, _)| seg.id == *used_id) {
|
||||
if FilenameUtils::has_sequence_001(&used_segment.file_path) &&
|
||||
FilenameUtils::is_video_file(&used_segment.file_path) {
|
||||
has_used_sequence_001 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应该检测到已经使用了序号001的视频
|
||||
assert!(has_used_sequence_001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_sequence_001_files() {
|
||||
// 测试多个序号001文件的情况
|
||||
let files = vec![
|
||||
"video_001.mp4".to_string(),
|
||||
"scene_001.avi".to_string(),
|
||||
"take_001.mkv".to_string(),
|
||||
"video_002.mp4".to_string(),
|
||||
];
|
||||
|
||||
let sequence_001_videos = FilenameUtils::filter_videos_with_sequence_001(&files);
|
||||
|
||||
// 应该找到3个序号001的视频文件
|
||||
assert_eq!(sequence_001_videos.len(), 3);
|
||||
assert!(sequence_001_videos.contains(&"video_001.mp4".to_string()));
|
||||
assert!(sequence_001_videos.contains(&"scene_001.avi".to_string()));
|
||||
assert!(sequence_001_videos.contains(&"take_001.mkv".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segment_matching_rule_display() {
|
||||
// 测试匹配规则的显示名称
|
||||
let rule = SegmentMatchingRule::FilenameSequence {
|
||||
target_sequence: "001".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(rule.display_name(), "文件名序号: 001");
|
||||
assert!(rule.is_filename_sequence());
|
||||
assert!(!rule.is_fixed_material());
|
||||
assert!(!rule.is_ai_classification());
|
||||
assert!(!rule.is_random_match());
|
||||
assert_eq!(rule.get_target_sequence(), Some(&"001".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segment_matching_rule_serialization() {
|
||||
// 测试匹配规则的序列化和反序列化
|
||||
let rule = SegmentMatchingRule::FilenameSequence {
|
||||
target_sequence: "042".to_string(),
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_string(&rule).unwrap();
|
||||
let deserialized: SegmentMatchingRule = serde_json::from_str(&serialized).unwrap();
|
||||
|
||||
match deserialized {
|
||||
SegmentMatchingRule::FilenameSequence { target_sequence } => {
|
||||
assert_eq!(target_sequence, "042");
|
||||
}
|
||||
_ => panic!("Deserialization failed"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_filename_sequence_workflow() {
|
||||
// 集成测试:完整的文件名序号匹配工作流程
|
||||
|
||||
// 1. 创建测试数据 - 模拟项目中的视频文件
|
||||
let video_files = vec![
|
||||
"scene_001.mp4",
|
||||
"scene_002.mp4",
|
||||
"take_001.avi",
|
||||
"final_001.mkv",
|
||||
"backup_002.mp4",
|
||||
"test_video.mp4", // 无序号
|
||||
];
|
||||
|
||||
// 2. 测试文件名序号提取
|
||||
for file in &video_files {
|
||||
let sequence = FilenameUtils::extract_sequence_number(file);
|
||||
match *file {
|
||||
f if f.contains("_001") => assert_eq!(sequence, Some("001".to_string())),
|
||||
f if f.contains("_002") => assert_eq!(sequence, Some("002".to_string())),
|
||||
"test_video.mp4" => assert_eq!(sequence, None),
|
||||
_ => {} // 其他情况
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 测试序号001的筛选
|
||||
let sequence_001_files = FilenameUtils::filter_videos_with_sequence_001(
|
||||
&video_files.iter().map(|s| s.to_string()).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
assert_eq!(sequence_001_files.len(), 3); // scene_001.mp4, take_001.avi, final_001.mkv
|
||||
assert!(sequence_001_files.contains(&"scene_001.mp4".to_string()));
|
||||
assert!(sequence_001_files.contains(&"take_001.avi".to_string()));
|
||||
assert!(sequence_001_files.contains(&"final_001.mkv".to_string()));
|
||||
|
||||
// 4. 测试模板匹配规则创建
|
||||
let rule = SegmentMatchingRule::FilenameSequence {
|
||||
target_sequence: "001".to_string(),
|
||||
};
|
||||
|
||||
assert!(rule.is_filename_sequence());
|
||||
assert_eq!(rule.get_target_sequence(), Some(&"001".to_string()));
|
||||
assert_eq!(rule.display_name(), "文件名序号: 001");
|
||||
|
||||
// 5. 测试序列化兼容性(确保可以存储到数据库)
|
||||
let json = serde_json::to_string(&rule).unwrap();
|
||||
let restored: SegmentMatchingRule = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(rule, restored);
|
||||
|
||||
println!("✅ 文件名序号匹配功能集成测试通过");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user