Update station_parser.py
This commit is contained in:
@@ -1,10 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
|
||||||
Radio Station Playlist Parser.
|
|
||||||
|
|
||||||
Supports parsing of M3U, M3U8, and PLS playlist formats to resolving
|
|
||||||
actual stream URLs. Handles recursive playlists and HLS stream detection.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -15,9 +9,7 @@ import logging
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class StationParser:
|
class StationParser:
|
||||||
"""Parses playlist files to extract the underlying media stream URL."""
|
|
||||||
|
|
||||||
def __init__(self, timeout: int = 10):
|
def __init__(self, timeout: int = 10):
|
||||||
self.timeout = aiohttp.ClientTimeout(total=timeout)
|
self.timeout = aiohttp.ClientTimeout(total=timeout)
|
||||||
@@ -38,13 +30,11 @@ class StationParser:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def is_playlist_url(self, url: str) -> bool:
|
def is_playlist_url(self, url: str) -> bool:
|
||||||
"""Checks if a URL points to a supported playlist format."""
|
|
||||||
parsed = urlparse(url.lower())
|
parsed = urlparse(url.lower())
|
||||||
path = parsed.path
|
path = parsed.path
|
||||||
return any(path.endswith(ext) for ext in ['.m3u', '.m3u8', '.pls'])
|
return any(path.endswith(ext) for ext in ['.m3u', '.m3u8', '.pls'])
|
||||||
|
|
||||||
def parse_m3u(self, content: str, base_url: str = "") -> List[Dict[str, str]]:
|
def parse_m3u(self, content: str, base_url: str = "") -> List[Dict[str, str]]:
|
||||||
"""Parses M3U/M3U8 content."""
|
|
||||||
streams = []
|
streams = []
|
||||||
lines = content.strip().split('\n')
|
lines = content.strip().split('\n')
|
||||||
|
|
||||||
@@ -69,7 +59,6 @@ class StationParser:
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle M3U8 stream attributes (bandwidth, resolution)
|
|
||||||
if line.startswith('#EXT-X-STREAM-INF:'):
|
if line.startswith('#EXT-X-STREAM-INF:'):
|
||||||
attrs = self._parse_attributes(line[18:])
|
attrs = self._parse_attributes(line[18:])
|
||||||
current_info = {
|
current_info = {
|
||||||
@@ -95,7 +84,6 @@ class StationParser:
|
|||||||
return streams
|
return streams
|
||||||
|
|
||||||
def parse_pls(self, content: str) -> List[Dict[str, str]]:
|
def parse_pls(self, content: str) -> List[Dict[str, str]]:
|
||||||
"""Parses PLS INI-style content."""
|
|
||||||
streams = []
|
streams = []
|
||||||
entries = {}
|
entries = {}
|
||||||
|
|
||||||
@@ -124,7 +112,6 @@ class StationParser:
|
|||||||
return streams
|
return streams
|
||||||
|
|
||||||
def _parse_attributes(self, attr_string: str) -> Dict[str, str]:
|
def _parse_attributes(self, attr_string: str) -> Dict[str, str]:
|
||||||
"""Helper to parse key="value" attributes in M3U8 tags."""
|
|
||||||
attrs = {}
|
attrs = {}
|
||||||
pattern = r'([A-Z-]+)=(?:"([^"]+)"|([^,]+))'
|
pattern = r'([A-Z-]+)=(?:"([^"]+)"|([^,]+))'
|
||||||
for match in re.finditer(pattern, attr_string):
|
for match in re.finditer(pattern, attr_string):
|
||||||
@@ -134,12 +121,6 @@ class StationParser:
|
|||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
async def resolve_stream_url(self, url: str) -> Tuple[Optional[str], Optional[Dict]]:
|
async def resolve_stream_url(self, url: str) -> Tuple[Optional[str], Optional[Dict]]:
|
||||||
"""
|
|
||||||
Recursively resolves a URL until a raw stream is found.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple containing the resolved URL and its metadata.
|
|
||||||
"""
|
|
||||||
if not self.is_playlist_url(url):
|
if not self.is_playlist_url(url):
|
||||||
return url, {'original_url': url}
|
return url, {'original_url': url}
|
||||||
|
|
||||||
@@ -147,7 +128,6 @@ class StationParser:
|
|||||||
if not content:
|
if not content:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# Return the URL immediately if it's an HLS master playlist, as ffmpeg handles these.
|
|
||||||
if '#EXT-X-TARGETDURATION' in content:
|
if '#EXT-X-TARGETDURATION' in content:
|
||||||
logger.info(f"Detected HLS Media Playlist: {url}")
|
logger.info(f"Detected HLS Media Playlist: {url}")
|
||||||
return url, {'original_url': url, 'is_hls': True}
|
return url, {'original_url': url, 'is_hls': True}
|
||||||
@@ -160,7 +140,6 @@ class StationParser:
|
|||||||
if not streams:
|
if not streams:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# Default to the first stream, but prefer higher bandwidth for adaptive streams.
|
|
||||||
best_stream = streams[0]
|
best_stream = streams[0]
|
||||||
|
|
||||||
if url.lower().endswith('.m3u8'):
|
if url.lower().endswith('.m3u8'):
|
||||||
@@ -171,7 +150,6 @@ class StationParser:
|
|||||||
|
|
||||||
stream_url = best_stream['url']
|
stream_url = best_stream['url']
|
||||||
|
|
||||||
# Recurse if the result is another playlist (nested playlists).
|
|
||||||
if self.is_playlist_url(stream_url):
|
if self.is_playlist_url(stream_url):
|
||||||
return await self.resolve_stream_url(stream_url)
|
return await self.resolve_stream_url(stream_url)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user