Update scripts/health_checker.py
All checks were successful
Generate M3U Playlist / build (push) Successful in 2m7s
All checks were successful
Generate M3U Playlist / build (push) Successful in 2m7s
This commit is contained in:
parent
e8a792a46b
commit
4ed1b80ac4
1 changed files with 95 additions and 69 deletions
|
@ -1,16 +1,18 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Health Checker - Simple URL health checking for IPTV channels
|
Health Checker - Simple health checking without external dependencies
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
import time
|
|
||||||
|
|
||||||
class HealthChecker:
|
class HealthChecker:
|
||||||
"""Simple health checker for IPTV channel URLs."""
|
"""Simple health checker using only standard library."""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -18,29 +20,52 @@ class HealthChecker:
|
||||||
self.timeout = config.settings.get('health_check_timeout', 5)
|
self.timeout = config.settings.get('health_check_timeout', 5)
|
||||||
self.max_workers = config.settings.get('max_workers', 4)
|
self.max_workers = config.settings.get('max_workers', 4)
|
||||||
|
|
||||||
|
# Set default socket timeout
|
||||||
|
socket.setdefaulttimeout(self.timeout)
|
||||||
|
|
||||||
def check_single_url(self, url: str) -> Dict:
|
def check_single_url(self, url: str) -> Dict:
|
||||||
"""Check a single URL for accessibility."""
|
"""Check a single URL for accessibility using urllib."""
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.head(
|
# Create request with proper headers
|
||||||
|
req = urllib.request.Request(
|
||||||
url,
|
url,
|
||||||
timeout=self.timeout,
|
|
||||||
allow_redirects=True,
|
|
||||||
headers={'User-Agent': 'IPTV-Health-Checker/1.0'}
|
headers={'User-Agent': 'IPTV-Health-Checker/1.0'}
|
||||||
)
|
)
|
||||||
|
|
||||||
response_time = time.time() - start_time
|
# Try to open the URL
|
||||||
|
with urllib.request.urlopen(req, timeout=self.timeout) as response:
|
||||||
|
response_time = time.time() - start_time
|
||||||
|
status_code = response.getcode()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'url': url,
|
||||||
|
'status': 'healthy' if status_code < 400 else 'unhealthy',
|
||||||
|
'status_code': status_code,
|
||||||
|
'response_time': round(response_time, 2),
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
return {
|
return {
|
||||||
'url': url,
|
'url': url,
|
||||||
'status': 'healthy' if response.status_code < 400 else 'unhealthy',
|
'status': 'unhealthy',
|
||||||
'status_code': response.status_code,
|
'status_code': e.code,
|
||||||
'response_time': round(response_time, 2),
|
'response_time': time.time() - start_time,
|
||||||
'error': None
|
'error': f'HTTP {e.code}: {e.reason}'
|
||||||
}
|
}
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
except urllib.error.URLError as e:
|
||||||
|
return {
|
||||||
|
'url': url,
|
||||||
|
'status': 'unreachable',
|
||||||
|
'status_code': None,
|
||||||
|
'response_time': time.time() - start_time,
|
||||||
|
'error': f'URL Error: {e.reason}'
|
||||||
|
}
|
||||||
|
|
||||||
|
except socket.timeout:
|
||||||
return {
|
return {
|
||||||
'url': url,
|
'url': url,
|
||||||
'status': 'timeout',
|
'status': 'timeout',
|
||||||
|
@ -49,15 +74,6 @@ class HealthChecker:
|
||||||
'error': 'Request timeout'
|
'error': 'Request timeout'
|
||||||
}
|
}
|
||||||
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
return {
|
|
||||||
'url': url,
|
|
||||||
'status': 'unreachable',
|
|
||||||
'status_code': None,
|
|
||||||
'response_time': time.time() - start_time,
|
|
||||||
'error': 'Connection error'
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
'url': url,
|
'url': url,
|
||||||
|
@ -90,29 +106,68 @@ class HealthChecker:
|
||||||
"""Perform batch health check on multiple channels."""
|
"""Perform batch health check on multiple channels."""
|
||||||
if not self.config.settings.get('enable_health_check', False):
|
if not self.config.settings.get('enable_health_check', False):
|
||||||
self.logger.info("Health checking is disabled")
|
self.logger.info("Health checking is disabled")
|
||||||
return {'enabled': False, 'results': []}
|
return {
|
||||||
|
'enabled': False,
|
||||||
|
'results': [],
|
||||||
|
'summary': {
|
||||||
|
'total': len(channels),
|
||||||
|
'healthy': 0,
|
||||||
|
'health_percentage': 0,
|
||||||
|
'total_check_time': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.logger.info(f"Starting health check for {len(channels)} channels...")
|
self.logger.info(f"Starting health check for {len(channels)} channels...")
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
# Use ThreadPoolExecutor for concurrent checks
|
# Limit concurrent checks to avoid overwhelming servers
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
max_workers = min(self.max_workers, len(channels))
|
||||||
# Submit all health check tasks
|
|
||||||
future_to_channel = {
|
|
||||||
executor.submit(self.check_channel_health, channel): channel
|
|
||||||
for channel in channels
|
|
||||||
}
|
|
||||||
|
|
||||||
# Collect results as they complete
|
if max_workers > 1:
|
||||||
for future in concurrent.futures.as_completed(future_to_channel):
|
# Use ThreadPoolExecutor for concurrent checks
|
||||||
|
try:
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
# Submit all health check tasks
|
||||||
|
future_to_channel = {
|
||||||
|
executor.submit(self.check_channel_health, channel): channel
|
||||||
|
for channel in channels
|
||||||
|
}
|
||||||
|
|
||||||
|
# Collect results as they complete
|
||||||
|
for future in concurrent.futures.as_completed(future_to_channel, timeout=60):
|
||||||
|
try:
|
||||||
|
result = future.result(timeout=10)
|
||||||
|
results.append(result)
|
||||||
|
except Exception as e:
|
||||||
|
channel = future_to_channel[future]
|
||||||
|
self.logger.warning(f"Health check failed for {channel.get('Stream name', 'Unknown')}: {e}")
|
||||||
|
results.append({
|
||||||
|
'channel_name': channel.get('Stream name', 'Unknown'),
|
||||||
|
'url': channel.get('Stream URL', ''),
|
||||||
|
'status': 'error',
|
||||||
|
'status_code': None,
|
||||||
|
'response_time': 0,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Concurrent health check failed: {e}")
|
||||||
|
# Fall back to sequential processing
|
||||||
|
for channel in channels:
|
||||||
|
try:
|
||||||
|
result = self.check_channel_health(channel)
|
||||||
|
results.append(result)
|
||||||
|
except Exception as channel_error:
|
||||||
|
self.logger.warning(f"Channel check failed: {channel_error}")
|
||||||
|
else:
|
||||||
|
# Sequential processing for single worker
|
||||||
|
for channel in channels:
|
||||||
try:
|
try:
|
||||||
result = future.result()
|
result = self.check_channel_health(channel)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
channel = future_to_channel[future]
|
self.logger.warning(f"Health check failed for {channel.get('Stream name', 'Unknown')}: {e}")
|
||||||
self.logger.error(f"Health check failed for {channel.get('Stream name', 'Unknown')}: {e}")
|
|
||||||
results.append({
|
results.append({
|
||||||
'channel_name': channel.get('Stream name', 'Unknown'),
|
'channel_name': channel.get('Stream name', 'Unknown'),
|
||||||
'url': channel.get('Stream URL', ''),
|
'url': channel.get('Stream URL', ''),
|
||||||
|
@ -217,32 +272,3 @@ class HealthChecker:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Could not save health report: {e}")
|
self.logger.error(f"Could not save health report: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Simple fallback for when requests is not available
|
|
||||||
class SimpleHealthChecker:
|
|
||||||
"""Fallback health checker that doesn't require external dependencies."""
|
|
||||||
|
|
||||||
def __init__(self, config):
|
|
||||||
self.config = config
|
|
||||||
self.logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
def batch_health_check(self, channels: List[Dict]) -> Dict:
|
|
||||||
"""Fallback that skips health checking."""
|
|
||||||
self.logger.info("Health checking disabled (requests library not available)")
|
|
||||||
return {
|
|
||||||
'enabled': False,
|
|
||||||
'results': [],
|
|
||||||
'summary': {'total': len(channels), 'healthy': 0, 'health_percentage': 0},
|
|
||||||
'total_time': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Try to use the full health checker, fall back to simple one if requests isn't available
|
|
||||||
try:
|
|
||||||
import requests
|
|
||||||
# If requests is available, use the full HealthChecker
|
|
||||||
except ImportError:
|
|
||||||
# If requests is not available, use the fallback
|
|
||||||
class HealthChecker(SimpleHealthChecker):
|
|
||||||
pass
|
|
Loading…
Add table
Add a link
Reference in a new issue