moved youtube scirpts into there own folder

This commit is contained in:
SumGuyV5 committed 2026-09-13 16:01:40 +00:00
1 parent 8bf85fea2e
commit afe6490d20
15 files changed
+127 -102

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
# package/youtube/__init__.py
# Expose components from individual modules using relative imports
from .find_tags import find_tags
from .hashtags import get_top_nouns
from .shorts import fit_to_9_16_letterbox
from .uploader import upload_video, CategoryId
# Explicitly define what is exposed when importing the subpackage
__all__ = [
"find_tags",
"get_top_nouns",
"fit_to_9_16_letterbox",
"upload_video",
]
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
import json
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
CREDENTIALS = None
def load_credentials() -> Credentials:
"""Load credentials from secrets.json"""
try:
with open('secrets.json', 'r') as f:
creds_data = json.load(f)
credentials = Credentials(
token=creds_data['token'],
refresh_token=creds_data['refresh_token'],
token_uri=creds_data['token_uri'],
client_id=creds_data['client_id'],
client_secret=creds_data['client_secret'],
scopes=creds_data['scopes']
)
# Refresh token if expired
if credentials.expired:
credentials.refresh(Request())
return credentials
except FileNotFoundError:
print("Error: secrets.json not found! Please run setup.py first.")
return None
except Exception as e:
print(f"Error loading credentials: {str(e)}")
return None
CREDENTIALS = load_credentials()
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
from typing import Any
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
import package.youtube.config as config
def get_my_uploads_playlist_id(youtube: build) -> Any:
"""Retrieves the system upload playlist ID for the authenticated user's channel."""
try:
# mine=True automatically references the authorized account
request = youtube.channels().list(part="contentDetails", mine=True)
response = request.execute()
if "items" in response and len(response['items']) > 0:
return response['items'][0]['contentDetails']['relatedPlaylists']['uploads']
else:
print("No channel found for these credentials.")
return None
except HttpError as e:
print(f"API Error retrieving channel details: {str(e)}")
return None
def scan_channel_videos_for_tag(youtube: build, uploads_playlist_id: str, target_tag: str) -> list:
"""
Loops through all channel video uploads and filters those possessing the target tag.
"""
target_tag_lower = target_tag.lower()
all_videos_count = 0
matched_videos = []
next_page_token = None
print("Beginning channel scan...")
while True:
try:
# Step A: Retrieve a batch of video IDs from the uploads playlist container
playlist_request = youtube.playlistItems().list(
part="snippet",
playlistId=uploads_playlist_id,
maxResults=50,
pageToken=next_page_token
)
playlist_response = playlist_request.execute()
video_ids_batch = [
item['snippet']['resourceId']['videoId']
for item in playlist_response.get("items", [])
]
if not video_ids_batch:
break
all_videos_count += len(video_ids_batch)
# Step B: Pass batch to videos().list to extract metadata details (including tags)
video_request = youtube.videos().list(
part="snippet",
id=",".join(video_ids_batch)
)
video_response = video_request.execute()
for video in video_response.get("items", []):
title = video['snippet']['title']
video_id = video['id']
# Tags are optional fields on YouTube; default to an empty list if absent
tags = video['snippet'].get("tags", [])
# Normalize tags to lowercase for clean matching evaluation
tags_lower = [tag.lower() for tag in tags]
if target_tag_lower in tags_lower:
matched_videos.append({
"id": video_id,
"title": title,
"tags": tags
})
print(f"🔍 Found Match: '{title}' (ID: {video_id})")
# Check if another page token exists, if not break the pagination loop
next_page_token = playlist_response.get("nextPageToken")
if not next_page_token:
break
except HttpError as e:
print(f"An error occurred while fetching video batches: {str(e)}")
break
# Summary reporting
print("\n" + "="*40)
print(f"Scan complete. Analyzed {all_videos_count} total videos.")
print(f"Found {len(matched_videos)} videos containing the '{target_tag}' tag.")
print("="*40)
return matched_videos
def find_tags():
if not config.CREDENTIALS:
return
youtube = build('youtube', 'v3', credentials=config.CREDENTIALS)
# 1. Fetch your dynamic uploads playlist pointer
uploads_id = get_my_uploads_playlist_id(youtube)
if uploads_id:
print(f"Target Uploads Playlist ID: {uploads_id}")
# 2. Run the iterative match parser targeting the 'clips' keyword tag
target_keyword = "clips"
results = scan_channel_videos_for_tag(youtube, uploads_id, target_keyword)
# 3. Print out a neat clean list of matches
if results:
print(f"\n--- List of matching videos for tag '{target_keyword}': ---")
for index, item in enumerate(results, start=1):
print(f"{index}. {item['title']} -> https://youtu.be{item['id']}")
if __name__ == "__main__":
find_tags()
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
import re
import json
import nltk
import string
from collections import Counter
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
# Download necessary NLTK data modules
nltk.download('punkt', quiet=True)
nltk.download('stopwords', quiet=True)
nltk.download('punkt_tab', quiet=True)
nltk.download('averaged_perceptron_tagger', quiet=True) # Required for POS tagging
nltk.download('averaged_perceptron_tagger_eng', quiet=True)
def extract_text_from_srt(file_path: str) -> str:
"""
Extracts the text from the srt file.
Args:
file_path (str): is the path to the str file.
Returns:
returns (str): returns the cleaned up text.
"""
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# Remove SRT timestamps and sequence numbers
clean_text = re.sub(r'\d+\n\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}\n', '', content)
clean_text = re.sub(r'\d+', '', clean_text)
return clean_text
def extract_text_from_json(file_path: str) -> str:
"""
Extracts the text from the json file.
Args:
file_path (str): is the path to the json file.
Returns:
returns (str): returns the cleaned up text.
"""
with open(file_path, "r", encoding="utf-8") as file:
data = json.load(file)
messages = []
for comment in data.get("comments", []):
message_text = comment.get("message", {}).get("body", "")
messages.append(message_text)
# Return a single merged string of all chat text
return " ".join(messages)
def get_top_nouns(file_path: str, top_n: int = 10) -> list:
"""
Gets the top number of nouns based on past (top_n) arg.
Args:
file_path (str): is the path to the text file.
top_n (int): is the number of nouns to return.
Returns:
returns (list): returns a list of nouns.
"""
# 1. Extract raw text
if file_path.endswith(".srt"):
raw_text = extract_text_from_srt(file_path)
else:
raw_text = extract_text_from_json(file_path)
# 2. Basic cleanup (Keep original case for proper noun accuracy)
# Strip basic punctuation but leave words intact
clean_text = raw_text.translate(str.maketrans('', '', string.punctuation))
# 3. Tokenize words
words = word_tokenize(clean_text)
# 4. Part-of-Speech Tagging
tagged_words = nltk.pos_tag(words)
# 5. Filter for Nouns (NN = Singular Noun, NNP = Proper Noun, NNS = Plural Noun)
stop_words = set(stopwords.words('english'))
nouns = []
for word, tag in tagged_words:
word_lower = word.lower()
# Filter out short fragments and standard stopwords
if tag in ['NN', 'NNP', 'NNS'] and len(word_lower) > 2 and word_lower not in stop_words:
nouns.append(word_lower)
# 6. Count frequencies
noun_counts = Counter(nouns)
return [noun for noun, count in noun_counts.most_common(top_n)]
if __name__ == "__main__":
# Example usage
# Replace 'your_video.srt' with the path to your file
results = get_top_nouns('download/videos/2813112936/2813112936.srt', top_n=10)
print("Trending Hashtags SRT:", results)
results = get_top_nouns("download/videos/2813112936/2813112936_chat.json", top_n=10)
print("Trending Hashtags JSON:", results)
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
import os
import json
import package.utils as utils
import tempfile
from pathlib import Path
from moviepy import VideoFileClip, ColorClip, CompositeVideoClip
from moviepy.video.VideoClip import TextClip
from pathlib import Path
import numpy as np
from PIL import Image, ImageFilter
def get_video_info(input_path: Path) -> tuple:
"""Uses ffprobe to instantly read input video dimensions and frame rate."""
cmd = f"ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate -of json {input_path}"
# Run command and capture output (assumes linux.run_command prints or you use subprocess)
import subprocess
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
#linux.run_command(cmd)
try:
data = json.loads(result.stdout)
stream = data['streams'][0]
w = int(stream['width'])
h = int(stream['height'])
# Convert fractional FPS string (e.g. "60/1" or "30000/1001") to float
fps_parts = stream['r_frame_rate'].split('/')
fps = float(fps_parts[0]) / float(fps_parts[1]) if len(fps_parts) > 1 else float(fps_parts[0])
return w, h, fps
except Exception:
return 1920, 1080, 60.0 # Safe defaults if probe fails
def fit_to_9_16_letterbox(input_path: str, top_text: str = "TOP TEXT", bottom_text: str = "BOTTOM TEXT", use_blur: bool = True, force: bool = False):
threads = "8"
input_file = Path(input_path)
output_suffix = "gaussian_9_16" if use_blur else "black_9_16"
output_path = input_file.parent / f"{input_file.stem}_{output_suffix}{input_file.suffix}"
if os.path.exists(output_path):
if not force:
print(f"❌ Error: Short video already exists: {output_path}")
return
else:
os.remove(output_path)
# 1. Probe input metadata instantly
orig_w, orig_h, fps = get_video_info(input_file)
canvas_w = 1080
canvas_h = 1920
print("✍️ Generating text overlay graphics via MoviePy...")
# Render static images for text instead of running a video context
title_clip = TextClip(
text=top_text, font_size=55, color="white", font="DejaVuSans-Bold",
text_align="center", size=(canvas_w - 100, 300), method="caption"
)
bottom_clip = TextClip(
text=bottom_text, font_size=55, color="white", font="DejaVuSans-Bold",
text_align="center", size=(canvas_w - 100, 300), method="caption"
)
# Save text layers to temporary PNGs
top_png = tempfile.NamedTemporaryFile(suffix=".png", delete=False).name
bottom_png = tempfile.NamedTemporaryFile(suffix=".png", delete=False).name
title_clip.save_frame(top_png)
bottom_clip.save_frame(bottom_png)
title_clip.close()
bottom_clip.close()
print("🎬 Dispatching compilation workload to FFmpeg filtergraph...")
# 2. Build the complex FFmpeg filtergraph
# [0:v] is the raw input video stream
filter_complex = []
if use_blur:
# Scale height to 1920, crop center 1080x1920, apply fast boxblur (power of 3 approximates Gaussian)
filter_complex.append(
f"[0:v]scale=-1:{canvas_h},crop={canvas_w}:{canvas_h}:(iw-{canvas_w})/2:0,boxblur=luma_radius=35:luma_power=3[bg];"
)
else:
# Generate a pure black background canvas matching video frame specs
filter_complex.append(
f"color=c=black:s={canvas_w}x{canvas_h}:r={fps}[bg];"
)
# Scale the foreground video to a clean 1080 width, keeping aspect ratio
filter_complex.append(
f"[0:v]scale={canvas_w}:-1[fg];"
)
# Layer composition chain:
# Overlay 1: Put scaled foreground onto background (centered vertically)
filter_complex.append(
f"[bg][fg]overlay=0:(H-h)/2[tmp1];"
)
# Overlay 2: Drop top text asset onto position Y=180
filter_complex.append(
f"[tmp1][1:v]overlay=(W-w)/2:180[tmp2];"
)
# Overlay 3: Drop bottom text asset onto position Y=1430
filter_complex.append(
f"[tmp2][2:v]overlay=(W-w)/2:1430[finalv]"
)
filter_graph = "".join(filter_complex)
# 3. Execute the native assembly command
# -map_chapters -1 -sn: Strips unnecessary metadata chunks instantly
# -c:a copy: Safely pulls original digital audio directly without decompression cycles
# -threads 0: Forces FFmpeg to auto-consume all available processing cores
ffmpeg_cmd = (
f'ffmpeg -y -v error -i "{input_file}" -i "{top_png}" -i "{bottom_png}" '
f'-filter_complex "{filter_graph}" '
f'-map "[finalv]" -map 0:a? -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p '
f'-c:a copy -map_chapters -1 -sn -threads {threads} "{output_path}"'
)
try:
utils.run_command(ffmpeg_cmd)
print(f"🎉 High-speed processing complete! Video saved to: {output_path}")
finally:
# Clean up temporary PNG picture files safely
for path in (top_png, bottom_png):
if os.path.exists(path):
os.unlink(path)
if __name__ == "__main__":
import time
start_time = time.perf_counter()
creator_name = "greenskiesbluegrass"
top_txt = "TeamPGP Live on Twitch...\n Every Friday and Sunday @7:30 ET.\n twitch.tv/teampgp"
bottom_txt = f"Clipped By: {creator_name}."
fit_to_9_16_letterbox("download/clips/AbnegateAgitatedGrassPJSalt/AbnegateAgitatedGrassPJSalt.mp4", top_txt, bottom_txt, True, True)
end_time = time.perf_counter()
execution_time = end_time - start_time
print(f"The function took {execution_time:.6f} seconds to complete.")
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
import os
import argparse
from enum import Enum
from datetime import datetime
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from googleapiclient.errors import HttpError
import package.youtube.config as config
class CategoryId(Enum):
"""Official YouTube Category IDs for API Uploads."""
FILM_AND_ANIMATION = "1"
AUTOS_AND_VEHICLES = "2"
MUSIC = "10"
PETS_AND_ANIMALS = "15"
SPORTS = "17"
TRAVEL_AND_EVENTS = "19"
GAMING = "20"
PEOPLE_AND_BLOGS = "22"
COMEDY = "23"
ENTERTAINMENT = "24"
NEWS_AND_POLITICS = "25"
HOWTO_AND_STYLE = "26"
EDUCATION = "27"
SCIENCE_AND_TECHNOLOGY = "28"
NONPROFITS_AND_ACTIVISM = "29"
def upload_video(file_path: str, title: str, category: CategoryId, description: str = "", privacyStatus: str = 'private', tags: list = None, release_time: datetime = None) -> bool:
"""
Upload a video to YouTube
Args:
file_path (str): Path to the video file
title (str): Title of the Video
categoryId (str): categoryId of the video
description (str): Video description
privacyStatus (str): privacyStatus of the video
tags (str): tags to be use on the video
release_time (datetime): Optional timezone-aware UTC datetime object for timed release
schedule_date = datetime.now(timezone.utc) + timedelta(days=2)
"""
if tags is None:
tags = []
try:
# Check if file exists
if not os.path.exists(file_path):
print(f"❌ Error: File not found: {file_path}")
return False
# Load credentials
if not config.CREDENTIALS:
return False
# Create YouTube API client
youtube = build('youtube', 'v3', credentials=config.CREDENTIALS)
# Configure the status object dynamically
status_body = {
'selfDeclaredMadeForKids': False
}
if release_time is not None:
if release_time.tzinfo is None:
release_time = release_time.replace(tzinfo=timezone.utc)
# If release_time is passed, YouTube forces privacyStatus to 'private'
status_body['privacyStatus'] = 'private'
status_body['publishAt'] = release_time.strftime('%Y-%m-%dT%H:%M:%S.000Z')
print(f"Configuring timed release for: {status_body['publishAt']}")
else:
# Standard immediate upload
status_body['privacyStatus'] = privacyStatus
print(f"Configuring immediate upload with status: {privacyStatus}")
# Prepare the video upload request
body = {
'snippet': {
'title': title,
'description': description,
'tags': tags,
'categoryId': category.value
},
'status': status_body
}
# Create media file upload
media = MediaFileUpload(
file_path,
chunksize=1024*1024,
resumable=True
)
# Create the video insert request
insert_request = youtube.videos().insert(
part=','.join(body.keys()),
body=body,
media_body=media
)
print(f"Starting upload for '{title}'...")
response = None
while response is None:
status, response = insert_request.next_chunk()
if status:
print(f"Uploaded {int(status.progress() * 100)}%")
print(f"\nUpload Complete!")
print(f"Video ID: {response['id']}")
print(f"Title: {response['snippet']['title']}")
print(f"URL: https://youtu.be/{response['id']}")
# Output confirmation based on what was chosen
if 'publishAt' in response['status']:
print(f"Scheduled Release Time: {response['status']['publishAt']}")
else:
print(f"Current Privacy Status: {response['status']['privacyStatus']}")
return True
except HttpError as e:
print(f"An HTTP error occurred: {str(e)}")
return False
except Exception as e:
print(f"An error occurred: {str(e)}")
return False
def main():
parser = argparse.ArgumentParser(description='Upload a video to YouTube')
parser.add_argument('--file', required=True, help='Path to the video file')
parser.add_argument('--title', required=True, help='Title of the video')
parser.add_argument('--category', default='PEOPLE_AND_BLOGS', choices=[c.name for c in CategoryId], help='Video category genre')
parser.add_argument('--description', default='', help='Video description text')
parser.add_argument('--privacy', default='private', choices=['public', 'private', 'unlisted'], help='Video privacy settings')
# ADDED: Feature parsing to easily pass tags from the CLI split by commas
parser.add_argument('--tags', default='', help='Comma-separated tags list (e.g. "python,coding,api")')
# ADDED: Option to provide a scheduled upload timestamp natively from CLI
parser.add_argument('--schedule', default=None, help='UTC Release date/time in ISO format: YYYY-MM-DDTHH:MM:SS (e.g. 2026-08-15T14:30:00)')
args = parser.parse_args()
chosen_category = CategoryId[args.category]
parsed_tags = [t.strip() for t in args.tags.split(',')] if args.tags else []
# ADDED: Parse schedule string into datetime object dynamically
release_datetime = None
if args.schedule:
try:
# Assumes format matches CLI help instruction text
release_datetime = datetime.strptime(args.schedule, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc)
except ValueError:
print("Error: Schedule date must format strictly as YYYY-MM-DDTHH:MM:SS")
return
upload_video(
file_path=args.file,
title=args.title,
category=chosen_category,
description=args.description,
privacyStatus=args.privacy,
tags=parsed_tags,
release_time=release_datetime
)
if __name__ == "__main__":
main()