299 lines
11 KiB
Python
299 lines
11 KiB
Python
"""
|
||
项目素材管理服务
|
||
"""
|
||
|
||
import json
|
||
import hashlib
|
||
from pathlib import Path
|
||
from typing import List, Dict, Any, Optional
|
||
from datetime import datetime
|
||
|
||
from python_core.utils.logger import logger
|
||
|
||
|
||
class ProjectMaterialService:
|
||
"""项目素材管理服务"""
|
||
|
||
def __init__(self):
|
||
logger.info("ProjectMaterialService 初始化完成")
|
||
|
||
def import_video_materials(
|
||
self,
|
||
video_segments: List[Any],
|
||
project_id: str,
|
||
project_directory: Path,
|
||
source_video_path: str,
|
||
material_tags: Optional[List[str]] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
导入视频素材到项目
|
||
|
||
Args:
|
||
video_segments: 视频片段列表
|
||
project_id: 项目ID
|
||
project_directory: 项目目录
|
||
source_video_path: 源视频路径
|
||
material_tags: 素材标签
|
||
|
||
Returns:
|
||
Dict: 导入结果
|
||
"""
|
||
|
||
try:
|
||
# 创建项目素材目录
|
||
uncategorized_dir = project_directory / "未分类文件夹"
|
||
uncategorized_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 加载现有的项目素材列表
|
||
material_list_file = project_directory / "project_material.json"
|
||
existing_materials = self._load_material_list(material_list_file)
|
||
|
||
# 处理视频片段
|
||
new_materials = []
|
||
imported_count = 0
|
||
skipped_count = 0
|
||
|
||
for segment in video_segments:
|
||
if not segment.success:
|
||
continue
|
||
|
||
try:
|
||
result = self._import_single_segment(
|
||
segment=segment,
|
||
uncategorized_dir=uncategorized_dir,
|
||
existing_materials=existing_materials,
|
||
project_id=project_id,
|
||
source_video_path=source_video_path,
|
||
material_tags=material_tags or []
|
||
)
|
||
|
||
if result["imported"]:
|
||
new_materials.append(result["material_info"])
|
||
imported_count += 1
|
||
logger.info(f"✅ 素材已导入: {result['original_filename']} -> {result['target_filename']}")
|
||
else:
|
||
skipped_count += 1
|
||
logger.info(f"📋 素材已存在,跳过: {result['original_filename']}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 导入素材失败 {segment.output_path.name}: {e}")
|
||
skipped_count += 1
|
||
|
||
# 更新项目素材列表
|
||
if new_materials:
|
||
existing_materials.extend(new_materials)
|
||
self._save_material_list(material_list_file, existing_materials)
|
||
logger.info(f"📝 项目素材列表已更新: 新增 {len(new_materials)} 个素材")
|
||
|
||
return {
|
||
"success": True,
|
||
"imported_count": imported_count,
|
||
"skipped_count": skipped_count,
|
||
"total_count": len(video_segments),
|
||
"new_materials": new_materials
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 项目素材导入失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": str(e),
|
||
"imported_count": 0,
|
||
"skipped_count": 0,
|
||
"total_count": len(video_segments)
|
||
}
|
||
|
||
def _import_single_segment(
|
||
self,
|
||
segment: Any,
|
||
uncategorized_dir: Path,
|
||
existing_materials: List[Dict],
|
||
project_id: str,
|
||
source_video_path: str,
|
||
material_tags: List[str]
|
||
) -> Dict[str, Any]:
|
||
"""导入单个视频片段"""
|
||
|
||
# 复制文件到项目目录
|
||
import shutil
|
||
temp_target_path = uncategorized_dir / f"temp_{segment.output_path.name}"
|
||
shutil.copy2(segment.output_path, temp_target_path)
|
||
|
||
# 计算复制后文件的MD5
|
||
file_md5 = self._calculate_file_md5(temp_target_path)
|
||
|
||
# 检查是否已存在相同MD5的素材
|
||
if any(material.get('md5') == file_md5 for material in existing_materials):
|
||
# 删除临时文件
|
||
temp_target_path.unlink()
|
||
return {
|
||
"imported": False,
|
||
"original_filename": segment.output_path.name,
|
||
"reason": "duplicate_md5"
|
||
}
|
||
|
||
# 重命名为最终文件名
|
||
target_filename = f"{file_md5}.mp4"
|
||
target_path = uncategorized_dir / target_filename
|
||
temp_target_path.rename(target_path)
|
||
|
||
# 创建素材信息
|
||
material_info = {
|
||
"id": file_md5,
|
||
"md5": file_md5,
|
||
"original_filename": segment.output_path.name,
|
||
"filename": target_filename,
|
||
"file_path": str(target_path),
|
||
"relative_path": f"未分类文件夹/{target_filename}",
|
||
"file_size": segment.file_size,
|
||
"duration": segment.duration,
|
||
"start_time": segment.start_time,
|
||
"end_time": segment.end_time,
|
||
"scene_index": segment.scene_index,
|
||
"tags": material_tags.copy(),
|
||
"created_at": datetime.now().isoformat(),
|
||
"use_count": 0,
|
||
"source_video": source_video_path,
|
||
"project_id": project_id
|
||
}
|
||
|
||
return {
|
||
"imported": True,
|
||
"material_info": material_info,
|
||
"original_filename": segment.output_path.name,
|
||
"target_filename": target_filename
|
||
}
|
||
|
||
def _load_material_list(self, material_list_file: Path) -> List[Dict]:
|
||
"""加载项目素材列表"""
|
||
if not material_list_file.exists():
|
||
return []
|
||
|
||
try:
|
||
with open(material_list_file, 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
except Exception as e:
|
||
logger.warning(f"⚠️ 读取现有素材列表失败: {e}")
|
||
return []
|
||
|
||
def _save_material_list(self, material_list_file: Path, materials: List[Dict]):
|
||
"""保存项目素材列表"""
|
||
try:
|
||
with open(material_list_file, 'w', encoding='utf-8') as f:
|
||
json.dump(materials, f, ensure_ascii=False, indent=2)
|
||
except Exception as e:
|
||
logger.error(f"❌ 保存项目素材列表失败: {e}")
|
||
raise
|
||
|
||
def _calculate_file_md5(self, file_path: Path) -> str:
|
||
"""计算文件MD5值"""
|
||
hash_md5 = hashlib.md5()
|
||
try:
|
||
with open(file_path, "rb") as f:
|
||
for chunk in iter(lambda: f.read(4096), b""):
|
||
hash_md5.update(chunk)
|
||
return hash_md5.hexdigest()
|
||
except Exception as e:
|
||
logger.error(f"❌ 计算MD5失败 {file_path}: {e}")
|
||
# 如果计算MD5失败,使用文件名和大小作为备用标识
|
||
return hashlib.md5(f"{file_path.name}_{file_path.stat().st_size}".encode()).hexdigest()
|
||
|
||
def get_project_materials(self, project_directory: Path) -> List[Dict]:
|
||
"""获取项目素材列表"""
|
||
material_list_file = project_directory / "project_material.json"
|
||
return self._load_material_list(material_list_file)
|
||
|
||
def add_material_tags(self, project_directory: Path, material_id: str, tags: List[str]) -> bool:
|
||
"""为素材添加标签"""
|
||
try:
|
||
material_list_file = project_directory / "project_material.json"
|
||
materials = self._load_material_list(material_list_file)
|
||
|
||
for material in materials:
|
||
if material.get('id') == material_id:
|
||
existing_tags = set(material.get('tags', []))
|
||
new_tags = existing_tags.union(set(tags))
|
||
material['tags'] = list(new_tags)
|
||
|
||
self._save_material_list(material_list_file, materials)
|
||
logger.info(f"✅ 素材标签已更新: {material_id}")
|
||
return True
|
||
|
||
logger.warning(f"⚠️ 未找到素材: {material_id}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 添加素材标签失败: {e}")
|
||
return False
|
||
|
||
def remove_material(self, project_directory: Path, material_id: str) -> bool:
|
||
"""删除项目素材"""
|
||
try:
|
||
material_list_file = project_directory / "project_material.json"
|
||
materials = self._load_material_list(material_list_file)
|
||
|
||
# 查找要删除的素材
|
||
material_to_remove = None
|
||
for i, material in enumerate(materials):
|
||
if material.get('id') == material_id:
|
||
material_to_remove = materials.pop(i)
|
||
break
|
||
|
||
if material_to_remove:
|
||
# 删除文件
|
||
file_path = Path(material_to_remove['file_path'])
|
||
if file_path.exists():
|
||
file_path.unlink()
|
||
logger.info(f"🗑️ 已删除素材文件: {file_path.name}")
|
||
|
||
# 更新素材列表
|
||
self._save_material_list(material_list_file, materials)
|
||
logger.info(f"✅ 素材已删除: {material_id}")
|
||
return True
|
||
else:
|
||
logger.warning(f"⚠️ 未找到素材: {material_id}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 删除素材失败: {e}")
|
||
return False
|
||
|
||
def get_material_stats(self, project_directory: Path) -> Dict[str, Any]:
|
||
"""获取项目素材统计信息"""
|
||
try:
|
||
materials = self.get_project_materials(project_directory)
|
||
|
||
total_count = len(materials)
|
||
total_size = sum(material.get('file_size', 0) for material in materials)
|
||
total_duration = sum(material.get('duration', 0) for material in materials)
|
||
|
||
# 按标签统计
|
||
tag_stats = {}
|
||
for material in materials:
|
||
for tag in material.get('tags', []):
|
||
tag_stats[tag] = tag_stats.get(tag, 0) + 1
|
||
|
||
# 按使用次数统计
|
||
used_count = sum(1 for material in materials if material.get('use_count', 0) > 0)
|
||
unused_count = total_count - used_count
|
||
|
||
return {
|
||
"total_count": total_count,
|
||
"total_size": total_size,
|
||
"total_duration": total_duration,
|
||
"used_count": used_count,
|
||
"unused_count": unused_count,
|
||
"tag_stats": tag_stats
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 获取素材统计失败: {e}")
|
||
return {
|
||
"total_count": 0,
|
||
"total_size": 0,
|
||
"total_duration": 0,
|
||
"used_count": 0,
|
||
"unused_count": 0,
|
||
"tag_stats": {}
|
||
}
|