Refactor configuration values in global.json and global.schema.json to use boolean types for better clarity and consistency; update default values in twitch-archive.py accordingly.
This commit is contained in:
parent
7f8b3d1bf9
commit
efb320eb05
3 changed files with 292 additions and 144 deletions
|
|
@ -4,25 +4,25 @@
|
|||
"root_path": "archive",
|
||||
"rclone_path": "remote:path/to/streams",
|
||||
"refresh": 60.0,
|
||||
"notifications": 0,
|
||||
"downloadMETADATA": 1,
|
||||
"downloadVOD": 1,
|
||||
"downloadCHAT": 1,
|
||||
"downloadLiveCHAT": 1,
|
||||
"notifications": false,
|
||||
"downloadMETADATA": true,
|
||||
"downloadVOD": true,
|
||||
"downloadCHAT": true,
|
||||
"downloadLiveCHAT": true,
|
||||
"vodTimeout": 300,
|
||||
"uploadCloud": 1,
|
||||
"deleteFiles": 0,
|
||||
"onlyRaw": 0,
|
||||
"cleanRaw": 1,
|
||||
"uploadCloud": true,
|
||||
"deleteFiles": false,
|
||||
"onlyRaw": false,
|
||||
"cleanRaw": true,
|
||||
"hls_segments": 3,
|
||||
"hls_segmentsVOD": 10,
|
||||
"streamlink_ttvlol": 0,
|
||||
"streamlink_ttvlol": false,
|
||||
"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
|
||||
"ffmpeg_error_recovery": true,
|
||||
"ffmpeg_faststart": true,
|
||||
"ffmpeg_progress": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,34 +28,29 @@
|
|||
"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"
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Email notifications: false = disabled, true = enabled"
|
||||
},
|
||||
"downloadMETADATA": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1],
|
||||
"default": 1,
|
||||
"description": "Download stream metadata: 0 = disabled, 1 = enabled"
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Download stream metadata: false = disabled, true = enabled"
|
||||
},
|
||||
"downloadVOD": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1],
|
||||
"default": 1,
|
||||
"description": "Download VODs after stream ends: 0 = disabled, 1 = enabled"
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Download VODs after stream ends: false = disabled, true = enabled"
|
||||
},
|
||||
"downloadCHAT": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1],
|
||||
"default": 1,
|
||||
"description": "Download and render chat from VOD: 0 = disabled, 1 = enabled"
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Download and render chat from VOD: false = disabled, true = enabled"
|
||||
},
|
||||
"downloadLiveCHAT": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1],
|
||||
"default": 1,
|
||||
"description": "Download chat during live stream: 0 = disabled, 1 = enabled"
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Download chat during live stream: false = disabled, true = enabled"
|
||||
},
|
||||
"vodTimeout": {
|
||||
"type": "integer",
|
||||
|
|
@ -64,28 +59,24 @@
|
|||
"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"
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Upload to rclone remote: false = disabled, true = enabled"
|
||||
},
|
||||
"deleteFiles": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1],
|
||||
"default": 0,
|
||||
"description": "Delete local files after upload: 0 = disabled, 1 = enabled (BE CAREFUL)"
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Delete local files after upload: false = disabled, true = 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"
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Keep only raw .ts files: false = convert to mp3/mp4, true = keep raw only"
|
||||
},
|
||||
"cleanRaw": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1],
|
||||
"default": 1,
|
||||
"description": "Delete raw .ts files after processing: 0 = keep, 1 = delete"
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Delete raw .ts files after processing: false = keep, true = delete"
|
||||
},
|
||||
"hls_segments": {
|
||||
"type": "integer",
|
||||
|
|
@ -102,9 +93,8 @@
|
|||
"description": "Number of parallel download threads for VODs (1-10)"
|
||||
},
|
||||
"streamlink_ttvlol": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1],
|
||||
"default": 0,
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "DEPRECATED: Ad-blocking with ttvlol (--twitch-proxy-playlist removed in newer streamlink)"
|
||||
},
|
||||
"ffmpeg_hwaccel": {
|
||||
|
|
@ -138,22 +128,19 @@
|
|||
"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"
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable error recovery for corrupted/incomplete streams: false = disabled, true = enabled"
|
||||
},
|
||||
"ffmpeg_faststart": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1],
|
||||
"default": 1,
|
||||
"description": "Enable MP4 faststart flag for better streaming/playback: 0 = disabled, 1 = enabled"
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable MP4 faststart flag for better streaming/playback: false = disabled, true = enabled"
|
||||
},
|
||||
"ffmpeg_progress": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1],
|
||||
"default": 0,
|
||||
"description": "Show FFmpeg encoding progress: 0 = silent, 1 = verbose output"
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Show FFmpeg encoding progress: false = silent, true = verbose output"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,17 +60,17 @@ DEFAULT_CONFIG = {
|
|||
'root_path': 'archive',
|
||||
'rclone_path': 'remote:path/to/streams',
|
||||
'refresh': 60.0,
|
||||
'streamlink_ttvlol': 0,
|
||||
'notifications': 0,
|
||||
'downloadMETADATA': 1,
|
||||
'downloadVOD': 1,
|
||||
'downloadCHAT': 1,
|
||||
'downloadLiveCHAT': 1,
|
||||
'streamlink_ttvlol': False,
|
||||
'notifications': False,
|
||||
'downloadMETADATA': True,
|
||||
'downloadVOD': True,
|
||||
'downloadCHAT': True,
|
||||
'downloadLiveCHAT': True,
|
||||
'vodTimeout': 300,
|
||||
'uploadCloud': 1,
|
||||
'deleteFiles': 0,
|
||||
'onlyRaw': 0,
|
||||
'cleanRaw': 1,
|
||||
'uploadCloud': True,
|
||||
'deleteFiles': False,
|
||||
'onlyRaw': False,
|
||||
'cleanRaw': True,
|
||||
'hls_segments': 3,
|
||||
'hls_segmentsVOD': 10,
|
||||
# FFmpeg 8.0+ Enhancement Options
|
||||
|
|
@ -79,9 +79,9 @@ DEFAULT_CONFIG = {
|
|||
'ffmpeg_audio_codec': 'aac', # Audio codec for audio-only streams
|
||||
'ffmpeg_audio_samplerate': 48000, # Audio sample rate (48000 recommended for broadcasts)
|
||||
'ffmpeg_audio_bitrate': '192k', # Audio bitrate
|
||||
'ffmpeg_error_recovery': 1, # Enable error recovery for corrupted streams (0/1)
|
||||
'ffmpeg_faststart': 1, # Enable faststart for MP4 (better streaming compatibility) (0/1)
|
||||
'ffmpeg_progress': 0 # Show encoding progress (0/1)
|
||||
'ffmpeg_error_recovery': True, # Enable error recovery for corrupted streams
|
||||
'ffmpeg_faststart': True, # Enable faststart for MP4 (better streaming compatibility)
|
||||
'ffmpeg_progress': False # Show encoding progress
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
|
|
@ -390,9 +390,9 @@ class TwitchArchive:
|
|||
self._print_toggle('Cloud upload', self.uploadCloud)
|
||||
|
||||
# Warning messages
|
||||
if self.deleteFiles == 1:
|
||||
if self.deleteFiles:
|
||||
print(f'\n{Fore.RED}⚠ WARNING: Files will be DELETED after processing{Style.RESET_ALL}')
|
||||
if self.uploadCloud == 0:
|
||||
if not self.uploadCloud:
|
||||
print(f'{Fore.RED}⚠ CRITICAL: Files will be deleted WITHOUT cloud backup!{Style.RESET_ALL}')
|
||||
print(f'{Fore.YELLOW} Press CTRL+C to stop and change configuration{Style.RESET_ALL}')
|
||||
else:
|
||||
|
|
@ -400,9 +400,9 @@ class TwitchArchive:
|
|||
|
||||
print(f'\n{Fore.CYAN}{"=" * 60}{Style.RESET_ALL}\n')
|
||||
|
||||
def _print_toggle(self, label: str, value: int) -> None:
|
||||
def _print_toggle(self, label: str, value: bool) -> None:
|
||||
"""Helper method to print a configuration toggle in a consistent format."""
|
||||
status = f'{Fore.GREEN}Enabled{Style.RESET_ALL}' if value == 1 else f'{Fore.RED}Disabled{Style.RESET_ALL}'
|
||||
status = f'{Fore.GREEN}Enabled{Style.RESET_ALL}' if value else f'{Fore.RED}Disabled{Style.RESET_ALL}'
|
||||
print(f'{label}: {status}')
|
||||
|
||||
def run(self) -> None:
|
||||
|
|
@ -528,7 +528,7 @@ class TwitchArchive:
|
|||
print(f'{Fore.YELLOW}⚠ Warning: Could not verify FFmpeg: {e}{Style.RESET_ALL}')
|
||||
|
||||
# Check for TwitchDownloaderCLI (if VOD or Chat download enabled)
|
||||
if self.downloadVOD == 1 or self.downloadCHAT == 1:
|
||||
if self.downloadVOD or self.downloadCHAT:
|
||||
try:
|
||||
downloader_path = self._get_twitch_downloader_executable()
|
||||
if os.path.exists(downloader_path):
|
||||
|
|
@ -634,7 +634,7 @@ class TwitchArchive:
|
|||
subject: Email subject line
|
||||
content: Email body content
|
||||
"""
|
||||
if self.notifications != 1:
|
||||
if not self.notifications:
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
@ -768,7 +768,7 @@ class TwitchArchive:
|
|||
# Add ad-blocking if enabled (Note: twitch-proxy-playlist was removed in newer streamlink versions)
|
||||
# For ad-blocking, you may need to use alternative methods like --twitch-low-latency
|
||||
# or rely on Twitch's own ad-free viewing for subscribers
|
||||
if self.streamlink_ttvlol == 1:
|
||||
if self.streamlink_ttvlol:
|
||||
# The old --twitch-proxy-playlist option has been removed from streamlink
|
||||
# Consider using alternative ad-blocking approaches or updating your method
|
||||
print(f'{Fore.YELLOW}⚠ Warning: ttv-lol proxy option is deprecated in newer streamlink versions{Style.RESET_ALL}')
|
||||
|
|
@ -792,7 +792,8 @@ class TwitchArchive:
|
|||
|
||||
if self.shutdown_requested:
|
||||
print(f'{Fore.YELLOW}✓ Recording stopped by user{Style.RESET_ALL}')
|
||||
return False
|
||||
# Return True so processing continues - we still want to process what was recorded
|
||||
return True
|
||||
|
||||
print(f'{Fore.GREEN}✓ Stream recording complete{Style.RESET_ALL}')
|
||||
return True
|
||||
|
|
@ -813,7 +814,7 @@ class TwitchArchive:
|
|||
print(f'{Fore.YELLOW}⚠ Raw file not found, skipping processing{Style.RESET_ALL}')
|
||||
return
|
||||
|
||||
if self.onlyRaw == 1:
|
||||
if self.onlyRaw:
|
||||
print(f'{Fore.CYAN}Keeping raw .ts file (onlyRaw mode){Style.RESET_ALL}')
|
||||
return
|
||||
|
||||
|
|
@ -837,7 +838,7 @@ class TwitchArchive:
|
|||
cmd.extend(['-threads', str(self.ffmpeg_threads)])
|
||||
|
||||
# Add faststart for better streaming compatibility (MP4/M4A)
|
||||
if self.ffmpeg_faststart == 1 and output_path.endswith(('.mp4', '.m4a')):
|
||||
if self.ffmpeg_faststart and output_path.endswith(('.mp4', '.m4a')):
|
||||
cmd.extend(['-movflags', '+faststart'])
|
||||
|
||||
cmd.append(output_path)
|
||||
|
|
@ -865,7 +866,7 @@ class TwitchArchive:
|
|||
cmd.extend(['-threads', str(self.ffmpeg_threads)])
|
||||
|
||||
# Error recovery options for corrupted streams
|
||||
if self.ffmpeg_error_recovery == 1:
|
||||
if self.ffmpeg_error_recovery:
|
||||
cmd.extend([
|
||||
'-fflags', '+genpts', # Generate missing timestamps
|
||||
'-avoid_negative_ts', 'make_zero', # Handle timestamp issues
|
||||
|
|
@ -881,13 +882,13 @@ class TwitchArchive:
|
|||
])
|
||||
|
||||
# Add faststart for MP4 files
|
||||
if self.ffmpeg_faststart == 1 and output_path.endswith('.mp4'):
|
||||
if self.ffmpeg_faststart and output_path.endswith('.mp4'):
|
||||
cmd.extend(['-movflags', '+faststart'])
|
||||
|
||||
cmd.append(output_path)
|
||||
|
||||
# Run ffmpeg with optional progress output
|
||||
if self.ffmpeg_progress == 1:
|
||||
if self.ffmpeg_progress:
|
||||
subprocess.call(cmd)
|
||||
else:
|
||||
subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
||||
|
|
@ -905,7 +906,7 @@ class TwitchArchive:
|
|||
Returns:
|
||||
bool: True if download succeeded, False otherwise
|
||||
"""
|
||||
if self.downloadVOD != 1:
|
||||
if not self.downloadVOD:
|
||||
return False
|
||||
|
||||
print(f'\n{Fore.CYAN}Downloading VOD: {vod_info["title"]}{Style.RESET_ALL}')
|
||||
|
|
@ -959,7 +960,7 @@ class TwitchArchive:
|
|||
Returns:
|
||||
bool: True if succeeded, False otherwise
|
||||
"""
|
||||
if self.downloadCHAT != 1:
|
||||
if not self.downloadCHAT:
|
||||
return False
|
||||
|
||||
print(f'\n{Fore.CYAN}Downloading chat: {vod_info["title"]}{Style.RESET_ALL}')
|
||||
|
|
@ -1041,7 +1042,7 @@ class TwitchArchive:
|
|||
Returns:
|
||||
subprocess.Popen: The process handle, or None if failed to start
|
||||
"""
|
||||
if self.downloadLiveCHAT != 1:
|
||||
if not self.downloadLiveCHAT:
|
||||
return None
|
||||
|
||||
print(f'\n{Fore.CYAN}Starting live chat download...{Style.RESET_ALL}')
|
||||
|
|
@ -1086,7 +1087,7 @@ class TwitchArchive:
|
|||
Returns:
|
||||
bool: True if succeeded, False otherwise
|
||||
"""
|
||||
if self.downloadCHAT != 1:
|
||||
if not self.downloadCHAT:
|
||||
return False
|
||||
|
||||
print(f'\n{Fore.CYAN}Downloading chat: {vod_info["title"]}{Style.RESET_ALL}')
|
||||
|
|
@ -1250,7 +1251,7 @@ class TwitchArchive:
|
|||
vod_info: VOD metadata from Twitch API
|
||||
filename_base: Base filename (without extension)
|
||||
"""
|
||||
if self.downloadMETADATA != 1:
|
||||
if not self.downloadMETADATA:
|
||||
return
|
||||
|
||||
metadata_path = os.path.join(self.metadata_path, f"{PREFIX_METADATA}{filename_base}.json")
|
||||
|
|
@ -1385,11 +1386,11 @@ class TwitchArchive:
|
|||
live_chat_process = None
|
||||
chat_json_path = os.path.join(self.chatJSON_path, f"{PREFIX_CHAT}{filename_base}.json")
|
||||
|
||||
if self.downloadLiveCHAT == 1 and is_live.get('archiveVideo') and is_live['archiveVideo'].get('id'):
|
||||
if self.downloadLiveCHAT and is_live.get('archiveVideo') and is_live['archiveVideo'].get('id'):
|
||||
live_vod_id = is_live['archiveVideo']['id']
|
||||
print(f'{Fore.CYAN}Live VOD ID detected: {live_vod_id}{Style.RESET_ALL}')
|
||||
live_chat_process = self._download_live_chat(live_vod_id, chat_json_path)
|
||||
elif self.downloadLiveCHAT == 1:
|
||||
elif self.downloadLiveCHAT:
|
||||
print(f'{Fore.YELLOW}⚠ No VOD ID available yet for live chat download{Style.RESET_ALL}')
|
||||
|
||||
# Record the live stream
|
||||
|
|
@ -1482,7 +1483,7 @@ class TwitchArchive:
|
|||
print(f'{Fore.YELLOW}⚠ No matching VOD found for this stream{Style.RESET_ALL}')
|
||||
|
||||
# Clean up raw files if configured
|
||||
if self.cleanRaw == 1 and os.path.exists(live_raw_path):
|
||||
if self.cleanRaw and os.path.exists(live_raw_path):
|
||||
print(f'{Fore.YELLOW}Deleting raw .ts file...{Style.RESET_ALL}')
|
||||
os.remove(live_raw_path)
|
||||
|
||||
|
|
@ -1490,7 +1491,7 @@ class TwitchArchive:
|
|||
upload_success = self._upload_to_cloud(filename_base)
|
||||
|
||||
# Delete local files if configured and upload succeeded
|
||||
if self.deleteFiles == 1 and upload_success:
|
||||
if self.deleteFiles and upload_success:
|
||||
self._delete_local_files(filename_base, live_raw_path, live_proc_path)
|
||||
|
||||
# Done processing this stream
|
||||
|
|
@ -1542,7 +1543,7 @@ class TwitchArchive:
|
|||
Returns:
|
||||
bool: True if upload succeeded or is disabled, False if failed
|
||||
"""
|
||||
if self.uploadCloud != 1:
|
||||
if not self.uploadCloud:
|
||||
return True # Consider upload "successful" if disabled
|
||||
|
||||
print(f'\n{Fore.CYAN}Uploading to cloud storage...{Style.RESET_ALL}')
|
||||
|
|
@ -1618,18 +1619,18 @@ class TwitchArchive:
|
|||
files_to_delete = []
|
||||
|
||||
# Live files
|
||||
if self.cleanRaw == 0 and os.path.exists(live_raw_path):
|
||||
if not self.cleanRaw and os.path.exists(live_raw_path):
|
||||
files_to_delete.append(live_raw_path)
|
||||
if os.path.exists(live_proc_path):
|
||||
files_to_delete.append(live_proc_path)
|
||||
|
||||
# VOD files
|
||||
if self.downloadVOD == 1:
|
||||
if self.downloadVOD:
|
||||
vod_raw = os.path.join(self.raw_path, f"{PREFIX_VOD}{filename_base}.ts")
|
||||
vod_mp4 = os.path.join(self.video_path, f"{PREFIX_VOD}{filename_base}.mp4")
|
||||
vod_mp3 = os.path.join(self.video_path, f"{PREFIX_VOD}{filename_base}.mp3")
|
||||
|
||||
if self.cleanRaw == 0 and os.path.exists(vod_raw):
|
||||
if not self.cleanRaw and os.path.exists(vod_raw):
|
||||
files_to_delete.append(vod_raw)
|
||||
if os.path.exists(vod_mp4):
|
||||
files_to_delete.append(vod_mp4)
|
||||
|
|
@ -1637,7 +1638,7 @@ class TwitchArchive:
|
|||
files_to_delete.append(vod_mp3)
|
||||
|
||||
# Chat files
|
||||
if self.downloadCHAT == 1:
|
||||
if self.downloadCHAT:
|
||||
chat_json = os.path.join(self.chatJSON_path, f"{PREFIX_CHAT}{filename_base}.json")
|
||||
chat_mp4 = os.path.join(self.chatMP4_path, f"{PREFIX_CHAT}{filename_base}.mp4")
|
||||
|
||||
|
|
@ -1647,7 +1648,7 @@ class TwitchArchive:
|
|||
files_to_delete.append(chat_mp4)
|
||||
|
||||
# Metadata files
|
||||
if self.downloadMETADATA == 1:
|
||||
if self.downloadMETADATA:
|
||||
metadata = os.path.join(self.metadata_path, f"{PREFIX_METADATA}{filename_base}.json")
|
||||
if os.path.exists(metadata):
|
||||
files_to_delete.append(metadata)
|
||||
|
|
@ -1672,17 +1673,20 @@ class TwitchArchiveManager:
|
|||
Manages multiple TwitchArchive instances for monitoring multiple streamers.
|
||||
"""
|
||||
|
||||
def __init__(self, specific_streamer: Optional[str] = None):
|
||||
def __init__(self, specific_streamer: Optional[str] = None, verbose: bool = False):
|
||||
"""
|
||||
Initialize the manager.
|
||||
|
||||
Args:
|
||||
specific_streamer: If provided, only monitor this streamer (ignore enabled status)
|
||||
verbose: Enable verbose debug output
|
||||
"""
|
||||
self.config_manager = ConfigManager()
|
||||
self.specific_streamer = specific_streamer
|
||||
self.verbose = verbose
|
||||
self.archivers: Dict[str, TwitchArchive] = {}
|
||||
self.shutdown_requested = False
|
||||
self.active_recordings: Dict[str, str] = {} # Track active recordings: {username: stream_id}
|
||||
|
||||
# Setup signal handlers
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
|
|
@ -1705,7 +1709,7 @@ class TwitchArchiveManager:
|
|||
list: List of streamer usernames to monitor
|
||||
"""
|
||||
if self.specific_streamer:
|
||||
# Monitor only the specified streamer
|
||||
# Monitor only the specified streamer (ignore enabled flag)
|
||||
return [self.specific_streamer]
|
||||
else:
|
||||
# Monitor all enabled streamers
|
||||
|
|
@ -1789,10 +1793,17 @@ class TwitchArchiveManager:
|
|||
Checks each streamer's status and processes streams as needed.
|
||||
"""
|
||||
last_check = {}
|
||||
last_status_print = time.time()
|
||||
|
||||
while not self.shutdown_requested:
|
||||
current_time = time.time()
|
||||
|
||||
# Print periodic status every 60 seconds
|
||||
if current_time - last_status_print >= 60:
|
||||
status_line = " | ".join([f"{username}: checking" for username in self.archivers.keys()])
|
||||
print(f'{Fore.CYAN}[Status] {status_line}{Style.RESET_ALL}')
|
||||
last_status_print = current_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:
|
||||
|
|
@ -1800,27 +1811,68 @@ class TwitchArchiveManager:
|
|||
|
||||
# Check stream status
|
||||
try:
|
||||
stream_info = archiver._check_stream_status()
|
||||
response = archiver._check_stream_status()
|
||||
|
||||
if stream_info:
|
||||
# Stream is live
|
||||
stream_id = stream_info['archiveVideo']['id']
|
||||
# Debug: Print the full response (if verbose)
|
||||
if self.verbose:
|
||||
print(f'\n{Fore.MAGENTA}[DEBUG {username}] API Response: {response}{Style.RESET_ALL}')
|
||||
|
||||
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}')
|
||||
stream_data = response['data']['user']['stream'] if response else None
|
||||
|
||||
# Process the stream
|
||||
self._process_stream(archiver, stream_info, stream_id)
|
||||
if self.verbose:
|
||||
print(f'{Fore.MAGENTA}[DEBUG {username}] Stream data: {stream_data}{Style.RESET_ALL}')
|
||||
|
||||
# Mark as processed
|
||||
archiver._mark_stream_as_processed(stream_id)
|
||||
if stream_data:
|
||||
# Stream is live - check if it has required data
|
||||
if stream_data.get('archiveVideo') and stream_data['archiveVideo'].get('id'):
|
||||
# Create composite stream ID like single-streamer mode
|
||||
# This prevents duplicate recordings in the same session
|
||||
stream_id = f"{stream_data['createdAt']} - {username} - {stream_data.get('title', 'Untitled')}"
|
||||
|
||||
if self.verbose:
|
||||
print(f'{Fore.MAGENTA}[DEBUG {username}] VOD ID: {stream_data["archiveVideo"]["id"]}{Style.RESET_ALL}')
|
||||
print(f'{Fore.MAGENTA}[DEBUG {username}] Composite Stream ID: {stream_id}{Style.RESET_ALL}')
|
||||
|
||||
# Check if we're currently recording this stream
|
||||
currently_recording = username in self.active_recordings and self.active_recordings[username] == stream_id
|
||||
|
||||
if self.verbose:
|
||||
print(f'{Fore.MAGENTA}[DEBUG {username}] Currently recording: {currently_recording}{Style.RESET_ALL}')
|
||||
print(f'{Fore.MAGENTA}[DEBUG {username}] Active recordings: {self.active_recordings}{Style.RESET_ALL}')
|
||||
|
||||
# Record if not currently recording (ignore .log file - always record if live)
|
||||
if not currently_recording:
|
||||
print(f'\n{Fore.GREEN}[{username}] Stream detected!{Style.RESET_ALL}')
|
||||
print(f'{Fore.CYAN}Title: {stream_data.get("title", "No title")}{Style.RESET_ALL}')
|
||||
print(f'{Fore.CYAN}Started at: {stream_data["createdAt"]}{Style.RESET_ALL}')
|
||||
|
||||
# Mark as currently recording
|
||||
self.active_recordings[username] = stream_id
|
||||
|
||||
# Process the stream (this blocks until stream ends)
|
||||
self._process_stream(archiver, stream_data, stream_id)
|
||||
|
||||
# Mark as processed in log (for record keeping)
|
||||
archiver._mark_stream_as_processed(stream_id)
|
||||
|
||||
# Remove from active recordings
|
||||
if username in self.active_recordings:
|
||||
del self.active_recordings[username]
|
||||
else:
|
||||
if self.verbose:
|
||||
print(f'{Fore.CYAN}[{username}] Currently recording this stream, skipping duplicate...{Style.RESET_ALL}')
|
||||
else:
|
||||
# Stream is live but VOD ID not available yet
|
||||
print(f'{Fore.YELLOW}[{username}] Stream is live but VOD ID not ready yet (title: {stream_data.get("title", "No title")}){Style.RESET_ALL}')
|
||||
else:
|
||||
# Not live - check for new VODs if needed
|
||||
pass
|
||||
# Not live
|
||||
if self.verbose:
|
||||
print(f'{Fore.CYAN}[{username}] Offline - checking again in {archiver.refresh}s{Style.RESET_ALL}', end='\r')
|
||||
|
||||
except Exception as e:
|
||||
print(f'{Fore.RED}[{username}] Error checking stream: {e}{Style.RESET_ALL}')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Sleep briefly before next iteration
|
||||
time.sleep(1)
|
||||
|
|
@ -1845,12 +1897,18 @@ class TwitchArchiveManager:
|
|||
timestamp = datetime.now(timezone('UTC')).strftime("%Y%m%d_%Hh%Mm%Ss")
|
||||
filename_base = f"{PREFIX_LIVE}{archiver.username}_{timestamp}"
|
||||
|
||||
# Parse stream start time
|
||||
live_date = datetime.strptime(
|
||||
stream_info["createdAt"], '%Y-%m-%dT%H:%M:%SZ'
|
||||
).replace(tzinfo=timezone('UTC')).astimezone(tz=None).replace(tzinfo=None)
|
||||
|
||||
# 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}")
|
||||
chat_json_path = str(archiver.chatJSON_path / f"{PREFIX_CHAT}{filename_base}.json")
|
||||
|
||||
# Send notification
|
||||
archiver.send_notification(
|
||||
|
|
@ -1858,35 +1916,133 @@ class TwitchArchiveManager:
|
|||
f"Recording: {stream_info['title']}"
|
||||
)
|
||||
|
||||
# Start live chat download if enabled and VOD ID is available
|
||||
live_chat_process = None
|
||||
if archiver.downloadLiveCHAT and stream_info.get('archiveVideo') and stream_info['archiveVideo'].get('id'):
|
||||
live_vod_id = stream_info['archiveVideo']['id']
|
||||
print(f'{Fore.CYAN}Live VOD ID detected: {live_vod_id}{Style.RESET_ALL}')
|
||||
live_chat_process = archiver._download_live_chat(live_vod_id, chat_json_path)
|
||||
elif archiver.downloadLiveCHAT:
|
||||
print(f'{Fore.YELLOW}⚠ No VOD ID available yet for live chat download{Style.RESET_ALL}')
|
||||
|
||||
# Record livestream
|
||||
recording_successful = archiver._record_livestream(stream_info, live_raw_path)
|
||||
|
||||
if not recording_successful:
|
||||
# Check if raw file exists (may exist even after interrupted recording)
|
||||
if not os.path.exists(live_raw_path):
|
||||
print(f'{Fore.RED}✗ No recording file found, skipping processing{Style.RESET_ALL}')
|
||||
return
|
||||
|
||||
# Get file size to check if anything was recorded
|
||||
file_size = os.path.getsize(live_raw_path)
|
||||
if file_size < 1024: # Less than 1KB means essentially nothing was recorded
|
||||
print(f'{Fore.RED}✗ Recording file too small ({file_size} bytes), skipping processing{Style.RESET_ALL}')
|
||||
return
|
||||
|
||||
print(f'{Fore.CYAN}Processing recorded content ({file_size / (1024*1024):.2f} MB)...{Style.RESET_ALL}')
|
||||
|
||||
# Process raw stream
|
||||
if archiver.onlyRaw != 1:
|
||||
if not archiver.onlyRaw:
|
||||
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)
|
||||
# Wait for live chat download if it was started
|
||||
live_chat_downloaded = False
|
||||
if live_chat_process is not None:
|
||||
live_chat_downloaded = archiver._wait_for_chat_download(live_chat_process, chat_json_path)
|
||||
|
||||
# Save metadata
|
||||
if archiver.downloadMETADATA == 1:
|
||||
archiver._save_metadata(stream_info, filename_base)
|
||||
# Render live chat if downloaded successfully
|
||||
if live_chat_downloaded:
|
||||
chat_video_path = str(archiver.chatMP4_path / f"{PREFIX_CHAT}{filename_base}.mp4")
|
||||
archiver._render_chat(chat_json_path, chat_video_path)
|
||||
|
||||
# Wait for VOD and download it
|
||||
if archiver.downloadVOD == 1 and archiver.vodTimeout > 0:
|
||||
# This would need the full VOD logic from loopcheck
|
||||
pass
|
||||
vod_response = None
|
||||
if archiver.vodTimeout == 0:
|
||||
print(f'{Fore.CYAN}VOD check disabled (vodTimeout=0). Skipping VOD download.{Style.RESET_ALL}')
|
||||
elif archiver.shutdown_requested:
|
||||
print(f'{Fore.YELLOW}Skipping VOD download due to shutdown request{Style.RESET_ALL}')
|
||||
else:
|
||||
# Try to match stream with VOD (with timeout)
|
||||
print(f'{Fore.CYAN}Waiting for VOD to become available (timeout: {archiver.vodTimeout}s)...{Style.RESET_ALL}')
|
||||
vod_found = False
|
||||
vod_wait_start = time.time()
|
||||
|
||||
while time.time() - vod_wait_start < archiver.vodTimeout:
|
||||
# Check for shutdown request
|
||||
if archiver.shutdown_requested:
|
||||
print(f'\n{Fore.YELLOW}VOD check interrupted by shutdown{Style.RESET_ALL}')
|
||||
break
|
||||
|
||||
vod_response = archiver._get_latest_vod()
|
||||
|
||||
if vod_response and vod_response['data']['user']['videos']['edges']:
|
||||
current_vod = vod_response['data']['user']['videos']['edges'][0]['node']
|
||||
vod_date = datetime.strptime(
|
||||
current_vod["recordedAt"], '%Y-%m-%dT%H:%M:%SZ'
|
||||
).replace(tzinfo=timezone('UTC')).astimezone(tz=None).replace(tzinfo=None)
|
||||
|
||||
# Check if VOD matches the stream (within 1 minute tolerance)
|
||||
time_tolerance = timedelta(minutes=1)
|
||||
if (live_date - time_tolerance) <= vod_date <= (live_date + time_tolerance):
|
||||
vod_found = True
|
||||
break
|
||||
|
||||
# Wait before checking again
|
||||
if not vod_found:
|
||||
print(f'{Fore.CYAN}VOD not found yet, waiting...{Style.RESET_ALL}', end='\r')
|
||||
time.sleep(min(10, archiver.vodTimeout - (time.time() - vod_wait_start)))
|
||||
|
||||
if not vod_found:
|
||||
print(f'\n{Fore.YELLOW}⚠ VOD not found after {archiver.vodTimeout}s - streamer may have VODs disabled{Style.RESET_ALL}')
|
||||
print(f'{Fore.CYAN} → Live recording and chat (if enabled) were saved successfully{Style.RESET_ALL}')
|
||||
vod_response = None
|
||||
|
||||
# Process VOD if found
|
||||
if vod_response and vod_response['data']['user']['videos']['edges']:
|
||||
current_vod = vod_response['data']['user']['videos']['edges'][0]['node']
|
||||
vod_date = datetime.strptime(
|
||||
current_vod["recordedAt"], '%Y-%m-%dT%H:%M:%SZ'
|
||||
).replace(tzinfo=timezone('UTC')).astimezone(tz=None).replace(tzinfo=None)
|
||||
|
||||
# Check if VOD matches the stream (within 1 minute tolerance)
|
||||
time_tolerance = timedelta(minutes=1)
|
||||
if (live_date - time_tolerance) <= vod_date <= (live_date + time_tolerance):
|
||||
print(f'\n{Fore.GREEN}✓ Found matching VOD{Style.RESET_ALL}')
|
||||
|
||||
# Save metadata
|
||||
if archiver.downloadMETADATA:
|
||||
archiver._save_metadata(current_vod, filename_base)
|
||||
|
||||
# Download VOD
|
||||
if archiver.downloadVOD:
|
||||
vod_ext = '.mp3' if archiver.quality == 'audio_only' else '.mp4'
|
||||
vod_path = str(archiver.video_path / f"{PREFIX_VOD}{filename_base}{vod_ext}")
|
||||
archiver._download_vod(current_vod, vod_path)
|
||||
|
||||
# Download and render chat from VOD (if not already done via live chat)
|
||||
if archiver.downloadCHAT and not live_chat_downloaded:
|
||||
chat_video_path = str(archiver.chatMP4_path / f"{PREFIX_CHAT}{filename_base}.mp4")
|
||||
archiver._download_and_render_chat(current_vod, chat_json_path, chat_video_path)
|
||||
elif live_chat_downloaded:
|
||||
print(f'{Fore.CYAN}Chat already downloaded from live stream, skipping VOD chat download{Style.RESET_ALL}')
|
||||
else:
|
||||
print(f'{Fore.YELLOW}⚠ No matching VOD found for this stream{Style.RESET_ALL}')
|
||||
elif archiver.downloadMETADATA:
|
||||
# Save what metadata we have from the live stream
|
||||
archiver._save_metadata(stream_info, filename_base)
|
||||
|
||||
# Clean up raw file if configured
|
||||
if archiver.cleanRaw and os.path.exists(live_raw_path):
|
||||
print(f'{Fore.YELLOW}Deleting raw .ts file...{Style.RESET_ALL}')
|
||||
os.remove(live_raw_path)
|
||||
|
||||
# Upload to cloud if configured
|
||||
if archiver.uploadCloud == 1:
|
||||
archiver._upload_to_cloud(filename_base)
|
||||
upload_success = False
|
||||
if archiver.uploadCloud:
|
||||
upload_success = archiver._upload_to_cloud(filename_base)
|
||||
|
||||
# Delete files if configured
|
||||
if archiver.deleteFiles == 1:
|
||||
if archiver.deleteFiles and upload_success:
|
||||
archiver._delete_local_files(filename_base, live_raw_path, live_proc_path)
|
||||
|
||||
# Send completion notification
|
||||
|
|
@ -1933,6 +2089,7 @@ TWITCH ARCHIVE - Automated Stream Recording & Archiving
|
|||
{Fore.GREEN}OPTIONS:{Style.RESET_ALL}
|
||||
-h, --help Display this help information
|
||||
-u, --username <name> Monitor only this Twitch channel
|
||||
--verbose Enable verbose debug output
|
||||
--legacy Force legacy mode (use config.json)
|
||||
|
||||
{Fore.GREEN}LEGACY OPTIONS (when using --legacy):{Style.RESET_ALL}
|
||||
|
|
@ -1955,6 +2112,7 @@ TWITCH ARCHIVE - Automated Stream Recording & Archiving
|
|||
{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 -u hackerling --verbose # Monitor with debug output
|
||||
python twitch-archive.py --legacy # Use old config.json mode
|
||||
|
||||
{Fore.CYAN}{"=" * 70}{Style.RESET_ALL}
|
||||
|
|
@ -1965,7 +2123,7 @@ 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=", "legacy"]
|
||||
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose"]
|
||||
)
|
||||
except getopt.GetoptError as e:
|
||||
print(f'{Fore.RED}Error: {e}{Style.RESET_ALL}\n')
|
||||
|
|
@ -1977,31 +2135,34 @@ TWITCH ARCHIVE - Automated Stream Recording & Archiving
|
|||
|
||||
# Parse command line args
|
||||
legacy_overrides = {}
|
||||
verbose_mode = False
|
||||
for opt, arg in opts:
|
||||
if opt in ('-h', '--help'):
|
||||
print(help_msg)
|
||||
sys.exit(0)
|
||||
elif opt in ("-u", "--username"):
|
||||
specific_streamer = arg
|
||||
elif opt == "--verbose":
|
||||
verbose_mode = True
|
||||
elif opt == "--legacy":
|
||||
use_legacy_mode = True
|
||||
# Legacy options (only used in legacy mode)
|
||||
elif opt in ("-q", "--quality"):
|
||||
legacy_overrides['quality'] = arg
|
||||
elif opt in ("-a", "--ttv-lol"):
|
||||
legacy_overrides['streamlink_ttvlol'] = int(arg)
|
||||
legacy_overrides['streamlink_ttvlol'] = bool(int(arg))
|
||||
elif opt in ("-v", "--vod"):
|
||||
legacy_overrides['downloadVOD'] = int(arg)
|
||||
legacy_overrides['downloadVOD'] = bool(int(arg))
|
||||
elif opt in ("-c", "--chat"):
|
||||
legacy_overrides['downloadCHAT'] = int(arg)
|
||||
legacy_overrides['downloadCHAT'] = bool(int(arg))
|
||||
elif opt in ("-m", "--metadata"):
|
||||
legacy_overrides['downloadMETADATA'] = int(arg)
|
||||
legacy_overrides['downloadMETADATA'] = bool(int(arg))
|
||||
elif opt in ("-r", "--upload"):
|
||||
legacy_overrides['uploadCloud'] = int(arg)
|
||||
legacy_overrides['uploadCloud'] = bool(int(arg))
|
||||
elif opt in ("-d", "--delete"):
|
||||
legacy_overrides['deleteFiles'] = int(arg)
|
||||
legacy_overrides['deleteFiles'] = bool(int(arg))
|
||||
elif opt in ("-n", "--notifications"):
|
||||
legacy_overrides['notifications'] = int(arg)
|
||||
legacy_overrides['notifications'] = bool(int(arg))
|
||||
|
||||
# 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')):
|
||||
|
|
@ -2019,7 +2180,7 @@ TWITCH ARCHIVE - Automated Stream Recording & Archiving
|
|||
twitch_archive.run()
|
||||
else:
|
||||
# New multi-streamer mode
|
||||
manager = TwitchArchiveManager(specific_streamer=specific_streamer)
|
||||
manager = TwitchArchiveManager(specific_streamer=specific_streamer, verbose=verbose_mode)
|
||||
manager.run()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue