Fix Black/isort formatting to pass lint CI checks

Agent-Logs-Url: https://github.com/thaitien280401-stack/RedditVideoMakerBot/sessions/29d5b341-a15e-4235-bb26-8e2de812f684

Co-authored-by: thaitien280401-stack <271128961+thaitien280401-stack@users.noreply.github.com>
pull/2482/head
copilot-swe-agent[bot] 4 days ago committed by GitHub
parent bbcd520fbb
commit 2a9ca5e6c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -130,11 +130,19 @@ class TTSEngine:
subprocess.run(
[
"ffmpeg", "-f", "concat", "-y",
"-hide_banner", "-loglevel", "panic",
"-safe", "0",
"-i", f"{self.path}/list.txt",
"-c", "copy",
"ffmpeg",
"-f",
"concat",
"-y",
"-hide_banner",
"-loglevel",
"panic",
"-safe",
"0",
"-i",
f"{self.path}/list.txt",
"-c",
"copy",
f"{self.path}/{idx}.mp3",
],
check=False,

@ -35,8 +35,7 @@ from utils.id import extract_id
__VERSION__ = "4.0.0"
print(
"""
print("""
@ -50,8 +49,7 @@ print(
Auto-post: TikTok | YouTube | Facebook
"""
)
""")
print_markdown(
"### 🇻🇳 Threads Video Maker Bot - Phiên bản Việt Nam\n"
"Tạo video tự động từ nội dung Threads và đăng lên TikTok, YouTube, Facebook.\n"
@ -97,6 +95,7 @@ def main_threads(POST_ID=None) -> None:
# Lưu title vào lịch sử để tránh tạo trùng lặp
from utils.title_history import save_title
title = thread_object.get("thread_title", "")
tid = thread_object.get("thread_id", "")
if title:
@ -106,6 +105,7 @@ def main_threads(POST_ID=None) -> None:
def main_threads_with_upload(POST_ID=None) -> None:
"""Pipeline đầy đủ: Threads → Video → Upload lên các platform."""
from scheduler.pipeline import run_pipeline
run_pipeline(POST_ID)
@ -160,6 +160,7 @@ def shutdown() -> NoReturn:
def parse_args():
"""Parse command line arguments."""
import argparse
parser = argparse.ArgumentParser(description="Threads Video Maker Bot - Vietnam Edition")
parser.add_argument(
"--mode",
@ -177,9 +178,7 @@ def parse_args():
if __name__ == "__main__":
if sys.version_info.major != 3 or sys.version_info.minor not in [10, 11, 12]:
print(
"Ứng dụng yêu cầu Python 3.10, 3.11 hoặc 3.12. Vui lòng cài đặt phiên bản phù hợp."
)
print("Ứng dụng yêu cầu Python 3.10, 3.11 hoặc 3.12. Vui lòng cài đặt phiên bản phù hợp.")
sys.exit()
args = parse_args()
@ -191,9 +190,9 @@ if __name__ == "__main__":
config is False and sys.exit()
# Kiểm tra TikTok TTS session
if (
not settings.config["settings"]["tts"].get("tiktok_sessionid", "")
) and config["settings"]["tts"]["voice_choice"] == "tiktok":
if (not settings.config["settings"]["tts"].get("tiktok_sessionid", "")) and config["settings"][
"tts"
]["voice_choice"] == "tiktok":
print_substep(
"TikTok TTS cần sessionid! Xem tài liệu để biết cách lấy.",
"bold red",
@ -205,6 +204,7 @@ if __name__ == "__main__":
# Chế độ lên lịch tự động
print_step("🕐 Khởi động chế độ lên lịch tự động...")
from scheduler.pipeline import run_scheduled
run_scheduled()
elif args.mode == "auto":
@ -234,9 +234,12 @@ if __name__ == "__main__":
if args.reddit:
# Legacy Reddit mode
from prawcore import ResponseException
try:
if config["reddit"]["thread"]["post_id"]:
for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")):
for index, post_id in enumerate(
config["reddit"]["thread"]["post_id"].split("+")
):
index += 1
print_step(f"Đang xử lý post {index}...")
main_reddit(post_id)

@ -81,7 +81,9 @@ def run_pipeline(post_id: Optional[str] = None) -> Optional[str]:
make_final_video(number_of_comments, length, thread_object, bg_config)
# Tìm file video đã tạo
subreddit = settings.config.get("threads", {}).get("thread", {}).get("channel_name", "threads")
subreddit = (
settings.config.get("threads", {}).get("thread", {}).get("channel_name", "threads")
)
results_dir = f"./results/{subreddit}"
video_path = None
if os.path.exists(results_dir):
@ -96,8 +98,7 @@ def run_pipeline(post_id: Optional[str] = None) -> Optional[str]:
# Step 6: Upload (nếu cấu hình)
upload_config = settings.config.get("uploaders", {})
has_uploaders = any(
upload_config.get(p, {}).get("enabled", False)
for p in ["youtube", "tiktok", "facebook"]
upload_config.get(p, {}).get("enabled", False) for p in ["youtube", "tiktok", "facebook"]
)
if has_uploaders and video_path:
@ -166,13 +167,17 @@ def run_scheduled():
return
timezone = scheduler_config.get("timezone", "Asia/Ho_Chi_Minh")
cron_expression = scheduler_config.get("cron", "0 */3 * * *") # Mặc định mỗi 3 giờ (8 lần/ngày: 00, 03, 06, 09, 12, 15, 18, 21h)
cron_expression = scheduler_config.get(
"cron", "0 */3 * * *"
) # Mặc định mỗi 3 giờ (8 lần/ngày: 00, 03, 06, 09, 12, 15, 18, 21h)
max_videos_per_day = scheduler_config.get("max_videos_per_day", 8)
# Parse cron expression
cron_parts = cron_expression.split()
if len(cron_parts) != 5:
print_substep("Cron expression không hợp lệ! Format: minute hour day month weekday", style="bold red")
print_substep(
"Cron expression không hợp lệ! Format: minute hour day month weekday", style="bold red"
)
return
scheduler = BlockingScheduler(timezone=timezone)

@ -16,7 +16,6 @@ from utils.title_history import is_title_used
from utils.videos import check_done
from utils.voice import sanitize_text
THREADS_API_BASE = "https://graph.threads.net/v1.0"
@ -27,9 +26,11 @@ class ThreadsClient:
self.access_token = settings.config["threads"]["creds"]["access_token"]
self.user_id = settings.config["threads"]["creds"]["user_id"]
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {self.access_token}",
})
self.session.headers.update(
{
"Authorization": f"Bearer {self.access_token}",
}
)
def _get(self, endpoint: str, params: Optional[dict] = None) -> dict:
"""Make a GET request to the Threads API."""
@ -52,10 +53,13 @@ class ThreadsClient:
Danh sách các thread objects.
"""
uid = user_id or self.user_id
data = self._get(f"{uid}/threads", params={
"fields": "id,media_type,media_url,permalink,text,timestamp,username,shortcode,is_reply,reply_audience",
"limit": limit,
})
data = self._get(
f"{uid}/threads",
params={
"fields": "id,media_type,media_url,permalink,text,timestamp,username,shortcode,is_reply,reply_audience",
"limit": limit,
},
)
return data.get("data", [])
def get_thread_replies(self, thread_id: str, limit: int = 50) -> List[dict]:
@ -68,11 +72,14 @@ class ThreadsClient:
Returns:
Danh sách replies.
"""
data = self._get(f"{thread_id}/replies", params={
"fields": "id,text,timestamp,username,permalink,hide_status",
"limit": limit,
"reverse": "true",
})
data = self._get(
f"{thread_id}/replies",
params={
"fields": "id,text,timestamp,username,permalink,hide_status",
"limit": limit,
"reverse": "true",
},
)
return data.get("data", [])
def get_thread_by_id(self, thread_id: str) -> dict:
@ -84,9 +91,12 @@ class ThreadsClient:
Returns:
Thread object.
"""
return self._get(thread_id, params={
"fields": "id,media_type,media_url,permalink,text,timestamp,username,shortcode",
})
return self._get(
thread_id,
params={
"fields": "id,media_type,media_url,permalink,text,timestamp,username,shortcode",
},
)
def search_threads_by_keyword(self, threads: List[dict], keywords: List[str]) -> List[dict]:
"""Lọc threads theo từ khóa.
@ -194,7 +204,9 @@ def get_threads_posts(POST_ID: str = None) -> dict:
thread_id = thread.get("id", "")
thread_text = thread.get("text", "")
thread_url = thread.get("permalink", f"https://www.threads.net/post/{thread.get('shortcode', '')}")
thread_url = thread.get(
"permalink", f"https://www.threads.net/post/{thread.get('shortcode', '')}"
)
thread_username = thread.get("username", "unknown")
print_substep(f"Video sẽ được tạo từ: {thread_text[:100]}...", style="bold green")
@ -240,12 +252,14 @@ def get_threads_posts(POST_ID: str = None) -> dict:
if len(reply_text) < min_comment_length:
continue
content["comments"].append({
"comment_body": reply_text,
"comment_url": reply.get("permalink", ""),
"comment_id": re.sub(r"[^\w\s-]", "", reply.get("id", "")),
"comment_author": f"@{reply_username}",
})
content["comments"].append(
{
"comment_body": reply_text,
"comment_url": reply.get("permalink", ""),
"comment_id": re.sub(r"[^\w\s-]", "", reply.get("id", "")),
"comment_author": f"@{reply_username}",
}
)
print_substep(
f"Đã lấy nội dung từ Threads thành công! ({len(content.get('comments', []))} replies)",

@ -6,7 +6,7 @@ import os
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional, List
from typing import List, Optional
from utils.console import print_step, print_substep
@ -14,6 +14,7 @@ from utils.console import print_step, print_substep
@dataclass
class VideoMetadata:
"""Metadata cho video cần upload."""
file_path: str
title: str
description: str = ""
@ -65,12 +66,16 @@ class BaseUploader(ABC):
True nếu hợp lệ.
"""
if not os.path.exists(metadata.file_path):
print_substep(f"[{self.platform_name}] File không tồn tại: {metadata.file_path}", style="bold red")
print_substep(
f"[{self.platform_name}] File không tồn tại: {metadata.file_path}", style="bold red"
)
return False
file_size = os.path.getsize(metadata.file_path)
if file_size == 0:
print_substep(f"[{self.platform_name}] File rỗng: {metadata.file_path}", style="bold red")
print_substep(
f"[{self.platform_name}] File rỗng: {metadata.file_path}", style="bold red"
)
return False
if not metadata.title:
@ -114,9 +119,11 @@ class BaseUploader(ABC):
style="bold red",
)
if attempt < max_retries:
backoff = min(2 ** attempt, 60) # Exponential backoff, max 60s
backoff = min(2**attempt, 60) # Exponential backoff, max 60s
print_substep(f"Chờ {backoff}s trước khi thử lại...", style="bold yellow")
time.sleep(backoff)
print_substep(f"Upload {self.platform_name} thất bại sau {max_retries} lần thử!", style="bold red")
print_substep(
f"Upload {self.platform_name} thất bại sau {max_retries} lần thử!", style="bold red"
)
return None

@ -70,7 +70,10 @@ class FacebookUploader(BaseUploader):
if "id" in data:
self._authenticated = True
print_substep(f"Facebook: Xác thực thành công (Page: {data.get('name', self.page_id)}) ✅", style="bold green")
print_substep(
f"Facebook: Xác thực thành công (Page: {data.get('name', self.page_id)}) ✅",
style="bold green",
)
return True
else:
print_substep("Facebook: Token không hợp lệ", style="bold red")
@ -96,7 +99,7 @@ class FacebookUploader(BaseUploader):
file_size = os.path.getsize(metadata.file_path)
title = metadata.title[:self.MAX_TITLE_LENGTH]
title = metadata.title[: self.MAX_TITLE_LENGTH]
description = self._build_description(metadata)
# Step 1: Initialize upload session
@ -163,7 +166,7 @@ class FacebookUploader(BaseUploader):
"upload_session_id": upload_session_id,
"access_token": self.access_token,
"title": title,
"description": description[:self.MAX_DESCRIPTION_LENGTH],
"description": description[: self.MAX_DESCRIPTION_LENGTH],
}
if metadata.schedule_time:

@ -58,12 +58,16 @@ class TikTokUploader(BaseUploader):
return False
try:
response = requests.post(self.TOKEN_URL, json={
"client_key": client_key,
"client_secret": client_secret,
"grant_type": "refresh_token",
"refresh_token": refresh_token,
}, timeout=30)
response = requests.post(
self.TOKEN_URL,
json={
"client_key": client_key,
"client_secret": client_secret,
"grant_type": "refresh_token",
"refresh_token": refresh_token,
},
timeout=30,
)
response.raise_for_status()
token_data = response.json()
@ -209,7 +213,7 @@ class TikTokUploader(BaseUploader):
hashtag_str = " ".join(f"#{tag}" for tag in metadata.hashtags)
parts.append(hashtag_str)
caption = " ".join(parts)
return caption[:self.MAX_CAPTION_LENGTH]
return caption[: self.MAX_CAPTION_LENGTH]
@staticmethod
def _map_privacy(privacy: str) -> str:

@ -6,9 +6,9 @@ import os
from typing import Dict, List, Optional
from uploaders.base_uploader import BaseUploader, VideoMetadata
from uploaders.youtube_uploader import YouTubeUploader
from uploaders.tiktok_uploader import TikTokUploader
from uploaders.facebook_uploader import FacebookUploader
from uploaders.tiktok_uploader import TikTokUploader
from uploaders.youtube_uploader import YouTubeUploader
from utils import settings
from utils.console import print_step, print_substep

@ -7,8 +7,8 @@ Yêu cầu:
- Scopes: https://www.googleapis.com/auth/youtube.upload
"""
import os
import json
import os
import time
from typing import Optional
@ -63,12 +63,16 @@ class YouTubeUploader(BaseUploader):
return False
try:
response = requests.post(self.TOKEN_URL, data={
"client_id": client_id,
"client_secret": client_secret,
"refresh_token": refresh_token,
"grant_type": "refresh_token",
}, timeout=30)
response = requests.post(
self.TOKEN_URL,
data={
"client_id": client_id,
"client_secret": client_secret,
"refresh_token": refresh_token,
"grant_type": "refresh_token",
},
timeout=30,
)
response.raise_for_status()
token_data = response.json()
@ -93,7 +97,7 @@ class YouTubeUploader(BaseUploader):
if not self.access_token:
return None
title = metadata.title[:self.MAX_TITLE_LENGTH]
title = metadata.title[: self.MAX_TITLE_LENGTH]
description = self._build_description(metadata)
tags = metadata.tags or []
@ -106,7 +110,7 @@ class YouTubeUploader(BaseUploader):
video_metadata = {
"snippet": {
"title": title,
"description": description[:self.MAX_DESCRIPTION_LENGTH],
"description": description[: self.MAX_DESCRIPTION_LENGTH],
"tags": tags,
"categoryId": self._get_category_id(metadata.category),
"defaultLanguage": metadata.language,
@ -168,7 +172,9 @@ class YouTubeUploader(BaseUploader):
video_id = video_data.get("id", "")
if not video_id:
print_substep("YouTube: Upload thành công nhưng không lấy được video ID", style="bold yellow")
print_substep(
"YouTube: Upload thành công nhưng không lấy được video ID", style="bold yellow"
)
return None
# Step 3: Upload thumbnail if available

@ -152,10 +152,8 @@ def check_toml(template_file, config_file) -> Tuple[bool, Dict]:
try:
config = toml.load(config_file)
except toml.TomlDecodeError:
console.print(
f"""[blue]Couldn't read {config_file}.
Overwrite it?(y/n)"""
)
console.print(f"""[blue]Couldn't read {config_file}.
Overwrite it?(y/n)""")
if not input().startswith("y"):
print("Unable to read config, and not allowed to overwrite it. Giving up.")
return False
@ -169,10 +167,8 @@ Overwrite it?(y/n)"""
)
return False
except FileNotFoundError:
console.print(
f"""[blue]Couldn't find {config_file}
Creating it now."""
)
console.print(f"""[blue]Couldn't find {config_file}
Creating it now.""")
try:
with open(config_file, "x") as f:
f.write("")
@ -183,16 +179,14 @@ Creating it now."""
)
return False
console.print(
"""\
console.print("""\
[blue bold]###############################
# #
# Checking TOML configuration #
# #
###############################
If you see any prompts, that means that you have unset/incorrectly set variables, please input the correct values.\
"""
)
""")
crawl(template, check_vars)
with open(config_file, "w") as f:
toml.dump(config, f)

@ -17,7 +17,6 @@ from rich.progress import track
from utils import settings
from utils.console import print_step, print_substep
# Threads color themes
THEMES = {
"dark": {
@ -42,11 +41,11 @@ THEMES = {
# Avatar color palette for comments
AVATAR_COLORS = [
(88, 101, 242), # Blue
(237, 66, 69), # Red
(87, 242, 135), # Green
(254, 231, 92), # Yellow
(235, 69, 158), # Pink
(88, 101, 242), # Blue
(237, 66, 69), # Red
(87, 242, 135), # Green
(254, 231, 92), # Yellow
(235, 69, 158), # Pink
]
@ -329,9 +328,7 @@ def get_screenshots_of_threads_posts(thread_object: dict, screenshot_num: int):
else:
# Comment mode - tạo hình cho từng reply
comments = thread_object.get("comments", [])[:screenshot_num]
for idx, comment in enumerate(
track(comments, "Đang tạo hình ảnh replies...")
):
for idx, comment in enumerate(track(comments, "Đang tạo hình ảnh replies...")):
if idx >= screenshot_num:
break

Loading…
Cancel
Save