diff --git a/nodes.py b/nodes.py index 73a800f..d7107e0 100644 --- a/nodes.py +++ b/nodes.py @@ -1,6 +1,7 @@ import os import json import requests +import time from datetime import datetime, timedelta from PIL import Image import numpy as np @@ -545,16 +546,243 @@ class PublishThread: traceback.print_exc() return None +class ThreadPublishVideo: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"multiline": True, "default": ""}), + "video_path": ("STRING", {"multiline": False, "default": ""}), + }, + "optional": { + "ComfyUIHttpsURL": ("STRING", {"multiline": False, "default": ""}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("result",) + FUNCTION = "publish_video" + CATEGORY = "ComfyUI-Thread" + + def publish_video(self, text, video_path, ComfyUIHttpsURL=""): + try: + # 讀取配置 + if not os.path.exists(CONFIG_FILE): + return ("錯誤: 請先執行 StartWithLongLiveToken 節點",) + + with open(CONFIG_FILE, 'r', encoding='utf-8') as f: + config = json.load(f) + + threads_api = ThreadsAPI( + config["USER_ID"], + config["ACCESS_TOKEN"], + config["APP_SECRET"] + ) + + # 處理 ComfyUI URL 配置 + base_url = load_base_url() + if ComfyUIHttpsURL.strip(): + # 更新 base URL + base_url = ComfyUIHttpsURL.strip() + save_base_url(base_url) + print(f"更新基礎網址為: {base_url}") + + print(f"使用基礎網址: {base_url}") + + # 檢查是否提供了視頻路徑/網址 + if not video_path.strip(): + return ("錯誤: 請提供視頻路徑或網址",) + + # 自動判斷是本地路徑還是網路網址 + video_path_clean = video_path.strip() + final_video_url = "" + + if self._is_url(video_path_clean): + # 網路網址 + print(f"檢測到網路網址: {video_path_clean}") + final_video_url = video_path_clean + else: + # 本地路徑 + print(f"檢測到本地路徑: {video_path_clean}") + final_video_url = self._process_local_video(video_path_clean, base_url) + if not final_video_url: + return ("錯誤: 無法處理本地視頻文件",) + + # 驗證最終的視頻 URL + if not (final_video_url.startswith('http://') or final_video_url.startswith('https://')): + return ("錯誤: 無法生成有效的視頻網址",) + + print(f"開始處理視頻發布...") + print(f"最終視頻網址: {final_video_url}") + print(f"文字內容: {text}") + + # 步驟 1: 創建視頻媒體容器 + print("正在創建視頻媒體容器...") + media = threads_api.create_media_container( + text=text, + media_type="VIDEO", + video_url=final_video_url + ) + + media_id = media["id"] + print(f"視頻媒體容器已創建,ID: {media_id}") + + # 步驟 2: 檢查媒體容器狀態 + print("開始檢查媒體容器處理狀態...") + max_attempts = 30 # 最多檢查 30 次 (10 分鐘) + check_interval = 20 # 每 20 秒檢查一次 + + for attempt in range(max_attempts): + print(f"第 {attempt + 1} 次檢查媒體容器狀態...") + + # 獲取媒體容器狀態 + status_response = self._check_media_status(threads_api, media_id, config["ACCESS_TOKEN"]) + + if status_response: + status = status_response.get("status") + print(f"媒體容器狀態: {status}") + + if status == "FINISHED": + print("媒體容器處理完成,準備發布...") + break + elif status == "ERROR": + error_message = status_response.get("error_message", "未知錯誤") + return (f"錯誤: 媒體容器處理失敗 - {error_message}",) + elif status in ["IN_PROGRESS", "PUBLISHED"]: + if attempt < max_attempts - 1: + print(f"媒體容器仍在處理中,{check_interval} 秒後重新檢查...") + time.sleep(check_interval) + else: + return ("錯誤: 媒體容器處理超時,請稍後再試",) + else: + print(f"未知狀態: {status},繼續等待...") + if attempt < max_attempts - 1: + time.sleep(check_interval) + else: + print("無法獲取媒體容器狀態,繼續等待...") + if attempt < max_attempts - 1: + time.sleep(check_interval) + else: + return ("錯誤: 媒體容器處理超時,請檢查視頻是否符合 Threads 規格要求",) + + # 步驟 3: 發布視頻 + print("正在發布視頻...") + result = threads_api.publish_container(media_id) + + # 取得用戶名稱來組合網址 + user_info = threads_api.get_user_bio() + username = user_info.get("username", "") + thread_id = result["id"] + + post_url = f"https://www.threads.net/@{username}/post/{thread_id}" + + return (f"視頻發送成功!貼文網址: {post_url}",) + + except Exception as e: + print(f"視頻發布錯誤: {str(e)}") + import traceback + traceback.print_exc() + return (f"錯誤: {str(e)}",) + + def _is_url(self, path): + """判斷輸入是否為網路網址""" + return path.startswith('http://') or path.startswith('https://') + + def _process_local_video(self, video_path, base_url): + """處理本地視頻文件,拷貝到 ComfyUI output 目錄並生成 URL""" + try: + import shutil + + # 檢查本地文件是否存在 + if not os.path.exists(video_path): + print(f"錯誤: 本地視頻文件不存在: {video_path}") + return None + + # 檢查是否為視頻文件 + video_extensions = ['.mp4', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.webm'] + file_extension = os.path.splitext(video_path)[1].lower() + + if file_extension not in video_extensions: + print(f"警告: 檔案可能不是視頻格式: {file_extension}") + + # 生成唯一文件名 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + import random + random_id = random.randint(1000, 9999) + original_filename = os.path.basename(video_path) + name_without_ext = os.path.splitext(original_filename)[0] + new_filename = f"thread_video_{timestamp}_{random_id}_{name_without_ext}{file_extension}" + + # 獲取 ComfyUI output 目錄 + output_dir = get_output_directory() + destination_path = os.path.join(output_dir, new_filename) + + print(f"正在拷貝視頻文件...") + print(f"來源: {video_path}") + print(f"目標: {destination_path}") + + # 拷貝文件到 output 目錄 + shutil.copy2(video_path, destination_path) + + # 檢查拷貝是否成功 + if not os.path.exists(destination_path): + print(f"錯誤: 文件拷貝失敗") + return None + + file_size = os.path.getsize(destination_path) + print(f"視頻文件已拷貝成功,大小: {file_size / (1024*1024):.2f} MB") + + # 檢查文件大小限制 (Threads 限制 1GB) + max_size = 1024 * 1024 * 1024 # 1GB + if file_size > max_size: + print(f"警告: 文件大小 ({file_size / (1024*1024*1024):.2f} GB) 超過 Threads 限制 (1GB)") + # 不刪除文件,讓用戶決定是否繼續 + + # 生成訪問 URL + video_url = f"{base_url}/api/view?filename={new_filename}" + print(f"生成視頻 URL: {video_url}") + + return video_url + + except Exception as e: + print(f"處理本地視頻文件時發生錯誤: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _check_media_status(self, threads_api, media_id, access_token): + """檢查媒體容器狀態""" + try: + # 使用 Threads API 檢查媒體容器狀態 + resp = requests.get( + f"{threads_api.api_url}/{media_id}", + params={ + "fields": "status,error_message", + "access_token": access_token, + }, + ) + + if resp.status_code == 200: + return resp.json() + else: + print(f"狀態檢查 API 錯誤: {resp.status_code} - {resp.text}") + return None + + except Exception as e: + print(f"檢查媒體狀態時發生錯誤: {str(e)}") + return None + -# 註冊節點 NODE_CLASS_MAPPINGS = { "StartWithLongLiveToken": StartWithLongLiveToken, "PublishThread": PublishThread, "ThreadsHistory": ThreadsHistory, + "ThreadPublishVideo": ThreadPublishVideo, } NODE_DISPLAY_NAME_MAPPINGS = { "StartWithLongLiveToken": "Start With Long Live Token", "PublishThread": "Publish Thread", "ThreadsHistory": "Threads History", + "ThreadPublishVideo": "Thread Publish Video", } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 50909a0..4c3e574 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "thread" description = "A ComfyUI custom node package for seamless integration with Threads (Meta's social platform). This package allows you to publish posts, manage images, and retrieve post history directly from your ComfyUI workflows." -version = "1.0.1" +version = "1.0.2" license = {file = "LICENSE"} dependencies = ["threads_sdk"]