✅ Update yt_poster.py: [describe your changes briefly]
This commit is contained in:
@ -1,186 +1,115 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
yt_poster.py
|
yt_poster.py
|
||||||
|
|
||||||
Handles video uploads to YouTube using the YouTube Data API.
|
This module handles the upload of videos to YouTube using the YouTube Data API v3.
|
||||||
|
It supports setting metadata such as title, description, tags, category, and privacy settings.
|
||||||
|
It also ensures that the game title "Fortnite" is included in the metadata to trigger proper categorization.
|
||||||
|
|
||||||
This module manages:
|
Author: gramps@llamachile.shop
|
||||||
- Title and description generation
|
|
||||||
- Playlist assignment
|
|
||||||
- Custom thumbnail generation (widescreen only)
|
|
||||||
- Upload record persistence
|
|
||||||
- Session cleanup on success
|
|
||||||
|
|
||||||
Author: Llama Chile Shop
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import google.auth
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
from googleapiclient.discovery import build
|
from googleapiclient.discovery import build
|
||||||
from googleapiclient.errors import HttpError
|
|
||||||
from googleapiclient.http import MediaFileUpload
|
from googleapiclient.http import MediaFileUpload
|
||||||
|
|
||||||
from modules.title_utils import generate_montage_title
|
from modules.config import OPENAI_API_KEY, DEBUG
|
||||||
from modules.description_utils import generate_montage_description, generate_clip_description
|
from modules.archive import save_metadata_record
|
||||||
from modules.thumbnail_utils import generate_thumbnail, generate_thumbnail_prompt
|
|
||||||
from modules.metadata_utils import derive_session_metadata, save_metadata_record
|
|
||||||
from modules.config import DEBUG
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
# Category ID for "Gaming" on YouTube (required for accurate categorization)
|
||||||
load_dotenv()
|
CATEGORY_ID = "20"
|
||||||
|
|
||||||
HISTORY_DIR = Path("Z:/LCS/Logs/processed")
|
# Default tags to include if none are provided
|
||||||
|
DEFAULT_TAGS = [
|
||||||
|
"Fortnite", "Zero Build", "Gramps", "CoolHandGramps",
|
||||||
|
"funny", "gaming", "highlights"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Default visibility setting
|
||||||
|
DEFAULT_PRIVACY = "public"
|
||||||
|
|
||||||
def count_existing_uploads(session_date: str) -> int:
|
def ensure_fortnite_tag(metadata):
|
||||||
"""
|
"""
|
||||||
Returns the number of existing metadata records for a given session date.
|
Ensures that the word 'Fortnite' appears in at least one of the following:
|
||||||
Used to compute title suffixes (e.g., "Video 2").
|
- Title
|
||||||
"""
|
- Description
|
||||||
session_folder = HISTORY_DIR / session_date.replace("-", ".")
|
- Tags list
|
||||||
return len(list(session_folder.glob("*.json"))) if session_folder.exists() else 0
|
|
||||||
|
|
||||||
|
This helps YouTube automatically detect the game and associate the video
|
||||||
def upload_video(file_path: Path, session_dir: Path, is_vertical: bool) -> str:
|
with Fortnite gameplay.
|
||||||
"""
|
"""
|
||||||
Uploads a single video to YouTube, with title/description/thumbnail handling.
|
if "fortnite" not in metadata["title"].lower() and \
|
||||||
|
"fortnite" not in metadata["description"].lower() and \
|
||||||
|
not any("fortnite" in tag.lower() for tag in metadata.get("tags", [])):
|
||||||
|
metadata.setdefault("tags", []).append("Fortnite")
|
||||||
|
|
||||||
|
def upload_video(youtube, video_path, metadata):
|
||||||
|
"""
|
||||||
|
Uploads a video to YouTube with the provided metadata.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path (Path): Path to the rendered .mp4 file.
|
youtube: Authenticated YouTube API service object.
|
||||||
session_dir (Path): Parent directory of the session (used for metadata).
|
video_path: Path to the video file to be uploaded.
|
||||||
is_vertical (bool): Format flag for Shorts logic.
|
metadata: Dictionary containing video metadata fields.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Public YouTube video URL.
|
str: URL of the uploaded YouTube video.
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
from authorize_youtube import get_authenticated_service
|
|
||||||
youtube = get_authenticated_service()
|
|
||||||
|
|
||||||
# Derive session metadata and isolate the clip record
|
# Ensure the 'Fortnite' keyword is present somewhere in metadata
|
||||||
session_meta = derive_session_metadata(session_dir)
|
ensure_fortnite_tag(metadata)
|
||||||
session_date = session_meta["session_date"]
|
|
||||||
stem = file_path.stem
|
|
||||||
|
|
||||||
clip_record = next(
|
# Construct the request body for YouTube API
|
||||||
(clip for clip in session_meta["clips"] if clip["stem"] == stem),
|
request_body = {
|
||||||
None
|
|
||||||
)
|
|
||||||
|
|
||||||
if clip_record is None:
|
|
||||||
raise RuntimeError(f"Clip {stem} not found in session metadata.")
|
|
||||||
|
|
||||||
metadata = {**session_meta, **clip_record}
|
|
||||||
|
|
||||||
# Title logic with sequential suffixing
|
|
||||||
suffix = ""
|
|
||||||
existing_count = count_existing_uploads(session_date)
|
|
||||||
if existing_count > 0:
|
|
||||||
suffix = f"Video {existing_count + 1}"
|
|
||||||
|
|
||||||
title = generate_montage_title(session_date, suffix=suffix)
|
|
||||||
|
|
||||||
# Description
|
|
||||||
if metadata["highlight"]:
|
|
||||||
description = generate_clip_description(metadata["highlight"])
|
|
||||||
else:
|
|
||||||
description = generate_montage_description()
|
|
||||||
|
|
||||||
# Upload video
|
|
||||||
body = {
|
|
||||||
"snippet": {
|
"snippet": {
|
||||||
"title": title,
|
"title": metadata["title"],
|
||||||
"description": description,
|
"description": metadata["description"],
|
||||||
"tags": metadata.get("tags", []),
|
"tags": metadata.get("tags", DEFAULT_TAGS),
|
||||||
"categoryId": "20", # Gaming
|
"categoryId": CATEGORY_ID # Set to "Gaming"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"privacyStatus": "private" if DEBUG else "public",
|
"privacyStatus": metadata.get("privacy", DEFAULT_PRIVACY)
|
||||||
"selfDeclaredMadeForKids": False,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print(f"📤 Uploading to YouTube: {file_path.name}")
|
# Wrap the video file in a MediaFileUpload object
|
||||||
media = MediaFileUpload(str(file_path), chunksize=-1, resumable=True)
|
media = MediaFileUpload(video_path, mimetype="video/*", resumable=True)
|
||||||
|
|
||||||
|
print(f"📤 Uploading {video_path} to YouTube...")
|
||||||
|
|
||||||
|
# Execute the video insert request
|
||||||
request = youtube.videos().insert(
|
request = youtube.videos().insert(
|
||||||
part="snippet,status",
|
part="snippet,status",
|
||||||
body=body,
|
body=request_body,
|
||||||
media_body=media
|
media_body=media
|
||||||
)
|
)
|
||||||
|
|
||||||
response = None
|
response = request.execute()
|
||||||
while response is None:
|
|
||||||
status, response = request.next_chunk()
|
|
||||||
if status:
|
|
||||||
print(f"🟡 Uploading: {int(status.progress() * 100)}%")
|
|
||||||
|
|
||||||
video_id = response["id"]
|
video_id = response["id"]
|
||||||
video_url = f"https://youtu.be/{video_id}"
|
youtube_url = f"https://www.youtube.com/watch?v={video_id}"
|
||||||
print(f"✅ Upload complete: {video_url}")
|
|
||||||
|
|
||||||
# Upload thumbnail if wide
|
print(f"✅ Uploaded to YouTube: {youtube_url}")
|
||||||
if not is_vertical:
|
|
||||||
notes = metadata.get("notes", {})
|
|
||||||
prompt = generate_thumbnail_prompt(notes.get("highlight", "Fortnite moment"))
|
|
||||||
thumbnail_path = generate_thumbnail(file_path, output_path=f"{file_path.stem}_thumb.jpg")
|
|
||||||
if thumbnail_path:
|
|
||||||
youtube.thumbnails().set(
|
|
||||||
videoId=video_id,
|
|
||||||
media_body=str(thumbnail_path)
|
|
||||||
).execute()
|
|
||||||
print("✅ Custom thumbnail uploaded.")
|
|
||||||
|
|
||||||
# Add to playlist
|
# Record the YouTube URL in the metadata for archive history
|
||||||
playlist_id = os.getenv("YT_PLAYLIST_ID_SHORTS" if is_vertical else "YT_PLAYLIST_ID_CLIPS")
|
metadata.setdefault("youtube_url", []).append(youtube_url)
|
||||||
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
|
# Persist the metadata archive only if we're not in DEBUG mode
|
||||||
date_obj = datetime.strptime(session_date, "%Y-%m-%d")
|
|
||||||
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}")
|
|
||||||
|
|
||||||
# Save metadata (include YouTube URL)
|
|
||||||
metadata["youtube_urls"] = [video_url]
|
|
||||||
save_metadata_record(metadata)
|
|
||||||
|
|
||||||
# Clean up session directory if DEBUG is off
|
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
try:
|
save_metadata_record(video_path, metadata)
|
||||||
shutil.rmtree(session_dir)
|
|
||||||
print(f"🧹 Cleaned up source directory: {session_dir}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Cleanup failed: {e}")
|
|
||||||
|
|
||||||
return video_url
|
return youtube_url
|
||||||
|
|
||||||
except HttpError as e:
|
def get_authenticated_service():
|
||||||
print(f"❌ YouTube API error: {e}")
|
"""
|
||||||
return ""
|
Returns an authenticated YouTube API service using Application Default Credentials.
|
||||||
|
This requires that `gcloud auth application-default login` has been run successfully,
|
||||||
|
or that a service account token is available in the environment.
|
||||||
|
|
||||||
except Exception as e:
|
Returns:
|
||||||
print(f"❌ Unexpected error: {e}")
|
googleapiclient.discovery.Resource: The YouTube API client object.
|
||||||
return ""
|
"""
|
||||||
|
credentials, _ = google.auth.default(
|
||||||
|
scopes=["https://www.googleapis.com/auth/youtube.upload"]
|
||||||
|
)
|
||||||
|
return build("youtube", "v3", credentials=credentials)
|
||||||
|
|||||||
Reference in New Issue
Block a user