From 2a9ca5e6c07b406de29db4affc122c6de08f8a47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:22:17 +0000 Subject: [PATCH] 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> --- TTS/engine_wrapper.py | 18 ++++++--- main.py | 25 +++++++----- scheduler/pipeline.py | 15 ++++--- threads/threads_client.py | 60 +++++++++++++++++----------- uploaders/base_uploader.py | 17 +++++--- uploaders/facebook_uploader.py | 9 +++-- uploaders/tiktok_uploader.py | 18 +++++---- uploaders/upload_manager.py | 4 +- uploaders/youtube_uploader.py | 26 +++++++----- utils/settings.py | 18 +++------ video_creation/threads_screenshot.py | 15 +++---- 11 files changed, 133 insertions(+), 92 deletions(-) diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index 9020da9..0699bb9 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -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, diff --git a/main.py b/main.py index 320dd8d..138714d 100755 --- a/main.py +++ b/main.py @@ -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) diff --git a/scheduler/pipeline.py b/scheduler/pipeline.py index f98e477..ef520f8 100644 --- a/scheduler/pipeline.py +++ b/scheduler/pipeline.py @@ -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) diff --git a/threads/threads_client.py b/threads/threads_client.py index dc42cf8..444195b 100644 --- a/threads/threads_client.py +++ b/threads/threads_client.py @@ -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)", diff --git a/uploaders/base_uploader.py b/uploaders/base_uploader.py index e67b0f1..8dea3d7 100644 --- a/uploaders/base_uploader.py +++ b/uploaders/base_uploader.py @@ -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 diff --git a/uploaders/facebook_uploader.py b/uploaders/facebook_uploader.py index 527b7d1..26ae0d1 100644 --- a/uploaders/facebook_uploader.py +++ b/uploaders/facebook_uploader.py @@ -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: diff --git a/uploaders/tiktok_uploader.py b/uploaders/tiktok_uploader.py index 561c44c..40abe69 100644 --- a/uploaders/tiktok_uploader.py +++ b/uploaders/tiktok_uploader.py @@ -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: diff --git a/uploaders/upload_manager.py b/uploaders/upload_manager.py index e6c46e4..9b8c63c 100644 --- a/uploaders/upload_manager.py +++ b/uploaders/upload_manager.py @@ -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 diff --git a/uploaders/youtube_uploader.py b/uploaders/youtube_uploader.py index 10b4a82..c4a05e1 100644 --- a/uploaders/youtube_uploader.py +++ b/uploaders/youtube_uploader.py @@ -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 diff --git a/utils/settings.py b/utils/settings.py index 085a963..c9a0905 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -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) diff --git a/video_creation/threads_screenshot.py b/video_creation/threads_screenshot.py index af42728..f44fbf8 100644 --- a/video_creation/threads_screenshot.py +++ b/video_creation/threads_screenshot.py @@ -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