geopromini commited on
Commit
35f26df
·
verified ·
1 Parent(s): 2b94e8e

Upload 5 files

Browse files
Files changed (5) hide show
  1. config.py +25 -0
  2. facebook_utils.py +50 -0
  3. google_utils.py +63 -0
  4. main.py +60 -0
  5. processor.py +103 -0
config.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # Cấu hình chung
4
+ DRIVE_EXCEL_FILE_ID = "ID_FILE_EXCEL_CUA_BAN" # Thay ID file input.xlsx trên Drive vào đây
5
+ LOCAL_EXCEL_PATH = "input.xlsx"
6
+ TEMP_VIDEO_DIR = "temp_videos"
7
+
8
+ # Cấu hình các Page (Thêm Page mới thì thêm 1 block vào đây)
9
+ PAGES_CONFIG = [
10
+ {
11
+ "sheet_name": "BeYeu", # Tên Sheet trong Excel
12
+ "page_id": os.getenv("PAGE_ID_BEYEU"),
13
+ "access_token": os.getenv("TOKEN_BEYEU"),
14
+ "schedule_times": ["07:00", "19:00"], # Giờ đăng bài
15
+ "active": True
16
+ },
17
+ {
18
+ "sheet_name": "BeYeu2",
19
+ "page_id": os.getenv("PAGE_ID_BEYEU2"),
20
+ "access_token": os.getenv("TOKEN_BEYEU2"),
21
+ "schedule_times": ["08:00", "20:00"],
22
+ "active": True
23
+ },
24
+ # Muốn thêm BeYeu3, chỉ cần copy block trên và đổi thông tin
25
+ ]
facebook_utils.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+
5
+ async def upload_reel(page_id, access_token, video_path, description):
6
+ api_version = "v19.0"
7
+ base_url = f"https://graph.facebook.com/{api_version}"
8
+
9
+ async with httpx.AsyncClient(timeout=120.0) as client:
10
+ # 1. Init
11
+ init_res = await client.post(
12
+ f"{base_url}/{page_id}/video_reels",
13
+ data={"upload_phase": "start", "access_token": access_token}
14
+ )
15
+ init_data = init_res.json()
16
+ video_id = init_data.get("video_id")
17
+ upload_url = init_data.get("upload_url")
18
+
19
+ if not video_id: raise Exception(f"Init Error: {init_data}")
20
+
21
+ # 2. Upload
22
+ file_size = os.path.getsize(video_path)
23
+ with open(video_path, "rb") as f:
24
+ video_data = f.read()
25
+
26
+ headers = {"Authorization": f"OAuth {access_token}", "offset": "0", "file_size": str(file_size)}
27
+ upload_res = await client.post(upload_url, content=video_data, headers=headers)
28
+ if upload_res.status_code != 200: raise Exception(f"Upload Error: {upload_res.text}")
29
+
30
+ # 3. Publish
31
+ pub_res = await client.post(
32
+ f"{base_url}/{page_id}/video_reels",
33
+ data={
34
+ "access_token": access_token,
35
+ "video_id": video_id,
36
+ "upload_phase": "finish",
37
+ "video_state": "PUBLISHED",
38
+ "description": description
39
+ }
40
+ )
41
+ if not pub_res.json().get("success"): raise Exception(f"Publish Error: {pub_res.text}")
42
+
43
+ return video_id
44
+
45
+ async def comment_on_video(video_id, access_token, message):
46
+ # Đợi 1 chút để video xử lý
47
+ await asyncio.sleep(15)
48
+ url = f"https://graph.facebook.com/v19.0/{video_id}/comments"
49
+ async with httpx.AsyncClient() as client:
50
+ await client.post(url, data={"message": message, "access_token": access_token})
google_utils.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import google.generativeai as genai
3
+ import random
4
+ import asyncio
5
+ import os
6
+ from googleapiclient.discovery import build
7
+ from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
8
+ import io
9
+ import pickle
10
+
11
+ # --- Biến toàn cục cho Gemini Keys ---
12
+ GEMINI_KEYS = []
13
+
14
+ async def load_gemini_keys(excel_path):
15
+ """Đọc list API Key từ sheet APIKey"""
16
+ global GEMINI_KEYS
17
+ try:
18
+ df = pd.read_excel(excel_path, sheet_name='APIKey', header=None)
19
+ # Giả sử key nằm ở cột A, từ dòng 2
20
+ GEMINI_KEYS = df.iloc[1:, 0].dropna().astype(str).tolist()
21
+ print(f"✅ Đã load {len(GEMINI_KEYS)} Gemini Keys.")
22
+ except Exception as e:
23
+ print(f"❌ Lỗi load Gemini Keys: {e}")
24
+
25
+ async def generate_content_with_gemini(product_desc):
26
+ """Viết lại nội dung bằng Gemini với cơ chế xoay vòng Key"""
27
+ if not GEMINI_KEYS:
28
+ return product_desc # Fallback nếu lỗi
29
+
30
+ prompt = (
31
+ f"Viết một caption Facebook Reels ngắn gọn, hấp dẫn, bắt trend "
32
+ f"cho sản phẩm sau. Có icon sinh động và kêu gọi mua hàng khéo léo.\n"
33
+ f"Sản phẩm: {product_desc}"
34
+ )
35
+
36
+ # Thử ngẫu nhiên key cho đến khi thành công
37
+ for _ in range(3):
38
+ key = random.choice(GEMINI_KEYS)
39
+ genai.configure(api_key=key)
40
+ try:
41
+ model = genai.GenerativeModel('gemini-1.5-flash')
42
+ response = await model.generate_content_async(prompt)
43
+ return response.text.strip()
44
+ except Exception:
45
+ continue
46
+ return product_desc
47
+
48
+ # --- Hàm Drive (Tái sử dụng code của bạn) ---
49
+ # (Bạn giữ nguyên các hàm get_drive_credentials, download_file_from_drive, upload_file_to_drive cũ)
50
+ # Chỉ thêm hàm download video theo ID:
51
+
52
+ async def download_video_by_id(drive_service, file_id, save_path):
53
+ if os.path.exists(save_path):
54
+ os.remove(save_path) # Xóa file cũ nếu có
55
+ request = drive_service.files().get_media(fileId=file_id)
56
+ fh = io.BytesIO()
57
+ downloader = MediaIoBaseDownload(fh, request)
58
+ done = False
59
+ while not done:
60
+ status, done = downloader.next_chunk()
61
+ with open(save_path, 'wb') as f:
62
+ f.write(fh.getvalue())
63
+ return save_path
main.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
3
+ from apscheduler.triggers.cron import CronTrigger
4
+ import asyncio
5
+ from contextlib import asynccontextmanager
6
+ import config
7
+ import processor
8
+ import google_utils
9
+
10
+ # --- Khởi tạo Scheduler ---
11
+ scheduler = AsyncIOScheduler()
12
+
13
+ async def job_runner(sheet_name):
14
+ """Hàm wrapper để chạy job"""
15
+ # Tìm config dựa trên sheet_name
16
+ page_cfg = next((p for p in config.PAGES_CONFIG if p['sheet_name'] == sheet_name), None)
17
+ if not page_cfg: return
18
+
19
+ # Lấy credentials (dùng lại hàm cũ của bạn)
20
+ # Lưu ý: cần truyền chat_id nếu dùng hàm cũ, hoặc sửa hàm get_drive_credentials để không cần chat_id
21
+ credentials = await google_utils.get_drive_credentials_no_chat_id()
22
+ if credentials:
23
+ await google_utils.load_gemini_keys(config.LOCAL_EXCEL_PATH) # Load key mới nhất
24
+ await processor.process_single_page(page_cfg, credentials)
25
+
26
+ @asynccontextmanager
27
+ async def lifespan(app: FastAPI):
28
+ print("🚀 Bot khởi động...")
29
+
30
+ # 1. Tải file Excel lần đầu để lấy API Key
31
+ # (Thực hiện logic tải file và load key ở đây)
32
+
33
+ # 2. Đăng ký lịch chạy từ Config
34
+ for page in config.PAGES_CONFIG:
35
+ if page['active']:
36
+ for time_str in page['schedule_times']:
37
+ hour, minute = time_str.split(':')
38
+ scheduler.add_job(
39
+ job_runner,
40
+ CronTrigger(hour=int(hour), minute=int(minute)),
41
+ args=[page['sheet_name']],
42
+ name=f"{page['sheet_name']}_{time_str}"
43
+ )
44
+ print(f"📅 Đã hẹn giờ: {page['sheet_name']} lúc {time_str}")
45
+
46
+ scheduler.start()
47
+ yield
48
+ print("🛑 Bot dừng.")
49
+
50
+ app = FastAPI(lifespan=lifespan)
51
+
52
+ @app.get("/")
53
+ def health_check():
54
+ return {"status": "Running", "jobs": [str(j) for j in scheduler.get_jobs()]}
55
+
56
+ # Endpoint để test thủ công 1 page
57
+ @app.post("/trigger/{sheet_name}")
58
+ async def manual_trigger(sheet_name: str):
59
+ await job_runner(sheet_name)
60
+ return {"message": f"Triggered {sheet_name}"}
processor.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import random
3
+ import os
4
+ import asyncio
5
+ from googleapiclient.discovery import build
6
+ from config import LOCAL_EXCEL_PATH, DRIVE_EXCEL_FILE_ID, TEMP_VIDEO_DIR
7
+ import google_utils
8
+ import facebook_utils
9
+
10
+ # Lock để tránh 2 page cùng ghi file Excel 1 lúc
11
+ excel_lock = asyncio.Lock()
12
+
13
+ async def process_single_page(page_config, drive_credentials):
14
+ sheet_name = page_config['sheet_name']
15
+ print(f"🚀 [Processor] Bắt đầu xử lý cho Sheet: {sheet_name}")
16
+
17
+ drive_service = build('drive', 'v3', credentials=drive_credentials)
18
+
19
+ async with excel_lock:
20
+ # 1. Tải file Excel mới nhất về
21
+ await google_utils.download_file_from_drive(drive_service, DRIVE_EXCEL_FILE_ID, LOCAL_EXCEL_PATH)
22
+
23
+ # 2. Đọc dữ liệu
24
+ try:
25
+ df = pd.read_excel(LOCAL_EXCEL_PATH, sheet_name=sheet_name)
26
+ except ValueError:
27
+ print(f"❌ Không tìm thấy sheet {sheet_name}")
28
+ return
29
+
30
+ # 3. Lọc các dòng NYS (Giả sử cột Status là cột E - index 4)
31
+ # Cấu trúc: A:STT, B:Desc, C:Shopee, D:DriveID, E:Status
32
+ # Pandas index cột: 0, 1, 2, 3, 4
33
+
34
+ # Chuẩn hóa tên cột để dễ gọi
35
+ df.columns = ['STT', 'Desc', 'ShopeeLink', 'DriveVideoID', 'Status', 'NewContent'] # Thêm cột NewContent nếu cần
36
+
37
+ nys_rows = df[df['Status'] == 'NYS']
38
+
39
+ if nys_rows.empty:
40
+ print(f"⚠️ Sheet {sheet_name} đã hết bài (không còn NYS).")
41
+ return
42
+
43
+ # 4. Chọn ngẫu nhiên
44
+ selected_row_index = random.choice(nys_rows.index)
45
+ row_data = df.loc[selected_row_index]
46
+
47
+ print(f"--- Đã chọn dòng {selected_row_index + 2} ---")
48
+
49
+ # (Nhả lock Excel ra để xử lý nặng)
50
+
51
+ try:
52
+ # 5. Gemini viết lại nội dung
53
+ original_desc = str(row_data['Desc'])
54
+ new_content = await google_utils.generate_content_with_gemini(original_desc)
55
+
56
+ # 6. Tải video
57
+ video_drive_id = str(row_data['DriveVideoID'])
58
+ if not os.path.exists(TEMP_VIDEO_DIR): os.makedirs(TEMP_VIDEO_DIR)
59
+ local_video_path = os.path.join(TEMP_VIDEO_DIR, f"{sheet_name}_{video_drive_id}.mp4")
60
+
61
+ await google_utils.download_video_by_id(drive_service, video_drive_id, local_video_path)
62
+
63
+ # 7. Upload Facebook
64
+ video_id = await facebook_utils.upload_reel(
65
+ page_config['page_id'],
66
+ page_config['access_token'],
67
+ local_video_path,
68
+ new_content
69
+ )
70
+ print(f"✅ Upload thành công. ID: {video_id}")
71
+
72
+ # 8. Comment
73
+ cta = "🔥 Săn ngay tại đây cả nhà ơi: "
74
+ comment_msg = f"{cta} {row_data['ShopeeLink']}"
75
+ await facebook_utils.comment_on_video(video_id, page_config['access_token'], comment_msg)
76
+
77
+ # 9. Cập nhật Excel (Cần Lock lại)
78
+ async with excel_lock:
79
+ # Đọc lại file để đảm bảo không đè dữ liệu của luồng khác
80
+ # (Trong thực tế nên dùng Google Sheets API update cell trực tiếp sẽ tốt hơn download/upload)
81
+ # Nhưng ở đây ta làm theo logic file Excel local
82
+ df_update = pd.read_excel(LOCAL_EXCEL_PATH, sheet_name=None) # Đọc tất cả sheet
83
+
84
+ # Cập nhật sheet hiện tại
85
+ sheet_df = df_update[sheet_name]
86
+ sheet_df.at[selected_row_index, 'Status'] = 'Uploaded'
87
+ # Giả sử cột F là nội dung mới
88
+ if len(sheet_df.columns) > 5:
89
+ sheet_df.iloc[selected_row_index, 5] = new_content
90
+
91
+ # Lưu và Upload
92
+ with pd.ExcelWriter(LOCAL_EXCEL_PATH, engine='openpyxl') as writer:
93
+ for s_name, s_df in df_update.items():
94
+ s_df.to_excel(writer, sheet_name=s_name, index=False)
95
+
96
+ await google_utils.upload_file_to_drive(drive_service, LOCAL_EXCEL_PATH, DRIVE_EXCEL_FILE_ID)
97
+ print(f"✅ Đã cập nhật Excel lên Drive.")
98
+
99
+ # Dọn dẹp
100
+ if os.path.exists(local_video_path): os.remove(local_video_path)
101
+
102
+ except Exception as e:
103
+ print(f"❌ Lỗi xử lý Page {sheet_name}: {e}")