From 483a771575db55aaffad4983ddae6da6b6768d4a Mon Sep 17 00:00:00 2001 From: Richard Allen Date: Fri, 17 Jul 2026 23:08:26 -0400 Subject: [PATCH] fix and working Clips downloader/uploader --- .vscode/launch.json | 21 ++++++++++++++++++++ database.py | 7 ++++--- twitch_download_clips.py | 18 +++++++++-------- uploader.py | 33 ++++++++++++++++++++---------- youtube_short.py | 43 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 youtube_short.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3bbaa4a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Python File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "args": [ + "--file", "my_video.mp4", + "--title", "My Automated Video Title", + "--category", "SCIENCE_AND_TECHNOLOGY", + "--description", "Testing out the Data API!", + "--privacy", "private" + ] + } + ] +} \ No newline at end of file diff --git a/database.py b/database.py index 9178328..eb9f9c7 100644 --- a/database.py +++ b/database.py @@ -37,7 +37,8 @@ class Database: clip_by TEXT NOT NULL, view_count INTEGER NOT NULL, downloaded INTEGER NOT NULL, - uploaded_yt INTEGER NOT NULL + uploaded_yt INTEGER NOT NULL, + shorts_upload_yt INTEGER NOT NULL ) """ ) @@ -71,11 +72,11 @@ class Database: if self.table == "clips": id = "slug" - CURSOR.execute( + self.CURSOR.execute( f"UPDATE {self.table} SET uploaded_yt = 1 WHERE {id} = ?", (record_id,) ) - CONN.commit() + self.CONN.commit() def mark_as_downloaded(self, record_id: int): """Updates the downloaded status to True (1) for a specific record ID.""" diff --git a/twitch_download_clips.py b/twitch_download_clips.py index 68429a8..5244cf4 100644 --- a/twitch_download_clips.py +++ b/twitch_download_clips.py @@ -24,21 +24,21 @@ def run_linux_command(command: str): def download_clips(): """Loops over undownloaded metadata entries to write files down locally.""" - undownloaded_clips = get_undownloaded_clips() + undownloaded_clips = DB.get_undownloaded() print("Downloading Clips...") - for slug, record_date, title, game_name, clip_by, views, downloaded in undownloaded_clips: + for slug, record_date, title, game_name, clip_by, views, downloaded, uploaded_yt in undownloaded_clips: print(f"Slug: {slug} | Date: {record_date} | Game: {game_name} | By: {clip_by} | Views: {views} | Title: {title}") # Uses standard clipdownload directive - output = run_linux_command(f"TwitchDownloaderCLI clipdownload --id {slug} -o save/clips/{slug}/{title}.mp4") + output = run_linux_command(f"TwitchDownloaderCLI clipdownload --id {slug} -o save/clips/{slug}/{slug}.mp4") if output["success"] is True: print(f"Slug: {slug} | Was successfully downloaded.") DB.mark_as_downloaded(slug) else: - print(f"Slug: {slug} | Download Process failed.") + print(f"Slug: {slug} | Download Process failed. {output["stdout"]}. Error: {output["stderr"]}") print("Finished Downloading Clips...") @@ -46,19 +46,21 @@ def upload_clips(): """Loops over the downloaded videos entries and uploaded them to youtube.""" print("Uploading Clips...") - unpuloaded_clips = get_unuploaded_clips() + unpuloaded_clips = DB.get_unuploaded() + + categoryId = CategoryId.GAMING for slug, record_date, title, game_name, clip_by, views, downloaded, uploaded_yt in unpuloaded_clips: print(f"Slug: {slug} | Date: {record_date} | Game: {game_name} | By: {clip_by} | Views: {views} | Title: {title}") file_path = f"save/clips/{slug}/{slug}.mp4" title = title - description = f"Game: {game_name}, Cliped By: {clip_by}, on {record_date}, #Shorts" + description = f"Game: {game_name}, Cliped By: {clip_by}, on {record_date}, #Shorts #Clips #Twitch" categoryId = CategoryId.GAMING privatcyStatus = 'private' - tags = ['shorts', 'gaming', 'TeamPGP', f'{game_name}', f'{clip_by}'] + tags = ['shorts', 'gaming', 'TeamPGP', f'{game_name}', f'{clip_by}', 'twitch_clips', 'clips'] - output = uploader.upload_video(file_path, title, description, categoryId, privatcyStatus, tags) + output = uploader.upload_video(file_path, title, categoryId, description, privatcyStatus, tags) if output is True: print(f"Slug: {slug} | Was successfully uploaded.") diff --git a/uploader.py b/uploader.py index 369af61..84fdc9b 100644 --- a/uploader.py +++ b/uploader.py @@ -86,10 +86,6 @@ def upload_video(file_path: str, title: str, category: CategoryId, description: if not credentials: return False - # Format release time to ISO 8601 UTC format (Required by YouTube) - # Example format generated: "2026-07-20T15:00:00Z" - publish_at_iso = release_time.strftime('%Y-%m-%dT%H:%M:%S.000Z') - # Create YouTube API client youtube = build('youtube', 'v3', credentials=credentials) @@ -98,7 +94,10 @@ def upload_video(file_path: str, title: str, category: CategoryId, description: 'selfDeclaredMadeForKids': False } - if release_time: + 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') @@ -119,7 +118,6 @@ def upload_video(file_path: str, title: str, category: CategoryId, description: 'status': status_body } - # Create media file upload media = MediaFileUpload( file_path, @@ -162,12 +160,27 @@ def upload_video(file_path: str, title: str, category: CategoryId, description: return False def main(): + # FIXED: Reconstructed arguments to provide all required inputs for upload_video parser = argparse.ArgumentParser(description='Upload a video to YouTube') - parser.add_argument('file', help='Path to the video file') - parser.add_argument('description', help='Video description') + 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') args = parser.parse_args() - upload_video(args.file, args.description) + + # Map the text input choice back to your CategoryId Enum object + chosen_category = CategoryId[args.category] + + # FIXED: Properly pass all required positional and optional parameters + upload_video( + file_path=args.file, + title=args.title, + category=chosen_category, + description=args.description, + privacyStatus=args.privacy + ) if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/youtube_short.py b/youtube_short.py new file mode 100644 index 0000000..775c0db --- /dev/null +++ b/youtube_short.py @@ -0,0 +1,43 @@ +import sys +from moviepy.editor import VideoFileClip + +def convert_to_short(input_path, output_path="youtube_short.mp4"): + # Load the video file + clip = VideoFileClip(input_path) + + # 1. Check video duration + if clip.duration >= 60: + print(f"❌ Error: Video is {clip.duration:.2f}s long. It must be under 60 seconds.") + clip.close() + return False + + print(f"✅ Video length check passed ({clip.duration:.2f}s). Processing...") + + # 2. Calculate the target width for a 9:16 aspect ratio based on height + target_height = clip.h + target_width = int(target_height * (9 / 16)) + + # 3. Crop the center of the video + # x_center centers the crop horizontally + short_clip = clip.crop(width=target_width, height=target_height, x_center=clip.w / 2, y_center=clip.h / 2) + + # Optional: Resize to standard YouTube Short resolution (1080x1920) + if short_clip.h != 1920: + short_clip = short_clip.resize(newsize=(1080, 1920)) + + # 4. Export the final video file + short_clip.write_videofile( + output_path, + codec="libx264", + audio_codec="aac", + fps=clip.fps + ) + + # Clean up files + clip.close() + short_clip.close() + print(f"🎉 Success! Short saved as: {output_path}") + return True + +if __name__ == "__main__": + convert_to_short() \ No newline at end of file