From 38d51636af9b9acade0e13fb64d6cd9102ba4999 Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Wed, 11 Feb 2026 13:23:14 +0100 Subject: [PATCH] Chat monitors stream to end --- modules/downloader.py | 23 ++++++++++++++++++++++- modules/stream_monitor.py | 28 ++++++++++++++++++++++++++++ run_tests.ps1 | 37 +++++++++++++++++++++++++++++++++++++ twitch-archive.py | 2 ++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 run_tests.ps1 diff --git a/modules/downloader.py b/modules/downloader.py index 2507bd6..a1f166b 100644 --- a/modules/downloader.py +++ b/modules/downloader.py @@ -365,6 +365,7 @@ class ContentDownloader: max_messages: Optional[int] = None, timeout: Optional[float] = None, shutdown_check: Optional[callable] = None, + stream_monitor = None, verbose: bool = False) -> bool: """ Download live chat using chat_downloader library as fallback. @@ -376,6 +377,7 @@ class ContentDownloader: max_messages: Maximum messages to download (None = unlimited) timeout: Stop after this many seconds (None = until stream ends) shutdown_check: Optional callback function that returns True when shutdown requested + stream_monitor: Optional stream monitor to check if stream is still live verbose: Show chat message previews Returns: @@ -414,7 +416,10 @@ class ContentDownloader: # The get_chat with output parameter writes to file automatically # We just need to iterate to trigger the download message_count = 0 - print(f'{Fore.CYAN}Receiving chat messages (press Ctrl+C to stop)...{Style.RESET_ALL}') + last_check_time = time.time() + check_interval = 10.0 # Check if stream is still live every 10 seconds + + print(f'{Fore.CYAN}Receiving chat messages (will stop when stream ends)...{Style.RESET_ALL}') try: for message in chat: # Check for shutdown request @@ -422,6 +427,19 @@ class ContentDownloader: print(f'\n{Fore.YELLOW}⚠ Chat download stopped by shutdown request{Style.RESET_ALL}') break + # Periodically check if stream is still live + current_time = time.time() + if stream_monitor and (current_time - last_check_time) >= check_interval: + last_check_time = current_time + try: + is_live = stream_monitor.is_user_live() + if not is_live: + print(f'\n{Fore.YELLOW}⚠ Stream ended, stopping chat download{Style.RESET_ALL}') + break + except Exception as check_error: + print(f'\n{Fore.YELLOW}⚠ Could not check stream status: {check_error}{Style.RESET_ALL}') + # Continue downloading to avoid false positives from API errors + message_count += 1 # Show progress every 100 messages @@ -467,6 +485,7 @@ class ContentDownloader: def start_chat_downloader_thread(self, username: str, json_path: str, shutdown_check: Optional[callable] = None, + stream_monitor = None, verbose: bool = False) -> threading.Thread: """ Start chat_downloader in a background thread. @@ -475,6 +494,7 @@ class ContentDownloader: username: Twitch username json_path: Path to save chat JSON shutdown_check: Callback to check for shutdown + stream_monitor: Optional stream monitor to check if stream is still live verbose: Show chat previews Returns: @@ -485,6 +505,7 @@ class ContentDownloader: self.chat_thread_success = self.download_live_chat_with_chat_downloader( username, json_path, shutdown_check=shutdown_check, + stream_monitor=stream_monitor, verbose=verbose ) except Exception as e: diff --git a/modules/stream_monitor.py b/modules/stream_monitor.py index 6c1607a..bea3545 100644 --- a/modules/stream_monitor.py +++ b/modules/stream_monitor.py @@ -115,6 +115,34 @@ class StreamMonitor: print(f'{Fore.YELLOW} {str(e)}{Style.RESET_ALL}') sys.exit(1) + def is_user_live(self) -> bool: + """ + Check if the configured user is currently live. + + Returns: + bool: True if user is live, False if offline + + Raises: + Exception: If API request fails (caller should handle) + """ + query = f'query{{user(login: "{self.username}") {{stream{{id title}}}}}}' + + try: + response = requests.post( + TWITCH_GQL_URL, + json={'query': query}, + headers={"Client-ID": TWITCH_GQL_CLIENT_ID}, + timeout=15 + ) + response.raise_for_status() + data = response.json() + stream_data = data.get('data', {}).get('user', {}).get('stream') + return stream_data is not None + + except requests.exceptions.RequestException as e: + # Don't exit, let caller handle this + raise Exception(f"Failed to check if user is live: {str(e)}") + def get_latest_vod(self) -> Optional[Dict[str, Any]]: """ Get the most recent VOD for the configured user. diff --git a/run_tests.ps1 b/run_tests.ps1 new file mode 100644 index 0000000..f547251 --- /dev/null +++ b/run_tests.ps1 @@ -0,0 +1,37 @@ +# PowerShell script to run Twitch Archive unit tests +# Run this script to execute all unit tests + +Write-Host "======================================================================" -ForegroundColor Cyan +Write-Host "TWITCH ARCHIVE - Running Unit Tests" -ForegroundColor Cyan +Write-Host "======================================================================" -ForegroundColor Cyan +Write-Host "" + +# Check if virtual environment exists and activate it +$venvPath = ".\venv314\Scripts\Activate.ps1" +if (Test-Path $venvPath) { + Write-Host "✓ Activating virtual environment..." -ForegroundColor Green + & $venvPath +} else { + Write-Host "⚠ Virtual environment not found at $venvPath" -ForegroundColor Yellow + Write-Host " Continuing with system Python..." -ForegroundColor Yellow +} + +Write-Host "" + +# Run the tests +Write-Host "Running unit tests..." -ForegroundColor Cyan +python test_twitch_archive_simple.py + +# Check exit code +if ($LASTEXITCODE -eq 0) { + Write-Host "" + Write-Host "======================================================================" -ForegroundColor Green + Write-Host "✓ ALL TESTS PASSED" -ForegroundColor Green + Write-Host "======================================================================" -ForegroundColor Green +} else { + Write-Host "" + Write-Host "======================================================================" -ForegroundColor Red + Write-Host "✗ SOME TESTS FAILED" -ForegroundColor Red + Write-Host "======================================================================" -ForegroundColor Red + exit $LASTEXITCODE +} diff --git a/twitch-archive.py b/twitch-archive.py index 5844cde..2fdcb4e 100644 --- a/twitch-archive.py +++ b/twitch-archive.py @@ -1088,6 +1088,7 @@ class TwitchArchiveManager: archiver.downloader.start_chat_downloader_thread( archiver.username, chat_json_path, shutdown_check=lambda: self.shutdown_requested or archiver.shutdown_requested, + stream_monitor=archiver.stream_monitor, verbose=self.verbose ) except Exception as e: @@ -1116,6 +1117,7 @@ class TwitchArchiveManager: archiver.downloader.start_chat_downloader_thread( archiver.username, chat_json_path, shutdown_check=lambda: self.shutdown_requested or archiver.shutdown_requested, + stream_monitor=archiver.stream_monitor, verbose=self.verbose or self.chat_only ) # Wait for completion