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] 3 weeks 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( subprocess.run(
[ [
"ffmpeg", "-f", "concat", "-y", "ffmpeg",
"-hide_banner", "-loglevel", "panic", "-f",
"-safe", "0", "concat",
"-i", f"{self.path}/list.txt", "-y",
"-c", "copy", "-hide_banner",
"-loglevel",
"panic",
"-safe",
"0",
"-i",
f"{self.path}/list.txt",
"-c",
"copy",
f"{self.path}/{idx}.mp3", f"{self.path}/{idx}.mp3",
], ],
check=False, check=False,

@ -35,8 +35,7 @@ from utils.id import extract_id
__VERSION__ = "4.0.0" __VERSION__ = "4.0.0"
print( print("""
"""
@ -50,8 +49,7 @@ print(
Auto-post: TikTok | YouTube | Facebook Auto-post: TikTok | YouTube | Facebook
""" """)
)
print_markdown( print_markdown(
"### 🇻🇳 Threads Video Maker Bot - Phiên bản Việt Nam\n" "### 🇻🇳 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" "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 # Lưu title vào lịch sử để tránh tạo trùng lặp
from utils.title_history import save_title from utils.title_history import save_title
title = thread_object.get("thread_title", "") title = thread_object.get("thread_title", "")
tid = thread_object.get("thread_id", "") tid = thread_object.get("thread_id", "")
if title: if title:
@ -106,6 +105,7 @@ def main_threads(POST_ID=None) -> None:
def main_threads_with_upload(POST_ID=None) -> None: def main_threads_with_upload(POST_ID=None) -> None:
"""Pipeline đầy đủ: Threads → Video → Upload lên các platform.""" """Pipeline đầy đủ: Threads → Video → Upload lên các platform."""
from scheduler.pipeline import run_pipeline from scheduler.pipeline import run_pipeline
run_pipeline(POST_ID) run_pipeline(POST_ID)
@ -160,6 +160,7 @@ def shutdown() -> NoReturn:
def parse_args(): def parse_args():
"""Parse command line arguments.""" """Parse command line arguments."""
import argparse import argparse
parser = argparse.ArgumentParser(description="Threads Video Maker Bot - Vietnam Edition") parser = argparse.ArgumentParser(description="Threads Video Maker Bot - Vietnam Edition")
parser.add_argument( parser.add_argument(
"--mode", "--mode",
@ -177,9 +178,7 @@ def parse_args():
if __name__ == "__main__": if __name__ == "__main__":
if sys.version_info.major != 3 or sys.version_info.minor not in [10, 11, 12]: if sys.version_info.major != 3 or sys.version_info.minor not in [10, 11, 12]:
print( 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.")
"Ứ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() sys.exit()
args = parse_args() args = parse_args()
@ -191,9 +190,9 @@ if __name__ == "__main__":
config is False and sys.exit() config is False and sys.exit()
# Kiểm tra TikTok TTS session # Kiểm tra TikTok TTS session
if ( if (not settings.config["settings"]["tts"].get("tiktok_sessionid", "")) and config["settings"][
not settings.config["settings"]["tts"].get("tiktok_sessionid", "") "tts"
) and config["settings"]["tts"]["voice_choice"] == "tiktok": ]["voice_choice"] == "tiktok":
print_substep( print_substep(
"TikTok TTS cần sessionid! Xem tài liệu để biết cách lấy.", "TikTok TTS cần sessionid! Xem tài liệu để biết cách lấy.",
"bold red", "bold red",
@ -205,6 +204,7 @@ if __name__ == "__main__":
# Chế độ lên lịch tự động # Chế độ lên lịch tự động
print_step("🕐 Khởi động 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 from scheduler.pipeline import run_scheduled
run_scheduled() run_scheduled()
elif args.mode == "auto": elif args.mode == "auto":
@ -234,9 +234,12 @@ if __name__ == "__main__":
if args.reddit: if args.reddit:
# Legacy Reddit mode # Legacy Reddit mode
from prawcore import ResponseException from prawcore import ResponseException
try: try:
if config["reddit"]["thread"]["post_id"]: 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 index += 1
print_step(f"Đang xử lý post {index}...") print_step(f"Đang xử lý post {index}...")
main_reddit(post_id) 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) make_final_video(number_of_comments, length, thread_object, bg_config)
# Tìm file video đã tạo # 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}" results_dir = f"./results/{subreddit}"
video_path = None video_path = None
if os.path.exists(results_dir): 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) # Step 6: Upload (nếu cấu hình)
upload_config = settings.config.get("uploaders", {}) upload_config = settings.config.get("uploaders", {})
has_uploaders = any( has_uploaders = any(
upload_config.get(p, {}).get("enabled", False) upload_config.get(p, {}).get("enabled", False) for p in ["youtube", "tiktok", "facebook"]
for p in ["youtube", "tiktok", "facebook"]
) )
if has_uploaders and video_path: if has_uploaders and video_path:
@ -166,13 +167,17 @@ def run_scheduled():
return return
timezone = scheduler_config.get("timezone", "Asia/Ho_Chi_Minh") 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) max_videos_per_day = scheduler_config.get("max_videos_per_day", 8)
# Parse cron expression # Parse cron expression
cron_parts = cron_expression.split() cron_parts = cron_expression.split()
if len(cron_parts) != 5: 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 return
scheduler = BlockingScheduler(timezone=timezone) scheduler = BlockingScheduler(timezone=timezone)

@ -16,7 +16,6 @@ from utils.title_history import is_title_used
from utils.videos import check_done from utils.videos import check_done
from utils.voice import sanitize_text from utils.voice import sanitize_text
THREADS_API_BASE = "https://graph.threads.net/v1.0" 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.access_token = settings.config["threads"]["creds"]["access_token"]
self.user_id = settings.config["threads"]["creds"]["user_id"] self.user_id = settings.config["threads"]["creds"]["user_id"]
self.session = requests.Session() self.session = requests.Session()
self.session.headers.update({ self.session.headers.update(
"Authorization": f"Bearer {self.access_token}", {
}) "Authorization": f"Bearer {self.access_token}",
}
)
def _get(self, endpoint: str, params: Optional[dict] = None) -> dict: def _get(self, endpoint: str, params: Optional[dict] = None) -> dict:
"""Make a GET request to the Threads API.""" """Make a GET request to the Threads API."""
@ -52,10 +53,13 @@ class ThreadsClient:
Danh sách các thread objects. Danh sách các thread objects.
""" """
uid = user_id or self.user_id uid = user_id or self.user_id
data = self._get(f"{uid}/threads", params={ data = self._get(
"fields": "id,media_type,media_url,permalink,text,timestamp,username,shortcode,is_reply,reply_audience", f"{uid}/threads",
"limit": limit, params={
}) "fields": "id,media_type,media_url,permalink,text,timestamp,username,shortcode,is_reply,reply_audience",
"limit": limit,
},
)
return data.get("data", []) return data.get("data", [])
def get_thread_replies(self, thread_id: str, limit: int = 50) -> List[dict]: def get_thread_replies(self, thread_id: str, limit: int = 50) -> List[dict]:
@ -68,11 +72,14 @@ class ThreadsClient:
Returns: Returns:
Danh sách replies. Danh sách replies.
""" """
data = self._get(f"{thread_id}/replies", params={ data = self._get(
"fields": "id,text,timestamp,username,permalink,hide_status", f"{thread_id}/replies",
"limit": limit, params={
"reverse": "true", "fields": "id,text,timestamp,username,permalink,hide_status",
}) "limit": limit,
"reverse": "true",
},
)
return data.get("data", []) return data.get("data", [])
def get_thread_by_id(self, thread_id: str) -> dict: def get_thread_by_id(self, thread_id: str) -> dict:
@ -84,9 +91,12 @@ class ThreadsClient:
Returns: Returns:
Thread object. Thread object.
""" """
return self._get(thread_id, params={ return self._get(
"fields": "id,media_type,media_url,permalink,text,timestamp,username,shortcode", 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]: def search_threads_by_keyword(self, threads: List[dict], keywords: List[str]) -> List[dict]:
"""Lọc threads theo từ khóa. """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_id = thread.get("id", "")
thread_text = thread.get("text", "") 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") thread_username = thread.get("username", "unknown")
print_substep(f"Video sẽ được tạo từ: {thread_text[:100]}...", style="bold green") 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: if len(reply_text) < min_comment_length:
continue continue
content["comments"].append({ content["comments"].append(
"comment_body": reply_text, {
"comment_url": reply.get("permalink", ""), "comment_body": reply_text,
"comment_id": re.sub(r"[^\w\s-]", "", reply.get("id", "")), "comment_url": reply.get("permalink", ""),
"comment_author": f"@{reply_username}", "comment_id": re.sub(r"[^\w\s-]", "", reply.get("id", "")),
}) "comment_author": f"@{reply_username}",
}
)
print_substep( print_substep(
f"Đã lấy nội dung từ Threads thành công! ({len(content.get('comments', []))} replies)", 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 import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, List from typing import List, Optional
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
@ -14,6 +14,7 @@ from utils.console import print_step, print_substep
@dataclass @dataclass
class VideoMetadata: class VideoMetadata:
"""Metadata cho video cần upload.""" """Metadata cho video cần upload."""
file_path: str file_path: str
title: str title: str
description: str = "" description: str = ""
@ -65,12 +66,16 @@ class BaseUploader(ABC):
True nếu hợp lệ. True nếu hợp lệ.
""" """
if not os.path.exists(metadata.file_path): 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 return False
file_size = os.path.getsize(metadata.file_path) file_size = os.path.getsize(metadata.file_path)
if file_size == 0: 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 return False
if not metadata.title: if not metadata.title:
@ -114,9 +119,11 @@ class BaseUploader(ABC):
style="bold red", style="bold red",
) )
if attempt < max_retries: 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") print_substep(f"Chờ {backoff}s trước khi thử lại...", style="bold yellow")
time.sleep(backoff) 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 return None

@ -70,7 +70,10 @@ class FacebookUploader(BaseUploader):
if "id" in data: if "id" in data:
self._authenticated = True 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 return True
else: else:
print_substep("Facebook: Token không hợp lệ", style="bold red") 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) 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) description = self._build_description(metadata)
# Step 1: Initialize upload session # Step 1: Initialize upload session
@ -163,7 +166,7 @@ class FacebookUploader(BaseUploader):
"upload_session_id": upload_session_id, "upload_session_id": upload_session_id,
"access_token": self.access_token, "access_token": self.access_token,
"title": title, "title": title,
"description": description[:self.MAX_DESCRIPTION_LENGTH], "description": description[: self.MAX_DESCRIPTION_LENGTH],
} }
if metadata.schedule_time: if metadata.schedule_time:

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

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

@ -7,8 +7,8 @@ Yêu cầu:
- Scopes: https://www.googleapis.com/auth/youtube.upload - Scopes: https://www.googleapis.com/auth/youtube.upload
""" """
import os
import json import json
import os
import time import time
from typing import Optional from typing import Optional
@ -63,12 +63,16 @@ class YouTubeUploader(BaseUploader):
return False return False
try: try:
response = requests.post(self.TOKEN_URL, data={ response = requests.post(
"client_id": client_id, self.TOKEN_URL,
"client_secret": client_secret, data={
"refresh_token": refresh_token, "client_id": client_id,
"grant_type": "refresh_token", "client_secret": client_secret,
}, timeout=30) "refresh_token": refresh_token,
"grant_type": "refresh_token",
},
timeout=30,
)
response.raise_for_status() response.raise_for_status()
token_data = response.json() token_data = response.json()
@ -93,7 +97,7 @@ class YouTubeUploader(BaseUploader):
if not self.access_token: if not self.access_token:
return None return None
title = metadata.title[:self.MAX_TITLE_LENGTH] title = metadata.title[: self.MAX_TITLE_LENGTH]
description = self._build_description(metadata) description = self._build_description(metadata)
tags = metadata.tags or [] tags = metadata.tags or []
@ -106,7 +110,7 @@ class YouTubeUploader(BaseUploader):
video_metadata = { video_metadata = {
"snippet": { "snippet": {
"title": title, "title": title,
"description": description[:self.MAX_DESCRIPTION_LENGTH], "description": description[: self.MAX_DESCRIPTION_LENGTH],
"tags": tags, "tags": tags,
"categoryId": self._get_category_id(metadata.category), "categoryId": self._get_category_id(metadata.category),
"defaultLanguage": metadata.language, "defaultLanguage": metadata.language,
@ -168,7 +172,9 @@ class YouTubeUploader(BaseUploader):
video_id = video_data.get("id", "") video_id = video_data.get("id", "")
if not video_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 return None
# Step 3: Upload thumbnail if available # Step 3: Upload thumbnail if available

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

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

Loading…
Cancel
Save