add video node

This commit is contained in:
pk
2026-03-10 11:32:55 +01:00
parent 1244072ddf
commit 1ed09fee52
2 changed files with 222 additions and 0 deletions
+3
View File
@@ -12,6 +12,7 @@ from .nodes.api.AQ_Gemini import (
from .nodes.api.AQ_SendImageToAPI import AQ_SendImageToAPI
from .nodes.api.AQ_Cover import AQ_CoverPrompt
from .nodes.api.AQ_VideoFromImage import AQ_StillImageToVideo
from .nodes.api.AQ_YouTubeUpload import AQ_YouTubeUpload
from .nodes.clip.AQ_CLIPSetLastLayer import AQ_CLIPSetLastLayer
from .nodes.numbers.AQ_Numbers import AQ_Increment, AQ_Random
@@ -48,6 +49,7 @@ NODE_CLASS_MAPPINGS = {
"AQ_TextEncodeQwenImageEditPlus": AQ_TextEncodeQwenImageEditPlus,
"AQ_CoverPrompt": AQ_CoverPrompt,
"AQ_StillImageToVideo": AQ_StillImageToVideo,
"AQ_YouTubeUpload": AQ_YouTubeUpload,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -76,6 +78,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"AQ_TextEncodeQwenImageEditPlus": "AQ_TextEncodeQwenImageEditPlus",
"AQ_CoverPrompt": "AQ_CoverPrompt",
"AQ_StillImageToVideo": "AQ_StillImageToVideo",
"AQ_YouTubeUpload": "AQ_YouTubeUpload",
}
+219
View File
@@ -0,0 +1,219 @@
import os
import requests
_INSTRUCTION = """AQ_YouTubeUpload — How to get credentials (one-time setup)
=================================================================
YouTube uploads require OAuth 2.0, NOT a plain API key.
You need three values: Client ID, Client Secret, Refresh Token.
STEP 1 — Create a Google Cloud project & enable YouTube API
-----------------------------------------------------------
1. Go to https://console.cloud.google.com/
2. Create a new project (top bar → "New Project").
3. Navigate to: APIs & Services → Library
4. Search "YouTube Data API v3" → click it → Enable.
STEP 2 — Create OAuth 2.0 credentials
--------------------------------------
1. Navigate to: APIs & Services → Credentials
2. Click "Create Credentials" → "OAuth client ID"
3. If prompted, configure the OAuth consent screen first:
- User type: External
- Fill in app name, your email → Save
- Scopes: add "YouTube Data API v3 → .../auth/youtube.upload"
- Add yourself as a Test User (your Google account email)
4. Back in Credentials → Create Credentials → OAuth client ID:
- Application type: "Desktop app"
- Give it a name → Create
5. Copy "Client ID" and "Client Secret" from the dialog.
STEP 3 — Generate a Refresh Token (one-time)
--------------------------------------------
1. Visit https://developers.google.com/oauthplayground/
2. Click the gear icon (⚙️) in the top-right:
- Check "Use your own OAuth credentials"
- Enter your Client ID and Client Secret → Close
3. In "Step 1 — Select & authorize APIs":
- Paste or find: https://www.googleapis.com/auth/youtube.upload
- Click "Authorize APIs"
- Sign in with the Google account that owns the YouTube channel
4. In "Step 2 — Exchange authorization code for tokens":
- Click "Exchange authorization code for tokens"
- Copy the "Refresh token" value
STEP 4 — Fill in the node
--------------------------
- client_id → your OAuth Client ID
- client_secret → your OAuth Client Secret
- refresh_token → the refresh token from Step 3
The refresh token does not expire unless you revoke it.
You only need to do this setup once per YouTube channel.
PRIVACY STATUS
--------------
- private → only you can see it
- unlisted → anyone with the link can see it
- public → visible to everyone
DESCRIPTION AUTO-BUILD
-----------------------
If you leave 'description' empty, the node auto-composes it
from tagline, tags, and keyscale inputs.
"""
class AQ_YouTubeUpload:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"video_path": ("STRING", {"default": ""}),
"client_id": ("STRING", {"default": ""}),
"client_secret": ("STRING", {"default": ""}),
"refresh_token": ("STRING", {"default": ""}),
"title": ("STRING", {"default": "My Song"}),
"privacy_status": (["private", "unlisted", "public"], {"default": "private"}),
},
"optional": {
"description": ("STRING", {"multiline": True, "default": ""}),
"tagline": ("STRING", {"multiline": False, "default": ""}),
"headline": ("STRING", {"multiline": False, "default": ""}),
"tags": ("STRING", {"multiline": False, "default": ""}),
"keyscale": ("STRING", {"multiline": False, "default": ""}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("video_url", "video_id", "instruction")
OUTPUT_NODE = True
FUNCTION = "upload"
CATEGORY = "Aquasite/Media"
def upload(self, video_path, client_id, client_secret, refresh_token, title,
privacy_status="private", description="", tagline="", headline="",
tags="", keyscale=""):
if not all([video_path, client_id, client_secret, refresh_token]):
return ("", "", _INSTRUCTION)
if not os.path.isfile(video_path):
raise FileNotFoundError(f"Video file not found: {video_path}")
# Auto-compose description if not provided
if not description.strip():
parts = []
if tagline:
parts.append(tagline)
if tags:
parts.append(f"Style: {tags}")
if keyscale:
parts.append(f"Key: {keyscale}")
if headline:
parts.append(f"\n{headline}")
description = "\n".join(parts)
access_token = self._get_access_token(client_id, client_secret, refresh_token)
tags_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else []
video_id = self._resumable_upload(
access_token=access_token,
video_path=video_path,
title=title,
description=description,
tags=tags_list,
privacy_status=privacy_status,
category_id="10", # 10 = Music
)
video_url = f"https://www.youtube.com/watch?v={video_id}"
print(f"[AQ_YouTubeUpload] Uploaded: {video_url}")
return (video_url, video_id, _INSTRUCTION)
# ------------------------------------------------------------------
def _get_access_token(self, client_id, client_secret, refresh_token):
resp = requests.post(
"https://oauth2.googleapis.com/token",
data={
"client_id": client_id,
"client_secret": client_secret,
"refresh_token": refresh_token,
"grant_type": "refresh_token",
},
timeout=30,
)
if not resp.ok:
raise RuntimeError(f"Failed to get access token: {resp.status_code} {resp.text}")
return resp.json()["access_token"]
def _resumable_upload(self, access_token, video_path, title, description,
tags, privacy_status, category_id):
file_size = os.path.getsize(video_path)
metadata = {
"snippet": {
"title": title,
"description": description,
"tags": tags,
"categoryId": category_id,
},
"status": {
"privacyStatus": privacy_status,
},
}
# Initiate upload session
init = requests.post(
"https://www.googleapis.com/upload/youtube/v3/videos",
params={"uploadType": "resumable", "part": "snippet,status"},
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=UTF-8",
"X-Upload-Content-Type": "video/mp4",
"X-Upload-Content-Length": str(file_size),
},
json=metadata,
timeout=60,
)
if not init.ok:
raise RuntimeError(f"Upload init failed: {init.status_code} {init.text}")
upload_url = init.headers["Location"]
# Upload in 10 MB chunks
chunk_size = 10 * 1024 * 1024
with open(video_path, "rb") as fh:
start = 0
while start < file_size:
chunk = fh.read(chunk_size)
end = start + len(chunk) - 1
put = requests.put(
upload_url,
headers={
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Content-Type": "video/mp4",
},
data=chunk,
timeout=300,
)
if put.status_code in (200, 201):
return put.json()["id"]
elif put.status_code == 308: # Resume Incomplete
range_header = put.headers.get("Range", "")
if range_header:
start = int(range_header.split("-")[1]) + 1
else:
start = end + 1
else:
raise RuntimeError(
f"Upload chunk failed: {put.status_code} {put.text}"
)
raise RuntimeError("Upload ended without receiving a video ID")