""" 项目素材管理服务 """ 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": {} }