Files
tw/services/service_tuhui_upload.py

216 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
图绘平台上传服务
将处理好的图片上传到图绘平台,返回图片 URL
"""
import os
import httpx
import logging
import mimetypes
from pathlib import Path
from typing import Optional, Tuple
from dotenv import load_dotenv
logger = logging.getLogger(__name__)
load_dotenv()
# 图绘平台配置
TUHUI_BASE_URL = os.getenv("TUHUI_BASE_URL", "https://tuhui.cloud")
TUHUI_FALLBACK_BASE_URL = "https://tuhui.cloud"
TUHUI_PHONE = os.getenv("TUHUI_PHONE", "17520145271") # 图绘账号手机号
TUHUI_PASSWORD = os.getenv("TUHUI_PASSWORD", "zuowei1216") # 图绘账号密码
TUHUI_DEFAULT_PRICE = int(os.getenv("TUHUI_DEFAULT_PRICE", "20")) # 默认定价(元)
TUHUI_DEFAULT_CATEGORY = os.getenv("TUHUI_DEFAULT_CATEGORY", "设计素材")
class TuhuiUploadService:
"""图绘平台上传服务"""
def __init__(self):
self.base_url = TUHUI_BASE_URL.rstrip("/")
self.base_urls = []
for candidate in (TUHUI_FALLBACK_BASE_URL.rstrip("/"), self.base_url):
if candidate and candidate not in self.base_urls:
self.base_urls.append(candidate)
if self.base_urls:
self.base_url = self.base_urls[0]
self.phone = TUHUI_PHONE
self.password = TUHUI_PASSWORD
self.default_price = TUHUI_DEFAULT_PRICE
self.access_token = None
self.user_id = None
@staticmethod
def _build_api_url(base_url: str, path: str) -> str:
normalized = path if path.startswith("/") else f"/{path}"
if base_url.endswith("/api"):
return f"{base_url}{normalized}"
return f"{base_url}/api{normalized}"
def _api_url(self, path: str) -> str:
return self._build_api_url(self.base_url, path)
@staticmethod
def _guess_file_meta(image_path: str) -> tuple[str, str]:
path = Path(image_path)
filename = path.name or "image.jpg"
mime_type, _ = mimetypes.guess_type(filename)
return filename, mime_type or "application/octet-stream"
async def login(self) -> bool:
"""登录图绘平台获取 token"""
last_error = ""
for base_url in self.base_urls:
try:
async with httpx.AsyncClient() as client:
response = await client.post(
self._build_api_url(base_url, "/auth/login"),
json={
"phone": self.phone,
"password": self.password
},
timeout=10.0
)
if response.status_code == 200:
data = response.json()
self.access_token = data.get("access_token")
user = data.get("user", {})
self.user_id = user.get("id")
self.base_url = base_url
logger.info(f"图绘平台登录成功,用户 ID: {self.user_id}base={self.base_url}")
return True
last_error = f"{response.status_code} {response.text}"
logger.warning(f"图绘平台登录失败base={base_url}{last_error}")
except Exception as e:
last_error = str(e)
logger.warning(f"图绘平台登录异常base={base_url}{type(e).__name__}: {e!r}")
logger.error(f"图绘平台登录失败:{last_error}")
return False
async def upload_image(
self,
image_path: str,
title: str,
description: str = "",
price: Optional[int] = None,
category: str = TUHUI_DEFAULT_CATEGORY,
tags: str = "",
) -> Tuple[bool, str, int]:
"""
上传图片到图绘平台
Args:
image_path: 图片文件路径
title: 作品标题
description: 作品描述
price: 定价(元),默认使用 TUHUI_DEFAULT_PRICE
category: 分类
Returns:
(success, image_url, work_id)
- success: 是否上传成功
- image_url: 图片 URL
- work_id: 作品 ID
"""
try:
# 如果 token 过期,重新登录
if not self.access_token:
if not await self.login():
return False, "登录失败", 0
# 准备上传数据
price = price or self.default_price
# 读取图片文件
if not os.path.exists(image_path):
logger.error(f"图片文件不存在:{image_path}")
return False, "文件不存在", 0
filename, mime_type = self._guess_file_meta(image_path)
with open(image_path, "rb") as f:
files = {
"file": (filename, f, mime_type)
}
data = {
"title": title,
"description": description,
"price": str(price),
"category": category,
}
if tags:
data["tags"] = tags
headers = {
"Authorization": f"Bearer {self.access_token}"
}
async with httpx.AsyncClient() as client:
response = await client.post(
self._api_url("/upload"),
files=files,
data=data,
headers=headers,
timeout=30.0
)
if response.status_code in [200, 201]:
payload = response.json()
if not payload.get("success", False):
logger.error(f"图绘平台上传返回失败:{payload}")
return False, payload.get("message", "上传失败"), 0
work_id = int(payload.get("work_id") or payload.get("work", {}).get("id") or 0)
image_url = str(payload.get("image_url") or payload.get("work", {}).get("original_image") or "")
logger.info(f"图绘平台上传成功,作品 ID: {work_id}, URL: {image_url}")
return True, image_url, work_id
else:
logger.error(f"图绘平台上传失败:{response.status_code} {response.text}")
# 如果 token 过期,尝试重新登录后再上传
if response.status_code == 401:
logger.info("Token 可能过期,尝试重新登录...")
self.access_token = None
if await self.login():
# 重新上传
return await self.upload_image(
image_path, title, description, price, category
)
return False, f"上传失败:{response.text}", 0
except Exception as e:
logger.error(f"图绘平台上传异常:{e}")
return False, f"上传异常:{e}", 0
# 单例
_tuhui_service: Optional[TuhuiUploadService] = None
def get_tuhui_service() -> TuhuiUploadService:
"""获取图绘上传服务单例"""
global _tuhui_service
if _tuhui_service is None:
_tuhui_service = TuhuiUploadService()
return _tuhui_service
# 便捷函数
async def upload_to_tuhui(
image_path: str,
title: str,
description: str = "",
price: int = 20,
category: str = TUHUI_DEFAULT_CATEGORY,
tags: str = "",
) -> Tuple[bool, str, int]:
"""
便捷函数:上传图片到图绘平台
Returns:
(success, image_url, work_id)
"""
service = get_tuhui_service()
return await service.upload_image(image_path, title, description, price, category, tags)