app.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  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 argparse
  15. import traceback
  16. from datetime import datetime
  17. from flask import Flask, request, jsonify
  18. from flask_cors import CORS
  19. from platforms import get_publisher, PLATFORM_MAP
  20. from platforms.base import PublishParams
  21. from utils.helpers import parse_datetime, validate_video_file
  22. # 创建 Flask 应用
  23. app = Flask(__name__)
  24. CORS(app)
  25. # 全局配置
  26. HEADLESS_MODE = os.environ.get('HEADLESS', 'true').lower() == 'true'
  27. # ==================== 签名相关(小红书专用) ====================
  28. @app.route("/sign", methods=["POST"])
  29. def sign_endpoint():
  30. """小红书签名接口"""
  31. try:
  32. from platforms.xiaohongshu import XiaohongshuPublisher
  33. data = request.json
  34. publisher = XiaohongshuPublisher(headless=True)
  35. result = asyncio.run(publisher.get_sign(
  36. data.get("uri", ""),
  37. data.get("data"),
  38. data.get("a1", ""),
  39. data.get("web_session", "")
  40. ))
  41. return jsonify(result)
  42. except Exception as e:
  43. traceback.print_exc()
  44. return jsonify({"error": str(e)}), 500
  45. # ==================== 统一发布接口 ====================
  46. @app.route("/publish", methods=["POST"])
  47. def publish_video():
  48. """
  49. 统一发布接口
  50. 请求体:
  51. {
  52. "platform": "douyin", # douyin | xiaohongshu | weixin | kuaishou
  53. "cookie": "cookie字符串或JSON",
  54. "title": "视频标题",
  55. "description": "视频描述(可选)",
  56. "video_path": "视频文件绝对路径",
  57. "cover_path": "封面图片绝对路径(可选)",
  58. "tags": ["话题1", "话题2"],
  59. "post_time": "定时发布时间(可选,格式:2024-01-20 12:00:00)",
  60. "location": "位置(可选,默认:重庆市)"
  61. }
  62. 响应:
  63. {
  64. "success": true,
  65. "platform": "douyin",
  66. "video_id": "xxx",
  67. "video_url": "xxx",
  68. "message": "发布成功"
  69. }
  70. """
  71. try:
  72. data = request.json
  73. # 获取参数
  74. platform = data.get("platform", "").lower()
  75. cookie_str = data.get("cookie", "")
  76. title = data.get("title", "")
  77. description = data.get("description", "")
  78. video_path = data.get("video_path", "")
  79. cover_path = data.get("cover_path")
  80. tags = data.get("tags", [])
  81. post_time = data.get("post_time")
  82. location = data.get("location", "重庆市")
  83. # 参数验证
  84. if not platform:
  85. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  86. if platform not in PLATFORM_MAP:
  87. return jsonify({
  88. "success": False,
  89. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  90. }), 400
  91. if not cookie_str:
  92. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  93. if not title:
  94. return jsonify({"success": False, "error": "缺少 title 参数"}), 400
  95. if not video_path:
  96. return jsonify({"success": False, "error": "缺少 video_path 参数"}), 400
  97. if not validate_video_file(video_path):
  98. return jsonify({"success": False, "error": f"视频文件无效: {video_path}"}), 400
  99. # 解析发布时间
  100. publish_date = parse_datetime(post_time) if post_time else None
  101. # 创建发布参数
  102. params = PublishParams(
  103. title=title,
  104. video_path=video_path,
  105. description=description,
  106. cover_path=cover_path,
  107. tags=tags,
  108. publish_date=publish_date,
  109. location=location
  110. )
  111. print("=" * 60)
  112. print(f"[Publish] 平台: {platform}")
  113. print(f"[Publish] 标题: {title}")
  114. print(f"[Publish] 视频: {video_path}")
  115. print(f"[Publish] 封面: {cover_path}")
  116. print(f"[Publish] 话题: {tags}")
  117. print(f"[Publish] 定时: {publish_date}")
  118. print("=" * 60)
  119. # 获取对应平台的发布器
  120. PublisherClass = get_publisher(platform)
  121. publisher = PublisherClass(headless=HEADLESS_MODE)
  122. # 执行发布
  123. result = asyncio.run(publisher.run(cookie_str, params))
  124. return jsonify({
  125. "success": result.success,
  126. "platform": result.platform,
  127. "video_id": result.video_id,
  128. "video_url": result.video_url,
  129. "message": result.message,
  130. "error": result.error
  131. })
  132. except Exception as e:
  133. traceback.print_exc()
  134. return jsonify({"success": False, "error": str(e)}), 500
  135. # ==================== 批量发布接口 ====================
  136. @app.route("/publish/batch", methods=["POST"])
  137. def publish_batch():
  138. """
  139. 批量发布接口 - 发布到多个平台
  140. 请求体:
  141. {
  142. "platforms": ["douyin", "xiaohongshu"],
  143. "cookies": {
  144. "douyin": "cookie字符串",
  145. "xiaohongshu": "cookie字符串"
  146. },
  147. "title": "视频标题",
  148. "video_path": "视频文件绝对路径",
  149. ...
  150. }
  151. """
  152. try:
  153. data = request.json
  154. platforms = data.get("platforms", [])
  155. cookies = data.get("cookies", {})
  156. if not platforms:
  157. return jsonify({"success": False, "error": "缺少 platforms 参数"}), 400
  158. results = []
  159. for platform in platforms:
  160. platform = platform.lower()
  161. cookie_str = cookies.get(platform, "")
  162. if not cookie_str:
  163. results.append({
  164. "platform": platform,
  165. "success": False,
  166. "error": f"缺少 {platform} 的 cookie"
  167. })
  168. continue
  169. try:
  170. # 创建参数
  171. params = PublishParams(
  172. title=data.get("title", ""),
  173. video_path=data.get("video_path", ""),
  174. description=data.get("description", ""),
  175. cover_path=data.get("cover_path"),
  176. tags=data.get("tags", []),
  177. publish_date=parse_datetime(data.get("post_time")),
  178. location=data.get("location", "重庆市")
  179. )
  180. # 发布
  181. PublisherClass = get_publisher(platform)
  182. publisher = PublisherClass(headless=HEADLESS_MODE)
  183. result = asyncio.run(publisher.run(cookie_str, params))
  184. results.append({
  185. "platform": result.platform,
  186. "success": result.success,
  187. "video_id": result.video_id,
  188. "message": result.message,
  189. "error": result.error
  190. })
  191. except Exception as e:
  192. results.append({
  193. "platform": platform,
  194. "success": False,
  195. "error": str(e)
  196. })
  197. # 统计成功/失败数量
  198. success_count = sum(1 for r in results if r.get("success"))
  199. return jsonify({
  200. "success": success_count > 0,
  201. "total": len(platforms),
  202. "success_count": success_count,
  203. "fail_count": len(platforms) - success_count,
  204. "results": results
  205. })
  206. except Exception as e:
  207. traceback.print_exc()
  208. return jsonify({"success": False, "error": str(e)}), 500
  209. # ==================== Cookie 验证接口 ====================
  210. @app.route("/check_cookie", methods=["POST"])
  211. def check_cookie():
  212. """检查 cookie 是否有效"""
  213. try:
  214. data = request.json
  215. platform = data.get("platform", "").lower()
  216. cookie_str = data.get("cookie", "")
  217. if not cookie_str:
  218. return jsonify({"valid": False, "error": "缺少 cookie 参数"}), 400
  219. # 目前只支持小红书的 cookie 验证
  220. if platform == "xiaohongshu":
  221. try:
  222. from platforms.xiaohongshu import XiaohongshuPublisher, XHS_SDK_AVAILABLE
  223. if XHS_SDK_AVAILABLE:
  224. from xhs import XhsClient
  225. publisher = XiaohongshuPublisher()
  226. xhs_client = XhsClient(cookie_str, sign=publisher.sign_sync)
  227. info = xhs_client.get_self_info()
  228. if info:
  229. return jsonify({
  230. "valid": True,
  231. "user_info": {
  232. "user_id": info.get("user_id"),
  233. "nickname": info.get("nickname"),
  234. "avatar": info.get("images")
  235. }
  236. })
  237. except Exception as e:
  238. return jsonify({"valid": False, "error": str(e)})
  239. # 其他平台返回格式正确但未验证
  240. return jsonify({
  241. "valid": True,
  242. "message": "Cookie 格式正确,但未进行在线验证"
  243. })
  244. except Exception as e:
  245. traceback.print_exc()
  246. return jsonify({"valid": False, "error": str(e)})
  247. # ==================== 健康检查 ====================
  248. @app.route("/health", methods=["GET"])
  249. def health_check():
  250. """健康检查"""
  251. # 检查 xhs SDK 是否可用
  252. xhs_available = False
  253. try:
  254. from platforms.xiaohongshu import XHS_SDK_AVAILABLE
  255. xhs_available = XHS_SDK_AVAILABLE
  256. except:
  257. pass
  258. return jsonify({
  259. "status": "ok",
  260. "xhs_sdk": xhs_available,
  261. "supported_platforms": list(PLATFORM_MAP.keys()),
  262. "headless_mode": HEADLESS_MODE
  263. })
  264. @app.route("/", methods=["GET"])
  265. def index():
  266. """首页"""
  267. return jsonify({
  268. "name": "多平台视频发布服务",
  269. "version": "1.0.0",
  270. "endpoints": {
  271. "GET /": "服务信息",
  272. "GET /health": "健康检查",
  273. "POST /publish": "发布视频",
  274. "POST /publish/batch": "批量发布",
  275. "POST /check_cookie": "检查 Cookie",
  276. "POST /sign": "小红书签名"
  277. },
  278. "supported_platforms": list(PLATFORM_MAP.keys())
  279. })
  280. # ==================== 命令行启动 ====================
  281. def main():
  282. parser = argparse.ArgumentParser(description='多平台视频发布服务')
  283. parser.add_argument('--port', type=int, default=5005, help='服务端口 (默认: 5005)')
  284. parser.add_argument('--host', type=str, default='0.0.0.0', help='监听地址 (默认: 0.0.0.0)')
  285. parser.add_argument('--headless', type=str, default='true', help='是否无头模式 (默认: true)')
  286. parser.add_argument('--debug', action='store_true', help='调试模式')
  287. args = parser.parse_args()
  288. global HEADLESS_MODE
  289. HEADLESS_MODE = args.headless.lower() == 'true'
  290. # 检查 xhs SDK
  291. xhs_status = "未安装"
  292. try:
  293. from platforms.xiaohongshu import XHS_SDK_AVAILABLE
  294. xhs_status = "已安装" if XHS_SDK_AVAILABLE else "未安装"
  295. except:
  296. pass
  297. print("=" * 60)
  298. print("多平台视频发布服务")
  299. print("=" * 60)
  300. print(f"XHS SDK: {xhs_status}")
  301. print(f"Headless 模式: {HEADLESS_MODE}")
  302. print(f"支持平台: {', '.join(PLATFORM_MAP.keys())}")
  303. print("=" * 60)
  304. print(f"启动服务: http://{args.host}:{args.port}")
  305. print("=" * 60)
  306. app.run(host=args.host, port=args.port, debug=args.debug, threaded=True)
  307. if __name__ == '__main__':
  308. main()