Implement configuration management for multi-streamer support and update .gitignore

This commit is contained in:
MaddoScientisto 2026-02-09 22:20:04 +01:00
commit 7f8b3d1bf9
6 changed files with 655 additions and 77 deletions

10
.gitignore vendored
View file

@ -1,5 +1,13 @@
# User configuration file (contains personal settings) # User configuration files (contains personal settings)
config.json config.json
config/global.json
# Streamer-specific configurations (personal settings)
config/streamers/*.json
# Python cache
__pycache__/
*.pyc
# Environments # Environments
.env .env

View file

@ -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)"
}

View file

@ -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
}

159
config/global.schema.json Normal file
View file

@ -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"
}
}
}

View file

@ -0,0 +1,5 @@
{
"$schema": "./streamer.schema.json",
"username": "streamer_username",
"enabled": false
}

View file

@ -88,6 +88,152 @@ DEFAULT_CONFIG = {
# MAIN CLASS # 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: class TwitchArchive:
""" """
Main class for the Twitch Archive system. Main class for the Twitch Archive system.
@ -96,9 +242,21 @@ class TwitchArchive:
VODs, chat logs, and metadata. Can optionally upload to cloud storage. VODs, chat logs, and metadata. Can optionally upload to cloud storage.
""" """
def __init__(self): def __init__(self, config: Optional[Dict[str, Any]] = None):
"""Initialize the TwitchArchive with configuration settings.""" """
self.load_config() 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.os = self._detect_operating_system()
self.paths_initialized = False self.paths_initialized = False
self.shutdown_requested = False self.shutdown_requested = False
@ -1505,6 +1663,239 @@ class TwitchArchive:
print(f'{Fore.RED}\n✓ Cleanup complete{Style.RESET_ALL}') 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 <username> 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 # COMMAND-LINE INTERFACE
# ============================================================================ # ============================================================================
@ -1518,7 +1909,8 @@ def main(argv: list) -> None:
Args: Args:
argv: Command-line arguments argv: Command-line arguments
""" """
twitch_archive = TwitchArchive() specific_streamer = None
use_legacy_mode = False
help_msg = f''' help_msg = f'''
{Fore.CYAN}{"=" * 70} {Fore.CYAN}{"=" * 70}
@ -1528,9 +1920,22 @@ TWITCH ARCHIVE - Automated Stream Recording & Archiving
{Fore.GREEN}USAGE:{Style.RESET_ALL} {Fore.GREEN}USAGE:{Style.RESET_ALL}
python twitch-archive.py [OPTIONS] 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 <username> to monitor only one streamer
Legacy Mode:
Uses config.json if it exists (deprecated)
{Fore.GREEN}OPTIONS:{Style.RESET_ALL} {Fore.GREEN}OPTIONS:{Style.RESET_ALL}
-h, --help Display this help information -h, --help Display this help information
-u, --username <name> Twitch channel username to monitor -u, --username <name> Monitor only this Twitch channel
--legacy Force legacy mode (use config.json)
{Fore.GREEN}LEGACY OPTIONS (when using --legacy):{Style.RESET_ALL}
-q, --quality <qual> Stream quality: best/source, high/720p, -q, --quality <qual> Stream quality: best/source, high/720p,
medium/480p, low/360p, audio_only medium/480p, low/360p, audio_only
-a, --ttv-lol <0|1> Enable ad-blocking (1) or disable (0) -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 -n, --notifications <0|1> Send email notifications
{Fore.YELLOW}TIPS:{Style.RESET_ALL} {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/<username>.json for each streamer
Set enabled: true/false in each streamer config
Set up API credentials in .env file 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} {Fore.CYAN}{"=" * 70}{Style.RESET_ALL}
''' '''
@ -1554,38 +1965,62 @@ TWITCH ARCHIVE - Automated Stream Recording & Archiving
argv, argv,
"h:u:q:a:v:c:m:r:d:n:", "h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=", ["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications="] "metadata=", "upload=", "delete=", "notifications=", "legacy"]
) )
except getopt.GetoptError as e: except getopt.GetoptError as e:
print(f'{Fore.RED}Error: {e}{Style.RESET_ALL}\n') print(f'{Fore.RED}Error: {e}{Style.RESET_ALL}\n')
print(help_msg) print(help_msg)
sys.exit(2) 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: for opt, arg in opts:
if opt in ('-h', '--help'): if opt in ('-h', '--help'):
print(help_msg) print(help_msg)
sys.exit(0) sys.exit(0)
elif opt in ("-u", "--username"): 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"): elif opt in ("-q", "--quality"):
twitch_archive.quality = arg legacy_overrides['quality'] = arg
elif opt in ("-a", "--ttv-lol"): elif opt in ("-a", "--ttv-lol"):
twitch_archive.streamlink_ttvlol = int(arg) legacy_overrides['streamlink_ttvlol'] = int(arg)
elif opt in ("-v", "--vod"): elif opt in ("-v", "--vod"):
twitch_archive.downloadVOD = int(arg) legacy_overrides['downloadVOD'] = int(arg)
elif opt in ("-c", "--chat"): elif opt in ("-c", "--chat"):
twitch_archive.downloadCHAT = int(arg) legacy_overrides['downloadCHAT'] = int(arg)
elif opt in ("-m", "--metadata"): elif opt in ("-m", "--metadata"):
twitch_archive.downloadMETADATA = int(arg) legacy_overrides['downloadMETADATA'] = int(arg)
elif opt in ("-r", "--upload"): elif opt in ("-r", "--upload"):
twitch_archive.uploadCloud = int(arg) legacy_overrides['uploadCloud'] = int(arg)
elif opt in ("-d", "--delete"): elif opt in ("-d", "--delete"):
twitch_archive.deleteFiles = int(arg) legacy_overrides['deleteFiles'] = int(arg)
elif opt in ("-n", "--notifications"): elif opt in ("-n", "--notifications"):
twitch_archive.notifications = int(arg) legacy_overrides['notifications'] = int(arg)
# Start the archive system # Determine which mode to use
twitch_archive.run() 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__": if __name__ == "__main__":