🛠️ Initial YouTube upload and description generation: OAuth, montage flow, thumbnail setup, env config. Work in progress by gramps@llamachile.shop

This commit is contained in:
2025-07-24 19:33:49 -07:00
parent 2b8bf4ce19
commit 961c43fbd5
4 changed files with 156 additions and 77 deletions

61
.gitignore vendored
View File

@ -1,44 +1,59 @@
# Byte-compiled / cache # Python
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*.pyo *.pyo
*.pyd *.pyd
*.so
# Virtual environment # Virtual environments
.venv/ .venv/
env/ env/
venv/ venv/
ENV/
# VS Code settings # VSCode
.vscode/ .vscode/
# OS files # OS files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Tokens and API keys # Environment variables and secrets
.env .env
token.pickle client_secrets.json
token.zip
token (2).zip
# Build artifacts
*.mp4
*.mov
*.mp3
*.zip
*.odt
# Logs # Logs
logs/
*.log *.log
# Assets not for versioning # Jupyter Notebook checkpoints
assets/*.mp4 .ipynb_checkpoints/
assets/*.mp3
assets/*.png
assets/*.otf
# Processed data # Compiled C extensions
202*/**/rendered/ *.c
202*/**/*.mp4 *.o
*.obj
*.dll
*.a
*.lib
*.exp
*.pdb
# Test and coverage
.coverage
.tox/
.nox/
.cache/
pytest_cache/
htmlcov/
# Distribution / packaging
build/
dist/
*.egg-info/
.eggs/
MANIFEST
# Misc
*.bak
*.swp
*.tmp

View File

@ -1,25 +1,61 @@
"""
authorize_youtube.py
Handles OAuth2 authorization for the YouTube Data API.
This module loads the client_secrets.json file and generates an authorized
YouTube API service object for use by other modules. The token is cached
in token.pickle to avoid repeated authorization.
Author: gramps@llamachile.shop
"""
import os import os
import sys import pickle
from pathlib import Path
# Automatically locate this file's directory (e.g., \\chong\LCS\Videos\eklipse) from google.auth.transport.requests import Request
project_root = os.path.dirname(os.path.abspath(__file__)) from google_auth_oauthlib.flow import InstalledAppFlow
modules_dir = os.path.join(project_root, "modules") from googleapiclient.discovery import build
# Add modules directory to the Python path # Scopes define what access is requested from the YouTube API
sys.path.insert(0, modules_dir) SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
# Change working directory so relative paths (like client_secrets.json) resolve # Default token and client secret filenames
os.chdir(modules_dir) TOKEN_PATH = "token.pickle"
CLIENT_SECRET_FILE = "client_secrets.json"
# Import from yt_poster in modules
from yt_poster import authenticate_youtube
# Run the OAuth flow
print("🔐 Starting YouTube OAuth authorization...")
try: def get_authenticated_service():
service = authenticate_youtube() """
print("✅ YouTube authorization complete.") Returns an authorized YouTube API client.
except Exception as e:
print(f"❌ Authorization failed: {e}") If the token does not exist or is expired, initiates the OAuth flow.
Requires client_secrets.json in project root.
Returns:
googleapiclient.discovery.Resource: Authenticated YouTube service
"""
creds = None
# Check if token.pickle exists
if Path(TOKEN_PATH).exists():
with open(TOKEN_PATH, "rb") as token:
creds = pickle.load(token)
# If no valid creds, go through OAuth flow
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
print("🔐 Starting YouTube OAuth authorization...")
if not Path(CLIENT_SECRET_FILE).exists():
raise FileNotFoundError(f"Missing required file: {CLIENT_SECRET_FILE}")
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for future use
with open(TOKEN_PATH, "wb") as token:
pickle.dump(creds, token)
return build("youtube", "v3", credentials=creds)

View File

@ -13,44 +13,44 @@ Author: Llama Chile Shop
""" """
import os import os
from pathlib import Path
from googleapiclient.discovery import build from googleapiclient.discovery import build
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
from googleapiclient.http import MediaFileUpload from googleapiclient.http import MediaFileUpload
from modules.title_utils import generate_montage_title, generate_output_filename from modules.title_utils import generate_montage_title
from modules.description_utils import generate_montage_description from modules.description_utils import generate_montage_description
from modules.config import DEBUG from modules.config import DEBUG
from dotenv import load_dotenv
from datetime import datetime
from pathlib import Path
load_dotenv()
def upload_video(file_path: Path, is_vertical: bool, stream_date: str, description: str = None, private: bool = DEBUG) -> str: def upload_video(file_path: Path, is_vertical: bool, stream_date: str, description: str = None, private: bool = DEBUG) -> str:
""" """
Uploads a video file to YouTube. Uploads a video to YouTube, assigns to playlist, sets recording date, and optionally uploads a thumbnail.
Args: Args:
file_path (str): Full path to the rendered video file. file_path (Path): Full path to the rendered video file.
is_vertical (bool): True if video is vertical format (9:16), else widescreen (16:9). is_vertical (bool): True if video is vertical.
stream_date (str): Date of the stream in YYYY.MM.DD or YYYY.MM.DD.N format. stream_date (str): Stream session date in YYYY.MM.DD[.N] format.
description (str): Optional description. Generated if None.
private (bool): If True, uploads as private (used for debug mode).
Returns: Returns:
str: URL of the uploaded YouTube video. str: YouTube video URL.
""" """
try: try:
# Build title I have this:"and description
file_path = str(file_path)
session_name = Path(file_path).parents[1].name
title = generate_montage_title(session_name)
if not description:
description = str(generate_montage_description())
# Construct tags and privacy status
tags = ["Fortnite", "Zero Build", "Solo", "Gramps", "CoolHandGramps"]
privacy_status = "private" if private else "public"
# Authenticate
from authorize_youtube import get_authenticated_service from authorize_youtube import get_authenticated_service
youtube = get_authenticated_service() youtube = get_authenticated_service()
title = generate_montage_title(stream_date)
if not description:
description = generate_montage_description()
tags = ["Fortnite", "Zero Build", "Solo", "Gramps", "CoolHandGramps"]
privacy_status = "private" if private else "public"
# Upload video
body = { body = {
"snippet": { "snippet": {
"title": title, "title": title,
@ -64,21 +64,9 @@ def upload_video(file_path: Path, is_vertical: bool, stream_date: str, descripti
} }
} }
# media = MediaFileUpload(file_path, chunksize=-1, resumable=True) print(f"📤 Uploading to YouTube: {file_path.name}")
media = MediaFileUpload(str(file_path), chunksize=-1, resumable=True) media = MediaFileUpload(str(file_path), chunksize=-1, resumable=True)
if DEBUG:
print("🔍 DEBUGGING upload_video")
print(f" • file_path: {file_path} ({type(file_path)})")
print(f" • is_vertical: {is_vertical}")
print(f" • stream_date: {stream_date}")
print(f" • private: {private}")
print(f" • title: {title}")
print(f" • description: {description}")
print(f" • tags: {tags}")
print(f" • categoryId: {'20'} (should be int or str)")
request = youtube.videos().insert( request = youtube.videos().insert(
part="snippet,status", part="snippet,status",
body=body, body=body,
@ -91,8 +79,47 @@ def upload_video(file_path: Path, is_vertical: bool, stream_date: str, descripti
if status: if status:
print(f"🟡 Uploading: {int(status.progress() * 100)}%") print(f"🟡 Uploading: {int(status.progress() * 100)}%")
print(f"✅ Upload complete: https://youtu.be/{response['id']}") video_id = response["id"]
return f"https://youtu.be/{response['id']}" video_url = f"https://youtu.be/{video_id}"
print(f"✅ Upload complete: {video_url}")
# Add to playlist
playlist_id = os.getenv("YT_PLAYLIST_ID_SHORTS" if is_vertical else "YT_PLAYLIST_ID_CLIPS")
if playlist_id:
youtube.playlistItems().insert(
part="snippet",
body={
"snippet": {
"playlistId": playlist_id,
"resourceId": {
"kind": "youtube#video",
"videoId": video_id
}
}
}
).execute()
print(f"✅ Added to playlist: {playlist_id}")
# Set recording date
parts = stream_date.split(".")
date_obj = datetime(int(parts[0]), int(parts[1]), int(parts[2]))
recording_date = date_obj.strftime("%Y-%m-%dT00:00:00Z")
youtube.videos().update(
part="recordingDetails",
body={
"id": video_id,
"recordingDetails": {
"recordingDate": recording_date
}
}
).execute()
print(f"✅ Recording date set: {recording_date}")
# TODO: Thumbnail upload (stub)
# generate_thumbnail(file_path) → upload via thumbnails().set()
return video_url
except HttpError as e: except HttpError as e:
print(f"❌ YouTube API error: {e}") print(f"❌ YouTube API error: {e}")

View File

@ -22,6 +22,7 @@ from pathlib import Path
from modules.config import DEBUG from modules.config import DEBUG
from modules.yt_poster import upload_video from modules.yt_poster import upload_video
from modules.description_utils import generate_montage_description from modules.description_utils import generate_montage_description
from authorize_youtube import get_authenticated_service
def main(): def main():