Last active
February 13, 2025 06:57
-
-
Save sbc/d88fc8370c568698625c046b9e4bda48 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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