228 lines
7.2 KiB
Python
228 lines
7.2 KiB
Python
|
|
"""
|
||
|
|
数据模型定义
|
||
|
|
定义了项目中使用的主要数据结构。
|
||
|
|
"""
|
||
|
|
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from datetime import datetime
|
||
|
|
from enum import Enum
|
||
|
|
from typing import List, Optional, Dict, Any
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
class PlatformType(Enum):
|
||
|
|
"""支持的平台类型"""
|
||
|
|
XIAOHONGSHU = "xiaohongshu"
|
||
|
|
DOUYIN = "douyin"
|
||
|
|
|
||
|
|
|
||
|
|
class UploadStatus(Enum):
|
||
|
|
"""上传状态枚举"""
|
||
|
|
PENDING = "pending" # 等待上传
|
||
|
|
UPLOADING = "uploading" # 上传中
|
||
|
|
PROCESSING = "processing" # 处理中
|
||
|
|
COMPLETED = "completed" # 上传完成
|
||
|
|
FAILED = "failed" # 上传失败
|
||
|
|
CANCELLED = "cancelled" # 取消上传
|
||
|
|
|
||
|
|
|
||
|
|
class PublishStatus(Enum):
|
||
|
|
"""发布状态枚举"""
|
||
|
|
DRAFT = "draft" # 草稿
|
||
|
|
PENDING = "pending" # 等待发布
|
||
|
|
PUBLISHING = "publishing" # 发布中
|
||
|
|
PUBLISHED = "published" # 已发布
|
||
|
|
FAILED = "failed" # 发布失败
|
||
|
|
SCHEDULED = "scheduled" # 定时发布
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class AccountInfo:
|
||
|
|
"""账号信息"""
|
||
|
|
platform: PlatformType = field()
|
||
|
|
username: str = field()
|
||
|
|
cookie_file: str = field()
|
||
|
|
is_active: bool = True
|
||
|
|
user_data_dir: Optional[str] = None
|
||
|
|
extra_config: Dict[str, Any] = field(default_factory=dict)
|
||
|
|
|
||
|
|
def __post_init__(self):
|
||
|
|
"""后处理,确保路径存在"""
|
||
|
|
if not self.cookie_file.endswith('.json'):
|
||
|
|
self.cookie_file = f"{self.cookie_file}.json"
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Content:
|
||
|
|
"""内容基类"""
|
||
|
|
title: str = field()
|
||
|
|
description: str = field()
|
||
|
|
tags: List[str] = field(default_factory=list)
|
||
|
|
visibility: str = "public" # public, private, friends
|
||
|
|
location: Optional[str] = None
|
||
|
|
scheduled_time: Optional[datetime] = None
|
||
|
|
extra_data: Dict[str, Any] = field(default_factory=dict)
|
||
|
|
|
||
|
|
def __post_init__(self):
|
||
|
|
"""后处理,清理数据"""
|
||
|
|
self.title = self.title.strip()
|
||
|
|
self.description = self.description.strip()
|
||
|
|
self.tags = [tag.strip() for tag in self.tags if tag.strip()]
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class ImageNote(Content):
|
||
|
|
"""图文笔记内容"""
|
||
|
|
images: List[str] = field(default_factory=list)
|
||
|
|
cover_image: Optional[str] = None
|
||
|
|
image_captions: List[str] = field(default_factory=list) # 图片说明
|
||
|
|
|
||
|
|
def __post_init__(self):
|
||
|
|
super().__post_init__()
|
||
|
|
# 验证图片文件
|
||
|
|
self.images = [str(Path(img)) for img in self.images if Path(img).exists()]
|
||
|
|
if self.cover_image and not Path(self.cover_image).exists():
|
||
|
|
self.cover_image = None
|
||
|
|
# 如果没有指定封面,使用第一张图片
|
||
|
|
if not self.cover_image and self.images:
|
||
|
|
self.cover_image = self.images[0]
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class VideoContent(Content):
|
||
|
|
"""视频内容"""
|
||
|
|
video_path: str
|
||
|
|
cover_image: Optional[str] = None
|
||
|
|
duration: Optional[int] = None # 秒
|
||
|
|
resolution: Optional[tuple] = None # (width, height)
|
||
|
|
file_size: Optional[int] = None # bytes
|
||
|
|
|
||
|
|
def __post_init__(self):
|
||
|
|
super().__post_init__()
|
||
|
|
video_path = Path(self.video_path)
|
||
|
|
if video_path.exists():
|
||
|
|
self.video_path = str(video_path)
|
||
|
|
else:
|
||
|
|
raise FileNotFoundError(f"视频文件不存在: {self.video_path}")
|
||
|
|
|
||
|
|
if self.cover_image and not Path(self.cover_image).exists():
|
||
|
|
self.cover_image = None
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class PublishTask:
|
||
|
|
"""发布任务"""
|
||
|
|
id: str = field()
|
||
|
|
platform: PlatformType = field()
|
||
|
|
account: AccountInfo = field()
|
||
|
|
content: Content = field()
|
||
|
|
scheduled_time: Optional[datetime] = None
|
||
|
|
retry_count: int = 0
|
||
|
|
max_retries: int = 3
|
||
|
|
created_at: datetime = field(default_factory=datetime.now)
|
||
|
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
||
|
|
|
||
|
|
def __post_init__(self):
|
||
|
|
"""生成任务ID如果没有提供"""
|
||
|
|
if not self.id:
|
||
|
|
self.id = f"{self.platform.value}_{self.account.username}_{int(self.created_at.timestamp())}"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def should_retry(self) -> bool:
|
||
|
|
"""是否应该重试"""
|
||
|
|
return self.retry_count < self.max_retries
|
||
|
|
|
||
|
|
def increment_retry(self):
|
||
|
|
"""增加重试次数"""
|
||
|
|
self.retry_count += 1
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class PublishResult:
|
||
|
|
"""发布结果"""
|
||
|
|
task_id: str = field()
|
||
|
|
platform: PlatformType = field()
|
||
|
|
account: str = field()
|
||
|
|
success: bool = field()
|
||
|
|
message: str = field()
|
||
|
|
published_url: Optional[str] = None
|
||
|
|
upload_status: UploadStatus = UploadStatus.PENDING
|
||
|
|
publish_status: PublishStatus = PublishStatus.DRAFT
|
||
|
|
timestamp: datetime = field(default_factory=datetime.now)
|
||
|
|
error_details: Optional[Dict[str, Any]] = None
|
||
|
|
duration: Optional[float] = None # 执行耗时(秒)
|
||
|
|
|
||
|
|
def to_dict(self) -> Dict[str, Any]:
|
||
|
|
"""转换为字典格式"""
|
||
|
|
return {
|
||
|
|
"task_id": self.task_id,
|
||
|
|
"platform": self.platform.value,
|
||
|
|
"account": self.account,
|
||
|
|
"success": self.success,
|
||
|
|
"message": self.message,
|
||
|
|
"published_url": self.published_url,
|
||
|
|
"upload_status": self.upload_status.value,
|
||
|
|
"publish_status": self.publish_status.value,
|
||
|
|
"timestamp": self.timestamp.isoformat(),
|
||
|
|
"error_details": self.error_details,
|
||
|
|
"duration": self.duration
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class PlatformConfig:
|
||
|
|
"""平台配置信息"""
|
||
|
|
name: str
|
||
|
|
login_url: str
|
||
|
|
supported_content_types: List[str]
|
||
|
|
max_file_size: int # bytes
|
||
|
|
max_duration: Optional[int] = None # 秒
|
||
|
|
supported_formats: List[str] = field(default_factory=list)
|
||
|
|
extra_config: Dict[str, Any] = field(default_factory=dict)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def from_dict(cls, config_dict: Dict[str, Any]) -> 'PlatformConfig':
|
||
|
|
"""从字典创建配置"""
|
||
|
|
return cls(
|
||
|
|
name=config_dict.get("name", ""),
|
||
|
|
login_url=config_dict.get("login_url", ""),
|
||
|
|
supported_content_types=config_dict.get("supported_content_types", []),
|
||
|
|
max_file_size=config_dict.get("max_file_size", 0),
|
||
|
|
max_duration=config_dict.get("max_duration"),
|
||
|
|
supported_formats=config_dict.get("supported_formats", []),
|
||
|
|
extra_config=config_dict.get("extra_config", {})
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
# 常用的平台配置模板
|
||
|
|
XIAOHONGSHU_CONFIG = PlatformConfig(
|
||
|
|
name="小红书",
|
||
|
|
login_url="https://creator.xiaohongshu.com/",
|
||
|
|
supported_content_types=["image_note", "video_note"],
|
||
|
|
max_file_size=500 * 1024 * 1024, # 500MB
|
||
|
|
max_duration=300, # 5分钟
|
||
|
|
supported_formats=["jpg", "jpeg", "png", "mp4", "mov"],
|
||
|
|
extra_config={
|
||
|
|
"max_images": 9,
|
||
|
|
"image_note_url": "https://creator.xiaohongshu.com/publish/publish",
|
||
|
|
"video_note_url": "https://creator.xiaohongshu.com/publish/video",
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
DOUYIN_CONFIG = PlatformConfig(
|
||
|
|
name="抖音",
|
||
|
|
login_url="https://creator.douyin.com/creator-micro/home",
|
||
|
|
supported_content_types=["video"],
|
||
|
|
max_file_size=2 * 1024 * 1024 * 1024, # 2GB
|
||
|
|
max_duration=600, # 10分钟
|
||
|
|
supported_formats=["mp4", "mov", "avi"],
|
||
|
|
extra_config={
|
||
|
|
"upload_url": "https://creator.douyin.com/creator-micro/content/upload",
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
# 平台配置映射
|
||
|
|
PLATFORM_CONFIGS = {
|
||
|
|
PlatformType.XIAOHONGSHU: XIAOHONGSHU_CONFIG,
|
||
|
|
PlatformType.DOUYIN: DOUYIN_CONFIG
|
||
|
|
}
|