Files
StarPilot/starpilot/assets/theme_manager.py
firestar5683 0ffae39d22 boot logo
2026-04-14 12:57:14 -05:00

903 lines
37 KiB
Python

#!/usr/bin/env python3
import glob
import io
import json
import os
import random
import requests
import shutil
import subprocess
import zipfile
from datetime import date, timedelta
from dateutil import easter
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
CANCEL_DOWNLOAD_PARAM = "CancelThemeDownload"
DOWNLOAD_PROGRESS_PARAM = "ThemeDownloadProgress"
HOLIDAY_THEME_PATH = Path(__file__).parent / "holiday_themes"
STOCKOP_THEME_PATH = Path(__file__).parent / "stock_theme"
LOCAL_RESOURCES_PATH = Path(os.getenv("STARPILOT_LOCAL_RESOURCES_PATH", "~/StarPilot-Resources")).expanduser()
HOLIDAY_SLUGS = {
"new_years": "New Year's",
"valentines_day": "Valentine's Day",
"st_patricks_day": "St. Patrick's Day",
"world_frog_day": "World Frog Day",
"april_fools": "April Fools",
"easter_week": "Easter",
"may_the_fourth": "May the Fourth",
"cinco_de_mayo": "Cinco de Mayo",
"stitch_day": "Stitch Day",
"fourth_of_july": "Fourth of July",
"halloween_week": "Halloween",
"thanksgiving_week": "Thanksgiving",
"christmas_week": "Christmas"
}
THEME_COMPONENT_PARAMS = {
"boot_logos": "BootLogoToDownload",
"colors": "ColorToDownload",
"distance_icons": "DistanceIconToDownload",
"icons": "IconToDownload",
"signals": "SignalToDownload",
"sounds": "SoundToDownload",
"steering_wheels": "WheelToDownload"
}
class ThemeManager:
def __init__(self, params, params_memory, boot_run=False):
self.params = params
self.params_memory = params_memory
self.downloading_theme = False
self.theme_updated = False
self.holiday_theme = "stock"
self.previous_asset_mappings = {}
self.theme_sizes_path = THEME_SAVE_PATH / "theme_sizes.json"
# Ensure theme storage layout exists on desktop and device alike.
(THEME_SAVE_PATH / "bootlogos").mkdir(parents=True, exist_ok=True)
(THEME_SAVE_PATH / "theme_packs").mkdir(parents=True, exist_ok=True)
(THEME_SAVE_PATH / "steering_wheels").mkdir(parents=True, exist_ok=True)
self.sync_local_resources()
self.theme_sizes = load_json_file(self.theme_sizes_path)
self.session = requests.Session()
self.session.headers.update({
"Accept": "application/vnd.github.v3+json",
"Accept-Language": "en",
"User-Agent": "starpilot-theme-downloader/1.0 (https://github.com/FrogAi/StarPilot)"
})
if boot_run:
self.copy_default_theme()
@staticmethod
def _local_resources_available():
return LOCAL_RESOURCES_PATH.is_dir() and (LOCAL_RESOURCES_PATH / ".git").exists()
@staticmethod
def _git_list_tree(ref):
result = subprocess.run(
["git", "-C", str(LOCAL_RESOURCES_PATH), "ls-tree", "-r", "--name-only", ref],
capture_output=True,
text=True,
check=True,
)
return [line for line in result.stdout.splitlines() if line]
@staticmethod
def _git_show_bytes(ref, path):
result = subprocess.run(
["git", "-C", str(LOCAL_RESOURCES_PATH), "show", f"{ref}:{path}"],
capture_output=True,
check=True,
)
return result.stdout
@staticmethod
def _directory_has_files(path):
return path.is_dir() and any(path.iterdir())
@staticmethod
def _write_file_if_missing(destination, data):
if destination.exists() and destination.stat().st_size > 0:
return False
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(data)
return True
@staticmethod
def _extract_zip_if_missing(zip_bytes, destination):
if ThemeManager._directory_has_files(destination):
return False
destination.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as archive:
archive.extractall(destination)
return True
def sync_local_resources(self):
if not self._local_resources_available():
return
try:
imported_assets = 0
for path in self._git_list_tree("Steering-Wheels"):
suffix = Path(path).suffix.lower()
if suffix not in {".gif", ".png", ".webp", ".jpg", ".jpeg"}:
continue
destination = THEME_SAVE_PATH / "steering_wheels" / Path(path).name
imported_assets += int(self._write_file_if_missing(destination, self._git_show_bytes("Steering-Wheels", path)))
for path in self._git_list_tree("Themes"):
path_obj = Path(path)
suffix = path_obj.suffix.lower()
if path_obj.parts[:1] == ("bootlogo",) and suffix in {".png", ".jpg", ".jpeg"}:
destination = THEME_SAVE_PATH / "bootlogos" / path_obj.name
imported_assets += int(self._write_file_if_missing(destination, self._git_show_bytes("Themes", path)))
continue
if suffix != ".zip" or len(path_obj.parts) != 2:
continue
theme_name, archive_name = path_obj.parts
component = Path(archive_name).stem.lower()
if component not in {"colors", "distance_icons", "icons", "signals", "sounds"}:
continue
destination = THEME_SAVE_PATH / "theme_packs" / theme_name / component
imported_assets += int(self._extract_zip_if_missing(self._git_show_bytes("Themes", path), destination))
for path in self._git_list_tree("Distance-Icons"):
path_obj = Path(path)
if path_obj.suffix.lower() != ".zip":
continue
destination = THEME_SAVE_PATH / "theme_packs" / path_obj.stem / "distance_icons"
imported_assets += int(self._extract_zip_if_missing(self._git_show_bytes("Distance-Icons", path), destination))
if imported_assets:
print(f"Imported {imported_assets} local theme assets from {LOCAL_RESOURCES_PATH}")
except (FileNotFoundError, subprocess.CalledProcessError, zipfile.BadZipFile, OSError) as error:
print(f"Failed to sync local theme resources from {LOCAL_RESOURCES_PATH}: {error}")
@staticmethod
def calculate_thanksgiving(year):
november_first = date(year, 11, 1)
days_to_thursday = (3 - november_first.weekday()) % 7
first_thursday = november_first + timedelta(days=days_to_thursday)
return first_thursday + timedelta(days=21)
@staticmethod
def copy_default_theme():
world_frog_day_theme_path = HOLIDAY_THEME_PATH / "world_frog_day"
for theme_subfolder_name, save_subfolder_path in [
("colors", "theme_packs/frog/colors"),
("distance_icons", "theme_packs/frog-animated/distance_icons"),
("icons", "theme_packs/frog-animated/icons"),
("signals", "theme_packs/frog/signals"),
("sounds", "theme_packs/frog/sounds"),
]:
source_folder_path = world_frog_day_theme_path / theme_subfolder_name
destination_folder_path = THEME_SAVE_PATH / save_subfolder_path
destination_folder_path.mkdir(parents=True, exist_ok=True)
shutil.copytree(source_folder_path, destination_folder_path, dirs_exist_ok=True)
steering_wheel_image_path = world_frog_day_theme_path / "steering_wheel/wheel.png"
steering_wheel_save_path = THEME_SAVE_PATH / "steering_wheels/frog.png"
steering_wheel_save_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(steering_wheel_image_path, steering_wheel_save_path)
default_boot_logo_path = Path(__file__).parent / "other_images/starpilot_boot_logo.jpg"
boot_logo_save_path = THEME_SAVE_PATH / "bootlogos/starpilot.jpg"
boot_logo_save_path.parent.mkdir(parents=True, exist_ok=True)
if default_boot_logo_path.exists():
should_refresh_default_boot_logo = not boot_logo_save_path.exists()
if not should_refresh_default_boot_logo:
try:
should_refresh_default_boot_logo = default_boot_logo_path.read_bytes() != boot_logo_save_path.read_bytes()
except OSError:
should_refresh_default_boot_logo = True
if should_refresh_default_boot_logo:
shutil.copy2(default_boot_logo_path, boot_logo_save_path)
def download_theme(self, theme_component, theme_name, asset_param, starpilot_toggles):
self.downloading_theme = True
repo_url = get_repository_url(self.session)
if not repo_url:
handle_error(None, asset_param, "Repository unavailable", "GitHub and GitLab are offline...", self.params_memory, DOWNLOAD_PROGRESS_PARAM)
self.downloading_theme = False
return
if theme_component == "boot_logos":
download_link = f"{repo_url}/Themes/bootlogo"
download_path = THEME_SAVE_PATH / "bootlogos" / theme_name
extensions = [".png", ".jpg", ".jpeg"]
name_candidates = list(dict.fromkeys([theme_name, theme_name.replace("_", "-"), theme_name.replace("-", "_")]))
elif theme_component == "distance_icons":
download_link = f"{repo_url}/Distance-Icons/{theme_name}"
download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
extensions = [".zip"]
name_candidates = [theme_name]
elif theme_component == "steering_wheels":
download_link = f"{repo_url}/Steering-Wheels/{theme_name}"
download_path = THEME_SAVE_PATH / theme_component / theme_name
extensions = [".gif", ".png"]
name_candidates = [theme_name]
else:
download_link = f"{repo_url}/Themes/{theme_name}/{theme_component}"
download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
extensions = [".zip"]
name_candidates = [theme_name]
for extension in extensions:
theme_path = download_path.with_suffix(extension)
theme_urls = [f"{download_link}/{candidate}{extension}" for candidate in name_candidates] if theme_component == "boot_logos" else [download_link + extension]
for theme_url in theme_urls:
delete_file(theme_path)
print(f"Downloading theme from GitHub: {theme_name}")
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, asset_param, self.params_memory, DOWNLOAD_PROGRESS_PARAM, self.session, theme_url)
if self.params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
delete_file(theme_path)
handle_error(None, asset_param, "Download cancelled...", "Download cancelled...", self.params_memory, DOWNLOAD_PROGRESS_PARAM)
self.downloading_theme = False
return
if verify_download(theme_path, self.params_memory, self.session, theme_url):
print(f"Theme {theme_name} downloaded and verified successfully from GitHub!")
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
if extension == ".zip":
self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
extract_zip(theme_path, download_path)
self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
self.params_memory.remove(asset_param)
self.downloading_theme = False
self.update_themes(starpilot_toggles)
return
if self.handle_verification_failure(extension, theme_component, theme_name, asset_param, theme_path, download_path, starpilot_toggles):
return
handle_error(download_path, asset_param, "Download failed...", "Download failed...", self.params_memory, DOWNLOAD_PROGRESS_PARAM)
self.downloading_theme = False
def fetch_assets(self, repo_url, starpilot_toggles):
is_github = "github" in repo_url
is_gitlab = "gitlab" in repo_url
repo_encoded = quote_plus(RESOURCES_REPO)
assets = {"boot_logos": [], "themes": {}, "wheels": []}
try:
def list_files(branch):
if is_github:
response = self.session.get(f"https://api.github.com/repos/{RESOURCES_REPO}/git/trees/{branch}?recursive=1", timeout=10)
response.raise_for_status()
return [
{
"path": item.get("path", ""),
"name": Path(item.get("path", "")).name,
"type": item.get("type"),
"size": item.get("size", 0),
}
for item in response.json().get("tree", [])
if item.get("type") == "blob"
]
if is_gitlab:
response = self.session.get(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?ref={branch}&recursive=true", timeout=10)
response.raise_for_status()
return [
{
"path": item.get("path", ""),
"name": item.get("name", ""),
"type": item.get("type"),
"size": 0,
}
for item in response.json()
if item.get("type") in ("blob", "file")
]
print(f"Unsupported repository URL: {repo_url}")
return []
def file_size(branch, path, fallback):
if is_github:
return int(fallback or 0)
response = self.session.head(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/files/{quote_plus(path)}/raw?ref={branch}", timeout=10)
return int(response.headers.get("content-length", 0)) if response.ok else 0
for branch in ["Distance-Icons", "Steering-Wheels"]:
for item in list_files(branch):
if item.get("type") not in ("file", "blob"):
continue
path = item["path"]
size = file_size(branch, path, item.get("size", 0))
if branch == "Steering-Wheels":
assets["wheels"].append(path)
theme_name = Path(path).stem
local_files = list((THEME_SAVE_PATH / "steering_wheels").glob(f"{theme_name}.*"))
if local_files and size > 0:
local_size = self.theme_sizes.get("wheels", {}).get(theme_name)
if local_size != size:
self.download_theme("steering_wheels", theme_name, THEME_COMPONENT_PARAMS["steering_wheels"], starpilot_toggles)
elif branch == "Distance-Icons":
component_name = "distance_icons"
theme_name = Path(path).stem
assets["themes"].setdefault(theme_name, set()).add(component_name)
local_path = THEME_SAVE_PATH / "theme_packs" / theme_name / component_name
if local_path.exists() and size > 0:
local_size = self.theme_sizes.get("themes", {}).get(theme_name, {}).get(component_name)
if local_size != size:
self.download_theme(component_name, theme_name, THEME_COMPONENT_PARAMS[component_name], starpilot_toggles)
branch = "Themes"
for item in list_files(branch):
if item.get("type") not in ("file", "blob") or "/" not in item["path"]:
continue
expected_size = file_size(branch, item["path"], item.get("size", 0))
theme_name, sub_path = item["path"].split("/", 1)
theme_path = sub_path.lower()
if theme_name.lower() == "bootlogo":
if Path(sub_path).suffix.lower() not in (".png", ".jpg", ".jpeg"):
continue
assets["boot_logos"].append(sub_path)
logo_name = Path(sub_path).stem
local_files = list((THEME_SAVE_PATH / "bootlogos").glob(f"{logo_name}.*"))
if local_files and expected_size > 0:
local_size = self.theme_sizes.get("boot_logos", {}).get(logo_name)
if local_size != expected_size:
print(f"boot logo {logo_name} is outdated, redownloading...")
self.download_theme("boot_logos", logo_name, THEME_COMPONENT_PARAMS["boot_logos"], starpilot_toggles)
continue
for key in ("colors", "icons", "signals", "sounds"):
if key in theme_path:
assets["themes"].setdefault(theme_name, set()).add(key)
local_path = THEME_SAVE_PATH / "theme_packs" / theme_name / key
if local_path.exists():
local_size = self.theme_sizes.get("themes", {}).get(theme_name, {}).get(key)
if local_size != expected_size:
print(f"{key} {theme_name} is outdated, redownloading...")
self.download_theme(key, theme_name, THEME_COMPONENT_PARAMS[key], starpilot_toggles)
break
assets["boot_logos"].sort()
assets["themes"] = {key: sorted(list(value)) for key, value in assets["themes"].items()}
assets["wheels"].sort()
return assets
except requests.exceptions.RequestException as error:
print(f"Failed to fetch theme sizes from {'GitHub' if is_github else 'GitLab'}: {error}")
return {}
@staticmethod
def format_name(name, component):
base = Path(name).stem
creator = ""
if "~" in base:
base, creator = base.split("~", 1)
parts = base.replace("_", " ").replace("-", " ").split()
display = " ".join(part.capitalize() for part in parts)
if creator:
return f"{display} - by: {creator}"
return display
@staticmethod
def get_full_themes():
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
if not theme_packs_path.exists():
return []
valid_themes = set()
for theme_directory in theme_packs_path.iterdir():
if not theme_directory.is_dir():
continue
base_name = theme_directory.name.replace("-animated", "")
animated_path = theme_packs_path / f"{base_name}-animated"
base_path = theme_packs_path / base_name
base_valid = all((base_path / asset).is_dir() for asset in {"colors", "sounds"})
animated_icons_exist = (animated_path / "icons").is_dir()
base_icons_exist = (base_path / "icons").is_dir()
if base_valid and (animated_icons_exist or base_icons_exist):
if animated_icons_exist:
valid_themes.add(f"{base_name}-animated")
else:
valid_themes.add(base_name)
return sorted(valid_themes)
@staticmethod
def get_holiday_theme_dates(year):
return {
"new_years": date(year, 1, 1),
"valentines_day": date(year, 2, 14),
"st_patricks_day": date(year, 3, 17),
"world_frog_day": date(year, 3, 20),
"april_fools": date(year, 4, 1),
"easter_week": easter.easter(year),
"may_the_fourth": date(year, 5, 4),
"cinco_de_mayo": date(year, 5, 5),
"stitch_day": date(year, 6, 26),
"fourth_of_july": date(year, 7, 4),
"halloween_week": date(year, 10, 31),
"thanksgiving_week": ThemeManager.calculate_thanksgiving(year),
"christmas_week": date(year, 12, 25)
}
def handle_verification_failure(self, extension, theme_component, theme_name, asset_param, theme_path, download_path, starpilot_toggles):
if theme_component == "boot_logos":
download_link = f"{GITLAB_URL}/Themes/bootlogo"
name_candidates = list(dict.fromkeys([theme_name, theme_name.replace("_", "-"), theme_name.replace("-", "_")]))
elif theme_component == "distance_icons":
download_link = f"{GITLAB_URL}/Distance-Icons/{theme_name}"
name_candidates = [theme_name]
elif theme_component == "steering_wheels":
download_link = f"{GITLAB_URL}/Steering-Wheels/{theme_name}"
name_candidates = [theme_name]
else:
download_link = f"{GITLAB_URL}/Themes/{theme_name}/{theme_component}"
name_candidates = [theme_name]
for candidate in name_candidates:
delete_file(theme_path)
theme_url = f"{download_link}/{candidate}{extension}" if theme_component == "boot_logos" else download_link + extension
print(f"Downloading theme from GitLab: {theme_name}")
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, asset_param, self.params_memory, DOWNLOAD_PROGRESS_PARAM, self.session, theme_url)
if verify_download(theme_path, self.params_memory, self.session, theme_url):
print(f"Theme {theme_name} downloaded and verified successfully from GitLab!")
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
if extension == ".zip":
self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
extract_zip(theme_path, download_path)
self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
self.params_memory.remove(asset_param)
self.downloading_theme = False
self.update_themes(starpilot_toggles)
return True
return False
@staticmethod
def is_within_week_of(target_date, current_date):
start_of_week = target_date - timedelta(days=target_date.weekday())
return start_of_week <= current_date < target_date
@staticmethod
def randomize_distance_icons(available_themes, selected_theme):
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
if not theme_packs_path.exists():
return "stock"
candidates = []
for theme_pack in theme_packs_path.iterdir():
if not theme_pack.is_dir():
continue
distance_icons_dir = theme_pack / "distance_icons"
if not distance_icons_dir.is_dir():
continue
icon_name = theme_pack.name.lower()
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in icon_name]
if theme_association and selected_theme not in icon_name:
continue
weight = 5 if selected_theme in icon_name else 1
candidates.extend([theme_pack.name] * weight)
return random.choice(candidates) if candidates else "stock"
@staticmethod
def randomize_theme_asset(available_themes):
if not available_themes:
return "stock"
return random.choice(available_themes)
@staticmethod
def randomize_wheel_image(available_themes, selected_theme):
steering_wheels_path = THEME_SAVE_PATH / "steering_wheels"
if not steering_wheels_path.exists():
return "stock"
candidates = []
for wheel_file in steering_wheels_path.iterdir():
if not wheel_file.is_file():
continue
name = wheel_file.stem.lower()
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in name]
if theme_association and selected_theme not in name:
continue
weight = 5 if selected_theme in name else 1
candidates.extend([wheel_file.stem] * weight)
return random.choice(candidates) if candidates else "stock"
def update_active_theme(self, time_validated, starpilot_toggles, boot_run=False, randomize_theme=False):
boot_logo = getattr(starpilot_toggles, "boot_logo", "starpilot")
if time_validated and starpilot_toggles.holiday_themes:
self.holiday_theme = self.update_holiday()
else:
self.holiday_theme = "stock"
if self.holiday_theme != "stock":
asset_mappings = {
"boot_logo": ("boot_logo", boot_logo),
"color_scheme": ("colors", self.holiday_theme),
"distance_icons": ("distance_icons", self.holiday_theme),
"icon_pack": ("icons", self.holiday_theme),
"sound_pack": ("sounds", self.holiday_theme),
"turn_signal_pack": ("signals", self.holiday_theme),
"wheel_image": ("wheel_image", self.holiday_theme)
}
elif (boot_run or randomize_theme) and starpilot_toggles.random_themes:
available_themes = self.get_full_themes()
if starpilot_toggles.random_themes_holidays:
available_themes.extend(HOLIDAY_SLUGS.keys())
selected_theme = self.randomize_theme_asset(available_themes)
asset_mappings = {
"boot_logo": ("boot_logo", boot_logo),
"color_scheme": ("colors", selected_theme.replace("-animated", "")),
"distance_icons": ("distance_icons", self.randomize_distance_icons(available_themes, selected_theme.replace("-animated", ""))),
"icon_pack": ("icons", selected_theme),
"sound_pack": ("sounds", selected_theme.replace("-animated", "")),
"turn_signal_pack": ("signals", selected_theme.replace("-animated", "")),
"wheel_image": ("wheel_image", self.randomize_wheel_image(available_themes, selected_theme.replace("-animated", "")))
}
elif not starpilot_toggles.random_themes:
asset_mappings = {
"boot_logo": ("boot_logo", boot_logo),
"color_scheme": ("colors", starpilot_toggles.color_scheme),
"distance_icons": ("distance_icons", starpilot_toggles.distance_icons),
"icon_pack": ("icons", starpilot_toggles.icon_pack),
"sound_pack": ("sounds", starpilot_toggles.sound_pack),
"turn_signal_pack": ("signals", starpilot_toggles.signal_icons),
"wheel_image": ("wheel_image", starpilot_toggles.wheel_image)
}
else:
return
if asset_mappings != self.previous_asset_mappings:
for asset, (asset_type, current_value) in asset_mappings.items():
print(f"Updating {asset}: {asset_type} with value {current_value}")
if asset_type == "boot_logo":
self.update_boot_logo(current_value)
elif asset_type == "wheel_image":
self.update_wheel_image(current_value, boot_run=boot_run)
else:
self.update_theme_asset(asset_type, current_value, boot_run=boot_run)
self.previous_asset_mappings = asset_mappings
self.theme_updated = True
def update_holiday(self):
current_date = date.today()
holidays = self.get_holiday_theme_dates(current_date.year)
for holiday, holiday_date in holidays.items():
if (holiday.endswith("_week") and self.is_within_week_of(holiday_date, current_date)) or (current_date == holiday_date):
return holiday
return "stock"
def update_theme_asset(self, asset_type, theme, boot_run=False):
save_location = ACTIVE_THEME_PATH / asset_type
if self.holiday_theme != "stock":
asset_location = HOLIDAY_THEME_PATH / self.holiday_theme / asset_type
elif theme in HOLIDAY_SLUGS:
asset_location = HOLIDAY_THEME_PATH / theme / asset_type
elif f"{theme}_week" in HOLIDAY_SLUGS:
asset_location = HOLIDAY_THEME_PATH / f"{theme}_week" / asset_type
else:
asset_location = THEME_SAVE_PATH / "theme_packs" / theme / asset_type
if not asset_location.exists() or theme == "stock":
asset_location = STOCKOP_THEME_PATH / asset_type
print(f"Using the stock {asset_type[:-1]} instead")
delete_file(save_location, print_error=not boot_run)
save_location.parent.mkdir(parents=True, exist_ok=True)
save_location.symlink_to(asset_location, target_is_directory=True)
print(f"Linked {save_location} to {asset_location}")
def update_theme_params(self, downloadable_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels):
boot_logos_dir = THEME_SAVE_PATH / "bootlogos"
theme_packs_dir = THEME_SAVE_PATH / "theme_packs"
steering_wheels_dir = THEME_SAVE_PATH / "steering_wheels"
boot_logos_dir.mkdir(parents=True, exist_ok=True)
theme_packs_dir.mkdir(parents=True, exist_ok=True)
steering_wheels_dir.mkdir(parents=True, exist_ok=True)
def update_param(key, assets, subfolder):
if subfolder == "boot_logos":
existing_assets = {item.stem.lower() for item in boot_logos_dir.glob("*") if item.is_file()}
pending_assets = [asset for asset in assets if asset.lower() not in existing_assets]
self.params.put(key, ",".join(sorted(set(pending_assets))))
print(f"{key} updated successfully")
return
if subfolder == "steering_wheels":
themes_path = steering_wheels_dir
existing_assets = {self.format_name(item.name, "steering_wheels") for item in themes_path.glob("*") if item.is_file()}
else:
themes_path = theme_packs_dir
existing_assets = {self.format_name(item.parent.name, subfolder) for item in themes_path.glob(f"*/{subfolder}") if item.is_dir()}
self.params.put(key, ",".join(sorted(set(assets) - existing_assets)))
print(f"{key} updated successfully")
update_param("DownloadableBootLogos", downloadable_boot_logos, "boot_logos")
update_param("DownloadableColors", downloadable_colors, "colors")
update_param("DownloadableDistanceIcons", downloadable_distance_icons, "distance_icons")
update_param("DownloadableIcons", downloadable_icons, "icons")
update_param("DownloadableSignals", downloadable_signals, "signals")
update_param("DownloadableSounds", downloadable_sounds, "sounds")
update_param("DownloadableWheels", downloadable_wheels, "steering_wheels")
downloaded_themes = {}
for theme_dir in theme_packs_dir.iterdir():
components = []
for component in ["colors", "distance_icons", "icons", "signals", "sounds"]:
if (theme_dir / component).is_dir():
components.append(component)
if components:
theme_name = self.format_name(theme_dir.name, "theme_packs")
downloaded_themes[theme_name] = sorted(components)
downloaded_boot_logos = []
for boot_logo_file in boot_logos_dir.iterdir():
if boot_logo_file.is_file():
downloaded_boot_logos.append(self.format_name(boot_logo_file.name, "boot_logos"))
downloaded_wheels = []
for wheel_file in steering_wheels_dir.iterdir():
if wheel_file.is_file():
downloaded_wheels.append(self.format_name(wheel_file.name, "steering_wheels"))
self.params.put("ThemesDownloaded", {
"boot_logos": sorted(downloaded_boot_logos),
"themes": {key: downloaded_themes[key] for key in sorted(downloaded_themes)},
"steering_wheels": sorted(downloaded_wheels)
})
print("ThemesDownloaded updated successfully")
def update_theme_size(self, theme_component, theme_name, file_size):
if theme_component == "boot_logos":
key = "boot_logos"
elif theme_component == "steering_wheels":
key = "wheels"
else:
key = "themes"
if key not in self.theme_sizes:
self.theme_sizes[key] = {}
if key in {"boot_logos", "wheels"}:
self.theme_sizes[key][theme_name] = file_size
else:
if theme_name not in self.theme_sizes[key]:
self.theme_sizes[key][theme_name] = {}
self.theme_sizes[key][theme_name][theme_component] = file_size
update_json_file(self.theme_sizes_path, self.theme_sizes)
def update_themes(self, starpilot_toggles, boot_run=False):
if self.downloading_theme:
return
self.sync_local_resources()
repo_url = get_repository_url(self.session)
if repo_url is None:
print("GitHub and GitLab are offline...")
self.update_theme_params([], [], [], [], [], [], [])
return
assets = self.fetch_assets(repo_url, starpilot_toggles)
if not assets:
return
downloadable_boot_logos = []
downloadable_colors = []
downloadable_distance_icons = []
downloadable_icons = []
downloadable_signals = []
downloadable_sounds = []
for theme, available_assets in assets["themes"].items():
theme_name = self.format_name(theme, "theme_packs")
print(f"Theme found: {theme_name}")
if "colors" in available_assets:
downloadable_colors.append(theme_name)
if "distance_icons" in available_assets:
downloadable_distance_icons.append(theme_name)
if "icons" in available_assets:
downloadable_icons.append(theme_name)
if "signals" in available_assets:
downloadable_signals.append(theme_name)
if "sounds" in available_assets:
downloadable_sounds.append(theme_name)
downloadable_boot_logos = [Path(boot_logo).stem for boot_logo in assets["boot_logos"]]
downloadable_wheels = [self.format_name(wheel, "steering_wheels") for wheel in assets["wheels"]]
print(f"Downloadable Boot Logos: {downloadable_boot_logos}")
print(f"Downloadable Colors: {downloadable_colors}")
print(f"Downloadable Icons: {downloadable_icons}")
print(f"Downloadable Signals: {downloadable_signals}")
print(f"Downloadable Sounds: {downloadable_sounds}")
print(f"Downloadable Distance Icons: {downloadable_distance_icons}")
print(f"Downloadable Wheels: {downloadable_wheels}")
if boot_run:
self.validate_themes(downloadable_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, starpilot_toggles)
self.update_theme_params(downloadable_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels)
@staticmethod
def update_boot_logo(image):
default_boot_logo = Path(__file__).parent / "other_images/starpilot_boot_logo.jpg"
if not default_boot_logo.exists():
return
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}")
return
shutil.copy2(source_file, default_boot_logo)
print(f"Copied {source_file} to {default_boot_logo}")
def update_wheel_image(self, image, boot_run=False, random_event=False):
wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel"
if self.holiday_theme != "stock":
wheel_location = HOLIDAY_THEME_PATH / self.holiday_theme / "steering_wheel"
elif random_event:
wheel_location = RANDOM_EVENTS_PATH / "steering_wheels"
elif image == "stock":
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
elif image in HOLIDAY_SLUGS:
wheel_location = HOLIDAY_THEME_PATH / image / "steering_wheel"
elif f"{image}_week" in HOLIDAY_SLUGS:
wheel_location = HOLIDAY_THEME_PATH / f"{image}_week" / "steering_wheel"
else:
wheel_location = THEME_SAVE_PATH / "steering_wheels"
if not wheel_location.exists():
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
print("Using the stock steering wheel instead")
delete_file(wheel_save_location, print_error=not boot_run)
wheel_save_location.mkdir(parents=True, exist_ok=True)
image_name = image.replace(" ", "_").lower()
matching_files = [images for images in wheel_location.iterdir() if images.stem.lower() in {image_name, "wheel"}]
if not matching_files:
stock_location = STOCKOP_THEME_PATH / "steering_wheel"
matching_files = [images for images in stock_location.iterdir() if images.stem.lower() == "wheel"]
if matching_files:
print(f"Steering wheel '{image}' not found, using the stock steering wheel instead")
if not matching_files:
print(f"No steering wheel asset found for '{image}'")
return
source_file = matching_files[0]
destination_file = wheel_save_location / f"wheel{source_file.suffix}"
destination_file.symlink_to(source_file)
print(f"Linked {destination_file} to {source_file}")
def validate_themes(self, downloadable_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, starpilot_toggles):
downloaded_data = self.params.get("ThemesDownloaded")
if isinstance(downloaded_data, (bytes, bytearray)):
downloaded_data = downloaded_data.decode("utf-8", "ignore")
if isinstance(downloaded_data, str):
try:
downloaded_data = json.loads(downloaded_data)
except json.JSONDecodeError:
downloaded_data = {}
if not isinstance(downloaded_data, dict):
downloaded_data = {}
boot_logos_path = THEME_SAVE_PATH / "bootlogos"
for display_name in downloaded_data.get("boot_logos", []):
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)
for display_name, components in downloaded_data.get("themes", {}).items():
raw_name = display_name.lower().replace(" ", "_").replace("(", "").replace(")", "")
theme_folder_name = raw_name.replace("_animated", "-animated")
for component in components:
component_path = THEME_SAVE_PATH / "theme_packs" / theme_folder_name / component
if not component_path.is_dir() or not any(component_path.iterdir()):
print(f"Missing or empty component '{component}' for theme '{theme_folder_name}'. Downloading...")
self.download_theme(component, theme_folder_name, THEME_COMPONENT_PARAMS.get(component), starpilot_toggles)
self.update_active_theme(True, starpilot_toggles)
wheels_path = THEME_SAVE_PATH / "steering_wheels"
for display_name in downloaded_data.get("steering_wheels", []):
file_stem = display_name.replace(" ", "_").lower()
matching_files = list(wheels_path.glob(f"{file_stem}.*"))
if not matching_files:
print(f"Missing steering wheel '{display_name}'. Downloading...")
self.download_theme("steering_wheels", file_stem, THEME_COMPONENT_PARAMS["steering_wheels"], starpilot_toggles)
self.update_active_theme(True, starpilot_toggles)
protected_dirs = {THEME_SAVE_PATH / "bootlogos", THEME_SAVE_PATH / "theme_packs", THEME_SAVE_PATH / "steering_wheels"}
for dir_path in THEME_SAVE_PATH.glob("**/*"):
if dir_path.is_dir() and not any(dir_path.iterdir()):
if dir_path in protected_dirs:
continue
print(f"Deleting empty folder: {dir_path}")
delete_file(dir_path)
elif dir_path.is_file() and dir_path.name.startswith("tmp"):
print(f"Deleting temp file: {dir_path}")
delete_file(dir_path)
print("Theme validation complete.")