diff --git a/.cursorrules b/.cursorrules index 85d0adb..9425cb3 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,160 +1,51 @@ # LEDMatrix Plugin Development Rules -## Plugin Version Management & Registry Updates +## Monorepo Structure + +All official plugins live in `plugins//` within this repository. +The registry file `plugins.json` is the source of truth for the Plugin Store. + +## Plugin Version Management ### When Making Changes to Any Plugin -**ALWAYS follow this exact sequence** when making changes to plugins: +**ALWAYS follow this exact sequence:** #### 1. Update Plugin Files -- Make your code changes to `manager.py`, `config_schema.json`, etc. -- Test changes if possible - -#### 2. Update `manifest.json` -- **Bump version number** using semantic versioning: - - `MAJOR.MINOR.PATCH` (e.g., 1.2.3) - - **MAJOR**: Breaking changes, incompatible API changes - - **MINOR**: New features, backward-compatible functionality - - **PATCH**: Bug fixes, backward-compatible fixes -- Update BOTH places in manifest.json: +- Make your code changes in `plugins//` +- Files: `manager.py`, `config_schema.json`, etc. + +#### 2. Bump Version in `manifest.json` +- **Bump the `version` field** using semantic versioning (MAJOR.MINOR.PATCH) +- Update the `versions` array — add the NEW version FIRST (most recent at top): ```json - "version": "1.2.3", ← Update this - + "version": "1.2.3", + "versions": [ { - "released": "2025-10-20", - "version": "1.2.3", ← Add new entry here + "released": "2026-02-11", + "version": "1.2.3", "ledmatrix_min": "2.0.0" }, { "released": "2025-10-20", - "version": "1.2.2", ← Keep previous versions + "version": "1.2.2", ... } ] ``` -- **IMPORTANT**: Add the NEW version FIRST in the versions array (most recent at top) - -#### 3. Update `CHANGELOG.md` -- Add new section at the TOP of the file: - ```markdown - ## [1.2.3] - 2025-10-20 - - ### Added - - New feature descriptions - - ### Changed - - Modified behavior descriptions - - ### Fixed - - Bug fix descriptions - - ### Removed - - Deprecated feature removals - ``` - -#### 4. Commit and Tag the Plugin -```bash -cd plugin-repos/ledmatrix- -git add . -git commit -m "Descriptive commit message - v1.2.3" -git tag v1.2.3 -git push origin master -git push origin v1.2.3 -``` - -**CRITICAL**: The tag MUST match the version in manifest.json exactly (with 'v' prefix) - -#### 5. Update the Plugin Registry -```bash -cd ledmatrix-plugins -python update_registry.py -git add plugins.json -git commit -m "Update to v1.2.3 - Brief description" -git push origin main -``` - -**What `update_registry.py` does**: -- Checks GitHub for latest tags from each plugin repo -- Compares with current versions in `plugins.json` -- Automatically adds new versions to the registry -- Updates download URLs and metadata - -#### 6. Verify in Web UI -- Open web interface → Plugins tab -- Click "Refresh Plugins" or reload page -- The new version should appear in the update/install options - ---- - -## Common Pitfalls & How to Avoid Them -### ❌ **Mistake 1: Forgetting to Add Version to versions[] Array** -**Problem**: Tag exists but registry doesn't find it -**Solution**: Always add new version entry to `versions` array in manifest.json +#### 3. Commit +- The **pre-commit hook** automatically runs `update_registry.py` and stages `plugins.json` +- You do NOT need to manually run `update_registry.py` or manually edit `plugins.json` -### ❌ **Mistake 2: Version Mismatch Between Tag and Manifest** -**Problem**: Tag is `v1.2.3` but manifest says `1.2.4` -**Solution**: Ensure `manifest.json` version matches the git tag exactly - -### ❌ **Mistake 3: Not Updating Registry** -**Problem**: Tag is pushed but web UI doesn't show update -**Solution**: Always run `update_registry.py` and push to ledmatrix-plugins - -### ❌ **Mistake 4: Wrong Version Order in versions[] Array** -**Problem**: Newest version added at bottom instead of top -**Solution**: ALWAYS add new versions at the TOP of the versions array - -### ❌ **Mistake 5: Forgetting to Push Tags** -**Problem**: `git push origin master` but not `git push origin v1.2.3` -**Solution**: Always push both the branch AND the tag - -### ❌ **Mistake 6: Skipping CHANGELOG.md** -**Problem**: Users don't know what changed -**Solution**: Always document changes in CHANGELOG.md before releasing - ---- - -## Quick Reference Commands - -### Standard Plugin Update Workflow ```bash -# 1. Make changes to plugin code - -# 2. Update manifest.json version (manually in editor) - -# 3. Update CHANGELOG.md (manually in editor) - -# 4. Commit and tag plugin -cd plugin-repos/ledmatrix- -git add . -git commit -m "Description - v1.2.3" -git tag v1.2.3 -git push origin master -git push origin v1.2.3 - -# 5. Update registry -cd ../../ledmatrix-plugins -python update_registry.py -git add plugins.json -git commit -m "Update to v1.2.3" +git add plugins// +git commit -m "fix(plugin-id): description of change" git push origin main - -# 6. Verify in web UI ``` -### Emergency: Fix a Bad Tag -```bash -# Delete local tag -git tag -d v1.2.3 - -# Delete remote tag -git push origin :refs/tags/v1.2.3 - -# Recreate tag on correct commit -git tag v1.2.3 -git push origin v1.2.3 -``` +**If the pre-commit hook is not installed:** `cp scripts/pre-commit .git/hooks/pre-commit` --- @@ -164,176 +55,62 @@ git push origin v1.2.3 - Breaking changes to config schema (not backward compatible) - Removed features or config options - Complete rewrite or architecture change -- Changes that require user intervention ### When to Bump MINOR (1.x.0) - New features added - New config options (backward compatible) -- Significant enhancements - New display modes or functionality ### When to Bump PATCH (1.2.x) - Bug fixes - Performance improvements -- Logging improvements - Documentation updates -- Minor tweaks and adjustments - ---- - -## Registry Update Automation - -The `update_registry.py` script: -1. Reads `plugins.json` to get current versions -2. Fetches latest releases/tags from each plugin's GitHub repo -3. Compares versions -4. Automatically updates `plugins.json` with new versions -5. Preserves all existing metadata - -**You should run this after EVERY plugin version bump!** +- Minor tweaks --- -## Multiple Plugins Update +## Common Pitfalls -If updating multiple plugins at once: +### Forgetting to Bump Version +**Problem**: Users won't receive the update — the store compares `manifest.json` version against `plugins.json` `latest_version`. +**Solution**: Always bump `version` in `manifest.json` for every change. -```bash -# Update Plugin 1 -cd plugin-repos/ledmatrix-plugin-1 -git add . && git commit -m "Changes - v1.1.0" && git tag v1.1.0 -git push origin master && git push origin v1.1.0 - -# Update Plugin 2 -cd ../ledmatrix-plugin-2 -git add . && git commit -m "Changes - v2.0.0" && git tag v2.0.0 -git push origin master && git push origin v2.0.0 - -# Update registry ONCE for all plugins -cd ../../ledmatrix-plugins -python update_registry.py # Will detect both updates -git add plugins.json -git commit -m "Update plugin-1 to v1.1.0 and plugin-2 to v2.0.0" -git push origin main -``` +### Version Mismatch +**Problem**: `version` field doesn't match top entry in `versions` array. +**Solution**: Keep both in sync. --- -## GitHub Rate Limits +## Plugin Manifest Required Fields -**Problem**: `update_registry.py` is rate limited (60 requests/hour) +Every `plugins//manifest.json` must have: +- `id` — Plugin identifier (must match directory name) +- `name` — Human-readable display name +- `version` — Semver string (e.g., "1.2.3") +- `class_name` — Python class name in manager.py +- `display_modes` — Array of supported display modes -**Solution**: Add GitHub token to `LEDMatrix/config/config_secrets.json`: -```json -{ - "github": { - "api_token": "ghp_your_token_here" - } -} -``` +## Registry Format -Or use command line: -```bash -python update_registry.py --token ghp_your_token_here -``` +`plugins.json` entries for monorepo plugins use: +- `repo`: `https://github.com/ChuckBuilds/ledmatrix-plugins` +- `plugin_path`: `plugins/` +- `branch`: `main` +- `latest_version`: Synced from manifest by `update_registry.py` -**Rate limits**: -- No token: 60 requests/hour -- With token: 5,000 requests/hour +Third-party plugins keep their own `repo` URL and empty `plugin_path`. --- -## Checklist Template +## Checklist -Before pushing a plugin update, verify: +Before pushing a plugin update: - [ ] Code changes completed and tested -- [ ] `manifest.json` version bumped (in 2 places!) +- [ ] `manifest.json` version bumped - [ ] New version added to TOP of `versions` array -- [ ] `CHANGELOG.md` updated with changes -- [ ] Changes committed with descriptive message -- [ ] Git tag created matching manifest version -- [ ] Tag and branch pushed to origin -- [ ] Registry updated with `update_registry.py` -- [ ] `plugins.json` changes committed and pushed -- [ ] Verified update appears in web UI - ---- - -## Example: Complete Update Flow - -```bash -# === STEP 1: Update Plugin Code === -cd plugin-repos/ledmatrix-football-scoreboard -# (Make code changes to manager.py) - -# === STEP 2: Update Version Files === -# Edit manifest.json: -# - Change "version": "1.5.2" to "1.6.0" -# - Add new entry to versions array - -# Edit CHANGELOG.md: -# - Add new section for [1.6.0] - -# === STEP 3: Commit and Tag === -git add . -git commit -m "Add nested config schema support - v1.6.0" -git tag v1.6.0 -git push origin master -git push origin v1.6.0 - -# === STEP 4: Update Registry === -cd ../../ledmatrix-plugins -python update_registry.py -# Should show: "Update available: 1.5.2 → 1.6.0" - -git add plugins.json -git commit -m "Update football-scoreboard to v1.6.0 - Nested config" -git push origin main - -# === STEP 5: Verify === -# Open web UI → Plugins → Should see v1.6.0 available -``` - ---- - -## Troubleshooting - -### "update_registry.py says already up to date but I pushed a new tag" - -**Cause**: Tag was pushed AFTER running update_registry.py - -**Fix**: Run update_registry.py again after pushing tags - -### "Web UI doesn't show my new version" - -**Checks**: -1. Is tag pushed? `git ls-remote --tags origin | grep v1.2.3` -2. Does manifest.json have the version? -3. Is plugins.json updated? `git pull origin main` in ledmatrix-plugins -4. Did you restart web service? `sudo systemctl restart ledmatrix-web` -5. Clear browser cache: Ctrl+Shift+R - -### "Registry shows wrong download URL" - -**Cause**: `download_url_template` in manifest.json is incorrect - -**Fix**: Ensure template matches your GitHub structure: -```json -"download_url_template": "https://github.com/ChuckBuilds/ledmatrix-{plugin-name}/archive/refs/tags/v{version}.zip" -``` - ---- - -## Best Practices - -1. **Test locally first** before releasing -2. **Semantic versioning**: Be consistent with version bumps -3. **Descriptive commits**: Explain what changed and why -4. **CHANGELOG clarity**: Users read this to understand updates -5. **Tag immediately**: Don't commit code without tagging the release -6. **Update registry same day**: Keep plugins.json in sync with tags -7. **Version in commit message**: Makes git history searchable +- [ ] Pre-commit hook installed (`cp scripts/pre-commit .git/hooks/pre-commit`) +- [ ] Committed and pushed --- @@ -350,13 +127,3 @@ But if you update: 1. Test all installed plugins still work 2. Update minimum LEDMatrix version in affected plugin manifests 3. Document breaking changes - ---- - -## Notes - -- The registry (`plugins.json`) is the **source of truth** for the web UI -- GitHub releases/tags are detected automatically by `update_registry.py` -- Users see versions listed in `plugins.json`, not directly from GitHub -- Always update `plugins.json` after pushing new plugin tags - diff --git a/AUTOMATED_REGISTRY_GUIDE.md b/AUTOMATED_REGISTRY_GUIDE.md deleted file mode 100644 index 37ab088..0000000 --- a/AUTOMATED_REGISTRY_GUIDE.md +++ /dev/null @@ -1,146 +0,0 @@ -# Automated Plugin Registry Update Guide - -## Overview - -The plugin registry (`plugins.json`) is now automatically updated every 6 hours using a Python script and GitHub Actions. This eliminates the need to manually update version numbers when you release new plugin versions. - -## How It Works - -### Automated Updates (GitHub Actions) -- **Schedule**: Runs every 6 hours automatically -- **Manual Trigger**: Can be triggered manually from GitHub Actions tab -- **Process**: - 1. Checks each plugin's GitHub repository for releases/tags - 2. Compares with current versions in `plugins.json` - 3. Updates the registry if new versions are found - 4. Automatically commits and pushes changes - -### Manual Updates (Local Development) - -You can also run the update script locally: - -```bash -# Install dependencies -pip install -r requirements.txt - -# Dry run (preview changes without applying) -python update_registry.py --dry-run - -# Update the registry -python update_registry.py - -# Use with GitHub token (avoids rate limits) -python update_registry.py --token YOUR_GITHUB_TOKEN -``` - -## Creating New Plugin Releases - -To publish a new version of a plugin: - -1. **Update the plugin's `manifest.json`** with the new version number -2. **Commit and push your changes** to the plugin repository -3. **Create a Git tag** for the new version: - ```bash - cd plugin-repos/ledmatrix-your-plugin - git tag -a v1.0.1 -m "Version 1.0.1 - Description of changes" - git push origin v1.0.1 - ``` -4. **Wait for the automated update** (runs every 6 hours) or trigger manually - -### Creating GitHub Releases (Optional) - -While tags are sufficient, you can also create formal GitHub releases: - -1. Go to your plugin repository on GitHub -2. Click "Releases" → "Create a new release" -3. Choose your tag (e.g., `v1.0.1`) -4. Add release notes -5. Publish the release - -The automated script will detect both tags and releases. - -## Troubleshooting - -### Rate Limits - -GitHub's API has rate limits: -- **Without token**: 60 requests per hour -- **With token**: 5000 requests per hour - -The automated GitHub Actions workflow uses a token automatically, so it won't hit rate limits. - -For local testing, you can create a GitHub Personal Access Token: -1. Go to GitHub Settings → Developer settings → Personal access tokens -2. Generate a new token with `public_repo` scope -3. Use it with: `python update_registry.py --token YOUR_TOKEN` - -### Version Not Detected - -If the script doesn't detect your new version: - -1. **Verify the tag exists on GitHub**: - ```bash - git ls-remote --tags https://github.com/ChuckBuilds/ledmatrix-your-plugin - ``` - -2. **Check tag format**: Tags should be `v1.0.0` format (with 'v' prefix) - -3. **Wait a few minutes**: GitHub's API might take a moment to reflect new tags - -4. **Run manually with token**: `python update_registry.py --token YOUR_TOKEN` - -### Script Not Finding Updates - -If you know there's a new version but the script doesn't find it: - -1. Check that the repository URL in `plugins.json` is correct -2. Ensure the tag/release is published (not draft) -3. Verify the tag follows semantic versioning (e.g., `v1.0.0`) - -## Files - -- **`update_registry.py`** - Python script that checks GitHub and updates registry -- **`.github/workflows/update-registry.yml`** - GitHub Actions workflow -- **`requirements.txt`** - Python dependencies for the script -- **`plugins.json`** - The actual plugin registry - -## Benefits - -✅ **Automatic version detection** - No manual updates needed -✅ **Consistent format** - Ensures all plugins follow the same versioning -✅ **Always up-to-date** - Registry updates every 6 hours -✅ **Easy releases** - Just create a git tag and push -✅ **Version history** - All versions tracked in `plugins.json` - -## Example Workflow - -Here's a typical workflow for releasing a plugin update: - -```bash -# 1. Make your changes to the plugin -cd plugin-repos/ledmatrix-weather -# ... edit files ... - -# 2. Update the version in manifest.json -# Change "version": "1.0.0" to "version": "2.0.0" - -# 3. Commit your changes -git add -A -git commit -m "Add new features for v2.0.0" -git push - -# 4. Create and push a tag -git tag -a v2.0.0 -m "Version 2.0.0 - New features" -git push origin v2.0.0 - -# 5. Done! The registry will automatically update within 6 hours -# Or trigger the GitHub Action manually for immediate update -``` - -## Notes - -- The script prioritizes **releases** over **tags** when both exist -- Only **non-draft**, **non-prerelease** versions are considered -- The script preserves all existing version history -- Release dates are taken from GitHub's published date - diff --git a/CLAUDE.md b/CLAUDE.md index a1d6699..aa44ea2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,8 +12,9 @@ ### Steps for every plugin change: 1. Make your code changes in `plugins//` 2. Bump `version` in `plugins//manifest.json` (semver: major.minor.patch) -3. Run `python update_registry.py` to sync `plugins.json` -4. Commit both the plugin changes AND the updated `plugins.json` +3. Commit — the pre-commit hook automatically runs `update_registry.py` and stages `plugins.json` + +> **Note:** The pre-commit hook only triggers when a `plugins/*/manifest.json` is staged. If it's not installed, run `cp scripts/pre-commit .git/hooks/pre-commit` to set it up. ### Version bump guidelines: - **Patch** (1.0.0 → 1.0.1): Bug fixes, minor text changes @@ -44,3 +45,7 @@ Third-party plugins keep their own `repo` URL and empty `plugin_path`. - `python update_registry.py` — Update plugins.json from manifests - `python update_registry.py --dry-run` — Preview without writing - `scripts/archive_old_repos.sh` — Archive old individual repos (one-time, use `--apply`) + +## Git Hooks +- `scripts/pre-commit` — Auto-syncs `plugins.json` when manifest versions change +- Install: `cp scripts/pre-commit .git/hooks/pre-commit` diff --git a/README.md b/README.md index 24c44d3..3c9c552 100644 --- a/README.md +++ b/README.md @@ -187,15 +187,23 @@ ledmatrix-plugins/ ... ``` +### Setup + +Install the git pre-commit hook so `plugins.json` stays in sync automatically: + +```bash +cp scripts/pre-commit .git/hooks/pre-commit +``` + ### Updating Plugin Versions After making changes to a plugin: 1. Bump `version` in the plugin's `manifest.json` -2. Run `python update_registry.py` to sync `plugins.json` -3. Commit and push both changes +2. Commit — the pre-commit hook automatically syncs `plugins.json` ```bash +# Manual alternative (if hook isn't installed): python update_registry.py # Update plugins.json python update_registry.py --dry-run # Preview changes ``` diff --git a/REGISTRY_TEMPLATE_GUIDE.md b/REGISTRY_TEMPLATE_GUIDE.md deleted file mode 100644 index a5e2fc5..0000000 --- a/REGISTRY_TEMPLATE_GUIDE.md +++ /dev/null @@ -1,228 +0,0 @@ -# Plugin Registry Template System - -## Overview - -The plugin registry now supports **download URL templates**, eliminating the need to manually update download URLs for each version. - -## New Format (Simplified) - -### Before (Manual URL per version) -```json -{ - "id": "weather", - "repo": "https://github.com/ChuckBuilds/ledmatrix-weather", - "versions": [ - { - "version": "1.0.4", - "download_url": "https://github.com/ChuckBuilds/ledmatrix-weather/archive/refs/tags/v1.0.4.zip" - }, - { - "version": "1.0.3", - "download_url": "https://github.com/ChuckBuilds/ledmatrix-weather/archive/refs/tags/v1.0.3.zip" - } - ] -} -``` - -### After (Template with automatic URL generation) -```json -{ - "id": "weather", - "repo": "https://github.com/ChuckBuilds/ledmatrix-weather", - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-weather/archive/refs/tags/v{version}.zip", - "latest_version": "1.0.4", - "versions": [ - { - "version": "1.0.4", - "released": "2025-10-16" - }, - { - "version": "1.0.3", - "released": "2025-10-14" - } - ] -} -``` - -## Benefits - -1. **Less repetition** - No more copying download URLs -2. **Easier updates** - Only update `latest_version` and add version entry -3. **Fewer errors** - Template ensures consistent URL format -4. **Backward compatible** - Still supports individual `download_url` in version entries - -## Adding a New Version - -### Old Way (5 edits needed) -```json -{ - "versions": [ - { - "version": "1.0.4", // ← Edit 1 - "ledmatrix_min": "2.0.0", - "released": "2025-10-16", // ← Edit 2 - "download_url": "https://github.com/ChuckBuilds/ledmatrix-weather/archive/refs/tags/v1.0.4.zip" // ← Edit 3 - }, - // ... previous versions - ], - "last_updated": "2025-10-16" // ← Edit 4 -} -``` - -### New Way (3 edits needed) -```json -{ - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-weather/archive/refs/tags/v{version}.zip", - "latest_version": "1.0.4", // ← Edit 1 - "versions": [ - { - "version": "1.0.4", // ← Edit 2 - "released": "2025-10-16" // ← Edit 3 - }, - // ... previous versions - ], - "last_updated": "2025-10-16" // ← Edit 4 -} -``` - -## Template Variables - -Currently supported: -- `{version}` - Version number (e.g., "1.0.4") - -Future possibilities: -- `{branch}` - Branch name -- `{repo}` - Repository name -- `{author}` - Author name - -## Fallback Behavior - -The store manager tries URLs in this order: - -1. **Version-specific URL** (if `download_url` in version entry) - ```json - "versions": [{"version": "1.0.4", "download_url": "custom_url.zip"}] - ``` - -2. **Plugin template** (if `download_url_template` at plugin level) - ```json - "download_url_template": "https://example.com/{version}.zip" - ``` - -3. **Auto-constructed** (fallback pattern) - ``` - {repo}/archive/refs/tags/v{version}.zip - ``` - -## Migration Guide - -### Step 1: Add Template and Latest Version - -```json -{ - "id": "my-plugin", - "download_url_template": "https://github.com/user/repo/archive/refs/tags/v{version}.zip", - "latest_version": "1.0.5", - "versions": [...] -} -``` - -### Step 2: Remove download_url from Version Entries - -```json -// Before -{ - "version": "1.0.4", - "ledmatrix_min": "2.0.0", - "released": "2025-10-16", - "download_url": "https://github.com/..." // ← Remove this -} - -// After -{ - "version": "1.0.4", - "ledmatrix_min": "2.0.0", - "released": "2025-10-16" -} -``` - -### Step 3: Test Installation - -```bash -# Test via web interface or CLI -# The download URL will be auto-generated from template -``` - -## Examples - -### Standard GitHub Release -```json -{ - "id": "weather", - "repo": "https://github.com/ChuckBuilds/ledmatrix-weather", - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-weather/archive/refs/tags/v{version}.zip", - "latest_version": "1.0.4" -} -``` - -### Custom URL Pattern -```json -{ - "id": "custom-plugin", - "repo": "https://example.com/repo", - "download_url_template": "https://cdn.example.com/plugins/custom-{version}.zip", - "latest_version": "2.3.1" -} -``` - -### Override for Specific Version -```json -{ - "download_url_template": "https://github.com/user/repo/archive/refs/tags/v{version}.zip", - "versions": [ - { - "version": "1.0.4", - "released": "2025-10-16" - // Uses template: v1.0.4.zip - }, - { - "version": "1.0.3-beta", - "released": "2025-10-14", - "download_url": "https://github.com/user/repo/releases/download/v1.0.3-beta/custom.zip" - // Uses specific URL (overrides template) - } - ] -} -``` - -## Future Updates Process - -### When releasing v1.0.5: - -1. Update plugin repo (tag release as v1.0.5) -2. Update `plugins.json`: - ```json - { - "latest_version": "1.0.5", // ← Change this - "versions": [ - { - "version": "1.0.5", // ← Add this - "released": "2025-10-XX" - }, - // ... older versions - ] - } - ``` -3. **Done!** Download URL is auto-generated from template - -## Validation - -The template system validates that: -- Template contains `{version}` placeholder -- Latest version exists in versions array -- Generated URL is valid (optional check) - -## Backward Compatibility - -✅ Old format still works - if `download_url` exists in version entry, it takes priority over template - diff --git a/SETUP_TOKEN.md b/SETUP_TOKEN.md deleted file mode 100644 index d933f8d..0000000 --- a/SETUP_TOKEN.md +++ /dev/null @@ -1,51 +0,0 @@ -# Quick Setup: GitHub API Token - -To avoid rate limits when updating the plugin registry, follow these steps: - -## Step 1: Create config_secrets.json - -```powershell -cd c:/Users/Charles/Documents/GitHub/ledmatrix-plugins -cp config_secrets.template.json config_secrets.json -``` - -## Step 2: Get Your GitHub Token - -1. Go to: https://github.com/settings/tokens/new -2. Description: `LEDMatrix Plugin Registry` -3. **Scopes: NONE needed** (for public repos) -4. Click "Generate token" -5. **Copy the token** (you won't see it again!) - -## Step 3: Add Token to config_secrets.json - -Edit `config_secrets.json` and replace the placeholder: - -```json -{ - "github": { - "api_token": "ghp_YourActualTokenHere1234567890abcdef" - } -} -``` - -## Step 4: Test It - -```powershell -python update_registry.py -``` - -You should see: -``` -✓ Loaded GitHub token from c:\Users\Charles\Documents\GitHub\ledmatrix-plugins\config_secrets.json -``` - -## Benefits - -✅ **Before:** 60 API requests/hour (rate limited) -✅ **After:** 5,000 API requests/hour (plenty!) - -## Note - -The `config_secrets.json` file is already in `.gitignore` so it won't be committed to Git. Your token is safe! 🔒 - diff --git a/SUBMISSION.md b/SUBMISSION.md index b2ee54c..eac970e 100644 --- a/SUBMISSION.md +++ b/SUBMISSION.md @@ -16,7 +16,33 @@ Before submitting, ensure your plugin: - ✅ Uses logging appropriately - ✅ Has no hardcoded API keys or secrets -## Submission Process +## Submission Options + +### Option A: Add to the Monorepo (Preferred) + +Submit a PR to add your plugin directly to this repository: + +1. **Fork This Repo** + Fork [ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins) + +2. **Add Your Plugin Directory** + ``` + plugins/your-plugin-id/ + manifest.json + manager.py + config_schema.json + requirements.txt + README.md + ``` + +3. **Submit Pull Request** + Create PR with title: "Add plugin: your-plugin-name" + +After approval, your plugin will be added to `plugins.json` and available in the Plugin Store. + +### Option B: Keep Your Own Repository (3rd-Party) + +If you prefer to maintain your own repo: 1. **Test Your Plugin** ```bash @@ -25,41 +51,11 @@ Before submitting, ensure your plugin: -d '{"repo_url": "https://github.com/you/ledmatrix-your-plugin"}' ``` -2. **Create Release** - ```bash - git tag v1.0.0 - git push origin v1.0.0 - ``` - -3. **Fork This Repo** - Fork [ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins) +2. **Open an Issue** + Open an issue on this repository with your repo URL, description, and screenshots. -4. **Update plugins.json** - Add your plugin entry: - ```json - { - "id": "your-plugin", - "name": "Your Plugin Name", - "description": "What it does", - "author": "YourName", - "category": "custom", - "tags": ["tag1", "tag2"], - "repo": "https://github.com/you/ledmatrix-your-plugin", - "branch": "main", - "versions": [ - { - "version": "1.0.0", - "ledmatrix_min": "2.0.0", - "released": "2025-10-09", - "download_url": "https://github.com/you/ledmatrix-your-plugin/archive/refs/tags/v1.0.0.zip" - } - ], - "verified": false - } - ``` - -5. **Submit Pull Request** - Create PR with title: "Add plugin: your-plugin-name" +3. **After Review** + We'll add a registry entry in `plugins.json` pointing to your repo. ## Review Process @@ -77,14 +73,15 @@ Before submitting, ensure your plugin: ## Updating Your Plugin -To release a new version: +### Monorepo Plugins (Option A) +1. Submit a PR with your code changes +2. Bump `version` in your plugin's `manifest.json` +3. The pre-commit hook will automatically update `plugins.json` -1. Create new release in your repo -2. Update `versions` array in plugins.json -3. Submit PR with changes -4. We'll review and merge +### 3rd-Party Plugins (Option B) +1. Push updates to your repository +2. Open a PR or issue to update the version in `plugins.json` ## Questions? -Open an issue in this repo or the main LEDMatrix repo. - +Open an issue in this repo or the main [LEDMatrix repo](https://github.com/ChuckBuilds/LEDMatrix). diff --git a/docs/PLUGIN_REGISTRY_SETUP_GUIDE.md b/docs/PLUGIN_REGISTRY_SETUP_GUIDE.md deleted file mode 100644 index 3d6e41c..0000000 --- a/docs/PLUGIN_REGISTRY_SETUP_GUIDE.md +++ /dev/null @@ -1,446 +0,0 @@ -# Plugin Registry Setup Guide - -This guide explains how to set up and maintain your official plugin registry at [https://github.com/ChuckBuilds/ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins). - -## Overview - -Your plugin registry serves as a **central directory** that lists all official, verified plugins. The registry is just a JSON file; the actual plugins live in their own repositories. - -## Repository Structure - -``` -ledmatrix-plugins/ -├── README.md # Main documentation -├── LICENSE # GPL-3.0 -├── plugins.json # The registry file (main file!) -├── SUBMISSION.md # Guidelines for submitting plugins -├── VERIFICATION.md # Verification checklist -└── assets/ # Optional: screenshots, badges - └── screenshots/ -``` - -## Step 1: Create plugins.json - -This is the **core file** that the Plugin Store reads from. - -**File**: `plugins.json` - -```json -{ - "version": "1.0.0", - "last_updated": "2025-01-09T12:00:00Z", - "plugins": [ - { - "id": "clock-simple", - "name": "Simple Clock", - "description": "A clean, simple clock display with date and time", - "author": "ChuckBuilds", - "category": "time", - "tags": ["clock", "time", "date"], - "repo": "https://github.com/ChuckBuilds/ledmatrix-clock-simple", - "branch": "main", - "versions": [ - { - "version": "1.0.0", - "ledmatrix_min": "2.0.0", - "released": "2025-01-09", - "download_url": "https://github.com/ChuckBuilds/ledmatrix-clock-simple/archive/refs/tags/v1.0.0.zip" - } - ], - "stars": 12, - "downloads": 156, - "last_updated": "2025-01-09", - "verified": true, - "screenshot": "https://raw.githubusercontent.com/ChuckBuilds/ledmatrix-plugins/main/assets/screenshots/clock-simple.png" - } - ] -} -``` - -## Step 2: Create Plugin Repositories - -Each plugin should have its own repository: - -### Example: Creating clock-simple Plugin - -1. **Create new repo**: `ledmatrix-clock-simple` -2. **Add plugin files**: - ``` - ledmatrix-clock-simple/ - ├── manifest.json - ├── manager.py - ├── requirements.txt - ├── config_schema.json - ├── README.md - └── assets/ - ``` -3. **Tag a release**: `git tag v1.0.0 && git push origin v1.0.0` -4. **Add to registry**: Update `plugins.json` in ledmatrix-plugins repo - -## Step 3: Update README.md - -Create a comprehensive README for your plugin registry: - -```markdown -# LEDMatrix Official Plugins - -Official plugin registry for [LEDMatrix](https://github.com/ChuckBuilds/LEDMatrix). - -## Available Plugins - - - -| Plugin | Description | Category | Version | -|--------|-------------|----------|---------| -| [Simple Clock](https://github.com/ChuckBuilds/ledmatrix-clock-simple) | Clean clock display | Time | 1.0.0 | -| [NHL Scores](https://github.com/ChuckBuilds/ledmatrix-nhl-scores) | Live NHL scores | Sports | 1.0.0 | - -## Installation - -All plugins can be installed through the LEDMatrix web interface: - -1. Open web interface (http://your-pi-ip:5050) -2. Go to Plugin Store tab -3. Browse or search for plugins -4. Click Install - -Or via API: -```bash -curl -X POST http://your-pi-ip:5050/api/plugins/install \ - -d '{"plugin_id": "clock-simple"}' -``` - -## Submitting Plugins - -See [SUBMISSION.md](SUBMISSION.md) for guidelines on submitting your plugin. - -## Creating Plugins - -See the main [LEDMatrix Plugin Developer Guide](https://github.com/ChuckBuilds/LEDMatrix/wiki/Plugin-Development). - -## Plugin Categories - -- **Time**: Clocks, timers, countdowns -- **Sports**: Scoreboards, schedules, stats -- **Weather**: Forecasts, current conditions -- **Finance**: Stocks, crypto, market data -- **Entertainment**: Games, animations, media -- **Custom**: Unique displays -``` - -## Step 4: Create SUBMISSION.md - -Guidelines for community plugin submissions: - -```markdown -# Plugin Submission Guidelines - -Want to add your plugin to the official registry? Follow these steps! - -## Requirements - -Before submitting, ensure your plugin: - -- ✅ Has a complete `manifest.json` with all required fields -- ✅ Follows the plugin architecture specification -- ✅ Has comprehensive README documentation -- ✅ Includes example configuration -- ✅ Has been tested on Raspberry Pi hardware -- ✅ Follows coding standards (PEP 8) -- ✅ Has proper error handling -- ✅ Uses logging appropriately -- ✅ Has no hardcoded API keys or secrets - -## Submission Process - -1. **Test Your Plugin** - ```bash - # Install via URL on your Pi - curl -X POST http://your-pi:5050/api/plugins/install-from-url \ - -d '{"repo_url": "https://github.com/you/ledmatrix-your-plugin"}' - ``` - -2. **Create Release** - ```bash - git tag v1.0.0 - git push origin v1.0.0 - ``` - -3. **Fork This Repo** - Fork [ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins) - -4. **Update plugins.json** - Add your plugin entry: - ```json - { - "id": "your-plugin", - "name": "Your Plugin Name", - "description": "What it does", - "author": "YourName", - "category": "custom", - "tags": ["tag1", "tag2"], - "repo": "https://github.com/you/ledmatrix-your-plugin", - "branch": "main", - "versions": [ - { - "version": "1.0.0", - "ledmatrix_min": "2.0.0", - "released": "2025-01-09", - "download_url": "https://github.com/you/ledmatrix-your-plugin/archive/refs/tags/v1.0.0.zip" - } - ], - "verified": false - } - ``` - -5. **Submit Pull Request** - Create PR with title: "Add plugin: your-plugin-name" - -## Review Process - -1. **Automated Checks**: Manifest validation, structure check -2. **Code Review**: Manual review of plugin code -3. **Testing**: Test installation and basic functionality -4. **Approval**: If accepted, merged and marked as verified - -## After Approval - -- Plugin appears in official store -- `verified: true` badge shown -- Included in plugin count -- Featured in README - -## Updating Your Plugin - -To release a new version: - -1. Create new release in your repo -2. Update `versions` array in plugins.json -3. Submit PR with changes -4. We'll review and merge - -## Questions? - -Open an issue in this repo or the main LEDMatrix repo. -``` - -## Step 5: Create VERIFICATION.md - -Checklist for verifying plugins: - -```markdown -# Plugin Verification Checklist - -Use this checklist when reviewing plugin submissions. - -## Code Review - -- [ ] Follows BasePlugin interface -- [ ] Has proper error handling -- [ ] Uses logging appropriately -- [ ] No hardcoded secrets/API keys -- [ ] Follows Python coding standards -- [ ] Has type hints where appropriate -- [ ] Has docstrings for classes/methods - -## Manifest Validation - -- [ ] All required fields present -- [ ] Valid JSON syntax -- [ ] Correct version format (semver) -- [ ] Category is valid -- [ ] Tags are descriptive - -## Functionality - -- [ ] Installs successfully via URL -- [ ] Dependencies install correctly -- [ ] Plugin loads without errors -- [ ] Display output works correctly -- [ ] Configuration schema validates -- [ ] Example config provided - -## Documentation - -- [ ] README.md exists and is comprehensive -- [ ] Installation instructions clear -- [ ] Configuration options documented -- [ ] Examples provided -- [ ] License specified - -## Security - -- [ ] No malicious code -- [ ] Safe dependency versions -- [ ] Appropriate permissions -- [ ] No network access without disclosure -- [ ] No file system access outside plugin dir - -## Testing - -- [ ] Tested on Raspberry Pi -- [ ] Works with 64x32 matrix (minimum) -- [ ] No excessive CPU/memory usage -- [ ] No crashes or freezes - -## Approval - -Once all checks pass: -- [ ] Set `verified: true` in plugins.json -- [ ] Merge PR -- [ ] Welcome plugin author -- [ ] Update stats (downloads, stars) -``` - -## Step 6: Workflow for Adding Plugins - -### For Your Own Plugins - -```bash -# 1. Create plugin in separate repo -mkdir ledmatrix-clock-simple -cd ledmatrix-clock-simple -# ... create plugin files ... - -# 2. Push to GitHub -git init -git add . -git commit -m "Initial commit" -git remote add origin https://github.com/ChuckBuilds/ledmatrix-clock-simple -git push -u origin main - -# 3. Create release -git tag v1.0.0 -git push origin v1.0.0 - -# 4. Update registry -cd ../ledmatrix-plugins -# Edit plugins.json to add new entry -git add plugins.json -git commit -m "Add clock-simple plugin" -git push -``` - -### For Community Submissions - -```bash -# 1. Receive PR on ledmatrix-plugins repo -# 2. Review using VERIFICATION.md checklist -# 3. Test installation: -curl -X POST http://pi:5050/api/plugins/install-from-url \ - -d '{"repo_url": "https://github.com/contributor/plugin"}' - -# 4. If approved, merge PR -# 5. Set verified: true in plugins.json -``` - -## Step 7: Maintaining the Registry - -### Regular Updates - -```bash -# Update stars/downloads counts -python3 scripts/update_stats.py - -# Validate all plugin entries -python3 scripts/validate_registry.py - -# Check for plugin updates -python3 scripts/check_updates.py -``` - -### Adding New Versions - -When a plugin releases a new version, update the `versions` array: - -```json -{ - "id": "clock-simple", - "versions": [ - { - "version": "1.1.0", - "ledmatrix_min": "2.0.0", - "released": "2025-01-15", - "download_url": "https://github.com/ChuckBuilds/ledmatrix-clock-simple/archive/refs/tags/v1.1.0.zip" - }, - { - "version": "1.0.0", - "ledmatrix_min": "2.0.0", - "released": "2025-01-09", - "download_url": "https://github.com/ChuckBuilds/ledmatrix-clock-simple/archive/refs/tags/v1.0.0.zip" - } - ] -} -``` - -## Converting Existing Plugins - -To convert your existing plugins (hello-world, clock-simple) to this system: - -### 1. Move to Separate Repos - -```bash -# For each plugin in plugins/ -cd plugins/clock-simple - -# Create new repo -git init -git add . -git commit -m "Extract clock-simple plugin" -git remote add origin https://github.com/ChuckBuilds/ledmatrix-clock-simple -git push -u origin main -git tag v1.0.0 -git push origin v1.0.0 -``` - -### 2. Add to Registry - -Update `plugins.json` in ledmatrix-plugins repo. - -### 3. Keep or Remove from Main Repo - -Decision: -- **Keep**: Leave in main repo for backward compatibility -- **Remove**: Delete from main repo, users install via store - -## Testing the Registry - -After setting up: - -```bash -# Test registry fetch -curl https://raw.githubusercontent.com/ChuckBuilds/ledmatrix-plugins/main/plugins.json - -# Test plugin installation -python3 -c " -from src.plugin_system.store_manager import PluginStoreManager -store = PluginStoreManager() -registry = store.fetch_registry() -print(f'Found {len(registry[\"plugins\"])} plugins') -" -``` - -## Benefits of This Setup - -✅ **Centralized Discovery**: One place to find all official plugins -✅ **Decentralized Storage**: Each plugin in its own repo -✅ **Easy Maintenance**: Update registry without touching plugin code -✅ **Community Friendly**: Anyone can submit via PR -✅ **Version Control**: Track plugin versions and updates -✅ **Verified Badge**: Show trust with verified plugins - -## Next Steps - -1. Create `plugins.json` in your repo -2. Update the registry URL in LEDMatrix code (already done) -3. Create SUBMISSION.md and README.md -4. Move existing plugins to separate repos -5. Add them to the registry -6. Announce the plugin store! - -## References - -- Plugin Store Implementation: See `PLUGIN_STORE_IMPLEMENTATION_SUMMARY.md` -- User Guide: See `PLUGIN_STORE_USER_GUIDE.md` -- Architecture: See `PLUGIN_ARCHITECTURE_SPEC.md` - diff --git a/plugins.json b/plugins.json index 3b94b5c..6ddd988 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-02-12", + "last_updated": "2026-02-11", "plugins": [ { "id": "hello-world", @@ -18,7 +18,7 @@ "plugin_path": "plugins/hello-world", "stars": 0, "downloads": 0, - "last_updated": "2026-02-12", + "last_updated": "2026-02-11", "verified": true, "screenshot": "", "latest_version": "1.0.2" @@ -196,10 +196,10 @@ "plugin_path": "plugins/hockey-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-12", + "last_updated": "2026-02-11", "verified": true, "screenshot": "", - "latest_version": "1.0.8" + "latest_version": "1.1.0" }, { "id": "football-scoreboard", @@ -301,7 +301,7 @@ "plugin_path": "plugins/soccer-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-12", + "last_updated": "2026-02-11", "verified": true, "screenshot": "", "latest_version": "1.1.0" @@ -328,10 +328,10 @@ "plugin_path": "plugins/odds-ticker", "stars": 0, "downloads": 0, - "last_updated": "2025-10-19", + "last_updated": "2026-02-11", "verified": true, "screenshot": "", - "latest_version": "1.0.2" + "latest_version": "1.0.3" }, { "id": "leaderboard", @@ -379,10 +379,10 @@ "plugin_path": "plugins/news", "stars": 0, "downloads": 0, - "last_updated": "2025-10-19", + "last_updated": "2026-02-11", "verified": true, "screenshot": "", - "latest_version": "1.0.2" + "latest_version": "1.0.3" }, { "id": "stock-news", @@ -501,7 +501,7 @@ "plugin_path": "plugins/olympics", "stars": 0, "downloads": 0, - "last_updated": "2026-02-12", + "last_updated": "2026-02-11", "verified": true, "screenshot": "", "latest_version": "2.0.0" diff --git a/plugins/7-segment-clock/README.md b/plugins/7-segment-clock/README.md index 8b8deb0..5e13faf 100644 --- a/plugins/7-segment-clock/README.md +++ b/plugins/7-segment-clock/README.md @@ -14,27 +14,22 @@ A retro-style 7-segment clock plugin for LEDMatrix that displays time using digi ## Installation -### From Plugin Store (Coming Soon) +### From Plugin Store (Recommended) -The plugin can be installed via the LEDMatrix web interface plugin store. +1. Open the LEDMatrix web interface (`http://your-pi-ip:5000`) +2. Go to **Plugin Store** +3. Find **7-Segment Clock** and click **Install** ### Manual Installation -1. Clone or download this repository to your LEDMatrix `plugins/` directory: +1. Copy the plugin from the monorepo: ```bash - cd /path/to/LEDMatrix/plugins - git clone --recurse-submodules 7-segment-clock - ``` - - **Note**: If you've already cloned without submodules, initialize them with: - ```bash - cd 7-segment-clock - git submodule update --init --recursive + cp -r ledmatrix-plugins/plugins/7-segment-clock /path/to/LEDMatrix/plugin-repos/ ``` 2. Install dependencies: ```bash - pip install -r plugins/7-segment-clock/requirements.txt + pip install -r plugin-repos/7-segment-clock/requirements.txt ``` 3. Enable the plugin in `config/config.json`: diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 71faf61..38595eb 100644 --- a/plugins/baseball-scoreboard/manifest.json +++ b/plugins/baseball-scoreboard/manifest.json @@ -20,7 +20,6 @@ "baseball_recent", "baseball_upcoming" ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-baseball-scoreboard/archive/refs/tags/v{version}.zip", "versions": [ { "released": "2025-10-20", @@ -45,4 +44,4 @@ "screenshot": "", "class_name": "BaseballScoreboardPlugin", "entry_point": "manager.py" -} \ No newline at end of file +} diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 9896183..78627a8 100644 --- a/plugins/basketball-scoreboard/manifest.json +++ b/plugins/basketball-scoreboard/manifest.json @@ -1,19 +1,27 @@ { "id": "basketball-scoreboard", "name": "Basketball Scoreboard", + "version": "1.0.5", "description": "Live, recent, and upcoming basketball games across NBA, NCAA Men's, NCAA Women's, and WNBA with real-time scores and schedules", "author": "ChuckBuilds", "category": "sports", - "tags": ["basketball", "nba", "ncaa", "wnba", "sports", "scoreboard", "live-scores"], - "repo": "https://github.com/ChuckBuilds/ledmatrix-basketball-scoreboard", + "tags": [ + "basketball", + "nba", + "ncaa", + "wnba", + "sports", + "scoreboard", + "live-scores" + ], + "repo": "https://github.com/ChuckBuilds/ledmatrix-plugins", "branch": "main", "plugin_path": "plugins/basketball-scoreboard", "versions": [ { "version": "1.0.5", "ledmatrix_min": "2.0.0", - "released": "2025-10-26", - "download_url": "https://github.com/ChuckBuilds/ledmatrix-basketball-scoreboard/archive/refs/tags/v1.0.5.zip" + "released": "2025-10-26" } ], "stars": 0, diff --git a/plugins/calendar/manifest.json b/plugins/calendar/manifest.json index 2720cb7..0e4a00f 100644 --- a/plugins/calendar/manifest.json +++ b/plugins/calendar/manifest.json @@ -32,7 +32,6 @@ "error_message": "Google Calendar authentication failed" } ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-calendar/archive/refs/tags/v{version}.zip", "versions": [ { "released": "2025-10-19", @@ -45,4 +44,4 @@ "downloads": 0, "verified": true, "screenshot": "" -} \ No newline at end of file +} diff --git a/plugins/christmas-countdown/manifest.json b/plugins/christmas-countdown/manifest.json index 58ed27f..3b5049e 100644 --- a/plugins/christmas-countdown/manifest.json +++ b/plugins/christmas-countdown/manifest.json @@ -16,7 +16,6 @@ "display_modes": [ "christmas-countdown" ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-christmas-countdown/archive/refs/tags/v{version}.zip", "versions": [ { "released": "2025-01-27", @@ -30,4 +29,3 @@ "verified": true, "screenshot": "" } - diff --git a/plugins/clock-simple/manifest.json b/plugins/clock-simple/manifest.json index 769be88..6e51917 100644 --- a/plugins/clock-simple/manifest.json +++ b/plugins/clock-simple/manifest.json @@ -15,7 +15,6 @@ "display_modes": [ "clock-simple" ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-clock-simple/archive/refs/tags/v{version}.zip", "versions": [ { "released": "2025-10-19", @@ -28,4 +27,4 @@ "downloads": 0, "verified": true, "screenshot": "" -} \ No newline at end of file +} diff --git a/plugins/countdown/README.md b/plugins/countdown/README.md index 2e4aeb5..08c0ea1 100644 --- a/plugins/countdown/README.md +++ b/plugins/countdown/README.md @@ -15,20 +15,17 @@ Display customizable countdowns with images on your LED matrix. Perfect for birt ## Installation -### Via LEDMatrix Plugin Manager (Recommended) +### From Plugin Store (Recommended) -1. Open the LEDMatrix web UI -2. Navigate to Settings > Plugins -3. Click "Install from URL" -4. Enter your repository URL -5. Click Install +1. Open the LEDMatrix web interface (`http://your-pi-ip:5000`) +2. Go to **Plugin Store** +3. Find **Countdown Display** and click **Install** ### Manual Installation -1. Clone or download this repository to your Raspberry Pi: +1. Copy the plugin from the monorepo: ```bash - cd /path/to/LEDMatrix/plugin-repos/ - git clone countdown + cp -r ledmatrix-plugins/plugins/countdown /path/to/LEDMatrix/plugin-repos/ ``` 2. Restart LEDMatrix or reload plugins via the web UI diff --git a/plugins/countdown/manifest.json b/plugins/countdown/manifest.json index ae06c58..779ccca 100644 --- a/plugins/countdown/manifest.json +++ b/plugins/countdown/manifest.json @@ -17,11 +17,13 @@ "display_modes": [ "countdown" ], - "compatible_versions": [">=2.0.0"], + "compatible_versions": [ + ">=2.0.0" + ], "update_interval": 60, "default_duration": 15, "icon": "fa-clock", "license": "MIT", - "homepage": "", + "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/countdown", "config_schema": "config_schema.json" } diff --git a/plugins/countdown/plan.md b/plugins/countdown/plan.md deleted file mode 100644 index 7b64e7a..0000000 --- a/plugins/countdown/plan.md +++ /dev/null @@ -1,5 +0,0 @@ -I want to make a plugin for the LEDMatrix project that is a user customizable count-down. I want the user to provide a date, name, and uploadable image that the display will use the current date to count down until the target date. I want the user to be able to make multiple entries, I want the user to be able to enable or disable multiple of these entries, and I want to be able to manage the countdowns via the web ui. - -I want to re-use the image upload functions from: https://github.com/ChuckBuilds/ledmatrix-static-image - -The font size, color, and font should be user configurable. \ No newline at end of file diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index 9c11d09..ab37058 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -23,7 +23,6 @@ "ncaa_fb_upcoming", "ncaa_fb_live" ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-football-scoreboard/archive/refs/tags/v{version}.zip", "versions": [ { "version": "2.0.7", @@ -207,4 +206,4 @@ "verified": true, "screenshot": "", "config_schema": "config_schema.json" -} \ No newline at end of file +} diff --git a/plugins/hello-world/manifest.json b/plugins/hello-world/manifest.json index 0d50a02..7421596 100644 --- a/plugins/hello-world/manifest.json +++ b/plugins/hello-world/manifest.json @@ -15,7 +15,6 @@ "display_modes": [ "hello-world" ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-hello-world/archive/refs/tags/v{version}.zip", "versions": [ { "released": "2025-10-19", @@ -29,6 +28,7 @@ "verified": true, "screenshot": "", "config_schema": "config_schema.json", - "compatible_versions": [">=2.0.0"], + "compatible_versions": [ + ">=2.0.0" + ] } - diff --git a/plugins/hockey-scoreboard/config_schema.json b/plugins/hockey-scoreboard/config_schema.json index a7f09a8..91af2dc 100644 --- a/plugins/hockey-scoreboard/config_schema.json +++ b/plugins/hockey-scoreboard/config_schema.json @@ -1084,6 +1084,674 @@ } } }, + "olympic_mens": { + "type": "object", + "description": "Olympic Men's Ice Hockey configuration", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable Olympic Men's Ice Hockey games" + }, + "display_modes": { + "type": "object", + "title": "Display Modes", + "description": "Control which game types to show and how they are displayed", + "properties": { + "live": { + "type": "boolean", + "default": true, + "description": "Show Olympic Men's Hockey live games" + }, + "live_display_mode": { + "type": "string", + "enum": ["switch", "scroll"], + "default": "switch", + "description": "Display mode for live games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "recent": { + "type": "boolean", + "default": true, + "description": "Show Olympic Men's Hockey recent games" + }, + "recent_display_mode": { + "type": "string", + "enum": ["switch", "scroll"], + "default": "switch", + "description": "Display mode for recent games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "upcoming": { + "type": "boolean", + "default": true, + "description": "Show Olympic Men's Hockey upcoming games" + }, + "upcoming_display_mode": { + "type": "string", + "enum": ["switch", "scroll"], + "default": "switch", + "description": "Display mode for upcoming games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + } + } + }, + "scroll_settings": { + "type": "object", + "title": "Scroll Settings", + "description": "Settings for scroll display mode (when display mode is set to 'scroll')", + "properties": { + "scroll_speed": { + "type": "number", + "default": 50.0, + "minimum": 1.0, + "maximum": 200.0, + "description": "Scroll speed in pixels per second (default: 50). Higher values scroll faster." + }, + "scroll_delay": { + "type": "number", + "default": 0.01, + "minimum": 0.001, + "maximum": 0.1, + "description": "Delay between scroll frames in seconds (default: 0.01 = 100 FPS). Lower values = smoother scrolling." + }, + "gap_between_games": { + "type": "integer", + "default": 48, + "minimum": 8, + "maximum": 128, + "description": "Gap in pixels between game cards when scrolling" + }, + "show_league_separators": { + "type": "boolean", + "default": true, + "description": "Show league icons between different leagues" + }, + "dynamic_duration": { + "type": "boolean", + "default": true, + "description": "Automatically calculate display duration based on content width" + } + } + }, + "teams": { + "type": "object", + "description": "Team filtering and favorites configuration", + "properties": { + "favorite_teams": { + "type": "array", + "items": {"type": "string"}, + "default": [], + "description": "Olympic Men's Hockey favorite country codes (e.g., ['USA', 'CAN', 'SWE', 'FIN'])" + }, + "favorite_teams_only": { + "type": "boolean", + "default": false, + "description": "Only show Olympic Men's Hockey games with favorite teams" + }, + "show_all_live": { + "type": "boolean", + "default": false, + "description": "Show all live Olympic Men's Hockey games, not just favorites" + } + } + }, + "filtering": { + "type": "object", + "description": "Game filtering and quantity settings", + "properties": { + "recent_games_to_show": { + "type": "integer", + "default": 5, + "minimum": 1, + "maximum": 20, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + }, + "upcoming_games_to_show": { + "type": "integer", + "default": 10, + "minimum": 1, + "maximum": 50, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + } + } + }, + "update_intervals": { + "type": "object", + "description": "Data update frequency settings (in seconds)", + "properties": { + "base": { + "type": "integer", + "default": 60, + "minimum": 15, + "maximum": 300, + "description": "Base update interval for Olympic Men's Hockey data" + }, + "live": { + "type": "integer", + "default": 30, + "minimum": 10, + "maximum": 300, + "description": "Update interval for live Olympic Men's Hockey games" + }, + "recent": { + "type": "integer", + "default": 3600, + "minimum": 60, + "maximum": 86400, + "description": "Update interval for recent Olympic Men's Hockey games" + }, + "upcoming": { + "type": "integer", + "default": 3600, + "minimum": 60, + "maximum": 86400, + "description": "Update interval for upcoming Olympic Men's Hockey games" + } + } + }, + "display_durations": { + "type": "object", + "description": "How long to display games on screen (in seconds)", + "properties": { + "base": { + "type": "number", + "default": 15, + "minimum": 5, + "maximum": 60, + "description": "Base display duration for Olympic Men's Hockey games" + }, + "live": { + "type": "number", + "default": 20, + "minimum": 5, + "maximum": 120, + "description": "Display duration for live Olympic Men's Hockey games" + }, + "recent": { + "type": "number", + "default": 15, + "minimum": 5, + "maximum": 60, + "description": "Display duration for recent Olympic Men's Hockey games" + }, + "upcoming": { + "type": "number", + "default": 15, + "minimum": 5, + "maximum": 60, + "description": "Display duration for upcoming Olympic Men's Hockey games" + } + } + }, + "display_options": { + "type": "object", + "description": "What information to display on the scoreboard", + "properties": { + "show_records": { + "type": "boolean", + "default": true, + "description": "Show Olympic Men's Hockey team records (wins-losses)" + }, + "show_ranking": { + "type": "boolean", + "default": false, + "description": "Show Olympic Men's Hockey team rankings" + }, + "show_odds": { + "type": "boolean", + "default": false, + "description": "Show betting odds for Olympic Men's Hockey games" + }, + "show_shots_on_goal": { + "type": "boolean", + "default": true, + "description": "Display shots on goal statistics for Olympic Men's Hockey games" + }, + "show_powerplay": { + "type": "boolean", + "default": true, + "description": "Highlight powerplay situations for Olympic Men's Hockey games" + } + } + }, + "mode_durations": { + "type": "object", + "title": "Mode-Level Durations", + "description": "Control total duration for each mode type for Olympic Men's Hockey. If not set, uses dynamic calculation (total_games × per_game_duration).", + "properties": { + "recent_mode_duration": { + "type": ["number", "null"], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Total duration in seconds for Recent mode before rotating to next mode." + }, + "upcoming_mode_duration": { + "type": ["number", "null"], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Total duration in seconds for Upcoming mode before rotating to next mode." + }, + "live_mode_duration": { + "type": ["number", "null"], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Total duration in seconds for Live mode before rotating to next mode." + } + } + }, + "live_priority": { + "type": "boolean", + "default": true, + "description": "Prioritize live Olympic Men's Hockey games over scheduled games" + }, + "dynamic_duration": { + "type": "object", + "description": "Dynamic duration settings - automatically adjust display time based on content", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for Olympic Men's Hockey games" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Maximum duration in seconds when dynamic duration is enabled" + }, + "modes": { + "type": "object", + "description": "Per-mode dynamic duration settings", + "properties": { + "live": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for Olympic Men's Hockey live games" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Maximum duration for Olympic Men's Hockey live games" + } + } + }, + "recent": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for Olympic Men's Hockey recent games" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Maximum duration for Olympic Men's Hockey recent games" + } + } + }, + "upcoming": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for Olympic Men's Hockey upcoming games" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Maximum duration for Olympic Men's Hockey upcoming games" + } + } + } + } + } + } + } + } + }, + "olympic_womens": { + "type": "object", + "description": "Olympic Women's Ice Hockey configuration", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable Olympic Women's Ice Hockey games" + }, + "display_modes": { + "type": "object", + "title": "Display Modes", + "description": "Control which game types to show and how they are displayed", + "properties": { + "live": { + "type": "boolean", + "default": true, + "description": "Show Olympic Women's Hockey live games" + }, + "live_display_mode": { + "type": "string", + "enum": ["switch", "scroll"], + "default": "switch", + "description": "Display mode for live games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "recent": { + "type": "boolean", + "default": true, + "description": "Show Olympic Women's Hockey recent games" + }, + "recent_display_mode": { + "type": "string", + "enum": ["switch", "scroll"], + "default": "switch", + "description": "Display mode for recent games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "upcoming": { + "type": "boolean", + "default": true, + "description": "Show Olympic Women's Hockey upcoming games" + }, + "upcoming_display_mode": { + "type": "string", + "enum": ["switch", "scroll"], + "default": "switch", + "description": "Display mode for upcoming games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + } + } + }, + "scroll_settings": { + "type": "object", + "title": "Scroll Settings", + "description": "Settings for scroll display mode (when display mode is set to 'scroll')", + "properties": { + "scroll_speed": { + "type": "number", + "default": 50.0, + "minimum": 1.0, + "maximum": 200.0, + "description": "Scroll speed in pixels per second (default: 50). Higher values scroll faster." + }, + "scroll_delay": { + "type": "number", + "default": 0.01, + "minimum": 0.001, + "maximum": 0.1, + "description": "Delay between scroll frames in seconds (default: 0.01 = 100 FPS). Lower values = smoother scrolling." + }, + "gap_between_games": { + "type": "integer", + "default": 48, + "minimum": 8, + "maximum": 128, + "description": "Gap in pixels between game cards when scrolling" + }, + "show_league_separators": { + "type": "boolean", + "default": true, + "description": "Show league icons between different leagues" + }, + "dynamic_duration": { + "type": "boolean", + "default": true, + "description": "Automatically calculate display duration based on content width" + } + } + }, + "teams": { + "type": "object", + "description": "Team filtering and favorites configuration", + "properties": { + "favorite_teams": { + "type": "array", + "items": {"type": "string"}, + "default": [], + "description": "Olympic Women's Hockey favorite country codes (e.g., ['USA', 'CAN', 'SWE', 'FIN'])" + }, + "favorite_teams_only": { + "type": "boolean", + "default": false, + "description": "Only show Olympic Women's Hockey games with favorite teams" + }, + "show_all_live": { + "type": "boolean", + "default": false, + "description": "Show all live Olympic Women's Hockey games, not just favorites" + } + } + }, + "filtering": { + "type": "object", + "description": "Game filtering and quantity settings", + "properties": { + "recent_games_to_show": { + "type": "integer", + "default": 5, + "minimum": 1, + "maximum": 20, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + }, + "upcoming_games_to_show": { + "type": "integer", + "default": 10, + "minimum": 1, + "maximum": 50, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + } + } + }, + "update_intervals": { + "type": "object", + "description": "Data update frequency settings (in seconds)", + "properties": { + "base": { + "type": "integer", + "default": 60, + "minimum": 15, + "maximum": 300, + "description": "Base update interval for Olympic Women's Hockey data" + }, + "live": { + "type": "integer", + "default": 30, + "minimum": 10, + "maximum": 300, + "description": "Update interval for live Olympic Women's Hockey games" + }, + "recent": { + "type": "integer", + "default": 3600, + "minimum": 60, + "maximum": 86400, + "description": "Update interval for recent Olympic Women's Hockey games" + }, + "upcoming": { + "type": "integer", + "default": 3600, + "minimum": 60, + "maximum": 86400, + "description": "Update interval for upcoming Olympic Women's Hockey games" + } + } + }, + "display_durations": { + "type": "object", + "description": "How long to display games on screen (in seconds)", + "properties": { + "base": { + "type": "number", + "default": 15, + "minimum": 5, + "maximum": 60, + "description": "Base display duration for Olympic Women's Hockey games" + }, + "live": { + "type": "number", + "default": 20, + "minimum": 5, + "maximum": 120, + "description": "Display duration for live Olympic Women's Hockey games" + }, + "recent": { + "type": "number", + "default": 15, + "minimum": 5, + "maximum": 60, + "description": "Display duration for recent Olympic Women's Hockey games" + }, + "upcoming": { + "type": "number", + "default": 15, + "minimum": 5, + "maximum": 60, + "description": "Display duration for upcoming Olympic Women's Hockey games" + } + } + }, + "display_options": { + "type": "object", + "description": "What information to display on the scoreboard", + "properties": { + "show_records": { + "type": "boolean", + "default": true, + "description": "Show Olympic Women's Hockey team records (wins-losses)" + }, + "show_ranking": { + "type": "boolean", + "default": false, + "description": "Show Olympic Women's Hockey team rankings" + }, + "show_odds": { + "type": "boolean", + "default": false, + "description": "Show betting odds for Olympic Women's Hockey games" + }, + "show_shots_on_goal": { + "type": "boolean", + "default": true, + "description": "Display shots on goal statistics for Olympic Women's Hockey games" + }, + "show_powerplay": { + "type": "boolean", + "default": true, + "description": "Highlight powerplay situations for Olympic Women's Hockey games" + } + } + }, + "mode_durations": { + "type": "object", + "title": "Mode-Level Durations", + "description": "Control total duration for each mode type for Olympic Women's Hockey. If not set, uses dynamic calculation (total_games × per_game_duration).", + "properties": { + "recent_mode_duration": { + "type": ["number", "null"], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Total duration in seconds for Recent mode before rotating to next mode." + }, + "upcoming_mode_duration": { + "type": ["number", "null"], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Total duration in seconds for Upcoming mode before rotating to next mode." + }, + "live_mode_duration": { + "type": ["number", "null"], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Total duration in seconds for Live mode before rotating to next mode." + } + } + }, + "live_priority": { + "type": "boolean", + "default": true, + "description": "Prioritize live Olympic Women's Hockey games over scheduled games" + }, + "dynamic_duration": { + "type": "object", + "description": "Dynamic duration settings - automatically adjust display time based on content", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for Olympic Women's Hockey games" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Maximum duration in seconds when dynamic duration is enabled" + }, + "modes": { + "type": "object", + "description": "Per-mode dynamic duration settings", + "properties": { + "live": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for Olympic Women's Hockey live games" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Maximum duration for Olympic Women's Hockey live games" + } + } + }, + "recent": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for Olympic Women's Hockey recent games" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Maximum duration for Olympic Women's Hockey recent games" + } + } + }, + "upcoming": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for Olympic Women's Hockey upcoming games" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Maximum duration for Olympic Women's Hockey upcoming games" + } + } + } + } + } + } + } + } + }, "customization": { "type": "object", "title": "Display Customization", diff --git a/plugins/hockey-scoreboard/manager.py b/plugins/hockey-scoreboard/manager.py index 573c36e..1424868 100644 --- a/plugins/hockey-scoreboard/manager.py +++ b/plugins/hockey-scoreboard/manager.py @@ -41,6 +41,16 @@ NCAAWHockeyRecentManager, NCAAWHockeyUpcomingManager, ) +from olympic_mens_hockey_managers import ( + OlympicMensHockeyLiveManager, + OlympicMensHockeyRecentManager, + OlympicMensHockeyUpcomingManager, +) +from olympic_womens_hockey_managers import ( + OlympicWomensHockeyLiveManager, + OlympicWomensHockeyRecentManager, + OlympicWomensHockeyUpcomingManager, +) logger = logging.getLogger(__name__) @@ -93,8 +103,10 @@ def __init__( self.nhl_enabled = config.get("nhl", {}).get("enabled", False) self.ncaa_mens_enabled = config.get("ncaa_mens", {}).get("enabled", False) self.ncaa_womens_enabled = config.get("ncaa_womens", {}).get("enabled", False) - - self.logger.info(f"League enabled states - NHL: {self.nhl_enabled}, NCAA Men's: {self.ncaa_mens_enabled}, NCAA Women's: {self.ncaa_womens_enabled}") + self.olympic_mens_enabled = config.get("olympic_mens", {}).get("enabled", False) + self.olympic_womens_enabled = config.get("olympic_womens", {}).get("enabled", False) + + self.logger.info(f"League enabled states - NHL: {self.nhl_enabled}, NCAA Men's: {self.ncaa_mens_enabled}, NCAA Women's: {self.ncaa_womens_enabled}, Olympic Men's: {self.olympic_mens_enabled}, Olympic Women's: {self.olympic_womens_enabled}") # Live priority settings self.nhl_live_priority = self.config.get("nhl", {}).get("live_priority", False) @@ -104,6 +116,12 @@ def __init__( self.ncaa_womens_live_priority = self.config.get("ncaa_womens", {}).get( "live_priority", False ) + self.olympic_mens_live_priority = self.config.get("olympic_mens", {}).get( + "live_priority", False + ) + self.olympic_womens_live_priority = self.config.get("olympic_womens", {}).get( + "live_priority", False + ) # Global settings - read from defaults section with fallback defaults = config.get("defaults", {}) @@ -236,7 +254,8 @@ def __init__( f"Hockey scoreboard plugin initialized - {self.display_width}x{self.display_height}" ) self.logger.info( - f"NHL enabled: {self.nhl_enabled}, NCAA Men's enabled: {self.ncaa_mens_enabled}, NCAA Women's enabled: {self.ncaa_womens_enabled}" + f"NHL enabled: {self.nhl_enabled}, NCAA Men's enabled: {self.ncaa_mens_enabled}, NCAA Women's enabled: {self.ncaa_womens_enabled}, " + f"Olympic Men's enabled: {self.olympic_mens_enabled}, Olympic Women's enabled: {self.olympic_womens_enabled}" ) def _initialize_managers(self): @@ -246,6 +265,8 @@ def _initialize_managers(self): nhl_config = self._adapt_config_for_manager("nhl") ncaa_mens_config = self._adapt_config_for_manager("ncaa_mens") ncaa_womens_config = self._adapt_config_for_manager("ncaa_womens") + olympic_mens_config = self._adapt_config_for_manager("olympic_mens") + olympic_womens_config = self._adapt_config_for_manager("olympic_womens") # Initialize NHL managers if enabled if self.nhl_enabled: @@ -316,6 +337,50 @@ def _initialize_managers(self): if not hasattr(self, "ncaa_womens_upcoming"): self.ncaa_womens_upcoming = None + # Initialize Olympic Men's managers if enabled + if self.olympic_mens_enabled: + try: + self.olympic_mens_live = OlympicMensHockeyLiveManager( + olympic_mens_config, self.display_manager, self.cache_manager + ) + self.olympic_mens_recent = OlympicMensHockeyRecentManager( + olympic_mens_config, self.display_manager, self.cache_manager + ) + self.olympic_mens_upcoming = OlympicMensHockeyUpcomingManager( + olympic_mens_config, self.display_manager, self.cache_manager + ) + self.logger.info("Olympic Men's Hockey managers initialized") + except Exception as e: + self.logger.error(f"Failed to initialize Olympic Men's Hockey managers: {e}", exc_info=True) + if not hasattr(self, "olympic_mens_live"): + self.olympic_mens_live = None + if not hasattr(self, "olympic_mens_recent"): + self.olympic_mens_recent = None + if not hasattr(self, "olympic_mens_upcoming"): + self.olympic_mens_upcoming = None + + # Initialize Olympic Women's managers if enabled + if self.olympic_womens_enabled: + try: + self.olympic_womens_live = OlympicWomensHockeyLiveManager( + olympic_womens_config, self.display_manager, self.cache_manager + ) + self.olympic_womens_recent = OlympicWomensHockeyRecentManager( + olympic_womens_config, self.display_manager, self.cache_manager + ) + self.olympic_womens_upcoming = OlympicWomensHockeyUpcomingManager( + olympic_womens_config, self.display_manager, self.cache_manager + ) + self.logger.info("Olympic Women's Hockey managers initialized") + except Exception as e: + self.logger.error(f"Failed to initialize Olympic Women's Hockey managers: {e}", exc_info=True) + if not hasattr(self, "olympic_womens_live"): + self.olympic_womens_live = None + if not hasattr(self, "olympic_womens_recent"): + self.olympic_womens_recent = None + if not hasattr(self, "olympic_womens_upcoming"): + self.olympic_womens_upcoming = None + except Exception as e: self.logger.error(f"Error initializing managers: {e}", exc_info=True) @@ -381,7 +446,31 @@ def _initialize_league_registry(self) -> None: 'upcoming': getattr(self, 'ncaa_womens_upcoming', None), } } - + + # Olympic Men's Hockey league entry - fourth priority (4) + self._league_registry['olympic_mens'] = { + 'enabled': self.olympic_mens_enabled, + 'priority': 4, # Fourth priority - shows after NCAA Women's + 'live_priority': self.olympic_mens_live_priority, + 'managers': { + 'live': getattr(self, 'olympic_mens_live', None), + 'recent': getattr(self, 'olympic_mens_recent', None), + 'upcoming': getattr(self, 'olympic_mens_upcoming', None), + } + } + + # Olympic Women's Hockey league entry - fifth priority (5) + self._league_registry['olympic_womens'] = { + 'enabled': self.olympic_womens_enabled, + 'priority': 5, # Fifth priority - shows after Olympic Men's + 'live_priority': self.olympic_womens_live_priority, + 'managers': { + 'live': getattr(self, 'olympic_womens_live', None), + 'recent': getattr(self, 'olympic_womens_recent', None), + 'upcoming': getattr(self, 'olympic_womens_upcoming', None), + } + } + # Log registry state for debugging enabled_leagues = [lid for lid, data in self._league_registry.items() if data['enabled']] self.logger.info( @@ -566,6 +655,8 @@ def _get_default_logo_dir(self, league: str) -> str: 'nhl': 'assets/sports/nhl_logos', 'ncaa_mens': 'assets/sports/ncaa_logos', # NCAA Men's Hockey uses ncaa_logos 'ncaa_womens': 'assets/sports/ncaa_logos', # NCAA Women's Hockey uses ncaa_logos + 'olympic_mens': 'assets/sports/olympic_logos', # Olympic Hockey uses country flags + 'olympic_womens': 'assets/sports/olympic_logos', # Olympic Hockey uses country flags } # Default to league-specific directory if not in map return logo_dir_map.get(league, f"assets/sports/{league}_logos") @@ -580,7 +671,7 @@ def _parse_display_mode_settings(self) -> Dict[str, Dict[str, str]]: """ settings = {} - for league in ['nhl', 'ncaa_mens', 'ncaa_womens']: + for league in ['nhl', 'ncaa_mens', 'ncaa_womens', 'olympic_mens', 'olympic_womens']: league_config = self.config.get(league, {}) display_modes_config = league_config.get("display_modes", {}) @@ -713,6 +804,8 @@ def _adapt_config_for_manager(self, league: str) -> Dict[str, Any]: "nhl": "nhl", "ncaa_mens": "ncaam_hockey", "ncaa_womens": "ncaaw_hockey", + "olympic_mens": "olympic_mens_hockey", + "olympic_womens": "olympic_womens_hockey", } sport_key = sport_key_map.get(league, league) @@ -933,6 +1026,28 @@ def _get_current_manager(self): elif mode_type == "upcoming": return self.ncaa_womens_upcoming + elif current_mode.startswith("olympic_mens_"): + if not self.olympic_mens_enabled: + return None + mode_type = current_mode.split("_", 2)[2] # "live", "recent", "upcoming" + if mode_type == "live": + return self.olympic_mens_live + elif mode_type == "recent": + return self.olympic_mens_recent + elif mode_type == "upcoming": + return self.olympic_mens_upcoming + + elif current_mode.startswith("olympic_womens_"): + if not self.olympic_womens_enabled: + return None + mode_type = current_mode.split("_", 2)[2] # "live", "recent", "upcoming" + if mode_type == "live": + return self.olympic_womens_live + elif mode_type == "recent": + return self.olympic_womens_recent + elif mode_type == "upcoming": + return self.olympic_womens_upcoming + return None def _ensure_manager_updated(self, manager) -> None: @@ -991,6 +1106,28 @@ def update(self) -> None: if manager: manager.update() + # Update Olympic Men's managers if enabled + if self.olympic_mens_enabled: + for attr in ( + "olympic_mens_live", + "olympic_mens_recent", + "olympic_mens_upcoming", + ): + manager = getattr(self, attr, None) + if manager: + manager.update() + + # Update Olympic Women's managers if enabled + if self.olympic_womens_enabled: + for attr in ( + "olympic_womens_live", + "olympic_womens_recent", + "olympic_womens_upcoming", + ): + manager = getattr(self, attr, None) + if manager: + manager.update() + except Exception as e: self.logger.error(f"Error updating managers: {e}", exc_info=True) @@ -1168,10 +1305,20 @@ def _set_display_context_from_manager(self, manager, mode_type: str) -> None: getattr(self, 'ncaa_mens_upcoming', None)): self._current_display_league = 'ncaa_mens' # Check NCAA Women's managers - elif manager in (getattr(self, 'ncaa_womens_live', None), - getattr(self, 'ncaa_womens_recent', None), + elif manager in (getattr(self, 'ncaa_womens_live', None), + getattr(self, 'ncaa_womens_recent', None), getattr(self, 'ncaa_womens_upcoming', None)): self._current_display_league = 'ncaa_womens' + # Check Olympic Men's managers + elif manager in (getattr(self, 'olympic_mens_live', None), + getattr(self, 'olympic_mens_recent', None), + getattr(self, 'olympic_mens_upcoming', None)): + self._current_display_league = 'olympic_mens' + # Check Olympic Women's managers + elif manager in (getattr(self, 'olympic_womens_live', None), + getattr(self, 'olympic_womens_recent', None), + getattr(self, 'olympic_womens_upcoming', None)): + self._current_display_league = 'olympic_womens' @staticmethod def _build_manager_key(mode_name: str, manager) -> str: @@ -1247,9 +1394,11 @@ def _get_rankings_cache(self) -> Dict[str, int]: rankings = {} # Try to get rankings from each manager - for manager_attr in ['nhl_live', 'nhl_recent', 'nhl_upcoming', + for manager_attr in ['nhl_live', 'nhl_recent', 'nhl_upcoming', 'ncaa_mens_live', 'ncaa_mens_recent', 'ncaa_mens_upcoming', - 'ncaa_womens_live', 'ncaa_womens_recent', 'ncaa_womens_upcoming']: + 'ncaa_womens_live', 'ncaa_womens_recent', 'ncaa_womens_upcoming', + 'olympic_mens_live', 'olympic_mens_recent', 'olympic_mens_upcoming', + 'olympic_womens_live', 'olympic_womens_recent', 'olympic_womens_upcoming']: manager = getattr(self, manager_attr, None) if manager: manager_rankings = getattr(manager, '_team_rankings_cache', {}) @@ -1819,7 +1968,7 @@ def _evaluate_dynamic_cycle_completion(self, display_mode: str = None) -> None: if manager_key in self._single_game_manager_start_times: start_time = self._single_game_manager_start_times[manager_key] # Extract league and mode_type from mode_name - league = 'nhl' if mode_name.startswith('nhl_') else ('ncaa_mens' if mode_name.startswith('ncaa_mens_') else ('ncaa_womens' if mode_name.startswith('ncaa_womens_') else None)) + league = 'nhl' if mode_name.startswith('nhl_') else ('ncaa_mens' if mode_name.startswith('ncaa_mens_') else ('ncaa_womens' if mode_name.startswith('ncaa_womens_') else ('olympic_mens' if mode_name.startswith('olympic_mens_') else ('olympic_womens' if mode_name.startswith('olympic_womens_') else None)))) mode_type_str = mode_name.split('_')[-1] if mode_name else None game_duration = self._get_game_duration(league, mode_type_str, manager) if league and mode_type_str else getattr(manager, 'game_display_duration', 15) current_time = time.time() @@ -1858,7 +2007,7 @@ def _evaluate_dynamic_cycle_completion(self, display_mode: str = None) -> None: if manager and manager.__class__.__name__ == manager_class_name: start_time = self._single_game_manager_start_times[manager_key] # Extract league and mode_type from mode_name - league = 'nhl' if mode_name.startswith('nhl_') else ('ncaa_mens' if mode_name.startswith('ncaa_mens_') else ('ncaa_womens' if mode_name.startswith('ncaa_womens_') else None)) + league = 'nhl' if mode_name.startswith('nhl_') else ('ncaa_mens' if mode_name.startswith('ncaa_mens_') else ('ncaa_womens' if mode_name.startswith('ncaa_womens_') else ('olympic_mens' if mode_name.startswith('olympic_mens_') else ('olympic_womens' if mode_name.startswith('olympic_womens_') else None)))) mode_type_str = mode_name.split('_')[-1] if mode_name else None game_duration = self._get_game_duration(league, mode_type_str, manager) if league and mode_type_str else getattr(manager, 'game_display_duration', 15) elapsed = time.time() - start_time @@ -2008,6 +2157,8 @@ def has_live_priority(self) -> bool: self.nhl_enabled and self.nhl_live_priority, self.ncaa_mens_enabled and self.ncaa_mens_live_priority, self.ncaa_womens_enabled and self.ncaa_womens_live_priority, + self.olympic_mens_enabled and self.olympic_mens_live_priority, + self.olympic_womens_enabled and self.olympic_womens_live_priority, ] ) @@ -2102,22 +2253,68 @@ def has_live_content(self) -> bool: # No favorite teams configured, return True if any live games exist ncaa_womens_live = True - result = nhl_live or ncaa_mens_live or ncaa_womens_live - + # Check Olympic Men's live content + olympic_mens_live = False + if ( + self.olympic_mens_enabled + and self.olympic_mens_live_priority + and hasattr(self, "olympic_mens_live") + ): + live_games = getattr(self.olympic_mens_live, "live_games", []) + if live_games: + live_games = [g for g in live_games if not g.get("is_final", False)] + if hasattr(self.olympic_mens_live, "_is_game_really_over"): + live_games = [g for g in live_games if not self.olympic_mens_live._is_game_really_over(g)] + + if live_games: + favorite_teams = getattr(self.olympic_mens_live, "favorite_teams", []) + if favorite_teams: + olympic_mens_live = any( + game.get("home_abbr") in favorite_teams + or game.get("away_abbr") in favorite_teams + for game in live_games + ) + else: + olympic_mens_live = True + + # Check Olympic Women's live content + olympic_womens_live = False + if ( + self.olympic_womens_enabled + and self.olympic_womens_live_priority + and hasattr(self, "olympic_womens_live") + ): + live_games = getattr(self.olympic_womens_live, "live_games", []) + if live_games: + live_games = [g for g in live_games if not g.get("is_final", False)] + if hasattr(self.olympic_womens_live, "_is_game_really_over"): + live_games = [g for g in live_games if not self.olympic_womens_live._is_game_really_over(g)] + + if live_games: + favorite_teams = getattr(self.olympic_womens_live, "favorite_teams", []) + if favorite_teams: + olympic_womens_live = any( + game.get("home_abbr") in favorite_teams + or game.get("away_abbr") in favorite_teams + for game in live_games + ) + else: + olympic_womens_live = True + + result = nhl_live or ncaa_mens_live or ncaa_womens_live or olympic_mens_live or olympic_womens_live + # Throttle logging when returning False to reduce log noise # Always log True immediately (important), but only log False every 60 seconds current_time = time.time() should_log = result or (current_time - self._last_live_content_false_log >= self._live_content_log_interval) - + if should_log: if result: - # Always log True results immediately - self.logger.info(f"has_live_content() returning {result}: nhl_live={nhl_live}, ncaa_mens_live={ncaa_mens_live}, ncaa_womens_live={ncaa_womens_live}") + self.logger.info(f"has_live_content() returning {result}: nhl_live={nhl_live}, ncaa_mens_live={ncaa_mens_live}, ncaa_womens_live={ncaa_womens_live}, olympic_mens_live={olympic_mens_live}, olympic_womens_live={olympic_womens_live}") else: - # Log False results only every 60 seconds - self.logger.info(f"has_live_content() returning {result}: nhl_live={nhl_live}, ncaa_mens_live={ncaa_mens_live}, ncaa_womens_live={ncaa_womens_live}") + self.logger.info(f"has_live_content() returning {result}: nhl_live={nhl_live}, ncaa_mens_live={ncaa_mens_live}, ncaa_womens_live={ncaa_womens_live}, olympic_mens_live={olympic_mens_live}, olympic_womens_live={olympic_womens_live}") self._last_live_content_false_log = current_time - + return result def get_live_modes(self) -> list: @@ -2218,7 +2415,55 @@ def get_live_modes(self) -> list: else: # No favorite teams configured, include if any live games exist live_modes.append("ncaa_womens_live") - + + # Check Olympic Men's live content + if ( + self.olympic_mens_enabled + and self.olympic_mens_live_priority + and hasattr(self, "olympic_mens_live") + ): + live_games = getattr(self.olympic_mens_live, "live_games", []) + if live_games: + live_games = [g for g in live_games if not g.get("is_final", False)] + if hasattr(self.olympic_mens_live, "_is_game_really_over"): + live_games = [g for g in live_games if not self.olympic_mens_live._is_game_really_over(g)] + + if live_games: + favorite_teams = getattr(self.olympic_mens_live, "favorite_teams", []) + if favorite_teams: + if any( + game.get("home_abbr") in favorite_teams + or game.get("away_abbr") in favorite_teams + for game in live_games + ): + live_modes.append("olympic_mens_live") + else: + live_modes.append("olympic_mens_live") + + # Check Olympic Women's live content + if ( + self.olympic_womens_enabled + and self.olympic_womens_live_priority + and hasattr(self, "olympic_womens_live") + ): + live_games = getattr(self.olympic_womens_live, "live_games", []) + if live_games: + live_games = [g for g in live_games if not g.get("is_final", False)] + if hasattr(self.olympic_womens_live, "_is_game_really_over"): + live_games = [g for g in live_games if not self.olympic_womens_live._is_game_really_over(g)] + + if live_games: + favorite_teams = getattr(self.olympic_womens_live, "favorite_teams", []) + if favorite_teams: + if any( + game.get("home_abbr") in favorite_teams + or game.get("away_abbr") in favorite_teams + for game in live_games + ): + live_modes.append("olympic_womens_live") + else: + live_modes.append("olympic_womens_live") + return live_modes def _should_use_scroll_mode(self, league: str, mode_type: str) -> bool: @@ -2257,9 +2502,11 @@ def _get_rankings_cache(self) -> Dict[str, int]: rankings = {} # Try to get rankings from each manager - for manager_attr in ['nhl_live', 'nhl_recent', 'nhl_upcoming', + for manager_attr in ['nhl_live', 'nhl_recent', 'nhl_upcoming', 'ncaa_mens_live', 'ncaa_mens_recent', 'ncaa_mens_upcoming', - 'ncaa_womens_live', 'ncaa_womens_recent', 'ncaa_womens_upcoming']: + 'ncaa_womens_live', 'ncaa_womens_recent', 'ncaa_womens_upcoming', + 'olympic_mens_live', 'olympic_mens_recent', 'olympic_mens_upcoming', + 'olympic_womens_live', 'olympic_womens_recent', 'olympic_womens_upcoming']: manager = getattr(self, manager_attr, None) if manager: manager_rankings = getattr(manager, '_team_rankings_cache', {}) @@ -2722,7 +2969,7 @@ def validate_config(self) -> bool: """Validate plugin configuration.""" try: # Check that at least one league is enabled - if not (self.nhl_enabled or self.ncaa_mens_enabled or self.ncaa_womens_enabled): + if not (self.nhl_enabled or self.ncaa_mens_enabled or self.ncaa_womens_enabled or self.olympic_mens_enabled or self.olympic_womens_enabled): self.logger.warning("No leagues enabled in hockey scoreboard plugin") return False @@ -2824,45 +3071,11 @@ def get_cycle_duration(self, display_mode: str = None) -> Optional[float]: managers_to_check.append((league, manager)) else: # Combined mode - check all enabled leagues - if mode_type == 'live': - if self.nhl_enabled: - nhl_manager = self._get_league_manager_for_mode('nhl', 'live') - if nhl_manager: - managers_to_check.append(('nhl', nhl_manager)) - if self.ncaa_mens_enabled: - ncaa_mens_manager = self._get_league_manager_for_mode('ncaa_mens', 'live') - if ncaa_mens_manager: - managers_to_check.append(('ncaa_mens', ncaa_mens_manager)) - if self.ncaa_womens_enabled: - ncaa_womens_manager = self._get_league_manager_for_mode('ncaa_womens', 'live') - if ncaa_womens_manager: - managers_to_check.append(('ncaa_womens', ncaa_womens_manager)) - elif mode_type == 'recent': - if self.nhl_enabled: - nhl_manager = self._get_league_manager_for_mode('nhl', 'recent') - if nhl_manager: - managers_to_check.append(('nhl', nhl_manager)) - if self.ncaa_mens_enabled: - ncaa_mens_manager = self._get_league_manager_for_mode('ncaa_mens', 'recent') - if ncaa_mens_manager: - managers_to_check.append(('ncaa_mens', ncaa_mens_manager)) - if self.ncaa_womens_enabled: - ncaa_womens_manager = self._get_league_manager_for_mode('ncaa_womens', 'recent') - if ncaa_womens_manager: - managers_to_check.append(('ncaa_womens', ncaa_womens_manager)) - elif mode_type == 'upcoming': - if self.nhl_enabled: - nhl_manager = self._get_league_manager_for_mode('nhl', 'upcoming') - if nhl_manager: - managers_to_check.append(('nhl', nhl_manager)) - if self.ncaa_mens_enabled: - ncaa_mens_manager = self._get_league_manager_for_mode('ncaa_mens', 'upcoming') - if ncaa_mens_manager: - managers_to_check.append(('ncaa_mens', ncaa_mens_manager)) - if self.ncaa_womens_enabled: - ncaa_womens_manager = self._get_league_manager_for_mode('ncaa_womens', 'upcoming') - if ncaa_womens_manager: - managers_to_check.append(('ncaa_womens', ncaa_womens_manager)) + # Use league registry to get all enabled leagues for this mode type + for league_id in self._get_enabled_leagues_for_mode(mode_type): + league_manager = self._get_league_manager_for_mode(league_id, mode_type) + if league_manager: + managers_to_check.append((league_id, league_manager)) # CRITICAL: Update managers BEFORE checking game counts! self.logger.info(f"get_cycle_duration: updating {len(managers_to_check)} manager(s) before counting games") @@ -2950,6 +3163,8 @@ def get_info(self) -> Dict[str, Any]: "nhl_enabled": self.nhl_enabled, "ncaa_mens_enabled": self.ncaa_mens_enabled, "ncaa_womens_enabled": self.ncaa_womens_enabled, + "olympic_mens_enabled": self.olympic_mens_enabled, + "olympic_womens_enabled": self.olympic_womens_enabled, "current_mode": current_mode, "available_modes": self.modes, "display_duration": self.display_duration, @@ -2967,6 +3182,12 @@ def get_info(self) -> Dict[str, Any]: "ncaa_womens_live": hasattr(self, "ncaa_womens_live"), "ncaa_womens_recent": hasattr(self, "ncaa_womens_recent"), "ncaa_womens_upcoming": hasattr(self, "ncaa_womens_upcoming"), + "olympic_mens_live": hasattr(self, "olympic_mens_live"), + "olympic_mens_recent": hasattr(self, "olympic_mens_recent"), + "olympic_mens_upcoming": hasattr(self, "olympic_mens_upcoming"), + "olympic_womens_live": hasattr(self, "olympic_womens_live"), + "olympic_womens_recent": hasattr(self, "olympic_womens_recent"), + "olympic_womens_upcoming": hasattr(self, "olympic_womens_upcoming"), }, "live_priority": { "nhl": self.nhl_enabled and self.nhl_live_priority, @@ -2974,6 +3195,10 @@ def get_info(self) -> Dict[str, Any]: and self.ncaa_mens_live_priority, "ncaa_womens": self.ncaa_womens_enabled and self.ncaa_womens_live_priority, + "olympic_mens": self.olympic_mens_enabled + and self.olympic_mens_live_priority, + "olympic_womens": self.olympic_womens_enabled + and self.olympic_womens_live_priority, }, } diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index aa4a55b..6aa8bf6 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -1,10 +1,10 @@ { "id": "hockey-scoreboard", "name": "Hockey Scoreboard", - "version": "1.0.8", + "version": "1.1.0", "author": "ChuckBuilds", - "description": "Live, recent, and upcoming hockey games across NHL, NCAA Men's, and NCAA Women's hockey with real-time scores and schedules", - "homepage": "https://github.com/ChuckBuilds/ledmatrix-hockey-plugin", + "description": "Live, recent, and upcoming hockey games across NHL, NCAA Men's, NCAA Women's, and Olympic hockey with real-time scores and schedules", + "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hockey-scoreboard", "entry_point": "manager.py", "class_name": "HockeyScoreboardPlugin", "category": "sports", @@ -12,6 +12,7 @@ "hockey", "nhl", "ncaa", + "olympics", "sports", "scoreboard", "live-scores" @@ -30,7 +31,7 @@ }, "config_schema": "config_schema.json", "assets": { - "logos": "Uses shared LEDMatrix assets/sports/nhl_logos and assets/sports/ncaa_logos directories" + "logos": "Uses shared LEDMatrix assets/sports/nhl_logos, assets/sports/ncaa_logos, and assets/sports/olympic_logos directories" }, "update_interval": 60, "default_duration": 15, @@ -43,19 +44,29 @@ "ncaa_mens_live", "ncaa_womens_recent", "ncaa_womens_upcoming", - "ncaa_womens_live" + "ncaa_womens_live", + "olympic_mens_recent", + "olympic_mens_upcoming", + "olympic_mens_live", + "olympic_womens_recent", + "olympic_womens_upcoming", + "olympic_womens_live" ], "api_requirements": [ { "name": "ESPN API", "required": true, - "description": "ESPN public API for NHL and NCAA hockey scores and schedules", + "description": "ESPN public API for NHL, NCAA, and Olympic hockey scores and schedules", "url": "https://site.api.espn.com/apis/site/v2/sports/hockey/", "rate_limit": "No official rate limit, but please use responsibly" } ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-hockey-scoreboard/archive/refs/tags/v{version}.zip", "versions": [ + { + "version": "1.1.0", + "ledmatrix_min": "2.0.0", + "released": "2026-02-11" + }, { "version": "1.0.8", "ledmatrix_min": "2.0.0", @@ -97,9 +108,9 @@ "ledmatrix_min": "2.0.0" } ], - "last_updated": "2025-11-06", + "last_updated": "2026-02-11", "stars": 0, "downloads": 0, "verified": true, "screenshot": "" -} \ No newline at end of file +} diff --git a/plugins/hockey-scoreboard/olympic_mens_hockey_managers.py b/plugins/hockey-scoreboard/olympic_mens_hockey_managers.py new file mode 100644 index 0000000..c8ea3e9 --- /dev/null +++ b/plugins/hockey-scoreboard/olympic_mens_hockey_managers.py @@ -0,0 +1,202 @@ +import logging +from datetime import datetime +from pathlib import Path +from typing import Any, ClassVar, Dict, Optional + +import pytz + +from hockey import Hockey, HockeyLive +from sports import SportsRecent, SportsUpcoming + +# Constants +ESPN_OLYMPIC_MENS_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/hockey/olympics-mens-ice-hockey/scoreboard" + + +class BaseOlympicMensHockeyManager(Hockey): + """Base class for Olympic Men's Ice Hockey managers with common functionality.""" + + # Class variables for warning tracking + _no_data_warning_logged: ClassVar[bool] = False + _last_warning_time: ClassVar[float] = 0 + _warning_cooldown: ClassVar[int] = 60 # Only log warnings once per minute + _shared_data: ClassVar[Optional[Dict]] = None + _last_shared_update: ClassVar[float] = 0 + _processed_games_cache: ClassVar[Dict] = {} # Cache for processed game data + _processed_games_timestamp: ClassVar[float] = 0 + + def __init__( + self, + config: Dict[str, Any], + display_manager, + cache_manager, + ): + self.logger = logging.getLogger("OlympicMensHockey") + super().__init__( + config=config, + display_manager=display_manager, + cache_manager=cache_manager, + logger=self.logger, + sport_key="olympic_mens_hockey", + ) + + # Check display modes to determine what data to fetch + display_modes = self.mode_config.get("display_modes", {}) + self.recent_enabled = display_modes.get("hockey_recent", False) + self.upcoming_enabled = display_modes.get("hockey_upcoming", False) + self.live_enabled = display_modes.get("hockey_live", False) + self.league = "olympics-mens-ice-hockey" + + self.logger.info( + f"Initialized OlympicMensHockey manager with display dimensions: {self.display_width}x{self.display_height}" + ) + self.logger.info(f"Logo directory: {self.logo_dir}") + self.logger.info( + f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}" + ) + + def _fetch_olympic_hockey_api_data(self, use_cache: bool = True) -> Optional[Dict]: + """ + Fetches the Olympic Men's hockey schedule, caches it, and then filters + for relevant games based on the current configuration. + """ + now = datetime.now(pytz.utc) + year = now.year + # Olympic hockey window: early Feb to late Feb + datestring = f"{year}0201-{year}0301" + cache_key = f"olympic_mens_hockey_schedule_{year}" + + if use_cache: + cached_data = self.cache_manager.get(cache_key) + if cached_data: + if isinstance(cached_data, dict) and "events" in cached_data: + self.logger.info(f"Using cached Olympic Men's schedule for {year}") + return cached_data + elif isinstance(cached_data, list): + self.logger.info( + f"Using cached Olympic Men's schedule for {year} (legacy format)" + ) + return {"events": cached_data} + else: + self.logger.warning( + f"Invalid cached data format for {year}: {type(cached_data)}" + ) + self.cache_manager.clear_cache(cache_key) + + self.logger.info(f"Fetching Olympic Men's {year} schedule from ESPN API...") + + def fetch_callback(result): + """Callback when background fetch completes.""" + if result.success: + self.logger.info( + f"Background fetch completed for Olympic Men's {year}: {len(result.data.get('events', []))} events" + ) + else: + self.logger.error( + f"Background fetch failed for Olympic Men's {year}: {result.error}" + ) + if year in self.background_fetch_requests: + del self.background_fetch_requests[year] + + background_config = self.mode_config.get("background_service", {}) + timeout = background_config.get("request_timeout", 30) + max_retries = background_config.get("max_retries", 3) + priority = background_config.get("priority", 2) + + request_id = self.background_service.submit_fetch_request( + sport="olympic_mens_hockey", + year=year, + url=ESPN_OLYMPIC_MENS_SCOREBOARD_URL, + cache_key=cache_key, + params={"dates": datestring, "limit": 1000}, + headers=self.headers, + timeout=timeout, + max_retries=max_retries, + priority=priority, + callback=fetch_callback, + ) + + self.background_fetch_requests[year] = request_id + + partial_data = self._get_weeks_data() + if partial_data: + return partial_data + return None + + def _fetch_data(self) -> Optional[Dict]: + """Fetch data using shared data mechanism or direct fetch for live.""" + if isinstance(self, OlympicMensHockeyLiveManager): + return self._fetch_todays_games() + else: + return self._fetch_olympic_hockey_api_data(use_cache=True) + + +class OlympicMensHockeyLiveManager(BaseOlympicMensHockeyManager, HockeyLive): + """Manager for live Olympic Men's Ice Hockey games.""" + + def __init__( + self, + config: Dict[str, Any], + display_manager, + cache_manager, + ): + super().__init__(config, display_manager, cache_manager) + self.logger = logging.getLogger("OlympicMensHockeyLiveManager") + + if self.test_mode: + self.current_game = { + "id": "401845663", + "home_abbr": "USA", + "away_abbr": "CAN", + "home_score": "3", + "away_score": "2", + "period": 2, + "period_text": "P2", + "home_id": "1", + "away_id": "16", + "clock": "12:34", + "home_logo_path": Path(self.logo_dir, "USA.png"), + "away_logo_path": Path(self.logo_dir, "CAN.png"), + "game_time": "7:30 PM", + "game_date": "Feb 18", + "is_live": True, + "is_final": False, + "is_upcoming": False, + } + self.live_games = [self.current_game] + self.logger.info( + "Initialized OlympicMensHockeyLiveManager with test game: USA vs CAN" + ) + else: + self.logger.info("Initialized OlympicMensHockeyLiveManager in live mode") + + +class OlympicMensHockeyRecentManager(BaseOlympicMensHockeyManager, SportsRecent): + """Manager for recently completed Olympic Men's Ice Hockey games.""" + + def __init__( + self, + config: Dict[str, Any], + display_manager, + cache_manager, + ): + super().__init__(config, display_manager, cache_manager) + self.logger = logging.getLogger("OlympicMensHockeyRecentManager") + self.logger.info( + f"Initialized OlympicMensHockeyRecentManager with {len(self.favorite_teams)} favorite teams" + ) + + +class OlympicMensHockeyUpcomingManager(BaseOlympicMensHockeyManager, SportsUpcoming): + """Manager for upcoming Olympic Men's Ice Hockey games.""" + + def __init__( + self, + config: Dict[str, Any], + display_manager, + cache_manager, + ): + super().__init__(config, display_manager, cache_manager) + self.logger = logging.getLogger("OlympicMensHockeyUpcomingManager") + self.logger.info( + f"Initialized OlympicMensHockeyUpcomingManager with {len(self.favorite_teams)} favorite teams" + ) diff --git a/plugins/hockey-scoreboard/olympic_womens_hockey_managers.py b/plugins/hockey-scoreboard/olympic_womens_hockey_managers.py new file mode 100644 index 0000000..817dcc3 --- /dev/null +++ b/plugins/hockey-scoreboard/olympic_womens_hockey_managers.py @@ -0,0 +1,202 @@ +import logging +from datetime import datetime +from pathlib import Path +from typing import Any, ClassVar, Dict, Optional + +import pytz + +from hockey import Hockey, HockeyLive +from sports import SportsRecent, SportsUpcoming + +# Constants +ESPN_OLYMPIC_WOMENS_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/hockey/olympics-womens-ice-hockey/scoreboard" + + +class BaseOlympicWomensHockeyManager(Hockey): + """Base class for Olympic Women's Ice Hockey managers with common functionality.""" + + # Class variables for warning tracking + _no_data_warning_logged: ClassVar[bool] = False + _last_warning_time: ClassVar[float] = 0 + _warning_cooldown: ClassVar[int] = 60 # Only log warnings once per minute + _shared_data: ClassVar[Optional[Dict]] = None + _last_shared_update: ClassVar[float] = 0 + _processed_games_cache: ClassVar[Dict] = {} # Cache for processed game data + _processed_games_timestamp: ClassVar[float] = 0 + + def __init__( + self, + config: Dict[str, Any], + display_manager, + cache_manager, + ): + self.logger = logging.getLogger("OlympicWomensHockey") + super().__init__( + config=config, + display_manager=display_manager, + cache_manager=cache_manager, + logger=self.logger, + sport_key="olympic_womens_hockey", + ) + + # Check display modes to determine what data to fetch + display_modes = self.mode_config.get("display_modes", {}) + self.recent_enabled = display_modes.get("hockey_recent", False) + self.upcoming_enabled = display_modes.get("hockey_upcoming", False) + self.live_enabled = display_modes.get("hockey_live", False) + self.league = "olympics-womens-ice-hockey" + + self.logger.info( + f"Initialized OlympicWomensHockey manager with display dimensions: {self.display_width}x{self.display_height}" + ) + self.logger.info(f"Logo directory: {self.logo_dir}") + self.logger.info( + f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}" + ) + + def _fetch_olympic_hockey_api_data(self, use_cache: bool = True) -> Optional[Dict]: + """ + Fetches the Olympic Women's hockey schedule, caches it, and then filters + for relevant games based on the current configuration. + """ + now = datetime.now(pytz.utc) + year = now.year + # Olympic hockey window: early Feb to late Feb + datestring = f"{year}0201-{year}0301" + cache_key = f"olympic_womens_hockey_schedule_{year}" + + if use_cache: + cached_data = self.cache_manager.get(cache_key) + if cached_data: + if isinstance(cached_data, dict) and "events" in cached_data: + self.logger.info(f"Using cached Olympic Women's schedule for {year}") + return cached_data + elif isinstance(cached_data, list): + self.logger.info( + f"Using cached Olympic Women's schedule for {year} (legacy format)" + ) + return {"events": cached_data} + else: + self.logger.warning( + f"Invalid cached data format for {year}: {type(cached_data)}" + ) + self.cache_manager.clear_cache(cache_key) + + self.logger.info(f"Fetching Olympic Women's {year} schedule from ESPN API...") + + def fetch_callback(result): + """Callback when background fetch completes.""" + if result.success: + self.logger.info( + f"Background fetch completed for Olympic Women's {year}: {len(result.data.get('events', []))} events" + ) + else: + self.logger.error( + f"Background fetch failed for Olympic Women's {year}: {result.error}" + ) + if year in self.background_fetch_requests: + del self.background_fetch_requests[year] + + background_config = self.mode_config.get("background_service", {}) + timeout = background_config.get("request_timeout", 30) + max_retries = background_config.get("max_retries", 3) + priority = background_config.get("priority", 2) + + request_id = self.background_service.submit_fetch_request( + sport="olympic_womens_hockey", + year=year, + url=ESPN_OLYMPIC_WOMENS_SCOREBOARD_URL, + cache_key=cache_key, + params={"dates": datestring, "limit": 1000}, + headers=self.headers, + timeout=timeout, + max_retries=max_retries, + priority=priority, + callback=fetch_callback, + ) + + self.background_fetch_requests[year] = request_id + + partial_data = self._get_weeks_data() + if partial_data: + return partial_data + return None + + def _fetch_data(self) -> Optional[Dict]: + """Fetch data using shared data mechanism or direct fetch for live.""" + if isinstance(self, OlympicWomensHockeyLiveManager): + return self._fetch_todays_games() + else: + return self._fetch_olympic_hockey_api_data(use_cache=True) + + +class OlympicWomensHockeyLiveManager(BaseOlympicWomensHockeyManager, HockeyLive): + """Manager for live Olympic Women's Ice Hockey games.""" + + def __init__( + self, + config: Dict[str, Any], + display_manager, + cache_manager, + ): + super().__init__(config, display_manager, cache_manager) + self.logger = logging.getLogger("OlympicWomensHockeyLiveManager") + + if self.test_mode: + self.current_game = { + "id": "401845610", + "home_abbr": "USA", + "away_abbr": "CAN", + "home_score": "3", + "away_score": "2", + "period": 2, + "period_text": "P2", + "home_id": "1", + "away_id": "16", + "clock": "12:34", + "home_logo_path": Path(self.logo_dir, "USA.png"), + "away_logo_path": Path(self.logo_dir, "CAN.png"), + "game_time": "7:30 PM", + "game_date": "Feb 17", + "is_live": True, + "is_final": False, + "is_upcoming": False, + } + self.live_games = [self.current_game] + self.logger.info( + "Initialized OlympicWomensHockeyLiveManager with test game: USA vs CAN" + ) + else: + self.logger.info("Initialized OlympicWomensHockeyLiveManager in live mode") + + +class OlympicWomensHockeyRecentManager(BaseOlympicWomensHockeyManager, SportsRecent): + """Manager for recently completed Olympic Women's Ice Hockey games.""" + + def __init__( + self, + config: Dict[str, Any], + display_manager, + cache_manager, + ): + super().__init__(config, display_manager, cache_manager) + self.logger = logging.getLogger("OlympicWomensHockeyRecentManager") + self.logger.info( + f"Initialized OlympicWomensHockeyRecentManager with {len(self.favorite_teams)} favorite teams" + ) + + +class OlympicWomensHockeyUpcomingManager(BaseOlympicWomensHockeyManager, SportsUpcoming): + """Manager for upcoming Olympic Women's Ice Hockey games.""" + + def __init__( + self, + config: Dict[str, Any], + display_manager, + cache_manager, + ): + super().__init__(config, display_manager, cache_manager) + self.logger = logging.getLogger("OlympicWomensHockeyUpcomingManager") + self.logger.info( + f"Initialized OlympicWomensHockeyUpcomingManager with {len(self.favorite_teams)} favorite teams" + ) diff --git a/plugins/ledmatrix-flights/README.md b/plugins/ledmatrix-flights/README.md index db3e894..06229cf 100644 --- a/plugins/ledmatrix-flights/README.md +++ b/plugins/ledmatrix-flights/README.md @@ -32,24 +32,22 @@ Real-time aircraft tracking plugin for LEDMatrix with ADS-B data, map background ## Installation -### From GitHub (Recommended) +### From Plugin Store (Recommended) -```bash -cd /path/to/LEDMatrix -git submodule add https://github.com/ChuckBuilds/ledmatrix-flights.git plugins/ledmatrix-flights -``` +1. Open the LEDMatrix web interface (`http://your-pi-ip:5000`) +2. Go to **Plugin Store** +3. Find **Flight Tracker** and click **Install** ### Manual Installation -1. Clone this repository into the `plugins/` directory: +1. Copy the plugin from the monorepo: ```bash -cd /path/to/LEDMatrix/plugins -git clone https://github.com/ChuckBuilds/ledmatrix-flights.git +cp -r ledmatrix-plugins/plugins/ledmatrix-flights /path/to/LEDMatrix/plugin-repos/ ``` 2. Install dependencies: ```bash -pip install -r plugins/ledmatrix-flights/requirements.txt +pip install -r plugin-repos/ledmatrix-flights/requirements.txt ``` ## Configuration diff --git a/plugins/ledmatrix-flights/manifest.json b/plugins/ledmatrix-flights/manifest.json index 8cd91ab..618982f 100644 --- a/plugins/ledmatrix-flights/manifest.json +++ b/plugins/ledmatrix-flights/manifest.json @@ -7,7 +7,9 @@ "entry_point": "manager.py", "class_name": "FlightTrackerPlugin", "api_version": "1.0.0", - "display_modes": ["flight_tracker"], + "display_modes": [ + "flight_tracker" + ], "update_interval": 5, "dependencies": [ "requests", @@ -15,13 +17,21 @@ ], "config_schema": "config_schema.json", "requirements_file": "requirements.txt", - "tags": ["flight", "aircraft", "ads-b", "skyaware", "aviation", "map"], - "homepage": "https://github.com/ChuckBuilds/ledmatrix-flights", - "website": "https://github.com/ChuckBuilds/ledmatrix-flights", + "tags": [ + "flight", + "aircraft", + "ads-b", + "skyaware", + "aviation", + "map" + ], + "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/ledmatrix-flights", + "website": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/ledmatrix-flights", "license": "MIT", - "compatible_versions": [">=2.0.0"], + "compatible_versions": [ + ">=2.0.0" + ], "ledmatrix_version": "2.0.0", "min_ledmatrix_version": "2.0.0", "max_ledmatrix_version": "3.0.0" } - diff --git a/plugins/ledmatrix-music/manifest.json b/plugins/ledmatrix-music/manifest.json index 6b710a1..7965f46 100644 --- a/plugins/ledmatrix-music/manifest.json +++ b/plugins/ledmatrix-music/manifest.json @@ -7,7 +7,9 @@ "entry_point": "manager.py", "class_name": "MusicPlugin", "api_version": "1.0.0", - "display_modes": ["now_playing"], + "display_modes": [ + "now_playing" + ], "update_interval": 2, "dependencies": [ "spotipy", @@ -17,15 +19,22 @@ ], "config_schema": "config_schema.json", "requirements_file": "requirements.txt", - "tags": ["music", "spotify", "youtube-music", "now-playing", "album-art"], - "homepage": "https://github.com/ChuckBuilds/ledmatrix-music", - "website": "https://github.com/ChuckBuilds/ledmatrix-music", + "tags": [ + "music", + "spotify", + "youtube-music", + "now-playing", + "album-art" + ], + "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/ledmatrix-music", + "website": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/ledmatrix-music", "license": "MIT", - "compatible_versions": [">=2.0.0"], + "compatible_versions": [ + ">=2.0.0" + ], "ledmatrix_version": "2.0.0", "min_ledmatrix_version": "2.0.0", "max_ledmatrix_version": "3.0.0", - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-music/archive/refs/tags/v{version}.zip", "web_ui_actions": [ { "id": "authenticate-spotify", diff --git a/plugins/ledmatrix-stocks/manifest.json b/plugins/ledmatrix-stocks/manifest.json index 8e4054a..1c5aa65 100644 --- a/plugins/ledmatrix-stocks/manifest.json +++ b/plugins/ledmatrix-stocks/manifest.json @@ -7,7 +7,9 @@ "entry_point": "manager.py", "class_name": "StockTickerPlugin", "api_version": "1.0.0", - "display_modes": ["stocks"], + "display_modes": [ + "stocks" + ], "update_interval": 600, "dependencies": [ "requests", @@ -17,8 +19,14 @@ ], "config_schema": "config_schema.json", "requirements_file": "requirements.txt", - "tags": ["stocks", "crypto", "finance", "ticker", "scrolling"], - "website": "https://github.com/ChuckBuilds/ledmatrix-stocks", + "tags": [ + "stocks", + "crypto", + "finance", + "ticker", + "scrolling" + ], + "website": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/ledmatrix-stocks", "license": "MIT", "min_ledmatrix_version": "2.0.0", "max_ledmatrix_version": "3.0.0" diff --git a/plugins/ledmatrix-weather/manifest.json b/plugins/ledmatrix-weather/manifest.json index 8b5f17c..e5ddd1a 100644 --- a/plugins/ledmatrix-weather/manifest.json +++ b/plugins/ledmatrix-weather/manifest.json @@ -22,7 +22,6 @@ "hourly_forecast", "daily_forecast" ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-weather/archive/refs/tags/v{version}.zip", "versions": [ { "version": "2.0.9", @@ -50,4 +49,4 @@ "downloads": 0, "verified": true, "screenshot": "" -} \ No newline at end of file +} diff --git a/plugins/mqtt-notifications/manifest.json b/plugins/mqtt-notifications/manifest.json index 68a4c7a..0b81fa6 100644 --- a/plugins/mqtt-notifications/manifest.json +++ b/plugins/mqtt-notifications/manifest.json @@ -17,7 +17,6 @@ ], "entry_point": "manager.py", "class_name": "MQTTNotificationsPlugin", - "download_url_template": "", "versions": [ { "released": "2025-01-20", diff --git a/plugins/news/manifest.json b/plugins/news/manifest.json index fadc52f..f16a94f 100644 --- a/plugins/news/manifest.json +++ b/plugins/news/manifest.json @@ -1,7 +1,7 @@ { "id": "news", "name": "News Ticker", - "version": "1.0.1", + "version": "1.0.3", "description": "Displays scrolling news headlines from RSS feeds including sports news from ESPN, NCAA updates, and custom RSS sources", "author": "ChuckBuilds", "category": "content", @@ -14,15 +14,19 @@ "plugin_path": "plugins/news", "versions": [ { - "version": "1.0.1", + "version": "1.0.3", "ledmatrix_min": "2.0.0", - "released": "2025-10-12", - "download_url": "https://github.com/ChuckBuilds/ledmatrix-plugins/archive/refs/heads/main.zip" + "released": "2026-02-12" + }, + { + "version": "1.0.2", + "ledmatrix_min": "2.0.0", + "released": "2025-10-19" } ], "stars": 0, "downloads": 0, - "last_updated": "2025-10-12", + "last_updated": "2026-02-12", "verified": true, "screenshot": "", "display_modes": [ diff --git a/plugins/odds-ticker/manifest.json b/plugins/odds-ticker/manifest.json index 1f736d0..ad3a008 100644 --- a/plugins/odds-ticker/manifest.json +++ b/plugins/odds-ticker/manifest.json @@ -1,6 +1,7 @@ { "id": "odds-ticker", "name": "Odds Ticker", + "version": "1.0.3", "description": "Displays scrolling odds and betting lines for upcoming games across multiple sports leagues including NFL, NBA, MLB, NCAA Football, and more", "author": "ChuckBuilds", "category": "sports", @@ -10,15 +11,19 @@ "plugin_path": "plugins/odds-ticker", "versions": [ { - "version": "1.0.1", + "version": "1.0.3", "ledmatrix_min": "2.0.0", - "released": "2025-10-12", - "download_url": "https://github.com/ChuckBuilds/ledmatrix-plugins/archive/refs/heads/main.zip" + "released": "2026-02-12" + }, + { + "version": "1.0.2", + "ledmatrix_min": "2.0.0", + "released": "2025-10-19" } ], "stars": 0, "downloads": 0, - "last_updated": "2025-10-12", + "last_updated": "2026-02-12", "verified": true, "screenshot": "", "display_modes": [ diff --git a/plugins/of-the-day/manifest.json b/plugins/of-the-day/manifest.json index 44ab331..4e90795 100644 --- a/plugins/of-the-day/manifest.json +++ b/plugins/of-the-day/manifest.json @@ -20,7 +20,6 @@ "display_modes": [ "of_the_day" ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-of-the-day/archive/refs/tags/v{version}.zip", "versions": [ { "released": "2025-10-19", @@ -70,4 +69,4 @@ "script": "scripts/toggle_category.py" } ] -} \ No newline at end of file +} diff --git a/plugins/olympics/README.md b/plugins/olympics/README.md index 2f0de53..a1f5ad8 100644 --- a/plugins/olympics/README.md +++ b/plugins/olympics/README.md @@ -32,25 +32,20 @@ Screenshot Preview: ## Installation -### From GitHub (Recommended) +### From Plugin Store (Recommended) -```bash -./dev_plugin_setup.sh link-github olympics -``` +1. Open the LEDMatrix web interface (`http://your-pi-ip:5000`) +2. Go to **Plugin Store** +3. Find **Olympics Countdown** and click **Install** ### Manual Installation -1. Clone the repository: - ```bash - git clone https://github.com/ChuckBuilds/ledmatrix-olympics.git - ``` - -2. Link to LEDMatrix plugins directory: +1. Copy the plugin from the monorepo: ```bash - ./dev_plugin_setup.sh link olympics /path/to/ledmatrix-olympics + cp -r ledmatrix-plugins/plugins/olympics /path/to/LEDMatrix/plugin-repos/ ``` -3. Enable the plugin in `config/config.json`: +2. Enable the plugin in `config/config.json`: ```json { "olympics": { diff --git a/plugins/olympics/manifest.json b/plugins/olympics/manifest.json index 61eb2ea..9ba957e 100644 --- a/plugins/olympics/manifest.json +++ b/plugins/olympics/manifest.json @@ -22,7 +22,11 @@ ], "vegas_mode": { "supported": true, - "modes": ["scroll", "fixed", "static"], + "modes": [ + "scroll", + "fixed", + "static" + ], "default_mode": "scroll", "content_type": "multi" }, @@ -32,7 +36,6 @@ "sport_filters": true, "country_tracking": true }, - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-olympics-countdown/archive/refs/tags/v{version}.zip", "versions": [ { "released": "2026-02-08", diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index e9a8bcc..e821aa0 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -23,7 +23,6 @@ "soccer_recent", "soccer_upcoming" ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-soccer-scoreboard/archive/refs/tags/v{version}.zip", "versions": [ { "released": "2025-10-19", diff --git a/plugins/static-image/manifest.json b/plugins/static-image/manifest.json index 758fa0b..a9a6b75 100644 --- a/plugins/static-image/manifest.json +++ b/plugins/static-image/manifest.json @@ -17,7 +17,6 @@ "display_modes": [ "static_image" ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-static-image/archive/refs/tags/v{version}.zip", "versions": [ { "version": "1.0.2", @@ -35,4 +34,4 @@ "downloads": 0, "verified": true, "screenshot": "" -} \ No newline at end of file +} diff --git a/plugins/stock-news/manager.py b/plugins/stock-news/manager.py index 81c06dc..919706e 100644 --- a/plugins/stock-news/manager.py +++ b/plugins/stock-news/manager.py @@ -187,7 +187,10 @@ def update(self) -> None: def _fetch_stock_news(self, symbol: str) -> List[Dict]: """Fetch news for a specific stock symbol.""" cache_key = f"stock_news_{symbol}_{datetime.now().strftime('%Y%m%d%H')}" - update_interval = self.global_config.get('update_interval_seconds', 300) + try: + update_interval = int(self.global_config.get('update_interval_seconds', 300)) + except (ValueError, TypeError): + update_interval = 300 # Check cache first cached_data = self.cache_manager.get(cache_key) @@ -221,7 +224,10 @@ def _fetch_stock_news(self, symbol: str) -> List[Dict]: def _fetch_feed_headlines(self, feed_name: str, feed_url: str) -> List[Dict]: """Fetch headlines from a custom RSS feed.""" cache_key = f"stock_feed_{feed_name}_{datetime.now().strftime('%Y%m%d%H')}" - update_interval = self.global_config.get('update_interval_seconds', 300) + try: + update_interval = int(self.global_config.get('update_interval_seconds', 300)) + except (ValueError, TypeError): + update_interval = 300 # Check cache first cached_data = self.cache_manager.get(cache_key) diff --git a/plugins/stock-news/manifest.json b/plugins/stock-news/manifest.json index 17e9115..1c89c42 100644 --- a/plugins/stock-news/manifest.json +++ b/plugins/stock-news/manifest.json @@ -1,29 +1,38 @@ { "id": "stock-news", "name": "Stock News Ticker", - "description": "Displays scrolling stock-specific news headlines and financial updates from RSS feeds, focused on market news and company updates", + "version": "1.0.2", "author": "ChuckBuilds", + "description": "Displays scrolling stock-specific news headlines and financial updates from RSS feeds, focused on market news and company updates", "category": "financial", - "tags": ["stock", "news", "financial", "market", "ticker", "headlines", "rss", "scrolling"], + "tags": [ + "stock", + "news", + "financial", + "market", + "ticker", + "headlines", + "rss", + "scrolling" + ], + "display_modes": [ + "stock_news_ticker" + ], "repo": "https://github.com/ChuckBuilds/ledmatrix-plugins", "branch": "main", "plugin_path": "plugins/stock-news", "versions": [ { - "version": "1.0.1", - "ledmatrix_min": "2.0.0", - "released": "2025-10-12", - "download_url": "https://github.com/ChuckBuilds/ledmatrix-plugins/archive/refs/heads/main.zip" + "released": "2025-10-19", + "version": "1.0.2", + "ledmatrix_min": "2.0.0" } ], + "last_updated": "2025-10-19", "stars": 0, "downloads": 0, - "last_updated": "2025-10-12", "verified": true, "screenshot": "", - "display_modes": [ - "stock_news_ticker" - ], "dependencies": { "managers": [ "cache_manager" diff --git a/plugins/text-display/manifest.json b/plugins/text-display/manifest.json index b5fe5dc..0b4d15b 100644 --- a/plugins/text-display/manifest.json +++ b/plugins/text-display/manifest.json @@ -17,7 +17,6 @@ ], "entry_point": "manager.py", "class_name": "TextDisplayPlugin", - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-text-display/archive/refs/tags/v{version}.zip", "versions": [ { "released": "2025-10-19", @@ -30,4 +29,4 @@ "downloads": 0, "verified": true, "screenshot": "" -} \ No newline at end of file +} diff --git a/plugins/youtube-stats/manifest.json b/plugins/youtube-stats/manifest.json index 4b3ad44..2b854dc 100644 --- a/plugins/youtube-stats/manifest.json +++ b/plugins/youtube-stats/manifest.json @@ -31,6 +31,5 @@ "url": "https://developers.google.com/youtube/v3", "rate_limit": "10,000 units per day (default quota)" } - ], - "download_url_template": "https://github.com/ChuckBuilds/ledmatrix-youtube-stats/archive/refs/tags/v{version}.zip" + ] } diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 0000000..6634b61 --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,33 @@ +#!/bin/bash +# +# Pre-commit hook: auto-sync plugins.json from manifest versions. +# +# If any plugins/*/manifest.json was modified, runs update_registry.py +# and stages plugins.json so the registry stays in sync automatically. +# +# Install: cp scripts/pre-commit .git/hooks/pre-commit +# + +# Only run if a manifest was staged +staged_manifests=$(git diff --cached --name-only -- 'plugins/*/manifest.json') + +if [ -z "$staged_manifests" ]; then + exit 0 +fi + +echo "Pre-commit: manifest change detected, syncing plugins.json..." + +# Run the registry update script +python3 "$(git rev-parse --show-toplevel)/update_registry.py" +rc=$? + +if [ $rc -ne 0 ]; then + echo "Error: update_registry.py failed (exit code $rc)" + exit 1 +fi + +# Stage plugins.json if it was modified +if ! git diff --quiet -- plugins.json; then + git add plugins.json + echo "Pre-commit: plugins.json updated and staged." +fi