mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
* fix(install): add --prefer-binary to pip installs to avoid /tmp exhaustion timezonefinder (~54 MB) includes large timezone polygon data files that pip unpacks into /tmp during installation. On Raspberry Pi, the default tmpfs /tmp size (often ~half of RAM) can be too small, causing the install to fail with an out-of-space error. Adding --prefer-binary tells pip to prefer pre-built binary wheels over source distributions. Since timezonefinder and most other packages publish wheels on PyPI (and piwheels.org has ARM wheels), this avoids the large temporary /tmp extraction and speeds up installs generally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(timezone): use America/New_York instead of EST for ESPN API date queries EST is a fixed UTC-5 offset that does not observe daylight saving time, causing the ESPN API date to be off by one hour during EDT (March–November). America/New_York correctly handles DST transitions. The ESPN scoreboard API anchors its schedule calendar to Eastern US time, so this Eastern timezone is intentionally kept for the API date — it is not user-configurable. Game time display is converted separately to the user's configured timezone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(web): add Google Calendar picker widget for dynamic calendar selection Adds a new google-calendar-picker widget and API endpoint that lets users load their available Google Calendars by name and check the ones they want, instead of manually typing calendar IDs. - GET /api/v3/plugins/calendar/list-calendars — calls plugin.get_calendars() and returns all accessible calendars with id, summary, and primary flag - google-calendar-picker.js — new widget: "Load My Calendars" button renders a checklist; selections update a hidden comma-separated input for form submit - plugin_config.html — handles x-widget: google-calendar-picker in array branch - base.html — loads the new widget script Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web): address PR review findings in google-calendar-picker - api_v3.py: replace broad except block with specific exception handling, log full traceback via module logger, normalize/validate get_calendars() output to stable {id,summary,primary} objects, return opaque user-friendly error message instead of leaking str(e) - google-calendar-picker.js: fix button label only updating to "Refresh Calendars" on success (restore original label on error); update summary paragraph via syncHiddenAndSummary() on every checkbox change so UI stays in sync with hidden input; pass summary element through loadCalendars and renderCheckboxes instead of re-querying DOM - plugin_config.html: bound initWidget retry loop with MAX_RETRIES=40 to prevent infinite timers; normalize legacy comma-separated string values to arrays before passing to widget.render so pre-existing config populates correctly - install_dependencies_apt.py: update install_via_pip docstring to document both --break-system-packages and --prefer-binary flags Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web): harden list_calendar_calendars input validation - Remove unused `as e` binding from ValueError/TypeError/KeyError except clause - Replace hasattr(__iter__) with isinstance(list|tuple) so non-sequence returns are rejected before iteration - Validate each calendar entry is a collections.abc.Mapping; skip and warn on malformed items rather than propagating a TypeError - Coerce id/summary to str safely if not already strings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web): skip calendar entries with empty id in list_calendar_calendars After coercing cal_id to str, check it is non-empty before appending to the calendars list so entries with no usable id are never forwarded to the client. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
174 lines
6.6 KiB
Python
174 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Alternative dependency installer that tries apt packages first,
|
|
then falls back to pip with --break-system-packages
|
|
"""
|
|
|
|
import subprocess
|
|
import sys
|
|
import warnings
|
|
from pathlib import Path
|
|
|
|
def install_via_apt(package_name):
|
|
"""Try to install a package via apt."""
|
|
try:
|
|
# Map pip package names to apt package names
|
|
apt_package_map = {
|
|
'flask': 'python3-flask',
|
|
'PIL': 'python3-pil',
|
|
'freetype': 'python3-freetype',
|
|
'psutil': 'python3-psutil',
|
|
'werkzeug': 'python3-werkzeug',
|
|
'numpy': 'python3-numpy',
|
|
'requests': 'python3-requests',
|
|
'python-dateutil': 'python3-dateutil',
|
|
'pytz': 'python3-tz',
|
|
'geopy': 'python3-geopy',
|
|
'unidecode': 'python3-unidecode',
|
|
'websockets': 'python3-websockets',
|
|
'websocket-client': 'python3-websocket-client'
|
|
}
|
|
|
|
apt_package = apt_package_map.get(package_name, f'python3-{package_name}')
|
|
|
|
print(f"Trying to install {apt_package} via apt...")
|
|
subprocess.check_call([
|
|
'sudo', 'apt', 'update'
|
|
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
subprocess.check_call([
|
|
'sudo', 'apt', 'install', '-y', apt_package
|
|
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
print(f"Successfully installed {apt_package} via apt")
|
|
return True
|
|
|
|
except subprocess.CalledProcessError:
|
|
print(f"Failed to install {package_name} via apt, will try pip")
|
|
return False
|
|
|
|
def install_via_pip(package_name):
|
|
"""Install a package via pip with --break-system-packages and --prefer-binary.
|
|
|
|
--break-system-packages allows pip to install into the system Python on
|
|
Debian/Ubuntu-based systems without a virtual environment.
|
|
--prefer-binary prefers pre-built wheels over source distributions to avoid
|
|
exhausting /tmp space during compilation.
|
|
"""
|
|
try:
|
|
print(f"Installing {package_name} via pip...")
|
|
subprocess.check_call([
|
|
sys.executable, '-m', 'pip', 'install', '--break-system-packages', '--prefer-binary', package_name
|
|
])
|
|
print(f"Successfully installed {package_name} via pip")
|
|
return True
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Failed to install {package_name} via pip: {e}")
|
|
return False
|
|
|
|
def check_package_installed(package_name):
|
|
"""Check if a package is already installed."""
|
|
# Suppress deprecation warnings when checking if packages are installed
|
|
# (we're just checking, not using them)
|
|
with warnings.catch_warnings():
|
|
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
|
try:
|
|
__import__(package_name)
|
|
return True
|
|
except ImportError:
|
|
return False
|
|
|
|
def main():
|
|
"""Main installation function."""
|
|
print("Installing dependencies for LED Matrix Web Interface V2...")
|
|
|
|
# List of required packages
|
|
required_packages = [
|
|
'flask',
|
|
'PIL',
|
|
'freetype',
|
|
'psutil',
|
|
'werkzeug',
|
|
'numpy',
|
|
'requests',
|
|
'python-dateutil',
|
|
'pytz',
|
|
'geopy',
|
|
'unidecode',
|
|
'websockets',
|
|
'websocket-client'
|
|
]
|
|
|
|
failed_packages = []
|
|
|
|
for package in required_packages:
|
|
if check_package_installed(package):
|
|
print(f"{package} is already installed")
|
|
continue
|
|
|
|
# Try apt first, then pip
|
|
if not install_via_apt(package):
|
|
if not install_via_pip(package):
|
|
failed_packages.append(package)
|
|
|
|
# Install packages that don't have apt equivalents
|
|
special_packages = [
|
|
'timezonefinder>=6.5.0,<7.0.0',
|
|
'google-auth-oauthlib>=1.2.0,<2.0.0',
|
|
'google-auth-httplib2>=0.2.0,<1.0.0',
|
|
'google-api-python-client>=2.147.0,<3.0.0',
|
|
'spotipy',
|
|
'icalevents',
|
|
'python-socketio>=5.11.0,<6.0.0',
|
|
'python-engineio>=4.9.0,<5.0.0'
|
|
]
|
|
|
|
for package in special_packages:
|
|
if not install_via_pip(package):
|
|
failed_packages.append(package)
|
|
|
|
# Install rgbmatrix module from local source (optional - may already be installed in Step 6)
|
|
# Check if already installed first
|
|
if check_package_installed('rgbmatrix'):
|
|
print("rgbmatrix module already installed, skipping...")
|
|
else:
|
|
print("Installing rgbmatrix module from local source...")
|
|
try:
|
|
# Get project root (parent of scripts directory)
|
|
PROJECT_ROOT = Path(__file__).parent.parent
|
|
rgbmatrix_path = PROJECT_ROOT / 'rpi-rgb-led-matrix-master' / 'bindings' / 'python'
|
|
if rgbmatrix_path.exists():
|
|
# Check if the module has been built (look for setup.py)
|
|
setup_py = rgbmatrix_path / 'setup.py'
|
|
if setup_py.exists():
|
|
# Try installing - use regular install, not editable mode
|
|
# This is optional for web interface and should already be installed in Step 6
|
|
subprocess.check_call([
|
|
sys.executable, '-m', 'pip', 'install', '--break-system-packages', str(rgbmatrix_path)
|
|
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
print("rgbmatrix module installed successfully")
|
|
else:
|
|
print("Warning: rgbmatrix setup.py not found, module may need to be built first")
|
|
print(" This is normal if Step 6 hasn't completed yet.")
|
|
else:
|
|
print("Warning: rgbmatrix source not found (this is normal if Step 6 hasn't run yet)")
|
|
except subprocess.CalledProcessError as e:
|
|
# Don't fail the whole installation - rgbmatrix is optional for web interface
|
|
# and should be installed in Step 6 of first_time_install.sh
|
|
print(f"Warning: Failed to install rgbmatrix module: {e}")
|
|
print(" This is normal if rgbmatrix hasn't been built yet (Step 6).")
|
|
print(" The web interface will work without it.")
|
|
# Don't add to failed_packages since it's optional
|
|
|
|
if failed_packages:
|
|
print(f"\nFailed to install the following packages: {failed_packages}")
|
|
print("You may need to install them manually or check your system configuration.")
|
|
return False
|
|
else:
|
|
print("\nAll dependencies installed successfully!")
|
|
return True
|
|
|
|
if __name__ == '__main__':
|
|
success = main()
|
|
sys.exit(0 if success else 1)
|