Add YouTube Uploader functionality with authentication and video upload features

- Implemented YouTubeUploader class for video uploads
- Created YouTubeUploaderNode and YouTubeAuthNode for ComfyUI integration
- Added configuration management for YouTube API credentials
- Included README with installation instructions and features
- Updated .gitignore to exclude sensitive files and temporary data
- Defined requirements for dependencies in requirements.txt and constraints.txt
This commit is contained in:
Chao
2025-06-10 23:47:08 +02:00
parent 824c6cc445
commit 010f482695
9 changed files with 841 additions and 182 deletions
+29 -182
View File
@@ -1,194 +1,41 @@
# Byte-compiled / optimized / DLL files
# Sensitive files - OAuth credentials and tokens
client_secret.json
token.pickle
*.pickle
# Python cache
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
# Virtual environments
venv/
env/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# IDE files
.vscode/
.idea/
*.swp
*.swo
# Rope project settings
.ropeproject
# OS files
.DS_Store
Thumbs.db
# mkdocs documentation
/site
# Temporary files
*.tmp
*.temp
import\ os.py
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Log files
*.log
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the enitre vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Test videos and media
*.mp4
*.avi
*.mov
*.jpg
*.png
test_*
+178
View File
@@ -0,0 +1,178 @@
[Support me and buy me a coffee :)](coff.ee/chao.k)
Discamler: AI models have been utilized in creation of this repo.
# 🎬 YouTube Uploader Nodes for ComfyUI
Upload videos directly to YouTube from your ComfyUI workflows!
## 🚀 Features
- **Direct Upload**: Upload generated videos straight to YouTube
- **Custom Thumbnails**: Set custom thumbnails for your videos
- **Audio Support**: Include audio in your uploads
- **Safety Features**: Upload protection to prevent accidents
- **Shorts Optimized**: Perfect for vertical video workflows
## 📦 Installation
### Method 1: Copy to ComfyUI Custom Nodes
```bash
# Copy the entire comfyui_nodes folder to your ComfyUI custom_nodes directory
cp -r f:/YouTubeUploader/comfyui_nodes /path/to/ComfyUI/custom_nodes/youtube_uploader
```
### Method 2: Symbolic Link (Recommended for Development)
```bash
# Create symbolic link in ComfyUI custom_nodes
ln -s f:/YouTubeUploader/comfyui_nodes /path/to/ComfyUI/custom_nodes/youtube_uploader
```
### Install Dependencies
```bash
cd /path/to/ComfyUI
pip install -r custom_nodes/youtube_uploader/requirements_comfyui.txt
```
## 🔧 Setup
### 1. YouTube API Credentials
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create project and enable YouTube Data API v3
3. Create OAuth 2.0 credentials (Desktop application)
4. Note your Client ID and Client Secret
### 2. Node Configuration
#### YouTube Auth Node
- **Input**: Client ID, Client Secret, Authenticate Now
- **Output**: Authentication status and channel info
- **Usage**: Set up once per session
#### YouTube Uploader Node
- **Video Input**: Connect from any video generation node
- **Text Inputs**: Title, description, tags
- **Settings**: Privacy level, FPS, upload enabled
- **Optional**: Audio track, custom thumbnail
## 🎯 Workflow Examples
### Basic Video Upload
```
[Video Generation] → [YouTube Uploader]
↑
[YouTube Auth] ──────┘
```
### Advanced with Audio and Thumbnail
```
[Video Generation] → [YouTube Uploader] ← [Audio Generation]
↑ ↑
[YouTube Auth] ──────┘ [Thumbnail Generator]
```
## ⚠️ Safety Features
### Upload Protection
- **upload_enabled**: Must be set to `True` to actually upload
- **Status Display**: Shows current upload status
### Testing Mode
1. Set `upload_enabled` to `False`
2. Test your workflow
3. Check video preview
4. Enable upload only when ready
## 🎨 Node Features
### YouTube Uploader Node
- **Smart Video Conversion**: Handles image sequences and single frames
- **Audio Integration**: Automatically combines video with audio
- **Error Handling**: Graceful failure with detailed messages
### YouTube Auth Node
- **One-Time Setup**: Authenticate once per session
- **Channel Info**: Displays connected channel details
- **Credential Management**: Secure handling of API keys
## 🔍 Troubleshooting
### Common Issues
#### Authentication Failed
- Check Client ID and Secret
- Ensure YouTube Data API v3 is enabled
- Verify OAuth consent screen is configured
#### Video Upload Failed
- Check file size (max 256MB)
- Verify video format compatibility
- Ensure stable internet connection
#### Node Not Appearing
- Restart ComfyUI after installation
- Check console for error messages
- Verify all dependencies are installed
## 🎵 Audio Support
### Supported Formats
- WAV, MP3, FLAC (via librosa)
- Raw audio tensors from ComfyUI nodes
- Auto-conversion to AAC for upload
### Audio Processing
- Automatic sample rate conversion
- Volume normalization
- Sync with video duration
## 🖼️ Thumbnail Features
### Auto-Thumbnails
- Uses first frame if no custom thumbnail
- Automatic aspect ratio correction
- Quality optimization for YouTube
### Custom Thumbnails
- Connect any image generation node
- Supports batch processing
- Auto-resize to YouTube specs
## 📊 Analytics Integration
### Upload Tracking
- Video ID capture for analytics
- Direct YouTube Studio links
- Upload timestamp logging
### Workflow Metrics
- Processing time tracking
- Success/failure rates
- File size optimization stats
## 🤝 Contributing
### Development Setup
1. Fork the repository
2. Create feature branch
3. Test with multiple ComfyUI versions
4. Submit pull request
### Adding Features
- Follow ComfyUI node conventions
- Add comprehensive error handling
- Include unit tests for core functions
- Update documentation
## 🆘 Support
- GitHub Issues: Report bugs and feature requests
---
**⚠️ Important**: Always test uploads with private videos first. Respect YouTube's Terms of Service and community guidelines.
+21
View File
@@ -0,0 +1,21 @@
import os
import sys
# Add the current directory to path to help Python find the modules
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.append(current_dir)
# Now import the node mappings
from youtube_uploader_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
# Version info
__version__ = "1.0.0"
__author__ = "ComfyUI YouTube Extension"
__description__ = "Upload videos to YouTube directly from ComfyUI workflows"
# Display loading message
print(f"🎬 YouTube Uploader Nodes v{__version__} loaded!")
print("📺 Ready to upload videos to YouTube from ComfyUI")
+44
View File
@@ -0,0 +1,44 @@
import os
import json
class Config:
def __init__(self):
self.CLIENT_ID = ""
self.CLIENT_SECRET = ""
self.PROJECT_ID = "comfyui-youtube-uploader"
self.CLIENT_SECRETS_FILE = "client_secret.json"
# Get the directory where this config file is located
self.base_dir = os.path.dirname(os.path.abspath(__file__))
self.client_secrets_path = os.path.join(self.base_dir, self.CLIENT_SECRETS_FILE)
def create_client_secrets_file(self):
"""Create client_secret.json file with provided credentials"""
if not self.CLIENT_ID or not self.CLIENT_SECRET:
raise ValueError("CLIENT_ID and CLIENT_SECRET must be provided")
client_secrets = {
"installed": {
"client_id": self.CLIENT_ID,
"project_id": self.PROJECT_ID,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": self.CLIENT_SECRET,
"redirect_uris": ["http://localhost"]
}
}
with open(self.client_secrets_path, 'w') as f:
json.dump(client_secrets, f, indent=2)
print(f"✅ Created client secrets file: {self.client_secrets_path}")
return self.client_secrets_path
def get_client_secrets_path(self):
"""Get the full path to client_secret.json"""
return self.client_secrets_path
def has_client_secrets(self):
"""Check if client_secret.json exists"""
return os.path.exists(self.client_secrets_path)
+7
View File
@@ -0,0 +1,7 @@
google-auth==2.23.0
google-auth-oauthlib==1.0.0
google-auth-httplib2==0.1.0
google-api-python-client==2.95.0
requests==2.31.0
opencv-python>=4.8.0
Pillow>=10.0.0
+4
View File
@@ -0,0 +1,4 @@
google-auth
google-auth-oauthlib
google-auth-httplib2
google-api-python-client
+11
View File
@@ -0,0 +1,11 @@
# ComfyUI YouTube Uploader Node Requirements
opencv-python>=4.8.0
scipy>=1.10.0
Pillow>=10.0.0
# Video processing (optional but recommended)
ffmpeg-python>=0.2.0
# Audio processing (optional)
librosa>=0.10.0
soundfile>=0.12.0
+192
View File
@@ -0,0 +1,192 @@
import os
import pickle
# Check if Google API dependencies are available
try:
from google.auth.transport.requests import Request
from google.auth.exceptions import RefreshError
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from googleapiclient.http import MediaFileUpload
GOOGLE_APIS_AVAILABLE = True
except ImportError as e:
print(f"Google API dependencies not available: {e}")
GOOGLE_APIS_AVAILABLE = False
SCOPES = [
'https://www.googleapis.com/auth/youtube.upload',
'https://www.googleapis.com/auth/youtube.readonly'
]
if GOOGLE_APIS_AVAILABLE:
class YouTubeUploader:
def __init__(self):
self.service = None
self.credentials = None
def authenticate(self, client_id=None, client_secret=None):
"""Authenticate with YouTube API"""
creds = None
# Get the directory where this script is located
script_dir = os.path.dirname(os.path.abspath(__file__))
token_file = os.path.join(script_dir, 'token.pickle')
if os.path.exists(token_file):
with open(token_file, 'rb') as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
try:
creds.refresh(Request())
except RefreshError:
creds = None
if not creds:
# Try to use client secrets file first
client_secrets_file = os.path.join(script_dir, 'client_secret.json')
print(f"🔍 Looking for client secrets file: {client_secrets_file}")
if os.path.exists(client_secrets_file):
print(f"✅ Found client secrets file: {client_secrets_file}")
flow = InstalledAppFlow.from_client_secrets_file(client_secrets_file, SCOPES)
creds = flow.run_local_server(port=0)
elif client_id and client_secret:
# Fall back to using provided credentials
print("🔑 Using provided credentials")
client_config = {
"installed": {
"client_id": client_id,
"client_secret": client_secret,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token"
}
}
flow = InstalledAppFlow.from_client_config(client_config, SCOPES)
creds = flow.run_local_server(port=0)
else:
raise Exception(f"No client secrets file found at {client_secrets_file} and no credentials provided")
with open(token_file, 'wb') as token:
pickle.dump(creds, token)
self.credentials = creds
self.service = build('youtube', 'v3', credentials=creds)
return creds
def upload_video(self, video_path, title, description, tags, privacy='private'):
"""Upload video to YouTube"""
if not self.service:
raise Exception("Not authenticated. Call authenticate() first.")
# Handle tags - convert to list if it's a string, or use as-is if it's already a list
if isinstance(tags, str):
tags_list = [tag.strip() for tag in tags.split(',') if tag.strip()]
elif isinstance(tags, list):
tags_list = [str(tag).strip() for tag in tags if str(tag).strip()]
else:
tags_list = []
body = {
'snippet': {
'title': title,
'description': description,
'tags': tags_list,
'categoryId': '22' # People & Blogs
},
'status': {
'privacyStatus': privacy
}
}
media = MediaFileUpload(video_path, chunksize=-1, resumable=True)
try:
request = self.service.videos().insert(
part=','.join(body.keys()),
body=body,
media_body=media
)
response = request.execute()
video_id = response['id']
video_url = f"https://www.youtube.com/watch?v={video_id}"
return video_id, video_url, True
except HttpError as e:
print(f"An HTTP error occurred: {e}")
return "", "", False
except Exception as e:
print(f"An error occurred: {e}")
return "", "", False
def _resumable_upload(self, insert_request):
"""Handle resumable upload with retry logic"""
response = None
error = None
retry = 0
while response is None:
try:
status, response = insert_request.next_chunk()
if response is not None:
if 'id' in response:
return response
else:
raise Exception(f"Upload failed with unexpected response: {response}")
except HttpError as e:
if e.resp.status in [500, 502, 503, 504]:
error = f"A retriable HTTP error {e.resp.status} occurred:\n{e.content}"
retry += 1
if retry > 3:
break
else:
raise
except Exception as e:
error = f"An error occurred: {e}"
break
if error:
print(error)
return None
def get_channel_info(self):
"""Get information about the authenticated channel"""
if not self.service:
return None
try:
response = self.service.channels().list(
part='snippet,statistics',
mine=True
).execute()
if response['items']:
channel = response['items'][0]
return {
'title': channel['snippet']['title'],
'subscriber_count': channel['statistics'].get('subscriberCount', 'Hidden'),
'video_count': channel['statistics']['videoCount']
}
except Exception as e:
print(f"Error getting channel info: {e}")
return None
else:
# Placeholder class when Google APIs are not available
class YouTubeUploader:
def __init__(self):
self.service = None
self.credentials = None
def authenticate(self, client_id=None, client_secret=None):
raise ImportError("Google API dependencies not installed")
def upload_video(self, video_path, title, description, tags, privacy='private'):
raise ImportError("Google API dependencies not installed")
def get_channel_info(self):
raise ImportError("Google API dependencies not installed")
+355
View File
@@ -0,0 +1,355 @@
import os
import sys
import tempfile
import torch
import numpy as np
from PIL import Image
import cv2
import folder_paths
# Make sure we can import from the same directory
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.append(current_dir)
# Try absolute import
try:
from youtube_uploader import YouTubeUploader, GOOGLE_APIS_AVAILABLE
from config import Config
if not GOOGLE_APIS_AVAILABLE:
raise ImportError("Google API libraries not installed")
DEPENDENCIES_AVAILABLE = True
except ImportError as e:
print(f"Error: Cannot load YouTube uploader. {e}")
print("Please install dependencies with: pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client")
YouTubeUploader = None
Config = None
DEPENDENCIES_AVAILABLE = False
class YouTubeUploaderNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video": ("IMAGE",), # ComfyUI video as image sequence
"title": ("STRING", {
"multiline": False,
"default": "My Awesome Short #1"
}),
"description": ("STRING", {
"multiline": True,
"default": "Check out this amazing video! 🔥\n\n#Shorts #YouTube #ComfyUI #AI"
}),
"tags": ("STRING", {
"multiline": False,
"default": "shorts,youtube,ai,comfyui,viral"
}),
"privacy": (["private", "unlisted", "public"], {
"default": "private"
}),
"fps": ("INT", {
"default": 30,
"min": 1,
"max": 60,
"step": 1
}),
"upload_enabled": ("BOOLEAN", {
"default": False
}),
},
"optional": {
"audio": ("AUDIO",), # Optional audio input
"thumbnail": ("IMAGE",), # Optional custom thumbnail
}
}
RETURN_TYPES = ("STRING", "STRING", "BOOLEAN")
RETURN_NAMES = ("video_id", "upload_url", "success")
FUNCTION = "upload_to_youtube"
CATEGORY = "YouTube/Upload"
OUTPUT_NODE = True
def __init__(self):
if not DEPENDENCIES_AVAILABLE:
raise RuntimeError("YouTube Uploader module not available. Check youtube_uploader.py file.")
if not DEPENDENCIES_AVAILABLE:
raise RuntimeError("YouTube Uploader dependencies not installed. Please run: pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client")
self.uploader = YouTubeUploader()
self.config = Config()
def upload_to_youtube(self, video, title, description, tags, privacy, fps, upload_enabled, audio=None, thumbnail=None):
"""Upload video to YouTube"""
if not upload_enabled:
print("⚠️ YouTube upload is disabled. Set 'upload_enabled' to True to upload.")
return ("", "", False)
try:
# Convert image sequence to video file
video_path = self._create_video_from_images(video, fps, audio)
# Prepare thumbnail if provided
thumbnail_path = None
if thumbnail is not None:
thumbnail_path = self._save_thumbnail(thumbnail)
# Authenticate if needed
if not self.uploader.service:
print("🔐 Authenticating with YouTube...")
self.uploader.authenticate()
# Upload video
print(f"🚀 Uploading video: {title}")
video_id = self.uploader.upload_video(
video_path, title, description, tags, privacy
)
if video_id:
upload_url = f"https://www.youtube.com/watch?v={video_id}"
print(f"✅ Upload successful! Video ID: {video_id}")
print(f"🔗 Video URL: {upload_url}")
# Set thumbnail if provided
if thumbnail_path and video_id:
self._set_video_thumbnail(video_id, thumbnail_path)
# Cleanup temporary files
self._cleanup_temp_files(video_path, thumbnail_path)
return (video_id, upload_url, True)
else:
print("❌ Upload failed")
return ("", "", False)
except Exception as e:
print(f"❌ Error uploading to YouTube: {str(e)}")
return ("", "", False)
def _create_video_from_images(self, images, fps, audio=None):
"""Convert ComfyUI image sequence to video file"""
if len(images.shape) == 3:
# Single image, create 3-second video
images = images.unsqueeze(0).repeat(fps * 3, 1, 1, 1)
# Convert tensor to numpy
if isinstance(images, torch.Tensor):
images_np = images.cpu().numpy()
else:
images_np = images
# Ensure values are in 0-255 range
if images_np.max() <= 1.0:
images_np = (images_np * 255).astype(np.uint8)
else:
images_np = images_np.astype(np.uint8)
# Create temporary video file
temp_dir = tempfile.gettempdir()
video_path = os.path.join(temp_dir, f"comfyui_youtube_upload_{hash(str(images_np.shape))}.mp4")
# Get video dimensions
height, width = images_np.shape[1:3]
# Ensure dimensions are even (required for some codecs)
if width % 2 != 0:
width -= 1
if height % 2 != 0:
height -= 1
# Create video writer
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(video_path, fourcc, fps, (width, height))
try:
for i in range(len(images_np)):
frame = images_np[i]
# Resize if needed
if frame.shape[1] != width or frame.shape[0] != height:
frame = cv2.resize(frame, (width, height))
# Convert RGB to BGR for OpenCV
if len(frame.shape) == 3 and frame.shape[2] == 3:
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
out.write(frame)
out.release()
# Add audio if provided
if audio is not None:
video_path = self._add_audio_to_video(video_path, audio)
print(f"📹 Created video: {video_path}")
return video_path
except Exception as e:
out.release()
print(f"❌ Error creating video: {str(e)}")
raise
def _add_audio_to_video(self, video_path, audio):
"""Add audio to video using ffmpeg"""
try:
import subprocess
# Save audio to temporary file
audio_path = video_path.replace('.mp4', '_audio.wav')
# Convert audio tensor to wav file
if isinstance(audio, torch.Tensor):
audio_np = audio.cpu().numpy()
else:
audio_np = audio
# Save audio using scipy or librosa if available
try:
import scipy.io.wavfile as wavfile
# Ensure audio is in correct format
if audio_np.max() <= 1.0:
audio_np = (audio_np * 32767).astype(np.int16)
wavfile.write(audio_path, 44100, audio_np)
except ImportError:
print("⚠️ scipy not available, skipping audio")
return video_path
# Combine video and audio
output_path = video_path.replace('.mp4', '_with_audio.mp4')
cmd = [
'ffmpeg', '-y',
'-i', video_path,
'-i', audio_path,
'-c:v', 'copy',
'-c:a', 'aac',
'-shortest',
output_path
]
subprocess.run(cmd, check=True, capture_output=True)
# Cleanup
os.remove(video_path)
os.remove(audio_path)
return output_path
except Exception as e:
print(f"⚠️ Could not add audio: {str(e)}")
return video_path
def _save_thumbnail(self, thumbnail):
"""Save thumbnail image to temporary file"""
if isinstance(thumbnail, torch.Tensor):
thumbnail_np = thumbnail.cpu().numpy()
else:
thumbnail_np = thumbnail
# Convert to PIL Image
if thumbnail_np.max() <= 1.0:
thumbnail_np = (thumbnail_np * 255).astype(np.uint8)
if len(thumbnail_np.shape) == 4:
thumbnail_np = thumbnail_np[0] # Take first image if batch
img = Image.fromarray(thumbnail_np)
# Save to temporary file
temp_dir = tempfile.gettempdir()
thumbnail_path = os.path.join(temp_dir, f"comfyui_thumbnail_{hash(str(thumbnail_np.shape))}.jpg")
img.save(thumbnail_path, 'JPEG', quality=95)
return thumbnail_path
def _set_video_thumbnail(self, video_id, thumbnail_path):
"""Set custom thumbnail for uploaded video"""
try:
# This requires additional YouTube API permissions
self.uploader.youtube.thumbnails().set(
videoId=video_id,
media_body=thumbnail_path
).execute()
print("🖼️ Custom thumbnail set successfully")
except Exception as e:
print(f"⚠️ Could not set thumbnail: {str(e)}")
def _cleanup_temp_files(self, *file_paths):
"""Clean up temporary files"""
for file_path in file_paths:
if file_path and os.path.exists(file_path):
try:
os.remove(file_path)
except:
pass
class YouTubeAuthNode:
"""Separate node for YouTube authentication"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"client_id": ("STRING", {
"multiline": False,
"default": ""
}),
"client_secret": ("STRING", {
"multiline": False,
"default": ""
}),
"authenticate_now": ("BOOLEAN", {
"default": False
}),
}
}
RETURN_TYPES = ("BOOLEAN", "STRING")
RETURN_NAMES = ("authenticated", "channel_info")
FUNCTION = "authenticate_youtube"
CATEGORY = "YouTube/Auth"
def authenticate_youtube(self, client_id, client_secret, authenticate_now):
"""Authenticate with YouTube API"""
if not authenticate_now:
return (False, "Authentication not requested")
try:
# Create uploader instance
uploader = YouTubeUploader()
# Create client secrets if provided
if client_id and client_secret:
config = Config()
config.CLIENT_ID = client_id
config.CLIENT_SECRET = client_secret
config.PROJECT_ID = "comfyui-youtube-uploader"
config.create_client_secrets_file()
# Authenticate
uploader.authenticate()
# Get channel info
channel_info = uploader.get_channel_info()
if channel_info:
info_text = f"Channel: {channel_info['title']}\nSubscribers: {channel_info['subscriber_count']}\nVideos: {channel_info['video_count']}"
else:
info_text = "Authentication successful"
print(f"✅ YouTube authentication successful!")
print(f"📺 {info_text}")
return (True, info_text)
except Exception as e:
error_msg = f"Authentication failed: {str(e)}"
print(f"❌ {error_msg}")
return (False, error_msg)
# Node mappings for ComfyUI
NODE_CLASS_MAPPINGS = {
"YouTubeUploaderNode": YouTubeUploaderNode,
"YouTubeAuthNode": YouTubeAuthNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"YouTubeUploaderNode": "🎬 YouTube Uploader",
"YouTubeAuthNode": "🔐 YouTube Auth"
}