From 3caeae51775b3df8f7dc5eb01025e528e55665e6 Mon Sep 17 00:00:00 2001 From: Dispatcharr Date: Tue, 11 Mar 2025 13:25:38 -0500 Subject: [PATCH] M3U Parse Fix Updated how M3U files are parsed. Added preferred-region to core settings. --- apps/m3u/tasks.py | 74 +++++++++++++++++++++++++++++------------------ fixtures.json | 14 +++++++-- 2 files changed, 57 insertions(+), 31 deletions(-) diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py index 57c298e0..211524d6 100644 --- a/apps/m3u/tasks.py +++ b/apps/m3u/tasks.py @@ -16,6 +16,34 @@ logger = logging.getLogger(__name__) LOCK_EXPIRE = 120 # Lock expires after 120 seconds +def parse_extinf_line(line: str) -> dict: + """ + Parse an EXTINF line from an M3U file. + This function removes the "#EXTINF:" prefix, then splits the remaining + string on the first comma that is not enclosed in quotes. + + Returns a dictionary with: + - 'attributes': a dict of attribute key/value pairs (e.g. tvg-id, tvg-logo, group-title) + - 'display_name': the text after the comma (the fallback display name) + - 'name': the value from tvg-name (if present) or the display name otherwise. + """ + if not line.startswith("#EXTINF:"): + return None + content = line[len("#EXTINF:"):].strip() + # Split on the first comma that is not inside quotes. + parts = re.split(r',(?=(?:[^"]*"[^"]*")*[^"]*$)', content, maxsplit=1) + if len(parts) != 2: + return None + attributes_part, display_name = parts[0], parts[1].strip() + attrs = dict(re.findall(r'(\w+)=["\']([^"\']+)["\']', attributes_part)) + # Use tvg-name attribute if available; otherwise, use the display name. + name = attrs.get('tvg-name', display_name) + return { + 'attributes': attrs, + 'display_name': display_name, + 'name': name + } + def _get_group_title(extinf_line: str) -> str: """Extract group title from EXTINF line.""" match = re.search(r'group-title="([^"]*)"', extinf_line) @@ -117,26 +145,21 @@ def refresh_single_m3u_account(account_id): for line in lines: line = line.strip() if line.startswith('#EXTINF'): - tvg_name_match = re.search(r'tvg-name="([^"]*)"', line) - tvg_logo_match = re.search(r'tvg-logo="([^"]*)"', line) - # Extract tvg-id - tvg_id_match = re.search(r'tvg-id="([^"]*)"', line) - tvg_id = tvg_id_match.group(1) if tvg_id_match else "" - - fallback_name = line.split(",", 1)[-1].strip() if "," in line else "Default Stream" - - name = tvg_name_match.group(1) if tvg_name_match else fallback_name - logo_url = tvg_logo_match.group(1) if tvg_logo_match else "" - group_title = _get_group_title(line) - - logger.debug(f"Parsed EXTINF: name={name}, logo_url={logo_url}, tvg_id={tvg_id}, group_title={group_title}") + extinf = parse_extinf_line(line) + if not extinf: + continue + name = extinf['name'] + tvg_id = extinf['attributes'].get('tvg-id', '') + tvg_logo = extinf['attributes'].get('tvg-logo', '') + # Prefer group-title from attributes if available. + group_title = extinf['attributes'].get('group-title', _get_group_title(line)) + logger.debug(f"Parsed EXTINF: name={name}, logo_url={tvg_logo}, tvg_id={tvg_id}, group_title={group_title}") current_info = { "name": name, - "logo_url": logo_url, + "logo_url": tvg_logo, "group_title": group_title, - "tvg_id": tvg_id, # save the tvg-id here + "tvg_id": tvg_id, } - elif current_info and line.startswith('http'): lower_line = line.lower() if any(lower_line.endswith(ext) for ext in skip_exts): @@ -156,7 +179,6 @@ def refresh_single_m3u_account(account_id): current_info = None continue - # Include tvg_id in the defaults so it gets saved defaults = { "logo_url": current_info["logo_url"], "tvg_id": current_info["tvg_id"] @@ -223,17 +245,13 @@ def parse_m3u_file(file_path, account): for line in lines: line = line.strip() if line.startswith('#EXTINF'): - tvg_name_match = re.search(r'tvg-name="([^"]*)"', line) - tvg_logo_match = re.search(r'tvg-logo="([^"]*)"', line) - fallback_name = line.split(",", 1)[-1].strip() if "," in line else "Stream" - tvg_id_match = re.search(r'tvg-id="([^"]*)"', line) - tvg_id = tvg_id_match.group(1) if tvg_id_match else "" - - name = tvg_name_match.group(1) if tvg_name_match else fallback_name - logo_url = tvg_logo_match.group(1) if tvg_logo_match else "" - - current_info = {"name": name, "logo_url": logo_url, "tvg_id": tvg_id} - + extinf = parse_extinf_line(line) + if not extinf: + continue + name = extinf['name'] + tvg_id = extinf['attributes'].get('tvg-id', '') + tvg_logo = extinf['attributes'].get('tvg-logo', '') + current_info = {"name": name, "logo_url": tvg_logo, "tvg_id": tvg_id} elif current_info and line.startswith('http'): lower_line = line.lower() if any(lower_line.endswith(ext) for ext in skip_exts): diff --git a/fixtures.json b/fixtures.json index c9d735af..85912634 100644 --- a/fixtures.json +++ b/fixtures.json @@ -69,12 +69,20 @@ "value": "1" } }, + { + "model": "core.coresettings", + "fields": { + "key": "preferred-region", + "name": "Preferred Region", + "value": "us" + } + }, { "model": "core.coresettings", "fields": { - "key": "preferred-region", - "name": "Preferred Region", - "value": "us" + "key": "cache-images", + "name": "Cache Images", + "value": "true" } } ]