exparmental run_command in linux.py migrate_sql1.py
This commit is contained in:
+2
-3
@@ -5,11 +5,10 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
def __init__(self):
|
def __init__(self, db_path: str = "database.db"):
|
||||||
self.columns = "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"
|
self.columns = "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"
|
||||||
|
|
||||||
# IMPORTANT Must check if database.db exists before connecting to it.
|
file_exists = self.db_path.is_file()
|
||||||
file_exists = Path("database.db").is_file()
|
|
||||||
|
|
||||||
self.conn = sqlite3.connect("database.db")
|
self.conn = sqlite3.connect("database.db")
|
||||||
self.cursor = self.conn.cursor()
|
self.cursor = self.conn.cursor()
|
||||||
|
|||||||
@@ -1,7 +1,83 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
def run_command(command: str):
|
def run_command_ffmpeg(cmd_str: str, progress_prefix: str = "Progress") -> bool:
|
||||||
|
"""Runs a system command and streams its output live to the console."""
|
||||||
|
# shlex safely handles quotes and paths inside the command string
|
||||||
|
args = shlex.split(cmd_str)
|
||||||
|
|
||||||
|
# Redirect stderr to stdout because FFmpeg outputs status updates to stderr
|
||||||
|
process = subprocess.Popen(
|
||||||
|
args,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Executing: {cmd_str[:90]}...")
|
||||||
|
|
||||||
|
# Stream the output live to the terminal
|
||||||
|
while True:
|
||||||
|
line = process.stdout.readline()
|
||||||
|
if not line and process.poll() is not None:
|
||||||
|
break
|
||||||
|
if line:
|
||||||
|
clean_line = line.strip()
|
||||||
|
# Only print updates that show progress metrics to keep terminal clean
|
||||||
|
if any(metric in clean_line for metric in ["frame=", "time=", "fps=", "Rendering frame"]):
|
||||||
|
sys.stdout.write(f"\r[{progress_prefix}] {clean_line}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
elif "Error" in clean_line or "failed" in clean_line:
|
||||||
|
print(f"\n[Alert] {clean_line}")
|
||||||
|
|
||||||
|
print("\n") # New line after process finishes
|
||||||
|
return process.returncode == 0
|
||||||
|
|
||||||
|
def run_command(command: list[str]) -> dict:
|
||||||
|
"""Executes a command, streams stdout only when the line changes, and detects OOM."""
|
||||||
|
try:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
command,
|
||||||
|
shell=False,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
last_line = None # Track the previous line
|
||||||
|
|
||||||
|
if process.stdout:
|
||||||
|
for line in process.stdout:
|
||||||
|
if line != last_line: # Only print if the content changed
|
||||||
|
print(line, end="")
|
||||||
|
sys.stdout.flush()
|
||||||
|
last_line = line # Update the tracking variable
|
||||||
|
|
||||||
|
_, stderr = process.communicate()
|
||||||
|
|
||||||
|
if process.returncode != 0:
|
||||||
|
if process.returncode in [137, -9]:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Process was KILLED by the Linux kernel (Out of Memory).",
|
||||||
|
"stderr": stderr,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Exit code {process.returncode}",
|
||||||
|
"stderr": stderr,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"success": True, "error": "", "stderr": stderr}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": str(e), "stderr": ""}
|
||||||
|
|
||||||
|
def run_command_old(command: str):
|
||||||
"""Executes a Linux command, waits for completion, and returns output."""
|
"""Executes a Linux command, waits for completion, and returns output."""
|
||||||
try:
|
try:
|
||||||
# shell=True allows running full command strings with pipes/wildcards
|
# shell=True allows running full command strings with pipes/wildcards
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Connect to your database file
|
||||||
|
db_name = "database.db" # Change to your actual file name
|
||||||
|
conn = sqlite3.connect(db_name)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Create a temporary table with the new text-based game_id
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS twitch_videos_temp (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
view_count INTEGER NOT NULL,
|
||||||
|
duration TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
thumbnail_url TEXT NOT NULL,
|
||||||
|
game_id TEXT NOT NULL,
|
||||||
|
game_name TEXT NOT NULL,
|
||||||
|
stream_id TEXT NOT NULL,
|
||||||
|
creator_name TEXT NOT NULL,
|
||||||
|
clip_is BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
downloaded BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
uploaded_yt BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
uploaded_yt_chats BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
uploaded_yt_shorts BOOLEAN NOT NULL DEFAULT 0
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Copy and convert data to the temporary table
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO twitch_videos_temp (
|
||||||
|
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
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id, title, created_at, view_count, duration, url, thumbnail_url,
|
||||||
|
CAST(game_id AS TEXT), game_name, stream_id, creator_name, clip_is, downloaded,
|
||||||
|
uploaded_yt, uploaded_yt_chats, uploaded_yt_shorts
|
||||||
|
FROM twitch_videos
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Drop the old table configuration
|
||||||
|
cursor.execute("DROP TABLE twitch_videos")
|
||||||
|
|
||||||
|
# 4. Rename the temporary table to your exact original table name
|
||||||
|
cursor.execute("ALTER TABLE twitch_videos_temp RENAME TO twitch_videos")
|
||||||
|
|
||||||
|
# Commit changes if everything succeeded
|
||||||
|
conn.commit()
|
||||||
|
print("Migration successful! 'twitch_videos' table updated.")
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
# Roll back changes if an error occurs
|
||||||
|
conn.rollback()
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Close database connection
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import linux
|
||||||
|
|
||||||
|
def combine_twitch_vod_and_chat(vod_path: str, layout: str = "side-by-side") -> bool:
|
||||||
|
"""
|
||||||
|
Renders Twitch chat JSON to video and combines it with the source VOD.
|
||||||
|
Provides real-time terminal feedback for all processing steps.
|
||||||
|
"""
|
||||||
|
video_path = vod_path
|
||||||
|
chat_path = video_path.replace(".mp4", "_chat.json")
|
||||||
|
temp_chat = video_path.replace(".mp4", "_temp_chat.mp4")
|
||||||
|
video_chat = video_path.replace(".mp4", "_with_chat.mp4")
|
||||||
|
mask_path = video_chat.replace(".mp4", "_mask.mp4")
|
||||||
|
|
||||||
|
# Step 1: Pre-flight checks
|
||||||
|
if not os.path.exists(video_path):
|
||||||
|
print(f"❌ Error: Source video not found: {video_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not os.path.exists(chat_path):
|
||||||
|
print(f"❌ Error: Chat file not found: {chat_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Step 2: Render Chat to Video
|
||||||
|
print("====================================================")
|
||||||
|
print("🚀 STEP 1: Rendering Chat JSON to Video Layer")
|
||||||
|
print("====================================================")
|
||||||
|
|
||||||
|
chat_cmd = (
|
||||||
|
f"TwitchDownloaderCLI chatrender "
|
||||||
|
f"-i {chat_path} "
|
||||||
|
f"-w 400 -h 1080 "
|
||||||
|
f"--collision Overwrite "
|
||||||
|
f"--temp-path download/temp "
|
||||||
|
f"--font-size 25 "
|
||||||
|
f"--generate-mask "
|
||||||
|
f"-o {temp_chat}"
|
||||||
|
f'--output-args="-threads 8 "'
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_success = linux.run_command(chat_cmd, progress_prefix="Chat Render")
|
||||||
|
if not chat_success:
|
||||||
|
print("❌ Error: TwitchDownloaderCLI failed to render chat video.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Step 3: Combine Video and Chat using FFmpeg
|
||||||
|
print("====================================================")
|
||||||
|
print(f"🚀 STEP 2: Merging VOD and Chat Layout ({layout})")
|
||||||
|
print("====================================================")
|
||||||
|
|
||||||
|
# -preset superfast speeds up the 3+ hour encoding process significantly
|
||||||
|
# -map 0:a? safely includes audio if it exists, without breaking on silent VODs
|
||||||
|
if layout == "side-by-side":
|
||||||
|
ffmpeg_cmd = (
|
||||||
|
f"ffmpeg -y -i {video_path} -i {temp_chat} "
|
||||||
|
f"-filter_complex '[0:v][1:v]hstack=inputs=2[v]' "
|
||||||
|
f"-map '[v]' -map 0:a? -c:a copy -preset superfast {video_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];"
|
||||||
|
f"[0:v][masked_chat]overlay=x=10:y=10[v]' "
|
||||||
|
f"-map '[v]' -map 0:a? -c:a copy -preset superfast {video_chat}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid layout choice. Choose 'side-by-side' or 'overlay'.")
|
||||||
|
|
||||||
|
ffmpeg_success = linux.run_command_ffmpeg(ffmpeg_cmd, progress_prefix="FFmpeg Merge")
|
||||||
|
|
||||||
|
# Step 4: Final verification and cleanup
|
||||||
|
if ffmpeg_success and os.path.exists(video_chat):
|
||||||
|
print("====================================================")
|
||||||
|
print(f"🎉 SUCCESS: Video processing complete!")
|
||||||
|
print(f"📁 Output Saved: {video_chat}")
|
||||||
|
print("====================================================")
|
||||||
|
|
||||||
|
# Clean up the massive temporary chat video to save storage space
|
||||||
|
if os.path.exists(temp_chat):
|
||||||
|
print("🧹 Cleaning up temporary chat render video...")
|
||||||
|
#os.remove(temp_chat)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("❌ Error: FFmpeg failed to merge the video streams.")
|
||||||
|
return False
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import requests
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import csv
|
import csv
|
||||||
|
import subprocess
|
||||||
|
|
||||||
import linux
|
import linux
|
||||||
from database import Database
|
from database import Database
|
||||||
@@ -19,6 +17,70 @@ def write_csv(data, file_name):
|
|||||||
writer.writerows(data)
|
writer.writerows(data)
|
||||||
|
|
||||||
def download():
|
def download():
|
||||||
|
"""Find all undownloaded videos and clips, and download them safely."""
|
||||||
|
undownloaded = DB.get_undownloaded()
|
||||||
|
|
||||||
|
print("Starting downloads...")
|
||||||
|
for row in undownloaded:
|
||||||
|
# Unpack variables clearly
|
||||||
|
(
|
||||||
|
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,
|
||||||
|
) = row
|
||||||
|
|
||||||
|
data = [list(row)]
|
||||||
|
print(
|
||||||
|
f"ID: {id} | Date: {created_at} | Game: {game_name} | Title: {title}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1. Define paths and isolate base directory
|
||||||
|
if clip_is:
|
||||||
|
target_dir = f"download/clips/{id}"
|
||||||
|
cmd = f"TwitchDownloaderCLI clipdownload --id {id} -o {target_dir}/{id}.mp4 --collision Overwrite --temp-path download/temp"
|
||||||
|
else:
|
||||||
|
target_dir = f"download/videos/{id}"
|
||||||
|
# FIXED: Removed the duplicated command string combined with '&&'
|
||||||
|
cmd = f"TwitchDownloaderCLI videodownload --id {id} -o {target_dir}/{id}.mp4 --collision Overwrite --threads 2 --temp-path download/temp"
|
||||||
|
|
||||||
|
csv_file = f"{target_dir}/{id}.csv"
|
||||||
|
|
||||||
|
# 3. FIXED: Use the live streaming function to prevent Out-Of-Memory crashes
|
||||||
|
output = linux.run_command_streaming(cmd)
|
||||||
|
output2 = linux.run_command(f"TwitchDownloaderCLI chatdownload --id {id} -o {target_dir}/{id}_chat.json -E --collision Overwrite --temp-path download/temp")
|
||||||
|
|
||||||
|
if output["success"]:
|
||||||
|
# Only write CSV and update database if download actually completed
|
||||||
|
write_csv(data, csv_file)
|
||||||
|
print(f"ID: {id} | Download was successful.")
|
||||||
|
DB.mark_as_downloaded(id)
|
||||||
|
else:
|
||||||
|
print(f"ID: {id} | Download Process failed.")
|
||||||
|
print(f"Reason/Error: {output.get('error', 'Unknown Error')}")
|
||||||
|
if output.get("stderr"):
|
||||||
|
print(f"Details: {output['stderr']}")
|
||||||
|
|
||||||
|
# Clear temporary chunk clutter immediately if a VOD crashes out
|
||||||
|
if not clip_is:
|
||||||
|
print("Flushing temporary crash chunks...")
|
||||||
|
subprocess.run("rm -rf download/temp/*", shell=True)
|
||||||
|
|
||||||
|
print("Finished Downloading Pipeline.")
|
||||||
|
|
||||||
|
def download_old():
|
||||||
"""Find all undownload videos and download them."""
|
"""Find all undownload videos and download them."""
|
||||||
undownloaded = DB.get_undownloaded()
|
undownloaded = DB.get_undownloaded()
|
||||||
|
|
||||||
@@ -27,15 +89,13 @@ def download():
|
|||||||
data = [[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 = [[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]]
|
||||||
print(f"ID: {id} | Date: {created_at} | Game: {game_name} | Title: {title}")
|
print(f"ID: {id} | Date: {created_at} | Game: {game_name} | Title: {title}")
|
||||||
if not clip_is:
|
if not clip_is:
|
||||||
output = linux.run_command(f"TwitchDownloaderCLI videodownload --id {id} -o download/videos/{id}/{id}.mp4 --collision Overwrite")
|
output = linux.run_command(f"TwitchDownloaderCLI videodownload --id {id} -o download/videos/{id}/{id}.mp4 --collision Overwrite --temp-path download/temp")
|
||||||
output2 = linux.run_command(f"TwitchDownloaderCLI chatdownload --id {id} -o download/videos/{id}/{id}_chat.json -E --collision Overwrite")
|
output2 = linux.run_command(f"TwitchDownloaderCLI chatdownload --id {id} -o download/videos/{id}/{id}_chat.json -E --collision Overwrite --temp-path download/temp")
|
||||||
write_csv(data, f"download/videos/{id}/{id}.csv")
|
write_csv(data, f"download/videos/{id}/{id}.csv")
|
||||||
elif clip_is:
|
elif clip_is:
|
||||||
output = linux.run_command(f"TwitchDownloaderCLI clipdownload --id {id} -o download/clips/{id}/{id}.mp4 --collision Overwrite")
|
output = linux.run_command(f"TwitchDownloaderCLI clipdownload --id {id} -o download/clips/{id}/{id}.mp4 --collision Overwrite")
|
||||||
write_csv(data, f"download/clips/{id}/{id}.csv")
|
write_csv(data, f"download/clips/{id}/{id}.csv")
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
if output['success'] is True:
|
if output['success'] is True:
|
||||||
print(f"ID: {id} | Date: {created_at} | Game: {game_name} | Title: {title} | was successful")
|
print(f"ID: {id} | Date: {created_at} | Game: {game_name} | Title: {title} | was successful")
|
||||||
DB.mark_as_downloaded(id)
|
DB.mark_as_downloaded(id)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ async def get_streamer_vods():
|
|||||||
|
|
||||||
# Safely convert game_id to integer if possible, otherwise default to 0
|
# Safely convert game_id to integer if possible, otherwise default to 0
|
||||||
try:
|
try:
|
||||||
clean_game_id = int(game_id)
|
clean_game_id = game_id
|
||||||
except ValueError:
|
except ValueError:
|
||||||
clean_game_id = 0
|
clean_game_id = 0
|
||||||
|
|
||||||
@@ -132,11 +132,6 @@ async def get_streamer_clips():
|
|||||||
|
|
||||||
async for c in clip_generator:
|
async for c in clip_generator:
|
||||||
game_name = await get_game_name_by_id(c.game_id)
|
game_name = await get_game_name_by_id(c.game_id)
|
||||||
|
|
||||||
try:
|
|
||||||
clean_game_id = int(c.game_id)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
clean_game_id = 0
|
|
||||||
|
|
||||||
clip_data = {
|
clip_data = {
|
||||||
"id": c.id,
|
"id": c.id,
|
||||||
@@ -146,7 +141,7 @@ async def get_streamer_clips():
|
|||||||
"duration": c.duration,
|
"duration": c.duration,
|
||||||
"url": c.url,
|
"url": c.url,
|
||||||
"thumbnail_url": c.thumbnail_url,
|
"thumbnail_url": c.thumbnail_url,
|
||||||
"game_id": clean_game_id,
|
"game_id": c.game_id,
|
||||||
"game_name": game_name,
|
"game_name": game_name,
|
||||||
"stream_id": "0",
|
"stream_id": "0",
|
||||||
"creator_name": c.creator_name,
|
"creator_name": c.creator_name,
|
||||||
|
|||||||
Reference in New Issue
Block a user