diff --git a/build_videos.py b/build_videos.py index 0c60f96..7a195de 100644 --- a/build_videos.py +++ b/build_videos.py @@ -30,10 +30,14 @@ def build_chat_video(): ) = chat print("====================================================") - print(f"๐Ÿš€ Transcribe: {title}") + print(f"๐Ÿš€ Chat Video: {title}") print("====================================================") - twitch_chat_vod.combine_twitch_vod_and_chat(f"download/videos/{id}/{id}.mp4", "side-by-side", True) + twitch_chat_vod.combine_twitch_vod_and_chat(f"download/videos/{id}/{id}.mp4", "side-by-side") + + print("====================================================") + print(f"โœ… Processing: Chat Video Done.") + print("====================================================") def build_transcribe(): @@ -68,7 +72,11 @@ def build_transcribe(): print(f"๐Ÿš€ Transcribe: {title}") print("====================================================") - transcribe_video.transcribe_to_srt(f"{target_dir}/{id}/{id}.mp4", True) + transcribe_video.transcribe_to_srt(f"{target_dir}/{id}/{id}.mp4") + + print("====================================================") + print(f"โœ… Processing: Transcribing Done.") + print("====================================================") def build_shorts(): @@ -102,7 +110,11 @@ 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}.", True) + youtube_short.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(): global DB diff --git a/database.py b/database.py index 7ae7f4d..96abb45 100755 --- a/database.py +++ b/database.py @@ -16,7 +16,7 @@ class Database: if not file_exists: self.create_database() - def __del__(self): + def __exit__(self): # Destructors are unpredictable in Python; explicitly close when done instead try: self.close_database() @@ -102,11 +102,20 @@ class Database: self.cursor.execute(f"SELECT {self.columns} FROM twitch_videos WHERE downloaded = 0") return self.cursor.fetchall() + def get_download(self) -> list[Any]: + """Retrieves all rows that were downloaded""" + self.cursor.execute(f"SELECT {self.columns} FROM twitch_videos WHERE downloaded = 1") + return self.cursor.fetchall() + def get_clips(self) -> list[Any]: """Retrieves all rows that are clip_is""" self.cursor.execute(f"SELECT {self.columns} FROM twitch_videos WHERE clip_is = 1") return self.cursor.fetchall() + def get_vods(self) -> list[Any]: + self.cursor.execute(f"SELECT {self.columns} FROM twitch_videos WHERE clip_is = 0") + return self.cursor.fetchall() + def insert_video_record( self, id: str, title: str, created_at: str, view_count: int, duration: str, url: str, thumbnail_url: str, game_id: int, game_name: str, stream_id: str, @@ -120,10 +129,23 @@ class Database: # Explicitly defining columns removes the security risk and column-count bug query = """ - INSERT OR IGNORE INTO twitch_videos ( + INSERT INTO twitch_videos ( id, title, created_at, view_count, duration, url, thumbnail_url, game_id, game_name, stream_id, creator_name, clip_is ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + title = excluded.title, + created_at = excluded.created_at, + view_count = excluded.view_count, + duration = excluded.duration, + url = excluded.url, + thumbnail_url = excluded.thumbnail_url, + game_id = excluded.game_id, + game_name = excluded.game_name, + stream_id = excluded.stream_id, + creator_name = excluded.creator_name, + clip_is = excluded.clip_is + WHERE excluded.duration != twitch_videos.duration """ values = ( diff --git a/main.py b/main.py index 2cdfdbe..b30d4d4 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,7 @@ import asyncio import twitch_video_info import twitch_download_videos +import twitch_download_thumbnails import build_videos @@ -13,5 +14,7 @@ if __name__ == "__main__": # Download Twitch Videos twitch_download_videos.main() + twitch_download_thumbnails.main() + #Build build_videos.main() diff --git a/remove_uneed_files.py b/remove_uneed_files.py new file mode 100644 index 0000000..4f0cc6a --- /dev/null +++ b/remove_uneed_files.py @@ -0,0 +1,72 @@ +import os + +import database + +DB = None + +def remove_files(): + datas = DB.get_download() + + top_txt = "TeamPGP Live on Twitch...\n Every Friday and Sunday @7:30 ET.\n twitch.tv/teampgp" + + for data in datas: + ( + id, + title, + created_at, + view_count, + duration, + url, + thumbnail_url, + game_id, + game_name, + stream_id, + creator_name, + clip_is, + downloaded, + uploaded_yt, + uploaded_yt_chats, + uploaded_yt_shorts, + ) = data + + if clip_is == 0: + video_path = f"download/videos/{id}/{id}.mp4" + else: + video_path = f"download/clips/{id}/{id}.mp4" + + temp_chat = video_path.replace(".mp4", "_temp_chat.mp4") + temp_with_chat = video_path.replace(".mp4", "_temp_with_chat.mp4") + video_chat = video_path.replace(".mp4", "_with_chat.mp4") + video_chatSS = video_path.replace(".mp4", "_SS_with_chat.mp4") + video_chat_over = video_path.replace(".mp4", "_over_with_chat.mp4") + mask_path = video_path.replace(".mp4", "_temp_chat_mask.mp4") + + shorts_gaussian_9_16 = ""#video_path.replace(".mp4", "_gaussian_9_16.mp4") + shorts_9_16 = ""#video_path.replace(".mp4", "_9_16.mp4") + + srt_file = ""#video_path.replace(".mp4", ".srt") + + + files = [temp_chat, temp_with_chat, video_chatSS, video_chat_over, mask_path, shorts_gaussian_9_16, shorts_9_16, srt_file, video_chat] + + for file in files: + if os.path.exists(file): + if file == video_path: + print("โŒ Error: trying to delete import file.") + return + else: + os.remove(file) + +def redownload(): + DB.unmark_as_download("2840110867") + + +def main(): + global DB + DB = database.Database() + + remove_files() + redownload() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/twitch_chat_vod.py b/twitch_chat_vod.py index 2fc4a0f..9d6595b 100644 --- a/twitch_chat_vod.py +++ b/twitch_chat_vod.py @@ -10,9 +10,12 @@ def combine_twitch_vod_and_chat(vod_path: str, layout: str = "side-by-side", for threads = "8" video_path = vod_path + video_chat = "" chat_path = video_path.replace(".mp4", "_chat.json") + temp_with_chat = video_path.replace(".mp4", "_temp_with_chat.mp4") temp_chat = video_path.replace(".mp4", "_temp_chat.mp4") - video_chat = video_path.replace(".mp4", "_with_chat.mp4") + video_chat_SS = video_path.replace(".mp4", "_SS_with_chat.mp4") + video_chat_over = video_path.replace(".mp4", "_over_with_chat.mp4") mask_path = video_path.replace(".mp4", "_temp_chat_mask.mp4") # Step 1: Pre-flight checks @@ -23,7 +26,17 @@ def combine_twitch_vod_and_chat(vod_path: str, layout: str = "side-by-side", for if not os.path.exists(chat_path): print(f"โŒ Error: Chat file not found: {chat_path}") return False - + + if os.path.exists(temp_with_chat): + os.remove(temp_with_chat) + + if layout == "side-by-side": + video_chat = video_chat_SS + elif layout == "overlay": + video_chat = video_chat_over + else: + return False + if os.path.exists(video_chat): if not force: print(f"โŒ Error: Chat video already exists: {video_chat}") @@ -33,6 +46,7 @@ def combine_twitch_vod_and_chat(vod_path: str, layout: str = "side-by-side", for if os.path.exists(temp_chat): os.remove(temp_chat) + if os.path.exists(mask_path): os.remove(mask_path) # Step 2: Render Chat to Video @@ -74,16 +88,16 @@ def combine_twitch_vod_and_chat(vod_path: str, layout: str = "side-by-side", for if layout == "side-by-side": ffmpeg_cmd = ( f"ffmpeg -y -i {video_path} -i {temp_chat} " - f"-filter_complex '[1:v]scale=-1:H[scaled_chat];[0:v][scaled_chat]hstack=inputs=2[v]' " + f"-filter_complex '[1:v]scale=-1:ih[scaled_chat];[0:v][scaled_chat]hstack=inputs=2[v]' " f"-map '[v]' -map 0:a? -c:v libx264 -crf 18 -preset slow -c:a copy " - f"-threads {threads} {video_chat}" + f"-threads {threads} {temp_with_chat}" ) elif layout == "overlay": ffmpeg_cmd = ( f"ffmpeg -y -i {video_path} -i {temp_chat} -i {mask_path} " f"-filter_complex '[1:v][2:v]alphamerge[masked_chat];[0:v][masked_chat]overlay=x=0:y=10[v]' " f"-map '[v]' -map 0:a? -c:v libx264 -crf 18 -preset slow -c:a copy " - f"-threads {threads} {video_chat}" + f"-threads {threads} {temp_with_chat}" ) else: raise ValueError("Invalid layout choice. Choose 'side-by-side' or 'overlay'.") @@ -91,7 +105,8 @@ def combine_twitch_vod_and_chat(vod_path: str, layout: str = "side-by-side", for ffmpeg_success = linux.run_command(ffmpeg_cmd, progress_prefix="FFmpeg Merge") # Step 4: Final verification and cleanup - if ffmpeg_success and os.path.exists(video_chat): + if ffmpeg_success and os.path.exists(temp_with_chat): + os.rename(temp_with_chat, video_chat) print("====================================================") print(f"๐ŸŽ‰ SUCCESS: Video processing complete!") print(f"๐Ÿ“ Output Saved: {video_chat}") @@ -101,11 +116,17 @@ def combine_twitch_vod_and_chat(vod_path: str, layout: str = "side-by-side", for if os.path.exists(temp_chat): print("๐Ÿงน Cleaning up temporary chat render video...") os.remove(temp_chat) + if os.path.exists(mask_path): os.remove(mask_path) + if os.path.exists(temp_with_chat): + os.remove(temp_with_chat) return True else: print("โŒ Error: FFmpeg failed to merge the video streams.") - os.remove(video_chat) + if os.path.exists(video_chat): + os.remove(video_chat) + if os.path.exists(temp_with_chat): + os.remove(temp_with_chat) return False if __name__ == "__main__": diff --git a/twitch_download_thumbnails.py b/twitch_download_thumbnails.py new file mode 100644 index 0000000..65a5bbb --- /dev/null +++ b/twitch_download_thumbnails.py @@ -0,0 +1,162 @@ +import os +import json +import asyncio +import requests +from twitchAPI.twitch import Twitch +from twitchAPI.helper import first + +import database + +# 1. Fill in your credentials from the Twitch Developer Console +SECRETS = None +TWITCH = None +USER = None +CHANNEL_NAME = "teampgp" + +async def get_twitch(): + global SECRETS, TWITCH, USER + if SECRETS is None: + with open('twitch_secrets.json', 'r') as f: + SECRETS = json.load(f) + if TWITCH is None: + TWITCH = await Twitch(SECRETS['client_id'], SECRETS['client_secret']) + if USER is None: + USER = await first(TWITCH.get_users(logins=[CHANNEL_NAME])) + if not USER: + print("User not found.") + + +async def download_live_thumbnail(twitch_client, streamer_username: str, w: int = 1920, h: int = 1080): + """Fetches and saves the live stream thumbnail for an active broadcast.""" + print(f"๐Ÿ”Ž Checking live status for: {streamer_username}...") + + # Query the live streams endpoint + stream_generator = twitch_client.get_streams(user_logins=[streamer_username]) + stream_data = await first(stream_generator) + + if not stream_data: + print(f"โŒ User '{streamer_username}' is offline. Live thumbnails require an active stream.") + return + + # Twitch API live streams use the {width} and {height} format + raw_url = stream_data.thumbnail_url + clean_url = raw_url.replace('{width}', str(w)).replace('{height}', str(h)) + + filename = f"live_{streamer_username}_{w}x{h}.jpg" + save_image(clean_url, filename) + +async def download_vod_thumbnail(twitch_client, vod_id: str, w: int = 1920, h: int = 1080): + """Fetches and saves a thumbnail from a past broadcast VOD ID.""" + print(f"๐Ÿ”Ž Searching for VOD ID: {vod_id}...") + + # Query the videos endpoint + video_generator = twitch_client.get_videos(vod_id) + video_data = await first(video_generator) + + filename = f"download/videos/{vod_id}/{vod_id}_{w}x{h}.jpg" + if os.path.exists(filename): + return + + if not video_data: + print(f"โŒ VOD ID {vod_id} could not be found.") + return + + # Twitch VOD endpoints typically format string tokens as %{width} and %{height} + raw_url = video_data.thumbnail_url + if not raw_url: + print("โŒ This VOD does not have an available thumbnail.") + return + + clean_url = raw_url.replace('%{width}', str(w)).replace('%{height}', str(h)) + + #filename = f"vod_{vod_id}_{w}x{h}.jpg" + save_image(clean_url, filename) + +async def download_clip_thumbnail(clip_id: str, url: str): + print(f"๐Ÿ”Ž Searching for Clip ID: {clip_id}...") + + filename = f"download/clips/{clip_id}/{clip_id}.jpg" + if os.path.exists(filename): + return + + save_image(url, filename, False) + +def save_image(url: str, filename: str, stream: bool = True): + """Helper function to stream image bytes directly to a file.""" + try: + response = requests.get(url, stream) + if response.status_code == 200: + with open(filename, 'wb') as file: + if stream is True: + for chunk in response.iter_content(1024): + file.write(chunk) + else: + file.write(response.content) + print(f"โœ… Success! Saved as: {filename}") + else: + print(f"โŒ Download failed. HTTP Status: {response.status_code}") + except Exception as e: + print(f"โŒ An error occurred during file writing: {e}") + +async def main(): + # Initialize connection & automatically authorize the App token + await get_twitch() + twitch = TWITCH # Twitch(APP_ID, APP_SECRET) + + # --- OPTION A: Download Live Thumbnail --- + # Target user must be streaming live right now + target_streamer = CHANNEL_NAME + #await download_live_thumbnail(twitch, target_streamer, 1920, 1080) + + # --- OPTION B: Download Past Broadcast VOD Thumbnail --- + # Extract the ID sequence from your target video link + # target_vod = "2145678901" + db = database.Database() + vods = db.get_vods() + for vod in vods: + ( + id, + title, + created_at, + view_count, + duration, + url, + thumbnail_url, + game_id, + game_name, + stream_id, + creator_name, + clip_is, + downloaded, + uploaded_yt, + uploaded_yt_chats, + uploaded_yt_shorts, + ) = vod + + await download_vod_thumbnail(twitch, id, 1920, 1080) + + clips = db.get_clips() + for clip in clips: + ( + id, + title, + created_at, + view_count, + duration, + url, + thumbnail_url, + game_id, + game_name, + stream_id, + creator_name, + clip_is, + downloaded, + uploaded_yt, + uploaded_yt_chats, + uploaded_yt_shorts, + ) = clip + + await download_clip_thumbnail(id, thumbnail_url) + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/twitch_download_videos.py b/twitch_download_videos.py index eb73187..db48d3c 100755 --- a/twitch_download_videos.py +++ b/twitch_download_videos.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import csv import subprocess +from datetime import datetime, timedelta, timezone import linux from database import Database @@ -42,6 +43,24 @@ def download(): uploaded_yt_shorts, ) = row + # 1. Define your data's timestamp (Example: April 10, 2026, at 10:00 AM) + #datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%SZ") + data_timestamp = datetime.fromisoformat(created_at) + + # 2. Get the exact current date and time + current_time = datetime.now(timezone.utc) + + # 3. Calculate the difference between the two times + time_difference = current_time - data_timestamp + + # 4. Check if the difference is greater than 24 hours + if time_difference > timedelta(hours=24): + print("The data is more than 24 hours old.") + else: + #lets wait 24 hours befor downloading + print("The data is less than 24 hours old.") + continue + data = [list(row)] print( f"ID: {id} | Date: {created_at} | Game: {game_name} | Title: {title}"