app.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 多平台视频发布服务 - 统一入口
  5. 支持平台: 抖音、小红书、视频号、快手
  6. 参考项目: matrix (https://github.com/kebenxiaoming/matrix)
  7. 使用方式:
  8. python app.py # 启动 HTTP 服务 (端口 5005)
  9. python app.py --port 8080 # 指定端口
  10. python app.py --headless false # 显示浏览器窗口
  11. """
  12. import asyncio
  13. import os
  14. import sys
  15. import argparse
  16. import traceback
  17. from datetime import datetime
  18. from pathlib import Path
  19. # 确保当前目录在 Python 路径中
  20. CURRENT_DIR = Path(__file__).parent.resolve()
  21. if str(CURRENT_DIR) not in sys.path:
  22. sys.path.insert(0, str(CURRENT_DIR))
  23. from flask import Flask, request, jsonify
  24. from flask_cors import CORS
  25. from platforms import get_publisher, PLATFORM_MAP
  26. from platforms.base import PublishParams
  27. def parse_datetime(date_str: str):
  28. """解析日期时间字符串"""
  29. if not date_str:
  30. return None
  31. formats = [
  32. "%Y-%m-%d %H:%M:%S",
  33. "%Y-%m-%d %H:%M",
  34. "%Y/%m/%d %H:%M:%S",
  35. "%Y/%m/%d %H:%M",
  36. "%Y-%m-%dT%H:%M:%S",
  37. "%Y-%m-%dT%H:%M:%SZ",
  38. ]
  39. for fmt in formats:
  40. try:
  41. return datetime.strptime(date_str, fmt)
  42. except ValueError:
  43. continue
  44. return None
  45. def validate_video_file(video_path: str) -> bool:
  46. """验证视频文件是否有效"""
  47. if not video_path:
  48. return False
  49. if not os.path.exists(video_path):
  50. return False
  51. if not os.path.isfile(video_path):
  52. return False
  53. valid_extensions = ['.mp4', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.webm']
  54. ext = os.path.splitext(video_path)[1].lower()
  55. if ext not in valid_extensions:
  56. return False
  57. if os.path.getsize(video_path) < 1024:
  58. return False
  59. return True
  60. # 创建 Flask 应用
  61. app = Flask(__name__)
  62. CORS(app)
  63. # 全局配置
  64. HEADLESS_MODE = os.environ.get('HEADLESS', 'true').lower() == 'true'
  65. # ==================== 签名相关(小红书专用) ====================
  66. @app.route("/sign", methods=["POST"])
  67. def sign_endpoint():
  68. """小红书签名接口"""
  69. try:
  70. from platforms.xiaohongshu import XiaohongshuPublisher
  71. data = request.json
  72. publisher = XiaohongshuPublisher(headless=True)
  73. result = asyncio.run(publisher.get_sign(
  74. data.get("uri", ""),
  75. data.get("data"),
  76. data.get("a1", ""),
  77. data.get("web_session", "")
  78. ))
  79. return jsonify(result)
  80. except Exception as e:
  81. traceback.print_exc()
  82. return jsonify({"error": str(e)}), 500
  83. # ==================== 统一发布接口 ====================
  84. @app.route("/publish", methods=["POST"])
  85. def publish_video():
  86. """
  87. 统一发布接口
  88. 请求体:
  89. {
  90. "platform": "douyin", # douyin | xiaohongshu | weixin | kuaishou
  91. "cookie": "cookie字符串或JSON",
  92. "title": "视频标题",
  93. "description": "视频描述(可选)",
  94. "video_path": "视频文件绝对路径",
  95. "cover_path": "封面图片绝对路径(可选)",
  96. "tags": ["话题1", "话题2"],
  97. "post_time": "定时发布时间(可选,格式:2024-01-20 12:00:00)",
  98. "location": "位置(可选,默认:重庆市)"
  99. }
  100. 响应:
  101. {
  102. "success": true,
  103. "platform": "douyin",
  104. "video_id": "xxx",
  105. "video_url": "xxx",
  106. "message": "发布成功"
  107. }
  108. """
  109. try:
  110. data = request.json
  111. # 获取参数
  112. platform = data.get("platform", "").lower()
  113. cookie_str = data.get("cookie", "")
  114. title = data.get("title", "")
  115. description = data.get("description", "")
  116. video_path = data.get("video_path", "")
  117. cover_path = data.get("cover_path")
  118. tags = data.get("tags", [])
  119. post_time = data.get("post_time")
  120. location = data.get("location", "重庆市")
  121. # 调试日志
  122. print(f"[Publish] 收到请求: platform={platform}, title={title}, video_path={video_path}")
  123. # 参数验证
  124. if not platform:
  125. print("[Publish] 错误: 缺少 platform 参数")
  126. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  127. if platform not in PLATFORM_MAP:
  128. print(f"[Publish] 错误: 不支持的平台 {platform}")
  129. return jsonify({
  130. "success": False,
  131. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  132. }), 400
  133. if not cookie_str:
  134. print("[Publish] 错误: 缺少 cookie 参数")
  135. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  136. if not title:
  137. print("[Publish] 错误: 缺少 title 参数")
  138. return jsonify({"success": False, "error": "缺少 title 参数"}), 400
  139. if not video_path:
  140. print("[Publish] 错误: 缺少 video_path 参数")
  141. return jsonify({"success": False, "error": "缺少 video_path 参数"}), 400
  142. # 视频文件验证(增加详细信息)
  143. if not os.path.exists(video_path):
  144. print(f"[Publish] 错误: 视频文件不存在: {video_path}")
  145. return jsonify({"success": False, "error": f"视频文件不存在: {video_path}"}), 400
  146. if not os.path.isfile(video_path):
  147. print(f"[Publish] 错误: 路径不是文件: {video_path}")
  148. return jsonify({"success": False, "error": f"路径不是文件: {video_path}"}), 400
  149. # 解析发布时间
  150. publish_date = parse_datetime(post_time) if post_time else None
  151. # 创建发布参数
  152. params = PublishParams(
  153. title=title,
  154. video_path=video_path,
  155. description=description,
  156. cover_path=cover_path,
  157. tags=tags,
  158. publish_date=publish_date,
  159. location=location
  160. )
  161. print("=" * 60)
  162. print(f"[Publish] 平台: {platform}")
  163. print(f"[Publish] 标题: {title}")
  164. print(f"[Publish] 视频: {video_path}")
  165. print(f"[Publish] 封面: {cover_path}")
  166. print(f"[Publish] 话题: {tags}")
  167. print(f"[Publish] 定时: {publish_date}")
  168. print("=" * 60)
  169. # 获取对应平台的发布器
  170. PublisherClass = get_publisher(platform)
  171. publisher = PublisherClass(headless=HEADLESS_MODE)
  172. # 执行发布
  173. result = asyncio.run(publisher.run(cookie_str, params))
  174. return jsonify({
  175. "success": result.success,
  176. "platform": result.platform,
  177. "video_id": result.video_id,
  178. "video_url": result.video_url,
  179. "message": result.message,
  180. "error": result.error
  181. })
  182. except Exception as e:
  183. traceback.print_exc()
  184. return jsonify({"success": False, "error": str(e)}), 500
  185. # ==================== 批量发布接口 ====================
  186. @app.route("/publish/batch", methods=["POST"])
  187. def publish_batch():
  188. """
  189. 批量发布接口 - 发布到多个平台
  190. 请求体:
  191. {
  192. "platforms": ["douyin", "xiaohongshu"],
  193. "cookies": {
  194. "douyin": "cookie字符串",
  195. "xiaohongshu": "cookie字符串"
  196. },
  197. "title": "视频标题",
  198. "video_path": "视频文件绝对路径",
  199. ...
  200. }
  201. """
  202. try:
  203. data = request.json
  204. platforms = data.get("platforms", [])
  205. cookies = data.get("cookies", {})
  206. if not platforms:
  207. return jsonify({"success": False, "error": "缺少 platforms 参数"}), 400
  208. results = []
  209. for platform in platforms:
  210. platform = platform.lower()
  211. cookie_str = cookies.get(platform, "")
  212. if not cookie_str:
  213. results.append({
  214. "platform": platform,
  215. "success": False,
  216. "error": f"缺少 {platform} 的 cookie"
  217. })
  218. continue
  219. try:
  220. # 创建参数
  221. params = PublishParams(
  222. title=data.get("title", ""),
  223. video_path=data.get("video_path", ""),
  224. description=data.get("description", ""),
  225. cover_path=data.get("cover_path"),
  226. tags=data.get("tags", []),
  227. publish_date=parse_datetime(data.get("post_time")),
  228. location=data.get("location", "重庆市")
  229. )
  230. # 发布
  231. PublisherClass = get_publisher(platform)
  232. publisher = PublisherClass(headless=HEADLESS_MODE)
  233. result = asyncio.run(publisher.run(cookie_str, params))
  234. results.append({
  235. "platform": result.platform,
  236. "success": result.success,
  237. "video_id": result.video_id,
  238. "message": result.message,
  239. "error": result.error
  240. })
  241. except Exception as e:
  242. results.append({
  243. "platform": platform,
  244. "success": False,
  245. "error": str(e)
  246. })
  247. # 统计成功/失败数量
  248. success_count = sum(1 for r in results if r.get("success"))
  249. return jsonify({
  250. "success": success_count > 0,
  251. "total": len(platforms),
  252. "success_count": success_count,
  253. "fail_count": len(platforms) - success_count,
  254. "results": results
  255. })
  256. except Exception as e:
  257. traceback.print_exc()
  258. return jsonify({"success": False, "error": str(e)}), 500
  259. # ==================== Cookie 验证接口 ====================
  260. @app.route("/check_cookie", methods=["POST"])
  261. def check_cookie():
  262. """检查 cookie 是否有效"""
  263. try:
  264. data = request.json
  265. platform = data.get("platform", "").lower()
  266. cookie_str = data.get("cookie", "")
  267. if not cookie_str:
  268. return jsonify({"valid": False, "error": "缺少 cookie 参数"}), 400
  269. # 目前只支持小红书的 cookie 验证
  270. if platform == "xiaohongshu":
  271. try:
  272. from platforms.xiaohongshu import XiaohongshuPublisher, XHS_SDK_AVAILABLE
  273. if XHS_SDK_AVAILABLE:
  274. from xhs import XhsClient
  275. publisher = XiaohongshuPublisher()
  276. xhs_client = XhsClient(cookie_str, sign=publisher.sign_sync)
  277. info = xhs_client.get_self_info()
  278. if info:
  279. return jsonify({
  280. "valid": True,
  281. "user_info": {
  282. "user_id": info.get("user_id"),
  283. "nickname": info.get("nickname"),
  284. "avatar": info.get("images")
  285. }
  286. })
  287. except Exception as e:
  288. return jsonify({"valid": False, "error": str(e)})
  289. # 其他平台返回格式正确但未验证
  290. return jsonify({
  291. "valid": True,
  292. "message": "Cookie 格式正确,但未进行在线验证"
  293. })
  294. except Exception as e:
  295. traceback.print_exc()
  296. return jsonify({"valid": False, "error": str(e)})
  297. # ==================== 健康检查 ====================
  298. @app.route("/health", methods=["GET"])
  299. def health_check():
  300. """健康检查"""
  301. # 检查 xhs SDK 是否可用
  302. xhs_available = False
  303. try:
  304. from platforms.xiaohongshu import XHS_SDK_AVAILABLE
  305. xhs_available = XHS_SDK_AVAILABLE
  306. except:
  307. pass
  308. return jsonify({
  309. "status": "ok",
  310. "xhs_sdk": xhs_available,
  311. "supported_platforms": list(PLATFORM_MAP.keys()),
  312. "headless_mode": HEADLESS_MODE
  313. })
  314. @app.route("/", methods=["GET"])
  315. def index():
  316. """首页"""
  317. return jsonify({
  318. "name": "多平台视频发布服务",
  319. "version": "1.0.0",
  320. "endpoints": {
  321. "GET /": "服务信息",
  322. "GET /health": "健康检查",
  323. "POST /publish": "发布视频",
  324. "POST /publish/batch": "批量发布",
  325. "POST /check_cookie": "检查 Cookie",
  326. "POST /sign": "小红书签名"
  327. },
  328. "supported_platforms": list(PLATFORM_MAP.keys())
  329. })
  330. # ==================== 命令行启动 ====================
  331. def main():
  332. parser = argparse.ArgumentParser(description='多平台视频发布服务')
  333. parser.add_argument('--port', type=int, default=5005, help='服务端口 (默认: 5005)')
  334. parser.add_argument('--host', type=str, default='0.0.0.0', help='监听地址 (默认: 0.0.0.0)')
  335. parser.add_argument('--headless', type=str, default='true', help='是否无头模式 (默认: true)')
  336. parser.add_argument('--debug', action='store_true', help='调试模式')
  337. args = parser.parse_args()
  338. global HEADLESS_MODE
  339. HEADLESS_MODE = args.headless.lower() == 'true'
  340. # 检查 xhs SDK
  341. xhs_status = "未安装"
  342. try:
  343. from platforms.xiaohongshu import XHS_SDK_AVAILABLE
  344. xhs_status = "已安装" if XHS_SDK_AVAILABLE else "未安装"
  345. except:
  346. pass
  347. print("=" * 60)
  348. print("多平台视频发布服务")
  349. print("=" * 60)
  350. print(f"XHS SDK: {xhs_status}")
  351. print(f"Headless 模式: {HEADLESS_MODE}")
  352. print(f"支持平台: {', '.join(PLATFORM_MAP.keys())}")
  353. print("=" * 60)
  354. print(f"启动服务: http://{args.host}:{args.port}")
  355. print("=" * 60)
  356. app.run(host=args.host, port=args.port, debug=args.debug, threaded=True)
  357. if __name__ == '__main__':
  358. main()