313 lines
12 KiB
Python
313 lines
12 KiB
Python
"""
|
||
发布服务 (支持用户隔离)
|
||
"""
|
||
import json
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Optional, List, Dict, Any
|
||
from loguru import logger
|
||
from app.core.config import settings
|
||
from app.core.paths import get_user_cookie_dir, get_platform_cookie_path, get_legacy_cookie_dir, get_legacy_cookie_path
|
||
|
||
# Import platform uploaders
|
||
from .uploader.bilibili_uploader import BilibiliUploader
|
||
from .uploader.douyin_uploader import DouyinUploader
|
||
from .uploader.xiaohongshu_uploader import XiaohongshuUploader
|
||
|
||
|
||
class PublishService:
|
||
"""Social media publishing service (with user isolation)"""
|
||
|
||
# 支持的平台配置
|
||
PLATFORMS: Dict[str, Dict[str, Any]] = {
|
||
"bilibili": {"name": "B站", "url": "https://member.bilibili.com/platform/upload/video/frame", "enabled": True},
|
||
"douyin": {"name": "抖音", "url": "https://creator.douyin.com/", "enabled": True},
|
||
"xiaohongshu": {"name": "小红书", "url": "https://creator.xiaohongshu.com/", "enabled": True},
|
||
"weixin": {"name": "微信视频号", "url": "https://channels.weixin.qq.com/", "enabled": False},
|
||
"kuaishou": {"name": "快手", "url": "https://cp.kuaishou.com/", "enabled": False},
|
||
}
|
||
|
||
def __init__(self) -> None:
|
||
# 存储活跃的登录会话,用于跟踪登录状态
|
||
# key 格式: "{user_id}_{platform}" 或 "{platform}" (兼容旧版)
|
||
self.active_login_sessions: Dict[str, Any] = {}
|
||
|
||
def _get_cookies_dir(self, user_id: Optional[str] = None) -> Path:
|
||
"""获取 Cookie 目录 (支持用户隔离)"""
|
||
if user_id:
|
||
return get_user_cookie_dir(user_id)
|
||
return get_legacy_cookie_dir()
|
||
|
||
def _get_cookie_path(self, platform: str, user_id: Optional[str] = None) -> Path:
|
||
"""获取 Cookie 文件路径 (支持用户隔离)"""
|
||
if user_id:
|
||
return get_platform_cookie_path(user_id, platform)
|
||
return get_legacy_cookie_path(platform)
|
||
|
||
def _get_session_key(self, platform: str, user_id: Optional[str] = None) -> str:
|
||
"""获取会话 key"""
|
||
if user_id:
|
||
return f"{user_id}_{platform}"
|
||
return platform
|
||
|
||
def get_accounts(self, user_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
||
"""Get list of platform accounts with login status"""
|
||
accounts = []
|
||
for pid, pinfo in self.PLATFORMS.items():
|
||
cookie_file = self._get_cookie_path(pid, user_id)
|
||
accounts.append({
|
||
"platform": pid,
|
||
"name": pinfo["name"],
|
||
"logged_in": cookie_file.exists(),
|
||
"enabled": pinfo.get("enabled", True)
|
||
})
|
||
return accounts
|
||
|
||
async def publish(
|
||
self,
|
||
video_path: str,
|
||
platform: str,
|
||
title: str,
|
||
tags: List[str],
|
||
description: str = "",
|
||
publish_time: Optional[datetime] = None,
|
||
user_id: Optional[str] = None,
|
||
**kwargs: Any
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
Publish video to specified platform
|
||
|
||
Args:
|
||
video_path: Path to video file
|
||
platform: Platform ID (bilibili, douyin, etc.)
|
||
title: Video title
|
||
tags: List of tags
|
||
description: Video description
|
||
publish_time: Scheduled publish time (None = immediate)
|
||
user_id: User ID for cookie isolation
|
||
**kwargs: Additional platform-specific parameters
|
||
|
||
Returns:
|
||
dict: Publish result
|
||
"""
|
||
# Validate platform
|
||
if platform not in self.PLATFORMS:
|
||
logger.error(f"[发布] 不支持的平台: {platform}")
|
||
return {
|
||
"success": False,
|
||
"message": f"不支持的平台: {platform}",
|
||
"platform": platform
|
||
}
|
||
|
||
# Get account file path (with user isolation)
|
||
account_file = self._get_cookie_path(platform, user_id)
|
||
|
||
if not account_file.exists():
|
||
return {
|
||
"success": False,
|
||
"message": f"请先登录 {self.PLATFORMS[platform]['name']}",
|
||
"platform": platform
|
||
}
|
||
|
||
logger.info(f"[发布] 平台: {self.PLATFORMS[platform]['name']}")
|
||
logger.info(f"[发布] 视频: {video_path}")
|
||
logger.info(f"[发布] 标题: {title}")
|
||
logger.info(f"[发布] 用户: {user_id or 'legacy'}")
|
||
|
||
try:
|
||
# Select appropriate uploader
|
||
if platform == "bilibili":
|
||
uploader = BilibiliUploader(
|
||
title=title,
|
||
file_path=str(settings.BASE_DIR.parent / video_path),
|
||
tags=tags,
|
||
publish_date=publish_time,
|
||
account_file=str(account_file),
|
||
description=description,
|
||
tid=kwargs.get('tid', 122),
|
||
copyright=kwargs.get('copyright', 1)
|
||
)
|
||
elif platform == "douyin":
|
||
uploader = DouyinUploader(
|
||
title=title,
|
||
file_path=str(settings.BASE_DIR.parent / video_path),
|
||
tags=tags,
|
||
publish_date=publish_time,
|
||
account_file=str(account_file),
|
||
description=description
|
||
)
|
||
elif platform == "xiaohongshu":
|
||
uploader = XiaohongshuUploader(
|
||
title=title,
|
||
file_path=str(settings.BASE_DIR.parent / video_path),
|
||
tags=tags,
|
||
publish_date=publish_time,
|
||
account_file=str(account_file),
|
||
description=description
|
||
)
|
||
else:
|
||
logger.warning(f"[发布] {platform} 上传功能尚未实现")
|
||
return {
|
||
"success": False,
|
||
"message": f"{self.PLATFORMS[platform]['name']} 上传功能开发中",
|
||
"platform": platform
|
||
}
|
||
|
||
# Execute upload
|
||
result = await uploader.main()
|
||
result['platform'] = platform
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.exception(f"[发布] 上传异常: {e}")
|
||
return {
|
||
"success": False,
|
||
"message": f"上传异常: {str(e)}",
|
||
"platform": platform
|
||
}
|
||
|
||
async def login(self, platform: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
||
"""
|
||
启动QR码登录流程
|
||
|
||
Args:
|
||
platform: 平台 ID
|
||
user_id: 用户 ID (用于 Cookie 隔离)
|
||
|
||
Returns:
|
||
dict: 包含二维码base64图片
|
||
"""
|
||
if platform not in self.PLATFORMS:
|
||
return {"success": False, "message": "不支持的平台"}
|
||
|
||
try:
|
||
from .qr_login_service import QRLoginService
|
||
|
||
# 获取用户专属的 Cookie 目录
|
||
cookies_dir = self._get_cookies_dir(user_id)
|
||
|
||
# 创建QR登录服务
|
||
qr_service = QRLoginService(platform, cookies_dir)
|
||
|
||
# 存储活跃会话 (带用户隔离)
|
||
session_key = self._get_session_key(platform, user_id)
|
||
self.active_login_sessions[session_key] = qr_service
|
||
|
||
# 启动登录并获取二维码
|
||
result = await qr_service.start_login()
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.exception(f"[登录] QR码登录失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"message": f"登录失败: {str(e)}"
|
||
}
|
||
|
||
def get_login_session_status(self, platform: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
||
"""获取活跃登录会话的状态"""
|
||
session_key = self._get_session_key(platform, user_id)
|
||
|
||
# 1. 如果有活跃的扫码会话,优先检查它
|
||
if session_key in self.active_login_sessions:
|
||
qr_service = self.active_login_sessions[session_key]
|
||
status = qr_service.get_login_status()
|
||
|
||
# 如果登录成功且Cookie已保存,清理会话
|
||
if status["success"] and status["cookies_saved"]:
|
||
del self.active_login_sessions[session_key]
|
||
return {"success": True, "message": "登录成功"}
|
||
|
||
return {"success": False, "message": "等待扫码..."}
|
||
|
||
# 2. 检查本地Cookie文件是否存在
|
||
cookie_file = self._get_cookie_path(platform, user_id)
|
||
if cookie_file.exists():
|
||
return {"success": True, "message": "已登录 (历史状态)"}
|
||
|
||
return {"success": False, "message": "未登录"}
|
||
|
||
def logout(self, platform: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
||
"""
|
||
Logout from platform (delete cookie file)
|
||
"""
|
||
if platform not in self.PLATFORMS:
|
||
return {"success": False, "message": "不支持的平台"}
|
||
|
||
try:
|
||
session_key = self._get_session_key(platform, user_id)
|
||
|
||
# 1. 移除活跃会话
|
||
if session_key in self.active_login_sessions:
|
||
del self.active_login_sessions[session_key]
|
||
|
||
# 2. 删除Cookie文件
|
||
cookie_file = self._get_cookie_path(platform, user_id)
|
||
if cookie_file.exists():
|
||
cookie_file.unlink()
|
||
logger.info(f"[登出] {platform} Cookie已删除 (user: {user_id or 'legacy'})")
|
||
|
||
return {"success": True, "message": "已注销"}
|
||
|
||
except Exception as e:
|
||
logger.exception(f"[登出] 失败: {e}")
|
||
return {"success": False, "message": f"注销失败: {str(e)}"}
|
||
|
||
async def save_cookie_string(self, platform: str, cookie_string: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
||
"""
|
||
保存从客户端浏览器提取的Cookie字符串
|
||
|
||
Args:
|
||
platform: 平台ID
|
||
cookie_string: document.cookie 格式的Cookie字符串
|
||
user_id: 用户 ID (用于 Cookie 隔离)
|
||
"""
|
||
try:
|
||
account_file = self._get_cookie_path(platform, user_id)
|
||
|
||
# 解析Cookie字符串
|
||
cookie_dict = {}
|
||
for item in cookie_string.split('; '):
|
||
if '=' in item:
|
||
name, value = item.split('=', 1)
|
||
cookie_dict[name] = value
|
||
|
||
# 对B站进行特殊处理
|
||
if platform == "bilibili":
|
||
bilibili_cookies = {}
|
||
required_fields = ['SESSDATA', 'bili_jct', 'DedeUserID', 'DedeUserID__ckMd5']
|
||
|
||
for field in required_fields:
|
||
if field in cookie_dict:
|
||
bilibili_cookies[field] = cookie_dict[field]
|
||
|
||
if len(bilibili_cookies) < 3:
|
||
return {
|
||
"success": False,
|
||
"message": "Cookie不完整,请确保已登录"
|
||
}
|
||
|
||
cookie_dict = bilibili_cookies
|
||
|
||
# 确保目录存在
|
||
account_file.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 保存Cookie
|
||
with open(account_file, 'w', encoding='utf-8') as f:
|
||
json.dump(cookie_dict, f, indent=2)
|
||
|
||
logger.success(f"[登录] {platform} Cookie已保存 (user: {user_id or 'legacy'})")
|
||
|
||
return {
|
||
"success": True,
|
||
"message": f"{self.PLATFORMS[platform]['name']} 登录成功"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.exception(f"[登录] Cookie保存失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"message": f"Cookie保存失败: {str(e)}"
|
||
}
|