Compare commits

..
2 Commits
5 changed files with 227 additions and 159 deletions
+2
View File
@@ -2,3 +2,5 @@
/client_secrets.json /client_secrets.json
/clips_database.db /clips_database.db
__pycache__/uploader.cpython-313.pyc __pycache__/uploader.cpython-313.pyc
database.db
__pycache__/database.cpython-313.pyc
+116
View File
@@ -0,0 +1,116 @@
import sqlite3
from datetime import datetime
from datetime import date as datetime_date
from pathlib import Path
class Database:
def __init__(self, table):
self.table = table
self.columns = ""
# IMPORTANT Must check if database.db exists before connecting to it.
file_exists = Path("database.db").is_file()
self.CONN = sqlite3.connect("database.db")
self.CURSOR = self.CONN.cursor()
if self.table == "vods":
self.columns = "id, date, title, gamename, downloaded, uploaded_yt, chat_upload_yt"
elif self.table == "clips":
self.columns = "slug, date, title, gamename, clip_by, view_count, downloaded, uploaded_yt"
if not file_exists:
self.create_database()
def __del__(self):
self.close_database()
def create_database(self):
"""Creates a table structured explicitly for Twitch clip properties."""
self.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
)
"""
)
"""Creates a table structured explicitly for Twitch vods properties."""
self.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
)
"""
)
self.CONN.commit()
def close_database(self):
"""Commit before we close."""
self.CONN.commit()
self.CONN.close()
def mark_as_downloaded(self, record_id: int):
"""Updates the downloaded status to True (1) for a specific record ID."""
# Updates the row matching the specific ID
# TODO clips uses slug
self.CURSOR.execute(
f"UPDATE {self.table} SET downloaded = 1 WHERE id = ?",
(record_id,)
)
self.CONN.commit()
def get_unuploaded(self):
"""Retrieves all clip rows remaining to be chat uploaded."""
self.CURSOR.execute(f"SELECT {self.columns} FROM {self.table} WHERE downloaded = 1 AND uploaded_yt = 0")
return self.CURSOR.fetchall()
def get_undownloaded(self):
"""Retrieves all rows where downloaded status is False (0)."""
self.CURSOR.execute(f"SELECT {self.columns} FROM {self.table} WHERE downloaded = 0")
return self.CURSOR.fetchall()
def insert_vods_record(self, record_id: int, record_date_str: datetime_date, title: str, gamename: str):
"""Inserts a record with ID, date, gamename, and title into a SQLite database."""
try:
clean_date = datetime.strptime(record_date_str, "%Y-%m-%dT%H:%M:%SZ").date()
except ValueError:
clean_date = record_date_str
# Connects to database file (creates it if missing)
# Inserts data using parameterized queries to prevent SQL injection
self.CURSOR.execute(
f"INSERT OR IGNORE INTO {self.table} ({self.columns}) VALUES (?, ?, ?, ?, ?, ?, ?)",
(record_id, str(clean_date), title, gamename, False, False, False),
)
# Saves changes and closes the connection
self.CONN.commit()
def insert_clips_record(slug: str, record_date_str: str, title: str, gamename: str, clip_by: str, views: int):
"""Cleans up ISO-8601 strings into unified date structures for the database."""
try:
clean_date = datetime.strptime(record_date_str, "%Y-%m-%dT%H:%M:%SZ").date()
except ValueError:
clean_date = record_date_str
self.CURSOR.execute(
f"INSERT OR IGNORE INTO {self.table} ({self.columns}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(slug, str(clean_date), title, gamename, clip_by, views, False, False),
)
self.CONN.commit()
+6 -39
View File
@@ -9,33 +9,6 @@ from uploader import CategoryId
CHANNEL_NAME = 'teampgp' # Replace with the streamer's username CHANNEL_NAME = 'teampgp' # Replace with the streamer's username
# Stores data locally
CONN = sqlite3.connect("clips_database.db")
CURSOR = CONN.cursor()
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
)
"""
)
CONN.commit()
def close_database():
"""Commits queries before ending connection context."""
CONN.commit()
CONN.close()
def download_clips(): def download_clips():
"""Loops over undownloaded metadata entries to write files down locally.""" """Loops over undownloaded metadata entries to write files down locally."""
undownloaded_clips = get_undownloaded_clips() undownloaded_clips = get_undownloaded_clips()
@@ -70,14 +43,15 @@ def upload_clips():
file_path = f"save/clips/{slug}/{slug}.mp4" file_path = f"save/clips/{slug}/{slug}.mp4"
title = title title = title
description = f"Game: {game_name}, Cliped By: {clip_by}, on {record_date}" description = f"Game: {game_name}, Cliped By: {clip_by}, on {record_date}, #Shorts"
categoryId = CategoryId.SHORTS categoryId = CategoryId.GAMING
privatcyStatus = 'private' 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.") print(f"Slug: {slug} | Was successfully uploaded.")
mark_as_uploaded(slug) mark_as_uploaded(slug)
else: else:
print(f"Slug: {slug} | Upload Process failed.") print(f"Slug: {slug} | Upload Process failed.")
@@ -108,13 +82,6 @@ def mark_as_downloaded(slug: str):
) )
CONN.commit() CONN.commit()
def get_unuploaded_clips():
"""Retrieves all clip rows remaining to be uploaded."""
CURSOR.execute(
"SELECT slug, date, title, gamename, clip_by, view_count, downloaded, uploaded_yt FROM clips WHERE downloaded = 1 AND uploaded_yt = 0"
)
return CURSOR.fetchall()
def get_undownloaded_clips(): def get_undownloaded_clips():
"""Retrieves all clip rows remaining to be captured.""" """Retrieves all clip rows remaining to be captured."""
CURSOR.execute( CURSOR.execute(
+48 -76
View File
@@ -2,54 +2,15 @@
import requests import requests
import sqlite3 import sqlite3
from datetime import date as datetime_date
import subprocess import subprocess
from database import Database
import uploader
from uploader import CategoryId
CHANNEL_NAME = 'teampgp' # Replace with the streamer's username CHANNEL_NAME = 'teampgp' # Replace with the streamer's username
DB = Database("vods")
CONN = sqlite3.connect("database.db")
CURSOR = CONN.cursor()
def create_database():
# Creates table safely using multi-line string
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
)
"""
)
CONN.commit()
def close_database():
"""Commit before we close."""
CONN.commit()
CONN.close()
def download_vods():
"""Find all undownload vods and download them."""
undownload_vods = get_undownloaded_vods()
print("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")
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)
else:
print(f"ID: {record_id} | Date: {record_date} | Game: {game_name} | Title: {title} | was Failed")
print("Finished Downloading VODs...")
def run_linux_command(command: str): def run_linux_command(command: str):
"""Executes a Linux command, waits for completion, and returns output.""" """Executes a Linux command, waits for completion, and returns output."""
@@ -66,40 +27,50 @@ def run_linux_command(command: str):
# Handles errors if the Linux command returns a non-zero exit code # Handles errors if the Linux command returns a non-zero exit code
return {"success": False, "stdout": e.stdout, "stderr": e.stderr} return {"success": False, "stdout": e.stdout, "stderr": e.stderr}
def mark_as_downloaded(record_id: int): def download_vods():
"""Updates the downloaded status to True (1) for a specific record ID.""" """Find all undownload vods and download them."""
undownloaded_vods = DB.get_undownloaded()
# Updates the row matching the specific ID
CURSOR.execute(
"UPDATE vods SET downloaded = 1 WHERE id = ?",
(record_id,)
)
CONN.commit() print("Download VODs...")
def get_undownloaded_vods(): for record_id, record_date, title, game_name, downloaded, uploaded_yt, chat_upload_yt in undownloaded_vods:
"""Retrieves all rows where downloaded status is False (0).""" 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/vods/{record_id}/{title}.mp4")
# Query filters by 0 because SQLite stores booleans as integers output2 = run_linux_command(f"TwitchDownloaderCLI chatdownload --id {record_id} -o save/vods/{record_id}/{title}_chat.json -E")
CURSOR.execute( if output["success"] is True:
"SELECT id, date, title, gamename, downloaded FROM vods WHERE downloaded = 0" print(f"ID: {record_id} | Date: {record_date} | Game: {game_name} | Title: {title} | was successful")
) DB.mark_as_downloaded(record_id)
records = CURSOR.fetchall() else:
print(f"ID: {record_id} | Date: {record_date} | Game: {game_name} | Title: {title} | was Failed")
return records print("Finished Downloading VODs...")
def insert_record(record_id: int, record_date: datetime_date, title: str, gamename: str): def upload_vods():
"""Inserts a record with ID, date, gamename, and title into a SQLite database.""" """Loops over the downloaded videos entries and uploaded them to youtube."""
# Connects to database file (creates it if missing) print("Uploading Clips...")
# Inserts data using parameterized queries to prevent SQL injection unuploaded_vods = DB.get_unuploaded()
CURSOR.execute(
"INSERT OR IGNORE INTO vods (id, date, title, gamename, downloaded, uploaded_yt) VALUES (?, ?, ?, ?, ?, ?)",
(record_id, str(record_date), title, gamename, False, False),
)
# Saves changes and closes the connection for record_id, record_date, title, game_name, downloaded, uploaded_yt, chat_upload_yt in unuploaded_vods:
CONN.commit() print(f"ID: {record_id} | Date: {record_date} | Game: {game_name} | Title: {title}")
file_path = f"save/vods/{record_id}/{title}.mp4"
title = title
description = f"Game: {game_name}, on {record_date}, #Shorts"
categoryId = CategoryId.GAMING
privatcyStatus = 'private'
tags = ['gaming', 'TeamPGP', f'{game_name}']
output = uploader.upload_video(file_path, title, description, categoryId, privatcyStatus, tags)
if output is True:
print(f"ID: {record_id} | Was successfully uploaded.")
DB.mark_as_uploaded(record_id)
else:
print(f"ID: {record_id} | Upload Process failed.")
def create_chat_vods():
pass
def get_vod_ids_simplified(channel_name: str): def get_vod_ids_simplified(channel_name: str):
session = requests.Session() session = requests.Session()
@@ -177,7 +148,8 @@ def get_vod_ids_simplified(channel_name: str):
print(f"ID: {node['id']} | Date: {node['publishedAt']} | Game: {game_name} | Title: {node['title']}") print(f"ID: {node['id']} | Date: {node['publishedAt']} | Game: {game_name} | Title: {node['title']}")
# Pass game_name to your database logic # Pass game_name to your database logic
insert_record(node['id'], node['publishedAt'], node['title'], game_name) DB.insert_vods_record(node['id'], node['publishedAt'], node['title'], game_name)
#insert_record(node['id'], node['publishedAt'], node['title'], game_name)
vod_ids.append(node['id']) vod_ids.append(node['id'])
return vod_ids return vod_ids
@@ -189,7 +161,7 @@ def get_vod_ids_simplified(channel_name: str):
return [] return []
if __name__ == "__main__": if __name__ == "__main__":
create_database()
get_vod_ids_simplified(CHANNEL_NAME) get_vod_ids_simplified(CHANNEL_NAME)
download_vods() download_vods()
close_database() upload_vods()
#close_database()
+55 -44
View File
@@ -2,7 +2,8 @@
import os import os
import json import json
import argparse import argparse
from enum import Enum, auto, unique from enum import Enum
from datetime import datetime
from google.oauth2.credentials import Credentials from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request from google.auth.transport.requests import Request
@@ -10,40 +11,24 @@ from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload from googleapiclient.http import MediaFileUpload
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
@unique
class CategoryId(Enum): class CategoryId(Enum):
FILM_ANIMATION = 1 """Official YouTube Category IDs for API Uploads."""
AUTOS_VEHICLES = 2
MUSIC = 10 FILM_AND_ANIMATION = "1"
PETS_ANIMALS = 15 AUTOS_AND_VEHICLES = "2"
SPORTS = 17 MUSIC = "10"
SHORT_MOVIES = 18 PETS_AND_ANIMALS = "15"
TRAVEL_EVENTS = 19 SPORTS = "17"
GAMING = 20 TRAVEL_AND_EVENTS = "19"
VIDEOBLOGGING = 21 GAMING = "20"
PEOPLE_BLOGS = 22 PEOPLE_AND_BLOGS = "22"
COMEDY = 23 COMEDY = "23"
ENTERTAINMENT = 24 ENTERTAINMENT = "24"
NEWS_POLITICS = 25 NEWS_AND_POLITICS = "25"
HOWTO_STYLE = 26 HOWTO_AND_STYLE = "26"
EDUCATION = 27 EDUCATION = "27"
SCIENCE_TECHNOLOGY = 28 SCIENCE_AND_TECHNOLOGY = "28"
NONPROFITS_ACTIVISM = 29 NONPROFITS_AND_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
def load_credentials(): def load_credentials():
"""Load credentials from secrets.json""" """Load credentials from secrets.json"""
@@ -72,18 +57,24 @@ def load_credentials():
print(f"Error loading credentials: {str(e)}") print(f"Error loading credentials: {str(e)}")
return None 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 Upload a video to YouTube
Args: Args:
file_path (str): Path to the video file file_path (str): Path to the video file
title (str): Title of the Video title (str): Title of the Video
description (str): Video description
categoryId (str): categoryId of the video categoryId (str): categoryId of the video
description (str): Video description
privacyStatus (str): privacyStatus of the video privacyStatus (str): privacyStatus of the video
tags (str): tags to be use on 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: try:
# Check if file exists # Check if file exists
if not os.path.exists(file_path): 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: if not credentials:
return False 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 # Create YouTube API client
youtube = build('youtube', 'v3', credentials=credentials) youtube = build('youtube', 'v3', credentials=credentials)
# Get the filename without extension as default title # Configure the status object dynamically
title = os.path.splitext(os.path.basename(file_path))[0] 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 # Prepare the video upload request
body = { body = {
@@ -107,12 +114,9 @@ def upload_video(file_path: str, title: str, description: str, category: Categor
'title': title, 'title': title,
'description': description, 'description': description,
'tags': tags, 'tags': tags,
'categoryId': str(category.value) # Default to 'People & Blogs' category 'categoryId': category.value
}, },
'status': { 'status': status_body
'privacyStatus': privacyStatus, # Default to private
'selfDeclaredMadeForKids': False
}
} }
@@ -130,7 +134,7 @@ def upload_video(file_path: str, title: str, description: str, category: Categor
media_body=media media_body=media
) )
print("Starting upload...") print(f"Starting upload for '{title}'...")
response = None response = None
while response is None: while response is None:
status, response = insert_request.next_chunk() 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"Video ID: {response['id']}")
print(f"Title: {response['snippet']['title']}") print(f"Title: {response['snippet']['title']}")
print(f"URL: https://youtu.be/{response['id']}") 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 return True
except HttpError as e: except HttpError as e: