Files
aistory-generate/services/project_manager.py
T

339 lines
12 KiB
Python

"""
Mô-đun Quản lý dự án - Hỗ trợ lưu, tải, xuất dự án
"""
import json
import os
import re
import tempfile
import logging
from typing import List, Dict, Optional, Tuple
from datetime import datetime
from pathlib import Path
from services.novel_generator import NovelProject, Chapter
from locales.i18n import t
from core.database import get_db
logger = logging.getLogger(__name__)
class ProjectManager:
"""quản lý dự án"""
@staticmethod
def _slugify(name: str) -> str:
s = str(name or "").lower()
s = re.sub(r'[^\w\s-]', '', s)
s = re.sub(r'[\s_]+', '-', s)
s = re.sub(r'-+', '-', s).strip('-')
return s or "untitled"
@staticmethod
def create_project(
title: str,
genre: str,
sub_genres: List[str],
character_setting: str,
world_setting: str,
plot_idea: str
) -> Tuple[Optional[NovelProject], str]:
"""
Tạo dự án mới
Returns:
(Đối tượng dự án, Thông tin trạng thái)
"""
try:
if not title or not title.strip():
return None, "Title cannot be empty"
project_id = ProjectManager._slugify(title)
now = datetime.now().isoformat()
project = NovelProject(
title=title.strip(),
genre=genre.strip() if genre else "",
sub_genres=sub_genres if sub_genres else [],
character_setting=character_setting.strip() if character_setting else "",
world_setting=world_setting.strip() if world_setting else "",
plot_idea=plot_idea.strip() if plot_idea else "",
id=project_id,
created_at=now,
updated_at=now
)
logger.info(f"Project created: {project_id}")
return project, t("project_manager.create_success", title=title)
except Exception as e:
logger.error(f"Project create failed: {e}")
return None, t("project_manager.create_failed", error=str(e))
@staticmethod
def save_project(project: NovelProject) -> Tuple[bool, str]:
"""
Lưu dự án vào SQLite
Returns:
(Cờ thành công (boolean), Thông tin trạng thái)
"""
try:
if not project or not project.title:
return False, "Project data incomplete"
# Sử dụng project.id hiện có hoặc tạo mới
if getattr(project, 'id', None):
project_id = project.id
else:
project_id = ProjectManager._slugify(project.title)
project.id = project_id
conn = get_db()
now = datetime.now().isoformat()
project.updated_at = now
# Lưu project
conn.execute("""
INSERT OR REPLACE INTO projects
(id, title, genre, sub_genres, character_setting, world_setting, plot_idea, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
project_id,
project.title,
project.genre,
json.dumps(project.sub_genres if isinstance(project.sub_genres, list) else [], ensure_ascii=False),
project.character_setting,
project.world_setting,
project.plot_idea,
project.created_at,
now
))
# Xóa chapters cũ và insert lại
conn.execute("DELETE FROM chapters WHERE project_id = ?", (project_id,))
for ch in project.chapters:
conn.execute("""
INSERT INTO chapters
(project_id, num, title, desc, content, word_count, generated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
project_id,
ch.num,
ch.title,
ch.desc,
ch.content,
ch.word_count,
ch.generated_at
))
conn.commit()
logger.info(f"Project saved to database: {project_id}")
return True, t("project_manager.save_success", title=project_id)
except Exception as e:
logger.error(f"Project save failed: {e}")
return False, t("project_manager.save_failed", error=str(e))
@staticmethod
def load_project(project_id: str) -> Tuple[Optional[NovelProject], str]:
"""
Tải dự án từ SQLite
Returns:
(Đối tượng dự án, Thông tin trạng thái)
"""
try:
conn = get_db()
row = conn.execute(
"SELECT * FROM projects WHERE id = ?", (project_id,)
).fetchone()
if not row:
return None, t("project_manager.load_not_found", id=project_id)
# Lấy list json string
try:
sg_str = row["sub_genres"]
except (IndexError, KeyError):
sg_str = "[]"
try:
sg_list = json.loads(sg_str) if sg_str else []
except (json.JSONDecodeError, TypeError, ValueError):
sg_list = []
# Xây dựng lại dự án
project = NovelProject(
title=row["title"],
genre=row["genre"],
sub_genres=sg_list,
character_setting=row["character_setting"],
world_setting=row["world_setting"],
plot_idea=row["plot_idea"],
created_at=row["created_at"],
updated_at=row["updated_at"]
)
project.id = row["id"]
# Tải chapters
ch_rows = conn.execute(
"SELECT * FROM chapters WHERE project_id = ? ORDER BY num", (project_id,)
).fetchall()
for ch_row in ch_rows:
chapter = Chapter(
num=ch_row["num"],
title=ch_row["title"],
desc=ch_row["desc"],
content=ch_row["content"],
word_count=ch_row["word_count"],
generated_at=ch_row["generated_at"]
)
project.chapters.append(chapter)
logger.info(f"Project loaded from core.database: {project_id}")
return project, t("project_manager.load_success")
except Exception as e:
logger.error(f"Project load failed: {e}")
return None, t("project_manager.load_failed", error=str(e))
@staticmethod
def list_projects() -> List[Dict]:
"""
Liệt kê tất cả dự án từ SQLite
Returns:
Danh sách thông tin dự án
"""
try:
conn = get_db()
rows = conn.execute(
"SELECT id, title, genre, created_at, updated_at FROM projects ORDER BY updated_at DESC"
).fetchall()
projects = []
for row in rows:
# Đếm chapters
ch_count = conn.execute(
"SELECT COUNT(*) as total, SUM(CASE WHEN content != '' THEN 1 ELSE 0 END) as completed FROM chapters WHERE project_id = ?",
(row["id"],)
).fetchone()
projects.append({
"id": row["id"],
"title": row["title"],
"genre": row["genre"],
"created_at": row["created_at"],
"updated_at": row["updated_at"],
"chapter_count": ch_count["total"] if ch_count else 0,
"completed_chapters": ch_count["completed"] if ch_count else 0
})
logger.info(f"Found {len(projects)} projects in database")
return projects
except Exception as e:
logger.error(f"List projects failed: {e}")
return []
@staticmethod
def get_project_by_title(project_title: str) -> Optional[Dict]:
"""
Lấy thông tin dự án theo tiêu đề
Returns:
Từ điển dự án hoặc None
"""
projects = ProjectManager.list_projects()
for project in projects:
if project.get("title") == project_title:
return project
return None
@staticmethod
def delete_project(project_id: str) -> Tuple[bool, str]:
"""
Xóa dự án từ SQLite
Returns:
(Cờ thành công (boolean), Thông tin trạng thái)
"""
try:
conn = get_db()
cursor = conn.execute("DELETE FROM projects WHERE id = ?", (project_id,))
conn.commit()
if cursor.rowcount == 0:
return False, t("project_manager.delete_not_found", id=project_id)
logger.info(f"Project deleted from core.database: {project_id}")
return True, t("project_manager.delete_success")
except Exception as e:
logger.error(f"Project delete failed: {e}")
return False, t("project_manager.delete_failed", error=str(e))
@staticmethod
def export_project(project: NovelProject, export_format: str = "json") -> Tuple[Optional[str], str]:
"""
Xuất cấu hình dự án (để chia sẻ hoặc sao lưu)
Args:
project: Đối tượng dự án
export_format: Định dạng xuất (json/zip)
Returns:
(Đường dẫn tệp, Thông tin trạng thái)
"""
try:
if not project:
return None, ""
export_dir = os.path.join("exports", "project_backups")
os.makedirs(export_dir, exist_ok=True)
if export_format == "json":
data = {
"title": project.title,
"genre": project.genre,
"sub_genres": project.sub_genres,
"character_setting": project.character_setting,
"world_setting": project.world_setting,
"plot_idea": project.plot_idea,
"created_at": project.created_at,
"chapters": [ch.to_dict() for ch in project.chapters]
}
filename = f"{project.title}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
filepath = os.path.join(export_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
logger.info(f"Project exported: {filename}")
return filepath, t("project_manager.export_success", filepath=filename)
else:
return None, f"Unsupported format: {export_format}"
except Exception as e:
logger.error(f"Project export failed: {e}")
return None, t("project_manager.export_failed", error=str(e))
def get_project_manager() -> ProjectManager:
"""Nhận phiên bản quản lý dự án"""
return ProjectManager()
def list_project_titles():
"""Lấy danh sách tiêu đề dự án"""
try:
projects = ProjectManager.list_projects()
return [p["title"] for p in projects]
except Exception:
return []