diff --git a/starpilot/assets/theme_manager.py b/starpilot/assets/theme_manager.py index 6f2687a1a..efedb8782 100644 --- a/starpilot/assets/theme_manager.py +++ b/starpilot/assets/theme_manager.py @@ -15,6 +15,7 @@ from pathlib import Path from urllib.parse import quote_plus from openpilot.starpilot.common.starpilot_download_utilities import GITLAB_URL, download_file, get_repository_url, handle_error, verify_download +from openpilot.starpilot.common.theme_asset_names import find_matching_theme_asset_file, find_matching_theme_asset_name from openpilot.starpilot.common.starpilot_utilities import delete_file, extract_zip, load_json_file, update_json_file from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH, RANDOM_EVENTS_PATH, RESOURCES_REPO, THEME_SAVE_PATH @@ -798,8 +799,7 @@ class ThemeManager: if not default_boot_logo.exists(): return - image_name = image.replace(" ", "_").lower() - source_file = next((file for file in (THEME_SAVE_PATH / "bootlogos").glob("*") if file.is_file() and file.stem.lower() == image_name), default_boot_logo) + source_file = find_matching_theme_asset_file(THEME_SAVE_PATH / "bootlogos", image) or default_boot_logo if source_file.resolve() == default_boot_logo.resolve(): print(f"Boot logo unchanged: {default_boot_logo}") @@ -862,9 +862,8 @@ class ThemeManager: boot_logos_path = THEME_SAVE_PATH / "bootlogos" for display_name in downloaded_data.get("boot_logos", []): - file_stem = display_name.replace(" ", "_").lower() - matching_files = list(boot_logos_path.glob(f"{file_stem}.*")) - if not matching_files: + if not find_matching_theme_asset_file(boot_logos_path, display_name): + file_stem = find_matching_theme_asset_name(downloadable_boot_logos, display_name) or display_name.replace(" ", "_").lower() print(f"Missing boot logo '{display_name}'. Downloading...") self.download_theme("boot_logos", file_stem, THEME_COMPONENT_PARAMS["boot_logos"], starpilot_toggles) self.update_active_theme(True, starpilot_toggles) diff --git a/starpilot/common/starpilot_functions.py b/starpilot/common/starpilot_functions.py index 16354dd75..538877dd3 100644 --- a/starpilot/common/starpilot_functions.py +++ b/starpilot/common/starpilot_functions.py @@ -19,6 +19,7 @@ from openpilot.system.version import get_build_metadata from openpilot.starpilot.assets.theme_manager import ThemeManager from openpilot.starpilot.common.starpilot_backups import backup_starpilot from openpilot.starpilot.common.maps_catalog import normalize_schedule_value, sanitize_selected_locations_csv +from openpilot.starpilot.common.theme_asset_names import find_matching_theme_asset_file from openpilot.starpilot.common.starpilot_utilities import get_starpilot_api_info, is_FrogsGoMoo, is_url_pingable, run_cmd, use_konik_server from openpilot.starpilot.common.starpilot_variables import ( ERROR_LOGS_PATH, STARPILOT_API, FROGS_GO_MOO_PATH, HD_LOGS_PATH, KONIK_LOGS_PATH, MAPS_PATH, THEME_SAVE_PATH, @@ -167,11 +168,11 @@ def update_boot_logo(starpilot=False, stock=False, selected_logo=None): target_logo = Path(BASEDIR) / "starpilot/assets/other_images/starpilot_boot_logo.jpg" if selected_logo: selected = selected_logo.decode("utf-8", "ignore") if isinstance(selected_logo, (bytes, bytearray)) else str(selected_logo) - selected = selected.strip().lower().replace(" ", "_") - if selected and selected not in {"stock", "default"}: - candidates = list((THEME_SAVE_PATH / "bootlogos").glob(f"{selected}.*")) - if candidates: - target_logo = candidates[0] + selected = selected.strip() + if selected.lower() not in {"", "stock", "default"}: + matched_logo = find_matching_theme_asset_file(THEME_SAVE_PATH / "bootlogos", selected) + if matched_logo is not None: + target_logo = matched_logo elif stock: target_logo = Path(BASEDIR) / "starpilot/assets/other_images/stock_bg.jpg" else: diff --git a/starpilot/common/tests/test_theme_asset_names.py b/starpilot/common/tests/test_theme_asset_names.py new file mode 100644 index 000000000..e86ddaedd --- /dev/null +++ b/starpilot/common/tests/test_theme_asset_names.py @@ -0,0 +1,28 @@ +from openpilot.starpilot.common.theme_asset_names import ( + canonicalize_theme_asset_name, + find_matching_theme_asset_file, + find_matching_theme_asset_name, +) + + +def test_canonicalize_theme_asset_name_normalizes_boot_logo_variants(): + assert canonicalize_theme_asset_name("new-years.jpg") == canonicalize_theme_asset_name("New Years") + assert canonicalize_theme_asset_name("frog's_day.png") == canonicalize_theme_asset_name("Frogs Day") + assert canonicalize_theme_asset_name("foo~creator.jpeg") == canonicalize_theme_asset_name("Foo - by: creator") + + +def test_find_matching_theme_asset_name_handles_display_name_variants(): + candidates = ["new-years", "frog's_day", "foo~creator"] + + assert find_matching_theme_asset_name(candidates, "New Years") == "new-years" + assert find_matching_theme_asset_name(candidates, "Frogs Day") == "frog's_day" + assert find_matching_theme_asset_name(candidates, "Foo - by: creator") == "foo~creator" + + +def test_find_matching_theme_asset_file_handles_non_exact_boot_logo_names(tmp_path): + expected = tmp_path / "new-years.jpg" + expected.write_bytes(b"jpg") + (tmp_path / "frog's_day.png").write_bytes(b"png") + + assert find_matching_theme_asset_file(tmp_path, "New Years") == expected + assert find_matching_theme_asset_file(tmp_path, "Frogs Day").name == "frog's_day.png" diff --git a/starpilot/common/theme_asset_names.py b/starpilot/common/theme_asset_names.py new file mode 100644 index 000000000..4e3d88217 --- /dev/null +++ b/starpilot/common/theme_asset_names.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import re + +from pathlib import Path + + +def _coerce_asset_name(name): + if isinstance(name, (bytes, bytearray)): + name = name.decode("utf-8", "ignore") + return str(name or "").strip() + + +def canonicalize_theme_asset_name(name): + text = Path(_coerce_asset_name(name)).stem.lower() + tokens = [token for token in re.findall(r"[a-z0-9]+", text) if token != "by"] + return "".join(tokens) + + +def find_matching_theme_asset_name(candidates, requested_name): + requested_raw = Path(_coerce_asset_name(requested_name)).stem.lower() + requested_key = canonicalize_theme_asset_name(requested_name) + + for candidate in candidates: + candidate_raw = Path(_coerce_asset_name(candidate)).stem.lower() + if candidate_raw == requested_raw: + return candidate + + for candidate in candidates: + if canonicalize_theme_asset_name(candidate) == requested_key: + return candidate + + return None + + +def find_matching_theme_asset_file(directory, requested_name): + directory = Path(directory) + if not directory.is_dir(): + return None + + candidates = sorted(file for file in directory.iterdir() if file.is_file()) + matched_name = find_matching_theme_asset_name([file.stem for file in candidates], requested_name) + if matched_name is None: + return None + + matched_path = directory / matched_name + if matched_path.is_file(): + return matched_path + + matched_stem = Path(matched_name).stem + for file in candidates: + if file.stem == matched_stem: + return file + + return None