diff --git a/.gitignore b/.gitignore index 48f3717..57cfa2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ /secrets.json /client_secrets.json /clips_database.db -__pycache__/uploader.cpython-313.pyc database.db -__pycache__/database.cpython-313.pyc +save +__pycache__ diff --git a/.vscode/launch.json b/.vscode/launch.json index 3bbaa4a..cc3a575 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,21 +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" - ] - } - ] +{ + // 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 f91d6e1..5e0bbd6 100644 --- a/database.py +++ b/database.py @@ -15,7 +15,7 @@ class Database: self.CURSOR = self.CONN.cursor() if self.table == "vods": - self.columns = "id, date, title, gamename, downloaded, uploaded_yt, chat_upload_yt" + self.columns = "id, date, title, gamename, downloaded, uploaded_yt, chats_upload_yt" elif self.table == "clips": self.columns = "slug, date, title, gamename, clip_by, view_count, downloaded, uploaded_yt, shorts_upload_yt" @@ -53,7 +53,7 @@ class Database: gamename TEXT NOT NULL, downloaded INTEGER NOT NULL, uploaded_yt INTEGER NOT NULL, - chat_upload_yt INTEGER NOT NULL + chats_upload_yt INTEGER NOT NULL ) """ ) @@ -80,7 +80,7 @@ class Database: def mark_as_uploaded_shorts_chats(self, record_id: int): """Flags a specific row record to uploaded (1).""" - set_sql = "chat_upload_yt" + set_sql = "chats_upload_yt" if self.table == "clips": set_sql = "shorts_uploaded_yt" @@ -94,12 +94,12 @@ class Database: def mark_as_downloaded(self, record_id: int): """Updates the downloaded status to True (1) for a specific record ID.""" - self.__mark_as(record_id, "download") + self.__mark_as(record_id, "downloaded") def get_unuploaded_shorts_chats(self): """Retrieves all clip rows remaining to be chat uploaded.""" - where_sql = "chat_upload_yt" + where_sql = "chats_upload_yt" if self.table == "clips": where_sql = "shorts_uploaded_yt" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8491f41 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,97 @@ +apt-listchanges==4.8 +attrs==26.1.0 +beautifulsoup4==4.14.3 +certifi==2026.2.25 +cffi==2.0.0 +chardet==5.2.0 +charset-normalizer==3.4.7 +click==8.4.2 +colorama==0.4.6 +cryptography==49.0.0 +cuda-bindings==13.3.1 +cuda-pathfinder==1.5.6 +cuda-toolkit==13.0.3.0 +decorator==5.3.1 +defusedxml==0.7.1 +docopt==0.6.2 +filelock==3.31.0 +fsspec==2026.6.0 +google-api-core==2.31.0 +google-api-python-client==2.198.0 +google-auth==2.56.0 +google-auth-httplib2==0.4.0 +google-auth-oauthlib==1.4.0 +googleapis-common-protos==1.75.0 +h11==0.16.0 +httplib2==0.32.0 +idna==3.11 +ImageIO==2.37.3 +imageio-ffmpeg==0.6.0 +Jinja2==3.1.6 +joblib==1.5.3 +llvmlite==0.48.0 +MarkupSafe==3.0.3 +more-itertools==11.1.0 +moviepy==2.2.1 +mpmath==1.3.0 +networkx==3.6.1 +nltk==3.10.0 +numba==0.66.0 +numpy==2.4.6 +nvidia-cublas==13.1.1.3 +nvidia-cuda-cupti==13.0.85 +nvidia-cuda-nvrtc==13.0.88 +nvidia-cuda-runtime==13.0.96 +nvidia-cudnn-cu13==9.20.0.48 +nvidia-cufft==12.0.0.61 +nvidia-cufile==1.15.1.6 +nvidia-curand==10.4.0.35 +nvidia-cusolver==12.0.4.66 +nvidia-cusparse==12.6.3.3 +nvidia-cusparselt-cu13==0.8.1 +nvidia-nccl-cu13==2.29.7 +nvidia-nvjitlink==13.3.33 +nvidia-nvshmem-cu13==3.4.5 +nvidia-nvtx==13.0.85 +oauthlib==3.3.1 +openai-whisper==20250625 +outcome==1.3.0.post0 +packaging==26.2 +pillow==11.3.0 +pipreqs==0.4.13 +proglog==0.1.12 +proto-plus==1.28.1 +protobuf==7.35.1 +pyasn1==0.6.4 +pyasn1_modules==0.4.2 +pycparser==3.0 +pyparsing==3.3.2 +PySocks==1.7.1 +python-apt==3.0.0 +python-debian==1.0.1 +python-debianbts==4.1.1 +python-dotenv==1.2.2 +regex==2026.7.19 +reportbug==13.2.0 +requests==2.33.1 +requests-oauthlib==2.0.0 +selenium==4.43.0 +setuptools==83.0.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +soupsieve==2.8.3 +sympy==1.14.0 +tiktoken==0.13.0 +torch==2.13.0 +tqdm==4.67.3 +trio==0.33.0 +trio-websocket==0.12.2 +triton==3.7.1 +typing_extensions==4.15.0 +uritemplate==4.2.0 +urllib3==2.6.3 +webdriver-manager==4.0.2 +websocket-client==1.9.0 +wheel==0.46.1 +wsproto==1.3.2 +yarg==0.1.10 diff --git a/transcribe_video.py b/transcribe_video.py index 08daa1b..698f711 100644 --- a/transcribe_video.py +++ b/transcribe_video.py @@ -1,23 +1,45 @@ import os +import subprocess import whisper -from moviepy.editor import VideoFileClip +from moviepy import VideoFileClip from whisper.utils import get_writer -def extract_audio(video_path, audio_temp_path): - print("Extracting uncompressed WAV audio...") - video = VideoFileClip(video_path) - # Extract as WAV, strictly setting the sample rate to 16000Hz for Whisper - video.audio.write_audiofile( - audio_temp_path, - codec="pcm_s16le", - ffmpeg_params=["-ar", "16000", "-ac", "1"] - ) - video.close() +model = whisper.load_model("base") -def transcribe_to_srt(audio_path, output_directory, output_filename): - print("Loading Whisper model...") - model = whisper.load_model("base") +def extract_audio(video_path, audio_temp_path): + # Create a temporary path for a sanitized copy of the video + sanitized_video_path = video_path.replace(".mp4", "_clean.mp4") + print("Sanitizing video metadata for MoviePy parser...") + # -map_chapters -1 removes chapter layouts that break the parser. + # -sn strips text/subtitle streams that crash MoviePy. + # -c copy copies video and audio instantly without quality loss. + cleanup_cmd = [ + "ffmpeg", "-y", "-i", video_path, + "-map_chapters", "-1", "-sn", + "-c", "copy", sanitized_video_path + ] + + # Run the sanitization process silently + subprocess.run(cleanup_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print("Extracting uncompressed WAV audio...") + try: + # Load the sanitized file instead of the raw Twitch clip + with VideoFileClip(sanitized_video_path) as video: + video.audio.write_audiofile( + audio_temp_path, + fps=16000, + codec="pcm_s16le", + ffmpeg_params=["-ac", "1"] + ) + finally: + # Always clean up the temporary sanitized video on Windows 11 + if os.path.exists(sanitized_video_path): + os.remove(sanitized_video_path) + + +def transcribe_to_srt(audio_path, output_directory, output_filename): print("Transcribing audio...") result = model.transcribe(audio_path) @@ -26,6 +48,9 @@ def transcribe_to_srt(audio_path, output_directory, output_filename): srt_writer(result, output_filename, {}) print(f"SRT subtitle file saved in: {output_directory}") + if os.path.exists(audio_path): + os.remove(audio_path) + if __name__ == "__main__": video_path = "my_video.mp4" diff --git a/twitch_down_uploader.py b/twitch_down_uploader.py new file mode 100644 index 0000000..aafe944 --- /dev/null +++ b/twitch_down_uploader.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +import requests +import os +import subprocess + +from database import Database + +import uploader +from uploader import CategoryId + +import youtube_hashtags +import transcribe_video + +CHANNEL_NAME = 'teampgp' # Replace with the streamer's username +DB = None + +def run_linux_command(command: str): + """Executes a Linux command, waits for completion, and returns output.""" + try: + # shell=True allows running full command strings with pipes/wildcards + # text=True returns strings instead of bytes + result = subprocess.run( + command, shell=True, check=True, capture_output=True, text=True + ) + + return {"success": True, "stdout": result.stdout, "stderr": result.stderr} + + except subprocess.CalledProcessError as e: + # Handles errors if the Linux command returns a non-zero exit code + return {"success": False, "stdout": e.stdout, "stderr": e.stderr} + +def transcribe(slug: str): + """Transcribes the Video File.""" + video_file = f"save/{DB.table}/{slug}/{slug}.mp4" + + # Check if the video file exists + if not os.path.exists(video_file): + print(f"Error: File not found: {video_file}") + return False + + # no need to continue if srt transcribe file already exists + if os.path.exists(f"save/{DB.table}/{slug}/transcribe_{slug}.srt"): + print(f"video already transcribed:") + return True + + transcribe_video.extract_audio(video_file, f"save/{DB.table}/{slug}/temp_{slug}_audio.wav") + transcribe_video.transcribe_to_srt(f"save/{DB.table}/{slug}/temp_{slug}_audio.wav", f"save/{DB.table}/{slug}/", f"transcribe_{slug}") + + return True + +def top_hashtags(slug: str): + """"Hashtags from transcribed SRT file.""" + file_srt = f"save/{DB.table}/{slug}/transcribe_{slug}.srt" + + # if srt transcribe file not exists + if not os.path.exists(file_srt): + print(f"Transcribe file not found {file_srt}") + return [] + + tags = youtube_hashtags.get_top_hashtags(file_srt) + return tags + +def download(): + """Find all undownload vods and download them.""" + undownloaded = DB.get_undownloaded() + + print(f"Download {DB.table}...") + + if DB.table == "vods": + for record_id, record_date, title, game_name, downloaded, uploaded_yt, chat_upload_yt in undownloaded: + 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}/{record_id}.mp4 --collision Overwrite") + output2 = run_linux_command(f"TwitchDownloaderCLI chatdownload --id {record_id} -o save/vods/{record_id}/{record_id}_chat.json -E --collision Overwrite") + if output["success"] is True: + print(f"ID: {record_id} | Date: {record_date} | Game: {game_name} | Title: {title} | was successful") + DB.mark_as_downloaded(record_id) + else: + print(f"ID: {record_id} | Download Process failed. {output["stdout"]}. Error: {output["stderr"]}") + + elif DB.table == "clips": + for slug, record_date, title, game_name, clip_by, views, downloaded, uploaded_yt, uploaded_shorts_yt in undownloaded: + 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}/{slug}.mp4 --collision Overwrite") + + 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. {output["stdout"]}. Error: {output["stderr"]}") + + + print(f"Finished Downloading {DB.table}...") + +def upload(): + """Loops over the downloaded videos entries and uploaded them to youtube.""" + print(f"Uploading {DB.table}...") + + unuploaded = DB.get_unuploaded() + + twitch_datetime = " #Twitch Every Friday and Sunday @7:30 EST https://twitch.tv/teampgp" + file_path = "" + title = "" + description = "" + categoryId = CategoryId.GAMING + privatcyStatus = 'private' + base_tags = ['gaming', 'TeamPGP', 'twitch', 'Level1Techs'] + + upload_queue = [] + + if DB.table == "vods": + for record_id, record_date, title, game_name, downloaded, uploaded_yt, chats_upload_yt in unuploaded: + print(f"ID: {record_id} | Date: {record_date} | Game: {game_name} | Title: {title}") + + file_path = f"save/vods/{record_id}/{record_id}.mp4" + description = f"Game: {game_name}, on {record_date}, #VODS {twitch_datetime}" + tags = list(base_tags) + tags.extend([f'{game_name}', 'twitch_vods', 'vods']) + tags.extend(top_hashtags(record_id)) + + upload_queue.append([record_id, file_path, title, categoryId, description, privatcyStatus, tags]) + + elif DB.table == "clips": + for slug, record_date, title, game_name, clip_by, views, downloaded, uploaded_yt, shorts_uploaded_yt in unuploaded: + print(f"Slug: {slug} | Date: {record_date} | Game: {game_name} | By: {clip_by} | Views: {views} | Title: {title}") + + transcribe(slug) + + file_path = f"save/clips/{slug}/{slug}.mp4" + description = f"Game: {game_name}, Cliped By: {clip_by}, on {record_date}, #Shorts #Clips {twitch_datetime}" + tags = list(base_tags) + tags.extend([f'{game_name}', 'twitch_clips', 'clips', f'{clip_by}', 'shorts']) + tags.extend(top_hashtags(slug)) + + upload_queue.append([slug, file_path, title, categoryId, description, privatcyStatus, tags]) + + for db_id, file_path, title, categoryId, description, privatcyStatus, tags in upload_queue: + output = uploader.upload_video(file_path, title, categoryId, description, privatcyStatus, tags) + if output is True: + print(f"Title: {title} | Was successfully uploaded.") + DB.mark_as_uploaded(db_id) + else: + print(f"Title: {title} | Download Process failed.") + +def create_chats(): + pass + +def create_shorts(): + pass + +def get_vod_ids_simplified(channel_name: str): + """Queries Twitch's public endpoint directly for trending clips.""" + session = requests.Session() + url = "https://gql.twitch.tv/gql" + + # Case-preserved headers to prevent 400 Bad Request errors + session.headers = { + "Client-ID": "kimne78kx3ncx6brgo4mv6wki5h1ko", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Content-Type": "text/plain" + } + + vods_query_string = """ + query GetChannelVideos($login: String!, $limit: Int!) { + user(login: $login) { + videos(first: $limit, types: [ARCHIVE]) { + edges { + node { + id + title + publishedAt + game { + displayName + } + } + } + } + } + } + """ + + clips_query_string = """ + query GetChannelClips($login: String!, $limit: Int!) { + user(login: $login) { + clips(first: $limit, criteria: { period: ALL_TIME }) { + edges { + node { + slug + title + createdAt + viewCount + game { + displayName + } + curator { + login + } + } + } + } + } + } + """ + + limit = 50 + query_string = "" + operation_name = "" + if DB.table == "vods": + query_string = vods_query_string + limit = 50 + operation_name = "GetChannelVideos" + elif DB.table == "clips": + query_string = clips_query_string + limit = 40 + operation_name = "GetChannelClips" + # A simplified query string that hardcodes the ARCHIVE type filter into the request structure + payload = [{ + "operationName": operation_name, + "query": query_string, + "variables": { + "login": channel_name.lower(), + "limit": limit + } + }] + + try: + # Prepping ensures Python does not rewrite the Client-ID header case + req = requests.Request('POST', url, json=payload) + prepped = session.prepare_request(req) + + response = session.send(prepped) + response.raise_for_status() + + data = response.json() + + # Pull out the target index array dictionary object + result = data[0] if isinstance(data, list) else data + + if "errors" in result: + print(f"Twitch GraphQL Error: {result['errors']}") + return [] + + user_data = result['data']['user'] + if not user_data: + print(f"Channel '{channel_name}' not found.") + return [] + + edges = None + if DB.table == "vods": + edges = user_data['videos']['edges'] + elif DB.table == "clips": + edges = user_data['clips']['edges'] + video_ids = [] + + print(f"--- Latest VODs for {channel_name} ---") + for edge in edges: + node = edge['node'] + + # Safe extraction in case a VOD has no category set (Just Chatting, Uncategorized, etc.) + game_info = node.get('game') + game_name = game_info.get('displayName') if game_info else "Unknown/No Category" + if DB.table == "vods": + print(f"ID: {node['id']} | Date: {node['publishedAt']} | Game: {game_name} | Title: {node['title']}") + + # Pass game_name to your database logic + DB.insert_vods_record(node['id'], node['publishedAt'], node['title'], game_name) + video_ids.append(node['id']) + elif DB.table == "clips": + # Safe extraction in case the curator account was deleted/missing + curator_info = node.get('curator') + clip_by = curator_info.get('login') if curator_info else "Unknown Creator" + + print(f"Slug: {node['slug']} | Date: {node['createdAt']} | Game: {game_name} | By: {clip_by} | Views: {node['viewCount']} | Title: {node['title']}") + + DB.insert_clips_record(node['slug'], node['createdAt'], node['title'], game_name, clip_by, int(node['viewCount'])) + video_ids.append(node['slug']) + + return video_ids + + except Exception as e: + print(f"An unexpected error occurred: {e}") + if 'response' in locals(): + print(f"Server Response Text: {response.text}") + return [] + +if __name__ == "__main__": + tables = ["vods", "clips"] + for table in tables: + DB = Database(table) + get_vod_ids_simplified(CHANNEL_NAME) + download() + upload() + DB.close_database() \ No newline at end of file diff --git a/twitch_download_vod.py b/twitch_download_vod.py index dacfa6b..2420865 100644 --- a/twitch_download_vod.py +++ b/twitch_download_vod.py @@ -74,7 +74,7 @@ def download(): print(f"ID: {record_id} | Date: {record_date} | Game: {game_name} | Title: {title} | was successful") DB.mark_as_downloaded(record_id) else: - print(f"ID: {record_id} | Date: {record_date} | Game: {game_name} | Title: {title} | was Failed") + print(f"ID: {record_id} | Download Process failed. {output["stdout"]}. Error: {output["stderr"]}") elif DB.table == "clips": for slug, record_date, title, game_name, clip_by, views, downloaded, uploaded_yt, uploaded_shorts_yt in undownloaded: @@ -98,49 +98,56 @@ def upload(): unuploaded = DB.get_unuploaded() + twitch_datetime = " #Twitch Every Friday and Sunday @7:30 EST https://twitch.tv/teampgp" + file_path = "" + title = "" + description = "" + categoryId = CategoryId.GAMING + privatcyStatus = 'private' + base_tags = ['gaming', 'TeamPGP', 'twitch', 'Level1Techs', 'twitch'] + + upload_queue = [] + if DB.table == "vods": - for record_id, record_date, title, game_name, downloaded, uploaded_yt, chat_upload_yt in unuploaded_vods: + for record_id, record_date, title, game_name, downloaded, uploaded_yt, chats_upload_yt in unuploaded: 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}, #VODS #Twitch Every Friday and Sunday @7:30 EST https://twitch.tv/teampgp" - categoryId = CategoryId.GAMING - privatcyStatus = 'private' - tags = ['gaming', 'TeamPGP', f'{game_name}', 'twitch_vods', 'vods', 'Level1Techs', 'twitch'] + description = f"Game: {game_name}, on {record_date}, #VODS {twitch_datetime}" + tags = list(base_tags) + tags.extend([f'{game_name}', 'twitch_vods', 'vods']) + tags.extend(top_hashtags(record_id)) - output = uploader.upload_video(file_path, title, description, categoryId, privatcyStatus, tags) + upload_queue.append([record_id, file_path, title, categoryId, description, 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.") elif DB.table == "clips": - for slug, record_date, title, game_name, clip_by, views, downloaded, uploaded_yt in unuploaded_clips: + for slug, record_date, title, game_name, clip_by, views, downloaded, uploaded_yt in unuploaded: print(f"Slug: {slug} | Date: {record_date} | Game: {game_name} | By: {clip_by} | Views: {views} | Title: {title}") file_path = f"save/clips/{slug}/{title}.mp4" - title = title - description = f"Game: {game_name}, Cliped By: {clip_by}, on {record_date}, #Shorts #Clips #Twitch Every Friday and Sunday @7:30 EST https://twitch.tv/teampgp" - categoryId = CategoryId.GAMING - privatcyStatus = 'private' - tags = ['shorts', 'gaming', 'TeamPGP', f'{game_name}', f'{clip_by}', 'twitch_clips', 'clips', 'Level1Techs', 'twitch'] + description = f"Game: {game_name}, Cliped By: {clip_by}, on {record_date}, #Shorts #Clips {twitch_datetime}" + tags = list(base_tags) + tags.extend([f'{game_name}', 'twitch_clips', 'clips', f'{clip_by}', 'shorts']) + tags.extend(top_hashtags(slug)) - tags.extend(top_hashtags({slug})) + upload_queue.append([slug, file_path, title, categoryId, description, privatcyStatus, tags]) - #output = uploader.upload_video(file_path, title, categoryId, description, privatcyStatus, tags) + for db_id, file_path, title, categoryId, description, privatcyStatus, tags in upload_queue: + output = uploader.upload_video(file_path, title, categoryId, description, privatcyStatus, tags) + if output is True: + print(f"Title: {title} | Was successfully uploaded.") + DB.mark_as_uploaded(db_id) + else: + print(f"Title: {title} | Download Process failed.") - if output is True: - print(f"Slug: {slug} | Was successfully uploaded.") - DB.mark_as_uploaded(slug) - else: - print(f"Slug: {slug} | Upload Process failed.") +def create_chats(): + pass -def create_chat_vods(): +def create_shorts(): pass def get_vod_ids_simplified(channel_name: str): + """Queries Twitch's public endpoint directly for trending clips.""" session = requests.Session() url = "https://gql.twitch.tv/gql" @@ -150,9 +157,8 @@ def get_vod_ids_simplified(channel_name: str): "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Content-Type": "text/plain" } - - # A simplified query string that hardcodes the ARCHIVE type filter into the request structure - query_string = """ + + vods_query_string = """ query GetChannelVideos($login: String!, $limit: Int!) { user(login: $login) { videos(first: $limit, types: [ARCHIVE]) { @@ -171,12 +177,44 @@ def get_vod_ids_simplified(channel_name: str): } """ + clips_query_string = """ + query GetChannelClips($login: String!, $limit: Int!) { + user(login: $login) { + clips(first: $limit, criteria: { period: ALL_TIME }) { + edges { + node { + slug + title + createdAt + viewCount + game { + displayName + } + curator { + login + } + } + } + } + } + } + """ + + limit = 50 + query_string = "" + if DB.table == "vods": + query_string = vods_query_string + limit = 50 + elif DB.table == "clips": + query_string = clips_query_string + limit = 40 + # A simplified query string that hardcodes the ARCHIVE type filter into the request structure payload = [{ "operationName": "GetChannelVideos", "query": query_string, "variables": { "login": channel_name.lower(), - "limit": 50 + "limit": limit } }] @@ -201,9 +239,13 @@ def get_vod_ids_simplified(channel_name: str): if not user_data: print(f"Channel '{channel_name}' not found.") return [] - - edges = user_data['videos']['edges'] - vod_ids = [] + + edges = None + if DB.table == "vods": + edges = user_data['videos']['edges'] + elif DB.table == "clips": + edges = user_data['clips']['edges'] + video_ids = [] print(f"--- Latest VODs for {channel_name} ---") for edge in edges: @@ -212,15 +254,24 @@ def get_vod_ids_simplified(channel_name: str): # Safe extraction in case a VOD has no category set (Just Chatting, Uncategorized, etc.) game_info = node.get('game') game_name = game_info.get('displayName') if game_info else "Unknown/No Category" + if DB.table == "vods": + 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 + DB.insert_vods_record(node['id'], node['publishedAt'], node['title'], game_name) + #insert_record(node['id'], node['publishedAt'], node['title'], game_name) + video_ids.append(node['id']) + elif DB.table == "clips": + # Safe extraction in case the curator account was deleted/missing + curator_info = node.get('curator') + clip_by = curator_info.get('login') if curator_info else "Unknown Creator" + + print(f"Slug: {node['slug']} | Date: {node['createdAt']} | Game: {game_name} | By: {clip_by} | Views: {node['viewCount']} | Title: {node['title']}") + + DB.insert_clips_record(node['slug'], node['createdAt'], node['title'], game_name, clip_by, int(node['viewCount'])) + video_ids.append(node['slug']) - # Pass game_name to your database logic - 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']) - - return vod_ids + return video_ids except Exception as e: print(f"An unexpected error occurred: {e}") @@ -232,4 +283,4 @@ if __name__ == "__main__": get_vod_ids_simplified(CHANNEL_NAME) download_vods() upload_vods() - #close_database() \ No newline at end of file + DB.close_database() \ No newline at end of file diff --git a/youtube_hashtags.py b/youtube_hashtags.py index 73e0b47..78ddedd 100644 --- a/youtube_hashtags.py +++ b/youtube_hashtags.py @@ -8,6 +8,7 @@ import nltk # Download necessary NLTK data nltk.download('punkt', quiet=True) nltk.download('stopwords', quiet=True) +nltk.download('punkt_tab', quiet=True) def extract_text_from_srt(file_path): with open(file_path, 'r', encoding='utf-8') as file: @@ -38,7 +39,7 @@ def get_top_hashtags(srt_file_path, top_n=10): word_counts = Counter(filtered_words) top_words = word_counts.most_common(top_n) - hashtags = [f"#{word[0]}" for word in top_words] + hashtags = [f"{word[0]}" for word in top_words] return hashtags if __name__ == "__main__": diff --git a/youtube_short.py b/youtube_short.py index fc36c42..7d3bcf5 100644 --- a/youtube_short.py +++ b/youtube_short.py @@ -1,5 +1,5 @@ import sys -from moviepy.editor import VideoFileClip +from moviepy import VideoFileClip def convert_to_short(input_path):