返回 Social Auto Upload
sau_backend.py
根目录 / sau_backend.py
1 import asyncio
2 import os
3 import sqlite3
4 import threading
5 import time
6 import uuid
7 from pathlib import Path
8 from queue import Queue
9 from flask_cors import CORS
10 from myUtils.auth import check_cookie
11 from flask import Flask, request, jsonify, Response, render_template, send_from_directory
12 from werkzeug.utils import secure_filename
13 from conf import BASE_DIR
14 from myUtils.login import get_tencent_cookie, douyin_cookie_gen, get_ks_cookie, xiaohongshu_cookie_gen
15 from myUtils.postVideo import post_video_tencent, post_video_DouYin, post_video_ks, post_video_xhs
16
17 active_queues = {}
18 app = Flask(__name__)
19
20 #允许所有来源跨域访问
21 CORS(app)
22
23 # 限制上传文件大小为160MB
24 app.config['MAX_CONTENT_LENGTH'] = 160 * 1024 * 1024
25
26 # 获取当前目录(假设 index.html 和 assets 在这里)
27 current_dir = os.path.dirname(os.path.abspath(__file__))
28
29 # 处理所有静态资源请求(未来打包用)
30 @app.route('/assets/<filename>')
31 def custom_static(filename):
32 return send_from_directory(os.path.join(current_dir, 'assets'), filename)
33
34 # 处理 favicon.ico 静态资源(未来打包用)
35 @app.route('/favicon.ico')
36 def favicon():
37 return send_from_directory(os.path.join(current_dir, 'assets'), 'vite.svg')
38
39 @app.route('/vite.svg')
40 def vite_svg():
41 return send_from_directory(os.path.join(current_dir, 'assets'), 'vite.svg')
42
43 # (未来打包用)
44 @app.route('/')
45 def index(): # put application's code here
46 return send_from_directory(current_dir, 'index.html')
47
48 @app.route('/upload', methods=['POST'])
49 def upload_file():
50 if 'file' not in request.files:
51 return jsonify({
52 "code": 400,
53 "data": None,
54 "msg": "No file part in the request"
55 }), 400
56 file = request.files['file']
57 if file.filename == '':
58 return jsonify({
59 "code": 400,
60 "data": None,
61 "msg": "No selected file"
62 }), 400
63 try:
64 # 保存文件到指定位置
65 uuid_v1 = uuid.uuid1()
66 print(f"UUID v1: {uuid_v1}")
67 safe_name = secure_filename(file.filename)
68 if not safe_name:
69 return jsonify({"code": 400, "data": None, "msg": "Invalid filename"}), 400
70 filepath = Path(BASE_DIR / "videoFile" / f"{uuid_v1}_{safe_name}")
71 file.save(filepath)
72 return jsonify({"code":200,"msg": "File uploaded successfully", "data": f"{uuid_v1}_{safe_name}"}), 200
73 except Exception as e:
74 return jsonify({"code":500,"msg": str(e),"data":None}), 500
75
76 @app.route('/getFile', methods=['GET'])
77 def get_file():
78 # 获取 filename 参数
79 filename = request.args.get('filename')
80
81 if not filename:
82 return jsonify({"code": 400, "msg": "filename is required", "data": None}), 400
83
84 # 防止路径穿越攻击
85 if '..' in filename or filename.startswith('/'):
86 return jsonify({"code": 400, "msg": "Invalid filename", "data": None}), 400
87
88 # 拼接完整路径
89 file_path = str(Path(BASE_DIR / "videoFile"))
90
91 # 返回文件
92 return send_from_directory(file_path,filename)
93
94
95 @app.route('/uploadSave', methods=['POST'])
96 def upload_save():
97 if 'file' not in request.files:
98 return jsonify({
99 "code": 400,
100 "data": None,
101 "msg": "No file part in the request"
102 }), 400
103
104 file = request.files['file']
105 if file.filename == '':
106 return jsonify({
107 "code": 400,
108 "data": None,
109 "msg": "No selected file"
110 }), 400
111
112 # 获取表单中的自定义文件名(可选)
113 custom_filename = request.form.get('filename', None)
114 if custom_filename:
115 filename = secure_filename(custom_filename + "." + file.filename.split('.')[-1])
116 else:
117 filename = secure_filename(file.filename)
118 if not filename:
119 return jsonify({"code": 400, "data": None, "msg": "Invalid filename"}), 400
120
121 try:
122 # 生成 UUID v1
123 uuid_v1 = uuid.uuid1()
124 print(f"UUID v1: {uuid_v1}")
125
126 # 构造文件名和路径
127 final_filename = f"{uuid_v1}_{filename}"
128 filepath = Path(BASE_DIR / "videoFile" / f"{uuid_v1}_{filename}")
129
130 # 保存文件
131 file.save(filepath)
132
133 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
134 cursor = conn.cursor()
135 cursor.execute('''
136 INSERT INTO file_records (filename, filesize, file_path)
137 VALUES (?, ?, ?)
138 ''', (filename, round(float(os.path.getsize(filepath)) / (1024 * 1024),2), final_filename))
139 conn.commit()
140 print("✅ 上传文件已记录")
141
142 return jsonify({
143 "code": 200,
144 "msg": "File uploaded and saved successfully",
145 "data": {
146 "filename": filename,
147 "filepath": final_filename
148 }
149 }), 200
150
151 except Exception as e:
152 print(f"Upload failed: {e}")
153 return jsonify({
154 "code": 500,
155 "msg": f"upload failed: {e}",
156 "data": None
157 }), 500
158
159 @app.route('/getFiles', methods=['GET'])
160 def get_all_files():
161 try:
162 # 使用 with 自动管理数据库连接
163 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
164 conn.row_factory = sqlite3.Row # 允许通过列名访问结果
165 cursor = conn.cursor()
166
167 # 查询所有记录
168 cursor.execute("SELECT * FROM file_records")
169 rows = cursor.fetchall()
170
171 # 将结果转为字典列表,并提取UUID
172 data = []
173 for row in rows:
174 row_dict = dict(row)
175 # 从 file_path 中提取 UUID (文件名的第一部分,下划线前)
176 if row_dict.get('file_path'):
177 file_path_parts = row_dict['file_path'].split('_', 1) # 只分割第一个下划线
178 if len(file_path_parts) > 0:
179 row_dict['uuid'] = file_path_parts[0] # UUID 部分
180 else:
181 row_dict['uuid'] = ''
182 else:
183 row_dict['uuid'] = ''
184 data.append(row_dict)
185
186 return jsonify({
187 "code": 200,
188 "msg": "success",
189 "data": data
190 }), 200
191 except Exception as e:
192 return jsonify({
193 "code": 500,
194 "msg": str("get file failed!"),
195 "data": None
196 }), 500
197
198
199 @app.route("/getAccounts", methods=['GET'])
200 def getAccounts():
201 """快速获取所有账号信息,不进行cookie验证"""
202 try:
203 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
204 conn.row_factory = sqlite3.Row
205 cursor = conn.cursor()
206 cursor.execute('''
207 SELECT * FROM user_info''')
208 rows = cursor.fetchall()
209 rows_list = [list(row) for row in rows]
210
211 print("\n📋 当前数据表内容(快速获取):")
212 for row in rows:
213 print(row)
214
215 return jsonify(
216 {
217 "code": 200,
218 "msg": None,
219 "data": rows_list
220 }), 200
221 except Exception as e:
222 print(f"获取账号列表时出错: {str(e)}")
223 return jsonify({
224 "code": 500,
225 "msg": f"获取账号列表失败: {str(e)}",
226 "data": None
227 }), 500
228
229
230 @app.route("/getValidAccounts",methods=['GET'])
231 async def getValidAccounts():
232 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
233 cursor = conn.cursor()
234 cursor.execute('''
235 SELECT * FROM user_info''')
236 rows = cursor.fetchall()
237 rows_list = [list(row) for row in rows]
238 print("\n📋 当前数据表内容:")
239 for row in rows:
240 print(row)
241 for row in rows_list:
242 flag = await check_cookie(row[1],row[2])
243 if not flag:
244 row[4] = 0
245 cursor.execute('''
246 UPDATE user_info
247 SET status = ?
248 WHERE id = ?
249 ''', (0,row[0]))
250 conn.commit()
251 print("✅ 用户状态已更新")
252 for row in rows:
253 print(row)
254 return jsonify(
255 {
256 "code": 200,
257 "msg": None,
258 "data": rows_list
259 }),200
260
261 @app.route('/deleteFile', methods=['GET'])
262 def delete_file():
263 file_id = request.args.get('id')
264
265 if not file_id or not file_id.isdigit():
266 return jsonify({
267 "code": 400,
268 "msg": "Invalid or missing file ID",
269 "data": None
270 }), 400
271
272 try:
273 # 获取数据库连接
274 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
275 conn.row_factory = sqlite3.Row
276 cursor = conn.cursor()
277
278 # 查询要删除的记录
279 cursor.execute("SELECT * FROM file_records WHERE id = ?", (file_id,))
280 record = cursor.fetchone()
281
282 if not record:
283 return jsonify({
284 "code": 404,
285 "msg": "File not found",
286 "data": None
287 }), 404
288
289 record = dict(record)
290
291 # 获取文件路径并删除实际文件
292 file_path = Path(BASE_DIR / "videoFile" / record['file_path'])
293 if file_path.exists():
294 try:
295 file_path.unlink() # 删除文件
296 print(f"✅ 实际文件已删除: {file_path}")
297 except Exception as e:
298 print(f"⚠️ 删除实际文件失败: {e}")
299 # 即使删除文件失败,也要继续删除数据库记录,避免数据不一致
300 else:
301 print(f"⚠️ 实际文件不存在: {file_path}")
302
303 # 删除数据库记录
304 cursor.execute("DELETE FROM file_records WHERE id = ?", (file_id,))
305 conn.commit()
306
307 return jsonify({
308 "code": 200,
309 "msg": "File deleted successfully",
310 "data": {
311 "id": record['id'],
312 "filename": record['filename']
313 }
314 }), 200
315
316 except Exception as e:
317 return jsonify({
318 "code": 500,
319 "msg": str("delete failed!"),
320 "data": None
321 }), 500
322
323 @app.route('/deleteAccount', methods=['GET'])
324 def delete_account():
325 account_id = request.args.get('id')
326
327 if not account_id or not account_id.isdigit():
328 return jsonify({
329 "code": 400,
330 "msg": "Invalid or missing account ID",
331 "data": None
332 }), 400
333
334 account_id = int(account_id)
335
336 try:
337 # 获取数据库连接
338 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
339 conn.row_factory = sqlite3.Row
340 cursor = conn.cursor()
341
342 # 查询要删除的记录
343 cursor.execute("SELECT * FROM user_info WHERE id = ?", (account_id,))
344 record = cursor.fetchone()
345
346 if not record:
347 return jsonify({
348 "code": 404,
349 "msg": "account not found",
350 "data": None
351 }), 404
352
353 record = dict(record)
354
355 # 删除关联的cookie文件
356 if record.get('filePath'):
357 cookie_file_path = Path(BASE_DIR / "cookiesFile" / record['filePath'])
358 if cookie_file_path.exists():
359 try:
360 cookie_file_path.unlink()
361 print(f"✅ Cookie文件已删除: {cookie_file_path}")
362 except Exception as e:
363 print(f"⚠️ 删除Cookie文件失败: {e}")
364
365 # 删除数据库记录
366 cursor.execute("DELETE FROM user_info WHERE id = ?", (account_id,))
367 conn.commit()
368
369 return jsonify({
370 "code": 200,
371 "msg": "account deleted successfully",
372 "data": None
373 }), 200
374
375 except Exception as e:
376 return jsonify({
377 "code": 500,
378 "msg": f"delete failed: {str(e)}",
379 "data": None
380 }), 500
381
382
383 # SSE 登录接口
384 @app.route('/login')
385 def login():
386 # 1 小红书 2 视频号 3 抖音 4 快手
387 type = request.args.get('type')
388 # 账号名
389 id = request.args.get('id')
390
391 # 模拟一个用于异步通信的队列
392 status_queue = Queue()
393 active_queues[id] = status_queue
394
395 def on_close():
396 print(f"清理队列: {id}")
397 del active_queues[id]
398 # 启动异步任务线程
399 thread = threading.Thread(target=run_async_function, args=(type,id,status_queue), daemon=True)
400 thread.start()
401 response = Response(sse_stream(status_queue,), mimetype='text/event-stream')
402 response.headers['Cache-Control'] = 'no-cache'
403 response.headers['X-Accel-Buffering'] = 'no' # 关键:禁用 Nginx 缓冲
404 response.headers['Content-Type'] = 'text/event-stream'
405 response.headers['Connection'] = 'keep-alive'
406 return response
407
408 @app.route('/postVideo', methods=['POST'])
409 def postVideo():
410 # 获取JSON数据
411 data = request.get_json()
412
413 if not data:
414 return jsonify({"code": 400, "msg": "请求数据不能为空", "data": None}), 400
415
416 # 从JSON数据中提取fileList和accountList
417 file_list = data.get('fileList', [])
418 account_list = data.get('accountList', [])
419 type = data.get('type')
420 title = data.get('title')
421 tags = data.get('tags')
422 category = data.get('category')
423 enableTimer = data.get('enableTimer')
424 if category == 0:
425 category = None
426 productLink = data.get('productLink', '')
427 productTitle = data.get('productTitle', '')
428 thumbnail_path = data.get('thumbnail', '')
429 is_draft = data.get('isDraft', False) # 新增参数:是否保存为草稿
430
431 videos_per_day = data.get('videosPerDay')
432 daily_times = data.get('dailyTimes')
433 start_days = data.get('startDays')
434
435 # 参数校验
436 if not file_list:
437 return jsonify({"code": 400, "msg": "文件列表不能为空", "data": None}), 400
438 if not account_list:
439 return jsonify({"code": 400, "msg": "账号列表不能为空", "data": None}), 400
440 if not type:
441 return jsonify({"code": 400, "msg": "平台类型不能为空", "data": None}), 400
442 if not title:
443 return jsonify({"code": 400, "msg": "标题不能为空", "data": None}), 400
444
445 # 打印获取到的数据(仅作为示例)
446 print("File List:", file_list)
447 print("Account List:", account_list)
448
449 try:
450 match type:
451 case 1:
452 post_video_xhs(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
453 start_days)
454 case 2:
455 post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
456 start_days, is_draft)
457 case 3:
458 post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
459 start_days, thumbnail_path, productLink, productTitle)
460 case 4:
461 post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
462 start_days)
463 case _:
464 return jsonify({"code": 400, "msg": f"不支持的平台类型: {type}", "data": None}), 400
465
466 # 返回响应给客户端
467 return jsonify(
468 {
469 "code": 200,
470 "msg": "发布任务已提交",
471 "data": None
472 }), 200
473 except Exception as e:
474 print(f"发布视频时出错: {str(e)}")
475 return jsonify({
476 "code": 500,
477 "msg": f"发布失败: {str(e)}",
478 "data": None
479 }), 500
480
481
482 @app.route('/updateUserinfo', methods=['POST'])
483 def updateUserinfo():
484 # 获取JSON数据
485 data = request.get_json()
486
487 # 从JSON数据中提取 type 和 userName
488 user_id = data.get('id')
489 type = data.get('type')
490 userName = data.get('userName')
491 try:
492 # 获取数据库连接
493 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
494 conn.row_factory = sqlite3.Row
495 cursor = conn.cursor()
496
497 # 更新数据库记录
498 cursor.execute('''
499 UPDATE user_info
500 SET type = ?,
501 userName = ?
502 WHERE id = ?;
503 ''', (type, userName, user_id))
504 conn.commit()
505
506 return jsonify({
507 "code": 200,
508 "msg": "account update successfully",
509 "data": None
510 }), 200
511
512 except Exception as e:
513 return jsonify({
514 "code": 500,
515 "msg": str("update failed!"),
516 "data": None
517 }), 500
518
519 @app.route('/postVideoBatch', methods=['POST'])
520 def postVideoBatch():
521 data_list = request.get_json()
522
523 if not isinstance(data_list, list):
524 return jsonify({"code": 400, "msg": "Expected a JSON array", "data": None}), 400
525 for data in data_list:
526 # 从JSON数据中提取fileList和accountList
527 file_list = data.get('fileList', [])
528 account_list = data.get('accountList', [])
529 type = data.get('type')
530 title = data.get('title')
531 tags = data.get('tags')
532 category = data.get('category')
533 enableTimer = data.get('enableTimer')
534 if category == 0:
535 category = None
536 productLink = data.get('productLink', '')
537 productTitle = data.get('productTitle', '')
538 is_draft = data.get('isDraft', False)
539
540 videos_per_day = data.get('videosPerDay')
541 daily_times = data.get('dailyTimes')
542 start_days = data.get('startDays')
543 # 打印获取到的数据(仅作为示例)
544 print("File List:", file_list)
545 print("Account List:", account_list)
546 match type:
547 case 1:
548 post_video_xhs(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
549 start_days)
550 case 2:
551 post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
552 start_days, is_draft)
553 case 3:
554 post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
555 start_days, productLink, productTitle)
556 case 4:
557 post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
558 start_days)
559 # 返回响应给客户端
560 return jsonify(
561 {
562 "code": 200,
563 "msg": None,
564 "data": None
565 }), 200
566
567 # Cookie文件上传API
568 @app.route('/uploadCookie', methods=['POST'])
569 def upload_cookie():
570 try:
571 if 'file' not in request.files:
572 return jsonify({
573 "code": 400,
574 "msg": "没有找到Cookie文件",
575 "data": None
576 }), 400
577
578 file = request.files['file']
579 if file.filename == '':
580 return jsonify({
581 "code": 400,
582 "msg": "Cookie文件名不能为空",
583 "data": None
584 }), 400
585
586 if not file.filename.endswith('.json'):
587 return jsonify({
588 "code": 400,
589 "msg": "Cookie文件必须是JSON格式",
590 "data": None
591 }), 400
592
593 # 获取账号信息
594 account_id = request.form.get('id')
595 platform = request.form.get('platform')
596
597 if not account_id or not platform:
598 return jsonify({
599 "code": 400,
600 "msg": "缺少账号ID或平台信息",
601 "data": None
602 }), 400
603
604 # 从数据库获取账号的文件路径
605 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
606 conn.row_factory = sqlite3.Row
607 cursor = conn.cursor()
608 cursor.execute('SELECT filePath FROM user_info WHERE id = ?', (account_id,))
609 result = cursor.fetchone()
610
611 if not result:
612 return jsonify({
613 "code": 500,
614 "msg": "账号不存在",
615 "data": None
616 }), 404
617
618 # 保存上传的Cookie文件到对应路径
619 cookie_file_path = Path(BASE_DIR / "cookiesFile" / result['filePath'])
620 cookie_file_path.parent.mkdir(parents=True, exist_ok=True)
621
622 file.save(str(cookie_file_path))
623
624 # 更新数据库中的账号信息(可选,比如更新更新时间)
625 # 这里可以根据需要添加额外的处理逻辑
626
627 return jsonify({
628 "code": 200,
629 "msg": "Cookie文件上传成功",
630 "data": None
631 }), 200
632
633 except Exception as e:
634 print(f"上传Cookie文件时出错: {str(e)}")
635 return jsonify({
636 "code": 500,
637 "msg": f"上传Cookie文件失败: {str(e)}",
638 "data": None
639 }), 500
640
641
642 # Cookie文件下载API
643 @app.route('/downloadCookie', methods=['GET'])
644 def download_cookie():
645 try:
646 file_path = request.args.get('filePath')
647 if not file_path:
648 return jsonify({
649 "code": 500,
650 "msg": "缺少文件路径参数",
651 "data": None
652 }), 400
653
654 # 验证文件路径的安全性,防止路径遍历攻击
655 cookie_file_path = Path(BASE_DIR / "cookiesFile" / file_path).resolve()
656 base_path = Path(BASE_DIR / "cookiesFile").resolve()
657
658 if not cookie_file_path.is_relative_to(base_path):
659 return jsonify({
660 "code": 500,
661 "msg": "非法文件路径",
662 "data": None
663 }), 400
664
665 if not cookie_file_path.exists():
666 return jsonify({
667 "code": 500,
668 "msg": "Cookie文件不存在",
669 "data": None
670 }), 404
671
672 # 返回文件
673 return send_from_directory(
674 directory=str(cookie_file_path.parent),
675 path=cookie_file_path.name,
676 as_attachment=True
677 )
678
679 except Exception as e:
680 print(f"下载Cookie文件时出错: {str(e)}")
681 return jsonify({
682 "code": 500,
683 "msg": f"下载Cookie文件失败: {str(e)}",
684 "data": None
685 }), 500
686
687
688 # 包装函数:在线程中运行异步函数
689 def run_async_function(type,id,status_queue):
690 match type:
691 case '1':
692 loop = asyncio.new_event_loop()
693 asyncio.set_event_loop(loop)
694 loop.run_until_complete(xiaohongshu_cookie_gen(id, status_queue))
695 loop.close()
696 case '2':
697 loop = asyncio.new_event_loop()
698 asyncio.set_event_loop(loop)
699 loop.run_until_complete(get_tencent_cookie(id,status_queue))
700 loop.close()
701 case '3':
702 loop = asyncio.new_event_loop()
703 asyncio.set_event_loop(loop)
704 loop.run_until_complete(douyin_cookie_gen(id,status_queue))
705 loop.close()
706 case '4':
707 loop = asyncio.new_event_loop()
708 asyncio.set_event_loop(loop)
709 loop.run_until_complete(get_ks_cookie(id,status_queue))
710 loop.close()
711
712 # SSE 流生成器函数
713 def sse_stream(status_queue):
714 while True:
715 if not status_queue.empty():
716 msg = status_queue.get()
717 yield f"data: {msg}\n\n"
718 else:
719 # 避免 CPU 占满
720 time.sleep(0.1)
721
722 if __name__ == '__main__':
723 app.run(host='0.0.0.0' ,port=5409)
724
724 lines PYTHON