From 7f8b3d1bf9e4c5b0edf64703fe8fd0ecebd8ed6a Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Mon, 9 Feb 2026 22:20:04 +0100 Subject: [PATCH] Implement configuration management for multi-streamer support and update .gitignore --- .gitignore | 10 +- config.sample.json | 57 --- config/global.json.example | 28 ++ config/global.schema.json | 159 +++++++++ config/streamers/streamer.json.example | 5 + twitch-archive.py | 473 ++++++++++++++++++++++++- 6 files changed, 655 insertions(+), 77 deletions(-) delete mode 100644 config.sample.json create mode 100644 config/global.json.example create mode 100644 config/global.schema.json create mode 100644 config/streamers/streamer.json.example diff --git a/.gitignore b/.gitignore index 896b29f..b1a3beb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,13 @@ -# User configuration file (contains personal settings) +# User configuration files (contains personal settings) config.json +config/global.json + +# Streamer-specific configurations (personal settings) +config/streamers/*.json + +# Python cache +__pycache__/ +*.pyc # Environments .env diff --git a/config.sample.json b/config.sample.json deleted file mode 100644 index 0c2b68b..0000000 --- a/config.sample.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "_comment": "Copy this file to config.json and edit with your settings", - - "username": "your_twitch_username", - "_username_comment": "Twitch streamer username to monitor and archive", - - "quality": "best", - "_quality_comment": "Quality options: best/source, high/720p, medium/540p, low/360p, audio_only", - - "root_path": "archive", - "_root_path_comment": "Path where this script saves everything (livestream, VODs, chat, metadata)", - - "rclone_path": "remote:path/to/streams", - "_rclone_path_comment": "Path to rclone remote storage (e.g., MyDrive:Backups/streams)", - - "refresh": 60.0, - "_refresh_comment": "Time between checking in seconds (5.0 is recommended), avoid less than 1.0", - - "streamlink_ttvlol": 0, - "_streamlink_ttvlol_comment": "0 = disable, 1 = enable blocking ads with ttvlol (DEPRECATED: --twitch-proxy-playlist removed in newer streamlink versions)", - - "notifications": 0, - "_notifications_comment": "0 = disable, 1 = enable email notifications", - - "downloadMETADATA": 1, - "_downloadMETADATA_comment": "0 = disable, 1 = enable metadata downloading", - - "downloadVOD": 1, - "_downloadVOD_comment": "0 = disable, 1 = enable VOD downloading after stream finished", - - "downloadCHAT": 1, - "_downloadCHAT_comment": "0 = disable, 1 = enable chat downloading and rendering from VOD (after stream ends)", - - "downloadLiveCHAT": 1, - "_downloadLiveCHAT_comment": "0 = disable, 1 = enable downloading chat during live stream (useful if VODs are disabled)", - - "vodTimeout": 300, - "_vodTimeout_comment": "Seconds to wait for VOD to appear after stream ends (set to 0 to skip VOD check entirely, useful if streamer has VODs disabled)", - - "uploadCloud": 1, - "_uploadCloud_comment": "0 = disable, 1 = enable upload to remote cloud", - - "deleteFiles": 0, - "_deleteFiles_comment": "0 = disable, 1 = enable deleting files after upload (BE CAREFUL WITH THIS OPTION)", - - "onlyRaw": 0, - "_onlyRaw_comment": "0 = convert ts files to mp3/mp4, 1 = keep only raw ts files for recording", - - "cleanRaw": 1, - "_cleanRaw_comment": "0 = keep raw .ts files, 1 = delete raw .ts files after processing", - - "hls_segments": 3, - "_hls_segments_comment": "Number of threads for live stream downloading (1-10, recommended 2-3)", - - "hls_segmentsVOD": 10, - "_hls_segmentsVOD_comment": "Number of threads for VOD downloading (1-10)" -} diff --git a/config/global.json.example b/config/global.json.example new file mode 100644 index 0000000..13eb2ae --- /dev/null +++ b/config/global.json.example @@ -0,0 +1,28 @@ +{ + "$schema": "./global.schema.json", + "quality": "best", + "root_path": "archive", + "rclone_path": "remote:path/to/streams", + "refresh": 60.0, + "notifications": 0, + "downloadMETADATA": 1, + "downloadVOD": 1, + "downloadCHAT": 1, + "downloadLiveCHAT": 1, + "vodTimeout": 300, + "uploadCloud": 1, + "deleteFiles": 0, + "onlyRaw": 0, + "cleanRaw": 1, + "hls_segments": 3, + "hls_segmentsVOD": 10, + "streamlink_ttvlol": 0, + "ffmpeg_hwaccel": "auto", + "ffmpeg_threads": 0, + "ffmpeg_audio_codec": "aac", + "ffmpeg_audio_samplerate": 48000, + "ffmpeg_audio_bitrate": "192k", + "ffmpeg_error_recovery": 1, + "ffmpeg_faststart": 1, + "ffmpeg_progress": 0 +} diff --git a/config/global.schema.json b/config/global.schema.json new file mode 100644 index 0000000..e29d4f5 --- /dev/null +++ b/config/global.schema.json @@ -0,0 +1,159 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/piero0920/Twitch-Archive/config/global.schema.json", + "title": "Twitch Archive Global Configuration", + "description": "Global settings and defaults for all streamers", + "type": "object", + "properties": { + "quality": { + "type": "string", + "enum": ["best", "source", "high", "720p", "medium", "480p", "low", "360p", "audio_only"], + "default": "best", + "description": "Default video quality for stream recording" + }, + "root_path": { + "type": "string", + "default": "archive", + "description": "Root directory path where all archives are saved" + }, + "rclone_path": { + "type": "string", + "default": "remote:path/to/streams", + "description": "Default rclone remote path for cloud uploads (can be overridden per streamer)" + }, + "refresh": { + "type": "number", + "default": 60.0, + "minimum": 1.0, + "description": "Time between status checks in seconds (60.0 recommended for multiple streamers)" + }, + "notifications": { + "type": "integer", + "enum": [0, 1], + "default": 0, + "description": "Email notifications: 0 = disabled, 1 = enabled" + }, + "downloadMETADATA": { + "type": "integer", + "enum": [0, 1], + "default": 1, + "description": "Download stream metadata: 0 = disabled, 1 = enabled" + }, + "downloadVOD": { + "type": "integer", + "enum": [0, 1], + "default": 1, + "description": "Download VODs after stream ends: 0 = disabled, 1 = enabled" + }, + "downloadCHAT": { + "type": "integer", + "enum": [0, 1], + "default": 1, + "description": "Download and render chat from VOD: 0 = disabled, 1 = enabled" + }, + "downloadLiveCHAT": { + "type": "integer", + "enum": [0, 1], + "default": 1, + "description": "Download chat during live stream: 0 = disabled, 1 = enabled" + }, + "vodTimeout": { + "type": "integer", + "default": 300, + "minimum": 0, + "description": "Seconds to wait for VOD to appear after stream ends" + }, + "uploadCloud": { + "type": "integer", + "enum": [0, 1], + "default": 1, + "description": "Upload to rclone remote: 0 = disabled, 1 = enabled" + }, + "deleteFiles": { + "type": "integer", + "enum": [0, 1], + "default": 0, + "description": "Delete local files after upload: 0 = disabled, 1 = enabled (BE CAREFUL)" + }, + "onlyRaw": { + "type": "integer", + "enum": [0, 1], + "default": 0, + "description": "Keep only raw .ts files: 0 = convert to mp3/mp4, 1 = keep raw only" + }, + "cleanRaw": { + "type": "integer", + "enum": [0, 1], + "default": 1, + "description": "Delete raw .ts files after processing: 0 = keep, 1 = delete" + }, + "hls_segments": { + "type": "integer", + "default": 3, + "minimum": 1, + "maximum": 10, + "description": "Number of parallel download threads for live streams (1-10, recommended 2-3)" + }, + "hls_segmentsVOD": { + "type": "integer", + "default": 10, + "minimum": 1, + "maximum": 10, + "description": "Number of parallel download threads for VODs (1-10)" + }, + "streamlink_ttvlol": { + "type": "integer", + "enum": [0, 1], + "default": 0, + "description": "DEPRECATED: Ad-blocking with ttvlol (--twitch-proxy-playlist removed in newer streamlink)" + }, + "ffmpeg_hwaccel": { + "type": "string", + "enum": ["auto", "nvenc", "qsv", "amf", "vaapi", "none"], + "default": "auto", + "description": "Hardware acceleration: auto (recommended), nvenc (NVIDIA), qsv (Intel), amf (AMD), vaapi (Linux), none (software only)" + }, + "ffmpeg_threads": { + "type": "integer", + "default": 0, + "minimum": 0, + "description": "FFmpeg encoding thread count (0 = auto-detect optimal count)" + }, + "ffmpeg_audio_codec": { + "type": "string", + "enum": ["aac", "libmp3lame"], + "default": "aac", + "description": "Audio codec for audio_only quality streams (aac recommended)" + }, + "ffmpeg_audio_samplerate": { + "type": "integer", + "enum": [44100, 48000], + "default": 48000, + "description": "Audio sample rate in Hz (48000 recommended for broadcasts, 44100 for CD quality)" + }, + "ffmpeg_audio_bitrate": { + "type": "string", + "pattern": "^[0-9]+k$", + "default": "192k", + "description": "Audio bitrate (e.g., 128k, 192k, 256k, 320k)" + }, + "ffmpeg_error_recovery": { + "type": "integer", + "enum": [0, 1], + "default": 1, + "description": "Enable error recovery for corrupted/incomplete streams: 0 = disabled, 1 = enabled" + }, + "ffmpeg_faststart": { + "type": "integer", + "enum": [0, 1], + "default": 1, + "description": "Enable MP4 faststart flag for better streaming/playback: 0 = disabled, 1 = enabled" + }, + "ffmpeg_progress": { + "type": "integer", + "enum": [0, 1], + "default": 0, + "description": "Show FFmpeg encoding progress: 0 = silent, 1 = verbose output" + } + } +} diff --git a/config/streamers/streamer.json.example b/config/streamers/streamer.json.example new file mode 100644 index 0000000..e84d85d --- /dev/null +++ b/config/streamers/streamer.json.example @@ -0,0 +1,5 @@ +{ + "$schema": "./streamer.schema.json", + "username": "streamer_username", + "enabled": false +} diff --git a/twitch-archive.py b/twitch-archive.py index c3c9846..2c38604 100644 --- a/twitch-archive.py +++ b/twitch-archive.py @@ -88,6 +88,152 @@ DEFAULT_CONFIG = { # MAIN CLASS # ============================================================================ +class ConfigManager: + """ + Manages global and per-streamer configurations. + + Loads global defaults from config/global.json and merges with per-streamer + configs from config/streamers/*.json. + """ + + def __init__(self): + """Initialize the configuration manager.""" + self.config_dir = pathlib.Path(__file__).parent / "config" + self.streamers_dir = self.config_dir / "streamers" + self.global_config = self._load_global_config() + + def _load_global_config(self) -> Dict[str, Any]: + """ + Load global configuration from config/global.json. + + Returns: + dict: Global configuration with defaults + """ + global_file = self.config_dir / "global.json" + + # Start with DEFAULT_CONFIG as ultimate fallback + config = DEFAULT_CONFIG.copy() + + # Try to load global config + if global_file.exists(): + try: + with open(global_file, 'r', encoding='utf-8') as f: + user_config = json.load(f) + # Filter out comment fields and schema references + user_config = {k: v for k, v in user_config.items() + if not k.startswith('_') and k != '$schema'} + config.update(user_config) + print(f'{Fore.GREEN}✓ Global configuration loaded from config/global.json{Style.RESET_ALL}') + except json.JSONDecodeError as e: + print(f'{Fore.YELLOW}⚠ Warning: Invalid JSON in config/global.json: {e}{Style.RESET_ALL}') + print(f'{Fore.YELLOW} Using default configuration{Style.RESET_ALL}') + except Exception as e: + print(f'{Fore.YELLOW}⚠ Warning: Could not load config/global.json: {e}{Style.RESET_ALL}') + else: + print(f'{Fore.YELLOW}⚠ Warning: config/global.json not found{Style.RESET_ALL}') + print(f'{Fore.CYAN} → Create config/global.json with default settings{Style.RESET_ALL}') + + return config + + def load_streamer_config(self, username: str) -> Dict[str, Any]: + """ + Load configuration for a specific streamer. + + Merges global config with streamer-specific overrides. + + Args: + username: Twitch username + + Returns: + dict: Complete configuration for the streamer + """ + # Start with global config + config = self.global_config.copy() + + # Load streamer-specific config + streamer_file = self.streamers_dir / f"{username}.json" + + if streamer_file.exists(): + try: + with open(streamer_file, 'r', encoding='utf-8') as f: + streamer_config = json.load(f) + # Filter out comments and schema references + streamer_config = {k: v for k, v in streamer_config.items() + if not k.startswith('_') and k != '$schema'} + # Merge streamer config (overrides global) + config.update(streamer_config) + print(f'{Fore.GREEN}✓ Loaded config for {username}{Style.RESET_ALL}') + except json.JSONDecodeError as e: + print(f'{Fore.YELLOW}⚠ Warning: Invalid JSON in {streamer_file}: {e}{Style.RESET_ALL}') + except Exception as e: + print(f'{Fore.YELLOW}⚠ Warning: Could not load {streamer_file}: {e}{Style.RESET_ALL}') + else: + # Create default config for new streamer + print(f'{Fore.CYAN}→ Creating default config for new streamer: {username}{Style.RESET_ALL}') + self.create_default_streamer_config(username) + config['username'] = username + config['enabled'] = True + + # Ensure username is set + config['username'] = username + + return config + + def create_default_streamer_config(self, username: str) -> None: + """ + Create a default configuration file for a new streamer. + + Args: + username: Twitch username + """ + # Ensure streamers directory exists + self.streamers_dir.mkdir(parents=True, exist_ok=True) + + streamer_file = self.streamers_dir / f"{username}.json" + + default_config = { + "$schema": "./streamer.schema.json", + "username": username, + "enabled": True + } + + try: + with open(streamer_file, 'w', encoding='utf-8') as f: + json.dump(default_config, f, indent=2) + print(f'{Fore.GREEN}✓ Created config file: config/streamers/{username}.json{Style.RESET_ALL}') + print(f'{Fore.CYAN} → Edit the file to add custom settings or overrides{Style.RESET_ALL}') + except Exception as e: + print(f'{Fore.RED}✗ Could not create config file for {username}: {e}{Style.RESET_ALL}') + + def get_all_enabled_streamers(self) -> list: + """ + Get list of all enabled streamers. + + Returns: + list: List of usernames configured and enabled + """ + if not self.streamers_dir.exists(): + return [] + + enabled_streamers = [] + + for config_file in self.streamers_dir.glob("*.json"): + try: + with open(config_file, 'r', encoding='utf-8') as f: + config = json.load(f) + # Filter comments and schema references + config = {k: v for k, v in config.items() + if not k.startswith('_') and k != '$schema'} + + if config.get('enabled', False): + username = config.get('username') or config_file.stem + enabled_streamers.append(username) + except Exception as e: + print(f'{Fore.YELLOW}⚠ Warning: Could not read {config_file}: {e}{Style.RESET_ALL}') + + return enabled_streamers + + class TwitchArchive: """ Main class for the Twitch Archive system. @@ -96,9 +242,21 @@ class TwitchArchive: VODs, chat logs, and metadata. Can optionally upload to cloud storage. """ - def __init__(self): - """Initialize the TwitchArchive with configuration settings.""" - self.load_config() + def __init__(self, config: Optional[Dict[str, Any]] = None): + """ + Initialize the TwitchArchive with configuration settings. + + Args: + config: Configuration dictionary. If None, loads from legacy config.json + """ + if config is None: + # Legacy mode: load from config.json + self.load_config() + else: + # New mode: use provided config + for key, value in config.items(): + setattr(self, key, value) + self.os = self._detect_operating_system() self.paths_initialized = False self.shutdown_requested = False @@ -1505,6 +1663,239 @@ class TwitchArchive: print(f'{Fore.RED}\n✓ Cleanup complete{Style.RESET_ALL}') +# ============================================================================ +# MULTI-STREAMER MANAGER +# ============================================================================ + +class TwitchArchiveManager: + """ + Manages multiple TwitchArchive instances for monitoring multiple streamers. + """ + + def __init__(self, specific_streamer: Optional[str] = None): + """ + Initialize the manager. + + Args: + specific_streamer: If provided, only monitor this streamer (ignore enabled status) + """ + self.config_manager = ConfigManager() + self.specific_streamer = specific_streamer + self.archivers: Dict[str, TwitchArchive] = {} + self.shutdown_requested = False + + # Setup signal handlers + signal.signal(signal.SIGTERM, self._signal_handler) + signal.signal(signal.SIGINT, self._signal_handler) + + def _signal_handler(self, signum, frame): + """Handle shutdown signals gracefully.""" + print(f'\n{Fore.YELLOW}⚠ Shutdown signal received...{Style.RESET_ALL}') + self.shutdown_requested = True + + # Signal all archivers to shut down + for archiver in self.archivers.values(): + archiver.shutdown_requested = True + + def _get_streamers_to_monitor(self) -> list: + """ + Get list of streamers to monitor. + + Returns: + list: List of streamer usernames to monitor + """ + if self.specific_streamer: + # Monitor only the specified streamer + return [self.specific_streamer] + else: + # Monitor all enabled streamers + return self.config_manager.get_all_enabled_streamers() + + def _initialize_archiver(self, username: str) -> TwitchArchive: + """ + Initialize a TwitchArchive instance for a streamer. + + Args: + username: Twitch username + + Returns: + TwitchArchive: Initialized archiver instance + """ + config = self.config_manager.load_streamer_config(username) + archiver = TwitchArchive(config) + return archiver + + def run(self) -> None: + """ + Main entry point for multi-streamer monitoring. + + Monitors all enabled streamers (or a specific one if provided). + """ + print(f'\n{Fore.CYAN}{"=" * 70}{Style.RESET_ALL}') + print(f'{Fore.CYAN}TWITCH ARCHIVE - Multi-Streamer Mode{Style.RESET_ALL}') + print(f'{Fore.CYAN}{"=" * 70}{Style.RESET_ALL}\n') + + # Get streamers to monitor + streamers = self._get_streamers_to_monitor() + + if not streamers: + print(f'{Fore.RED}✗ No streamers configured or enabled{Style.RESET_ALL}') + print(f'{Fore.CYAN}→ Create config files in config/streamers/{Style.RESET_ALL}') + print(f'{Fore.CYAN}→ Or run with -u to create a new config{Style.RESET_ALL}') + sys.exit(1) + + print(f'{Fore.GREEN}Monitoring {len(streamers)} streamer(s):{Style.RESET_ALL}') + for streamer in streamers: + print(f' • {Fore.CYAN}{streamer}{Style.RESET_ALL}') + print() + + # Initialize archivers for all streamers + for username in streamers: + try: + archiver = self._initialize_archiver(username) + + # Load environment and validate + archiver._load_environment_variables() + archiver._validate_username() + archiver._initialize_paths() + + self.archivers[username] = archiver + print(f'{Fore.GREEN}✓ Initialized {username}{Style.RESET_ALL}') + except Exception as e: + print(f'{Fore.RED}✗ Failed to initialize {username}: {e}{Style.RESET_ALL}') + + if not self.archivers: + print(f'{Fore.RED}✗ No archivers could be initialized{Style.RESET_ALL}') + sys.exit(1) + + # Verify dependencies once (shared across all streamers) + print(f'\n{Fore.CYAN}Verifying dependencies...{Style.RESET_ALL}') + first_archiver = next(iter(self.archivers.values())) + first_archiver._verify_dependencies() + + # Print configuration summary for each streamer + for username, archiver in self.archivers.items(): + archiver._print_configuration_summary() + + print(f'\n{Fore.GREEN}🚀 Starting monitoring loop...{Style.RESET_ALL}\n') + + # Start monitoring loop + self._monitoring_loop() + + def _monitoring_loop(self) -> None: + """ + Main monitoring loop for all streamers. + + Checks each streamer's status and processes streams as needed. + """ + last_check = {} + + while not self.shutdown_requested: + current_time = time.time() + + for username, archiver in self.archivers.items(): + # Check if enough time has passed since last check for this streamer + if username not in last_check or (current_time - last_check[username]) >= archiver.refresh: + last_check[username] = current_time + + # Check stream status + try: + stream_info = archiver._check_stream_status() + + if stream_info: + # Stream is live + stream_id = stream_info['archiveVideo']['id'] + + if not archiver._is_stream_already_processed(stream_id): + print(f'\n{Fore.GREEN}[{username}] Stream detected!{Style.RESET_ALL}') + print(f'{Fore.CYAN}Title: {stream_info["title"]}{Style.RESET_ALL}') + + # Process the stream + self._process_stream(archiver, stream_info, stream_id) + + # Mark as processed + archiver._mark_stream_as_processed(stream_id) + else: + # Not live - check for new VODs if needed + pass + + except Exception as e: + print(f'{Fore.RED}[{username}] Error checking stream: {e}{Style.RESET_ALL}') + + # Sleep briefly before next iteration + time.sleep(1) + + def _process_stream(self, archiver: TwitchArchive, stream_info: Dict[str, Any], stream_id: str) -> None: + """ + Process a detected stream for a specific archiver. + + Args: + archiver: The TwitchArchive instance + stream_info: Stream information from API + stream_id: Unique stream ID + """ + # Store stream data + archiver.current_stream_data = { + 'stream_id': stream_id, + 'title': stream_info['title'], + 'started_at': stream_info['createdAt'] + } + + # Generate timestamp and filename + timestamp = datetime.now(timezone('UTC')).strftime("%Y%m%d_%Hh%Mm%Ss") + filename_base = f"{PREFIX_LIVE}{archiver.username}_{timestamp}" + + # Define paths + raw_extension = '.ts' + proc_extension = '.mp3' if archiver.quality == 'audio_only' else '.mp4' + + live_raw_path = str(archiver.raw_path / f"{filename_base}{raw_extension}") + live_proc_path = str(archiver.video_path / f"{filename_base}{proc_extension}") + + # Send notification + archiver.send_notification( + f"Stream Started - {archiver.username}", + f"Recording: {stream_info['title']}" + ) + + # Record livestream + recording_successful = archiver._record_livestream(stream_info, live_raw_path) + + if not recording_successful: + return + + # Process raw stream + if archiver.onlyRaw != 1: + archiver._process_raw_stream(live_raw_path, live_proc_path) + + # Clean up raw file if configured + if archiver.cleanRaw == 1 and os.path.exists(live_raw_path): + os.remove(live_raw_path) + + # Save metadata + if archiver.downloadMETADATA == 1: + archiver._save_metadata(stream_info, filename_base) + + # Wait for VOD and download it + if archiver.downloadVOD == 1 and archiver.vodTimeout > 0: + # This would need the full VOD logic from loopcheck + pass + + # Upload to cloud if configured + if archiver.uploadCloud == 1: + archiver._upload_to_cloud(filename_base) + + # Delete files if configured + if archiver.deleteFiles == 1: + archiver._delete_local_files(filename_base, live_raw_path, live_proc_path) + + # Send completion notification + archiver.send_notification( + f"Stream Archived - {archiver.username}", + f"Completed: {stream_info['title']}" + ) + + # ============================================================================ # COMMAND-LINE INTERFACE # ============================================================================ @@ -1518,7 +1909,8 @@ def main(argv: list) -> None: Args: argv: Command-line arguments """ - twitch_archive = TwitchArchive() + specific_streamer = None + use_legacy_mode = False help_msg = f''' {Fore.CYAN}{"=" * 70} @@ -1528,9 +1920,22 @@ TWITCH ARCHIVE - Automated Stream Recording & Archiving {Fore.GREEN}USAGE:{Style.RESET_ALL} python twitch-archive.py [OPTIONS] +{Fore.GREEN}MODES:{Style.RESET_ALL} + • Multi-Streamer Mode (default): + Monitor all enabled streamers from config/streamers/*.json + + • Single-Streamer Mode: + Use -u to monitor only one streamer + + • Legacy Mode: + Uses config.json if it exists (deprecated) + {Fore.GREEN}OPTIONS:{Style.RESET_ALL} -h, --help Display this help information - -u, --username Twitch channel username to monitor + -u, --username Monitor only this Twitch channel + --legacy Force legacy mode (use config.json) + +{Fore.GREEN}LEGACY OPTIONS (when using --legacy):{Style.RESET_ALL} -q, --quality Stream quality: best/source, high/720p, medium/480p, low/360p, audio_only -a, --ttv-lol <0|1> Enable ad-blocking (1) or disable (0) @@ -1542,9 +1947,15 @@ TWITCH ARCHIVE - Automated Stream Recording & Archiving -n, --notifications <0|1> Send email notifications {Fore.YELLOW}TIPS:{Style.RESET_ALL} - • Configure settings in config.json (copy from config.sample.json) + • Create config/global.json for default settings + • Create config/streamers/.json for each streamer + • Set enabled: true/false in each streamer config • Set up API credentials in .env file - • Most users only need to edit config.json, no command-line args needed + +{Fore.CYAN}EXAMPLES:{Style.RESET_ALL} + python twitch-archive.py # Monitor all enabled streamers + python twitch-archive.py -u vinesauce # Monitor only vinesauce + python twitch-archive.py --legacy # Use old config.json mode {Fore.CYAN}{"=" * 70}{Style.RESET_ALL} ''' @@ -1554,38 +1965,62 @@ TWITCH ARCHIVE - Automated Stream Recording & Archiving argv, "h:u:q:a:v:c:m:r:d:n:", ["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=", - "metadata=", "upload=", "delete=", "notifications="] + "metadata=", "upload=", "delete=", "notifications=", "legacy"] ) except getopt.GetoptError as e: print(f'{Fore.RED}Error: {e}{Style.RESET_ALL}\n') print(help_msg) sys.exit(2) + # Check if legacy mode is requested or if config.json exists (fallback) + legacy_config_exists = os.path.exists(os.path.join(os.path.dirname(__file__), 'config.json')) + + # Parse command line args + legacy_overrides = {} for opt, arg in opts: if opt in ('-h', '--help'): print(help_msg) sys.exit(0) elif opt in ("-u", "--username"): - twitch_archive.username = arg + specific_streamer = arg + elif opt == "--legacy": + use_legacy_mode = True + # Legacy options (only used in legacy mode) elif opt in ("-q", "--quality"): - twitch_archive.quality = arg + legacy_overrides['quality'] = arg elif opt in ("-a", "--ttv-lol"): - twitch_archive.streamlink_ttvlol = int(arg) + legacy_overrides['streamlink_ttvlol'] = int(arg) elif opt in ("-v", "--vod"): - twitch_archive.downloadVOD = int(arg) + legacy_overrides['downloadVOD'] = int(arg) elif opt in ("-c", "--chat"): - twitch_archive.downloadCHAT = int(arg) + legacy_overrides['downloadCHAT'] = int(arg) elif opt in ("-m", "--metadata"): - twitch_archive.downloadMETADATA = int(arg) + legacy_overrides['downloadMETADATA'] = int(arg) elif opt in ("-r", "--upload"): - twitch_archive.uploadCloud = int(arg) + legacy_overrides['uploadCloud'] = int(arg) elif opt in ("-d", "--delete"): - twitch_archive.deleteFiles = int(arg) + legacy_overrides['deleteFiles'] = int(arg) elif opt in ("-n", "--notifications"): - twitch_archive.notifications = int(arg) + legacy_overrides['notifications'] = int(arg) - # Start the archive system - twitch_archive.run() + # Determine which mode to use + if use_legacy_mode or (legacy_config_exists and not specific_streamer and not os.path.exists('config/global.json')): + # Legacy mode: single streamer using config.json + print(f'{Fore.YELLOW}⚠ Using legacy mode (config.json){Style.RESET_ALL}') + print(f'{Fore.CYAN}→ Consider migrating to new config structure (config/global.json + config/streamers/*.json){Style.RESET_ALL}\n') + + twitch_archive = TwitchArchive() # Loads from config.json + + # Apply command-line overrides + for key, value in legacy_overrides.items(): + setattr(twitch_archive, key, value) + + # Start the archive system + twitch_archive.run() + else: + # New multi-streamer mode + manager = TwitchArchiveManager(specific_streamer=specific_streamer) + manager.run() if __name__ == "__main__":