app.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948
  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, date
  18. from pathlib import Path
  19. import pymysql
  20. from pymysql.cursors import DictCursor
  21. # 确保当前目录在 Python 路径中
  22. CURRENT_DIR = Path(__file__).parent.resolve()
  23. if str(CURRENT_DIR) not in sys.path:
  24. sys.path.insert(0, str(CURRENT_DIR))
  25. # 从 server/.env 文件加载环境变量
  26. def load_env_file():
  27. """从 server/.env 文件加载环境变量"""
  28. env_path = CURRENT_DIR.parent / '.env'
  29. if env_path.exists():
  30. print(f"[Config] Loading env from: {env_path}")
  31. with open(env_path, 'r', encoding='utf-8') as f:
  32. for line in f:
  33. line = line.strip()
  34. if line and not line.startswith('#') and '=' in line:
  35. key, value = line.split('=', 1)
  36. key = key.strip()
  37. value = value.strip()
  38. # 移除引号
  39. if value.startswith('"') and value.endswith('"'):
  40. value = value[1:-1]
  41. elif value.startswith("'") and value.endswith("'"):
  42. value = value[1:-1]
  43. # 只在环境变量未设置时加载
  44. if key not in os.environ:
  45. os.environ[key] = value
  46. print(f"[Config] Loaded: {key}=***" if 'PASSWORD' in key or 'SECRET' in key else f"[Config] Loaded: {key}={value}")
  47. else:
  48. print(f"[Config] .env file not found: {env_path}")
  49. # 加载环境变量
  50. load_env_file()
  51. from flask import Flask, request, jsonify
  52. from flask_cors import CORS
  53. from platforms import get_publisher, PLATFORM_MAP
  54. from platforms.base import PublishParams
  55. def parse_datetime(date_str: str):
  56. """解析日期时间字符串"""
  57. if not date_str:
  58. return None
  59. formats = [
  60. "%Y-%m-%d %H:%M:%S",
  61. "%Y-%m-%d %H:%M",
  62. "%Y/%m/%d %H:%M:%S",
  63. "%Y/%m/%d %H:%M",
  64. "%Y-%m-%dT%H:%M:%S",
  65. "%Y-%m-%dT%H:%M:%SZ",
  66. ]
  67. for fmt in formats:
  68. try:
  69. return datetime.strptime(date_str, fmt)
  70. except ValueError:
  71. continue
  72. return None
  73. def validate_video_file(video_path: str) -> bool:
  74. """验证视频文件是否有效"""
  75. if not video_path:
  76. return False
  77. if not os.path.exists(video_path):
  78. return False
  79. if not os.path.isfile(video_path):
  80. return False
  81. valid_extensions = ['.mp4', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.webm']
  82. ext = os.path.splitext(video_path)[1].lower()
  83. if ext not in valid_extensions:
  84. return False
  85. if os.path.getsize(video_path) < 1024:
  86. return False
  87. return True
  88. # 创建 Flask 应用
  89. app = Flask(__name__)
  90. CORS(app)
  91. # 全局配置
  92. HEADLESS_MODE = os.environ.get('HEADLESS', 'true').lower() == 'true'
  93. # 数据库配置
  94. DB_CONFIG = {
  95. 'host': os.environ.get('DB_HOST', 'localhost'),
  96. 'port': int(os.environ.get('DB_PORT', 3306)),
  97. 'user': os.environ.get('DB_USERNAME', 'root'),
  98. 'password': os.environ.get('DB_PASSWORD', ''),
  99. 'database': os.environ.get('DB_DATABASE', 'media_manager'),
  100. 'charset': 'utf8mb4',
  101. 'cursorclass': DictCursor
  102. }
  103. print(f"[DB_CONFIG] host={DB_CONFIG['host']}, port={DB_CONFIG['port']}, user={DB_CONFIG['user']}, db={DB_CONFIG['database']}, pwd_len={len(DB_CONFIG['password'])}", flush=True)
  104. def get_db_connection():
  105. """获取数据库连接"""
  106. print(f"[DEBUG DB] 正在连接数据库...", flush=True)
  107. print(f"[DEBUG DB] host={DB_CONFIG['host']}, port={DB_CONFIG['port']}, user={DB_CONFIG['user']}, db={DB_CONFIG['database']}", flush=True)
  108. try:
  109. conn = pymysql.connect(**DB_CONFIG)
  110. print(f"[DEBUG DB] 数据库连接成功!", flush=True)
  111. return conn
  112. except Exception as e:
  113. print(f"[DEBUG DB] 数据库连接失败: {e}", flush=True)
  114. raise
  115. # ==================== 签名相关(小红书专用) ====================
  116. @app.route("/sign", methods=["POST"])
  117. def sign_endpoint():
  118. """小红书签名接口"""
  119. try:
  120. from platforms.xiaohongshu import XiaohongshuPublisher
  121. data = request.json
  122. publisher = XiaohongshuPublisher(headless=True)
  123. result = asyncio.run(publisher.get_sign(
  124. data.get("uri", ""),
  125. data.get("data"),
  126. data.get("a1", ""),
  127. data.get("web_session", "")
  128. ))
  129. return jsonify(result)
  130. except Exception as e:
  131. traceback.print_exc()
  132. return jsonify({"error": str(e)}), 500
  133. # ==================== 统一发布接口 ====================
  134. @app.route("/publish", methods=["POST"])
  135. def publish_video():
  136. """
  137. 统一发布接口
  138. 请求体:
  139. {
  140. "platform": "douyin", # douyin | xiaohongshu | weixin | kuaishou
  141. "cookie": "cookie字符串或JSON",
  142. "title": "视频标题",
  143. "description": "视频描述(可选)",
  144. "video_path": "视频文件绝对路径",
  145. "cover_path": "封面图片绝对路径(可选)",
  146. "tags": ["话题1", "话题2"],
  147. "post_time": "定时发布时间(可选,格式:2024-01-20 12:00:00)",
  148. "location": "位置(可选,默认:重庆市)"
  149. }
  150. 响应:
  151. {
  152. "success": true,
  153. "platform": "douyin",
  154. "video_id": "xxx",
  155. "video_url": "xxx",
  156. "message": "发布成功"
  157. }
  158. """
  159. try:
  160. data = request.json
  161. # 获取参数
  162. platform = data.get("platform", "").lower()
  163. cookie_str = data.get("cookie", "")
  164. title = data.get("title", "")
  165. description = data.get("description", "")
  166. video_path = data.get("video_path", "")
  167. cover_path = data.get("cover_path")
  168. tags = data.get("tags", [])
  169. post_time = data.get("post_time")
  170. location = data.get("location", "重庆市")
  171. # 调试日志
  172. print(f"[Publish] 收到请求: platform={platform}, title={title}, video_path={video_path}")
  173. # 参数验证
  174. if not platform:
  175. print("[Publish] 错误: 缺少 platform 参数")
  176. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  177. if platform not in PLATFORM_MAP:
  178. print(f"[Publish] 错误: 不支持的平台 {platform}")
  179. return jsonify({
  180. "success": False,
  181. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  182. }), 400
  183. if not cookie_str:
  184. print("[Publish] 错误: 缺少 cookie 参数")
  185. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  186. if not title:
  187. print("[Publish] 错误: 缺少 title 参数")
  188. return jsonify({"success": False, "error": "缺少 title 参数"}), 400
  189. if not video_path:
  190. print("[Publish] 错误: 缺少 video_path 参数")
  191. return jsonify({"success": False, "error": "缺少 video_path 参数"}), 400
  192. # 视频文件验证(增加详细信息)
  193. if not os.path.exists(video_path):
  194. print(f"[Publish] 错误: 视频文件不存在: {video_path}")
  195. return jsonify({"success": False, "error": f"视频文件不存在: {video_path}"}), 400
  196. if not os.path.isfile(video_path):
  197. print(f"[Publish] 错误: 路径不是文件: {video_path}")
  198. return jsonify({"success": False, "error": f"路径不是文件: {video_path}"}), 400
  199. # 解析发布时间
  200. publish_date = parse_datetime(post_time) if post_time else None
  201. # 创建发布参数
  202. params = PublishParams(
  203. title=title,
  204. video_path=video_path,
  205. description=description,
  206. cover_path=cover_path,
  207. tags=tags,
  208. publish_date=publish_date,
  209. location=location
  210. )
  211. print("=" * 60)
  212. print(f"[Publish] 平台: {platform}")
  213. print(f"[Publish] 标题: {title}")
  214. print(f"[Publish] 视频: {video_path}")
  215. print(f"[Publish] 封面: {cover_path}")
  216. print(f"[Publish] 话题: {tags}")
  217. print(f"[Publish] 定时: {publish_date}")
  218. print("=" * 60)
  219. # 获取对应平台的发布器
  220. PublisherClass = get_publisher(platform)
  221. publisher = PublisherClass(headless=HEADLESS_MODE)
  222. # 执行发布
  223. result = asyncio.run(publisher.run(cookie_str, params))
  224. response_data = {
  225. "success": result.success,
  226. "platform": result.platform,
  227. "video_id": result.video_id,
  228. "video_url": result.video_url,
  229. "message": result.message,
  230. "error": result.error,
  231. "need_captcha": result.need_captcha,
  232. "captcha_type": result.captcha_type
  233. }
  234. # 如果需要验证码,打印明确的日志
  235. if result.need_captcha:
  236. print(f"[Publish] 需要验证码: type={result.captcha_type}")
  237. return jsonify(response_data)
  238. except Exception as e:
  239. traceback.print_exc()
  240. return jsonify({"success": False, "error": str(e)}), 500
  241. # ==================== 批量发布接口 ====================
  242. @app.route("/publish/batch", methods=["POST"])
  243. def publish_batch():
  244. """
  245. 批量发布接口 - 发布到多个平台
  246. 请求体:
  247. {
  248. "platforms": ["douyin", "xiaohongshu"],
  249. "cookies": {
  250. "douyin": "cookie字符串",
  251. "xiaohongshu": "cookie字符串"
  252. },
  253. "title": "视频标题",
  254. "video_path": "视频文件绝对路径",
  255. ...
  256. }
  257. """
  258. try:
  259. data = request.json
  260. platforms = data.get("platforms", [])
  261. cookies = data.get("cookies", {})
  262. if not platforms:
  263. return jsonify({"success": False, "error": "缺少 platforms 参数"}), 400
  264. results = []
  265. for platform in platforms:
  266. platform = platform.lower()
  267. cookie_str = cookies.get(platform, "")
  268. if not cookie_str:
  269. results.append({
  270. "platform": platform,
  271. "success": False,
  272. "error": f"缺少 {platform} 的 cookie"
  273. })
  274. continue
  275. try:
  276. # 创建参数
  277. params = PublishParams(
  278. title=data.get("title", ""),
  279. video_path=data.get("video_path", ""),
  280. description=data.get("description", ""),
  281. cover_path=data.get("cover_path"),
  282. tags=data.get("tags", []),
  283. publish_date=parse_datetime(data.get("post_time")),
  284. location=data.get("location", "重庆市")
  285. )
  286. # 发布
  287. PublisherClass = get_publisher(platform)
  288. publisher = PublisherClass(headless=HEADLESS_MODE)
  289. result = asyncio.run(publisher.run(cookie_str, params))
  290. results.append({
  291. "platform": result.platform,
  292. "success": result.success,
  293. "video_id": result.video_id,
  294. "message": result.message,
  295. "error": result.error
  296. })
  297. except Exception as e:
  298. results.append({
  299. "platform": platform,
  300. "success": False,
  301. "error": str(e)
  302. })
  303. # 统计成功/失败数量
  304. success_count = sum(1 for r in results if r.get("success"))
  305. return jsonify({
  306. "success": success_count > 0,
  307. "total": len(platforms),
  308. "success_count": success_count,
  309. "fail_count": len(platforms) - success_count,
  310. "results": results
  311. })
  312. except Exception as e:
  313. traceback.print_exc()
  314. return jsonify({"success": False, "error": str(e)}), 500
  315. # ==================== Cookie 验证接口 ====================
  316. @app.route("/check_cookie", methods=["POST"])
  317. def check_cookie():
  318. """检查 cookie 是否有效"""
  319. try:
  320. data = request.json
  321. platform = data.get("platform", "").lower()
  322. cookie_str = data.get("cookie", "")
  323. if not cookie_str:
  324. return jsonify({"valid": False, "error": "缺少 cookie 参数"}), 400
  325. # 目前只支持小红书的 cookie 验证
  326. if platform == "xiaohongshu":
  327. try:
  328. from platforms.xiaohongshu import XiaohongshuPublisher, XHS_SDK_AVAILABLE
  329. if XHS_SDK_AVAILABLE:
  330. from xhs import XhsClient
  331. publisher = XiaohongshuPublisher()
  332. xhs_client = XhsClient(cookie_str, sign=publisher.sign_sync)
  333. info = xhs_client.get_self_info()
  334. if info:
  335. return jsonify({
  336. "valid": True,
  337. "user_info": {
  338. "user_id": info.get("user_id"),
  339. "nickname": info.get("nickname"),
  340. "avatar": info.get("images")
  341. }
  342. })
  343. except Exception as e:
  344. return jsonify({"valid": False, "error": str(e)})
  345. # 其他平台返回格式正确但未验证
  346. return jsonify({
  347. "valid": True,
  348. "message": "Cookie 格式正确,但未进行在线验证"
  349. })
  350. except Exception as e:
  351. traceback.print_exc()
  352. return jsonify({"valid": False, "error": str(e)})
  353. # ==================== 获取作品列表接口 ====================
  354. @app.route("/works", methods=["POST"])
  355. def get_works():
  356. """
  357. 获取作品列表
  358. 请求体:
  359. {
  360. "platform": "douyin", # douyin | xiaohongshu | kuaishou
  361. "cookie": "cookie字符串或JSON",
  362. "page": 0, # 页码(从0开始,可选,默认0)
  363. "page_size": 20 # 每页数量(可选,默认20)
  364. }
  365. 响应:
  366. {
  367. "success": true,
  368. "platform": "douyin",
  369. "works": [...],
  370. "total": 100,
  371. "has_more": true
  372. }
  373. """
  374. try:
  375. data = request.json
  376. platform = data.get("platform", "").lower()
  377. cookie_str = data.get("cookie", "")
  378. page = data.get("page", 0)
  379. page_size = data.get("page_size", 20)
  380. print(f"[Works] 收到请求: platform={platform}, page={page}, page_size={page_size}")
  381. if not platform:
  382. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  383. if platform not in PLATFORM_MAP:
  384. return jsonify({
  385. "success": False,
  386. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  387. }), 400
  388. if not cookie_str:
  389. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  390. # 获取对应平台的发布器
  391. PublisherClass = get_publisher(platform)
  392. publisher = PublisherClass(headless=HEADLESS_MODE)
  393. # 执行获取作品
  394. result = asyncio.run(publisher.run_get_works(cookie_str, page, page_size))
  395. return jsonify(result.to_dict())
  396. except Exception as e:
  397. traceback.print_exc()
  398. return jsonify({"success": False, "error": str(e)}), 500
  399. # ==================== 保存作品日统计数据接口 ====================
  400. @app.route("/work_day_statistics", methods=["POST"])
  401. def save_work_day_statistics():
  402. """
  403. 保存作品每日统计数据
  404. 当天的数据走更新流,日期变化走新增流
  405. 请求体:
  406. {
  407. "statistics": [
  408. {
  409. "work_id": 1,
  410. "fans_count": 1000,
  411. "play_count": 5000,
  412. "like_count": 200,
  413. "comment_count": 50,
  414. "share_count": 30,
  415. "collect_count": 100
  416. },
  417. ...
  418. ]
  419. }
  420. 响应:
  421. {
  422. "success": true,
  423. "inserted": 5,
  424. "updated": 3,
  425. "message": "保存成功"
  426. }
  427. """
  428. print("=" * 60, flush=True)
  429. print("[DEBUG] ===== 进入 save_work_day_statistics 方法 =====", flush=True)
  430. print(f"[DEBUG] 请求方法: {request.method}", flush=True)
  431. print(f"[DEBUG] 请求数据: {request.json}", flush=True)
  432. print("=" * 60, flush=True)
  433. try:
  434. data = request.json
  435. statistics_list = data.get("statistics", [])
  436. if not statistics_list:
  437. return jsonify({"success": False, "error": "缺少 statistics 参数"}), 400
  438. today = date.today()
  439. inserted_count = 0
  440. updated_count = 0
  441. print(f"[WorkDayStatistics] 收到请求: {len(statistics_list)} 条统计数据")
  442. conn = get_db_connection()
  443. try:
  444. with conn.cursor() as cursor:
  445. for stat in statistics_list:
  446. work_id = stat.get("work_id")
  447. if not work_id:
  448. continue
  449. fans_count = stat.get("fans_count", 0)
  450. play_count = stat.get("play_count", 0)
  451. like_count = stat.get("like_count", 0)
  452. comment_count = stat.get("comment_count", 0)
  453. share_count = stat.get("share_count", 0)
  454. collect_count = stat.get("collect_count", 0)
  455. # 检查当天是否已有记录
  456. cursor.execute(
  457. "SELECT id FROM work_day_statistics WHERE work_id = %s AND record_date = %s",
  458. (work_id, today)
  459. )
  460. existing = cursor.fetchone()
  461. if existing:
  462. # 更新已有记录
  463. cursor.execute(
  464. """UPDATE work_day_statistics
  465. SET fans_count = %s, play_count = %s, like_count = %s,
  466. comment_count = %s, share_count = %s, collect_count = %s,
  467. updated_at = NOW()
  468. WHERE id = %s""",
  469. (fans_count, play_count, like_count, comment_count,
  470. share_count, collect_count, existing['id'])
  471. )
  472. updated_count += 1
  473. else:
  474. # 插入新记录
  475. cursor.execute(
  476. """INSERT INTO work_day_statistics
  477. (work_id, record_date, fans_count, play_count, like_count,
  478. comment_count, share_count, collect_count, created_at, updated_at)
  479. VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())""",
  480. (work_id, today, fans_count, play_count, like_count,
  481. comment_count, share_count, collect_count)
  482. )
  483. inserted_count += 1
  484. conn.commit()
  485. finally:
  486. conn.close()
  487. print(f"[WorkDayStatistics] 完成: 新增 {inserted_count} 条, 更新 {updated_count} 条")
  488. return jsonify({
  489. "success": True,
  490. "inserted": inserted_count,
  491. "updated": updated_count,
  492. "message": f"保存成功: 新增 {inserted_count} 条, 更新 {updated_count} 条"
  493. })
  494. except Exception as e:
  495. traceback.print_exc()
  496. return jsonify({"success": False, "error": str(e)}), 500
  497. @app.route("/work_day_statistics/batch", methods=["POST"])
  498. def get_work_statistics_history():
  499. """
  500. 批量获取作品的历史统计数据
  501. 请求体:
  502. {
  503. "work_ids": [1, 2, 3],
  504. "start_date": "2025-01-01", # 可选
  505. "end_date": "2025-01-21" # 可选
  506. }
  507. 响应:
  508. {
  509. "success": true,
  510. "data": {
  511. "1": [
  512. {"record_date": "2025-01-20", "play_count": 100, ...},
  513. {"record_date": "2025-01-21", "play_count": 150, ...}
  514. ],
  515. ...
  516. }
  517. }
  518. """
  519. try:
  520. data = request.json
  521. work_ids = data.get("work_ids", [])
  522. start_date = data.get("start_date")
  523. end_date = data.get("end_date")
  524. if not work_ids:
  525. return jsonify({"success": False, "error": "缺少 work_ids 参数"}), 400
  526. conn = get_db_connection()
  527. try:
  528. with conn.cursor() as cursor:
  529. # 构建查询
  530. placeholders = ', '.join(['%s'] * len(work_ids))
  531. sql = f"""SELECT work_id, record_date, fans_count, play_count, like_count,
  532. comment_count, share_count, collect_count
  533. FROM work_day_statistics
  534. WHERE work_id IN ({placeholders})"""
  535. params = list(work_ids)
  536. if start_date:
  537. sql += " AND record_date >= %s"
  538. params.append(start_date)
  539. if end_date:
  540. sql += " AND record_date <= %s"
  541. params.append(end_date)
  542. sql += " ORDER BY work_id, record_date"
  543. cursor.execute(sql, params)
  544. results = cursor.fetchall()
  545. finally:
  546. conn.close()
  547. # 按 work_id 分组
  548. grouped_data = {}
  549. for row in results:
  550. work_id = str(row['work_id'])
  551. if work_id not in grouped_data:
  552. grouped_data[work_id] = []
  553. grouped_data[work_id].append({
  554. 'record_date': row['record_date'].strftime('%Y-%m-%d') if row['record_date'] else None,
  555. 'fans_count': row['fans_count'],
  556. 'play_count': row['play_count'],
  557. 'like_count': row['like_count'],
  558. 'comment_count': row['comment_count'],
  559. 'share_count': row['share_count'],
  560. 'collect_count': row['collect_count']
  561. })
  562. return jsonify({
  563. "success": True,
  564. "data": grouped_data
  565. })
  566. except Exception as e:
  567. traceback.print_exc()
  568. return jsonify({"success": False, "error": str(e)}), 500
  569. # ==================== 获取评论列表接口 ====================
  570. @app.route("/comments", methods=["POST"])
  571. def get_comments():
  572. """
  573. 获取作品评论
  574. 请求体:
  575. {
  576. "platform": "douyin", # douyin | xiaohongshu | kuaishou
  577. "cookie": "cookie字符串或JSON",
  578. "work_id": "作品ID",
  579. "cursor": "" # 分页游标(可选)
  580. }
  581. 响应:
  582. {
  583. "success": true,
  584. "platform": "douyin",
  585. "work_id": "xxx",
  586. "comments": [...],
  587. "total": 50,
  588. "has_more": true,
  589. "cursor": "xxx"
  590. }
  591. """
  592. try:
  593. data = request.json
  594. platform = data.get("platform", "").lower()
  595. cookie_str = data.get("cookie", "")
  596. work_id = data.get("work_id", "")
  597. cursor = data.get("cursor", "")
  598. print(f"[Comments] 收到请求: platform={platform}, work_id={work_id}")
  599. if not platform:
  600. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  601. if platform not in PLATFORM_MAP:
  602. return jsonify({
  603. "success": False,
  604. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  605. }), 400
  606. if not cookie_str:
  607. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  608. if not work_id:
  609. return jsonify({"success": False, "error": "缺少 work_id 参数"}), 400
  610. # 获取对应平台的发布器
  611. PublisherClass = get_publisher(platform)
  612. publisher = PublisherClass(headless=HEADLESS_MODE)
  613. # 执行获取评论
  614. result = asyncio.run(publisher.run_get_comments(cookie_str, work_id, cursor))
  615. result_dict = result.to_dict()
  616. # 添加 cursor 到响应
  617. if hasattr(result, '__dict__') and 'cursor' in result.__dict__:
  618. result_dict['cursor'] = result.__dict__['cursor']
  619. return jsonify(result_dict)
  620. except Exception as e:
  621. traceback.print_exc()
  622. return jsonify({"success": False, "error": str(e)}), 500
  623. # ==================== 获取所有作品评论接口 ====================
  624. @app.route("/all_comments", methods=["POST"])
  625. def get_all_comments():
  626. """
  627. 获取所有作品的评论(一次性获取)
  628. 请求体:
  629. {
  630. "platform": "douyin", # douyin | xiaohongshu
  631. "cookie": "cookie字符串或JSON"
  632. }
  633. 响应:
  634. {
  635. "success": true,
  636. "platform": "douyin",
  637. "work_comments": [
  638. {
  639. "work_id": "xxx",
  640. "title": "作品标题",
  641. "cover_url": "封面URL",
  642. "comments": [...]
  643. }
  644. ],
  645. "total": 5
  646. }
  647. """
  648. try:
  649. data = request.json
  650. platform = data.get("platform", "").lower()
  651. cookie_str = data.get("cookie", "")
  652. print(f"[AllComments] 收到请求: platform={platform}")
  653. if not platform:
  654. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  655. if platform not in ['douyin', 'xiaohongshu']:
  656. return jsonify({
  657. "success": False,
  658. "error": f"该接口只支持 douyin 和 xiaohongshu 平台"
  659. }), 400
  660. if not cookie_str:
  661. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  662. # 获取对应平台的发布器
  663. PublisherClass = get_publisher(platform)
  664. publisher = PublisherClass(headless=HEADLESS_MODE)
  665. # 执行获取所有评论
  666. result = asyncio.run(publisher.get_all_comments(cookie_str))
  667. return jsonify(result)
  668. except Exception as e:
  669. traceback.print_exc()
  670. return jsonify({"success": False, "error": str(e)}), 500
  671. # ==================== 登录状态检查接口 ====================
  672. @app.route("/check_login", methods=["POST"])
  673. def check_login():
  674. """
  675. 检查 Cookie 登录状态(通过浏览器访问后台页面检测)
  676. 请求体:
  677. {
  678. "platform": "douyin", # douyin | xiaohongshu | kuaishou | weixin
  679. "cookie": "cookie字符串或JSON"
  680. }
  681. 响应:
  682. {
  683. "success": true,
  684. "valid": true, # Cookie 是否有效
  685. "need_login": false, # 是否需要重新登录
  686. "message": "登录状态有效"
  687. }
  688. """
  689. try:
  690. data = request.json
  691. platform = data.get("platform", "").lower()
  692. cookie_str = data.get("cookie", "")
  693. print(f"[CheckLogin] 收到请求: platform={platform}")
  694. if not platform:
  695. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  696. if platform not in PLATFORM_MAP:
  697. return jsonify({
  698. "success": False,
  699. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  700. }), 400
  701. if not cookie_str:
  702. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  703. # 获取对应平台的发布器
  704. PublisherClass = get_publisher(platform)
  705. publisher = PublisherClass(headless=HEADLESS_MODE)
  706. # 执行登录检查
  707. result = asyncio.run(publisher.check_login_status(cookie_str))
  708. return jsonify(result)
  709. except Exception as e:
  710. traceback.print_exc()
  711. return jsonify({
  712. "success": False,
  713. "valid": False,
  714. "need_login": True,
  715. "error": str(e)
  716. }), 500
  717. # ==================== 健康检查 ====================
  718. @app.route("/health", methods=["GET"])
  719. def health_check():
  720. """健康检查"""
  721. # 检查 xhs SDK 是否可用
  722. xhs_available = False
  723. try:
  724. from platforms.xiaohongshu import XHS_SDK_AVAILABLE
  725. xhs_available = XHS_SDK_AVAILABLE
  726. except:
  727. pass
  728. return jsonify({
  729. "status": "ok",
  730. "xhs_sdk": xhs_available,
  731. "supported_platforms": list(PLATFORM_MAP.keys()),
  732. "headless_mode": HEADLESS_MODE
  733. })
  734. @app.route("/", methods=["GET"])
  735. def index():
  736. """首页"""
  737. return jsonify({
  738. "name": "多平台视频发布服务",
  739. "version": "1.2.0",
  740. "endpoints": {
  741. "GET /": "服务信息",
  742. "GET /health": "健康检查",
  743. "POST /publish": "发布视频",
  744. "POST /publish/batch": "批量发布",
  745. "POST /works": "获取作品列表",
  746. "POST /comments": "获取作品评论",
  747. "POST /all_comments": "获取所有作品评论",
  748. "POST /work_day_statistics": "保存作品每日统计数据",
  749. "POST /work_day_statistics/batch": "获取作品历史统计数据",
  750. "POST /check_cookie": "检查 Cookie",
  751. "POST /sign": "小红书签名"
  752. },
  753. "supported_platforms": list(PLATFORM_MAP.keys())
  754. })
  755. # ==================== 命令行启动 ====================
  756. def main():
  757. parser = argparse.ArgumentParser(description='多平台视频发布服务')
  758. parser.add_argument('--port', type=int, default=5005, help='服务端口 (默认: 5005)')
  759. parser.add_argument('--host', type=str, default='0.0.0.0', help='监听地址 (默认: 0.0.0.0)')
  760. parser.add_argument('--headless', type=str, default='true', help='是否无头模式 (默认: true)')
  761. parser.add_argument('--debug', action='store_true', help='调试模式')
  762. args = parser.parse_args()
  763. global HEADLESS_MODE
  764. HEADLESS_MODE = args.headless.lower() == 'true'
  765. # 检查 xhs SDK
  766. xhs_status = "未安装"
  767. try:
  768. from platforms.xiaohongshu import XHS_SDK_AVAILABLE
  769. xhs_status = "已安装" if XHS_SDK_AVAILABLE else "未安装"
  770. except:
  771. pass
  772. print("=" * 60)
  773. print("多平台视频发布服务")
  774. print("=" * 60)
  775. print(f"XHS SDK: {xhs_status}")
  776. print(f"Headless 模式: {HEADLESS_MODE}")
  777. print(f"支持平台: {', '.join(PLATFORM_MAP.keys())}")
  778. print("=" * 60)
  779. print(f"启动服务: http://{args.host}:{args.port}")
  780. print("=" * 60)
  781. app.run(host=args.host, port=args.port, debug=args.debug, threaded=True)
  782. if __name__ == '__main__':
  783. main()