first commit

This commit is contained in:
root
2025-07-10 09:41:40 +08:00
commit 7d72e07639
58 changed files with 7492 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
"""
Services Module
服务层模块
Provides background services, file management, and task queue functionality.
"""
from .file_manager import FileManager
from .task_queue import TaskQueue
from .project_manager import ProjectManager
from .cache_manager import CacheManager
__all__ = [
"FileManager",
"TaskQueue",
"ProjectManager",
"CacheManager"
]

View File

@@ -0,0 +1,383 @@
#!/usr/bin/env python3
"""
File Manager Service
文件管理服务
Handles file operations, media import, and asset management.
"""
import os
import shutil
from pathlib import Path
from typing import Dict, Any, List, Optional, Union
import hashlib
import mimetypes
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import settings
from utils import setup_logger, validate_video_file, validate_audio_file, validate_image_file, get_file_info
logger = setup_logger(__name__)
class FileManager:
"""File management service."""
def __init__(self):
self.temp_dir = settings.temp_dir
self.cache_dir = settings.cache_dir
self.projects_dir = settings.projects_dir
def import_file(self, source_path: str, project_path: str, copy_file: bool = True) -> Dict[str, Any]:
"""
Import a media file into a project.
Args:
source_path: Source file path
project_path: Project directory path
copy_file: Whether to copy file to project assets
Returns:
Dictionary with import result
"""
try:
source = Path(source_path)
if not source.exists():
raise FileNotFoundError(f"Source file not found: {source_path}")
# Validate file type
file_type = self._get_file_type(source_path)
if file_type == "unknown":
raise ValueError(f"Unsupported file type: {source.suffix}")
# Get file info
file_info = get_file_info(source_path)
if "error" in file_info:
raise ValueError(f"Failed to get file info: {file_info['error']}")
# Determine destination
project_dir = Path(project_path)
assets_dir = project_dir / "assets"
assets_dir.mkdir(exist_ok=True)
if copy_file:
# Copy file to project assets
dest_path = assets_dir / source.name
# Handle name conflicts
counter = 1
while dest_path.exists():
stem = source.stem
suffix = source.suffix
dest_path = assets_dir / f"{stem}_{counter}{suffix}"
counter += 1
shutil.copy2(source, dest_path)
final_path = str(dest_path)
logger.info(f"Copied file to: {final_path}")
else:
# Use original path
final_path = source_path
logger.info(f"Linked file: {final_path}")
# Generate file hash for deduplication
file_hash = self._calculate_file_hash(final_path)
return {
"status": "success",
"file_path": final_path,
"file_type": file_type,
"file_info": file_info,
"file_hash": file_hash,
"imported_at": file_info.get("modified", 0)
}
except Exception as e:
logger.error(f"Failed to import file: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def export_file(self, source_path: str, dest_path: str,
move_file: bool = False) -> Dict[str, Any]:
"""
Export a file from project to external location.
Args:
source_path: Source file path
dest_path: Destination file path
move_file: Whether to move instead of copy
Returns:
Dictionary with export result
"""
try:
source = Path(source_path)
dest = Path(dest_path)
if not source.exists():
raise FileNotFoundError(f"Source file not found: {source_path}")
# Ensure destination directory exists
dest.parent.mkdir(parents=True, exist_ok=True)
# Handle name conflicts
if dest.exists():
counter = 1
while dest.exists():
stem = dest.stem
suffix = dest.suffix
dest = dest.parent / f"{stem}_{counter}{suffix}"
counter += 1
if move_file:
shutil.move(source, dest)
logger.info(f"Moved file to: {dest}")
else:
shutil.copy2(source, dest)
logger.info(f"Copied file to: {dest}")
return {
"status": "success",
"dest_path": str(dest),
"operation": "move" if move_file else "copy"
}
except Exception as e:
logger.error(f"Failed to export file: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def list_project_assets(self, project_path: str) -> Dict[str, Any]:
"""
List all assets in a project.
Args:
project_path: Project directory path
Returns:
Dictionary with asset list
"""
try:
project_dir = Path(project_path)
assets_dir = project_dir / "assets"
if not assets_dir.exists():
return {
"status": "success",
"assets": [],
"count": 0
}
assets = []
for file_path in assets_dir.rglob("*"):
if file_path.is_file():
file_type = self._get_file_type(str(file_path))
if file_type != "unknown":
file_info = get_file_info(str(file_path))
assets.append({
"name": file_path.name,
"path": str(file_path),
"relative_path": str(file_path.relative_to(project_dir)),
"type": file_type,
"size": file_info.get("size", 0),
"size_mb": file_info.get("size_mb", 0),
"modified": file_info.get("modified", 0),
"duration": file_info.get("duration"),
"width": file_info.get("width"),
"height": file_info.get("height"),
"file_hash": self._calculate_file_hash(str(file_path))
})
# Sort by name
assets.sort(key=lambda x: x["name"].lower())
return {
"status": "success",
"assets": assets,
"count": len(assets)
}
except Exception as e:
logger.error(f"Failed to list project assets: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def create_temp_file(self, suffix: str = "", prefix: str = "mixvideo_") -> str:
"""
Create a temporary file.
Args:
suffix: File suffix/extension
prefix: File prefix
Returns:
Path to temporary file
"""
import tempfile
fd, temp_path = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=self.temp_dir)
os.close(fd) # Close file descriptor
logger.debug(f"Created temp file: {temp_path}")
return temp_path
def cleanup_temp_files(self, max_age_hours: int = 24) -> Dict[str, Any]:
"""
Clean up old temporary files.
Args:
max_age_hours: Maximum age of files to keep
Returns:
Dictionary with cleanup result
"""
try:
from utils import clean_temp_files
clean_temp_files(self.temp_dir, max_age_hours)
clean_temp_files(self.cache_dir, max_age_hours)
return {
"status": "success",
"message": f"Cleaned temp files older than {max_age_hours} hours"
}
except Exception as e:
logger.error(f"Failed to cleanup temp files: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def get_disk_usage(self, path: str) -> Dict[str, Any]:
"""
Get disk usage information for a path.
Args:
path: Directory path
Returns:
Dictionary with disk usage info
"""
try:
path_obj = Path(path)
if not path_obj.exists():
raise FileNotFoundError(f"Path not found: {path}")
# Get disk usage
usage = shutil.disk_usage(path)
# Calculate directory size if it's a directory
if path_obj.is_dir():
dir_size = sum(f.stat().st_size for f in path_obj.rglob('*') if f.is_file())
else:
dir_size = path_obj.stat().st_size
return {
"status": "success",
"path": path,
"total_space": usage.total,
"used_space": usage.used,
"free_space": usage.free,
"directory_size": dir_size,
"usage_percent": (usage.used / usage.total) * 100
}
except Exception as e:
logger.error(f"Failed to get disk usage: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def _get_file_type(self, file_path: str) -> str:
"""
Determine file type based on extension and validation.
Args:
file_path: Path to file
Returns:
File type: 'video', 'audio', 'image', or 'unknown'
"""
if validate_video_file(file_path):
return "video"
elif validate_audio_file(file_path):
return "audio"
elif validate_image_file(file_path):
return "image"
else:
return "unknown"
def _calculate_file_hash(self, file_path: str) -> str:
"""
Calculate MD5 hash of a file.
Args:
file_path: Path to file
Returns:
MD5 hash string
"""
try:
hash_md5 = hashlib.md5()
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:
return ""
def find_duplicates(self, project_path: str) -> Dict[str, Any]:
"""
Find duplicate files in a project based on hash.
Args:
project_path: Project directory path
Returns:
Dictionary with duplicate files
"""
try:
assets_result = self.list_project_assets(project_path)
if assets_result["status"] != "success":
return assets_result
# Group files by hash
hash_groups = {}
for asset in assets_result["assets"]:
file_hash = asset["file_hash"]
if file_hash:
if file_hash not in hash_groups:
hash_groups[file_hash] = []
hash_groups[file_hash].append(asset)
# Find duplicates (groups with more than one file)
duplicates = {hash_val: files for hash_val, files in hash_groups.items() if len(files) > 1}
return {
"status": "success",
"duplicates": duplicates,
"duplicate_count": sum(len(files) - 1 for files in duplicates.values()),
"total_duplicate_size": sum(
sum(file["size"] for file in files[1:]) # Skip first file in each group
for files in duplicates.values()
)
}
except Exception as e:
logger.error(f"Failed to find duplicates: {str(e)}")
return {
"status": "error",
"error": str(e)
}

View File

@@ -0,0 +1,427 @@
#!/usr/bin/env python3
"""
Project Manager Service
项目管理服务
Handles project creation, loading, saving, and management.
"""
import argparse
import json
import sys
from pathlib import Path
from typing import Dict, Any, List, Optional
from datetime import datetime
import uuid
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import settings
from utils import setup_logger, ensure_directory, get_unique_filename
logger = setup_logger(__name__)
class ProjectManager:
"""Project management service."""
def __init__(self):
self.projects_dir = settings.projects_dir
self.temp_dir = settings.temp_dir
def create_project(self, name: str, description: str = "") -> Dict[str, Any]:
"""
Create a new project.
Args:
name: Project name
description: Project description
Returns:
Dictionary with project information
"""
try:
# Create project directory
project_path = self.projects_dir / name
ensure_directory(project_path)
# Create project structure
(project_path / "assets").mkdir(exist_ok=True)
(project_path / "exports").mkdir(exist_ok=True)
(project_path / "temp").mkdir(exist_ok=True)
# Create project info
project_info = {
"id": str(uuid.uuid4()),
"name": name,
"description": description,
"path": str(project_path),
"created_at": datetime.now().isoformat(),
"modified_at": datetime.now().isoformat(),
"version": "1.0",
"settings": {
"resolution": settings.max_video_resolution,
"fps": settings.default_fps,
"video_codec": settings.default_video_codec,
"audio_codec": settings.default_audio_codec
},
"video_tracks": [],
"audio_tracks": [],
"timeline": {
"duration": 0.0,
"zoom": 1.0,
"position": 0.0
}
}
# Save project file
project_file = project_path / f"{name}.mixvideo"
with open(project_file, 'w', encoding='utf-8') as f:
json.dump(project_info, f, indent=2, ensure_ascii=False)
logger.info(f"Created project: {name} at {project_path}")
return {
"status": "success",
"project": project_info
}
except Exception as e:
logger.error(f"Failed to create project: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def load_project(self, project_path: str) -> Dict[str, Any]:
"""
Load an existing project.
Args:
project_path: Path to project file or directory
Returns:
Dictionary with project information
"""
try:
path = Path(project_path)
# If it's a directory, look for .mixvideo file
if path.is_dir():
mixvideo_files = list(path.glob("*.mixvideo"))
if not mixvideo_files:
raise FileNotFoundError(f"No .mixvideo file found in {path}")
project_file = mixvideo_files[0]
else:
project_file = path
if not project_file.exists():
raise FileNotFoundError(f"Project file not found: {project_file}")
# Load project data
with open(project_file, 'r', encoding='utf-8') as f:
project_info = json.load(f)
# Update modified time
project_info["modified_at"] = datetime.now().isoformat()
logger.info(f"Loaded project: {project_info['name']}")
return {
"status": "success",
"project": project_info
}
except Exception as e:
logger.error(f"Failed to load project: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def save_project(self, project_info: Dict[str, Any]) -> Dict[str, Any]:
"""
Save project to file.
Args:
project_info: Project information dictionary
Returns:
Dictionary with save result
"""
try:
project_path = Path(project_info["path"])
project_name = project_info["name"]
# Update modified time
project_info["modified_at"] = datetime.now().isoformat()
# Save project file
project_file = project_path / f"{project_name}.mixvideo"
with open(project_file, 'w', encoding='utf-8') as f:
json.dump(project_info, f, indent=2, ensure_ascii=False)
logger.info(f"Saved project: {project_name}")
return {
"status": "success",
"message": f"Project saved to {project_file}"
}
except Exception as e:
logger.error(f"Failed to save project: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def list_projects(self) -> Dict[str, Any]:
"""
List all projects in the projects directory.
Returns:
Dictionary with list of projects
"""
try:
projects = []
for project_file in self.projects_dir.glob("**/*.mixvideo"):
try:
with open(project_file, 'r', encoding='utf-8') as f:
project_info = json.load(f)
# Add basic project info
projects.append({
"id": project_info.get("id"),
"name": project_info.get("name"),
"description": project_info.get("description", ""),
"path": str(project_file.parent),
"created_at": project_info.get("created_at"),
"modified_at": project_info.get("modified_at"),
"file_size": project_file.stat().st_size
})
except Exception as e:
logger.warning(f"Failed to read project file {project_file}: {str(e)}")
continue
# Sort by modified time (newest first)
projects.sort(key=lambda x: x.get("modified_at", ""), reverse=True)
return {
"status": "success",
"projects": projects,
"count": len(projects)
}
except Exception as e:
logger.error(f"Failed to list projects: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def delete_project(self, project_path: str) -> Dict[str, Any]:
"""
Delete a project.
Args:
project_path: Path to project directory
Returns:
Dictionary with deletion result
"""
try:
import shutil
path = Path(project_path)
if not path.exists():
raise FileNotFoundError(f"Project path not found: {path}")
if path.is_dir():
shutil.rmtree(path)
else:
path.unlink()
logger.info(f"Deleted project: {path}")
return {
"status": "success",
"message": f"Project deleted: {path}"
}
except Exception as e:
logger.error(f"Failed to delete project: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def add_video_track(self, project_info: Dict[str, Any], file_path: str,
start_time: float = 0.0, duration: Optional[float] = None) -> Dict[str, Any]:
"""
Add a video track to the project.
Args:
project_info: Project information
file_path: Path to video file
start_time: Start time in timeline
duration: Duration (auto-detect if None)
Returns:
Updated project information
"""
try:
from utils import get_file_info
file_info = get_file_info(file_path)
if "error" in file_info:
raise ValueError(f"Invalid video file: {file_info['error']}")
track_id = str(uuid.uuid4())
video_track = {
"id": track_id,
"name": Path(file_path).stem,
"file_path": file_path,
"start_time": start_time,
"duration": duration or file_info.get("duration", 0.0),
"file_info": file_info,
"effects": [],
"volume": 1.0,
"enabled": True
}
project_info["video_tracks"].append(video_track)
project_info["modified_at"] = datetime.now().isoformat()
logger.info(f"Added video track: {video_track['name']}")
return {
"status": "success",
"project": project_info,
"track_id": track_id
}
except Exception as e:
logger.error(f"Failed to add video track: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def add_audio_track(self, project_info: Dict[str, Any], file_path: str,
start_time: float = 0.0, duration: Optional[float] = None) -> Dict[str, Any]:
"""
Add an audio track to the project.
Args:
project_info: Project information
file_path: Path to audio file
start_time: Start time in timeline
duration: Duration (auto-detect if None)
Returns:
Updated project information
"""
try:
from utils import get_file_info
file_info = get_file_info(file_path)
if "error" in file_info:
raise ValueError(f"Invalid audio file: {file_info['error']}")
track_id = str(uuid.uuid4())
audio_track = {
"id": track_id,
"name": Path(file_path).stem,
"file_path": file_path,
"start_time": start_time,
"duration": duration or file_info.get("duration", 0.0),
"file_info": file_info,
"effects": [],
"volume": 1.0,
"enabled": True
}
project_info["audio_tracks"].append(audio_track)
project_info["modified_at"] = datetime.now().isoformat()
logger.info(f"Added audio track: {audio_track['name']}")
return {
"status": "success",
"project": project_info,
"track_id": track_id
}
except Exception as e:
logger.error(f"Failed to add audio track: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def main():
"""Command line interface for project management."""
parser = argparse.ArgumentParser(description="Project Manager")
parser.add_argument("--action", required=True,
choices=["create", "load", "save", "list", "delete", "get_info"],
help="Action to perform")
parser.add_argument("--name", help="Project name")
parser.add_argument("--path", help="Project path")
parser.add_argument("--data", help="Project data as JSON string")
parser.add_argument("--description", help="Project description")
args = parser.parse_args()
try:
manager = ProjectManager()
if args.action == "create":
if not args.name:
raise ValueError("Project name is required for create action")
result = manager.create_project(args.name, args.description or "")
elif args.action == "load":
if not args.path:
raise ValueError("Project path is required for load action")
result = manager.load_project(args.path)
elif args.action == "save":
if not args.data:
raise ValueError("Project data is required for save action")
project_data = json.loads(args.data)
result = manager.save_project(project_data)
elif args.action == "list":
result = manager.list_projects()
elif args.action == "delete":
if not args.path:
raise ValueError("Project path is required for delete action")
result = manager.delete_project(args.path)
elif args.action == "get_info":
if not args.path:
raise ValueError("Project path is required for get_info action")
result = manager.load_project(args.path)
else:
raise ValueError(f"Unknown action: {args.action}")
print(json.dumps(result))
except Exception as e:
error_result = {
"status": "error",
"error": str(e)
}
print(json.dumps(error_result))
sys.exit(1)
if __name__ == "__main__":
main()