-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathstream_validator.py
More file actions
165 lines (130 loc) · 5.3 KB
/
stream_validator.py
File metadata and controls
165 lines (130 loc) · 5.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""
Stream validation system for BunBot favorites.
Validates streams and extracts station names using existing streamscrobbler integration.
"""
import logging
import urllib.parse
from typing import Dict, Any
from streamscrobbler import streamscrobbler
logger = logging.getLogger('discord')
class StreamValidator:
"""Validates radio streams and extracts metadata"""
async def validate_stream(self, url: str) -> Dict[str, Any]:
"""
Validate stream and return metadata
Returns:
Dict with keys: 'valid', 'station_name', 'metadata', 'error'
"""
try:
logger.info(f"Validating stream: {url}")
# Use existing streamscrobbler validation
stationinfo = streamscrobbler.get_server_info(url)
if stationinfo['status'] <= 0:
logger.warning(f"Stream validation failed - status: {stationinfo['status']}")
return {
'valid': False,
'error': "Stream is offline or unreachable",
'station_name': None,
'metadata': None
}
station_name = self.extract_station_name(url, stationinfo)
logger.info(f"Stream validation successful - station: {station_name}")
return {
'valid': True,
'station_name': station_name,
'metadata': stationinfo['metadata'],
'error': None
}
except Exception as e:
logger.error(f"Stream validation error for {url}: {e}")
return {
'valid': False,
'error': f"Validation error: {str(e)}",
'station_name': None,
'metadata': None
}
def extract_station_name(self, url: str, stationinfo: Dict) -> str:
"""
Extract station name from stream metadata or URL
Args:
url: Stream URL
stationinfo: Stream information from streamscrobbler
Returns:
Station name string
"""
metadata = stationinfo.get('metadata', {})
if metadata:
# Try various metadata fields for station name
for field in ['station_name', 'icy-name', 'server_name', 'title', 'name']:
if metadata.get(field):
name = str(metadata[field]).strip()
if name and name.lower() != 'unknown':
logger.debug(f"Found station name in metadata field '{field}': {name}")
return name
# Fallback to URL-based name extraction
return self.extract_name_from_url(url)
def extract_name_from_url(self, url: str) -> str:
"""
Extract a reasonable station name from URL
Args:
url: Stream URL
Returns:
Station name extracted from URL
"""
try:
parsed = urllib.parse.urlparse(url)
# Try to get hostname without www
hostname = parsed.hostname or parsed.netloc
if hostname:
hostname = hostname.lower()
if hostname.startswith('www.'):
hostname = hostname[4:]
# Remove common streaming domains and get the main part
if '.' in hostname:
parts = hostname.split('.')
# Use the main domain part (usually the second-to-last part)
if len(parts) >= 2:
main_part = parts[-2] # e.g., 'example' from 'stream.example.com'
# Clean up common streaming prefixes/suffixes
main_part = main_part.replace('stream', '').replace('radio', '').replace('cast', '')
main_part = main_part.strip('-_')
if main_part:
# Capitalize first letter
return main_part.capitalize() + " Radio"
# If hostname extraction fails, use the full hostname
if hostname:
return hostname.capitalize() + " Radio"
except Exception as e:
logger.warning(f"Failed to extract name from URL {url}: {e}")
# Final fallback
return "Unknown Station"
def is_valid_stream_url(self, url: str) -> bool:
"""
Quick check if URL looks like a valid stream URL
Args:
url: URL to check
Returns:
True if URL appears to be a valid stream URL
"""
try:
parsed = urllib.parse.urlparse(url)
# Must have scheme and netloc
if not parsed.scheme or not parsed.netloc:
return False
# Must be http or https
if parsed.scheme.lower() not in ['http', 'https']:
return False
# Should have a port or path (most streams do)
if not parsed.port and not parsed.path:
return False
return True
except Exception:
return False
# Global validator instance
_validator_instance = None
def get_stream_validator() -> StreamValidator:
"""Get global stream validator instance"""
global _validator_instance
if _validator_instance is None:
_validator_instance = StreamValidator()
return _validator_instance