🛠️ 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__/
*.py[cod]
*.pyo
*.pyd
*.so
# Virtual environment
# Virtual environments
.venv/
env/
venv/
ENV/
# VS Code settings
# VSCode
.vscode/
# OS files
.DS_Store
Thumbs.db
# Tokens and API keys
# Environment variables and secrets
.env
token.pickle
token.zip
token (2).zip
# Build artifacts
*.mp4
*.mov
*.mp3
*.zip
*.odt
client_secrets.json
# Logs
logs/
*.log
# Assets not for versioning
assets/*.mp4
assets/*.mp3
assets/*.png
assets/*.otf
# Jupyter Notebook checkpoints
.ipynb_checkpoints/
# Processed data
202*/**/rendered/
202*/**/*.mp4
# Compiled C extensions
*.c
*.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 sys
import pickle
from pathlib import Path
# Automatically locate this file's directory (e.g., \\chong\LCS\Videos\eklipse)
project_root = os.path.dirname(os.path.abspath(__file__))
modules_dir = os.path.join(project_root, "modules")
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
# Add modules directory to the Python path
sys.path.insert(0, modules_dir)
# Scopes define what access is requested from the YouTube API
SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
# Change working directory so relative paths (like client_secrets.json) resolve
os.chdir(modules_dir)
# Import from yt_poster in modules
from yt_poster import authenticate_youtube
# Run the OAuth flow
print("🔐 Starting YouTube OAuth authorization...")
# Default token and client secret filenames
TOKEN_PATH = "token.pickle"
CLIENT_SECRET_FILE = "client_secrets.json"
try:
service = authenticate_youtube()
print("✅ YouTube authorization complete.")
except Exception as e:
print(f"❌ Authorization failed: {e}")
def get_authenticated_service():
"""
Returns an authorized YouTube API client.
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
from pathlib import Path
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
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.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:
"""
Uploads a video file to YouTube.
Uploads a video to YouTube, assigns to playlist, sets recording date, and optionally uploads a thumbnail.
Args:
file_path (str): Full path to the rendered video file.
is_vertical (bool): True if video is vertical format (9:16), else widescreen (16:9).
stream_date (str): Date of the stream in YYYY.MM.DD or YYYY.MM.DD.N format.
file_path (Path): Full path to the rendered video file.
is_vertical (bool): True if video is vertical.
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:
str: URL of the uploaded YouTube video.
str: YouTube video URL.
"""
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
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 = {
"snippet": {
"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)
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(
part="snippet,status",
body=body,
@ -91,8 +79,47 @@ def upload_video(file_path: Path, is_vertical: bool, stream_date: str, descripti
if status:
print(f"🟡 Uploading: {int(status.progress() * 100)}%")
print(f"✅ Upload complete: https://youtu.be/{response['id']}")
return f"https://youtu.be/{response['id']}"
video_id = 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:
print(f"❌ YouTube API error: {e}")

View File

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