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

+1 -1
View File
@@ -14,7 +14,7 @@ os.makedirs(CACHE_DIR, exist_ok=True)
# 3. Expose Database Tools (Added)
# Replace 'DatabaseClient' and 'connect_to_db' with your actual class/function names
from .build_videos import main as build_videos
from .build_videos import build_videos
from .config import CHANNEL_NAME, DB
from .database import Database
from .transcribe_video import transcribe_to_srt
+4 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
import youtube_short
import package.youtube.shorts as yt_shorts
import package.transcribe_video as transcribe_video
import package.twitch.chat_vod as chat_vod
from package.config import DB as DB
@@ -108,16 +108,16 @@ def build_shorts():
print(f"🚀 Processing: Clip to Youtube Short {title}")
print("====================================================")
youtube_short.fit_to_9_16_letterbox(target_dir, top_txt, f"Clipped By: {creator_name}.")
yt_shorts.fit_to_9_16_letterbox(target_dir, top_txt, f"Clipped By: {creator_name}.")
print("====================================================")
print(f"✅ Processing: Clip to Youtube Short Done.")
print("====================================================")
def main():
def build_videos():
build_shorts()
build_transcribe()
build_chat_video()
if __name__ == "__main__":
main()
build_videos()
+3 -3
View File
@@ -2,9 +2,9 @@
# Expose components from individual modules using relative imports
from .chat_vod import combine_twitch_vod_and_chat
from .download_thumbnails import main as download_thumbnails
from .download_videos import main as download_videos
from .video_info import main as video_info
from .download_thumbnails import download_thumbnails
from .download_videos import download_videos
from .video_info import video_info
# Explicitly define what is exposed when importing the subpackage
__all__ = [
+3
View File
@@ -4,6 +4,9 @@ import cv2
import package.utils as utils
def get_video_height(video_path: str) -> int:
"""
Gets the videos height
"""
# Open the video file
video = cv2.VideoCapture(video_path)
+2 -2
View File
@@ -80,7 +80,7 @@ def save_image(url: str, filename: str, stream: bool = True):
except Exception as e:
print(f"❌ An error occurred during file writing: {e}")
async def main():
async def download_thumbnails():
# Initialize connection & automatically authorize the App token
await config.get_twitch()
twitch = config.TWITCH # Twitch(APP_ID, APP_SECRET)
@@ -141,4 +141,4 @@ async def main():
await download_clip_thumbnail(id, thumbnail_url)
if __name__ == '__main__':
asyncio.run(main())
asyncio.run(download_thumbnails())
+2 -5
View File
@@ -14,7 +14,7 @@ def write_csv(data, file_name):
# Write all rows at once
writer.writerows(data)
def download():
def download_videos():
"""Find all undownloaded videos and clips, and download them safely."""
undownloaded = DB.get_undownloaded()
@@ -96,8 +96,5 @@ def download():
print("Finished Downloading Pipeline.")
def main():
download()
if __name__ == "__main__":
main()
download_videos()
+5 -5
View File
@@ -40,7 +40,7 @@ async def get_game_name_by_id(game_id: str) -> str:
pass
return "Unknown Game"
async def get_vod_game_name(vod_id: str):
async def get_vod_game_name(vod_id: str) -> tuple:
"""Asynchronously query Twitch GQL endpoint for VOD game metadata."""
game_id = "0"
game_name = "Unknown Game"
@@ -83,7 +83,7 @@ async def get_vod_game_name(vod_id: str):
return game_id, game_name
async def get_streamer_vods():
async def get_streamer_vods() -> list:
await config.get_twitch()
print(f"Starting VOD extraction for {config.USER.display_name}...")
@@ -121,7 +121,7 @@ async def get_streamer_vods():
print(f"\nFinished extracting VODs. Total gathered: {len(all_vods)}")
return all_vods
async def get_streamer_clips():
async def get_streamer_clips() -> list:
await config.get_twitch()
print(f"Starting clip extraction for {config.USER.display_name}...")
@@ -151,7 +151,7 @@ async def get_streamer_clips():
print(f"\nFinished extracting clips. Total gathered: {len(all_clips)}")
return all_clips
async def main():
async def video_info():
print("--- Script Started ---")
vods_list = await get_streamer_vods()
@@ -179,4 +179,4 @@ async def main():
print(f"Recent VOD: '{clips_list[0]['title']}'")
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(video_info())
+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()