diff --git a/modules/downloader.py b/modules/downloader.py index 1a7ba00..a4ea645 100644 --- a/modules/downloader.py +++ b/modules/downloader.py @@ -505,6 +505,16 @@ class ContentDownloader: if not self.download_live_chat: print(f'{Fore.YELLOW}⚠ downloadLiveCHAT is disabled in config{Style.RESET_ALL}') return False + + # If a stream monitor was provided, check that the user is currently live + if stream_monitor is not None: + try: + if not stream_monitor.is_user_live(): + print(f'{Fore.YELLOW}⚠ Stream is not live; skipping chat download{Style.RESET_ALL}') + return False + except Exception as e: + # If we couldn't determine live status, continue and let chat_downloader handle it + print(f'{Fore.YELLOW}⚠ Could not determine live status: {e} - proceeding with chat download{Style.RESET_ALL}') print(f'\n{Fore.CYAN}Starting live chat download (chat_downloader)...{Style.RESET_ALL}') print(f'{Fore.MAGENTA}[VERBOSE] chat_downloader library version: {ChatDownloader.__module__}{Style.RESET_ALL}') diff --git a/modules/file_manager.py b/modules/file_manager.py index 97f3909..4567cb6 100644 --- a/modules/file_manager.py +++ b/modules/file_manager.py @@ -129,51 +129,60 @@ class FileManager: os.makedirs(os.path.dirname(upload_list_path), exist_ok=True) files_to_upload = [] - - # Always include metadata and chat JSON - files_to_upload.append(f"{PREFIX_METADATA}{filename_base}.json") - files_to_upload.append(f"{PREFIX_CHAT}{filename_base}.json") - - # Add pre-merge videos (original LIVE and VOD files) + + # Build files list relative to root_path so rclone can read them with --files-from + # Metadata and chat JSON + files_to_upload.append(os.path.join(self.username, 'metadata', f"{PREFIX_METADATA}{filename_base}.json")) + files_to_upload.append(os.path.join(self.username, 'chat', 'json', f"{PREFIX_CHAT}{filename_base}.json")) + + # Pre-merge videos (raw .ts in video/raw, mp4/mp3 in video) if self.upload_pre_merge_video: files_to_upload.extend([ - f"{PREFIX_LIVE}{filename_base}.ts", - f"{PREFIX_LIVE}{filename_base}.mp4", - f"{PREFIX_LIVE}{filename_base}.mp3", - f"{PREFIX_VOD}{filename_base}.ts", - f"{PREFIX_VOD}{filename_base}.mp4", - f"{PREFIX_VOD}{filename_base}.mp3" + os.path.join(self.username, 'video', 'raw', f"{PREFIX_LIVE}{filename_base}.ts"), + os.path.join(self.username, 'video', f"{PREFIX_LIVE}{filename_base}.mp4"), + os.path.join(self.username, 'video', f"{PREFIX_LIVE}{filename_base}.mp3"), + os.path.join(self.username, 'video', 'raw', f"{PREFIX_VOD}{filename_base}.ts"), + os.path.join(self.username, 'video', f"{PREFIX_VOD}{filename_base}.mp4"), + os.path.join(self.username, 'video', f"{PREFIX_VOD}{filename_base}.mp3") ]) - - # Add merged videos + + # Merged videos (in video folder) if self.upload_merged_video: files_to_upload.extend([ - f"{PREFIX_MERGED}{filename_base}.mp4", - f"{PREFIX_MERGED}{filename_base}.mp3", - f"{PREFIX_MERGED}{PREFIX_VOD}{filename_base}.mp4", - f"{PREFIX_MERGED}{PREFIX_VOD}{filename_base}.mp3" + os.path.join(self.username, 'video', f"{PREFIX_MERGED}{filename_base}.mp4"), + os.path.join(self.username, 'video', f"{PREFIX_MERGED}{filename_base}.mp3"), + os.path.join(self.username, 'video', f"{PREFIX_MERGED}{PREFIX_VOD}{filename_base}.mp4"), + os.path.join(self.username, 'video', f"{PREFIX_MERGED}{PREFIX_VOD}{filename_base}.mp3") ]) - - # Add standalone chat video + + # Standalone chat video (in chat folder) if self.upload_chat_video: - files_to_upload.append(f"{PREFIX_CHAT}{filename_base}.mp4") + files_to_upload.append(os.path.join(self.username, 'chat', f"{PREFIX_CHAT}{filename_base}.mp4")) with open(upload_list_path, 'w') as f: f.write('\n'.join(files_to_upload)) - # Run rclone + # Run rclone using --files-from so the listed paths (relative to root_path) are uploaded. try: - result = subprocess.call([ + cmd = [ 'rclone', 'copy', str(self.root_path.resolve()), self.rclone_path, - '--include-from', upload_list_path - ]) - + '--files-from', upload_list_path + ] + + # Stream rclone output to console so user can see progress/errors + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + if proc.stdout: + for line in proc.stdout: + print(line, end='') + proc.wait() + result = proc.returncode + # Clean up upload list if os.path.exists(upload_list_path): os.remove(upload_list_path) - + if result == 0: print(f'{Fore.GREEN}✓ Upload complete{Style.RESET_ALL}') if notification_callback: @@ -186,7 +195,7 @@ class FileManager: notification_callback(f'✗ Upload Failed - {filename_base}', f'Upload failed with code {result}. Files preserved locally.') return False - + except Exception as e: print(f'{Fore.RED}✗ Upload error: {str(e)}{Style.RESET_ALL}') return False diff --git a/run_chat_only.py b/run_chat_only.py index ff94189..5134f38 100644 --- a/run_chat_only.py +++ b/run_chat_only.py @@ -20,6 +20,7 @@ from modules.config import ConfigManager from modules.file_manager import FileManager from modules.utils import get_ffmpeg_executable, get_twitch_downloader_executable, detect_operating_system from modules.downloader import ContentDownloader +from modules.stream_monitor import StreamMonitor def main(): @@ -72,12 +73,49 @@ def main(): def shutdown_check(): return stop_requested['stop'] - # Start thread + # Prepare stream monitor + stream_monitor = StreamMonitor(args.username) + + # If chat downloads are disabled in config, enter monitoring mode instead + if not downloader.download_live_chat: + print(f"{Fore.YELLOW}⚠ downloadLiveCHAT is disabled in config - entering monitoring mode for {args.username}{Style.RESET_ALL}") + try: + while True: + try: + is_live = stream_monitor.is_user_live() + if is_live: + print(f"{Fore.GREEN}✓ {args.username} is live! Exiting monitor. Run the archiver to record video.{Style.RESET_ALL}") + break + else: + print(f"{Fore.CYAN}{args.username} is offline - checking again in 30s...{Style.RESET_ALL}") + except Exception as e: + print(f"{Fore.YELLOW}⚠ Could not check stream status: {e}{Style.RESET_ALL}") + time.sleep(30) + except KeyboardInterrupt: + print('\nKeyboard interrupt received; stopping monitor...') + return + + # If chat download is enabled, but the stream is currently offline, wait until it goes live + try: + try: + if not stream_monitor.is_user_live(): + print(f"{Fore.CYAN}{args.username} is currently offline - waiting for live stream to start...{Style.RESET_ALL}") + while not stream_monitor.is_user_live(): + time.sleep(10) + except Exception: + # If we cannot determine live status, proceed to start chat downloader anyway + pass + + except KeyboardInterrupt: + print('\nKeyboard interrupt received; exiting...') + return + + # Start thread (stream_monitor passed so downloader can stop when stream ends) thread = downloader.start_chat_downloader_thread( args.username, json_path, shutdown_check=shutdown_check, - stream_monitor=None, + stream_monitor=stream_monitor, verbose=args.verbose )