I forgot what I chanagerd
This commit is contained in:
1 parent
24081e1576
commit
c4f5018833
20 files changed
+678
-77
No files matched your search
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Finds the absolute path to 'project_directory' and adds it to Python's search path
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))
|
||||
sys.path.append(PROJECT_ROOT)
|
||||
|
||||
from package.config import DB as DB
|
||||
from package.database import Chapter
|
||||
|
||||
def get_chapters(json_path):
|
||||
"""Loads the JSON file and returns all chapters."""
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# The chapters are under video -> chapters
|
||||
video = data.get('video', {})
|
||||
chapters = video.get('chapters', [])
|
||||
|
||||
if not chapters:
|
||||
print("No chapters found.")
|
||||
return []
|
||||
|
||||
print(f"Found {len(chapters)} chapter(s) in video '{video.get('title')}':\n")
|
||||
|
||||
for i, chapter in enumerate(chapters, 1):
|
||||
start_sec = chapter.get('startMilliseconds', 0) / 1000
|
||||
length_sec = chapter.get('lengthMilliseconds', 0) / 1000
|
||||
|
||||
print(f"{i}. {chapter.get('description', 'No Title')}")
|
||||
print(f" - Type: {chapter.get('type')}")
|
||||
print(f" - Start: {start_sec}s ({chapter.get('startMilliseconds')}ms)")
|
||||
print(f" - Length: {length_sec}s ({chapter.get('lengthMilliseconds')}ms)")
|
||||
print(f" - Game: {chapter.get('gameDisplayName')} (ID: {chapter.get('gameId')})")
|
||||
print()
|
||||
|
||||
return chapters
|
||||
|
||||
def main():
|
||||
downloads = DB.get_downloaded()
|
||||
|
||||
for download in downloads:
|
||||
json_path = f"download/clips/{download.id}/{download.id}_chat.json" if download.clip_is else f"download/videos/{download.id}/{download.id}_chat.json"
|
||||
if os.path.exists(json_path):
|
||||
chapters = get_chapters(json_path)
|
||||
for chapter in chapters:
|
||||
chapter_record = Chapter(
|
||||
twitch_video_id=download.id,
|
||||
start_milliseconds=chapter.get('startMilliseconds'),
|
||||
length_milliseconds=chapter.get('lengthMilliseconds'),
|
||||
description=chapter.get('description'),
|
||||
type=chapter.get('type'),
|
||||
game_display_name=chapter.get('gameDisplayName'),
|
||||
game_id=chapter.get('gameId')
|
||||
)
|
||||
DB.insert(chapter_record)
|
||||
else:
|
||||
print(f"JSON file not found for video {download.id}: {json_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
# Usage: python get_chapters.py your_file.json
|
||||
#path = sys.argv[1] if len(sys.argv) > 1 else "/home/richard/python_scripts/download/videos/2873107835/2873107835_chat.json"
|
||||
#try:
|
||||
# get_chapters(path)
|
||||
#except FileNotFoundError:
|
||||
# print(f"Error: File not found -> {path}")
|
||||
#except json.JSONDecodeError:
|
||||
# print(f"Error: File is not valid JSON -> {path}")
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Combine multiple video files into one file using FFmpeg.
|
||||
|
||||
Example:
|
||||
python helper_files/combine_videos.py output.mp4 video1.mp4 video2.mp4 video3.mp4
|
||||
python helper_files/combine_videos.py --output merged.mp4 --inputs video1.mp4 video2.mp4
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def build_concat_list(input_files, list_path):
|
||||
with open(list_path, "w", encoding="utf-8") as f:
|
||||
for path in input_files:
|
||||
f.write(f"file '{os.path.abspath(path)}'\n")
|
||||
|
||||
|
||||
def run_ffmpeg(cmd):
|
||||
print("Running:", " ".join(cmd))
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(result.stdout)
|
||||
print(result.stderr)
|
||||
raise RuntimeError(f"FFmpeg failed with exit code {result.returncode}")
|
||||
print("✅ Finished successfully.")
|
||||
|
||||
|
||||
def concat_videos_copy(input_files, output_path):
|
||||
list_path = Path(output_path).with_suffix(".concat.txt")
|
||||
build_concat_list(input_files, list_path)
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-f", "concat",
|
||||
"-safe", "0",
|
||||
"-i", str(list_path),
|
||||
"-c", "copy",
|
||||
str(output_path),
|
||||
]
|
||||
run_ffmpeg(cmd)
|
||||
list_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def concat_videos_reencode(input_files, output_path):
|
||||
inputs = []
|
||||
filter_parts = []
|
||||
audio_parts = []
|
||||
|
||||
for i, video_path in enumerate(input_files):
|
||||
inputs.extend(["-i", str(video_path)])
|
||||
filter_parts.append(f"[{i}:v]scale=trunc(iw/2)*2:trunc(ih/2)*2,setpts=PTS-STARTPTS[v{i}]")
|
||||
audio_parts.append(f"[{i}:a]aresample=async=1[a{i}]")
|
||||
|
||||
concat_filter = ",".join(filter_parts + audio_parts)
|
||||
map_parts = []
|
||||
for i in range(len(input_files)):
|
||||
map_parts.append(f"[v{i}]")
|
||||
map_parts.append(f"[a{i}]")
|
||||
|
||||
# Join the video and audio streams in order and concatenate them.
|
||||
# This works well when the clips are similar enough in format.
|
||||
filter_complex = f"{','.join(filter_parts)};{';'.join(audio_parts)};"
|
||||
filter_complex += "".join(f"[{i}:v][{i}:a]" for i in range(len(input_files)))
|
||||
filter_complex += f"concat=n={len(input_files)}:v=1:a=1[outv][outa]"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
*inputs,
|
||||
"-filter_complex",
|
||||
filter_complex,
|
||||
"-map",
|
||||
"[outv]",
|
||||
"-map",
|
||||
"[outa]",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
str(output_path),
|
||||
]
|
||||
run_ffmpeg(cmd)
|
||||
|
||||
|
||||
def merge_videos(input_files, output_path):
|
||||
input_files = [str(Path(p).expanduser().resolve()) for p in input_files]
|
||||
output_path = str(Path(output_path).expanduser().resolve())
|
||||
|
||||
if len(input_files) < 2:
|
||||
raise ValueError("You need at least 2 video files to merge.")
|
||||
|
||||
for path in input_files:
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"File not found: {path}")
|
||||
|
||||
output_dir = Path(output_path).parent
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
concat_videos_copy(input_files, output_path)
|
||||
except Exception as exc:
|
||||
print(f"Fast copy method failed: {exc}")
|
||||
print("Retrying with re-encode fallback...")
|
||||
concat_videos_reencode(input_files, output_path)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Join two or more video files together with FFmpeg.")
|
||||
parser.add_argument("output_file", help="Path to the merged output video")
|
||||
parser.add_argument("input_files", nargs="+", help="Two or more videos to concatenate")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
merge_videos(args.input_files, args.output_file)
|
||||
print(f"Merged video saved to: {args.output_file}")
|
||||
except Exception as exc:
|
||||
print(f"Error: {exc}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -55,7 +55,7 @@ def fix_time():
|
||||
|
||||
def main():
|
||||
global DB
|
||||
DB = database.Database()
|
||||
DB = database.Repository()
|
||||
|
||||
fix_time()
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ def fix_download():
|
||||
|
||||
def main():
|
||||
global DB
|
||||
DB = database.Database()
|
||||
DB = database.Repository()
|
||||
|
||||
fix_download()
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ def read_json(file_path: str):
|
||||
|
||||
def main():
|
||||
global DB
|
||||
DB = database.Database()
|
||||
DB = database.Repository()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import sqlite3
|
||||
DB_PATH = "database.db"
|
||||
|
||||
def migrate_to_many(db_path):
|
||||
con = sqlite3.connect(db_path)
|
||||
cur = con.cursor()
|
||||
cur.execute("PRAGMA foreign_keys=OFF;")
|
||||
try:
|
||||
con.execute("BEGIN;")
|
||||
cur.execute("ALTER TABLE youtube_info RENAME TO youtube_info_old;")
|
||||
|
||||
cur.execute("""
|
||||
CREATE TABLE youtube_info (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
twitch_video_id VARCHAR NOT NULL,
|
||||
yt_id VARCHAR,
|
||||
yt_date VARCHAR,
|
||||
yt_short_id VARCHAR,
|
||||
yt_short_date VARCHAR,
|
||||
yt_chat_SS_id VARCHAR,
|
||||
yt_chat_SS_date VARCHAR,
|
||||
yt_chat_over_id VARCHAR,
|
||||
yt_chat_over_date VARCHAR,
|
||||
FOREIGN KEY(twitch_video_id) REFERENCES twitch_videos(id) ON DELETE CASCADE
|
||||
);
|
||||
""")
|
||||
cur.execute("CREATE INDEX ix_youtube_info_twitch_video_id ON youtube_info (twitch_video_id);")
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO youtube_info (twitch_video_id, yt_id, yt_date, yt_short_id, yt_short_date, yt_chat_SS_id, yt_chat_SS_date, yt_chat_over_id, yt_chat_over_date)
|
||||
SELECT id, yt_id, yt_date, yt_short_id, yt_short_date, yt_chat_SS_id, yt_chat_SS_date, yt_chat_over_id, yt_chat_over_date
|
||||
FROM youtube_info_old;
|
||||
""")
|
||||
|
||||
cur.execute("DROP TABLE youtube_info_old;")
|
||||
con.commit()
|
||||
print("Done - now 1-to-Many")
|
||||
except Exception as e:
|
||||
con.rollback()
|
||||
raise e
|
||||
finally:
|
||||
cur.execute("PRAGMA foreign_keys=ON;")
|
||||
con.close()
|
||||
|
||||
migrate_to_many(DB_PATH)
|
||||
@@ -49,7 +49,7 @@ def remove_vods():
|
||||
|
||||
def main():
|
||||
global DB
|
||||
DB = database.Database()
|
||||
DB = database.Repository()
|
||||
|
||||
remove_vods()
|
||||
|
||||
|
||||
@@ -31,5 +31,5 @@ def redownload_clips():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
DB = database.Database()
|
||||
DB = database.Repository()
|
||||
redownload_clips()
|
||||
@@ -0,0 +1,225 @@
|
||||
"""
|
||||
YouTube Description Updater
|
||||
Requires: pip install google-api-python-client google-auth-oauthlib google-auth
|
||||
|
||||
1. Enable YouTube Data API v3 in Google Cloud Console
|
||||
2. Create OAuth 2.0 credentials (Desktop App)
|
||||
3. Download as client_secret.json
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from google.auth.transport.requests import Request
|
||||
from google.oauth2.credentials import Credentials
|
||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
# Finds the absolute path to 'project_directory' and adds it to Python's search path
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))
|
||||
sys.path.append(PROJECT_ROOT)
|
||||
|
||||
from package.config import DB as DB
|
||||
from package.youtube.config import CREDENTIALS as CREDENTIALS
|
||||
from package.database import YoutubeInfo as YoutubeInfo
|
||||
|
||||
"""
|
||||
SCOPES = ["https://www.googleapis.com/auth/youtube.force-ssl"]
|
||||
TOKEN_FILE = "token.json"
|
||||
CLIENT_SECRET_FILE = "client_secret.json"
|
||||
|
||||
def get_authenticated_service():
|
||||
""Authenticates and returns a YouTube service object.""
|
||||
creds = None
|
||||
if os.path.exists(TOKEN_FILE):
|
||||
creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
|
||||
|
||||
if not creds or not creds.valid:
|
||||
if creds and creds.expired and creds.refresh_token:
|
||||
creds.refresh(Request())
|
||||
else:
|
||||
if not os.path.exists(CLIENT_SECRET_FILE):
|
||||
raise FileNotFoundError(
|
||||
f"{CLIENT_SECRET_FILE} not found. Download it from Google Cloud Console."
|
||||
)
|
||||
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES)
|
||||
creds = flow.run_local_server(port=0)
|
||||
|
||||
with open(TOKEN_FILE, "w") as token:
|
||||
token.write(creds.to_json())
|
||||
|
||||
return build("youtube", "v3", credentials=creds)
|
||||
"""
|
||||
|
||||
def get_video_details(youtube, video_id: str):
|
||||
"""Finds the video and returns its current snippet."""
|
||||
try:
|
||||
response = youtube.videos().list(
|
||||
part="snippet,status",
|
||||
id=video_id
|
||||
).execute()
|
||||
|
||||
if not response["items"]:
|
||||
print(f"❌ Video not found: {video_id}")
|
||||
return None
|
||||
|
||||
return response["items"][0]
|
||||
except HttpError as e:
|
||||
print(f"❌ API Error while fetching video: {e}")
|
||||
return None
|
||||
|
||||
def update_video_description(video_id: str, new_description: str, append: bool = False):
|
||||
"""
|
||||
Takes a YouTube video ID, finds the video, and changes its description.
|
||||
|
||||
Args:
|
||||
video_id (str): The YouTube video ID (e.g., 'dQw4w9WgXcQ')
|
||||
new_description (str): The new description to set
|
||||
append (bool): If True, appends new_description to existing description instead of replacing
|
||||
|
||||
Returns:
|
||||
dict | None: Updated video snippet or None on failure
|
||||
"""
|
||||
# Load credentials
|
||||
if not CREDENTIALS:
|
||||
print("❌ Error: YouTube API credentials not configured.")
|
||||
return None
|
||||
|
||||
# Create YouTube API client
|
||||
youtube = build('youtube', 'v3', credentials=CREDENTIALS)
|
||||
|
||||
video = get_video_details(youtube, video_id)
|
||||
if not video:
|
||||
return None
|
||||
|
||||
snippet = video["snippet"]
|
||||
current_title = snippet["title"]
|
||||
current_category = snippet.get("categoryId", "22")
|
||||
|
||||
print(f"Found video: '{current_title}' [{video_id}]")
|
||||
print(f"Current description length: {len(snippet.get('description', ''))}")
|
||||
current_description = snippet.get("description", "") or ""
|
||||
|
||||
# Decide final description
|
||||
if append:
|
||||
if new_description.strip().lower() in current_description.lower():
|
||||
print("ℹ️ Description already contains that string, skipping")
|
||||
return snippet
|
||||
final_description = (snippet.get("description", "") + "\n\n" + new_description).strip()
|
||||
else:
|
||||
final_description = new_description
|
||||
|
||||
# YouTube requires you to send title + categoryId + description when updating snippet
|
||||
body = {
|
||||
"id": video_id,
|
||||
"snippet": {
|
||||
"title": current_title,
|
||||
"description": final_description,
|
||||
"categoryId": current_category,
|
||||
# Keep existing tags, etc if you want
|
||||
"tags": snippet.get("tags", []),
|
||||
"defaultLanguage": snippet.get("defaultLanguage"),
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
update_response = youtube.videos().update(
|
||||
part="snippet",
|
||||
body=body
|
||||
).execute()
|
||||
|
||||
print(f"✅ Description updated for {video_id}")
|
||||
print(f"New length: {len(final_description)}")
|
||||
return update_response["snippet"]
|
||||
|
||||
except HttpError as e:
|
||||
print(f"❌ Failed to update video {video_id}: {e}")
|
||||
if "quotaExceeded" in str(e):
|
||||
print("You hit the YouTube API quota.")
|
||||
return None
|
||||
|
||||
def append_tag_if_not_exists(video_id: str, new_tag: str):
|
||||
"""
|
||||
Appends a new tag to a YouTube video if it doesn't already exist.
|
||||
Case-insensitive check.
|
||||
"""
|
||||
if not new_tag or not new_tag.strip():
|
||||
print("❌ Tag is empty")
|
||||
return None
|
||||
|
||||
new_tag_clean = new_tag.strip()
|
||||
|
||||
# Load credentials
|
||||
if not CREDENTIALS:
|
||||
print("❌ Error: YouTube API credentials not configured.")
|
||||
return None
|
||||
|
||||
# Create YouTube API client
|
||||
youtube = build('youtube', 'v3', credentials=CREDENTIALS)
|
||||
|
||||
video = get_video_details(youtube, video_id)
|
||||
if not video:
|
||||
return None
|
||||
|
||||
snippet = video["snippet"]
|
||||
existing_tags = snippet.get("tags", []) or []
|
||||
existing_lower = {t.lower().strip() for t in existing_tags}
|
||||
|
||||
if new_tag_clean.lower() in existing_lower:
|
||||
print(f"ℹ️ Tag '{new_tag_clean}' already exists on {video_id}, skipping")
|
||||
return existing_tags
|
||||
|
||||
updated_tags = existing_tags + [new_tag_clean]
|
||||
|
||||
total_chars = sum(len(t) for t in updated_tags)
|
||||
if total_chars > 450:
|
||||
print(f"⚠️ Tags approaching 500 char limit ({total_chars})")
|
||||
if len(updated_tags) > 15:
|
||||
print(f"⚠️ {len(updated_tags)} tags - YouTube ignores after ~15-20")
|
||||
|
||||
print(f"Current tags: {existing_tags}")
|
||||
print(f"Adding: '{new_tag_clean}'")
|
||||
|
||||
body = {
|
||||
"id": video_id,
|
||||
"snippet": {
|
||||
"title": snippet["title"],
|
||||
"description": snippet.get("description", ""),
|
||||
"categoryId": snippet.get("categoryId", "22"),
|
||||
"tags": updated_tags,
|
||||
}
|
||||
}
|
||||
try:
|
||||
resp = youtube.videos().update(part="snippet", body=body).execute()
|
||||
print(f"✅ Tag '{new_tag_clean}' added to {video_id}")
|
||||
return resp["snippet"].get("tags", [])
|
||||
except HttpError as e:
|
||||
print(f"❌ Failed to add tag: {e}")
|
||||
return None
|
||||
|
||||
def main():
|
||||
videos = DB.get_youtube_info_all()
|
||||
for video in videos:
|
||||
if video.yt_id:
|
||||
update_video_description(video.yt_id, f"({video.id})", True)
|
||||
append_tag_if_not_exists(video.yt_id, video.id)
|
||||
if video.yt_short_id:
|
||||
update_video_description(video.yt_short_id, f"({video.id})", True)
|
||||
append_tag_if_not_exists(video.yt_short_id, video.id)
|
||||
|
||||
|
||||
# --- Example Usage ---
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# Example 1: Replace description
|
||||
# update_video_description("YOUR_VIDEO_ID_HERE", "This is my new description!\nGame: Valorant")
|
||||
|
||||
# Example 2: Append to description
|
||||
# update_video_description("YOUR_VIDEO_ID_HERE", "\n\nCheck out https://twitch.tv/teampgp", append=True)
|
||||
|
||||
# For integration with your existing code:
|
||||
#vid = input("Enter YouTube Video ID: ").strip()
|
||||
#if vid:
|
||||
# new_desc = input("Enter new description: ").strip()
|
||||
# update_video_description(vid, new_desc)
|
||||
Reference in new issue
Block a user