From b573ca05aae9a99bc2107ed15ff106e4c6d602fc Mon Sep 17 00:00:00 2001 From: Richard Allen Date: Fri, 17 Jul 2026 14:59:58 -0400 Subject: [PATCH] code before moving database code to its own file. --- database.py | 50 ++++++++++++++++++++ twitch_download_clips.py | 11 +++-- twitch_download_vod.py | 29 ++++++++---- uploader.py | 99 ++++++++++++++++++++++------------------ 4 files changed, 132 insertions(+), 57 deletions(-) create mode 100644 database.py diff --git a/database.py b/database.py new file mode 100644 index 0000000..0cd16da --- /dev/null +++ b/database.py @@ -0,0 +1,50 @@ +import sqlite3 + +# Stores data locally +CONN = sqlite3.connect("database.db") +CURSOR = CONN.cursor() + +class Database(): + def __init__(): + file_path = Path("database.db") + + if file_path.is_file(): + self.create_database() + + def create_database(): + """Creates a table structured explicitly for Twitch clip properties.""" + CURSOR.execute( + """ + CREATE TABLE IF NOT EXISTS clips ( + slug TEXT PRIMARY KEY, + date TEXT NOT NULL, + title TEXT NOT NULL, + gamename TEXT NOT NULL, + clip_by TEXT NOT NULL, + view_count INTEGER NOT NULL, + downloaded INTEGER NOT NULL, + uploaded_yt INTEGER NOT NULL + ) + """ + ) + + CURSOR.execute( + """ + CREATE TABLE IF NOT EXISTS vods ( + id INTEGER PRIMARY KEY, + date TEXT NOT NULL, + title TEXT NOT NULL, + gamename TEXT NOT NULL, + downloaded INTEGER NOT NULL, + uploaded_yt INTEGER NOT NULL, + chat_upload_yt INTEGER NOT NULL + ) + """ + ) + + CONN.commit() + + def close_database(): + """Commit before we close.""" + CONN.commit() + CONN.close() \ No newline at end of file diff --git a/twitch_download_clips.py b/twitch_download_clips.py index b30a146..2899d54 100644 --- a/twitch_download_clips.py +++ b/twitch_download_clips.py @@ -70,14 +70,15 @@ def upload_clips(): file_path = f"save/clips/{slug}/{slug}.mp4" title = title - description = f"Game: {game_name}, Cliped By: {clip_by}, on {record_date}" - categoryId = CategoryId.SHORTS + description = f"Game: {game_name}, Cliped By: {clip_by}, on {record_date}, #Shorts" + categoryId = CategoryId.GAMING privatcyStatus = 'private' - tags = ['#SHORT', '#GAMING', '#TeamPGP', f'#{game_name}', f'#{clip_by}'] + tags = ['shorts', 'gaming', 'TeamPGP', f'{game_name}', f'{clip_by}'] - if (uploader.upload_video(file_path, title, description, categoryId, privatcyStatus, tags) is True): + output = uploader.upload_video(file_path, title, description, categoryId, privatcyStatus, tags) + + if output is True: print(f"Slug: {slug} | Was successfully uploaded.") - mark_as_uploaded(slug) else: print(f"Slug: {slug} | Upload Process failed.") diff --git a/twitch_download_vod.py b/twitch_download_vod.py index e884deb..10a4727 100644 --- a/twitch_download_vod.py +++ b/twitch_download_vod.py @@ -21,7 +21,8 @@ def create_database(): title TEXT NOT NULL, gamename TEXT NOT NULL, downloaded INTEGER NOT NULL, - uploaded_yt INTEGER NOT NULL + uploaded_yt INTEGER NOT NULL, + chat_upload_yt INTEGER NOT NULL ) """ ) @@ -41,8 +42,8 @@ def download_vods(): for record_id, record_date, title, game_name, downloaded in undownload_vods: print(f"ID: {record_id} | Date: {record_date} | Game: {game_name} | Title: {title}") - output = run_linux_command(f"TwitchDownloaderCLI videodownload --id {record_id} -o save/{record_id}/{record_id}.mp4") - output2 = run_linux_command(f"TwitchDownloaderCLI chatdownload --id {record_id} -o save/{record_id}/{record_id}_chat.json -E") + output = run_linux_command(f"TwitchDownloaderCLI videodownload --id {record_id} -o save/vod/{record_id}/{record_id}.mp4") + output2 = run_linux_command(f"TwitchDownloaderCLI chatdownload --id {record_id} -o save/vod/{record_id}/{record_id}_chat.json -E") if output["success"] is True: print(f"ID: {record_id} | Date: {record_date} | Game: {game_name} | Title: {title} | was successful") mark_as_downloaded(record_id) @@ -77,6 +78,20 @@ def mark_as_downloaded(record_id: int): CONN.commit() +def get_unuploaded_clips(): + """Retrieves all clip rows remaining to be chat uploaded.""" + CURSOR.execute( + "SELECT id, date, title, gamename, clip_by, view_count, downloaded, uploaded_yt, chat_uploaded_yt FROM vods WHERE downloaded = 1 AND chat_uploaded_yt = 0" + ) + return CURSOR.fetchall() + +def get_unuploaded_clips(): + """Retrieves all clip rows remaining to be uploaded.""" + CURSOR.execute( + "SELECT id, date, title, gamename, clip_by, view_count, downloaded, uploaded_yt FROM vods WHERE downloaded = 1 AND uploaded_yt = 0" + ) + return CURSOR.fetchall() + def get_undownloaded_vods(): """Retrieves all rows where downloaded status is False (0).""" @@ -84,9 +99,7 @@ def get_undownloaded_vods(): CURSOR.execute( "SELECT id, date, title, gamename, downloaded FROM vods WHERE downloaded = 0" ) - records = CURSOR.fetchall() - - return records + return CURSOR.fetchall() def insert_record(record_id: int, record_date: datetime_date, title: str, gamename: str): """Inserts a record with ID, date, gamename, and title into a SQLite database.""" @@ -94,8 +107,8 @@ def insert_record(record_id: int, record_date: datetime_date, title: str, gamena # Inserts data using parameterized queries to prevent SQL injection CURSOR.execute( - "INSERT OR IGNORE INTO vods (id, date, title, gamename, downloaded, uploaded_yt) VALUES (?, ?, ?, ?, ?, ?)", - (record_id, str(record_date), title, gamename, False, False), + "INSERT OR IGNORE INTO vods (id, date, title, gamename, downloaded, uploaded_yt, chat_uploaded_yt) VALUES (?, ?, ?, ?, ?, ?, ?)", + (record_id, str(record_date), title, gamename, False, False, False), ) # Saves changes and closes the connection diff --git a/uploader.py b/uploader.py index b5d0856..369af61 100644 --- a/uploader.py +++ b/uploader.py @@ -2,7 +2,8 @@ import os import json import argparse -from enum import Enum, auto, unique +from enum import Enum +from datetime import datetime from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request @@ -10,40 +11,24 @@ from googleapiclient.discovery import build from googleapiclient.http import MediaFileUpload from googleapiclient.errors import HttpError -@unique class CategoryId(Enum): - FILM_ANIMATION = 1 - AUTOS_VEHICLES = 2 - MUSIC = 10 - PETS_ANIMALS = 15 - SPORTS = 17 - SHORT_MOVIES = 18 - TRAVEL_EVENTS = 19 - GAMING = 20 - VIDEOBLOGGING = 21 - PEOPLE_BLOGS = 22 - COMEDY = 23 - ENTERTAINMENT = 24 - NEWS_POLITICS = 25 - HOWTO_STYLE = 26 - EDUCATION = 27 - SCIENCE_TECHNOLOGY = 28 - NONPROFITS_ACTIVISM = 29 - MOVIES = 30 - ANIME_ANIMATION = 31 - ACTION_ADVENTURE = 32 - CLASSICS = 33 - COMEDY_MOVIE = 34 # Renamed to avoid name collision - DOCUMENTARY = 35 - DRAMA = 36 - FAMILY = 37 - FOREIGN = 38 - HORROR = 39 - SCI_FI_FANTASY = 40 - THRILLER = 41 - SHORTS = 42 - SHOWS = 43 - TRAILERS = 44 + """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 load_credentials(): """Load credentials from secrets.json""" @@ -72,18 +57,24 @@ def load_credentials(): print(f"Error loading credentials: {str(e)}") return None -def upload_video(file_path: str, title: str, description: str, category: CategoryId, privacyStatus: str = 'private', tags: list = []): +def upload_video(file_path: str, title: str, category: CategoryId, description: str = "", privacyStatus: str = 'private', tags: list = None, release_time: datetime = None): """ Upload a video to YouTube Args: file_path (str): Path to the video file title (str): Title of the Video - description (str): Video description 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): @@ -95,11 +86,27 @@ def upload_video(file_path: str, title: str, description: str, category: Categor 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) - # Get the filename without extension as default title - title = os.path.splitext(os.path.basename(file_path))[0] + # Configure the status object dynamically + status_body = { + 'selfDeclaredMadeForKids': False + } + + if release_time: + # 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 = { @@ -107,12 +114,9 @@ def upload_video(file_path: str, title: str, description: str, category: Categor 'title': title, 'description': description, 'tags': tags, - 'categoryId': str(category.value) # Default to 'People & Blogs' category + 'categoryId': category.value }, - 'status': { - 'privacyStatus': privacyStatus, # Default to private - 'selfDeclaredMadeForKids': False - } + 'status': status_body } @@ -130,7 +134,7 @@ def upload_video(file_path: str, title: str, description: str, category: Categor media_body=media ) - print("Starting upload...") + print(f"Starting upload for '{title}'...") response = None while response is None: status, response = insert_request.next_chunk() @@ -141,6 +145,13 @@ def upload_video(file_path: str, title: str, description: str, category: Categor 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: