Skip to content

Instantly share code, notes, and snippets.

@sbc
Last active February 13, 2025 06:57
Show Gist options
  • Select an option

  • Save sbc/d88fc8370c568698625c046b9e4bda48 to your computer and use it in GitHub Desktop.

Select an option

Save sbc/d88fc8370c568698625c046b9e4bda48 to your computer and use it in GitHub Desktop.
import os
import json
import http.client as httplib
import httplib2
import ssl
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from googleapiclient.errors import HttpError
import time
import random
import logging
from logging.handlers import RotatingFileHandler
# Set up logging
HOME = os.environ.get('HOME')
log_file = f'{HOME}/.n8n/logs/yt-upload.log'
os.makedirs(os.path.dirname(log_file), exist_ok=True)
logger = logging.getLogger('youtube_upload')
logger.setLevel(logging.INFO)
handler = RotatingFileHandler(log_file, maxBytes=1024*1024, backupCount=5)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
CHUNKSIZE = int(os.environ.get('CHUNKSIZE', 10 * 1024 * 1024))
# Load environment variables
VIDEO_FILE = os.environ.get('VIDEO_FILE')
CREDENTIALS_RAW = os.environ.get('CREDENTIALS')
VIDEO_METADATA_RAW = os.environ.get('VIDEO_METADATA')
if not VIDEO_FILE:
raise ValueError("Environment variable VIDEO_FILE is not set or is empty.")
if not CREDENTIALS_RAW:
raise ValueError("Environment variable CREDENTIALS is not set or is empty.")
if not VIDEO_METADATA_RAW:
raise ValueError("Environment variable VIDEO_METADATA is not set or is empty.")
# Parse the outer CREDENTIALS JSON
try:
CREDENTIALS_WRAPPER = json.loads(CREDENTIALS_RAW)
if 'stdout' not in CREDENTIALS_WRAPPER:
raise ValueError("CREDENTIALS does not contain a 'stdout' key.")
CREDENTIALS = json.loads(CREDENTIALS_WRAPPER['stdout'])
except json.JSONDecodeError as e:
raise ValueError(f"Error parsing CREDENTIALS JSON: {e}")
# Parse VIDEO_METADATA JSON
try:
VIDEO_METADATA = json.loads(VIDEO_METADATA_RAW)
except json.JSONDecodeError as e:
raise ValueError(f"Error parsing VIDEO_METADATA JSON: {e}")
# Create credentials object
credentials = Credentials(
token=CREDENTIALS["data"]["oauthTokenData"]["access_token"],
refresh_token=CREDENTIALS["data"]["oauthTokenData"]["refresh_token"],
client_id=CREDENTIALS["data"]["clientId"],
client_secret=CREDENTIALS["data"]["clientSecret"],
token_uri="https://oauth2.googleapis.com/token",
scopes=CREDENTIALS["data"]["oauthTokenData"]["scope"].split()
)
# Explicitly tell the underlying HTTP transport library not to retry, since
# we are handling retry logic ourselves.
httplib2.RETRIES = 1
# Maximum number of times to retry before giving up.
MAX_RETRIES = 10
# Always retry when these exceptions are raised.
RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError, httplib.NotConnected,
httplib.IncompleteRead, httplib.ImproperConnectionState,
httplib.CannotSendRequest, httplib.CannotSendHeader,
httplib.ResponseNotReady, httplib.BadStatusLine, ssl.SSLError)
# Always retry when an apiclient.errors.HttpError with one of these status
# codes is raised.
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
def resumable_upload(request):
"""Perform the resumable upload and return the video ID and URL."""
response = None
retry = 0
last_progress = 0
error = None
logger.info("Starting file upload...")
while response is None:
try:
status, response = request.next_chunk()
if status:
progress = int(status.progress() * 100)
# Only log progress if it has increased by 10% or more
if progress - last_progress >= 10:
logger.info(f"Upload progress: {progress}%")
last_progress = progress
if response and "id" in response:
video_id = response["id"]
video_url = f"https://www.youtube.com/watch?v={video_id}"
logger.info(f"Video ID: {video_id}")
logger.info(f"Video URL: {video_url}")
return video_id, video_url
except HttpError as e:
if e.resp.status in RETRIABLE_STATUS_CODES:
error = f"A retriable HTTP error {e.resp.status} occurred:\n{e.content}"
else:
raise
except RETRIABLE_EXCEPTIONS as e:
error = f"A retriable error occurred: {e}"
if error is not None:
logger.error(error)
retry += 1
if retry > MAX_RETRIES:
logger.error('No longer attempting to retry.')
exit('No longer attempting to retry.')
max_sleep = 2 ** retry
sleep_seconds = random.random() * max_sleep
logger.info(f'Sleeping {sleep_seconds} seconds and then retrying...')
time.sleep(sleep_seconds)
if response and "id" in response:
video_id = response["id"]
video_url = f"https://www.youtube.com/watch?v={video_id}"
logger.info(f"Video ID: {video_id}")
logger.info(f"Video URL: {video_url}")
return video_id, video_url
else:
logger.error("Failed to upload video.")
raise RuntimeError("Failed to upload video.")
def initiate_upload(youtube):
"""Initiates a YouTube video upload."""
tags = VIDEO_METADATA.get('tags', [])
if not isinstance(tags, list):
tags = []
body = {
'snippet': {
'title': VIDEO_METADATA.get('title', 'Untitled Video'),
'description': VIDEO_METADATA.get('description', ''),
'tags': tags,
'categoryId': VIDEO_METADATA.get('category', '28')
},
'status': {
'privacyStatus': VIDEO_METADATA.get('privacyStatus', 'private'),
'selfDeclaredMadeForKids': False
}
}
request = youtube.videos().insert(
part=','.join(body.keys()),
body=body,
media_body=MediaFileUpload(VIDEO_FILE, chunksize=CHUNKSIZE, resumable=True)
)
video_id, video_url = resumable_upload(request)
return video_id, video_url
# Build YouTube API client
youtube = build('youtube', 'v3', credentials=credentials)
try:
# Upload video
video_id, video_url = initiate_upload(youtube)
# Output result
output = {"video_id": video_id, "video_url": video_url}
print(json.dumps(output))
logger.info(f"Upload successful: {video_id}")
except Exception as e:
logger.error(f"Error during upload: {str(e)}", exc_info=True)
raise
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment